@llblab/pi-actors 0.43.1 → 0.45.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 (79) hide show
  1. package/AGENTS.md +6 -6
  2. package/BACKLOG.md +1 -189
  3. package/CHANGELOG.md +419 -402
  4. package/README.md +8 -6
  5. package/dist/index.js +7 -3
  6. package/dist/lib/async-runs.d.ts +2 -0
  7. package/dist/lib/async-runs.js +41 -34
  8. package/dist/lib/config.js +1 -0
  9. package/dist/lib/file-state.d.ts +3 -1
  10. package/dist/lib/file-state.js +25 -7
  11. package/dist/lib/inspector-overlay.js +46 -30
  12. package/dist/lib/inspector.js +8 -2
  13. package/dist/lib/limits.d.ts +10 -0
  14. package/dist/lib/limits.js +10 -0
  15. package/dist/lib/observability.d.ts +4 -2
  16. package/dist/lib/observability.js +43 -36
  17. package/dist/lib/recipes-discovery.js +20 -3
  18. package/dist/lib/recipes-references.d.ts +10 -0
  19. package/dist/lib/recipes-references.js +168 -5
  20. package/dist/lib/run-evidence-policy.d.ts +95 -0
  21. package/dist/lib/run-evidence-policy.js +177 -0
  22. package/dist/lib/run-ui-runtime.js +2 -0
  23. package/dist/lib/runs-controls.d.ts +7 -5
  24. package/dist/lib/runs-controls.js +186 -49
  25. package/dist/lib/runs-retention.js +27 -14
  26. package/dist/lib/runs-trace.d.ts +25 -1
  27. package/dist/lib/runs-trace.js +410 -21
  28. package/dist/lib/runtime-triage.js +6 -22
  29. package/dist/lib/schema.d.ts +3 -0
  30. package/dist/lib/schema.js +34 -1
  31. package/dist/lib/tools-inspect.js +58 -14
  32. package/dist/lib/tools-local.js +27 -10
  33. package/dist/lib/tools-spawn.js +2 -2
  34. package/dist/lib/trace-projection.js +90 -44
  35. package/dist/scripts/conformance.mjs +5 -0
  36. package/dist/scripts/locker.mjs +31 -74
  37. package/dist/scripts/music-player.mjs +41 -129
  38. package/dist/scripts/release-gates.mjs +26 -4
  39. package/dist/skills/actors/SKILL.md +9 -8
  40. package/dist/skills/swarm/SKILL.md +2 -2
  41. package/docs/README.md +0 -1
  42. package/docs/actor-inspector.md +3 -2
  43. package/docs/async-runs.md +10 -8
  44. package/docs/command-templates.md +2 -2
  45. package/docs/recipe-library.md +4 -2
  46. package/docs/template-recipes.md +10 -4
  47. package/docs/tool-registry.md +1 -1
  48. package/index.ts +9 -3
  49. package/lib/async-runs.ts +51 -54
  50. package/lib/config.ts +4 -0
  51. package/lib/file-state.ts +28 -7
  52. package/lib/inspector-overlay.ts +29 -17
  53. package/lib/inspector.ts +16 -2
  54. package/lib/limits.ts +10 -0
  55. package/lib/observability.ts +55 -57
  56. package/lib/recipes-discovery.ts +23 -3
  57. package/lib/recipes-references.ts +227 -7
  58. package/lib/run-evidence-policy.ts +242 -0
  59. package/lib/run-ui-runtime.ts +2 -0
  60. package/lib/runs-controls.ts +177 -108
  61. package/lib/runs-retention.ts +28 -20
  62. package/lib/runs-trace.ts +496 -21
  63. package/lib/runtime-triage.ts +11 -25
  64. package/lib/schema.ts +53 -1
  65. package/lib/tools-inspect.ts +56 -15
  66. package/lib/tools-local.ts +49 -19
  67. package/lib/tools-spawn.ts +2 -2
  68. package/lib/trace-projection.ts +137 -75
  69. package/package.json +1 -1
  70. package/scripts/conformance.mjs +5 -0
  71. package/scripts/locker.mjs +31 -74
  72. package/scripts/music-player.mjs +41 -129
  73. package/scripts/release-gates.mjs +26 -4
  74. package/skills/actors/SKILL.md +9 -8
  75. package/skills/swarm/SKILL.md +2 -2
  76. package/dist/lib/runtime-notifier.d.ts +0 -48
  77. package/dist/lib/runtime-notifier.js +0 -138
  78. package/docs/0.43-baseline.md +0 -39
  79. package/lib/runtime-notifier.ts +0 -211
