@llblab/pi-actors 0.43.1 → 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 +6 -6
- package/BACKLOG.md +1 -189
- package/CHANGELOG.md +412 -402
- package/README.md +5 -5
- package/dist/lib/async-runs.js +8 -30
- package/dist/lib/file-state.d.ts +3 -1
- package/dist/lib/file-state.js +25 -7
- package/dist/lib/inspector-overlay.js +46 -30
- package/dist/lib/limits.d.ts +10 -0
- package/dist/lib/limits.js +10 -0
- package/dist/lib/observability.d.ts +4 -2
- package/dist/lib/observability.js +43 -36
- 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-controls.d.ts +7 -5
- package/dist/lib/runs-controls.js +186 -49
- package/dist/lib/runs-retention.js +27 -14
- package/dist/lib/runs-trace.d.ts +25 -1
- package/dist/lib/runs-trace.js +410 -21
- package/dist/lib/runtime-triage.js +6 -22
- package/dist/lib/tools-inspect.js +48 -14
- package/dist/lib/trace-projection.js +90 -44
- package/dist/scripts/conformance.mjs +5 -0
- package/dist/scripts/locker.mjs +31 -74
- package/dist/scripts/music-player.mjs +41 -129
- package/dist/scripts/release-gates.mjs +26 -4
- package/dist/skills/actors/SKILL.md +7 -6
- package/dist/skills/swarm/SKILL.md +2 -2
- package/docs/README.md +0 -1
- package/docs/actor-inspector.md +3 -2
- package/docs/async-runs.md +10 -8
- package/docs/recipe-library.md +2 -2
- package/lib/async-runs.ts +8 -45
- package/lib/file-state.ts +28 -7
- package/lib/inspector-overlay.ts +29 -17
- package/lib/limits.ts +10 -0
- package/lib/observability.ts +55 -57
- package/lib/run-evidence-policy.ts +242 -0
- package/lib/run-ui-runtime.ts +2 -0
- package/lib/runs-controls.ts +177 -108
- package/lib/runs-retention.ts +28 -20
- package/lib/runs-trace.ts +496 -21
- package/lib/runtime-triage.ts +11 -25
- package/lib/tools-inspect.ts +45 -15
- package/lib/trace-projection.ts +137 -75
- package/package.json +1 -1
- package/scripts/conformance.mjs +5 -0
- package/scripts/locker.mjs +31 -74
- package/scripts/music-player.mjs +41 -129
- package/scripts/release-gates.mjs +26 -4
- package/skills/actors/SKILL.md +7 -6
- package/skills/swarm/SKILL.md +2 -2
- package/dist/lib/runtime-notifier.d.ts +0 -48
- package/dist/lib/runtime-notifier.js +0 -138
- package/docs/0.43-baseline.md +0 -39
- package/lib/runtime-notifier.ts +0 -211
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* Zones: Run Recipe/Trace/Control views and runtime/recipe/tool diagnostics
|
|
4
4
|
* Owns exact inspect target/view dispatch; source projection stays in domain modules.
|
|
5
5
|
*/
|
|
6
|
+
import { statSync } from "node:fs";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import * as AsyncRuns from "./async-runs.js";
|
|
8
9
|
import * as ControlProjection from "./control-projection.js";
|
|
9
10
|
import * as Inspector from "./inspector.js";
|
|
10
11
|
import * as Limits from "./limits.js";
|
|
12
|
+
import { computeRunControlCapacity } from "./run-evidence-policy.js";
|
|
11
13
|
import * as Paths from "./paths.js";
|
|
12
14
|
import * as RecipesDiscovery from "./recipes-discovery.js";
|
|
13
15
|
import * as ReviewDiagnostics from "./review-diagnostics.js";
|
|
@@ -55,7 +57,9 @@ function runtimeTriage(ctx, deps) {
|
|
|
55
57
|
const failed = runs.filter((run) => run.status === "failed");
|
|
56
58
|
const pendingControls = [];
|
|
57
59
|
const staleControls = [];
|
|
60
|
+
const backpressuredRuns = [];
|
|
58
61
|
const controlDiagnostics = [];
|
|
62
|
+
const traceDiagnostics = [];
|
|
59
63
|
const nowMs = Date.now();
|
|
60
64
|
for (const run of runs) {
|
|
61
65
|
const stateDir = typeof run.state_dir === "string" ? run.state_dir : undefined;
|
|
@@ -63,13 +67,19 @@ function runtimeTriage(ctx, deps) {
|
|
|
63
67
|
continue;
|
|
64
68
|
const runId = String(run.run ?? "unknown");
|
|
65
69
|
const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
70
|
+
const capacity = computeRunControlCapacity(journal.records);
|
|
66
71
|
controlDiagnostics.push(...journal.diagnostics.map((diagnostic) => ({
|
|
67
72
|
...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
|
|
68
|
-
reason: diagnostic.line === undefined
|
|
69
|
-
? "unreadable_control_journal"
|
|
70
|
-
: "invalid_control_json",
|
|
71
|
-
run: runId,
|
|
73
|
+
reason: diagnostic.line === undefined ? "unreadable_control_journal" : "invalid_control_json", run: runId,
|
|
72
74
|
})));
|
|
75
|
+
if (capacity.backpressured)
|
|
76
|
+
backpressuredRuns.push({
|
|
77
|
+
run: runId, pending: capacity.pending, pending_limit: capacity.limit,
|
|
78
|
+
journal_bytes: statSync(RunsControls.runControlsFile(stateDir)).size,
|
|
79
|
+
});
|
|
80
|
+
const traceSummary = RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(stateDir));
|
|
81
|
+
if (!traceSummary.history_complete)
|
|
82
|
+
traceDiagnostics.push({ run: runId, ...traceSummary });
|
|
73
83
|
for (const control of journal.records) {
|
|
74
84
|
const classified = RuntimeTriage.classifyRuntimeControl({
|
|
75
85
|
run: runId,
|
|
@@ -114,12 +124,13 @@ function runtimeTriage(ctx, deps) {
|
|
|
114
124
|
pending_controls: boundedPendingControls,
|
|
115
125
|
stale_control_count: staleControls.length,
|
|
116
126
|
stale_controls: boundedStaleControls,
|
|
117
|
-
|
|
118
|
-
control_diagnostics: controlDiagnostics.slice(0, 40),
|
|
127
|
+
backpressured_run_count: backpressuredRuns.length, backpressured_runs: backpressuredRuns.slice(0, 40),
|
|
128
|
+
control_diagnostic_count: controlDiagnostics.length, control_diagnostics: controlDiagnostics.slice(0, 40),
|
|
129
|
+
trace_diagnostic_count: traceDiagnostics.length, trace_diagnostics: traceDiagnostics.slice(0, 40),
|
|
119
130
|
attention_events: attentionEvents.slice(-40).reverse(),
|
|
120
131
|
next_actions: [
|
|
121
132
|
...(failed.length ? ["inspect target=runtime view=runs status=failed"] : []),
|
|
122
|
-
...staleControls.slice(0, 3).map((entry) => `inspect target=run:${entry.run} view=control`),
|
|
133
|
+
...new Set([...staleControls, ...backpressuredRuns].slice(0, 3).map((entry) => `inspect target=run:${entry.run} view=control`)),
|
|
123
134
|
...(attentionEvents.length
|
|
124
135
|
? attentionEvents.slice(-3).map((entry) => `inspect target=run:${String(entry.run)} view=trace source=runtime`)
|
|
125
136
|
: []),
|
|
@@ -131,7 +142,7 @@ function compactRuntime(view, details) {
|
|
|
131
142
|
return `\nruntime runs=${details.runs?.length ?? 0}`;
|
|
132
143
|
}
|
|
133
144
|
if (view === "triage") {
|
|
134
|
-
return `\nruntime failed=${details.failed_runs?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} attention=${details.attention_events?.length ?? 0}`;
|
|
145
|
+
return `\nruntime failed=${details.failed_runs?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} backpressured=${Number(details.backpressured_run_count ?? 0)} incomplete_trace=${Number(details.trace_diagnostic_count ?? 0)} attention=${details.attention_events?.length ?? 0}`;
|
|
135
146
|
}
|
|
136
147
|
return `\nruntime version=${String(details.version)} state_schema=${String(details.state_schema)} automatic_review=${String(details.automatic_review)}`;
|
|
137
148
|
}
|
|
@@ -199,8 +210,8 @@ function inspectRun(run, view, input, ctx, deps) {
|
|
|
199
210
|
throw new Error(`unsupported Trace source: ${source}`);
|
|
200
211
|
}
|
|
201
212
|
return {
|
|
202
|
-
run,
|
|
203
|
-
|
|
213
|
+
run, source,
|
|
214
|
+
summary: RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(stateDir)),
|
|
204
215
|
items: TraceProjection.projectRunTrace(stateDir, {
|
|
205
216
|
artifacts: status.artifacts,
|
|
206
217
|
limit: Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
|
|
@@ -209,19 +220,42 @@ function inspectRun(run, view, input, ctx, deps) {
|
|
|
209
220
|
};
|
|
210
221
|
}
|
|
211
222
|
const runInstanceId = String(status.run_instance_id ?? "");
|
|
223
|
+
const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
224
|
+
const capacity = computeRunControlCapacity(journal.records);
|
|
225
|
+
const structuralDiagnostics = journal.records.flatMap((value) => {
|
|
226
|
+
const classified = RuntimeTriage.classifyRuntimeControl({ run,
|
|
227
|
+
runInstanceId: runInstanceId || undefined, status: String(status.status ?? "unknown") }, value, Date.now());
|
|
228
|
+
return classified.diagnostic ? [classified.diagnostic.reason] : [];
|
|
229
|
+
});
|
|
230
|
+
let journalBytes = 0;
|
|
231
|
+
try {
|
|
232
|
+
journalBytes = statSync(RunsControls.runControlsFile(stateDir)).size;
|
|
233
|
+
}
|
|
234
|
+
catch { }
|
|
235
|
+
const diagnostics = [...journal.diagnostics.map((item) => ({
|
|
236
|
+
...(item.line === undefined ? {} : { line: item.line }),
|
|
237
|
+
reason: item.line === undefined ? "unreadable_control_journal" : "invalid_control_json",
|
|
238
|
+
})), ...structuralDiagnostics.map((reason) => ({ reason }))].slice(0, 40);
|
|
239
|
+
const nowMs = Date.now();
|
|
240
|
+
const stalePending = journal.records.reduce((count, control) => count + Number(Boolean(RuntimeTriage.classifyRuntimeControl({ run, runInstanceId: runInstanceId || undefined,
|
|
241
|
+
status: String(status.status ?? "unknown") }, control, nowMs).stale)), 0);
|
|
212
242
|
return {
|
|
213
243
|
run,
|
|
214
244
|
run_instance_id: runInstanceId,
|
|
215
245
|
status: status.status,
|
|
246
|
+
pending: capacity.pending, pending_limit: capacity.limit,
|
|
247
|
+
available: capacity.available, backpressured: capacity.backpressured,
|
|
248
|
+
journal_bytes: journalBytes, journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
|
|
249
|
+
...(capacity.oldest_pending_at ? { oldest_pending_at: capacity.oldest_pending_at } : {}),
|
|
250
|
+
stale_pending: stalePending,
|
|
251
|
+
diagnostics,
|
|
216
252
|
actor_actions: Array.isArray(status.control) ? status.control : [],
|
|
217
253
|
runtime_actions: status.status === "running" ? ["kill"] : ["archive", "prune"],
|
|
218
254
|
endpoint: runInstanceId
|
|
219
255
|
? RunControlDelivery.readRunControlEndpoint(stateDir, runInstanceId)
|
|
220
256
|
: undefined,
|
|
221
|
-
recent_controls:
|
|
222
|
-
.
|
|
223
|
-
.reverse()
|
|
224
|
-
.map(ControlProjection.projectRunControl),
|
|
257
|
+
recent_controls: journal.records.slice(-Math.max(1, Number(input.lines || 20))).reverse()
|
|
258
|
+
.map((control) => ControlProjection.projectRunControl(control)),
|
|
225
259
|
};
|
|
226
260
|
}
|
|
227
261
|
function inspectTool(name, view, deps) {
|
|
@@ -9,11 +9,21 @@ import * as ControlProjection from "./control-projection.js";
|
|
|
9
9
|
import * as ExecutionSessions from "./execution-sessions.js";
|
|
10
10
|
import * as Limits from "./limits.js";
|
|
11
11
|
import { resolveArtifactManifest, } from "./runs-artifacts.js";
|
|
12
|
+
import * as RunsControls from "./runs-controls.js";
|
|
12
13
|
import { tailFile } from "./runs-status.js";
|
|
14
|
+
import * as RunsTrace from "./runs-trace.js";
|
|
13
15
|
import * as SessionEvidence from "./session-evidence.js";
|
|
14
|
-
import { readJsonFileResilient,
|
|
16
|
+
import { readJsonFileResilient, } from "./state-readers.js";
|
|
15
17
|
const LIFECYCLE_PREFIXES = ["run.", "command."];
|
|
16
18
|
const SOURCE_LIMIT = 100;
|
|
19
|
+
const SOURCE_RANK = {
|
|
20
|
+
lifecycle: 6,
|
|
21
|
+
control: 5,
|
|
22
|
+
process: 4,
|
|
23
|
+
agent: 3,
|
|
24
|
+
artifact: 2,
|
|
25
|
+
runtime: 1,
|
|
26
|
+
};
|
|
17
27
|
function redact(value) {
|
|
18
28
|
return SessionEvidence.redactSessionEvidenceValue(value);
|
|
19
29
|
}
|
|
@@ -22,6 +32,21 @@ function sourceForEvent(event) {
|
|
|
22
32
|
? "lifecycle"
|
|
23
33
|
: "runtime";
|
|
24
34
|
}
|
|
35
|
+
function ordered(item, sourceKey, ordinal) {
|
|
36
|
+
return { item, ordinal, sourceKey };
|
|
37
|
+
}
|
|
38
|
+
function compareTraceItems(left, right) {
|
|
39
|
+
const timestamp = right.item.ts.localeCompare(left.item.ts);
|
|
40
|
+
if (timestamp)
|
|
41
|
+
return timestamp;
|
|
42
|
+
if (left.sourceKey === right.sourceKey) {
|
|
43
|
+
const ordinal = right.ordinal - left.ordinal;
|
|
44
|
+
if (ordinal)
|
|
45
|
+
return ordinal;
|
|
46
|
+
}
|
|
47
|
+
return SOURCE_RANK[right.item.source] - SOURCE_RANK[left.item.source] ||
|
|
48
|
+
right.item.id.localeCompare(left.item.id);
|
|
49
|
+
}
|
|
25
50
|
function latestControlTimestamp(control) {
|
|
26
51
|
return (control.failed_at ??
|
|
27
52
|
control.handled_at ??
|
|
@@ -57,39 +82,57 @@ function controlDiagnosticItems(diagnostics) {
|
|
|
57
82
|
}));
|
|
58
83
|
}
|
|
59
84
|
function traceEventItems(stateDir) {
|
|
60
|
-
const read =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
const read = RunsTrace.readRunTraceJournal(stateDir);
|
|
86
|
+
const retained = read.events.slice(-SOURCE_LIMIT);
|
|
87
|
+
const projected = retained.map(({ event, ordinal }) => ordered({
|
|
88
|
+
id: event.id,
|
|
89
|
+
ts: event.ts,
|
|
90
|
+
source: sourceForEvent(event),
|
|
91
|
+
kind: event.kind,
|
|
92
|
+
summary: event.summary ?? event.kind,
|
|
93
|
+
...(event.level ? { level: event.level } : {}),
|
|
94
|
+
detail: redact({
|
|
95
|
+
...(event.attention ? { attention: event.attention } : {}),
|
|
96
|
+
...(event.data !== undefined ? { data: event.data } : {}),
|
|
97
|
+
}),
|
|
98
|
+
}, "trace", ordinal));
|
|
99
|
+
const diagnostics = diagnosticItems(read.diagnostics).map((item, index) => ordered(item, "trace", read.events.length + index + 1));
|
|
100
|
+
const hasMarker = read.events.some(({ event }) => event.kind === "runtime.trace_compacted");
|
|
101
|
+
if (read.omittedPrefixBytes > 0 && !hasMarker) {
|
|
102
|
+
diagnostics.push(ordered({
|
|
103
|
+
id: "runtime:trace-history-incomplete",
|
|
104
|
+
ts: new Date(0).toISOString(),
|
|
105
|
+
source: "runtime",
|
|
106
|
+
kind: "runtime.trace_history_incomplete",
|
|
107
|
+
summary: "Trace history is incomplete",
|
|
108
|
+
level: "warning",
|
|
109
|
+
detail: {
|
|
110
|
+
omitted_prefix_bytes: read.omittedPrefixBytes,
|
|
111
|
+
read_bytes: read.readBytes,
|
|
112
|
+
retained_events: read.events.length,
|
|
113
|
+
},
|
|
114
|
+
}, "trace", 0));
|
|
115
|
+
}
|
|
116
|
+
return [...projected, ...diagnostics];
|
|
76
117
|
}
|
|
77
118
|
function controlItems(stateDir) {
|
|
78
|
-
const read =
|
|
119
|
+
const read = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
120
|
+
const start = Math.max(0, read.records.length - SOURCE_LIMIT);
|
|
121
|
+
const projected = read.records.slice(start).map((control, index) => {
|
|
122
|
+
const value = ControlProjection.projectRunControl(control);
|
|
123
|
+
return ordered({
|
|
124
|
+
id: `control:${value.id}`,
|
|
125
|
+
ts: latestControlTimestamp(value),
|
|
126
|
+
source: "control",
|
|
127
|
+
kind: `control.${value.status}`,
|
|
128
|
+
summary: `${value.action} ${value.status}`,
|
|
129
|
+
...(value.status === "failed" ? { level: "error" } : {}),
|
|
130
|
+
detail: value,
|
|
131
|
+
}, "control", start + index + 1);
|
|
132
|
+
});
|
|
79
133
|
return [
|
|
80
|
-
...
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
id: `control:${projected.id}`,
|
|
84
|
-
ts: latestControlTimestamp(projected),
|
|
85
|
-
source: "control",
|
|
86
|
-
kind: `control.${projected.status}`,
|
|
87
|
-
summary: `${projected.action} ${projected.status}`,
|
|
88
|
-
...(projected.status === "failed" ? { level: "error" } : {}),
|
|
89
|
-
detail: projected,
|
|
90
|
-
};
|
|
91
|
-
}),
|
|
92
|
-
...controlDiagnosticItems(read.diagnostics),
|
|
134
|
+
...projected,
|
|
135
|
+
...controlDiagnosticItems(read.diagnostics).map((item, index) => ordered(item, "control", read.records.length + index + 1)),
|
|
93
136
|
];
|
|
94
137
|
}
|
|
95
138
|
function agentItems(stateDir) {
|
|
@@ -98,7 +141,7 @@ function agentItems(stateDir) {
|
|
|
98
141
|
.map((turn) => {
|
|
99
142
|
const text = turn.assistantText ?? turn.userText ?? "Agent turn";
|
|
100
143
|
const summary = text.replaceAll(/\s+/g, " ").trim().slice(0, 160);
|
|
101
|
-
return {
|
|
144
|
+
return ordered({
|
|
102
145
|
id: `agent:${turn.commandId}:${turn.sessionFile}:${turn.index}`,
|
|
103
146
|
ts: turn.timestamp ?? new Date(0).toISOString(),
|
|
104
147
|
source: "agent",
|
|
@@ -106,7 +149,7 @@ function agentItems(stateDir) {
|
|
|
106
149
|
summary: summary || "Agent turn",
|
|
107
150
|
...(turn.error ? { level: "error" } : {}),
|
|
108
151
|
detail: redact(turn),
|
|
109
|
-
};
|
|
152
|
+
}, "agent", turn.index);
|
|
110
153
|
});
|
|
111
154
|
}
|
|
112
155
|
function processItems(stateDir) {
|
|
@@ -118,7 +161,7 @@ function processItems(stateDir) {
|
|
|
118
161
|
const tail = tailFile(path, Limits.DEFAULT_INSPECT_LINES);
|
|
119
162
|
if (!tail && stat.size === 0)
|
|
120
163
|
return [];
|
|
121
|
-
return [{
|
|
164
|
+
return [ordered({
|
|
122
165
|
id: `process:${stream}`,
|
|
123
166
|
ts: stat.mtime.toISOString(),
|
|
124
167
|
source: "process",
|
|
@@ -126,17 +169,18 @@ function processItems(stateDir) {
|
|
|
126
169
|
summary: `${stream} (${stat.size} bytes)`,
|
|
127
170
|
...(stream === "stderr" ? { level: "warning" } : {}),
|
|
128
171
|
detail: redact({ bytes: stat.size, path, tail }),
|
|
129
|
-
}];
|
|
172
|
+
}, `process:${stream}`, 1)];
|
|
130
173
|
});
|
|
131
174
|
}
|
|
132
175
|
function resultItems(stateDir) {
|
|
133
176
|
const path = join(stateDir, "result.json");
|
|
134
177
|
const read = readJsonFileResilient(path, {});
|
|
135
|
-
if (Object.keys(read.value).length === 0)
|
|
136
|
-
return diagnosticItems(read.diagnostics);
|
|
178
|
+
if (Object.keys(read.value).length === 0) {
|
|
179
|
+
return diagnosticItems(read.diagnostics).map((item, index) => ordered(item, "process:result", index));
|
|
180
|
+
}
|
|
137
181
|
const stat = statSync(path);
|
|
138
182
|
const failed = Number(read.value.code ?? 0) !== 0;
|
|
139
|
-
return [{
|
|
183
|
+
return [ordered({
|
|
140
184
|
id: "process:result",
|
|
141
185
|
ts: typeof read.value.completedAt === "string"
|
|
142
186
|
? read.value.completedAt
|
|
@@ -146,13 +190,13 @@ function resultItems(stateDir) {
|
|
|
146
190
|
summary: failed ? "Run process failed" : "Run process completed",
|
|
147
191
|
...(failed ? { level: "error" } : {}),
|
|
148
192
|
detail: redact(read.value),
|
|
149
|
-
}, ...diagnosticItems(read.diagnostics)];
|
|
193
|
+
}, "process:result", 1), ...diagnosticItems(read.diagnostics).map((item, index) => ordered(item, "process:result", index + 2))];
|
|
150
194
|
}
|
|
151
195
|
function artifactItems(stateDir, declarations) {
|
|
152
196
|
const manifest = resolveArtifactManifest(declarations);
|
|
153
197
|
if (!manifest)
|
|
154
198
|
return [];
|
|
155
|
-
return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(([name, artifact]) => ({
|
|
199
|
+
return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(([name, artifact], index) => ordered({
|
|
156
200
|
id: `artifact:${name}`,
|
|
157
201
|
ts: artifact.exists && existsSync(artifact.path)
|
|
158
202
|
? statSync(artifact.path).mtime.toISOString()
|
|
@@ -167,7 +211,7 @@ function artifactItems(stateDir, declarations) {
|
|
|
167
211
|
required: artifact.required,
|
|
168
212
|
...(artifact.size !== undefined ? { size: artifact.size } : {}),
|
|
169
213
|
}),
|
|
170
|
-
}));
|
|
214
|
+
}, "artifact", index + 1));
|
|
171
215
|
}
|
|
172
216
|
export function projectRunTrace(stateDir, options = {}) {
|
|
173
217
|
const source = options.source ?? "all";
|
|
@@ -179,7 +223,9 @@ export function projectRunTrace(stateDir, options = {}) {
|
|
|
179
223
|
...resultItems(stateDir),
|
|
180
224
|
...artifactItems(stateDir, options.artifacts),
|
|
181
225
|
]
|
|
182
|
-
.filter((item) => source === "all" || item.source === source)
|
|
183
|
-
.sort(
|
|
184
|
-
return items
|
|
226
|
+
.filter(({ item }) => source === "all" || item.source === source)
|
|
227
|
+
.sort(compareTraceItems);
|
|
228
|
+
return items
|
|
229
|
+
.slice(0, Math.max(1, Math.min(options.limit ?? 100, 200)))
|
|
230
|
+
.map(({ item }) => item);
|
|
185
231
|
}
|
|
@@ -15,6 +15,11 @@ const conformanceSuites = [
|
|
|
15
15
|
"tests/control.test.ts",
|
|
16
16
|
"tests/runs-controls.test.ts",
|
|
17
17
|
"tests/runs-trace.test.ts",
|
|
18
|
+
"tests/observability.test.ts",
|
|
19
|
+
"tests/async-run-control.test.ts",
|
|
20
|
+
"tests/run-kernel-dogfood.test.ts",
|
|
21
|
+
"tests/installed.scripts.test.ts",
|
|
22
|
+
"tests/inspector-overlay.test.ts",
|
|
18
23
|
"tests/tools-inspect-kernel.test.ts",
|
|
19
24
|
"tests/recipes-discovery.test.ts",
|
|
20
25
|
"tests/review-swarm-dogfood.test.ts",
|
package/dist/scripts/locker.mjs
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import {
|
|
14
|
-
appendFileSync,
|
|
15
14
|
existsSync,
|
|
16
15
|
mkdirSync,
|
|
17
16
|
readFileSync,
|
|
@@ -32,8 +31,12 @@ async function importRuntimeModule(name) {
|
|
|
32
31
|
return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
|
|
36
|
-
|
|
34
|
+
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } = await importRuntimeModule("file-state");
|
|
35
|
+
const { readJsonlFileResilient } = await importRuntimeModule("state-readers");
|
|
36
|
+
const LOCKER_JOURNAL_MAX_RECORDS = 512;
|
|
37
|
+
const LOCKER_JOURNAL_MAX_BYTES = 1024 * 1024;
|
|
38
|
+
const { claimRunControlByIdInStateDir, updateRunControlStatusInStateDir } =
|
|
39
|
+
await importRuntimeModule("runs-controls");
|
|
37
40
|
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
38
41
|
|
|
39
42
|
function parseArgs(argv) {
|
|
@@ -59,7 +62,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
59
62
|
const queuePath = join(stateDir, "queue.json");
|
|
60
63
|
const locksPath = join(stateDir, "locks.json");
|
|
61
64
|
const journalPath = join(stateDir, "journal.jsonl");
|
|
62
|
-
const controlsPath = join(stateDir, "controls.jsonl");
|
|
63
65
|
const controlPath = join(stateDir, "control.fifo");
|
|
64
66
|
let runInstanceId;
|
|
65
67
|
mkdirSync(stateDir, { recursive: true });
|
|
@@ -96,10 +98,22 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function journal(event, data = {}) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const release = acquireFileMutationLock(journalPath);
|
|
102
|
+
try {
|
|
103
|
+
const records = [...readJsonlFileResilient(journalPath).records,
|
|
104
|
+
{ event, ts: new Date().toISOString(), ...data }]
|
|
105
|
+
.slice(-LOCKER_JOURNAL_MAX_RECORDS);
|
|
106
|
+
let content = encodeJournal(records);
|
|
107
|
+
while (records.length && Buffer.byteLength(content) > LOCKER_JOURNAL_MAX_BYTES) {
|
|
108
|
+
records.shift();
|
|
109
|
+
content = encodeJournal(records);
|
|
110
|
+
}
|
|
111
|
+
writeTextAtomic(journalPath, content);
|
|
112
|
+
} finally { release(); }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function encodeJournal(records) {
|
|
116
|
+
return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
function emitTrace(kind, summary, data = {}, level = "info") {
|
|
@@ -147,69 +161,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
147
161
|
}
|
|
148
162
|
}
|
|
149
163
|
|
|
150
|
-
function acquireControlsLock() {
|
|
151
|
-
return acquireFileMutationLock(controlsPath);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function readControls() {
|
|
155
|
-
if (!existsSync(controlsPath)) return [];
|
|
156
|
-
return readFileSync(controlsPath, "utf8")
|
|
157
|
-
.split("\n")
|
|
158
|
-
.filter((line) => line.trim())
|
|
159
|
-
.map((line) => {
|
|
160
|
-
try {
|
|
161
|
-
return JSON.parse(line);
|
|
162
|
-
} catch {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
.filter(Boolean);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function writeControls(controls) {
|
|
170
|
-
writeTextAtomic(
|
|
171
|
-
controlsPath,
|
|
172
|
-
controls.length
|
|
173
|
-
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
174
|
-
: "",
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function claimControl(id) {
|
|
179
|
-
const release = acquireControlsLock();
|
|
180
|
-
try {
|
|
181
|
-
const controls = readControls();
|
|
182
|
-
const control = controls.find((item) => item.id === id);
|
|
183
|
-
if (
|
|
184
|
-
!control ||
|
|
185
|
-
control.run_instance_id !== runInstanceId ||
|
|
186
|
-
(control.status !== "queued" && control.status !== "delivered")
|
|
187
|
-
) {
|
|
188
|
-
return undefined;
|
|
189
|
-
}
|
|
190
|
-
control.claimed_at = new Date().toISOString();
|
|
191
|
-
control.status = "claimed";
|
|
192
|
-
writeControls(controls);
|
|
193
|
-
return { ...control };
|
|
194
|
-
} finally {
|
|
195
|
-
release();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function finalizeControl(id, status, error) {
|
|
200
|
-
const release = acquireControlsLock();
|
|
201
|
-
try {
|
|
202
|
-
const controls = readControls();
|
|
203
|
-
const control = controls.find((item) => item.id === id);
|
|
204
|
-
if (!control || control.status !== "claimed") return;
|
|
205
|
-
control.status = status;
|
|
206
|
-
control[`${status}_at`] = new Date().toISOString();
|
|
207
|
-
if (error) control.error = error;
|
|
208
|
-
writeControls(controls);
|
|
209
|
-
} finally {
|
|
210
|
-
release();
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
164
|
|
|
214
165
|
function tailJournal(count) {
|
|
215
166
|
if (!existsSync(journalPath)) return [];
|
|
@@ -397,7 +348,9 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
397
348
|
function handleLine(line) {
|
|
398
349
|
const control = normalizeControl(line);
|
|
399
350
|
if (!control) return false;
|
|
400
|
-
const claimed =
|
|
351
|
+
const claimed = claimRunControlByIdInStateDir(
|
|
352
|
+
stateDir, runInstanceId, control.id,
|
|
353
|
+
);
|
|
401
354
|
if (!claimed) {
|
|
402
355
|
emitTrace(
|
|
403
356
|
"lock.control_rejected",
|
|
@@ -409,11 +362,15 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
409
362
|
}
|
|
410
363
|
try {
|
|
411
364
|
const stopping = handle(claimed) === true;
|
|
412
|
-
|
|
365
|
+
updateRunControlStatusInStateDir(
|
|
366
|
+
stateDir, claimed.id, "handled", {}, ["claimed"],
|
|
367
|
+
);
|
|
413
368
|
return stopping;
|
|
414
369
|
} catch (error) {
|
|
415
370
|
const text = error instanceof Error ? error.message : String(error);
|
|
416
|
-
|
|
371
|
+
updateRunControlStatusInStateDir(
|
|
372
|
+
stateDir, claimed.id, "failed", { error: text }, ["claimed"],
|
|
373
|
+
);
|
|
417
374
|
journal("lock.error", { error: text });
|
|
418
375
|
emitTrace("lock.error", text, { error: text }, "error");
|
|
419
376
|
return false;
|