opencode-context-indicator 1.1.6 → 1.2.1

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/lib/limits.js CHANGED
@@ -6,6 +6,16 @@
6
6
  // client.config.providers(), ensureModelLimits for V2 via ctx.model.list()).
7
7
  // hydrateModelLimitsFromState() seeds the maps from the shared state.json
8
8
  // snapshot via ./state.js. Extracted 1:1 (code + WHY comments) from index.js.
9
+ //
10
+ // Compaction ceiling (v2.0.23): opencode's core calculateCeiling() in
11
+ // packages/core/src/session/compaction.ts derives the auto-compaction trigger
12
+ // ceiling as window - reserve, where window = limit.input || limit.context
13
+ // (null/zero falls through to the context window) and
14
+ // reserve = max(floor(window * 0.1), window >= 32_000 ? 16_000 : 0)
15
+ // A compaction.buffer configured in opencode itself is NOT visible to the
16
+ // plugin (the plugin cannot read opencode's internal compaction config); the
17
+ // env var OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER overrides the reserve for
18
+ // testing and power users only (positive finite integer <= 1e9, ignore invalid).
9
19
 
10
20
  import { finitePos, withTimeout } from "./estimate.js";
11
21
  import { logErr, readStateSnapshot } from "./state.js";
@@ -67,32 +77,73 @@ function getModelLimit(providerID, modelID) {
67
77
  return typeof rec.context === "number" && rec.context > 0 ? rec.context : null;
68
78
  }
69
79
 
70
- // Native opencode usable window (see packages/opencode/src/session/overflow.ts):
71
- // usable = limit.input ? limit.input − reserved : limit.context − maxOutput
72
- // reserved = compaction.reserved ?? min(20_000, maxOutput)
73
- // maxOutput = model.limit.output
74
- // Returns { usable, limit, maxOutput, reserved } or null when not derivable
75
- // (callers then degrade to count / limit.context, or to no percentage at all).
76
- function getUsableContext(providerID, modelID) {
80
+ // Compute the reserve for a given window, matching opencode v2.0.23
81
+ // packages/core/src/session/compaction.ts calculateCeiling(). Returns null when
82
+ // window is not a finite positive number.
83
+ function reserveFor(window, bufferOverride) {
84
+ if (
85
+ typeof window !== "number" ||
86
+ !Number.isFinite(window) ||
87
+ window <= 0
88
+ ) return null;
89
+ // bufferOverride: accept only finite, non-negative integers <= 1e9.
90
+ if (
91
+ typeof bufferOverride === "number" &&
92
+ Number.isFinite(bufferOverride) &&
93
+ bufferOverride >= 0 &&
94
+ bufferOverride <= 1e9
95
+ ) {
96
+ return bufferOverride;
97
+ }
98
+ // Default reserve formula (mirror of opencode core):
99
+ // max(floor(window * 0.1), window >= 32_000 ? 16_000 : 0)
100
+ return Math.max(
101
+ Math.floor(window * 0.1),
102
+ window >= 32_000 ? 16_000 : 0,
103
+ );
104
+ }
105
+
106
+ // Native opencode compaction ceiling (v2.0.23 calculateCeiling):
107
+ // window = limit.input || limit.context (zero/null falls through to context)
108
+ // reserve = reserveFor(window) [or bufferOverride when provided]
109
+ // usable = window - reserve
110
+ // Returns { usable, limit: window, maxOutput, reserved } or null when not
111
+ // derivable (callers then degrade to count / limit.context, or no percentage).
112
+ // The per-model compaction.reserved and the old min(20000, maxOutput) no longer
113
+ // participate in this calculation; opencode's internal buffer is not visible.
114
+ function getUsableContext(providerID, modelID, bufferOverride) {
77
115
  const rec = getModelLimitRecord(providerID, modelID);
78
116
  if (!rec) return null;
79
117
  const context =
80
118
  typeof rec.context === "number" && rec.context > 0 ? rec.context : null;
81
119
  const input =
82
120
  typeof rec.input === "number" && rec.input > 0 ? rec.input : null;
121
+ if (input == null && context == null) return null;
122
+ // Mirror core's limit.input || limit.context (zero/null falls through to context).
123
+ const window = input != null && input > 0 ? input : context;
124
+ if (window == null || !(window > 0)) return null;
125
+
126
+ // When bufferOverride is omitted, read it from the env var once per call.
127
+ let resolvedBuffer = bufferOverride;
128
+ if (resolvedBuffer === undefined) {
129
+ const raw = process.env.OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER;
130
+ // Strict digits-only: reject "5000abc", "5e3", " 5000", "", "-1" and other
131
+ // partial parses (parseInt/parseFloat stop at the first non-digit).
132
+ if (typeof raw === "string" && /^\d+$/.test(raw)) {
133
+ const parsed = parseInt(raw, 10);
134
+ if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 1e9) {
135
+ resolvedBuffer = parsed;
136
+ }
137
+ }
138
+ }
139
+
140
+ const reserve = reserveFor(window, resolvedBuffer);
141
+ if (reserve == null) return null;
142
+ const usable = window - reserve;
143
+ if (usable <= 0) return null;
83
144
  const maxOutput =
84
145
  typeof rec.output === "number" && rec.output > 0 ? rec.output : 0;
85
- if (input == null && context == null) return null;
86
- // Mirrors opencode overflow.ts: reserved = min(20000, maxOutput), i.e. 0 when
87
- // the model declares no output limit (NOT a 20000 default — that would
88
- // understate the usable window).
89
- const reserved =
90
- typeof rec.reserved === "number" && rec.reserved >= 0
91
- ? rec.reserved
92
- : Math.min(20000, maxOutput);
93
- const usable = input != null ? input - reserved : context - maxOutput;
94
- if (!(usable > 0)) return null;
95
- return { usable, limit: context, maxOutput, reserved };
146
+ return { usable, limit: window, maxOutput, reserved: reserve };
96
147
  }
