opencode-clm 0.1.0 → 0.2.0
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/NOTICE +9 -1
- package/README.md +85 -258
- package/index.ts +68 -14
- package/package.json +54 -9
- package/src/atomic.ts +27 -0
- package/src/budget.ts +129 -5
- package/src/clm.ts +374 -20
- package/src/commands.ts +18 -0
- package/src/context-document.ts +31 -4
- package/src/mirror-store.ts +1 -1
- package/src/overrides.ts +193 -0
- package/src/panel/command.ts +69 -0
- package/src/panel/diff.ts +422 -0
- package/src/panel/files.ts +103 -0
- package/src/panel/keys.ts +235 -0
- package/src/panel/lines.ts +269 -0
- package/src/panel/model.ts +547 -0
- package/src/panel/text.ts +22 -0
- package/src/panel/timeline.ts +454 -0
- package/src/panel/view.ts +545 -0
- package/src/presentation.ts +10 -1
- package/src/session-files.ts +184 -0
- package/src/settings-table.ts +406 -0
- package/src/settings.ts +2 -2
- package/src/state.ts +49 -2
- package/src/tui/data.ts +161 -0
- package/src/tui/intercept.ts +56 -0
- package/src/tui/panel.ts +194 -0
- package/src/tui/plugin.ts +280 -0
- package/tui.ts +7 -0
package/src/atomic.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Atomic replacement of small session files (overrides.json, snapshot.json, rN.json): write
|
|
2
|
+
// a temp file with mode 0600 next to the target, then rename it over the target, so a
|
|
3
|
+
// reader never sees a torn file. Written for this package.
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { mkdir, open, rename, rm } from "node:fs/promises";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
|
|
8
|
+
export async function writeFileAtomic(path: string, text: string): Promise<void> {
|
|
9
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
10
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
11
|
+
try {
|
|
12
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
13
|
+
try {
|
|
14
|
+
await handle.writeFile(text, "utf8");
|
|
15
|
+
} finally {
|
|
16
|
+
await handle.close();
|
|
17
|
+
}
|
|
18
|
+
await rename(temporary, path);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeJsonAtomic(path: string, value: unknown): Promise<void> {
|
|
26
|
+
return writeFileAtomic(path, `${JSON.stringify(value, null, 1)}\n`);
|
|
27
|
+
}
|
package/src/budget.ts
CHANGED
|
@@ -39,6 +39,14 @@ export const DEFAULT_BUDGET_POLICY: BudgetPolicyConfig = {
|
|
|
39
39
|
remindAtReserve: true,
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Room the conversation needs on top of OpenCode's fixed overhead (system prompt and tool
|
|
44
|
+
* schemas) and the reserve: the pinned task, a few tool results and the model's own edits.
|
|
45
|
+
* A budget with less room than this makes the overflow guard withhold nearly every tool
|
|
46
|
+
* result; `budgetFit` then raises the session's effective budget.
|
|
47
|
+
*/
|
|
48
|
+
export const WORKING_MARGIN = 8000;
|
|
49
|
+
|
|
42
50
|
export interface BudgetReading {
|
|
43
51
|
/** Effective budget in tokens (config or model window). */
|
|
44
52
|
budget: number;
|
|
@@ -64,6 +72,8 @@ export interface BudgetReading {
|
|
|
64
72
|
observedStale?: boolean;
|
|
65
73
|
/** Where `budget` came from, for status text. */
|
|
66
74
|
source: "config" | "model-window";
|
|
75
|
+
/** Set when `budget` was raised above the configured value to cover the fixed overhead (see `budgetFit`). */
|
|
76
|
+
raisedFrom?: number;
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
/** A reminder threshold in absolute tokens plus a stable label for re-arming. */
|
|
@@ -112,6 +122,115 @@ export function resolveBudget(
|
|
|
112
122
|
return undefined;
|
|
113
123
|
}
|
|
114
124
|
|
|
125
|
+
/** Budget arithmetic for status text, the TUI panel and the budget-too-small check. */
|
|
126
|
+
export interface BudgetFit {
|
|
127
|
+
/** Budget before any raise (configured, or the model window). */
|
|
128
|
+
configured: number;
|
|
129
|
+
reserve: number;
|
|
130
|
+
/** OpenCode's fixed overhead per request in tokens; undefined until measured. */
|
|
131
|
+
overhead?: number;
|
|
132
|
+
/** configured − reserve − overhead: what the conversation gets without a raise (may be negative). */
|
|
133
|
+
usable?: number;
|
|
134
|
+
/** True when configured − reserve leaves less than `margin` after the overhead. */
|
|
135
|
+
tooSmall: boolean;
|
|
136
|
+
/** overhead + margin + reserve: the smallest budget that leaves the margin. */
|
|
137
|
+
minimum?: number;
|
|
138
|
+
/** The budget CLM applies: `configured`, or raised toward `minimum` (capped by `cap`). */
|
|
139
|
+
effective: number;
|
|
140
|
+
/** effective − reserve − overhead. */
|
|
141
|
+
effectiveUsable?: number;
|
|
142
|
+
raised: boolean;
|
|
143
|
+
/** True when `cap` stopped the raise short of `minimum`. */
|
|
144
|
+
capped: boolean;
|
|
145
|
+
margin: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pure: how the budget compares with OpenCode's fixed overhead. When budget − reserve leaves
|
|
150
|
+
* less than `margin` after the overhead, the effective budget rises to overhead + margin +
|
|
151
|
+
* reserve, but never above `cap` (the model window minus its output limit) nor below the
|
|
152
|
+
* configured budget.
|
|
153
|
+
*/
|
|
154
|
+
export function budgetFit(
|
|
155
|
+
configured: number,
|
|
156
|
+
reserve: number,
|
|
157
|
+
overhead: number | undefined,
|
|
158
|
+
cap?: number,
|
|
159
|
+
margin = WORKING_MARGIN,
|
|
160
|
+
): BudgetFit {
|
|
161
|
+
if (overhead === undefined) {
|
|
162
|
+
return { configured, reserve, tooSmall: false, effective: configured, raised: false, capped: false, margin };
|
|
163
|
+
}
|
|
164
|
+
const usable = configured - reserve - overhead;
|
|
165
|
+
const minimum = overhead + margin + reserve;
|
|
166
|
+
const tooSmall = usable < margin;
|
|
167
|
+
const limit = cap !== undefined && Number.isFinite(cap) && cap > 0 ? cap : Number.POSITIVE_INFINITY;
|
|
168
|
+
const effective = tooSmall ? Math.max(configured, Math.min(minimum, limit)) : configured;
|
|
169
|
+
return {
|
|
170
|
+
configured,
|
|
171
|
+
reserve,
|
|
172
|
+
overhead,
|
|
173
|
+
usable,
|
|
174
|
+
tooSmall,
|
|
175
|
+
minimum,
|
|
176
|
+
effective,
|
|
177
|
+
effectiveUsable: effective - reserve - overhead,
|
|
178
|
+
raised: effective > configured,
|
|
179
|
+
capped: tooSmall && minimum > limit,
|
|
180
|
+
margin,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Status line for `budgetFit`: fixed overhead, usable budget, and the raise if any. */
|
|
185
|
+
export function budgetFitLine(fit: BudgetFit): string {
|
|
186
|
+
if (fit.overhead === undefined) return "fixed overhead (system prompt + tool schemas): not measured yet";
|
|
187
|
+
const parts = [
|
|
188
|
+
`fixed overhead (system prompt + tool schemas) ${formatTokens(fit.overhead)} tok`,
|
|
189
|
+
`usable ${formatTokens(fit.usable!)} tok (budget ${formatTokens(fit.configured)} − reserve ${formatTokens(fit.reserve)} − overhead)`,
|
|
190
|
+
];
|
|
191
|
+
if (fit.raised) {
|
|
192
|
+
parts.push(
|
|
193
|
+
`effective budget ${formatTokens(fit.effective)} tok (raised${fit.capped ? ", capped by the model window" : ""}; usable ${formatTokens(fit.effectiveUsable!)} tok)`,
|
|
194
|
+
);
|
|
195
|
+
} else if (fit.tooSmall) {
|
|
196
|
+
parts.push("too small, and the model window allows no raise");
|
|
197
|
+
}
|
|
198
|
+
return parts.join(" · ");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The one-time model notice for a budget that cannot hold OpenCode's fixed overhead plus a
|
|
203
|
+
* working margin. Text written for this package.
|
|
204
|
+
*/
|
|
205
|
+
export function budgetTooSmallNoticeText(fit: BudgetFit): string {
|
|
206
|
+
const head =
|
|
207
|
+
`[CLM BUDGET] The configured budget of ${formatTokens(fit.configured)} tokens is too small for this session: ` +
|
|
208
|
+
`OpenCode's system prompt and tool schemas take about ${formatTokens(fit.overhead ?? 0)} tokens of every request, ` +
|
|
209
|
+
`which leaves ${formatTokens(Math.max(0, fit.usable ?? 0))} tokens for the conversation after the ${formatTokens(fit.reserve)}-token reserve.`;
|
|
210
|
+
const left = formatTokens(Math.max(0, fit.effectiveUsable ?? 0));
|
|
211
|
+
if (fit.capped) {
|
|
212
|
+
const raise = fit.raised ? `CLM raised this session's budget to ${formatTokens(fit.effective)} tokens, the most the model window allows, ` : "The model window allows no larger budget, ";
|
|
213
|
+
return (
|
|
214
|
+
`${head} ${raise}which still leaves only ${left} tokens for the conversation. ` +
|
|
215
|
+
"The overflow guard will hold back most tool results: keep the context mirror short and read files in small parts."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return (
|
|
219
|
+
`${head} CLM raised this session's budget to ${formatTokens(fit.effective)} tokens, ` +
|
|
220
|
+
`about ${left} tokens for the conversation. ` +
|
|
221
|
+
"Budget reminders and the overflow guard count against the raised budget from now on."
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The user-facing toast for the same case: what happened and what to change. */
|
|
226
|
+
export function budgetTooSmallAlertText(fit: BudgetFit): string {
|
|
227
|
+
const head = `Budget ${formatTokens(fit.configured)} is too small: OpenCode's system prompt and tools take ~${formatTokens(fit.overhead ?? 0)} tokens.`;
|
|
228
|
+
if (fit.capped) {
|
|
229
|
+
return `${head} The model window limits this session to ${formatTokens(fit.effective)}, leaving ~${formatTokens(Math.max(0, fit.effectiveUsable ?? 0))} for the conversation; lower reserve, use fewer tools, or use a model with a larger context window.`;
|
|
230
|
+
}
|
|
231
|
+
return `${head} Raised to ${formatTokens(fit.effective)} for this session; set budget to at least ${formatTokens(fit.minimum!)}.`;
|
|
232
|
+
}
|
|
233
|
+
|
|
115
234
|
export function budgetTiers(policy: BudgetPolicyConfig, budget: number, reserve: number): BudgetTier[] {
|
|
116
235
|
const tiers: BudgetTier[] = policy.remindAtFractions.map((fraction) => ({
|
|
117
236
|
label: `${Math.round(fraction * 100)}%`,
|
|
@@ -176,8 +295,11 @@ function calibrated(reading: Pick<BudgetReading, "calibration">): boolean {
|
|
|
176
295
|
|
|
177
296
|
/** One line for status text: budget, estimate, and observation, each labelled. */
|
|
178
297
|
export function budgetSummaryLine(reading: BudgetReading): string {
|
|
298
|
+
const origin = reading.raisedFrom !== undefined
|
|
299
|
+
? `raised from ${formatTokens(reading.raisedFrom)} to cover the fixed overhead`
|
|
300
|
+
: reading.source === "config" ? "configured" : "model window";
|
|
179
301
|
const parts = [
|
|
180
|
-
`budget ${formatTokens(reading.budget)} tok (${
|
|
302
|
+
`budget ${formatTokens(reading.budget)} tok (${origin}, reserve ${formatTokens(reading.reserve)})`,
|
|
181
303
|
`estimated next request ${formatTokens(reading.estimated)}${calibrated(reading) ? ` (×${reading.calibration!.toFixed(2)} calibrated)` : ""}`,
|
|
182
304
|
];
|
|
183
305
|
parts.push(
|
|
@@ -238,7 +360,7 @@ export function budgetNoticeText(
|
|
|
238
360
|
* odd sample cannot run away.
|
|
239
361
|
*/
|
|
240
362
|
export class EstimateCalibrator {
|
|
241
|
-
private pending: { estimate: number; rawCount: number } | undefined;
|
|
363
|
+
private pending: { estimate: number; rawCount: number; offset: number } | undefined;
|
|
242
364
|
private current = 1;
|
|
243
365
|
private samples = 0;
|
|
244
366
|
|
|
@@ -268,9 +390,11 @@ export class EstimateCalibrator {
|
|
|
268
390
|
* Remember the raw (uncalibrated) estimate of the request about to be sent. The estimate
|
|
269
391
|
* must cover the same scope as the provider count it will be compared with; an estimate
|
|
270
392
|
* that omits the system prompt and tool schemas reads their size as undercounting.
|
|
393
|
+
* `offset`: tokens of the provider count the estimate does not cover, already measured
|
|
394
|
+
* (the fixed overhead); they are subtracted from the observation before the comparison.
|
|
271
395
|
*/
|
|
272
|
-
record(estimate: number, rawCount: number): void {
|
|
273
|
-
if (estimate > 0) this.pending = { estimate, rawCount };
|
|
396
|
+
record(estimate: number, rawCount: number, offset = 0): void {
|
|
397
|
+
if (estimate > 0) this.pending = { estimate, rawCount, offset };
|
|
274
398
|
}
|
|
275
399
|
|
|
276
400
|
/**
|
|
@@ -280,7 +404,7 @@ export class EstimateCalibrator {
|
|
|
280
404
|
*/
|
|
281
405
|
observe(observed: { tokens: number; index: number } | undefined): number {
|
|
282
406
|
if (!observed || !this.pending || observed.index < this.pending.rawCount) return this.current;
|
|
283
|
-
const sample = observed.tokens / this.pending.estimate;
|
|
407
|
+
const sample = (observed.tokens - this.pending.offset) / this.pending.estimate;
|
|
284
408
|
this.pending = undefined;
|
|
285
409
|
if (!Number.isFinite(sample) || sample <= 0) return this.current;
|
|
286
410
|
const smoothing = this.options.smoothing ?? 0.5;
|