@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
@@ -1,14 +1,24 @@
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
6
  import { randomUUID } from "node:crypto";
7
- import { writeFileSync } from "node:fs";
7
+ import { closeSync, fstatSync, openSync, readSync, writeFileSync, } from "node:fs";
8
8
  import { join } from "node:path";
9
+ import { withFileMutationLock, writeTextAtomic } from "./file-state.js";
9
10
  import * as Limits from "./limits.js";
10
- import { readJsonlFileResilient } from "./state-readers.js";
11
+ import { accumulateTraceCompactionStatistics, selectNewestTraceSuffix, traceAppendFits, traceCompactionMarkerInput, TRACE_COMPACTION_KIND, TRACE_COMPACTION_VERSION, } from "./run-evidence-policy.js";
11
12
  const KIND_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)*$/;
13
+ const TRACE_FIELDS = new Set([
14
+ "attention",
15
+ "data",
16
+ "id",
17
+ "kind",
18
+ "level",
19
+ "summary",
20
+ "ts",
21
+ ]);
12
22
  const TRACE_INPUT_FIELDS = new Set([
13
23
  "attention",
14
24
  "data",
@@ -25,9 +35,25 @@ const FORBIDDEN_FIELDS = new Set([
25
35
  "to",
26
36
  "type",
27
37
  ]);
38
+ const COMPACTION_DATA_FIELDS = new Set([
39
+ "compactions_total",
40
+ "dropped_bytes_total",
41
+ "dropped_event_count_exact",
42
+ "dropped_malformed_lines_total",
43
+ "dropped_valid_events_total",
44
+ "history_complete",
45
+ "retained_bytes",
46
+ "retained_events",
47
+ "version",
48
+ ]);
49
+ const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
28
50
  export function runTraceFile(stateDir) {
29
51
  return join(stateDir, "trace.jsonl");
30
52
  }
53
+ function hasExactFields(record, fields) {
54
+ const keys = Object.keys(record);
55
+ return keys.length === fields.size && keys.every((key) => fields.has(key));
56
+ }
31
57
  function normalizeTraceEventInput(input) {
32
58
  if (!input || typeof input !== "object" || Array.isArray(input)) {
33
59
  throw new Error("Trace event must be an object");
@@ -44,6 +70,9 @@ function normalizeTraceEventInput(input) {
44
70
  if (typeof record.kind !== "string" || !KIND_PATTERN.test(record.kind)) {
45
71
  throw new Error("Trace event kind must be a lowercase semantic token");
46
72
  }
73
+ if (record.kind === TRACE_COMPACTION_KIND) {
74
+ throw new Error(`Trace event kind is runtime-reserved: ${TRACE_COMPACTION_KIND}`);
75
+ }
47
76
  if (record.summary !== undefined && typeof record.summary !== "string") {
48
77
  throw new Error("Trace event summary must be a string");
49
78
  }
@@ -72,27 +101,391 @@ function normalizeTraceEventInput(input) {
72
101
  : {}),
73
102
  };
74
103
  }
75
- export function appendRunTraceEvent(stateDir, input) {
76
- const normalized = normalizeTraceEventInput(input);
77
- const event = {
78
- id: randomUUID(),
79
- ts: new Date().toISOString(),
80
- ...normalized,
81
- };
82
- let encoded;
104
+ function encodeTraceEvent(event) {
105
+ let serialized;
83
106
  try {
84
- encoded = JSON.stringify(event);
107
+ serialized = JSON.stringify(event);
85
108
  }
86
109
  catch {
87
110
  throw new Error("Trace event data must be JSON-serializable");
88
111
  }
89
- if (Buffer.byteLength(encoded) > Limits.TRACE_EVENT_MAX_BYTES) {
112
+ if (Buffer.byteLength(serialized) > Limits.TRACE_EVENT_MAX_BYTES) {
90
113
  throw new Error(`Trace event exceeds ${Limits.TRACE_EVENT_MAX_BYTES} bytes`);
91
114
  }
92
- writeFileSync(runTraceFile(stateDir), `${encoded}\n`, { flag: "a" });
93
- return event;
115
+ const encoded = `${serialized}\n`;
116
+ return { encoded, encodedBytes: Buffer.byteLength(encoded), event };
117
+ }
118
+ function createTraceEvent(input) {
119
+ return encodeTraceEvent({
120
+ id: randomUUID(),
121
+ ts: new Date().toISOString(),
122
+ ...normalizeTraceEventInput(input),
123
+ });
124
+ }
125
+ function readTraceWindow(path) {
126
+ let fd;
127
+ try {
128
+ fd = openSync(path, "r");
129
+ }
130
+ catch (error) {
131
+ if (error.code === "ENOENT") {
132
+ return {
133
+ content: Buffer.alloc(0),
134
+ fileBytes: 0,
135
+ leadingPartialLines: 0,
136
+ omittedPrefixBytes: 0,
137
+ readBytes: 0,
138
+ };
139
+ }
140
+ throw error;
141
+ }
142
+ try {
143
+ const fileBytes = fstatSync(fd).size;
144
+ if (!Number.isSafeInteger(fileBytes) || fileBytes < 0) {
145
+ throw new Error("Trace journal size is not safely representable");
146
+ }
147
+ const readBytes = Math.min(fileBytes, Limits.TRACE_JOURNAL_MAX_BYTES);
148
+ const offset = fileBytes - readBytes;
149
+ const buffer = Buffer.allocUnsafe(readBytes);
150
+ let consumed = 0;
151
+ while (consumed < readBytes) {
152
+ const count = readSync(fd, buffer, consumed, readBytes - consumed, offset + consumed);
153
+ if (count === 0)
154
+ throw new Error("Trace journal changed during bounded read");
155
+ consumed += count;
156
+ }
157
+ if (fstatSync(fd).size !== fileBytes) {
158
+ throw new Error("Trace journal changed during bounded read");
159
+ }
160
+ if (offset === 0) {
161
+ return {
162
+ content: buffer,
163
+ fileBytes,
164
+ leadingPartialLines: 0,
165
+ omittedPrefixBytes: 0,
166
+ readBytes,
167
+ };
168
+ }
169
+ const preceding = Buffer.allocUnsafe(1);
170
+ if (readSync(fd, preceding, 0, 1, offset - 1) !== 1) {
171
+ throw new Error("Trace journal changed during bounded read");
172
+ }
173
+ if (preceding[0] === 0x0a) {
174
+ return {
175
+ content: buffer,
176
+ fileBytes,
177
+ leadingPartialLines: 0,
178
+ omittedPrefixBytes: offset,
179
+ readBytes,
180
+ };
181
+ }
182
+ const newline = buffer.indexOf(0x0a);
183
+ const discarded = newline < 0 ? buffer.length : newline + 1;
184
+ return {
185
+ content: buffer.subarray(discarded),
186
+ fileBytes,
187
+ leadingPartialLines: 1,
188
+ omittedPrefixBytes: offset + discarded,
189
+ readBytes,
190
+ };
191
+ }
192
+ finally {
193
+ closeSync(fd);
194
+ }
195
+ }
196
+ function isTraceEvent(value) {
197
+ if (!value || typeof value !== "object" || Array.isArray(value))
198
+ return false;
199
+ const record = value;
200
+ if (Object.keys(record).some((key) => !TRACE_FIELDS.has(key)))
201
+ return false;
202
+ if (typeof record.id !== "string" || !record.id ||
203
+ typeof record.ts !== "string" || !Number.isFinite(Date.parse(record.ts)) ||
204
+ typeof record.kind !== "string" || !KIND_PATTERN.test(record.kind))
205
+ return false;
206
+ if (record.summary !== undefined && typeof record.summary !== "string")
207
+ return false;
208
+ if (record.level !== undefined &&
209
+ record.level !== "info" &&
210
+ record.level !== "warning" &&
211
+ record.level !== "error")
212
+ return false;
213
+ return record.attention === undefined ||
214
+ record.attention === "notify" ||
215
+ record.attention === "followup";
216
+ }
217
+ function isCount(value) {
218
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
219
+ }
220
+ function compactionData(value) {
221
+ if (!value || typeof value !== "object" || Array.isArray(value))
222
+ return undefined;
223
+ const record = value;
224
+ if (!hasExactFields(record, COMPACTION_DATA_FIELDS))
225
+ return undefined;
226
+ if (record.version !== TRACE_COMPACTION_VERSION ||
227
+ !isCount(record.compactions_total) ||
228
+ !isCount(record.dropped_valid_events_total) ||
229
+ !isCount(record.dropped_malformed_lines_total) ||
230
+ !isCount(record.dropped_bytes_total) ||
231
+ typeof record.dropped_event_count_exact !== "boolean" ||
232
+ !isCount(record.retained_events) ||
233
+ !isCount(record.retained_bytes) ||
234
+ record.history_complete !== false)
235
+ return undefined;
236
+ return record;
237
+ }
238
+ function markerData(event) {
239
+ return event.kind === TRACE_COMPACTION_KIND &&
240
+ event.level === "warning" &&
241
+ event.summary === undefined &&
242
+ event.attention === undefined
243
+ ? compactionData(event.data)
244
+ : undefined;
245
+ }
246
+ function decodeTraceLine(line) {
247
+ if (line.length === 0 || line.length > Limits.TRACE_EVENT_MAX_BYTES) {
248
+ throw new Error("Trace journal line is empty or oversized");
249
+ }
250
+ const text = UTF8_DECODER.decode(line);
251
+ const event = JSON.parse(text);
252
+ if (!isTraceEvent(event) ||
253
+ (event.kind === TRACE_COMPACTION_KIND && !markerData(event))) {
254
+ throw new Error("Trace journal line is not a canonical event");
255
+ }
256
+ return { event, text };
257
+ }
258
+ function parseTraceJournal(window) {
259
+ let candidates = [];
260
+ let candidateHead = 0;
261
+ let candidateBytes = 0;
262
+ let droppedBytes = window.omittedPrefixBytes;
263
+ let droppedValidEvents = 0;
264
+ let malformedLines = window.leadingPartialLines;
265
+ let physicalEvents = 0;
266
+ let previousCompaction;
267
+ const dropOldestCandidate = () => {
268
+ const dropped = candidates[candidateHead++];
269
+ candidateBytes -= dropped.encodedBytes;
270
+ droppedBytes += dropped.encodedBytes;
271
+ droppedValidEvents += 1;
272
+ if (candidateHead > 2_048) {
273
+ candidates = candidates.slice(candidateHead);
274
+ candidateHead = 0;
275
+ }
276
+ };
277
+ const consume = (line, physicalBytes) => {
278
+ let decoded;
279
+ try {
280
+ decoded = decodeTraceLine(line);
281
+ }
282
+ catch {
283
+ malformedLines += 1;
284
+ droppedBytes += physicalBytes;
285
+ return;
286
+ }
287
+ const { event, text } = decoded;
288
+ physicalEvents += 1;
289
+ if (event.kind === TRACE_COMPACTION_KIND) {
290
+ previousCompaction = markerData(event);
291
+ return;
292
+ }
293
+ const encoded = `${text}\n`;
294
+ const candidate = {
295
+ encodedBytes: Buffer.byteLength(encoded),
296
+ value: { encoded, encodedBytes: Buffer.byteLength(encoded), event },
297
+ };
298
+ candidates.push(candidate);
299
+ candidateBytes += candidate.encodedBytes;
300
+ while (candidates.length - candidateHead > Limits.TRACE_JOURNAL_TARGET_EVENTS ||
301
+ candidateBytes > Limits.TRACE_JOURNAL_TARGET_BYTES)
302
+ dropOldestCandidate();
303
+ };
304
+ let start = 0;
305
+ while (start < window.content.length) {
306
+ const newline = window.content.indexOf(0x0a, start);
307
+ if (newline < 0) {
308
+ malformedLines += 1;
309
+ droppedBytes += window.content.length - start;
310
+ break;
311
+ }
312
+ consume(window.content.subarray(start, newline), newline - start + 1);
313
+ start = newline + 1;
314
+ }
315
+ candidates = candidates.slice(candidateHead);
316
+ return {
317
+ candidateBytes,
318
+ candidates,
319
+ droppedBytes,
320
+ droppedValidEvents,
321
+ malformedLines,
322
+ physicalEvents,
323
+ ...(previousCompaction ? { previousCompaction } : {}),
324
+ };
325
+ }
326
+ function markerReserveBytes(id, ts) {
327
+ const maximum = Number.MAX_SAFE_INTEGER;
328
+ return encodeTraceEvent({
329
+ id,
330
+ ts,
331
+ ...traceCompactionMarkerInput({
332
+ version: TRACE_COMPACTION_VERSION,
333
+ compactions_total: maximum,
334
+ dropped_valid_events_total: maximum,
335
+ dropped_malformed_lines_total: maximum,
336
+ dropped_bytes_total: maximum,
337
+ dropped_event_count_exact: false,
338
+ retained_events: maximum,
339
+ retained_bytes: maximum,
340
+ history_complete: false,
341
+ }),
342
+ }).encodedBytes;
343
+ }
344
+ function createCompactionMarker(id, ts, previous, delta, retainedWithoutMarkerBytes) {
345
+ let markerBytes = 0;
346
+ for (let attempt = 0; attempt < 8; attempt += 1) {
347
+ const data = accumulateTraceCompactionStatistics(previous, {
348
+ ...delta,
349
+ retained_bytes: retainedWithoutMarkerBytes + markerBytes,
350
+ });
351
+ const marker = encodeTraceEvent({
352
+ id,
353
+ ts,
354
+ ...traceCompactionMarkerInput(data),
355
+ });
356
+ if (marker.encodedBytes === markerBytes)
357
+ return marker;
358
+ markerBytes = marker.encodedBytes;
359
+ }
360
+ throw new Error("Trace compaction marker size did not converge");
361
+ }
362
+ function compactTraceJournal(path, window, parsed, appended) {
363
+ const markerId = randomUUID();
364
+ const markerTs = new Date().toISOString();
365
+ const reserveBytes = markerReserveBytes(markerId, markerTs);
366
+ const selection = selectNewestTraceSuffix(parsed.candidates, {
367
+ bytes: appended.encodedBytes + reserveBytes,
368
+ events: 2,
369
+ });
370
+ const marker = createCompactionMarker(markerId, markerTs, parsed.previousCompaction, {
371
+ dropped_valid_events: parsed.droppedValidEvents + selection.droppedEvents,
372
+ dropped_malformed_lines: parsed.malformedLines,
373
+ dropped_bytes: parsed.droppedBytes + parsed.candidateBytes - selection.retainedBytes,
374
+ dropped_event_count_exact: window.omittedPrefixBytes === 0,
375
+ retained_events: selection.retainedEvents + 2,
376
+ }, selection.retainedBytes + appended.encodedBytes);
377
+ if (marker.encodedBytes > reserveBytes) {
378
+ throw new Error("Trace compaction marker exceeded its reserved bound");
379
+ }
380
+ const content = [
381
+ ...selection.retained.map(({ value }) => value.encoded),
382
+ appended.encoded,
383
+ marker.encoded,
384
+ ].join("");
385
+ const bytes = Buffer.byteLength(content);
386
+ const events = selection.retainedEvents + 2;
387
+ if (bytes > Limits.TRACE_JOURNAL_TARGET_BYTES ||
388
+ events > Limits.TRACE_JOURNAL_TARGET_EVENTS ||
389
+ bytes > Limits.TRACE_JOURNAL_MAX_BYTES ||
390
+ events > Limits.TRACE_JOURNAL_MAX_EVENTS) {
391
+ throw new Error("Trace compaction could not satisfy journal bounds");
392
+ }
393
+ writeTextAtomic(path, content);
394
+ }
395
+ export function appendRunTraceEvent(stateDir, input) {
396
+ const appended = createTraceEvent(input);
397
+ const path = runTraceFile(stateDir);
398
+ return withFileMutationLock(path, () => {
399
+ const window = readTraceWindow(path);
400
+ const parsed = parseTraceJournal(window);
401
+ const completeJournal = window.omittedPrefixBytes === 0 && parsed.malformedLines === 0;
402
+ if (completeJournal &&
403
+ traceAppendFits({ bytes: window.fileBytes, events: parsed.physicalEvents }, appended.encodedBytes)) {
404
+ writeFileSync(path, appended.encoded, { flag: "a" });
405
+ }
406
+ else {
407
+ compactTraceJournal(path, window, parsed, appended);
408
+ }
409
+ return appended.event;
410
+ });
411
+ }
412
+ export function readRunTraceJournal(stateDir) {
413
+ const path = runTraceFile(stateDir);
414
+ let window;
415
+ try {
416
+ window = readTraceWindow(path);
417
+ }
418
+ catch (error) {
419
+ return {
420
+ diagnostics: [{
421
+ message: error instanceof Error ? error.message : String(error),
422
+ path,
423
+ }],
424
+ events: [],
425
+ fileBytes: 0,
426
+ omittedPrefixBytes: 0,
427
+ readBytes: 0,
428
+ };
429
+ }
430
+ const diagnostics = [];
431
+ let events = [];
432
+ let eventHead = 0;
433
+ let ordinal = window.leadingPartialLines;
434
+ const diagnose = (message, line) => {
435
+ diagnostics.push({ message, path, ...(line === undefined ? {} : { line }) });
436
+ if (diagnostics.length > Limits.TRACE_EVENT_MAX_READ)
437
+ diagnostics.shift();
438
+ };
439
+ const retain = (event) => {
440
+ events.push({ event, ordinal });
441
+ if (events.length - eventHead > Limits.TRACE_JOURNAL_MAX_EVENTS)
442
+ eventHead += 1;
443
+ if (eventHead > Limits.TRACE_JOURNAL_MAX_EVENTS) {
444
+ events = events.slice(eventHead);
445
+ eventHead = 0;
446
+ }
447
+ };
448
+ if (window.leadingPartialLines) {
449
+ diagnose("Omitted partial leading Trace line", 1);
450
+ }
451
+ let start = 0;
452
+ while (start < window.content.length) {
453
+ ordinal += 1;
454
+ const newline = window.content.indexOf(0x0a, start);
455
+ if (newline < 0) {
456
+ diagnose("Incomplete trailing Trace line", ordinal);
457
+ break;
458
+ }
459
+ try {
460
+ retain(decodeTraceLine(window.content.subarray(start, newline)).event);
461
+ }
462
+ catch (error) {
463
+ diagnose(error instanceof Error ? error.message : String(error), ordinal);
464
+ }
465
+ start = newline + 1;
466
+ }
467
+ return {
468
+ diagnostics,
469
+ events: events.slice(eventHead),
470
+ fileBytes: window.fileBytes,
471
+ omittedPrefixBytes: window.omittedPrefixBytes,
472
+ readBytes: window.readBytes,
473
+ };
474
+ }
475
+ export function summarizeRunTraceJournal(read) {
476
+ const marker = [...read.events].reverse().map(({ event }) => markerData(event)).find(Boolean);
477
+ const incomplete = Boolean(marker) || read.omittedPrefixBytes > 0;
478
+ return {
479
+ history_complete: !incomplete, compacted: Boolean(marker),
480
+ compactions_total: marker?.compactions_total ?? 0,
481
+ dropped_events: marker?.dropped_valid_events_total ?? 0,
482
+ dropped_bytes: marker?.dropped_bytes_total ?? read.omittedPrefixBytes,
483
+ dropped_event_count_exact: marker?.dropped_event_count_exact ?? !incomplete,
484
+ retained_events: read.events.length, retained_bytes: read.readBytes,
485
+ };
94
486
  }
95
487
  export function readRunTraceEvents(stateDir, limit = Limits.TRACE_EVENT_MAX_READ) {
96
- const records = readJsonlFileResilient(runTraceFile(stateDir)).records;
97
- return records.slice(-Math.max(1, Math.min(limit, Limits.TRACE_EVENT_MAX_READ)));
488
+ return readRunTraceJournal(stateDir).events
489
+ .slice(-Math.max(1, Math.min(limit, Limits.TRACE_EVENT_MAX_READ)))
490
+ .map(({ event }) => event);
98
491
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Immutable runtime package identity.
3
+ * Zones: source/installed package version resolution and Run-state schema marker
4
+ * Owns package-local identity reads; runtime status and Run persistence remain in their domains.
5
+ */
6
+ export declare const RUN_STATE_SCHEMA: "run-kernel-v1";
7
+ export declare function getPackageVersion(): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Immutable runtime package identity.
3
+ * Zones: source/installed package version resolution and Run-state schema marker
4
+ * Owns package-local identity reads; runtime status and Run persistence remain in their domains.
5
+ */
6
+ import { readFileSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ export const RUN_STATE_SCHEMA = "run-kernel-v1";
10
+ let cachedVersion;
11
+ export function getPackageVersion() {
12
+ if (cachedVersion)
13
+ return cachedVersion;
14
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
15
+ for (const path of [
16
+ join(moduleDir, "..", "package.json"),
17
+ join(moduleDir, "..", "..", "package.json"),
18
+ ]) {
19
+ try {
20
+ const pkg = JSON.parse(readFileSync(path, "utf8"));
21
+ if (pkg.name === "@llblab/pi-actors" &&
22
+ typeof pkg.version === "string" &&
23
+ pkg.version.length > 0 &&
24
+ pkg.version.length <= 128 &&
25
+ pkg.version.trim() === pkg.version) {
26
+ cachedVersion = pkg.version;
27
+ return cachedVersion;
28
+ }
29
+ }
30
+ catch {
31
+ // Source and installed-dist modes resolve through different package ancestors.
32
+ }
33
+ }
34
+ throw new Error("pi-actors package identity is unavailable");
35
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pure runtime Control triage policy.
3
+ * Zones: pending admission, status-age classification, terminal/replacement staleness
4
+ * Owns Control triage decisions; journal reads and owner filtering stay in adapters, while safe fields come from control-projection.
5
+ */
6
+ import * as ControlProjection from "./control-projection.ts";
7
+ export declare const RUNTIME_CONTROL_STALE_AFTER_MS: number;
8
+ export interface RuntimeTriageRun {
9
+ run: string;
10
+ runInstanceId?: string;
11
+ status: string;
12
+ }
13
+ export interface RuntimePendingControl extends ControlProjection.ProjectedRunControl {
14
+ age_ms: number;
15
+ reason: "age_threshold_reached" | "run_generation_replaced" | "run_generation_unavailable" | "run_terminal" | "within_age_threshold";
16
+ run: string;
17
+ status: "queued" | "delivered" | "claimed";
18
+ status_at: string;
19
+ }
20
+ export interface RuntimeControlDiagnostic {
21
+ reason: "invalid_action" | "invalid_control_id" | "invalid_generation" | "invalid_record" | "invalid_status" | "invalid_status_timestamp";
22
+ run: string;
23
+ }
24
+ export interface RuntimeControlClassification {
25
+ diagnostic?: RuntimeControlDiagnostic;
26
+ pending?: RuntimePendingControl;
27
+ stale?: RuntimePendingControl;
28
+ }
29
+ export declare function classifyRuntimeControl(run: RuntimeTriageRun, value: unknown, nowMs: number): RuntimeControlClassification;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pure runtime Control triage policy.
3
+ * Zones: pending admission, status-age classification, terminal/replacement staleness
4
+ * Owns Control triage decisions; journal reads and owner filtering stay in adapters, while safe fields come from control-projection.
5
+ */
6
+ import * as ControlProjection from "./control-projection.js";
7
+ import { classifyRunControlRecord, runControlStatusTimestamp, } from "./run-evidence-policy.js";
8
+ export const RUNTIME_CONTROL_STALE_AFTER_MS = 5 * 60 * 1_000;
9
+ function diagnostic(run, reason) {
10
+ return { diagnostic: { reason, run } };
11
+ }
12
+ export function classifyRuntimeControl(run, value, nowMs) {
13
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
14
+ return diagnostic(run.run, "invalid_record");
15
+ }
16
+ const record = value;
17
+ const status = record.status;
18
+ const recordClass = classifyRunControlRecord(record);
19
+ if (!recordClass)
20
+ return diagnostic(run.run, "invalid_status");
21
+ if (typeof record.id !== "string" || !record.id) {
22
+ return diagnostic(run.run, "invalid_control_id");
23
+ }
24
+ if (typeof record.action !== "string" || !record.action) {
25
+ return diagnostic(run.run, "invalid_action");
26
+ }
27
+ if (typeof record.run_instance_id !== "string" || !record.run_instance_id) {
28
+ return diagnostic(run.run, "invalid_generation");
29
+ }
30
+ const timestamp = runControlStatusTimestamp(record, status);
31
+ const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
32
+ if (!Number.isFinite(timestampMs)) {
33
+ return diagnostic(run.run, "invalid_status_timestamp");
34
+ }
35
+ if (recordClass === "terminal")
36
+ return {};
37
+ const ageMs = Math.max(0, Math.floor(nowMs - timestampMs));
38
+ const staleReason = !run.runInstanceId
39
+ ? "run_generation_unavailable"
40
+ : record.run_instance_id !== run.runInstanceId
41
+ ? "run_generation_replaced"
42
+ : run.status !== "running"
43
+ ? "run_terminal"
44
+ : ageMs >= RUNTIME_CONTROL_STALE_AFTER_MS
45
+ ? "age_threshold_reached"
46
+ : undefined;
47
+ const projected = ControlProjection.projectRunControl(record);
48
+ const pending = {
49
+ ...projected,
50
+ age_ms: ageMs,
51
+ reason: staleReason ?? "within_age_threshold",
52
+ run: run.run,
53
+ status: status,
54
+ status_at: runControlStatusTimestamp(projected, status),
55
+ };
56
+ return {
57
+ pending,
58
+ ...(staleReason ? { stale: pending } : {}),
59
+ };
60
+ }
@@ -264,7 +264,7 @@ export function applyApprovedToolReviewAtSessionBoundary(options = {}) {
264
264
  ...state,
265
265
  failedStage: "approval_state",
266
266
  lastError: "approved review path is unavailable",
267
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
267
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
268
268
  phase: "processing_failed",
269
269
  updatedAt: (options.now ?? (() => new Date()))().toISOString(),
270
270
  };
@@ -287,7 +287,7 @@ export function applyApprovedToolReviewAtSessionBoundary(options = {}) {
287
287
  ...state,
288
288
  failedStage: "transaction",
289
289
  lastError: `tool review transaction remained ${transaction.phase}`,
290
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
290
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
291
291
  phase: "processing_failed",
292
292
  updatedAt: (options.now ?? (() => new Date()))().toISOString(),
293
293
  };
@@ -316,7 +316,7 @@ export function applyApprovedToolReviewAtSessionBoundary(options = {}) {
316
316
  ...pending,
317
317
  failedStage: "lineage",
318
318
  lastError: error instanceof Error ? error.message : String(error),
319
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
319
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
320
320
  };
321
321
  writeJsonAtomic(statePath, failedPending);
322
322
  return { outcome: "lineage_pending", state: failedPending };
@@ -337,7 +337,7 @@ export function applyApprovedToolReviewAtSessionBoundary(options = {}) {
337
337
  ...state,
338
338
  failedStage: "transaction",
339
339
  lastError: error instanceof Error ? error.message : String(error),
340
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
340
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
341
341
  phase: "processing_failed",
342
342
  updatedAt: (options.now ?? (() => new Date()))().toISOString(),
343
343
  };
@@ -401,7 +401,7 @@ export function createToolReviewScheduler(deps) {
401
401
  ...existing,
402
402
  failedStage: processed.stage ?? "review_result",
403
403
  lastError: processed.error ?? "automatic tool reviewer failed",
404
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
404
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
405
405
  phase: processed.outcome === "processing_failed"
406
406
  ? "processing_failed"
407
407
  : "failed",
@@ -437,7 +437,7 @@ export function createToolReviewScheduler(deps) {
437
437
  attempts: existing.attempts + 1,
438
438
  failedStage: "review_launch",
439
439
  lastError: error instanceof Error ? error.message : String(error),
440
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
440
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
441
441
  phase: "failed",
442
442
  updatedAt: now().toISOString(),
443
443
  });
@@ -471,7 +471,7 @@ export function createToolReviewScheduler(deps) {
471
471
  failedStage: "review_launch",
472
472
  inputPath: batch.inputPath,
473
473
  lastError: error instanceof Error ? error.message : String(error),
474
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
474
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
475
475
  phase: "failed",
476
476
  reviewerInputPath: batch.reviewerInputPath,
477
477
  reviewId: batch.reviewId,