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,894 +0,0 @@
|
|
|
1
|
-
// Engine daemon client: spawn-or-attach + the REMOTE ENGINE PROXY that both
|
|
2
|
-
// the terminal TUI and the desktop engine host consume in place of a local
|
|
3
|
-
// createEngineSession(). The proxy keeps the store contract intact —
|
|
4
|
-
// getState() stays synchronous by mirroring the daemon's snapshot frames, and
|
|
5
|
-
// every other method is forwarded over the transport.
|
|
6
|
-
import http from 'node:http';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { fork } from 'node:child_process';
|
|
10
|
-
import { readFileSync } from 'node:fs';
|
|
11
|
-
import { randomUUID } from 'node:crypto';
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import {
|
|
14
|
-
ENGINE_DAEMON_PROTOCOL,
|
|
15
|
-
} from './engine-daemon-protocol.mjs';
|
|
16
|
-
import { engineDaemonLocalBridge } from './engine-daemon-local-bridge.mjs';
|
|
17
|
-
|
|
18
|
-
function runtimeRoot() {
|
|
19
|
-
return process.env.MIXDOG_RUNTIME_ROOT
|
|
20
|
-
? path.resolve(process.env.MIXDOG_RUNTIME_ROOT)
|
|
21
|
-
: path.join(os.tmpdir(), 'mixdog');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function engineDaemonDiscoveryPath() {
|
|
25
|
-
return path.join(runtimeRoot(), 'engine-daemon.json');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function daemonEntry() {
|
|
29
|
-
// ONE daemon: the channels/memory host also owns the engine pool, so both
|
|
30
|
-
// spawn paths converge on the same singleton process.
|
|
31
|
-
return fileURLToPath(new URL('./backend-daemon.mjs', import.meta.url));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Data calls and lifecycle control must never share a socket pool. A burst of
|
|
35
|
-
// long /call requests can occupy every data socket; health/register/deregister
|
|
36
|
-
// still need a reserved lane so a new terminal can always attach or recover.
|
|
37
|
-
const daemonCallAgent = new http.Agent({ keepAlive: true, keepAliveMsecs: 5_000, maxSockets: 8 });
|
|
38
|
-
const daemonControlAgent = new http.Agent({ keepAlive: true, keepAliveMsecs: 5_000, maxSockets: 4 });
|
|
39
|
-
|
|
40
|
-
function request({
|
|
41
|
-
port,
|
|
42
|
-
token,
|
|
43
|
-
method = 'GET',
|
|
44
|
-
path: urlPath,
|
|
45
|
-
body = null,
|
|
46
|
-
timeoutMs = 120_000,
|
|
47
|
-
control = false,
|
|
48
|
-
}) {
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
const payload = body ? JSON.stringify(body) : null;
|
|
51
|
-
const req = http.request({
|
|
52
|
-
hostname: '127.0.0.1',
|
|
53
|
-
port,
|
|
54
|
-
path: urlPath,
|
|
55
|
-
method,
|
|
56
|
-
agent: control ? daemonControlAgent : daemonCallAgent,
|
|
57
|
-
headers: {
|
|
58
|
-
'X-Mixdog-Daemon-Token': token,
|
|
59
|
-
...(payload ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
60
|
-
},
|
|
61
|
-
timeout: timeoutMs,
|
|
62
|
-
}, (res) => {
|
|
63
|
-
let data = '';
|
|
64
|
-
res.setEncoding('utf8');
|
|
65
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
66
|
-
res.on('end', () => {
|
|
67
|
-
let parsed = null;
|
|
68
|
-
try { parsed = data ? JSON.parse(data) : {}; } catch { parsed = null; }
|
|
69
|
-
if (res.statusCode && res.statusCode >= 400) {
|
|
70
|
-
const err = new Error(parsed?.error || data || `HTTP ${res.statusCode}`);
|
|
71
|
-
err.statusCode = res.statusCode;
|
|
72
|
-
reject(err);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
resolve(parsed ?? {});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
req.on('timeout', () => { req.destroy(new Error(`engine daemon timeout: ${method} ${urlPath}`)); });
|
|
79
|
-
req.on('error', reject);
|
|
80
|
-
if (payload) req.write(payload);
|
|
81
|
-
req.end();
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function probeEngineDaemonHealth({ port, token, timeoutMs = 800 } = {}) {
|
|
86
|
-
try {
|
|
87
|
-
const health = await request({ port, token, path: '/health', timeoutMs, control: true });
|
|
88
|
-
return health?.status === 'ok' ? health : null;
|
|
89
|
-
} catch { return null; }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function readEngineDaemonDiscovery(discoveryPath = engineDaemonDiscoveryPath()) {
|
|
93
|
-
try {
|
|
94
|
-
const parsed = JSON.parse(readFileSync(discoveryPath, 'utf8'));
|
|
95
|
-
if (!parsed?.port || !parsed?.token || !parsed?.pid) return null;
|
|
96
|
-
return parsed;
|
|
97
|
-
} catch { return null; }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
101
|
-
|
|
102
|
-
/** Version skew verdict for a live daemon:
|
|
103
|
-
* ok — same wire protocol, attach. A different BUILD is not a reason to
|
|
104
|
-
* touch it: the daemon dies with its last client, so closing the app
|
|
105
|
-
* and launching the new version is what picks up new code.
|
|
106
|
-
* restart — the daemon speaks an OLDER protocol; stop it and open fresh
|
|
107
|
-
* engines on a daemon this client can talk to.
|
|
108
|
-
* defer — the daemon is NEWER (another, newer install owns the box); this
|
|
109
|
-
* client must not kill it and must surface the version conflict. */
|
|
110
|
-
export function negotiateEngineDaemon(health) {
|
|
111
|
-
const protocol = Number(health?.protocol) || 0;
|
|
112
|
-
if (protocol > ENGINE_DAEMON_PROTOCOL) return 'defer';
|
|
113
|
-
if (protocol < ENGINE_DAEMON_PROTOCOL) return 'restart';
|
|
114
|
-
return 'ok';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function waitForDaemonExit(discovery, timeoutMs = 5_000) {
|
|
118
|
-
const deadline = Date.now() + timeoutMs;
|
|
119
|
-
while (Date.now() < deadline) {
|
|
120
|
-
const health = await probeEngineDaemonHealth({ port: discovery.port, token: discovery.token, timeoutMs: 400 });
|
|
121
|
-
if (!health || Number(health.pid) !== Number(discovery.pid)) return true;
|
|
122
|
-
await delay(150);
|
|
123
|
-
}
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Fork one daemon candidate DETACHED (it outlives this client — machine
|
|
128
|
-
* global) and resolve when it reports ready OR exits (race loss/crash); the
|
|
129
|
-
* caller then re-reads discovery and attaches to whoever won. */
|
|
130
|
-
function spawnDaemonCandidate({ cwd, log }) {
|
|
131
|
-
return new Promise((resolve) => {
|
|
132
|
-
let settled = false;
|
|
133
|
-
const done = () => { if (!settled) { settled = true; resolve(); } };
|
|
134
|
-
let child;
|
|
135
|
-
try {
|
|
136
|
-
child = fork(daemonEntry(), [], {
|
|
137
|
-
cwd,
|
|
138
|
-
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
139
|
-
detached: process.platform !== 'win32',
|
|
140
|
-
windowsHide: true,
|
|
141
|
-
env: {
|
|
142
|
-
...process.env,
|
|
143
|
-
MIXDOG_ENGINE_DAEMON_HOST: '1',
|
|
144
|
-
// Engine-only spawn: the backend stays dormant on the channels side
|
|
145
|
-
// until a channels client registers.
|
|
146
|
-
MIXDOG_DAEMON_SPAWNED_FOR: 'engine',
|
|
147
|
-
MIXDOG_SUPERVISOR_PID: process.env.MIXDOG_SUPERVISOR_PID || String(process.pid),
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
} catch (err) {
|
|
151
|
-
log(`daemon spawn failed: ${err?.message || err}`);
|
|
152
|
-
done();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const mirror = (chunk) => {
|
|
156
|
-
const text = String(chunk || '').trimEnd();
|
|
157
|
-
if (text) log(text);
|
|
158
|
-
};
|
|
159
|
-
child.stderr?.on('data', mirror);
|
|
160
|
-
child.once('message', (msg) => {
|
|
161
|
-
if (msg?.type !== 'ready') return;
|
|
162
|
-
try { child.stderr?.off?.('data', mirror); } catch {}
|
|
163
|
-
try { child.disconnect?.(); } catch {}
|
|
164
|
-
try { child.unref?.(); } catch {}
|
|
165
|
-
try { child.stderr?.unref?.(); } catch {}
|
|
166
|
-
done();
|
|
167
|
-
});
|
|
168
|
-
child.once('exit', done);
|
|
169
|
-
child.once('error', (err) => { log(`daemon spawn error: ${err?.message || err}`); done(); });
|
|
170
|
-
const timer = setTimeout(done, 30_000);
|
|
171
|
-
timer.unref?.();
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Spawn-or-attach discovery for the machine-global engine daemon. */
|
|
176
|
-
export async function ensureEngineDaemon({ cwd = process.cwd(), log = () => {}, attempts = 5 } = {}) {
|
|
177
|
-
// A daemon we asked to drain but that kept serving live clients is a
|
|
178
|
-
// DIFFERENT failure from "no daemon came up": the user has to close the
|
|
179
|
-
// running views, so say that instead of a generic unavailability.
|
|
180
|
-
let upgradeBlocked = null;
|
|
181
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
182
|
-
const discovery = readEngineDaemonDiscovery();
|
|
183
|
-
if (discovery) {
|
|
184
|
-
const health = await probeEngineDaemonHealth({ port: discovery.port, token: discovery.token });
|
|
185
|
-
if (health && Number(health.pid) === Number(discovery.pid)) {
|
|
186
|
-
const verdict = negotiateEngineDaemon(health);
|
|
187
|
-
if (verdict === 'ok') return discovery;
|
|
188
|
-
if (verdict === 'defer') {
|
|
189
|
-
const err = new Error(
|
|
190
|
-
`engine daemon ${health.version || '?'} (protocol ${health.protocol ?? 0}) is newer than this client`,
|
|
191
|
-
);
|
|
192
|
-
err.daemonNewerThanClient = true;
|
|
193
|
-
throw err;
|
|
194
|
-
}
|
|
195
|
-
log(`stopping older engine daemon ${health.version || '?'} (protocol ${health.protocol ?? 0})`);
|
|
196
|
-
await shutdownEngineDaemon(discovery);
|
|
197
|
-
upgradeBlocked = await waitForDaemonExit(discovery) ? null : health;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
await spawnDaemonCandidate({ cwd, log });
|
|
201
|
-
await delay(100);
|
|
202
|
-
}
|
|
203
|
-
if (upgradeBlocked) {
|
|
204
|
-
const err = new Error(
|
|
205
|
-
`an older Mixdog backend (protocol ${upgradeBlocked.protocol ?? 0}) is still serving live clients`
|
|
206
|
-
+ ' — close the running Mixdog windows and terminals, then start again',
|
|
207
|
-
);
|
|
208
|
-
err.daemonUpgradeBlocked = true;
|
|
209
|
-
throw err;
|
|
210
|
-
}
|
|
211
|
-
throw new Error('engine daemon is unavailable');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** Attach to a live daemon: register, open the frame stream, return a handle
|
|
215
|
-
* whose call() dispatches engine operations. */
|
|
216
|
-
export async function attachEngineDaemon({
|
|
217
|
-
discovery,
|
|
218
|
-
leadPid = process.pid,
|
|
219
|
-
cwd = process.cwd(),
|
|
220
|
-
lifecycle = true,
|
|
221
|
-
registrationId = randomUUID(),
|
|
222
|
-
onFrame = () => {},
|
|
223
|
-
onFatal = () => {},
|
|
224
|
-
log = () => {},
|
|
225
|
-
} = {}) {
|
|
226
|
-
if (!discovery?.port || !discovery?.token) throw new Error('daemon discovery {port, token} required');
|
|
227
|
-
const { port, token: serverToken } = discovery;
|
|
228
|
-
const stableRegistrationId = String(registrationId || randomUUID());
|
|
229
|
-
let reg = null;
|
|
230
|
-
let registrationError = null;
|
|
231
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
232
|
-
try {
|
|
233
|
-
reg = await request({
|
|
234
|
-
port, token: serverToken, method: 'POST', path: '/client/register',
|
|
235
|
-
body: {
|
|
236
|
-
leadPid,
|
|
237
|
-
cwd,
|
|
238
|
-
lifecycle: lifecycle !== false,
|
|
239
|
-
registrationId: stableRegistrationId,
|
|
240
|
-
},
|
|
241
|
-
timeoutMs: 3000,
|
|
242
|
-
control: true,
|
|
243
|
-
});
|
|
244
|
-
break;
|
|
245
|
-
} catch (error) {
|
|
246
|
-
registrationError = error;
|
|
247
|
-
if (Number(error?.statusCode) >= 400 && Number(error?.statusCode) < 500) throw error;
|
|
248
|
-
if (attempt < 2) await delay(100 * (attempt + 1));
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (!reg) throw registrationError || new Error('engine daemon registration failed');
|
|
252
|
-
const clientToken = reg?.token;
|
|
253
|
-
if (!clientToken) throw new Error('engine daemon register returned no client token');
|
|
254
|
-
|
|
255
|
-
let closed = false;
|
|
256
|
-
let streamRequest = null;
|
|
257
|
-
|
|
258
|
-
function openStream() {
|
|
259
|
-
if (closed) return;
|
|
260
|
-
const req = http.request({
|
|
261
|
-
hostname: '127.0.0.1',
|
|
262
|
-
port,
|
|
263
|
-
path: `/events?token=${encodeURIComponent(clientToken)}&server_token=${encodeURIComponent(serverToken)}`,
|
|
264
|
-
method: 'GET',
|
|
265
|
-
headers: { Accept: 'text/event-stream', 'X-Mixdog-Daemon-Token': serverToken },
|
|
266
|
-
}, (res) => {
|
|
267
|
-
if (req !== streamRequest || closed) { res.resume(); return; }
|
|
268
|
-
if (res.statusCode !== 200) {
|
|
269
|
-
res.resume();
|
|
270
|
-
if (!closed) { try { onFatal(`bad sse status ${res.statusCode}`); } catch {} }
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
res.setEncoding('utf8');
|
|
274
|
-
let buffer = '';
|
|
275
|
-
res.on('data', (chunk) => {
|
|
276
|
-
if (req !== streamRequest || closed) return;
|
|
277
|
-
buffer += chunk;
|
|
278
|
-
let index;
|
|
279
|
-
while ((index = buffer.indexOf('\n\n')) >= 0) {
|
|
280
|
-
const raw = buffer.slice(0, index);
|
|
281
|
-
buffer = buffer.slice(index + 2);
|
|
282
|
-
for (const line of raw.split('\n')) {
|
|
283
|
-
if (!line.startsWith('data:')) continue; // ': ka' keepalives
|
|
284
|
-
const json = line.slice(5).trim();
|
|
285
|
-
if (!json) continue;
|
|
286
|
-
let frame = null;
|
|
287
|
-
try { frame = JSON.parse(json); } catch { continue; }
|
|
288
|
-
try { onFrame(frame); } catch (err) { log(`onFrame threw: ${err?.message || err}`); }
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
const lost = (reason) => {
|
|
293
|
-
if (req !== streamRequest || closed) return;
|
|
294
|
-
try { onFatal(reason); } catch {}
|
|
295
|
-
};
|
|
296
|
-
res.on('end', () => lost('sse ended'));
|
|
297
|
-
res.on('error', () => lost('sse error'));
|
|
298
|
-
});
|
|
299
|
-
req.on('error', () => { if (req === streamRequest && !closed) { try { onFatal('sse request error'); } catch {} } });
|
|
300
|
-
streamRequest = req;
|
|
301
|
-
req.end();
|
|
302
|
-
}
|
|
303
|
-
openStream();
|
|
304
|
-
|
|
305
|
-
async function call(name, args = {}, { timeoutMs = 300_000, callId = null } = {}) {
|
|
306
|
-
let out;
|
|
307
|
-
try {
|
|
308
|
-
out = await request({
|
|
309
|
-
port, token: serverToken, method: 'POST', path: '/call',
|
|
310
|
-
body: { token: clientToken, name, args: args || {}, ...(callId ? { callId } : {}) },
|
|
311
|
-
timeoutMs,
|
|
312
|
-
});
|
|
313
|
-
} catch (err) {
|
|
314
|
-
// Transport death (daemon restarted/unreachable) is recoverable by
|
|
315
|
-
// re-attaching; an engine error comes back as a 200 {error} envelope.
|
|
316
|
-
err.daemonTransportError = true;
|
|
317
|
-
throw err;
|
|
318
|
-
}
|
|
319
|
-
if (out && out.error) throw new Error(out.error);
|
|
320
|
-
return out?.result;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
async function close(reason = 'client close') {
|
|
324
|
-
if (closed) return;
|
|
325
|
-
closed = true;
|
|
326
|
-
try { streamRequest?.destroy?.(); } catch {}
|
|
327
|
-
try {
|
|
328
|
-
await request({
|
|
329
|
-
port, token: serverToken, method: 'POST', path: '/client/deregister',
|
|
330
|
-
body: { token: clientToken }, timeoutMs: 1500, control: true,
|
|
331
|
-
});
|
|
332
|
-
} catch { /* the daemon sweep reaps us anyway */ }
|
|
333
|
-
log(`detached (${reason})`);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return { call, close, clientToken, port, pid: Number(discovery.pid) };
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** Ask a live daemon to exit. Used by dev/test teardown; normal clients just
|
|
340
|
-
* detach and let the client-grace shutdown fire. */
|
|
341
|
-
export async function shutdownEngineDaemon(discovery = readEngineDaemonDiscovery()) {
|
|
342
|
-
if (!discovery?.port) return false;
|
|
343
|
-
try {
|
|
344
|
-
await request({
|
|
345
|
-
port: discovery.port, token: discovery.token, method: 'POST',
|
|
346
|
-
path: '/shutdown', body: {}, timeoutMs: 3000, control: true,
|
|
347
|
-
});
|
|
348
|
-
return true;
|
|
349
|
-
} catch { return false; }
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ── Shared per-process attachment ────────────────────────────────────────────
|
|
353
|
-
// Every session view in this process rides ONE attachment. Frames and calls are
|
|
354
|
-
// addressed only by durable sessionId; daemon engine handles never cross wire.
|
|
355
|
-
let shared = null;
|
|
356
|
-
|
|
357
|
-
function addSessionView(views, sessionId, view) {
|
|
358
|
-
let bucket = views.get(sessionId);
|
|
359
|
-
if (!bucket) { bucket = new Set(); views.set(sessionId, bucket); }
|
|
360
|
-
bucket.add(view);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function removeSessionView(views, sessionId, view) {
|
|
364
|
-
const bucket = views.get(sessionId);
|
|
365
|
-
if (!bucket) return;
|
|
366
|
-
bucket.delete(view);
|
|
367
|
-
if (bucket.size === 0) views.delete(sessionId);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function sessionViewCount(views, sessionId) {
|
|
371
|
-
return views.get(sessionId)?.size ?? 0;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function applyStatePatch(state, patch) {
|
|
375
|
-
const next = { ...state, ...(patch.set || {}) };
|
|
376
|
-
if (patch.itemsAppend) {
|
|
377
|
-
const base = Array.isArray(state.items) ? state.items : [];
|
|
378
|
-
next.items = base.slice(0, patch.itemsAppend.from).concat(patch.itemsAppend.values || []);
|
|
379
|
-
}
|
|
380
|
-
for (const key of patch.remove || []) delete next[key];
|
|
381
|
-
return next;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const liveViews = new Set();
|
|
385
|
-
let reattachTimer = null;
|
|
386
|
-
|
|
387
|
-
function scheduleReattach({ cwd, log }) {
|
|
388
|
-
if (reattachTimer || liveViews.size === 0) return;
|
|
389
|
-
reattachTimer = setTimeout(() => {
|
|
390
|
-
reattachTimer = null;
|
|
391
|
-
if (liveViews.size === 0) return;
|
|
392
|
-
void ensureSharedAttachment({ cwd, log }).catch((err) => {
|
|
393
|
-
log(`backend daemon re-attach failed: ${err?.message || err}`);
|
|
394
|
-
scheduleReattach({ cwd, log });
|
|
395
|
-
});
|
|
396
|
-
}, 500);
|
|
397
|
-
reattachTimer.unref?.();
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
async function ensureSharedAttachment({ cwd, log }) {
|
|
401
|
-
if (shared?.client) return shared;
|
|
402
|
-
if (shared?.pending) return shared.pending;
|
|
403
|
-
const pending = (async () => {
|
|
404
|
-
const localBridge = process.env.MIXDOG_ENGINE_DAEMON_HOST === '1'
|
|
405
|
-
? engineDaemonLocalBridge()
|
|
406
|
-
: null;
|
|
407
|
-
const discovery = localBridge
|
|
408
|
-
? { pid: process.pid, local: true }
|
|
409
|
-
: await ensureEngineDaemon({ cwd, log });
|
|
410
|
-
const views = new Map();
|
|
411
|
-
const state = { client: null, views, discovery, refs: liveViews.size, log, cwd };
|
|
412
|
-
const attachmentOptions = {
|
|
413
|
-
cwd,
|
|
414
|
-
log,
|
|
415
|
-
onFrame: (frame) => {
|
|
416
|
-
const bucket = views.get(String(frame?.sessionId || ''));
|
|
417
|
-
if (!bucket) return;
|
|
418
|
-
for (const view of [...bucket]) {
|
|
419
|
-
if (frame.type === 'session-state') view.applyFrame(frame);
|
|
420
|
-
else if (frame.type === 'session-gone') view.applyGone(frame.reason || 'session unloaded');
|
|
421
|
-
}
|
|
422
|
-
},
|
|
423
|
-
onFatal: (reason) => {
|
|
424
|
-
log(`backend daemon attachment lost (${reason})`);
|
|
425
|
-
if (shared === state) shared = null;
|
|
426
|
-
for (const bucket of views.values()) {
|
|
427
|
-
for (const view of [...bucket]) view.applyDetached(reason);
|
|
428
|
-
}
|
|
429
|
-
scheduleReattach({ cwd: state.cwd, log });
|
|
430
|
-
},
|
|
431
|
-
};
|
|
432
|
-
state.client = localBridge
|
|
433
|
-
? await localBridge.attach(attachmentOptions)
|
|
434
|
-
: await attachEngineDaemon({ discovery, ...attachmentOptions });
|
|
435
|
-
shared = state;
|
|
436
|
-
for (const view of liveViews) {
|
|
437
|
-
addSessionView(views, view.sessionId(), view);
|
|
438
|
-
void view.recover(state).catch((err) => {
|
|
439
|
-
log(`session ${view.sessionId()} recovery failed: ${err?.message || err}`);
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
return state;
|
|
443
|
-
})();
|
|
444
|
-
shared = { pending };
|
|
445
|
-
try {
|
|
446
|
-
return await pending;
|
|
447
|
-
} catch (err) {
|
|
448
|
-
shared = null;
|
|
449
|
-
throw err;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const LOCAL_ENGINE_KEYS = new Set([
|
|
454
|
-
'getState', 'subscribe', 'dispose', 'isRemoteEngine', 'disposedView',
|
|
455
|
-
'then', 'catch', 'finally', 'toJSON', 'inspect', Symbol.toStringTag,
|
|
456
|
-
]);
|
|
457
|
-
|
|
458
|
-
/** Remote counterpart of createEngineSession(): this is a session projection,
|
|
459
|
-
* while execution and durable intake stay in the backend daemon. */
|
|
460
|
-
export async function createRemoteEngineSession(options = {}) {
|
|
461
|
-
const log = typeof options.log === 'function' ? options.log : () => {};
|
|
462
|
-
const cwd = options.cwd || process.cwd();
|
|
463
|
-
const openParams = {
|
|
464
|
-
cwd,
|
|
465
|
-
provider: options.provider,
|
|
466
|
-
model: options.model,
|
|
467
|
-
toolMode: options.toolMode || 'full',
|
|
468
|
-
remote: options.remote === true,
|
|
469
|
-
desktopSession: options.desktopSession ?? null,
|
|
470
|
-
};
|
|
471
|
-
let attachment = await ensureSharedAttachment({ cwd, log });
|
|
472
|
-
const created = await attachment.client.call('session.create', openParams, {
|
|
473
|
-
callId: `session-create:${process.pid}:${randomUUID()}`,
|
|
474
|
-
});
|
|
475
|
-
let sessionId = String(created?.sessionId || '');
|
|
476
|
-
if (!sessionId) throw new Error('session.create returned no sessionId');
|
|
477
|
-
let state = created?.full ?? {};
|
|
478
|
-
let revision = Number(created?.revision) || 0;
|
|
479
|
-
let sync = created?.sync ?? {};
|
|
480
|
-
let disposed = false;
|
|
481
|
-
const listeners = new Set();
|
|
482
|
-
|
|
483
|
-
function emit() {
|
|
484
|
-
for (const listener of [...listeners]) {
|
|
485
|
-
try { listener(); } catch (err) { log(`session listener threw: ${err?.message || err}`); }
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function applyBody(body) {
|
|
490
|
-
if (disposed || !body) return false;
|
|
491
|
-
if (body.full !== undefined && body.full !== null) {
|
|
492
|
-
state = body.full;
|
|
493
|
-
revision = Number(body.revision) || revision;
|
|
494
|
-
emit();
|
|
495
|
-
return true;
|
|
496
|
-
}
|
|
497
|
-
if (body.patch) {
|
|
498
|
-
// The daemon answers a state-changing call AND broadcasts the same step,
|
|
499
|
-
// so every such call delivers one revision twice. Recognising the
|
|
500
|
-
// duplicate as a no-op keeps it from looking like a revision gap, which
|
|
501
|
-
// used to cost one extra full session.read per call.
|
|
502
|
-
if (Number(body.revision) === revision
|
|
503
|
-
&& Number(body.baseRevision) === revision - 1) return true;
|
|
504
|
-
if (Number(body.baseRevision) !== revision) return false;
|
|
505
|
-
state = applyStatePatch(state, body.patch);
|
|
506
|
-
revision = Number(body.revision) || revision;
|
|
507
|
-
emit();
|
|
508
|
-
return true;
|
|
509
|
-
}
|
|
510
|
-
if (Number.isFinite(Number(body.revision))) {
|
|
511
|
-
revision = Number(body.revision);
|
|
512
|
-
return true;
|
|
513
|
-
}
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
let resyncing = false;
|
|
518
|
-
function resync(reason) {
|
|
519
|
-
if (disposed || resyncing) return;
|
|
520
|
-
resyncing = true;
|
|
521
|
-
void attachment.client.call('session.read', {
|
|
522
|
-
sessionId, open: openParams, baseRevision: revision,
|
|
523
|
-
}).then((result) => {
|
|
524
|
-
if (disposed) return;
|
|
525
|
-
if (result?.sync) sync = result.sync;
|
|
526
|
-
if (!applyBody(result) && result?.revision !== undefined) {
|
|
527
|
-
log(`session ${sessionId} resync returned an unusable body`);
|
|
528
|
-
}
|
|
529
|
-
}).catch((err) => {
|
|
530
|
-
log(`session ${sessionId} resync after ${reason} failed: ${err?.message || err}`);
|
|
531
|
-
}).finally(() => { resyncing = false; });
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
let recoveryPromise = null;
|
|
535
|
-
let view;
|
|
536
|
-
async function recover(nextAttachment) {
|
|
537
|
-
if (disposed) return;
|
|
538
|
-
if (recoveryPromise) return recoveryPromise;
|
|
539
|
-
recoveryPromise = (async () => {
|
|
540
|
-
const previousAttachment = attachment;
|
|
541
|
-
if (previousAttachment !== nextAttachment) {
|
|
542
|
-
removeSessionView(previousAttachment.views, sessionId, view);
|
|
543
|
-
attachment = nextAttachment;
|
|
544
|
-
}
|
|
545
|
-
let result;
|
|
546
|
-
try {
|
|
547
|
-
result = await attachment.client.call('session.subscribe', {
|
|
548
|
-
sessionId, open: openParams, baseRevision: revision,
|
|
549
|
-
});
|
|
550
|
-
} catch {
|
|
551
|
-
result = await attachment.client.call('session.create', {
|
|
552
|
-
...openParams, sessionId,
|
|
553
|
-
}, { callId: `session-recreate:${sessionId}` });
|
|
554
|
-
}
|
|
555
|
-
addSessionView(attachment.views, sessionId, view);
|
|
556
|
-
if (result?.sync) sync = result.sync;
|
|
557
|
-
if (!applyBody(result) && result?.revision !== undefined) resync('recovery body gap');
|
|
558
|
-
log(`session ${sessionId} projection recovered`);
|
|
559
|
-
})();
|
|
560
|
-
try {
|
|
561
|
-
return await recoveryPromise;
|
|
562
|
-
} finally {
|
|
563
|
-
recoveryPromise = null;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
view = {
|
|
568
|
-
sessionId: () => sessionId,
|
|
569
|
-
applyFrame(frame) {
|
|
570
|
-
if (disposed) return;
|
|
571
|
-
if (!applyBody(frame)) resync('revision gap');
|
|
572
|
-
},
|
|
573
|
-
applyGone(reason) {
|
|
574
|
-
if (disposed) return;
|
|
575
|
-
void recover(attachment).catch((err) => {
|
|
576
|
-
log(`session ${sessionId} reload after ${reason} failed: ${err?.message || err}`);
|
|
577
|
-
});
|
|
578
|
-
},
|
|
579
|
-
applyDetached(reason) {
|
|
580
|
-
log(`session ${sessionId} projection detached (${reason})`);
|
|
581
|
-
},
|
|
582
|
-
recover,
|
|
583
|
-
};
|
|
584
|
-
addSessionView(attachment.views, sessionId, view);
|
|
585
|
-
attachment.refs += 1;
|
|
586
|
-
liveViews.add(view);
|
|
587
|
-
|
|
588
|
-
async function sendCall(route, payload, callId) {
|
|
589
|
-
const send = () => attachment.client.call(route, payload, { callId });
|
|
590
|
-
try {
|
|
591
|
-
return await send();
|
|
592
|
-
} catch (err) {
|
|
593
|
-
const recoverable = err?.daemonTransportError
|
|
594
|
-
|| /unknown client token/i.test(String(err?.message || ''));
|
|
595
|
-
if (!recoverable || disposed) throw err;
|
|
596
|
-
if (shared === attachment) shared = null;
|
|
597
|
-
const next = await ensureSharedAttachment({ cwd, log });
|
|
598
|
-
await recover(next);
|
|
599
|
-
return await send();
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
async function applyResult(result, reason) {
|
|
604
|
-
const nextSessionId = String(result?.sessionId || sessionId);
|
|
605
|
-
if (nextSessionId && nextSessionId !== sessionId) {
|
|
606
|
-
const previousSessionId = sessionId;
|
|
607
|
-
addSessionView(attachment.views, nextSessionId, view);
|
|
608
|
-
removeSessionView(attachment.views, previousSessionId, view);
|
|
609
|
-
sessionId = nextSessionId;
|
|
610
|
-
}
|
|
611
|
-
if (result?.sync) sync = result.sync;
|
|
612
|
-
if (!applyBody(result) && result?.revision !== undefined) resync(`${reason} body gap`);
|
|
613
|
-
return result;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
let callChain = Promise.resolve();
|
|
617
|
-
function serialize(task) {
|
|
618
|
-
const run = callChain.then(task);
|
|
619
|
-
callChain = run.then(() => {}, () => {});
|
|
620
|
-
return run;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function remoteCall(method, args, callOptions = {}) {
|
|
624
|
-
return serialize(async () => {
|
|
625
|
-
if (disposed) throw new Error('This session view is disposed.');
|
|
626
|
-
const stableCallId = typeof callOptions.callId === 'string' && callOptions.callId.trim()
|
|
627
|
-
? callOptions.callId.trim()
|
|
628
|
-
: randomUUID();
|
|
629
|
-
const result = await sendCall('session.invoke', {
|
|
630
|
-
sessionId,
|
|
631
|
-
method,
|
|
632
|
-
args,
|
|
633
|
-
open: openParams,
|
|
634
|
-
baseRevision: revision,
|
|
635
|
-
}, stableCallId);
|
|
636
|
-
await applyResult(result, method);
|
|
637
|
-
return result?.value ?? null;
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function dispatch(method, args) {
|
|
642
|
-
void remoteCall(method, args).catch((err) => {
|
|
643
|
-
log(`session ${method} failed: ${err?.message || err}`);
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
async function rebindTo(result, previousSessionId) {
|
|
648
|
-
const nextSessionId = String(result?.sessionId || '');
|
|
649
|
-
if (!nextSessionId) throw new Error('session route returned no sessionId');
|
|
650
|
-
const lastLocalView = sessionViewCount(attachment.views, previousSessionId) <= 1;
|
|
651
|
-
addSessionView(attachment.views, nextSessionId, view);
|
|
652
|
-
removeSessionView(attachment.views, previousSessionId, view);
|
|
653
|
-
sessionId = nextSessionId;
|
|
654
|
-
if (result?.sync) sync = result.sync;
|
|
655
|
-
if (!applyBody(result) && result?.revision !== undefined) resync('session rebind body gap');
|
|
656
|
-
if (previousSessionId && previousSessionId !== nextSessionId && lastLocalView) {
|
|
657
|
-
try {
|
|
658
|
-
await attachment.client.call('session.unsubscribe', { sessionId: previousSessionId });
|
|
659
|
-
} catch (err) {
|
|
660
|
-
log(`session ${previousSessionId} unsubscribe failed: ${err?.message || err}`);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
return nextSessionId;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
async function createReservedSession() {
|
|
667
|
-
return serialize(async () => {
|
|
668
|
-
if (disposed) throw new Error('This session view is disposed.');
|
|
669
|
-
const previousSessionId = sessionId;
|
|
670
|
-
const result = await sendCall('session.create', openParams,
|
|
671
|
-
`session-create:${process.pid}:${randomUUID()}`);
|
|
672
|
-
return await rebindTo(result, previousSessionId);
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
async function resumeSession(targetSessionId, resumeOptions) {
|
|
677
|
-
const target = String(targetSessionId || '');
|
|
678
|
-
if (!target) return false;
|
|
679
|
-
return serialize(async () => {
|
|
680
|
-
if (disposed) throw new Error('This session view is disposed.');
|
|
681
|
-
if (target === sessionId) {
|
|
682
|
-
const result = await sendCall('session.read', {
|
|
683
|
-
sessionId, open: openParams, baseRevision: revision,
|
|
684
|
-
}, randomUUID());
|
|
685
|
-
await applyResult(result, 'resume');
|
|
686
|
-
return true;
|
|
687
|
-
}
|
|
688
|
-
const previousSessionId = sessionId;
|
|
689
|
-
const result = await sendCall('session.subscribe', {
|
|
690
|
-
sessionId: target,
|
|
691
|
-
open: { ...openParams, resumeOptions: resumeOptions || undefined },
|
|
692
|
-
baseRevision: null,
|
|
693
|
-
}, randomUUID());
|
|
694
|
-
await rebindTo(result, previousSessionId);
|
|
695
|
-
return true;
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
let submitSeq = 0;
|
|
700
|
-
async function submitAsync(prompt, options = {}) {
|
|
701
|
-
if (disposed) throw new Error('This session view is disposed.');
|
|
702
|
-
const submissionId = String(options?.id || '').trim()
|
|
703
|
-
|| `session-submit-${process.pid}-${Date.now()}-${(submitSeq += 1)}`;
|
|
704
|
-
return serialize(async () => {
|
|
705
|
-
const result = await sendCall('session.submit', {
|
|
706
|
-
sessionId,
|
|
707
|
-
prompt,
|
|
708
|
-
options: { ...(options || {}), id: submissionId },
|
|
709
|
-
open: openParams,
|
|
710
|
-
baseRevision: revision,
|
|
711
|
-
}, `session-submit:${sessionId}:${submissionId}`);
|
|
712
|
-
await applyResult(result, 'submit');
|
|
713
|
-
return result?.accepted === true;
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const base = {
|
|
718
|
-
isRemoteEngine: true,
|
|
719
|
-
get disposedView() { return disposed; },
|
|
720
|
-
getState: () => state,
|
|
721
|
-
subscribe(listener) {
|
|
722
|
-
listeners.add(listener);
|
|
723
|
-
return () => listeners.delete(listener);
|
|
724
|
-
},
|
|
725
|
-
listSessions(options) {
|
|
726
|
-
if (options?.refreshFromStorage) dispatch('listSessions', [options]);
|
|
727
|
-
return Array.isArray(sync.sessions) ? sync.sessions : [];
|
|
728
|
-
},
|
|
729
|
-
sessionStoreDir() {
|
|
730
|
-
return typeof sync.sessionStoreDir === 'string' ? sync.sessionStoreDir : null;
|
|
731
|
-
},
|
|
732
|
-
async newSession() {
|
|
733
|
-
await createReservedSession();
|
|
734
|
-
return true;
|
|
735
|
-
},
|
|
736
|
-
resume: resumeSession,
|
|
737
|
-
async prefetchSession(targetSessionId) {
|
|
738
|
-
const target = String(targetSessionId || '');
|
|
739
|
-
if (!target) return false;
|
|
740
|
-
await callDaemonSession({
|
|
741
|
-
sessionId: target,
|
|
742
|
-
method: 'read',
|
|
743
|
-
open: openParams,
|
|
744
|
-
cwd,
|
|
745
|
-
log,
|
|
746
|
-
attempts: 1,
|
|
747
|
-
});
|
|
748
|
-
return true;
|
|
749
|
-
},
|
|
750
|
-
submitAsync,
|
|
751
|
-
submit(prompt, options) {
|
|
752
|
-
if (disposed) return false;
|
|
753
|
-
void submitAsync(prompt, options).catch((err) => {
|
|
754
|
-
log(`legacy submit failed: ${err?.message || err}`);
|
|
755
|
-
});
|
|
756
|
-
return true;
|
|
757
|
-
},
|
|
758
|
-
abort() {
|
|
759
|
-
void serialize(async () => {
|
|
760
|
-
const result = await sendCall('session.abort', {
|
|
761
|
-
sessionId, open: openParams, baseRevision: revision,
|
|
762
|
-
}, randomUUID());
|
|
763
|
-
await applyResult(result, 'abort');
|
|
764
|
-
}).catch((err) => log(`session abort failed: ${err?.message || err}`));
|
|
765
|
-
return true;
|
|
766
|
-
},
|
|
767
|
-
resolveToolApproval(id, decision) {
|
|
768
|
-
void serialize(async () => {
|
|
769
|
-
const result = await sendCall('session.approve', {
|
|
770
|
-
sessionId,
|
|
771
|
-
approvalId: id,
|
|
772
|
-
decision,
|
|
773
|
-
open: openParams,
|
|
774
|
-
baseRevision: revision,
|
|
775
|
-
}, randomUUID());
|
|
776
|
-
await applyResult(result, 'approval');
|
|
777
|
-
}).catch((err) => log(`session approval failed: ${err?.message || err}`));
|
|
778
|
-
return true;
|
|
779
|
-
},
|
|
780
|
-
async dispose(reason = 'view dispose') {
|
|
781
|
-
if (disposed) return;
|
|
782
|
-
disposed = true;
|
|
783
|
-
liveViews.delete(view);
|
|
784
|
-
const releasedSessionId = sessionId;
|
|
785
|
-
const lastLocalView = sessionViewCount(attachment.views, releasedSessionId) <= 1;
|
|
786
|
-
if (lastLocalView) {
|
|
787
|
-
try {
|
|
788
|
-
await attachment.client.call('session.unsubscribe', { sessionId: releasedSessionId });
|
|
789
|
-
} catch (err) {
|
|
790
|
-
log(`session ${releasedSessionId} unsubscribe failed: ${err?.message || err}`);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
removeSessionView(attachment.views, releasedSessionId, view);
|
|
794
|
-
listeners.clear();
|
|
795
|
-
attachment.refs = Math.max(0, attachment.refs - 1);
|
|
796
|
-
if (attachment.refs === 0 && liveViews.size === 0) {
|
|
797
|
-
try { await attachment.client.close(reason); } catch {}
|
|
798
|
-
// Last view gone: drop pooled keep-alive sockets so an idle connection
|
|
799
|
-
// cannot keep the process (or a test runner) alive.
|
|
800
|
-
try { daemonCallAgent.destroy(); } catch {}
|
|
801
|
-
try { daemonControlAgent.destroy(); } catch {}
|
|
802
|
-
if (shared === attachment) shared = null;
|
|
803
|
-
}
|
|
804
|
-
},
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
return new Proxy(base, {
|
|
808
|
-
get(target, property, receiver) {
|
|
809
|
-
if (property in target || LOCAL_ENGINE_KEYS.has(property)) {
|
|
810
|
-
return Reflect.get(target, property, receiver);
|
|
811
|
-
}
|
|
812
|
-
if (typeof property === 'symbol') return undefined;
|
|
813
|
-
return (...args) => remoteCall(property, args);
|
|
814
|
-
},
|
|
815
|
-
has(target, property) {
|
|
816
|
-
return typeof property === 'string' ? true : Reflect.has(target, property);
|
|
817
|
-
},
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/** Session-addressed backend request from a view that holds NO engine for that
|
|
822
|
-
* session. The daemon owns the engine pool, so a renderer (desktop pane, TUI
|
|
823
|
-
* tab) hands it the request and the daemon resolves — or loads — the session's
|
|
824
|
-
* engine. This is what makes "no live engine here" impossible to hit.
|
|
825
|
-
* Only a dead transport is retried; an engine-side verdict comes straight back. */
|
|
826
|
-
export async function callDaemonSession({
|
|
827
|
-
sessionId,
|
|
828
|
-
method,
|
|
829
|
-
args = [],
|
|
830
|
-
open: openHints = {},
|
|
831
|
-
cwd = process.cwd(),
|
|
832
|
-
log = () => {},
|
|
833
|
-
attempts = 3,
|
|
834
|
-
} = {}) {
|
|
835
|
-
const id = String(sessionId || '');
|
|
836
|
-
if (!id) throw new TypeError('sessionId is required');
|
|
837
|
-
const name = String(method || '');
|
|
838
|
-
if (!name) throw new TypeError('method is required');
|
|
839
|
-
let route = 'session.invoke';
|
|
840
|
-
let payload = { sessionId: id, method: name, args, open: openHints || {} };
|
|
841
|
-
if (name === 'submit') {
|
|
842
|
-
route = 'session.submit';
|
|
843
|
-
payload = {
|
|
844
|
-
sessionId: id,
|
|
845
|
-
prompt: args?.[0],
|
|
846
|
-
options: args?.[1] || {},
|
|
847
|
-
open: openHints || {},
|
|
848
|
-
};
|
|
849
|
-
} else if (name === 'abort') {
|
|
850
|
-
route = 'session.abort';
|
|
851
|
-
payload = { sessionId: id, open: openHints || {} };
|
|
852
|
-
} else if (name === 'resolveToolApproval') {
|
|
853
|
-
route = 'session.approve';
|
|
854
|
-
payload = {
|
|
855
|
-
sessionId: id,
|
|
856
|
-
approvalId: args?.[0],
|
|
857
|
-
decision: args?.[1],
|
|
858
|
-
open: openHints || {},
|
|
859
|
-
};
|
|
860
|
-
} else if (name === 'read') {
|
|
861
|
-
// Prefetch/peek: WARM (or load) the session's engine. This is a daemon
|
|
862
|
-
// route, not an engine method — sending it as session.invoke asked the
|
|
863
|
-
// engine for a `read()` that the store contract has never had, so every
|
|
864
|
-
// prefetch failed with "engine method read is unavailable".
|
|
865
|
-
route = 'session.read';
|
|
866
|
-
payload = { sessionId: id, open: openHints || {}, baseRevision: null };
|
|
867
|
-
}
|
|
868
|
-
// One id for the whole retry sequence. In particular, a submit whose HTTP
|
|
869
|
-
// response was lost must hit the transport cache instead of being accepted
|
|
870
|
-
// twice by the daemon.
|
|
871
|
-
const submissionId = name === 'submit' ? String(args?.[1]?.id || '').trim() : '';
|
|
872
|
-
const callId = submissionId
|
|
873
|
-
? `session-submit:${id}:${submissionId}`
|
|
874
|
-
: randomUUID();
|
|
875
|
-
let lastError = null;
|
|
876
|
-
for (let attempt = 0; attempt < Math.max(1, attempts); attempt += 1) {
|
|
877
|
-
const attachment = await ensureSharedAttachment({ cwd, log });
|
|
878
|
-
try {
|
|
879
|
-
const result = await attachment.client.call(route, payload, { callId });
|
|
880
|
-
if (name === 'submit') return { ...result, value: result?.accepted === true };
|
|
881
|
-
if (name === 'abort') return { ...result, value: result?.aborted === true };
|
|
882
|
-
if (name === 'resolveToolApproval') return { ...result, value: result?.approved === true };
|
|
883
|
-
return result;
|
|
884
|
-
} catch (err) {
|
|
885
|
-
if (!err?.daemonTransportError) throw err;
|
|
886
|
-
lastError = err;
|
|
887
|
-
// The attachment died under us; drop it so the next attempt re-discovers
|
|
888
|
-
// (or respawns) the daemon instead of calling into a closed socket.
|
|
889
|
-
if (shared === attachment) shared = null;
|
|
890
|
-
await delay(200 * (attempt + 1));
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
throw lastError || new Error('engine daemon session call failed');
|
|
894
|
-
}
|