mixdog 1.2.5 → 1.2.6

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 (290) hide show
  1. package/package.json +1 -1
  2. package/scripts/lib/gate-scheduler.mjs +2 -2
  3. package/scripts/lib/run-node-tests.mjs +5 -14
  4. package/scripts/provider-pingpong-live.mjs +9 -9
  5. package/src/agents/advisor/AGENT.md +4 -4
  6. package/src/agents/advisor/agent.json +1 -1
  7. package/src/agents/reviewer/AGENT.md +2 -3
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +2 -1
  11. package/src/defaults/skills/computer-use/SKILL.md +3 -1
  12. package/src/defaults/skills/visualize/SKILL.md +3 -2
  13. package/src/lib/rules-builder.cjs +1 -1
  14. package/src/output-styles/common.md +22 -6
  15. package/src/output-styles/detailed.md +7 -5
  16. package/src/output-styles/minimal.md +7 -3
  17. package/src/output-styles/simple.md +2 -3
  18. package/src/rules/agent/AGENT.md +3 -1
  19. package/src/rules/lead/LEAD.md +4 -3
  20. package/src/rules/shared/10-tool-workflow.md +2 -1
  21. package/src/rules/shared/60-verification.md +4 -5
  22. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +6 -0
  24. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
  25. package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
  26. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
  27. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  28. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
  29. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
  32. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
  38. package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
  39. package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
  40. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
  41. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
  42. package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
  43. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
  44. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
  65. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
  66. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
  67. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
  68. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
  69. package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
  70. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
  71. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
  72. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
  73. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
  74. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
  76. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
  77. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
  78. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
  79. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
  80. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
  82. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
  83. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
  84. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
  85. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
  86. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
  87. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
  89. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
  90. package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
  91. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
  92. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
  93. package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
  94. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
  95. package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
  96. package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
  97. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +258 -43
  98. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
  99. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
  100. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
  101. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
  102. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
  103. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
  104. package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
  105. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
  106. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
  109. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
  112. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
  113. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
  115. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
  117. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
  119. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
  122. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
  123. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
  125. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
  126. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
  133. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
  134. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
  135. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
  136. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
  137. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
  138. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
  140. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
  141. package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
  143. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
  144. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
  145. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
  146. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
  153. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
  155. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
  156. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
  157. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
  158. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  159. package/src/runtime/attachments/office-extract.mjs +11 -9
  160. package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
  161. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
  162. package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
  163. package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
  164. package/src/runtime/office/core/tabular.mjs +1 -1
  165. package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
  166. package/src/runtime/office/portable/docx-formatting.mjs +1 -1
  167. package/src/runtime/office/portable/docx-runs.mjs +1 -1
  168. package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
  169. package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
  170. package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
  171. package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
  172. package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
  173. package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
  174. package/src/runtime/office/portable/portable-opc.mjs +27 -13
  175. package/src/runtime/office/portable/portable-pivot.mjs +3 -3
  176. package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
  177. package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
  178. package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
  179. package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
  180. package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
  181. package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
  182. package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
  183. package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
  184. package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
  185. package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
  186. package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
  187. package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
  188. package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
  189. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
  190. package/src/runtime/office/portable/portable-xml.mjs +1 -1
  191. package/src/runtime/shared/atomic-file.mjs +22 -13
  192. package/src/runtime/shared/background-tasks.mjs +60 -20
  193. package/src/runtime/shared/child-spawn-gate.mjs +26 -16
  194. package/src/runtime/shared/config.mjs +0 -10
  195. package/src/runtime/shared/file-lock.mjs +7 -1
  196. package/src/runtime/shared/file-permissions-worker.mjs +38 -0
  197. package/src/runtime/shared/file-permissions.d.mts +19 -4
  198. package/src/runtime/shared/file-permissions.mjs +310 -4
  199. package/src/runtime/shared/git-runner-worker.mjs +6 -78
  200. package/src/runtime/shared/llm/http-agent.mjs +7 -3
  201. package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
  202. package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
  203. package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
  204. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
  205. package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
  206. package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
  207. package/src/runtime/shared/memory-snapshot.mjs +5 -1
  208. package/src/runtime/shared/notify-trace.mjs +8 -1
  209. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  210. package/src/runtime/shared/process-runner-worker.mjs +7 -0
  211. package/src/runtime/shared/process-runner.mjs +38 -0
  212. package/src/runtime/shared/provider-accounts.mjs +2 -1
  213. package/src/runtime/shared/provider-api-key.mjs +0 -1
  214. package/src/runtime/shared/read-row-numbers.mjs +1 -1
  215. package/src/runtime/shared/resource-admission.mjs +60 -18
  216. package/src/runtime/shared/run-child.mjs +84 -0
  217. package/src/runtime/shared/session-activity-signal.mjs +23 -0
  218. package/src/runtime/shared/shell-output-summary.mjs +23 -0
  219. package/src/runtime/shared/singleton-owner.mjs +126 -42
  220. package/src/runtime/shared/tool-output-limit.mjs +4 -2
  221. package/src/runtime/shared/tool-output-marker.mjs +58 -0
  222. package/src/runtime/shared/user-data-guard.mjs +28 -9
  223. package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
  224. package/src/runtime/shared/worktrees/git-head.mjs +20 -2
  225. package/src/runtime/shared/worktrees/lock.mjs +3 -2
  226. package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
  227. package/src/runtime/tidy/runners/biome.mjs +3 -1
  228. package/src/runtime/tidy/shadow.mjs +2 -1
  229. package/src/runtime/web-search/index.mjs +5 -7
  230. package/src/runtime/web-search/lib/formatter.mjs +42 -6
  231. package/src/runtime/web-search/tool-defs.mjs +6 -1
  232. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
  233. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
  234. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
  235. package/src/session-runtime/lifecycle/shared.mjs +19 -2
  236. package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
  237. package/src/session-runtime/lifecycle/teardown.mjs +5 -2
  238. package/src/session-runtime/lifecycle-api.mjs +3 -1
  239. package/src/session-runtime/provider-auth/login.mjs +10 -6
  240. package/src/session-runtime/provider-auth/setup.mjs +19 -2
  241. package/src/session-runtime/provider-catalog-cache.mjs +101 -25
  242. package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
  243. package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
  244. package/src/session-runtime/provider-models.mjs +76 -10
  245. package/src/session-runtime/provider-readiness.mjs +4 -0
  246. package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
  247. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
  248. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
  249. package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
  250. package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
  251. package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
  252. package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
  253. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
  254. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
  255. package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
  256. package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
  257. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
  258. package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
  259. package/src/session-runtime/services/agent-tool.mjs +20 -0
  260. package/src/session-runtime/services/provider-admin.mjs +92 -20
  261. package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
  262. package/src/session-runtime/services/usage-dashboard.mjs +27 -69
  263. package/src/session-runtime/turn/session-ops.mjs +2 -2
  264. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  265. package/src/session-runtime/usage-stats-api.mjs +6 -1
  266. package/src/standalone/channel-transport/http-routes.mjs +10 -9
  267. package/src/standalone/daemon-desktop-runtime.mjs +20 -7
  268. package/src/standalone/daemon-owner.mjs +117 -25
  269. package/src/standalone/daemon.mjs +43 -13
  270. package/src/standalone/session-call-cache.mjs +6 -0
  271. package/src/standalone/session-protocol.mjs +1 -1
  272. package/src/standalone/session-service/retention/busy.mjs +7 -2
  273. package/src/standalone/session-transport/routes.mjs +7 -6
  274. package/src/tui/app/model-options.mjs +39 -2
  275. package/src/tui/app/model-picker.mjs +16 -2
  276. package/src/tui/app/provider-setup-picker.mjs +1 -2
  277. package/src/tui/dist/index.mjs +385 -104
  278. package/src/tui/session/notice-text.mjs +0 -1
  279. package/src/tui/session/session-api/intake/submission.mjs +7 -13
  280. package/src/tui/session/session-api/integrations.mjs +1 -5
  281. package/src/tui/session/session-flow/drain.mjs +8 -0
  282. package/src/tui/session/session-flow/steering.mjs +46 -0
  283. package/src/tui/session/session-flow.mjs +2 -0
  284. package/src/tui/session/turn-stream.mjs +33 -0
  285. package/src/tui/session/turn.mjs +30 -3
  286. package/src/ui/statusline.mjs +1 -6
  287. package/src/ui/streaming-markdown-heal.mjs +24 -7
  288. package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
  289. package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
  290. package/src/workflows/default/WORKFLOW.md +9 -13
