mixdog 1.3.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/NOTICE.md +2 -2
  2. package/README.md +2 -2
  3. package/package.json +2 -11
  4. package/scripts/daemon-stub.mjs +1 -1
  5. package/scripts/gate-local.mjs +4 -2
  6. package/scripts/native-release-status.mjs +2 -2
  7. package/scripts/recall-history-cases.json +1 -1
  8. package/scripts/suite-health.mjs +10 -1
  9. package/scripts/test.mjs +7 -11
  10. package/src/agents/reviewer/AGENT.md +7 -11
  11. package/src/agents/reviewer/agent.json +1 -1
  12. package/src/defaults/shipped-history.json +17 -1
  13. package/src/defaults/skills/browser-use/SKILL.md +59 -11
  14. package/src/defaults/skills/code-tidy/SKILL.md +142 -270
  15. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
  16. package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
  17. package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
  18. package/src/defaults/skills/computer-use/SKILL.md +33 -9
  19. package/src/defaults/skills/setup/SKILL.md +22 -0
  20. package/src/defaults/skills/setup/references/actions.md +39 -5
  21. package/src/defaults/skills/setup/references/surfaces.md +11 -2
  22. package/src/defaults/skills/visualize/SKILL.md +2 -1
  23. package/src/lib/rules-builder.cjs +10 -8
  24. package/src/rules/agent/AGENT.md +6 -6
  25. package/src/rules/lead/LEAD.md +18 -20
  26. package/src/rules/shared/05-tool-calls.md +5 -6
  27. package/src/rules/shared/10-tool-workflow.md +2 -0
  28. package/src/rules/shared/60-verification.md +7 -5
  29. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  30. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  32. package/src/runtime/agent/orchestrator/config.mjs +17 -1
  33. package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
  35. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
  36. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +176 -72
  38. package/src/runtime/agent/orchestrator/mcp/client.mjs +19 -0
  39. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +181 -133
  40. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +42 -6
  41. package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
  42. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
  45. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +145 -66
  46. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
  47. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
  48. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
  49. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +123 -102
  50. package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
  52. package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -13
  53. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
  54. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
  55. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
  56. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  57. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
  58. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -1
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
  64. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
  65. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +26 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
  68. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
  69. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
  70. package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
  72. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
  75. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
  76. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
  77. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +133 -22
  78. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
  79. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +11 -5
  82. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +0 -2
  83. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
  84. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
  86. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
  87. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +3 -1
  88. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
  90. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
  92. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +3 -2
  93. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  94. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
  97. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +12 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
  99. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
  101. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
  102. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
  103. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
  104. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
  105. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
  106. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
  107. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  109. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
  110. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
  112. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
  113. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
  114. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
  115. package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
  116. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
  117. package/src/runtime/agent/orchestrator/session/routine-result.mjs +36 -9
  118. package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
  120. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
  121. package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
  122. package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
  123. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
  124. package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
  125. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
  126. package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
  127. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +8 -6
  128. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
  129. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
  130. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
  131. package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
  132. package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
  133. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
  135. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +18 -1
  136. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
  138. package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
  141. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  143. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
  144. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
  145. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
  146. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
  147. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
  148. package/src/runtime/browser-bridge/action-schema.mjs +13 -2
  149. package/src/runtime/browser-bridge/client.mjs +73 -8
  150. package/src/runtime/browser-bridge/input-fields.mjs +8 -9
  151. package/src/runtime/browser-bridge/mail-links.mjs +107 -0
  152. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  153. package/src/runtime/channels/lib/config.mjs +6 -61
  154. package/src/runtime/channels/lib/crash-log.mjs +1 -1
  155. package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
  156. package/src/runtime/channels/lib/event-queue.mjs +3 -18
  157. package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
  158. package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
  159. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
  160. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
  161. package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
  162. package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
  163. package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
  164. package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
  165. package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
  166. package/src/runtime/channels/lib/scheduler.mjs +33 -147
  167. package/src/runtime/channels/lib/schedules-db.mjs +34 -11
  168. package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
  169. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -2
  170. package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
  171. package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
  172. package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
  173. package/src/runtime/channels/lib/worker-main.mjs +10 -34
  174. package/src/runtime/computer-bridge/action-schema.mjs +42 -1
  175. package/src/runtime/computer-bridge/actions.mjs +9 -1
  176. package/src/runtime/computer-bridge/client.mjs +180 -8
  177. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  178. package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
  179. package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
  180. package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
  181. package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
  182. package/src/runtime/connectors/gmail-app-password.mjs +172 -39
  183. package/src/runtime/connectors/google-mcp.mjs +10 -12
  184. package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
  185. package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
  186. package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
  187. package/src/runtime/session-search/session-search-db.mjs +73 -9
  188. package/src/runtime/session-search/session-search-ingest.mjs +110 -20
  189. package/src/runtime/shared/abort-race.mjs +89 -0
  190. package/src/runtime/shared/agent-avatar.d.mts +0 -1
  191. package/src/runtime/shared/agent-kind.mjs +4 -3
  192. package/src/runtime/shared/bounded-download.mjs +8 -3
  193. package/src/runtime/shared/config.mjs +2 -13
  194. package/src/runtime/shared/connector-accounts.mjs +20 -10
  195. package/src/runtime/shared/connector-catalog.d.mts +29 -0
  196. package/src/runtime/shared/connector-catalog.mjs +150 -0
  197. package/src/runtime/shared/connector-triggers.mjs +22 -19
  198. package/src/runtime/shared/defaults-separation.mjs +2 -14
  199. package/src/runtime/shared/delegated-approval-host.mjs +103 -11
  200. package/src/runtime/shared/err-text.mjs +24 -15
  201. package/src/runtime/shared/file-permissions.d.mts +0 -8
  202. package/src/runtime/shared/file-permissions.mjs +0 -11
  203. package/src/runtime/shared/git-direct.test-support.mjs +19 -0
  204. package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
  205. package/src/runtime/shared/heartbeat-interval.mjs +19 -0
  206. package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
  207. package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
  208. package/src/runtime/shared/mixy-presence.mjs +22 -0
  209. package/src/runtime/shared/orchestration.d.mts +2 -0
  210. package/src/runtime/shared/pg-schema-db.mjs +27 -16
  211. package/src/runtime/shared/profile-config.cjs +1 -1
  212. package/src/runtime/shared/prompt-origin.d.mts +4 -0
  213. package/src/runtime/shared/prompt-origin.mjs +21 -0
  214. package/src/runtime/shared/provider-accounts.mjs +7 -1
  215. package/src/runtime/shared/provider-display-name.mjs +29 -0
  216. package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
  217. package/src/runtime/shared/routine-trigger.d.mts +2 -0
  218. package/src/runtime/shared/routine-trigger.mjs +10 -0
  219. package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
  220. package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
  221. package/src/runtime/shared/session-activity-signal.mjs +34 -1
  222. package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
  223. package/src/runtime/shared/session-sidebar-host.mjs +7 -3
  224. package/src/runtime/shared/session-turn-prelude.mjs +18 -0
  225. package/src/runtime/shared/tool-result-summary.mjs +1 -14
  226. package/src/runtime/shared/worktrees/test-support.mjs +9 -4
  227. package/src/runtime/terminal-bridge/client.mjs +10 -1
  228. package/src/runtime/visualize-bridge/client.mjs +27 -8
  229. package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
  230. package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
  231. package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
  232. package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
  233. package/src/session-runtime/agent-home-delivery.mjs +448 -185
  234. package/src/session-runtime/agent-task-sessions.mjs +329 -180
  235. package/src/session-runtime/boot/apis.mjs +9 -0
  236. package/src/session-runtime/boot/infrastructure.mjs +4 -0
  237. package/src/session-runtime/boot/providers.mjs +3 -0
  238. package/src/session-runtime/boot/tools.mjs +6 -1
  239. package/src/session-runtime/config-lifecycle.mjs +6 -0
  240. package/src/session-runtime/connector-merge-migration.mjs +305 -0
  241. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
  242. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
  243. package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
  244. package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
  245. package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
  246. package/src/session-runtime/lifecycle/teardown.mjs +23 -5
  247. package/src/session-runtime/mcp-glue/server-catalog.mjs +55 -3
  248. package/src/session-runtime/mcp-oauth-login.mjs +5 -3
  249. package/src/session-runtime/mixy-heartbeat.mjs +153 -33
  250. package/src/session-runtime/notification-bus.mjs +31 -117
  251. package/src/session-runtime/notification-completion-queue.mjs +54 -0
  252. package/src/session-runtime/notification-listeners.mjs +76 -0
  253. package/src/session-runtime/owned-automation-run.mjs +2 -1
  254. package/src/session-runtime/prewarm/channel-start.mjs +3 -7
  255. package/src/session-runtime/resource-mcp-api.mjs +143 -224
  256. package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
  257. package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
  258. package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
  259. package/src/session-runtime/resource-mcp-servers.mjs +78 -0
  260. package/src/session-runtime/resource-mcp-store.mjs +49 -0
  261. package/src/session-runtime/routine-owner.mjs +40 -9
  262. package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
  263. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
  264. package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
  265. package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
  266. package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
  267. package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
  268. package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
  269. package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
  270. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
  271. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
  272. package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
  273. package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
  274. package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
  275. package/src/session-runtime/services/channel-admin.mjs +7 -13
  276. package/src/session-runtime/services/channel-session-router.mjs +4 -2
  277. package/src/session-runtime/services/seeds.mjs +0 -28
  278. package/src/session-runtime/session-compaction-sync.mjs +27 -0
  279. package/src/session-runtime/session-create/session-options.mjs +1 -0
  280. package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
  281. package/src/session-runtime/settings-compaction-api.mjs +2 -40
  282. package/src/session-runtime/settings-system-api.mjs +13 -1
  283. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
  284. package/src/session-runtime/setup-tool/executor.mjs +145 -22
  285. package/src/session-runtime/setup-tool/extended-actions.mjs +8 -3
  286. package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
  287. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -3
  288. package/src/session-runtime/setup-tool/tool-defs.mjs +64 -5
  289. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
  290. package/src/session-runtime/tool-policy/policy-stage.mjs +16 -2
  291. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  292. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
  293. package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
  294. package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
  295. package/src/standalone/channel-transport/client-registry.mjs +24 -163
  296. package/src/standalone/channel-transport.mjs +1 -1
  297. package/src/standalone/daemon-channels-loader.mjs +10 -8
  298. package/src/standalone/daemon-session-control/list-page.mjs +72 -0
  299. package/src/standalone/daemon-session-control/request.mjs +51 -0
  300. package/src/standalone/daemon-session-control/room-host.mjs +57 -0
  301. package/src/standalone/daemon-session-control/route-control.mjs +48 -0
  302. package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
  303. package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
  304. package/src/standalone/daemon-session-control.mjs +124 -188
  305. package/src/standalone/daemon.mjs +26 -3
  306. package/src/standalone/session-client.mjs +7 -3
  307. package/src/standalone/session-protocol.mjs +5 -0
  308. package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
  309. package/src/standalone/session-service/quota-resume.mjs +3 -0
  310. package/src/standalone/session-service/retention/busy.mjs +65 -1
  311. package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
  312. package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
  313. package/src/standalone/session-tool-output.mjs +15 -7
  314. package/src/tui/app/model-options.mjs +3 -28
  315. package/src/tui/dist/index.mjs +58 -64
  316. package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
  317. package/src/tui/session/initial-state.mjs +3 -0
  318. package/src/tui/session/notice-surface.mjs +7 -1
  319. package/src/tui/session/queue-helpers.mjs +3 -42
  320. package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
  321. package/src/tui/session/session-api/routes-write.mjs +5 -1
  322. package/src/tui/session/session-api/routes.mjs +1 -0
  323. package/src/tui/session/session-api/settings.mjs +17 -1
  324. package/src/tui/session/session-flow/auto-clear.mjs +4 -33
  325. package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
  326. package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
  327. package/src/tui/session/session-flow/drain-retry.mjs +35 -0
  328. package/src/tui/session/session-flow/drain.mjs +118 -149
  329. package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
  330. package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
  331. package/src/tui/session/tool-approval-delegation.mjs +64 -0
  332. package/src/tui/session/tool-approval-request.mjs +39 -0
  333. package/src/tui/session/tool-approval-settle.mjs +62 -0
  334. package/src/tui/session/tool-approval.mjs +69 -115
  335. package/src/tui/session/tui-steering-persist.mjs +8 -1
  336. package/src/tui/session/turn-stream.mjs +19 -2
  337. package/src/tui/session/turn.mjs +1 -0
  338. package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
  339. package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
  340. package/scripts/code-graph-description-contract.mjs +0 -188
  341. package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
