pi-subagents 0.49.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/agents/gpt-pro.md +17 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +37 -2
- package/docs/configuration.md +76 -14
- package/docs/extension-api.md +78 -1
- package/docs/missions.md +1 -1
- package/docs/observability.md +20 -4
- package/docs/tool-reference.md +55 -39
- package/docs/workflows.md +171 -5
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
- package/skills/pi-subagents/references/execution-controls.md +22 -18
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +135 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +42 -11
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +36 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +210 -90
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +14 -36
- package/src/extension/tool-description.ts +37 -24
- package/src/inspectors/herdr/actions.ts +5 -9
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +4 -8
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +49 -51
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +94 -1
- package/src/runs/background/async-execution.ts +98 -38
- package/src/runs/background/async-job-tracker.ts +21 -4
- package/src/runs/background/async-resume.ts +30 -17
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +47 -56
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +505 -0
- package/src/runs/background/result-watcher.ts +250 -51
- package/src/runs/background/retained-children.ts +79 -20
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +37 -29
- package/src/runs/background/run-status.ts +34 -20
- package/src/runs/background/scheduled-runs.ts +71 -27
- package/src/runs/background/stale-run-reconciler.ts +33 -16
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +535 -161
- package/src/runs/background/subagent-wait.ts +9 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +22 -2
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +221 -15
- package/src/runs/foreground/subagent-executor.ts +529 -1519
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/external-job-bridge.ts +450 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +34 -3
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +437 -0
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +6 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +41 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +88 -27
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +45 -28
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +186 -28
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +7 -5
- package/src/workflows/scripted-workflow.ts +424 -125
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1565
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
6
|
+
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
7
|
+
import { TEMP_ROOT_DIR, type OrcaProgressTabsConfig } from "../../shared/types.ts";
|
|
8
|
+
import { extractTextFromContent, extractToolArgsPreview, getAgentDir } from "../../shared/utils.ts";
|
|
9
|
+
|
|
10
|
+
const ORCA_CREATE_TIMEOUT_MS = 20_000;
|
|
11
|
+
const ORCA_KILL_GRACE_MS = 2_000;
|
|
12
|
+
const CLEANUP_DELAY_MS = 5 * 60_000;
|
|
13
|
+
const STALE_PROGRESS_MAX_AGE_MS = 24 * 60 * 60_000;
|
|
14
|
+
const VIEWER_POLL_MS = 150;
|
|
15
|
+
const MAX_MIRROR_BYTES = 1024 * 1024;
|
|
16
|
+
const MIRROR_FOOTER_RESERVE_BYTES = 16 * 1024;
|
|
17
|
+
const COUNTER_LOCK_STALE_MS = 30_000;
|
|
18
|
+
const COUNTER_LOCK_RETRIES = 200;
|
|
19
|
+
const COUNTER_LOCK_RETRY_MS = 10;
|
|
20
|
+
const ORCA_CREATE_WAIT_TIMEOUT_MS = ORCA_CREATE_TIMEOUT_MS + ORCA_KILL_GRACE_MS + 3_000;
|
|
21
|
+
|
|
22
|
+
const ORCA_CREATE_WATCHDOG_SCRIPT = [
|
|
23
|
+
"const {spawn}=require('node:child_process');",
|
|
24
|
+
"const fs=require('node:fs');",
|
|
25
|
+
"const timeout=Number(process.argv[1]),grace=Number(process.argv[2]),waitTimeout=Number(process.argv[3]);",
|
|
26
|
+
"const previous=process.argv[4],done=process.argv[5],command=process.argv[6],args=process.argv.slice(7);",
|
|
27
|
+
"function mark(){try{fs.writeFileSync(done.replace(/\\.pending$/,'.ready'),'')}catch{}}",
|
|
28
|
+
"function exists(file){try{return fs.existsSync(file)}catch{return false}}",
|
|
29
|
+
"function keepQueued(){try{const now=new Date();fs.utimesSync(done,now,now)}catch{}}",
|
|
30
|
+
"function predecessorReady(){if(previous==='-')return true;if(exists(previous.replace(/\\.pending$/,'.ready')))return true;try{return Date.now()-fs.statSync(previous).mtimeMs>=waitTimeout}catch{return true}}",
|
|
31
|
+
"function start(){",
|
|
32
|
+
" try{",
|
|
33
|
+
" const child=spawn(command,args,{stdio:'ignore',windowsHide:true});",
|
|
34
|
+
" let hardKill;",
|
|
35
|
+
" const timer=setTimeout(()=>{child.kill('SIGTERM');hardKill=setTimeout(()=>child.kill('SIGKILL'),grace)},timeout);",
|
|
36
|
+
" const clear=()=>{clearTimeout(timer);if(hardKill)clearTimeout(hardKill);mark()};",
|
|
37
|
+
" child.once('error',()=>{clear();process.exitCode=1});",
|
|
38
|
+
" child.once('close',code=>{clear();process.exitCode=code===0?0:1});",
|
|
39
|
+
" }catch{mark();process.exitCode=1}",
|
|
40
|
+
"}",
|
|
41
|
+
"if(predecessorReady())start();",
|
|
42
|
+
"else{(function waitPrev(){keepQueued();if(predecessorReady())return start();setTimeout(waitPrev,20)})();}",
|
|
43
|
+
].join("");
|
|
44
|
+
|
|
45
|
+
const ORCA_CLEANUP_WATCHDOG_SCRIPT = [
|
|
46
|
+
"const fs=require('node:fs');",
|
|
47
|
+
"const deadline=Date.now()+Number(process.argv[1]),files=process.argv.slice(2);",
|
|
48
|
+
"function check(){if(!files.some(file=>fs.existsSync(file)))process.exit(0);if(Date.now()<deadline)return;for(const file of files){try{fs.rmSync(file,{force:true})}catch{}}process.exit(0)}",
|
|
49
|
+
"setInterval(check,1000);check();",
|
|
50
|
+
].join("");
|
|
51
|
+
|
|
52
|
+
export interface OrcaProgressTab {
|
|
53
|
+
append(text: string): void;
|
|
54
|
+
event(event: { type?: string; message?: Message; toolName?: string; args?: unknown }): void;
|
|
55
|
+
finish(status: "completed" | "failed" | "stopped", sessionFile?: string): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function executableFile(candidate: string): boolean {
|
|
59
|
+
try {
|
|
60
|
+
const stats = fs.statSync(candidate);
|
|
61
|
+
if (!stats.isFile()) return false;
|
|
62
|
+
if (process.platform === "win32") return true;
|
|
63
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function executableNames(command: string, env: NodeJS.ProcessEnv): string[] {
|
|
71
|
+
if (process.platform !== "win32") return [command];
|
|
72
|
+
const extensions = (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean);
|
|
73
|
+
return [command, ...extensions.map((extension) => `${command}${extension.toLowerCase()}`), ...extensions.map((extension) => `${command}${extension.toUpperCase()}`)];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveOrcaCommand(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
77
|
+
const override = env.PI_SUBAGENT_ORCA_BINARY?.trim();
|
|
78
|
+
if (override) return executableFile(override) ? override : undefined;
|
|
79
|
+
for (const directory of (env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
|
|
80
|
+
for (const name of executableNames("orca", env)) {
|
|
81
|
+
const candidate = path.join(directory, name);
|
|
82
|
+
if (executableFile(candidate)) return candidate;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shellQuote(value: string): string {
|
|
89
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const VIEWER_SCRIPT = [
|
|
93
|
+
"const fs=require('fs'),{StringDecoder}=require('string_decoder');",
|
|
94
|
+
"const log=process.argv[1],done=process.argv[2],decoder=new StringDecoder('utf8');",
|
|
95
|
+
"let offset=0,finishing=false,state='text';",
|
|
96
|
+
"function sanitize(input){let output='';for(const char of input){const code=char.charCodeAt(0);",
|
|
97
|
+
"if(state==='text'){if(code===27){state='escape';continue}if(code===155){state='csi';continue}if(code===157){state='osc';continue}if(code===144||code===152||code===158||code===159){state='string';continue}if(code===10||(code>=32&&code!==127&&(code<128||code>159)))output+=char;continue}",
|
|
98
|
+
"if(state==='escape'){if(char==='[')state='csi';else if(char===']')state='osc';else if(char==='P'||char==='X'||char==='^'||char==='_')state='string';else if(code<32||code>47)state='text';continue}",
|
|
99
|
+
"if(state==='csi'){if(code>=64&&code<=126)state='text';continue}",
|
|
100
|
+
"if(state==='osc'){if(code===7)state='text';else if(code===27)state='osc-escape';continue}",
|
|
101
|
+
"if(state==='osc-escape'){state=char==='\\\\'?'text':code===27?'osc-escape':'osc';continue}",
|
|
102
|
+
"if(state==='string'){if(code===27)state='string-escape';continue}",
|
|
103
|
+
"if(state==='string-escape')state=char==='\\\\'?'text':code===27?'string-escape':'string';",
|
|
104
|
+
"}return output}",
|
|
105
|
+
"function write(buffer){const output=sanitize(decoder.write(buffer));if(output)process.stdout.write(output)}",
|
|
106
|
+
"function pump(){",
|
|
107
|
+
" try{const size=fs.statSync(log).size;if(size<offset)offset=0;if(size>offset){const fd=fs.openSync(log,'r');const b=Buffer.alloc(size-offset);fs.readSync(fd,b,0,b.length,offset);fs.closeSync(fd);offset=size;write(b);}}catch{}",
|
|
108
|
+
" if(!finishing&&fs.existsSync(done)){finishing=true;pump();setTimeout(()=>{const output=sanitize(decoder.end());if(output)process.stdout.write(output);try{fs.unlinkSync(done)}catch{}try{fs.unlinkSync(log)}catch{}process.exit(0)},250);}",
|
|
109
|
+
"}",
|
|
110
|
+
`const timer=setInterval(pump,${VIEWER_POLL_MS});`,
|
|
111
|
+
"pump();",
|
|
112
|
+
"process.on('exit',()=>clearInterval(timer));",
|
|
113
|
+
].join("");
|
|
114
|
+
|
|
115
|
+
function viewerCommand(nodeExecutable: string, logPath: string, donePath: string): string {
|
|
116
|
+
const invocation = [nodeExecutable, "-e", VIEWER_SCRIPT, logPath, donePath].map(shellQuote).join(" ");
|
|
117
|
+
// Do not replace or exit Orca's interactive shell. The short-lived viewer may
|
|
118
|
+
// finish and delete its mirror files, while the completed tab remains open at
|
|
119
|
+
// the shell prompt until the user closes it.
|
|
120
|
+
return invocation;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function progressRoot(): string {
|
|
124
|
+
return path.join(TEMP_ROOT_DIR, "orca-progress");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function pruneStaleProgressFiles(root: string, now = Date.now()): void {
|
|
128
|
+
try {
|
|
129
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
130
|
+
if (!entry.isFile() || (!entry.name.endsWith(".log") && !entry.name.endsWith(".done") && !entry.name.endsWith(".ready") && !entry.name.endsWith(".pending"))) continue;
|
|
131
|
+
const file = path.join(root, entry.name);
|
|
132
|
+
try {
|
|
133
|
+
if (now - fs.statSync(file).mtimeMs > STALE_PROGRESS_MAX_AGE_MS) fs.rmSync(file, { force: true });
|
|
134
|
+
} catch { /* best-effort stale cleanup */ }
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// The observer remains optional when temp cleanup is unavailable.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function safeSegment(value: unknown, fallback = "subagent"): string {
|
|
142
|
+
if (typeof value !== "string") return fallback;
|
|
143
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || fallback;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sleepSync(ms: number): void {
|
|
147
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
148
|
+
Atomics.wait(signal, 0, 0, ms);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveSequenceScope(cwd: string): string {
|
|
152
|
+
let current = path.resolve(cwd);
|
|
153
|
+
try { current = fs.realpathSync(current); } catch { /* use the lexical cwd */ }
|
|
154
|
+
while (true) {
|
|
155
|
+
try {
|
|
156
|
+
if (fs.existsSync(path.join(current, ".git"))) return current;
|
|
157
|
+
} catch { /* keep walking */ }
|
|
158
|
+
const parent = path.dirname(current);
|
|
159
|
+
if (parent === current) return path.resolve(cwd);
|
|
160
|
+
current = parent;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sequenceKey(cwd: string): string {
|
|
165
|
+
return createHash("sha256").update(resolveSequenceScope(cwd)).digest("hex").slice(0, 20);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function withSequenceLock<T>(root: string, key: string, fn: () => T): T | undefined {
|
|
169
|
+
const lockPath = path.join(root, `counter-${key}.lock`);
|
|
170
|
+
let locked = false;
|
|
171
|
+
for (let attempt = 0; attempt < COUNTER_LOCK_RETRIES; attempt++) {
|
|
172
|
+
try {
|
|
173
|
+
fs.mkdirSync(lockPath, { mode: 0o700 });
|
|
174
|
+
locked = true;
|
|
175
|
+
break;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") break;
|
|
178
|
+
try {
|
|
179
|
+
if (Date.now() - fs.statSync(lockPath).mtimeMs > COUNTER_LOCK_STALE_MS) {
|
|
180
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
} catch { /* another process released the lock */ }
|
|
184
|
+
sleepSync(COUNTER_LOCK_RETRY_MS);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!locked) return undefined;
|
|
188
|
+
try {
|
|
189
|
+
return fn();
|
|
190
|
+
} finally {
|
|
191
|
+
try { fs.rmSync(lockPath, { recursive: true, force: true }); } catch { /* stale lock cleanup handles crashes */ }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function predecessorMarker(pathValue: string | undefined): string | undefined {
|
|
196
|
+
if (!pathValue) return undefined;
|
|
197
|
+
const ready = pathValue.endsWith(".pending") ? pathValue.replace(/\.pending$/, ".ready") : pathValue;
|
|
198
|
+
const pending = ready.replace(/\.ready$/, ".pending");
|
|
199
|
+
if (fs.existsSync(ready) || fs.existsSync(pending)) return pending;
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function reserveTabSequence(root: string, cwd: string): { sequence: number; previousCreateDone?: string; createDone: string } | undefined {
|
|
204
|
+
const key = sequenceKey(cwd);
|
|
205
|
+
const counterPath = path.join(root, `counter-${key}`);
|
|
206
|
+
const createDone = path.join(root, `create-${key}-${randomUUID()}.pending`);
|
|
207
|
+
return withSequenceLock(root, key, () => {
|
|
208
|
+
let current = 0;
|
|
209
|
+
let previousCreateDone: string | undefined;
|
|
210
|
+
try {
|
|
211
|
+
const raw = fs.readFileSync(counterPath, "utf-8");
|
|
212
|
+
const [countLine, previousLine] = raw.split("\n");
|
|
213
|
+
const parsed = Number.parseInt(countLine ?? "", 10);
|
|
214
|
+
if (Number.isSafeInteger(parsed) && parsed >= 0) current = parsed;
|
|
215
|
+
previousCreateDone = predecessorMarker(previousLine?.trim());
|
|
216
|
+
} catch { /* first tab for this worktree */ }
|
|
217
|
+
const next = current + 1;
|
|
218
|
+
try {
|
|
219
|
+
fs.writeFileSync(createDone, "", { encoding: "utf-8", mode: 0o600 });
|
|
220
|
+
fs.writeFileSync(counterPath, `${next}\n${createDone}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
221
|
+
return { sequence: next, previousCreateDone, createDone };
|
|
222
|
+
} catch {
|
|
223
|
+
try { fs.rmSync(createDone, { force: true }); } catch { /* best effort */ }
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function resolvePiSessionId(sessionFile: string | undefined): string | undefined {
|
|
230
|
+
if (!sessionFile) return undefined;
|
|
231
|
+
try {
|
|
232
|
+
const fd = fs.openSync(sessionFile, "r");
|
|
233
|
+
try {
|
|
234
|
+
const buffer = Buffer.alloc(16 * 1024);
|
|
235
|
+
const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
236
|
+
const line = buffer.toString("utf-8", 0, bytes).split("\n", 1)[0];
|
|
237
|
+
if (!line) return undefined;
|
|
238
|
+
const header = JSON.parse(line) as { type?: unknown; id?: unknown };
|
|
239
|
+
if (header.type === "session" && typeof header.id === "string" && /^[A-Za-z0-9-]{8,}$/.test(header.id)) return header.id;
|
|
240
|
+
return undefined;
|
|
241
|
+
} finally {
|
|
242
|
+
fs.closeSync(fd);
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function scheduleCleanup(nodeExecutable: string, paths: string[]): void {
|
|
250
|
+
try {
|
|
251
|
+
const watchdog = spawn(nodeExecutable, ["-e", ORCA_CLEANUP_WATCHDOG_SCRIPT, String(CLEANUP_DELAY_MS), ...paths], {
|
|
252
|
+
detached: true,
|
|
253
|
+
stdio: "ignore",
|
|
254
|
+
windowsHide: true,
|
|
255
|
+
});
|
|
256
|
+
watchdog.once("error", () => {});
|
|
257
|
+
watchdog.unref();
|
|
258
|
+
} catch {
|
|
259
|
+
// Cleanup remains best effort for this optional observer.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function loadOrcaProgressTabsConfig(): OrcaProgressTabsConfig | undefined {
|
|
264
|
+
try {
|
|
265
|
+
const configPath = path.join(getAgentDir(), "extensions", "subagent", "config.json");
|
|
266
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
|
|
267
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
268
|
+
const value = (parsed as Record<string, unknown>).orcaProgressTabs;
|
|
269
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
270
|
+
const config = value as Record<string, unknown>;
|
|
271
|
+
if (Object.keys(config).some((key) => key !== "enabled") || typeof config.enabled !== "boolean") return undefined;
|
|
272
|
+
return { enabled: config.enabled };
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function createOrcaProgressTab(input: {
|
|
279
|
+
cwd: string;
|
|
280
|
+
runId: string;
|
|
281
|
+
agent: string;
|
|
282
|
+
index: number;
|
|
283
|
+
config?: OrcaProgressTabsConfig;
|
|
284
|
+
env?: NodeJS.ProcessEnv;
|
|
285
|
+
command?: string;
|
|
286
|
+
}): OrcaProgressTab | undefined {
|
|
287
|
+
const config = input.config ?? loadOrcaProgressTabsConfig();
|
|
288
|
+
if (config?.enabled !== true || process.platform === "win32") return undefined;
|
|
289
|
+
const command = input.command ?? resolveOrcaCommand(input.env);
|
|
290
|
+
if (!command) return undefined;
|
|
291
|
+
const nodeExecutable = resolveNodeExecutable();
|
|
292
|
+
|
|
293
|
+
const root = progressRoot();
|
|
294
|
+
try {
|
|
295
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
296
|
+
} catch {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
pruneStaleProgressFiles(root);
|
|
300
|
+
const runId = safeSegment(input.runId, "run");
|
|
301
|
+
const agent = safeSegment(input.agent, "subagent");
|
|
302
|
+
const index = typeof input.index === "number" && Number.isInteger(input.index) && input.index >= 0 ? input.index : 0;
|
|
303
|
+
const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
|
|
304
|
+
const reservation = reserveTabSequence(root, cwd);
|
|
305
|
+
if (reservation === undefined) return undefined;
|
|
306
|
+
const tabSequence = reservation.sequence;
|
|
307
|
+
const stem = `${runId}-${index}-${randomUUID()}`;
|
|
308
|
+
const logPath = path.join(root, `${stem}.log`);
|
|
309
|
+
const donePath = path.join(root, `${stem}.done`);
|
|
310
|
+
const title = `subagent · ${agent} · ${tabSequence}`;
|
|
311
|
+
const markCreateReady = () => {
|
|
312
|
+
try { fs.writeFileSync(reservation.createDone.replace(/\.pending$/, ".ready"), ""); } catch { /* unblock later tabs */ }
|
|
313
|
+
};
|
|
314
|
+
try {
|
|
315
|
+
fs.writeFileSync(logPath, `pi-subagents / ${agent}\nrun ${runId} · child ${index + 1}\n${"─".repeat(48)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
316
|
+
fs.rmSync(donePath, { force: true });
|
|
317
|
+
} catch {
|
|
318
|
+
markCreateReady();
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let available = true;
|
|
323
|
+
let truncated = false;
|
|
324
|
+
let scheduledBytes = fs.statSync(logPath).size;
|
|
325
|
+
const progressByteLimit = MAX_MIRROR_BYTES - MIRROR_FOOTER_RESERVE_BYTES;
|
|
326
|
+
const logStream = fs.createWriteStream(logPath, { flags: "a", mode: 0o600 });
|
|
327
|
+
const failObserver = () => {
|
|
328
|
+
if (!available) return;
|
|
329
|
+
available = false;
|
|
330
|
+
logStream.destroy();
|
|
331
|
+
try { fs.rmSync(logPath, { force: true }); } catch { /* best effort */ }
|
|
332
|
+
try { fs.rmSync(donePath, { force: true }); } catch { /* best effort */ }
|
|
333
|
+
};
|
|
334
|
+
logStream.once("error", failObserver);
|
|
335
|
+
const writeProgress = (text: string) => {
|
|
336
|
+
if (!available || !text || truncated) return;
|
|
337
|
+
const bytes = Buffer.byteLength(text);
|
|
338
|
+
if (scheduledBytes + bytes > progressByteLimit) {
|
|
339
|
+
truncated = true;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
scheduledBytes += bytes;
|
|
343
|
+
try {
|
|
344
|
+
logStream.write(text);
|
|
345
|
+
} catch {
|
|
346
|
+
failObserver();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
let createSettled = false;
|
|
350
|
+
let cleanupPaths: string[] | undefined;
|
|
351
|
+
const scheduleDeferredCleanup = () => {
|
|
352
|
+
if (!createSettled || cleanupPaths === undefined) return;
|
|
353
|
+
scheduleCleanup(nodeExecutable, cleanupPaths);
|
|
354
|
+
cleanupPaths = undefined;
|
|
355
|
+
};
|
|
356
|
+
try {
|
|
357
|
+
const watchdog = spawn(nodeExecutable, [
|
|
358
|
+
"-e", ORCA_CREATE_WATCHDOG_SCRIPT,
|
|
359
|
+
String(ORCA_CREATE_TIMEOUT_MS),
|
|
360
|
+
String(ORCA_KILL_GRACE_MS),
|
|
361
|
+
String(ORCA_CREATE_WAIT_TIMEOUT_MS),
|
|
362
|
+
reservation.previousCreateDone ?? "-",
|
|
363
|
+
reservation.createDone,
|
|
364
|
+
command,
|
|
365
|
+
"terminal", "create",
|
|
366
|
+
"--worktree", `path:${path.resolve(cwd)}`,
|
|
367
|
+
"--title", title,
|
|
368
|
+
"--command", viewerCommand(nodeExecutable, logPath, donePath),
|
|
369
|
+
"--json",
|
|
370
|
+
], {
|
|
371
|
+
cwd,
|
|
372
|
+
detached: true,
|
|
373
|
+
stdio: "ignore",
|
|
374
|
+
windowsHide: true,
|
|
375
|
+
env: input.env ?? process.env,
|
|
376
|
+
});
|
|
377
|
+
watchdog.once("close", (code) => {
|
|
378
|
+
createSettled = true;
|
|
379
|
+
if (code !== 0) failObserver();
|
|
380
|
+
scheduleDeferredCleanup();
|
|
381
|
+
});
|
|
382
|
+
watchdog.once("error", () => {
|
|
383
|
+
markCreateReady();
|
|
384
|
+
createSettled = true;
|
|
385
|
+
failObserver();
|
|
386
|
+
scheduleDeferredCleanup();
|
|
387
|
+
});
|
|
388
|
+
watchdog.unref();
|
|
389
|
+
} catch {
|
|
390
|
+
markCreateReady();
|
|
391
|
+
failObserver();
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let finished = false;
|
|
396
|
+
return {
|
|
397
|
+
append(text) {
|
|
398
|
+
if (finished) return;
|
|
399
|
+
writeProgress(text);
|
|
400
|
+
},
|
|
401
|
+
event(event) {
|
|
402
|
+
if (!available || finished) return;
|
|
403
|
+
if (event.type === "tool_execution_start" && event.toolName) {
|
|
404
|
+
const args = event.args && typeof event.args === "object" && !Array.isArray(event.args)
|
|
405
|
+
? extractToolArgsPreview(event.args as Record<string, unknown>)
|
|
406
|
+
: "";
|
|
407
|
+
writeProgress(`\n› ${event.toolName}${args ? `: ${args}` : ""}\n`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
|
|
411
|
+
const text = extractTextFromContent(event.message.content);
|
|
412
|
+
if (text.trim()) writeProgress(`${text}${text.endsWith("\n") ? "" : "\n"}`);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
finish(status, sessionFile) {
|
|
416
|
+
if (!available || finished) return;
|
|
417
|
+
finished = true;
|
|
418
|
+
const sessionId = status === "completed" ? resolvePiSessionId(sessionFile) : undefined;
|
|
419
|
+
let verifiedSessionFile: string | undefined;
|
|
420
|
+
if (sessionId && sessionFile) {
|
|
421
|
+
try { verifiedSessionFile = fs.realpathSync(sessionFile); } catch { /* the session is no longer available */ }
|
|
422
|
+
}
|
|
423
|
+
const terminalMessage = verifiedSessionFile
|
|
424
|
+
? `completed. To remove the Pi session of this subagent, run rm -- ${shellQuote(verifiedSessionFile)}`
|
|
425
|
+
: status;
|
|
426
|
+
const truncation = truncated ? `\n[progress mirror truncated at ${MAX_MIRROR_BYTES} bytes]\n` : "";
|
|
427
|
+
let footer = `${truncation}\n${"─".repeat(48)}\n${terminalMessage}\n`;
|
|
428
|
+
if (scheduledBytes + Buffer.byteLength(footer) > MAX_MIRROR_BYTES) footer = `\n${status}\n`;
|
|
429
|
+
logStream.end(footer, () => {
|
|
430
|
+
if (!available) return;
|
|
431
|
+
try { fs.writeFileSync(donePath, `${status}\n`, { encoding: "utf-8", mode: 0o600 }); } catch { /* best effort */ }
|
|
432
|
+
cleanupPaths = [logPath, donePath];
|
|
433
|
+
scheduleDeferredCleanup();
|
|
434
|
+
});
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
@@ -34,6 +34,48 @@ function readManifest(manifestPath: string): ParallelHandoffManifest | undefined
|
|
|
34
34
|
return parsed;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function resolveExistingPath(candidate: string): string {
|
|
38
|
+
try {
|
|
39
|
+
return fs.realpathSync(candidate);
|
|
40
|
+
} catch {
|
|
41
|
+
return path.resolve(candidate);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pathInside(root: string, candidate: string): boolean {
|
|
46
|
+
const relative = path.relative(root, candidate);
|
|
47
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveRetainedWorktreeCwd(manifestPath: string, runId: string, childIndex: number): string | undefined {
|
|
51
|
+
const manifest = readManifest(manifestPath);
|
|
52
|
+
if (!manifest) return undefined;
|
|
53
|
+
if (manifest.runId !== runId) throw new Error(`Managed worktree handoff belongs to run '${manifest.runId}', not '${runId}'.`);
|
|
54
|
+
const match = manifest.groups
|
|
55
|
+
.flatMap((group) => group.children.map((child) => ({ group, child })))
|
|
56
|
+
.find(({ child }) => child.index === childIndex);
|
|
57
|
+
if (!match) return undefined;
|
|
58
|
+
const cleanup = match.group.cleanup.tasks.find((task) => task.index === match.child.taskIndex);
|
|
59
|
+
if (!cleanup) throw new Error(`Async run '${runId}' child ${childIndex} has no managed worktree cleanup record.`);
|
|
60
|
+
const relativeCwd = path.relative(resolveExistingPath(match.group.repoRoot), resolveExistingPath(manifest.cwd));
|
|
61
|
+
if (path.isAbsolute(relativeCwd) || relativeCwd === ".." || relativeCwd.startsWith(`..${path.sep}`)) throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
|
|
62
|
+
const requiredCwd = path.join(cleanup.path, relativeCwd);
|
|
63
|
+
if (cleanup.worktreeRemoved || cleanup.branchRemoved) throw new Error(`Async run '${runId}' required managed worktree was removed: ${requiredCwd}`);
|
|
64
|
+
let worktreeRoot = "";
|
|
65
|
+
let resolvedRequiredCwd = "";
|
|
66
|
+
try {
|
|
67
|
+
const rootStat = fs.lstatSync(cleanup.path);
|
|
68
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("worktree root is not a directory");
|
|
69
|
+
worktreeRoot = fs.realpathSync(cleanup.path);
|
|
70
|
+
if (!fs.statSync(requiredCwd).isDirectory()) throw new Error("path is not a directory");
|
|
71
|
+
resolvedRequiredCwd = fs.realpathSync(requiredCwd);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`Async run '${runId}' required managed worktree cwd is missing: ${requiredCwd}`, { cause: error instanceof Error ? error : undefined });
|
|
74
|
+
}
|
|
75
|
+
if (!pathInside(worktreeRoot, resolvedRequiredCwd)) throw new Error(`Async run '${runId}' has an invalid managed worktree cwd.`);
|
|
76
|
+
return requiredCwd;
|
|
77
|
+
}
|
|
78
|
+
|
|
37
79
|
function referenceFor(manifestPath: string, manifest: ParallelHandoffManifest): ParallelHandoffReference {
|
|
38
80
|
const children = manifest.groups.flatMap((group) => group.children);
|
|
39
81
|
return {
|
|
@@ -74,7 +116,7 @@ function missingDiff(input: { manifestPath: string; stepIndex: number; taskIndex
|
|
|
74
116
|
export function writeParallelHandoffGroup(input: {
|
|
75
117
|
manifestPath: string;
|
|
76
118
|
runId: string;
|
|
77
|
-
mode: "parallel" | "chain";
|
|
119
|
+
mode: "single" | "parallel" | "chain";
|
|
78
120
|
source: "foreground" | "async";
|
|
79
121
|
cwd: string;
|
|
80
122
|
stepIndex: number;
|
|
@@ -162,7 +204,7 @@ export function parallelHandoffPath(baseDir: string, runId?: string): string {
|
|
|
162
204
|
export function writePendingParallelHandoff(input: {
|
|
163
205
|
manifestPath: string;
|
|
164
206
|
runId: string;
|
|
165
|
-
mode: "parallel" | "chain";
|
|
207
|
+
mode: "single" | "parallel" | "chain";
|
|
166
208
|
source: "foreground" | "async";
|
|
167
209
|
cwd: string;
|
|
168
210
|
stepIndex: number;
|
|
@@ -173,11 +215,11 @@ export function writePendingParallelHandoff(input: {
|
|
|
173
215
|
}
|
|
174
216
|
|
|
175
217
|
export function formatParallelHandoffReference(reference: ParallelHandoffReference): string {
|
|
176
|
-
return `
|
|
218
|
+
return `Worktree handoff: ${reference.path} (${reference.childCount} children, ${reference.changedPatches} changed patches, cleanup ${reference.cleanupState})`;
|
|
177
219
|
}
|
|
178
220
|
|
|
179
221
|
export function formatParallelHandoffError(error: unknown): string {
|
|
180
|
-
return `
|
|
222
|
+
return `Worktree handoff unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
181
223
|
}
|
|
182
224
|
|
|
183
225
|
export function discardPreservedWorktrees(
|
|
@@ -41,6 +41,8 @@ export interface RunnerSubagentStep {
|
|
|
41
41
|
sessionFile?: string;
|
|
42
42
|
maxSubagentDepth?: number;
|
|
43
43
|
timeoutMs?: number;
|
|
44
|
+
/** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
|
|
45
|
+
toolTimeoutMs?: number;
|
|
44
46
|
waitToolEnabled?: boolean;
|
|
45
47
|
structuredOutput?: {
|
|
46
48
|
schema: import("../../shared/types.ts").JsonSchemaObject;
|
|
@@ -63,13 +65,8 @@ export interface RunnerSubagentStep {
|
|
|
63
65
|
capabilityAudit?: import("./capability-ceiling.ts").SubagentCapabilityAudit;
|
|
64
66
|
/** Private stable logical-child path for inherited run fan-out accounting. */
|
|
65
67
|
runFanoutPath?: string;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
export interface RunnerCheckpointStep {
|
|
69
|
-
checkpoint: string;
|
|
70
|
-
message?: string;
|
|
71
|
-
phase?: string;
|
|
72
|
-
label?: string;
|
|
68
|
+
/** Run this single child in one managed worktree. */
|
|
69
|
+
worktree?: boolean;
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
export interface ParallelStepGroup {
|
|
@@ -98,11 +95,7 @@ export interface DynamicRunnerGroup {
|
|
|
98
95
|
capabilityAudit?: import("./capability-ceiling.ts").SubagentCapabilityAudit;
|
|
99
96
|
}
|
|
100
97
|
|
|
101
|
-
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup
|
|
102
|
-
|
|
103
|
-
export function isCheckpointRunnerStep(step: RunnerStep): step is RunnerCheckpointStep {
|
|
104
|
-
return "checkpoint" in step;
|
|
105
|
-
}
|
|
98
|
+
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
|
|
106
99
|
|
|
107
100
|
export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
|
|
108
101
|
return "parallel" in step && Array.isArray(step.parallel);
|
|
@@ -115,9 +108,7 @@ export function isDynamicRunnerGroup(step: RunnerStep): step is DynamicRunnerGro
|
|
|
115
108
|
export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
|
|
116
109
|
const flat: RunnerSubagentStep[] = [];
|
|
117
110
|
for (const step of steps) {
|
|
118
|
-
if (
|
|
119
|
-
continue;
|
|
120
|
-
} else if (isParallelGroup(step)) {
|
|
111
|
+
if (isParallelGroup(step)) {
|
|
121
112
|
for (const task of step.parallel) flat.push(task);
|
|
122
113
|
} else if (isDynamicRunnerGroup(step)) {
|
|
123
114
|
continue;
|
|
@@ -14,6 +14,10 @@ const MAX_PREVIEW_BYTES = 2048;
|
|
|
14
14
|
const SECRET_KEY = /(?:authorization|cookie|credential|password|secret|token|api[-_]?key)/i;
|
|
15
15
|
const SECRET_VALUE = /\b(?:Bearer\s+\S+|(?:sk|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{8,})\b/gi;
|
|
16
16
|
|
|
17
|
+
export function redactSecretValues(value: string): string {
|
|
18
|
+
return value.replace(SECRET_VALUE, "[redacted]");
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
export function validatePermissionRules(value: unknown, label: string): PermissionRules | undefined {
|
|
18
22
|
if (value === undefined) return undefined;
|
|
19
23
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object mapping tool names to allow, ask, or deny.`);
|
|
@@ -66,7 +70,7 @@ function redact(value: unknown, key = "", depth = 0): unknown {
|
|
|
66
70
|
if (Array.isArray(value)) return value.slice(0, 10).map((item) => redact(item, "", depth + 1));
|
|
67
71
|
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value as Record<string, unknown>).slice(0, 20).map(([entryKey, entryValue]) => [entryKey, redact(entryValue, entryKey, depth + 1)]));
|
|
68
72
|
if (typeof value === "string") {
|
|
69
|
-
const redacted = value
|
|
73
|
+
const redacted = redactSecretValues(value);
|
|
70
74
|
return redacted.length > 500 ? `${redacted.slice(0, 500)}…` : redacted;
|
|
71
75
|
}
|
|
72
76
|
return value;
|
|
@@ -433,13 +433,20 @@ export function resolvePiLaunchToolPlan(
|
|
|
433
433
|
...internalTools,
|
|
434
434
|
]),
|
|
435
435
|
];
|
|
436
|
+
// Supervisor-coordination names stay in the --tools allowlist but are never
|
|
437
|
+
// strict requirements: children register contact_supervisor at runtime through
|
|
438
|
+
// the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
|
|
439
|
+
// appended intercom alongside contact_supervisor, so that exact pairing is
|
|
440
|
+
// legacy plumbing, not a user demand for an external intercom provider;
|
|
441
|
+
// a lone intercom entry stays strictly required (#1207).
|
|
442
|
+
const legacySupervisorPairing = declaredBuiltinTools.includes("contact_supervisor");
|
|
436
443
|
const requiredChildTools = explicitToolAllowlist
|
|
437
444
|
? [
|
|
438
445
|
...new Set([
|
|
439
446
|
...(input.tools !== undefined ? declaredBuiltinTools : []),
|
|
440
447
|
...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
|
|
441
448
|
...internalTools,
|
|
442
|
-
]),
|
|
449
|
+
].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
|
|
443
450
|
]
|
|
444
451
|
: [];
|
|
445
452
|
const permSystemExt = capabilityCeiling?.denyExtensions
|