@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.
Files changed (109) hide show
  1. package/AGENTS.md +16 -10
  2. package/CHANGELOG.md +425 -536
  3. package/README.md +13 -11
  4. package/dist/index.js +1 -1
  5. package/dist/lib/async-runs.d.ts +2 -1
  6. package/dist/lib/async-runs.js +24 -34
  7. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  8. package/dist/lib/automatic-review-runtime.js +5 -5
  9. package/dist/lib/command-templates.d.ts +2 -0
  10. package/dist/lib/command-templates.js +38 -4
  11. package/dist/lib/control-projection.d.ts +20 -0
  12. package/dist/lib/control-projection.js +66 -0
  13. package/dist/lib/control.d.ts +3 -0
  14. package/dist/lib/control.js +27 -14
  15. package/dist/lib/draft-sleep.js +3 -3
  16. package/dist/lib/file-state.d.ts +4 -1
  17. package/dist/lib/file-state.js +118 -44
  18. package/dist/lib/inspector-overlay.d.ts +2 -0
  19. package/dist/lib/inspector-overlay.js +124 -69
  20. package/dist/lib/limits.d.ts +15 -3
  21. package/dist/lib/limits.js +15 -3
  22. package/dist/lib/observability.d.ts +4 -2
  23. package/dist/lib/observability.js +43 -36
  24. package/dist/lib/prompts.d.ts +1 -1
  25. package/dist/lib/prompts.js +1 -1
  26. package/dist/lib/recipe-control.js +6 -2
  27. package/dist/lib/review-control.d.ts +1 -1
  28. package/dist/lib/review-control.js +4 -5
  29. package/dist/lib/run-evidence-policy.d.ts +95 -0
  30. package/dist/lib/run-evidence-policy.js +177 -0
  31. package/dist/lib/run-ui-runtime.js +2 -0
  32. package/dist/lib/runs-control-delivery.d.ts +8 -1
  33. package/dist/lib/runs-control-delivery.js +38 -15
  34. package/dist/lib/runs-controls.d.ts +8 -4
  35. package/dist/lib/runs-controls.js +189 -50
  36. package/dist/lib/runs-retention.js +27 -14
  37. package/dist/lib/runs-trace.d.ts +26 -2
  38. package/dist/lib/runs-trace.js +411 -18
  39. package/dist/lib/runtime-identity.d.ts +7 -0
  40. package/dist/lib/runtime-identity.js +35 -0
  41. package/dist/lib/runtime-triage.d.ts +29 -0
  42. package/dist/lib/runtime-triage.js +60 -0
  43. package/dist/lib/tool-review-scheduler.js +7 -7
  44. package/dist/lib/tools-inspect.js +91 -18
  45. package/dist/lib/tools-message.d.ts +1 -2
  46. package/dist/lib/tools-message.js +6 -6
  47. package/dist/lib/tools-response.d.ts +0 -1
  48. package/dist/lib/tools-response.js +0 -9
  49. package/dist/lib/tools.d.ts +1 -1
  50. package/dist/lib/tools.js +1 -1
  51. package/dist/lib/trace-projection.js +107 -41
  52. package/dist/scripts/conformance.mjs +5 -0
  53. package/dist/scripts/locker.mjs +40 -90
  54. package/dist/scripts/music-player.mjs +48 -142
  55. package/dist/scripts/release-gates.mjs +56 -3
  56. package/dist/scripts/validate-recipe.mjs +5 -4
  57. package/dist/skills/actors/SKILL.md +17 -11
  58. package/dist/skills/swarm/SKILL.md +2 -4
  59. package/docs/README.md +1 -4
  60. package/docs/actor-inspector.md +6 -5
  61. package/docs/async-runs.md +11 -9
  62. package/docs/command-templates.md +6 -116
  63. package/docs/recipe-library.md +4 -6
  64. package/docs/releasing.md +28 -0
  65. package/docs/template-recipes.md +1 -1
  66. package/docs/tool-registry.md +2 -2
  67. package/index.ts +1 -1
  68. package/lib/async-runs.ts +26 -50
  69. package/lib/automatic-review-runtime.ts +7 -7
  70. package/lib/command-templates.ts +44 -4
  71. package/lib/control-projection.ts +105 -0
  72. package/lib/control.ts +33 -18
  73. package/lib/draft-sleep.ts +3 -3
  74. package/lib/file-state.ts +91 -63
  75. package/lib/inspector-overlay.ts +108 -61
  76. package/lib/limits.ts +15 -3
  77. package/lib/observability.ts +55 -57
  78. package/lib/prompts.ts +1 -1
  79. package/lib/recipe-control.ts +9 -2
  80. package/lib/review-control.ts +4 -5
  81. package/lib/run-evidence-policy.ts +242 -0
  82. package/lib/run-ui-runtime.ts +2 -0
  83. package/lib/runs-control-delivery.ts +45 -17
  84. package/lib/runs-controls.ts +180 -102
  85. package/lib/runs-retention.ts +28 -20
  86. package/lib/runs-trace.ts +499 -20
  87. package/lib/runtime-identity.ts +39 -0
  88. package/lib/runtime-triage.ts +106 -0
  89. package/lib/tool-review-scheduler.ts +7 -7
  90. package/lib/tools-inspect.ts +94 -20
  91. package/lib/tools-message.ts +7 -8
  92. package/lib/tools-response.ts +0 -12
  93. package/lib/tools.ts +4 -4
  94. package/lib/trace-projection.ts +156 -71
  95. package/package.json +1 -1
  96. package/scripts/conformance.mjs +5 -0
  97. package/scripts/locker.mjs +40 -90
  98. package/scripts/music-player.mjs +48 -142
  99. package/scripts/release-gates.mjs +56 -3
  100. package/scripts/validate-recipe.mjs +5 -4
  101. package/skills/actors/SKILL.md +17 -11
  102. package/skills/swarm/SKILL.md +2 -4
  103. package/dist/lib/runtime-notifier.d.ts +0 -48
  104. package/dist/lib/runtime-notifier.js +0 -138
  105. package/docs/0.43-baseline.md +0 -44
  106. package/docs/actors-deep-reference.md +0 -108
  107. package/docs/component-recipes.md +0 -45
  108. package/docs/task-first-recipes.md +0 -261
  109. package/lib/runtime-notifier.ts +0 -211
