@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 +8 -90
- package/build/index.mjs +29 -89
- package/build/index.mjs.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
};
|
|
45
|
+
};
|
|
95
46
|
buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
389
|
-
*
|
|
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);
|
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 // 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.
|
|
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/
|
|
21
|
-
"@workerdeck/
|
|
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",
|