@llblab/pi-kit 0.1.4 → 0.1.6

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 (58) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +14 -1
  5. package/node_modules/@llblab/pi-actors/README.md +19 -3
  6. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +29 -10
  7. package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.d.ts +3 -0
  8. package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.js +120 -38
  9. package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +1 -0
  10. package/node_modules/@llblab/pi-actors/dist/lib/limits.js +1 -0
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +6 -1
  12. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +10 -3
  13. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +109 -52
  14. package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.js +24 -4
  15. package/node_modules/@llblab/pi-actors/dist/lib/runtime.d.ts +2 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +24 -4
  17. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +1 -0
  18. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +3 -2
  19. package/node_modules/@llblab/pi-actors/dist/lib/state-readers.d.ts +5 -1
  20. package/node_modules/@llblab/pi-actors/dist/lib/state-readers.js +30 -3
  21. package/node_modules/@llblab/pi-actors/dist/lib/tools-access.d.ts +1 -0
  22. package/node_modules/@llblab/pi-actors/dist/lib/tools-access.js +13 -11
  23. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +5 -5
  24. package/node_modules/@llblab/pi-actors/dist/lib/tools-message.d.ts +1 -0
  25. package/node_modules/@llblab/pi-actors/dist/lib/tools-message.js +3 -1
  26. package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
  27. package/node_modules/@llblab/pi-actors/docs/actor-inspector.md +21 -5
  28. package/node_modules/@llblab/pi-actors/docs/async-runs.md +16 -4
  29. package/node_modules/@llblab/pi-actors/docs/command-templates.md +5 -4
  30. package/node_modules/@llblab/pi-actors/docs/inspection.md +83 -0
  31. package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
  32. package/node_modules/@llblab/pi-actors/docs/template-recipes.md +225 -66
  33. package/node_modules/@llblab/pi-actors/docs/tool-registry.md +24 -3
  34. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +26 -10
  35. package/node_modules/@llblab/pi-actors/lib/inspector-overlay.ts +161 -33
  36. package/node_modules/@llblab/pi-actors/lib/limits.ts +1 -0
  37. package/node_modules/@llblab/pi-actors/lib/observability.ts +5 -1
  38. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +128 -50
  39. package/node_modules/@llblab/pi-actors/lib/runs-artifacts.ts +34 -4
  40. package/node_modules/@llblab/pi-actors/lib/runtime.ts +26 -7
  41. package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +7 -2
  42. package/node_modules/@llblab/pi-actors/lib/state-readers.ts +45 -3
  43. package/node_modules/@llblab/pi-actors/lib/tools-access.ts +26 -12
  44. package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +9 -5
  45. package/node_modules/@llblab/pi-actors/lib/tools-message.ts +8 -1
  46. package/node_modules/@llblab/pi-actors/package.json +1 -1
  47. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  48. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  49. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  50. package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
  51. package/node_modules/@llblab/pi-telegram/index.ts +25 -0
  52. package/node_modules/@llblab/pi-telegram/lib/journal.ts +192 -9
  53. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +52 -2
  54. package/node_modules/@llblab/pi-telegram/lib/status.ts +3 -3
  55. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +5 -1
  56. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  57. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +14 -6
  58. package/package.json +4 -4
@@ -8,22 +8,78 @@ import * as Observability from "./observability.js";
8
8
  import * as Paths from "./paths.js";
9
9
  import * as Pi from "./pi.js";
10
10
  export function createRunUiRuntime(deps) {
11
+ let activeContext;
12
+ let activeOwnerId;
11
13
  let animationInterval;
12
14
  let notifyTimeout;
15
+ let running = false;
13
16
  let lastWatcherDiagnosticId = 0;
14
17
  const observation = Observability.createRunUiObservationState();
15
18
  const retirementAttempts = new Set();
16
19
  const terminalNotificationsInFlight = new Set();
20
+ const close = () => {
21
+ running = false;
22
+ activeContext = undefined;
23
+ activeOwnerId = undefined;
24
+ try {
25
+ watcher.close();
26
+ }
27
+ catch {
28
+ /* cleanup must not escape a host callback */
29
+ }
30
+ try {
31
+ reconciliation.close();
32
+ }
33
+ catch {
34
+ /* cleanup must not escape a host callback */
35
+ }
36
+ if (notifyTimeout)
37
+ clearTimeout(notifyTimeout);
38
+ notifyTimeout = undefined;
39
+ if (animationInterval)
40
+ clearInterval(animationInterval);
41
+ animationInterval = undefined;
42
+ };
43
+ const stopAfterCallbackFailure = (label, error, expectedContext) => {
44
+ if (activeContext !== expectedContext)
45
+ return;
46
+ close();
47
+ try {
48
+ deps.onCallbackError?.(error);
49
+ }
50
+ catch {
51
+ /* host callback containment must remain no-throw */
52
+ }
53
+ const message = error instanceof Error ? error.message : String(error);
54
+ try {
55
+ expectedContext.ui.notify(`Actor ${label} failed: ${message}`, "error");
56
+ }
57
+ catch {
58
+ /* stale context or unavailable UI */
59
+ }
60
+ };
61
+ const runActiveCallback = (label, callback) => {
62
+ if (!running || !activeContext || !activeOwnerId)
63
+ return;
64
+ const ctx = activeContext;
65
+ try {
66
+ if (deps.getActiveContext() !== ctx)
67
+ return;
68
+ callback(ctx, activeOwnerId);
69
+ }
70
+ catch (error) {
71
+ stopAfterCallbackFailure(label, error, ctx);
72
+ }
73
+ };
17
74
  const retireCandidateRuns = (ctx, summary) => {
18
75
  void Observability.executeRunRetirements(summary, {
19
76
  attempted: retirementAttempts,
20
77
  cancelRun: (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
21
78
  notify: (message, level) => ctx.ui.notify(message, level),
22
79
  sendStop: async (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
23
- });
80
+ }).catch((error) => stopAfterCallbackFailure("Run retirement callback", error, ctx));
24
81
  };
25
- const update = (ctx, notify = false, terminalOnly = false) => {
26
- const ownerId = deps.getRunOwnerId(ctx);
82
+ const update = (ctx, ownerId, notify = false, terminalOnly = false) => {
27
83
  const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
28
84
  ctx.ui.setStatus("zz-pi-actors-runs", snapshot.status ? ctx.ui.theme.fg("dim", snapshot.status) : undefined);
29
85
  if (!notify)
@@ -45,62 +101,55 @@ export function createRunUiRuntime(deps) {
45
101
  }
46
102
  };
47
103
  const scheduleUpdate = () => {
104
+ if (!running)
105
+ return;
48
106
  if (notifyTimeout)
49
107
  clearTimeout(notifyTimeout);
50
108
  notifyTimeout = setTimeout(() => {
51
- const ctx = deps.getActiveContext();
52
- if (!ctx)
53
- return;
54
- watcher.refresh();
55
- update(ctx, true);
56
- deps.onRunEvent();
57
- reportDiagnostics(ctx);
58
- }, 50);
109
+ runActiveCallback("Run watcher callback", (ctx, ownerId) => {
110
+ watcher.refresh();
111
+ update(ctx, ownerId, true);
112
+ deps.onRunEvent();
113
+ reportDiagnostics(ctx);
114
+ });
115
+ }, deps.notificationDelayMs ?? 50);
59
116
  notifyTimeout.unref?.();
60
117
  };
