@llblab/pi-actors 0.49.1 → 0.50.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 (44) hide show
  1. package/BACKLOG.md +1 -1
  2. package/CHANGELOG.md +14 -1
  3. package/README.md +19 -3
  4. package/dist/lib/extension-runtime.js +29 -10
  5. package/dist/lib/inspector-overlay.d.ts +3 -0
  6. package/dist/lib/inspector-overlay.js +120 -38
  7. package/dist/lib/limits.d.ts +1 -0
  8. package/dist/lib/limits.js +1 -0
  9. package/dist/lib/observability.js +6 -1
  10. package/dist/lib/run-ui-runtime.d.ts +10 -3
  11. package/dist/lib/run-ui-runtime.js +109 -52
  12. package/dist/lib/runs-artifacts.js +24 -4
  13. package/dist/lib/runtime.d.ts +2 -0
  14. package/dist/lib/runtime.js +24 -4
  15. package/dist/lib/session-evidence.d.ts +1 -0
  16. package/dist/lib/session-evidence.js +3 -2
  17. package/dist/lib/state-readers.d.ts +5 -1
  18. package/dist/lib/state-readers.js +30 -3
  19. package/dist/lib/tools-access.d.ts +1 -0
  20. package/dist/lib/tools-access.js +13 -11
  21. package/dist/lib/tools-inspect.js +5 -5
  22. package/dist/lib/tools-message.d.ts +1 -0
  23. package/dist/lib/tools-message.js +3 -1
  24. package/docs/README.md +1 -0
  25. package/docs/actor-inspector.md +21 -5
  26. package/docs/async-runs.md +16 -4
  27. package/docs/command-templates.md +5 -4
  28. package/docs/inspection.md +83 -0
  29. package/docs/recipe-library.md +1 -1
  30. package/docs/template-recipes.md +225 -66
  31. package/docs/tool-registry.md +24 -3
  32. package/lib/extension-runtime.ts +26 -10
  33. package/lib/inspector-overlay.ts +161 -33
  34. package/lib/limits.ts +1 -0
  35. package/lib/observability.ts +5 -1
  36. package/lib/run-ui-runtime.ts +128 -50
  37. package/lib/runs-artifacts.ts +34 -4
  38. package/lib/runtime.ts +26 -7
  39. package/lib/session-evidence.ts +7 -2
  40. package/lib/state-readers.ts +45 -3
  41. package/lib/tools-access.ts +26 -12
  42. package/lib/tools-inspect.ts +9 -5
  43. package/lib/tools-message.ts +8 -1
  44. package/package.json +1 -1
@@ -11,25 +11,89 @@ import * as Pi from "./pi.ts";
11
11
 
12
12
  export interface RunUiRuntime {
13
13
  close(): void;
14
- shutdown(eventReason: string, ctx: Pi.ExtensionContext): void;
15
- start(ctx: Pi.ExtensionContext): void;
14
+ shutdown(
15
+ eventReason: string,
16
+ ownerId: string | undefined,
17
+ ctx?: Pi.ExtensionContext,
18
+ ): void;
19
+ start(ctx: Pi.ExtensionContext, ownerId: string): void;
16
20
  }
17
21
 
18
22
  export interface RunUiRuntimeDeps {
23
+ animationIntervalMs?: number;
24
+ createRunStateWatcher?: typeof Observability.createRunStateWatcher;
25
+ createRunTerminalReconciliationLoop?:
26
+ typeof Observability.createRunTerminalReconciliationLoop;
19
27
  getActiveContext(): Pi.ExtensionContext | undefined;
20
- getRunOwnerId(ctx: Pi.ExtensionContext): string;
28
+ notificationDelayMs?: number;
29
+ onCallbackError?: (error: unknown) => void;
21
30
  onRunEvent(): void;
22
31
  pi: Pi.ExtensionAPI;
32
+ teardownRunsOwnedByParent?: typeof AsyncRuns.teardownRunsOwnedByParent;
23
33
  }
24
34
 
25
35
  export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
36
+ let activeContext: Pi.ExtensionContext | undefined;
37
+ let activeOwnerId: string | undefined;
26
38
  let animationInterval: NodeJS.Timeout | undefined;
