@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
@@ -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
- export const FIFO_ATOMIC_CONTROL_MAX_BYTES = 512;
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 control = appendRunControlInStateDir(stateDir, request);
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: request.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: request.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: request.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 Control records, claim locks, status transitions, bounded compaction
4
- * Owns Run-local Control persistence; owner/generation authorization and transport delivery stay in lifecycle adapters.
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
- export declare const RUN_CONTROL_TERMINAL_LIMIT = 128;
7
- export type RunControlStatus = "queued" | "delivered" | "claimed" | "handled" | "failed";
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 Control records, claim locks, status transitions, bounded compaction
4
- * Owns Run-local Control persistence; owner/generation authorization and transport delivery stay in lifecycle adapters.
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 { writeFileSync } from "node:fs";
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 { readJsonlFileResilient } from "./state-readers.js";
11
- export const RUN_CONTROL_TERMINAL_LIMIT = 128;
12
- const TERMINAL_STATUSES = new Set(["handled", "failed"]);
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 readJsonlFileResilient(runControlsFile(stateDir))
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) => !TERMINAL_STATUSES.has(record.status));
25
- const terminal = records
26
- .filter((record) => TERMINAL_STATUSES.has(record.status))
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 compacted = compactRunControls(records);
33
- writeTextAtomic(runControlsFile(stateDir), compacted.length
34
- ? `${compacted.map((record) => JSON.stringify(record)).join("\n")}\n`
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: request.action,
42
- ...(request.input !== undefined ? { input: request.input } : {}),
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
- writeFileSync(runControlsFile(stateDir), `${JSON.stringify(record)}\n`, {
49
- flag: "a",
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 = readRunControlsFromStateDir(stateDir);
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 (TERMINAL_STATUSES.has(record.status) || nextStatus === "queued")
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
- const timestampKey = `${nextStatus}_at`;
89
- records[index] = {
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
- export function claimRunControlInStateDir(stateDir, runInstanceId) {
237
+ function claimRunControl(stateDir, runInstanceId, controlId) {
104
238
  const releaseLock = acquireRunControlsLock(stateDir);
105
239
  try {
106
- const records = readRunControlsFromStateDir(stateDir);
107
- const index = records.findIndex((record) => record.run_instance_id === runInstanceId &&
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 { appendFileSync, cpSync, mkdirSync, renameSync, rmSync } from "node:fs";
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
- appendFileSync(path, `${JSON.stringify({
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"
26
- ? { run_instance_id: status.run_instance_id }
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")
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Run Trace event journal.
3
- * Zones: structured event validation, bounded append, resilient bounded reads
4
- * Owns Run-local semantic Trace events; lifecycle projection and owner attention delivery stay in adapters.
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[];