mixdog 1.1.1 → 1.2.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 (317) hide show
  1. package/README.md +1 -0
  2. package/package.json +2 -2
  3. package/scripts/lib/run-node-tests.mjs +9 -0
  4. package/scripts/prune-desktop-runtime.mjs +3 -0
  5. package/scripts/release-paths.mjs +39 -2
  6. package/scripts/test-direct.mjs +4 -2
  7. package/src/defaults/shipped-history.json +6 -0
  8. package/src/defaults/skills/setup/references/actions.md +5 -2
  9. package/src/defaults/skills/visualize/SKILL.md +157 -0
  10. package/src/defaults/skills/visualize/references/craft.md +88 -0
  11. package/src/headless-command.mjs +56 -3
  12. package/src/headless-exec.mjs +17 -1
  13. package/src/headless-role.mjs +2 -2
  14. package/src/help.mjs +2 -0
  15. package/src/lib/rules-builder.cjs +0 -23
  16. package/src/rules/lead/LEAD.md +2 -2
  17. package/src/rules/shared/70-delivery.md +2 -2
  18. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
  19. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  21. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  22. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
  23. package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
  24. package/src/runtime/agent/orchestrator/config.mjs +9 -1
  25. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
  26. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
  27. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
  28. package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
  29. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
  34. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
  35. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
  37. package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
  38. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  40. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
  41. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
  43. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
  44. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
  45. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
  46. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
  47. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
  48. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
  49. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  51. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
  53. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
  57. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
  58. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
  59. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +11 -8
  61. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
  62. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
  63. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
  64. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
  65. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
  66. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
  67. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
  68. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
  69. package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
  70. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
  72. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
  74. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
  75. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
  76. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
  82. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
  83. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
  84. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
  89. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  91. package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
  92. package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
  93. package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
  94. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
  95. package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
  96. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
  97. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
  98. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
  99. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
  100. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
  101. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
  102. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
  103. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
  104. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
  106. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
  107. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
  111. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
  112. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
  113. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
  114. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
  115. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
  117. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  118. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
  119. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
  121. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
  123. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
  124. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
  125. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
  126. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
  127. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +0 -10
  128. package/src/runtime/attachments/store.mjs +38 -0
  129. package/src/runtime/channels/index.mjs +1 -5
  130. package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
  131. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
  132. package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
  133. package/src/runtime/channels/lib/worker-main.mjs +4 -34
  134. package/src/runtime/shared/atomic-file.mjs +8 -2
  135. package/src/runtime/shared/best-effort-stderr.mjs +8 -0
  136. package/src/runtime/shared/code-graph-state.mjs +12 -0
  137. package/src/runtime/shared/defaults-separation.mjs +2 -0
  138. package/src/runtime/shared/git-runner-worker.mjs +3 -2
  139. package/src/runtime/shared/git-runner.mjs +4 -3
  140. package/src/runtime/shared/llm/default-context-window.mjs +6 -0
  141. package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
  142. package/src/runtime/shared/llm/model-catalog.mjs +240 -118
  143. package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
  144. package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
  145. package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
  146. package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
  147. package/src/runtime/shared/model-proxy-mode.mjs +19 -0
  148. package/src/runtime/shared/provider-accounts.mjs +2 -2
  149. package/src/runtime/shared/provider-api-key.mjs +21 -1
  150. package/src/runtime/shared/schedule-model-ref.mjs +11 -2
  151. package/src/runtime/shared/semver.d.mts +1 -0
  152. package/src/runtime/shared/semver.mjs +57 -0
  153. package/src/runtime/shared/session-usage-totals.mjs +19 -0
  154. package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
  155. package/src/runtime/shared/singleton-owner.mjs +36 -6
  156. package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
  157. package/src/runtime/shared/tool-surface.mjs +9 -2
  158. package/src/runtime/shared/tool-work-units.mjs +14 -0
  159. package/src/runtime/shared/transcript-metadata.mjs +17 -1
  160. package/src/runtime/shared/turn-changes.mjs +791 -0
  161. package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
  162. package/src/runtime/shared/turn-snapshot.mjs +690 -81
  163. package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
  164. package/src/runtime/shared/update-checker.mjs +2 -31
  165. package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
  166. package/src/runtime/shared/worktrees/contract.mjs +551 -0
  167. package/src/runtime/shared/worktrees/disk.mjs +57 -0
  168. package/src/runtime/shared/worktrees/git.mjs +247 -0
  169. package/src/runtime/shared/worktrees/include.mjs +480 -0
  170. package/src/runtime/shared/worktrees/lock.mjs +46 -0
  171. package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
  172. package/src/runtime/shared/worktrees/manager.mjs +1215 -0
  173. package/src/runtime/shared/worktrees/merge.mjs +182 -0
  174. package/src/runtime/shared/worktrees/owner.mjs +46 -0
  175. package/src/runtime/shared/worktrees/paths.mjs +62 -0
  176. package/src/runtime/shared/worktrees/registry.mjs +110 -0
  177. package/src/runtime/shared/worktrees/settings.mjs +84 -0
  178. package/src/runtime/shared/worktrees/slug.mjs +62 -0
  179. package/src/runtime/shared/worktrees/test-support.mjs +96 -0
  180. package/src/runtime/visualize-bridge/client.mjs +181 -0
  181. package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
  182. package/src/session-runtime/boot/apis.mjs +35 -0
  183. package/src/session-runtime/boot/begin.mjs +0 -2
  184. package/src/session-runtime/boot/config.mjs +2 -0
  185. package/src/session-runtime/boot/facade.mjs +20 -0
  186. package/src/session-runtime/boot/infrastructure.mjs +1 -0
  187. package/src/session-runtime/boot/session.mjs +1 -0
  188. package/src/session-runtime/boot/tools.mjs +9 -4
  189. package/src/session-runtime/config-lifecycle.mjs +10 -1
  190. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
  191. package/src/session-runtime/global-extensions.mjs +2 -2
  192. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
  193. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
  194. package/src/session-runtime/internal-tool-executor.mjs +9 -1
  195. package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
  196. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
  197. package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
  198. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
  199. package/src/session-runtime/mcp-glue/connect.mjs +11 -6
  200. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
  201. package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
  202. package/src/session-runtime/model-route/route-persist.mjs +3 -0
  203. package/src/session-runtime/model-route/set-route.mjs +15 -2
  204. package/src/session-runtime/notification-bus.mjs +9 -1
  205. package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
  206. package/src/session-runtime/resource-extension-sync.mjs +30 -7
  207. package/src/session-runtime/resource-mcp-api.mjs +27 -10
  208. package/src/session-runtime/route-resolution.mjs +10 -1
  209. package/src/session-runtime/route-state.mjs +16 -2
  210. package/src/session-runtime/runtime-bootstrap.mjs +0 -4
  211. package/src/session-runtime/runtime-core.mjs +5 -0
  212. package/src/session-runtime/runtime-facade.mjs +8 -0
  213. package/src/session-runtime/runtime-paths.mjs +0 -1
  214. package/src/session-runtime/runtime-review-api.mjs +9 -0
  215. package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
  216. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
  217. package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
  218. package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
  219. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
  220. package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
  221. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
  222. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
  223. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
  224. package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
  225. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
  226. package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
  227. package/src/session-runtime/services/agent-tool.mjs +1 -0
  228. package/src/session-runtime/services/channel-worker.mjs +1 -1
  229. package/src/session-runtime/services/daemon-candidate.mjs +3 -2
  230. package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
  231. package/src/session-runtime/session-create/session-options.mjs +10 -1
  232. package/src/session-runtime/session-turn-api.mjs +1 -0
  233. package/src/session-runtime/settings-api.mjs +4 -2
  234. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
  235. package/src/session-runtime/settings-profile-api.mjs +27 -2
  236. package/src/session-runtime/settings-worktree-api.mjs +17 -0
  237. package/src/session-runtime/setup-tool/executor.mjs +11 -4
  238. package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
  239. package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
  240. package/src/session-runtime/starter-guide-state.mjs +62 -0
  241. package/src/session-runtime/turn/agent-ops.mjs +12 -1
  242. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
  243. package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
  244. package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
  245. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
  246. package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
  247. package/src/session-runtime/turn/turn-run.mjs +6 -7
  248. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
  249. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
  250. package/src/session-runtime/workspace/admission-gate.mjs +197 -0
  251. package/src/session-runtime/workspace/project-branches.mjs +136 -0
  252. package/src/session-runtime/workspace/transition-steps.mjs +364 -0
  253. package/src/session-runtime/workspace/transitions.mjs +651 -0
  254. package/src/session-runtime/workspace/workspace-state.mjs +112 -0
  255. package/src/standalone/channel-binding.mjs +1 -2
  256. package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
  257. package/src/standalone/channel-transport/http-routes.mjs +17 -0
  258. package/src/standalone/channel-transport.mjs +13 -1
  259. package/src/standalone/daemon-channels-loader.mjs +2 -3
  260. package/src/standalone/daemon-desktop-runtime.mjs +1 -0
  261. package/src/standalone/daemon-owner.mjs +372 -0
  262. package/src/standalone/daemon-stored-session-views.mjs +4 -1
  263. package/src/standalone/daemon.mjs +139 -5
  264. package/src/standalone/desktop-service-registry.mjs +20 -0
  265. package/src/standalone/local-session-runtime.mjs +13 -78
  266. package/src/standalone/session-client/attachment-calls.mjs +3 -1
  267. package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
  268. package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
  269. package/src/standalone/session-client.mjs +104 -13
  270. package/src/standalone/session-protocol.mjs +17 -0
  271. package/src/standalone/session-proxy.mjs +3 -0
  272. package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
  273. package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
  274. package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
  275. package/src/standalone/session-service/projection/revisions.mjs +2 -0
  276. package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
  277. package/src/standalone/session-service/projection/workspace.mjs +14 -0
  278. package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
  279. package/src/standalone/session-transport/call-route.mjs +13 -0
  280. package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
  281. package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
  282. package/src/standalone/session-transport/client-registry.mjs +20 -3
  283. package/src/standalone/session-transport/routes.mjs +79 -2
  284. package/src/standalone/session-transport/stack.mjs +2 -0
  285. package/src/standalone/session-transport.mjs +24 -1
  286. package/src/tui/app/message-selector.mjs +2 -1
  287. package/src/tui/app/model-options.mjs +35 -5
  288. package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
  289. package/src/tui/app/model-picker.mjs +4 -2
  290. package/src/tui/dist/index.mjs +332 -36
  291. package/src/tui/index.jsx +10 -2
  292. package/src/tui/session/compaction-view.mjs +147 -0
  293. package/src/tui/session/context-state.mjs +6 -0
  294. package/src/tui/session/initial-state.mjs +3 -0
  295. package/src/tui/session/quota-resume.mjs +2 -1
  296. package/src/tui/session/session-api/commands.mjs +10 -2
  297. package/src/tui/session/session-api/controls.mjs +4 -0
  298. package/src/tui/session/session-api/extensions.mjs +12 -8
  299. package/src/tui/session/session-api/intake/rewind.mjs +2 -1
  300. package/src/tui/session/session-api/integrations.mjs +18 -0
  301. package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
  302. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
  303. package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
  304. package/src/tui/session/session-api/routes.mjs +10 -1
  305. package/src/tui/session/session-api/settings.mjs +2 -0
  306. package/src/tui/session/session-api/shared.mjs +10 -6
  307. package/src/tui/session/session-api/worktree.mjs +60 -0
  308. package/src/tui/session/session-api.mjs +2 -0
  309. package/src/tui/session/session-flow/auto-clear.mjs +31 -12
  310. package/src/tui/session/session-flow/drain.mjs +15 -1
  311. package/src/tui/session/session-flow/session-reset.mjs +4 -3
  312. package/src/tui/session/session-flow.mjs +2 -0
  313. package/src/tui/session/transcript-intake.mjs +14 -5
  314. package/src/tui/session/turn-stream.mjs +32 -4
  315. package/src/tui/session/turn-tool-cards.mjs +10 -2
  316. package/src/tui/session/turn.mjs +39 -10
  317. package/src/runtime/channels/tool-defs.mjs +0 -17
