billion-context-pi 0.1.48 → 0.1.49-pr.201.19

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/README.md CHANGED
@@ -104,6 +104,7 @@ The full delegate result is saved to a file (`/tmp/acp-delegate/<runId>.out`); t
104
104
 
105
105
  - **Interactive (TUI) & RPC modes**: `async:true` (default) runs the child in the background; a short completion notification is injected into the chat when it finishes.
106
106
  - **Print / JSON modes** (`pi -p`, SDK): `async:true` auto-downgrades to **synchronous** — the result returns as the tool result in the same turn (the parent exits after one turn, so background injection would be lost).
107
+ - **Failures are loud, never silent.** A run that fails (nonzero exit, spawn error, watchdog timeout) injects a `FAILED ⚠️` notification carrying a short error excerpt, so a failed delegate cannot hide among sibling completions. If a notification cannot be delivered at all, a recovery notice is attached to the next delegate notification or the next `acp_delegate` / `acp_delegate_wait` / `acp_delegate_cancel` tool result — a dispatched run's failure always reaches the model before it wraps up.
107
108
 
108
109
  In the **interactive TUI**, async runs also show a live status widget below the editor (agent, elapsed seconds, task preview), so you always know what's running and for how long. Disabled automatically in RPC/print/JSON.
109
110
 
@@ -104,6 +104,26 @@ declare const WaitParams: Type.TObject<{
104
104
  }>;
105
105
  export declare function accumulateUsage(a: Usage | undefined, b: Usage): Usage;
106
106
  export declare function makeDelegateTool(pi: ExtensionAPI): ToolDefinition<typeof DelegateParams>;
107
+ /** Runs that reached a terminal state but whose result never reached the
108
+ * model: no parked waiter, not consumed by a tool result, and never injected
109
+ * as a notification. The model was promised a notification ("do NOT keep
110
+ * waiting"), so these must eventually be recovered, or a failed delegate
111
+ * stays invisible until the very end of the task. */
112
+ export declare function findUndeliveredRuns(all: DelegateRun[], excludeRunId?: string): DelegateRun[];
113
+ /** Compute the recovery notice for undelivered runs WITHOUT marking them
114
+ * delivered. The caller commits the marking (covered[].injected = true) only
115
+ * after the carrier message is actually sent: if the send throws, the runs
116
+ * must stay undelivered so a later carrier can recover them. */
117
+ export declare function buildRecoveryNotice(all: DelegateRun[], excludeRunId?: string): {
118
+ text: string;
119
+ covered: DelegateRun[];
120
+ };
121
+ /** Build a recovery notice for undelivered runs and mark each covered run
122
+ * injected=true (this notice IS its delivery) so it is never re-notified and
123
+ * a later wait dedups instead of re-delivering the payload. For carriers the
124
+ * host owns (delegate tool results); injectResult commits the marking itself
125
+ * only after its send succeeds (see buildRecoveryNotice). */
126
+ export declare function undeliveredNoticeFrom(all: DelegateRun[], excludeRunId?: string): string;
107
127
  /** If the delegate already delivered its result via a system notification
108
128
  * (the close handler injected before this wait was called), return a short
109
129
  * "already delivered" message pointing at the result file, so the model
@@ -143,5 +163,5 @@ export declare function buildChildArgs(args: DelegateArgs, rolePrompt: string, c
143
163
  isAsync: boolean;
144
164
  useJsonStream: boolean;
145
165
  }>;
146
- export declare function injectResult(pi: ExtensionAPI, agent: string, runId: string, task: string, code: number | null, file: string, timedOut?: string, usage?: Usage, mode?: "merged" | "separate", usageAlreadyReported?: boolean): boolean;
166
+ export declare function injectResult(pi: ExtensionAPI, agent: string, runId: string, task: string, code: number | null, file: string, timedOut?: string, usage?: Usage, mode?: "merged" | "separate", usageAlreadyReported?: boolean, body?: string): boolean;
147
167
  export {};
package/dist/index.js CHANGED
@@ -3753,6 +3753,15 @@ function extractText(content) {
3753
3753
  function stripRefTag(text) {
3754
3754
  return text.replace(REF_TAG, "").replace(TRAILING_REF_TAG, "");
3755
3755
  }
3756
+ function countImageBlocks(content) {
3757
+ if (!Array.isArray(content)) return 0;
3758
+ let n = 0;
3759
+ for (const block of content) {
3760
+ const b = block;
3761
+ if (b?.type === "image") n++;
3762
+ }
3763
+ return n;
3764
+ }
3756
3765
  function messageIdentity(message) {
3757
3766
  return JSON.stringify(normalizeIdentityValue(message, true));
3758
3767
  }
@@ -9172,12 +9181,29 @@ function collectCoveredMessageIds(state) {
9172
9181
  }
9173
9182
  return ids;
9174
9183
  }
9175
- function estimateTokens(messages, coveredIds) {
9184
+ var IMAGE_TOKEN_COST = 1600;
9185
+ function modelSupportsImages(model) {
9186
+ const input = model?.input;
9187
+ return Array.isArray(input) && input.includes("image");
9188
+ }
9189
+ function collectImageTokens(entries, visionCapable) {
9190
+ const out = /* @__PURE__ */ new Map();
9191
+ if (!visionCapable) return out;
9192
+ for (const e of entries) {
9193
+ if (e.type !== "message") continue;
9194
+ const n = countImageBlocks(e.message?.content);
9195
+ if (n > 0) out.set(e.id, n * IMAGE_TOKEN_COST);
9196
+ }
9197
+ return out;
9198
+ }
9199
+ function estimateTokens(messages, coveredIds, imageTokensById) {
9176
9200
  let tokens = 0;
9177
9201
  for (const m of messages) {
9178
9202
  if (m.toolName === "compress") continue;
9179
9203
  if (coveredIds?.has(m.id)) continue;
9180
9204
  tokens += defaultCountTokens(m.text ?? "");
9205
+ const img = imageTokensById?.get(m.id);
9206
+ if (img) tokens += img;
9181
9207
  }
9182
9208
  return tokens;
9183
9209
  }
