@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
|
@@ -6,9 +6,23 @@
|
|
|
6
6
|
import { closeSync, constants, existsSync, openSync, statSync, writeSync, } from "node:fs";
|
|
7
7
|
import { createConnection } from "node:net";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import * as Control from "./control.js";
|
|
10
|
+
import * as Limits from "./limits.js";
|
|
9
11
|
import { appendRunControlInStateDir, updateRunControlStatusInStateDir, } from "./runs-controls.js";
|
|
10
12
|
import { readJsonFileResilient } from "./state-readers.js";
|
|
11
|
-
|
|
13
|
+
const PREFLIGHT_CONTROL_ID = "00000000-0000-4000-8000-000000000000";
|
|
14
|
+
export function encodeRunControlWire(wire) {
|
|
15
|
+
const payload = `${JSON.stringify({
|
|
16
|
+
id: wire.id,
|
|
17
|
+
action: wire.action,
|
|
18
|
+
...(wire.input !== undefined ? { input: wire.input } : {}),
|
|
19
|
+
})}\n`;
|
|
20
|
+
const bytes = Buffer.byteLength(payload);
|
|
21
|
+
if (bytes > Limits.CONTROL_WIRE_MAX_BYTES) {
|
|
22
|
+
throw new Error(`Run Control wire record exceeds the ${Limits.CONTROL_WIRE_MAX_BYTES}-byte portable bound`);
|
|
23
|
+
}
|
|
24
|
+
return { bytes, payload };
|
|
25
|
+
}
|
|
12
26
|
export function readRunControlEndpoint(stateDir, runInstanceId) {
|
|
13
27
|
const endpoint = readJsonFileResilient(join(stateDir, "control-endpoint.json"), {}).value;
|
|
14
28
|
if (endpoint.run_instance_id !== runInstanceId)
|
|
@@ -65,34 +79,43 @@ function sendToNamedPipe(endpoint, payload, send) {
|
|
|
65
79
|
});
|
|
66
80
|
}
|
|
67
81
|
export async function deliverRunControl(run, stateDir, request, options = {}) {
|
|
68
|
-
const
|
|
82
|
+
const action = Control.normalizeControlAction(request.action);
|
|
83
|
+
const input = request.input === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: Control.normalizeControlInput(request.input);
|
|
86
|
+
const normalizedRequest = {
|
|
87
|
+
action,
|
|
88
|
+
...(input !== undefined ? { input } : {}),
|
|
89
|
+
run_instance_id: request.run_instance_id,
|
|
90
|
+
};
|
|
91
|
+
encodeRunControlWire({
|
|
92
|
+
id: PREFLIGHT_CONTROL_ID,
|
|
93
|
+
action,
|
|
94
|
+
...(input !== undefined ? { input } : {}),
|
|
95
|
+
});
|
|
96
|
+
const control = appendRunControlInStateDir(stateDir, normalizedRequest);
|
|
69
97
|
const endpoint = readRunControlEndpoint(stateDir, request.run_instance_id);
|
|
70
98
|
if (!endpoint) {
|
|
71
99
|
updateRunControlStatusInStateDir(stateDir, control.id, "failed", { error: "control endpoint is not ready for this Run generation" }, ["queued", "delivered"]);
|
|
72
100
|
throw Object.assign(new Error("Run Control endpoint is not ready."), {
|
|
73
|
-
action
|
|
101
|
+
action,
|
|
74
102
|
control_id: control.id,
|
|
75
103
|
reason: "endpoint_not_ready",
|
|
76
104
|
run,
|
|
77
105
|
run_instance_id: request.run_instance_id,
|
|
78
106
|
});
|
|
79
107
|
}
|
|
80
|
-
const wire = {
|
|
81
|
-
id: control.id,
|
|
82
|
-
action: request.action,
|
|
83
|
-
...(request.input !== undefined ? { input: request.input } : {}),
|
|
84
|
-
};
|
|
85
|
-
const payload = `${JSON.stringify(wire)}\n`;
|
|
86
|
-
const bytes = Buffer.byteLength(payload);
|
|
87
108
|
try {
|
|
109
|
+
const { bytes, payload } = encodeRunControlWire({
|
|
110
|
+
id: control.id,
|
|
111
|
+
action,
|
|
112
|
+
...(input !== undefined ? { input } : {}),
|
|
113
|
+
});
|
|
88
114
|
let written;
|
|
89
115
|
if (endpoint.type === "fifo") {
|
|
90
116
|
if ((options.platform ?? process.platform) === "win32") {
|
|
91
117
|
throw new Error("FIFO Control delivery is unsupported on native Windows");
|
|
92
118
|
}
|
|
93
|
-
if (bytes > FIFO_ATOMIC_CONTROL_MAX_BYTES) {
|
|
94
|
-
throw new Error(`FIFO Control payload exceeds the ${FIFO_ATOMIC_CONTROL_MAX_BYTES}-byte portable atomic-write bound`);
|
|
95
|
-
}
|
|
96
119
|
written = sendToFifo(endpoint, payload);
|
|
97
120
|
}
|
|
98
121
|
else {
|
|
@@ -103,7 +126,7 @@ export async function deliverRunControl(run, stateDir, request, options = {}) {
|
|
|
103
126
|
}
|
|
104
127
|
updateRunControlStatusInStateDir(stateDir, control.id, "delivered");
|
|
105
128
|
return {
|
|
106
|
-
action
|
|
129
|
+
action,
|
|
107
130
|
bytes,
|
|
108
131
|
control_id: control.id,
|
|
109
132
|
delivery: "delivered",
|
|
@@ -116,7 +139,7 @@ export async function deliverRunControl(run, stateDir, request, options = {}) {
|
|
|
116
139
|
const reason = error instanceof Error ? error.message : String(error);
|
|
117
140
|
updateRunControlStatusInStateDir(stateDir, control.id, "failed", { error: reason }, ["queued", "delivered"]);
|
|
118
141
|
throw Object.assign(new Error(`Run Control delivery failed: ${reason}`), {
|
|
119
|
-
action
|
|
142
|
+
action,
|
|
120
143
|
control_id: control.id,
|
|
121
144
|
endpoint_type: endpoint.type,
|
|
122
145
|
reason: "delivery_failed",
|
|
@@ -1,10 +1,12 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
6
|
+
import { type RunControlStatus } from "./run-evidence-policy.ts";
|
|
7
|
+
import type { JsonlReadResult } from "./state-readers.ts";
|
|
8
|
+
export { RUN_CONTROL_TERMINAL_LIMIT } from "./limits.ts";
|
|
9
|
+
export type { RunControlStatus } from "./run-evidence-policy.ts";
|
|
8
10
|
export interface RunControlRecord {
|
|
9
11
|
id: string;
|
|
10
12
|
run_instance_id: string;
|
|
@@ -24,6 +26,7 @@ export interface ProcessRunControlsResult {
|
|
|
24
26
|
handled: number;
|
|
25
27
|
}
|
|
26
28
|
export declare function runControlsFile(stateDir: string): string;
|
|
29
|
+
export declare function readRunControlJournalFromStateDir(stateDir: string): JsonlReadResult<unknown>;
|
|
27
30
|
export declare function readRunControlsFromStateDir(stateDir: string): RunControlRecord[];
|
|
28
31
|
export declare function appendRunControlInStateDir(stateDir: string, request: {
|
|
29
32
|
run_instance_id: string;
|
|
@@ -32,4 +35,5 @@ export declare function appendRunControlInStateDir(stateDir: string, request: {
|
|
|
32
35
|
}): RunControlRecord;
|
|
33
36
|
export declare function updateRunControlStatusInStateDir(stateDir: string, id: string, nextStatus: RunControlStatus, metadata?: Pick<RunControlRecord, "error">, expectedStatuses?: readonly RunControlStatus[]): boolean;
|
|
34
37
|
export declare function claimRunControlInStateDir(stateDir: string, runInstanceId: string): RunControlRecord | undefined;
|
|
38
|
+
export declare function claimRunControlByIdInStateDir(stateDir: string, runInstanceId: string, controlId: string): RunControlRecord | undefined;
|
|
35
39
|
export declare function processRunControlsInStateDir(stateDir: string, runInstanceId: string, handler: (control: RunControlRecord) => Promise<void> | void, limit?: number): Promise<ProcessRunControlsResult>;
|
|
@@ -1,67 +1,210 @@
|
|
|
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
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import * as Control from "./control.js";
|
|
9
10
|
import { acquireFileMutationLock, writeTextAtomic } from "./file-state.js";
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import * as Limits from "./limits.js";
|
|
12
|
+
import { classifyRunControlRecord, computeRunControlCapacity, decideRunControlAdmission, runControlStatusTimestamp, } from "./run-evidence-policy.js";
|
|
13
|
+
export { RUN_CONTROL_TERMINAL_LIMIT } from "./limits.js";
|
|
14
|
+
function admissionError(details) {
|
|
15
|
+
return Object.assign(new Error(details.reason === "control_backpressure"
|
|
16
|
+
? `Run Control admission backpressured: pending=${details.pending}/${details.limit}`
|
|
17
|
+
: `Run Control journal integrity failed: ${details.integrity_reason ?? "unknown"}`), details);
|
|
18
|
+
}
|
|
19
|
+
const STATUSES = new Set([
|
|
20
|
+
"queued", "delivered", "claimed", "handled", "failed",
|
|
21
|
+
]);
|
|
13
22
|
export function runControlsFile(stateDir) {
|
|
14
23
|
return join(stateDir, "controls.jsonl");
|
|
15
24
|
}
|
|
25
|
+
export function readRunControlJournalFromStateDir(stateDir) {
|
|
26
|
+
const path = runControlsFile(stateDir);
|
|
27
|
+
let fd;
|
|
28
|
+
try {
|
|
29
|
+
fd = openSync(path, "r");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code === "ENOENT")
|
|
33
|
+
return { diagnostics: [], records: [] };
|
|
34
|
+
return { diagnostics: [{ message: String(error), path }], records: [] };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const bytes = fstatSync(fd).size;
|
|
38
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
|
|
39
|
+
return { diagnostics: [{ message: "Run Control journal exceeds bounded read limit", path }], records: [] };
|
|
40
|
+
}
|
|
41
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
42
|
+
let consumed = 0;
|
|
43
|
+
while (consumed < bytes) {
|
|
44
|
+
const count = readSync(fd, buffer, consumed, bytes - consumed, consumed);
|
|
45
|
+
if (count === 0)
|
|
46
|
+
return { diagnostics: [{ message: "Run Control journal changed during bounded read", path }], records: [] };
|
|
47
|
+
consumed += count;
|
|
48
|
+
}
|
|
49
|
+
if (fstatSync(fd).size !== bytes)
|
|
50
|
+
return { diagnostics: [{ message: "Run Control journal changed during bounded read", path }], records: [] };
|
|
51
|
+
const records = [];
|
|
52
|
+
const diagnostics = [];
|
|
53
|
+
for (const [index, line] of buffer.toString("utf8").split("\n").entries()) {
|
|
54
|
+
if (!line.trim())
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
records.push(JSON.parse(line));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
diagnostics.push({ line: index + 1, message: String(error), path });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { diagnostics, records };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { diagnostics: [{ message: String(error), path }], records: [] };
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
16
72
|
export function readRunControlsFromStateDir(stateDir) {
|
|
17
|
-
return
|
|
18
|
-
.records;
|
|
73
|
+
return readRunControlJournalFromStateDir(stateDir).records;
|
|
19
74
|
}
|
|
20
75
|
function acquireRunControlsLock(stateDir) {
|
|
21
76
|
return acquireFileMutationLock(runControlsFile(stateDir));
|
|
22
77
|
}
|
|
23
78
|
function compactRunControls(records) {
|
|
24
|
-
const active = records.filter((record) =>
|
|
25
|
-
const terminal = records
|
|
26
|
-
.
|
|
27
|
-
.slice(-RUN_CONTROL_TERMINAL_LIMIT);
|
|
79
|
+
const active = records.filter((record) => classifyRunControlRecord(record) !== "terminal");
|
|
80
|
+
const terminal = records.filter((record) => classifyRunControlRecord(record) === "terminal")
|
|
81
|
+
.slice(-Limits.RUN_CONTROL_TERMINAL_LIMIT);
|
|
28
82
|
const retained = new Set([...active, ...terminal]);
|
|
29
|
-
return records.filter((record) => retained.has(record))
|
|
83
|
+
return records.filter((record) => retained.has(record)).map((record) => typeof record.error === "string" && Buffer.byteLength(record.error) > Limits.RUN_CONTROL_ERROR_MAX_BYTES
|
|
84
|
+
? { ...record, error: boundedControlError(record.error) } : record);
|
|
85
|
+
}
|
|
86
|
+
function encodeRunControls(records) {
|
|
87
|
+
return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
|
|
88
|
+
}
|
|
89
|
+
function boundedControlError(value) {
|
|
90
|
+
if (Buffer.byteLength(value) <= Limits.RUN_CONTROL_ERROR_MAX_BYTES)
|
|
91
|
+
return value;
|
|
92
|
+
const suffix = "… [truncated]";
|
|
93
|
+
const limit = Limits.RUN_CONTROL_ERROR_MAX_BYTES - Buffer.byteLength(suffix);
|
|
94
|
+
const bytes = Buffer.from(value);
|
|
95
|
+
let end = limit;
|
|
96
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80)
|
|
97
|
+
end -= 1;
|
|
98
|
+
return `${bytes.subarray(0, end).toString("utf8")}${suffix}`;
|
|
30
99
|
}
|
|
31
100
|
function writeRunControls(stateDir, records) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
101
|
+
const encoded = encodeRunControls(compactRunControls(records));
|
|
102
|
+
if (Buffer.byteLength(encoded) > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
|
|
103
|
+
throw new Error("Run Control journal transition exceeds bounded byte limit");
|
|
104
|
+
}
|
|
105
|
+
writeTextAtomic(runControlsFile(stateDir), encoded);
|
|
106
|
+
}
|
|
107
|
+
function readMutableRunControls(stateDir, runInstanceId) {
|
|
108
|
+
const journal = readRunControlJournalFromStateDir(stateDir);
|
|
109
|
+
if (journal.diagnostics.length)
|
|
110
|
+
return undefined;
|
|
111
|
+
const records = journal.records;
|
|
112
|
+
if (runInstanceId && records.some((record) => controlIntegrity(record, runInstanceId) !== "valid"))
|
|
113
|
+
return undefined;
|
|
114
|
+
return records;
|
|
115
|
+
}
|
|
116
|
+
function controlIntegrity(value, runInstanceId) {
|
|
117
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
118
|
+
return "invalid_record";
|
|
119
|
+
const record = value;
|
|
120
|
+
if (typeof record.id !== "string" || !record.id ||
|
|
121
|
+
typeof record.run_instance_id !== "string" || !record.run_instance_id ||
|
|
122
|
+
typeof record.status !== "string" || !STATUSES.has(record.status) ||
|
|
123
|
+
(record.error !== undefined && typeof record.error !== "string"))
|
|
124
|
+
return "invalid_record";
|
|
125
|
+
try {
|
|
126
|
+
Control.normalizeControlAction(record.action);
|
|
127
|
+
if (record.input !== undefined)
|
|
128
|
+
Control.normalizeControlInput(record.input);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return "invalid_record";
|
|
132
|
+
}
|
|
133
|
+
const timestamp = runControlStatusTimestamp(record, record.status);
|
|
134
|
+
if (typeof timestamp !== "string" || !Number.isFinite(Date.parse(timestamp)))
|
|
135
|
+
return "invalid_status_timestamp";
|
|
136
|
+
if (classifyRunControlRecord(record) === "pending" &&
|
|
137
|
+
record.run_instance_id !== runInstanceId)
|
|
138
|
+
return "generation_mismatch";
|
|
139
|
+
return "valid";
|
|
36
140
|
}
|
|
37
141
|
export function appendRunControlInStateDir(stateDir, request) {
|
|
142
|
+
const action = Control.normalizeControlAction(request.action);
|
|
143
|
+
const input = request.input === undefined ? undefined : Control.normalizeControlInput(request.input);
|
|
38
144
|
const record = {
|
|
39
145
|
id: randomUUID(),
|
|
40
146
|
run_instance_id: request.run_instance_id,
|
|
41
|
-
action
|
|
42
|
-
...(
|
|
147
|
+
action,
|
|
148
|
+
...(input !== undefined ? { input } : {}),
|
|
43
149
|
status: "queued",
|
|
44
150
|
queued_at: new Date().toISOString(),
|
|
45
151
|
};
|
|
152
|
+
const encodedRecordBytes = Buffer.byteLength(`${JSON.stringify(record)}\n`);
|
|
46
153
|
const releaseLock = acquireRunControlsLock(stateDir);
|
|
47
154
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
155
|
+
const path = runControlsFile(stateDir);
|
|
156
|
+
let bytes = 0;
|
|
157
|
+
try {
|
|
158
|
+
bytes = statSync(path).size;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error.code !== "ENOENT")
|
|
162
|
+
bytes = Number.MAX_SAFE_INTEGER;
|
|
163
|
+
}
|
|
164
|
+
const journal = bytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES
|
|
165
|
+
? { diagnostics: [], records: [] } : readRunControlJournalFromStateDir(stateDir);
|
|
166
|
+
let integrity = bytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES
|
|
167
|
+
? "journal_bytes"
|
|
168
|
+
: journal.diagnostics.some((item) => item.line !== undefined)
|
|
169
|
+
? "malformed"
|
|
170
|
+
: journal.diagnostics.length ? "unreadable" : "valid";
|
|
171
|
+
for (const value of journal.records) {
|
|
172
|
+
if (integrity !== "valid")
|
|
173
|
+
break;
|
|
174
|
+
integrity = controlIntegrity(value, request.run_instance_id);
|
|
175
|
+
}
|
|
176
|
+
const records = journal.records;
|
|
177
|
+
const retained = encodeRunControls(compactRunControls(records));
|
|
178
|
+
const decision = decideRunControlAdmission({
|
|
179
|
+
integrity,
|
|
180
|
+
journalBytes: bytes,
|
|
181
|
+
newRecordBytes: encodedRecordBytes,
|
|
182
|
+
records,
|
|
183
|
+
retainedJournalBytes: Buffer.byteLength(retained),
|
|
184
|
+
runInstanceId: request.run_instance_id,
|
|
185
|
+
capacity: computeRunControlCapacity(records),
|
|
50
186
|
});
|
|
187
|
+
if (!decision.admitted)
|
|
188
|
+
throw admissionError(decision.error);
|
|
189
|
+
writeTextAtomic(path, `${retained}${JSON.stringify(record)}\n`);
|
|
190
|
+
return record;
|
|
51
191
|
}
|
|
52
192
|
finally {
|
|
53
193
|
releaseLock();
|
|
54
194
|
}
|
|
55
|
-
return record;
|
|
56
195
|
}
|
|
57
196
|
export function updateRunControlStatusInStateDir(stateDir, id, nextStatus, metadata = {}, expectedStatuses) {
|
|
58
197
|
const releaseLock = acquireRunControlsLock(stateDir);
|
|
59
198
|
try {
|
|
60
|
-
const records =
|
|
199
|
+
const records = readMutableRunControls(stateDir);
|
|
200
|
+
if (!records)
|
|
201
|
+
return false;
|
|
61
202
|
const index = records.findIndex((record) => record.id === id);
|
|
62
203
|
if (index < 0)
|
|
63
204
|
return false;
|
|
64
205
|
const record = records[index];
|
|
206
|
+
if (records.some((value) => controlIntegrity(value, record.run_instance_id) !== "valid"))
|
|
207
|
+
return false;
|
|
65
208
|
if (expectedStatuses && !expectedStatuses.includes(record.status))
|
|
66
209
|
return false;
|
|
67
210
|
if (record.status === nextStatus)
|
|
@@ -70,28 +213,19 @@ export function updateRunControlStatusInStateDir(stateDir, id, nextStatus, metad
|
|
|
70
213
|
if (nextStatus === "delivered") {
|
|
71
214
|
if (record.delivered_at)
|
|
72
215
|
return false;
|
|
73
|
-
records[index] = {
|
|
74
|
-
...record
|
|
75
|
-
delivered_at: timestamp,
|
|
76
|
-
...(record.status === "queued" ? { status: "delivered" } : {}),
|
|
77
|
-
};
|
|
216
|
+
records[index] = { ...record, delivered_at: timestamp,
|
|
217
|
+
...(record.status === "queued" ? { status: "delivered" } : {}) };
|
|
78
218
|
}
|
|
79
219
|
else {
|
|
80
|
-
if (
|
|
220
|
+
if (classifyRunControlRecord(record) === "terminal" || nextStatus === "queued")
|
|
81
221
|
return false;
|
|
82
|
-
if ((nextStatus === "claimed" &&
|
|
83
|
-
record.status !== "queued" &&
|
|
222
|
+
if ((nextStatus === "claimed" && record.status !== "queued" &&
|
|
84
223
|
record.status !== "delivered") ||
|
|
85
|
-
(nextStatus === "handled" && record.status !== "claimed"))
|
|
224
|
+
(nextStatus === "handled" && record.status !== "claimed"))
|
|
86
225
|
return false;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
...record,
|
|
91
|
-
...metadata,
|
|
92
|
-
[timestampKey]: timestamp,
|
|
93
|
-
status: nextStatus,
|
|
94
|
-
};
|
|
226
|
+
records[index] = { ...record,
|
|
227
|
+
...(metadata.error !== undefined ? { error: boundedControlError(metadata.error) } : {}),
|
|
228
|
+
[`${nextStatus}_at`]: timestamp, status: nextStatus };
|
|
95
229
|
}
|
|
96
230
|
writeRunControls(stateDir, records);
|
|
97
231
|
return true;
|
|
@@ -100,18 +234,19 @@ export function updateRunControlStatusInStateDir(stateDir, id, nextStatus, metad
|
|
|
100
234
|
releaseLock();
|
|
101
235
|
}
|
|
102
236
|
}
|
|
103
|
-
|
|
237
|
+
function claimRunControl(stateDir, runInstanceId, controlId) {
|
|
104
238
|
const releaseLock = acquireRunControlsLock(stateDir);
|
|
105
239
|
try {
|
|
106
|
-
const records =
|
|
107
|
-
|
|
240
|
+
const records = readMutableRunControls(stateDir, runInstanceId);
|
|
241
|
+
if (!records)
|
|
242
|
+
return undefined;
|
|
243
|
+
const index = records.findIndex((record) => (controlId === undefined || record.id === controlId) &&
|
|
244
|
+
record.run_instance_id === runInstanceId &&
|
|
108
245
|
(record.status === "queued" || record.status === "delivered"));
|
|
109
246
|
if (index < 0)
|
|
110
247
|
return undefined;
|
|
111
248
|
const claimed = {
|
|
112
|
-
...records[index],
|
|
113
|
-
claimed_at: new Date().toISOString(),
|
|
114
|
-
status: "claimed",
|
|
249
|
+
...records[index], claimed_at: new Date().toISOString(), status: "claimed"
|
|
115
250
|
};
|
|
116
251
|
records[index] = claimed;
|
|
117
252
|
writeRunControls(stateDir, records);
|
|
@@ -121,6 +256,12 @@ export function claimRunControlInStateDir(stateDir, runInstanceId) {
|
|
|
121
256
|
releaseLock();
|
|
122
257
|
}
|
|
123
258
|
}
|
|
259
|
+
export function claimRunControlInStateDir(stateDir, runInstanceId) {
|
|
260
|
+
return claimRunControl(stateDir, runInstanceId);
|
|
261
|
+
}
|
|
262
|
+
export function claimRunControlByIdInStateDir(stateDir, runInstanceId, controlId) {
|
|
263
|
+
return claimRunControl(stateDir, runInstanceId, controlId);
|
|
264
|
+
}
|
|
124
265
|
export async function processRunControlsInStateDir(stateDir, runInstanceId, handler, limit = 1) {
|
|
125
266
|
const result = { claimed: 0, failed: 0, handled: 0 };
|
|
126
267
|
for (let index = 0; index < Math.max(1, limit); index += 1) {
|
|
@@ -130,14 +271,12 @@ export async function processRunControlsInStateDir(stateDir, runInstanceId, hand
|
|
|
130
271
|
result.claimed += 1;
|
|
131
272
|
try {
|
|
132
273
|
await handler(control);
|
|
133
|
-
if (updateRunControlStatusInStateDir(stateDir, control.id, "handled", {}, ["claimed"]))
|
|
274
|
+
if (updateRunControlStatusInStateDir(stateDir, control.id, "handled", {}, ["claimed"]))
|
|
134
275
|
result.handled += 1;
|
|
135
|
-
}
|
|
136
276
|
}
|
|
137
277
|
catch (error) {
|
|
138
|
-
if (updateRunControlStatusInStateDir(stateDir, control.id, "failed", { error: error instanceof Error ? error.message : String(error) }, ["claimed"]))
|
|
278
|
+
if (updateRunControlStatusInStateDir(stateDir, control.id, "failed", { error: error instanceof Error ? error.message : String(error) }, ["claimed"]))
|
|
139
279
|
result.failed += 1;
|
|
140
|
-
}
|
|
141
280
|
}
|
|
142
281
|
}
|
|
143
282
|
return result;
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Owns: terminal-run archive and prune filesystem behavior.
|
|
4
4
|
*/
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import {
|
|
6
|
+
import { cpSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
7
7
|
import { basename, dirname, join } from "node:path";
|
|
8
|
-
import { acquireFileMutationLock, writeJsonAtomic } from "./file-state.js";
|
|
8
|
+
import { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } from "./file-state.js";
|
|
9
|
+
import * as Limits from "./limits.js";
|
|
10
|
+
import { readJsonlFileResilient } from "./state-readers.js";
|
|
9
11
|
import { resolveArtifactManifest, } from "./runs-artifacts.js";
|
|
10
12
|
import { safeRunId } from "./runs-identity.js";
|
|
11
13
|
import { assertOwnedRunStateDirectory } from "./runs-ownership.js";
|
|
@@ -13,26 +15,37 @@ export function appendRunRetentionEvidence(status, action, outcome, options = {}
|
|
|
13
15
|
const stateDir = String(status.state_dir);
|
|
14
16
|
const path = join(dirname(stateDir), "retention.jsonl");
|
|
15
17
|
const id = options.id ?? randomUUID();
|
|
18
|
+
const record = {
|
|
19
|
+
action,
|
|
20
|
+
...(options.error ? { error: options.error } : {}),
|
|
21
|
+
id,
|
|
22
|
+
outcome,
|
|
23
|
+
...(options.result ? { result: options.result } : {}),
|
|
24
|
+
run: String(status.run ?? basename(stateDir)),
|
|
25
|
+
...(typeof status.run_instance_id === "string" ? { run_instance_id: status.run_instance_id } : {}),
|
|
26
|
+
ts: new Date().toISOString(),
|
|
27
|
+
};
|
|
16
28
|
const release = acquireFileMutationLock(path);
|
|
17
29
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
ts: new Date().toISOString(),
|
|
29
|
-
})}\n`);
|
|
30
|
+
const records = [...readJsonlFileResilient(path).records, record]
|
|
31
|
+
.slice(-Limits.RUN_RETENTION_MAX_RECORDS);
|
|
32
|
+
let content = encodeRecords(records);
|
|
33
|
+
while (records.length && Buffer.byteLength(content) > Limits.RUN_RETENTION_MAX_BYTES) {
|
|
34
|
+
records.shift();
|
|
35
|
+
content = encodeRecords(records);
|
|
36
|
+
}
|
|
37
|
+
if (!records.includes(record))
|
|
38
|
+
throw new Error("Run retention record exceeds journal byte limit");
|
|
39
|
+
writeTextAtomic(path, content);
|
|
30
40
|
}
|
|
31
41
|
finally {
|
|
32
42
|
release();
|
|
33
43
|
}
|
|
34
44
|
return id;
|
|
35
45
|
}
|
|
46
|
+
function encodeRecords(records) {
|
|
47
|
+
return records.length ? `${records.map((value) => JSON.stringify(value)).join("\n")}\n` : "";
|
|
48
|
+
}
|
|
36
49
|
function retainedArtifactFilename(name, path) {
|
|
37
50
|
const readableName = name.replace(/[^A-Za-z0-9_.-]+/g, "_") || "artifact";
|
|
38
51
|
const identity = createHash("sha256")
|
package/dist/lib/runs-trace.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Run Trace event journal.
|
|
3
|
-
* Zones: structured event validation, bounded
|
|
4
|
-
* Owns Run-local
|
|
3
|
+
* Zones: structured event validation, bounded atomic retention, resilient reads
|
|
4
|
+
* Owns canonical Run-local Trace persistence; lifecycle projection and owner attention delivery stay in adapters.
|
|
5
5
|
*/
|
|
6
|
+
import type { StateReadDiagnostic } from "./state-readers.ts";
|
|
6
7
|
export interface TraceEvent {
|
|
7
8
|
id: string;
|
|
8
9
|
ts: string;
|
|
@@ -19,6 +20,29 @@ export interface AppendTraceEventInput {
|
|
|
19
20
|
level?: TraceEvent["level"];
|
|
20
21
|
attention?: TraceEvent["attention"];
|
|
21
22
|
}
|
|
23
|
+
export interface RunTraceReadEvent {
|
|
24
|
+
event: TraceEvent;
|
|
25
|
+
ordinal: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RunTraceJournalRead {
|
|
28
|
+
diagnostics: StateReadDiagnostic[];
|
|
29
|
+
events: RunTraceReadEvent[];
|
|
30
|
+
fileBytes: number;
|
|
31
|
+
omittedPrefixBytes: number;
|
|
32
|
+
readBytes: number;
|
|
33
|
+
}
|
|
34
|
+
export interface RunTraceSummary {
|
|
35
|
+
compacted: boolean;
|
|
36
|
+
compactions_total: number;
|
|
37
|
+
dropped_bytes: number;
|
|
38
|
+
dropped_event_count_exact: boolean;
|
|
39
|
+
dropped_events: number;
|
|
40
|
+
history_complete: boolean;
|
|
41
|
+
retained_bytes: number;
|
|
42
|
+
retained_events: number;
|
|
43
|
+
}
|
|
22
44
|
export declare function runTraceFile(stateDir: string): string;
|
|
23
45
|
export declare function appendRunTraceEvent(stateDir: string, input: AppendTraceEventInput): TraceEvent;
|
|
46
|
+
export declare function readRunTraceJournal(stateDir: string): RunTraceJournalRead;
|
|
47
|
+
export declare function summarizeRunTraceJournal(read: RunTraceJournalRead): RunTraceSummary;
|
|
24
48
|
export declare function readRunTraceEvents(stateDir: string, limit?: number): TraceEvent[];
|