@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
package/lib/runs-trace.ts CHANGED
@@ -1,15 +1,32 @@
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
 
7
7
  import { randomUUID } from "node:crypto";
8
- import { writeFileSync } from "node:fs";
8
+ import {
9
+ closeSync,
10
+ fstatSync,
11
+ openSync,
12
+ readSync,
13
+ writeFileSync,
14
+ } from "node:fs";
9
15
  import { join } from "node:path";
10
16
 
17
+ import { withFileMutationLock, writeTextAtomic } from "./file-state.ts";
11
18
  import * as Limits from "./limits.ts";
12
- import { readJsonlFileResilient } from "./state-readers.ts";
19
+ import {
20
+ accumulateTraceCompactionStatistics,
21
+ selectNewestTraceSuffix,
22
+ traceAppendFits,
23
+ traceCompactionMarkerInput,
24
+ TRACE_COMPACTION_KIND,
25
+ TRACE_COMPACTION_VERSION,
26
+ type TraceCompactionData,
27
+ type TraceRetentionCandidate,
28
+ } from "./run-evidence-policy.ts";
29
+ import type { StateReadDiagnostic } from "./state-readers.ts";
13
30
 
14
31
  export interface TraceEvent {
15
32
  id: string;
@@ -29,7 +46,56 @@ export interface AppendTraceEventInput {
29
46
  attention?: TraceEvent["attention"];
30
47
  }
31
48
 
49
+ interface EncodedTraceEvent {
50
+ encoded: string;
51
+ encodedBytes: number;
52
+ event: TraceEvent;
53
+ }
54
+
55
+ interface TraceReadWindow {
56
+ content: Buffer;
57
+ fileBytes: number;
58
+ leadingPartialLines: number;
59
+ omittedPrefixBytes: number;
60
+ readBytes: number;
61
+ }
62
+
63
+ export interface RunTraceReadEvent {
64
+ event: TraceEvent;
65
+ ordinal: number;
66
+ }
67
+ export interface RunTraceJournalRead {
68
+ diagnostics: StateReadDiagnostic[];
69
+ events: RunTraceReadEvent[];
70
+ fileBytes: number;
71
+ omittedPrefixBytes: number;
72
+ readBytes: number;
73
+ }
74
+ export interface RunTraceSummary {
75
+ compacted: boolean; compactions_total: number; dropped_bytes: number;
76
+ dropped_event_count_exact: boolean; dropped_events: number;
77
+ history_complete: boolean; retained_bytes: number; retained_events: number;
78
+ }
79
+ interface ParsedTraceJournal {
80
+ candidateBytes: number;
81
+ candidates: TraceRetentionCandidate<EncodedTraceEvent>[];
82
+ droppedBytes: number;
83
+ droppedValidEvents: number;
84
+ malformedLines: number;
85
+ physicalEvents: number;
86
+ previousCompaction?: TraceCompactionData;
87
+ }
88
+
32
89
  const KIND_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)*$/;