@@ -9306,12 +9332,13 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
9306
9332
  if (typeof maybeRanges === "string") throw new Error(maybeRanges);
9307
9333
  const ranges = maybeRanges;
9308
9334
  if (ranges.length === 0) return "No ranges provided.";
9309
- const { state: initialState, coreMessages } = await runtime.stateFor(ctx);
9335
+ const { state: initialState, coreMessages, entries } = await runtime.stateFor(ctx);
9310
9336
  const config = runtime.configFor(ctx);
9311
9337
  const modelId = ctx.model?.id ?? "default";
9312
9338
  const systemPromptText = getSystemPromptText(ctx);
9313
9339
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
9314
- const sentTokens = estimateTokens(coreMessages, collectCoveredMessageIds(initialState)) + systemPromptTokens;
9340
+ const imageTokens = collectImageTokens(entries, modelSupportsImages(ctx.model));
9341
+ const sentTokens = estimateTokens(coreMessages, collectCoveredMessageIds(initialState), imageTokens) + systemPromptTokens;
9315
9342
  const turn = runtime.core.processTurn({
9316
9343
  messages: coreMessages,
9317
9344
  state: initialState,
@@ -9321,7 +9348,7 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
9321
9348
  const state = turn.state;
9322
9349
  const messages = turn.messages;
9323
9350
  const density = runtime.density.densityFor(modelId);
9324
- const beforeTokens = calibrateTokens(estimateTokens(messages, collectCoveredMessageIds(state)), density);
9351
+ const beforeTokens = calibrateTokens(estimateTokens(messages, collectCoveredMessageIds(state), imageTokens), density);
9325
9352
  const summaryMaxChars = args.summaryMaxChars;
9326
9353
  const topLevelTopic = args.topic;
9327
9354
  debug.event("compress-in", {
@@ -9361,7 +9388,7 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
9361
9388
  config,
9362
9389
  tokenCount: calibrateTokens(sentTokens, density)
9363
9390
  });
9364
- const afterTokens = calibrateTokens(estimateTokens(afterTurn.messages, collectCoveredMessageIds(applied.state)), density);
9391
+ const afterTokens = calibrateTokens(estimateTokens(afterTurn.messages, collectCoveredMessageIds(applied.state), imageTokens), density);
9365
9392
  const reclaimed = Math.max(0, beforeTokens - afterTokens);
9366
9393
  const newBlocks = applied.state.blocks.slice(-blocksCreated);
9367
9394
  debug.event("compress-out", {
@@ -10414,30 +10441,63 @@ The delegate runs in its own clean pi process \u2014 it does NOT see this conver
10414
10441
  "Delegate to get a focused result in a clean context, or to parallelize independent work.",
10415
10442
  "The sub-agent has NO access to this conversation \u2014 write a fully self-contained task.",
10416
10443
  "Prefer async=true and launch several; results arrive back automatically when each finishes.",
10444
+ "A FAILED notification (\u26A0\uFE0F) means that task produced no usable result \u2014 decide whether to re-dispatch it before wrapping up.",
10417
10445
  "For changes you must apply yourself, delegate read-only investigation (reviewer/researcher/oracle) and keep the main context as the sole writer."
10418
10446
  ],
10419
10447
  parameters: DelegateParams,
10420
10448
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
10421
10449
  const args = params;
10422
- const outcome = await runDelegate(pi, args, ctx, signal);
10450
+ let outcome = await runDelegate(pi, args, ctx, signal);
10451
+ const notice = undeliveredNotice();
10452
+ if (notice) outcome = `${notice}
10453
+
10454
+ ${outcome}`;
10423
10455
  return { details: void 0, content: [{ type: "text", text: outcome }] };
10424
10456
  }
10425
10457
  };
10426
10458
  }
