@llblab/pi-actors 0.42.0 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKLOG.md +1 -5
- package/CHANGELOG.md +10 -0
- package/README.md +2 -0
- package/dist/lib/async-runs.d.ts +12 -0
- package/dist/lib/async-runs.js +46 -5
- package/dist/lib/command-templates.js +1 -1
- package/dist/lib/inspector-overlay.d.ts +4 -1
- package/dist/lib/inspector-overlay.js +71 -15
- package/dist/lib/inspector.js +1 -1
- package/dist/lib/observability.d.ts +12 -0
- package/dist/lib/observability.js +149 -5
- package/dist/lib/runs-control.d.ts +2 -0
- package/dist/lib/runs-control.js +14 -1
- package/dist/lib/runs-ownership.js +17 -3
- package/dist/lib/runs-process.js +4 -3
- package/dist/lib/runs-start.js +1 -0
- package/dist/lib/runs-status.js +3 -0
- package/dist/lib/tools-inspect.js +2 -1
- package/dist/lib/tools-local.js +17 -2
- package/dist/lib/tools-spawn.js +10 -1
- package/dist/scripts/async-runner.mjs +9 -9
- package/dist/scripts/build-dist.mjs +6 -1
- package/dist/scripts/conformance.mjs +6 -1
- package/dist/scripts/recipe-utils.mjs +3 -3
- package/dist/skills/actors/SKILL.md +1 -1
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +1 -1
- package/docs/async-runs.md +7 -1
- package/docs/tool-registry.md +2 -0
- package/lib/async-runs.ts +65 -5
- package/lib/command-templates.ts +1 -1
- package/lib/inspector-overlay.ts +67 -15
- package/lib/inspector.ts +1 -1
- package/lib/observability.ts +180 -4
- package/lib/runs-control.ts +20 -1
- package/lib/runs-ownership.ts +22 -3
- package/lib/runs-process.ts +4 -3
- package/lib/runs-start.ts +1 -0
- package/lib/runs-status.ts +5 -0
- package/lib/tools-inspect.ts +2 -1
- package/lib/tools-local.ts +21 -2
- package/lib/tools-spawn.ts +14 -1
- package/package.json +4 -3
- package/scripts/async-runner.mjs +9 -9
- package/scripts/build-dist.mjs +6 -1
- package/scripts/conformance.mjs +6 -1
- package/scripts/recipe-utils.mjs +3 -3
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
package/BACKLOG.md
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
1
|
# Project Backlog
|
|
2
2
|
|
|
3
|
-
|
|
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,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.42.1: Terminal Delivery and Cross-platform Validation
|
|
6
|
+
|
|
7
|
+
- `[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.
|
|
8
|
+
- `[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.
|
|
9
|
+
- `[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.
|
|
10
|
+
- `[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.
|
|
11
|
+
- `[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.
|
|
12
|
+
- `[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.
|
|
13
|
+
- `[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.
|
|
14
|
+
|
|
5
15
|
## 0.42.0: Automatic Recipe Evolution and Recipe-first Inspector
|
|
6
16
|
|
|
7
17
|
- `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
|
@@ -295,6 +295,8 @@ Packaged recipes are building blocks. Use `spawn file=<recipe>` for maintained p
|
|
|
295
295
|
|
|
296
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.
|
|
297
297
|
|
|
298
|
+
Terminal completion returns one bounded semantic result to the originating coordinator, not only run-state paths. `spawn` and saved async tools preserve their tool-call correlation automatically; callers may add `correlation_id` plus bounded `transport_context` when an adapter must retain an exact detached route, including Telegram `chat_id`/`thread_id`. Recipes that advertise `review.completed` either emit that envelope explicitly or receive one synthesized from successfully accepted review output. Watcher and periodic reconciliation share one in-flight guard, while failed sends remain retryable and visible in run status.
|
|
299
|
+
|
|
298
300
|
## Platform support
|
|
299
301
|
|
|
300
302
|
Core actor state, inspection, foreground tools, and basic async runs are portable Node.js behavior. Run-local messaging and stop/kill use platform adapters under the same `message` API.
|
package/dist/lib/async-runs.d.ts
CHANGED
|
@@ -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?: {
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -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
|
-
|
|
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,6 +317,8 @@ 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);
|
|
297
324
|
const child = spawn(process.execPath, argv, {
|
|
@@ -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(
|
|
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
|
|
68
|
+
private renderKillDialog;
|
|
67
69
|
private listItemCount;
|
|
68
70
|
private selectorAnchor;
|
|
69
71
|
private renderKeyHints;
|
|
@@ -108,5 +110,6 @@ export declare class ActorInspectorOverlay {
|
|
|
108
110
|
private row;
|
|
109
111
|
private takeVisiblePrefix;
|
|
110
112
|
private dropVisiblePrefix;
|
|
113
|
+
private center;
|
|
111
114
|
private fit;
|
|
112
115
|
}
|
|
@@ -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
|
-
|
|
71
|
-
|
|
72
|
-
this.
|
|
71
|
+
const key = data.toLowerCase();
|
|
72
|
+
if (matchesKey(data, "escape") || key === "n") {
|
|
73
|
+
this.cancelKill();
|
|
73
74
|
}
|
|
74
|
-
else if (matchesKey(data, "
|
|
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));
|
|
@@ -229,13 +244,11 @@ export class ActorInspectorOverlay {
|
|
|
229
244
|
else
|
|
230
245
|
lines.push(this.border("├", "", "┤", innerWidth));
|
|
231
246
|
this.contentStripeIndices = [];
|
|
232
|
-
const baseContent = this.
|
|
233
|
-
? this.
|
|
234
|
-
: this.
|
|
235
|
-
? this.
|
|
236
|
-
: this.
|
|
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
|
|
@@ -337,11 +350,19 @@ export class ActorInspectorOverlay {
|
|
|
337
350
|
this.killConfirmation = {
|
|
338
351
|
run: run.run,
|
|
339
352
|
runInstanceId: run.runInstanceId,
|
|
353
|
+
status: run.status,
|
|
340
354
|
};
|
|
355
|
+
this.killDialogChoice = "cancel";
|
|
356
|
+
}
|
|
357
|
+
cancelKill() {
|
|
358
|
+
this.feedback = { ok: false, message: "Kill cancelled." };
|
|
359
|
+
this.killConfirmation = undefined;
|
|
360
|
+
this.killDialogChoice = "cancel";
|
|
341
361
|
}
|
|
342
362
|
confirmKill() {
|
|
343
363
|
const confirmation = this.killConfirmation;
|
|
344
364
|
this.killConfirmation = undefined;
|
|
365
|
+
this.killDialogChoice = "cancel";
|
|
345
366
|
if (!confirmation || !this.killRun) {
|
|
346
367
|
this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
|
|
347
368
|
return;
|
|
@@ -357,11 +378,41 @@ export class ActorInspectorOverlay {
|
|
|
357
378
|
};
|
|
358
379
|
}
|
|
359
380
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
381
|
+
renderKillDialog(_width, innerWidth) {
|
|
382
|
+
const confirmation = this.killConfirmation;
|
|
383
|
+
const totalRows = this.contentViewportRows() + 7;
|
|
384
|
+
const cancel = this.killDialogChoice === "cancel"
|
|
385
|
+
? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
|
|
386
|
+
: this.theme.fg("muted", " Cancel ");
|
|
387
|
+
const kill = this.killDialogChoice === "kill"
|
|
388
|
+
? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
|
|
389
|
+
: this.theme.fg("error", " Kill actor ");
|
|
390
|
+
const body = [
|
|
391
|
+
"",
|
|
392
|
+
this.theme.fg("error", "Kill this running actor?"),
|
|
393
|
+
"",
|
|
394
|
+
`${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
|
|
395
|
+
`${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
|
|
396
|
+
"",
|
|
397
|
+
this.theme.fg("text", "This sends canonical control.kill."),
|
|
398
|
+
this.theme.fg("error", "The action is destructive and cannot be undone."),
|
|
399
|
+
"",
|
|
400
|
+
`${cancel} ${kill}`,
|
|
401
|
+
"",
|
|
364
402
|
];
|
|
403
|
+
const lines = [this.border("╭", " Confirm Actor Kill ", "╮", innerWidth)];
|
|
404
|
+
const availableRows = Math.max(1, totalRows - 2);
|
|
405
|
+
const topPadding = Math.max(0, Math.floor((availableRows - body.length) / 2));
|
|
406
|
+
const dialogRows = [
|
|
407
|
+
...Array.from({ length: topPadding }, () => ""),
|
|
408
|
+
...body,
|
|
409
|
+
].slice(0, availableRows);
|
|
410
|
+
while (dialogRows.length < availableRows)
|
|
411
|
+
dialogRows.push("");
|
|
412
|
+
for (const line of dialogRows)
|
|
413
|
+
lines.push(this.row(this.center(line, innerWidth), innerWidth));
|
|
414
|
+
lines.push(this.border("╰", " ←→/tab choose · enter activate · esc cancel ", "╯", innerWidth));
|
|
415
|
+
return lines;
|
|
365
416
|
}
|
|
366
417
|
listItemCount() {
|
|
367
418
|
if (this.tab === "recipe")
|
|
@@ -1062,6 +1113,11 @@ export class ActorInspectorOverlay {
|
|
|
1062
1113
|
}
|
|
1063
1114
|
return plain.slice(index);
|
|
1064
1115
|
}
|
|
1116
|
+
center(content, width) {
|
|
1117
|
+
const contentWidth = visibleWidth(content);
|
|
1118
|
+
const leading = Math.max(0, Math.floor((width - contentWidth) / 2));
|
|
1119
|
+
return `${" ".repeat(leading)}${content}`;
|
|
1120
|
+
}
|
|
1065
1121
|
fit(content, width) {
|
|
1066
1122
|
// A Component render entry must remain exactly one terminal row. Persisted
|
|
1067
1123
|
// prompts, messages, and tool results may contain line breaks or tabs;
|
package/dist/lib/inspector.js
CHANGED
|
@@ -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;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Zones: async runtime, ambient UI, diagnostics
|
|
4
4
|
* Owns ambient summaries, terminal events, and run outbox delivery for detached command-template runs
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, readdirSync, readFileSync, watch, } from "node:fs";
|
|
6
|
+
import { closeSync, existsSync, fstatSync, openSync, readdirSync, readFileSync, readSync, watch, } from "node:fs";
|
|
7
7
|
import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
|
|
8
8
|
import * as AsyncRuns from "./async-runs.js";
|
|
9
9
|
import * as Paths from "./paths.js";
|
|
@@ -54,6 +54,13 @@ export function deliverRunTransitionNotifications(transitions, sink, inFlight =
|
|
|
54
54
|
AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (transition.stateDir) {
|
|
59
|
+
AsyncRuns.recordRunTerminalDeliveryFailure(transition.stateDir, transition.to, error);
|
|
60
|
+
}
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
sink.notify(`Actor terminal delivery failed for run:${transition.run}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`, "error");
|
|
63
|
+
}
|
|
57
64
|
finally {
|
|
58
65
|
inFlight.delete(key);
|
|
59
66
|
}
|
|
@@ -282,6 +289,121 @@ function scanRunStateDirs(stateRoot, depth = 0, seen = new Set()) {
|
|
|
282
289
|
}
|
|
283
290
|
return result;
|
|
284
291
|
}
|
|
292
|
+
const TERMINAL_RESULT_BYTES = 8 * 1024;
|
|
293
|
+
const TERMINAL_RESULT_CHARS = 4_000;
|
|
294
|
+
function readBoundedStart(path) {
|
|
295
|
+
if (!existsSync(path))
|
|
296
|
+
return "";
|
|
297
|
+
const fd = openSync(path, "r");
|
|
298
|
+
try {
|
|
299
|
+
const size = Math.min(fstatSync(fd).size, TERMINAL_RESULT_BYTES);
|
|
300
|
+
const buffer = Buffer.alloc(size);
|
|
301
|
+
const bytes = readSync(fd, buffer, 0, size, 0);
|
|
302
|
+
const text = buffer.subarray(0, bytes).toString("utf8").trim();
|
|
303
|
+
return text.length > TERMINAL_RESULT_CHARS
|
|
304
|
+
? `${text.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
|
|
305
|
+
: text;
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
closeSync(fd);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function semanticBodyFromStdout(stateDir) {
|
|
312
|
+
const text = readBoundedStart(join(stateDir, "stdout.log"));
|
|
313
|
+
if (!text)
|
|
314
|
+
return undefined;
|
|
315
|
+
const withoutMarker = text.replace(/^ACTOR_REVIEW_RESULT\s*(?:\r?\n)?/, "").trim();
|
|
316
|
+
return withoutMarker || undefined;
|
|
317
|
+
}
|
|
318
|
+
function terminalSemanticResult(status, stateDir, observedStatus) {
|
|
319
|
+
const correlation = status.launch_correlation &&
|
|
320
|
+
typeof status.launch_correlation === "object" &&
|
|
321
|
+
!Array.isArray(status.launch_correlation)
|
|
322
|
+
? status.launch_correlation
|
|
323
|
+
: {};
|
|
324
|
+
const correlationId = typeof correlation.correlation_id === "string"
|
|
325
|
+
? correlation.correlation_id
|
|
326
|
+
: typeof correlation.tool_call_id === "string"
|
|
327
|
+
? correlation.tool_call_id
|
|
328
|
+
: undefined;
|
|
329
|
+
const mailbox = status.mailbox &&
|
|
330
|
+
typeof status.mailbox === "object" &&
|
|
331
|
+
!Array.isArray(status.mailbox)
|
|
332
|
+
? status.mailbox
|
|
333
|
+
: {};
|
|
334
|
+
const emits = Array.isArray(mailbox.emits)
|
|
335
|
+
? mailbox.emits.filter((item) => typeof item === "string")
|
|
336
|
+
: [];
|
|
337
|
+
const outbox = readJsonlFileResilient(join(stateDir, "outbox.jsonl")).records;
|
|
338
|
+
const explicit = outbox.findLast((record) => {
|
|
339
|
+
const type = String(record.type ?? record.event ?? "");
|
|
340
|
+
return emits.includes(type) && !["command.done", "run.done", "run.failed"].includes(type);
|
|
341
|
+
});
|
|
342
|
+
if (explicit) {
|
|
343
|
+
const type = String(explicit.type ?? explicit.event);
|
|
344
|
+
return {
|
|
345
|
+
...(explicit.body === undefined
|
|
346
|
+
? {} : { body: formatSemanticBody(explicit.body) }),
|
|
347
|
+
...(typeof explicit.correlation_id === "string"
|
|
348
|
+
? { correlationId: explicit.correlation_id } : correlationId ? { correlationId } : {}),
|
|
349
|
+
metadata: {
|
|
350
|
+
...(explicit.metadata &&
|
|
351
|
+
typeof explicit.metadata === "object" &&
|
|
352
|
+
!Array.isArray(explicit.metadata)
|
|
353
|
+
? explicit.metadata : {}),
|
|
354
|
+
...(status.transport_context &&
|
|
355
|
+
typeof status.transport_context === "object" &&
|
|
356
|
+
!Array.isArray(status.transport_context)
|
|
357
|
+
? {
|
|
358
|
+
transport_context: status.transport_context,
|
|
359
|
+
} : {}),
|
|
360
|
+
run: String(status.run ?? ""),
|
|
361
|
+
status: observedStatus,
|
|
362
|
+
},
|
|
363
|
+
summary: String(explicit.summary ?? type),
|
|
364
|
+
synthesized: false,
|
|
365
|
+
type,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const reviewCompleted = observedStatus === "done" && emits.includes("review.completed");
|
|
369
|
+
const result = status.result &&
|
|
370
|
+
typeof status.result === "object" &&
|
|
371
|
+
!Array.isArray(status.result)
|
|
372
|
+
? status.result
|
|
373
|
+
: {};
|
|
374
|
+
const type = reviewCompleted
|
|
375
|
+
? "review.completed"
|
|
376
|
+
: observedStatus === "done" ? "run.done" : "run.failed";
|
|
377
|
+
const stdoutBody = observedStatus === "done"
|
|
378
|
+
? semanticBodyFromStdout(stateDir) : undefined;
|
|
379
|
+
return {
|
|
380
|
+
...(stdoutBody
|
|
381
|
+
? { body: stdoutBody }
|
|
382
|
+
: typeof result.error === "string" ? { body: result.error } : {}),
|
|
383
|
+
...(correlationId ? { correlationId } : {}),
|
|
384
|
+
metadata: {
|
|
385
|
+
...(status.transport_context &&
|
|
386
|
+
typeof status.transport_context === "object" &&
|
|
387
|
+
!Array.isArray(status.transport_context)
|
|
388
|
+
? {
|
|
389
|
+
transport_context: status.transport_context,
|
|
390
|
+
} : {}),
|
|
391
|
+
run: String(status.run ?? ""), status: observedStatus,
|
|
392
|
+
},
|
|
393
|
+
summary: reviewCompleted ? "Review completed." :
|
|
394
|
+
observedStatus === "done" ? "Run completed." : `Run ${observedStatus}.`,
|
|
395
|
+
synthesized: true,
|
|
396
|
+
type,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function formatSemanticBody(body) {
|
|
400
|
+
const rendered = typeof body === "string" ? body : JSON.stringify(body);
|
|
401
|
+
const text = typeof rendered === "string" ? rendered : String(body);
|
|
402
|
+
const compact = text.trim();
|
|
403
|
+
return compact.length > TERMINAL_RESULT_CHARS
|
|
404
|
+
? `${compact.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
|
|
405
|
+
: compact;
|
|
406
|
+
}
|
|
285
407
|
function observeRun(stateDir) {
|
|
286
408
|
try {
|
|
287
409
|
const status = AsyncRuns.getRunStatus(stateDir);
|
|
@@ -289,9 +411,15 @@ function observeRun(stateDir) {
|
|
|
289
411
|
const run = typeof status.run === "string" ? status.run : undefined;
|
|
290
412
|
if (!run)
|
|
291
413
|
return undefined;
|
|
414
|
+
const observedStatus = status.status;
|
|
292
415
|
return {
|
|
293
416
|
activeSubagents: toNumber(progress.activeSubagents),
|
|
294
417
|
completed: toNumber(progress.completed),
|
|
418
|
+
...(status.launch_correlation &&
|
|
419
|
+
typeof status.launch_correlation === "object" &&
|
|
420
|
+
!Array.isArray(status.launch_correlation)
|
|
421
|
+
? { launchCorrelation: status.launch_correlation }
|
|
422
|
+
: {}),
|
|
295
423
|
failures: Array.isArray(progress.failures)
|
|
296
424
|
? progress.failures.length
|
|
297
425
|
: undefined,
|
|
@@ -321,9 +449,12 @@ function observeRun(stateDir) {
|
|
|
321
449
|
...(typeof status.retire_when === "string"
|
|
322
450
|
? { retireWhen: status.retire_when }
|
|
323
451
|
: {}),
|
|
452
|
+
...(TERMINAL.has(observedStatus)
|
|
453
|
+
? { semanticResult: terminalSemanticResult(status, stateDir, observedStatus) }
|
|
454
|
+
: {}),
|
|
324
455
|
run,
|
|
325
456
|
stateDir,
|
|
326
|
-
status:
|
|
457
|
+
status: observedStatus,
|
|
327
458
|
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
328
459
|
updatedAt: getUpdatedAt(status),
|
|
329
460
|
};
|
|
@@ -626,9 +757,11 @@ export function detectRunTransitions(previous, summary) {
|
|
|
626
757
|
run: run.run,
|
|
627
758
|
...(run.stateDir ? { stateDir: run.stateDir } : {}),
|
|
628
759
|
...(run.artifacts ? { artifacts: run.artifacts } : {}),
|
|
760
|
+
...(run.launchCorrelation ? { launchCorrelation: run.launchCorrelation } : {}),
|
|
629
761
|
...(run.launchSource ? { launchSource: run.launchSource } : {}),
|
|
630
762
|
...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
|
|
631
763
|
...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
|
|
764
|
+
...(run.semanticResult ? { semanticResult: run.semanticResult } : {}),
|
|
632
765
|
...(run.terminalHandled ? { terminalHandled: true } : {}),
|
|
633
766
|
to: run.status,
|
|
634
767
|
...(run.tool ? { tool: run.tool } : {}),
|
|
@@ -841,7 +974,8 @@ function isUserRecipeFile(file) {
|
|
|
841
974
|
return false;
|
|
842
975
|
const recipeRoot = resolve(Paths.getRecipeRoot());
|
|
843
976
|
const path = resolve(file);
|
|
844
|
-
|
|
977
|
+
const relation = relative(recipeRoot, path);
|
|
978
|
+
return relation === "" || (!relation.startsWith("..") && !isAbsolute(relation));
|
|
845
979
|
}
|
|
846
980
|
export function shouldSuggestRecipePersistence(transition) {
|
|
847
981
|
if (transition.to !== "done")
|
|
@@ -887,16 +1021,26 @@ function formatTransitionNextActions(transition) {
|
|
|
887
1021
|
].filter(Boolean);
|
|
888
1022
|
return `\nNext actions: ${actions.join(" | ")}`;
|
|
889
1023
|
}
|
|
1024
|
+
function formatTransitionSemanticResult(transition) {
|
|
1025
|
+
const result = transition.semanticResult;
|
|
1026
|
+
if (!result)
|
|
1027
|
+
return "";
|
|
1028
|
+
const correlation = result.correlationId
|
|
1029
|
+
? `\nCorrelation: ${result.correlationId}` : "";
|
|
1030
|
+
const body = result.body ? `\nResult:\n${result.body}` : "";
|
|
1031
|
+
return `\nSemantic result: ${result.type} — ${result.summary}${correlation}${body}`;
|
|
1032
|
+
}
|
|
890
1033
|
export function formatRunTransitionMessage(transition) {
|
|
891
1034
|
const artifacts = formatNamedArtifacts(transition.artifacts);
|
|
892
1035
|
const runFiles = formatRunFileList(getRunArtifacts(transition));
|
|
893
1036
|
const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
|
|
894
1037
|
const policy = formatTransitionPolicy(transition);
|
|
895
1038
|
const nextActions = formatTransitionNextActions(transition);
|
|
1039
|
+
const semanticResult = formatTransitionSemanticResult(transition);
|
|
896
1040
|
if (transition.to === "done")
|
|
897
|
-
return `Run ${transition.run} completed successfully.${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
|
|
1041
|
+
return `Run ${transition.run} completed successfully.${semanticResult}${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
|
|
898
1042
|
if (transition.to === "failed")
|
|
899
|
-
return `Run ${transition.run} failed.${policy}${artifacts}${runFiles}${nextActions}`;
|
|
1043
|
+
return `Run ${transition.run} failed.${semanticResult}${policy}${artifacts}${runFiles}${nextActions}`;
|
|
900
1044
|
if (transition.to === "cancelled")
|
|
901
1045
|
return `Run ${transition.run} was cancelled.${policy}${nextActions}`;
|
|
902
1046
|
if (transition.to === "killed")
|