mixdog 0.9.133 → 0.9.134

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 (205) hide show
  1. package/LICENSES/editor-assets-NOTICE.txt +11 -0
  2. package/package.json +2 -2
  3. package/scripts/agent-lead-e2e-probe.mjs +160 -0
  4. package/scripts/agent-long-prompt-repro.mjs +82 -0
  5. package/scripts/patch-replay.mjs +87 -29
  6. package/scripts/patch-replay.test.mjs +94 -0
  7. package/src/defaults/skills/setup/SKILL.md +3 -3
  8. package/src/headless-exec.mjs +28 -7
  9. package/src/headless-exec.test.mjs +41 -3
  10. package/src/headless-role.mjs +1 -4
  11. package/src/lib/mixdog-debug.cjs +2 -8
  12. package/src/rules/agent/43-title-agent.md +3 -3
  13. package/src/rules/shared/01-tool.md +20 -24
  14. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +3 -3
  15. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +1 -1
  16. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -1
  17. package/src/runtime/agent/orchestrator/config.mjs +5 -38
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +37 -7
  19. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +52 -37
  20. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +47 -11
  21. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +179 -0
  22. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +828 -0
  23. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +193 -1020
  24. package/src/runtime/agent/orchestrator/providers/cursor.mjs +57 -10
  25. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +8 -5
  26. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +30 -7
  27. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +19 -3
  28. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +14 -1
  29. package/src/runtime/agent/orchestrator/providers/openai-codex-model.test.mjs +24 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +8 -5
  31. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +1 -1
  32. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +48 -0
  33. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +53 -6
  34. package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +60 -0
  35. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +8 -1
  36. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +66 -0
  37. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +54 -7
  38. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
  40. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +34 -60
  42. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +0 -3
  43. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +1 -0
  44. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +31 -4
  45. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +40 -0
  46. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/session/result-classification.mjs +15 -1
  48. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +56 -17
  49. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +69 -0
  50. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +34 -9
  51. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -3
  52. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +33 -6
  54. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +12 -9
  55. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
  56. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +24 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +139 -14
  58. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +9 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +87 -32
  60. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +10 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +4 -0
  62. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +34 -2
  63. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-chunk-merge.mjs +123 -0
  64. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-fixed-fallback.mjs +123 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +265 -0
  66. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +277 -0
  67. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +234 -0
  69. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.test.mjs +102 -0
  70. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +19 -7
  71. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +19 -13
  72. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/tools/builtin/read-office-files.mjs +131 -0
  74. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +10 -0
  75. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +511 -0
  76. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +891 -0
  77. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +2 -1783
  78. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +65 -0
  79. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +26 -1
  80. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +61 -2
  81. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -1
  82. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  83. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  85. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +186 -42
  86. package/src/runtime/agent/orchestrator/tools/patch/v4a-anchors.mjs +239 -0
  87. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +42 -534
  88. package/src/runtime/agent/orchestrator/tools/patch/v4a-windows.mjs +299 -0
  89. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  90. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +3 -2
  91. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -0
  92. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +39 -0
  93. package/src/runtime/memory/index.mjs +0 -1
  94. package/src/runtime/memory/lib/core-memory-store.mjs +38 -11
  95. package/src/runtime/memory/lib/cycle-scheduler.mjs +14 -55
  96. package/src/runtime/memory/lib/http-router.mjs +0 -4
  97. package/src/runtime/memory/lib/memory-action-handlers.mjs +14 -14
  98. package/src/runtime/memory/lib/memory-config-flags.mjs +2 -19
  99. package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +43 -0
  100. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -12
  101. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +231 -81
  102. package/src/runtime/memory/lib/memory-cycle2.mjs +15 -0
  103. package/src/runtime/memory/lib/memory-cycle3.mjs +33 -4
  104. package/src/runtime/memory/lib/pg/process.mjs +1 -1
  105. package/src/runtime/memory/lib/query-handlers.mjs +15 -262
  106. package/src/runtime/memory/lib/query-ranking.mjs +256 -0
  107. package/src/runtime/shared/agent-route-config.mjs +7 -53
  108. package/src/runtime/shared/background-tasks.mjs +6 -10
  109. package/src/runtime/shared/channel-notification-routing.mjs +2 -9
  110. package/src/runtime/shared/channel-notification-routing.test.mjs +25 -0
  111. package/src/runtime/shared/config.mjs +8 -42
  112. package/src/runtime/shared/notify-trace.mjs +30 -0
  113. package/src/runtime/shared/pristine-execution-contract.json +7 -1
  114. package/src/runtime/shared/pristine-execution.mjs +12 -2
  115. package/src/runtime/shared/remote-intent.mjs +47 -0
  116. package/src/runtime/shared/remote-intent.test.mjs +40 -0
  117. package/src/runtime/shared/sleep.mjs +5 -0
  118. package/src/runtime/shared/tool-card-model.mjs +20 -9
  119. package/src/runtime/shared/tool-surface.mjs +45 -2
  120. package/src/runtime/shared/tool-surface.test.mjs +79 -0
  121. package/src/session-runtime/config-helpers.mjs +17 -1
  122. package/src/session-runtime/lifecycle-api.mjs +29 -37
  123. package/src/session-runtime/model-capabilities.mjs +8 -11
  124. package/src/session-runtime/model-recency.mjs +2 -2
  125. package/src/session-runtime/model-route-api.mjs +2 -0
  126. package/src/session-runtime/model-settings-persist.test.mjs +4 -2
  127. package/src/session-runtime/notification-bus.mjs +163 -47
  128. package/src/session-runtime/notification-bus.test.mjs +148 -0
  129. package/src/session-runtime/provider-models.mjs +1 -0
  130. package/src/session-runtime/remote-transcript.mjs +5 -5
  131. package/src/session-runtime/runtime-core.mjs +114 -39
  132. package/src/session-runtime/session-lifecycle.mjs +56 -12
  133. package/src/session-runtime/session-lifecycle.test.mjs +102 -0
  134. package/src/session-runtime/session-title.mjs +15 -2
  135. package/src/session-runtime/session-title.test.mjs +36 -0
  136. package/src/session-runtime/session-turn-api.mjs +34 -3
  137. package/src/session-runtime/tool-catalog.mjs +8 -5
  138. package/src/session-runtime/tool-policy-surface.test.mjs +4 -4
  139. package/src/session-runtime/workflow-agents-api.mjs +5 -7
  140. package/src/session-runtime/workflow.mjs +4 -8
  141. package/src/standalone/agent-task-status.mjs +3 -3
  142. package/src/standalone/agent-tool/helpers.mjs +23 -8
  143. package/src/standalone/agent-tool/helpers.test.mjs +62 -0
  144. package/src/standalone/agent-tool/lead-worker-index.mjs +136 -3
  145. package/src/standalone/agent-tool/notify.mjs +17 -48
  146. package/src/standalone/agent-tool/shard-spread.mjs +40 -6
  147. package/src/standalone/agent-tool/shard-spread.test.mjs +53 -0
  148. package/src/standalone/agent-tool/spawn-flow.mjs +33 -12
  149. package/src/standalone/agent-tool/spawn-flow.test.mjs +63 -0
  150. package/src/standalone/agent-tool.mjs +46 -17
  151. package/src/standalone/channel-restart.test.mjs +36 -54
  152. package/src/standalone/channel-session-router.mjs +29 -0
  153. package/src/standalone/channel-session-router.test.mjs +37 -0
  154. package/src/standalone/channel-transport.mjs +113 -154
  155. package/src/standalone/daemon.mjs +65 -29
  156. package/src/standalone/memory-runtime-proxy.mjs +36 -5
  157. package/src/standalone/provider-admin.mjs +6 -0
  158. package/src/standalone/session-client.mjs +2 -2
  159. package/src/standalone/session-protocol.mjs +0 -2
  160. package/src/standalone/session-runtime-pool-health.test.mjs +40 -6
  161. package/src/standalone/session-runtime-pool.mjs +28 -1
  162. package/src/standalone/session-runtime-worker.mjs +14 -5
  163. package/src/standalone/session-service.mjs +72 -23
  164. package/src/standalone/session-transport.mjs +2 -1
  165. package/src/tui/App.jsx +1 -1
  166. package/src/tui/app/app-format.mjs +9 -6
  167. package/src/tui/app/message-selector.mjs +1 -1
  168. package/src/tui/app/model-options.mjs +1 -1
  169. package/src/tui/app/model-picker.mjs +74 -7
  170. package/src/tui/app/provider-setup-picker.mjs +12 -4
  171. package/src/tui/app/use-prompt-draft-flow.mjs +1 -1
  172. package/src/tui/app/use-prompt-handlers.mjs +2 -2
  173. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  174. package/src/tui/app/use-transcript-activity.mjs +25 -8
  175. package/src/tui/components/ContextPanel.jsx +18 -12
  176. package/src/tui/components/PromptInput.jsx +2 -2
  177. package/src/tui/components/prompt-input/edit-helpers.mjs +1 -1
  178. package/src/tui/components/prompt-input/escape-policy.mjs +2 -2
  179. package/src/tui/components/prompt-input/interrupt-policy.mjs +1 -1
  180. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  181. package/src/tui/dist/index.mjs +187 -90
  182. package/src/tui/paste-attachments.mjs +10 -17
  183. package/src/tui/paste-text-policy.mjs +27 -0
  184. package/src/tui/session/agent-job-feed.mjs +12 -0
  185. package/src/tui/session/completion-card-restore.test.mjs +93 -0
  186. package/src/tui/session/context-state.mjs +8 -2
  187. package/src/tui/session/live-share.mjs +4 -1
  188. package/src/tui/session/oauth-flows.mjs +55 -18
  189. package/src/tui/session/oauth-flows.test.mjs +134 -0
  190. package/src/tui/session/queue-helpers.mjs +7 -1
  191. package/src/tui/session/queue-helpers.test.mjs +37 -0
  192. package/src/tui/session/render-timing.mjs +0 -2
  193. package/src/tui/session/session-api-ext.mjs +60 -58
  194. package/src/tui/session/session-api.mjs +3 -0
  195. package/src/tui/session/session-flow.mjs +19 -6
  196. package/src/tui/session/tool-card-results.mjs +9 -5
  197. package/src/tui/session/tool-result-status.mjs +21 -6
  198. package/src/tui/session/tool-result-status.test.mjs +101 -0
  199. package/src/tui/session/tool-result-text.mjs +2 -2
  200. package/src/tui/session/turn.mjs +21 -7
  201. package/src/tui/session-local.mjs +69 -33
  202. package/src/tui/themes/teal.mjs +1 -1
  203. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +0 -333
  204. package/src/runtime/agent/orchestrator/tools/mutation-planner.mjs +0 -75
  205. package/src/standalone/agent-host-runtime.mjs +0 -315
