@llblab/pi-actors 0.43.0 → 0.44.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/AGENTS.md +16 -10
- package/CHANGELOG.md +425 -536
- package/README.md +13 -11
- package/dist/index.js +1 -1
- package/dist/lib/async-runs.d.ts +2 -1
- package/dist/lib/async-runs.js +24 -34
- package/dist/lib/automatic-review-runtime.d.ts +1 -1
- package/dist/lib/automatic-review-runtime.js +5 -5
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/command-templates.js +38 -4
- package/dist/lib/control-projection.d.ts +20 -0
- package/dist/lib/control-projection.js +66 -0
- package/dist/lib/control.d.ts +3 -0
- package/dist/lib/control.js +27 -14
- package/dist/lib/draft-sleep.js +3 -3
- package/dist/lib/file-state.d.ts +4 -1
- package/dist/lib/file-state.js +118 -44
- package/dist/lib/inspector-overlay.d.ts +2 -0
- package/dist/lib/inspector-overlay.js +124 -69
- package/dist/lib/limits.d.ts +15 -3
- package/dist/lib/limits.js +15 -3
- package/dist/lib/observability.d.ts +4 -2
- package/dist/lib/observability.js +43 -36
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-control.js +6 -2
- package/dist/lib/review-control.d.ts +1 -1
- package/dist/lib/review-control.js +4 -5
- package/dist/lib/run-evidence-policy.d.ts +95 -0
- package/dist/lib/run-evidence-policy.js +177 -0
- package/dist/lib/run-ui-runtime.js +2 -0
- package/dist/lib/runs-control-delivery.d.ts +8 -1
- package/dist/lib/runs-control-delivery.js +38 -15
- package/dist/lib/runs-controls.d.ts +8 -4
- package/dist/lib/runs-controls.js +189 -50
- package/dist/lib/runs-retention.js +27 -14
- package/dist/lib/runs-trace.d.ts +26 -2
- package/dist/lib/runs-trace.js +411 -18
- package/dist/lib/runtime-identity.d.ts +7 -0
- package/dist/lib/runtime-identity.js +35 -0
- package/dist/lib/runtime-triage.d.ts +29 -0
- package/dist/lib/runtime-triage.js +60 -0
- package/dist/lib/tool-review-scheduler.js +7 -7
- package/dist/lib/tools-inspect.js +91 -18
- package/dist/lib/tools-message.d.ts +1 -2
- package/dist/lib/tools-message.js +6 -6
- package/dist/lib/tools-response.d.ts +0 -1
- package/dist/lib/tools-response.js +0 -9
- package/dist/lib/tools.d.ts +1 -1
- package/dist/lib/tools.js +1 -1
- package/dist/lib/trace-projection.js +107 -41
- package/dist/scripts/conformance.mjs +5 -0
- package/dist/scripts/locker.mjs +40 -90
- package/dist/scripts/music-player.mjs +48 -142
- package/dist/scripts/release-gates.mjs +56 -3
- package/dist/scripts/validate-recipe.mjs +5 -4
- package/dist/skills/actors/SKILL.md +17 -11
- package/dist/skills/swarm/SKILL.md +2 -4
- package/docs/README.md +1 -4
- package/docs/actor-inspector.md +6 -5
- package/docs/async-runs.md +11 -9
- package/docs/command-templates.md +6 -116
- package/docs/recipe-library.md +4 -6
- package/docs/releasing.md +28 -0
- package/docs/template-recipes.md +1 -1
- package/docs/tool-registry.md +2 -2
- package/index.ts +1 -1
- package/lib/async-runs.ts +26 -50
- package/lib/automatic-review-runtime.ts +7 -7
- package/lib/command-templates.ts +44 -4
- package/lib/control-projection.ts +105 -0
- package/lib/control.ts +33 -18
- package/lib/draft-sleep.ts +3 -3
- package/lib/file-state.ts +91 -63
- package/lib/inspector-overlay.ts +108 -61
- package/lib/limits.ts +15 -3
- package/lib/observability.ts +55 -57
- package/lib/prompts.ts +1 -1
- package/lib/recipe-control.ts +9 -2
- package/lib/review-control.ts +4 -5
- package/lib/run-evidence-policy.ts +242 -0
- package/lib/run-ui-runtime.ts +2 -0
- package/lib/runs-control-delivery.ts +45 -17
- package/lib/runs-controls.ts +180 -102
- package/lib/runs-retention.ts +28 -20
- package/lib/runs-trace.ts +499 -20
- package/lib/runtime-identity.ts +39 -0
- package/lib/runtime-triage.ts +106 -0
- package/lib/tool-review-scheduler.ts +7 -7
- package/lib/tools-inspect.ts +94 -20
- package/lib/tools-message.ts +7 -8
- package/lib/tools-response.ts +0 -12
- package/lib/tools.ts +4 -4
- package/lib/trace-projection.ts +156 -71
- package/package.json +1 -1
- package/scripts/conformance.mjs +5 -0
- package/scripts/locker.mjs +40 -90
- package/scripts/music-player.mjs +48 -142
- package/scripts/release-gates.mjs +56 -3
- package/scripts/validate-recipe.mjs +5 -4
- package/skills/actors/SKILL.md +17 -11
- package/skills/swarm/SKILL.md +2 -4
- package/dist/lib/runtime-notifier.d.ts +0 -48
- package/dist/lib/runtime-notifier.js +0 -138
- package/docs/0.43-baseline.md +0 -44
- package/docs/actors-deep-reference.md +0 -108
- package/docs/component-recipes.md +0 -45
- package/docs/task-first-recipes.md +0 -261
- package/lib/runtime-notifier.ts +0 -211
package/lib/runs-controls.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Run Control journal.
|
|
3
|
-
* Zones: durable
|
|
4
|
-
* Owns Run-local Control persistence;
|
|
3
|
+
* Zones: durable records, atomic admission, claims, transitions, compaction
|
|
4
|
+
* Owns Run-local Control persistence; lifecycle authorization and transport stay in adapters.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import {
|
|
8
|
+
import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
|
+
import * as Control from "./control.ts";
|
|
11
12
|
import { acquireFileMutationLock, writeTextAtomic } from "./file-state.ts";
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| "handled"
|
|
20
|
-
| "failed";
|
|
13
|
+
import * as Limits from "./limits.ts";
|
|
14
|
+
import {
|
|
15
|
+
classifyRunControlRecord, computeRunControlCapacity, decideRunControlAdmission,
|
|
16
|
+
runControlStatusTimestamp, type RunControlAdmissionErrorDetails,
|
|
17
|
+
type RunControlAdmissionIntegrity, type RunControlStatus,
|
|
18
|
+
} from "./run-evidence-policy.ts";
|
|
19
|
+
import type { JsonlReadResult, StateReadDiagnostic } from "./state-readers.ts";
|
|
21
20
|
|
|
21
|
+
export { RUN_CONTROL_TERMINAL_LIMIT } from "./limits.ts";
|
|
22
|
+
export type { RunControlStatus } from "./run-evidence-policy.ts";
|
|
22
23
|
export interface RunControlRecord {
|
|
23
24
|
id: string;
|
|
24
25
|
run_instance_id: string;
|
|
@@ -32,110 +33,196 @@ export interface RunControlRecord {
|
|
|
32
33
|
failed_at?: string;
|
|
33
34
|
error?: string;
|
|
34
35
|
}
|
|
35
|
-
|
|
36
36
|
export interface ProcessRunControlsResult {
|
|
37
|
-
claimed: number;
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
claimed: number; failed: number; handled: number;
|
|
38
|
+
}
|
|
39
|
+
function admissionError(details: RunControlAdmissionErrorDetails): Error {
|
|
40
|
+
return Object.assign(new Error(details.reason === "control_backpressure"
|
|
41
|
+
? `Run Control admission backpressured: pending=${details.pending}/${details.limit}`
|
|
42
|
+
: `Run Control journal integrity failed: ${details.integrity_reason ?? "unknown"}`), details);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
const
|
|
43
|
-
|
|
45
|
+
const STATUSES = new Set<RunControlStatus>([
|
|
46
|
+
"queued", "delivered", "claimed", "handled", "failed",
|
|
47
|
+
]);
|
|
44
48
|
export function runControlsFile(stateDir: string): string {
|
|
45
49
|
return join(stateDir, "controls.jsonl");
|
|
46
50
|
}
|
|
47
|
-
|
|
51
|
+
export function readRunControlJournalFromStateDir(stateDir: string): JsonlReadResult<unknown> {
|
|
52
|
+
const path = runControlsFile(stateDir);
|
|
53
|
+
let fd: number;
|
|
54
|
+
try { fd = openSync(path, "r"); }
|
|
55
|
+
catch (error) {
|
|
56
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { diagnostics: [], records: [] };
|
|
57
|
+
return { diagnostics: [{ message: String(error), path }], records: [] };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const bytes = fstatSync(fd).size;
|
|
61
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
|
|
62
|
+
return { diagnostics: [{ message: "Run Control journal exceeds bounded read limit", path }], records: [] };
|
|
63
|
+
}
|
|
64
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
65
|
+
let consumed = 0;
|
|
66
|
+
while (consumed < bytes) {
|
|
67
|
+
const count = readSync(fd, buffer, consumed, bytes - consumed, consumed);
|
|
68
|
+
if (count === 0) return { diagnostics: [{ message: "Run Control journal changed during bounded read", path }], records: [] };
|
|
69
|
+
consumed += count;
|
|
70
|
+
}
|
|
71
|
+
if (fstatSync(fd).size !== bytes) return { diagnostics: [{ message: "Run Control journal changed during bounded read", path }], records: [] };
|
|
72
|
+
const records: unknown[] = [];
|
|
73
|
+
const diagnostics: StateReadDiagnostic[] = [];
|
|
74
|
+
for (const [index, line] of buffer.toString("utf8").split("\n").entries()) {
|
|
75
|
+
if (!line.trim()) continue;
|
|
76
|
+
try { records.push(JSON.parse(line)); }
|
|
77
|
+
catch (error) { diagnostics.push({ line: index + 1, message: String(error), path }); }
|
|
78
|
+
}
|
|
79
|
+
return { diagnostics, records };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return { diagnostics: [{ message: String(error), path }], records: [] };
|
|
82
|
+
} finally { closeSync(fd); }
|
|
83
|
+
}
|
|
48
84
|
export function readRunControlsFromStateDir(stateDir: string): RunControlRecord[] {
|
|
49
|
-
return
|
|
50
|
-
.records;
|
|
85
|
+
return readRunControlJournalFromStateDir(stateDir).records as RunControlRecord[];
|
|
51
86
|
}
|
|
52
|
-
|
|
53
87
|
function acquireRunControlsLock(stateDir: string): () => void {
|
|
54
88
|
return acquireFileMutationLock(runControlsFile(stateDir));
|
|
55
89
|
}
|
|
56
90
|
|
|
57
91
|
function compactRunControls(records: RunControlRecord[]): RunControlRecord[] {
|
|
58
|
-
const active = records.filter((record) =>
|
|
59
|
-
const terminal = records
|
|
60
|
-
.
|
|
61
|
-
.slice(-RUN_CONTROL_TERMINAL_LIMIT);
|
|
92
|
+
const active = records.filter((record) => classifyRunControlRecord(record) !== "terminal");
|
|
93
|
+
const terminal = records.filter((record) => classifyRunControlRecord(record) === "terminal")
|
|
94
|
+
.slice(-Limits.RUN_CONTROL_TERMINAL_LIMIT);
|
|
62
95
|
const retained = new Set([...active, ...terminal]);
|
|
63
|
-
return records.filter((record) => retained.has(record))
|
|
96
|
+
return records.filter((record) => retained.has(record)).map((record) =>
|
|
97
|
+
typeof record.error === "string" && Buffer.byteLength(record.error) > Limits.RUN_CONTROL_ERROR_MAX_BYTES
|
|
98
|
+
? { ...record, error: boundedControlError(record.error) } : record);
|
|
99
|
+
}
|
|
100
|
+
function encodeRunControls(records: RunControlRecord[]): string {
|
|
101
|
+
return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
|
|
102
|
+
}
|
|
103
|
+
function boundedControlError(value: string): string {
|
|
104
|
+
if (Buffer.byteLength(value) <= Limits.RUN_CONTROL_ERROR_MAX_BYTES) return value;
|
|
105
|
+
const suffix = "… [truncated]";
|
|
106
|
+
const limit = Limits.RUN_CONTROL_ERROR_MAX_BYTES - Buffer.byteLength(suffix);
|
|
107
|
+
const bytes = Buffer.from(value);
|
|
108
|
+
let end = limit;
|
|
109
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
|
|
110
|
+
return `${bytes.subarray(0, end).toString("utf8")}${suffix}`;
|
|
64
111
|
}
|
|
65
|
-
|
|
66
112
|
function writeRunControls(stateDir: string, records: RunControlRecord[]): void {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
113
|
+
const encoded = encodeRunControls(compactRunControls(records));
|
|
114
|
+
if (Buffer.byteLength(encoded) > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
|
|
115
|
+
throw new Error("Run Control journal transition exceeds bounded byte limit");
|
|
116
|
+
}
|
|
117
|
+
writeTextAtomic(runControlsFile(stateDir), encoded);
|
|
118
|
+
}
|
|
119
|
+
function readMutableRunControls(
|
|
120
|
+
stateDir: string, runInstanceId?: string,
|
|
121
|
+
): RunControlRecord[] | undefined {
|
|
122
|
+
const journal = readRunControlJournalFromStateDir(stateDir);
|
|
123
|
+
if (journal.diagnostics.length) return undefined;
|
|
124
|
+
const records = journal.records as RunControlRecord[];
|
|
125
|
+
if (runInstanceId && records.some((record) => controlIntegrity(record, runInstanceId) !== "valid"))
|
|
126
|
+
return undefined;
|
|
127
|
+
return records;
|
|
128
|
+
}
|
|
129
|
+
function controlIntegrity(value: unknown, runInstanceId: string): RunControlAdmissionIntegrity {
|
|
130
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "invalid_record";
|
|
131
|
+
const record = value as Record<string, unknown>;
|
|
132
|
+
if (typeof record.id !== "string" || !record.id ||
|
|
133
|
+
typeof record.run_instance_id !== "string" || !record.run_instance_id ||
|
|
134
|
+
typeof record.status !== "string" || !STATUSES.has(record.status as RunControlStatus) ||
|
|
135
|
+
(record.error !== undefined && typeof record.error !== "string")) return "invalid_record";
|
|
136
|
+
try {
|
|
137
|
+
Control.normalizeControlAction(record.action);
|
|
138
|
+
if (record.input !== undefined) Control.normalizeControlInput(record.input);
|
|
139
|
+
} catch { return "invalid_record"; }
|
|
140
|
+
const timestamp = runControlStatusTimestamp(record, record.status as RunControlStatus);
|
|
141
|
+
if (typeof timestamp !== "string" || !Number.isFinite(Date.parse(timestamp)))
|
|
142
|
+
return "invalid_status_timestamp";
|
|
143
|
+
if (classifyRunControlRecord(record) === "pending" &&
|
|
144
|
+
record.run_instance_id !== runInstanceId) return "generation_mismatch";
|
|
145
|
+
return "valid";
|
|
74
146
|
}
|
|
75
|
-
|
|
76
147
|
export function appendRunControlInStateDir(
|
|
77
|
-
stateDir: string,
|
|
78
|
-
request: { run_instance_id: string; action: string; input?: unknown },
|
|
148
|
+
stateDir: string, request: { run_instance_id: string; action: string; input?: unknown },
|
|
79
149
|
): RunControlRecord {
|
|
150
|
+
const action = Control.normalizeControlAction(request.action);
|
|
151
|
+
const input = request.input === undefined ? undefined : Control.normalizeControlInput(request.input);
|
|
80
152
|
const record: RunControlRecord = {
|
|
81
153
|
id: randomUUID(),
|
|
82
154
|
run_instance_id: request.run_instance_id,
|
|
83
|
-
action
|
|
84
|
-
...(
|
|
155
|
+
action,
|
|
156
|
+
...(input !== undefined ? { input } : {}),
|
|
85
157
|
status: "queued",
|
|
86
158
|
queued_at: new Date().toISOString(),
|
|
87
159
|
};
|
|
160
|
+
const encodedRecordBytes = Buffer.byteLength(`${JSON.stringify(record)}\n`);
|
|
88
161
|
const releaseLock = acquireRunControlsLock(stateDir);
|
|
89
162
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
163
|
+
const path = runControlsFile(stateDir);
|
|
164
|
+
let bytes = 0;
|
|
165
|
+
try { bytes = statSync(path).size; }
|
|
166
|
+
catch (error) {
|
|
167
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") bytes = Number.MAX_SAFE_INTEGER;
|
|
168
|
+
}
|
|
169
|
+
const journal = bytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES
|
|
170
|
+
? { diagnostics: [], records: [] } : readRunControlJournalFromStateDir(stateDir);
|
|
171
|
+
let integrity: RunControlAdmissionIntegrity = bytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES
|
|
172
|
+
? "journal_bytes"
|
|
173
|
+
: journal.diagnostics.some((item) => item.line !== undefined)
|
|
174
|
+
? "malformed"
|
|
175
|
+
: journal.diagnostics.length ? "unreadable" : "valid";
|
|
176
|
+
for (const value of journal.records) {
|
|
177
|
+
if (integrity !== "valid") break;
|
|
178
|
+
integrity = controlIntegrity(value, request.run_instance_id);
|
|
179
|
+
}
|
|
180
|
+
const records = journal.records as RunControlRecord[];
|
|
181
|
+
const retained = encodeRunControls(compactRunControls(records));
|
|
182
|
+
const decision = decideRunControlAdmission({
|
|
183
|
+
integrity,
|
|
184
|
+
journalBytes: bytes,
|
|
185
|
+
newRecordBytes: encodedRecordBytes,
|
|
186
|
+
records,
|
|
187
|
+
retainedJournalBytes: Buffer.byteLength(retained),
|
|
188
|
+
runInstanceId: request.run_instance_id,
|
|
189
|
+
capacity: computeRunControlCapacity(records),
|
|
92
190
|
});
|
|
191
|
+
if (!decision.admitted) throw admissionError(decision.error);
|
|
192
|
+
writeTextAtomic(path, `${retained}${JSON.stringify(record)}\n`);
|
|
193
|
+
return record;
|
|
93
194
|
} finally {
|
|
94
195
|
releaseLock();
|
|
95
196
|
}
|
|
96
|
-
return record;
|
|
97
197
|
}
|
|
98
|
-
|
|
99
198
|
export function updateRunControlStatusInStateDir(
|
|
100
|
-
stateDir: string,
|
|
101
|
-
id: string,
|
|
102
|
-
nextStatus: RunControlStatus,
|
|
199
|
+
stateDir: string, id: string, nextStatus: RunControlStatus,
|
|
103
200
|
metadata: Pick<RunControlRecord, "error"> = {},
|
|
104
201
|
expectedStatuses?: readonly RunControlStatus[],
|
|
105
202
|
): boolean {
|
|
106
203
|
const releaseLock = acquireRunControlsLock(stateDir);
|
|
107
204
|
try {
|
|
108
|
-
const records =
|
|
205
|
+
const records = readMutableRunControls(stateDir);
|
|
206
|
+
if (!records) return false;
|
|
109
207
|
const index = records.findIndex((record) => record.id === id);
|
|
110
208
|
if (index < 0) return false;
|
|
111
209
|
const record = records[index]!;
|
|
210
|
+
if (records.some((value) => controlIntegrity(value, record.run_instance_id) !== "valid")) return false;
|
|
112
211
|
if (expectedStatuses && !expectedStatuses.includes(record.status)) return false;
|
|
113
212
|
if (record.status === nextStatus) return false;
|
|
114
213
|
const timestamp = new Date().toISOString();
|
|
115
214
|
if (nextStatus === "delivered") {
|
|
116
215
|
if (record.delivered_at) return false;
|
|
117
|
-
records[index] = {
|
|
118
|
-
...record
|
|
119
|
-
delivered_at: timestamp,
|
|
120
|
-
...(record.status === "queued" ? { status: "delivered" as const } : {}),
|
|
121
|
-
};
|
|
216
|
+
records[index] = { ...record, delivered_at: timestamp,
|
|
217
|
+
...(record.status === "queued" ? { status: "delivered" as const } : {}) };
|
|
122
218
|
} else {
|
|
123
|
-
if (
|
|
124
|
-
if (
|
|
125
|
-
(nextStatus === "claimed" &&
|
|
126
|
-
record.status !== "queued" &&
|
|
219
|
+
if (classifyRunControlRecord(record) === "terminal" || nextStatus === "queued") return false;
|
|
220
|
+
if ((nextStatus === "claimed" && record.status !== "queued" &&
|
|
127
221
|
record.status !== "delivered") ||
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const timestampKey = `${nextStatus}_at` as const;
|
|
133
|
-
records[index] = {
|
|
134
|
-
...record,
|
|
135
|
-
...metadata,
|
|
136
|
-
[timestampKey]: timestamp,
|
|
137
|
-
status: nextStatus,
|
|
138
|
-
};
|
|
222
|
+
(nextStatus === "handled" && record.status !== "claimed")) return false;
|
|
223
|
+
records[index] = { ...record,
|
|
224
|
+
...(metadata.error !== undefined ? { error: boundedControlError(metadata.error) } : {}),
|
|
225
|
+
[`${nextStatus}_at`]: timestamp, status: nextStatus };
|
|
139
226
|
}
|
|
140
227
|
writeRunControls(stateDir, records);
|
|
141
228
|
return true;
|
|
@@ -143,38 +230,38 @@ export function updateRunControlStatusInStateDir(
|
|
|
143
230
|
releaseLock();
|
|
144
231
|
}
|
|
145
232
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
stateDir: string,
|
|
149
|
-
runInstanceId: string,
|
|
233
|
+
function claimRunControl(
|
|
234
|
+
stateDir: string, runInstanceId: string, controlId?: string,
|
|
150
235
|
): RunControlRecord | undefined {
|
|
151
236
|
const releaseLock = acquireRunControlsLock(stateDir);
|
|
152
237
|
try {
|
|
153
|
-
const records =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
238
|
+
const records = readMutableRunControls(stateDir, runInstanceId);
|
|
239
|
+
if (!records) return undefined;
|
|
240
|
+
const index = records.findIndex((record) =>
|
|
241
|
+
(controlId === undefined || record.id === controlId) &&
|
|
242
|
+
record.run_instance_id === runInstanceId &&
|
|
243
|
+
(record.status === "queued" || record.status === "delivered"));
|
|
159
244
|
if (index < 0) return undefined;
|
|
160
245
|
const claimed: RunControlRecord = {
|
|
161
|
-
...records[index]!,
|
|
162
|
-
claimed_at: new Date().toISOString(),
|
|
163
|
-
status: "claimed",
|
|
164
|
-
};
|
|
246
|
+
...records[index]!, claimed_at: new Date().toISOString(), status: "claimed" };
|
|
165
247
|
records[index] = claimed;
|
|
166
248
|
writeRunControls(stateDir, records);
|
|
167
249
|
return claimed;
|
|
168
|
-
} finally {
|
|
169
|
-
|
|
170
|
-
|
|
250
|
+
} finally { releaseLock(); }
|
|
251
|
+
}
|
|
252
|
+
export function claimRunControlInStateDir(
|
|
253
|
+
stateDir: string, runInstanceId: string,
|
|
254
|
+
): RunControlRecord | undefined {
|
|
255
|
+
return claimRunControl(stateDir, runInstanceId);
|
|
256
|
+
}
|
|
257
|
+
export function claimRunControlByIdInStateDir(
|
|
258
|
+
stateDir: string, runInstanceId: string, controlId: string,
|
|
259
|
+
): RunControlRecord | undefined {
|
|
260
|
+
return claimRunControl(stateDir, runInstanceId, controlId);
|
|
171
261
|
}
|
|
172
|
-
|
|
173
262
|
export async function processRunControlsInStateDir(
|
|
174
|
-
stateDir: string,
|
|
175
|
-
|
|
176
|
-
handler: (control: RunControlRecord) => Promise<void> | void,
|
|
177
|
-
limit = 1,
|
|
263
|
+
stateDir: string, runInstanceId: string,
|
|
264
|
+
handler: (control: RunControlRecord) => Promise<void> | void, limit = 1,
|
|
178
265
|
): Promise<ProcessRunControlsResult> {
|
|
179
266
|
const result: ProcessRunControlsResult = { claimed: 0, failed: 0, handled: 0 };
|
|
180
267
|
for (let index = 0; index < Math.max(1, limit); index += 1) {
|
|
@@ -183,21 +270,12 @@ export async function processRunControlsInStateDir(
|
|
|
183
270
|
result.claimed += 1;
|
|
184
271
|
try {
|
|
185
272
|
await handler(control);
|
|
186
|
-
if (updateRunControlStatusInStateDir(stateDir, control.id, "handled", {}, ["claimed"]))
|
|
273
|
+
if (updateRunControlStatusInStateDir(stateDir, control.id, "handled", {}, ["claimed"]))
|
|
187
274
|
result.handled += 1;
|
|
188
|
-
}
|
|
189
275
|
} catch (error) {
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
stateDir,
|
|
193
|
-
control.id,
|
|
194
|
-
"failed",
|
|
195
|
-
{ error: error instanceof Error ? error.message : String(error) },
|
|
196
|
-
["claimed"],
|
|
197
|
-
)
|
|
198
|
-
) {
|
|
276
|
+
if (updateRunControlStatusInStateDir(stateDir, control.id, "failed",
|
|
277
|
+
{ error: error instanceof Error ? error.message : String(error) }, ["claimed"]))
|
|
199
278
|
result.failed += 1;
|
|
200
|
-
}
|
|
201
279
|
}
|
|
202
280
|
}
|
|
203
281
|
return result;
|
package/lib/runs-retention.ts
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { cpSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
9
9
|
|
|
10
|
-
import { acquireFileMutationLock, writeJsonAtomic } from "./file-state.ts";
|
|
10
|
+
import { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } from "./file-state.ts";
|
|
11
|
+
import * as Limits from "./limits.ts";
|
|
12
|
+
import { readJsonlFileResilient } from "./state-readers.ts";
|
|
11
13
|
import {
|
|
12
14
|
resolveArtifactManifest,
|
|
13
15
|
type RunArtifactDeclaration,
|
|
@@ -27,29 +29,35 @@ export function appendRunRetentionEvidence(
|
|
|
27
29
|
const stateDir = String(status.state_dir);
|
|
28
30
|
const path = join(dirname(stateDir), "retention.jsonl");
|
|
29
31
|
const id = options.id ?? randomUUID();
|
|
32
|
+
const record = {
|
|
33
|
+
action,
|
|
34
|
+
...(options.error ? { error: options.error } : {}),
|
|
35
|
+
id,
|
|
36
|
+
outcome,
|
|
37
|
+
...(options.result ? { result: options.result } : {}),
|
|
38
|
+
run: String(status.run ?? basename(stateDir)),
|
|
39
|
+
...(typeof status.run_instance_id === "string" ? { run_instance_id: status.run_instance_id } : {}),
|
|
40
|
+
ts: new Date().toISOString(),
|
|
41
|
+
};
|
|
30
42
|
const release = acquireFileMutationLock(path);
|
|
31
43
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
? { run_instance_id: status.run_instance_id }
|
|
43
|
-
: {}),
|
|
44
|
-
ts: new Date().toISOString(),
|
|
45
|
-
})}\n`,
|
|
46
|
-
);
|
|
47
|
-
} finally {
|
|
48
|
-
release();
|
|
49
|
-
}
|
|
44
|
+
const records = [...readJsonlFileResilient<Record<string, unknown>>(path).records, record]
|
|
45
|
+
.slice(-Limits.RUN_RETENTION_MAX_RECORDS);
|
|
46
|
+
let content = encodeRecords(records);
|
|
47
|
+
while (records.length && Buffer.byteLength(content) > Limits.RUN_RETENTION_MAX_BYTES) {
|
|
48
|
+
records.shift();
|
|
49
|
+
content = encodeRecords(records);
|
|
50
|
+
}
|
|
51
|
+
if (!records.includes(record)) throw new Error("Run retention record exceeds journal byte limit");
|
|
52
|
+
writeTextAtomic(path, content);
|
|
53
|
+
} finally { release(); }
|
|
50
54
|
return id;
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
function encodeRecords(records: Record<string, unknown>[]): string {
|
|
58
|
+
return records.length ? `${records.map((value) => JSON.stringify(value)).join("\n")}\n` : "";
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
function retainedArtifactFilename(name: string, path: string): string {
|
|
54
62
|
const readableName = name.replace(/[^A-Za-z0-9_.-]+/g, "_") || "artifact";
|
|
55
63
|
const identity = createHash("sha256")
|