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,45 +1,108 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
import { availableParallelism } from 'node:os';
|
|
4
|
+
import { createOwnerFairGate } from './owner-fair-gate.mjs';
|
|
5
|
+
import { currentToolExecutionOwner } from './tool-execution-owner.mjs';
|
|
6
|
+
|
|
3
7
|
// ── Module-global child-spawn semaphore ──────────────────────────────────
|
|
4
8
|
//
|
|
5
9
|
// Single-daemon premise: tool execution is in-process, so a module-level
|
|
6
10
|
// singleton semaphore CAN bound the number of concurrent child processes (rg,
|
|
7
11
|
// mixdog-graph, …) across ALL agents/workers in this daemon.
|
|
8
12
|
//
|
|
9
|
-
// Default:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// Default: Windows search scales conservatively from 1..4 with CPU count;
|
|
14
|
+
// code-graph remains 1. Other platforms are unbounded. Windows measurements
|
|
15
|
+
// showed both extremes are poor: cap 1 creates multi-session queue latency,
|
|
16
|
+
// while unbounded multithreaded rg amplifies Defender/process-scan and disk
|
|
17
|
+
// contention. Separate lanes isolate short search from long graph builds.
|
|
18
|
+
// MIXDOG_CHILD_SPAWN_MAX_INFLIGHT overrides globally.
|
|
15
19
|
//
|
|
16
|
-
// IMPORTANT:
|
|
17
|
-
// on any tool JSON schema / tool parameter surface.
|
|
18
|
-
//
|
|
20
|
+
// IMPORTANT: these are resource-control knobs and are deliberately NOT exposed
|
|
21
|
+
// on any tool JSON schema / tool parameter surface. Operators may set the
|
|
22
|
+
// shared MIXDOG_CHILD_SPAWN_MAX_INFLIGHT fallback or a lane-specific override.
|
|
19
23
|
|
|
20
|
-
function
|
|
21
|
-
const override = Number(
|
|
24
|
+
export function resolveDefaultChildSpawnMaxInflight(env = process.env, platform = process.platform) {
|
|
25
|
+
const override = Number(env.MIXDOG_CHILD_SPAWN_MAX_INFLIGHT);
|
|
22
26
|
if (Number.isFinite(override) && override >= 1) return Math.floor(override);
|
|
23
|
-
return
|
|
27
|
+
return platform === 'win32' ? 1 : Infinity;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveDefaultChildSpawnLaneMaxInflight(
|
|
31
|
+
laneName,
|
|
32
|
+
env = process.env,
|
|
33
|
+
platform = process.platform,
|
|
34
|
+
parallelism = availableParallelism(),
|
|
35
|
+
) {
|
|
36
|
+
const lane = _laneName(laneName);
|
|
37
|
+
const key = lane.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
|
|
38
|
+
const laneOverride = Number(env[`MIXDOG_CHILD_SPAWN_${key}_MAX_INFLIGHT`]);
|
|
39
|
+
if (Number.isFinite(laneOverride) && laneOverride >= 1) return Math.floor(laneOverride);
|
|
40
|
+
const sharedOverride = Number(env.MIXDOG_CHILD_SPAWN_MAX_INFLIGHT);
|
|
41
|
+
if (Number.isFinite(sharedOverride) && sharedOverride >= 1) return Math.floor(sharedOverride);
|
|
42
|
+
if (platform !== 'win32') return Infinity;
|
|
43
|
+
if (lane !== 'search') return 1;
|
|
44
|
+
// Multiple rg processes beat one under multi-session load, but each process
|
|
45
|
+
// is itself threaded and Defender amplifies excess fan-out. Scale slowly and
|
|
46
|
+
// cap at four: 4 cores→1, 8→2, 12→3, 16+→4.
|
|
47
|
+
const cpus = Math.max(1, Math.floor(Number(parallelism) || 1));
|
|
48
|
+
return Math.max(1, Math.min(4, Math.floor(cpus / 4)));
|
|
24
49
|
}
|
|
25
50
|
|
|
26
|
-
const
|
|
51
|
+
const DEFAULT_MAX_INFLIGHT = resolveDefaultChildSpawnMaxInflight();
|
|
52
|
+
const LANE_ALIASES = new Map([
|
|
53
|
+
['default', 'search'],
|
|
54
|
+
['build', 'code-graph'],
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
function _laneName(name) {
|
|
58
|
+
const clean = String(name || 'search').trim().toLowerCase() || 'search';
|
|
59
|
+
return LANE_ALIASES.get(clean) || clean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _laneLimit(name) {
|
|
63
|
+
return resolveDefaultChildSpawnLaneMaxInflight(name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _laneSetting(name, suffix, fallback) {
|
|
67
|
+
const key = _laneName(name).toUpperCase().replace(/[^A-Z0-9]+/g, '_');
|
|
68
|
+
const laneValue = Number(process.env[`MIXDOG_CHILD_SPAWN_${key}_${suffix}`]);
|
|
69
|
+
if (Number.isFinite(laneValue) && laneValue >= 1) return Math.floor(laneValue);
|
|
70
|
+
const sharedValue = Number(process.env[`MIXDOG_CHILD_SPAWN_${suffix}`]);
|
|
71
|
+
return Number.isFinite(sharedValue) && sharedValue >= 1
|
|
72
|
+
? Math.floor(sharedValue)
|
|
73
|
+
: fallback;
|
|
74
|
+
}
|
|
27
75
|
|
|
28
76
|
// ── Lanes ────────────────────────────────────────────────────────────────
|
|
29
|
-
//
|
|
30
|
-
// '
|
|
31
|
-
// '
|
|
32
|
-
// graph build can hold its slot for seconds-to-minutes;
|
|
33
|
-
//
|
|
77
|
+
// Independent optional limits and queues:
|
|
78
|
+
// 'search' — short-lived rg processes.
|
|
79
|
+
// 'code-graph' — long-lived graph workers / native graph binaries. A cold
|
|
80
|
+
// graph build can hold its slot for seconds-to-minutes; a
|
|
81
|
+
// single shared lane would let one build
|
|
34
82
|
// stall every rg spawn behind it. Separate lanes remove that
|
|
35
83
|
// starvation class while keeping each spawn family bounded.
|
|
36
|
-
function _makeLane() {
|
|
37
|
-
|
|
84
|
+
function _makeLane(name) {
|
|
85
|
+
const limit = _laneLimit(name);
|
|
86
|
+
const queueMax = _laneSetting(name, 'MAX_QUEUE', 1024);
|
|
87
|
+
const waitTimeoutMs = _laneSetting(name, 'WAIT_TIMEOUT_MS', 30_000);
|
|
88
|
+
return {
|
|
89
|
+
limit,
|
|
90
|
+
queueMax,
|
|
91
|
+
waitTimeoutMs,
|
|
92
|
+
gate: createOwnerFairGate({
|
|
93
|
+
name: `${name} child spawn`,
|
|
94
|
+
activeMax: limit,
|
|
95
|
+
queueMax,
|
|
96
|
+
minOwnerQueue: Math.max(8, Math.floor(queueMax / 32)),
|
|
97
|
+
waitTimeoutMs,
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
38
100
|
}
|
|
39
101
|
const _lanes = new Map();
|
|
40
102
|
function _lane(name) {
|
|
41
|
-
|
|
42
|
-
|
|
103
|
+
const normalized = _laneName(name);
|
|
104
|
+
let lane = _lanes.get(normalized);
|
|
105
|
+
if (!lane) { lane = _makeLane(normalized); _lanes.set(normalized, lane); }
|
|
43
106
|
return lane;
|
|
44
107
|
}
|
|
45
108
|
|
|
@@ -61,24 +124,12 @@ function _maybeWarnSlow(waitedMs, laneName, lane) {
|
|
|
61
124
|
_lastSlowWarnAt = now;
|
|
62
125
|
try {
|
|
63
126
|
process.stderr.write(
|
|
64
|
-
`[child-spawn-gate] lane=${laneName} queue wait ${waitedMs}ms (inflight cap=${
|
|
65
|
-
+ 'raise
|
|
127
|
+
`[child-spawn-gate] lane=${laneName} queue wait ${waitedMs}ms (inflight cap=${lane.limit}, queued=${lane.queue.length}); `
|
|
128
|
+
+ 'raise the lane-specific MIXDOG_CHILD_SPAWN_*_MAX_INFLIGHT if this persists\n',
|
|
66
129
|
);
|
|
67
130
|
} catch { /* ignore */ }
|
|
68
131
|
}
|
|
69
132
|
|
|
70
|
-
function _drain(laneName, lane) {
|
|
71
|
-
while (lane.inflight < MAX_INFLIGHT && lane.queue.length > 0) {
|
|
72
|
-
const waiter = lane.queue.shift();
|
|
73
|
-
if (waiter.onAbort && waiter.signal) {
|
|
74
|
-
try { waiter.signal.removeEventListener('abort', waiter.onAbort); } catch { /* ignore */ }
|
|
75
|
-
}
|
|
76
|
-
lane.inflight++;
|
|
77
|
-
_maybeWarnSlow(Date.now() - waiter.enqueuedAt, laneName, lane);
|
|
78
|
-
waiter.resolve();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
133
|
/**
|
|
83
134
|
* Acquire one child-spawn slot. Resolves immediately when below the cap,
|
|
84
135
|
* otherwise queues until a slot frees. The returned function releases the
|
|
@@ -89,45 +140,37 @@ function _drain(laneName, lane) {
|
|
|
89
140
|
* call release().
|
|
90
141
|
*
|
|
91
142
|
* @param {AbortSignal | null} [signal]
|
|
92
|
-
* @param {string} [laneName] — '
|
|
143
|
+
* @param {string} [laneName] — 'search' (rg) or 'code-graph'.
|
|
93
144
|
* @returns {Promise<() => void>}
|
|
94
145
|
*/
|
|
95
|
-
export function acquire(signal = null, laneName = '
|
|
146
|
+
export function acquire(signal = null, laneName = 'search', options = {}) {
|
|
96
147
|
if (signal && signal.aborted) {
|
|
97
148
|
return Promise.reject(signal.reason ?? _abortError());
|
|
98
149
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
if (signal) {
|
|
121
|
-
waiter.onAbort = () => {
|
|
122
|
-
const idx = lane.queue.indexOf(waiter);
|
|
123
|
-
if (idx !== -1) lane.queue.splice(idx, 1);
|
|
124
|
-
try { signal.removeEventListener('abort', waiter.onAbort); } catch { /* ignore */ }
|
|
125
|
-
reject(signal.reason ?? _abortError());
|
|
126
|
-
};
|
|
127
|
-
try { signal.addEventListener('abort', waiter.onAbort, { once: true }); } catch { /* ignore */ }
|
|
128
|
-
}
|
|
129
|
-
lane.queue.push(waiter);
|
|
150
|
+
const normalizedLaneName = _laneName(laneName);
|
|
151
|
+
const lane = _lane(normalizedLaneName);
|
|
152
|
+
const admitted = Promise.withResolvers();
|
|
153
|
+
const held = Promise.withResolvers();
|
|
154
|
+
let released = false;
|
|
155
|
+
const release = () => {
|
|
156
|
+
if (released) return;
|
|
157
|
+
released = true;
|
|
158
|
+
held.resolve();
|
|
159
|
+
};
|
|
160
|
+
const ownerKey = options?.ownerKey || currentToolExecutionOwner();
|
|
161
|
+
const running = lane.gate.run(ownerKey, async () => {
|
|
162
|
+
admitted.resolve(release);
|
|
163
|
+
await held.promise;
|
|
164
|
+
}, {
|
|
165
|
+
signal,
|
|
166
|
+
waitTimeoutMs: options?.waitTimeoutMs ?? lane.waitTimeoutMs,
|
|
167
|
+
onAdmit: (waitedMs) => _maybeWarnSlow(waitedMs, normalizedLaneName, {
|
|
168
|
+
limit: lane.limit,
|
|
169
|
+
queue: { length: lane.gate.queued },
|
|
170
|
+
}),
|
|
130
171
|
});
|
|
172
|
+
running.catch((error) => admitted.reject(error));
|
|
173
|
+
return admitted.promise;
|
|
131
174
|
}
|
|
132
175
|
|
|
133
176
|
function _abortError() {
|
|
@@ -147,9 +190,33 @@ function _abortError() {
|
|
|
147
190
|
* best-effort work. Real queries must use acquire()/withGate() and never gate
|
|
148
191
|
* themselves on this.
|
|
149
192
|
*/
|
|
150
|
-
export function hasSpareCapacity(laneName = '
|
|
193
|
+
export function hasSpareCapacity(laneName = 'search') {
|
|
151
194
|
const lane = _lane(laneName);
|
|
152
|
-
return lane.
|
|
195
|
+
return lane.gate.active < lane.limit && lane.gate.queued === 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function snapshot() {
|
|
199
|
+
return {
|
|
200
|
+
maxInflight: Number.isFinite(DEFAULT_MAX_INFLIGHT) ? DEFAULT_MAX_INFLIGHT : null,
|
|
201
|
+
lanes: [..._lanes.entries()].map(([name, lane]) => {
|
|
202
|
+
const state = lane.gate.snapshot();
|
|
203
|
+
return {
|
|
204
|
+
name,
|
|
205
|
+
inflight: state.active,
|
|
206
|
+
queued: state.queued,
|
|
207
|
+
limit: Number.isFinite(lane.limit) ? lane.limit : null,
|
|
208
|
+
queueMax: lane.queueMax,
|
|
209
|
+
waitTimeoutMs: lane.waitTimeoutMs,
|
|
210
|
+
owners: state.owners,
|
|
211
|
+
oldestWaitMs: state.oldestWaitMs,
|
|
212
|
+
admitted: state.admitted,
|
|
213
|
+
rejected: state.rejected,
|
|
214
|
+
timedOut: state.timedOut,
|
|
215
|
+
averageWaitMs: state.averageWaitMs,
|
|
216
|
+
maxWaitMs: state.maxWaitMs,
|
|
217
|
+
};
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
153
220
|
}
|
|
154
221
|
|
|
155
222
|
/**
|
|
@@ -162,7 +229,7 @@ export function hasSpareCapacity(laneName = 'default') {
|
|
|
162
229
|
* @param {string} [laneName]
|
|
163
230
|
* @returns {Promise<T>}
|
|
164
231
|
*/
|
|
165
|
-
async function withGate(fn, signal = null, laneName = '
|
|
232
|
+
async function withGate(fn, signal = null, laneName = 'search') {
|
|
166
233
|
const release = await acquire(signal, laneName);
|
|
167
234
|
try {
|
|
168
235
|
return await fn({ signal: signal || null });
|
|
@@ -117,6 +117,107 @@ function isPlainObject(value) {
|
|
|
117
117
|
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function hasOwn(value, key) {
|
|
121
|
+
return Object.prototype.hasOwnProperty.call(value || {}, key)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Canonical on-disk channel shape. Provider-specific ids are durable. Schedule
|
|
125
|
+
// arrays moved to the scheduler DB and prompt injection moved to runtime hooks,
|
|
126
|
+
// so neither belongs in config anymore.
|
|
127
|
+
export function canonicalizeStoredChannelsConfig(value = {}) {
|
|
128
|
+
const next = isPlainObject(value) ? { ...value } : {}
|
|
129
|
+
delete next.promptInjection
|
|
130
|
+
delete next.schedules
|
|
131
|
+
delete next.nonInteractive
|
|
132
|
+
delete next.interactive
|
|
133
|
+
|
|
134
|
+
const channel = isPlainObject(next.channel) ? { ...next.channel } : {}
|
|
135
|
+
next.provider = next.provider === 'telegram' || next.backend === 'telegram'
|
|
136
|
+
? 'telegram'
|
|
137
|
+
: 'discord'
|
|
138
|
+
delete next.backend
|
|
139
|
+
delete channel.channelId
|
|
140
|
+
next.channel = channel
|
|
141
|
+
return next
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Normalize cross-section legacy keys without touching independent domains
|
|
145
|
+
// (ui/theme, desktop, voice, channels access, memory schedules/embedding, …).
|
|
146
|
+
// Reads use this shape in-memory; every subsequent locked write persists it.
|
|
147
|
+
export function canonicalizeUnifiedConfig(value = {}) {
|
|
148
|
+
const next = isPlainObject(value) ? { ...value } : {}
|
|
149
|
+
const hadAgent = isPlainObject(next.agent)
|
|
150
|
+
const agent = hadAgent ? { ...next.agent } : {}
|
|
151
|
+
|
|
152
|
+
if (!String(next.outputStyle || '').trim() && String(agent.outputStyle || '').trim()) {
|
|
153
|
+
next.outputStyle = agent.outputStyle
|
|
154
|
+
}
|
|
155
|
+
delete agent.outputStyle
|
|
156
|
+
|
|
157
|
+
for (const key of ['autoClear', 'compaction', 'shell']) {
|
|
158
|
+
if (!isPlainObject(agent[key]) && isPlainObject(next[key])) agent[key] = next[key]
|
|
159
|
+
delete next[key]
|
|
160
|
+
}
|
|
161
|
+
if (isPlainObject(next.mcpServers)) {
|
|
162
|
+
agent.mcpServers = {
|
|
163
|
+
...next.mcpServers,
|
|
164
|
+
...(isPlainObject(agent.mcpServers) ? agent.mcpServers : {}),
|
|
165
|
+
}
|
|
166
|
+
delete next.mcpServers
|
|
167
|
+
}
|
|
168
|
+
if (!isPlainObject(agent.searchRoute) && isPlainObject(agent.search)) {
|
|
169
|
+
agent.searchRoute = agent.search
|
|
170
|
+
}
|
|
171
|
+
delete agent.search
|
|
172
|
+
delete agent.capabilities
|
|
173
|
+
|
|
174
|
+
const modules = isPlainObject(agent.modules) ? { ...agent.modules } : {}
|
|
175
|
+
if (hasOwn(modules, 'memory')) {
|
|
176
|
+
const legacyMemory = modules.memory
|
|
177
|
+
const explicitRecap = isPlainObject(agent.recap) && hasOwn(agent.recap, 'enabled')
|
|
178
|
+
if (!explicitRecap) {
|
|
179
|
+
const enabled = isPlainObject(legacyMemory)
|
|
180
|
+
? legacyMemory.enabled !== false
|
|
181
|
+
: legacyMemory !== false
|
|
182
|
+
agent.recap = { ...(isPlainObject(agent.recap) ? agent.recap : {}), enabled }
|
|
183
|
+
}
|
|
184
|
+
delete modules.memory
|
|
185
|
+
agent.modules = modules
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (isPlainObject(next.memory)) {
|
|
189
|
+
const memory = { ...next.memory }
|
|
190
|
+
const user = isPlainObject(memory.user) ? { ...memory.user } : {}
|
|
191
|
+
const legacyTitle = String(user.title || '').trim()
|
|
192
|
+
if (legacyTitle && !String(agent.profile?.title || '').trim()) {
|
|
193
|
+
agent.profile = { ...(isPlainObject(agent.profile) ? agent.profile : {}), title: legacyTitle }
|
|
194
|
+
}
|
|
195
|
+
delete user.title
|
|
196
|
+
if (Object.keys(user).length) memory.user = user
|
|
197
|
+
else delete memory.user
|
|
198
|
+
if (hasOwn(memory, 'enabled')) {
|
|
199
|
+
const explicitRecap = isPlainObject(agent.recap) && hasOwn(agent.recap, 'enabled')
|
|
200
|
+
if (!explicitRecap) {
|
|
201
|
+
agent.recap = { ...(isPlainObject(agent.recap) ? agent.recap : {}), enabled: memory.enabled !== false }
|
|
202
|
+
}
|
|
203
|
+
delete memory.enabled
|
|
204
|
+
}
|
|
205
|
+
if (Object.keys(memory).length) next.memory = memory
|
|
206
|
+
else delete next.memory
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isPlainObject(next.channels)) {
|
|
210
|
+
next.channels = canonicalizeStoredChannelsConfig(next.channels)
|
|
211
|
+
}
|
|
212
|
+
// Both sections are retired. `search` had only a fixed timeout that the
|
|
213
|
+
// runtime now owns, while `capabilities.homeAccess` never had a live path
|
|
214
|
+
// consumer or settings writer. Do not keep inert user-facing config.
|
|
215
|
+
delete next.search
|
|
216
|
+
delete next.capabilities
|
|
217
|
+
if (hadAgent || Object.keys(agent).length) next.agent = agent
|
|
218
|
+
return next
|
|
219
|
+
}
|
|
220
|
+
|
|
120
221
|
export function stripGeneratedMarker(data) {
|
|
121
222
|
if (!isPlainObject(data) || !Object.prototype.hasOwnProperty.call(data, GENERATED_KEY)) return data
|
|
122
223
|
const { [GENERATED_KEY]: _generated, ...rest } = data
|
|
@@ -188,7 +289,7 @@ function writeJsonFile(path, data) {
|
|
|
188
289
|
|
|
189
290
|
function readAll() {
|
|
190
291
|
const parsed = readJsonFile(CONFIG_PATH)
|
|
191
|
-
if (parsed != null) return parsed
|
|
292
|
+
if (parsed != null) return canonicalizeUnifiedConfig(parsed)
|
|
192
293
|
// Symmetric with readAllForRmW(): a missing/unreadable config on a data
|
|
193
294
|
// dir that was previously initialized means the file was LOST (deleted,
|
|
194
295
|
// failed write), not a fresh install. Self-heal from the newest
|
|
@@ -202,7 +303,7 @@ function readAll() {
|
|
|
202
303
|
const restored = loadLatestMixdogConfigFromBackup(DATA_DIR)
|
|
203
304
|
if (restored && isPlainObject(restored)) {
|
|
204
305
|
process.stderr.write('[config] read: restored mixdog-config.json from latest user-data backup (missing after init)\n')
|
|
205
|
-
return restored
|
|
306
|
+
return canonicalizeUnifiedConfig(restored)
|
|
206
307
|
}
|
|
207
308
|
}
|
|
208
309
|
return {}
|
|
@@ -259,7 +360,7 @@ function readAllForRmW() {
|
|
|
259
360
|
}
|
|
260
361
|
|
|
261
362
|
function writeAll(data) {
|
|
262
|
-
writeJsonFile(CONFIG_PATH, data)
|
|
363
|
+
writeJsonFile(CONFIG_PATH, canonicalizeUnifiedConfig(data))
|
|
263
364
|
}
|
|
264
365
|
|
|
265
366
|
// Serialize a read-modify-write under the same file lock. Concurrent
|
|
@@ -268,10 +369,13 @@ function writeAll(data) {
|
|
|
268
369
|
// silently clobber the earlier section update. Holding the lock across
|
|
269
370
|
// read+modify+write keeps RMW linearizable.
|
|
270
371
|
function withConfigLock(fn) {
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
372
|
+
// The lock file itself carries no secret — only `<pid> <ts> <token>`, an
|
|
373
|
+
// integrity marker. Clamping IT owner-only meant two synchronous icacls
|
|
374
|
+
// spawns (plus whoami) INSIDE the critical section on every config write:
|
|
375
|
+
// hundreds of ms with the daemon's event loop parked and every other writer
|
|
376
|
+
// queued behind it. The CONFIG file keeps secret:true, which is where the
|
|
377
|
+
// API keys actually live.
|
|
378
|
+
return withFileLockSync(`${CONFIG_PATH}.lock`, fn)
|
|
275
379
|
}
|
|
276
380
|
|
|
277
381
|
export function readSection(section) {
|
|
@@ -288,7 +392,7 @@ export function updateConfig(updater) {
|
|
|
288
392
|
const current = stripGeneratedMarker(readAllForRmW()) || {}
|
|
289
393
|
const next = typeof updater === 'function' ? updater({ ...current }) : updater
|
|
290
394
|
if (!isPlainObject(next)) throw new Error('[config] updateConfig updater must return an object')
|
|
291
|
-
saved = stripGeneratedMarker(next) || {}
|
|
395
|
+
saved = canonicalizeUnifiedConfig(stripGeneratedMarker(next) || {})
|
|
292
396
|
writeAll(saved)
|
|
293
397
|
})
|
|
294
398
|
return saved
|
|
@@ -335,13 +439,13 @@ async function writeJsonFileAsync(path, data) {
|
|
|
335
439
|
}
|
|
336
440
|
|
|
337
441
|
async function writeAllAsync(data) {
|
|
338
|
-
await writeJsonFileAsync(CONFIG_PATH, data)
|
|
442
|
+
await writeJsonFileAsync(CONFIG_PATH, canonicalizeUnifiedConfig(data))
|
|
339
443
|
}
|
|
340
444
|
|
|
341
|
-
// secret:true clamps the shared-home lock file owner-only on win32 via the
|
|
342
|
-
// ASYNC icacls variant inside withFileLock (fail-closed, non-blocking).
|
|
343
445
|
function withConfigLockAsync(fn) {
|
|
344
|
-
|
|
446
|
+
// Same reasoning as withConfigLock: the ACL belongs on the config file, not
|
|
447
|
+
// on its lock marker, and icacls has no business inside the lock.
|
|
448
|
+
return withFileLock(`${CONFIG_PATH}.lock`, fn)
|
|
345
449
|
}
|
|
346
450
|
|
|
347
451
|
// Process-wide registry at the lock-owning layer: every public async RMW path
|
|
@@ -376,7 +480,7 @@ export function updateConfigAsync(updater) {
|
|
|
376
480
|
const current = stripGeneratedMarker(readAllForRmW()) || {}
|
|
377
481
|
const next = typeof updater === 'function' ? updater({ ...current }) : updater
|
|
378
482
|
if (!isPlainObject(next)) throw new Error('[config] updateConfigAsync updater must return an object')
|
|
379
|
-
saved = stripGeneratedMarker(next) || {}
|
|
483
|
+
saved = canonicalizeUnifiedConfig(stripGeneratedMarker(next) || {})
|
|
380
484
|
await writeAllAsync(saved)
|
|
381
485
|
})
|
|
382
486
|
return saved
|
|
@@ -400,37 +504,6 @@ export function updateSectionAsync(section, updater) {
|
|
|
400
504
|
}))
|
|
401
505
|
}
|
|
402
506
|
|
|
403
|
-
// ── Capabilities (B2 central path policy) ───────────────────────────
|
|
404
|
-
// Top-level `capabilities` section in mixdog-config.json. Safe defaults
|
|
405
|
-
// win on missing/malformed input — every cap is OFF unless explicitly
|
|
406
|
-
// enabled. Settings round-trip through the setup UI; the in-process
|
|
407
|
-
// path gate reads them via `getCapabilities()`.
|
|
408
|
-
//
|
|
409
|
-
// homeAccess: when true, file tools may write anywhere under $HOME. When
|
|
410
|
-
// false (default), file tools are cwd-scoped — matches the setup UI's
|
|
411
|
-
// out-of-the-box "OFF" toggle so a fresh install is restrictive until the
|
|
412
|
-
// user explicitly opts in. This ONLY controls the main-agent path gate —
|
|
413
|
-
// agent role Edit/Write to HOME paths always go through Discord approval
|
|
414
|
-
// regardless (enforced in hooks/pre-tool-subagent.cjs).
|
|
415
|
-
const CAPABILITY_DEFAULTS = Object.freeze({ homeAccess: false })
|
|
416
|
-
|
|
417
|
-
function readCapabilities() {
|
|
418
|
-
const raw = readAll().capabilities
|
|
419
|
-
const out = { ...CAPABILITY_DEFAULTS }
|
|
420
|
-
if (raw && typeof raw === 'object') {
|
|
421
|
-
if (raw.homeAccess === true) out.homeAccess = true
|
|
422
|
-
else if (raw.homeAccess === false) out.homeAccess = false
|
|
423
|
-
}
|
|
424
|
-
return out
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Convenience alias requested by B2 call-site plumbing. Returns the
|
|
428
|
-
// same object shape as readCapabilities(); callers that only need a
|
|
429
|
-
// boolean can read `.homeAccess` directly.
|
|
430
|
-
function getCapabilities() {
|
|
431
|
-
return readCapabilities()
|
|
432
|
-
}
|
|
433
|
-
|
|
434
507
|
// ── Secret account names ─────────────────────────────────────────────────────
|
|
435
508
|
// Canonical account strings stored in the OS keychain. Must stay in sync with
|
|
436
509
|
// the migration logic in seed.mjs.
|
|
@@ -519,9 +592,9 @@ export function saveSecret(account, value) {
|
|
|
519
592
|
try {
|
|
520
593
|
_setSecret(account, value)
|
|
521
594
|
} catch (err) {
|
|
522
|
-
// On WSL/headless Linux (and any host missing a usable keychain
|
|
595
|
+
// On WSL/headless Linux (and any host missing a usable keychain provider,
|
|
523
596
|
// e.g. keytar/libsecret not installed or no running Secret Service) the OS
|
|
524
|
-
// write throws a cryptic
|
|
597
|
+
// write throws a cryptic provider error. Surface an actionable message that
|
|
525
598
|
// points at the env-var read path the getters already honor, instead of
|
|
526
599
|
// silently writing the plaintext secret to mixdog-config.json.
|
|
527
600
|
const envKey = _envKey(account)
|
|
@@ -530,7 +603,7 @@ export function saveSecret(account, value) {
|
|
|
530
603
|
const envHint = stdEnv ? `${stdEnv} (or ${envKey})` : envKey
|
|
531
604
|
const e = new Error(
|
|
532
605
|
`[config] could not save secret to the OS keychain: ${err && err.message ? err.message : err}\n` +
|
|
533
|
-
` No usable keychain
|
|
606
|
+
` No usable keychain provider on this host (common on WSL / headless Linux without libsecret).\n` +
|
|
534
607
|
` Set the ${envHint} environment variable instead — the runtime reads it directly.`
|
|
535
608
|
)
|
|
536
609
|
e.cause = err
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (`makeAgentDispatch({ taskType })`) and, for memory maintenance specifically,
|
|
7
7
|
* through `maintenance-llm.mjs`'s thin wrapper.
|
|
8
8
|
*
|
|
9
|
-
* Only preset resolution remains here: memory-cycle and future
|
|
9
|
+
* Only preset resolution remains here: memory-cycle and future provider
|
|
10
10
|
* callers still need a consistent way to map `(task, agent-config)` to a
|
|
11
11
|
* preset id.
|
|
12
12
|
*/
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createFairCallScheduler } from '../../standalone/fair-call-scheduler.mjs';
|
|
2
|
+
import { currentToolExecutionOwner } from './tool-execution-owner.mjs';
|
|
3
|
+
|
|
4
|
+
function positiveInt(value, fallback) {
|
|
5
|
+
const parsed = Math.floor(Number(value));
|
|
6
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function waitTimeoutError(name, waitTimeoutMs) {
|
|
10
|
+
const error = new Error(`${name} admission wait exceeded ${waitTimeoutMs}ms; retry after running work completes`);
|
|
11
|
+
error.code = 'EADMISSIONWAITTIMEOUT';
|
|
12
|
+
error.statusCode = 503;
|
|
13
|
+
error.waitTimeoutMs = waitTimeoutMs;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Owner-fair bounded work gate. The underlying scheduler lets a lone owner
|
|
19
|
+
* borrow all capacity, then rotates contending session owners and rebalances a
|
|
20
|
+
* full borrowed queue so a late interactive session is not trapped behind one
|
|
21
|
+
* session's entire fan-out.
|
|
22
|
+
*/
|
|
23
|
+
export function createOwnerFairGate({
|
|
24
|
+
name = 'work',
|
|
25
|
+
activeMax = 8,
|
|
26
|
+
queueMax = 1024,
|
|
27
|
+
minOwnerQueue = 8,
|
|
28
|
+
waitTimeoutMs = 30_000,
|
|
29
|
+
schedule = setImmediate,
|
|
30
|
+
now = Date.now,
|
|
31
|
+
} = {}) {
|
|
32
|
+
const defaultWaitTimeoutMs = Math.max(0, Math.floor(Number(waitTimeoutMs) || 0));
|
|
33
|
+
const scheduler = createFairCallScheduler({
|
|
34
|
+
name,
|
|
35
|
+
activeMax,
|
|
36
|
+
queueMax,
|
|
37
|
+
minOwnerQueue,
|
|
38
|
+
dispatchBurst: Math.max(1, Math.min(8, positiveInt(activeMax, 8))),
|
|
39
|
+
schedule,
|
|
40
|
+
now,
|
|
41
|
+
});
|
|
42
|
+
const stats = {
|
|
43
|
+
admitted: 0,
|
|
44
|
+
rejected: 0,
|
|
45
|
+
timedOut: 0,
|
|
46
|
+
totalWaitMs: 0,
|
|
47
|
+
maxWaitMs: 0,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function run(ownerKey, task, {
|
|
51
|
+
signal = null,
|
|
52
|
+
weight = 1,
|
|
53
|
+
waitTimeoutMs: callWaitTimeoutMs = defaultWaitTimeoutMs,
|
|
54
|
+
onAdmit = null,
|
|
55
|
+
} = {}) {
|
|
56
|
+
if (typeof task !== 'function') return Promise.reject(new TypeError(`${name} task must be a function`));
|
|
57
|
+
const owner = String(ownerKey || currentToolExecutionOwner() || 'anonymous').trim().slice(0, 240) || 'anonymous';
|
|
58
|
+
const queuedAt = now();
|
|
59
|
+
const timeoutMs = Math.max(0, Math.floor(Number(callWaitTimeoutMs) || 0));
|
|
60
|
+
const controller = (signal || timeoutMs > 0) ? new AbortController() : null;
|
|
61
|
+
let timer = null;
|
|
62
|
+
let started = false;
|
|
63
|
+
let timeoutError = null;
|
|
64
|
+
let onAbort = null;
|
|
65
|
+
|
|
66
|
+
const cleanupQueueWait = () => {
|
|
67
|
+
if (timer) {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
timer = null;
|
|
70
|
+
}
|
|
71
|
+
if (onAbort && signal) {
|
|
72
|
+
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
73
|
+
onAbort = null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (controller && signal) {
|
|
78
|
+
onAbort = () => {
|
|
79
|
+
if (!controller.signal.aborted) controller.abort(signal.reason);
|
|
80
|
+
};
|
|
81
|
+
if (signal.aborted) onAbort();
|
|
82
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
83
|
+
}
|
|
84
|
+
if (controller && timeoutMs > 0 && !controller.signal.aborted) {
|
|
85
|
+
timer = setTimeout(() => {
|
|
86
|
+
if (started || controller.signal.aborted) return;
|
|
87
|
+
timeoutError = waitTimeoutError(name, timeoutMs);
|
|
88
|
+
controller.abort(timeoutError);
|
|
89
|
+
}, timeoutMs);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const promise = scheduler.enqueue(owner, async () => {
|
|
93
|
+
started = true;
|
|
94
|
+
cleanupQueueWait();
|
|
95
|
+
const waitedMs = Math.max(0, now() - queuedAt);
|
|
96
|
+
stats.admitted += 1;
|
|
97
|
+
stats.totalWaitMs += waitedMs;
|
|
98
|
+
stats.maxWaitMs = Math.max(stats.maxWaitMs, waitedMs);
|
|
99
|
+
try { onAdmit?.(waitedMs); } catch {}
|
|
100
|
+
return task();
|
|
101
|
+
}, {
|
|
102
|
+
weight,
|
|
103
|
+
signal: controller?.signal || signal,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return promise.catch((error) => {
|
|
107
|
+
if (!started) {
|
|
108
|
+
stats.rejected += 1;
|
|
109
|
+
if (timeoutError && error === timeoutError) stats.timedOut += 1;
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}).finally(cleanupQueueWait);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function snapshot() {
|
|
116
|
+
const base = scheduler.snapshot();
|
|
117
|
+
return {
|
|
118
|
+
...base,
|
|
119
|
+
admitted: stats.admitted,
|
|
120
|
+
rejected: stats.rejected,
|
|
121
|
+
timedOut: stats.timedOut,
|
|
122
|
+
averageWaitMs: stats.admitted > 0 ? Math.round(stats.totalWaitMs / stats.admitted) : 0,
|
|
123
|
+
maxWaitMs: stats.maxWaitMs,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
run,
|
|
129
|
+
close: (reason) => scheduler.close(reason),
|
|
130
|
+
snapshot,
|
|
131
|
+
get active() { return scheduler.active; },
|
|
132
|
+
get queued() { return scheduler.queued; },
|
|
133
|
+
};
|
|
134
|
+
}
|