billion-context-pi 0.1.43 → 0.1.45

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/dist/index.js CHANGED
@@ -580,7 +580,7 @@ function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, opt
580
580
  );
581
581
  return { messages: updated, truncatedCount, savedTokens };
582
582
  }
583
- var KEEP_LAST_ORPHANED = 0;
583
+ var KEEP_LAST_ORPHANED = 2;
584
584
  function rangeKey(startRef, endRef) {
585
585
  return `${startRef}::${endRef}`;
586
586
  }
@@ -3604,7 +3604,7 @@ async function abortableSleep(ms, signal) {
3604
3604
  if (signal.aborted) return "aborted";
3605
3605
  const remaining = end - Date.now();
3606
3606
  if (remaining <= 0) return "ok";
3607
- await new Promise((resolve2) => setTimeout(resolve2, Math.min(250, remaining)));
3607
+ await new Promise((resolve3) => setTimeout(resolve3, Math.min(250, remaining)));
3608
3608
  }
3609
3609
  }
3610
3610
 
@@ -3930,17 +3930,20 @@ function createRuntime(adapter) {
3930
3930
  for (const o of outcomes) {
3931
3931
  if (compressOutcomeSeen.has(o.toolCallId)) continue;
3932
3932
  compressOutcomeSeen.add(o.toolCallId);
3933
- if (o.isError) {
3933
+ if (o.isError || o.noop === true) {
3934
3934
  compressFailCount += 1;
3935
3935
  } else if (o.success) {
3936
3936
  compressFailCount = 0;
3937
3937
  }
3938
3938
  }
3939
3939
  const latest = outcomes.length > 0 ? outcomes[outcomes.length - 1] : void 0;
3940
- const retryFor = latest && latest.isError && compressFailCount >= 1 && compressFailCount < MAX_COMPRESS_ATTEMPTS ? latest.toolCallId : null;
3940
+ const retryFor = latest && (latest.isError || latest.noop === true) && compressFailCount >= 1 && compressFailCount < MAX_COMPRESS_ATTEMPTS ? latest.toolCallId : null;
3941
3941
  const cappedNow = compressFailCount >= MAX_COMPRESS_ATTEMPTS && prevCount < MAX_COMPRESS_ATTEMPTS;
3942
3942
  return { count: compressFailCount, retryFor, cappedNow };
3943
3943
  }
3944
+ function compressRetryCappedFor(turnKey) {
3945
+ return compressFailTurnKey === turnKey && compressFailCount >= MAX_COMPRESS_ATTEMPTS;
3946
+ }
3944
3947
  function clearCompressRetryTracking() {
3945
3948
  compressOutcomeSeen.clear();
3946
3949
  compressFailTurnKey = null;
@@ -3949,10 +3952,10 @@ function createRuntime(adapter) {
3949
3952
  async function acquireLock(sid) {
3950
3953
  const prev = locks.get(sid) ?? Promise.resolve();
3951
3954
  let release;
3952
- const next = new Promise((resolve2) => {
3955
+ const next = new Promise((resolve3) => {
3953
3956
  release = () => {
3954
3957
  locks.delete(sid);
3955
- resolve2();
3958
+ resolve3();
3956
3959
  };
3957
3960
  });
3958
3961
  locks.set(sid, prev.then(() => next));
@@ -4033,7 +4036,7 @@ function createRuntime(adapter) {
4033
4036
  nudgeShownTurns.add(k);
4034
4037
  }, nudgeShownFor: (k) => nudgeShownTurns.has(k), clearNudgeTracking: () => {
4035
4038
  nudgeShownTurns.clear();
4036
- }, noteCompressOutcomes, clearCompressRetryTracking, liveContextLimit, configFor, reloadConfig, stateFor, save, acquireLock, overflowFor, overflowDrop, throttleFor, throttleDrop };
4039
+ }, noteCompressOutcomes, compressRetryCappedFor, clearCompressRetryTracking, liveContextLimit, configFor, reloadConfig, stateFor, save, acquireLock, overflowFor, overflowDrop, throttleFor, throttleDrop };
4037
4040
  }
4038
4041
 
4039
4042
  // node_modules/typebox/build/system/memory/memory.mjs
@@ -8545,8 +8548,16 @@ function normalizeRanges(content) {
8545
8548
  }
8546
8549
  return ranges;
8547
8550
  }
8551
+ function compressPanelBlocks(text) {
8552
+ if (!text.trimStart().startsWith("\u25A3 ACP |")) return -1;
8553
+ const m = text.match(/, (\d+) blocks?\)/);
8554
+ return m ? Number(m[1]) : -1;
8555
+ }
8548
8556
  function isCompressSuccessText(text) {
8549
- return text.trimStart().startsWith("\u25A3 ACP |");
8557
+ return compressPanelBlocks(text) > 0;
8558
+ }
8559
+ function isCompressNoopText(text) {
8560
+ return compressPanelBlocks(text) === 0;
8550
8561
  }
