@llblab/pi-actors 0.43.0 → 0.43.1

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 (95) hide show
  1. package/AGENTS.md +12 -6
  2. package/BACKLOG.md +189 -1
  3. package/CHANGELOG.md +180 -301
  4. package/README.md +9 -7
  5. package/dist/index.js +1 -1
  6. package/dist/lib/async-runs.d.ts +2 -1
  7. package/dist/lib/async-runs.js +16 -4
  8. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  9. package/dist/lib/automatic-review-runtime.js +5 -5
  10. package/dist/lib/command-templates.d.ts +2 -0
  11. package/dist/lib/command-templates.js +38 -4
  12. package/dist/lib/control-projection.d.ts +20 -0
  13. package/dist/lib/control-projection.js +66 -0
  14. package/dist/lib/control.d.ts +3 -0
  15. package/dist/lib/control.js +27 -14
  16. package/dist/lib/draft-sleep.js +3 -3
  17. package/dist/lib/file-state.d.ts +1 -0
  18. package/dist/lib/file-state.js +98 -42
  19. package/dist/lib/inspector-overlay.d.ts +2 -0
  20. package/dist/lib/inspector-overlay.js +92 -53
  21. package/dist/lib/limits.d.ts +5 -3
  22. package/dist/lib/limits.js +5 -3
  23. package/dist/lib/prompts.d.ts +1 -1
  24. package/dist/lib/prompts.js +1 -1
  25. package/dist/lib/recipe-control.js +6 -2
  26. package/dist/lib/review-control.d.ts +1 -1
  27. package/dist/lib/review-control.js +4 -5
  28. package/dist/lib/runs-control-delivery.d.ts +8 -1
  29. package/dist/lib/runs-control-delivery.js +38 -15
  30. package/dist/lib/runs-controls.d.ts +2 -0
  31. package/dist/lib/runs-controls.js +5 -3
  32. package/dist/lib/runs-trace.d.ts +2 -2
  33. package/dist/lib/runs-trace.js +24 -20
  34. package/dist/lib/runtime-identity.d.ts +7 -0
  35. package/dist/lib/runtime-identity.js +35 -0
  36. package/dist/lib/runtime-triage.d.ts +29 -0
  37. package/dist/lib/runtime-triage.js +76 -0
  38. package/dist/lib/tool-review-scheduler.js +7 -7
  39. package/dist/lib/tools-inspect.js +53 -14
  40. package/dist/lib/tools-message.d.ts +1 -2
  41. package/dist/lib/tools-message.js +6 -6
  42. package/dist/lib/tools-response.d.ts +0 -1
  43. package/dist/lib/tools-response.js +0 -9
  44. package/dist/lib/tools.d.ts +1 -1
  45. package/dist/lib/tools.js +1 -1
  46. package/dist/lib/trace-projection.js +30 -10
  47. package/dist/scripts/locker.mjs +9 -16
  48. package/dist/scripts/music-player.mjs +7 -13
  49. package/dist/scripts/release-gates.mjs +33 -2
  50. package/dist/scripts/validate-recipe.mjs +5 -4
  51. package/dist/skills/actors/SKILL.md +12 -7
  52. package/dist/skills/swarm/SKILL.md +0 -2
  53. package/docs/0.43-baseline.md +18 -23
  54. package/docs/README.md +2 -4
  55. package/docs/actor-inspector.md +5 -5
  56. package/docs/async-runs.md +2 -2
  57. package/docs/command-templates.md +6 -116
  58. package/docs/recipe-library.md +2 -4
  59. package/docs/releasing.md +28 -0
  60. package/docs/template-recipes.md +1 -1
  61. package/docs/tool-registry.md +2 -2
  62. package/index.ts +1 -1
  63. package/lib/async-runs.ts +18 -5
  64. package/lib/automatic-review-runtime.ts +7 -7
  65. package/lib/command-templates.ts +44 -4
  66. package/lib/control-projection.ts +105 -0
  67. package/lib/control.ts +33 -18
  68. package/lib/draft-sleep.ts +3 -3
  69. package/lib/file-state.ts +68 -61
  70. package/lib/inspector-overlay.ts +93 -58
  71. package/lib/limits.ts +5 -3
  72. package/lib/prompts.ts +1 -1
  73. package/lib/recipe-control.ts +9 -2
  74. package/lib/review-control.ts +4 -5
  75. package/lib/runs-control-delivery.ts +45 -17
  76. package/lib/runs-controls.ts +12 -3
  77. package/lib/runs-trace.ts +23 -19
  78. package/lib/runtime-identity.ts +39 -0
  79. package/lib/runtime-triage.ts +120 -0
  80. package/lib/tool-review-scheduler.ts +7 -7
  81. package/lib/tools-inspect.ts +60 -16
  82. package/lib/tools-message.ts +7 -8
  83. package/lib/tools-response.ts +0 -12
  84. package/lib/tools.ts +4 -4
  85. package/lib/trace-projection.ts +33 -10
  86. package/package.json +1 -1
  87. package/scripts/locker.mjs +9 -16
  88. package/scripts/music-player.mjs +7 -13
  89. package/scripts/release-gates.mjs +33 -2
  90. package/scripts/validate-recipe.mjs +5 -4
  91. package/skills/actors/SKILL.md +12 -7
  92. package/skills/swarm/SKILL.md +0 -2
  93. package/docs/actors-deep-reference.md +0 -108
  94. package/docs/component-recipes.md +0 -45
  95. package/docs/task-first-recipes.md +0 -261
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Pure runtime Control triage policy.
3
+ * Zones: pending admission, status-age classification, terminal/replacement staleness
4
+ * Owns Control triage decisions; journal reads and owner filtering stay in adapters, while safe fields come from control-projection.
5
+ */
6
+
7
+ import * as ControlProjection from "./control-projection.ts";
8
+ import type { RunControlRecord, RunControlStatus } from "./runs-controls.ts";
9
+
10
+ export const RUNTIME_CONTROL_STALE_AFTER_MS = 5 * 60 * 1_000;
11
+
12
+ const PENDING_STATUSES = new Set<RunControlStatus>([
13
+ "queued",
14
+ "delivered",
15
+ "claimed",
16
+ ]);
17
+ const TERMINAL_STATUSES = new Set<RunControlStatus>(["handled", "failed"]);
18
+
19
+ export interface RuntimeTriageRun {
20
+ run: string;
21
+ runInstanceId?: string;
22
+ status: string;
23
+ }
24
+
25
+ export interface RuntimePendingControl
26
+ extends ControlProjection.ProjectedRunControl {
27
+ age_ms: number;
28
+ reason: "age_threshold_reached" | "run_generation_replaced" | "run_generation_unavailable" | "run_terminal" | "within_age_threshold";
29
+ run: string;
30
+ status: "queued" | "delivered" | "claimed";
31
+ status_at: string;
32
+ }
33
+
34
+ export interface RuntimeControlDiagnostic {
35
+ reason: "invalid_action" | "invalid_control_id" | "invalid_generation" | "invalid_record" | "invalid_status" | "invalid_status_timestamp";
36
+ run: string;
37
+ }
38
+
39
+ export interface RuntimeControlClassification {
40
+ diagnostic?: RuntimeControlDiagnostic;
41
+ pending?: RuntimePendingControl;
42
+ stale?: RuntimePendingControl;
43
+ }
44
+
45
+ function diagnostic(
46
+ run: string,
47
+ reason: RuntimeControlDiagnostic["reason"],
48
+ ): RuntimeControlClassification {
49
+ return { diagnostic: { reason, run } };
50
+ }
51
+
52
+ function statusTimestamp(
53
+ record: Record<string, unknown>,
54
+ status: RunControlStatus,
55
+ ): unknown {
56
+ if (status === "failed") return record.failed_at;
57
+ if (status === "handled") return record.handled_at;
58
+ if (status === "claimed") return record.claimed_at;
59
+ if (status === "delivered") return record.delivered_at;
60
+ return record.queued_at;
61
+ }
62
+
63
+ export function classifyRuntimeControl(
64
+ run: RuntimeTriageRun,
65
+ value: unknown,
66
+ nowMs: number,
67
+ ): RuntimeControlClassification {
68
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
69
+ return diagnostic(run.run, "invalid_record");
70
+ }
71
+ const record = value as Record<string, unknown>;
72
+ const status = record.status as RunControlStatus;
73
+ if (!PENDING_STATUSES.has(status) && !TERMINAL_STATUSES.has(status)) {
74
+ return diagnostic(run.run, "invalid_status");
75
+ }
76
+ if (typeof record.id !== "string" || !record.id) {
77
+ return diagnostic(run.run, "invalid_control_id");
78
+ }
79
+ if (typeof record.action !== "string" || !record.action) {
80
+ return diagnostic(run.run, "invalid_action");
81
+ }
82
+ if (typeof record.run_instance_id !== "string" || !record.run_instance_id) {
83
+ return diagnostic(run.run, "invalid_generation");
84
+ }
85
+ const timestamp = statusTimestamp(record, status);
86
+ const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
87
+ if (!Number.isFinite(timestampMs)) {
88
+ return diagnostic(run.run, "invalid_status_timestamp");
89
+ }
90
+ if (TERMINAL_STATUSES.has(status)) return {};
91
+ const ageMs = Math.max(0, Math.floor(nowMs - timestampMs));
92
+ const staleReason: RuntimePendingControl["reason"] | undefined =
93
+ !run.runInstanceId
94
+ ? "run_generation_unavailable"
95
+ : record.run_instance_id !== run.runInstanceId
96
+ ? "run_generation_replaced"
97
+ : run.status !== "running"
98
+ ? "run_terminal"
99
+ : ageMs >= RUNTIME_CONTROL_STALE_AFTER_MS
100
+ ? "age_threshold_reached"
101
+ : undefined;
102
+ const projected = ControlProjection.projectRunControl(
103
+ record as unknown as RunControlRecord,
104
+ );
105
+ const pending: RuntimePendingControl = {
106
+ ...projected,
107
+ age_ms: ageMs,
108
+ reason: staleReason ?? "within_age_threshold",
109
+ run: run.run,
110
+ status: status as RuntimePendingControl["status"],
111
+ status_at: statusTimestamp(
112
+ projected as unknown as Record<string, unknown>,
113
+ status,
114
+ ) as string,
115
+ };
116
+ return {
117
+ pending,
118
+ ...(staleReason ? { stale: pending } : {}),
119
+ };
120
+ }
@@ -404,7 +404,7 @@ export function applyApprovedToolReviewAtSessionBoundary(
404
404
  ...state,
405
405
  failedStage: "approval_state",
406
406
  lastError: "approved review path is unavailable",
407
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
407
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
408
408
  phase: "processing_failed" as const,
409
409
  updatedAt: (options.now ?? (() => new Date()))().toISOString(),
410
410
  };