@@ -8,6 +8,7 @@ import { noEnabledProvidersError } from './registry-errors.mjs';
8
8
  import { providerTakesNativePdf } from '../../../attachments/store.mjs';
9
9
  import { modelProxyClientMode } from '../../../shared/model-proxy-mode.mjs';
10
10
  import { getModelProxyChannel } from './model-proxy-channel.mjs';
11
+ import { boundedProviderCatalogOp } from './catalog-deadline.mjs';
11
12
  // OpenAI-compat provider names are self-declared by openai-compat-presets.mjs via
12
13
  // OPENAI_COMPAT_PRESETS. No parallel list maintained here.
13
14
  const providers = new Map();
@@ -33,6 +34,7 @@ export const OAUTH_PROVIDER_NAMES = new Set([
33
34
  'mimo-oauth',
34
35
  'cursor-oauth',
35
36
  'antigravity-oauth',
37
+ 'opencode-go-oauth',
36
38
  ]);
37
39
  // Providers the most recently applied config declared with enabled:false for a
38
40
  // USER reason. A deliberate opt-out (logout / disable in settings) must survive
@@ -178,7 +180,7 @@ const PROVIDER_MODULES = {
178
180
  'cursor-api': ['./cursor.mjs', 'CursorApiProvider'],
179
181
  openai: ['./openai-ws.mjs', 'OpenAIDirectProvider'],
180
182
  'mixdog-local': ['./mixdog-local.mjs', 'MixdogLocalProvider'],
181
- 'opencode-go': ['./opencode-go.mjs', 'OpenCodeGoProvider'],
183
+ 'opencode-go-oauth': ['./opencode-go-oauth.mjs', 'OpenCodeGoOAuthProvider'],
182
184
  // Multi-wire preset gateways (wire-route.mjs picks the wire per model).
183
185
  openrouter: ['./openrouter.mjs', 'OpenRouterProvider'],
184
186
  deepseek: ['./deepseek.mjs', 'DeepSeekProvider'],
@@ -288,6 +290,8 @@ async function _initProvidersUnsynchronized(config, signal = null, proxyChannel
288
290
  const enabledResults = await Promise.all(
289
291
  entries.map(async ([name, cfg]) => {
290
292
  if (!cfg.enabled) return null;
293
+ // A retired provider's saved config is inert: its replacement is its own entry.
294
+ if (Object.hasOwn(RETIRED_PROVIDER_ALIASES, name)) return null;
291
295
  // Idempotent reuse: an enabled provider whose config signature is
292
296
  // unchanged from the live registry is carried forward as-is. Only
293
297
  // added or changed providers are (re)constructed below.
@@ -438,7 +442,14 @@ function _scheduleOAuthSelfHeal(name) {
438
442
  _oauthSelfHealLoads.delete(name);
439
443
  });
440
444
  }
441
- export function getProvider(name) {
445
+ // Provider ids that were retired: a saved route or session naming one is served
446
+ // by its replacement account. When that account is not connected the lookup
447
+ // misses like any other unconnected provider.
448
+ const RETIRED_PROVIDER_ALIASES = Object.freeze({ 'opencode-go': 'opencode-go-oauth' });
449
+ export function getProvider(requestedName) {
450
+ const name = Object.hasOwn(RETIRED_PROVIDER_ALIASES, requestedName)
451
+ ? RETIRED_PROVIDER_ALIASES[requestedName]
452
+ : requestedName;
442
453
  const cached = providers.get(name);
443
454
  if (cached) return cached;
444
455
  // OAuth lazy fallback. Covers the boot-time race where the credential probe
@@ -560,10 +571,23 @@ export async function _withRegisteredProviderForTestAsync(name, instance, fn) {
560
571
  // Shared by the startup refresh and the 24h/forced refresh below.
561
572
  function providerCatalogRefreshFn(provider) {
562
573
  if (typeof provider?._refreshModelCache === 'function') return () => provider._refreshModelCache();
563
- if (typeof provider?.listModels === 'function') return () => provider.listModels();
574
+ if (typeof provider?.listModels === 'function') return (signal) => provider.listModels({ signal });
564
575
  return null;
565
576
  }
566
577
 
578
+ // One provider's refresh, bounded per provider (catalog-deadline.mjs): a hung
579
+ // or offline provider is reported and left out after the deadline, so the
580
+ // refresh epoch still completes for everyone else. Its late answer lands in
581
+ // its own model cache; the session catalog retries its listing after the
582
+ // partial-catalog backoff.
583
+ function boundedCatalogRefresh(name, provider, refreshFn, label) {
584
+ return boundedProviderCatalogOp({ name, provider, kind: 'refresh', run: refreshFn }).then(({ error }) => {
585
+ if (!error) return;
586
+ const msg = error instanceof Error ? error.message : String(error);
587
+ process.stderr.write(`[provider:${name}] ${label} failed: ${msg}\n`);
588
+ });
589
+ }
590
+
567
591
  function armNextCatalogRefresh(startedAt = Date.now()) {
568
592
  _catalogRefreshNotBefore = startedAt + CATALOG_REFRESH_INTERVAL_MS;
569
593
  if (_catalogRefreshTimer) clearTimeout(_catalogRefreshTimer);
@@ -589,14 +613,7 @@ export function refreshProviderCatalogsOnStartup() {
589
613
  for (const [name, provider] of providers) {
590
614
  const refreshFn = providerCatalogRefreshFn(provider);
591
615
  if (!refreshFn) continue;
592
- pending.push(
593
- Promise.resolve()
594
- .then(() => refreshFn())
595
- .catch((err) => {
596
- const msg = err instanceof Error ? err.message : String(err);
597
- process.stderr.write(`[provider:${name}] startup catalog refresh failed: ${msg}\n`);
598
- })
599
- );
616
+ pending.push(boundedCatalogRefresh(name, provider, refreshFn, 'startup catalog refresh'));
600
617
  }
601
618
  // Returns a completion promise so callers can invalidate stale model
602
619
  // caches once the fresh catalogs land. Still fire-and-forget: unawaited
@@ -621,25 +638,22 @@ export function refreshCatalogs(options = {}) {
621
638
  }
622
639
  armNextCatalogRefresh();
623
640
  const pending = [];
624
- const metadataReady = Promise.resolve()
625
- .then(() => refreshMetadataCatalog())
626
- .catch((err) => {
627
- const msg = err instanceof Error ? err.message : String(err);
628
- process.stderr.write(`[model-catalog] metadata refresh failed: ${msg}\n`);
629
- return null;
630
- });
641
+ // The shared metadata fetch is bounded the same way: providers wait for it
642
+ // at most one deadline before refreshing without fresh metadata.
643
+ const metadataReady = boundedProviderCatalogOp({
644
+ name: '(model-metadata)',
645
+ provider: null,
646
+ kind: 'refresh',
647
+ run: () => refreshMetadataCatalog(),
648
+ }).then(({ error }) => {
649
+ if (!error) return;
650
+ const msg = error instanceof Error ? error.message : String(error);
651
+ process.stderr.write(`[model-catalog] metadata refresh failed: ${msg}\n`);
652
+ });
631
653
  for (const [name, provider] of providers) {
632
654
  const refreshFn = providerCatalogRefreshFn(provider);
633
655
  if (!refreshFn) continue;
634
- pending.push(
635
- Promise.resolve()
636
- .then(() => metadataReady)
637
- .then(() => refreshFn())
638
- .catch((err) => {
639
- const msg = err instanceof Error ? err.message : String(err);
640
- process.stderr.write(`[provider:${name}] catalog refresh failed: ${msg}\n`);
641
- })
642
- );
656
+ pending.push(metadataReady.then(() => boundedCatalogRefresh(name, provider, refreshFn, 'catalog refresh')));
643
657
  }
644
658
  // Completion promise: lets callers drop stale model caches after refresh.
645
659
  _catalogRefreshPromise = Promise.allSettled([metadataReady, ...pending])
@@ -1,6 +1,5 @@
1
- export const ASK_USER_MAX_QUESTIONS = 4;
1
+ const ASK_USER_MAX_QUESTIONS = 4;
2
2
  export const ASK_USER_MAX_TEXT = 4000;
3
- export const ASK_USER_MAX_LABEL = 80;
4
3
 
5
4
  export const ASK_USER_TOOL_DEFS = Object.freeze([
6
5
  {
@@ -9,7 +8,7 @@ export const ASK_USER_TOOL_DEFS = Object.freeze([
9
8
  trigger:
10
9
  'Ask the user 1–4 multiple-choice questions and wait; when a plan needs the user to pick between clear options, instead of listing them in chat.',
11
10
  description:
12
- 'Ask the user multiple-choice questions and wait. Only while planning, for a decision only the user can make with clear options; open questions, minor choices and plan approval go in chat. Do not repeat the options in your message.',
11
+ 'Ask the user multiple-choice questions and wait. Only for a decision only the user can make, with clear options; never for minor choices or approval of requested work, and open questions go in chat. Do not repeat the options in your message.',
13
12
  annotations: {
14
13
  title: 'Ask User',
15
14
  readOnlyHint: true,
@@ -29,8 +29,7 @@ ${BATCHING[mode]}
29
29
  The mode sets how work splits; an agent's description sets its role and when
30
30
  it runs. Splitting applies to every role alike — implementation, review,
31
31
  investigation, writing — by file, module, commit range or risk area.
32
- If the plan has only one implementation scope, Lead executes it directly; a
33
- role tied to a point in the work, such as the final check, still runs then.
32
+ If the plan has only one implementation scope, Lead executes it directly.
34
33
 
35
34
  Delegation is for work whose size justifies a brief. When the brief would have
36
35
  to spell out the edits themselves — a few localized changes whose sites Lead
@@ -41,11 +40,7 @@ Dispatch all ready independent scopes in one turn. Only a scope that requires
41
40
  another's output waits. Brief each agent as the Lead rules describe.
42
41
  A running agent receives only changes within its own scope; a new independent
43
42
  need goes to a new agent. When scopes touch the same file, name one owner for
44
- that file in each brief. Agents run only the checks covering their own change;
45
- Lead runs the full suite once after integration.
43
+ that file in each brief.
46
44
  Use only available agents; when none fits a role, Lead does that work itself
47
- and never changes settings to enable one. If the reviewer is disabled or
48
- unavailable, Lead alone reviews the result against the approved scope and
49
- performs the required verification, without a substitute agent; never skip
50
- review or describe Lead's own review as independent.`;
45
+ and never changes settings to enable one.`;
51
46
  }
@@ -6,14 +6,26 @@ import { displayProgressStage, lastProgressAt } from '../../../shared/quiet-prog
6
6
  import { resolveAgentStallThresholds } from '../stall-policy.mjs';
7
7
 
8
8
  /** `rows` with progress fields on every row `runtimeEntry(sessionId)` reports
9
- * in flight; other rows pass through untouched. */
10
- export function withTurnProgress(rows, runtimeEntry, isTurnInFlight) {
9
+ * in flight. `awaitingTask` marks a row whose agent waits on its background
10
+ * work: 'wait' while its turn sits in `task wait`, 'background' while it is
11
+ * idle with a background task it handed off still running. Other rows pass
12
+ * through untouched. */
13
+ export function withTurnProgress(rows, runtimeEntry, isTurnInFlight, hasRunningTasks = () => false) {
11
14
  return rows.map((row) => {
12
15
  const entry = runtimeEntry(row?.sessionId);
13
- if (!isTurnInFlight(entry)) return row;
16
+ if (!isTurnInFlight(entry)) {
17
+ return row?.sessionId && hasRunningTasks(row.sessionId) ? { ...row, awaitingTask: 'background' } : row;
18
+ }
14
19
  const stage = typeof entry.stage === 'string' ? entry.stage : null;
20
+ // The in-flight turn's own start is the elapsed origin. The index row's
21
+ // stamp is written deferred (and a Lead row may never get one), so right
22
+ // after a wake it still holds the PREVIOUS turn's start or none.
23
+ const askStartedAt = Number(entry.askStartedAt) || 0;
24
+ const rowStart = Number(row.turnStartedAt) || Date.parse(row.turnStartedAt) || 0;
15
25
  return {
16
26
  ...row,
27
+ ...(askStartedAt > rowStart ? { turnStartedAt: new Date(askStartedAt).toISOString() } : {}),
28
+ ...(entry.taskWaits > 0 ? { awaitingTask: 'wait' } : {}),
17
29
  progressAt: lastProgressAt(entry) || null,
18
30
  // `model_thinking` while the sent request waits for its headers.
19
31
  progressStage: displayProgressStage(entry),
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { estimateMessagesTokens } from '../context-utils.mjs';
2
3
  import { estimateTokens } from '../token-estimate.mjs';
3
4
  import { persistToolResultArtifactSync, previewToolResultArtifact } from '../tool-result-offload.mjs';
@@ -12,9 +13,64 @@ import { carryMessageId, withoutMessageIdsReplacer } from '../message-ids.mjs';
12
13
  export const EXECUTION_RECOVERY_SOURCE = 'compact-execution-recovery';
13
14
  const TOOL_HISTORY_CONTEXT_RATIO = 0.05;
14
15
  // The working set of recent execution evidence does not grow with the window:
15
- // above 400k, 5% kept a large block of old tool output after every Compact.
16
- const TOOL_HISTORY_MAX_TOKENS = 20_000;
16
+ // above 800k, 5% kept a large block of old tool output after every Compact.
17
+ export const TOOL_HISTORY_MAX_TOKENS = 40_000;
17
18
  const ARCHIVE_THRESHOLD_TOKENS = 512;
19
+ // Only the newest reminder survives a Compact; it names the dumps it replaced
20
+ // (each dump holds the reminder that preceded it, so older ones stay reachable).
21
+ const MAX_EARLIER_DUMPS = 4;
22
+ const RECOVERY_PATH_RE = /available at (.+?) \(sha256:/;
23
+ const EARLIER_DUMPS_RE = /^Earlier dumps \(older first\): (.+)$/m;
24
+ const EARLIER_DUMPS_SEPARATOR = ' | ';
25
+ // Below this a reference line saves little and every blob is one more file
26
+ // for the offload prune walk to stat and read.
27
+ const ARCHIVE_BLOB_MIN_BYTES = 16_384;
28
+ const ARCHIVED_MESSAGE_KEY = 'archivedMessage';
29
+
30
+ // One message per line (JSONL): grep returns whole messages, and line N + 1
31
+ // holds message index N. JSON.stringify never emits a raw newline.
32
+ // A message whose line reaches ARCHIVE_BLOB_MIN_BYTES is stored once per
33
+ // session as its own content-addressed one-line file (`storeLine`); its dump
34
+ // line is a reference to that file. Every later dump that still carries the
35
+ // message (a retained tool result, an unchanged summary) repeats only the
36
+ // reference, so a session's dumps no longer re-save their large bodies.
37
+ function serializeExecutionArchive(messages, storeLine = null) {
38
+ return messages
39
+ .map((message) => {
40
+ const line = `${JSON.stringify(message, withoutMessageIdsReplacer)}\n`;
41
+ if (!storeLine || Buffer.byteLength(line, 'utf8') < ARCHIVE_BLOB_MIN_BYTES) return line;
42
+ const blob = storeLine(line);
43
+ if (!blob) throw new Error('compact: execution history could not be archived; original context preserved');
44
+ // The file name is the line's sha256; the call id keeps the line greppable.
45
+ const reference = { path: blob.path, toolCallId: message?.toolCallId };
46
+ return `${JSON.stringify({ [ARCHIVED_MESSAGE_KEY]: reference })}\n`;
47
+ })
48
+ .join('');
49
+ }
50
+
51
+ function archivedReference(value) {
52
+ return value && !Object.hasOwn(value, 'role') ? value[ARCHIVED_MESSAGE_KEY] : null;
53
+ }
54
+
55
+ // The archived transcript, with every referenced message read back.
56
+ export function parseExecutionArchive(text) {
57
+ return text
58
+ .split('\n')
59
+ .filter(Boolean)
60
+ .map((line) => {
61
+ const value = JSON.parse(line);
62
+ const reference = archivedReference(value);
63
+ return reference ? JSON.parse(readFileSync(reference.path, 'utf8')) : value;
64
+ });
65
+ }
66
+
67
+ // Dump paths a recovery reminder names, oldest first.
68
+ function recoveryArchivePaths(message) {
69
+ const content = String(message?.content ?? '');
70
+ const own = RECOVERY_PATH_RE.exec(content)?.[1];
71
+ const earlier = EARLIER_DUMPS_RE.exec(content)?.[1].split(EARLIER_DUMPS_SEPARATOR) ?? [];
72
+ return own ? [...earlier, own] : earlier;
73
+ }
18
74
 
19
75
  export function toolHistoryBudget(contextWindow) {
20
76
  const window = Number(contextWindow);
@@ -89,7 +145,7 @@ function withoutStaleReplay(messages) {
89
145
  // call keeps its paired result.
90
146
  function shrinkGroup(group, archive, level) {
91
147
  const reference = (offset, what, id) =>
92
- `[${what} archived without interpretation: ${archive.path}; message index ${group.start + offset}; toolCallId=${id}.`;
148
+ `[${what} archived without interpretation: ${archive.path}; message index ${group.start + offset} (line ${group.start + offset + 1}); toolCallId=${id}.`;
93
149
  return group.messages.map((message, offset) => {
94
150
  if (message.role === 'tool') {
95
151
  if (executionTokens([message]) < ARCHIVE_THRESHOLD_TOKENS) return message;
@@ -157,22 +213,27 @@ export function buildExecutionTail(
157
213
  const kept = new Map();
158
214
  let tokens = 0;
159
215
  let archive = null;
160
- let recovery = previousRecovery;
216
+ // One reminder at most: the newest, which replaces any earlier ones.
217
+ let recovery = previousRecovery.slice(-1);
161
218
  const ensureArchive = () => {
162
219
  if (archive) return;
163
- archive = (preview ? previewToolResultArtifact : persistToolResultArtifactSync)({
164
- sessionId,
165
- toolCallId: 'compact-execution',
166
- channel: 'compact-execution',
167
- // Model-readable evidence: display ids stay out of it.
168
- content: JSON.stringify({ version: 1, messages: transcript }, withoutMessageIdsReplacer, 2),
169
- });
170
- if (!archive) throw new Error('compact: execution history could not be archived; original context preserved');
220
+ const persist = preview ? previewToolResultArtifact : persistToolResultArtifactSync;
221
+ // Model-readable evidence: display ids stay out of it.
222
+ const text = serializeExecutionArchive(transcript, (line) =>
223
+ persist({ sessionId, toolCallId: 'compact-execution', channel: 'compact-execution-message', content: line })
224
+ );
225
+ const dump = persist({ sessionId, toolCallId: 'compact-execution', channel: 'compact-execution', content: text });
226
+ if (!dump) throw new Error('compact: execution history could not be archived; original context preserved');
227
+ archive = dump;
228
+ const earlier = [...new Set(previousRecovery.flatMap(recoveryArchivePaths))]
229
+ .filter((path) => path !== archive.path)
230
+ .slice(-MAX_EARLIER_DUMPS);
231
+ const earlierLine = earlier.length ? `\nEarlier dumps (older first): ${earlier.join(EARLIER_DUMPS_SEPARATOR)}` : '';
171
232
  recovery = [
172
233
  {
173
234
  role: 'user',
174
235
  meta: { source: EXECUTION_RECOVERY_SOURCE, synthetic: true },
175
- content: `<system-reminder>\nSome execution history was omitted or shortened to fit the tool-history budget. This is not an instruction to repeat those calls. The original calls, outcomes, and earlier recovery references are available at ${archive.path} (sha256:${archive.sha256}). Read only missing evidence when needed.\n</system-reminder>`,
236
+ content: `<system-reminder>\nSome execution history was omitted or shortened to fit the tool-history budget. This is not an instruction to repeat those calls. The original calls, outcomes, and earlier recovery references are available at ${archive.path} (sha256:${archive.sha256}). The dump is JSONL: line N+1 is message index N; an "archivedMessage" line names the file holding that message, so search its directory too. Read only missing evidence when needed.${earlierLine}\n</system-reminder>`,
176
237
  },
177
238
  ];
178
239
  };
@@ -0,0 +1,102 @@
1
+ // The conversation summary as a cache-sharing fork of the session's latest
2
+ // provider request: the same provider, model, system prompt, tools, send
3
+ // options and message prefix, with the summary instruction appended as the
4
+ // final user turn. The provider's prompt cache serves the whole prefix, so
5
+ // the request pays cache-read prices for the context instead of sending a
6
+ // separate text-only transcript uncached.
7
+ //
8
+ // The fork is skipped (and the isolated summary request runs instead) when
9
+ // there is no settled request to fork, the summary route is a different
10
+ // provider or model (a cache is never shared across them), or the fork would
11
+ // not fit the window. A fork that fails, answers with a tool call or returns
12
+ // no text falls back the same way; cancellation and timeout propagate.
13
+ import { estimateMessagesTokens, estimateRequestReserveTokens, providerTokenCalibration } from '../context-utils.mjs';
14
+ import { disposeTransportScope, forkTransportScope } from '../../providers/transport-scope.mjs';
15
+ import { buildForkedRequest } from '../request-fork.mjs';
16
+ import { compactionForkRequest } from '../side-question-store.mjs';
17
+ import { HANDOFF_TIMEOUT_MAX_MS, SUMMARY_OUTPUT_TOKENS } from './constants.mjs';
18
+ import { enforceCompactSummarySchema, extractResponseText, forkedCompactionPrompt } from './summary.mjs';
19
+
20
+ const FORK_TOOL_STUB = 'Result omitted from this summary request; the call and its result are kept after the summary.';
21
+
22
+ function forkSignal(parent, timeoutMs) {
23
+ const timeout = AbortSignal.timeout(Number(timeoutMs) > 0 ? Math.floor(timeoutMs) : HANDOFF_TIMEOUT_MAX_MS);
24
+ return parent ? AbortSignal.any([parent, timeout]) : timeout;
25
+ }
26
+
27
+ function forkUsage(usage) {
28
+ if (!usage) return null;
29
+ return {
30
+ inputTokens: Number(usage.inputTokens) || 0,
31
+ outputTokens: Number(usage.outputTokens) || 0,
32
+ cachedTokens: Number(usage.cachedTokens) || 0,
33
+ cacheWriteTokens: Number(usage.cacheWriteTokens) || 0,
34
+ };
35
+ }
36
+
37
+ // Why the fork cannot run on this route, or null when it can.
38
+ function forkSkipReason(request, route) {
39
+ if (!request) return 'no_settled_request';
40
+ const sameProvider = request.provider === route.provider || request.provider?.name === route.providerName;
41
+ if (!sameProvider || request.model !== route.model) return 'different_route';
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Summarize `conversationInput`'s history through a fork of the session's
47
+ * latest request on `route`. Returns `{ generated }` in the shape of
48
+ * generateFreshHandoffSummary's result, or `{ fallback }` naming why the
49
+ * isolated request must run instead.
50
+ */
51
+ export async function summarizeOnSharedPrefix({ sessionId, route, conversationInput, signal, timeoutMs }) {
52
+ const request = compactionForkRequest(sessionId);
53
+ const skip = forkSkipReason(request, route);
54
+ if (skip) return { fallback: skip };
55
+ const startedAt = Date.now();
56
+ const { messages, opts } = buildForkedRequest(request, forkedCompactionPrompt(), FORK_TOOL_STUB);
57
+ const promptTokens = estimateMessagesTokens(messages) + estimateRequestReserveTokens(request.tools);
58
+ const window = Math.floor(Number(route.contextWindow) / providerTokenCalibration(route.providerName));
59
+ if (!(promptTokens + SUMMARY_OUTPUT_TOKENS <= window)) return { fallback: 'window' };
60
+ const requestSignal = forkSignal(signal, timeoutMs);
61
+ // The fork keeps the session id (prompt-cache routing) on its own transport
62
+ // scope: no pooled socket or response chain of the main session is touched.
63
+ const transportScope = forkTransportScope(sessionId, 'compact');
64
+ let response;
65
+ try {
66
+ response = await route.provider.send(messages, request.model, request.tools.length ? request.tools : undefined, {
67
+ ...opts,
68
+ transportScope,
69
+ admissionOwner: sessionId,
70
+ signal: requestSignal,
71
+ });
72
+ } catch (error) {
73
+ if (requestSignal.aborted) throw requestSignal.reason ?? error;
74
+ return { fallback: 'error', error };
75
+ } finally {
76
+ disposeTransportScope(transportScope, route.provider);
77
+ }
78
+ requestSignal.throwIfAborted();
79
+ const usage = forkUsage(response?.usage);
80
+ if (response?.toolCalls?.length) return { fallback: 'tool_call', usage };
81
+ const raw = extractResponseText(response);
82
+ if (!raw) return { fallback: 'empty', usage };
83
+ const enforced = enforceCompactSummarySchema(raw, { head: conversationInput, tail: [] });
84
+ return {
85
+ generated: {
86
+ summary: enforced.summary,
87
+ summaryRepaired: enforced.repaired === true,
88
+ usage,
89
+ handoffGenerated: true,
90
+ diagnostics: {
91
+ mode: 'shared-prefix',
92
+ calls: 1,
93
+ sharedPrefixMessages: request.messages.length,
94
+ promptTokens,
95
+ summaryChars: enforced.summary.length,
96
+ rawSummaryChars: raw.length,
97
+ summaryRepaired: enforced.repaired === true,
98
+ durationMs: Date.now() - startedAt,
99
+ },
100
+ },
101
+ };
102
+ }
@@ -18,7 +18,7 @@ import {
18
18
  splitProtectedContext,
19
19
  } from './messages.mjs';
20
20
  import { activeTurnContinuationMessage } from './continuation.mjs';
21
- import { buildExecutionTail } from './execution-tail.mjs';
21
+ import { EXECUTION_RECOVERY_SOURCE, buildExecutionTail } from './execution-tail.mjs';
22
22
  import { latestSkillBodies } from '../../context/skill-state.mjs';
23
23
  import { effectiveBudget } from './budget.mjs';
24
24
  import { stripRuntimeUserContext, withRuntimeUserContext } from '../runtime-user-context.mjs';
@@ -48,7 +48,13 @@ function combinedSignal(parent, timeoutMs) {
48
48
 
49
49
  function splitFreshSource(messages) {
50
50
  const sanitized = redactToolCallSecretsInMessages(reconcileDedupStubs(sanitizeToolPairs(messages)));
51
- const { protectedPrefix, conversation } = splitProtectedContext(sanitized);
51
+ const split = splitProtectedContext(sanitized);
52
+ // A recovery reminder right after the system prompt reads as protected
53
+ // prefix and would outlive every pass. It belongs to the conversation, where
54
+ // buildExecutionTail replaces it with the newest compaction's reminder.
55
+ const isRecovery = (message) => message?.meta?.source === EXECUTION_RECOVERY_SOURCE;
56
+ const protectedPrefix = split.protectedPrefix.filter((message) => !isRecovery(message));
57
+ const conversation = [...split.protectedPrefix.filter(isRecovery), ...split.conversation];
52
58
  let previousSummaryMessage = null;
53
59
  let previousAck = null;
54
60
  for (let index = conversation.length - 1; index >= 0; index -= 1) {
@@ -61,6 +61,22 @@ Rules:
61
61
  - Use the same language as the active user thread when it is clear.
62
62
  - Do not mention the summary process or that context was compacted.`;
63
63
 
64
+ // The final user turn of a summary forked from the session's own cached
65
+ // request: the conversation itself is the history, so the instruction carries
66
+ // what COMPACTION_SYSTEM_PROMPT says for the isolated request.
67
+ export function forkedCompactionPrompt() {
68
+ return [
69
+ '<compaction-request>',
70
+ 'Pause the task for this one response. No tools are available here: do not call any.',
71
+ 'Write the anchored handoff summary this work continues from. The latest user request and the recent tool calls and results are kept verbatim after it, so focus on the older context that still matters.',
72
+ 'If the conversation holds an earlier summary, update it: preserve still-true details, remove stale details, and merge in the new facts.',
73
+ 'Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation.',
74
+ '',
75
+ SUMMARY_TEMPLATE,
76
+ '</compaction-request>',
77
+ ].join('\n');
78
+ }
79
+
64
80
  // The index-independent part of one transcript line. A batch fit rebuilds the
65
81
  // prompt for many overlapping slices of the same head; `lineCache` (scoped to
66
82
  // one fit sequence at a fixed perMessageChars) keeps each message's rendering
@@ -189,6 +189,15 @@ export function readTopLevelLifecycleRecord(raw) {
189
189
  // — for EVERY top-level key, since consumers reserialize and act on the
190
190
  // whole record, not only on the lifecycle three.
191
191
  if (!hasUniqueTopLevelKeys(raw)) return LIFECYCLE_SCAN_CONFLICT;
192
+ return lifecycleRecordOf(doc);
193
+ }
194
+
195
+ /**
196
+ * Step 3 alone, for a reader that has already proven `doc` is the complete,
197
+ * well-formed, duplicate-free top-level object of its document (see
198
+ * store-record-strict-scan.mjs): the record, or LIFECYCLE_SCAN_CONFLICT.
199
+ */
200
+ export function lifecycleRecordOf(doc) {
192
201
  const has = (key) => Object.hasOwn(doc, key);
193
202
  const record = { doc, id: undefined, closed: undefined, generation: undefined };
194
203
  if (has('id')) {
@@ -15,6 +15,8 @@ import {
15
15
  SUMMARY_OUTPUT_TOKENS,
16
16
  COMPACT_TARGET_MIN_TOKENS,
17
17
  } from '../compact.mjs';
18
+ import { summarizeOnSharedPrefix } from '../compact/forked-summary.mjs';
19
+ import { retireCompactionFork } from '../side-question-store.mjs';
18
20
 
19
21
  // Conversation summarization triggers independently of the post-compact target:
20
22
  // at 5% of the window, capped so a large window does not carry tens of
@@ -110,6 +112,39 @@ function summarizeConversation(target, ctx) {
110
112
  );
111
113
  }
112
114
 
115
+ function sumUsage(a, b) {
116
+ if (!a || !b) return a || b || null;
117
+ return Object.fromEntries(Object.keys(a).map((key) => [key, (a[key] || 0) + (Number(b[key]) || 0)]));
118
+ }
119
+
120
+ // Summary policy. On the session's own provider and model the summary forks
121
+ // the latest cached request (compact/forked-summary.mjs): the context is
122
+ // already in the prompt cache, so reading it costs a tenth to a twentieth of
123
+ // sending the text-only transcript uncached, and the summary sees the full
124
+ // execution context. A different summary route (the maintenance model)
125
+ // cannot share that cache; there the isolated text-only request runs: on
126
+ // the usage ledger it cost a median $0.16 and 24.5 s against a projected
127
+ // $0.19 and 27 s for a cache-read fork of the main model's median 500k-token
128
+ // context (the fork is cheaper than a same-model isolated request, $0.20).
129
+ // A fork that cannot run or fails falls back to the isolated request.
130
+ async function summarizeOnRoute(route, ctx) {
131
+ const shared = await summarizeOnSharedPrefix({
132
+ sessionId: ctx.sessionId,
133
+ route,
134
+ conversationInput: ctx.conversationInput,
135
+ signal: ctx.signal,
136
+ timeoutMs: ctx.compactPolicy.handoffTimeoutMs,
137
+ });
138
+ if (shared.generated) return shared.generated;
139
+ ctx.signal?.throwIfAborted();
140
+ const generated = await summarizeConversation(route, ctx);
141
+ return {
142
+ ...generated,
143
+ usage: sumUsage(generated.usage, shared.usage),
144
+ diagnostics: { ...generated.diagnostics, mode: 'isolated', sharedPrefixFallback: shared.fallback },
145
+ };
146
+ }
147
+
113
148
  // A maintenance summary model that is out of quota must not take the whole
114
149
  // turn down: the conversation's own model still has budget and can
115
150
  // summarize. Only exhaustion reroutes — every other compaction failure
@@ -118,7 +153,7 @@ function summarizeConversation(target, ctx) {
118
153
  async function summarizeWithQuotaFallback(ctx, routeInput) {
119
154
  const route = await resolveCompactionRoute(routeInput);
120
155
  try {
121
- return { route, generated: await summarizeConversation(route, ctx) };
156
+ return { route, generated: await summarizeOnRoute(route, ctx) };
122
157
  } catch (error) {
123
158
  ctx.signal?.throwIfAborted();
124
159
  const status = Number(error?.status || error?.httpStatus || error?.response?.status || 0);
@@ -132,7 +167,7 @@ async function summarizeWithQuotaFallback(ctx, routeInput) {
132
167
  throw error;
133
168
  }
134
169
  try {
135
- return { route: fallback, generated: await summarizeConversation(fallback, ctx) };
170
+ return { route: fallback, generated: await summarizeOnRoute(fallback, ctx) };
136
171
  } catch (fallbackError) {
137
172
  fallbackError.compactRoute = { provider: fallback.providerName, model: fallback.model };
138
173
  throw fallbackError;
@@ -236,6 +271,8 @@ export async function runFreshContextCompact({
236
271
  diagnostics: { ...result.diagnostics, noOp: true, reason: 'no_token_reduction' },
237
272
  };
238
273
  }
274
+ // The rewritten transcript is no longer what the saved request carried.
275
+ if (result.freshContext) retireCompactionFork(sessionId);
239
276
  return result;
240
277
  };
241
278
  const pipeline = {
@@ -9,7 +9,7 @@ import { beginSessionApprovalWait } from '../../manager/runtime-liveness.mjs';
9
9
 
10
10
  const isArgsObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
11
11
 
12
- async function askApproval(call, { callerSessionId, executeOpts, toolApprovalHook, decision, keepArgs = false }) {
12
+ async function askApproval(call, { callerSessionId, executeOpts, toolApprovalHook, decision }) {
13
13
  const askReason = String(decision?.reason || 'approval requested by hook').trim();
14
14
  // While the user decides, the session is waiting on them, not working.
15
15
  const releaseApprovalWait = beginSessionApprovalWait(callerSessionId);
@@ -31,33 +31,23 @@ async function askApproval(call, { callerSessionId, executeOpts, toolApprovalHoo
31
31
  }
32
32
  if (askOutcome.denial) return { denial: askOutcome.denial };
33
33
  const approval = askOutcome.approval;
34
- if (!keepArgs && approval && typeof approval === 'object' && isArgsObject(approval.args)) call.args = approval.args;
34
+ if (approval && typeof approval === 'object' && isArgsObject(approval.args)) call.args = approval.args;
35
35
  return null;
36
36
  }
37
37
 
38
38
  /**
39
- * Approval gate for a shell call (or a terminal `run`) whose exec policy
40
- * decision is 'ask' (elevated launch). Runs after the final call is known. The
41
- * approval is granted for the exact command being asked about: an approval
42
- * reply cannot rewrite the args.
43
- * @returns {Promise<{ denial: string } | { elevationApproved: boolean }>}
39
+ * Elevation pass for a shell call (or a terminal `run`) whose exec policy
40
+ * decision is 'ask' (elevated launch). Runs after the final call is known.
41
+ * Mixdog does not prompt first: the OS elevation prompt (Windows UAC) is the
42
+ * user's confirmation. A deny is never lifted.
43
+ * @returns {{ elevationApproved: boolean }}
44
44
  */
45
- export async function applyShellElevationGate(call, { callerSessionId, executeOpts, toolApprovalHook }) {
45
+ export function applyShellElevationGate(call) {
46
46
  const gated = call.name === 'shell' || (call.name === 'terminal' && call.args?.action === 'run');
47
47
  if (!gated || !isArgsObject(call.args) || call.args.command == null) {
48
48
  return { elevationApproved: false };
49
49
  }
50
- const decision = evaluateExecPolicy(String(call.args.command));
51
- if (decision.decision !== 'ask') return { elevationApproved: false };
52
- const denied = await askApproval(call, {
53
- callerSessionId,
54
- executeOpts,
55
- toolApprovalHook,
56
- decision,
57
- keepArgs: true,
58
- });
59
- if (denied) return denied;
60
- return { elevationApproved: true };
50
+ return { elevationApproved: evaluateExecPolicy(String(call.args.command)).decision === 'ask' };
61
51
  }
62
52
 
63
53
  /**
@@ -74,9 +74,8 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
74
74
  const unavailable = bridgeTargetUnavailable(sessionRef, finalCall.schemaName || finalCall.name);
75
75
  if (unavailable) return unavailable;
76
76
  }
77
- // Elevated launches ask the user first; denied / no approval UI refuses the call.
78
- const elevation = await applyShellElevationGate(call, { callerSessionId, executeOpts, toolApprovalHook });
79
- if (elevation.denial) return elevation.denial;
77
+ // Elevated launches skip Mixdog's own prompt; the OS elevation prompt confirms them.
78
+ const elevation = applyShellElevationGate(call);
80
79
  throwIfAborted(executeOpts.signal);
81
80
  const result = await runReadOnlyIoWithDeadline(call.name, executeOpts.signal || null, async (deadlineSignal) => {
82
81
  let callOpts = executeOpts;
@@ -130,6 +130,9 @@ export function buildAgentLoopOptions({
130
130
  checkpoint.schedule(true);
131
131
  relay(askOpts?.onAssistantToolCallObserved, call, detail);
132
132
  },
133
+ // A provider output item started (a tool call, a phased message): display
134
+ // only, so it bypasses the interruption journal.
135
+ onStreamItemStart: optionalFn(askOpts?.onStreamItemStart),
133
136
  onProviderSendStarted: () => {
134
137
  interruption.markProviderSendStarted();
135
138
  checkpoint.schedule(true);