8551
8562
  function tier3OnlyRewrite(newBlocks, allBlocks) {
8552
8563
  if (newBlocks.length === 0) return null;
@@ -9874,7 +9885,7 @@ function makeDelegateWaitTool(_pi) {
9874
9885
  if (run.waiter) {
9875
9886
  return { details: void 0, content: [{ type: "text", text: `Delegate \`${args.runId}\` already has a wait in progress; do not wait on it twice.` }] };
9876
9887
  }
9877
- return new Promise((resolve2) => {
9888
+ return new Promise((resolve3) => {
9878
9889
  let settled = false;
9879
9890
  let timer2;
9880
9891
  const finish = (result) => {
@@ -9883,7 +9894,7 @@ function makeDelegateWaitTool(_pi) {
9883
9894
  run.waiter = void 0;
9884
9895
  if (timer2) clearTimeout(timer2);
9885
9896
  signal?.removeEventListener("abort", onAbort);
9886
- resolve2(result);
9897
+ resolve3(result);
9887
9898
  };
9888
9899
  const onAbort = () => {
9889
9900
  finish({ 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.` }] });
@@ -9996,9 +10007,9 @@ async function runDelegate(pi, args, ctx, signal) {
9996
10007
  await mkdir2(OUT_DIR, { recursive: true });
9997
10008
  const replyStream = createWriteStream(replyFile, { flags: "a" });
9998
10009
  const activityStream = useJsonStream ? createWriteStream(activityFile, { flags: "a" }) : null;
9999
- const endStream = (s) => new Promise((resolve2) => {
10000
- if (!s || s.destroyed || s.closed) return resolve2();
10001
- s.end(() => resolve2());
10010
+ const endStream = (s) => new Promise((resolve3) => {
10011
+ if (!s || s.destroyed || s.closed) return resolve3();
10012
+ s.end(() => resolve3());
10002
10013
  });
10003
10014
  let stdoutBuf = "";
10004
10015
  const applier = makeEventApplier(
@@ -10167,7 +10178,7 @@ Complete the task below.`, "utf8");
10167
10178
  return { cliArgs, tmpDir, isAsync, useJsonStream };
10168
10179
  }
10169
10180
  function waitForChild(child, signal) {
10170
- return new Promise((resolve2) => {
10181
+ return new Promise((resolve3) => {
10171
10182
  const stdoutChunks = [];
10172
10183
  let stderrText = "";
10173
10184
  child.stdout?.on("data", (c) => stdoutChunks.push(c));
@@ -10186,7 +10197,7 @@ function waitForChild(child, signal) {
10186
10197
  function finish(r) {
10187
10198
  clearTimeout(timer2);
10188
10199
  signal?.removeEventListener("abort", onAbort);
10189
- resolve2(r);
10200
+ resolve3(r);
10190
10201
  }
10191
10202
  child.on("close", (code) => {
10192
10203
  finish({
@@ -10389,6 +10400,199 @@ async function handleStatus(args, runtime, ctx) {
10389
10400
  ${extra.join("\n")}` : base;
10390
10401
  }
10391
10402
 
10403
+ // src/setup-subagent-tools.ts
10404
+ import * as fs3 from "fs";
10405
+ import * as os from "os";
10406
+ import * as path4 from "path";
10407
+ import { CONFIG_DIR_NAME as CONFIG_DIR_NAME3 } from "@earendil-works/pi-coding-agent";
10408
+ var ACP_TOOLS2 = ["compress", "decompress", "search_context", "acp_status"];
10409
+ function resolveAgentDir() {
10410
+ const envDir = process.env.PI_CODING_AGENT_DIR;
10411
+ if (envDir) {
10412
+ if (envDir === "~") return os.homedir();
10413
+ if (envDir.startsWith("~/")) return path4.join(os.homedir(), envDir.slice(2));
10414
+ return envDir;
10415
+ }
10416
+ return path4.join(os.homedir(), CONFIG_DIR_NAME3, "agent");
10417
+ }
10418
+ function parseFrontmatterTools(content) {
10419
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
10420
+ if (!match) return null;
10421
+ const body = match[1];
10422
+ if (!body) return null;
10423
+ let name;
10424
+ let tools;
10425
+ for (const rawLine of body.split(/\r?\n/)) {
10426
+ const line = rawLine.trim();
10427
+ if (line.startsWith("name:")) {
10428
+ name = line.slice(5).trim().replace(/^["']|["']$/g, "");
10429
+ } else if (line.startsWith("tools:")) {
10430
+ const value = line.slice(6).trim();
10431
+ if (value) {
10432
+ tools = value.split(",").map((t) => t.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
10433
+ }
10434
+ }
10435
+ }
10436
+ if (!name) return null;
10437
+ return tools ? { name, tools } : { name };
10438
+ }
10439
+ function findPiSubagentsInstall(agentDir, cwd) {
10440
+ const candidates = [
10441
+ path4.join(agentDir, "npm", "node_modules", "pi-subagents"),
10442
+ path4.join(cwd, CONFIG_DIR_NAME3, "npm", "node_modules", "pi-subagents")
10443
+ ];
10444
+ const extensionRoots = [
10445
+ path4.join(agentDir, "extensions"),
10446
+ path4.join(cwd, CONFIG_DIR_NAME3, "extensions")
10447
+ ];
10448
+ for (const dir of candidates) {
10449
+ if (fs3.existsSync(path4.join(dir, "package.json"))) return dir;
10450
+ }
10451
+ for (const root of extensionRoots) {
10452
+ let entries;
10453
+ try {
10454
+ entries = fs3.readdirSync(root, { withFileTypes: true });
10455
+ } catch {
10456
+ continue;
10457
+ }
10458
+ for (const entry of entries) {
10459
+ if (!entry.isDirectory()) continue;
10460
+ const pkgPath = path4.join(root, entry.name, "package.json");
10461
+ try {
10462
+ if (JSON.parse(fs3.readFileSync(pkgPath, "utf-8")).name === "pi-subagents") {
10463
+ return path4.join(root, entry.name);
10464
+ }
10465
+ } catch {
10466
+ }
10467
+ }
10468
+ }
10469
+ return null;
10470
+ }
10471
+ function discoverBuiltinAgents(installDir) {
10472
+ const agentsDir = path4.join(installDir, "agents");
10473
+ let entries;
10474
+ try {
10475
+ entries = fs3.readdirSync(agentsDir, { withFileTypes: true });
10476
+ } catch {
10477
+ return [];
10478
+ }
10479
+ const parsed = [];
10480
+ for (const entry of entries) {
10481
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
10482
+ try {
10483
+ const result = parseFrontmatterTools(fs3.readFileSync(path4.join(agentsDir, entry.name), "utf-8"));
10484
+ if (result) parsed.push(result);
10485
+ } catch {
10486
+ }
10487
+ }
10488
+ return parsed;
10489
+ }
10490
+ function desiredTools(baseTools) {
10491
+ const tools = baseTools ? [...baseTools] : [];
10492
+ for (const tool of ACP_TOOLS2) {
10493
+ if (!tools.includes(tool)) tools.push(tool);
10494
+ }
10495
+ return tools;
10496
+ }
10497
+ function ensureSubagentAcpTools(settingsPath, options) {
10498
+ const agentDir = options?.agentDir ?? resolveAgentDir();
10499
+ const cwd = options?.cwd ?? process.cwd();
10500
+ const path_ = settingsPath ?? path4.join(agentDir, "settings.json");
10501
+ let installDir;
10502
+ if (options?.installDir) {
10503
+ installDir = path4.resolve(options.installDir);
10504
+ if (!fs3.existsSync(path4.join(installDir, "package.json"))) {
10505
+ return { path: path_, action: "failed", reason: `not a package: ${installDir}` };
10506
+ }
10507
+ } else {
10508
+ const detected = findPiSubagentsInstall(agentDir, cwd);
10509
+ if (!detected) {
10510
+ return { path: path_, action: "skipped", reason: "pi-subagents not installed" };
10511
+ }
10512
+ installDir = detected;
10513
+ }
10514
+ const builtins = discoverBuiltinAgents(installDir);
10515
+ if (builtins.length === 0) {
10516
+ return { path: path_, action: "skipped", reason: "pi-subagents ships no agents/*.md" };
10517
+ }
10518
+ let settingsRaw;
10519
+ try {
10520
+ settingsRaw = fs3.readFileSync(path_, "utf-8");
10521
+ } catch {
10522
+ return { path: path_, action: "skipped", reason: "not found" };
10523
+ }
10524
+ let settings2;
10525
+ try {
10526
+ settings2 = JSON.parse(settingsRaw);
10527
+ if (typeof settings2 !== "object" || settings2 === null || Array.isArray(settings2)) {
10528
+ return { path: path_, action: "failed", reason: "settings.json root is not an object" };
10529
+ }
10530
+ } catch {
10531
+ return { path: path_, action: "failed", reason: "settings.json is not valid JSON" };
10532
+ }
10533
+ const subagents = typeof settings2.subagents === "object" && settings2.subagents !== null ? settings2.subagents : {};
10534
+ const existingOverrides = typeof subagents.agentOverrides === "object" && subagents.agentOverrides !== null ? subagents.agentOverrides : {};
10535
+ let changed = false;
10536
+ const overrides = {};
10537
+ for (const [name, existing] of Object.entries(existingOverrides)) overrides[name] = existing ?? {};
10538
+ const frontmatterByName = new Map(builtins.map((b) => [b.name, b.tools]));
10539
+ for (const name of builtins.map((b) => b.name)) {
10540
+ const existing = overrides[name];
10541
+ const baseTools = existing?.tools && Array.isArray(existing.tools) && existing.tools.length > 0 ? existing.tools : frontmatterByName.get(name);
10542
+ if (baseTools === void 0) continue;
10543
+ const wanted = desiredTools(baseTools);
10544
+ const current = existing?.tools;
10545
+ if (Array.isArray(current) && current.length > 0 && wanted.every((tool) => current.includes(tool))) {
10546
+ continue;
10547
+ }
10548
+ overrides[name] = { ...existing, tools: wanted };
10549
+ changed = true;
10550
+ }
10551
+ if (!changed) {
10552
+ return { path: path_, action: "skipped", reason: "already have ACP tools" };
10553
+ }
10554
+ subagents.agentOverrides = overrides;
10555
+ settings2.subagents = subagents;
10556
+ const backupPath = `${path_}.acp-bak`;
10557
+ try {
10558
+ if (!fs3.existsSync(backupPath)) fs3.copyFileSync(path_, backupPath);
10559
+ } catch (err) {
10560
+ const message = err instanceof Error ? err.message : String(err);
10561
+ return { path: path_, action: "failed", reason: `backup failed: ${message}` };
10562
+ }
10563
+ const expectedMtimeMs = fs3.statSync(path_).mtimeMs;
10564
+ const tmpPath = `${path_}.tmp-${process.pid}`;
10565
+ try {
10566
+ fs3.writeFileSync(tmpPath, JSON.stringify(settings2, null, 2) + "\n", "utf-8");
10567
+ if (fs3.statSync(path_).mtimeMs !== expectedMtimeMs) {
10568
+ fs3.unlinkSync(tmpPath);
10569
+ return { path: path_, action: "failed", reason: "settings.json changed during write" };
10570
+ }
10571
+ fs3.renameSync(tmpPath, path_);
10572
+ const written = JSON.parse(fs3.readFileSync(path_, "utf-8"));
10573
+ const writtenSub = written.subagents;
10574
+ const writtenOverrides = writtenSub?.agentOverrides ?? {};
10575
+ for (const b of builtins) {
10576
+ const entry = writtenOverrides[b.name];
10577
+ const tools = entry?.tools ?? [];
10578
+ if (frontmatterByName.get(b.name) !== void 0 && !ACP_TOOLS2.every((t) => tools.includes(t))) {
10579
+ fs3.copyFileSync(backupPath, path_);
10580
+ return { path: path_, action: "failed", reason: "post-write verification failed" };
10581
+ }
10582
+ }
10583
+ return { path: path_, action: "updated" };
10584
+ } catch (err) {
10585
+ const message = err instanceof Error ? err.message : String(err);
10586
+ if (fs3.existsSync(backupPath)) {
10587
+ try {
10588
+ fs3.copyFileSync(backupPath, path_);
10589
+ } catch {
10590
+ }
10591
+ }
10592
+ return { path: path_, action: "failed", reason: message };
10593
+ }
10594
+ }
10595
+
10392
10596
  // src/commands.ts
10393
10597
  function makeCommands(runtime) {
10394
10598
  return [
@@ -10453,6 +10657,25 @@ ${text}`);
10453
10657
  ctx.ui.notify(lines.join("\n"));
10454
10658
  }
10455
10659
  }
10660
+ },
10661
+ {
10662
+ name: "acp-subagents",
10663
+ options: {
10664
+ description: "Add ACP context tools (compress/decompress/search_context/acp_status) to pi-subagents' builtin agents. One-time setup \u2014 re-run after upgrading pi-subagents. Usage: /acp-subagents [installDir]",
10665
+ handler: async (args, ctx) => {
10666
+ const installDir = args.trim();
10667
+ const result = ensureSubagentAcpTools(void 0, installDir ? { installDir } : void 0);
10668
+ if (result.action === "updated") {
10669
+ ctx.ui.notify(`ACP tools enabled for pi-subagents agents in ${result.path}`);
10670
+ } else if (result.action === "skipped") {
10671
+ ctx.ui.notify(
10672
+ `Nothing to do: ${result.reason ?? ""}. Install pi-subagents (pi install npm:pi-subagents) or pass its directory: /acp-subagents <installDir>`
10673
+ );
10674
+ } else {
10675
+ ctx.ui.notify(`Failed to update ${result.path}: ${result.reason ?? "unknown"}`);
10676
+ }
10677
+ }
10678
+ }
10456
10679
  }
10457
10680
  ];
10458
10681
  }
@@ -10467,7 +10690,7 @@ async function statusReport(runtime, ctx) {
10467
10690
  const modelId = ctx.model?.id ?? "default";
10468
10691
  const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
10469
10692
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId)) });
10470
- const versionStr = "0.1.43" ? `billion-context-pi@${"0.1.43"}` : void 0;
10693
+ const versionStr = "0.1.45" ? `billion-context-pi@${"0.1.45"}` : void 0;
10471
10694
  let text = buildStatusPanel({
10472
10695
  version: versionStr,
10473
10696
  tokenCount: sessionTokens,
@@ -10685,18 +10908,48 @@ function wireToolGuardrails(pi, runtime) {
10685
10908
  }
10686
10909
 
10687
10910
  // src/update.ts
10688
- import { readFile, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
10689
- import { join as join7, dirname as dirname5 } from "path";
10911
+ import { readFile, writeFile as writeFile3, mkdir as mkdir3, access } from "fs/promises";
10912
+ import { join as join8, dirname as dirname5 } from "path";
10690
10913
  import { fileURLToPath } from "url";
10691
10914
  import { execFile } from "child_process";
10692
- import { homedir as homedir4 } from "os";
10693
- import { CONFIG_DIR_NAME as CONFIG_DIR_NAME3 } from "@earendil-works/pi-coding-agent";
10915
+ import { homedir as homedir5 } from "os";
10916
+ import { CONFIG_DIR_NAME as CONFIG_DIR_NAME4 } from "@earendil-works/pi-coding-agent";
10694
10917
  var PACKAGE_NAME = "billion-context-pi";
10695
10918
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
10696
10919
  var SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/;
10697
10920
  var CHECK_INTERVAL_MS = 3 * 60 * 1e3;
10698
- var THROTTLE_FILE = join7(homedir4(), CONFIG_DIR_NAME3, "agent", ".billion-context-pi-update-check");
10921
+ var throttleFile = () => process.env.ACP_UPDATE_THROTTLE_FILE ?? join8(homedir5(), CONFIG_DIR_NAME4, "agent", ".billion-context-pi-update-check");
10699
10922
  var updateInFlight = false;
10923
+ var runNpm = async (args, opts) => {
10924
+ return new Promise((resolve3) => {
10925
+ execFile(
10926
+ "npm",
10927
+ args,
10928
+ { ...opts, shell: process.platform === "win32", maxBuffer: 4 * 1024 * 1024 },
10929
+ (err, stdout, stderr) => resolve3({
10930
+ code: err ? 1 : 0,
10931
+ stdout: String(stdout ?? ""),
10932
+ stderr: String(stderr ?? "")
10933
+ })
10934
+ );
10935
+ });
10936
+ };
10937
+ var runNpmImpl = runNpm;
10938
+ var runNode = (args, opts) => {
10939
+ return new Promise((resolve3) => {
10940
+ execFile(
10941
+ process.execPath,
10942
+ args,
10943
+ { ...opts, maxBuffer: 4 * 1024 * 1024 },
10944
+ (err, stdout, stderr) => resolve3({
10945
+ code: err ? 1 : 0,
10946
+ stdout: String(stdout ?? ""),
10947
+ stderr: String(stderr ?? "")
10948
+ })
10949
+ );
10950
+ });
10951
+ };
10952
+ var runNodeImpl = runNode;
10700
10953
  function parseVersion(v) {
10701
10954
  return v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
10702
10955
  }
@@ -10711,7 +10964,7 @@ function isNewer(latest, current) {
10711
10964
  }
10712
10965
  async function readLastCheck() {
10713
10966
  try {
10714
- const data = await readFile(THROTTLE_FILE, "utf-8");
10967
+ const data = await readFile(throttleFile(), "utf-8");
10715
10968
  return parseInt(data.trim(), 10) || 0;
10716
10969
  } catch {
10717
10970
  return 0;
@@ -10719,14 +10972,14 @@ async function readLastCheck() {
10719
10972
  }
10720
10973
  async function writeLastCheck(timestamp) {
10721
10974
  try {
10722
- await mkdir3(dirname5(THROTTLE_FILE), { recursive: true });
10723
- await writeFile3(THROTTLE_FILE, String(timestamp), "utf-8");
10975
+ await mkdir3(dirname5(throttleFile()), { recursive: true });
10976
+ await writeFile3(throttleFile(), String(timestamp), "utf-8");
10724
10977
  } catch {
10725
10978
  }
10726
10979
  }
10727
- async function readPackageJson(path4) {
10980
+ async function readPackageJson(path5) {
10728
10981
  try {
10729
- const data = JSON.parse(await readFile(path4, "utf-8"));
10982
+ const data = JSON.parse(await readFile(path5, "utf-8"));
10730
10983
  return data && typeof data === "object" ? data : void 0;
10731
10984
  } catch {
10732
10985
  return void 0;
@@ -10744,31 +10997,118 @@ function findNpmRoot(extDir) {
10744
10997
  async function findExtensionDir() {
10745
10998
  let dir = dirname5(fileURLToPath(import.meta.url));
10746
10999
  for (; ; ) {
10747
- const pkg = await readPackageJson(join7(dir, "package.json"));
11000
+ const pkg = await readPackageJson(join8(dir, "package.json"));
10748
11001
  if (pkg?.name === PACKAGE_NAME) return dir;
10749
11002
  const parent = dirname5(dir);
10750
11003
  if (parent === dir) return void 0;
10751
11004
  dir = parent;
10752
11005
  }
10753
11006
  }
10754
- async function autoInstallLatest(latest) {
10755
- if (!SEMVER_RE.test(latest)) return false;
10756
- const extDir = await findExtensionDir();
10757
- if (!extDir) return false;
11007
+ function declaredEntries(pkg) {
11008
+ const dot = pkg.exports?.["."];
11009
+ const exportEntry = typeof dot === "string" ? dot : dot?.import;
11010
+ return [.../* @__PURE__ */ new Set([pkg.pi?.extensions?.[0], exportEntry, pkg.main])].filter((v) => typeof v === "string" && v.length > 0);
11011
+ }
11012
+ async function verifyInstall(npmDir, latest) {
11013
+ const dir = join8(npmDir, "node_modules", PACKAGE_NAME);
11014
+ const pkg = await readPackageJson(join8(dir, "package.json"));
11015
+ if (!pkg?.version) return { ok: false, reason: "package-json-missing" };
11016
+ if (pkg.version !== latest) return { ok: false, reason: `version-mismatch:${pkg.version}` };
11017
+ const entries = declaredEntries(pkg);
11018
+ if (entries.length === 0) return { ok: false, reason: "no-entry-declared" };
11019
+ for (const rel of entries) {
11020
+ try {
11021
+ await access(join8(dir, rel));
11022
+ } catch {
11023
+ return { ok: false, reason: `entry-missing:${rel}` };
11024
+ }
11025
+ }
11026
+ const smokeEntry = pkg.pi?.extensions?.[0] ?? entries[0];
11027
+ if (!smokeEntry) return { ok: false, reason: "no-entry-declared" };
11028
+ const entry = join8(dir, smokeEntry);
11029
+ const SMOKE = "const{pathToFileURL}=require('node:url');import(pathToFileURL(process.argv[1]).href).then(()=>{},(e)=>{console.error(e&&e.stack||e);process.exit(1)})";
11030
+ const { code, stderr } = await runNodeImpl(["-e", SMOKE, entry], { timeout: 15e3 });
11031
+ if (code !== 0) return { ok: false, reason: `entry-import-failed:${stderr.trim().slice(-500)}` };
11032
+ return { ok: true };
11033
+ }
11034
+ async function autoInstallLatest(latest, extDirOverride) {
11035
+ if (!SEMVER_RE.test(latest)) return "failed";
11036
+ const extDir = extDirOverride ?? await findExtensionDir();
11037
+ if (!extDir) {
11038
+ logWarn("update", { event: "install-skip", reason: "extension-dir-not-found" });
11039
+ return "failed";
11040
+ }
10758
11041
  const npmDir = findNpmRoot(extDir);
10759
- if (!npmDir) return false;
11042
+ if (!npmDir) {
11043
+ logWarn("update", { event: "install-skip", reason: "not-under-node-modules", extDir });
11044
+ return "failed";
11045
+ }
10760
11046
  try {
10761
- const code = await new Promise((resolve2) => {
10762
- execFile(
10763
- "npm",
10764
- ["install", `${PACKAGE_NAME}@${latest}`, "--silent", "--no-audit", "--no-fund"],
10765
- { cwd: npmDir, timeout: 6e4, shell: process.platform === "win32" },
10766
- (err) => resolve2(err ? 1 : 0)
10767
- );
11047
+ const installArgs = (v) => [
11048
+ "install",
11049
+ `${PACKAGE_NAME}@${v}`,
11050
+ "--silent",
11051
+ "--no-audit",
11052
+ "--no-fund",
11053
+ "--no-save"
11054
+ ];
11055
+ const prevVersion = (await readPackageJson(join8(extDir, "package.json")))?.version ?? "0.1.45";
11056
+ const { code, stderr } = await runNpmImpl(installArgs(latest), { cwd: npmDir, timeout: 6e4 });
11057
+ if (code !== 0) {
11058
+ logWarn("update", {
11059
+ event: "auto-install-failed",
11060
+ latest,
11061
+ npmDir,
11062
+ stderr: stderr.trim().slice(-2e3)
11063
+ });
11064
+ return "failed";
11065
+ }
11066
+ const verify = await verifyInstall(npmDir, latest);
11067
+ if (!verify.ok) {
11068
+ const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.45";
11069
+ logWarn("update", { event: "auto-install-verify-failed", latest, reason: verify.reason, rollbackTo });
11070
+ const rb = await runNpmImpl(installArgs(rollbackTo), { cwd: npmDir, timeout: 6e4 });
11071
+ logInfo("update", { event: "rollback", from: latest, to: rollbackTo, ok: rb.code === 0 });
11072
+ return "rolled-back";
11073
+ }
11074
+ return "ok";
11075
+ } catch (e) {
11076
+ logWarn("update", {
11077
+ event: "auto-install-error",
11078
+ latest,
11079
+ error: e instanceof Error ? e.message : String(e)
11080
+ });
11081
+ return "failed";
11082
+ }
11083
+ }
11084
+ async function fetchLatestVersion() {
11085
+ try {
11086
+ const { code, stdout } = await runNpmImpl(["view", PACKAGE_NAME, "version"], {
11087
+ timeout: 2e4
10768
11088
  });
10769
- return code === 0;
11089
+ if (code === 0) {
11090
+ const v = stdout.trim().split("\n").map((s) => s.trim()).filter(Boolean).pop();
11091
+ if (v && SEMVER_RE.test(v)) return v;
11092
+ }
10770
11093
  } catch {
10771
- return false;
11094
+ }
11095
+ try {
11096
+ const res = await fetch(REGISTRY_URL, {
11097
+ signal: AbortSignal.timeout(1e4),
11098
+ headers: { Accept: "application/json" }
11099
+ });
11100
+ if (!res.ok) {
11101
+ logWarn("update", { event: "check-http", status: res.status });
11102
+ return void 0;
11103
+ }
11104
+ const data = await res.json();
11105
+ return data.version;
11106
+ } catch (e) {
11107
+ logWarn("update", {
11108
+ event: "check-fetch-error",
11109
+ error: e instanceof Error ? e.message : String(e)
11110
+ });
11111
+ return void 0;
10772
11112
  }
10773
11113
  }
10774
11114
  async function checkForUpdate(autoUpdate, notify) {
@@ -10784,18 +11124,9 @@ async function checkForUpdate(autoUpdate, notify) {
10784
11124
  if (now - lastCheck < CHECK_INTERVAL_MS) return;
10785
11125
  await writeLastCheck(now);
10786
11126
  const runtimeVersion = await getRuntimeVersion();
10787
- const res = await fetch(REGISTRY_URL, {
10788
- signal: AbortSignal.timeout(5e3),
10789
- headers: { Accept: "application/json" }
10790
- });
10791
- if (!res.ok) {
10792
- logWarn("update", { event: "check-http", status: res.status });
10793
- return;
10794
- }
10795
- const data = await res.json();
10796
- const latest = data.version;
11127
+ const latest = await fetchLatestVersion();
10797
11128
  if (!latest) return;
10798
- const current = runtimeVersion ?? "0.1.43";
11129
+ const current = runtimeVersion ?? "0.1.45";
10799
11130
  const hasUpdate = isNewer(latest, current);
10800
11131
  debug.event("update-check", {
10801
11132
  current,
@@ -10804,13 +11135,17 @@ async function checkForUpdate(autoUpdate, notify) {
10804
11135
  });
10805
11136
  logInfo("update", { event: "check", current, latest, hasUpdate });
10806
11137
  if (hasUpdate) {
10807
- const installed = await autoInstallLatest(latest);
10808
- if (installed && notify) {
11138
+ const outcome = await autoInstallLatest(latest);
11139
+ if (outcome === "ok" && notify) {
10809
11140
  notify(
10810
11141
  `\x1B[32m\u2714 ACP auto-updated ${current} \u2192 ${latest}. Restart Pi to finish.\x1B[0m`
10811
11142
  );
10812
11143
  logInfo("update", { event: "auto-installed", from: current, to: latest });
10813
- } else if (!installed && notify) {
11144
+ } else if (outcome === "rolled-back" && notify) {
11145
+ notify(
11146
+ `\x1B[33mACP ${latest} failed verification and was rolled back. Keeping ${current}. A later release will auto-update.\x1B[0m`
11147
+ );
11148
+ } else if (notify) {
10814
11149
  notify(
10815
11150
  `${PACKAGE_NAME} ${latest} available (you have ${current}). Run: pi update --extension npm:${PACKAGE_NAME}`
10816
11151
  );
@@ -10825,140 +11160,10 @@ async function checkForUpdate(autoUpdate, notify) {
10825
11160
  async function getRuntimeVersion() {
10826
11161
  const extDir = await findExtensionDir();
10827
11162
  if (!extDir) return void 0;
10828
- const pkg = await readPackageJson(join7(extDir, "package.json"));
11163
+ const pkg = await readPackageJson(join8(extDir, "package.json"));
10829
11164
  return pkg?.version;
10830
11165
  }
10831
11166
 
10832
- // src/setup-subagent-tools.ts
10833
- import { readFile as readFile2, writeFile as writeFile4, stat, copyFile, rename } from "fs/promises";
10834
- import { existsSync as existsSync4 } from "fs";
10835
- import { homedir as homedir5 } from "os";
10836
- import { join as join8 } from "path";
10837
- import { CONFIG_DIR_NAME as CONFIG_DIR_NAME4 } from "@earendil-works/pi-coding-agent";
10838
- var ACP_TOOLS2 = ["compress", "decompress", "search_context", "acp_status"];
10839
- var BUILTIN_DEFAULT_TOOLS = {
10840
- advisor: ["read", "grep", "find", "ls", "bash", "intercom"],
10841
- "context-builder": ["read", "grep", "find", "ls", "bash", "write", "web_search", "intercom"],
10842
- delegate: ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"],
10843
- oracle: ["read", "grep", "find", "ls", "bash", "intercom"],
10844
- planner: ["read", "grep", "find", "ls", "intercom"],
10845
- researcher: ["read", "write", "web_search", "fetch_content", "get_search_content", "intercom"],
10846
- reviewer: ["read", "grep", "find", "ls", "bash", "edit", "write", "intercom"],
10847
- scout: ["read", "grep", "find", "ls", "bash", "write", "intercom"],
10848
- worker: ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"]
10849
- };
10850
- function resolveAgentDir() {
10851
- const configured = process.env.PI_CODING_AGENT_DIR;
10852
- if (configured === "~") return homedir5();
10853
- if (configured?.startsWith("~/")) return join8(homedir5(), configured.slice(2));
10854
- return configured || join8(homedir5(), CONFIG_DIR_NAME4, "agent");
10855
- }
10856
- function desiredTools(existing, name) {
10857
- const base = Array.isArray(existing?.tools) && existing.tools.length > 0 ? [...existing.tools] : [...BUILTIN_DEFAULT_TOOLS[name] ?? []];
10858
- const hasAll = ACP_TOOLS2.every((t) => base.includes(t));
10859
- if (hasAll) return { tools: base, changed: false };
10860
- for (const t of ACP_TOOLS2) if (!base.includes(t)) base.push(t);
10861
- return { tools: base, changed: true };
10862
- }
10863
- async function ensureSubagentAcpTools(settingsPath) {
10864
- const path4 = settingsPath ?? join8(resolveAgentDir(), "settings.json");
10865
- let raw;
10866
- let mtimeMs;
10867
- try {
10868
- raw = await readFile2(path4, "utf-8");
10869
- mtimeMs = (await stat(path4)).mtimeMs;
10870
- } catch {
10871
- return { path: path4, action: "skipped", reason: "settings.json not found; will retry next session" };
10872
- }
10873
- let settings2;
10874
- try {
10875
- const parsed = JSON.parse(raw);
10876
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10877
- return { path: path4, action: "failed", reason: "settings.json root is not a JSON object" };
10878
- }
10879
- settings2 = parsed;
10880
- } catch (e) {
10881
- return { path: path4, action: "failed", reason: `settings.json is not valid JSON: ${e.message}` };
10882
- }
10883
- const subagents = typeof settings2.subagents === "object" && settings2.subagents !== null && !Array.isArray(settings2.subagents) ? { ...settings2.subagents } : {};
10884
- const overridesRaw = typeof subagents.agentOverrides === "object" && subagents.agentOverrides !== null && !Array.isArray(subagents.agentOverrides) ? { ...subagents.agentOverrides } : {};
10885
- const updatedOverrides = { ...overridesRaw };
10886
- let anyChanged = false;
10887
- for (const name of Object.keys(BUILTIN_DEFAULT_TOOLS)) {
10888
- const existing = overridesRaw[name] ?? void 0;
10889
- const { tools, changed } = desiredTools(existing, name);
10890
- if (changed) {
10891
- updatedOverrides[name] = { ...existing ?? {}, tools };
10892
- anyChanged = true;
10893
- }
10894
- }
10895
- if (!anyChanged) {
10896
- return { path: path4, action: "skipped", reason: "all builtin agents already have ACP tools" };
10897
- }
10898
- const backupPath = `${path4}.acp-bak`;
10899
- if (!existsSync4(backupPath)) {
10900
- try {
10901
- await copyFile(path4, backupPath);
10902
- } catch {
10903
- }
10904
- }
10905
- try {
10906
- if ((await stat(path4)).mtimeMs !== mtimeMs) {
10907
- return { path: path4, action: "skipped", reason: "settings.json changed during setup (concurrent write); will retry next session" };
10908
- }
10909
- } catch {
10910
- return { path: path4, action: "failed", reason: "settings.json disappeared during setup" };
10911
- }
10912
- const next = {
10913
- ...settings2,
10914
- subagents: { ...subagents, agentOverrides: updatedOverrides }
10915
- };
10916
- const tmpPath = `${path4}.tmp-${process.pid}`;
10917
- try {
10918
- await writeFile4(tmpPath, `${JSON.stringify(next, null, 2)}
10919
- `, "utf-8");
10920
- await rename(tmpPath, path4);
10921
- } catch (e) {
10922
- return { path: path4, action: "failed", reason: `write failed: ${e.message}` };
10923
- }
10924
- try {
10925
- const verify = JSON.parse(await readFile2(path4, "utf-8"));
10926
- const sub = verify.subagents;
10927
- const v = sub?.agentOverrides;
10928
- if (typeof v !== "object" || v === null) throw new Error("agentOverrides missing after write");
10929
- for (const name of Object.keys(BUILTIN_DEFAULT_TOOLS)) {
10930
- const tools = v[name]?.tools;
10931
- if (!Array.isArray(tools) || !ACP_TOOLS2.every((t) => tools.includes(t))) {
10932
- throw new Error(`${name} missing ACP tools after write`);
10933
- }
10934
- }
10935
- } catch (e) {
10936
- try {
10937
- await copyFile(backupPath, path4);
10938
- } catch {
10939
- }
10940
- return { path: path4, action: "failed", reason: `verification failed; restored backup: ${e.message}` };
10941
- }
10942
- return { path: path4, action: "updated" };
10943
- }
10944
- async function runSetupAndNotify(notify) {
10945
- try {
10946
- const result = await ensureSubagentAcpTools();
10947
- debug.event("setup-subagent-tools", { action: result.action, reason: result.reason });
10948
- if (result.action === "updated" && notify) {
10949
- notify(`ACP: enabled context tools (compress/decompress/search_context/acp_status) for subagents`);
10950
- }
10951
- if (result.action === "failed") {
10952
- logWarn("setup", { event: "subagent-tools", action: result.action, reason: result.reason });
10953
- }
10954
- return result;
10955
- } catch (e) {
10956
- debug.event("setup-subagent-tools-error", { msg: String(e) });
10957
- logError("setup", { event: "subagent-tools-error", error: e instanceof Error ? e.message : String(e), stack: e instanceof Error ? e.stack ?? "" : "" });
10958
- return { path: "", action: "failed", reason: String(e) };
10959
- }
10960
- }
10961
-
10962
11167
  // src/index.ts
10963
11168
  function createAcpExtension(adapter = {}) {
10964
11169
  return (pi) => {
@@ -10996,7 +11201,7 @@ function wireSessionLifecycle(pi, runtime) {
10996
11201
  const sid = ctx.sessionManager.getSessionId();
10997
11202
  runtime.clearSessionTracking(sid);
10998
11203
  const modelInfo = ctx.model;
10999
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.43" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
11204
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.45" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
11000
11205
  try {
11001
11206
  await runtime.reloadConfig(ctx.cwd);
11002
11207
  setDelegateDisplayUsage(resolveDelegate(runtime.adapter).displayUsage);
@@ -11014,10 +11219,10 @@ function wireSessionLifecycle(pi, runtime) {
11014
11219
  pi.registerTool(makeDelegateWaitTool(pi));
11015
11220
  pi.registerTool(makeDelegateCancelTool(pi));
11016
11221
  }
11017
- void checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
11222
+ const updateCheck = checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
11018
11223
  if (ctx.hasUI) ctx.ui.notify(msg);
11019
11224
  });
11020
- void runSetupAndNotify(ctx.hasUI ? (m) => ctx.ui.notify(m) : void 0);
11225
+ if (!ctx.hasUI) await updateCheck;
11021
11226
  delegateStatusWidget.setContext(ctx, runningRunsSnapshot);
11022
11227
  });
11023
11228
  pi.on("session_shutdown", () => {
@@ -11119,10 +11324,13 @@ function wireContextTransform(pi, runtime) {
11119
11324
  const rebuilt = coreOutToAgentMessages(turn.messages, originalById);
11120
11325
  const debugOn2 = debug.enabled;
11121
11326
  const turnKey = lastUserMessageId(entries) ?? sid;
11327
+ const compressOutcomes = collectCompressOutcomes(entries, turnStartIndex(entries));
11328
+ const outcome = compressOutcomes.length > 0 ? runtime.noteCompressOutcomes(turnKey, compressOutcomes) : null;
11122
11329
  if (turn.nudge?.shouldInject) {
11123
11330
  const emergency = turn.nudge.breakdown?.emergencyOverride === 1;
11124
11331
  turn.nudge.compressibleRanges = viableRanges(turn.nudge.compressibleRanges);
11125
- const alreadyShown = !emergency && runtime.nudgeShownFor(turnKey);
11332
+ const retryCapped = runtime.compressRetryCappedFor(turnKey);
11333
+ const alreadyShown = retryCapped || !emergency && runtime.nudgeShownFor(turnKey);
11126
11334
  if (!alreadyShown) {
11127
11335
  rebuilt.push(nudgeMessage(turn.nudge, turn.state.blocks.filter((b) => b.active), runtime.prompts));
11128
11336
  const rendered = renderNudgeText(turn.nudge, runtime.prompts);
@@ -11143,9 +11351,7 @@ ${rendered.text}${example}`);
11143
11351
  debug.event("nudge-suppressed", { sid: ctx.sessionManager.getSessionId(), turnKey, reason: turn.nudge.reason });
11144
11352
  }
11145
11353
  }
11146
- const compressOutcomes = collectCompressOutcomes(entries, turnStartIndex(entries));
11147
- if (compressOutcomes.length > 0) {
11148
- const outcome = runtime.noteCompressOutcomes(turnKey, compressOutcomes);
11354
+ if (outcome !== null) {
11149
11355
  const failed = outcome.retryFor !== null ? compressOutcomes.find((o) => o.toolCallId === outcome.retryFor) : void 0;
11150
11356
  if (failed) {
11151
11357
  rebuilt.push(compressRetryMessage(failed.text, outcome.count, MAX_COMPRESS_ATTEMPTS));
@@ -11160,9 +11366,10 @@ ${rendered.text}${example}`);
11160
11366
  }
11161
11367
  }
11162
11368
  debug.event("context-out", { outMsgs: rebuilt.length, injected: turn.nudge?.shouldInject ?? false, emergency: turn.nudge?.breakdown?.emergencyOverride === 1 });
11163
- void checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
11369
+ const updateCheck = checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
11164
11370
  if (ctx.hasUI) ctx.ui.notify(msg);
11165
11371
  });
11372
+ if (!ctx.hasUI) await updateCheck;
11166
11373
  return { messages: rebuilt };
11167
11374
  } catch (e) {
11168
11375
  logThrow("context", e, { sid, phase: "transform" });
@@ -11288,7 +11495,7 @@ function collectCompressOutcomes(entries, startIndex) {
11288
11495
  const m = entry.message;
11289
11496
  if (m.role !== "toolResult" || m.toolName !== "compress" || !m.toolCallId) continue;
11290
11497
  const text = extractText(m.content);
11291
- out.push({ toolCallId: m.toolCallId, isError: m.isError === true, success: m.isError !== true && isCompressSuccessText(text), text });
11498
+ out.push({ toolCallId: m.toolCallId, isError: m.isError === true, success: m.isError !== true && isCompressSuccessText(text), noop: m.isError !== true && isCompressNoopText(text), text });
11292
11499
  }
11293
11500
  return out;
11294
11501
  }
@@ -11304,7 +11511,8 @@ function compressRetryMessage(errorText, attempt, maxAttempts) {
11304
11511
  "- content must be an ARRAY of { startId, endId, summary } objects (topic optional) \u2014 not a JSON-encoded string.",
11305
11512
  '- Example: compress({ content: [{ startId: "m00005", endId: "m00080", summary: "..." }] })',
11306
11513
  '- startId/endId are the mNNNNN refs from the <acp> tags (or block ids like "b3").',
11307
- attempt >= maxAttempts - 1 ? "- This is your LAST retry for this turn \u2014 if it fails again, compression pauses until the next user message." : null
11514
+ attempt >= maxAttempts - 1 ? "- This is your LAST retry for this turn \u2014 if it fails again, compression pauses until the next user message." : null,
11515
+ "- If ranges were skipped (already compressed / too small), do NOT retry the same refs \u2014 run acp_status and use its CURRENT compressible ranges."
11308
11516
  ].filter((l) => l !== null).join("\n");
11309
11517
  return { role: "user", content: [{ type: "text", text }], timestamp: Date.now() };
11310
11518
  }