10427
10459
  function formatRunResult(run) {
10428
10460
  const timeoutNote = run.timedOut ? ` (timed out: ${run.timedOut})` : "";
10429
- 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)}`;
10461
+ 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 === "failed" ? "FAILED \u26A0\uFE0F" : run.status} (exit ${run.exitCode ?? "?"})${timeoutNote}${remainingLineForWait(run.runId)}`;
10430
10462
  return formatPayload(header, run.result?.file ?? "", run.task, run.result?.body);
10431
10463
  }
10432
10464
  function remainingLineForWait(selfRunId) {
10433
10465
  const remaining = Array.from(runs.values()).filter((r) => r.status === "running" && r.runId !== selfRunId).length;
10434
10466
  return remaining > 0 ? ` ${remaining} delegate${remaining === 1 ? " is" : "s are"} still running.` : "";
10435
10467
  }
10468
+ function findUndeliveredRuns(all, excludeRunId) {
10469
+ return all.filter(
10470
+ (r) => r.runId !== excludeRunId && (r.status === "completed" || r.status === "failed") && !r.waiter && !r.consumed && !r.injected
10471
+ );
10472
+ }
10473
+ function buildRecoveryNotice(all, excludeRunId) {
10474
+ const pending = findUndeliveredRuns(all, excludeRunId);
10475
+ if (pending.length === 0) return { text: "", covered: [] };
10476
+ const anyFailed = pending.some((r) => r.status === "failed");
10477
+ const header = `\u26A0\uFE0F Recovery notice: ${pending.length} earlier delegate result${pending.length === 1 ? "" : "s"} never reached you (notification delivery failed).${anyFailed ? " At least one FAILED \u2014 that task's work is missing; read its result below and decide whether to re-dispatch before concluding." : ""}`;
10478
+ return { text: [header, ...pending.map((r) => formatRunResult(r))].join("\n"), covered: pending };
10479
+ }
10480
+ function undeliveredNoticeFrom(all, excludeRunId) {
10481
+ const { text, covered } = buildRecoveryNotice(all, excludeRunId);
10482
+ for (const r of covered) r.injected = true;
10483
+ return text;
10484
+ }
10485
+ function undeliveredNotice(excludeRunId) {
10486
+ return undeliveredNoticeFrom(Array.from(runs.values()), excludeRunId);
10487
+ }
10488
+ function withUndeliveredNotice(result) {
10489
+ const notice = undeliveredNotice();
10490
+ const first = result.content[0];
10491
+ if (notice && first && first.type === "text") first.text = `${notice}
10492
+
10493
+ ${first.text}`;
10494
+ return result;
10495
+ }
10436
10496
  function injectedWaitMessage(run, runId, remainingLine) {
10437
10497
  if (!run.injected) return null;
10438
10498
  const file = run.result?.file;
10439
10499
  const fileLine = file ? ` If you need details, read the result file: \`${file}\`.` : "";
10440
- return `Delegate \`${runId}\` already delivered its result via a system notification when it finished \u2014 no need to wait on it again.${remainingLine}${fileLine}`;
10500
+ return `Delegate \`${runId}\` already delivered its result (system notification or recovery notice) when it finished \u2014 no need to wait on it again.${remainingLine}${fileLine}`;
10441
10501
  }
