@workerdeck/queue 0.23.0 → 1.0.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/build/index.d.mts CHANGED
@@ -2,52 +2,21 @@ import { CreateJobRequest, CreateSessionRequest, JobEvent, JobInfo, QueueStats }
2
2
  import { Runner, SessionRunnerConfig } from "@workerdeck/core";
3
3
 
4
4
  //#region src/adapter.d.ts
5
- /** A job as the adapter stores it: the wire-visible info plus the original request. */
6
5
  type JobRecord = {
7
6
  info: JobInfo;
8
7
  request: CreateJobRequest;
9
8
  };
10
- /**
11
- * Storage + claiming contract the JobQueue runs against. The bundled implementation
12
- * is {@link InMemoryQueueAdapter}; redis/bullmq/pubsub adapters implement the same
13
- * interface. Everything is Promise-based so remote backends fit without changing the
14
- * queue, and `claimNext` is the one operation that must be atomic across workers
15
- * (two concurrent claims must never return the same job).
16
- */
17
9
  interface QueueAdapter {
18
- /** Persist a newly submitted job (status 'queued'). */
19
10
  add(job: JobRecord): Promise<void>;
20
- /**
21
- * Atomically claim the oldest claimable queued job, transitioning it to 'running'.
22
- * A queued job whose `nextRunAt` is in the future (retry backoff) is not claimable
23
- * yet. Returns null when nothing is claimable.
24
- */
25
11
  claimNext(): Promise<JobRecord | null>;
26
12
  get(id: string): Promise<JobRecord | null>;
27
- /** All known jobs, oldest first. Backends may cap retention of terminal jobs. */
28
13
  list(): Promise<JobRecord[]>;
29
- /** Merge a partial info patch into a job (a key explicitly set to undefined clears
30
- * that field). Returns the updated record, or null if unknown. */
31
14
  update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null>;
32
- /** Delete terminal jobs (succeeded/failed/canceled) that finished more than
33
- * `olderThanMs` ago. Returns how many were removed. */
34
15
  prune(olderThanMs: number): Promise<number>;
35
- /**
36
- * Add tokens to a day's global counter and return the new total. `dayKey` is a UTC
37
- * 'YYYY-MM-DD'; keeping the counter in the adapter makes daily budgets hold across
38
- * multiple workers sharing a backend.
39
- */
40
16
  addDailyTokens(dayKey: string, tokens: number): Promise<number>;
41
17
  dailyTokens(dayKey: string): Promise<number>;
42
- /**
43
- * Optional: notify the queue that work may be available (a job added by another
44
- * producer on a shared backend). The bundled queue also pumps after its own
45
- * submits/completions, so purely local adapters can omit this.
46
- */
47
18
  onWork?(listener: () => void): () => void;
48
19
  }