97
148
 
98
149
  // Store a model limit record under `<providerID>:<modelID>`. Written by
@@ -366,13 +417,18 @@ function hydrateModelLimitsFromState() {
366
417
  }
367
418
  }
368
419
 
369
- // Build the record: precise usable-as-input when known (so getUsableContext
370
- // re-derives EXACTLY the persisted window), else the raw context window.
420
+ // Build the record: seed the raw window (context, preferring limit when
421
+ // present) so the NEW formula derives the ceiling fresh on every call.
422
+ // The old precise-usable-as-input seeding (which caused double-reserve
423
+ // under the new formula) is retired: getUsableContext is now a pure
424
+ // function of the window and needs no pre-derived state.
371
425
  let rec = null;
372
- if (best.usable != null) {
373
- rec = { context: best.limit, input: best.usable, output: null, reserved: 0 };
374
- } else if (best.limit != null) {
426
+ if (best.limit != null) {
375
427
  rec = { context: best.limit, input: null, output: null, reserved: null };
428
+ } else if (best.usable != null) {
429
+ // No limit in snapshot: fall back to persisted usable as a best-effort
430
+ // window (it was the ceiling before the formula change, close enough).
431
+ rec = { context: best.usable, input: null, output: null, reserved: null };
376
432
  }
377
433
  if (rec == null) continue;
378
434
  modelLimits.set(key, rec);
@@ -396,6 +452,7 @@ export {
396
452
  modelKey,
397
453
  getModelLimit,
398
454
  getUsableContext,
455
+ reserveFor,
399
456
  setModelLimit,
400
457
  ensureProviderLimits,
401
458
  ensureModelLimits,
package/lib/state.js CHANGED
@@ -331,14 +331,23 @@ function writeStateFile(
331
331
  }
332
332
  try {
333
333
  const bd = breakdownCache.get(sessionID) || null;
334
- const limit = getModelLimit(providerID, modelID);
335
334
  const usableInfo = getUsableContext(providerID, modelID);
335
+ // Persist the EFFECTIVE window (limit.input || limit.context — the same
336
+ // window the ceiling was derived from), not the raw context window: on a
337
+ // cold instance hydrateModelLimitsFromState seeds context = limit, so a
338
+ // mismatched limit would re-derive a WRONG (too large) ceiling for models
339
+ // where limit.input < limit.context.
340
+ const limit =
341
+ usableInfo != null && usableInfo.limit != null
342
+ ? usableInfo.limit
343
+ : getModelLimit(providerID, modelID);
336
344
  const denom =
337
345
  usableInfo != null
338
346
  ? usableInfo.usable
339
347
  : limit != null && limit > 0
340
348
  ? limit
341
349
  : null;
350
+ const reserve = usableInfo != null ? usableInfo.reserved : null;
342
351
  const total =
343
352
  typeof ctxTokens === "number" && ctxTokens > 0
344
353
  ? ctxTokens
@@ -433,6 +442,12 @@ function writeStateFile(
433
442
  : sameModelAsPrev && finitePos(prevEntry.usable)
434
443
  ? prevEntry.usable
435
444
  : null,
445
+ reserve:
446
+ reserve != null
447
+ ? reserve
448
+ : sameModelAsPrev && finitePos(prevEntry.reserve)
449
+ ? prevEntry.reserve
450
+ : null,
436
451
  limit:
437
452
  limit != null
438
453
  ? limit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-context-indicator",
3
- "version": "1.1.6",
3
+ "version": "1.2.1",
4
4
  "description": "Real-time OpenCode context-window usage indicator: per-category token breakdown written to a log file, plus an optional TUI sidebar. Dual V1 + V2 plugin entry.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,11 +56,11 @@
56
56
  }
57
57
  },
58
58
  "dependencies": {
59
- "@opentui/solid": "0.5.14"
59
+ "@opentui/solid": "0.5.16"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@opencode/plugin": "^2.0.19",
63
- "@opentui/core": "0.5.14",
63
+ "@opentui/core": "0.5.16",
64
64
  "esbuild": "^0.28.2",
65
65
  "node-pty": "^1.1.0",
66
66
  "solid-js": "^1.9.12"