mixdog 0.9.151 → 0.9.152

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 (175) hide show
  1. package/README.md +110 -68
  2. package/package.json +3 -2
  3. package/scripts/tool-stress.mjs +0 -2
  4. package/src/defaults/skills/setup/SKILL.md +18 -27
  5. package/src/headless-exec.mjs +3 -10
  6. package/src/headless-exec.test.mjs +2 -7
  7. package/src/runtime/agent/orchestrator/config.mjs +6 -66
  8. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  9. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  11. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  12. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  17. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
  20. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  25. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  26. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
  29. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
  32. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
  33. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  34. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  35. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  38. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
  39. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
  40. package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
  43. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
  44. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
  45. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
  48. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
  49. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
  50. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
  51. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  52. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  57. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  58. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  59. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  60. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  62. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  63. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  64. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  69. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  73. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  74. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  75. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  76. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
  77. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  78. package/src/runtime/browser-bridge/client.mjs +103 -0
  79. package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
  80. package/src/runtime/computer-bridge/client.mjs +101 -0
  81. package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
  82. package/src/runtime/media/store.mjs +18 -8
  83. package/src/runtime/media/store.test.mjs +14 -0
  84. package/src/runtime/memory/index.mjs +1 -1
  85. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  86. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  87. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  88. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  89. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  90. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  91. package/src/runtime/shared/provider-api-key.mjs +1 -0
  92. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  93. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  94. package/src/runtime/shared/skill-document.mjs +86 -0
  95. package/src/runtime/shared/skill-document.test.mjs +81 -0
  96. package/src/runtime/shared/tool-surface.mjs +18 -0
  97. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  98. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  99. package/src/runtime/shared/user-cwd.mjs +33 -14
  100. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  101. package/src/session-runtime/cwd-plugins.mjs +26 -26
  102. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  103. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  104. package/src/session-runtime/global-extensions.mjs +26 -0
  105. package/src/session-runtime/global-extensions.test.mjs +28 -0
  106. package/src/session-runtime/goal-runtime.mjs +721 -0
  107. package/src/session-runtime/goal-runtime.test.mjs +108 -0
  108. package/src/session-runtime/lifecycle-api.mjs +18 -4
  109. package/src/session-runtime/lifecycle-api.test.mjs +21 -0
  110. package/src/session-runtime/mcp-glue.mjs +49 -47
  111. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  112. package/src/session-runtime/plugin-mcp.mjs +96 -22
  113. package/src/session-runtime/prewarm.mjs +1 -10
  114. package/src/session-runtime/resource-api-global.test.mjs +69 -0
  115. package/src/session-runtime/resource-api.mjs +173 -58
  116. package/src/session-runtime/runtime-core.mjs +148 -20
  117. package/src/session-runtime/runtime-tunables.mjs +6 -8
  118. package/src/session-runtime/session-turn-api.mjs +22 -11
  119. package/src/session-runtime/skills-api.mjs +79 -25
  120. package/src/session-runtime/skills-api.test.mjs +99 -0
  121. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  122. package/src/session-runtime/tool-catalog.mjs +4 -13
  123. package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
  124. package/src/session-runtime/tool-surface.mjs +12 -0
  125. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  126. package/src/standalone/agent-tool/notify.mjs +6 -8
  127. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  128. package/src/standalone/daemon.mjs +31 -15
  129. package/src/standalone/hook-bus/config.mjs +1 -0
  130. package/src/standalone/plugin-admin.mjs +17 -0
  131. package/src/standalone/plugin-admin.test.mjs +7 -0
  132. package/src/standalone/provider-admin.mjs +1 -0
  133. package/src/standalone/session-protocol.mjs +5 -0
  134. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  135. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  136. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  137. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  138. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  139. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  140. package/src/standalone/session-runtime-host.mjs +696 -58
  141. package/src/standalone/session-runtime-inline-host.mjs +351 -0
  142. package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
  143. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  144. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  145. package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
  146. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  147. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  148. package/src/standalone/session-runtime-worker.mjs +476 -18
  149. package/src/standalone/session-service.mjs +81 -6
  150. package/src/tui/app/app-view.jsx +1 -0
  151. package/src/tui/app/core-memory-picker.mjs +2 -0
  152. package/src/tui/app/extension-pickers.mjs +1 -1
  153. package/src/tui/app/maintenance-pickers.mjs +3 -3
  154. package/src/tui/app/model-options.mjs +2 -0
  155. package/src/tui/app/model-picker.mjs +14 -4
  156. package/src/tui/app/panel-handoff.test.mjs +311 -0
  157. package/src/tui/app/project-picker.mjs +1 -0
  158. package/src/tui/app/route-pickers.mjs +21 -6
  159. package/src/tui/app/settings-picker.mjs +26 -3
  160. package/src/tui/app/slash-commands.mjs +1 -0
  161. package/src/tui/app/slash-dispatch.mjs +73 -22
  162. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  163. package/src/tui/components/Picker.jsx +10 -1
  164. package/src/tui/dist/index.mjs +163 -44
  165. package/src/tui/session/context-state.mjs +1 -0
  166. package/src/tui/session/goal-continuation.mjs +110 -0
  167. package/src/tui/session/goal-continuation.test.mjs +84 -0
  168. package/src/tui/session/queue-helpers.mjs +5 -2
  169. package/src/tui/session/session-action-surface.test.mjs +108 -0
  170. package/src/tui/session/session-api-ext.mjs +39 -8
  171. package/src/tui/session/session-api.mjs +58 -0
  172. package/src/tui/session/session-flow.mjs +6 -0
  173. package/src/tui/session/turn.mjs +1 -0
  174. package/src/tui/session-local.mjs +15 -1
  175. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ // Runtime-shard client for distributed Agent control.