49
- /** Reference adapter: single-process, no persistence. Jobs and daily counters are lost
50
- * on restart — production deployments should back the queue with a shared store. */
51
20
  declare class InMemoryQueueAdapter implements QueueAdapter {
52
21
  #private;
53
22
  add(job: JobRecord): Promise<void>;
@@ -62,86 +31,35 @@ declare class InMemoryQueueAdapter implements QueueAdapter {
62
31
  //#endregion
63
32
  //#region src/queue.d.ts
64
33
  type JobQueueOptions = {
65
- /** Turn a session config into a live runner — typically the server registry's create(),
66
- * so job sessions are ordinary sessions clients can attach to and watch. May be
67
- * async: engines whose assembly awaits (a provider session's MCP connect) resolve
68
- * here, and a rejection fails the job like any other start error. */
69
- createRunner: (config: SessionRunnerConfig) => Runner | Promise<Runner>; /** Storage/claiming backend. Defaults to the in-memory adapter (single process). */
70
- adapter?: QueueAdapter; /** Concurrent job sessions. Default 1. */
71
- maxConcurrency?: number; /** Token cap per job session; exceeding it interrupts the run and fails the job. */
34
+ createRunner: (config: SessionRunnerConfig) => Runner | Promise<Runner>;
35
+ adapter?: QueueAdapter;
36
+ maxConcurrency?: number;
72
37
  sessionTokenLimit?: number;
73
- /** Global token budget per UTC day; when exhausted, queued jobs are held until the
74
- * day rolls over (running jobs finish and are accounted). */
75
38
  dailyTokenLimit?: number;
76
- /** Wall-clock cap per job run; exceeding it interrupts the run and fails the job.
77
- * The watchdog for stuck CLIs — without it, a run that never yields a result keeps
78
- * its job (and concurrency slot) forever. Time spent parked on a deferred
79
- * execution does not count against it: the run isn't stuck, it's waiting. */
80
39
  maxJobDurationMs?: number;
81
- /** Cap on time parked on a deferred execution, across all parks of one run.
82
- * Exceeding it fails the job (the execution's own watchdog, when the backend set
83
- * one, usually fires first and lets the agent adapt instead). Unset = unbounded. */
84
40
  maxParkedDurationMs?: number;
85
- /** How long a killed run (token/duration limit) may wind down after interrupt()
86
- * before the queue force-finalizes it and closes the session. Default 5000. */
87
41
  killGraceMs?: number;
88
- /** Expire terminal jobs: prune those finished more than `maxAgeMs` ago, sweeping
89
- * every `sweepIntervalMs` (default min(maxAgeMs, 60s)) and after each completion.
90
- * Unset = keep forever (the in-memory adapter then grows unboundedly). */
91
42
  retention?: {
92
43
  maxAgeMs: number;
93
44
  sweepIntervalMs?: number;
94
- }; /** Patch job session configs (inject queryFn, env, tool policy) before they run. */
45
+ };
95
46
  buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig;
96
- /**
97
- * Drop a parked session's persisted state: the run ended (cancel, kill, retry)
98
- * while parked, so nothing will ever rehydrate it. The host that parks sessions
99
- * — {@link JobQueue.onSessionParking}'s caller — wires this to its session store.
100
- */
101
- discardSession?: (sessionId: string) => void | Promise<void>; /** Webhook transport. Defaults to global fetch. */
102
- fetchImpl?: typeof fetch; /** Webhook delivery attempts per event (exponential backoff). Default 3. */
103
- webhookAttempts?: number; /** Initial backoff between webhook attempts. Default 500ms. */
104
- webhookRetryDelayMs?: number; /** Local observer invoked for every job event (in addition to any webhook). */
47
+ discardSession?: (sessionId: string) => void | Promise<void>;
48
+ fetchImpl?: typeof fetch;
49
+ webhookAttempts?: number;
50
+ webhookRetryDelayMs?: number;
105
51
  onEvent?: (event: JobEvent) => void;
106
52
  };
107
- /**
108
- * One-shot job execution over the session runner: submitted jobs run `session.prompt`
109
- * unattended, bounded by `maxConcurrency` and token budgets, and report progress and
110
- * completion through webhooks (plus `onEvent` locally). Job state lives in the
111
- * {@link QueueAdapter}; this class owns scheduling and the live runs.
112
- */
113
53
  declare class JobQueue {
114
54
  #private;
115
55
  constructor(options: JobQueueOptions);
116
56
  submit(request: CreateJobRequest): Promise<JobInfo>;
117
57
  get(id: string): Promise<JobInfo | null>;
118
58
  list(): Promise<JobInfo[]>;
119
- /**
120
- * A run's session is about to be parked on a deferred execution: the host has
121
- * snapshotted it and is tearing the live runner down. The queue drops its
122
- * subscription to the doomed runner, frees the concurrency slot, and stops the
123
- * duration clock.
124
- *
125
- * Returns false when the queue refuses the park — the run is already finalizing
126
- * or has been killed, so the host must leave the session alone. A session that
127
- * belongs to no job accepts trivially (there is nothing to account for).
128
- */
129
59
  onSessionParking(sessionId: string, executionId: string): boolean;
130
- /**
131
- * The parked session was rehydrated (same session id, new runner object) because
132
- * its execution's result arrived. Re-subscribe and restart the clock with the
133
- * budget the run had left.
134
- *
135
- * A resume takes its slot back immediately, so a burst of resumes can transiently
136
- * exceed `maxConcurrency` — the alternative would be holding a result the agent
137
- * loop has already been handed.
138
- */
139
60
  onSessionResumed(sessionId: string, runner: Runner): void;
140
- /** Cancel a queued, running, or parked job. Returns the job, or null if unknown. */
141
61
  cancel(id: string): Promise<JobInfo | null>;
142
62
  stats(): Promise<QueueStats>;
143
- /** Stop scheduling new jobs. Running jobs keep finalizing (e.g. when the host closes
144
- * their sessions); job state stays in the adapter. */
145
63
  close(): void;
146
64
  }
147
65
  //#endregion
package/build/index.mjs CHANGED
@@ -1,7 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  //#region src/adapter.ts
3
- /** Reference adapter: single-process, no persistence. Jobs and daily counters are lost
4
- * on restart — production deployments should back the queue with a shared store. */
5
3
  var InMemoryQueueAdapter = class {
6
4
  #jobs = /* @__PURE__ */ new Map();
7
5
  #dailyTokens = /* @__PURE__ */ new Map();
@@ -58,13 +56,15 @@ var InMemoryQueueAdapter = class {
58
56
  };
59
57
  //#endregion
60
58
  //#region src/queue.ts
61
- const dayKey = (epochMs) => new Date(epochMs).toISOString().slice(0, 10);
62
- const sumUsage = (usage) => {
59
+ function dayKey(epochMs) {
60
+ return new Date(epochMs).toISOString().slice(0, 10);
61
+ }
62
+ function sumUsage(usage) {
63
63
  if (typeof usage !== "object" || usage === null) return 0;
64
64
  const u = usage;
65
65
  return (typeof u.input_tokens === "number" ? u.input_tokens : 0) + (typeof u.output_tokens === "number" ? u.output_tokens : 0) + (typeof u.cache_creation_input_tokens === "number" ? u.cache_creation_input_tokens : 0) + (typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : 0);
66
- };
67
- const textPreview = (message, max = 140) => {
66
+ }
67
+ function textPreview(message, max = 140) {
68
68
  const blocks = typeof message.content === "string" ? [{
69
69
  type: "text",
70
70
  text: message.content
@@ -83,25 +83,16 @@ const textPreview = (message, max = 140) => {
83
83
  }
84
84
  }
85
85
  return null;
86
- };
87
- /**
88
- * One-shot job execution over the session runner: submitted jobs run `session.prompt`
89
- * unattended, bounded by `maxConcurrency` and token budgets, and report progress and
90
- * completion through webhooks (plus `onEvent` locally). Job state lives in the
91
- * {@link QueueAdapter}; this class owns scheduling and the live runs.
92
- */
86
+ }
93
87
  var JobQueue = class {
94
88
  #options;
95
89
  #adapter;
96
90
  #running = /* @__PURE__ */ new Map();
97
- /** Runs waiting on a deferred execution: alive, but holding no concurrency slot
98
- * and no live runner. Keyed by job id like `#running`. */
99
91
  #parked = /* @__PURE__ */ new Map();
100
92
  #pumping = false;
101
93
  #closed = false;
102
94
  #offWork;
103
95
  #sweepTimer;
104
- /** Pending retry-backoff wakeups, cleared on close(). */
105
96
  #retryTimers = /* @__PURE__ */ new Set();
106
97
  constructor(options) {
107
98
  this.#options = options;
@@ -157,16 +148,6 @@ var JobQueue = class {
157
148
  async list() {
158
149
  return (await this.#adapter.list()).map((j) => j.info);
159
150
  }
160
- /**
161
- * A run's session is about to be parked on a deferred execution: the host has
162
- * snapshotted it and is tearing the live runner down. The queue drops its
163
- * subscription to the doomed runner, frees the concurrency slot, and stops the
164
- * duration clock.
165
- *
166
- * Returns false when the queue refuses the park — the run is already finalizing
167
- * or has been killed, so the host must leave the session alone. A session that
168
- * belongs to no job accepts trivially (there is nothing to account for).
169
- */
170
151
  onSessionParking(sessionId, executionId) {
171
152
  const job = this.#bySession(this.#running, sessionId);
172
153
  if (!job) return true;
@@ -203,15 +184,6 @@ var JobQueue = class {
203
184
  }, job);
204
185
  this.#pump();
205
186
  }
206
- /**
207
- * The parked session was rehydrated (same session id, new runner object) because
208
- * its execution's result arrived. Re-subscribe and restart the clock with the
209
- * budget the run had left.
210
- *
211
- * A resume takes its slot back immediately, so a burst of resumes can transiently
212
- * exceed `maxConcurrency` — the alternative would be holding a result the agent
213
- * loop has already been handed.
214
- */
215
187
  onSessionResumed(sessionId, runner) {
216
188
  const job = this.#bySession(this.#parked, sessionId);
217
189
  if (!job || job.finalized) return;
@@ -251,7 +223,6 @@ var JobQueue = class {
251
223
  #bySession(jobs, sessionId) {
252
224
  for (const job of jobs.values()) if (job.record.info.sessionId === sessionId) return job;
253
225
  }
254
- /** Cancel a queued, running, or parked job. Returns the job, or null if unknown. */
255
226
  async cancel(id) {
256
227
  const record = await this.#adapter.get(id);
257
228
  if (!record) return null;
@@ -259,15 +230,7 @@ var JobQueue = class {
259
230
  if (running) {
260
231
  running.canceled = true;
261
232
  running.killReason = "canceled";
262
- await this.#finalize(running, {
263
- usage: {
264
- tokens: running.estimatedTokens,
265
- totalCostUsd: 0,
266
- numTurns: 0
267
- },
268
- status: "canceled",
269
- error: "canceled"
270
- });
233
+ await this.#finalize(running, this.#abortPatch(running, "canceled"));
271
234
  return running.record.info;
272
235
  }
273
236
  if (record.info.status !== "queued") return record.info;
@@ -298,8 +261,6 @@ var JobQueue = class {
298
261
  paused: dailyTokenLimit !== void 0 && dailyTokensUsed >= dailyTokenLimit
299
262
  };
300
263
  }
301
- /** Stop scheduling new jobs. Running jobs keep finalizing (e.g. when the host closes
302
- * their sessions); job state stays in the adapter. */
303
264
  close() {
304
265
  this.#closed = true;
305
266
  this.#offWork?.();
@@ -385,34 +346,33 @@ var JobQueue = class {
385
346
  }
386
347
  job.unsubscribe = runner.subscribe((event) => void this.#handleEvent(job, event));
387
348
  }
388
- /** Kill a run: interrupt it and, if the CLI never yields a result (stuck process),
389
- * force-finalize after the grace period so the job can't hang forever. */
349
+ /**
350
+ * How a run that never produced a `turn_result` is written down. There is no authoritative
351
+ * usage for such a run, so it reports the tokens counted from the message stream, no cost
352
+ * and no turns. `turn_result` is the one path that does NOT come through here — it has real
353
+ * numbers — which is exactly the distinction five inline copies of this used to hide.
354
+ */
355
+ #abortPatch(job, error) {
356
+ return {
357
+ usage: {
358
+ tokens: job.estimatedTokens,
359
+ totalCostUsd: 0,
360
+ numTurns: 0
361
+ },
362
+ status: job.canceled ? "canceled" : "failed",
363
+ error
364
+ };
365
+ }
390
366
  #kill(job, reason) {
391
367
  if (job.finalized || job.killReason) return;
392
368
  job.killReason = reason;
393
369
  if (job.parkedAt !== void 0) {
394
- this.#finalize(job, {
395
- usage: {
396
- tokens: job.estimatedTokens,
397
- totalCostUsd: 0,
398
- numTurns: 0
399
- },
400
- status: job.canceled ? "canceled" : "failed",
401
- error: reason
402
- });
370
+ this.#finalize(job, this.#abortPatch(job, reason));
403
371
  return;
404
372
  }
405
373
  job.runner.interrupt().catch(() => {});
406
374
  job.forceTimer = setTimeout(() => {
407
- this.#finalize(job, {
408
- usage: {
409
- tokens: job.estimatedTokens,
410
- totalCostUsd: 0,
411
- numTurns: 0
412
- },
413
- status: job.canceled ? "canceled" : "failed",
414
- error: reason
415
- });
375
+ this.#finalize(job, this.#abortPatch(job, reason));
416
376
  }, this.#options.killGraceMs ?? 5e3);
417
377
  job.forceTimer.unref?.();
418
378
  }
@@ -466,26 +426,10 @@ var JobQueue = class {
466
426
  return;
467
427
  }
468
428
  case "session_error":
469
- await this.#finalize(job, {
470
- usage: {
471
- tokens: job.estimatedTokens,
472
- totalCostUsd: 0,
473
- numTurns: 0
474
- },
475
- status: job.canceled ? "canceled" : "failed",
476
- error: job.killReason ?? event.message
477
- });
429
+ await this.#finalize(job, this.#abortPatch(job, job.killReason ?? event.message));
478
430
  return;
