billion-context-pi 0.1.75 → 0.1.76-pr.512.330

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
@@ -10,6 +10,14 @@ The model decides <em>when</em> and <em>what</em> to compress — not a hard lim
10
10
 
11
11
  ---
12
12
 
13
+ ## Community
14
+
15
+ QQ Group:
16
+ 1056132097 (full)
17
+ 1108730198 (open)
18
+
19
+ ---
20
+
13
21
  ## 📄 Paper / Preprint
14
22
 
15
23
  - **[Model-Driven Incremental Hierarchical Compression: Training-Free Multi-Generational Context Management for Long-Lived Coding Agents](./paper/model-driven-incremental-hierarchical-compression-training-free-multi-generational-context-management-for-long-lived-coding-agents.md)** (English, v0.2)
@@ -34,12 +42,6 @@ A production-scale longitudinal study: 4.5 months, three hosts, 174,327 model ca
34
42
 
35
43
  > **Host support:** this plugin is for **Pi**. It does **not** support **OMP (oh-my-pi)** — on an OMP host it refuses to run. OMP users: use [billion-context](https://github.com/ranxianglei/billion-context) instead (`bili omp`, built-in plugin). Full client → package table: see [Which do I need?](#which-do-i-need); OMP details: [docs/omp.md](./docs/omp.md).
36
44
 
37
- ## Community
38
-
39
- Discussion, help, and updates on QQ — one group covers all three projects (`billion-context`, `billion-context-pi`, `opencode-acp`):
40
-
41
- **QQ Group: 1056132097**
42
-
43
45
  ## Why?
44
46
 
45
47
  When conversations get long, the model runs out of context. Most tools hard-truncate — silently dropping earlier messages. **billion-context** gives the model a `compress` tool: the LLM decides **when** and **what** to compress into high-fidelity summaries, preserving critical details (file paths, decisions, error strings) while reclaiming context space.
package/README.zh-CN.md CHANGED
@@ -8,6 +8,14 @@
8
8
 
9
9
  ---
10
10
 
11
+ ## 社区
12
+
13
+ QQ群:
14
+ 1056132097(已满)
15
+ 1108730198(未满)
16
+
17
+ ---
18
+
11
19
  ## 📄 论文 / 预印本
12
20
 
13
21
  - **[模型驱动的分层增量压缩:面向长寿命编码 Agent 的免训练多代上下文管理](./paper/模型驱动的分层增量压缩-免训练多代上下文管理.md)**(中文版,v0.2)
@@ -32,12 +40,6 @@
32
40
 
33
41
  > **宿主支持:** 本插件面向 **Pi**。它**不支持 OMP(oh-my-pi)** —— 在 OMP 宿主上会拒绝运行。OMP 用户请直接改用 [billion-context](https://github.com/ranxianglei/billion-context)(启动命令 bili omp);其他客户端的完整对照见[该选哪个?](#该选哪个)。OMP 详细说明:[docs/omp.zh-CN.md](./docs/omp.zh-CN.md)。
34
42
 
35
- ## 社区
36
-
37
- 交流、求助与更新都在 QQ——同一个群覆盖三个项目(`billion-context`、`billion-context-pi`、`opencode-acp`):
38
-
39
- **QQ 群:1056132097**
40
-
41
43
  ## 为什么选择 billion-context
42
44
 
43
45
  当对话变长,模型的上下文会耗尽。多数工具采用硬截断 —— 静默丢弃早期消息。**billion-context** 把 `compress` 工具交给模型:由 LLM 决定**何时**压缩、压缩**什么**,将内容压缩成高保真摘要,在回收上下文空间的同时保留关键细节(文件路径、决策、错误字符串)。
@@ -7,7 +7,7 @@ var __export = (target, all) => {
7
7
  // src/contract.ts
8
8
  var SIDECAR_SCHEMA_VERSION = 1;
9
9
  function sidecarProducer() {
10
- return `billion-context-pi@${true ? "0.1.75" : "dev"}`;
10
+ return `billion-context-pi@${true ? "0.1.76" : "dev"}`;
11
11
  }
12
12
 
13
13
  export {
@@ -15,4 +15,4 @@ export {
15
15
  SIDECAR_SCHEMA_VERSION,
16
16
  sidecarProducer
17
17
  };
18
- //# sourceMappingURL=chunk-FTCH7KHF.js.map
18
+ //# sourceMappingURL=chunk-44F6OZJV.js.map
package/dist/contract.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SIDECAR_SCHEMA_VERSION,
3
3
  sidecarProducer
4
- } from "./chunk-FTCH7KHF.js";
4
+ } from "./chunk-44F6OZJV.js";
5
5
  export {
6
6
  SIDECAR_SCHEMA_VERSION,
7
7
  sidecarProducer
@@ -278,10 +278,26 @@ export declare function buildChildArgs(args: DelegateArgs, rolePrompt: string, c
278
278
  useJsonStream: boolean;
279
279
  sessionFile: string | null;
280
280
  }>;
281
+ interface ChildResult {
282
+ code: number | null;
283
+ signal?: NodeJS.Signals | null;
284
+ stdout: string;
285
+ stderr: string;
286
+ timedOut: boolean;
287
+ }
281
288
  /** Model-facing summary of the ACTIVE async watchdog limits — built from the
282
289
  * resolved policy so it stays truthful when timeouts are customized or
283
290
  * disabled via acp.json/env. */
284
291
  export declare function asyncWatchdogDescription(overrideAsyncMs?: number | null): string;
292
+ export declare function formatSyncResult(agent: string, runId: string, task: string, r: ChildResult, file: string): string;
293
+ /** Exit-0 runs that produced no final reply text did not deliver anything
294
+ * actionable (#506): a thinking-only stop (thinking budget exhausted) or a
295
+ * silent no-op termination must not be announced as a plain completion — the
296
+ * parent would trust the notification and never re-dispatch the task. Real
297
+ * non-zero exits and watchdog kills (code null / timedOut) are handled by the
298
+ * existing failure paths and stay excluded here. */
299
+ export declare function genuineNoOutput(code: number | null, timedOut: boolean, output: string): boolean;
300
+ export declare const NO_FINAL_OUTPUT_BODY: string;
285
301
  /** Watchdog/EOF finalize arrives with code === null (the child was killed or
286
302
  * never exited). If a result was delivered (non-empty reply or stderr), the
287
303
  * run counts as completed (0); otherwise it stays null = genuine failure. */
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  SIDECAR_SCHEMA_VERSION,
3
3
  __export,
4
4
  sidecarProducer
5
- } from "./chunk-FTCH7KHF.js";
5
+ } from "./chunk-44F6OZJV.js";
6
6
 
7
7
  // src/config-dir.ts
8
8
  import * as piModule from "@earendil-works/pi-coding-agent";
@@ -2599,18 +2599,30 @@ function mergeRangesToThreshold(ranges, minChars) {
2599
2599
  const result = [];
2600
2600
  let batch = [];
2601
2601
  let batchChars = 0;
2602
- for (const r of ranges) {
2603
- batch.push(r);
2604
- batchChars += rangeChars(r);
2605
- if (batchChars >= minChars) {
2606
- result.push(mergeBatch(batch));
2602
+ const closeBatch = () => {
2603
+ if (batch.length > 0) {
2604
+ if (batchChars >= minChars) result.push(mergeBatch(batch));
2607
2605
  batch = [];
2608
2606
  batchChars = 0;
2609
2607
  }
2608
+ };
2609
+ for (const r of ranges) {
2610
+ const prev = batch[batch.length - 1];
2611
+ if (prev && prev.endIndex !== void 0 && r.startIndex !== void 0 && r.startIndex > prev.endIndex + 1) {
2612
+ closeBatch();
2613
+ }
2614
+ batch.push(r);
2615
+ batchChars += rangeChars(r);
2616
+ if (batchChars >= minChars) closeBatch();
2610
2617
  }
2611
2618
  if (batch.length > 0 && result.length > 0) {
2612
2619
  const prev = result[result.length - 1];
2613
- result[result.length - 1] = mergeBatch([prev, ...batch]);
2620
+ const contiguous = prev.endIndex === void 0 || batch[0].startIndex === void 0 || batch[0].startIndex <= prev.endIndex + 1;
2621
+ if (contiguous) {
2622
+ result[result.length - 1] = mergeBatch([prev, ...batch]);
2623
+ } else if (batchChars >= minChars) {
2624
+ result.push(mergeBatch(batch));
2625
+ }
2614
2626
  }
2615
2627
  return result;
2616
2628
  }
@@ -3127,10 +3139,29 @@ function applySingleRange(input) {
3127
3139
  messages: input.messages,
3128
3140
  state: input.state
3129
3141
  });
3130
- const rangeMessageIds = applyPairBoundaryAdjustments(
3131
- resolved,
3132
- input.messages
3133
- ).filter((id) => !isSummaryMessageId(id));
3142
+ const plainRange = resolved.boundaryKind !== "block";
3143
+ const liveCarrierIds = /* @__PURE__ */ new Set();
3144
+ if (plainRange) {
3145
+ for (const message of input.messages) {
3146
+ const carrierOf = message.summaryOfBlockId;
3147
+ if (carrierOf === void 0) continue;
3148
+ if (blockById(input.state, carrierOf)?.active) {
3149
+ liveCarrierIds.add(message.id);
3150
+ }
3151
+ }
3152
+ }
3153
+ const adjustedIds = applyPairBoundaryAdjustments(resolved, input.messages);
3154
+ const skippedCarriers = adjustedIds.filter((id) => liveCarrierIds.has(id));
3155
+ if (skippedCarriers.length > 0) {
3156
+ warnings.push(
3157
+ `Excluded ${skippedCarriers.length} checkpoint message(s) ${skippedCarriers.join(
3158
+ ", "
3159
+ )} from the compression range \u2014 they carry the visible summary of still-active block(s), which a plain message-ref range does not supersede. The checkpoints stay visible; to fold them, reference the block ids (bN..bM) instead.`
3160
+ );
3161
+ }
3162
+ const rangeMessageIds = adjustedIds.filter(
3163
+ (id) => !isSummaryMessageId(id) && !liveCarrierIds.has(id)
3164
+ );
3134
3165
  if (rangeMessageIds.length > resolved.messageIds.length) {
3135
3166
  const indexByMessageId = /* @__PURE__ */ new Map();
3136
3167
  input.messages.forEach((m2, i) => indexByMessageId.set(m2.id, i));
@@ -12729,7 +12760,7 @@ function resolveActivePack(adapter, cwd, provider, modelId, resolver) {
12729
12760
  }
12730
12761
  var HOST_VERSION = (() => {
12731
12762
  try {
12732
- return "0.1.75";
12763
+ return "0.1.76";
12733
12764
  } catch {
12734
12765
  return "dev";
12735
12766
  }
@@ -14271,9 +14302,19 @@ async function runDelegate(pi, args, ctx, signal) {
14271
14302
  run.exitCode = code;
14272
14303
  run.exitSignal = signal2 ?? void 0;
14273
14304
  const output = applier.getReplyText().trim();
14305
+ const noOutput = genuineNoOutput(code, Boolean(run.timedOut), output);
14274
14306
  let body2;
14275
- if (code === 0) {
14307
+ if (code === 0 && !noOutput) {
14276
14308
  body2 = output || "(no output)";
14309
+ } else if (noOutput) {
14310
+ const parts = [NO_FINAL_OUTPUT_BODY];
14311
+ const err = stderrText.trim();
14312
+ if (err) parts.push(`stderr:
14313
+ ${err}`);
14314
+ const tail = activityStream ? await readActivityTail(activityFile) : "";
14315
+ if (tail) parts.push(`last activity (full log: \`${activityFile}\`)
14316
+ ${tail}`);
14317
+ body2 = parts.join("\n\n");
14277
14318
  } else {
14278
14319
  const parts = [];
14279
14320
  const err = stderrText.trim();
@@ -14302,13 +14343,22 @@ ${output}`);
14302
14343
  try {
14303
14344
  const file2 = replyFile;
14304
14345
  if (output === "") {
14305
- const fallback = stderrText.trim();
14306
- await appendFile(file2, fallback ? `${fallback}
14346
+ if (noOutput) {
14347
+ const err = stderrText.trim();
14348
+ await appendFile(file2, `${NO_FINAL_OUTPUT_BODY}
14349
+ ${err ? `
14350
+ ${err}` : ""}
14351
+ `);
14352
+ } else {
14353
+ const fallback = stderrText.trim();
14354
+ await appendFile(file2, fallback ? `${fallback}
14307
14355
  ` : "(no output)\n");
14356
+ }
14308
14357
  }
14309
14358
  const effectiveCode = effectiveExitCode(code, output, stderrText);
14310
14359
  run.result = { code, file: file2, body: body2 };
14311
- run.status = effectiveCode === 0 ? "completed" : "failed";
14360
+ run.status = effectiveCode === 0 && !noOutput ? "completed" : "failed";
14361
+ if (noOutput) logWarn("delegate", { event: "no-final-output", runId, agent: args.agent, code });
14312
14362
  run.finishedAt = Date.now();
14313
14363
  if (run.waiter) {
14314
14364
  debug.event("delegate-done", { runId, code, status: run.status, injected: false, via: "wait", outLen: output.length, file: file2 });
@@ -14400,7 +14450,19 @@ ${output}`);
14400
14450
  });
14401
14451
  const result = await waitForChild(child, signal, effectiveSyncMs);
14402
14452
  void cleanupTmp(tmpDir);
14403
- const body = result.timedOut || result.code !== 0 ? result.stderr.trim() || "(no stderr)" : result.stdout || "(no output)";
14453
+ const syncNoOutput = genuineNoOutput(result.code, result.timedOut, result.stdout);
14454
+ let body;
14455
+ if (result.timedOut || result.code !== 0) {
14456
+ body = result.stderr.trim() || "(no stderr)";
14457
+ } else if (syncNoOutput) {
14458
+ const err = result.stderr.trim();
14459
+ body = err ? `${NO_FINAL_OUTPUT_BODY}
14460
+
14461
+ stderr:
14462
+ ${err}` : NO_FINAL_OUTPUT_BODY;
14463
+ } else {
14464
+ body = result.stdout;
14465
+ }
14404
14466
  const file = await persistResult(runId, body);
14405
14467
  return formatSyncResult(args.agent, runId, taskText, result, file);
14406
14468
  }
@@ -14528,14 +14590,19 @@ function asyncWatchdogDescription(overrideAsyncMs) {
14528
14590
  return `A watchdog force-finishes a hung run: ${joined} \u2014 the result reflects whatever was produced.`;
14529
14591
  }
14530
14592
  function formatSyncResult(agent, runId, task, r, file) {
14531
- const status = r.timedOut ? "timed out" : r.code === 0 ? "completed" : "FAILED \u26A0\uFE0F";
14593
+ const noOutput = genuineNoOutput(r.code, r.timedOut, r.stdout);
14594
+ const status = r.timedOut ? "timed out" : r.code === 0 && !noOutput ? "completed" : "FAILED \u26A0\uFE0F";
14532
14595
  const header = `Delegate **${agent}** ${status} (runId \`${runId}\`, ${exitLabel(r.code, r.signal)}).`;
14533
- if (r.code === 0 && !r.timedOut) {
14596
+ if (r.code === 0 && !r.timedOut && !noOutput) {
14534
14597
  return formatPayload(header, file, task);
14535
14598
  }
14536
- const body = r.timedOut ? "(timed out)" : r.stderr.trim() || "(no stderr)";
14599
+ const body = r.timedOut ? "(timed out)" : r.code === 0 ? NO_FINAL_OUTPUT_BODY : r.stderr.trim() || "(no stderr)";
14537
14600
  return formatPayload(header, file, task, body);
14538
14601
  }
14602
+ function genuineNoOutput(code, timedOut, output) {
14603
+ return !timedOut && code === 0 && output.trim().length === 0;
14604
+ }
14605
+ var NO_FINAL_OUTPUT_BODY = "child exited 0 but produced NO final reply text \u2014 likely a thinking-only stop (thinking budget exhausted) or a no-op termination. Treat the task as NOT delivered: inspect the activity log and session file, then re-dispatch if needed.";
14539
14606
  function effectiveExitCode(code, output, stderr) {
14540
14607
  return code ?? (output || stderr ? 0 : null);
14541
14608
  }
@@ -19477,7 +19544,7 @@ async function statusReport(runtime, ctx) {
19477
19544
  const sentTokens = estimateTokens(coreMessages, coveredIds, imageTokens) + systemPromptTokens;
19478
19545
  const viewSentTokens = adjustedTokenCount(runtime.core, coreMessages, state, config, sentTokens, imageTokens, systemPromptTokens);
19479
19546
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: anchorStale ? viewSentTokens : Math.max(viewSentTokens, realUsage?.tokens ?? 0) });
19480
- const versionStr = "0.1.75" ? `billion-context-pi@${"0.1.75"} \xB7 pack: ${resolveSurfaceMeta(runtime.adapter, ctx?.cwd ?? process.cwd(), ctx?.model?.provider, ctx?.model?.id).pack}` : void 0;
19547
+ const versionStr = "0.1.76" ? `billion-context-pi@${"0.1.76"} \xB7 pack: ${resolveSurfaceMeta(runtime.adapter, ctx?.cwd ?? process.cwd(), ctx?.model?.provider, ctx?.model?.id).pack}` : void 0;
19481
19548
  let text = buildStatusPanel({
19482
19549
  version: versionStr,
19483
19550
  tokenCount: sessionTokens,
@@ -19586,6 +19653,41 @@ function carryHostSystemMessages(rebuilt, input) {
19586
19653
  return changed ? patched : rebuilt;
19587
19654
  }
19588
19655
 
19656
+ // src/tool-pair-sanitizer.ts
19657
+ function sanitizeToolPairing(messages) {
19658
+ const empty = { messages, droppedResults: [] };
19659
+ if (!Array.isArray(messages) || messages.length === 0) return empty;
19660
+ try {
19661
+ const callIds = /* @__PURE__ */ new Set();
19662
+ for (const raw of messages) {
19663
+ const msg = raw;
19664
+ if (!msg || typeof msg !== "object" || msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
19665
+ for (const b2 of msg.content) {
19666
+ if (b2 && b2.type === "toolCall" && typeof b2.id === "string" && b2.id.length > 0) callIds.add(b2.id);
19667
+ }
19668
+ }
19669
+ let changed = false;
19670
+ const out = [];
19671
+ const droppedResults = [];
19672
+ for (const raw of messages) {
19673
+ const msg = raw;
19674
+ if (msg && typeof msg === "object" && msg.role === "toolResult") {
19675
+ const tcid = msg.toolCallId;
19676
+ const label = typeof tcid === "string" && tcid.length > 0 ? tcid : "(missing toolCallId)";
19677
+ if (typeof tcid !== "string" || tcid.length === 0 || !callIds.has(tcid)) {
19678
+ droppedResults.push(label);
19679
+ changed = true;
19680
+ continue;
19681
+ }
19682
+ }
19683
+ out.push(raw);
19684
+ }
19685
+ return changed ? { messages: out, droppedResults } : empty;
19686
+ } catch {
19687
+ return empty;
19688
+ }
19689
+ }
19690
+
19589
19691
  // src/degeneration.ts
19590
19692
  var DEFAULT_DEGENERATION_GUARD = { enabled: true, minRun: 200 };
19591
19693
  var MIN_VALID_MIN_RUN = 8;
@@ -20317,7 +20419,7 @@ async function autoInstallLatest(latest, extDirOverride) {
20317
20419
  "--no-fund",
20318
20420
  "--no-save"
20319
20421
  ];
20320
- const prevVersion = (await readPackageJson(join15(extDir, "package.json")))?.version ?? "0.1.75";
20422
+ const prevVersion = (await readPackageJson(join15(extDir, "package.json")))?.version ?? "0.1.76";
20321
20423
  const { code, stderr } = await runNpmImpl(installArgs(latest), { cwd: npmDir, timeout: 6e4 });
20322
20424
  if (code !== 0) {
20323
20425
  logWarn("update", {
@@ -20335,7 +20437,7 @@ async function autoInstallLatest(latest, extDirOverride) {
20335
20437
  }
20336
20438
  const verify = await verifyInstall(npmDir, latest);
20337
20439
  if (!verify.ok) {
20338
- const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.75";
20440
+ const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.76";
20339
20441
  logWarn("update", { event: "auto-install-verify-failed", latest, reason: verify.reason, rollbackTo });
20340
20442
  const rb = await runNpmImpl(installArgs(rollbackTo), { cwd: npmDir, timeout: 6e4 });
20341
20443
  logInfo("update", { event: "rollback", from: latest, to: rollbackTo, ok: rb.code === 0 });
@@ -20410,7 +20512,7 @@ async function checkForUpdate(autoUpdate, notify) {
20410
20512
  }
20411
20513
  const latest = await fetchLatestVersion(tag);
20412
20514
  if (!latest) return;
20413
- const current = runtimeVersion ?? "0.1.75";
20515
+ const current = runtimeVersion ?? "0.1.76";
20414
20516
  const hasUpdate = isVersionNewer(latest, current);
20415
20517
  debug.event("update-check", {
20416
20518
  current,
@@ -20629,7 +20731,7 @@ function wireSessionLifecycle(pi, runtime, standDownIfProxied) {
20629
20731
  setDelegatePolicy(DEFAULT_DELEGATE_POLICY);
20630
20732
  const sid = ctx.sessionManager.getSessionId();
20631
20733
  const modelInfo = ctx.model;
20632
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.75" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
20734
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.76" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
20633
20735
  let delegateStoodDown = false;
20634
20736
  try {
20635
20737
  await runtime.reloadConfig(ctx.cwd);
@@ -20940,6 +21042,13 @@ ${rendered.text}${example}`);
20940
21042
  rebuilt = withHostSystem;
20941
21043
  logInfo("system-passthrough", { sid, event: "carried", systems: event.messages.filter((m2) => m2.role === "system").length, outMsgs: rebuilt.length });
20942
21044
  }
21045
+ if (turn.state.blocks.length > 0) {
21046
+ const sanitized = sanitizeToolPairing(rebuilt);
21047
+ if (sanitized.droppedResults.length > 0) {
21048
+ rebuilt = sanitized.messages;
21049
+ logWarn("tool-pair-sanitize", { sid, event: "dropped-orphan-results", count: sanitized.droppedResults.length, droppedResults: sanitized.droppedResults });
21050
+ }
21051
+ }
20943
21052
  debug.event("context-out", { outMsgs: rebuilt.length, injected: turn.nudge?.shouldInject ?? false, emergency: turn.nudge?.breakdown?.emergencyOverride === 1 });
20944
21053
  const updateCheck = checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
20945
21054
  if (ctx.hasUI) ctx.ui.notify(msg);