@cortexkit/opencode-magic-context 0.43.2 → 0.44.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 (297) hide show
  1. package/README.md +18 -2
  2. package/dist/agents/magic-context-prompt.d.ts +2 -0
  3. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  4. package/dist/{announcement-1a8bvswd.js → announcement-n8hs7kjw.js} +2 -2
  5. package/dist/config/index.d.ts +13 -0
  6. package/dist/config/index.d.ts.map +1 -1
  7. package/dist/config/live-child-output-cap.d.ts +18 -0
  8. package/dist/config/live-child-output-cap.d.ts.map +1 -0
  9. package/dist/config/project-security.d.ts +6 -3
  10. package/dist/config/project-security.d.ts.map +1 -1
  11. package/dist/config/schema/magic-context.d.ts +28 -1
  12. package/dist/config/schema/magic-context.d.ts.map +1 -1
  13. package/dist/{conflict-warning-hook-hyk9y29x.js → conflict-warning-hook-d14ah73v.js} +6 -6
  14. package/dist/{derive-budgets-b8ybssh9.js → derive-budgets-gd2270s0.js} +3 -3
  15. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  16. package/dist/features/magic-context/compartment-embedding.d.ts +3 -2
  17. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -1
  18. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/curate-category-rotation.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/curate-memory-safety.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/failed-invocation-evidence.d.ts +13 -0
  23. package/dist/features/magic-context/dreamer/failed-invocation-evidence.d.ts.map +1 -0
  24. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/lease.d.ts +5 -2
  26. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  27. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  28. package/dist/features/magic-context/dreamer/memory-claim-safety.d.ts.map +1 -1
  29. package/dist/features/magic-context/dreamer/module-apply.d.ts +1 -0
  30. package/dist/features/magic-context/dreamer/module-apply.d.ts.map +1 -1
  31. package/dist/features/magic-context/dreamer/open-opencode-db.d.ts +2 -0
  32. package/dist/features/magic-context/dreamer/open-opencode-db.d.ts.map +1 -1
  33. package/dist/features/magic-context/dreamer/primer-seed.d.ts +11 -5
  34. package/dist/features/magic-context/dreamer/primer-seed.d.ts.map +1 -1
  35. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +2 -2
  36. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  37. package/dist/features/magic-context/dreamer/retrospective-learnings.d.ts.map +1 -1
  38. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +1 -1
  39. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  40. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  41. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +2 -0
  42. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  43. package/dist/features/magic-context/dreamer/verify.d.ts +19 -0
  44. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  45. package/dist/features/magic-context/fail-closed-block.d.ts +7 -0
  46. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  47. package/dist/features/magic-context/index.d.ts +1 -0
  48. package/dist/features/magic-context/index.d.ts.map +1 -1
  49. package/dist/features/magic-context/install-instance-id.d.ts +33 -0
  50. package/dist/features/magic-context/install-instance-id.d.ts.map +1 -0
  51. package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
  52. package/dist/features/magic-context/memory/embedding-backfill.d.ts.map +1 -1
  53. package/dist/features/magic-context/memory/project-identity.d.ts +7 -0
  54. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  55. package/dist/features/magic-context/memory/single-store-embedding-drain.d.ts +35 -0
  56. package/dist/features/magic-context/memory/single-store-embedding-drain.d.ts.map +1 -0
  57. package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
  58. package/dist/features/magic-context/message-index.d.ts +23 -0
  59. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  60. package/dist/features/magic-context/migrations.d.ts +7 -0
  61. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  62. package/dist/features/magic-context/mural/mural-font.generated.d.ts.map +1 -1
  63. package/dist/features/magic-context/overflow-detection.d.ts +31 -8
  64. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  65. package/dist/features/magic-context/project-embedding-registry.d.ts +40 -1
  66. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  67. package/dist/features/magic-context/search.d.ts.map +1 -1
  68. package/dist/features/magic-context/session-cache-ttl.d.ts +10 -0
  69. package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -0
  70. package/dist/features/magic-context/session-decision-calibration.d.ts.map +1 -1
  71. package/dist/features/magic-context/storage-db.d.ts +1 -1
  72. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  73. package/dist/features/magic-context/storage-meta-persisted.d.ts +20 -5
  74. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  75. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  76. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  77. package/dist/features/magic-context/storage-notes.d.ts +1 -0
  78. package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
  79. package/dist/features/magic-context/storage-primers.d.ts.map +1 -1
  80. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  81. package/dist/features/magic-context/storage-unavailable-reason.d.ts +17 -0
  82. package/dist/features/magic-context/storage-unavailable-reason.d.ts.map +1 -0
  83. package/dist/features/magic-context/storage.d.ts +1 -1
  84. package/dist/features/magic-context/storage.d.ts.map +1 -1
  85. package/dist/features/magic-context/store-generation-rebase.d.ts +33 -1
  86. package/dist/features/magic-context/store-generation-rebase.d.ts.map +1 -1
  87. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
  88. package/dist/features/magic-context/transform-decision-log.d.ts +1 -1
  89. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  90. package/dist/features/magic-context/types.d.ts +8 -3
  91. package/dist/features/magic-context/types.d.ts.map +1 -1
  92. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/apply-operations.d.ts +105 -9
  94. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/compartment-parser.d.ts +2 -0
  97. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +1 -0
  99. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -0
  104. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/context-limit-resolution.d.ts +31 -0
  107. package/dist/hooks/magic-context/context-limit-resolution.d.ts.map +1 -0
  108. package/dist/hooks/magic-context/embed-history-runner.d.ts +0 -1
  109. package/dist/hooks/magic-context/embed-history-runner.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  111. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/format-embed-status.d.ts.map +1 -1
  115. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  116. package/dist/hooks/magic-context/historian-host-runner.d.ts +233 -0
  117. package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -0
  118. package/dist/hooks/magic-context/hook.d.ts +1 -3
  119. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  120. package/dist/hooks/magic-context/image-token-estimate.d.ts +9 -0
  121. package/dist/hooks/magic-context/image-token-estimate.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/inject-compartments.d.ts +60 -1
  123. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/lkg-slot.d.ts +6 -0
  125. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  126. package/dist/hooks/magic-context/module-client.d.ts +21 -0
  127. package/dist/hooks/magic-context/module-client.d.ts.map +1 -0
  128. package/dist/hooks/magic-context/module-state-sync.d.ts +6 -2
  129. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  130. package/dist/hooks/magic-context/module-tool-backends.d.ts +47 -0
  131. package/dist/hooks/magic-context/module-tool-backends.d.ts.map +1 -0
  132. package/dist/hooks/magic-context/module-transport.d.ts +1 -1
  133. package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
  134. package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts +12 -0
  135. package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts.map +1 -0
  136. package/dist/hooks/magic-context/persist-filtered-noise.d.ts.map +1 -1
  137. package/dist/hooks/magic-context/read-session-chunk.d.ts +28 -0
  138. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  139. package/dist/hooks/magic-context/read-session-db.d.ts +5 -1
  140. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  141. package/dist/hooks/magic-context/read-session-raw.d.ts +10 -0
  142. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  143. package/dist/hooks/magic-context/rust-mode-transform.d.ts +35 -0
  144. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  145. package/dist/hooks/magic-context/rust-refusal-recovery.d.ts +18 -0
  146. package/dist/hooks/magic-context/rust-refusal-recovery.d.ts.map +1 -1
  147. package/dist/hooks/magic-context/rust-served-m0.d.ts +20 -0
  148. package/dist/hooks/magic-context/rust-served-m0.d.ts.map +1 -0
  149. package/dist/hooks/magic-context/store-ahead-refusal.d.ts +40 -0
  150. package/dist/hooks/magic-context/store-ahead-refusal.d.ts.map +1 -0
  151. package/dist/hooks/magic-context/strip-content.d.ts +5 -5
  152. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  153. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  154. package/dist/hooks/magic-context/tag-messages.d.ts +19 -0
  155. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  156. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  157. package/dist/hooks/magic-context/tool-drop-target.d.ts +14 -0
  158. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  159. package/dist/hooks/magic-context/tool-input-size.d.ts +21 -0
  160. package/dist/hooks/magic-context/tool-input-size.d.ts.map +1 -0
  161. package/dist/hooks/magic-context/tool-sweep-policy.d.ts.map +1 -1
  162. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +124 -11
  163. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  164. package/dist/hooks/magic-context/transform.d.ts +14 -0
  165. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  166. package/dist/{index-pa67brzn.js → index-0rp6pj54.js} +3 -3
  167. package/dist/{index-4b30nf6z.js → index-32b096cc.js} +1 -1
  168. package/dist/{index-wgzyeyf5.js → index-3bygzk2d.js} +23 -7
  169. package/dist/{index-jgnag7sa.js → index-a9vbtdzv.js} +4 -4
  170. package/dist/{index-bmzt6xgy.js → index-cz88kw0n.js} +13 -3
  171. package/dist/{index-6j8th2hd.js → index-d107az47.js} +2 -2
  172. package/dist/{index-g6hpfkqa.js → index-jtdrvmcn.js} +3290 -286
  173. package/dist/{index-9r1cbh9e.js → index-kr5qehbd.js} +1 -1
  174. package/dist/{index-febg06dy.js → index-kwe03yde.js} +1 -1
  175. package/dist/{index-tdrv6nd0.js → index-s6b7h12g.js} +352 -71
  176. package/dist/{index-6z9g64js.js → index-s70wtd8k.js} +4 -1
  177. package/dist/{index-0ccmyzbj.js → index-stb5fg12.js} +2 -2
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +5230 -5663
  180. package/dist/{message-time-backfill-0ccvyk9n.js → message-time-backfill-q374fxc5.js} +4 -4
  181. package/dist/plugin/dream-timer.d.ts +2 -0
  182. package/dist/plugin/dream-timer.d.ts.map +1 -1
  183. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  184. package/dist/plugin/messages-transform.d.ts.map +1 -1
  185. package/dist/plugin/native-compaction-guard.d.ts +22 -0
  186. package/dist/plugin/native-compaction-guard.d.ts.map +1 -0
  187. package/dist/plugin/rpc-handlers.d.ts +11 -0
  188. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  189. package/dist/{read-session-chunk-53a7qbrh.js → read-session-chunk-fkdv2g5b.js} +8 -4
  190. package/dist/{recomp-orchestrator-adsgqgse.js → recomp-orchestrator-k4h5n0p3.js} +9 -9
  191. package/dist/{safe-notification-target-vrkjrbgb.js → safe-notification-target-50gm9c58.js} +3 -3
  192. package/dist/{send-session-notification-e28r4xx2.js → send-session-notification-g2k03zyn.js} +5 -5
  193. package/dist/shared/cache-ttl-display.d.ts +3 -1
  194. package/dist/shared/cache-ttl-display.d.ts.map +1 -1
  195. package/dist/shared/harness.d.ts +11 -0
  196. package/dist/shared/harness.d.ts.map +1 -1
  197. package/dist/shared/index.d.ts +1 -0
  198. package/dist/shared/index.d.ts.map +1 -1
  199. package/dist/shared/logger.d.ts.map +1 -1
  200. package/dist/shared/model-cache-ttl.d.ts +9 -0
  201. package/dist/shared/model-cache-ttl.d.ts.map +1 -0
  202. package/dist/shared/model-suggestion-retry.d.ts +13 -1
  203. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  204. package/dist/shared/prompt-async-transport.d.ts +25 -0
  205. package/dist/shared/prompt-async-transport.d.ts.map +1 -0
  206. package/dist/shared/rpc-server.d.ts +1 -0
  207. package/dist/shared/rpc-server.d.ts.map +1 -1
  208. package/dist/shared/rpc-types.d.ts +17 -1
  209. package/dist/shared/rpc-types.d.ts.map +1 -1
  210. package/dist/shared/rpc-utils.d.ts +3 -0
  211. package/dist/shared/rpc-utils.d.ts.map +1 -1
  212. package/dist/shared/sqlite-write-locker-test-support.d.ts +7 -0
  213. package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -0
  214. package/dist/shared/status-summary.d.ts +9 -1
  215. package/dist/shared/status-summary.d.ts.map +1 -1
  216. package/dist/shared/status-view.d.ts +1 -1
  217. package/dist/shared/status-view.d.ts.map +1 -1
  218. package/dist/shared/tag-transcript.d.ts.map +1 -1
  219. package/dist/shared/user-facing-codes.d.ts +10 -5
  220. package/dist/shared/user-facing-codes.d.ts.map +1 -1
  221. package/dist/shared/window-geometry.d.ts +6 -3
  222. package/dist/shared/window-geometry.d.ts.map +1 -1
  223. package/dist/tools/ctx-expand/render.d.ts.map +1 -1
  224. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  225. package/dist/tools/ctx-note/tools.d.ts.map +1 -1
  226. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  227. package/dist/tools/ctx-search/tools.d.ts.map +1 -1
  228. package/dist/v2/fold/boundary.d.ts +61 -0
  229. package/dist/v2/fold/boundary.d.ts.map +1 -0
  230. package/dist/v2/fold/restore.d.ts.map +1 -1
  231. package/dist/v2/hidden-completion.d.ts +16 -3
  232. package/dist/v2/hidden-completion.d.ts.map +1 -1
  233. package/dist/v2/hooks/context.d.ts +45 -14
  234. package/dist/v2/hooks/context.d.ts.map +1 -1
  235. package/dist/v2/hooks/hidden-child.d.ts +6 -0
  236. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  237. package/dist/v2/hooks/payload.d.ts.map +1 -1
  238. package/dist/v2/hooks/rpc-live-state.d.ts +1 -1
  239. package/dist/v2/hooks/rpc-live-state.d.ts.map +1 -1
  240. package/dist/v2/hooks/rust-mode.d.ts +47 -0
  241. package/dist/v2/hooks/rust-mode.d.ts.map +1 -0
  242. package/dist/v2/hooks/session-project-backfill.d.ts +15 -0
  243. package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -0
  244. package/dist/v2/hooks/storage-gate.d.ts +42 -0
  245. package/dist/v2/hooks/storage-gate.d.ts.map +1 -0
  246. package/dist/v2/hooks/storage-notice.d.ts +29 -0
  247. package/dist/v2/hooks/storage-notice.d.ts.map +1 -0
  248. package/dist/v2/hooks/store.d.ts +16 -0
  249. package/dist/v2/hooks/store.d.ts.map +1 -1
  250. package/dist/v2/hooks/tools.d.ts +11 -2
  251. package/dist/v2/hooks/tools.d.ts.map +1 -1
  252. package/dist/v2/hooks/types.d.ts +4 -0
  253. package/dist/v2/hooks/types.d.ts.map +1 -1
  254. package/dist/v2/hooks/usage-persist.d.ts +31 -0
  255. package/dist/v2/hooks/usage-persist.d.ts.map +1 -0
  256. package/dist/v2/server.js +27201 -22695
  257. package/dist/v2/store-reader.d.ts +9 -0
  258. package/dist/v2/store-reader.d.ts.map +1 -1
  259. package/dist/wrapup-orchestrator-35y56636.js +18 -0
  260. package/package.json +1 -1
  261. package/src/agents/magic-context-prompt.test.ts +125 -0
  262. package/src/agents/magic-context-prompt.ts +15 -10
  263. package/src/config/index.ts +40 -0
  264. package/src/config/live-child-output-cap.test.ts +75 -0
  265. package/src/config/live-child-output-cap.ts +70 -0
  266. package/src/config/live-run-config.test.ts +24 -11
  267. package/src/config/misplaced-agent-model.test.ts +61 -0
  268. package/src/config/project-security.test.ts +17 -0
  269. package/src/config/project-security.ts +28 -4
  270. package/src/config/schema/magic-context.test.ts +16 -1
  271. package/src/config/schema/magic-context.ts +35 -6
  272. package/src/shared/cache-ttl-display.ts +14 -20
  273. package/src/shared/harness.ts +14 -0
  274. package/src/shared/index.ts +1 -0
  275. package/src/shared/logger.test.ts +63 -2
  276. package/src/shared/logger.ts +23 -5
  277. package/src/shared/model-cache-ttl.test.ts +40 -0
  278. package/src/shared/model-cache-ttl.ts +58 -0
  279. package/src/shared/model-suggestion-retry.test.ts +115 -4
  280. package/src/shared/model-suggestion-retry.ts +57 -12
  281. package/src/shared/prompt-async-transport.test.ts +217 -0
  282. package/src/shared/prompt-async-transport.ts +242 -0
  283. package/src/shared/prompt-surface-a1-golden.md +17 -17
  284. package/src/shared/rpc-server.ts +13 -1
  285. package/src/shared/rpc-types.ts +18 -1
  286. package/src/shared/rpc-utils.ts +28 -0
  287. package/src/shared/sqlite-transaction-mode-guard.test.ts +198 -0
  288. package/src/shared/sqlite-write-locker-test-support.ts +62 -0
  289. package/src/shared/status-detail-text.test.ts +23 -0
  290. package/src/shared/status-summary.ts +25 -1
  291. package/src/shared/status-view.ts +1 -1
  292. package/src/shared/tag-transcript.ts +82 -19
  293. package/src/shared/user-facing-codes.ts +12 -5
  294. package/src/shared/window-geometry.ts +6 -3
  295. package/dist/v2/hooks/provider-admission.d.ts +0 -22
  296. package/dist/v2/hooks/provider-admission.d.ts.map +0 -1
  297. package/dist/wrapup-orchestrator-8xzbpvm8.js +0 -18
