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,60 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { mkdirSync, rmSync, writeFile } from 'node:fs';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
5
|
-
import { fileURLToPath
|
|
6
|
-
import { startChildGuardian } from '../runtime/shared/child-guardian.mjs';
|
|
7
|
-
import { detachedSpawnOpts } from '../runtime/shared/spawn-flags.mjs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
8
7
|
import { appendBuffered } from '../runtime/shared/buffered-appender.mjs';
|
|
8
|
+
import { detachedSpawnOpts } from '../runtime/shared/spawn-flags.mjs';
|
|
9
9
|
import { scrubLoaderVars } from '../runtime/agent/orchestrator/tools/env-scrub.mjs';
|
|
10
10
|
import { rotateBoundedLog, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../lib/mixdog-debug.cjs';
|
|
11
|
-
import {
|
|
12
|
-
import { claimSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
|
|
13
|
-
import { safeIpcSend } from '../runtime/shared/safe-ipc-send.mjs';
|
|
14
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { attachChannel, readChannelDiscovery, probeChannelHealth } from './channel-client.mjs';
|
|
15
12
|
|
|
16
13
|
const CHANNEL_TOOLS = new Set([
|
|
17
14
|
'reply',
|
|
18
15
|
'fetch',
|
|
19
|
-
// activate_channel_bridge/reload_config are NOT model-facing tools (no
|
|
20
|
-
// TOOL_DEFS entry) but stay in the allow-set: mixdog-session-runtime.mjs
|
|
21
|
-
// calls channels.execute() with these names directly as internal
|
|
22
|
-
// Lead-only runtime plumbing (bridge-claim on start, config hot-reload).
|
|
23
16
|
'activate_channel_bridge',
|
|
24
17
|
'reload_config',
|
|
25
|
-
// Lead-pushed transcript repoint (auto-acquire / newSession / resume /
|
|
26
|
-
// clear). Not model-facing; internal Lead-only runtime plumbing.
|
|
27
18
|
'rebind_current_transcript',
|
|
28
19
|
]);
|
|
29
20
|
|
|
30
21
|
const WORKER_PRELOAD = fileURLToPath(new URL('./channel-worker-preload.cjs', import.meta.url));
|
|
31
|
-
|
|
32
|
-
// via env so smokes can point at a stub daemon (no Discord token). Resolved
|
|
33
|
-
// LAZILY at spawn time — an import-time constant would freeze before a smoke
|
|
34
|
-
// that imports this module first gets a chance to set the env override.
|
|
22
|
+
|
|
35
23
|
function daemonEntry() {
|
|
36
|
-
return process.env.
|
|
37
|
-
? resolve(process.env.
|
|
38
|
-
|
|
39
|
-
: fileURLToPath(new URL('./backend-daemon.mjs', import.meta.url));
|
|
24
|
+
return process.env.MIXDOG_DAEMON_ENTRY
|
|
25
|
+
? resolve(process.env.MIXDOG_DAEMON_ENTRY)
|
|
26
|
+
: fileURLToPath(new URL('./daemon.mjs', import.meta.url));
|
|
40
27
|
}
|
|
41
28
|
|
|
42
|
-
// A global package update can briefly remove channel-worker-preload.cjs while
|
|
43
|
-
// the worker is spawning (MODULE_NOT_FOUND boot crash). Retry once before
|
|
44
|
-
// surfacing the failure to callers.
|
|
45
|
-
const WORKER_BOOT_MAX_ATTEMPTS = 2;
|
|
46
|
-
const WORKER_BOOT_RETRY_DELAY_MS = 1000;
|
|
47
|
-
// Fail an in-flight IPC send whose flush callback never fires (wedged channel)
|
|
48
|
-
// so the FIFO send queue can't stall permanently on one stuck message.
|
|
49
|
-
const SEND_FLUSH_TIMEOUT_MS = 10_000;
|
|
50
|
-
|
|
51
29
|
function logLine(path, line) {
|
|
52
30
|
try {
|
|
53
31
|
mkdirSync(dirname(path), { recursive: true });
|
|
54
32
|
appendBuffered(path, `[${new Date().toISOString()}] ${line}\n`);
|
|
55
|
-
} catch {
|
|
56
|
-
// Logging must never break the TUI.
|
|
57
|
-
}
|
|
33
|
+
} catch {}
|
|
58
34
|
}
|
|
59
35
|
|
|
60
36
|
const CHANNEL_WORKER_EXIT_CLEANUPS = new Set();
|
|
@@ -66,75 +42,41 @@ function registerChannelWorkerExitCleanup(cleanup) {
|
|
|
66
42
|
if (!channelWorkerExitHookInstalled) {
|
|
67
43
|
channelWorkerExitHookInstalled = true;
|
|
68
44
|
process.once('exit', () => {
|
|
69
|
-
for (const fn of
|
|
45
|
+
for (const fn of CHANNEL_WORKER_EXIT_CLEANUPS) {
|
|
70
46
|
try { fn(); } catch {}
|
|
71
47
|
}
|
|
72
48
|
CHANNEL_WORKER_EXIT_CLEANUPS.clear();
|
|
73
49
|
});
|
|
74
50
|
}
|
|
75
|
-
return () => {
|
|
76
|
-
CHANNEL_WORKER_EXIT_CLEANUPS.delete(cleanup);
|
|
77
|
-
};
|
|
51
|
+
return () => { CHANNEL_WORKER_EXIT_CLEANUPS.delete(cleanup); };
|
|
78
52
|
}
|
|
79
53
|
|
|
80
54
|
function runtimeRoot() {
|
|
81
|
-
return process.env.MIXDOG_RUNTIME_ROOT
|
|
55
|
+
return process.env.MIXDOG_RUNTIME_ROOT
|
|
56
|
+
? resolve(process.env.MIXDOG_RUNTIME_ROOT)
|
|
57
|
+
: join(tmpdir(), 'mixdog');
|
|
82
58
|
}
|
|
83
59
|
|
|
84
60
|
export function createStandaloneChannelWorker({
|
|
85
|
-
entry,
|
|
86
61
|
rootDir,
|
|
87
62
|
dataDir,
|
|
88
63
|
cwd = process.cwd(),
|
|
89
64
|
onNotify,
|
|
90
65
|
} = {}) {
|
|
91
|
-
if (!entry) throw new Error('channels runtime entry is required');
|
|
92
66
|
if (!rootDir) throw new Error('channels runtime rootDir is required');
|
|
93
67
|
if (!dataDir) throw new Error('channels runtime dataDir is required');
|
|
94
68
|
|
|
95
|
-
let child = null;
|
|
96
|
-
let readyPromise = null;
|
|
97
|
-
let readyResolve = null;
|
|
98
|
-
let readyReject = null;
|
|
99
|
-
let stopPromise = null;
|
|
100
|
-
let stopRequested = false;
|
|
101
|
-
let bootGeneration = 0;
|
|
102
|
-
let inProcessMod = null;
|
|
103
|
-
let inProcessStartPromise = null;
|
|
104
|
-
// Set when in-process mode (MIXDOG_CHANNEL_WORKER_PROCESS=0) holds the daemon
|
|
105
|
-
// singleton lock, so stop() releases it (never double-own beside a daemon).
|
|
106
|
-
let inProcessOwned = false;
|
|
107
|
-
// Machine-global channels daemon attach handle. Replaces the per-TUI fork +
|
|
108
|
-
// node-IPC call/notify plumbing; the daemon is SHARED and never killed on
|
|
109
|
-
// this TUI's exit (only detached-from). The fork machinery below is retained
|
|
110
|
-
// as an unreachable legacy path while useProcessWorker is true.
|
|
111
69
|
let daemonClient = null;
|
|
70
|
+
let daemonPid = null;
|
|
112
71
|
let attachPromise = null;
|
|
113
72
|
let attachGeneration = 0;
|
|
114
|
-
let
|
|
73
|
+
let stopPromise = null;
|
|
74
|
+
let stopRequested = false;
|
|
115
75
|
let nextCallId = 1;
|
|
116
|
-
// Per-proxy unique prefix so a callId can never collide across TUIs sharing
|
|
117
|
-
// the same supervisor pid (which would cross-dedup two distinct calls).
|
|
118
76
|
const proxyId = randomUUID();
|
|
119
|
-
|
|
120
|
-
const pending = new Map();
|
|
121
|
-
const ownedChildPids = new Set();
|
|
122
|
-
// Strict-FIFO backpressure queue for child IPC. child.send returns false
|
|
123
|
-
// when the channel is over its backpressure threshold; we keep at most one
|
|
124
|
-
// send in flight and only dispatch the next once the prior send's flush
|
|
125
|
-
// callback fires, so bursts don't unboundedly buffer and order is preserved.
|
|
126
|
-
const sendQueue = [];
|
|
127
|
-
let sendInFlight = false;
|
|
128
|
-
// Guards the single in-flight send: the token invalidates a stale settler
|
|
129
|
-
// (flush callback / watchdog / rejectPending) so only the first one to fire
|
|
130
|
-
// releases the slot; the watchdog handle lets rejectPending cancel it.
|
|
131
|
-
let inFlightToken = 0;
|
|
132
|
-
let inFlightWatchdog = null;
|
|
133
|
-
const logPath = join(dataDir, 'channels-worker-standalone.log');
|
|
134
|
-
// One-shot bound at own-process boot: this runtime may never pass through
|
|
135
|
-
// the channels-worker rotation path, so cap the log writer-side.
|
|
77
|
+
const logPath = join(dataDir, 'daemon.log');
|
|
136
78
|
rotateBoundedLog(logPath, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES);
|
|
137
|
-
|
|
79
|
+
|
|
138
80
|
const clientDir = join(runtimeRoot(), 'channel-clients');
|
|
139
81
|
const clientPath = join(clientDir, `${process.pid}.json`);
|
|
140
82
|
let clientHeartbeatTimer = null;
|
|
@@ -142,9 +84,6 @@ export function createStandaloneChannelWorker({
|
|
|
142
84
|
let clientDirReady = false;
|
|
143
85
|
|
|
144
86
|
function writeClientHeartbeat() {
|
|
145
|
-
// Must write every tick: liveness readers key off the file's rolling
|
|
146
|
-
// mtime/updatedAt, so an unchanged-skip would make the client look dead.
|
|
147
|
-
// mkdir once and write async to keep the 5s interval off the sync-fs path.
|
|
148
87
|
try {
|
|
149
88
|
if (!clientDirReady) {
|
|
150
89
|
mkdirSync(clientDir, { recursive: true });
|
|
@@ -158,14 +97,6 @@ export function createStandaloneChannelWorker({
|
|
|
158
97
|
} catch {}
|
|
159
98
|
}
|
|
160
99
|
|
|
161
|
-
function startClientHeartbeat() {
|
|
162
|
-
if (clientHeartbeatTimer) return;
|
|
163
|
-
writeClientHeartbeat();
|
|
164
|
-
clientHeartbeatTimer = setInterval(writeClientHeartbeat, 5000);
|
|
165
|
-
clientHeartbeatTimer.unref?.();
|
|
166
|
-
clientHeartbeatExitCleanup ||= registerChannelWorkerExitCleanup(stopClientHeartbeat);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
100
|
function stopClientHeartbeat() {
|
|
170
101
|
if (clientHeartbeatExitCleanup) {
|
|
171
102
|
const unregister = clientHeartbeatExitCleanup;
|
|
@@ -179,17 +110,17 @@ export function createStandaloneChannelWorker({
|
|
|
179
110
|
try { rmSync(clientPath, { force: true }); } catch {}
|
|
180
111
|
}
|
|
181
112
|
|
|
113
|
+
function startClientHeartbeat() {
|
|
114
|
+
if (clientHeartbeatTimer) return;
|
|
115
|
+
writeClientHeartbeat();
|
|
116
|
+
clientHeartbeatTimer = setInterval(writeClientHeartbeat, 5_000);
|
|
117
|
+
clientHeartbeatTimer.unref?.();
|
|
118
|
+
clientHeartbeatExitCleanup ||= registerChannelWorkerExitCleanup(stopClientHeartbeat);
|
|
119
|
+
}
|
|
120
|
+
|
|
182
121
|
startClientHeartbeat();
|
|
183
122
|
|
|
184
123
|
function status() {
|
|
185
|
-
if (!useProcessWorker) {
|
|
186
|
-
return {
|
|
187
|
-
running: Boolean(inProcessMod),
|
|
188
|
-
pid: inProcessMod ? process.pid : null,
|
|
189
|
-
pending: 0,
|
|
190
|
-
mode: 'in-process',
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
124
|
return {
|
|
194
125
|
running: Boolean(daemonClient),
|
|
195
126
|
pid: daemonPid,
|
|
@@ -198,219 +129,21 @@ export function createStandaloneChannelWorker({
|
|
|
198
129
|
};
|
|
199
130
|
}
|
|
200
131
|
|
|
201
|
-
function rejectPending(error) {
|
|
202
|
-
for (const [, item] of pending) {
|
|
203
|
-
clearTimeout(item.timer);
|
|
204
|
-
item.reject(error);
|
|
205
|
-
}
|
|
206
|
-
pending.clear();
|
|
207
|
-
// Fail any sends still queued/in-flight for the now-dead channel so their
|
|
208
|
-
// callers unblock instead of waiting on a flush callback that never fires.
|
|
209
|
-
// Invalidate + disarm the in-flight watchdog so it can't later flip
|
|
210
|
-
// sendInFlight under a freshly-started send (its owning call was already
|
|
211
|
-
// rejected via the pending map above).
|
|
212
|
-
inFlightToken++;
|
|
213
|
-
if (inFlightWatchdog) { clearTimeout(inFlightWatchdog); inFlightWatchdog = null; }
|
|
214
|
-
sendInFlight = false;
|
|
215
|
-
while (sendQueue.length) {
|
|
216
|
-
const item = sendQueue.shift();
|
|
217
|
-
try { item.cb?.(error); } catch {}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Dispatch queued IPC sends one at a time, gating the next on the current
|
|
222
|
-
// send's flush callback so child.send backpressure (false return) is honored.
|
|
223
|
-
function drainSendQueue() {
|
|
224
|
-
if (sendInFlight) return;
|
|
225
|
-
const item = sendQueue.shift();
|
|
226
|
-
if (!item) return;
|
|
227
|
-
if (!child || !child.send) {
|
|
228
|
-
try { item.cb?.(new Error('channels worker is not running')); } catch {}
|
|
229
|
-
drainSendQueue();
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
sendInFlight = true;
|
|
233
|
-
const token = ++inFlightToken;
|
|
234
|
-
// Settle exactly once, whichever of {flush callback, watchdog} fires first;
|
|
235
|
-
// a stale token (superseded by rejectPending or the sibling) is ignored.
|
|
236
|
-
const finish = (error) => {
|
|
237
|
-
if (token !== inFlightToken) return;
|
|
238
|
-
inFlightToken++;
|
|
239
|
-
if (inFlightWatchdog) { clearTimeout(inFlightWatchdog); inFlightWatchdog = null; }
|
|
240
|
-
sendInFlight = false;
|
|
241
|
-
try { item.cb?.(error); } catch {}
|
|
242
|
-
drainSendQueue();
|
|
243
|
-
};
|
|
244
|
-
// Watchdog: if the flush callback never fires (wedged IPC channel), fail
|
|
245
|
-
// this request via its normal rejection path and keep draining so a single
|
|
246
|
-
// stuck send can't wedge the queue forever.
|
|
247
|
-
inFlightWatchdog = setTimeout(() => finish(new Error('channels worker send timed out')), SEND_FLUSH_TIMEOUT_MS);
|
|
248
|
-
inFlightWatchdog.unref?.();
|
|
249
|
-
safeIpcSend(child, item.msg, {
|
|
250
|
-
onComplete: (error) => finish(error),
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function sendToChild(msg, cb) {
|
|
255
|
-
sendQueue.push({ msg, cb });
|
|
256
|
-
drainSendQueue();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function start() {
|
|
260
|
-
if (stopPromise) return stopPromise.then(() => start());
|
|
261
|
-
if (!useProcessWorker) return startInProcess();
|
|
262
|
-
// Daemon mode (default): spawn-or-attach to the machine-global channels
|
|
263
|
-
// daemon. The fork code below is unreachable while useProcessWorker is true.
|
|
264
|
-
stopRequested = false;
|
|
265
|
-
return ensureDaemonAttached().then(() => status());
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function spawnWorkerChild(attempt) {
|
|
269
|
-
const workerEnv = { ...process.env };
|
|
270
|
-
scrubLoaderVars(workerEnv);
|
|
271
|
-
child = fork(entry, [], {
|
|
272
|
-
cwd,
|
|
273
|
-
execArgv: ['--require', WORKER_PRELOAD],
|
|
274
|
-
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
275
|
-
detached: false,
|
|
276
|
-
env: {
|
|
277
|
-
...workerEnv,
|
|
278
|
-
MIXDOG_ROOT: rootDir,
|
|
279
|
-
MIXDOG_DATA_DIR: dataDir,
|
|
280
|
-
MIXDOG_STANDALONE: '1',
|
|
281
|
-
MIXDOG_WORKER_MODE: '1',
|
|
282
|
-
MIXDOG_CLI_OWNED: '0',
|
|
283
|
-
// Preserve the real terminal-lead PID (host TUI, or an outer run-mcp
|
|
284
|
-
// supervisor if one injected MIXDOG_SUPERVISOR_PID) through the fork.
|
|
285
|
-
// Without this the worker resolves getTerminalLeadPid() to its OWN pid,
|
|
286
|
-
// so the seat's ownerLeadPid tracks the headless worker instead of the
|
|
287
|
-
// terminal that owns it — and the seat can never be evicted when the
|
|
288
|
-
// owning terminal/TUI dies while the worker stays alive.
|
|
289
|
-
MIXDOG_SUPERVISOR_PID: process.env.MIXDOG_SUPERVISOR_PID || String(process.pid),
|
|
290
|
-
MIXDOG_QUIET_SESSION_LOG: process.env.MIXDOG_QUIET_SESSION_LOG ?? '1',
|
|
291
|
-
},
|
|
292
|
-
windowsHide: true,
|
|
293
|
-
});
|
|
294
|
-
const spawnedPid = child.pid;
|
|
295
|
-
// Per-boot generation: a later start()/respawn bumps this, so a stale old
|
|
296
|
-
// child's late exit is ignored instead of respawning/rejecting the current one.
|
|
297
|
-
const myGeneration = ++bootGeneration;
|
|
298
|
-
startChildGuardian({ childPid: spawnedPid, label: 'channel-worker', orphanGraceMs: 8000, forceGraceMs: 3000 });
|
|
299
|
-
if (spawnedPid) ownedChildPids.add(spawnedPid);
|
|
300
|
-
installParentExitHook();
|
|
301
|
-
let becameReady = false;
|
|
302
|
-
|
|
303
|
-
child.stderr?.on('data', (chunk) => {
|
|
304
|
-
const text = String(chunk || '').trimEnd();
|
|
305
|
-
if (text) logLine(logPath, text);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
child.on('message', (msg) => {
|
|
309
|
-
if (!msg || typeof msg !== 'object') return;
|
|
310
|
-
if (msg.type === 'ready') {
|
|
311
|
-
becameReady = true;
|
|
312
|
-
readyResolve?.(status());
|
|
313
|
-
readyResolve = null;
|
|
314
|
-
readyReject = null;
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
if (msg.type === 'result' && msg.callId) {
|
|
318
|
-
const item = pending.get(msg.callId);
|
|
319
|
-
if (!item) return;
|
|
320
|
-
pending.delete(msg.callId);
|
|
321
|
-
clearTimeout(item.timer);
|
|
322
|
-
if (msg.error) item.reject(new Error(msg.error));
|
|
323
|
-
else item.resolve(msg.result);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (msg.type === 'notify') {
|
|
327
|
-
try { onNotify?.(msg); } catch {}
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
child.on('exit', (code, signal) => {
|
|
332
|
-
if (myGeneration !== bootGeneration) return;
|
|
333
|
-
if (spawnedPid) ownedChildPids.delete(spawnedPid);
|
|
334
|
-
const error = new Error(`channels runtime exited (${signal || (code ?? 'unknown')})`);
|
|
335
|
-
// Exit code 2 = terminal (non-transient) worker start failure: reject, never respawn.
|
|
336
|
-
// A requested stop (stopRequested) also suppresses respawn even if stopPromise's
|
|
337
|
-
// settle timer already cleared it before the child exited.
|
|
338
|
-
if (!becameReady && readyReject && !stopPromise && !stopRequested && code !== 2 && attempt < WORKER_BOOT_MAX_ATTEMPTS) {
|
|
339
|
-
logLine(logPath, `worker exited before ready (${signal || (code ?? 'unknown')}), attempt ${attempt}/${WORKER_BOOT_MAX_ATTEMPTS}; retrying in ${WORKER_BOOT_RETRY_DELAY_MS}ms`);
|
|
340
|
-
child = null;
|
|
341
|
-
setTimeout(() => {
|
|
342
|
-
if (stopPromise || stopRequested || !readyReject) return;
|
|
343
|
-
spawnWorkerChild(attempt + 1);
|
|
344
|
-
}, WORKER_BOOT_RETRY_DELAY_MS);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (readyReject) readyReject(error);
|
|
348
|
-
readyResolve = null;
|
|
349
|
-
readyReject = null;
|
|
350
|
-
rejectPending(error);
|
|
351
|
-
child = null;
|
|
352
|
-
readyPromise = null;
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
child.on('error', (error) => {
|
|
356
|
-
if (myGeneration !== bootGeneration) return;
|
|
357
|
-
logLine(logPath, `runtime error: ${error?.message || error}`);
|
|
358
|
-
if (readyReject) readyReject(error);
|
|
359
|
-
readyResolve = null;
|
|
360
|
-
readyReject = null;
|
|
361
|
-
rejectPending(error);
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
async function startInProcess() {
|
|
366
|
-
if (inProcessMod || inProcessStartPromise) return _startInProcess();
|
|
367
|
-
// Singleton guard: in-process mode must not run a SECOND live bridge owner
|
|
368
|
-
// beside the machine-global daemon (double Discord gateway). Claim the same
|
|
369
|
-
// owner lock the daemon uses; if a LIVE daemon holds it, fail loudly rather
|
|
370
|
-
// than silently double-own (unset MIXDOG_CHANNEL_WORKER_PROCESS=0 to attach).
|
|
371
|
-
const claim = claimSingletonOwner(daemonOwnerPath, { kind: 'channel-runtime-daemon', pid: process.pid, meta: { cwd, mode: 'in-process' } });
|
|
372
|
-
if (!claim.owned) {
|
|
373
|
-
throw new Error(`in-process channels runtime refused: a live channels daemon (pid=${claim.owner?.pid}) already owns the bridge — refusing to double-own. Unset MIXDOG_CHANNEL_WORKER_PROCESS=0 to attach, or stop the daemon.`);
|
|
374
|
-
}
|
|
375
|
-
inProcessOwned = true;
|
|
376
|
-
try {
|
|
377
|
-
return await _startInProcess();
|
|
378
|
-
} catch (err) {
|
|
379
|
-
// Boot failed: release the just-claimed lock so a retry (or the daemon)
|
|
380
|
-
// can own it instead of leaking a live lock held by a non-running runtime.
|
|
381
|
-
if (inProcessOwned && !inProcessMod) {
|
|
382
|
-
try { releaseSingletonOwner(daemonOwnerPath, process.pid); } catch {}
|
|
383
|
-
inProcessOwned = false;
|
|
384
|
-
}
|
|
385
|
-
throw err;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ── Machine-global daemon: spawn-or-attach ────────────────────────────────
|
|
390
|
-
// Second+ TUI attaches to the live daemon instead of forking. Discovery is a
|
|
391
|
-
// pid-verified file (channel-daemon.json, 127.0.0.1 only). A stale daemon
|
|
392
|
-
// (dead pid) or unhealthy one is respawned; a spawn race loser exits(0) so we
|
|
393
|
-
// re-read discovery and attach to the winner (pid-verified singleton lock).
|
|
394
132
|
const daemonLeadPid = Number(process.env.MIXDOG_SUPERVISOR_PID) || process.pid;
|
|
395
|
-
const discoveryPath = join(runtimeRoot(), '
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const daemonOwnerPath = join(dataDir, 'channel-daemon-owner.json');
|
|
400
|
-
// Drop the cached attach so the next ensureDaemonAttached() re-reads discovery,
|
|
401
|
-
// health-probes, and respawns a dead daemon — the recovery hinge for a daemon
|
|
402
|
-
// restart mid-session (no TUI process restart needed).
|
|
133
|
+
const discoveryPath = join(runtimeRoot(), 'daemon.json');
|
|
134
|
+
const discoverChannel = () => readChannelDiscovery(discoveryPath);
|
|
135
|
+
const daemonDelay = (ms) => new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
136
|
+
|
|
403
137
|
function invalidateDaemonClient(reason = 'invalidate', expected = null) {
|
|
404
|
-
// CAS: only tear down when the live handle is still the one that failed, so
|
|
405
|
-
// a concurrent call that already re-attached a fresh daemon isn't clobbered.
|
|
406
138
|
if (expected && daemonClient !== expected) return;
|
|
407
|
-
attachGeneration
|
|
139
|
+
attachGeneration += 1;
|
|
408
140
|
const client = daemonClient;
|
|
409
141
|
daemonClient = null;
|
|
410
142
|
attachPromise = null;
|
|
411
143
|
daemonPid = null;
|
|
412
144
|
if (client) { try { client.close(reason); } catch {} }
|
|
413
145
|
}
|
|
146
|
+
|
|
414
147
|
function daemonEnv() {
|
|
415
148
|
const env = { ...process.env };
|
|
416
149
|
scrubLoaderVars(env);
|
|
@@ -420,19 +153,21 @@ export function createStandaloneChannelWorker({
|
|
|
420
153
|
MIXDOG_DATA_DIR: dataDir,
|
|
421
154
|
MIXDOG_STANDALONE: '1',
|
|
422
155
|
MIXDOG_WORKER_MODE: '1',
|
|
423
|
-
|
|
156
|
+
MIXDOG_DAEMON_HOST: '1',
|
|
424
157
|
MIXDOG_CLI_OWNED: '0',
|
|
425
158
|
MIXDOG_SUPERVISOR_PID: process.env.MIXDOG_SUPERVISOR_PID || String(process.pid),
|
|
426
159
|
MIXDOG_QUIET_SESSION_LOG: process.env.MIXDOG_QUIET_SESSION_LOG ?? '1',
|
|
427
160
|
};
|
|
428
161
|
}
|
|
429
|
-
|
|
430
|
-
// and NEVER track it for parent-exit kill. Resolves when the candidate reports
|
|
431
|
-
// ready OR exits (race loss/crash); the caller then re-checks discovery.
|
|
162
|
+
|
|
432
163
|
function spawnDaemonCandidate() {
|
|
433
164
|
return new Promise((resolveSpawn) => {
|
|
434
165
|
let settled = false;
|
|
435
|
-
const done = () => {
|
|
166
|
+
const done = () => {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
resolveSpawn();
|
|
170
|
+
};
|
|
436
171
|
let daemon;
|
|
437
172
|
try {
|
|
438
173
|
daemon = fork(daemonEntry(), [], {
|
|
@@ -442,56 +177,48 @@ export function createStandaloneChannelWorker({
|
|
|
442
177
|
env: daemonEnv(),
|
|
443
178
|
...detachedSpawnOpts,
|
|
444
179
|
});
|
|
445
|
-
} catch (
|
|
446
|
-
logLine(logPath, `daemon spawn failed: ${
|
|
180
|
+
} catch (error) {
|
|
181
|
+
logLine(logPath, `daemon spawn failed: ${error?.message || error}`);
|
|
447
182
|
done();
|
|
448
183
|
return;
|
|
449
184
|
}
|
|
450
|
-
|
|
451
|
-
// ready the daemon appends its own lines directly (backend-daemon.mjs), and
|
|
452
|
-
// this TUI may exit at any time — a live mirror would both duplicate those
|
|
453
|
-
// lines and die with the parent. Detach the listener at the ready boundary.
|
|
454
|
-
const mirrorDaemonStderr = (chunk) => {
|
|
185
|
+
const mirrorStderr = (chunk) => {
|
|
455
186
|
const text = String(chunk || '').trimEnd();
|
|
456
187
|
if (text) logLine(logPath, text);
|
|
457
188
|
};
|
|
458
|
-
daemon.stderr?.on('data',
|
|
459
|
-
daemon.once('message', (
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
try { daemon.stderr?.unref?.(); } catch {}
|
|
467
|
-
done();
|
|
468
|
-
}
|
|
189
|
+
daemon.stderr?.on('data', mirrorStderr);
|
|
190
|
+
daemon.once('message', (message) => {
|
|
191
|
+
if (message?.type !== 'ready') return;
|
|
192
|
+
try { daemon.stderr?.off?.('data', mirrorStderr); } catch {}
|
|
193
|
+
try { daemon.disconnect?.(); } catch {}
|
|
194
|
+
try { daemon.unref?.(); } catch {}
|
|
195
|
+
try { daemon.stderr?.unref?.(); } catch {}
|
|
196
|
+
done();
|
|
469
197
|
});
|
|
470
|
-
daemon.once('exit', done);
|
|
471
|
-
daemon.once('error', (
|
|
472
|
-
|
|
473
|
-
|
|
198
|
+
daemon.once('exit', done);
|
|
199
|
+
daemon.once('error', (error) => {
|
|
200
|
+
logLine(logPath, `daemon spawn error: ${error?.message || error}`);
|
|
201
|
+
done();
|
|
202
|
+
});
|
|
203
|
+
const timer = setTimeout(done, 20_000);
|
|
204
|
+
timer.unref?.();
|
|
474
205
|
});
|
|
475
206
|
}
|
|
207
|
+
|
|
476
208
|
function attachCancelledError() {
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
return
|
|
480
|
-
}
|
|
481
|
-
function discoveryMatchesHealth(discovery, health) {
|
|
482
|
-
return Number(health?.pid) === Number(discovery?.pid);
|
|
209
|
+
const error = new Error('channel service attach superseded');
|
|
210
|
+
error.daemonAttachCancelled = true;
|
|
211
|
+
return error;
|
|
483
212
|
}
|
|
213
|
+
|
|
484
214
|
async function doAttach(discovery, generation) {
|
|
485
215
|
let client = null;
|
|
486
216
|
let fatalDuringAttach = false;
|
|
487
|
-
client = await
|
|
217
|
+
client = await attachChannel({
|
|
488
218
|
discovery,
|
|
489
219
|
leadPid: daemonLeadPid,
|
|
490
220
|
cwd,
|
|
491
|
-
onNotify: (
|
|
492
|
-
// A stale/dead SSE endpoint drops this attach immediately. Do not revive
|
|
493
|
-
// it after this worker has explicitly stopped; otherwise proactively
|
|
494
|
-
// re-read discovery so notifies resume without waiting for the next call.
|
|
221
|
+
onNotify: (message) => { try { onNotify?.(message); } catch {} },
|
|
495
222
|
onFatal: () => {
|
|
496
223
|
fatalDuringAttach = true;
|
|
497
224
|
invalidateDaemonClient('sse fatal', client);
|
|
@@ -501,17 +228,17 @@ export function createStandaloneChannelWorker({
|
|
|
501
228
|
});
|
|
502
229
|
if (stopRequested || generation !== attachGeneration || fatalDuringAttach) {
|
|
503
230
|
await client.close('attach superseded');
|
|
231
|
+
const error = attachCancelledError();
|
|
504
232
|
if (fatalDuringAttach && !stopRequested && generation === attachGeneration) {
|
|
505
|
-
|
|
506
|
-
err.daemonDiscoveryStale = true;
|
|
507
|
-
throw err;
|
|
233
|
+
error.daemonDiscoveryStale = true;
|
|
508
234
|
}
|
|
509
|
-
throw
|
|
235
|
+
throw error;
|
|
510
236
|
}
|
|
511
237
|
daemonClient = client;
|
|
512
238
|
daemonPid = discovery.pid;
|
|
513
239
|
return client;
|
|
514
240
|
}
|
|
241
|
+
|
|
515
242
|
async function ensureDaemonAttached() {
|
|
516
243
|
if (stopRequested) throw attachCancelledError();
|
|
517
244
|
if (daemonClient) return daemonClient;
|
|
@@ -519,345 +246,95 @@ export function createStandaloneChannelWorker({
|
|
|
519
246
|
const generation = attachGeneration;
|
|
520
247
|
const promise = (async () => {
|
|
521
248
|
const deadline = Date.now() + 30_000;
|
|
522
|
-
const MAX_AUTH_REJECTIONS = 5;
|
|
523
249
|
let authRejections = 0;
|
|
524
|
-
|
|
525
|
-
if (!err?.daemonAuthRejected) {
|
|
526
|
-
await daemonDelay(200);
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
authRejections++;
|
|
530
|
-
if (authRejections >= MAX_AUTH_REJECTIONS || Date.now() >= deadline) {
|
|
531
|
-
throw new Error(`channels daemon register rejected discovery auth ${authRejections} times`);
|
|
532
|
-
}
|
|
533
|
-
await daemonDelay(Math.min(200 * (2 ** (authRejections - 1)), 2000));
|
|
534
|
-
};
|
|
535
|
-
for (let attempt = 0; ; attempt++) {
|
|
250
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
536
251
|
if (stopRequested || generation !== attachGeneration) throw attachCancelledError();
|
|
537
|
-
let discovery =
|
|
252
|
+
let discovery = discoverChannel();
|
|
538
253
|
if (discovery) {
|
|
539
|
-
const health = await
|
|
254
|
+
const health = await probeChannelHealth({
|
|
255
|
+
port: discovery.port,
|
|
256
|
+
token: discovery.token,
|
|
257
|
+
timeoutMs: attempt === 0 ? 800 : 2_000,
|
|
258
|
+
});
|
|
540
259
|
if (stopRequested || generation !== attachGeneration) throw attachCancelledError();
|
|
541
|
-
if (
|
|
260
|
+
if (Number(health?.pid) === Number(discovery.pid)) {
|
|
542
261
|
try {
|
|
543
262
|
return await doAttach(discovery, generation);
|
|
544
|
-
} catch (
|
|
545
|
-
if (!
|
|
546
|
-
|
|
547
|
-
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (!error?.daemonDiscoveryStale) throw error;
|
|
265
|
+
if (error?.daemonAuthRejected) authRejections += 1;
|
|
266
|
+
if (authRejections >= 5 || Date.now() >= deadline) {
|
|
267
|
+
throw new Error('channel service repeatedly rejected discovery authentication');
|
|
268
|
+
}
|
|
269
|
+
await daemonDelay(Math.min(200 * (2 ** authRejections), 2_000));
|
|
548
270
|
continue;
|
|
549
271
|
}
|
|
550
272
|
}
|
|
551
|
-
discovery = null; // published but unhealthy → respawn
|
|
552
273
|
}
|
|
553
|
-
// No live daemon (absent / dead pid / unhealthy): spawn a candidate. A
|
|
554
|
-
// race loser exits(0); the winner publishes discovery we then attach to.
|
|
555
274
|
await spawnDaemonCandidate();
|
|
556
|
-
|
|
557
|
-
if (
|
|
558
|
-
const health = await
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
continue;
|
|
275
|
+
discovery = discoverChannel();
|
|
276
|
+
if (discovery) {
|
|
277
|
+
const health = await probeChannelHealth({
|
|
278
|
+
port: discovery.port,
|
|
279
|
+
token: discovery.token,
|
|
280
|
+
timeoutMs: 3_000,
|
|
281
|
+
});
|
|
282
|
+
if (Number(health?.pid) === Number(discovery.pid)) {
|
|
283
|
+
try { return await doAttach(discovery, generation); }
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (!error?.daemonDiscoveryStale) throw error;
|
|
568
286
|
}
|
|
569
287
|
}
|
|
570
288
|
}
|
|
571
|
-
if (Date.now()
|
|
289
|
+
if (Date.now() >= deadline) throw new Error('channel service did not become ready');
|
|
572
290
|
await daemonDelay(200);
|
|
573
291
|
}
|
|
574
292
|
})();
|
|
575
293
|
attachPromise = promise;
|
|
576
|
-
try {
|
|
577
|
-
|
|
578
|
-
} finally {
|
|
579
|
-
if (attachPromise === promise) attachPromise = null;
|
|
580
|
-
}
|
|
294
|
+
try { return await promise; }
|
|
295
|
+
finally { if (attachPromise === promise) attachPromise = null; }
|
|
581
296
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
process.env.MIXDOG_STANDALONE ??= '1';
|
|
589
|
-
const mod = await import(pathToFileURL(entry).href);
|
|
590
|
-
if (typeof mod?.start !== 'function') throw new Error('channels runtime does not export start()');
|
|
591
|
-
await mod.start();
|
|
592
|
-
inProcessMod = mod;
|
|
593
|
-
return status();
|
|
594
|
-
})().finally(() => {
|
|
595
|
-
inProcessStartPromise = null;
|
|
596
|
-
});
|
|
597
|
-
return inProcessStartPromise;
|
|
297
|
+
|
|
298
|
+
function start() {
|
|
299
|
+
if (stopPromise) return stopPromise.then(() => start());
|
|
300
|
+
stopRequested = false;
|
|
301
|
+
startClientHeartbeat();
|
|
302
|
+
return ensureDaemonAttached().then(() => status());
|
|
598
303
|
}
|
|
599
304
|
|
|
600
305
|
async function execute(name, args = {}, { timeoutMs = 120_000 } = {}) {
|
|
601
306
|
if (!CHANNEL_TOOLS.has(name)) throw new Error(`unknown channel tool: ${name}`);
|
|
602
307
|
await start();
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
let timer = null;
|
|
607
|
-
try {
|
|
608
|
-
return await Promise.race([
|
|
609
|
-
call(name, args || {}),
|
|
610
|
-
new Promise((_, reject) => {
|
|
611
|
-
timer = setTimeout(() => reject(new Error(`channels tool timed out: ${name}`)), timeoutMs);
|
|
612
|
-
}),
|
|
613
|
-
]);
|
|
614
|
-
} finally {
|
|
615
|
-
if (timer) clearTimeout(timer);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
// Daemon path: dispatch over HTTP with bounded re-attach so a daemon
|
|
619
|
-
// death/restart is transparent — a transport failure drops the stale attach
|
|
620
|
-
// and ensureDaemonAttached re-reads discovery + respawns-if-dead.
|
|
621
|
-
let lastDaemonErr = null;
|
|
622
|
-
// One stable callId for this logical call, reused across retries so the
|
|
623
|
-
// daemon dedups a retried transport failure to a single side-effect.
|
|
624
|
-
const logicalCallId = `ch_${proxyId}_${nextCallId++}`;
|
|
625
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
308
|
+
let lastError = null;
|
|
309
|
+
const callId = `ch_${proxyId}_${nextCallId++}`;
|
|
310
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
626
311
|
const daemon = await ensureDaemonAttached();
|
|
627
312
|
try {
|
|
628
|
-
return await daemon.call(name, args || {}, { timeoutMs, callId
|
|
629
|
-
} catch (
|
|
630
|
-
if (!
|
|
631
|
-
|
|
313
|
+
return await daemon.call(name, args || {}, { timeoutMs, callId });
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (!error?.daemonTransportError) throw error;
|
|
316
|
+
lastError = error;
|
|
632
317
|
invalidateDaemonClient('transport failure', daemon);
|
|
633
318
|
await daemonDelay(200 * (attempt + 1));
|
|
634
319
|
}
|
|
635
320
|
}
|
|
636
|
-
throw
|
|
637
|
-
if (!child || !child.send) throw new Error('channels worker is not running');
|
|
638
|
-
const callId = `ch_${nextCallId++}`;
|
|
639
|
-
return await new Promise((resolve, reject) => {
|
|
640
|
-
const timer = setTimeout(() => {
|
|
641
|
-
pending.delete(callId);
|
|
642
|
-
reject(new Error(`channels tool timed out: ${name}`));
|
|
643
|
-
}, timeoutMs);
|
|
644
|
-
pending.set(callId, { resolve, reject, timer });
|
|
645
|
-
sendToChild({ type: 'call', callId, name, args: args || {} }, (error) => {
|
|
646
|
-
if (!error) return;
|
|
647
|
-
pending.delete(callId);
|
|
648
|
-
clearTimeout(timer);
|
|
649
|
-
reject(error);
|
|
650
|
-
});
|
|
651
|
-
});
|
|
321
|
+
throw lastError || new Error('channel service call failed');
|
|
652
322
|
}
|
|
653
323
|
|
|
654
|
-
function
|
|
655
|
-
if (!pid) return;
|
|
656
|
-
if (process.platform === 'win32') {
|
|
657
|
-
execFile('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { windowsHide: true }, () => {});
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function forceKillTreeSync(pid) {
|
|
664
|
-
if (!pid) return;
|
|
665
|
-
if (process.platform === 'win32') {
|
|
666
|
-
try {
|
|
667
|
-
spawnSync('taskkill.exe', ['/PID', String(pid), '/T', '/F'], {
|
|
668
|
-
stdio: 'ignore',
|
|
669
|
-
windowsHide: true,
|
|
670
|
-
});
|
|
671
|
-
} catch {}
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function installParentExitHook() {
|
|
678
|
-
if (parentExitCleanup) return;
|
|
679
|
-
parentExitCleanup = registerChannelWorkerExitCleanup(() => {
|
|
680
|
-
parentExitCleanup = null;
|
|
681
|
-
for (const pid of Array.from(ownedChildPids)) {
|
|
682
|
-
forceKillTreeSync(pid);
|
|
683
|
-
}
|
|
684
|
-
ownedChildPids.clear();
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function uninstallParentExitHook() {
|
|
689
|
-
if (!parentExitCleanup) return;
|
|
690
|
-
// Refcount-aware: never strip the shared parent-exit protection while any
|
|
691
|
-
// owned child PID is still tracked. A newer worker may have been spawned
|
|
692
|
-
// (new PID added, hook install is a no-op because it is already present)
|
|
693
|
-
// before an older worker finishes its async teardown; letting that old
|
|
694
|
-
// teardown uninstall here would leave the live newer PID unprotected.
|
|
695
|
-
if (ownedChildPids.size > 0) return;
|
|
696
|
-
const unregister = parentExitCleanup;
|
|
697
|
-
parentExitCleanup = null;
|
|
698
|
-
unregister();
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function unrefChildHandles(target) {
|
|
702
|
-
try { target?.unref?.(); } catch {}
|
|
703
|
-
try { target?.stderr?.unref?.(); } catch {}
|
|
704
|
-
try { target?.stdout?.unref?.(); } catch {}
|
|
705
|
-
try { target?.stdin?.unref?.(); } catch {}
|
|
706
|
-
try { target?.channel?.unref?.(); } catch {}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function stop(reason = 'standalone shutdown', options = {}) {
|
|
710
|
-
const waitForExit = options?.waitForExit !== false;
|
|
324
|
+
function stop(reason = 'standalone shutdown') {
|
|
711
325
|
stopRequested = true;
|
|
712
326
|
stopClientHeartbeat();
|
|
713
327
|
if (stopPromise) return stopPromise;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
try { await inProcessMod?.stop?.(reason); } catch {}
|
|
725
|
-
inProcessMod = null;
|
|
726
|
-
if (inProcessOwned) { try { releaseSingletonOwner(daemonOwnerPath, process.pid); } catch {} inProcessOwned = false; }
|
|
727
|
-
return true;
|
|
728
|
-
})
|
|
729
|
-
.finally(() => {
|
|
730
|
-
stopPromise = null;
|
|
731
|
-
});
|
|
732
|
-
return stopPromise;
|
|
733
|
-
}
|
|
734
|
-
// Daemon path: detach this TUI's client only. The shared daemon reaps
|
|
735
|
-
// itself via client-grace once the last TUI leaves; never kill it here.
|
|
736
|
-
if (useProcessWorker) {
|
|
737
|
-
const inFlightAttach = attachPromise;
|
|
738
|
-
attachGeneration++;
|
|
739
|
-
const client = daemonClient;
|
|
740
|
-
daemonClient = null;
|
|
741
|
-
attachPromise = null;
|
|
742
|
-
daemonPid = null;
|
|
743
|
-
stopPromise = Promise.all([
|
|
744
|
-
client ? client.close(reason).then(() => true).catch(() => true) : Promise.resolve(false),
|
|
745
|
-
Promise.resolve(inFlightAttach).catch(() => null),
|
|
746
|
-
]).then(([detached]) => detached).finally(() => {
|
|
747
|
-
stopPromise = null;
|
|
748
|
-
});
|
|
749
|
-
return stopPromise;
|
|
750
|
-
}
|
|
751
|
-
if (!child) {
|
|
752
|
-
return Promise.resolve(false);
|
|
753
|
-
}
|
|
754
|
-
const target = child;
|
|
755
|
-
const targetPid = target.pid;
|
|
756
|
-
child = null;
|
|
757
|
-
if (!waitForExit) {
|
|
758
|
-
rejectPending(new Error(`channels runtime shutdown requested (${reason})`));
|
|
759
|
-
// Fast/detached path is still TERMINAL: the caller does not block on the
|
|
760
|
-
// full teardown, but a background escalation ladder guarantees the worker
|
|
761
|
-
// dies so no zombie survives. Exit-hook + PID tracking stay installed
|
|
762
|
-
// until the process ACTUALLY exits (not on IPC ack), so a parent that
|
|
763
|
-
// force-exits mid-grace still force-kills the tree via the exit cleanup.
|
|
764
|
-
let torn = false;
|
|
765
|
-
const teardown = () => {
|
|
766
|
-
if (torn) return;
|
|
767
|
-
torn = true;
|
|
768
|
-
clearTimeout(termTimer);
|
|
769
|
-
clearTimeout(killTimer);
|
|
770
|
-
if (targetPid) ownedChildPids.delete(targetPid);
|
|
771
|
-
unrefChildHandles(target);
|
|
772
|
-
uninstallParentExitHook();
|
|
773
|
-
};
|
|
774
|
-
// Bounded grace after IPC shutdown, then SIGTERM.
|
|
775
|
-
const termTimer = setTimeout(() => {
|
|
776
|
-
try {
|
|
777
|
-
if (target.exitCode == null && !target.killed) target.kill('SIGTERM');
|
|
778
|
-
} catch {}
|
|
779
|
-
}, 1500);
|
|
780
|
-
// Hard fallback: taskkill /T /F the whole tree, then SIGKILL the handle.
|
|
781
|
-
const killTimer = setTimeout(() => {
|
|
782
|
-
try {
|
|
783
|
-
if (target.exitCode == null) forceKillTree(targetPid);
|
|
784
|
-
} catch {}
|
|
785
|
-
try {
|
|
786
|
-
if (target.exitCode == null && !target.killed) target.kill('SIGKILL');
|
|
787
|
-
} catch {}
|
|
788
|
-
teardown();
|
|
789
|
-
}, 3000);
|
|
790
|
-
// Unref so these background escalation timers never keep the event loop
|
|
791
|
-
// (or a pending /exit) alive waiting out the grace/hard-kill window.
|
|
792
|
-
termTimer.unref?.();
|
|
793
|
-
killTimer.unref?.();
|
|
794
|
-
// Actual exit (or spawn error) is the only thing that tears down tracking.
|
|
795
|
-
target.once('exit', teardown);
|
|
796
|
-
target.once('error', teardown);
|
|
797
|
-
stopPromise = new Promise((resolve) => {
|
|
798
|
-
let settled = false;
|
|
799
|
-
const settle = (ok) => {
|
|
800
|
-
if (settled) return;
|
|
801
|
-
settled = true;
|
|
802
|
-
clearTimeout(sendTimer);
|
|
803
|
-
stopPromise = null;
|
|
804
|
-
resolve(ok);
|
|
805
|
-
};
|
|
806
|
-
// Resolve the caller quickly once the IPC shutdown is acked/timed out;
|
|
807
|
-
// the escalation ladder above keeps running in the background.
|
|
808
|
-
const sendTimer = setTimeout(() => settle(true), 250);
|
|
809
|
-
sendTimer.unref?.();
|
|
810
|
-
target.once('exit', () => settle(true));
|
|
811
|
-
safeIpcSend(target, { type: 'shutdown', reason }, {
|
|
812
|
-
onComplete: (error) => {
|
|
813
|
-
try { target.disconnect?.(); } catch {}
|
|
814
|
-
if (!error) {
|
|
815
|
-
settle(true);
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
// IPC unavailable: skip the grace window and escalate now.
|
|
819
|
-
try {
|
|
820
|
-
if (target.exitCode == null && !target.killed) target.kill('SIGTERM');
|
|
821
|
-
} catch {}
|
|
822
|
-
settle(false);
|
|
823
|
-
},
|
|
824
|
-
});
|
|
825
|
-
});
|
|
826
|
-
return stopPromise;
|
|
827
|
-
}
|
|
828
|
-
stopPromise = new Promise((resolve) => {
|
|
829
|
-
let settled = false;
|
|
830
|
-
const finish = (ok) => {
|
|
831
|
-
if (settled) return;
|
|
832
|
-
settled = true;
|
|
833
|
-
clearTimeout(termTimer);
|
|
834
|
-
clearTimeout(killTimer);
|
|
835
|
-
stopPromise = null;
|
|
836
|
-
uninstallParentExitHook();
|
|
837
|
-
resolve(ok);
|
|
838
|
-
};
|
|
839
|
-
const termTimer = setTimeout(() => {
|
|
840
|
-
try {
|
|
841
|
-
if (target.exitCode == null && !target.killed) target.kill('SIGTERM');
|
|
842
|
-
} catch {}
|
|
843
|
-
}, 1500);
|
|
844
|
-
const killTimer = setTimeout(() => {
|
|
845
|
-
try {
|
|
846
|
-
if (target.exitCode == null) forceKillTree(targetPid);
|
|
847
|
-
} catch {}
|
|
848
|
-
try {
|
|
849
|
-
if (target.exitCode == null && !target.killed) target.kill('SIGKILL');
|
|
850
|
-
} catch {}
|
|
851
|
-
finish(false);
|
|
852
|
-
}, 5000);
|
|
853
|
-
target.once('exit', () => finish(true));
|
|
854
|
-
target.once('error', () => finish(false));
|
|
855
|
-
safeIpcSend(target, { type: 'shutdown', reason }, {
|
|
856
|
-
onComplete: () => {
|
|
857
|
-
try { target.disconnect?.(); } catch {}
|
|
858
|
-
},
|
|
859
|
-
});
|
|
860
|
-
});
|
|
328
|
+
const inFlightAttach = attachPromise;
|
|
329
|
+
attachGeneration += 1;
|
|
330
|
+
const client = daemonClient;
|
|
331
|
+
daemonClient = null;
|
|
332
|
+
attachPromise = null;
|
|
333
|
+
daemonPid = null;
|
|
334
|
+
stopPromise = Promise.all([
|
|
335
|
+
client ? client.close(reason).then(() => true).catch(() => true) : Promise.resolve(false),
|
|
336
|
+
Promise.resolve(inFlightAttach).catch(() => null),
|
|
337
|
+
]).then(([detached]) => detached).finally(() => { stopPromise = null; });
|
|
861
338
|
return stopPromise;
|
|
862
339
|
}
|
|
863
340
|
|