@workerdeck/queue 0.12.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/build/index.mjs +11 -4
- package/build/index.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -100,6 +100,17 @@ Per-request, `CreateJobRequest` adds `attempts`, `retryDelayMs`, `maxTokens`, `m
|
|
|
100
100
|
(the stricter of request and queue limits wins), `webhook.progress: 'completion'` to quiet
|
|
101
101
|
progress deliveries, and free-form `meta`.
|
|
102
102
|
|
|
103
|
+
## Rules you cannot infer from the types
|
|
104
|
+
|
|
105
|
+
- **`claimNext` must be atomic, and must skip a future `nextRunAt`.** Those two properties are the
|
|
106
|
+
entire contract a custom `QueueAdapter` has to hold; get either wrong and a job runs twice or a
|
|
107
|
+
retry runs early.
|
|
108
|
+
- **Jobs are one-shot, but parking is not failure.** A run that parks frees its concurrency slot
|
|
109
|
+
and stops its duration clock — the watchdog must not kill work that is waiting on a deferred
|
|
110
|
+
result by design.
|
|
111
|
+
- **Retention is not optional with the bundled adapter.** In-memory terminal jobs accumulate for
|
|
112
|
+
the life of the process; set `retention` or accept the growth knowingly.
|
|
113
|
+
|
|
103
114
|
## License
|
|
104
115
|
|
|
105
116
|
MIT © Tobias Strebitzer — see
|
package/build/index.mjs
CHANGED
|
@@ -117,7 +117,6 @@ var JobQueue = class {
|
|
|
117
117
|
async submit(request) {
|
|
118
118
|
if (this.#closed) throw new Error("queue is closed");
|
|
119
119
|
if (!request.session?.prompt?.trim()) throw new Error("session.prompt is required");
|
|
120
|
-
if (!request.session.cwd) throw new Error("session.cwd is required");
|
|
121
120
|
if (request.session.resume || request.session.forkSession) throw new Error("resume/forkSession are not supported for queued jobs");
|
|
122
121
|
const attempts = request.attempts ?? 1;
|
|
123
122
|
if (!Number.isInteger(attempts) || attempts < 1) throw new Error("attempts must be a positive integer");
|
|
@@ -125,7 +124,7 @@ var JobQueue = class {
|
|
|
125
124
|
const info = {
|
|
126
125
|
id: randomUUID(),
|
|
127
126
|
status: "queued",
|
|
128
|
-
cwd: request.session.cwd,
|
|
127
|
+
cwd: request.session.cwd ?? "",
|
|
129
128
|
profile: request.session.profile,
|
|
130
129
|
prompt: request.session.prompt,
|
|
131
130
|
createdAt: Date.now(),
|
|
@@ -136,7 +135,8 @@ var JobQueue = class {
|
|
|
136
135
|
totalCostUsd: 0,
|
|
137
136
|
numTurns: 0
|
|
138
137
|
},
|
|
139
|
-
meta: request.meta
|
|
138
|
+
meta: request.meta,
|
|
139
|
+
scope: request.session.scope
|
|
140
140
|
};
|
|
141
141
|
const record = {
|
|
142
142
|
info,
|
|
@@ -333,7 +333,14 @@ var JobQueue = class {
|
|
|
333
333
|
const build = this.#options.buildRunnerConfig ?? ((req) => req);
|
|
334
334
|
let runner;
|
|
335
335
|
try {
|
|
336
|
-
|
|
336
|
+
const request = {
|
|
337
|
+
...record.request.session,
|
|
338
|
+
meta: {
|
|
339
|
+
...record.request.session.meta,
|
|
340
|
+
jobId: id
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
runner = await this.#options.createRunner(build(request));
|
|
337
344
|
} catch (error) {
|
|
338
345
|
const failed = await this.#adapter.update(id, {
|
|
339
346
|
status: "failed",
|
package/build/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["#jobs","#dailyTokens","#options","#adapter","#offWork","#pump","#sweepTimer","#sweep","#closed","#emit","#bySession","#running","#parked","#kill","#recordPark","#effectiveDurationLimit","#handleEvent","#recordResume","#finalize","#retryTimers","#pumping","#start","#effectiveTokenLimit","#progress","#deliver"],"sources":["../src/adapter.ts","../src/queue.ts"],"sourcesContent":["import type { CreateJobRequest, JobInfo } from '@workerdeck/protocol'\n\n/** A job as the adapter stores it: the wire-visible info plus the original request. */\nexport type JobRecord = {\n info: JobInfo\n request: CreateJobRequest\n}\n\n/**\n * Storage + claiming contract the JobQueue runs against. The bundled implementation\n * is {@link InMemoryQueueAdapter}; redis/bullmq/pubsub adapters implement the same\n * interface. Everything is Promise-based so remote backends fit without changing the\n * queue, and `claimNext` is the one operation that must be atomic across workers\n * (two concurrent claims must never return the same job).\n */\nexport interface QueueAdapter {\n /** Persist a newly submitted job (status 'queued'). */\n add(job: JobRecord): Promise<void>\n /**\n * Atomically claim the oldest claimable queued job, transitioning it to 'running'.\n * A queued job whose `nextRunAt` is in the future (retry backoff) is not claimable\n * yet. Returns null when nothing is claimable.\n */\n claimNext(): Promise<JobRecord | null>\n get(id: string): Promise<JobRecord | null>\n /** All known jobs, oldest first. Backends may cap retention of terminal jobs. */\n list(): Promise<JobRecord[]>\n /** Merge a partial info patch into a job (a key explicitly set to undefined clears\n * that field). Returns the updated record, or null if unknown. */\n update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null>\n /** Delete terminal jobs (succeeded/failed/canceled) that finished more than\n * `olderThanMs` ago. Returns how many were removed. */\n prune(olderThanMs: number): Promise<number>\n /**\n * Add tokens to a day's global counter and return the new total. `dayKey` is a UTC\n * 'YYYY-MM-DD'; keeping the counter in the adapter makes daily budgets hold across\n * multiple workers sharing a backend.\n */\n addDailyTokens(dayKey: string, tokens: number): Promise<number>\n dailyTokens(dayKey: string): Promise<number>\n /**\n * Optional: notify the queue that work may be available (a job added by another\n * producer on a shared backend). The bundled queue also pumps after its own\n * submits/completions, so purely local adapters can omit this.\n */\n onWork?(listener: () => void): () => void\n}\n\n/** Reference adapter: single-process, no persistence. Jobs and daily counters are lost\n * on restart — production deployments should back the queue with a shared store. */\nexport class InMemoryQueueAdapter implements QueueAdapter {\n #jobs = new Map<string, JobRecord>()\n #dailyTokens = new Map<string, number>()\n\n add(job: JobRecord): Promise<void> {\n this.#jobs.set(job.info.id, job)\n return Promise.resolve()\n }\n\n claimNext(): Promise<JobRecord | null> {\n const now = Date.now()\n for (const job of this.#jobs.values()) {\n if (\n job.info.status === 'queued' &&\n (job.info.nextRunAt === undefined || job.info.nextRunAt <= now)\n ) {\n job.info = { ...job.info, status: 'running' }\n return Promise.resolve(job)\n }\n }\n return Promise.resolve(null)\n }\n\n get(id: string): Promise<JobRecord | null> {\n return Promise.resolve(this.#jobs.get(id) ?? null)\n }\n\n list(): Promise<JobRecord[]> {\n return Promise.resolve([...this.#jobs.values()])\n }\n\n update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null> {\n const job = this.#jobs.get(id)\n if (!job) return Promise.resolve(null)\n job.info = { ...job.info, ...patch }\n return Promise.resolve(job)\n }\n\n prune(olderThanMs: number): Promise<number> {\n const cutoff = Date.now() - olderThanMs\n let removed = 0\n for (const [id, job] of this.#jobs) {\n const { status, finishedAt } = job.info\n const terminal = status === 'succeeded' || status === 'failed' || status === 'canceled'\n if (terminal && (finishedAt ?? 0) <= cutoff) {\n this.#jobs.delete(id)\n removed++\n }\n }\n return Promise.resolve(removed)\n }\n\n addDailyTokens(dayKey: string, tokens: number): Promise<number> {\n const next = (this.#dailyTokens.get(dayKey) ?? 0) + tokens\n this.#dailyTokens.set(dayKey, next)\n return Promise.resolve(next)\n }\n\n dailyTokens(dayKey: string): Promise<number> {\n return Promise.resolve(this.#dailyTokens.get(dayKey) ?? 0)\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { Runner, SessionRunnerConfig } from '@workerdeck/core'\nimport type {\n ApiMessage,\n CreateJobRequest,\n CreateSessionRequest,\n JobEvent,\n JobInfo,\n JobProgress,\n QueueStats,\n SessionEvent,\n} from '@workerdeck/protocol'\nimport { InMemoryQueueAdapter, type JobRecord, type QueueAdapter } from './adapter.ts'\n\nexport type JobQueueOptions = {\n /** Turn a session config into a live runner — typically the server registry's create(),\n * so job sessions are ordinary sessions clients can attach to and watch. May be\n * async: engines whose assembly awaits (a provider session's MCP connect) resolve\n * here, and a rejection fails the job like any other start error. */\n createRunner: (config: SessionRunnerConfig) => Runner | Promise<Runner>\n /** Storage/claiming backend. Defaults to the in-memory adapter (single process). */\n adapter?: QueueAdapter\n /** Concurrent job sessions. Default 1. */\n maxConcurrency?: number\n /** Token cap per job session; exceeding it interrupts the run and fails the job. */\n sessionTokenLimit?: number\n /** Global token budget per UTC day; when exhausted, queued jobs are held until the\n * day rolls over (running jobs finish and are accounted). */\n dailyTokenLimit?: number\n /** Wall-clock cap per job run; exceeding it interrupts the run and fails the job.\n * The watchdog for stuck CLIs — without it, a run that never yields a result keeps\n * its job (and concurrency slot) forever. Time spent parked on a deferred\n * execution does not count against it: the run isn't stuck, it's waiting. */\n maxJobDurationMs?: number\n /** Cap on time parked on a deferred execution, across all parks of one run.\n * Exceeding it fails the job (the execution's own watchdog, when the backend set\n * one, usually fires first and lets the agent adapt instead). Unset = unbounded. */\n maxParkedDurationMs?: number\n /** How long a killed run (token/duration limit) may wind down after interrupt()\n * before the queue force-finalizes it and closes the session. Default 5000. */\n killGraceMs?: number\n /** Expire terminal jobs: prune those finished more than `maxAgeMs` ago, sweeping\n * every `sweepIntervalMs` (default min(maxAgeMs, 60s)) and after each completion.\n * Unset = keep forever (the in-memory adapter then grows unboundedly). */\n retention?: { maxAgeMs: number; sweepIntervalMs?: number }\n /** Patch job session configs (inject queryFn, env, tool policy) before they run. */\n buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig\n /**\n * Drop a parked session's persisted state: the run ended (cancel, kill, retry)\n * while parked, so nothing will ever rehydrate it. The host that parks sessions\n * — {@link JobQueue.onSessionParking}'s caller — wires this to its session store.\n */\n discardSession?: (sessionId: string) => void | Promise<void>\n /** Webhook transport. Defaults to global fetch. */\n fetchImpl?: typeof fetch\n /** Webhook delivery attempts per event (exponential backoff). Default 3. */\n webhookAttempts?: number\n /** Initial backoff between webhook attempts. Default 500ms. */\n webhookRetryDelayMs?: number\n /** Local observer invoked for every job event (in addition to any webhook). */\n onEvent?: (event: JobEvent) => void\n}\n\ntype RunningJob = {\n record: JobRecord\n /** Rebuilt on every resume: a rehydrated session is a NEW runner object under\n * the same session id, so nothing may hold the old reference. */\n runner: Runner\n unsubscribe: () => void\n /** Highest event seq seen. A resumed run re-subscribes from here, so the parked\n * session's replayed log doesn't re-deliver a turn the queue already acted on. */\n lastSeq: number\n /** Mid-run token estimate from assistant-message usage (enforcement + progress). */\n estimatedTokens: number\n /** Set when the queue killed the run (limits, cancel) — decides the terminal status. */\n killReason?: string\n canceled: boolean\n finalized: boolean\n /** Per-job webhook chain so deliveries stay ordered. */\n deliveries: Promise<void>\n /** Watchdog: fires killReason when the run exceeds its wall-clock cap. */\n durationTimer?: ReturnType<typeof setTimeout>\n /** Backstop after a kill: force-finalizes if interrupt() never yields a result. */\n forceTimer?: ReturnType<typeof setTimeout>\n /** Wall-clock actually spent running (parked stretches excluded), plus when the\n * current running leg started. Together they bound the duration watchdog across\n * any number of parks. */\n runningMs: number\n legStartedAt: number\n /** Set while parked: when it parked and what it waits on. */\n parkedAt?: number\n parkedExecutionId?: string\n /** Watchdog on total parked time (maxParkedDurationMs). */\n parkTimer?: ReturnType<typeof setTimeout>\n parkedMs: number\n}\n\nconst dayKey = (epochMs: number): string => new Date(epochMs).toISOString().slice(0, 10)\n\nconst sumUsage = (usage: unknown): number => {\n if (typeof usage !== 'object' || usage === null) return 0\n const u = usage as Record<string, unknown>\n return (\n (typeof u.input_tokens === 'number' ? u.input_tokens : 0) +\n (typeof u.output_tokens === 'number' ? u.output_tokens : 0) +\n (typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : 0) +\n (typeof u.cache_read_input_tokens === 'number' ? u.cache_read_input_tokens : 0)\n )\n}\n\nconst textPreview = (message: ApiMessage, max = 140): JobProgress | null => {\n const blocks = typeof message.content === 'string'\n ? [{ type: 'text', text: message.content }]\n : message.content\n for (const block of blocks) {\n if (block.type === 'tool_use') {\n return { kind: 'tool_use', preview: (block as { name?: string }).name }\n }\n if (block.type === 'text') {\n const text = (block as { text?: string }).text ?? ''\n if (text.trim()) {\n return {\n kind: 'assistant_text',\n preview: text.length > max ? text.slice(0, max - 1) + '…' : text,\n }\n }\n }\n }\n return null\n}\n\n/**\n * One-shot job execution over the session runner: submitted jobs run `session.prompt`\n * unattended, bounded by `maxConcurrency` and token budgets, and report progress and\n * completion through webhooks (plus `onEvent` locally). Job state lives in the\n * {@link QueueAdapter}; this class owns scheduling and the live runs.\n */\nexport class JobQueue {\n #options: JobQueueOptions\n #adapter: QueueAdapter\n #running = new Map<string, RunningJob>()\n /** Runs waiting on a deferred execution: alive, but holding no concurrency slot\n * and no live runner. Keyed by job id like `#running`. */\n #parked = new Map<string, RunningJob>()\n #pumping = false\n #closed = false\n #offWork: (() => void) | undefined\n #sweepTimer: ReturnType<typeof setInterval> | undefined\n /** Pending retry-backoff wakeups, cleared on close(). */\n #retryTimers = new Set<ReturnType<typeof setTimeout>>()\n\n constructor(options: JobQueueOptions) {\n this.#options = options\n this.#adapter = options.adapter ?? new InMemoryQueueAdapter()\n this.#offWork = this.#adapter.onWork?.(() => void this.#pump())\n const retention = options.retention\n if (retention) {\n const interval = retention.sweepIntervalMs ?? Math.min(retention.maxAgeMs, 60_000)\n this.#sweepTimer = setInterval(() => this.#sweep(), interval)\n this.#sweepTimer.unref?.()\n }\n }\n\n async submit(request: CreateJobRequest): Promise<JobInfo> {\n if (this.#closed) throw new Error('queue is closed')\n if (!request.session?.prompt?.trim()) throw new Error('session.prompt is required')\n if (!request.session.cwd) throw new Error('session.cwd is required')\n if (request.session.resume || request.session.forkSession) {\n throw new Error('resume/forkSession are not supported for queued jobs')\n }\n const attempts = request.attempts ?? 1\n if (!Number.isInteger(attempts) || attempts < 1) {\n throw new Error('attempts must be a positive integer')\n }\n if (request.retryDelayMs !== undefined && !(request.retryDelayMs >= 0)) {\n throw new Error('retryDelayMs must be >= 0')\n }\n const info: JobInfo = {\n id: randomUUID(),\n status: 'queued',\n cwd: request.session.cwd,\n profile: request.session.profile,\n prompt: request.session.prompt,\n createdAt: Date.now(),\n attempt: 1,\n maxAttempts: attempts,\n usage: { tokens: 0, totalCostUsd: 0, numTurns: 0 },\n meta: request.meta,\n }\n const record: JobRecord = { info, request }\n await this.#adapter.add(record)\n this.#emit(record, { type: 'job_submitted', job: info, ts: Date.now() }, undefined, {\n skipWebhook: true,\n })\n void this.#pump()\n return info\n }\n\n async get(id: string): Promise<JobInfo | null> {\n return (await this.#adapter.get(id))?.info ?? null\n }\n\n async list(): Promise<JobInfo[]> {\n return (await this.#adapter.list()).map((j) => j.info)\n }\n\n /**\n * A run's session is about to be parked on a deferred execution: the host has\n * snapshotted it and is tearing the live runner down. The queue drops its\n * subscription to the doomed runner, frees the concurrency slot, and stops the\n * duration clock.\n *\n * Returns false when the queue refuses the park — the run is already finalizing\n * or has been killed, so the host must leave the session alone. A session that\n * belongs to no job accepts trivially (there is nothing to account for).\n */\n onSessionParking(sessionId: string, executionId: string): boolean {\n const job = this.#bySession(this.#running, sessionId)\n if (!job) return true\n if (job.finalized || job.killReason) return false\n const now = Date.now()\n job.unsubscribe()\n job.runningMs += now - job.legStartedAt\n clearTimeout(job.durationTimer)\n job.durationTimer = undefined\n job.parkedAt = now\n job.parkedExecutionId = executionId\n this.#running.delete(job.record.info.id)\n this.#parked.set(job.record.info.id, job)\n const parkedLimit = this.#options.maxParkedDurationMs\n if (parkedLimit !== undefined) {\n job.parkTimer = setTimeout(\n () => this.#kill(job, `job exceeded max parked duration (${parkedLimit}ms)`),\n Math.max(0, parkedLimit - job.parkedMs),\n )\n job.parkTimer.unref?.()\n }\n void this.#recordPark(job, executionId)\n return true\n }\n\n async #recordPark(job: RunningJob, executionId: string): Promise<void> {\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'parked',\n parkedAt: job.parkedAt,\n parkedExecutionId: executionId,\n })\n if (updated) job.record = updated\n this.#emit(\n job.record,\n { type: 'job_parked', job: job.record.info, executionId, ts: Date.now() },\n job,\n )\n // The slot is free now — let a queued job take it.\n void this.#pump()\n }\n\n /**\n * The parked session was rehydrated (same session id, new runner object) because\n * its execution's result arrived. Re-subscribe and restart the clock with the\n * budget the run had left.\n *\n * A resume takes its slot back immediately, so a burst of resumes can transiently\n * exceed `maxConcurrency` — the alternative would be holding a result the agent\n * loop has already been handed.\n */\n onSessionResumed(sessionId: string, runner: Runner): void {\n const job = this.#bySession(this.#parked, sessionId)\n if (!job || job.finalized) return\n const now = Date.now()\n const executionId = job.parkedExecutionId ?? ''\n clearTimeout(job.parkTimer)\n job.parkTimer = undefined\n job.parkedMs += now - (job.parkedAt ?? now)\n job.parkedAt = undefined\n job.parkedExecutionId = undefined\n job.legStartedAt = now\n job.runner = runner\n this.#parked.delete(job.record.info.id)\n this.#running.set(job.record.info.id, job)\n const durationLimit = this.#effectiveDurationLimit(job.record.request)\n if (durationLimit !== undefined) {\n job.durationTimer = setTimeout(\n () => this.#kill(job, `job exceeded max duration (${durationLimit}ms)`),\n Math.max(0, durationLimit - job.runningMs),\n )\n job.durationTimer.unref?.()\n }\n // From lastSeq, not 0: the rehydrated runner replays the whole persisted log,\n // and re-handling those events would double-count tokens and could re-finalize\n // the job on an old turn_result.\n job.unsubscribe = runner.subscribe((event) => void this.#handleEvent(job, event), job.lastSeq)\n void this.#recordResume(job, executionId)\n }\n\n async #recordResume(job: RunningJob, executionId: string): Promise<void> {\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'running',\n parkedAt: undefined,\n parkedExecutionId: undefined,\n })\n if (updated) job.record = updated\n this.#emit(\n job.record,\n { type: 'job_resumed', job: job.record.info, executionId, ts: Date.now() },\n job,\n )\n }\n\n #bySession(jobs: Map<string, RunningJob>, sessionId: string): RunningJob | undefined {\n for (const job of jobs.values()) {\n if (job.record.info.sessionId === sessionId) return job\n }\n return undefined\n }\n\n /** Cancel a queued, running, or parked job. Returns the job, or null if unknown. */\n async cancel(id: string): Promise<JobInfo | null> {\n const record = await this.#adapter.get(id)\n if (!record) return null\n const running = this.#running.get(id) ?? this.#parked.get(id)\n if (running) {\n running.canceled = true\n running.killReason = 'canceled'\n await this.#finalize(running, {\n usage: { tokens: running.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: 'canceled',\n error: 'canceled',\n })\n return running.record.info\n }\n if (record.info.status !== 'queued') return record.info\n const updated = await this.#adapter.update(id, {\n status: 'canceled',\n finishedAt: Date.now(),\n error: 'canceled',\n })\n if (updated) this.#emit(updated, { type: 'job_completed', job: updated.info, ts: Date.now() })\n return updated?.info ?? null\n }\n\n async stats(): Promise<QueueStats> {\n const jobs = await this.#adapter.list()\n const dailyTokensUsed = await this.#adapter.dailyTokens(dayKey(Date.now()))\n const dailyTokenLimit = this.#options.dailyTokenLimit\n return {\n maxConcurrency: this.#options.maxConcurrency ?? 1,\n running: this.#running.size,\n parked: this.#parked.size,\n queued: jobs.filter((j) => j.info.status === 'queued').length,\n sessionTokenLimit: this.#options.sessionTokenLimit,\n dailyTokenLimit,\n dailyTokensUsed,\n paused: dailyTokenLimit !== undefined && dailyTokensUsed >= dailyTokenLimit,\n }\n }\n\n /** Stop scheduling new jobs. Running jobs keep finalizing (e.g. when the host closes\n * their sessions); job state stays in the adapter. */\n close(): void {\n this.#closed = true\n this.#offWork?.()\n clearInterval(this.#sweepTimer)\n for (const timer of this.#retryTimers) clearTimeout(timer)\n this.#retryTimers.clear()\n }\n\n #sweep(): void {\n const retention = this.#options.retention\n if (!retention) return\n this.#adapter.prune(retention.maxAgeMs).catch(() => {\n // sweep failures must not break the queue; the next sweep retries\n })\n }\n\n async #pump(): Promise<void> {\n if (this.#pumping || this.#closed) return\n this.#pumping = true\n try {\n const maxConcurrency = this.#options.maxConcurrency ?? 1\n while (this.#running.size < maxConcurrency) {\n const limit = this.#options.dailyTokenLimit\n if (limit !== undefined && (await this.#adapter.dailyTokens(dayKey(Date.now()))) >= limit) {\n return\n }\n const record = await this.#adapter.claimNext()\n if (!record) return\n await this.#start(record)\n }\n } finally {\n this.#pumping = false\n }\n }\n\n async #start(record: JobRecord): Promise<void> {\n const id = record.info.id\n const build = this.#options.buildRunnerConfig ?? ((req: CreateSessionRequest) => req)\n let runner: Runner\n try {\n runner = await this.#options.createRunner(build(record.request.session))\n } catch (error) {\n const failed = await this.#adapter.update(id, {\n status: 'failed',\n finishedAt: Date.now(),\n error: error instanceof Error ? error.message : String(error),\n })\n if (failed) this.#emit(failed, { type: 'job_completed', job: failed.info, ts: Date.now() })\n return\n }\n const job: RunningJob = {\n record,\n runner,\n unsubscribe: () => {},\n lastSeq: 0,\n estimatedTokens: 0,\n canceled: false,\n finalized: false,\n deliveries: Promise.resolve(),\n runningMs: 0,\n legStartedAt: Date.now(),\n parkedMs: 0,\n }\n this.#running.set(id, job)\n const updated = await this.#adapter.update(id, {\n startedAt: Date.now(),\n sessionId: runner.id,\n })\n if (updated) job.record = updated\n this.#emit(job.record, { type: 'job_started', job: job.record.info, ts: Date.now() })\n const durationLimit = this.#effectiveDurationLimit(record.request)\n if (durationLimit !== undefined) {\n job.durationTimer = setTimeout(\n () => this.#kill(job, `job exceeded max duration (${durationLimit}ms)`),\n durationLimit,\n )\n job.durationTimer.unref?.()\n }\n job.unsubscribe = runner.subscribe((event) => void this.#handleEvent(job, event))\n }\n\n /** Kill a run: interrupt it and, if the CLI never yields a result (stuck process),\n * force-finalize after the grace period so the job can't hang forever. */\n #kill(job: RunningJob, reason: string): void {\n if (job.finalized || job.killReason) return\n job.killReason = reason\n if (job.parkedAt !== undefined) {\n // Parked: there is no live runner to interrupt and no result coming, so the\n // grace period would just be dead time.\n void this.#finalize(job, {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error: reason,\n })\n return\n }\n void job.runner.interrupt().catch(() => {})\n job.forceTimer = setTimeout(() => {\n void this.#finalize(job, {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error: reason,\n })\n }, this.#options.killGraceMs ?? 5000)\n job.forceTimer.unref?.()\n }\n\n async #handleEvent(job: RunningJob, event: SessionEvent): Promise<void> {\n if (job.finalized) return\n job.lastSeq = Math.max(job.lastSeq, event.seq)\n switch (event.type) {\n case 'system_init':\n await this.#adapter.update(job.record.info.id, { sdkSessionId: event.sdkSessionId })\n return\n case 'assistant_message': {\n if (event.replay) return\n job.estimatedTokens += sumUsage(event.message.usage)\n const limit = this.#effectiveTokenLimit(job.record.request)\n if (limit !== undefined && job.estimatedTokens > limit) {\n this.#kill(job, `session token limit exceeded (${job.estimatedTokens} > ${limit})`)\n }\n const progress = textPreview(event.message)\n if (progress) this.#progress(job, progress)\n return\n }\n case 'permission_requested':\n // The full request rides along so webhook consumers can answer it over REST\n // (questions, approvals) instead of only seeing a preview string.\n this.#progress(job, {\n kind: 'permission_requested',\n preview: event.request.title ?? event.request.toolName,\n request: event.request,\n })\n return\n case 'permission_resolved':\n this.#progress(job, { kind: 'permission_resolved', preview: event.behavior })\n return\n case 'turn_result': {\n // One job = one unattended run: the first result is the outcome.\n const tokens = sumUsage(event.usage) || job.estimatedTokens\n await this.#finalize(job, {\n usage: {\n tokens,\n totalCostUsd: event.totalCostUsd,\n numTurns: event.numTurns,\n },\n result: {\n subtype: event.subtype,\n isError: event.isError,\n result: event.result,\n errors: event.errors,\n durationMs: event.durationMs,\n },\n status: job.killReason\n ? (job.canceled ? 'canceled' : 'failed')\n : event.isError\n ? 'failed'\n : 'succeeded',\n error: job.killReason ?? (event.isError ? (event.errors?.join('; ') || event.subtype) : undefined),\n })\n return\n }\n case 'session_error':\n await this.#finalize(job, {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error: job.killReason ?? event.message,\n })\n return\n case 'session_closed':\n await this.#finalize(job, {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error: job.killReason ?? 'session closed before completing',\n })\n return\n default:\n return\n }\n }\n\n #effectiveTokenLimit(request: CreateJobRequest): number | undefined {\n const limits = [request.maxTokens, this.#options.sessionTokenLimit].filter(\n (n): n is number => typeof n === 'number',\n )\n return limits.length > 0 ? Math.min(...limits) : undefined\n }\n\n #effectiveDurationLimit(request: CreateJobRequest): number | undefined {\n const limits = [request.maxDurationMs, this.#options.maxJobDurationMs].filter(\n (n): n is number => typeof n === 'number',\n )\n return limits.length > 0 ? Math.min(...limits) : undefined\n }\n\n /** End the current run. `patch.usage` is this attempt's usage alone — prior attempts'\n * totals live on the stored info and are folded in here. A failed (not canceled) run\n * with attempts left re-queues with backoff instead of completing. */\n async #finalize(job: RunningJob, patch: Partial<JobInfo>): Promise<void> {\n if (job.finalized) return\n job.finalized = true\n job.unsubscribe()\n clearTimeout(job.durationTimer)\n clearTimeout(job.forceTimer)\n clearTimeout(job.parkTimer)\n this.#running.delete(job.record.info.id)\n const wasParked = this.#parked.delete(job.record.info.id)\n job.runner.close('server')\n if (wasParked && job.record.info.sessionId) {\n // The live runner is already gone; what survives the run is the persisted\n // snapshot, and nothing will ever rehydrate it now.\n try {\n void Promise.resolve(this.#options.discardSession?.(job.record.info.sessionId)).catch(\n () => {},\n )\n } catch {\n // discard failures must not break finalization\n }\n }\n const attemptUsage = patch.usage ?? { tokens: 0, totalCostUsd: 0, numTurns: 0 }\n if (attemptUsage.tokens > 0) {\n await this.#adapter.addDailyTokens(dayKey(Date.now()), attemptUsage.tokens)\n }\n const prior = job.record.info.usage\n const usage = {\n tokens: prior.tokens + attemptUsage.tokens,\n totalCostUsd: prior.totalCostUsd + attemptUsage.totalCostUsd,\n numTurns: prior.numTurns + attemptUsage.numTurns,\n }\n const attempt = job.record.info.attempt ?? 1\n const maxAttempts = job.record.request.attempts ?? 1\n if (patch.status === 'failed' && attempt < maxAttempts && !this.#closed) {\n const baseDelay = job.record.request.retryDelayMs ?? 5000\n const delay = baseDelay * 2 ** (attempt - 1)\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'queued',\n attempt: attempt + 1,\n nextRunAt: Date.now() + delay,\n error: patch.error,\n usage,\n sessionId: undefined,\n sdkSessionId: undefined,\n startedAt: undefined,\n result: undefined,\n })\n if (updated) {\n job.record = updated\n this.#emit(updated, { type: 'job_retrying', job: updated.info, ts: Date.now() }, job)\n const timer = setTimeout(() => {\n this.#retryTimers.delete(timer)\n void this.#pump()\n }, delay)\n timer.unref?.()\n this.#retryTimers.add(timer)\n }\n void this.#pump()\n return\n }\n const updated = await this.#adapter.update(job.record.info.id, {\n ...patch,\n usage,\n nextRunAt: undefined,\n finishedAt: Date.now(),\n })\n if (updated) {\n job.record = updated\n // Pass the job so the completion webhook stays ordered behind its progress\n // deliveries (the running-map entry is already gone).\n this.#emit(job.record, { type: 'job_completed', job: updated.info, ts: Date.now() }, job)\n }\n this.#sweep()\n void this.#pump()\n }\n\n #progress(job: RunningJob, progress: JobProgress): void {\n const event: JobEvent = { type: 'job_progress', job: job.record.info, progress, ts: Date.now() }\n // 'completion' granularity: local observers still see progress; the webhook doesn't.\n if (job.record.request.webhook?.progress === 'completion') {\n try {\n this.#options.onEvent?.(event)\n } catch {\n // observer errors must not break the queue\n }\n return\n }\n this.#emit(job.record, event, job)\n }\n\n /** Notify the local observer and, when configured, the job's webhook (ordered per job). */\n #emit(\n record: JobRecord,\n event: JobEvent,\n chainOwner?: RunningJob,\n { skipWebhook = false }: { skipWebhook?: boolean } = {},\n ): void {\n try {\n this.#options.onEvent?.(event)\n } catch {\n // observer errors must not break the queue\n }\n const webhook = record.request.webhook\n if (!webhook || skipWebhook) return\n const running = chainOwner ?? this.#running.get(record.info.id)\n const deliver = () => this.#deliver(webhook.url, webhook.headers, event)\n if (running) running.deliveries = running.deliveries.then(deliver)\n else void deliver()\n }\n\n async #deliver(\n url: string,\n headers: Record<string, string> | undefined,\n event: JobEvent,\n ): Promise<void> {\n const fetchImpl = this.#options.fetchImpl ?? fetch\n const attempts = this.#options.webhookAttempts ?? 3\n const baseDelay = this.#options.webhookRetryDelayMs ?? 500\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...headers },\n body: JSON.stringify(event),\n })\n if (res.ok) return\n } catch {\n // network error — retry below\n }\n if (attempt < attempts - 1) {\n await new Promise((resolve) => setTimeout(resolve, baseDelay * 2 ** attempt))\n }\n }\n // Deliveries are best-effort; clients can always poll GET /jobs/:id.\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["#jobs","#dailyTokens","#options","#adapter","#offWork","#pump","#sweepTimer","#sweep","#closed","#emit","#bySession","#running","#parked","#kill","#recordPark","#effectiveDurationLimit","#handleEvent","#recordResume","#finalize","#retryTimers","#pumping","#start","#effectiveTokenLimit","#progress","#deliver"],"sources":["../src/adapter.ts","../src/queue.ts"],"sourcesContent":["import type { CreateJobRequest, JobInfo } from '@workerdeck/protocol'\n\n/** A job as the adapter stores it: the wire-visible info plus the original request. */\nexport type JobRecord = {\n info: JobInfo\n request: CreateJobRequest\n}\n\n/**\n * Storage + claiming contract the JobQueue runs against. The bundled implementation\n * is {@link InMemoryQueueAdapter}; redis/bullmq/pubsub adapters implement the same\n * interface. Everything is Promise-based so remote backends fit without changing the\n * queue, and `claimNext` is the one operation that must be atomic across workers\n * (two concurrent claims must never return the same job).\n */\nexport interface QueueAdapter {\n /** Persist a newly submitted job (status 'queued'). */\n add(job: JobRecord): Promise<void>\n /**\n * Atomically claim the oldest claimable queued job, transitioning it to 'running'.\n * A queued job whose `nextRunAt` is in the future (retry backoff) is not claimable\n * yet. Returns null when nothing is claimable.\n */\n claimNext(): Promise<JobRecord | null>\n get(id: string): Promise<JobRecord | null>\n /** All known jobs, oldest first. Backends may cap retention of terminal jobs. */\n list(): Promise<JobRecord[]>\n /** Merge a partial info patch into a job (a key explicitly set to undefined clears\n * that field). Returns the updated record, or null if unknown. */\n update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null>\n /** Delete terminal jobs (succeeded/failed/canceled) that finished more than\n * `olderThanMs` ago. Returns how many were removed. */\n prune(olderThanMs: number): Promise<number>\n /**\n * Add tokens to a day's global counter and return the new total. `dayKey` is a UTC\n * 'YYYY-MM-DD'; keeping the counter in the adapter makes daily budgets hold across\n * multiple workers sharing a backend.\n */\n addDailyTokens(dayKey: string, tokens: number): Promise<number>\n dailyTokens(dayKey: string): Promise<number>\n /**\n * Optional: notify the queue that work may be available (a job added by another\n * producer on a shared backend). The bundled queue also pumps after its own\n * submits/completions, so purely local adapters can omit this.\n */\n onWork?(listener: () => void): () => void\n}\n\n/** Reference adapter: single-process, no persistence. Jobs and daily counters are lost\n * on restart — production deployments should back the queue with a shared store. */\nexport class InMemoryQueueAdapter implements QueueAdapter {\n #jobs = new Map<string, JobRecord>()\n #dailyTokens = new Map<string, number>()\n\n add(job: JobRecord): Promise<void> {\n this.#jobs.set(job.info.id, job)\n return Promise.resolve()\n }\n\n claimNext(): Promise<JobRecord | null> {\n const now = Date.now()\n for (const job of this.#jobs.values()) {\n if (\n job.info.status === 'queued' &&\n (job.info.nextRunAt === undefined || job.info.nextRunAt <= now)\n ) {\n job.info = { ...job.info, status: 'running' }\n return Promise.resolve(job)\n }\n }\n return Promise.resolve(null)\n }\n\n get(id: string): Promise<JobRecord | null> {\n return Promise.resolve(this.#jobs.get(id) ?? null)\n }\n\n list(): Promise<JobRecord[]> {\n return Promise.resolve([...this.#jobs.values()])\n }\n\n update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null> {\n const job = this.#jobs.get(id)\n if (!job) return Promise.resolve(null)\n job.info = { ...job.info, ...patch }\n return Promise.resolve(job)\n }\n\n prune(olderThanMs: number): Promise<number> {\n const cutoff = Date.now() - olderThanMs\n let removed = 0\n for (const [id, job] of this.#jobs) {\n const { status, finishedAt } = job.info\n const terminal = status === 'succeeded' || status === 'failed' || status === 'canceled'\n if (terminal && (finishedAt ?? 0) <= cutoff) {\n this.#jobs.delete(id)\n removed++\n }\n }\n return Promise.resolve(removed)\n }\n\n addDailyTokens(dayKey: string, tokens: number): Promise<number> {\n const next = (this.#dailyTokens.get(dayKey) ?? 0) + tokens\n this.#dailyTokens.set(dayKey, next)\n return Promise.resolve(next)\n }\n\n dailyTokens(dayKey: string): Promise<number> {\n return Promise.resolve(this.#dailyTokens.get(dayKey) ?? 0)\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { Runner, SessionRunnerConfig } from '@workerdeck/core'\nimport type {\n ApiMessage,\n CreateJobRequest,\n CreateSessionRequest,\n JobEvent,\n JobInfo,\n JobProgress,\n QueueStats,\n SessionEvent,\n} from '@workerdeck/protocol'\nimport { InMemoryQueueAdapter, type JobRecord, type QueueAdapter } from './adapter.ts'\n\nexport type JobQueueOptions = {\n /** Turn a session config into a live runner — typically the server registry's create(),\n * so job sessions are ordinary sessions clients can attach to and watch. May be\n * async: engines whose assembly awaits (a provider session's MCP connect) resolve\n * here, and a rejection fails the job like any other start error. */\n createRunner: (config: SessionRunnerConfig) => Runner | Promise<Runner>\n /** Storage/claiming backend. Defaults to the in-memory adapter (single process). */\n adapter?: QueueAdapter\n /** Concurrent job sessions. Default 1. */\n maxConcurrency?: number\n /** Token cap per job session; exceeding it interrupts the run and fails the job. */\n sessionTokenLimit?: number\n /** Global token budget per UTC day; when exhausted, queued jobs are held until the\n * day rolls over (running jobs finish and are accounted). */\n dailyTokenLimit?: number\n /** Wall-clock cap per job run; exceeding it interrupts the run and fails the job.\n * The watchdog for stuck CLIs — without it, a run that never yields a result keeps\n * its job (and concurrency slot) forever. Time spent parked on a deferred\n * execution does not count against it: the run isn't stuck, it's waiting. */\n maxJobDurationMs?: number\n /** Cap on time parked on a deferred execution, across all parks of one run.\n * Exceeding it fails the job (the execution's own watchdog, when the backend set\n * one, usually fires first and lets the agent adapt instead). Unset = unbounded. */\n maxParkedDurationMs?: number\n /** How long a killed run (token/duration limit) may wind down after interrupt()\n * before the queue force-finalizes it and closes the session. Default 5000. */\n killGraceMs?: number\n /** Expire terminal jobs: prune those finished more than `maxAgeMs` ago, sweeping\n * every `sweepIntervalMs` (default min(maxAgeMs, 60s)) and after each completion.\n * Unset = keep forever (the in-memory adapter then grows unboundedly). */\n retention?: { maxAgeMs: number; sweepIntervalMs?: number }\n /** Patch job session configs (inject queryFn, env, tool policy) before they run. */\n buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig\n /**\n * Drop a parked session's persisted state: the run ended (cancel, kill, retry)\n * while parked, so nothing will ever rehydrate it. The host that parks sessions\n * — {@link JobQueue.onSessionParking}'s caller — wires this to its session store.\n */\n discardSession?: (sessionId: string) => void | Promise<void>\n /** Webhook transport. Defaults to global fetch. */\n fetchImpl?: typeof fetch\n /** Webhook delivery attempts per event (exponential backoff). Default 3. */\n webhookAttempts?: number\n /** Initial backoff between webhook attempts. Default 500ms. */\n webhookRetryDelayMs?: number\n /** Local observer invoked for every job event (in addition to any webhook). */\n onEvent?: (event: JobEvent) => void\n}\n\ntype RunningJob = {\n record: JobRecord\n /** Rebuilt on every resume: a rehydrated session is a NEW runner object under\n * the same session id, so nothing may hold the old reference. */\n runner: Runner\n unsubscribe: () => void\n /** Highest event seq seen. A resumed run re-subscribes from here, so the parked\n * session's replayed log doesn't re-deliver a turn the queue already acted on. */\n lastSeq: number\n /** Mid-run token estimate from assistant-message usage (enforcement + progress). */\n estimatedTokens: number\n /** Set when the queue killed the run (limits, cancel) — decides the terminal status. */\n killReason?: string\n canceled: boolean\n finalized: boolean\n /** Per-job webhook chain so deliveries stay ordered. */\n deliveries: Promise<void>\n /** Watchdog: fires killReason when the run exceeds its wall-clock cap. */\n durationTimer?: ReturnType<typeof setTimeout>\n /** Backstop after a kill: force-finalizes if interrupt() never yields a result. */\n forceTimer?: ReturnType<typeof setTimeout>\n /** Wall-clock actually spent running (parked stretches excluded), plus when the\n * current running leg started. Together they bound the duration watchdog across\n * any number of parks. */\n runningMs: number\n legStartedAt: number\n /** Set while parked: when it parked and what it waits on. */\n parkedAt?: number\n parkedExecutionId?: string\n /** Watchdog on total parked time (maxParkedDurationMs). */\n parkTimer?: ReturnType<typeof setTimeout>\n parkedMs: number\n}\n\nconst dayKey = (epochMs: number): string => new Date(epochMs).toISOString().slice(0, 10)\n\nconst sumUsage = (usage: unknown): number => {\n if (typeof usage !== 'object' || usage === null) return 0\n const u = usage as Record<string, unknown>\n return (\n (typeof u.input_tokens === 'number' ? u.input_tokens : 0) +\n (typeof u.output_tokens === 'number' ? u.output_tokens : 0) +\n (typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : 0) +\n (typeof u.cache_read_input_tokens === 'number' ? u.cache_read_input_tokens : 0)\n )\n}\n\nconst textPreview = (message: ApiMessage, max = 140): JobProgress | null => {\n const blocks = typeof message.content === 'string'\n ? [{ type: 'text', text: message.content }]\n : message.content\n for (const block of blocks) {\n if (block.type === 'tool_use') {\n return { kind: 'tool_use', preview: (block as { name?: string }).name }\n }\n if (block.type === 'text') {\n const text = (block as { text?: string }).text ?? ''\n if (text.trim()) {\n return {\n kind: 'assistant_text',\n preview: text.length > max ? text.slice(0, max - 1) + '…' : text,\n }\n }\n }\n }\n return null\n}\n\n/**\n * One-shot job execution over the session runner: submitted jobs run `session.prompt`\n * unattended, bounded by `maxConcurrency` and token budgets, and report progress and\n * completion through webhooks (plus `onEvent` locally). Job state lives in the\n * {@link QueueAdapter}; this class owns scheduling and the live runs.\n */\nexport class JobQueue {\n #options: JobQueueOptions\n #adapter: QueueAdapter\n #running = new Map<string, RunningJob>()\n /** Runs waiting on a deferred execution: alive, but holding no concurrency slot\n * and no live runner. Keyed by job id like `#running`. */\n #parked = new Map<string, RunningJob>()\n #pumping = false\n #closed = false\n #offWork: (() => void) | undefined\n #sweepTimer: ReturnType<typeof setInterval> | undefined\n /** Pending retry-backoff wakeups, cleared on close(). */\n #retryTimers = new Set<ReturnType<typeof setTimeout>>()\n\n constructor(options: JobQueueOptions) {\n this.#options = options\n this.#adapter = options.adapter ?? new InMemoryQueueAdapter()\n this.#offWork = this.#adapter.onWork?.(() => void this.#pump())\n const retention = options.retention\n if (retention) {\n const interval = retention.sweepIntervalMs ?? Math.min(retention.maxAgeMs, 60_000)\n this.#sweepTimer = setInterval(() => this.#sweep(), interval)\n this.#sweepTimer.unref?.()\n }\n }\n\n async submit(request: CreateJobRequest): Promise<JobInfo> {\n if (this.#closed) throw new Error('queue is closed')\n if (!request.session?.prompt?.trim()) throw new Error('session.prompt is required')\n // No `cwd` check: whether one is required depends on the engine's\n // capability record, which the gateway resolves at the door (see the\n // server's `checkCwd`). A second, engine-blind copy of the rule here would\n // refuse the filesystem-less engine outright.\n if (request.session.resume || request.session.forkSession) {\n throw new Error('resume/forkSession are not supported for queued jobs')\n }\n const attempts = request.attempts ?? 1\n if (!Number.isInteger(attempts) || attempts < 1) {\n throw new Error('attempts must be a positive integer')\n }\n if (request.retryDelayMs !== undefined && !(request.retryDelayMs >= 0)) {\n throw new Error('retryDelayMs must be >= 0')\n }\n const info: JobInfo = {\n id: randomUUID(),\n status: 'queued',\n cwd: request.session.cwd ?? '',\n profile: request.session.profile,\n prompt: request.session.prompt,\n createdAt: Date.now(),\n attempt: 1,\n maxAttempts: attempts,\n usage: { tokens: 0, totalCostUsd: 0, numTurns: 0 },\n meta: request.meta,\n // Copied from the request so the job routes can be gated by the same rule\n // as the session routes — the run's session inherits it at claim time\n // through `record.request.session`.\n scope: request.session.scope,\n }\n const record: JobRecord = { info, request }\n await this.#adapter.add(record)\n this.#emit(record, { type: 'job_submitted', job: info, ts: Date.now() }, undefined, {\n skipWebhook: true,\n })\n void this.#pump()\n return info\n }\n\n async get(id: string): Promise<JobInfo | null> {\n return (await this.#adapter.get(id))?.info ?? null\n }\n\n async list(): Promise<JobInfo[]> {\n return (await this.#adapter.list()).map((j) => j.info)\n }\n\n /**\n * A run's session is about to be parked on a deferred execution: the host has\n * snapshotted it and is tearing the live runner down. The queue drops its\n * subscription to the doomed runner, frees the concurrency slot, and stops the\n * duration clock.\n *\n * Returns false when the queue refuses the park — the run is already finalizing\n * or has been killed, so the host must leave the session alone. A session that\n * belongs to no job accepts trivially (there is nothing to account for).\n */\n onSessionParking(sessionId: string, executionId: string): boolean {\n const job = this.#bySession(this.#running, sessionId)\n if (!job) return true\n if (job.finalized || job.killReason) return false\n const now = Date.now()\n job.unsubscribe()\n job.runningMs += now - job.legStartedAt\n clearTimeout(job.durationTimer)\n job.durationTimer = undefined\n job.parkedAt = now\n job.parkedExecutionId = executionId\n this.#running.delete(job.record.info.id)\n this.#parked.set(job.record.info.id, job)\n const parkedLimit = this.#options.maxParkedDurationMs\n if (parkedLimit !== undefined) {\n job.parkTimer = setTimeout(\n () => this.#kill(job, `job exceeded max parked duration (${parkedLimit}ms)`),\n Math.max(0, parkedLimit - job.parkedMs),\n )\n job.parkTimer.unref?.()\n }\n void this.#recordPark(job, executionId)\n return true\n }\n\n async #recordPark(job: RunningJob, executionId: string): Promise<void> {\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'parked',\n parkedAt: job.parkedAt,\n parkedExecutionId: executionId,\n })\n if (updated) job.record = updated\n this.#emit(\n job.record,\n { type: 'job_parked', job: job.record.info, executionId, ts: Date.now() },\n job,\n )\n // The slot is free now — let a queued job take it.\n void this.#pump()\n }\n\n /**\n * The parked session was rehydrated (same session id, new runner object) because\n * its execution's result arrived. Re-subscribe and restart the clock with the\n * budget the run had left.\n *\n * A resume takes its slot back immediately, so a burst of resumes can transiently\n * exceed `maxConcurrency` — the alternative would be holding a result the agent\n * loop has already been handed.\n */\n onSessionResumed(sessionId: string, runner: Runner): void {\n const job = this.#bySession(this.#parked, sessionId)\n if (!job || job.finalized) return\n const now = Date.now()\n const executionId = job.parkedExecutionId ?? ''\n clearTimeout(job.parkTimer)\n job.parkTimer = undefined\n job.parkedMs += now - (job.parkedAt ?? now)\n job.parkedAt = undefined\n job.parkedExecutionId = undefined\n job.legStartedAt = now\n job.runner = runner\n this.#parked.delete(job.record.info.id)\n this.#running.set(job.record.info.id, job)\n const durationLimit = this.#effectiveDurationLimit(job.record.request)\n if (durationLimit !== undefined) {\n job.durationTimer = setTimeout(\n () => this.#kill(job, `job exceeded max duration (${durationLimit}ms)`),\n Math.max(0, durationLimit - job.runningMs),\n )\n job.durationTimer.unref?.()\n }\n // From lastSeq, not 0: the rehydrated runner replays the whole persisted log,\n // and re-handling those events would double-count tokens and could re-finalize\n // the job on an old turn_result.\n job.unsubscribe = runner.subscribe((event) => void this.#handleEvent(job, event), job.lastSeq)\n void this.#recordResume(job, executionId)\n }\n\n async #recordResume(job: RunningJob, executionId: string): Promise<void> {\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'running',\n parkedAt: undefined,\n parkedExecutionId: undefined,\n })\n if (updated) job.record = updated\n this.#emit(\n job.record,\n { type: 'job_resumed', job: job.record.info, executionId, ts: Date.now() },\n job,\n )\n }\n\n #bySession(jobs: Map<string, RunningJob>, sessionId: string): RunningJob | undefined {\n for (const job of jobs.values()) {\n if (job.record.info.sessionId === sessionId) return job\n }\n return undefined\n }\n\n /** Cancel a queued, running, or parked job. Returns the job, or null if unknown. */\n async cancel(id: string): Promise<JobInfo | null> {\n const record = await this.#adapter.get(id)\n if (!record) return null\n const running = this.#running.get(id) ?? this.#parked.get(id)\n if (running) {\n running.canceled = true\n running.killReason = 'canceled'\n await this.#finalize(running, {\n usage: { tokens: running.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: 'canceled',\n error: 'canceled',\n })\n return running.record.info\n }\n if (record.info.status !== 'queued') return record.info\n const updated = await this.#adapter.update(id, {\n status: 'canceled',\n finishedAt: Date.now(),\n error: 'canceled',\n })\n if (updated) this.#emit(updated, { type: 'job_completed', job: updated.info, ts: Date.now() })\n return updated?.info ?? null\n }\n\n async stats(): Promise<QueueStats> {\n const jobs = await this.#adapter.list()\n const dailyTokensUsed = await this.#adapter.dailyTokens(dayKey(Date.now()))\n const dailyTokenLimit = this.#options.dailyTokenLimit\n return {\n maxConcurrency: this.#options.maxConcurrency ?? 1,\n running: this.#running.size,\n parked: this.#parked.size,\n queued: jobs.filter((j) => j.info.status === 'queued').length,\n sessionTokenLimit: this.#options.sessionTokenLimit,\n dailyTokenLimit,\n dailyTokensUsed,\n paused: dailyTokenLimit !== undefined && dailyTokensUsed >= dailyTokenLimit,\n }\n }\n\n /** Stop scheduling new jobs. Running jobs keep finalizing (e.g. when the host closes\n * their sessions); job state stays in the adapter. */\n close(): void {\n this.#closed = true\n this.#offWork?.()\n clearInterval(this.#sweepTimer)\n for (const timer of this.#retryTimers) clearTimeout(timer)\n this.#retryTimers.clear()\n }\n\n #sweep(): void {\n const retention = this.#options.retention\n if (!retention) return\n this.#adapter.prune(retention.maxAgeMs).catch(() => {\n // sweep failures must not break the queue; the next sweep retries\n })\n }\n\n async #pump(): Promise<void> {\n if (this.#pumping || this.#closed) return\n this.#pumping = true\n try {\n const maxConcurrency = this.#options.maxConcurrency ?? 1\n while (this.#running.size < maxConcurrency) {\n const limit = this.#options.dailyTokenLimit\n if (limit !== undefined && (await this.#adapter.dailyTokens(dayKey(Date.now()))) >= limit) {\n return\n }\n const record = await this.#adapter.claimNext()\n if (!record) return\n await this.#start(record)\n }\n } finally {\n this.#pumping = false\n }\n }\n\n async #start(record: JobRecord): Promise<void> {\n const id = record.info.id\n const build = this.#options.buildRunnerConfig ?? ((req: CreateSessionRequest) => req)\n let runner: Runner\n try {\n // Stamped so the session says what it is. A job run is an ordinary\n // registry session — attachable, listable, indistinguishable — and a\n // client with a jobs surface of its own needs to know not to show it\n // twice. `meta` is free-form and already rides SessionInfo, so this costs\n // no protocol change; `isJobRun` is the one place the key is spelled.\n const request: CreateSessionRequest = {\n ...record.request.session,\n meta: { ...record.request.session.meta, jobId: id },\n }\n runner = await this.#options.createRunner(build(request))\n } catch (error) {\n const failed = await this.#adapter.update(id, {\n status: 'failed',\n finishedAt: Date.now(),\n error: error instanceof Error ? error.message : String(error),\n })\n if (failed) this.#emit(failed, { type: 'job_completed', job: failed.info, ts: Date.now() })\n return\n }\n const job: RunningJob = {\n record,\n runner,\n unsubscribe: () => {},\n lastSeq: 0,\n estimatedTokens: 0,\n canceled: false,\n finalized: false,\n deliveries: Promise.resolve(),\n runningMs: 0,\n legStartedAt: Date.now(),\n parkedMs: 0,\n }\n this.#running.set(id, job)\n const updated = await this.#adapter.update(id, {\n startedAt: Date.now(),\n sessionId: runner.id,\n })\n if (updated) job.record = updated\n this.#emit(job.record, { type: 'job_started', job: job.record.info, ts: Date.now() })\n const durationLimit = this.#effectiveDurationLimit(record.request)\n if (durationLimit !== undefined) {\n job.durationTimer = setTimeout(\n () => this.#kill(job, `job exceeded max duration (${durationLimit}ms)`),\n durationLimit,\n )\n job.durationTimer.unref?.()\n }\n job.unsubscribe = runner.subscribe((event) => void this.#handleEvent(job, event))\n }\n\n /** Kill a run: interrupt it and, if the CLI never yields a result (stuck process),\n * force-finalize after the grace period so the job can't hang forever. */\n #kill(job: RunningJob, reason: string): void {\n if (job.finalized || job.killReason) return\n job.killReason = reason\n if (job.parkedAt !== undefined) {\n // Parked: there is no live runner to interrupt and no result coming, so the\n // grace period would just be dead time.\n void this.#finalize(job, {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error: reason,\n })\n return\n }\n void job.runner.interrupt().catch(() => {})\n job.forceTimer = setTimeout(() => {\n void this.#finalize(job, {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error: reason,\n })\n }, this.#options.killGraceMs ?? 5000)\n job.forceTimer.unref?.()\n }\n\n async #handleEvent(job: RunningJob, event: SessionEvent): Promise<void> {\n if (job.finalized) return\n job.lastSeq = Math.max(job.lastSeq, event.seq)\n switch (event.type) {\n case 'system_init':\n await this.#adapter.update(job.record.info.id, { sdkSessionId: event.sdkSessionId })\n return\n case 'assistant_message': {\n if (event.replay) return\n job.estimatedTokens += sumUsage(event.message.usage)\n const limit = this.#effectiveTokenLimit(job.record.request)\n if (limit !== undefined && job.estimatedTokens > limit) {\n this.#kill(job, `session token limit exceeded (${job.estimatedTokens} > ${limit})`)\n }\n const progress = textPreview(event.message)\n if (progress) this.#progress(job, progress)\n return\n }\n case 'permission_requested':\n // The full request rides along so webhook consumers can answer it over REST\n // (questions, approvals) instead of only seeing a preview string.\n this.#progress(job, {\n kind: 'permission_requested',\n preview: event.request.title ?? event.request.toolName,\n request: event.request,\n })\n return\n case 'permission_resolved':\n this.#progress(job, { kind: 'permission_resolved', preview: event.behavior })\n return\n case 'turn_result': {\n // One job = one unattended run: the first result is the outcome.\n const tokens = sumUsage(event.usage) || job.estimatedTokens\n await this.#finalize(job, {\n usage: {\n tokens,\n totalCostUsd: event.totalCostUsd,\n numTurns: event.numTurns,\n },\n result: {\n subtype: event.subtype,\n isError: event.isError,\n result: event.result,\n errors: event.errors,\n durationMs: event.durationMs,\n },\n status: job.killReason\n ? (job.canceled ? 'canceled' : 'failed')\n : event.isError\n ? 'failed'\n : 'succeeded',\n error: job.killReason ?? (event.isError ? (event.errors?.join('; ') || event.subtype) : undefined),\n })\n return\n }\n case 'session_error':\n await this.#finalize(job, {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error: job.killReason ?? event.message,\n })\n return\n case 'session_closed':\n await this.#finalize(job, {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error: job.killReason ?? 'session closed before completing',\n })\n return\n default:\n return\n }\n }\n\n #effectiveTokenLimit(request: CreateJobRequest): number | undefined {\n const limits = [request.maxTokens, this.#options.sessionTokenLimit].filter(\n (n): n is number => typeof n === 'number',\n )\n return limits.length > 0 ? Math.min(...limits) : undefined\n }\n\n #effectiveDurationLimit(request: CreateJobRequest): number | undefined {\n const limits = [request.maxDurationMs, this.#options.maxJobDurationMs].filter(\n (n): n is number => typeof n === 'number',\n )\n return limits.length > 0 ? Math.min(...limits) : undefined\n }\n\n /** End the current run. `patch.usage` is this attempt's usage alone — prior attempts'\n * totals live on the stored info and are folded in here. A failed (not canceled) run\n * with attempts left re-queues with backoff instead of completing. */\n async #finalize(job: RunningJob, patch: Partial<JobInfo>): Promise<void> {\n if (job.finalized) return\n job.finalized = true\n job.unsubscribe()\n clearTimeout(job.durationTimer)\n clearTimeout(job.forceTimer)\n clearTimeout(job.parkTimer)\n this.#running.delete(job.record.info.id)\n const wasParked = this.#parked.delete(job.record.info.id)\n job.runner.close('server')\n if (wasParked && job.record.info.sessionId) {\n // The live runner is already gone; what survives the run is the persisted\n // snapshot, and nothing will ever rehydrate it now.\n try {\n void Promise.resolve(this.#options.discardSession?.(job.record.info.sessionId)).catch(\n () => {},\n )\n } catch {\n // discard failures must not break finalization\n }\n }\n const attemptUsage = patch.usage ?? { tokens: 0, totalCostUsd: 0, numTurns: 0 }\n if (attemptUsage.tokens > 0) {\n await this.#adapter.addDailyTokens(dayKey(Date.now()), attemptUsage.tokens)\n }\n const prior = job.record.info.usage\n const usage = {\n tokens: prior.tokens + attemptUsage.tokens,\n totalCostUsd: prior.totalCostUsd + attemptUsage.totalCostUsd,\n numTurns: prior.numTurns + attemptUsage.numTurns,\n }\n const attempt = job.record.info.attempt ?? 1\n const maxAttempts = job.record.request.attempts ?? 1\n if (patch.status === 'failed' && attempt < maxAttempts && !this.#closed) {\n const baseDelay = job.record.request.retryDelayMs ?? 5000\n const delay = baseDelay * 2 ** (attempt - 1)\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'queued',\n attempt: attempt + 1,\n nextRunAt: Date.now() + delay,\n error: patch.error,\n usage,\n sessionId: undefined,\n sdkSessionId: undefined,\n startedAt: undefined,\n result: undefined,\n })\n if (updated) {\n job.record = updated\n this.#emit(updated, { type: 'job_retrying', job: updated.info, ts: Date.now() }, job)\n const timer = setTimeout(() => {\n this.#retryTimers.delete(timer)\n void this.#pump()\n }, delay)\n timer.unref?.()\n this.#retryTimers.add(timer)\n }\n void this.#pump()\n return\n }\n const updated = await this.#adapter.update(job.record.info.id, {\n ...patch,\n usage,\n nextRunAt: undefined,\n finishedAt: Date.now(),\n })\n if (updated) {\n job.record = updated\n // Pass the job so the completion webhook stays ordered behind its progress\n // deliveries (the running-map entry is already gone).\n this.#emit(job.record, { type: 'job_completed', job: updated.info, ts: Date.now() }, job)\n }\n this.#sweep()\n void this.#pump()\n }\n\n #progress(job: RunningJob, progress: JobProgress): void {\n const event: JobEvent = { type: 'job_progress', job: job.record.info, progress, ts: Date.now() }\n // 'completion' granularity: local observers still see progress; the webhook doesn't.\n if (job.record.request.webhook?.progress === 'completion') {\n try {\n this.#options.onEvent?.(event)\n } catch {\n // observer errors must not break the queue\n }\n return\n }\n this.#emit(job.record, event, job)\n }\n\n /** Notify the local observer and, when configured, the job's webhook (ordered per job). */\n #emit(\n record: JobRecord,\n event: JobEvent,\n chainOwner?: RunningJob,\n { skipWebhook = false }: { skipWebhook?: boolean } = {},\n ): void {\n try {\n this.#options.onEvent?.(event)\n } catch {\n // observer errors must not break the queue\n }\n const webhook = record.request.webhook\n if (!webhook || skipWebhook) return\n const running = chainOwner ?? this.#running.get(record.info.id)\n const deliver = () => this.#deliver(webhook.url, webhook.headers, event)\n if (running) running.deliveries = running.deliveries.then(deliver)\n else void deliver()\n }\n\n async #deliver(\n url: string,\n headers: Record<string, string> | undefined,\n event: JobEvent,\n ): Promise<void> {\n const fetchImpl = this.#options.fetchImpl ?? fetch\n const attempts = this.#options.webhookAttempts ?? 3\n const baseDelay = this.#options.webhookRetryDelayMs ?? 500\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...headers },\n body: JSON.stringify(event),\n })\n if (res.ok) return\n } catch {\n // network error — retry below\n }\n if (attempt < attempts - 1) {\n await new Promise((resolve) => setTimeout(resolve, baseDelay * 2 ** attempt))\n }\n }\n // Deliveries are best-effort; clients can always poll GET /jobs/:id.\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workerdeck/queue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Job queue over the WorkerDeck session runner: remote services schedule one-shot runs; the queue executes them as sessions with bounded concurrency and token budgets, delivering progress and completion via webhooks. Pluggable adapter interface (in-memory bundled; redis/bullmq/pubsub adapters can implement the same contract).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@workerdeck/core": "0.
|
|
21
|
-
"@workerdeck/protocol": "0.
|
|
20
|
+
"@workerdeck/core": "0.15.0",
|
|
21
|
+
"@workerdeck/protocol": "0.15.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@anthropic-ai/claude-agent-sdk": "^0.3.221",
|