479
431
  case "session_closed":
480
- await this.#finalize(job, {
481
- usage: {
482
- tokens: job.estimatedTokens,
483
- totalCostUsd: 0,
484
- numTurns: 0
485
- },
486
- status: job.canceled ? "canceled" : "failed",
487
- error: job.killReason ?? "session closed before completing"
488
- });
432
+ await this.#finalize(job, this.#abortPatch(job, job.killReason ?? "session closed before completing"));
489
433
  return;
490
434
  default: return;
491
435
  }
@@ -498,9 +442,6 @@ var JobQueue = class {
498
442
  const limits = [request.maxDurationMs, this.#options.maxJobDurationMs].filter((n) => typeof n === "number");
499
443
  return limits.length > 0 ? Math.min(...limits) : void 0;
500
444
  }
501
- /** End the current run. `patch.usage` is this attempt's usage alone — prior attempts'
502
- * totals live on the stored info and are folded in here. A failed (not canceled) run
503
- * with attempts left re-queues with backoff instead of completing. */
504
445
  async #finalize(job, patch) {
505
446
  if (job.finalized) return;
506
447
  job.finalized = true;
@@ -590,7 +531,6 @@ var JobQueue = class {
590
531
  }
591
532
  this.#emit(job.record, event, job);
592
533
  }
