mixdog 0.9.96 → 0.9.97

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 (170) hide show
  1. package/README.md +0 -17
  2. package/package.json +8 -7
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/lead/01-general.md +1 -2
  9. package/src/rules/lead/lead-tool.md +0 -3
  10. package/src/rules/shared/01-tool.md +11 -10
  11. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  12. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  13. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  14. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  15. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  16. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  18. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  19. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  20. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
  21. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  22. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  23. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  24. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  25. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  27. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  28. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  29. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  30. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  33. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  34. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  35. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  36. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  37. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  38. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  39. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  41. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
  44. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  47. package/src/runtime/channels/lib/config.mjs +33 -33
  48. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  49. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  50. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  51. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  52. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  53. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  54. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  55. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  56. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  57. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  58. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  59. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  60. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  61. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  62. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  63. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  64. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  65. package/src/runtime/channels/tool-defs.mjs +1 -1
  66. package/src/runtime/memory/index.mjs +72 -211
  67. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  68. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  69. package/src/runtime/memory/lib/http-router.mjs +1 -6
  70. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  71. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  72. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  73. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  74. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  75. package/src/runtime/memory/lib/memory.mjs +1 -1
  76. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  77. package/src/runtime/search/index.mjs +2 -2
  78. package/src/runtime/shared/abort-race.mjs +70 -0
  79. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  80. package/src/runtime/shared/config.mjs +11 -24
  81. package/src/runtime/shared/llm/index.mjs +1 -1
  82. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  83. package/src/runtime/shared/resource-admission.mjs +6 -5
  84. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  85. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  86. package/src/runtime/shared/tool-surface.mjs +91 -22
  87. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  88. package/src/runtime/shared/turn-snapshot.mjs +17 -2
  89. package/src/session-runtime/channel-config-api.mjs +5 -5
  90. package/src/session-runtime/config-lifecycle.mjs +27 -27
  91. package/src/session-runtime/lifecycle-api.mjs +22 -3
  92. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  93. package/src/session-runtime/prewarm.mjs +1 -1
  94. package/src/session-runtime/provider-models.mjs +1 -1
  95. package/src/session-runtime/remote-control.mjs +6 -7
  96. package/src/session-runtime/runtime-core.mjs +52 -28
  97. package/src/session-runtime/self-update.mjs +1 -1
  98. package/src/session-runtime/session-lifecycle.mjs +36 -15
  99. package/src/session-runtime/session-title.mjs +1 -1
  100. package/src/session-runtime/session-turn-api.mjs +55 -15
  101. package/src/session-runtime/settings-api.mjs +7 -7
  102. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  103. package/src/standalone/channel-admin.mjs +42 -53
  104. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  105. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
  106. package/src/standalone/channel-worker.mjs +124 -647
  107. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
  108. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  109. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  110. package/src/standalone/session-protocol.mjs +8 -4
  111. package/src/standalone/session-runtime-pool.mjs +393 -0
  112. package/src/standalone/session-runtime-record.mjs +20 -0
  113. package/src/standalone/session-runtime-worker.mjs +203 -0
  114. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
  115. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
  116. package/src/standalone/session-wire.mjs +55 -0
  117. package/src/tui/App.jsx +4 -4
  118. package/src/tui/app/app-format.mjs +1 -1
  119. package/src/tui/app/channel-pickers.mjs +41 -41
  120. package/src/tui/app/input-parsers.mjs +2 -2
  121. package/src/tui/app/onboarding-steps.mjs +2 -2
  122. package/src/tui/app/project-picker.mjs +8 -8
  123. package/src/tui/app/prompt-submit.mjs +8 -8
  124. package/src/tui/app/settings-picker.mjs +25 -25
  125. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  126. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  127. package/src/tui/components/ToolExecution.jsx +1 -1
  128. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  129. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  130. package/src/tui/dist/index.mjs +154 -127
  131. package/src/tui/hooks/useSession.mjs +16 -0
  132. package/src/tui/index.jsx +11 -11
  133. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  134. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  135. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  136. package/src/tui/{engine → session}/context-state.mjs +3 -3
  137. package/src/tui/{engine → session}/labels.mjs +2 -2
  138. package/src/tui/{engine → session}/live-share.mjs +7 -7
  139. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  140. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  141. package/src/tui/session/oauth-flows.mjs +140 -0
  142. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  143. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  144. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  145. package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
  146. package/src/tui/{engine → session}/session-api.mjs +7 -60
  147. package/src/tui/{engine → session}/session-flow.mjs +1 -1
  148. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  149. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  150. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  151. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  152. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  153. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  154. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  155. package/src/tui/{engine → session}/turn.mjs +29 -107
  156. package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
  157. package/src/tui/session.mjs +22 -0
  158. package/src/ui/statusline-segments.mjs +1 -1
  159. package/src/ui/statusline.mjs +1 -1
  160. package/vendor/ink/build/components/App.js +10 -1
  161. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  162. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  163. package/src/tui/engine/turn-watchdog.mjs +0 -92
  164. package/src/tui/engine.mjs +0 -22
  165. package/src/tui/hooks/useEngine.mjs +0 -16
  166. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  167. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  168. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  169. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  170. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
package/README.md CHANGED
@@ -65,23 +65,6 @@ measured cache writes; the archived OpenAI runs did not retain
65
65
  artifacts, the exact run commands, and the metric scripts that recompute
