@cortexkit/opencode-magic-context 0.16.3 → 0.17.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.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/features/magic-context/message-index-async.d.ts +12 -0
  3. package/dist/features/magic-context/message-index-async.d.ts.map +1 -0
  4. package/dist/features/magic-context/message-index.d.ts +4 -0
  5. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  6. package/dist/features/magic-context/migrations.d.ts +7 -0
  7. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  8. package/dist/features/magic-context/search.d.ts +2 -2
  9. package/dist/features/magic-context/search.d.ts.map +1 -1
  10. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  11. package/dist/features/magic-context/storage-meta-persisted.d.ts +3 -6
  12. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-tags.d.ts +163 -1
  14. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  15. package/dist/features/magic-context/storage.d.ts +1 -1
  16. package/dist/features/magic-context/storage.d.ts.map +1 -1
  17. package/dist/features/magic-context/tagger.d.ts +52 -2
  18. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  19. package/dist/features/magic-context/tool-definition-tokens.d.ts +26 -3
  20. package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
  21. package/dist/features/magic-context/tool-owner-backfill.d.ts +90 -0
  22. package/dist/features/magic-context/tool-owner-backfill.d.ts.map +1 -0
  23. package/dist/features/magic-context/types.d.ts +17 -0
  24. package/dist/features/magic-context/types.d.ts.map +1 -1
  25. package/dist/hooks/auto-update-checker/index.d.ts +29 -1
  26. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
  27. package/dist/hooks/auto-update-checker/types.d.ts +19 -0
  28. package/dist/hooks/auto-update-checker/types.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts +23 -0
  31. package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/event-payloads.d.ts +8 -0
  34. package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/inject-compartments.d.ts +16 -0
  39. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/nudger.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/read-session-chunk.d.ts +24 -1
  42. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/read-session-db.d.ts +1 -0
  44. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/read-session-raw.d.ts +1 -0
  46. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/sentinel.d.ts +60 -11
  48. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/strip-content.d.ts +12 -9
  50. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/tag-messages.d.ts +1 -1
  52. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/tool-drop-target.d.ts +16 -1
  54. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +7 -11
  56. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +3215 -1413
  60. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  61. package/dist/plugin/rpc-handlers.d.ts +3 -0
  62. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  63. package/dist/shared/models-dev-cache.d.ts +3 -10
  64. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  65. package/dist/shared/tag-transcript.d.ts.map +1 -1
  66. package/package.json +4 -4
  67. package/src/shared/models-dev-cache.test.ts +64 -57
  68. package/src/shared/models-dev-cache.ts +49 -68
  69. package/src/shared/tag-transcript.ts +137 -126
  70. package/src/tui/types/opencode-plugin-tui.d.ts +1 -1
  71. package/dist/hooks/magic-context/reasoning-capability.d.ts +0 -23
  72. package/dist/hooks/magic-context/reasoning-capability.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"create-session-hooks.d.ts","sourceRoot":"","sources":["../../../src/plugin/hooks/create-session-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAU7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,aAAa,CAAC;IACnB,YAAY,EAAE,wBAAwB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;CACtC;;;;;;qBAqDyvJ,CAAC;;;;;;;;;;;;qBAArnE,CAAC;mBAAyB,CAAC;iBAAuB,CAAC;iBAAuB,CAAC;0BAAc,CAAC;uBAAiB,CAAC;;;;;;0BAA62pB,CAAC;;;;;;EAD/lvB"}