@@ -1,15 +1,24 @@
1
1
  /**
2
2
  * Run Trace event journal.
3
- * Zones: structured event validation, token-locked append-only writes, resilient bounded reads
3
+ * Zones: structured event validation, bounded atomic retention, resilient reads
4
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 } from "./file-state.js";
9
+ import { withFileMutationLock, writeTextAtomic } from "./file-state.js";
10
10
  import * as Limits from "./limits.js";
11
- import { readJsonlFileResilient } from "./state-readers.js";
11
+ import { accumulateTraceCompactionStatistics, selectNewestTraceSuffix, traceAppendFits, traceCompactionMarkerInput, TRACE_COMPACTION_KIND, TRACE_COMPACTION_VERSION, } from "./run-evidence-policy.js";
12
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
+ ]);
13
22
  const TRACE_INPUT_FIELDS = new Set([
14
23
  "attention",
15
24
  "data",
@@ -26,9 +35,25 @@ const FORBIDDEN_FIELDS = new Set([
26
35
  "to",
27
36
  "type",
28
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 });
29
50
  export function runTraceFile(stateDir) {
30
51
  return join(stateDir, "trace.jsonl");
31
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
+ }
32
57
  function normalizeTraceEventInput(input) {
33
58
  if (!input || typeof input !== "object" || Array.isArray(input)) {
34
59
  throw new Error("Trace event must be an object");
@@ -45,6 +70,9 @@ function normalizeTraceEventInput(input) {
45
70
  if (typeof record.kind !== "string" || !KIND_PATTERN.test(record.kind)) {
46
71
  throw new Error("Trace event kind must be a lowercase semantic token");
47
72
  }
73
+ if (record.kind === TRACE_COMPACTION_KIND) {
74
+ throw new Error(`Trace event kind is runtime-reserved: ${TRACE_COMPACTION_KIND}`);
75
+ }
48
76
  if (record.summary !== undefined && typeof record.summary !== "string") {
49
77
  throw new Error("Trace event summary must be a string");
50
78
  }
@@ -73,30 +101,391 @@ function normalizeTraceEventInput(input) {
73
101
  : {}),
74
102
  };
75
103
  }
76
- export function appendRunTraceEvent(stateDir, input) {
77
- const path = runTraceFile(stateDir);
78
- return withFileMutationLock(path, () => {
79
- const normalized = normalizeTraceEventInput(input);
80
- const event = {
81
- id: randomUUID(),
82
- ts: new Date().toISOString(),
83
- ...normalized,
104
+ function encodeTraceEvent(event) {
105
+ let serialized;
106
+ try {
107
+ serialized = JSON.stringify(event);
108
+ }
109
+ catch {
110
+ throw new Error("Trace event data must be JSON-serializable");
111
+ }
112
+ if (Buffer.byteLength(serialized) > Limits.TRACE_EVENT_MAX_BYTES) {
113
+ throw new Error(`Trace event exceeds ${Limits.TRACE_EVENT_MAX_BYTES} bytes`);
114
+ }
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,
84
190
  };
85
- let encoded;
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;
86
279
  try {
87
- encoded = JSON.stringify(event);
280
+ decoded = decodeTraceLine(line);
88
281
  }
89
282
  catch {
90
- throw new Error("Trace event data must be JSON-serializable");
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" });
91
405
  }
92
- if (Buffer.byteLength(encoded) > Limits.TRACE_EVENT_MAX_BYTES) {
93
- throw new Error(`Trace event exceeds ${Limits.TRACE_EVENT_MAX_BYTES} bytes`);
406
+ else {
407
+ compactTraceJournal(path, window, parsed, appended);
94
408
  }
95
- writeFileSync(path, `${encoded}\n`, { flag: "a" });
96
- return event;
409
+ return appended.event;
97
410
  });
98
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
+ };
486
+ }
99
487
  export function readRunTraceEvents(stateDir, limit = Limits.TRACE_EVENT_MAX_READ) {
100
- const records = readJsonlFileResilient(runTraceFile(stateDir)).records;
101
- 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);
102
491
  }
@@ -4,36 +4,20 @@
4
4
  * Owns Control triage decisions; journal reads and owner filtering stay in adapters, while safe fields come from control-projection.
5
5
  */
