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
@@ -0,0 +1,38 @@
1
+ /**
2
+ * src/tui/session/session-flow/drain-closed-queue.mjs - settling the queue of a
3
+ * session whose canonical close completed (drain.mjs admission `terminal`).
4
+ */
5
+ import { dropTuiSteeringPersist } from '../tui-steering-persist.mjs';
6
+
7
+ export function createClosedQueueSettler({
8
+ pending,
9
+ pendingNotificationKeys,
10
+ clearBlockedDrainRetry,
11
+ queue,
12
+ steering,
13
+ submissions,
14
+ }) {
15
+ // Canonical close settled: nothing queued can ever run. Every accepted
16
+ // submission is settled explicitly (rejected), never silently dropped, and
17
+ // its durable steering mirror is released so a restart cannot resurrect it
18
+ // into the closed conversation.
19
+ return function settleClosedQueue() {
20
+ const entries = pending.splice(0);
21
+ pendingNotificationKeys.clear();
22
+ clearBlockedDrainRetry();
23
+ if (entries.length === 0) return;
24
+ queue.removeQueuedEntries(entries);
25
+ const mirrored = entries.filter((entry) => steering.shouldMirrorSteeringEntry(entry) && entry.steeringPersistId);
26
+ if (mirrored.length > 0) {
27
+ void Promise.resolve(dropTuiSteeringPersist(steering.leadSessionId(), mirrored)).catch(() => {});
28
+ }
29
+ for (const entry of entries) {
30
+ if (entry.id) submissions.forget(entry.id);
31
+ try {
32
+ entry.onSettled?.({ status: 'rejected', reason: 'session-closed', result: null, session: null });
33
+ } catch {
34
+ // A throwing callback must not leave the remaining entries unsettled.
35
+ }
36
+ }
37
+ };
38
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * src/tui/session/session-flow/drain-enqueue.mjs - idempotent intake of the
3
+ * unified prompt queue (drain.mjs): admission fence, duplicate submissions,
4
+ * notification keys, task-wait wakes and the durable steering mirror.
5
+ */
6
+ import { isQueuedEntryEditable, isQueuedEntryVisible } from '../queue-helpers.mjs';
7
+ import { appendTuiSteeringPersist } from '../tui-steering-persist.mjs';
8
+
9
+ export function createEnqueue({
10
+ admission,
11
+ tuiDebug,
12
+ flags,
13
+ pending,
14
+ pendingNotificationKeys,
15
+ getState,
16
+ set,
17
+ flushEmitImmediate,
18
+ getRuntime,
19
+ queue,
20
+ steering,
21
+ submissions,
22
+ drain,
23
+ }) {
24
+ return function enqueue(text, options = {}) {
25
+ // Admission fence. While closing, only user prompts are accepted (retained
26
+ // so a failed close loses nothing); notification-driven/model-visible
27
+ // enqueues are refused. A closed session accepts nothing.
28
+ const state = admission();
29
+ const mode = options.mode || 'prompt';
30
+ if (state !== 'open' && (state === 'terminal' || mode !== 'prompt')) {
31
+ tuiDebug(`enqueue refused: session admission=${state} mode=${mode}`);
32
+ return false;
33
+ }
34
+ const submissionId = String(options.id || '').trim();
35
+ // Idempotent intake: a transport that re-sent this exact submission (its
36
+ // first attempt landed but the response was lost) must not double-post.
37
+ if (submissionId && submissions.has(submissionId)) {
38
+ tuiDebug(`prompt-duplicate id=${submissionId} ignored`);
39
+ // This is an idempotent success, not a rejected submit. Returning false
40
+ // makes Desktop restore and retry a message that is already queued.
41
+ return true;
42
+ }
43
+ const entry = queue.makeQueueEntry(text, options);
44
+ if (entry.mode === 'task-notification' && entry.key) {
45
+ if (pendingNotificationKeys.has(entry.key)) return false;
46
+ pendingNotificationKeys.add(entry.key);
47
+ }
48
+ submissions.remember(submissionId);
49
+ pending.push(entry);
50
+ // Typed input or a deliverable notification ends a passive task wait or
51
+ // shell hold so the next boundary hands it to the model now.
52
+ const wake = steering.taskWaitWakeForEntry(entry);
53
+ if (wake) {
54
+ try {
55
+ getRuntime()?.interruptTaskWait?.(wake, wake === 'notification' ? entry.execution?.id || null : null);
56
+ } catch {}
57
+ }
58
+ const needsDurableSteering =
59
+ (getState().busy || getState().commandBusy || flags.autoClearRunning || flags.quotaResumePending) &&
60
+ steering.shouldMirrorSteeringEntry(entry);
61
+ const persistence = needsDurableSteering ? appendTuiSteeringPersist(steering.leadSessionId(), entry) : null;
62
+ if (isQueuedEntryVisible(entry)) {
63
+ set({ queued: [...getState().queued, entry] });
64
+ if (isQueuedEntryEditable(entry)) flushEmitImmediate?.();
65
+ }
66
+ const queueAgeMs = Math.max(0, Date.now() - Number(entry.submittedAt || Date.now()));
67
+ tuiDebug(`prompt-queued id=${String(entry.id)} busy=${getState().busy ? 1 : 0} ageMs=${queueAgeMs}`);
68
+ if (getState().busy) tuiDebug(`busy-queue enqueue mode=${entry.mode} pending=${pending.length}`);
69
+ void drain();
70
+ if (options.awaitPersistence === true && persistence) {
71
+ return Promise.resolve(persistence).then((persisted) => {
72
+ if (persisted !== false) return true;
73
+ const index = pending.indexOf(entry);
74
+ // A turn already took the prompt while its durable mirror was being
75
+ // written: it was delivered, so rejecting it would hand the sender a
76
+ // copy of a message the model already has.
77
+ if (index < 0) return true;
78
+ pending.splice(index, 1);
79
+ queue.removeQueuedEntries([entry]);
80
+ if (submissionId) submissions.forget(submissionId);
81
+ return false;
82
+ });
83
+ }
84
+ return true;
85
+ };
86
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * src/tui/session/session-flow/drain-retry.mjs - the re-check timer of a drain
3
+ * that found a gate closed (drain.mjs). One timer at most, kept on
4
+ * `flags.blockedDrainRetryTimer`.
5
+ */
6
+
7
+ // Re-check cadence for a drain that found a gate closed. A gate is state, not
8
+ // an event: the queue re-reads it instead of trusting that whoever closes it
9
+ // will remember to kick the queue on the way out. Both timers exist only while
10
+ // the queue actually holds work, so an idle session never polls.
11
+ const BLOCKED_DRAIN_RETRY_MS = 50;
12
+ // Session commands (setModel/newSession/resume/auto-clear) release in seconds;
13
+ // an active turn can run for many minutes, so its gate is re-read coarsely.
14
+ export const ACTIVE_TURN_RECHECK_MS = 2_000;
15
+
16
+ export function createBlockedDrainRetry({ flags, pending, drain }) {
17
+ function scheduleBlockedDrainRetry(delayMs = BLOCKED_DRAIN_RETRY_MS) {
18
+ if (pending.length === 0 || flags.disposed) return;
19
+ if (flags.blockedDrainRetryTimer) return;
20
+ const timer = setTimeout(() => {
21
+ flags.blockedDrainRetryTimer = null;
22
+ if (pending.length > 0) void drain();
23
+ }, delayMs);
24
+ if (typeof timer.unref === 'function') timer.unref();
25
+ flags.blockedDrainRetryTimer = timer;
26
+ }
27
+
28
+ function clearBlockedDrainRetry() {
29
+ if (!flags.blockedDrainRetryTimer) return;
30
+ clearTimeout(flags.blockedDrainRetryTimer);
31
+ flags.blockedDrainRetryTimer = null;
32
+ }
33
+
34
+ return { scheduleBlockedDrainRetry, clearBlockedDrainRetry };
35
+ }
@@ -6,24 +6,18 @@
6
6
  import {
7
7
  isGoalQueuedEntry,
8
8
  isQueuedEntryEditable,
9
- isQueuedEntryVisible,
10
9
  isSlashQueuedEntry,
11
10
  mergePromptContents,
12
11
  mergePastedImages,
13
12
  mergePastedTexts,
14
13
  } from '../queue-helpers.mjs';
15
- import { appendTuiSteeringPersist, dropTuiSteeringPersist } from '../tui-steering-persist.mjs';
16
14
  import { parseModelVisibleCompletionWrapper } from '../agent-envelope.mjs';
17
15
  import { rewoundFailedTurnItems } from './failed-turn-rewind.mjs';
18
-
19
- // Re-check cadence for a drain that found a gate closed. A gate is state, not
20
- // an event: the queue re-reads it instead of trusting that whoever closes it
21
- // will remember to kick the queue on the way out. Both timers exist only while
22
- // the queue actually holds work, so an idle session never polls.
23
- const BLOCKED_DRAIN_RETRY_MS = 50;
24
- // Session commands (setModel/newSession/resume/auto-clear) release in seconds;
25
- // an active turn can run for many minutes, so its gate is re-read coarsely.
26
- const ACTIVE_TURN_RECHECK_MS = 2_000;
16
+ import { PROMPT_ORIGIN_AGENT_REPORT, PROMPT_ORIGIN_CHANNEL } from '../../../runtime/shared/prompt-origin.mjs';
17
+ import { ACTIVE_TURN_RECHECK_MS, createBlockedDrainRetry } from './drain-retry.mjs';
18
+ import { createClosedQueueSettler } from './drain-closed-queue.mjs';
19
+ import { createEnqueue } from './drain-enqueue.mjs';
20
+ import { sessionTurnPrelude } from '../../../runtime/shared/session-turn-prelude.mjs';
27
21
 
28
22
  function earliestSubmittedAt(batch) {
29
23
  const earliest = batch.reduce((min, entry) => {
@@ -89,6 +83,18 @@ function batchPromptSource(batch) {
89
83
  return undefined;
90
84
  }
91
85
 
86
+ // The batch's provenance for the prompt-cache warmer (cache-warm.mjs
87
+ // cacheWarmTurnKind): one inbound channel message makes it real external
88
+ // activity whatever its presentation; it is an agent report only when every
89
+ // entry is one (a user prompt merged into it is the user speaking).
90
+ function batchPromptOrigin(batch) {
91
+ if (batch.some((entry) => entry?.origin === PROMPT_ORIGIN_CHANNEL)) return { promptOrigin: PROMPT_ORIGIN_CHANNEL };
92
+ if (batch.length > 0 && batch.every((entry) => entry?.origin === PROMPT_ORIGIN_AGENT_REPORT)) {
93
+ return { promptOrigin: PROMPT_ORIGIN_AGENT_REPORT };
94
+ }
95
+ return {};
96
+ }
97
+
92
98
  /** The batch's transcript metadata; a batch of completion wakes also records
93
99
  * the execution ids it delivers (`consumedExecutionIds`, from each entry's
94
100
  * execution or resume keys; null for one with neither), the structured
@@ -117,6 +123,7 @@ export function turnOptionsForBatch(batch, steering) {
117
123
  );
118
124
  return {
119
125
  promptSource: batchPromptSource(batch),
126
+ ...batchPromptOrigin(batch),
120
127
  retryFailedTurn: batch[0]?.retryFailedTurn === true,
121
128
  retryTurnId: batch[0]?.retryTurnId || null,
122
129
  retryAttemptId: batch[0]?.retryAttemptId || null,
@@ -151,19 +158,36 @@ export function turnOptionsForBatch(batch, steering) {
151
158
  for (const entry of batch) {
152
159
  try {
153
160
  entry.onToolResult?.(message);
154
- } catch {}
161
+ } catch {
162
+ // One submitter's callback must not stop the batch's other entries.
163
+ }
155
164
  }
156
165
  },
157
166
  onSettled: (detail) => {
158
167
  for (const entry of batch) {
159
168
  try {
160
169
  entry.onSettled?.(detail);
161
- } catch {}
170
+ } catch {
171
+ // One submitter's callback must not stop the batch's other entries.
172
+ }
162
173
  }
163
174
  },
164
175
  };
165
176
  }
166
177
 
178
+ // A failed-turn retry: drop the failed turn's rows when it produced no
179
+ // output, so the resubmitted prompt renders once (the session rewinds the
180
+ // matching message under its own lock).
181
+ function rewindFailedTurnRows(batch, { getState, set, replaceItems }) {
182
+ const entry = batch[0];
183
+ if (entry?.retryFailedTurn !== true || entry.retryTurnId) return;
184
+ const rewound = rewoundFailedTurnItems(getState().items, entry.text);
185
+ if (!rewound) return;
186
+ set({
187
+ items: replaceItems(rewound, { preserveStreamingTail: true, preserveSpill: true, preserveTranscriptView: true }),
188
+ });
189
+ }
190
+
167
191
  export function createDrainLoop(
168
192
  bag,
169
193
  { queue, steering, submissions, flushDeferredClearedSessionUi, autoClearBeforeWake }
@@ -181,35 +205,7 @@ export function createDrainLoop(
181
205
  flushDeferredExecutionPendingResumeKick,
182
206
  } = bag;
183
207
 
184
- // A failed-turn retry: drop the failed turn's rows when it produced no
185
- // output, so the resubmitted prompt renders once (the session rewinds the
186
- // matching message under its own lock).
187
- function rewindFailedTurnRows(batch) {
188
- const entry = batch[0];
189
- if (entry?.retryFailedTurn !== true || entry.retryTurnId) return;
190
- const rewound = rewoundFailedTurnItems(getState().items, entry.text);
191
- if (!rewound) return;
192
- set({
193
- items: replaceItems(rewound, { preserveStreamingTail: true, preserveSpill: true, preserveTranscriptView: true }),
194
- });
195
- }
196
-
197
- function scheduleBlockedDrainRetry(delayMs = BLOCKED_DRAIN_RETRY_MS) {
198
- if (pending.length === 0 || flags.disposed) return;
199
- if (flags.blockedDrainRetryTimer) return;
200
- const timer = setTimeout(() => {
201
- flags.blockedDrainRetryTimer = null;
202
- if (pending.length > 0) void drain();
203
- }, delayMs);
204
- if (typeof timer.unref === 'function') timer.unref();
205
- flags.blockedDrainRetryTimer = timer;
206
- }
207
-
208
- function clearBlockedDrainRetry() {
209
- if (!flags.blockedDrainRetryTimer) return;
210
- clearTimeout(flags.blockedDrainRetryTimer);
211
- flags.blockedDrainRetryTimer = null;
212
- }
208
+ const { scheduleBlockedDrainRetry, clearBlockedDrainRetry } = createBlockedDrainRetry({ flags, pending, drain });
213
209
 
214
210
  function hasModelDrainablePending() {
215
211
  return pending.some((entry) => !isSlashQueuedEntry(entry));
@@ -217,45 +213,34 @@ export function createDrainLoop(
217
213
 
218
214
  const admission = () => flags.admission || 'open';
219
215
 
220
- // Canonical close settled: nothing queued can ever run. Every accepted
221
- // submission is settled explicitly (rejected), never silently dropped, and
222
- // its durable steering mirror is released so a restart cannot resurrect it
223
- // into the closed conversation.
224
- function settleClosedQueue() {
225
- const entries = pending.splice(0);
226
- pendingNotificationKeys.clear();
227
- clearBlockedDrainRetry();
228
- if (entries.length === 0) return;
229
- queue.removeQueuedEntries(entries);
230
- const mirrored = entries.filter((entry) => steering.shouldMirrorSteeringEntry(entry) && entry.steeringPersistId);
231
- if (mirrored.length > 0) {
232
- void Promise.resolve(dropTuiSteeringPersist(steering.leadSessionId(), mirrored)).catch(() => {});
233
- }
234
- for (const entry of entries) {
235
- if (entry.id) submissions.forget(entry.id);
236
- try {
237
- entry.onSettled?.({ status: 'rejected', reason: 'session-closed', result: null, session: null });
238
- } catch {}
239
- }
240
- }
216
+ const settleClosedQueue = createClosedQueueSettler({
217
+ pending,
218
+ pendingNotificationKeys,
219
+ clearBlockedDrainRetry,
220
+ queue,
221
+ steering,
222
+ submissions,
223
+ });
241
224
 
242
- async function drain() {
225
+ // Whether a drain may start now. A closed gate either settles (terminal),
226
+ // waits for its own release edge, or schedules a re-check of itself.
227
+ function mayStartDrain() {
243
228
  // Execution admission fence: a closing session starts nothing (the close
244
229
  // path re-kicks the queue if the durable close fails); a closed one
245
230
  // settles what was queued.
246
- if (admission() === 'closing') return;
231
+ if (admission() === 'closing') return false;
247
232
  if (admission() === 'terminal') {
248
233
  settleClosedQueue();
249
- return;
234
+ return false;
250
235
  }
251
- if (flags.draining) return;
236
+ if (flags.draining) return false;
252
237
  // Bail while any session command holds commandBusy (auto-clear implies it,
253
238
  // but so do setModel/newSession/resume/etc). Running a turn concurrently
254
239
  // with a command that swaps or reroutes the live session is a race; the
255
240
  // commandBusy-release hook re-kicks drain once the command finishes.
256
241
  if (flags.autoClearRunning || flags.quotaResumePending || getState().commandBusy) {
257
242
  scheduleBlockedDrainRetry();
258
- return;
243
+ return false;
259
244
  }
260
245
  // A queued prompt/notification can arrive while a
261
246
  // provider turn is already in flight (scheduled message, webhook, agent
@@ -273,8 +258,44 @@ export function createDrainLoop(
273
258
  // a lead idled 24 minutes on ten queued agent completions and only moved
274
259
  // when the user typed. Queued work re-reads this gate on its own.
275
260
  scheduleBlockedDrainRetry(ACTIVE_TURN_RECHECK_MS);
276
- return;
261
+ return false;
277
262
  }
263
+ return true;
264
+ }
265
+
266
+ // Slash commands must run through the TUI command dispatcher, not be
267
+ // delivered to the model as plain text, so slash entries are handled
268
+ // outside the queued-command drain.
269
+ function dequeueModelBatch(firstBatch) {
270
+ return queue.dequeueQueueBatch('later', {
271
+ limit: firstBatch ? 1 : Infinity,
272
+ predicate: (entry) => !isSlashQueuedEntry(entry),
273
+ });
274
+ }
275
+
276
+ // The batch's rows go on the transcript before its turn starts; returns the
277
+ // merged prompt the turn sends.
278
+ function presentBatch(batch) {
279
+ tuiDebug(`busy-queue drain batch=${batch.length} remaining=${pending.length}`);
280
+ const merged = mergePromptContents(batch);
281
+ rewindFailedTurnRows(batch, { getState, set, replaceItems });
282
+ renderBatchItems(batch, pushUserOrSyntheticItem);
283
+ return merged;
284
+ }
285
+
286
+ // The drain that still owns the loop hands off: settle a closed queue, run
287
+ // again for model work that arrived meanwhile, or release the deferred
288
+ // pending-resume kick.
289
+ function finishDrain() {
290
+ flags.draining = false;
291
+ flushDeferredClearedSessionUi();
292
+ if (admission() === 'terminal') settleClosedQueue();
293
+ else if (hasModelDrainablePending()) void drain();
294
+ else flushDeferredExecutionPendingResumeKick();
295
+ }
296
+
297
+ async function drain() {
298
+ if (!mayStartDrain()) return;
278
299
  clearBlockedDrainRetry();
279
300
  const drainEpoch = (Number(flags.drainEpoch) || 0) + 1;
280
301
  flags.drainEpoch = drainEpoch;
@@ -293,13 +314,7 @@ export function createDrainLoop(
293
314
  // Drain one priority/mode bucket at a time (unified command queue):
294
315
  // prompt steering stays editable and task notifications stay
295
316
  // non-editable but model-visible.
296
- const batch = queue.dequeueQueueBatch('later', {
297
- limit: firstBatch ? 1 : Infinity,
298
- // Slash commands must run through the TUI command dispatcher, not be
299
- // delivered to the model as plain text, so slash entries are handled
300
- // outside the queued-command drain.
301
- predicate: (entry) => !isSlashQueuedEntry(entry),
302
- });
317
+ const batch = dequeueModelBatch(firstBatch);
303
318
  firstBatch = false;
304
319
  if (batch.length === 0) break;
305
320
  if (isGoalQueuedEntry(batch[0]) && bag.shouldRunGoalContinuation?.(batch[0]) !== true) {
@@ -314,10 +329,17 @@ export function createDrainLoop(
314
329
  return;
315
330
  }
316
331
  }
317
- tuiDebug(`busy-queue drain batch=${batch.length} remaining=${pending.length}`);
318
- const merged = mergePromptContents(batch);
319
- rewindFailedTurnRows(batch);
320
- renderBatchItems(batch, pushUserOrSyntheticItem);
332
+ // Rows owed to this thread before its next prompt (a bot's progress
333
+ // updates from the sessions it handed work to) land first.
334
+ const prelude = sessionTurnPrelude(bag.runtime?.id);
335
+ if (prelude) {
336
+ await prelude.catch((error) => tuiDebug(`turn prelude failed: ${error?.message || error}`));
337
+ if (flags.drainEpoch !== drainEpoch || admission() !== 'open' || getState().commandBusy) {
338
+ queue.requeueEntriesFront(batch);
339
+ return;
340
+ }
341
+ }
342
+ const merged = presentBatch(batch);
321
343
  const turnStatus = await bag.runTurn(merged, turnOptionsForBatch(batch, steering));
322
344
  if (flags.drainEpoch !== drainEpoch) return;
323
345
  // A deferred cleared-session UI sync (from a late-settling abandoned
@@ -330,78 +352,25 @@ export function createDrainLoop(
330
352
  if (turnStatus === 'cancelled' && pending.length === 0) break;
331
353
  }
332
354
  } finally {
333
- if (flags.drainEpoch === drainEpoch) {
334
- flags.draining = false;
335
- flushDeferredClearedSessionUi();
336
- if (admission() === 'terminal') settleClosedQueue();
337
- else if (hasModelDrainablePending()) void drain();
338
- else flushDeferredExecutionPendingResumeKick();
339
- }
355
+ if (flags.drainEpoch === drainEpoch) finishDrain();
340
356
  }
341
357
  }
342
358
 
343
- function enqueue(text, options = {}) {
344
- // Admission fence. While closing, only user prompts are accepted (retained
345
- // so a failed close loses nothing); notification-driven/model-visible
346
- // enqueues are refused. A closed session accepts nothing.
347
- const state = admission();
348
- const mode = options.mode || 'prompt';
349
- if (state !== 'open' && (state === 'terminal' || mode !== 'prompt')) {
350
- tuiDebug(`enqueue refused: session admission=${state} mode=${mode}`);
351
- return false;
352
- }
353
- const submissionId = String(options.id || '').trim();
354
- // Idempotent intake: a transport that re-sent this exact submission (its
355
- // first attempt landed but the response was lost) must not double-post.
356
- if (submissionId && submissions.has(submissionId)) {
357
- tuiDebug(`prompt-duplicate id=${submissionId} ignored`);
358
- // This is an idempotent success, not a rejected submit. Returning false
359
- // makes Desktop restore and retry a message that is already queued.
360
- return true;
361
- }
362
- const entry = queue.makeQueueEntry(text, options);
363
- if (entry.mode === 'task-notification' && entry.key) {
364
- if (pendingNotificationKeys.has(entry.key)) return false;
365
- pendingNotificationKeys.add(entry.key);
366
- }
367
- submissions.remember(submissionId);
368
- pending.push(entry);
369
- // Typed input or a deliverable notification ends a passive task wait or
370
- // shell hold so the next boundary hands it to the model now.
371
- const wake = steering.taskWaitWakeForEntry(entry);
372
- if (wake) {
373
- try {
374
- bag.runtime?.interruptTaskWait?.(wake, wake === 'notification' ? entry.execution?.id || null : null);
375
- } catch {}
376
- }
377
- const needsDurableSteering =
378
- (getState().busy || getState().commandBusy || flags.autoClearRunning || flags.quotaResumePending) &&
379
- steering.shouldMirrorSteeringEntry(entry);
380
- const persistence = needsDurableSteering ? appendTuiSteeringPersist(steering.leadSessionId(), entry) : null;
381
- if (isQueuedEntryVisible(entry)) {
382
- set({ queued: [...getState().queued, entry] });
383
- if (isQueuedEntryEditable(entry)) flushEmitImmediate?.();
384
- }
385
- const queueAgeMs = Math.max(0, Date.now() - Number(entry.submittedAt || Date.now()));
386
- tuiDebug(`prompt-queued id=${String(entry.id)} busy=${getState().busy ? 1 : 0} ageMs=${queueAgeMs}`);
387
- if (getState().busy) tuiDebug(`busy-queue enqueue mode=${entry.mode} pending=${pending.length}`);
388
- void drain();
389
- if (options.awaitPersistence === true && persistence) {
390
- return Promise.resolve(persistence).then((persisted) => {
391
- if (persisted !== false) return true;
392
- const index = pending.indexOf(entry);
393
- // A turn already took the prompt while its durable mirror was being
394
- // written: it was delivered, so rejecting it would hand the sender a
395
- // copy of a message the model already has.
396
- if (index < 0) return true;
397
- pending.splice(index, 1);
398
- queue.removeQueuedEntries([entry]);
399
- if (submissionId) submissions.forget(submissionId);
400
- return false;
401
- });
402
- }
403
- return true;
404
- }
359
+ const enqueue = createEnqueue({
360
+ admission,
361
+ tuiDebug,
362
+ flags,
363
+ pending,
364
+ pendingNotificationKeys,
365
+ getState,
366
+ set,
367
+ flushEmitImmediate,
368
+ getRuntime: () => bag.runtime,
369
+ queue,
370
+ steering,
371
+ submissions,
372
+ drain,
373
+ });
405
374
 
406
375
  return { drain, enqueue, settleClosedQueue };
407
376
  }
@@ -9,6 +9,7 @@ import {
9
9
  promptDisplayText,
10
10
  promptContentImageMeta,
11
11
  } from '../../queue-helpers.mjs';
12
+ import { promptOriginOf } from '../../../../runtime/shared/prompt-origin.mjs';
12
13
 
13
14
  export function makeQueueEntry(text, options = {}, nextId) {
14
15
  const mode = options.mode || 'prompt';
@@ -33,6 +34,10 @@ export function makeQueueEntry(text, options = {}, nextId) {
33
34
  ...(options.execution && typeof options.execution === 'object' ? { execution: { ...options.execution } } : {}),
34
35
  priority,
35
36
  key: options.key || null,
37
+ // Provenance apart from the mode (shared/prompt-origin.mjs): an inbound
38
+ // channel message queued as a task notification is real external
39
+ // activity; an agent report queued as a prompt is the runtime's.
40
+ origin: promptOriginOf(options.origin),
36
41
  skipSlashCommands: options.skipSlashCommands === true,
37
42
  displayText: mode === 'task-notification' ? notificationDisplayText(displayText) : String(displayText || ''),
38
43
  suppressDisplay: options.suppressDisplay === true,
@@ -19,6 +19,9 @@ const attributionOf = (entry) =>
19
19
  entry?.transcriptMeta?.hop || 0
20
20
  }`;
21
21
 
22
+ /** A retry of a failed turn runs alone: its options belong to its own turn. */
23
+ const isRetryEntry = (entry) => entry.retryFailedTurn === true || Boolean(entry.retryTurnId);
24
+
22
25
  export function createTakeEntriesOps({ pending, pendingNotificationKeys, removeQueuedEntries, leadSessionId }) {
23
26
  function dequeueQueueBatch(maxPriority = 'later', options = {}) {
24
27
  if (pending.length === 0) return [];
@@ -45,14 +48,14 @@ export function createTakeEntriesOps({ pending, pendingNotificationKeys, removeQ
45
48
  (entry.mode || 'prompt') === targetMode &&
46
49
  queuePriorityValue(entry.priority) === bestPriority
47
50
  ) {
48
- if (batch.length > 0 && entry.retryTurnId) break;
51
+ if (batch.length > 0 && isRetryEntry(entry)) break;
49
52
  // One turn records one sender: a bot's message never merges with the user's.
50
53
  if (batch.length > 0 && attributionOf(entry) !== attributionOf(batch[0])) break;
51
54
  batch.push(entry);
52
55
  pending.splice(i, 1);
53
56
  if (entry.mode === 'task-notification' && entry.key) pendingNotificationKeys.delete(entry.key);
54
57
  // A routine fire is a turn of its own, never merged with another.
55
- if (entry.retryTurnId || entry.mode === 'routine' || batch.length >= limit) break;
58
+ if (isRetryEntry(entry) || entry.mode === 'routine' || batch.length >= limit) break;
56
59
  } else {
57
60
  i += 1;
58
61
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * src/tui/session/tool-approval-delegation.mjs — where else a tool-approval
3
+ * request shows: the top-level chat above a sub-agent (`delegationSurface`) or
4
+ * the chats handing this session work (`handsOff`, `offerToHandoffs`;
5
+ * runtime/shared/delegated-approval-host.mjs).
6
+ */
7
+ import { agentIdOfHomeSession } from '../../runtime/agent/orchestrator/session/agent-home.mjs';
8
+ import { roomMemberOfSession } from '../../runtime/agent/orchestrator/session/agent-room.mjs';
9
+ import { delegateToHandoffs } from '../../runtime/shared/delegated-approval-host.mjs';
10
+
11
+ /** Requests a sub-agent (or a session a chat handed work to) shows in the
12
+ * conversation the user watches too. A secret and a takeover go there as
13
+ * well: the value goes to this PC by its request id and the sign-in window
14
+ * is bound to the asking session, wherever the card is answered. A
15
+ * sub-agent's question stays in its own session (its answer belongs to that
16
+ * conversation); a handed-off session's question shows in the chats handing
17
+ * it work too, beside their hand-off line (tool-approval.mjs
18
+ * `handoffQuestions`), never in their queue. */
19
+ const DELEGATED_KINDS = new Set(['', 'approval', 'outbound', 'permission', 'secret', 'takeover']);
20
+ const HANDOFF_KINDS = new Set([...DELEGATED_KINDS, 'question']);
21
+
22
+ export function createApprovalDelegation({ surfaceOf, ownSessionId }) {
23
+ /** `{ sessionId, chain }` of the top-level chat a sub-agent's request also
24
+ * shows in, or null (no delegation, another kind, or this is the top). */
25
+ function delegationSurface(request) {
26
+ if (typeof surfaceOf !== 'function' || !DELEGATED_KINDS.has(request.kind)) return null;
27
+ const own = String(request.sessionId || ownSessionId());
28
+ let surface = null;
29
+ try {
30
+ surface = surfaceOf(own);
31
+ } catch {
32
+ return null;
33
+ }
34
+ const sessionId = String(surface?.sessionId || '');
35
+ if (!sessionId || sessionId === own || sessionId === ownSessionId()) return null;
36
+ return { sessionId, chain: Array.isArray(surface.chain) ? surface.chain.map(String) : [] };
37
+ }
38
+ /** Whether a request this (top-level) session itself makes also shows in
39
+ * the chats handing it work (delegated-approval-host.mjs), while it waits. */
40
+ function handsOff(request) {
41
+ if (typeof surfaceOf !== 'function' || !HANDOFF_KINDS.has(request.kind)) return false;
42
+ const own = ownSessionId();
43
+ return Boolean(own) && String(request.sessionId || own) === own;
44
+ }
45
+ function offerToHandoffs(id, request, answer) {
46
+ const own = ownSessionId();
47
+ const agentId = agentIdOfHomeSession(own) || roomMemberOfSession(own)?.agentId || '';
48
+ // Each chat handing this session work names it its own way.
49
+ delegateToHandoffs({
50
+ id,
51
+ sessionId: own,
52
+ request,
53
+ answer,
54
+ copyFor: (chat, { title }) => ({
55
+ ...request,
56
+ sessionId: request.sessionId || own,
57
+ chain: [],
58
+ surfaceSessionId: chat,
59
+ handoff: { title, ...(agentId ? { agentId } : {}) },
60
+ }),
61
+ });
62
+ }
63
+ return { delegationSurface, handsOff, offerToHandoffs };
64
+ }