593
- /** Notify the local observer and, when configured, the job's webhook (ordered per job). */
594
534
  #emit(record, event, chainOwner, { skipWebhook = false } = {}) {
595
535
  try {
596
536
  this.#options.onEvent?.(event);
@@ -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 // 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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"}
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","#abortPatch","#retryTimers","#pumping","#start","#effectiveTokenLimit","#progress","#deliver"],"sources":["../src/adapter.ts","../src/queue.ts"],"sourcesContent":["import type { CreateJobRequest, JobInfo } from '@workerdeck/protocol'\n\nexport type JobRecord = {\n info: JobInfo\n request: CreateJobRequest\n}\n\nexport interface QueueAdapter {\n add(job: JobRecord): Promise<void>\n claimNext(): Promise<JobRecord | null>\n get(id: string): Promise<JobRecord | null>\n list(): Promise<JobRecord[]>\n update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null>\n prune(olderThanMs: number): Promise<number>\n addDailyTokens(dayKey: string, tokens: number): Promise<number>\n dailyTokens(dayKey: string): Promise<number>\n onWork?(listener: () => void): () => void\n}\n\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 (job.info.status === 'queued' && (job.info.nextRunAt === undefined || job.info.nextRunAt <= now)) {\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) {\n return Promise.resolve(null)\n }\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 createRunner: (config: SessionRunnerConfig) => Runner | Promise<Runner>\n adapter?: QueueAdapter\n maxConcurrency?: number\n sessionTokenLimit?: number\n dailyTokenLimit?: number\n maxJobDurationMs?: number\n maxParkedDurationMs?: number\n killGraceMs?: number\n retention?: { maxAgeMs: number; sweepIntervalMs?: number }\n buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig\n discardSession?: (sessionId: string) => void | Promise<void>\n fetchImpl?: typeof fetch\n webhookAttempts?: number\n webhookRetryDelayMs?: number\n onEvent?: (event: JobEvent) => void\n}\n\ntype RunningJob = {\n record: JobRecord\n runner: Runner\n unsubscribe: () => void\n lastSeq: number\n estimatedTokens: number\n killReason?: string\n canceled: boolean\n finalized: boolean\n deliveries: Promise<void>\n durationTimer?: ReturnType<typeof setTimeout>\n forceTimer?: ReturnType<typeof setTimeout>\n runningMs: number\n legStartedAt: number\n parkedAt?: number\n parkedExecutionId?: string\n parkTimer?: ReturnType<typeof setTimeout>\n parkedMs: number\n}\n\nfunction dayKey(epochMs: number): string {\n return new Date(epochMs).toISOString().slice(0, 10)\n}\n\nfunction sumUsage(usage: unknown): number {\n if (typeof usage !== 'object' || usage === null) {\n return 0\n }\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\nfunction textPreview(message: ApiMessage, max = 140): JobProgress | null {\n const blocks = typeof message.content === 'string' ? [{ type: 'text', text: message.content }] : 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\nexport class JobQueue {\n #options: JobQueueOptions\n #adapter: QueueAdapter\n #running = new Map<string, RunningJob>()\n #parked = new Map<string, RunningJob>()\n #pumping = false\n #closed = false\n #offWork: (() => void) | undefined\n #sweepTimer: ReturnType<typeof setInterval> | undefined\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) {\n throw new Error('queue is closed')\n }\n if (!request.session?.prompt?.trim()) {\n throw new Error('session.prompt is required')\n }\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 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 onSessionParking(sessionId: string, executionId: string): boolean {\n const job = this.#bySession(this.#running, sessionId)\n if (!job) {\n return true\n }\n if (job.finalized || job.killReason) {\n return false\n }\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) {\n job.record = updated\n }\n this.#emit(job.record, { type: 'job_parked', job: job.record.info, executionId, ts: Date.now() }, job)\n void this.#pump()\n }\n\n onSessionResumed(sessionId: string, runner: Runner): void {\n const job = this.#bySession(this.#parked, sessionId)\n if (!job || job.finalized) {\n return\n }\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 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) {\n job.record = updated\n }\n this.#emit(job.record, { type: 'job_resumed', job: job.record.info, executionId, ts: Date.now() }, job)\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) {\n return job\n }\n }\n return undefined\n }\n\n async cancel(id: string): Promise<JobInfo | null> {\n const record = await this.#adapter.get(id)\n if (!record) {\n return null\n }\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, this.#abortPatch(running, 'canceled'))\n return running.record.info\n }\n if (record.info.status !== 'queued') {\n return record.info\n }\n const updated = await this.#adapter.update(id, {\n status: 'canceled',\n finishedAt: Date.now(),\n error: 'canceled',\n })\n if (updated) {\n this.#emit(updated, { type: 'job_completed', job: updated.info, ts: Date.now() })\n }\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 close(): void {\n this.#closed = true\n this.#offWork?.()\n clearInterval(this.#sweepTimer)\n for (const timer of this.#retryTimers) {\n clearTimeout(timer)\n }\n this.#retryTimers.clear()\n }\n\n #sweep(): void {\n const retention = this.#options.retention\n if (!retention) {\n return\n }\n this.#adapter.prune(retention.maxAgeMs).catch(() => {})\n }\n\n async #pump(): Promise<void> {\n if (this.#pumping || this.#closed) {\n return\n }\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) {\n return\n }\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 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) {\n this.#emit(failed, { type: 'job_completed', job: failed.info, ts: Date.now() })\n }\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) {\n job.record = updated\n }\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(() => this.#kill(job, `job exceeded max duration (${durationLimit}ms)`), durationLimit)\n job.durationTimer.unref?.()\n }\n job.unsubscribe = runner.subscribe((event) => void this.#handleEvent(job, event))\n }\n\n /**\n * How a run that never produced a `turn_result` is written down. There is no authoritative\n * usage for such a run, so it reports the tokens counted from the message stream, no cost\n * and no turns. `turn_result` is the one path that does NOT come through here — it has real\n * numbers — which is exactly the distinction five inline copies of this used to hide.\n */\n #abortPatch(job: RunningJob, error: string): Partial<JobInfo> {\n return {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error,\n }\n }\n\n #kill(job: RunningJob, reason: string): void {\n if (job.finalized || job.killReason) {\n return\n }\n job.killReason = reason\n if (job.parkedAt !== undefined) {\n void this.#finalize(job, this.#abortPatch(job, reason))\n return\n }\n void job.runner.interrupt().catch(() => {})\n job.forceTimer = setTimeout(() => {\n void this.#finalize(job, this.#abortPatch(job, reason))\n }, this.#options.killGraceMs ?? 5000)\n job.forceTimer.unref?.()\n }\n\n async #handleEvent(job: RunningJob, event: SessionEvent): Promise<void> {\n if (job.finalized) {\n return\n }\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 }\n case 'assistant_message': {\n if (event.replay) {\n return\n }\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) {\n this.#progress(job, progress)\n }\n return\n }\n case 'permission_requested': {\n this.#progress(job, {\n kind: 'permission_requested',\n preview: event.request.title ?? event.request.toolName,\n request: event.request,\n })\n return\n }\n case 'permission_resolved': {\n this.#progress(job, { kind: 'permission_resolved', preview: event.behavior })\n return\n }\n case 'turn_result': {\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 ? (job.canceled ? 'canceled' : 'failed') : event.isError ? 'failed' : '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, this.#abortPatch(job, job.killReason ?? event.message))\n return\n }\n case 'session_closed': {\n await this.#finalize(job, this.#abortPatch(job, job.killReason ?? 'session closed before completing'))\n return\n }\n default: {\n return\n }\n }\n }\n\n #effectiveTokenLimit(request: CreateJobRequest): number | undefined {\n const limits = [request.maxTokens, this.#options.sessionTokenLimit].filter((n): n is number => typeof n === 'number')\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 is number => typeof n === 'number')\n return limits.length > 0 ? Math.min(...limits) : undefined\n }\n\n async #finalize(job: RunningJob, patch: Partial<JobInfo>): Promise<void> {\n if (job.finalized) {\n return\n }\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 try {\n void Promise.resolve(this.#options.discardSession?.(job.record.info.sessionId)).catch(() => {})\n } catch {}\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 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 if (job.record.request.webhook?.progress === 'completion') {\n try {\n this.#options.onEvent?.(event)\n } catch {}\n return\n }\n this.#emit(job.record, event, job)\n }\n\n #emit(record: JobRecord, event: JobEvent, chainOwner?: RunningJob, { skipWebhook = false }: { skipWebhook?: boolean } = {}): void {\n try {\n this.#options.onEvent?.(event)\n } catch {}\n const webhook = record.request.webhook\n if (!webhook || skipWebhook) {\n return\n }\n const running = chainOwner ?? this.#running.get(record.info.id)\n const deliver = () => this.#deliver(webhook.url, webhook.headers, event)\n if (running) {\n running.deliveries = running.deliveries.then(deliver)\n } else {\n void deliver()\n }\n }\n\n async #deliver(url: string, headers: Record<string, string> | undefined, event: JobEvent): 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) {\n return\n }\n } catch {}\n if (attempt < attempts - 1) {\n await new Promise((resolve) => setTimeout(resolve, baseDelay * 2 ** attempt))\n }\n }\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.23.0",
3
+ "version": "1.0.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/protocol": "0.23.0",
21
- "@workerdeck/core": "0.23.0"
20
+ "@workerdeck/core": "1.0.0",
21
+ "@workerdeck/protocol": "1.0.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@anthropic-ai/claude-agent-sdk": "^0.3.221",