mixdog 1.4.1 → 1.4.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 (326) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/gate-local.mjs +172 -61
  4. package/scripts/lib/gate-scheduler.mjs +138 -32
  5. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  6. package/scripts/lib/run-node-tests.mjs +54 -29
  7. package/scripts/lib/scoped-temp.mjs +1 -0
  8. package/scripts/lib/test-failure-records.mjs +14 -1
  9. package/scripts/lib/test-failure-summary.mjs +16 -4
  10. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  11. package/scripts/pptx-kit-index.mjs +5 -6
  12. package/scripts/suite-health.mjs +110 -28
  13. package/scripts/test.mjs +9 -14
  14. package/src/agents/advisor/AGENT.md +4 -4
  15. package/src/agents/advisor/agent.json +1 -1
  16. package/src/agents/explore/AGENT.md +22 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/cli.mjs +7 -0
  19. package/src/defaults/agents.json +7 -0
  20. package/src/defaults/shipped-history.json +18 -2
  21. package/src/defaults/skills/browser-use/SKILL.md +4 -1
  22. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  23. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  24. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  25. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  26. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  27. package/src/defaults/skills/computer-use/SKILL.md +12 -2
  28. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  29. package/src/defaults/skills/setup/references/actions.md +12 -6
  30. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  31. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  32. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  33. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  34. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  35. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  36. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  37. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  38. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  39. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  40. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  41. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  42. package/src/defaults/skills/visualize/SKILL.md +6 -2
  43. package/src/lib/rules-builder.cjs +6 -30
  44. package/src/rules/lead/LEAD.md +3 -3
  45. package/src/rules/shared/20-research.md +3 -6
  46. package/src/rules/shared/40-editing.md +1 -0
  47. package/src/rules/shared/60-verification.md +10 -9
  48. package/src/rules/shared/70-delivery.md +4 -3
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
  52. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  54. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  55. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
  56. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  57. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  58. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  59. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  60. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  61. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  63. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
  64. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  65. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
  66. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
  67. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  69. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  70. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  74. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  75. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  78. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  80. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
  81. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  82. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  83. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  85. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
  88. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
  93. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  94. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  95. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  96. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  97. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  98. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
  99. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  100. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  102. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  103. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  104. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  105. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
  106. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
  107. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
  108. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
  109. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
  110. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  111. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  112. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  116. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  117. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  118. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  123. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
  124. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
  125. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  126. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  127. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
  128. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
  129. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  130. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  131. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
  132. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
  133. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  134. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  135. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  136. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  137. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
  138. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
  139. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  143. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  144. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  145. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  154. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  155. package/src/runtime/channels/lib/whisper-port.mjs +19 -10
  156. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  157. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  158. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  159. package/src/runtime/explore/explorer.mjs +93 -0
  160. package/src/runtime/explore/tool-defs.mjs +35 -0
  161. package/src/runtime/explore/tool.mjs +123 -0
  162. package/src/runtime/memory/index.mjs +4 -0
  163. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  164. package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
  165. package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
  166. package/src/runtime/shared/agent-route-config.mjs +17 -2
  167. package/src/runtime/shared/atomic-file.mjs +103 -2
  168. package/src/runtime/shared/background-tasks.mjs +15 -6
  169. package/src/runtime/shared/connector-accounts.mjs +5 -5
  170. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  171. package/src/runtime/shared/connector-catalog.mjs +60 -52
  172. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  173. package/src/runtime/shared/file-has-data.mjs +26 -2
  174. package/src/runtime/shared/file-lock.mjs +13 -15
  175. package/src/runtime/shared/git-launch.d.mts +9 -0
  176. package/src/runtime/shared/git-launch.mjs +213 -0
  177. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  178. package/src/runtime/shared/llm/cost.mjs +6 -1
  179. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  180. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  181. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  182. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  183. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  184. package/src/runtime/shared/provider-accounts.mjs +52 -2
  185. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  186. package/src/runtime/shared/staged-update.mjs +47 -2
  187. package/src/runtime/shared/tool-surface.mjs +110 -36
  188. package/src/runtime/shared/tool-work-units.mjs +4 -0
  189. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  190. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  191. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  192. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  193. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  194. package/src/runtime/shared/worktrees/git.mjs +57 -0
  195. package/src/runtime/shared/worktrees/manager.mjs +205 -137
  196. package/src/runtime/tidy/install.mjs +95 -15
  197. package/src/runtime/tidy/languages.mjs +17 -15
  198. package/src/runtime/tidy/process.mjs +7 -4
  199. package/src/runtime/tidy/resolve.mjs +19 -16
  200. package/src/runtime/tidy/runners/biome.mjs +3 -3
  201. package/src/runtime/tidy/tool.mjs +1 -1
  202. package/src/runtime/tidy/worktree.mjs +10 -6
  203. package/src/runtime/visualize-bridge/client.mjs +4 -1
  204. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  205. package/src/runtime/web-search/index.mjs +1 -1
  206. package/src/session-runtime/agent-task-sessions.mjs +410 -353
  207. package/src/session-runtime/boot/tools.mjs +2 -0
  208. package/src/session-runtime/bot-status.mjs +103 -103
  209. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  210. package/src/session-runtime/conversation-import/discovery.mjs +159 -40
  211. package/src/session-runtime/conversation-import/service.mjs +9 -3
  212. package/src/session-runtime/handoff-attachments.mjs +1 -1
  213. package/src/session-runtime/held-requests.mjs +3 -1
  214. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  215. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  216. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
  217. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  218. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  219. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  220. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  221. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  222. package/src/session-runtime/provider-auth/login.mjs +2 -2
  223. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  224. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  225. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  226. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  227. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  228. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  229. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  230. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  231. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  232. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  233. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  234. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  235. package/src/session-runtime/services/provider-admin.mjs +281 -10
  236. package/src/session-runtime/session-turn-api.mjs +2 -0
  237. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  238. package/src/session-runtime/settings-system-api.mjs +1 -1
  239. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  240. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  241. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  242. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  243. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  244. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  245. package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
  246. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  247. package/src/session-runtime/workspace/session-view.mjs +149 -0
  248. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  249. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  250. package/src/session-runtime/workspace/transitions.mjs +108 -1066
  251. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  252. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  253. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  254. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  255. package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
  256. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  257. package/src/standalone/daemon-session-control.mjs +151 -136
  258. package/src/standalone/daemon.mjs +7 -0
  259. package/src/standalone/desktop-service-registry.mjs +9 -0
  260. package/src/standalone/local-session-runtime.mjs +5 -1
  261. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  262. package/src/standalone/session-client.mjs +5 -1
  263. package/src/standalone/session-frame-stream.mjs +83 -11
  264. package/src/standalone/session-protocol.mjs +4 -0
  265. package/src/standalone/session-service/agent-tree.mjs +45 -9
  266. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  267. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  268. package/src/standalone/session-service/stored-reader.mjs +127 -40
  269. package/src/tui/App.jsx +154 -164
  270. package/src/tui/app/app-view/floating-panel.jsx +3 -0
  271. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  272. package/src/tui/app/clipboard.mjs +21 -10
  273. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  274. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  275. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  276. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  277. package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
  278. package/src/tui/app/provider-setup-picker.mjs +20 -13
  279. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  280. package/src/tui/app/slash-commands.mjs +1 -0
  281. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  282. package/src/tui/app/slash-dispatch.mjs +1 -1
  283. package/src/tui/app/use-btw-panel.mjs +29 -0
  284. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  285. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  286. package/src/tui/app/use-mouse-input.mjs +11 -0
  287. package/src/tui/app/use-onboarding-start.mjs +8 -3
  288. package/src/tui/app/use-panel-transition.mjs +10 -1
  289. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  290. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  291. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  292. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  293. package/src/tui/app/use-selection-focus.mjs +11 -2
  294. package/src/tui/app/use-selection-geometry.mjs +18 -9
  295. package/src/tui/app/use-selection-paint.mjs +4 -4
  296. package/src/tui/app/use-selection-stitch.mjs +3 -3
  297. package/src/tui/app/use-slash-palette.mjs +62 -0
  298. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  299. package/src/tui/app/use-transcript-activity.mjs +47 -32
  300. package/src/tui/app/use-transcript-follow.mjs +12 -4
  301. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  302. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  303. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  304. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  305. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  306. package/src/tui/app/use-transcript-window.mjs +21 -11
  307. package/src/tui/app/use-tui-ready.mjs +18 -0
  308. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  309. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  310. package/src/tui/components/Picker.jsx +27 -3
  311. package/src/tui/components/SignInUrl.jsx +68 -0
  312. package/src/tui/components/TextEntryPanel.jsx +76 -46
  313. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  314. package/src/tui/dist/index.mjs +1896 -1341
  315. package/src/tui/session/goal-turn-state.mjs +6 -2
  316. package/src/tui/session/session-api/controls.mjs +4 -0
  317. package/src/tui/session/session-api/extensions.mjs +8 -0
  318. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  319. package/src/tui/session/session-api/intake.mjs +2 -0
  320. package/src/tui/session/session-api/integrations.mjs +2 -2
  321. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  322. package/src/tui/session/session-api/settings.mjs +2 -2
  323. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  324. package/src/tui/session/transcript-spill.mjs +92 -4
  325. package/src/tui/session/transcript-store.mjs +2 -0
  326. package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
