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
@@ -80,6 +80,154 @@ async function storedTitle(sessionId) {
80
80
  }
81
81
 
82
82
  /** Model-facing session operations over the same service used by desktop and TUI. */
83
+ /** `session action=list`: live sessions first, then the filtered catalog scan. */
84
+ async function listSessions({ service, liveWork, readRow }, args, context, limit) {
85
+ assertListFlags(args);
86
+ // Scoped like bots (`callerScope`, bot-status.mjs): the human's sessions
87
+ // list everything with previews, a bot (or its unattended run) lists the
88
+ // same rows (so it can find a session to send to) but only its own carry
89
+ // the bot, activity, preview and working state, anything else the plain
90
+ // rows without previews. Sub-agent sessions (a
91
+ // parent finds its own through the agent tool) and archived ones (the
92
+ // desktop's flag) only on request; the scan pages past the rows left
93
+ // out, so a page is never short while more rows match.
94
+ const own = scopedBot(context.callerScope);
95
+ const archived = (await readSessionSidebarState())?.archived ?? {};
96
+ const keep = listRowFilter(args, archived);
97
+ // Live work is overlaid on every row: a catalog row keeps the status and
98
+ // recency of its last settled turn, so a long-running agent read `idle`
99
+ // and sorted by its first prompt. The first page lists the live rows
100
+ // first (newest sign of work first); a later page keeps catalog order,
101
+ // where such a row may show again (never skipped).
102
+ const live = await liveWork();
103
+ const withLive = liveRowOverlay(live);
104
+ // Each live session's row is read by id (wherever it sits in the
105
+ // catalog), filtered like the listing, and the newest `limit` lead.
106
+ const candidates = liveCandidates(live);
107
+ let pinned = [];
108
+ if (!args.cursor && candidates.length) {
109
+ pinned = pinnedLiveRows(await Promise.all(candidates.map((id) => readRow(id))), keep, withLive, limit);
110
+ }
111
+ const pinnedIds = new Set(pinned.map((row) => row.id));
112
+ const want = limit - pinned.length;
113
+ // Bounded: past 10000 rows scanned with fewer than `limit` matching, the
114
+ // page ends (possibly empty) with a nextCursor to continue from.
115
+ const scan =
116
+ want > 0
117
+ ? await scanCatalog(service, (row) => !pinnedIds.has(row.id) && keep(row), {
118
+ cursor: args.cursor || null,
119
+ want,
120
+ maxRows: 10_000,
121
+ })
122
+ : { rows: [], nextCursor: TOP_CURSOR };
123
+ const sessions = listedSessions(pinned, scan.rows, withLive, archived);
124
+ return sessionListOutput({ sessions, nextCursor: scan.nextCursor }, limit, listOutputOptions(own, pinnedIds));
125
+ }
126
+
127
+ /** `session action=send`: queue a prompt in another session (a room member, a task session or any session). */
128
+ async function sendToSession({ service, agentTasks }, args, context, sessionId, caller) {
129
+ assertSendRequest(args, sessionId);
130
+ // A send to a room's log is a message to one member there (`to`, or a
131
+ // leading @name), run by the desktop room host; the member's turn in
132
+ // its member session is the turn that answers and reports.
133
+ const room = roomIdOfSession(sessionId) ? await roomAddressee(sessionId, args) : null;
134
+ if (room?.sessionId === caller) throw new Error('session: send must target another session');
135
+ // Work goes on to a bot (a room seat's, or a bot's home) only outside
136
+ // the caller's bot chain (`callerBotChain`, bot-chain.mjs), never back
137
+ // to the caller's own bot; the turn it starts carries that chain on.
138
+ const botChain = botChainOf(context.callerBotChain);
139
+ assertBotChainTarget(
140
+ room ? room.agentId : agentIdOfHomeSession(sessionId),
141
+ { chain: botChain, self: context.callerBotId || context.callerAgentId || '' },
142
+ 'session'
143
+ );
144
+ // An agent conversation's send reports its turn back to the agent's
145
+ // home: a task session it created by its stored origin, any other
146
+ // session by an origin for this send only.
147
+ // Forwarded files ride along as stored attachment parts (workspace-tools.mjs).
148
+ const prompt = handoffContent(room ? room.text : args.prompt, context.attachmentParts);
149
+ const turnSessionId = room ? room.sessionId : sessionId;
150
+ const from = { sessionId: caller, ...(context.callerAgentId ? { agentId: context.callerAgentId } : {}) };
151
+ const submit = room ? roomSubmitter(room, sessionId, prompt, from) : null;
152
+ const addressed = room ? { to: room.agentId } : {};
153
+ let origin = await agentTasks.reportOrigin(turnSessionId, context.callerAgentId, caller);
154
+ // `effort` is the menu's effort pick on the target: checked now against the
155
+ // levels of its model, applied just before the prompt is queued.
156
+ let applyEffort = async () => {};
157
+ if (args.effort !== undefined) {
158
+ if (room) throw new Error('session: effort applies to a session, not a group room');
159
+ // Any bot, attended or not (`callerBotId`), changes only the task
160
+ // sessions it created; reports go only to a bot conversation.
161
+ const botId = context.callerBotId || context.callerAgentId || '';
162
+ if (botId && (await agentTasks.reportOrigin(sessionId, botId, caller))?.target === 'session') {
163
+ throw new Error('session: a bot may set the effort only of the task sessions it created');
164
+ }
165
+ const target = (await service.readSession({ sessionId, transcriptItemLimit: 1 })).full;
166
+ if (!target) throw new Error(`session: ${sessionId} is unavailable`);
167
+ const level = checkEffort(args.effort, target, await readModelCatalog(service, caller));
168
+ applyEffort = effortApplier(service, sessionId, level);
169
+ }
170
+ // A send made in a bot-descended turn (`callerHop` > 0), or by a bot to
171
+ // another session, extends the chain by one; a bot's own task session
172
+ // only continues it. Refused past the limit, whatever the target.
173
+ const turnHop = Number(context.callerHop) || 0;
174
+ if (origin) {
175
+ const hop = chainedHop(origin.target === 'session' ? turnHop + 1 : turnHop);
176
+ origin = chainedOrigin(origin, hop, botChain);
177
+ // The task session a bot created acts as the sending conversation does
178
+ // from now on (attended chat or run); another's session keeps its own.
179
+ if (origin.target !== 'session') await rebindDelegation(sessionId, caller);
180
+ await applyEffort();
181
+ const sent = await agentTasks.send(turnSessionId, prompt, origin, submit);
182
+ return { sessionId, ...addressed, ...sent, ...(hop > 0 ? { hop } : {}) };
183
+ }
184
+ const hop = turnHop > 0 ? chainedHop(turnHop + 1) : 0;
185
+ await applyEffort();
186
+ const chainMeta = { ...(hop > 0 ? { hop } : {}), ...(botChain.length ? { botChain } : {}) };
187
+ let accepted;
188
+ if (submit) accepted = (await submit(hop > 0 ? { transcriptMeta: { hop } } : {})).accepted;
189
+ else {
190
+ // New work from this sender: a delegated target acts as the sender does.
191
+ await rebindDelegation(sessionId, caller);
192
+ const result = await service.submitSession({
193
+ sessionId,
194
+ prompt,
195
+ options: { mode: 'prompt', transcriptMeta: { sender: 'lead', ...chainMeta } },
196
+ });
197
+ accepted = result.accepted === true;
198
+ }
199
+ return unreportedSendResult(sessionId, addressed, accepted, hop);
200
+ }
201
+
202
+ /** `session action=status` (and the default read): one session's route, busy state and pending work. */
203
+ async function readSessionStatus({ service, readTitle, pendingWork }, action, sessionId, limit) {
204
+ const result = await service.readSession({
205
+ sessionId,
206
+ transcriptItemLimit: action === 'status' ? 1 : limit,
207
+ transcriptByteBudget: SESSION_TOOL_OUTPUT_BYTES,
208
+ });
209
+ const snapshot = result.full;
210
+ if (!snapshot) throw new Error(`session: ${sessionId} is unavailable`);
211
+ // The snapshot carries no title: it lives on the session record. Quoted
212
+ // like a listed title.
213
+ const title = await readTitle(sessionId);
214
+ return {
215
+ sessionId,
216
+ title: title ? escapeHarnessTags(truncateToolText(title, SESSION_TITLE_LIMIT)) : null,
217
+ cwd: snapshot.cwd ?? null,
218
+ provider: snapshot.provider ?? null,
219
+ model: snapshot.model ?? null,
220
+ effort: snapshot.effort ?? null,
221
+ autoEffort: snapshot.autoEffort ?? null,
222
+ fast: snapshot.fast ?? null,
223
+ busy: snapshot.busy === true,
224
+ // A Lead idle between turns while its agents run is still working:
225
+ // their results wake it, so `busy` alone would read as stalled.
226
+ agentsWorking: (await pendingWork(sessionId)) === true,
227
+ quotaResume: snapshot.quotaResume ?? null,
228
+ };
229
+ }
230
+
83
231
  export function createSessionControl({
84
232
  getSessionService,
85
233
  agentTasks = createAgentTaskSessions({ getSessionService }),
@@ -108,119 +256,10 @@ export function createSessionControl({
108
256
  const service = getSessionService();
109
257
  if (!service) throw new Error('session: session service is unavailable');
110
258
  if (action === 'list') {
111
- assertListFlags(args);
112
- // Scoped like bots (`callerScope`, bot-status.mjs): the human's sessions
113
- // list everything with previews, a bot (or its unattended run) lists the
114
- // same rows (so it can find a session to send to) but only its own carry
115
- // the bot, activity, preview and working state, anything else the plain
116
- // rows without previews. Sub-agent sessions (a
117
- // parent finds its own through the agent tool) and archived ones (the
118
- // desktop's flag) only on request; the scan pages past the rows left
119
- // out, so a page is never short while more rows match.
120
- const own = scopedBot(context.callerScope);
121
- const archived = (await readSessionSidebarState())?.archived ?? {};
122
- const keep = listRowFilter(args, archived);
123
- // Live work is overlaid on every row: a catalog row keeps the status and
124
- // recency of its last settled turn, so a long-running agent read `idle`
125
- // and sorted by its first prompt. The first page lists the live rows
126
- // first (newest sign of work first); a later page keeps catalog order,
127
- // where such a row may show again (never skipped).
128
- const live = await liveWork();
129
- const withLive = liveRowOverlay(live);
130
- // Each live session's row is read by id (wherever it sits in the
131
- // catalog), filtered like the listing, and the newest `limit` lead.
132
- const candidates = liveCandidates(live);
133
- let pinned = [];
134
- if (!args.cursor && candidates.length) {
135
- pinned = pinnedLiveRows(await Promise.all(candidates.map((id) => readRow(id))), keep, withLive, limit);
136
- }
137
- const pinnedIds = new Set(pinned.map((row) => row.id));
138
- const want = limit - pinned.length;
139
- // Bounded: past 10000 rows scanned with fewer than `limit` matching, the
140
- // page ends (possibly empty) with a nextCursor to continue from.
141
- const scan =
142
- want > 0
143
- ? await scanCatalog(service, (row) => !pinnedIds.has(row.id) && keep(row), {
144
- cursor: args.cursor || null,
145
- want,
146
- maxRows: 10_000,
147
- })
148
- : { rows: [], nextCursor: TOP_CURSOR };
149
- const sessions = listedSessions(pinned, scan.rows, withLive, archived);
150
- return sessionListOutput({ sessions, nextCursor: scan.nextCursor }, limit, listOutputOptions(own, pinnedIds));
259
+ return listSessions({ service, liveWork, readRow }, args, context, limit);
151
260
  }
152
261
  if (action === 'send') {
153
- assertSendRequest(args, sessionId);
154
- // A send to a room's log is a message to one member there (`to`, or a
155
- // leading @name), run by the desktop room host; the member's turn in
156
- // its member session is the turn that answers and reports.
157
- const room = roomIdOfSession(sessionId) ? await roomAddressee(sessionId, args) : null;
158
- if (room?.sessionId === caller) throw new Error('session: send must target another session');
159
- // Work goes on to a bot (a room seat's, or a bot's home) only outside
160
- // the caller's bot chain (`callerBotChain`, bot-chain.mjs), never back
161
- // to the caller's own bot; the turn it starts carries that chain on.
162
- const botChain = botChainOf(context.callerBotChain);
163
- assertBotChainTarget(
164
- room ? room.agentId : agentIdOfHomeSession(sessionId),
165
- { chain: botChain, self: context.callerBotId || context.callerAgentId || '' },
166
- 'session'
167
- );
168
- // An agent conversation's send reports its turn back to the agent's
169
- // home: a task session it created by its stored origin, any other
170
- // session by an origin for this send only.
171
- // Forwarded files ride along as stored attachment parts (workspace-tools.mjs).
172
- const prompt = handoffContent(room ? room.text : args.prompt, context.attachmentParts);
173
- const turnSessionId = room ? room.sessionId : sessionId;
174
- const from = { sessionId: caller, ...(context.callerAgentId ? { agentId: context.callerAgentId } : {}) };
175
- const submit = room ? roomSubmitter(room, sessionId, prompt, from) : null;
176
- const addressed = room ? { to: room.agentId } : {};
177
- let origin = await agentTasks.reportOrigin(turnSessionId, context.callerAgentId, caller);
178
- // `effort` is the menu's effort pick on the target: checked now against the
179
- // levels of its model, applied just before the prompt is queued.
180
- let applyEffort = async () => {};
181
- if (args.effort !== undefined) {
182
- if (room) throw new Error('session: effort applies to a session, not a group room');
183
- // Any bot, attended or not (`callerBotId`), changes only the task
184
- // sessions it created; reports go only to a bot conversation.
185
- const botId = context.callerBotId || context.callerAgentId || '';
186
- if (botId && (await agentTasks.reportOrigin(sessionId, botId, caller))?.target === 'session') {
187
- throw new Error('session: a bot may set the effort only of the task sessions it created');
188
- }
189
- const target = (await service.readSession({ sessionId, transcriptItemLimit: 1 })).full;
190
- if (!target) throw new Error(`session: ${sessionId} is unavailable`);
191
- const level = checkEffort(args.effort, target, await readModelCatalog(service, caller));
192
- applyEffort = effortApplier(service, sessionId, level);
193
- }
194
- // A send made in a bot-descended turn (`callerHop` > 0), or by a bot to
195
- // another session, extends the chain by one; a bot's own task session
196
- // only continues it. Refused past the limit, whatever the target.
197
- const turnHop = Number(context.callerHop) || 0;
198
- if (origin) {
199
- const hop = chainedHop(origin.target === 'session' ? turnHop + 1 : turnHop);
200
- origin = chainedOrigin(origin, hop, botChain);
201
- // The task session a bot created acts as the sending conversation does
202
- // from now on (attended chat or run); another's session keeps its own.
203
- if (origin.target !== 'session') await rebindDelegation(sessionId, caller);
204
- await applyEffort();
205
- const sent = await agentTasks.send(turnSessionId, prompt, origin, submit);
206
- return { sessionId, ...addressed, ...sent, ...(hop > 0 ? { hop } : {}) };
207
- }
208
- const hop = turnHop > 0 ? chainedHop(turnHop + 1) : 0;
209
- await applyEffort();
210
- const chainMeta = { ...(hop > 0 ? { hop } : {}), ...(botChain.length ? { botChain } : {}) };
211
- let accepted;
212
- if (submit) accepted = (await submit(hop > 0 ? { transcriptMeta: { hop } } : {})).accepted;
213
- else {
214
- // New work from this sender: a delegated target acts as the sender does.
215
- await rebindDelegation(sessionId, caller);
216
- const result = await service.submitSession({
217
- sessionId,
218
- prompt,
219
- options: { mode: 'prompt', transcriptMeta: { sender: 'lead', ...chainMeta } },
220
- });
221
- accepted = result.accepted === true;
222
- }
223
- return unreportedSendResult(sessionId, addressed, accepted, hop);
262
+ return sendToSession({ service, agentTasks }, args, context, sessionId, caller);
224
263
  }
225
264
  if (action === 'cancel') {
226
265
  // A room's exchange is run by the desktop room host, so its stop (the UI
@@ -240,31 +279,7 @@ export function createSessionControl({
240
279
  const result = await service.readSession({ sessionId, transcriptRange: { offset, limit } });
241
280
  return sessionReadOutput(sessionId, result.range, { textOffset, itemId: args.item_id });
242
281
  }
243
- const result = await service.readSession({
244
- sessionId,
245
- transcriptItemLimit: action === 'status' ? 1 : limit,
246
- transcriptByteBudget: SESSION_TOOL_OUTPUT_BYTES,
247
- });
248
- const snapshot = result.full;
249
- if (!snapshot) throw new Error(`session: ${sessionId} is unavailable`);
250
- // The snapshot carries no title: it lives on the session record. Quoted
251
- // like a listed title.
252
- const title = await readTitle(sessionId);
253
- return {
254
- sessionId,
255
- title: title ? escapeHarnessTags(truncateToolText(title, SESSION_TITLE_LIMIT)) : null,
256
- cwd: snapshot.cwd ?? null,
257
- provider: snapshot.provider ?? null,
258
- model: snapshot.model ?? null,
259
- effort: snapshot.effort ?? null,
260
- autoEffort: snapshot.autoEffort ?? null,
261
- fast: snapshot.fast ?? null,
262
- busy: snapshot.busy === true,
263
- // A Lead idle between turns while its agents run is still working:
264
- // their results wake it, so `busy` alone would read as stalled.
265
- agentsWorking: (await pendingWork(sessionId)) === true,
266
- quotaResume: snapshot.quotaResume ?? null,
267
- };
282
+ return readSessionStatus({ service, readTitle, pendingWork }, action, sessionId, limit);
268
283
  }
269
284
 
270
285
  return async function executeSessionControl(args = {}, context = {}) {
@@ -22,6 +22,13 @@ process.env.MIXDOG_DAEMON_HOST = '1';
22
22
  await import('../runtime/shared/uv-threadpool-boot.mjs');
23
23
  // Before anything can import sharp: Electron on Linux must use its wasm build.
24
24
  await import('../runtime/shared/sharp-electron-linux.mjs');
25
+ // Proxy variables (the user's, or the desktop's system proxy): fetch goes
26
+ // through the proxy while local and LAN hosts stay direct. The system proxy
27
+ // (handed over by the CLI or desktop that started this daemon, or read on
28
+ // Windows when started alone) is applied to this process's fetch only; its
29
+ // handoff variable is removed here so no child inherits it.
30
+ (await import('../runtime/shared/llm/system-proxy.mjs')).adoptSystemProxy();
31
+ (await import('../runtime/shared/llm/http-agent.mjs')).adoptEnvProxy();
25
32
 
26
33
  // No V8 compile cache here, on purpose. Measured on this runtime: a cache hit
27
34
  // saves nothing (spawn→ready 316-347ms with the cache vs 318-323ms without,
@@ -4,6 +4,7 @@ import { sanitizeForWire } from './session-wire-values.mjs';
4
4
  import { annotateSpan } from '../runtime/shared/slow-span.mjs';
5
5
  import { modelProxyClientMode } from '../runtime/shared/model-proxy-mode.mjs';
6
6
  import { getModelProxyChannel } from '../runtime/agent/orchestrator/providers/model-proxy-channel.mjs';
7
+ import { SYSTEM_PROXY_REQUEST_EVENT } from '../runtime/shared/llm/system-proxy.mjs';
7
8
 
8
9
  const requireDesktopModule = createRequire(import.meta.url);
9
10
 
@@ -40,6 +41,8 @@ function desktopEventKey(desktopId, message) {
40
41
  let terminalId = '';
41
42
  if (name === 'terminal-data') terminalId = String(message?.value?.id || '');
42
43
  else if (name === 'session-runtime-released') terminalId = String(message?.value?.sessionId || '');
44
+ // Each lookup waits for its own answer: a backed-up stream must not drop one.
45
+ else if (name === SYSTEM_PROXY_REQUEST_EVENT) terminalId = String(message?.value?.id || '');
43
46
  return `desktop-event:${desktopId}:${kind}:${name}${terminalId ? `:${terminalId}` : ''}`;
44
47
  }
45
48
  return `desktop-event:${desktopId}:${kind}`;
@@ -236,6 +239,12 @@ export class DesktopServiceRegistry {
236
239
  }
237
240
  callArgs = [callArgs[0], callArgs[1], epoch];
238
241
  }
242
+ // The resolver stays attached while this client is: its epoch is released with it.
243
+ if (name === 'invokeDesktopOperation' && callArgs[0] === 'systemProxyAttach') {
244
+ const epoch = token ? service.visibilityEpochs.get(token) : undefined;
245
+ if (epoch === undefined) throw new Error('desktop service client is not initialized');
246
+ callArgs = ['systemProxyAttach', [epoch]];
247
+ }
239
248
  // `[slow-op] desktop.invoke` names the call it timed: a desktop operation
240
249
  // (Git reads) or a session capability, not only the generic route.
241
250
  const target = name === 'invokeDesktopOperation' || name === 'invokeCapability' ? String(args?.[0] || '') : '';
@@ -39,6 +39,7 @@ import {
39
39
  tuiDebug,
40
40
  nextId,
41
41
  createTranscriptSpillBuffer,
42
+ spillWarningToStderr,
42
43
  sweepStaleTranscriptSpillDirsOnce,
43
44
  } from '../tui/session/transcript-spill.mjs';
44
45
  import { importInStages } from './staged-import.mjs';
@@ -278,7 +279,8 @@ export async function createLocalSessionRuntime({
278
279
  const { getState, getPublishedState, set, flushEmit, flushEmitImmediate } = store;
279
280
 
280
281
  const itemIndexById = new Map();
281
- const transcriptSpill = createTranscriptSpillBuffer();
282
+ // stderr is daemon.log in the daemon: a pinned spill must not stay silent.
283
+ const transcriptSpill = createTranscriptSpillBuffer({ onWarning: spillWarningToStderr });
282
284
  const { markToolCallActive, markToolCallDone, clearActiveToolSummary, resetActiveToolCalls } =
283
285
  createActiveToolTracker({ getState, set });
284
286
  const {
@@ -457,6 +459,8 @@ export async function createLocalSessionRuntime({
457
459
  snapshotTranscriptSpill: () => transcriptSpill.snapshot(),
458
460
  restoreTranscriptSpill: (snapshot) => transcriptSpill.restoreSnapshot(snapshot),
459
461
  releaseTranscriptSpill: (snapshot) => transcriptSpill.releaseSnapshot(snapshot),
462
+ // Paged views read the rows spilled out of memory (transcript-window.mjs).
463
+ readSpilledTranscript: (range) => transcriptSpill.readSpilled(range),
460
464
  presentNextToolApproval,
461
465
  finishToolApproval,
462
466
  denyAllToolApprovals,
@@ -8,6 +8,9 @@ export const DAEMON_UNAVAILABLE_MESSAGE = 'daemon session endpoint is unavailabl
8
8
 
9
9
  function exitCodeText(code) {
10
10
  // Windows reports NTSTATUS crash codes as large unsigned values.
11
+ // 0xE0000008 is Chromium's kOomExceptionCode; 134 is a V8 fatal error (abort).
12
+ if (code === 0xe0000008 || code === -536870904) return `${code === -536870904 ? code : '0xE0000008'} (out of memory)`;
13
+ if (code === 134) return '134 (V8 fatal error (JavaScript heap out of memory))';
11
14
  return Number.isSafeInteger(code) && code > 0x7fffffff ? `0x${code.toString(16).toUpperCase()}` : String(code);
12
15
  }
13
16
 
@@ -18,6 +18,7 @@ import { readSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
18
18
  import { isPidAlive } from '../runtime/shared/pid-liveness.mjs';
19
19
  import { resolveRuntimeRoot } from '../runtime/shared/runtime-root.mjs';
20
20
  import { withHeapCap } from '../runtime/shared/heap-cap.mjs';
21
+ import { systemProxyHandoffEnv } from '../runtime/shared/llm/system-proxy.mjs';
21
22
  import { daemonCrashCaptureDir, daemonDataDir } from '../session-runtime/services/daemon-crash-capture.mjs';
22
23
  import { forkDaemonCandidate } from '../session-runtime/services/daemon-candidate.mjs';
23
24
  import { daemonOwnerIsStale, proveDaemonOwnerRecord } from './daemon-owner.mjs';
@@ -338,7 +339,7 @@ async function replaceLowerDaemon(discovery, initialHealth, { log, onUpgradeWait
338
339
  // PostgreSQL stop included — so the launcher's quit stops it first.
339
340
  const inTreeDaemons = new Map();
340
341
 
341
- export function spawnDaemonCandidate({ cwd, log, timeoutMs = 30_000, entry = daemonEntry() }) {
342
+ export async function spawnDaemonCandidate({ cwd, log, timeoutMs = 30_000, entry = daemonEntry() }) {
342
343
  const t0 = performance.now();
343
344
  const at = () => Math.round(performance.now() - t0);
344
345
  const detached = daemonShouldDetach();
@@ -353,6 +354,9 @@ export function spawnDaemonCandidate({ cwd, log, timeoutMs = 30_000, entry = dae
353
354
  detached,
354
355
  env: {
355
356
  ...process.env,
357
+ // The fallback system proxy this launcher settled (the CLI), not as
358
+ // standard proxy env; a desktop is asked per origin once attached.
359
+ ...systemProxyHandoffEnv(),
356
360
  ELECTRON_RUN_AS_NODE: '1',
357
361
  MIXDOG_DAEMON_HOST: '1',
358
362
  MIXDOG_RUNTIME_ROOT: resolveRuntimeRoot(),
@@ -1,6 +1,8 @@
1
- // Session snapshots are latest-wins under backpressure; terminal byte streams
2
- // concatenate instead. Keep socket ownership and bounded backlog policy here.
1
+ // Session snapshots are latest-wins under backpressure; a session patch chains
2
+ // onto the queued frame it builds on, and terminal byte streams concatenate.
3
+ // Keep socket ownership and bounded backlog policy here.
3
4
  const TERMINAL_BACKLOG_MAX_CHARS = 256 * 1024;
5
+ const KEEPALIVE_MS = 15_000;
4
6
 
5
7
  function terminalDataFrame(frame) {
6
8
  return frame?.type === 'desktop-event' &&
@@ -26,13 +28,28 @@ function mergePendingFrame(existing, frame, json) {
26
28
  }
27
29
 
28
30
  function pendingEntry(frame, json, queuedAt) {
29
- return { frame, json, bytes: Buffer.byteLength(json) + 8, queuedAt };
31
+ const sse = `data: ${json}\n\n`;
32
+ return { frame, sse, bytes: Buffer.byteLength(sse), queuedAt };
30
33
  }
31
34
 
32
35
  function isResyncMarker(frame) {
33
36
  return frame?.resyncRequired === true || frame?.message?.resyncRequired === true;
34
37
  }
35
38
 
39
+ // A patch builds on the frame queued before it for the same key. Replacing
40
+ // that frame hands the receiver a lane it cannot decode, and the resync it
41
+ // asks for re-sends the full snapshot into the same backlog.
42
+ function isPatchFrame(frame) {
43
+ if (isResyncMarker(frame)) return false;
44
+ if (frame?.type === 'session-state') return Boolean(frame.patch) && Number.isSafeInteger(frame.baseRevision);
45
+ const wire = frame?.type === 'desktop-event' && frame.message?.kind === 'session-state' ? frame.message.wire : null;
46
+ return Boolean(wire?.__itemsPatch);
47
+ }
48
+
49
+ function readTraceIdOf(frame) {
50
+ return frame?.readTraceId ?? frame?.message?.readTraceId;
51
+ }
52
+
36
53
  function resyncEntry(frame) {
37
54
  if (frame?.type === 'desktop-event' && frame.message?.kind === 'session-state' && frame.message.sessionId) {
38
55
  // The desktop decoder cannot apply a patch against base -2, so it resets
@@ -116,13 +133,39 @@ export function createSessionFrameStream({ clients, maxPendingBytes, nowMs, onAt
116
133
  } catch {}
117
134
  }
118
135
 
136
+ function pause(client) {
137
+ if (!client.paused) client.pausedAt = nowMs();
138
+ client.paused = true;
139
+ }
140
+
141
+ // A stream paused across a keepalive period: bytes still buffered mean the
142
+ // consumer stopped reading; none buffered means a drain that never fired.
143
+ function reportStall(client, res) {
144
+ if (typeof onDiagnostic !== 'function') return;
145
+ const atMs = nowMs();
146
+ if (atMs - client.pausedAt < KEEPALIVE_MS) return;
147
+ try {
148
+ onDiagnostic({
149
+ stage: 'stream-stalled',
150
+ atMs,
151
+ pausedMs: atMs - client.pausedAt,
152
+ bufferedBytes: res.writableLength,
153
+ needDrain: res.writableNeedDrain,
154
+ pendingBytes: client.pendingBytes || 0,
155
+ maxPendingBytes,
156
+ });
157
+ } catch {
158
+ /* A diagnostic failure cannot change backpressure. */
159
+ }
160
+ }
161
+
119
162
  function flushPending(client) {
120
163
  while (client.sse && !client.paused && client.pending.size > 0) {
121
164
  const [key, entry] = client.pending.entries().next().value;
122
165
  client.pending.delete(key);
123
166
  client.pendingBytes = Math.max(0, client.pendingBytes - (entry.bytes || 0));
124
167
  try {
125
- if (client.sse.write(`data: ${entry.json}\n\n`) === false) client.paused = true;
168
+ if (client.sse.write(entry.sse) === false) pause(client);
126
169
  traceFrame(client, entry.frame, 'stream-write', entry);
127
170
  } catch {
128
171
  traceFrame(client, entry.frame, 'stream-write-failed', entry, true);
@@ -144,13 +187,18 @@ export function createSessionFrameStream({ clients, maxPendingBytes, nowMs, onAt
144
187
  client.pending.set(key, entry);
145
188
  client.pendingBytes += entry.bytes || 0;
146
189
  traceFrame(client, entry.frame, 'stream-queued', entry);
190
+ boundPending(client, key);
191
+ }
192
+
193
+ function boundPending(client, key) {
147
194
  // The cap bounds accumulated backlog: older entries collapse to resync
148
195
  // markers or drop, oldest first. The newest frame is always admitted, so a
149
196
  // snapshot larger than the cap still reaches a client with no backlog, and
150
197
  // markers are kept so a receiver always learns what it missed.
151
198
  for (const [oldestKey, oldest] of client.pending) {
152
199
  if (client.pendingBytes <= maxPendingBytes) break;
153
- if (oldestKey === key || isResyncMarker(oldest.frame)) continue;
200
+ // Only a single newest frame is exempt: a patch chain is backlog too.
201
+ if ((oldestKey === key && !oldest.chained) || isResyncMarker(oldest.frame)) continue;
154
202
  const marker = resyncEntry(oldest.frame);
155
203
  if (marker && marker.bytes < oldest.bytes) {
156
204
  traceFrame(client, oldest.frame, 'stream-resync-marker', oldest, true);
@@ -166,7 +214,26 @@ export function createSessionFrameStream({ clients, maxPendingBytes, nowMs, onAt
166
214
  }
167
215
 
168
216
  function queueFrame(client, key, frame, json) {
169
- const merged = mergePendingFrame(client.pending.get(key), frame, json);
217
+ const existing = client.pending.get(key);
218
+ if (existing && isPatchFrame(frame)) {
219
+ // A queued marker already makes the receiver fetch a fresh full
220
+ // snapshot, which supersedes every patch issued before it.
221
+ if (isResyncMarker(existing.frame)) return;
222
+ const next = pendingEntry(frame, json, nowMs());
223
+ // Map.set on a present key keeps its place: the chain leaves with its base.
224
+ client.pending.set(key, {
225
+ frame: readTraceIdOf(existing.frame) ? existing.frame : frame,
226
+ sse: existing.sse + next.sse,
227
+ bytes: existing.bytes + next.bytes,
228
+ queuedAt: existing.queuedAt,
229
+ chained: true,
230
+ });
231
+ client.pendingBytes += next.bytes;
232
+ traceFrame(client, frame, 'stream-queued', next);
233
+ boundPending(client, key);
234
+ return;
235
+ }
236
+ const merged = mergePendingFrame(existing, frame, json);
170
237
  setPending(client, key, pendingEntry(merged.frame, merged.json, nowMs()));
171
238
  }
172
239
 
@@ -177,7 +244,7 @@ export function createSessionFrameStream({ clients, maxPendingBytes, nowMs, onAt
177
244
  return;
178
245
  }
179
246
  try {
180
- if (client.sse.write(`data: ${json}\n\n`) === false) client.paused = true;
247
+ if (client.sse.write(`data: ${json}\n\n`) === false) pause(client);
181
248
  traceFrame(client, frame, 'stream-write');
182
249
  } catch {
183
250
  traceFrame(client, frame, 'stream-write-failed', {}, true);
@@ -221,7 +288,8 @@ export function createSessionFrameStream({ clients, maxPendingBytes, nowMs, onAt
221
288
  const paused = res.write(': attached\n\n') === false;
222
289
  const previous = client.sse;
223
290
  client.sse = res;
224
- client.paused = paused;
291
+ client.paused = false;
292
+ if (paused) pause(client);
225
293
  client.lastSeen = nowMs();
226
294
  // Replacement transfers ownership before closing the old stream, so its
227
295
  // close/error callbacks cannot detach the new response.
@@ -252,13 +320,17 @@ export function createSessionFrameStream({ clients, maxPendingBytes, nowMs, onAt
252
320
  res.on('close', cleanup);
253
321
  res.on('error', cleanup);
254
322
  keepAlive = setInterval(() => {
255
- if (client.sse !== res || client.paused) return;
323
+ if (client.sse !== res) return;
324
+ if (client.paused) {
325
+ reportStall(client, res);
326
+ return;
327
+ }
256
328
  try {
257
- if (res.write(': ka\n\n') === false) client.paused = true;
329
+ if (res.write(': ka\n\n') === false) pause(client);
258
330
  } catch {
259
331
  retireStream(res);
260
332
  }
261
- }, 15_000);
333
+ }, KEEPALIVE_MS);
262
334
  keepAlive.unref?.();
263
335
  flushPending(client);
264
336
  return true;
@@ -16,6 +16,7 @@ export const SESSION_READ_ACTIONS = Object.freeze([
16
16
  'getAgentConnectors',
17
17
  'getProjectConnectors',
18
18
  'getAgentAlwaysAllowedTools',
19
+ 'getAgentConnectorTools',
19
20
  'getAgentPermissions',
20
21
  'listConnectors',
21
22
  'listConnectorTools',
@@ -94,6 +95,7 @@ export const SESSION_READ_ACTIONS = Object.freeze([
94
95
  'listWebSearchModels',
95
96
  'listThemes',
96
97
  'listShellJobs',
98
+ 'listStoppableShellJobs',
97
99
  'listWorkflows',
98
100
  'mcpStatus',
99
101
  'pluginsStatus',
@@ -107,6 +109,7 @@ export const SESSION_READ_ACTIONS = Object.freeze([
107
109
  export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
108
110
  'addHookRule',
109
111
  'addMcpServer',
112
+ 'addConnectorAccount',
110
113
  'addPlugin',
111
114
  'addSkill',
112
115
  'agentControl',
@@ -269,6 +272,7 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
269
272
  'startConversationImport',
270
273
  'startMediaJob',
271
274
  'stopShellJob',
275
+ 'stopShellJobs',
272
276
  'releaseShellJob',
273
277
  'switchContext',
274
278
  'taskControl',