@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
package/lib/async-runs.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  import * as Paths from "./paths.ts";
34
34
  import * as RecipesReferences from "./recipes-references.ts";
35
35
  import * as RecipesUsage from "./recipes-usage.ts";
36
+ import * as Schema from "./schema.ts";
36
37
  import {
37
38
  resolveArtifactManifest,
38
39
  resolveArtifactPaths,
@@ -64,10 +65,6 @@ import * as RunsStart from "./runs-start.ts";
64
65
  import { appendRunTraceEvent } from "./runs-trace.ts";
65
66
  import * as RunsIndex from "./runs-index.ts";
66
67
  import * as RunsParentTeardown from "./runs-parent-teardown.ts";
67
- import {
68
- appendRunControlInStateDir,
69
- updateRunControlStatusInStateDir,
70
- } from "./runs-controls.ts";
71
68
  import {
72
69
  deliverRunControl,
73
70
  type DeliverRunControlOptions,
@@ -132,6 +129,8 @@ export interface AsyncRunStartParams {
132
129
  template?: CommandTemplateValue;
133
130
  args?: string[];
134
131
  defaults?: Record<string, unknown>;
132
+ recipe_dir?: string;
133
+ skill_dir?: string;
135
134
  parallel?: boolean;
136
135
  concurrency?: number | string;
137
136
  min_successful?: number | string;
@@ -313,7 +312,14 @@ function resolveStartParams(params: AsyncRunStartParams): AsyncRunStartParams {
313
312
  fileParams.run_id ||
314
313
  fileParams.name ||
315
314
  getRunIdFromFile(fileParams.file),
316
- values: { ...(fileParams.values ?? {}), ...(params.values ?? {}) },
315
+ values: {
316
+ ...(fileParams.values ?? {}),
317
+ ...(params.values ?? {}),
318
+ ...(fileParams.recipe_dir
319
+ ? { recipe_dir: fileParams.recipe_dir }
320
+ : {}),
321
+ ...(fileParams.skill_dir ? { skill_dir: fileParams.skill_dir } : {}),
322
+ },
317
323
  };
318
324
  }
319
325
 
@@ -481,13 +487,37 @@ export function startRun(
481
487
  !isAbsolute(recipeRelation)
482
488
  ? dirname(packagedRecipeRoot)
483
489
  : undefined;
484
- const values = {
485
- ...(packagedRepo ? { repo: packagedRepo } : {}),
486
- ...(startParams.values || {}),
487
- run_id: run,
488
- state_dir: stateDir,
489
- trace_file: join(stateDir, "trace.jsonl"),
490
- };
490
+ const argSpec = Schema.parseToolArgDeclarationList(startParams.args ?? []);
491
+ if (argSpec.error) throw new Error(argSpec.error);
492
+ const declaredArgs = new Set(argSpec.args);
493
+ for (const key of Object.keys(startParams.defaults ?? {})) {
494
+ if (declaredArgs.size > 0 && !declaredArgs.has(key))
495
+ throw new Error(`Unknown Recipe default argument: ${key}`);
496
+ }
497
+ const values = Schema.normalizeRuntimeValues(
498
+ {
499
+ ...argSpec.defaults,
500
+ ...(startParams.defaults || {}),
501
+ ...(packagedRepo ? { repo: packagedRepo } : {}),
502
+ ...(startParams.values || {}),
503
+ run_id: run,
504
+ state_dir: stateDir,
505
+ trace_file: join(stateDir, "trace.jsonl"),
506
+ },
507
+ argSpec.argTypes,
508
+ );
509
+ const runtimeTemplate =
510
+ resolved.template &&
511
+ typeof resolved.template === "object" &&
512
+ !Array.isArray(resolved.template)
513
+ ? {
514
+ ...resolved.template,
515
+ defaults: {
516
+ ...(resolved.template.defaults ?? {}),
517
+ ...values,
518
+ },
519
+ }
520
+ : resolved.template;
491
521
  const modelPolicy = describeCurrentPolicyProvenance({
492
522
  defaults: startParams.defaults,
493
523
  template: resolved.template,
@@ -561,7 +591,7 @@ export function startRun(
561
591
  state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
562
592
  status: "running",
563
593
  ...(startParams.tool ? { tool: startParams.tool } : {}),
564
- template: resolved.template,
594
+ template: runtimeTemplate,
565
595
  values,
566
596
  model_policy: modelPolicy,
567
597
  ...(artifacts ? { artifacts } : {}),
@@ -933,37 +963,9 @@ function stopRun(
933
963
  ) {
934
964
  return { stopped: false, reason: "run generation changed", status };
935
965
  }
936
- const control =
937
- event === "run.kill" && typeof status.run_instance_id === "string"
938
- ? appendRunControlInStateDir(stateDir, {
939
- action: "kill",
940
- run_instance_id: status.run_instance_id,
941
- })
942
- : undefined;
943
- if (control) {
944
- updateRunControlStatusInStateDir(
945
- stateDir,
946
- control.id,
947
- "claimed",
948
- {},
949
- ["queued"],
950
- );
951
- }
952
- const finish = (result: Record<string, unknown>): Record<string, unknown> => {
953
- if (!control) return result;
954
- const handled = result.stopped === true;
955
- updateRunControlStatusInStateDir(
956
- stateDir,
957
- control.id,
958
- handled ? "handled" : "failed",
959
- handled ? {} : { error: String(result.reason ?? "kill rejected") },
960
- ["claimed"],
961
- );
962
- return { ...result, control_id: control.id };
963
- };
964
966
  const pid = Number(status.pid || 0);
965
967
  if (status.status !== "running" && status.status !== "exited") {
966
- return finish({ stopped: false, reason: "not running", status });
968
+ return { stopped: false, reason: "not running", status };
967
969
  }
968
970
  const identity = verifyRunProcessIdentity(
969
971
  pid,
@@ -974,22 +976,22 @@ function stopRun(
974
976
  identity.status === "owner_mismatch" ||
975
977
  identity.status === "unsupported_proof"
976
978
  ) {
977
- return finish({
979
+ return {
978
980
  stopped: false,
979
981
  reason: identity.status.replaceAll("_", " "),
980
982
  process_identity_status: identity.status,
981
983
  status,
982
- });
984
+ };
983
985
  }
984
- return finish({ stopped: false, reason: "not running", status });
986
+ return { stopped: false, reason: "not running", status };
985
987
  }
986
988
  if (!identity.valid) {
987
- return finish({
989
+ return {
988
990
  stopped: false,
989
991
  reason: identity.status.replaceAll("_", " "),
990
992
  process_identity_status: identity.status,
991
993
  status,
992
- });
994
+ };
993
995
  }
994
996
  let signalResult: RunProcessSignalPlan;
995
997
  try {
@@ -999,11 +1001,6 @@ function stopRun(
999
1001
  status.process_identity as RunProcessIdentity,
1000
1002
  );
1001
1003
  } catch (error) {
1002
- if (control) {
1003
- updateRunControlStatusInStateDir(stateDir, control.id, "failed", {
1004
- error: error instanceof Error ? error.message : String(error),
1005
- });
1006
- }
1007
1004
  throw error;
1008
1005
  }
1009
1006
  appendRunTraceEvent(stateDir, {
@@ -1020,13 +1017,13 @@ function stopRun(
1020
1017
  finalizeInterruptedExecution(stateDir, "cancelled", signal);
1021
1018
  markTerminalProgress(stateDir, "cancelled");
1022
1019
  }
1023
- return finish({
1020
+ return {
1024
1021
  stopped: true,
1025
1022
  pid,
1026
1023
  signal,
1027
1024
  ...signalResult,
1028
1025
  state_dir: stateDir,
1029
- });
1026
+ };
1030
1027
  } finally {
1031
1028
  releaseControlLock();
1032
1029
  }
package/lib/config.ts CHANGED
@@ -188,6 +188,10 @@ export function normalizeStoredTool(
188
188
  template: argTemplate,
189
189
  };
190
190
  const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
191
+ Schema.assertCompatibleToolArgTypes(
192
+ inferredArgTypes,
193
+ declarations.argTypes,
194
+ );
191
195
  const argTypes = { ...inferredArgTypes, ...declarations.argTypes };
192
196
  const cfg = {
193
197
  name,
package/lib/file-state.ts CHANGED
@@ -10,8 +10,11 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSyn
10
10
  import { tmpdir } from "node:os";
11
11
  import { basename, dirname, join, parse, resolve } from "node:path";
12
12
 
13
- const FILE_MUTATION_LOCK_TIMEOUT_MS = process.platform === "win32" ? 15000 : 5000;
13
+ const FILE_MUTATION_LOCK_TIMEOUT_MS = process.platform === "win32" ? 30000 : 15000;
14
14
  const FILE_MUTATION_LOCK_STALE_MS = 30000;
15
+ const FILE_MUTATION_LOCK_RECLAIM_POLL_MS = 100;
16
+ const FILE_MUTATION_LOCK_REMOVAL_GRACE_MS = 250;
17
+ const FILE_MUTATION_LOCK_MAX_WAIT_MS = 50;
15
18
  const FILE_MUTATION_LOCK_ROOT = join(tmpdir(), "pi-actors-file-locks");
16
19
 
17
20
  function canonicalMutationPath(path: string): string {
@@ -65,7 +68,8 @@ function lockOwnerStatus(lockPath: string): "alive" | "dead" | "unknown" {
65
68
  if (!Number.isInteger(pid) || pid <= 0) return "unknown";
66
69
  try {
67
70
  process.kill(pid, 0);
68
- if (Date.now() - statSync(lockPath).mtimeMs > 50 && isZombieProcess(pid)) return "dead";
71
+ if (Date.now() - statSync(lockPath).mtimeMs > FILE_MUTATION_LOCK_REMOVAL_GRACE_MS &&
72
+ isZombieProcess(pid)) return "dead";
69
73
  return "alive";
70
74
  } catch (error) {
71
75
  return (error as NodeJS.ErrnoException).code === "ESRCH"
@@ -120,8 +124,9 @@ function removeLockBoundary(lockPath: string, token: string | undefined): boolea
120
124
  function tryReclaimRemovalBoundary(reclaimPath: string): void {
121
125
  if (!existsSync(reclaimPath)) return;
122
126
  try {
123
- const inspectedToken = readLockToken(reclaimPath);
124
127
  const age = Date.now() - statSync(reclaimPath).mtimeMs;
128
+ if (age <= FILE_MUTATION_LOCK_REMOVAL_GRACE_MS) return;
129
+ const inspectedToken = readLockToken(reclaimPath);
125
130
  const ownerStatus = lockOwnerStatus(reclaimPath);
126
131
  if (
127
132
  (ownerStatus === "dead" ||
@@ -195,6 +200,8 @@ export function acquireFileMutationLock(
195
200
  const token = randomUUID();
196
201
  const pendingPath = prepareLockBoundary(lockPath, token, options.onBeforeLockPublish);
197
202
  let contentionReported = false;
203
+ let nextReclaimAt = 0;
204
+ let waitMs = 10;
198
205
  try {
199
206
  for (;;) {
200
207
  try {
@@ -205,13 +212,22 @@ export function acquireFileMutationLock(
205
212
  contentionReported = true;
206
213
  options.onContention?.();
207
214
  }
208
- tryReclaimMutationLock(lockPath, options);
209
- if (Date.now() >= deadline) {
215
+ const now = Date.now();
216
+ if (now >= nextReclaimAt) {
217
+ nextReclaimAt = now + FILE_MUTATION_LOCK_RECLAIM_POLL_MS;
218
+ if (tryReclaimMutationLock(lockPath, options)) {
219
+ waitMs = 10;
220
+ continue;
221
+ }
222
+ }
223
+ if (now >= deadline) {
210
224
  throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
211
225
  cause: error,
212
226
  });
213
227
  }
214
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
228
+ const jitter = Math.floor(Math.random() * Math.max(1, Math.floor(waitMs / 2)));
229
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs + jitter);
230
+ waitMs = Math.min(FILE_MUTATION_LOCK_MAX_WAIT_MS, waitMs + 5);
215
231
  }
216
232
  }
217
233
  } finally {
@@ -251,11 +267,16 @@ export function withFileMutationLock<T>(
251
267
  }
252
268
  }
253
269
 
254
- export function writeTextAtomic(path: string, content: string): void {
270
+ export function writeTextAtomic(
271
+ path: string,
272
+ content: string,
273
+ options: { onBeforeReplace?(): void } = {},
274
+ ): void {
255
275
  mkdirSync(dirname(path), { recursive: true });
256
276
  const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
257
277
  try {
258
278
  writeFileSync(tempPath, content, "utf8");
279
+ options.onBeforeReplace?.();
259
280
  renameSync(tempPath, path);
260
281
  } catch (error) {
261
282
  try {
@@ -4,7 +4,7 @@
4
4
  * Owns manual kernel navigation; evidence parsing and lifecycle mutation stay in ports.
5
5
  */
6
6
 
7
- import { readFileSync } from "node:fs";
7
+ import { readFileSync, statSync } from "node:fs";
8
8
  import * as path from "node:path";
9
9
 
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
@@ -19,6 +19,10 @@ import * as ActorInspector from "./inspector.ts";
19
19
  import * as ControlProjection from "./control-projection.ts";
20
20
  import * as RunsControls from "./runs-controls.ts";
21
21
  import * as RunControlDelivery from "./runs-control-delivery.ts";
22
+ import * as RunsTrace from "./runs-trace.ts";
23
+ import * as Limits from "./limits.ts";
24
+ import { computeRunControlCapacity } from "./run-evidence-policy.ts";
25
+ import * as RuntimeTriage from "./runtime-triage.ts";
22
26
  import * as TraceProjection from "./trace-projection.ts";
23
27
 
24
28
  export type ActorInspectorTab = "recipe" | "trace" | "control";
@@ -59,12 +63,6 @@ const TRACE_SOURCES: TraceProjection.TraceSourceFilter[] = [
59
63
  "runtime",
60
64
  ];
61
65
 
62
- function newestTraceItems(items: readonly TraceProjection.TraceItem[]): TraceProjection.TraceItem[] {
63
- return [...items].sort((left, right) =>
64
- right.ts.localeCompare(left.ts) || right.id.localeCompare(left.id),
65
- );
66
- }
67
-
68
66
  export class ActorInspectorOverlay {
69
67
  private readonly done: () => void;
70
68
  private readonly killRun?: (run: string, runInstanceId: string) => ActorInspectorActionResult;
@@ -442,7 +440,7 @@ export class ActorInspectorOverlay {
442
440
  `${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
443
441
  `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
444
442
  "",
445
- this.theme.fg("text", "This sends canonical kill Control."),
443
+ this.theme.fg("text", "This invokes generation-fenced runtime kill."),
446
444
  "",
447
445
  `${cancel} ${kill}`,
448
446
  "",
@@ -544,7 +542,11 @@ export class ActorInspectorOverlay {
544
542
 
545
543
  private renderTraceList(run: ActorInspector.ActorInspectorRunItem): string[] {
546
544
  const items = this.traceItems(run);
547
- if (items.length === 0) return [this.theme.fg("muted", " No Trace evidence")];
545
+ const summary = RunsTrace.summarizeRunTraceJournal(
546
+ RunsTrace.readRunTraceJournal(path.join(this.stateRoot, run.run)));
547
+ const banner = summary.history_complete ? [] : [this.theme.fg("warning",
548
+ ` ! Trace history incomplete: ${summary.compacted ? `${summary.compactions_total} compaction(s), ` : ""}${summary.dropped_event_count_exact ? summary.dropped_events : "unknown"} dropped event(s), ${summary.dropped_bytes} bytes`)];
549
+ if (items.length === 0) return [...banner, this.theme.fg("muted", " No Trace evidence")];
548
550
  if (this.selectedTraceId) {
549
551
  const selectedIndex = items.findIndex((item) => item.id === this.selectedTraceId);
550
552
  if (selectedIndex >= 0) this.rowIndex = selectedIndex;
@@ -560,7 +562,7 @@ export class ActorInspectorOverlay {
560
562
  this.contentStripeIndices = items
561
563
  .slice(start, start + viewportRows)
562
564
  .map((_item, offset) => start + offset);
563
- return items.slice(start, start + viewportRows).map((item, offset) => {
565
+ return [...banner, ...items.slice(start, start + viewportRows - banner.length).map((item, offset) => {
564
566
  const index = start + offset;
565
567
  const detail = item.detail && typeof item.detail === "object"
566
568
  ? item.detail as Record<string, unknown>
@@ -577,7 +579,7 @@ export class ActorInspectorOverlay {
577
579
  return this.focus === "list" && index === this.rowIndex
578
580
  ? this.theme.fg("accent", row)
579
581
  : row;
580
- });
582
+ })];
581
583
  }
582
584
 
583
585
  private renderTraceDetail(width: number): string[] {
@@ -603,16 +605,26 @@ export class ActorInspectorOverlay {
603
605
  ? RunControlDelivery.readRunControlEndpoint(stateDir, run.runInstanceId)
604
606
  : undefined;
605
607
  const meta = this.readRunMeta(stateDir);
608
+ const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
609
+ const capacity = computeRunControlCapacity(journal.records);
610
+ const stalePending = journal.records.reduce<number>((count, control) => count + Number(Boolean(
611
+ RuntimeTriage.classifyRuntimeControl({ run: run.run,
612
+ runInstanceId: run.runInstanceId, status: run.status }, control, Date.now()).stale)), 0);
606
613
  return {
607
614
  status: run.status,
608
615
  run_instance_id: run.runInstanceId,
616
+ pending: capacity.pending, pending_limit: capacity.limit,
617
+ available: capacity.available, backpressured: capacity.backpressured,
618
+ journal_bytes: (() => { try { return statSync(RunsControls.runControlsFile(stateDir)).size; } catch { return 0; } })(),
619
+ journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
620
+ stale_pending: stalePending,
621
+ diagnostics: journal.diagnostics.map((item) => item.line === undefined
622
+ ? { reason: "unreadable_control_journal" } : { line: item.line, reason: "invalid_control_json" }),
609
623
  actor_actions: Array.isArray(meta.control) ? meta.control : [],
610
624
  runtime_actions: run.status === "running" ? ["kill"] : ["archive", "prune"],
611
625
  endpoint,
612
- recent_controls: RunsControls.readRunControlsFromStateDir(stateDir)
613
- .slice(-20)
614
- .reverse()
615
- .map(ControlProjection.projectRunControl),
626
+ recent_controls: journal.records.slice(-20).reverse().map((control) =>
627
+ ControlProjection.projectRunControl(control as RunsControls.RunControlRecord)),
616
628
  };
617
629
  }
618
630
 
@@ -624,10 +636,10 @@ export class ActorInspectorOverlay {
624
636
  ) {
625
637
  return this.traceCache.items;
626
638
  }
627
- const items = newestTraceItems(this.readTrace(path.join(this.stateRoot, run.run), {
639
+ const items = this.readTrace(path.join(this.stateRoot, run.run), {
628
640
  limit: 100,
629
641
  source: "all",
630
- }));
642
+ });
631
643
  this.traceCache = { run: run.run, runInstanceId: run.runInstanceId, items };
632
644
  return items;
633
645
  }
package/lib/inspector.ts CHANGED
@@ -105,9 +105,22 @@ export function readActorInspectorRecipe(
105
105
  const redactedRecord = (value: Record<string, unknown>): Record<string, unknown> =>
106
106
  SessionEvidence.redactSessionEvidenceValue(
107
107
  Object.fromEntries(
108
- Object.entries(value).filter(([, entry]) => entry !== undefined),
108
+ Object.entries(value).filter(
109
+ ([key, entry]) =>
110
+ entry !== undefined &&
111
+ key !== "recipe_dir" &&
112
+ key !== "skill_dir",
113
+ ),
109
114
  ),
110
115
  ) as Record<string, unknown>;
116
+ const launchValues =
117
+ meta.values && typeof meta.values === "object" && !Array.isArray(meta.values)
118
+ ? Object.fromEntries(
119
+ Object.entries(meta.values as Record<string, unknown>).filter(
120
+ ([key]) => key !== "recipe_dir" && key !== "skill_dir",
121
+ ),
122
+ )
123
+ : meta.values;
111
124
  return {
112
125
  composition: contexts.map((entry) =>
113
126
  redactedRecord({
@@ -116,6 +129,7 @@ export function readActorInspectorRecipe(
116
129
  file: entry.file,
117
130
  import_path: entry.import_path,
118
131
  name: entry.name,
132
+ qualified_name: entry.qualified_name,
119
133
  }),
120
134
  ),
121
135
  ...(definition ? { definition: redactedRecord(definition) } : {}),
@@ -129,7 +143,7 @@ export function readActorInspectorRecipe(
129
143
  launch: redactedRecord({
130
144
  cwd: meta.cwd,
131
145
  template: meta.template,
132
- values: meta.values,
146
+ values: launchValues,
133
147
  model_policy: meta.model_policy,
134
148
  control: meta.control,
135
149
  artifacts: meta.artifacts,
package/lib/limits.ts CHANGED
@@ -17,3 +17,13 @@ export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
17
17
  export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
18
18
  export const TRACE_EVENT_MAX_BYTES = 64 * 1024;
19
19
  export const TRACE_EVENT_MAX_READ = 200;
20
+ export const TRACE_JOURNAL_MAX_EVENTS = 2_048;
21
+ export const TRACE_JOURNAL_TARGET_EVENTS = 1_536;
22
+ export const TRACE_JOURNAL_MAX_BYTES = 4 * 1024 * 1024;
23
+ export const TRACE_JOURNAL_TARGET_BYTES = 3 * 1024 * 1024;
24
+ export const RUN_CONTROL_PENDING_LIMIT = 64;
25
+ export const RUN_CONTROL_TERMINAL_LIMIT = 128;
26
+ export const RUN_CONTROL_ERROR_MAX_BYTES = 4 * 1024;
27
+ export const RUN_CONTROL_JOURNAL_MAX_BYTES = 1024 * 1024;
28
+ export const RUN_RETENTION_MAX_RECORDS = 256;
29
+ export const RUN_RETENTION_MAX_BYTES = 1024 * 1024;
@@ -25,6 +25,7 @@ import {
25
25
 
26
26
  import * as AsyncRuns from "./async-runs.ts";
27
27
  import * as Paths from "./paths.ts";
28
+ import * as RunsTrace from "./runs-trace.ts";
28
29
  import { readJsonlFileResilient } from "./state-readers.ts";
29
30
 
30
31
  export type RunObservedStatus =
@@ -73,7 +74,7 @@ export interface RunSummary {
73
74
 
74
75
  export interface RunUiObservationState {
75
76
  attentionEventIds: Map<string, Set<string>>;
76
- eventLines: Map<string, number>;
77
+ legacyEventLines: Map<string, number>;
77
78
  frame: number;
78
79
  observed: Map<string, RunObservedStatus>;
79
80
  }
@@ -98,12 +99,19 @@ export interface RunUiNotificationSink {
98
99
  export function createRunUiObservationState(): RunUiObservationState {
99
100
  return {
100
101
  attentionEventIds: new Map<string, Set<string>>(),
101
- eventLines: new Map<string, number>(),
102
+ legacyEventLines: new Map<string, number>(),
102
103
  frame: 0,
103
104
  observed: new Map<string, RunObservedStatus>(),
104
105
  };
105
106
  }
106
107
 
108
+ export function primeRunAttentionState(
109
+ state: RunUiObservationState, ownerId: string, stateRoot?: string,
110
+ ): void {
111
+ detectRunAttentionEvents(state.legacyEventLines, summarizeRuns(stateRoot, ownerId),
112
+ state.attentionEventIds, true);
113
+ }
114
+
107
115
  export function readRunUiSnapshot(
108
116
  state: RunUiObservationState,
109
117
  ownerId: string,
@@ -116,7 +124,7 @@ export function readRunUiSnapshot(
116
124
  options.includeAttention === false
117
125
  ? []
118
126
  : detectRunAttentionEvents(
119
- state.eventLines,
127
+ state.legacyEventLines,
120
128
  summary,
121
129
  state.attentionEventIds,
122
130
  ),
@@ -132,7 +140,7 @@ export function pruneRunUiObservationState(
132
140
  ): void {
133
141
  pruneRunObservationState(
134
142
  state.observed,
135
- state.eventLines,
143
+ state.legacyEventLines,
136
144
  snapshot.summary,
137
145
  snapshot.transitions.map(
138
146
  (transition) => transition.stateDir ?? transition.run,
@@ -192,16 +200,15 @@ export function reconcileRunTerminalNotifications(input: {
192
200
  sink: RunUiNotificationSink;
193
201
  state: RunUiObservationState;
194
202
  stateRoot?: string;
203
+ includeAttention?: boolean;
195
204
  }): RunUiSnapshot {
196
205
  const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
197
- includeAttention: false,
206
+ includeAttention: input.includeAttention,
198
207
  stateRoot: input.stateRoot,
199
208
  });
200
- deliverRunTransitionNotifications(
201
- snapshot.transitions,
202
- input.sink,
203
- input.inFlight,
204
- );
209
+ if (input.includeAttention)
210
+ deliverRunAttentionNotifications(snapshot.attentionEvents, input.sink);
211
+ deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
205
212
  pruneRunUiObservationState(input.state, snapshot);
206
213
  return snapshot;
207
214
  }
@@ -1114,15 +1121,17 @@ function parseAttentionRecord(
1114
1121
  };
1115
1122
  }
1116
1123
 
1117
- function readTraceAttentionRecords(run: RunObservation): Record<string, unknown>[] {
1118
- if (!run.stateDir) return [];
1119
- const tracePath = join(run.stateDir, "trace.jsonl");
1120
- if (existsSync(tracePath)) {
1121
- return readJsonlFileResilient<Record<string, unknown>>(tracePath).records;
1122
- }
1123
- return readJsonlFileResilient<Record<string, unknown>>(
1124
- join(run.stateDir, "outbox.jsonl"),
1125
- ).records;
1124
+ function readTraceAttentionRecords(run: RunObservation): {
1125
+ canonical: boolean; records: Record<string, unknown>[];
1126
+ } {
1127
+ if (!run.stateDir) return { canonical: true, records: [] };
1128
+ if (existsSync(join(run.stateDir, "trace.jsonl"))) return {
1129
+ canonical: true,
1130
+ records: RunsTrace.readRunTraceJournal(run.stateDir).events.map(
1131
+ ({ event }) => event as unknown as Record<string, unknown>),
1132
+ };
1133
+ return { canonical: false, records: readJsonlFileResilient<Record<string, unknown>>(
1134
+ join(run.stateDir, "outbox.jsonl")).records };
1126
1135
  }
1127
1136
 
1128
1137
  export function pruneRunObservationState(
@@ -1134,55 +1143,44 @@ export function pruneRunObservationState(
1134
1143
  ): void {
1135
1144
  const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
1136
1145
  const terminalRunSet = new Set(terminalRuns);
1137
- const terminalLineKeys = new Set(
1138
- summary.runs
1139
- .filter((run) => terminalRunSet.has(runObservationKey(run)))
1140
- .map((run) => runObservationKey(run)),
1141
- );
1142
- const activeLineKeys = new Set(
1143
- summary.runs.map((run) => run.stateDir ?? run.run),
1144
- );
1146
+ const terminalLineKeys = new Set(summary.runs
1147
+ .filter((run) => terminalRunSet.has(runObservationKey(run)))
1148
+ .map((run) => runObservationKey(run)));
1149
+ const activeLineKeys = new Set(summary.runs.map((run) => run.stateDir ?? run.run));
1145
1150
  for (const run of terminalRunSet) previousStatuses.delete(run);
1146
- for (const run of previousStatuses.keys()) {
1151
+ for (const run of previousStatuses.keys())
1147
1152
  if (!activeRuns.has(run)) previousStatuses.delete(run);
1148
- }
1149
- for (const key of previousLineCounts.keys()) {
1150
- if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) {
1151
- previousLineCounts.delete(key);
1152
- }
1153
- }
1154
- for (const key of seenEventIds.keys()) {
1155
- if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) {
1156
- seenEventIds.delete(key);
1157
- }
1158
- }
1153
+ for (const key of previousLineCounts.keys())
1154
+ if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) previousLineCounts.delete(key);
1155
+ for (const key of seenEventIds.keys())
1156
+ if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) seenEventIds.delete(key);
1159
1157
  }
1160
1158
 
1161
1159
  export function detectRunAttentionEvents(
1162
- previousLineCounts: Map<string, number>,
1163
- summary: RunSummary,
1164
- seenEventIds: Map<string, Set<string>> = new Map(),
1160
+ legacyLineCounts: Map<string, number>, summary: RunSummary,
1161
+ seenEventIds: Map<string, Set<string>> = new Map(), prime = false,
1165
1162
  ): RunAttentionEvent[] {
1166
1163
  const events: RunAttentionEvent[] = [];
1167
1164
  for (const run of summary.runs) {
1168
1165
  const key = run.stateDir ?? run.run;
1169
- const records = readTraceAttentionRecords(run);
1170
- const previousCount = previousLineCounts.get(key) ?? 0;
1171
- if (run.notificationPolicy === "silent") {
1172
- previousLineCounts.set(key, records.length);
1173
- seenEventIds.set(key, seenEventIds.get(key) ?? new Set<string>());
1174
- continue;
1175
- }
1176
- const start = Math.min(previousCount, records.length);
1166
+ const read = readTraceAttentionRecords(run);
1167
+ const retained = new Set<string>();
1177
1168
  const seen = seenEventIds.get(key) ?? new Set<string>();
1178
- for (let index = start; index < records.length; index += 1) {
1179
- const event = parseAttentionRecord(records[index], run, index);
1180
- if (!event || seen.has(event.id)) continue;
1181
- events.push(event);
1182
- seen.add(event.id);
1169
+ const start = read.canonical ? 0
1170
+ : Math.min(legacyLineCounts.get(key) ?? 0, read.records.length);
1171
+ for (const [index, record] of read.records.entries()) {
1172
+ const event = parseAttentionRecord(record, run, index);
1173
+ if (!event || !shouldNotifyRunAttentionEvent(event) ||
1174
+ event.kind === "runtime.trace_compacted") continue;
1175
+ retained.add(event.id);
1176
+ if (prime || run.notificationPolicy === "silent") seen.add(event.id);
1177
+ else if (index >= start && !seen.has(event.id)) {
1178
+ events.push(event); seen.add(event.id);
1179
+ }
1183
1180
  }
1184
- previousLineCounts.set(key, records.length);
1185
- seenEventIds.set(key, seen);
1181
+ seenEventIds.set(key, new Set([...retained].filter((id) => seen.has(id))));
1182
+ if (read.canonical) legacyLineCounts.delete(key);
1183
+ else legacyLineCounts.set(key, read.records.length);
1186
1184
  }
1187
1185
  return events;
1188
1186
  }