@cortexkit/opencode-magic-context 0.41.0 → 0.41.2

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 (143) hide show
  1. package/dist/config/schema/magic-context.d.ts +15 -0
  2. package/dist/config/schema/magic-context.d.ts.map +1 -1
  3. package/dist/{conflict-warning-hook-xj1r88a7.js → conflict-warning-hook-rf17b6et.js} +3 -3
  4. package/dist/{derive-budgets-3xfyphvk.js → derive-budgets-9q2d3r93.js} +2 -2
  5. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +16 -17
  6. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  7. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -1
  8. package/dist/features/magic-context/context-authority.d.ts.map +1 -1
  9. package/dist/features/magic-context/dreamer/curate-memory-safety.d.ts +21 -0
  10. package/dist/features/magic-context/dreamer/curate-memory-safety.d.ts.map +1 -0
  11. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -6
  13. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  14. package/dist/features/magic-context/memory/embedding-failure.d.ts +10 -2
  15. package/dist/features/magic-context/memory/embedding-failure.d.ts.map +1 -1
  16. package/dist/features/magic-context/memory/embedding-local.d.ts +19 -2
  17. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  18. package/dist/features/magic-context/memory/embedding-synapse.d.ts +10 -1
  19. package/dist/features/magic-context/memory/embedding-synapse.d.ts.map +1 -1
  20. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  21. package/dist/features/magic-context/memory/onnxruntime-node-wasm-shim.d.ts +3 -0
  22. package/dist/features/magic-context/memory/onnxruntime-node-wasm-shim.d.ts.map +1 -0
  23. package/dist/features/magic-context/memory/project-identity.d.ts +4 -0
  24. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  25. package/dist/features/magic-context/memory/sharp-wasm-shim.d.ts +2 -0
  26. package/dist/features/magic-context/memory/sharp-wasm-shim.d.ts.map +1 -0
  27. package/dist/features/magic-context/memory/transformers-node-wasm-entry.d.ts +3 -0
  28. package/dist/features/magic-context/memory/transformers-node-wasm-entry.d.ts.map +1 -0
  29. package/dist/features/magic-context/memory/transformers-web-entry.d.ts +2 -0
  30. package/dist/features/magic-context/memory/transformers-web-entry.d.ts.map +1 -0
  31. package/dist/features/magic-context/message-fts-rowid-map.d.ts +28 -0
  32. package/dist/features/magic-context/message-fts-rowid-map.d.ts.map +1 -0
  33. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  34. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  35. package/dist/features/magic-context/overflow-detection.d.ts +15 -2
  36. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  37. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  38. package/dist/features/magic-context/search.d.ts +18 -0
  39. package/dist/features/magic-context/search.d.ts.map +1 -1
  40. package/dist/features/magic-context/storage-db.d.ts +2 -1
  41. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  42. package/dist/features/magic-context/storage-meta-persisted.d.ts +19 -2
  43. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  44. package/dist/features/magic-context/storage-meta-session.d.ts +3 -2
  45. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  46. package/dist/features/magic-context/storage-meta.d.ts +2 -2
  47. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  48. package/dist/features/magic-context/storage-ops.d.ts +2 -0
  49. package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
  50. package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
  51. package/dist/features/magic-context/storage-session-tables.d.ts +9 -1
  52. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  53. package/dist/features/magic-context/storage-tags.d.ts +13 -0
  54. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  55. package/dist/features/magic-context/storage.d.ts +2 -2
  56. package/dist/features/magic-context/storage.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/boundary-execution.d.ts +3 -0
  58. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/event-handler.d.ts +4 -1
  60. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/format-embed-failure.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
  63. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/hook.d.ts +1 -1
  65. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/live-session-state.d.ts +3 -1
  67. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/rust-mode-transform.d.ts +9 -1
  69. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/sentinel.d.ts +13 -37
  71. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  72. package/dist/hooks/magic-context/strip-content.d.ts +12 -5
  73. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  74. package/dist/hooks/magic-context/tag-id-fallback.d.ts +3 -1
  75. package/dist/hooks/magic-context/tag-id-fallback.d.ts.map +1 -1
  76. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +28 -2
  78. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/transform.d.ts +1 -1
  80. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  81. package/dist/{index-b4gv9f7g.js → index-4x691bz5.js} +87 -2
  82. package/dist/{index-knna83k0.js → index-9j3tx8hy.js} +1 -1
  83. package/dist/{index-jmwq2wxr.js → index-b9j85d29.js} +1 -1
  84. package/dist/{index-2bykz3bx.js → index-cvda5jb3.js} +7 -2
  85. package/dist/{index-bf799gbq.js → index-d1mxrd3p.js} +0 -1
  86. package/dist/{index-dmgbar7r.js → index-eq9wr75c.js} +2 -2
  87. package/dist/{index-w8rkbhn6.js → index-j8r0ejb3.js} +1232 -261
  88. package/dist/{index-26ds03jd.js → index-vnz6jvdc.js} +1 -1
  89. package/dist/{index-x9dmzyp5.js → index-y1yr57fw.js} +81 -17
  90. package/dist/{index-88za3sz8.js → index-zwjhba2r.js} +3 -3
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +1771 -1109
  93. package/dist/{model-resolution-jgrkt0z2.js → model-resolution-6w6wzw18.js} +1 -1
  94. package/dist/plugin/dream-timer.d.ts +4 -0
  95. package/dist/plugin/dream-timer.d.ts.map +1 -1
  96. package/dist/plugin/embedding-routing.d.ts.map +1 -1
  97. package/dist/plugin/hooks/create-session-hooks.d.ts +2 -2
  98. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  99. package/dist/{recomp-orchestrator-1w4xj6jt.js → recomp-orchestrator-m37p3fq6.js} +6 -6
  100. package/dist/{safe-notification-target-p0c0csxw.js → safe-notification-target-2a8py95v.js} +2 -2
  101. package/dist/{send-session-notification-kn34g7mx.js → send-session-notification-bw7eex6w.js} +3 -3
  102. package/dist/shared/index.d.ts +1 -0
  103. package/dist/shared/index.d.ts.map +1 -1
  104. package/dist/shared/logger.d.ts.map +1 -1
  105. package/dist/shared/model-resolution.d.ts.map +1 -1
  106. package/dist/shared/rpc-utils.d.ts +3 -1
  107. package/dist/shared/rpc-utils.d.ts.map +1 -1
  108. package/dist/shared/summarize-child-stderr.d.ts +2 -0
  109. package/dist/shared/summarize-child-stderr.d.ts.map +1 -0
  110. package/dist/shared/test-temp-dir.d.ts +40 -0
  111. package/dist/shared/test-temp-dir.d.ts.map +1 -0
  112. package/dist/shared/window-geometry.d.ts +2 -0
  113. package/dist/shared/window-geometry.d.ts.map +1 -1
  114. package/dist/{storage-meta-session-snbzd26z.js → storage-meta-session-dse3qv6s.js} +5 -3
  115. package/dist/{task-config-ty08ptc2.js → task-config-gv6s04fp.js} +2 -2
  116. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  117. package/dist/tools/ctx-memory/types.d.ts +2 -0
  118. package/dist/tools/ctx-memory/types.d.ts.map +1 -1
  119. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  120. package/dist/tools/ctx-search/tools.d.ts.map +1 -1
  121. package/dist/transformers-node-wasm.js +25676 -0
  122. package/dist/transformers-web.js +24570 -0
  123. package/dist/transformers.node-b1wa0hgx.js +28808 -0
  124. package/package.json +8 -3
  125. package/src/config/index.test.ts +50 -4
  126. package/src/config/latch-permanence-guard.test.ts +5 -0
  127. package/src/config/schema/magic-context.test.ts +20 -0
  128. package/src/config/schema/magic-context.ts +8 -0
  129. package/src/shared/conflict-detector.test.ts +6 -19
  130. package/src/shared/conflict-fixer.test.ts +5 -8
  131. package/src/shared/index.ts +1 -0
  132. package/src/shared/logger.test.ts +135 -40
  133. package/src/shared/logger.ts +115 -2
  134. package/src/shared/model-resolution.test.ts +21 -19
  135. package/src/shared/model-resolution.ts +0 -1
  136. package/src/shared/rpc-utils.test.ts +246 -5
  137. package/src/shared/rpc-utils.ts +340 -49
  138. package/src/shared/summarize-child-stderr.test.ts +55 -0
  139. package/src/shared/summarize-child-stderr.ts +33 -0
  140. package/src/shared/test-temp-dir.test.ts +57 -0
  141. package/src/shared/test-temp-dir.ts +165 -0
  142. package/src/shared/window-geometry.ts +9 -0
  143. /package/dist/{index-rpgg35ms.js → index-8sseka65.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -35,9 +35,10 @@
35
35
  "README.md"
36
36
  ],
