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
@@ -14,7 +14,7 @@
14
14
  // whole tree, but only these paths may be restored from it.
15
15
  import { createHash } from 'node:crypto';
16
16
  import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
17
- import { join } from 'node:path';
17
+ import { join, resolve } from 'node:path';
18
18
  import { cleanString as clean } from './clean.mjs';
19
19
  import { resolvePluginData } from './plugin-paths.mjs';
20
20
 
@@ -131,20 +131,19 @@ async function readRecordFile(path) {
131
131
  }
132
132
  }
133
133
 
134
- /** Oldest-first sweep so a long-lived install cannot accumulate records. */
135
- async function sweepRecords() {
136
- if (sweepDone) return;
137
- sweepDone = true;
134
+ /** Oldest-first sweep of one record directory so a long-lived install cannot
135
+ * accumulate records. */
136
+ async function sweepDirectory(directory, ttlMs, maxFiles) {
138
137
  try {
139
- const names = await readdir(recordRoot());
138
+ const names = await readdir(directory);
140
139
  const now = Date.now();
141
140
  const kept = [];
142
141
  for (const name of names) {
143
142
  if (!name.endsWith('.json')) continue;
144
- const path = join(recordRoot(), name);
143
+ const path = join(directory, name);
145
144
  try {
146
145
  const info = await stat(path);
147
- if (now - info.mtimeMs > RECORD_TTL_MS) {
146
+ if (now - info.mtimeMs > ttlMs) {
148
147
  await rm(path, { force: true });
149
148
  continue;
150
149
  }
@@ -154,7 +153,7 @@ async function sweepRecords() {
154
153
  }
155
154
  }
156
155
  kept.sort((left, right) => left.mtimeMs - right.mtimeMs);
157
- for (const entry of kept.slice(0, Math.max(0, kept.length - MAX_RECORD_FILES))) {
156
+ for (const entry of kept.slice(0, Math.max(0, kept.length - maxFiles))) {
158
157
  await rm(entry.path, { force: true });
159
158
  }
160
159
  } catch {
@@ -162,6 +161,18 @@ async function sweepRecords() {
162
161
  }
163
162
  }
164
163
 
164
+ async function sweepRecords() {
165
+ if (sweepDone) return;
166
+ sweepDone = true;
167
+ await sweepDirectory(recordRoot(), RECORD_TTL_MS, MAX_RECORD_FILES);
168
+ }
169
+
170
+ async function writeJsonAtomically(path, payload) {
171
+ const temporary = `${path}.${process.pid}.tmp`;
172
+ await writeFile(temporary, JSON.stringify(payload), 'utf8');
173
+ await rename(temporary, path);
174
+ }
175
+
165
176
  /**
166
177
  * Fold one turn into a session's record. `generation` identifies the turn, so
167
178
  * re-recording the same turn updates it in place rather than appending.
@@ -219,9 +230,7 @@ export async function saveTurnSnapshotRecord(sessionId, turn) {
219
230
  .slice(-MAX_SESSION_SCOPES)
220
231
  .map(serializeScope),
221
232
  };
222
- const temporary = `${path}.${process.pid}.tmp`;
223
- await writeFile(temporary, JSON.stringify(payload), 'utf8');
224
- await rename(temporary, path);
233
+ await writeJsonAtomically(path, payload);
225
234
  return true;
226
235
  } catch {
227
236
  return false;
@@ -259,8 +268,333 @@ export async function loadSessionSnapshotRecords(sessionId) {
259
268
  return sessionScopes(record).map(serializeScope);
260
269
  }
261
270
 
271
+ // Absolute, slash-normalized and (on Windows) case-folded: equal for every
272
+ // spelling of one folder, so a stored root and a caller's root compare equal.
273
+ function matchKey(value) {
274
+ const text = clean(value);
275
+ if (!text) return '';
276
+ const key = resolve(text).replace(/\\/g, '/').replace(/\/+$/, '');
277
+ return process.platform === 'win32' ? key.toLowerCase() : key;
278
+ }
279
+
280
+ /**
281
+ * Remove one worktree root from every session record: its turns and its
282
+ * session scope. Other roots' scopes, turns and the rest of the session history
283
+ * stay; a record left with nothing is deleted. Serialized with saves through
284
+ * the per-session chain. The per-turn change records rooted there are marked
285
+ * unavailable (see forgetChangeRecordsRoot). Returns `{ removed, kept }`:
286
+ * removed are `<recordFile>#<root>` scopes (or the record file itself once
287
+ * emptied) and `<changesFile>#<checkpointId>` change records; kept are files
288
+ * that matched but could not be rewritten.
289
+ */
290
+ export async function forgetTurnSnapshotRoot(root) {
291
+ const removed = [];
292
+ const kept = [];
293
+ const target = matchKey(root);
294
+ if (!target) return { removed, kept };
295
+ let names = [];
296
+ try {
297
+ names = await readdir(recordRoot());
298
+ } catch {
299
+ return { removed, kept };
300
+ }
301
+ for (const name of names) {
302
+ if (!name.endsWith('.json')) continue;
303
+ const path = join(recordRoot(), name);
304
+ const sessionId = clean((await readRecordFile(path))?.sessionId);
305
+ if (!sessionId) continue;
306
+ await chain(sessionId, async () => {
307
+ const record = await readRecordFile(path);
308
+ if (!record) return;
309
+ const turns = record.turns.filter((turn) => matchKey(turn?.root) !== target);
310
+ const scopes = sessionScopes(record);
311
+ const remaining = scopes.filter((scope) => matchKey(scope.root) !== target);
312
+ if (turns.length === record.turns.length && remaining.length === scopes.length) return;
313
+ try {
314
+ if (turns.length === 0 && remaining.length === 0) {
315
+ await rm(path, { force: true });
316
+ removed.push(path);
317
+ return;
318
+ }
319
+ await writeJsonAtomically(path, { ...record, turns, sessionScopes: remaining.map(serializeScope) });
320
+ removed.push(`${path}#${target}`);
321
+ } catch {
322
+ kept.push(path);
323
+ }
324
+ });
325
+ }
326
+ await forgetChangeRecordsRoot(target, removed, kept);
327
+ return { removed, kept };
328
+ }
329
+
330
+ /** Why a change-record file of a removed worktree can no longer be undone or
331
+ * reapplied. */
332
+ export const REMOVED_WORKTREE_CHANGE_REASON = 'its worktree was removed';
333
+
334
+ const unavailableChangeFile = (file) => {
335
+ const { patch, patchEncoding, patchOmitted, ...rest } = file;
336
+ return { ...rest, revertable: false, reason: REMOVED_WORKTREE_CHANGE_REASON, patch: null };
337
+ };
338
+
339
+ const changeFileUnavailable = (file) =>
340
+ file.revertable === false && file.reason === REMOVED_WORKTREE_CHANGE_REASON && file.patch === null;
341
+
342
+ /**
343
+ * Mark every per-turn change record rooted at `target` (a matchKey) as
344
+ * unavailable: the record and its file list stay, so the turn's history card
345
+ * still says what changed, but each file drops its patch (the removed
346
+ * worktree's contents) and becomes non-revertable, so no Undo or Reapply is
347
+ * offered. Records of other roots are untouched. Serialized with every other
348
+ * change-record write of the session.
349
+ */
350
+ async function forgetChangeRecordsRoot(target, removed, kept) {
351
+ let names = [];
352
+ try {
353
+ names = await readdir(changesRoot());
354
+ } catch {
355
+ return;
356
+ }
357
+ for (const name of names) {
358
+ if (!name.endsWith('.json')) continue;
359
+ const path = join(changesRoot(), name);
360
+ let sessionId = '';
361
+ try {
362
+ sessionId = clean(JSON.parse(await readFile(path, 'utf8'))?.sessionId);
363
+ } catch {
364
+ // Unreadable or corrupt file: no session id, skipped below.
365
+ }
366
+ if (!sessionId || changesPath(sessionId) !== path) continue;
367
+ await chain(`changes:${sessionId}`, async () => {
368
+ const records = (await readChangesFile(sessionId))?.records;
369
+ if (!records) return;
370
+ const marked = [];
371
+ const next = records.map((record) => {
372
+ const files = Array.isArray(record?.files) ? record.files : [];
373
+ if (matchKey(record?.root) !== target || files.every(changeFileUnavailable)) return record;
374
+ marked.push(record.checkpointId);
375
+ return {
376
+ ...record,
377
+ files: files.map(unavailableChangeFile),
378
+ revision: (Number(record.revision) || 0) + 1,
379
+ updatedAt: Date.now(),
380
+ };
381
+ });
382
+ if (marked.length === 0) return;
383
+ try {
384
+ await writeChangeRecords(sessionId, next);
385
+ removed.push(...marked.map((checkpointId) => `${path}#${checkpointId}`));
386
+ } catch {
387
+ kept.push(path);
388
+ }
389
+ });
390
+ }
391
+ }
392
+
393
+ // ── Per-turn change records ─────────────────────────────────────────────────
394
+ //
395
+ // One file per session holding every retained turn's frozen net diff, keyed by
396
+ // checkpointId (the outer prompt id). Unlike the baseline index above these
397
+ // carry their own complete patches, so they need no shadow object to survive.
398
+
399
+ const CHANGES_VERSION = 1;
400
+ // Byte budget of one session's change records; the oldest turns go first.
401
+ const MAX_CHANGE_BYTES_PER_SESSION = 32 * 1024 * 1024;
402
+ const MAX_CHANGE_RECORD_FILES = 512;
403
+ const CHANGE_RECORD_TTL_MS = 30 * 24 * 60 * 60 * 1000;
404
+ let changesSweepDone = false;
405
+
406
+ function changesRoot() {
407
+ return join(recordRoot(), 'changes');
408
+ }
409
+
410
+ function changesPath(sessionId) {
411
+ const hash = createHash('sha256').update(sessionId).digest('hex').slice(0, 24);
412
+ return join(changesRoot(), `${hash}.json`);
413
+ }
414
+
415
+ async function readChangesFile(sessionId) {
416
+ try {
417
+ const parsed = JSON.parse(await readFile(changesPath(sessionId), 'utf8'));
418
+ if (parsed?.version !== CHANGES_VERSION || parsed.sessionId !== sessionId) return null;
419
+ return Array.isArray(parsed.records) ? parsed : null;
420
+ } catch {
421
+ return null;
422
+ }
423
+ }
424
+
425
+ /** Evict the oldest turns until the session fits its byte budget; a turn that
426
+ * alone exceeds it drops its largest patches (those files stop being
427
+ * revertable) instead of the whole record. */
428
+ function boundChangeRecords(records, keepCheckpointId) {
429
+ const sizes = records.map((record) => Buffer.byteLength(JSON.stringify(record), 'utf8'));
430
+ let total = sizes.reduce((sum, size) => sum + size, 0);
431
+ for (let index = 0; total > MAX_CHANGE_BYTES_PER_SESSION && index < records.length; ) {
432
+ if (records[index].checkpointId === keepCheckpointId) {
433
+ index += 1;
434
+ continue;
435
+ }
436
+ total -= sizes[index];
437
+ records.splice(index, 1);
438
+ sizes.splice(index, 1);
439
+ }
440
+ if (total <= MAX_CHANGE_BYTES_PER_SESSION) return;
441
+ const kept = records.find((record) => record.checkpointId === keepCheckpointId);
442
+ // Replay patches and display patches share one budget, measured in UTF-8 bytes.
443
+ const weight = (file) =>
444
+ ['patch', 'viewPatch'].reduce(
445
+ (sum, key) => sum + (typeof file[key] === 'string' ? Buffer.byteLength(file[key], 'utf8') : 0),
446
+ 0
447
+ );
448
+ const largest = [...(kept?.files || []), ...(kept?.outside || [])]
449
+ .filter((file) => weight(file) > 0)
450
+ .sort((left, right) => weight(right) - weight(left));
451
+ for (const file of largest) {
452
+ if (total <= MAX_CHANGE_BYTES_PER_SESSION) break;
453
+ total -= weight(file);
454
+ file.patch = null;
455
+ delete file.patchEncoding;
456
+ delete file.viewPatch;
457
+ file.revertable = false;
458
+ file.reason = 'change is too large to keep for undo';
459
+ file.patchOmitted = true;
460
+ }
461
+ }
462
+
463
+ // Test seam: runs before every change-record write; a throw fails the write.
464
+ let changeWriteHookForTest = null;
465
+
466
+ async function writeChangeRecords(sessionId, records) {
467
+ await changeWriteHookForTest?.(sessionId, records);
468
+ changeRecordsCache.delete(sessionId);
469
+ await mkdir(changesRoot(), { recursive: true });
470
+ if (!changesSweepDone) {
471
+ changesSweepDone = true;
472
+ void sweepDirectory(changesRoot(), CHANGE_RECORD_TTL_MS, MAX_CHANGE_RECORD_FILES);
473
+ }
474
+ await writeJsonAtomically(changesPath(sessionId), { version: CHANGES_VERSION, sessionId, records });
475
+ }
476
+
477
+ const changeContent = (record) =>
478
+ JSON.stringify([record.kind, record.root, record.agents || [], record.files || [], record.outside || []]);
479
+
480
+ /**
481
+ * Upsert one turn's frozen changes. Every file of the new freeze is `applied`;
482
+ * a file the user had already undone and that the new net diff no longer
483
+ * contains keeps its `reverted` entry, so Reapply survives a re-seal. The
484
+ * revision bumps only when the record actually changed. Resolves to the
485
+ * stored record.
486
+ */
487
+ export async function upsertTurnChangeRecord(sessionId, next) {
488
+ const id = clean(sessionId);
489
+ const checkpointId = clean(next?.checkpointId);
490
+ if (!id || !checkpointId) return null;
491
+ return await chain(`changes:${id}`, async () => {
492
+ const records = (await readChangesFile(id))?.records || [];
493
+ const index = records.findIndex((record) => record?.checkpointId === checkpointId);
494
+ const prior = index >= 0 ? records[index] : null;
495
+ const files = (Array.isArray(next.files) ? next.files : []).map((file) => ({ ...file, state: 'applied' }));
496
+ const present = new Set(files.map((file) => rootKey(file.path)));
497
+ for (const file of prior?.files || []) {
498
+ if (file.state === 'reverted' && !present.has(rootKey(file.path))) files.push(file);
499
+ }
500
+ files.sort((left, right) => left.path.localeCompare(right.path));
501
+ const candidate = {
502
+ checkpointId,
503
+ kind: clean(next.kind) || 'git',
504
+ root: clean(next.root),
505
+ agents: Array.isArray(next.agents) ? next.agents : [],
506
+ files,
507
+ // View-only edits outside the root of a Git turn: never undone or
508
+ // replayed, listed only by the session review.
509
+ ...(Array.isArray(next.outside) && next.outside.length ? { outside: next.outside } : {}),
510
+ };
511
+ if (prior && changeContent(prior) === changeContent(candidate)) return prior;
512
+ const now = Date.now();
513
+ const record = {
514
+ ...candidate,
515
+ revision: (Number(prior?.revision) || 0) + 1,
516
+ createdAt: Number(prior?.createdAt) || now,
517
+ updatedAt: now,
518
+ };
519
+ if (index >= 0) records[index] = record;
520
+ else records.push(record);
521
+ boundChangeRecords(records, checkpointId);
522
+ await writeChangeRecords(id, records);
523
+ return records.find((entry) => entry.checkpointId === checkpointId) || null;
524
+ });
525
+ }
526
+
527
+ /** Apply `mutate(record)` (returning the next files, or null for no change)
528
+ * and bump the revision. Serialized with every other write of the session. */
529
+ export async function updateTurnChangeRecord(sessionId, checkpointId, mutate) {
530
+ const id = clean(sessionId);
531
+ const key = clean(checkpointId);
532
+ if (!id || !key) return null;
533
+ return await chain(`changes:${id}`, async () => {
534
+ const records = (await readChangesFile(id))?.records || [];
535
+ const index = records.findIndex((record) => record?.checkpointId === key);
536
+ if (index < 0) return null;
537
+ const files = mutate(records[index]);
538
+ if (!files) return records[index];
539
+ records[index] = {
540
+ ...records[index],
541
+ files,
542
+ revision: (Number(records[index].revision) || 0) + 1,
543
+ updatedAt: Date.now(),
544
+ };
545
+ await writeChangeRecords(id, records);
546
+ return records[index];
547
+ });
548
+ }
549
+
550
+ /** One turn's change record by its exact checkpoint id; never a fallback. */
551
+ export async function loadTurnChangeRecord(sessionId, checkpointId) {
552
+ const id = clean(sessionId);
553
+ const key = clean(checkpointId);
554
+ if (!id || !key) return null;
555
+ const records = (await readChangesFile(id))?.records || [];
556
+ return records.find((record) => record?.checkpointId === key) || null;
557
+ }
558
+
559
+ // Parsed change records by session, valid while the file's mtime and size are
560
+ // unchanged: the session review refreshes every few seconds and must not
561
+ // re-read and re-parse a multi-megabyte file each time. Callers never mutate.
562
+ const changeRecordsCache = new Map();
563
+ const CHANGE_RECORDS_CACHE_SESSIONS = 8;
564
+
565
+ /** Every retained turn's change record of a session, oldest first. */
566
+ export async function loadTurnChangeRecords(sessionId) {
567
+ const id = clean(sessionId);
568
+ if (!id) return [];
569
+ let signature = '';
570
+ try {
571
+ const info = await stat(changesPath(id));
572
+ signature = `${info.mtimeMs}:${info.size}`;
573
+ } catch {
574
+ changeRecordsCache.delete(id);
575
+ return [];
576
+ }
577
+ const cached = changeRecordsCache.get(id);
578
+ if (cached?.signature === signature) return cached.records;
579
+ const records = ((await readChangesFile(id))?.records || [])
580
+ .filter(Boolean)
581
+ .sort((left, right) => (Number(left.createdAt) || 0) - (Number(right.createdAt) || 0));
582
+ changeRecordsCache.delete(id);
583
+ changeRecordsCache.set(id, { signature, records });
584
+ if (changeRecordsCache.size > CHANGE_RECORDS_CACHE_SESSIONS) {
585
+ changeRecordsCache.delete(changeRecordsCache.keys().next().value);
586
+ }
587
+ return records;
588
+ }
589
+
590
+ export function _setTurnChangeRecordWriteHookForTest(hook) {
591
+ changeWriteHookForTest = typeof hook === 'function' ? hook : null;
592
+ }
593
+
262
594
  export function _setTurnSnapshotStoreRootForTest(directory) {
263
595
  recordRootOverride = clean(directory);
264
596
  writeChains.clear();
597
+ changeRecordsCache.clear();
265
598
  sweepDone = false;
599
+ changesSweepDone = false;
266
600
  }