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.
- package/README.md +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- 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 {
|