billion-context-pi 0.1.27 → 0.1.28

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.
@@ -0,0 +1,69 @@
1
+ export interface ToolStartEvent {
2
+ kind: "tool-start";
3
+ toolName: string;
4
+ argsText: string;
5
+ }
6
+ export interface ToolUpdateEvent {
7
+ kind: "tool-update";
8
+ toolCallId: string;
9
+ text: string;
10
+ }
11
+ export interface ToolEndEvent {
12
+ kind: "tool-end";
13
+ toolName: string;
14
+ isError: boolean;
15
+ }
16
+ export interface ReplyDeltaEvent {
17
+ kind: "reply-delta";
18
+ delta: string;
19
+ }
20
+ export interface ReplyCompleteEvent {
21
+ kind: "reply-complete";
22
+ content: string;
23
+ }
24
+ export interface ThinkingDeltaEvent {
25
+ kind: "thinking-delta";
26
+ delta: string;
27
+ }
28
+ export type ParsedEvent = ToolStartEvent | ToolUpdateEvent | ToolEndEvent | ReplyDeltaEvent | ReplyCompleteEvent | ThinkingDeltaEvent | ThinkingEndEvent | RetryStartEvent | RetryEndEvent;
29
+ export interface ThinkingEndEvent {
30
+ kind: "thinking-end";
31
+ }
32
+ /**
33
+ * Accumulates thinking_delta tokens and emits one human-readable line per
34
+ * thinking segment (a segment ends at thinking_end / text_start). Nothing is
35
+ * emitted when showThinking is off.
36
+ */
37
+ export declare class ThinkingCollector {
38
+ private readonly showThinking;
39
+ private buf;
40
+ constructor(showThinking: boolean);
41
+ push(delta: string): void;
42
+ /** Return the segment line to write ("" when empty or disabled), resetting. */
43
+ flush(): string;
44
+ }
45
+ export interface RetryStartEvent {
46
+ kind: "retry-start";
47
+ attempt: number;
48
+ maxAttempts: number;
49
+ delayMs: number;
50
+ errorMessage: string;
51
+ }
52
+ export interface RetryEndEvent {
53
+ kind: "retry-end";
54
+ success: boolean;
55
+ attempt: number;
56
+ }
57
+ export declare function parseEventLine(line: string): ParsedEvent | null;
58
+ /** Join `content[].text` blocks from a tool result / partialResult payload. */
59
+ export declare function extractContentText(payload: unknown): string;
60
+ /** Format a parsed event as human-readable activity file lines (each with a
61
+ * trailing newline; empty when none). */
62
+ export declare function activityLines(ev: ParsedEvent, opts: {
63
+ showThinking: boolean;
64
+ }): string[];
65
+ /**
66
+ * partialResult is an accumulated snapshot (not a delta), so each update
67
+ * carries everything so far. Return only the newly-appended portion.
68
+ */
69
+ export declare function newPortion(text: string, prev: string): string;
@@ -13,6 +13,7 @@ declare const DelegateParams: Type.TObject<{
13
13
  cwd: Type.TOptional<Type.TString>;
14
14
  model: Type.TOptional<Type.TString>;
15
15
  async: Type.TOptional<Type.TBoolean>;
16
+ showThinking: Type.TOptional<Type.TBoolean>;
16
17
  }>;
17
18
  type DelegateArgs = Static<typeof DelegateParams>;