@@ -107,8 +107,17 @@ export function wireApis(boot) {
107
107
  try {
108
108
  boot.resourceApi.migratePluginMcpCopies();
109
109
  } catch (error) {
110
+ console.warn(`[mcp] adopting plugin MCP copies failed: ${error?.message || error}`);
110
111
  bootProfile('plugin-mcp:adopt-failed', { error: error?.message || String(error) });
111
112
  }
113
+ // One server per multi-account connector: `gmail`, `gmail-2`… merge into
114
+ // `gmail` holding the accounts (their sign-ins and app passwords stay).
115
+ try {
116
+ boot.resourceApi.migrateConnectorAccounts();
117
+ } catch (error) {
118
+ console.warn(`[connectors] merging connector accounts failed: ${error?.message || error}`);
119
+ bootProfile('connector-accounts:merge-failed', { error: error?.message || String(error) });
120
+ }
112
121
  }
113
122
  boot.disposeGlobalExtensionSubscription = () => {
114
123
  boot.disposeConfigSync?.();
@@ -20,6 +20,7 @@ import { listProjectsStrict } from '../../runtime/shared/projects.mjs';
20
20
  import { isWithin } from '../../runtime/shared/worktrees/paths.mjs';
21
21
  import { standaloneDataDir } from '../runtime-paths.mjs';
22
22
  import { SessionAddressError, isAdmissionOpen, ownerLifecycle } from '../session-address.mjs';
23
+ import { syncSessionCompaction, usesInjectedConfig } from '../session-compaction-sync.mjs';
23
24
  import { dataDirOf } from './shared.mjs';
24
25
 
25
26
  export function wireInfrastructure(boot) {
@@ -187,6 +188,9 @@ export function sessionAdopter(boot) {
187
188
  }
188
189
  }
189
190
  rt.session = v;
191
+ // A resumed or swapped-in session keeps the compaction it was saved with
192
+ // unless it follows the current global settings (not on an injected config).
193
+ if (v?.id && !usesInjectedConfig(boot.params)) syncSessionCompaction(v, rt.config);
190
194
  // The PreToolUse/PostToolUse bridge is a non-enumerable property that
191
195
  // neither the store nor a resumed record carries: every session installed
192
196
  // here (create, resume, route swap, clear, the manager's copy) gets it, so
@@ -31,6 +31,7 @@ import {
31
31
  } from '../model-recency.mjs';
32
32
  import { createNativeWebSearch } from '../native-web-search.mjs';
33
33
  import { createConfigLifecycle } from '../config-lifecycle.mjs';
34
+ import { usesInjectedConfig } from '../session-compaction-sync.mjs';
34
35
  import { createQuickModelRows } from '../quick-model-rows.mjs';
35
36
  import { createProviderModels } from '../provider-models.mjs';
36
37
  import { createProviderUsage } from '../provider-usage.mjs';
@@ -66,6 +67,8 @@ function wireConfigLifecycle(boot) {
66
67
  rt.configHasSecrets = next;
67
68
  },
68
69
  getRoute: () => rt.route,
70
+ getSession: () => rt.session,
71
+ fixedSessionCompaction: usesInjectedConfig(boot.params),
69
72
  setRouteState: (next) => {
70
73
  rt.route = next;
71
74
  },
@@ -9,6 +9,7 @@ import {
9
9
  channelNotificationSessionId,
10
10
  } from '../../runtime/shared/channel-notification-routing.mjs';
11
11
  import { CHANNEL_NOTIFICATION_METHOD } from '../services/channel-session-router.mjs';
12
+ import { PROMPT_ORIGIN_CHANNEL } from '../../runtime/agent/orchestrator/session/manager/pending-message-entry.mjs';
12
13
  import { envFlag } from '../../runtime/shared/env.mjs';
13
14
  // Desktop-app bridges: tiny fs/fetch clients, so a static import adds no
14
15
  // meaningful boot cost. The tools themselves are gated per session by the
@@ -144,7 +145,9 @@ function wireChannels(boot) {
144
145
  const meta = p.meta && typeof p.meta === 'object' ? p.meta : {};
145
146
  const content = channelNotificationModelContent(p);
146
147
  if (!content) return;
147
- notifySession(channelNotificationSessionId(rt.session, rt.reservedSessionId), content, meta);
148
+ notifySession(channelNotificationSessionId(rt.session, rt.reservedSessionId), content, meta, {
149
+ origin: PROMPT_ORIGIN_CHANNEL,
150
+ });
148
151
  },
149
152
  });