@@ -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 type { TraceEvent } from "./runs-trace.ts";
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 traceEventItems(stateDir: string): TraceItem[] {
82
- const read = readJsonlFileResilient<TraceEvent>(join(stateDir, "trace.jsonl"));
83
- return [
84
- ...read.records.slice(-SOURCE_LIMIT).map((event) => ({
85
- id: event.id,
86
- ts: event.ts,
87
- source: sourceForEvent(event),
88
- kind: event.kind,
89
- summary: event.summary ?? event.kind,
90
- ...(event.level ? { level: event.level } : {}),
91
- detail: redact({
92
- ...(event.attention ? { attention: event.attention } : {}),
93
- ...(event.data !== undefined ? { data: event.data } : {}),
94
- }),
95
- } satisfies TraceItem)),
96
- ...diagnosticItems(read.diagnostics),
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 controlItems(stateDir: string): TraceItem[] {
101
- const read = readJsonlFileResilient<RunControlRecord>(
102
- join(stateDir, "controls.jsonl"),
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
- ...read.records.slice(-SOURCE_LIMIT).map((control) => ({
106
- id: `control:${control.id}`,
107
- ts: latestControlTimestamp(control),
108
- source: "control" as const,
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): TraceItem[] {
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" as const,
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): TraceItem[] {
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" as const,
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): TraceItem[] {
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) return diagnosticItems(read.diagnostics);
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
- typeof read.value.completedAt === "string"
165
- ? read.value.completedAt
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
- ): TraceItem[] {
261
+ ): OrderedTraceItem[] {
179
262
  const manifest = resolveArtifactManifest(declarations);
180
263
  if (!manifest) return [];
181
- return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(([name, artifact]) => ({
182
- id: `artifact:${name}`,
183
- ts: artifact.exists && existsSync(artifact.path)
184
- ? statSync(artifact.path).mtime.toISOString()
185
- : new Date(0).toISOString(),
186
- source: "artifact",
187
- kind: artifact.exists ? "artifact.ready" : "artifact.missing",
188
- summary: `${name}: ${artifact.exists ? "ready" : "missing"}`,
189
- ...(artifact.required && !artifact.exists ? { level: "warning" as const } : {}),
190
- detail: redact({
191
- exists: artifact.exists,
192
- path: relative(stateDir, artifact.path).replaceAll("\\", "/"),
193
- required: artifact.required,
194
- ...(artifact.size !== undefined ? { size: artifact.size } : {}),
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((left, right) =>
218
- right.ts.localeCompare(left.ts) || right.id.localeCompare(left.id),
219
- );
220
- return items.slice(0, Math.max(1, Math.min(options.limit ?? 100, 200)));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -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",
@@ -9,13 +9,11 @@
9
9
  */
10
10
 
11
11
  import { spawnSync } from "node:child_process";
12
- import { createHash, randomUUID } from "node:crypto";
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
- await importRuntimeModule("file-state");
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
- appendFileSync(
101
- journalPath,
102
- `${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`,
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
- appendFileSync(
108
- tracePath,
109
- `${JSON.stringify({
110
- attention: "followup",
111
- data,
112
- id: randomUUID(),
113
- kind,
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 = claimControl(control.id);
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
- finalizeControl(claimed.id, "handled");
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
- finalizeControl(claimed.id, "failed", text);
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;