18
19
  declare const CancelParams: Type.TObject<{
@@ -40,5 +41,7 @@ export declare function makeDelegateCancelTool(_pi: ExtensionAPI): ToolDefinitio
40
41
  export declare function buildChildArgs(args: DelegateArgs, rolePrompt: string, ctx: ExtensionContext): Promise<{
41
42
  cliArgs: string[];
42
43
  tmpDir: string;
44
+ isAsync: boolean;
45
+ useJsonStream: boolean;
43
46
  }>;
44
47
  export {};
@@ -0,0 +1,31 @@
1
+ import type { Readable } from "node:stream";
2
+ export interface WatchdogOptions {
3
+ eofGraceMs: number;
4
+ idleMs: number;
5
+ timeoutMs: number;
6
+ killGraceMs: number;
7
+ }
8
+ export interface WatchdogHooks {
9
+ /** True once the run is finalized; watchdogs stop firing. */
10
+ isSettled(): boolean;
11
+ /** The child is about to be killed (SIGTERM). reason explains why. */
12
+ onKill(reason: string): void;
13
+ /** stdout EOF passed without the process exiting; force-finalize now. */
14
+ onEofGrace(): void;
15
+ }
16
+ export interface WatchdogHandle {
17
+ /** Re-arm the idle timer (call on every stdout data). */
18
+ poke(): void;
19
+ /** Stop all timers (call on finalize). */
20
+ dispose(): void;
21
+ }
22
+ /**
23
+ * Guarantees a hung child process gets killed. A stuck child holds its stdout
24
+ * fd open, so stdout EOF never fires — hence the idle timer (no output for
25
+ * idleMs) is the main defense; the hard time limit and the EOF grace period
26
+ * cover the rest. Kill is SIGTERM, escalated to SIGKILL after killGraceMs.
27
+ */
28
+ export declare function attachWatchdogs(child: {
29
+ kill(signal: NodeJS.Signals): boolean;
30
+ stdout: Readable | null;
31
+ }, hooks: WatchdogHooks, opts: WatchdogOptions): WatchdogHandle;
package/dist/index.js CHANGED
@@ -2635,6 +2635,10 @@ function readContextEntries(sm) {
2635
2635
  if (typeof source.getBranch === "function") return source.getBranch();
2636
2636
  return [];
2637
2637
  }
2638
+ function isPiHost(sm) {
2639
+ const source = sm;
2640
+ return typeof source.buildContextEntries === "function";
2641
+ }
2638
2642
  function createRuntime(adapter) {
2639
2643
  const core = createCore({ countTokens: defaultCountTokens });
2640
2644
  const store = new SessionStateStore();
@@ -7566,7 +7570,8 @@ ${extra.join("\n")}` : base;
7566
7570
  import {
7567
7571
  spawn
7568
7572
  } from "child_process";
7569
- import { mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm } from "fs/promises";
7573
+ import { createWriteStream } from "fs";
7574
+ import { mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm, appendFile } from "fs/promises";
7570
7575
  import { tmpdir as tmpdir2 } from "os";
7571
7576
  import { join as join4 } from "path";
7572
7577
 
@@ -7658,9 +7663,203 @@ var delegateStatusWidget = {
7658
7663
  }
7659
7664
  };
7660
7665
 
7666
+ // src/delegate-watchdog.ts
7667
+ function attachWatchdogs(child, hooks, opts) {
7668
+ let idleTimer;
7669
+ let eofTimer;
7670
+ let killGraceTimer;
7671
+ let timeoutTimer;
7672
+ const clearTimers = () => {
7673
+ if (idleTimer) clearTimeout(idleTimer);
7674
+ if (eofTimer) clearTimeout(eofTimer);
7675
+ if (killGraceTimer) clearTimeout(killGraceTimer);
7676
+ if (timeoutTimer) clearTimeout(timeoutTimer);
7677
+ };
7678
+ const killByWatchdog = (reason) => {
7679
+ if (hooks.isSettled()) return;
7680
+ hooks.onKill(reason);
7681
+ try {
7682
+ child.kill("SIGTERM");
7683
+ } catch {
7684
+ }
7685
+ killGraceTimer = setTimeout(() => {
7686
+ if (hooks.isSettled()) return;
7687
+ try {
7688
+ child.kill("SIGKILL");
7689
+ } catch {
7690
+ }
7691
+ }, opts.killGraceMs);
7692
+ killGraceTimer.unref?.();
7693
+ };
7694
+ const poke = () => {
7695
+ if (idleTimer) clearTimeout(idleTimer);
7696
+ idleTimer = setTimeout(() => killByWatchdog(`no output for ${opts.idleMs / 6e4}m`), opts.idleMs);
7697
+ idleTimer.unref?.();
7698
+ };
7699
+ poke();
7700
+ timeoutTimer = setTimeout(() => killByWatchdog(`${opts.timeoutMs / 6e4}m limit`), opts.timeoutMs);
7701
+ timeoutTimer.unref?.();
7702
+ const onStdoutEnd = () => {
7703
+ if (hooks.isSettled()) return;
7704
+ eofTimer = setTimeout(() => {
7705
+ if (hooks.isSettled()) return;
7706
+ hooks.onEofGrace();
7707
+ try {
7708
+ child.kill("SIGTERM");
7709
+ } catch {
7710
+ }
7711
+ }, opts.eofGraceMs);
7712
+ eofTimer.unref?.();
7713
+ };
7714
+ child.stdout?.once("end", onStdoutEnd);
7715
+ return {
7716
+ poke,
7717
+ dispose: () => {
7718
+ clearTimers();
7719
+ child.stdout?.removeListener("end", onStdoutEnd);
7720
+ }
7721
+ };
7722
+ }
7723
+
7724
+ // src/delegate-events.ts
7725
+ var ThinkingCollector = class {
7726
+ constructor(showThinking) {
7727
+ this.showThinking = showThinking;
7728
+ }
7729
+ showThinking;
7730
+ buf = "";
7731
+ push(delta) {
7732
+ this.buf += delta;
7733
+ }
7734
+ /** Return the segment line to write ("" when empty or disabled), resetting. */
7735
+ flush() {
7736
+ const text = this.buf.trim();
7737
+ this.buf = "";
7738
+ if (!this.showThinking || !text) return "";
7739
+ return `[thinking] ${text}
7740
+ `;
7741
+ }
7742
+ };
7743
+ function parseEventLine(line) {
7744
+ let ev;
7745
+ try {
7746
+ ev = JSON.parse(line);
7747
+ } catch {
7748
+ return null;
7749
+ }
7750
+ if (typeof ev !== "object" || ev === null) return null;
7751
+ const e = ev;
7752
+ if (e.type === "message_update") {
7753
+ const am = e.assistantMessageEvent;
7754
+ if (typeof am !== "object" || am === null) return null;
7755
+ const msg = am;
7756
+ switch (msg.type) {
7757
+ case "text_delta":
7758
+ return { kind: "reply-delta", delta: String(msg.delta ?? "") };
7759
+ case "text_end":
7760
+ return { kind: "reply-complete", content: String(msg.content ?? "") };
7761
+ case "thinking_delta":
7762
+ return { kind: "thinking-delta", delta: String(msg.delta ?? "") };
7763
+ case "thinking_end":
7764
+ return { kind: "thinking-end" };
7765
+ default:
7766
+ return null;
7767
+ }
7768
+ }
7769
+ if (e.type === "tool_execution_start") {
7770
+ return {
7771
+ kind: "tool-start",
7772
+ toolName: String(e.toolName ?? ""),
7773
+ argsText: formatArgs(e.args)
7774
+ };
7775
+ }
7776
+ if (e.type === "tool_execution_update") {
7777
+ return {
7778
+ kind: "tool-update",
7779
+ toolCallId: String(e.toolCallId ?? ""),
7780
+ text: extractContentText(e.partialResult)
7781
+ };
7782
+ }
7783
+ if (e.type === "tool_execution_end") {
7784
+ return {
7785
+ kind: "tool-end",
7786
+ toolName: String(e.toolName ?? ""),
7787
+ isError: Boolean(e.isError)
7788
+ };
7789
+ }
7790
+ if (e.type === "auto_retry_start") {
7791
+ return {
7792
+ kind: "retry-start",
7793
+ attempt: Number(e.attempt ?? 0),
7794
+ maxAttempts: Number(e.maxAttempts ?? 0),
7795
+ delayMs: Number(e.delayMs ?? 0),
7796
+ errorMessage: String(e.errorMessage ?? "")
7797
+ };
7798
+ }
7799
+ if (e.type === "auto_retry_end") {
7800
+ return {
7801
+ kind: "retry-end",
7802
+ success: Boolean(e.success),
7803
+ attempt: Number(e.attempt ?? 0)
7804
+ };
7805
+ }
7806
+ return null;
7807
+ }
7808
+ function formatArgs(args) {
7809
+ if (args && typeof args === "object") {
7810
+ const a = args;
7811
+ if (typeof a.command === "string") return a.command;
7812
+ }
7813
+ try {
7814
+ return JSON.stringify(args);
7815
+ } catch {
7816
+ return String(args);
7817
+ }
7818
+ }
7819
+ function extractContentText(payload) {
7820
+ if (!payload || typeof payload !== "object") return "";
7821
+ const content = payload.content;
7822
+ if (!Array.isArray(content)) return "";
7823
+ return content.map((c) => c && typeof c === "object" ? String(c.text ?? "") : "").join("");
7824
+ }
7825
+ function activityLines(ev, opts) {
7826
+ switch (ev.kind) {
7827
+ case "tool-start":
7828
+ return [`[tool] ${ev.toolName}${ev.argsText ? ` ${ev.argsText}` : ""}
7829
+ `];
7830
+ case "tool-update": {
7831
+ if (!ev.text) return [];
7832
+ return [ev.text.endsWith("\n") ? ev.text : `${ev.text}
7833
+ `];
7834
+ }
7835
+ case "tool-end":
7836
+ return [`[done] ${ev.toolName}${ev.isError ? " (error)" : ""}
7837
+ `];
7838
+ case "thinking-delta":
7839
+ return opts.showThinking ? [`[thinking] ${ev.delta}
7840
+ `] : [];
7841
+ case "retry-start":
7842
+ return [`[retry] attempt ${ev.attempt}/${ev.maxAttempts}, backoff ${ev.delayMs}ms${ev.errorMessage ? ` \u2014 ${ev.errorMessage}` : ""}
7843
+ `];
7844
+ case "retry-end":
7845
+ return [`[retry] attempt ${ev.attempt} ${ev.success ? "succeeded" : "failed"}
7846
+ `];
7847
+ default:
7848
+ return [];
7849
+ }
7850
+ }
7851
+ function newPortion(text, prev) {
7852
+ if (text.startsWith(prev)) return text.slice(prev.length);
7853
+ return text;
7854
+ }
7855
+
7661
7856
  // src/delegate-tool.ts
7662
7857
  var MAX_DEPTH = 2;
7663
7858
  var SYNC_TIMEOUT_MS = 5 * 6e4;
7859
+ var EOF_GRACE_MS = 1e4;
7860
+ var IDLE_GRACE_MS = 5 * 6e4;
7861
+ var ASYNC_TIMEOUT_MS = 30 * 6e4;
7862
+ var KILL_GRACE_MS = 1e4;
7664
7863
  var RESULT_SUMMARY_CHARS = 500;
7665
7864
  var OUT_DIR = join4(tmpdir2(), "acp-delegate");
7666
7865
  var ACP_TOOLS = ["compress", "decompress", "search_context", "acp_status"];
@@ -7728,6 +7927,11 @@ var DelegateParams = typebox_exports.Object({
7728
7927
  typebox_exports.Boolean({
7729
7928
  description: "If true (default), return immediately with a runId. In long-lived sessions (interactive/rpc) a short notification is injected into chat when the delegate finishes; in one-shot sessions (print/json, e.g. `pi -p` / SDK) async auto-downgrades to sync and the result is returned here. If false, always block and return the output here."
7730
7929
  })
7930
+ ),
7931
+ showThinking: typebox_exports.Optional(
7932
+ typebox_exports.Boolean({
7933
+ description: "If true, the delegate's thinking deltas are also written to the live activity file (default: false \u2014 only tool activity is shown)."
7934
+ })
7731
7935
  )
7732
7936
  });
7733
7937
  var CancelParams = typebox_exports.Object({
@@ -7785,7 +7989,8 @@ The delegate runs in its own clean pi process \u2014 it does NOT see this conver
7785
7989
  };
7786
7990
  }
7787
7991
  function formatRunResult(run) {
7788
- const header = run.status === "completed" ? `Delegate **${run.agent}** (runId \`${run.runId}\`) completed (exit ${run.exitCode ?? "?"})${remainingLineForWait(run.runId)}` : `Delegate **${run.agent}** (runId \`${run.runId}\`) ${run.status} (exit ${run.exitCode ?? "?"})${remainingLineForWait(run.runId)}`;
7992
+ const timeoutNote = run.timedOut ? ` (timed out: ${run.timedOut})` : "";
7993
+ const header = run.status === "completed" ? `Delegate **${run.agent}** (runId \`${run.runId}\`) completed (exit ${run.exitCode ?? "?"})${timeoutNote}${remainingLineForWait(run.runId)}` : `Delegate **${run.agent}** (runId \`${run.runId}\`) ${run.status} (exit ${run.exitCode ?? "?"})${timeoutNote}${remainingLineForWait(run.runId)}`;
7789
7994
  return formatPayload(header, run.result?.file ?? "", run.task, run.result?.body);
7790
7995
  }
7791
7996
  function remainingLineForWait(selfRunId) {
@@ -7924,13 +8129,12 @@ async function runDelegate(pi, args, ctx, signal) {
7924
8129
  ...process.env,
7925
8130
  PI_ACP_DELEGATE_DEPTH: String(parentDepth + 1)
7926
8131
  };
7927
- const { cliArgs, tmpDir } = await buildChildArgs(args, agent.prompt, ctx);
8132
+ const { cliArgs, tmpDir, isAsync, useJsonStream } = await buildChildArgs(args, agent.prompt, ctx);
7928
8133
  const requestedAsync = args.async !== false;
7929
- const isAsync = requestedAsync && ctx.mode !== "print" && ctx.mode !== "json";
7930
8134
  if (requestedAsync && !isAsync) {
7931
8135
  debug.event("delegate-async-downgraded", { reason: `mode=${ctx.mode}` });
7932
8136
  }
7933
- debug.event("delegate-spawn", { agent: args.agent, cwd, async: isAsync, cliArgs });
8137
+ debug.event("delegate-spawn", { agent: args.agent, cwd, async: isAsync, useJsonStream, cliArgs });
7934
8138
  const child = spawn(process.execPath, [process.argv[1], ...cliArgs], {
7935
8139
  cwd,
7936
8140
  env: childEnv,
@@ -7941,12 +8145,94 @@ async function runDelegate(pi, args, ctx, signal) {
7941
8145
  debug.event("delegate-stdin-error", { runId: "pre-spawn", error: String(e) });
7942
8146
  });
7943
8147
  child.stdin?.end(args.task);
7944
- let stdoutChunks = [];
7945
8148
  let stderrText = "";
7946
8149
  const runId = `del_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
7947
8150
  const startedAt = Date.now();
7948
8151
  if (isAsync) {
7949
- child.stdout?.on("data", (c) => stdoutChunks.push(c));
8152
+ let settled = false;
8153
+ const watchdog = attachWatchdogs(
8154
+ child,
8155
+ {
8156
+ isSettled: () => settled || run.status !== "running",
8157
+ onKill: (reason) => {
8158
+ run.timedOut = reason;
8159
+ debug.event("delegate-watchdog", { runId, reason });
8160
+ },
8161
+ onEofGrace: () => {
8162
+ run.timedOut = "output ended but process did not exit";
8163
+ debug.event("delegate-eof-grace", { runId, ms: EOF_GRACE_MS });
8164
+ }
8165
+ },
8166
+ { eofGraceMs: EOF_GRACE_MS, idleMs: IDLE_GRACE_MS, timeoutMs: ASYNC_TIMEOUT_MS, killGraceMs: KILL_GRACE_MS }
8167
+ );
8168
+ const replyFile = join4(OUT_DIR, `${runId}.out`);
8169
+ const activityFile = join4(OUT_DIR, `${runId}.activity`);
8170
+ await mkdir2(OUT_DIR, { recursive: true });
8171
+ const replyStream = createWriteStream(replyFile, { flags: "a" });
8172
+ const activityStream = useJsonStream ? createWriteStream(activityFile, { flags: "a" }) : null;
8173
+ const endStream = (s) => new Promise((resolve2) => {
8174
+ if (!s || s.destroyed || s.closed) return resolve2();
8175
+ s.end(() => resolve2());
8176
+ });
8177
+ let replyText = "";
8178
+ let stdoutBuf = "";
8179
+ const lastToolText = /* @__PURE__ */ new Map();
8180
+ const thinking = new ThinkingCollector(args.showThinking === true);
8181
+ const flushThinking = () => {
8182
+ const line = thinking.flush();
8183
+ if (line) activityStream?.write(line);
8184
+ };
8185
+ const handleEventLine = (line) => {
8186
+ const ev = parseEventLine(line);
8187
+ if (!ev) return;
8188
+ if (ev.kind === "thinking-delta") {
8189
+ thinking.push(ev.delta);
8190
+ return;
8191
+ }
8192
+ if (ev.kind === "thinking-end") {
8193
+ flushThinking();
8194
+ return;
8195
+ }
8196
+ if (ev.kind === "reply-delta") {
8197
+ flushThinking();
8198
+ replyText += ev.delta;
8199
+ replyStream.write(ev.delta);
8200
+ return;
8201
+ }
8202
+ if (ev.kind === "reply-complete") {
8203
+ flushThinking();
8204
+ replyText = ev.content;
8205
+ return;
8206
+ }
8207
+ if (ev.kind === "tool-update") {
8208
+ flushThinking();
8209
+ const prev = lastToolText.get(ev.toolCallId) ?? "";
8210
+ const add = newPortion(ev.text, prev);
8211
+ lastToolText.set(ev.toolCallId, ev.text);
8212
+ if (add) activityStream?.write(add.endsWith("\n") ? add : `${add}
8213
+ `);
8214
+ return;
8215
+ }
8216
+ flushThinking();
8217
+ const lines = activityLines(ev, { showThinking: args.showThinking === true });
8218
+ if (lines.length) activityStream?.write(lines.join(""));
8219
+ };
8220
+ child.stdout?.on("data", (c) => {
8221
+ watchdog.poke();
8222
+ if (useJsonStream) {
8223
+ stdoutBuf += c.toString("utf8");
8224
+ let nl;
8225
+ while ((nl = stdoutBuf.indexOf("\n")) >= 0) {
8226
+ const line = stdoutBuf.slice(0, nl);
8227
+ stdoutBuf = stdoutBuf.slice(nl + 1);
8228
+ handleEventLine(line);
8229
+ }
8230
+ } else {
8231
+ const text = c.toString("utf8");
8232
+ replyText += text;
8233
+ replyStream.write(text);
8234
+ }
8235
+ });
7950
8236
  child.stderr?.on("data", (c) => {
7951
8237
  stderrText += c.toString("utf8");
7952
8238
  });
@@ -7961,47 +8247,69 @@ async function runDelegate(pi, args, ctx, signal) {
7961
8247
  };
7962
8248
  runs.set(runId, run);
7963
8249
  delegateStatusWidget.poke();
7964
- child.on("close", (code) => {
7965
- void cleanupTmp(tmpDir);
7966
- const output = Buffer.concat(stdoutChunks).toString("utf8").trim();
7967
- run.exitCode = code;
7968
- const body2 = code === 0 ? output || "(no output)" : stderrText.trim() || output || "(no output)";
7969
- if (run.status === "cancelled") {
7970
- run.finishedAt = Date.now();
7971
- debug.event("delegate-done", { runId, code, status: run.status, injected: false, outLen: output.length });
7972
- run.waiter?.();
7973
- delegateStatusWidget.poke();
7974
- return;
7975
- }
7976
- void persistResult(runId, body2).then((file2) => {
7977
- run.result = { code, file: file2, body: body2 };
7978
- run.status = code === 0 ? "completed" : "failed";
7979
- run.finishedAt = Date.now();
7980
- if (run.waiter) {
7981
- debug.event("delegate-done", { runId, code, status: run.status, injected: false, via: "wait", outLen: output.length, file: file2 });
7982
- run.waiter();
8250
+ const finalize = (code) => {
8251
+ void (async () => {
8252
+ if (settled) return;
8253
+ settled = true;
8254
+ watchdog.dispose();
8255
+ void cleanupTmp(tmpDir);
8256
+ await Promise.all([endStream(replyStream), endStream(activityStream)]);
8257
+ run.exitCode = code;
8258
+ const output = replyText.trim();
8259
+ const body2 = code === 0 ? output || "(no output)" : stderrText.trim() || output || "(no output)";
8260
+ if (run.status === "cancelled") {
8261
+ await Promise.all([rm(replyFile, { force: true }), rm(activityFile, { force: true })]);
8262
+ run.finishedAt = Date.now();
8263
+ debug.event("delegate-done", { runId, code, status: run.status, injected: false, outLen: output.length });
8264
+ run.waiter?.();
7983
8265
  delegateStatusWidget.poke();
7984
8266
  return;
7985
8267
  }
7986
- if (run.consumed) {
7987
- debug.event("delegate-done", { runId, code, status: run.status, injected: false, via: "consumed", outLen: output.length, file: file2 });
8268
+ try {
8269
+ const file2 = replyFile;
8270
+ if (output === "") {
8271
+ const fallback = stderrText.trim();
8272
+ await appendFile(file2, fallback ? `${fallback}
8273
+ ` : "(no output)\n");
8274
+ }
8275
+ const effectiveCode = code ?? (output || stderrText ? 0 : null);
8276
+ run.result = { code, file: file2, body: body2 };
8277
+ run.status = effectiveCode === 0 ? "completed" : "failed";
8278
+ run.finishedAt = Date.now();
8279
+ if (run.waiter) {
8280
+ debug.event("delegate-done", { runId, code, status: run.status, injected: false, via: "wait", outLen: output.length, file: file2 });
8281
+ run.waiter();
8282
+ delegateStatusWidget.poke();
8283
+ return;
8284
+ }
8285
+ if (run.consumed) {
8286
+ debug.event("delegate-done", { runId, code, status: run.status, injected: false, via: "consumed", outLen: output.length, file: file2 });
8287
+ delegateStatusWidget.poke();
8288
+ return;
8289
+ }
8290
+ const injected = injectResult(pi, args.agent, runId, args.task, code, file2, run.timedOut);
8291
+ run.injected = injected;
8292
+ debug.event("delegate-done", { runId, code, status: run.status, injected, outLen: output.length, file: file2 });
8293
+ delegateStatusWidget.poke();
8294
+ } catch (err) {
8295
+ run.status = "failed";
8296
+ run.finishedAt = Date.now();
8297
+ debug.event("delegate-done-error", { runId, error: String(err) });
8298
+ run.waiter?.();
7988
8299
  delegateStatusWidget.poke();
7989
- return;
7990
8300
  }
7991
- const injected = injectResult(pi, args.agent, runId, args.task, code, file2);
7992
- run.injected = injected;
7993
- debug.event("delegate-done", { runId, code, status: run.status, injected, outLen: output.length, file: file2 });
7994
- delegateStatusWidget.poke();
7995
- }).catch((err) => {
7996
- run.status = "failed";
7997
- run.finishedAt = Date.now();
7998
- debug.event("delegate-done-error", { runId, error: String(err) });
7999
- run.waiter?.();
8000
- delegateStatusWidget.poke();
8001
- });
8002
- });
8301
+ })();
8302
+ };
8303
+ child.on("close", (code) => finalize(code));
8003
8304
  child.on("error", (err) => {
8305
+ if (settled) return;
8306
+ settled = true;
8307
+ watchdog.dispose();
8004
8308
  void cleanupTmp(tmpDir);
8309
+ void replyStream.destroy();
8310
+ void activityStream?.destroy();
8311
+ void rm(replyFile, { force: true });
8312
+ void rm(activityFile, { force: true });
8005
8313
  if (run.status === "running" || run.status === "cancelled") {
8006
8314
  run.status = run.status === "cancelled" ? "cancelled" : "failed";
8007
8315
  run.finishedAt = Date.now();
@@ -8016,6 +8324,8 @@ async function runDelegate(pi, args, ctx, signal) {
8016
8324
  `Delegated to **${args.agent}** (runId \`${runId}\`).`,
8017
8325
  `Task: ${truncate2(args.task, 160)}`,
8018
8326
  `Running in the background at \`${cwd}\`.`,
8327
+ useJsonStream ? `Live activity is streaming to \`${activityFile}\` \u2014 read it anytime to watch the delegate work (tool calls and their output${args.showThinking ? ", plus thinking" : ""}).` : `The reply is streaming to \`${replyFile}\` \u2014 read it anytime to see partial output (this host has no json event mode, so tool activity is not visible).`,
8328
+ `A watchdog force-finishes a hung run: no output for ${IDLE_GRACE_MS / 6e4}m, 10s after output ends, or a ${ASYNC_TIMEOUT_MS / 6e4}m hard limit \u2014 the result reflects whatever was produced.`,
8019
8329
  ``,
8020
8330
  `Call acp_delegate_wait({ runId: "${runId}" }) to block for the result (default 10s timeout). If the wait times out, or you skip it, a completion notification (with the result file path) is still injected here automatically when the delegate finishes \u2014 so you may also just continue other work now and let the result find you.`
8021
8331
  ].join("\n");
@@ -8034,7 +8344,9 @@ async function buildChildArgs(args, rolePrompt, ctx) {
8034
8344
  ---
8035
8345
 
8036
8346
  Complete the task below.`, "utf8");
8037
- const cliArgs = ["-p", "--no-session", "--append-system-prompt", promptFile];
8347
+ const isAsync = args.async !== false && ctx.mode !== "print" && ctx.mode !== "json";
8348
+ const useJsonStream = isAsync && isPiHost(ctx.sessionManager);
8349
+ const cliArgs = useJsonStream ? ["--mode", "json", "--no-session", "--append-system-prompt", promptFile] : ["-p", "--no-session", "--append-system-prompt", promptFile];
8038
8350
  const agentDef = AGENTS[args.agent];
8039
8351
  if (agentDef?.restricted) {
8040
8352
  const merged = [.../* @__PURE__ */ new Set([...agentDef.tools.split(",").map((s) => s.trim()), ...ACP_TOOLS])];
@@ -8047,7 +8359,7 @@ Complete the task below.`, "utf8");
8047
8359
  } else if (ctx.model) {
8048
8360
  cliArgs.push("--provider", ctx.model.provider, "--model", ctx.model.id);
8049
8361
  }
8050
- return { cliArgs, tmpDir };
8362
+ return { cliArgs, tmpDir, isAsync, useJsonStream };
8051
8363
  }
8052
8364
  function waitForChild(child, signal) {
8053
8365
  return new Promise((resolve2) => {
@@ -8093,7 +8405,7 @@ function formatSyncResult(agent, runId, task, r, file) {
8093
8405
  const body = r.timedOut ? "(timed out)" : r.stderr.trim() || "(no stderr)";
8094
8406
  return formatPayload(header, file, task, body);
8095
8407
  }
8096
- function injectResult(pi, agent, runId, task, code, file) {
8408
+ function injectResult(pi, agent, runId, task, code, file, timedOut) {
8097
8409
  const send = pi.sendUserMessage;
8098
8410
  if (typeof send !== "function") {
8099
8411
  debug.event("delegate-inject-skipped", { runId, reason: "sendUserMessage unavailable" });
@@ -8102,7 +8414,8 @@ function injectResult(pi, agent, runId, task, code, file) {
8102
8414
  const status = code === 0 ? "completed" : "failed";
8103
8415
  const remaining = Array.from(runs.values()).filter((r) => r.status === "running").length;
8104
8416
  const remainingLine = remaining > 0 ? ` ${remaining} delegate${remaining === 1 ? " is" : "s are"} still running; keep doing other work and their notifications will arrive as they finish.` : " No delegates are currently running.";
8105
- const header = `[acp_delegate ${status}] **${agent}** (runId \`${runId}\`, exit ${code ?? "?"})${remainingLine} This is an automated system notification, NOT a user message. Read the result file if you need the details, then continue your original task; do not treat this as a new user request.`;
8417
+ const timeoutNote = timedOut ? ` (timed out: ${timedOut})` : "";
8418
+ const header = `[acp_delegate ${status}] **${agent}** (runId \`${runId}\`, exit ${code ?? "?"})${timeoutNote}${remainingLine} This is an automated system notification, NOT a user message. Read the result file if you need the details, then continue your original task; do not treat this as a new user request.`;
8106
8419
  const text = formatPayload(header, file, task);
8107
8420
  try {
8108
8421
  send.call(pi, text, { deliverAs: "followUp" });
@@ -8246,7 +8559,7 @@ async function statusReport(runtime, ctx) {
8246
8559
  const activeBlocksList = state.blocks.filter((b) => b.active);
8247
8560
  const totalBlocksList = state.blocks;
8248
8561
  const lines = [];
8249
- const versionStr = "0.1.27" ? `billion-context-pi@${"0.1.27"}` : "";
8562
+ const versionStr = "0.1.28" ? `billion-context-pi@${"0.1.28"}` : "";
8250
8563
  lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
8251
8564
  lines.push("\u2502 ACP Context Analysis \u2502");
8252
8565
  lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
@@ -8606,7 +8919,7 @@ async function checkForUpdate(autoUpdate, notify) {
8606
8919
  const data = await res.json();
8607
8920
  const latest = data.version;
8608
8921
  if (!latest) return;
8609
- const current = runtimeVersion ?? "0.1.27";
8922
+ const current = runtimeVersion ?? "0.1.28";
8610
8923
  debug.event("update-check", {
8611
8924
  current,
8612
8925
  latest,