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
@@ -158,8 +158,9 @@ function runtimePaths(verDir) {
158
158
  // Download with retry
159
159
  // ---------------------------------------------------------------------------
160
160
 
161
- async function downloadWithRetry(url, destPath, expectedBytes) {
161
+ async function downloadWithRetry(url, destPath, expectedBytes, signal) {
162
162
  return downloadToFileWithRetry(url, destPath, {
163
+ signal,
163
164
  maxBytes: expectedBytes,
164
165
  expectedBytes,
165
166
  label: 'memory runtime download',
@@ -532,11 +533,12 @@ function publishRuntimeDir(runtimeBaseDir, stagingDir, key, sha256) {
532
533
  * staging lock, ending with the stale-version GC. Returns the installed
533
534
  * runtime-{version} directory.
534
535
  */
535
- async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, version, pkey }) {
536
+ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, version, pkey, signal }) {
536
537
  // tar is only required for the download/extract path. Probe here (not at
537
538
  // ensureRuntime entry) so a machine without tar can still reuse an
538
539
  // already-extracted, sha-matching cached runtime via the fast path.
539
540
  await probeTar();
541
+ signal?.throwIfAborted();
540
542
 
541
543
  __mixdogMemoryLog(`[runtime-fetcher] downloading runtime ${version} for ${pkey} (~${size} bytes) …\n`);
542
544
 
@@ -566,7 +568,7 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
566
568
  `[runtime-fetcher] retrying runtime download from fallback release repo — ${candidateUrl}\n`
567
569
  );
568
570
  }
569
- await downloadWithRetry(candidateUrl, tarPath, size);
571
+ await downloadWithRetry(candidateUrl, tarPath, size, signal);
570
572
  await verifySha256(tarPath, sha256);
571
573
  downloadOk = true;
572
574
  break;
@@ -575,9 +577,12 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
575
577
  try {
576
578
  rmSync(tarPath, { force: true });
577
579
  } catch {}
580
+ // A cancelled install tries no fallback repository.
581
+ signal?.throwIfAborted();
578
582
  }
579
583
  }
580
584
  if (!downloadOk && lastDownloadErr) throw lastDownloadErr;
585
+ signal?.throwIfAborted();
581
586
  await _extractRuntimeTarGz(tarPath, stagingDir, stagingDir);
582
587
  } finally {
583
588
  try {
@@ -591,6 +596,11 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
591
596
  } catch {}
592
597
  throw new Error(`[runtime-fetcher] download or verify failed for ${version}`);
593
598
  }
599
+ // Cancelled after the extract: drop the unpublished tree, publish nothing.
600
+ if (signal?.aborted) {
601
+ rmSync(stagingDir, { recursive: true, force: true });
602
+ signal.throwIfAborted();
603
+ }
594
604
 
595
605
  // Stamp sha256 inside staging dir.
596
606
  writeFileAtomicSync(join(stagingDir, '.version-sha256'), sha256);
@@ -615,6 +625,11 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
615
625
  gcRuntimeDir(runtimeBaseDir, key);
616
626
  } finally {
617
627
  releaseStagingLock(stagingLock);
628
+ // Publishing is synchronous past the last cancellation check, so an
629
+ // aborted signal here means nothing was published.
630
+ if (signal?.aborted) {
631
+ __mixdogMemoryLog(`[runtime-fetcher] install of ${version} cancelled; its staging was removed\n`);
632
+ }
618
633
  }
619
634
 
620
635
  __mixdogMemoryLog(`[runtime-fetcher] runtime ready at ${verDir}\n`);
@@ -631,7 +646,10 @@ export const _runtimeLayoutInternals = {
631
646
  runtimeInstallKey,
632
647
  };
633
648
 
