mixdog 0.9.95 → 0.9.97
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +21 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +9 -6
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +31 -33
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +10 -3
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +38 -4
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { resolvePluginData } from '../../shared/plugin-paths.mjs';
|
|
2
2
|
import { readSection, updateSection, updateSectionAsync, getAgentApiKey, AGENT_PROVIDER_ENV } from '../../shared/config.mjs';
|
|
3
|
+
import {
|
|
4
|
+
agentRouteStorageNeedsMigration,
|
|
5
|
+
canonicalizeAgentRouteStorage,
|
|
6
|
+
} from '../../shared/agent-route-config.mjs';
|
|
3
7
|
import { OPENAI_COMPAT_PRESETS } from './providers/openai-compat-presets.mjs';
|
|
4
8
|
import {
|
|
5
9
|
hasAnthropicOAuthCredentials,
|
|
@@ -7,20 +11,6 @@ import {
|
|
|
7
11
|
hasGrokOAuthCredentials,
|
|
8
12
|
} from './providers/oauth-credential-probes.mjs';
|
|
9
13
|
|
|
10
|
-
// Keep config loading free of standalone provider-admin and its provider
|
|
11
|
-
// runtimes. These identifiers mirror that module's static catalog, assembled
|
|
12
|
-
// solely from the lightweight config/provider constants already imported here.
|
|
13
|
-
const CONFIG_PROVIDER_IDS = new Set([
|
|
14
|
-
...Object.keys(AGENT_PROVIDER_ENV),
|
|
15
|
-
...Object.keys(OPENAI_COMPAT_PRESETS),
|
|
16
|
-
'openai-oauth',
|
|
17
|
-
'anthropic-oauth',
|
|
18
|
-
'grok-oauth',
|
|
19
|
-
]);
|
|
20
|
-
function isConfiguredProviderId(provider) {
|
|
21
|
-
return CONFIG_PROVIDER_IDS.has(String(provider || '').trim());
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
// Thin wrapper around resolvePluginData so callers in this orchestrator tree
|
|
25
15
|
// can import a single helper without reaching into shared/.
|
|
26
16
|
export function getPluginData() {
|
|
@@ -33,19 +23,13 @@ export function getPluginData() {
|
|
|
33
23
|
// llm/index.mjs and setup-server.mjs so UI/runtime cannot drift from config.
|
|
34
24
|
//
|
|
35
25
|
// Explore and Maintainer start without a route so they dynamically inherit the
|
|
36
|
-
// Main route.
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// (cycle1/2/3-agent, distinct slots and invokedBy) but resolve their model from
|
|
40
|
-
// `maint.memory` via the `maintKey` override on their hidden-role entries.
|
|
26
|
+
// Main route. Their explicit routes live canonically in `agents.explore` and
|
|
27
|
+
// `agents.maintainer`; load-time migration still accepts the older workflow /
|
|
28
|
+
// maintenance aliases.
|
|
41
29
|
// scheduler/webhook still let a per-entry config.json model win first (the
|
|
42
30
|
// caller passes it explicitly via opts.preset); the haiku default below only
|
|
43
31
|
// applies when an entry omits its own model.
|
|
44
|
-
//
|
|
45
|
-
// UI / allow-list view (GET cleanup + POST validation) and is intentionally a
|
|
46
|
-
// SUBSET of DEFAULT_MAINTENANCE: scheduler/webhook carry a per-entry model and
|
|
47
|
-
// are not shown as shared rows, but still inherit the haiku default above when
|
|
48
|
-
// an entry omits its own model.
|
|
32
|
+
// Legacy route slots accepted only at config ingress for migration.
|
|
49
33
|
const MAINTENANCE_SLOTS = Object.freeze(['explore', 'memory']);
|
|
50
34
|
|
|
51
35
|
// --- User profile (statusline /profile) -------------------------------------
|
|
@@ -213,6 +197,13 @@ function normalizeSearchRoute(route) {
|
|
|
213
197
|
return null;
|
|
214
198
|
const provider = normalizeAgentProviderId(route.provider);
|
|
215
199
|
const model = String(route.model || '').trim();
|
|
200
|
+
if (!provider && model) {
|
|
201
|
+
return {
|
|
202
|
+
provider: 'default',
|
|
203
|
+
model: 'default',
|
|
204
|
+
...(String(route.toolType || '').trim() ? { toolType: String(route.toolType).trim() } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
216
207
|
if (!provider || !model)
|
|
217
208
|
return null;
|
|
218
209
|
const out = { provider, model };
|
|
@@ -230,17 +221,11 @@ function normalizeSearchRoute(route) {
|
|
|
230
221
|
// Normalize stored maintenance slots to the direct {provider, model} route
|
|
231
222
|
// shape (provider/model/effort/fast only). Non-route values are dropped so the
|
|
232
223
|
// DEFAULT_MAINTENANCE route fills the slot.
|
|
233
|
-
function normalizeMaintenanceRoutes(rawMaint
|
|
224
|
+
function normalizeMaintenanceRoutes(rawMaint) {
|
|
234
225
|
const out = {};
|
|
235
|
-
const configuredProvider = String(defaultProvider || '').trim();
|
|
236
|
-
const fallbackProvider = isConfiguredProviderId(configuredProvider)
|
|
237
|
-
? configuredProvider
|
|
238
|
-
: 'anthropic-oauth';
|
|
239
226
|
for (const [slot, value] of Object.entries(rawMaint || {})) {
|
|
240
227
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
241
|
-
const provider = normalizeAgentProviderId(
|
|
242
|
-
value.provider || ((slot === 'explore' || slot === 'memory') ? fallbackProvider : ''),
|
|
243
|
-
);
|
|
228
|
+
const provider = normalizeAgentProviderId(value.provider);
|
|
244
229
|
const model = String(value.model || '').trim();
|
|
245
230
|
if (provider && model) {
|
|
246
231
|
const route = { provider, model };
|
|
@@ -281,6 +266,174 @@ function normalizeRecapConfig(rawRecap) {
|
|
|
281
266
|
return recap;
|
|
282
267
|
}
|
|
283
268
|
|
|
269
|
+
function configObject(value) {
|
|
270
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function nonEmptyConfigObject(value) {
|
|
274
|
+
return Object.keys(value).length > 0 ? value : undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeStoredCompactionType(value) {
|
|
278
|
+
const raw = String(value ?? '').trim().toLowerCase().replace(/_/g, '-');
|
|
279
|
+
if (['1', 'type1', 'type-1', 'semantic', 'summary', 'default'].includes(raw)) return 'semantic';
|
|
280
|
+
if (['2', 'type2', 'type-2', 'recall', 'recall-fast', 'recall-fasttrack',
|
|
281
|
+
'recall-fast-track', 'fasttrack', 'fast-track'].includes(raw)) return 'recall-fasttrack';
|
|
282
|
+
return 'recall-fasttrack';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function canonicalizeAutoClearStorage(value) {
|
|
286
|
+
const raw = configObject(value);
|
|
287
|
+
const next = { ...raw };
|
|
288
|
+
const idleMs = Number(raw.idleMs ?? raw.thresholdMs ?? raw.idleMillis);
|
|
289
|
+
const providerSource = configObject(raw.providerIdleMs ?? raw.providerDefaults ?? raw.providers);
|
|
290
|
+
const providerIdleMs = {};
|
|
291
|
+
for (const [key, candidate] of Object.entries(providerSource)) {
|
|
292
|
+
const provider = String(key || '').trim().toLowerCase();
|
|
293
|
+
const duration = Number(candidate);
|
|
294
|
+
if (!provider || !Number.isFinite(duration) || duration <= 0) continue;
|
|
295
|
+
providerIdleMs[provider] = Math.max(60_000, Math.round(duration));
|
|
296
|
+
}
|
|
297
|
+
delete next.thresholdMs;
|
|
298
|
+
delete next.idleMillis;
|
|
299
|
+
delete next.providerDefaults;
|
|
300
|
+
delete next.providers;
|
|
301
|
+
delete next.custom;
|
|
302
|
+
if (Number.isFinite(idleMs) && idleMs > 0) next.idleMs = Math.max(60_000, Math.round(idleMs));
|
|
303
|
+
else delete next.idleMs;
|
|
304
|
+
if (Object.keys(providerIdleMs).length) next.providerIdleMs = providerIdleMs;
|
|
305
|
+
else delete next.providerIdleMs;
|
|
306
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'enabled')) next.enabled = raw.enabled !== false;
|
|
307
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'minContextPercent')) {
|
|
308
|
+
const percent = Number(raw.minContextPercent);
|
|
309
|
+
if (Number.isFinite(percent)) next.minContextPercent = Math.min(100, Math.max(0, Math.round(percent)));
|
|
310
|
+
else delete next.minContextPercent;
|
|
311
|
+
}
|
|
312
|
+
return nonEmptyConfigObject(next);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function canonicalizeCompactionStorage(value) {
|
|
316
|
+
const raw = configObject(value);
|
|
317
|
+
const next = { ...raw };
|
|
318
|
+
const hasType = ['type', 'compactType', 'compact_type']
|
|
319
|
+
.some((key) => Object.prototype.hasOwnProperty.call(raw, key));
|
|
320
|
+
if (hasType) next.type = normalizeStoredCompactionType(raw.type ?? raw.compactType ?? raw.compact_type);
|
|
321
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'auto')
|
|
322
|
+
|| Object.prototype.hasOwnProperty.call(raw, 'enabled')) {
|
|
323
|
+
next.auto = raw.auto !== false && raw.enabled !== false;
|
|
324
|
+
}
|
|
325
|
+
delete next.compactType;
|
|
326
|
+
delete next.compact_type;
|
|
327
|
+
delete next.enabled;
|
|
328
|
+
return nonEmptyConfigObject(next);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function canonicalizeShellStorage(value) {
|
|
332
|
+
const raw = configObject(value);
|
|
333
|
+
const next = { ...raw };
|
|
334
|
+
const command = String(raw.command ?? raw.path ?? raw.executable ?? raw.shell ?? '').trim();
|
|
335
|
+
delete next.path;
|
|
336
|
+
delete next.executable;
|
|
337
|
+
delete next.shell;
|
|
338
|
+
if (command) next.command = command;
|
|
339
|
+
else delete next.command;
|
|
340
|
+
return nonEmptyConfigObject(next);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function canonicalizeModulesStorage(value) {
|
|
344
|
+
const modules = configObject(value);
|
|
345
|
+
delete modules.memory;
|
|
346
|
+
return nonEmptyConfigObject(modules);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function migrateLegacyModelSettings(raw = {}) {
|
|
350
|
+
const modelSettings = raw.modelSettings && typeof raw.modelSettings === 'object'
|
|
351
|
+
? { ...raw.modelSettings }
|
|
352
|
+
: {};
|
|
353
|
+
const legacyFastModels = raw.fastModels && typeof raw.fastModels === 'object'
|
|
354
|
+
? raw.fastModels
|
|
355
|
+
: {};
|
|
356
|
+
for (const [key, enabled] of Object.entries(legacyFastModels)) {
|
|
357
|
+
if (enabled !== true) continue;
|
|
358
|
+
const current = modelSettings[key];
|
|
359
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
360
|
+
modelSettings[key] = { fast: true };
|
|
361
|
+
} else if (!Object.prototype.hasOwnProperty.call(current, 'fast')) {
|
|
362
|
+
modelSettings[key] = { ...current, fast: true };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return modelSettings;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function canonicalizeLegacyAgentStorage(value = {}) {
|
|
369
|
+
const next = canonicalizeAgentRouteStorage(value);
|
|
370
|
+
next.presets = Array.isArray(next.presets)
|
|
371
|
+
? next.presets.map((preset) => normalizePreset(preset)).filter(Boolean)
|
|
372
|
+
: [];
|
|
373
|
+
next.modelSettings = migrateLegacyModelSettings(value);
|
|
374
|
+
const autoClear = canonicalizeAutoClearStorage(next.autoClear);
|
|
375
|
+
if (autoClear) next.autoClear = autoClear;
|
|
376
|
+
else delete next.autoClear;
|
|
377
|
+
const compaction = canonicalizeCompactionStorage(next.compaction);
|
|
378
|
+
if (compaction) next.compaction = compaction;
|
|
379
|
+
else delete next.compaction;
|
|
380
|
+
const shell = canonicalizeShellStorage(next.shell);
|
|
381
|
+
if (shell) next.shell = shell;
|
|
382
|
+
else delete next.shell;
|
|
383
|
+
if (Object.prototype.hasOwnProperty.call(next, 'profile')) {
|
|
384
|
+
next.profile = normalizeProfileConfig(next.profile);
|
|
385
|
+
}
|
|
386
|
+
if (Object.prototype.hasOwnProperty.call(next, 'skills')) {
|
|
387
|
+
const skills = normalizeSkillsConfig(next.skills);
|
|
388
|
+
if (skills.disabled.length) next.skills = skills;
|
|
389
|
+
else delete next.skills;
|
|
390
|
+
}
|
|
391
|
+
if (!next.searchRoute && value?.search && typeof value.search === 'object') {
|
|
392
|
+
next.searchRoute = value.search;
|
|
393
|
+
}
|
|
394
|
+
next.searchRoute = normalizeSearchRoute(next.searchRoute);
|
|
395
|
+
const modules = next.modules && typeof next.modules === 'object' && !Array.isArray(next.modules)
|
|
396
|
+
? { ...next.modules }
|
|
397
|
+
: {};
|
|
398
|
+
if (Object.prototype.hasOwnProperty.call(modules, 'memory')) {
|
|
399
|
+
const legacyMemory = modules.memory;
|
|
400
|
+
if (!(next.recap && typeof next.recap === 'object' && Object.prototype.hasOwnProperty.call(next.recap, 'enabled'))) {
|
|
401
|
+
const enabled = legacyMemory && typeof legacyMemory === 'object'
|
|
402
|
+
? legacyMemory.enabled !== false
|
|
403
|
+
: legacyMemory !== false;
|
|
404
|
+
next.recap = { ...(next.recap || {}), enabled };
|
|
405
|
+
}
|
|
406
|
+
delete modules.memory;
|
|
407
|
+
}
|
|
408
|
+
if (Object.keys(modules).length) next.modules = modules;
|
|
409
|
+
else delete next.modules;
|
|
410
|
+
delete next.fastModels;
|
|
411
|
+
delete next.agentMaintenance;
|
|
412
|
+
delete next.runtime;
|
|
413
|
+
delete next.search;
|
|
414
|
+
delete next.capabilities;
|
|
415
|
+
delete next.defaultProvider;
|
|
416
|
+
delete next.guide;
|
|
417
|
+
return next;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function agentConfigStorageNeedsMigration(value = {}) {
|
|
421
|
+
const modules = value?.modules;
|
|
422
|
+
const presets = Array.isArray(value?.presets) ? value.presets : [];
|
|
423
|
+
const canonical = canonicalizeLegacyAgentStorage(value);
|
|
424
|
+
const normalizedFields = ['autoClear', 'compaction', 'shell', 'profile', 'skills', 'modules', 'guide'];
|
|
425
|
+
return agentRouteStorageNeedsMigration(value)
|
|
426
|
+
|| Object.prototype.hasOwnProperty.call(value || {}, 'fastModels')
|
|
427
|
+
|| Object.prototype.hasOwnProperty.call(value || {}, 'agentMaintenance')
|
|
428
|
+
|| Object.prototype.hasOwnProperty.call(value || {}, 'runtime')
|
|
429
|
+
|| Object.prototype.hasOwnProperty.call(value || {}, 'search')
|
|
430
|
+
|| Object.prototype.hasOwnProperty.call(value || {}, 'capabilities')
|
|
431
|
+
|| Object.prototype.hasOwnProperty.call(value || {}, 'defaultProvider')
|
|
432
|
+
|| presets.some((preset) => !normalizePreset(preset))
|
|
433
|
+
|| (modules && typeof modules === 'object' && Object.prototype.hasOwnProperty.call(modules, 'memory'))
|
|
434
|
+
|| normalizedFields.some((key) => JSON.stringify(value?.[key]) !== JSON.stringify(canonical?.[key]));
|
|
435
|
+
}
|
|
436
|
+
|
|
284
437
|
export function loadConfig(options = {}) {
|
|
285
438
|
const includeSecrets = options.secrets !== false;
|
|
286
439
|
const sectionRaw = readSection('agent');
|
|
@@ -290,6 +443,8 @@ export function loadConfig(options = {}) {
|
|
|
290
443
|
if (raw.agent && raw.agent.providers) {
|
|
291
444
|
raw = raw.agent;
|
|
292
445
|
}
|
|
446
|
+
const storageNeedsMigration = agentConfigStorageNeedsMigration(raw);
|
|
447
|
+
raw = canonicalizeLegacyAgentStorage(raw);
|
|
293
448
|
const defaults = buildDefaultConfig({ detectCredentials: includeSecrets });
|
|
294
449
|
// Deep-merge provider subkeys: unknown per-provider values are
|
|
295
450
|
// preserved through save/load so future fields round-trip
|
|
@@ -371,27 +526,13 @@ export function loadConfig(options = {}) {
|
|
|
371
526
|
// without overriding an explicit modelSettings.fast value. Keep
|
|
372
527
|
// the compatibility read here, but never expose/persist the
|
|
373
528
|
// legacy container again.
|
|
374
|
-
const modelSettings = raw
|
|
375
|
-
? { ...raw.modelSettings }
|
|
376
|
-
: {};
|
|
377
|
-
const legacyFastModels = raw.fastModels && typeof raw.fastModels === 'object'
|
|
378
|
-
? raw.fastModels
|
|
379
|
-
: {};
|
|
380
|
-
for (const [key, enabled] of Object.entries(legacyFastModels)) {
|
|
381
|
-
if (enabled !== true) continue;
|
|
382
|
-
const current = modelSettings[key];
|
|
383
|
-
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
384
|
-
modelSettings[key] = { fast: true };
|
|
385
|
-
} else if (!Object.prototype.hasOwnProperty.call(current, 'fast')) {
|
|
386
|
-
modelSettings[key] = { ...current, fast: true };
|
|
387
|
-
}
|
|
388
|
-
}
|
|
529
|
+
const modelSettings = migrateLegacyModelSettings(raw);
|
|
389
530
|
const workflowRoutes = raw.workflowRoutes && typeof raw.workflowRoutes === 'object' ? { ...raw.workflowRoutes } : {};
|
|
390
531
|
delete workflowRoutes.search;
|
|
391
532
|
// Normalize maintenance slots to routes, then overlay onto the
|
|
392
533
|
// route-shaped defaults.
|
|
393
|
-
const normalizedMaint = normalizeMaintenanceRoutes(rawMaint
|
|
394
|
-
|
|
534
|
+
const normalizedMaint = normalizeMaintenanceRoutes(rawMaint);
|
|
535
|
+
const loaded = canonicalizeAgentRouteStorage({
|
|
395
536
|
providers: mergedProviders,
|
|
396
537
|
mcpServers,
|
|
397
538
|
mcpProjectOverrides: raw.mcpProjectOverrides && typeof raw.mcpProjectOverrides === 'object' && !Array.isArray(raw.mcpProjectOverrides)
|
|
@@ -418,7 +559,40 @@ export function loadConfig(options = {}) {
|
|
|
418
559
|
update: raw.update && typeof raw.update === 'object' ? { ...raw.update } : {},
|
|
419
560
|
recap: recapConfig,
|
|
420
561
|
modules: raw.modules && typeof raw.modules === 'object' ? { ...raw.modules } : {},
|
|
421
|
-
};
|
|
562
|
+
});
|
|
563
|
+
if (storageNeedsMigration) {
|
|
564
|
+
try {
|
|
565
|
+
// `readSection('agent')` sees cross-section migrations from
|
|
566
|
+
// canonicalizeUnifiedConfig (root autoClear/compaction/
|
|
567
|
+
// shell and memory.user.title), while the in-lock raw
|
|
568
|
+
// section does not. Seed only those missing/partial values
|
|
569
|
+
// from the canonical read snapshot; the current in-lock
|
|
570
|
+
// section still wins for concurrent edits.
|
|
571
|
+
persistAgentConfig((current) => {
|
|
572
|
+
const merged = { ...current };
|
|
573
|
+
for (const key of ['autoClear', 'compaction', 'shell', 'recap']) {
|
|
574
|
+
if (!Object.prototype.hasOwnProperty.call(current, key)
|
|
575
|
+
&& Object.prototype.hasOwnProperty.call(raw, key)) {
|
|
576
|
+
merged[key] = raw[key];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'profile')) {
|
|
580
|
+
const currentProfile = normalizeProfileConfig(current.profile);
|
|
581
|
+
const migratedProfile = normalizeProfileConfig(raw.profile);
|
|
582
|
+
merged.profile = {
|
|
583
|
+
...currentProfile,
|
|
584
|
+
...(!currentProfile.title && migratedProfile.title
|
|
585
|
+
? { title: migratedProfile.title }
|
|
586
|
+
: {}),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return canonicalizeLegacyAgentStorage(merged);
|
|
590
|
+
});
|
|
591
|
+
} catch (err) {
|
|
592
|
+
process.stderr.write(`[config] persist canonical agent config failed: ${err?.message}\n`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return loaded;
|
|
422
596
|
}
|
|
423
597
|
catch { /* fall through */ }
|
|
424
598
|
}
|
|
@@ -430,7 +604,6 @@ export function loadConfig(options = {}) {
|
|
|
430
604
|
presets: DEFAULT_PRESETS.map(p => ({ ...p })),
|
|
431
605
|
default: null,
|
|
432
606
|
maintenance: { ...DEFAULT_MAINTENANCE },
|
|
433
|
-
workflowRoutes: {},
|
|
434
607
|
searchRoute: null,
|
|
435
608
|
modelSettings: {},
|
|
436
609
|
onboarding: {},
|
|
@@ -491,6 +664,7 @@ export async function patchSkillsDisabledAsync(disabledNames) {
|
|
|
491
664
|
}
|
|
492
665
|
|
|
493
666
|
function buildAgentSaveBuilder(config, appliedDirty) {
|
|
667
|
+
const canonicalRoutes = canonicalizeLegacyAgentStorage(config);
|
|
494
668
|
// Strip ephemeral defaults from providers but preserve any unknown
|
|
495
669
|
// per-provider subkey so future schema additions round-trip through the
|
|
496
670
|
// setup UI without changes here. apiKey is intentionally omitted —
|
|
@@ -514,13 +688,15 @@ function buildAgentSaveBuilder(config, appliedDirty) {
|
|
|
514
688
|
persistedProviders[name] = slim;
|
|
515
689
|
}
|
|
516
690
|
}
|
|
517
|
-
const
|
|
518
|
-
?
|
|
519
|
-
: {};
|
|
520
|
-
delete workflowRoutes.search;
|
|
521
|
-
const presets = Array.isArray(config.presets)
|
|
522
|
-
? config.presets.filter(p => p?.id !== 'workflow-search')
|
|
691
|
+
const presets = Array.isArray(canonicalRoutes.presets)
|
|
692
|
+
? canonicalRoutes.presets.filter(p => p?.id !== 'workflow-search')
|
|
523
693
|
: [];
|
|
694
|
+
const profile = normalizeProfileConfig(config.profile);
|
|
695
|
+
const skills = normalizeSkillsConfig(config.skills);
|
|
696
|
+
const autoClear = canonicalizeAutoClearStorage(config.autoClear);
|
|
697
|
+
const compaction = canonicalizeCompactionStorage(config.compaction);
|
|
698
|
+
const shell = canonicalizeShellStorage(config.shell);
|
|
699
|
+
const modules = canonicalizeModulesStorage(config.modules);
|
|
524
700
|
// Build the replacement from `existingRaw` — the section read INSIDE the
|
|
525
701
|
// file lock — not a snapshot taken before it, so unmanaged keys written by
|
|
526
702
|
// a concurrent instance survive the save (lost-update guard).
|
|
@@ -559,27 +735,25 @@ function buildAgentSaveBuilder(config, appliedDirty) {
|
|
|
559
735
|
}
|
|
560
736
|
const next = {
|
|
561
737
|
...existingRaw,
|
|
562
|
-
guide: config.guide || existingRaw.guide || undefined,
|
|
563
738
|
providers: persistedProviders,
|
|
564
739
|
mcpServers,
|
|
565
740
|
mcpProjectOverrides,
|
|
566
741
|
presets,
|
|
567
742
|
default: config.default || null,
|
|
568
|
-
maintenance:
|
|
569
|
-
workflowRoutes,
|
|
743
|
+
maintenance: canonicalRoutes.maintenance,
|
|
570
744
|
searchRoute: normalizeSearchRoute(config.searchRoute),
|
|
571
745
|
modelSettings: config.modelSettings || {},
|
|
572
746
|
onboarding: config.onboarding || {},
|
|
573
|
-
agents:
|
|
747
|
+
agents: canonicalRoutes.agents,
|
|
574
748
|
workflow: config.workflow || { active: 'default' },
|
|
575
|
-
profile
|
|
576
|
-
skills:
|
|
577
|
-
autoClear
|
|
578
|
-
compaction
|
|
579
|
-
shell
|
|
749
|
+
profile,
|
|
750
|
+
skills: skills.disabled.length ? skills : undefined,
|
|
751
|
+
autoClear,
|
|
752
|
+
compaction,
|
|
753
|
+
shell,
|
|
580
754
|
update: config.update || {},
|
|
581
755
|
recap: config.recap || {},
|
|
582
|
-
modules
|
|
756
|
+
modules,
|
|
583
757
|
};
|
|
584
758
|
// These keys were previously round-tripped despite having no runtime
|
|
585
759
|
// consumer. Explicitly remove them from the in-lock baseline so a
|
|
@@ -587,6 +761,11 @@ function buildAgentSaveBuilder(config, appliedDirty) {
|
|
|
587
761
|
delete next.fastModels;
|
|
588
762
|
delete next.agentMaintenance;
|
|
589
763
|
delete next.runtime;
|
|
764
|
+
delete next.workflowRoutes;
|
|
765
|
+
delete next.search;
|
|
766
|
+
delete next.capabilities;
|
|
767
|
+
delete next.defaultProvider;
|
|
768
|
+
delete next.guide;
|
|
590
769
|
return next;
|
|
591
770
|
};
|
|
592
771
|
}
|
|
@@ -8,6 +8,9 @@ import { shutdownStdioChild, killStdioChildTreeFast } from './child-tree.mjs';
|
|
|
8
8
|
import { readServicePort, markServiceUnreachable, isConnRefuseError } from '../../../shared/service-discovery.mjs';
|
|
9
9
|
import { makeToolEnvelope, normalizeToolEnvelope } from '../session/tool-envelope.mjs';
|
|
10
10
|
import { classifyResultKind } from '../session/result-classification.mjs';
|
|
11
|
+
import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
|
|
12
|
+
import { createOwnerFairGate } from '../../../shared/owner-fair-gate.mjs';
|
|
13
|
+
import { currentToolExecutionOwner } from '../../../shared/tool-execution-owner.mjs';
|
|
11
14
|
// --- Types ---
|
|
12
15
|
/** Known auto-detect targets: port file path relative to tmpdir.
|
|
13
16
|
* Note: `mixdog` used to self-loopback via active-instance.json's
|
|
@@ -16,13 +19,15 @@ import { classifyResultKind } from '../session/result-classification.mjs';
|
|
|
16
19
|
* in-process through agent's toolExecutor (see orchestrator/internal-tools),
|
|
17
20
|
* so this registry is for genuinely external port-based MCP targets only. */
|
|
18
21
|
const AUTO_DETECT_PORTS = {
|
|
19
|
-
'mixdog-memory': { discovery: 'memory',
|
|
22
|
+
'mixdog-memory': { discovery: 'memory', endpoint: '/mcp' },
|
|
20
23
|
};
|
|
21
24
|
const DEFAULT_MCP_CALL_TIMEOUT_MS = 120000;
|
|
22
25
|
// Per-server STARTUP handshake budget (connect + listTools): 10s.
|
|
23
26
|
const DEFAULT_MCP_STARTUP_TIMEOUT_MS = 10000;
|
|
24
27
|
// --- State ---
|
|
25
28
|
const servers = new Map();
|
|
29
|
+
const reconnects = createKeyedSingleflight();
|
|
30
|
+
const callAdmissions = new Map();
|
|
26
31
|
let mcpSdkPromise = null;
|
|
27
32
|
// Memo for mcpToolHasField(name, field) — keyed by `${toolName}|${field}`.
|
|
28
33
|
// The lookup (regex parse + servers Map get + tools.find + schema property
|
|
@@ -163,6 +168,45 @@ export function getMcpServerStatus() {
|
|
|
163
168
|
})(),
|
|
164
169
|
}));
|
|
165
170
|
}
|
|
171
|
+
function positiveInt(value, fallback) {
|
|
172
|
+
const parsed = Math.floor(Number(value));
|
|
173
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
174
|
+
}
|
|
175
|
+
function callAdmissionFor(server) {
|
|
176
|
+
let gate = callAdmissions.get(server.name);
|
|
177
|
+
if (gate) return gate;
|
|
178
|
+
const cfg = server?.cfg || {};
|
|
179
|
+
gate = createOwnerFairGate({
|
|
180
|
+
name: `MCP ${server.name}`,
|
|
181
|
+
activeMax: positiveInt(
|
|
182
|
+
cfg.maxConcurrency ?? cfg.max_concurrency ?? process.env.MIXDOG_MCP_MAX_INFLIGHT,
|
|
183
|
+
8,
|
|
184
|
+
),
|
|
185
|
+
queueMax: positiveInt(
|
|
186
|
+
cfg.maxQueue ?? cfg.max_queue ?? process.env.MIXDOG_MCP_MAX_QUEUE,
|
|
187
|
+
256,
|
|
188
|
+
),
|
|
189
|
+
minOwnerQueue: 8,
|
|
190
|
+
waitTimeoutMs: positiveInt(
|
|
191
|
+
cfg.waitTimeoutMs ?? cfg.wait_timeout_ms ?? process.env.MIXDOG_MCP_WAIT_TIMEOUT_MS,
|
|
192
|
+
30_000,
|
|
193
|
+
),
|
|
194
|
+
});
|
|
195
|
+
callAdmissions.set(server.name, gate);
|
|
196
|
+
return gate;
|
|
197
|
+
}
|
|
198
|
+
function closeCallAdmission(name, reason) {
|
|
199
|
+
const gate = callAdmissions.get(name);
|
|
200
|
+
if (!gate) return;
|
|
201
|
+
callAdmissions.delete(name);
|
|
202
|
+
gate.close(reason || `MCP ${name} disconnected`);
|
|
203
|
+
}
|
|
204
|
+
export function getMcpAdmissionSnapshot() {
|
|
205
|
+
return [...callAdmissions.entries()].map(([name, gate]) => ({
|
|
206
|
+
name,
|
|
207
|
+
...gate.snapshot(),
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
166
210
|
|
|
167
211
|
/** Snapshot of MCP initialize `instructions` per connected server (handshake time). */
|
|
168
212
|
export function getMcpServerInstructionsMap() {
|
|
@@ -173,11 +217,31 @@ export function getMcpServerInstructionsMap() {
|
|
|
173
217
|
}
|
|
174
218
|
return out;
|
|
175
219
|
}
|
|
220
|
+
|
|
221
|
+
async function reconnectMcpServer(serverName, failedServer) {
|
|
222
|
+
return reconnects.run(serverName, async () => {
|
|
223
|
+
const current = servers.get(serverName);
|
|
224
|
+
// A peer already completed the replacement while this failed call was
|
|
225
|
+
// unwinding. Reuse it immediately instead of closing a fresh transport.
|
|
226
|
+
if (current && current !== failedServer) return current;
|
|
227
|
+
if (current) {
|
|
228
|
+
await _closeServer(current);
|
|
229
|
+
if (servers.get(serverName) === current) servers.delete(serverName);
|
|
230
|
+
}
|
|
231
|
+
await connectServer(serverName, failedServer.cfg);
|
|
232
|
+
const replacement = servers.get(serverName);
|
|
233
|
+
if (!replacement) {
|
|
234
|
+
throw new Error(`reconnect succeeded but server "${serverName}" entry is missing from registry`);
|
|
235
|
+
}
|
|
236
|
+
return replacement;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
176
240
|
/**
|
|
177
241
|
* Execute an MCP tool call.
|
|
178
242
|
* Name format: `mcp__{serverName}__{toolName}`
|
|
179
243
|
*/
|
|
180
|
-
export async function executeMcpTool(name, args) {
|
|
244
|
+
export async function executeMcpTool(name, args, options = {}) {
|
|
181
245
|
// Parse: mcp__{server}__{tool}
|
|
182
246
|
const match = name.match(/^mcp__(.+?)__(.+)$/);
|
|
183
247
|
if (!match)
|
|
@@ -186,42 +250,40 @@ export async function executeMcpTool(name, args) {
|
|
|
186
250
|
const server = servers.get(serverName);
|
|
187
251
|
if (!server)
|
|
188
252
|
throw new Error(`MCP server "${serverName}" not connected`);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
result
|
|
192
|
-
} catch (firstErr) {
|
|
193
|
-
const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
194
|
-
if (isMcpToolCallTimeoutError(firstErr)) {
|
|
195
|
-
mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
|
|
196
|
-
throw firstErr;
|
|
197
|
-
}
|
|
198
|
-
mcpLog(`[mcp-client] Tool call failed, attempting reconnect...\n`);
|
|
199
|
-
await new Promise(r => setTimeout(r, 500));
|
|
200
|
-
try {
|
|
201
|
-
await _closeServer(server);
|
|
202
|
-
} catch { /* ignore close error */ }
|
|
253
|
+
const gate = callAdmissionFor(server);
|
|
254
|
+
return gate.run(options?.ownerKey || currentToolExecutionOwner(), async () => {
|
|
255
|
+
let result;
|
|
203
256
|
try {
|
|
204
|
-
await
|
|
205
|
-
} catch (
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
257
|
+
result = await _callToolWithTimeout(server, toolName, args);
|
|
258
|
+
} catch (firstErr) {
|
|
259
|
+
const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
260
|
+
if (isMcpToolCallTimeoutError(firstErr)) {
|
|
261
|
+
mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
|
|
262
|
+
throw firstErr;
|
|
263
|
+
}
|
|
264
|
+
mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
|
|
265
|
+
let retryServer;
|
|
266
|
+
try {
|
|
267
|
+
retryServer = await reconnectMcpServer(serverName, server);
|
|
268
|
+
} catch (reconnectErr) {
|
|
269
|
+
const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
|
|
270
|
+
throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
result = await _callToolWithTimeout(retryServer, toolName, args);
|
|
274
|
+
} catch (retryErr) {
|
|
275
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
276
|
+
throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
|
|
277
|
+
}
|
|
218
278
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
279
|
+
const normalized = normalizeToolEnvelope(normalizeMcpToolResult(result));
|
|
280
|
+
const text = capMcpOutput(normalized.result);
|
|
281
|
+
return normalized.explicitSuccess
|
|
282
|
+
? makeToolEnvelope(text, [], { explicitSuccess: true })
|
|
283
|
+
: text;
|
|
284
|
+
}, {
|
|
285
|
+
signal: options?.signal || null,
|
|
286
|
+
});
|
|
225
287
|
}
|
|
226
288
|
|
|
227
289
|
// Preserve MCP failure metadata across the object→string boundary. The
|
|
@@ -410,6 +472,7 @@ export async function disconnectAll() {
|
|
|
410
472
|
}
|
|
411
473
|
catch { /* ignore */ }
|
|
412
474
|
servers.delete(name);
|
|
475
|
+
closeCallAdmission(name, `MCP ${name} disconnected`);
|
|
413
476
|
}
|
|
414
477
|
_invalidateMcpToolFieldMemo();
|
|
415
478
|
}
|
|
@@ -427,6 +490,7 @@ export async function disconnectMcpServer(name) {
|
|
|
427
490
|
}
|
|
428
491
|
catch { /* ignore */ }
|
|
429
492
|
servers.delete(name);
|
|
493
|
+
closeCallAdmission(name, `MCP ${name} disconnected`);
|
|
430
494
|
_invalidateMcpToolFieldMemo();
|
|
431
495
|
return true;
|
|
432
496
|
}
|
|
@@ -479,13 +543,11 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
|
|
|
479
543
|
const spec = AUTO_DETECT_PORTS[cfg.autoDetect];
|
|
480
544
|
if (!spec)
|
|
481
545
|
throw new Error(`Unknown autoDetect target: "${cfg.autoDetect}"`);
|
|
482
|
-
//
|
|
483
|
-
// pid-validated. Fall back to the legacy active-instance.json portField
|
|
484
|
-
// when no live advert is present (cross-version compat).
|
|
546
|
+
// Read the pid-validated single-writer discovery advert.
|
|
485
547
|
let port = spec.discovery ? readServicePort(spec.discovery, { requirePid: false }) : null;
|
|
486
548
|
if (port && spec.discovery) _autoDetectAdvert = { service: spec.discovery, port };
|
|
487
549
|
let portFile = null;
|
|
488
|
-
if (!port) {
|
|
550
|
+
if (!port && spec.file) {
|
|
489
551
|
portFile = spec.dir === 'mixdog' && process.env.MIXDOG_RUNTIME_ROOT
|
|
490
552
|
? join(process.env.MIXDOG_RUNTIME_ROOT, spec.file)
|
|
491
553
|
: join(tmpdir(), spec.dir, spec.file);
|
|
@@ -510,6 +572,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
|
|
|
510
572
|
port = parseInt(raw, 10);
|
|
511
573
|
}
|
|
512
574
|
}
|
|
575
|
+
if (!port) throw new Error(`autoDetect server "${name}": live service advert missing`);
|
|
513
576
|
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
514
577
|
throw new Error(`autoDetect server "${name}": invalid port value${portFile ? ` in ${portFile}` : ''}`);
|
|
515
578
|
}
|
|
@@ -632,7 +695,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
|
|
|
632
695
|
...(t.annotations && typeof t.annotations === 'object' ? { annotations: t.annotations } : {}),
|
|
633
696
|
}));
|
|
634
697
|
const toolNames = tools.map(t => t.name);
|
|
635
|
-
servers.set(name, { name, client, transport, tools, cfg, instructions });
|
|
698
|
+
servers.set(name, { name, client, transport, tools, cfg, instructions, generation: genAtStart });
|
|
636
699
|
_invalidateMcpToolFieldMemo();
|
|
637
700
|
mcpLog(`[mcp] connected: ${tools.length} tools — ${toolNames.join(', ')}\n`);
|
|
638
701
|
}
|