billion-context-omp 0.1.5 → 0.1.7

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
@@ -90,20 +90,23 @@ Rich status display for the user:
90
90
  ╭─────────────────────────────────────────────╮
91
91
  │ ACP Context Analysis │
92
92
  ╰─────────────────────────────────────────────╯
93
- billion-context-omp@0.1.1
93
+ billion-context-omp@0.2.0
94
94
 
95
- Context: 6% (57k / 1.0M)
95
+ Context (session accounting, host footer scale): 9% (93k / 1.0M) — never shrinks; includes compressed originals
96
96
 
97
- Token Breakdown:
98
- SysPrompt ██░░░░░░░░░░░░░░░░░░ 10% 5.9k
99
- Framework ██████████████████░░ 90% 51k
97
+ Sent to LLM (after compression, est.): 63k (6% of limit)
98
+ Session-only (compressed originals, est.): 110k — pruned from every request; the footer/nudge still count them
100
99
 
101
- Nudge: idle max compressible 0 < threshold 20000; growth 0 < floor 20000
100
+ Token Breakdown (sent view):
101
+ Tool ██████████████████░░ 88% 55k
102
+ SysPrompt ██░░░░░░░░░░░░░░░░░ 9% 5.9k
103
+ Text ░░░░░░░░░░░░░░░░░░░░ 1% 553
104
+ Summaries ░░░░░░░░░░░░░░░░░░░░ 2% 1.5k
102
105
 
103
- Blocks: 2 active / 2 total (21k tokens compressed)
104
- [b1] T1 5.1k→1.0k: Fold architecture port
105
- [b2] T1 15k→799: Replay guard hardening
106
+ Nudge: idle growth 0 < floor 20000, ready: T1 50394
106
107
 
108
+ Blocks: 1 active / 1 total (112k tokens compressed)
109
+ [b1] T1 112k→1.5k: PR141 review + follow-up fixes
107
110
  Tag visibility: tags injected to LLM only (deep copy), not persisted in session, not shown in terminal.
108
111
  ```
109
112
 
package/README.zh-CN.md CHANGED
@@ -90,20 +90,23 @@ omp 内置的 `/compact` 被拦截,替换为 ACP 模型摘要式 compaction,
90
90
  ╭─────────────────────────────────────────────╮
91
91
  │ ACP Context Analysis │
92
92
  ╰─────────────────────────────────────────────╯
93
- billion-context-omp@0.1.1
93
+ billion-context-omp@0.2.0
94
94
 
95
- Context: 6% (57k / 1.0M)
95
+ Context (session accounting, host footer scale): 9% (93k / 1.0M) — never shrinks; includes compressed originals
96
96
 
97
- Token Breakdown:
98
- SysPrompt ██░░░░░░░░░░░░░░░░░░ 10% 5.9k
99
- Framework ██████████████████░░ 90% 51k
97
+ Sent to LLM (after compression, est.): 63k (6% of limit)
98
+ Session-only (compressed originals, est.): 110k — pruned from every request; the footer/nudge still count them
100
99
 
101
- Nudge: idle max compressible 0 < threshold 20000; growth 0 < floor 20000
100
+ Token Breakdown (sent view):
101
+ Tool ██████████████████░░ 88% 55k
102
+ SysPrompt ██░░░░░░░░░░░░░░░░░ 9% 5.9k
103
+ Text ░░░░░░░░░░░░░░░░░░░░ 1% 553
104
+ Summaries ░░░░░░░░░░░░░░░░░░░░ 2% 1.5k
102
105
 
103
- Blocks: 2 active / 2 total (21k tokens compressed)
104
- [b1] T1 5.1k→1.0k: Fold architecture port
105
- [b2] T1 15k→799: Replay guard hardening
106
+ Nudge: idle growth 0 < floor 20000, ready: T1 50394
106
107
 
108
+ Blocks: 1 active / 1 total (112k tokens compressed)
109
+ [b1] T1 112k→1.5k: PR141 review + follow-up fixes
107
110
  Tag visibility: tags injected to LLM only (deep copy), not persisted in session, not shown in terminal.
108
111
  ```
109
112
 
