mixdog 0.9.147 → 0.9.148
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/package.json +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +2 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +17 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +20 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +132 -42
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +58 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +45 -8
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +178 -29
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +48 -66
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
|
@@ -5,10 +5,28 @@ import { join } from 'node:path';
|
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
|
|
7
7
|
import { classifyCliInvocation } from './headless-command.mjs';
|
|
8
|
-
import { runHeadlessExec } from './headless-exec.mjs';
|
|
8
|
+
import { prewarmHeadlessSearch, runHeadlessExec } from './headless-exec.mjs';
|
|
9
9
|
import { resolveCursorOAuthAccessToken } from './runtime/agent/orchestrator/providers/cursor-auth.mjs';
|
|
10
10
|
import { createPristineExecutionBoundary } from './runtime/shared/pristine-execution.mjs';
|
|
11
11
|
|
|
12
|
+
test('headless search prewarm starts the native server and Project inventory together', async () => {
|
|
13
|
+
const calls = [];
|
|
14
|
+
await prewarmHeadlessSearch('/app/project', {
|
|
15
|
+
loadNativeSearch: async () => ({
|
|
16
|
+
async warmNativeSearchServer() {
|
|
17
|
+
calls.push('server');
|
|
18
|
+
return true;
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
loadListTool: async () => ({
|
|
22
|
+
async prewarmFindEnumeration(root) {
|
|
23
|
+
calls.push(`index:${root}`);
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
assert.deepEqual(calls.sort(), ['index:/app/project', 'server']);
|
|
28
|
+
});
|
|
29
|
+
|
|
12
30
|
test('pristine headless execution binds Cursor OAuth credentials in process', async () => {
|
|
13
31
|
const root = mkdtempSync(join(tmpdir(), 'mixdog-headless-cursor-auth-test-'));
|
|
14
32
|
const dataDir = join(root, 'data');
|
|
@@ -71,6 +89,7 @@ test('headless exec runs one implicit-approval session and waits for tracked tas
|
|
|
71
89
|
clientHostPid: 123,
|
|
72
90
|
ask: async (_prompt, optionsForAsk) => {
|
|
73
91
|
optionsForAsk.onUsageDelta({
|
|
92
|
+
model: 'gpt-fallback',
|
|
74
93
|
deltaInput: 11,
|
|
75
94
|
deltaCachedRead: 7,
|
|
76
95
|
deltaCacheWrite: 3,
|
|
@@ -110,6 +129,7 @@ test('headless exec runs one implicit-approval session and waits for tracked tas
|
|
|
110
129
|
assert.equal(runtimeClosed, true);
|
|
111
130
|
assert.deepEqual(cleanupOrder, ['runtime', 'memory', 'boundary']);
|
|
112
131
|
const usage = JSON.parse(readFileSync(usageLogPath, 'utf8'));
|
|
132
|
+
assert.deepEqual(usage.sessions[0].models, ['gpt-test', 'gpt-fallback']);
|
|
113
133
|
assert.deepEqual(usage.totals, {
|
|
114
134
|
inputTokens: 11,
|
|
115
135
|
cacheTokens: 7,
|
|
@@ -122,18 +142,17 @@ test('headless exec runs one implicit-approval session and waits for tracked tas
|
|
|
122
142
|
}
|
|
123
143
|
});
|
|
124
144
|
|
|
125
|
-
test('headless exec
|
|
145
|
+
test('headless exec answers an arrived completion and exits without waiting on live work', async () => {
|
|
126
146
|
const output = [];
|
|
127
147
|
const errors = [];
|
|
128
148
|
const prompts = [];
|
|
129
|
-
let
|
|
149
|
+
let closeOptions = null;
|
|
130
150
|
let notificationListener = null;
|
|
131
151
|
const code = await runHeadlessExec({
|
|
132
152
|
message: 'start the long task',
|
|
133
153
|
provider: 'openai-oauth',
|
|
134
154
|
model: 'gpt-test',
|
|
135
155
|
usageLogPath: '',
|
|
136
|
-
idlePollMs: 1,
|
|
137
156
|
write: (text) => output.push(text),
|
|
138
157
|
writeErr: (text) => errors.push(text),
|
|
139
158
|
boundaryFactory: () => ({
|
|
@@ -151,20 +170,20 @@ test('headless exec drains tracked work and returns the completion follow-up ans
|
|
|
151
170
|
async ask(prompt) {
|
|
152
171
|
prompts.push(prompt);
|
|
153
172
|
if (prompts.length === 1) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
meta: { status: 'completed', execution_id: 'job_drain' },
|
|
159
|
-
});
|
|
160
|
-
}, 10);
|
|
173
|
+
notificationListener?.({
|
|
174
|
+
content: 'background task completed',
|
|
175
|
+
meta: { status: 'completed', execution_id: 'job_drain' },
|
|
176
|
+
});
|
|
161
177
|
return { result: { content: 'task started' } };
|
|
162
178
|
}
|
|
163
179
|
return { result: { content: 'final result' } };
|
|
164
180
|
},
|
|
165
|
-
async close() {},
|
|
181
|
+
async close(reason, options) { closeOptions = options ?? null; },
|
|
166
182
|
}),
|
|
167
|
-
|
|
183
|
+
// The job the model left running never ends — a server it was asked to
|
|
184
|
+
// keep up. Exit must not wait for it, and must not reap it either: this
|
|
185
|
+
// test hangs on a regression instead of failing slowly.
|
|
186
|
+
hasActiveTasks: () => true,
|
|
168
187
|
installSignalCleanupFn: () => ({ uninstall() {} }),
|
|
169
188
|
});
|
|
170
189
|
|
|
@@ -172,6 +191,7 @@ test('headless exec drains tracked work and returns the completion follow-up ans
|
|
|
172
191
|
assert.deepEqual(errors, []);
|
|
173
192
|
assert.deepEqual(prompts, ['start the long task', '']);
|
|
174
193
|
assert.deepEqual(output, ['final result\n']);
|
|
194
|
+
assert.deepEqual(closeOptions, { keepBackgroundWork: true });
|
|
175
195
|
});
|
|
176
196
|
|
|
177
197
|
test('headless exec emits a timestamped JSONL lifecycle and exact tool count', async () => {
|
|
@@ -351,6 +371,47 @@ test('headless exec emits structured JSONL failure before returning exit 1', asy
|
|
|
351
371
|
assert.deepEqual(errors, ['mixdog: boom\n']);
|
|
352
372
|
});
|
|
353
373
|
|
|
374
|
+
test('headless exec exposes typed refusal as an API error result', async () => {
|
|
375
|
+
const output = [];
|
|
376
|
+
const code = await runHeadlessExec({
|
|
377
|
+
message: 'request',
|
|
378
|
+
provider: 'anthropic-oauth',
|
|
379
|
+
model: 'claude-test',
|
|
380
|
+
json: true,
|
|
381
|
+
usageLogPath: '',
|
|
382
|
+
write: (text) => output.push(text),
|
|
383
|
+
writeErr() {},
|
|
384
|
+
boundaryFactory: () => ({
|
|
385
|
+
loadConfig: () => ({ providers: { 'anthropic-oauth': { enabled: true } } }),
|
|
386
|
+
cleanup() {},
|
|
387
|
+
}),
|
|
388
|
+
runtimeFactory: async () => ({
|
|
389
|
+
id: 'sess_json_refusal',
|
|
390
|
+
model: 'claude-test',
|
|
391
|
+
clientHostPid: 123,
|
|
392
|
+
async ask() {
|
|
393
|
+
return {
|
|
394
|
+
result: {
|
|
395
|
+
content: 'Unable to respond.',
|
|
396
|
+
stopReason: 'refusal',
|
|
397
|
+
terminationReason: 'refusal',
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
},
|
|
401
|
+
async close() {},
|
|
402
|
+
}),
|
|
403
|
+
hasActiveTasks: () => false,
|
|
404
|
+
installSignalCleanupFn: () => ({ uninstall() {} }),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const terminal = output.join('').trim().split('\n').map((line) => JSON.parse(line)).at(-1);
|
|
408
|
+
assert.equal(code, 0);
|
|
409
|
+
assert.equal(terminal.subtype, 'success');
|
|
410
|
+
assert.equal(terminal.is_error, true);
|
|
411
|
+
assert.equal(terminal.stop_reason, 'refusal');
|
|
412
|
+
assert.equal(terminal.termination_reason, 'refusal');
|
|
413
|
+
});
|
|
414
|
+
|
|
354
415
|
test('--json is accepted for exec and rejected for the interactive command', () => {
|
|
355
416
|
const exec = classifyCliInvocation([
|
|
356
417
|
'exec',
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
recoverability.
|
|
11
11
|
- Mid-task: replacement supersedes; addition folds in; status gets a brief
|
|
12
12
|
answer while work continues. After compaction, resume the summary.
|
|
13
|
+
- Periodic task reports stay in-turn: after each update, call `task wait` for
|
|
14
|
+
the requested interval again until the task settles or the request changes.
|
|
13
15
|
- Auto-compact owns context management: never propose stopping work to the
|
|
14
16
|
user and never stop on your own judgment; resume and continue the work
|
|
15
17
|
after every compaction.
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
apply one analysis to many targets as one parameterized call when supported.
|
|
14
14
|
- Route each evidence facet once to its primary owner, preferring the operation
|
|
15
15
|
that directly returns the evidence needed for the next decision. A summary,
|
|
16
|
-
overview, or enumeration is not a prerequisite
|
|
17
|
-
inputs are already known
|
|
18
|
-
detailed
|
|
16
|
+
overview, or enumeration is not a prerequisite to an operation whose complete
|
|
17
|
+
inputs are already known, and is itself that operation when structure is the
|
|
18
|
+
evidence sought; if independently required, batch it with the detailed
|
|
19
|
+
operation.
|
|
19
20
|
- Known state — system guarantees, supplied facts, visible tool returns,
|
|
20
21
|
applied patches, and passed checks — is never re-found, re-derived, or
|
|
21
22
|
re-verified at any granularity: no re-query call, no confirmation subcommand
|
|
@@ -27,7 +28,9 @@
|
|
|
27
28
|
necessary.
|
|
28
29
|
- Evidence that determines the answer, edit, or deliverable ends retrieval.
|
|
29
30
|
- Treat failure as new evidence and re-enter that loop only for the affected
|
|
30
|
-
facets.
|
|
31
|
+
facets. Do not abandon a viable approach after one failure or leave the
|
|
32
|
+
required deliverable half-finished. Report a blocker when no deterministic
|
|
33
|
+
next action remains.
|
|
31
34
|
- Use only named tools present in the current tool surface; deferred tools
|
|
32
35
|
auto-load on a direct call, and when their exact arguments are not visible,
|
|
33
36
|
call `load_tool` first and read the surfaced schema.
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
a successful owner result closes that facet.
|
|
7
7
|
- Route the missing evidence to its primary owner:
|
|
8
8
|
repository state, history, or diff→`git`;
|
|
9
|
-
exact symbol declaration, body,
|
|
9
|
+
source-file structure or outline, and exact symbol declaration, body,
|
|
10
|
+
usage, or relation→`code_graph`;
|
|
10
11
|
literal, regex, or text location→`grep`;
|
|
11
12
|
known-file content, range, or image→`read`;
|
|
12
13
|
wildcard or recursive file paths→`glob`;
|
|
@@ -412,19 +412,38 @@ export function resolveProviderPromptCacheLane(provider, opts = {}, config = {})
|
|
|
412
412
|
|| promptCacheLaneAutoRequested(opts?.promptCacheLane?.auto)
|
|
413
413
|
|| promptCacheLaneAutoRequested(config?.promptCacheLaneAuto)
|
|
414
414
|
|| promptCacheLaneAutoRequested(config?.openaiCacheLaneAuto);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
415
|
+
// Lane SHARDS, requested either by their own name (`*CacheLaneShards`) or
|
|
416
|
+
// through the legacy `*CacheMaxParallel` / MIXDOG_*_CACHE_MAX_PARALLEL
|
|
417
|
+
// aliases. Those knobs also bounded in-flight admission for the removed
|
|
418
|
+
// compat cache lane — that half is gone for good, admission now belongs
|
|
419
|
+
// solely to the provider/account scheduler — but on OpenAI direct/OAuth
|
|
420
|
+
// they have always selected the prompt-cache shard count as well, and
|
|
421
|
+
// existing configurations depend on it. They stay accepted here at LOWER
|
|
422
|
+
// precedence than the explicitly named shard settings.
|
|
423
|
+
// Exception: xAI compatibility routing passes promptCacheLaneIgnoreAliases
|
|
424
|
+
// so the alias keeps meaning nothing there (openai-compat-xai.mjs), where
|
|
425
|
+
// it never selected shards and would silently fan out cache keys.
|
|
426
|
+
const ignoreAliases = opts?.promptCacheLaneIgnoreAliases === true
|
|
427
|
+
|| config?.promptCacheLaneIgnoreAliases === true;
|
|
428
|
+
const rawShards = opts?.promptCacheLaneShards
|
|
429
|
+
?? opts?.promptCacheLane?.shards
|
|
430
|
+
?? opts?.openaiCacheLaneShards
|
|
431
|
+
?? config?.promptCacheLaneShards
|
|
432
|
+
?? config?.openaiCacheLaneShards
|
|
433
|
+
?? env[`MIXDOG_${envKey}_CACHE_LANE_SHARDS`]
|
|
434
|
+
?? env.MIXDOG_OPENAI_CACHE_LANE_SHARDS;
|
|
435
|
+
const rawAlias = ignoreAliases
|
|
436
|
+
? undefined
|
|
437
|
+
: (opts?.promptCacheLaneMaxParallel
|
|
438
|
+
?? opts?.promptCacheLane?.maxParallel
|
|
420
439
|
?? opts?.openaiCacheMaxParallel
|
|
421
|
-
?? config?.
|
|
422
|
-
?? config?.openaiCacheLaneShards
|
|
440
|
+
?? config?.promptCacheLaneMaxParallel
|
|
423
441
|
?? config?.openaiCacheMaxParallel
|
|
424
|
-
?? env[`MIXDOG_${envKey}_CACHE_LANE_SHARDS`]
|
|
425
442
|
?? env[`MIXDOG_${envKey}_CACHE_MAX_PARALLEL`]
|
|
426
|
-
?? env.MIXDOG_OPENAI_CACHE_LANE_SHARDS
|
|
427
443
|
?? env.MIXDOG_OPENAI_CACHE_MAX_PARALLEL);
|
|
444
|
+
const rawLimit = requestedAuto
|
|
445
|
+
? 'auto'
|
|
446
|
+
: (rawShards ?? rawAlias);
|
|
428
447
|
const shards = parsePromptCacheLaneLimit(rawLimit, defaultPromptCacheLaneShards(provider));
|
|
429
448
|
const auto = shards <= 0;
|
|
430
449
|
const seed = cleanString(
|
|
@@ -347,7 +347,7 @@ function classifyToolFailure(resultText, toolName) {
|
|
|
347
347
|
if (/^\[shell-tool-failed\](?:\s|$)/i.test(leading)) return 'tool-call/failure';
|
|
348
348
|
if (/^\[shell-run-failed\](?:\s|$)/i.test(leading)) {
|
|
349
349
|
if (/\[timeout:|cause:\s*(?:timeout|cancellation)\b/i.test(leading)) return 'timeout/abort';
|
|
350
|
-
if (/cause:\s*(?:output-limit|output-capture-error|background-
|
|
350
|
+
if (/cause:\s*(?:output-limit|output-capture-error|background-promotion-failed)\b/i.test(leading)) return 'runtime/failure';
|
|
351
351
|
if (/\[signal:\s*[^\]]+/i.test(leading)) return 'process/signal';
|
|
352
352
|
return 'command-exit';
|
|
353
353
|
}
|
|
@@ -377,6 +377,11 @@ function classifyToolFailure(resultText, toolName) {
|
|
|
377
377
|
if (/not in allow-list|not allowed/.test(text)) return 'permission';
|
|
378
378
|
if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(raw)) return 'command-exit';
|
|
379
379
|
if (isReadOnlyNavigationMiss(toolName, raw)) return 'navigation/miss';
|
|
380
|
+
// An absent executable is a fact about the environment, not a defect in
|
|
381
|
+
// the tool that reported it: the caller installs it or takes another
|
|
382
|
+
// route. Filed under runtime/failure it padded the count that is supposed
|
|
383
|
+
// to surface real tool bugs.
|
|
384
|
+
if (/executable not found in this environment/.test(text)) return 'environment/unavailable';
|
|
380
385
|
if (/enoent|cannot find|not found at this path|path does not exist|no such file|file not found in graph|unreadable/.test(text)) return 'path/enoent';
|
|
381
386
|
if (/timed out|timeout|interrupted|aborted/.test(text)) return 'timeout/abort';
|
|
382
387
|
if (/unknown tool|tool.*not.*available|missing.*tool/.test(text)) return 'tool-surface';
|
|
@@ -5,13 +5,7 @@ import {
|
|
|
5
5
|
canonicalizeAgentRouteStorage,
|
|
6
6
|
} from '../../shared/agent-route-config.mjs';
|
|
7
7
|
import { OPENAI_COMPAT_PRESETS } from './providers/openai-compat-presets.mjs';
|
|
8
|
-
import {
|
|
9
|
-
hasAnthropicOAuthCredentials,
|
|
10
|
-
hasOpenAIOAuthCredentials,
|
|
11
|
-
hasGrokOAuthCredentials,
|
|
12
|
-
hasCursorOAuthCredentials,
|
|
13
|
-
hasAntigravityOAuthCredentials,
|
|
14
|
-
} from './providers/oauth-credential-probes.mjs';
|
|
8
|
+
import { oauthCredentialProbeState } from './providers/oauth-credential-probes.mjs';
|
|
15
9
|
|
|
16
10
|
// Thin wrapper around resolvePluginData so callers in this orchestrator tree
|
|
17
11
|
// can import a single helper without reaching into shared/.
|
|
@@ -165,22 +159,40 @@ function buildDefaultConfig(options = {}) {
|
|
|
165
159
|
};
|
|
166
160
|
}
|
|
167
161
|
// OAuth provider detection uses lightweight credential probes so config
|
|
168
|
-
// load does not import provider runtimes or SDKs.
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
162
|
+
// load does not import provider runtimes or SDKs.
|
|
163
|
+
//
|
|
164
|
+
// The probe has THREE outcomes and `enabled` is only a boolean, so the
|
|
165
|
+
// third one is carried alongside it: a credential file that EXISTS but
|
|
166
|
+
// could not be read/parsed this instant (lock contention, EACCES, a torn
|
|
167
|
+
// atomic write) yields enabled:false + credentialProbeUnavailable:true.
|
|
168
|
+
// Without that marker the registry cannot tell such a load apart from a
|
|
169
|
+
// real logout/disable and would latch a momentary FS failure as a user
|
|
170
|
+
// opt-out. The marker is ephemeral: loadConfig drops it as soon as the
|
|
171
|
+
// stored config states `enabled` itself, and saveConfig never persists it.
|
|
172
|
+
const oauthEntry = (name, extra = {}) => {
|
|
173
|
+
const state = detectCredentials ? oauthCredentialProbeState(name) : 'absent';
|
|
174
|
+
return {
|
|
175
|
+
enabled: state === 'present',
|
|
176
|
+
...(state === 'unreadable' ? { credentialProbeUnavailable: true } : {}),
|
|
177
|
+
...extra,
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
// WebSocket transport is on by default — measured ~96% cross-session cache
|
|
181
|
+
// hit with delta payloads. Users who need to force SSE (e.g. a corporate
|
|
182
|
+
// proxy blocking WSS) can set `websocket: false` in mixdog-config.json
|
|
183
|
+
// (agent.providers.openai-oauth).
|
|
184
|
+
providers['openai-oauth'] = oauthEntry('openai-oauth', { websocket: true });
|
|
185
|
+
providers['anthropic-oauth'] = oauthEntry('anthropic-oauth');
|
|
174
186
|
// Grok OAuth ("Grok Build"). Like the other OAuth entries it is not
|
|
175
187
|
// stored in mixdog-config.json — enabled at runtime from the presence of
|
|
176
188
|
// Mixdog-owned credentials.
|
|
177
|
-
providers['grok-oauth'] =
|
|
189
|
+
providers['grok-oauth'] = oauthEntry('grok-oauth');
|
|
178
190
|
// Experimental direct Cursor wire provider. It remains disabled unless a
|
|
179
191
|
// Mixdog-owned login or CURSOR_ACCESS_TOKEN is present.
|
|
180
|
-
providers['cursor-oauth'] =
|
|
192
|
+
providers['cursor-oauth'] = oauthEntry('cursor-oauth');
|
|
181
193
|
// Google Antigravity (Cloud Code Assist). Gemini and Claude behind one
|
|
182
194
|
// Google login; enabled only once a login has stored tokens + project.
|
|
183
|
-
providers['antigravity-oauth'] =
|
|
195
|
+
providers['antigravity-oauth'] = oauthEntry('antigravity-oauth');
|
|
184
196
|
// Local providers — opt-in via setup UI after HTTP ping confirms server is running
|
|
185
197
|
providers.ollama = { enabled: false, baseURL: 'http://localhost:11434/v1' };
|
|
186
198
|
providers.lmstudio = { enabled: false, baseURL: 'http://localhost:1234/v1' };
|
|
@@ -461,6 +473,14 @@ export function loadConfig(options = {}) {
|
|
|
461
473
|
for (const [name, val] of Object.entries(raw.providers)) {
|
|
462
474
|
if (val && typeof val === 'object') {
|
|
463
475
|
mergedProviders[name] = { ...(mergedProviders[name] || {}), ...val };
|
|
476
|
+
// A STORED `enabled` is the user's own decision (setup
|
|
477
|
+
// UI disable / hand edit) and outranks the probe: keep
|
|
478
|
+
// it authoritative by dropping the probe-availability
|
|
479
|
+
// marker, so a disable still removes the provider even
|
|
480
|
+
// while its credential file happens to be unreadable.
|
|
481
|
+
if (Object.prototype.hasOwnProperty.call(val, 'enabled')) {
|
|
482
|
+
delete mergedProviders[name].credentialProbeUnavailable;
|
|
483
|
+
}
|
|
464
484
|
} else {
|
|
465
485
|
mergedProviders[name] = val;
|
|
466
486
|
}
|
|
@@ -679,13 +699,28 @@ function buildAgentSaveBuilder(config, appliedDirty) {
|
|
|
679
699
|
// provider keys are keychain-only (loadConfig overlays them into memory;
|
|
680
700
|
// persisting would leak plaintext back into mixdog-config.json). It stays
|
|
681
701
|
// in KNOWN_PROVIDER_KEYS so the generic passthrough loop also skips it.
|
|
682
|
-
|
|
702
|
+
// `credentialProbeUnavailable` is a per-load probe observation, not user
|
|
703
|
+
// state: persisting it would turn one unreadable-file moment into a marker
|
|
704
|
+
// that outlives the condition on every later load. Listed here so the
|
|
705
|
+
// generic passthrough loop below skips it.
|
|
706
|
+
const KNOWN_PROVIDER_KEYS = new Set(['apiKey', 'enabled', 'baseURL', 'credentialProbeUnavailable']);
|
|
683
707
|
const persistedProviders = {};
|
|
684
708
|
if (config.providers) {
|
|
685
709
|
for (const [name, val] of Object.entries(config.providers)) {
|
|
686
710
|
if (!val || typeof val !== 'object') continue;
|
|
687
711
|
const slim = {};
|
|
688
|
-
|
|
712
|
+
// NEVER persist an `enabled:false` that came from a credential probe
|
|
713
|
+
// which could not READ the credential this load. Writing it would
|
|
714
|
+
// convert a momentary FS failure into a stored user decision: the
|
|
715
|
+
// marker is stripped on save, so the next load would read that false
|
|
716
|
+
// as a deliberate disable and the provider would stay off for good.
|
|
717
|
+
// Omitting the key leaves `enabled` derived from the probe again on
|
|
718
|
+
// the next load. This can never erase a real user disable: loadConfig
|
|
719
|
+
// drops the marker whenever the stored config states `enabled`
|
|
720
|
+
// itself, so a marker in memory means nothing was stored for it.
|
|
721
|
+
const probeUnavailable = val.credentialProbeUnavailable === true;
|
|
722
|
+
const skipEnabled = probeUnavailable && val.enabled === false;
|
|
723
|
+
if (typeof val.enabled === 'boolean' && !skipEnabled) slim.enabled = val.enabled;
|
|
689
724
|
if (val.baseURL) slim.baseURL = val.baseURL;
|
|
690
725
|
for (const [k, v] of Object.entries(val)) {
|
|
691
726
|
if (KNOWN_PROVIDER_KEYS.has(k)) continue;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// MCP tool-call cancellation: an aborted caller (hook timeout, aborted turn,
|
|
2
|
+
// closed pane) must settle immediately AND release its admission slot, in every
|
|
3
|
+
// phase of a call — queued, admitted-and-running, and stalled mid-reconnect.
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
_registerMcpServerForTest,
|
|
9
|
+
executeMcpTool,
|
|
10
|
+
getMcpAdmissionSnapshot,
|
|
11
|
+
} from './client.mjs';
|
|
12
|
+
|
|
13
|
+
const never = () => new Promise(() => {});
|
|
14
|
+
|
|
15
|
+
function deferred() {
|
|
16
|
+
let resolve;
|
|
17
|
+
const promise = new Promise((res) => { resolve = res; });
|
|
18
|
+
return { promise, resolve };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function waitFor(predicate, label, timeoutMs = 2_000) {
|
|
22
|
+
const deadline = Date.now() + timeoutMs;
|
|
23
|
+
while (Date.now() < deadline) {
|
|
24
|
+
if (predicate()) return;
|
|
25
|
+
await new Promise((resolve) => { setTimeout(resolve, 10); });
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`timed out waiting for ${label}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function slotsFor(scopeId, serverName) {
|
|
31
|
+
const gate = getMcpAdmissionSnapshot(scopeId).find((entry) => entry.name === serverName);
|
|
32
|
+
return { active: gate?.active ?? 0, queued: gate?.queued ?? 0 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Settle-state probe: a cancellation fix is only real if the promise settles
|
|
36
|
+
* while the callee is still stuck. */
|
|
37
|
+
function track(promise) {
|
|
38
|
+
const state = { settled: false, error: null };
|
|
39
|
+
promise.then(
|
|
40
|
+
() => { state.settled = true; },
|
|
41
|
+
(error) => { state.settled = true; state.error = error; },
|
|
42
|
+
);
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test('an aborted caller settles a QUEUED call and frees its queue seat', async () => {
|
|
47
|
+
const scopeId = 'mcp-cancel-queued';
|
|
48
|
+
_registerMcpServerForTest(scopeId, 'busy', [{ name: 'hang' }], {
|
|
49
|
+
cfg: { maxConcurrency: 1, timeoutMs: 'off' },
|
|
50
|
+
callTool: never,
|
|
51
|
+
});
|
|
52
|
+
const held = executeMcpTool('mcp__busy__hang', {}, { scopeId });
|
|
53
|
+
held.catch(() => {});
|
|
54
|
+
await waitFor(() => slotsFor(scopeId, 'busy').active === 1, 'the first call to occupy the slot');
|
|
55
|
+
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const queued = executeMcpTool('mcp__busy__hang', {}, { scopeId, signal: controller.signal });
|
|
58
|
+
const state = track(queued);
|
|
59
|
+
await waitFor(() => slotsFor(scopeId, 'busy').queued === 1, 'the second call to queue');
|
|
60
|
+
|
|
61
|
+
controller.abort(new Error('queued hook aborted'));
|
|
62
|
+
await assert.rejects(queued, /queued hook aborted/);
|
|
63
|
+
assert.equal(state.settled, true);
|
|
64
|
+
assert.equal(slotsFor(scopeId, 'busy').queued, 0, 'the queue seat is released');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('an aborted caller settles an ADMITTED call whose server ignores cancellation', async () => {
|
|
68
|
+
const scopeId = 'mcp-cancel-active';
|
|
69
|
+
let sawSignal = false;
|
|
70
|
+
_registerMcpServerForTest(scopeId, 'deaf', [{ name: 'hang' }], {
|
|
71
|
+
// Server call timeout DISABLED: the caller's abort is the only bound.
|
|
72
|
+
cfg: { maxConcurrency: 1, timeoutMs: 'off' },
|
|
73
|
+
callTool: (_params, _schema, options) => {
|
|
74
|
+
sawSignal = Boolean(options?.signal);
|
|
75
|
+
return never();
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const call = executeMcpTool('mcp__deaf__hang', {}, { scopeId, signal: controller.signal });
|
|
80
|
+
const state = track(call);
|
|
81
|
+
await waitFor(() => slotsFor(scopeId, 'deaf').active === 1, 'the call to be admitted');
|
|
82
|
+
assert.equal(state.settled, false, 'a running call does not settle on its own');
|
|
83
|
+
|
|
84
|
+
controller.abort(new Error('mcp_tool hook timed out: mcp__deaf__hang'));
|
|
85
|
+
await assert.rejects(call, /mcp_tool hook timed out/);
|
|
86
|
+
assert.equal(sawSignal, true, 'the abort signal reaches the SDK request');
|
|
87
|
+
await waitFor(() => slotsFor(scopeId, 'deaf').active === 0, 'the admission slot to be released');
|
|
88
|
+
|
|
89
|
+
// The freed slot is genuinely reusable.
|
|
90
|
+
_registerMcpServerForTest(scopeId, 'deaf-ok', [{ name: 'go' }], {
|
|
91
|
+
cfg: { maxConcurrency: 1 },
|
|
92
|
+
callTool: async () => ({ content: [{ type: 'text', text: 'ran after cancel' }] }),
|
|
93
|
+
});
|
|
94
|
+
assert.equal(await executeMcpTool('mcp__deaf-ok__go', {}, { scopeId }), 'ran after cancel');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('an aborted caller settles a call stalled inside a shared reconnect', async () => {
|
|
98
|
+
const scopeId = 'mcp-cancel-reconnect';
|
|
99
|
+
const closeEntered = deferred();
|
|
100
|
+
_registerMcpServerForTest(scopeId, 'flaky', [{ name: 'boom' }], {
|
|
101
|
+
cfg: { maxConcurrency: 1, timeoutMs: 'off' },
|
|
102
|
+
// A plain failure (not a timeout, not an abort) is what sends the call into
|
|
103
|
+
// the shared reconnect path.
|
|
104
|
+
callTool: async () => { throw new Error('transport went away'); },
|
|
105
|
+
// Teardown never completes, so the reconnect stays in flight forever.
|
|
106
|
+
close: () => { closeEntered.resolve(); return never(); },
|
|
107
|
+
});
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const call = executeMcpTool('mcp__flaky__boom', {}, { scopeId, signal: controller.signal });
|
|
110
|
+
const state = track(call);
|
|
111
|
+
await closeEntered.promise;
|
|
112
|
+
await waitFor(() => slotsFor(scopeId, 'flaky').active === 1, 'the call to hold its slot in reconnect');
|
|
113
|
+
assert.equal(state.settled, false, 'the stalled reconnect keeps the call pending');
|
|
114
|
+
|
|
115
|
+
controller.abort(new Error('hook aborted during reconnect'));
|
|
116
|
+
await assert.rejects(call, /hook aborted during reconnect/);
|
|
117
|
+
await waitFor(
|
|
118
|
+
() => slotsFor(scopeId, 'flaky').active === 0,
|
|
119
|
+
'the admission slot to be released while the reconnect is still stalled',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('cancellation never turns into a reconnect-and-retry (no duplicate side effect)', async () => {
|
|
124
|
+
const scopeId = 'mcp-cancel-no-retry';
|
|
125
|
+
let calls = 0;
|
|
126
|
+
const closeEntered = deferred();
|
|
127
|
+
_registerMcpServerForTest(scopeId, 'once', [{ name: 'effect' }], {
|
|
128
|
+
cfg: { maxConcurrency: 1, timeoutMs: 'off' },
|
|
129
|
+
callTool: async () => { calls += 1; throw new Error('transport went away'); },
|
|
130
|
+
close: () => { closeEntered.resolve(); return never(); },
|
|
131
|
+
});
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
const call = executeMcpTool('mcp__once__effect', {}, { scopeId, signal: controller.signal });
|
|
134
|
+
await closeEntered.promise;
|
|
135
|
+
controller.abort(new Error('aborted before retry'));
|
|
136
|
+
await assert.rejects(call, /aborted before retry/);
|
|
137
|
+
await new Promise((resolve) => { setTimeout(resolve, 50); });
|
|
138
|
+
assert.equal(calls, 1, 'the aborted call is never replayed against a reconnected server');
|
|
139
|
+
});
|
|
@@ -44,23 +44,25 @@ function run(cmd, args) {
|
|
|
44
44
|
async function enumerate() {
|
|
45
45
|
const childrenOf = new Map();
|
|
46
46
|
const tokenOf = new Map();
|
|
47
|
+
const nameOf = new Map();
|
|
47
48
|
const out = isWin
|
|
48
49
|
? await run('powershell.exe', [
|
|
49
50
|
'-NoProfile', '-NonInteractive', '-Command',
|
|
50
|
-
`Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o'))" }`,
|
|
51
|
+
`Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.Name) $($_.CreationDate.ToString('o'))" }`,
|
|
51
52
|
])
|
|
52
|
-
: await run('ps', ['-A', '-o', 'pid=,ppid=,lstart=']);
|
|
53
|
+
: await run('ps', ['-A', '-o', 'pid=,ppid=,comm=,lstart=']);
|
|
53
54
|
for (const line of out.split(/\r?\n/)) {
|
|
54
|
-
const m = line.trim().match(/^(\d+)\s+(\d+)(?:\s+(.+))?$/);
|
|
55
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)(?:\s+(.+))?$/);
|
|
55
56
|
if (!m) continue;
|
|
56
57
|
const pid = Number(m[1]);
|
|
57
58
|
const ppid = Number(m[2]);
|
|
58
|
-
const token = (m[
|
|
59
|
+
const token = (m[4] || '').trim() || null;
|
|
59
60
|
if (!childrenOf.has(ppid)) childrenOf.set(ppid, []);
|
|
60
61
|
childrenOf.get(ppid).push(pid);
|
|
61
62
|
tokenOf.set(pid, token);
|
|
63
|
+
nameOf.set(pid, m[3]);
|
|
62
64
|
}
|
|
63
|
-
return { childrenOf, tokenOf };
|
|
65
|
+
return { childrenOf, tokenOf, nameOf };
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
// BFS every descendant pid of rootPid (excludes rootPid).
|
|
@@ -87,6 +89,33 @@ async function collectDescendants(rootPid) {
|
|
|
87
89
|
return descendantsOf(childrenOf, rootPid);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/** Descendant pids WITH their image names, from one batched process-table
|
|
93
|
+
* read. Shared with the shell runner's post-exit descendant observation so
|
|
94
|
+
* both use a single enumeration implementation. */
|
|
95
|
+
export async function collectDescendantProcesses(rootPid) {
|
|
96
|
+
const { childrenOf, nameOf } = await enumerate().catch(() => ({ childrenOf: new Map(), nameOf: new Map() }));
|
|
97
|
+
return descendantsOf(childrenOf, rootPid).map((pid) => ({ pid, name: nameOf.get(pid) || '' }));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Force-kill whole process trees by pid. Windows batches one taskkill /T /F
|
|
101
|
+
* for every pid; POSIX escalates SIGTERM → SIGKILL. Best-effort: the caller
|
|
102
|
+
* verifies liveness afterwards rather than trusting the signal. */
|
|
103
|
+
export async function killProcessTrees(pids) {
|
|
104
|
+
const targets = [...new Set((pids || []).map(Number).filter((pid) => Number.isInteger(pid) && pid > 0))];
|
|
105
|
+
if (targets.length === 0) return;
|
|
106
|
+
if (isWin) {
|
|
107
|
+
const args = ['/F', '/T'];
|
|
108
|
+
for (const pid of targets) args.push('/PID', String(pid));
|
|
109
|
+
await run('taskkill', args);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
for (const pid of targets) { try { process.kill(pid, 'SIGTERM'); } catch { /* ignore */ } }
|
|
113
|
+
await delay(300);
|
|
114
|
+
for (const pid of targets) {
|
|
115
|
+
if (isAlive(pid)) { try { process.kill(pid, 'SIGKILL'); } catch { /* ignore */ } }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
90
119
|
/**
|
|
91
120
|
* Immediate, non-blocking tree kill for a transport whose MCP handshake never
|
|
92
121
|
* completed (shutdown-during-connect). Unlike shutdownStdioChild there is no
|