27
39
  let notifyTimeout: NodeJS.Timeout | undefined;
40
+ let running = false;
28
41
  let lastWatcherDiagnosticId = 0;
29
42
  const observation = Observability.createRunUiObservationState();
30
43
  const retirementAttempts = new Set<string>();
31
44
  const terminalNotificationsInFlight = new Set<string>();
32
45
 
46
+ const close = (): void => {
47
+ running = false;
48
+ activeContext = undefined;
49
+ activeOwnerId = undefined;
50
+ try {
51
+ watcher.close();
52
+ } catch {
53
+ /* cleanup must not escape a host callback */
54
+ }
55
+ try {
56
+ reconciliation.close();
57
+ } catch {
58
+ /* cleanup must not escape a host callback */
59
+ }
60
+ if (notifyTimeout) clearTimeout(notifyTimeout);
61
+ notifyTimeout = undefined;
62
+ if (animationInterval) clearInterval(animationInterval);
63
+ animationInterval = undefined;
64
+ };
65
+ const stopAfterCallbackFailure = (
66
+ label: string,
67
+ error: unknown,
68
+ expectedContext: Pi.ExtensionContext,
69
+ ): void => {
70
+ if (activeContext !== expectedContext) return;
71
+ close();
72
+ try {
73
+ deps.onCallbackError?.(error);
74
+ } catch {
75
+ /* host callback containment must remain no-throw */
76
+ }
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ try {
79
+ expectedContext.ui.notify(`Actor ${label} failed: ${message}`, "error");
80
+ } catch {
81
+ /* stale context or unavailable UI */
82
+ }
83
+ };
84
+ const runActiveCallback = (
85
+ label: string,
86
+ callback: (ctx: Pi.ExtensionContext, ownerId: string) => void,
87
+ ): void => {
88
+ if (!running || !activeContext || !activeOwnerId) return;
89
+ const ctx = activeContext;
90
+ try {
91
+ if (deps.getActiveContext() !== ctx) return;
92
+ callback(ctx, activeOwnerId);
93
+ } catch (error) {
94
+ stopAfterCallbackFailure(label, error, ctx);
95
+ }
96
+ };
33
97
  const retireCandidateRuns = (
34
98
  ctx: Pi.ExtensionContext,
35
99
  summary: Observability.RunSummary,
@@ -39,14 +103,16 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
39
103
  cancelRun: (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
40
104
  notify: (message, level) => ctx.ui.notify(message, level),
41
105
  sendStop: async (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
42
- });
106
+ }).catch((error) =>
107
+ stopAfterCallbackFailure("Run retirement callback", error, ctx),
108
+ );
43
109
  };
44
110
  const update = (
45
111
  ctx: Pi.ExtensionContext,
112
+ ownerId: string,
46
113
  notify = false,
47
114
  terminalOnly = false,
48
115
  ): void => {
49
- const ownerId = deps.getRunOwnerId(ctx);
50
116
  const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
51
117
  ctx.ui.setStatus(
52
118
  "zz-pi-actors-runs",
@@ -76,61 +142,63 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
76
142
  }
77
143
  };
78
144
  const scheduleUpdate = (): void => {
145
+ if (!running) return;
79
146
  if (notifyTimeout) clearTimeout(notifyTimeout);
80
147
  notifyTimeout = setTimeout(() => {
81
- const ctx = deps.getActiveContext();
82
- if (!ctx) return;
83
- watcher.refresh();
84
- update(ctx, true);
85
- deps.onRunEvent();
86
- reportDiagnostics(ctx);
87
- }, 50);
148
+ runActiveCallback("Run watcher callback", (ctx, ownerId) => {
149
+ watcher.refresh();
150
+ update(ctx, ownerId, true);
151
+ deps.onRunEvent();
152
+ reportDiagnostics(ctx);
153
+ });
154
+ }, deps.notificationDelayMs ?? 50);
88
155
  notifyTimeout.unref?.();
89
156
  };
