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
@@ -9,11 +9,11 @@
9
9
  import { createHash } from 'node:crypto';
10
10
  import keychain from '../../lib/keychain-cjs.cjs';
11
11
  import { GMAIL_APP_PASSWORD, gmailAddress } from '../shared/connector-catalog.mjs';
12
- import { accountInstructions } from '../shared/connector-accounts.mjs';
12
+ import { connectorAccountsInstructions } from '../shared/connector-accounts.mjs';
13
13
 
14
14
  const PROTOCOL_VERSION = '2025-06-18';
15
- export const GMAIL_IMAP = Object.freeze({ host: 'imap.gmail.com', port: 993, secure: true });
16
- export const GMAIL_SMTP = Object.freeze({ host: 'smtp.gmail.com', port: 465, secure: true });
15
+ const GMAIL_IMAP = Object.freeze({ host: 'imap.gmail.com', port: 993, secure: true });
16
+ const GMAIL_SMTP = Object.freeze({ host: 'smtp.gmail.com', port: 465, secure: true });
17
17
  const TIMEOUTS = { connectionTimeout: 20_000, greetingTimeout: 15_000, socketTimeout: 60_000 };
18
18
  const SEARCH_DEFAULT = 10;
19
19
  const SEARCH_MAX = 25;
@@ -48,7 +48,7 @@ const MESSAGES = {
48
48
 
49
49
  /** `GMAIL_APP_PASSWORD_<CODE>: <sentence>` — the code is what Desktop shows
50
50
  * in the user's language. */
51
- export class GmailAppPasswordError extends Error {
51
+ class GmailAppPasswordError extends Error {
52
52
  constructor(code, detail = '') {
53
53
  super(`GMAIL_APP_PASSWORD_${code}: ${MESSAGES[code]}${detail ? ` (${detail})` : ''}`);
54
54
  this.code = code;
@@ -101,7 +101,7 @@ function gmailError(error, password) {
101
101
  }
102
102
 
103
103
  // ── Clients (tests pass fakes with the same surface) ────────────────────────
104
- export const gmailClients = {
104
+ const gmailClients = {
105
105
  async imap({ email, password }) {
106
106
  const { ImapFlow } = await import('imapflow');
107
107
  const client = new ImapFlow({
@@ -186,6 +186,64 @@ const addresses = (list) =>
186
186
  .map((entry) => (entry?.name ? `${entry.name} <${entry.address || ''}>` : entry?.address || ''))
187
187
  .filter(Boolean)
188
188
  .join(', ');
189
+ /** The From mailboxes as Gmail parsed them, one entry each; a group shows as
190
+ * its name with an empty address (imapflow parseEnvelope). */
191
+ const mailboxes = (list) =>
192
+ (Array.isArray(list) ? list : []).map((entry) => ({
193
+ name: String(entry?.name || ''),
194
+ address: String(entry?.address || ''),
195
+ }));
196
+
197
+ const stripComments = (text) => {
198
+ let out = text;
199
+ for (let previous = ''; previous !== out; ) {
200
+ previous = out;
201
+ out = out.replace(/\([^()]*\)/g, ' ');
202
+ }
203
+ return out;
204
+ };
205
+
206
+ /** Gmail's own verdict on the sender, read from the topmost
207
+ * Authentication-Results only, and only when its authserv-id is Gmail's
208
+ * (mx.google.com):
209
+ * - the receiving MTA prepends the field above the trace fields it adds
210
+ * (RFC 8601 §4, §4.1), so the last receiver's copy is the first one;
211
+ * - a conforming MTA deletes any copy that arrives bearing its own
212
+ * authserv-id (RFC 8601 §5), so a sender cannot pass one off as Gmail's;
213
+ * - Gmail directs other mail clients to this field for its verdict (Gmail
214
+ * Help, "Check if your Gmail message is authenticated", answer 180707).
215
+ * A message no MTA delivered — one appended to the mailbox over IMAP — has
216
+ * only the headers its author wrote; that is the account owner's own mail.
217
+ * `{ dmarc: { result, from }, dkim: [{ result, domain }] }` (`domain` is a
218
+ * signature's d= as header.d reports it, RFC 8601 §2.7.1), or null. */
219
+ function gmailAuthentication(headers) {
220
+ const unfolded = String(headers || '').replace(/\r?\n[ \t]+/g, ' ');
221
+ const first = /^authentication-results:(.*)$/im.exec(unfolded)?.[1];
222
+ if (!first) return null;
223
+ const [server = '', ...results] = stripComments(first).split(';');
224
+ if (server.trim().split(/\s+/)[0].toLowerCase() !== 'mx.google.com') return null;
225
+ const verdict = { dmarc: null, dkim: [] };
226
+ for (const entry of results) {
227
+ const method = /^\s*([a-z0-9-]+)\s*=\s*([a-z]+)/i.exec(entry);
228
+ if (!method) continue;
229
+ const props = Object.fromEntries(
230
+ [...entry.matchAll(/([a-z]+\.[a-z]+)\s*=\s*("[^"]*"|[^\s;]+)/gi)].map(([, name, value]) => [
231
+ name.toLowerCase(),
232
+ value.replace(/^"|"$/g, '').toLowerCase(),
233
+ ])
234
+ );
235
+ const result = method[2].toLowerCase();
236
+ if (method[1].toLowerCase() === 'dmarc' && !verdict.dmarc)
237
+ verdict.dmarc = { result, from: props['header.from'] || '' };
238
+ // Only the signing domain d= counts toward the From domain (RFC 7489
239
+ // §3.1.1); the agent identity i= is not an alignment identifier.
240
+ if (method[1].toLowerCase() === 'dkim' && props['header.d']) {
241
+ verdict.dkim.push({ result, domain: props['header.d'] });
242
+ }
243
+ }
244
+ return verdict;
245
+ }
246
+
189
247
  const isoDate = (value) => {
190
248
  const date = value ? new Date(value) : null;
191
249
  return date && !Number.isNaN(date.getTime()) ? date.toISOString() : '';
@@ -214,25 +272,30 @@ const isAttachment = (node) =>
214
272
  String(node.type || '').startsWith('message/') ||
215
273
  Boolean(node.dispositionParameters?.filename || node.parameters?.name);
216
274
 
217
- /** The text/plain part (else text/html) that is the message's body. */
218
- function bodyPart(root) {
219
- let html = null;
275
+ /** The first text/plain and the first text/html part of the message that are
276
+ * not attachments (part numbers), each null without one. */
277
+ function textParts(root) {
278
+ const found = { plain: null, html: null };
220
279
  const walk = (node) => {
221
- if (!node || String(node.type || '').startsWith('message/')) return null;
280
+ if (!node || String(node.type || '').startsWith('message/')) return;
222
281
  if (node.childNodes?.length) {
223
- for (const child of node.childNodes) {
224
- const found = walk(child);
225
- if (found) return found;
226
- }
227
- return null;
282
+ for (const child of node.childNodes) walk(child);
283
+ return;
228
284
  }
229
- if (node.disposition === 'attachment') return null;
285
+ if (node.disposition === 'attachment') return;
230
286
  const part = node.part || '1';
231
- if (node.type === 'text/plain') return { part, html: false };
232
- if (node.type === 'text/html' && !html) html = { part, html: true };
233
- return null;
287
+ if (node.type === 'text/plain' && !found.plain) found.plain = part;
288
+ if (node.type === 'text/html' && !found.html) found.html = part;
234
289
  };
235
- return walk(root) || html;
290
+ walk(root);
291
+ return found;
292
+ }
293
+
294
+ /** The text/plain part (else text/html) that is the message's body. */
295
+ function bodyPart(root) {
296
+ const { plain, html } = textParts(root);
297
+ if (plain) return { part: plain, html: false };
298
+ return html ? { part: html, html: true } : null;
236
299
  }
237
300
 
238
301
  /** Attachments by name, type and (decoded, approximate) size only. */
@@ -264,16 +327,15 @@ const htmlText = (html) =>
264
327
  .replace(/ *\n\s*/g, '\n')
265
328
  .trim();
266
329
 
267
- /** Up to `maxBytes` of the body text, decoded to UTF-8. */
268
- async function bodyText(client, uid, structure, maxBytes) {
269
- const picked = bodyPart(structure);
270
- if (!picked) return '';
271
- const download = await client.download(String(uid), picked.part, {
330
+ /** Up to `maxBytes` of part `part`, decoded to UTF-8; `complete` is false when
331
+ * the part may go on past them. */
332
+ async function partText(client, uid, part, maxBytes) {
333
+ const download = await client.download(String(uid), part, {
272
334
  uid: true,
273
335
  maxBytes,
274
336
  chunkSize: Math.min(maxBytes, 65_536),
275
337
  });
276
- if (!download?.content) return '';
338
+ if (!download?.content) return { value: '', complete: true };
277
339
  const chunks = [];
278
340
  let size = 0;
279
341
  for await (const chunk of download.content) {
@@ -281,10 +343,63 @@ async function bodyText(client, uid, structure, maxBytes) {
281
343
  size += chunk.length;
282
344
  if (size >= maxBytes) break;
283
345
  }
284
- const value = Buffer.concat(chunks).subarray(0, maxBytes).toString('utf8');
346
+ return { value: Buffer.concat(chunks).subarray(0, maxBytes).toString('utf8'), complete: size < maxBytes };
347
+ }
348
+
349
+ /** Up to `maxBytes` of the body text, decoded to UTF-8. */
350
+ async function bodyText(client, uid, structure, maxBytes) {
351
+ const picked = bodyPart(structure);
352
+ if (!picked) return '';
353
+ const { value } = await partText(client, uid, picked.part, maxBytes);
285
354
  return picked.html ? htmlText(value) : value.trim();
286
355
  }
287
356
 
357
+ let domModule;
358
+ const loadDom = () => (domModule ||= import('jsdom'));
359
+
360
+ /** The targets of an HTML part's hyperlinks (`a` and `area` with href, HTML
361
+ * Standard §4.6 Links) as the HTML parser reads the attribute, character
362
+ * references decoded. A tag the download cut off is no element (HTML
363
+ * tokenization: the eof-in-tag error emits no tag). */
364
+ async function htmlLinks(html) {
365
+ if (!html) return [];
366
+ const { JSDOM } = await loadDom();
367
+ const dom = new JSDOM(html);
368
+ try {
369
+ return [...dom.window.document.querySelectorAll('a[href], area[href]')].map((node) => node.getAttribute('href'));
370
+ } finally {
371
+ dom.window.close();
372
+ }
373
+ }
374
+
375
+ // An absolute http(s) URI in plain text as RFC 3986 spells one: the scheme,
376
+ // then a run of URI characters — unreserved, reserved or percent-encoded
377
+ // (§2.1-§2.3). Whitespace, quotes and angle brackets are no URI characters,
378
+ // so they end it (Appendix C).
379
+ const TEXT_URI = /https?:\/\/(?:[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=]|%[0-9A-Fa-f]{2})+/gi;
380
+
381
+ const isWebUrl = (value) => {
382
+ try {
383
+ return ['http:', 'https:'].includes(new URL(value).protocol);
384
+ } catch {
385
+ return false;
386
+ }
387
+ };
388
+
389
+ /** The message's link targets, exactly as written: the hyperlinks of its
390
+ * HTML part, then the URIs of its text part (one the download cut off is
391
+ * left out). Absolute http(s) only, each once. */
392
+ function messageLinks(plain, hrefs) {
393
+ const links = [...hrefs];
394
+ if (plain) {
395
+ for (const match of plain.value.matchAll(TEXT_URI)) {
396
+ if (!plain.complete && match.index + match[0].length === plain.value.length) continue;
397
+ links.push(match[0]);
398
+ }
399
+ }
400
+ return [...new Set(links.filter((link) => typeof link === 'string' && isWebUrl(link)))];
401
+ }
402
+
288
403
  const references = (headers) =>
289
404
  String(headers || '')
290
405
  .replace(/\r?\n[ \t]+/g, ' ')
@@ -317,7 +432,9 @@ const LABEL_MAX = 225;
317
432
 
318
433
  /** The distinct message ids of `args.ids` (an array, or one id). */
319
434
  function messageIds(args) {
320
- const raw = Array.isArray(args.ids) ? args.ids : args.ids == null ? [] : [args.ids];
435
+ let raw = [];
436
+ if (Array.isArray(args.ids)) raw = args.ids;
437
+ else if (args.ids != null) raw = [args.ids];
321
438
  const ids = [...new Set(raw.map((id) => text(id)))];
322
439
  if (!ids.length || ids.some((id) => !id)) throw new ToolInputError('ids must list ids from search_emails');
323
440
  if (ids.length > MANAGE_MAX) throw new ToolInputError(`At most ${MANAGE_MAX} emails per call; got ${ids.length}.`);
@@ -519,7 +636,8 @@ const TOOLS = [
519
636
  },
520
637
  {
521
638
  name: 'read_email',
522
- description: 'Read one email by its id from search_emails: headers, text body, and attachment names and sizes.',
639
+ description:
640
+ 'Read one email by its id from search_emails: headers, text body, link targets, and attachment names and sizes.',
523
641
  inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
524
642
  annotations: { readOnlyHint: true, openWorldHint: true },
525
643
  async run(ctx, args) {
@@ -530,22 +648,38 @@ const TOOLS = [
530
648
  const [message] = uid
531
649
  ? await client.fetchAll(
532
650
  String(uid),
533
- { uid: true, envelope: true, internalDate: true, bodyStructure: true, threadId: true },
651
+ {
652
+ uid: true,
653
+ envelope: true,
654
+ internalDate: true,
655
+ bodyStructure: true,
656
+ threadId: true,
657
+ headers: ['authentication-results'],
658
+ },
534
659
  { uid: true }
535
660
  )
536
661
  : [];
537
662
  if (!message) throw new ToolInputError(`No email with id ${id}.`);
538
663
  const { envelope = {} } = message;
539
- const body = await bodyText(client, message.uid, message.bodyStructure, READ_BYTES);
664
+ const parts = textParts(message.bodyStructure);
665
+ const plain = parts.plain ? await partText(client, message.uid, parts.plain, READ_BYTES) : null;
666
+ const html = parts.html ? await partText(client, message.uid, parts.html, READ_BYTES) : null;
667
+ let body = '';
668
+ if (plain) body = plain.value.trim();
669
+ else if (html) body = htmlText(html.value);
670
+ const links = messageLinks(plain, await htmlLinks(html?.value));
540
671
  const head = {
541
672
  id: message.emailId || id,
542
673
  threadId: message.threadId,
543
674
  messageId: envelope.messageId,
544
675
  from: addresses(envelope.from),
676
+ fromAddresses: mailboxes(envelope.from),
677
+ authentication: gmailAuthentication(message.headers),
545
678
  to: addresses(envelope.to),
546
679
  cc: addresses(envelope.cc) || undefined,
547
680
  date: isoDate(envelope.date || message.internalDate),
548
681
  subject: text(envelope.subject),
682
+ links,
549
683
  attachments: attachmentsOf(message.bodyStructure),
550
684
  };
551
685
  const clipped = body.length > READ_CHARS ? `${body.slice(0, READ_CHARS)}\n[truncated]` : body;
@@ -588,6 +722,12 @@ const TOOLS = [
588
722
  ...MANAGE_TOOLS,
589
723
  ];
590
724
 
725
+ /** The tool definitions (name, description, schema, annotations) the server
726
+ * lists; with `hidden`, also the ones only the approval gate calls. */
727
+ export function gmailAppPasswordTools({ hidden = false } = {}) {
728
+ return TOOLS.filter((tool) => hidden || !tool.hidden).map(({ run, hidden: _hidden, ...tool }) => tool);
729
+ }
730
+
591
731
  // ── The MCP server at the reserved address ──────────────────────────────────
592
732
  const GMAIL_HOST = new URL(GMAIL_APP_PASSWORD.url).host;
593
733
 
@@ -616,18 +756,11 @@ async function rpc(session, message) {
616
756
  protocolVersion: text(params?.protocolVersion) || PROTOCOL_VERSION,
617
757
  capabilities: { tools: { listChanged: false } },
618
758
  serverInfo: { name: 'mixdog-gmail-app-password', version: '1.0.0' },
619
- instructions: accountInstructions('Gmail', session.email),
759
+ instructions: connectorAccountsInstructions([{ email: session.email }]),
620
760
  });
621
761
  }
622
762
  if (method === 'ping') return reply({});
623
- if (method === 'tools/list') {
624
- return reply({
625
- tools: TOOLS.filter((tool) => !tool.hidden).map(({ run, hidden, ...tool }) => ({
626
- ...tool,
627
- description: `${tool.description} (Gmail account: ${session.email})`,
628
- })),
629
- });
630
- }
763
+ if (method === 'tools/list') return reply({ tools: gmailAppPasswordTools() });
631
764
  if (method === 'tools/call') {
632
765
  const tool = TOOLS.find((candidate) => candidate.name === params?.name);
633
766
  if (!tool) return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } };
@@ -9,7 +9,7 @@
9
9
  // live in the keychain, and each MCP request carries the access token, which
10
10
  // this server spends on the Gmail / Calendar / Drive REST APIs.
11
11
 
12
- import { accountInstructions } from '../shared/connector-accounts.mjs';
12
+ import { connectorAccountsInstructions } from '../shared/connector-accounts.mjs';
13
13
 
14
14
  const PROTOCOL_VERSION = '2025-06-18';
15
15
  const ORIGIN_SUFFIX = '.mixdog.invalid';
@@ -329,6 +329,12 @@ const SERVICES = {
329
329
  drive: { name: 'Google Drive', tools: DRIVE_TOOLS },
330
330
  };
331
331
 
332
+ /** The tool definitions (name, description, schema, annotations) of built-in
333
+ * Google service `service` (`gmail`, `calendar`, `drive`). */
334
+ export function googleMcpTools(service) {
335
+ return (SERVICES[service]?.tools || []).map(({ run, ...tool }) => tool);
336
+ }
337
+
332
338
  /** The reserved MCP address of built-in Google service `service`. */
333
339
  export function googleMcpUrl(service) {
334
340
  return `https://${service}${ORIGIN_SUFFIX}/mcp`;
@@ -367,20 +373,12 @@ async function rpc(service, token, message) {
367
373
  protocolVersion: text(params?.protocolVersion) || PROTOCOL_VERSION,
368
374
  capabilities: { tools: { listChanged: false } },
369
375
  serverInfo: { name: `mixdog-${service.key}`, version: '1.0.0' },
370
- instructions: accountInstructions(service.name, info.email),
376
+ // The one account this sign-in is (the connector's accounts server reads it back).
377
+ instructions: connectorAccountsInstructions([{ email: info.email }]),
371
378
  });
372
379
  }
373
380
  if (method === 'ping') return reply({});
374
- if (method === 'tools/list') {
375
- // Several accounts of one service are separate servers: each tool says whose it is.
376
- const { email } = await tokenInfo(token).catch(() => ({ email: '' }));
377
- return reply({
378
- tools: service.tools.map(({ run, ...tool }) => ({
379
- ...tool,
380
- ...(email ? { description: `${tool.description} (${service.name} account: ${email})` } : {}),
381
- })),
382
- });
383
- }
381
+ if (method === 'tools/list') return reply({ tools: googleMcpTools(service.key) });
384
382
  if (method === 'tools/call') {
385
383
  const tool = service.tools.find((candidate) => candidate.name === params?.name);
386
384
  if (!tool) return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } };
@@ -91,6 +91,7 @@ function installPoolErrorHandler(pool, label, { onConnectionLoss } = {}) {
91
91
 
92
92
  import { resolve } from 'node:path';
93
93
  import { ensurePgInstance as supervisorEnsure, stopOwnedPgForShutdown } from './supervisor.mjs';
94
+ import { createSharedAbortable } from '../../../shared/abort-race.mjs';
94
95
 
95
96
  export { installOwnedPgExitStop, pgClusterExists } from './supervisor.mjs';
96
97
 
@@ -195,7 +196,7 @@ export function pendingDurablePgOps() {
195
196
  * Wait, bounded, until no durable-schema operation is in flight (including
196
197
  * ones issued while waiting). Resolves true when drained, false at the bound.
197
198
  */
198
- export async function drainDurablePgOps({ timeoutMs = 0 } = {}) {
199
+ async function drainDurablePgOps({ timeoutMs = 0 } = {}) {
199
200
  const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
200
201
  while (_durableInFlight.size > 0) {
201
202
  const remaining = deadline - Date.now();
@@ -488,78 +489,84 @@ export async function ensurePgInstance(dataDir, opts = {}) {
488
489
  const key = instanceKey(dataDir, schema);
489
490
 
490
491
  if (instances.has(key)) return instances.get(key);
491
- if (opening.has(key)) return opening.get(key);
492
-
493
- const promise = (async () => {
494
- // 1. Let supervisor-pg own PG startup and health-checking.
495
- // Returns { host, port, runtimeDir, pgdataDir }.
496
- const { host, port, runtimeDir, pgdataDir } = await supervisorEnsure(dataDir);
497
-
498
- // 2. Connect via node-postgres; auto-create the mixdog database if absent.
499
- const { default: pg } = await import('pg');
500
-
501
- const PG_USER = 'postgres';
502
- const PG_DB = 'mixdog';
503
-
504
- const adminPool = new pg.Pool({
505
- host,
506
- port,
507
- user: PG_USER,
508
- database: 'postgres',
509
- password: '',
510
- max: 1,
511
- idleTimeoutMillis: 5_000,
512
- });
513
- installPoolErrorHandler(adminPool, 'admin-pool');
514
- try {
515
- // CREATE DATABASE cannot run inside a transaction, so guard the
516
- // check-then-create with a session-level advisory lock held on a single
517
- // dedicated client. Concurrent first-boot workers block on the same
518
- // cluster-global key here and only one issues the CREATE; the rest see
519
- // the row present after the holder releases. Released in finally.
520
- await withClusterAdvisoryLock(adminPool, ADVISORY_OBJID_CREATE_DB, 'create-db', async (adminClient) => {
521
- const r = await adminClient.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [PG_DB]);
522
- if (r.rows.length === 0) {
523
- await adminClient.query(`CREATE DATABASE ${PG_DB}`);
524
- __mixdogMemoryLog(`[pg-adapter] created database ${PG_DB}\n`);
525
- }
492
+ // `opts.signal` withdraws this caller; the shared open is cancelled once
493
+ // every caller has (createSharedAbortable).
494
+ if (opening.has(key)) return opening.get(key).join(opts.signal);
495
+
496
+ const shared = createSharedAbortable(
497
+ async (signal) => {
498
+ // 1. Let supervisor-pg own PG startup and health-checking.
499
+ // Returns { host, port, runtimeDir, pgdataDir }.
500
+ const { host, port, runtimeDir, pgdataDir } = await supervisorEnsure(dataDir, { signal });
501
+ signal.throwIfAborted();
502
+
503
+ // 2. Connect via node-postgres; auto-create the mixdog database if absent.
504
+ const { default: pg } = await import('pg');
505
+
506
+ const PG_USER = 'postgres';
507
+ const PG_DB = 'mixdog';
508
+
509
+ const adminPool = new pg.Pool({
510
+ host,
511
+ port,
512
+ user: PG_USER,
513
+ database: 'postgres',
514
+ password: '',
515
+ max: 1,
516
+ idleTimeoutMillis: 5_000,
526
517
  });
527
- } finally {
528
- await adminPool.end();
529
- }
518
+ installPoolErrorHandler(adminPool, 'admin-pool');
519
+ try {
520
+ // CREATE DATABASE cannot run inside a transaction, so guard the
521
+ // check-then-create with a session-level advisory lock held on a single
522
+ // dedicated client. Concurrent first-boot workers block on the same
523
+ // cluster-global key here and only one issues the CREATE; the rest see
524
+ // the row present after the holder releases. Released in finally.
525
+ await withClusterAdvisoryLock(adminPool, ADVISORY_OBJID_CREATE_DB, 'create-db', async (adminClient) => {
526
+ const r = await adminClient.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [PG_DB]);
527
+ if (r.rows.length === 0) {
528
+ await adminClient.query(`CREATE DATABASE ${PG_DB}`);
529
+ __mixdogMemoryLog(`[pg-adapter] created database ${PG_DB}\n`);
530
+ }
531
+ });
532
+ } finally {
533
+ await adminPool.end();
534
+ }
530
535
 
531
- // 3. Production pool.
532
- const pgPool = new pg.Pool({
533
- host,
534
- port,
535
- user: PG_USER,
536
- database: PG_DB,
537
- password: '',
538
- max: 5,
539
- idleTimeoutMillis: 30_000,
540
- connectionTimeoutMillis: 10_000,
541
- });
542
- installPoolErrorHandler(pgPool, `pool:${schema}`, {
543
- onConnectionLoss: () => _recoverPgConnection(dataDir, schema),
544
- });
545
-
546
- // 4. Bootstrap extensions + schemas once (idempotent).
547
- await bootstrapInstance(pgPool, resolve(dataDir));
548
-
549
- // 5. Build the compat db shim.
550
- const db = makeCompatDb(pgPool, schema, dataDir);
551
-
552
- const result = { db, pool: pgPool, host, port, runtimeDir, pgdataDir };
553
- instances.set(key, result);
554
- return result;
555
- })();
536
+ // 3. Production pool.
537
+ const pgPool = new pg.Pool({
538
+ host,
539
+ port,
540
+ user: PG_USER,
541
+ database: PG_DB,
542
+ password: '',
543
+ max: 5,
544
+ idleTimeoutMillis: 30_000,
545
+ connectionTimeoutMillis: 10_000,
546
+ });
547
+ installPoolErrorHandler(pgPool, `pool:${schema}`, {
548
+ onConnectionLoss: () => _recoverPgConnection(dataDir, schema),
549
+ });
556
550
 
557
- opening.set(key, promise);
558
- try {
559
- return await promise;
560
- } finally {
561
- opening.delete(key);
562
- }
551
+ // 4. Bootstrap extensions + schemas once (idempotent).
552
+ await bootstrapInstance(pgPool, resolve(dataDir));
553
+
554
+ // 5. Build the compat db shim.
555
+ const db = makeCompatDb(pgPool, schema, dataDir);
556
+
557
+ const result = { db, pool: pgPool, host, port, runtimeDir, pgdataDir };
558
+ instances.set(key, result);
559
+ return result;
560
+ },
561
+ {
562
+ onSettled: () => {
563
+ if (opening.get(key) === shared) opening.delete(key);
564
+ },
565
+ }
566
+ );
567
+
568
+ opening.set(key, shared);
569
+ return shared.join(opts.signal);
563
570
  }
564
571
 
565
572
  // ---------------------------------------------------------------------------
@@ -30,6 +30,7 @@ import {
30
30
  import { withFileLockSync } from '../../../shared/atomic-file.mjs';
31
31
  import { isPidAlive } from '../../../shared/pid-liveness.mjs';
32
32
  import { envFlag } from '../../../shared/env.mjs';
33
+ import { createSharedAbortable } from '../../../shared/abort-race.mjs';
33
34
  import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../../../shared/runtime-root.mjs';
34
35
 
35
36
  let _pgProc = null;
@@ -51,7 +52,7 @@ async function _getPgProc() {
51
52
  /** @type {{ port: number, pgdata: string, runtimeDir: string, proc: unknown } | null} */
52
53
  let _live = null;
53
54
  /** Dedup: one ensure coroutine at a time. */
54
- let _ensureInFlight = null;
55
+ let _ensureShared = null;
55
56
  /** Per-process flag — postgresql.conf reconcile attempted once per supervisor. */
56
57
  let _v2ReconcileTried = false;
57
58
  /** Set by stopOwnedPgForShutdown: this process is exiting, no further ensure. */
@@ -542,7 +543,7 @@ async function tryReusePgInstance({ pgdata, runtimeDir, healthcheckPg, source =
542
543
  return null;
543
544
  }
544
545
 
545
- // ── Internal: full ensure logic (runs exclusively via _ensureInFlight) ────────
546
+ // ── Internal: full ensure logic (runs exclusively via _ensureShared) ──────────
546
547
 
547
548
  function _pgdataDir(dataDir) {
548
549
  return join(dataDir, 'pgdata');
@@ -555,13 +556,15 @@ export function pgClusterExists(dataDir) {
555
556
  return existsSync(join(_pgdataDir(dataDir), 'PG_VERSION'));
556
557
  }
557
558
 
558
- async function _doEnsure(dataDir) {
559
+ async function _doEnsure(dataDir, signal) {
559
560
  const { healthcheckPg } = await _getPgProc();
560
561
  const pgdata = _pgdataDir(dataDir);
561
562
 
562
563
  // Resolve runtimeDir via runtime-fetcher (cache-hits immediately when already downloaded).
563
564
  const { ensureRuntime } = await import('../runtime-fetcher.mjs');
564
- const { runtimeDir } = await ensureRuntime(dataDir);
565
+ const { runtimeDir } = await ensureRuntime(dataDir, { signal });
566
+ // Cancelled while installing: start no cluster for callers that are gone.
567
+ signal.throwIfAborted();
565
568
 
566
569
  // One-shot v2 conf reconcile — idempotent. Covers attach paths (in-process
567
570
  // fast path / cross-process active-instance reuse) where startPg is never
@@ -713,10 +716,15 @@ async function _doEnsure(dataDir) {
713
716
  * Concurrent calls are deduplicated — exactly one spawn/recover sequence runs
714
717
  * at a time; additional callers await the same promise.
715
718
  *
719
+ * `signal` (optional) withdraws this caller: the shared ensure is cancelled
720
+ * (its runtime install included) only once every caller has withdrawn and
721
+ * none joined without a signal (createSharedAbortable).
722
+ *
716
723
  * @param {string} dataDir Mixdog data directory.
724
+ * @param {{ signal?: AbortSignal }} [opts]
717
725
  * @returns {Promise<{ host: string, port: number, runtimeDir: string, pgdataDir: string }>}
718
726
  */
719
- export function ensurePgInstance(dataDir) {
727
+ export function ensurePgInstance(dataDir, { signal = null } = {}) {
720
728
  // Once this process began stopping the cluster it owns, nothing may start
721
729
  // or re-attach one: a late connection-loss recovery would otherwise bring a
722
730
  // postmaster back up right after the stop and leave it running past exit.
@@ -727,12 +735,15 @@ export function ensurePgInstance(dataDir) {
727
735
  // in-process fast path — so this is the one place the sweep cannot be
728
736
  // skipped. Latched internally, so repeated calls schedule it only once.
729
737
  scheduleOrphanTempPostmasterSweep();
730
- if (!_ensureInFlight) {
731
- _ensureInFlight = _doEnsure(dataDir).finally(() => {
732
- _ensureInFlight = null;
738
+ if (!_ensureShared) {
739
+ const shared = createSharedAbortable((sharedSignal) => _doEnsure(dataDir, sharedSignal), {
740
+ onSettled: () => {
741
+ if (_ensureShared === shared) _ensureShared = null;
742
+ },
733
743
  });
744
+ _ensureShared = shared;
734
745
  }
735
- return _ensureInFlight;
746
+ return _ensureShared.join(signal);
736
747
  }
737
748
 
738
749
  /**
@@ -779,7 +790,7 @@ export async function stopPgForShutdown() {
779
790
  return;
780
791
  }
781
792
  const snap = _live;
782
- _ensureInFlight = null;
793
+ _ensureShared = null;
783
794
  try {
784
795
  const { stopPg } = await _getPgProc();
785
796
  await stopPg({ runtimeDir: snap.runtimeDir, pgdataDir: snap.pgdata });
@@ -865,10 +876,11 @@ export async function stopOwnedPgForShutdown({ timeoutMs = 6_000 } = {}) {
865
876
  // publishes its cluster only when it settles; stopping before that would
866
877
  // miss the postmaster it is about to own. Bounded: a cold first start can
867
878
  // be a download.
868
- if (_ensureInFlight) {
879
+ const inFlight = _ensureShared?.promise;
880
+ if (inFlight) {
869
881
  let timer = null;
870
882
  await Promise.race([
871
- _ensureInFlight.catch(() => {}),
883
+ inFlight.catch(() => {}),
872
884
  new Promise((resolveWait) => {
873
885
  timer = setTimeout(resolveWait, timeoutMs);
874
886
  timer.unref?.();