mixdog 1.4.2 → 1.4.3

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 (417) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-shipped-history.mjs +15 -5
  3. package/scripts/gate-local.mjs +65 -19
  4. package/scripts/graph-parity.mjs +8 -3
  5. package/scripts/lib/gate-scheduler.mjs +2 -1
  6. package/scripts/lib/run-node-tests.mjs +17 -5
  7. package/scripts/lib/test-failure-summary.mjs +1 -8
  8. package/scripts/lib/test-spawn-ledger.mjs +33 -2
  9. package/scripts/post-release-check.mjs +26 -16
  10. package/scripts/provider-pingpong-live.mjs +1 -1
  11. package/scripts/prune-embedding-runtime.mjs +3 -1
  12. package/scripts/release-paths.mjs +7 -1
  13. package/src/defaults/shipped-history.json +20 -0
  14. package/src/defaults/skills/setup/SKILL.md +2 -2
  15. package/src/defaults/skills/setup/references/actions.md +16 -3
  16. package/src/defaults/skills/setup/references/surfaces.md +3 -0
  17. package/src/defaults/skills/setup-guide/SKILL.md +6 -1
  18. package/src/lib/instruction-text.cjs +36 -0
  19. package/src/lib/rules-builder.cjs +34 -20
  20. package/src/orchestration/balanced.md +23 -0
  21. package/src/orchestration/focused.md +22 -0
  22. package/src/orchestration/none.md +5 -0
  23. package/src/orchestration/swarm.md +24 -0
  24. package/src/rules/agent-mode/AGENT_MODE.md +3 -1
  25. package/src/rules/agent-mode/MIXY.md +2 -2
  26. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +3 -3
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -2
  29. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +0 -9
  30. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +67 -0
  32. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -0
  33. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -4
  34. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -1
  35. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +199 -46
  36. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +3 -5
  37. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +9 -3
  38. package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -5
  39. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +8 -2
  41. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +14 -0
  43. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-stop.mjs +3 -3
  46. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +4 -6
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-head-parity.json +2784 -2872
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +5 -8
  51. package/src/runtime/agent/orchestrator/providers/openai-responses-payload-parity.fixture.json +288 -300
  52. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +10 -1
  53. package/src/runtime/agent/orchestrator/providers/xai-tool-search-head-parity.json +10 -30
  54. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +30 -0
  55. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +0 -11
  56. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-availability.mjs +8 -0
  57. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +17 -3
  58. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +47 -27
  59. package/src/runtime/agent/orchestrator/runtime-core/goal-deadlines.mjs +141 -10
  60. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +26 -63
  61. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +28 -3
  62. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +36 -11
  63. package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +88 -56
  64. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +4 -1
  65. package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +51 -32
  66. package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +45 -145
  67. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -98
  68. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +32 -99
  69. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +23 -78
  70. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +69 -48
  71. package/src/runtime/agent/orchestrator/runtime-core/todo-list.mjs +367 -0
  72. package/src/runtime/agent/orchestrator/runtime-core/todo-tool-defs.mjs +47 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/tool-availability.mjs +155 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +9 -0
  75. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +119 -35
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +1 -1
  78. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -22
  79. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +30 -0
  80. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +1 -1
  81. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +2 -2
  83. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/session/context-utils.mjs +8 -12
  88. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +7 -3
  92. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +1 -1
  93. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +1 -1
  95. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +1 -1
  96. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +11 -1
  97. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  99. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/todo-check.mjs +196 -0
  100. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +116 -50
  101. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +8 -1
  102. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +1 -1
  104. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -1
  105. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +5 -6
  106. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +6 -1
  107. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  108. package/src/runtime/agent/orchestrator/session/loop/turn-report.mjs +35 -0
  109. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +57 -26
  111. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +9 -7
  112. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +21 -3
  113. package/src/runtime/agent/orchestrator/session/manager/ask-turn-failure.mjs +27 -11
  114. package/src/runtime/agent/orchestrator/session/manager/ask-turn-open.mjs +37 -34
  115. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +15 -0
  116. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +25 -15
  117. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +79 -24
  118. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +3 -0
  119. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +12 -1
  120. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  121. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +6 -1
  122. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +9 -1
  123. package/src/runtime/agent/orchestrator/session/manager/pending-spool-path.mjs +2 -2
  124. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +1 -1
  125. package/src/runtime/agent/orchestrator/session/manager/prompt-settings.mjs +598 -0
  126. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +19 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +5 -0
  129. package/src/runtime/agent/orchestrator/session/manager/session-fallback-title.mjs +1 -1
  130. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +31 -0
  131. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +31 -0
  132. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +42 -6
  133. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +2 -0
  134. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +11 -1
  135. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +8 -9
  136. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +339 -88
  137. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-recovery.mjs +135 -11
  138. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +377 -104
  139. package/src/runtime/agent/orchestrator/session/manager/turn-durability.mjs +144 -0
  140. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +1 -1
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +4 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +6 -0
  143. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +1 -1
  144. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +8 -1
  145. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +5 -1
  146. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +1 -1
  147. package/src/runtime/agent/orchestrator/session/store/liveness-veto.mjs +2 -2
  148. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +23 -2
  149. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +3 -3
  150. package/src/runtime/agent/orchestrator/session/store/transcript-durability.mjs +88 -0
  151. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +5 -0
  152. package/src/runtime/agent/orchestrator/session/store-summary-log.mjs +1 -1
  153. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +14 -5
  154. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +2 -2
  155. package/src/runtime/agent/orchestrator/session/store.mjs +5 -0
  156. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +7 -1
  157. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +9 -1
  158. package/src/runtime/agent/orchestrator/session/transcript-restore/agent-envelope.mjs +1 -1
  159. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +3 -1
  160. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +2 -2
  161. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -5
  162. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -1
  163. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +21 -3
  164. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  165. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/worktree-add-guard.mjs +77 -0
  166. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -1
  167. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +7 -3
  168. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +27 -9
  169. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +2 -2
  170. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +1 -1
  171. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +0 -1
  172. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +0 -10
  173. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +1 -2
  174. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -24
  175. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary/child-kill.mjs +4 -3
  176. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +1 -2
  177. package/src/runtime/agent/orchestrator/tools/code-graph/modes/calls.mjs +1 -1
  178. package/src/runtime/agent/orchestrator/tools/code-graph/modes/symbols.mjs +2 -2
  179. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +4 -5
  180. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +3 -4
  181. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4 -22
  182. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +2 -2
  183. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +1 -1
  184. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +0 -4
  185. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +0 -1
  186. package/src/runtime/attachments/attachment-reference.mjs +1 -1
  187. package/src/runtime/attachments/pdf-extract.mjs +1 -1
  188. package/src/runtime/attachments/store.d.mts +1 -0
  189. package/src/runtime/attachments/store.mjs +2 -2
  190. package/src/runtime/channels/lib/config-reload-watch.mjs +42 -0
  191. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +29 -9
  192. package/src/runtime/channels/lib/scheduler.mjs +33 -0
  193. package/src/runtime/channels/lib/schedules-db.mjs +11 -4
  194. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +4 -1
  195. package/src/runtime/channels/lib/webhooks-db.mjs +9 -3
  196. package/src/runtime/channels/lib/whisper-server.mjs +12 -12
  197. package/src/runtime/channels/lib/worker-main.mjs +9 -31
  198. package/src/runtime/computer-bridge/linux-readiness.mjs +10 -14
  199. package/src/runtime/connectors/connector-accounts-mcp.mjs +169 -33
  200. package/src/runtime/connectors/gmail-app-password.mjs +2 -2
  201. package/src/runtime/connectors/google-mcp.mjs +1 -1
  202. package/src/runtime/effort-judge/judge-client.mjs +2 -2
  203. package/src/runtime/effort-judge/judge-input.mjs +4 -4
  204. package/src/runtime/explore/explorer.mjs +0 -5
  205. package/src/runtime/explore/tool.mjs +3 -3
  206. package/src/runtime/local-provider/hardware.mjs +1 -1
  207. package/src/runtime/media/adapters/codex-image.mjs +3 -3
  208. package/src/runtime/media/adapters/gemini-video.mjs +1 -1
  209. package/src/runtime/media/media-usage.mjs +1 -1
  210. package/src/runtime/media/store.mjs +1 -1
  211. package/src/runtime/memory/index.mjs +11 -1
  212. package/src/runtime/memory/lib/embedding-reindex.mjs +27 -0
  213. package/src/runtime/memory/lib/memory-embed.mjs +83 -14
  214. package/src/runtime/memory/lib/memory-process-lock.mjs +159 -36
  215. package/src/runtime/memory/lib/memory-text-utils.mjs +6 -1
  216. package/src/runtime/memory/lib/memory.mjs +17 -0
  217. package/src/runtime/memory/lib/pg/process.mjs +1 -1
  218. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -96
  219. package/src/runtime/memory/lib/recall-hybrid/hybrid-sql.mjs +12 -3
  220. package/src/runtime/office/authoring/docx-html-runner.mjs +1 -1
  221. package/src/runtime/office/authoring/pptx-author-action.mjs +1 -1
  222. package/src/runtime/office/authoring/xlsx-html-build.mjs +1 -1
  223. package/src/runtime/office/authoring/xlsx-html-runner.mjs +1 -1
  224. package/src/runtime/office/capabilities.mjs +1 -1
  225. package/src/runtime/office/core/office-core.mjs +1 -1
  226. package/src/runtime/office/design/content-model.mjs +3 -3
  227. package/src/runtime/office/design/docx/design-docx/section.mjs +6 -6
  228. package/src/runtime/office/design/xlsx/design-xlsx-components.mjs +1 -1
  229. package/src/runtime/office/design/xlsx/design-xlsx.mjs +1 -1
  230. package/src/runtime/office/pdf/ocr-engine.mjs +1 -1
  231. package/src/runtime/office/pdf/pdf-writer.mjs +4 -4
  232. package/src/runtime/office/portable/portable-docx-operations.mjs +2 -2
  233. package/src/runtime/office/portable/portable-soffice.mjs +8 -6
  234. package/src/runtime/office/portable/xlsx-contract.mjs +1 -1
  235. package/src/runtime/shared/atomic-file.d.mts +8 -0
  236. package/src/runtime/shared/background-tasks.mjs +22 -4
  237. package/src/runtime/shared/background-wait.mjs +66 -0
  238. package/src/runtime/shared/child-guardian.mjs +17 -7
  239. package/src/runtime/shared/config.mjs +313 -16
  240. package/src/runtime/shared/connector-catalog.d.mts +4 -5
  241. package/src/runtime/shared/connector-catalog.mjs +11 -1
  242. package/src/runtime/shared/file-lock.mjs +8 -3
  243. package/src/runtime/shared/git-runner.mjs +7 -2
  244. package/src/runtime/shared/idle-gc.mjs +30 -7
  245. package/src/runtime/shared/llm/http-agent.mjs +8 -5
  246. package/src/runtime/shared/llm/system-proxy.mjs +2 -1
  247. package/src/runtime/shared/llm/usage-ledger-quota.mjs +31 -12
  248. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +84 -8
  249. package/src/runtime/shared/llm/usage-ledger.mjs +3 -0
  250. package/src/runtime/shared/llm/usage-rollup.mjs +48 -0
  251. package/src/runtime/shared/maintenance-schedule.mjs +109 -0
  252. package/src/runtime/shared/process-runner.mjs +8 -4
  253. package/src/runtime/shared/routine-trigger.d.mts +13 -0
  254. package/src/runtime/shared/routine-trigger.mjs +34 -0
  255. package/src/runtime/shared/run-child.d.mts +2 -1
  256. package/src/runtime/shared/run-child.mjs +70 -18
  257. package/src/runtime/shared/session-scratch.mjs +14 -2
  258. package/src/runtime/shared/shipped-definitions.mjs +7 -1
  259. package/src/runtime/shared/tool-card-model.golden.json +3 -12
  260. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  261. package/src/runtime/shared/tool-work-units.mjs +31 -6
  262. package/src/runtime/shared/turn-snapshot.mjs +26 -4
  263. package/src/runtime/shared/turn-worktree-snapshot.mjs +6 -11
  264. package/src/runtime/shared/worker-requests.mjs +17 -3
  265. package/src/runtime/shared/worktrees/branch-management.mjs +1 -5
  266. package/src/runtime/shared/worktrees/contract.mjs +16 -6
  267. package/src/runtime/shared/worktrees/dependencies.mjs +127 -89
  268. package/src/runtime/shared/worktrees/disk.mjs +79 -2
  269. package/src/runtime/shared/worktrees/git.mjs +44 -11
  270. package/src/runtime/shared/worktrees/include.mjs +6 -4
  271. package/src/runtime/shared/worktrees/maintenance.mjs +422 -30
  272. package/src/runtime/shared/worktrees/manager.mjs +772 -75
  273. package/src/runtime/shared/worktrees/settings.mjs +33 -7
  274. package/src/runtime/shared/worktrees/test-support.mjs +5 -1
  275. package/src/runtime/shared/worktrees/worker-snapshots.mjs +82 -0
  276. package/src/runtime/tidy/process.mjs +2 -1
  277. package/src/runtime/web-search/index.mjs +3 -22
  278. package/src/runtime/web-search/lib/web-tools.mjs +2 -2
  279. package/src/runtime/web-search/tool-defs.mjs +17 -26
  280. package/src/session-runtime/agent-profile-changes.mjs +55 -4
  281. package/src/session-runtime/boot/apis.mjs +27 -4
  282. package/src/session-runtime/boot/begin.mjs +2 -0
  283. package/src/session-runtime/boot/config.mjs +10 -1
  284. package/src/session-runtime/boot/facade.mjs +1 -0
  285. package/src/session-runtime/boot/providers.mjs +70 -3
  286. package/src/session-runtime/boot/session.mjs +2 -0
  287. package/src/session-runtime/boot/tools.mjs +69 -25
  288. package/src/session-runtime/channel-config-api.mjs +1 -0
  289. package/src/session-runtime/config-change-effects.mjs +179 -0
  290. package/src/session-runtime/config-lifecycle/config-writers.mjs +1 -0
  291. package/src/session-runtime/config-lifecycle.mjs +160 -24
  292. package/src/session-runtime/conversation-import/discovery.mjs +86 -7
  293. package/src/session-runtime/conversation-import/service.mjs +3 -2
  294. package/src/session-runtime/goal-facade-api.mjs +49 -3
  295. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +3 -0
  296. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +3 -2
  297. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +3 -11
  298. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +18 -4
  299. package/src/session-runtime/internal-tool-executor.mjs +12 -19
  300. package/src/session-runtime/lifecycle/cold-heir.mjs +7 -0
  301. package/src/session-runtime/lifecycle/memory-ingest.mjs +3 -3
  302. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +53 -5
  303. package/src/session-runtime/lifecycle/session-switch.mjs +13 -2
  304. package/src/session-runtime/lifecycle/teardown/session-close.mjs +4 -4
  305. package/src/session-runtime/lifecycle-api.mjs +1 -0
  306. package/src/session-runtime/mcp-glue/server-catalog.mjs +14 -3
  307. package/src/session-runtime/mcp-glue/server-connection.mjs +34 -8
  308. package/src/session-runtime/model-route/set-route.mjs +40 -7
  309. package/src/session-runtime/native-web-search/routes.mjs +3 -3
  310. package/src/session-runtime/provider-auth/refresh.mjs +6 -3
  311. package/src/session-runtime/provider-auth/setup.mjs +1 -1
  312. package/src/session-runtime/provider-readiness.mjs +53 -4
  313. package/src/session-runtime/resource-api.mjs +1 -1
  314. package/src/session-runtime/resource-extension-sync.mjs +4 -8
  315. package/src/session-runtime/resource-mcp-connector-accounts.mjs +5 -1
  316. package/src/session-runtime/resource-mcp-secrets.mjs +3 -3
  317. package/src/session-runtime/resource-plugins-api.mjs +4 -0
  318. package/src/session-runtime/routine-executor.mjs +152 -0
  319. package/src/session-runtime/routine-home-turn.mjs +46 -5
  320. package/src/session-runtime/runtime-facade.mjs +5 -0
  321. package/src/session-runtime/runtime-feature-gates.mjs +2 -11
  322. package/src/session-runtime/schedule-session-run.mjs +1 -0
  323. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +6 -1
  324. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +7 -0
  325. package/src/session-runtime/services/agent-tool/send-flow.mjs +110 -6
  326. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +12 -0
  327. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +5 -0
  328. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +24 -6
  329. package/src/session-runtime/services/agent-tool.mjs +51 -1
  330. package/src/session-runtime/services/channel-admin.mjs +18 -0
  331. package/src/session-runtime/services/hook-bus/command-handler.mjs +7 -11
  332. package/src/session-runtime/services/plugin-admin.mjs +4 -0
  333. package/src/session-runtime/services/seeds.mjs +7 -0
  334. package/src/session-runtime/services/usage-stats-model.mjs +32 -11
  335. package/src/session-runtime/session-create/materialize.mjs +4 -2
  336. package/src/session-runtime/session-create/session-options.mjs +2 -0
  337. package/src/session-runtime/settings-builtin-tools-api.mjs +1 -9
  338. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +1 -1
  339. package/src/session-runtime/setup-tool/executor.mjs +8 -1
  340. package/src/session-runtime/setup-tool/extended-actions.mjs +2 -0
  341. package/src/session-runtime/setup-tool/settings-contract.mjs +28 -0
  342. package/src/session-runtime/setup-tool/tool-defs.mjs +26 -1
  343. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +40 -43
  344. package/src/session-runtime/tool-policy/mode-switch.mjs +1 -1
  345. package/src/session-runtime/tool-policy/policy-stage.mjs +106 -8
  346. package/src/session-runtime/tool-policy/workflow-overlay.mjs +2 -1
  347. package/src/session-runtime/tool-policy-refresh.mjs +182 -5
  348. package/src/session-runtime/tool-surface.mjs +38 -1
  349. package/src/session-runtime/turn/agent-ops.mjs +10 -2
  350. package/src/session-runtime/turn/session-ops.mjs +2 -0
  351. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +5 -0
  352. package/src/session-runtime/turn/turn-run.mjs +1 -1
  353. package/src/session-runtime/webhook-session-run.mjs +2 -0
  354. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +1 -4
  355. package/src/session-runtime/workflow-agents-api/agent-route.mjs +3 -10
  356. package/src/session-runtime/workflow-agents-api/instruction-files.mjs +165 -0
  357. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +5 -4
  358. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +2 -3
  359. package/src/session-runtime/workflow-agents-api.mjs +2 -0
  360. package/src/session-runtime/workspace/project-branches.mjs +1 -1
  361. package/src/session-runtime/workspace/session-view.mjs +1 -1
  362. package/src/session-runtime/workspace/transitions.mjs +20 -4
  363. package/src/session-runtime/workspace/worktree-maintenance.mjs +126 -6
  364. package/src/session-runtime/workspace/worktree-setup.mjs +13 -1
  365. package/src/session-runtime/workspace/worktree-tool.mjs +16 -0
  366. package/src/standalone/daemon-agent-control.mjs +25 -8
  367. package/src/standalone/daemon-desktop-runtime.mjs +6 -2
  368. package/src/standalone/daemon-lag-profiler.mjs +7 -7
  369. package/src/standalone/daemon.mjs +54 -2
  370. package/src/standalone/local-session-runtime.mjs +16 -0
  371. package/src/standalone/session-client/daemon-unavailable.mjs +1 -1
  372. package/src/standalone/session-protocol.mjs +11 -0
  373. package/src/standalone/session-service/agent-route-follow.mjs +23 -20
  374. package/src/standalone/session-service/agent-tree/agent-turns.mjs +18 -6
  375. package/src/standalone/session-service/quota-resume.mjs +9 -0
  376. package/src/standalone/session-service/session-calls/session-action.mjs +51 -1
  377. package/src/standalone/session-service/session-calls/session-turns.mjs +37 -18
  378. package/src/standalone/session-service/session-calls/turn-gate.mjs +34 -0
  379. package/src/standalone/session-service/session-calls.mjs +2 -1
  380. package/src/tui/App.jsx +8 -0
  381. package/src/tui/app/clipboard.mjs +1 -1
  382. package/src/tui/app/create-app-pickers.mjs +2 -0
  383. package/src/tui/app/doctor.mjs +2 -2
  384. package/src/tui/app/panel-signature.mjs +1 -1
  385. package/src/tui/app/settings-picker.mjs +53 -8
  386. package/src/tui/app/shell-layout/row-budgets.mjs +2 -2
  387. package/src/tui/app/slash-commands.mjs +1 -1
  388. package/src/tui/app/slash-dispatch/route-commands.mjs +1 -1
  389. package/src/tui/app/slash-dispatch/session-commands.mjs +1 -1
  390. package/src/tui/app/use-prompt-queue-history.mjs +2 -2
  391. package/src/tui/app/use-transcript-activity.mjs +1 -1
  392. package/src/tui/app/use-transcript-window/geometry.mjs +1 -1
  393. package/src/tui/components/prompt-input/escape-policy.d.mts +0 -1
  394. package/src/tui/components/prompt-input/escape-policy.mjs +1 -1
  395. package/src/tui/components/question-panel-model.mjs +4 -4
  396. package/src/tui/components/tool-execution/standard-card.jsx +3 -4
  397. package/src/tui/components/user-message-skills.mjs +1 -1
  398. package/src/tui/dist/index.mjs +309 -30
  399. package/src/tui/markdown/render-ansi.mjs +1 -1
  400. package/src/tui/session/agent-job-feed/notification-router.mjs +10 -0
  401. package/src/tui/session/agent-job-feed.mjs +2 -0
  402. package/src/tui/session/context-state.mjs +3 -0
  403. package/src/tui/session/quota-resume.mjs +1 -1
  404. package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
  405. package/src/tui/session/session-api/lifecycle/session-reset.mjs +7 -0
  406. package/src/tui/session/session-api/routes-agent-definitions.mjs +5 -0
  407. package/src/tui/session/session-api/worktree.mjs +13 -1
  408. package/src/tui/session/session-flow/drain.mjs +11 -14
  409. package/src/tui/session/tool-card-results/grouped-result.mjs +2 -2
  410. package/src/tui/session/turn-quiet.mjs +1 -1
  411. package/src/tui/session/turn.mjs +3 -0
  412. package/src/tui/session/worktree-setup-notice.mjs +1 -1
  413. package/src/tui/spinner-meta.mjs +3 -3
  414. package/src/tui/spinner-verbs.mjs +2 -2
  415. package/src/defaults/skills/goal-management/SKILL.md +0 -169
  416. package/src/runtime/agent/orchestrator/runtime-core/goal-tasks.mjs +0 -156
  417. package/src/session-runtime/goal-tasks.mjs +0 -1
