mixdog 0.9.147 → 0.9.149

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 (265) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +5 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
  48. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
  49. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  53. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  57. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  62. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  63. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
  64. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  65. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  66. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  67. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  71. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  75. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  80. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  81. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  82. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  84. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  86. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  87. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  88. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
  89. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  95. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  97. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
  100. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  102. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  103. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  109. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  112. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  113. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  114. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  117. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  118. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  119. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  120. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  124. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  125. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  126. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  128. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  130. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  131. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  132. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  133. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  134. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  136. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  137. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  138. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  139. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  140. package/src/runtime/attachments/store.mjs +11 -3
  141. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  142. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  143. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  144. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  146. package/src/runtime/channels/lib/webhook.mjs +39 -20
  147. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  148. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  149. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  150. package/src/runtime/media/jobs.mjs +13 -0
  151. package/src/runtime/media/renditions.mjs +7 -1
  152. package/src/runtime/media/store.mjs +42 -22
  153. package/src/runtime/memory/index.mjs +4 -9
  154. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  155. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  156. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  157. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  158. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  159. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  160. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  161. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  162. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  164. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  165. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  166. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  167. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  168. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  169. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  170. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  171. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  172. package/src/runtime/memory/lib/memory.mjs +50 -12
  173. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  174. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  175. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  176. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  177. package/src/runtime/shared/background-tasks.mjs +15 -0
  178. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  179. package/src/runtime/shared/child-guardian.mjs +2 -10
  180. package/src/runtime/shared/pid-liveness.mjs +27 -0
  181. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  182. package/src/runtime/shared/runtime-root.mjs +1 -11
  183. package/src/runtime/shared/service-discovery.mjs +1 -17
  184. package/src/runtime/shared/singleton-owner.mjs +1 -11
  185. package/src/runtime/shared/staged-update.mjs +2 -12
  186. package/src/runtime/shared/tool-status.mjs +5 -1
  187. package/src/runtime/shared/tool-surface.mjs +15 -7
  188. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  189. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  190. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  191. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  192. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  193. package/src/runtime/web-search/lib/state.mjs +15 -15
  194. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  195. package/src/session-runtime/context-status.mjs +4 -12
  196. package/src/session-runtime/runtime-core.mjs +20 -1
  197. package/src/session-runtime/session-lifecycle.mjs +9 -0
  198. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  199. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  200. package/src/standalone/agent-tool/notify.mjs +3 -1
  201. package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
  202. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  203. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  204. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  205. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  206. package/src/standalone/agent-tool.mjs +20 -55
  207. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  208. package/src/standalone/channel-client.mjs +47 -15
  209. package/src/standalone/channel-transport.mjs +44 -25
  210. package/src/standalone/daemon.mjs +31 -2
  211. package/src/standalone/hook-bus/config.mjs +25 -9
  212. package/src/standalone/hook-bus/handlers.mjs +16 -52
  213. package/src/standalone/hook-bus.mjs +29 -23
  214. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  215. package/src/standalone/session-client.mjs +86 -23
  216. package/src/standalone/session-protocol.mjs +0 -2
  217. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  218. package/src/standalone/session-runtime-host.mjs +101 -6
  219. package/src/standalone/session-runtime-record.mjs +1 -5
  220. package/src/standalone/session-runtime-worker.mjs +52 -57
  221. package/src/standalone/session-service.mjs +4 -1
  222. package/src/standalone/session-transport.mjs +56 -25
  223. package/src/tui/App.jsx +40 -49
  224. package/src/tui/app/app-view.jsx +14 -21
  225. package/src/tui/app/core-memory-picker.mjs +29 -16
  226. package/src/tui/app/create-app-pickers.mjs +15 -36
  227. package/src/tui/app/extension-pickers.mjs +60 -49
  228. package/src/tui/app/maintenance-pickers.mjs +100 -57
  229. package/src/tui/app/message-selector.mjs +6 -5
  230. package/src/tui/app/model-picker.mjs +30 -13
  231. package/src/tui/app/onboarding-steps.mjs +21 -12
  232. package/src/tui/app/panel-epoch.mjs +56 -0
  233. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  234. package/src/tui/app/panel-surface.mjs +98 -0
  235. package/src/tui/app/project-picker.mjs +30 -29
  236. package/src/tui/app/prompt-submit.mjs +192 -97
  237. package/src/tui/app/prompt-submit.test.mjs +206 -0
  238. package/src/tui/app/provider-setup-picker.mjs +74 -43
  239. package/src/tui/app/resume-picker.mjs +11 -8
  240. package/src/tui/app/route-pickers.mjs +45 -28
  241. package/src/tui/app/settings-picker.mjs +69 -18
  242. package/src/tui/app/shell-layout.mjs +1 -3
  243. package/src/tui/app/slash-commands.mjs +0 -2
  244. package/src/tui/app/slash-dispatch.mjs +4 -2
  245. package/src/tui/app/text-entry-policy.mjs +25 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  247. package/src/tui/app/usage-context-panels.mjs +21 -25
  248. package/src/tui/app/use-global-key-input.mjs +3 -2
  249. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  250. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  251. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  252. package/src/tui/components/Picker.jsx +1 -1
  253. package/src/tui/components/TextEntryPanel.jsx +7 -2
  254. package/src/tui/dist/index.mjs +652 -561
  255. package/src/tui/lib/voice-setup.mjs +1 -2
  256. package/src/tui/session/agent-job-feed.mjs +73 -11
  257. package/src/tui/session/session-api-ext.mjs +5 -6
  258. package/src/tui/session/session-api.mjs +1 -0
  259. package/src/tui/session/tool-approval.mjs +15 -5
  260. package/src/tui/session/tui-steering-persist.mjs +33 -19
  261. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  262. package/src/tui/session-local.mjs +0 -22
  263. package/scripts/.tmp-coverage-report.mjs +0 -96
  264. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  265. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -17,6 +17,7 @@ import { randomUUID } from 'node:crypto';
