@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/trace-projection.ts
CHANGED
|
@@ -7,19 +7,20 @@
|
|
|
7
7
|
import { existsSync, statSync } from "node:fs";
|
|
8
8
|
import { join, relative } from "node:path";
|
|
9
9
|
|
|
10
|
+
import * as ControlProjection from "./control-projection.ts";
|
|
10
11
|
import * as ExecutionSessions from "./execution-sessions.ts";
|
|
11
12
|
import * as Limits from "./limits.ts";
|
|
12
13
|
import {
|
|
13
14
|
resolveArtifactManifest,
|
|
14
15
|
type RunArtifactDeclaration,
|
|
15
16
|
} from "./runs-artifacts.ts";
|
|
17
|
+
import * as RunsControls from "./runs-controls.ts";
|
|
16
18
|
import type { RunControlRecord } from "./runs-controls.ts";
|
|
17
19
|
import { tailFile } from "./runs-status.ts";
|
|
18
|
-
import
|
|
20
|
+
import * as RunsTrace from "./runs-trace.ts";
|
|
19
21
|
import * as SessionEvidence from "./session-evidence.ts";
|
|
20
22
|
import {
|
|
21
23
|
readJsonFileResilient,
|
|
22
|
-
readJsonlFileResilient,
|
|
23
24
|
type StateReadDiagnostic,
|
|
24
25
|
} from "./state-readers.ts";
|
|
25
26
|
|
|
@@ -43,19 +44,52 @@ export interface TraceItem {
|
|
|
43
44
|
detail: unknown;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
interface OrderedTraceItem {
|
|
48
|
+
item: TraceItem;
|
|
49
|
+
ordinal: number;
|
|
50
|
+
sourceKey: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
const LIFECYCLE_PREFIXES = ["run.", "command."];
|
|
47
54
|
const SOURCE_LIMIT = 100;
|
|
55
|
+
const SOURCE_RANK: Record<TraceSource, number> = {
|
|
56
|
+
lifecycle: 6,
|
|
57
|
+
control: 5,
|
|
58
|
+
process: 4,
|
|
59
|
+
agent: 3,
|
|
60
|
+
artifact: 2,
|
|
61
|
+
runtime: 1,
|
|
62
|
+
};
|
|
48
63
|
|
|
49
64
|
function redact(value: unknown): unknown {
|
|
50
65
|
return SessionEvidence.redactSessionEvidenceValue(value);
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
function sourceForEvent(event: TraceEvent): TraceSource {
|
|
68
|
+
function sourceForEvent(event: RunsTrace.TraceEvent): TraceSource {
|
|
54
69
|
return LIFECYCLE_PREFIXES.some((prefix) => event.kind.startsWith(prefix))
|
|
55
70
|
? "lifecycle"
|
|
56
71
|
: "runtime";
|
|
57
72
|
}
|
|
58
73
|
|
|
74
|
+
function ordered(
|
|
75
|
+
item: TraceItem,
|
|
76
|
+
sourceKey: string,
|
|
77
|
+
ordinal: number,
|
|
78
|
+
): OrderedTraceItem {
|
|
79
|
+
return { item, ordinal, sourceKey };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function compareTraceItems(left: OrderedTraceItem, right: OrderedTraceItem): number {
|
|
83
|
+
const timestamp = right.item.ts.localeCompare(left.item.ts);
|
|
84
|
+
if (timestamp) return timestamp;
|
|
85
|
+
if (left.sourceKey === right.sourceKey) {
|
|
86
|
+
const ordinal = right.ordinal - left.ordinal;
|
|
87
|
+
if (ordinal) return ordinal;
|
|
88
|
+
}
|
|
89
|
+
return SOURCE_RANK[right.item.source] - SOURCE_RANK[left.item.source] ||
|
|
90
|
+
right.item.id.localeCompare(left.item.id);
|
|
91
|
+
}
|
|
92
|
+
|
|
59
93
|
function latestControlTimestamp(control: RunControlRecord): string {
|
|
60
94
|
return (
|
|
61
95
|
control.failed_at ??
|
|
@@ -78,122 +112,173 @@ function diagnosticItems(diagnostics: StateReadDiagnostic[]): TraceItem[] {
|
|
|
78
112
|
}));
|
|
79
113
|
}
|
|
80
114
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
function controlDiagnosticItems(
|
|
116
|
+
diagnostics: StateReadDiagnostic[],
|
|
117
|
+
): TraceItem[] {
|
|
118
|
+
return diagnostics.slice(-SOURCE_LIMIT).map((diagnostic, index) => ({
|
|
119
|
+
id: `control-diagnostic:${diagnostic.line ?? 0}:${index}`,
|
|
120
|
+
ts: new Date(0).toISOString(),
|
|
121
|
+
source: "runtime",
|
|
122
|
+
kind: "state.read_error",
|
|
123
|
+
summary: "Malformed or unreadable Control evidence",
|
|
124
|
+
level: "warning",
|
|
125
|
+
detail: {
|
|
126
|
+
...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
|
|
127
|
+
reason: diagnostic.line === undefined
|
|
128
|
+
? "unreadable_control_journal"
|
|
129
|
+
: "invalid_control_json",
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
98
132
|
}
|
|
99
133
|
|
|
100
|
-
function
|
|
101
|
-
const read =
|
|
102
|
-
|
|
134
|
+
function traceEventItems(stateDir: string): OrderedTraceItem[] {
|
|
135
|
+
const read = RunsTrace.readRunTraceJournal(stateDir);
|
|
136
|
+
const retained = read.events.slice(-SOURCE_LIMIT);
|
|
137
|
+
const projected = retained.map(({ event, ordinal }) => ordered({
|
|
138
|
+
id: event.id,
|
|
139
|
+
ts: event.ts,
|
|
140
|
+
source: sourceForEvent(event),
|
|
141
|
+
kind: event.kind,
|
|
142
|
+
summary: event.summary ?? event.kind,
|
|
143
|
+
...(event.level ? { level: event.level } : {}),
|
|
144
|
+
detail: redact({
|
|
145
|
+
...(event.attention ? { attention: event.attention } : {}),
|
|
146
|
+
...(event.data !== undefined ? { data: event.data } : {}),
|
|
147
|
+
}),
|
|
148
|
+
}, "trace", ordinal));
|
|
149
|
+
const diagnostics = diagnosticItems(read.diagnostics).map((item, index) =>
|
|
150
|
+
ordered(item, "trace", read.events.length + index + 1)
|
|
151
|
+
);
|
|
152
|
+
const hasMarker = read.events.some(
|
|
153
|
+
({ event }) => event.kind === "runtime.trace_compacted",
|
|
103
154
|
);
|
|
155
|
+
if (read.omittedPrefixBytes > 0 && !hasMarker) {
|
|
156
|
+
diagnostics.push(ordered({
|
|
157
|
+
id: "runtime:trace-history-incomplete",
|
|
158
|
+
ts: new Date(0).toISOString(),
|
|
159
|
+
source: "runtime",
|
|
160
|
+
kind: "runtime.trace_history_incomplete",
|
|
161
|
+
summary: "Trace history is incomplete",
|
|
162
|
+
level: "warning",
|
|
163
|
+
detail: {
|
|
164
|
+
omitted_prefix_bytes: read.omittedPrefixBytes,
|
|
165
|
+
read_bytes: read.readBytes,
|
|
166
|
+
retained_events: read.events.length,
|
|
167
|
+
},
|
|
168
|
+
}, "trace", 0));
|
|
169
|
+
}
|
|
170
|
+
return [...projected, ...diagnostics];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function controlItems(stateDir: string): OrderedTraceItem[] {
|
|
174
|
+
const read = RunsControls.readRunControlJournalFromStateDir(stateDir);
|
|
175
|
+
const start = Math.max(0, read.records.length - SOURCE_LIMIT);
|
|
176
|
+
const projected = read.records.slice(start).map((control, index) => {
|
|
177
|
+
const value = ControlProjection.projectRunControl(control as RunControlRecord);
|
|
178
|
+
return ordered({
|
|
179
|
+
id: `control:${value.id}`,
|
|
180
|
+
ts: latestControlTimestamp(value),
|
|
181
|
+
source: "control",
|
|
182
|
+
kind: `control.${value.status}`,
|
|
183
|
+
summary: `${value.action} ${value.status}`,
|
|
184
|
+
...(value.status === "failed" ? { level: "error" as const } : {}),
|
|
185
|
+
detail: value,
|
|
186
|
+
}, "control", start + index + 1);
|
|
187
|
+
});
|
|
104
188
|
return [
|
|
105
|
-
...
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
kind: `control.${control.status}`,
|
|
110
|
-
summary: `${control.action} ${control.status}`,
|
|
111
|
-
...(control.status === "failed" ? { level: "error" as const } : {}),
|
|
112
|
-
detail: redact(control),
|
|
113
|
-
})),
|
|
114
|
-
...diagnosticItems(read.diagnostics),
|
|
189
|
+
...projected,
|
|
190
|
+
...controlDiagnosticItems(read.diagnostics).map((item, index) =>
|
|
191
|
+
ordered(item, "control", read.records.length + index + 1)
|
|
192
|
+
),
|
|
115
193
|
];
|
|
116
194
|
}
|
|
117
195
|
|
|
118
|
-
function agentItems(stateDir: string):
|
|
196
|
+
function agentItems(stateDir: string): OrderedTraceItem[] {
|
|
119
197
|
return ExecutionSessions.readExecutionTurns(stateDir)
|
|
120
198
|
.slice(-SOURCE_LIMIT)
|
|
121
199
|
.map((turn) => {
|
|
122
200
|
const text = turn.assistantText ?? turn.userText ?? "Agent turn";
|
|
123
201
|
const summary = text.replaceAll(/\s+/g, " ").trim().slice(0, 160);
|
|
124
|
-
return {
|
|
202
|
+
return ordered({
|
|
125
203
|
id: `agent:${turn.commandId}:${turn.sessionFile}:${turn.index}`,
|
|
126
204
|
ts: turn.timestamp ?? new Date(0).toISOString(),
|
|
127
|
-
source: "agent"
|
|
205
|
+
source: "agent",
|
|
128
206
|
kind: "agent.turn",
|
|
129
207
|
summary: summary || "Agent turn",
|
|
130
208
|
...(turn.error ? { level: "error" as const } : {}),
|
|
131
209
|
detail: redact(turn),
|
|
132
|
-
};
|
|
210
|
+
}, "agent", turn.index);
|
|
133
211
|
});
|
|
134
212
|
}
|
|
135
213
|
|
|
136
|
-
function processItems(stateDir: string):
|
|
214
|
+
function processItems(stateDir: string): OrderedTraceItem[] {
|
|
137
215
|
return ["stdout", "stderr"].flatMap((stream) => {
|
|
138
216
|
const path = join(stateDir, `${stream}.log`);
|
|
139
217
|
if (!existsSync(path)) return [];
|
|
140
218
|
const stat = statSync(path);
|
|
141
219
|
const tail = tailFile(path, Limits.DEFAULT_INSPECT_LINES);
|
|
142
220
|
if (!tail && stat.size === 0) return [];
|
|
143
|
-
return [{
|
|
221
|
+
return [ordered({
|
|
144
222
|
id: `process:${stream}`,
|
|
145
223
|
ts: stat.mtime.toISOString(),
|
|
146
|
-
source: "process"
|
|
224
|
+
source: "process",
|
|
147
225
|
kind: `process.${stream}`,
|
|
148
226
|
summary: `${stream} (${stat.size} bytes)`,
|
|
149
227
|
...(stream === "stderr" ? { level: "warning" as const } : {}),
|
|
150
228
|
detail: redact({ bytes: stat.size, path, tail }),
|
|
151
|
-
}];
|
|
229
|
+
}, `process:${stream}`, 1)];
|
|
152
230
|
});
|
|
153
231
|
}
|
|
154
232
|
|
|
155
|
-
function resultItems(stateDir: string):
|
|
233
|
+
function resultItems(stateDir: string): OrderedTraceItem[] {
|
|
156
234
|
const path = join(stateDir, "result.json");
|
|
157
235
|
const read = readJsonFileResilient<Record<string, unknown>>(path, {});
|
|
158
|
-
if (Object.keys(read.value).length === 0)
|
|
236
|
+
if (Object.keys(read.value).length === 0) {
|
|
237
|
+
return diagnosticItems(read.diagnostics).map((item, index) =>
|
|
238
|
+
ordered(item, "process:result", index)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
159
241
|
const stat = statSync(path);
|
|
160
242
|
const failed = Number(read.value.code ?? 0) !== 0;
|
|
161
|
-
return [{
|
|
243
|
+
return [ordered({
|
|
162
244
|
id: "process:result",
|
|
163
|
-
ts:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
: stat.mtime.toISOString(),
|
|
245
|
+
ts: typeof read.value.completedAt === "string"
|
|
246
|
+
? read.value.completedAt
|
|
247
|
+
: stat.mtime.toISOString(),
|
|
167
248
|
source: "process",
|
|
168
249
|
kind: failed ? "process.failed" : "process.result",
|
|
169
250
|
summary: failed ? "Run process failed" : "Run process completed",
|
|
170
251
|
...(failed ? { level: "error" as const } : {}),
|
|
171
252
|
detail: redact(read.value),
|
|
172
|
-
}, ...diagnosticItems(read.diagnostics)
|
|
253
|
+
}, "process:result", 1), ...diagnosticItems(read.diagnostics).map(
|
|
254
|
+
(item, index) => ordered(item, "process:result", index + 2),
|
|
255
|
+
)];
|
|
173
256
|
}
|
|
174
257
|
|
|
175
258
|
function artifactItems(
|
|
176
259
|
stateDir: string,
|
|
177
260
|
declarations: Record<string, RunArtifactDeclaration> | undefined,
|
|
178
|
-
):
|
|
261
|
+
): OrderedTraceItem[] {
|
|
179
262
|
const manifest = resolveArtifactManifest(declarations);
|
|
180
263
|
if (!manifest) return [];
|
|
181
|
-
return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
264
|
+
return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(
|
|
265
|
+
([name, artifact], index) => ordered({
|
|
266
|
+
id: `artifact:${name}`,
|
|
267
|
+
ts: artifact.exists && existsSync(artifact.path)
|
|
268
|
+
? statSync(artifact.path).mtime.toISOString()
|
|
269
|
+
: new Date(0).toISOString(),
|
|
270
|
+
source: "artifact",
|
|
271
|
+
kind: artifact.exists ? "artifact.ready" : "artifact.missing",
|
|
272
|
+
summary: `${name}: ${artifact.exists ? "ready" : "missing"}`,
|
|
273
|
+
...(artifact.required && !artifact.exists ? { level: "warning" as const } : {}),
|
|
274
|
+
detail: redact({
|
|
275
|
+
exists: artifact.exists,
|
|
276
|
+
path: relative(stateDir, artifact.path).replaceAll("\\", "/"),
|
|
277
|
+
required: artifact.required,
|
|
278
|
+
...(artifact.size !== undefined ? { size: artifact.size } : {}),
|
|
279
|
+
}),
|
|
280
|
+
}, "artifact", index + 1),
|
|
281
|
+
);
|
|
197
282
|
}
|
|
198
283
|
|
|
199
284
|
export function projectRunTrace(
|
|
@@ -213,9 +298,9 @@ export function projectRunTrace(
|
|
|
213
298
|
...resultItems(stateDir),
|
|
214
299
|
...artifactItems(stateDir, options.artifacts),
|
|
215
300
|
]
|
|
216
|
-
.filter((item) => source === "all" || item.source === source)
|
|
217
|
-
.sort(
|
|
218
|
-
|
|
219
|
-
)
|
|
220
|
-
|
|
301
|
+
.filter(({ item }) => source === "all" || item.source === source)
|
|
302
|
+
.sort(compareTraceItems);
|
|
303
|
+
return items
|
|
304
|
+
.slice(0, Math.max(1, Math.min(options.limit ?? 100, 200)))
|
|
305
|
+
.map(({ item }) => item);
|
|
221
306
|
}
|
package/package.json
CHANGED
package/scripts/conformance.mjs
CHANGED
|
@@ -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/scripts/locker.mjs
CHANGED
|
@@ -9,13 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import { createHash
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
13
|
import {
|
|
14
|
-
appendFileSync,
|
|
15
14
|
existsSync,
|
|
16
15
|
mkdirSync,
|
|
17
16
|
readFileSync,
|
|
18
|
-
rmSync,
|
|
19
17
|
} from "node:fs";
|
|
20
18
|
import { createServer } from "node:net";
|
|
21
19
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -33,8 +31,13 @@ async function importRuntimeModule(name) {
|
|
|
33
31
|
return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
|
|
37
|
-
|
|
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");
|
|
40
|
+
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
38
41
|
|
|
39
42
|
function parseArgs(argv) {
|
|
40
43
|
const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
|
|
@@ -59,8 +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 tracePath = join(stateDir, "trace.jsonl");
|
|
63
|
-
const controlsPath = join(stateDir, "controls.jsonl");
|
|
64
65
|
const controlPath = join(stateDir, "control.fifo");
|
|
65
66
|
let runInstanceId;
|
|
66
67
|
mkdirSync(stateDir, { recursive: true });
|
|
@@ -97,25 +98,32 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
function journal(event, data = {}) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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` : "";
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
function emitTrace(kind, summary, data = {}, level = "info") {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
level,
|
|
115
|
-
summary,
|
|
116
|
-
ts: new Date().toISOString(),
|
|
117
|
-
})}\n`,
|
|
118
|
-
);
|
|
120
|
+
appendRunTraceEvent(stateDir, {
|
|
121
|
+
attention: "followup",
|
|
122
|
+
data,
|
|
123
|
+
kind,
|
|
124
|
+
level,
|
|
125
|
+
summary,
|
|
126
|
+
});
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
function now() {
|
|
@@ -153,69 +161,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
163
|
|
|
156
|
-
function acquireControlsLock() {
|
|
157
|
-
return acquireFileMutationLock(controlsPath);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function readControls() {
|
|
161
|
-
if (!existsSync(controlsPath)) return [];
|
|
162
|
-
return readFileSync(controlsPath, "utf8")
|
|
163
|
-
.split("\n")
|
|
164
|
-
.filter((line) => line.trim())
|
|
165
|
-
.map((line) => {
|
|
166
|
-
try {
|
|
167
|
-
return JSON.parse(line);
|
|
168
|
-
} catch {
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
.filter(Boolean);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function writeControls(controls) {
|
|
176
|
-
writeTextAtomic(
|
|
177
|
-
controlsPath,
|
|
178
|
-
controls.length
|
|
179
|
-
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
180
|
-
: "",
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function claimControl(id) {
|
|
185
|
-
const release = acquireControlsLock();
|
|
186
|
-
try {
|
|
187
|
-
const controls = readControls();
|
|
188
|
-
const control = controls.find((item) => item.id === id);
|
|
189
|
-
if (
|
|
190
|
-
!control ||
|
|
191
|
-
control.run_instance_id !== runInstanceId ||
|
|
192
|
-
(control.status !== "queued" && control.status !== "delivered")
|
|
193
|
-
) {
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
control.claimed_at = new Date().toISOString();
|
|
197
|
-
control.status = "claimed";
|
|
198
|
-
writeControls(controls);
|
|
199
|
-
return { ...control };
|
|
200
|
-
} finally {
|
|
201
|
-
release();
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function finalizeControl(id, status, error) {
|
|
206
|
-
const release = acquireControlsLock();
|
|
207
|
-
try {
|
|
208
|
-
const controls = readControls();
|
|
209
|
-
const control = controls.find((item) => item.id === id);
|
|
210
|
-
if (!control || control.status !== "claimed") return;
|
|
211
|
-
control.status = status;
|
|
212
|
-
control[`${status}_at`] = new Date().toISOString();
|
|
213
|
-
if (error) control.error = error;
|
|
214
|
-
writeControls(controls);
|
|
215
|
-
} finally {
|
|
216
|
-
release();
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
164
|
|
|
220
165
|
function tailJournal(count) {
|
|
221
166
|
if (!existsSync(journalPath)) return [];
|
|
@@ -403,7 +348,9 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
403
348
|
function handleLine(line) {
|
|
404
349
|
const control = normalizeControl(line);
|
|
405
350
|
if (!control) return false;
|
|
406
|
-
const claimed =
|
|
351
|
+
const claimed = claimRunControlByIdInStateDir(
|
|
352
|
+
stateDir, runInstanceId, control.id,
|
|
353
|
+
);
|
|
407
354
|
if (!claimed) {
|
|
408
355
|
emitTrace(
|
|
409
356
|
"lock.control_rejected",
|
|
@@ -415,11 +362,15 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
415
362
|
}
|
|
416
363
|
try {
|
|
417
364
|
const stopping = handle(claimed) === true;
|
|
418
|
-
|
|
365
|
+
updateRunControlStatusInStateDir(
|
|
366
|
+
stateDir, claimed.id, "handled", {}, ["claimed"],
|
|
367
|
+
);
|
|
419
368
|
return stopping;
|
|
420
369
|
} catch (error) {
|
|
421
370
|
const text = error instanceof Error ? error.message : String(error);
|
|
422
|
-
|
|
371
|
+
updateRunControlStatusInStateDir(
|
|
372
|
+
stateDir, claimed.id, "failed", { error: text }, ["claimed"],
|
|
373
|
+
);
|
|
423
374
|
journal("lock.error", { error: text });
|
|
424
375
|
emitTrace("lock.error", text, { error: text }, "error");
|
|
425
376
|
return false;
|
|
@@ -454,7 +405,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
454
405
|
}
|
|
455
406
|
|
|
456
407
|
async function serveNamedPipe(endpoint) {
|
|
457
|
-
rmSync(endpoint.path, { force: true });
|
|
458
408
|
let resolveStopped;
|
|
459
409
|
const stopped = new Promise((resolve) => {
|
|
460
410
|
resolveStopped = resolve;
|