@@ -427,7 +427,7 @@ export function applyApprovedToolReviewAtSessionBoundary(
427
427
  ...state,
428
428
  failedStage: "transaction",
429
429
  lastError: `tool review transaction remained ${transaction.phase}`,
430
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
430
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
431
431
  phase: "processing_failed" as const,
432
432
  updatedAt: (options.now ?? (() => new Date()))().toISOString(),
433
433
  };
@@ -458,7 +458,7 @@ export function applyApprovedToolReviewAtSessionBoundary(
458
458
  ...pending,
459
459
  failedStage: "lineage",
460
460
  lastError: error instanceof Error ? error.message : String(error),
461
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
461
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
462
462
  };
463
463
  writeJsonAtomic(statePath, failedPending);
464
464
  return { outcome: "lineage_pending", state: failedPending };
@@ -478,7 +478,7 @@ export function applyApprovedToolReviewAtSessionBoundary(
478
478
  ...state,
479
479
  failedStage: "transaction",
480
480
  lastError: error instanceof Error ? error.message : String(error),
481
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
481
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
482
482
  phase: "processing_failed" as const,
483
483
  updatedAt: (options.now ?? (() => new Date()))().toISOString(),
484
484
  };
@@ -541,7 +541,7 @@ export function createToolReviewScheduler(
541
541
  ...existing,
542
542
  failedStage: processed.stage ?? "review_result",
543
543
  lastError: processed.error ?? "automatic tool reviewer failed",
544
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
544
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
545
545
  phase: processed.outcome === "processing_failed"
546
546
  ? "processing_failed"
547
547
  : "failed",
@@ -577,7 +577,7 @@ export function createToolReviewScheduler(
577
577
  attempts: existing.attempts + 1,
578
578
  failedStage: "review_launch",
579
579
  lastError: error instanceof Error ? error.message : String(error),
580
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
580
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
581
581
  phase: "failed",
582
582
  updatedAt: now().toISOString(),
583
583
  } satisfies ToolReviewAdmissionState);