@@ -0,0 +1,11 @@
1
+ Editor compatibility data and icon tables include material derived from:
2
+
3
+ Visual Studio Code
4
+ Copyright (c) 2015 - present Microsoft Corporation
5
+ https://github.com/microsoft/vscode
6
+ Licensed under the MIT License. See LICENSES/MIT.txt.
7
+
8
+ Seti UI
9
+ Copyright (c) 2014 Jesse Weed
10
+ https://github.com/jesseweed/seti-ui
11
+ Licensed under the MIT License. See LICENSES/MIT.txt.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.133",
3
+ "version": "0.9.134",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -67,7 +67,7 @@
67
67
  "test:release-critical": "npm run test:release-assets && npm run smoke:patch && npm run test:providers",
68
68
  "test:spec-advisory": "node scripts/advisory-spec-test.mjs && npm run test:spec-advisory --prefix apps/desktop",
69
69
  "test:session-transport": "node --test scripts/session-transport-test.mjs scripts/daemon-bootstrap-test.mjs",
70
- "test:session": "node --test scripts/runtime-turn-contract-test.mjs scripts/session-save-fault-store-test.mjs",
70
+ "test:session": "node --test scripts/runtime-turn-contract-test.mjs scripts/session-save-fault-store-test.mjs src/runtime/shared/tool-surface.test.mjs",
71
71
  "test:media": "node --test src/runtime/media/store.test.mjs src/runtime/media/renditions.test.mjs src/runtime/media/adapters/codex-image.test.mjs",