17
17
  import { readFileSync, rmSync } from 'node:fs';
18
18
  import { basename } from 'node:path';
19
19
  import { writeJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
20
+ import { isPidAlive, parsePid } from '../runtime/shared/pid-liveness.mjs';
20
21
  import { readBody, sendJson, sendError } from '../runtime/memory/lib/http-wire.mjs';
21
22
  import { createFairCallScheduler } from './fair-call-scheduler.mjs';
22
23
 
@@ -26,17 +27,6 @@ function readChannelBody(req) {
26
27
  return readBody(req, { maxBytes: CHANNEL_HTTP_BODY_MAX_BYTES });
27
28
  }
28
29
 
29
- function parsePid(value) {
30
- const n = Number(value);
31
- return Number.isInteger(n) && n > 0 ? n : null;
32
- }
33
-
34
- function isPidAlive(pid) {
35
- const n = parsePid(pid);
36
- if (!n) return false;
37
- try { process.kill(n, 0); return true; }
38
- catch (error) { return error?.code === 'EPERM'; }
39
- }
40
30
 
41
31
  const ACTIVATE_TOOL = 'activate_channel_bridge';
42
32
  const REBIND_TOOL = 'rebind_current_transcript';
@@ -103,6 +93,16 @@ export function createChannelTransport({
103
93
  queueMax: Math.max(16, Math.min(64, CALL_QUEUE_MAX)),
104
94
  minOwnerQueue: 4,
105
95
  });
96
+ // Binding calls (manual ON/OFF, rebind) mutate GLOBAL pointer/pin state after
97
+ // reading it. The control lane is unbounded, so two sessions toggling at once
98
+ // would interleave read and write and clobber each other's binding. Every
99
+ // binding call therefore runs alone on this chain.
100
+ let bindingChain = Promise.resolve();
101
+ function runExclusiveBinding(run) {
102
+ const result = bindingChain.then(run, run);
103
+ bindingChain = result.then(() => {}, () => {});
104
+ return result;
105
+ }
106
106
  // Reconnect register replay: a server may commit replacement just before its
107
107
  // HTTP response is lost. The retry supplies this stable id and receives the
108
108
  // already-created fresh token instead of creating an orphan replacement.
@@ -813,6 +813,17 @@ export function createChannelTransport({
813
813
  const previousRemoteSessionId = c.remoteSessionId || null;
814
814
  const previousRemoteAcquired = remoteAcquired;
815
815
  const previousStickyRemoteFrame = stickyRemoteFrame;
816
+ // A failed manual ON must leave ownership exactly as it was.
817
+ const rollbackActivation = () => {
818
+ if (!activating || pointerToken !== clientToken
819
+ || c.remoteSessionId !== bindingSessionId) return;
820
+ pointerToken = previousPointerToken && clients.has(previousPointerToken)
821
+ ? previousPointerToken
822
+ : null;
823
+ c.remoteSessionId = previousRemoteSessionId;
824
+ remoteAcquired = previousRemoteAcquired;
825
+ stickyRemoteFrame = previousStickyRemoteFrame;
826
+ };
816
827
  if (activating) {
817
828
  c.remoteSessionId = bindingSessionId;
818
829
  movePointer(clientToken, 'manual remote ON', { notifyDisplaced: false });
@@ -823,6 +834,16 @@ export function createChannelTransport({
823
834
  leadPid: c?.leadPid ?? null,
824
835
  cwd: c?.cwd ?? null,
825
836
  });
837
+ // A soft `{isError:true}` envelope is a FAILED call, not a
838
+ // result: taking the success path here moved control ownership
839
+ // on a failed ON and cleared the durable pin on a failed OFF
840
+ // while the runtime was still bound.
841
+ if (result?.isError === true) {
842
+ rollbackActivation();
843
+ log(`${name} failed (soft error); binding state unchanged for session=${bindingSessionId || '?'}`);
844
+ publishRemoteState();
845
+ return result;
846
+ }
826
847
  if (activating && previousPointerToken && previousPointerToken !== clientToken) {
827
848
  const displaced = clients.get(previousPointerToken);
828
849
  if (displaced && isPidAlive(displaced.leadPid)
@@ -837,7 +858,7 @@ export function createChannelTransport({
837
858
  remoteAcquired = false;
838
859
  stickyRemoteFrame = null;
839
860
  }
840
- if (activating && result?.isError !== true
861
+ if (activating
841
862
  && pointerToken === clientToken
842
863
  && c.remoteSessionId === bindingSessionId) {
843
864
  remoteAcquired = true;
@@ -846,21 +867,14 @@ export function createChannelTransport({
846
867
  publishRemoteState();
847
868
  return result;
848
869
  } catch (err) {
849
- if (activating && pointerToken === clientToken
850
- && c.remoteSessionId === bindingSessionId) {
851
- pointerToken = previousPointerToken && clients.has(previousPointerToken)
852
- ? previousPointerToken
853
- : null;
854
- c.remoteSessionId = previousRemoteSessionId;
855
- remoteAcquired = previousRemoteAcquired;
856
- stickyRemoteFrame = previousStickyRemoteFrame;
857
- publishRemoteState();
858
- }
870
+ rollbackActivation();
871
+ publishRemoteState();
859
872
  throw err;
860
873
  }
861
874
  };
862
- const scheduler = BINDING_TOOLS.has(name) ? channelControlCalls : channelCalls;
863
- dispatch = scheduler.enqueue(ownerKey, run);
875
+ const bindingCall = BINDING_TOOLS.has(name);
876
+ const scheduler = bindingCall ? channelControlCalls : channelCalls;
877
+ dispatch = scheduler.enqueue(ownerKey, bindingCall ? () => runExclusiveBinding(run) : run);
864
878
  if (cacheKey) {
865
879
  const record = { promise: dispatch, at: nowMs() };
866
880
  callCache.set(cacheKey, record);
@@ -880,7 +894,12 @@ export function createChannelTransport({
880
894
  const result = await dispatch;
881
895
  sendJson(res, { result });
882
896
  } catch (err) {
883
- sendJson(res, { error: err?.message || String(err) }, 200);
897
+ // Keep the machine-readable code beside the message: callers branch
898
+ // on it instead of pattern-matching free text.
899
+ sendJson(res, {
900
+ error: err?.message || String(err),
901
+ ...(err?.code ? { code: String(err.code) } : {}),
902
+ }, 200);
884
903
  }
885
904
  return;
886
905
  }
@@ -40,6 +40,7 @@ import { writeJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
40
40
  import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../runtime/shared/runtime-root.mjs';
41
41
  import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
42
42
  import { claimSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
43
+ import { remoteIntentPath } from '../runtime/shared/remote-intent.mjs';
43
44
  import { PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../lib/mixdog-debug.cjs';
44
45
  import { setChannelNotifySink } from '../runtime/channels/lib/parent-bridge.mjs';
45
46
  import { setOwnerContext } from '../runtime/channels/lib/runtime-paths.mjs';
@@ -484,6 +485,19 @@ async function main() {
484
485
  handleCall,
485
486
  agentBroker: agentDispatchBroker,
486
487
  log,
488
+ // The durable channel link lives in this file. Without it a restart loses
489
+ // the pinned session (nothing to restore) and every catalog reports Remote
490
+ // disabled because no state ever reaches getRemoteSessionState().
491
+ remoteIntentPath: remoteIntentPath(RUNTIME_ROOT),
492
+ onRemoteStateChange: (state) => {
493
+ remoteSessionState = {
494
+ enabled: state?.enabled === true,
495
+ sessionId: state?.sessionId ?? null,
496
+ cwd: state?.cwd ?? null,
497
+ daemonPid: state?.daemonPid ?? process.pid,
498
+ updatedAt: state?.updatedAt ?? Date.now(),
499
+ };
500
+ },
487
501
  // Self-shutdown when the last attached TUI leaves (reuses the SSE/client
488
502
  // registry as the liveness signal).
489
503
  onClientsEmpty: () => { maybeSelfShutdown('no live channel clients'); },
@@ -566,7 +580,11 @@ async function main() {
566
580
  };
567
581
  let sessionRuntimePrewarmStarted = false;
568
582
  let sessionRuntimePrewarmPromise = null;
569
- sessionRuntimeHost = createSessionRuntimeHost({ cwd: CWD, log });
583
+ sessionRuntimeHost = createSessionRuntimeHost({
584
+ cwd: CWD,
585
+ log,
586
+ canRecycle: () => (sessionService?.busyCount ?? 1) === 0,
587
+ });
570
588
  function prewarmSessionRuntime() {
571
589
  if (sessionRuntimePrewarmPromise) return sessionRuntimePrewarmPromise;
572
590
  sessionRuntimePrewarmStarted = true;
@@ -575,7 +593,7 @@ async function main() {
575
593
  .catch((error) => log(`image pipeline prewarm failed (non-fatal): ${error?.message || error}`));
576
594
  sessionRuntimePrewarmPromise = sessionRuntimeHost.prewarm()
577
595
  .then(() => {
578
- log('session runtime worker/agent-loop/keychain/provider prewarm ready');
596
+ log('session runtime worker/agent-loop/keychain/memory prewarm ready');
579
597
  return true;
580
598
  })
581
599
  .catch((error) => {
@@ -712,6 +730,17 @@ async function main() {
712
730
  // client — see the transport's onClientRegistered hook.
713
731
  if (process.env.MIXDOG_DAEMON_SPAWNED_FOR !== 'session') startChannels();
714
732
 
733
+ // A pinned channel session outlives the daemon that pinned it. Reactivate the
734
+ // durable intent AFTER the ready handshake (activation itself brings the
735
+ // messaging bridge up); with no intent on disk this is a no-op and the
736
+ // channels graph stays dormant.
737
+ if (transport.remoteIntentSessionId) {
738
+ void transport.restoreRemoteIntent()
739
+ .then((restored) => {
740
+ if (!restored) log('remote intent restore declined (session unavailable)');
741
+ })
742
+ .catch((error) => log(`remote intent restore failed: ${error?.message || error}`));
743
+ }
715
744
  }
716
745
 
717
746
  process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
@@ -116,16 +116,32 @@ export function hookConfigEntries(dataDir, cwd) {
116
116
  ]);
117
117
  }
118
118
 
119
- export function isStandardConfig(parsed) {
120
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
119
+ /** Classify a hooks file and name the malformed events inside it.
120
+ *
121
+ * One bad value under `hooks` used to demote the WHOLE file to legacy, which
122
+ * silently disabled every standard policy handler in it. A file with at least
123
+ * one well-formed event stays standard; the malformed keys are reported so the
124
+ * caller can surface them instead of dropping them in silence. */
125
+ export function standardConfigReport(parsed) {
126
+ const empty = { standard: false, validEvents: [], invalidEvents: [] };
127
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return empty;
121
128
  const hooks = parsed.hooks;
122
- if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return false;
123
- const values = Object.values(hooks);
124
- if (values.length === 0) return false;
125
- return values.every((groups) => (
126
- Array.isArray(groups)
127
- && groups.every((g) => g && typeof g === 'object' && Array.isArray(g.hooks))
128
- ));
129
+ if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return empty;
130
+ const entries = Object.entries(hooks);
131
+ if (entries.length === 0) return empty;
132
+ const validEvents = [];
133
+ const invalidEvents = [];
134
+ for (const [eventName, groups] of entries) {
135
+ const wellFormed = Array.isArray(groups)
136
+ && groups.every((g) => g && typeof g === 'object' && Array.isArray(g.hooks));
137
+ if (wellFormed) validEvents.push(eventName);
138
+ else invalidEvents.push(eventName);
139
+ }
140
+ return { standard: validEvents.length > 0, validEvents, invalidEvents };
141
+ }
142
+
143
+ export function isStandardConfig(parsed) {
144
+ return standardConfigReport(parsed).standard;
129
145
  }
130
146
 
131
147
  const SIMPLE_MATCHER_RE = /^[A-Za-z0-9_ ,|]*$/;
@@ -158,7 +158,7 @@ function hookEnv(projectDir, pluginData, payload, pluginRoot = null) {
158
158
  return env;
159
159
  }
160
160
 
161
- export function runCommandHandler(handler, payload, eventName, pluginData, onSpawnError = null, onRewake = null) {
161
+ export function runCommandHandler(handler, payload, eventName, pluginData, onSpawnError = null) {
162
162
  const projectDir = payload.cwd || process.cwd();
163
163
  const effectivePluginData = handler._pluginData || pluginData || null;
164
164
  const stdin = JSON.stringify(payload);
@@ -171,55 +171,6 @@ export function runCommandHandler(handler, payload, eventName, pluginData, onSpa
171
171
  stdio: ['pipe', 'pipe', 'pipe'],
172
172
  };
173
173
 
174
- if (handler.asyncRewake === true) {
175
- try {
176
- const child = spawn(spec.command, spec.args, withProcessGroup(baseOpts));
177
- let bgStdout = '';
178
- let bgStderr = '';
179
- let bgStdoutBytes = 0;
180
- let bgStderrBytes = 0;
181
- let killed = false;
182
- child.stdout?.on('data', (chunk) => {
183
- bgStdoutBytes += chunk.length;
184
- if (bgStdoutBytes <= MAX_BUFFER_BYTES) bgStdout += chunk.toString('utf8');
185
- });
186
- child.stderr?.on('data', (chunk) => {
187
- bgStderrBytes += chunk.length;
188
- if (bgStderrBytes <= MAX_BUFFER_BYTES) bgStderr += chunk.toString('utf8');
189
- });
190
- const killTimer = setTimeout(() => {
191
- killed = true;
192
- try { child.stdout?.removeAllListeners('data'); } catch {}
193
- try { child.stderr?.removeAllListeners('data'); } catch {}
194
- terminateTree(child);
195
- }, timeoutMs);
196
- killTimer.unref?.();
197
- child.on('error', (error) => {
198
- clearTimeout(killTimer);
199
- if (typeof onSpawnError === 'function') onSpawnError(error);
200
- });
201
- child.on('close', (code) => {
202
- clearTimeout(killTimer);
203
- try {
204
- if (!killed && code === 2 && typeof onRewake === 'function') {
205
- const text = (bgStderr || '').trim() || (bgStdout || '').trim();
206
- onRewake({ eventName, payload, text });
207
- }
208
- } catch {}
209
- });
210
- child.stdin?.end(stdin);
211
- return Promise.resolve({ exitCode: 0, stdout: '', stderr: '', async: true });
212
- } catch (error) {
213
- return Promise.resolve({
214
- exitCode: -1,
215
- stdout: '',
216
- stderr: error?.message || String(error),
217
- timedOut: false,
218
- spawnError: error,
219
- });
220
- }
221
- }
222
-
223
174
  if (handler.async === true) {
224
175
  try {
225
176
  const child = spawn(spec.command, spec.args, withProcessGroup({
@@ -448,12 +399,25 @@ export async function runMcpToolHandler(handler, payload, eventName, mcpToolRunn
448
399
  return { exitCode: -1, stdout: '', stderr: 'mcp_tool handler missing tool name', timedOut: false, spawnError: null };
449
400
  }
450
401
  let timer = null;
402
+ // Losing the race is not cancellation: without this signal the tool call kept
403
+ // running (and holding its MCP slot) long after the hook gave up on it.
404
+ const controller = new AbortController();
451
405
  try {
452
- const runPromise = Promise.resolve(mcpToolRunner({ name, args: payload }));
406
+ const runPromise = Promise.resolve(mcpToolRunner({
407
+ name,
408
+ args: payload,
409
+ signal: controller.signal,
410
+ timeoutMs,
411
+ }));
412
+ // The abandoned call still settles somewhere; keep its rejection handled.
413
+ runPromise.catch(() => {});
453
414
  const text = await Promise.race([
454
415
  runPromise,
455
416
  new Promise((_r, reject) => {
456
- timer = setTimeout(() => reject(new Error(`mcp_tool hook timed out: ${name}`)), timeoutMs);
417
+ timer = setTimeout(() => {
418
+ try { controller.abort(new Error(`mcp_tool hook timed out: ${name}`)); } catch {}
419
+ reject(new Error(`mcp_tool hook timed out: ${name}`));
420
+ }, timeoutMs);
457
421
  // No unref: this timer must keep the event loop alive so the race can
458
422
  // settle even when the runner promise never resolves. Cleared in finally.
459
423
  }),
@@ -6,28 +6,25 @@ import {
6
6
  SUPPORTED_HANDLER_TYPES,
7
7
  EXIT2_BLOCK_EVENTS,
8
8
  TOP_LEVEL_DECISION_EVENTS,
9
- limitText,
10
9
  } from './hook-bus/constants.mjs';
11
10
  import {
12
11
  buildEventPayload,
13
12
  hookConfigEntries,
14
13
  hookRulesPath,
15
- isStandardConfig,
16
14
  matchFieldFor,
17
15
  matcherFires,
18
16
  mergeEvents,
19
17
  normalizeRules,
20
18
  parseStandardConfig,
19
+ standardConfigReport,
21
20
  } from './hook-bus/config.mjs';
22
21
  import {
23
- handlerTimeoutS,
24
22
  ifConditionPasses,
25
23
  parseHandlerOutput,
26
24
  runCommandHandler,
27
25
  runHttpHandler,
28
26
  runMcpToolHandler,
29
27
  runPromptHandler,
30
- defaultShellKind,
31
28
  } from './hook-bus/handlers.mjs';
32
29
  import {
33
30
  compactValue,
@@ -57,6 +54,7 @@ export {
57
54
  mergeEvents,
58
55
  normalizeRules,
59
56
  parseStandardConfig,
57
+ standardConfigReport,
60
58
  } from './hook-bus/config.mjs';
61
59
  export {
62
60
  handlerTimeoutS,
@@ -84,7 +82,6 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null, prompt
84
82
  const counts = new Map(DEFAULT_EVENTS.map((name) => [name, 0]));
85
83
  const rulesPath = hookRulesPath(dataDir);
86
84
  const pluginData = dataDir || null;
87
- let rewakeHandler = null;
88
85
  let rulesCache = { mtimeMs: -1, rules: [] };
89
86
  let configCache = {
90
87
  key: '',
@@ -163,10 +160,27 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null, prompt
163
160
  disabled = parsed.disableAllHooks === true;
164
161
  disableSeen = true;
165
162
  }
166
- if (isStandardConfig(parsed)) {
163
+ const report = standardConfigReport(parsed);
164
+ if (report.standard) {
165
+ // A malformed event no longer demotes the whole file to legacy (which
166
+ // silently disabled every standard handler in it): keep the valid
167
+ // events and report the broken ones.
168
+ if (report.invalidEvents.length > 0) {
169
+ errors.push({
170
+ file: filePath,
171
+ error: `ignored malformed hook event(s): ${report.invalidEvents.join(', ')}`,
172
+ });
173
+ }
167
174
  mergeEvents(events, parseStandardConfig(parsed, filePath, entry));
168
175
  } else {
169
- legacyRules.push(...normalizeRules(parsed).filter((rule) => rule && typeof rule === 'object'));
176
+ const fileRules = normalizeRules(parsed).filter((rule) => rule && typeof rule === 'object');
177
+ if (fileRules.length === 0 && report.invalidEvents.length > 0) {
178
+ errors.push({
179
+ file: filePath,
180
+ error: `no usable hooks: malformed event(s) under "hooks": ${report.invalidEvents.join(', ')}`,
181
+ });
182
+ }
183
+ legacyRules.push(...fileRules);
170
184
  }
171
185
  }
172
186
  configCache = {
@@ -230,16 +244,7 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null, prompt
230
244
  error: `hook spawn failed: ${error?.message || error}`,
231
245
  });
232
246
  };
233
- const reportRewake = ({ eventName: en, payload: pl, text }) => {
234
- try {
235
- if (typeof rewakeHandler === 'function') {
236
- rewakeHandler({ eventName: en, payload: pl, text });
237
- } else {
238
- emit('hook:rewake', { name: pl?.tool_name || en, text: text || null });
239
- }
240
- } catch {}
241
- };
242
- return await runCommandHandler(handler, payload, eventName, pluginData, reportSpawnError, reportRewake);
247
+ return await runCommandHandler(handler, payload, eventName, pluginData, reportSpawnError);
243
248
  }
244
249
  if (type === 'http') {
245
250
  if (!handler.url) return null;
@@ -334,6 +339,8 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null, prompt
334
339
  }
335
340
 
336
341
  function addRule(rule = {}) {
342
+ // Same index-safety rule as deleteRule: settle debounced toggles first.
343
+ flushRules();
337
344
  const action = String(rule.action || rule.decision || '').trim().toLowerCase();
338
345
  if (!action || !['allow', 'deny', 'block', 'modify', 'rewrite', 'ask'].includes(action)) {
339
346
  throw new Error('hook rule action must be allow, deny, block, modify, rewrite, or ask');
@@ -428,6 +435,10 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null, prompt
428
435
  }
429
436
 
430
437
  function deleteRule(index) {
438
+ // Pending enabled-state patches are addressed by INDEX. Flushing them
439
+ // before the splice keeps a debounced toggle from landing on whatever rule
440
+ // shifted into that slot after the delete.
441
+ flushRules();
431
442
  const rules = [...loadRules()];
432
443
  if (!Number.isInteger(index) || index < 0 || index >= rules.length) throw new Error(`hook rule not found: ${index}`);
433
444
  rules.splice(index, 1);
@@ -564,10 +575,5 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null, prompt
564
575
  };
565
576
  }
566
577
 
567
- function setRewakeHandler(fn) {
568
- rewakeHandler = typeof fn === 'function' ? fn : null;
569
- return rewakeHandler;
570
- }
571
-
572
- return { addRule, beforeTool, deleteRule, dispatch, emit, flushRules, listRules, setRewakeHandler, setRuleEnabled, status };
578
+ return { addRule, beforeTool, deleteRule, dispatch, emit, flushRules, listRules, setRuleEnabled, status };
573
579
  }
@@ -6,6 +6,7 @@ import { dirname, join, resolve } from 'node:path';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { claimSingletonOwner, handoffSingletonOwner, readSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
8
8
  import { readLiveServiceAdvert } from '../runtime/shared/service-discovery.mjs';
9
+ import { isPidAlive, parsePid } from '../runtime/shared/pid-liveness.mjs';
9
10
  import { resolveRuntimeRoot } from '../runtime/shared/runtime-root.mjs';
10
11
  import { sleep as delay } from '../runtime/shared/sleep.mjs';
11
12
  import { scrubLoaderVars } from '../runtime/agent/orchestrator/tools/env-scrub.mjs';
@@ -40,22 +41,6 @@ function parsePort(value) {
40
41
  return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
41
42
  }
42
43
 
43
- function parsePid(value) {
44
- const pid = Number(value);
45
- return Number.isInteger(pid) && pid > 0 ? pid : null;
46
- }
47
-
48
- function isPidAlive(pid) {
49
- const n = parsePid(pid);
50
- if (!n) return false;
51
- try {
52
- process.kill(n, 0);
53
- return true;
54
- } catch (error) {
55
- return error?.code === 'EPERM';
56
- }
57
- }
58
-
59
44
  const TRANSIENT_MEMORY_RPC_BACKOFF_MS = 400;
60
45
  // A child that dies during startup with one of these signatures is a hard,
61
46
  // deterministic failure (bad entry path, syntax/require error) — never a