@retaw106/mmc-oc 2.2.1 → 2.4.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 (285) hide show
  1. package/dist/cli.js +14 -3
  2. package/dist/config/deprecated-config.d.ts +4 -0
  3. package/dist/config/deprecated-config.d.ts.map +1 -0
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/project-security.d.ts +2 -2
  6. package/dist/config/schema/magic-context.d.ts +36 -3
  7. package/dist/config/schema/magic-context.d.ts.map +1 -1
  8. package/dist/config-init.js +14 -3
  9. package/dist/{context-mrvm92n8.js → context-c72brxe5.js} +2410 -1801
  10. package/dist/{derive-budgets-rjjz5f7y.js → derive-budgets-s1p37t2g.js} +2 -2
  11. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  12. package/dist/features/magic-context/historian-failure-reason.d.ts +7 -0
  13. package/dist/features/magic-context/historian-failure-reason.d.ts.map +1 -0
  14. package/dist/features/magic-context/memory-maintainer/launcher.d.ts +13 -4
  15. package/dist/features/magic-context/memory-maintainer/launcher.d.ts.map +1 -1
  16. package/dist/features/magic-context/memory-maintainer/manual-commands.d.ts.map +1 -1
  17. package/dist/features/magic-context/memory-maintainer/model-chain.d.ts +47 -0
  18. package/dist/features/magic-context/memory-maintainer/model-chain.d.ts.map +1 -0
  19. package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts +17 -4
  20. package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts.map +1 -1
  21. package/dist/features/magic-context/memory-maintainer/pipeline.d.ts +6 -0
  22. package/dist/features/magic-context/memory-maintainer/pipeline.d.ts.map +1 -1
  23. package/dist/features/magic-context/message-index-async.d.ts +21 -3
  24. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  25. package/dist/features/magic-context/message-index.d.ts +7 -1
  26. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  27. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  28. package/dist/features/magic-context/pi-content-decisions.d.ts +7 -1
  29. package/dist/features/magic-context/pi-content-decisions.d.ts.map +1 -1
  30. package/dist/features/magic-context/search.d.ts +29 -0
  31. package/dist/features/magic-context/search.d.ts.map +1 -1
  32. package/dist/features/magic-context/storage-clone.d.ts +2 -0
  33. package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
  34. package/dist/features/magic-context/storage-compartment-history-version.d.ts +4 -0
  35. package/dist/features/magic-context/storage-compartment-history-version.d.ts.map +1 -0
  36. package/dist/features/magic-context/storage-db.d.ts +1 -1
  37. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  38. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
  39. package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -5
  40. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  41. package/dist/features/magic-context/storage-meta-session.d.ts +7 -1
  42. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  43. package/dist/features/magic-context/storage-session-tables.d.ts +5 -1
  44. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  45. package/dist/features/magic-context/storage-tag-identity-revision.d.ts +50 -0
  46. package/dist/features/magic-context/storage-tag-identity-revision.d.ts.map +1 -0
  47. package/dist/features/magic-context/storage-tags.d.ts +159 -5
  48. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  49. package/dist/features/magic-context/tagger.d.ts +2 -0
  50. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  51. package/dist/features/magic-context/transform-decision-log.d.ts +60 -2
  52. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts +34 -0
  54. package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts.map +1 -0
  55. package/dist/hooks/magic-context/active-anthropic-turn.d.ts +2 -0
  56. package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/apply-operations.d.ts +10 -1
  58. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/auto-search-runner.d.ts +2 -2
  60. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -2
  62. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/compartment-runner.d.ts +4 -0
  64. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/compartment-trigger-error.d.ts +11 -0
  66. package/dist/hooks/magic-context/compartment-trigger-error.d.ts.map +1 -0
  67. package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
  68. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +1 -0
  70. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/degraded-pass-refusal.d.ts +2 -0
  72. package/dist/hooks/magic-context/degraded-pass-refusal.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/edit-marker.d.ts +2 -0
  74. package/dist/hooks/magic-context/edit-marker.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  76. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/history-boundary-repair.d.ts +11 -2
  79. package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
  80. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/lkg-measured-request.d.ts +3 -1
  82. package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/lkg-persist.d.ts +10 -13
  84. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +62 -0
  86. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -0
  87. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +38 -0
  88. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -0
  89. package/dist/hooks/magic-context/lkg-replay.d.ts +16 -9
  90. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/lkg-slot.d.ts +141 -2
  92. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/opaque-thinking.d.ts +12 -0
  94. package/dist/hooks/magic-context/opaque-thinking.d.ts.map +1 -0
  95. package/dist/hooks/magic-context/pass-outcome.d.ts +1 -0
  96. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts +14 -0
  98. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -0
  99. package/dist/hooks/magic-context/pending-materialization.d.ts +10 -0
  100. package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -0
  101. package/dist/hooks/magic-context/postprocess-read-cache.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/provider-visible-parts.d.ts +51 -0
  103. package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -0
  104. package/dist/hooks/magic-context/read-session-chunk.d.ts +22 -1
  105. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/read-session-db.d.ts +3 -0
  107. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/read-session-formatting.d.ts +5 -0
  109. package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/read-session-raw.d.ts +123 -1
  111. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/render-options.d.ts +19 -0
  115. package/dist/hooks/magic-context/render-options.d.ts.map +1 -0
  116. package/dist/hooks/magic-context/sentinel.d.ts +41 -1
  117. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/strict-thinking-prefix.d.ts +8 -0
  119. package/dist/hooks/magic-context/strict-thinking-prefix.d.ts.map +1 -0
  120. package/dist/hooks/magic-context/strip-content.d.ts +38 -24
  121. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/supersession-reclaim.d.ts +5 -3
  123. package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/system-date-refresh.d.ts +29 -0
  125. package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -0
  126. package/dist/hooks/magic-context/system-prompt-dates.d.ts +6 -0
  127. package/dist/hooks/magic-context/system-prompt-dates.d.ts.map +1 -0
  128. package/dist/hooks/magic-context/system-prompt-delivery.d.ts +47 -0
  129. package/dist/hooks/magic-context/system-prompt-delivery.d.ts.map +1 -0
  130. package/dist/hooks/magic-context/system-prompt-hash.d.ts +18 -5
  131. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  132. package/dist/hooks/magic-context/tag-messages.d.ts +19 -0
  133. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  134. package/dist/hooks/magic-context/tag-owner-warmup.d.ts +14 -0
  135. package/dist/hooks/magic-context/tag-owner-warmup.d.ts.map +1 -0
  136. package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts +18 -0
  137. package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts.map +1 -0
  138. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -4
  139. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  140. package/dist/hooks/magic-context/token-count-exact.d.ts +35 -0
  141. package/dist/hooks/magic-context/token-count-exact.d.ts.map +1 -0
  142. package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
  143. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  144. package/dist/hooks/magic-context/tool-reclaim.d.ts +5 -2
  145. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
  146. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  147. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +9 -0
  148. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  149. package/dist/hooks/magic-context/transform.d.ts +21 -11
  150. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  151. package/dist/hooks/magic-context/unresolved-history-boundary.d.ts +5 -1
  152. package/dist/hooks/magic-context/unresolved-history-boundary.d.ts.map +1 -1
  153. package/dist/{index-g2z2k5m4.js → index-27ddyx39.js} +2 -2
  154. package/dist/{index-btp5tdc8.js → index-48e9znrc.js} +1248 -570
  155. package/dist/{index-5cep2r9n.js → index-5ty9rnbx.js} +60 -258
  156. package/dist/{index-z2g20msd.js → index-azbmnekv.js} +4 -4
  157. package/dist/{index-1j34a56j.js → index-dwh99z66.js} +293 -36
  158. package/dist/{index-qbkzdkk0.js → index-k59af64t.js} +6 -2
  159. package/dist/{index-5b7x80ec.js → index-kahkxcqw.js} +12532 -10348
  160. package/dist/{index-53g38ggt.js → index-kmbme1v1.js} +1 -1
  161. package/dist/{index-amycb7b1.js → index-r7s7d350.js} +3 -3
  162. package/dist/{index-33gprt9g.js → index-yjv8dpht.js} +1 -1
  163. package/dist/index-ykq24x1n.js +1477 -0
  164. package/dist/index.d.ts.map +1 -1
  165. package/dist/index.js +114 -2
  166. package/dist/{message-time-backfill-phrwp2fz.js → message-time-backfill-j96x1sab.js} +3 -3
  167. package/dist/model-resolution-cvyrrcjw.js +8 -0
  168. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  169. package/dist/{read-session-chunk-2xm4sc4y.js → read-session-chunk-t8bg6wkz.js} +6 -4
  170. package/dist/recomp-orchestrator-k760qy1n.js +21 -0
  171. package/dist/{safe-notification-target-ph3xd1ve.js → safe-notification-target-1jkf0m82.js} +2 -2
  172. package/dist/{send-session-notification-rnc4nrsr.js → send-session-notification-njjf3b4s.js} +5 -5
  173. package/dist/shared/auto-search-lifecycle.d.ts +14 -0
  174. package/dist/shared/auto-search-lifecycle.d.ts.map +1 -1
  175. package/dist/shared/logger.d.ts.map +1 -1
  176. package/dist/shared/model-suggestion-retry.d.ts +21 -0
  177. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  178. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  179. package/dist/shared/models-dev-temperature.d.ts +18 -0
  180. package/dist/shared/models-dev-temperature.d.ts.map +1 -0
  181. package/dist/shared/processed-image-eligibility.d.ts +22 -0
  182. package/dist/shared/processed-image-eligibility.d.ts.map +1 -0
  183. package/dist/shared/project-directory-key.d.ts +16 -0
  184. package/dist/shared/project-directory-key.d.ts.map +1 -1
  185. package/dist/shared/prompt-surface-runtime.d.ts +5 -0
  186. package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
  187. package/dist/shared/reclaim-distance.d.ts +33 -0
  188. package/dist/shared/reclaim-distance.d.ts.map +1 -0
  189. package/dist/shared/recovery-reasoning-prefix.d.ts +9 -0
  190. package/dist/shared/recovery-reasoning-prefix.d.ts.map +1 -0
  191. package/dist/shared/sqlite.d.ts +35 -1
  192. package/dist/shared/sqlite.d.ts.map +1 -1
  193. package/dist/shared/stable-json.d.ts +7 -0
  194. package/dist/shared/stable-json.d.ts.map +1 -1
  195. package/dist/shared/storage-file-mutex.d.ts +9 -0
  196. package/dist/shared/storage-file-mutex.d.ts.map +1 -0
  197. package/dist/shared/storage-permissions.d.ts +6 -0
  198. package/dist/shared/storage-permissions.d.ts.map +1 -1
  199. package/dist/shared/subagent-temperature.d.ts +46 -0
  200. package/dist/shared/subagent-temperature.d.ts.map +1 -0
  201. package/dist/shared/tag-owner-id.d.ts +1 -0
  202. package/dist/shared/tag-owner-id.d.ts.map +1 -1
  203. package/dist/shared/tag-transcript.d.ts +9 -0
  204. package/dist/shared/tag-transcript.d.ts.map +1 -1
  205. package/dist/shared/tail-hygiene-json.d.ts +22 -0
  206. package/dist/shared/tail-hygiene-json.d.ts.map +1 -0
  207. package/dist/shared/user-facing-codes.d.ts +2 -2
  208. package/dist/shared/window-geometry.d.ts +9 -66
  209. package/dist/shared/window-geometry.d.ts.map +1 -1
  210. package/dist/standalone-plugin-marker.d.ts +8 -0
  211. package/dist/standalone-plugin-marker.d.ts.map +1 -0
  212. package/dist/tools/ctx-expand/search-archive.d.ts +4 -0
  213. package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -0
  214. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  215. package/dist/tools/ctx-reduce/constants.d.ts +1 -2
  216. package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
  217. package/dist/tools/ctx-reduce/tools.d.ts +1 -1
  218. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  219. package/dist/v2/hidden-completion.d.ts +20 -1
  220. package/dist/v2/hidden-completion.d.ts.map +1 -1
  221. package/dist/v2/hooks/context.d.ts +1 -1
  222. package/dist/v2/hooks/context.d.ts.map +1 -1
  223. package/dist/v2/hooks/copied-tag-notation.d.ts +19 -0
  224. package/dist/v2/hooks/copied-tag-notation.d.ts.map +1 -0
  225. package/dist/v2/hooks/generate.d.ts +9 -2
  226. package/dist/v2/hooks/generate.d.ts.map +1 -1
  227. package/dist/v2/hooks/hidden-child.d.ts +22 -1
  228. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  229. package/dist/v2/hooks/payload.d.ts.map +1 -1
  230. package/dist/v2/hooks/system-prompt-response.d.ts +5 -0
  231. package/dist/v2/hooks/system-prompt-response.d.ts.map +1 -0
  232. package/dist/v2/server.d.ts.map +1 -1
  233. package/dist/v2/server.js +26593 -21940
  234. package/dist/v2/store-reader.d.ts +27 -1
  235. package/dist/v2/store-reader.d.ts.map +1 -1
  236. package/dist/web/main.js +401 -23
  237. package/dist/{wrapup-orchestrator-yknks3ad.js → wrapup-orchestrator-b2gswatj.js} +10 -10
  238. package/package.json +1 -1
  239. package/src/config/deprecated-config.ts +18 -0
  240. package/src/config/index.test.ts +13 -37
  241. package/src/config/index.ts +2 -2
  242. package/src/config/latch-permanence-guard.test.ts +4 -0
  243. package/src/config/project-security.test.ts +17 -0
  244. package/src/config/project-security.ts +3 -3
  245. package/src/config/schema/magic-context.test.ts +50 -0
  246. package/src/config/schema/magic-context.ts +44 -7
  247. package/src/shared/auto-search-lifecycle.ts +43 -0
  248. package/src/shared/logger.ts +10 -2
  249. package/src/shared/model-suggestion-retry.ts +5 -1
  250. package/src/shared/models-dev-cache.ts +1 -7
  251. package/src/shared/models-dev-temperature.ts +107 -0
  252. package/src/shared/processed-image-eligibility.test.ts +68 -0
  253. package/src/shared/processed-image-eligibility.ts +41 -0
  254. package/src/shared/processed-image-position-review.json +20 -0
  255. package/src/shared/processed-image-position-review.test.ts +19 -0
  256. package/src/shared/project-directory-key.ts +20 -0
  257. package/src/shared/prompt-surface-runtime.ts +12 -0
  258. package/src/shared/reclaim-distance.test.ts +111 -0
  259. package/src/shared/reclaim-distance.ts +91 -0
  260. package/src/shared/recovery-reasoning-prefix.test.ts +34 -0
  261. package/src/shared/recovery-reasoning-prefix.ts +35 -0
  262. package/src/shared/root-async-context.probe.mjs +36 -0
  263. package/src/shared/root-async-context.test.ts +49 -0
  264. package/src/shared/sqlite-acquisition-routing.test.ts +4 -3
  265. package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
  266. package/src/shared/sqlite-foreground-wait.test.ts +304 -0
  267. package/src/shared/sqlite-pass-scope.test.ts +4 -4
  268. package/src/shared/sqlite-writer-diagnostics.test.ts +52 -12
  269. package/src/shared/sqlite.ts +428 -46
  270. package/src/shared/stable-json.ts +79 -7
  271. package/src/shared/storage-file-mutex.test.ts +50 -0
  272. package/src/shared/storage-file-mutex.ts +130 -0
  273. package/src/shared/storage-permissions.ts +46 -0
  274. package/src/shared/subagent-temperature.ts +110 -0
  275. package/src/shared/tag-owner-id.ts +13 -0
  276. package/src/shared/tag-transcript.ts +118 -0
  277. package/src/shared/tail-hygiene-json.test.ts +117 -0
  278. package/src/shared/tail-hygiene-json.ts +217 -0
  279. package/src/shared/user-facing-codes.ts +2 -2
  280. package/src/shared/window-geometry.test.ts +102 -236
  281. package/src/shared/window-geometry.ts +23 -413
  282. package/dist/index-ramz5kkm.js +0 -514
  283. package/dist/model-resolution-wazv26n0.js +0 -6
  284. package/dist/recomp-orchestrator-frmwj9hz.js +0 -21
  285. package/src/shared/__fixtures__/window-overlay.v1.json +0 -337
