billion-context-pi 0.1.70 → 0.1.71-pr.457.264

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
@@ -79,6 +79,8 @@ That's it. The extension auto-loads on next Pi startup. No configuration needed
79
79
  > **Using another sub-agent extension?** billion-context-pi ships its own `acp_delegate` sub-agent tool (see below) at a fraction of the context cost (~600 tok vs ~7K tok/turn). Two delegation tools in one session only make the model's choice noisier, so pick one:
80
80
  > - **Use ACP's delegate** — remove the other extension: `pi remove npm:pi-subagents`
81
81
  > - **Keep your own sub-agent** — turn ACP's delegate off in `acp.json`: `{ "delegate": false }` (see *Using your own sub-agent instead* below)
82
+ >
83
+ > If you keep `pi-subagents` installed, billion-context-pi detects it at session start: a **project-level** install (`<cwd>/.pi/npm` or the project extensions dir) automatically stands `acp_delegate` down for that project — a reminder then tells you how to give pi-subagents' agents ACP compression via `/acp-subagents`. A **user-level-only** install (`~/.pi/npm`, user extensions dir) leaves `acp_delegate` active and logs a warning instead. Set `"delegate": { "forceEnable": true }` in `acp.json` to keep `acp_delegate` active regardless of detection.
82
84
 
83
85
  ## How it works
84
86
 