6
6
  import * as ControlProjection from "./control-projection.js";
7
+ import { classifyRunControlRecord, runControlStatusTimestamp, } from "./run-evidence-policy.js";
7
8
  export const RUNTIME_CONTROL_STALE_AFTER_MS = 5 * 60 * 1_000;
8
- const PENDING_STATUSES = new Set([
9
- "queued",
10
- "delivered",
11
- "claimed",
12
- ]);
13
- const TERMINAL_STATUSES = new Set(["handled", "failed"]);
14
9
  function diagnostic(run, reason) {
15
10
  return { diagnostic: { reason, run } };
16
11
  }
17
- function statusTimestamp(record, status) {
18
- if (status === "failed")
19
- return record.failed_at;
20
- if (status === "handled")
21
- return record.handled_at;
22
- if (status === "claimed")
23
- return record.claimed_at;
24
- if (status === "delivered")
25
- return record.delivered_at;
26
- return record.queued_at;
27
- }
28
12
  export function classifyRuntimeControl(run, value, nowMs) {
29
13
  if (!value || typeof value !== "object" || Array.isArray(value)) {
30
14
  return diagnostic(run.run, "invalid_record");
31
15
  }
32
16
  const record = value;
33
17
  const status = record.status;
34
- if (!PENDING_STATUSES.has(status) && !TERMINAL_STATUSES.has(status)) {
18
+ const recordClass = classifyRunControlRecord(record);
19
+ if (!recordClass)
35
20
  return diagnostic(run.run, "invalid_status");
36
- }
37
21
  if (typeof record.id !== "string" || !record.id) {
38
22
  return diagnostic(run.run, "invalid_control_id");
39
23
  }
@@ -43,12 +27,12 @@ export function classifyRuntimeControl(run, value, nowMs) {
43
27
  if (typeof record.run_instance_id !== "string" || !record.run_instance_id) {
44
28
  return diagnostic(run.run, "invalid_generation");
45
29
  }
46
- const timestamp = statusTimestamp(record, status);
30
+ const timestamp = runControlStatusTimestamp(record, status);
47
31
  const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
48
32
  if (!Number.isFinite(timestampMs)) {
49
33
  return diagnostic(run.run, "invalid_status_timestamp");
50
34
  }
51
- if (TERMINAL_STATUSES.has(status))
35
+ if (recordClass === "terminal")
52
36
  return {};
53
37
  const ageMs = Math.max(0, Math.floor(nowMs - timestampMs));
54
38
  const staleReason = !run.runInstanceId
@@ -67,7 +51,7 @@ export function classifyRuntimeControl(run, value, nowMs) {
67
51
  reason: staleReason ?? "within_age_threshold",
68
52
  run: run.run,
69
53
  status: status,
70
- status_at: statusTimestamp(projected, status),
54
+ status_at: runControlStatusTimestamp(projected, status),
71
55
  };
72
56
  return {
73
57
  pending,
@@ -44,14 +44,17 @@ export declare function objectSchema(properties: Record<string, JsonSchema>, req
44
44
  export declare function looseObjectSchema(description: string): JsonSchema;
45
45
  export declare function parseToolArgToken(value: string): ParsedToolArgToken;
46
46
  export declare function parseToolArgDeclarations(value: string): ToolArgSpec;
47
+ export declare function parseToolArgDeclarationList(value: unknown[]): ToolArgSpec;
47
48
  export declare function normalizeStoredToolArgDeclarations(argsValue: unknown, defaultsValue: unknown): ToolArgSpec & {
48
49
  changed: boolean;
49
50
  provided: boolean;
50
51
  };
51
52
  export declare function formatToolArgs(args: string[]): string;
52
53
  export declare function getTemplatePlaceholderNames(config: CommandTemplates.CommandTemplateConfig): string[];
54
+ export declare function assertCompatibleToolArgTypes(...sources: Array<Record<string, ToolArgType> | undefined>): void;
53
55
  export declare function getTemplateArgTypes(config: CommandTemplates.CommandTemplateConfig): Record<string, ToolArgType>;
54
56
  export declare function getExplicitToolArgNames(args: string[] | undefined): string[];
57
+ export declare function getExplicitToolArgDefaults(args: string[] | string | undefined): Record<string, string>;
55
58
  export declare function getToolArgNames(config: CommandTemplates.CommandTemplateConfig): string[];
56
59
  export declare function getRequiredToolArgNames(config: CommandTemplates.CommandTemplateConfig): Set<string>;
57
60
  export declare function normalizeRuntimeValues(values: Record<string, unknown>, argTypes: Record<string, ToolArgType> | undefined): Record<string, unknown>;
@@ -147,7 +147,10 @@ function isValidDefault(type, value) {
147
147
  }
148
148
  }
149
149
  export function parseToolArgDeclarations(value) {
150
- const source = splitArgDeclarations(value);
150
+ return parseToolArgDeclarationList(splitArgDeclarations(value));
151
+ }
152
+ export function parseToolArgDeclarationList(value) {
153
+ const source = value.map(String);
151
154
  const seen = new Set();
152
155
  const duplicates = new Set();
153
156
  const args = [];
@@ -272,6 +275,8 @@ function collectCommandTemplateConfigDeclarations(config, declarations) {
272
275
  collectTemplatePlaceholderDeclarations(config, declarations);
273
276
  return;
274
277
  }
278
+ if (!config || typeof config !== "object")
279
+ return;
275
280
  if (Array.isArray(config)) {
276
281
  for (const step of config)
277
282
  collectCommandTemplateConfigDeclarations(step, declarations);
@@ -296,9 +301,30 @@ function getTemplatePlaceholderDeclarations(config) {
296
301
  export function getTemplatePlaceholderNames(config) {
297
302
  return mergeUnique(getTemplatePlaceholderDeclarations(config).map((item) => item.arg));
298
303
  }
304
+ function toolArgTypesEqual(left, right) {
305
+ return JSON.stringify(left) === JSON.stringify(right);
306
+ }
307
+ export function assertCompatibleToolArgTypes(...sources) {
308
+ const seen = {};
309
+ for (const source of sources) {
310
+ for (const [name, type] of Object.entries(source ?? {})) {
311
+ const previous = seen[name];
312
+ if (previous && !toolArgTypesEqual(previous, type)) {
313
+ throw new Error(`Conflicting argument type for ${name}: ${canonicalArgDeclaration(name, previous)} vs ${canonicalArgDeclaration(name, type)}`);
314
+ }
315
+ seen[name] = type;
316
+ }
317
+ }
318
+ }
299
319
  export function getTemplateArgTypes(config) {
300
320
  const argTypes = {};
301
321
  for (const declaration of getTemplatePlaceholderDeclarations(config)) {
322
+ const previous = argTypes[declaration.arg];
323
+ if (previous &&
324
+ declaration.type.kind !== "string" &&
325
+ !toolArgTypesEqual(previous, declaration.type)) {
326
+ throw new Error(`Conflicting argument type for ${declaration.arg}: ${canonicalArgDeclaration(declaration.arg, previous)} vs ${declaration.declaration}`);
327
+ }
302
328
  if (declaration.type.kind !== "string")
303
329
  argTypes[declaration.arg] = declaration.type;
304
330
  }
@@ -307,6 +333,13 @@ export function getTemplateArgTypes(config) {
307
333
  export function getExplicitToolArgNames(args) {
308
334
  return mergeUnique((args ?? []).map((item) => parseToolArgToken(String(item)).arg));
309
335
  }
336
+ export function getExplicitToolArgDefaults(args) {
337
+ const declarations = typeof args === "string" ? splitArgDeclarations(args) : (args ?? []);
338
+ return Object.fromEntries(declarations
339
+ .map((item) => parseToolArgToken(String(item)))
340
+ .filter((item) => item.defaultValue !== undefined)
341
+ .map((item) => [item.arg, item.defaultValue]));
342
+ }
310
343
  export function getToolArgNames(config) {
311
344
  const normalizedConfig = CommandTemplates.normalizeCommandTemplateConfig(config);
312
345
  const declaredArgs = Array.isArray(normalizedConfig.args)