66
66
  every number above live under `benchmarks/terminal-bench-2.1/`.
67
67
 
68
- ## Boot overhead — measured, not estimated
69
-
70
- Fixed cost of the default system prompt + tool schemas, measured with a
71
- minimal one-turn ping ("hi"). Every number below is what the provider's own
72
- usage accounting reported for the first API call — same tokenizer on both
73
- sides of each row, no offline estimates (2026-08).
74
-
75
- | Tokenizer | Baseline harness | First-call prompt | mixdog Lead | mixdog worker agent |
76
- |-----------|------------------|------------------:|------------:|--------------------:|
77
- | Anthropic — pristine containers, TB2.1 harness | Claude Code 2.1.220 | 19,149 tok (median, n=89) | **7,295 (−62%)** | 4,856 (−75%) |
78
- | OpenAI — same host, same model (`gpt-5.6-sol`) | Codex CLI 0.145.0 | 17,706 tok | **7,488 (−58%)** | 4,616 (−74%) |
79
-
80
- The Lead surface carries the full default rule set, skills manifest, and
81
- per-user profile; spawning a sub-agent costs a flat ~4.6–4.9k tokens. Low
82
- boot overhead compounds: it is re-read on every request, so it is the
83
- baseline of every cache write, compaction, and long-session turn.
84
-
85
68
  ## Why mixdog
86
69
 
87
70
  **Maximum performance at minimum cost**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.96",
3
+ "version": "0.9.97",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -71,18 +71,19 @@
71
71
  "test:agent-job-views": "node --test scripts/agent-job-terminal-view-test.mjs",
72
72
  "test:agent-fanout": "node scripts/agent-parallel-smoke.mjs && node --test scripts/agent-route-batch-test.mjs scripts/execution-completion-dedup-test.mjs",
73
73
  "test:toolcall": "node --test scripts/toolcall-args-test.mjs",
74
- "test:tool-batching": "node --test scripts/eager-patch-shell-order-test.mjs scripts/session-bench-batching-test.mjs",
74
+ "test:tool-batching": "node --test scripts/eager-patch-shell-order-test.mjs scripts/session-bench-batching-test.mjs scripts/tool-cwd-rebind-test.mjs",
75
75
  "test:shipmode": "node --test scripts/ship-mode-test.mjs",
76
76
  "test:shellhardening": "node --test scripts/shell-hardening-test.mjs scripts/shell-failure-diagnostics-test.mjs scripts/windows-hide-spawn-options-test.mjs scripts/shell-job-session-scope-test.mjs",
77
77
  "test:placeholder": "node --test scripts/compacted-placeholder-scrub-test.mjs",
78
78
  "test:providers": "node --test scripts/provider-toolcall-test.mjs scripts/provider-contract-test.mjs scripts/provider-stream-stall-test.mjs scripts/provider-stream-outcome-test.mjs scripts/stream-frame-fault-matrix-test.mjs scripts/gemini-provider-test.mjs scripts/anthropic-transport-policy-test.mjs scripts/anthropic-native-block-replay-test.mjs scripts/openai-oauth-ws-1006-retry-test.mjs scripts/stream-close-retry-test.mjs scripts/openai-end-turn-signal-test.mjs",
79
79
  "test:provider-admission": "node --test scripts/provider-admission-scheduler-test.mjs scripts/provider-stream-json-test.mjs",
80
- "test:resource-admission": "node --test scripts/resource-admission-test.mjs scripts/child-spawn-isolation-test.mjs",
80
+ "test:resource-admission": "node --test scripts/resource-admission-test.mjs scripts/child-spawn-isolation-test.mjs scripts/owner-fair-gate-test.mjs",
81
81
  "test:deferred-tools": "node --test scripts/deferred-tool-loading-test.mjs",
82
82
  "test:anthropic-oauth-race": "node --test scripts/anthropic-oauth-refresh-race-test.mjs",
83
83
  "test:grok-oauth-race": "node --test scripts/grok-oauth-refresh-race-test.mjs",
84
84
  "test:atomiclock": "node --test scripts/atomic-lock-tryonce-test.mjs",
85
85
  "test:keychain": "node --test scripts/keychain-prewarm-test.mjs",
86
+ "test:memory-leaks": "node --expose-gc --test scripts/memory-retention-test.mjs scripts/memory-worker-stability-test.mjs scripts/session-transport-test.mjs apps/desktop/src/renderer/memory-retention.test.mjs",
86
87
  "test:memory-routing": "node --test scripts/memory-cycle-routing-test.mjs scripts/maintenance-default-routes-test.mjs scripts/embedding-worker-exit-test.mjs scripts/embedding-runtime-prune-test.mjs",
87
88
  "test:embedding-runtime": "node --test scripts/embedding-runtime-prune-test.mjs scripts/memory-pg-recovery-test.mjs && node scripts/verify-embedding-runtime.mjs",
88
89
  "test:embedding-runtime:core": "node --test scripts/embedding-runtime-prune-test.mjs scripts/memory-pg-recovery-test.mjs && node scripts/verify-embedding-runtime.mjs --core",
@@ -98,9 +99,9 @@
98
99
  "test:patch-binary-cache": "node --test scripts/patch-binary-cache-test.mjs",
