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

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.
@@ -251,11 +253,12 @@ The model receives detailed guidance (in its system prompt) on **when** to compr
251
253
 
252
254
  ### What gets protected
253
255
 
254
- billion-context protects three categories of content from compression:
256
+ billion-context protects four categories of content from compression:
255
257
 
256
258
  1. **Always-protected tools** — `compress` calls are hard-protected (they're load-bearing metadata; compressing them breaks decompress and the "summary is historical" contract).
257
259
  2. **Soft recent-zone** — the last N messages (default 5) and last ~5K tokens are soft-protected so the model keeps its working set. Tool results from `decompress`, `search_context`, `read`, and `bash` are **excluded** from this zone: they're large and meant to be compressible once consumed, so they don't eat the protected budget.
258
260
  3. **Last user message** — always protected (user intent must survive).
261
+ 4. **User-configured tool protection** — `protectedTools` / `protectedLatestTools` in `acp.json` hard-exclude matched tool calls+results (full history vs. latest-only; see [CONFIGURATION.md](./CONFIGURATION.md)).
259
262
 
260
263
  ## Session storage & migration
261
264
 
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 决定**何时**压缩、压缩**什么**,将内容压缩成高保真摘要,在回收上下文空间的同时保留关键细节(文件路径、决策、错误字符串)。
@@ -240,11 +242,12 @@ grep '\[error\]' ~/.pi/acp.log # 汇总所有记录的失败
240
242
 
241
243
  ### 哪些内容会被保护
242
244
 
243
- billion-context 保护三类内容不被压缩:
245
+ billion-context 保护四类内容不被压缩:
244
246
 
245
247
  1. **永久保护的工具** — `compress` 调用被硬保护(它们是承载关键元数据的;压缩它们会破坏 decompress 和"摘要是历史"的契约)。
246
248
  2. **软近期区** — 最后 N 条消息(默认 5)和最后约 5K token 被软保护,让模型保留工作集。来自 `decompress`、`search_context`、`read`、`bash` 的工具结果被**排除**出此区:它们体量大、消费后就该能压缩,所以不该占用保护预算。
247
249
  3. **最后一条用户消息** — 始终保护(用户意图必须存活)。
250
+ 4. **用户配置的工具保护** — `acp.json` 中的 `protectedTools` / `protectedLatestTools` 硬排除匹配的工具 call+result(全历史 vs 仅最近一次;见 [CONFIGURATION.zh-CN.md](./CONFIGURATION.zh-CN.md))。
248
251
 
249
252
  ## 会话存储与迁移
250
253
 
@@ -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/config.d.ts CHANGED
@@ -216,7 +216,18 @@ export interface AdapterConfig {
216
216
  /** When omitted, the adapter reads `ctx.model.contextWindow` live each turn.
217
217
  * Set explicitly for tests/headless runs. */
218
218
  modelContextLimit?: number;
219
+ /** Tool-name patterns (glob suffix allowed) whose EVERY call+result pair is
220
+ * hard-excluded from compression — matching refs render as BLOCKED in every
221
+ * view. Default: none. Intended for low-frequency high-value tools whose
222
+ * outputs are independent content (e.g. skill loads); do NOT use for chatty
223
+ * tools — protecting every instance grows context unboundedly (#639
224
+ * rationale). Settable via acp.json since #499. */
219
225
  protectedTools?: string[];
226
+ /** Tool-name patterns (glob suffix allowed) whose LATEST call+result pair is
227
+ * hard-excluded from compression; older pairs remain compressible. Default:
228
+ * none. Intended for cumulative-snapshot tools where each call supersedes
229
+ * the last. Settable via acp.json since #499. */
230
+ protectedLatestTools?: string[];
220
231
  preserveRecentMessages?: number;
221
232
  /** Check npm for a newer billion-context-pi on startup and auto-install it. Default: true.
222
233
  * Disable via `autoUpdate: false` or env `ACP_AUTO_UPDATE=0` to avoid all
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";
@@ -170,6 +170,8 @@ var KNOWN = /* @__PURE__ */ new Set([
170
170
  "debug",
171
171
  "autoUpdate",
172
172
  "modelContextLimit",
173
+ "protectedTools",
174
+ "protectedLatestTools",
173
175
  "toolBashDefaultTimeout",
174
176
  "toolOutputMaxBytes",
175
177
  "delegate",
@@ -195,13 +197,27 @@ function pickKnown(parsed) {
195
197
  return out;
196
198
  }
197
199
  function applyUserConfig(adapter, user) {
198
- return {
200
+ const result = {
199
201
  ...adapter,
200
202
  ...user,
201
203
  coreOverrides: adapter.coreOverrides,
202
- protectedTools: adapter.protectedTools,
203
204
  preserveRecentMessages: adapter.preserveRecentMessages
204
205
  };
206
+ for (const key of ["protectedTools", "protectedLatestTools"]) {
207
+ if (!(key in user)) continue;
208
+ const cleaned = cleanProtectionList(key, user[key]);
209
+ if (cleaned !== void 0) result[key] = cleaned;
210
+ else if (adapter[key] !== void 0) result[key] = adapter[key];
211
+ else delete result[key];
212
+ }
213
+ return result;
214
+ }
215
+ function cleanProtectionList(key, value) {
216
+ if (!Array.isArray(value) || value.length === 0 || !value.every((v2) => typeof v2 === "string" && v2.trim() !== "")) {
217
+ console.warn(`[bcp] acp.json "${key}" must be a non-empty array of non-empty strings \u2014 ignoring the value`);
218
+ return void 0;
219
+ }
220
+ return value.map((v2) => v2.trim());
205
221
  }
206
222
 
207
223
  // src/index.ts
@@ -2599,18 +2615,30 @@ function mergeRangesToThreshold(ranges, minChars) {
2599
2615
  const result = [];
2600
2616
  let batch = [];
2601
2617
  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));
2618
+ const closeBatch = () => {
2619
+ if (batch.length > 0) {
2620
+ if (batchChars >= minChars) result.push(mergeBatch(batch));
2607
2621
  batch = [];
2608
2622
  batchChars = 0;
2609
2623
  }
2624
+ };
2625
+ for (const r of ranges) {
2626
+ const prev = batch[batch.length - 1];
2627
+ if (prev && prev.endIndex !== void 0 && r.startIndex !== void 0 && r.startIndex > prev.endIndex + 1) {
2628
+ closeBatch();
2629
+ }
2630
+ batch.push(r);
2631
+ batchChars += rangeChars(r);
2632
+ if (batchChars >= minChars) closeBatch();
2610
2633
  }
2611
2634
  if (batch.length > 0 && result.length > 0) {
2612
2635
  const prev = result[result.length - 1];
2613
- result[result.length - 1] = mergeBatch([prev, ...batch]);
2636
+ const contiguous = prev.endIndex === void 0 || batch[0].startIndex === void 0 || batch[0].startIndex <= prev.endIndex + 1;
2637
+ if (contiguous) {
2638
+ result[result.length - 1] = mergeBatch([prev, ...batch]);
2639
+ } else if (batchChars >= minChars) {
2640
+ result.push(mergeBatch(batch));
2641
+ }
2614
2642
  }
2615
2643
  return result;
2616
2644
  }
@@ -3127,10 +3155,29 @@ function applySingleRange(input) {
3127
3155
  messages: input.messages,
3128
3156
  state: input.state
3129
3157
  });
3130
- const rangeMessageIds = applyPairBoundaryAdjustments(
3131
- resolved,
3132
- input.messages
3133
- ).filter((id) => !isSummaryMessageId(id));
3158
+ const plainRange = resolved.boundaryKind !== "block";
3159
+ const liveCarrierIds = /* @__PURE__ */ new Set();
3160
+ if (plainRange) {
3161
+ for (const message of input.messages) {
3162
+ const carrierOf = message.summaryOfBlockId;
3163
+ if (carrierOf === void 0) continue;
3164
+ if (blockById(input.state, carrierOf)?.active) {
3165
+ liveCarrierIds.add(message.id);
3166
+ }
3167
+ }
3168
+ }
3169
+ const adjustedIds = applyPairBoundaryAdjustments(resolved, input.messages);
3170
+ const skippedCarriers = adjustedIds.filter((id) => liveCarrierIds.has(id));
3171
+ if (skippedCarriers.length > 0) {
3172
+ warnings.push(
3173
+ `Excluded ${skippedCarriers.length} checkpoint message(s) ${skippedCarriers.join(
3174
+ ", "
3175
+ )} 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.`
3176
+ );
3177
+ }
3178
+ const rangeMessageIds = adjustedIds.filter(
3179
+ (id) => !isSummaryMessageId(id) && !liveCarrierIds.has(id)
3180
+ );
3134
3181
  if (rangeMessageIds.length > resolved.messageIds.length) {
3135
3182
  const indexByMessageId = /* @__PURE__ */ new Map();
3136
3183
  input.messages.forEach((m2, i) => indexByMessageId.set(m2.id, i));
@@ -5649,6 +5696,7 @@ function resolveConfig(adapter, liveContextLimit, provider, modelId) {
5649
5696
  const limit = !Number.isNaN(envLimitNum) && envLimitNum > 0 ? envLimitNum : adapter.modelContextLimit && adapter.modelContextLimit > 0 ? adapter.modelContextLimit : liveContextLimit > 0 ? liveContextLimit : FALLBACK_LIMIT;
5650
5697
  const config = defaultConfig(limit, {
5651
5698
  protectedTools: adapter.protectedTools ?? [],
5699
+ protectedLatestTools: adapter.protectedLatestTools ?? [],
5652
5700
  preserveRecentMessages: adapter.preserveRecentMessages ?? 5,
5653
5701
  ...adapter.coreOverrides
5654
5702
  });
@@ -12729,7 +12777,7 @@ function resolveActivePack(adapter, cwd, provider, modelId, resolver) {
12729
12777
  }
12730
12778
  var HOST_VERSION = (() => {
12731
12779
  try {
12732
- return "0.1.75";
12780
+ return "0.1.76";
12733
12781
  } catch {
12734
12782
  return "dev";
12735
12783
  }
@@ -14271,9 +14319,19 @@ async function runDelegate(pi, args, ctx, signal) {
14271
14319
  run.exitCode = code;
14272
14320
  run.exitSignal = signal2 ?? void 0;
14273
14321
  const output = applier.getReplyText().trim();
14322
+ const noOutput = genuineNoOutput(code, Boolean(run.timedOut), output);
14274
14323
  let body2;
14275
- if (code === 0) {
14324
+ if (code === 0 && !noOutput) {
14276
14325
  body2 = output || "(no output)";
14326
+ } else if (noOutput) {
14327
+ const parts = [NO_FINAL_OUTPUT_BODY];
14328
+ const err = stderrText.trim();
14329
+ if (err) parts.push(`stderr:
14330
+ ${err}`);
14331
+ const tail = activityStream ? await readActivityTail(activityFile) : "";
14332
+ if (tail) parts.push(`last activity (full log: \`${activityFile}\`)
14333
+ ${tail}`);
14334
+ body2 = parts.join("\n\n");
14277
14335
  } else {
14278
14336
  const parts = [];
14279
14337
  const err = stderrText.trim();
@@ -14302,13 +14360,22 @@ ${output}`);
14302
14360
  try {
14303
14361
  const file2 = replyFile;
14304
14362
  if (output === "") {
14305
- const fallback = stderrText.trim();
14306
- await appendFile(file2, fallback ? `${fallback}
14363
+ if (noOutput) {
14364
+ const err = stderrText.trim();
14365
+ await appendFile(file2, `${NO_FINAL_OUTPUT_BODY}
14366
+ ${err ? `
14367
+ ${err}` : ""}
14368
+ `);
14369
+ } else {
14370
+ const fallback = stderrText.trim();
14371
+ await appendFile(file2, fallback ? `${fallback}
14307
14372
  ` : "(no output)\n");
14373
+ }
14308
14374
  }
14309
14375
  const effectiveCode = effectiveExitCode(code, output, stderrText);
14310
14376
  run.result = { code, file: file2, body: body2 };
14311
- run.status = effectiveCode === 0 ? "completed" : "failed";
14377
+ run.status = effectiveCode === 0 && !noOutput ? "completed" : "failed";
14378
+ if (noOutput) logWarn("delegate", { event: "no-final-output", runId, agent: args.agent, code });
14312
14379
  run.finishedAt = Date.now();
14313
14380
  if (run.waiter) {
14314
14381
  debug.event("delegate-done", { runId, code, status: run.status, injected: false, via: "wait", outLen: output.length, file: file2 });
@@ -14400,7 +14467,19 @@ ${output}`);
14400
14467
  });
14401
14468
  const result = await waitForChild(child, signal, effectiveSyncMs);
14402
14469
  void cleanupTmp(tmpDir);
14403
- const body = result.timedOut || result.code !== 0 ? result.stderr.trim() || "(no stderr)" : result.stdout || "(no output)";
14470
+ const syncNoOutput = genuineNoOutput(result.code, result.timedOut, result.stdout);
14471
+ let body;
14472
+ if (result.timedOut || result.code !== 0) {
14473
+ body = result.stderr.trim() || "(no stderr)";
14474
+ } else if (syncNoOutput) {
14475
+ const err = result.stderr.trim();
14476
+ body = err ? `${NO_FINAL_OUTPUT_BODY}
14477
+
14478
+ stderr:
14479
+ ${err}` : NO_FINAL_OUTPUT_BODY;
14480
+ } else {
14481
+ body = result.stdout;
14482
+ }
14404
14483
  const file = await persistResult(runId, body);
14405
14484
  return formatSyncResult(args.agent, runId, taskText, result, file);
14406
14485
  }
@@ -14528,14 +14607,19 @@ function asyncWatchdogDescription(overrideAsyncMs) {
14528
14607
  return `A watchdog force-finishes a hung run: ${joined} \u2014 the result reflects whatever was produced.`;
14529
14608
  }
14530
14609
  function formatSyncResult(agent, runId, task, r, file) {
14531
- const status = r.timedOut ? "timed out" : r.code === 0 ? "completed" : "FAILED \u26A0\uFE0F";
14610
+ const noOutput = genuineNoOutput(r.code, r.timedOut, r.stdout);
14611
+ const status = r.timedOut ? "timed out" : r.code === 0 && !noOutput ? "completed" : "FAILED \u26A0\uFE0F";
14532
14612
  const header = `Delegate **${agent}** ${status} (runId \`${runId}\`, ${exitLabel(r.code, r.signal)}).`;
14533
- if (r.code === 0 && !r.timedOut) {
14613
+ if (r.code === 0 && !r.timedOut && !noOutput) {
14534
14614
  return formatPayload(header, file, task);
14535
14615
  }
14536
- const body = r.timedOut ? "(timed out)" : r.stderr.trim() || "(no stderr)";
14616
+ const body = r.timedOut ? "(timed out)" : r.code === 0 ? NO_FINAL_OUTPUT_BODY : r.stderr.trim() || "(no stderr)";
14537
14617
  return formatPayload(header, file, task, body);
14538
14618
  }
14619
+ function genuineNoOutput(code, timedOut, output) {
14620
+ return !timedOut && code === 0 && output.trim().length === 0;
14621
+ }
14622
+ 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
14623
  function effectiveExitCode(code, output, stderr) {
14540
14624
  return code ?? (output || stderr ? 0 : null);
14541
14625
  }
@@ -19477,7 +19561,7 @@ async function statusReport(runtime, ctx) {
19477
19561
  const sentTokens = estimateTokens(coreMessages, coveredIds, imageTokens) + systemPromptTokens;
19478
19562
  const viewSentTokens = adjustedTokenCount(runtime.core, coreMessages, state, config, sentTokens, imageTokens, systemPromptTokens);
19479
19563
  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;
19564
+ 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
19565
  let text = buildStatusPanel({
19482
19566
  version: versionStr,
19483
19567
  tokenCount: sessionTokens,
@@ -19586,6 +19670,41 @@ function carryHostSystemMessages(rebuilt, input) {
19586
19670
  return changed ? patched : rebuilt;
19587
19671
  }
19588
19672
 
19673
+ // src/tool-pair-sanitizer.ts
19674
+ function sanitizeToolPairing(messages) {
19675
+ const empty = { messages, droppedResults: [] };
19676
+ if (!Array.isArray(messages) || messages.length === 0) return empty;
19677
+ try {
19678
+ const callIds = /* @__PURE__ */ new Set();
19679
+ for (const raw of messages) {
19680
+ const msg = raw;
19681
+ if (!msg || typeof msg !== "object" || msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
19682
+ for (const b2 of msg.content) {
19683
+ if (b2 && b2.type === "toolCall" && typeof b2.id === "string" && b2.id.length > 0) callIds.add(b2.id);
19684
+ }
19685
+ }
19686
+ let changed = false;
19687
+ const out = [];
19688
+ const droppedResults = [];
19689
+ for (const raw of messages) {
19690
+ const msg = raw;
19691
+ if (msg && typeof msg === "object" && msg.role === "toolResult") {
19692
+ const tcid = msg.toolCallId;
19693
+ const label = typeof tcid === "string" && tcid.length > 0 ? tcid : "(missing toolCallId)";
19694
+ if (typeof tcid !== "string" || tcid.length === 0 || !callIds.has(tcid)) {
19695
+ droppedResults.push(label);
19696
+ changed = true;
19697
+ continue;
19698
+ }
19699
+ }
19700
+ out.push(raw);
19701
+ }
19702
+ return changed ? { messages: out, droppedResults } : empty;
19703
+ } catch {
19704
+ return empty;
19705
+ }
19706
+ }
19707
+
19589
19708
  // src/degeneration.ts
19590
19709
  var DEFAULT_DEGENERATION_GUARD = { enabled: true, minRun: 200 };
19591
19710
  var MIN_VALID_MIN_RUN = 8;
@@ -20317,7 +20436,7 @@ async function autoInstallLatest(latest, extDirOverride) {
20317
20436
  "--no-fund",
20318
20437
  "--no-save"
20319
20438
  ];
20320
- const prevVersion = (await readPackageJson(join15(extDir, "package.json")))?.version ?? "0.1.75";
20439
+ const prevVersion = (await readPackageJson(join15(extDir, "package.json")))?.version ?? "0.1.76";
20321
20440
  const { code, stderr } = await runNpmImpl(installArgs(latest), { cwd: npmDir, timeout: 6e4 });
20322
20441
  if (code !== 0) {
20323
20442
  logWarn("update", {
@@ -20335,7 +20454,7 @@ async function autoInstallLatest(latest, extDirOverride) {
20335
20454
  }
20336
20455
  const verify = await verifyInstall(npmDir, latest);
20337
20456
  if (!verify.ok) {
20338
- const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.75";
20457
+ const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.76";
20339
20458
  logWarn("update", { event: "auto-install-verify-failed", latest, reason: verify.reason, rollbackTo });
20340
20459
  const rb = await runNpmImpl(installArgs(rollbackTo), { cwd: npmDir, timeout: 6e4 });
20341
20460
  logInfo("update", { event: "rollback", from: latest, to: rollbackTo, ok: rb.code === 0 });
@@ -20410,7 +20529,7 @@ async function checkForUpdate(autoUpdate, notify) {
20410
20529
  }
20411
20530
  const latest = await fetchLatestVersion(tag);
20412
20531
  if (!latest) return;
20413
- const current = runtimeVersion ?? "0.1.75";
20532
+ const current = runtimeVersion ?? "0.1.76";
20414
20533
  const hasUpdate = isVersionNewer(latest, current);
20415
20534
  debug.event("update-check", {
20416
20535
  current,
@@ -20629,7 +20748,7 @@ function wireSessionLifecycle(pi, runtime, standDownIfProxied) {
20629
20748
  setDelegatePolicy(DEFAULT_DELEGATE_POLICY);
20630
20749
  const sid = ctx.sessionManager.getSessionId();
20631
20750
  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 });
20751
+ 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
20752
  let delegateStoodDown = false;
20634
20753
  try {
20635
20754
  await runtime.reloadConfig(ctx.cwd);
@@ -20940,6 +21059,13 @@ ${rendered.text}${example}`);
20940
21059
  rebuilt = withHostSystem;
20941
21060
  logInfo("system-passthrough", { sid, event: "carried", systems: event.messages.filter((m2) => m2.role === "system").length, outMsgs: rebuilt.length });
20942
21061
  }
21062
+ if (turn.state.blocks.length > 0) {
21063
+ const sanitized = sanitizeToolPairing(rebuilt);
21064
+ if (sanitized.droppedResults.length > 0) {
21065
+ rebuilt = sanitized.messages;
21066
+ logWarn("tool-pair-sanitize", { sid, event: "dropped-orphan-results", count: sanitized.droppedResults.length, droppedResults: sanitized.droppedResults });
21067
+ }
21068
+ }
20943
21069
  debug.event("context-out", { outMsgs: rebuilt.length, injected: turn.nudge?.shouldInject ?? false, emergency: turn.nudge?.breakdown?.emergencyOverride === 1 });
20944
21070
  const updateCheck = checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
20945
21071
  if (ctx.hasUI) ctx.ui.notify(msg);