@selesai/code 0.8.4 → 0.8.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.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/defaults/models.json +46 -55
- package/dist/defaults/settings.json +7 -8
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
- package/dist/extensions/pi-subagents/README.md +2 -0
- package/dist/extensions/pi-subagents/agents/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
- package/dist/extensions/pi-subagents/docs/observability.md +1 -1
- package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
- package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
- package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
- package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
- package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
- package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
- package/dist/extensions/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +18 -0
- package/dist/extensions/question/question-list.ts +22 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- package/dist/extensions/question/tests/helpers.test.ts +68 -35
- package/dist/extensions/question/tests/question-list.test.ts +506 -204
- package/dist/extensions/question/tests/row-layout.test.ts +180 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +48 -1
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/docs/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/quickstart.md +27 -43
- package/docs/settings.md +4 -0
- package/docs/usage.md +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import type { ProcessTreeTerminalV1 } from "../../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TERM_GRACE_MS = 3000;
|
|
5
|
+
const DEFAULT_KILL_VERIFY_MS = 1000;
|
|
6
|
+
const VERIFY_INTERVAL_MS = 25;
|
|
7
|
+
|
|
8
|
+
type SignalResult = "sent" | "absent" | { diagnostic: string };
|
|
9
|
+
|
|
10
|
+
function diagnostic(error: unknown): string {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function signalProcess(id: number, signal: NodeJS.Signals): SignalResult {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(id, signal);
|
|
17
|
+
return "sent";
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if ((error as NodeJS.ErrnoException).code === "ESRCH") return "absent";
|
|
20
|
+
return { diagnostic: diagnostic(error) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function activeProcessGroupMembers(processGroupId: number): number[] | { diagnostic: string } {
|
|
25
|
+
const result = spawnSync("ps", ["-axo", "pid=,pgid=,stat="], { encoding: "utf-8" });
|
|
26
|
+
if (result.error || result.status !== 0) {
|
|
27
|
+
return { diagnostic: result.error ? diagnostic(result.error) : (result.stderr.trim() || `ps exited with ${result.status}`) };
|
|
28
|
+
}
|
|
29
|
+
const members: number[] = [];
|
|
30
|
+
for (const line of result.stdout.split("\n")) {
|
|
31
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)/.exec(line);
|
|
32
|
+
if (!match || Number(match[2]) !== processGroupId || match[3]!.startsWith("Z")) continue;
|
|
33
|
+
members.push(Number(match[1]));
|
|
34
|
+
}
|
|
35
|
+
return members;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function waitUntilGroupTerminal(
|
|
39
|
+
processGroupId: number,
|
|
40
|
+
timeoutMs: number,
|
|
41
|
+
): Promise<false | { state: "enumeration-failed" | "still-active"; diagnostic: string }> {
|
|
42
|
+
const deadline = Date.now() + timeoutMs;
|
|
43
|
+
while (true) {
|
|
44
|
+
const members = activeProcessGroupMembers(processGroupId);
|
|
45
|
+
if (Array.isArray(members) && members.length === 0) return false;
|
|
46
|
+
const remaining = deadline - Date.now();
|
|
47
|
+
if (remaining <= 0) {
|
|
48
|
+
if (!Array.isArray(members)) return { state: "enumeration-failed", diagnostic: members.diagnostic };
|
|
49
|
+
return { state: "still-active", diagnostic: `Process group ${processGroupId} still has active members: ${members.join(", ")}.` };
|
|
50
|
+
}
|
|
51
|
+
await new Promise<void>((resolve) => setTimeout(resolve, Math.min(VERIFY_INTERVAL_MS, remaining)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function observed(processGroupId: number): ProcessTreeTerminalV1 {
|
|
56
|
+
return { state: "observed", mechanism: "posix-process-group", processGroupId, verifiedAt: Date.now() };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Owns one writer process group and arbitrates its cleanup exactly once. */
|
|
60
|
+
export interface OwnedProcessTreeController {
|
|
61
|
+
terminate(): Promise<ProcessTreeTerminalV1>;
|
|
62
|
+
finishAfterWriterClose(): Promise<ProcessTreeTerminalV1>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createOwnedProcessTreeController(
|
|
66
|
+
pid: number,
|
|
67
|
+
options: { termGraceMs?: number; killVerifyMs?: number } = {},
|
|
68
|
+
): OwnedProcessTreeController {
|
|
69
|
+
let termination: Promise<ProcessTreeTerminalV1> | undefined;
|
|
70
|
+
const posixGroupOwned = process.platform !== "win32";
|
|
71
|
+
const target = posixGroupOwned ? -pid : pid;
|
|
72
|
+
|
|
73
|
+
const terminate = (): Promise<ProcessTreeTerminalV1> => {
|
|
74
|
+
if (termination) return termination;
|
|
75
|
+
termination = (async () => {
|
|
76
|
+
if (!posixGroupOwned) {
|
|
77
|
+
signalProcess(target, "SIGTERM");
|
|
78
|
+
return { state: "unknown", reason: "unsupported-platform" };
|
|
79
|
+
}
|
|
80
|
+
const term = signalProcess(target, "SIGTERM");
|
|
81
|
+
if (term !== "sent" && term !== "absent") {
|
|
82
|
+
return { state: "unknown", reason: "signal-failed", diagnostic: term.diagnostic };
|
|
83
|
+
}
|
|
84
|
+
const termExit = await waitUntilGroupTerminal(pid, options.termGraceMs ?? DEFAULT_TERM_GRACE_MS);
|
|
85
|
+
if (termExit === false) return observed(pid);
|
|
86
|
+
|
|
87
|
+
const kill = signalProcess(target, "SIGKILL");
|
|
88
|
+
if (kill !== "sent" && kill !== "absent") {
|
|
89
|
+
const members = activeProcessGroupMembers(pid);
|
|
90
|
+
if (!Array.isArray(members) || members.length > 0) {
|
|
91
|
+
return { state: "unknown", reason: "signal-failed", diagnostic: kill.diagnostic };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const killExit = await waitUntilGroupTerminal(pid, options.killVerifyMs ?? DEFAULT_KILL_VERIFY_MS);
|
|
95
|
+
if (killExit !== false) {
|
|
96
|
+
return { state: "unknown", reason: "verification-failed", diagnostic: killExit.diagnostic };
|
|
97
|
+
}
|
|
98
|
+
return observed(pid);
|
|
99
|
+
})();
|
|
100
|
+
return termination;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return { terminate, finishAfterWriterClose: terminate };
|
|
104
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ProcessTerminalV1,
|
|
11
11
|
} from "../../shared/types.ts";
|
|
12
12
|
import { canonicalSessionId, inspectSessionLease } from "../shared/session-lease.ts";
|
|
13
|
+
import { releaseActiveRunIndex } from "./active-run-index.ts";
|
|
13
14
|
|
|
14
15
|
export interface ProcessTerminalCandidate {
|
|
15
16
|
version: 1;
|
|
@@ -40,9 +41,19 @@ function validProcessInstance(value: unknown, kind?: "runner" | "pi-writer"): va
|
|
|
40
41
|
if (typeof value.closeObservedAt !== "number" || !Number.isFinite(value.closeObservedAt)) return false;
|
|
41
42
|
if (typeof value.exitCode !== "number" && value.exitCode !== null) return false;
|
|
42
43
|
if (typeof value.signal !== "string" && value.signal !== null) return false;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
if (value.kind === "runner") return value.attempt === undefined;
|
|
45
|
+
if (typeof value.attempt !== "number" || !Number.isInteger(value.attempt) || value.attempt < 0 || !isRecord(value.processTree)) return false;
|
|
46
|
+
if (value.processTree.state === "observed") {
|
|
47
|
+
return value.processTree.mechanism === "posix-process-group"
|
|
48
|
+
&& typeof value.processTree.processGroupId === "number"
|
|
49
|
+
&& Number.isInteger(value.processTree.processGroupId)
|
|
50
|
+
&& value.processTree.processGroupId > 0
|
|
51
|
+
&& typeof value.processTree.verifiedAt === "number"
|
|
52
|
+
&& Number.isFinite(value.processTree.verifiedAt);
|
|
53
|
+
}
|
|
54
|
+
return value.processTree.state === "unknown"
|
|
55
|
+
&& ["unsupported-platform", "signal-failed", "verification-failed"].includes(String(value.processTree.reason))
|
|
56
|
+
&& (value.processTree.diagnostic === undefined || typeof value.processTree.diagnostic === "string");
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
function validInstance(value: unknown): value is ProcessInstanceExitV1 {
|
|
@@ -249,6 +260,8 @@ export function finalizeProcessTerminal(
|
|
|
249
260
|
proof = unknownProof(runId, runnerClose.processInstanceId, "canonical-session-release-unverified");
|
|
250
261
|
} else if (inconsistentWriters || (allWriters.length === 0 && expectedEntries.length === 0)) {
|
|
251
262
|
proof = unknownProof(runId, runnerClose.processInstanceId, "writer-close-unverified");
|
|
263
|
+
} else if (allWriters.some((writer) => writer.kind === "pi-writer" && writer.processTree.state !== "observed")) {
|
|
264
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, "process-tree-unverified");
|
|
252
265
|
} else {
|
|
253
266
|
const runner: ProcessInstanceExitV1 = { kind: "runner", ...runnerClose };
|
|
254
267
|
const canonicalSession = session && sessionProjection(candidate, session);
|
|
@@ -271,6 +284,7 @@ export function finalizeProcessTerminal(
|
|
|
271
284
|
try {
|
|
272
285
|
writeAtomicJson(processTerminalPath(asyncDir), proof);
|
|
273
286
|
durable = true;
|
|
287
|
+
if (proof.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
274
288
|
overlayStatus(asyncDir, proof, candidateForOverlay);
|
|
275
289
|
fs.appendFileSync(path.join(asyncDir, "events.jsonl"), `${JSON.stringify({ type: "subagent.run.process_terminal", lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, ts: Date.now(), runId, processTerminal: proof })}\n`, "utf-8");
|
|
276
290
|
} catch {
|
|
@@ -168,7 +168,24 @@ export function createResultWatcher(
|
|
|
168
168
|
const ownsSession = (sessionId: string, epoch: number) => {
|
|
169
169
|
if (!deliveryActive || epoch !== deliveryEpoch) return false;
|
|
170
170
|
if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
|
|
171
|
-
|
|
171
|
+
if (activeSessionId !== state.currentSessionId) return false;
|
|
172
|
+
if (sessionId === state.currentSessionId) return true;
|
|
173
|
+
return (state.sessionLineage ?? []).includes(sessionId);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const lineageOwnsResult = (identity: ResultFileIdentity, file: string): boolean => {
|
|
177
|
+
const lineage = state.sessionLineage;
|
|
178
|
+
if (!identity.sessionId || !lineage || !lineage.includes(identity.sessionId)) return false;
|
|
179
|
+
// Replay guard: only accept lineage results written around or after the
|
|
180
|
+
// adoption window. The previous session's watcher deleted every result it
|
|
181
|
+
// delivered, so older files are stale leftovers, not missed deliveries.
|
|
182
|
+
const windowStart = state.adoptedResultWindowStart;
|
|
183
|
+
if (windowStart === undefined) return false;
|
|
184
|
+
try {
|
|
185
|
+
return fsApi.statSync(path.join(resultsDir, file)).mtimeMs >= windowStart;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
172
189
|
};
|
|
173
190
|
|
|
174
191
|
const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
|
|
@@ -210,6 +227,7 @@ export function createResultWatcher(
|
|
|
210
227
|
// files keep their existing diagnostics and compatibility behavior.
|
|
211
228
|
if (!identity.sessionId) return true;
|
|
212
229
|
if (identity.sessionId === state.currentSessionId) return true;
|
|
230
|
+
if (lineageOwnsResult(identity, file)) return true;
|
|
213
231
|
if (identity.asyncDir && fsApi.existsSync(path.join(identity.asyncDir, MISSION_BINDING_FILE))) return true;
|
|
214
232
|
if (identity.runId && (observed ?? observedRunIds()).has(identity.runId)) return true;
|
|
215
233
|
return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
|
|
@@ -32,8 +32,8 @@ function boundedTaskSummary(value: string | undefined): string {
|
|
|
32
32
|
: normalized;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function listRetainedChildren(asyncDirRoot: string,
|
|
36
|
-
return listAsyncRuns(asyncDirRoot, {
|
|
35
|
+
export function listRetainedChildren(asyncDirRoot: string, sessionIds: string[]): RetainedChild[] {
|
|
36
|
+
return listAsyncRuns(asyncDirRoot, { sessionIds, states: ["complete"], reconcile: false })
|
|
37
37
|
.flatMap((run) => {
|
|
38
38
|
if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
|
|
39
39
|
const step = run.steps[0]!;
|
|
@@ -94,9 +94,10 @@ function toolCallIdAsyncLocations(toolCallId: string, asyncDirRoot: string, resu
|
|
|
94
94
|
|
|
95
95
|
function foregroundIds(state: SubagentState | undefined): string[] {
|
|
96
96
|
if (!state) return [];
|
|
97
|
+
const acceptedSessions = new Set(state.sessionLineage?.length ? state.sessionLineage : state.currentSessionId ? [state.currentSessionId] : []);
|
|
97
98
|
const remembered = state.currentSessionId
|
|
98
99
|
? [...(state.foregroundRuns?.values() ?? [])]
|
|
99
|
-
.filter((run) => run.sessionId
|
|
100
|
+
.filter((run) => run.sessionId !== undefined && acceptedSessions.has(run.sessionId))
|
|
100
101
|
.map((run) => run.runId)
|
|
101
102
|
: [];
|
|
102
103
|
return [...new Set([...state.foregroundControls.keys(), ...remembered])];
|
|
@@ -106,7 +107,9 @@ function hasExactForegroundId(state: SubagentState | undefined, id: string): boo
|
|
|
106
107
|
if (!state) return false;
|
|
107
108
|
if (state.foregroundControls.has(id)) return true;
|
|
108
109
|
const remembered = state.foregroundRuns?.get(id);
|
|
109
|
-
|
|
110
|
+
if (!remembered || !state.currentSessionId || remembered.sessionId === undefined) return false;
|
|
111
|
+
const acceptedSessions = new Set(state.sessionLineage?.length ? state.sessionLineage : [state.currentSessionId]);
|
|
112
|
+
return acceptedSessions.has(remembered.sessionId);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolutionScope | undefined {
|
|
@@ -19,6 +19,7 @@ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-
|
|
|
19
19
|
import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
20
20
|
import { readMissionBinding } from "../../missions/lifecycle.ts";
|
|
21
21
|
import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts";
|
|
22
|
+
import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
22
23
|
|
|
23
24
|
interface RunStatusParams {
|
|
24
25
|
action?: string;
|
|
@@ -216,6 +217,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
216
217
|
const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
|
|
217
218
|
const resultsDir = deps.resultsDir ?? DIRS.results;
|
|
218
219
|
const currentSessionId = deps.state?.currentSessionId ?? undefined;
|
|
220
|
+
const acceptedSessions = new Set<string>(deps.state?.sessionLineage?.length ? deps.state.sessionLineage : currentSessionId ? [currentSessionId] : []);
|
|
221
|
+
const ownsSession = (sessionId: string | undefined) => sessionId !== undefined && acceptedSessions.has(sessionId);
|
|
219
222
|
if (params.view && params.view !== "fleet" && params.view !== "transcript") {
|
|
220
223
|
return {
|
|
221
224
|
content: [{ type: "text", text: `Unknown status view: ${params.view}. Valid: fleet, transcript.` }],
|
|
@@ -235,7 +238,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
235
238
|
};
|
|
236
239
|
}
|
|
237
240
|
try {
|
|
238
|
-
const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"],
|
|
241
|
+
const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], ...(acceptedSessions.size ? { sessionIds: [...acceptedSessions] } : {}), resultsDir, kill: deps.kill, now: deps.now });
|
|
239
242
|
if (params.view === "transcript") {
|
|
240
243
|
if (runs.length === 1) return inspectSubagentStatus({ ...params, id: runs[0]!.id }, deps);
|
|
241
244
|
return {
|
|
@@ -331,9 +334,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
331
334
|
const status = reconciliation.status;
|
|
332
335
|
if (!status && diskStatus?.displayDismissedAt !== undefined) {
|
|
333
336
|
if (params.view === "transcript") {
|
|
334
|
-
if (currentSessionId && diskStatus.sessionId
|
|
337
|
+
if (currentSessionId && !ownsSession(diskStatus.sessionId)) {
|
|
335
338
|
return {
|
|
336
|
-
content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session." }],
|
|
339
|
+
content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session or its carried-over predecessors." }],
|
|
337
340
|
isError: true,
|
|
338
341
|
details: { mode: "single", results: [] },
|
|
339
342
|
};
|
|
@@ -350,9 +353,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
350
353
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
351
354
|
if (status) {
|
|
352
355
|
if (params.view === "transcript") {
|
|
353
|
-
if (currentSessionId && status.sessionId
|
|
356
|
+
if (currentSessionId && !ownsSession(status.sessionId)) {
|
|
354
357
|
return {
|
|
355
|
-
content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session." }],
|
|
358
|
+
content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session or its carried-over predecessors." }],
|
|
356
359
|
isError: true,
|
|
357
360
|
details: { mode: "single", results: [] },
|
|
358
361
|
};
|
|
@@ -389,6 +392,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
389
392
|
const steeringText = formatSteeringSummary(status);
|
|
390
393
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
|
|
391
394
|
?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
|
|
395
|
+
const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
|
|
396
|
+
const runFanoutBudget = runFanoutBudgetDescriptor ? getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor) : status.runFanoutBudget;
|
|
392
397
|
let missionId: string | undefined;
|
|
393
398
|
try {
|
|
394
399
|
missionId = readMissionBinding(asyncDir)?.missionId;
|
|
@@ -410,6 +415,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
410
415
|
statusActivityText ? `Activity: ${statusActivityText}` : undefined,
|
|
411
416
|
steeringText ? `Steering: ${steeringText}` : undefined,
|
|
412
417
|
`Mode: ${status.mode}`,
|
|
418
|
+
runFanoutBudget ? formatRunFanoutBudget(runFanoutBudget) : undefined,
|
|
413
419
|
status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
|
|
414
420
|
status.mode === "workflow" && workflowReturnPreview !== undefined ? `Return: ${workflowReturnPreview}` : undefined,
|
|
415
421
|
status.mode === "workflow" && workflowEmitPreview !== undefined ? `Latest emit: ${workflowEmitPreview}` : undefined,
|
|
@@ -466,7 +472,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
466
472
|
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
467
473
|
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
468
474
|
|
|
469
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
|
|
475
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
|
|
470
476
|
}
|
|
471
477
|
}
|
|
472
478
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
-
import { updateActiveRunIndex } from "./active-run-index.ts";
|
|
4
|
+
import { releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
5
5
|
import { readStatus } from "../../shared/utils.ts";
|
|
6
6
|
import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
|
|
7
7
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
@@ -261,7 +261,7 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
|
|
|
261
261
|
const repair = buildFailedRepair(status, asyncDir, now, reason);
|
|
262
262
|
writeAtomicJson(resultPath, repair.result);
|
|
263
263
|
writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
|
|
264
|
-
|
|
264
|
+
if (repair.status.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
265
265
|
appendJsonlBestEffort(path.join(asyncDir, "events.jsonl"), {
|
|
266
266
|
type: "subagent.run.repaired_stale",
|
|
267
267
|
ts: now,
|
|
@@ -351,7 +351,7 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
|
|
|
351
351
|
: undefined;
|
|
352
352
|
if (terminalStatus) {
|
|
353
353
|
writeAtomicJson(path.join(asyncDir, "status.json"), terminalStatus);
|
|
354
|
-
|
|
354
|
+
if (terminalStatus.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
355
355
|
return { status: terminalStatus, repaired: true, resultPath, message: "Existing async result file was used to repair stale running status." };
|
|
356
356
|
}
|
|
357
357
|
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath };
|