99
100
  "test:patch-parity": "node --test scripts/v4a-parity-test.mjs",
100
101
  "test:project-registry": "node --test scripts/project-registry-isolation-test.mjs",
101
- "test:engine-daemon": "node --test scripts/fair-call-scheduler-test.mjs scripts/engine-daemon-test.mjs scripts/engine-daemon-tui-parity-test.mjs scripts/engine-daemon-recovery-test.mjs scripts/engine-daemon-submit-delivery-test.mjs scripts/engine-daemon-process-lifetime-test.mjs scripts/engine-daemon-multipane-stress-test.mjs && node --test scripts/runtime-isolation-stress-test.mjs",
102
- "smoke:engine-daemon": "node scripts/engine-daemon-smoke.mjs",
103
- "test:session": "node --test scripts/session-orphan-sweep-test.mjs scripts/interrupted-turn-history-test.mjs scripts/turn-checkpoint-crash-test.mjs scripts/turn-outcome-fault-matrix-test.mjs scripts/session-save-fault-store-test.mjs scripts/session-disk-authority-test.mjs scripts/session-load-cache-race-test.mjs scripts/agent-loop-complete-turn-test.mjs scripts/session-heartbeat-lifecycle-test.mjs scripts/remote-transition-order-test.mjs scripts/session-new-reset-test.mjs scripts/turn-ttft-critical-path-test.mjs",
102
+ "test:session-transport": "node --test scripts/fair-call-scheduler-test.mjs scripts/session-transport-test.mjs scripts/session-tui-parity-test.mjs scripts/session-recovery-test.mjs scripts/session-submit-delivery-test.mjs scripts/daemon-process-lifetime-test.mjs scripts/session-multipane-stress-test.mjs && node --test scripts/runtime-isolation-stress-test.mjs",
103
+ "smoke:session": "node scripts/session-smoke.mjs",
104
+ "test:session": "node --test scripts/runtime-turn-contract-test.mjs scripts/session-orphan-sweep-test.mjs scripts/interrupted-turn-history-test.mjs scripts/turn-checkpoint-crash-test.mjs scripts/turn-outcome-fault-matrix-test.mjs scripts/session-save-fault-store-test.mjs scripts/session-disk-authority-test.mjs scripts/session-load-cache-race-test.mjs scripts/agent-loop-complete-turn-test.mjs scripts/session-heartbeat-lifecycle-test.mjs scripts/remote-transition-order-test.mjs scripts/session-new-reset-test.mjs scripts/turn-ttft-critical-path-test.mjs",
104
105
  "test:live-canary": "node --test scripts/live-canary-test.mjs",
105
106
  "test:rebindtail": "node --test scripts/forwarder-rebind-tail-test.mjs scripts/channel-remote-format-test.mjs",
106
107
  "test:workflow-editor": "node --test scripts/workflow-id-test.mjs scripts/workflow-pack-editor-test.mjs",
@@ -122,7 +123,7 @@
122
123
  "bench:explore": "node scripts/explore-bench.mjs",
123
124
  "bench:output-style": "node scripts/output-style-bench.mjs",
124
125
  "bench:session-context": "node scripts/session-context-bench.mjs",
125
- "bench:engine-daemon": "node scripts/engine-daemon-bench.mjs",
126
+ "bench:session-transport": "node scripts/session-transport-bench.mjs",
126
127
  "bench:tool-overhead": "node scripts/tool-overhead-microbench.mjs",
127
128
  "bench:internal-comms": "node scripts/internal-comms-bench.mjs",
128
129
  "audit:models": "node scripts/model-catalog-audit.mjs",
@@ -41,13 +41,13 @@ const sharedRuntimeExternalPlugin = {
41
41
  },
42
42
  };
43
43
 