37
37
  "scripts": {
38
- "build": "tsc -p ../retina-local-fs/tsconfig.build.json && bun run build:tui && rm -f dist/index.js dist/*-*.js && bun build src/index.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --external @huggingface/transformers --external onnxruntime-web --external bun:sqlite --external node:sqlite && tsc --emitDeclarationOnly",
38
+ "build": "tsc -p ../retina-local-fs/tsconfig.build.json && bun run build:tui && rm -f dist/index.js dist/transformers-web.js dist/transformers-node-wasm.js dist/*-*.js && bun build src/features/magic-context/memory/transformers-web-entry.ts --outfile dist/transformers-web.js --target browser --format esm --external onnxruntime-web && bun scripts/build-transformers-node-wasm.ts dist && bun build src/index.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --external onnxruntime-node --external onnxruntime-web --external sharp --external bun:sqlite --external node:sqlite && tsc --emitDeclarationOnly",
39
39
  "build:tui": "bun scripts/build-tui.ts",
40
40
  "check:tui-compiled": "bun run build:tui && git diff --exit-code -- src/tui-compiled && test -z \"$(git status --porcelain -- src/tui-compiled)\"",
41
+ "check:transformers-node-wasm": "bun scripts/verify-transformers-node-wasm.ts",
41
42
  "typecheck": "tsc -p ../retina-local-fs/tsconfig.build.json && tsc --noEmit && tsc -p tsconfig.scripts.json",
42
43
  "test": "bun install --frozen-lockfile && bun test --parallel --timeout 30000",
43
44
  "test:serial": "bun install --frozen-lockfile && bun test --timeout 30000",
@@ -51,7 +52,6 @@
51
52
  },
