@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
@@ -3,13 +3,16 @@
3
3
  * Zones: Run Recipe/Trace/Control views and runtime/recipe/tool diagnostics
4
4
  * Owns exact inspect target/view dispatch; source projection stays in domain modules.
5
5
  */
6
+ import { statSync } from "node:fs";
6
7
  import { join } from "node:path";
7
8
  import * as AsyncRuns from "./async-runs.js";
8
9
  import * as ControlProjection from "./control-projection.js";
9
10
  import * as Inspector from "./inspector.js";
10
11
  import * as Limits from "./limits.js";
12
+ import { computeRunControlCapacity } from "./run-evidence-policy.js";
11
13
  import * as Paths from "./paths.js";
12
14
  import * as RecipesDiscovery from "./recipes-discovery.js";
15
+ import * as RecipesReferences from "./recipes-references.js";
13
16
  import * as ReviewDiagnostics from "./review-diagnostics.js";
14
17
  import * as RunsControls from "./runs-controls.js";
15
18
  import * as RunControlDelivery from "./runs-control-delivery.js";
@@ -55,7 +58,9 @@ function runtimeTriage(ctx, deps) {
55
58
  const failed = runs.filter((run) => run.status === "failed");
56
59
  const pendingControls = [];
57
60
  const staleControls = [];
61
+ const backpressuredRuns = [];
58
62
  const controlDiagnostics = [];
63
+ const traceDiagnostics = [];
59
64
  const nowMs = Date.now();
60
65
  for (const run of runs) {
61
66
  const stateDir = typeof run.state_dir === "string" ? run.state_dir : undefined;
@@ -63,13 +68,19 @@ function runtimeTriage(ctx, deps) {
63
68
  continue;
64
69
  const runId = String(run.run ?? "unknown");
65
70
  const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
71
+ const capacity = computeRunControlCapacity(journal.records);
66
72
  controlDiagnostics.push(...journal.diagnostics.map((diagnostic) => ({
67
73
  ...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
68
- reason: diagnostic.line === undefined
69
- ? "unreadable_control_journal"
70
- : "invalid_control_json",
71
- run: runId,
74
+ reason: diagnostic.line === undefined ? "unreadable_control_journal" : "invalid_control_json", run: runId,
72
75
  })));
76
+ if (capacity.backpressured)
77
+ backpressuredRuns.push({
78
+ run: runId, pending: capacity.pending, pending_limit: capacity.limit,
79
+ journal_bytes: statSync(RunsControls.runControlsFile(stateDir)).size,
80
+ });
81
+ const traceSummary = RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(stateDir));
82
+ if (!traceSummary.history_complete)
83
+ traceDiagnostics.push({ run: runId, ...traceSummary });
73
84
  for (const control of journal.records) {
74
85
  const classified = RuntimeTriage.classifyRuntimeControl({
75
86
  run: runId,
@@ -114,12 +125,13 @@ function runtimeTriage(ctx, deps) {
114
125
  pending_controls: boundedPendingControls,
115
126
  stale_control_count: staleControls.length,
116
127
  stale_controls: boundedStaleControls,
117
- control_diagnostic_count: controlDiagnostics.length,
118
- control_diagnostics: controlDiagnostics.slice(0, 40),
128
+ backpressured_run_count: backpressuredRuns.length, backpressured_runs: backpressuredRuns.slice(0, 40),
129
+ control_diagnostic_count: controlDiagnostics.length, control_diagnostics: controlDiagnostics.slice(0, 40),
130
+ trace_diagnostic_count: traceDiagnostics.length, trace_diagnostics: traceDiagnostics.slice(0, 40),
119
131
  attention_events: attentionEvents.slice(-40).reverse(),
120
132
  next_actions: [
121
133
  ...(failed.length ? ["inspect target=runtime view=runs status=failed"] : []),
122
- ...staleControls.slice(0, 3).map((entry) => `inspect target=run:${entry.run} view=control`),
134
+ ...new Set([...staleControls, ...backpressuredRuns].slice(0, 3).map((entry) => `inspect target=run:${entry.run} view=control`)),
123
135
  ...(attentionEvents.length
124
136
  ? attentionEvents.slice(-3).map((entry) => `inspect target=run:${String(entry.run)} view=trace source=runtime`)
125
137
  : []),
@@ -131,7 +143,7 @@ function compactRuntime(view, details) {
131
143
  return `\nruntime runs=${details.runs?.length ?? 0}`;
132
144
  }
133
145
  if (view === "triage") {
134
- return `\nruntime failed=${details.failed_runs?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} attention=${details.attention_events?.length ?? 0}`;
146
+ return `\nruntime failed=${details.failed_runs?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} backpressured=${Number(details.backpressured_run_count ?? 0)} incomplete_trace=${Number(details.trace_diagnostic_count ?? 0)} attention=${details.attention_events?.length ?? 0}`;
135
147
  }
136
148
  return `\nruntime version=${String(details.version)} state_schema=${String(details.state_schema)} automatic_review=${String(details.automatic_review)}`;
137
149
  }
@@ -161,8 +173,17 @@ function inspectRecipes(view, deps) {
161
173
  { root: recipeRoot, defaultTool: true, mutableUsage: true },
162
174
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
163
175
  ]);
176
+ const skillRecipeNamespaces = RecipesReferences.getActiveSkillRecipeNamespaces();
164
177
  const summary = {
165
178
  ...RecipesDiscovery.summarizeDiscovery(discovered),
179
+ skill_recipe_namespaces: skillRecipeNamespaces,
180
+ skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
181
+ .filter(([, roots]) => roots.length > 1)
182
+ .map(([name, roots]) => ({
183
+ name,
184
+ roots,
185
+ error: `Ambiguous active Skill Recipe namespace ${name}`,
186
+ })),
166
187
  drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
167
188
  };
168
189
  return {
@@ -199,8 +220,8 @@ function inspectRun(run, view, input, ctx, deps) {
199
220
  throw new Error(`unsupported Trace source: ${source}`);
200
221
  }
201
222
  return {
202
- run,
203
- source,
223
+ run, source,
224
+ summary: RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(stateDir)),
204
225
  items: TraceProjection.projectRunTrace(stateDir, {
205
226
  artifacts: status.artifacts,
206
227
  limit: Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
@@ -209,19 +230,42 @@ function inspectRun(run, view, input, ctx, deps) {
209
230
  };
210
231
  }
211
232
  const runInstanceId = String(status.run_instance_id ?? "");
233
+ const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
234
+ const capacity = computeRunControlCapacity(journal.records);
235
+ const structuralDiagnostics = journal.records.flatMap((value) => {
236
+ const classified = RuntimeTriage.classifyRuntimeControl({ run,
237
+ runInstanceId: runInstanceId || undefined, status: String(status.status ?? "unknown") }, value, Date.now());
238
+ return classified.diagnostic ? [classified.diagnostic.reason] : [];
239
+ });
240
+ let journalBytes = 0;
241
+ try {
242
+ journalBytes = statSync(RunsControls.runControlsFile(stateDir)).size;
243
+ }
244
+ catch { }
245
+ const diagnostics = [...journal.diagnostics.map((item) => ({
246
+ ...(item.line === undefined ? {} : { line: item.line }),
247
+ reason: item.line === undefined ? "unreadable_control_journal" : "invalid_control_json",
248
+ })), ...structuralDiagnostics.map((reason) => ({ reason }))].slice(0, 40);
249
+ const nowMs = Date.now();
250
+ const stalePending = journal.records.reduce((count, control) => count + Number(Boolean(RuntimeTriage.classifyRuntimeControl({ run, runInstanceId: runInstanceId || undefined,
251
+ status: String(status.status ?? "unknown") }, control, nowMs).stale)), 0);
212
252
  return {
213
253
  run,
214
254
  run_instance_id: runInstanceId,
215
255
  status: status.status,
256
+ pending: capacity.pending, pending_limit: capacity.limit,
257
+ available: capacity.available, backpressured: capacity.backpressured,
258
+ journal_bytes: journalBytes, journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
259
+ ...(capacity.oldest_pending_at ? { oldest_pending_at: capacity.oldest_pending_at } : {}),
260
+ stale_pending: stalePending,
261
+ diagnostics,
216
262
  actor_actions: Array.isArray(status.control) ? status.control : [],
217
263
  runtime_actions: status.status === "running" ? ["kill"] : ["archive", "prune"],
218
264
  endpoint: runInstanceId
219
265
  ? RunControlDelivery.readRunControlEndpoint(stateDir, runInstanceId)
220
266
  : undefined,
221
- recent_controls: RunsControls.readRunControlsFromStateDir(stateDir)
222
- .slice(-Math.max(1, Number(input.lines || 20)))
223
- .reverse()
224
- .map(ControlProjection.projectRunControl),
267
+ recent_controls: journal.records.slice(-Math.max(1, Number(input.lines || 20))).reverse()
268
+ .map((control) => ControlProjection.projectRunControl(control)),
225
269
  };
226
270
  }
227
271
  function inspectTool(name, view, deps) {
@@ -45,11 +45,16 @@ function shouldAddRuntimeToolUsageHint(error) {
45
45
  }
46
46
  function formatRuntimeToolUsageHint(cfg, required, includeRunId) {
47
47
  const optional = cfg.args.filter((arg) => !required.includes(arg));
48
+ const exampleDefaults = {
49
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
50
+ ...cfg.defaults,
51
+ ...(cfg.recipe?.values ?? {}),
52
+ };
48
53
  const example = {};
49
54
  for (const arg of required)
50
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
55
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
51
56
  for (const arg of optional)
52
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
57
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
53
58
  if (includeRunId)
54
59
  example.run_id = `${cfg.name}-1`;
55
60
  const lines = [
@@ -62,6 +67,21 @@ function formatRuntimeToolUsageHint(cfg, required, includeRunId) {
62
67
  lines.push(`Optional: ${[...optional, ...(includeRunId ? ["run_id"] : [])].join(", ")}`);
63
68
  return lines.join("\n");
64
69
  }
70
+ function resolveRecipeToolValues(cfg, callerValues, ctx) {
71
+ const recipeRuntimeValues = {
72
+ ...(cfg.recipe?.recipe_dir
73
+ ? { recipe_dir: cfg.recipe.recipe_dir }
74
+ : {}),
75
+ ...(cfg.recipe?.skill_dir ? { skill_dir: cfg.recipe.skill_dir } : {}),
76
+ };
77
+ return Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues({
78
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
79
+ ...cfg.defaults,
80
+ ...(cfg.recipe?.values ?? {}),
81
+ ...callerValues,
82
+ ...recipeRuntimeValues,
83
+ }, ctx), cfg.argTypes);
84
+ }
65
85
  function formatRuntimeToolArgumentError(cfg, error, required, includeRunId) {
66
86
  const message = error instanceof Error ? error.message : String(error);
67
87
  if (!shouldAddRuntimeToolUsageHint(error))
@@ -87,8 +107,10 @@ export function createRuntimeToolDefinition(cfg, exec) {
87
107
  defaults: cfg.defaults,
88
108
  template: requiredTemplate,
89
109
  };
110
+ const recipeInlineDefaults = Schema.getExplicitToolArgDefaults(cfg.recipe?.args);
90
111
  const requiredArgs = isRecipe && cfg.storedArgs !== undefined
91
- ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
112
+ ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg) &&
113
+ !Object.hasOwn(recipeInlineDefaults, arg)))
92
114
  : RecipesReferences.isRecipeReference(cfg.template) && !recipeTemplate
93
115
  ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
94
116
  : Schema.getRequiredToolArgNames(requiredTemplateConfig);
@@ -137,7 +159,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
137
159
  run_id: runId,
138
160
  tool: cfg.name,
139
161
  policy_values: ModelContext.withCurrentModelValues({ ...(cfg.recipe?.values ?? {}), ...values }, ctx),
140
- values: Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues({ ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values }, ctx), cfg.argTypes),
162
+ values: resolveRecipeToolValues(cfg, values, ctx),
141
163
  }, ctx.cwd);
142
164
  return {
143
165
  content: [
@@ -150,12 +172,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
150
172
  };
151
173
  }
152
174
  if (isRecipe && recipeTemplate) {
153
- const paramsWithDefaults = ModelContext.withCurrentModelValues({
154
- ...(cfg.recipe?.values ?? {}),
155
- ...cfg.defaults,
156
- ...params,
157
- }, ctx);
158
- return await Execution.executeRegisteredTool({ ...cfg, template: recipeTemplate }, Schema.normalizeRuntimeValues(paramsWithDefaults, cfg.argTypes), exec, ctx.cwd, signal);
175
+ return await Execution.executeRegisteredTool({ ...cfg, template: recipeTemplate }, resolveRecipeToolValues(cfg, params, ctx), exec, ctx.cwd, signal);
159
176
  }
160
177
  return await Execution.executeRegisteredTool(cfg, Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues(params, ctx), cfg.argTypes), exec, ctx.cwd, signal);
161
178
  }
@@ -90,8 +90,8 @@ export function createSpawnToolDefinition() {
90
90
  parameters: Schema.objectSchema({
91
91
  artifacts: Schema.looseObjectSchema("Optional named artifact paths for the spawned Run."),
92
92
  as: Schema.stringSchema("Optional Run identity in exact run:<id> form."),
93
- file: Schema.stringSchema("Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes."),
94
- recipe: Schema.stringSchema("Alias for file; template recipe JSON file/name to spawn."),
93
+ file: Schema.stringSchema("Optional template Recipe reference. Supports paths, bare user/adjacent/std names, std:<name>, and skill:<skill>/<recipe>."),
94
+ recipe: Schema.stringSchema("Alias for file; qualified Skill/std or ordinary Recipe reference to spawn."),
95
95
  template: Schema.unionSchema([
96
96
  Schema.stringSchema("Inline command template string"),
97
97
  Schema.arraySchema("Inline command-template sequence or parallel tree"),
@@ -9,11 +9,21 @@ import * as ControlProjection from "./control-projection.js";
9
9
  import * as ExecutionSessions from "./execution-sessions.js";
10
10
  import * as Limits from "./limits.js";
11
11
  import { resolveArtifactManifest, } from "./runs-artifacts.js";
12
+ import * as RunsControls from "./runs-controls.js";
12
13
  import { tailFile } from "./runs-status.js";
14
+ import * as RunsTrace from "./runs-trace.js";
13
15
  import * as SessionEvidence from "./session-evidence.js";
14
- import { readJsonFileResilient, readJsonlFileResilient, } from "./state-readers.js";
16
+ import { readJsonFileResilient, } from "./state-readers.js";
15
17
  const LIFECYCLE_PREFIXES = ["run.", "command."];
16
18
  const SOURCE_LIMIT = 100;
19
+ const SOURCE_RANK = {
20
+ lifecycle: 6,
21
+ control: 5,
22
+ process: 4,
23
+ agent: 3,
24
+ artifact: 2,
25
+ runtime: 1,
26
+ };
17
27
  function redact(value) {
18
28
  return SessionEvidence.redactSessionEvidenceValue(value);
19
29
  }
@@ -22,6 +32,21 @@ function sourceForEvent(event) {
22
32
  ? "lifecycle"
23
33
  : "runtime";
24
34
  }
35
+ function ordered(item, sourceKey, ordinal) {
36
+ return { item, ordinal, sourceKey };
37
+ }
38
+ function compareTraceItems(left, right) {
39
+ const timestamp = right.item.ts.localeCompare(left.item.ts);
40
+ if (timestamp)
41
+ return timestamp;
42
+ if (left.sourceKey === right.sourceKey) {
43
+ const ordinal = right.ordinal - left.ordinal;
44
+ if (ordinal)
45
+ return ordinal;
46
+ }
47
+ return SOURCE_RANK[right.item.source] - SOURCE_RANK[left.item.source] ||
48
+ right.item.id.localeCompare(left.item.id);
49
+ }
25
50
  function latestControlTimestamp(control) {
26
51
  return (control.failed_at ??
27
52
  control.handled_at ??
@@ -57,39 +82,57 @@ function controlDiagnosticItems(diagnostics) {
57
82
  }));
58
83
  }
59
84
  function traceEventItems(stateDir) {
60
- const read = readJsonlFileResilient(join(stateDir, "trace.jsonl"));
61
- return [
62
- ...read.records.slice(-SOURCE_LIMIT).map((event) => ({
63
- id: event.id,
64
- ts: event.ts,
65
- source: sourceForEvent(event),
66
- kind: event.kind,
67
- summary: event.summary ?? event.kind,
68
- ...(event.level ? { level: event.level } : {}),
69
- detail: redact({
70
- ...(event.attention ? { attention: event.attention } : {}),
71
- ...(event.data !== undefined ? { data: event.data } : {}),
72
- }),
73
- })),
74
- ...diagnosticItems(read.diagnostics),
75
- ];
85
+ const read = RunsTrace.readRunTraceJournal(stateDir);
86
+ const retained = read.events.slice(-SOURCE_LIMIT);
87
+ const projected = retained.map(({ event, ordinal }) => ordered({
88
+ id: event.id,
89
+ ts: event.ts,
90
+ source: sourceForEvent(event),
91
+ kind: event.kind,
92
+ summary: event.summary ?? event.kind,
93
+ ...(event.level ? { level: event.level } : {}),
94
+ detail: redact({
95
+ ...(event.attention ? { attention: event.attention } : {}),
96
+ ...(event.data !== undefined ? { data: event.data } : {}),
97
+ }),
98
+ }, "trace", ordinal));
99
+ const diagnostics = diagnosticItems(read.diagnostics).map((item, index) => ordered(item, "trace", read.events.length + index + 1));
100
+ const hasMarker = read.events.some(({ event }) => event.kind === "runtime.trace_compacted");
101
+ if (read.omittedPrefixBytes > 0 && !hasMarker) {
102
+ diagnostics.push(ordered({
103
+ id: "runtime:trace-history-incomplete",
104
+ ts: new Date(0).toISOString(),
105
+ source: "runtime",
106
+ kind: "runtime.trace_history_incomplete",
107
+ summary: "Trace history is incomplete",
108
+ level: "warning",
109
+ detail: {
110
+ omitted_prefix_bytes: read.omittedPrefixBytes,
111
+ read_bytes: read.readBytes,
112
+ retained_events: read.events.length,
113
+ },
114
+ }, "trace", 0));
115
+ }
116
+ return [...projected, ...diagnostics];
76
117
  }
77
118
  function controlItems(stateDir) {
78
- const read = readJsonlFileResilient(join(stateDir, "controls.jsonl"));
119
+ const read = RunsControls.readRunControlJournalFromStateDir(stateDir);
120
+ const start = Math.max(0, read.records.length - SOURCE_LIMIT);
121
+ const projected = read.records.slice(start).map((control, index) => {
122
+ const value = ControlProjection.projectRunControl(control);
123
+ return ordered({
124
+ id: `control:${value.id}`,
125
+ ts: latestControlTimestamp(value),
126
+ source: "control",
127
+ kind: `control.${value.status}`,
128
+ summary: `${value.action} ${value.status}`,
129
+ ...(value.status === "failed" ? { level: "error" } : {}),
130
+ detail: value,
131
+ }, "control", start + index + 1);
132
+ });
79
133
  return [
80
- ...read.records.slice(-SOURCE_LIMIT).map((control) => {
81
- const projected = ControlProjection.projectRunControl(control);
82
- return {
83
- id: `control:${projected.id}`,
84
- ts: latestControlTimestamp(projected),
85
- source: "control",
86
- kind: `control.${projected.status}`,
87
- summary: `${projected.action} ${projected.status}`,
88
- ...(projected.status === "failed" ? { level: "error" } : {}),
89
- detail: projected,
90
- };
91
- }),
92
- ...controlDiagnosticItems(read.diagnostics),
134
+ ...projected,
135
+ ...controlDiagnosticItems(read.diagnostics).map((item, index) => ordered(item, "control", read.records.length + index + 1)),
93
136
  ];
94
137
  }
95
138
  function agentItems(stateDir) {
@@ -98,7 +141,7 @@ function agentItems(stateDir) {
98
141
  .map((turn) => {
99
142
  const text = turn.assistantText ?? turn.userText ?? "Agent turn";
100
143
  const summary = text.replaceAll(/\s+/g, " ").trim().slice(0, 160);
101
- return {
144
+ return ordered({
102
145
  id: `agent:${turn.commandId}:${turn.sessionFile}:${turn.index}`,
103
146
  ts: turn.timestamp ?? new Date(0).toISOString(),
104
147
  source: "agent",
@@ -106,7 +149,7 @@ function agentItems(stateDir) {
106
149
  summary: summary || "Agent turn",
107
150
  ...(turn.error ? { level: "error" } : {}),
108
151
  detail: redact(turn),
109
- };
152
+ }, "agent", turn.index);
110
153
  });
111
154
  }
112
155
  function processItems(stateDir) {
@@ -118,7 +161,7 @@ function processItems(stateDir) {
118
161
  const tail = tailFile(path, Limits.DEFAULT_INSPECT_LINES);
119
162
  if (!tail && stat.size === 0)
120
163
  return [];
121
- return [{
164
+ return [ordered({
122
165
  id: `process:${stream}`,
123
166
  ts: stat.mtime.toISOString(),
124
167
  source: "process",
@@ -126,17 +169,18 @@ function processItems(stateDir) {
126
169
  summary: `${stream} (${stat.size} bytes)`,
127
170
  ...(stream === "stderr" ? { level: "warning" } : {}),
128
171
  detail: redact({ bytes: stat.size, path, tail }),
129
- }];
172
+ }, `process:${stream}`, 1)];
130
173
  });
131
174
  }
132
175
  function resultItems(stateDir) {
133
176
  const path = join(stateDir, "result.json");
134
177
  const read = readJsonFileResilient(path, {});
135
- if (Object.keys(read.value).length === 0)
136
- return diagnosticItems(read.diagnostics);
178
+ if (Object.keys(read.value).length === 0) {
179
+ return diagnosticItems(read.diagnostics).map((item, index) => ordered(item, "process:result", index));
180
+ }
137
181
  const stat = statSync(path);
138
182
  const failed = Number(read.value.code ?? 0) !== 0;
139
- return [{
183
+ return [ordered({
140
184
  id: "process:result",
141
185
  ts: typeof read.value.completedAt === "string"
142
186
  ? read.value.completedAt
@@ -146,13 +190,13 @@ function resultItems(stateDir) {
146
190
  summary: failed ? "Run process failed" : "Run process completed",
147
191
  ...(failed ? { level: "error" } : {}),
148
192
  detail: redact(read.value),
149
- }, ...diagnosticItems(read.diagnostics)];
193
+ }, "process:result", 1), ...diagnosticItems(read.diagnostics).map((item, index) => ordered(item, "process:result", index + 2))];
150
194
  }
151
195
  function artifactItems(stateDir, declarations) {
152
196
  const manifest = resolveArtifactManifest(declarations);
153
197
  if (!manifest)
154
198
  return [];
155
- return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(([name, artifact]) => ({
199
+ return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(([name, artifact], index) => ordered({
156
200
  id: `artifact:${name}`,
157
201
  ts: artifact.exists && existsSync(artifact.path)
158
202
  ? statSync(artifact.path).mtime.toISOString()
@@ -167,7 +211,7 @@ function artifactItems(stateDir, declarations) {
167
211
  required: artifact.required,
168
212
  ...(artifact.size !== undefined ? { size: artifact.size } : {}),
169
213
  }),
170
- }));
214
+ }, "artifact", index + 1));
171
215
  }
172
216
  export function projectRunTrace(stateDir, options = {}) {
173
217
  const source = options.source ?? "all";
@@ -179,7 +223,9 @@ export function projectRunTrace(stateDir, options = {}) {
179
223
  ...resultItems(stateDir),
180
224
  ...artifactItems(stateDir, options.artifacts),
181
225
  ]
182
- .filter((item) => source === "all" || item.source === source)
183
- .sort((left, right) => right.ts.localeCompare(left.ts) || right.id.localeCompare(left.id));
184
- return items.slice(0, Math.max(1, Math.min(options.limit ?? 100, 200)));
226
+ .filter(({ item }) => source === "all" || item.source === source)
227
+ .sort(compareTraceItems);
228
+ return items
229
+ .slice(0, Math.max(1, Math.min(options.limit ?? 100, 200)))
230
+ .map(({ item }) => item);
185
231
  }
@@ -15,6 +15,11 @@ const conformanceSuites = [
15
15
  "tests/control.test.ts",
16
16
  "tests/runs-controls.test.ts",
17
17
  "tests/runs-trace.test.ts",
18
+ "tests/observability.test.ts",
19
+ "tests/async-run-control.test.ts",
20
+ "tests/run-kernel-dogfood.test.ts",
21
+ "tests/installed.scripts.test.ts",
22
+ "tests/inspector-overlay.test.ts",
18
23
  "tests/tools-inspect-kernel.test.ts",
19
24
  "tests/recipes-discovery.test.ts",
20
25
  "tests/review-swarm-dogfood.test.ts",
@@ -11,7 +11,6 @@
11
11
  import { spawnSync } from "node:child_process";
12
12
  import { createHash } from "node:crypto";
13
13
  import {
14
- appendFileSync,
15
14
  existsSync,
16
15
  mkdirSync,
17
16
  readFileSync,
@@ -32,8 +31,12 @@ async function importRuntimeModule(name) {
32
31
  return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
33
32
  }
34
33
 
35
- const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
36
- await importRuntimeModule("file-state");
34
+ const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } = await importRuntimeModule("file-state");
35
+ const { readJsonlFileResilient } = await importRuntimeModule("state-readers");
36
+ const LOCKER_JOURNAL_MAX_RECORDS = 512;
37
+ const LOCKER_JOURNAL_MAX_BYTES = 1024 * 1024;
38
+ const { claimRunControlByIdInStateDir, updateRunControlStatusInStateDir } =
39
+ await importRuntimeModule("runs-controls");
37
40
  const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
38
41
 
39
42
  function parseArgs(argv) {
@@ -59,7 +62,6 @@ async function runLocker(argv = process.argv.slice(2)) {
59
62
  const queuePath = join(stateDir, "queue.json");
60
63
  const locksPath = join(stateDir, "locks.json");
61
64
  const journalPath = join(stateDir, "journal.jsonl");
62
- const controlsPath = join(stateDir, "controls.jsonl");
63
65
  const controlPath = join(stateDir, "control.fifo");
64
66
  let runInstanceId;
65
67
  mkdirSync(stateDir, { recursive: true });
@@ -96,10 +98,22 @@ async function runLocker(argv = process.argv.slice(2)) {
96
98
  }
97
99
 
98
100
  function journal(event, data = {}) {
99
- appendFileSync(
100
- journalPath,
101
- `${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`,
102
- );
101
+ const release = acquireFileMutationLock(journalPath);
102
+ try {
103
+ const records = [...readJsonlFileResilient(journalPath).records,
104
+ { event, ts: new Date().toISOString(), ...data }]
105
+ .slice(-LOCKER_JOURNAL_MAX_RECORDS);
106
+ let content = encodeJournal(records);
107
+ while (records.length && Buffer.byteLength(content) > LOCKER_JOURNAL_MAX_BYTES) {
108
+ records.shift();
109
+ content = encodeJournal(records);
110
+ }
111
+ writeTextAtomic(journalPath, content);
112
+ } finally { release(); }
113
+ }
114
+
115
+ function encodeJournal(records) {
116
+ return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
103
117
  }
104
118
 
105
119
  function emitTrace(kind, summary, data = {}, level = "info") {
@@ -147,69 +161,6 @@ async function runLocker(argv = process.argv.slice(2)) {
147
161
  }
148
162
  }
149
163
 
150
- function acquireControlsLock() {
151
- return acquireFileMutationLock(controlsPath);
152
- }
153
-
154
- function readControls() {
155
- if (!existsSync(controlsPath)) return [];
156
- return readFileSync(controlsPath, "utf8")
157
- .split("\n")
158
- .filter((line) => line.trim())
159
- .map((line) => {
160
- try {
161
- return JSON.parse(line);
162
- } catch {
163
- return undefined;
164
- }
165
- })
166
- .filter(Boolean);
167
- }
168
-
169
- function writeControls(controls) {
170
- writeTextAtomic(
171
- controlsPath,
172
- controls.length
173
- ? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
174
- : "",
175
- );
176
- }
177
-
178
- function claimControl(id) {
179
- const release = acquireControlsLock();
180
- try {
181
- const controls = readControls();
182
- const control = controls.find((item) => item.id === id);
183
- if (
184
- !control ||
185
- control.run_instance_id !== runInstanceId ||
186
- (control.status !== "queued" && control.status !== "delivered")
187
- ) {
188
- return undefined;
189
- }
190
- control.claimed_at = new Date().toISOString();
191
- control.status = "claimed";
192
- writeControls(controls);
193
- return { ...control };
194
- } finally {
195
- release();
196
- }
197
- }
198
-
199
- function finalizeControl(id, status, error) {
200
- const release = acquireControlsLock();
201
- try {
202
- const controls = readControls();
203
- const control = controls.find((item) => item.id === id);
204
- if (!control || control.status !== "claimed") return;
205
- control.status = status;
206
- control[`${status}_at`] = new Date().toISOString();
207
- if (error) control.error = error;
208
- writeControls(controls);
209
- } finally {
210
- release();
211
- }
212
- }
213
164
 
214
165
  function tailJournal(count) {
215
166
  if (!existsSync(journalPath)) return [];
@@ -397,7 +348,9 @@ async function runLocker(argv = process.argv.slice(2)) {
397
348
  function handleLine(line) {
398
349
  const control = normalizeControl(line);
399
350
  if (!control) return false;
400
- const claimed = claimControl(control.id);
351
+ const claimed = claimRunControlByIdInStateDir(
352
+ stateDir, runInstanceId, control.id,
353
+ );
401
354
  if (!claimed) {
402
355
  emitTrace(
403
356
  "lock.control_rejected",
@@ -409,11 +362,15 @@ async function runLocker(argv = process.argv.slice(2)) {
409
362
  }
410
363
  try {
411
364
  const stopping = handle(claimed) === true;
412
- finalizeControl(claimed.id, "handled");
365
+ updateRunControlStatusInStateDir(
366
+ stateDir, claimed.id, "handled", {}, ["claimed"],
367
+ );
413
368
  return stopping;
414
369
  } catch (error) {
415
370
  const text = error instanceof Error ? error.message : String(error);
416
- finalizeControl(claimed.id, "failed", text);
371
+ updateRunControlStatusInStateDir(
372
+ stateDir, claimed.id, "failed", { error: text }, ["claimed"],
373
+ );
417
374
  journal("lock.error", { error: text });
418
375
  emitTrace("lock.error", text, { error: text }, "error");
419
376
  return false;