@wolfx/opencode-magic-context 0.21.8 → 0.22.1-patch.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  2. package/dist/agents/permissions.d.ts +29 -14
  3. package/dist/agents/permissions.d.ts.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/migrate-experimental.d.ts +30 -0
  6. package/dist/config/migrate-experimental.d.ts.map +1 -0
  7. package/dist/config/schema/agent-overrides.d.ts.map +1 -1
  8. package/dist/config/schema/magic-context.d.ts +110 -109
  9. package/dist/config/schema/magic-context.d.ts.map +1 -1
  10. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  11. package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
  12. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
  13. package/dist/features/magic-context/compartment-events.d.ts +50 -0
  14. package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
  15. package/dist/features/magic-context/compartment-storage.d.ts +22 -0
  16. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
  19. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
  21. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
  23. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  24. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  25. package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
  26. package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
  27. package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -1
  28. package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
  29. package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/constants.d.ts +4 -0
  31. package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
  32. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  33. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  34. package/dist/features/magic-context/memory/embedding-openai.d.ts +6 -0
  35. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  36. package/dist/features/magic-context/memory/embedding-probe.d.ts +5 -0
  37. package/dist/features/magic-context/memory/embedding-probe.d.ts.map +1 -1
  38. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/index.d.ts +1 -1
  40. package/dist/features/magic-context/memory/index.d.ts.map +1 -1
  41. package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
  42. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
  43. package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
  44. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  45. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  46. package/dist/features/magic-context/memory/storage-memory.d.ts +15 -1
  47. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  48. package/dist/features/magic-context/memory/types.d.ts +3 -1
  49. package/dist/features/magic-context/memory/types.d.ts.map +1 -1
  50. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  51. package/dist/features/magic-context/migrations.d.ts +7 -0
  52. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  53. package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
  54. package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
  55. package/dist/features/magic-context/project-identity.d.ts +2 -0
  56. package/dist/features/magic-context/project-identity.d.ts.map +1 -0
  57. package/dist/features/magic-context/storage-db.d.ts +51 -7
  58. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  59. package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
  60. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
  61. package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
  62. package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
  63. package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
  64. package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
  65. package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
  66. package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
  67. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  68. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  69. package/dist/features/magic-context/storage-meta-shared.d.ts +44 -0
  70. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  71. package/dist/features/magic-context/storage-meta.d.ts +1 -0
  72. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  73. package/dist/features/magic-context/storage-project-state.d.ts +19 -0
  74. package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
  75. package/dist/features/magic-context/storage-subagent-invocations.d.ts +9 -0
  76. package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
  77. package/dist/features/magic-context/storage-tags.d.ts +21 -1
  78. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  79. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
  80. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
  81. package/dist/features/magic-context/storage.d.ts +12 -3
  82. package/dist/features/magic-context/storage.d.ts.map +1 -1
  83. package/dist/features/magic-context/subagent-token-capture.d.ts +1 -1
  84. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
  85. package/dist/features/magic-context/tagger.d.ts +15 -1
  86. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  87. package/dist/features/magic-context/types.d.ts +21 -0
  88. package/dist/features/magic-context/types.d.ts.map +1 -1
  89. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  90. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  91. package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
  92. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
  93. package/dist/features/magic-context/work-metrics.d.ts +66 -0
  94. package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
  96. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/command-handler.d.ts +13 -1
  98. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
  100. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
  102. package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  104. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +6 -2
  105. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
  108. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/compartment-runner-types.d.ts +67 -4
  110. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  111. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
  114. package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
  115. package/dist/hooks/magic-context/decay-render.d.ts +67 -0
  116. package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
  117. package/dist/hooks/magic-context/event-handler.d.ts +1 -1
  118. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  119. package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
  120. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  121. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
  123. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
  124. package/dist/hooks/magic-context/historian-state-file.d.ts +4 -4
  125. package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
  126. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  127. package/dist/hooks/magic-context/hook.d.ts +12 -20
  128. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  129. package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
  130. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  131. package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
  132. package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
  133. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  134. package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
  135. package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
  136. package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
  137. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  138. package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
  139. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  140. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
  141. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
  142. package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
  143. package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
  144. package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
  145. package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
  146. package/dist/hooks/magic-context/send-session-notification.d.ts +1 -1
  147. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  148. package/dist/hooks/magic-context/system-prompt-hash.d.ts +5 -6
  149. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  150. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  151. package/dist/hooks/magic-context/text-complete.d.ts +41 -1
  152. package/dist/hooks/magic-context/text-complete.d.ts.map +1 -1
  153. package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
  154. package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
  155. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
  156. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  157. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
  158. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  159. package/dist/hooks/magic-context/transform.d.ts +9 -7
  160. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  161. package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
  162. package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
  163. package/dist/index.d.ts.map +1 -1
  164. package/dist/index.js +10024 -5179
  165. package/dist/plugin/conflict-warning-hook.d.ts +13 -0
  166. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  167. package/dist/plugin/dream-timer.d.ts.map +1 -1
  168. package/dist/plugin/event.d.ts +10 -3
  169. package/dist/plugin/event.d.ts.map +1 -1
  170. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  171. package/dist/plugin/messages-transform.d.ts.map +1 -1
  172. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  173. package/dist/plugin/tool-registry.d.ts.map +1 -1
  174. package/dist/shared/announcement.d.ts +17 -1
  175. package/dist/shared/announcement.d.ts.map +1 -1
  176. package/dist/shared/data-path.d.ts +9 -11
  177. package/dist/shared/data-path.d.ts.map +1 -1
  178. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  179. package/dist/shared/rpc-client.d.ts +1 -0
  180. package/dist/shared/rpc-client.d.ts.map +1 -1
  181. package/dist/shared/rpc-notifications.d.ts +27 -5
  182. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  183. package/dist/shared/rpc-server.d.ts +1 -0
  184. package/dist/shared/rpc-server.d.ts.map +1 -1
  185. package/dist/shared/rpc-types.d.ts +30 -2
  186. package/dist/shared/rpc-types.d.ts.map +1 -1
  187. package/dist/shared/rpc-utils.d.ts +9 -0
  188. package/dist/shared/rpc-utils.d.ts.map +1 -1
  189. package/dist/shared/sqlite-helpers.d.ts +7 -7
  190. package/dist/shared/sqlite.d.ts +23 -14
  191. package/dist/shared/sqlite.d.ts.map +1 -1
  192. package/dist/shared/tag-transcript.d.ts +10 -1
  193. package/dist/shared/tag-transcript.d.ts.map +1 -1
  194. package/dist/tools/ctx-expand/tools.d.ts +5 -1
  195. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  196. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  197. package/dist/tui/data/context-db.d.ts +16 -1
  198. package/dist/tui/data/context-db.d.ts.map +1 -1
  199. package/package.json +2 -3
  200. package/src/shared/announcement.test.ts +23 -7
  201. package/src/shared/announcement.ts +30 -8
  202. package/src/shared/conflict-detector.test.ts +19 -6
  203. package/src/shared/conflict-detector.ts +1 -1
  204. package/src/shared/conflict-fixer.test.ts +7 -3
  205. package/src/shared/data-path.test.ts +9 -10
  206. package/src/shared/data-path.ts +10 -12
  207. package/src/shared/models-dev-cache.test.ts +72 -4
  208. package/src/shared/models-dev-cache.ts +47 -8
  209. package/src/shared/opencode-compaction-detector.test.ts +10 -2
  210. package/src/shared/rpc-client.test.ts +54 -3
  211. package/src/shared/rpc-client.ts +19 -9
  212. package/src/shared/rpc-notifications.test.ts +54 -1
  213. package/src/shared/rpc-notifications.ts +82 -13
  214. package/src/shared/rpc-server.ts +33 -4
  215. package/src/shared/rpc-types.ts +30 -2
  216. package/src/shared/rpc-utils.ts +10 -0
  217. package/src/shared/sqlite-helpers.ts +9 -9
  218. package/src/shared/sqlite.ts +99 -80
  219. package/src/shared/tag-transcript.test.ts +280 -0
  220. package/src/shared/tag-transcript.ts +162 -33
  221. package/src/shared/tui-config.ts +2 -2
  222. package/src/tui/data/context-db.ts +75 -11
  223. package/src/tui/index.tsx +227 -36
  224. package/src/tui/slots/sidebar-content.tsx +368 -36
  225. package/dist/hooks/auto-update-checker/cache.d.ts +0 -23
  226. package/dist/hooks/auto-update-checker/cache.d.ts.map +0 -1
  227. package/dist/hooks/auto-update-checker/checker.d.ts +0 -13
  228. package/dist/hooks/auto-update-checker/checker.d.ts.map +0 -1
  229. package/dist/hooks/auto-update-checker/constants.d.ts +0 -10
  230. package/dist/hooks/auto-update-checker/constants.d.ts.map +0 -1
  231. package/dist/hooks/auto-update-checker/index.d.ts +0 -40
  232. package/dist/hooks/auto-update-checker/index.d.ts.map +0 -1
  233. package/dist/hooks/auto-update-checker/types.d.ts +0 -50
  234. package/dist/hooks/auto-update-checker/types.d.ts.map +0 -1
  235. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
  236. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
  237. package/dist/shared/native-binding.d.ts +0 -87
  238. package/dist/shared/native-binding.d.ts.map +0 -1
  239. package/src/shared/native-binding.ts +0 -311
