agents
Version:
A home for your AI agents
1,314 lines • 62 kB
JavaScript
import { i as runWithoutCurrentAgent, r as runInLifecycleHostContext } from "./current-agent-DhoDkSnH.js";
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js";
import { n as bindLifecycleCapability, r as lifecycleCapabilityId, t as LifecycleCapability } from "./capability-B4WbF81e.js";
import { t as _classPrivateMethodInitSpec } from "./classPrivateMethodInitSpec-qMjJ6sHQ.js";
import { n as publishDiagnosticsEvent } from "./diagnostics-BzvaX2UT.js";
import { isDurableObjectCodeUpdateReset, isDurableObjectMemoryLimitReset, isPlatformFailure, tryN } from "./retries.js";
import { SqlError } from "./sql-error.js";
import { AsyncLocalStorage } from "node:async_hooks";
import "cloudflare:workers";
import { nanoid } from "nanoid";
//#region src/lifecycle/capability-runner.ts
var _resolveCapabilities = /* @__PURE__ */ new WeakMap();
var _capabilities$1 = /* @__PURE__ */ new WeakMap();
var _startPromise = /* @__PURE__ */ new WeakMap();
var _started = /* @__PURE__ */ new WeakMap();
var _CapabilityRunner_brand = /* @__PURE__ */ new WeakSet();
/**
* Runs ordered lifecycle phases for capabilities installed in a Durable Object.
*
* Capabilities are resolved lazily on the first phase and retained for the
* lifetime of this runner. Startup runs in declaration order.
* Requests dispatch as a middleware chain: the first capability to return a
* response handles the request.
*/
var CapabilityRunner = class {
/**
* Create a lifecycle whose capabilities are resolved immediately before the
* first phase.
*
* @param resolveCapabilities - Returns capabilities in their startup order.
*/
constructor(resolveCapabilities) {
_classPrivateMethodInitSpec(this, _CapabilityRunner_brand);
_classPrivateFieldInitSpec(this, _resolveCapabilities, void 0);
_classPrivateFieldInitSpec(this, _capabilities$1, void 0);
_classPrivateFieldInitSpec(this, _startPromise, void 0);
_classPrivateFieldInitSpec(this, _started, false);
_classPrivateFieldSet2(_resolveCapabilities, this, resolveCapabilities);
}
/**
* Start every capability sequentially.
*
* Concurrent callers share one startup attempt. A failed attempt is not
* cached, allowing the host to retry its complete startup phase.
*
* @param context - Properties supplied while resolving the Durable Object.
*/
async start(context) {
if (_classPrivateFieldGet2(_started, this)) return;
const pending = _classPrivateFieldGet2(_startPromise, this);
if (pending) {
await pending;
return;
}
const attempt = _assertClassBrand(_CapabilityRunner_brand, this, _runStart).call(this, context);
_classPrivateFieldSet2(_startPromise, this, attempt);
try {
await attempt;
} catch (error) {
if (_classPrivateFieldGet2(_startPromise, this) === attempt) _classPrivateFieldSet2(_startPromise, this, void 0);
throw error;
}
}
/**
* Offer a request to each capability middleware in registration order.
*
* @param context - The request entering the Durable Object.
* @returns The first capability response, or `undefined` when no
* capability claimed the request.
*/
async request(context) {
await _assertClassBrand(_CapabilityRunner_brand, this, _ensureReady).call(this, "handle a request");
for (const capability of _assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this)) {
const response = await capability.onRequest?.(context);
if (response !== void 0) return response;
}
}
/**
* Offer a WebSocket upgrade to each capability in declaration order.
*
* @param context - The upgrade request entering the Durable Object.
* @returns The first capability response, or `undefined` when unclaimed.
*/
async webSocketUpgrade(context) {
await _assertClassBrand(_CapabilityRunner_brand, this, _ensureReady).call(this, "handle a WebSocket upgrade");
for (const capability of _assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this)) {
const response = await capability.onWebSocketUpgrade?.(context);
if (response !== void 0) return response;
}
}
/**
* Offer a platform `webSocketMessage` wake to each capability in
* declaration order.
*
* @returns Whether a capability consumed the event.
*/
async webSocketMessage(ws, message) {
await _assertClassBrand(_CapabilityRunner_brand, this, _ensureReady).call(this, "handle a WebSocket message");
for (const capability of _assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this)) if (await capability.onWebSocketMessage?.(ws, message) === true) return true;
return false;
}
/** Offer a platform `webSocketClose` wake to each capability. */
async webSocketClose(ws, code, reason, wasClean) {
await _assertClassBrand(_CapabilityRunner_brand, this, _ensureReady).call(this, "handle a WebSocket close");
for (const capability of _assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this)) if (await capability.onWebSocketClose?.(ws, code, reason, wasClean) === true) return true;
return false;
}
/** Offer a platform `webSocketError` wake to each capability. */
async webSocketError(ws, error) {
await _assertClassBrand(_CapabilityRunner_brand, this, _ensureReady).call(this, "handle a WebSocket error");
for (const capability of _assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this)) if (await capability.onWebSocketError?.(ws, error) === true) return true;
return false;
}
/** Find one installed capability by its stable id. */
async findById(capabilityId) {
await _assertClassBrand(_CapabilityRunner_brand, this, _ensureReady).call(this, "dispatch capability work");
return _assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this).find((candidate) => lifecycleCapabilityId(candidate) === capabilityId);
}
/** Route one message to an installed named capability. */
async route(capabilityId, context) {
await _assertClassBrand(_CapabilityRunner_brand, this, _ensureReady).call(this, "route a capability message");
const capability = _assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this).find((candidate) => lifecycleCapabilityId(candidate) === capabilityId);
if (!capability?.onRoute) throw new Error(`Lifecycle capability ${JSON.stringify(capabilityId)} cannot receive routed messages`);
return capability.onRoute(context);
}
/**
* Offer a memory-limit strike to every capability, best-effort.
*
* Deliberately not gated on startup: a strike can land while startup
* itself is the work that exceeded the memory limit, and the breaker's
* policy must still reach capabilities. One capability's failure does not
* stop the next — the isolate is about to reset either way.
*/
async memoryLimit(context) {
for (const capability of _assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this)) try {
await capability.onMemoryLimit?.(context);
} catch (error) {
console.error("Lifecycle capability memory-limit policy failed", error);
}
}
/** Dispose installed capabilities in reverse registration order. */
async dispose() {
for (const capability of [..._assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this)].reverse()) try {
await capability.dispose?.();
} catch (error) {
console.error("Lifecycle capability disposal failed", error);
}
}
};
async function _runStart(context) {
for (const capability of _assertClassBrand(_CapabilityRunner_brand, this, _getCapabilities).call(this)) await capability.onStart?.(context);
_classPrivateFieldSet2(_started, this, true);
}
async function _ensureReady(operation) {
const pending = _classPrivateFieldGet2(_startPromise, this);
if (pending) await pending;
if (!_classPrivateFieldGet2(_started, this)) throw new Error(`Cannot ${operation} before the Durable Object lifecycle has started`);
}
function _getCapabilities() {
if (!_classPrivateFieldGet2(_capabilities$1, this)) _classPrivateFieldSet2(_capabilities$1, this, Object.freeze([..._classPrivateFieldGet2(_resolveCapabilities, this).call(this)]));
return _classPrivateFieldGet2(_capabilities$1, this);
}
//#endregion
//#region src/lifecycle/abort.ts
/**
* Reset the Durable Object instance without the platform retrying the alarm
* this invocation was handling. Never returns: `abort()` terminates
* execution.
*/
function abortWithoutAlarmRetry(ctx, reason) {
ctx.abort(reason, { retryAlarm: false });
}
//#endregion
//#region src/lifecycle/job-queue.ts
/**
* Durable job queue owned by Lifecycle.
*
* One timestamp-ordered table holds every pending job for the Durable
* Object. A job is a serialisable callback address — owning capability plus
* function name — with a due time and a payload. Capabilities and the host
* push jobs through their scoped `LifecycleJobs` surface; Lifecycle's alarm
* event loop drives due jobs and derives the physical alarm from queue
* state. Payloads are opaque to the queue.
*/
/** Capability id under which host-owned jobs are stored. */
const HOST_JOB_CAPABILITY = "host";
/** Seconds before an in-flight single-flight job is treated as hung. */
const DEFAULT_HUNG_TIMEOUT_SECONDS = 30;
/** @internal Convert one raw queue row into its public job shape. */
function jobFromRow(row) {
return {
id: row.id,
capability: row.capability,
fn: row.fn,
time: row.time,
payload: typeof row.payload === "string" ? JSON.parse(row.payload) : void 0,
retry: typeof row.retry_options === "string" ? JSON.parse(row.retry_options) : void 0,
singleflight: row.singleflight === 1,
exclusive: row.exclusive === 1,
recoveryLoop: row.recovery_loop === 1,
createdAt: row.created_at
};
}
/** @internal One row's hung/slow-dispatch threshold in milliseconds. */
function hungTimeoutMs(row) {
return (row.hung_timeout_seconds ?? DEFAULT_HUNG_TIMEOUT_SECONDS) * 1e3;
}
/** Whether an in-flight single-flight job has crossed its hung timeout. */
function isHungRow(row, nowMs) {
return nowMs - (row.execution_started_at ?? 0) >= hungTimeoutMs(row);
}
var _storage = /* @__PURE__ */ new WeakMap();
var _tableEnsured = /* @__PURE__ */ new WeakMap();
var _JobQueue_brand = /* @__PURE__ */ new WeakSet();
/**
* @internal SQL-backed job queue. Lifecycle owns the single instance; the
* scoped `LifecycleJobs` surfaces delegate here with a fixed capability id.
*/
var JobQueue = class {
constructor(storage) {
_classPrivateMethodInitSpec(this, _JobQueue_brand);
_classPrivateFieldInitSpec(this, _storage, void 0);
_classPrivateFieldInitSpec(this, _tableEnsured, false);
_classPrivateFieldSet2(_storage, this, storage);
}
push(capability, options) {
if (!Number.isFinite(options.time) || options.time < 0) throw new Error(`Invalid job time: ${String(options.time)}`);
if (typeof options.fn !== "string" || options.fn.trim() === "") throw new Error("Jobs require a non-empty fn");
const id = options.id ?? nanoid(9);
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, `INSERT INTO cf_agents_jobs
(id, capability, fn, time, payload, retry_options, singleflight,
hung_timeout_seconds, exclusive, recovery_loop, running,
execution_started_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL)
ON CONFLICT(id) DO UPDATE SET
fn = excluded.fn,
time = excluded.time,
payload = excluded.payload,
retry_options = excluded.retry_options,
singleflight = excluded.singleflight,
hung_timeout_seconds = excluded.hung_timeout_seconds,
exclusive = excluded.exclusive,
recovery_loop = excluded.recovery_loop,
running = 0,
execution_started_at = NULL
WHERE cf_agents_jobs.capability = excluded.capability`, id, capability, options.fn, Math.floor(options.time), options.payload === void 0 ? null : JSON.stringify(options.payload), options.retry ? JSON.stringify(options.retry) : null, options.singleflight ? 1 : 0, options.hungTimeoutSeconds ?? null, options.exclusive ? 1 : 0, options.recoveryLoop ? 1 : 0);
const job = this.get(capability, id);
if (!job) {
const owner = _assertClassBrand(_JobQueue_brand, this, _sql).call(this, "SELECT capability FROM cf_agents_jobs WHERE id = ?", id)[0]?.capability;
throw new Error(owner !== void 0 ? `Job id ${JSON.stringify(id)} already belongs to ${JSON.stringify(owner)}; job ids are scoped to their owner` : `Failed to persist job ${id}`);
}
return job;
}
cancel(capability, id) {
if (_assertClassBrand(_JobQueue_brand, this, _sql).call(this, "SELECT id FROM cf_agents_jobs WHERE id = ? AND capability = ?", id, capability).length === 0) return false;
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, "DELETE FROM cf_agents_jobs WHERE id = ? AND capability = ?", id, capability);
return true;
}
reschedule(capability, id, time) {
if (!Number.isFinite(time) || time < 0) throw new Error(`Invalid job time: ${String(time)}`);
if (_assertClassBrand(_JobQueue_brand, this, _sql).call(this, "SELECT id FROM cf_agents_jobs WHERE id = ? AND capability = ?", id, capability).length === 0) return false;
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, `UPDATE cf_agents_jobs
SET time = ?, running = 0, execution_started_at = NULL
WHERE id = ? AND capability = ?`, Math.floor(time), id, capability);
return true;
}
get(capability, id) {
const rows = _assertClassBrand(_JobQueue_brand, this, _sql).call(this, "SELECT * FROM cf_agents_jobs WHERE id = ? AND capability = ?", id, capability);
return rows[0] ? jobFromRow(rows[0]) : void 0;
}
list(capability) {
return _assertClassBrand(_JobQueue_brand, this, _sql).call(this, "SELECT * FROM cf_agents_jobs WHERE capability = ? ORDER BY time ASC", capability).map(jobFromRow);
}
/** Raw due rows at `nowMs`, ordered by due time. */
due(nowMs) {
return _assertClassBrand(_JobQueue_brand, this, _sql).call(this, "SELECT * FROM cf_agents_jobs WHERE time <= ? ORDER BY time ASC", Math.floor(nowMs));
}
/** One job's current row when it still exists and is still due. */
dueRow(id, nowMs) {
return _assertClassBrand(_JobQueue_brand, this, _sql).call(this, "SELECT * FROM cf_agents_jobs WHERE id = ? AND time <= ?", id, Math.floor(nowMs))[0];
}
markRunning(id, nowMs) {
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, `UPDATE cf_agents_jobs
SET running = 1, execution_started_at = ?
WHERE id = ?`, Math.floor(nowMs), id);
}
clearRunning(id) {
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, `UPDATE cf_agents_jobs
SET running = 0, execution_started_at = NULL
WHERE id = ?`, id);
}
delete(id) {
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, "DELETE FROM cf_agents_jobs WHERE id = ?", id);
}
/**
* Unguarded retime for the alarm memory-limit breaker's backoff: the
* platform-failure path clears the dispatch marker before the breaker
* runs, so the guarded {@link applyOutcome} would no-op — and the
* breaker's backoff must land regardless, it is protecting the object.
*/
retime(id, time) {
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, `UPDATE cf_agents_jobs
SET time = ?, running = 0, execution_started_at = NULL
WHERE id = ?`, Math.floor(time), id);
}
/**
* Back off every pending recovery-loop job that would fire before the
* breaker's backoff time (#1825), so a doomed loop's sibling rows cannot
* re-trigger it on the next wake. Unguarded like {@link retime}: the
* breaker's backoff must land regardless of dispatch markers.
*/
delayRecoveryLoopJobs(time) {
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, `UPDATE cf_agents_jobs
SET time = ?, running = 0, execution_started_at = NULL
WHERE recovery_loop = 1 AND time <= ?`, Math.floor(time), Math.floor(time));
}
/**
* Read every recovery-loop job before breaker sealing. The memory-limit
* policy phase uses this snapshot after the rows are purged so routed owners
* can terminalize their own durable recovery state.
*/
recoveryLoopJobs() {
return _assertClassBrand(_JobQueue_brand, this, _sql).call(this, "SELECT * FROM cf_agents_jobs WHERE recovery_loop = 1 ORDER BY time ASC").map(jobFromRow);
}
/**
* Purge every recovery-loop job when the breaker seals at its strike
* budget (#1825). Unrelated jobs are untouched.
*/
purgeRecoveryLoopJobs() {
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, "DELETE FROM cf_agents_jobs WHERE recovery_loop = 1");
}
/**
* Apply one drive result, guarded on the dispatch marker: every driven
* job carries `running = 1` for the duration of its dispatch, and a
* same-id `push()` or `reschedule()` made meanwhile clears it. A cleared
* marker means newer durable intent exists, so the outcome quietly
* defers to it instead of deleting or retiming the fresher job.
*/
applyOutcome(id, outcome) {
if (outcome === void 0) {
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, "DELETE FROM cf_agents_jobs WHERE id = ? AND running = 1", id);
return;
}
if (outcome === "yield") {
this.clearRunning(id);
return;
}
if (typeof outcome === "object" && Number.isFinite(outcome.rescheduleAt) && outcome.rescheduleAt >= 0) {
_assertClassBrand(_JobQueue_brand, this, _sql).call(this, `UPDATE cf_agents_jobs
SET time = ?, running = 0, execution_started_at = NULL
WHERE id = ? AND running = 1`, Math.floor(outcome.rescheduleAt), id);
return;
}
throw new Error(`Invalid job outcome for ${id}`);
}
/**
* The next physical alarm time derived from queue state, or `null` when
* the queue holds nothing to wake for.
*
* Exclusive jobs suppress ordinary candidates. An ordinary candidate is
* the earliest ready job clamped to the future (overdue rows survive
* restarts and must re-fire immediately), merged with the earliest
* hung-timeout recheck for in-flight single-flight jobs.
*/
nextAlarmTime(nowMs) {
const exclusive = _assertClassBrand(_JobQueue_brand, this, _sql).call(this, "SELECT MIN(time) AS time FROM cf_agents_jobs WHERE exclusive = 1");
if (exclusive[0]?.time !== null && exclusive[0]?.time !== void 0) return exclusive[0].time;
const now = Math.floor(nowMs);
let candidate = null;
const ready = _assertClassBrand(_JobQueue_brand, this, _sql).call(this, `SELECT MIN(time) AS time FROM cf_agents_jobs
WHERE singleflight = 0
OR running = 0
OR coalesce(execution_started_at, 0) + coalesce(hung_timeout_seconds, ?) * 1000 <= ?`, DEFAULT_HUNG_TIMEOUT_SECONDS, now);
if (ready[0]?.time !== null && ready[0]?.time !== void 0) candidate = Math.max(ready[0].time, now + 1);
const recheck = _assertClassBrand(_JobQueue_brand, this, _sql).call(this, `SELECT MIN(coalesce(execution_started_at, 0) + coalesce(hung_timeout_seconds, ?) * 1000) AS recheck
FROM cf_agents_jobs
WHERE singleflight = 1
AND running = 1
AND coalesce(execution_started_at, 0) + coalesce(hung_timeout_seconds, ?) * 1000 > ?`, DEFAULT_HUNG_TIMEOUT_SECONDS, DEFAULT_HUNG_TIMEOUT_SECONDS, now)[0]?.recheck;
if (recheck !== null && recheck !== void 0) candidate = candidate === null ? recheck : Math.min(candidate, recheck);
return candidate;
}
};
function _sql(query, ...params) {
_assertClassBrand(_JobQueue_brand, this, _ensureTable).call(this);
try {
return [..._classPrivateFieldGet2(_storage, this).sql.exec(query, ...params)];
} catch (cause) {
throw new SqlError(query, cause);
}
}
function _ensureTable() {
if (_classPrivateFieldGet2(_tableEnsured, this)) return;
_classPrivateFieldGet2(_storage, this).sql.exec(`
CREATE TABLE IF NOT EXISTS cf_agents_jobs (
id TEXT PRIMARY KEY NOT NULL,
capability TEXT NOT NULL,
fn TEXT NOT NULL,
time INTEGER NOT NULL,
payload TEXT,
retry_options TEXT,
singleflight INTEGER NOT NULL DEFAULT 0,
hung_timeout_seconds INTEGER,
exclusive INTEGER NOT NULL DEFAULT 0,
recovery_loop INTEGER NOT NULL DEFAULT 0,
running INTEGER NOT NULL DEFAULT 0,
execution_started_at INTEGER,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
) WITHOUT ROWID`);
_classPrivateFieldSet2(_tableEnsured, this, true);
}
//#endregion
//#region src/lifecycle/job-driver.ts
/**
* Alarm event loop for the Lifecycle job queue.
*
* The driver owns everything that happens when the physical alarm fires:
* the deadman pre-arm, driving due jobs in due order (single-flight skip and
* hung recovery, per-job retries, platform-failure deferral, terminal
* failure hooks), the backlog warning, and the alarm memory-limit circuit
* breaker (#1825). Lifecycle wires it to the host and capabilities through
* the narrow {@link JobDriverOptions} contract and keeps only the alarm
* entry point itself.
*/
/** Default consecutive memory-limit strikes tolerated before sealing. */
const DEFAULT_MAX_ALARM_MEMORY_LIMIT_STRIKES = 3;
/** Durable storage key for the alarm memory-limit strike counter (#1825). */
const OOM_ALARM_STRIKES_KEY = "cf_agents:oom_alarm_strikes";
/** Default retry policy applied to jobs pushed without one. */
const DEFAULT_JOB_RETRY = {
maxAttempts: 3,
baseDelayMs: 100,
maxDelayMs: 3e3
};
/** Due jobs for one capability above this count log a backlog warning. */
const JOB_BACKLOG_WARNING_THRESHOLD = 10;
/**
* Deadman pre-arm delay: armed before the event loop drives due jobs so an
* isolate death mid-drive still wakes this object to resume its queue.
*/
const DEADMAN_ALARM_DELAY_MS = 3e4;
/**
* Carries the row a platform failure escaped from out to `runAlarm`'s catch,
* across the rethrow from `#driveJob`. Attribution must not live in a shared
* instance field: overlapping `alarm()` invocations (a deadman or the
* platform's own re-fire racing a still-running invocation) dispatch jobs
* concurrently, and a field would let a later dispatch overwrite the row an
* earlier one is still unwinding for.
*/
var AttributedPlatformFailure = class {
constructor(row, cause) {
this.row = row;
this.cause = cause;
}
};
var _options = /* @__PURE__ */ new WeakMap();
var _alarmScope = /* @__PURE__ */ new WeakMap();
var _alarmsInFlight = /* @__PURE__ */ new WeakMap();
var _outstandingAlarmWork = /* @__PURE__ */ new WeakMap();
var _strike = /* @__PURE__ */ new WeakMap();
var _strikeRecordedThisIsolate = /* @__PURE__ */ new WeakMap();
var _JobDriver_brand = /* @__PURE__ */ new WeakSet();
/** @internal Drives the job queue when the Durable Object alarm fires. */
var JobDriver = class {
constructor(options) {
_classPrivateMethodInitSpec(this, _JobDriver_brand);
_classPrivateFieldInitSpec(this, _options, void 0);
_classPrivateFieldInitSpec(this, _alarmScope, new AsyncLocalStorage());
_classPrivateFieldInitSpec(this, _alarmsInFlight, 0);
_classPrivateFieldInitSpec(this, _outstandingAlarmWork, /* @__PURE__ */ new Set());
_classPrivateFieldInitSpec(this, _strike, void 0);
_classPrivateFieldInitSpec(this, _strikeRecordedThisIsolate, false);
_classPrivateFieldSet2(_options, this, options);
}
/**
* Run one alarm invocation: initialize the lifecycle, drive due jobs, run
* the host's alarm callback, and re-arm the physical alarm from queue
* state — all inside the alarm memory-limit circuit breaker (#1825). A
* memory-limit reset that propagates here is intercepted — every other
* error re-throws unchanged so platform alarm-retry semantics hold — and
* broken from this outermost frame, where the heavy turn has unwound and
* small writes can land. Initialization runs inside the breaker because a
* severe reset can be thrown before any job runs (boot hydration, #1825);
* left unhandled it would re-throw to the platform, which auto-retries
* the alarm forever.
*/
async runAlarm(initialize, runHostAlarm) {
var _this$alarmsInFlight;
_classPrivateFieldSet2(_alarmsInFlight, this, (_this$alarmsInFlight = _classPrivateFieldGet2(_alarmsInFlight, this), _this$alarmsInFlight++, _this$alarmsInFlight));
let clean = false;
try {
try {
await initialize();
await _assertClassBrand(_JobDriver_brand, this, _driveDueJobs).call(this);
await _classPrivateFieldGet2(_alarmScope, this).run({ executing: void 0 }, runHostAlarm);
clean = true;
} catch (error) {
const attributed = error instanceof AttributedPlatformFailure;
const cause = attributed ? error.cause : error;
if (!isDurableObjectMemoryLimitReset(cause)) throw attributed ? cause : error;
await _assertClassBrand(_JobDriver_brand, this, _handleMemoryLimitReset).call(this, cause, attributed ? error.row : void 0);
return;
}
} finally {
var _this$alarmsInFlight3;
_classPrivateFieldSet2(_alarmsInFlight, this, (_this$alarmsInFlight3 = _classPrivateFieldGet2(_alarmsInFlight, this), _this$alarmsInFlight3--, _this$alarmsInFlight3));
if (clean) await _assertClassBrand(_JobDriver_brand, this, _clearMemoryLimitStrikesWhenQuiescent).call(this);
}
await _classPrivateFieldGet2(_options, this).rearm();
}
/**
* Keep work a job handed off at a bounded return inside this alarm's
* memory-limit breaker domain (#1825). The alarm itself returns promptly,
* so other jobs stay live; the handoff is classified when it settles. A
* memory-limit reset it reports records a strike against the job that
* handed it off, exactly as an in-alarm reset would. Strikes clear only
* once no handed-off work is outstanding and the last of it settled clean.
*
* @returns True when called from an alarm-driven dispatch or the host
* alarm hook; false otherwise, in which case nothing is tracked. Tracking
* the same promise again (a claim-backstop wake for an attempt already
* handed off) is a no-op.
*/
trackAlarmWork(work) {
const scope = _classPrivateFieldGet2(_alarmScope, this).getStore();
if (!scope) return false;
if (_classPrivateFieldGet2(_outstandingAlarmWork, this).has(work)) return true;
_classPrivateFieldGet2(_outstandingAlarmWork, this).add(work);
work.then(() => _assertClassBrand(_JobDriver_brand, this, _settleAlarmWork).call(this, work, void 0, scope.executing), (error) => _assertClassBrand(_JobDriver_brand, this, _settleAlarmWork).call(this, work, error, scope.executing));
return true;
}
};
async function _settleAlarmWork(work, error, executing) {
try {
if (_classPrivateFieldGet2(_options, this).disabled()) return;
if (isDurableObjectMemoryLimitReset(error)) {
await _assertClassBrand(_JobDriver_brand, this, _handleMemoryLimitReset).call(this, error, executing);
return;
}
} finally {
_classPrivateFieldGet2(_outstandingAlarmWork, this).delete(work);
}
await _assertClassBrand(_JobDriver_brand, this, _clearMemoryLimitStrikesWhenQuiescent).call(this);
}
/**
* Clear the strike counter once the alarm domain is fully quiescent: no
* alarm invocation in flight, no handed-off work outstanding, and no
* strike recorded in this isolate that a fresh isolate hasn't yet
* superseded. Called at every transition that could make either counter
* newly zero (an alarm ending, a handoff settling) — each call re-reads
* both fresh, so whichever transition happens last is the one that
* performs the clear. Idempotent: redundant calls no-op.
*/
async function _clearMemoryLimitStrikesWhenQuiescent() {
if (_classPrivateFieldGet2(_alarmsInFlight, this) > 0) return;
if (_classPrivateFieldGet2(_outstandingAlarmWork, this).size > 0) return;
if (_classPrivateFieldGet2(_strikeRecordedThisIsolate, this)) return;
_classPrivateFieldSet2(_strike, this, void 0);
await _assertClassBrand(_JobDriver_brand, this, _clearMemoryLimitStrikes).call(this);
}
/** Drive every due job once, in due-time order. */
async function _driveDueJobs() {
const nowMs = Date.now();
const due = _classPrivateFieldGet2(_options, this).queue.due(nowMs);
if (due.length === 0) return;
_assertClassBrand(_JobDriver_brand, this, _warnBacklog).call(this, due);
if (!_classPrivateFieldGet2(_options, this).disabled()) await _classPrivateFieldGet2(_options, this).storage.setAlarm(nowMs + DEADMAN_ALARM_DELAY_MS);
for (const stale of due) {
if (_classPrivateFieldGet2(_options, this).disabled()) return;
const row = _classPrivateFieldGet2(_options, this).queue.dueRow(stale.id, nowMs);
if (!row) continue;
if (row.singleflight === 1 && row.running === 1) {
if (!isHungRow(row, nowMs)) {
console.warn(`Skipping job ${row.id}: previous execution still running`);
continue;
}
console.warn(`Forcing reset of hung job ${row.id} (started ${Math.round((nowMs - (row.execution_started_at ?? 0)) / 1e3)}s ago)`);
}
_classPrivateFieldGet2(_options, this).queue.markRunning(row.id, nowMs);
await _assertClassBrand(_JobDriver_brand, this, _driveJob).call(this, row);
}
}
/** Dispatch one due row to its owner with retry and failure policy. */
async function _driveJob(row) {
const { queue, resolveDispatch, disabled } = _classPrivateFieldGet2(_options, this);
const job = jobFromRow(row);
const dispatch = await resolveDispatch(row.capability);
if (!dispatch) {
console.error(`No installed capability or host handler for job ${row.id} (owner ${JSON.stringify(row.capability)}); dropping it`);
queue.delete(row.id);
return;
}
const maxAttempts = job.retry?.maxAttempts ?? DEFAULT_JOB_RETRY.maxAttempts;
const slowWatchdog = setTimeout(() => {
const seconds = Math.round(hungTimeoutMs(row) / 1e3);
console.warn(`Job ${row.id} (${row.capability}/${row.fn}) has been dispatching for over ${seconds}s. Long dispatches starve every other job on this object; onJob must detach unbounded work and return.`);
try {
_classPrivateFieldGet2(_options, this).emit("job:slow_dispatch", {
capability: row.capability,
fn: row.fn,
id: row.id,
thresholdMs: hungTimeoutMs(row)
});
} catch {}
}, hungTimeoutMs(row));
let outcome;
try {
outcome = await _classPrivateFieldGet2(_alarmScope, this).run({ executing: row }, () => tryN(maxAttempts, (attempt) => dispatch.onJob({
job,
attempt
}), {
baseDelayMs: job.retry?.baseDelayMs ?? DEFAULT_JOB_RETRY.baseDelayMs,
maxDelayMs: job.retry?.maxDelayMs ?? DEFAULT_JOB_RETRY.maxDelayMs,
shouldRetry: (error) => !isDurableObjectCodeUpdateReset(error) && !isDurableObjectMemoryLimitReset(error)
}));
} catch (error) {
if (disabled()) return;
if (isPlatformFailure(error)) {
try {
queue.clearRunning(row.id);
} catch {}
console.warn(`Deferring job ${row.id} to a fresh invocation after a platform failure; the job is preserved.`);
throw new AttributedPlatformFailure(row, error);
}
try {
outcome = await dispatch.onJobError?.({
job,
attempt: maxAttempts
}, error);
} catch (hookError) {
console.error(`Job failure hook threw for ${row.id}`, hookError);
}
} finally {
clearTimeout(slowWatchdog);
}
if (disabled()) return;
queue.applyOutcome(row.id, outcome ?? void 0);
}
function _warnBacklog(due) {
const counts = /* @__PURE__ */ new Map();
for (const row of due) counts.set(row.capability, (counts.get(row.capability) ?? 0) + 1);
for (const [owner, count] of counts) {
if (count < JOB_BACKLOG_WARNING_THRESHOLD) continue;
try {
console.warn(`Processing ${count} due jobs for ${JSON.stringify(owner)} in a single alarm cycle. This usually means one-shot jobs are pushed repeatedly without a stable id.`);
_classPrivateFieldGet2(_options, this).emit("job:backlog_warning", {
capability: owner,
count
});
} catch {}
}
}
/**
* Clear the durable memory-limit strike counter after a clean alarm so the
* breaker counts CONSECUTIVE resets rather than lifetime ones (#1825).
* Reads first and only writes when a strike is recorded. Best-effort.
*/
async function _clearMemoryLimitStrikes() {
const { storage } = _classPrivateFieldGet2(_options, this);
try {
const prior = await storage.get(OOM_ALARM_STRIKES_KEY);
if (typeof prior === "number" && prior > 0) await storage.delete(OOM_ALARM_STRIKES_KEY);
} catch {}
}
/**
* Alarm-boundary circuit breaker for Durable Object memory-limit resets
* (#1825). Unhandled, the platform would auto-retry the alarm forever,
* re-running the doomed work each cycle. A durable strike counter
* tolerates a few consecutive resets — backing off the executing job and
* every pending recovery-loop job so the retry is not a hot loop — then
* seals: those jobs are purged and the capability + host memory-limit
* policy hooks run.
*
* One reset is one event even when several flows observe it. The strike
* is recorded once per event ({@link #recordMemoryLimitStrike}); every
* observer then applies the per-job policy for the job it belongs to, and
* the first observer finishes the event by re-arming, syncing, and
* resetting the isolate. Each step is best-effort: even these small writes
* can OOM, but swallowing still halts the platform's auto-retry, and a
* later wake re-arms legitimate work.
*/
async function _handleMemoryLimitReset(error, executing) {
const { queue } = _classPrivateFieldGet2(_options, this);
const first = _classPrivateFieldGet2(_strike, this) === void 0;
_classPrivateFieldSet2(_strikeRecordedThisIsolate, this, true);
_classPrivateFieldGet2(_strike, this) ?? _classPrivateFieldSet2(_strike, this, _assertClassBrand(_JobDriver_brand, this, _recordMemoryLimitStrike).call(this, error));
const strike = await _classPrivateFieldGet2(_strike, this);
try {
if (executing) {
if (strike.sealed) queue.delete(executing.id);
else if (strike.nextTime !== void 0) queue.retime(executing.id, strike.nextTime);
}
} catch {}
try {
await _classPrivateFieldGet2(_options, this).onMemoryLimit({
sealed: strike.sealed,
nextTime: strike.nextTime,
executing: executing ? jobFromRow(executing) : void 0,
purgedRecoveryLoopJobs: first ? strike.purgedRecoveryLoopJobs : void 0
});
} catch {}
if (!first) return;
try {
await _classPrivateFieldGet2(_options, this).rearm();
} catch {}
try {
await _classPrivateFieldGet2(_options, this).storage.sync();
_classPrivateFieldGet2(_options, this).reset(`Alarm memory-limit strike ${strike.strikes}/${strike.limit}${strike.sealed ? " (sealed)" : ""}; resetting isolate (#1825)`);
} catch {}
}
/**
* Record one strike durably and apply the queue-wide policy that belongs
* to the event rather than to any one job: recovery-loop rows back off (or
* purge) as a pack, a sealing strike resets the counter, and the event is
* emitted once.
*/
async function _recordMemoryLimitStrike(error) {
const { queue, storage } = _classPrivateFieldGet2(_options, this);
let strikes = 1;
try {
const prior = await storage.get(OOM_ALARM_STRIKES_KEY);
strikes = (typeof prior === "number" ? prior : 0) + 1;
await storage.put(OOM_ALARM_STRIKES_KEY, strikes);
} catch {}
const limit = _classPrivateFieldGet2(_options, this).maxMemoryLimitStrikes() ?? DEFAULT_MAX_ALARM_MEMORY_LIMIT_STRIKES;
const sealed = strikes >= limit;
console.error(`Alarm hit a Durable Object memory-limit reset (strike ${strikes}/${limit}${sealed ? ", sealing recovery" : ", will retry with backoff"}). Breaking the platform alarm-retry loop (#1825).`, error instanceof Error ? error.message : String(error));
const nextTime = sealed ? void 0 : Date.now() + Math.min(300, 30 * strikes) * 1e3;
let purgedRecoveryLoopJobs;
try {
if (sealed) purgedRecoveryLoopJobs = queue.recoveryLoopJobs();
} catch {}
try {
if (sealed) queue.purgeRecoveryLoopJobs();
else if (nextTime !== void 0) queue.delayRecoveryLoopJobs(nextTime);
} catch {}
if (sealed) try {
await storage.delete(OOM_ALARM_STRIKES_KEY);
} catch {}
try {
_classPrivateFieldGet2(_options, this).emit("alarm:memory_limit_reset", {
strikes,
limit,
sealed,
error: error instanceof Error ? error.message : String(error)
});
} catch {}
return {
strikes,
limit,
sealed,
nextTime,
purgedRecoveryLoopJobs
};
}
//#endregion
//#region src/lifecycle/transport-errors.ts
/** Standard `WebSocket.readyState` values. */
const CLOSING = 2;
const CLOSED = 3;
/**
* A retryable transport-teardown error ("Network connection lost" /
* "WebSocket peer disconnected") that fires on a connection which is already
* CLOSING/CLOSED is just the socket going away during or right after the close
* handshake - not an application error. Surfacing it via `onError` spams logs
* on every abrupt client disconnect, and even on clean closes when the peer
* tears down its transport before our reciprocal Close frame lands. Suppress
* it in that specific case only; genuine mid-connection (OPEN) errors still
* reach `onError`.
*
* Detection prefers the structured `retryable` flag over message text so it
* stays correct across `enhanced-error-serialization` (compat date
* >= 2026-04-21), with a substring fallback for older error shapes.
*/
function isBenignTeardownError(ws, error) {
const state = ws.readyState;
if (state !== CLOSING && state !== CLOSED) return false;
if (typeof error !== "object" || error === null) return false;
const typed = error;
if (typed.retryable === true) return true;
const message = typeof typed.message === "string" ? typed.message : "";
return /Network connection lost|WebSocket peer disconnected/i.test(message);
}
//#endregion
//#region src/lifecycle/durable-object-lifecycle.ts
const LEGACY_NAME_STORAGE_KEY = "__ps_name";
function mutableRequest(request) {
return new Request(request);
}
/**
* Decode props from the internal lifecycle props header.
*
* Handles both base64-encoded lifecycle props and, for
* backwards compatibility with stubs/requests created by older versions,
* raw JSON. Base64 never starts with `{` or `[`, so a leading brace/bracket
* unambiguously identifies the legacy raw-JSON form.
*/
function decodeProps(header) {
const trimmed = header.trim();
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return JSON.parse(trimmed);
const binary = atob(header);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return JSON.parse(new TextDecoder().decode(bytes));
}
const lifecycleEventSinks = /* @__PURE__ */ new WeakMap();
const lifecycleRouteTransports = /* @__PURE__ */ new WeakMap();
const lifecycleHostInvokers = /* @__PURE__ */ new WeakMap();
/** @internal Adapt the host invocation boundary at a composition root. */
function setLifecycleHostInvoker(lifecycle, invoker) {
lifecycleHostInvokers.set(lifecycle, invoker);
}
/** @internal Supply a host's routed Lifecycle transport. */
function setLifecycleRouteTransport(lifecycle, transport) {
lifecycleRouteTransports.set(lifecycle, transport);
}
/** @internal Adapt Lifecycle's default diagnostics sink at a composition root. */
function setLifecycleEventSink(lifecycle, sink) {
lifecycleEventSinks.set(lifecycle, sink);
}
/**
* Installs and coordinates the runtime lifecycle for a Durable Object.
*
* Construct this as an instance field on a class that directly extends
* `DurableObject`, then call {@link Lifecycle.installHandlers}
* from that class's constructor.
*
* @experimental The API surface may change before stabilizing.
*/
/** The dispatch hooks a catch-all can monopolize; uniqueness is per hook. */
const CATCH_ALL_HOOKS = ["onRequest", "onWebSocketUpgrade"];
var _host = /* @__PURE__ */ new WeakMap();
var _ctx = /* @__PURE__ */ new WeakMap();
var _parentClassName = /* @__PURE__ */ new WeakMap();
var _capabilities = /* @__PURE__ */ new WeakMap();
var _capabilityRunner = /* @__PURE__ */ new WeakMap();
var _jobQueue = /* @__PURE__ */ new WeakMap();
var _jobDriver = /* @__PURE__ */ new WeakMap();
var _status = /* @__PURE__ */ new WeakMap();
var _alarmRearmQueue = /* @__PURE__ */ new WeakMap();
var _rearmRequestedDuringStart = /* @__PURE__ */ new WeakMap();
var _pendingEvents = /* @__PURE__ */ new WeakMap();
var _alarmsDisabled = /* @__PURE__ */ new WeakMap();
var _capabilitiesLocked = /* @__PURE__ */ new WeakMap();
var _handlersInstalled = /* @__PURE__ */ new WeakMap();
var _Lifecycle_brand = /* @__PURE__ */ new WeakSet();
var _legacyName = /* @__PURE__ */ new WeakMap();
var _props = /* @__PURE__ */ new WeakMap();
var Lifecycle = class Lifecycle {
/**
* Construct and install a lifecycle in one explicit operation.
*
* @param host - The Durable Object whose runtime handlers the lifecycle owns.
* @returns The installed lifecycle.
*/
static install(host, options) {
const lifecycle = new Lifecycle(host, options);
lifecycle.installHandlers();
return lifecycle;
}
/**
* Bind a lifecycle to a Durable Object instance without mutating its handlers.
*
* @param host - The Durable Object whose runtime lifecycle this object owns.
* @param options - Policy configuration for this lifecycle.
*/
constructor(host, options) {
_classPrivateMethodInitSpec(this, _Lifecycle_brand);
_classPrivateFieldInitSpec(this, _host, void 0);
_classPrivateFieldInitSpec(this, _ctx, void 0);
_classPrivateFieldInitSpec(this, _parentClassName, void 0);
_classPrivateFieldInitSpec(this, _capabilities, []);
_classPrivateFieldInitSpec(this, _capabilityRunner, new CapabilityRunner(() => _classPrivateFieldGet2(_capabilities, this)));
_classPrivateFieldInitSpec(this, _jobQueue, void 0);
_classPrivateFieldInitSpec(this, _jobDriver, void 0);
_classPrivateFieldInitSpec(this, _status, "zero");
_classPrivateFieldInitSpec(this, _alarmRearmQueue, Promise.resolve());
_classPrivateFieldInitSpec(this, _rearmRequestedDuringStart, false);
_classPrivateFieldInitSpec(this, _pendingEvents, []);
_classPrivateFieldInitSpec(this, _alarmsDisabled, false);
_classPrivateFieldInitSpec(this, _capabilitiesLocked, false);
_classPrivateFieldInitSpec(this, _handlersInstalled, false);
_classPrivateFieldInitSpec(this, _legacyName, void 0);
_classPrivateFieldInitSpec(this, _props, void 0);
_classPrivateFieldSet2(_host, this, host);
_classPrivateFieldSet2(_ctx, this, _classPrivateFieldGet2(_host, this).ctx);
_classPrivateFieldSet2(_parentClassName, this, _classPrivateFieldGet2(_host, this).constructor.name);
_classPrivateFieldSet2(_jobQueue, this, new JobQueue(_classPrivateFieldGet2(_ctx, this).storage));
_classPrivateFieldSet2(_jobDriver, this, new JobDriver({
queue: _classPrivateFieldGet2(_jobQueue, this),
storage: _classPrivateFieldGet2(_ctx, this).storage,
disabled: () => _classPrivateFieldGet2(_alarmsDisabled, this),
resolveDispatch: (owner) => _assertClassBrand(_Lifecycle_brand, this, _resolveJobDispatch).call(this, owner),
maxMemoryLimitStrikes: () => options?.maxAlarmMemoryLimitStrikes,
onMemoryLimit: async (context) => {
await _classPrivateFieldGet2(_capabilityRunner, this).memoryLimit(context);
await runInLifecycleHostContext({ host: _classPrivateFieldGet2(_host, this) }, () => _classPrivateFieldGet2(_host, this).onAlarmMemoryLimit?.(context));
},
emit: (type, payload) => _assertClassBrand(_Lifecycle_brand, this, _emitCapabilityEvent).call(this, {
source: "lifecycle",
type,
payload
}),
rearm: () => this.rearmAlarm(),
reset: (reason) => {
setTimeout(() => abortWithoutAlarmRetry(_classPrivateFieldGet2(_ctx, this), reason), 0);
}
}));
}
/**
* Install platform fetch, alarm, and hibernating WebSocket handlers.
*
* Existing handlers are preserved for framework-owned dispatch such as the
* Agent's sub-agent router and alarm circuit breaker. Calling this method
* more than once is an error.
*/
installHandlers() {
if (_classPrivateFieldGet2(_handlersInstalled, this)) throw new Error("Durable Object lifecycle handlers are already installed");
_classPrivateFieldSet2(_handlersInstalled, this, true);
const handlers = {
fetch: this.fetch.bind(this),
alarm: this.alarm.bind(this),
webSocketMessage: this.webSocketMessage.bind(this),
webSocketClose: this.webSocketClose.bind(this),
webSocketError: this.webSocketError.bind(this)
};
for (const [name, handler] of Object.entries(handlers)) {
if (name in _classPrivateFieldGet2(_host, this)) continue;
Object.defineProperty(_classPrivateFieldGet2(_host, this), name, {
value: handler,
configurable: true
});
}
}
/**
* Add a reusable capability before this lifecycle starts.
*
* Capabilities dispatch in registration order, except that a capability
* declaring `claims: "catch-all"` always comes last, whenever it was
* installed. Catch-alls are unique per dispatch hook: two may coexist
* when they claim disjoint traffic (one `onRequest`, one
* `onWebSocketUpgrade`), but a second catch-all for the same hook could
* never be reached and is refused.
*
* @param capability - The capability to add.
* @returns This lifecycle.
*/
use(capability) {
if (_classPrivateFieldGet2(_capabilitiesLocked, this)) throw new Error("Lifecycle capabilities must be added before startup");
const capabilityId = lifecycleCapabilityId(capability);
if (capabilityId && _classPrivateFieldGet2(_capabilities, this).some((candidate) => lifecycleCapabilityId(candidate) === capabilityId)) throw new Error(`Lifecycle capability ${JSON.stringify(capabilityId)} is already installed`);
const catchAllIndex = _classPrivateFieldGet2(_capabilities, this).findIndex((candidate) => candidate.claims === "catch-all");
if (capability.claims === "catch-all") {
for (const hook of CATCH_ALL_HOOKS) {
if (!capability[hook]) continue;
const rival = _classPrivateFieldGet2(_capabilities, this).find((candidate) => candidate.claims === "catch-all" && candidate[hook]);
if (!rival) continue;
const installed = lifecycleCapabilityId(rival);
throw new Error(`Lifecycle already has a catch-all for ${hook}${installed ? ` (${JSON.stringify(installed)})` : ""}; a second one could never be reached`);
}
_classPrivateFieldGet2(_capabilities, this).push(capability);
} else _classPrivateFieldGet2(_capabilities, this).splice(catchAllIndex === -1 ? _classPrivateFieldGet2(_capabilities, this).length : catchAllIndex, 0, capability);
if (capability instanceof LifecycleCapability) bindLifecycleCapability(capability, _assertClassBrand(_Lifecycle_brand, this, _servicesForCapability).call(this, capability.capabilityId));
return this;
}
/** @internal Deliver a generic capability envelope to this Lifecycle. */
route(envelope) {
return _assertClassBrand(_Lifecycle_brand, this, _dispatchRoute).call(this, envelope);
}
/**
* Execute SQL queries against the Durable Object's database
* @template T Type of the returned rows
* @param strings SQL query template strings
* @param values Values to be inserted into the query
* @returns Array of query results
*/
sql(strings, ...values) {
let query = "";
try {
query = strings.reduce((acc, str, i) => acc + str + (i < values.length ? "?" : ""), "");
return [..._classPrivateFieldGet2(_ctx, this).storage.sql.exec(query, ...values)];
} catch (error) {
console.error(`failed to execute sql query: ${query}`, error);
throw error;
}
}
/**
* Handle an incoming request for the owning Durable Object.
*
* Non-upgrade requests run through the capability middleware chain first,
* then fall through to the host's `onRequest`.
*/
async fetch(request) {
try {
const encodedProps = request.headers.get("x-agents-lifecycle-props");
if (encodedProps) {
_classPrivateFieldSet2(_props, this, decodeProps(encodedProps));
request = mutableRequest(request);
request.headers.delete("x-agents-lifecycle-props");
}
await _assertClassBrand(_Lifecycle_brand, this, _ensureInitialized).call(this);
if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
const capabilityResponse = await runWithoutCurrentAgent(() => _classPrivateFieldGet2(_capabilityRunner, this).request({ request }));
if (capabilityResponse !== void 0) return capabilityResponse;
if (_classPrivateFieldGet2(_host, this).onRequest) return await runInLifecycleHostContext({
host: _classPrivateFieldGet2(_host, this),
request
}, () => _classPrivateFieldGet2(_host, this).onRequest(request));
return new Response("Not implemented", { status: 404 });
} else {
const upgradeResponse = await runWithoutCurrentAgent(() => _classPrivateFieldGet2(_capabilityRunner, this).webSocketUpgrade({ request }));
if (upgradeResponse !== void 0) return upgradeResponse;
return new Response("WebSocket upgrades are not enabled on this Durable Object. Install a capability that claims them (e.g. WebSockets).", { status: 404 });
}
} catch (err) {
console.error(`Error in ${_classPrivateFieldGet2(_parentClassName, this)}:${_classPrivateFieldGet2(_ctx, this).id.name ?? "<unnamed>"} fetch:`, err);
if (!(err instanceof Error)) throw err;
if (request.headers.get("Upgrade") === "websocket") {
const pair = new WebSocketPair();
pair[1].accept();
pair[1].send(JSON.stringify({ error: err.stack }));
pair[1].close(1011, "Uncaught exception during session setup");
return new Response(null, {
status: 101,
webSocket: pair[0]
});
} else return new Response(err.stack, { status: 500 });
}
}
/** @internal Dispatch a hibernating WebSocket message. */
async webSocketMessage(ws, message) {
try {
await _assertClassBrand(_Lifecycle_brand, this, _ensureInitialized).call(this);
await runWithoutCurrentAgent(() => _classPrivateFieldGet2(_capabilityRunner, this).webSocketMessage(ws, message));
} catch (e) {
console.error(`Error in ${_classPrivateFieldGet2(_parentClassName, this)}:${_classPrivateFieldGet2(_ctx, this).id.name ?? "<unnamed>"} webSocketMessage:`, e);
}
}
/** @internal Dispatch a hibernating WebSocket close. */
async webSocketClose(ws, code, reason, wasClean) {
try {
await _assertClassBrand(_Lifecycle_brand, this, _ensureInitialized).call(this);
await runWithoutCurrentAgent(() => _classPrivateFieldGet2(_capabilityRunner, this).webSocketClose(ws, code, reason, wasClean));
} catch (e) {
console.error(`Error in ${_classPrivateFieldGet2(_parentClassName, this)}:${_classPrivateFieldGet2(_ctx, this).id.name ?? "<unnamed>"} webSocketClose:`, e);
}
}
/** @internal Dispatch a hibernating WebSocket error. */
async webSocketError(ws, error) {
if (isBenignTeardownError(ws, error)) return;
try {
await _assertClassBrand(_Lifecycle_brand, this, _ensureInitialized).call(this);
await runWithoutCurrentAgent(() => _classPrivateFieldGet2(_capabilityRunner, this).webSocketError(ws, error));
} catch (e) {
console.error(`Error in ${_classPrivateFieldGet2(_parentClassName, this)}:${_classPrivateFieldGet2(_ctx, this).id.name ?? "<unnamed>"} webSocketError:`, e);
}
}
/**
* Start lifecycle capabilities and the owning Durable Object.
*
* Runtime fetch, alarm, and WebSocket entry points call this automatically.
* RPC methods may call it explicitly because native RPC bypasses fetch.
*
* @param props - Optional properties supplied to capability and host startup.
*/
async start(props) {
if (props !== void 0) _classPrivateFieldSet2(_props, this, props);
await _assertClassBrand(_Lifecycle_brand, this, _ensureInitialized).call(this);
}
/**
* The name used to address this Durable Object.
*
* Native `ctx.id.name` is authoritative. A read-only legacy storage fallback
* lets objects created by older PartyServer releases migrate without new
* name writes.
*/
get name() {
const name = _classPrivateFieldGet2(_ctx, this).id.name ?? _classPrivateFieldGet2(_legacyName, this);
if (name !== void 0) return name;
throw new Error(`${_classPrivateFieldGet2(_parentClassName, this)} could not determine its Durable Object name. Address it with idFromName() or getByName(). In local development, update Wrangler/workerd and use a current compatibility_date. newUniqueId(), idFromString(), and names over 1,024 bytes do not expose ctx.id.name. Alarms created before 2026-03-15 must be rescheduled from a named fetch or RPC handler.`);
}
/**
* The host's scoped access to the Lifecycle work queue. Items pushed here
* are dispatched to the host's `onJob` inside the host invocation
* boundary.
*/
get jobs() {
return _assertClassBrand(_Lifecycle_brand, this, _jobsForOwner).call(this, HOST_JOB_CAPABILITY);
}
/**
* Recompute the physical Durable Object alarm from job-queue state.
*
* Concurrent requests are serialized so a later durable-state change cannot
* be overwritten by an earlier alarm calculation. Queue mutations call this
* automatically; it stays public for composition roots and tests.
*/
async rearmAlarm() {
if (_classPrivateFieldGet2(_alarmsDisabled, this)) return;
if (_classPrivateFieldGet2(_status, this) === "starting") {
_classPrivateFieldSet2(_rearmRequestedDuringStart, this, true);
return;
}
const next = _classPrivateFieldGet2(_alarmRearmQueue, this).catch(() => {}).then(async () => {
if (_classPrivateFieldGet2(_alarmsDisabled, this)) return;
const alarm = _classPrivateFieldGet2(_jobQueue, this).nextAlarmTime(Date.now());
if (alarm === null) await _classPrivateFieldGet2(_ctx, this).storage.deleteAlarm();
else await _classPrivateFieldGet2(_ctx, this).storage.setAlarm(alarm);
});
_classPrivateFieldSet2(_alarmRearmQueue, this, next);
await next;
}
/**
* Keep work a job handed off at a bounded return inside the current
* alarm's memory-limit breaker domain (#1825). Hosts call this where a
* queue-driven callback detaches long work and returns.
*
* @returns True when called during an alarm invocation; false otherwise.
*/
trackAlarmWork(work) {
return _classPrivateFieldGet2(_jobDriver, this).trackAlarmWork(work);
}
/** Dispose installed capabilities in reverse registration order. */
async dispose() {
await runWithoutCurrentAgent(() => _classPrivateFieldGet2(_capabilityRunner, this).dispose());
}
/** Permanently disable and clear alarms during explicit object teardown. */
async disableAlarms() {
_classPrivateFieldSet2(_alarmsDisabled, this, true);
await _classPrivateFieldGet2(_alarmRearmQueue, this).catch(() => {});
await _classPrivateFieldGet2(_ctx, this).storage.deleteAlarm();
}
/**
* Run one alarm invocation. The job driver owns the event loop — deadman
* pre-arm, due-job dispatch with retry and deferral policy, the alarm
* memory-limit circuit breaker (#1825) — and re-arms the physical alarm
* from queue state. The host's `onAlarm()` runs after due jobs, inside
* the host invocation boundary.
*/
async alarm() {
await _classPrivateFieldGet2(_jobDriver, this).runAlarm(() => _assertClassBrand(_Lifecycle_brand, this, _ensureInitialized).call(this), () => runInLifecycleHostContext({ host: _classPrivateFieldGet2(_host, this) }, async () => {
await _classPrivateFieldGet2(_host, this).onAlarm?.();
}));
}
};
function _servicesForCapability(capabilityId) {
const lifecycle = this;
const envelope = (payload) => ({
capability: capabilityId,
source: lifecycleRouteTransports.get(lifecycle)?.source,
payload
});
return Object.freeze({
get name() {
return lifecycle.name;
},
className: _classPrivateFieldGet2(_parentClassName, this),
storage: _classPrivateFieldGet2(_ctx, this).storage,
sockets: Object.freeze({
accept: (ws, tags) => _classPrivateFieldGet2(_ctx, this).acceptWebSocket(ws, tags),
get: (tag) => _classPrivateFieldGet2(_ctx, this).getWebSockets(tag)
}),
ready: () => _assertClassBrand(_Lifecycle_brand, this, _readyForCapabilityOperation).call(this),
status: () => _classPrivateFieldGet2(_status, this),
jobs: _assertClassBrand(_Lifecycle_brand, this, _jobsForOwner).call(this, capabilityId),
trackAlarmWork: (work) => _classPrivateFieldGet2(_jobDriver, this).trackAlarmWork(work),
runInHostContext: async (fn, scope) => _assertClassBrand(_Lifecycle_brand, this, _runInHostBoundary).call(this, fn, scope),
events: Object.freeze({ emit: (type, payload) => _assertClassBrand(_Lifecycle_brand, this, _emitCapabilityEvent).call(this, {
source: capabilityId,
type,
payload
}) }),
routes: Object.freeze({
get source() {
return lifecycleRouteTransports.get(lifecycle)?.source;
},
toRoot: (payload) => {
const transport = lifecycleRouteTransports.get(lifecycle);
return transport ? transport.toRoot(envelope(payload)) : _assertClassBrand(_Lifecycle_brand, this, _dispatchRoute).call(this, envelope(payload));
},
to: (target, payload) => {
const transport = lifecycleRouteTransports.get(lifecycle);
if (!transport) throw new Error("Lifecycle has no transport for routed capabilities");
return transport.to(target, envelope(payload));
}
})
});
}
/**
* Run a user callback inside the host invocation boundary — plain host
* context by default, or the composition root's substitute (Agent installs
* its tracing invocation scope).
*/
function _runInHostBoundary(fn, scope) {
const boundary = lifecycleHostInvokers.get(this);
return Promise.resolve(boundary ? boundary(fn, scope) : runInLifecycleHostContext({
host: _classPrivateFieldGet2(_host, this),
connection: scope?.connection,
request: scope?.request
}, fn));
}
async function _readyForCapabilityOperation() {
if (_classPrivateFieldGet2(_status, this) === "starting" || _classPrivateFieldGet2(_status, this) === "started") return;
await this.start();
}
async function _dispatchRoute(envelope) {
await _assertClassBrand(_Lifecycle_brand, this, _ensureInitialized).call(this);
return runWithoutCurrentAgent(() => _classPrivateFieldGet2(_capabilityRunner, this).route(envelope.capability, {
source: envelope.source,
payload: envelope.payload
}));
}
function _emitCapabilityEvent(event) {
if (event.source.trim() === "" || event.type.trim() === "") throw new Error("Lifecycle events require non-empty source and type");
if (_classPrivateFieldGet2(_status, this) !== "started") {
_classPrivateFieldGet2(_pendingEvents, this).push(event);
return;
}
_assertClassBrand(_Lifecycle_brand, this, _publishCapabilityEvent).call(this, event);
}
function _publishCapabilityEvent(event) {
runWithoutCurrentAgent(() => {
const sink = lifecycleEventSinks.get(this);
try {
if (!sink) {
publishDiagnosticsEvent({
source: event.source,
type: event.type,
agent: _classPrivateFieldGet2(_parentClassName, this),
name: this.name,
payload: event.payload,
timestamp: Date.now()
});
return;
}
const pending = sink(event);
if (pending !== void 0) _classPrivateFieldGet2(_ctx, this).waitUntil(Promise.resolve(pending).catch((error) => {
_assertClassBrand(_Lifecycle_brand, this, _reportEventSinkFailure).call(this, event, error);
}));
} catch (error) {
_assertClassBrand(_Lifecycle_brand, this, _reportEventSinkFailure).call(this, event, error);
}
});
}
function _reportEventSinkFailure(event, error) {
console.error(`Lifecycle event sink failed for ${event.source}:${event.type}`, error);
}
function _deliverPendingEvents() {
for (const event of _classPrivateFieldGet2(_pendingEvents, this).splice(0)) _assertClassBrand(_Lifecycle_brand, this, _publishCapabilityEvent).call(this, event);
}
async function _ensureInitialized() {
if (_classPrivateFieldGet2(_status, this) === "started") return;
if (_classPrivateFieldGet2(_ctx, this).id.name === void 0 && _classPrivateFieldGet2(_legacyName, this) === void 0) _classPrivateFieldSet2(_legacyName, this, await _classPrivateFieldGet2(_ctx, this).storage.get(LEGACY_NAME_STORAGE_KEY));
this.name;
_classPrivateFieldSet2(_capabilitiesLocked, this, true);
let error;
await _classPrivateFieldGet2(_ctx, this).blockConcurrencyWhile(async () => {
_classPrivateFieldSet2(_status, this, "starting");
try {
await runWithoutCurrentAgent(() => _classPrivateFieldGet2(_capabilityRunner, this).start({ props: _classPrivateFieldGet2(_props, this) }));
await runInLifecycleHostContext({ host: _classPrivateFieldGet2(_host, this) }, () => _classPrivateFieldGet2(_host, this).onStart?.(_classPrivateFieldGet2(_props, this)));
_classPrivateFieldSet2(_status, this, "started");
} catch (cause) {
_classPrivateFieldSet2(_status, this, "zero");
error = cause;
}
});
if (error) {
_classPrivateFieldSet2(_rearmRequestedDuringStart, this, false);
_classPrivateFieldGet2(_pendingEvents, this).length = 0;
throw error;
}
_assertClassBrand(_Lifecycle_brand, this, _deliverPendingEvents).call(this);
if (_classPrivateFieldGet2(_rearmRequestedDuringStart, this)) {
_classPrivateFieldSet2(_rearmRequestedDuringStart, this, false);
await this.rearmAlarm();
}
}
function _jobsForOwner(owner) {
const rearmAfter = async (mutate) => {
const result = mutate();
await this.rearmAlarm();
return result;
};
return Object.freeze({
push: (options) => rearmAfter(() => _classPrivateFieldGet2(_jobQueue, this).push(owner, options)),
cancel: (id) => rearmAfter(() => _classPrivateFieldGet2(_jobQueue, this).cancel(owner, id)),
reschedule: (id, time) => rearmAfter(() => _classPrivateFieldGet2(_jobQueue, this).reschedule(owner, id, time)),
get: (id) => _classPrivateFieldGet2(_jobQueue, this).get(owner, id),
list: () => _classPrivateFieldGet2(_jobQueue, this).list(owner),
rearm: () => this.rearmAlarm()
});
}
/**
* Resolve a job owner to its dispatch hooks. Host jobs run inside the
* host invocation boundary; capability jobs run outside ambient host
* context, like every other capability hook.
*/
async function _resolveJobDispatch(owner) {
if (owner === "host") {
const host = _classPrivateFieldGet2(_host, this);
if (!host.onJob) return void 0;
return { onJob: async (context) => runInLifecycleHostContext({ host }, () => host.onJob(context)) };
}
const capability = await _classPrivateFieldGet2(_capabilityRunner, this).findById(owner);
if (!capability?.onJob) return void 0;
return {
onJob: async (context) => runWithoutCurrentAgent(() => capability.onJob(context)),
onJobError: capability.onJobError ? async (context, error) => runWithoutCurrentAgent(() => capability.onJobError(context, error)) : void 0
};
}
//#endregion
export { abortWithoutAlarmRetry as a, setLifecycleRouteTransport as i, setLifecycleEventSink as n, setLifecycleHostInvoker as r, Lifecycle as t };
//# sourceMappingURL=lifecycle-Mm_jQh7r.js.map