mixdog 1.1.1 → 1.2.0
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 +1 -0
- package/package.json +2 -2
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/release-paths.mjs +39 -2
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +6 -0
- package/src/defaults/skills/setup/references/actions.md +5 -2
- package/src/defaults/skills/visualize/SKILL.md +157 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/headless-role.mjs +2 -2
- package/src/help.mjs +2 -0
- package/src/lib/rules-builder.cjs +0 -23
- package/src/rules/lead/LEAD.md +2 -2
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +9 -1
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +11 -8
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
- package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +0 -10
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/channels/index.mjs +1 -5
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
- package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
- package/src/runtime/channels/lib/worker-main.mjs +4 -34
- package/src/runtime/shared/atomic-file.mjs +8 -2
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -118
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -2
- package/src/runtime/shared/provider-api-key.mjs +21 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/semver.d.mts +1 -0
- package/src/runtime/shared/semver.mjs +57 -0
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-surface.mjs +9 -2
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
- package/src/runtime/shared/update-checker.mjs +2 -31
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/boot/apis.mjs +35 -0
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +20 -0
- package/src/session-runtime/boot/infrastructure.mjs +1 -0
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +9 -4
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +11 -6
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/notification-bus.mjs +9 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/resource-extension-sync.mjs +30 -7
- package/src/session-runtime/resource-mcp-api.mjs +27 -10
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-bootstrap.mjs +0 -4
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-paths.mjs +0 -1
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-profile-api.mjs +27 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +11 -4
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
- package/src/session-runtime/starter-guide-state.mjs +62 -0
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/standalone/channel-binding.mjs +1 -2
- package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-channels-loader.mjs +2 -3
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +4 -1
- package/src/standalone/daemon.mjs +139 -5
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +13 -78
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +17 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +35 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/dist/index.mjs +332 -36
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +12 -8
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +18 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +2 -0
- package/src/tui/session/session-api/shared.mjs +10 -6
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +15 -1
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +39 -10
- package/src/runtime/channels/tool-defs.mjs +0 -17
|
@@ -22,9 +22,9 @@ import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
|
|
|
22
22
|
import { createOwnerFairGate } from '../../../shared/owner-fair-gate.mjs';
|
|
23
23
|
import { currentToolExecutionOwner } from '../../../shared/tool-execution-owner.mjs';
|
|
24
24
|
import { positiveInt } from '../../../shared/numbers.mjs';
|
|
25
|
-
import {
|
|
26
|
-
import { createMcpTransport } from './client-transport.mjs';
|
|
27
|
-
import { runBoundedHandshake } from './client-handshake.mjs';
|
|
25
|
+
import { resolveMcpRequestTimeouts, resolveMcpTransportKind, scrubMcpConnectionMessage } from './client-config.mjs';
|
|
26
|
+
import { appendMcpStderrTail, createMcpTransport } from './client-transport.mjs';
|
|
27
|
+
import { listAllMcpTools, runBoundedHandshake } from './client-handshake.mjs';
|
|
28
28
|
|
|
29
29
|
// Config interpretation (transport kind, URL policy, env expansion, headers,
|
|
30
30
|
// timeouts, log scrubbing) lives in client-config.mjs; re-exported so every
|
|
@@ -37,6 +37,9 @@ export {
|
|
|
37
37
|
resolveMcpHttpHeaders,
|
|
38
38
|
resolveMcpTransportKind,
|
|
39
39
|
resolveMcpStartupTimeoutMs,
|
|
40
|
+
resolveMcpCallTimeoutMs,
|
|
41
|
+
resolveMcpMaxTotalTimeoutMs,
|
|
42
|
+
resolveMcpRequestTimeouts,
|
|
40
43
|
} from './client-config.mjs';
|
|
41
44
|
const servers = new Map();
|
|
42
45
|
const reconnects = createKeyedSingleflight();
|
|
@@ -226,7 +229,15 @@ function mcpFeatureTools(serverName, capabilities, protocolTools) {
|
|
|
226
229
|
export function getMcpServerStatus(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
227
230
|
return scopedServerEntries(scopeId).map(([, server]) => ({
|
|
228
231
|
name: server.name,
|
|
229
|
-
connected:
|
|
232
|
+
connected: server.liveness?.alive !== false,
|
|
233
|
+
...(server.liveness?.alive === false
|
|
234
|
+
? {
|
|
235
|
+
error: appendMcpStderrTail(
|
|
236
|
+
server.liveness.lastError || `MCP server "${server.name}" closed`,
|
|
237
|
+
server.stderrTail?.()
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
: {}),
|
|
230
241
|
toolCount: Array.isArray(server.tools) ? server.tools.length : 0,
|
|
231
242
|
tools: (server.tools || []).map((tool) => ({
|
|
232
243
|
name: tool.name,
|
|
@@ -296,28 +307,98 @@ export function getMcpServerInstructionsMap(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
|
296
307
|
return out;
|
|
297
308
|
}
|
|
298
309
|
|
|
310
|
+
// Close `current` (when registered) and connect `name` afresh with `cfg`.
|
|
311
|
+
// Callers run it inside the per-server reconnect lane. `genAtStart` is the
|
|
312
|
+
// shutdown generation captured BEFORE the caller queued/closed anything: a
|
|
313
|
+
// disconnectAll() landing during the close (or the connect) bumps it, and the
|
|
314
|
+
// replacement must then never register (no resurrection after shutdown).
|
|
315
|
+
async function replaceServerConnection(scopeId, name, current, cfg, genAtStart) {
|
|
316
|
+
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
317
|
+
if (current) {
|
|
318
|
+
await _closeServer(current);
|
|
319
|
+
if (servers.get(registryKey) === current) servers.delete(registryKey);
|
|
320
|
+
_invalidateMcpToolFieldMemo();
|
|
321
|
+
}
|
|
322
|
+
if (genAtStart !== currentConnectAbortGeneration(scopeId)) {
|
|
323
|
+
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
// connectServer re-checks the same generation after loading the SDK and
|
|
327
|
+
// again right before registering, so a shutdown mid-handshake also aborts.
|
|
328
|
+
await connectServer(name, cfg, scopeId, genAtStart);
|
|
329
|
+
} catch (reason) {
|
|
330
|
+
const rawMessage = reason instanceof Error ? reason.message : String(reason);
|
|
331
|
+
throw new Error(scrubMcpConnectionMessage(rawMessage, cfg));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Explicit single-server operations (connect/reconnect/disconnect) never join
|
|
336
|
+
// a failure-driven reconnect that is already in flight — its config may be
|
|
337
|
+
// stale — they queue behind it on the same lane instead, so at most one
|
|
338
|
+
// connect per server is ever running (no doubly registered transports).
|
|
339
|
+
async function runExclusiveServerOp(registryKey, task) {
|
|
340
|
+
for (let pending = reconnects.wait(registryKey); pending; pending = reconnects.wait(registryKey)) {
|
|
341
|
+
await pending.catch(() => {});
|
|
342
|
+
}
|
|
343
|
+
return reconnects.run(registryKey, task);
|
|
344
|
+
}
|
|
345
|
+
|
|
299
346
|
async function reconnectMcpServer(scopeId, serverName, failedServer) {
|
|
300
347
|
const registryKey = mcpServerRegistryKey(scopeId, serverName);
|
|
301
|
-
|
|
348
|
+
// The generation the failed server was registered under, NOT the current
|
|
349
|
+
// one: when disconnectAll() itself caused the failure it has already bumped
|
|
350
|
+
// the generation (and still holds the entry while awaiting the close), so a
|
|
351
|
+
// freshly captured value would bless a post-shutdown reconnect.
|
|
352
|
+
const genAtStart = failedServer.generation ?? currentConnectAbortGeneration(scopeId);
|
|
353
|
+
await reconnects.run(registryKey, async () => {
|
|
302
354
|
const current = servers.get(registryKey);
|
|
303
355
|
// A peer already completed the replacement while this failed call was
|
|
304
356
|
// unwinding. Reuse it immediately instead of closing a fresh transport.
|
|
305
|
-
if (current && current !== failedServer) return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
357
|
+
if (current && current !== failedServer) return;
|
|
358
|
+
// Unregistered meanwhile (explicit disconnect / shutdown), shut down since
|
|
359
|
+
// it was registered, or being deliberately closed right now: a failed call
|
|
360
|
+
// must not resurrect a server someone deliberately removed.
|
|
361
|
+
if (!current || genAtStart !== currentConnectAbortGeneration(scopeId) || current.liveness?.closing) {
|
|
362
|
+
throw new Error(`MCP server "${serverName}" was disconnected`);
|
|
309
363
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
364
|
+
await replaceServerConnection(scopeId, serverName, current, failedServer.cfg, genAtStart);
|
|
365
|
+
});
|
|
366
|
+
const replacement = servers.get(registryKey);
|
|
367
|
+
if (!replacement) {
|
|
368
|
+
throw new Error(`reconnect succeeded but server "${serverName}" entry is missing from registry`);
|
|
369
|
+
}
|
|
370
|
+
return replacement;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Connect, or reconnect, ONE named MCP server in `scopeId` without touching
|
|
375
|
+
* any other server. A registered instance is closed first; `cfg` defaults to
|
|
376
|
+
* the registered config. `cfg.enabled === false` only disconnects. Resolves
|
|
377
|
+
* true when the server is connected afterwards, false when it was disabled;
|
|
378
|
+
* rejects (credential-scrubbed message) when the connect fails, leaving the
|
|
379
|
+
* server unregistered.
|
|
380
|
+
*/
|
|
381
|
+
export async function reconnectSingleMcpServer(scopeId, name, cfg = undefined) {
|
|
382
|
+
const normalized = normalizeMcpScopeId(scopeId);
|
|
383
|
+
_knownMcpScopes.add(normalized);
|
|
384
|
+
const registryKey = mcpServerRegistryKey(normalized, name);
|
|
385
|
+
// Captured before queuing: a shutdown while this op waits or closes wins.
|
|
386
|
+
const genAtStart = currentConnectAbortGeneration(normalized);
|
|
387
|
+
return runExclusiveServerOp(registryKey, async () => {
|
|
388
|
+
const current = servers.get(registryKey);
|
|
389
|
+
const nextCfg = cfg ?? current?.cfg;
|
|
390
|
+
if (!nextCfg) throw new Error(`MCP server "${name}" has no config to connect with`);
|
|
391
|
+
if (nextCfg.enabled === false) {
|
|
392
|
+
if (current) {
|
|
393
|
+
await _closeServer(current);
|
|
394
|
+
if (servers.get(registryKey) === current) servers.delete(registryKey);
|
|
395
|
+
_invalidateMcpToolFieldMemo();
|
|
396
|
+
}
|
|
397
|
+
closeCallAdmission(normalized, name, `MCP ${name} disabled`);
|
|
398
|
+
return false;
|
|
319
399
|
}
|
|
320
|
-
|
|
400
|
+
await replaceServerConnection(normalized, name, current, nextCfg, genAtStart);
|
|
401
|
+
return true;
|
|
321
402
|
});
|
|
322
403
|
}
|
|
323
404
|
|
|
@@ -366,6 +447,16 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
366
447
|
mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
|
|
367
448
|
throw firstErr;
|
|
368
449
|
}
|
|
450
|
+
const serverDead = server.liveness?.alive === false;
|
|
451
|
+
if (!serverDead && isMcpServerResponseError(firstErr)) {
|
|
452
|
+
// The server answered (JSON-RPC error): it is alive — nothing to
|
|
453
|
+
// reconnect, and the call already ran its course.
|
|
454
|
+
throw firstErr;
|
|
455
|
+
}
|
|
456
|
+
// Only a failure that proves the request never left this process
|
|
457
|
+
// may be replayed; anything else may already have run server-side.
|
|
458
|
+
const undelivered = isMcpUndeliveredError(firstErr);
|
|
459
|
+
const deadTail = serverDead ? server.stderrTail?.() : '';
|
|
369
460
|
mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
|
|
370
461
|
let retryServer;
|
|
371
462
|
try {
|
|
@@ -382,7 +473,17 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
382
473
|
} catch (reconnectErr) {
|
|
383
474
|
if (isMcpCallAbortError(reconnectErr) || callSignal?.aborted) throw reconnectErr;
|
|
384
475
|
const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
|
|
385
|
-
throw new Error(
|
|
476
|
+
throw new Error(
|
|
477
|
+
appendMcpStderrTail(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`, deadTail)
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (!undelivered) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
appendMcpStderrTail(
|
|
483
|
+
`Tool call failed: ${firstMsg}; MCP server "${serverName}" was reconnected for the next call, but this call was not replayed because the request may already have reached the server`,
|
|
484
|
+
deadTail
|
|
485
|
+
)
|
|
486
|
+
);
|
|
386
487
|
}
|
|
387
488
|
// A reconnect that outlived the caller's abort must not dispatch the
|
|
388
489
|
// retry: that would run the side effect after cancellation.
|
|
@@ -410,71 +511,120 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
410
511
|
);
|
|
411
512
|
}
|
|
412
513
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
request = server.client.listResourceTemplates(cursor, requestOptions);
|
|
421
|
-
} else if (operation === 'read-resource') {
|
|
422
|
-
request = server.client.readResource({ uri: String(args?.uri || '') }, requestOptions);
|
|
423
|
-
} else if (operation === 'list-prompts') {
|
|
424
|
-
request = server.client.listPrompts(cursor, requestOptions);
|
|
425
|
-
} else if (operation === 'get-prompt') {
|
|
426
|
-
request = server.client.getPrompt(
|
|
514
|
+
const MCP_FEATURE_REQUESTS = {
|
|
515
|
+
'list-resources': (client, args, options) => client.listResources(mcpCursorParams(args), options),
|
|
516
|
+
'list-resource-templates': (client, args, options) => client.listResourceTemplates(mcpCursorParams(args), options),
|
|
517
|
+
'read-resource': (client, args, options) => client.readResource({ uri: String(args?.uri || '') }, options),
|
|
518
|
+
'list-prompts': (client, args, options) => client.listPrompts(mcpCursorParams(args), options),
|
|
519
|
+
'get-prompt': (client, args, options) =>
|
|
520
|
+
client.getPrompt(
|
|
427
521
|
{
|
|
428
522
|
name: String(args?.name || ''),
|
|
429
523
|
arguments:
|
|
430
524
|
args?.arguments && typeof args.arguments === 'object' && !Array.isArray(args.arguments) ? args.arguments : {},
|
|
431
525
|
},
|
|
432
|
-
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
|
|
526
|
+
options
|
|
527
|
+
),
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
function mcpCursorParams(args) {
|
|
531
|
+
return typeof args?.cursor === 'string' && args.cursor ? { cursor: args.cursor } : {};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function _callMcpFeatureWithTimeout(server, operation, args, signal = null) {
|
|
535
|
+
const issue = MCP_FEATURE_REQUESTS[operation];
|
|
536
|
+
if (!issue) throw new Error(`Unsupported MCP feature operation: ${operation}`);
|
|
537
|
+
const result = await runMcpRequest(server, signal, (options) => issue(server.client, args, options), {
|
|
538
|
+
abortMessage: `MCP feature call aborted (server="${server?.name}", operation="${operation}")`,
|
|
539
|
+
timeoutMessage: (ms) =>
|
|
540
|
+
`MCP feature call timed out after ${ms}ms without progress (server="${server.name}", operation="${operation}")`,
|
|
541
|
+
errorFields: { serverName: server.name, operation },
|
|
542
|
+
});
|
|
543
|
+
return {
|
|
544
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
545
|
+
isError: false,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Our per-call deadline. It is reset by every progress notification, but never
|
|
550
|
+
// beyond maxTotalTimeoutMs from the start. On expiry the call rejects with
|
|
551
|
+
// EMCPTOOLTIMEOUT and `cancelRequest` cancels ONLY this request (the SDK then
|
|
552
|
+
// sends notifications/cancelled); the server and its other in-flight calls
|
|
553
|
+
// are untouched. The caller clears the timer once its own race settles.
|
|
554
|
+
function mcpCallTimeout({ callTimeoutMs, maxTotalTimeoutMs }, message, errorFields, cancelRequest) {
|
|
555
|
+
const startedAt = Date.now();
|
|
556
|
+
let timer = null;
|
|
557
|
+
let rejectTimeout;
|
|
558
|
+
const promise = new Promise((_, reject) => {
|
|
559
|
+
rejectTimeout = reject;
|
|
560
|
+
});
|
|
561
|
+
const fire = () => {
|
|
562
|
+
timer = null;
|
|
563
|
+
const error = Object.assign(new Error(message), { code: 'EMCPTOOLTIMEOUT', ...errorFields });
|
|
564
|
+
rejectTimeout(error);
|
|
565
|
+
cancelRequest(error);
|
|
566
|
+
};
|
|
567
|
+
const arm = (ms) => {
|
|
568
|
+
if (timer) clearTimeout(timer);
|
|
569
|
+
timer = setTimeout(fire, ms);
|
|
570
|
+
if (timer.unref) timer.unref();
|
|
571
|
+
};
|
|
572
|
+
arm(callTimeoutMs);
|
|
573
|
+
return {
|
|
574
|
+
promise,
|
|
575
|
+
clear: () => {
|
|
576
|
+
if (timer) clearTimeout(timer);
|
|
577
|
+
timer = null;
|
|
578
|
+
},
|
|
579
|
+
progress: () => {
|
|
580
|
+
if (!timer) return;
|
|
581
|
+
const remainingTotal = maxTotalTimeoutMs ? maxTotalTimeoutMs - (Date.now() - startedAt) : Infinity;
|
|
582
|
+
if (remainingTotal > 0) arm(Math.min(callTimeoutMs, remainingTotal));
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Issue one SDK request under the per-call limits. The request gets its OWN
|
|
589
|
+
* abort signal (linked to the caller's), our timeout as the SDK `timeout`
|
|
590
|
+
* (or the largest timer when disabled, so the SDK's 60s default never
|
|
591
|
+
* applies), and progress handling: each progress report resets both the SDK
|
|
592
|
+
* timeout and ours, capped by `maxTotalTimeout`.
|
|
593
|
+
*/
|
|
594
|
+
async function runMcpRequest(server, signal, issue, { abortMessage, timeoutMessage, errorFields }) {
|
|
595
|
+
const timeouts = resolveMcpRequestTimeouts(server?.cfg);
|
|
596
|
+
const controller = new AbortController();
|
|
597
|
+
const forwardAbort = () => controller.abort(signal.reason);
|
|
598
|
+
if (signal) {
|
|
599
|
+
allowConcurrentAbortListeners(signal);
|
|
600
|
+
if (signal.aborted) forwardAbort();
|
|
601
|
+
else signal.addEventListener('abort', forwardAbort, { once: true });
|
|
436
602
|
}
|
|
437
|
-
const timeoutMs = resolveMcpCallTimeoutMs(server?.cfg);
|
|
438
|
-
const abortMessage = `MCP feature call aborted (server="${server?.name}", operation="${operation}")`;
|
|
439
603
|
const timeout =
|
|
440
|
-
|
|
604
|
+
timeouts.callTimeoutMs > 0
|
|
441
605
|
? mcpCallTimeout(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
606
|
+
timeouts,
|
|
607
|
+
timeoutMessage(timeouts.callTimeoutMs),
|
|
608
|
+
{ ...errorFields, timeoutMs: timeouts.callTimeoutMs },
|
|
609
|
+
(error) => controller.abort(error)
|
|
445
610
|
)
|
|
446
611
|
: null;
|
|
612
|
+
const requestOptions = {
|
|
613
|
+
signal: controller.signal,
|
|
614
|
+
timeout: timeouts.sdkTimeoutMs,
|
|
615
|
+
resetTimeoutOnProgress: true,
|
|
616
|
+
onprogress: () => timeout?.progress(),
|
|
617
|
+
...(timeouts.maxTotalTimeoutMs ? { maxTotalTimeout: timeouts.maxTotalTimeoutMs } : {}),
|
|
618
|
+
};
|
|
447
619
|
try {
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
signal,
|
|
451
|
-
abortMessage
|
|
452
|
-
);
|
|
453
|
-
return {
|
|
454
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
455
|
-
isError: false,
|
|
456
|
-
};
|
|
620
|
+
const request = issue(requestOptions);
|
|
621
|
+
return await raceMcpAbort(timeout ? Promise.race([request, timeout.promise]) : request, signal, abortMessage);
|
|
457
622
|
} finally {
|
|
458
623
|
timeout?.clear();
|
|
624
|
+
signal?.removeEventListener?.('abort', forwardAbort);
|
|
459
625
|
}
|
|
460
626
|
}
|
|
461
627
|
|
|
462
|
-
// Server-side per-call deadline. On expiry the server goes through the full
|
|
463
|
-
// tree-shutdown path so a timed-out stdio server never orphans grandchildren
|
|
464
|
-
// (fire-and-forget), and the call rejects with EMCPTOOLTIMEOUT. The caller
|
|
465
|
-
// clears the timer once its own (abort-aware) race settles.
|
|
466
|
-
function mcpCallTimeout(server, timeoutMs, message, errorFields = {}) {
|
|
467
|
-
let timer;
|
|
468
|
-
const promise = new Promise((_, reject) => {
|
|
469
|
-
timer = setTimeout(() => {
|
|
470
|
-
_closeServer(server).catch(() => {});
|
|
471
|
-
reject(Object.assign(new Error(message), { code: 'EMCPTOOLTIMEOUT', ...errorFields }));
|
|
472
|
-
}, timeoutMs);
|
|
473
|
-
if (timer.unref) timer.unref();
|
|
474
|
-
});
|
|
475
|
-
return { promise, clear: () => clearTimeout(timer) };
|
|
476
|
-
}
|
|
477
|
-
|
|
478
628
|
function mcpResourceFilename(uri) {
|
|
479
629
|
const tail = String(uri || '')
|
|
480
630
|
.split(/[?#]/)[0]
|
|
@@ -506,14 +656,21 @@ function mcpCanonicalBase64(data, maxBytes) {
|
|
|
506
656
|
const mcpMiB = (bytes) => `${bytes / 1024 / 1024} MiB`;
|
|
507
657
|
|
|
508
658
|
function mcpImagePart(data, rawMime) {
|
|
509
|
-
const mimeType =
|
|
659
|
+
const mimeType =
|
|
660
|
+
String(rawMime || 'image/png')
|
|
661
|
+
.split(';')[0]
|
|
662
|
+
.trim()
|
|
663
|
+
.toLowerCase() || 'image/png';
|
|
510
664
|
if (!SUPPORTED_IMAGE_MIME_TYPES.has(mimeType)) {
|
|
511
665
|
return { type: 'text', text: `[MCP image omitted: ${mimeType} is not a supported image type]` };
|
|
512
666
|
}
|
|
513
667
|
const canon = mcpCanonicalBase64(data, MAX_IMAGE_BYTES);
|
|
514
668
|
if (!canon) return { type: 'text', text: '[MCP image omitted: empty image data]' };
|
|
515
669
|
if (canon.tooLarge) {
|
|
516
|
-
return {
|
|
670
|
+
return {
|
|
671
|
+
type: 'text',
|
|
672
|
+
text: `[MCP image omitted: ${mimeType} image exceeds the ${mcpMiB(MAX_IMAGE_BYTES)} limit]`,
|
|
673
|
+
};
|
|
517
674
|
}
|
|
518
675
|
return { type: 'image', data: canon.data, mimeType };
|
|
519
676
|
}
|
|
@@ -584,10 +741,19 @@ function mcpAudioPart(data, rawMime) {
|
|
|
584
741
|
const canon = typeof data === 'string' && data ? mcpCanonicalBase64(data, MAX_ATTACHMENT_BLOB_BYTES) : null;
|
|
585
742
|
if (!canon) return { type: 'text', text: `[MCP audio content: ${mimeType}]` };
|
|
586
743
|
if (canon.tooLarge) {
|
|
587
|
-
return {
|
|
744
|
+
return {
|
|
745
|
+
type: 'text',
|
|
746
|
+
text: `[MCP audio omitted: ${mimeType} exceeds the ${mcpMiB(MAX_ATTACHMENT_BLOB_BYTES)} limit]`,
|
|
747
|
+
};
|
|
588
748
|
}
|
|
589
749
|
const subtype = mimeType.split(';')[0].split('/')[1] || '';
|
|
590
|
-
const path = spillMcpBinary(
|
|
750
|
+
const path = spillMcpBinary(
|
|
751
|
+
canon.buffer,
|
|
752
|
+
subtype
|
|
753
|
+
.replace(/[^a-z0-9]/gi, '')
|
|
754
|
+
.slice(0, 8)
|
|
755
|
+
.toLowerCase() || 'bin'
|
|
756
|
+
);
|
|
591
757
|
return {
|
|
592
758
|
type: 'text',
|
|
593
759
|
text: path
|
|
@@ -648,8 +814,32 @@ export async function normalizeMcpToolResult(result) {
|
|
|
648
814
|
return parts ? { content: parts } : text;
|
|
649
815
|
}
|
|
650
816
|
|
|
817
|
+
// JSON-RPC codes the SDK uses for client-side failures (McpError).
|
|
818
|
+
const MCP_CONNECTION_CLOSED_CODE = -32000;
|
|
819
|
+
const MCP_REQUEST_TIMEOUT_CODE = -32001;
|
|
820
|
+
|
|
821
|
+
// Our own deadline, or the SDK's (McpError RequestTimeout, incl. the
|
|
822
|
+
// progress maxTotalTimeout).
|
|
651
823
|
function isMcpToolCallTimeoutError(err) {
|
|
652
|
-
return err?.code === 'EMCPTOOLTIMEOUT';
|
|
824
|
+
return err?.code === 'EMCPTOOLTIMEOUT' || err?.code === MCP_REQUEST_TIMEOUT_CODE;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// The server answered with a JSON-RPC error: proof it is alive.
|
|
828
|
+
function isMcpServerResponseError(err) {
|
|
829
|
+
return (
|
|
830
|
+
typeof err?.code === 'number' && err.code !== MCP_CONNECTION_CLOSED_CODE && err.code !== MCP_REQUEST_TIMEOUT_CODE
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Connection-level failures that prove the request was never delivered: the
|
|
835
|
+
// SDK/transport had no live connection to send on, or the socket was refused
|
|
836
|
+
// before anything was written. ECONNRESET etc. are excluded — the request may
|
|
837
|
+
// have been read before the reset.
|
|
838
|
+
const MCP_UNDELIVERED_CODES = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH', 'ENETUNREACH']);
|
|
839
|
+
function isMcpUndeliveredError(err) {
|
|
840
|
+
if (!err) return false;
|
|
841
|
+
if (/^Not connected$/i.test(String(err.message || '').trim())) return true;
|
|
842
|
+
return MCP_UNDELIVERED_CODES.has(String(err.code || err.cause?.code || ''));
|
|
653
843
|
}
|
|
654
844
|
|
|
655
845
|
// Caller-driven cancellation (hook timeout, aborted turn, closed pane).
|
|
@@ -714,38 +904,22 @@ export function raceMcpAbort(promise, signal, message) {
|
|
|
714
904
|
}
|
|
715
905
|
|
|
716
906
|
async function _callToolWithTimeout(server, toolName, args, signal = null) {
|
|
717
|
-
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (!(timeoutMs > 0)) {
|
|
724
|
-
// Server timeout disabled: the caller's signal is then the ONLY bound.
|
|
725
|
-
return raceMcpAbort(
|
|
726
|
-
server.client.callTool({ name: toolName, arguments: args }, undefined, requestOptions),
|
|
727
|
-
signal,
|
|
728
|
-
abortMessage
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
const timeout = mcpCallTimeout(
|
|
907
|
+
// The request's signal goes INTO the SDK: an aborted caller (or our own
|
|
908
|
+
// timeout) cancels the live JSON-RPC request (notifications/cancelled)
|
|
909
|
+
// instead of merely walking away from it. The abort race additionally frees
|
|
910
|
+
// the admission slot at once. With the timeout disabled the caller's signal
|
|
911
|
+
// is the ONLY bound.
|
|
912
|
+
return runMcpRequest(
|
|
732
913
|
server,
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
{
|
|
914
|
+
signal,
|
|
915
|
+
(options) => server.client.callTool({ name: toolName, arguments: args }, undefined, options),
|
|
916
|
+
{
|
|
917
|
+
abortMessage: `MCP tool call aborted (server="${server?.name}", tool="${toolName}")`,
|
|
918
|
+
timeoutMessage: (ms) =>
|
|
919
|
+
`MCP tool call timed out after ${ms}ms without progress (server="${server.name}", tool="${toolName}")`,
|
|
920
|
+
errorFields: { serverName: server.name, toolName },
|
|
921
|
+
}
|
|
736
922
|
);
|
|
737
|
-
try {
|
|
738
|
-
return await raceMcpAbort(
|
|
739
|
-
Promise.race([
|
|
740
|
-
server.client.callTool({ name: toolName, arguments: args }, undefined, requestOptions),
|
|
741
|
-
timeout.promise,
|
|
742
|
-
]),
|
|
743
|
-
signal,
|
|
744
|
-
abortMessage
|
|
745
|
-
);
|
|
746
|
-
} finally {
|
|
747
|
-
timeout.clear();
|
|
748
|
-
}
|
|
749
923
|
}
|
|
750
924
|
|
|
751
925
|
function countTextLines(text) {
|
|
@@ -932,17 +1106,19 @@ export async function disconnectAll(options = undefined) {
|
|
|
932
1106
|
export async function disconnectMcpServer(name, options = {}) {
|
|
933
1107
|
const scopeId = normalizeMcpScopeId(options);
|
|
934
1108
|
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1109
|
+
return runExclusiveServerOp(registryKey, async () => {
|
|
1110
|
+
const server = servers.get(registryKey);
|
|
1111
|
+
if (!server) return false;
|
|
1112
|
+
try {
|
|
1113
|
+
await _closeServer(server);
|
|
1114
|
+
} catch {
|
|
1115
|
+
/* ignore */
|
|
1116
|
+
}
|
|
1117
|
+
if (servers.get(registryKey) === server) servers.delete(registryKey);
|
|
1118
|
+
closeCallAdmission(scopeId, name, `MCP ${name} disconnected`);
|
|
1119
|
+
_invalidateMcpToolFieldMemo();
|
|
1120
|
+
return true;
|
|
1121
|
+
});
|
|
946
1122
|
}
|
|
947
1123
|
/**
|
|
948
1124
|
* Close a single server: for stdio transports first shut down the full child
|
|
@@ -952,6 +1128,8 @@ export async function disconnectMcpServer(name, options = {}) {
|
|
|
952
1128
|
* only kills the direct child and discards the pid we need to walk the tree.
|
|
953
1129
|
*/
|
|
954
1130
|
async function _closeServer(server) {
|
|
1131
|
+
// A deliberate close is not a crash: suppress the unexpected-close report.
|
|
1132
|
+
if (server?.liveness) server.liveness.closing = true;
|
|
955
1133
|
const transport = server?.transport;
|
|
956
1134
|
// Live stdio transports expose the spawned ChildProcess on _process.
|
|
957
1135
|
if (transport?._process) {
|
|
@@ -986,7 +1164,8 @@ async function connectServer(
|
|
|
986
1164
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
987
1165
|
}
|
|
988
1166
|
const client = createMcpClient(sdk, name, cfg);
|
|
989
|
-
const { transport, autoDetectAdvert } = createMcpTransport({ name, cfg, sdk, log: mcpLog });
|
|
1167
|
+
const { transport, autoDetectAdvert, stderrTail } = createMcpTransport({ name, cfg, sdk, log: mcpLog });
|
|
1168
|
+
const liveness = trackMcpLiveness(client, name, cfg, stderrTail);
|
|
990
1169
|
const pending = { scopeId, name, client, transport };
|
|
991
1170
|
_pendingConnects.add(pending);
|
|
992
1171
|
try {
|
|
@@ -996,11 +1175,12 @@ async function connectServer(
|
|
|
996
1175
|
client,
|
|
997
1176
|
transport,
|
|
998
1177
|
autoDetectAdvert,
|
|
999
|
-
closeServer: _closeServer,
|
|
1178
|
+
closeServer: (target) => _closeServer({ ...target, liveness }),
|
|
1179
|
+
stderrTail,
|
|
1000
1180
|
});
|
|
1001
1181
|
if (!toolsResult || !Array.isArray(toolsResult.tools)) {
|
|
1002
1182
|
try {
|
|
1003
|
-
await _closeServer({ client, transport });
|
|
1183
|
+
await _closeServer({ client, transport, liveness });
|
|
1004
1184
|
} catch {
|
|
1005
1185
|
/* ignore */
|
|
1006
1186
|
}
|
|
@@ -1009,7 +1189,7 @@ async function connectServer(
|
|
|
1009
1189
|
if (genAtStart !== currentConnectAbortGeneration(scopeId)) {
|
|
1010
1190
|
// disconnectAll() ran mid-handshake: never register — tear down.
|
|
1011
1191
|
try {
|
|
1012
|
-
await _closeServer({ client, transport });
|
|
1192
|
+
await _closeServer({ client, transport, liveness });
|
|
1013
1193
|
} catch {
|
|
1014
1194
|
/* ignore */
|
|
1015
1195
|
}
|
|
@@ -1027,6 +1207,8 @@ async function connectServer(
|
|
|
1027
1207
|
cfg,
|
|
1028
1208
|
instructions,
|
|
1029
1209
|
capabilities,
|
|
1210
|
+
liveness,
|
|
1211
|
+
stderrTail,
|
|
1030
1212
|
generation: genAtStart,
|
|
1031
1213
|
});
|
|
1032
1214
|
if (capabilities?.tools?.listChanged && sdk.ToolListChangedNotificationSchema) {
|
|
@@ -1041,6 +1223,33 @@ async function connectServer(
|
|
|
1041
1223
|
}
|
|
1042
1224
|
}
|
|
1043
1225
|
|
|
1226
|
+
/**
|
|
1227
|
+
* Liveness of one server connection. The SDK calls client.onclose whenever
|
|
1228
|
+
* the transport closes (child exit, socket drop, our own close) — the server
|
|
1229
|
+
* is then dead and getMcpServerStatus reports it disconnected. onerror only
|
|
1230
|
+
* records the last error (parse/send hiccups do not mean the server died).
|
|
1231
|
+
* Handlers already installed on the client are chained, not replaced.
|
|
1232
|
+
*/
|
|
1233
|
+
function trackMcpLiveness(client, name, cfg, stderrTail) {
|
|
1234
|
+
const liveness = { alive: true, closing: false, closedAt: 0, lastError: '' };
|
|
1235
|
+
const priorClose = client.onclose;
|
|
1236
|
+
const priorError = client.onerror;
|
|
1237
|
+
client.onclose = (...args) => {
|
|
1238
|
+
const unexpected = liveness.alive && !liveness.closing;
|
|
1239
|
+
liveness.alive = false;
|
|
1240
|
+
liveness.closedAt = Date.now();
|
|
1241
|
+
if (unexpected) {
|
|
1242
|
+
mcpLog(`${appendMcpStderrTail(`[mcp-client] MCP server "${name}" closed unexpectedly`, stderrTail())}\n`);
|
|
1243
|
+
}
|
|
1244
|
+
if (typeof priorClose === 'function') priorClose.apply(client, args);
|
|
1245
|
+
};
|
|
1246
|
+
client.onerror = (error, ...rest) => {
|
|
1247
|
+
liveness.lastError = scrubMcpConnectionMessage(error instanceof Error ? error.message : String(error), cfg);
|
|
1248
|
+
if (typeof priorError === 'function') priorError.call(client, error, ...rest);
|
|
1249
|
+
};
|
|
1250
|
+
return liveness;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1044
1253
|
/** The SDK client for one server, advertising the project root when known. */
|
|
1045
1254
|
function createMcpClient(sdk, name, cfg) {
|
|
1046
1255
|
const projectRoot = String(cfg?._mixdogProjectRoot || cfg?.cwd || '').trim();
|
|
@@ -1075,11 +1284,26 @@ function mcpServerTools(name, capabilities, rawTools) {
|
|
|
1075
1284
|
return [...protocolTools, ...mcpFeatureTools(name, capabilities, protocolTools)];
|
|
1076
1285
|
}
|
|
1077
1286
|
|
|
1078
|
-
|
|
1287
|
+
// tools/list_changed refreshes run one at a time per server connection, so an
|
|
1288
|
+
// older listing can never land after a newer one.
|
|
1289
|
+
function refreshServerTools(registryKey, client, name, cfg, capabilities) {
|
|
1079
1290
|
const live = servers.get(registryKey);
|
|
1080
|
-
if (!live || live.client !== client) return;
|
|
1291
|
+
if (!live || live.client !== client) return Promise.resolve();
|
|
1292
|
+
live.refreshTail = (live.refreshTail || Promise.resolve()).then(() =>
|
|
1293
|
+
runToolRefresh(registryKey, live, name, cfg, capabilities)
|
|
1294
|
+
);
|
|
1295
|
+
return live.refreshTail;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async function runToolRefresh(registryKey, live, name, cfg, capabilities) {
|
|
1299
|
+
if (servers.get(registryKey) !== live) return;
|
|
1081
1300
|
try {
|
|
1082
|
-
const
|
|
1301
|
+
const { sdkTimeoutMs } = resolveMcpRequestTimeouts(cfg);
|
|
1302
|
+
const refreshed = await listAllMcpTools(live.client, { timeout: sdkTimeoutMs });
|
|
1303
|
+
if (!refreshed || !Array.isArray(refreshed.tools)) {
|
|
1304
|
+
throw new Error('ListTools returned invalid shape: missing or non-array tools field');
|
|
1305
|
+
}
|
|
1306
|
+
if (servers.get(registryKey) !== live) return;
|
|
1083
1307
|
live.tools = mcpServerTools(name, capabilities, refreshed.tools);
|
|
1084
1308
|
_invalidateMcpToolFieldMemo();
|
|
1085
1309
|
} catch (error) {
|
|
@@ -1109,6 +1333,8 @@ export function _registerMcpServerForTest(scopeId, name, rawTools = [], options
|
|
|
1109
1333
|
cfg: options.cfg || {},
|
|
1110
1334
|
instructions: options.instructions || '',
|
|
1111
1335
|
capabilities,
|
|
1336
|
+
liveness: { alive: true, closing: false, closedAt: 0, lastError: '' },
|
|
1337
|
+
stderrTail: typeof options.stderrTail === 'function' ? options.stderrTail : () => '',
|
|
1112
1338
|
transport: null,
|
|
1113
1339
|
client: {
|
|
1114
1340
|
callTool:
|
|
@@ -13,6 +13,10 @@ export function createKeyedSingleflight() {
|
|
|
13
13
|
pending.set(lane, tracked);
|
|
14
14
|
return tracked;
|
|
15
15
|
},
|
|
16
|
+
/** The in-flight task promise for `key`, or undefined. */
|
|
17
|
+
wait(key) {
|
|
18
|
+
return pending.get(String(key || ''));
|
|
19
|
+
},
|
|
16
20
|
has(key) {
|
|
17
21
|
return pending.has(String(key || ''));
|
|
18
22
|
},
|