mixdog 1.2.0 → 1.2.2

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 (386) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/scripts/gate-local.mjs +19 -3
  4. package/scripts/model-catalog-audit.mjs +1 -15
  5. package/src/agents/reviewer/AGENT.md +3 -3
  6. package/src/agents/reviewer/agent.json +1 -1
  7. package/src/defaults/shipped-history.json +5 -0
  8. package/src/defaults/skills/pptx/SKILL.md +6 -6
  9. package/src/defaults/skills/pptx/references/review.md +6 -4
  10. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  11. package/src/defaults/skills/setup/references/actions.md +13 -3
  12. package/src/defaults/skills/visualize/SKILL.md +12 -9
  13. package/src/repl/slash-commands.mjs +16 -1
  14. package/src/rules/shared/60-verification.md +3 -1
  15. package/src/rules/shared/70-delivery.md +4 -2
  16. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  17. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  18. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  19. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  20. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  21. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  22. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  23. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +122 -15
  24. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +54 -11
  25. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +104 -9
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +358 -140
  27. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  28. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  29. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  30. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  34. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  36. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  37. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +294 -44
  38. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  40. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  41. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  42. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  43. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  44. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  45. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  46. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  47. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  49. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  50. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  51. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  52. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  53. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  54. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  55. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  56. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +53 -18
  57. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  58. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  59. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  60. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  61. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  62. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  63. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  64. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  65. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  66. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +447 -0
  67. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  68. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  70. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  71. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  72. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  73. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  74. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  75. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +86 -19
  76. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  78. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  79. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  80. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  81. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  82. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  83. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  84. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  85. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  86. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  87. package/src/runtime/agent/orchestrator/providers/gemini.mjs +97 -51
  88. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  89. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  90. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  91. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  92. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  93. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  94. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  95. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  96. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  97. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  98. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  99. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +5 -3
  100. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  101. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  103. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  109. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  110. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  111. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  112. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  113. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +6 -3
  114. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  115. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +26 -6
  116. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  117. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  119. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  120. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  121. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  123. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  124. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  125. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  126. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  127. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  128. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  129. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  130. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  131. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  132. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  133. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  134. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  135. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +35 -24
  136. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  137. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  138. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  139. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  140. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  141. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  142. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  143. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  144. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  145. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  146. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  147. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  148. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  149. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  150. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +268 -181
  151. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  152. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  153. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  154. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  155. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +270 -0
  156. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  157. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  158. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  159. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  160. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  161. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  162. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  163. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  164. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  165. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  166. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  167. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  168. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  169. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -4
  170. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  171. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  172. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  173. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  174. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +42 -5
  175. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +14 -3
  176. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  177. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +50 -23
  178. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  179. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +22 -14
  180. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  181. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  182. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  183. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  184. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  185. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  186. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  187. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  188. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  189. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  190. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +819 -0
  191. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +12 -0
  192. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +22 -12
  193. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  194. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  195. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  196. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  197. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +41 -3
  198. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  199. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  200. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  201. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  202. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  203. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  204. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  205. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  206. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  207. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  208. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  209. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  210. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  211. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  212. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  213. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  214. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  215. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  216. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  217. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  218. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  219. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  220. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  221. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  222. package/src/runtime/attachments/store.d.mts +3 -0
  223. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  224. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  225. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  226. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  227. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  228. package/src/runtime/memory/lib/memory.mjs +1 -1
  229. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  230. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  231. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  232. package/src/runtime/office/README.md +1 -1
  233. package/src/runtime/office/shared/values.mjs +0 -5
  234. package/src/runtime/shared/atomic-file.mjs +25 -4
  235. package/src/runtime/shared/background-tasks.mjs +92 -14
  236. package/src/runtime/shared/err-text.mjs +25 -0
  237. package/src/runtime/shared/error-presentation.mjs +159 -19
  238. package/src/runtime/shared/extension-scopes.mjs +5 -3
  239. package/src/runtime/shared/file-lock.mjs +37 -11
  240. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  241. package/src/runtime/shared/llm/anthropic-effort.mjs +134 -131
  242. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  243. package/src/runtime/shared/llm/model-catalog.mjs +53 -52
  244. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  245. package/src/runtime/shared/llm/provider-catalog-registry.mjs +17 -0
  246. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  247. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  248. package/src/runtime/shared/model-display.mjs +13 -0
  249. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  250. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  251. package/src/runtime/shared/pristine-execution.mjs +28 -4
  252. package/src/runtime/shared/provider-accounts.mjs +124 -20
  253. package/src/runtime/shared/provider-api-key.mjs +12 -0
  254. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  255. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  256. package/src/runtime/shared/shell-display.mjs +9 -3
  257. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  258. package/src/runtime/shared/skill-mentions.mjs +133 -0
  259. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  260. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  261. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  262. package/src/runtime/shared/tool-primitives.mjs +35 -2
  263. package/src/runtime/shared/turn-changes.mjs +447 -219
  264. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  265. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  266. package/src/runtime/shared/turn-worktree-snapshot.mjs +160 -51
  267. package/src/runtime/shared/worker-requests.mjs +15 -2
  268. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  269. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  270. package/src/session-runtime/automation-agents.mjs +8 -2
  271. package/src/session-runtime/boot/apis.mjs +61 -0
  272. package/src/session-runtime/boot/config.mjs +5 -2
  273. package/src/session-runtime/boot/facade.mjs +1 -0
  274. package/src/session-runtime/boot/infrastructure.mjs +35 -18
  275. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  276. package/src/session-runtime/conversation-import/service.mjs +369 -0
  277. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  278. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  279. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  280. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  281. package/src/session-runtime/mcp-glue/connect.mjs +83 -16
  282. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  283. package/src/session-runtime/mcp-glue/server-catalog.mjs +135 -33
  284. package/src/session-runtime/mcp-glue.mjs +25 -2
  285. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  286. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  287. package/src/session-runtime/model-route/set-route.mjs +83 -15
  288. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  289. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  290. package/src/session-runtime/new-session-config.mjs +3 -2
  291. package/src/session-runtime/notification-bus.mjs +9 -2
  292. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  293. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  294. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  295. package/src/session-runtime/provider-readiness.mjs +16 -10
  296. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  297. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  298. package/src/session-runtime/resource-extension-sync.mjs +24 -5
  299. package/src/session-runtime/resource-mcp-api.mjs +198 -78
  300. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  301. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  302. package/src/session-runtime/resource-skills-api.mjs +23 -2
  303. package/src/session-runtime/runtime-review-api.mjs +13 -0
  304. package/src/session-runtime/schedule-session-run.mjs +1 -1
  305. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  306. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  307. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  308. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  309. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  310. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  311. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  312. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  313. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  314. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  315. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  316. package/src/session-runtime/services/provider-admin.mjs +64 -18
  317. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  318. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  319. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  320. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  321. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  322. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  323. package/src/session-runtime/skills-api.mjs +7 -3
  324. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  325. package/src/session-runtime/webhook-session-run.mjs +16 -4
  326. package/src/skill-script-entry.mjs +36 -0
  327. package/src/standalone/daemon-drain-policy.mjs +8 -0
  328. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  329. package/src/standalone/daemon-telemetry.mjs +55 -0
  330. package/src/standalone/daemon.mjs +44 -11
  331. package/src/standalone/session-protocol.mjs +12 -0
  332. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  333. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  334. package/src/standalone/session-service/agent-tree.mjs +12 -0
  335. package/src/standalone/session-service/quota-resume.mjs +8 -0
  336. package/src/standalone/session-service/retention/busy.mjs +15 -1
  337. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  338. package/src/standalone/session-service-api.mjs +5 -0
  339. package/src/standalone/session-service.mjs +1 -0
  340. package/src/tui/App.jsx +9 -1
  341. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  342. package/src/tui/app/app-view.jsx +7 -0
  343. package/src/tui/app/create-app-pickers.mjs +4 -0
  344. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  345. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  346. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  347. package/src/tui/app/extension-pickers.mjs +2 -1
  348. package/src/tui/app/model-options.mjs +13 -16
  349. package/src/tui/app/skill-mention-palette.mjs +88 -0
  350. package/src/tui/app/slash-commands.mjs +7 -2
  351. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  352. package/src/tui/app/slash-palette.mjs +23 -7
  353. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  354. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  355. package/src/tui/app/use-prompt-state.mjs +5 -0
  356. package/src/tui/components/Message.jsx +22 -1
  357. package/src/tui/components/PromptInput.jsx +21 -3
  358. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  359. package/src/tui/components/StatusLine.jsx +2 -0
  360. package/src/tui/components/TranscriptItem.jsx +9 -1
  361. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  362. package/src/tui/components/status-line/local-line.mjs +5 -2
  363. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  364. package/src/tui/components/user-message-skills.mjs +38 -0
  365. package/src/tui/dist/index.mjs +1323 -214
  366. package/src/tui/session/queue-helpers.mjs +7 -2
  367. package/src/tui/session/quota-resume.mjs +46 -14
  368. package/src/tui/session/session-api/commands.mjs +33 -24
  369. package/src/tui/session/session-api/extensions.mjs +26 -2
  370. package/src/tui/session/session-api/integrations.mjs +7 -0
  371. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  372. package/src/tui/session/session-api/routes.mjs +71 -7
  373. package/src/tui/session/session-api/settings.mjs +12 -14
  374. package/src/tui/session/session-flow/drain.mjs +21 -5
  375. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  376. package/src/tui/session/tui-steering-persist.mjs +25 -9
  377. package/src/tui/session/turn.mjs +55 -15
  378. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  379. package/src/ui/statusline-agents.mjs +15 -4
  380. package/src/ui/statusline.mjs +7 -3
  381. package/src/ui/streaming-markdown-heal.mjs +34 -3
  382. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  383. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  384. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
  385. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  386. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -175,7 +175,9 @@ export function mergePastedImages(entries) {
175
175
 
176
176
  // Byte-free image metadata for the user transcript item. Prompt content carries
177
177
  // only a content-addressed attachment ref after daemon intake; transcript items
178
- // and desktop snapshots carry just name/mime/size.
178
+ // and desktop snapshots carry name/mime/size plus, for attachment-reference
179
+ // parts, the `attachmentRef` so the desktop can rebuild thumbnails from the
180
+ // stored original.
179
181
  export function promptContentImageMeta(content, pastedImages) {
180
182
  const parts = Array.isArray(content) ? content.filter((part) => part?.type === 'image') : [];
181
183
  if (parts.length === 0) return null;
@@ -190,6 +192,7 @@ export function promptContentImageMeta(content, pastedImages) {
190
192
  name: String(meta?.filename || `Image ${index + 1}`),
191
193
  mimeType: String(part.mimeType || part.mediaType || meta?.mediaType || 'image/png'),
192
194
  bytes: Number(part.sizeBytes) || data.length,
195
+ ...(isAttachmentReference(part) ? { attachmentRef: part.attachmentRef } : {}),
193
196
  };
194
197
  });
195
198
  }
@@ -202,6 +205,8 @@ export function callCommitCallbacks(entries) {
202
205
  for (const entry of entries || []) {
203
206
  try {
204
207
  entry?.onCommitted?.();
205
- } catch {}
208
+ } catch {
209
+ // A throwing commit callback must not block the remaining entries.
210
+ }
206
211
  }
207
212
  }
@@ -11,6 +11,7 @@
11
11
  import {
12
12
  cancelQuotaResumeEntry,
13
13
  forgetQuotaSuspendedGoal,
14
+ isAgentOnlySession,
14
15
  noteQuotaSuspendedGoal,
15
16
  publicQuotaResume,
16
17
  quotaAutoResumeEnabled,
@@ -73,6 +74,20 @@ export function latestQuotaFailure(items) {
73
74
  return null;
74
75
  }
75
76
 
77
+ /** The turn key of the failed turndone that ends the transcript, when no
78
+ * later turn has begun; '' otherwise. */
79
+ export function latestFailedTurnKey(items) {
80
+ const transcript = Array.isArray(items) ? items : [];
81
+ for (let index = transcript.length - 1; index >= 0; index -= 1) {
82
+ const item = transcript[index];
83
+ if (item?.kind === 'user' || item?.archived === true) return '';
84
+ if (item?.kind !== 'turndone') continue;
85
+ if (String(item.status || '').toLowerCase() !== 'failed') return '';
86
+ return transcriptTurnKeys(transcript)[index] || '';
87
+ }
88
+ return '';
89
+ }
90
+
76
91
  /** What retrying the turn submits: its prompt when it produced no output, the
77
92
  * continuation instruction when it did, null when the prompt carried no text
78
93
  * (a media/attachment-only prompt cannot be replayed). */
@@ -125,6 +140,8 @@ export function createQuotaResumeLane(bag) {
125
140
  routeProvider: String(runtime.sessionProvider ?? runtime.provider ?? ''),
126
141
  routeModel: String(runtime.sessionModel ?? runtime.model ?? ''),
127
142
  });
143
+ // A delegated agent child never owns a resume: the Lead decides its retries.
144
+ const agentOnly = () => isAgentOnlySession({ id: currentId(), ...runtime.session });
128
145
  const queueResume = (sessionId, failure) =>
129
146
  scheduleQuotaResumeEntry({ sessionId, turnKey: failure.turnKey, ...failure.quota, ...sessionRoute() });
130
147
 
@@ -141,6 +158,21 @@ export function createQuotaResumeLane(bag) {
141
158
  forgetQuotaSuspendedGoal(sessionId);
142
159
  };
143
160
 
161
+ // Anything running, settling, closing or queued owns the session first.
162
+ const retryBlocked = () => {
163
+ const state = getState();
164
+ return (
165
+ flags.disposed ||
166
+ settlement !== null ||
167
+ (flags.admission || 'open') !== 'open' ||
168
+ state.busy ||
169
+ state.commandBusy ||
170
+ flags.autoClearRunning ||
171
+ flags.draining ||
172
+ pending.length > 0
173
+ );
174
+ };
175
+
144
176
  /** The one retry operation. `automatic` is the scheduler's resume: it
145
177
  * enqueues directly, because the hours it waited must not trigger the idle
146
178
  * auto-clear a user submission starts. */
@@ -166,7 +198,7 @@ export function createQuotaResumeLane(bag) {
166
198
  settlement = null;
167
199
  if (!id || !failure) return null;
168
200
  settlement = { sessionId: id, turnKey: failure.turnKey, epoch: cancelEpoch };
169
- if (failure.quota.resetAt === null || !quotaAutoResumeEnabled()) return null;
201
+ if (failure.quota.resetAt === null || !quotaAutoResumeEnabled() || agentOnly()) return null;
170
202
  return queueResume(id, failure);
171
203
  },
172
204
 
@@ -188,6 +220,7 @@ export function createQuotaResumeLane(bag) {
188
220
  /** Explicit request to auto-resume the latest quota-failed turn. */
189
221
  scheduleQuotaResume(sessionId) {
190
222
  requireAddressed(sessionId);
223
+ if (agentOnly()) throw new Error('agent sessions do not auto-resume; the Lead decides retries');
191
224
  const failure = latestQuotaFailure(getState().items);
192
225
  if (!failure || failure.quota.resetAt === null) {
193
226
  throw new Error('latest turn did not fail on exhausted provider quota with a known reset time');
@@ -207,9 +240,15 @@ export function createQuotaResumeLane(bag) {
207
240
  if (sessionId) cancelFor(String(sessionId));
208
241
  },
209
242
 
243
+ /** A deferred retry request (the desktop's switch-model flow): checked
244
+ * against the session's own state when it arrives, so it runs only while
245
+ * `turnKey` is still the latest failed turn and nothing is running,
246
+ * settling or queued. False when refused. */
210
247
  async retryFailedTurn(sessionId, turnKey) {
211
248
  requireAddressed(sessionId);
212
- return (await retryFailedTurn(String(turnKey || ''))) === true;
249
+ const key = String(turnKey || '');
250
+ if (!key || retryBlocked() || latestFailedTurnKey(getState().items) !== key) return false;
251
+ return (await retryFailedTurn(key)) === true;
213
252
  },
214
253
 
215
254
  /** Scheduler fire for the entry of `generation`, after a usable reading.
@@ -219,19 +258,12 @@ export function createQuotaResumeLane(bag) {
219
258
  const id = currentId();
220
259
  const entry = quotaResumeEntry(id);
221
260
  if (!id || !entry || entry.generation !== generation) return 'stale';
222
- const state = getState();
223
- if (
224
- flags.disposed ||
225
- settlement !== null ||
226
- (flags.admission || 'open') !== 'open' ||
227
- state.busy ||
228
- state.commandBusy ||
229
- flags.autoClearRunning ||
230
- flags.draining ||
231
- pending.length > 0
232
- ) {
233
- return 'busy';
261
+ if (agentOnly()) {
262
+ cancelQuotaResumeEntry(id, { generation });
263
+ return 'agent-session';
234
264
  }
265
+ const state = getState();
266
+ if (retryBlocked()) return 'busy';
235
267
  const failure = latestQuotaFailure(state.items);
236
268
  if (!failure || failure.turnKey !== entry.turnKey) {
237
269
  cancelQuotaResumeEntry(id, { generation });
@@ -92,34 +92,43 @@ export function createSessionCommandsApi(bag) {
92
92
  };
93
93
  };
94
94
 
95
+ // The compaction pass itself, under a lock its caller holds. `quiet` (the
96
+ // model switch's pre-compaction) asks the compactor to skip a pass that
97
+ // would not reduce anything and then adds no row for it.
98
+ async function runCompaction(startedAt, { quiet = false } = {}) {
99
+ // Give Ink one event-loop turn to paint the compacting spinner before
100
+ // runtime.compact() starts synchronous session/transcript work (same
101
+ // yield as the auto-clear path; without it /compact looks frozen with
102
+ // no spinner until the compact already finished).
103
+ await new Promise((resolve) => setTimeout(resolve, 0));
104
+ const result = await runtime.compact({ recoverAgent: true, ...(quiet ? { requireReduction: true } : {}) });
105
+ const compactChanged = Boolean(result && !result.error && result.changed !== false);
106
+ syncContextStats({
107
+ allowEstimated: true,
108
+ invalidateExact: compactChanged,
109
+ });
110
+ set({ ...routeState(), stats: { ...getState().stats } });
111
+ if (compactChanged) {
112
+ // The compacted rows stay visible (archived) above the divider.
113
+ const { items } = archiveItemsForCompaction(getState().items, compactDoneItem(result, startedAt), null, {
114
+ archived: compactionArchivedRows(result),
115
+ });
116
+ set({ items: replaceItems(items, { preserveSpill: true }) });
117
+ } else if (!quiet || result?.error) {
118
+ pushItem(compactDoneItem(result, startedAt));
119
+ }
120
+ return result;
121
+ }
122
+
123
+ // An in-place model switch compacts first on the current model (routes.mjs
124
+ // holds the command lock and shows the compacting status meanwhile).
125
+ bag.compactBeforeModelSwitch = () => runCompaction(Date.now(), { quiet: true });
126
+
95
127
  async function compactCommand() {
96
128
  if (getState().commandBusy) return null;
97
129
  if (getState().busy) return scheduleCompactForTurnEnd(compactCommand);
98
130
  try {
99
- return await withCommandStatus('Compacting conversation', 'compacting', async (startedAt) => {
100
- // Give Ink one event-loop turn to paint the compacting spinner before
101
- // runtime.compact() starts synchronous session/transcript work (same
102
- // yield as the auto-clear path; without it /compact looks frozen with
103
- // no spinner until the compact already finished).
104
- await new Promise((resolve) => setTimeout(resolve, 0));
105
- const result = await runtime.compact({ recoverAgent: true });
106
- const compactChanged = Boolean(result && !result.error && result.changed !== false);
107
- syncContextStats({
108
- allowEstimated: true,
109
- invalidateExact: compactChanged,
110
- });
111
- set({ ...routeState(), stats: { ...getState().stats } });
112
- if (compactChanged) {
113
- // The compacted rows stay visible (archived) above the divider.
114
- const { items } = archiveItemsForCompaction(getState().items, compactDoneItem(result, startedAt), null, {
115
- archived: compactionArchivedRows(result),
116
- });
117
- set({ items: replaceItems(items, { preserveSpill: true }) });
118
- } else {
119
- pushItem(compactDoneItem(result, startedAt));
120
- }
121
- return result;
122
- });
131
+ return await withCommandStatus('Compacting conversation', 'compacting', (startedAt) => runCompaction(startedAt));
123
132
  } finally {
124
133
  // A command reply reads the published snapshot, not the draft. Commit
125
134
  // completion before the awaiting service constructs that reply.
@@ -28,6 +28,13 @@ export function createSessionExtensionsApi(bag) {
28
28
  { busyError: 'Another command is still running; try again when it finishes.' }
29
29
  );
30
30
  const pluginLabel = (result, plugin) => result?.plugin?.title || result?.plugin?.name || plugin?.name || plugin;
31
+ // MCP OAuth sign-in methods the runtime must implement (no silent fallback).
32
+ const mcpOAuth =
33
+ (name) =>
34
+ (...args) => {
35
+ if (typeof runtime[name] !== 'function') throw new Error('MCP sign-in is unavailable');
36
+ return runtime[name](...args);
37
+ };
31
38
 
32
39
  return {
33
40
  mcpStatus: () => {
@@ -55,6 +62,19 @@ export function createSessionExtensionsApi(bag) {
55
62
  (name) => runtime.removeMcpServer?.(name),
56
63
  (_status, name) => [`mcp removed: ${name}`]
57
64
  ),
65
+ // Explicit OAuth sign-in: no command lock, the flow runs on the server's
66
+ // own connect lane and the caller polls its status. The caller (TUI app)
67
+ // owns the sign-in completion notice; the store never posts one.
68
+ beginMcpOAuthLogin: async (name, options) => {
69
+ const result = await mcpOAuth('beginMcpOAuthLogin')(name, options);
70
+ if (result?.state === 'pending') pushNotice(`mcp sign-in started: ${name}`, 'info');
71
+ return result;
72
+ },
73
+ getMcpOAuthLoginStatus: mcpOAuth('getMcpOAuthLoginStatus'),
74
+ cancelMcpOAuthLogin: mcpOAuth('cancelMcpOAuthLogin'),
75
+ completeMcpOAuthLogin: mcpOAuth('completeMcpOAuthLogin'),
76
+ signOutMcpServer: lockedChange(mcpOAuth('signOutMcpServer'), (_status, name) => [`mcp signed out: ${name}`]),
77
+ probeMcpServerAuth: mcpOAuth('probeMcpServerAuth'),
58
78
  setMcpServerEnabled: async (name, enabled) => {
59
79
  // No global commandBusy: the runtime adopts config synchronously and
60
80
  // serializes the heavy connect/close/recreate per server name, so rapid
@@ -70,6 +90,8 @@ export function createSessionExtensionsApi(bag) {
70
90
  const row = status?.servers?.find((s) => s.name === name);
71
91
  if (enabled && row && (row.status === 'failed' || row.error)) {
72
92
  pushNotice(`mcp enable failed: ${name}${row.error ? ` — ${row.error}` : ''}`, 'error');
93
+ } else if (enabled && row?.authRequired) {
94
+ pushNotice(`mcp enabled: ${name} — sign-in required`, 'warn');
73
95
  } else {
74
96
  pushNotice(`mcp ${enabled ? 'enabled' : 'disabled'}: ${name}`, 'info');
75
97
  }
@@ -83,8 +105,10 @@ export function createSessionExtensionsApi(bag) {
83
105
  }
84
106
  return await runtime.setExtensionScope(kind, name, projects);
85
107
  },
86
- skillsStatus: () => {
87
- return runtime.skillsStatus?.() || { cwd: getState().cwd, count: 0, skills: [] };
108
+ // `options.cwd`: list and scope skills for that folder instead of this
109
+ // session's (the desktop resolves a new-task draft's Project to it).
110
+ skillsStatus: (options) => {
111
+ return runtime.skillsStatus?.(options) || { cwd: getState().cwd, count: 0, skills: [] };
88
112
  },
89
113
  skillContent: (name) => {
90
114
  return runtime.skillContent?.(name);
@@ -78,6 +78,13 @@ export function createSessionIntegrationsApi(bag, { oauthFlows }) {
78
78
  revision
79
79
  );
80
80
  },
81
+ clearSessionChangeHistory: async () => {
82
+ // A queued or running prompt/command may still be about to record changes.
83
+ if (getState().busy || getState().commandBusy) {
84
+ throw new Error('Change history cannot be deleted while this session is running a turn');
85
+ }
86
+ return await requireRuntimeMethod('clearSessionChangeHistory', 'Change history deletion is unavailable')();
87
+ },
81
88
  getOnboardingStatus: () => {
82
89
  return runtime.getOnboardingStatus?.() || { completed: true, workflowRoutes: {} };
83
90
  },
@@ -8,6 +8,7 @@ import { createSessionResetActions } from './lifecycle/session-reset.mjs';
8
8
  import { createInheritanceActions } from './lifecycle/inheritance.mjs';
9
9
  import { createResumeAction } from './lifecycle/resume.mjs';
10
10
  import { createDisposeAction } from './lifecycle/dispose.mjs';
11
+ import { publishModelSwitch } from './routes.mjs';
11
12
 
12
13
  export function createSessionLifecycleApi(bag, { restoreTranscriptItems, oauthFlows }) {
13
14
  const { runtime, pushNotice, removeNotice, setProgressHint, finishToolApproval } = bag;
@@ -85,9 +86,12 @@ export function createSessionLifecycleApi(bag, { restoreTranscriptItems, oauthFl
85
86
  bag.supersedeQuotaResume?.(sessionId);
86
87
  bag.set({ commandBusy: true });
87
88
  try {
88
- await runtime.continueSessionInModel(sessionId, route);
89
+ const result = await runtime.continueSessionInModel(sessionId, route);
89
90
  bag.set({ ...bag.routeState(), stats: { ...bag.getState().stats } });
90
91
  bag.syncContextStats?.({ allowEstimated: true });
92
+ // The same live divider as a picker switch; IPC and remote callers
93
+ // see it through the published snapshot.
94
+ publishModelSwitch(bag, result?.modelSwitch);
91
95
  return true;
92
96
  } finally {
93
97
  bag.set({ commandBusy: false });
@@ -3,6 +3,9 @@
3
3
  * the serialized optimistic setRoute, web-search route, workflows, agent
4
4
  * definitions, output style and orchestration mode.
5
5
  */
6
+ import { modelSwitchItem } from '../../../runtime/agent/orchestrator/session/transcript-restore/restore.mjs';
7
+ import { selectedModel, switchesSessionModel } from '../../../session-runtime/model-route/model-switch-target.mjs';
8
+ import { latestQuotaFailure } from '../quota-resume.mjs';
6
9
  import { createApiHelpers } from './shared.mjs';
7
10
 
8
11
  // Optimistic route preview: the requested provider/model/effort/fast are
@@ -47,6 +50,12 @@ function optimisticRoutePatch(requested = {}, current = {}) {
47
50
  return patch;
48
51
  }
49
52
 
53
+ /** The live divider of an in-place model switch (restored from message meta
54
+ * after a reload); every switch path publishes it the same way. */
55
+ export function publishModelSwitch(bag, modelSwitch) {
56
+ if (modelSwitch) bag.pushItem(modelSwitchItem(bag.nextId(), modelSwitch));
57
+ }
58
+
50
59
  export function createSessionRouteApi(bag) {
51
60
  const {
52
61
  runtime,
@@ -68,32 +77,84 @@ export function createSessionRouteApi(bag) {
68
77
  let routeWrite = null;
69
78
  let routeSequence = 0;
70
79
 
80
+ // A preset selector previews as the preset's model, never as its id.
81
+ const previewedRequest = (routeOpts) => {
82
+ if (routeOpts.provider || !routeOpts.model) return routeOpts;
83
+ const target = selectedModel(runtime, routeOpts, '', '');
84
+ return target.provider ? { ...routeOpts, ...target } : routeOpts;
85
+ };
86
+
87
+ // Auto-compact on model change (default on): compact on the CURRENT model,
88
+ // whose cache and window still hold the history, before the switch. A model
89
+ // stopped on its quota cannot run it; the next turn's auto-compaction on the
90
+ // new model fits the history instead. A failure never blocks the switch.
91
+ const compactBeforeModelSwitch = async () => {
92
+ if (runtime.getCompactionSettings?.()?.onModelChange === false) return;
93
+ if (latestQuotaFailure(getState().items)) return;
94
+ if (typeof bag.compactBeforeModelSwitch !== 'function') return;
95
+ set({
96
+ commandStatus: { active: true, verb: 'Compacting conversation', startedAt: Date.now(), mode: 'compacting' },
97
+ });
98
+ flushEmitImmediate();
99
+ try {
100
+ await bag.compactBeforeModelSwitch();
101
+ } catch {
102
+ /* the switch goes ahead; next-turn auto-compaction fits the window */
103
+ } finally {
104
+ set({ commandStatus: null });
105
+ }
106
+ };
107
+
71
108
  const setRoute = async (opts) => {
72
109
  if (getState().commandBusy && !routeWrite) return false;
110
+ const routeOpts = opts && typeof opts === 'object' ? opts : {};
111
+ // Every user model choice on this surface (composer picker, /model,
112
+ // presets, the TUI picker) switches the conversation itself in place.
113
+ const switchesModel = switchesSessionModel(runtime, routeOpts);
114
+ // A running turn keeps the route it started on.
115
+ if (switchesModel && getState().busy) {
116
+ throw new Error('The model cannot change while a response is running. Try again when it finishes.');
117
+ }
73
118
  // Any route change supersedes waiting for the old route's quota reset.
74
119
  bag.supersedeQuotaResume?.(getState().sessionId);
75
120
  const token = ++routeSequence;
76
121
  const previousWrite = routeWrite;
77
- const routeOpts = opts && typeof opts === 'object' ? opts : {};
78
- const preview = optimisticRoutePatch(routeOpts, getState());
122
+ const preview = optimisticRoutePatch(previewedRequest(routeOpts), getState());
79
123
  // Preview immediately, serialize persistence without dropping a later
80
124
  // click. Earlier completions cannot replace the newest visible choice.
81
- set({ commandBusy: true, ...(preview || {}) });
125
+ // A model switch previews once its pre-compaction (which republishes the
126
+ // current route) is done.
127
+ set({ commandBusy: true, ...(switchesModel ? {} : preview || {}) });
82
128
  flushEmitImmediate();
83
129
  const write = (async () => {
84
130
  if (previousWrite) await previousWrite.catch(() => {});
85
131
  const previousRoute = routeState();
86
132
  try {
87
- // Explicit addressing initializes an empty session in place; the
88
- // runtime continues to own established-session route policy.
133
+ // Explicit addressing applies the route to this session: an empty
134
+ // one is initialized in place. Either way an established
135
+ // conversation follows the user's choice in place.
89
136
  const applyToCurrentSession = routeOpts.applyToCurrentSession === true;
90
137
  const keepMainModel = routeOpts.keepMainModel === true;
91
138
  const { applyToCurrentSession: _drop, keepMainModel: _keep, ...nextRoute } = routeOpts;
92
- const resolvedRoute = await runtime.setRoute(nextRoute, { applyToCurrentSession, keepMainModel });
139
+ if (switchesModel) {
140
+ // The destination must be accepted before the history is compacted.
141
+ await runtime.setRoute(nextRoute, { validateOnly: true });
142
+ await compactBeforeModelSwitch();
143
+ if (token === routeSequence && preview) {
144
+ set(preview);
145
+ flushEmitImmediate();
146
+ }
147
+ }
148
+ const resolvedRoute = await runtime.setRoute(nextRoute, {
149
+ applyToCurrentSession,
150
+ keepMainModel,
151
+ switchEstablished: true,
152
+ });
93
153
  if (token === routeSequence) {
94
- if (applyToCurrentSession) syncContextStats({ allowEstimated: true });
154
+ if (applyToCurrentSession || resolvedRoute?.modelSwitch) syncContextStats({ allowEstimated: true });
95
155
  publishRoute();
96
156
  }
157
+ publishModelSwitch(bag, resolvedRoute?.modelSwitch);
97
158
  return resolvedRoute;
98
159
  } catch (error) {
99
160
  if (token === routeSequence && preview) {
@@ -116,6 +177,9 @@ export function createSessionRouteApi(bag) {
116
177
  if (routeWrite === write) routeWrite = null;
117
178
  }
118
179
  };
180
+ // The settings surface's setModel (/model, presets) is the same user
181
+ // model choice and runs through this one path.
182
+ bag.setSessionRoute = setRoute;
119
183
 
120
184
  const setWebSearchRoute = async (opts) => {
121
185
  if (getState().commandBusy) return null;
@@ -28,20 +28,11 @@ export function createSessionSettingsApi(bag) {
28
28
  const lockedRuntimeCall = (call) => withCommandLock(async (...args) => await call(...args));
29
29
 
30
30
  return {
31
- // Model changes apply to the NEXT session only (default setRoute behavior)
32
- // — never rewrite the live session's provider/model, which would force a
33
- // full prompt-cache rewrite mid-conversation. RPC replies read the
34
- // published snapshot, not the mutable draft: commit before the caller can
35
- // acknowledge an older route.
36
- setModel: withCommandLock(
37
- async (m) => {
38
- bag.supersedeQuotaResume?.(getState().sessionId);
39
- await runtime.setRoute({ model: m });
40
- publishRoute();
41
- return true;
42
- },
43
- { busyResult: false, onRelease: flushEmitImmediate }
44
- ),
31
+ // A model selector (/model, a preset) is the user's model choice like any
32
+ // picker selection: the route surface switches an established
33
+ // conversation in place (pre-compaction, busy refusal, divider). False
34
+ // while another command holds the lock.
35
+ setModel: async (m) => (await bag.setSessionRoute({ model: m })) !== false,
45
36
  setEffort: withCommandLock(
46
37
  async (value) => {
47
38
  await runtime.setEffort(value);
@@ -154,6 +145,13 @@ export function createSessionSettingsApi(bag) {
154
145
  inspectHuggingFaceModel: (options) => runtime.inspectHuggingFaceModel(options),
155
146
  registerHuggingFaceModel: (previewId, licenseAccepted) =>
156
147
  runtime.registerHuggingFaceModel(previewId, licenseAccepted),
148
+ // Conversation import (advertised session actions; one object argument).
149
+ // Background jobs, so never behind commandBusy.
150
+ listImportableConversations: (params) => runtime.listImportableConversations(params),
151
+ getConversationImportJobs: (params) => runtime.getConversationImportJobs(params),
152
+ registerConversationImportFile: (params) => runtime.registerConversationImportFile(params),
153
+ startConversationImport: (params) => runtime.startConversationImport(params),
154
+ cancelConversationImport: (params) => runtime.cancelConversationImport(params),
157
155
  getChannelSettings: (options = {}) => {
158
156
  return (
159
157
  runtime.getChannelSettings?.(options) || {
@@ -65,7 +65,19 @@ function renderBatchItems(batch, pushUserOrSyntheticItem) {
65
65
  }
66
66
  }
67
67
 
68
- function turnOptionsForBatch(batch, steering) {
68
+ const COMPLETION_WAKE_MODES = new Set(['task-notification', 'pending-resume']);
69
+
70
+ // Trusted provenance of a drained batch: Goal turns keep their own source; a
71
+ // batch made only of completion wakes (task notifications, pending resumes)
72
+ // is the runtime reporting, carried as a task notification so the runtime
73
+ // stores it as one and never treats its text as a user skill selection.
74
+ function batchPromptSource(batch) {
75
+ if (isGoalQueuedEntry(batch[0])) return batch[0].mode;
76
+ if (batch.length > 0 && batch.every((entry) => COMPLETION_WAKE_MODES.has(entry?.mode))) return 'task-notification';
77
+ return undefined;
78
+ }
79
+
80
+ export function turnOptionsForBatch(batch, steering) {
69
81
  const nonEditable = batch.filter((entry) => !isQueuedEntryEditable(entry));
70
82
  // A completion resume is owned by the completion that woke it. Esc
71
83
  // consumes that ownership; unlike ordinary notifications it must never
@@ -75,7 +87,7 @@ function turnOptionsForBatch(batch, steering) {
75
87
  (entry) => entry?.abortDiscardOnAbort === true || entry?.mode === 'pending-resume'
76
88
  );
77
89
  return {
78
- promptSource: isGoalQueuedEntry(batch[0]) ? batch[0].mode : undefined,
90
+ promptSource: batchPromptSource(batch),
79
91
  retryFailedTurn: batch[0]?.retryFailedTurn === true,
80
92
  displayText: batch
81
93
  .map((entry) => entry.text)
@@ -114,8 +126,6 @@ function turnOptionsForBatch(batch, steering) {
114
126
  };
115
127
  }
116
128
 
117
- const COMPLETION_WAKE_MODES = new Set(['task-notification', 'pending-resume']);
118
-
119
129
  export function createDrainLoop(
120
130
  bag,
121
131
  { queue, steering, submissions, flushDeferredClearedSessionUi, autoClearBeforeWake }
@@ -238,6 +248,12 @@ export function createDrainLoop(
238
248
  while (pending.length > 0) {
239
249
  if (flags.drainEpoch !== drainEpoch) return;
240
250
  if (admission() !== 'open') return;
251
+ // Admission is rechecked before every batch: a command that took the
252
+ // lock while the previous turn settled (an in-place model switch and
253
+ // its pre-compaction, a reset) runs before the next turn starts. The
254
+ // finally below re-enters drain(), which waits on the same gate, and
255
+ // the commandBusy release re-kicks the queue.
256
+ if (flags.autoClearRunning || getState().commandBusy) return;
241
257
  // Drain one priority/mode bucket at a time (unified command queue):
242
258
  // prompt steering stays editable and task notifications stay
243
259
  // non-editable but model-visible.
@@ -257,7 +273,7 @@ export function createDrainLoop(
257
273
  // after a long idle is compacted first; the wake runs regardless.
258
274
  if (autoClearBeforeWake && batch.every((entry) => COMPLETION_WAKE_MODES.has(entry.mode))) {
259
275
  await autoClearBeforeWake();
260
- if (flags.drainEpoch !== drainEpoch || admission() !== 'open') {
276
+ if (flags.drainEpoch !== drainEpoch || admission() !== 'open' || getState().commandBusy) {
261
277
  queue.requeueEntriesFront(batch);
262
278
  return;
263
279
  }
@@ -5,8 +5,12 @@
5
5
  // prompt, so a continuation prompt or a merged batch never removes rows.
6
6
  const TURN_CHROME_KINDS = new Set(['user', 'turndone', 'notice']);
7
7
 
8
- /** The items with the trailing failed, output-less turn removed, or null
9
- * when nothing qualifies. */
8
+ // An in-place model switch made after the stop (the quota "switch model"
9
+ // path) is not the failed turn's output and outlives its rewind.
10
+ const isModelSwitchDivider = (item) => item?.kind === 'statusdone' && item.status === 'model-switched';
11
+
12
+ /** The items with the trailing failed, output-less turn removed (a later
13
+ * model switch divider kept), or null when nothing qualifies. */
10
14
  export function rewoundFailedTurnItems(items, text) {
11
15
  const list = Array.isArray(items) ? items : [];
12
16
  const resubmitted = String(text || '').trim();
@@ -16,6 +20,8 @@ export function rewoundFailedTurnItems(items, text) {
16
20
  const tail = list.slice(start + 1);
17
21
  const failed = tail.some((item) => item?.kind === 'turndone' && String(item.status || '').toLowerCase() === 'failed');
18
22
  if (!failed) return null;
19
- if (tail.some((item) => !TURN_CHROME_KINDS.has(String(item?.kind || '')))) return null;
20
- return list.slice(0, start);
23
+ if (tail.some((item) => !TURN_CHROME_KINDS.has(String(item?.kind || '')) && !isModelSwitchDivider(item))) {
24
+ return null;
25
+ }
26
+ return [...list.slice(0, start), ...tail.filter(isModelSwitchDivider)];
21
27
  }
@@ -15,11 +15,19 @@ const STALE_STEERING_RESTORE_TTL_MS = 30 * 60 * 1000;
15
15
  // Cross-process mutual exclusion still comes from the shared lock protocol;
16
16
  // this chain only orders same-process operations that were previously
17
17
  // naturally ordered by the synchronous call site.
18
- let _persistChain = Promise.resolve();
19
- function _serialize(task) {
20
- const run = _persistChain.then(task, task);
18
+ // One chain per session key so a slow write for one session never delays
19
+ // another; an entry is removed once its chain is idle.
20
+ const _persistChains = new Map();
21
+ function _serialize(key, task) {
22
+ const prev = _persistChains.get(key) || Promise.resolve();
23
+ const run = prev.then(task, task);
21
24
  // Never let a rejection poison the chain; each op logs its own error.
22
- _persistChain = run.catch(() => {});
25
+ const tail = run
26
+ .catch(() => {})
27
+ .then(() => {
28
+ if (_persistChains.get(key) === tail) _persistChains.delete(key);
29
+ });
30
+ _persistChains.set(key, tail);
23
31
  return run;
24
32
  }
25
33
 
@@ -159,7 +167,7 @@ export function appendTuiSteeringPersist(leadSessionId, entry) {
159
167
  ...(submissionId ? { submissionId } : {}),
160
168
  ...(Number.isFinite(submittedAt) && submittedAt > 0 ? { submittedAt } : {}),
161
169
  };
162
- return _serialize(async () => {
170
+ return _serialize(key, async () => {
163
171
  try {
164
172
  await updatePendingShard(key, (raw) => {
165
173
  const next = normalizePendingStore(raw);
@@ -184,7 +192,7 @@ export function dropTuiSteeringPersist(leadSessionId, entries) {
184
192
  if (!key) return Promise.resolve();
185
193
  const batch = Array.isArray(entries) ? entries : [];
186
194
  if (batch.length === 0) return Promise.resolve();
187
- return _serialize(async () => {
195
+ return _serialize(key, async () => {
188
196
  try {
189
197
  await updatePendingShard(key, (raw) => {
190
198
  const next = normalizePendingStore(raw);
@@ -207,8 +215,16 @@ export function dropTuiSteeringPersist(leadSessionId, entries) {
207
215
  });
208
216
  }
209
217
 
210
- export function flushTuiSteeringPersist() {
211
- return _persistChain.catch(() => {});
218
+ export async function flushTuiSteeringPersist() {
219
+ // Loop: chains started while waiting are awaited too.
220
+ while (_persistChains.size > 0) {
221
+ await Promise.all([..._persistChains.values()]);
222
+ }
223
+ }
224
+
225
+ // Test hook: number of live per-key chains.
226
+ export function _steeringPersistChainCount() {
227
+ return _persistChains.size;
212
228
  }
213
229
 
214
230
  function drainedRowToRestore(row) {
@@ -233,7 +249,7 @@ function drainedRowToRestore(row) {
233
249
  export function drainTuiSteeringPersist(leadSessionId) {
234
250
  const key = tuiSteeringSessionKey(leadSessionId);
235
251
  if (!key) return Promise.resolve([]);
236
- return _serialize(async () => {
252
+ return _serialize(key, async () => {
237
253
  let drained = [];
238
254
  let droppedStale = 0;
239
255
  try {