@@ -0,0 +1,61 @@
1
+ /// <reference types="bun-types" />
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import { resolveHistorianModel } from "../shared/model-resolution";
5
+ import { misplacedAgentModelWarnings, parsePluginConfig } from "./index";
6
+
7
+ /**
8
+ * A bare `historian.model` is schema-valid, reads naturally, and resolves to
9
+ * nothing. These tests pin both halves: that the resolver really does ignore it,
10
+ * and that the loader now says so instead of leaving the user with a subsystem
11
+ * that looks configured and never runs.
12
+ */
13
+ describe("misplaced agent model warning", () => {
14
+ it("is what a top-level historian.model actually resolves to", () => {
15
+ // The premise, asserted rather than assumed: if this ever starts
16
+ // resolving, the warning below becomes wrong and should be deleted.
17
+ expect(resolveHistorianModel({ historian: { model: "openai/gpt-5" } }, "opencode")).toEqual(
18
+ {
19
+ primary: undefined,
20
+ fallbacks: [],
21
+ },
22
+ );
23
+ });
24
+
25
+ it("warns when the model sits outside every harness block", () => {
26
+ const warnings = misplacedAgentModelWarnings({ historian: { model: "openai/gpt-5" } });
27
+ expect(warnings).toHaveLength(1);
28
+ expect(warnings[0]).toContain("historian.model is not read");
29
+ expect(warnings[0]).toContain("historian.opencode.model");
30
+ });
31
+
32
+ it("stays quiet when a harness block carries the model", () => {
33
+ expect(
34
+ misplacedAgentModelWarnings({
35
+ historian: { model: "openai/gpt-5", opencode: { model: "openai/gpt-5" } },
36
+ }),
37
+ ).toEqual([]);
38
+ expect(
39
+ misplacedAgentModelWarnings({ historian: { opencode: { model: "openai/gpt-5" } } }),
40
+ ).toEqual([]);
41
+ expect(misplacedAgentModelWarnings({})).toEqual([]);
42
+ expect(misplacedAgentModelWarnings({ historian: { disable: true } })).toEqual([]);
43
+ });
44
+
45
+ it("covers the dreamer on the same rule", () => {
46
+ const warnings = misplacedAgentModelWarnings({ dreamer: { model: "openai/gpt-5" } });
47
+ expect(warnings).toHaveLength(1);
48
+ expect(warnings[0]).toContain("dreamer.model is not read");
49
+ });
50
+
51
+ it("reaches the pinned config-warning channel", () => {
52
+ // configWarnings is what /ctx-status, the Pi session-start notice and
53
+ // doctor read, so landing here is what makes the warning visible.
54
+ const parsed = parsePluginConfig({ historian: { model: "openai/gpt-5" } });
55
+ expect(
56
+ (parsed.configWarnings ?? []).some((warning) =>
57
+ warning.includes("historian.model is not read"),
58
+ ),
59
+ ).toBe(true);
60
+ });
61
+ });
@@ -225,6 +225,23 @@ describe("stripUnsafeProjectConfigFields", () => {
225
225
  expect(warning).toContain("historian.pi.thinking_level");
226
226
  });