90
- const watcher = Observability.createRunStateWatcher({
157
+ const watcher = (deps.createRunStateWatcher ?? Observability.createRunStateWatcher)({
91
158
  stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
92
159
  onChange: scheduleUpdate,
93
160
  });
94
- const reconciliation = Observability.createRunTerminalReconciliationLoop({
161
+ const reconciliation = (
162
+ deps.createRunTerminalReconciliationLoop ??
163
+ Observability.createRunTerminalReconciliationLoop
164
+ )({
95
165
  onError: (error) => {
96
- const ctx = deps.getActiveContext();
97
- if (!ctx) return;
98
- const message = error instanceof Error ? error.message : String(error);
99
- ctx.ui.notify(`Actor terminal reconciliation failed: ${message}`, "error");
166
+ if (!running || !activeContext) return;
167
+ stopAfterCallbackFailure(
168
+ "terminal reconciliation callback",
169
+ error,
170
+ activeContext,
171
+ );
100
172
  },
101
173
  reconcile: () => {
102
- const ctx = deps.getActiveContext();
103
- if (!ctx) return;
104
- Observability.reconcileRunTerminalNotifications({
105
- inFlight: terminalNotificationsInFlight,
106
- ownerId: deps.getRunOwnerId(ctx),
107
- sink: Pi.createNotificationSink(deps.pi, ctx),
108
- state: observation,
109
- includeAttention: true,
174
+ runActiveCallback("terminal reconciliation callback", (ctx, ownerId) => {
175
+ Observability.reconcileRunTerminalNotifications({
176
+ inFlight: terminalNotificationsInFlight,
177
+ ownerId,
178
+ sink: Pi.createNotificationSink(deps.pi, ctx),
179
+ state: observation,
180
+ includeAttention: true,
181
+ });
182
+ reportDiagnostics(ctx);
110
183
  });
111
- reportDiagnostics(ctx);
112
184
  },
113
- refreshWatcher: () => watcher.refresh(),
185
+ refreshWatcher: () => {
186
+ if (running) watcher.refresh();
187
+ },
114
188
  });
115
- const close = (): void => {
116
- watcher.close();
117
- reconciliation.close();
118
- if (notifyTimeout) clearTimeout(notifyTimeout);
119
- notifyTimeout = undefined;
120
- if (animationInterval) clearInterval(animationInterval);
121
- animationInterval = undefined;
122
- };
123
189
 
124
190
  return {
125
191
  close,
126
- shutdown(eventReason, ctx) {
127
- close();
128
- const teardown = AsyncRuns.teardownRunsOwnedByParent(
129
- deps.getRunOwnerId(ctx),
192
+ shutdown(eventReason, ownerId, ctx) {
193
+ if (!ownerId) return;
194
+ const teardown = (
195
+ deps.teardownRunsOwnedByParent ?? AsyncRuns.teardownRunsOwnedByParent
196
+ )(
197
+ ownerId,
130
198
  Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
131
199
  { trigger: `session_shutdown:${eventReason}` },
132
200
  );
133
- if (teardown.failed === 0) return;
201
+ if (teardown.failed === 0 || !ctx) return;
134
202
  try {
135
203
  ctx.ui.notify(
136
204
  `Actor shutdown teardown: killed=${teardown.killed} failed=${teardown.failed} skipped=${teardown.skipped} discovery_failed=${teardown.discoveryFailed}. Summary: ${teardown.summaryPath ?? "unavailable"}.`,
@@ -140,16 +208,26 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
140
208
  /* stale shutdown context */
141
209
  }
142
210
  },
143
- start(ctx) {
211
+ start(ctx, ownerId) {
144
212
  close();
145
- Observability.primeRunAttentionState(observation, deps.getRunOwnerId(ctx));
146
- update(ctx, true, true);
147
- watcher.refresh();
148
- reconciliation.start();
149
- animationInterval = setInterval(() => {
150
- if (deps.getActiveContext() === ctx) update(ctx);
151
- }, 1000);
152
- animationInterval.unref?.();
213
+ activeContext = ctx;
214
+ activeOwnerId = ownerId;
215
+ running = true;
216
+ try {
217
+ Observability.primeRunAttentionState(observation, ownerId);
218
+ update(ctx, ownerId, true, true);
219
+ watcher.refresh();
220
+ reconciliation.start();
221
+ animationInterval = setInterval(() => {
222
+ runActiveCallback("status animation callback", (current, currentOwnerId) =>
223
+ update(current, currentOwnerId),
224
+ );
225
+ }, deps.animationIntervalMs ?? 1000);
226
+ animationInterval.unref?.();
227
+ } catch (error) {
228
+ close();
229
+ throw error;
230
+ }
153
231
  },
154
232
  };
155
233
  }
@@ -4,7 +4,12 @@
4
4
  */
5
5
 
6
6
  import { createHash } from "node:crypto";
7
- import { readFileSync } from "node:fs";
7
+ import {
8
+ closeSync,
9
+ fstatSync,
10
+ openSync,
11
+ readSync,
12
+ } from "node:fs";
8
13
 
9
14
  import { substituteCommandTemplateToken } from "./command-templates.ts";
10
15
 
@@ -22,6 +27,31 @@ export interface RunArtifactManifestEntry {
22
27
  size?: number;
23
28
  }
24
29
 
30
+ function hashArtifactFile(path: string): { sha256: string; size: number } {
31
+ const fd = openSync(path, "r");
32
+ try {
33
+ const size = fstatSync(fd).size;
34
+ const hash = createHash("sha256");
35
+ const chunk = Buffer.allocUnsafe(64 * 1024);
36
+ let position = 0;
37
+ while (position < size) {
38
+ const bytesRead = readSync(
39
+ fd,
40
+ chunk,
41
+ 0,
42
+ Math.min(chunk.byteLength, size - position),
43
+ position,
44
+ );
45
+ if (bytesRead === 0) break;
46
+ hash.update(chunk.subarray(0, bytesRead));
47
+ position += bytesRead;
48
+ }
49
+ return { sha256: hash.digest("hex"), size: position };
50
+ } finally {
51
+ closeSync(fd);
52
+ }
53
+ }
54
+
25
55
  export function resolveArtifactPaths(
26
56
  artifacts: Record<string, RunArtifactDeclaration> | undefined,
27
57
  values: Record<string, unknown>,
@@ -64,7 +94,7 @@ export function resolveArtifactManifest(
64
94
  typeof artifact === "string" ? { path: artifact } : artifact;
65
95
  if (!declaration?.path) continue;
66
96
  try {
67
- const content = readFileSync(declaration.path);
97
+ const hashed = hashArtifactFile(declaration.path);
68
98
  manifest[name] = {
69
99
  exists: true,
70
100
  ...(declaration.kind ? { kind: declaration.kind } : {}),
@@ -75,8 +105,8 @@ export function resolveArtifactManifest(
75
105
  ...(declaration.required !== undefined
76
106
  ? { required: declaration.required }
77
107
  : {}),
78
- sha256: createHash("sha256").update(content).digest("hex"),
79
- size: content.byteLength,
108
+ sha256: hashed.sha256,
109
+ size: hashed.size,
80
110
  };
81
111
  } catch {
82
112
  manifest[name] = {
package/lib/runtime.ts CHANGED
@@ -308,7 +308,9 @@ export function createAutoToolsRuntime(
308
308
  export interface RecipeToolReloadWatcherDeps {
309
309
  exists?: (path: string) => boolean;
310
310
  getResolutionContext?: () => RecipeResolutionContext | undefined;
311
+ onCallbackError?: (error: unknown) => void;
311
312
  recipeRoot?: string;
313
+ reloadDelayMs?: number;
312
314
  watchPath?: typeof watch;
313
315
  }
314
316
 
@@ -336,22 +338,39 @@ export function createRecipeToolReloadWatcher(
336
338
  reloadTimeout = undefined;
337
339
  setWatchStatus("closed");
338
340
  };
341
+ const reportCallbackError = (error: unknown): void => {
342
+ try {
343
+ deps.onCallbackError?.(error);
344
+ } catch {
345
+ /* host callback containment must remain no-throw */
346
+ }
347
+ };
339
348
  const notifyFailure = (ctx: RuntimeContext): void => {
340
349
  if (failureNotified) return;
341
350
  failureNotified = true;
342
351
  setWatchStatus("failed");
343
- ctx.ui.notify(
344
- "Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
345
- "warning",
346
- );
352
+ try {
353
+ ctx.ui.notify(
354
+ "Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
355
+ "warning",
356
+ );
357
+ } catch (error) {
358
+ reportCallbackError(error);
359
+ }
347
360
  };
348
361
  const scheduleReload = (ctx: RuntimeContext): void => {
349
362
  failureNotified = false;
350
363
  if (reloadTimeout) clearTimeout(reloadTimeout);
351
364
  reloadTimeout = setTimeout(() => {
352
- runtime.loadTools(ctx, deps.getResolutionContext?.());
353
- ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
354
- }, 150);
365
+ reloadTimeout = undefined;
366
+ try {
367
+ runtime.loadTools(ctx, deps.getResolutionContext?.());
368
+ ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
369
+ } catch (error) {
370
+ notifyFailure(ctx);
371
+ reportCallbackError(error);
372
+ }
373
+ }, deps.reloadDelayMs ?? 150);
355
374
  reloadTimeout.unref?.();
356
375
  };
357
376
  const watchParent = (ctx: RuntimeContext, recipeRoot: string): void => {
@@ -55,6 +55,7 @@ export interface SessionEvidence {
55
55
  }
56
56
 
57
57
  export interface SessionEvidenceReadOptions {
58
+ maxBytes?: number;
58
59
  maxTextChars?: number;
59
60
  maxToolCalls?: number;
60
61
  maxTurns?: number;
@@ -195,6 +196,10 @@ export function readSessionEvidence(
195
196
  path: string,
196
197
  options: SessionEvidenceReadOptions = {},
197
198
  ): SessionEvidence {
199
+ const maxBytes = Math.max(
200
+ 1,
201
+ options.maxBytes ?? Limits.SESSION_EVIDENCE_MAX_BYTES,
202
+ );
198
203
  const maxTextChars = Math.max(
199
204
  1,
200
205
  options.maxTextChars ?? Limits.SESSION_EVIDENCE_TEXT_CHARS,
@@ -207,7 +212,7 @@ export function readSessionEvidence(
207
212
  1,
208
213
  options.maxTurns ?? Limits.SESSION_EVIDENCE_MAX_TURNS,
209
214
  );
210
- const read = readJsonlFileResilient<SessionEntry>(path);
215
+ const read = readJsonlFileResilient<SessionEntry>(path, { maxBytes });
211
216
  const diagnostics = [...read.diagnostics];
212
217
  const header = read.records.find((entry) => entry.type === "session");
213
218
  const branch = activeBranch(read.records, path, diagnostics);
@@ -296,7 +301,7 @@ export function readSessionEvidence(
296
301
  path,
297
302
  ...(header ? { session: asRecord(header) } : {}),
298
303
  totalTurns: turns.length,
299
- truncated: turns.length > visibleTurns.length,
304
+ truncated: read.truncated === true || turns.length > visibleTurns.length,
300
305
  turns: visibleTurns,
301
306
  };
302
307
  }
@@ -4,7 +4,13 @@
4
4
  * Owns best-effort JSON and JSONL parsing helpers for operator-facing state reads.
5
5
  */
6
6
 
7
- import { readFileSync } from "node:fs";
7
+ import {
8
+ closeSync,
9
+ fstatSync,
10
+ openSync,
11
+ readFileSync,
12
+ readSync,
13
+ } from "node:fs";
8
14
 
9
15
  export interface StateReadDiagnostic {
10
16
  line?: number;
@@ -20,6 +26,11 @@ export interface JsonReadResult<T> {
20
26
  export interface JsonlReadResult<T> {
21
27
  diagnostics: StateReadDiagnostic[];
22
28
  records: T[];
29
+ truncated?: boolean;
30
+ }
31
+
32
+ export interface JsonlReadOptions {
33
+ maxBytes?: number;
23
34
  }
24
35
 
25
36
  function isEnoent(error: unknown): boolean {
@@ -30,6 +41,31 @@ function diagnosticMessage(error: unknown): string {
30
41
  return error instanceof Error ? error.message : String(error);
31
42
  }
32
43
 
44
+ function readTextFileCapped(path: string, maxBytes: number | undefined): string {
45
+ if (maxBytes === undefined) return readFileSync(path, "utf8");
46
+ const limit = Math.max(0, Math.floor(maxBytes));
47
+ const fd = openSync(path, "r");
48
+ try {
49
+ const size = fstatSync(fd).size;
50
+ if (size > limit) {
51
+ throw Object.assign(
52
+ new Error(`file exceeds bounded read limit (${size} > ${limit} bytes)`),
53
+ { code: "EFBIG" },
54
+ );
55
+ }
56
+ const content = Buffer.allocUnsafe(size);
57
+ let offset = 0;
58
+ while (offset < size) {
59
+ const bytesRead = readSync(fd, content, offset, size - offset, offset);
60
+ if (bytesRead === 0) break;
61
+ offset += bytesRead;
62
+ }
63
+ return content.subarray(0, offset).toString("utf8");
64
+ } finally {
65
+ closeSync(fd);
66
+ }
67
+ }
68
+
33
69
  export function readJsonFileResilient<T>(path: string, fallback: T): JsonReadResult<T> {
34
70
  try {
35
71
  return {
@@ -50,11 +86,14 @@ export function readJsonFileResilient<T>(path: string, fallback: T): JsonReadRes
50
86
  }
51
87
  }
52
88
 
53
- export function readJsonlFileResilient<T>(path: string): JsonlReadResult<T> {
89
+ export function readJsonlFileResilient<T>(
90
+ path: string,
91
+ options: JsonlReadOptions = {},
92
+ ): JsonlReadResult<T> {
54
93
  try {
55
94
  const records: T[] = [];
56
95
  const diagnostics: StateReadDiagnostic[] = [];
57
- const lines = readFileSync(path, "utf8").split("\n");
96
+ const lines = readTextFileCapped(path, options.maxBytes).split("\n");
58
97
  for (const [index, line] of lines.entries()) {
59
98
  if (!line.trim()) continue;
60
99
  try {
@@ -78,6 +117,9 @@ export function readJsonlFileResilient<T>(path: string): JsonlReadResult<T> {
78
117
  },
79
118
  ],
80
119
  records: [],
120
+ ...((error as NodeJS.ErrnoException).code === "EFBIG"
121
+ ? { truncated: true }
122
+ : {}),
81
123
  };
82
124
  }
83
125
  }
@@ -17,8 +17,11 @@ export function getContextSessionId(ctx: unknown): string | undefined {
17
17
  export function requireContextSessionId(ctx: unknown, actor: string): string {
18
18
  const sessionId = getContextSessionId(ctx);
19
19
  if (!sessionId) {
20
- throw new Error(
21
- `${actor} requires a current coordinator session; use session:<id> or session:all for explicit session inventory.`,
20
+ throw Object.assign(
21
+ new Error(
22
+ `${actor} reason=session_unavailable requires a current coordinator session; retry from an active coordinator session.`,
23
+ ),
24
+ { reason: "session_unavailable" },
22
25
  );
23
26
  }
24
27
  return sessionId;
@@ -33,16 +36,13 @@ export function sessionMismatchError(input: {
33
36
  const ownerSession = input.expectedSession ?? "none";
34
37
  const currentSession = input.currentSession ?? "none";
35
38
  const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
36
- const hintTarget = input.expectedSession
37
- ? `session:${input.expectedSession}`
38
- : "session:all";
39
39
  return Object.assign(
40
40
  new Error(
41
- `${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`,
41
+ `${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_runtime_runs`,
42
42
  ),
43
43
  {
44
44
  current_session: input.currentSession,
45
- hint: `inspect target=${hintTarget} view=status`,
45
+ hint: "inspect target=runtime view=runs",
46
46
  owner_session: input.expectedSession,
47
47
  reason: "session_mismatch",
48
48
  run: input.run,
@@ -51,18 +51,32 @@ export function sessionMismatchError(input: {
51
51
  );
52
52
  }
53
53
 
54
- export function assertRunAccessibleToContext(
54
+ export function assertRunStatusAccessibleToContext(
55
55
  runId: string,
56
+ status: Record<string, unknown>,
56
57
  ctx: unknown,
57
58
  ): Record<string, unknown> {
58
- const status = AsyncRuns.getRunStatus(runId);
59
- const sessionId = getContextSessionId(ctx);
60
- if (sessionId && status.ownerId && status.ownerId !== sessionId) {
59
+ const sessionId = requireContextSessionId(ctx, `run:${runId}`);
60
+ const ownerId = typeof status.ownerId === "string" && status.ownerId
61
+ ? status.ownerId
62
+ : undefined;
63
+ if (ownerId !== sessionId) {
61
64
  throw sessionMismatchError({
62
65
  currentSession: sessionId,
63
- expectedSession: String(status.ownerId),
66
+ expectedSession: ownerId,
64
67
  run: runId,
65
68
  });
66
69
  }
67
70
  return status;
68
71
  }
72
+
73
+ export function assertRunAccessibleToContext(
74
+ runId: string,
75
+ ctx: unknown,
76
+ ): Record<string, unknown> {
77
+ return assertRunStatusAccessibleToContext(
78
+ runId,
79
+ AsyncRuns.getRunStatus(runId),
80
+ ctx,
81
+ );
82
+ }
@@ -42,7 +42,7 @@ export interface InspectToolDeps<TContext = unknown> {
42
42
 
43
43
  function runtimeStatus(): Record<string, unknown> {
44
44
  return {
45
- automatic_review: process.env.PI_ACTORS_AUTOMATIC_REVIEW !== "off",
45
+ automatic_review: Paths.isAutomaticRecipeReviewEnabled(),
46
46
  run_root: Paths.getRunStateRoot(),
47
47
  state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
48
48
  version: RuntimeIdentity.getPackageVersion(),
@@ -54,7 +54,7 @@ function runtimeRuns(
54
54
  deps: InspectToolDeps,
55
55
  status: string | undefined,
56
56
  ): Record<string, unknown> {
57
- const session = ToolsAccess.getContextSessionId(ctx);
57
+ const session = ToolsAccess.requireContextSessionId(ctx, "runtime Run inventory");
58
58
  const listed = deps.listRuns
59
59
  ? deps.listRuns()
60
60
  : AsyncRuns.listRuns(undefined, status);
@@ -68,8 +68,8 @@ function runtimeRuns(
68
68
  return run;
69
69
  }
70
70
  })
71
- .filter((run) => !session || !run.ownerId || run.ownerId === session);
72
- return { ...(session ? { owner_session: session } : {}), runs };
71
+ .filter((run) => run.ownerId === session);
72
+ return { owner_session: session, runs };
73
73
  }
74
74
 
75
75
  function runtimeTriage(
@@ -463,7 +463,11 @@ function inspectRun(
463
463
  throw new Error("inspect run:<id> supports view=recipe, view=trace, or view=control.");
464
464
  }
465
465
  const status = deps.getRunStatus
466
- ? deps.getRunStatus(run)
466
+ ? ToolsAccess.assertRunStatusAccessibleToContext(
467
+ run,
468
+ deps.getRunStatus(run),
469
+ ctx,
470
+ )
467
471
  : ToolsAccess.assertRunAccessibleToContext(run, ctx);
468
472
  const stateDir = String(status.state_dir);
469
473
  if (view === "recipe") {
@@ -46,6 +46,7 @@ function runNextActions(run: string, result: Record<string, unknown>): string[]
46
46
  }
47
47
 
48
48
  export interface ControlToolDeps {
49
+ getRunStatus?: (run: string) => Record<string, unknown>;
49
50
  handleRuntimeControl?: (
50
51
  action: string,
51
52
  input: unknown,
@@ -94,7 +95,13 @@ export function createControlToolDefinition<TContext = unknown>(
94
95
  result = deps.handleRuntimeControl(request.action, request.input);
95
96
  } else {
96
97
  const run = request.target.slice(4);
97
- const status = ToolsAccess.assertRunAccessibleToContext(run, ctx);
98
+ const status = deps.getRunStatus
99
+ ? ToolsAccess.assertRunStatusAccessibleToContext(
100
+ run,
101
+ deps.getRunStatus(run),
102
+ ctx,
103
+ )
104
+ : ToolsAccess.assertRunAccessibleToContext(run, ctx);
98
105
  const runInstanceId =
99
106
  typeof status.run_instance_id === "string"
100
107
  ? status.run_instance_id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.49.1",
3
+ "version": "0.50.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [