@llblab/pi-actors 0.42.0 → 0.42.2

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 (54) hide show
  1. package/AGENTS.md +2 -2
  2. package/BACKLOG.md +1 -5
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +1 -1
  5. package/dist/lib/async-runs.d.ts +12 -0
  6. package/dist/lib/async-runs.js +53 -12
  7. package/dist/lib/command-templates.js +1 -1
  8. package/dist/lib/inspector-overlay.d.ts +5 -1
  9. package/dist/lib/inspector-overlay.js +104 -36
  10. package/dist/lib/inspector.js +1 -1
  11. package/dist/lib/observability.d.ts +12 -1
  12. package/dist/lib/observability.js +159 -80
  13. package/dist/lib/prompts.d.ts +1 -1
  14. package/dist/lib/prompts.js +1 -1
  15. package/dist/lib/runs-control.d.ts +2 -0
  16. package/dist/lib/runs-control.js +14 -1
  17. package/dist/lib/runs-ownership.js +17 -3
  18. package/dist/lib/runs-process.js +4 -3
  19. package/dist/lib/runs-start.js +1 -0
  20. package/dist/lib/runs-status.js +3 -0
  21. package/dist/lib/tools-inspect.js +2 -1
  22. package/dist/lib/tools-local.js +17 -2
  23. package/dist/lib/tools-spawn.js +10 -1
  24. package/dist/scripts/async-runner.mjs +24 -24
  25. package/dist/scripts/build-dist.mjs +6 -1
  26. package/dist/scripts/conformance.mjs +6 -1
  27. package/dist/scripts/recipe-utils.mjs +3 -3
  28. package/dist/skills/actors/SKILL.md +3 -3
  29. package/dist/skills/swarm/SKILL.md +1 -1
  30. package/docs/actor-inspector.md +3 -3
  31. package/docs/async-runs.md +10 -4
  32. package/docs/recipe-library.md +1 -1
  33. package/docs/tool-registry.md +2 -0
  34. package/lib/async-runs.ts +72 -12
  35. package/lib/command-templates.ts +1 -1
  36. package/lib/inspector-overlay.ts +129 -36
  37. package/lib/inspector.ts +1 -1
  38. package/lib/observability.ts +194 -76
  39. package/lib/prompts.ts +1 -1
  40. package/lib/runs-control.ts +20 -1
  41. package/lib/runs-ownership.ts +22 -3
  42. package/lib/runs-process.ts +4 -3
  43. package/lib/runs-start.ts +1 -0
  44. package/lib/runs-status.ts +5 -0
  45. package/lib/tools-inspect.ts +2 -1
  46. package/lib/tools-local.ts +21 -2
  47. package/lib/tools-spawn.ts +14 -1
  48. package/package.json +4 -3
  49. package/scripts/async-runner.mjs +24 -24
  50. package/scripts/build-dist.mjs +6 -1
  51. package/scripts/conformance.mjs +6 -1
  52. package/scripts/recipe-utils.mjs +3 -3
  53. package/skills/actors/SKILL.md +3 -3
  54. package/skills/swarm/SKILL.md +1 -1
package/AGENTS.md CHANGED
@@ -108,7 +108,7 @@ Pi host
108
108
  - Preserve node controls: `when`, positive `timeout`, `delay`, bounded `retry`, `failure`, and `recover` cleanup.
109
109
  - Persist every async command's complete byte-exact stdout/stderr under command- and retry-specific run-state paths while keeping returned tails bounded and pipeline stdin complete.
110
110
  - Keep async run state under `~/.pi/agent/tmp/pi-actors/runs` with injected `{run_id}` and `{state_dir}` values.
111
- - Preserve event-driven observability with bounded reconciliation: file watchers accelerate durable retrying terminal follow-up notifications, while a conservative terminal-only interval recovers missed watcher activity, rearms degraded watchers, and never replays outbox traffic. Queue coordinator context through Pi follow-up delivery rather than steering so current work finishes before async results arrive and host follow-up batching policy can combine concurrent completions. Terminal delivery is at-least-once across the unavoidable send/handled-marker crash window; watch-triggered and periodic delivery share one live-runtime in-flight guard.
111
+ - Preserve event-driven observability with bounded reconciliation: file watchers accelerate durable retrying terminal follow-up notifications, while a conservative terminal-only interval recovers missed watcher activity, rearms degraded watchers, and never replays outbox traffic. Queue compact terminal coordinator notices through Pi follow-up delivery rather than steering so current work finishes before async results arrive and host follow-up batching policy can combine concurrent completions; LLM content contains only run id, status, one base path, and relative artifact names, while semantic output/error/correlation stays in non-LLM details and run state. Terminal delivery is at-least-once across the unavoidable send/handled-marker crash window; watch-triggered and periodic delivery share one live-runtime in-flight guard.
112
112
  - When a deferred actor result gates the next step, wait for its terminal follow-up. Do not schedule continuation loops, repeatedly inspect, or mutate its reviewed scope while it runs; inspect early only on operator request, meaningful actor event, or diagnosis of an overdue/stuck run.
113
113
  - Do not restore busy-polling examples, duplicate terminal notifications, or duplicate notifications for handled `cancel`, `kill`, or control-stop actions.
114
114
 
@@ -142,7 +142,7 @@ Pi host
142
142
  - Keep tail truncation, full-output temp files, failure formatting, and centralized limits intact.
143
143
  - Published docs must not include machine-local absolute paths.
144
144
  - Any view scanning run directories must apply coordinator/session ownership filters before exposing summaries or previews.
145
- - Actor Inspector remains evidence-first; its `Kill` action is available only for a focused owned running run, requires in-overlay confirmation, revalidates exact ownership/status at action time, routes through canonical `control.kill`, and renders bounded success/failure feedback without direct process signaling.
145
+ - Actor Inspector remains evidence-first; its `Kill` action is available only for a focused owned running run, requires in-overlay confirmation, revalidates exact ownership/status at action time, routes through canonical `control.kill`, and renders bounded success/failure feedback without direct process signaling. Keep active key hints in the bottom border as one blue-key/border-accent-description rail joined by border-accent `─`, never as a dedicated body row or bullet-separated footer.
146
146
  - Direct branch messages are active inbox queues; guard branch-local append/status rewrites with the branch inbox lock and keep claim/handled/failed transitions tested.
147
147
  - Run-state launch and destructive retention require the runtime ownership marker bound to the canonical directory and run id; reject non-run directories, missing/mismatched markers, and symlink aliases rather than trusting `run.json`.
148
148
  - Runner lifecycle and destructive process controls require the persisted cross-platform process identity proof (start time, command, and cwd where available), revalidated at authorization and immediately before signaling; dead, mismatched, or unsupported proofs stay distinct and fail closed rather than degrading to pid liveness. On Unix, only process-group `ESRCH` followed by another matching identity proof permits exact-pid fallback. Keep the residual post-read OS PID/PGID reuse window explicit because Node lacks one portable identity-stable group handle.
package/BACKLOG.md CHANGED
@@ -1,7 +1,3 @@
1
1
  # Project Backlog
2
2
 
3
- ## Open Work
4
-
5
- ### Windows filesystem fencing verification
6
-
7
- - [ ] Add Windows CI and native NTFS regressions for consolidation root identity, directory junction/reparse-point substitution, lifecycle locks, and recovery without requiring privileged symbolic-link creation. Keep bigint device/inode capture, document filesystems that report weak or zero file identity, and evaluate a native handle-relative mutation layer only if real Windows evidence justifies expanding beyond Node’s portable process-crash and trusted-state-tree contract.
3
+ No open items.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.42.2: Inspector Key Rail and Terminal Follow-up Context
6
+
7
+ - `[Inspector Key Rail]` Moved every Inspector hotkey hint onto the bottom border, replacing the dedicated two-row footer with a border-connected rail. Blue key labels remain, while descriptions and `─` connectors use the border accent instead of bullet separators; the main viewport cap rises from 16 to 24 rows. Impact: the Inspector gains two content rows without losing keyboard discoverability, and the kill confirmation dialog now shares the same visual grammar.
8
+ - `[Terminal Follow-up Context]` Kept terminal delivery on Pi's `followUp` queue but reduced LLM content to run id, status, one base path, and relative artifact names. Bounded semantic stdout/error payloads, correlation, and adapter transport metadata remain available only in non-LLM details and run state; the launcher writes initial progress before spawning its runner, and runners write terminal progress and review evidence before the `result.json` completion marker. Impact: completed background actors no longer inject raw output or unsolicited workflow prompts into coordinator context, and result readers never see an incomplete terminal state.
9
+
10
+ ## 0.42.1: Terminal Delivery and Cross-platform Validation
11
+
12
+ - `[Terminal Delivery]` Added one bounded semantic terminal result with durable launch/tool-call correlation and bounded adapter-provided transport context. Explicit advertised semantic envelopes win; successful accepted reviews that advertise `review.completed` deterministically synthesize it when absent, while failed runs include their terminal error. Watcher and reconciliation delivery share the existing live in-flight dedupe guard, failed sends persist bounded retry evidence without writing the handled marker, and status exposes the latest failure. Impact: coordinators and Telegram-style chat/thread adapters can retain the exact launch/result relationship across detached completion instead of receiving only run-file metadata.
13
+ - `[Inspector Kill Dialog]` Replaced the inline kill prompt with a dedicated responsive confirmation overlay showing the exact run and current status, explicit destructive copy, Cancel-first focus, arrow/Tab selection, Enter activation, and direct Y/N/Escape behavior. Exact owner, generation, and running status still revalidate through canonical control after confirmation, and bounded feedback returns to the Inspector. Impact: destructive actor termination now has an unmistakable, keyboard-predictable safety boundary.
14
+ - `[Cross-platform CI]` Added identical release-validation matrix jobs for Ubuntu, macOS, and Windows with fail-fast disabled, bounded timeouts, pinned Node setup, npm caching, and `npm ci`; dependency audit now runs once in a separate Ubuntu job. Added invariants that keep audit outside platform-neutral validation. Impact: the package has one baseline release path across all supported host filesystems and shells.
15
+ - `[macOS Run Ownership]` Recognized the exact platform-owned `/var` to `/private/var` temporary-root alias without weakening nested-parent or leaf-symlink rejection. Impact: run-state ownership remains fail-closed for caller-controlled aliases while macOS temporary directories pass canonical ownership checks.
16
+ - `[Portable Build and Cleanup]` Invoked local JavaScript entrypoints through Node instead of relying on shell-specific executable resolution, recognized both path separators when resolving run state, converted file URLs with the platform API, made path assertions native, serialized release suites that exercise shared watchers, and cancelled live fixtures before removing their state. Impact: Windows paths and builds remain shell-free while macOS and Windows cleanup no longer races active worker writes.
17
+ - `[Windows Filesystem Fencing]` Added native-only NTFS regressions for bigint consolidation-root identity, unprivileged directory-junction alias locking, lifecycle locking, draft-root reparse substitution, and trusted-root recovery replacement. Documented weak/zero device-inode filesystems and retained the portable realpath/root-identity/CAS contract without preemptively introducing a native handle-relative mutation layer. Impact: the Windows runner now owns concrete evidence for the deeper filesystem boundary rather than inheriting Unix symlink assumptions.
18
+ - `[Cross-platform Validation Evidence]` Completed the release-validation matrix on Ubuntu, macOS, and Windows in GitHub Actions run `30322093465`, including native NTFS bigint root identity, directory-junction mutation/lifecycle locking, draft-root junction substitution, and trusted-root replacement recovery regressions. The failures found while establishing the matrix were portable path, command, process-tree, temporary-root alias, line-ending, and fixture-lifecycle gaps; no residual trusted-state-tree substitution window was reproduced, so a native handle-relative mutation layer is not warranted. Impact: the release boundary now has direct platform evidence for the portable process-crash and trusted-state-tree contract.
19
+
5
20
  ## 0.42.0: Automatic Recipe Evolution and Recipe-first Inspector
6
21
 
7
22
  - `Recipe-first Inspector`: The Inspector now opens on a bounded, redacted `Recipe` document for the selected owned run, showing authored identity, resolved launch template and values, composition, policy, mailbox, and artifacts without following mutable external recipe paths. `Messages` and `Turns` remain adjacent tabs; Turns use one scrollable detail level without duplicated recipe context, and documents/timelines/details support arrows plus PageUp/PageDown. Nested values render as compact indented text with label-first, separator-aware wrapping and consistent lowercase hints. Recipe documents retain ↑-at-top/← navigation to tabs, and ← now also returns a scrolled Messages or Turns list directly to its tabs without resetting the selected row. Turns retain one cached evidence snapshot while list/detail navigation remains active, eliminating repeated full session-file parsing on every arrow key while preserving periodic refresh when focus returns to higher-level controls.
package/README.md CHANGED
@@ -293,7 +293,7 @@ Packaged recipes are building blocks. Use `spawn file=<recipe>` for maintained p
293
293
  | A useful output that should survive context compression | Artifacts |
294
294
  | A repeated local workflow | Recipe/tool memory |
295
295
 
296
- When a directly spawned inline/ad hoc actor or a recipe outside the user recipe root completes successfully, `pi-actors` may include a promotion suggestion in its terminal follow-up notification. The agent should ask first and never auto-save.
296
+ Terminal completion queues a minimal follow-up with run id, status, one base path, and relative artifact names only. Bounded semantic output, launch/tool-call correlation, and optional transport context remain in non-LLM follow-up details and run state, so adapters retain the exact launch/result relationship without injecting actor output into coordinator context. Inspect the run before deciding whether a successful pattern deserves recipe persistence; never auto-save without the operator's confirmation.
297
297
 
298
298
  ## Platform support
299
299
 
@@ -18,6 +18,7 @@ export interface AsyncRunControlEndpoint {
18
18
  path: string;
19
19
  type: "fifo" | "mailbox" | "named-pipe";
20
20
  }