61
- const watcher = Observability.createRunStateWatcher({
118
+ const watcher = (deps.createRunStateWatcher ?? Observability.createRunStateWatcher)({
62
119
  stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
63
120
  onChange: scheduleUpdate,
64
121
  });
65
- const reconciliation = Observability.createRunTerminalReconciliationLoop({
122
+ const reconciliation = (deps.createRunTerminalReconciliationLoop ??
123
+ Observability.createRunTerminalReconciliationLoop)({
66
124
  onError: (error) => {
67
- const ctx = deps.getActiveContext();
68
- if (!ctx)
125
+ if (!running || !activeContext)
69
126
  return;
70
- const message = error instanceof Error ? error.message : String(error);
71
- ctx.ui.notify(`Actor terminal reconciliation failed: ${message}`, "error");
127
+ stopAfterCallbackFailure("terminal reconciliation callback", error, activeContext);
72
128
  },
73
129
  reconcile: () => {
74
- const ctx = deps.getActiveContext();
75
- if (!ctx)
76
- return;
77
- Observability.reconcileRunTerminalNotifications({
78
- inFlight: terminalNotificationsInFlight,
79
- ownerId: deps.getRunOwnerId(ctx),
80
- sink: Pi.createNotificationSink(deps.pi, ctx),
81
- state: observation,
82
- includeAttention: true,
130
+ runActiveCallback("terminal reconciliation callback", (ctx, ownerId) => {
131
+ Observability.reconcileRunTerminalNotifications({
132
+ inFlight: terminalNotificationsInFlight,
133
+ ownerId,
134
+ sink: Pi.createNotificationSink(deps.pi, ctx),
135
+ state: observation,
136
+ includeAttention: true,
137
+ });
138
+ reportDiagnostics(ctx);
83
139
  });
84
- reportDiagnostics(ctx);
85
140
  },
86
- refreshWatcher: () => watcher.refresh(),
141
+ refreshWatcher: () => {
142
+ if (running)
143
+ watcher.refresh();
144
+ },
87
145
  });
88
- const close = () => {
89
- watcher.close();
90
- reconciliation.close();
91
- if (notifyTimeout)
92
- clearTimeout(notifyTimeout);
93
- notifyTimeout = undefined;
94
- if (animationInterval)
95
- clearInterval(animationInterval);
96
- animationInterval = undefined;
97
- };
98
146
  return {
99
147
  close,
100
- shutdown(eventReason, ctx) {
101
- close();
102
- const teardown = AsyncRuns.teardownRunsOwnedByParent(deps.getRunOwnerId(ctx), Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, { trigger: `session_shutdown:${eventReason}` });
103
- if (teardown.failed === 0)
148
+ shutdown(eventReason, ownerId, ctx) {
149
+ if (!ownerId)
150
+ return;
151
+ const teardown = (deps.teardownRunsOwnedByParent ?? AsyncRuns.teardownRunsOwnedByParent)(ownerId, Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, { trigger: `session_shutdown:${eventReason}` });
152
+ if (teardown.failed === 0 || !ctx)
104
153
  return;
105
154
  try {
106
155
  ctx.ui.notify(`Actor shutdown teardown: killed=${teardown.killed} failed=${teardown.failed} skipped=${teardown.skipped} discovery_failed=${teardown.discoveryFailed}. Summary: ${teardown.summaryPath ?? "unavailable"}.`, "warning");
@@ -109,17 +158,25 @@ export function createRunUiRuntime(deps) {
109
158
  /* stale shutdown context */
110
159
  }
111
160
  },
112
- start(ctx) {
161
+ start(ctx, ownerId) {
113
162
  close();
114
- Observability.primeRunAttentionState(observation, deps.getRunOwnerId(ctx));
115
- update(ctx, true, true);
116
- watcher.refresh();
117
- reconciliation.start();
118
- animationInterval = setInterval(() => {
119
- if (deps.getActiveContext() === ctx)
120
- update(ctx);
121
- }, 1000);
122
- animationInterval.unref?.();
163
+ activeContext = ctx;
164
+ activeOwnerId = ownerId;
165
+ running = true;
166
+ try {
167
+ Observability.primeRunAttentionState(observation, ownerId);
168
+ update(ctx, ownerId, true, true);
169
+ watcher.refresh();
170
+ reconciliation.start();
171
+ animationInterval = setInterval(() => {
172
+ runActiveCallback("status animation callback", (current, currentOwnerId) => update(current, currentOwnerId));
173
+ }, deps.animationIntervalMs ?? 1000);
174
+ animationInterval.unref?.();
175
+ }
176
+ catch (error) {
177
+ close();
178
+ throw error;
179
+ }
123
180
  },
124
181
  };
125
182
  }
@@ -3,8 +3,28 @@
3
3
  * Owns: artifact path template expansion and filesystem-backed artifact metadata.
4
4
  */
5
5
  import { createHash } from "node:crypto";
6
- import { readFileSync } from "node:fs";
6
+ import { closeSync, fstatSync, openSync, readSync, } from "node:fs";
7
7
  import { substituteCommandTemplateToken } from "./command-templates.js";
8
+ function hashArtifactFile(path) {
9
+ const fd = openSync(path, "r");
10
+ try {
11
+ const size = fstatSync(fd).size;
12
+ const hash = createHash("sha256");
13
+ const chunk = Buffer.allocUnsafe(64 * 1024);
14
+ let position = 0;
15
+ while (position < size) {
16
+ const bytesRead = readSync(fd, chunk, 0, Math.min(chunk.byteLength, size - position), position);
17
+ if (bytesRead === 0)
18
+ break;
19
+ hash.update(chunk.subarray(0, bytesRead));
20
+ position += bytesRead;
21
+ }
22
+ return { sha256: hash.digest("hex"), size: position };
23
+ }
24
+ finally {
25
+ closeSync(fd);
26
+ }
27
+ }
8
28
  export function resolveArtifactPaths(artifacts, values) {
9
29
  if (!artifacts)
10
30
  return undefined;
@@ -35,7 +55,7 @@ export function resolveArtifactManifest(artifacts) {
35
55
  if (!declaration?.path)
36
56
  continue;
37
57
  try {
38
- const content = readFileSync(declaration.path);
58
+ const hashed = hashArtifactFile(declaration.path);
39
59
  manifest[name] = {
40
60
  exists: true,
41
61
  ...(declaration.kind ? { kind: declaration.kind } : {}),
@@ -46,8 +66,8 @@ export function resolveArtifactManifest(artifacts) {
46
66
  ...(declaration.required !== undefined
47
67
  ? { required: declaration.required }
48
68
  : {}),
49
- sha256: createHash("sha256").update(content).digest("hex"),
50
- size: content.byteLength,
69
+ sha256: hashed.sha256,
70
+ size: hashed.size,
51
71
  };
52
72
  }
53
73
  catch {
@@ -63,7 +63,9 @@ export declare function createAutoToolsRuntime(deps: ToolRegistryRuntimeDeps): T
63
63
  export interface RecipeToolReloadWatcherDeps {
64
64
  exists?: (path: string) => boolean;
65
65
  getResolutionContext?: () => RecipeResolutionContext | undefined;
66
+ onCallbackError?: (error: unknown) => void;
66
67
  recipeRoot?: string;
68
+ reloadDelayMs?: number;
67
69
  watchPath?: typeof watch;
68
70
  }
69
71
  export declare function createRecipeToolReloadWatcher(runtime: Pick<ToolRegistryRuntime, "loadTools"> & Partial<Pick<ToolRegistryRuntime, "setWatchStatus">>, deps?: RecipeToolReloadWatcherDeps): RecipeToolReloadWatcher;
@@ -236,21 +236,41 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
236
236
  reloadTimeout = undefined;
237
237
  setWatchStatus("closed");
238
238
  };
239
+ const reportCallbackError = (error) => {
240
+ try {
241
+ deps.onCallbackError?.(error);
242
+ }
243
+ catch {
244
+ /* host callback containment must remain no-throw */
245
+ }
246
+ };
239
247
  const notifyFailure = (ctx) => {
240
248
  if (failureNotified)
241
249
  return;
242
250
  failureNotified = true;
243
251
  setWatchStatus("failed");
244
- ctx.ui.notify("Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.", "warning");
252
+ try {
253
+ ctx.ui.notify("Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.", "warning");
254
+ }
255
+ catch (error) {
256
+ reportCallbackError(error);
257
+ }
245
258
  };
246
259
  const scheduleReload = (ctx) => {
247
260
  failureNotified = false;
248
261
  if (reloadTimeout)
249
262
  clearTimeout(reloadTimeout);
250
263
  reloadTimeout = setTimeout(() => {
251
- runtime.loadTools(ctx, deps.getResolutionContext?.());
252
- ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
253
- }, 150);
264
+ reloadTimeout = undefined;
265
+ try {
266
+ runtime.loadTools(ctx, deps.getResolutionContext?.());
267
+ ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
268
+ }
269
+ catch (error) {
270
+ notifyFailure(ctx);
271
+ reportCallbackError(error);
272
+ }
273
+ }, deps.reloadDelayMs ?? 150);
254
274
  reloadTimeout.unref?.();
255
275
  };
256
276
  const watchParent = (ctx, recipeRoot) => {
@@ -37,6 +37,7 @@ export interface SessionEvidence {
37
37
  turns: SessionEvidenceTurn[];
38
38
  }
39
39
  export interface SessionEvidenceReadOptions {
40
+ maxBytes?: number;
40
41
  maxTextChars?: number;
41
42
  maxToolCalls?: number;
42
43
  maxTurns?: number;
@@ -105,10 +105,11 @@ function toolCalls(message, maxTextChars, maxToolCalls) {
105
105
  }));
106
106
  }
107
107
  export function readSessionEvidence(path, options = {}) {
108
+ const maxBytes = Math.max(1, options.maxBytes ?? Limits.SESSION_EVIDENCE_MAX_BYTES);
108
109
  const maxTextChars = Math.max(1, options.maxTextChars ?? Limits.SESSION_EVIDENCE_TEXT_CHARS);
109
110
  const maxToolCalls = Math.max(1, options.maxToolCalls ?? Limits.SESSION_EVIDENCE_MAX_TOOL_CALLS);
110
111
  const maxTurns = Math.max(1, options.maxTurns ?? Limits.SESSION_EVIDENCE_MAX_TURNS);
111
- const read = readJsonlFileResilient(path);
112
+ const read = readJsonlFileResilient(path, { maxBytes });
112
113
  const diagnostics = [...read.diagnostics];
113
114
  const header = read.records.find((entry) => entry.type === "session");
114
115
  const branch = activeBranch(read.records, path, diagnostics);
@@ -198,7 +199,7 @@ export function readSessionEvidence(path, options = {}) {
198
199
  path,
199
200
  ...(header ? { session: asRecord(header) } : {}),
200
201
  totalTurns: turns.length,
201
- truncated: turns.length > visibleTurns.length,
202
+ truncated: read.truncated === true || turns.length > visibleTurns.length,
202
203
  turns: visibleTurns,
203
204
  };
204
205
  }
@@ -15,7 +15,11 @@ export interface JsonReadResult<T> {
15
15
  export interface JsonlReadResult<T> {
16
16
  diagnostics: StateReadDiagnostic[];
17
17
  records: T[];
18
+ truncated?: boolean;
19
+ }
20
+ export interface JsonlReadOptions {
21
+ maxBytes?: number;
18
22
  }
19
23
  export declare function readJsonFileResilient<T>(path: string, fallback: T): JsonReadResult<T>;
20
- export declare function readJsonlFileResilient<T>(path: string): JsonlReadResult<T>;
24
+ export declare function readJsonlFileResilient<T>(path: string, options?: JsonlReadOptions): JsonlReadResult<T>;
21
25
  export declare function formatStateReadDiagnostics(diagnostics: StateReadDiagnostic[], limit?: number): string[];
@@ -3,13 +3,37 @@
3
3
  * Zones: file-backed actor state, JSON/JSONL diagnostics, inspect safety
4
4
  * Owns best-effort JSON and JSONL parsing helpers for operator-facing state reads.
5
5
  */
6
- import { readFileSync } from "node:fs";
6
+ import { closeSync, fstatSync, openSync, readFileSync, readSync, } from "node:fs";
7
7
  function isEnoent(error) {
8
8
  return error.code === "ENOENT";
9
9
  }
10
10
  function diagnosticMessage(error) {
11
11
  return error instanceof Error ? error.message : String(error);
12
12
  }
13
+ function readTextFileCapped(path, maxBytes) {
14
+ if (maxBytes === undefined)
15
+ return readFileSync(path, "utf8");
16
+ const limit = Math.max(0, Math.floor(maxBytes));
17
+ const fd = openSync(path, "r");
18
+ try {
19
+ const size = fstatSync(fd).size;
20
+ if (size > limit) {
21
+ throw Object.assign(new Error(`file exceeds bounded read limit (${size} > ${limit} bytes)`), { code: "EFBIG" });
22
+ }
23
+ const content = Buffer.allocUnsafe(size);
24
+ let offset = 0;
25
+ while (offset < size) {
26
+ const bytesRead = readSync(fd, content, offset, size - offset, offset);
27
+ if (bytesRead === 0)
28
+ break;
29
+ offset += bytesRead;
30
+ }
31
+ return content.subarray(0, offset).toString("utf8");
32
+ }
33
+ finally {
34
+ closeSync(fd);
35
+ }
36
+ }
13
37
  export function readJsonFileResilient(path, fallback) {
14
38
  try {
15
39
  return {
@@ -31,11 +55,11 @@ export function readJsonFileResilient(path, fallback) {
31
55
  };
32
56
  }
33
57
  }
34
- export function readJsonlFileResilient(path) {
58
+ export function readJsonlFileResilient(path, options = {}) {
35
59
  try {
36
60
  const records = [];
37
61
  const diagnostics = [];
38
- const lines = readFileSync(path, "utf8").split("\n");
62
+ const lines = readTextFileCapped(path, options.maxBytes).split("\n");
39
63
  for (const [index, line] of lines.entries()) {
40
64
  if (!line.trim())
41
65
  continue;
@@ -63,6 +87,9 @@ export function readJsonlFileResilient(path) {
63
87
  },
64
88
  ],
65
89
  records: [],
90
+ ...(error.code === "EFBIG"
91
+ ? { truncated: true }
92
+ : {}),
66
93
  };
67
94
  }
68
95
  }
@@ -16,4 +16,5 @@ export declare function sessionMismatchError(input: {
16
16
  run?: string;
17
17
  target?: string;
18
18
  }): Error;
19
+ export declare function assertRunStatusAccessibleToContext(runId: string, status: Record<string, unknown>, ctx: unknown): Record<string, unknown>;
19
20
  export declare function assertRunAccessibleToContext(runId: string, ctx: unknown): Record<string, unknown>;
@@ -10,7 +10,7 @@ export function getContextSessionId(ctx) {
10
10
  export function requireContextSessionId(ctx, actor) {
11
11
  const sessionId = getContextSessionId(ctx);
12
12
  if (!sessionId) {
13
- throw new Error(`${actor} requires a current coordinator session; use session:<id> or session:all for explicit session inventory.`);
13
+ throw Object.assign(new Error(`${actor} reason=session_unavailable requires a current coordinator session; retry from an active coordinator session.`), { reason: "session_unavailable" });
14
14
  }
15
15
  return sessionId;
16
16
  }
@@ -18,27 +18,29 @@ export function sessionMismatchError(input) {
18
18
  const ownerSession = input.expectedSession ?? "none";
19
19
  const currentSession = input.currentSession ?? "none";
20
20
  const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
21
- const hintTarget = input.expectedSession
22
- ? `session:${input.expectedSession}`
23
- : "session:all";
24
- return Object.assign(new Error(`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`), {
21
+ return Object.assign(new Error(`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_runtime_runs`), {
25
22
  current_session: input.currentSession,
26
- hint: `inspect target=${hintTarget} view=status`,
23
+ hint: "inspect target=runtime view=runs",
27
24
  owner_session: input.expectedSession,
28
25
  reason: "session_mismatch",
29
26
  run: input.run,
30
27
  target: input.target,
31
28
  });
32
29
  }
33
- export function assertRunAccessibleToContext(runId, ctx) {
34
- const status = AsyncRuns.getRunStatus(runId);
35
- const sessionId = getContextSessionId(ctx);
36
- if (sessionId && status.ownerId && status.ownerId !== sessionId) {
30
+ export function assertRunStatusAccessibleToContext(runId, status, ctx) {
31
+ const sessionId = requireContextSessionId(ctx, `run:${runId}`);
32
+ const ownerId = typeof status.ownerId === "string" && status.ownerId
33
+ ? status.ownerId
34
+ : undefined;
35
+ if (ownerId !== sessionId) {
37
36
  throw sessionMismatchError({
38
37
  currentSession: sessionId,
39
- expectedSession: String(status.ownerId),
38
+ expectedSession: ownerId,
40
39
  run: runId,
41
40
  });
42
41
  }
43
42
  return status;
44
43
  }
44
+ export function assertRunAccessibleToContext(runId, ctx) {
45
+ return assertRunStatusAccessibleToContext(runId, AsyncRuns.getRunStatus(runId), ctx);
46
+ }
@@ -27,14 +27,14 @@ const asRecord = ToolsResponse.asRecord;
27
27
  const maybeJsonText = ToolsResponse.maybeJsonText;
28
28
  function runtimeStatus() {
29
29
  return {
30
- automatic_review: process.env.PI_ACTORS_AUTOMATIC_REVIEW !== "off",
30
+ automatic_review: Paths.isAutomaticRecipeReviewEnabled(),
31
31
  run_root: Paths.getRunStateRoot(),
32
32
  state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
33
33
  version: RuntimeIdentity.getPackageVersion(),
34
34
  };
35
35
  }
36
36
  function runtimeRuns(ctx, deps, status) {
37
- const session = ToolsAccess.getContextSessionId(ctx);
37
+ const session = ToolsAccess.requireContextSessionId(ctx, "runtime Run inventory");
38
38
  const listed = deps.listRuns
39
39
  ? deps.listRuns()
40
40
  : AsyncRuns.listRuns(undefined, status);
@@ -49,8 +49,8 @@ function runtimeRuns(ctx, deps, status) {
49
49
  return run;
50
50
  }
51
51
  })
52
- .filter((run) => !session || !run.ownerId || run.ownerId === session);
53
- return { ...(session ? { owner_session: session } : {}), runs };
52
+ .filter((run) => run.ownerId === session);
53
+ return { owner_session: session, runs };
54
54
  }
55
55
  function runtimeTriage(ctx, deps) {
56
56
  const inventory = runtimeRuns(ctx, deps, undefined);
@@ -354,7 +354,7 @@ function inspectRun(run, view, input, ctx, deps) {
354
354
  throw new Error("inspect run:<id> supports view=recipe, view=trace, or view=control.");
355
355
  }
356
356
  const status = deps.getRunStatus
357
- ? deps.getRunStatus(run)
357
+ ? ToolsAccess.assertRunStatusAccessibleToContext(run, deps.getRunStatus(run), ctx)
358
358
  : ToolsAccess.assertRunAccessibleToContext(run, ctx);
359
359
  const stateDir = String(status.state_dir);
360
360
  if (view === "recipe") {
@@ -4,6 +4,7 @@
4
4
  * Owns public Control execution; journaling, delivery, and lifecycle mutation stay in Run domains.
5
5
  */
6
6
  export interface ControlToolDeps {
7
+ getRunStatus?: (run: string) => Record<string, unknown>;
7
8
  handleRuntimeControl?: (action: string, input: unknown) => Record<string, unknown>;
8
9
  }
9
10
  export declare function createControlToolDefinition<TContext = unknown>(deps?: ControlToolDeps): any;
@@ -67,7 +67,9 @@ export function createControlToolDefinition(deps = {}) {
67
67
  }
68
68
  else {
69
69
  const run = request.target.slice(4);
70
- const status = ToolsAccess.assertRunAccessibleToContext(run, ctx);
70
+ const status = deps.getRunStatus
71
+ ? ToolsAccess.assertRunStatusAccessibleToContext(run, deps.getRunStatus(run), ctx)
72
+ : ToolsAccess.assertRunAccessibleToContext(run, ctx);
71
73
  const runInstanceId = typeof status.run_instance_id === "string"
72
74
  ? status.run_instance_id
73
75
  : undefined;
@@ -8,6 +8,7 @@ Living index of all documentation in the `/docs` directory.
8
8
  - [template-recipes.md](./template-recipes.md) — Saved JSON/Markdown recipe standard, imports, and reusable command-template graph composition
9
9
  - [async-runs.md](./async-runs.md) — Run lifecycle, state, Control, Trace, cancellation, and terminal reconciliation
10
10
  - [actor-inspector.md](./actor-inspector.md) — Owner-filtered actor-instance navigation through Recipe, Trace, and Control
11
+ - [inspection.md](./inspection.md) — Complete `inspect` target/view matrix, authorization boundaries, and diagnostic routes
11
12
  - [tool-registry.md](./tool-registry.md) — Local `pi-actors` registry storage and `register_tool` adaptation
12
13
  - [recipe-library.md](./recipe-library.md) — Packaged standard recipe library such as async subagents, coordinator pipelines, utilities, and music playback
13
14
  - [releasing.md](./releasing.md) — Guarded tag validation, npm Trusted Publisher setup, registry verification, and GitHub Release convergence
@@ -19,13 +19,13 @@ Shows captured execution provenance:
19
19
  - declared artifacts and actor-local actions;
20
20
  - model/thinking policy and launch source.
21
21
 
22
- Captured Recipe evidence belongs to the Run generation and does not change when an active Recipe file later changes. Skill components display logical identities such as `artifacts/report`; private physical `source_file`, `skill_dir`, and `recipe_dir` stay out of Inspector and model-facing views. Non-empty object values render as indented brace-delimited property lists rather than flattened inline strings.
22
+ Captured Recipe evidence belongs to the Run generation and does not change when an active Recipe file later changes. Skill components display logical identities such as `artifacts/report`; private physical `source_file`, `skill_dir`, and `recipe_dir` stay out of Inspector and model-facing views. Non-empty objects render as indented brace-delimited property lists. Complex arrays use compact zero-based entries such as `#0: {` rather than Markdown list markers.
23
23
 
24
24
  ## Trace
25
25
 
26
- Shows the unified bounded Trace projection. Sources include lifecycle/runtime observations, Controls, owned Pi turns, command-log tails, results, artifacts, and diagnostics. Filter by source and open a row for structured detail.
26
+ Shows the unified bounded Trace projection. Sources include lifecycle/runtime observations, Controls, owned Pi turns, command-log tails, results, artifacts, and diagnostics. The source selector displays only `all` plus sources present in the current projection from `lifecycle`, `control`, `process`, `agent`, `artifact`, and `runtime`. Select a row to open structured detail.
27
27
 
28
- Trace ordering stays deterministic and newest-first: timestamp descending, same-source physical ordinal descending, fixed internal source rank, then stable id. Internal ordinals are never displayed or interpreted as cross-source causality. Row numbers still read chronologically from bottom to top: the oldest visible event is `#1` and the newest carries the highest number. The summary states whether retained history is complete; `runtime.trace_compacted` means older history was discarded and shows bounded cumulative drop evidence. Terminal/result/execution/artifact evidence keeps its own authority. The projection applies path containment and redaction before rendering.
28
+ Trace ordering stays deterministic and newest-first: timestamp descending, same-source physical ordinal descending, fixed internal source rank, then stable id. Internal ordinals are never displayed or interpreted as cross-source causality. Row numbers are zero-based chronological identities even though display is newest-first: the oldest visible event is `#0` and the newest carries the highest number. The summary states whether retained history is complete; `runtime.trace_compacted` means older history was discarded and shows bounded cumulative drop evidence. Terminal/result/execution/artifact evidence keeps its own authority. The projection applies path containment and redaction before rendering.
29
29
 
30
30
  ## Control
31
31
 
@@ -39,12 +39,28 @@ Shows:
39
39
 
40
40
  A service endpoint counts as ready only when `control-endpoint.json` matches the Run's immutable `run_instance_id`. Capacity reaches zero at 64 pending Controls; further requests are rejected before admission, while admitted nonterminal Controls never expire automatically. Runtime-owned kill remains available for a stuck saturated Run. Recent Control input and errors use the same bounded structured redaction as tool inspection. The durable `controls.jsonl` journal remains raw and local; rendering never mutates it or attaches an unredacted copy.
41
41
 
42
+ ## Focus and Selectors
43
+
44
+ `selectedBg` marks the current focus or selection; `customMessageBg` remains reserved for alternating content stripes. Opening the Run or Trace-source selector preserves `selectedBg` on its parent control, so focus reads as parent → child menu → selected option. Menus are composited over only their bounded rectangle; base content before, beside, and below that rectangle remains rendered.
45
+
46
+ The Run selector uses aligned zero-based sequence, Run name, and semantic status columns. The Trace selector uses `Trace: <source>`; when a non-`all` source is active, the tab projects the same colon grammar and value color.
47
+
42
48
  ## Keys
43
49
 
44
- The footer displays current bindings. Use tab navigation to switch Recipe/Trace/Control, movement keys to select rows, detail navigation to inspect evidence, refresh to reconcile disk state, and the documented kill key for lifecycle termination.
50
+ The footer is authoritative for the current focus. The stable navigation contract is:
51
+
52
+ | Focus | Keys |
53
+ | --- | --- |
54
+ | Run control | `←`/`→` change Run, `↓` enters tabs, `Enter` opens the Run selector, `k` requests kill when available |
55
+ | Tabs | `←`/`→` or `Tab` changes tab, `↑` returns to Run, `↓` enters content, `Enter` opens content or the Trace-source selector |
56
+ | Trace tab | `f` cycles present sources without opening the selector |
57
+ | List/document/detail | `↑`/`↓` and `PgUp`/`PgDn` navigate; `→`/`Enter` opens a Trace row; `←` or `Esc` moves back |
58
+ | Selector | `↑`/`↓` chooses, `Enter`/`→` applies, `←`/`Esc` cancels |
59
+ | Kill confirmation | `←`/`→` or `Tab` chooses, `Enter`/`y` confirms, `Esc`/`n` cancels |
60
+ | Overlay | `Esc` closes from the top level; `Ctrl-C` closes immediately |
45
61
 
46
62
  Run kill revalidates owner and generation through the canonical lifecycle path. After success, the Run status header is the sole confirmation; the content area does not duplicate it. The Inspector never edits state directly and never derives authority from displayed data.
47
63
 
48
64
  ## Scope
49
65
 
50
- The Actor Inspector treats each Run as a concrete actor instance. It does not expose group conversations, peer addresses, routing, or communication topology. Use `inspect target=runtime view=status`, `inspect target=recipes view=status`, and `inspect target=tool:<name> view=status` for non-Run management targets.
66
+ The Actor Inspector treats each Run as a concrete actor instance. It does not expose group conversations, peer addresses, routing, or communication topology. Use `inspect target=runtime view=status|runs|triage`, `inspect target=recipes view=status|summary|doctor|imports|reviews`, and `inspect target=tool:<name> view=status|schema` for non-Run management targets. See [Management Inspection](./inspection.md) for exact applicability and authorization boundaries.