@rulvar/cli 1.25.0 → 1.27.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/dist/cli.js +1 -1
- package/dist/index.d.ts +59 -3
- package/dist/index.js +206 -75
- package/dist/{io-CAXfPWq7.js → io-C3T5nbNg.js} +4 -4
- package/package.json +7 -7
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -164,13 +164,69 @@ interface CreateServerOptions {
|
|
|
164
164
|
*/
|
|
165
165
|
priceUsd?: (servedBy: ModelRef, usage: Usage) => number | undefined;
|
|
166
166
|
/**
|
|
167
|
-
* Opt-in retention (OQ-20 executed at M8-T04): evaluated
|
|
167
|
+
* Opt-in DURABLE retention (OQ-20 executed at M8-T04): evaluated
|
|
168
168
|
* when a tracked run settles terminally; a true verdict applies
|
|
169
169
|
* engine.deleteRun (transcript cascade, then the journal) and
|
|
170
|
-
* untracks the run.
|
|
170
|
+
* untracks the run. This deletes the durable record; to release only
|
|
171
|
+
* process memory, use `memoryRetention` or `maxTrackedRuns`. Absent
|
|
172
|
+
* means nothing is deleted.
|
|
171
173
|
*/
|
|
172
174
|
retention?: (meta: RunMeta) => boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Opt-in retention of PROCESS MEMORY, decoupled from the durable kind
|
|
177
|
+
* (v1.25.0 scale review P1-2): evaluated when a tracked run settles
|
|
178
|
+
* terminally, after `retention`; a true verdict releases the tracked
|
|
179
|
+
* state (args, outcome, handle, SSE buffer) while the journal and
|
|
180
|
+
* transcripts stay untouched, after which GET status/cost serve from
|
|
181
|
+
* the store exactly as for a run another process owns, and GET events
|
|
182
|
+
* answers with the documented empty stream for a run not live here.
|
|
183
|
+
*/
|
|
184
|
+
memoryRetention?: (meta: RunMeta) => boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Cap on SETTLED tracked runs kept in process memory: when a run
|
|
187
|
+
* settles terminally and neither retention released it, the oldest
|
|
188
|
+
* settled tracked runs beyond the cap are released exactly like a
|
|
189
|
+
* `memoryRetention` verdict (durable state untouched). Live runs are
|
|
190
|
+
* never evicted and do not count toward the cap. Absent means no cap.
|
|
191
|
+
* Validated at construction: a non-negative safe integer (zero keeps
|
|
192
|
+
* no settled runs), anything else is a typed ConfigError.
|
|
193
|
+
*/
|
|
194
|
+
maxTrackedRuns?: number;
|
|
195
|
+
/**
|
|
196
|
+
* Upper bound on buffered SSE replay events per tracked run: past the
|
|
197
|
+
* bound the OLDEST buffered events are dropped in chunks (so the
|
|
198
|
+
* retained replay window stays at least seven eighths of the bound)
|
|
199
|
+
* and counted. A replay that no longer reaches back to a client's
|
|
200
|
+
* cursor carries `x-rulvar-events-dropped: <count>` and a leading SSE
|
|
201
|
+
* comment naming the first retained seq; the journal remains the
|
|
202
|
+
* durable record of the run itself. Absent means unbounded (the
|
|
203
|
+
* historical behavior). Validated at construction: a positive safe
|
|
204
|
+
* integer, anything else is a typed ConfigError.
|
|
205
|
+
*/
|
|
206
|
+
maxBufferedEventsPerRun?: number;
|
|
207
|
+
/**
|
|
208
|
+
* Upper bound on SSE frames PENDING in one client connection's
|
|
209
|
+
* response queue, replay and live feed alike (v1.26.0 deep E2E
|
|
210
|
+
* review P1-2: the replay buffer bound does not bound what a
|
|
211
|
+
* connected consumer that stopped reading accumulates). When a
|
|
212
|
+
* connection's pending queue reaches the bound, the server unhooks
|
|
213
|
+
* the feed, appends an SSE comment naming the bound, and CLOSES that
|
|
214
|
+
* connection; queued frames stay readable, and the standard
|
|
215
|
+
* Last-Event-ID reconnect resumes strictly after the last frame the
|
|
216
|
+
* client consumed. A replay longer than the bound is likewise
|
|
217
|
+
* delivered in bounded chunks across reconnects, so pending memory
|
|
218
|
+
* per connection is O(bound), never O(events). Validated at
|
|
219
|
+
* construction: a positive safe integer. Defaults to 10000.
|
|
220
|
+
*/
|
|
221
|
+
maxPendingEventsPerClient?: number;
|
|
173
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* The default per-connection pending-frame bound: generous enough that
|
|
225
|
+
* a reading consumer never notices (a normal reader keeps the queue
|
|
226
|
+
* near empty), small enough that a consumer that stopped reading
|
|
227
|
+
* cannot grow process memory past a few megabytes per connection.
|
|
228
|
+
*/
|
|
229
|
+
declare const DEFAULT_MAX_PENDING_EVENTS_PER_CLIENT = 1e4;
|
|
174
230
|
interface RulvarServer {
|
|
175
231
|
fetch(req: Request): Promise<Response>;
|
|
176
232
|
}
|
|
@@ -284,4 +340,4 @@ declare function toOtel(run: {
|
|
|
284
340
|
result: Promise<RunOutcome<unknown>>;
|
|
285
341
|
}, tracer: TracerLike, options?: ToOtelOptions): Promise<number>;
|
|
286
342
|
//#endregion
|
|
287
|
-
export { type AssembledCli, type CliConfig, type CliIo, type CommandContext, type CreateServerOptions, type CreateWorkerOptions, DEFAULT_STORE_DIR, DEFAULT_WORKER_TTL_MS, HELP, type RulvarServer, type SpanLike, type ToOtelOptions, type TracerLike, type Worker, assembleEngine, attachProgress, createServer, createWorker, driveRun, inspectCommand, loadCliConfig, loadWorkflowModule, looksLikeFile, processIo, renderEventLine, reportOutcome, resumeCommand, runCli, runCommand, runsLsCommand, toOtel };
|
|
343
|
+
export { type AssembledCli, type CliConfig, type CliIo, type CommandContext, type CreateServerOptions, type CreateWorkerOptions, DEFAULT_MAX_PENDING_EVENTS_PER_CLIENT, DEFAULT_STORE_DIR, DEFAULT_WORKER_TTL_MS, HELP, type KbSweepCliConfig, type LoadedWorkflowModule, type OtelContextApi, type RulvarServer, type SpanLike, type ToOtelOptions, type TracerLike, type Worker, assembleEngine, attachProgress, createServer, createWorker, driveRun, inspectCommand, loadCliConfig, loadWorkflowModule, looksLikeFile, processIo, renderEventLine, reportOutcome, resumeCommand, runCli, runCommand, runsLsCommand, toOtel };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as resumeCommand, c as driveRun, d as renderEventLine, f as DEFAULT_STORE_DIR, g as looksLikeFile, h as loadWorkflowModule, i as inspectCommand, l as reportOutcome, m as loadCliConfig, n as HELP, o as runCommand, p as assembleEngine, r as runCli, s as runsLsCommand, t as processIo, u as attachProgress } from "./io-
|
|
2
|
-
import { ConfigError, InvalidResolutionError, JournalCompatibilityError, LeaseHeldError, Replayer, RulvarError, buildDeriverRegistry, costReportFromJournal, maskSecrets, normalizeEntry, scanJournalCompatibility, validateSchemaSpec } from "@rulvar/core";
|
|
1
|
+
import { a as resumeCommand, c as driveRun, d as renderEventLine, f as DEFAULT_STORE_DIR, g as looksLikeFile, h as loadWorkflowModule, i as inspectCommand, l as reportOutcome, m as loadCliConfig, n as HELP, o as runCommand, p as assembleEngine, r as runCli, s as runsLsCommand, t as processIo, u as attachProgress } from "./io-C3T5nbNg.js";
|
|
2
|
+
import { ConfigError, InvalidResolutionError, JournalCompatibilityError, LeaseHeldError, Replayer, RulvarError, buildDeriverRegistry, costReportFromJournal, maskSecrets, normalizeEntry, readRunMeta, scanJournalCompatibility, validateSchemaSpec } from "@rulvar/core";
|
|
3
3
|
//#region src/server.ts
|
|
4
4
|
/**
|
|
5
5
|
* createServer (M8-T01): the HTTP shell over the public engine API
|
|
@@ -20,7 +20,12 @@ import { ConfigError, InvalidResolutionError, JournalCompatibilityError, LeaseHe
|
|
|
20
20
|
* maps Last-Event-ID to the event seq (the per-run telemetry counter);
|
|
21
21
|
* replay is at-least-once by design, matching the
|
|
22
22
|
* journal-backed re-emission contract (consumers
|
|
23
|
-
* deduplicate on `replayed`).
|
|
23
|
+
* deduplicate on `replayed`). A terminal settle closes connected
|
|
24
|
+
* streams only AFTER the segment's event pump has delivered the full
|
|
25
|
+
* tail (run:end included), and every connection's pending queue is
|
|
26
|
+
* bounded by maxPendingEventsPerClient: a consumer that stops reading
|
|
27
|
+
* is closed at the bound and resumes through the standard
|
|
28
|
+
* Last-Event-ID replay window (v1.26.0 deep E2E review P1-1/P1-2).
|
|
24
29
|
*
|
|
25
30
|
* The server is a single-process shell: it tracks the runs it started
|
|
26
31
|
* (or resumed) in memory and serves everything else from the engine's
|
|
@@ -30,6 +35,13 @@ import { ConfigError, InvalidResolutionError, JournalCompatibilityError, LeaseHe
|
|
|
30
35
|
* run resumes on a queue worker (createWorker, M8-T02), not here,
|
|
31
36
|
* because original run arguments are not journaled in v1 (OQ-21).
|
|
32
37
|
*/
|
|
38
|
+
/**
|
|
39
|
+
* The default per-connection pending-frame bound: generous enough that
|
|
40
|
+
* a reading consumer never notices (a normal reader keeps the queue
|
|
41
|
+
* near empty), small enough that a consumer that stopped reading
|
|
42
|
+
* cannot grow process memory past a few megabytes per connection.
|
|
43
|
+
*/
|
|
44
|
+
const DEFAULT_MAX_PENDING_EVENTS_PER_CLIENT = 1e4;
|
|
33
45
|
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
34
46
|
const wallClock = Date.now.bind(globalThis);
|
|
35
47
|
function json(status, body) {
|
|
@@ -73,34 +85,83 @@ function suspensionKeyOf(entry) {
|
|
|
73
85
|
}
|
|
74
86
|
if (entry.kind === "approval") return `${APPROVAL_KEY_PREFIX}${entry.seq}`;
|
|
75
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Rejects a numeric cap outside its documented domain with a typed
|
|
90
|
+
* ConfigError at construction (v1.26.0 deep E2E review P2-1: NaN
|
|
91
|
+
* silently meant unbounded, Infinity looked like a cap without capping,
|
|
92
|
+
* negatives and fractions produced policies nobody asked for).
|
|
93
|
+
*/
|
|
94
|
+
function requireCap(name, value, minimum) {
|
|
95
|
+
if (value === void 0) return;
|
|
96
|
+
if (!Number.isSafeInteger(value) || value < minimum) throw new ConfigError(`createServer ${name} must be a ${minimum === 0 ? "non-negative" : "positive"} safe integer, got ${String(value)}`);
|
|
97
|
+
}
|
|
76
98
|
function createServer(options) {
|
|
77
99
|
const { engine, workflows } = options;
|
|
100
|
+
requireCap("maxTrackedRuns", options.maxTrackedRuns, 0);
|
|
101
|
+
requireCap("maxBufferedEventsPerRun", options.maxBufferedEventsPerRun, 1);
|
|
102
|
+
requireCap("maxPendingEventsPerClient", options.maxPendingEventsPerClient, 1);
|
|
103
|
+
const pendingCap = options.maxPendingEventsPerClient ?? 1e4;
|
|
78
104
|
const journal = engine.stores.journal;
|
|
79
105
|
const runs = /* @__PURE__ */ new Map();
|
|
106
|
+
/**
|
|
107
|
+
* Buffers one event under the configured bound. Overflow drops the
|
|
108
|
+
* oldest chunk (an eighth of the bound) in one splice, so the
|
|
109
|
+
* amortized cost per event stays O(1) and the retained window never
|
|
110
|
+
* falls below seven eighths of the bound.
|
|
111
|
+
*/
|
|
112
|
+
function pushBuffered(run, event) {
|
|
113
|
+
run.buffer.push(event);
|
|
114
|
+
const max = options.maxBufferedEventsPerRun;
|
|
115
|
+
if (max !== void 0 && run.buffer.length > max) {
|
|
116
|
+
const chunk = Math.max(1, Math.floor(max / 8));
|
|
117
|
+
run.buffer.splice(0, chunk);
|
|
118
|
+
run.dropped += chunk;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Releases settled tracked runs beyond maxTrackedRuns, oldest first
|
|
123
|
+
* (Map insertion order), durable state untouched. Live runs never
|
|
124
|
+
* count and are never evicted.
|
|
125
|
+
*/
|
|
126
|
+
function enforceTrackedCap() {
|
|
127
|
+
const cap = options.maxTrackedRuns;
|
|
128
|
+
if (cap === void 0) return;
|
|
129
|
+
const settled = [...runs.values()].filter((run) => run.done);
|
|
130
|
+
for (const run of settled.slice(0, Math.max(0, settled.length - cap))) runs.delete(run.runId);
|
|
131
|
+
}
|
|
80
132
|
/** Pumps one resume segment's events into the buffer and the feeds. */
|
|
81
133
|
function attach(run, handle) {
|
|
82
134
|
run.handle = handle;
|
|
83
135
|
run.outcome = void 0;
|
|
84
|
-
|
|
136
|
+
let pumpFailed = false;
|
|
137
|
+
const pump = (async () => {
|
|
85
138
|
for await (const event of handle.events) {
|
|
86
|
-
run
|
|
139
|
+
pushBuffered(run, event);
|
|
87
140
|
for (const feed of [...run.feeds]) feed(event);
|
|
88
141
|
}
|
|
89
|
-
})().catch(() =>
|
|
90
|
-
|
|
142
|
+
})().catch(() => {
|
|
143
|
+
pumpFailed = true;
|
|
144
|
+
});
|
|
145
|
+
handle.result.then(async (outcome) => {
|
|
91
146
|
run.outcome = outcome;
|
|
92
|
-
if (outcome.status
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
147
|
+
if (outcome.status === "suspended") return;
|
|
148
|
+
await pump;
|
|
149
|
+
run.done = true;
|
|
150
|
+
for (const feed of [...run.feeds]) feed(null, pumpFailed ? "event pump failed; the stream may be incomplete, reconnect with Last-Event-ID to replay" : void 0);
|
|
151
|
+
run.feeds.clear();
|
|
152
|
+
(async () => {
|
|
153
|
+
const meta = options.retention === void 0 && options.memoryRetention === void 0 ? void 0 : await metaOf(run.runId);
|
|
154
|
+
if (meta !== void 0 && options.retention?.(meta) === true) {
|
|
155
|
+
await engine.deleteRun(run.runId);
|
|
156
|
+
runs.delete(run.runId);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (meta !== void 0 && options.memoryRetention?.(meta) === true) {
|
|
160
|
+
runs.delete(run.runId);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
enforceTrackedCap();
|
|
164
|
+
})().catch(() => void 0);
|
|
104
165
|
}).catch(() => void 0);
|
|
105
166
|
}
|
|
106
167
|
function track(runId, workflowName, args, handle) {
|
|
@@ -109,6 +170,7 @@ function createServer(options) {
|
|
|
109
170
|
workflowName,
|
|
110
171
|
args,
|
|
111
172
|
buffer: [],
|
|
173
|
+
dropped: 0,
|
|
112
174
|
feeds: /* @__PURE__ */ new Set(),
|
|
113
175
|
handle,
|
|
114
176
|
done: false,
|
|
@@ -119,7 +181,7 @@ function createServer(options) {
|
|
|
119
181
|
return run;
|
|
120
182
|
}
|
|
121
183
|
async function metaOf(runId) {
|
|
122
|
-
return (
|
|
184
|
+
return readRunMeta(journal, runId);
|
|
123
185
|
}
|
|
124
186
|
async function startRun(req) {
|
|
125
187
|
let body;
|
|
@@ -221,26 +283,68 @@ function createServer(options) {
|
|
|
221
283
|
const lastEventId = req.headers.get("last-event-id");
|
|
222
284
|
const encoder = new TextEncoder();
|
|
223
285
|
let feed;
|
|
286
|
+
const pendingOf = (controller) => {
|
|
287
|
+
const desired = controller.desiredSize;
|
|
288
|
+
return desired === null ? null : 1 - desired;
|
|
289
|
+
};
|
|
290
|
+
const slowConsumerComment = `: pending frames reached maxPendingEventsPerClient (${pendingCap}); reconnect with Last-Event-ID to continue
|
|
291
|
+
|
|
292
|
+
`;
|
|
293
|
+
let cursor;
|
|
294
|
+
if (lastEventId !== null) {
|
|
295
|
+
const parsed = Number(lastEventId);
|
|
296
|
+
if (Number.isFinite(parsed)) cursor = parsed;
|
|
297
|
+
}
|
|
298
|
+
let startIndex = 0;
|
|
299
|
+
if (cursor !== void 0) {
|
|
300
|
+
let lo = 0;
|
|
301
|
+
let hi = run.buffer.length;
|
|
302
|
+
while (lo < hi) {
|
|
303
|
+
const mid = lo + hi >> 1;
|
|
304
|
+
if (run.buffer[mid].seq <= cursor) lo = mid + 1;
|
|
305
|
+
else hi = mid;
|
|
306
|
+
}
|
|
307
|
+
startIndex = lo;
|
|
308
|
+
}
|
|
309
|
+
const firstRetained = run.buffer.length > 0 ? run.buffer[0].seq : void 0;
|
|
310
|
+
const gap = run.dropped > 0 && (cursor === void 0 || firstRetained === void 0 || cursor < firstRetained);
|
|
311
|
+
const headers = {
|
|
312
|
+
"content-type": "text/event-stream",
|
|
313
|
+
"cache-control": "no-cache",
|
|
314
|
+
...run.dropped > 0 ? { "x-rulvar-events-dropped": String(run.dropped) } : {}
|
|
315
|
+
};
|
|
224
316
|
const stream = new ReadableStream({
|
|
225
317
|
start(controller) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
318
|
+
if (gap) controller.enqueue(encoder.encode(`: replay window starts at seq ${String(firstRetained ?? "none")}; ${run.dropped} earlier events were dropped from the in-memory buffer (the journal is the durable record)
|
|
319
|
+
|
|
320
|
+
`));
|
|
321
|
+
for (let i = startIndex; i < run.buffer.length; i += 1) {
|
|
322
|
+
const pending = pendingOf(controller);
|
|
323
|
+
if (pending === null) return;
|
|
324
|
+
if (pending >= pendingCap) {
|
|
325
|
+
controller.enqueue(encoder.encode(slowConsumerComment));
|
|
326
|
+
controller.close();
|
|
327
|
+
return;
|
|
234
328
|
}
|
|
329
|
+
controller.enqueue(encoder.encode(sseFrame(run.buffer[i])));
|
|
235
330
|
}
|
|
236
|
-
for (const event of run.buffer.slice(startIndex)) controller.enqueue(encoder.encode(sseFrame(event)));
|
|
237
331
|
if (run.done) {
|
|
238
332
|
controller.close();
|
|
239
333
|
return;
|
|
240
334
|
}
|
|
241
|
-
feed = (event) => {
|
|
335
|
+
feed = (event, note) => {
|
|
242
336
|
if (event === null) {
|
|
243
337
|
try {
|
|
338
|
+
if (note !== void 0) controller.enqueue(encoder.encode(`: ${note}\n\n`));
|
|
339
|
+
controller.close();
|
|
340
|
+
} catch {}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const pending = pendingOf(controller);
|
|
344
|
+
if (pending !== null && pending >= pendingCap) {
|
|
345
|
+
if (feed !== void 0) run.feeds.delete(feed);
|
|
346
|
+
try {
|
|
347
|
+
controller.enqueue(encoder.encode(slowConsumerComment));
|
|
244
348
|
controller.close();
|
|
245
349
|
} catch {}
|
|
246
350
|
return;
|
|
@@ -257,16 +361,13 @@ function createServer(options) {
|
|
|
257
361
|
});
|
|
258
362
|
return new Response(stream, {
|
|
259
363
|
status: 200,
|
|
260
|
-
headers
|
|
261
|
-
"content-type": "text/event-stream",
|
|
262
|
-
"cache-control": "no-cache"
|
|
263
|
-
}
|
|
364
|
+
headers
|
|
264
365
|
});
|
|
265
366
|
}
|
|
266
367
|
/** The tracked path: live (or settled-suspended) in this process. */
|
|
267
368
|
async function resolveTracked(run, key, value) {
|
|
268
369
|
const section = run.queue.then(async () => {
|
|
269
|
-
if (run.done) return json(409, { error: {
|
|
370
|
+
if (run.done || run.outcome !== void 0 && run.outcome.status !== "suspended") return json(409, { error: {
|
|
270
371
|
code: "config",
|
|
271
372
|
message: `run '${run.runId}' already settled '${run.outcome?.status ?? "unknown"}'`
|
|
272
373
|
} });
|
|
@@ -487,14 +588,28 @@ function createWorker(engine, options) {
|
|
|
487
588
|
const pollMs = options.pollMs ?? 1e3;
|
|
488
589
|
const registry = buildDeriverRegistry(options.extraDerivers);
|
|
489
590
|
const active = /* @__PURE__ */ new Map();
|
|
490
|
-
/** Runs this worker must not retry (DEF-6 violations, binding errors). */
|
|
491
|
-
const poisoned = /* @__PURE__ */ new Set();
|
|
492
591
|
/**
|
|
493
|
-
*
|
|
494
|
-
*
|
|
592
|
+
* Runs this worker must not retry (DEF-6 violations, binding errors),
|
|
593
|
+
* keyed to the run's generation (RunMeta.genesis) at poison time: a
|
|
594
|
+
* deleteRun and recreate of the same runId is a NEW run and must not
|
|
595
|
+
* inherit the poison. Entries for runIds that leave the candidate set
|
|
596
|
+
* are dropped each sweep, so an external delete cannot pin
|
|
597
|
+
* process-local state forever (v1.25.0 scale review).
|
|
598
|
+
*/
|
|
599
|
+
const poisoned = /* @__PURE__ */ new Map();
|
|
600
|
+
/**
|
|
601
|
+
* Journal length AND generation at our last release of a
|
|
602
|
+
* still-suspended run: nothing new to consume until the journal grows
|
|
603
|
+
* (an offline resolution appends) or the generation changes (the same
|
|
604
|
+
* runId was deleted and recreated; length alone cannot tell the new
|
|
605
|
+
* run from the old unchanged one, the v1.25.0 scale review). Runs
|
|
606
|
+
* whose meta predates the genesis field compare as equal when both
|
|
607
|
+
* sides are undefined, the historical behavior of length alone.
|
|
495
608
|
*/
|
|
496
609
|
const suspendedAt = /* @__PURE__ */ new Map();
|
|
497
610
|
let pollTimer;
|
|
611
|
+
/** An interval tick never overlaps a sweep that is still running. */
|
|
612
|
+
let sweeping = false;
|
|
498
613
|
let stopping = false;
|
|
499
614
|
function reportError(runId, error) {
|
|
500
615
|
try {
|
|
@@ -523,10 +638,13 @@ function createWorker(engine, options) {
|
|
|
523
638
|
const settled = handle.result.then(async (outcome) => {
|
|
524
639
|
if (outcome.status === "suspended") {
|
|
525
640
|
const entries = await store.load(runId);
|
|
526
|
-
suspendedAt.set(runId,
|
|
641
|
+
suspendedAt.set(runId, {
|
|
642
|
+
length: entries.length,
|
|
643
|
+
genesis: meta.genesis
|
|
644
|
+
});
|
|
527
645
|
} else suspendedAt.delete(runId);
|
|
528
646
|
}).catch((thrown) => {
|
|
529
|
-
if (thrown instanceof ConfigError || thrown instanceof JournalCompatibilityError) poisoned.
|
|
647
|
+
if (thrown instanceof ConfigError || thrown instanceof JournalCompatibilityError) poisoned.set(runId, meta.genesis);
|
|
530
648
|
reportError(runId, thrown);
|
|
531
649
|
}).finally(async () => {
|
|
532
650
|
clearInterval(renewTimer);
|
|
@@ -562,45 +680,58 @@ function createWorker(engine, options) {
|
|
|
562
680
|
}
|
|
563
681
|
}
|
|
564
682
|
async function sweep() {
|
|
565
|
-
if (stopping) return 0;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
lease = await store.acquire(meta.runId, owner);
|
|
580
|
-
} catch (thrown) {
|
|
581
|
-
if (thrown instanceof LeaseHeldError) continue;
|
|
582
|
-
throw thrown;
|
|
583
|
-
}
|
|
584
|
-
try {
|
|
585
|
-
const entries = (await store.load(meta.runId)).map((raw) => normalizeEntry(raw));
|
|
586
|
-
scanJournalCompatibility(meta.runId, entries, registry);
|
|
587
|
-
if (meta.status === "suspended" && suspendedAt.get(meta.runId) === entries.length) {
|
|
588
|
-
await releaseQuietly(lease);
|
|
683
|
+
if (stopping || sweeping) return 0;
|
|
684
|
+
sweeping = true;
|
|
685
|
+
try {
|
|
686
|
+
let picked = 0;
|
|
687
|
+
const metas = options.retention === void 0 ? await store.listRuns({ statuses: [...CANDIDATE_STATUSES] }) : await store.listRuns();
|
|
688
|
+
const candidateIds = new Set(metas.filter((meta) => CANDIDATE_STATUSES.has(meta.status)).map((meta) => meta.runId));
|
|
689
|
+
for (const runId of [...suspendedAt.keys()]) if (!candidateIds.has(runId)) suspendedAt.delete(runId);
|
|
690
|
+
for (const runId of [...poisoned.keys()]) if (!candidateIds.has(runId)) poisoned.delete(runId);
|
|
691
|
+
for (const meta of metas) {
|
|
692
|
+
if (active.size >= concurrency) break;
|
|
693
|
+
if (!CANDIDATE_STATUSES.has(meta.status)) {
|
|
694
|
+
if (options.retention !== void 0 && !active.has(meta.runId)) await applyRetention(meta).catch((thrown) => {
|
|
695
|
+
reportError(meta.runId, thrown);
|
|
696
|
+
});
|
|
589
697
|
continue;
|
|
590
698
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
699
|
+
if (active.has(meta.runId)) continue;
|
|
700
|
+
if (poisoned.has(meta.runId)) {
|
|
701
|
+
if (poisoned.get(meta.runId) === meta.genesis) continue;
|
|
702
|
+
poisoned.delete(meta.runId);
|
|
703
|
+
}
|
|
704
|
+
let lease;
|
|
705
|
+
try {
|
|
706
|
+
lease = await store.acquire(meta.runId, owner);
|
|
707
|
+
} catch (thrown) {
|
|
708
|
+
if (thrown instanceof LeaseHeldError) continue;
|
|
709
|
+
throw thrown;
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
const entries = (await store.load(meta.runId)).map((raw) => normalizeEntry(raw));
|
|
713
|
+
scanJournalCompatibility(meta.runId, entries, registry);
|
|
714
|
+
const cached = suspendedAt.get(meta.runId);
|
|
715
|
+
if (meta.status === "suspended" && cached !== void 0 && cached.length === entries.length && cached.genesis === meta.genesis) {
|
|
716
|
+
await releaseQuietly(lease);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
picked += 1;
|
|
720
|
+
drive(meta.runId, meta, lease);
|
|
721
|
+
} catch (thrown) {
|
|
722
|
+
await releaseQuietly(lease);
|
|
723
|
+
if (thrown instanceof JournalCompatibilityError || thrown instanceof ConfigError) {
|
|
724
|
+
poisoned.set(meta.runId, meta.genesis);
|
|
725
|
+
reportError(meta.runId, thrown);
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
597
728
|
reportError(meta.runId, thrown);
|
|
598
|
-
continue;
|
|
599
729
|
}
|
|
600
|
-
reportError(meta.runId, thrown);
|
|
601
730
|
}
|
|
731
|
+
return picked;
|
|
732
|
+
} finally {
|
|
733
|
+
sweeping = false;
|
|
602
734
|
}
|
|
603
|
-
return picked;
|
|
604
735
|
}
|
|
605
736
|
return {
|
|
606
737
|
start: () => {
|
|
@@ -759,4 +890,4 @@ async function toOtel(run, tracer, options = {}) {
|
|
|
759
890
|
return created;
|
|
760
891
|
}
|
|
761
892
|
//#endregion
|
|
762
|
-
export { DEFAULT_STORE_DIR, DEFAULT_WORKER_TTL_MS, HELP, assembleEngine, attachProgress, createServer, createWorker, driveRun, inspectCommand, loadCliConfig, loadWorkflowModule, looksLikeFile, processIo, renderEventLine, reportOutcome, resumeCommand, runCli, runCommand, runsLsCommand, toOtel };
|
|
893
|
+
export { DEFAULT_MAX_PENDING_EVENTS_PER_CLIENT, DEFAULT_STORE_DIR, DEFAULT_WORKER_TTL_MS, HELP, assembleEngine, attachProgress, createServer, createWorker, driveRun, inspectCommand, loadCliConfig, loadWorkflowModule, looksLikeFile, processIo, renderEventLine, reportOutcome, resumeCommand, runCli, runCommand, runsLsCommand, toOtel };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigError, FileModelKnowledgeStore, INBOX_PROPOSAL_TTL_DAYS, JsonlFileStore, claimExpired, claimExpiry, compilePermissionPreset, costReportFromJournal, createEngine, hashRunArgs, parseModelRef, priceUsdOf, proposalStatement, remeasureQueue, resolvePricing, runProfile, sanitizeTerminalText } from "@rulvar/core";
|
|
1
|
+
import { ConfigError, FileModelKnowledgeStore, INBOX_PROPOSAL_TTL_DAYS, JsonlFileStore, claimExpired, claimExpiry, compilePermissionPreset, costReportFromJournal, createEngine, hashRunArgs, parseModelRef, priceUsdOf, proposalStatement, readRunMeta, remeasureQueue, resolvePricing, runProfile, sanitizeTerminalText } from "@rulvar/core";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { existsSync, statSync } from "node:fs";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -777,7 +777,7 @@ async function resumeCommand(argv, context) {
|
|
|
777
777
|
...store === void 0 ? {} : { storePath: store },
|
|
778
778
|
cwd: context.cwd
|
|
779
779
|
});
|
|
780
|
-
const meta =
|
|
780
|
+
const meta = await readRunMeta(assembled.store, runId);
|
|
781
781
|
if (meta === void 0) throw new ConfigError(`run '${runId}' not found in the store`);
|
|
782
782
|
enforceArgsBinding({
|
|
783
783
|
meta,
|
|
@@ -829,7 +829,7 @@ async function inspectCommand(argv, context) {
|
|
|
829
829
|
...store === void 0 ? {} : { storePath: store },
|
|
830
830
|
cwd: context.cwd
|
|
831
831
|
});
|
|
832
|
-
const meta =
|
|
832
|
+
const meta = await readRunMeta(assembled.store, runId);
|
|
833
833
|
if (meta === void 0) throw new ConfigError(`run '${runId}' not found in the store`);
|
|
834
834
|
const entries = await assembled.store.load(runId);
|
|
835
835
|
context.io.out(`run ${meta.runId}: ${meta.status} (updated ${meta.updatedAt})`);
|
|
@@ -1087,7 +1087,7 @@ async function kbGateCommand(argv, context) {
|
|
|
1087
1087
|
...values.store === void 0 ? {} : { storePath: values.store },
|
|
1088
1088
|
cwd: context.cwd
|
|
1089
1089
|
});
|
|
1090
|
-
const meta =
|
|
1090
|
+
const meta = await readRunMeta(assembled.store, runId);
|
|
1091
1091
|
if (meta === void 0) throw new ConfigError(`run '${runId}' not found in the store`);
|
|
1092
1092
|
if (meta.status === "running") throw new ConfigError(`run '${runId}' is still running; proposals gate from finished runs`);
|
|
1093
1093
|
if (Date.parse(meta.updatedAt) < Date.now() - INBOX_PROPOSAL_TTL_DAYS * 24 * 60 * 60 * 1e3) throw new ConfigError(`the proposal expired: run '${runId}' finished ${meta.updatedAt}, and inbox entries expire after ${String(INBOX_PROPOSAL_TTL_DAYS)} days`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "Rulvar shell: run/resume/runs/inspect/plan/kb commands, TUI progress, createServer, createWorker, OTel exporter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,17 +22,17 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rulvar/core": "1.
|
|
25
|
+
"@rulvar/core": "1.27.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.20.0",
|
|
29
29
|
"tsdown": "^0.22.3",
|
|
30
30
|
"typescript": "~6.0.3",
|
|
31
|
-
"@rulvar/
|
|
32
|
-
"@rulvar/
|
|
33
|
-
"@rulvar/
|
|
34
|
-
"@rulvar/
|
|
35
|
-
"@rulvar/evals": "1.
|
|
31
|
+
"@rulvar/store-sqlite": "1.27.0",
|
|
32
|
+
"@rulvar/planner": "1.27.0",
|
|
33
|
+
"@rulvar/testing": "1.27.0",
|
|
34
|
+
"@rulvar/plan": "1.27.0",
|
|
35
|
+
"@rulvar/evals": "1.27.0"
|
|
36
36
|
},
|
|
37
37
|
"bin": {
|
|
38
38
|
"rulvar": "./dist/cli.js"
|