@llblab/pi-actors 0.41.0 → 0.42.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 (150) hide show
  1. package/AGENTS.md +13 -8
  2. package/BACKLOG.md +2 -81
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +22 -4
  5. package/dist/index.js +29 -82
  6. package/dist/lib/async-runs.d.ts +25 -5
  7. package/dist/lib/async-runs.js +136 -47
  8. package/dist/lib/automatic-review-runtime.d.ts +18 -0
  9. package/dist/lib/automatic-review-runtime.js +96 -0
  10. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  11. package/dist/lib/draft-consolidation-transaction.js +610 -0
  12. package/dist/lib/draft-consolidation.d.ts +35 -0
  13. package/dist/lib/draft-consolidation.js +126 -0
  14. package/dist/lib/draft-review.d.ts +56 -0
  15. package/dist/lib/draft-review.js +254 -0
  16. package/dist/lib/draft-sleep.d.ts +65 -0
  17. package/dist/lib/draft-sleep.js +468 -0
  18. package/dist/lib/file-state.d.ts +8 -1
  19. package/dist/lib/file-state.js +115 -18
  20. package/dist/lib/inspector-actions.d.ts +16 -0
  21. package/dist/lib/inspector-actions.js +57 -0
  22. package/dist/lib/inspector-command.d.ts +7 -0
  23. package/dist/lib/inspector-command.js +37 -0
  24. package/dist/lib/inspector-overlay.d.ts +29 -1
  25. package/dist/lib/inspector-overlay.js +537 -90
  26. package/dist/lib/inspector.d.ts +9 -0
  27. package/dist/lib/inspector.js +52 -1
  28. package/dist/lib/observability.d.ts +39 -3
  29. package/dist/lib/observability.js +162 -34
  30. package/dist/lib/paths.d.ts +7 -0
  31. package/dist/lib/paths.js +28 -0
  32. package/dist/lib/recipes-discovery.js +32 -24
  33. package/dist/lib/recipes-usage.d.ts +18 -6
  34. package/dist/lib/recipes-usage.js +445 -34
  35. package/dist/lib/review-control.d.ts +14 -0
  36. package/dist/lib/review-control.js +111 -0
  37. package/dist/lib/review-diagnostics.d.ts +11 -0
  38. package/dist/lib/review-diagnostics.js +148 -0
  39. package/dist/lib/review-projection.d.ts +14 -0
  40. package/dist/lib/review-projection.js +170 -0
  41. package/dist/lib/run-ui-runtime.d.ts +18 -0
  42. package/dist/lib/run-ui-runtime.js +123 -0
  43. package/dist/lib/runs-artifacts.d.ts +1 -1
  44. package/dist/lib/runs-artifacts.js +1 -1
  45. package/dist/lib/runs-control.d.ts +8 -2
  46. package/dist/lib/runs-control.js +23 -6
  47. package/dist/lib/runs-identity.d.ts +1 -1
  48. package/dist/lib/runs-identity.js +1 -1
  49. package/dist/lib/runs-index.d.ts +11 -2
  50. package/dist/lib/runs-index.js +46 -23
  51. package/dist/lib/runs-mailbox.d.ts +1 -1
  52. package/dist/lib/runs-mailbox.js +1 -1
  53. package/dist/lib/runs-messages.d.ts +1 -1
  54. package/dist/lib/runs-messages.js +1 -1
  55. package/dist/lib/runs-outbox.d.ts +1 -1
  56. package/dist/lib/runs-outbox.js +1 -1
  57. package/dist/lib/runs-ownership.d.ts +1 -1
  58. package/dist/lib/runs-ownership.js +1 -1
  59. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  60. package/dist/lib/runs-parent-teardown.js +172 -0
  61. package/dist/lib/runs-process.d.ts +1 -1
  62. package/dist/lib/runs-process.js +1 -1
  63. package/dist/lib/runs-retention.d.ts +1 -1
  64. package/dist/lib/runs-retention.js +1 -1
  65. package/dist/lib/runs-start.d.ts +5 -3
  66. package/dist/lib/runs-start.js +6 -48
  67. package/dist/lib/runs-status.d.ts +5 -3
  68. package/dist/lib/runs-status.js +4 -6
  69. package/dist/lib/runtime.d.ts +7 -1
  70. package/dist/lib/runtime.js +32 -18
  71. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  72. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  73. package/dist/lib/tool-review-lineage.d.ts +24 -0
  74. package/dist/lib/tool-review-lineage.js +98 -0
  75. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  76. package/dist/lib/tool-review-scheduler.js +494 -0
  77. package/dist/lib/tool-review-transaction.d.ts +50 -0
  78. package/dist/lib/tool-review-transaction.js +362 -0
  79. package/dist/lib/tool-review.d.ts +56 -0
  80. package/dist/lib/tool-review.js +197 -0
  81. package/dist/lib/tools-inspect.js +26 -3
  82. package/dist/lib/tools-local.js +4 -2
  83. package/dist/lib/tools-message.d.ts +1 -0
  84. package/dist/lib/tools-message.js +29 -17
  85. package/dist/lib/tools-response.d.ts +1 -1
  86. package/dist/lib/tools-response.js +5 -7
  87. package/dist/lib/tools-spawn.js +4 -1
  88. package/dist/lib/tools.d.ts +1 -0
  89. package/dist/lib/tools.js +1 -0
  90. package/dist/recipes/draft-review.json +24 -0
  91. package/dist/recipes/tool-review.json +24 -0
  92. package/dist/scripts/release-gates.mjs +165 -0
  93. package/dist/skills/actors/SKILL.md +11 -14
  94. package/dist/skills/swarm/SKILL.md +1 -1
  95. package/docs/actor-inspector.md +32 -18
  96. package/docs/async-runs.md +10 -2
  97. package/docs/recipe-library.md +7 -3
  98. package/docs/template-recipes.md +4 -11
  99. package/docs/tool-registry.md +11 -4
  100. package/index.ts +29 -103
  101. package/lib/async-runs.ts +218 -62
  102. package/lib/automatic-review-runtime.ts +135 -0
  103. package/lib/draft-consolidation-transaction.ts +821 -0
  104. package/lib/draft-consolidation.ts +181 -0
  105. package/lib/draft-review.ts +325 -0
  106. package/lib/draft-sleep.ts +576 -0
  107. package/lib/file-state.ts +143 -19
  108. package/lib/inspector-actions.ts +79 -0
  109. package/lib/inspector-command.ts +54 -0
  110. package/lib/inspector-overlay.ts +675 -105
  111. package/lib/inspector.ts +78 -3
  112. package/lib/observability.ts +219 -40
  113. package/lib/paths.ts +43 -0
  114. package/lib/recipes-discovery.ts +34 -26
  115. package/lib/recipes-usage.ts +569 -40
  116. package/lib/review-control.ts +137 -0
  117. package/lib/review-diagnostics.ts +164 -0
  118. package/lib/review-projection.ts +200 -0
  119. package/lib/run-ui-runtime.ts +153 -0
  120. package/lib/runs-artifacts.ts +1 -1
  121. package/lib/runs-control.ts +49 -5
  122. package/lib/runs-identity.ts +1 -1
  123. package/lib/runs-index.ts +57 -21
  124. package/lib/runs-mailbox.ts +1 -1
  125. package/lib/runs-messages.ts +1 -1
  126. package/lib/runs-outbox.ts +1 -1
  127. package/lib/runs-ownership.ts +1 -1
  128. package/lib/runs-parent-teardown.ts +257 -0
  129. package/lib/runs-process.ts +1 -1
  130. package/lib/runs-retention.ts +1 -1
  131. package/lib/runs-start.ts +12 -68
  132. package/lib/runs-status.ts +12 -8
  133. package/lib/runtime.ts +34 -17
  134. package/lib/tool-review-lineage-transaction.ts +881 -0
  135. package/lib/tool-review-lineage.ts +145 -0
  136. package/lib/tool-review-scheduler.ts +635 -0
  137. package/lib/tool-review-transaction.ts +563 -0
  138. package/lib/tool-review.ts +270 -0
  139. package/lib/tools-inspect.ts +33 -3
  140. package/lib/tools-local.ts +8 -2
  141. package/lib/tools-message.ts +45 -30
  142. package/lib/tools-response.ts +5 -6
  143. package/lib/tools-spawn.ts +8 -1
  144. package/lib/tools.ts +5 -0
  145. package/package.json +3 -2
  146. package/recipes/draft-review.json +24 -0
  147. package/recipes/tool-review.json +24 -0
  148. package/scripts/release-gates.mjs +165 -0
  149. package/skills/actors/SKILL.md +11 -14
  150. package/skills/swarm/SKILL.md +1 -1
package/lib/file-state.ts CHANGED
@@ -40,42 +40,144 @@ function canonicalMutationPath(path: string): string {
40
40
  return process.platform === "win32" ? canonical.toLowerCase() : canonical;
41
41
  }
42
42
 
43
- function mutationLockPath(path: string): string {
43
+ export function mutationLockPath(path: string): string {
44
44
  const key = createHash("sha256")
45
45
  .update(canonicalMutationPath(path))
46
46
  .digest("hex");
47
47
  return join(FILE_MUTATION_LOCK_ROOT, `${key}.lock`);
48
48
  }
49
49
 
50
- function lockOwnerIsDead(lockPath: string): boolean {
50
+ function lockOwnerStatus(lockPath: string): "alive" | "dead" | "unknown" {
51
51
  try {
52
52
  const owner = JSON.parse(
53
53
  readFileSync(join(lockPath, "owner.json"), "utf8"),
54
54
  ) as { pid?: unknown };
55
55
  const pid = Number(owner.pid);
56
- if (!Number.isInteger(pid) || pid <= 0) return false;
56
+ if (!Number.isInteger(pid) || pid <= 0) return "unknown";
57
57
  try {
58
58
  process.kill(pid, 0);
59
- return false;
59
+ return "alive";
60
60
  } catch (error) {
61
- return (error as NodeJS.ErrnoException).code === "ESRCH";
61
+ return (error as NodeJS.ErrnoException).code === "ESRCH"
62
+ ? "dead"
63
+ : "unknown";
62
64
  }
63
65
  } catch {
66
+ return "unknown";
67
+ }
68
+ }
69
+
70
+ export interface FileMutationLockOptions {
71
+ onBeforeReclaimRemove?(): void;
72
+ onContention?(): void;
73
+ onRemovalContention?(): void;
74
+ }
75
+
76
+ function readLockToken(lockPath: string): string | undefined {
77
+ try {
78
+ const owner = JSON.parse(
79
+ readFileSync(join(lockPath, "owner.json"), "utf8"),
80
+ ) as { token?: unknown };
81
+ return typeof owner.token === "string" ? owner.token : undefined;
82
+ } catch {
83
+ return undefined;
84
+ }
85
+ }
86
+
87
+ function tryReclaimRemovalBoundary(reclaimPath: string): void {
88
+ if (!existsSync(reclaimPath)) return;
89
+ try {
90
+ const inspectedToken = readLockToken(reclaimPath);
91
+ const age = Date.now() - statSync(reclaimPath).mtimeMs;
92
+ const ownerStatus = lockOwnerStatus(reclaimPath);
93
+ if (
94
+ (ownerStatus === "dead" ||
95
+ (ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
96
+ readLockToken(reclaimPath) === inspectedToken
97
+ ) {
98
+ rmSync(reclaimPath, { recursive: true, force: true });
99
+ }
100
+ } catch {
101
+ /* another contender changed the boundary */
102
+ }
103
+ }
104
+
105
+ function withRemovalBoundary(
106
+ lockPath: string,
107
+ action: () => void,
108
+ ): boolean {
109
+ const reclaimPath = `${lockPath}.reclaim`;
110
+ const token = randomUUID();
111
+ try {
112
+ mkdirSync(reclaimPath);
113
+ try {
114
+ writeFileSync(
115
+ join(reclaimPath, "owner.json"),
116
+ `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`,
117
+ "utf8",
118
+ );
119
+ } catch (error) {
120
+ rmSync(reclaimPath, { recursive: true, force: true });
121
+ throw error;
122
+ }
123
+ } catch {
124
+ tryReclaimRemovalBoundary(reclaimPath);
125
+ return false;
126
+ }
127
+ try {
128
+ action();
129
+ return true;
130
+ } finally {
131
+ if (readLockToken(reclaimPath) === token) {
132
+ rmSync(reclaimPath, { recursive: true, force: true });
133
+ }
134
+ }
135
+ }
136
+
137
+ function tryReclaimMutationLock(
138
+ lockPath: string,
139
+ options: FileMutationLockOptions,
140
+ ): boolean {
141
+ let reclaimed = false;
142
+ if (!withRemovalBoundary(lockPath, () => {
143
+ if (!existsSync(lockPath)) {
144
+ reclaimed = true;
145
+ return;
146
+ }
147
+ const inspectedToken = readLockToken(lockPath);
148
+ const age = Date.now() - statSync(lockPath).mtimeMs;
149
+ const ownerStatus = lockOwnerStatus(lockPath);
150
+ if (
151
+ (ownerStatus === "dead" ||
152
+ (ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
153
+ readLockToken(lockPath) === inspectedToken
154
+ ) {
155
+ options.onBeforeReclaimRemove?.();
156
+ rmSync(lockPath, { recursive: true, force: true });
157
+ reclaimed = true;
158
+ }
159
+ })) {
64
160
  return false;
65
161
  }
162
+ return reclaimed;
66
163
  }
67
164
 
68
- export function withFileMutationLock<T>(path: string, mutate: () => T): T {
165
+ export function acquireFileMutationLock(
166
+ path: string,
167
+ options: FileMutationLockOptions = {},
168
+ ): () => void {
69
169
  mkdirSync(FILE_MUTATION_LOCK_ROOT, { recursive: true });
70
170
  const lockPath = mutationLockPath(path);
71
171
  const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
172
+ const token = randomUUID();
173
+ let contentionReported = false;
72
174
  for (;;) {
73
175
  try {
74
176
  mkdirSync(lockPath);
75
177
  try {
76
178
  writeFileSync(
77
179
  join(lockPath, "owner.json"),
78
- `${JSON.stringify({ pid: process.pid, acquired_at: new Date().toISOString() })}\n`,
180
+ `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`,
79
181
  "utf8",
80
182
  );
81
183
  } catch (error) {
@@ -84,18 +186,11 @@ export function withFileMutationLock<T>(path: string, mutate: () => T): T {
84
186
  }
85
187
  break;
86
188
  } catch (error) {
87
- try {
88
- if (
89
- Date.now() - statSync(lockPath).mtimeMs >
90
- FILE_MUTATION_LOCK_STALE_MS &&
91
- lockOwnerIsDead(lockPath)
92
- ) {
93
- rmSync(lockPath, { recursive: true, force: true });
94
- continue;
95
- }
96
- } catch {
97
- continue;
189
+ if (!contentionReported) {
190
+ contentionReported = true;
191
+ options.onContention?.();
98
192
  }
193
+ tryReclaimMutationLock(lockPath, options);
99
194
  if (Date.now() >= deadline) {
100
195
  throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
101
196
  cause: error,
@@ -104,10 +199,39 @@ export function withFileMutationLock<T>(path: string, mutate: () => T): T {
104
199
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
105
200
  }
106
201
  }
202
+ let released = false;
203
+ return () => {
204
+ if (released) return;
205
+ released = true;
206
+ const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
207
+ let removalContentionReported = false;
208
+ while (
209
+ !withRemovalBoundary(lockPath, () => {
210
+ if (readLockToken(lockPath) === token) {
211
+ rmSync(lockPath, { recursive: true, force: true });
212
+ }
213
+ })
214
+ ) {
215
+ if (!removalContentionReported) {
216
+ removalContentionReported = true;
217
+ options.onRemovalContention?.();
218
+ }
219
+ if (Date.now() >= deadline) return;
220
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
221
+ }
222
+ };
223
+ }
224
+
225
+ export function withFileMutationLock<T>(
226
+ path: string,
227
+ mutate: () => T,
228
+ options: FileMutationLockOptions = {},
229
+ ): T {
230
+ const release = acquireFileMutationLock(path, options);
107
231
  try {
108
232
  return mutate();
109
233
  } finally {
110
- rmSync(lockPath, { recursive: true, force: true });
234
+ release();
111
235
  }
112
236
  }
113
237
 
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Actor Inspector run actions.
3
+ * Owns: exact-session run-action authorization, canonical kill routing, and bounded operator feedback.
4
+ */
5
+
6
+ import { join } from "node:path";
7
+
8
+ import { safeRunId } from "./runs-identity.ts";
9
+
10
+ const FEEDBACK_LIMIT = 180;
11
+
12
+ export interface ActorInspectorKillResult {
13
+ ok: boolean;
14
+ message: string;
15
+ }
16
+
17
+ export interface ActorInspectorKillDeps {
18
+ getRunStatus: (runOrDir: string) => Record<string, unknown>;
19
+ killRun: (
20
+ runOrDir: string,
21
+ expected: { ownerId: string; runInstanceId: string },
22
+ ) => Record<string, unknown>;
23
+ }
24
+
25
+ function bounded(value: unknown): string {
26
+ const text = String(value ?? "").replaceAll(/\s+/g, " ").trim();
27
+ return text.length <= FEEDBACK_LIMIT
28
+ ? text
29
+ : `${text.slice(0, FEEDBACK_LIMIT - 1)}…`;
30
+ }
31
+
32
+ export function killOwnedInspectorRun(
33
+ ownerId: string,
34
+ run: string,
35
+ stateRoot: string,
36
+ expectedRunInstanceId: string,
37
+ deps: ActorInspectorKillDeps,
38
+ ): ActorInspectorKillResult {
39
+ let stateDir: string;
40
+ try {
41
+ stateDir = join(stateRoot, safeRunId(run));
42
+ } catch (error) {
43
+ return {
44
+ ok: false,
45
+ message: `Kill rejected: ${bounded(error instanceof Error ? error.message : error)}`,
46
+ };
47
+ }
48
+ try {
49
+ const status = deps.getRunStatus(stateDir);
50
+ if (status.ownerId !== ownerId) {
51
+ return { ok: false, message: "Kill rejected: run ownership changed." };
52
+ }
53
+ if (status.status !== "running") {
54
+ return {
55
+ ok: false,
56
+ message: `Kill unavailable: run is ${bounded(status.status || "terminal")}.`,
57
+ };
58
+ }
59
+ if (status.run_instance_id !== expectedRunInstanceId) {
60
+ return { ok: false, message: "Kill rejected: run generation changed." };
61
+ }
62
+ const result = deps.killRun(stateDir, {
63
+ ownerId,
64
+ runInstanceId: expectedRunInstanceId,
65
+ });
66
+ if (result.killed === true) {
67
+ return { ok: true, message: `Killed run:${run}.` };
68
+ }
69
+ return {
70
+ ok: false,
71
+ message: `Kill failed: ${bounded(result.reason || "control rejected")}.`,
72
+ };
73
+ } catch (error) {
74
+ return {
75
+ ok: false,
76
+ message: `Kill failed: ${bounded(error instanceof Error ? error.message : error)}.`,
77
+ };
78
+ }
79
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Actor Inspector command adapter.
3
+ * Zones: Pi command registration, overlay construction, owned-run control ports
4
+ * Owns host-facing Inspector launch wiring without owning Inspector state or actions.
5
+ */
6
+
7
+ import * as AsyncRuns from "./async-runs.ts";
8
+ import * as InspectorActions from "./inspector-actions.ts";
9
+ import * as InspectorOverlay from "./inspector-overlay.ts";
10
+ import * as Paths from "./paths.ts";
11
+ import type * as Pi from "./pi.ts";
12
+
13
+ export function registerActorInspectorCommand(
14
+ pi: Pi.ExtensionAPI,
15
+ getRunOwnerId: (ctx: Pi.ExtensionContext) => string,
16
+ ): void {
17
+ pi.registerCommand("actors-inspector", {
18
+ description: "Open the keyboard-driven actor inspector overlay",
19
+ handler: async (_args, ctx) => {
20
+ ctx.ui.setWidget("zz-pi-actors-comms", undefined);
21
+ await ctx.ui.custom<void>(
22
+ (tui, theme, _keybindings, done) =>
23
+ new InspectorOverlay.ActorInspectorOverlay({
24
+ done,
25
+ killRun: (run, runInstanceId) =>
26
+ InspectorActions.killOwnedInspectorRun(
27
+ getRunOwnerId(ctx),
28
+ run,
29
+ Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
30
+ runInstanceId,
31
+ {
32
+ getRunStatus: AsyncRuns.getRunStatus,
33
+ killRun: AsyncRuns.killRun,
34
+ },
35
+ ),
36
+ ownerId: getRunOwnerId(ctx),
37
+ stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
38
+ theme,
39
+ tui,
40
+ }),
41
+ {
42
+ overlay: true,
43
+ overlayOptions: {
44
+ anchor: "center",
45
+ width: "94%",
46
+ minWidth: 72,
47
+ maxHeight: "94%",
48
+ margin: 1,
49
+ },
50
+ },
51
+ );
52
+ },
53
+ });
54
+ }