634
- export async function ensureRuntime(dataDir) {
649
+ /** `signal` cancels a download/install in progress: its retry waits end, its
650
+ * partial files and staging lock are removed and nothing is published, so the
651
+ * next call starts a fresh install. */
652
+ export async function ensureRuntime(dataDir, { signal = null } = {}) {
635
653
  const key = resolve(dataDir);
636
654
  if (runtimeCache.has(key)) return runtimeCache.get(key);
637
655
 
@@ -655,7 +673,7 @@ export async function ensureRuntime(dataDir) {
655
673
 
656
674
  const verDir =
657
675
  cachedRuntimeDir(runtimeBaseDir, version, sha256) ??
658
- (await installRuntimeVersion({ runtimeBaseDir, url, sha256, size, version, pkey }));
676
+ (await installRuntimeVersion({ runtimeBaseDir, url, sha256, size, version, pkey, signal }));
659
677
 
660
678
  const result = { runtimeDir: verDir, ...runtimePaths(verDir), version };
661
679
  runtimeCache.set(key, result);
@@ -6,9 +6,12 @@
6
6
  *
7
7
  * Rows are append-only: compaction may drop old messages from a session file,
8
8
  * but their search rows stay, so earlier conversation remains findable until
9
- * the session itself is deleted.
9
+ * the session itself is deleted. The one rewrite: a value marked sensitive
10
+ * replaces the rows that hold it (replaceSessionSearchMessages).
10
11
  */
11
12
 
13
+ import { randomBytes } from 'node:crypto';
14
+
12
15
  import { ensurePgInstance, withSchemaBootstrapLock } from '../memory/lib/pg/adapter.mjs';
13
16
  import { buildFtsPrefixQuery } from '../memory/lib/memory-text-utils.mjs';
14
17
  import {
@@ -91,13 +94,9 @@ export function sessionSearchRows(sessionId, messages) {
91
94
  return rows;
92
95
  }
93
96
 
94
- /** Idempotent append of a transcript's searchable messages; returns the count
95
- * of newly stored rows. Re-sending an already stored message is a no-op. */
96
- export async function appendSessionSearchMessages(sessionId, messages, { dataDir } = {}) {
97
- const rows = sessionSearchRows(sessionId, messages);
97
+ async function insertRows(queryable, rows) {
98
98
  if (rows.length === 0) return 0;
99
- const db = await getDb(dataDir);
100
- const result = await db.query(
99
+ const result = await queryable.query(
101
100
  `INSERT INTO session_search.messages (source_ref, session_id, ordinal, role, ts, content)
102
101
  SELECT * FROM unnest($1::text[], $2::text[], $3::int[], $4::text[], $5::bigint[], $6::text[])
103
102
  ON CONFLICT (source_ref) DO NOTHING`,
@@ -113,8 +112,73 @@ export async function appendSessionSearchMessages(sessionId, messages, { dataDir
113
112
  return Number(result?.rowCount) || 0;
114
113
  }
115
114
 
116
- export async function deleteSessionSearchMessages(sessionId, { dataDir } = {}) {
117
- const db = await getDb(dataDir);
115
+ /** Idempotent append of a transcript's searchable messages; returns the count
116
+ * of newly stored rows. Re-sending an already stored message is a no-op. */
117
+ export async function appendSessionSearchMessages(sessionId, messages, { dataDir, signal } = {}) {
118
+ const rows = sessionSearchRows(sessionId, messages);
119
+ if (rows.length === 0) return 0;
120
+ const db = await getDb(dataDir, { signal });
121
+ return insertRows(db, rows);
122
+ }
123
+
124
+ /** In one transaction: every stored row of `sessionId` whose content
125
+ * `redactText` changes is deleted, the rows of the transcript as stored
126
+ * (`redactedMessages`) are written, and a deleted row that is no message of
127
+ * the transcript is written again with its content redacted.
128
+ *
129
+ * A row is matched to its message by the row's own identity
130
+ * (stableSessionSourceRef: role, original timestamp or else ordinal, tool
131
+ * ids, content): the id `plainMessages` — the same transcript before
132
+ * redaction — gives that message. Every other deleted row (its message was
133
+ * compacted away, or is under an identity the transcript no longer has) has
134
+ * no message to derive an identity from; its replacement gets a fresh random
135
+ * id, so no two rows ever merge and no id is derived from the plaintext.
136
+ * Returns the count of deleted rows.
137
+ *
138
+ * FOR UPDATE locks only the rows that exist: a write of the same session still
139
+ * in flight is not seen, so callers must not overlap it with this one
140
+ * (session-search-ingest.mjs runs a session's writes one at a time). */
141
+ export async function replaceSessionSearchMessages(
142
+ sessionId,
143
+ plainMessages,
144
+ redactedMessages,
145
+ redactText,
146
+ { dataDir, signal } = {}
147
+ ) {
148
+ const rows = sessionSearchRows(sessionId, redactedMessages);
149
+ const previous = new Set(sessionSearchRows(sessionId, plainMessages).map((row) => row.sourceRef));
150
+ const db = await getDb(dataDir, { signal });
151
+ return db.transaction(async (tx) => {
152
+ const { rows: stored } = await tx.query(
153
+ `SELECT source_ref, ordinal, role, ts, content FROM session_search.messages
154
+ WHERE session_id = $1 FOR UPDATE`,
155
+ [sessionId]
156
+ );
157
+ const changed = stored
158
+ .map((row) => ({ row, content: redactText(String(row.content)) }))
159
+ .filter(({ row, content }) => content !== row.content);
160
+ if (changed.length) {
161
+ await tx.query(`DELETE FROM session_search.messages WHERE source_ref = ANY($1::text[])`, [
162
+ changed.map(({ row }) => row.source_ref),
163
+ ]);
164
+ }
165
+ const kept = changed
166
+ .filter(({ row }) => !previous.has(row.source_ref))
167
+ .map(({ row, content }) => ({
168
+ sourceRef: `session:${sessionId}:${randomBytes(12).toString('hex')}`,
169
+ sessionId,
170
+ ordinal: Number(row.ordinal),
171
+ role: row.role,
172
+ ts: row.ts == null ? null : Number(row.ts),
173
+ content,
174
+ }));
175
+ await insertRows(tx, [...rows, ...kept]);
176
+ return changed.length;
177
+ });
178
+ }
179
+
180
+ export async function deleteSessionSearchMessages(sessionId, { dataDir, signal } = {}) {
181
+ const db = await getDb(dataDir, { signal });
118
182
  await db.query(`DELETE FROM session_search.messages WHERE session_id = $1`, [sessionId]);
119
183
  await db.query(`DELETE FROM session_search.backfill WHERE session_id = $1`, [sessionId]);
120
184
  }
@@ -2,19 +2,32 @@
2
2
  * Keeps the PG session-search copy (session-search-db.mjs) in step with the
3
3
  * session store: per-turn ingest, a once-per-run backfill of stored sessions,
4
4
  * and deletion. Everything is best-effort and never touches the memory runtime.
5
+ *
6
+ * A session's writes run one at a time, in the order they were issued
7
+ * (serialized). That order is what keeps a value marked sensitive out of the
8
+ * index: the replacement that redacts the stored rows
9
+ * (replaceSessionSearchMessages) locks only rows that already exist, so an
10
+ * earlier plaintext append still in flight would otherwise land after it and
11
+ * bring the value back. A write also redacts with the values known when it
12
+ * runs, not when it was issued.
5
13
  */
6
14
  import { readdirSync, statSync } from 'node:fs';
7
15
  import { join } from 'node:path';
8
16
  import { isAgentOnlySession } from '../agent/orchestrator/session/store-summary-visibility.mjs';
17
+ import { redactSensitiveText, sensitiveValuesVersion } from '../agent/orchestrator/session/sensitive-values.mjs';
18
+ import { redactSensitiveMessages } from '../agent/orchestrator/session/sensitive-projection.mjs';
9
19
  import {
10
20
  appendSessionSearchMessages,
11
21
  deleteSessionSearchMessages,
12
22
  readSessionSearchBackfillStamps,
13
23
  readSessionSearchSessionIds,
14
24
  recordSessionSearchBackfillStamp,
25
+ replaceSessionSearchMessages,
15
26
  } from './session-search-db.mjs';
16
27
 
17
- const lastIngestedLength = new Map();
28
+ /** sessionId → { length, version }: the transcript length and the
29
+ * sensitive-values version (sensitive-values.mjs) of the last stored ingest. */
30
+ const lastIngested = new Map();
18
31
  const LAST_INGESTED_LIMIT = 500;
19
32
 
20
33
  function logFailure(what, error) {
@@ -23,6 +36,87 @@ function logFailure(what, error) {
23
36
  } catch {}
24
37
  }
25
38
 
39
+ /** sessionId → { tail, controller }: the tail of the session's write queue
40
+ * (see serialized) and the lifetime its writes run under (closeSessionSearch
41
+ * aborts it). Present only while a write of the session is queued or running. */
42
+ const writeQueues = new Map();
43
+
44
+ /** The reason a closed session's writes stop with; never reported as a failure. */
45
+ class SessionSearchClosedError extends Error {
46
+ constructor(sessionId) {
47
+ super(`session search writes of ${sessionId} closed`);
48
+ this.name = 'AbortError';
49
+ }
50
+ }
51
+
52
+ const reportUnlessClosed = (what) => (error) => {
53
+ if (!(error instanceof SessionSearchClosedError)) logFailure(what, error);
54
+ };
55
+
56
+ /** Runs `task(signal)` once every earlier task of `sessionId` has settled; the
57
+ * task's own promise is returned, so its failure reaches the caller to report.
58
+ * The queue moves on past a failed task: that failure is the caller's, not the
59
+ * next write's. `signal` is the session's write lifetime. */
60
+ function serialized(sessionId, task) {
61
+ const queue = writeQueues.get(sessionId);
62
+ const controller = queue && !queue.controller.signal.aborted ? queue.controller : new AbortController();
63
+ // A write cancelled while queued never starts (rejects with the close reason).
64
+ const run = (queue?.tail ?? Promise.resolve()).then(() => {
65
+ controller.signal.throwIfAborted();
66
+ return task(controller.signal);
67
+ });
68
+ const tail = run.then(
69
+ () => {},
70
+ () => {}
71
+ );
72
+ const entry = { tail, controller };
73
+ writeQueues.set(sessionId, entry);
74
+ tail.then(() => {
75
+ if (writeQueues.get(sessionId) === entry) writeQueues.delete(sessionId);
76
+ });
77
+ return run;
78
+ }
79
+
80
+ /**
81
+ * Stops the session's search writes for a runtime close: the queued ones never
82
+ * start and the running one is cancelled — a PostgreSQL runtime install or
83
+ * start it is waiting on included, once no other writer still waits on it
84
+ * (createSharedAbortable). Resolves when the queue has settled, so nothing this
85
+ * session issued writes into the data dir afterwards. The rows are derived:
86
+ * the stored-session backfill restores what a cancelled write left out.
87
+ */
88
+ export async function closeSessionSearch(sessionId) {
89
+ const queue = writeQueues.get(sessionId);
90
+ if (!queue) return;
91
+ queue.controller.abort(new SessionSearchClosedError(sessionId));
92
+ await queue.tail;
93
+ }
94
+
95
+ /** Stores `messages`, the transcript as it was when the turn was ingested, as
96
+ * the session's search rows; runs in the session's write queue. */
97
+ async function storeTranscript(sessionId, messages, signal) {
98
+ // A turn always grows the transcript; an unchanged length means nothing
99
+ // new. This is only a shortcut: the insert itself is idempotent.
100
+ const length = messages.length;
101
+ const version = sensitiveValuesVersion(sessionId);
102
+ const last = lastIngested.get(sessionId);
103
+ if (last?.length === length && last.version === version) return;
104
+ // A value a sensitive fill marked is stored as a marker here too. While
105
+ // the session has values not yet applied to its stored rows (rows made
106
+ // before the mark, compacted ones included), the rows that hold one are
107
+ // replaced in one transaction instead of only appending.
108
+ const redacted = redactSensitiveMessages(sessionId, messages);
109
+ if (version && last?.version !== version) {
110
+ await replaceSessionSearchMessages(sessionId, messages, redacted, (text) => redactSensitiveText(sessionId, text), {
111
+ signal,
112
+ });
113
+ } else {
114
+ await appendSessionSearchMessages(sessionId, redacted, { signal });
115
+ }
116
+ if (lastIngested.size >= LAST_INGESTED_LIMIT) lastIngested.clear();
117
+ lastIngested.set(sessionId, { length, version });
118
+ }
119
+
26
120
  /** Fire-and-forget: store a completed turn's transcript. Never throws. */
27
121
  export function ingestSessionTurn(session) {
28
122
  try {
@@ -30,30 +124,24 @@ export function ingestSessionTurn(session) {
30
124
  const messages = session?.messages;
31
125
  if (!sessionId || !Array.isArray(messages) || messages.length === 0) return;
32
126
  if (isAgentOnlySession(session)) return;
33
- // A turn always grows the transcript; an unchanged length means nothing
34
- // new. This is only a shortcut: the insert itself is idempotent.
35
- const length = messages.length;
36
- if (lastIngestedLength.get(sessionId) === length) return;
37
- appendSessionSearchMessages(sessionId, messages).then(
38
- () => {
39
- if (lastIngestedLength.size >= LAST_INGESTED_LIMIT) lastIngestedLength.clear();
40
- lastIngestedLength.set(sessionId, length);
41
- },
42
- (error) => logFailure(`ingest ${sessionId}`, error)
127
+ // The write runs after the session's earlier ones, so it keeps the
128
+ // transcript of this call: compaction may rewrite the live array before then.
129
+ const transcript = messages.slice();
130
+ serialized(sessionId, (signal) => storeTranscript(sessionId, transcript, signal)).catch(
131
+ reportUnlessClosed(`ingest ${sessionId}`)
43
132
  );
44
133
  } catch (error) {
45
134
  logFailure('ingest', error);
46
135
  }
47
136
  }
48
137
 
49
- /** Best-effort removal of a deleted session's search rows. Never throws. */
138
+ /** Best-effort removal of a deleted session's search rows, after the
139
+ * session's pending writes. Never throws. */
50
140
  export async function forgetSessionSearch(sessionId) {
51
- lastIngestedLength.delete(sessionId);
52
- try {
53
- await deleteSessionSearchMessages(sessionId);
54
- } catch (error) {
55
- logFailure(`delete ${sessionId}`, error);
56
- }
141
+ await serialized(sessionId, async (signal) => {
142
+ lastIngested.delete(sessionId);
143
+ await deleteSessionSearchMessages(sessionId, { signal });
144
+ }).catch(reportUnlessClosed(`delete ${sessionId}`));
57
145
  }
58
146
 
59
147
  async function loadStoreDeps() {
@@ -89,8 +177,10 @@ export async function backfillSessionSearch({ deps } = {}) {
89
177
  if (stamps.get(sessionId) === stamp) continue;
90
178
  const session = readSession(dir, file);
91
179
  if (session?.id && !isAgentOnlySession(session)) {
92
- await appendSessionSearchMessages(session.id, session.messages);
93
- await recordSessionSearchBackfillStamp(session.id, stamp);
180
+ await serialized(session.id, async () => {
181
+ await appendSessionSearchMessages(session.id, redactSensitiveMessages(session.id, session.messages));
182
+ await recordSessionSearchBackfillStamp(session.id, stamp);
183
+ });
94
184
  ingested += 1;
95
185
  }
96
186
  } catch (error) {
@@ -50,6 +50,95 @@ export async function runAbortable(signal, task, fallback) {
50
50
  });
51
51
  }
52
52
 
53
+ /** Resolves after `ms`, or rejects with the abort reason once `signal` aborts. */
54
+ export function sleepUnlessAborted(ms, signal) {
55
+ if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
56
+ return new Promise((resolve, reject) => {
57
+ if (signal.aborted) return reject(abortReason(signal));
58
+ const onAbort = () => {
59
+ clearTimeout(timer);
60
+ reject(abortReason(signal));
61
+ };
62
+ const timer = setTimeout(() => {
63
+ signal.removeEventListener('abort', onAbort);
64
+ resolve();
65
+ }, ms);
66
+ signal.addEventListener('abort', onAbort, { once: true });
67
+ });
68
+ }
69
+
70
+ /**
71
+ * One operation that several callers await. `start(signal)` runs once, when
72
+ * the first caller that still wants it joins — a live signal, or none — so a
73
+ * caller that is already aborted starts nothing. Its signal aborts only when
74
+ * every caller that joined with a signal has aborted and no caller joined
75
+ * without one (such a caller keeps it running). The caller whose abort cancels
76
+ * the work waits for the work to settle, its cleanup included, so nothing it
77
+ * started outlives it; any other aborted caller stops waiting at once and
78
+ * leaves the work to the callers still on it. `onSettled(failed)` runs once
79
+ * the started work settles; `promise` is that work, null until started.
80
+ */
81
+ export function createSharedAbortable(start, { onSettled = null } = {}) {
82
+ const controller = new AbortController();
83
+ let waiting = 0;
84
+ let pinned = false;
85
+ let promise = null;
86
+ const begin = () => {
87
+ if (!promise) {
88
+ promise = Promise.resolve().then(() => start(controller.signal));
89
+ promise.then(
90
+ () => onSettled?.(false),
91
+ () => onSettled?.(true)
92
+ );
93
+ }
94
+ return promise;
95
+ };
96
+ return {
97
+ get promise() {
98
+ return promise;
99
+ },
100
+ join(signal) {
101
+ if (!signal) {
102
+ pinned = true;
103
+ return begin();
104
+ }
105
+ if (signal.aborted) return Promise.reject(abortReason(signal));
106
+ waiting += 1;
107
+ const work = begin();
108
+ return new Promise((resolve, reject) => {
109
+ const onAbort = () => {
110
+ waiting -= 1;
111
+ const reason = abortReason(signal);
112
+ if (waiting > 0 || pinned) {
113
+ reject(reason);
114
+ return;
115
+ }
116
+ controller.abort(reason);
117
+ work.then(
118
+ () => reject(reason),
119
+ () => reject(reason)
120
+ );
121
+ };
122
+ signal.addEventListener('abort', onAbort, { once: true });
123
+ work.then(
124
+ (value) => {
125
+ if (signal.aborted) return;
126
+ signal.removeEventListener('abort', onAbort);
127
+ waiting -= 1;
128
+ resolve(value);
129
+ },
130
+ (error) => {
131
+ if (signal.aborted) return;
132
+ signal.removeEventListener('abort', onAbort);
133
+ waiting -= 1;
134
+ reject(error);
135
+ }
136
+ );
137
+ });
138
+ },
139
+ };
140
+ }
141
+
53
142
  /**
54
143
  * Bound best-effort cleanup without cancelling the underlying work. The
55
144
  * detached promise remains observed and may still finish in the background.
@@ -42,7 +42,6 @@ export const MASCOT_EXPRESSIONS: readonly string[];
42
42
  export const MASCOT_INKS: Readonly<Record<string, string>>;
43
43
  export const MIXY_AVATAR: Readonly<MascotAvatar>;
44
44
  export const AGENT_AVATAR_STATES: readonly AgentAvatarState[];
45
- export const AGENT_AVATAR_LATER_STATES: readonly string[];
46
45
  export const AGENT_AVATAR_IMAGE_FILE: RegExp;
47
46
  export const AGENT_AVATAR_STATE_FILE: RegExp;
48
47
  export const AGENT_AVATAR_DIR: string;
@@ -45,8 +45,10 @@ const homeEvidenceCache = new Map();
45
45
 
46
46
  /** What `agentId`'s home thread (sessions/sess_home_<id>.json) holds:
47
47
  * whether it exists, whether it has the creation greeting, and whether the
48
- * user and the agent talked (any user message that is not a runtime turn,
49
- * or any assistant message). A missing or unreadable file holds nothing. */
48
+ * user talked in it: a message the user wrote (a user message that is not a
49
+ * runtime turn or the greeting). The agent's own messages never count: Mixy's
50
+ * heartbeat posts its reports there unasked. A missing or unreadable file
51
+ * holds nothing. */
50
52
  export function homeThreadEvidence(dataDir, agentId) {
51
53
  const sessionId = agentHomeSessionId(agentId);
52
54
  if (!dataDir || !sessionId) return NO_HOME;
@@ -69,7 +71,6 @@ export function homeThreadEvidence(dataDir, agentId) {
69
71
  let greeted = false;
70
72
  let chatted = false;
71
73
  for (const message of messages) {
72
- if (message?.role === 'assistant') chatted = true;
73
74
  if (message?.role !== 'user') continue;
74
75
  const text = messageText(message.content);
75
76
  if (CREATION_GREETING.test(text)) greeted = true;
@@ -1,7 +1,7 @@
1
1
  import { createWriteStream, rmSync } from 'node:fs';
2
2
  import { Transform } from 'node:stream';
3
3
  import { pipeline } from 'node:stream/promises';
4
- import { sleep } from './sleep.mjs';
4
+ import { sleepUnlessAborted } from './abort-race.mjs';
5
5
 
6
6
  export const MAX_NATIVE_BINARY_DOWNLOAD_BYTES = 256 * 1024 * 1024;
7
7
  const DEFAULT_RETRY_DELAYS_MS = [1000, 3000, 9000];
@@ -118,13 +118,17 @@ export async function downloadToFileWithRetry(
118
118
  timeoutMs = 180_000,
119
119
  retryDelaysMs = DEFAULT_RETRY_DELAYS_MS,
120
120
  onRetry = null,
121
+ // Cancels the request, its body and any retry wait; rejects with its reason.
122
+ signal = null,
121
123
  } = {}
122
124
  ) {
123
125
  const { maximum, expected } = downloadBounds(maxBytes, expectedBytes, label);
124
126
  let lastError;
125
127
  for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
128
+ signal?.throwIfAborted();
126
129
  try {
127
- const response = await fetchFn(url, { signal: AbortSignal.timeout(timeoutMs) });
130
+ const timeout = AbortSignal.timeout(timeoutMs);
131
+ const response = await fetchFn(url, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
128
132
  try {
129
133
  if (!response.ok) throw new DownloadHttpError(response.status, httpLabel, url);
130
134
  return await streamResponseToFile(response, destPath, {
@@ -142,12 +146,13 @@ export async function downloadToFileWithRetry(
142
146
  }
143
147
  }
144
148
  } catch (error) {
149
+ signal?.throwIfAborted();
145
150
  lastError = error;
146
151
  if (error instanceof DownloadHttpError && error.terminal) throw error;
147
152
  const delayMs = retryDelaysMs[attempt];
148
153
  if (delayMs === undefined) break;
149
154
  onRetry?.({ attempt: attempt + 1, delayMs, error });
150
- await sleep(delayMs);
155
+ await sleepUnlessAborted(delayMs, signal);
151
156
  }
152
157
  }
153
158
  throw lastError;
@@ -150,23 +150,12 @@ function readConfigRawCached() {
150
150
  return raw;
151
151
  }
152
152
 
153
- // Canonical on-disk channel shape. Provider-specific ids are durable. Schedule
154
- // arrays moved to the scheduler DB and prompt injection moved to runtime hooks,
155
- // so neither belongs in config anymore.
153
+ // Canonical on-disk channel shape. Schedules live in the scheduler DB and
154
+ // prompt injection moved to runtime hooks, so neither belongs in config.
156
155
  export function canonicalizeStoredChannelsConfig(value = {}) {
157
156
  const next = isPlainObject(value) ? { ...value } : {};
158
157
  delete next.promptInjection;
159
158
  delete next.schedules;
160
- delete next.nonInteractive;
161
- delete next.interactive;
162
-
163
- // Messaging (Discord/Telegram) is retired: scrub the provider selector and
164
- // per-provider routing sections from stored config.
165
- delete next.backend;
166
- delete next.provider;
167
- delete next.discord;
168
- delete next.telegram;
169
- delete next.channel;
170
159
  return next;
171
160
  }
172
161
 
@@ -1,15 +1,25 @@
1
1
  // What a multi-account connector server tells the model about itself (MCP
2
- // `instructions`): the account it acts for, and to ask which one when the
3
- // user did not say and several are connected. The deferred-tool manifest
4
- // (context/deferred-tools.mjs) reads the leading "Account: <address>" back.
2
+ // `instructions`), in one line: the connected accounts, and that each tool
3
+ // takes `account` (asked of the user when several are connected and none was
4
+ // named). An account that cannot be used right now is listed with why.
5
5
 
6
- /** The MCP instructions of a `service` server acting for `account` ('' when unknown). */
7
- export function accountInstructions(service, account) {
8
- if (!account) return undefined;
9
- return `Account: ${account}. This server acts only for this ${service} account; other ${service} accounts are separate servers. If several are connected and the user did not name one, ask which account before acting.`;
6
+ /** The MCP instructions naming `accounts` (`{ email, note? }`, `note` a short
7
+ * reason it is unusable such as "needs sign-in"; one without an address is
8
+ * left out), or undefined when none has one. */
9
+ export function connectorAccountsInstructions(accounts) {
10
+ const named = (Array.isArray(accounts) ? accounts : []).filter((account) => account?.email);
11
+ if (!named.length) return undefined;
12
+ const list = named.map(({ email, note }) => (note ? `${email} (${note})` : email)).join(', ');
13
+ return named.length === 1
14
+ ? `Accounts: ${list}. \`account\` may be left out.`
15
+ : `Accounts: ${list}. Pass \`account\`; if the user named none and there are several, ask which.`;
10
16
  }
11
17
 
12
- /** The account a server's instructions name, or ''. */
13
- export function accountOfInstructions(instructions) {
14
- return /^Account: (\S+?)\. /.exec(String(instructions || ''))?.[1] || '';
18
+ /** The account addresses instructions from connectorAccountsInstructions name. */
19
+ export function accountsOfInstructions(instructions) {
20
+ const list = /^Accounts: (.+?)\. (?:`account`|Pass `account`)/.exec(String(instructions || ''))?.[1] || '';
21
+ return list
22
+ .split(', ')
23
+ .map((part) => part.replace(/ \(.*\)$/, '').trim())
24
+ .filter(Boolean);
15
25
  }
@@ -51,3 +51,32 @@ export function gmailAddress(value: unknown): string;
51
51
  export function gmailAppPasswordValue(value: unknown): string;
52
52
  export function gmailAppPasswordAccount(cfg: unknown): string;
53
53
  export function gmailAppPasswordServerInput(name: string, email: string, password: string): Record<string, unknown>;
54
+
55
+ /** One stored account of a multi-account connector server. */
56
+ export interface ConnectorAccountEntry {
57
+ name: string;
58
+ url: string;
59
+ headers?: Record<string, string>;
60
+ oauthClientId?: string;
61
+ /** `false`: the account is kept but neither connected nor called. */
62
+ enabled?: boolean;
63
+ }
64
+ export function connectorAccountsUrl(id: string): string;
65
+ export function connectorAccountsOf(cfg: unknown): ConnectorAccountEntry[];
66
+ export function connectorAccountServer(
67
+ cfg: Record<string, unknown>,
68
+ account: ConnectorAccountEntry
69
+ ): Record<string, unknown>;
70
+ export function connectorAccountServers(cfg: unknown): Record<string, unknown>[];
71
+ export function connectorAccountEnabled(account: ConnectorAccountEntry): boolean;
72
+ export function connectorLiveAccountServers(cfg: unknown): Record<string, unknown>[];
73
+ export function withConnectorAccountEnabled(
74
+ cfg: Record<string, unknown>,
75
+ name: string,
76
+ enabled: boolean
77
+ ): Record<string, unknown>;
78
+ export function connectorAccountEntry(name: string, cfg: Record<string, unknown>): ConnectorAccountEntry;
79
+ export function mergeConnectorAccountServers(servers: Record<string, unknown>): {
80
+ servers: Record<string, unknown>;
81
+ merged: { connector: string; name: string; from: string[] }[];
82
+ };