@cortexkit/opencode-magic-context 0.42.5 → 0.42.6
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 +2 -2
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +1 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-jyj54gbe.js → conflict-warning-hook-aktepwpz.js} +3 -3
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/context-authority.d.ts +28 -1
- package/dist/features/magic-context/context-authority.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/open-opencode-db.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts +1 -0
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +12 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +1 -0
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +1 -1
- package/dist/features/magic-context/tool-owner-backfill.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +0 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts +8 -0
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts +12 -0
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +4 -4
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts +3 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts +24 -2
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-wire.d.ts +1 -1
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
- package/dist/hooks/magic-context/producer-window-guard.d.ts +31 -1
- package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +6 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +8 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-formatting.d.ts +6 -0
- package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +30 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-refusal-recovery.d.ts +28 -0
- package/dist/hooks/magic-context/rust-refusal-recovery.d.ts.map +1 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +7 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-jkrqhc7y.js → index-a9ssj59b.js} +94 -15
- package/dist/{index-3wbd8sdq.js → index-djp6rwz6.js} +455 -46
- package/dist/{index-p474rw9p.js → index-ngjmj6xx.js} +1 -1
- package/dist/{index-d8mgqezq.js → index-s61hh7gv.js} +50 -17
- package/dist/{index-1ar7b3e9.js → index-znraa0kv.js} +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1485 -360
- package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/memory-id-translation.d.ts +23 -0
- package/dist/plugin/memory-id-translation.d.ts.map +1 -0
- package/dist/plugin/rust-tool-backends.d.ts +1 -1
- package/dist/plugin/rust-tool-backends.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-k20sfpm2.js → recomp-orchestrator-0xx1364v.js} +4 -4
- package/dist/{send-session-notification-xnypkxpx.js → send-session-notification-jbxknvar.js} +2 -2
- package/dist/shared/auto-update-provenance.d.ts +9 -0
- package/dist/shared/auto-update-provenance.d.ts.map +1 -0
- package/dist/shared/conflict-detector.d.ts +5 -1
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/opencode-db-path.d.ts +31 -2
- package/dist/shared/opencode-db-path.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts +3 -3
- package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
- package/dist/{storage-meta-session-x3j0q9qy.js → storage-meta-session-z818pn1e.js} +2 -2
- package/dist/tools/ctx-expand/mode.d.ts +31 -0
- package/dist/tools/ctx-expand/mode.d.ts.map +1 -0
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/constants.d.ts +1 -1
- package/dist/tools/ctx-note/constants.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/types.d.ts +1 -1
- package/dist/tools/ctx-note/types.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/constants.d.ts +1 -1
- package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
- package/dist/tools/ctx-search/types.d.ts +1 -1
- package/dist/tools/ctx-search/types.d.ts.map +1 -1
- package/dist/tools/light-descriptions.d.ts +2 -2
- package/dist/tools/light-descriptions.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts +69 -13
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts +13 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +31 -0
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -0
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/hooks/types.d.ts +99 -19
- package/dist/v2/hooks/types.d.ts.map +1 -1
- package/dist/v2/server.d.ts +19 -0
- package/dist/v2/server.d.ts.map +1 -1
- package/dist/v2/server.js +1781 -253
- package/dist/v2/store-reader.d.ts +11 -5
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/v2/tui/index.d.ts +10 -0
- package/dist/v2/tui/index.d.ts.map +1 -0
- package/dist/v2/tui/types.d.ts +80 -0
- package/dist/v2/tui/types.d.ts.map +1 -0
- package/package.json +5 -4
- package/src/agents/magic-context-prompt.ts +1 -1
- package/src/config/index.ts +31 -1
- package/src/config/protected-tokens.test.ts +40 -0
- package/src/config/schema/magic-context.ts +8 -3
- package/src/shared/auto-update-provenance.ts +42 -0
- package/src/shared/conflict-detector.test.ts +53 -0
- package/src/shared/conflict-detector.ts +88 -19
- package/src/shared/models-dev-cache.test.ts +25 -0
- package/src/shared/opencode-db-path.test.ts +96 -0
- package/src/shared/opencode-db-path.ts +146 -15
- package/src/shared/prompt-surface-a1-golden.md +12 -16
- package/src/shared/prompt-surface-runtime.test.ts +20 -0
- package/src/shared/prompt-surface-runtime.ts +7 -5
- package/src/tui/entry.mjs +11 -1
- package/src/v2/tui/host-contract.test.ts +202 -0
- package/src/v2/tui/index.ts +270 -0
- package/src/v2/tui/types.ts +67 -0
|
@@ -3,11 +3,14 @@ import { join } from "node:path";
|
|
|
3
3
|
import { readJsoncFile } from "./jsonc-parser";
|
|
4
4
|
import { log } from "./logger";
|
|
5
5
|
import { getOpenCodeConfigPaths } from "./opencode-config-dir";
|
|
6
|
+
import type { OpenCodeHostGeneration } from "./opencode-db-path";
|
|
6
7
|
|
|
7
8
|
interface OpenCodeConfig {
|
|
8
9
|
compaction?: {
|
|
9
10
|
auto?: boolean;
|
|
10
11
|
prune?: boolean;
|
|
12
|
+
keep?: { tokens?: number };
|
|
13
|
+
buffer?: number;
|
|
11
14
|
};
|
|
12
15
|
// OpenCode allows plugins as plain strings or [name, options] tuples.
|
|
13
16
|
plugin?: Array<string | [string, unknown]>;
|
|
@@ -62,6 +65,8 @@ export interface ConflictResult {
|
|
|
62
65
|
export interface ResolvedCompaction {
|
|
63
66
|
auto: boolean;
|
|
64
67
|
prune: boolean;
|
|
68
|
+
keepTokens?: number;
|
|
69
|
+
buffer?: number;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|
|
@@ -92,6 +97,7 @@ export interface ResolvedCompaction {
|
|
|
92
97
|
export interface DetectConflictsOptions {
|
|
93
98
|
compactionEnabled?: boolean;
|
|
94
99
|
resolvedCompaction?: ResolvedCompaction;
|
|
100
|
+
hostGeneration?: OpenCodeHostGeneration;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
/**
|
|
@@ -113,6 +119,7 @@ export function detectConflicts(
|
|
|
113
119
|
options?: DetectConflictsOptions,
|
|
114
120
|
): ConflictResult {
|
|
115
121
|
const compactionEnabled = options?.compactionEnabled ?? true;
|
|
122
|
+
const hostGeneration = options?.hostGeneration ?? "v1";
|
|
116
123
|
const conflicts: ConflictResult["conflicts"] = {
|
|
117
124
|
compactionAuto: false,
|
|
118
125
|
compactionPrune: false,
|
|
@@ -129,7 +136,8 @@ export function detectConflicts(
|
|
|
129
136
|
// wrongly disabling the plugin for users whose auto=false lives in a layer
|
|
130
137
|
// the file reader cannot see). When the resolved fetch failed, fall back to
|
|
131
138
|
// the file-based check unchanged.
|
|
132
|
-
let compactionResult =
|
|
139
|
+
let compactionResult =
|
|
140
|
+
options?.resolvedCompaction ?? checkCompaction(directory, hostGeneration);
|
|
133
141
|
// OPENCODE_DISABLE_AUTOCOMPACT short-circuits BOTH arms: it is the first,
|
|
134
142
|
// cheapest check and is correct regardless of which arm produced the value.
|
|
135
143
|
// (checkCompaction already applies it internally for the file arm; this
|
|
@@ -141,7 +149,7 @@ export function detectConflicts(
|
|
|
141
149
|
// compaction-off mode the user has explicitly handed the window to native
|
|
142
150
|
// compaction (or nothing), so compaction.auto=true / prune=true are the
|
|
143
151
|
// intended state, not a plugin-disabling conflict.
|
|
144
|
-
if (compactionEnabled && compactionResult.auto) {
|
|
152
|
+
if (hostGeneration === "v1" && compactionEnabled && compactionResult.auto) {
|
|
145
153
|
conflicts.compactionAuto = true;
|
|
146
154
|
reasons.push(
|
|
147
155
|
options?.resolvedCompaction
|
|
@@ -149,7 +157,7 @@ export function detectConflicts(
|
|
|
149
157
|
: "OpenCode auto-compaction is enabled (compaction.auto=true)",
|
|
150
158
|
);
|
|
151
159
|
}
|
|
152
|
-
if (compactionEnabled && compactionResult.prune) {
|
|
160
|
+
if (hostGeneration === "v1" && compactionEnabled && compactionResult.prune) {
|
|
153
161
|
conflicts.compactionPrune = true;
|
|
154
162
|
reasons.push(
|
|
155
163
|
options?.resolvedCompaction
|
|
@@ -192,7 +200,10 @@ export function detectConflicts(
|
|
|
192
200
|
hasConflict: reasons.length > 0,
|
|
193
201
|
reasons,
|
|
194
202
|
conflicts,
|
|
195
|
-
nativeCompaction: {
|
|
203
|
+
nativeCompaction: {
|
|
204
|
+
auto: compactionResult.auto,
|
|
205
|
+
prune: hostGeneration === "v1" ? compactionResult.prune : false,
|
|
206
|
+
},
|
|
196
207
|
};
|
|
197
208
|
}
|
|
198
209
|
|
|
@@ -221,6 +232,8 @@ interface ResolvedCompactionBlock {
|
|
|
221
232
|
compaction?: {
|
|
222
233
|
auto?: boolean;
|
|
223
234
|
prune?: boolean;
|
|
235
|
+
keep?: { tokens?: number };
|
|
236
|
+
buffer?: number;
|
|
224
237
|
};
|
|
225
238
|
}
|
|
226
239
|
|
|
@@ -249,6 +262,7 @@ interface ResolvedCompactionBlock {
|
|
|
249
262
|
export async function resolveCompactionForBoot(
|
|
250
263
|
client: OpencodeConfigClientLike,
|
|
251
264
|
timeoutMs = 2_000,
|
|
265
|
+
hostGeneration: OpenCodeHostGeneration = "v1",
|
|
252
266
|
): Promise<ResolvedCompaction | null> {
|
|
253
267
|
const controller = new AbortController();
|
|
254
268
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -276,6 +290,28 @@ export async function resolveCompactionForBoot(
|
|
|
276
290
|
// The SDK's generated `Config` type has no `compaction` key, so read it
|
|
277
291
|
// defensively from the runtime response.
|
|
278
292
|
const compaction = (result?.data as ResolvedCompactionBlock | undefined)?.compaction;
|
|
293
|
+
if (hostGeneration === "v2") {
|
|
294
|
+
if (typeof compaction?.auto !== "boolean") {
|
|
295
|
+
log(
|
|
296
|
+
`[magic-context] conflict-detector: resolved v2 config carried no explicit compaction.auto (${JSON.stringify(compaction) ?? "absent"}); falling back to file-based detection`,
|
|
297
|
+
);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const keepTokens = compaction.keep?.tokens;
|
|
301
|
+
const buffer = compaction.buffer;
|
|
302
|
+
if (
|
|
303
|
+
(keepTokens !== undefined && (!Number.isInteger(keepTokens) || keepTokens < 0)) ||
|
|
304
|
+
(buffer !== undefined && (!Number.isInteger(buffer) || buffer < 0))
|
|
305
|
+
) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
auto: compaction.auto,
|
|
310
|
+
prune: false,
|
|
311
|
+
...(keepTokens === undefined ? {} : { keepTokens }),
|
|
312
|
+
...(buffer === undefined ? {} : { buffer }),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
279
315
|
// Explicit booleans only. An absent block (or non-boolean values) means
|
|
280
316
|
// the response shape did not carry the resolved state — fall back to the
|
|
281
317
|
// file arm rather than resolving to the plugin-disabling default.
|
|
@@ -293,28 +329,30 @@ export async function resolveCompactionForBoot(
|
|
|
293
329
|
}
|
|
294
330
|
}
|
|
295
331
|
|
|
296
|
-
function checkCompaction(
|
|
332
|
+
function checkCompaction(
|
|
333
|
+
directory: string,
|
|
334
|
+
hostGeneration: OpenCodeHostGeneration,
|
|
335
|
+
): ResolvedCompaction {
|
|
297
336
|
if (process.env.OPENCODE_DISABLE_AUTOCOMPACT) {
|
|
298
337
|
return { auto: false, prune: false };
|
|
299
338
|
}
|
|
300
339
|
|
|
301
340
|
// Check project-level config first (higher precedence)
|
|
302
|
-
const projectResult = readProjectCompaction(directory);
|
|
341
|
+
const projectResult = readProjectCompaction(directory, hostGeneration);
|
|
303
342
|
if (projectResult.resolved) return projectResult;
|
|
304
343
|
|
|
305
344
|
// Fall back to user-level config
|
|
306
|
-
const userResult = readUserCompaction();
|
|
345
|
+
const userResult = readUserCompaction(hostGeneration);
|
|
307
346
|
if (userResult.resolved) return userResult;
|
|
308
347
|
|
|
309
348
|
// Default: OpenCode has compaction enabled by default
|
|
310
349
|
return { auto: true, prune: false };
|
|
311
350
|
}
|
|
312
351
|
|
|
313
|
-
function readProjectCompaction(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
} {
|
|
352
|
+
function readProjectCompaction(
|
|
353
|
+
directory: string,
|
|
354
|
+
hostGeneration: OpenCodeHostGeneration,
|
|
355
|
+
): ResolvedCompaction & { resolved: boolean } {
|
|
318
356
|
// .opencode/ config has higher precedence
|
|
319
357
|
const dotOcJsonc = join(directory, ".opencode", "opencode.jsonc");
|
|
320
358
|
const dotOcJson = join(directory, ".opencode", "opencode.json");
|
|
@@ -323,8 +361,12 @@ function readProjectCompaction(directory: string): {
|
|
|
323
361
|
|
|
324
362
|
if (dotOcConfig?.compaction) {
|
|
325
363
|
const c = dotOcConfig.compaction;
|
|
326
|
-
if (
|
|
327
|
-
|
|
364
|
+
if (
|
|
365
|
+
c.auto !== undefined ||
|
|
366
|
+
(hostGeneration === "v1" && c.prune !== undefined) ||
|
|
367
|
+
(hostGeneration === "v2" && (c.keep?.tokens !== undefined || c.buffer !== undefined))
|
|
368
|
+
) {
|
|
369
|
+
return resolvedCompactionBlock(c, hostGeneration);
|
|
328
370
|
}
|
|
329
371
|
}
|
|
330
372
|
|
|
@@ -336,15 +378,21 @@ function readProjectCompaction(directory: string): {
|
|
|
336
378
|
|
|
337
379
|
if (rootConfig?.compaction) {
|
|
338
380
|
const c = rootConfig.compaction;
|
|
339
|
-
if (
|
|
340
|
-
|
|
381
|
+
if (
|
|
382
|
+
c.auto !== undefined ||
|
|
383
|
+
(hostGeneration === "v1" && c.prune !== undefined) ||
|
|
384
|
+
(hostGeneration === "v2" && (c.keep?.tokens !== undefined || c.buffer !== undefined))
|
|
385
|
+
) {
|
|
386
|
+
return resolvedCompactionBlock(c, hostGeneration);
|
|
341
387
|
}
|
|
342
388
|
}
|
|
343
389
|
|
|
344
390
|
return { auto: false, prune: false, resolved: false };
|
|
345
391
|
}
|
|
346
392
|
|
|
347
|
-
function readUserCompaction(
|
|
393
|
+
function readUserCompaction(
|
|
394
|
+
hostGeneration: OpenCodeHostGeneration,
|
|
395
|
+
): ResolvedCompaction & { resolved: boolean } {
|
|
348
396
|
try {
|
|
349
397
|
const paths = getOpenCodeConfigPaths({ binary: "opencode" });
|
|
350
398
|
const config =
|
|
@@ -353,8 +401,13 @@ function readUserCompaction(): { auto: boolean; prune: boolean; resolved: boolea
|
|
|
353
401
|
|
|
354
402
|
if (config?.compaction) {
|
|
355
403
|
const c = config.compaction;
|
|
356
|
-
if (
|
|
357
|
-
|
|
404
|
+
if (
|
|
405
|
+
c.auto !== undefined ||
|
|
406
|
+
(hostGeneration === "v1" && c.prune !== undefined) ||
|
|
407
|
+
(hostGeneration === "v2" &&
|
|
408
|
+
(c.keep?.tokens !== undefined || c.buffer !== undefined))
|
|
409
|
+
) {
|
|
410
|
+
return resolvedCompactionBlock(c, hostGeneration);
|
|
358
411
|
}
|
|
359
412
|
}
|
|
360
413
|
} catch {
|
|
@@ -363,6 +416,22 @@ function readUserCompaction(): { auto: boolean; prune: boolean; resolved: boolea
|
|
|
363
416
|
return { auto: false, prune: false, resolved: false };
|
|
364
417
|
}
|
|
365
418
|
|
|
419
|
+
function resolvedCompactionBlock(
|
|
420
|
+
block: NonNullable<OpenCodeConfig["compaction"]>,
|
|
421
|
+
hostGeneration: OpenCodeHostGeneration,
|
|
422
|
+
): ResolvedCompaction & { resolved: true } {
|
|
423
|
+
if (hostGeneration === "v2") {
|
|
424
|
+
return {
|
|
425
|
+
auto: block.auto !== false,
|
|
426
|
+
prune: false,
|
|
427
|
+
resolved: true,
|
|
428
|
+
...(typeof block.keep?.tokens === "number" ? { keepTokens: block.keep.tokens } : {}),
|
|
429
|
+
...(typeof block.buffer === "number" ? { buffer: block.buffer } : {}),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return { auto: block.auto === true, prune: block.prune === true, resolved: true };
|
|
433
|
+
}
|
|
434
|
+
|
|
366
435
|
// --- DCP detection ---
|
|
367
436
|
|
|
368
437
|
/**
|
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { resolveKnownHistorianContextLimit } from "../hooks/magic-context/derive-budgets";
|
|
5
6
|
import {
|
|
6
7
|
clearModelsDevCache,
|
|
7
8
|
getModelsDevCacheState,
|
|
@@ -230,6 +231,30 @@ describe("models-dev-cache (SDK-only)", () => {
|
|
|
230
231
|
expect(getSdkContextLimit("openai", "gpt-5.4-mini")).toBe(922000);
|
|
231
232
|
});
|
|
232
233
|
|
|
234
|
+
test("explicit SDK-resolved config limit wins over a larger catalog or detected value", async () => {
|
|
235
|
+
// OpenCode's config.providers() has already applied opencode.json over its
|
|
236
|
+
// catalog. The cache must preserve that 120K effective value, and a later
|
|
237
|
+
// API-detected 262K value may narrow but never enlarge it.
|
|
238
|
+
await refreshModelLimitsFromApi(
|
|
239
|
+
makeClient([
|
|
240
|
+
{
|
|
241
|
+
id: "regolo",
|
|
242
|
+
models: {
|
|
243
|
+
"qwen3.5-122b": { limit: { context: 120_000, output: 32_000 } },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
]),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
expect(
|
|
250
|
+
getSdkContextLimit("regolo", "qwen3.5-122b", 262_144, {
|
|
251
|
+
reservation: "none",
|
|
252
|
+
detectedLimitProvenance: "combined",
|
|
253
|
+
}),
|
|
254
|
+
).toBe(120_000);
|
|
255
|
+
expect(resolveKnownHistorianContextLimit("regolo/qwen3.5-122b")).toBe(120_000);
|
|
256
|
+
});
|
|
257
|
+
|
|
233
258
|
test("narrows raw context with detected wire truth before reserving output", async () => {
|
|
234
259
|
await refreshModelLimitsFromApi(
|
|
235
260
|
makeClient([
|
|
@@ -5,13 +5,17 @@ import { mkdirSync, mkdtempSync, rmSync, unlinkSync, utimesSync, writeFileSync }
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import {
|
|
8
|
+
assertOpenCodeStoreGeneration,
|
|
9
|
+
detectOpenCodeStoreGeneration,
|
|
8
10
|
formatOpenCodeDbDoctorLine,
|
|
9
11
|
formatOpenCodeDbMissingBanner,
|
|
10
12
|
formatOpenCodeDbMissingStatusLine,
|
|
11
13
|
openCodeDbPathExists,
|
|
12
14
|
resetOpenCodeDbPathStateForTesting,
|
|
13
15
|
resolveOpenCodeDbPath,
|
|
16
|
+
sourceOpenCodeDatabaseFilename,
|
|
14
17
|
} from "./opencode-db-path";
|
|
18
|
+
import { Database } from "./sqlite";
|
|
15
19
|
|
|
16
20
|
const ORIGINAL_ENV = {
|
|
17
21
|
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
|
|
@@ -173,6 +177,98 @@ describe("resolveOpenCodeDbPath", () => {
|
|
|
173
177
|
});
|
|
174
178
|
});
|
|
175
179
|
|
|
180
|
+
it("uses the verbatim v2 channel filename table without changing v1 defaults", () => {
|
|
181
|
+
const { dataHome, openCodeDir } = useDataHome();
|
|
182
|
+
for (const channel of ["latest", "dev", "beta", "next", "prod"]) {
|
|
183
|
+
expect(sourceOpenCodeDatabaseFilename("v2", channel, {})).toBe("opencode.db");
|
|
184
|
+
}
|
|
185
|
+
expect(sourceOpenCodeDatabaseFilename("v2", "a/b c!._-", {})).toBe("opencode-abc._-.db");
|
|
186
|
+
expect(
|
|
187
|
+
resolveOpenCodeDbPath("v2", {
|
|
188
|
+
dataHome,
|
|
189
|
+
channel: "local",
|
|
190
|
+
env: { OPENCODE_DB: "opencode2.db" },
|
|
191
|
+
}),
|
|
192
|
+
).toEqual({
|
|
193
|
+
path: join(openCodeDir, "opencode2.db"),
|
|
194
|
+
source: "OPENCODE_DB",
|
|
195
|
+
channel: null,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("detects store generations and refuses a mismatched schema before reading", () => {
|
|
200
|
+
const { openCodeDir } = useDataHome();
|
|
201
|
+
const v1Path = join(openCodeDir, "v1.db");
|
|
202
|
+
const v2Path = join(openCodeDir, "v2.db");
|
|
203
|
+
const v1 = new Database(v1Path);
|
|
204
|
+
const v2 = new Database(v2Path);
|
|
205
|
+
try {
|
|
206
|
+
v1.exec("CREATE TABLE message(id TEXT); CREATE TABLE part(id TEXT)");
|
|
207
|
+
v2.exec("CREATE TABLE session_message(id TEXT)");
|
|
208
|
+
expect(detectOpenCodeStoreGeneration(v1)).toBe("v1");
|
|
209
|
+
expect(detectOpenCodeStoreGeneration(v2)).toBe("v2");
|
|
210
|
+
expect(() => assertOpenCodeStoreGeneration(v2, "v1", v2Path)).toThrow(
|
|
211
|
+
"expected v1, found v2",
|
|
212
|
+
);
|
|
213
|
+
expect(() => assertOpenCodeStoreGeneration(v1, "v2", v1Path)).toThrow(
|
|
214
|
+
"expected v2, found v1",
|
|
215
|
+
);
|
|
216
|
+
} finally {
|
|
217
|
+
v1.close();
|
|
218
|
+
v2.close();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("reads a live OpenCode 1.18.x store as v1 even though it ships session_message", () => {
|
|
223
|
+
const { openCodeDir } = useDataHome();
|
|
224
|
+
const livePath = join(openCodeDir, "live-v1.db");
|
|
225
|
+
const live = new Database(livePath);
|
|
226
|
+
try {
|
|
227
|
+
// Captured from a running OpenCode 1.18.30 store. session_message exists in v1,
|
|
228
|
+
// so a detector keyed on its presence calls a v1 host v2 and every v1 reader
|
|
229
|
+
// (historian chunk, marker discovery, message index, tool-owner backfill) then
|
|
230
|
+
// refuses. Keep this table list as observed, not as remembered.
|
|
231
|
+
for (const table of [
|
|
232
|
+
"account",
|
|
233
|
+
"event",
|
|
234
|
+
"message",
|
|
235
|
+
"part",
|
|
236
|
+
"permission",
|
|
237
|
+
"project",
|
|
238
|
+
"session",
|
|
239
|
+
"session_message",
|
|
240
|
+
"session_run_lease",
|
|
241
|
+
"todo",
|
|
242
|
+
"workspace",
|
|
243
|
+
]) {
|
|
244
|
+
live.exec(`CREATE TABLE ${table}(id TEXT)`);
|
|
245
|
+
}
|
|
246
|
+
expect(detectOpenCodeStoreGeneration(live)).toBe("v1");
|
|
247
|
+
expect(() => assertOpenCodeStoreGeneration(live, "v1", livePath)).not.toThrow();
|
|
248
|
+
expect(() => assertOpenCodeStoreGeneration(live, "v2", livePath)).toThrow(
|
|
249
|
+
"expected v2, found v1",
|
|
250
|
+
);
|
|
251
|
+
} finally {
|
|
252
|
+
live.close();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("treats a store with no schema yet as empty rather than as a conflicting host", () => {
|
|
257
|
+
const { openCodeDir } = useDataHome();
|
|
258
|
+
const freshPath = join(openCodeDir, "fresh.db");
|
|
259
|
+
const fresh = new Database(freshPath);
|
|
260
|
+
try {
|
|
261
|
+
// A host that has not written its first row looks like this. Readers have always
|
|
262
|
+
// seen it as empty; refusing here throws inside the historian, marker and index
|
|
263
|
+
// readers on every fresh data directory.
|
|
264
|
+
expect(detectOpenCodeStoreGeneration(fresh)).toBe("unknown");
|
|
265
|
+
expect(() => assertOpenCodeStoreGeneration(fresh, "v1", freshPath)).not.toThrow();
|
|
266
|
+
expect(() => assertOpenCodeStoreGeneration(fresh, "v2", freshPath)).not.toThrow();
|
|
267
|
+
} finally {
|
|
268
|
+
fresh.close();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
176
272
|
it("formats the missing banner, status, and doctor lines by value", () => {
|
|
177
273
|
const { openCodeDir } = useDataHome();
|
|
178
274
|
const lookedFor = [
|
|
@@ -3,6 +3,25 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
4
|
|
|
5
5
|
export type OpenCodeDbPathSource = "OPENCODE_DB" | "channel" | "default" | "discovered";
|
|
6
|
+
export type OpenCodeHostGeneration = "v1" | "v2";
|
|
7
|
+
export type OpenCodeStoreGeneration = OpenCodeHostGeneration | "unknown";
|
|
8
|
+
|
|
9
|
+
export interface ResolveOpenCodeDbPathOptions {
|
|
10
|
+
dataHome?: string;
|
|
11
|
+
channel?: string;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function openCodeHostGenerationFromVersion(
|
|
16
|
+
version: string | null | undefined,
|
|
17
|
+
): OpenCodeHostGeneration {
|
|
18
|
+
const major = Number.parseInt(version?.match(/\d+/)?.[0] ?? "", 10);
|
|
19
|
+
return Number.isFinite(major) && major >= 2 ? "v2" : "v1";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OpenCodeStoreSchemaDatabase {
|
|
23
|
+
prepare(sql: string): { all(...params: unknown[]): unknown[] };
|
|
24
|
+
}
|
|
6
25
|
|
|
7
26
|
export interface OpenCodeDbPathResolution {
|
|
8
27
|
path: string;
|
|
@@ -24,16 +43,22 @@ let cachedResolution: CachedResolution | null = null;
|
|
|
24
43
|
let lastReadFailure: OpenCodeDbReadFailure | null = null;
|
|
25
44
|
const claimedDiagnostics = new Set<string>();
|
|
26
45
|
|
|
27
|
-
function openCodeDataDir(): string {
|
|
28
|
-
return join(
|
|
46
|
+
function openCodeDataDir(env: NodeJS.ProcessEnv = process.env, dataHome?: string): string {
|
|
47
|
+
return join(dataHome ?? env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "opencode");
|
|
29
48
|
}
|
|
30
49
|
|
|
31
|
-
function environmentKey(
|
|
50
|
+
function environmentKey(
|
|
51
|
+
dataDir: string,
|
|
52
|
+
hostGeneration: OpenCodeHostGeneration,
|
|
53
|
+
channel: string | undefined,
|
|
54
|
+
env: NodeJS.ProcessEnv,
|
|
55
|
+
): string {
|
|
32
56
|
return [
|
|
57
|
+
hostGeneration,
|
|
33
58
|
dataDir,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
env.OPENCODE_DB ?? "",
|
|
60
|
+
env.OPENCODE_DISABLE_CHANNEL_DB ?? "",
|
|
61
|
+
channel ?? env.OPENCODE_CHANNEL ?? "",
|
|
37
62
|
].join("\0");
|
|
38
63
|
}
|
|
39
64
|
|
|
@@ -81,8 +106,11 @@ function discoverOpenCodeDb(dataDir: string): OpenCodeDbPathResolution {
|
|
|
81
106
|
return { path: existing.path, source: "discovered", channel };
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
function
|
|
85
|
-
|
|
109
|
+
function resolveV1Fresh(
|
|
110
|
+
dataDir: string,
|
|
111
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
112
|
+
): OpenCodeDbPathResolution {
|
|
113
|
+
const explicit = env.OPENCODE_DB;
|
|
86
114
|
if (explicit !== undefined && explicit.length > 0) {
|
|
87
115
|
if (explicit === ":memory:") {
|
|
88
116
|
return { path: explicit, source: "OPENCODE_DB", channel: null };
|
|
@@ -94,12 +122,12 @@ function resolveFresh(dataDir: string): OpenCodeDbPathResolution {
|
|
|
94
122
|
};
|
|
95
123
|
}
|
|
96
124
|
|
|
97
|
-
const disableChannelDb =
|
|
125
|
+
const disableChannelDb = env.OPENCODE_DISABLE_CHANNEL_DB;
|
|
98
126
|
if (disableChannelDb === "1" || disableChannelDb === "true") {
|
|
99
127
|
return { path: join(dataDir, "opencode.db"), source: "default", channel: null };
|
|
100
128
|
}
|
|
101
129
|
|
|
102
|
-
const channel =
|
|
130
|
+
const channel = env.OPENCODE_CHANNEL;
|
|
103
131
|
if (channel !== undefined && channel.length > 0) {
|
|
104
132
|
return { path: channelPath(dataDir, channel), source: "channel", channel };
|
|
105
133
|
}
|
|
@@ -107,10 +135,59 @@ function resolveFresh(dataDir: string): OpenCodeDbPathResolution {
|
|
|
107
135
|
return discoverOpenCodeDb(dataDir);
|
|
108
136
|
}
|
|
109
137
|
|
|
110
|
-
/**
|
|
111
|
-
export function
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
/** Match OpenCode 2's filename table and strip path separators from custom channels. */
|
|
139
|
+
export function sourceOpenCodeDatabaseFilename(
|
|
140
|
+
hostGeneration: OpenCodeHostGeneration,
|
|
141
|
+
channel: string,
|
|
142
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
143
|
+
): string {
|
|
144
|
+
if (hostGeneration === "v1") {
|
|
145
|
+
const explicit = env.OPENCODE_DB;
|
|
146
|
+
if (explicit !== undefined && explicit.length > 0) return explicit;
|
|
147
|
+
if (env.OPENCODE_DISABLE_CHANNEL_DB === "1" || env.OPENCODE_DISABLE_CHANNEL_DB === "true") {
|
|
148
|
+
return "opencode.db";
|
|
149
|
+
}
|
|
150
|
+
return ["latest", "beta", "prod"].includes(channel)
|
|
151
|
+
? "opencode.db"
|
|
152
|
+
: `opencode-${channel}.db`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
env.OPENCODE_DB ??
|
|
157
|
+
(["latest", "dev", "beta", "next", "prod"].includes(channel) ||
|
|
158
|
+
env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
|
|
159
|
+
env.OPENCODE_DISABLE_CHANNEL_DB === "true"
|
|
160
|
+
? "opencode.db"
|
|
161
|
+
: `opencode-${channel.replace(/[^a-zA-Z0-9._-]/g, "")}.db`)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resolveV2Fresh(
|
|
166
|
+
dataDir: string,
|
|
167
|
+
channel: string,
|
|
168
|
+
env: NodeJS.ProcessEnv,
|
|
169
|
+
): OpenCodeDbPathResolution {
|
|
170
|
+
const filename = sourceOpenCodeDatabaseFilename("v2", channel, env);
|
|
171
|
+
const explicit = env.OPENCODE_DB !== undefined;
|
|
172
|
+
return {
|
|
173
|
+
path: filename === ":memory:" ? filename : join(dataDir, filename),
|
|
174
|
+
source: explicit ? "OPENCODE_DB" : env.OPENCODE_CHANNEL ? "channel" : "default",
|
|
175
|
+
channel: explicit ? null : channel,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolve the store for one host generation. The default is the historical v1
|
|
181
|
+
* resolver, including candidate discovery and all existing diagnostic text.
|
|
182
|
+
*/
|
|
183
|
+
export function resolveOpenCodeDbPath(
|
|
184
|
+
hostGeneration: OpenCodeHostGeneration = "v1",
|
|
185
|
+
options: ResolveOpenCodeDbPathOptions = {},
|
|
186
|
+
): OpenCodeDbPathResolution {
|
|
187
|
+
const env = options.env ?? process.env;
|
|
188
|
+
const dataDir = openCodeDataDir(env, options.dataHome);
|
|
189
|
+
const channel = options.channel ?? env.OPENCODE_CHANNEL;
|
|
190
|
+
const key = environmentKey(dataDir, hostGeneration, channel, env);
|
|
114
191
|
if (
|
|
115
192
|
cachedResolution?.key === key &&
|
|
116
193
|
(!cachedResolution.existed || existsSync(cachedResolution.resolution.path))
|
|
@@ -119,7 +196,10 @@ export function resolveOpenCodeDbPath(): OpenCodeDbPathResolution {
|
|
|
119
196
|
// An absent result is re-probed so a DB created after plugin boot is detected.
|
|
120
197
|
}
|
|
121
198
|
|
|
122
|
-
const resolution =
|
|
199
|
+
const resolution =
|
|
200
|
+
hostGeneration === "v2"
|
|
201
|
+
? resolveV2Fresh(dataDir, channel ?? "latest", env)
|
|
202
|
+
: resolveV1Fresh(dataDir, env);
|
|
123
203
|
cachedResolution = {
|
|
124
204
|
key,
|
|
125
205
|
resolution,
|
|
@@ -128,6 +208,57 @@ export function resolveOpenCodeDbPath(): OpenCodeDbPathResolution {
|
|
|
128
208
|
return resolution;
|
|
129
209
|
}
|
|
130
210
|
|
|
211
|
+
function schemaTableNames(
|
|
212
|
+
db: OpenCodeStoreSchemaDatabase,
|
|
213
|
+
schema: "main" | "oc_backfill" = "main",
|
|
214
|
+
): Set<string> {
|
|
215
|
+
const rows = db
|
|
216
|
+
.prepare(
|
|
217
|
+
`SELECT name FROM ${schema}.sqlite_master WHERE type = 'table' AND name IN ('message', 'part', 'session', 'project', 'session_message')`,
|
|
218
|
+
)
|
|
219
|
+
.all() as Array<{ name?: unknown }>;
|
|
220
|
+
return new Set(rows.flatMap((row) => (typeof row.name === "string" ? [row.name] : [])));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Detect the persisted host schema.
|
|
225
|
+
*
|
|
226
|
+
* `session_message` does NOT identify v2: OpenCode 1.18.x ships that table beside `message`
|
|
227
|
+
* and `part` (verified against a live 1.18.30 store, pinned in this module's tests). Only the
|
|
228
|
+
* ABSENCE of the v1 message tables identifies a v2 store, so a v1 host is never mistaken for
|
|
229
|
+
* a v2 one. A store carrying neither is a host that has not written its schema yet.
|
|
230
|
+
*/
|
|
231
|
+
export function detectOpenCodeStoreGeneration(
|
|
232
|
+
db: OpenCodeStoreSchemaDatabase,
|
|
233
|
+
schema: "main" | "oc_backfill" = "main",
|
|
234
|
+
): OpenCodeStoreGeneration {
|
|
235
|
+
const tables = schemaTableNames(db, schema);
|
|
236
|
+
const hasV1Messages = tables.has("message") && tables.has("part");
|
|
237
|
+
if (hasV1Messages) return "v1";
|
|
238
|
+
if (tables.has("session_message")) return "v2";
|
|
239
|
+
if (tables.has("session") || tables.has("project")) return "v1";
|
|
240
|
+
return "unknown";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Refuse before a generation-specific query can read the other host's schema. */
|
|
244
|
+
export function assertOpenCodeStoreGeneration(
|
|
245
|
+
db: OpenCodeStoreSchemaDatabase,
|
|
246
|
+
expected: OpenCodeHostGeneration,
|
|
247
|
+
path: string,
|
|
248
|
+
schema: "main" | "oc_backfill" = "main",
|
|
249
|
+
): void {
|
|
250
|
+
const actual = detectOpenCodeStoreGeneration(db, schema);
|
|
251
|
+
if (actual === expected) return;
|
|
252
|
+
// A store with none of these tables has no schema YET — a host that has not written its
|
|
253
|
+
// first row, or a fresh data directory. That is "nothing to read", not a conflicting host,
|
|
254
|
+
// and readers have always treated it as empty. Refusing here made every reader throw before
|
|
255
|
+
// OpenCode created its tables, which is how this guard took down 15 host e2e tests.
|
|
256
|
+
if (actual === "unknown") return;
|
|
257
|
+
throw new Error(
|
|
258
|
+
`OpenCode store generation mismatch at ${path}: expected ${expected}, found ${actual}; refusing generation-specific database access`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
131
262
|
export function openCodeDbPathExists(
|
|
132
263
|
resolution: OpenCodeDbPathResolution = resolveOpenCodeDbPath(),
|
|
133
264
|
): boolean {
|