@@ -8,8 +8,11 @@ import * as path from "node:path";
8
8
  * `getMagicContextStorageDir()`. The behavior we test:
9
9
  * 1. `markAnnouncementSeen` then `readLastAnnouncedVersion` round-trips
10
10
  * 2. `shouldShowAnnouncement` returns false after a matching mark
11
- * 3. `shouldShowAnnouncement` returns true after a non-matching mark
12
- * 4. Empty-version inputs are no-ops (don't crash, don't write garbage)
11
+ * 3. `shouldShowAnnouncement` returns true after a non-matching (older) mark
12
+ * 4. `shouldShowAnnouncement` seeds state + returns false on first run / wiped
13
+ * sandbox (no prior file), so fresh installs and ephemeral envs aren't
14
+ * spammed with a changelog (issue #99)
15
+ * 5. Empty-version inputs are no-ops (don't crash, don't write garbage)
13
16
  *
14
17
  * We isolate writes by pointing `XDG_DATA_HOME` at a temp dir before requiring
15
18
  * the module fresh per test, since the module captures the storage path at
@@ -32,7 +35,8 @@ afterEach(() => {
32
35
  process.env.XDG_DATA_HOME = originalXdg;
33
36
  }
34
37
  try {
35
- fs.rmSync(tmpRoot, { recursive: true, force: true });
38
+ // maxRetries/retryDelay ride out transient EBUSY/EPERM on Windows.
39
+ fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
36
40
  } catch {
37
41
  // best-effort
38
42
  }
@@ -110,9 +114,14 @@ describe("shouldShowAnnouncement gating", () => {
110
114
  expect(shouldShowAnnouncement()).toBe(false);
111
115
  });
112
116
 
113
- test("returns true when the live version was never marked", async () => {
117
+ test("seeds state and returns false on first run / wiped sandbox (issue #99)", async () => {
114
118
  const mod = await import(`./announcement?t=${Date.now()}-none`);
115
- const { ANNOUNCEMENT_VERSION, ANNOUNCEMENT_FEATURES, shouldShowAnnouncement } = mod;
119
+ const {
120
+ ANNOUNCEMENT_VERSION,
121
+ ANNOUNCEMENT_FEATURES,
122
+ shouldShowAnnouncement,
123
+ readLastAnnouncedVersion,
124
+ } = mod;
116
125
 
117
126
  if (!ANNOUNCEMENT_VERSION || ANNOUNCEMENT_FEATURES.length === 0) {
118
127
  // When empty, the gate is always false regardless of state
@@ -120,8 +129,15 @@ describe("shouldShowAnnouncement gating", () => {
120
129
  return;
121
130
  }
122
131
 
123
- // No mark has been written in this fresh tmpRoot, so the gate is open
124
- expect(shouldShowAnnouncement()).toBe(true);
132
+ // No mark exists yet (fresh install or ephemeral/wiped sandbox). The
133
+ // gate must NOT announce — it seeds the state to the current version and
134
+ // returns false, so first-run users and disposable containers are never
135
+ // spammed with a changelog they have no context for.
136
+ expect(readLastAnnouncedVersion()).toBe("");
137
+ expect(shouldShowAnnouncement()).toBe(false);
138
+ // The seed was written, so a subsequent check stays quiet too.
139
+ expect(readLastAnnouncedVersion()).toBe(ANNOUNCEMENT_VERSION);
140
+ expect(shouldShowAnnouncement()).toBe(false);
125
141
  });
126
142
 
127
143
  test("returns true when a different (older) version is marked", async () => {
@@ -23,19 +23,18 @@ import { getMagicContextStorageDir } from "./data-path";
23
23
  * Bump only when there are user-visible changes worth a startup dialog.
24
24
  * Does NOT need to match the published package version.
25
25
  */
26
- export const ANNOUNCEMENT_VERSION = "0.21.7";
26
+ export const ANNOUNCEMENT_VERSION = "0.22.0";
27
27
 
28
28
  /**
29
29
  * Short, user-facing bullet strings. Keep each line ~80 chars or shorter so the
30
30
  * TUI dialog renders cleanly without horizontal scroll on a typical terminal.
31
31
  */
32
32
  export const ANNOUNCEMENT_FEATURES: ReadonlyArray<string> = [
33
- "Pi parity sweep: 44 audit findings fixed, including a critical SHIP-BLOCKER where /ctx-flush did not drain the pending Pi compaction queue.",
34
- "Pi historian recovery fix: empty/no-op historian returns now clear emergency recovery so sessions cannot loop forever at 95%.",
35
- "trimPiMessagesToBoundary now sweeps non-contiguous tool-result orphans, fixing provider 400s after compaction in long Pi sessions.",
36
- "Hidden subagent tool isolation: historian, dreamer, and sidekick can no longer spawn subagents or run unsafe tools.",
37
- "TUI sidebar and /ctx-status header now show execute threshold inline: '47.5% / 65%' on the left, '475K / 1.0M' on the right.",
38
- "doctor --issue now caps GitHub issue bodies at ~60KB with a dedicated 'Recent errors' section so reports stay submittable.",
33
+ "NOW ON BY DEFAULT Temporal awareness: the agent sees elapsed-time markers (e.g. +2h 15m) between messages and dated compartments, so it knows how long ago things happened. Opt out with temporal_awareness: false.",
34
+ "NOW ON BY DEFAULT — Auto-search hints: each turn a background ctx_search whispers a compact 'vague recall' when something relevant exists in your memories, past conversation, or git history. No full content injected. Opt out with memory.auto_search.enabled: false.",
35
+ "Experimental features graduated to stable config: temporal_awareness and caveman_text_compression are now top-level keys; auto_search and git_commit_indexing moved under memory.* . Run `doctor` to migrate old experimental.* settings (your opt-ins/opt-outs are preserved).",
36
+ "git_commit_indexing (make project history semantically searchable) stays opt-in enable with memory.git_commit_indexing.enabled: true.",
37
+ "Audit hardening across both harnesses: memory config-bypass fix, supersede-delta cache-stability fixes, and dashboard correctness fixes.",
39
38
  ];
40
39
 
41
40
  /**
@@ -90,8 +89,31 @@ export function markAnnouncementSeen(version: string): void {
90
89
  * True when the configured `ANNOUNCEMENT_VERSION` has not yet been dismissed
91
90
  * AND there is at least one feature to show. Used by both the TUI dialog path
92
91
  * and the Desktop ignored-message fallback.
92
+ *
93
+ * First-run / sandbox handling: when NO state file exists yet, we seed it to the
94
+ * current `ANNOUNCEMENT_VERSION` and return false instead of announcing. This
95
+ * covers two cases that previously spammed the dialog (issue #99):
96
+ * - Fresh installs: a brand-new user shouldn't be shown a changelog of release
97
+ * bullets they have no context for — they need onboarding, not patch notes.
98
+ * - Ephemeral/sandbox environments (Docker, CI, disposable dev containers)
99
+ * where the storage dir is wiped between launches: without the seed, the
100
+ * missing file made the announcement re-show on every single startup.
101
+ * Real upgrades still announce exactly once: an existing user already has a
102
+ * state file at the prior version, so the version mismatch shows the dialog and
103
+ * dismissing it advances the file to the current version.
104
+ *
105
+ * The seed is a deliberate write side-effect on the "no file" branch — folding
106
+ * it here (rather than a separate startup call) makes every caller path (plugin
107
+ * startup, Pi startup, TUI rpc pull) consistent with no ordering dependency.
93
108
  */
94
109
  export function shouldShowAnnouncement(): boolean {
95
110
  if (!ANNOUNCEMENT_VERSION || ANNOUNCEMENT_FEATURES.length === 0) return false;
96
- return readLastAnnouncedVersion() !== ANNOUNCEMENT_VERSION;
111
+ const lastVersion = readLastAnnouncedVersion();
112
+ if (!lastVersion) {
113
+ // No prior state: fresh install or wiped sandbox. Seed to current and
114
+ // skip the announcement so we never pester first-run / ephemeral envs.
115
+ markAnnouncementSeen(ANNOUNCEMENT_VERSION);
116
+ return false;
117
+ }
118
+ return lastVersion !== ANNOUNCEMENT_VERSION;
97
119
  }
@@ -46,8 +46,21 @@ describe("detectConflicts", () => {
46
46
  else process.env[k] = v;
47
47
  }
48
48
  // Test directories live under tmpdir(); cleanup is best-effort.
49
- rmSync(projectDir, { recursive: true, force: true });
50
- rmSync(userConfigDir, { recursive: true, force: true });
49
+ try {
50
+ rmSync(projectDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
51
+ } catch {
52
+ /* Ignore EBUSY on Windows */
53
+ }
54
+ try {
55
+ rmSync(userConfigDir, {
56
+ recursive: true,
57
+ force: true,
58
+ maxRetries: 10,
59
+ retryDelay: 100,
60
+ });
61
+ } catch {
62
+ /* Ignore EBUSY on Windows */
63
+ }
51
64
  });
52
65
 
53
66
  function writeProjectConfig(plugins: Array<string | [string, unknown]>): void {
@@ -182,7 +195,7 @@ describe("detectConflicts", () => {
182
195
  });
183
196
 
184
197
  it("returns no conflicts for unrelated plugins", () => {
185
- writeProjectConfig(["@cortexkit/opencode-magic-context@latest", "some-other-plugin"]);
198
+ writeProjectConfig(["@wolfx/opencode-magic-context@latest", "some-other-plugin"]);
186
199
  const result = detectConflicts(projectDir);
187
200
  expect(result.hasConflict).toBe(false);
188
201
  });
@@ -195,7 +208,7 @@ describe("detectConflicts", () => {
195
208
  describe("tuple plugin entries (issue #49)", () => {
196
209
  it("does not crash when a plugin is defined as a [name, options] tuple", () => {
197
210
  writeProjectConfig([
198
- "@cortexkit/opencode-magic-context@latest",
211
+ "@wolfx/opencode-magic-context@latest",
199
212
  ["@plannotator/opencode@latest", { workflow: "plan-agent" }],
200
213
  ]);
201
214
  expect(() => detectConflicts(projectDir)).not.toThrow();
@@ -203,7 +216,7 @@ describe("detectConflicts", () => {
203
216
 
204
217
  it("detects DCP conflict when DCP is expressed as a tuple", () => {
205
218
  writeProjectConfig([
206
- "@cortexkit/opencode-magic-context@latest",
219
+ "@wolfx/opencode-magic-context@latest",
207
220
  ["@tarquinen/opencode-dcp@latest", {}],
208
221
  ]);
209
222
  const result = detectConflicts(projectDir);
@@ -223,7 +236,7 @@ describe("detectConflicts", () => {
223
236
  "@plannotator/opencode@latest",
224
237
  { workflow: "plan-agent", planningAgents: ["plan"] },
225
238
  ],
226
- "@cortexkit/opencode-magic-context@latest",
239
+ "@wolfx/opencode-magic-context@latest",
227
240
  ]);
228
241
  const result = detectConflicts(projectDir);
229
242
  expect(result.hasConflict).toBe(false);
@@ -365,7 +365,7 @@ export function formatConflictShort(result: ConflictResult): string {
365
365
  "",
366
366
  ...result.reasons.map((r) => `• ${r}`),
367
367
  "",
368
- "Fix: run `npx @cortexkit/opencode-magic-context@latest doctor`",
368
+ "Fix: run `npx @wolfx/opencode-magic-context@latest doctor`",
369
369
  ];
370
370
  return lines.join("\n");
371
371
  }
@@ -38,7 +38,11 @@ describe("fixConflicts", () => {
38
38
  if (value === undefined) delete process.env[key];
39
39
  else process.env[key] = value;
40
40
  }
41
- rmSync(root, { recursive: true, force: true });
41
+ try {
42
+ rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
43
+ } catch {
44
+ /* Ignore EBUSY on Windows */
45
+ }
42
46
  });
43
47
 
44
48
  it("preserves JSONC comments and tuple plugin entries while removing canonical DCP", () => {
@@ -50,7 +54,7 @@ describe("fixConflicts", () => {
50
54
  "plugin": [
51
55
  ["@plannotator/opencode@latest", { "workflow": "plan-agent" }],
52
56
  ["@tarquinen/opencode-dcp@latest", { "enabled": true }],
53
- "@cortexkit/opencode-magic-context@latest"
57
+ "@wolfx/opencode-magic-context@latest"
54
58
  ],
55
59
  "compaction": {
56
60
  // keep this compaction comment
@@ -76,7 +80,7 @@ describe("fixConflicts", () => {
76
80
  expect(updated.compaction).toEqual({ auto: false, prune: false });
77
81
  expect(updated.plugin).toEqual([
78
82
  ["@plannotator/opencode@latest", { workflow: "plan-agent" }],
79
- "@cortexkit/opencode-magic-context@latest",
83
+ "@wolfx/opencode-magic-context@latest",
80
84
  ]);
81
85
  });
82
86
 
@@ -121,20 +121,19 @@ describe("data-path", () => {
121
121
  expect(shared).toContain("cortexkit");
122
122
  });
123
123
 
124
- test("getProjectMagicContextDir composes <project>/.opencode/magic-context", () => {
125
- // Project-local artifacts (historian state file, failure dumps) live
126
- // inside the project so OpenCode's external_directory permission system
127
- // treats them as project-internal. Without this, historian's Read tool
128
- // would trigger a permission prompt on every run when artifacts lived
129
- // under os.tmpdir().
124
+ test("getProjectMagicContextDir composes <project>/.magic-context", () => {
125
+ // Project-local artifacts live inside the project so OpenCode's
126
+ // external_directory permission system treats them as project-internal.
127
+ // Without this, historian's Read tool would trigger a permission prompt
128
+ // on every run when artifacts lived under os.tmpdir().
130
129
  expect(getProjectMagicContextDir("/Users/me/Work/proj")).toBe(
131
- path.join("/Users/me/Work/proj", ".opencode", "magic-context"),
130
+ path.join("/Users/me/Work/proj", ".magic-context"),
132
131
  );
133
132
  });
134
133
 
135
134
  test("getProjectMagicContextHistorianDir appends historian/", () => {
136
135
  expect(getProjectMagicContextHistorianDir("/Users/me/Work/proj")).toBe(
137
- path.join("/Users/me/Work/proj", ".opencode", "magic-context", "historian"),
136
+ path.join("/Users/me/Work/proj", ".magic-context", "historian"),
138
137
  );
139
138
  });
140
139
 
@@ -145,7 +144,7 @@ describe("data-path", () => {
145
144
  // project-local historian dir.
146
145
  process.env.XDG_DATA_HOME = "/tmp/custom-data";
147
146
  expect(getProjectMagicContextDir("/some/project")).toBe(
148
- path.join("/some/project", ".opencode", "magic-context"),
147
+ path.join("/some/project", ".magic-context"),
149
148
  );
150
149
  });
151
150
 
@@ -153,7 +152,7 @@ describe("data-path", () => {
153
152
  // path.join normalizes redundant separators so callers don't need to
154
153
  // worry about how the project directory was constructed.
155
154
  expect(getProjectMagicContextDir("/some/project/")).toBe(
156
- path.join("/some/project/", ".opencode", "magic-context"),
155
+ path.join("/some/project/", ".magic-context"),
157
156
  );
158
157
  });
159
158
  });
@@ -62,34 +62,31 @@ export function getMagicContextHistorianDir(harness: HarnessId = getHarness()):
62
62
  /**
63
63
  * Project-local magic-context artifact directory.
64
64
  *
65
- * Layout: `<project-directory>/.opencode/magic-context/`
65
+ * Layout: `<project-directory>/.magic-context/`
66
66
  *
67
67
  * Used for artifacts that the historian/recomp pipeline writes during a run
68
68
  * and that the model is asked to read via its native Read tool. OpenCode's
69
69
  * `external_directory` permission system asks the user before reading any
70
70
  * file outside the project directory or its worktree, which interrupts every
71
71
  * historian run when artifacts live under `os.tmpdir()`. Writing under the
72
- * project's own `.opencode/` subtree falls inside the project boundary and
73
- * never triggers a permission prompt.
72
+ * project's own `.magic-context/` subtree falls inside the project boundary
73
+ * and never triggers a permission prompt.
74
74
  *
75
- * The `.opencode/` parent dir is OpenCode's own per-project convention (used
76
- * for project-local config, plans, dumps, plugin installs). Anchoring
77
- * magic-context artifacts under `.opencode/magic-context/` keeps them
78
- * co-located with related OpenCode metadata and makes them easy for users to
79
- * locate when debugging.
75
+ * Using a harness-agnostic name (`/.magic-context/` rather than
76
+ * `/.opencode/`) ensures the same path works for both OpenCode and Pi.
80
77
  *
81
78
  * Logger does NOT use this — log files stay in the per-harness tmp subtree
82
79
  * because they are written by the plugin process itself (no model-side Read
83
80
  * tool call, no permission prompt) and span sessions/projects.
84
81
  */
85
82
  export function getProjectMagicContextDir(directory: string): string {
86
- return path.join(directory, ".opencode", "magic-context");
83
+ return path.join(directory, ".magic-context");
87
84
  }
88
85
 
89
86
  /**
90
87
  * Project-local historian artifact directory.
91
88
  *
92
- * Layout: `<project-directory>/.opencode/magic-context/historian/`
89
+ * Layout: `<project-directory>/.magic-context/historian/`
93
90
  *
94
91
  * Used for:
95
92
  * - existing-state offload XMLs that long historian/recomp passes write
@@ -97,8 +94,9 @@ export function getProjectMagicContextDir(directory: string): string {
97
94
  * - validation-failure dump XMLs preserved for debugging
98
95
  *
99
96
  * Callers must `mkdirSync(dir, { recursive: true })` before writing — the
100
- * `.opencode/` parent may not exist on a fresh project, and write failures
101
- * here must degrade gracefully (e.g. historian falls back to inline state).
97
+ * `.magic-context/` parent may not exist on a fresh project, and write
98
+ * failures here must degrade gracefully (e.g. historian falls back to inline
99
+ * state).
102
100
  */
103
101
  export function getProjectMagicContextHistorianDir(directory: string): string {
104
102
  return path.join(getProjectMagicContextDir(directory), "historian");
@@ -39,7 +39,11 @@ describe("models-dev-cache", () => {
39
39
  if (v === undefined) delete process.env[k];
40
40
  else process.env[k] = v;
41
41
  }
42
- rmSync(tempDir, { recursive: true, force: true });
42
+ try {
43
+ rmSync(tempDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
44
+ } catch {
45
+ /* Ignore EBUSY on Windows */
46
+ }
43
47
  clearModelsDevCache();
44
48
  });
45
49
 
@@ -234,7 +238,7 @@ describe("models-dev-cache", () => {
234
238
  expect(getModelsDevContextLimit("anthropic", "claude-4")).toBeUndefined();
235
239
  });
236
240
 
237
- test("API cache takes priority over file cache", async () => {
241
+ test("takes the larger limit when both layers know the model (API larger)", async () => {
238
242
  // Seed file layer with one value.
239
243
  const opencodeDir = join(tempDir, "opencode");
240
244
  mkdirSync(opencodeDir, { recursive: true });
@@ -248,7 +252,7 @@ describe("models-dev-cache", () => {
248
252
  // Sanity: file layer returns 100000 before API refresh.
249
253
  expect(getModelsDevContextLimit("anthropic", "claude-4")).toBe(100000);
250
254
 
251
- // Mock client providing DIFFERENT value via API.
255
+ // Mock client providing a LARGER value via API.
252
256
  const mockClient = {
253
257
  config: {
254
258
  providers: async () => ({
@@ -267,7 +271,7 @@ describe("models-dev-cache", () => {
267
271
  };
268
272
  await refreshModelLimitsFromApi(mockClient);
269
273
 
270
- // API value wins.
274
+ // Larger (API) value wins.
271
275
  expect(getModelsDevContextLimit("anthropic", "claude-4")).toBe(1000000);
272
276
 
273
277
  const state = getModelsDevCacheState();
@@ -275,6 +279,70 @@ describe("models-dev-cache", () => {
275
279
  expect(state.apiCount).toBe(1);
276
280
  });
277
281
 
282
+ test("file value wins when the live API reports a smaller (wrong) limit (issue #117)", async () => {
283
+ // The ollama-cloud scenario: models.dev has the correct large window, but
284
+ // ollama reports its tiny default num_ctx via the live /config/providers
285
+ // API. The larger, correct file value must win so pressure isn't bogus.
286
+ const opencodeDir = join(tempDir, "opencode");
287
+ mkdirSync(opencodeDir, { recursive: true });
288
+ writeFileSync(
289
+ join(opencodeDir, "models.json"),
290
+ JSON.stringify({
291
+ "ollama-cloud": {
292
+ models: { "deepseek-v4-pro": { limit: { context: 1048576 } } },
293
+ },
294
+ }),
295
+ );
296
+
297
+ const mockClient = {
298
+ config: {
299
+ providers: async () => ({
300
+ data: {
301
+ providers: [
302
+ {
303
+ id: "ollama-cloud",
304
+ models: {
305
+ // Bogus tiny default num_ctx from ollama.
306
+ "deepseek-v4-pro": { limit: { context: 8192 } },
307
+ },
308
+ },
309
+ ],
310
+ },
311
+ }),
312
+ },
313
+ };
314
+ await refreshModelLimitsFromApi(mockClient);
315
+
316
+ // Larger (file/models.dev) value wins, not the tiny live-API value.
317
+ expect(getModelsDevContextLimit("ollama-cloud", "deepseek-v4-pro")).toBe(1048576);
318
+ });
319
+
320
+ test("matches a tagged ollama model against its tag-less models.dev entry (issue #117)", () => {
321
+ // ollama invokes cloud models with a tag (deepseek-v4-pro:cloud) while
322
+ // models.dev stores them tag-less (deepseek-v4-pro).
323
+ const opencodeDir = join(tempDir, "opencode");
324
+ mkdirSync(opencodeDir, { recursive: true });
325
+ writeFileSync(
326
+ join(opencodeDir, "models.json"),
327
+ JSON.stringify({
328
+ "ollama-cloud": {
329
+ models: {
330
+ "deepseek-v4-pro": { limit: { context: 1048576 } },
331
+ // A legitimately-tagged model must still match exactly.
332
+ "gemma3:27b": { limit: { context: 131072 } },
333
+ },
334
+ },
335
+ }),
336
+ );
337
+
338
+ // Tagged invocation falls back to the tag-less entry.
339
+ expect(getModelsDevContextLimit("ollama-cloud", "deepseek-v4-pro:cloud")).toBe(1048576);
340
+ // Exact tagged match still wins (no wrongful collapse).
341
+ expect(getModelsDevContextLimit("ollama-cloud", "gemma3:27b")).toBe(131072);
342
+ // Unknown tagged model with no tag-less base stays undefined.
343
+ expect(getModelsDevContextLimit("ollama-cloud", "nonexistent:cloud")).toBeUndefined();
344
+ });
345
+
278
346
  test("refreshModelLimitsFromApi tolerates empty/malformed responses", async () => {
279
347
  // Undefined data.
280
348
  await refreshModelLimitsFromApi({
@@ -298,19 +298,58 @@ export async function refreshModelLimitsFromApi(client: OpencodeClientLike): Pro
298
298
  * Returns `undefined` if neither layer knows the model.
299
299
  */
300
300
  export function getModelsDevContextLimit(providerID: string, modelID: string): number | undefined {
301
- const key = `${providerID}/${modelID}`;
302
-
303
- if (apiCache) {
304
- const fromApi = apiCache.get(key)?.limit;
305
- if (typeof fromApi === "number") return fromApi;
306
- }
307
-
308
301
  const now = Date.now();
309
302
  if (!fileCache || now - fileLastAttempt > RELOAD_INTERVAL_MS) {
310
303
  fileLastAttempt = now;
311
304
  fileCache = loadModelsDevMetadataFromFile();
312
305
  }
313
- return fileCache.get(key)?.limit;
306
+
307
+ const fromApi = lookupLimitWithTagFallback(apiCache, providerID, modelID);
308
+ const fromFile = lookupLimitWithTagFallback(fileCache, providerID, modelID);
309
+
310
+ // When BOTH layers know the model, take the LARGER limit. Providers never
311
+ // under-report their real window, so a suspiciously small value — e.g.
312
+ // ollama reporting its default `num_ctx` (4k/8k) for a cloud model via the
313
+ // live `/config/providers` API — must not override the correct, larger
314
+ // models.dev value. A genuinely smaller real limit (provider actually
315
+ // rejects at N) is captured separately via the overflow-detection path
316
+ // (detectedContextLimit), not here. (issue #117)
317
+ if (typeof fromApi === "number" && typeof fromFile === "number") {
318
+ return Math.max(fromApi, fromFile);
319
+ }
320
+ return fromApi ?? fromFile;
321
+ }
322
+
323
+ /**
324
+ * Look up a model's limit in one cache layer, with an ollama-style tag-suffix
325
+ * fallback.
326
+ *
327
+ * models.dev stores some models WITH a colon tag (e.g. `gemma3:27b`,
328
+ * `deepseek-v3.1:671b`) and ollama-cloud base models WITHOUT one
329
+ * (`deepseek-v4-pro`). But ollama invokes cloud models with a tag at runtime
330
+ * (`deepseek-v4-pro:cloud`), so OpenCode reports the tagged id. An exact-only
331
+ * match therefore misses → falls back to the 128k default → wrong pressure
332
+ * denominator (issue #117).
333
+ *
334
+ * Strategy: exact match first (never collapses a legitimately-tagged model),
335
+ * then retry once with the last `:tag` segment stripped.
336
+ */
337
+ function lookupLimitWithTagFallback(
338
+ cache: Map<string, CachedModelMetadata> | null,
339
+ providerID: string,
340
+ modelID: string,
341
+ ): number | undefined {
342
+ if (!cache) return undefined;
343
+ const exact = cache.get(`${providerID}/${modelID}`)?.limit;
344
+ if (typeof exact === "number") return exact;
345
+
346
+ const colonIdx = modelID.lastIndexOf(":");
347
+ if (colonIdx > 0) {
348
+ const baseModel = modelID.slice(0, colonIdx);
349
+ const fallback = cache.get(`${providerID}/${baseModel}`)?.limit;
350
+ if (typeof fallback === "number") return fallback;
351
+ }
352
+ return undefined;
314
353
  }
315
354
 
316
355
  /** Clear in-memory caches (for testing). */
@@ -18,7 +18,11 @@ describe("opencode-compaction-detector", () => {
18
18
  });
19
19
 
20
20
  afterEach(() => {
21
- rmSync(tmpDir, { recursive: true, force: true });
21
+ try {
22
+ rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
23
+ } catch {
24
+ /* Ignore EBUSY on Windows */
25
+ }
22
26
  delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
23
27
  });
24
28
 
@@ -30,7 +34,11 @@ describe("opencode-compaction-detector", () => {
30
34
  const result = isOpenCodeAutoCompactionEnabled(emptyDir);
31
35
 
32
36
  expect(result).toBe(true);
33
- rmSync(emptyDir, { recursive: true, force: true });
37
+ try {
38
+ rmSync(emptyDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
39
+ } catch {
40
+ /* Ignore EBUSY on Windows */
41
+ }
34
42
  });
35
43
  });
36
44
 
@@ -1,10 +1,11 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { createServer } from "node:http";
4
4
  import { tmpdir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { MagicContextRpcClient } from "./rpc-client";
7
- import { rpcPortFilePath } from "./rpc-utils";
7
+ import { MagicContextRpcServer } from "./rpc-server";
8
+ import { parseRpcPortFile, rpcPortFilePath } from "./rpc-utils";
8
9
 
9
10
  interface TestServer {
10
11
  port: number;
@@ -19,7 +20,11 @@ afterEach(async () => {
19
20
  await server.close();
20
21
  }
21
22
  for (const dir of tempDirs.splice(0)) {
22
- rmSync(dir, { recursive: true, force: true });
23
+ try {
24
+ rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
25
+ } catch {
26
+ /* Ignore EBUSY on Windows */
27
+ }
23
28
  }
24
29
  });
25
30
 
@@ -116,6 +121,52 @@ describe("MagicContextRpcClient", () => {
116
121
  expect(await client.call<{ value: string }>("value")).toEqual({ value: "second" });
117
122
  });
118
123
 
124
+ test("authenticates against a real server with the published token", async () => {
125
+ const storageDir = makeTempDir();
126
+ const directory = "/repo-auth";
127
+ const server = new MagicContextRpcServer(storageDir, directory);
128
+ server.handle("ping", async () => ({ pong: true }));
129
+ await server.start();
130
+ try {
131
+ const client = new MagicContextRpcClient(storageDir, directory);
132
+ // Real round-trip: client must read the token from the port file and
133
+ // send it as Bearer auth, or the server returns 401.
134
+ expect(await client.call<{ pong: boolean }>("ping")).toEqual({ pong: true });
135
+ } finally {
136
+ server.stop();
137
+ }
138
+ });
139
+
140
+ test("a request without the token is rejected 401 by the server", async () => {
141
+ const storageDir = makeTempDir();
142
+ const directory = "/repo-noauth";
143
+ const server = new MagicContextRpcServer(storageDir, directory);
144
+ server.handle("ping", async () => ({ pong: true }));
145
+ const port = await server.start();
146
+ try {
147
+ // Sanity: the port file carries a non-empty token.
148
+ const record = parseRpcPortFile(
149
+ readFileSync(rpcPortFilePath(storageDir, directory), "utf-8"),
150
+ );
151
+ expect(typeof record?.token).toBe("string");
152
+ expect((record?.token ?? "").length).toBeGreaterThan(0);
153
+
154
+ // A raw fetch with no Authorization header must be rejected.
155
+ const res = await fetch(`http://127.0.0.1:${port}/rpc/ping`, {
156
+ method: "POST",
157
+ headers: { "Content-Type": "application/json" },
158
+ body: "{}",
159
+ });
160
+ expect(res.status).toBe(401);
161
+
162
+ // Health stays open (no token required) for discovery.
163
+ const health = await fetch(`http://127.0.0.1:${port}/health`);
164
+ expect(health.status).toBe(200);
165
+ } finally {
166
+ server.stop();
167
+ }
168
+ });
169
+
119
170
  test("gives up when the port file points at a dead server", async () => {
120
171
  const storageDir = makeTempDir();
121
172
  const directory = "/repo";