@@ -3,7 +3,6 @@ import type { AcpRuntime } from "./runtime.js";
3
3
  /** Label shown for a block when the model did not pass a topic: first
4
4
  * sentence-ish slice of the summary (≤30 chars). Decorative only — never
5
5
  * blocks compression. */
6
- export declare function topicFallback(summary: string): string;
7
6
  declare const CompressParams: import("@oh-my-pi/omptype").FluentType<{
8
7
  content: {
9
8
  endId: string;
package/dist/index.js CHANGED
@@ -126,9 +126,11 @@ function prune(messages, state, options = {}) {
126
126
  const indexById = /* @__PURE__ */ new Map();
127
127
  messages.forEach((message, index) => indexById.set(message.id, index));
128
128
  const anchors = inject ? collectSummaryAnchors(state, indexById) : [];
129
- return stripOrphanedToolResults(
130
- stripOrphanedToolCalls(
131
- rebuildMessages(messages, covered, firstUserIndex, anchors)
129
+ return stripOrphanedReasoning(
130
+ stripOrphanedToolResults(
131
+ stripOrphanedToolCalls(
132
+ rebuildMessages(messages, covered, firstUserIndex, anchors)
133
+ )
132
134
  )
133
135
  );
134
136
  }
@@ -205,6 +207,24 @@ function stripOrphanedToolCalls(messages) {
205
207
  (m) => m.contentType !== "tool-call" || !m.toolCallId || m.toolName === "compress" || knownResultIds.has(m.toolCallId)
206
208
  );
207
209
  }
210
+ function stripOrphanedReasoning(messages) {
211
+ const drop = /* @__PURE__ */ new Set();
212
+ for (let i = 0; i < messages.length; i++) {
213
+ if (drop.has(i)) continue;
214
+ if (messages[i].contentType !== "reasoning") continue;
215
+ let j = i;
216
+ while (j + 1 < messages.length && messages[j + 1].contentType === "reasoning") {
217
+ j++;
218
+ }
219
+ const companion = messages[j + 1];
220
+ const hasCompanion = companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call");
221
+ if (!hasCompanion) {
222
+ for (let k = i; k <= j; k++) drop.add(k);
223
+ }
224
+ }
225
+ if (drop.size === 0) return messages;
226
+ return messages.filter((_, i) => !drop.has(i));
227
+ }
208
228
  function syncBlocks(messages, state) {
209
229
  const presentIds = new Set(messages.map((message) => message.id));
210
230
  const deactivated = [];
@@ -814,6 +834,38 @@ function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan =
814
834
  }
815
835
  return { startIndex: newStartIndex, endIndex: newEndIndex };
816
836
  }
837
+ function adjustBoundariesForReasoningPairs(startIndex, endIndex, messages) {
838
+ if (startIndex > endIndex) {
839
+ return { startIndex, endIndex };
840
+ }
841
+ let newStartIndex = startIndex;
842
+ let newEndIndex = endIndex;
843
+ for (let i = startIndex; i <= endIndex && i < messages.length; i++) {
844
+ const msg = messages[i];
845
+ if (!msg) continue;
846
+ if (msg.contentType === "reasoning") {
847
+ let j = i;
848
+ while (j + 1 < messages.length && messages[j + 1].contentType === "reasoning") {
849
+ j++;
850
+ }
851
+ const companion = messages[j + 1];
852
+ if (companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call") && j + 1 > newEndIndex) {
853
+ newEndIndex = j + 1;
854
+ }
855
+ }
856
+ if (msg.role === "assistant" && (msg.contentType === "text" || msg.contentType === "tool-call")) {
857
+ let k = i - 1;
858
+ while (k >= 0 && messages[k].contentType === "reasoning") {
859
+ k--;
860
+ }
861
+ const runStart = k + 1;
862
+ if (runStart < i && runStart >= 0 && messages[runStart].contentType === "reasoning" && runStart < newStartIndex) {
863
+ newStartIndex = runStart;
864
+ }
865
+ }
866
+ }
867
+ return { startIndex: newStartIndex, endIndex: newEndIndex };
868
+ }
817
869
  function refNum(ref) {
818
870
  const n = parseInt(ref.slice(1), 10);
819
871
  return Number.isNaN(n) ? -1 : n;
@@ -1309,7 +1361,7 @@ function applySingleRange(input) {
1309
1361
  messages: input.messages,
1310
1362
  state: input.state
1311
1363
  });
1312
- const rangeMessageIds = applyToolPairAdjustment(
1364
+ const rangeMessageIds = applyPairBoundaryAdjustments(
1313
1365
  resolved,
1314
1366
  input.messages
1315
1367
  );
@@ -1423,20 +1475,33 @@ function applySingleRange(input) {
1423
1475
  }
1424
1476
  return { tokens: compressedTokens, warnings };
1425
1477
  }
1426
- function applyToolPairAdjustment(resolved, messages) {
1478
+ function applyPairBoundaryAdjustments(resolved, messages) {
1427
1479
  if (resolved.boundaryKind === "block") {
1428
1480
  return resolved.messageIds;
1429
1481
  }
1430
- const adjusted = adjustBoundariesForToolPairs(
1431
- resolved.startIndex,
1432
- resolved.endIndex,
1433
- messages
1434
- );
1435
- if (adjusted.startIndex === resolved.startIndex && adjusted.endIndex === resolved.endIndex) {
1482
+ let startIndex = resolved.startIndex;
1483
+ let endIndex = resolved.endIndex;
1484
+ for (let pass = 0; pass < 2; pass++) {
1485
+ const reasoningAdjusted = adjustBoundariesForReasoningPairs(
1486
+ startIndex,
1487
+ endIndex,
1488
+ messages
1489
+ );
1490
+ const toolAdjusted = adjustBoundariesForToolPairs(
1491
+ reasoningAdjusted.startIndex,
1492
+ reasoningAdjusted.endIndex,
1493
+ messages
1494
+ );
1495
+ const changed = toolAdjusted.startIndex !== startIndex || toolAdjusted.endIndex !== endIndex;
1496
+ startIndex = toolAdjusted.startIndex;
1497
+ endIndex = toolAdjusted.endIndex;
1498
+ if (!changed) break;
1499
+ }
1500
+ if (startIndex === resolved.startIndex && endIndex === resolved.endIndex) {
1436
1501
  return resolved.messageIds;
1437
1502
  }
1438
1503
  const ids = [];
1439
- for (let i = adjusted.startIndex; i <= adjusted.endIndex; i++) {
1504
+ for (let i = startIndex; i <= endIndex; i++) {
1440
1505
  const msg = messages[i];
1441
1506
  if (msg) ids.push(msg.id);
1442
1507
  }
@@ -3003,10 +3068,6 @@ function boundaryRaw(ref, byRef, blocks, pick) {
3003
3068
  const pos = (id) => rawPos(byRef[id] ?? id);
3004
3069
  return pick === "min" ? ids.reduce((a, b) => pos(a) <= pos(b) ? a : b) : ids.reduce((a, b) => pos(a) >= pos(b) ? a : b);
3005
3070
  }
3006
- var VIABLE_RANGE_MIN_TOKENS = 200;
3007
- function viableRanges(ranges) {
3008
- return ranges.filter((r) => r.tokens >= VIABLE_RANGE_MIN_TOKENS);
3009
- }
3010
3071
  function rangeFingerprints(ranges, coreMessages, byRef, blocks) {
3011
3072
  return ranges.map((r) => {
3012
3073
  const start = boundaryRaw(r.startRef, byRef, blocks, "min");
@@ -3240,11 +3301,6 @@ var RangeSpec = type({
3240
3301
  summary: type("string").describe("Complete technical summary replacing all content in range. Keep only essential details (conclusions, file paths, decisions, exact values, etc.)."),
3241
3302
  "topic?": type("string").describe("Short label (3-5 words) for THIS range, e.g. 'Auth System Exploration'. Recommended for every range; omit to use top-level topic.")
3242
3303
  });
3243
- function topicFallback(summary) {
3244
- const first = summary.split(/[.\n]/)[0] ?? "";
3245
- const t = first.trim().replace(/^["'`]+/, "").trim();
3246
- return t.length <= 30 ? t : `${t.slice(0, 30).trimEnd()}\u2026`;
3247
- }
3248
3304
  var CompressParams = type({
3249
3305
  "topic?": type("string").describe("Fallback topic for entries without their own. Omit when each content entry specifies its own topic."),
3250
3306
  content: RangeSpec.array().describe("One or more ranges to compress, each with start/end boundaries and a summary. When compressing multiple unrelated ranges in one call, give each its own topic."),
@@ -3623,6 +3679,130 @@ function truncate(s, n) {
3623
3679
 
3624
3680
  // src/status-tool.ts
3625
3681
  import { type as type4 } from "@oh-my-pi/omptype";
3682
+
3683
+ // src/compat.ts
3684
+ function normalizeSystemPrompt(input) {
3685
+ if (input === void 0) return "";
3686
+ if (Array.isArray(input)) return input.join("\n");
3687
+ return input;
3688
+ }
3689
+ function formatSystemPromptForEvent(base, append) {
3690
+ const normalized = normalizeSystemPrompt(base);
3691
+ return [`${normalized}
3692
+
3693
+ ${append}`];
3694
+ }
3695
+ function getSystemPromptText(ctx) {
3696
+ const result = ctx.getSystemPrompt?.();
3697
+ return normalizeSystemPrompt(result);
3698
+ }
3699
+
3700
+ // node_modules/billion-context-kit/dist/index.js
3701
+ var VIABLE_RANGE_MIN_TOKENS = 200;
3702
+ function viableRanges(ranges) {
3703
+ return ranges.filter((r) => r.tokens >= VIABLE_RANGE_MIN_TOKENS);
3704
+ }
3705
+ function topicFallback(summary) {
3706
+ const first = summary.split(/[.\n]/)[0] ?? "";
3707
+ const t = first.trim().replace(/^["'`]+/, "").trim();
3708
+ return t.length <= 30 ? t : `${t.slice(0, 30).trimEnd()}\u2026`;
3709
+ }
3710
+ function formatCompactTokens(count) {
3711
+ if (count < 1e3) return count.toString();
3712
+ if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
3713
+ if (count < 1e6) return `${Math.round(count / 1e3)}k`;
3714
+ if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
3715
+ return `${Math.round(count / 1e6)}M`;
3716
+ }
3717
+ function bar(value, total, width = 20) {
3718
+ if (total === 0) return "";
3719
+ const filled = Math.max(0, Math.min(width, Math.round(value / total * width)));
3720
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
3721
+ }
3722
+ function buildStatusPanel(input) {
3723
+ const { tokenCount, state, nudge, modelContextLimit } = input;
3724
+ const fmt2 = input.fmtTokens ?? formatCompactTokens;
3725
+ const bd = nudge?.contextBreakdown;
3726
+ const limit = modelContextLimit;
3727
+ const classified = bd ? bd.system + bd.tool + bd.summaries + bd.code + bd.text : 0;
3728
+ const systemPromptTokens = input.systemPromptTokens;
3729
+ const sentTotal = classified + systemPromptTokens;
3730
+ const sessionOnly = input.unprunedTokens !== void 0 ? Math.max(0, input.unprunedTokens - sentTotal) : 0;
3731
+ const displayTotal = tokenCount;
3732
+ const displayPct = limit > 0 ? Math.round(displayTotal / limit * 100) : 0;
3733
+ const sentPct = limit > 0 ? Math.round(sentTotal / limit * 100) : 0;
3734
+ const activeBlocksList = state.blocks.filter((b) => b.active);
3735
+ const totalBlocksList = state.blocks;
3736
+ const lines = [];
3737
+ lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
3738
+ lines.push("\u2502 ACP Context Analysis \u2502");
3739
+ lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
3740
+ if (input.version) lines.push(input.version);
3741
+ lines.push("");
3742
+ lines.push(`Context (session accounting, host footer scale): ${displayPct}% (${fmt2(displayTotal)} / ${fmt2(limit)}) \u2014 never shrinks; includes compressed originals`);
3743
+ if (nudge && bd) {
3744
+ const growth = bd.growth;
3745
+ if (growth > 0 && displayTotal > 0) {
3746
+ lines.push(`Growth: +${fmt2(growth)} since last nudge`);
3747
+ }
3748
+ lines.push("");
3749
+ lines.push(`Sent to LLM (after compression, est.): ${fmt2(sentTotal)}${limit > 0 ? ` (${sentPct}% of limit)` : ""}`);
3750
+ if (input.unprunedTokens !== void 0 && sessionOnly > 0) {
3751
+ lines.push(`Session-only (compressed originals, est.): ${fmt2(sessionOnly)} \u2014 pruned from every request; the footer/nudge still count them`);
3752
+ }
3753
+ lines.push("");
3754
+ lines.push("Token Breakdown (sent view):");
3755
+ const categories = [
3756
+ { label: "Tool", value: bd.tool },
3757
+ { label: "SysPrompt", value: systemPromptTokens },
3758
+ { label: "Text", value: bd.text },
3759
+ { label: "Code", value: bd.code },
3760
+ { label: "Summaries", value: bd.summaries }
3761
+ ];
3762
+ for (const cat of categories) {
3763
+ if (cat.value <= 0) continue;
3764
+ const pct2 = sentTotal > 0 ? Math.round(cat.value / sentTotal * 100) : 0;
3765
+ const b = bar(cat.value, sentTotal);
3766
+ lines.push(` ${cat.label.padEnd(10)} ${b} ${String(pct2).padStart(3)}% ${fmt2(cat.value)}`);
3767
+ }
3768
+ }
3769
+ lines.push("");
3770
+ if (nudge) {
3771
+ if (nudge.shouldInject) {
3772
+ const tierInfo = nudge.tier ? ` [T${nudge.tier} distillation]` : "";
3773
+ lines.push(`Nudge: ACTIVE${tierInfo} \u2014 ${nudge.reason}`);
3774
+ } else {
3775
+ lines.push(`Nudge: idle \u2014 ${nudge.reason}`);
3776
+ }
3777
+ }
3778
+ const ranges = viableRanges(nudge?.compressibleRanges ?? []);
3779
+ const protectedRanges = nudge?.protectedRanges ?? [];
3780
+ if (ranges.length > 0 || protectedRanges.length > 0) {
3781
+ lines.push("");
3782
+ lines.push(formatRanges(ranges, protectedRanges));
3783
+ }
3784
+ if (activeBlocksList.length > 0) {
3785
+ lines.push("");
3786
+ lines.push(`Blocks: ${activeBlocksList.length} active / ${totalBlocksList.length} total (${fmt2(state.stats.tokensCompressed)} tokens compressed)`);
3787
+ for (const b of activeBlocksList) {
3788
+ const topic = b.topic ? `: ${b.topic}` : `: ${topicFallback(b.summary || "")}`;
3789
+ const summaryTok = defaultCountTokens(b.summary || "");
3790
+ const origTok = b.compressedTokens > 0 ? b.compressedTokens : summaryTok;
3791
+ lines.push(` [${b.blockId}] T${b.tier} ${fmt2(origTok)}\u2192${fmt2(summaryTok)}${topic}`);
3792
+ }
3793
+ } else if (totalBlocksList.length > 0) {
3794
+ lines.push("");
3795
+ lines.push(`Blocks: 0 active / ${totalBlocksList.length} total (${fmt2(state.stats.tokensCompressed)} tokens compressed)`);
3796
+ } else {
3797
+ lines.push("");
3798
+ lines.push("Blocks: none (nothing compressed yet)");
3799
+ }
3800
+ lines.push("");
3801
+ lines.push("Tag visibility: tags injected to LLM only (deep copy), not persisted in session, not shown in terminal.");
3802
+ return lines.join("\n");
3803
+ }
3804
+
3805
+ // src/status-tool.ts
3626
3806
  var StatusParams = type4({
3627
3807
  "scope?": type4('"compressed" | "uncompressed"').describe('"compressed" = drill into blocks; "uncompressed" = show visible messages/ranges. Default: overview.'),
3628
3808
  "view?": type4('"ranges" | "messages"').describe('For uncompressed scope: "ranges" (default) or "messages" (per-message listing).'),
@@ -3651,13 +3831,16 @@ function makeStatusTool(runtime) {
3651
3831
  async function handleStatus(args, runtime, ctx) {
3652
3832
  const { state, coreMessages } = await runtime.stateFor(ctx);
3653
3833
  const config = runtime.configFor(ctx);
3654
- const tokenCount = estimateTokens(coreMessages, collectCoveredMessageIds(state));
3655
- const realUsage = ctx.getContextUsage?.();
3834
+ const tokenCount = estimateTokens(coreMessages, collectCoveredMessageIds(state)) + estimateTextTokens2(getSystemPromptText(ctx) ?? "");
3656
3835
  const turn = runtime.core.processTurn({
3657
3836
  messages: coreMessages,
3658
3837
  state,
3659
3838
  config,
3660
- tokenCount: realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : tokenCount
3839
+ // Sent-view scale see src/index.ts context handler. The session-tree
3840
+ // number (ctx.getContextUsage) must never arbitrate emergencies for the
3841
+ // sent view: a tree that outgrew the model window reads as a permanent
3842
+ // 200%+ emergency while the real sent view is a few percent.
3843
+ tokenCount
3661
3844
  });
3662
3845
  const processed = turn.messages;
3663
3846
  const base = buildStatusReport(turn.state, processed, defaultCountTokens, {
@@ -3686,32 +3869,6 @@ async function handleStatus(args, runtime, ctx) {
3686
3869
  ${extra.join("\n")}` : base;
3687
3870
  }
3688
3871
 
3689
- // src/compat.ts
3690
- function normalizeSystemPrompt(input) {
3691
- if (input === void 0) return "";
3692
- if (Array.isArray(input)) return input.join("\n");
3693
- return input;
3694
- }
3695
- function formatSystemPromptForEvent(base, append) {
3696
- const normalized = normalizeSystemPrompt(base);
3697
- return [`${normalized}
3698
-
3699
- ${append}`];
3700
- }
3701
- function getSystemPromptText(ctx) {
3702
- const result = ctx.getSystemPrompt?.();
3703
- return normalizeSystemPrompt(result);
3704
- }
3705
-
3706
- // src/footer-status.ts
3707
- function formatCompactTokens(count) {
3708
- if (count < 1e3) return count.toString();
3709
- if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
3710
- if (count < 1e6) return `${Math.round(count / 1e3)}k`;
3711
- if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
3712
- return `${Math.round(count / 1e6)}M`;
3713
- }
3714
-
3715
3872
  // src/commands.ts
3716
3873
  function safeHandler(handler) {
3717
3874
  return async (args, ctx) => {
@@ -3789,100 +3946,26 @@ ${text}`);
3789
3946
  }
3790
3947
  ];
3791
3948
  }
3792
- function fmtTokens(n) {
3793
- return formatCompactTokens(n);
3794
- }
3795
- function bar(value, total, width = 20) {
3796
- if (total === 0) return "";
3797
- const filled = Math.max(0, Math.min(width, Math.round(value / total * width)));
3798
- return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
3799
- }
3800
3949
  async function statusReport(runtime, ctx) {
3801
3950
  const { state, coreMessages } = await runtime.stateFor(ctx);
3802
3951
  const config = runtime.configFor(ctx);
3803
3952
  const realUsage = ctx.getContextUsage?.();
3804
- const tokenCount = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n"));
3805
- const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount });
3806
- const nudge = turn.nudge;
3807
- const bd = nudge?.contextBreakdown;
3808
- const limit = config.modelContextLimit;
3809
- const classified = bd ? bd.system + bd.tool + bd.summaries + bd.code + bd.text : 0;
3953
+ const sessionTokens = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n"));
3810
3954
  const systemPromptText = getSystemPromptText(ctx);
3811
3955
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
3812
- const sentTotal = classified + systemPromptTokens;
3813
- const sessionOnly = Math.max(0, tokenCount - sentTotal);
3814
- const displayTotal = tokenCount;
3815
- const displayPct = limit > 0 ? Math.round(displayTotal / limit * 100) : 0;
3816
- const activeBlocksList = state.blocks.filter((b) => b.active);
3817
- const totalBlocksList = state.blocks;
3818
- const lines = [];
3819
- const versionStr = "0.1.5" ? `billion-context-omp@${"0.1.5"}` : "";
3820
- lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
3821
- lines.push("\u2502 ACP Context Analysis \u2502");
3822
- lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
3823
- if (versionStr) lines.push(versionStr);
3824
- lines.push("");
3825
- lines.push(`Context (session accounting): ${displayPct}% (${fmtTokens(displayTotal)} / ${fmtTokens(limit)})`);
3826
- if (nudge && bd) {
3827
- const growth = bd.growth;
3828
- if (growth > 0 && displayTotal > 0) {
3829
- lines.push(`Growth: +${fmtTokens(growth)} since last nudge`);
3830
- }
3831
- lines.push("");
3832
- lines.push(`Sent to LLM (after compression): ${fmtTokens(sentTotal)}`);
3833
- if (sessionOnly > 0) {
3834
- lines.push(`Session-only (compressed originals + host overhead): ${fmtTokens(sessionOnly)} \u2014 pruned from every request; the footer counts it`);
3835
- }
3836
- lines.push("");
3837
- lines.push("Token Breakdown (sent view):");
3838
- const categories = [
3839
- { label: "Tool", value: bd.tool },
3840
- { label: "SysPrompt", value: systemPromptTokens },
3841
- { label: "Text", value: bd.text },
3842
- { label: "Code", value: bd.code },
3843
- { label: "Summaries", value: bd.summaries }
3844
- ];
3845
- for (const cat of categories) {
3846
- if (cat.value <= 0) continue;
3847
- const pct2 = sentTotal > 0 ? Math.round(cat.value / sentTotal * 100) : 0;
3848
- const b = bar(cat.value, sentTotal);
3849
- lines.push(` ${cat.label.padEnd(10)} ${b} ${String(pct2).padStart(3)}% ${fmtTokens(cat.value)}`);
3850
- }
3851
- }
3852
- lines.push("");
3853
- if (nudge) {
3854
- if (nudge.shouldInject) {
3855
- const tierInfo = nudge.tier ? ` [T${nudge.tier} distillation]` : "";
3856
- lines.push(`Nudge: ACTIVE${tierInfo} \u2014 ${nudge.reason}`);
3857
- } else {
3858
- lines.push(`Nudge: idle \u2014 ${nudge.reason}`);
3859
- }
3860
- }
3861
- const ranges = viableRanges(nudge?.compressibleRanges ?? []);
3862
- const protectedRanges = nudge?.protectedRanges ?? [];
3863
- if (ranges.length > 0 || protectedRanges.length > 0) {
3864
- lines.push("");
3865
- lines.push(formatRanges(ranges, protectedRanges));
3866
- }
3867
- if (activeBlocksList.length > 0) {
3868
- lines.push("");
3869
- lines.push(`Blocks: ${activeBlocksList.length} active / ${totalBlocksList.length} total (${fmtTokens(state.stats.tokensCompressed)} tokens compressed)`);
3870
- for (const b of activeBlocksList) {
3871
- const topic = b.topic ? `: ${b.topic}` : `: ${topicFallback(b.summary || "")}`;
3872
- const summaryTok = defaultCountTokens(b.summary || "");
3873
- const origTok = b.compressedTokens > 0 ? b.compressedTokens : summaryTok;
3874
- lines.push(` [${b.blockId}] T${b.tier} ${fmtTokens(origTok)}\u2192${fmtTokens(summaryTok)}${topic}`);
3875
- }
3876
- } else if (totalBlocksList.length > 0) {
3877
- lines.push("");
3878
- lines.push(`Blocks: 0 active / ${totalBlocksList.length} total (${fmtTokens(state.stats.tokensCompressed)} tokens compressed)`);
3879
- } else {
3880
- lines.push("");
3881
- lines.push("Blocks: none (nothing compressed yet)");
3882
- }
3883
- lines.push("");
3884
- lines.push("Tag visibility: tags injected to LLM only (deep copy), not persisted in session, not shown in terminal.");
3885
- return lines.join("\n");
3956
+ const coveredIds = collectCoveredMessageIds(state);
3957
+ const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
3958
+ const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: sentTokens });
3959
+ const versionStr = "0.1.7" ? `billion-context-omp@${"0.1.7"}` : void 0;
3960
+ return buildStatusPanel({
3961
+ version: versionStr,
3962
+ tokenCount: sessionTokens,
3963
+ systemPromptTokens,
3964
+ state: turn.state,
3965
+ nudge: turn.nudge,
3966
+ modelContextLimit: config.modelContextLimit,
3967
+ unprunedTokens: coreMessages.reduce((sum, m) => sum + defaultCountTokens(m.text ?? ""), 0)
3968
+ });
3886
3969
  }
3887
3970
 
3888
3971
  // src/auto-compress.ts
@@ -4313,7 +4396,7 @@ async function checkForUpdate(autoUpdate, notify) {
4313
4396
  const data = await res.json();
4314
4397
  const latest = data.version;
4315
4398
  if (!latest) return;
4316
- const current = runtimeVersion ?? "0.1.5";
4399
+ const current = runtimeVersion ?? "0.1.7";
4317
4400
  const hasUpdate = isNewer(latest, current);
4318
4401
  debug.event("update-check", {
4319
4402
  current,
@@ -4601,7 +4684,7 @@ function wireCompactionDisable(pi, runtime) {
4601
4684
  function wireSessionLifecycle(pi, runtime) {
4602
4685
  pi.on("session_start", async (_event, ctx) => {
4603
4686
  const sid = ctx.sessionManager.getSessionId();
4604
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.5" : null });
4687
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.7" : null });
4605
4688
  try {
4606
4689
  const user = await loadUserConfig(ctx.cwd);
4607
4690
  runtime.setAdapter(applyUserConfig(runtime.adapter, user));
@@ -4642,18 +4725,17 @@ function wireContextTransform(pi, runtime) {
4642
4725
  const { state, coreMessages, originalById, streamLen } = runtime.foldStream(ctx, input);
4643
4726
  const config = runtime.configFor(ctx);
4644
4727
  const coveredIds = collectCoveredMessageIds(state);
4645
- const realUsage = ctx.getContextUsage?.();
4646
- const estimated = estimateTokens(coreMessages, coveredIds);
4647
- const tokenCount = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : estimated;
4728
+ const systemPromptTokens = estimateTextTokens2(getSystemPromptText(ctx) ?? "");
4729
+ const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
4730
+ const sessionTokens = ctx.getContextUsage?.()?.tokens ?? null;
4731
+ const tokenCount = sentTokens;
4648
4732
  debug.event("context-in", {
4649
4733
  sid,
4650
4734
  eventMsgs: event.messages?.length ?? 0,
4651
4735
  streamLen,
4652
4736
  coreMsgs: coreMessages.length,
4653
4737
  tokenCount,
4654
- estimatedTokens: estimated,
4655
- realTokens: realUsage?.tokens ?? null,
4656
- realPercent: realUsage?.percent ?? null,
4738
+ sessionTokens,
4657
4739
  limit: config.modelContextLimit,
4658
4740
  blocksBefore: state.blocks.length,
4659
4741
  activeBefore: state.blocks.filter((b) => b.active).length
@@ -4665,7 +4747,8 @@ function wireContextTransform(pi, runtime) {
4665
4747
  inMsgs: coreMessages.length,
4666
4748
  outMsgs: turn.messages.length,
4667
4749
  tokens: tokenCount,
4668
- pct: realUsage?.percent ?? (config.modelContextLimit > 0 ? Math.round(tokenCount / config.modelContextLimit * 100) : null),
4750
+ sessionTokens,
4751
+ pct: config.modelContextLimit > 0 ? Math.round(tokenCount / config.modelContextLimit * 100) : null,
4669
4752
  limit: config.modelContextLimit,
4670
4753
  nudge: turn.nudge?.shouldInject ? turn.nudge.breakdown?.emergencyOverride === 1 ? "emergency" : "active" : "idle",
4671
4754
  nudgeReason: turn.nudge?.reason ?? null,