@llblab/pi-actors 0.42.3 → 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 (279) hide show
  1. package/AGENTS.md +127 -175
  2. package/BACKLOG.md +189 -1
  3. package/CHANGELOG.md +183 -292
  4. package/README.md +115 -276
  5. package/dist/fixtures/protocol/control-endpoint.json +6 -0
  6. package/dist/fixtures/protocol/control-record.json +9 -0
  7. package/dist/fixtures/protocol/recipe-summary.json +4 -12
  8. package/dist/fixtures/protocol/trace-event.json +9 -0
  9. package/dist/index.js +1 -1
  10. package/dist/lib/async-runs.d.ts +15 -38
  11. package/dist/lib/async-runs.js +173 -111
  12. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  13. package/dist/lib/automatic-review-runtime.js +5 -5
  14. package/dist/lib/command-templates.d.ts +2 -0
  15. package/dist/lib/command-templates.js +38 -4
  16. package/dist/lib/control-projection.d.ts +20 -0
  17. package/dist/lib/control-projection.js +66 -0
  18. package/dist/lib/control.d.ts +15 -0
  19. package/dist/lib/control.js +97 -0
  20. package/dist/lib/draft-sleep.js +3 -3
  21. package/dist/lib/execution-sessions.d.ts +17 -0
  22. package/dist/lib/execution-sessions.js +85 -0
  23. package/dist/lib/file-state.d.ts +2 -0
  24. package/dist/lib/file-state.js +114 -46
  25. package/dist/lib/inspector-actions.d.ts +2 -2
  26. package/dist/lib/inspector-actions.js +2 -2
  27. package/dist/lib/inspector-command.js +3 -3
  28. package/dist/lib/inspector-overlay.d.ts +54 -70
  29. package/dist/lib/inspector-overlay.js +576 -910
  30. package/dist/lib/inspector.d.ts +3 -71
  31. package/dist/lib/inspector.js +19 -665
  32. package/dist/lib/limits.d.ts +7 -3
  33. package/dist/lib/limits.js +7 -3
  34. package/dist/lib/observability.d.ts +16 -16
  35. package/dist/lib/observability.js +43 -82
  36. package/dist/lib/prompts.d.ts +1 -1
  37. package/dist/lib/prompts.js +3 -3
  38. package/dist/lib/recipe-control.d.ts +7 -0
  39. package/dist/lib/recipe-control.js +43 -0
  40. package/dist/lib/recipes-discovery.js +2 -0
  41. package/dist/lib/recipes-references.d.ts +1 -14
  42. package/dist/lib/recipes-references.js +6 -21
  43. package/dist/lib/review-control.d.ts +1 -1
  44. package/dist/lib/review-control.js +4 -5
  45. package/dist/lib/review-projection.js +1 -5
  46. package/dist/lib/run-ui-runtime.js +2 -2
  47. package/dist/lib/runs-control-delivery.d.ts +28 -0
  48. package/dist/lib/runs-control-delivery.js +150 -0
  49. package/dist/lib/runs-controls.d.ts +37 -0
  50. package/dist/lib/runs-controls.js +146 -0
  51. package/dist/lib/runs-retention.d.ts +7 -0
  52. package/dist/lib/runs-retention.js +27 -3
  53. package/dist/lib/runs-start.js +4 -2
  54. package/dist/lib/runs-status.js +11 -6
  55. package/dist/lib/runs-trace.d.ts +24 -0
  56. package/dist/lib/runs-trace.js +102 -0
  57. package/dist/lib/runtime-identity.d.ts +7 -0
  58. package/dist/lib/runtime-identity.js +35 -0
  59. package/dist/lib/runtime-notifier.d.ts +1 -1
  60. package/dist/lib/runtime-notifier.js +1 -1
  61. package/dist/lib/runtime-triage.d.ts +29 -0
  62. package/dist/lib/runtime-triage.js +76 -0
  63. package/dist/lib/tool-review-scheduler.js +7 -7
  64. package/dist/lib/tools-inspect.d.ts +3 -3
  65. package/dist/lib/tools-inspect.js +241 -707
  66. package/dist/lib/tools-local.js +2 -10
  67. package/dist/lib/tools-message.d.ts +6 -7
  68. package/dist/lib/tools-message.js +95 -396
  69. package/dist/lib/tools-response.d.ts +1 -5
  70. package/dist/lib/tools-response.js +5 -48
  71. package/dist/lib/tools-spawn.js +16 -28
  72. package/dist/lib/tools.d.ts +1 -1
  73. package/dist/lib/tools.js +2 -3
  74. package/dist/lib/trace-projection.d.ts +22 -0
  75. package/dist/lib/trace-projection.js +185 -0
  76. package/dist/recipes/draft-review.json +0 -10
  77. package/dist/recipes/lens-swarm.json +0 -14
  78. package/dist/recipes/music-player.json +10 -19
  79. package/dist/recipes/pipeline-architect-coordinator.json +0 -11
  80. package/dist/recipes/pipeline-artifact-bundle.json +1 -22
  81. package/dist/recipes/pipeline-artifact-report.json +1 -18
  82. package/dist/recipes/pipeline-artifact-write.json +1 -18
  83. package/dist/recipes/pipeline-async-run-ops.json +0 -12
  84. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
  85. package/dist/recipes/pipeline-development-tasking.json +0 -12
  86. package/dist/recipes/pipeline-docs-maintenance.json +0 -12
  87. package/dist/recipes/pipeline-media-library.json +0 -12
  88. package/dist/recipes/pipeline-quorum-review.json +0 -12
  89. package/dist/recipes/pipeline-release-readiness.json +0 -12
  90. package/dist/recipes/pipeline-release-summary.json +0 -12
  91. package/dist/recipes/pipeline-repo-health.json +0 -12
  92. package/dist/recipes/pipeline-research-synthesis.json +0 -11
  93. package/dist/recipes/pipeline-review-readiness.json +0 -12
  94. package/dist/recipes/resource-locker.json +27 -0
  95. package/dist/recipes/subagent-artifact.json +0 -9
  96. package/dist/recipes/subagent-checkpoint.json +0 -10
  97. package/dist/recipes/subagent-conflict-report.json +0 -11
  98. package/dist/recipes/subagent-contradiction-map.json +0 -11
  99. package/dist/recipes/subagent-critic.json +0 -11
  100. package/dist/recipes/subagent-evidence-map.json +0 -11
  101. package/dist/recipes/subagent-followup.json +0 -10
  102. package/dist/recipes/subagent-judge.json +0 -11
  103. package/dist/recipes/subagent-merge.json +0 -11
  104. package/dist/recipes/subagent-normalize.json +0 -11
  105. package/dist/recipes/subagent-plan.json +0 -11
  106. package/dist/recipes/subagent-preflight.json +0 -11
  107. package/dist/recipes/subagent-prompt.json +0 -10
  108. package/dist/recipes/subagent-quorum.json +0 -10
  109. package/dist/recipes/subagent-review-coordinator.json +0 -14
  110. package/dist/recipes/subagent-review.json +0 -11
  111. package/dist/recipes/subagent-task-card.json +0 -11
  112. package/dist/recipes/subagent-tools.json +0 -10
  113. package/dist/recipes/subagent-verify.json +0 -11
  114. package/dist/recipes/subagents-prompts.json +0 -10
  115. package/dist/recipes/tool-review.json +0 -10
  116. package/dist/scripts/async-runner.mjs +25 -25
  117. package/dist/scripts/conformance.mjs +4 -2
  118. package/dist/scripts/locker.mjs +196 -69
  119. package/dist/scripts/music-player.mjs +162 -159
  120. package/dist/scripts/recipe-utils.mjs +6 -96
  121. package/dist/scripts/release-gates.mjs +91 -0
  122. package/dist/scripts/validate-recipe.mjs +8 -57
  123. package/dist/skills/actors/SKILL.md +57 -265
  124. package/dist/skills/swarm/SKILL.md +10 -34
  125. package/docs/0.43-baseline.md +39 -0
  126. package/docs/README.md +4 -6
  127. package/docs/actor-inspector.md +26 -64
  128. package/docs/async-runs.md +81 -328
  129. package/docs/command-templates.md +8 -118
  130. package/docs/recipe-library.md +55 -182
  131. package/docs/releasing.md +28 -0
  132. package/docs/template-recipes.md +76 -289
  133. package/docs/tool-registry.md +41 -161
  134. package/fixtures/protocol/control-endpoint.json +6 -0
  135. package/fixtures/protocol/control-record.json +9 -0
  136. package/fixtures/protocol/recipe-summary.json +4 -12
  137. package/fixtures/protocol/trace-event.json +9 -0
  138. package/index.ts +1 -1
  139. package/lib/async-runs.ts +218 -204
  140. package/lib/automatic-review-runtime.ts +7 -7
  141. package/lib/command-templates.ts +44 -4
  142. package/lib/control-projection.ts +105 -0
  143. package/lib/control.ts +117 -0
  144. package/lib/draft-sleep.ts +3 -3
  145. package/lib/execution-sessions.ts +111 -0
  146. package/lib/file-state.ts +84 -64
  147. package/lib/inspector-actions.ts +2 -2
  148. package/lib/inspector-command.ts +3 -3
  149. package/lib/inspector-overlay.ts +617 -1126
  150. package/lib/inspector.ts +46 -979
  151. package/lib/limits.ts +7 -3
  152. package/lib/observability.ts +60 -101
  153. package/lib/prompts.ts +3 -3
  154. package/lib/recipe-control.ts +52 -0
  155. package/lib/recipes-discovery.ts +2 -0
  156. package/lib/recipes-references.ts +9 -45
  157. package/lib/review-control.ts +4 -5
  158. package/lib/review-projection.ts +1 -5
  159. package/lib/run-ui-runtime.ts +2 -2
  160. package/lib/runs-control-delivery.ts +209 -0
  161. package/lib/runs-controls.ts +213 -0
  162. package/lib/runs-retention.ts +38 -3
  163. package/lib/runs-start.ts +4 -2
  164. package/lib/runs-status.ts +11 -6
  165. package/lib/runs-trace.ts +136 -0
  166. package/lib/runtime-identity.ts +39 -0
  167. package/lib/runtime-notifier.ts +1 -1
  168. package/lib/runtime-triage.ts +120 -0
  169. package/lib/tool-review-scheduler.ts +7 -7
  170. package/lib/tools-inspect.ts +283 -900
  171. package/lib/tools-local.ts +2 -12
  172. package/lib/tools-message.ts +112 -520
  173. package/lib/tools-response.ts +5 -64
  174. package/lib/tools-spawn.ts +16 -32
  175. package/lib/tools.ts +5 -6
  176. package/lib/trace-projection.ts +244 -0
  177. package/package.json +2 -1
  178. package/recipes/draft-review.json +0 -10
  179. package/recipes/lens-swarm.json +0 -14
  180. package/recipes/music-player.json +10 -19
  181. package/recipes/pipeline-architect-coordinator.json +0 -11
  182. package/recipes/pipeline-artifact-bundle.json +1 -22
  183. package/recipes/pipeline-artifact-report.json +1 -18
  184. package/recipes/pipeline-artifact-write.json +1 -18
  185. package/recipes/pipeline-async-run-ops.json +0 -12
  186. package/recipes/pipeline-checkpoint-continuation.json +0 -14
  187. package/recipes/pipeline-development-tasking.json +0 -12
  188. package/recipes/pipeline-docs-maintenance.json +0 -12
  189. package/recipes/pipeline-media-library.json +0 -12
  190. package/recipes/pipeline-quorum-review.json +0 -12
  191. package/recipes/pipeline-release-readiness.json +0 -12
  192. package/recipes/pipeline-release-summary.json +0 -12
  193. package/recipes/pipeline-repo-health.json +0 -12
  194. package/recipes/pipeline-research-synthesis.json +0 -11
  195. package/recipes/pipeline-review-readiness.json +0 -12
  196. package/recipes/resource-locker.json +27 -0
  197. package/recipes/subagent-artifact.json +0 -9
  198. package/recipes/subagent-checkpoint.json +0 -10
  199. package/recipes/subagent-conflict-report.json +0 -11
  200. package/recipes/subagent-contradiction-map.json +0 -11
  201. package/recipes/subagent-critic.json +0 -11
  202. package/recipes/subagent-evidence-map.json +0 -11
  203. package/recipes/subagent-followup.json +0 -10
  204. package/recipes/subagent-judge.json +0 -11
  205. package/recipes/subagent-merge.json +0 -11
  206. package/recipes/subagent-normalize.json +0 -11
  207. package/recipes/subagent-plan.json +0 -11
  208. package/recipes/subagent-preflight.json +0 -11
  209. package/recipes/subagent-prompt.json +0 -10
  210. package/recipes/subagent-quorum.json +0 -10
  211. package/recipes/subagent-review-coordinator.json +0 -14
  212. package/recipes/subagent-review.json +0 -11
  213. package/recipes/subagent-task-card.json +0 -11
  214. package/recipes/subagent-tools.json +0 -10
  215. package/recipes/subagent-verify.json +0 -11
  216. package/recipes/subagents-prompts.json +0 -10
  217. package/recipes/tool-review.json +0 -10
  218. package/scripts/async-runner.mjs +25 -25
  219. package/scripts/conformance.mjs +4 -2
  220. package/scripts/locker.mjs +196 -69
  221. package/scripts/music-player.mjs +162 -159
  222. package/scripts/recipe-utils.mjs +6 -96
  223. package/scripts/release-gates.mjs +91 -0
  224. package/scripts/validate-recipe.mjs +8 -57
  225. package/skills/actors/SKILL.md +57 -265
  226. package/skills/swarm/SKILL.md +10 -34
  227. package/dist/fixtures/protocol/actor-message-branch.json +0 -13
  228. package/dist/fixtures/protocol/mailbox-contract.json +0 -15
  229. package/dist/fixtures/protocol/room-message.json +0 -11
  230. package/dist/fixtures/protocol/room-roster.json +0 -11
  231. package/dist/fixtures/protocol/run-inbox-message.json +0 -9
  232. package/dist/fixtures/protocol/run-outbox-event.json +0 -9
  233. package/dist/lib/mailbox-loop.d.ts +0 -41
  234. package/dist/lib/mailbox-loop.js +0 -60
  235. package/dist/lib/messages.d.ts +0 -25
  236. package/dist/lib/messages.js +0 -122
  237. package/dist/lib/rooms.d.ts +0 -104
  238. package/dist/lib/rooms.js +0 -647
  239. package/dist/lib/runs-mailbox.d.ts +0 -25
  240. package/dist/lib/runs-mailbox.js +0 -146
  241. package/dist/lib/runs-messages.d.ts +0 -15
  242. package/dist/lib/runs-messages.js +0 -179
  243. package/dist/lib/runs-outbox.d.ts +0 -41
  244. package/dist/lib/runs-outbox.js +0 -87
  245. package/dist/lib/tools-mailbox.d.ts +0 -8
  246. package/dist/lib/tools-mailbox.js +0 -48
  247. package/dist/recipes/actor-worker.json +0 -39
  248. package/dist/recipes/coordinator-locker.json +0 -45
  249. package/dist/recipes/locker.json +0 -45
  250. package/dist/recipes/pipeline-room-swarm.json +0 -50
  251. package/dist/recipes/subagent-message.json +0 -32
  252. package/dist/recipes/utility-actor-message.json +0 -23
  253. package/dist/scripts/actor-worker.mjs +0 -214
  254. package/dist/scripts/coordinator.mjs +0 -799
  255. package/docs/actor-messages.md +0 -225
  256. package/docs/actors-deep-reference.md +0 -66
  257. package/docs/component-recipes.md +0 -148
  258. package/docs/task-first-recipes.md +0 -263
  259. package/fixtures/protocol/actor-message-branch.json +0 -13
  260. package/fixtures/protocol/mailbox-contract.json +0 -15
  261. package/fixtures/protocol/room-message.json +0 -11
  262. package/fixtures/protocol/room-roster.json +0 -11
  263. package/fixtures/protocol/run-inbox-message.json +0 -9
  264. package/fixtures/protocol/run-outbox-event.json +0 -9
  265. package/lib/mailbox-loop.ts +0 -144
  266. package/lib/messages.ts +0 -151
  267. package/lib/rooms.ts +0 -939
  268. package/lib/runs-mailbox.ts +0 -208
  269. package/lib/runs-messages.ts +0 -252
  270. package/lib/runs-outbox.ts +0 -144
  271. package/lib/tools-mailbox.ts +0 -56
  272. package/recipes/actor-worker.json +0 -39
  273. package/recipes/coordinator-locker.json +0 -45
  274. package/recipes/locker.json +0 -45
  275. package/recipes/pipeline-room-swarm.json +0 -50
  276. package/recipes/subagent-message.json +0 -32
  277. package/recipes/utility-actor-message.json +0 -23
  278. package/scripts/actor-worker.mjs +0 -214
  279. package/scripts/coordinator.mjs +0 -799
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Model-facing Run Control projection.
3
+ * Zones: structured redaction, bounded Control input/error, decision-useful evidence
4
+ * Owns safe Control reads; durable journals and lifecycle transitions remain in runs-controls.
5
+ */
6
+ import type { RunControlRecord, RunControlStatus } from "./runs-controls.ts";
7
+ export interface ProjectedRunControl {
8
+ action: string;
9
+ claimed_at?: string;
10
+ delivered_at?: string;
11
+ error?: string;
12
+ failed_at?: string;
13
+ handled_at?: string;
14
+ id: string;
15
+ input?: unknown;
16
+ queued_at: string;
17
+ run_instance_id: string;
18
+ status: RunControlStatus;
19
+ }
20
+ export declare function projectRunControl(control: RunControlRecord): ProjectedRunControl;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Model-facing Run Control projection.
3
+ * Zones: structured redaction, bounded Control input/error, decision-useful evidence
4
+ * Owns safe Control reads; durable journals and lifecycle transitions remain in runs-controls.
5
+ */
6
+ import * as Limits from "./limits.js";
7
+ import * as SessionEvidence from "./session-evidence.js";
8
+ const OPTIONAL_TIMESTAMP_FIELDS = [
9
+ "claimed_at",
10
+ "delivered_at",
11
+ "failed_at",
12
+ "handled_at",
13
+ ];
14
+ function boundedRedactedString(value, maxChars) {
15
+ const redacted = SessionEvidence.redactSessionEvidenceValue(value, maxChars);
16
+ return typeof redacted === "string"
17
+ ? redacted
18
+ : String(redacted).slice(0, maxChars);
19
+ }
20
+ function boundedRedactedValue(value) {
21
+ const redacted = SessionEvidence.redactSessionEvidenceValue(value, Limits.INSPECTOR_BODY_PREVIEW_CHARS);
22
+ let serialized;
23
+ try {
24
+ serialized = JSON.stringify(redacted);
25
+ }
26
+ catch {
27
+ return "[UNSERIALIZABLE]";
28
+ }
29
+ if (serialized === undefined)
30
+ return "[UNSERIALIZABLE]";
31
+ if (serialized.length <= Limits.INSPECTOR_BODY_PREVIEW_CHARS)
32
+ return redacted;
33
+ let preview = serialized.slice(0, Limits.INSPECTOR_BODY_PREVIEW_CHARS);
34
+ let bounded = { preview: `${preview}…`, truncated: true };
35
+ while (preview &&
36
+ JSON.stringify(bounded).length > Limits.INSPECTOR_BODY_PREVIEW_CHARS) {
37
+ const excess = JSON.stringify(bounded).length - Limits.INSPECTOR_BODY_PREVIEW_CHARS;
38
+ preview = preview.slice(0, Math.max(0, preview.length - Math.max(1, excess)));
39
+ bounded = { preview: `${preview}…`, truncated: true };
40
+ }
41
+ return bounded;
42
+ }
43
+ export function projectRunControl(control) {
44
+ const timestamps = Object.fromEntries(OPTIONAL_TIMESTAMP_FIELDS.flatMap((field) => {
45
+ const value = control[field];
46
+ return typeof value === "string"
47
+ ? [[field, boundedRedactedString(value, Limits.COMPACT_PREVIEW_CHARS)]]
48
+ : [];
49
+ }));
50
+ return {
51
+ action: boundedRedactedString(control.action, Limits.COMPACT_PREVIEW_CHARS),
52
+ ...timestamps,
53
+ ...(typeof control.error === "string"
54
+ ? {
55
+ error: boundedRedactedString(control.error, Limits.INSPECTOR_BODY_PREVIEW_CHARS),
56
+ }
57
+ : {}),
58
+ id: boundedRedactedString(control.id, Limits.COMPACT_PREVIEW_CHARS),
59
+ ...(control.input !== undefined
60
+ ? { input: boundedRedactedValue(control.input) }
61
+ : {}),
62
+ queued_at: boundedRedactedString(control.queued_at, Limits.COMPACT_PREVIEW_CHARS),
63
+ run_instance_id: boundedRedactedString(control.run_instance_id, Limits.COMPACT_PREVIEW_CHARS),
64
+ status: boundedRedactedString(control.status, Limits.COMPACT_PREVIEW_CHARS),
65
+ };
66
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Run Control request contract.
3
+ * Zones: public request validation, Run/runtime target normalization, input bounds
4
+ * Owns pure Control validation; journaling, delivery, and lifecycle mutation stay in adapters.
5
+ */
6
+ export interface ControlRequest {
7
+ target: `run:${string}` | "runtime";
8
+ action: string;
9
+ input?: unknown;
10
+ verbose?: boolean;
11
+ }
12
+ export declare function isControlAction(value: string): boolean;
13
+ export declare function normalizeControlAction(value: unknown): string;
14
+ export declare function normalizeControlInput(input: unknown): unknown;
15
+ export declare function normalizeControlRequest(input: unknown): ControlRequest;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Run Control request contract.
3
+ * Zones: public request validation, Run/runtime target normalization, input bounds
4
+ * Owns pure Control validation; journaling, delivery, and lifecycle mutation stay in adapters.
5
+ */
6
+ import * as Limits from "./limits.js";
7
+ const RUN_ID_PATTERN = /^[A-Za-z0-9_.-]+$/;
8
+ const ACTION_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)*$/;
9
+ const CONTROL_FIELDS = new Set(["action", "input", "target", "verbose"]);
10
+ const REMOVED_FIELDS = new Set([
11
+ "body",
12
+ "correlation_id",
13
+ "from",
14
+ "metadata",
15
+ "reply_to",
16
+ "summary",
17
+ "to",
18
+ "type",
19
+ ]);
20
+ export function isControlAction(value) {
21
+ return ACTION_PATTERN.test(value);
22
+ }
23
+ export function normalizeControlAction(value) {
24
+ if (typeof value !== "string" || !value.trim()) {
25
+ throw new Error("control.action is required");
26
+ }
27
+ const action = value.trim();
28
+ if (!isControlAction(action)) {
29
+ throw new Error(`invalid control action: ${action}`);
30
+ }
31
+ if (action.length > Limits.CONTROL_ACTION_MAX_LENGTH) {
32
+ throw new Error(`control.action exceeds ${Limits.CONTROL_ACTION_MAX_LENGTH} ASCII characters`);
33
+ }
34
+ return action;
35
+ }
36
+ export function normalizeControlInput(input) {
37
+ let serialized;
38
+ try {
39
+ serialized = JSON.stringify(input);
40
+ }
41
+ catch {
42
+ throw new Error("control.input must be JSON-serializable");
43
+ }
44
+ if (serialized === undefined) {
45
+ throw new Error("control.input must be JSON-serializable");
46
+ }
47
+ const bytes = Buffer.byteLength(serialized);
48
+ if (bytes > Limits.CONTROL_INPUT_MAX_BYTES) {
49
+ throw new Error(`control.input exceeds ${Limits.CONTROL_INPUT_MAX_BYTES} serialized bytes`);
50
+ }
51
+ return JSON.parse(serialized);
52
+ }
53
+ function normalizeTarget(value) {
54
+ if (typeof value !== "string" || !value.trim()) {
55
+ throw new Error("control.target is required");
56
+ }
57
+ const target = value.trim();
58
+ if (target === "runtime")
59
+ return target;
60
+ if (!target.startsWith("run:")) {
61
+ throw new Error(`unsupported control target: ${target}; use run:<id> or runtime`);
62
+ }
63
+ const run = target.slice(4);
64
+ if (!RUN_ID_PATTERN.test(run)) {
65
+ throw new Error(`invalid control Run target: ${target}`);
66
+ }
67
+ return `run:${run}`;
68
+ }
69
+ export function normalizeControlRequest(input) {
70
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
71
+ throw new Error("control request must be an object");
72
+ }
73
+ const record = input;
74
+ const fields = Object.keys(record);
75
+ const removed = fields.filter((field) => REMOVED_FIELDS.has(field));
76
+ if (removed.length > 0) {
77
+ throw new Error(`actor-message fields are removed: ${removed.sort().join(", ")}; use target, action, input, verbose`);
78
+ }
79
+ const unknown = fields.filter((field) => !CONTROL_FIELDS.has(field));
80
+ if (unknown.length > 0) {
81
+ throw new Error(`unsupported control fields: ${unknown.sort().join(", ")}`);
82
+ }
83
+ const target = normalizeTarget(record.target);
84
+ const action = normalizeControlAction(record.action);
85
+ if (record.verbose !== undefined && typeof record.verbose !== "boolean") {
86
+ throw new Error("control.verbose must be a boolean");
87
+ }
88
+ const normalizedInput = record.input === undefined
89
+ ? undefined
90
+ : normalizeControlInput(record.input);
91
+ return {
92
+ target,
93
+ action,
94
+ ...(normalizedInput !== undefined ? { input: normalizedInput } : {}),
95
+ ...(record.verbose !== undefined ? { verbose: record.verbose } : {}),
96
+ };
97
+ }
@@ -391,7 +391,7 @@ export function createDraftSleepScheduler(deps) {
391
391
  ...existing,
392
392
  failedStage: processed.stage ?? "result_processing",
393
393
  lastError: processed.error ?? "automatic draft review processing failed",
394
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"draft\"}",
394
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"draft\"}",
395
395
  phase: "processing_failed",
396
396
  processingAttempts: (existing.processingAttempts ?? 0) + 1,
397
397
  updatedAt: now().toISOString(),
@@ -402,7 +402,7 @@ export function createDraftSleepScheduler(deps) {
402
402
  ...existing,
403
403
  failedStage: processed.stage ?? "review_result",
404
404
  lastError: processed.error ?? "automatic draft reviewer failed",
405
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"draft\"}",
405
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"draft\"}",
406
406
  phase: "failed",
407
407
  runId: undefined,
408
408
  updatedAt: now().toISOString(),
@@ -444,7 +444,7 @@ export function createDraftSleepScheduler(deps) {
444
444
  attempts: (prior?.batchId === batch.batchId ? prior.attempts : 0) + 1,
445
445
  failedStage: "review_launch",
446
446
  lastError: error instanceof Error ? error.message : String(error),
447
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"draft\"}",
447
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"draft\"}",
448
448
  batchId: batch.batchId,
449
449
  inputPath: batch.inputPath,
450
450
  phase: "failed",
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Run-owned execution session evidence.
3
+ * Zones: execution manifest session discovery, canonical containment, rich turn correlation
4
+ * Owns reusable agent-turn loading; Trace and TUI projection stay in their adapters.
5
+ */
6
+ import * as SessionEvidence from "./session-evidence.ts";
7
+ export interface ExecutionTurnItem extends SessionEvidence.SessionEvidenceTurn {
8
+ commandId: string;
9
+ diagnostics: string[];
10
+ promptBytes?: number;
11
+ promptFile?: string;
12
+ recipeContext?: unknown;
13
+ sessionFile: string;
14
+ sessionTruncated: boolean;
15
+ stage?: string;
16
+ }
17
+ export declare function readExecutionTurns(stateDir: string): ExecutionTurnItem[];
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Run-owned execution session evidence.
3
+ * Zones: execution manifest session discovery, canonical containment, rich turn correlation
4
+ * Owns reusable agent-turn loading; Trace and TUI projection stay in their adapters.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import * as SessionEvidence from "./session-evidence.js";
9
+ import { readJsonFileResilient } from "./state-readers.js";
10
+ function ownedSessionPath(stateDir, sessionFile) {
11
+ if (path.isAbsolute(sessionFile))
12
+ return undefined;
13
+ const sessionsRoot = path.resolve(stateDir, "sessions");
14
+ const resolved = path.resolve(stateDir, sessionFile);
15
+ if (!resolved.startsWith(`${sessionsRoot}${path.sep}`))
16
+ return undefined;
17
+ try {
18
+ const canonicalRoot = fs.realpathSync(sessionsRoot);
19
+ const canonicalFile = fs.realpathSync(resolved);
20
+ return canonicalFile.startsWith(`${canonicalRoot}${path.sep}`)
21
+ ? canonicalFile
22
+ : undefined;
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ export function readExecutionTurns(stateDir) {
29
+ const execution = readJsonFileResilient(path.join(stateDir, "execution.json"), {}).value;
30
+ const commands = Array.isArray(execution.commands)
31
+ ? execution.commands
32
+ : [];
33
+ const recordedFiles = new Set(commands.flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : []).filter((file) => typeof file === "string")));
34
+ let discoveredCommands = [];
35
+ try {
36
+ const sessionsDir = path.join(stateDir, "sessions");
37
+ discoveredCommands = fs
38
+ .readdirSync(sessionsDir, { withFileTypes: true })
39
+ .filter((entry) => entry.isDirectory())
40
+ .flatMap((entry) => {
41
+ const files = fs
42
+ .readdirSync(path.join(sessionsDir, entry.name), {
43
+ withFileTypes: true,
44
+ })
45
+ .filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
46
+ .map((file) => path.posix.join("sessions", entry.name, file.name))
47
+ .filter((file) => !recordedFiles.has(file));
48
+ return files.length > 0
49
+ ? [{ id: entry.name, session_files: files, stage: "subagent" }]
50
+ : [];
51
+ });
52
+ }
53
+ catch {
54
+ discoveredCommands = [];
55
+ }
56
+ return [...commands, ...discoveredCommands]
57
+ .flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : [])
58
+ .filter((file) => typeof file === "string")
59
+ .flatMap((file) => {
60
+ const sessionPath = ownedSessionPath(stateDir, file);
61
+ if (!sessionPath)
62
+ return [];
63
+ const session = SessionEvidence.readSessionEvidence(sessionPath);
64
+ return session.turns.map((turn) => ({
65
+ ...turn,
66
+ commandId: String(command.id ?? "unknown"),
67
+ diagnostics: session.diagnostics.map((item) => `${item.line === undefined ? "" : `line ${item.line}: `}${item.message}`),
68
+ ...(typeof command.prompt_bytes === "number"
69
+ ? { promptBytes: command.prompt_bytes }
70
+ : {}),
71
+ ...(typeof command.prompt_file === "string"
72
+ ? { promptFile: command.prompt_file }
73
+ : {}),
74
+ ...(command.recipe_context !== undefined
75
+ ? { recipeContext: command.recipe_context }
76
+ : {}),
77
+ sessionFile: file,
78
+ sessionTruncated: session.truncated,
79
+ ...(typeof command.stage === "string"
80
+ ? { stage: command.stage }
81
+ : {}),
82
+ }));
83
+ }))
84
+ .map((turn, index) => ({ ...turn, index: index + 1 }));
85
+ }
@@ -5,10 +5,12 @@
5
5
  */
6
6
  export declare function mutationLockPath(path: string): string;
7
7
  export interface FileMutationLockOptions {
8
+ onBeforeLockPublish?(): void;
8
9
  onBeforeReclaimRemove?(): void;
9
10
  onContention?(): void;
10
11
  onRemovalContention?(): void;
11
12
  }
12
13
  export declare function acquireFileMutationLock(path: string, options?: FileMutationLockOptions): () => void;
13
14
  export declare function withFileMutationLock<T>(path: string, mutate: () => T, options?: FileMutationLockOptions): T;
15
+ export declare function writeTextAtomic(path: string, content: string): void;
14
16
  export declare function writeJsonAtomic(path: string, value: unknown): void;
@@ -3,17 +3,27 @@
3
3
  * Zones: file persistence, atomic writes, runtime state support
4
4
  * Owns generic durable JSON file writes shared by registry config and async run state.
5
5
  */
6
+ import { spawnSync } from "node:child_process";
6
7
  import { createHash, randomUUID } from "node:crypto";
7
- import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
8
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
8
9
  import { tmpdir } from "node:os";
9
10
  import { basename, dirname, join, parse, resolve } from "node:path";
10
- const FILE_MUTATION_LOCK_TIMEOUT_MS = 5000;
11
+ const FILE_MUTATION_LOCK_TIMEOUT_MS = process.platform === "win32" ? 15000 : 5000;
11
12
  const FILE_MUTATION_LOCK_STALE_MS = 30000;
12
13
  const FILE_MUTATION_LOCK_ROOT = join(tmpdir(), "pi-actors-file-locks");
13
14
  function canonicalMutationPath(path) {
14
15
  const absolute = resolve(path);
15
- const suffix = [];
16
- let existing = absolute;
16
+ try {
17
+ if (lstatSync(absolute).isSymbolicLink()) {
18
+ const target = realpathSync.native(absolute);
19
+ return process.platform === "win32" ? target.toLowerCase() : target;
20
+ }
21
+ }
22
+ catch {
23
+ /* Materialization may race lock-key derivation; canonicalize through the parent. */
24
+ }
25
+ const suffix = [basename(absolute)];
26
+ let existing = dirname(absolute);
17
27
  while (!existsSync(existing)) {
18
28
  const parent = dirname(existing);
19
29
  if (parent === existing || existing === parse(existing).root)
@@ -33,6 +43,20 @@ export function mutationLockPath(path) {
33
43
  .digest("hex");
34
44
  return join(FILE_MUTATION_LOCK_ROOT, `${key}.lock`);
35
45
  }
46
+ function isZombieProcess(pid) {
47
+ if (process.platform === "linux")
48
+ try {
49
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
50
+ return stat.slice(stat.lastIndexOf(")") + 2).startsWith("Z ");
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ if (process.platform !== "darwin")
56
+ return false;
57
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "stat="], { encoding: "utf8" });
58
+ return result.status === 0 && result.stdout.trim().startsWith("Z");
59
+ }
36
60
  function lockOwnerStatus(lockPath) {
37
61
  try {
38
62
  const owner = JSON.parse(readFileSync(join(lockPath, "owner.json"), "utf8"));
@@ -41,6 +65,8 @@ function lockOwnerStatus(lockPath) {
41
65
  return "unknown";
42
66
  try {
43
67
  process.kill(pid, 0);
68
+ if (Date.now() - statSync(lockPath).mtimeMs > 50 && isZombieProcess(pid))
69
+ return "dead";
44
70
  return "alive";
45
71
  }
46
72
  catch (error) {
@@ -62,6 +88,46 @@ function readLockToken(lockPath) {
62
88
  return undefined;
63
89
  }
64
90
  }
91
+ function prepareLockBoundary(lockPath, token, onBeforePublish) {
92
+ const pendingPath = `${lockPath}.${process.pid}.${token}.pending`;
93
+ try {
94
+ mkdirSync(pendingPath);
95
+ writeFileSync(join(pendingPath, "owner.json"), `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`, "utf8");
96
+ onBeforePublish?.();
97
+ return pendingPath;
98
+ }
99
+ catch (error) {
100
+ try {
101
+ rmSync(pendingPath, { recursive: true, force: true });
102
+ }
103
+ catch { }
104
+ throw error;
105
+ }
106
+ }
107
+ function removeLockBoundary(lockPath, token) {
108
+ if (readLockToken(lockPath) !== token)
109
+ return false;
110
+ if (process.platform === "win32")
111
+ try {
112
+ rmSync(lockPath, { recursive: true, force: true, maxRetries: 20, retryDelay: 5 });
113
+ return true;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ const removingPath = `${lockPath}.${process.pid}.${randomUUID()}.removing`;
119
+ try {
120
+ renameSync(lockPath, removingPath);
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ try {
126
+ rmSync(removingPath, { recursive: true, force: true });
127
+ }
128
+ catch { }
129
+ return true;
130
+ }
65
131
  function tryReclaimRemovalBoundary(reclaimPath) {
66
132
  if (!existsSync(reclaimPath))
67
133
  return;
@@ -72,7 +138,7 @@ function tryReclaimRemovalBoundary(reclaimPath) {
72
138
  if ((ownerStatus === "dead" ||
73
139
  (ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
74
140
  readLockToken(reclaimPath) === inspectedToken) {
75
- rmSync(reclaimPath, { recursive: true, force: true });
141
+ removeLockBoundary(reclaimPath, inspectedToken);
76
142
  }
77
143
  }
78
144
  catch {
@@ -82,28 +148,28 @@ function tryReclaimRemovalBoundary(reclaimPath) {
82
148
  function withRemovalBoundary(lockPath, action) {
83
149
  const reclaimPath = `${lockPath}.reclaim`;
84
150
  const token = randomUUID();
151
+ const pendingPath = prepareLockBoundary(reclaimPath, token);
85
152
  try {
86
- mkdirSync(reclaimPath);
87
153
  try {
88
- writeFileSync(join(reclaimPath, "owner.json"), `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`, "utf8");
154
+ renameSync(pendingPath, reclaimPath);
89
155
  }
90
- catch (error) {
91
- rmSync(reclaimPath, { recursive: true, force: true });
92
- throw error;
156
+ catch {
157
+ tryReclaimRemovalBoundary(reclaimPath);
158
+ return false;
159
+ }
160
+ try {
161
+ action();
162
+ return true;
163
+ }
164
+ finally {
165
+ removeLockBoundary(reclaimPath, token);
93
166
  }
94
- }
95
- catch {
96
- tryReclaimRemovalBoundary(reclaimPath);
97
- return false;
98
- }
99
- try {
100
- action();
101
- return true;
102
167
  }
103
168
  finally {
104
- if (readLockToken(reclaimPath) === token) {
105
- rmSync(reclaimPath, { recursive: true, force: true });
169
+ try {
170
+ rmSync(pendingPath, { recursive: true, force: true });
106
171
  }
172
+ catch { }
107
173
  }
108
174
  }
109
175
  function tryReclaimMutationLock(lockPath, options) {
@@ -120,8 +186,7 @@ function tryReclaimMutationLock(lockPath, options) {
120
186
  (ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
121
187
  readLockToken(lockPath) === inspectedToken) {
122
188
  options.onBeforeReclaimRemove?.();
123
- rmSync(lockPath, { recursive: true, force: true });
124
- reclaimed = true;
189
+ reclaimed = removeLockBoundary(lockPath, inspectedToken);
125
190
  }
126
191
  })) {
127
192
  return false;
@@ -133,32 +198,34 @@ export function acquireFileMutationLock(path, options = {}) {
133
198
  const lockPath = mutationLockPath(path);
134
199
  const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
135
200
  const token = randomUUID();
201
+ const pendingPath = prepareLockBoundary(lockPath, token, options.onBeforeLockPublish);
136
202
  let contentionReported = false;
137
- for (;;) {
138
- try {
139
- mkdirSync(lockPath);
203
+ try {
204
+ for (;;) {
140
205
  try {
141
- writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`, "utf8");
206
+ renameSync(pendingPath, lockPath);
207
+ break;
142
208
  }
143
209
  catch (error) {
144
- rmSync(lockPath, { recursive: true, force: true });
145
- throw error;
210
+ if (!contentionReported) {
211
+ contentionReported = true;
212
+ options.onContention?.();
213
+ }
214
+ tryReclaimMutationLock(lockPath, options);
215
+ if (Date.now() >= deadline) {
216
+ throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
217
+ cause: error,
218
+ });
219
+ }
220
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
146
221
  }
147
- break;
148
222
  }
149
- catch (error) {
150
- if (!contentionReported) {
151
- contentionReported = true;
152
- options.onContention?.();
153
- }
154
- tryReclaimMutationLock(lockPath, options);
155
- if (Date.now() >= deadline) {
156
- throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
157
- cause: error,
158
- });
159
- }
160
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
223
+ }
224
+ finally {
225
+ try {
226
+ rmSync(pendingPath, { recursive: true, force: true });
161
227
  }
228
+ catch { }
162
229
  }
163
230
  let released = false;
164
231
  return () => {
@@ -168,9 +235,7 @@ export function acquireFileMutationLock(path, options = {}) {
168
235
  const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
169
236
  let removalContentionReported = false;
170
237
  while (!withRemovalBoundary(lockPath, () => {
171
- if (readLockToken(lockPath) === token) {
172
- rmSync(lockPath, { recursive: true, force: true });
173
- }
238
+ removeLockBoundary(lockPath, token);
174
239
  })) {
175
240
  if (!removalContentionReported) {
176
241
  removalContentionReported = true;
@@ -191,11 +256,11 @@ export function withFileMutationLock(path, mutate, options = {}) {
191
256
  release();
192
257
  }
193
258
  }
194
- export function writeJsonAtomic(path, value) {
259
+ export function writeTextAtomic(path, content) {
195
260
  mkdirSync(dirname(path), { recursive: true });
196
261
  const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
197
262
  try {
198
- writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
263
+ writeFileSync(tempPath, content, "utf8");
199
264
  renameSync(tempPath, path);
200
265
  }
201
266
  catch (error) {
@@ -208,3 +273,6 @@ export function writeJsonAtomic(path, value) {
208
273
  throw error;
209
274
  }
210
275
  }
276
+ export function writeJsonAtomic(path, value) {
277
+ writeTextAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
278
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Actor Inspector run actions.
2
+ * Actor Inspector actions for concrete Run instances.
3
3
  * Owns: exact-session run-action authorization, canonical kill routing, and bounded operator feedback.
4
4
  */
5
5
  export interface ActorInspectorKillResult {
@@ -13,4 +13,4 @@ export interface ActorInspectorKillDeps {
13
13
  runInstanceId: string;
14
14
  }) => Record<string, unknown>;
15
15
  }
16
- export declare function killOwnedInspectorRun(ownerId: string, run: string, stateRoot: string, expectedRunInstanceId: string, deps: ActorInspectorKillDeps): ActorInspectorKillResult;
16
+ export declare function killOwnedRunFromInspector(ownerId: string, run: string, stateRoot: string, expectedRunInstanceId: string, deps: ActorInspectorKillDeps): ActorInspectorKillResult;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Actor Inspector run actions.
2
+ * Actor Inspector actions for concrete Run instances.
3
3
  * Owns: exact-session run-action authorization, canonical kill routing, and bounded operator feedback.
4
4
  */
5
5
  import { join } from "node:path";
@@ -11,7 +11,7 @@ function bounded(value) {
11
11
  ? text
12
12
  : `${text.slice(0, FEEDBACK_LIMIT - 1)}…`;
13
13
  }
14
- export function killOwnedInspectorRun(ownerId, run, stateRoot, expectedRunInstanceId, deps) {
14
+ export function killOwnedRunFromInspector(ownerId, run, stateRoot, expectedRunInstanceId, deps) {
15
15
  let stateDir;
16
16
  try {
17
17
  stateDir = join(stateRoot, safeRunId(run));
@@ -8,13 +8,13 @@ import * as InspectorActions from "./inspector-actions.js";
8
8
  import * as InspectorOverlay from "./inspector-overlay.js";
9
9
  import * as Paths from "./paths.js";
10
10
  export function registerActorInspectorCommand(pi, getRunOwnerId) {
11
- pi.registerCommand("actors-inspector", {
12
- description: "Open the keyboard-driven actor inspector overlay",
11
+ pi.registerCommand("actor-inspector", {
12
+ description: "Open the keyboard-driven Actor Inspector",
13
13
  handler: async (_args, ctx) => {
14
14
  ctx.ui.setWidget("zz-pi-actors-comms", undefined);
15
15
  await ctx.ui.custom((tui, theme, _keybindings, done) => new InspectorOverlay.ActorInspectorOverlay({
16
16
  done,
17
- killRun: (run, runInstanceId) => InspectorActions.killOwnedInspectorRun(getRunOwnerId(ctx), run, Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, runInstanceId, {
17
+ killRun: (run, runInstanceId) => InspectorActions.killOwnedRunFromInspector(getRunOwnerId(ctx), run, Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, runInstanceId, {
18
18
  getRunStatus: AsyncRuns.getRunStatus,
19
19
  killRun: AsyncRuns.killRun,
20
20
  }),