52
53
  "dependencies": {
53
54
  "@cortexkit/subc-client": "0.4.1",
54
- "@huggingface/transformers": "^4.1.0",
55
55
  "@jitl/quickjs-singlefile-cjs-release-asyncify": "0.32.0",
56
56
  "@opencode-ai/plugin": "^1.17.11",
57
57
  "@opencode-ai/sdk": "^1.17.11",
@@ -65,8 +65,13 @@
65
65
  "solid-js": "1.9.12",
66
66
  "zod": "^4.1.8"
67
67
  },
68
+ "optionalDependencies": {
69
+ "onnxruntime-node": "1.24.3",
70
+ "sharp": "^0.35.0"
71
+ },
68
72
  "devDependencies": {
69
73
  "@ai-sdk/anthropic": "3.0.82",
74
+ "@huggingface/transformers": "^4.1.0",
70
75
  "@ai-sdk/openai-compatible": "2.0.41",
71
76
  "@biomejs/biome": "^2.5.1",
72
77
  "@types/better-sqlite3": "^7.6.13",
@@ -2,7 +2,14 @@ import { describe, expect, it } from "bun:test";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+
5
6
  import { resolveHistorianModel } from "../shared/model-resolution";
7
+ import { createTestTempDir } from "../shared/test-temp-dir";
8
+ import {
9
+ getWindowOverlay,
10
+ reloadWindowOverlay,
11
+ setWindowOverlayPath,
12
+ } from "../shared/window-geometry";
6
13
  import { loadPluginConfig, loadPluginConfigDetailed } from "./index";
7
14
  import { resolveConfigProfile } from "./profiles";
8
15
  import { DEFAULT_LOCAL_EMBEDDING_MODEL } from "./schema/magic-context";
@@ -111,6 +118,42 @@ function loadWithUserAndProjectConfig(
111
118
  }
112
119
  }
113
120
 