90
+ const TRACE_FIELDS = new Set([
91
+ "attention",
92
+ "data",
93
+ "id",
94
+ "kind",
95
+ "level",
96
+ "summary",
97
+ "ts",
98
+ ]);
33
99
  const TRACE_INPUT_FIELDS = new Set([
34
100
  "attention",
35
101
  "data",
@@ -46,11 +112,28 @@ const FORBIDDEN_FIELDS = new Set([
46
112
  "to",
47
113
  "type",
48
114
  ]);
115
+ const COMPACTION_DATA_FIELDS = new Set([
116
+ "compactions_total",
117
+ "dropped_bytes_total",
118
+ "dropped_event_count_exact",
119
+ "dropped_malformed_lines_total",
120
+ "dropped_valid_events_total",
121
+ "history_complete",
122
+ "retained_bytes",
123
+ "retained_events",
124
+ "version",
125
+ ]);
126
+ const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
49
127
 
50
128
  export function runTraceFile(stateDir: string): string {
51
129
  return join(stateDir, "trace.jsonl");
52
130
  }
53
131
 
132
+ function hasExactFields(record: Record<string, unknown>, fields: Set<string>): boolean {
133
+ const keys = Object.keys(record);
134
+ return keys.length === fields.size && keys.every((key) => fields.has(key));
135
+ }
136
+
54
137
  function normalizeTraceEventInput(input: unknown): AppendTraceEventInput {
55
138
  if (!input || typeof input !== "object" || Array.isArray(input)) {
56
139
  throw new Error("Trace event must be an object");
@@ -67,6 +150,9 @@ function normalizeTraceEventInput(input: unknown): AppendTraceEventInput {
67
150
  if (typeof record.kind !== "string" || !KIND_PATTERN.test(record.kind)) {
68
151
  throw new Error("Trace event kind must be a lowercase semantic token");
69
152
  }
153
+ if (record.kind === TRACE_COMPACTION_KIND) {
154
+ throw new Error(`Trace event kind is runtime-reserved: ${TRACE_COMPACTION_KIND}`);
155
+ }
70
156
  if (record.summary !== undefined && typeof record.summary !== "string") {
71
157
  throw new Error("Trace event summary must be a string");
72
158
  }
@@ -100,33 +186,426 @@ function normalizeTraceEventInput(input: unknown): AppendTraceEventInput {
100
186
  };
101
187
  }
102
188
 
189
+ function encodeTraceEvent(event: TraceEvent): EncodedTraceEvent {
190
+ let serialized: string;
191
+ try {
192
+ serialized = JSON.stringify(event);
193
+ } catch {
194
+ throw new Error("Trace event data must be JSON-serializable");
195
+ }
196
+ if (Buffer.byteLength(serialized) > Limits.TRACE_EVENT_MAX_BYTES) {
197
+ throw new Error(`Trace event exceeds ${Limits.TRACE_EVENT_MAX_BYTES} bytes`);
198
+ }
199
+ const encoded = `${serialized}\n`;
200
+ return { encoded, encodedBytes: Buffer.byteLength(encoded), event };
201
+ }
202
+
203
+ function createTraceEvent(input: AppendTraceEventInput): EncodedTraceEvent {
204
+ return encodeTraceEvent({
205
+ id: randomUUID(),
206
+ ts: new Date().toISOString(),
207
+ ...normalizeTraceEventInput(input),
208
+ });
209
+ }
210
+
211
+ function readTraceWindow(path: string): TraceReadWindow {
212
+ let fd: number;
213
+ try {
214
+ fd = openSync(path, "r");
215
+ } catch (error) {
216
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
217
+ return {
218
+ content: Buffer.alloc(0),
219
+ fileBytes: 0,
220
+ leadingPartialLines: 0,
221
+ omittedPrefixBytes: 0,
222
+ readBytes: 0,
223
+ };
224
+ }
225
+ throw error;
226
+ }
227
+ try {
228
+ const fileBytes = fstatSync(fd).size;
229
+ if (!Number.isSafeInteger(fileBytes) || fileBytes < 0) {
230
+ throw new Error("Trace journal size is not safely representable");
231
+ }
232
+ const readBytes = Math.min(fileBytes, Limits.TRACE_JOURNAL_MAX_BYTES);
233
+ const offset = fileBytes - readBytes;
234
+ const buffer = Buffer.allocUnsafe(readBytes);
235
+ let consumed = 0;
236
+ while (consumed < readBytes) {
237
+ const count = readSync(fd, buffer, consumed, readBytes - consumed, offset + consumed);
238
+ if (count === 0) throw new Error("Trace journal changed during bounded read");
239
+ consumed += count;
240
+ }
241
+ if (fstatSync(fd).size !== fileBytes) {
242
+ throw new Error("Trace journal changed during bounded read");
243
+ }
244
+ if (offset === 0) {
245
+ return {
246
+ content: buffer,
247
+ fileBytes,
248
+ leadingPartialLines: 0,
249
+ omittedPrefixBytes: 0,
250
+ readBytes,
251
+ };
252
+ }
253
+ const preceding = Buffer.allocUnsafe(1);
254
+ if (readSync(fd, preceding, 0, 1, offset - 1) !== 1) {
255
+ throw new Error("Trace journal changed during bounded read");
256
+ }
257
+ if (preceding[0] === 0x0a) {
258
+ return {
259
+ content: buffer,
260
+ fileBytes,
261
+ leadingPartialLines: 0,
262
+ omittedPrefixBytes: offset,
263
+ readBytes,
264
+ };
265
+ }
266
+ const newline = buffer.indexOf(0x0a);
267
+ const discarded = newline < 0 ? buffer.length : newline + 1;
268
+ return {
269
+ content: buffer.subarray(discarded),
270
+ fileBytes,
271
+ leadingPartialLines: 1,
272
+ omittedPrefixBytes: offset + discarded,
273
+ readBytes,
274
+ };
275
+ } finally {
276
+ closeSync(fd);
277
+ }
278
+ }
279
+
280
+ function isTraceEvent(value: unknown): value is TraceEvent {
281
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
282
+ const record = value as Record<string, unknown>;
283
+ if (Object.keys(record).some((key) => !TRACE_FIELDS.has(key))) return false;
284
+ if (
285
+ typeof record.id !== "string" || !record.id ||
286
+ typeof record.ts !== "string" || !Number.isFinite(Date.parse(record.ts)) ||
287
+ typeof record.kind !== "string" || !KIND_PATTERN.test(record.kind)
288
+ ) return false;
289
+ if (record.summary !== undefined && typeof record.summary !== "string") return false;
290
+ if (
291
+ record.level !== undefined &&
292
+ record.level !== "info" &&
293
+ record.level !== "warning" &&
294
+ record.level !== "error"
295
+ ) return false;
296
+ return record.attention === undefined ||
297
+ record.attention === "notify" ||
298
+ record.attention === "followup";
299
+ }
300
+
301
+ function isCount(value: unknown): value is number {
302
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
303
+ }
304
+
305
+ function compactionData(value: unknown): TraceCompactionData | undefined {
306
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
307
+ const record = value as Record<string, unknown>;
308
+ if (!hasExactFields(record, COMPACTION_DATA_FIELDS)) return undefined;
309
+ if (
310
+ record.version !== TRACE_COMPACTION_VERSION ||
311
+ !isCount(record.compactions_total) ||
312
+ !isCount(record.dropped_valid_events_total) ||
313
+ !isCount(record.dropped_malformed_lines_total) ||
314
+ !isCount(record.dropped_bytes_total) ||
315
+ typeof record.dropped_event_count_exact !== "boolean" ||
316
+ !isCount(record.retained_events) ||
317
+ !isCount(record.retained_bytes) ||
318
+ record.history_complete !== false
319
+ ) return undefined;
320
+ return record as unknown as TraceCompactionData;
321
+ }
322
+
323
+ function markerData(event: TraceEvent): TraceCompactionData | undefined {
324
+ return event.kind === TRACE_COMPACTION_KIND &&
325
+ event.level === "warning" &&
326
+ event.summary === undefined &&
327
+ event.attention === undefined
328
+ ? compactionData(event.data)
329
+ : undefined;
330
+ }
331
+
332
+ function decodeTraceLine(line: Buffer): { event: TraceEvent; text: string } {
333
+ if (line.length === 0 || line.length > Limits.TRACE_EVENT_MAX_BYTES) {
334
+ throw new Error("Trace journal line is empty or oversized");
335
+ }
336
+ const text = UTF8_DECODER.decode(line);
337
+ const event = JSON.parse(text) as unknown;
338
+ if (!isTraceEvent(event) ||
339
+ (event.kind === TRACE_COMPACTION_KIND && !markerData(event))) {
340
+ throw new Error("Trace journal line is not a canonical event");
341
+ }
342
+ return { event, text };
343
+ }
344
+
345
+ function parseTraceJournal(window: TraceReadWindow): ParsedTraceJournal {
346
+ let candidates: TraceRetentionCandidate<EncodedTraceEvent>[] = [];
347
+ let candidateHead = 0;
348
+ let candidateBytes = 0;
349
+ let droppedBytes = window.omittedPrefixBytes;
350
+ let droppedValidEvents = 0;
351
+ let malformedLines = window.leadingPartialLines;
352
+ let physicalEvents = 0;
353
+ let previousCompaction: TraceCompactionData | undefined;
354
+ const dropOldestCandidate = (): void => {
355
+ const dropped = candidates[candidateHead++]!;
356
+ candidateBytes -= dropped.encodedBytes;
357
+ droppedBytes += dropped.encodedBytes;
358
+ droppedValidEvents += 1;
359
+ if (candidateHead > 2_048) {
360
+ candidates = candidates.slice(candidateHead);
361
+ candidateHead = 0;
362
+ }
363
+ };
364
+ const consume = (line: Buffer, physicalBytes: number): void => {
365
+ let decoded: ReturnType<typeof decodeTraceLine>;
366
+ try {
367
+ decoded = decodeTraceLine(line);
368
+ } catch {
369
+ malformedLines += 1;
370
+ droppedBytes += physicalBytes;
371
+ return;
372
+ }
373
+ const { event, text } = decoded;
374
+ physicalEvents += 1;
375
+ if (event.kind === TRACE_COMPACTION_KIND) {
376
+ previousCompaction = markerData(event)!;
377
+ return;
378
+ }
379
+ const encoded = `${text}\n`;
380
+ const candidate: TraceRetentionCandidate<EncodedTraceEvent> = {
381
+ encodedBytes: Buffer.byteLength(encoded),
382
+ value: { encoded, encodedBytes: Buffer.byteLength(encoded), event },
383
+ };
384
+ candidates.push(candidate);
385
+ candidateBytes += candidate.encodedBytes;
386
+ while (
387
+ candidates.length - candidateHead > Limits.TRACE_JOURNAL_TARGET_EVENTS ||
388
+ candidateBytes > Limits.TRACE_JOURNAL_TARGET_BYTES
389
+ ) dropOldestCandidate();
390
+ };
391
+ let start = 0;
392
+ while (start < window.content.length) {
393
+ const newline = window.content.indexOf(0x0a, start);
394
+ if (newline < 0) {
395
+ malformedLines += 1;
396
+ droppedBytes += window.content.length - start;
397
+ break;
398
+ }
399
+ consume(window.content.subarray(start, newline), newline - start + 1);
400
+ start = newline + 1;
401
+ }
402
+ candidates = candidates.slice(candidateHead);
403
+ return {
404
+ candidateBytes,
405
+ candidates,
406
+ droppedBytes,
407
+ droppedValidEvents,
408
+ malformedLines,
409
+ physicalEvents,
410
+ ...(previousCompaction ? { previousCompaction } : {}),
411
+ };
412
+ }
413
+
414
+ function markerReserveBytes(id: string, ts: string): number {
415
+ const maximum = Number.MAX_SAFE_INTEGER;
416
+ return encodeTraceEvent({
417
+ id,
418
+ ts,
419
+ ...traceCompactionMarkerInput({
420
+ version: TRACE_COMPACTION_VERSION,
421
+ compactions_total: maximum,
422
+ dropped_valid_events_total: maximum,
423
+ dropped_malformed_lines_total: maximum,
424
+ dropped_bytes_total: maximum,
425
+ dropped_event_count_exact: false,
426
+ retained_events: maximum,
427
+ retained_bytes: maximum,
428
+ history_complete: false,
429
+ }),
430
+ }).encodedBytes;
431
+ }
432
+
433
+ function createCompactionMarker(
434
+ id: string,
435
+ ts: string,
436
+ previous: TraceCompactionData | undefined,
437
+ delta: Omit<Parameters<typeof accumulateTraceCompactionStatistics>[1], "retained_bytes">,
438
+ retainedWithoutMarkerBytes: number,
439
+ ): EncodedTraceEvent {
440
+ let markerBytes = 0;
441
+ for (let attempt = 0; attempt < 8; attempt += 1) {
442
+ const data = accumulateTraceCompactionStatistics(previous, {
443
+ ...delta,
444
+ retained_bytes: retainedWithoutMarkerBytes + markerBytes,
445
+ });
446
+ const marker = encodeTraceEvent({
447
+ id,
448
+ ts,
449
+ ...traceCompactionMarkerInput(data),
450
+ });
451
+ if (marker.encodedBytes === markerBytes) return marker;
452
+ markerBytes = marker.encodedBytes;
453
+ }
454
+ throw new Error("Trace compaction marker size did not converge");
455
+ }
456
+
457
+ function compactTraceJournal(
458
+ path: string,
459
+ window: TraceReadWindow,
460
+ parsed: ParsedTraceJournal,
461
+ appended: EncodedTraceEvent,
462
+ ): void {
463
+ const markerId = randomUUID();
464
+ const markerTs = new Date().toISOString();
465
+ const reserveBytes = markerReserveBytes(markerId, markerTs);
466
+ const selection = selectNewestTraceSuffix(parsed.candidates, {
467
+ bytes: appended.encodedBytes + reserveBytes,
468
+ events: 2,
469
+ });
470
+ const marker = createCompactionMarker(
471
+ markerId,
472
+ markerTs,
473
+ parsed.previousCompaction,
474
+ {
475
+ dropped_valid_events: parsed.droppedValidEvents + selection.droppedEvents,
476
+ dropped_malformed_lines: parsed.malformedLines,
477
+ dropped_bytes:
478
+ parsed.droppedBytes + parsed.candidateBytes - selection.retainedBytes,
479
+ dropped_event_count_exact: window.omittedPrefixBytes === 0,
480
+ retained_events: selection.retainedEvents + 2,
481
+ },
482
+ selection.retainedBytes + appended.encodedBytes,
483
+ );
484
+ if (marker.encodedBytes > reserveBytes) {
485
+ throw new Error("Trace compaction marker exceeded its reserved bound");
486
+ }
487
+ const content = [
488
+ ...selection.retained.map(({ value }) => value.encoded),
489
+ appended.encoded,
490
+ marker.encoded,
491
+ ].join("");
492
+ const bytes = Buffer.byteLength(content);
493
+ const events = selection.retainedEvents + 2;
494
+ if (
495
+ bytes > Limits.TRACE_JOURNAL_TARGET_BYTES ||
496
+ events > Limits.TRACE_JOURNAL_TARGET_EVENTS ||
497
+ bytes > Limits.TRACE_JOURNAL_MAX_BYTES ||
498
+ events > Limits.TRACE_JOURNAL_MAX_EVENTS
499
+ ) {
500
+ throw new Error("Trace compaction could not satisfy journal bounds");
501
+ }
502
+ writeTextAtomic(path, content);
503
+ }
504
+
103
505
  export function appendRunTraceEvent(
104
506
  stateDir: string,
105
507
  input: AppendTraceEventInput,
106
508
  ): TraceEvent {
107
- const normalized = normalizeTraceEventInput(input);
108
- const event: TraceEvent = {
109
- id: randomUUID(),
110
- ts: new Date().toISOString(),
111
- ...normalized,
112
- };
113
- let encoded: string;
509
+ const appended = createTraceEvent(input);
510
+ const path = runTraceFile(stateDir);
511
+ return withFileMutationLock(path, () => {
512
+ const window = readTraceWindow(path);
513
+ const parsed = parseTraceJournal(window);
514
+ const completeJournal =
515
+ window.omittedPrefixBytes === 0 && parsed.malformedLines === 0;
516
+ if (
517
+ completeJournal &&
518
+ traceAppendFits(
519
+ { bytes: window.fileBytes, events: parsed.physicalEvents },
520
+ appended.encodedBytes,
521
+ )
522
+ ) {
523
+ writeFileSync(path, appended.encoded, { flag: "a" });
524
+ } else {
525
+ compactTraceJournal(path, window, parsed, appended);
526
+ }
527
+ return appended.event;
528
+ });
529
+ }
530
+
531
+ export function readRunTraceJournal(stateDir: string): RunTraceJournalRead {
532
+ const path = runTraceFile(stateDir);
533
+ let window: TraceReadWindow;
114
534
  try {
115
- encoded = JSON.stringify(event);
116
- } catch {
117
- throw new Error("Trace event data must be JSON-serializable");
535
+ window = readTraceWindow(path);
536
+ } catch (error) {
537
+ return {
538
+ diagnostics: [{
539
+ message: error instanceof Error ? error.message : String(error),
540
+ path,
541
+ }],
542
+ events: [],
543
+ fileBytes: 0,
544
+ omittedPrefixBytes: 0,
545
+ readBytes: 0,
546
+ };
118
547
  }
119
- if (Buffer.byteLength(encoded) > Limits.TRACE_EVENT_MAX_BYTES) {
120
- throw new Error(`Trace event exceeds ${Limits.TRACE_EVENT_MAX_BYTES} bytes`);
548
+ const diagnostics: StateReadDiagnostic[] = [];
549
+ let events: RunTraceReadEvent[] = [];
550
+ let eventHead = 0;
551
+ let ordinal = window.leadingPartialLines;
552
+ const diagnose = (message: string, line?: number): void => {
553
+ diagnostics.push({ message, path, ...(line === undefined ? {} : { line }) });
554
+ if (diagnostics.length > Limits.TRACE_EVENT_MAX_READ) diagnostics.shift();
555
+ };
556
+ const retain = (event: TraceEvent): void => {
557
+ events.push({ event, ordinal });
558
+ if (events.length - eventHead > Limits.TRACE_JOURNAL_MAX_EVENTS) eventHead += 1;
559
+ if (eventHead > Limits.TRACE_JOURNAL_MAX_EVENTS) {
560
+ events = events.slice(eventHead);
561
+ eventHead = 0;
562
+ }
563
+ };
564
+ if (window.leadingPartialLines) {
565
+ diagnose("Omitted partial leading Trace line", 1);
566
+ }
567
+ let start = 0;
568
+ while (start < window.content.length) {
569
+ ordinal += 1;
570
+ const newline = window.content.indexOf(0x0a, start);
571
+ if (newline < 0) {
572
+ diagnose("Incomplete trailing Trace line", ordinal);
573
+ break;
574
+ }
575
+ try {
576
+ retain(decodeTraceLine(window.content.subarray(start, newline)).event);
577
+ } catch (error) {
578
+ diagnose(error instanceof Error ? error.message : String(error), ordinal);
579
+ }
580
+ start = newline + 1;
121
581
  }
122
- writeFileSync(runTraceFile(stateDir), `${encoded}\n`, { flag: "a" });
123
- return event;
582
+ return {
583
+ diagnostics,
584
+ events: events.slice(eventHead),
585
+ fileBytes: window.fileBytes,
586
+ omittedPrefixBytes: window.omittedPrefixBytes,
587
+ readBytes: window.readBytes,
588
+ };
589
+ }
590
+
591
+ export function summarizeRunTraceJournal(read: RunTraceJournalRead): RunTraceSummary {
592
+ const marker = [...read.events].reverse().map(({ event }) => markerData(event)).find(Boolean);
593
+ const incomplete = Boolean(marker) || read.omittedPrefixBytes > 0;
594
+ return {
595
+ history_complete: !incomplete, compacted: Boolean(marker),
596
+ compactions_total: marker?.compactions_total ?? 0,
597
+ dropped_events: marker?.dropped_valid_events_total ?? 0,
598
+ dropped_bytes: marker?.dropped_bytes_total ?? read.omittedPrefixBytes,
599
+ dropped_event_count_exact: marker?.dropped_event_count_exact ?? !incomplete,
600
+ retained_events: read.events.length, retained_bytes: read.readBytes,
601
+ };
124
602
  }
125
603
 
126
604
  export function readRunTraceEvents(
127
605
  stateDir: string,
128
606
  limit = Limits.TRACE_EVENT_MAX_READ,
129
607
  ): TraceEvent[] {
130
- const records = readJsonlFileResilient<TraceEvent>(runTraceFile(stateDir)).records;
131
- return records.slice(-Math.max(1, Math.min(limit, Limits.TRACE_EVENT_MAX_READ)));
608
+ return readRunTraceJournal(stateDir).events
609
+ .slice(-Math.max(1, Math.min(limit, Limits.TRACE_EVENT_MAX_READ)))
610
+ .map(({ event }) => event);
132
611
  }
@@ -0,0 +1,39 @@
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
+
7
+ import { readFileSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ export const RUN_STATE_SCHEMA = "run-kernel-v1" as const;
12
+
13
+ let cachedVersion: string | undefined;
14
+
15
+ export function getPackageVersion(): string {
16
+ if (cachedVersion) return cachedVersion;
17
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
18
+ for (const path of [
19
+ join(moduleDir, "..", "package.json"),
20
+ join(moduleDir, "..", "..", "package.json"),
21
+ ]) {
22
+ try {
23
+ const pkg = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
24
+ if (
25
+ pkg.name === "@llblab/pi-actors" &&
26
+ typeof pkg.version === "string" &&
27
+ pkg.version.length > 0 &&
28
+ pkg.version.length <= 128 &&
29
+ pkg.version.trim() === pkg.version
30
+ ) {
31
+ cachedVersion = pkg.version;
32
+ return cachedVersion;
33
+ }
34
+ } catch {
35
+ // Source and installed-dist modes resolve through different package ancestors.
36
+ }
37
+ }
38
+ throw new Error("pi-actors package identity is unavailable");
39
+ }