@@ -3,6 +3,7 @@
3
3
  import { clean, hasOwn } from './session-text.mjs';
4
4
  import { normalizeEffortInput, normalizeSavedEffort } from './effort.mjs';
5
5
  import { routeFastKey, fastPreferenceFor } from './model-capabilities.mjs';
6
+ import { normalizeContextTokens } from '../../../shared/llm/default-context-window.mjs';
6
7
 
7
8
  const DEFAULT_PROVIDER = 'anthropic-oauth';
8
9
  export const DEFAULT_MODEL = '';
@@ -53,6 +54,14 @@ function cleanContextPercent(value) {
53
54
  return Math.max(10, Math.min(100, Math.round(percent / 10) * 10));
54
55
  }
55
56
 
57
+ /** The route's exact window in tokens: an explicit request, else none when a
58
+ * percent is chosen instead, else the saved or preset selection. */
59
+ function resolveContextTokens({ contextTokens, hasExplicitContextPercent, saved, preset = null }) {
60
+ if (contextTokens !== undefined) return normalizeContextTokens(contextTokens);
61
+ if (hasExplicitContextPercent) return undefined;
62
+ return normalizeContextTokens(saved.contextTokens ?? preset?.contextTokens);
63
+ }
64
+
56
65
  /** The route's fast flag: an explicit request, else the saved choice, else the preset or the provider preference. */