121
+ describe("loadPluginConfig — Fusiform overlay reload", () => {
122
+ it("does not invalidate a same-path overlay during routine config reads", () => {
123
+ const overlayDir = mkdtempSync(join(tmpdir(), "mc-config-overlay-"));
124
+ const overlayPath = join(overlayDir, "window-overlay.json");
125
+ const configText = JSON.stringify({ models: { window_overlay_path: overlayPath } });
126
+ const writeOverlay = (modelId: string) =>
127
+ writeFileSync(
128
+ overlayPath,
129
+ JSON.stringify({
130
+ schema: "fusiform-window-overlay/v1",
131
+ generated_at: "2026-09-01T00:00:00Z",
132
+ minted_provider_ids: [],
133
+ cells: [{ provider_id: "hunt-provider", model_id: modelId, facts: {} }],
134
+ }),
135
+ );
136
+
137
+ try {
138
+ writeOverlay("before-rewrite");
139
+ loadWithUserConfig(configText);
140
+ expect(getWindowOverlay()?.cells[0]?.model_id).toBe("before-rewrite");
141
+
142
+ writeOverlay("after-rewrite");
143
+ expect(getWindowOverlay()?.cells[0]?.model_id).toBe("before-rewrite");
144
+
145
+ loadWithUserConfig(configText);
146
+ expect(getWindowOverlay()?.cells[0]?.model_id).toBe("before-rewrite");
147
+
148
+ reloadWindowOverlay(overlayPath);
149
+ expect(getWindowOverlay()?.cells[0]?.model_id).toBe("after-rewrite");
150
+ } finally {
151
+ setWindowOverlayPath(undefined);
152
+ rmSync(overlayDir, { recursive: true, force: true });
153
+ }
154
+ });
155
+ });
156
+
114
157
  describe("loadPluginConfig — preload user-config isolation", () => {
115
158
  it("resolves schema-default embedding config for a fixture with no config (#388)", () => {
116
159
  const projectDir = mkdtempSync(join(tmpdir(), "mc-config-preload-fixture-"));
@@ -121,6 +164,7 @@ describe("loadPluginConfig — preload user-config isolation", () => {
121
164
  expect(loadPluginConfig(projectDir).embedding).toEqual({
122
165
  provider: "local",
123
166
  model: DEFAULT_LOCAL_EMBEDDING_MODEL,
167
+ local_runtime: "auto",
124
168
  });
125
169
  } finally {
126
170
  rmSync(projectDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
@@ -693,7 +737,8 @@ describe("loadPluginConfig — legacy agent enabled migration", () => {
693
737
 
694
738
  describe("loadPluginConfig — variable expansion scope", () => {
695
739
  it("keeps {env:} and {file:} expansion enabled for user config", () => {
696
- const secretFile = join(mkdtempSync(join(tmpdir(), "mc-config-secret-")), "secret.txt");
740
+ const { dir: secretDir, cleanup } = createTestTempDir("mc-config-secret-");
741
+ const secretFile = join(secretDir, "secret.txt");
697
742
  writeFileSync(secretFile, "file-secret", "utf-8");
698
743
 
699
744
  try {
@@ -715,12 +760,13 @@ describe("loadPluginConfig — variable expansion scope", () => {
715
760
  }
716
761
  expect(result.configWarnings).toBeUndefined();
717
762
  } finally {
718
- rmSync(secretFile, { force: true });
763
+ cleanup();
719
764
  }
720
765
  });
721
766
 
722
767
  it("leaves {env:} and {file:} tokens literal in project config and warns", () => {
723
- const secretFile = join(mkdtempSync(join(tmpdir(), "mc-config-secret-")), "secret.txt");
768
+ const { dir: secretDir, cleanup } = createTestTempDir("mc-config-secret-");
769
+ const secretFile = join(secretDir, "secret.txt");
724
770
  writeFileSync(secretFile, "project-file-secret", "utf-8");
725
771
 
726
772
  try {
@@ -743,7 +789,7 @@ describe("loadPluginConfig — variable expansion scope", () => {
743
789
  expect(warnings).toContain("security reasons");
744
790
  expect(warnings).toContain("embedding.endpoint/provider");
745
791
  } finally {
746
- rmSync(secretFile, { force: true });
792
+ cleanup();
747
793
  }
748
794
  });
749
795
 
@@ -27,6 +27,11 @@ const KNOWN_SLOTS: Record<string, KnownSlot> = {
27
27
  classification: "VERDICT",
28
28
  reason: "Correct: a missing or unloadable native binding needs an install repair, which this process cannot observe.",
29
29
  },
30
+ "packages/plugin/src/features/magic-context/memory/embedding-local.ts:localEmbeddingProcessFailure":
31
+ {
32
+ classification: "DIAGNOSTIC",
33
+ reason: "Most-recent local runtime diagnostic: successful initialization and test reset both clear it; the separate runtime-mode slot owns the process verdict.",
34
+ },
30
35
  "packages/plugin/src/features/magic-context/memory/embedding-synapse.ts:sharedClientPromise": {
31
36
  classification: "VERDICT",
32
37
  reason: "DEFECT: a rejected connection promise remains shared after the daemon recovers.",
@@ -30,6 +30,7 @@ describe("MagicContextConfigSchema", () => {
30
30
  embedding: {
31
31
  provider: "local",
32
32
  model: DEFAULT_LOCAL_EMBEDDING_MODEL,
33
+ local_runtime: "auto",
33
34
  },
34
35
  memory: {
35
36
  enabled: true,
@@ -654,17 +655,35 @@ describe("MagicContextConfigSchema", () => {
654
655
  ).toThrow();
655
656
  });
656
657
 
658
+ it("defaults local embedding runtime to auto and accepts explicit overrides", () => {
659
+ expect(
660
+ MagicContextConfigSchema.parse({ embedding: { provider: "local" } }).embedding,
661
+ ).toMatchObject({ local_runtime: "auto" });
662
+ expect(
663
+ MagicContextConfigSchema.parse({
664
+ embedding: { provider: "local", local_runtime: "wasm" },
665
+ }).embedding,
666
+ ).toMatchObject({ local_runtime: "wasm" });
667
+ expect(
668
+ MagicContextConfigSchema.safeParse({
669
+ embedding: { provider: "local", local_runtime: "unsafe" },
670
+ }).success,
671
+ ).toBe(false);
672
+ });
673
+
657
674
  it("accepts a configured local embedding dtype", () => {
658
675
  const result = MagicContextConfigSchema.parse({
659
676
  embedding: {
660
677
  provider: "local",
661
678
  model: "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
679
+ local_runtime: "auto",
662
680
  local_dtype: "q8",
663
681
  },
664
682
  });
665
683
  expect(result.embedding).toEqual({
666
684
  provider: "local",
667
685
  model: "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
686
+ local_runtime: "auto",
668
687
  local_dtype: "q8",
669
688
  });
670
689
  });
@@ -676,6 +695,7 @@ describe("MagicContextConfigSchema", () => {
676
695
  expect(result.embedding).toEqual({
677
696
  provider: "local",
678
697
  model: DEFAULT_LOCAL_EMBEDDING_MODEL,
698
+ local_runtime: "auto",
679
699
  });
680
700
  expect("local_dtype" in result.embedding).toBe(false);
681
701
  });
@@ -619,6 +619,12 @@ const BaseEmbeddingConfigSchema = z
619
619
  .describe(
620
620
  "Optional maximum input tokens for chunk embeddings. Defaults conservatively to 512 when omitted.",
621
621
  ),
622
+ local_runtime: z
623
+ .enum(["auto", "native", "wasm"])
624
+ .default("auto")
625
+ .describe(
626
+ "Local provider only: ONNX runtime selection. 'auto' uses native under Node and uses WASM under Bun versions before 1.4.0, where Bun's NAPI teardown race can panic on quit; native is restored automatically on Bun 1.4.0+. Set 'native' only to prefer speed while accepting that pre-1.4.0 Bun crash risk, or 'wasm' to avoid loading the native addon.",
627
+ ),
622
628
  local_dtype: z
623
629
  .enum([
624
630
  "auto",
@@ -685,6 +691,7 @@ export const EmbeddingConfigSchema = BaseEmbeddingConfigSchema.transform((data)
685
691
  return {
686
692
  provider: "local" as const,
687
693
  model: data.model?.trim() || DEFAULT_LOCAL_EMBEDDING_MODEL,
694
+ local_runtime: data.local_runtime,
688
695
  ...(data.max_input_tokens ? { max_input_tokens: data.max_input_tokens } : {}),
689
696
  // local_dtype is spread CONDITIONALLY: omitting it when unset keeps
690
697
  // the identity byte-identical for the common no-dtype config, so
@@ -1161,6 +1168,7 @@ export const MagicContextConfigSchema = z
1161
1168
  embedding: EmbeddingConfigSchema.default({
1162
1169
  provider: "local",
1163
1170
  model: DEFAULT_LOCAL_EMBEDDING_MODEL,
1171
+ local_runtime: "auto",
1164
1172
  }).describe("Embedding provider configuration"),
1165
1173
  subc: z
1166
1174
  .object({
@@ -1,10 +1,11 @@
1
1
  /// <reference types="bun-types" />
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, it } from "bun:test";
4
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
- import { tmpdir } from "node:os";
4
+ import { mkdirSync, writeFileSync } from "node:fs";
6
5
  import { join } from "node:path";
6
+
7
7
  import { detectConflicts, resolveCompactionForBoot } from "./conflict-detector";
8
+ import { cleanupTestTempDir, createTestTempDir } from "./test-temp-dir";
8
9
 
9
10
  /**
10
11
  * Regression tests for plugin-conflict detection. The previous substring-
@@ -13,13 +14,14 @@ import { detectConflicts, resolveCompactionForBoot } from "./conflict-detector";
13
14
  * a false-positive conflict warning. See issue #43.
14
15
  */
15
16
  describe("detectConflicts", () => {
17
+ let root: string;
16
18
  let projectDir: string;
17
19
  let userConfigDir: string;
18
20
  let homeDir: string;
19
21
  let originalEnv: Record<string, string | undefined>;
20
22
 
21
23
  beforeEach(() => {
22
- const root = mkdtempSync(join(tmpdir(), "mc-conflict-"));
24
+ root = createTestTempDir("mc-conflict-").dir;
23
25
  projectDir = join(root, "project");
24
26
  mkdirSync(projectDir, { recursive: true });
25
27
  userConfigDir = join(root, "user-config", "opencode");
@@ -50,22 +52,7 @@ describe("detectConflicts", () => {
50
52
  if (v === undefined) delete process.env[k];
51
53
  else process.env[k] = v;
52
54
  }
53
- // Test directories live under tmpdir(); cleanup is best-effort.
54
- try {
55
- rmSync(projectDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
56
- } catch {
57
- /* Ignore EBUSY on Windows */
58
- }
59
- try {
60
- rmSync(userConfigDir, {
61
- recursive: true,
62
- force: true,
63
- maxRetries: 10,
64
- retryDelay: 100,
65
- });
66
- } catch {
67
- /* Ignore EBUSY on Windows */
68
- }
55
+ cleanupTestTempDir(root);
69
56
  });
70
57
 
71
58
  function writeProjectConfig(plugins: Array<string | [string, unknown]>): void {
@@ -1,12 +1,13 @@
1
1
  /// <reference types="bun-types" />
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, it } from "bun:test";
4
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
- import { tmpdir } from "node:os";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
5
  import { join } from "node:path";
7
6
  import { parse as parseJsonc } from "comment-json";
7
+
8
8
  import { detectConflicts } from "./conflict-detector";
9
9
  import { fixConflicts } from "./conflict-fixer";
10
+ import { cleanupTestTempDir, createTestTempDir } from "./test-temp-dir";
10
11
 
11
12
  const noOmoConflicts = {
12
13
  omoPreemptiveCompaction: false,
@@ -22,7 +23,7 @@ describe("fixConflicts", () => {
22
23
  let originalEnv: Record<string, string | undefined>;
23
24
 
24
25
  beforeEach(() => {
25
- root = mkdtempSync(join(tmpdir(), "mc-conflict-fixer-"));
26
+ root = createTestTempDir("mc-conflict-fixer-").dir;
26
27
  projectDir = join(root, "project");
27
28
  userConfigDir = join(root, "user-config", "opencode");
28
29
  homeDir = join(root, "home");
@@ -44,11 +45,7 @@ describe("fixConflicts", () => {
44
45
  if (value === undefined) delete process.env[key];
45
46
  else process.env[key] = value;
46
47
  }
47
- try {
48
- rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
49
- } catch {
50
- /* Ignore EBUSY on Windows */
51
- }
48
+ cleanupTestTempDir(root);
52
49
  });
53
50
 
54
51
  it("preserves JSONC comments and tuple plugin entries while removing canonical DCP", () => {
@@ -4,3 +4,4 @@ export * from "./normalize-sdk-response";
4
4
  export * from "./prompt-surface";
5
5
  export * from "./prompt-surface-runtime";
6
6
  export * from "./resolve-fallbacks";
7
+ export * from "./summarize-child-stderr";
@@ -18,49 +18,121 @@ type LoggerScenarioResult = {
18
18
  };
19
19
  };
20
20
 
21
+ type RotationScenarioResult = {
22
+ firstCurrent: string;
23
+ firstPredecessorFirstByte: number;
24
+ current: string;
25
+ predecessorFirstByte: number;
26
+ predecessorSize: number;
27
+ currentSize: number;
28
+ currentMode: number;
29
+ predecessorMode: number;
30
+ files: string[];
31
+ };
32
+
33
+ type HotPathScenarioResult = {
34
+ statCalls: number;
35
+ };
36
+
21
37
  const loggerScenario = `
22
- import { existsSync, readFileSync } from "node:fs";
38
+ import fs from "node:fs";
23
39
  import * as path from "node:path";
40
+ import { mock } from "bun:test";
24
41
 
25
42
  const root = process.env.LOGGER_SCENARIO_ROOT;
26
43
  const loggerModuleUrl = process.env.LOGGER_MODULE_URL;
27
- if (!root || !loggerModuleUrl) throw new Error("logger scenario environment is incomplete");
44
+ const scenario = process.env.LOGGER_SCENARIO;
45
+ if (!root || !loggerModuleUrl || !scenario) {
46
+ throw new Error("logger scenario environment is incomplete");
47
+ }
28
48
 
29
- const logger = await import(loggerModuleUrl);
30
49
  const logPath = path.join(root, "nested", "magic-context.log");
31
50
  process.env.MAGIC_CONTEXT_LOG_PATH = logPath;
32
- logger.log("first");
33
- logger.log(
34
- "tokens.input=45000 api_key=sk-proj-abcdefghijklmnopqrstuvwxyzABCDEFGH",
35
- { authorization: "Bearer abcdefghijklmnop", totalInputTokens: 132000 },
36
- );
37
- logger.flushLogger();
38
- const healthyDiagnostics = logger.getLoggerDiagnostics();
39
-
40
- const logDirectory = path.dirname(logPath);
41
- if (process.env.LOGGER_SCENARIO === "recovery") {
42
- const { rmSync } = await import("node:fs");
43
- rmSync(logDirectory, { recursive: true, force: true });
44
- logger.log("second");
45
- logger.flushLogger();
51
+ let statCalls = 0;
52
+ if (scenario === "hot-path") {
53
+ mock.module("node:fs", () => ({
54
+ ...fs,
55
+ statSync: (...args) => {
56
+ statCalls++;
57
+ return fs.statSync(...args);
58
+ },
59
+ }));
46
60
  }
47
61
 
48
- // A directory used as the file target makes append fail deterministically on every platform.
49
- const failedPath = path.join(root, "unwritable-log-target");
50
- const { mkdirSync } = await import("node:fs");
51
- mkdirSync(failedPath, { recursive: true });
52
- process.env.MAGIC_CONTEXT_LOG_PATH = failedPath;
53
- logger.log("failed write");
54
- logger.flushLogger();
55
- const failedDiagnostics = logger.getLoggerDiagnostics();
56
-
57
- const content = existsSync(logPath) ? readFileSync(logPath, "utf8") : "";
58
- console.log(JSON.stringify({
59
- exists: existsSync(logPath),
60
- content,
61
- healthyDiagnostics,
62
- failedDiagnostics,
63
- }));
62
+ const logger = await import(loggerModuleUrl);
63
+
64
+ if (scenario === "rotation") {
65
+ const maxLogBytes = 32 * 1024 * 1024;
66
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
67
+ fs.writeFileSync(logPath, Buffer.alloc(maxLogBytes, "o"));
68
+ fs.chmodSync(logPath, 0o600);
69
+ fs.writeFileSync(logPath + ".1", "prior predecessor");
70
+
71
+ logger.log("after first rotation");
72
+ logger.flushLogger();
73
+ const firstCurrent = fs.readFileSync(logPath, "utf8");
74
+ const firstPredecessorFirstByte = fs.readFileSync(logPath + ".1")[0];
75
+
76
+ // Switch paths once so the logger rechecks the primary path after this
77
+ // external replacement, as a different plugin process would require.
78
+ fs.writeFileSync(logPath, Buffer.alloc(maxLogBytes, "n"));
79
+ process.env.MAGIC_CONTEXT_LOG_PATH = path.join(root, "alternate.log");
80
+ logger.log("alternate path");
81
+ logger.flushLogger();
82
+ process.env.MAGIC_CONTEXT_LOG_PATH = logPath;
83
+ logger.log("after second rotation");
84
+ logger.flushLogger();
85
+
86
+ const predecessorPath = logPath + ".1";
87
+ console.log(JSON.stringify({
88
+ firstCurrent,
89
+ firstPredecessorFirstByte,
90
+ current: fs.readFileSync(logPath, "utf8"),
91
+ predecessorFirstByte: fs.readFileSync(predecessorPath)[0],
92
+ predecessorSize: fs.statSync(predecessorPath).size,
93
+ currentSize: fs.statSync(logPath).size,
94
+ currentMode: fs.statSync(logPath).mode & 0o777,
95
+ predecessorMode: fs.statSync(predecessorPath).mode & 0o777,
96
+ files: fs.readdirSync(path.dirname(logPath)).filter((name) => name.startsWith("magic-context.log")).sort(),
97
+ }));
98
+ } else if (scenario === "hot-path") {
99
+ for (let index = 0; index < 20; index++) {
100
+ logger.log("hot-path-" + index);
101
+ logger.flushLogger();
102
+ }
103
+ console.log(JSON.stringify({ statCalls }));
104
+ } else {
105
+ logger.log("first");
106
+ logger.log(
107
+ "tokens.input=45000 api_key=sk-proj-abcdefghijklmnopqrstuvwxyzABCDEFGH",
108
+ { authorization: "Bearer abcdefghijklmnop", totalInputTokens: 132000 },
109
+ );
110
+ logger.flushLogger();
111
+ const healthyDiagnostics = logger.getLoggerDiagnostics();
112
+
113
+ const logDirectory = path.dirname(logPath);
114
+ if (scenario === "recovery") {
115
+ fs.rmSync(logDirectory, { recursive: true, force: true });
116
+ logger.log("second");
117
+ logger.flushLogger();
118
+ }
119
+
120
+ // A directory used as the file target makes append fail deterministically on every platform.
121
+ const failedPath = path.join(root, "unwritable-log-target");
122
+ fs.mkdirSync(failedPath, { recursive: true });
123
+ process.env.MAGIC_CONTEXT_LOG_PATH = failedPath;
124
+ logger.log("failed write");
125
+ logger.flushLogger();
126
+ const failedDiagnostics = logger.getLoggerDiagnostics();
127
+
128
+ const content = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf8") : "";
129
+ console.log(JSON.stringify({
130
+ exists: fs.existsSync(logPath),
131
+ content,
132
+ healthyDiagnostics,
133
+ failedDiagnostics,
134
+ }));
135
+ }
64
136
  `;
65
137
 
66
138
  const scenarioRoots: string[] = [];
@@ -71,9 +143,9 @@ afterEach(() => {
71
143
  }
72
144
  });
73
145
 
74
- async function runLoggerScenario(
75
- scenario: "recovery" | "diagnostics",
76
- ): Promise<LoggerScenarioResult> {
146
+ async function runLoggerScenario<T>(
147
+ scenario: "recovery" | "diagnostics" | "rotation" | "hot-path",
148
+ ): Promise<T> {
77
149
  const root = mkdtempSync(path.join(os.tmpdir(), "magic-context-logger-test-"));
78
150
  scenarioRoots.push(root);
79
151
  const child = Bun.spawn({
@@ -95,19 +167,19 @@ async function runLoggerScenario(
95
167
  new Response(child.stderr).text(),
96
168
  ]);
97
169
  expect(exitCode, stderr).toBe(0);
98
- return JSON.parse(stdout.trim()) as LoggerScenarioResult;
170
+ return JSON.parse(stdout.trim()) as T;
99
171
  }
100
172
 
101
173
  describe("logger", () => {
102
174
  test("recreates a log directory removed while the process is running", async () => {
103
- const result = await runLoggerScenario("recovery");
175
+ const result = await runLoggerScenario<LoggerScenarioResult>("recovery");
104
176
 
105
177
  expect(result.exists).toBe(true);
106
178
  expect(result.content).toContain("second");
107
179
  });
108
180
 
109
181
  test("redacts secrets while preserving numeric diagnostics", async () => {
110
- const result = await runLoggerScenario("diagnostics");
182
+ const result = await runLoggerScenario<LoggerScenarioResult>("diagnostics");
111
183
 
112
184
  expect(result.content).toContain("tokens.input=45000");
113
185
  expect(result.content).toContain('"totalInputTokens":132000');
@@ -117,8 +189,31 @@ describe("logger", () => {
117
189
  expect(result.content).not.toContain("abcdefghijklmnop");
118
190
  });
119
191
 
192
+ test("rotates at the 32 MiB bound with one private predecessor", async () => {
193
+ const result = await runLoggerScenario<RotationScenarioResult>("rotation");
194
+ const maxLogBytes = 32 * 1024 * 1024;
195
+
196
+ expect(result.firstCurrent).toContain("after first rotation");
197
+ expect(result.firstPredecessorFirstByte).toBe("o".charCodeAt(0));
198
+ expect(result.current).toContain("after second rotation");
199
+ expect(result.predecessorFirstByte).toBe("n".charCodeAt(0));
200
+ expect(result.predecessorSize).toBeLessThanOrEqual(maxLogBytes);
201
+ expect(result.currentSize).toBeLessThanOrEqual(maxLogBytes);
202
+ expect(result.files).toEqual(["magic-context.log", "magic-context.log.1"]);
203
+ if (process.platform !== "win32") {
204
+ expect(result.currentMode).toBe(0o600);
205
+ expect(result.predecessorMode).toBe(0o600);
206
+ }
207
+ });
208
+
209
+ test("uses cached size between periodic rotation checks", async () => {
210
+ const result = await runLoggerScenario<HotPathScenarioResult>("hot-path");
211
+
212
+ expect(result.statCalls).toBeLessThanOrEqual(1);
213
+ });
214
+
120
215
  test("reports swallowed writes while healthy writes leave the counter at zero", async () => {
121
- const result = await runLoggerScenario("diagnostics");
216
+ const result = await runLoggerScenario<LoggerScenarioResult>("diagnostics");
122
217
 
123
218
  expect(result.healthyDiagnostics).toEqual({
124
219
  swallowedWriteCount: 0,