@@ -7,11 +7,7 @@ import { resolveEpochFloorForPass } from "../features/magic-context/storage-meta
7
7
  import { resolveHistorianModel } from "../shared/model-resolution";
8
8
  import { Database } from "../shared/sqlite";
9
9
  import { createTestTempDir, createTestTempDirFromPath } from "../shared/test-temp-dir";
10
- import {
11
- getWindowOverlay,
12
- reloadWindowOverlay,
13
- setWindowOverlayPath,
14
- } from "../shared/window-geometry";
10
+ import { WINDOW_OVERLAY_PATH_DEPRECATION_WARNING } from "./deprecated-config";
15
11
  import { loadPluginConfig, loadPluginConfigDetailed } from "./index";
16
12
  import { resolveConfigProfile } from "./profiles";
17
13
  import { getProtectedTokensTierOverrides } from "./project-security";
@@ -156,39 +152,19 @@ function loadDetailedWithConfigs(userConfigText: string, projectConfigText?: str
156
152
  }
157
153
  }
158
154
 
159
- describe("loadPluginConfig — Fusiform overlay reload", () => {
160
- it("does not invalidate a same-path overlay during routine config reads", () => {
161
- const overlayDir = createTestTempDirFromPath(join(tmpdir(), "mc-config-overlay-"));
162
- const overlayPath = join(overlayDir, "window-overlay.json");
163
- const configText = JSON.stringify({ models: { window_overlay_path: overlayPath } });
164
- const writeOverlay = (modelId: string) =>
165
- writeFileSync(
166
- overlayPath,
167
- JSON.stringify({
168
- schema: "fusiform-window-overlay/v1",
169
- generated_at: "2026-09-01T00:00:00Z",
170
- minted_provider_ids: [],
171
- cells: [{ provider_id: "hunt-provider", model_id: modelId, facts: {} }],
172
- }),
173
- );
174
-
175
- try {
176
- writeOverlay("before-rewrite");
177
- loadWithUserConfig(configText);
178
- expect(getWindowOverlay()?.cells[0]?.model_id).toBe("before-rewrite");
179
-
180
- writeOverlay("after-rewrite");
181
- expect(getWindowOverlay()?.cells[0]?.model_id).toBe("before-rewrite");
182
-
183
- loadWithUserConfig(configText);
184
- expect(getWindowOverlay()?.cells[0]?.model_id).toBe("before-rewrite");
155
+ describe("loadPluginConfig — retired Fusiform window path", () => {
156
+ it("parses the old key but warns once that it is ignored", () => {
157
+ const path = "/tmp/retired-fusiform-window.json";
158
+ const config = loadWithUserConfig(
159
+ JSON.stringify({ models: { window_overlay_path: path } }),
160
+ );
185
161
 
186
- reloadWindowOverlay(overlayPath);
187
- expect(getWindowOverlay()?.cells[0]?.model_id).toBe("after-rewrite");
188
- } finally {
189
- setWindowOverlayPath(undefined);
190
- rmSync(overlayDir, { recursive: true, force: true });
191
- }
162
+ expect(config.models?.window_overlay_path).toBe(path);
163
+ const warnings = config.configWarnings?.filter((message) =>
164
+ message.includes("models.window_overlay_path"),
165
+ );
166
+ expect(warnings).toHaveLength(1);
167
+ expect(warnings?.[0]).toContain(WINDOW_OVERLAY_PATH_DEPRECATION_WARNING);
192
168
  });
193
169
  });
194
170
 
@@ -13,9 +13,9 @@ import {
13
13
  } from "../shared/jsonc-parser";
14
14
  import { setOutputReserveConfig } from "../shared/models-dev-cache";
15
15
  import type { PromptSurfaceConfig } from "../shared/prompt-surface";
16
- import { setWindowOverlayPath } from "../shared/window-geometry";
17
16
  import { migrateLegacyAgentEnabledInMemory } from "./agent-disable";
18
17
  import { mmcProjectConfigBasePath, opencodeUserConfigBasePath } from "./config-paths";
18
+ import { warnDeprecatedWindowOverlayPath } from "./deprecated-config";
19
19
  import {
20
20
  legacyHarnessBlockPaths,
21
21
  RemovedHistorianHarnessBlockError,
@@ -370,6 +370,7 @@ export function parsePluginConfig(
370
370
  preMigrationWarnings,
371
371
  );
372
372
  const migrated = migrateLegacyAgentEnabledInMemory(migratedExperimental, preMigrationWarnings);
373
+ warnDeprecatedWindowOverlayPath(migrated, preMigrationWarnings);
373
374
  const parsed = MagicContextConfigSchema.safeParse(migrated);
374
375
  const disabledHooks = Array.isArray(rawConfig.disabled_hooks)
375
376
  ? rawConfig.disabled_hooks.filter((value): value is string => typeof value === "string")
@@ -711,7 +712,6 @@ export function loadPluginConfigDetailed(
711
712
  if (profileResolution.activeProfile) config.profile = profileResolution.activeProfile;
712
713
  if (applyRuntimeGlobals) {
713
714
  setOutputReserveConfig(config.output_reserve);
714
- setWindowOverlayPath(config.models?.window_overlay_path);
715
715
  }
716
716
  const leafValidationWarnings = [...(config.configWarnings ?? [])];
717
717
  if (config.configWarnings?.length) {
@@ -153,6 +153,10 @@ const KNOWN_SLOTS: Record<string, KnownSlot> = {
153
153
  classification: "DIAGNOSTIC",
154
154
  reason: "Most-recent diagnostic: every open attempt clears or overwrites it.",
155
155
  },
156
+ "packages/pi-plugin/src/index.ts:loggedPiMaintenanceModelMissing": {
157
+ classification: "PUBLICATION",
158
+ reason: "Once-per-process log dedup only (plan 18): the unconfigured-maintenance diagnosis is logged once to avoid per-pass spam. It gates no behavior — buildPiMaintenanceOptions re-evaluates the model chain on every call.",
159
+ },
156
160
  "packages/plugin/src/shared/models-dev-cache.ts:apiCache": {
157
161
  classification: "PUBLICATION",
158
162
  reason: "Publishes last-known-good model metadata, not a failure verdict; refresh writes a later successful value.",
@@ -268,6 +268,23 @@ describe("stripUnsafeProjectConfigFields", () => {
268
268
  expect(warnings.some((w) => w.includes("maintenance"))).toBe(true);
269
269
  });
270
270
 
271
+ it("plan 18: strips a project-injected maintenance model chain (no model hijack)", () => {
272
+ const raw: Record<string, unknown> = {
273
+ maintenance: {
274
+ enabled: true,
275
+ model: "attacker/spender",
276
+ fallback_models: ["attacker/backup"],
277
+ },
278
+ };
279
+ const warnings = stripUnsafeProjectConfigFields(raw);
280
+
281
+ // The whole-group strip removes the model fields with everything
282
+ // else: a cloned repo cannot redirect the user's maintenance runs
283
+ // onto its own (possibly billable) models.
284
+ expect(raw.maintenance).toBeUndefined();
285
+ expect(warnings.some((w) => w.includes("maintenance"))).toBe(true);
286
+ });
287
+
271
288
  it("is a no-op for a clean project config", () => {
272
289
  const raw: Record<string, unknown> = { dreamer: { model: "x" }, memory: { enabled: true } };
273
290
  const warnings = stripUnsafeProjectConfigFields(raw);
@@ -301,8 +301,8 @@ function makeProjectThresholdWarning(field: string, reason: string): string {
301
301
  * - `debug_rpc` — a repo must not enable process heap capture or memory diagnostics.
302
302
  * - `allow_home_project` — only the user may opt a home-directory session
303
303
  * into a durable project identity.
304
- * - `output_reserve` / `models.window_overlay_path` — only the user may change
305
- * process-wide window geometry inputs.
304
+ * - `output_reserve` — only the user may change process-wide window geometry.
305
+ * The deprecated `models.window_overlay_path` field is ignored at every tier.
306
306
  * - `language`: a repo must not inject prompt text through a user preference.
307
307
  * - `maintenance` — budgets, cooldowns, and anomaly thresholds of the
308
308
  * automatic memory-maintenance task are user-level only; a cloned repo
@@ -398,7 +398,7 @@ export function stripUnsafeProjectConfigFields(projectRaw: Record<string, unknow
398
398
  if (isPlainObject(models) && "window_overlay_path" in models) {
399
399
  delete models.window_overlay_path;
400
400
  warnings.push(
401
- "Ignoring models.window_overlay_path from project config (security: only user-level config may select model geometry metadata).",
401
+ "Ignoring models.window_overlay_path from project config (deprecated; this setting no longer changes model window geometry).",
402
402
  );
403
403
  }
404
404
 
@@ -135,6 +135,7 @@ describe("MagicContextConfigSchema", () => {
135
135
  },
136
136
  maintenance: {
137
137
  enabled: true,
138
+ fallback_models: [],
138
139
  budgets: {
139
140
  inputTokens: 400_000,
140
141
  outputTokens: 32_000,
@@ -159,6 +160,55 @@ describe("MagicContextConfigSchema", () => {
159
160
  expect(result).toEqual(input);
160
161
  });
161
162
 
163
+ it("parses both maintenance model spellings and distinguishes absent from empty fallbacks", () => {
164
+ const stringForm = MagicContextConfigSchema.parse({
165
+ maintenance: { enabled: true, model: "openai/gpt-6-luna" },
166
+ }).maintenance;
167
+ expect(stringForm.model).toBe("openai/gpt-6-luna");
168
+ expect(stringForm.fallback_models).toBeUndefined();
169
+ expect(
170
+ MagicContextConfigSchema.parse({
171
+ maintenance: { fallback_models: [] },
172
+ }).maintenance.fallback_models,
173
+ ).toEqual([]);
174
+
175
+ const objectForm = MagicContextConfigSchema.parse({
176
+ maintenance: {
177
+ enabled: true,
178
+ model: { model: "xiaomi/mimo-v2.6-flash", variant: "high" },
179
+ fallback_models: ["openai/gpt-6-luna", { model: "p/l", thinking_level: "off" }],
180
+ },
181
+ }).maintenance;
182
+ // Object entries keep both harness qualifiers verbatim — the
183
+ // active harness reads its own, no cross-harness mapping.
184
+ expect(objectForm.model).toEqual({
185
+ model: "xiaomi/mimo-v2.6-flash",
186
+ variant: "high",
187
+ });
188
+ expect(objectForm.fallback_models).toEqual([
189
+ "openai/gpt-6-luna",
190
+ { model: "p/l", thinking_level: "off" },
191
+ ]);
192
+ });
193
+
194
+ it("plan 18: rejects blank maintenance model references at parse time (no silent executor flow)", () => {
195
+ expect(
196
+ MagicContextConfigSchema.safeParse({
197
+ maintenance: { enabled: true, model: " " },
198
+ }).success,
199
+ ).toBe(false);
200
+ expect(
201
+ MagicContextConfigSchema.safeParse({
202
+ maintenance: { enabled: true, model: { model: "" } },
203
+ }).success,
204
+ ).toBe(false);
205
+ expect(
206
+ MagicContextConfigSchema.safeParse({
207
+ maintenance: { enabled: true, fallback_models: ["ok/one", " "] },
208
+ }).success,
209
+ ).toBe(false);
210
+ });
211
+
162
212
  it("accepts a boolean storage permission policy and rejects non-booleans", () => {
163
213
  expect(
164
214
  MagicContextConfigSchema.parse({
@@ -135,6 +135,23 @@ const HistorianModelEntryObjectSchema = z
135
135
  export const HistorianModelEntrySchema = z.union([z.string(), HistorianModelEntryObjectSchema]);
136
136
  export type HistorianModelEntry = z.infer<typeof HistorianModelEntrySchema>;
137
137
 
138
+ /**
139
+ * A maintenance model entry (plan 18): the same string/object shape as a
140
+ * historian model entry, but blank references are REJECTED at parse time —
141
+ * a blank primary must surface as a config diagnostic, never silently flow
142
+ * into the hidden executor or get corrected to some other model.
143
+ */
144
+ const MaintenanceModelEntrySchema = HistorianModelEntrySchema.superRefine((entry, ctx) => {
145
+ const model = typeof entry === "string" ? entry : entry.model;
146
+ if (typeof model !== "string" || model.trim().length === 0) {
147
+ ctx.addIssue({
148
+ code: "custom",
149
+ message:
150
+ "maintenance model entries must be non-empty: a bare provider/model string or { model, variant?, thinking_level? }",
151
+ });
152
+ }
153
+ });
154
+
138
155
  /**
139
156
  * A profile may select models but must not alter execution policy. It uses the
140
157
  * same flat historian model-resolution fields; execution metadata is not
@@ -261,8 +278,8 @@ export interface MagicContextConfig {
261
278
  prompt_surface: PromptSurfaceConfig;
262
279
  /** User-only output-token reservation override. Zero disables reservation. */
263
280
  output_reserve?: number | { default: number; [modelKey: string]: number };
264
- /** User-only model metadata inputs. */
265
- models?: { window_overlay_path?: string };
281
+ /** Deprecated user-only window metadata path; parsed only so loaders can warn and ignore it. */
282
+ models?: { window_overlay_path?: unknown };
266
283
  /** TUI toast lifetime in milliseconds for Magic Context notifications. Default: 5000. */
267
284
  toast_duration_ms?: number;
268
285
  execute_threshold_percentage: number | { default: number; [modelKey: string]: number };
@@ -400,6 +417,13 @@ export interface MagicContextConfig {
400
417
  */
401
418
  maintenance: {
402
419
  enabled: boolean;
420
+ /**
421
+ * Primary maintenance model entry. When absent, inherit historian
422
+ * model configuration; never inherit the main-session model.
423
+ */
424
+ model?: HistorianModelEntry;
425
+ /** Ordered fallbacks; absent inherits historian fallbacks only with an inherited primary. */
426
+ fallback_models?: HistorianModelEntry[];
403
427
  /** Per-run four-dimensional budget (decision 9; provisional, calibrated in P3/P4). */
404
428
  budgets: {
405
429
  inputTokens: number;
@@ -487,12 +511,16 @@ export const MagicContextConfigSchema = z.object({
487
511
  ),
488
512
  models: z
489
513
  .object({
490
- window_overlay_path: z.string().trim().min(1).optional(),
514
+ window_overlay_path: z
515
+ .unknown()
516
+ .optional()
517
+ .describe(
518
+ "Deprecated: ignored. Fusiform no longer publishes window-overlay data, so this setting no longer changes model window geometry. Remove this key.",
519
+ )
520
+ .meta({ deprecated: true }),
491
521
  })
492
522
  .optional()
493
- .describe(
494
- "User-only Fusiform window-overlay settings. The path defaults to <dataDir>/fusiform/window-overlay.json.",
495
- ),
523
+ .describe("Deprecated user-only Fusiform window-overlay settings; ignored."),
496
524
  toast_duration_ms: z
497
525
  .number()
498
526
  .min(0)
@@ -796,6 +824,15 @@ export const MagicContextConfigSchema = z.object({
796
824
  .describe(
797
825
  "Enable the automatic memory-maintenance task (default: true). USER-LEVEL ONLY — the whole maintenance group is stripped from project-tier config.",
798
826
  ),
827
+ model: MaintenanceModelEntrySchema.optional().describe(
828
+ "Primary maintenance model entry: a bare provider/model string or { model, variant? (OpenCode), thinking_level? (Pi) }. USER-LEVEL ONLY. When absent, inherit historian model configuration. If neither has a primary, automatic passes skip and manual runs refuse. The main-session model is never inherited.",
829
+ ),
830
+ fallback_models: z
831
+ .array(MaintenanceModelEntrySchema)
832
+ .optional()
833
+ .describe(
834
+ "Ordered fallback maintenance model entries, tried after CALL failures only. When absent and the primary is inherited, inherit historian fallbacks; explicit [] disables fallbacks. Illegal manifests, permission refusals, and exhausted budgets never switch models.",
835
+ ),
799
836
  budgets: z
800
837
  .object({
801
838
  inputTokens: z
@@ -918,7 +955,7 @@ export const MagicContextConfigSchema = z.object({
918
955
  anomalyThreshold: { absoluteRows: 10, poolFraction: 0.25 },
919
956
  })
920
957
  .describe(
921
- "Automatic memory-maintenance run controls (dreamer replacement). USER-LEVEL ONLY: the whole group is stripped from project-tier config so a cloned repository cannot raise budgets, relax cooldowns, or loosen the anomaly guard.",
958
+ "Automatic memory-maintenance run controls (dreamer replacement). USER-LEVEL ONLY: project-tier config cannot pick maintenance models or change run controls. An absent model inherits historian model configuration, never the session model.",
922
959
  ),
923
960
  });
924
961
 
@@ -1,4 +1,47 @@
1
+ import type { AutoSearchHintNoHintReason } from "../features/magic-context/storage-meta-persisted";
2
+
1
3
  const pending = new Map<string, Set<AbortController>>();
4
+ const unpublishedNoHints = new Map<string, Map<string, AutoSearchHintNoHintReason>>();
5
+
6
+ /**
7
+ * A failed save can still send the user message without a search hint. Remember
8
+ * that no-hint decision until a later pass saves it to SQLite, including across
9
+ * closing and reopening the database handle. Switching or shutting down a session
10
+ * does not prove deletion and must not discard it. If all compare-and-swap (CAS)
11
+ * save retries fail and the process restarts before repair, this unsaved record
12
+ * is lost: that compound failure is the remaining durability gap.
13
+ */
14
+ export function rememberUnpublishedAutoSearchNoHint(
15
+ sessionId: string,
16
+ messageId: string,
17
+ reason: AutoSearchHintNoHintReason,
18
+ ): void {
19
+ let messages = unpublishedNoHints.get(sessionId);
20
+ if (!messages) {
21
+ messages = new Map();
22
+ unpublishedNoHints.set(sessionId, messages);
23
+ }
24
+ if (!messages.has(messageId)) messages.set(messageId, reason);
25
+ }
26
+
27
+ export function getUnpublishedAutoSearchNoHint(
28
+ sessionId: string,
29
+ messageId: string,
30
+ ): AutoSearchHintNoHintReason | undefined {
31
+ return unpublishedNoHints.get(sessionId)?.get(messageId);
32
+ }
33
+
34
+ export function forgetUnpublishedAutoSearchNoHint(sessionId: string, messageId: string): void {
35
+ const messages = unpublishedNoHints.get(sessionId);
36
+ messages?.delete(messageId);
37
+ if (messages?.size === 0) unpublishedNoHints.delete(sessionId);
38
+ }
39
+
40
+ /** Deleting a session releases its unsaved no-hint records; switching away does not. */
41
+ export function clearUnpublishedAutoSearchNoHints(sessionId?: string): void {
42
+ if (sessionId === undefined) unpublishedNoHints.clear();
43
+ else unpublishedNoHints.delete(sessionId);
44
+ }
2
45
 
3
46
  /** Cleanup cancels optional searches and backfill, not durable served decisions. */
4
47
  export function registerAutoSearchWork(sessionId: string): {
@@ -162,8 +162,16 @@ function flush(): void {
162
162
  let currentSize = getCurrentLogSize(logFile);
163
163
  const dataSize = Buffer.byteLength(data);
164
164
  if (currentSize > 0 && currentSize + dataSize > MAX_LOG_FILE_BYTES) {
165
- rotateLogFile(logFile, currentSize);
166
- currentSize = 0;
165
+ // Several processes share this file and each caches its size. A stale
166
+ // cache must not rotate a file another process already rotated: that
167
+ // second rotation would overwrite the full predecessor with the few
168
+ // lines written since. Decide on the file's real size instead.
169
+ flushesSinceSizeCheck = SIZE_CHECK_INTERVAL_FLUSHES;
170
+ currentSize = getCurrentLogSize(logFile);
171
+ if (currentSize > 0 && currentSize + dataSize > MAX_LOG_FILE_BYTES) {
172
+ rotateLogFile(logFile, currentSize);
173
+ currentSize = 0;
174
+ }
167
175
  }
168
176
  fs.appendFileSync(logFile, data, { encoding: "utf8", mode: 0o600 });
169
177
  buffer = [];
@@ -361,8 +361,12 @@ async function abortChildRun(client: Client, sessionId: string): Promise<void> {
361
361
  * Everything else (auth errors, ProviderModelNotFoundError without suggestion,
362
362
  * rate limits, transient network failures, etc.) is considered retryable on a
363
363
  * different model.
364
+ * Exported as the shared fallback-switch classifier (plan 18): the
365
+ * memory-maintenance attempt loop reuses this exact predicate so "which
366
+ * failures may switch models" has ONE definition across historian and
367
+ * maintainer call sites.
364
368
  */
365
- function isNonRetryable(error: unknown, externalSignal?: AbortSignal): boolean {
369
+ export function isNonRetryable(error: unknown, externalSignal?: AbortSignal): boolean {
366
370
  if (
367
371
  externalSignal?.aborted ||
368
372
  (error instanceof HiddenCompletionRefusal && error.terminal) ||
@@ -33,12 +33,7 @@ import { getHarness } from "./harness";
33
33
  import { modelRefLookupOrder } from "./harness-provider-map";
34
34
  import { sessionLog } from "./logger";
35
35
  import { writeStorageFileAtomicSync } from "./storage-permissions";
36
- import {
37
- deriveWindowGeometry,
38
- getWindowOverlay,
39
- resolveWindowOverlayFacts,
40
- type WindowGeometryResult,
41
- } from "./window-geometry";
36
+ import { deriveWindowGeometry, type WindowGeometryResult } from "./window-geometry";
42
37
 
43
38
  interface OpencodeClientLike {
44
39
  config: {
@@ -515,7 +510,6 @@ export function getSdkWindowGeometry(
515
510
  output: metadata.outputLimit,
516
511
  },
517
512
  {
518
- overlay: resolveWindowOverlayFacts(providerID, modelID, getWindowOverlay()),
519
513
  outputReserveOverride: resolveOutputReserve(providerID, modelID),
520
514
  harness: options?.harness ?? "opencode",
521
515
  contextCap:
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Temperature capability from the models.dev catalog that OpenCode caches on disk.
3
+ *
4
+ * OpenCode 1 copies models.dev's `temperature` flag onto each model, but OpenCode 2's
5
+ * model rows, Pi's model registry and Oh My Pi's Gemini models carry no equivalent
6
+ * field. For those hosts the cached catalog is the only positive evidence on the
7
+ * machine, so a missing file, an unparsable file or a missing entry all mean "unknown"
8
+ * and the caller omits temperature.
9
+ *
10
+ * This module is bundled into the Pi/OMP historian child extension, so it must only
11
+ * depend on Node built-ins.
12
+ */
13
+ import { readFileSync, statSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ export interface CatalogTemperature {
18
+ temperature: boolean;
19
+ /** The catalog entry that decided, as `provider/model`. */
20
+ entry: string;
21
+ }
22
+
23
+ /**
24
+ * Harness provider ids that name the same models.dev provider. Codex (`openai-codex`)
25
+ * is deliberately absent: the ChatGPT backend is a different transport from the
26
+ * OpenAI API that models.dev describes, so it gets no catalog answer and no temperature.
27
+ */
28
+ const CATALOG_PROVIDER_ALIASES: Readonly<Record<string, string>> = {
29
+ "google-antigravity": "google",
30
+ "opencode-zen": "opencode",
31
+ };
32
+
33
+ /** The Antigravity auth plugins register Gemini models as `google/antigravity-<model>`. */
34
+ const ANTIGRAVITY_MODEL_PREFIX = "antigravity-";
35
+
36
+ type CatalogIndex = Map<string, boolean>;
37
+
38
+ let cached: { path: string; mtimeMs: number; size: number; index: CatalogIndex } | undefined;
39
+
40
+ /** OpenCode reads `OPENCODE_MODELS_PATH` instead of its cache when it is set. */
41
+ export function modelsDevCatalogPath(env: NodeJS.ProcessEnv = process.env): string {
42
+ if (env.OPENCODE_MODELS_PATH) return env.OPENCODE_MODELS_PATH;
43
+ return join(env.XDG_CACHE_HOME || join(homedir(), ".cache"), "opencode", "models.json");
44
+ }
45
+
46
+ function buildIndex(raw: unknown): CatalogIndex {
47
+ const index: CatalogIndex = new Map();
48
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return index;
49
+ for (const [providerID, provider] of Object.entries(raw as Record<string, unknown>)) {
50
+ const models = (provider as { models?: unknown } | null)?.models;
51
+ if (!models || typeof models !== "object") continue;
52
+ for (const [modelID, model] of Object.entries(models as Record<string, unknown>)) {
53
+ const temperature = (model as { temperature?: unknown } | null)?.temperature;
54
+ if (typeof temperature === "boolean")
55
+ index.set(`${providerID}/${modelID}`, temperature);
56
+ }
57
+ }
58
+ return index;
59
+ }
60
+
61
+ function loadIndex(): CatalogIndex | undefined {
62
+ const path = modelsDevCatalogPath();
63
+ try {
64
+ const stat = statSync(path);
65
+ if (cached?.path === path && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size)
66
+ return cached.index;
67
+ const index = buildIndex(JSON.parse(readFileSync(path, "utf8")));
68
+ cached = { path, mtimeMs: stat.mtimeMs, size: stat.size, index };
69
+ return index;
70
+ } catch {
71
+ // Absent or partially written catalog: no evidence either way.
72
+ return undefined;
73
+ }
74
+ }
75
+
76
+ /** Spellings to try for a harness `provider/model` reference, most specific first. */
77
+ export function catalogCandidates(providerID: string, modelID: string): string[] {
78
+ const provider = Object.hasOwn(CATALOG_PROVIDER_ALIASES, providerID)
79
+ ? CATALOG_PROVIDER_ALIASES[providerID]
80
+ : providerID;
81
+ const candidates = [`${provider}/${modelID}`];
82
+ if (provider === "google" && modelID.startsWith(ANTIGRAVITY_MODEL_PREFIX))
83
+ candidates.push(`google/${modelID.slice(ANTIGRAVITY_MODEL_PREFIX.length)}`);
84
+ return candidates;
85
+ }
86
+
87
+ /**
88
+ * Look `provider/model` up in the cached catalogue, trying only the exact spellings from
89
+ * `catalogCandidates`. A malformed reference, a missing catalogue or no entry returns undefined.
90
+ */
91
+ export function catalogTemperature(model: string): CatalogTemperature | undefined {
92
+ const slash = model.indexOf("/");
93
+ if (slash <= 0 || slash === model.length - 1) return undefined;
94
+ const index = loadIndex();
95
+ if (!index) return undefined;
96
+ for (const entry of catalogCandidates(model.slice(0, slash), model.slice(slash + 1))) {
97
+ const temperature = index.get(entry);
98
+ if (temperature !== undefined) return { temperature, entry };
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ export const __catalogTest = {
104
+ reset(): void {
105
+ cached = undefined;
106
+ },
107
+ };
@@ -0,0 +1,68 @@
1
+ import { expect, test } from "bun:test";
2
+ import { createProcessedImageEligibility } from "./processed-image-eligibility";
3
+
4
+ const messages = ["old", "image", "answer", "tail"];
5
+
6
+ test("image position age inherits its next positive tag, never zero or an earlier tag", () => {
7
+ const policy = createProcessedImageEligibility(
8
+ messages,
9
+ new Map([
10
+ ["old", 1],
11
+ ["answer", 6],
12
+ ]),
13
+ { watermark: 1 },
14
+ );
15
+ expect(policy.ageTags.get("image")).toBe(6);
16
+ expect(policy.mayStrip("image")).toBe(false);
17
+ expect(policy.ageTags.has("tail")).toBe(false);
18
+ expect(policy.mayStrip("tail")).toBe(false);
19
+ const own = createProcessedImageEligibility(
20
+ messages,
21
+ new Map([
22
+ ["image", 3],
23
+ ["answer", 6],
24
+ ]),
25
+ { watermark: 4 },
26
+ );
27
+ expect(own.ageTags.get("image")).toBe(3);
28
+ expect(own.mayStrip("image")).toBe(true);
29
+ });
30
+
31
+ test("answered image-only positions outside the protected window are eligible", () => {
32
+ const policy = createProcessedImageEligibility(
33
+ messages,
34
+ new Map([
35
+ ["answer", 3],
36
+ ["tail", 5],
37
+ ]),
38
+ { watermark: 4, protectedCutoff: 5, contextPercentage: 94.99 },
39
+ );
40
+ expect(policy.ageTags.get("image")).toBe(3);
41
+ expect(policy.mayStrip("image")).toBe(true);
42
+ });
43
+
44
+ test("image positions inside the protected window stay below the 95 percent emergency", () => {
45
+ const policy = createProcessedImageEligibility(messages, new Map([["answer", 6]]), {
46
+ watermark: 10,
47
+ protectedCutoff: 5,
48
+ contextPercentage: 94.99,
49
+ });
50
+ expect(policy.mayStrip("image")).toBe(false);
51
+ });
52
+
53
+ test("image positions follow emergency drops when the newest window yields at 95 percent", () => {
54
+ for (const contextPercentage of [95, 99]) {
55
+ const policy = createProcessedImageEligibility(messages, new Map([["answer", 6]]), {
56
+ watermark: 10,
57
+ protectedCutoff: 5,
58
+ contextPercentage,
59
+ });
60
+ expect(policy.mayStrip("image")).toBe(true);
61
+ const notDroppedYet = createProcessedImageEligibility(messages, new Map([["answer", 11]]), {
62
+ watermark: 10,
63
+ protectedCutoff: 5,
64
+ contextPercentage,
65
+ });
66
+ expect(notDroppedYet.mayStrip("image")).toBe(false);
67
+ }
68
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared processed-image age and recency policy for both OpenCode adapters.
3
+ * Use the owning message's positive maximum tag; an untagged image-only message
4
+ * inherits the next positive tagged message after it, never age zero or a tag
5
+ * from an earlier message. Without either tag, retain the image at the live tail.
6
+ * A positive dropped-tag watermark must cover that position. Below 95% usage,
7
+ * retain every position at or beyond the protected newest-window cutoff. At
8
+ * 95% and above only that window yields, exactly as for emergency tool drops;
9
+ * independent per-tool protections still apply. Content before retained provider-signed
10
+ * thinking must stay unchanged unless an existing whole-turn rewrite removes that
11
+ * thinking first. Callers enforce these protections, later-assistant/stable-id checks, durable
12
+ * persistence and rewrite-only discovery. Frozen replay bypasses eligibility.
13
+ */
14
+ export function createProcessedImageEligibility<T>(
15
+ messages: readonly T[],
16
+ messageTagNumbers: ReadonlyMap<T, number>,
17
+ options: { watermark: number; protectedCutoff?: number | null; contextPercentage?: number },
18
+ ): { ageTags: ReadonlyMap<T, number>; mayStrip: (message: T) => boolean } {
19
+ const ageTags = new Map<T, number>();
20
+ let nextTag: number | undefined;
21
+ for (let index = messages.length - 1; index >= 0; index--) {
22
+ const message = messages[index];
23
+ const ownTag = messageTagNumbers.get(message);
24
+ if (ownTag !== undefined && Number.isFinite(ownTag) && ownTag > 0) nextTag = ownTag;
25
+ if (nextTag !== undefined) ageTags.set(message, nextTag);
26
+ }
27
+ return {
28
+ ageTags,
29
+ mayStrip: (message) => {
30
+ const age = ageTags.get(message);
31
+ return (
32
+ options.watermark > 0 &&
33
+ age !== undefined &&
34
+ age <= options.watermark &&
35
+ ((options.contextPercentage ?? 0) >= 95 ||
36
+ options.protectedCutoff == null ||
37
+ age < options.protectedCutoff)
38
+ );
39
+ },
40
+ };
41
+ }