@@ -5,6 +5,7 @@
5
5
  import { readFileSync, readdirSync, statSync, mkdirSync, existsSync, watch as fsWatch } from 'node:fs';
6
6
  import { basename, join, dirname } from 'node:path';
7
7
  import { createRequire } from 'node:module';
8
+ import { randomUUID } from 'node:crypto';
8
9
  import { resolvePluginData } from './plugin-paths.mjs';
9
10
  import { hasOwn, isPlainObject } from './object.mjs';
10
11
  import {
@@ -13,6 +14,8 @@ import {
13
14
  renameWithRetrySync,
14
15
  writeJsonAtomicSync,
15
16
  writeJsonAtomicAsync,
17
+ updateJsonAtomicSync,
18
+ updateJsonAtomic,
16
19
  withFileLockSync,
17
20
  withFileLock,
18
21
  } from './atomic-file.mjs';
@@ -30,6 +33,7 @@ import { AGENT_PROVIDER_ENV, AGENT_PROVIDER_ENV_ALIASES, getAgentApiKey } from '
30
33
  export { AGENT_PROVIDER_ENV, AGENT_PROVIDER_ENV_ALIASES, getAgentApiKey };
31
34
 
32
35
  const _require = createRequire(import.meta.url);
36
+ const _keychain = _require('../../lib/keychain-cjs.cjs');
33
37
  const {
34
38
  getSecret: _getSecret,
35
39
  setSecret: _setSecret,
@@ -42,7 +46,8 @@ const {
42
46
  retryKeychain: _retryKeychain,
43
47
  refreshKeychainStatus: _refreshKeychainStatus,
44
48
  onKeychainRecovered: _onKeychainRecovered,
45
- } = _require('../../lib/keychain-cjs.cjs');
49
+ prewarmSecrets: _prewarmSecrets,
50
+ } = _keychain;
46
51
  const _setSecretAsync = _setSecretAsyncImpl ?? (async (account, value) => _setSecret(account, value));
47
52
  const _deleteSecretAsync = _deleteSecretAsyncImpl ?? (async (account) => _deleteSecret(account));
48
53
 
@@ -80,7 +85,9 @@ function withFreshDefaultsVersion(fresh, root) {
80
85
  // isolated, freely-mutable object (no shared-reference poisoning). Only the
81
86
  // non-RMW readAll() consults this; the RMW path (readAllForRmW) always reads
82
87
  // disk under the lock. Read errors and malformed JSON are never cached.
83
- let _configReadCache = null; // { raw, mtimeMs, size, atMs } | null
88
+ let _configReadCache = null; // { raw, path, stat, atMs } | null
89
+ // While a change notification runs its listeners (runConfigChangeListeners).
90
+ let _inNotificationPass = false;
84
91
  // Optional coarse fast-path window (ms). Default 0 => the cached raw string is
85
92
  // validated against the file's mtime+size on EVERY read via a cheap statSync
86
93
  // (no content read, no whole-file JSON.parse), so a cross-process edit is
@@ -97,6 +104,12 @@ export function invalidateConfigReadCache() {
97
104
  _configReadCache = null;
98
105
  }
99
106
 
107
+ // A replacement of the same size within the mtime's resolution still moves the
108
+ // inode (atomic rename) or the change time.
109
+ function statKey(st) {
110
+ return `${st.mtimeMs}:${st.ctimeMs}:${st.size}:${st.ino}`;
111
+ }
112
+
100
113
  function readConfigRawCached() {
101
114
  const now = Date.now();
102
115
  if (_configReadCache && _configReadCache.path !== configPath()) _configReadCache = null;
@@ -106,12 +119,12 @@ function readConfigRawCached() {
106
119
  if (CONFIG_READ_TTL_MS > 0 && now - _configReadCache.atMs < CONFIG_READ_TTL_MS) {
107
120
  return _configReadCache.raw;
108
121
  }
109
- // Freshness gate: metadata-only stat. Unchanged mtime+size => the cached
122
+ // Freshness gate: metadata-only stat. Unchanged mtime+ctime+size+inode => the cached
110
123
  // raw is still current, cross-process writes included — atomic writers land
111
124
  // a new inode with a fresh mtime, so this reliably detects them.
112
125
  try {
113
126
  const st = statSync(configPath());
114
- if (st.mtimeMs === _configReadCache.mtimeMs && st.size === _configReadCache.size) {
127
+ if (statKey(st) === _configReadCache.stat) {
115
128
  _configReadCache.atMs = now;
116
129
  return _configReadCache.raw;
117
130
  }
@@ -146,7 +159,7 @@ function readConfigRawCached() {
146
159
  process.stderr.write(`[config] readJsonFile: unexpected read error for ${configPath()}: ${err.message}\n`);
147
160
  throw err;
148
161
  }
149
- _configReadCache = { raw, path: configPath(), mtimeMs: st.mtimeMs, size: st.size, atMs: now };
162
+ _configReadCache = { raw, path: configPath(), stat: statKey(st), atMs: now };
150
163
  return raw;
151
164
  }
152
165
 
@@ -225,6 +238,7 @@ function readConfigFile() {
225
238
  // parse must not be re-served; drop it so the post-quarantine/restore read
226
239
  // hits disk fresh (malformed = never cached).
227
240
  invalidateConfigReadCache();
241
+ if (_inNotificationPass) throw configMidWriteError(err);
228
242
  quarantineMalformedConfig(err);
229
243
  return null;
230
244
  }
@@ -422,19 +436,164 @@ function readAllForRmW() {
422
436
  if (!isPlainObject(parsed)) throw new SyntaxError('config root must be a JSON object');
423
437
  return parsed;
424
438
  } catch (parseErr) {
439
+ if (_inNotificationPass) throw configMidWriteError(parseErr);
425
440
  return restoreAllForRmWOrThrow(parseErr.message, quarantineMalformedConfig(parseErr));
426
441
  }
427
442
  }
428
443
 
444
+ // Secret change marker. Keychain writes never touch mixdog-config.json, so they
445
+ // publish through this file beside it (account -> token of its last change):
446
+ // it rides the same directory watch and notification as a config write. Each
447
+ // process remembers the tokens it has applied and drops exactly the changed
448
+ // accounts from its keychain read cache before listeners reload.
449
+ // A deletion removes the account's entry (the marker holds live accounts only)
450
+ // and moves this token: a process that never saw the account's entry (a secret
451
+ // stored before the marker existed, or set and deleted between two of its
452
+ // syncs) may still cache it, so it drops its whole keychain read cache.
453
+ const SECRETS_MARKER_FILE = 'mixdog-secrets-changed.json';
454
+ const SECRET_DELETED_KEY = '#deleted';
455
+ let _secretsSeen = null;
456
+ let _secretsSeenPath = '';
457
+ let _secretsMarkerStat = '';
458
+
459
+ function secretsMarkerPath() {
460
+ return join(dirname(configPath()), SECRETS_MARKER_FILE);
461
+ }
462
+
463
+ function secretsMarkerStat(path) {
464
+ try {
465
+ const stat = statSync(path);
466
+ return `${stat.mtimeMs}:${stat.size}:${stat.ino}`;
467
+ } catch {
468
+ return 'missing';
469
+ }
470
+ }
471
+
472
+ function readSecretsMarker() {
473
+ try {
474
+ const parsed = parseJsonText(readFileSync(secretsMarkerPath(), 'utf8'));
475
+ return isPlainObject(parsed) ? parsed : {};
476
+ } catch {
477
+ return {};
478
+ }
479
+ }
480
+
481
+ /** Apply marker entries this process has not seen; answers the changed accounts. */
482
+ function syncSecretsMarker() {
483
+ const path = secretsMarkerPath();
484
+ if (_secretsSeenPath !== path) {
485
+ _secretsSeen = null;
486
+ _secretsSeenPath = path;
487
+ }
488
+ _secretsMarkerStat = secretsMarkerStat(path);
489
+ const marker = readSecretsMarker();
490
+ if (_secretsSeen === null) {
491
+ _secretsSeen = marker;
492
+ return [];
493
+ }
494
+ const seen = _secretsSeen;
495
+ const changed = [...new Set([...Object.keys(seen), ...Object.keys(marker)])].filter(
496
+ (account) => seen[account] !== marker[account]
497
+ );
498
+ _secretsSeen = marker;
499
+ if (changed.includes(SECRET_DELETED_KEY)) _keychain.invalidateSecretCache?.();
500
+ else for (const account of changed) if (!account.startsWith('#')) _keychain.invalidateSecretCache?.(account);
501
+ return changed;
502
+ }
503
+
504
+ // The read path of a process that may have no config-change subscriber (an
505
+ // injected config, a helper process): one stat, and a re-read only when the
506
+ // marker moved, so a secret changed or forgotten elsewhere is never served
507
+ // from this process's cache.
508
+ function syncSecretsMarkerIfMoved() {
509
+ if (_secretsSeen !== null && _secretsSeenPath === secretsMarkerPath()) {
510
+ if (secretsMarkerStat(_secretsSeenPath) === _secretsMarkerStat) return;
511
+ }
512
+ syncSecretsMarker();
513
+ }
514
+
515
+ /** account -> token of the last change of that secret this process has
516
+ * applied (its own writes and other processes'). Values are never included. */
517
+ export function secretChangeTokens() {
518
+ if (_secretsSeen === null) syncSecretsMarker();
519
+ return { ..._secretsSeen };
520
+ }
521
+
522
+ const secretChangeToken = () => `${process.pid}:${Date.now()}:${randomUUID()}`;
523
+ function withSecretChange(marker, account, token, deleted) {
524
+ const next = { ...(isPlainObject(marker) ? marker : {}) };
525
+ if (deleted) {
526
+ delete next[account];
527
+ next[SECRET_DELETED_KEY] = token;
528
+ } else {
529
+ next[account] = token;
530
+ }
531
+ return next;
532
+ }
533
+
534
+ function beginSecretChange() {
535
+ // Other processes' changes are applied first, so recording our own token
536
+ // never absorbs theirs unapplied.
537
+ syncSecretsMarker();
538
+ return secretChangeToken();
539
+ }
540
+
541
+ function finishSecretChange(account, token, deleted) {
542
+ // This process already holds the new value (the keychain module updated its
543
+ // cache), so its own change is recorded as applied.
544
+ _secretsSeen = withSecretChange(_secretsSeen, account, token, deleted);
545
+ _secretsMarkerStat = secretsMarkerStat(secretsMarkerPath());
546
+ notifyConfigChanged();
547
+ }
548
+
549
+ const secretMarkerFailure = (err) =>
550
+ process.stderr.write(`[config] secret change marker write failed: ${err?.message || err}\n`);
551
+
552
+ // Never throws: the secret itself is already stored; a marker that could not
553
+ // be written is reported and only delays what other processes see.
554
+ function publishSecretChange(account, deleted = false) {
555
+ const token = beginSecretChange();
556
+ try {
557
+ mkdirSync(dirname(secretsMarkerPath()), { recursive: true, mode: 0o700 });
558
+ updateJsonAtomicSync(secretsMarkerPath(), (marker) => withSecretChange(marker, account, token, deleted), {
559
+ mode: 0o600,
560
+ });
561
+ } catch (err) {
562
+ secretMarkerFailure(err);
563
+ }
564
+ finishSecretChange(account, token, deleted);
565
+ }
566
+
567
+ async function publishSecretChangeAsync(account, deleted = false) {
568
+ const token = beginSecretChange();
569
+ try {
570
+ mkdirSync(dirname(secretsMarkerPath()), { recursive: true, mode: 0o700 });
571
+ await updateJsonAtomic(secretsMarkerPath(), (marker) => withSecretChange(marker, account, token, deleted), {
572
+ mode: 0o600,
573
+ });
574
+ } catch (err) {
575
+ secretMarkerFailure(err);
576
+ }
577
+ finishSecretChange(account, token, deleted);
578
+ }
579
+
429
580
  // Config-change notification. In-process writes notify directly after the file
430
581
  // is persisted; writes from other processes arrive through one lazily started
431
582
  // directory watch (the atomic rename replaces the file, which would orphan a
432
583
  // file-level watch on POSIX). Bursts coalesce into one listener pass.
433
584
  const CONFIG_CHANGE_DEBOUNCE_MS = 100;
585
+ // A watch that failed (or could not start) is re-established on this delay
586
+ // while anyone listens; changes missed meanwhile are announced once it is back.
587
+ const CONFIG_WATCH_RETRY_MS = 1000;
588
+ const WATCHED_FILES = new Set(['mixdog-config.json', SECRETS_MARKER_FILE]);
434
589
  const _configChangeListeners = new Set();
435
590
  let _configChangeTimer = null;
436
591
  let _configWatcher = null;
437
592
  let _configWatchDir = '';
593
+ let _configWatchRetryTimer = null;
594
+
595
+ /** The directory watch implementation; tests substitute a failing watcher. */
596
+ export const configWatchDeps = { watch: fsWatch };
438
597
 
439
598
  function stopConfigWatcher() {
440
599
  try {
@@ -444,46 +603,142 @@ function stopConfigWatcher() {
444
603
  _configWatchDir = '';
445
604
  }
446
605
 
447
- function notifyConfigChanged() {
448
- if (!_configChangeListeners.size) return;
449
- if (_configChangeTimer) clearTimeout(_configChangeTimer);
450
- _configChangeTimer = setTimeout(() => {
451
- _configChangeTimer = null;
452
- invalidateConfigReadCache();
606
+ // The watched files' bytes at the last listener pass (or the first subscribe).
607
+ // A pass caused only by the watch finds nothing to do when they are unchanged,
608
+ // e.g. another process rewriting a marker with identical content.
609
+ let _configChangeForced = false;
610
+ let _watchedContent = null;
611
+
612
+ function watchedContent() {
613
+ const dir = dirname(configPath());
614
+ return [...WATCHED_FILES].map((file) => {
615
+ try {
616
+ return readFileSync(join(dir, file), 'latin1');
617
+ } catch {
618
+ return null;
619
+ }
620
+ });
621
+ }
622
+
623
+ // A config that does not parse while a notification pass runs its listeners
624
+ // is usually a write in progress (a hand edit saved half-typed or torn, maybe
625
+ // landing after the pass began): every read of the pass (readConfigFile,
626
+ // readAllForRmW) then throws instead of quarantining it, so the listeners keep
627
+ // their current config, and the pass runs again once shortly. Only reads
628
+ // outside a pass (boot, explicit reads, writes) quarantine it.
629
+ const CONFIG_SETTLE_RETRY_MS = 300;
630
+ let _configSettleTimer = null;
631
+ let _configSettleRetried = false;
632
+
633
+ function configMidWriteError(parseErr) {
634
+ // The retry runs the listeners even when the bytes then equal the ones this
635
+ // pass saw (the save completed back to them).
636
+ _configChangeForced = true;
637
+ if (!_configSettleRetried && !_configSettleTimer && _configChangeListeners.size) {
638
+ _configSettleTimer = setTimeout(() => {
639
+ _configSettleTimer = null;
640
+ _configSettleRetried = true;
641
+ void runConfigChangeListeners();
642
+ }, CONFIG_SETTLE_RETRY_MS);
643
+ _configSettleTimer.unref?.();
644
+ }
645
+ const error = new Error(
646
+ `[config] mixdog-config.json does not parse yet (${parseErr.message}); keeping the current config`
647
+ );
648
+ error.code = 'ECONFIG_MID_WRITE';
649
+ return error;
650
+ }
651
+
652
+ async function runConfigChangeListeners() {
653
+ const forced = _configChangeForced;
654
+ _configChangeForced = false;
655
+ const content = watchedContent();
656
+ const last = _watchedContent;
657
+ if (!forced && Array.isArray(last) && last.every((value, index) => value === content[index])) return;
658
+ _watchedContent = content;
659
+ invalidateConfigReadCache();
660
+ // A Linux keyring is read only through a snapshot, which the invalidation
661
+ // dropped: re-take it so listeners reload the changed keys.
662
+ if (syncSecretsMarker().length && process.platform === 'linux') {
663
+ try {
664
+ await _prewarmSecrets?.();
665
+ } catch {}
666
+ }
667
+ _inNotificationPass = true;
668
+ try {
453
669
  for (const listener of [..._configChangeListeners]) {
454
670
  try {
455
671
  listener();
456
672
  } catch {}
457
673
  }
674
+ } finally {
675
+ _inNotificationPass = false;
676
+ }
677
+ }
678
+
679
+ // In-process writes and a re-established watch always run the listeners (a
680
+ // secret stored while its marker write failed changes no watched byte).
681
+ function notifyConfigChanged({ fromWatch = false } = {}) {
682
+ if (!_configChangeListeners.size) return;
683
+ if (!fromWatch) _configChangeForced = true;
684
+ _configSettleRetried = false;
685
+ if (_configChangeTimer) clearTimeout(_configChangeTimer);
686
+ _configChangeTimer = setTimeout(() => {
687
+ _configChangeTimer = null;
688
+ void runConfigChangeListeners();
458
689
  }, CONFIG_CHANGE_DEBOUNCE_MS);
459
690
  _configChangeTimer.unref?.();
460
691
  }
461
692
 
693
+ function scheduleConfigWatchRetry() {
694
+ if (_configWatchRetryTimer || !_configChangeListeners.size) return;
695
+ _configWatchRetryTimer = setTimeout(() => {
696
+ _configWatchRetryTimer = null;
697
+ if (!_configChangeListeners.size) return;
698
+ ensureConfigWatcher();
699
+ if (_configWatcher) notifyConfigChanged();
700
+ }, CONFIG_WATCH_RETRY_MS);
701
+ _configWatchRetryTimer.unref?.();
702
+ }
703
+
462
704
  function ensureConfigWatcher() {
463
705
  const dir = dirname(configPath());
464
706
  if (_configWatcher && _configWatchDir === dir) return;
465
707
  stopConfigWatcher();
466
708
  try {
467
- _configWatcher = fsWatch(dir, (_event, filename) => {
468
- if (!filename || String(filename) === 'mixdog-config.json') notifyConfigChanged();
709
+ const watcher = configWatchDeps.watch(dir, (_event, filename) => {
710
+ if (!filename || WATCHED_FILES.has(String(filename))) notifyConfigChanged({ fromWatch: true });
469
711
  });
470
- _configWatcher.unref?.();
471
- _configWatcher.on?.('error', stopConfigWatcher);
712
+ watcher.unref?.();
713
+ watcher.on?.('error', () => {
714
+ if (_configWatcher !== watcher) return;
715
+ stopConfigWatcher();
716
+ scheduleConfigWatchRetry();
717
+ });
718
+ _configWatcher = watcher;
472
719
  _configWatchDir = dir;
473
720
  } catch {
474
721
  _configWatcher = null;
722
+ scheduleConfigWatchRetry();
475
723
  }
476
724
  }
477
725
 
478
- /** Subscribe to persisted config changes (this process or another). Returns unsubscribe. */
726
+ /** Subscribe to persisted config and secret changes (this process or another). Returns unsubscribe. */
479
727
  export function subscribeConfigChange(listener) {
728
+ if (!_configChangeListeners.size) _watchedContent = watchedContent();
480
729
  _configChangeListeners.add(listener);
730
+ // The first subscriber fixes the secret baseline this process applies from.
731
+ if (_secretsSeen === null) syncSecretsMarker();
481
732
  ensureConfigWatcher();
482
733
  return () => {
483
734
  _configChangeListeners.delete(listener);
484
735
  if (!_configChangeListeners.size) {
485
736
  if (_configChangeTimer) clearTimeout(_configChangeTimer);
486
737
  _configChangeTimer = null;
738
+ clearTimeout(_configSettleTimer);
739
+ _configSettleTimer = null;
740
+ if (_configWatchRetryTimer) clearTimeout(_configWatchRetryTimer);
741
+ _configWatchRetryTimer = null;
487
742
  stopConfigWatcher();
488
743
  }
489
744
  };
@@ -664,6 +919,7 @@ function _readSecret(account) {
664
919
  const envVal = process.env[_envKey(account)];
665
920
  if (envVal) return envVal;
666
921
  try {
922
+ syncSecretsMarkerIfMoved();
667
923
  return _getSecret(account);
668
924
  } catch {
669
925
  return null;
@@ -692,6 +948,7 @@ export function saveSecret(account, value) {
692
948
  } catch (err) {
693
949
  throw saveSecretError(account, err);
694
950
  }
951
+ publishSecretChange(account);
695
952
  }
696
953
 
697
954
  /** Non-blocking saveSecret: Linux secret-tool runs off the event loop. */
@@ -701,6 +958,7 @@ export async function saveSecretAsync(account, value) {
701
958
  } catch (err) {
702
959
  throw saveSecretError(account, err);
703
960
  }
961
+ await publishSecretChangeAsync(account);
704
962
  }
705
963
 
706
964
  function saveSecretError(account, err) {
@@ -725,11 +983,49 @@ function saveSecretError(account, err) {
725
983
  /** Remove a secret from the OS keychain (if present) and the Linux fallback file. */
726
984
  export function deleteSecret(account) {
727
985
  _deleteSecret(account);
986
+ publishSecretChange(account, true);
728
987
  }
729
988
 
730
989
  /** Non-blocking deleteSecret: Linux secret-tool runs off the event loop. */
731
990
  export async function deleteSecretAsync(account) {
732
991
  await _deleteSecretAsync(account);
992
+ await publishSecretChangeAsync(account, true);
993
+ }
994
+
995
+ /**
996
+ * The keychain as a credential store for modules that keep their own secrets
997
+ * in it (MCP OAuth tokens and client secrets, Gmail app passwords). Reads are
998
+ * the keychain's own; every write publishes the secret change marker exactly
999
+ * as saveSecret/deleteSecret do, so other processes learn of it. Write errors
1000
+ * stay the keychain's: the provider-key environment hint of saveSecret does
1001
+ * not apply to these accounts. Calls reach the keychain module at call time.
1002
+ * `{ announce: false }` writes without the marker, for a change other
1003
+ * processes observe by their own means (an MCP OAuth token refresh: its
1004
+ * revision marker), so it does not reach every runtime as a settings change.
1005
+ * Reads first apply the marker, so a process without a config-change
1006
+ * subscriber never serves a secret changed or forgotten elsewhere.
1007
+ */
1008
+ export const keychainSecretStore = Object.freeze({
1009
+ getSecretAsync(account) {
1010
+ syncSecretsMarkerIfMoved();
1011
+ return _keychain.getSecretAsync(account);
1012
+ },
1013
+ invalidateSecretCache: (account) => _keychain.invalidateSecretCache?.(account),
1014
+ async setSecretAsync(account, value, { announce = true } = {}) {
1015
+ await _keychain.setSecretAsync(account, value);
1016
+ if (announce) await publishSecretChangeAsync(account);
1017
+ },
1018
+ async deleteSecretAsync(account, { announce = true } = {}) {
1019
+ await _keychain.deleteSecretAsync(account);
1020
+ if (announce) await publishSecretChangeAsync(account, true);
1021
+ },
1022
+ });
1023
+
1024
+ /** Announce a change of state kept outside mixdog-config.json that settings
1025
+ * depend on (`plugins`: the plugin registry), under a reserved `#` key of the
1026
+ * secret change marker, so it reaches other processes on the same path. */
1027
+ export function announceStoredStateChange(key) {
1028
+ publishSecretChange(`#${key}`);
733
1029
  }
734
1030
 
735
1031
  /** Retry a failed Linux keyring read; true when it just recovered. */
@@ -763,6 +1059,7 @@ export function getKeychainStatus() {
763
1059
  */
764
1060
  export function hasStoredSecret(account) {
765
1061
  try {
1062
+ syncSecretsMarkerIfMoved();
766
1063
  if (typeof _hasSecret === 'function') return !!_hasSecret(account);
767
1064
  return !!_getSecret(account);
768
1065
  } catch {
@@ -37,11 +37,7 @@ export const CONNECTOR_IDS: readonly string[];
37
37
  export function connectorById(id: unknown): ConnectorCatalogEntry | null;
38
38
  export function connectorOfServer(cfg: unknown): string;
39
39
  export function connectorToolDefault(connectorId: string, tool: string): 'auto' | 'needs_approval';
40
- export function connectorServerInput(
41
- entry: ConnectorCatalogEntry,
42
- name: string,
43
- key?: string
44
- ): Record<string, unknown>;
40
+ export function connectorServerInput(entry: ConnectorCatalogEntry, name: string, key?: string): Record<string, unknown>;
45
41
  export function connectorAppPasswordOnly(entry: ConnectorCatalogEntry | null | undefined): boolean;
46
42
  export function connectorUsesGoogleClient(entry: ConnectorCatalogEntry | null | undefined): boolean;
47
43
  export function connectorMultiAccount(entry: ConnectorCatalogEntry | null | undefined): boolean;
@@ -54,6 +50,8 @@ export function gmailAppPasswordServerInput(name: string, email: string, passwor
54
50
  /** One stored account of a connector server. */
55
51
  export interface ConnectorAccountEntry {
56
52
  name: string;
53
+ /** Its identity, given when it was added (connectorAccountId). */
54
+ id?: string;
57
55
  /** Absent: http. */
58
56
  type?: string;
59
57
  url?: string;
@@ -73,6 +71,7 @@ export function connectorAccountServer(
73
71
  cfg: Record<string, unknown>,
74
72
  account: ConnectorAccountEntry
75
73
  ): Record<string, unknown>;
74
+ export function connectorAccountId(cfg: Record<string, unknown>, account: ConnectorAccountEntry): string;
76
75
  export function connectorAccountServers(cfg: unknown): Record<string, unknown>[];
77
76
  export function connectorAccountEnabled(account: ConnectorAccountEntry): boolean;
78
77
  export function connectorLiveAccountServers(cfg: unknown): Record<string, unknown>[];
@@ -193,8 +193,9 @@ export function connectorMultiAccount(entry) {
193
193
  // A catalog connector is ONE MCP server (`gmail`, `notion`, `postgres`…)
194
194
  // holding its accounts:
195
195
  // { type: 'http', url: connectorAccountsUrl(id), connector: id,
196
- // accounts: [{ name, type?, url?, headers?, oauthClientId?, oauthKey?,
196
+ // accounts: [{ name, id?, type?, url?, headers?, oauthClientId?, oauthKey?,
197
197
  // command?, args?, env? }, …] }
198
+ // (`id`: the account's random identity; connectorAccountId)
198
199
  // Each account keeps the transport it had as a server of its own (its remote
199
200
  // URL and sign-in, its key header, its app-password address, or its local
200
201
  // command and environment), so its keychain entries are untouched. A remote
@@ -241,6 +242,15 @@ export function connectorServerSettings(cfg) {
241
242
  return Object.fromEntries(Object.entries(cfg).filter(([key]) => key !== 'accounts' && !ACCOUNT_FIELDS.includes(key)));
242
243
  }
243
244
 
245
+ /** The identity of stored account `account`, which a chosen account is
246
+ * remembered by: the random `id` stored with it (given when it is added, or
247
+ * on load to one stored without: connector-accounts-mcp.mjs
248
+ * ensureConnectorAccountIds). A new account given a removed one's name gets
249
+ * a new id; an edit keeps it. '' while it has none (nothing is bound to it). */
250
+ export function connectorAccountId(_cfg, account) {
251
+ return typeof account?.id === 'string' ? account.id : '';
252
+ }
253
+
244
254
  /** Every account of `cfg` as its one-account server config. */
245
255
  export function connectorAccountServers(cfg) {
246
256
  return connectorAccountsOf(cfg).map((account) => connectorAccountServer(cfg, account));
@@ -114,9 +114,14 @@ async function runAsync(steps) {
114
114
 
115
115
  function* readLockOwner(lockPath) {
116
116
  try {
117
- const parts = String(yield ['read', lockPath])
118
- .trim()
119
- .split(/\s+/);
117
+ const text = String(yield ['read', lockPath]).trim();
118
+ // v1.4.2 pg-spawn.lock wrote `{"pid":N,"startedAt":T}`; an older process
119
+ // may still hold one, so its pid must count as an owner.
120
+ if (text.startsWith('{')) {
121
+ const legacyPid = Number(JSON.parse(text)?.pid);
122
+ return { pid: Number.isInteger(legacyPid) && legacyPid > 0 ? legacyPid : null, token: null };
123
+ }
124
+ const parts = text.split(/\s+/);
120
125
  const pid = Number.parseInt(parts[0], 10);
121
126
  return {
122
127
  pid: Number.isFinite(pid) && pid > 0 ? pid : null,
@@ -15,7 +15,12 @@ const requestGit = createWorkerRequestClient(new URL('./git-runner-worker.mjs',
15
15
  * timeoutMs (the child is killed), EMAXBUFFER once stdout+stderr exceed
16
16
  * maxBytes, or the spawn error itself. `stdoutEncoding: 'base64'` returns the
17
17
  * exact stdout bytes base64-encoded instead of decoded as UTF-8.
18
+ * `waitForExitMs`: a timeout rejects only once the killed child exited (or
19
+ * after that bound), with `childExited` and `pid` on the error (run-child.mjs).
18
20
  */
19
- export function runGitOffThread(args, { cwd, input = '', env = null, timeoutMs, maxBytes, stdoutEncoding = 'utf8' }) {
20
- return requestGit({ args, cwd, input, env, timeoutMs, maxBytes, stdoutEncoding });
21
+ export function runGitOffThread(
22
+ args,
23
+ { cwd, input = '', env = null, timeoutMs, maxBytes, stdoutEncoding = 'utf8', waitForExitMs = 0 }
24
+ ) {
25
+ return requestGit({ args, cwd, input, env, timeoutMs, maxBytes, stdoutEncoding, waitForExitMs });
21
26
  }
@@ -73,7 +73,12 @@ export async function collectGarbageNow() {
73
73
  * @param {(message: string) => void} [options.log]
74
74
  * @param {string} [options.label] Log prefix.
75
75
  */
76
- export function createIdleGc({ isBusy, log = () => {}, label = 'idle gc' } = {}) {
76
+ export function createIdleGc({
77
+ isBusy,
78
+ log = () => {},
79
+ label = 'idle gc',
80
+ memoryUsage = () => process.memoryUsage(),
81
+ } = {}) {
77
82
  const enabled = !DISABLED.test(String(process.env.MIXDOG_IDLE_GC || ''));
78
83
  // Long enough that the pause between two turns of one conversation never
79
84
  // triggers a sweep, short enough that a desktop left alone reclaims soon.
@@ -90,6 +95,11 @@ export function createIdleGc({ isBusy, log = () => {}, label = 'idle gc' } = {})
90
95
  // between committed and used is itself a reason to sweep — the collection is
91
96
  // what lets V8 release those pages — so either signal alone arms the sweep.
92
97
  const minSlackBytes = envNumber('MIXDOG_IDLE_GC_MIN_SLACK_MB', 64) * MB;
98
+ // Buffers, ArrayBuffers and external strings live outside the JS heap, so a
99
+ // daemon can sit on gigabytes of dead attachment data behind a tiny heap
100
+ // (external 2.8GB beside heapTotal 7MB was measured) and never trip the heap
101
+ // floors above. Only a collection frees them.
102
+ const minExternalBytes = envNumber('MIXDOG_IDLE_GC_MIN_EXTERNAL_MB', 96) * MB;
93
103
  // Re-sweeping a process that has not allocated since the last sweep only
94
104
  // burns CPU; require real growth (or an announced release, see
95
105
  // noteMemoryRelease) before running again.
@@ -98,6 +108,7 @@ export function createIdleGc({ isBusy, log = () => {}, label = 'idle gc' } = {})
98
108
  let timer = null;
99
109
  let lastBusyAt = Date.now();
100
110
  let lastSweptHeap = 0;
111
+ let lastSweptExternal = 0;
101
112
  let sweptReleaseSeq = releaseSeq;
102
113
  let appliedReleaseAt = lastReleaseAt;
103
114
  let sweeping = false;
@@ -123,12 +134,22 @@ export function createIdleGc({ isBusy, log = () => {}, label = 'idle gc' } = {})
123
134
  lastBusyAt = Math.max(lastBusyAt, Math.min(lastReleaseAt, Date.now()));
124
135
  }
125
136
  if (Date.now() - lastBusyAt < idleMs) return 'settling';
126
- const usageBefore = process.memoryUsage();
137
+ const usageBefore = memoryUsage();
127
138
  const before = usageBefore.heapUsed;
139
+ const externalBefore = usageBefore.external || 0;
128
140
  const slackBefore = Math.max(0, usageBefore.heapTotal - before);
129
- if (before < minHeapBytes && slackBefore < minSlackBytes) return 'small';
141
+ if (before < minHeapBytes && slackBefore < minSlackBytes && externalBefore < minExternalBytes) {
142
+ return 'small';
143
+ }
130
144
  const released = releaseSeq !== sweptReleaseSeq;
131
- if (lastSweptHeap && !released && before < lastSweptHeap + growthBytes) return 'unchanged';
145
+ if (
146
+ lastSweptHeap &&
147
+ !released &&
148
+ before < lastSweptHeap + growthBytes &&
149
+ externalBefore < lastSweptExternal + growthBytes
150
+ ) {
151
+ return 'unchanged';
152
+ }
132
153
 
133
154
  sweeping = true;
134
155
  const releaseSeqAtSweep = releaseSeq;
@@ -139,9 +160,10 @@ export function createIdleGc({ isBusy, log = () => {}, label = 'idle gc' } = {})
139
160
  disarm();
140
161
  return 'unavailable';
141
162
  }
142
- const usageAfter = process.memoryUsage();
163
+ const usageAfter = memoryUsage();
143
164
  const after = usageAfter.heapUsed;
144
165
  lastSweptHeap = after;
166
+ lastSweptExternal = usageAfter.external || 0;
145
167
  // Releases announced while this sweep ran still get their own.
146
168
  sweptReleaseSeq = releaseSeqAtSweep;
147
169
  const mb = (bytes) => (bytes / MB).toFixed(1);
@@ -150,7 +172,8 @@ export function createIdleGc({ isBusy, log = () => {}, label = 'idle gc' } = {})
150
172
  log(
151
173
  `${label}: heapUsed ${mb(before)} -> ${mb(after)} MB` +
152
174
  ` (reclaimed ${mb(before - after)} MB) in ${Date.now() - startedAt}ms` +
153
- `, committed ${mb(usageBefore.heapTotal)} -> ${mb(usageAfter.heapTotal)} MB`
175
+ `, committed ${mb(usageBefore.heapTotal)} -> ${mb(usageAfter.heapTotal)} MB` +
176
+ `, external ${mb(externalBefore)} -> ${mb(usageAfter.external || 0)} MB`
154
177
  );
155
178
  return 'swept';
156
179
  } catch (e) {
@@ -185,6 +208,6 @@ export function createIdleGc({ isBusy, log = () => {}, label = 'idle gc' } = {})
185
208
  },
186
209
  /** Drives one cycle synchronously for tests; returns the decision taken. */
187
210
  _tickForTest: tick,
188
- _configForTest: { enabled, idleMs, checkMs, minHeapBytes, minSlackBytes, growthBytes },
211
+ _configForTest: { enabled, idleMs, checkMs, minHeapBytes, minSlackBytes, minExternalBytes, growthBytes },
189
212
  };
190
213
  }