10442
10502
  function buildWaitResult(run, content, mode = "separate", contentType = "text") {
10443
10503
  if (run.usage && !run.usageReported) {
@@ -10472,6 +10532,61 @@ function buildCancelResult(run, content, mode = "separate") {
10472
10532
  return { details: void 0, content: [{ type: "text", text: content }] };
10473
10533
  }
10474
10534
  function makeDelegateWaitTool(_pi) {
10535
+ const exec = async (args, signal) => {
10536
+ const run = runs.get(args.runId);
10537
+ if (!run) {
10538
+ return { details: void 0, content: [{ type: "text", text: `No delegate run with runId \`${args.runId}\`. It may have already been reported or never existed.` }] };
10539
+ }
10540
+ const displayMode = delegateDisplayUsage;
10541
+ if (run.status === "cancelled") {
10542
+ run.consumed = true;
10543
+ return buildWaitResult(run, `Delegate \`${args.runId}\` was cancelled (no result).${remainingLineForWait(args.runId)}`, displayMode);
10544
+ }
10545
+ if (run.status !== "running") {
10546
+ const dedup = injectedWaitMessage(run, args.runId, remainingLineForWait(args.runId));
10547
+ if (dedup) {
10548
+ run.consumed = true;
10549
+ return buildWaitResult(run, dedup, displayMode);
10550
+ }
10551
+ run.consumed = true;
10552
+ if (!run.result) {
10553
+ return buildWaitResult(run, `Delegate \`${args.runId}\` finished but no result is available (persist error).`, displayMode);
10554
+ }
10555
+ return buildWaitResult(run, formatRunResult(run), displayMode);
10556
+ }
10557
+ const timeoutMs = resolveWaitTimeoutMs(args.timeout);
10558
+ if (run.waiter) {
10559
+ return { details: void 0, content: [{ type: "text", text: `Delegate \`${args.runId}\` already has a wait in progress; do not wait on it twice.` }] };
10560
+ }
10561
+ return new Promise((resolve3) => {
10562
+ let settled = false;
10563
+ let timer2;
10564
+ const finish2 = (result) => {
10565
+ if (settled) return;
10566
+ settled = true;
10567
+ run.waiter = void 0;
10568
+ if (timer2) clearTimeout(timer2);
10569
+ signal?.removeEventListener("abort", onAbort);
10570
+ resolve3(result);
10571
+ };
10572
+ const onAbort = () => {
10573
+ finish2({ details: void 0, content: [{ type: "text", text: `Aborted; delegate \`${args.runId}\` is still running in the background. A notification will be injected when it finishes.` }] });
10574
+ };
10575
+ run.waiter = () => {
10576
+ run.consumed = true;
10577
+ if (run.status === "cancelled") {
10578
+ finish2(buildWaitResult(run, `Delegate \`${run.runId}\` was cancelled (no result).${remainingLineForWait(run.runId)}`, displayMode));
10579
+ return;
10580
+ }
10581
+ finish2(buildWaitResult(run, formatRunResult(run), displayMode));
10582
+ };
10583
+ signal?.addEventListener("abort", onAbort);
10584
+ timer2 = setTimeout(
10585
+ () => finish2({ details: void 0, content: [{ type: "text", text: `Failed: delegate \`${args.runId}\` result not ready after ${Math.round(timeoutMs / 1e3)}s. Do NOT keep waiting or retry \u2014 go do other work now. The run continues in the background and a completion notification (with the result file path) will be injected into the chat when it finishes.` }] }),
10586
+ timeoutMs
10587
+ );
10588
+ });
10589
+ };
10475
10590
  return {
10476
10591
  name: "acp_delegate_wait",
10477
10592
  label: "ACP Delegate Wait",
@@ -10483,64 +10598,32 @@ function makeDelegateWaitTool(_pi) {
10483
10598
  ],
10484
10599
  parameters: WaitParams,
10485
10600
  async execute(_toolCallId, params, signal) {
10486
- const args = params;
10487
- const run = runs.get(args.runId);
10488
- if (!run) {
10489
- return { details: void 0, content: [{ type: "text", text: `No delegate run with runId \`${args.runId}\`. It may have already been reported or never existed.` }] };
10490
- }
10491
- const displayMode = delegateDisplayUsage;
10492
- if (run.status === "cancelled") {
10493
- run.consumed = true;
10494
- return buildWaitResult(run, `Delegate \`${args.runId}\` was cancelled (no result).${remainingLineForWait(args.runId)}`, displayMode);
10495
- }
10496
- if (run.status !== "running") {
10497
- const dedup = injectedWaitMessage(run, args.runId, remainingLineForWait(args.runId));
10498
- if (dedup) {
10499
- run.consumed = true;
10500
- return buildWaitResult(run, dedup, displayMode);
10501
- }
10502
- run.consumed = true;
10503
- if (!run.result) {
10504
- return buildWaitResult(run, `Delegate \`${args.runId}\` finished but no result is available (persist error).`, displayMode);
10505
- }
10506
- return buildWaitResult(run, formatRunResult(run), displayMode);
10507
- }
10508
- const timeoutMs = resolveWaitTimeoutMs(args.timeout);
10509
- if (run.waiter) {
10510
- return { details: void 0, content: [{ type: "text", text: `Delegate \`${args.runId}\` already has a wait in progress; do not wait on it twice.` }] };
10511
- }
10512
- return new Promise((resolve3) => {
10513
- let settled = false;
10514
- let timer2;
10515
- const finish2 = (result) => {
10516
- if (settled) return;
10517
- settled = true;
10518
- run.waiter = void 0;
10519
- if (timer2) clearTimeout(timer2);
10520
- signal?.removeEventListener("abort", onAbort);
10521
- resolve3(result);
10522
- };
10523
- const onAbort = () => {
10524
- finish2({ details: void 0, content: [{ type: "text", text: `Aborted; delegate \`${args.runId}\` is still running in the background. A notification will be injected when it finishes.` }] });
10525
- };
10526
- run.waiter = () => {
10527
- run.consumed = true;
10528
- if (run.status === "cancelled") {
10529
- finish2(buildWaitResult(run, `Delegate \`${run.runId}\` was cancelled (no result).${remainingLineForWait(run.runId)}`, displayMode));
10530
- return;
10531
- }
10532
- finish2(buildWaitResult(run, formatRunResult(run), displayMode));
10533
- };
10534
- signal?.addEventListener("abort", onAbort);
10535
- timer2 = setTimeout(
10536
- () => finish2({ details: void 0, content: [{ type: "text", text: `Failed: delegate \`${args.runId}\` result not ready after ${Math.round(timeoutMs / 1e3)}s. Do NOT keep waiting or retry \u2014 go do other work now. The run continues in the background and a completion notification (with the result file path) will be injected into the chat when it finishes.` }] }),
10537
- timeoutMs
10538
- );
10539
- });
10601
+ return withUndeliveredNotice(await exec(params, signal));
10540
10602
  }
10541
10603
  };
10542
10604
  }
10543
10605
  function makeDelegateCancelTool(_pi) {
10606
+ const exec = async (params) => {
10607
+ const { runId } = params;
10608
+ const run = runs.get(runId);
10609
+ if (!run) {
10610
+ return { details: void 0, content: [{ type: "text", text: `Unknown runId "${runId}".` }] };
10611
+ }
10612
+ if (run.status !== "running") {
10613
+ return buildCancelResult(run, `Run ${runId} already ${run.status} (no action).`);
10614
+ }
10615
+ run.status = "cancelled";
10616
+ run.consumed = true;
10617
+ try {
10618
+ run.child?.kill("SIGTERM");
10619
+ } catch (err) {
10620
+ debug.event("delegate-cancel-kill-error", { runId, error: String(err) });
10621
+ logError("delegate", { event: "cancel-kill-error", runId, error: String(err) });
10622
+ }
10623
+ delegateStatusWidget.poke();
10624
+ const displayMode = delegateDisplayUsage;
10625
+ return buildCancelResult(run, `Cancelled ${runId} (${run.agent}).`, displayMode);
10626
+ };
10544
10627
  return {
10545
10628
  name: "acp_delegate_cancel",
10546
10629
  label: "ACP Delegate Cancel",
@@ -10549,25 +10632,7 @@ function makeDelegateCancelTool(_pi) {
10549
10632
  promptGuidelines: [],
10550
10633
  parameters: CancelParams,
10551
10634
  async execute(toolCallId, params) {
10552
- const { runId } = params;
10553
- const run = runs.get(runId);
10554
- if (!run) {
10555
- return { details: void 0, content: [{ type: "text", text: `Unknown runId "${runId}".` }] };
10556
- }
10557
- if (run.status !== "running") {
10558
- return buildCancelResult(run, `Run ${runId} already ${run.status} (no action).`);
10559
- }
10560
- run.status = "cancelled";
10561
- run.consumed = true;
10562
- try {
10563
- run.child?.kill("SIGTERM");
10564
- } catch (err) {
10565
- debug.event("delegate-cancel-kill-error", { runId, error: String(err) });
10566
- logError("delegate", { event: "cancel-kill-error", runId, error: String(err) });
10567
- }
10568
- delegateStatusWidget.poke();
10569
- const displayMode = delegateDisplayUsage;
10570
- return buildCancelResult(run, `Cancelled ${runId} (${run.agent}).`, displayMode);
10635
+ return withUndeliveredNotice(await exec(params));
10571
10636
  }
10572
10637
  };
10573
10638
  }
@@ -10597,6 +10662,7 @@ async function runDelegate(pi, args, ctx, signal) {
10597
10662
  const runId = `del_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
10598
10663
  debug.event("delegate-spawn", { agent: args.agent, runId, cwd, async: isAsync, useJsonStream, cliArgs });
10599
10664
  logInfo("delegate", { event: "spawn", agent: args.agent, runId, cwd, async: isAsync, useJsonStream, mode: ctx.mode, parentDepth });
10665
+ await mkdir2(OUT_DIR, { recursive: true });
10600
10666
  const child = spawn(
10601
10667
  process.execPath,
10602
10668
  [resolvePiCliEntry(process.argv[1] ?? "", process.env, isPiHost(ctx.sessionManager)), ...cliArgs],
@@ -10628,7 +10694,6 @@ async function runDelegate(pi, args, ctx, signal) {
10628
10694
  );
10629
10695
  const replyFile = join6(OUT_DIR, `${runId}.out`);
10630
10696
  const activityFile = join6(OUT_DIR, `${runId}.activity`);
10631
- await mkdir2(OUT_DIR, { recursive: true });
10632
10697
  const replyStream = createWriteStream(replyFile, { flags: "a" });
10633
10698
  const activityStream = useJsonStream ? createWriteStream(activityFile, { flags: "a" }) : null;
10634
10699
  const endStream = (s) => new Promise((resolve3) => {
@@ -10721,7 +10786,7 @@ async function runDelegate(pi, args, ctx, signal) {
10721
10786
  return;
10722
10787
  }
10723
10788
  const mode = delegateDisplayUsage;
10724
- const injected = injectResult(pi, args.agent, runId, args.task, code, file2, run.timedOut, run.usage, mode, run.usageReported);
10789
+ const injected = injectResult(pi, args.agent, runId, args.task, code, file2, run.timedOut, run.usage, mode, run.usageReported, run.status === "failed" ? body2 : void 0);
10725
10790
  if (run.usage && !run.usageReported && (mode === "separate" || injected)) {
10726
10791
  run.usageReported = true;
10727
10792
  }
@@ -10734,7 +10799,7 @@ async function runDelegate(pi, args, ctx, signal) {
10734
10799
  run.finishedAt = Date.now();
10735
10800
  debug.event("delegate-done-error", { runId, error: String(err) });
10736
10801
  logError("delegate", { event: "done-error", runId, agent: args.agent, error: String(err) });
10737
- run.waiter?.();
10802
+ notifyTerminalFailure(pi, run, `result persistence error: ${String(err)}`);
10738
10803
  delegateStatusWidget.poke();
10739
10804
  }
10740
10805
  })();
@@ -10755,7 +10820,8 @@ async function runDelegate(pi, args, ctx, signal) {
10755
10820
  run.result = { code: null, file: "", body: `spawn error: ${String(err)}` };
10756
10821
  debug.event("delegate-spawn-error", { runId, error: String(err) });
10757
10822
  logError("delegate", { event: "spawn-error", runId, agent: args.agent, error: String(err) });
10758
- run.waiter?.();
10823
+ if (run.status === "failed") notifyTerminalFailure(pi, run, `spawn error: ${String(err)}`);
10824
+ else run.waiter?.();
10759
10825
  delegateStatusWidget.poke();
10760
10826
  }
10761
10827
  });
@@ -10837,7 +10903,7 @@ function waitForChild(child, signal) {
10837
10903
  });
10838
10904
  }
10839
10905
  function formatSyncResult(agent, runId, task, r, file) {
10840
- const status = r.timedOut ? "timed out" : r.code === 0 ? "completed" : "failed";
10906
+ const status = r.timedOut ? "timed out" : r.code === 0 ? "completed" : "FAILED \u26A0\uFE0F";
10841
10907
  const header = `Delegate **${agent}** ${status} (runId \`${runId}\`, exit ${r.code ?? "?"}).`;
10842
10908
  if (r.code === 0 && !r.timedOut) {
10843
10909
  return formatPayload(header, file, task);
@@ -10845,14 +10911,15 @@ function formatSyncResult(agent, runId, task, r, file) {
10845
10911
  const body = r.timedOut ? "(timed out)" : r.stderr.trim() || "(no stderr)";
10846
10912
  return formatPayload(header, file, task, body);
10847
10913
  }
10848
- function injectResult(pi, agent, runId, task, code, file, timedOut, usage, mode = "separate", usageAlreadyReported) {
10914
+ function injectResult(pi, agent, runId, task, code, file, timedOut, usage, mode = "separate", usageAlreadyReported, body) {
10849
10915
  const send = pi.sendUserMessage;
10850
10916
  if (typeof send !== "function") {
10851
10917
  debug.event("delegate-inject-skipped", { runId, reason: "sendUserMessage unavailable" });
10852
10918
  logWarn("delegate", { event: "inject-skipped", runId, reason: "sendUserMessage unavailable" });
10853
10919
  return false;
10854
10920
  }
10855
- const status = code === 0 ? "completed" : "failed";
10921
+ const failed = code !== 0;
10922
+ const status = failed ? "FAILED \u26A0\uFE0F" : "completed";
10856
10923
  const remaining = Array.from(runs.values()).filter((r) => r.status === "running").length;
10857
10924
  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.";
10858
10925
  const timeoutNote = timedOut ? ` (timed out: ${timedOut})` : "";
@@ -10886,10 +10953,15 @@ Tokens: ${totalUsage.input.toLocaleString()} in, ${totalUsage.output.toLocaleStr
10886
10953
  }
10887
10954
  if (lines.length) usageNote = ` Usage: ${lines.join(", ")}.`;
10888
10955
  }
10889
- const header = `[acp_delegate ${status}] **${agent}** (runId \`${runId}\`, exit ${code ?? "?"})${timeoutNote}${remainingLine}${usageNote} 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.`;
10890
- const text = formatPayload(header, file, task);
10956
+ const closing = failed ? "This delegate did NOT complete its task \u2014 its result is missing from your work. Read the error excerpt (and the result file if present), then decide whether to re-dispatch the task before wrapping up. This is an automated system notification, NOT a user message." : "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.";
10957
+ const header = `[acp_delegate ${status}] **${agent}** (runId \`${runId}\`, exit ${code ?? "?"})${timeoutNote}${remainingLine}${usageNote} ${closing}`;
10958
+ const { text: recoveryText, covered } = buildRecoveryNotice(Array.from(runs.values()), runId);
10959
+ const text = formatPayload(header, file, task, failed ? body : void 0) + (recoveryText ? `
10960
+
10961
+ ${recoveryText}` : "");
10891
10962
  try {
10892
10963
  send.call(pi, text, { deliverAs: "followUp" });
10964
+ for (const r of covered) r.injected = true;
10893
10965
  return true;
10894
10966
  } catch (err) {
10895
10967
  debug.event("delegate-inject-error", { runId, error: String(err) });
@@ -10897,6 +10969,29 @@ Tokens: ${totalUsage.input.toLocaleString()} in, ${totalUsage.output.toLocaleStr
10897
10969
  return false;
10898
10970
  }
10899
10971
  }
10972
+ function notifyTerminalFailure(pi, run, body) {
10973
+ const hadWaiter = run.waiter !== void 0;
10974
+ run.waiter?.();
10975
+ if (hadWaiter || run.consumed) return;
10976
+ const mode = delegateDisplayUsage;
10977
+ const injected = injectResult(
10978
+ pi,
10979
+ run.agent,
10980
+ run.runId,
10981
+ run.task,
10982
+ run.result?.code ?? null,
10983
+ run.result?.file ?? "",
10984
+ run.timedOut,
10985
+ run.usage,
10986
+ mode,
10987
+ run.usageReported,
10988
+ body
10989
+ );
10990
+ if (run.usage && !run.usageReported && (mode === "separate" || injected)) {
10991
+ run.usageReported = true;
10992
+ }
10993
+ run.injected = injected;
10994
+ }
10900
10995
  function formatPayload(header, file, task, body) {
10901
10996
  const lines = [header, "", `Task: ${truncate2(task, 160)}`];
10902
10997
  if (file) {
@@ -10970,13 +11065,13 @@ function makeStatusTool(runtime) {
10970
11065
  };
10971
11066
  }
10972
11067
  async function handleStatus(args, runtime, ctx) {
10973
- const { state, coreMessages } = await runtime.stateFor(ctx);
11068
+ const { state, coreMessages, entries } = await runtime.stateFor(ctx);
10974
11069
  const config = runtime.configFor(ctx);
10975
11070
  const coveredIds = collectCoveredMessageIds(state);
10976
11071
  const modelId = ctx.model?.id ?? "default";
10977
11072
  const systemPromptText = getSystemPromptText(ctx);
10978
11073
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10979
- const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
11074
+ const sentTokens = estimateTokens(coreMessages, coveredIds, collectImageTokens(entries, modelSupportsImages(ctx.model))) + systemPromptTokens;
10980
11075
  const turn = runtime.core.processTurn({
10981
11076
  messages: coreMessages,
10982
11077
  state,
@@ -11460,12 +11555,14 @@ async function statusReport(runtime, ctx) {
11460
11555
  const realUsage = ctx.getContextUsage?.();
11461
11556
  const systemPromptText = getSystemPromptText(ctx);
11462
11557
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
11463
- const sessionTokens = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n"));
11558
+ const imageTokens = collectImageTokens(entries, modelSupportsImages(ctx.model));
11559
+ const imageTokensTotal = [...imageTokens.values()].reduce((a, b) => a + b, 0);
11560
+ const sessionTokens = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n")) + imageTokensTotal;
11464
11561
  const coveredIds = collectCoveredMessageIds(state);
11465
11562
  const modelId = ctx.model?.id ?? "default";
11466
- const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
11563
+ const sentTokens = estimateTokens(coreMessages, coveredIds, imageTokens) + systemPromptTokens;
11467
11564
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId)) });
11468
- const versionStr = "0.1.48" ? `billion-context-pi@${"0.1.48"}` : void 0;
11565
+ const versionStr = "0.1.49" ? `billion-context-pi@${"0.1.49"}` : void 0;
11469
11566
  let text = buildStatusPanel({
11470
11567
  version: versionStr,
11471
11568
  tokenCount: sessionTokens,
@@ -11473,7 +11570,7 @@ async function statusReport(runtime, ctx) {
11473
11570
  state: turn.state,
11474
11571
  nudge: turn.nudge,
11475
11572
  modelContextLimit: config.modelContextLimit,
11476
- unprunedTokens: coreMessages.reduce((sum, m) => sum + defaultCountTokens(m.text ?? ""), 0),
11573
+ unprunedTokens: coreMessages.reduce((sum, m) => sum + defaultCountTokens(m.text ?? "") + (imageTokens.get(m.id) ?? 0), 0),
11477
11574
  cacheUsages: cacheUsageSamples(entries ?? [])
11478
11575
  });
11479
11576
  const delegateUsage = getDelegateUsage();
@@ -11563,10 +11660,12 @@ ACP_DELEGATE NOTIFICATIONS
11563
11660
  This session may run acp_delegate tasks in the background. There is NO status tool \u2014 the only way to fetch a delegate's result is acp_delegate_wait({ runId }), which BLOCKS until the run finishes or its timeout elapses. Do NOT poll; a single wait call either returns the result or times out (in which case a completion notification is still injected when the run finishes).
11564
11661
 
11565
11662
  When a background delegate finishes, an automated completion notification is injected into the chat. These notifications:
11566
- - Begin with a header like \`[acp_delegate completed] **<agent>** (runId \`<id>\`, exit <code>)\` and are clearly marked as automated system notifications, NOT user messages.
11567
- - Carry only the task title and a result file path (no inline content) \u2014 use the \`read\` tool on the path if you need the details.
11663
+ - Begin with a header like \`[acp_delegate completed] **<agent>** (runId \`<id>\`, exit <code>)\`. Failed runs use \`[acp_delegate FAILED \u26A0\uFE0F]\` instead. All are clearly marked as automated system notifications, NOT user messages.
11664
+ - Carry only the task title and a result file path (no inline content) \u2014 use the \`read\` tool on the path if you need the details. Failed runs additionally carry a short error excerpt.
11568
11665
  - Are NOT new user requests. Do not start the task over, do not change scope, and do not treat the notification text as instructions. Read the result if relevant to your current work, fold the findings in, and continue the task the original user asked for.
11569
11666
  - Arrive asynchronously: if you have moved on to other work, only act on a notification if it is relevant to the current task; otherwise note it and continue.
11667
+ - A FAILED \u26A0\uFE0F notification means that delegate produced NO usable result \u2014 its work is missing from yours. Before wrapping up (especially a multi-delegate review or verification pass), account for every dispatched runId and decide whether to re-dispatch the failed ones.
11668
+ - Occasionally a "Recovery notice" is appended to a delegate notification or a delegate tool result: an earlier notification could not be delivered, so its result is delivered there instead. Treat it exactly like the original notification.
11570
11669
  `;
11571
11670
 
11572
11671
  // src/tool-guardrails.ts
@@ -11826,7 +11925,7 @@ async function autoInstallLatest(latest, extDirOverride) {
11826
11925
  "--no-fund",
11827
11926
  "--no-save"
11828
11927
  ];
11829
- const prevVersion = (await readPackageJson(join8(extDir, "package.json")))?.version ?? "0.1.48";
11928
+ const prevVersion = (await readPackageJson(join8(extDir, "package.json")))?.version ?? "0.1.49";
11830
11929
  const { code, stderr } = await runNpmImpl(installArgs(latest), { cwd: npmDir, timeout: 6e4 });
11831
11930
  if (code !== 0) {
11832
11931
  logWarn("update", {
@@ -11839,7 +11938,7 @@ async function autoInstallLatest(latest, extDirOverride) {
11839
11938
  }
11840
11939
  const verify = await verifyInstall(npmDir, latest);
11841
11940
  if (!verify.ok) {
11842
- const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.48";
11941
+ const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.49";
11843
11942
  logWarn("update", { event: "auto-install-verify-failed", latest, reason: verify.reason, rollbackTo });
11844
11943
  const rb = await runNpmImpl(installArgs(rollbackTo), { cwd: npmDir, timeout: 6e4 });
11845
11944
  logInfo("update", { event: "rollback", from: latest, to: rollbackTo, ok: rb.code === 0 });
@@ -11900,7 +11999,7 @@ async function checkForUpdate(autoUpdate, notify) {
11900
11999
  const runtimeVersion = await getRuntimeVersion();
11901
12000
  const latest = await fetchLatestVersion();
11902
12001
  if (!latest) return;
11903
- const current = runtimeVersion ?? "0.1.48";
12002
+ const current = runtimeVersion ?? "0.1.49";
11904
12003
  const hasUpdate = isNewer(latest, current);
11905
12004
  debug.event("update-check", {
11906
12005
  current,
@@ -11979,7 +12078,7 @@ function wireSessionLifecycle(pi, runtime) {
11979
12078
  const sid = ctx.sessionManager.getSessionId();
11980
12079
  runtime.clearSessionTracking(sid);
11981
12080
  const modelInfo = ctx.model;
11982
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.48" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
12081
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.49" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
11983
12082
  try {
11984
12083
  await runtime.reloadConfig(ctx.cwd);
11985
12084
  setDelegateDisplayUsage(resolveDelegate(runtime.adapter).displayUsage);
@@ -12038,7 +12137,7 @@ function wireContextTransform(pi, runtime) {
12038
12137
  const realUsage = ctx.getContextUsage?.();
12039
12138
  const systemPromptText = getSystemPromptText(ctx);
12040
12139
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
12041
- const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
12140
+ const sentTokens = estimateTokens(coreMessages, coveredIds, collectImageTokens(entries, modelSupportsImages(ctx.model))) + systemPromptTokens;
12042
12141
  let tokenCount = calibrateTokens(sentTokens, runtime.density.densityFor(modelId));
12043
12142
  if (ov.armed && config.modelContextLimit > 0) {
12044
12143
  ov.armed = false;