@@ -612,7 +612,7 @@ export function createToolReviewScheduler(
612
612
  failedStage: "review_launch",
613
613
  inputPath: batch.inputPath,
614
614
  lastError: error instanceof Error ? error.message : String(error),
615
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"tool\"}",
615
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"tool\"}",
616
616
  phase: "failed",
617
617
  reviewerInputPath: batch.reviewerInputPath,
618
618
  reviewId: batch.reviewId,
@@ -7,6 +7,7 @@
7
7
  import { join } from "node:path";
8
8
 
9
9
  import * as AsyncRuns from "./async-runs.ts";
10
+ import * as ControlProjection from "./control-projection.ts";
10
11
  import * as Inspector from "./inspector.ts";
11
12
  import * as Limits from "./limits.ts";
12
13
  import * as Paths from "./paths.ts";
@@ -15,6 +16,8 @@ import * as ReviewDiagnostics from "./review-diagnostics.ts";
15
16
  import * as RunsControls from "./runs-controls.ts";
16
17
  import * as RunControlDelivery from "./runs-control-delivery.ts";
17
18
  import * as RunsTrace from "./runs-trace.ts";
19
+ import * as RuntimeIdentity from "./runtime-identity.ts";
20
+ import * as RuntimeTriage from "./runtime-triage.ts";
18
21
  import * as Schema from "./schema.ts";
19
22
  import * as ToolsAccess from "./tools-access.ts";
20
23
  import * as ToolsResponse from "./tools-response.ts";
@@ -35,7 +38,8 @@ function runtimeStatus(): Record<string, unknown> {
35
38
  return {
36
39
  automatic_review: process.env.PI_ACTORS_AUTOMATIC_REVIEW !== "off",
37
40
  run_root: Paths.getRunStateRoot(),
38
- version: process.env.npm_package_version ?? "unknown",
41
+ state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
42
+ version: RuntimeIdentity.getPackageVersion(),
39
43
  };
40
44
  }
41
45
 
@@ -69,13 +73,39 @@ function runtimeTriage(
69
73
  const inventory = runtimeRuns(ctx, deps, undefined);
70
74
  const runs = inventory.runs as Array<Record<string, unknown>>;
71
75
  const failed = runs.filter((run) => run.status === "failed");
72
- const staleControls = runs.flatMap((run) => {
76
+ const pendingControls: RuntimeTriage.RuntimePendingControl[] = [];
77
+ const staleControls: RuntimeTriage.RuntimePendingControl[] = [];
78
+ const controlDiagnostics: Array<Record<string, unknown>> = [];
79
+ const nowMs = Date.now();
80
+ for (const run of runs) {
73
81
  const stateDir = typeof run.state_dir === "string" ? run.state_dir : undefined;
74
- if (!stateDir) return [];
75
- return RunsControls.readRunControlsFromStateDir(stateDir)
76
- .filter((control) => control.status === "queued" || control.status === "claimed")
77
- .map((control) => ({ run: run.run, control }));
78
- });
82
+ if (!stateDir) continue;
83
+ const runId = String(run.run ?? "unknown");
84
+ const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
85
+ controlDiagnostics.push(...journal.diagnostics.map((diagnostic) => ({
86
+ ...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
87
+ reason: diagnostic.line === undefined
88
+ ? "unreadable_control_journal"
89
+ : "invalid_control_json",
90
+ run: runId,
91
+ })));
92
+ for (const control of journal.records) {
93
+ const classified = RuntimeTriage.classifyRuntimeControl(
94
+ {
95
+ run: runId,
96
+ ...(typeof run.run_instance_id === "string"
97
+ ? { runInstanceId: run.run_instance_id }
98
+ : {}),
99
+ status: String(run.status ?? "unknown"),
100
+ },
101
+ control,
102
+ nowMs,
103
+ );
104
+ if (classified.pending) pendingControls.push(classified.pending);
105
+ if (classified.stale) staleControls.push(classified.stale);
106
+ if (classified.diagnostic) controlDiagnostics.push({ ...classified.diagnostic });
107
+ }
108
+ }
79
109
  const attentionEvents = runs.flatMap((run) => {
80
110
  const stateDir = typeof run.state_dir === "string" ? run.state_dir : undefined;
81
111
  if (!stateDir) return [];
@@ -90,18 +120,31 @@ function runtimeTriage(
90
120
  ts: event.ts,
91
121
  }));
92
122
  });