227
227
 
228
+ it("strips the historian runner from project config so a repo cannot redirect the completion", () => {
229
+ const raw: Record<string, unknown> = {
230
+ historian: {
231
+ runner: "host",
232
+ opencode: { runner: "host" },
233
+ temperature: 0.2,
234
+ },
235
+ };
236
+
237
+ const warnings = stripUnsafeProjectConfigFields(raw);
238
+
239
+ expect(raw.historian).toEqual({ opencode: {}, temperature: 0.2 });
240
+ const warning = warnings.join("\n");
241
+ expect(warning).toContain("historian.runner");
242
+ expect(warning).toContain("historian.opencode.runner");
243
+ });
244
+
228
245
  it("strips mural.model from project config but keeps the feature switch", () => {
229
246
  const raw: Record<string, unknown> = {
230
247
  mural: { enabled: true, model: "repo-controlled-model" },
@@ -23,7 +23,17 @@ const HARNESS_KEYS = PER_HARNESS_MODEL_KEYS;
23
23
  /** Every historian model-resolution field, including per-harness qualifiers.
24
24
  * Variant and thinking_level merge onto the user's historian model at resolve
25
25
  * time, so leaving them would let a cloned repo force extra spend. */
26
- const HISTORIAN_USER_ONLY_FIELDS = PER_HARNESS_MIGRATION_INVENTORY.historian.migrated_execution;
26
+ const HISTORIAN_USER_ONLY_FIELDS = [
27
+ ...PER_HARNESS_MIGRATION_INVENTORY.historian.migrated_execution,
28
+ // `runner` chooses which process and which provider account runs the hidden
29
+ // historian completion. A cloned repo redirecting it would move that spend and
30
+ // that prompt text somewhere the user never agreed to.
31
+ "runner",
32
+ // `host_runner` turns this machine's pull loop on and off. A cloned repo
33
+ // turning it ON would start spending the user's provider account on folds for
34
+ // every project this process serves, not just its own.
35
+ "host_runner",
36
+ ] as const;
27
37
  const PROMPT_SURFACE_USER_ONLY_FIELDS = ["guidance_override_path", "tool_descriptions"] as const;
28
38
 
29
39
  /**
@@ -348,10 +358,13 @@ function makeProjectThresholdWarning(field: string, reason: string): string {
348
358
  * can opt its own runtime into the experimental Rust pipeline. The resolver
349
359
  * requires trusted user-level `subc` configuration before Rust can activate.
350
360
  * - historian model-resolution fields (model, fallback_models, variant,
351
- * thinking_level), including both per-harness blocks — historian model
352
- * spend is user-level only. Qualifiers merge onto the user's historian
361
+ * thinking_level) and `runner`, including both per-harness blocks — historian
362
+ * model spend is user-level only. Qualifiers merge onto the user's historian
353
363
  * model at resolve time, so a cloned repo cannot force extra thinking or
354
- * variant cost.
364
+ * variant cost, and cannot move the completion to a different process or
365
+ * provider account. `host_runner` is stripped for the same reason in the other
366
+ * direction: a repo must not switch this machine's pull loop on and start
367
+ * spending the user's provider account on folds.
355
368
  * - `mural.model` at the top-level block, the legacy experimental spelling,
356
369
  * and any nested `mural.model` under hidden agents — a cloned repo cannot
357
370
  * choose where project memory is sent.
@@ -579,6 +592,17 @@ export function stripUnsafeProjectConfigFields(projectRaw: Record<string, unknow
579
592
  }
580
593
  }
581
594
 
595
+ // `dreamer.runner` chooses which process and which provider account runs the
596
+ // module-routed dreamer completions, for the same reason `historian.runner`
597
+ // is user-level only.
598
+ const dreamer = projectRaw.dreamer;
599
+ if (isPlainObject(dreamer) && "runner" in dreamer) {
600
+ delete dreamer.runner;
601
+ warnings.push(
602
+ "Ignoring dreamer.runner from project config (security: which process and provider account run dreamer completions is a user-level setting).",
603
+ );
604
+ }
605
+
582
606
  const mural = projectRaw.mural;
583
607
  if (isPlainObject(mural) && "model" in mural) {
584
608
  delete mural.model;
@@ -38,7 +38,7 @@ describe("MagicContextConfigSchema", () => {
38
38
  retrieval_count_promotion_threshold: 3,
39
39
  },
40
40
  todowrite: {
41
- enabled: true,
41
+ enabled: false,
42
42
  overlay: true,
43
43
  },
44
44
  });
@@ -50,6 +50,21 @@ describe("MagicContextConfigSchema", () => {
50
50
  });
51
51
  });
52
52
 
53
+ it("defaults todowrite off but honors explicit enablement and disablement", () => {
54
+ expect(MagicContextConfigSchema.parse({}).todowrite).toEqual({
55
+ enabled: false,
56
+ overlay: true,
57
+ });
58
+ expect(MagicContextConfigSchema.parse({ todowrite: {} }).todowrite.enabled).toBe(false);
59
+ expect(MagicContextConfigSchema.parse({ todowrite: { enabled: true } }).todowrite).toEqual({
60
+ enabled: true,
61
+ overlay: true,
62
+ });
63
+ expect(
64
+ MagicContextConfigSchema.parse({ todowrite: { enabled: false } }).todowrite.enabled,
65
+ ).toBe(false);
66
+ });
67
+
53
68
  describe("budget configuration", () => {
54
69
  it("accepts 90% execute thresholds and rejects 91%", () => {
55
70
  expect(
@@ -624,6 +624,12 @@ const AgentMetadataSchema = AgentOverrideConfigSchema.pick({
624
624
 
625
625
  /** Combined dreamer metadata plus independent strict execution blocks. */
626
626
  export const DreamerConfigSchema = AgentMetadataSchema.extend({
627
+ runner: z
628
+ .enum(["broca", "host"])
629
+ .optional()
630
+ .describe(
631
+ 'Which side runs the dreamer completions the Rust module routes (classify-memories) in Rust transform mode: "host" runs them on this process\'s carrier, "broca" routes them to the Broca module. When unset, historian.runner applies, and when that is unset too the harness decides the same way it does for the historian. User-level config only.',
632
+ ),
627
633
  opencode: DreamerOpenCodeHarnessBlockSchema.optional(),
628
634
  pi: DreamerPiHarnessBlockSchema.optional(),
629
635
  omp: DreamerOmpHarnessBlockSchema.optional(),
@@ -645,6 +651,25 @@ export const HistorianConfigSchema = AgentMetadataSchema.extend({
645
651
  opencode: OpenCodeHarnessBlockSchema.optional(),
646
652
  pi: PiHarnessBlockSchema.optional(),
647
653
  omp: OmpHarnessBlockSchema.optional(),
654
+ runner: z
655
+ .enum(["broca", "host"])
656
+ .optional()
657
+ .describe(
658
+ 'Which side runs the historian completion in Rust transform mode: "host" queues it for this process to run on the configured historian model, "broca" routes it to the Broca module. When unset the harness decides: OpenCode 1 and OpenCode 2 use "host", Claude Code (through the Thalamus gateway, which has no host to run a completion) uses "broca". User-level config only — it decides whose provider account pays for the call.',
659
+ ),
660
+ host_runner: z
661
+ .object({
662
+ enabled: z
663
+ .boolean()
664
+ .optional()
665
+ .describe(
666
+ "Whether this process answers historian runs queued for a claimant (default true). Setting it to false stops the pull loop without changing historian.runner, so an operator can take one machine out of the lane and leave the queued runs for another claimant or for the runner setting to be changed deliberately.",
667
+ ),
668
+ })
669
+ .optional()
670
+ .describe(
671
+ "Controls for this process's historian pull loop, which answers runs the Rust module queues for the host runner (`historian.runner: \"host\"`, or unset on OpenCode 1 and OpenCode 2). User-level config only — it decides whether this machine's provider account is spent on folds.",
672
+ ),
648
673
  two_pass: z
649
674
  .boolean()
650
675
  .default(false)
@@ -675,7 +700,7 @@ const BaseEmbeddingConfigSchema = z
675
700
  .enum(["local", "openai-compatible", "off", "synapse"])
676
701
  .default("local")
677
702
  .describe(
678
- "Embedding provider. 'local' uses Xenova/all-MiniLM-L6-v2, 'openai-compatible' requires endpoint and model, 'synapse' uses the certified local Synapse lane with an explicit fallback provider, and 'off' disables embeddings.",
703
+ "Embedding provider. 'local' uses Xenova/all-MiniLM-L6-v2, 'openai-compatible' requires endpoint and model, 'synapse' uses the certified local Synapse lane with an explicit fallback provider, and 'off' disables embeddings. Session history is embedded for semantic ctx_search whenever the provider is not 'off', regardless of memory.enabled; memories are embedded only while memory.enabled is true.",
679
704
  ),
680
705
  fallback_provider: EmbeddingFallbackProviderSchema.optional().describe(
681
706
  "Fallback provider for the Synapse lane. Required when provider is 'synapse'; local, openai-compatible, and off are valid.",
@@ -1131,7 +1156,7 @@ export const MagicContextConfigSchema = z
1131
1156
  .union([z.string(), z.object({ default: z.string() }).catchall(z.string())])
1132
1157
  .default("5m")
1133
1158
  .describe(
1134
- 'How long Magic Context assumes the provider\'s cached prefix stays valid. This is MC\'s own deferral gate — it does not change the provider\'s actual cache lifetime. String (e.g. "5m", "1h", "30s") or per-model object ({ default: "5m", "provider/model": "1h", "provider/*": "never" }); keys resolve most-specific first (exact provider/model, bare model ID, shorter dash-prefixes, then the provider/* wildcard, then default). Set to "never" to mean MC never assumes expiry (for lanes kept warm externally by a cache-keep tool) — disables the idle-TTL heuristic so MC never initiates a rebuild based on elapsed time. Provider-side extended TTL is a separate request-level concern (cache_control: { ttl } in the request body).',
1159
+ 'How long Magic Context assumes the provider\'s cached prefix stays valid. This is MC\'s own deferral gate — it does not change the provider\'s actual cache lifetime. String (e.g. "5m", "1h", "30s") or per-model object ({ default: "5m", "provider/model": "1h", "provider/*": "never" }); keys resolve most-specific first (exact provider/model, bare model ID, shorter dash-prefixes, then the provider/* wildcard). Explicit per-model entries win; otherwise GPT-5.6 and later (including gpt-6*, through any provider prefix) use a built-in 30m lifetime before the object default or 5m fallback. An unset or global "5m" opts into built-in defaults; any other global string is an explicit policy and wins. Policy is frozen per session, including across restarts; a model switch resolves against that frozen policy. /ctx-status shows the effective value and source. OpenAI documents at least 30 minutes since the latest write or reuse: https://developers.openai.com/api/docs/guides/prompt-caching (Cache lifetime and Summary of model differences). Set to "never" to mean MC never assumes expiry (for lanes kept warm externally by a cache-keep tool) — disables the idle-TTL heuristic so MC never initiates a rebuild based on elapsed time. Provider-side extended TTL is a separate request-level concern (cache_control: { ttl } in the request body).',
1135
1160
  ),
1136
1161
  prompt_surface: PromptSurfaceConfigSchema.default({ default: "full" }).describe(
1137
1162
  "Prompt-surface presets: default is full; models use bare model IDs, provider/model, or provider/* routing keys. Guidance and tool-description overrides are user-level only. OpenCode 1.x, Pi, and OMP register tool descriptions once per process (they follow the default preset). OpenCode 2 rewrites the five ctx_* descriptions per request from the draft model.",
@@ -1363,9 +1388,9 @@ export const MagicContextConfigSchema = z
1363
1388
  .object({
1364
1389
  enabled: z
1365
1390
  .boolean()
1366
- .default(true)
1391
+ .default(false)
1367
1392
  .describe(
1368
- "Pi only: register Magic Context's todowrite task-list tool. Disable if you use your own todo extension. OpenCode ships its own built-in todowrite; this setting has no effect there.",
1393
+ "Pi only: off by default. Set todowrite.enabled=true to register Magic Context's todowrite task-list tool and /todos command. OpenCode ships its own built-in todowrite; this setting has no effect there.",
1369
1394
  ),
1370
1395
  overlay: z
1371
1396
  .boolean()
@@ -1374,7 +1399,7 @@ export const MagicContextConfigSchema = z
1374
1399
  "Pi only: show the persistent todo overlay above the editor while tasks are active.",
1375
1400
  ),
1376
1401
  })
1377
- .default({ enabled: true, overlay: true })
1402
+ .default({ enabled: false, overlay: true })
1378
1403
  .describe(
1379
1404
  "Pi-only todowrite tool and overlay controls. Pi registers tools and widgets at extension boot, so changing this after /cd requires /reload or restart.",
1380
1405
  ),
@@ -1413,7 +1438,9 @@ export const MagicContextConfigSchema = z
1413
1438
  enabled: z
1414
1439
  .boolean()
1415
1440
  .default(true)
1416
- .describe("Enable cross-session memory (default: true)"),
1441
+ .describe(
1442
+ "Enable cross-session memory (default: true). Does not affect history embedding or semantic ctx_search over session history; set embedding.provider to 'off' to stop all embedding.",
1443
+ ),
1417
1444
  injection_budget_tokens: z
1418
1445
  .number()
1419
1446
  .min(500)
@@ -1525,10 +1552,12 @@ export const LIVE_RELOAD_CONFIG_PATHS = [
1525
1552
  "historian.omp.fallback_models",
1526
1553
  "historian.omp.thinking_level",
1527
1554
  "historian.two_pass",
1555
+ "historian.maxTokens",
1528
1556
  "historian_timeout_ms",
1529
1557
  "commit_cluster_trigger.enabled",
1530
1558
  "commit_cluster_trigger.min_clusters",
1531
1559
  "memory.auto_promote",
1560
+ "dreamer.maxTokens",
1532
1561
  "dreamer.opencode.model",
1533
1562
  "dreamer.opencode.fallback_models",
1534
1563
  "dreamer.opencode.variant",
@@ -1,7 +1,7 @@
1
1
  import type { MagicContextConfig } from "../config/schema/magic-context";
2
- import { resolveModelConfigValue } from "./prompt-surface";
2
+ import { type ResolvedCacheTtl, resolveModelCacheTtl } from "./model-cache-ttl";
3
3
 
4
- export type CacheTtlDisplaySource = "config" | "session" | "default";
4
+ export type CacheTtlDisplaySource = ResolvedCacheTtl["source"] | "session";
5
5
 
6
6
  export interface CacheTtlDisplay {
7
7
  value: string;
@@ -10,6 +10,7 @@ export interface CacheTtlDisplay {
10
10
  }
11
11
 
12
12
  export interface ResolveCacheTtlDisplayArgs {
13
+ frozen?: ResolvedCacheTtl;
13
14
  configured: MagicContextConfig["cache_ttl"];
14
15
  configuredExplicitly: boolean;
15
16
  modelKey: string | undefined;
@@ -24,6 +25,7 @@ export interface ResolveCacheTtlDisplayArgs {
24
25
  * displayed; otherwise use live config because the session value may be for another model.
25
26
  */
26
27
  export function resolveCacheTtlDisplay(args: ResolveCacheTtlDisplayArgs): CacheTtlDisplay {
28
+ if (args.frozen) return args.frozen;
27
29
  if (
28
30
  (args.sessionModelKey && (!args.modelKey || args.sessionModelKey === args.modelKey)) ||
29
31
  (!args.modelKey && !args.sessionModelKey && args.sessionValue !== "5m")
@@ -35,27 +37,19 @@ export function resolveCacheTtlDisplay(args: ResolveCacheTtlDisplayArgs): CacheT
35
37
  };
36
38
  }
37
39
 
38
- if (typeof args.configured === "string") {
39
- return {
40
- value: args.configured,
41
- source: args.configuredExplicitly ? "config" : "default",
42
- modelKey: args.modelKey,
43
- };
44
- }
45
-
46
- const matched = resolveModelConfigValue(args.configured, args.modelKey);
47
- if (matched) {
48
- return { value: matched.value, source: "config", modelKey: args.modelKey };
49
- }
50
-
51
- return {
52
- value: args.configured.default ?? "5m",
53
- source: "default",
54
- modelKey: args.modelKey,
55
- };
40
+ const resolved = resolveModelCacheTtl(args.configured, args.modelKey);
41
+ if (
42
+ resolved.source === "default" &&
43
+ typeof args.configured === "string" &&
44
+ args.configuredExplicitly
45
+ )
46
+ return { ...resolved, source: "config" };
47
+ return resolved;
56
48
  }
57
49
 
58
50
  export function formatCacheTtlDisplay(display: CacheTtlDisplay): string {
51
+ if (display.source === "OpenAI GPT-5.6+ default")
52
+ return `Cache TTL: ${display.value} (${display.source})`;
59
53
  if (display.source === "session") return `Cache TTL: ${display.value} (session)`;
60
54
  if (display.source === "config") {
61
55
  return `Cache TTL: ${display.value} (config for ${display.modelKey ?? "current model"})`;
@@ -52,6 +52,20 @@ export function getHarness(): HarnessId {
52
52
  return currentHarness;
53
53
  }
54
54
 
55
+ /**
56
+ * Whether this process may read OpenCode's own session store (opencode.db).
57
+ *
58
+ * Only the OpenCode plugins own that store. A Pi or OMP process keeps its
59
+ * history in Pi JSONL sessions and shares only Magic Context's context.db with
60
+ * OpenCode. Any history read in a Pi process for a session with no Pi provider
61
+ * used to fall through to opencode.db, which let background work load another
62
+ * project's OpenCode sessions into Pi's heap. Pi therefore treats the OpenCode
63
+ * store as absent, the same as on a Pi-only install.
64
+ */
65
+ export function harnessOwnsOpenCodeStore(harness: HarnessId = currentHarness): boolean {
66
+ return harness === "opencode" || harness === "opencode2";
67
+ }
68
+
55
69
  /**
56
70
  * Test-only helper to reset harness state between test cases. Do NOT call
57
71
  * from production code paths.
@@ -1,6 +1,7 @@
1
1
  export * from "./logger";
2
2
  export * from "./model-suggestion-retry";
3
3
  export * from "./normalize-sdk-response";
4
+ export * from "./prompt-async-transport";
4
5
  export * from "./prompt-surface";
5
6
  export * from "./prompt-surface-runtime";
6
7
  export * from "./resolve-fallbacks";
@@ -30,6 +30,8 @@ type RotationScenarioResult = {
30
30
  files: string[];
31
31
  };
32
32
 
33
+ type RetryScenarioResult = { content: string; failed: number };
34
+
33
35
  type HotPathScenarioResult = {
34
36
  statCalls: number;
35
37
  };
@@ -49,6 +51,16 @@ if (!root || !loggerModuleUrl || !scenario) {
49
51
  const logPath = path.join(root, "nested", "magic-context.log");
50
52
  process.env.MAGIC_CONTEXT_LOG_PATH = logPath;
51
53
  let statCalls = 0;
54
+ if (scenario === "retry") {
55
+ let attempts = 0;
56
+ mock.module("node:fs", () => ({
57
+ ...fs,
58
+ appendFileSync: (...args) => {
59
+ if (attempts++ === 0) throw new Error("injected transient write failure");
60
+ return fs.appendFileSync(...args);
61
+ },
62
+ }));
63
+ }
52
64
  if (scenario === "hot-path") {
53
65
  mock.module("node:fs", () => ({
54
66
  ...fs,
@@ -61,7 +73,28 @@ if (scenario === "hot-path") {
61
73
 
62
74
  const logger = await import(loggerModuleUrl);
63
75
 
64
- if (scenario === "rotation") {
76
+ if (scenario === "retry") {
77
+ logger.log("first buffered line");
78
+ logger.log("second buffered line");
79
+ logger.flushLogger();
80
+ const failed = logger.getLoggerDiagnostics().swallowedWriteCount;
81
+ logger.log("third buffered line");
82
+ logger.flushLogger();
83
+ console.log(JSON.stringify({ content: fs.readFileSync(logPath, "utf8"), failed }));
84
+ } else if (scenario === "oversized") {
85
+ logger.log("x".repeat(2_000_000));
86
+ logger.flushLogger();
87
+ console.log(JSON.stringify({ content: fs.readFileSync(logPath, "utf8"), failed: logger.getLoggerDiagnostics().swallowedWriteCount }));
88
+ } else if (scenario === "bounded") {
89
+ const failedPath = path.join(root, "unwritable-target");
90
+ fs.mkdirSync(failedPath);
91
+ process.env.MAGIC_CONTEXT_LOG_PATH = failedPath;
92
+ for (let i = 0; i < 100; i++) logger.log("line-" + i + "-" + "x".repeat(20_000));
93
+ logger.flushLogger();
94
+ process.env.MAGIC_CONTEXT_LOG_PATH = logPath;
95
+ logger.flushLogger();
96
+ console.log(JSON.stringify({ content: fs.readFileSync(logPath, "utf8"), failed: logger.getLoggerDiagnostics().swallowedWriteCount }));
97
+ } else if (scenario === "rotation") {
65
98
  const maxLogBytes = 32 * 1024 * 1024;
66
99
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
67
100
  fs.writeFileSync(logPath, Buffer.alloc(maxLogBytes, "o"));
@@ -144,7 +177,14 @@ afterEach(() => {
144
177
  });
145
178
 
146
179
  async function runLoggerScenario<T>(
147
- scenario: "recovery" | "diagnostics" | "rotation" | "hot-path",
180
+ scenario:
181
+ | "recovery"
182
+ | "diagnostics"
183
+ | "rotation"
184
+ | "hot-path"
185
+ | "retry"
186
+ | "bounded"
187
+ | "oversized",
148
188
  ): Promise<T> {
149
189
  const root = mkdtempSync(path.join(os.tmpdir(), "magic-context-logger-test-"));
150
190
  scenarioRoots.push(root);
@@ -171,6 +211,27 @@ async function runLoggerScenario<T>(
171
211
  }
172
212
 
173
213
  describe("logger", () => {
214
+ test("retries the whole buffered batch after a transient append failure", async () => {
215
+ const result = await runLoggerScenario<RetryScenarioResult>("retry");
216
+ expect(result.failed).toBe(1);
217
+ expect(result.content).toContain("first buffered line");
218
+ expect(result.content).toContain("second buffered line");
219
+ expect(result.content).toContain("third buffered line");
220
+ });
221
+ test("reports a single oversized line even if no payload remains buffered", async () => {
222
+ const result = await runLoggerScenario<RetryScenarioResult>("oversized");
223
+ expect(result.content).toMatch(/logger dropped 1 line: buffer exceeded/);
224
+ expect(Buffer.byteLength(result.content)).toBeLessThan(1000);
225
+ });
226
+
227
+ test("bounds failed batches and reports the number of discarded lines", async () => {
228
+ const result = await runLoggerScenario<RetryScenarioResult>("bounded");
229
+ expect(result.failed).toBeGreaterThan(0);
230
+ expect(Buffer.byteLength(result.content)).toBeLessThan(1_100_000);
231
+ expect(result.content).toMatch(/logger dropped \d+ lines: buffer exceeded/);
232
+ expect(result.content).toContain("line-99-");
233
+ });
234
+
174
235
  test("recreates a log directory removed while the process is running", async () => {
175
236
  const result = await runLoggerScenario<LoggerScenarioResult>("recovery");
176
237
 
@@ -9,6 +9,16 @@ let buffer: string[] = [];
9
9
  let flushTimer: ReturnType<typeof setTimeout> | null = null;
10
10
  const FLUSH_INTERVAL_MS = 500;
11
11
  const BUFFER_SIZE_LIMIT = 50;
12
+ const MAX_BUFFERED_BYTES = 1024 * 1024;
13
+ let bufferedBytes = 0;
14
+ let droppedLines = 0;
15
+
16
+ function boundBuffer(): void {
17
+ while (bufferedBytes > MAX_BUFFERED_BYTES && buffer.length > 0) {
18
+ bufferedBytes -= Buffer.byteLength(buffer.shift() ?? "");
19
+ droppedLines++;
20
+ }
21
+ }
12
22
  const MAX_LOG_FILE_BYTES = 32 * 1024 * 1024;
13
23
  const SIZE_CHECK_INTERVAL_FLUSHES = 64;
14
24
 
@@ -141,11 +151,12 @@ function flush(): void {
141
151
  clearTimeout(flushTimer);
142
152
  flushTimer = null;
143
153
  }
144
- if (buffer.length === 0) return;
145
- const bufferedData = buffer.join("");
146
- buffer = [];
154
+ if (buffer.length === 0 && droppedLines === 0) return;
155
+ const notice = droppedLines
156
+ ? `[${new Date().toISOString()}] [magic-context][global] logger dropped ${droppedLines} ${droppedLines === 1 ? "line" : "lines"}: buffer exceeded ${MAX_BUFFERED_BYTES} bytes while writes were pending\n`
157
+ : "";
147
158
  try {
148
- const data = capLogData(bufferedData);
159
+ const data = capLogData(notice + buffer.join(""));
149
160
  const logFile = getMagicContextLogPath();
150
161
  ensureDir(logFile);
151
162
  let currentSize = getCurrentLogSize(logFile);
@@ -155,6 +166,9 @@ function flush(): void {
155
166
  currentSize = 0;
156
167
  }
157
168
  fs.appendFileSync(logFile, data, { encoding: "utf8", mode: 0o600 });
169
+ buffer = [];
170
+ bufferedBytes = 0;
171
+ droppedLines = 0;
158
172
  activeLogFile = logFile;
159
173
  activeLogSize = currentSize + dataSize;
160
174
  flushesSinceSizeCheck++;
@@ -163,6 +177,7 @@ function flush(): void {
163
177
  activeLogSize = null;
164
178
  flushesSinceSizeCheck = 0;
165
179
  recordSwallowedWrite(error);
180
+ boundBuffer();
166
181
  }
167
182
  }
168
183
 
@@ -186,7 +201,10 @@ export function log(message: string, data?: unknown): void {
186
201
  `${data.message}${data.stack ? `\n${data.stack}` : ""}`,
187
202
  )}`
188
203
  : ` ${JSON.stringify(sanitizeConfigValue(data))}`;
189
- buffer.push(`[${timestamp}] ${sanitizeDiagnosticText(message)}${serialized}\n`);
204
+ const line = `[${timestamp}] ${sanitizeDiagnosticText(message)}${serialized}\n`;
205
+ buffer.push(line);
206
+ bufferedBytes += Buffer.byteLength(line);
207
+ boundBuffer();
190
208
  if (buffer.length >= BUFFER_SIZE_LIMIT) {
191
209
  flush();
192
210
  } else {
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { MagicContextConfigSchema } from "../config/schema/magic-context";
3
+ import { resolveCacheTtl } from "../hooks/magic-context/event-resolvers";
4
+
5
+ describe("known model cache TTL", () => {
6
+ it("matches canonical model versions through provider prefixes", () => {
7
+ for (const prefix of ["", "openai/", "openai-codex/", "openrouter/openai/", "azure/"]) {
8
+ for (const model of [
9
+ "gpt-5.6",
10
+ "gpt-5.6-sol",
11
+ "gpt-5.7",
12
+ "gpt-5.10",
13
+ "gpt-6",
14
+ "gpt-6.1",
15
+ "gpt-6-mini",
16
+ "gpt-7",
17
+ ]) {
18
+ expect(resolveCacheTtl("5m", prefix + model), prefix + model).toBe("30m");
19
+ }
20
+ for (const model of ["gpt-5.5", "gpt-5.1", "gpt-5", "gpt-4o", "unknown", "not-gpt-6"]) {
21
+ expect(resolveCacheTtl("5m", prefix + model), prefix + model).toBe("5m");
22
+ }
23
+ }
24
+ });
25
+ it("uses built-ins for unset and global 5m, but honors global 10m", () => {
26
+ expect(resolveCacheTtl(MagicContextConfigSchema.parse({}).cache_ttl, "openai/gpt-6")).toBe(
27
+ "30m",
28
+ );
29
+ expect(resolveCacheTtl("5m", "openai/gpt-6")).toBe("30m");
30
+ expect(resolveCacheTtl("10m", "openai/gpt-6")).toBe("10m");
31
+ });
32
+ it("honors per-model entries before built-ins and built-ins before object defaults", () => {
33
+ expect(resolveCacheTtl({ default: "10m" }, "openai/gpt-6")).toBe("30m");
34
+ expect(resolveCacheTtl({ default: "10m", "gpt-6": "5m" }, "openai/gpt-6")).toBe("5m");
35
+ expect(resolveCacheTtl({ default: "10m", "openai/*": "never" }, "openai/gpt-6")).toBe(
36
+ "never",
37
+ );
38
+ expect(resolveCacheTtl({ default: "10m" }, "other/model")).toBe("10m");
39
+ });
40
+ });
@@ -0,0 +1,58 @@
1
+ import { canonicalModelIdentity } from "./harness-provider-map";
2
+ import { resolveModelConfigValue } from "./prompt-surface";
3
+
4
+ export type CacheTtlConfig = string | Record<string, string>;
5
+ export type CacheTtlSource = "config" | "default" | "OpenAI GPT-5.6+ default";
6
+ export interface ResolvedCacheTtl {
7
+ value: string;
8
+ source: CacheTtlSource;
9
+ modelKey: string | undefined;
10
+ }
11
+
12
+ // Provider documentation: https://developers.openai.com/api/docs/guides/prompt-caching
13
+ // Cache lifetime / Summary of model differences: at least 30 minutes since write or reuse.
14
+ const MODEL_CACHE_LIFETIMES = [
15
+ {
16
+ source: "OpenAI GPT-5.6+ default" as const,
17
+ value: "30m",
18
+ matches(model: string): boolean {
19
+ const version = /^gpt-(\d+)(?:\.(\d+))?(?:$|[^\d.])/.exec(model);
20
+ if (!version) return false;
21
+ const major = Number(version[1]);
22
+ const minor = Number(version[2] ?? 0);
23
+ return major > 5 || (major === 5 && minor >= 6);
24
+ },
25
+ },
26
+ ];
27
+
28
+ export function resolveModelCacheTtl(
29
+ config: CacheTtlConfig | undefined,
30
+ modelKey: string | undefined,
31
+ ): ResolvedCacheTtl {
32
+ if (config && typeof config !== "string") {
33
+ const match =
34
+ modelKey &&
35
+ !modelKey.includes("/") &&
36
+ Object.hasOwn(config, modelKey) &&
37
+ modelKey !== "default"
38
+ ? config[modelKey]
39
+ : resolveModelConfigValue(config, modelKey)?.value;
40
+ if (match !== undefined) return { value: match, source: "config", modelKey };
41
+ }
42
+ // A non-5m global string is an explicit policy. The generic 5m default is
43
+ // not evidence of provider eviction: documented lifetimes avoid paid rewrites.
44
+ if (typeof config === "string" && config !== "5m")
45
+ return { value: config, source: "config", modelKey };
46
+ const model =
47
+ canonicalModelIdentity(modelKey ?? "")
48
+ .toLowerCase()
49
+ .split("/")
50
+ .at(-1) ?? "";
51
+ const known = MODEL_CACHE_LIFETIMES.find((entry) => entry.matches(model));
52
+ if (known) return { value: known.value, source: known.source, modelKey };
53
+ return {
54
+ value: (typeof config === "object" ? config.default : config) ?? "5m",
55
+ source: "default",
56
+ modelKey,
57
+ };
58
+ }