@femtomc/mu-server 26.2.90 → 26.2.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -10
- package/dist/api/control_plane.js +64 -5
- package/dist/config.d.ts +3 -3
- package/dist/config.js +20 -15
- package/dist/control_plane.d.ts +20 -5
- package/dist/control_plane.js +303 -245
- package/dist/control_plane_adapter_registry.d.ts +1 -0
- package/dist/control_plane_adapter_registry.js +2 -0
- package/dist/control_plane_bootstrap_helpers.js +0 -1
- package/dist/control_plane_contract.d.ts +0 -35
- package/dist/control_plane_contract.js +1 -1
- package/dist/control_plane_telegram_generation.js +1 -0
- package/dist/control_plane_wake_delivery.d.ts +2 -1
- package/dist/control_plane_wake_delivery.js +3 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +0 -2
- package/dist/server.js +2 -41
- package/dist/{server_program_orchestration.d.ts → server_program_coordination.d.ts} +1 -1
- package/dist/{server_program_orchestration.js → server_program_coordination.js} +1 -1
- package/package.json +4 -4
- package/dist/api/runs.d.ts +0 -2
- package/dist/api/runs.js +0 -124
- package/dist/control_plane_run_outbox.d.ts +0 -7
- package/dist/control_plane_run_outbox.js +0 -52
- package/dist/control_plane_run_queue_coordinator.d.ts +0 -42
- package/dist/control_plane_run_queue_coordinator.js +0 -266
- package/dist/orchestration_queue.d.ts +0 -44
- package/dist/orchestration_queue.js +0 -111
- package/dist/run_queue.d.ts +0 -95
- package/dist/run_queue.js +0 -816
- package/dist/run_supervisor.d.ts +0 -108
- package/dist/run_supervisor.js +0 -460
package/dist/run_queue.js
DELETED
|
@@ -1,816 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { FsJsonlStore, getStorePaths } from "@femtomc/mu-core/node";
|
|
3
|
-
import { INTER_ROOT_QUEUE_RECONCILE_INVARIANTS, ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS, reconcileInterRootQueue, } from "./orchestration_queue.js";
|
|
4
|
-
const RUN_QUEUE_FILENAME = "run_queue.jsonl";
|
|
5
|
-
const DEFAULT_MAX_STEPS = 20;
|
|
6
|
-
const DEFAULT_MAX_OPERATION_IDS = 128;
|
|
7
|
-
const TERMINAL_QUEUE_STATES = new Set(["done", "failed", "cancelled"]);
|
|
8
|
-
const RUNNING_QUEUE_STATES = new Set(["queued", "active", "waiting_review", "refining"]);
|
|
9
|
-
const RUN_QUEUE_STATE_VALUES = [
|
|
10
|
-
"queued",
|
|
11
|
-
"active",
|
|
12
|
-
"waiting_review",
|
|
13
|
-
"refining",
|
|
14
|
-
"done",
|
|
15
|
-
"failed",
|
|
16
|
-
"cancelled",
|
|
17
|
-
];
|
|
18
|
-
const RUN_MODE_VALUES = ["run_start", "run_resume"];
|
|
19
|
-
const RUN_SOURCE_VALUES = ["command", "api"];
|
|
20
|
-
function defaultNowMs() {
|
|
21
|
-
return Date.now();
|
|
22
|
-
}
|
|
23
|
-
function normalizeOperationId(value) {
|
|
24
|
-
if (typeof value !== "string") {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
const trimmed = value.trim();
|
|
28
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
29
|
-
}
|
|
30
|
-
function normalizeRunMode(value) {
|
|
31
|
-
if (typeof value !== "string") {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
const trimmed = value.trim().toLowerCase();
|
|
35
|
-
if (RUN_MODE_VALUES.includes(trimmed)) {
|
|
36
|
-
return trimmed;
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
function normalizeRunSource(value) {
|
|
41
|
-
if (typeof value !== "string") {
|
|
42
|
-
return "api";
|
|
43
|
-
}
|
|
44
|
-
const trimmed = value.trim().toLowerCase();
|
|
45
|
-
return RUN_SOURCE_VALUES.includes(trimmed)
|
|
46
|
-
? trimmed
|
|
47
|
-
: "api";
|
|
48
|
-
}
|
|
49
|
-
function normalizeQueueState(value) {
|
|
50
|
-
if (typeof value !== "string") {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
const normalized = value.trim().toLowerCase().replaceAll("-", "_");
|
|
54
|
-
if (RUN_QUEUE_STATE_VALUES.includes(normalized)) {
|
|
55
|
-
return normalized;
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
function normalizeIssueId(value) {
|
|
60
|
-
if (typeof value !== "string") {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
const trimmed = value.trim();
|
|
64
|
-
if (!/^mu-[a-z0-9][a-z0-9-]*$/i.test(trimmed)) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
return trimmed.toLowerCase();
|
|
68
|
-
}
|
|
69
|
-
function normalizeString(value) {
|
|
70
|
-
if (typeof value !== "string") {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
const trimmed = value.trim();
|
|
74
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
75
|
-
}
|
|
76
|
-
function normalizePrompt(value) {
|
|
77
|
-
if (typeof value !== "string") {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
const trimmed = value.trim();
|
|
81
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
82
|
-
}
|
|
83
|
-
function normalizeMaxSteps(value, fallback = DEFAULT_MAX_STEPS) {
|
|
84
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
85
|
-
return Math.max(1, Math.trunc(value));
|
|
86
|
-
}
|
|
87
|
-
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
88
|
-
return Math.max(1, Number.parseInt(value, 10));
|
|
89
|
-
}
|
|
90
|
-
return Math.max(1, Math.trunc(fallback));
|
|
91
|
-
}
|
|
92
|
-
function normalizeTimestamp(value, fallback) {
|
|
93
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
94
|
-
return Math.trunc(value);
|
|
95
|
-
}
|
|
96
|
-
return Math.trunc(fallback);
|
|
97
|
-
}
|
|
98
|
-
function normalizeNullableTimestamp(value) {
|
|
99
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
100
|
-
return Math.trunc(value);
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
function normalizeNullableInt(value) {
|
|
105
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
106
|
-
return Math.trunc(value);
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
function normalizeAppliedOperationIds(value, max) {
|
|
111
|
-
if (!Array.isArray(value)) {
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
const out = [];
|
|
115
|
-
for (const item of value) {
|
|
116
|
-
const opId = normalizeOperationId(item);
|
|
117
|
-
if (!opId) {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
if (out.includes(opId)) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
out.push(opId);
|
|
124
|
-
if (out.length >= max) {
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return out;
|
|
129
|
-
}
|
|
130
|
-
function canTransition(from, to) {
|
|
131
|
-
if (from === to) {
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
return ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS[from].includes(to);
|
|
135
|
-
}
|
|
136
|
-
function isTerminalState(state) {
|
|
137
|
-
return TERMINAL_QUEUE_STATES.has(state);
|
|
138
|
-
}
|
|
139
|
-
function queueStateFromRunStatus(status) {
|
|
140
|
-
switch (status) {
|
|
141
|
-
case "completed":
|
|
142
|
-
return "done";
|
|
143
|
-
case "failed":
|
|
144
|
-
return "failed";
|
|
145
|
-
case "cancelled":
|
|
146
|
-
return "cancelled";
|
|
147
|
-
case "running":
|
|
148
|
-
default:
|
|
149
|
-
return "active";
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
export function runStatusFromQueueState(state) {
|
|
153
|
-
switch (state) {
|
|
154
|
-
case "done":
|
|
155
|
-
return "completed";
|
|
156
|
-
case "failed":
|
|
157
|
-
return "failed";
|
|
158
|
-
case "cancelled":
|
|
159
|
-
return "cancelled";
|
|
160
|
-
case "queued":
|
|
161
|
-
case "active":
|
|
162
|
-
case "waiting_review":
|
|
163
|
-
case "refining":
|
|
164
|
-
default:
|
|
165
|
-
return "running";
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
export function queueStatesForRunStatusFilter(status) {
|
|
169
|
-
if (typeof status !== "string") {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
const normalized = status.trim().toLowerCase().replaceAll("-", "_");
|
|
173
|
-
if (normalized.length === 0) {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
if (normalized === "running") {
|
|
177
|
-
return [...RUNNING_QUEUE_STATES];
|
|
178
|
-
}
|
|
179
|
-
if (normalized === "completed") {
|
|
180
|
-
return ["done"];
|
|
181
|
-
}
|
|
182
|
-
if (normalized === "done") {
|
|
183
|
-
return ["done"];
|
|
184
|
-
}
|
|
185
|
-
if (normalized === "failed") {
|
|
186
|
-
return ["failed"];
|
|
187
|
-
}
|
|
188
|
-
if (normalized === "cancelled") {
|
|
189
|
-
return ["cancelled"];
|
|
190
|
-
}
|
|
191
|
-
if (normalized === "waiting_review" ||
|
|
192
|
-
normalized === "refining" ||
|
|
193
|
-
normalized === "queued" ||
|
|
194
|
-
normalized === "active") {
|
|
195
|
-
return [normalized];
|
|
196
|
-
}
|
|
197
|
-
return [];
|
|
198
|
-
}
|
|
199
|
-
export const RUN_QUEUE_RECONCILE_INVARIANTS = INTER_ROOT_QUEUE_RECONCILE_INVARIANTS;
|
|
200
|
-
/**
|
|
201
|
-
* Server adapter wrapper around the orchestrator-owned inter-root planner.
|
|
202
|
-
*/
|
|
203
|
-
export function reconcileRunQueue(rows, policy) {
|
|
204
|
-
return reconcileInterRootQueue(rows, policy);
|
|
205
|
-
}
|
|
206
|
-
export function runQueuePath(repoRoot) {
|
|
207
|
-
return join(getStorePaths(repoRoot).storeDir, "control-plane", RUN_QUEUE_FILENAME);
|
|
208
|
-
}
|
|
209
|
-
function stableCompare(a, b) {
|
|
210
|
-
if (a.created_at_ms !== b.created_at_ms) {
|
|
211
|
-
return a.created_at_ms - b.created_at_ms;
|
|
212
|
-
}
|
|
213
|
-
return a.queue_id.localeCompare(b.queue_id);
|
|
214
|
-
}
|
|
215
|
-
function chooseLatest(a, b) {
|
|
216
|
-
if (a.updated_at_ms !== b.updated_at_ms) {
|
|
217
|
-
return a.updated_at_ms > b.updated_at_ms ? a : b;
|
|
218
|
-
}
|
|
219
|
-
if (a.revision !== b.revision) {
|
|
220
|
-
return a.revision > b.revision ? a : b;
|
|
221
|
-
}
|
|
222
|
-
return a.queue_id.localeCompare(b.queue_id) >= 0 ? a : b;
|
|
223
|
-
}
|
|
224
|
-
function snapshotClone(value) {
|
|
225
|
-
return {
|
|
226
|
-
...value,
|
|
227
|
-
applied_operation_ids: [...value.applied_operation_ids],
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
function normalizeRunStatus(value) {
|
|
231
|
-
if (typeof value !== "string") {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
const normalized = value.trim().toLowerCase();
|
|
235
|
-
if (normalized === "running" || normalized === "completed" || normalized === "failed" || normalized === "cancelled") {
|
|
236
|
-
return normalized;
|
|
237
|
-
}
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
function normalizeQueueRecordRow(row, nowMs, maxOperationIds) {
|
|
241
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
const record = row;
|
|
245
|
-
const queueId = normalizeString(record.queue_id);
|
|
246
|
-
const mode = normalizeRunMode(record.mode);
|
|
247
|
-
const state = normalizeQueueState(record.state);
|
|
248
|
-
if (!queueId || !mode || !state) {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
const createdAt = normalizeTimestamp(record.created_at_ms, nowMs);
|
|
252
|
-
const updatedAt = normalizeTimestamp(record.updated_at_ms, createdAt);
|
|
253
|
-
const revision = Math.max(1, normalizeMaxSteps(record.revision, 1));
|
|
254
|
-
const dedupeKey = normalizeString(record.dedupe_key) ?? `queue:${queueId}`;
|
|
255
|
-
const prompt = normalizePrompt(record.prompt);
|
|
256
|
-
const rootIssueId = normalizeIssueId(record.root_issue_id);
|
|
257
|
-
return {
|
|
258
|
-
v: 1,
|
|
259
|
-
queue_id: queueId,
|
|
260
|
-
dedupe_key: dedupeKey,
|
|
261
|
-
mode,
|
|
262
|
-
state,
|
|
263
|
-
prompt,
|
|
264
|
-
root_issue_id: rootIssueId,
|
|
265
|
-
max_steps: normalizeMaxSteps(record.max_steps, DEFAULT_MAX_STEPS),
|
|
266
|
-
command_id: normalizeString(record.command_id),
|
|
267
|
-
source: normalizeRunSource(record.source),
|
|
268
|
-
job_id: normalizeString(record.job_id),
|
|
269
|
-
started_at_ms: normalizeNullableTimestamp(record.started_at_ms),
|
|
270
|
-
updated_at_ms: updatedAt,
|
|
271
|
-
finished_at_ms: normalizeNullableTimestamp(record.finished_at_ms),
|
|
272
|
-
exit_code: normalizeNullableInt(record.exit_code),
|
|
273
|
-
pid: normalizeNullableInt(record.pid),
|
|
274
|
-
last_progress: normalizeString(record.last_progress),
|
|
275
|
-
created_at_ms: createdAt,
|
|
276
|
-
revision,
|
|
277
|
-
applied_operation_ids: normalizeAppliedOperationIds(record.applied_operation_ids, maxOperationIds),
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
function queueSnapshotFromRunSnapshotRecord(row, nowMs) {
|
|
281
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
const record = row;
|
|
285
|
-
const mode = normalizeRunMode(record.mode);
|
|
286
|
-
const status = normalizeRunStatus(record.status);
|
|
287
|
-
const jobId = normalizeString(record.job_id);
|
|
288
|
-
if (!mode || !status || !jobId) {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
const createdAt = normalizeTimestamp(record.started_at_ms, nowMs);
|
|
292
|
-
const updatedAt = normalizeTimestamp(record.updated_at_ms, createdAt);
|
|
293
|
-
const queueId = normalizeString(record.queue_id) ?? `rq-sync-${jobId}`;
|
|
294
|
-
return {
|
|
295
|
-
v: 1,
|
|
296
|
-
queue_id: queueId,
|
|
297
|
-
dedupe_key: `runtime:${jobId}`,
|
|
298
|
-
mode,
|
|
299
|
-
state: queueStateFromRunStatus(status),
|
|
300
|
-
prompt: normalizePrompt(record.prompt),
|
|
301
|
-
root_issue_id: normalizeIssueId(record.root_issue_id),
|
|
302
|
-
max_steps: normalizeMaxSteps(record.max_steps, DEFAULT_MAX_STEPS),
|
|
303
|
-
command_id: normalizeString(record.command_id),
|
|
304
|
-
source: normalizeRunSource(record.source),
|
|
305
|
-
job_id: jobId,
|
|
306
|
-
started_at_ms: normalizeNullableTimestamp(record.started_at_ms),
|
|
307
|
-
updated_at_ms: updatedAt,
|
|
308
|
-
finished_at_ms: normalizeNullableTimestamp(record.finished_at_ms),
|
|
309
|
-
exit_code: normalizeNullableInt(record.exit_code),
|
|
310
|
-
pid: normalizeNullableInt(record.pid),
|
|
311
|
-
last_progress: normalizeString(record.last_progress),
|
|
312
|
-
created_at_ms: createdAt,
|
|
313
|
-
revision: 1,
|
|
314
|
-
applied_operation_ids: [],
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
function mergeQueueAndRunSnapshot(queue, run) {
|
|
318
|
-
const status = runStatusFromQueueState(queue.state);
|
|
319
|
-
const startedAt = queue.started_at_ms ?? queue.created_at_ms;
|
|
320
|
-
const runtimeUpdatedAt = run?.updated_at_ms ?? 0;
|
|
321
|
-
const updatedAt = Math.max(queue.updated_at_ms, runtimeUpdatedAt);
|
|
322
|
-
const base = {
|
|
323
|
-
job_id: queue.job_id ?? queue.queue_id,
|
|
324
|
-
mode: queue.mode,
|
|
325
|
-
status,
|
|
326
|
-
prompt: queue.prompt,
|
|
327
|
-
root_issue_id: queue.root_issue_id,
|
|
328
|
-
max_steps: queue.max_steps,
|
|
329
|
-
command_id: queue.command_id,
|
|
330
|
-
source: queue.source,
|
|
331
|
-
started_at_ms: startedAt,
|
|
332
|
-
updated_at_ms: updatedAt,
|
|
333
|
-
finished_at_ms: queue.finished_at_ms,
|
|
334
|
-
exit_code: queue.exit_code,
|
|
335
|
-
pid: queue.pid,
|
|
336
|
-
last_progress: queue.last_progress,
|
|
337
|
-
queue_id: queue.queue_id,
|
|
338
|
-
queue_state: queue.state,
|
|
339
|
-
};
|
|
340
|
-
if (!run) {
|
|
341
|
-
return base;
|
|
342
|
-
}
|
|
343
|
-
return {
|
|
344
|
-
...base,
|
|
345
|
-
pid: run.pid ?? base.pid,
|
|
346
|
-
last_progress: run.last_progress ?? base.last_progress,
|
|
347
|
-
exit_code: run.exit_code ?? base.exit_code,
|
|
348
|
-
finished_at_ms: run.finished_at_ms ?? base.finished_at_ms,
|
|
349
|
-
updated_at_ms: Math.max(base.updated_at_ms, run.updated_at_ms),
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
export function runSnapshotFromQueueSnapshot(queue, runtime = null) {
|
|
353
|
-
return mergeQueueAndRunSnapshot(queue, runtime);
|
|
354
|
-
}
|
|
355
|
-
export class DurableRunQueue {
|
|
356
|
-
#store;
|
|
357
|
-
#nowMs;
|
|
358
|
-
#maxOperationIds;
|
|
359
|
-
#rowsById = new Map();
|
|
360
|
-
#idByDedupeKey = new Map();
|
|
361
|
-
#idByJobId = new Map();
|
|
362
|
-
#idsByRootIssueId = new Map();
|
|
363
|
-
#loaded = null;
|
|
364
|
-
#tail = Promise.resolve();
|
|
365
|
-
constructor(opts) {
|
|
366
|
-
this.#nowMs = opts.nowMs ?? defaultNowMs;
|
|
367
|
-
this.#maxOperationIds = Math.max(8, Math.trunc(opts.maxOperationIds ?? DEFAULT_MAX_OPERATION_IDS));
|
|
368
|
-
this.#store = opts.store ?? new FsJsonlStore(runQueuePath(opts.repoRoot));
|
|
369
|
-
}
|
|
370
|
-
async #runSerialized(fn) {
|
|
371
|
-
const run = this.#tail.then(fn, fn);
|
|
372
|
-
this.#tail = run.then(() => undefined, () => undefined);
|
|
373
|
-
return await run;
|
|
374
|
-
}
|
|
375
|
-
async #ensureLoaded() {
|
|
376
|
-
if (!this.#loaded) {
|
|
377
|
-
this.#loaded = this.#load();
|
|
378
|
-
}
|
|
379
|
-
await this.#loaded;
|
|
380
|
-
}
|
|
381
|
-
#rebuildIndexes() {
|
|
382
|
-
this.#idByDedupeKey.clear();
|
|
383
|
-
this.#idByJobId.clear();
|
|
384
|
-
this.#idsByRootIssueId.clear();
|
|
385
|
-
for (const row of this.#rowsById.values()) {
|
|
386
|
-
this.#idByDedupeKey.set(row.dedupe_key, row.queue_id);
|
|
387
|
-
if (row.job_id) {
|
|
388
|
-
this.#idByJobId.set(row.job_id, row.queue_id);
|
|
389
|
-
}
|
|
390
|
-
if (row.root_issue_id) {
|
|
391
|
-
const set = this.#idsByRootIssueId.get(row.root_issue_id) ?? new Set();
|
|
392
|
-
set.add(row.queue_id);
|
|
393
|
-
this.#idsByRootIssueId.set(row.root_issue_id, set);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
#replaceRow(next) {
|
|
398
|
-
this.#rowsById.set(next.queue_id, next);
|
|
399
|
-
this.#rebuildIndexes();
|
|
400
|
-
}
|
|
401
|
-
#allRowsSorted() {
|
|
402
|
-
return [...this.#rowsById.values()].sort(stableCompare);
|
|
403
|
-
}
|
|
404
|
-
#latestByRootIssueId(rootIssueId, preferRunning) {
|
|
405
|
-
const set = this.#idsByRootIssueId.get(rootIssueId);
|
|
406
|
-
if (!set || set.size === 0) {
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
const candidates = [];
|
|
410
|
-
for (const queueId of set) {
|
|
411
|
-
const row = this.#rowsById.get(queueId);
|
|
412
|
-
if (row) {
|
|
413
|
-
candidates.push(row);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
if (candidates.length === 0) {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
const running = preferRunning ? candidates.filter((row) => RUNNING_QUEUE_STATES.has(row.state)) : [];
|
|
420
|
-
const pool = running.length > 0 ? running : candidates;
|
|
421
|
-
return pool.reduce((best, row) => {
|
|
422
|
-
if (!best) {
|
|
423
|
-
return row;
|
|
424
|
-
}
|
|
425
|
-
if (row.updated_at_ms !== best.updated_at_ms) {
|
|
426
|
-
return row.updated_at_ms > best.updated_at_ms ? row : best;
|
|
427
|
-
}
|
|
428
|
-
return row.queue_id.localeCompare(best.queue_id) > 0 ? row : best;
|
|
429
|
-
}, null);
|
|
430
|
-
}
|
|
431
|
-
#rememberOperation(row, operationId) {
|
|
432
|
-
if (!operationId) {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if (row.applied_operation_ids.includes(operationId)) {
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
row.applied_operation_ids.push(operationId);
|
|
439
|
-
if (row.applied_operation_ids.length <= this.#maxOperationIds) {
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
row.applied_operation_ids.splice(0, row.applied_operation_ids.length - this.#maxOperationIds);
|
|
443
|
-
}
|
|
444
|
-
#isOperationReplay(row, operationId) {
|
|
445
|
-
if (!operationId) {
|
|
446
|
-
return false;
|
|
447
|
-
}
|
|
448
|
-
return row.applied_operation_ids.includes(operationId);
|
|
449
|
-
}
|
|
450
|
-
async #load() {
|
|
451
|
-
const rows = await this.#store.read();
|
|
452
|
-
const byId = new Map();
|
|
453
|
-
const nowMs = Math.trunc(this.#nowMs());
|
|
454
|
-
for (const row of rows) {
|
|
455
|
-
const normalized = normalizeQueueRecordRow(row, nowMs, this.#maxOperationIds);
|
|
456
|
-
if (!normalized) {
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
const existing = byId.get(normalized.queue_id);
|
|
460
|
-
if (!existing) {
|
|
461
|
-
byId.set(normalized.queue_id, normalized);
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
byId.set(normalized.queue_id, chooseLatest(existing, normalized));
|
|
465
|
-
}
|
|
466
|
-
this.#rowsById.clear();
|
|
467
|
-
for (const row of byId.values()) {
|
|
468
|
-
this.#rowsById.set(row.queue_id, row);
|
|
469
|
-
}
|
|
470
|
-
this.#rebuildIndexes();
|
|
471
|
-
}
|
|
472
|
-
async #persist() {
|
|
473
|
-
const rows = this.#allRowsSorted().map((row) => snapshotClone(row));
|
|
474
|
-
await this.#store.write(rows);
|
|
475
|
-
}
|
|
476
|
-
#newQueueId() {
|
|
477
|
-
return `rq-${crypto.randomUUID().slice(0, 12)}`;
|
|
478
|
-
}
|
|
479
|
-
#applyRunSnapshot(row, run, nowMs, operationId) {
|
|
480
|
-
if (this.#isOperationReplay(row, operationId)) {
|
|
481
|
-
return row;
|
|
482
|
-
}
|
|
483
|
-
const next = snapshotClone(row);
|
|
484
|
-
let changed = false;
|
|
485
|
-
const targetState = queueStateFromRunStatus(run.status);
|
|
486
|
-
if (next.state !== targetState) {
|
|
487
|
-
if (canTransition(next.state, targetState)) {
|
|
488
|
-
next.state = targetState;
|
|
489
|
-
changed = true;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (next.mode !== run.mode) {
|
|
493
|
-
next.mode = run.mode;
|
|
494
|
-
changed = true;
|
|
495
|
-
}
|
|
496
|
-
const prompt = normalizePrompt(run.prompt);
|
|
497
|
-
if (next.prompt !== prompt) {
|
|
498
|
-
next.prompt = prompt;
|
|
499
|
-
changed = true;
|
|
500
|
-
}
|
|
501
|
-
const rootIssueId = normalizeIssueId(run.root_issue_id);
|
|
502
|
-
if (rootIssueId && next.root_issue_id !== rootIssueId) {
|
|
503
|
-
next.root_issue_id = rootIssueId;
|
|
504
|
-
changed = true;
|
|
505
|
-
}
|
|
506
|
-
const maxSteps = normalizeMaxSteps(run.max_steps, next.max_steps || DEFAULT_MAX_STEPS);
|
|
507
|
-
if (next.max_steps !== maxSteps) {
|
|
508
|
-
next.max_steps = maxSteps;
|
|
509
|
-
changed = true;
|
|
510
|
-
}
|
|
511
|
-
const commandId = normalizeString(run.command_id);
|
|
512
|
-
if (next.command_id !== commandId) {
|
|
513
|
-
next.command_id = commandId;
|
|
514
|
-
changed = true;
|
|
515
|
-
}
|
|
516
|
-
if (next.source !== run.source) {
|
|
517
|
-
next.source = run.source;
|
|
518
|
-
changed = true;
|
|
519
|
-
}
|
|
520
|
-
const jobId = normalizeString(run.job_id);
|
|
521
|
-
if (jobId && next.job_id !== jobId) {
|
|
522
|
-
next.job_id = jobId;
|
|
523
|
-
changed = true;
|
|
524
|
-
}
|
|
525
|
-
const startedAt = normalizeNullableTimestamp(run.started_at_ms);
|
|
526
|
-
if (startedAt != null && next.started_at_ms !== startedAt) {
|
|
527
|
-
next.started_at_ms = startedAt;
|
|
528
|
-
changed = true;
|
|
529
|
-
}
|
|
530
|
-
const updatedAt = Math.max(next.updated_at_ms, normalizeTimestamp(run.updated_at_ms, nowMs), nowMs);
|
|
531
|
-
if (updatedAt !== next.updated_at_ms) {
|
|
532
|
-
next.updated_at_ms = updatedAt;
|
|
533
|
-
changed = true;
|
|
534
|
-
}
|
|
535
|
-
const finishedAt = normalizeNullableTimestamp(run.finished_at_ms);
|
|
536
|
-
if (finishedAt !== next.finished_at_ms) {
|
|
537
|
-
next.finished_at_ms = finishedAt;
|
|
538
|
-
changed = true;
|
|
539
|
-
}
|
|
540
|
-
const exitCode = normalizeNullableInt(run.exit_code);
|
|
541
|
-
if (exitCode !== next.exit_code) {
|
|
542
|
-
next.exit_code = exitCode;
|
|
543
|
-
changed = true;
|
|
544
|
-
}
|
|
545
|
-
const pid = normalizeNullableInt(run.pid);
|
|
546
|
-
if (pid !== next.pid) {
|
|
547
|
-
next.pid = pid;
|
|
548
|
-
changed = true;
|
|
549
|
-
}
|
|
550
|
-
const progress = normalizeString(run.last_progress);
|
|
551
|
-
if (progress !== next.last_progress) {
|
|
552
|
-
next.last_progress = progress;
|
|
553
|
-
changed = true;
|
|
554
|
-
}
|
|
555
|
-
if (isTerminalState(next.state) && next.finished_at_ms == null) {
|
|
556
|
-
next.finished_at_ms = nowMs;
|
|
557
|
-
changed = true;
|
|
558
|
-
}
|
|
559
|
-
if (!changed && !operationId) {
|
|
560
|
-
return row;
|
|
561
|
-
}
|
|
562
|
-
next.revision = next.revision + 1;
|
|
563
|
-
this.#rememberOperation(next, operationId);
|
|
564
|
-
return next;
|
|
565
|
-
}
|
|
566
|
-
#findByAnyId(idOrRoot) {
|
|
567
|
-
const trimmed = idOrRoot.trim();
|
|
568
|
-
if (!trimmed) {
|
|
569
|
-
return null;
|
|
570
|
-
}
|
|
571
|
-
const byQueueId = this.#rowsById.get(trimmed);
|
|
572
|
-
if (byQueueId) {
|
|
573
|
-
return byQueueId;
|
|
574
|
-
}
|
|
575
|
-
const byJobId = this.#idByJobId.get(trimmed);
|
|
576
|
-
if (byJobId) {
|
|
577
|
-
return this.#rowsById.get(byJobId) ?? null;
|
|
578
|
-
}
|
|
579
|
-
const normalizedRoot = normalizeIssueId(trimmed);
|
|
580
|
-
if (!normalizedRoot) {
|
|
581
|
-
return null;
|
|
582
|
-
}
|
|
583
|
-
return this.#latestByRootIssueId(normalizedRoot, false);
|
|
584
|
-
}
|
|
585
|
-
async enqueue(opts) {
|
|
586
|
-
return await this.#runSerialized(async () => {
|
|
587
|
-
await this.#ensureLoaded();
|
|
588
|
-
const dedupeKey = normalizeString(opts.dedupeKey);
|
|
589
|
-
if (!dedupeKey) {
|
|
590
|
-
throw new Error("run_queue_dedupe_key_required");
|
|
591
|
-
}
|
|
592
|
-
const operationId = normalizeOperationId(opts.operationId);
|
|
593
|
-
const existingId = this.#idByDedupeKey.get(dedupeKey);
|
|
594
|
-
if (existingId) {
|
|
595
|
-
const existing = this.#rowsById.get(existingId);
|
|
596
|
-
if (!existing) {
|
|
597
|
-
throw new Error("run_queue_internal_missing_existing_record");
|
|
598
|
-
}
|
|
599
|
-
return snapshotClone(existing);
|
|
600
|
-
}
|
|
601
|
-
const mode = normalizeRunMode(opts.mode);
|
|
602
|
-
if (!mode) {
|
|
603
|
-
throw new Error("run_queue_invalid_mode");
|
|
604
|
-
}
|
|
605
|
-
const nowMs = normalizeTimestamp(opts.nowMs, this.#nowMs());
|
|
606
|
-
const row = {
|
|
607
|
-
v: 1,
|
|
608
|
-
queue_id: this.#newQueueId(),
|
|
609
|
-
dedupe_key: dedupeKey,
|
|
610
|
-
mode,
|
|
611
|
-
state: "queued",
|
|
612
|
-
prompt: normalizePrompt(opts.prompt),
|
|
613
|
-
root_issue_id: normalizeIssueId(opts.rootIssueId),
|
|
614
|
-
max_steps: normalizeMaxSteps(opts.maxSteps, DEFAULT_MAX_STEPS),
|
|
615
|
-
command_id: normalizeString(opts.commandId),
|
|
616
|
-
source: normalizeRunSource(opts.source),
|
|
617
|
-
job_id: null,
|
|
618
|
-
started_at_ms: null,
|
|
619
|
-
updated_at_ms: nowMs,
|
|
620
|
-
finished_at_ms: null,
|
|
621
|
-
exit_code: null,
|
|
622
|
-
pid: null,
|
|
623
|
-
last_progress: null,
|
|
624
|
-
created_at_ms: nowMs,
|
|
625
|
-
revision: 1,
|
|
626
|
-
applied_operation_ids: [],
|
|
627
|
-
};
|
|
628
|
-
this.#rememberOperation(row, operationId);
|
|
629
|
-
this.#replaceRow(row);
|
|
630
|
-
await this.#persist();
|
|
631
|
-
return snapshotClone(row);
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
async claim(opts = {}) {
|
|
635
|
-
return await this.#runSerialized(async () => {
|
|
636
|
-
await this.#ensureLoaded();
|
|
637
|
-
const operationId = normalizeOperationId(opts.operationId);
|
|
638
|
-
const nowMs = normalizeTimestamp(opts.nowMs, this.#nowMs());
|
|
639
|
-
const queueId = normalizeString(opts.queueId);
|
|
640
|
-
const row = (queueId ? this.#rowsById.get(queueId) : null) ??
|
|
641
|
-
this.#allRowsSorted().find((candidate) => candidate.state === "queued") ??
|
|
642
|
-
null;
|
|
643
|
-
if (!row) {
|
|
644
|
-
return null;
|
|
645
|
-
}
|
|
646
|
-
if (row.state !== "queued") {
|
|
647
|
-
if (row.state === "active") {
|
|
648
|
-
return snapshotClone(row);
|
|
649
|
-
}
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
if (this.#isOperationReplay(row, operationId)) {
|
|
653
|
-
return snapshotClone(row);
|
|
654
|
-
}
|
|
655
|
-
const next = snapshotClone(row);
|
|
656
|
-
next.state = "active";
|
|
657
|
-
next.updated_at_ms = nowMs;
|
|
658
|
-
next.started_at_ms = next.started_at_ms ?? nowMs;
|
|
659
|
-
next.revision += 1;
|
|
660
|
-
this.#rememberOperation(next, operationId);
|
|
661
|
-
this.#replaceRow(next);
|
|
662
|
-
await this.#persist();
|
|
663
|
-
return snapshotClone(next);
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
async activate(opts) {
|
|
667
|
-
return await this.claim(opts);
|
|
668
|
-
}
|
|
669
|
-
async transition(opts) {
|
|
670
|
-
return await this.#runSerialized(async () => {
|
|
671
|
-
await this.#ensureLoaded();
|
|
672
|
-
const queueId = normalizeString(opts.queueId);
|
|
673
|
-
if (!queueId) {
|
|
674
|
-
throw new Error("run_queue_missing_queue_id");
|
|
675
|
-
}
|
|
676
|
-
const row = this.#rowsById.get(queueId);
|
|
677
|
-
if (!row) {
|
|
678
|
-
throw new Error("run_queue_not_found");
|
|
679
|
-
}
|
|
680
|
-
const operationId = normalizeOperationId(opts.operationId);
|
|
681
|
-
if (this.#isOperationReplay(row, operationId)) {
|
|
682
|
-
return snapshotClone(row);
|
|
683
|
-
}
|
|
684
|
-
const toState = normalizeQueueState(opts.toState);
|
|
685
|
-
if (!toState) {
|
|
686
|
-
throw new Error("run_queue_invalid_state");
|
|
687
|
-
}
|
|
688
|
-
if (!canTransition(row.state, toState)) {
|
|
689
|
-
throw new Error(`invalid_run_queue_transition:${row.state}->${toState}`);
|
|
690
|
-
}
|
|
691
|
-
if (row.state === toState && !operationId) {
|
|
692
|
-
return snapshotClone(row);
|
|
693
|
-
}
|
|
694
|
-
const nowMs = normalizeTimestamp(opts.nowMs, this.#nowMs());
|
|
695
|
-
const next = snapshotClone(row);
|
|
696
|
-
next.state = toState;
|
|
697
|
-
next.updated_at_ms = nowMs;
|
|
698
|
-
if (toState === "active" && next.started_at_ms == null) {
|
|
699
|
-
next.started_at_ms = nowMs;
|
|
700
|
-
}
|
|
701
|
-
if (isTerminalState(toState) && next.finished_at_ms == null) {
|
|
702
|
-
next.finished_at_ms = nowMs;
|
|
703
|
-
}
|
|
704
|
-
next.revision += 1;
|
|
705
|
-
this.#rememberOperation(next, operationId);
|
|
706
|
-
this.#replaceRow(next);
|
|
707
|
-
await this.#persist();
|
|
708
|
-
return snapshotClone(next);
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
async bindRunSnapshot(opts) {
|
|
712
|
-
return await this.#runSerialized(async () => {
|
|
713
|
-
await this.#ensureLoaded();
|
|
714
|
-
const queueId = normalizeString(opts.queueId);
|
|
715
|
-
if (!queueId) {
|
|
716
|
-
throw new Error("run_queue_missing_queue_id");
|
|
717
|
-
}
|
|
718
|
-
const row = this.#rowsById.get(queueId);
|
|
719
|
-
if (!row) {
|
|
720
|
-
throw new Error("run_queue_not_found");
|
|
721
|
-
}
|
|
722
|
-
const nowMs = normalizeTimestamp(opts.nowMs, this.#nowMs());
|
|
723
|
-
const operationId = normalizeOperationId(opts.operationId);
|
|
724
|
-
const next = this.#applyRunSnapshot(row, opts.run, nowMs, operationId);
|
|
725
|
-
if (next === row) {
|
|
726
|
-
return snapshotClone(row);
|
|
727
|
-
}
|
|
728
|
-
this.#replaceRow(next);
|
|
729
|
-
await this.#persist();
|
|
730
|
-
return snapshotClone(next);
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
async applyRunSnapshot(opts) {
|
|
734
|
-
return await this.#runSerialized(async () => {
|
|
735
|
-
await this.#ensureLoaded();
|
|
736
|
-
const nowMs = normalizeTimestamp(opts.nowMs, this.#nowMs());
|
|
737
|
-
const operationId = normalizeOperationId(opts.operationId);
|
|
738
|
-
const queueId = normalizeString(opts.queueId);
|
|
739
|
-
let row = null;
|
|
740
|
-
if (queueId) {
|
|
741
|
-
row = this.#rowsById.get(queueId) ?? null;
|
|
742
|
-
}
|
|
743
|
-
if (!row) {
|
|
744
|
-
const jobId = normalizeString(opts.run.job_id);
|
|
745
|
-
if (jobId) {
|
|
746
|
-
const existingQueueId = this.#idByJobId.get(jobId);
|
|
747
|
-
if (existingQueueId) {
|
|
748
|
-
row = this.#rowsById.get(existingQueueId) ?? null;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
if (!row) {
|
|
753
|
-
const rootIssueId = normalizeIssueId(opts.run.root_issue_id);
|
|
754
|
-
if (rootIssueId) {
|
|
755
|
-
row = this.#latestByRootIssueId(rootIssueId, opts.run.status === "running");
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
if (!row && opts.createIfMissing) {
|
|
759
|
-
const next = queueSnapshotFromRunSnapshotRecord(opts.run, nowMs);
|
|
760
|
-
if (!next) {
|
|
761
|
-
return null;
|
|
762
|
-
}
|
|
763
|
-
if (operationId) {
|
|
764
|
-
this.#rememberOperation(next, operationId);
|
|
765
|
-
next.revision += 1;
|
|
766
|
-
}
|
|
767
|
-
this.#replaceRow(next);
|
|
768
|
-
await this.#persist();
|
|
769
|
-
return snapshotClone(next);
|
|
770
|
-
}
|
|
771
|
-
if (!row) {
|
|
772
|
-
return null;
|
|
773
|
-
}
|
|
774
|
-
const next = this.#applyRunSnapshot(row, opts.run, nowMs, operationId);
|
|
775
|
-
if (next === row) {
|
|
776
|
-
return snapshotClone(row);
|
|
777
|
-
}
|
|
778
|
-
this.#replaceRow(next);
|
|
779
|
-
await this.#persist();
|
|
780
|
-
return snapshotClone(next);
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
async get(idOrRoot) {
|
|
784
|
-
await this.#ensureLoaded();
|
|
785
|
-
const row = this.#findByAnyId(idOrRoot);
|
|
786
|
-
return row ? snapshotClone(row) : null;
|
|
787
|
-
}
|
|
788
|
-
async list(opts = {}) {
|
|
789
|
-
await this.#ensureLoaded();
|
|
790
|
-
const limit = Math.max(1, Math.min(500, Math.trunc(opts.limit ?? 100)));
|
|
791
|
-
const stateFilter = opts.states && opts.states.length > 0
|
|
792
|
-
? new Set(opts.states.filter((state) => RUN_QUEUE_STATE_VALUES.includes(state)))
|
|
793
|
-
: null;
|
|
794
|
-
const rows = this.#allRowsSorted().filter((row) => {
|
|
795
|
-
if (!stateFilter) {
|
|
796
|
-
return true;
|
|
797
|
-
}
|
|
798
|
-
return stateFilter.has(row.state);
|
|
799
|
-
});
|
|
800
|
-
return rows
|
|
801
|
-
.slice(-limit)
|
|
802
|
-
.reverse()
|
|
803
|
-
.map((row) => snapshotClone(row));
|
|
804
|
-
}
|
|
805
|
-
async listRunSnapshots(opts = {}) {
|
|
806
|
-
const queueStates = queueStatesForRunStatusFilter(opts.status);
|
|
807
|
-
if (Array.isArray(queueStates) && queueStates.length === 0) {
|
|
808
|
-
return [];
|
|
809
|
-
}
|
|
810
|
-
const rows = await this.list({ states: queueStates ?? undefined, limit: opts.limit });
|
|
811
|
-
return rows.map((row) => {
|
|
812
|
-
const runtime = row.job_id ? (opts.runtimeByJobId?.get(row.job_id) ?? null) : null;
|
|
813
|
-
return runSnapshotFromQueueSnapshot(row, runtime);
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
}
|