1
+ {"version":3,"file":"create-session-hooks.d.ts","sourceRoot":"","sources":["../../../src/plugin/hooks/create-session-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAU7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,aAAa,CAAC;IACnB,YAAY,EAAE,wBAAwB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;CACtC;;;;;;qBAqDyvJ,CAAC;;;;;;;;;;;;qBAA/1D,CAAC;mBAAyB,CAAC;iBAAuB,CAAC;iBAAuB,CAAC;0BAAc,CAAC;uBAAiB,CAAC;;;;;;0BAAulpB,CAAC;;;;;;EAD/lvB"}
@@ -3,8 +3,11 @@
3
3
  * and returns typed responses for TUI consumption.
4
4
  */
5
5
  import type { MagicContextConfig } from "../config/schema/magic-context";
6
+ import { type ContextDatabase as Database } from "../features/magic-context/storage";
6
7
  import type { LiveSessionState } from "../hooks/magic-context/live-session-state";
7
8
  import type { MagicContextRpcServer } from "../shared/rpc-server";
9
+ import type { SidebarSnapshot } from "../shared/rpc-types";
10
+ export declare function buildSidebarSnapshot(db: Database, sessionId: string, directory: string, liveSessionState?: LiveSessionState, injectionBudgetTokens?: number): SidebarSnapshot;
8
11
  /**
9
12
  * Register all RPC handlers on the server.
10
13
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAMzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAQlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAkflE;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CA8HN"}
1
+ {"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,KAAK,eAAe,IAAI,QAAQ,EAAgB,MAAM,mCAAmC,CAAC;AAQnG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AASlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,qBAAqB,CAAC;AAmDzE,wBAAgB,oBAAoB,CAChC,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,qBAAqB,CAAC,EAAE,MAAM,GAC/B,eAAe,CAySjB;AAoMD;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CAoIN"}
@@ -14,10 +14,9 @@
14
14
  * Used during cold starts before the API cache warms up and in any
15
15
  * code path that cannot reach the SDK client.
16
16
  *
17
- * The public getters (`getModelsDevContextLimit()` and
18
- * `getModelsDevInterleavedField()`) are synchronous: they check the API cache
19
- * first, then the file cache. The plugin warms and refreshes the API cache
20
- * from `src/index.ts` at startup and on a timer.
17
+ * The public getter (`getModelsDevContextLimit()`) is synchronous: it checks
18
+ * the API cache first, then the file cache. The plugin warms and refreshes
19
+ * the API cache from `src/index.ts` at startup and on a timer.
21
20
  */
22
21
  interface OpencodeClientLike {
23
22
  config: {
@@ -52,12 +51,6 @@ export declare function refreshModelLimitsFromApi(client: OpencodeClientLike): P
52
51
  * Returns `undefined` if neither layer knows the model.
53
52
  */
54
53
  export declare function getModelsDevContextLimit(providerID: string, modelID: string): number | undefined;
55
- /**
56
- * Returns the provider-specific interleaved reasoning field when the model
57
- * requires one (for example `reasoning_content` for Moonshot/Kimi style
58
- * providers). Undefined means the cache has no such capability recorded.
59
- */
60
- export declare function getModelsDevInterleavedField(providerID: string, modelID: string): string | undefined;
61
54
  /** Clear in-memory caches (for testing). */
62
55
  export declare function clearModelsDevCache(): void;
63
56
  /** Inspection helpers (for logging / debugging). */
@@ -1 +1 @@
1
- {"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH,UAAU,kBAAkB;IACxB,MAAM,EAAE;QACJ,SAAS,EAAE,MAAM,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,SAAS,CAAC,EAAE,OAAO,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC;KAChE,CAAC;CACL;AAqND;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAchG;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAkBpB;AAED,4CAA4C;AAC5C,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED,oDAAoD;AACpD,wBAAgB,sBAAsB,IAAI;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB,CAOA"}
1
+ {"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,UAAU,kBAAkB;IACxB,MAAM,EAAE;QACJ,SAAS,EAAE,MAAM,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,SAAS,CAAC,EAAE,OAAO,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC;KAChE,CAAC;CACL;AA4MD;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmEzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAchG;AAED,4CAA4C;AAC5C,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C;AAED,oDAAoD;AACpD,wBAAgB,sBAAsB,IAAI;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB,CAOA"}
@@ -1 +1 @@
1
- {"version":3,"file":"tag-transcript.d.ts","sourceRoot":"","sources":["../../src/shared/tag-transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAGzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAM/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACjC;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACnC;AAyDD,wBAAgB,aAAa,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,eAAe,EACnB,OAAO,GAAE,oBAAyB,GACnC,mBAAmB,CA+JrB"}
1
+ {"version":3,"file":"tag-transcript.d.ts","sourceRoot":"","sources":["../../src/shared/tag-transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAGzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAM/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACjC;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACnC;AAyDD,wBAAgB,aAAa,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,eAAe,EACnB,OAAO,GAAE,oBAAyB,GACnC,mBAAmB,CAoKrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.16.3",
3
+ "version": "0.17.1",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@huggingface/transformers": "^4.1.0",
44
- "@opencode-ai/plugin": "^1.2.26",
45
- "@opencode-ai/sdk": "^1.2.26",
44
+ "@opencode-ai/plugin": "^1.14.39",
45
+ "@opencode-ai/sdk": "^1.14.39",
46
46
  "ai-tokenizer": "^1.0.6",
47
47
  "better-sqlite3": "^12.9.0",
48
48
  "comment-json": "^4.2.5",
@@ -71,6 +71,6 @@
71
71
  "tui"
72
72
  ],
73
73
  "peerDependencies": {
74
- "@opencode-ai/plugin": ">=1.2.0"
74
+ "@opencode-ai/plugin": ">=1.14.0"
75
75
  }
76
76
  }
@@ -6,7 +6,6 @@ import {
6
6
  clearModelsDevCache,
7
7
  getModelsDevCacheState,
8
8
  getModelsDevContextLimit,
9
- getModelsDevInterleavedField,
10
9
  refreshModelLimitsFromApi,
11
10
  } from "./models-dev-cache";
12
11
 
@@ -276,62 +275,6 @@ describe("models-dev-cache", () => {
276
275
  expect(state.apiCount).toBe(1);
277
276
  });
278
277
 
279
- test("reads interleaved reasoning field metadata from models.json", () => {
280
- const opencodeDir = join(tempDir, "opencode");
281
- mkdirSync(opencodeDir, { recursive: true });
282
- writeFileSync(
283
- join(opencodeDir, "models.json"),
284
- JSON.stringify({
285
- "opencode-go": {
286
- models: {
287
- "kimi-k2.6": {
288
- limit: { context: 262144 },
289
- interleaved: { field: "reasoning_content" },
290
- },
291
- "plain-model": {
292
- limit: { context: 262144 },
293
- },
294
- },
295
- },
296
- }),
297
- );
298
-
299
- expect(getModelsDevInterleavedField("opencode-go", "kimi-k2.6")).toBe("reasoning_content");
300
- expect(getModelsDevInterleavedField("opencode-go", "plain-model")).toBeUndefined();
301
- });
302
-
303
- test("API cache exposes interleaved reasoning metadata", async () => {
304
- const mockClient = {
305
- config: {
306
- providers: async () => ({
307
- data: {
308
- providers: [
309
- {
310
- id: "opencode-go",
311
- models: {
312
- "kimi-k2.6": {
313
- limit: { context: 262144 },
314
- capabilities: {
315
- interleaved: { field: "reasoning_content" },
316
- },
317
- },
318
- "plain-model": {
319
- limit: { context: 262144 },
320
- capabilities: { interleaved: false },
321
- },
322
- },
323
- },
324
- ],
325
- },
326
- }),
327
- },
328
- };
329
- await refreshModelLimitsFromApi(mockClient);
330
-
331
- expect(getModelsDevInterleavedField("opencode-go", "kimi-k2.6")).toBe("reasoning_content");
332
- expect(getModelsDevInterleavedField("opencode-go", "plain-model")).toBeUndefined();
333
- });
334
-
335
278
  test("refreshModelLimitsFromApi tolerates empty/malformed responses", async () => {
336
279
  // Undefined data.
337
280
  await refreshModelLimitsFromApi({
@@ -356,6 +299,70 @@ describe("models-dev-cache", () => {
356
299
  expect(getModelsDevCacheState().apiLoaded).toBe(false);
357
300
  });
358
301
 
302
+ test("suppresses repeated logs when API count oscillates between known sizes", async () => {
303
+ // Simulates github-copilot's /models endpoint returning different model sets
304
+ // between calls. We want first sighting of each new size to log, but once a
305
+ // size has been seen before, further flips between known sizes should be
306
+ // silent (with one "oscillating" notice).
307
+ const sizeA = {
308
+ data: {
309
+ providers: [
310
+ {
311
+ id: "p",
312
+ models: {
313
+ m1: { limit: { context: 100 } },
314
+ m2: { limit: { context: 100 } },
315
+ m3: { limit: { context: 100 } },
316
+ },
317
+ },
318
+ ],
319
+ },
320
+ };
321
+ const sizeB = {
322
+ data: {
323
+ providers: [
324
+ {
325
+ id: "p",
326
+ models: {
327
+ m1: { limit: { context: 100 } },
328
+ m2: { limit: { context: 100 } },
329
+ },
330
+ },
331
+ ],
332
+ },
333
+ };
334
+
335
+ const clientA = { config: { providers: async () => sizeA } };
336
+ const clientB = { config: { providers: async () => sizeB } };
337
+
338
+ // First sighting of size 3 → logs "loaded 3 entries".
339
+ await refreshModelLimitsFromApi(clientA);
340
+ expect(getModelsDevCacheState().apiCount).toBe(3);
341
+
342
+ // First sighting of size 2 → logs "loaded 2 entries (was 3)".
343
+ await refreshModelLimitsFromApi(clientB);
344
+ expect(getModelsDevCacheState().apiCount).toBe(2);
345
+
346
+ // Second sighting of size 3 → logs the "oscillating" notice once.
347
+ await refreshModelLimitsFromApi(clientA);
348
+ expect(getModelsDevCacheState().apiCount).toBe(3);
349
+
350
+ // Second sighting of size 2 → silent (no new log expected).
351
+ await refreshModelLimitsFromApi(clientB);
352
+ expect(getModelsDevCacheState().apiCount).toBe(2);
353
+
354
+ // Third sighting of size 3 → still silent.
355
+ await refreshModelLimitsFromApi(clientA);
356
+ expect(getModelsDevCacheState().apiCount).toBe(3);
357
+
358
+ // The cache itself still updates on every call (model contents are correct
359
+ // for whichever provider response just arrived). The suppression is purely
360
+ // a logging concern. Last call was clientA → all three models present.
361
+ expect(getModelsDevContextLimit("p", "m1")).toBe(100);
362
+ expect(getModelsDevContextLimit("p", "m2")).toBe(100);
363
+ expect(getModelsDevContextLimit("p", "m3")).toBe(100);
364
+ });
365
+
359
366
  test("falls back to file layer when API provider/model key is missing", async () => {
360
367
  const opencodeDir = join(tempDir, "opencode");
361
368
  mkdirSync(opencodeDir, { recursive: true });
@@ -14,10 +14,9 @@
14
14
  * Used during cold starts before the API cache warms up and in any
15
15
  * code path that cannot reach the SDK client.
16
16
  *
17
- * The public getters (`getModelsDevContextLimit()` and
18
- * `getModelsDevInterleavedField()`) are synchronous: they check the API cache
19
- * first, then the file cache. The plugin warms and refreshes the API cache
20
- * from `src/index.ts` at startup and on a timer.
17
+ * The public getter (`getModelsDevContextLimit()`) is synchronous: it checks
18
+ * the API cache first, then the file cache. The plugin warms and refreshes
19
+ * the API cache from `src/index.ts` at startup and on a timer.
21
20
  */
22
21
 
23
22
  import { createHash } from "node:crypto";
@@ -33,18 +32,30 @@ interface OpencodeClientLike {
33
32
  };
34
33
  }
35
34
 
36
- const RELOAD_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, matches OpenCode's TTL
35
+ // File-cache fallback only. The primary `models.json` API refresh is driven
36
+ // by `setInterval(refreshModelLimitsFromApi, ...)` in `index.ts` at a 1-hour
37
+ // cadence; this 5-minute interval governs the on-disk-cache fallback path
38
+ // when the API loader hasn't run yet (e.g. during plugin warmup).
39
+ const RELOAD_INTERVAL_MS = 5 * 60 * 1000;
37
40
 
38
41
  interface CachedModelMetadata {
39
42
  limit?: number;
40
- interleavedField?: string;
41
43
  }
42
44
 
43
- type InterleavedConfig = boolean | { field?: string } | undefined;
44
-
45
45
  /** Populated async from OpenCode SDK. Primary source of truth when available. */
46
46
  let apiCache: Map<string, CachedModelMetadata> | null = null;
47
47
  let apiLoadedAt = 0;
48
+ /**
49
+ * Recently-seen API cache sizes, used to detect oscillation between two
50
+ * stable values (typically caused by upstream provider plugins like
51
+ * github-copilot whose `/models` endpoint returns slightly different model
52
+ * sets between calls based on `model_picker_enabled` toggles). Once the
53
+ * same size has been observed before, we stop logging count changes —
54
+ * the count is a function of upstream behavior we can't control, and
55
+ * repeated logs only add noise.
56
+ */
57
+ const recentlySeenApiSizes = new Set<number>();
58
+ let oscillationLogged = false;
48
59
 
49
60
  /** Populated sync from disk as fallback. */
50
61
  let fileCache: Map<string, CachedModelMetadata> | null = null;
@@ -110,47 +121,28 @@ function resolveLimit(limit: { context?: number; input?: number } | undefined):
110
121
  return undefined;
111
122
  }
112
123
 
113
- function resolveInterleavedField(interleaved: InterleavedConfig): string | undefined {
114
- if (
115
- interleaved &&
116
- typeof interleaved === "object" &&
117
- typeof interleaved.field === "string" &&
118
- interleaved.field.length > 0
119
- ) {
120
- return interleaved.field;
121
- }
122
- return undefined;
123
- }
124
-
125
124
  function setCachedModelMetadata(
126
125
  cache: Map<string, CachedModelMetadata>,
127
126
  key: string,
128
127
  model:
129
128
  | {
130
129
  limit?: { context?: number; input?: number };
131
- capabilities?: { interleaved?: InterleavedConfig };
132
- interleaved?: InterleavedConfig;
133
130
  experimental?: { modes?: Record<string, unknown> };
134
131
  }
135
132
  | undefined,
136
133
  ): void {
137
134
  const limit = resolveLimit(model?.limit);
138
- const interleavedField =
139
- resolveInterleavedField(model?.capabilities?.interleaved) ??
140
- resolveInterleavedField(model?.interleaved);
141
135
 
142
- if (limit === undefined && interleavedField === undefined) {
136
+ if (limit === undefined) {
143
137
  return;
144
138
  }
145
139
 
146
- const value: CachedModelMetadata = {};
147
- if (limit !== undefined) value.limit = limit;
148
- if (interleavedField !== undefined) value.interleavedField = interleavedField;
140
+ const value: CachedModelMetadata = { limit };
149
141
  cache.set(key, value);
150
142
 
151
143
  // OpenCode creates derived model IDs from experimental.modes
152
144
  // e.g. gpt-5.4 + modes.fast → gpt-5.4-fast. These inherit the same
153
- // context limit and interleaved-reasoning contract as the parent model.
145
+ // context limit as the parent model.
154
146
  const modes = model?.experimental?.modes;
155
147
  if (modes && typeof modes === "object") {
156
148
  for (const mode of Object.keys(modes)) {
@@ -176,8 +168,6 @@ function loadModelsDevMetadataFromFile(): Map<string, CachedModelMetadata> {
176
168
  string,
177
169
  {
178
170
  limit?: { context?: number; input?: number };
179
- capabilities?: { interleaved?: InterleavedConfig };
180
- interleaved?: InterleavedConfig;
181
171
  experimental?: { modes?: Record<string, unknown> };
182
172
  }
183
173
  >;
@@ -273,8 +263,6 @@ export async function refreshModelLimitsFromApi(client: OpencodeClientLike): Pro
273
263
  string,
274
264
  {
275
265
  limit?: { context?: number; input?: number };
276
- capabilities?: { interleaved?: InterleavedConfig };
277
- interleaved?: InterleavedConfig;
278
266
  experimental?: { modes?: Record<string, unknown> };
279
267
  }
280
268
  >;
@@ -288,15 +276,34 @@ export async function refreshModelLimitsFromApi(client: OpencodeClientLike): Pro
288
276
  const previousSize = apiCache?.size ?? null;
289
277
  apiCache = map;
290
278
  apiLoadedAt = Date.now();
291
- // Log only on first successful load or when the model count changes,
292
- // so the 5-minute periodic refresh doesn't spam the log.
293
- if (previousSize === null || previousSize !== map.size) {
279
+
280
+ // Log policy:
281
+ // - Always log the first successful load.
282
+ // - Log a count change once per new size we haven't seen before.
283
+ // - When the count returns to a previously-seen size, log an
284
+ // "oscillation" message exactly once explaining the cause, then
285
+ // stay silent on further flips between known sizes.
286
+ if (previousSize === null) {
287
+ recentlySeenApiSizes.add(map.size);
294
288
  sessionLog(
295
289
  "global",
296
- `models-dev-cache: API layer loaded ${map.size} model metadata entries${
297
- previousSize !== null ? ` (was ${previousSize})` : ""
298
- }`,
290
+ `models-dev-cache: API layer loaded ${map.size} model metadata entries`,
299
291
  );
292
+ } else if (previousSize !== map.size) {
293
+ const sizeAlreadySeen = recentlySeenApiSizes.has(map.size);
294
+ recentlySeenApiSizes.add(map.size);
295
+ if (!sizeAlreadySeen) {
296
+ sessionLog(
297
+ "global",
298
+ `models-dev-cache: API layer loaded ${map.size} model metadata entries (was ${previousSize})`,
299
+ );
300
+ } else if (!oscillationLogged) {
301
+ oscillationLogged = true;
302
+ sessionLog(
303
+ "global",
304
+ `models-dev-cache: API count oscillating between ${[...recentlySeenApiSizes].sort((a, b) => a - b).join(" ↔ ")} — likely upstream provider plugin returning slightly different model sets between calls (e.g. github-copilot's /models endpoint toggling model_picker_enabled). Suppressing further size-change logs.`,
305
+ );
306
+ }
300
307
  }
301
308
  } catch (error) {
302
309
  sessionLog(
@@ -334,38 +341,12 @@ export function getModelsDevContextLimit(providerID: string, modelID: string): n
334
341
  return fileCache.get(key)?.limit;
335
342
  }
336
343
 
337
- /**
338
- * Returns the provider-specific interleaved reasoning field when the model
339
- * requires one (for example `reasoning_content` for Moonshot/Kimi style
340
- * providers). Undefined means the cache has no such capability recorded.
341
- */
342
- export function getModelsDevInterleavedField(
343
- providerID: string,
344
- modelID: string,
345
- ): string | undefined {
346
- const key = `${providerID}/${modelID}`;
347
-
348
- if (apiCache) {
349
- const fromApi = apiCache.get(key)?.interleavedField;
350
- if (typeof fromApi === "string" && fromApi.length > 0) {
351
- return fromApi;
352
- }
353
- }
354
-
355
- const now = Date.now();
356
- if (!fileCache || now - fileLastAttempt > RELOAD_INTERVAL_MS) {
357
- fileLastAttempt = now;
358
- fileCache = loadModelsDevMetadataFromFile();
359
- }
360
-
361
- const fromFile = fileCache.get(key)?.interleavedField;
362
- return typeof fromFile === "string" && fromFile.length > 0 ? fromFile : undefined;
363
- }
364
-
365
344
  /** Clear in-memory caches (for testing). */
366
345
  export function clearModelsDevCache(): void {
367
346
  apiCache = null;
368
347
  apiLoadedAt = 0;
348
+ recentlySeenApiSizes.clear();
349
+ oscillationLogged = false;
369
350
  fileCache = null;
370
351
  fileLastAttempt = 0;
371
352
  }