4
+ //
5
+ // A Lead runtime and every Subagent it creates used to share one worker
6
+ // process even after top-level session sharding. Agent control now crosses the
7
+ // existing shard IPC link: the daemon chooses a target shard, while the target
8
+ // keeps the ordinary persistent Agent tool/tag/session implementation.
9
+ import { safeIpcSend } from '../runtime/shared/safe-ipc-send.mjs';
10
+ import { isSessionRuntimeWorkerProcess } from '../runtime/shared/child-spawn-remote.mjs';
11
+
12
+ const pending = new Map();
13
+ let sequence = 0;
14
+ let listening = false;
15
+
16
+ function remoteError(body) {
17
+ const error = new Error(String(body?.message || 'remote agent control failed'));
18
+ if (body?.name) error.name = String(body.name);
19
+ if (body?.stack) error.stack = String(body.stack);
20
+ if (body?.code) error.code = String(body.code);
21
+ return error;
22
+ }
23
+
24
+ function ensureListener() {
25
+ if (listening) return;
26
+ listening = true;
27
+ process.on('message', (message) => {
28
+ if (!message || message.type !== 'agent-control-result') return;
29
+ const controlId = String(message.controlId || '');
30
+ const request = pending.get(controlId);
31
+ if (!request) return;
32
+ pending.delete(controlId);
33
+ request.cleanup();
34
+ if (message.ok === false) request.reject(remoteError(message.error));
35
+ else request.resolve(message.value);
36
+ });
37
+ process.on('disconnect', () => {
38
+ for (const [controlId, request] of pending) {
39
+ pending.delete(controlId);
40
+ request.cleanup();
41
+ request.reject(new Error('runtime shard disconnected during agent control'));
42
+ }
43
+ });
44
+ }
45
+
46
+ export function remoteAgentControlEnabled(env = process.env) {
47
+ return isSessionRuntimeWorkerProcess(env)
48
+ && typeof process.send === 'function'
49
+ && process.connected === true;
50
+ }
51
+
52
+ export function executeRemoteAgentControl(args = {}, context = {}) {
53
+ if (!remoteAgentControlEnabled()) {
54
+ return Promise.reject(new Error('remote agent control is unavailable'));
55
+ }
56
+ ensureListener();
57
+ const controlId = `agent-control-${process.pid}-${++sequence}`;
58
+ const signal = context?.signal || null;
59
+ if (signal?.aborted) {
60
+ return Promise.reject(signal.reason instanceof Error
61
+ ? signal.reason
62
+ : new Error(String(signal.reason || 'agent control canceled')));
63
+ }
64
+ return new Promise((resolve, reject) => {
65
+ let timer = null;
66
+ let onAbort = null;
67
+ const cleanup = () => {
68
+ if (timer) clearTimeout(timer);
69
+ timer = null;
70
+ if (onAbort && signal) {
71
+ try { signal.removeEventListener('abort', onAbort); } catch {}
72
+ }
73
+ onAbort = null;
74
+ };
75
+ pending.set(controlId, { resolve, reject, cleanup });
76
+ if (signal) {
77
+ onAbort = () => {
78
+ safeIpcSend(process, {
79
+ type: 'agent-control-cancel',
80
+ controlId,
81
+ reason: String(signal.reason?.message || signal.reason || 'agent control canceled'),
82
+ }, { onError: () => {} });
83
+ const request = pending.get(controlId);
84
+ if (!request) return;
85
+ pending.delete(controlId);
86
+ cleanup();
87
+ reject(signal.reason instanceof Error
88
+ ? signal.reason
89
+ : new Error(String(signal.reason || 'agent control canceled')));
90
+ };
91
+ signal.addEventListener('abort', onAbort, { once: true });
92
+ }
93
+ timer = setTimeout(() => {
94
+ const request = pending.get(controlId);
95
+ if (!request) return;
96
+ pending.delete(controlId);
97
+ cleanup();
98
+ reject(new Error('remote agent control timed out'));
99
+ }, 180_000);
100
+ timer.unref?.();
101
+ const sent = safeIpcSend(process, {
102
+ type: 'agent-control',
103
+ controlId,
104
+ args: args && typeof args === 'object' ? args : {},
105
+ context: {
106
+ callerCwd: typeof context?.callerCwd === 'string' ? context.callerCwd : null,
107
+ invocationSource: typeof context?.invocationSource === 'string'
108
+ ? context.invocationSource
109
+ : null,
110
+ callerSessionId: typeof context?.callerSessionId === 'string'
111
+ ? context.callerSessionId
112
+ : null,
113
+ clientHostPid: Number(context?.clientHostPid) || null,
114
+ },
115
+ }, {
116
+ onError: (error) => {
117
+ const request = pending.get(controlId);
118
+ if (!request) return;
119
+ pending.delete(controlId);
120
+ cleanup();
121
+ reject(error);
122
+ },
123
+ });
124
+ if (!sent) {
125
+ const request = pending.get(controlId);
126
+ if (request) {
127
+ pending.delete(controlId);
128
+ cleanup();
129
+ reject(new Error('runtime shard IPC is unavailable'));
130
+ }
131
+ }
132
+ });
133
+ }
@@ -0,0 +1,71 @@
1
+ import assert from 'node:assert/strict';
2
+ import { fork } from 'node:child_process';
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import test from 'node:test';
7
+
8
+ const CLIENT_URL = new URL('./session-runtime-agent-control-client.mjs', import.meta.url).href;
9
+
10
+ test('runtime shard Agent client sends serializable context and receives the routed result', async () => {
11
+ const dir = await mkdtemp(join(tmpdir(), 'mixdog-agent-control-client-'));
12
+ const entry = join(dir, 'client.mjs');
13
+ await writeFile(entry, `
14
+ process.env.MIXDOG_SESSION_RUNTIME_WORKER_PID = String(process.pid);
15
+ const client = await import(${JSON.stringify(CLIENT_URL)});
16
+ const value = await client.executeRemoteAgentControl(
17
+ { type: 'spawn', tag: 'routed', agent: 'worker', prompt: 'run' },
18
+ {
19
+ callerCwd: 'C:/Project/test',
20
+ invocationSource: 'model-tool',
21
+ callerSessionId: 'sess_lead',
22
+ clientHostPid: 4242,
23
+ },
24
+ );
25
+ process.send({ type: 'report', value });
26
+ `, 'utf8');
27
+
28
+ try {
29
+ const observed = await new Promise((resolve, reject) => {
30
+ const child = fork(entry, [], {
31
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
32
+ env: { ...process.env },
33
+ });
34
+ const timer = setTimeout(() => {
35
+ try { child.kill(); } catch {}
36
+ reject(new Error('agent control client test timed out'));
37
+ }, 10_000);
38
+ child.on('message', (message) => {
39
+ if (message?.type === 'agent-control') {
40
+ assert.deepEqual(message.args, {
41
+ type: 'spawn',
42
+ tag: 'routed',
43
+ agent: 'worker',
44
+ prompt: 'run',
45
+ });
46
+ assert.deepEqual(message.context, {
47
+ callerCwd: 'C:/Project/test',
48
+ invocationSource: 'model-tool',
49
+ callerSessionId: 'sess_lead',
50
+ clientHostPid: 4242,
51
+ });
52
+ child.send({
53
+ type: 'agent-control-result',
54
+ controlId: message.controlId,
55
+ ok: true,
56
+ value: 'agent task: task_routed',
57
+ });
58
+ return;
59
+ }
60
+ if (message?.type !== 'report') return;
61
+ clearTimeout(timer);
62
+ try { child.kill(); } catch {}
63
+ resolve(message.value);
64
+ });
65
+ child.on('error', reject);
66
+ });
67
+ assert.equal(observed, 'agent task: task_routed');
68
+ } finally {
69
+ await rm(dir, { recursive: true, force: true });
70
+ }
71
+ });
@@ -0,0 +1,139 @@
1
+ // Cold-start cancellation contract, exercised against the REAL runtime worker
2
+ // child (no stub): a cancel that arrives before or during the dispatch's
3
+ // module/provider cold start must never be answered "not running" and then let
4
+ // the dispatch run anyway.
5
+ import assert from 'node:assert/strict';
6
+ import { fork } from 'node:child_process';
7
+ import { mkdtemp, rm } from 'node:fs/promises';
8
+ import { tmpdir } from 'node:os';
9
+ import { dirname, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import test from 'node:test';
12
+
13
+ const WORKER_ENTRY = fileURLToPath(new URL('./session-runtime-worker.mjs', import.meta.url));
14
+ const REPO_ROOT = join(dirname(WORKER_ENTRY), '..', '..');
15
+
16
+ function startWorker(runtimeRoot) {
17
+ const child = fork(WORKER_ENTRY, [], {
18
+ cwd: REPO_ROOT,
19
+ execArgv: [],
20
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
21
+ env: {
22
+ ...process.env,
23
+ MIXDOG_RUNTIME_ROOT: runtimeRoot,
24
+ MIXDOG_DATA_DIR: runtimeRoot,
25
+ MIXDOG_SESSION_RUNTIME_SHARD: '0',
26
+ MIXDOG_SESSION_RUNTIME_SHARD_COUNT: '2',
27
+ },
28
+ });
29
+ const responses = new Map();
30
+ const waiters = new Map();
31
+ child.on('message', (message) => {
32
+ if (message?.type !== 'response') return;
33
+ const id = String(message.requestId || '');
34
+ const waiter = waiters.get(id);
35
+ if (waiter) {
36
+ waiters.delete(id);
37
+ waiter(message);
38
+ return;
39
+ }
40
+ responses.set(id, message);
41
+ });
42
+ const send = (payload) => child.send(payload);
43
+ const wait = (requestId, timeoutMs = 30_000) => new Promise((resolve, reject) => {
44
+ const settled = responses.get(requestId);
45
+ if (settled) {
46
+ responses.delete(requestId);
47
+ resolve(settled);
48
+ return;
49
+ }
50
+ const timer = setTimeout(
51
+ () => reject(new Error(`timed out waiting for ${requestId}`)),
52
+ timeoutMs,
53
+ );
54
+ waiters.set(requestId, (message) => { clearTimeout(timer); resolve(message); });
55
+ });
56
+ return { child, send, wait };
57
+ }
58
+
59
+ async function stopWorker(worker) {
60
+ try {
61
+ worker.send({ type: 'shutdown', requestId: 'shutdown-1', reason: 'test complete' });
62
+ await worker.wait('shutdown-1', 8_000).catch(() => {});
63
+ await new Promise((resolve) => setTimeout(resolve, 250));
64
+ } finally {
65
+ try { worker.child.kill(); } catch {}
66
+ }
67
+ }
68
+
69
+ test('a cancel that arrives before the dispatch is retained and aborts it', async () => {
70
+ const runtimeRoot = await mkdtemp(join(tmpdir(), 'mixdog-dispatch-cancel-'));
71
+ const worker = startWorker(runtimeRoot);
72
+ try {
73
+ worker.send({
74
+ type: 'agent-dispatch-cancel',
75
+ requestId: 'cancel-early',
76
+ dispatchId: 'dispatch-cold',
77
+ reason: 'user aborted during cold start',
78
+ });
79
+ const cancel = await worker.wait('cancel-early', 15_000);
80
+ assert.equal(cancel.ok, true);
81
+ // Never "cancelled: false": an unknown id means not-registered-YET.
82
+ assert.equal(cancel.value.cancelled, true);
83
+ assert.equal(cancel.value.retained, true);
84
+
85
+ const startedAt = Date.now();
86
+ worker.send({
87
+ type: 'agent-dispatch',
88
+ requestId: 'dispatch-cold-1',
89
+ dispatchId: 'dispatch-cold',
90
+ agent: 'memory',
91
+ params: { prompt: 'must never run' },
92
+ });
93
+ const dispatch = await worker.wait('dispatch-cold-1', 20_000);
94
+ assert.equal(dispatch.ok, false);
95
+ assert.match(String(dispatch.error?.message || ''), /user aborted during cold start/);
96
+ // The abort is adopted at registration — before the orchestrator/provider
97
+ // cold start, which alone takes seconds in this worker.
98
+ assert.ok(
99
+ Date.now() - startedAt < 3_000,
100
+ 'aborted dispatch never entered module/provider setup',
101
+ );
102
+ } finally {
103
+ await stopWorker(worker);
104
+ await rm(runtimeRoot, { recursive: true, force: true });
105
+ }
106
+ });
107
+
108
+ test('a cancel racing a cold-start dispatch is never answered "not running"', async () => {
109
+ const runtimeRoot = await mkdtemp(join(tmpdir(), 'mixdog-dispatch-cancel-warm-'));
110
+ const worker = startWorker(runtimeRoot);
111
+ try {
112
+ worker.send({
113
+ type: 'agent-dispatch',
114
+ requestId: 'dispatch-warm-1',
115
+ dispatchId: 'dispatch-warm',
116
+ agent: 'memory',
117
+ params: { prompt: 'cancelled while importing' },
118
+ });
119
+ // The cold start compiles the orchestrator graph and blocks this loop, so
120
+ // the cancel frame can only be handled once that window closes. Whichever
121
+ // side wins, the abort must be honoured or retained — never dropped with
122
+ // `cancelled: false`, which is what let a canceled dispatch keep running.
123
+ worker.send({
124
+ type: 'agent-dispatch-cancel',
125
+ requestId: 'cancel-warm',
126
+ dispatchId: 'dispatch-warm',
127
+ reason: 'aborted mid cold start',
128
+ });
129
+ const cancel = await worker.wait('cancel-warm', 60_000);
130
+ assert.equal(cancel.ok, true);
131
+ assert.equal(cancel.value.cancelled, true);
132
+
133
+ const dispatch = await worker.wait('dispatch-warm-1', 60_000);
134
+ assert.equal(dispatch.ok, false);
135
+ } finally {
136
+ await stopWorker(worker);
137
+ await rm(runtimeRoot, { recursive: true, force: true });
138
+ }
139
+ });
@@ -0,0 +1,17 @@
1
+ import { createInlineSessionRuntimeHost } from './session-runtime-inline-host.mjs';
2
+ import { createSessionRuntimeHost } from './session-runtime-host.mjs';
3
+
4
+ export function resolveSessionRuntimeMode(value = process.env.MIXDOG_SESSION_RUNTIME_MODE) {
5
+ const mode = String(value || 'inline').trim().toLowerCase();
6
+ if (mode === 'inline') return 'inline';
7
+ if (mode === 'process') return 'process';
8
+ throw new Error(`unsupported MIXDOG_SESSION_RUNTIME_MODE=${JSON.stringify(value)}`);
9
+ }
10
+
11
+ export function createDaemonSessionRuntimeHost(options = {}, {
12
+ createInline = createInlineSessionRuntimeHost,
13
+ createProcess = createSessionRuntimeHost,
14
+ mode = resolveSessionRuntimeMode(),
15
+ } = {}) {
16
+ return mode === 'process' ? createProcess(options) : createInline(options);
17
+ }
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ createDaemonSessionRuntimeHost,
6
+ resolveSessionRuntimeMode,
7
+ } from './session-runtime-host-factory.mjs';
8
+
9
+ test('session runtime mode defaults to inline and accepts only explicit canonical names', () => {
10
+ assert.equal(resolveSessionRuntimeMode(undefined), 'inline');
11
+ assert.equal(resolveSessionRuntimeMode('process'), 'process');
12
+ assert.throws(() => resolveSessionRuntimeMode('in-process'), /unsupported/);
13
+ assert.throws(() => resolveSessionRuntimeMode('worker'), /unsupported/);
14
+ assert.throws(() => resolveSessionRuntimeMode('unknown'), /unsupported/);
15
+ });
16
+
17
+ test('session runtime factory selects exactly one host implementation', () => {
18
+ const calls = [];
19
+ const createInline = (options) => { calls.push(['inline', options]); return { mode: 'inline' }; };
20
+ const createProcess = (options) => { calls.push(['process', options]); return { mode: 'process' }; };
21
+ assert.equal(createDaemonSessionRuntimeHost({ cwd: 'A' }, {
22
+ mode: 'inline', createInline, createProcess,
23
+ }).mode, 'inline');
24
+ assert.equal(createDaemonSessionRuntimeHost({ cwd: 'B' }, {
25
+ mode: 'process', createInline, createProcess,
26
+ }).mode, 'process');
27
+ assert.deepEqual(calls, [
28
+ ['inline', { cwd: 'A' }],
29
+ ['process', { cwd: 'B' }],
30
+ ]);
31
+ });
@@ -68,10 +68,13 @@ test('an unhealthy runtime worker is replaced and every runtime is recovered', a
68
68
  const logs = [];
69
69
  await writeFile(workerEntry, WORKER_STUB, 'utf8');
70
70
 
71
+ // Single shard: this covers the recycle/recovery contract INSIDE one shard.
72
+ // Cross-shard routing and isolation live in session-runtime-shard-host.test.mjs.
71
73
  const host = createSessionRuntimeHost({
72
74
  workerEntry,
73
75
  cwd: dir,
74
76
  env: { ...process.env },
77
+ shardCount: 1,
75
78
  log: (line) => logs.push(line),
76
79
  });
77
80
  try {
@@ -111,6 +114,7 @@ test('omitted trailing arguments stay omitted across the runtime wire', async ()
111
114
  workerEntry,
112
115
  cwd: dir,
113
116
  env: { ...process.env },
117
+ shardCount: 1,
114
118
  log: () => {},
115
119
  });
116
120
  try {
@@ -139,6 +143,7 @@ test('RSS telemetry never schedules a destructive runtime recycle', async () =>
139
143
  MIXDOG_SESSION_WORKER_RECYCLE_RSS_MB: '1',
140
144
  MIXDOG_SESSION_WORKER_RECYCLE_INTERVAL_MS: '1',
141
145
  },
146
+ shardCount: 1,
142
147
  log: () => {},
143
148
  });
144
149
  try {