pi-daddy 0.48.1 → 0.48.3
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 +14 -0
- package/README.md +25 -6
- package/dist/executors/captured-execution.d.ts.map +1 -1
- package/dist/executors/captured-execution.js +12 -1
- package/dist/executors/captured-execution.js.map +1 -1
- package/dist/executors/herdr-agent-display.d.ts +10 -0
- package/dist/executors/herdr-agent-display.d.ts.map +1 -0
- package/dist/executors/herdr-agent-display.js +68 -0
- package/dist/executors/herdr-agent-display.js.map +1 -0
- package/dist/executors/herdr-launcher.d.ts.map +1 -1
- package/dist/executors/herdr-launcher.js +30 -4
- package/dist/executors/herdr-launcher.js.map +1 -1
- package/dist/executors/herdr-owned.d.ts +2 -0
- package/dist/executors/herdr-owned.d.ts.map +1 -1
- package/dist/executors/herdr-owned.js +33 -3
- package/dist/executors/herdr-owned.js.map +1 -1
- package/dist/executors/owned-pi-ui.d.ts +3 -0
- package/dist/executors/owned-pi-ui.d.ts.map +1 -0
- package/dist/executors/owned-pi-ui.js +114 -0
- package/dist/executors/owned-pi-ui.js.map +1 -0
- package/dist/executors/owned-pi-view.d.ts +5 -0
- package/dist/executors/owned-pi-view.d.ts.map +1 -0
- package/dist/executors/owned-pi-view.js +60 -0
- package/dist/executors/owned-pi-view.js.map +1 -0
- package/dist/executors/owned-worker.d.ts +2 -0
- package/dist/executors/owned-worker.d.ts.map +1 -1
- package/dist/executors/owned-worker.js +27 -11
- package/dist/executors/owned-worker.js.map +1 -1
- package/dist/governance/approval-prompt.d.ts +1 -1
- package/extensions/approvals.ts +3 -2
- package/extensions/execute-child.ts +2 -0
- package/extensions/session.ts +3 -8
- package/package.json +1 -1
- package/src/executors/captured-execution.ts +13 -1
- package/src/executors/herdr-agent-display.ts +72 -0
- package/src/executors/herdr-launcher.ts +23 -3
- package/src/executors/herdr-owned.ts +36 -3
- package/src/executors/owned-pi-ui.ts +116 -0
- package/src/executors/owned-pi-view.ts +58 -0
- package/src/executors/owned-worker.ts +29 -11
- package/src/governance/approval-prompt.ts +1 -1
|
@@ -24,6 +24,8 @@ export interface HerdrOwnedOptions {
|
|
|
24
24
|
onDisplay?: (display: (text: string) => void) => void;
|
|
25
25
|
/** Tests may select a freshly built launcher. Production always uses the packaged build. */
|
|
26
26
|
launcherPath?: string;
|
|
27
|
+
/** Human-readable role; display only, never a dispatch selector. */
|
|
28
|
+
displayName?: string;
|
|
27
29
|
}
|
|
28
30
|
export async function runHerdrOwned(
|
|
29
31
|
request: OwnedChildRunRequest,
|
|
@@ -33,11 +35,16 @@ export async function runHerdrOwned(
|
|
|
33
35
|
const directory = await mkdtemp(join(tmpdir(), "pd-herdr-"));
|
|
34
36
|
const path = join(directory, "owner.sock"),
|
|
35
37
|
token = randomUUID();
|
|
38
|
+
const role = (options.displayName ?? "child").replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 48) || "child";
|
|
39
|
+
const display = { source: "pi-daddy:" + randomUUID(), role };
|
|
36
40
|
const launcher =
|
|
37
41
|
options.launcherPath ?? fileURLToPath(new URL("../../dist/executors/herdr-launcher.js", import.meta.url));
|
|
38
42
|
const helper = fileURLToPath(new URL("../../native/linux-x64/worker", import.meta.url));
|
|
39
43
|
let socket: Socket | undefined, send: ((value: unknown) => void) | undefined;
|
|
40
44
|
let identity: CapturedWorkerIdentity | undefined, outcome: OwnedChildRunResult | undefined, error: string | undefined;
|
|
45
|
+
let paneId: string | undefined,
|
|
46
|
+
settled = false,
|
|
47
|
+
displayReleased = false;
|
|
41
48
|
let tabId: string | undefined,
|
|
42
49
|
entered = false,
|
|
43
50
|
done = false,
|
|
@@ -86,7 +93,7 @@ export async function runHerdrOwned(
|
|
|
86
93
|
activityProbe: _probe,
|
|
87
94
|
...serializable
|
|
88
95
|
} = request;
|
|
89
|
-
send!({ type: "request", request: serializable });
|
|
96
|
+
send!({ type: "request", request: serializable, display });
|
|
90
97
|
return;
|
|
91
98
|
}
|
|
92
99
|
if (message?.type === "ownership") {
|
|
@@ -142,6 +149,7 @@ export async function runHerdrOwned(
|
|
|
142
149
|
}
|
|
143
150
|
if (message?.type === "result" && !outcome) {
|
|
144
151
|
outcome = message.result;
|
|
152
|
+
displayReleased = message.displayReleased === true;
|
|
145
153
|
return;
|
|
146
154
|
}
|
|
147
155
|
if (message?.type === "failure") {
|
|
@@ -190,7 +198,7 @@ export async function runHerdrOwned(
|
|
|
190
198
|
"--cwd",
|
|
191
199
|
request.cwd,
|
|
192
200
|
"--label",
|
|
193
|
-
"pi-daddy
|
|
201
|
+
"pi-daddy " + role,
|
|
194
202
|
"--no-focus",
|
|
195
203
|
...(options.workspace ? ["--workspace", options.workspace] : []),
|
|
196
204
|
]),
|
|
@@ -199,7 +207,8 @@ export async function runHerdrOwned(
|
|
|
199
207
|
const pane = created.result?.root_pane as { pane_id?: string; tab_id?: string } | undefined;
|
|
200
208
|
tabId = pane?.tab_id;
|
|
201
209
|
if (!pane?.pane_id || !tabId) throw Error("Herdr creation returned no exact pane/tab identity");
|
|
202
|
-
|
|
210
|
+
paneId = pane.pane_id;
|
|
211
|
+
options.onPane?.(paneId, tabId);
|
|
203
212
|
if (error || request.signal?.aborted) throw Error(error ?? "cancelled before Herdr command launch");
|
|
204
213
|
entered = true;
|
|
205
214
|
const launchReply = await exec([
|
|
@@ -218,6 +227,7 @@ export async function runHerdrOwned(
|
|
|
218
227
|
if (!outcome || !identity) throw Error(error ?? "Herdr launcher closed without a complete result/ownership");
|
|
219
228
|
const receipt = await readCapturedWorkerReceipt(identity);
|
|
220
229
|
if (!receipt) throw Error("Herdr worker supplied no independently verified cleanup receipt");
|
|
230
|
+
settled = true;
|
|
221
231
|
return { ...outcome, cleanup: { state: "settled", identity, receipt }, ...(error ? { spawnError: error } : {}) };
|
|
222
232
|
} catch (cause) {
|
|
223
233
|
fail(cause);
|
|
@@ -229,6 +239,7 @@ export async function runHerdrOwned(
|
|
|
229
239
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
230
240
|
receipt = await readCapturedWorkerReceipt(identity);
|
|
231
241
|
}
|
|
242
|
+
settled = Boolean(receipt);
|
|
232
243
|
return {
|
|
233
244
|
code: receipt?.workerCode ?? null,
|
|
234
245
|
text: "",
|
|
@@ -250,6 +261,28 @@ export async function runHerdrOwned(
|
|
|
250
261
|
request.signal?.removeEventListener("abort", abort);
|
|
251
262
|
socket?.destroy();
|
|
252
263
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
264
|
+
// Launcher SIGKILL can bypass its display release. Only the exact native receipt permits this fallback.
|
|
265
|
+
// Sequence ordering applies to an existing source; UI delivery remains best effort and never changes native results.
|
|
266
|
+
if (paneId && identity && !(settled && displayReleased)) {
|
|
267
|
+
try {
|
|
268
|
+
const reply = await exec([
|
|
269
|
+
"pane",
|
|
270
|
+
settled ? "release-agent" : "report-agent",
|
|
271
|
+
paneId,
|
|
272
|
+
"--source",
|
|
273
|
+
display.source,
|
|
274
|
+
"--agent",
|
|
275
|
+
"pi",
|
|
276
|
+
"--seq",
|
|
277
|
+
"3",
|
|
278
|
+
...(settled ? [] : ["--state", "unknown", "--message", "Native cleanup is unverified"]),
|
|
279
|
+
]);
|
|
280
|
+
if (reply.code !== 0 || (reply.stdout.trim() && parseReply(reply).error))
|
|
281
|
+
console.error("pi-daddy: Herdr agent sidebar cleanup report unavailable");
|
|
282
|
+
} catch {
|
|
283
|
+
console.error("pi-daddy: Herdr agent sidebar cleanup report unavailable");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
253
286
|
if (tabId && !options.keepPane) {
|
|
254
287
|
const closed = parseReply(await exec(["tab", "close", tabId]));
|
|
255
288
|
if (closed.error && !/tab .* not found|tab_not_found/.test(closed.error))
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Install LAST: observe final extension-transformed Pi events through the private inherited pipe. */
|
|
2
|
+
import { fstatSync } from "node:fs";
|
|
3
|
+
import { Socket } from "node:net";
|
|
4
|
+
import { getAgentDir, ProjectTrustStore, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ExtensionAPI, MessageUpdateEvent } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { OWNED_PI_VIEW_READY, failOwnedPiUi as fail } from "./owned-pi-view.ts";
|
|
8
|
+
|
|
9
|
+
const EVENT_FD = 6;
|
|
10
|
+
|
|
11
|
+
export default function ownedPiUi(pi: ExtensionAPI): void {
|
|
12
|
+
try {
|
|
13
|
+
const channel = fstatSync(EVENT_FD);
|
|
14
|
+
if (!channel.isFIFO() && !channel.isSocket()) throw Error("fd 6 is not an inherited event pipe");
|
|
15
|
+
const output = new Socket({ fd: EVENT_FD, readable: false, writable: true });
|
|
16
|
+
output.on("error", fail);
|
|
17
|
+
let pending = Promise.resolve();
|
|
18
|
+
const send = (event: unknown): Promise<void> => {
|
|
19
|
+
let record: string;
|
|
20
|
+
try {
|
|
21
|
+
record = JSON.stringify(event) + "\n";
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return fail(error);
|
|
24
|
+
}
|
|
25
|
+
pending = pending
|
|
26
|
+
.then(
|
|
27
|
+
() =>
|
|
28
|
+
new Promise<void>((resolve) => {
|
|
29
|
+
output.write(record, (error) => (error ? fail(error) : resolve()));
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
.catch(fail);
|
|
33
|
+
return pending;
|
|
34
|
+
};
|
|
35
|
+
// Keep the previous noninteractive trust policy; native UI must not introduce a blocking trust dialog.
|
|
36
|
+
pi.on("project_trust", (event) => {
|
|
37
|
+
try {
|
|
38
|
+
const agentDir = getAgentDir();
|
|
39
|
+
const remembered = new ProjectTrustStore(agentDir).get(event.cwd);
|
|
40
|
+
const trusted =
|
|
41
|
+
remembered ??
|
|
42
|
+
SettingsManager.create(event.cwd, agentDir, { projectTrusted: false }).getDefaultProjectTrust() === "always";
|
|
43
|
+
return { trusted: trusted ? "yes" : "no", remember: false };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return fail(error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
let guardedSession: { sessionManager: unknown; sessionId: unknown } | undefined;
|
|
49
|
+
pi.events.on(OWNED_PI_VIEW_READY, (ready) => {
|
|
50
|
+
if (ready && typeof ready === "object" && "sessionManager" in ready && "sessionId" in ready)
|
|
51
|
+
guardedSession = { sessionManager: ready.sessionManager, sessionId: ready.sessionId };
|
|
52
|
+
});
|
|
53
|
+
let started = false;
|
|
54
|
+
pi.on("session_start", (_event, ctx) => {
|
|
55
|
+
try {
|
|
56
|
+
if (started) throw Error("the owned session cannot be replaced");
|
|
57
|
+
if (
|
|
58
|
+
ctx.mode !== "tui" ||
|
|
59
|
+
guardedSession?.sessionManager !== ctx.sessionManager ||
|
|
60
|
+
guardedSession.sessionId !== ctx.sessionManager.getSessionId()
|
|
61
|
+
)
|
|
62
|
+
throw Error("the native Pi view guard is not ready for this session");
|
|
63
|
+
const header = ctx.sessionManager.getHeader();
|
|
64
|
+
if (
|
|
65
|
+
!header ||
|
|
66
|
+
header.type !== "session" ||
|
|
67
|
+
header.version !== 3 ||
|
|
68
|
+
header.id !== ctx.sessionManager.getSessionId()
|
|
69
|
+
)
|
|
70
|
+
throw Error("missing or mismatched actual Pi session header");
|
|
71
|
+
started = true;
|
|
72
|
+
return send(header);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return fail(error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const forward = (event: unknown): Promise<void> => {
|
|
78
|
+
if (!started) fail("Pi event arrived before the guarded session");
|
|
79
|
+
return send(event);
|
|
80
|
+
};
|
|
81
|
+
pi.on("agent_start", forward);
|
|
82
|
+
pi.on("agent_end", forward);
|
|
83
|
+
pi.on("turn_start", forward);
|
|
84
|
+
pi.on("turn_end", forward);
|
|
85
|
+
pi.on("message_start", forward);
|
|
86
|
+
pi.on("message_end", forward);
|
|
87
|
+
pi.on("tool_execution_start", forward);
|
|
88
|
+
pi.on("tool_execution_update", forward);
|
|
89
|
+
pi.on("tool_execution_end", forward);
|
|
90
|
+
pi.on("message_update", (event: MessageUpdateEvent) => {
|
|
91
|
+
// Match Pi's JSON delta projection without copying growing partial-message snapshots.
|
|
92
|
+
if (event.message.role !== "assistant") fail("message_update is not an assistant message");
|
|
93
|
+
const usage = event.message.usage,
|
|
94
|
+
update = event.assistantMessageEvent;
|
|
95
|
+
if (!("partial" in update)) return forward({ type: event.type, usage, assistantMessageEvent: update });
|
|
96
|
+
const { partial, ...delta } = update;
|
|
97
|
+
if (update.type === "toolcall_start") {
|
|
98
|
+
const tool = partial.content[update.contentIndex];
|
|
99
|
+
if (tool?.type !== "toolCall") fail("toolcall_start lacks its actual tool call");
|
|
100
|
+
return forward({
|
|
101
|
+
type: event.type,
|
|
102
|
+
usage,
|
|
103
|
+
assistantMessageEvent: { ...delta, id: tool.id, toolName: tool.name },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return forward({ type: event.type, usage, assistantMessageEvent: delta });
|
|
107
|
+
});
|
|
108
|
+
pi.on("agent_settled", async (event, ctx) => {
|
|
109
|
+
await forward(event);
|
|
110
|
+
// The pipe has flushed before orderly shutdown; native receipt/session checks remain authoritative.
|
|
111
|
+
ctx.shutdown();
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
fail(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Install FIRST: native Pi remains observable, but only its parent can control work. */
|
|
2
|
+
import { writeSync } from "node:fs";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export const OWNED_PI_VIEW_READY = "pi-daddy:owned-pi-view-ready";
|
|
6
|
+
export function failOwnedPiUi(error: unknown): never {
|
|
7
|
+
try {
|
|
8
|
+
writeSync(2, "pi-daddy: owned Pi UI refused: " + String(error) + "\n");
|
|
9
|
+
} finally {
|
|
10
|
+
process.exit(70);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function ownedPiView(pi: ExtensionAPI): void {
|
|
15
|
+
try {
|
|
16
|
+
if (typeof pi.registerMarkdownTransformer !== "function") throw Error("Pi lacks its public markdown renderer hook");
|
|
17
|
+
pi.registerMarkdownTransformer((markdown, context) =>
|
|
18
|
+
context.messageType === "assistant-thinking" ? "Thinking hidden" : markdown,
|
|
19
|
+
);
|
|
20
|
+
let guarded = false;
|
|
21
|
+
// No await here: the first session handler installs the guard before another handler can yield.
|
|
22
|
+
pi.on("session_start", (_event, ctx) => {
|
|
23
|
+
try {
|
|
24
|
+
if (guarded) throw Error("the owned session cannot be replaced");
|
|
25
|
+
if (
|
|
26
|
+
ctx.mode !== "tui" ||
|
|
27
|
+
typeof ctx.ui.onTerminalInput !== "function" ||
|
|
28
|
+
typeof ctx.ui.getToolsExpanded !== "function" ||
|
|
29
|
+
typeof ctx.ui.setToolsExpanded !== "function"
|
|
30
|
+
)
|
|
31
|
+
throw Error("Pi lacks the required native TUI controls");
|
|
32
|
+
const unsubscribe = ctx.ui.onTerminalInput((data) => {
|
|
33
|
+
// The renderer consumes this exact terminal response before dispatching any user action.
|
|
34
|
+
if (/^\x1b\[6;\d+;\d+t$/.test(data)) return;
|
|
35
|
+
try {
|
|
36
|
+
if (["\x0f", "\x1b[111;5u", "\x1b[111;5:1u", "\x1b[111;5:2u", "\x1b[27;5;111~"].includes(data))
|
|
37
|
+
ctx.ui.setToolsExpanded(!ctx.ui.getToolsExpanded());
|
|
38
|
+
} catch (error) {
|
|
39
|
+
failOwnedPiUi(error);
|
|
40
|
+
}
|
|
41
|
+
return { consume: true };
|
|
42
|
+
});
|
|
43
|
+
if (typeof unsubscribe !== "function") throw Error("Pi did not install the terminal input guard");
|
|
44
|
+
ctx.ui.setEditorText("");
|
|
45
|
+
ctx.ui.setStatus("pi-daddy-owned", "View only · parent controls the task · Ctrl+O expands tools");
|
|
46
|
+
guarded = true;
|
|
47
|
+
pi.events.emit(OWNED_PI_VIEW_READY, {
|
|
48
|
+
sessionManager: ctx.sessionManager,
|
|
49
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
failOwnedPiUi(error);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
failOwnedPiUi(error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -25,6 +25,8 @@ export interface OwnedChildRunRequest extends ChildRunRequest {
|
|
|
25
25
|
onOwnership?: (identity: CapturedWorkerIdentity) => Promise<void> | void;
|
|
26
26
|
/** JSON protocol consumers observe all bytes without capturing enormous replay streams as answer text. */
|
|
27
27
|
captureStdout?: boolean;
|
|
28
|
+
/** Actual pane terminal for Pi's native view. Protocol events arrive separately on fd 6. */
|
|
29
|
+
terminalUi?: boolean;
|
|
28
30
|
/** Bounded diagnostics may truncate without stopping independently captured primary output. */
|
|
29
31
|
stopOnOutputLimit?: boolean;
|
|
30
32
|
}
|
|
@@ -46,6 +48,8 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
46
48
|
if (request.signal?.aborted) return notStarted("cancelled before helper launch", true);
|
|
47
49
|
if (process.platform !== "linux" || process.arch !== "x64")
|
|
48
50
|
return notStarted("captured worker requires qualified Linux x64 helper");
|
|
51
|
+
if (request.terminalUi && (!process.stdin.isTTY || !process.stdout.isTTY || !process.stderr.isTTY))
|
|
52
|
+
return notStarted("native Pi view requires a real pane terminal on stdin, stdout and stderr");
|
|
49
53
|
let helper: string, root: string, hash: string;
|
|
50
54
|
const nonce = randomUUID();
|
|
51
55
|
let binary: import("node:fs/promises").FileHandle | undefined;
|
|
@@ -95,13 +99,26 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
95
99
|
{
|
|
96
100
|
cwd: root,
|
|
97
101
|
env: request.env,
|
|
98
|
-
stdio:
|
|
99
|
-
|
|
102
|
+
stdio: request.terminalUi
|
|
103
|
+
? ["inherit", "inherit", "inherit", "pipe", "pipe", binary!.fd, "pipe"]
|
|
104
|
+
: ["ignore", "pipe", "pipe", "pipe", "pipe", binary!.fd],
|
|
105
|
+
// The UI shares the pane's foreground process group for raw input and SIGWINCH.
|
|
106
|
+
// Cleanup still targets owned descendants by pidfd, never by process group.
|
|
107
|
+
detached: !request.terminalUi,
|
|
100
108
|
},
|
|
101
109
|
);
|
|
102
110
|
void binary!.close();
|
|
103
111
|
const control = child.stdio[3] as import("node:stream").Duplex;
|
|
104
112
|
const status = child.stdio[4] as import("node:stream").Readable;
|
|
113
|
+
const streams = {
|
|
114
|
+
stdout: request.terminalUi ? (child.stdio.at(6) as import("node:stream").Readable) : child.stdout,
|
|
115
|
+
stderr: child.stderr,
|
|
116
|
+
};
|
|
117
|
+
const closeObservations = () => {
|
|
118
|
+
streams.stdout?.destroy();
|
|
119
|
+
streams.stderr?.destroy();
|
|
120
|
+
status.destroy();
|
|
121
|
+
};
|
|
105
122
|
let identity: CapturedWorkerIdentity | undefined;
|
|
106
123
|
let text = "",
|
|
107
124
|
count = 0,
|
|
@@ -146,9 +163,7 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
146
163
|
stopWatchdog = setTimeout(() => {
|
|
147
164
|
failure ??= "worker helper failed to establish cleanup before its bound";
|
|
148
165
|
child.kill("SIGKILL");
|
|
149
|
-
|
|
150
|
-
child.stderr?.destroy();
|
|
151
|
-
status.destroy();
|
|
166
|
+
closeObservations();
|
|
152
167
|
}, cleanupCeiling + 1000);
|
|
153
168
|
};
|
|
154
169
|
function armIdle() {
|
|
@@ -160,10 +175,15 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
160
175
|
stop();
|
|
161
176
|
}, request.idleTimeoutMs);
|
|
162
177
|
}
|
|
163
|
-
|
|
164
|
-
|
|
178
|
+
streams.stdout?.on("data", (bytes: Buffer) => observe("stdout", bytes));
|
|
179
|
+
streams.stderr?.on("data", (bytes: Buffer) => observe("stderr", bytes));
|
|
180
|
+
for (const stream of Object.values(streams))
|
|
181
|
+
stream?.on("error", (error) => {
|
|
182
|
+
failure ??= "child event channel failed: " + String(error);
|
|
183
|
+
stop();
|
|
184
|
+
});
|
|
165
185
|
for (const stream of ["stdout", "stderr"] as const)
|
|
166
|
-
|
|
186
|
+
streams[stream]?.on("end", () => {
|
|
167
187
|
const tail = decoders[stream].end();
|
|
168
188
|
if (tail && !(stream === "stdout" && request.captureStdout === false)) {
|
|
169
189
|
const kept = takeBytes(tail, Math.max(0, (request.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES) - count));
|
|
@@ -276,9 +296,7 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
276
296
|
void (async () => {
|
|
277
297
|
if (!identity || !(await readCapturedWorkerReceipt(identity))) {
|
|
278
298
|
failure ??= "helper exited without an independent settlement receipt";
|
|
279
|
-
|
|
280
|
-
child.stderr?.destroy();
|
|
281
|
-
status.destroy();
|
|
299
|
+
closeObservations();
|
|
282
300
|
}
|
|
283
301
|
})();
|
|
284
302
|
});
|
|
@@ -80,7 +80,7 @@ export interface PromptOutcome {
|
|
|
80
80
|
|
|
81
81
|
export interface ApprovalGateOptions {
|
|
82
82
|
ui: ApprovalUI;
|
|
83
|
-
/**
|
|
83
|
+
/** Whether a human can answer here. False in print/json mode and view-only child tabs. */
|
|
84
84
|
hasUI: boolean;
|
|
85
85
|
/** pi's `ctx.mode`, quoted back in the refusal so an operator can see why. */
|
|
86
86
|
mode: string;
|