150
153
  bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
@@ -234,6 +237,8 @@ function wireInternalTools(boot) {
234
237
  getCallerSession: (sessionId) =>
235
238
  sessionId === rt.session?.id ? rt.session : boot.mgr?.getSession?.(sessionId) || null,
236
239
  flushSettings: () => boot.flushAllConfigSavesAsync({ requireSaved: true }),
240
+ // Another session's route and quota actions (daemon-session-control.mjs); null without a daemon.
241
+ executeSessionControl: boot.params.executeSessionControl || null,
237
242
  });
238
243
  const disposeInternalTools = internalTools.setInternalToolsProvider({
239
244
  scopeId: rt.mcpScopeId,
@@ -17,6 +17,7 @@ import {
17
17
  stripStoredRouteDefaults,
18
18
  withDefaultPresets,
19
19
  } from '../runtime/agent/orchestrator/config-presets.mjs';
20
+ import { syncSessionCompaction } from './session-compaction-sync.mjs';
20
21
  import { createConfigWriters } from './config-lifecycle/config-writers.mjs';
21
22
  import { createLeadRouteSync } from './config-lifecycle/lead-route-sync.mjs';
22
23
  import { createOutputStyleStatusCache } from './config-lifecycle/output-style-cache.mjs';
@@ -134,6 +135,10 @@ export function createConfigLifecycle({
134
135
  getConfigHasSecrets,
135
136
  setConfigHasSecrets,
136
137
  getRoute,
138
+ // The live session follows the reloaded global compaction settings, unless
139
+ // the runtime runs on an injected (headless) config.
140
+ getSession,
141
+ fixedSessionCompaction = false,
137
142
  // Main route sync on reload (both optional: a lifecycle without them never syncs)
138
143
  setRouteState,
139
144
  resolveRoute,
@@ -222,6 +227,7 @@ export function createConfigLifecycle({
222
227
  const adopted = adoptConfig(next, { hasSecrets: true });
223
228
  configSaveBaseline = structuredClone(adopted);
224
229
  leadRoute.syncEmptyMainRoute(adopted);
230
+ if (!fixedSessionCompaction) syncSessionCompaction(getSession?.(), adopted);
225
231
  return adopted;
226
232
  }
227
233
 
@@ -0,0 +1,305 @@
1
+ // After the multi-account connector merge (connector-catalog.mjs
2
+ // mergeConnectorAccountServers) `gmail-2`, `google-calendar-2`… are accounts of
3
+ // one server, no longer servers. Every store of ours that keys a setting by
4
+ // MCP server name must name the surviving server instead:
5
+ // agents/<id>/agent.json, bots/<id>/agent.json connectors.off / connectors.tools
6
+ // (a legacy allow list too)
7
+ // <data>/project-connectors.json projects.<root>.off
8
+ // <data>/hooks.json, <data>/hooks/hooks.json mcp_tool `server`, tool matchers
9
+ // (extension scopes are the config's and are dropped by migrateConnectorAccounts;
10
+ // an approval's "always allow" is the agent's connectors.tools; agent routes
11
+ // name models, not servers.) Project and plugin hook files are the user's or
12
+ // the plugin's own and are never rewritten here.
13
+ //
14
+ // Rule: the merged connector is OFF for an agent/project only when EVERY
15
+ // merged server was off (the same rule as the merged server's `enabled`: one
16
+ // working account keeps the connector usable). Per-tool permissions move to
17
+ // the surviving name; where two merged servers disagree on a tool the MORE
18
+ // PERMISSIVE permission wins, as "on beats off" does at server level. Entries
19
+ // of a server that was off are dormant and are dropped when the connector
20
+ // stays on, so they never become live. A setting that names no merged-away
21
+ // server is never touched, which makes every run after the first a no-op.
22
+ //
23
+ // The merge is saved to the config once, so the rename mapping outlives it:
24
+ // `<data>/connector-merge-pending.json` ({ merged: [{ name, from }] }) is
25
+ // written before any store is touched and deleted only when every store
26
+ // migrated. A store that fails (unreadable, malformed, not writable) is
27
+ // reported and the others still go on; the next start retries them all.
28
+ //
29
+ // Hooks: only tool identifiers are rewritten — an `mcp_tool` handler's
30
+ // `server`, and a matcher / tool-name field (or the tool part of an `if`
31
+ // before its `(`) that is an `mcp__<server>__…` identifier or an
32
+ // alternative of a pattern. Argument conditions such as `Bash(mcp__x__*)`
33
+ // and any other data are never touched.
34
+ import { readdirSync, readFileSync, rmSync } from 'node:fs';
35
+ import { join } from 'node:path';
36
+ import { normalizeRules, standardConfigReport } from './services/hook-bus/config.mjs';
37
+ import {
38
+ PERMISSION_RANK,
39
+ PROJECT_CONNECTORS_FILE,
40
+ connectorEnabled,
41
+ parseConnectorPolicy,
42
+ storedConnectorPolicy,
43
+ } from '../runtime/agent/orchestrator/mcp/agent-connectors.mjs';
44
+ import { parseJsonText, withFileLockSync, writeJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
45
+
46
+ const unique = (list) => [...new Set(list)].sort();
47
+
48
+ /** The policy `{ off, tools }` (or a legacy `{ allow, off, tools }`) with every
49
+ * merged-away server of `merged` mapped onto its surviving one; null when it
50
+ * names none. `merged` is mergeConnectorAccountServers' `merged`. */
51
+ function mapMergedConnectorPolicy(policy, merged) {
52
+ let current = policy;
53
+ for (const { name, from } of merged) {
54
+ const gone = from.filter((server) => server !== name);
55
+ const named = (server) =>
56
+ current.off.includes(server) || current.allow?.includes(server) || Object.hasOwn(current.tools, server);
57
+ if (!gone.some(named)) continue;
58
+ const live = from.filter((server) => connectorEnabled(current, server));
59
+ const on = live.length > 0;
60
+ const off = current.off.filter((server) => !from.includes(server));
61
+ const allow = current.allow?.filter((server) => !from.includes(server));
62
+ if (allow && on) allow.push(name);
63
+ if (!allow && !on) off.push(name);
64
+ const tools = { ...current.tools };
65
+ for (const server of from) delete tools[server];
66
+ const moved = {};
67
+ for (const server of on ? live : from) {
68
+ for (const [tool, permission] of Object.entries(current.tools[server] || {})) {
69
+ if (!(tool in moved) || PERMISSION_RANK[permission] > PERMISSION_RANK[moved[tool]]) moved[tool] = permission;
70
+ }
71
+ }
72
+ if (Object.keys(moved).length) tools[name] = moved;
73
+ current = { ...(allow ? { allow: unique(allow) } : {}), off: unique(off), tools };
74
+ }
75
+ return current === policy ? null : current;
76
+ }
77
+
78
+ /** A mapped policy as the value the agent.json `connectors` field stores
79
+ * (undefined: the field goes). */
80
+ function storedValue(policy) {
81
+ if (!policy.allow) return storedConnectorPolicy(policy) ?? undefined;
82
+ const ids = unique([...policy.allow, ...Object.keys(policy.tools)]);
83
+ return ids.map((id) => ({
84
+ id,
85
+ enabled: policy.allow.includes(id),
86
+ ...(policy.tools[id] ? { tools: policy.tools[id] } : {}),
87
+ }));
88
+ }
89
+
90
+ export const PENDING_MERGE_FILE = 'connector-merge-pending.json';
91
+
92
+ const warn = (file, error) => process.stderr.write(`[connectors] ${file} not migrated (${error?.message || error})\n`);
93
+
94
+ /** The JSON at `file`; null when it does not exist. Any other failure throws. */
95
+ function readJsonFile(file) {
96
+ try {
97
+ return parseJsonText(readFileSync(file, 'utf8'));
98
+ } catch (error) {
99
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return null;
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
105
+
106
+ /** Runs `step` for one store; a failure is reported and returned, never thrown. */
107
+ function attempt(file, step, outcome) {
108
+ try {
109
+ step();
110
+ } catch (error) {
111
+ warn(file, error);
112
+ outcome.failed.push(file);
113
+ }
114
+ }
115
+
116
+ function migrateAgentFiles(dataDir, merged, outcome, readdir) {
117
+ for (const store of ['agents', 'bots']) {
118
+ const dir = join(dataDir, store);
119
+ let entries;
120
+ try {
121
+ entries = readdir(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
122
+ } catch (error) {
123
+ // A store that does not exist has nothing to migrate; any other failure
124
+ // leaves its agents unprocessed, so the mapping must stay pending.
125
+ if (error?.code !== 'ENOENT') {
126
+ warn(dir, error);
127
+ outcome.failed.push(dir);
128
+ }
129
+ continue;
130
+ }
131
+ for (const entry of entries) {
132
+ const file = join(dataDir, store, entry.name, 'agent.json');
133
+ attempt(
134
+ file,
135
+ () => {
136
+ const manifest = readJsonFile(file);
137
+ if (!isRecord(manifest) || manifest.connectors == null) return;
138
+ const next = mapMergedConnectorPolicy(parseConnectorPolicy(manifest.connectors), merged);
139
+ if (!next) return;
140
+ const value = storedValue(next);
141
+ if (value) manifest.connectors = value;
142
+ else delete manifest.connectors;
143
+ writeJsonAtomicSync(file, manifest, { backup: true });
144
+ outcome.agents.push(`${store}/${entry.name}`);
145
+ },
146
+ outcome
147
+ );
148
+ }
149
+ }
150
+ }
151
+
152
+ function migrateProjectFile(dataDir, merged, outcome) {
153
+ const file = join(dataDir, PROJECT_CONNECTORS_FILE);
154
+ attempt(
155
+ file,
156
+ () => {
157
+ withFileLockSync(`${file}.lock`, () => {
158
+ const document = readJsonFile(file);
159
+ const projects = document?.projects;
160
+ if (!isRecord(projects)) return;
161
+ const changed = [];
162
+ for (const [root, entry] of Object.entries(projects)) {
163
+ if (!isRecord(entry) || !Array.isArray(entry.off)) continue;
164
+ const next = mapMergedConnectorPolicy({ off: entry.off.map(String), tools: {} }, merged);
165
+ if (!next) continue;
166
+ if (next.off.length) projects[root] = { ...entry, off: next.off };
167
+ else delete projects[root];
168
+ changed.push(root);
169
+ }
170
+ if (!changed.length) return;
171
+ writeJsonAtomicSync(file, document, { lock: false, backup: true });
172
+ outcome.projects.push(...changed);
173
+ });
174
+ },
175
+ outcome
176
+ );
177
+ }
178
+
179
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
180
+
181
+ /** A tool identifier or pattern (`mcp__a__x`, `^mcp__a__.*$`, `Bash|mcp__a__x`)
182
+ * with each `mcp__<merged-away>` alternative renamed. */
183
+ function renameToolPattern(value, renames) {
184
+ let next = value;
185
+ for (const [gone, name] of renames) {
186
+ const identifier = new RegExp(`(^|\\|)(\\^?)mcp__${escapeRegExp(gone)}(?=__|(?![A-Za-z0-9_-]))`, 'g');
187
+ next = next.replace(identifier, (_all, lead, anchor) => `${lead}${anchor}mcp__${name}`);
188
+ }
189
+ return next;
190
+ }
191
+
192
+ /** A condition `Tool(arguments)`: only the tool part is an identifier. */
193
+ function renameCondition(value, renames) {
194
+ const open = value.indexOf('(');
195
+ return open < 0
196
+ ? renameToolPattern(value, renames)
197
+ : renameToolPattern(value.slice(0, open), renames) + value.slice(open);
198
+ }
199
+
200
+ /** Renames in the hook structures of `document` (in place); true when any changed. */
201
+ function renameInHooks(document, renames) {
202
+ let changed = false;
203
+ const set = (owner, key, rename) => {
204
+ if (typeof owner[key] !== 'string') return;
205
+ const next = rename(owner[key], renames);
206
+ if (next === owner[key]) return;
207
+ owner[key] = next;
208
+ changed = true;
209
+ };
210
+ const handler = (hook) => {
211
+ if (!isRecord(hook)) return;
212
+ if (hook.type === 'mcp_tool') {
213
+ if (typeof hook.server === 'string' && renames.has(hook.server.trim())) {
214
+ hook.server = renames.get(hook.server.trim());
215
+ changed = true;
216
+ }
217
+ set(hook, 'tool', renameToolPattern);
218
+ }
219
+ set(hook, 'if', renameCondition);
220
+ };
221
+ if (standardConfigReport(document).standard) {
222
+ for (const groups of Object.values(document.hooks)) {
223
+ for (const group of Array.isArray(groups) ? groups : []) {
224
+ if (!isRecord(group)) continue;
225
+ set(group, 'matcher', renameToolPattern);
226
+ set(group, 'if', renameCondition);
227
+ for (const hook of Array.isArray(group.hooks) ? group.hooks : []) handler(hook);
228
+ }
229
+ }
230
+ } else {
231
+ for (const rule of normalizeRules(document)) {
232
+ if (!isRecord(rule)) continue;
233
+ set(rule, 'tool', renameToolPattern);
234
+ set(rule, 'name', renameToolPattern);
235
+ set(rule, 'if', renameCondition);
236
+ }
237
+ }
238
+ return changed;
239
+ }
240
+
241
+ function migrateHookFiles(dataDir, merged, outcome) {
242
+ const renames = new Map(
243
+ merged.flatMap(({ name, from }) => from.filter((server) => server !== name).map((gone) => [gone, name]))
244
+ );
245
+ for (const file of [join(dataDir, 'hooks.json'), join(dataDir, 'hooks', 'hooks.json')]) {
246
+ attempt(
247
+ file,
248
+ () => {
249
+ const document = readJsonFile(file);
250
+ if (!isRecord(document) && !Array.isArray(document)) return;
251
+ if (!renameInHooks(document, renames)) return;
252
+ writeJsonAtomicSync(file, document, { backup: true });
253
+ outcome.hooks.push(file);
254
+ },
255
+ outcome
256
+ );
257
+ }
258
+ }
259
+
260
+ const validGroup = (group) =>
261
+ isRecord(group) &&
262
+ typeof group.name === 'string' &&
263
+ Array.isArray(group.from) &&
264
+ group.from.every((n) => typeof n === 'string');
265
+
266
+ /** Maps every per-server setting of `dataDir` onto the surviving servers of
267
+ * `merged` (this start's merge, [] to only retry) plus any still pending from
268
+ * an earlier start. Returns what it rewrote and the files that failed:
269
+ * `{ agents, projects, hooks, failed, unrecorded? }`. The pending record is
270
+ * removed only when `failed` is empty; `unrecorded` says it could not be
271
+ * written, so the caller must not save the merge. `readdir` lists the agent
272
+ * and bot directories. */
273
+ export function migrateMergedConnectorServers(dataDir, merged, { readdir = readdirSync } = {}) {
274
+ const outcome = { agents: [], projects: [], hooks: [], failed: [] };
275
+ if (!dataDir) return outcome;
276
+ const pendingFile = join(dataDir, PENDING_MERGE_FILE);
277
+ let pending = [];
278
+ try {
279
+ const stored = readJsonFile(pendingFile)?.merged;
280
+ pending = Array.isArray(stored) ? stored.filter(validGroup).map(({ name, from }) => ({ name, from })) : [];
281
+ } catch (error) {
282
+ warn(pendingFile, error);
283
+ }
284
+ const groups = [...pending];
285
+ for (const { name, from } of merged) {
286
+ if (!groups.some((group) => group.name === name && JSON.stringify(group.from) === JSON.stringify(from))) {
287
+ groups.push({ name, from });
288
+ }
289
+ }
290
+ if (!groups.length) return outcome;
291
+ if (groups.length !== pending.length) {
292
+ try {
293
+ writeJsonAtomicSync(pendingFile, { merged: groups }, { backup: false });
294
+ } catch (error) {
295
+ warn(pendingFile, error);
296
+ // Nothing records the mapping: a failed store must keep the merge unsaved.
297
+ outcome.unrecorded = true;
298
+ }
299
+ }
300
+ migrateAgentFiles(dataDir, groups, outcome, readdir);
301
+ migrateProjectFile(dataDir, groups, outcome);
302
+ migrateHookFiles(dataDir, groups, outcome);
303
+ if (!outcome.failed.length) rmSync(pendingFile, { force: true });
304
+ return outcome;
305
+ }
@@ -8,29 +8,73 @@ import {
8
8
  } from '../../runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs';
9
9
  import { createCallerContextResolvers } from './caller-context.mjs';
10
10
  import { runAbortable } from '../../runtime/shared/abort-race.mjs';
11
- import { SECRET_REQUEST_TIMEOUT_MS } from '../../runtime/agent/orchestrator/session/human-gate/requests.mjs';
11
+ import {
12
+ HANDOFF_OUTCOMES as AGENT_OUTCOMES,
13
+ SECRET_REQUEST_TIMEOUT_MS,
14
+ } from '../../runtime/agent/orchestrator/session/human-gate/requests.mjs';
12
15
  import {
13
16
  agentIdForSession,
14
17
  recordHumanDecision,
15
18
  unattendedApprovalPort,
16
19
  } from '../../runtime/agent/orchestrator/session/human-gate/gate.mjs';
17
- import { browserSecretInput } from '../../runtime/browser-bridge/client.mjs';
18
- import { computerSecretInput } from '../../runtime/computer-bridge/client.mjs';
20
+ import { browserHandoffPage, browserSecretInput } from '../../runtime/browser-bridge/client.mjs';
21
+ import { computerSecretBind, computerSecretInput } from '../../runtime/computer-bridge/client.mjs';
22
+
23
+ /** The settle reason of a takeover that expired (desktop takeover-gate.ts
24
+ * TAKEOVER_EXPIRED_REASON). */
25
+ const TAKEOVER_EXPIRED_REASON = 'expired';
26
+
27
+ /**
28
+ * The agent's result of a secret request, from the card's answer and the
29
+ * desktop's fill: one of requests.mjs HANDOFF_OUTCOMES. `fill` runs only for a provided
30
+ * value and answers 'entered' / 'no_field' / 'page_changed' / 'blocked' /
31
+ * 'expired'.
32
+ */
33
+ export async function secretOutcome(result, fill) {
34
+ if (result?.timedOut === true || result?.reason === TAKEOVER_EXPIRED_REASON) return 'expired';
35
+ if (result?.approved !== true || result.response !== 'provide-secret') return 'user_skipped';
36
+ const filled = await fill();
37
+ return AGENT_OUTCOMES.includes(filled) && filled !== 'user_skipped' ? filled : 'blocked';
38
+ }
39
+
40
+ /** A page address as the agent may see it: origin and path, no query or
41
+ * fragment (a sign-in redirect can carry a code or token there). */
42
+ function factUrl(value) {
43
+ try {
44
+ const url = new URL(String(value || ''));
45
+ return url.protocol === 'http:' || url.protocol === 'https:' ? `${url.origin}${url.pathname}` : '';
46
+ } catch {
47
+ return '';
48
+ }
49
+ }
50
+
51
+ /** The agent's result of a takeover: what the user did and where the page is
52
+ * now, never "signed in" (the agent checks the page). */
53
+ export function takeoverOutcome(result, pageUrl = '') {
54
+ if (result?.timedOut === true || result?.reason === TAKEOVER_EXPIRED_REASON) return 'expired';
55
+ if (result?.approved !== true) return 'user_skipped';
56
+ const url = factUrl(pageUrl);
57
+ const did = result.reason === 'saved_login' ? 'User chose a saved login; it was filled in' : 'User pressed Done';
58
+ return url ? `${did}; page now at ${url}` : did;
59
+ }
19
60
 
20
61
  /**
21
- * A secret or takeover handoff. Secret: the card collects the value on the
22
- * desktop, which holds it for `requestId` and fills it into the target field
23
- * (isolated-world fill for the browser, one-shot typed input for computer
24
- * use); here only the verdict travels, and the result is exactly 'provided',
25
- * 'skipped' or 'timeout'. Takeover: the run waits until the user says done
26
- * (computer and browser actions stay blocked meanwhile, desktop
27
- * main/takeover-gate.ts); the result is 'done' or 'denied'.
62
+ * A secret or takeover handoff. Secret: at request time the desktop binds the
63
+ * request to its page ({pageId, origin}) or, for computer use, it is bound to
64
+ * {window_id, ref}; the card collects the value on the desktop, which holds it
65
+ * for `requestId` and fills only that page, only while its origin is
66
+ * unchanged. Here only the outcome travels (secretOutcome). Takeover: the run
67
+ * waits until the user says done (that session's actions stay blocked
68
+ * meanwhile, desktop main/takeover-gate.ts); the result is takeoverOutcome's.
28
69
  */
29
70
  export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId, signal, inputs = {} }) {
30
71
  const secretInput = {
31
72
  browser: inputs.browser || browserSecretInput,
32
73
  computer: inputs.computer || computerSecretInput,
33
74
  };
75
+ const page = inputs.page || browserHandoffPage;
76
+ const bindComputer = inputs.computerBind || computerSecretBind;
77
+ const identity = handoff.identity || {};
34
78
  const gate = {
35
79
  agentId: agentIdForSession(sessionId),
36
80
  kind: handoff.kind,
@@ -47,32 +91,84 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
47
91
  return UNATTENDED_TAKEOVER;
48
92
  }
49
93
  if (typeof hook !== 'function') {
50
- // Nobody can take over here: blocked, never "proceed".
94
+ // Nobody can take over or type here: blocked, never "proceed".
51
95
  if (handoff.kind === 'takeover') return NO_TAKEOVER;
52
- await recordHumanDecision(gate, { response: 'skipped' });
53
- return 'skipped';
96
+ await recordHumanDecision(gate, { response: 'blocked' });
97
+ return 'blocked';
54
98
  }
55
99
  if (handoff.kind === 'secret') {
56
100
  const requestId = `secret_${randomUUID()}`;
101
+ // Bound now: the page (and its origin) the value may go to, and what its
102
+ // field is. A browser page that cannot be bound, or has no such field,
103
+ // never asks the user for a value it could not place.
104
+ let bound = {};
105
+ if (handoff.surface === 'browser') {
106
+ const facts = await page(
107
+ sessionId,
108
+ 'secret_bind',
109
+ { request_id: requestId, ...(identity.tab ? { tab: identity.tab } : {}), field: handoff.field },
110
+ { signal }
111
+ );
112
+ let outcome = '';
113
+ if (!facts?.origin) outcome = 'blocked';
114
+ else if (!facts.form) outcome = 'no_field';
115
+ if (outcome) {
116
+ await recordHumanDecision(gate, { response: outcome });
117
+ return outcome;
118
+ }
119
+ bound = { origin: facts.origin, form: facts.form, ...(facts.length ? { length: facts.length } : {}) };
120
+ } else {
121
+ // Computer Use: bound to the exact window and field ref the agent named
122
+ // (from snapshot/find). Without both there is no field to confirm, and
123
+ // none is made up (never the focused field or the front window).
124
+ const outcome =
125
+ identity.windowId && identity.ref
126
+ ? await bindComputer(sessionId, requestId, { windowId: identity.windowId, ref: identity.ref, signal })
127
+ : 'no_field';
128
+ if (outcome !== 'bound') {
129
+ const refused = outcome === 'no_field' ? 'no_field' : 'blocked';
130
+ await recordHumanDecision(gate, { response: refused });
131
+ return refused;
132
+ }
133
+ }
57
134
  const result = await runAbortable(signal, () =>
58
135
  hook({
59
136
  name: 'ask_user',
60
137
  kind: 'secret',
61
- secret: { requestId, surface: handoff.surface, target: handoff.target, field: handoff.field },
138
+ secret: { requestId, surface: handoff.surface, target: handoff.target, field: handoff.field, ...bound },
62
139
  reason: '',
63
140
  timeoutMs: SECRET_REQUEST_TIMEOUT_MS,
64
141
  sessionId,
65
142
  toolCallId,
66
143
  })
67
144
  );
68
- let verdict = 'skipped';
69
- if (result?.timedOut === true) verdict = 'timeout';
70
- else if (result?.approved === true && result.response === 'provide-secret') {
71
- verdict = await secretInput[handoff.surface](sessionId, requestId, { signal });
72
- }
73
- await recordHumanDecision(gate, { response: verdict, device: result?.device });
74
- return verdict;
145
+ const computerTarget = {
146
+ ...(identity.ref ? { ref: identity.ref } : {}),
147
+ ...(identity.windowId ? { windowId: identity.windowId } : {}),
148
+ };
149
+ const outcome = await secretOutcome(result, () =>
150
+ secretInput[handoff.surface](sessionId, requestId, {
151
+ signal,
152
+ ...(handoff.surface === 'computer' ? computerTarget : {}),
153
+ })
154
+ );
155
+ await recordHumanDecision(gate, { response: outcome, device: result?.device });
156
+ return outcome;
75
157
  }
158
+ // A sign-in page's saved logins (masked) let the card fill one for the
159
+ // user: the desktop offers them only for a page on that origin, bound now
160
+ // under `fillId` so the fill reaches that page alone (host-agent.ts fillBoundLogin).
161
+ const fillId = handoff.login ? `secret_${randomUUID()}` : '';
162
+ const accounts = handoff.login
163
+ ? (
164
+ await page(
165
+ sessionId,
166
+ 'login_accounts',
167
+ { url: handoff.login.url, request_id: fillId, ...(identity.tab ? { tab: identity.tab } : {}) },
168
+ { signal }
169
+ )
170
+ )?.accounts
171
+ : null;
76
172
  const result = await runAbortable(signal, () =>
77
173
  hook({
78
174
  name: 'ask_user',
@@ -81,9 +177,10 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
81
177
  surface: handoff.surface,
82
178
  instructions: handoff.instructions,
83
179
  target: handoff.target,
84
- ...handoff.identity,
180
+ ...identity,
85
181
  // A login takeover: the desktop opens its sign-in window on this page.
86
182
  ...(handoff.login ? { purpose: 'login', url: handoff.login.url } : {}),
183
+ ...(Array.isArray(accounts) && accounts.length ? { accounts, fillId } : {}),
87
184
  },
88
185
  args: { surface: handoff.surface, instructions: handoff.instructions },
89
186
  reason: '',
@@ -92,8 +189,15 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
92
189
  toolCallId,
93
190
  })
94
191
  );
95
- const answer = result?.approved === true ? 'done' : 'denied';
96
- await recordHumanDecision(gate, { response: answer, device: result?.device });
192
+ const pageUrl =
193
+ result?.approved === true && handoff.surface === 'browser'
194
+ ? (await page(sessionId, 'page_url', identity.tab ? { tab: identity.tab } : {}, { signal }))?.url
195
+ : '';
196
+ const answer = takeoverOutcome(result, pageUrl);
197
+ await recordHumanDecision(gate, {
198
+ response: answer.startsWith('User ') ? 'done' : answer,
199
+ device: result?.device,
200
+ });
97
201
  return answer;
98
202
  }
99
203
 
@@ -125,7 +229,7 @@ const DISMISSED =
125
229
  'User closed the questions without answering. Proceed with your best judgment; ask again only if something new depends on it.';
126
230
 
127
231
  /** Wire answers -> array aligned with questions; invalid entries become null. */
128
- export function normalizeAskUserAnswers(answers, questions) {
232
+ function normalizeAskUserAnswers(answers, questions) {
129
233
  const list = Array.isArray(answers) ? answers.slice(0, 4) : [];
130
234
  return questions.map((q, i) => {
131
235
  const entry = list[i];
@@ -143,7 +247,7 @@ export function normalizeAskUserAnswers(answers, questions) {
143
247
 
144
248
  // Strings are JSON-quoted so a quote inside one cannot end it early; typed
145
249
  // text stays apart from the picked labels (`"picked" + "typed" (typed)`).
146
- export function formatAskUserAnswers(questions, answers) {
250
+ function formatAskUserAnswers(questions, answers) {
147
251
  const quote = (text) => JSON.stringify(text);
148
252
  const parts = questions.map((q, i) => {
149
253
  const a = answers[i];
@@ -157,14 +261,14 @@ export function formatAskUserAnswers(questions, answers) {
157
261
 
158
262
  /** A composer message sent instead of (or with) picking: kept when it is a
159
263
  * non-empty string within the typed-answer limit, else dropped. */
160
- export function normalizeAskUserReply(reply) {
264
+ function normalizeAskUserReply(reply) {
161
265
  if (typeof reply !== 'string' || reply.length > ASK_USER_MAX_TEXT) return '';
162
266
  return reply.trim();
163
267
  }
164
268
 
165
269
  // `User replied in chat: "<reply>". ` leads, then the usual picks sentence
166
270
  // when any question was answered, else just the closing sentence.
167
- export function formatAskUserResult(questions, answers, reply) {
271
+ function formatAskUserResult(questions, answers, reply) {
168
272
  if (!reply) return formatAskUserAnswers(questions, answers);
169
273
  const lead = `User replied in chat: ${JSON.stringify(reply)}. `;
170
274
  if (answers.some(Boolean)) return `${lead}${formatAskUserAnswers(questions, answers)}`;
@@ -45,6 +45,8 @@ export function createBridgeToolHandlers({ rt }) {
45
45
  cwd: callerCwd,
46
46
  requestApproval: callerCtx?.toolApprovalHook,
47
47
  toolCallId: callerCtx?.toolCallId || null,
48
+ // The agent turn, as for the browser: it scopes a saved-login fill's protection.
49
+ turnId: callerCtx?.turnId || rt.session?.usageMetricsTurnId,
48
50
  signal: signalFor(callerCtx),
49
51
  });
50
52
  };