57
66
  function resolveFast({ hasExplicitFast, explicitFast, saved, presetFast = false, preference }) {
58
67
  if (hasExplicitFast) return explicitFast;
@@ -60,8 +69,19 @@ function resolveFast({ hasExplicitFast, explicitFast, saved, presetFast = false,
60
69
  return presetFast || preference();
61
70
  }
62
71
 
72
+ /** The route's Auto effort flag: an explicit request, else the saved choice,
73
+ * else the preset. Absent means on; only `false` turns it off. */
74
+ function resolveAutoEffort({ autoEffort, saved, preset = null }) {
75
+ if (autoEffort !== undefined) return autoEffort !== false;
76
+ if (hasOwn(saved, 'autoEffort')) return saved.autoEffort !== false;
77
+ return preset?.autoEffort !== false;
78
+ }
79
+
63
80
  export function makeResolveRoute(resolveDefaultProvider) {
64
- return function resolveRoute(config, { provider, model, effort, fast, modelParameters, contextPercent } = {}) {
81
+ return function resolveRoute(
82
+ config,
83
+ { provider, model, effort, fast, autoEffort, modelParameters, contextPercent, contextTokens } = {}
84
+ ) {
65
85
  const explicitProvider = clean(provider);
66
86
  const explicitModel = clean(model);
67
87
  const hasExplicitEffort = effort !== undefined;
@@ -70,6 +90,9 @@ export function makeResolveRoute(resolveDefaultProvider) {
70
90
  const explicitFast = fast === true;
71
91
  const hasExplicitModelParameters = modelParameters !== undefined;
72
92
  const hasExplicitContextPercent = contextPercent !== undefined;
93
+ // An exact token selection replaces a saved percent unless a percent is
94
+ // requested alongside it.
95
+ const keepsSavedPercent = contextTokens === undefined;
73
96
 
74
97
  if (!explicitProvider) {
75
98
  const preset = findPreset(config, explicitModel || config?.default);
@@ -78,6 +101,7 @@ export function makeResolveRoute(resolveDefaultProvider) {
78
101
  const m = clean(preset.model) || DEFAULT_MODEL;
79
102
  if (p && m) {
80
103
  const saved = modelSettingsFor(config, p, m);
104
+ const tokens = resolveContextTokens({ contextTokens, hasExplicitContextPercent, saved, preset });
81
105
  return {
82
106
  provider: p,
83
107
  model: m,
@@ -90,12 +114,16 @@ export function makeResolveRoute(resolveDefaultProvider) {
90
114
  presetFast: preset.fast === true,
91
115
  preference: () => fastPreferenceFor(config, p, m),
92
116
  }),
117
+ autoEffort: resolveAutoEffort({ autoEffort, saved, preset }),
93
118
  modelParameters: hasExplicitModelParameters
94
119
  ? cleanModelParameters(modelParameters)
95
120
  : cleanModelParameters(saved.modelParameters ?? preset.modelParameters),
96
121
  contextPercent: hasExplicitContextPercent
97
122
  ? cleanContextPercent(contextPercent)
98
- : cleanContextPercent(saved.contextPercent ?? preset.contextPercent),
123
+ : keepsSavedPercent
124
+ ? cleanContextPercent(saved.contextPercent ?? preset.contextPercent)
125
+ : undefined,
126
+ ...(tokens ? { contextTokens: tokens } : {}),
99
127
  };
100
128
  }
101
129
  }
@@ -104,18 +132,23 @@ export function makeResolveRoute(resolveDefaultProvider) {
104
132
  const p = explicitProvider || resolveDefaultProvider(config);
105
133
  const m = explicitModel || DEFAULT_MODEL;
106
134
  const saved = modelSettingsFor(config, p, m);
135
+ const tokens = resolveContextTokens({ contextTokens, hasExplicitContextPercent, saved });
107
136
  return {
108
137
  provider: p,
109
138
  model: m,
110
139
  preset: null,
111
140
  effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort),
112
141
  fast: resolveFast({ hasExplicitFast, explicitFast, saved, preference: () => fastPreferenceFor(config, p, m) }),
142
+ autoEffort: resolveAutoEffort({ autoEffort, saved }),
113
143
  modelParameters: hasExplicitModelParameters
114
144
  ? cleanModelParameters(modelParameters)
115
145
  : cleanModelParameters(saved.modelParameters),
116
146
  contextPercent: hasExplicitContextPercent
117
147
  ? cleanContextPercent(contextPercent)
118
- : cleanContextPercent(saved.contextPercent),
148
+ : keepsSavedPercent
149
+ ? cleanContextPercent(saved.contextPercent)
150
+ : undefined,
151
+ ...(tokens ? { contextTokens: tokens } : {}),
119
152
  };
120
153
  };
121
154
  }
@@ -1,6 +1,7 @@
1
1
  // Reasoning-effort catalogs and coercion. Pure helpers.
2
2
  import { clean } from './session-text.mjs';
3
3
  import { customEffortLevels, snapCustomEffort } from '../providers/custom-reasoning.mjs';
4
+ import { rejectedEffortLevelsSync } from '../providers/effort-rejection.mjs';
4
5
 
5
6
  const TOOL_MODES = new Set(['full', 'readonly', 'lead']);
6
7
  const ALL_EFFORT_LEVELS = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
@@ -15,22 +16,15 @@ const EFFORT_LABELS = {
15
16
  ultra: 'Ultra',
16
17
  };
17
18
 
19
+ // Last-resort ladders for routes whose endpoints publish no effort metadata
20
+ // (no per-model levels on /models, no same-endpoint catalog row). Every other
21
+ // provider's ladder comes from its own catalog rows (live roster, models.dev
22
+ // via provider-catalog-registry, seeds); a route with none offers no effort.
18
23
  const EFFORT_OPTIONS_BY_PROVIDER = {
19
24
  openai: ['none', 'low', 'medium', 'high', 'xhigh'],
20
- // gpt-5.6+ catalogs declare max/ultra; the openai-oauth transport folds
21
- // ultra -> max on the wire (openai-oauth.mjs _normalizeReasoningEffort).
22
- 'openai-oauth': ['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
23
- anthropic: ['low', 'medium', 'high', 'xhigh', 'max'],
24
- 'anthropic-oauth': ['low', 'medium', 'high', 'xhigh', 'max'],
25
25
  // xAI Grok 4.6+: low/medium/high/xhigh. Reasoning cannot be disabled.
26
26
  xai: ['low', 'medium', 'high', 'xhigh'],
27
- 'grok-oauth': ['low', 'medium', 'high', 'xhigh'],
28
- 'muse-code-oauth': ['minimal', 'low', 'medium', 'high', 'xhigh'],
29
27
  'mimo-oauth': ['minimal', 'low', 'medium', 'high'],
30
- // Antigravity carries the effort tier inside the wire model id
31
- // (gemini-3-pro-high / -low, claude-*-thinking), so the route exposes none.
32
- 'antigravity-oauth': [],
33
- 'opencode-go': ['high', 'max'],
34
28
  };
35
29
  const EFFORT_BY_FAMILY = {
36
30
  opus: ['low', 'medium', 'high', 'xhigh', 'max'],
@@ -73,7 +67,14 @@ export function normalizeEffortInput(value) {
73
67
  return v;
74
68
  }
75
69
 
70
+ // Levels the endpoint has rejected for this route stop being offered.
76
71
  export function effortOptionsFor(provider, model) {
72
+ const options = catalogEffortOptions(provider, model);
73
+ const rejected = model?.id ? rejectedEffortLevelsSync(provider, model.id) : null;
74
+ return rejected?.size ? options.filter((level) => !rejected.has(level)) : options;
75
+ }
76
+
77
+ function catalogEffortOptions(provider, model) {
77
78
  // Custom-provider rows declare their levels authoritatively: an explicit []
78
79
  // (or no declaration) means none, with no family/provider fallback.
79
80
  if (model?.customReasoning) return customEffortLevels(model) || [];
@@ -6,6 +6,7 @@ import { clean, hasOwn } from './session-text.mjs';
6
6
  import { modelSupportsServiceTier } from '../providers/model-service-tiers.mjs';
7
7
  import { openAiDirectSupportsFast, openAiDirectSupportsUltrafast } from '../providers/openai-direct-request.mjs';
8
8
  import { supportsAnthropicFastMode } from '../../../shared/llm/anthropic-betas.mjs';
9
+ import { normalizeContextTokens } from '../../../shared/llm/default-context-window.mjs';
9
10
 
10
11
  const FAST_CAPABLE_PROVIDERS = new Set([
11
12
  'anthropic',
@@ -123,6 +124,11 @@ export function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfi
123
124
  else delete nextSetting.effort;
124
125
  if (fastCapable) nextSetting.fast = route.fast === true;
125
126
  else nextSetting.fast = false;
127
+ // Auto effort is on unless turned off: only the off state is stored.
128
+ if (hasOwn(route, 'autoEffort')) {
129
+ if (route.autoEffort === false) nextSetting.autoEffort = false;
130
+ else delete nextSetting.autoEffort;
131
+ }
126
132
  if (route.modelParameters && typeof route.modelParameters === 'object') {
127
133
  nextSetting.modelParameters = Object.fromEntries(
128
134
  Object.entries(route.modelParameters)
@@ -136,13 +142,22 @@ export function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfi
136
142
  // pickers send the default stop with every selection; storing it froze each
137
143
  // model at whatever its default was that day, so a later default never
138
144
  // reached anyone who had already selected the model.
145
+ // An exact window in tokens replaces the percentage, and a percentage
146
+ // replaces it. A token selection at the default window reaches here already
147
+ // dropped (resolveRouteContextState answers no contextTokens for it).
148
+ const contextTokens = normalizeContextTokens(route.contextTokens);
139
149
  const requestedPercent = Number(route.contextPercent);
140
150
  const contextPercent = Math.round(requestedPercent / 10) * 10;
141
151
  const isDefault = contextPercent === Number(route.contextDefaultPercent);
142
- if (requestedPercent >= 10 && requestedPercent <= 100 && !isDefault) {
152
+ if (contextTokens) {
153
+ nextSetting.contextTokens = contextTokens;
154
+ delete nextSetting.contextPercent;
155
+ } else if (requestedPercent >= 10 && requestedPercent <= 100 && !isDefault) {
143
156
  nextSetting.contextPercent = contextPercent;
157
+ delete nextSetting.contextTokens;
144
158
  } else {
145
159
  delete nextSetting.contextPercent;
160
+ delete nextSetting.contextTokens;
146
161
  }
147
162
  modelSettings[key] = nextSetting;
148
163
 
@@ -33,9 +33,20 @@ const MCP_TRANSPORT_FIELDS = new Set([
33
33
  'autoDetect',
34
34
  ]);
35
35
 
36
+ // Every spelling resolveMcpStartupTimeoutMs / resolveMcpCallTimeoutMs reads.
37
+ // A newly supplied value must replace the whole group, or a stale alias that
38
+ // the resolver prefers would keep winning.
39
+ export const MCP_TIMEOUT_FIELD_GROUPS = [
40
+ ['startupTimeoutMs', 'startup_timeout_ms', 'startupTimeoutSec', 'startup_timeout_sec'],
41
+ ['timeoutMs', 'timeout_ms', 'callTimeoutMs', 'call_timeout_ms'],
42
+ ];
43
+
36
44
  export function mergeMcpServerConfig(existing, next) {
37
45
  const merged = isPlainObject(existing) ? { ...existing } : {};
38
46
  for (const field of MCP_TRANSPORT_FIELDS) delete merged[field];
47
+ for (const group of MCP_TIMEOUT_FIELD_GROUPS) {
48
+ if (group.some((field) => Object.hasOwn(next, field))) for (const field of group) delete merged[field];
49
+ }
39
50
  return { ...merged, ...next };
40
51
  }
41
52
 
@@ -40,17 +40,21 @@ export const CWD_TOOL = {
40
40
  },
41
41
  trigger: 'Show or switch the active Project; not a shell-local cd.',
42
42
  description:
43
- 'Show or switch the active Project. list returns the registered projects (name, path); a project name instead of a path → list first, then set the matching path, ask only if several match. path must be an existing directory. A shell-local cd does not change the Project.',
43
+ 'Show or switch the active Project. list returns the registered projects (name, path); a project name instead of a path → list first, then set the matching path, ask only if several match. path must be an existing directory. A shell-local cd does not change the Project. merge-worktree / remove-worktree act on this session’s git worktree only when the user explicitly asks.',
44
44
  inputSchema: {
45
45
  type: 'object',
46
46
  properties: {
47
47
  action: {
48
48
  type: 'string',
49
- enum: ['get', 'set', 'list'],
49
+ enum: ['get', 'set', 'list', 'merge-worktree', 'remove-worktree', 'list-worktrees', 'prune-worktrees'],
50
50
  description:
51
- 'get shows the active Project (default without path), set switches to path, list returns registered projects.',
51
+ 'get shows the active Project (default without path), set switches to path, list returns registered projects; merge-worktree merges this session’s worktree into the project folder’s checked-out branch, remove-worktree deletes it (returning to the project folder; call again if it reports WORKTREE_REMOVAL_INCOMPLETE); list-worktrees lists every managed worktree and who retains it; prune-worktrees removes clean, merged ones no session retains.',
52
52
  },
53
53
  path: { type: 'string', description: 'Existing directory to switch to (implies action=set).' },
54
+ force: {
55
+ type: 'boolean',
56
+ description: 'remove-worktree: delete even with uncommitted or unmerged work (only when the user says so).',
57
+ },
54
58
  },
55
59
  additionalProperties: false,
56
60
  },
@@ -93,13 +97,13 @@ const TERMINAL_RUNTIME_TOOLS = ['terminal'];
93
97
  const OFFICE_RUNTIME_TOOLS = ['office'];
94
98
  const MEDIA_RUNTIME_TOOLS = ['media'];
95
99
  const TIDY_RUNTIME_TOOLS = ['tidy'];
100
+ const VISUALIZE_RUNTIME_TOOLS = ['visualize'];
96
101
 
97
102
  const admitted = (tools, names) => (tools || []).filter((tool) => names.includes(tool?.name));
98
103
 
99
104
  /**
100
105
  * Assemble this runtime's tool definitions from the loaded tool-def modules.
101
- * Pure: callers resolve their own availability flags (skills env gate, channel
102
- * ownership) and pass the already-built goal/agent tool arrays.
106
+ * Pure: callers resolve their own availability flags (skills env gate) and pass the already-built goal/agent tool arrays.
103
107
  *
104
108
  * - `standaloneTools` is the model-facing set (also the deferred catalog base).
105
109
  * - `internalToolDefs` adds the `public: false` web-search tools: reachable
@@ -108,7 +112,6 @@ const admitted = (tools, names) => (tools || []).filter((tool) => names.includes
108
112
  export function collectStandaloneToolDefs({
109
113
  webSearchToolDefs,
110
114
  memoryToolDefs,
111
- channelToolDefs,
112
115
  codeGraphToolDefs,
113
116
  browserToolDefs = [],
114
117
  computerToolDefs = [],
@@ -116,10 +119,10 @@ export function collectStandaloneToolDefs({
116
119
  officeToolDefs = [],
117
120
  mediaToolDefs = [],
118
121
  tidyToolDefs = [],
122
+ visualizeToolDefs = [],
119
123
  setupToolDefs = [],
120
124
  goalTools = [],
121
125
  agentTools = [],
122
- isChannelTool = () => false,
123
126
  skillToolEnabled = true,
124
127
  }) {
125
128
  const webSearchRuntimeTools = admitted(webSearchToolDefs?.TOOL_DEFS, WEB_SEARCH_RUNTIME_TOOLS);
@@ -129,7 +132,6 @@ export function collectStandaloneToolDefs({
129
132
  CWD_TOOL,
130
133
  ...webSearchRuntimeTools.filter((tool) => tool?.public !== false),
131
134
  ...admitted(memoryToolDefs?.TOOL_DEFS, MEMORY_RUNTIME_TOOLS),
132
- ...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => isChannelTool(tool?.name)),
133
135
  ...admitted(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS, CODE_GRAPH_RUNTIME_TOOLS),
134
136
  ...admitted(browserToolDefs, BROWSER_RUNTIME_TOOLS),
135
137
  ...admitted(computerToolDefs, COMPUTER_RUNTIME_TOOLS),
@@ -137,6 +139,7 @@ export function collectStandaloneToolDefs({
137
139
  ...admitted(officeToolDefs, OFFICE_RUNTIME_TOOLS),
138
140
  ...admitted(mediaToolDefs, MEDIA_RUNTIME_TOOLS),
139
141
  ...admitted(tidyToolDefs, TIDY_RUNTIME_TOOLS),
142
+ ...admitted(visualizeToolDefs, VISUALIZE_RUNTIME_TOOLS),
140
143
  ...setupToolDefs,
141
144
  ...goalTools,
142
145
  ...agentTools,
@@ -6,6 +6,7 @@ import { clean } from './session-text.mjs';
6
6
  import { normalizeEffortInput } from './effort.mjs';
7
7
  import { isLikelyRawModelId } from './config-helpers.mjs';
8
8
  import { configuredAgentRouteCandidates } from '../../../shared/agent-route-config.mjs';
9
+ import { normalizeContextTokens } from '../../../shared/llm/default-context-window.mjs';
9
10
  import { normalizeAgentId, normalizeWorkflowId } from './workflow-ids.mjs';
10
11
  import { createWorkflowPacks } from './workflow/packs.mjs';
11
12
  import { createWorkflowAgents } from './workflow/agents.mjs';
@@ -186,19 +187,27 @@ export function normalizeWorkflowRoute(routeLike, fallback = {}) {
186
187
  if (!isLikelyRawModelId(model)) return null;
187
188
  const effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
188
189
  const fast = routeLike?.fast ?? fallback.fast;
190
+ // Absent means Auto effort is on; only the off state is carried.
191
+ const autoEffort = routeLike?.autoEffort ?? fallback.autoEffort;
189
192
  const modelParameters = routeLike?.modelParameters ?? fallback.modelParameters;
190
193
  const requestedPercent = Number(routeLike?.contextPercent ?? fallback.contextPercent);
191
194
  const contextPercent =
192
195
  Number.isFinite(requestedPercent) && requestedPercent >= 10 && requestedPercent <= 100
193
196
  ? Math.round(requestedPercent / 10) * 10
194
197
  : null;
198
+ // A percent on the route replaces the fallback's exact window.
199
+ const contextTokens = normalizeContextTokens(
200
+ routeLike?.contextTokens ?? (routeLike?.contextPercent == null ? fallback.contextTokens : undefined)
201
+ );
195
202
  return {
196
203
  provider,
197
204
  model,
198
205
  ...(effort ? { effort } : {}),
199
206
  ...(typeof fast === 'boolean' ? { fast } : {}),
207
+ ...(autoEffort === false ? { autoEffort: false } : {}),
200
208
  ...(modelParameters && typeof modelParameters === 'object' ? { modelParameters: { ...modelParameters } } : {}),
201
209
  ...(contextPercent ? { contextPercent } : {}),
210
+ ...(contextTokens ? { contextTokens } : {}),
202
211
  };
203
212
  }
204
213
 
@@ -214,6 +223,7 @@ export function upsertWorkflowPreset(presets, slot, routeLike) {
214
223
  model: route.model,
215
224
  ...(route.effort ? { effort: route.effort } : {}),
216
225
  ...(route.fast === true ? { fast: true } : {}),
226
+ ...(route.autoEffort === false ? { autoEffort: false } : {}),
217
227
  ...(route.modelParameters ? { modelParameters: route.modelParameters } : {}),
218
228
  tools: 'full',
219
229
  };
@@ -13,6 +13,7 @@ import { createHash } from 'node:crypto';
13
13
  import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
14
14
  import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
15
15
  import { setBoundedTextCacheEntry } from './text-cache-budget.mjs';
16
+ import { canCacheReadFile } from '../../tools/builtin/read-cache-policy.mjs';
16
17
 
17
18
  // Entry cap: MIXDOG_PREFETCH_CACHE_MAX env var, default 200.
18
19
  const _envCap = Number(process.env.MIXDOG_PREFETCH_CACHE_MAX);
@@ -110,12 +111,14 @@ function rememberPrefetch(absPath, entry) {
110
111
  }
111
112
 
112
113
  export function capturePrefetchCacheState(absPath) {
113
- return _statTuple(_normalizeCacheKey(absPath));
114
+ const stat = _statTuple(_normalizeCacheKey(absPath));
115
+ return canCacheReadFile(stat) ? stat : null;
114
116
  }
115
117
 
116
118
  /**
117
119
  * Look up a cached prefetch result for `absPath`. Stat-validates the file on
118
- * every hit. Returns null on miss, TTL expiry, or stat mismatch. Returns
120
+ * every hit. Returns null on miss, TTL expiry, stat mismatch, or a file
121
+ * outside the prefix budget, including a persisted legacy row. Returns
119
122
  * `{ content, ts }` on a fresh hit.
120
123
  */
121
124
  export function tryPrefetchCached(absPath) {
@@ -135,7 +138,7 @@ export function tryPrefetchCached(absPath) {
135
138
  return null;
136
139
  }
137
140
  const fresh = _statTuple(absPath);
138
- if (!_statEqual(entry.stat, fresh)) {
141
+ if (!_statEqual(entry.stat, fresh) || !canCacheReadFile(fresh)) {
139
142
  _prefetchCache.delete(absPath);
140
143
  _deleteDiskEntry(absPath);
141
144
  return null;
@@ -152,7 +155,8 @@ export function tryPrefetchCached(absPath) {
152
155
 
153
156
  /**
154
157
  * Store a per-file prefetch result. Silently skips empty or error-prefixed
155
- * content so only clean read output enters the cache.
158
+ * content, and files outside the prefix budget, so only clean bounded read
159
+ * output enters the cache.
156
160
  * Async callers must pass the state captured before reading; null disables
157
161
  * insertion. Omission is reserved for synchronous current-body insertion.
158
162
  */
@@ -162,7 +166,7 @@ export function setPrefetchCached(absPath, content, readState) {
162
166
  if (classifyResultKind(content) === 'error') return;
163
167
  absPath = _normalizeCacheKey(absPath);
164
168
  const stat = _statTuple(absPath);
165
- if (!stat) return;
169
+ if (!stat || !canCacheReadFile(stat)) return;
166
170
  // Async readers supply the version observed before execution. Never label
167
171
  // an older body with the version that happens to exist at insertion time.
168
172
  if (readState !== undefined && !_statEqual(readState, stat)) return;
@@ -5,6 +5,7 @@ import { clearScopedToolsForSession } from './scoped-cache.mjs';
5
5
  import { registerSessionPurgeHook } from '../store.mjs';
6
6
  import { releaseReadSnapshotScope } from '../../tools/builtin/snapshot-store.mjs';
7
7
  import { setBoundedTextCacheEntry } from './text-cache-budget.mjs';
8
+ import { canCacheReadFile } from '../../tools/builtin/read-cache-policy.mjs';
8
9
 
9
10
  const MAX_PER_SESSION = 100;
10
11
 
@@ -114,6 +115,12 @@ function _arrayStatsValid(statsByAbs) {
114
115
  return true;
115
116
  }
116
117
 
118
+ // Equal stat tuples do not prove bytes past the hashed prefix. Files outside
119
+ // that budget must not be captured, reused, or inserted.
120
+ function _cacheableStats(statsByAbs) {
121
+ return !!statsByAbs && Object.values(statsByAbs).every((stat) => canCacheReadFile(stat));
122
+ }
123
+
117
124
  function _getOrCreate(sessionId) {
118
125
  let m = _bySession.get(sessionId);
119
126
  if (!m) {
@@ -157,20 +164,22 @@ export function captureReadCacheState({ args, cwd }) {
157
164
  const key = _keyFor(args, cwd);
158
165
  if (key === null && Array.isArray(args?.path ?? args?.file_path)) {
159
166
  const parsed = _arrayKeyAndStats(args, cwd);
160
- if (!parsed || Object.values(parsed.statsByAbs).some((stat) => !stat)) return null;
167
+ if (!parsed || !_cacheableStats(parsed.statsByAbs)) return null;
161
168
  return { ...parsed, kind: 'array' };
162
169
  }
163
170
  if (!key) return null;
164
171
  const abs = _absFromKey(key);
165
172
  const stat = _statTuple(abs);
166
- return stat ? { key, statsByAbs: { [abs]: stat }, kind: 'scalar' } : null;
173
+ if (!canCacheReadFile(stat)) return null;
174
+ return { key, statsByAbs: { [abs]: stat }, kind: 'scalar' };
167
175
  }
168
176
 
169
177
  /**
170
178
  * Look up a cached read result for this session+args. Stats the file at
171
179
  * lookup time and only returns when the stat tuple still matches. Returns
172
- * null on miss, on path-form mismatch, or on stat failure. On hit returns
173
- * the full entry { content, firstToolUseId, ts }.
180
+ * null on miss, on path-form mismatch, on stat failure, or when any file
181
+ * exceeds the prefix budget. On hit returns the full entry
182
+ * { content, firstToolUseId, ts }.
174
183
  */
175
184
  export function tryReadCached({ sessionId, args, cwd }) {
176
185
  if (!sessionId) return null;
@@ -182,7 +191,7 @@ export function tryReadCached({ sessionId, args, cwd }) {
182
191
  if (!map) return null;
183
192
  const entry = map.get(parsed.key);
184
193
  if (entry?.kind !== 'array') return null;
185
- if (!_arrayStatsValid(entry.statsByAbs)) {
194
+ if (!_arrayStatsValid(entry.statsByAbs) || !_cacheableStats(entry.statsByAbs)) {
186
195
  map.delete(parsed.key);
187
196
  _ridxPruneKey(sessionId, parsed.key);
188
197
  return null;
@@ -197,7 +206,7 @@ export function tryReadCached({ sessionId, args, cwd }) {
197
206
  const entry = map.get(key);
198
207
  if (!entry) return null;
199
208
  const fresh = _statTuple(_absFromKey(key));
200
- if (!_statEqual(entry.stat, fresh)) {
209
+ if (!_statEqual(entry.stat, fresh) || !canCacheReadFile(fresh)) {
201
210
  map.delete(key);
202
211
  _ridxPruneKey(sessionId, key);
203
212
  return null;
@@ -208,7 +217,8 @@ export function tryReadCached({ sessionId, args, cwd }) {
208
217
  }
209
218
 
210
219
  /**
211
- * Cache a successful read result. Skip caching if the file no longer exists.
220
+ * Cache a successful read result. Skip caching if a file is missing or
221
+ * outside the prefix budget.
212
222
  * `toolUseId` is the tool_use id of the FIRST call that populated the entry.
213
223
  * Async executors pass their pre-execution `readState`; null disables caching
214
224
  * when the source version could not be established. Omission retains direct
@@ -218,7 +228,7 @@ export function setReadCached({ sessionId, args, cwd, content, toolUseId, readSt
218
228
  if (!sessionId) return;
219
229
  if (typeof content !== 'string' || content.length === 0) return;
220
230
  const parsed = captureReadCacheState({ args, cwd });
221
- if (!parsed) return;
231
+ if (!parsed || !_cacheableStats(parsed.statsByAbs)) return;
222
232
  if (
223
233
  readState !== undefined &&
224
234
  (!readState ||
@@ -7,6 +7,7 @@ import {
7
7
  latestActualUserInstructionIndex,
8
8
  } from './messages.mjs';
9
9
  import { staleObservations, supersedeObservation } from './observation-supersession.mjs';
10
+ import { carryMessageId, withoutMessageIdsReplacer } from '../message-ids.mjs';
10
11
 
11
12
  export const EXECUTION_RECOVERY_SOURCE = 'compact-execution-recovery';
12
13
  const TOOL_HISTORY_CONTEXT_RATIO = 0.05;
@@ -34,7 +35,11 @@ export function executionTokens(messages) {
34
35
  const { uiDiff: _uiDiff, ...rest } = message;
35
36
  return rest;
36
37
  });
37
- return Math.max(estimateMessagesTokens(budgetMessages), estimateTokens(JSON.stringify(budgetMessages)));
38
+ // Display ids never reach the model either.
39
+ return Math.max(
40
+ estimateMessagesTokens(budgetMessages),
41
+ estimateTokens(JSON.stringify(budgetMessages, withoutMessageIdsReplacer))
42
+ );
38
43
  }
39
44
 
40
45
  function requestStart(messages, index) {
@@ -123,7 +128,8 @@ function assembleTail(messages, kept, { anchor, firstKept, preserveConversation
123
128
  // Omitted execution stays in the archive; its conversational text
124
129
  // remains verbatim without replaying an orphaned call or reasoning.
125
130
  if (message.toolCalls?.length) {
126
- if (message.content) tail.push({ role: 'assistant', content: message.content });
131
+ // The text-only copy is this message's rewrite: it keeps its id.
132
+ if (message.content) tail.push(carryMessageId({ role: 'assistant', content: message.content }, message));
127
133
  } else {
128
134
  tail.push(message);
129
135
  }
@@ -158,7 +164,8 @@ export function buildExecutionTail(
158
164
  sessionId,
159
165
  toolCallId: 'compact-execution',
160
166
  channel: 'compact-execution',
161
- content: JSON.stringify({ version: 1, messages: transcript }, null, 2),
167
+ // Model-readable evidence: display ids stay out of it.
168
+ content: JSON.stringify({ version: 1, messages: transcript }, withoutMessageIdsReplacer, 2),
162
169
  });
163
170
  if (!archive) throw new Error('compact: execution history could not be archived; original context preserved');
164
171
  recovery = [
@@ -2,13 +2,16 @@
2
2
  // construction helpers.
3
3
  import { SUMMARY_PREFIX, SUMMARY_PREFIX_ANCHOR } from './constants.mjs';
4
4
  import { sha16, roleCounts } from './text-utils.mjs';
5
+ import { carryMessageId, withoutMessageIdsReplacer } from '../message-ids.mjs';
5
6
  import {
6
7
  isInternalRuntimeNotificationText,
7
8
  isModelVisibleToolCompletionWrapper,
8
9
  } from '../../../../shared/tool-execution-contract.mjs';
9
10
 
10
11
  export function compactHeader(oldHistory) {
11
- const encoded = JSON.stringify(oldHistory ?? []);
12
+ // Stable display ids are bookkeeping, not conversation: the header names
13
+ // the same history whether or not its messages were stamped.
14
+ const encoded = JSON.stringify(oldHistory ?? [], withoutMessageIdsReplacer);
12
15
  return [
13
16
  SUMMARY_PREFIX,
14
17
  `messages=${oldHistory.length} sha256=${sha16(encoded)} roles=${roleCounts(oldHistory) || 'none'}`,
@@ -130,16 +133,20 @@ function referenceFilesManifestMessage(message) {
130
133
  const paths = [...text.matchAll(/^###\s+(.+?)\s*$/gm)].map((match) => String(match[1] || '').trim()).filter(Boolean);
131
134
  if (paths.length === 0) return null;
132
135
  const uniquePaths = [...new Set(paths)];
133
- return {
134
- role: 'user',
135
- content: [
136
- '<system-reminder>',
137
- '# Reference files',
138
- 'These files were attached when the session started. Re-read a path when its current contents are needed; the original bodies are not re-injected after Compact.',
139
- ...uniquePaths.map((path) => `- ${path}`),
140
- '</system-reminder>',
141
- ].join('\n'),
142
- };
136
+ // The manifest rewrites the reference-files message: it keeps that id.
137
+ return carryMessageId(
138
+ {
139
+ role: 'user',
140
+ content: [
141
+ '<system-reminder>',
142
+ '# Reference files',
143
+ 'These files were attached when the session started. Re-read a path when its current contents are needed; the original bodies are not re-injected after Compact.',
144
+ ...uniquePaths.map((path) => `- ${path}`),
145
+ '</system-reminder>',
146
+ ].join('\n'),
147
+ },
148
+ message
149
+ );
143
150
  }
144
151
 
145
152
  export function splitProtectedContext(messages) {
@@ -20,6 +20,7 @@ import { buildExecutionTail } from './execution-tail.mjs';
20
20
  import { latestSkillBodies } from '../../context/skill-state.mjs';
21
21
  import { effectiveBudget } from './budget.mjs';
22
22
  import { stripRuntimeUserContext, withRuntimeUserContext } from '../runtime-user-context.mjs';
23
+ import { carryMessageId } from '../message-ids.mjs';
23
24
  import { normalizeIngestRole, shouldExcludeIngestMessage } from '../../../../memory/lib/session-ingest.mjs';
24
25
  import { codexWireSendOpts } from '../manager/session-id.mjs';
25
26
  import { rebaseCompactedEffortConfiguration } from '../../providers/effort-configuration.mjs';
@@ -380,7 +381,8 @@ function skillLoaderPair(live, bodyIndex, name, retainedCallIds) {
380
381
  const call = owner?.toolCalls.find((entry) => entry.id === result.toolCallId);
381
382
  if (call?.name !== 'Skill' || call.arguments?.name !== name) continue;
382
383
  if (!result.nativeToolSearch || retainedCallIds.has(result.toolCallId)) return [];
383
- return [{ role: 'assistant', content: '', toolCalls: [call] }, result];
384
+ // The re-attached call is part of its owner message: it keeps that id.
385
+ return [carryMessageId({ role: 'assistant', content: '', toolCalls: [call] }, owner), result];
384
386
  }
385
387
  return [];
386
388
  }
@@ -549,11 +551,17 @@ function assertHandoffFits(summaryMessage, room) {
549
551
  export function freshContextCompactMessages(messages, budgetTokens, opts = {}) {
550
552
  const startedAt = Date.now();
551
553
  const targetBudget = effectiveBudget(budgetTokens, opts);
552
- let budget = targetBudget;
553
554
  const baseSanitized = reconcileDedupStubs(sanitizeToolPairs(messages));
554
555
  const baseTokens = safeEstimateMessagesTokens(baseSanitized);
555
- if (baseTokens != null && baseTokens <= budget && opts.force !== true) {
556
- return fitsBudgetResult({ startedAt, messages, baseSanitized, baseTokens, budget, query: opts.query || '' });
556
+ if (baseTokens != null && baseTokens <= targetBudget && opts.force !== true) {
557
+ return fitsBudgetResult({
558
+ startedAt,
559
+ messages,
560
+ baseSanitized,
561
+ baseTokens,
562
+ budget: targetBudget,
563
+ query: opts.query || '',
564
+ });
557
565
  }
558
566
 
559
567
  const source = splitFreshSource(baseSanitized);
@@ -562,7 +570,7 @@ export function freshContextCompactMessages(messages, budgetTokens, opts = {}) {
562
570
  budgetTokens,
563
571
  opts
564
572
  );
565
- budget = freshContextBudget({ targetBudget, budgetTokens, mandatoryCost, summaryMessage, opts });
573
+ const budget = freshContextBudget({ targetBudget, budgetTokens, mandatoryCost, summaryMessage, opts });
566
574
  assertMandatoryFits(budget, mandatoryCost, summaryMessage, opts.contextWindow);
567
575
  const handoff = handoffRoomFor(budget, mandatoryCost, opts);
568
576
  const summaryContent = String(summaryMessage?.content || '');