44
- const engineDaemonClientExternalPlugin = {
45
- name: 'mixdog-engine-daemon-client-external',
44
+ const sessionClientExternalPlugin = {
45
+ name: 'mixdog-session-client-external',
46
46
  setup(build) {
47
- build.onResolve({ filter: /^\.\.\/standalone\/engine-daemon-client\.mjs$/ }, () => ({
47
+ build.onResolve({ filter: /^\.\.\/standalone\/session-client\.mjs$/ }, () => ({
48
48
  // Keep this module outside the TUI bundle so its import.meta.url stays
49
- // anchored in src/standalone, where backend-daemon.mjs actually lives.
50
- path: '../../standalone/engine-daemon-client.mjs',
49
+ // anchored in src/standalone, where daemon.mjs actually lives.
50
+ path: '../../standalone/session-client.mjs',
51
51
  external: true,
52
52
  }));
53
53
  },
@@ -90,7 +90,7 @@ await build({
90
90
  plugins: [
91
91
  mixdogInkAliasPlugin,
92
92
  sharedRuntimeExternalPlugin,
93
- engineDaemonClientExternalPlugin,
93
+ sessionClientExternalPlugin,
94
94
  ],
95
95
  logLevel: 'info',
96
96
  });
@@ -1,17 +1,18 @@
1
- // Stub channels daemon for the flip smoke: same lifecycle contract as
2
- // src/standalone/channel-daemon.mjs (pid-verified singleton claim, HTTP+SSE
1
+ // Stub channel service for the flip smoke: same lifecycle contract as
2
+ // src/standalone/daemon.mjs (pid-verified singleton claim, HTTP+SSE
3
3
  // transport, discovery file, ready handshake, client-grace self-shutdown) but
4
4
  // with a STUB runtime instead of worker-main — no Discord token needed. The
5
5
  // real channel-worker.mjs spawn-or-attach path forks THIS via
6
- // MIXDOG_CHANNEL_DAEMON_ENTRY, so the flip is exercised end to end.
6
+ // MIXDOG_DAEMON_ENTRY, so the flip is exercised end to end.
7
7
  process.env.MIXDOG_WORKER_MODE = process.env.MIXDOG_WORKER_MODE || '1';
8
8
 
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
- import { mkdirSync, writeFileSync } from 'node:fs';
11
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { writeJsonAtomicSync } from '../src/runtime/shared/atomic-file.mjs';
12
13
  import { claimSingletonOwner, releaseSingletonOwner } from '../src/runtime/shared/singleton-owner.mjs';
13
14
  import { safeIpcSend } from '../src/runtime/shared/safe-ipc-send.mjs';
14
- import { createChannelDaemonTransport } from '../src/standalone/channel-daemon-transport.mjs';
15
+ import { createChannelTransport } from '../src/standalone/channel-transport.mjs';
15
16
 
16
17
  function runtimeRoot() {
17
18
  return process.env.MIXDOG_RUNTIME_ROOT
@@ -20,8 +21,8 @@ function runtimeRoot() {
20
21
  }
21
22
  const RUNTIME_ROOT = runtimeRoot();
22
23
  const DATA_DIR = process.env.MIXDOG_DATA_DIR ? path.resolve(process.env.MIXDOG_DATA_DIR) : RUNTIME_ROOT;
23
- const DISCOVERY_PATH = path.join(RUNTIME_ROOT, 'channel-daemon.json');
24
- const OWNER_PATH = path.join(DATA_DIR, 'channel-daemon-owner.json');
24
+ const DISCOVERY_PATH = path.join(RUNTIME_ROOT, 'daemon.json');
25
+ const OWNER_PATH = path.join(DATA_DIR, 'daemon-owner.json');
25
26
 
26
27
  function log(line) { if (process.env.DAEMON_SMOKE_VERBOSE) process.stderr.write(`[stub-daemon] ${line}\n`); }
27
28
 
@@ -31,23 +32,24 @@ async function shutdown(reason, code = 0) {
31
32
  if (shuttingDown) return;
32
33
  shuttingDown = true;
33
34
  try { await transport?.stop?.(); } catch {}
35
+ try { rmSync(DISCOVERY_PATH, { force: true }); } catch {}
34
36
  try { releaseSingletonOwner(OWNER_PATH, process.pid); } catch {}
35
37
  process.exit(code);
36
38
  }
37
39
 
38
40
  async function main() {
39
41
  try { mkdirSync(RUNTIME_ROOT, { recursive: true }); } catch {}
40
- const claim = claimSingletonOwner(OWNER_PATH, { kind: 'channel-runtime-daemon', pid: process.pid, meta: { cwd: process.cwd() } });
42
+ const claim = claimSingletonOwner(OWNER_PATH, { kind: 'mixdog-daemon', pid: process.pid, meta: { cwd: process.cwd() } });
41
43
  if (!claim.owned) { process.exit(0); } // race loser → spawner attaches to winner
42
44
  process.on('exit', () => { try { releaseSingletonOwner(OWNER_PATH, process.pid); } catch {} });
43
45
 
44
46
  // Optional env dump for the tool smoke: proves daemonEnv() spawned us with
45
- // the daemon-mode flags (MIXDOG_CHANNEL_DAEMON=1, MIXDOG_CLI_OWNED=0).
47
+ // the host-mode flags (MIXDOG_DAEMON_HOST=1, MIXDOG_CLI_OWNED=0).
46
48
  if (process.env.SMOKE_CHANNEL_ENV_OUT) {
47
49
  try {
48
50
  writeFileSync(process.env.SMOKE_CHANNEL_ENV_OUT, JSON.stringify({
49
51
  cliOwned: process.env.MIXDOG_CLI_OWNED,
50
- daemon: process.env.MIXDOG_CHANNEL_DAEMON,
52
+ host: process.env.MIXDOG_DAEMON_HOST,
51
53
  }));
52
54
  } catch { /* smoke-only, best-effort */ }
53
55
  }
@@ -61,15 +63,19 @@ async function main() {
61
63
  return { ok: true, name, args, leadPid: ctx.leadPid };
62
64
  };
63
65
 
64
- transport = createChannelDaemonTransport({
66
+ transport = createChannelTransport({
65
67
  handleCall,
66
- discoveryPath: DISCOVERY_PATH,
67
68
  clientGraceMs: 250,
68
69
  sweepMs: 1000,
69
70
  log,
70
71
  onClientsEmpty: () => { void shutdown('no live clients'); },
71
72
  });
72
73
  const { port, token } = await transport.start();
74
+ writeJsonAtomicSync(DISCOVERY_PATH, {
75
+ pid: process.pid,
76
+ startedAt: Date.now(),
77
+ endpoints: { channel: { port, token } },
78
+ }, { compact: true });
73
79
  safeIpcSend(process, { type: 'ready', port, token });
74
80
  log(`ready port=${port} pid=${process.pid}`);
75
81
  }
@@ -132,7 +132,7 @@ apply it, and verify the result.
132
132
  ### Discord / Telegram / voice channels
133
133
 
134
134
  1. Check `/channels` (or `/setting` → Channel) and runtime channel status.
135
- 2. Update the requested backend, credential, or main channel/chat setting.
135
+ 2. Update the requested provider, credential, or main channel/chat setting.
136
136
  3. Restart/reconnect only when the channel implementation requires it.
137
137
  4. Verify channel presence and a non-secret status signal.
138
138
 
@@ -6,7 +6,7 @@ const fs = require('fs');
6
6
  const { resolvePluginData } = require('./plugin-paths.cjs');
7
7
 
8
8
  const SERVICE = 'mixdog';
9
- // Shared bound for every synchronous keychain backend (DPAPI/PowerShell on
9
+ // Shared bound for every synchronous keychain provider (DPAPI/PowerShell on
10
10
  // Windows, security(1) on macOS). A single env override keeps them consistent.
11
11
  const KEYCHAIN_TIMEOUT_MS = Number(process.env.MIXDOG_KEYCHAIN_TIMEOUT_MS || 15000);
12
12
  const POWERSHELL_TIMEOUT_MS = KEYCHAIN_TIMEOUT_MS;
@@ -102,8 +102,7 @@ const CANONICAL_PLUGIN_LOG_NAMES = new Set([
102
102
  'webhook.log',
103
103
  'perf.log',
104
104
  'tool-events.log',
105
- 'memory-runtime-proxy.log',
106
- 'channels-worker-standalone.log',
105
+ 'daemon.log',
107
106
  'mixdog-tui.stderr.log',
108
107
  ]);
109
108
 
@@ -183,7 +182,7 @@ function rotateBoundedLog(filePath, maxBytes, keepBytes) {
183
182
  }
184
183
 
185
184
  // Shared bound for unbounded per-writer plugin logs (tool-events,
186
- // memory-runtime-proxy, channels-worker-standalone). Keep a tail so recent
185
+ // memory runtime and daemon logs). Keep a tail so recent
187
186
  // context survives rotation while the file stays under the 10 MB cap.
188
187
  const PLUGIN_LOG_MAX_BYTES = 10 * 1024 * 1024;
189
188
  const PLUGIN_LOG_KEEP_BYTES = 2 * 1024 * 1024;
@@ -7,12 +7,11 @@
7
7
  - Destructive/hard-to-reverse action needs explicit confirmation and explicit
8
8
  validated target paths — never `~`, a root, or unresolved variables/globs;
9
9
  report material deletions with recoverability.
10
- - Act proactively; ask only for decisions.
10
+ - Ask only for decisions.
11
11
  - Build only what the task requires; trust internal and framework guarantees.
12
12
  - Mid-task input: a replacement supersedes current work, an addition folds
13
13
  into it, a status question gets a brief answer while work continues; after
14
14
  context compaction continue from the summary — never restart or redo
15
15
  finished work.
16
- - When blocked, exhaust safe in-scope checks once and report the blocker.
17
16
  - Your final message ends the turn: answer only when the work is done. After a
18
17
  failed tool call, fix and re-run it, or state plainly that it is unresolved.
@@ -1,6 +1,3 @@
1
1
  # Lead Tools
2
2
 
3
- - Write-role agents self-verify with `shell`. Lead uses `shell` only for git,
4
- benches, and cross-scope verification no retrieval tool can produce;
5
- inspection stays on `read`/`grep`/`glob`/`list`/`find`/`code_graph`.
6
3
  - Use the current project/workspace unless the request or tool requires another.
@@ -7,16 +7,17 @@
7
7
  `read`; symbol/relation→`code_graph`; web/current→`search`; returned URL body→
8
8
  `web_fetch`; prior work→`recall`; durable compact English memory→`memory`;
9
9
  explicit project change→`cwd`; explicit user-requested conversation reset→
10
- `session_manage`; process/env, git, build/run/test→`shell`. Never use shell
11
- equivalents for file discovery or content retrieval.
12
- - Use verified paths (cwd, project root, user-provided, or tool-returned);
13
- guessed fragments use `find`. Merge independent calls per tool in one
14
- message and fetch all information needed in that batch. Follow up only after
15
- zero/error or a newly revealed dependency; never re-fetch an unchanged span.
16
- - Once every final edit is fully determined, send one assistant tool batch
17
- containing one `apply_patch` for all files/hunks and one `shell` chain for
18
- verification. The runtime supports this mixed batch. On failure fix and
19
- rerun only what failed; report verified versus assumed.
10
+ `session_manage`; process/env, git, build/run/test→`shell`. Call `shell` only
11
+ when the task actually requires one of those operations; do not treat it as
12
+ a routine investigation or completion step. Never use shell equivalents for
13
+ file discovery or content retrieval.
14
+ - Use verified paths (cwd/project/user/tool); explicit paths may be outside cwd;
15
+ guessed fragments use `find`. Maximize fan-out across routed retrieval tools
16
+ and fetch all information needed in one batch; never re-fetch an unchanged
17
+ span.
18
+ - Once the edit is determined, finish in one assistant turn with one
19
+ `apply_patch` for all edits. If final verification actually requires `shell`,
20
+ use one verification chain after the edit; otherwise finish without it.
20
21
  - After a call returns a background `task_id`, end the turn; its completion
21
22
  notification resumes work. Never poll; use task control only for recovery or
22
23
  a required blocking result.
@@ -9,7 +9,8 @@ import { isDiagnosticIOEnabled } from '../../../lib/mixdog-debug.cjs';
9
9
  const WARNED_KEYS = new Set();
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
- // In-memory buffer + HTTP flush to memory-service /admin/trace-record
12
+ // In-memory buffer. Product sessions flush directly into the daemon-hosted
13
+ // memory module; the standalone path can still use its local HTTP service.
13
14
  // ---------------------------------------------------------------------------
14
15
  let _buffer = [];
15
16
  const _BUFFER_MAX = 2000;
@@ -225,21 +226,11 @@ try {
225
226
  function _resolveServiceUrl() {
226
227
  if (_serviceUrl) return _serviceUrl;
227
228
  try {
228
- // Prefer the single-writer discovery advert (discovery/memory.json),
229
- // pid-validated; fall back to legacy active-instance.json memory_port.
229
+ // Standalone memory HTTP uses its pid-validated discovery advert.
230
230
  const advertPort = readServicePort('memory', { requirePid: false });
231
231
  if (advertPort) { _serviceAdvertPort = advertPort; _serviceUrl = `http://127.0.0.1:${advertPort}`; return _serviceUrl; }
232
232
  _serviceAdvertPort = null;
233
- const runtimeRoot = process.env.MIXDOG_RUNTIME_ROOT
234
- ? join(process.env.MIXDOG_RUNTIME_ROOT)
235
- : join(os.tmpdir(), 'mixdog');
236
- const activeFile = join(runtimeRoot, 'active-instance.json');
237
- if (!existsSync(activeFile)) return null;
238
- const active = JSON.parse(readFileSync(activeFile, 'utf-8'));
239
- const port = Number(active && active.memory_port);
240
- if (!Number.isFinite(port) || port <= 0) return null;
241
- _serviceUrl = `http://127.0.0.1:${port}`;
242
- return _serviceUrl;
233
+ return null;
243
234
  } catch {
244
235
  return null;
245
236
  }
@@ -9,6 +9,8 @@ import { readServicePort, markServiceUnreachable, isConnRefuseError } from '../.
9
9
  import { makeToolEnvelope, normalizeToolEnvelope } from '../session/tool-envelope.mjs';
10
10
  import { classifyResultKind } from '../session/result-classification.mjs';
11
11
  import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
12
+ import { createOwnerFairGate } from '../../../shared/owner-fair-gate.mjs';
13
+ import { currentToolExecutionOwner } from '../../../shared/tool-execution-owner.mjs';
12
14
  // --- Types ---
13
15
  /** Known auto-detect targets: port file path relative to tmpdir.
14
16
  * Note: `mixdog` used to self-loopback via active-instance.json's
@@ -17,7 +19,7 @@ import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
17
19
  * in-process through agent's toolExecutor (see orchestrator/internal-tools),
18
20
  * so this registry is for genuinely external port-based MCP targets only. */
19
21
  const AUTO_DETECT_PORTS = {
20
- 'mixdog-memory': { discovery: 'memory', dir: 'mixdog', file: 'active-instance.json', portField: 'memory_port', endpoint: '/mcp' },
22
+ 'mixdog-memory': { discovery: 'memory', endpoint: '/mcp' },
21
23
  };
22
24
  const DEFAULT_MCP_CALL_TIMEOUT_MS = 120000;
23
25
  // Per-server STARTUP handshake budget (connect + listTools): 10s.
@@ -25,6 +27,7 @@ const DEFAULT_MCP_STARTUP_TIMEOUT_MS = 10000;
25
27
  // --- State ---
26
28
  const servers = new Map();
27
29
  const reconnects = createKeyedSingleflight();
30
+ const callAdmissions = new Map();
28
31
  let mcpSdkPromise = null;
29
32
  // Memo for mcpToolHasField(name, field) — keyed by `${toolName}|${field}`.
30
33
  // The lookup (regex parse + servers Map get + tools.find + schema property
@@ -165,6 +168,45 @@ export function getMcpServerStatus() {
165
168
  })(),
166
169
  }));
167
170
  }
171
+ function positiveInt(value, fallback) {
172
+ const parsed = Math.floor(Number(value));
173
+ return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
174
+ }
175
+ function callAdmissionFor(server) {
176
+ let gate = callAdmissions.get(server.name);
177
+ if (gate) return gate;
178
+ const cfg = server?.cfg || {};
179
+ gate = createOwnerFairGate({
180
+ name: `MCP ${server.name}`,
181
+ activeMax: positiveInt(
182
+ cfg.maxConcurrency ?? cfg.max_concurrency ?? process.env.MIXDOG_MCP_MAX_INFLIGHT,
183
+ 8,
184
+ ),
185
+ queueMax: positiveInt(
186
+ cfg.maxQueue ?? cfg.max_queue ?? process.env.MIXDOG_MCP_MAX_QUEUE,
187
+ 256,
188
+ ),
189
+ minOwnerQueue: 8,
190
+ waitTimeoutMs: positiveInt(
191
+ cfg.waitTimeoutMs ?? cfg.wait_timeout_ms ?? process.env.MIXDOG_MCP_WAIT_TIMEOUT_MS,
192
+ 30_000,
193
+ ),
194
+ });
195
+ callAdmissions.set(server.name, gate);
196
+ return gate;
197
+ }
198
+ function closeCallAdmission(name, reason) {
199
+ const gate = callAdmissions.get(name);
200
+ if (!gate) return;
201
+ callAdmissions.delete(name);
202
+ gate.close(reason || `MCP ${name} disconnected`);
203
+ }
204
+ export function getMcpAdmissionSnapshot() {
205
+ return [...callAdmissions.entries()].map(([name, gate]) => ({
206
+ name,
207
+ ...gate.snapshot(),
208
+ }));
209
+ }
168
210
 
169
211
  /** Snapshot of MCP initialize `instructions` per connected server (handshake time). */
170
212
  export function getMcpServerInstructionsMap() {
@@ -199,7 +241,7 @@ async function reconnectMcpServer(serverName, failedServer) {
199
241
  * Execute an MCP tool call.
200
242
  * Name format: `mcp__{serverName}__{toolName}`
201
243
  */
202
- export async function executeMcpTool(name, args) {
244
+ export async function executeMcpTool(name, args, options = {}) {
203
245
  // Parse: mcp__{server}__{tool}
204
246
  const match = name.match(/^mcp__(.+?)__(.+)$/);
205
247
  if (!match)
@@ -208,35 +250,40 @@ export async function executeMcpTool(name, args) {
208
250
  const server = servers.get(serverName);
209
251
  if (!server)
210
252
  throw new Error(`MCP server "${serverName}" not connected`);
211
- let result;
212
- try {
213
- result = await _callToolWithTimeout(server, toolName, args);
214
- } catch (firstErr) {
215
- const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
216
- if (isMcpToolCallTimeoutError(firstErr)) {
217
- mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
218
- throw firstErr;
219
- }
220
- mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
221
- let retryServer;
253
+ const gate = callAdmissionFor(server);
254
+ return gate.run(options?.ownerKey || currentToolExecutionOwner(), async () => {
255
+ let result;
222
256
  try {
223
- retryServer = await reconnectMcpServer(serverName, server);
224
- } catch (reconnectErr) {
225
- const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
226
- throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
227
- }
228
- try {
229
- result = await _callToolWithTimeout(retryServer, toolName, args);
230
- } catch (retryErr) {
231
- const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
232
- throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
257
+ result = await _callToolWithTimeout(server, toolName, args);
258
+ } catch (firstErr) {
259
+ const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
260
+ if (isMcpToolCallTimeoutError(firstErr)) {
261
+ mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
262
+ throw firstErr;
263
+ }
264
+ mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
265
+ let retryServer;
266
+ try {
267
+ retryServer = await reconnectMcpServer(serverName, server);
268
+ } catch (reconnectErr) {
269
+ const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
270
+ throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
271
+ }
272
+ try {
273
+ result = await _callToolWithTimeout(retryServer, toolName, args);
274
+ } catch (retryErr) {
275
+ const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
276
+ throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
277
+ }
233
278
  }
234
- }
235
- const normalized = normalizeToolEnvelope(normalizeMcpToolResult(result));
236
- const text = capMcpOutput(normalized.result);
237
- return normalized.explicitSuccess
238
- ? makeToolEnvelope(text, [], { explicitSuccess: true })
239
- : text;
279
+ const normalized = normalizeToolEnvelope(normalizeMcpToolResult(result));
280
+ const text = capMcpOutput(normalized.result);
281
+ return normalized.explicitSuccess
282
+ ? makeToolEnvelope(text, [], { explicitSuccess: true })
283
+ : text;
284
+ }, {
285
+ signal: options?.signal || null,
286
+ });
240
287
  }
241
288
 
242
289
  // Preserve MCP failure metadata across the object→string boundary. The
@@ -425,6 +472,7 @@ export async function disconnectAll() {
425
472
  }
426
473
  catch { /* ignore */ }
427
474
  servers.delete(name);
475
+ closeCallAdmission(name, `MCP ${name} disconnected`);
428
476
  }
429
477
  _invalidateMcpToolFieldMemo();
430
478
  }
@@ -442,6 +490,7 @@ export async function disconnectMcpServer(name) {
442
490
  }
443
491
  catch { /* ignore */ }
444
492
  servers.delete(name);
493
+ closeCallAdmission(name, `MCP ${name} disconnected`);
445
494
  _invalidateMcpToolFieldMemo();
446
495
  return true;
447
496
  }
@@ -494,13 +543,11 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
494
543
  const spec = AUTO_DETECT_PORTS[cfg.autoDetect];
495
544
  if (!spec)
496
545
  throw new Error(`Unknown autoDetect target: "${cfg.autoDetect}"`);
497
- // Prefer the single-writer discovery advert (discovery/<service>.json),
498
- // pid-validated. Fall back to the legacy active-instance.json portField
499
- // when no live advert is present (cross-version compat).
546
+ // Read the pid-validated single-writer discovery advert.
500
547
  let port = spec.discovery ? readServicePort(spec.discovery, { requirePid: false }) : null;
501
548
  if (port && spec.discovery) _autoDetectAdvert = { service: spec.discovery, port };
502
549
  let portFile = null;
503
- if (!port) {
550
+ if (!port && spec.file) {
504
551
  portFile = spec.dir === 'mixdog' && process.env.MIXDOG_RUNTIME_ROOT
505
552
  ? join(process.env.MIXDOG_RUNTIME_ROOT, spec.file)
506
553
  : join(tmpdir(), spec.dir, spec.file);
@@ -525,6 +572,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
525
572
  port = parseInt(raw, 10);
526
573
  }
527
574
  }
575
+ if (!port) throw new Error(`autoDetect server "${name}": live service advert missing`);
528
576
  if (!Number.isFinite(port) || port < 1 || port > 65535) {
529
577
  throw new Error(`autoDetect server "${name}": invalid port value${portFile ? ` in ${portFile}` : ''}`);
530
578
  }
@@ -535,12 +535,9 @@ export async function _streamResponse({
535
535
  // only) trips a short, named terminal StreamStalledError instead of coasting
536
536
  // to the 300s inter-chunk cap / 30-min agent watchdog.
537
537
  let semanticIdleTimer = null;
538
- // WS gets its own shorter semantic-idle window. The shared SSE semantic
539
- // timeout is intentionally near the 300s stall warning to protect Anthropic
540
- // effort-mode "silent thinking"; using that value here lets a wedged WS
541
- // request race the TUI 300s watchdog. Keep websocket stalls provider-owned:
542
- // fail/retry/fallback here first, then let the TUI watchdog remain a final
543
- // safety net only.
538
+ // WS owns its semantic-idle window. The TUI backstop is deliberately much
539
+ // longer, so this timeout can fail/retry/fallback at the provider boundary
540
+ // without racing an outer turn abort (Claude Code/Codex parity).
544
541
  const semanticIdleMs = PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS;
545
542
  const semanticIdleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0;
546
543
  // First-meaningful-frame watchdog timer + one-shot latch. Armed alongside
@@ -35,7 +35,7 @@ let _initChain = Promise.resolve();
35
35
  let _inFlightPromise = null;
36
36
  let _inFlightSig = null;
37
37
  let _lastAppliedSig = null;
38
- // Provider instances are process-global inside the backend daemon. Catalog
38
+ // Provider instances are process-global inside the daemon. Catalog
39
39
  // readers use this revision to share one raw model snapshot while still
40
40
  // rebuilding their cheap route/config projection after auth/config changes.
41
41
  let _providerCatalogRevision = 0;
@@ -367,7 +367,7 @@ function warmupCatalogs() {
367
367
  }
368
368
  }
369
369
 
370
- // Force-refresh each provider's /models catalog ONCE per backend-daemon
370
+ // Force-refresh each provider's /models catalog once per daemon.
371
371
  // lifetime. Every session runtime joins this process-global promise and then
372
372
  // reads the same provider-instance caches. Unlike
373
373
  // warmupCatalogs (which calls listModels() and so respects the 24h provider
@@ -30,7 +30,7 @@ import {
30
30
  } from './loop/tool-classify.mjs';
31
31
  import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
32
32
  import { runRecallFastTrackCompact } from './loop/recall-fasttrack.mjs';
33
- import { executeTool, _scopedCacheOutcomeForCall } from './loop/tool-exec.mjs';
33
+ import { executeTool, _scopedCacheOutcomeForCall, resolveLiveToolCwd } from './loop/tool-exec.mjs';
34
34
 
35
35
  // classifyResultKind is imported from result-classification.mjs at the top of
36
36
  // this file; import it from there directly rather than via this module.
@@ -420,11 +420,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
420
420
  const name = String(call?.name || call?.toolName || call?.function?.name || '').toLowerCase();
421
421
  return name === 'sleep' || name.endsWith('/sleep') || name.endsWith('.sleep');
422
422
  };
423
- // Tool execution must use the session cwd even when the caller omitted the
424
- // legacy positional cwd argument. Agent workers always carry their cwd on
425
- // sessionRef; falling through to pwd()/process.cwd() resolves relatives
426
- // against the host/plugin root instead of the worker workspace.
427
- cwd = cwd || sessionRef?.cwd || undefined;
423
+ // sessionRef.cwd is the live SSOT. The legacy positional cwd is only the
424
+ // turn-start snapshot and becomes stale after an in-turn cwd tool call.
425
+ cwd = resolveLiveToolCwd(cwd, sessionRef);
428
426
  // Staged pre-cap warnings + one true hard stop. The ONLY count-based
429
427
  // forced termination is the hard cap at maxLoopIterations (default 200):
430
428
  // a genuine runaway guard. Before it, staged warnings fire at 50%/75%/90%
@@ -445,6 +443,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
445
443
  Math.floor(maxLoopIterations * 0.9),
446
444
  ];
447
445
  while (true) {
446
+ // A cwd tool call updates sessionRef in place. Refresh before building
447
+ // this iteration's eager dispatcher and cache keys so every following
448
+ // tool family, including apply_patch, observes the new write root.
449
+ cwd = resolveLiveToolCwd(cwd, sessionRef);
448
450
  const _iterT0 = Date.now();
449
451
  throwIfAborted();
450
452
  if (iterations >= maxLoopIterations) {
@@ -53,4 +53,4 @@ export {
53
53
  export {
54
54
  semanticCompactMessages,
55
55
  recallFastTrackCompactMessages,
56
- } from './compact/engine.mjs';
56
+ } from './compact/runner.mjs';