@@ -14,6 +14,7 @@ import {
14
14
  IMAGE_OMITTED_TEXT,
15
15
  IMPORT_WARNING_IMAGE_OMITTED,
16
16
  isWholeElement,
17
+ stringOrNull,
17
18
  textPart,
18
19
  titleFromText,
19
20
  toolNote,
@@ -92,7 +93,7 @@ export function isClaudeEnvelopeText(text) {
92
93
  const tagValue = (text, tag) => new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`).exec(text)?.[1].trim() || '';
93
94
 
94
95
  /** A slash-command envelope as Claude Code shows it (`/name args`), else null. */
95
- export function claudeCommandText(text) {
96
+ function claudeCommandText(text) {
96
97
  const trimmed = String(text || '').trim();
97
98
  if (!SLASH_COMMAND_RE.test(trimmed)) return null;
98
99
  const name = (tagValue(trimmed, 'command-name') || tagValue(trimmed, 'command-message')).replace(/^\//, '');
@@ -101,7 +102,6 @@ export function claudeCommandText(text) {
101
102
  }
102
103
 
103
104
  const isMessageEntry = (record) => record?.type === 'user' || record?.type === 'assistant';
104
- const stringOrNull = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
105
105
  const uuidOrNull = (value) => (typeof value === 'string' ? value : null);
106
106
  const timestampOrNull = (value) => (typeof value === 'string' || typeof value === 'number' ? value : null);
107
107
  // Rough fixed cost of one retained entry object, so tiny entries still count.
@@ -193,7 +193,7 @@ function entryParts(role, content, warnings, marks) {
193
193
  * (a cleared rename) ends the name tier without falling back to the AI title.
194
194
  * Other record types, such as `task-summary` or `agent-name`, never name it.
195
195
  */
196
- export function createClaudeTitleScan() {
196
+ function createClaudeTitleScan() {
197
197
  let custom;
198
198
  let ai;
199
199
  let summary;
@@ -5,7 +5,14 @@
5
5
  * messages, Codex's own wrapper user messages, tool outputs, reasoning,
6
6
  * event_msg, token counts and turn_context.
7
7
  */
8
- import { IMAGE_OMITTED_TEXT, IMPORT_WARNING_IMAGE_OMITTED, isWholeElement, textPart, toolNote } from './turns.mjs';
8
+ import {
9
+ IMAGE_OMITTED_TEXT,
10
+ IMPORT_WARNING_IMAGE_OMITTED,
11
+ isWholeElement,
12
+ stringOrNull,
13
+ textPart,
14
+ toolNote,
15
+ } from './turns.mjs';
9
16
 
10
17
  // User-role envelopes Codex injects itself (environment, instructions, abort
11
18
  // notices, plugin hints, shell-command echoes, image wrappers).
@@ -36,7 +43,7 @@ export function isCodexEnvelopeText(text) {
36
43
  const USER_REQUEST_HEADING = '## My request for Codex:';
37
44
 
38
45
  /** A user text part as Codex displays it. */
39
- export function codexUserDisplayText(text) {
46
+ function codexUserDisplayText(text) {
40
47
  const at = text.lastIndexOf(USER_REQUEST_HEADING);
41
48
  return at < 0 ? text : text.slice(at + USER_REQUEST_HEADING.length).trim();
42
49
  }
@@ -64,8 +71,6 @@ function toolName(payload) {
64
71
  return payload.type === 'local_shell_call' ? 'local_shell' : '';
65
72
  }
66
73
 
67
- const stringOrNull = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
68
-
69
74
  // `session_meta.thread_source` values Codex gives threads it runs for itself.
70
75
  const INTERNAL_THREAD_SOURCES = {
71
76
  subagent: 'subagent',
@@ -81,7 +86,7 @@ const INTERNAL_THREAD_SOURCES = {
81
86
  * is the approval reviewer) or `{ internal: … }`. Other sources (cli, vscode,
82
87
  * exec, mcp, custom) and features (automation, voice chat) are the user's.
83
88
  */
84
- export function codexInternalKind(payload) {
89
+ function codexInternalKind(payload) {
85
90
  const byThread = INTERNAL_THREAD_SOURCES[payload?.thread_source];
86
91
  if (byThread) return byThread;
87
92
  const source = payload?.source;
@@ -58,7 +58,7 @@ export function createConversationConverter(
58
58
  let records = 0;
59
59
  let malformed = 0;
60
60
  let tooLarge = false;
61
- let historyPartial = false;
61
+ let historyPartial = null;
62
62
  return {
63
63
  /** True while every message seen so far is a Claude sidechain message. */
64
64
  get sidechainOnly() {
@@ -67,9 +67,9 @@ export function createConversationConverter(
67
67
  markTooLarge() {
68
68
  tooLarge = true;
69
69
  },
70
- /** A history base could not be followed in full. */
71
- markHistoryPartial() {
72
- historyPartial = true;
70
+ /** A history base could not be followed in full; `warning` names why. */
71
+ markHistoryPartial(warning = IMPORT_WARNING_HISTORY_PARTIAL) {
72
+ historyPartial = warning;
73
73
  },
74
74
  /** Feed a line for the stored title only (Claude Code's tail window); other sources ignore it. */
75
75
  pushTitleLine(raw) {
@@ -117,7 +117,7 @@ export function createConversationConverter(
117
117
  if (transcriptChars(turns) > maxTranscriptChars) tooLarge = true;
118
118
  const warnings = new Set(parsed.warnings);
119
119
  if (malformed) warnings.add(IMPORT_WARNING_MALFORMED);
120
- if (historyPartial) warnings.add(IMPORT_WARNING_HISTORY_PARTIAL);
120
+ if (historyPartial) warnings.add(historyPartial);
121
121
  let errorCode = null;
122
122
  if (tooLarge) errorCode = IMPORT_ERROR_TOO_LARGE;
123
123
  else if (parsed.internalKind) errorCode = IMPORT_ERROR_INTERNAL;
@@ -11,6 +11,13 @@ export const IMPORT_WARNING_MALFORMED = 'malformed';
11
11
  export const IMPORT_WARNING_CWD_MISSING = 'cwdMissing';
12
12
  /** The conversation continues a history stored in another file that was not followed. */
13
13
  export const IMPORT_WARNING_HISTORY_PARTIAL = 'historyPartial';
14
+ /** Earlier history was left out because following it would pass the import size limit. */
15
+ export const IMPORT_WARNING_HISTORY_SIZE_LIMIT = 'historySizeLimit';
16
+ /** Earlier history was left out because its file is missing or could not be read. */
17
+ export const IMPORT_WARNING_HISTORY_UNAVAILABLE = 'historyUnavailable';
18
+
19
+ /** A compressed rollout needs a Node.js runtime with zstd; used as the failure code and as the warning of an unreadable base. */
20
+ export const IMPORT_ERROR_NEEDS_NEWER_NODE = 'needsNewerNode';
14
21
 
15
22
  /** Failure code of a record the source app writes for its own use (not a conversation). */
16
23
  export const IMPORT_ERROR_INTERNAL = 'internalRecord';
@@ -33,7 +40,7 @@ export function toolNote(name) {
33
40
  }
34
41
 
35
42
  /** Longest model-facing run line, and the longest tool name shown in it. */
36
- export const TOOL_RUN_LINE_MAX_CHARS = 400;
43
+ const TOOL_RUN_LINE_MAX_CHARS = 400;
37
44
  const TOOL_RUN_NAME_MAX_CHARS = 40;
38
45
 
39
46
  /**
@@ -67,6 +74,9 @@ function toolRunCounts(notes) {
67
74
  return [...counts].map(([name, count]) => ({ name, count }));
68
75
  }
69
76
 
77
+ /** A trimmed non-blank string, else null. */
78
+ export const stringOrNull = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
79
+
70
80
  export function textPart(text) {
71
81
  return { text: String(text), note: false };
72
82
  }
@@ -39,9 +39,10 @@ export function createEagerDispatcher({
39
39
  repeatFailLimit,
40
40
  });
41
41
  const barriers = createEagerBarriers({ cwd });
42
- // A capped turn runs its calls in order, so none past its cap starts early
43
- // (loop/turn-limits.mjs).
44
- const capped = turnLimitsOf(opts) !== null;
42
+ // A turn capped by calls or time runs its calls in order, so none past its
43
+ // cap starts early (loop/turn-limits.mjs).
44
+ const limits = turnLimitsOf(opts);
45
+ const capped = limits !== null && (limits.maxToolCalls !== Infinity || limits.wrapUpAt !== Infinity);
45
46
  const execute = (call, entry) =>
46
47
  executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, {
47
48
  toolCallId: call.id,
@@ -7,12 +7,19 @@
7
7
  // secret — the user types a value into a target field; the request names
8
8
  // the target and the field label, never a value;
9
9
  // takeover — the user does a step themselves and says "done";
10
- // permission — the user picks one of the offered scopes.
10
+ // permission — the user picks one of the offered scopes;
11
+ // connector — a bot asks for more authority on one connector tool of its
12
+ // own: the card names the tool, its current level and the one
13
+ // asked for, and the person picks the level (`scope`: one of
14
+ // `choices`) or denies. Answered once, never remembered.
11
15
  // A response is one of HUMAN_RESPONSES plus a `remember` scope; a policy lists
12
16
  // the scopes it allows and can leave out `always`. Nothing here ever carries a
13
17
  // secret value: a decision that names one is stripped to its verdict.
14
18
 
15
- const HUMAN_REQUEST_KINDS = Object.freeze(['approval', 'outbound', 'secret', 'takeover', 'permission']);
19
+ const HUMAN_REQUEST_KINDS = Object.freeze(['approval', 'outbound', 'secret', 'takeover', 'permission', 'connector']);
20
+ /** The levels a connector card may raise a tool to. */
21
+ export const CONNECTOR_WIDEN_LEVELS = Object.freeze(['needs_approval', 'auto']);
22
+ const CONNECTOR_LEVELS = ['disabled', ...CONNECTOR_WIDEN_LEVELS];
16
23
  const HUMAN_RESPONSES = Object.freeze(['approve', 'deny', 'edit-and-approve', 'provide-secret', 'skip', 'done']);
17
24
  const REMEMBER_SCOPES = Object.freeze(['once', 'agent', 'always']);
18
25
  export const SECRET_REQUEST_TIMEOUT_MS = 120_000;
@@ -36,6 +43,7 @@ const RESPONSES_BY_KIND = Object.freeze({
36
43
  secret: ['provide-secret', 'skip'],
37
44
  takeover: ['done', 'deny'],
38
45
  permission: ['approve', 'deny'],
46
+ connector: ['approve', 'deny'],
39
47
  });
40
48
  const GRANTING_RESPONSES = new Set(['approve', 'edit-and-approve', 'provide-secret', 'done']);
41
49
  const TEXT_LIMIT = 200;
@@ -89,12 +97,15 @@ function rememberScopes(value) {
89
97
  return ['once', ...new Set(listed.filter((scope) => scope !== 'once'))];
90
98
  }
91
99
 
92
- /** The kind-specific fields of a request, whitelisted. Spread onto the
93
- * presented request; `args` is replaced for a secret so no value rides it. */
100
+ /** The kind-specific fields of a request, whitelisted, plus the connector
101
+ * `account` a tool call is bound to (any kind; the card names it). Spread
102
+ * onto the presented request; `args` is replaced for a secret so no value
103
+ * rides it. */
94
104
  export function humanRequestFields(input = {}) {
105
+ const bound = text(input?.account) ? { account: text(input.account) } : {};
95
106
  const kind = input?.kind;
96
- if (!HUMAN_REQUEST_KINDS.includes(kind)) return {};
97
- const fields = { remember: rememberScopes(input.remember) };
107
+ if (!HUMAN_REQUEST_KINDS.includes(kind)) return bound;
108
+ const fields = { ...bound, remember: rememberScopes(input.remember) };
98
109
  // The agent the request acts for (its name on the card and the remembered grant).
99
110
  if (text(input.agentId)) fields.agentId = text(input.agentId);
100
111
  if (kind === 'outbound' && isRecord(input.outbound)) {
@@ -187,6 +198,20 @@ export function humanRequestFields(input = {}) {
187
198
  })),
188
199
  };
189
200
  }
201
+ if (kind === 'connector') {
202
+ const connector = isRecord(input.connector) ? input.connector : {};
203
+ const level = (value) => (CONNECTOR_LEVELS.includes(value) ? value : '');
204
+ fields.connector = {
205
+ server: text(connector.server),
206
+ tool: text(connector.tool),
207
+ from: level(connector.from),
208
+ requested: level(connector.requested),
209
+ choices: (Array.isArray(connector.choices) ? connector.choices : []).filter((choice) =>
210
+ CONNECTOR_WIDEN_LEVELS.includes(choice)
211
+ ),
212
+ };
213
+ fields.remember = ['once'];
214
+ }
190
215
  return fields;
191
216
  }
192
217
 
@@ -226,6 +251,11 @@ export function normalizeHumanResponse(request, approved, extra = {}) {
226
251
  const ids = (request?.permission?.scopes || []).map((scope) => scope.id);
227
252
  if (ids.includes(extra?.scope)) result.scope = extra.scope;
228
253
  }
254
+ // The level the person picked; none of the offered ones picks nothing.
255
+ if (kind === 'connector' && polarity) {
256
+ const choices = Array.isArray(request?.connector?.choices) ? request.connector.choices : [];
257
+ if (choices.includes(extra?.scope)) result.scope = extra.scope;
258
+ }
229
259
  return result;
230
260
  }
231
261
 
@@ -281,9 +281,11 @@ function currentSkillResolver(sessionRef, { preview = false } = {}) {
281
281
  const { resource } = delivery;
282
282
  // The tools whose use counts as use of the skill: its tool dependencies,
283
283
  // an MCP server as `mcp:<server>`.
284
- const tools = (resource.toolDependencies || []).flatMap(({ type, value }) =>
285
- type === 'tool' ? [value] : type === 'mcp' ? [`mcp:${value}`] : []
286
- );
284
+ const tools = (resource.toolDependencies || []).flatMap(({ type, value }) => {
285
+ if (type === 'tool') return [value];
286
+ if (type === 'mcp') return [`mcp:${value}`];
287
+ return [];
288
+ });
287
289
  if (preview) return { body: delivery.body, native: undefined, tools };
288
290
  const envelope = loadSkillToolDependencies(
289
291
  buildSkillToolEnvelope(name, resource.content, resource.dir, resource),
@@ -21,9 +21,20 @@ const isArgsObject = (value) => value && typeof value === 'object' && !Array.isA
21
21
 
22
22
  async function askApproval(
23
23
  call,
24
- { callerSessionId, executeOpts, toolApprovalHook, decision, request = null, gate = null, onApproved = null }
24
+ {
25
+ callerSessionId,
26
+ executeOpts,
27
+ toolApprovalHook,
28
+ decision,
29
+ request = null,
30
+ gate = null,
31
+ onApproved = null,
32
+ account = '',
33
+ }
25
34
  ) {
26
35
  const askReason = String(decision?.reason || 'approval requested by hook').trim();
36
+ // The connector account the call is bound to rides on the card.
37
+ if (account) request = { ...(request || {}), account };
27
38
  // While the user decides, the session is waiting on them, not working.
28
39
  const releaseApprovalWait = beginSessionApprovalWait(callerSessionId);
29
40
  let askOutcome;
@@ -72,9 +83,13 @@ export const CONNECTOR_APPROVAL_REASON = 'This connector tool is set to ask befo
72
83
  * remembered outbound grant answered it) is not asked a second time. With the
73
84
  * "Allow all actions" developer option on, `needs_approval` is not asked
74
85
  * either (a payment or permanent delete was asked by the outbound gate above).
86
+ * `account` is the connector account the call is bound to, for the card.
75
87
  * @returns {Promise<{ denial: string } | null>} the denial, or null to run
76
88
  */
77
- export function applyConnectorApproval(call, { agentId, callerSessionId, executeOpts, toolApprovalHook }) {
89
+ export function applyConnectorApproval(
90
+ call,
91
+ { agentId, callerSessionId, executeOpts, toolApprovalHook, account = '' }
92
+ ) {
78
93
  if (call.outboundApproved) return null;
79
94
  if (!actingBotId(callerSessionId)) return null;
80
95
  if (agentMcpToolPermission(agentId, call.name) !== 'needs_approval') return null;
@@ -89,6 +104,7 @@ export function applyConnectorApproval(call, { agentId, callerSessionId, execute
89
104
  toolApprovalHook,
90
105
  decision: { reason: CONNECTOR_APPROVAL_REASON },
91
106
  request: rememberable ? { kind: 'approval', agentId: botId, remember: ['once', 'agent'] } : null,
107
+ account,
92
108
  onApproved: async (approval) => {
93
109
  if (!rememberable || approval?.remember !== 'agent') return;
94
110
  try {
@@ -130,6 +146,7 @@ export async function applyBeforeToolHook({
130
146
  beforeToolHook,
131
147
  toolApprovalHook,
132
148
  mcpScopeId = null,
149
+ account = '',
133
150
  }) {
134
151
  const call = { name, args, cwd };
135
152
  // ask_user and handoff_user are themselves the user prompt; they never go through the ordinary approval gate.
@@ -183,6 +200,8 @@ export async function applyBeforeToolHook({
183
200
  decision,
184
201
  request,
185
202
  gate,
203
+ // Only while the call is still the one bound to it.
204
+ account: call.name === name ? account : '',
186
205
  });
187
206
  if (denied) return denied;
188
207
  }
@@ -67,6 +67,8 @@ function dispatchMcp(name, args, { cwd, callerSessionId, sessionRef, executeOpts
67
67
  signal: executeOpts.signal || null,
68
68
  ownerKey: callerSessionId,
69
69
  scopeId: mcpScopeId,
70
+ // A connector account defaults to (and becomes) this conversation's last used.
71
+ conversation: callerSessionId,
70
72
  });
71
73
  if (!isBuiltinMailRead(name, mcpScopeId)) return call;
72
74
  // The runtime's own record of the links in a mail it read for this session:
@@ -5,7 +5,7 @@
5
5
  // tool-exec/dispatch.mjs — executor per tool family
6
6
  // tool-exec/after-hook.mjs — PostToolUse result override
7
7
  // tool-exec/read-only-deadline.mjs — bounded read-only I/O with partial-output grace
8
- import { isMcpTool } from '../../mcp/client.mjs';
8
+ import { isMcpTool, resolveMcpToolAccount } from '../../mcp/client.mjs';
9
9
  import { connectorPolicyAgentOf } from '../../mcp/agent-connectors.mjs';
10
10
  import { prepareDeferredToolCallThrough } from './deferred-call-through.mjs';
11
11
  import { refreshDeferredMcpToolCatalog } from '../../runtime-core/tool-catalog.mjs';
@@ -58,6 +58,24 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
58
58
  callArgs = guarded.args;
59
59
  completionToolOpts.shellGuardNotes = guarded.notes;
60
60
  }
61
+ // A connector call is bound to its account before any hook or card sees it,
62
+ // so an approval approves that account, whatever is used last meanwhile.
63
+ // It is bound by its unique name; `account` is its label, for display.
64
+ let bound = '';
65
+ let account = '';
66
+ if (isMcpTool(name)) {
67
+ try {
68
+ const chosen = resolveMcpToolAccount(name, callArgs, {
69
+ scopeId: sessionRef?.mcpScopeId || null,
70
+ conversation: callerSessionId,
71
+ });
72
+ bound = chosen?.name || '';
73
+ account = chosen?.label || '';
74
+ } catch (error) {
75
+ return `Error: ${error?.message || String(error)}`;
76
+ }
77
+ if (bound) callArgs = { ...callArgs, account: bound };
78
+ }
61
79
  const hooked = await applyBeforeToolHook({
62
80
  name,
63
81
  args: callArgs,
@@ -67,6 +85,7 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
67
85
  beforeToolHook: hookFrom(executeOpts, sessionRef, 'beforeToolHook'),
68
86
  toolApprovalHook,
69
87
  mcpScopeId: sessionRef?.mcpScopeId || null,
88
+ account,
70
89
  });
71
90
  if (hooked.denial) return hooked.denial;
72
91
  const connectorDenied = await applyConnectorApproval(hooked, {
@@ -74,8 +93,11 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
74
93
  callerSessionId,
75
94
  executeOpts,
76
95
  toolApprovalHook,
96
+ account: hooked.name === name ? account : '',
77
97
  });
78
98
  if (connectorDenied) return connectorDenied.denial;
99
+ // A hook's rewrite of the same tool keeps the bound account.
100
+ if (bound && hooked.name === name) hooked.args = { ...hooked.args, account: bound };
79
101
  // A decision can settle after its turn was cancelled. It must never
80
102
  // authorize a new mutation, even if the hook ignores the abort signal.
81
103
  throwIfAborted(executeOpts.signal);
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * turn-limits.mjs — a turn's caps (sendOpts.turnLimits; a heartbeat turn's,
3
- * manager/ask-turn-loop-options.mjs): `maxToolCalls` tool calls and
4
- * `wrapUpAt` (epoch ms). A call past the cap, or one made after the wrap-up
5
- * time, is answered without running. Once a cap is reached the model is told
6
- * to reply, and every later request of the turn forbids tool use
7
- * (toolChoice 'none'). A response that still calls tools after that ends the
8
- * turn with no reply, its calls answered unrun. A capped turn runs no call
9
- * eagerly (eager-dispatch.mjs), so none past the cap starts while streaming.
3
+ * manager/ask-turn-loop-options.mjs): `maxToolCalls` tool calls,
4
+ * `wrapUpAt` (epoch ms) and `maxToolRounds` tool-calling responses (an
5
+ * explorer's, runtime/explore/explorer.mjs). A call past the cap, or one made
6
+ * after the wrap-up time, is answered without running. Once a cap is reached
7
+ * the model is told to reply, and every later request of the turn forbids
8
+ * tool use (toolChoice 'none'). A response that still calls tools after that
9
+ * ends the turn with no reply, its calls answered unrun. A turn capped by
10
+ * calls or time runs no call eagerly (eager-dispatch.mjs), so none past the
11
+ * cap starts while streaming; a round cap cuts only between rounds.
10
12
  */
11
13
 
12
14
  const NOTICE =
@@ -21,6 +23,8 @@ export function turnLimitsOf(opts) {
21
23
  return {
22
24
  maxToolCalls: Number.isInteger(limits.maxToolCalls) && limits.maxToolCalls >= 0 ? limits.maxToolCalls : Infinity,
23
25
  wrapUpAt: Number.isFinite(limits.wrapUpAt) ? limits.wrapUpAt : Infinity,
26
+ maxToolRounds:
27
+ Number.isInteger(limits.maxToolRounds) && limits.maxToolRounds >= 0 ? limits.maxToolRounds : Infinity,
24
28
  };
25
29
  }
26
30
 
@@ -49,7 +53,8 @@ export function settleTurnLimits(state, overrun, now = Date.now()) {
49
53
  }
50
54
  const limits = turnLimitsOf(state.opts);
51
55
  if (!limits || state.turnLimitNoticed) return;
52
- if (state.toolCallsTotal < limits.maxToolCalls && now < limits.wrapUpAt) return;
56
+ if (state.toolCallsTotal < limits.maxToolCalls && now < limits.wrapUpAt && state.iterations < limits.maxToolRounds)
57
+ return;
53
58
  state.turnLimitNoticed = true;
54
59
  state.messages.push({ role: 'user', content: NOTICE, meta: { source: 'turn-limit' } });
55
60
  }
@@ -19,6 +19,7 @@ import { armTurnDurability, claimTurnRuntime } from './ask-turn-open.mjs';
19
19
  import { prepareAskTurn, runAskAgentLoop } from './ask-turn-execute.mjs';
20
20
  import { commitAskTurn, publishAskTurn } from './ask-turn-settle.mjs';
21
21
  import { finalizeAskTurnFailure } from './ask-turn-failure.mjs';
22
+ import { armTurnTimeoutOnProviderError, reapTurnShellJobs } from './turn-reap.mjs';
22
23
  import { cacheWarmTurnKind, endCacheWarmTurn, failedTurnProvenance, noteCacheWarmTurn } from '../cache-warm.mjs';
23
24
 
24
25
  export { _api_call_with_interrupt } from './ask-call-interrupt.mjs';
@@ -125,6 +126,9 @@ async function runAskTurn({ sessionId, opened, input, askOpts, onToolCall, cwdOv
125
126
  });
126
127
  return { result: terminalResultPreview, drained };
127
128
  } catch (err) {
129
+ // The turn's terminal error: a provider timeout/stall that ended it (a
130
+ // retry the turn recovered from never reaches here) stops its shell work.
131
+ armTurnTimeoutOnProviderError(sessionId, opened.runtime, opened.turnSignal, err);
128
132
  await finalizeAskTurnFailure({
129
133
  sessionId,
130
134
  err,
@@ -138,12 +142,49 @@ async function runAskTurn({ sessionId, opened, input, askOpts, onToolCall, cwdOv
138
142
  transcriptMeta: transcript.raw,
139
143
  askStartedAt,
140
144
  });
145
+ reapTurnShellJobs(sessionId, opened.runtime);
141
146
  throw err;
142
147
  } finally {
143
- releaseWorkspace?.();
148
+ // The result is already published; this only holds the session lock until
149
+ // the reader lease is removed, so the next turn's admission follows it. A
150
+ // failed release (the admission thread stopped) never replaces the turn's
151
+ // own outcome.
152
+ try {
153
+ await releaseWorkspace?.();
154
+ } catch {
155
+ // Intentionally ignored: see the comment above.
156
+ }
144
157
  }
145
158
  }
146
159
 
160
+ /** The prompt-cache warmer learns idle gaps from real turns only; a retry is
161
+ * classified by the failed turn's stored provenance. The rebuilt retry
162
+ * message keeps that provenance (ask-turn-execute.mjs). Returns the warmer's
163
+ * turn handle. */
164
+ function noteTurnForCacheWarm(sessionId, claimed, input) {
165
+ const retryProvenance = failedTurnProvenance(claimed.preSession?.messages, input);
166
+ if (retryProvenance) input.retryProvenance = retryProvenance;
167
+ return noteCacheWarmTurn(sessionId, cacheWarmTurnKind(input, claimed.preSession?.messages));
168
+ }
169
+
170
+ /** Clear the controller only if it's still ours (closeSession may have
171
+ * swapped it). The rest of the runtime entry stays intact so agent type=list
172
+ * can still surface the final stage (done/error/cancelling). */
173
+ function detachAskRuntime(sessionId, askGeneration) {
174
+ const entry = _getRuntimeEntry(sessionId);
175
+ if (entry && entry.generation === askGeneration) {
176
+ _unlinkParentAbortListener(entry);
177
+ entry.controller = null;
178
+ // Detach the live session reference; ask is over.
179
+ entry.session = null;
180
+ }
181
+ // Final-stage runtime diagnostics are useful only while the turn is
182
+ // unwinding. Once its controller is detached, retaining the full entry
183
+ // (and any accidental references hanging from it) for the host lifetime
184
+ // turns one-shot agent traffic into an unbounded manager Map.
185
+ _evictTerminalSessionRuntime(sessionId);
186
+ }
187
+
147
188
  export async function askSession(sessionId, prompt, context, onToolCall, cwdOverride, explicitPrefetch, askOpts = {}) {
148
189
  const askStartedAt = Date.now();
149
190
  const transcript = createAskTranscriptMeta(askOpts?.transcriptMeta);
@@ -180,8 +221,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
180
221
  // Hoisted so the outer finally (which runs once after the whole turn loop)
181
222
  // can compare against the last turn's generation.
182
223
  let askGeneration = 0;
183
- let crashRecoveryChecked = false;
184
- let sessionStartEmitted = false;
224
+ // Crash-checkpoint recovery and the session-start event belong to the first
225
+ // turn of this ask only.
226
+ let firstTurn = true;
185
227
  // A failed-turn retry rewinds only the turn it retries, never a drained
186
228
  // follow-up turn.
187
229
  let retryFailedTurn = askOpts.retryFailedTurn === true;
@@ -218,17 +260,11 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
218
260
  const claimed = claimTurnRuntime({
219
261
  sessionId,
220
262
  askOpts,
221
- recoverCheckpoint: !crashRecoveryChecked,
222
- emitStart: !sessionStartEmitted,
263
+ recoverCheckpoint: firstTurn,
264
+ emitStart: firstTurn,
223
265
  });
224
- // The prompt-cache warmer learns idle gaps from real turns only; a
225
- // retry is classified by the failed turn's stored provenance.
226
- // The rebuilt retry message keeps that provenance (ask-turn-execute.mjs).
227
- const retryProvenance = failedTurnProvenance(claimed.preSession?.messages, input);
228
- if (retryProvenance) input.retryProvenance = retryProvenance;
229
- warmTurn = noteCacheWarmTurn(sessionId, cacheWarmTurnKind(input, claimed.preSession?.messages));
230
- crashRecoveryChecked = true;
231
- sessionStartEmitted = true;
266
+ warmTurn = noteTurnForCacheWarm(sessionId, claimed, input);
267
+ firstTurn = false;
232
268
  askGeneration = claimed.askGeneration;
233
269
  const opened = {
234
270
  ...claimed,
@@ -287,21 +323,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
287
323
  queue.releaseUnstarted();
288
324
  // A thrown setup/provider path must never leave a delayed checkpoint
289
325
  // writer alive after the turn lock is released.
290
- // Clear the controller only if it's still ours (closeSession may have
291
- // swapped it). Leave the rest of the runtime entry intact so agent type=list
292
- // can still surface the final stage (done/error/cancelling).
293
- const entry = _getRuntimeEntry(sessionId);
294
- if (entry && entry.generation === askGeneration) {
295
- _unlinkParentAbortListener(entry);
296
- entry.controller = null;
297
- // Detach the live session reference; ask is over.
298
- entry.session = null;
299
- }
300
- // Final-stage runtime diagnostics are useful only while the turn is
301
- // unwinding. Once its controller is detached, retaining the full entry
302
- // (and any accidental references hanging from it) for the host lifetime
303
- // turns one-shot agent traffic into an unbounded manager Map.
304
- _evictTerminalSessionRuntime(sessionId);
326
+ detachAskRuntime(sessionId, askGeneration);
305
327
  unlock();
306
328
  }
307
329
  }
@@ -578,6 +578,7 @@ function isReferenceFilesMessage(message) {
578
578
  }
579
579
 
580
580
  export { isProtectedContextUserMessage };
581
+ export { BP3_PART_SEPARATOR, bp3SystemMessage, bpEnvSystemMessage, replaceSystemMessageContent };
581
582
 
582
583
  // Compact summary messages (role:'user', content startsWith SUMMARY_PREFIX)
583
584
  // are synthetic anchors, not a real human turn — they must not count as
@@ -117,8 +117,7 @@ export function _buildBaseRules({
117
117
  .join('\n\n---\n\n');
118
118
  }
119
119
 
120
- export function _buildAgentRules(profile = 'full') {
121
- const key = String(profile || 'full');
120
+ export function _buildAgentRules() {
122
121
  const PLUGIN_ROOT = mixdogRoot();
123
122
  const DATA_DIR = resolvePluginData();
124
123
  const RULES_DIR = join(PLUGIN_ROOT, 'rules');
@@ -126,8 +125,8 @@ export function _buildAgentRules(profile = 'full') {
126
125
  'buildAgentRoleContent',
127
126
  'agent role rules',
128
127
  [join(RULES_DIR, 'agent'), join(DATA_DIR, 'mixdog-config.json')],
129
- { PLUGIN_ROOT, DATA_DIR, profile: key },
130
- key
128
+ { PLUGIN_ROOT, DATA_DIR },
129
+ 'full'
131
130
  );
132
131
  }
133
132
 
@@ -35,6 +35,7 @@ import { registerSessionPurgeHook } from '../store.mjs';
35
35
  import { finishComputerTurn } from '../../../../computer-bridge/client.mjs';
36
36
  import { configureUsageMetricsRuntime, dropMetricSeenState, bumpUsageMetricsTurnId } from './usage-metrics.mjs';
37
37
  import { runtimeState as _runtimeState, isTurnInFlight } from './runtime-state.mjs';
38
+ import { resetTurnShellReap } from './turn-reap.mjs';
38
39
 
39
40
  export { IN_FLIGHT_STAGES, inFlightSessionActivity } from './runtime-state.mjs';
40
41
 
@@ -216,6 +217,7 @@ export function markSessionAskStart(id) {
216
217
  // after provider admission (the scheduler emits stage=requesting).
217
218
  const now = Date.now();
218
219
  entry.askStartedAt = now;
220
+ resetTurnShellReap(id, entry);
219
221
  _setLogicalModelRequest(entry, null);
220
222
  entry.lastProgressAt = now;
221
223
  entry.updatedAt = now;
@@ -30,6 +30,7 @@ import { releaseReadSnapshotScope } from '../../tools/builtin/snapshot-store.mjs
30
30
  import { cancelOwnedShellTrees } from '../../tools/lib/owned-shell-trees.mjs';
31
31
  import { noteShellOwnerSessionClosed } from '../../tools/builtin/lib/shell-job-records.mjs';
32
32
  import { forgetSideQuestion } from '../side-question-store.mjs';
33
+ import { cancelBackgroundTasks } from '../../../../shared/background-tasks.mjs';
33
34
 
34
35
  /**
35
36
  * Close a session. Plants a `closed=true` tombstone on disk with a bumped
@@ -214,6 +215,12 @@ function cancelOwnedShellProcesses({ id, reason, tombstone }) {
214
215
  } catch {
215
216
  unconfirmed();
216
217
  }
218
+ // What a finished command left running is a tracked task, not an owned tree.
219
+ try {
220
+ cancelBackgroundTasks({ surface: 'shell', executionSessionId: id, reason, notify: false });
221
+ } catch {
222
+ unconfirmed();
223
+ }
217
224
  noteShellOwnerSessionClosed(id);
218
225
  }
219
226
 
@@ -14,6 +14,7 @@ import { runSessionCompaction, resolveSessionCompactionPolicy } from './compacti
14
14
  import {
15
15
  currentContextEstimateTokens,
16
16
  invalidateContextUsageSnapshot,
17
+ invalidateProviderContextBaseline,
17
18
  recordContextUsageSnapshot,
18
19
  resolveGaugeContextTokens,
19
20
  } from '../loop/compact-policy.mjs';
@@ -26,6 +27,8 @@ import {
26
27
  import { getProvider } from '../../providers/registry.mjs';
27
28
  import { isSessionCompactionBlocked, getSessionAbortSignal, _runtimeEntries } from './runtime-liveness.mjs';
28
29
  import { mintSessionId } from './session-id.mjs';
30
+ import { useOwnScratchEnvironment } from './session-scratch-environment.mjs';
31
+ import { rebindToChangedPrefix } from '../thinking-binding.mjs';
29
32
  import { hideCompactionArchive } from '../compaction-archive-capture.mjs';
30
33
  import { writeStderrBestEffort } from '../../../../shared/best-effort-stderr.mjs';
31
34
  import { forgetSideQuestion } from '../side-question-store.mjs';
@@ -234,6 +237,18 @@ function forkOutgoingTranscript(session, messages, now, archiveAs = '') {
234
237
  ...clearedTokenAccounting(now),
235
238
  };
236
239
  delete fork.liveTurnMessages;
240
+ // The spread copied the source's environment: the fork names its own
241
+ // scratch directory (its shell commands use it), not the source's.
242
+ if (useOwnScratchEnvironment(fork)) {
243
+ // The system prefix changed: thinking stored in the copy was bound to the
244
+ // source's prefix (and an open tool round ended), and every measurement
245
+ // taken against it is stale. Resume then finds a matching prefix and
246
+ // would repair nothing.
247
+ fork.messages = rebindToChangedPrefix(fork.messages);
248
+ delete fork._providerPrefixGuardState;
249
+ invalidateContextUsageSnapshot(fork);
250
+ invalidateProviderContextBaseline(fork);
251
+ }
237
252
  // The archive sidecar belongs to the source id; the fork has none.
238
253
  if (fork.compaction?.archive) {
239
254
  fork.compaction = { ...fork.compaction };