123
+ const boundedStaleControls = staleControls.slice(0, 40);
124
+ const staleKeys = new Set(boundedStaleControls.map((control) =>
125
+ `${control.run}\0${control.run_instance_id}\0${control.id}`,
126
+ ));
127
+ const boundedPendingControls = [
128
+ ...boundedStaleControls,
129
+ ...pendingControls.filter((control) =>
130
+ !staleKeys.has(`${control.run}\0${control.run_instance_id}\0${control.id}`),
131
+ ),
132
+ ].slice(0, 40);
93
133
  return {
94
134
  ...inventory,
95
135
  failed_runs: failed,
96
- stale_controls: staleControls.slice(0, 40),
136
+ pending_control_count: pendingControls.length,
137
+ pending_controls: boundedPendingControls,
138
+ stale_control_count: staleControls.length,
139
+ stale_controls: boundedStaleControls,
140
+ control_diagnostic_count: controlDiagnostics.length,
141
+ control_diagnostics: controlDiagnostics.slice(0, 40),
97
142
  attention_events: attentionEvents.slice(-40).reverse(),
98
143
  next_actions: [
99
144
  ...(failed.length ? ["inspect target=runtime view=runs status=failed"] : []),
100
- ...(staleControls.length
101
- ? staleControls.slice(0, 3).map((entry) =>
102
- `inspect target=run:${String(entry.run)} view=control`,
103
- )
104
- : []),
145
+ ...staleControls.slice(0, 3).map((entry) =>
146
+ `inspect target=run:${entry.run} view=control`,
147
+ ),
105
148
  ...(attentionEvents.length
106
149
  ? attentionEvents.slice(-3).map((entry) =>
107
150
  `inspect target=run:${String(entry.run)} view=trace source=runtime`,
@@ -116,9 +159,9 @@ function compactRuntime(view: string, details: Record<string, unknown>): string
116
159
  return `\nruntime runs=${(details.runs as unknown[] | undefined)?.length ?? 0}`;
117
160
  }
118
161
  if (view === "triage") {
119
- return `\nruntime failed=${(details.failed_runs as unknown[] | undefined)?.length ?? 0} stale_controls=${(details.stale_controls as unknown[] | undefined)?.length ?? 0} attention=${(details.attention_events as unknown[] | undefined)?.length ?? 0}`;
162
+ return `\nruntime failed=${(details.failed_runs as unknown[] | undefined)?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} attention=${(details.attention_events as unknown[] | undefined)?.length ?? 0}`;
120
163
  }
121
- return `\nruntime version=${String(details.version ?? "unknown")} automatic_review=${String(details.automatic_review)}`;
164
+ return `\nruntime version=${String(details.version)} state_schema=${String(details.state_schema)} automatic_review=${String(details.automatic_review)}`;
122
165
  }
123
166
 
124
167
  function inspectRuntime(
@@ -232,7 +275,8 @@ function inspectRun(
232
275
  : undefined,
233
276
  recent_controls: RunsControls.readRunControlsFromStateDir(stateDir)
234
277
  .slice(-Math.max(1, Number(input.lines || 20)))
235
- .reverse(),
278
+ .reverse()
279
+ .map(ControlProjection.projectRunControl),
236
280
  };
237
281
  }
238
282
 
@@ -46,8 +46,7 @@ function runNextActions(run: string, result: Record<string, unknown>): string[]
46
46
  }
47
47
 
48
48
  export interface ControlToolDeps {
49
- getTool?: (name: string) => unknown;
50
- handleRuntimeMessage?: (
49
+ handleRuntimeControl?: (
51
50
  action: string,
52
51
  input: unknown,
53
52
  ) => Record<string, unknown>;
@@ -64,11 +63,11 @@ export function createControlToolDefinition<TContext = unknown>(
64
63
  parameters: Schema.objectSchema(
65
64
  {
66
65
  target: Schema.stringSchema("Run target run:<id>, or literal runtime."),
67
- action: Schema.stringSchema("Lowercase semantic Control action."),
66
+ action: Schema.stringSchema("Lowercase ASCII Control action, at most 64 characters."),
68
67
  input: Schema.unionSchema([
69
- Schema.stringSchema("Optional Control input string."),
70
- Schema.looseObjectSchema("Optional structured Control input."),
71
- Schema.arraySchema("Optional structured Control input array."),
68
+ Schema.stringSchema("Optional Control input string; serialized JSON must fit 380 bytes."),
69
+ Schema.looseObjectSchema("Optional structured Control input; serialized JSON must fit 380 bytes."),
70
+ Schema.arraySchema("Optional structured Control input array; serialized JSON must fit 380 bytes."),
72
71
  ]),
73
72
  verbose: Schema.booleanSchema("Return full JSON instead of compact text."),
74
73
  },
@@ -89,10 +88,10 @@ export function createControlToolDefinition<TContext = unknown>(
89
88
  `unsupported runtime Control action: ${request.action}; use review.retry or review.reset`,
90
89
  );
91
90
  }
92
- if (!deps.handleRuntimeMessage) {
91
+ if (!deps.handleRuntimeControl) {
93
92
  throw new Error("runtime Control is unavailable");
94
93
  }
95
- result = deps.handleRuntimeMessage(request.action, request.input);
94
+ result = deps.handleRuntimeControl(request.action, request.input);
96
95
  } else {
97
96
  const run = request.target.slice(4);
98
97
  const status = ToolsAccess.assertRunAccessibleToContext(run, ctx);
@@ -133,18 +133,6 @@ export function compactAsyncRunStatus(value: unknown): string {
133
133
  return `\n${tokens.join(" ")}`;
134
134
  }
135
135
 
136
- export function artifactNextActions(
137
- run: unknown,
138
- artifacts: Record<string, unknown>,
139
- ): string[] {
140
- const id = String(run ?? "").trim();
141
- if (!id || Object.keys(artifacts).length === 0) return [];
142
- return [
143
- `inspect target=run:${id} view=artifacts verbose=true`,
144
- `inspect target=run:${id} view=messages`,
145
- ];
146
- }
147
-
148
136
  export function maybeJsonText(
149
137
  value: unknown,
150
138
  verbose: boolean | undefined,
package/lib/tools.ts CHANGED
@@ -25,9 +25,9 @@ export interface CoreActorToolDefinitionDeps<
25
25
  configPath: string;
26
26
  getActiveTools: () => string[];
27
27
  getRuntimeTool: (name: string) => unknown;
28
- handleRuntimeMessage?: (
29
- type: string,
30
- body: unknown,
28
+ handleRuntimeControl?: (
29
+ action: string,
30
+ input: unknown,
31
31
  ) => Record<string, unknown>;
32
32
  registryRuntime: Pick<
33
33
  RegisterToolRuntimeDeps<TContext>,
@@ -74,7 +74,7 @@ export function createCoreActorToolDefinitions<
74
74
  }),
75
75
  ToolsSpawn.createSpawnToolDefinition<TContext>(),
76
76
  ToolsMessage.createControlToolDefinition<TContext>({
77
- handleRuntimeMessage: deps.handleRuntimeMessage,
77
+ handleRuntimeControl: deps.handleRuntimeControl,
78
78
  }),
79
79
  ToolsInspect.createInspectToolDefinition<TContext>({
80
80
  getTool: (name) => deps.getRuntimeTool(name),
@@ -7,6 +7,7 @@
7
7
  import { existsSync, statSync } from "node:fs";
8
8
  import { join, relative } from "node:path";
9
9
 
10
+ import * as ControlProjection from "./control-projection.ts";
10
11
  import * as ExecutionSessions from "./execution-sessions.ts";
11
12
  import * as Limits from "./limits.ts";
12
13
  import {
@@ -78,6 +79,25 @@ function diagnosticItems(diagnostics: StateReadDiagnostic[]): TraceItem[] {
78
79
  }));
79
80
  }
80
81
 
82
+ function controlDiagnosticItems(
83
+ diagnostics: StateReadDiagnostic[],
84
+ ): TraceItem[] {
85
+ return diagnostics.slice(-SOURCE_LIMIT).map((diagnostic, index) => ({
86
+ id: `control-diagnostic:${diagnostic.line ?? 0}:${index}`,
87
+ ts: new Date(0).toISOString(),
88
+ source: "runtime",
89
+ kind: "state.read_error",
90
+ summary: "Malformed or unreadable Control evidence",
91
+ level: "warning",
92
+ detail: {
93
+ ...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
94
+ reason: diagnostic.line === undefined
95
+ ? "unreadable_control_journal"
96
+ : "invalid_control_json",
97
+ },
98
+ }));
99
+ }
100
+
81
101
  function traceEventItems(stateDir: string): TraceItem[] {
82
102
  const read = readJsonlFileResilient<TraceEvent>(join(stateDir, "trace.jsonl"));
83
103
  return [
@@ -102,16 +122,19 @@ function controlItems(stateDir: string): TraceItem[] {
102
122
  join(stateDir, "controls.jsonl"),
103
123
  );
104
124
  return [
105
- ...read.records.slice(-SOURCE_LIMIT).map((control) => ({
106
- id: `control:${control.id}`,
107
- ts: latestControlTimestamp(control),
108
- source: "control" as const,
109
- kind: `control.${control.status}`,
110
- summary: `${control.action} ${control.status}`,
111
- ...(control.status === "failed" ? { level: "error" as const } : {}),
112
- detail: redact(control),
113
- })),
114
- ...diagnosticItems(read.diagnostics),
125
+ ...read.records.slice(-SOURCE_LIMIT).map((control) => {
126
+ const projected = ControlProjection.projectRunControl(control);
127
+ return {
128
+ id: `control:${projected.id}`,
129
+ ts: latestControlTimestamp(projected),
130
+ source: "control" as const,
131
+ kind: `control.${projected.status}`,
132
+ summary: `${projected.action} ${projected.status}`,
133
+ ...(projected.status === "failed" ? { level: "error" as const } : {}),
134
+ detail: projected,
135
+ };
136
+ }),
137
+ ...controlDiagnosticItems(read.diagnostics),
115
138
  ];
116
139
  }
117
140
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.43.0",
3
+ "version": "0.43.1",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -9,13 +9,12 @@
9
9
  */
10
10
 
11
11
  import { spawnSync } from "node:child_process";
12
- import { createHash, randomUUID } from "node:crypto";
12
+ import { createHash } from "node:crypto";
13
13
  import {
14
14
  appendFileSync,
15
15
  existsSync,
16
16
  mkdirSync,
17
17
  readFileSync,
18
- rmSync,
19
18
  } from "node:fs";
20
19
  import { createServer } from "node:net";
21
20
  import { dirname, join, resolve } from "node:path";
@@ -35,6 +34,7 @@ async function importRuntimeModule(name) {
35
34
 
36
35
  const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
37
36
  await importRuntimeModule("file-state");
37
+ const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
38
38
 
39
39
  function parseArgs(argv) {
40
40
  const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
@@ -59,7 +59,6 @@ async function runLocker(argv = process.argv.slice(2)) {
59
59
  const queuePath = join(stateDir, "queue.json");
60
60
  const locksPath = join(stateDir, "locks.json");
61
61
  const journalPath = join(stateDir, "journal.jsonl");
62
- const tracePath = join(stateDir, "trace.jsonl");
63
62
  const controlsPath = join(stateDir, "controls.jsonl");
64
63
  const controlPath = join(stateDir, "control.fifo");
65
64
  let runInstanceId;
@@ -104,18 +103,13 @@ async function runLocker(argv = process.argv.slice(2)) {
104
103
  }
105
104
 
106
105
  function emitTrace(kind, summary, data = {}, level = "info") {
107
- appendFileSync(
108
- tracePath,
109
- `${JSON.stringify({
110
- attention: "followup",
111
- data,
112
- id: randomUUID(),
113
- kind,
114
- level,
115
- summary,
116
- ts: new Date().toISOString(),
117
- })}\n`,
118
- );
106
+ appendRunTraceEvent(stateDir, {
107
+ attention: "followup",
108
+ data,
109
+ kind,
110
+ level,
111
+ summary,
112
+ });
119
113
  }
120
114
 
121
115
  function now() {
@@ -454,7 +448,6 @@ async function runLocker(argv = process.argv.slice(2)) {
454
448
  }
455
449
 
456
450
  async function serveNamedPipe(endpoint) {
457
- rmSync(endpoint.path, { force: true });
458
451
  let resolveStopped;
459
452
  const stopped = new Promise((resolve) => {
460
453
  resolveStopped = resolve;
@@ -52,6 +52,7 @@ async function importRuntimeModule(name) {
52
52
 
53
53
  const { acquireFileMutationLock, writeTextAtomic } =
54
54
  await importRuntimeModule("file-state");
55
+ const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
55
56
 
56
57
  const AUDIO_EXTENSIONS = new Set([
57
58
  ".aac",
@@ -420,18 +421,12 @@ function readText(path) {
420
421
  }
421
422
 
422
423
  function emitPlayerEvent(ctx, kind, summary, data = {}) {
423
- writeText(
424
- ctx.eventFile,
425
- `${JSON.stringify({
426
- data,
427
- id: randomUUID(),
428
- kind,
429
- level: "info",
430
- summary,
431
- ts: new Date().toISOString(),
432
- })}\n`,
433
- "a",
434
- );
424
+ appendRunTraceEvent(ctx.stateDir, {
425
+ data,
426
+ kind,
427
+ level: "info",
428
+ summary,
429
+ });
435
430
  }
436
431
 
437
432
  function emitTrackEvent(ctx, index, count, track, player) {
@@ -859,7 +854,6 @@ async function playMain(args) {
859
854
  const ctx = {
860
855
  commandFile: join(stateDir, "command.txt"),
861
856
  current: undefined,
862
- eventFile: join(stateDir, "trace.jsonl"),
863
857
  controlsFile: join(stateDir, "controls.jsonl"),
864
858
  pidFile: join(stateDir, "current.pid"),
865
859
  playerControlFile: join(stateDir, "player-control.txt"),
@@ -145,13 +145,44 @@ try {
145
145
  }
146
146
  console.log("[release] removed-surface and legacy-fallback allowlists checked");
147
147
 
148
- const baselineShippedLines = 35_077;
148
+ const directTraceWrite = /(?:appendFileSync|writeFileSync|writeText(?:Atomic)?)\s*\(\s*[A-Za-z0-9_.]*?(?:trace|event)(?:Path|File)/iu;
149
+ for (const path of files.filter((candidate) =>
150
+ (candidate.startsWith("lib/") && candidate.endsWith(".ts")) ||
151
+ (candidate.startsWith("scripts/") && candidate.endsWith(".mjs"))
152
+ )) {
153
+ if (path === "lib/runs-trace.ts") continue;
154
+ check(!directTraceWrite.test(stagedText(path) ?? ""), `direct Trace writer outside canonical authority: ${path}`);
155
+ }
156
+ const canonicalTrace = stagedText("lib/runs-trace.ts") ?? "";
157
+ check(/withFileMutationLock\(path/u.test(canonicalTrace), "canonical Trace append lacks mutation lock");
158
+ for (const path of [
159
+ "scripts/async-runner.mjs",
160
+ "scripts/locker.mjs",
161
+ "scripts/music-player.mjs",
162
+ ]) {
163
+ const text = stagedText(path) ?? "";
164
+ check(
165
+ text.includes('importRuntimeModule("runs-trace")') && text.includes("appendRunTraceEvent"),
166
+ `first-party Trace writer bypasses canonical runtime module: ${path}`,
167
+ );
168
+ }
169
+ console.log("[release] canonical Trace writer residue checked");
170
+
171
+ check(
172
+ (stagedText("scripts/validate-recipe.mjs") ?? "").includes(
173
+ "qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0",
174
+ ),
175
+ "Recipe QA warnings are not release-blocking",
176
+ );
177
+ console.log("[release] zero-warning Recipe QA gate checked");
178
+
179
+ const baselineShippedLines = 28_853;
149
180
  const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
150
181
  const shippedLines = files
151
182
  .filter((path) => shippedPath.test(path))
152
183
  .reduce((total, path) => total + (((stagedText(path) ?? "").match(/\n/gu) ?? []).length + 1), 0);
153
184
  check(shippedLines < baselineShippedLines, `shipped lines ${shippedLines} are not below baseline ${baselineShippedLines}`);
154
- console.log(`[release] shipped lines ${shippedLines} < frozen baseline ${baselineShippedLines}`);
185
+ console.log(`[release] shipped lines ${shippedLines} < released baseline ${baselineShippedLines}`);
155
186
 
156
187
  const sources = files.filter((path) => path === "index.ts" || (path.startsWith("lib/") && path.endsWith(".ts")));
157
188
  const sourceSet = new Set(sources);
@@ -29,7 +29,7 @@ export function validateRecipeUsage() {
29
29
  return `Usage:
30
30
  validate-recipe.mjs <recipe-file-or-dir> [--all] [--qa] [--summary]
31
31
 
32
- Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks. Add --summary for compact CLI output.`;
32
+ Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks, where diagnostics and warnings fail validation. Add --summary for compact CLI output.`;
33
33
  }
34
34
 
35
35
  function expandPath(value) {
@@ -120,8 +120,6 @@ function validateHelperPaths(file, config) {
120
120
  function qaDiagnostics(file, config) {
121
121
  const diagnostics = [];
122
122
  const warnings = [];
123
- if (typeof config.description !== "string" || !config.description.trim())
124
- warnings.push("description: missing or empty");
125
123
  if (config.mailbox !== undefined)
126
124
  diagnostics.push("recipe.mailbox was removed; use control actions and Trace events");
127
125
  diagnostics.push(...validateArtifactDeclarations(config));
@@ -138,7 +136,7 @@ function qaDiagnostics(file, config) {
138
136
  }
139
137
 
140
138
  function qaOk(qaReport) {
141
- return qaReport.diagnostics.length === 0;
139
+ return qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0;
142
140
  }
143
141
 
144
142
  function validateFile(file, qa = false) {
@@ -225,6 +223,9 @@ function summarizeReport(report) {
225
223
  ...(result.qa?.diagnostics?.length
226
224
  ? { diagnostics: result.qa.diagnostics }
227
225
  : {}),
226
+ ...(result.qa?.warnings?.length
227
+ ? { warnings: result.qa.warnings }
228
+ : {}),
228
229
  })),
229
230
  };
230
231
  }