21
+ export declare function normalizeRunTransportContext(value: unknown): Record<string, string | number | boolean> | undefined;
21
22
  export interface AsyncRunStartParams {
22
23
  async?: boolean;
23
24
  control?: AsyncRunControlEndpoint;
@@ -26,6 +27,10 @@ export interface AsyncRunStartParams {
26
27
  lifecycleHooks?: {
27
28
  onLockContention?(): void;
28
29
  };
30
+ launch_correlation?: {
31
+ correlation_id?: string;
32
+ tool_call_id?: string;
33
+ };
29
34
  name?: string;
30
35
  ownerId?: string;
31
36
  run_id?: string;
@@ -50,6 +55,7 @@ export interface AsyncRunStartParams {
50
55
  retry?: number | string;
51
56
  failure?: CommandTemplateFailureScope;
52
57
  recover?: CommandTemplateValue;
58
+ transport_context?: Record<string, unknown>;
53
59
  repeat?: number;
54
60
  values?: Record<string, unknown>;
55
61
  policy_values?: Record<string, unknown>;
@@ -62,6 +68,10 @@ export interface AsyncRunMeta {
62
68
  createdAt: string;
63
69
  cwd: string;
64
70
  launch_source?: AsyncRunLaunchSource;
71
+ launch_correlation?: {
72
+ correlation_id?: string;
73
+ tool_call_id?: string;
74
+ };
65
75
  ownerId?: string;
66
76
  pid: number;
67
77
  recipe?: string;
@@ -81,6 +91,7 @@ export interface AsyncRunMeta {
81
91
  process_identity?: RunProcessIdentity;
82
92
  recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
83
93
  retire_when?: "children_terminal";
94
+ transport_context?: Record<string, unknown>;
84
95
  }
85
96
  export { safeRunId } from "./runs-identity.ts";
86
97
  export { resolveArtifactManifest } from "./runs-artifacts.ts";
@@ -136,6 +147,7 @@ export interface RunControlExpectation {
136
147
  runInstanceId?: string;
137
148
  }
138
149
  export declare function markRunTerminalNotificationHandled(stateDir: string, status: string): void;
150
+ export declare function recordRunTerminalDeliveryFailure(stateDir: string, status: string, error: unknown): void;
139
151
  export declare function cancelRun(runOrDir: string, expected?: RunControlExpectation): Record<string, unknown>;
140
152
  export declare function archiveRun(runOrDir: string): Record<string, unknown>;
141
153
  export declare function pruneRun(runOrDir: string, options?: {
@@ -5,7 +5,7 @@
5
5
  import { spawn } from "node:child_process";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, writeFileSync, } from "node:fs";
8
- import { basename, dirname, extname, join, relative, resolve } from "node:path";
8
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { writeJsonAtomic } from "./file-state.js";
11
11
  import { CURRENT_MODEL_VALUE_KEY, CURRENT_THINKING_VALUE_KEY, describeCurrentPolicyProvenance, } from "./model-context.js";
@@ -27,6 +27,27 @@ import { deliverRunMessage, } from "./runs-messages.js";
27
27
  import { buildRunStatus, tailFile, tailLines, } from "./runs-status.js";
28
28
  import { readJsonFileResilient } from "./state-readers.js";
29
29
  const RUNNER_IDENTITY_GRACE_MS = 5000;
30
+ export function normalizeRunTransportContext(value) {
31
+ if (!value || typeof value !== "object" || Array.isArray(value))
32
+ return undefined;
33
+ const normalized = {};
34
+ for (const [key, item] of Object.entries(value).slice(0, 16)) {
35
+ const safeKey = key.trim().slice(0, 64);
36
+ if (!safeKey)
37
+ continue;
38
+ if (typeof item === "string") {
39
+ normalized[safeKey] = item.trim().slice(0, 256);
40
+ continue;
41
+ }
42
+ if (typeof item === "number" && Number.isFinite(item)) {
43
+ normalized[safeKey] = item;
44
+ continue;
45
+ }
46
+ if (typeof item === "boolean")
47
+ normalized[safeKey] = item;
48
+ }
49
+ return Object.keys(normalized).length ? normalized : undefined;
50
+ }
30
51
  const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
31
52
  const DEFAULT_RECIPE_ROOT = Paths.getRecipeRoot();
32
53
  function packageRoot() {
@@ -91,7 +112,8 @@ function resolveRecipeFile(file) {
91
112
  function isMutableUsageRecipeFile(file) {
92
113
  const userRoot = resolve(DEFAULT_RECIPE_ROOT);
93
114
  const resolved = resolve(file);
94
- return resolved.startsWith(`${userRoot}/`);
115
+ const relation = relative(userRoot, resolved);
116
+ return relation !== "" && !relation.startsWith("..") && !isAbsolute(relation);
95
117
  }
96
118
  function readRecipeFile(file) {
97
119
  const path = resolveRecipeFile(file);
@@ -260,6 +282,7 @@ export function startRun(params, cwd) {
260
282
  ...(startParams.defaults || {}),
261
283
  ...values,
262
284
  };
285
+ const transportContext = normalizeRunTransportContext(startParams.transport_context);
263
286
  const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
264
287
  const meta = {
265
288
  argv: [process.execPath, ...argv],
@@ -268,6 +291,8 @@ export function startRun(params, cwd) {
268
291
  ...(startParams.launch_source
269
292
  ? { launch_source: startParams.launch_source }
270
293
  : {}),
294
+ ...(startParams.launch_correlation
295
+ ? { launch_correlation: startParams.launch_correlation } : {}),
271
296
  ...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
272
297
  pid: 0,
273
298
  ...(recipe ? { recipe } : {}),
@@ -292,8 +317,17 @@ export function startRun(params, cwd) {
292
317
  ...(startParams.retire_when === "children_terminal"
293
318
  ? { retire_when: "children_terminal" }
294
319
  : {}),
320
+ ...(transportContext
321
+ ? { transport_context: transportContext } : {}),
295
322
  };
296
323
  writeJsonAtomic(join(stateDir, "run.json"), meta);
324
+ writeJsonAtomic(join(stateDir, "progress.json"), {
325
+ completed: 0,
326
+ failures: [],
327
+ model_policy: modelPolicy,
328
+ phase: "starting",
329
+ updatedAt: new Date().toISOString(),
330
+ });
297
331
  const child = spawn(process.execPath, argv, {
298
332
  cwd,
299
333
  detached: true,
@@ -306,13 +340,6 @@ export function startRun(params, cwd) {
306
340
  if (processIdentity)
307
341
  meta.process_identity = processIdentity;
308
342
  writeJsonAtomic(join(stateDir, "run.json"), meta);
309
- writeJsonAtomic(join(stateDir, "progress.json"), {
310
- completed: 0,
311
- failures: [],
312
- model_policy: modelPolicy,
313
- phase: "starting",
314
- updatedAt: new Date().toISOString(),
315
- });
316
343
  writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event: "run.start", run, run_instance_id: meta.run_instance_id, pid: meta.pid, ts: new Date().toISOString() })}\n`, { flag: "a" });
317
344
  child.unref();
318
345
  return meta;
@@ -323,7 +350,7 @@ export function startRun(params, cwd) {
323
350
  }
324
351
  export { parseRunOutboxEventLine } from "./runs-outbox.js";
325
352
  function resolveRunStateDir(runOrDir) {
326
- return resolve(runOrDir.includes("/")
353
+ return resolve(/[\\/]/u.test(runOrDir)
327
354
  ? runOrDir
328
355
  : join(DEFAULT_STATE_ROOT, safeRunId(runOrDir)));
329
356
  }
@@ -515,11 +542,11 @@ function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
515
542
  return {
516
543
  attempt: index + 1,
517
544
  stdout: {
518
- path: relative(stateDir, stdoutFile),
545
+ path: relative(stateDir, stdoutFile).replaceAll("\\", "/"),
519
546
  bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
520
547
  },
521
548
  stderr: {
522
- path: relative(stateDir, stderrFile),
549
+ path: relative(stateDir, stderrFile).replaceAll("\\", "/"),
523
550
  bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
524
551
  },
525
552
  };
@@ -606,6 +633,20 @@ export function markRunTerminalNotificationHandled(stateDir, status) {
606
633
  status,
607
634
  });
608
635
  }
636
+ export function recordRunTerminalDeliveryFailure(stateDir, status, error) {
637
+ const path = join(stateDir, "terminal-delivery-failure.json");
638
+ const previous = readJson(path);
639
+ const message = (error instanceof Error ? error.message : String(error))
640
+ .replaceAll(/\s+/g, " ")
641
+ .trim()
642
+ .slice(0, 500);
643
+ writeJsonAtomic(path, {
644
+ attempts: Math.max(0, Number(previous?.attempts ?? 0)) + 1,
645
+ error: message || "unknown delivery failure",
646
+ status,
647
+ ts: new Date().toISOString(),
648
+ });
649
+ }
609
650
  export function cancelRun(runOrDir, expected = {}) {
610
651
  const result = stopRun(runOrDir, "SIGTERM", "run.cancel", expected);
611
652
  return Object.hasOwn(result, "stopped")
@@ -344,7 +344,7 @@ export function splitCommandTemplate(input) {
344
344
  let active = false;
345
345
  for (const char of input) {
346
346
  if (escaped) {
347
- current += char;
347
+ current += /[\s'"\\]/u.test(char) ? char : `\\${char}`;
348
348
  escaped = false;
349
349
  active = true;
350
350
  continue;
@@ -40,6 +40,7 @@ export declare class ActorInspectorOverlay {
40
40
  private detailTurn?;
41
41
  private feedback?;
42
42
  private killConfirmation?;
43
+ private killDialogChoice;
43
44
  private readonly readKeys;
44
45
  private focus;
45
46
  private filterControlIndex;
@@ -62,8 +63,9 @@ export declare class ActorInspectorOverlay {
62
63
  private selectRun;
63
64
  private cycleRun;
64
65
  private requestKill;
66
+ private cancelKill;
65
67
  private confirmKill;
66
- private renderKillConfirmation;
68
+ private renderKillDialog;
67
69
  private listItemCount;
68
70
  private selectorAnchor;
69
71
  private renderKeyHints;
@@ -102,11 +104,13 @@ export declare class ActorInspectorOverlay {
102
104
  private openDetail;
103
105
  private backDetail;
104
106
  private closeDetail;
107
+ private footerBorder;
105
108
  private border;
106
109
  private stripeBackground;
107
110
  private stripedRow;
108
111
  private row;
109
112
  private takeVisiblePrefix;
110
113
  private dropVisiblePrefix;
114
+ private center;
111
115
  private fit;
112
116
  }
@@ -33,6 +33,7 @@ export class ActorInspectorOverlay {
33
33
  detailTurn;
34
34
  feedback;
35
35
  killConfirmation;
36
+ killDialogChoice = "cancel";
36
37
  readKeys = new Set();
37
38
  focus = "tabs";
38
39
  filterControlIndex = 0;
@@ -67,13 +68,25 @@ export class ActorInspectorOverlay {
67
68
  return;
68
69
  }
69
70
  if (this.killConfirmation) {
70
- if (matchesKey(data, "escape") || data.toLowerCase() === "n") {
71
- this.feedback = { ok: false, message: "Kill cancelled." };
72
- this.killConfirmation = undefined;
71
+ const key = data.toLowerCase();
72
+ if (matchesKey(data, "escape") || key === "n") {
73
+ this.cancelKill();
73
74
  }
74
- else if (matchesKey(data, "return") || data.toLowerCase() === "y") {
75
+ else if (matchesKey(data, "left") ||
76
+ matchesKey(data, "right") ||
77
+ matchesKey(data, "tab")) {
78
+ this.killDialogChoice =
79
+ this.killDialogChoice === "cancel" ? "kill" : "cancel";
80
+ }
81
+ else if (key === "y") {
75
82
  this.confirmKill();
76
83
  }
84
+ else if (matchesKey(data, "return")) {
85
+ if (this.killDialogChoice === "kill")
86
+ this.confirmKill();
87
+ else
88
+ this.cancelKill();
89
+ }
77
90
  this.tui.requestRender();
78
91
  return;
79
92
  }
@@ -200,6 +213,8 @@ export class ActorInspectorOverlay {
200
213
  this.ensureSelectedRun(this.runs());
201
214
  const safeWidth = Math.max(24, width);
202
215
  const innerWidth = Math.max(1, safeWidth - 2);
216
+ if (this.killConfirmation)
217
+ return this.renderKillDialog(safeWidth, innerWidth);
203
218
  const lines = [];
204
219
  lines.push(this.border("╭", " Actor Inspector ", "╮", innerWidth));
205
220
  lines.push(this.row(this.renderRunControl(), innerWidth));
@@ -224,18 +239,16 @@ export class ActorInspectorOverlay {
224
239
  if (selectorTop) {
225
240
  const leadingBorder = "─".repeat(selectorAnchor);
226
241
  const remainingBorder = "─".repeat(Math.max(0, innerWidth - visibleWidth(selectorTop) - selectorAnchor));
227
- lines.push(`${this.theme.fg("border", `├${leadingBorder}`)}${selectorTop}${this.theme.fg("border", `${remainingBorder}┤`)}`);
242
+ lines.push(`${this.theme.fg("borderAccent", `├${leadingBorder}`)}${selectorTop}${this.theme.fg("borderAccent", `${remainingBorder}┤`)}`);
228
243
  }
229
244
  else
230
245
  lines.push(this.border("├", "", "┤", innerWidth));
231
246
  this.contentStripeIndices = [];
232
- const baseContent = this.killConfirmation
233
- ? this.renderKillConfirmation()
234
- : this.detailOpen
235
- ? this.renderDetail(innerWidth)
236
- : this.selectedRun
237
- ? this.renderTimeline(innerWidth)
238
- : this.renderRunSelector(innerWidth);
247
+ const baseContent = this.detailOpen
248
+ ? this.renderDetail(innerWidth)
249
+ : this.selectedRun
250
+ ? this.renderTimeline(innerWidth)
251
+ : this.renderRunSelector(innerWidth);
239
252
  const content = this.feedback && !this.killConfirmation
240
253
  ? [
241
254
  this.theme.fg(this.feedback.ok
@@ -267,9 +280,7 @@ export class ActorInspectorOverlay {
267
280
  const preservedBase = this.stripeBackground(this.fit(this.dropVisiblePrefix(base, popupWidth + popupAnchor), baseWidth), stripeIndex);
268
281
  lines.push(this.row(`${leadingBase}${visiblePopup}${preservedBase}`, innerWidth, true));
269
282
  }
270
- lines.push(this.border("├", "", "┤", innerWidth));
271
- lines.push(this.row(this.renderKeyHints(), innerWidth));
272
- lines.push(this.border("╰", "", "╯", innerWidth));
283
+ lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
273
284
  return lines;
274
285
  }
275
286
  invalidate() { }
@@ -285,7 +296,7 @@ export class ActorInspectorOverlay {
285
296
  }
286
297
  contentViewportRows() {
287
298
  const overlayRows = Math.floor(this.tui.terminal.rows * 0.94);
288
- return Math.max(4, Math.min(16, overlayRows - 7));
299
+ return Math.max(4, Math.min(24, overlayRows - 5));
289
300
  }
290
301
  selectRun(run, index) {
291
302
  this.selectedRun = run;
@@ -337,11 +348,19 @@ export class ActorInspectorOverlay {
337
348
  this.killConfirmation = {
338
349
  run: run.run,
339
350
  runInstanceId: run.runInstanceId,
351
+ status: run.status,
340
352
  };
353
+ this.killDialogChoice = "cancel";
354
+ }
355
+ cancelKill() {
356
+ this.feedback = { ok: false, message: "Kill cancelled." };
357
+ this.killConfirmation = undefined;
358
+ this.killDialogChoice = "cancel";
341
359
  }
342
360
  confirmKill() {
343
361
  const confirmation = this.killConfirmation;
344
362
  this.killConfirmation = undefined;
363
+ this.killDialogChoice = "cancel";
345
364
  if (!confirmation || !this.killRun) {
346
365
  this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
347
366
  return;
@@ -357,11 +376,41 @@ export class ActorInspectorOverlay {
357
376
  };
358
377
  }
359
378
  }
360
- renderKillConfirmation() {
361
- return [
362
- this.theme.fg("error", ` Kill running actor run:${this.killConfirmation?.run}?`),
363
- this.theme.fg("muted", " This sends canonical control.kill and cannot be undone."),
379
+ renderKillDialog(_width, innerWidth) {
380
+ const confirmation = this.killConfirmation;
381
+ const totalRows = this.contentViewportRows() + 5;
382
+ const cancel = this.killDialogChoice === "cancel"
383
+ ? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
384
+ : this.theme.fg("muted", " Cancel ");
385
+ const kill = this.killDialogChoice === "kill"
386
+ ? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
387
+ : this.theme.fg("error", " Kill actor ");
388
+ const body = [
389
+ "",
390
+ this.theme.fg("error", "Kill this running actor?"),
391
+ "",
392
+ `${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
393
+ `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
394
+ "",
395
+ this.theme.fg("text", "This sends canonical control.kill."),
396
+ this.theme.fg("error", "The action is destructive and cannot be undone."),
397
+ "",
398
+ `${cancel} ${kill}`,
399
+ "",
364
400
  ];
401
+ const lines = [this.border("╭", " Confirm Actor Kill ", "╮", innerWidth)];
402
+ const availableRows = Math.max(1, totalRows - 2);
403
+ const topPadding = Math.max(0, Math.floor((availableRows - body.length) / 2));
404
+ const dialogRows = [
405
+ ...Array.from({ length: topPadding }, () => ""),
406
+ ...body,
407
+ ].slice(0, availableRows);
408
+ while (dialogRows.length < availableRows)
409
+ dialogRows.push("");
410
+ for (const line of dialogRows)
411
+ lines.push(this.row(this.center(line, innerWidth), innerWidth));
412
+ lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
413
+ return lines;
365
414
  }
366
415
  listItemCount() {
367
416
  if (this.tab === "recipe")
@@ -384,27 +433,34 @@ export class ActorInspectorOverlay {
384
433
  return Math.max(0, tabs.indexOf(label) - 2);
385
434
  }
386
435
  renderKeyHints() {
387
- const hint = (keys, description) => `${this.theme.fg("accent", keys)}${this.theme.fg("dim", ` ${description}`)}`;
436
+ const hint = (keys, description) => `${this.theme.fg("accent", keys)}${this.theme.fg("borderAccent", ` ${description}`)}`;
437
+ const divider = this.theme.fg("borderAccent", " ─ ");
438
+ const hints = (...items) => items.map(([keys, description]) => hint(keys, description)).join(divider);
388
439
  if (this.killConfirmation)
389
- return ` ${hint("y/enter", "confirm kill")} ${hint("n/esc", "cancel")}`;
440
+ return hints(["←→/tab", "choose"], ["enter/y", "confirm"], ["esc/n", "cancel"]);
390
441
  if (this.focus === "select")
391
442
  return this.menuLevel === "value"
392
- ? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
393
- : ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
443
+ ? hints(["↑↓", "option"], ["enter", "apply"], ["←/esc", "back"])
444
+ : hints(["↑↓", "option"], ["→/enter", "open"], ["←/esc", "back"]);
394
445
  if (this.focus === "recipe")
395
- return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("↑ at top", "tabs")} ${hint("←/esc", "tabs")}`;
446
+ return hints(["↑↓/pgup/pgdn", "scroll"], ["↑ at top", "tabs"], ["←/esc", "tabs"]);
396
447
  if (this.focus === "detail")
397
- return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("←/esc", "back")}`;
448
+ return hints(["↑↓/pgup/pgdn", "scroll"], ["←/esc", "back"]);
398
449
  if (this.focus === "list")
399
- return ` ${hint("↑↓/pgup/pgdn", "row")} ${hint("→/enter", "open")} ${hint("←", "tabs")} ${hint("esc", "close")}`;
450
+ return hints(["↑↓/pgup/pgdn", "row"], ["→/enter", "open"], ["←", "tabs"], ["esc", "close"]);
400
451
  if (this.focus === "runs") {
452
+ const items = [
453
+ ["←→", "run"],
454
+ ["↓", "tabs"],
455
+ ["enter", "list"],
456
+ ];
401
457
  const run = this.runs().find((item) => item.run === this.selectedRun);
402
- const killHint = this.killRun && run?.status === "running" && run.runInstanceId
403
- ? ` ${hint("k", "kill")}`
404
- : "";
405
- return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")}${killHint} ${hint("esc", "close")}`;
458
+ if (this.killRun && run?.status === "running" && run.runInstanceId)
459
+ items.push(["k", "kill"]);
460
+ items.push(["esc", "close"]);
461
+ return hints(...items);
406
462
  }
407
- return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
463
+ return hints(["←→", "navigate"], ["↑↓", "change row"], ["enter", "select"], ["esc", "close"]);
408
464
  }
409
465
  renderRunControl() {
410
466
  const runs = this.runs();
@@ -962,7 +1018,7 @@ export class ActorInspectorOverlay {
962
1018
  const hiddenAbove = start > 0;
963
1019
  const hiddenBelow = start + visibleOptions.length < options.length;
964
1020
  const contentWidth = Math.max(4, ...options.map((option) => visibleWidth(option) + 4));
965
- const border = (left, right, marker = "") => this.theme.fg("border", `${omitLeftBorder ? "" : left}${marker}${"─".repeat(Math.max(0, contentWidth - visibleWidth(marker)))}${omitRightBorder ? "" : right}`);
1021
+ const border = (left, right, marker = "") => this.theme.fg("borderAccent", `${omitLeftBorder ? "" : left}${marker}${"─".repeat(Math.max(0, contentWidth - visibleWidth(marker)))}${omitRightBorder ? "" : right}`);
966
1022
  return [
967
1023
  border("╭", "╮", hiddenAbove ? "↑" : ""),
968
1024
  ...visibleOptions.map((option, localIndex) => {
@@ -975,7 +1031,7 @@ export class ActorInspectorOverlay {
975
1031
  const styled = focused || index === parentIndex
976
1032
  ? this.theme.bg("selectedBg", colored)
977
1033
  : colored;
978
- return `${omitLeftBorder ? "" : this.theme.fg("border", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("border", "│")}`;
1034
+ return `${omitLeftBorder ? "" : this.theme.fg("borderAccent", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("borderAccent", "│")}`;
979
1035
  }),
980
1036
  border("╰", "╯", hiddenBelow ? "↓" : ""),
981
1037
  ];
@@ -1017,10 +1073,17 @@ export class ActorInspectorOverlay {
1017
1073
  this.detailScroll = 0;
1018
1074
  this.focus = "list";
1019
1075
  }
1076
+ footerBorder(hints, width) {
1077
+ const prefix = " ";
1078
+ const suffix = " ─";
1079
+ const content = truncateToWidth(hints, Math.max(0, width - visibleWidth(prefix) - visibleWidth(suffix)), "");
1080
+ const fill = "─".repeat(Math.max(0, width - visibleWidth(prefix) - visibleWidth(suffix) - visibleWidth(content)));
1081
+ return `${this.theme.fg("borderAccent", `╰${prefix}`)}${content}${this.theme.fg("borderAccent", `${suffix}${fill}╯`)}`;
1082
+ }
1020
1083
  border(left, title, right, width) {
1021
1084
  const titleText = truncateToWidth(title, width, "");
1022
1085
  const fill = "─".repeat(Math.max(0, width - visibleWidth(titleText)));
1023
- return this.theme.fg("border", `${left}${titleText}${fill}${right}`);
1086
+ return this.theme.fg("borderAccent", `${left}${titleText}${fill}${right}`);
1024
1087
  }
1025
1088
  stripeBackground(content, index) {
1026
1089
  if (index % 2 === 0)
@@ -1035,7 +1098,7 @@ export class ActorInspectorOverlay {
1035
1098
  }
1036
1099
  row(content, width, fitted = false) {
1037
1100
  const body = fitted ? content : this.fit(content, width);
1038
- return `${this.theme.fg("border", "│")}${body}${this.theme.fg("border", "│")}`;
1101
+ return `${this.theme.fg("borderAccent", "│")}${body}${this.theme.fg("borderAccent", "│")}`;
1039
1102
  }
1040
1103
  takeVisiblePrefix(content, width) {
1041
1104
  const plain = stripVTControlCharacters(content);
@@ -1062,6 +1125,11 @@ export class ActorInspectorOverlay {
1062
1125
  }
1063
1126
  return plain.slice(index);
1064
1127
  }
1128
+ center(content, width) {
1129
+ const contentWidth = visibleWidth(content);
1130
+ const leading = Math.max(0, Math.floor((width - contentWidth) / 2));
1131
+ return `${" ".repeat(leading)}${content}`;
1132
+ }
1065
1133
  fit(content, width) {
1066
1134
  // A Component render entry must remain exactly one terminal row. Persisted
1067
1135
  // prompts, messages, and tool results may contain line breaks or tabs;
@@ -126,7 +126,7 @@ export function readActorInspectorTurns(stateDir) {
126
126
  withFileTypes: true,
127
127
  })
128
128
  .filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
129
- .map((file) => path.join("sessions", entry.name, file.name))
129
+ .map((file) => path.posix.join("sessions", entry.name, file.name))
130
130
  .filter((file) => !recordedFiles.has(file));
131
131
  return files.length > 0
132
132
  ? [{ id: entry.name, session_files: files, stage: "subagent" }]
@@ -15,6 +15,7 @@ export interface RunObservation {
15
15
  failures?: number;
16
16
  ownerId?: string;
17
17
  artifacts?: Record<string, string>;
18
+ launchCorrelation?: Record<string, string>;
18
19
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
19
20
  modelPolicy?: Record<string, unknown>;
20
21
  notificationPolicy?: "normal" | "silent";
@@ -22,6 +23,7 @@ export interface RunObservation {
22
23
  terminalHandled?: boolean;
23
24
  retireWhen?: string;
24
25
  run: string;
26
+ semanticResult?: RunTerminalSemanticResult;
25
27
  tool?: string;
26
28
  stateDir?: string;
27
29
  status: RunObservedStatus;
@@ -131,12 +133,22 @@ export interface RunTransition {
131
133
  run: string;
132
134
  stateDir?: string;
133
135
  artifacts?: Record<string, string>;
136
+ launchCorrelation?: Record<string, string>;
134
137
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
135
138
  modelPolicy?: Record<string, unknown>;
136
139
  recipeFile?: string;
137
140
  terminalHandled?: boolean;
138
141
  to: RunObservedStatus;
139
142
  tool?: string;
143
+ semanticResult?: RunTerminalSemanticResult;
144
+ }
145
+ export interface RunTerminalSemanticResult {
146
+ body?: string;
147
+ correlationId?: string;
148
+ metadata: Record<string, unknown>;
149
+ summary: string;
150
+ synthesized: boolean;
151
+ type: string;
140
152
  }
141
153
  export interface RunOutboxEvent {
142
154
  body?: unknown;
@@ -169,5 +181,4 @@ export declare function formatRunOutboxMessage(event: RunOutboxEvent): string;
169
181
  export declare function getRunTransitionNotificationType(transition: RunTransition): RunTransitionNotificationType;
170
182
  export declare function shouldNotifyRunTransition(transition: RunTransition): boolean;
171
183
  export declare function shouldSendRunTransitionFollowUp(transition: RunTransition): boolean;
172
- export declare function shouldSuggestRecipePersistence(transition: RunTransition): boolean;
173
184
  export declare function formatRunTransitionMessage(transition: RunTransition): string;