72
72
  "failures": "node scripts/tool-failures.mjs",
73
73
  "trace:llm": "node scripts/llm-trace-summary.mjs",
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ // Lead-delegation E2E probe (manual, real provider turns).
3
+ //
4
+ // Boots the REAL Lead session runtime (the TUI/desktop runtime, delegation
5
+ // enabled) and verifies the whole agent path from a model turn:
6
+ // Lead model turn -> agent tool spawn -> routing rules resolve the worker
7
+ // preset -> worker turn -> completion -> owner notification -> Lead reports
8
+ // the worker's answer -> session-scoped agent status surface.
9
+ //
10
+ // Run: node scripts/agent-lead-e2e-probe.mjs [provider model]
11
+ // Uses the real user config/credentials (copied read-only) in an isolated
12
+ // MIXDOG_DATA_DIR so the live daemon/app state is never touched.
13
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { homedir, tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+
17
+ const ROOT = mkdtempSync(join(tmpdir(), 'mixdog-lead-e2e-'));
18
+ // MIXDOG_LEAD_E2E_LIVE_DAEMON=1: keep the inherited runtime root (live daemon
19
+ // discovery) and opt into agent shard spread, so the spawned worker runs on
20
+ // the INSTALLED daemon's shard pool — the deployed remote-completion path.
21
+ const LIVE_DAEMON = process.env.MIXDOG_LEAD_E2E_LIVE_DAEMON === '1';
22
+ if (LIVE_DAEMON) process.env.MIXDOG_AGENT_SHARD_SPREAD = '1';
23
+ else process.env.MIXDOG_RUNTIME_ROOT = ROOT;
24
+ process.env.MIXDOG_BOOT_CORE_MEMORY = '0';
25
+ process.env.MIXDOG_DAEMON_SKIP_MEMORY = '1';
26
+ process.env.MIXDOG_FEATURE_MEMORY = '0';
27
+ process.env.MIXDOG_FEATURE_WEB_SEARCH = '0';
28
+ process.env.MIXDOG_AGENT_TRACE_DISABLE = '1';
29
+ const DATA_DIR = join(ROOT, 'data');
30
+ mkdirSync(DATA_DIR, { recursive: true });
31
+ const REAL_DATA_DIR = join(homedir(), '.mixdog', 'data');
32
+ for (const file of [
33
+ 'mixdog-config.json',
34
+ 'grok-oauth.json',
35
+ 'grok-oauth-models.json',
36
+ 'openai-oauth.json',
37
+ 'openai-oauth-models.json',
38
+ 'anthropic-oauth-credentials.json',
39
+ 'anthropic-oauth-models.json',
40
+ ]) {
41
+ const from = join(REAL_DATA_DIR, file);
42
+ if (existsSync(from)) copyFileSync(from, join(DATA_DIR, file));
43
+ }
44
+ // The user's active workflow may be Solo (delegatesAgents:false), which
45
+ // correctly removes the agent tool from the Lead surface. This probe verifies
46
+ // DELEGATION, so pin the isolated config to the delegating default workflow.
47
+ {
48
+ const configPath = join(DATA_DIR, 'mixdog-config.json');
49
+ if (existsSync(configPath)) {
50
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
51
+ // The config file nests runtime settings under the `agent` key.
52
+ const section = config.agent && typeof config.agent === 'object' ? config.agent : config;
53
+ section.workflow = { ...(section.workflow || {}), active: 'default' };
54
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
55
+ }
56
+ }
57
+ process.env.MIXDOG_DATA_DIR = DATA_DIR;
58
+
59
+ const PROVIDER = process.argv[2] || 'grok-oauth';
60
+ const MODEL = process.argv[3] || 'grok-4.3';
61
+ const WORKER_AGENT = process.env.MIXDOG_LEAD_E2E_AGENT || 'worker';
62
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
63
+
64
+ const failures = [];
65
+ function check(ok, label, detail = '') {
66
+ if (!ok) failures.push(label);
67
+ process.stdout.write(`${ok ? 'ok ' : 'FAIL'} - ${label}${detail ? ` (${detail})` : ''}\n`);
68
+ }
69
+
70
+ const { createMixdogSessionRuntime } = await import('../src/session-runtime/runtime-core.mjs');
71
+ let runtime = null;
72
+ try {
73
+ runtime = await createMixdogSessionRuntime({
74
+ provider: PROVIDER,
75
+ model: MODEL,
76
+ cwd: process.cwd(),
77
+ toolMode: 'full',
78
+ approvalMode: 'implicit',
79
+ disallowDelegation: false,
80
+ });
81
+ const notifications = [];
82
+ runtime.onNotification?.((event) => notifications.push(event));
83
+
84
+ const toolEvents = [];
85
+ const askOptions = {
86
+ onAssistantToolCallObserved: (call) => toolEvents.push({ kind: 'call', name: call?.name || call?.tool || '?' }),
87
+ onToolResult: (message) => toolEvents.push({
88
+ kind: 'result',
89
+ name: message?.name || message?.tool || '?',
90
+ preview: String(message?.content ?? message?.output ?? '').slice(0, 200),
91
+ }),
92
+ };
93
+ const ask1 = await runtime.ask(
94
+ 'Delegate one task via the agent tool. If `agent` is not directly callable, first call '
95
+ + `load_tool with names=['agent'] to activate it. Then call agent with exactly type=spawn, agent=${WORKER_AGENT} and prompt `
96
+ + "'Read package.json in the current repo and reply with exactly the value of its name field, one word, nothing else.' "
97
+ + 'Do NOT set provider/model/effort (routing decides). Do not use read/grep/shell yourself. '
98
+ + 'After the spawn tool call returns, reply with exactly: SPAWNED. '
99
+ + 'Only if activating AND calling the agent tool both fail, reply exactly: AGENT_UNAVAILABLE',
100
+ askOptions,
101
+ );
102
+ const spawnReply = String(ask1?.result?.content || '');
103
+ process.stdout.write(`turn1 tools: ${JSON.stringify(toolEvents)}\n`);
104
+ const agentToolUsed = toolEvents.some((event) => event.name === 'agent');
105
+ check(agentToolUsed, 'Lead model turn actually invoked the agent tool', spawnReply.slice(0, 120));
106
+
107
+ // Poll the SAME status surface the desktop top-right button consumes.
108
+ let sawWorkerRow = null;
109
+ let terminalJob = null;
110
+ let scope = null;
111
+ const deadline = Date.now() + 120_000;
112
+ let lastStatus = null;
113
+ while (Date.now() < deadline && !terminalJob) {
114
+ const status = runtime.agentStatus?.() || {};
115
+ lastStatus = status;
116
+ scope = status.agentScope || scope;
117
+ const workers = status.agentWorkers || [];
118
+ if (!sawWorkerRow && workers.length > 0) sawWorkerRow = workers[0];
119
+ terminalJob = (status.agentJobs || []).find(
120
+ (job) => /completed|failed|cancelled/i.test(String(job.status)),
121
+ ) || null;
122
+ if (!terminalJob) await sleep(300);
123
+ }
124
+ process.stdout.write(`final status surface: ${JSON.stringify({
125
+ workers: lastStatus?.agentWorkers || [],
126
+ jobs: lastStatus?.agentJobs || [],
127
+ }).slice(0, 1_500)}\n`);
128
+ check(Boolean(sawWorkerRow), 'worker visible on the session agent status surface',
129
+ sawWorkerRow ? `tag=${sawWorkerRow.tag} stage=${sawWorkerRow.stage}` : 'never appeared');
130
+ check(Boolean(terminalJob), 'agent job reached a terminal state');
131
+ check(terminalJob?.status === 'completed', 'agent job completed without error',
132
+ `status=${terminalJob?.status} error=${terminalJob?.error || 'none'}`);
133
+ check(Boolean(terminalJob?.provider && terminalJob?.model), 'routing rules resolved the worker route',
134
+ `agent=${terminalJob?.agent} route=${terminalJob?.provider}/${terminalJob?.model} preset=${terminalJob?.preset || '-'}`);
135
+ check(scope?.sessionId === runtime.id, 'agent status surface is scoped to the owner session',
136
+ `scope=${JSON.stringify(scope)} lead=${runtime.id}`);
137
+ await sleep(500);
138
+ check(notifications.length > 0, 'owner received the completion notification',
139
+ `count=${notifications.length}`);
140
+
141
+ // Mirror the surface's notification injection into the next Lead turn.
142
+ const notifText = notifications.map((event) => {
143
+ try { return typeof event === 'string' ? event : JSON.stringify(event); } catch { return String(event); }
144
+ }).join('\n').slice(0, 4_000);
145
+ const ask2 = await runtime.ask(
146
+ `Agent completion notification:\n${notifText}\n\n`
147
+ + 'Reply with exactly: WORKER_SAID=<the worker\'s one-word answer from the notification>',
148
+ {},
149
+ );
150
+ const finalReply = String(ask2?.result?.content || '');
151
+ check(/WORKER_SAID=\s*mixdog/i.test(finalReply), 'Lead surfaced the worker result',
152
+ finalReply.slice(0, 160));
153
+
154
+ process.stdout.write(`verdict: ${failures.length === 0 ? 'PASS' : `FAIL (${failures.join('; ')})`}\n`);
155
+ process.exitCode = failures.length === 0 ? 0 : 1;
156
+ } finally {
157
+ try { await runtime?.stop?.('lead-e2e-probe-exit'); } catch { /* teardown */ }
158
+ try { rmSync(ROOT, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); } catch { /* temp */ }
159
+ }
160
+ process.exit(process.exitCode || 0);
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // Repro: worker spawn with a LONG prompt (> 800B attachment-externalization
3
+ // threshold) through the shard-spread daemon path. Isolated daemon/root.
4
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
5
+ import { homedir, tmpdir } from 'node:os';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const ROOT = mkdtempSync(join(tmpdir(), 'mixdog-longprompt-'));
10
+ process.env.MIXDOG_RUNTIME_ROOT = ROOT;
11
+ process.env.MIXDOG_DAEMON_SKIP_MEMORY = '1';
12
+ process.env.MIXDOG_BOOT_CORE_MEMORY = '0';
13
+ process.env.MIXDOG_AGENT_TRACE_DISABLE = '1';
14
+ process.env.MIXDOG_AGENT_SHARD_SPREAD = '1';
15
+ const DATA_DIR = join(ROOT, 'data');
16
+ mkdirSync(DATA_DIR, { recursive: true });
17
+ const REAL = join(homedir(), '.mixdog', 'data');
18
+ for (const file of ['mixdog-config.json', 'grok-oauth.json', 'grok-oauth-models.json']) {
19
+ const from = join(REAL, file);
20
+ if (existsSync(from)) copyFileSync(from, join(DATA_DIR, file));
21
+ }
22
+ process.env.MIXDOG_DATA_DIR = DATA_DIR;
23
+
24
+ const cfgMod = await import('../src/runtime/agent/orchestrator/config.mjs');
25
+ const reg = await import('../src/runtime/agent/orchestrator/providers/registry.mjs');
26
+ const mgr = await import('../src/runtime/agent/orchestrator/session/manager.mjs');
27
+ const { createStandaloneAgent } = await import('../src/standalone/agent-tool.mjs');
28
+ const { ensureDaemon, shutdownDaemon } = await import('../src/standalone/session-client.mjs');
29
+
30
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
31
+ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..');
32
+ let agent = null;
33
+ let daemonDiscovery = null;
34
+ try {
35
+ daemonDiscovery = await ensureDaemon({ cwd: REPO, log: () => {} });
36
+ agent = createStandaloneAgent({ cfgMod, reg, mgr, dataDir: DATA_DIR, cwd: REPO });
37
+ // Padding pushes the prompt over TEXT_REFERENCE_THRESHOLD_BYTES (800).
38
+ const padding = 'Background context (ignore, filler): ' + 'lorem ipsum dolor sit amet. '.repeat(40);
39
+ const prompt = `${padding}\nActual task: read package.json in the current repo and reply with exactly the value of its "name" field, one word, nothing else.`;
40
+ process.stdout.write(`prompt bytes: ${Buffer.byteLength(prompt, 'utf8')}\n`);
41
+ const out = await agent.execute({
42
+ type: 'spawn', agent: 'worker', provider: 'grok-oauth', model: 'grok-4.3', effort: 'low',
43
+ tag: 'longprompt-w0', cwd: REPO, prompt,
44
+ }, { invocationSource: 'model-tool', cwd: REPO });
45
+ const taskId = String(out).match(/agent task: (\S+)/)?.[1];
46
+ if (!taskId) throw new Error(`spawn did not return a task id: ${String(out).slice(0, 300)}`);
47
+ process.stdout.write(`spawned: ${taskId}\n`);
48
+ let last = '';
49
+ const deadline = Date.now() + 120_000;
50
+ while (Date.now() < deadline) {
51
+ last = await agent.execute({ type: 'read', task_id: taskId }, { invocationSource: 'model-tool', cwd: REPO });
52
+ if (/status: (completed|failed|error|cancelled)/.test(last)) break;
53
+ await sleep(500);
54
+ }
55
+ process.stdout.write(`${last.slice(0, 900)}\n`);
56
+ const ok = /status: completed/.test(last) && /mixdog/i.test(last);
57
+ process.stdout.write(`verdict: ${ok ? 'PASS' : 'FAIL'}\n`);
58
+ if (!ok) {
59
+ // Autopsy: shard-side turn timing + persisted worker session shape.
60
+ const { readFileSync, readdirSync } = await import('node:fs');
61
+ try {
62
+ const log = readFileSync(join(DATA_DIR, 'daemon.log'), 'utf8').trim().split(/\r?\n/);
63
+ for (const line of log.slice(-30)) process.stdout.write(`[daemon.log] ${line}\n`);
64
+ } catch (error) { process.stdout.write(`daemon.log read failed: ${error?.message}\n`); }
65
+ try {
66
+ const dir = join(DATA_DIR, 'sessions');
67
+ for (const file of readdirSync(dir)) {
68
+ if (!file.startsWith('sess_daemon')) continue;
69
+ const x = JSON.parse(readFileSync(join(dir, file), 'utf8'));
70
+ const s = x.session || x;
71
+ process.stdout.write(`[worker-session] ${file} msgs=${(s.messages || []).length} roles=${(s.messages || []).map((m) => m.role).join(',')}\n`);
72
+ }
73
+ } catch (error) { process.stdout.write(`session dump failed: ${error?.message}\n`); }
74
+ }
75
+ process.exitCode = ok ? 0 : 1;
76
+ } finally {
77
+ try { agent?.closeAll('longprompt-end'); } catch {}
78
+ try { await shutdownDaemon(daemonDiscovery); } catch {}
79
+ await sleep(300);
80
+ try { rmSync(ROOT, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); } catch {}
81
+ }
82
+ process.exit(process.exitCode || 0);
@@ -8,7 +8,8 @@
8
8
  // node scripts/patch-replay.mjs --replay-all [--json]
9
9
  import { existsSync, readFileSync, readdirSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
10
10
  import { homedir, tmpdir } from 'node:os';
11
- import { dirname, join, resolve } from 'node:path';
11
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
12
13
  import { executePatchTool } from '../src/runtime/agent/orchestrator/tools/patch.mjs';
13
14
 
14
15
  function argValue(name, fallback = null) {
@@ -37,12 +38,53 @@ function loadRecords() {
37
38
 
38
39
  function isErr(text) { return /^Error[\s:[]/.test(String(text || '').trimStart()); }
39
40
 
40
- async function replayOne(rec) {
41
+ function resolveReplaySnapshotPath(root, rel) {
42
+ const text = String(rel || '');
43
+ const portableAbsolute = /^[A-Za-z]:[\\/]/.test(text) || /^[/\\]{2}/.test(text);
44
+ if (!text || text.includes('\0') || isAbsolute(text) || portableAbsolute
45
+ || text.split(/[\\/]+/).includes('..')) {
46
+ throw new Error(`unsafe snapshot path: ${text || '(empty)'}`);
47
+ }
48
+ const abs = resolve(root, text);
49
+ const fromRoot = relative(root, abs);
50
+ if (!fromRoot || fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
51
+ throw new Error(`unsafe snapshot path: ${text}`);
52
+ }
53
+ return abs;
54
+ }
55
+
56
+ export function legacyPartialReplayReason(rec) {
57
+ const partial = rec?.outcome?.kind === 'partial'
58
+ || /apply_patch file-level partial/i.test(String(rec?.error_first_line || ''));
59
+ if (partial && rec?.snapshot_phase !== 'pre') {
60
+ return 'legacy partial capture has post-mutation snapshots; replay would not reproduce the original pre-state';
61
+ }
62
+ return null;
63
+ }
64
+
65
+ export async function replayOne(rec) {
66
+ const skipReason = legacyPartialReplayReason(rec);
67
+ if (skipReason) {
68
+ return { id: rec.id, ok: false, skipped: true, skipReason, before: rec.error_first_line, after: null };
69
+ }
41
70
  const tmp = mkdtempSync(join(tmpdir(), 'mixdog-patch-replay-'));
71
+ const previousCapture = process.env.MIXDOG_PATCH_REPLAY_CAPTURE;
72
+ process.env.MIXDOG_PATCH_REPLAY_CAPTURE = '0';
42
73
  try {
43
74
  for (const [rel, content] of Object.entries(rec.file_snapshots || {})) {
44
75
  if (content == null) continue;
45
- const abs = join(tmp, rel);
76
+ let abs;
77
+ try {
78
+ abs = resolveReplaySnapshotPath(tmp, rel);
79
+ } catch (error) {
80
+ return {
81
+ id: rec.id,
82
+ ok: false,
83
+ skipped: false,
84
+ before: rec.error_first_line,
85
+ after: `Error: ${error?.message || String(error)}`,
86
+ };
87
+ }
46
88
  mkdirSync(dirname(abs), { recursive: true });
47
89
  writeFileSync(abs, content);
48
90
  }
@@ -50,41 +92,57 @@ async function replayOne(rec) {
50
92
  let result;
51
93
  try { result = await executePatchTool('apply_patch', args, tmp, {}); }
52
94
  catch (e) { result = `Error: ${e?.message || String(e)}`; }
53
- return { id: rec.id, ok: !isErr(result), before: rec.error_first_line, after: String(result).split('\n')[0].slice(0, 200) };
95
+ return { id: rec.id, ok: !isErr(result), skipped: false, before: rec.error_first_line, after: String(result).split('\n')[0].slice(0, 200) };
54
96
  } finally {
97
+ if (previousCapture === undefined) delete process.env.MIXDOG_PATCH_REPLAY_CAPTURE;
98
+ else process.env.MIXDOG_PATCH_REPLAY_CAPTURE = previousCapture;
55
99
  try { rmSync(tmp, { recursive: true, force: true }); } catch {}
56
100
  }
57
101
  }
58
102
 
59
- const jsonMode = hasFlag('--json');
60
- const records = loadRecords();
103
+ async function main() {
104
+ const jsonMode = hasFlag('--json');
105
+ const records = loadRecords();
61
106
 
62
- if (hasFlag('--list') || (!hasFlag('--replay-all') && !argValue('--replay'))) {
63
- if (jsonMode) { console.log(JSON.stringify(records.map(({ file_snapshots, args, ...m }) => m), null, 2)); process.exit(0); }
64
- console.log(`captured apply_patch failures: ${records.length} (dir: ${replayDir()})`);
65
- for (const r of records.slice(0, 50)) {
66
- console.log(`- ${r.id} targets=${(r.targets || []).length} ${new Date(r.ts).toISOString()}`);
67
- console.log(` ${String(r.error_first_line || '').slice(0, 140)}`);
107
+ if (hasFlag('--list') || (!hasFlag('--replay-all') && !argValue('--replay'))) {
108
+ if (jsonMode) { console.log(JSON.stringify(records.map(({ file_snapshots, args, ...m }) => m), null, 2)); return; }
109
+ console.log(`captured apply_patch failures: ${records.length} (dir: ${replayDir()})`);
110
+ for (const r of records.slice(0, 50)) {
111
+ const phase = r.snapshot_phase || 'legacy';
112
+ console.log(`- ${r.id} targets=${(r.targets || []).length} phase=${phase} ${new Date(r.ts).toISOString()}`);
113
+ console.log(` ${String(r.error_first_line || '').slice(0, 140)}`);
114
+ }
115
+ if (!records.length) console.log('(none - set MIXDOG_PATCH_REPLAY_CAPTURE=1 to capture)');
116
+ return;
68
117
  }
69
- if (!records.length) console.log('(none - set MIXDOG_PATCH_REPLAY_CAPTURE=1 to capture)');
70
- process.exit(0);
71
- }
72
118
 
73
- const one = argValue('--replay', null);
74
- const targets = one ? records.filter((r) => r.id === one || r.id.startsWith(one)) : records;
75
- if (!targets.length) { console.error(one ? `no replay matched: ${one}` : 'no captured failures'); process.exit(1); }
119
+ const one = argValue('--replay', null);
120
+ const targets = one ? records.filter((r) => r.id === one || r.id.startsWith(one)) : records;
121
+ if (!targets.length) {
122
+ console.error(one ? `no replay matched: ${one}` : 'no captured failures');
123
+ process.exitCode = 1;
124
+ return;
125
+ }
76
126
 
77
- const results = [];
78
- for (const rec of targets) results.push(await replayOne(rec));
79
- const passed = results.filter((r) => r.ok).length;
127
+ const results = [];
128
+ for (const rec of targets) results.push(await replayOne(rec));
129
+ const passed = results.filter((r) => r.ok).length;
130
+ const skipped = results.filter((r) => r.skipped).length;
131
+ const failed = results.length - passed - skipped;
80
132
 
81
- if (jsonMode) {
82
- console.log(JSON.stringify({ total: results.length, passed, failed: results.length - passed, results }, null, 2));
83
- } else {
84
- console.log(`patch-replay: ${passed}/${results.length} now succeed`);
85
- for (const r of results) {
86
- console.log(`- ${r.id}: ${r.ok ? 'PASS' : 'still fails'}`);
87
- if (!r.ok) console.log(` after: ${r.after}`);
133
+ if (jsonMode) {
134
+ console.log(JSON.stringify({ total: results.length, passed, failed, skipped, results }, null, 2));
135
+ } else {
136
+ console.log(`patch-replay: ${passed} passed · ${failed} still fail · ${skipped} legacy-skipped`);
137
+ for (const r of results) {
138
+ console.log(`- ${r.id}: ${r.skipped ? 'legacy-skip' : r.ok ? 'PASS' : 'still fails'}`);
139
+ if (r.skipped) console.log(` skip: ${r.skipReason}`);
140
+ else if (!r.ok) console.log(` after: ${r.after}`);
141
+ }
88
142
  }
143
+ process.exitCode = failed > 0 ? 1 : 0;
89
144
  }
90
- process.exitCode = passed === results.length ? 0 : 1;
145
+
146
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
147
+ await main();
148
+ }
@@ -0,0 +1,94 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { executePatchTool } from '../src/runtime/agent/orchestrator/tools/patch.mjs';
8
+ import { legacyPartialReplayReason, replayOne } from './patch-replay.mjs';
9
+
10
+ test('partial patch replay captures the pre-mutation state and never recaptures replay failures', async () => {
11
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-patch-capture-'));
12
+ const replayDir = join(root, 'replays');
13
+ mkdirSync(replayDir);
14
+ writeFileSync(join(root, 'good.txt'), 'one\n');
15
+ writeFileSync(join(root, 'stale.txt'), 'actual\n');
16
+ const previousDir = process.env.MIXDOG_PATCH_REPLAY_DIR;
17
+ const previousCapture = process.env.MIXDOG_PATCH_REPLAY_CAPTURE;
18
+ process.env.MIXDOG_PATCH_REPLAY_DIR = replayDir;
19
+ process.env.MIXDOG_PATCH_REPLAY_CAPTURE = '1';
20
+ try {
21
+ const result = await executePatchTool('apply_patch', {
22
+ patch: [
23
+ '*** Begin Patch',
24
+ '*** Update File: good.txt',
25
+ '@@',
26
+ '-one',
27
+ '+two',
28
+ '*** Update File: stale.txt',
29
+ '@@',
30
+ '-expected',
31
+ '+next',
32
+ '*** End Patch',
33
+ '',
34
+ ].join('\n'),
35
+ }, root, { sessionId: 'session-test', toolCallId: 'call-test' });
36
+ assert.match(result, /^Error: apply_patch file-level partial: 1\/2/m);
37
+ const captures = readdirSync(replayDir).filter((file) => file.endsWith('.json'));
38
+ assert.equal(captures.length, 1);
39
+ const record = JSON.parse(readFileSync(join(replayDir, captures[0]), 'utf8'));
40
+ assert.equal(record.snapshot_phase, 'pre');
41
+ assert.equal(record.file_snapshots['good.txt'], 'one\n');
42
+ assert.equal(record.file_snapshots['stale.txt'], 'actual\n');
43
+ assert.deepEqual(record.outcome, {
44
+ kind: 'partial',
45
+ applied: 1,
46
+ total: 2,
47
+ rejected: 1,
48
+ rejectedTargets: ['stale.txt'],
49
+ });
50
+ assert.equal(record.session_id, 'session-test');
51
+ assert.equal(record.tool_call_id, 'call-test');
52
+ assert.match(record.error_text, /expected first old line/);
53
+
54
+ const replayed = await replayOne(record);
55
+ assert.equal(replayed.skipped, false);
56
+ assert.equal(replayed.ok, false);
57
+ assert.equal(readdirSync(replayDir).filter((file) => file.endsWith('.json')).length, 1);
58
+ } finally {
59
+ if (previousDir === undefined) delete process.env.MIXDOG_PATCH_REPLAY_DIR;
60
+ else process.env.MIXDOG_PATCH_REPLAY_DIR = previousDir;
61
+ if (previousCapture === undefined) delete process.env.MIXDOG_PATCH_REPLAY_CAPTURE;
62
+ else process.env.MIXDOG_PATCH_REPLAY_CAPTURE = previousCapture;
63
+ rmSync(root, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ test('legacy partial captures are skipped instead of producing misleading replay failures', () => {
68
+ assert.match(legacyPartialReplayReason({
69
+ error_first_line: 'Error: apply_patch file-level partial: 1/2 file(s) applied to disk (committed); 1 file(s) rejected',
70
+ }), /post-mutation snapshots/);
71
+ assert.equal(legacyPartialReplayReason({
72
+ snapshot_phase: 'pre',
73
+ outcome: { kind: 'partial' },
74
+ }), null);
75
+ });
76
+
77
+ test('replay snapshots cannot write outside the throwaway replay root', async () => {
78
+ const escaped = join(tmpdir(), `mixdog-patch-replay-escape-${process.pid}-${Date.now()}.txt`);
79
+ try {
80
+ const result = await replayOne({
81
+ id: 'unsafe-snapshot',
82
+ args: { patch: '*** Begin Patch\n*** End Patch\n' },
83
+ file_snapshots: {
84
+ [`../${escaped.split(/[\\/]/).at(-1)}`]: 'must not be written',
85
+ },
86
+ });
87
+ assert.equal(result.ok, false);
88
+ assert.equal(result.skipped, false);
89
+ assert.match(result.after, /unsafe snapshot path/);
90
+ assert.equal(existsSync(escaped), false);
91
+ } finally {
92
+ rmSync(escaped, { force: true });
93
+ }
94
+ });
@@ -432,15 +432,15 @@ Desktop과 현재 runtime에서 소비되지 않는 아래 key는 사용자 옵
432
432
  | 레거시 key | 처리 |
433
433
  |---|---|
434
434
  | `agent.workflowRoutes` | `agent.agents`로 이관된 뒤 제거. 수동 편집 금지 |
435
- | `agent.fastModels` | explicit `modelSettings.<provider/model>.fast`가 없을 때만 `true`를 이관한 뒤 제거 |
435
+ | `agent.fastModels` | 제거 (이관 없음; `modelSettings.<provider/model>.fast`만 유효) |
436
436
  | `agent.agentMaintenance`, `agent.runtime` | 제거 |
437
437
  | `remote.autoStart` | 제거; Remote는 session header에서 수동 claim |
438
438
  | `ui.mouseMode` | 제거; `ui.theme`은 TUI 현행 설정 |
439
- | `channels.backend` | `channels.provider`로 이관 후 제거 |
439
+ | `channels.backend` | 제거 (이관 없음; `channels.provider`만 유효) |
440
440
  | `channels.channel.channelId` | 제거; `discordChannelId` / `telegramChatId`만 유지 |
441
441
  | `channels.quiet`, `channels.schedules` | 제거; schedule은 PG가 단일 저장소 |
442
442
  | `channels.webhook.ngrokDomain`, `channels.webhook.respectQuiet` | 제거; endpoint URL은 Desktop Webhooks가 발급 |
443
- | `agent.outputStyle` | 루트 `outputStyle`로 이관 후 제거 |
443
+ | `agent.outputStyle` | 제거 (이관 없음; 루트 `outputStyle`만 유효) |
444
444
 
445
445
  다음 항목은 레거시처럼 보여도 현행이므로 유지한다: 루트 `outputStyle`, `agent.shell`, `agent.modules`, `channels.channel.discordChannelId/telegramChatId`, compaction의 `type/compactType`과 recall tuning fields.
446
446
 
@@ -13,16 +13,14 @@ import {
13
13
  } from './runtime/shared/pristine-execution.mjs';
14
14
  import { hasActiveBackgroundTasks } from './runtime/shared/background-tasks.mjs';
15
15
  import { installProcessSignalCleanup } from './runtime/shared/process-shutdown.mjs';
16
+ import { sleep } from './runtime/shared/sleep.mjs';
17
+ import { stopStandaloneMemoryRuntimesForProcess } from './standalone/memory-runtime-proxy.mjs';
16
18
  import { applyUsageDelta, createSessionStats } from './ui/session-stats.mjs';
17
19
 
18
20
  function clean(value) {
19
21
  return String(value ?? '').trim();
20
22
  }
21
23
 
22
- function sleep(ms) {
23
- return new Promise((resolve) => setTimeout(resolve, ms));
24
- }
25
-
26
24
  function nonNegativeNumber(value) {
27
25
  const number = Number(value);
28
26
  return Number.isFinite(number) ? Math.max(0, number) : 0;
@@ -553,6 +551,7 @@ export async function runHeadlessExec({
553
551
  usageLogPath = process.env.MIXDOG_USAGE_LOG,
554
552
  boundaryFactory = createPristineExecutionBoundary,
555
553
  runtimeFactory = null,
554
+ memoryRuntimeCleanup = stopStandaloneMemoryRuntimesForProcess,
556
555
  hasActiveTasks = hasActiveBackgroundTasks,
557
556
  installSignalCleanupFn = installProcessSignalCleanup,
558
557
  idlePollMs = 100,
@@ -591,11 +590,33 @@ export async function runHeadlessExec({
591
590
  let code = 1;
592
591
  const cleanup = (reason = 'exec-exit') => {
593
592
  cleanupPromise ??= (async () => {
593
+ const errors = [];
594
594
  try {
595
595
  if (runtime) await runtime.close(reason);
596
- } finally {
597
- boundary?.cleanup();
596
+ } catch (error) {
597
+ errors.push(error);
598
+ }
599
+ let memoryCleanupFailed = false;
600
+ if (boundary) {
601
+ try {
602
+ await memoryRuntimeCleanup({ waitForExit: true, timeoutMs: 10_000 });
603
+ } catch (error) {
604
+ memoryCleanupFailed = true;
605
+ errors.push(error);
606
+ }
607
+ }
608
+ try {
609
+ const cleanupResult = boundary?.cleanup(memoryCleanupFailed
610
+ ? { preserveRoot: true }
611
+ : { tolerateRootRemovalFailure: true });
612
+ if (cleanupResult?.rootRemovalError) {
613
+ writeErr(`mixdog: shutdown cleanup failed (result unaffected): ${cleanupResult.rootRemovalError?.message || cleanupResult.rootRemovalError}\n`);
614
+ }
615
+ } catch (error) {
616
+ errors.push(error);
598
617
  }
618
+ if (errors.length === 1) throw errors[0];
619
+ if (errors.length > 1) throw new AggregateError(errors, 'headless shutdown failed');
599
620
  })();
600
621
  return cleanupPromise;
601
622
  };
@@ -604,7 +625,7 @@ export async function runHeadlessExec({
604
625
  boundary = boundaryFactory({ provider, model, effort, fast });
605
626
  signalCleanup = installSignalCleanupFn({
606
627
  name: 'mixdog-exec',
607
- timeoutMs: 6500,
628
+ timeoutMs: 20_000,
608
629
  cleanup,
609
630
  });
610
631
  const createRuntime = runtimeFactory || (