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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createFairCallScheduler } from './fair-call-scheduler.mjs';
|
|
2
|
+
|
|
3
|
+
const MEMORY_AGENTS = Object.freeze([
|
|
4
|
+
'cycle1-agent',
|
|
5
|
+
'cycle2-agent',
|
|
6
|
+
'cycle3-agent',
|
|
7
|
+
]);
|
|
8
|
+
const MEMORY_AGENT_SET = new Set(MEMORY_AGENTS);
|
|
9
|
+
|
|
10
|
+
function abortError(signal) {
|
|
11
|
+
return signal?.reason instanceof Error
|
|
12
|
+
? signal.reason
|
|
13
|
+
: new Error(String(signal?.reason || 'agent dispatch canceled'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function awaitSharedPreparation(promise, signal) {
|
|
17
|
+
if (!signal) return promise;
|
|
18
|
+
if (signal.aborted) return Promise.reject(abortError(signal));
|
|
19
|
+
let listener = null;
|
|
20
|
+
const aborted = new Promise((_, reject) => {
|
|
21
|
+
listener = () => reject(abortError(signal));
|
|
22
|
+
signal.addEventListener('abort', listener, { once: true });
|
|
23
|
+
});
|
|
24
|
+
return Promise.race([promise, aborted]).finally(() => {
|
|
25
|
+
if (listener) {
|
|
26
|
+
try { signal.removeEventListener('abort', listener); } catch {}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Process-singleton memory LLM broker.
|
|
33
|
+
*
|
|
34
|
+
* The daemon owns one provider/session/memory graph. Memory
|
|
35
|
+
* process keeps PG, embeddings, and recall, and forwards only cycle LLM calls
|
|
36
|
+
* here. A singleton is NOT a serial lane: every call gets its own dispatch
|
|
37
|
+
* session + AbortController and the fair scheduler starts calls in parallel
|
|
38
|
+
* unless an operator explicitly configures a finite active limit.
|
|
39
|
+
*/
|
|
40
|
+
export function createAgentDispatchBroker({
|
|
41
|
+
loadConfig,
|
|
42
|
+
initProviders,
|
|
43
|
+
makeAgentDispatch,
|
|
44
|
+
log = () => {},
|
|
45
|
+
activeMax = Infinity,
|
|
46
|
+
queueMax = 256,
|
|
47
|
+
onActivityChanged = null,
|
|
48
|
+
} = {}) {
|
|
49
|
+
if (typeof loadConfig !== 'function') throw new TypeError('loadConfig is required');
|
|
50
|
+
if (typeof initProviders !== 'function') throw new TypeError('initProviders is required');
|
|
51
|
+
if (typeof makeAgentDispatch !== 'function') throw new TypeError('makeAgentDispatch is required');
|
|
52
|
+
|
|
53
|
+
const scheduler = createFairCallScheduler({
|
|
54
|
+
name: 'memory agent dispatch',
|
|
55
|
+
activeMax,
|
|
56
|
+
queueMax,
|
|
57
|
+
minOwnerQueue: Math.max(8, Math.floor(queueMax / 16)),
|
|
58
|
+
});
|
|
59
|
+
const inFlight = new Map();
|
|
60
|
+
const dispatchers = new Map(MEMORY_AGENTS.map((agent) => [
|
|
61
|
+
agent,
|
|
62
|
+
makeAgentDispatch({
|
|
63
|
+
agent,
|
|
64
|
+
taskType: 'maintenance',
|
|
65
|
+
sourceType: 'memory-cycle',
|
|
66
|
+
brief: false,
|
|
67
|
+
}),
|
|
68
|
+
]));
|
|
69
|
+
let nextCallId = 0;
|
|
70
|
+
let closed = false;
|
|
71
|
+
let preparedProviderSignature = null;
|
|
72
|
+
let providerPreparePromise = null;
|
|
73
|
+
|
|
74
|
+
async function prepareProviders(signal = null) {
|
|
75
|
+
const config = loadConfig();
|
|
76
|
+
const providers = config?.providers || {};
|
|
77
|
+
let signature = null;
|
|
78
|
+
try { signature = JSON.stringify(providers); } catch {}
|
|
79
|
+
if (signature !== null && preparedProviderSignature === signature) return;
|
|
80
|
+
if (providerPreparePromise) {
|
|
81
|
+
await awaitSharedPreparation(providerPreparePromise, signal);
|
|
82
|
+
if (signature !== null && preparedProviderSignature === signature) return;
|
|
83
|
+
return prepareProviders(signal);
|
|
84
|
+
}
|
|
85
|
+
const pending = Promise.resolve()
|
|
86
|
+
// Process-global preparation is shared by every request. One caller's
|
|
87
|
+
// cancellation may stop waiting but must never abort initialization
|
|
88
|
+
// underneath independent calls.
|
|
89
|
+
.then(() => initProviders(providers))
|
|
90
|
+
.then(() => {
|
|
91
|
+
preparedProviderSignature = signature;
|
|
92
|
+
});
|
|
93
|
+
const tracked = pending.finally(() => {
|
|
94
|
+
if (providerPreparePromise === tracked) providerPreparePromise = null;
|
|
95
|
+
});
|
|
96
|
+
providerPreparePromise = tracked;
|
|
97
|
+
await awaitSharedPreparation(tracked, signal);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function warmup() {
|
|
101
|
+
if (closed) throw new Error('memory agent dispatch broker is closed');
|
|
102
|
+
await prepareProviders();
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function dispatch(params = {}, { callId = null, signal = null } = {}) {
|
|
107
|
+
if (closed) return Promise.reject(new Error('memory agent dispatch broker is closed'));
|
|
108
|
+
const agent = String(params.agent || '').trim();
|
|
109
|
+
if (!MEMORY_AGENT_SET.has(agent)) {
|
|
110
|
+
return Promise.reject(new Error(`memory agent dispatch denied for "${agent || 'unknown'}"`));
|
|
111
|
+
}
|
|
112
|
+
const prompt = String(params.prompt ?? '');
|
|
113
|
+
if (!prompt) return Promise.reject(new Error(`agent dispatch prompt required for "${agent}"`));
|
|
114
|
+
|
|
115
|
+
const id = String(callId || `broker-${process.pid}-${++nextCallId}`);
|
|
116
|
+
const existing = inFlight.get(id);
|
|
117
|
+
if (existing) return existing.promise;
|
|
118
|
+
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const abortFromCaller = () => {
|
|
121
|
+
if (controller.signal.aborted) return;
|
|
122
|
+
try { controller.abort(signal?.reason); } catch { try { controller.abort(); } catch {} }
|
|
123
|
+
};
|
|
124
|
+
if (signal?.aborted) abortFromCaller();
|
|
125
|
+
else signal?.addEventListener?.('abort', abortFromCaller, { once: true });
|
|
126
|
+
|
|
127
|
+
const run = async () => {
|
|
128
|
+
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
129
|
+
await prepareProviders(controller.signal);
|
|
130
|
+
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
131
|
+
const timeout = Number(params.timeout);
|
|
132
|
+
return dispatchers.get(agent)({
|
|
133
|
+
prompt,
|
|
134
|
+
preset: params.preset || undefined,
|
|
135
|
+
cwd: typeof params.cwd === 'string' && params.cwd ? params.cwd : undefined,
|
|
136
|
+
parentSignal: controller.signal,
|
|
137
|
+
...(Number.isFinite(timeout) && timeout > 0 ? { idleTimeoutMs: timeout } : {}),
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const promise = scheduler.enqueue(`memory:${agent}`, run, {
|
|
141
|
+
signal: controller.signal,
|
|
142
|
+
});
|
|
143
|
+
let record = null;
|
|
144
|
+
const tracked = promise.finally(() => {
|
|
145
|
+
signal?.removeEventListener?.('abort', abortFromCaller);
|
|
146
|
+
if (inFlight.get(id) === record) inFlight.delete(id);
|
|
147
|
+
try { onActivityChanged?.(snapshot()); } catch {}
|
|
148
|
+
});
|
|
149
|
+
record = { controller, promise: tracked };
|
|
150
|
+
inFlight.set(id, record);
|
|
151
|
+
try { onActivityChanged?.(snapshot()); } catch {}
|
|
152
|
+
return tracked;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cancel(callId, reason = 'memory agent dispatch canceled') {
|
|
156
|
+
const id = String(callId || '');
|
|
157
|
+
const record = inFlight.get(id);
|
|
158
|
+
if (!record) return false;
|
|
159
|
+
try { record.controller.abort(new Error(reason)); } catch { try { record.controller.abort(); } catch {} }
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function cancelAndWait(callId, reason = 'memory agent dispatch canceled') {
|
|
164
|
+
const id = String(callId || '');
|
|
165
|
+
const record = inFlight.get(id);
|
|
166
|
+
if (!record) return false;
|
|
167
|
+
cancel(id, reason);
|
|
168
|
+
try { await record.promise; } catch {}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function snapshot() {
|
|
173
|
+
return {
|
|
174
|
+
...scheduler.snapshot(),
|
|
175
|
+
inFlight: inFlight.size,
|
|
176
|
+
dispatchers: dispatchers.size,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function close(reason = 'memory agent dispatch broker is closed') {
|
|
181
|
+
if (closed) return;
|
|
182
|
+
closed = true;
|
|
183
|
+
for (const record of inFlight.values()) {
|
|
184
|
+
try { record.controller.abort(new Error(reason)); } catch {}
|
|
185
|
+
}
|
|
186
|
+
scheduler.close(reason);
|
|
187
|
+
log(`agent dispatch broker closed (${reason})`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
warmup,
|
|
192
|
+
dispatch,
|
|
193
|
+
cancel,
|
|
194
|
+
cancelAndWait,
|
|
195
|
+
snapshot,
|
|
196
|
+
close,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -151,8 +151,8 @@ export function synthesizePreset(config, key) {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
export function normalizeAgentRoute(routeLike
|
|
155
|
-
const provider = clean(routeLike?.provider)
|
|
154
|
+
export function normalizeAgentRoute(routeLike) {
|
|
155
|
+
const provider = clean(routeLike?.provider);
|
|
156
156
|
const model = clean(routeLike?.model);
|
|
157
157
|
if (!provider || !model) return null;
|
|
158
158
|
return {
|
|
@@ -76,6 +76,8 @@ export function createSpawnFlow({
|
|
|
76
76
|
const lease = await resourceAdmission.acquire('agent', {
|
|
77
77
|
signal: admissionController.signal,
|
|
78
78
|
label: jobMeta?.tag || type,
|
|
79
|
+
ownerKey: notifyContext?.callerSessionId || notifyContext?.sessionId
|
|
80
|
+
|| clientHostPid || null,
|
|
79
81
|
});
|
|
80
82
|
try {
|
|
81
83
|
// Yield one macrotask before doing agent work. startBackgroundTask uses
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_AGENT_PRESETS,
|
|
4
|
-
DEFAULT_PROVIDER,
|
|
5
|
-
} from './tool-def.mjs';
|
|
1
|
+
import { configuredAgentRouteCandidates } from '../../runtime/shared/agent-route-config.mjs';
|
|
6
2
|
import {
|
|
7
3
|
agentPresetName,
|
|
8
4
|
clean,
|
|
@@ -30,19 +26,10 @@ export function resolveAgentSpawnPreset(config, args = {}) {
|
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
const agentName = normalizeAgentName(args.agent);
|
|
33
|
-
const configuredDefault = clean(config?.defaultProvider);
|
|
34
|
-
const fallbackProvider = configuredDefault && isKnownProvider(configuredDefault)
|
|
35
|
-
? configuredDefault
|
|
36
|
-
: DEFAULT_PROVIDER;
|
|
37
|
-
const workflowSlot = agentName === 'explore' ? 'explorer'
|
|
38
|
-
: (agentName === 'maintainer' ? 'memory' : '');
|
|
39
|
-
const maintenanceSlot = agentName === 'explore' ? 'explore'
|
|
40
|
-
: (agentName === 'maintainer' ? 'memory' : '');
|
|
41
29
|
const agentRoute = !clean(args.preset)
|
|
42
|
-
? (
|
|
43
|
-
|
|
44
|
-
||
|
|
45
|
-
|| normalizeAgentRoute(config?.maintenance?.[maintenanceSlot], fallbackProvider))
|
|
30
|
+
? configuredAgentRouteCandidates(config, agentName)
|
|
31
|
+
.map((candidate) => normalizeAgentRoute(candidate))
|
|
32
|
+
.find(Boolean) || null
|
|
46
33
|
: null;
|
|
47
34
|
if (agentRoute) {
|
|
48
35
|
return {
|
|
@@ -60,14 +47,17 @@ export function resolveAgentSpawnPreset(config, args = {}) {
|
|
|
60
47
|
};
|
|
61
48
|
}
|
|
62
49
|
|
|
63
|
-
const mainPreset = !clean(args.preset)
|
|
50
|
+
const mainPreset = !clean(args.preset)
|
|
64
51
|
? findPreset(config, config?.default)
|
|
65
52
|
: null;
|
|
66
|
-
if (mainPreset)
|
|
53
|
+
if (normalizeAgentRoute(mainPreset)) {
|
|
54
|
+
return { presetName: mainPreset.id || mainPreset.name, preset: mainPreset };
|
|
55
|
+
}
|
|
67
56
|
|
|
68
|
-
const presetName = clean(args.preset)
|
|
69
|
-
if (!presetName) throw new Error(`agent: agent "${agentName}" has no model assignment`);
|
|
57
|
+
const presetName = clean(args.preset);
|
|
58
|
+
if (!presetName) throw new Error(`agent: agent "${agentName}" has no Main model assignment`);
|
|
70
59
|
const preset = findPreset(config, presetName) || synthesizePreset(config, presetName);
|
|
71
60
|
if (!preset) throw new Error(`agent: preset "${presetName}" not found`);
|
|
61
|
+
if (!normalizeAgentRoute(preset)) throw new Error(`agent: preset "${presetName}" has no complete route`);
|
|
72
62
|
return { presetName, preset };
|
|
73
63
|
}
|
|
@@ -6,20 +6,6 @@ export const PRESET_ALIASES = new Map([
|
|
|
6
6
|
['opus-xhigh', { base: 'opus-high', effort: 'xhigh', id: 'opus-xhigh', name: 'OPUS XHIGH' }],
|
|
7
7
|
]);
|
|
8
8
|
|
|
9
|
-
export const DEFAULT_AGENT_PRESETS = Object.freeze({
|
|
10
|
-
explore: 'sonnet-high',
|
|
11
|
-
maintainer: 'haiku',
|
|
12
|
-
worker: 'sonnet-high',
|
|
13
|
-
'heavy-worker': 'sonnet-high',
|
|
14
|
-
reviewer: 'opus-xhigh',
|
|
15
|
-
debugger: 'opus-xhigh',
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
// Mirrors DEFAULT_PROVIDER in mixdog-session-runtime.mjs. Used only as the
|
|
19
|
-
// last-resort fallback when a stored agent route omits its provider and the
|
|
20
|
-
// config carries no defaultProvider.
|
|
21
|
-
export const DEFAULT_PROVIDER = 'anthropic-oauth';
|
|
22
|
-
|
|
23
9
|
export const WORKER_INDEX_FILE = 'agent-workers.json';
|
|
24
10
|
|
|
25
11
|
export const ACTIVE_STAGES = new Set(['connecting', 'requesting', 'streaming', 'tool_running', 'running', 'cancelling']);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { basename, dirname, join } from 'node:path';
|
|
12
12
|
import {
|
|
13
13
|
SECRET_ACCOUNTS,
|
|
14
|
+
canonicalizeStoredChannelsConfig,
|
|
14
15
|
deleteSecret,
|
|
15
16
|
diagnoseDiscordTokenValue,
|
|
16
17
|
getDiscordToken,
|
|
@@ -43,10 +44,10 @@ import { readHookPublicBase } from '../runtime/channels/lib/webhook/relay-tunnel
|
|
|
43
44
|
|
|
44
45
|
const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
45
46
|
const DEFAULT_CHANNELS = Object.freeze({
|
|
46
|
-
|
|
47
|
+
provider: 'discord',
|
|
47
48
|
discord: {},
|
|
48
49
|
access: { dmPolicy: 'allowlist', allowFrom: [], channels: {} },
|
|
49
|
-
channel: {
|
|
50
|
+
channel: { discordChannelId: '', telegramChatId: '' },
|
|
50
51
|
webhook: { enabled: true, port: 3333 },
|
|
51
52
|
});
|
|
52
53
|
|
|
@@ -91,36 +92,28 @@ function assertScheduleName(name) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function normalizeChannelsConfig(raw = {}) {
|
|
95
|
+
const stored = canonicalizeStoredChannelsConfig(raw);
|
|
94
96
|
return {
|
|
95
97
|
...DEFAULT_CHANNELS,
|
|
96
|
-
...
|
|
97
|
-
discord: { ...DEFAULT_CHANNELS.discord, ...(
|
|
98
|
-
access: { ...DEFAULT_CHANNELS.access, ...(
|
|
99
|
-
channel: { ...DEFAULT_CHANNELS.channel, ...(
|
|
100
|
-
webhook: { ...DEFAULT_CHANNELS.webhook, ...(
|
|
98
|
+
...stored,
|
|
99
|
+
discord: { ...DEFAULT_CHANNELS.discord, ...(stored.discord || {}) },
|
|
100
|
+
access: { ...DEFAULT_CHANNELS.access, ...(stored.access || {}) },
|
|
101
|
+
channel: { ...DEFAULT_CHANNELS.channel, ...(stored.channel || {}) },
|
|
102
|
+
webhook: { ...DEFAULT_CHANNELS.webhook, ...(stored.webhook || {}) },
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
function
|
|
105
|
-
if (
|
|
106
|
-
return String(entry?.telegramChatId ||
|
|
106
|
+
function channelIdForProvider(entry = {}, provider = 'discord') {
|
|
107
|
+
if (provider === 'telegram') {
|
|
108
|
+
return String(entry?.telegramChatId || '');
|
|
107
109
|
}
|
|
108
|
-
return String(entry?.discordChannelId ||
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function seedBackendChannelIds(entry = {}, backend = 'discord') {
|
|
112
|
-
const next = { ...(entry || {}) };
|
|
113
|
-
if (next.channelId) {
|
|
114
|
-
if (backend === 'telegram' && !next.telegramChatId && !next.discordChannelId) next.telegramChatId = next.channelId;
|
|
115
|
-
if (backend !== 'telegram' && !next.discordChannelId && !next.telegramChatId) next.discordChannelId = next.channelId;
|
|
116
|
-
}
|
|
117
|
-
return next;
|
|
110
|
+
return String(entry?.discordChannelId || '');
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
// Resolve the single-channel entry from the config's `channel` object.
|
|
121
114
|
function resolveChannelEntry(cfg = {}) {
|
|
122
115
|
if (cfg.channel && typeof cfg.channel === 'object'
|
|
123
|
-
&& (cfg.channel.
|
|
116
|
+
&& (cfg.channel.discordChannelId || cfg.channel.telegramChatId)) {
|
|
124
117
|
return cfg.channel;
|
|
125
118
|
}
|
|
126
119
|
return cfg.channel && typeof cfg.channel === 'object' ? cfg.channel : {};
|
|
@@ -138,7 +131,7 @@ function updateChannelsSection(build) {
|
|
|
138
131
|
}
|
|
139
132
|
|
|
140
133
|
// Async twin of updateChannelsSection: identical normalize/strip logic, but the
|
|
141
|
-
// channels-section RMW runs through updateSectionAsync so a debounced
|
|
134
|
+
// channels-section RMW runs through updateSectionAsync so a debounced provider
|
|
142
135
|
// flush does not block the event loop. Same config lock file → linearizable
|
|
143
136
|
// with the sync writers.
|
|
144
137
|
async function updateChannelsSectionAsync(build) {
|
|
@@ -166,10 +159,10 @@ function listEntryDirs(dir) {
|
|
|
166
159
|
// settings/TUI layer consumes.
|
|
167
160
|
function getChannel(config = {}) {
|
|
168
161
|
const cfg = normalizeChannelsConfig(config);
|
|
169
|
-
const
|
|
162
|
+
const provider = cfg.provider === 'telegram' ? 'telegram' : 'discord';
|
|
170
163
|
const entry = resolveChannelEntry(cfg);
|
|
171
164
|
return {
|
|
172
|
-
channelId:
|
|
165
|
+
channelId: channelIdForProvider(entry, provider),
|
|
173
166
|
discordChannelId: String(entry?.discordChannelId || ''),
|
|
174
167
|
telegramChatId: String(entry?.telegramChatId || ''),
|
|
175
168
|
};
|
|
@@ -199,39 +192,38 @@ export function forgetTelegramToken() {
|
|
|
199
192
|
return { ok: true };
|
|
200
193
|
}
|
|
201
194
|
|
|
202
|
-
// Single-channel write: persists `cfg.channel`, keeping the per-
|
|
203
|
-
// fields (discordChannelId/telegramChatId) so a
|
|
204
|
-
// ids. `
|
|
205
|
-
// the id lands on the active
|
|
206
|
-
export function setChannel({ channelId,
|
|
195
|
+
// Single-channel write: persists `cfg.channel`, keeping the per-provider id
|
|
196
|
+
// fields (discordChannelId/telegramChatId) so a provider switch retains both
|
|
197
|
+
// ids. `provider` selects which per-provider field the id updates; when omitted
|
|
198
|
+
// the id lands on the active provider's field.
|
|
199
|
+
export function setChannel({ channelId, provider = null } = {}) {
|
|
207
200
|
const id = String(channelId || '').trim();
|
|
208
201
|
if (!id) throw new Error('channelId is required');
|
|
209
|
-
const
|
|
202
|
+
const targetProvider = provider === 'telegram' || provider === 'discord' ? provider : null;
|
|
210
203
|
return updateChannelsSection((cfg) => {
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
const current =
|
|
204
|
+
const activeProvider = cfg.provider === 'telegram' ? 'telegram' : 'discord';
|
|
205
|
+
const writeProvider = targetProvider || activeProvider;
|
|
206
|
+
const current = resolveChannelEntry(cfg);
|
|
214
207
|
const nextEntry = { ...current };
|
|
215
|
-
if (
|
|
208
|
+
if (writeProvider === 'telegram') nextEntry.telegramChatId = id;
|
|
216
209
|
else nextEntry.discordChannelId = id;
|
|
217
|
-
|
|
218
|
-
nextEntry.channelId = channelIdForBackend(nextEntry, activeBackend);
|
|
210
|
+
delete nextEntry.channelId;
|
|
219
211
|
return { ...cfg, channel: nextEntry };
|
|
220
212
|
});
|
|
221
213
|
}
|
|
222
214
|
|
|
223
|
-
export async function setChannelAsync({ channelId,
|
|
215
|
+
export async function setChannelAsync({ channelId, provider = null } = {}) {
|
|
224
216
|
const id = String(channelId || '').trim();
|
|
225
217
|
if (!id) throw new Error('channelId is required');
|
|
226
|
-
const
|
|
218
|
+
const targetProvider = provider === 'telegram' || provider === 'discord' ? provider : null;
|
|
227
219
|
return updateChannelsSectionAsync((cfg) => {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
const current =
|
|
220
|
+
const activeProvider = cfg.provider === 'telegram' ? 'telegram' : 'discord';
|
|
221
|
+
const writeProvider = targetProvider || activeProvider;
|
|
222
|
+
const current = resolveChannelEntry(cfg);
|
|
231
223
|
const nextEntry = { ...current };
|
|
232
|
-
if (
|
|
224
|
+
if (writeProvider === 'telegram') nextEntry.telegramChatId = id;
|
|
233
225
|
else nextEntry.discordChannelId = id;
|
|
234
|
-
nextEntry.channelId
|
|
226
|
+
delete nextEntry.channelId;
|
|
235
227
|
return { ...cfg, channel: nextEntry };
|
|
236
228
|
});
|
|
237
229
|
}
|
|
@@ -260,33 +252,30 @@ export async function setWebhookConfigAsync(patch = {}) {
|
|
|
260
252
|
}));
|
|
261
253
|
}
|
|
262
254
|
|
|
263
|
-
function
|
|
255
|
+
function validateProvider(name) {
|
|
264
256
|
const value = String(name || '').trim();
|
|
265
257
|
if (value !== 'discord' && value !== 'telegram') {
|
|
266
|
-
throw new Error('
|
|
258
|
+
throw new Error('provider must be discord or telegram');
|
|
267
259
|
}
|
|
268
260
|
return value;
|
|
269
261
|
}
|
|
270
|
-
function
|
|
262
|
+
function providerBuilder(value) {
|
|
271
263
|
return (cfg) => {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// id fields are retained so switching back keeps the other id.
|
|
276
|
-
const channel = { ...seeded, channelId: channelIdForBackend(seeded, value) };
|
|
277
|
-
return { ...cfg, channel, backend: value };
|
|
264
|
+
const channel = { ...resolveChannelEntry(cfg) };
|
|
265
|
+
delete channel.channelId;
|
|
266
|
+
return { ...cfg, channel, provider: value };
|
|
278
267
|
};
|
|
279
268
|
}
|
|
280
|
-
export function
|
|
281
|
-
const value =
|
|
282
|
-
updateChannelsSection(
|
|
283
|
-
return { ok: true,
|
|
269
|
+
export function setChannelProvider(name) {
|
|
270
|
+
const value = validateProvider(name);
|
|
271
|
+
updateChannelsSection(providerBuilder(value));
|
|
272
|
+
return { ok: true, provider: value };
|
|
284
273
|
}
|
|
285
|
-
// Async twin used by the debounced
|
|
286
|
-
export async function
|
|
287
|
-
const value =
|
|
288
|
-
await updateChannelsSectionAsync(
|
|
289
|
-
return { ok: true,
|
|
274
|
+
// Async twin used by the debounced provider-save flush timer.
|
|
275
|
+
export async function setChannelProviderAsync(name) {
|
|
276
|
+
const value = validateProvider(name);
|
|
277
|
+
await updateChannelsSectionAsync(providerBuilder(value));
|
|
278
|
+
return { ok: true, provider: value };
|
|
290
279
|
}
|
|
291
280
|
|
|
292
281
|
function normalizeCron(time) {
|
|
@@ -578,16 +567,16 @@ export async function channelSetup(config = null) {
|
|
|
578
567
|
const discordProblem = diagnoseDiscordTokenValue(discordToken, cfg);
|
|
579
568
|
const telegramToken = getTelegramToken();
|
|
580
569
|
return {
|
|
581
|
-
|
|
570
|
+
provider: cfg.provider || 'discord',
|
|
582
571
|
discord: {
|
|
583
|
-
|
|
572
|
+
provider: 'discord',
|
|
584
573
|
authenticated: Boolean(discordToken && !discordProblem),
|
|
585
574
|
stored: hasStoredSecret(SECRET_ACCOUNTS.discordToken),
|
|
586
575
|
status: discordToken ? (discordProblem ? 'Invalid' : 'Set') : 'Off',
|
|
587
576
|
problem: discordProblem || null,
|
|
588
577
|
},
|
|
589
578
|
telegram: {
|
|
590
|
-
|
|
579
|
+
provider: 'telegram',
|
|
591
580
|
authenticated: Boolean(telegramToken),
|
|
592
581
|
stored: hasStoredSecret(SECRET_ACCOUNTS.telegramToken),
|
|
593
582
|
status: telegramToken ? 'Set' : 'Off',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Machine-global
|
|
1
|
+
// Machine-global channel service — attach client (TUI side).
|
|
2
2
|
//
|
|
3
3
|
// Mirrors the memory proxy's attach pattern (POST /client/register + /health,
|
|
4
|
-
// 127.0.0.1 only). A TUI uses this to talk to the
|
|
4
|
+
// 127.0.0.1 only). A TUI uses this to talk to the shared channel front door
|
|
5
5
|
// instead of forking its own worker: tool calls go over POST /call and the
|
|
6
6
|
// worker->parent notify path arrives on a persistent SSE stream (GET /events),
|
|
7
7
|
// replacing the old node-IPC `{type:'notify'}` messages.
|
|
@@ -28,18 +28,22 @@ function isPidAlive(pid) {
|
|
|
28
28
|
|
|
29
29
|
// Read + validate the discovery file. Returns null when missing/corrupt or when
|
|
30
30
|
// the recorded pid is dead (stale daemon) so the caller reclaims + respawns.
|
|
31
|
-
export function
|
|
31
|
+
export function readChannelDiscovery(discoveryPath) {
|
|
32
32
|
let raw;
|
|
33
33
|
try { raw = JSON.parse(readFileSync(discoveryPath, 'utf8')); }
|
|
34
34
|
catch { return null; }
|
|
35
|
-
const
|
|
35
|
+
const endpoint = raw?.endpoints?.channel;
|
|
36
|
+
const port = parsePort(endpoint?.port);
|
|
36
37
|
const pid = parsePid(raw?.pid);
|
|
37
|
-
if (!port || !pid || !
|
|
38
|
+
if (!port || !pid || !endpoint?.token) return null;
|
|
38
39
|
if (!isPidAlive(pid)) return null; // dead daemon → treat as absent
|
|
39
|
-
return { port, pid, token: String(
|
|
40
|
+
return { port, pid, token: String(endpoint.token) };
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function request({
|
|
43
|
+
function request({
|
|
44
|
+
port, method = 'GET', path = '/', token, body = null, timeoutMs = 10_000,
|
|
45
|
+
agent = undefined,
|
|
46
|
+
}) {
|
|
43
47
|
return new Promise((resolve, reject) => {
|
|
44
48
|
const payload = body == null ? null : JSON.stringify(body);
|
|
45
49
|
let req = null;
|
|
@@ -69,6 +73,7 @@ function request({ port, method = 'GET', path = '/', token, body = null, timeout
|
|
|
69
73
|
port,
|
|
70
74
|
path,
|
|
71
75
|
method,
|
|
76
|
+
agent,
|
|
72
77
|
headers: {
|
|
73
78
|
...(token ? { 'X-Mixdog-Daemon-Token': token } : {}),
|
|
74
79
|
...(payload
|
|
@@ -107,7 +112,7 @@ function request({ port, method = 'GET', path = '/', token, body = null, timeout
|
|
|
107
112
|
});
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
export async function
|
|
115
|
+
export async function probeChannelHealth({ port, token, timeoutMs = 800 } = {}) {
|
|
111
116
|
try {
|
|
112
117
|
const health = await request({ port, token, path: '/health', timeoutMs });
|
|
113
118
|
return health?.status === 'ok' ? health : null;
|
|
@@ -119,7 +124,7 @@ export async function probeDaemonHealth({ port, token, timeoutMs = 800 } = {}) {
|
|
|
119
124
|
// dispatches channel tools over HTTP. onNotify receives the SAME
|
|
120
125
|
// `{type:'notify', method, params}` shape the old IPC path delivered, so the
|
|
121
126
|
// TUI-side onNotify handler stays unchanged (thin glue).
|
|
122
|
-
export async function
|
|
127
|
+
export async function attachChannel({
|
|
123
128
|
discovery,
|
|
124
129
|
leadPid = process.pid,
|
|
125
130
|
cwd = process.cwd(),
|
|
@@ -130,6 +135,12 @@ export async function attachToDaemon({
|
|
|
130
135
|
const expectedPid = parsePid(discovery?.pid);
|
|
131
136
|
if (!discovery?.port || !discovery?.token || !expectedPid) throw new Error('daemon discovery {port, pid, token} required');
|
|
132
137
|
const { port, token: serverToken } = discovery;
|
|
138
|
+
const callAgent = new http.Agent({
|
|
139
|
+
keepAlive: true, keepAliveMsecs: 5_000, maxSockets: 16, maxFreeSockets: 4,
|
|
140
|
+
});
|
|
141
|
+
const controlAgent = new http.Agent({
|
|
142
|
+
keepAlive: true, keepAliveMsecs: 5_000, maxSockets: 4, maxFreeSockets: 2,
|
|
143
|
+
});
|
|
133
144
|
const staleDiscoveryError = (reason) => {
|
|
134
145
|
const err = new Error(reason);
|
|
135
146
|
err.daemonDiscoveryStale = true;
|
|
@@ -137,7 +148,7 @@ export async function attachToDaemon({
|
|
|
137
148
|
};
|
|
138
149
|
const isExpectedDaemon = (health) => Number(health?.pid) === expectedPid;
|
|
139
150
|
|
|
140
|
-
const initialHealth = await
|
|
151
|
+
const initialHealth = await probeChannelHealth({ port, token: serverToken, timeoutMs: 800 });
|
|
141
152
|
if (!isExpectedDaemon(initialHealth)) throw staleDiscoveryError('daemon discovery pid does not match health');
|
|
142
153
|
|
|
143
154
|
let reg;
|
|
@@ -147,12 +158,11 @@ export async function attachToDaemon({
|
|
|
147
158
|
token: serverToken,
|
|
148
159
|
method: 'POST',
|
|
149
160
|
path: '/client/register',
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
// terminal still served by an older machine-global daemon.
|
|
154
|
-
body: { leadPid, cwd, reattach: true },
|
|
161
|
+
// Channel attachment observes the current owner and never claims the
|
|
162
|
+
// manual routing seat merely by opening the transport.
|
|
163
|
+
body: { leadPid, cwd, passive: true },
|
|
155
164
|
timeoutMs: 3000,
|
|
165
|
+
agent: controlAgent,
|
|
156
166
|
});
|
|
157
167
|
} catch (err) {
|
|
158
168
|
if (err?.statusCode === 401 || err?.statusCode === 403) {
|
|
@@ -193,6 +203,7 @@ export async function attachToDaemon({
|
|
|
193
203
|
...(registrationId ? { registrationId, replaceToken, leadPid, cwd } : {}),
|
|
194
204
|
},
|
|
195
205
|
timeoutMs: 1500,
|
|
206
|
+
agent: controlAgent,
|
|
196
207
|
});
|
|
197
208
|
} catch { /* best-effort; daemon sweep reaps us */ }
|
|
198
209
|
}
|
|
@@ -219,7 +230,7 @@ export async function attachToDaemon({
|
|
|
219
230
|
function handleStreamLoss(reason) {
|
|
220
231
|
if (closed || fatal || reconnectTimer || reconnectProbe) return;
|
|
221
232
|
reconnectProbe = true;
|
|
222
|
-
void
|
|
233
|
+
void probeChannelHealth({ port, token: serverToken, timeoutMs: 800 }).then((health) => {
|
|
223
234
|
reconnectProbe = false;
|
|
224
235
|
if (closed || fatal) return;
|
|
225
236
|
if (!isExpectedDaemon(health)) {
|
|
@@ -310,17 +321,17 @@ export async function attachToDaemon({
|
|
|
310
321
|
// Re-register (the daemon may have pruned us) then reopen the stream.
|
|
311
322
|
// Re-register mints a fresh client token (the old one was pruned); adopt
|
|
312
323
|
// it so the reopened stream and subsequent calls target the live entry.
|
|
313
|
-
//
|
|
314
|
-
// the ownership seat (last-wins is reserved for genuine fresh registers).
|
|
324
|
+
// A reconnect is passive and must not claim the ownership seat.
|
|
315
325
|
const generation = lifecycle;
|
|
316
326
|
const registrationId = reconnectRegistrationId ||= randomUUID();
|
|
317
327
|
const replaceToken = reconnectReplaceToken ||= clientToken;
|
|
318
328
|
const registration = request({
|
|
319
329
|
port, token: serverToken, method: 'POST', path: '/client/register',
|
|
320
330
|
body: {
|
|
321
|
-
leadPid, cwd,
|
|
331
|
+
leadPid, cwd, passive: true, replaceToken,
|
|
322
332
|
registrationId,
|
|
323
333
|
}, timeoutMs: 3000,
|
|
334
|
+
agent: controlAgent,
|
|
324
335
|
}).then(async (r) => {
|
|
325
336
|
const freshToken = r?.token;
|
|
326
337
|
if (!freshToken) return false;
|
|
@@ -356,6 +367,7 @@ export async function attachToDaemon({
|
|
|
356
367
|
// a retried transport failure to a single side-effect.
|
|
357
368
|
body: { token: clientToken, name, args: args || {}, ...(callId ? { callId } : {}) },
|
|
358
369
|
timeoutMs,
|
|
370
|
+
agent: callAgent,
|
|
359
371
|
});
|
|
360
372
|
} catch (err) {
|
|
361
373
|
// Transport failure (daemon dead/restarted/unreachable) — tag so the
|
|
@@ -383,6 +395,8 @@ export async function attachToDaemon({
|
|
|
383
395
|
closePromise = (async () => {
|
|
384
396
|
if (pendingRegistration) await pendingRegistration;
|
|
385
397
|
await deregister(clientToken, { registrationId, replaceToken });
|
|
398
|
+
callAgent.destroy();
|
|
399
|
+
controlAgent.destroy();
|
|
386
400
|
log(`detached (${reason})`);
|
|
387
401
|
})();
|
|
388
402
|
return closePromise;
|