@@ -120,6 +122,8 @@ billion-context-pi is built for the **Pi** coding agent (`@earendil-works/pi-cod
120
122
 
121
123
  - **Coexisting with the [billion-context](https://github.com/ranxianglei/billion-context) wire proxy** — running both on the same session double-compresses every request (wasted tokens, nested summaries, two ref coordinate systems). This is prevented automatically: launcher paths (`bili pi`, …) export `BILLION_CONTEXT_PROXY`, and models whose `baseUrl` routes through the proxy (`…/bili/https://upstream…`) are detected at session start — in both cases billion-context-pi stands down with a warning and leaves the proxy as the sole compressor. One exception: transparent mode, where traffic reaches the proxy via `HTTPS_PROXY` so the URL carries no `/bili/` prefix — that is undetectable from the URL, so export `BILLION_CONTEXT_PROXY=1` before starting pi in that case.
122
124
 
125
+ - **Reverse collision — the `billion-context` thin plugin shadows `/acp`.** If the pi plugin shipped inside [`billion-context`](https://github.com/ranxianglei/billion-context) is also installed (an entry pointing at a `billion-context` install in `"packages"` of `~/.pi/agent/settings.json`, e.g. from `bili plugin install pi`), it registers its own `/acp` command, and Pi has no duplicate-command guard — which one answers depends on load order. Telling symptom: on a plain `pi` launch, typing `/acp` shows ``bili: no proxy detected (run via `bili <client>` or set a /bili/ baseURL)`` — that string is emitted by the thin plugin, **not** by billion-context-pi, and pi does not need `bili` running. Fix: the collision only bites when you launch plain `pi` — remove the thin plugin there (`bili plugin remove pi`, or drop the non-`npm:billion-context-pi` entry from that settings file) so billion-context-pi owns `/acp`. If you launch via `bili pi` instead, nothing needs removing: the launcher exports `BILLION_CONTEXT_PROXY` and billion-context-pi stands down entirely (bullet above), so both plugins may stay installed side by side. Note: `bili plugin install pi` removes an existing `npm:billion-context-pi` entry from that file without saying so ([billion-context#788](https://github.com/ranxianglei/billion-context/issues/788)).
126
+
123
127
  ## Model-facing tools
124
128
 
125
129
  | Tool | What it does |
package/README.zh-CN.md CHANGED
@@ -78,6 +78,8 @@ pi install npm:billion-context-pi
78
78
  > **你另有子代理扩展?** billion-context-pi 自带 `acp_delegate` 子代理工具(见下文),上下文成本极低(~600 tok vs ~7K tok/轮)。同一会话里两套委派工具只会让模型的选择更混乱,二选一:
79
79
  > - **用 ACP 的 delegate** —— 卸载另一个扩展:`pi remove npm:pi-subagents`
80
80
  > - **保留你自己的子代理** —— 在 `acp.json` 里关掉 ACP 的 delegate:`{ "delegate": false }`(见下文*改用你自己的子代理*)
81
+ >
82
+ > 若保留已安装的 `pi-subagents`,billion-context-pi 会在会话启动时检测:**项目级**安装(`<cwd>/.pi/npm` 或项目内 extensions 目录)会自动停用该项目的 `acp_delegate`,并提醒你运行 `/acp-subagents` 让 pi-subagents 的子代理获得 ACP 压缩;仅**用户级**(全局)安装时只记一条警告日志,`acp_delegate` 保持启用。在 `acp.json` 中设置 `"delegate": { "forceEnable": true }` 可在检测到第三方子代理时仍强制保留 `acp_delegate`。
81
83
 
82
84
  ## 工作原理
83
85
 
@@ -119,6 +121,8 @@ billion-context-pi 面向 **Pi** 编码代理(`@earendil-works/pi-coding-agent`)
119
121
 
120
122
  - **与 [billion-context](https://github.com/ranxianglei/billion-context) 线代理共存** —— 两者同时作用于同一会话会对每个请求双重压缩(token 浪费、嵌套摘要、两套 ref 坐标系)。这会被自动防止:launcher 路径(`bili pi` 等)导出 `BILLION_CONTEXT_PROXY`;模型 `baseUrl` 经代理路由(`…/bili/https://upstream…`)时在会话开始即被检测到 —— 两种情况下 billion-context-pi 都会带警告让位,由代理独占压缩。一个例外:透明模式(流量经 `HTTPS_PROXY` 到达代理、URL 无 `/bili/` 前缀)无法从 URL 识别 —— 此时请在启动 pi 前导出 `BILLION_CONTEXT_PROXY=1`。
121
123
 
124
+ - **反向冲突 —— `billion-context` 薄插件抢占 `/acp`。** 如果 [`billion-context`](https://github.com/ranxianglei/billion-context) 内置的 pi 插件也被安装了(`~/.pi/agent/settings.json` 的 `"packages"` 里存在指向某个 `billion-context` 安装目录的条目,例如来自 `bili plugin install pi`),它会注册自己的 `/acp` 命令,而 Pi 没有重复命令防护 —— 哪个生效取决于加载顺序。典型症状:在普通 `pi` 启动下输入 `/acp` 显示 ``bili: no proxy detected (run via `bili <client>` or set a /bili/ baseURL)`` —— 这行字符串由薄插件发出,**不是** billion-context-pi 发出的,而且 pi 并不需要运行 `bili`。修复:该冲突只在你用普通 `pi` 启动时才有实际影响 —— 移除薄插件(`bili plugin remove pi`,或从该设置文件中删除除 `npm:billion-context-pi` 之外的条目),让 `/acp` 由 billion-context-pi 接管;若改用 `bili pi` 启动则无需移除任何东西:launcher 会导出 `BILLION_CONTEXT_PROXY`,billion-context-pi 整体让位(见上一条),两个插件可以并存。注意:`bili plugin install pi` 会静默删除该文件中已有的 `npm:billion-context-pi` 条目且不提示([billion-context#788](https://github.com/ranxianglei/billion-context/issues/788))。
125
+
122
126
  ## 模型工具
123
127
 
124
128
  | 工具 | 作用 |
package/dist/config.d.ts CHANGED
@@ -29,6 +29,13 @@ export interface DelegateConfig {
29
29
  /** Enable acp_delegate tools (delegate/wait/cancel) and their system-prompt
30
30
  * section. Default: true. Set `enabled: false` to skip registering them. */
31
31
  enabled?: boolean;
32
+ /** Keep acp_delegate active even when a third-party subagent extension
33
+ * (pi-subagents) is installed. Default: false — when pi-subagents is
34
+ * detected at session start, acp_delegate stands down (tools, fleet
35
+ * shortcut and system-prompt section skipped) to avoid two overlapping
36
+ * sub-agent systems, and a reminder points at /acp-subagents so the
37
+ * third-party agents can still get ACP compression tools (#415). */
38
+ forceEnable?: boolean;
32
39
  /** How delegate usage is reported back to the main session.
33
40
  * "separate" (default) — delegate tokens tracked in a separate accumulator;
34
41
  * main session totals stay clean, delegate usage shows as its own block in
@@ -89,6 +96,9 @@ export interface DelegateConfig {
89
96
  * corresponding timeout/watchdog is disabled. */
90
97
  export interface DelegatePolicy {
91
98
  enabled: boolean;
99
+ /** Resolved delegate.forceEnable (default false): keep acp_delegate even
100
+ * when a third-party subagent extension (pi-subagents) is installed (#415). */
101
+ forceEnable: boolean;
92
102
  displayUsage: "merged" | "separate";
93
103
  maxDepth: number;
94
104
  syncTimeoutMs: number | null;
package/dist/index.js CHANGED
@@ -4532,6 +4532,7 @@ var debug = {
4532
4532
  var DEFAULT_FLEET_SHORTCUT = "ctrl+alt+d";
4533
4533
  var DEFAULT_DELEGATE_POLICY = {
4534
4534
  enabled: true,
4535
+ forceEnable: false,
4535
4536
  displayUsage: "separate",
4536
4537
  maxDepth: 2,
4537
4538
  syncTimeoutMs: 5 * 6e4,
@@ -4565,19 +4566,28 @@ function resolveDelegate(adapter) {
4565
4566
  DEFAULT_DELEGATE_POLICY.asyncTimeoutMs
4566
4567
  );
4567
4568
  const maxConcurrent = resolveMaxConcurrent(process.env.PI_ACP_DELEGATE_MAX_CONCURRENT, cfg.maxConcurrent);
4569
+ const forceEnable = resolveForceEnable(process.env.PI_ACP_DELEGATE_FORCE_ENABLE, cfg.forceEnable);
4568
4570
  if (idleMs === null) {
4569
4571
  logWarn("config", {
4570
4572
  event: "delegate-idle-watchdog-disabled",
4571
4573
  hint: "no-output watchdog is off; hung async runs must be cancelled manually via acp_delegate_cancel"
4572
4574
  });
4573
4575
  }
4574
- return { enabled, displayUsage, maxDepth, syncTimeoutMs, idleMs, asyncTimeoutMs, maxConcurrent, thinkingLevel: cfg.thinkingLevel, agents: cfg.agents, notifyIfRead: cfg.notifyIfRead ?? "skip", fleetShortcut: resolveFleetShortcut(cfg.fleetShortcut) };
4576
+ return { enabled, forceEnable, displayUsage, maxDepth, syncTimeoutMs, idleMs, asyncTimeoutMs, maxConcurrent, thinkingLevel: cfg.thinkingLevel, agents: cfg.agents, notifyIfRead: cfg.notifyIfRead ?? "skip", fleetShortcut: resolveFleetShortcut(cfg.fleetShortcut) };
4575
4577
  }
4576
4578
  function resolveFleetShortcut(value) {
4577
4579
  if (typeof value === "string") return value;
4578
4580
  if (value !== void 0) logWarn("config", { event: "delegate-config-invalid", field: "fleetShortcut", value: String(value), fallback: DEFAULT_FLEET_SHORTCUT });
4579
4581
  return DEFAULT_FLEET_SHORTCUT;
4580
4582
  }
4583
+ function resolveForceEnable(envValue, cfgValue) {
4584
+ if (envValue === "true") return true;
4585
+ if (envValue === "false") return false;
4586
+ if (envValue !== void 0) {
4587
+ logWarn("config", { event: "delegate-config-invalid", field: "forceEnable", value: envValue, fallback: cfgValue === true });
4588
+ }
4589
+ return cfgValue === true;
4590
+ }
4581
4591
  function resolveMaxDepth(value) {
4582
4592
  if (value === void 0) return DEFAULT_DELEGATE_POLICY.maxDepth;
4583
4593
  const n = Number(value);
@@ -6016,6 +6026,25 @@ var OverflowEpisode = class {
6016
6026
  }
6017
6027
  };
6018
6028
 
6029
+ // src/turn-boundary.ts
6030
+ function isTurnBoundary(entry, policy = {}) {
6031
+ if (entry.message?.role === "user") return true;
6032
+ return policy.countCustomMessages === true && isCustomMessageEntry(entry);
6033
+ }
6034
+ function lastTurnBoundaryId(entries, policy) {
6035
+ for (let i = entries.length - 1; i >= 0; i--) {
6036
+ const e = entries[i];
6037
+ if (isTurnBoundary(e, policy)) return e.id;
6038
+ }
6039
+ return void 0;
6040
+ }
6041
+ function lastTurnBoundaryIndex(entries, policy) {
6042
+ for (let i = entries.length - 1; i >= 0; i--) {
6043
+ if (isTurnBoundary(entries[i], policy)) return i;
6044
+ }
6045
+ return -1;
6046
+ }
6047
+
6019
6048
  // src/runtime.ts
6020
6049
  function readContextEntries(sm) {
6021
6050
  const source = sm;
@@ -6027,6 +6056,9 @@ function isPiHost(sm) {
6027
6056
  const source = sm;
6028
6057
  return typeof source.buildContextEntries === "function";
6029
6058
  }
6059
+ function retryBreakerKey(sm, policy) {
6060
+ return lastTurnBoundaryId(readContextEntries(sm), policy);
6061
+ }
6030
6062
  function mergeLiveEntries(entries, live, state, origins) {
6031
6063
  const persisted = entries.filter((e) => e.type === "message");
6032
6064
  const liveIdentities = live.map(messageIdentity);
@@ -6230,6 +6262,40 @@ function createRuntime(adapter) {
6230
6262
  function dropTokenScale(sid) {
6231
6263
  tokenScaleStale.delete(sid);
6232
6264
  }
6265
+ const HOST_USAGE_WINDOW = 4;
6266
+ const HOST_USAGE_MIN_SAMPLES = 3;
6267
+ const HOST_USAGE_SPREAD_MAX = 0.25;
6268
+ const hostUsageSamples = /* @__PURE__ */ new Map();
6269
+ function noteHostUsage(sid, tokens) {
6270
+ if (tokens <= 0) return false;
6271
+ let win = hostUsageSamples.get(sid);
6272
+ if (!win) {
6273
+ win = [];
6274
+ hostUsageSamples.set(sid, win);
6275
+ }
6276
+ win.push(tokens);
6277
+ if (win.length > HOST_USAGE_WINDOW) win.shift();
6278
+ if (win.length < HOST_USAGE_MIN_SAMPLES) return false;
6279
+ const lo = Math.min(...win);
6280
+ const hi = Math.max(...win);
6281
+ return (hi - lo) / lo <= HOST_USAGE_SPREAD_MAX;
6282
+ }
6283
+ function dropHostUsageSamples(sid) {
6284
+ hostUsageSamples.delete(sid);
6285
+ }
6286
+ const sizeDivergenceStreaks = /* @__PURE__ */ new Map();
6287
+ function noteSizeDivergence(sid, divergent) {
6288
+ if (!divergent) {
6289
+ sizeDivergenceStreaks.delete(sid);
6290
+ return false;
6291
+ }
6292
+ const streak = (sizeDivergenceStreaks.get(sid) ?? 0) + 1;
6293
+ sizeDivergenceStreaks.set(sid, streak);
6294
+ return streak === 3;
6295
+ }
6296
+ function dropSizeDivergence(sid) {
6297
+ sizeDivergenceStreaks.delete(sid);
6298
+ }
6233
6299
  const strictEchoLogged = /* @__PURE__ */ new Set();
6234
6300
  const compressOutcomes = /* @__PURE__ */ new Map();
6235
6301
  function compressTrackerFor(sid) {
@@ -6379,6 +6445,7 @@ function createRuntime(adapter) {
6379
6445
  }
6380
6446
  let refused = false;
6381
6447
  let refusalMessage = null;
6448
+ let delegateStoodDown = false;
6382
6449
  return { core, store, get refused() {
6383
6450
  return refused;
6384
6451
  }, set refused(v2) {
@@ -6387,6 +6454,10 @@ function createRuntime(adapter) {
6387
6454
  return refusalMessage;
6388
6455
  }, set refusalMessage(v2) {
6389
6456
  refusalMessage = v2;
6457
+ }, get delegateStoodDown() {
6458
+ return delegateStoodDown;
6459
+ }, set delegateStoodDown(v2) {
6460
+ delegateStoodDown = v2;
6390
6461
  }, get adapter() {
6391
6462
  return adapterRef;
6392
6463
  }, setAdapter: (a) => {
@@ -6395,7 +6466,7 @@ function createRuntime(adapter) {
6395
6466
  return promptsRef;
6396
6467
  }, setPrompts: (p) => {
6397
6468
  promptsRef = p;
6398
- }, markNudgeShown, nudgeShownFor, nudgeShownTokensFor, clearNudgeTracking, clearNudgeTokenStamps, noteCompressOutcomes, compressRetryCappedFor, clearCompressRetryTracking, liveContextLimit, configFor, reasoningDropFor, reloadConfig, stateFor, save, deriveChildState: deriveChild, acquireLock, overflowFor, overflowDrop, noteDeadCompress, clearDeadCompress, throttleFor, throttleDrop, noteTokenScale, dropTokenScale };
6469
+ }, markNudgeShown, nudgeShownFor, nudgeShownTokensFor, clearNudgeTracking, clearNudgeTokenStamps, noteCompressOutcomes, compressRetryCappedFor, clearCompressRetryTracking, liveContextLimit, configFor, reasoningDropFor, reloadConfig, stateFor, save, deriveChildState: deriveChild, acquireLock, overflowFor, overflowDrop, noteDeadCompress, clearDeadCompress, throttleFor, throttleDrop, noteTokenScale, dropTokenScale, noteHostUsage, dropHostUsageSamples, noteSizeDivergence, dropSizeDivergence };
6399
6470
  }
6400
6471
 
6401
6472
  // node_modules/typebox/build/system/memory/memory.mjs
@@ -10792,6 +10863,15 @@ __export(typebox_exports, {
10792
10863
  With: () => With2
10793
10864
  });
10794
10865
 
10866
+ // src/host.ts
10867
+ function isDeclaredForkHost() {
10868
+ const v2 = process.env.PI_ACP_FORK_HOST;
10869
+ return v2 === "1" || v2?.toLowerCase() === "true";
10870
+ }
10871
+ function isUnsupportedHost(sm) {
10872
+ return !isPiHost(sm) && !isDeclaredForkHost();
10873
+ }
10874
+
10795
10875
  // src/tokens.ts
10796
10876
  function collectCoveredMessageIds(state) {
10797
10877
  const ids = /* @__PURE__ */ new Set();
@@ -10840,25 +10920,6 @@ function adjustedTokenCount(core, messages, state, config, prelim, imageTokensBy
10840
10920
  return view.drifted ? view.viewTokens : prelim;
10841
10921
  }
10842
10922
 
10843
- // src/turn-boundary.ts
10844
- function isTurnBoundary(entry, policy = {}) {
10845
- if (entry.message?.role === "user") return true;
10846
- return policy.countCustomMessages === true && isCustomMessageEntry(entry);
10847
- }
10848
- function lastTurnBoundaryId(entries, policy) {
10849
- for (let i = entries.length - 1; i >= 0; i--) {
10850
- const e = entries[i];
10851
- if (isTurnBoundary(e, policy)) return e.id;
10852
- }
10853
- return void 0;
10854
- }
10855
- function lastTurnBoundaryIndex(entries, policy) {
10856
- for (let i = entries.length - 1; i >= 0; i--) {
10857
- if (isTurnBoundary(entries[i], policy)) return i;
10858
- }
10859
- return -1;
10860
- }
10861
-
10862
10923
  // src/compat.ts
10863
10924
  function normalizeSystemPrompt(input) {
10864
10925
  if (input === void 0) return "";
@@ -10887,6 +10948,12 @@ var UNSUPPORTED_HOST_MESSAGE = [
10887
10948
  " bili omp",
10888
10949
  "Docs: https://github.com/ranxianglei/billion-context"
10889
10950
  ].join("\n");
10951
+ var FORK_HOST_WARNING_MESSAGE = [
10952
+ '[billion-context-pi] Running on a declared fork host (PI_ACP_FORK_HOST). Known limitation: live-tail refs can drift as the session grows, so compress calls may start failing with "does not exist in this session" (#454). Root fix tracked in #459.',
10953
+ "For long sessions on this host, prefer the billion-context proxy \u2014 it runs compression server-side and owns the ref coordinate space:",
10954
+ " npm install -g billion-context",
10955
+ " bili omp"
10956
+ ].join("\n");
10890
10957
 
10891
10958
  // src/compress-tool.ts
10892
10959
  function formatK2(n) {
@@ -11158,7 +11225,7 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
11158
11225
  }
11159
11226
  return s.text === r.summary ? r : { ...r, summary: s.text };
11160
11227
  });
11161
- const turnKey = lastTurnBoundaryId(entries, resolveHostSession(runtime.adapter)) ?? sid;
11228
+ const turnKey = retryBreakerKey(ctx.sessionManager, resolveHostSession(runtime.adapter)) ?? sid;
11162
11229
  const snapshot = compressibleSnapshotText(turn.nudge);
11163
11230
  if (runtime.compressRetryCappedFor(sid, turnKey)) {
11164
11231
  logWarn("compress", { sid, event: "capped-reject", turnKey });
@@ -11247,6 +11314,9 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
11247
11314
  });
11248
11315
  if (errors.length > 0) {
11249
11316
  logError("compress", { sid: ctx.sessionManager.getSessionId(), event: "errors", count: errors.length, errors: errors.slice(0, 5) });
11317
+ if (blocksCreated === 0 && isDeclaredForkHost() && !isPiHost(ctx.sessionManager) && /does not exist|cannot be anchored|is unknown|unknown refs/i.test(errors.join(" "))) {
11318
+ logWarn("compress", { sid: ctx.sessionManager.getSessionId(), event: "fork-ref-drift-suspected", seeIssue: "#459", errors: errors.slice(0, 3) });
11319
+ }
11250
11320
  }
11251
11321
  if (warnings.length > 0) {
11252
11322
  logWarn("compress", { sid: ctx.sessionManager.getSessionId(), event: "warnings", count: warnings.length, warnings: warnings.slice(0, 5) });
@@ -11602,7 +11672,7 @@ function resolveActivePack(adapter, cwd, provider, modelId, resolver) {
11602
11672
  }
11603
11673
  var HOST_VERSION = (() => {
11604
11674
  try {
11605
- return "0.1.70";
11675
+ return "0.1.71";
11606
11676
  } catch {
11607
11677
  return "dev";
11608
11678
  }
@@ -17897,6 +17967,11 @@ import * as fs6 from "fs";
17897
17967
  import * as os2 from "os";
17898
17968
  import * as path11 from "path";
17899
17969
  var ACP_TOOLS3 = ["compress", "decompress", "search_context", "acp_status"];
17970
+ var DELEGATE_STAND_DOWN_MESSAGE = [
17971
+ "[billion-context-pi] pi-subagents detected \u2014 acp_delegate has been automatically disabled to avoid two overlapping sub-agent systems.",
17972
+ "pi-subagents' agents do NOT get ACP context compression by default. Run /acp-subagents to inject compress/decompress/search_context/acp_status into its agent overrides.",
17973
+ 'To keep acp_delegate despite pi-subagents being installed, set "delegate": { "forceEnable": true } in acp.json.'
17974
+ ].join("\n");
17900
17975
  function resolveAgentDir() {
17901
17976
  const envDir = process.env.PI_CODING_AGENT_DIR;
17902
17977
  if (envDir) {
@@ -17927,38 +18002,39 @@ function parseFrontmatterTools(content) {
17927
18002
  if (!name) return null;
17928
18003
  return tools ? { name, tools } : { name };
17929
18004
  }
17930
- function findPiSubagentsInstall(agentDir, cwd) {
17931
- const candidates = [
17932
- path11.join(agentDir, "npm", "node_modules", "pi-subagents"),
17933
- path11.join(cwd, CONFIG_DIR_NAME2, "npm", "node_modules", "pi-subagents")
17934
- ];
17935
- const extensionRoots = [
17936
- path11.join(agentDir, "extensions"),
17937
- path11.join(cwd, CONFIG_DIR_NAME2, "extensions")
17938
- ];
17939
- for (const dir of candidates) {
17940
- if (fs6.existsSync(path11.join(dir, "package.json"))) return dir;
18005
+ function npmInstallAt(base) {
18006
+ const dir = path11.join(base, "npm", "node_modules", "pi-subagents");
18007
+ return fs6.existsSync(path11.join(dir, "package.json")) ? dir : null;
18008
+ }
18009
+ function extensionInstallUnder(root) {
18010
+ let entries;
18011
+ try {
18012
+ entries = fs6.readdirSync(root, { withFileTypes: true });
18013
+ } catch {
18014
+ return null;
17941
18015
  }
17942
- for (const root of extensionRoots) {
17943
- let entries;
18016
+ for (const entry of entries) {
18017
+ if (!entry.isDirectory()) continue;
18018
+ const pkgPath = path11.join(root, entry.name, "package.json");
17944
18019
  try {
17945
- entries = fs6.readdirSync(root, { withFileTypes: true });
17946
- } catch {
17947
- continue;
17948
- }
17949
- for (const entry of entries) {
17950
- if (!entry.isDirectory()) continue;
17951
- const pkgPath = path11.join(root, entry.name, "package.json");
17952
- try {
17953
- if (JSON.parse(fs6.readFileSync(pkgPath, "utf-8")).name === "pi-subagents") {
17954
- return path11.join(root, entry.name);
17955
- }
17956
- } catch {
18020
+ if (JSON.parse(fs6.readFileSync(pkgPath, "utf-8")).name === "pi-subagents") {
18021
+ return path11.join(root, entry.name);
17957
18022
  }
18023
+ } catch {
17958
18024
  }
17959
18025
  }
17960
18026
  return null;
17961
18027
  }
18028
+ function findPiSubagentsInstall(agentDir, cwd) {
18029
+ return npmInstallAt(agentDir) ?? npmInstallAt(path11.join(cwd, CONFIG_DIR_NAME2)) ?? extensionInstallUnder(path11.join(agentDir, "extensions")) ?? extensionInstallUnder(path11.join(cwd, CONFIG_DIR_NAME2, "extensions"));
18030
+ }
18031
+ function findPiSubagentsInstalls(agentDir, cwd) {
18032
+ const pick = (paths) => paths.filter((p) => p !== null);
18033
+ return {
18034
+ user: pick([npmInstallAt(agentDir), extensionInstallUnder(path11.join(agentDir, "extensions"))]),
18035
+ project: pick([npmInstallAt(path11.join(cwd, CONFIG_DIR_NAME2)), extensionInstallUnder(path11.join(cwd, CONFIG_DIR_NAME2, "extensions"))])
18036
+ };
18037
+ }
17962
18038
  function discoverBuiltinAgents(installDir) {
17963
18039
  const agentsDir = path11.join(installDir, "agents");
17964
18040
  let entries;
@@ -18256,7 +18332,7 @@ async function statusReport(runtime, ctx) {
18256
18332
  const sentTokens = estimateTokens(coreMessages, coveredIds, imageTokens) + systemPromptTokens;
18257
18333
  const viewSentTokens = adjustedTokenCount(runtime.core, coreMessages, state, config, sentTokens, imageTokens, systemPromptTokens);
18258
18334
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: anchorStale ? viewSentTokens : Math.max(viewSentTokens, realUsage?.tokens ?? 0) });
18259
- const versionStr = "0.1.70" ? `billion-context-pi@${"0.1.70"} \xB7 pack: ${resolveSurfaceMeta(runtime.adapter, ctx?.cwd ?? process.cwd(), ctx?.model?.provider, ctx?.model?.id).pack}` : void 0;
18335
+ const versionStr = "0.1.71" ? `billion-context-pi@${"0.1.71"} \xB7 pack: ${resolveSurfaceMeta(runtime.adapter, ctx?.cwd ?? process.cwd(), ctx?.model?.provider, ctx?.model?.id).pack}` : void 0;
18260
18336
  let text = buildStatusPanel({
18261
18337
  version: versionStr,
18262
18338
  tokenCount: sessionTokens,
@@ -18868,7 +18944,7 @@ async function autoInstallLatest(latest, extDirOverride) {
18868
18944
  "--no-fund",
18869
18945
  "--no-save"
18870
18946
  ];
18871
- const prevVersion = (await readPackageJson(join15(extDir, "package.json")))?.version ?? "0.1.70";
18947
+ const prevVersion = (await readPackageJson(join15(extDir, "package.json")))?.version ?? "0.1.71";
18872
18948
  const { code, stderr } = await runNpmImpl(installArgs(latest), { cwd: npmDir, timeout: 6e4 });
18873
18949
  if (code !== 0) {
18874
18950
  logWarn("update", {
@@ -18886,7 +18962,7 @@ async function autoInstallLatest(latest, extDirOverride) {
18886
18962
  }
18887
18963
  const verify = await verifyInstall(npmDir, latest);
18888
18964
  if (!verify.ok) {
18889
- const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.70";
18965
+ const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.71";
18890
18966
  logWarn("update", { event: "auto-install-verify-failed", latest, reason: verify.reason, rollbackTo });
18891
18967
  const rb = await runNpmImpl(installArgs(rollbackTo), { cwd: npmDir, timeout: 6e4 });
18892
18968
  logInfo("update", { event: "rollback", from: latest, to: rollbackTo, ok: rb.code === 0 });
@@ -18961,7 +19037,7 @@ async function checkForUpdate(autoUpdate, notify) {
18961
19037
  }
18962
19038
  const latest = await fetchLatestVersion(tag);
18963
19039
  if (!latest) return;
18964
- const current = runtimeVersion ?? "0.1.70";
19040
+ const current = runtimeVersion ?? "0.1.71";
18965
19041
  const hasUpdate = isVersionNewer(latest, current);
18966
19042
  debug.event("update-check", {
18967
19043
  current,
@@ -19019,15 +19095,6 @@ async function getInstalledSpec() {
19019
19095
  return hostPkg?.dependencies?.[PACKAGE_NAME];
19020
19096
  }
19021
19097
 
19022
- // src/host.ts
19023
- function isDeclaredForkHost() {
19024
- const v2 = process.env.PI_ACP_FORK_HOST;
19025
- return v2 === "1" || v2?.toLowerCase() === "true";
19026
- }
19027
- function isUnsupportedHost(sm) {
19028
- return !isPiHost(sm) && !isDeclaredForkHost();
19029
- }
19030
-
19031
19098
  // src/proxy-detect.ts
19032
19099
  function isBiliProxyBaseUrl(baseUrl) {
19033
19100
  if (!baseUrl) return false;
@@ -19048,6 +19115,13 @@ var PROXY_STAND_DOWN_MESSAGE = [
19048
19115
  "If your traffic reaches the proxy via HTTPS_PROXY instead (no /bili/ prefix in the URL), export BILLION_CONTEXT_PROXY=1 before starting pi.",
19049
19116
  "Docs: https://github.com/ranxianglei/billion-context"
19050
19117
  ].join("\n");
19118
+ function nativeStandDownMessage(host) {
19119
+ return [
19120
+ `[billion-context-pi] billion-context native host detected (BILLION_CONTEXT_NATIVE=${host}) \u2014 the in-process native plugin proxies model API traffic and owns compression.`,
19121
+ "ACP client-side compression has been disabled for this session to avoid double compression; nothing else to configure.",
19122
+ "Docs: https://github.com/ranxianglei/billion-context"
19123
+ ].join("\n");
19124
+ }
19051
19125
 
19052
19126
  // src/index.ts
19053
19127
  function createAcpExtension(adapter = {}) {
@@ -19061,16 +19135,18 @@ function createAcpExtension(adapter = {}) {
19061
19135
  return;
19062
19136
  }
19063
19137
  const runtime = createRuntime(adapter);
19064
- let proxyWarned = false;
19138
+ let standDownWarned = false;
19065
19139
  const standDownIfProxied = (ctx) => {
19066
- if (!isBiliProxyBaseUrl(ctx.model?.baseUrl)) return false;
19140
+ const nativeHost = process.env.BILLION_CONTEXT_NATIVE || void 0;
19141
+ if (nativeHost === void 0 && !process.env.BILLION_CONTEXT_PROXY && !isBiliProxyBaseUrl(ctx.model?.baseUrl)) return false;
19142
+ const message = nativeHost !== void 0 ? nativeStandDownMessage(nativeHost) : PROXY_STAND_DOWN_MESSAGE;
19067
19143
  runtime.refused = true;
19068
- runtime.refusalMessage = PROXY_STAND_DOWN_MESSAGE;
19069
- if (!proxyWarned) {
19070
- proxyWarned = true;
19071
- logWarn("host", { event: "proxy-baseurl-detected", sid: ctx.sessionManager.getSessionId(), action: "refused" });
19072
- if (ctx.hasUI) ctx.ui.notify(PROXY_STAND_DOWN_MESSAGE, "warning");
19073
- else console.error(PROXY_STAND_DOWN_MESSAGE);
19144
+ runtime.refusalMessage = message;
19145
+ if (!standDownWarned) {
19146
+ standDownWarned = true;
19147
+ logWarn("host", { event: nativeHost !== void 0 ? "native-host-detected" : "proxy-detected", sid: ctx.sessionManager.getSessionId(), action: "refused", native: nativeHost ?? null });
19148
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
19149
+ else console.error(message);
19074
19150
  }
19075
19151
  return true;
19076
19152
  };
@@ -19127,6 +19203,8 @@ function wireDelegateReadTracking(pi) {
19127
19203
  }
19128
19204
  function wireSessionLifecycle(pi, runtime, standDownIfProxied) {
19129
19205
  let ompWarned = false;
19206
+ let forkWarned = false;
19207
+ let subagentStandDownWarned = false;
19130
19208
  pi.on("session_start", async (_event, ctx) => {
19131
19209
  if (isUnsupportedHost(ctx.sessionManager)) {
19132
19210
  runtime.refused = true;
@@ -19139,17 +19217,29 @@ function wireSessionLifecycle(pi, runtime, standDownIfProxied) {
19139
19217
  }
19140
19218
  return;
19141
19219
  }
19220
+ if (isDeclaredForkHost() && !isPiHost(ctx.sessionManager)) {
19221
+ const sid2 = ctx.sessionManager.getSessionId();
19222
+ logWarn("host", { event: "fork-host-admitted", sid: sid2, knownLimitation: "ref-drift", seeIssue: "#454", rootFix: "#459" });
19223
+ if (!forkWarned) {
19224
+ forkWarned = true;
19225
+ if (ctx.hasUI) ctx.ui.notify(FORK_HOST_WARNING_MESSAGE, "warning");
19226
+ else console.error(FORK_HOST_WARNING_MESSAGE);
19227
+ }
19228
+ }
19142
19229
  if (standDownIfProxied(ctx)) return;
19143
19230
  runtime.store.invalidate();
19144
19231
  runtime.clearNudgeTracking(ctx.sessionManager.getSessionId());
19145
19232
  runtime.throttleFor(ctx.sessionManager.getSessionId()).reset();
19146
19233
  runtime.clearCompressRetryTracking(ctx.sessionManager.getSessionId());
19234
+ runtime.dropHostUsageSamples(ctx.sessionManager.getSessionId());
19235
+ runtime.dropSizeDivergence(ctx.sessionManager.getSessionId());
19147
19236
  resetDelegateUsage();
19148
19237
  setDelegateDisplayUsage("separate");
19149
19238
  setDelegatePolicy(DEFAULT_DELEGATE_POLICY);
19150
19239
  const sid = ctx.sessionManager.getSessionId();
19151
19240
  const modelInfo = ctx.model;
19152
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.70" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
19241
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.71" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
19242
+ let delegateStoodDown = false;
19153
19243
  try {
19154
19244
  await runtime.reloadConfig(ctx.cwd);
19155
19245
  const delegateCfg = resolveDelegate(runtime.adapter);
@@ -19157,9 +19247,24 @@ function wireSessionLifecycle(pi, runtime, standDownIfProxied) {
19157
19247
  setDelegatePolicy(delegateCfg);
19158
19248
  setDelegateDefaults({ thinkingLevel: delegateCfg.thinkingLevel, agents: delegateCfg.agents });
19159
19249
  setDelegateNotifyIfRead(delegateCfg.notifyIfRead);
19250
+ if (delegateCfg.enabled && !delegateCfg.forceEnable) {
19251
+ const scopes = findPiSubagentsInstalls(resolveAgentDir(), ctx.cwd ?? process.cwd());
19252
+ if (scopes.project[0] !== void 0) {
19253
+ delegateStoodDown = true;
19254
+ logWarn("delegate", { event: "delegate-auto-disabled", sid, install: scopes.project[0], scope: "project", hint: "run /acp-subagents to give its agents ACP compression tools; delegate.forceEnable=true keeps acp_delegate" });
19255
+ if (!subagentStandDownWarned) {
19256
+ subagentStandDownWarned = true;
19257
+ if (ctx.hasUI) ctx.ui.notify(DELEGATE_STAND_DOWN_MESSAGE, "warning");
19258
+ else console.error(DELEGATE_STAND_DOWN_MESSAGE);
19259
+ }
19260
+ } else if (scopes.user[0] !== void 0) {
19261
+ logWarn("delegate", { event: "delegate-user-scope-detected", sid, install: scopes.user[0], action: "warn-only", hint: "user-level pi-subagents does not disable acp_delegate; run /acp-subagents to give its agents ACP compression tools" });
19262
+ }
19263
+ }
19160
19264
  } catch (e) {
19161
19265
  logThrow("config", e, { sid, phase: "session_start" });
19162
19266
  }
19267
+ runtime.delegateStoodDown = delegateStoodDown;
19163
19268
  try {
19164
19269
  runtime.setPrompts(resolvePrompts(runtime.adapter.prompts, { acknowledgeRisk: runtime.adapter.acknowledgePromptsRisk === true }));
19165
19270
  } catch (e) {
@@ -19167,7 +19272,7 @@ function wireSessionLifecycle(pi, runtime, standDownIfProxied) {
19167
19272
  runtime.setPrompts(defaultPrompts);
19168
19273
  }
19169
19274
  const delegatePolicy2 = resolveDelegate(runtime.adapter);
19170
- if (delegatePolicy2.enabled) {
19275
+ if (delegatePolicy2.enabled && !runtime.delegateStoodDown) {
19171
19276
  pi.registerTool(makeDelegateTool(pi));
19172
19277
  pi.registerTool(makeDelegateWaitTool(pi));
19173
19278
  pi.registerTool(makeDelegateCancelTool(pi));
@@ -19192,6 +19297,8 @@ function wireSessionLifecycle(pi, runtime, standDownIfProxied) {
19192
19297
  runtime.dropTokenScale(sid);
19193
19298
  runtime.clearNudgeTracking(sid);
19194
19299
  runtime.clearCompressRetryTracking(sid);
19300
+ runtime.dropHostUsageSamples(sid);
19301
+ runtime.dropSizeDivergence(sid);
19195
19302
  delegateStatusWidget.dispose();
19196
19303
  closeLogStream();
19197
19304
  });
@@ -19235,7 +19342,9 @@ function wireContextTransform(pi, runtime, standDownIfProxied) {
19235
19342
  }
19236
19343
  const hostFloorActive = !usageAnchorPredatesCompression(entries);
19237
19344
  const realPromptTokens = realUsage?.tokens ?? 0;
19238
- const applyFloors = (base) => Math.max(base, hostFloorActive ? realPromptTokens : 0, armedFloor);
19345
+ const hostUsageStable = hostFloorActive && realPromptTokens > 0 ? runtime.noteHostUsage(sid, realPromptTokens) : false;
19346
+ const calibrate = (base) => hostUsageStable ? Math.min(base, Math.ceil(realPromptTokens * 1.2)) : base;
19347
+ const applyFloors = (base) => Math.max(calibrate(base), hostFloorActive ? realPromptTokens : 0, armedFloor);
19239
19348
  let tokenCount = applyFloors(sentTokens);
19240
19349
  if (state.blocks.some((b2) => b2.active && b2.effectiveMessageIds.length > 0)) {
19241
19350
  const view = sentViewTokenCount(runtime.core, coreMessages, state, config, tokenCount, imageTokens, systemPromptTokens);
@@ -19244,12 +19353,20 @@ function wireContextTransform(pi, runtime, standDownIfProxied) {
19244
19353
  logInfo("turn", { sid, event: "view-recount", prelim: sentTokens, viewTokens: view.viewTokens, tokenCount });
19245
19354
  }
19246
19355
  }
19356
+ const sizeDivergent = hostFloorActive && realPromptTokens > 0 && Math.abs(tokenCount - realPromptTokens) / realPromptTokens > 0.5;
19357
+ if (runtime.noteSizeDivergence(sid, sizeDivergent)) {
19358
+ logWarn("turn", { sid, event: "size-divergence", est: tokenCount, host: realPromptTokens, ratio: Number((tokenCount / realPromptTokens).toFixed(2)), stable: hostUsageStable });
19359
+ }
19247
19360
  if (runtime.noteTokenScale(sid, !hostFloorActive)) {
19248
- state.nudge.lastNudgeShownTokens = 0;
19249
- state.nudge.lastPerMessageNudgeTokens = 0;
19250
- state.nudge.lastShownByTier = {};
19361
+ state.nudge.lastNudgeShownTokens = state.nudge.lastNudgeShownTokens > 0 ? tokenCount : 0;
19362
+ state.nudge.lastPerMessageNudgeTokens = state.nudge.lastPerMessageNudgeTokens > 0 ? tokenCount : 0;
19363
+ const reanchored = {};
19364
+ for (const [tier, shown] of Object.entries(state.nudge.lastShownByTier)) {
19365
+ if (shown > 0) reanchored[Number(tier)] = tokenCount;
19366
+ }
19367
+ state.nudge.lastShownByTier = reanchored;
19251
19368
  runtime.clearNudgeTokenStamps(sid);
19252
- logInfo("growth-scale", { sid, event: "scale-flip-reset", anchorStale: !hostFloorActive });
19369
+ logInfo("growth-scale", { sid, event: "scale-flip-reanchor", anchorStale: !hostFloorActive, tokenCount });
19253
19370
  }
19254
19371
  debug.event("context-in", {
19255
19372
  sid,
@@ -19330,8 +19447,9 @@ function wireContextTransform(pi, runtime, standDownIfProxied) {
19330
19447
  const debugOn2 = debug.enabled;
19331
19448
  const turnPolicy = resolveHostSession(runtime.adapter);
19332
19449
  const turnKey = lastTurnBoundaryId(entries, turnPolicy) ?? sid;
19450
+ const retryTurnKey = retryBreakerKey(ctx.sessionManager, turnPolicy) ?? sid;
19333
19451
  const compressOutcomes = collectCompressOutcomes(entries, lastTurnBoundaryIndex(entries, turnPolicy));
19334
- const outcome = compressOutcomes.length > 0 ? runtime.noteCompressOutcomes(sid, turnKey, compressOutcomes) : null;
19452
+ const outcome = compressOutcomes.length > 0 ? runtime.noteCompressOutcomes(sid, retryTurnKey, compressOutcomes) : null;
19335
19453
  const adaptiveGrowth = !config.modelContextLimit || config.modelContextLimit <= 0 ? config.nudge.growthFloor : Math.min(
19336
19454
  config.nudge.growthCap,
19337
19455
  Math.max(config.nudge.growthFloor, Math.round(config.modelContextLimit * config.nudge.growthRatio))
@@ -19346,7 +19464,7 @@ function wireContextTransform(pi, runtime, standDownIfProxied) {
19346
19464
  if (turn.nudge?.shouldInject) {
19347
19465
  const emergency = turn.nudge.breakdown?.emergencyOverride === 1;
19348
19466
  turn.nudge.compressibleRanges = viableRanges(turn.nudge.compressibleRanges);
19349
- const retryCapped = runtime.compressRetryCappedFor(sid, turnKey);
19467
+ const retryCapped = runtime.compressRetryCappedFor(sid, retryTurnKey);
19350
19468
  const reInjectReady = shownAt === void 0 || tokenCount - shownAt >= reInjectFloor;
19351
19469
  const alreadyShown = retryCapped || !emergency && runtime.nudgeShownFor(sid, turnKey) && !reInjectReady;
19352
19470
  if (!alreadyShown) {
@@ -19414,7 +19532,7 @@ function wireSystemPrompt(pi, runtime) {
19414
19532
  }
19415
19533
  runtime.setPrompts(defaultPrompts);
19416
19534
  }
19417
- const delegate = resolveDelegate(runtime.adapter).enabled;
19535
+ const delegate = resolveDelegate(runtime.adapter).enabled && !runtime.delegateStoodDown;
19418
19536
  const acp = buildAcpSystemPrompt(runtime.prompts, merged.promptSections);
19419
19537
  const delegateText = merged.delegatePrompt !== void 0 ? merged.delegatePrompt : ACP_DELEGATE_PROMPT;
19420
19538
  const prompt = delegate && delegateText !== null ? `${acp}