mixdog 1.1.2 → 1.2.0
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/README.md +1 -0
- package/package.json +2 -1
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +6 -0
- package/src/defaults/skills/setup/references/actions.md +5 -2
- package/src/defaults/skills/visualize/SKILL.md +157 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/help.mjs +2 -0
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +9 -1
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/shared/atomic-file.mjs +8 -2
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -118
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -2
- package/src/runtime/shared/provider-api-key.mjs +21 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-surface.mjs +7 -0
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/boot/apis.mjs +35 -0
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +20 -0
- package/src/session-runtime/boot/infrastructure.mjs +1 -0
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +11 -6
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/notification-bus.mjs +9 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/resource-extension-sync.mjs +30 -7
- package/src/session-runtime/resource-mcp-api.mjs +20 -7
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +11 -4
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +4 -1
- package/src/standalone/daemon.mjs +139 -5
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +16 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +35 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/dist/index.mjs +332 -36
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +17 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +2 -0
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +15 -1
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +39 -10
|
@@ -20,6 +20,8 @@ process.env.MIXDOG_DAEMON_HOST = '1';
|
|
|
20
20
|
// Size the libuv threadpool before any async fs work spins it up (see
|
|
21
21
|
// uv-threadpool-boot.mjs) — imports below already touch fs.
|
|
22
22
|
await import('../runtime/shared/uv-threadpool-boot.mjs');
|
|
23
|
+
// Before anything can import sharp: Electron on Linux must use its wasm build.
|
|
24
|
+
await import('../runtime/shared/sharp-electron-linux.mjs');
|
|
23
25
|
|
|
24
26
|
// No V8 compile cache here, on purpose. Measured on this runtime: a cache hit
|
|
25
27
|
// saves nothing (spawn→ready 316-347ms with the cache vs 318-323ms without,
|
|
@@ -37,7 +39,15 @@ import { writeJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
|
|
|
37
39
|
import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../runtime/shared/runtime-root.mjs';
|
|
38
40
|
import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
|
|
39
41
|
import { listenerLeakWarningLine } from '../runtime/shared/listener-leak-warning.mjs';
|
|
40
|
-
import { claimSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
|
|
42
|
+
import { claimSingletonOwner, readSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
|
|
43
|
+
import {
|
|
44
|
+
createOwnerHeartbeat,
|
|
45
|
+
daemonOwnerIsStale,
|
|
46
|
+
daemonOwnerTimings,
|
|
47
|
+
discoveryPid,
|
|
48
|
+
proveDaemonOwnerRecord,
|
|
49
|
+
startOwnerLivenessListener,
|
|
50
|
+
} from './daemon-owner.mjs';
|
|
41
51
|
import { remoteIntentPath } from '../runtime/shared/remote-intent.mjs';
|
|
42
52
|
import { setChannelNotifySink } from '../runtime/channels/lib/parent-bridge.mjs';
|
|
43
53
|
import { setOwnerContext } from '../runtime/channels/lib/runtime-paths.mjs';
|
|
@@ -61,6 +71,7 @@ import { createSessionService } from './session-service.mjs';
|
|
|
61
71
|
import { createLocalSessionBridge } from './daemon-local-session-bridge.mjs';
|
|
62
72
|
import { createStoredSessionViews } from './daemon-stored-session-views.mjs';
|
|
63
73
|
import * as quotaResumeStore from '../runtime/shared/quota-resume-store.mjs';
|
|
74
|
+
import { registerWorktreeCacheCleanups } from '../runtime/shared/worktrees/cache-cleanups.mjs';
|
|
64
75
|
import { createInlineSessionRuntimeHost } from './session-runtime-inline-host.mjs';
|
|
65
76
|
import { getStandaloneMemoryRuntime } from '../session-runtime/services/memory-runtime-proxy.mjs';
|
|
66
77
|
import { createBootPhaseProfiler } from './boot-phase-profiler.mjs';
|
|
@@ -126,6 +137,13 @@ let remoteSessionState = { enabled: false, sessionId: null };
|
|
|
126
137
|
let shuttingDown = false;
|
|
127
138
|
let shutdownRecheckTimer = null;
|
|
128
139
|
let replacementRequested = null;
|
|
140
|
+
let ownerHeartbeat = null;
|
|
141
|
+
// Answers /health with this pid on the port the owner record names, from
|
|
142
|
+
// before the claim until exit (daemon-owner.mjs).
|
|
143
|
+
let ownerLiveness = null;
|
|
144
|
+
// The endpoints daemon.json carries once published; a heartbeat re-publishes
|
|
145
|
+
// them when the file goes missing (TEMP cleanup).
|
|
146
|
+
let publishedEndpoints = null;
|
|
129
147
|
const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
|
|
130
148
|
eventLoopDelay.enable();
|
|
131
149
|
let idleGc = null;
|
|
@@ -181,6 +199,7 @@ const SHUTDOWN_BUDGET_MS = 15_000;
|
|
|
181
199
|
async function shutdown(reason, code = 0) {
|
|
182
200
|
if (shuttingDown) return;
|
|
183
201
|
shuttingDown = true;
|
|
202
|
+
ownerHeartbeat?.stop();
|
|
184
203
|
exitRecorder.setReason(`shutdown: ${reason}`, code);
|
|
185
204
|
if (shutdownRecheckTimer) {
|
|
186
205
|
clearTimeout(shutdownRecheckTimer);
|
|
@@ -258,7 +277,9 @@ async function shutdown(reason, code = 0) {
|
|
|
258
277
|
daemonTelemetry?.stop();
|
|
259
278
|
eventLoopDelay.disable();
|
|
260
279
|
try {
|
|
261
|
-
|
|
280
|
+
// Only our own: a successor that reclaimed a stale owner record may
|
|
281
|
+
// already have published its endpoint here.
|
|
282
|
+
if (discoveryPid(DAEMON_DISCOVERY_PATH) === process.pid) rmSync(DAEMON_DISCOVERY_PATH, { force: true });
|
|
262
283
|
} catch {}
|
|
263
284
|
try {
|
|
264
285
|
releaseSingletonOwner(OWNER_PATH, process.pid);
|
|
@@ -421,7 +442,18 @@ function requestDaemonReplacement({ protocol, revision, version, force } = {}) {
|
|
|
421
442
|
/** Pid-verified singleton claim (claimSingletonOwner reclaims a dead-pid owner
|
|
422
443
|
* file and refuses only a LIVE peer). Loser exits so the spawner attaches to
|
|
423
444
|
* the winner instead of running a second daemon. */
|
|
424
|
-
function claimDaemonOwnership(bootPhases) {
|
|
445
|
+
async function claimDaemonOwnership(bootPhases) {
|
|
446
|
+
const timings = daemonOwnerTimings();
|
|
447
|
+
// A live-pid record is reclaimable only when its holder's endpoint does not
|
|
448
|
+
// answer with its pid (daemon-owner.mjs); probed before the sync claim.
|
|
449
|
+
const proof = await proveDaemonOwnerRecord(OWNER_PATH, {
|
|
450
|
+
discoveryPath: DAEMON_DISCOVERY_PATH,
|
|
451
|
+
staleMs: timings.staleMs,
|
|
452
|
+
});
|
|
453
|
+
// Up BEFORE the claim, so even the first record names a port that answers
|
|
454
|
+
// with this pid: a contender never reclaims a booting daemon on a stale-
|
|
455
|
+
// looking stamp (a clock jump) alone.
|
|
456
|
+
ownerLiveness = await startOwnerLivenessListener({ log });
|
|
425
457
|
const claim = claimSingletonOwner(OWNER_PATH, {
|
|
426
458
|
kind: 'mixdog-daemon',
|
|
427
459
|
pid: process.pid,
|
|
@@ -430,14 +462,47 @@ function claimDaemonOwnership(bootPhases) {
|
|
|
430
462
|
protocol: SESSION_PROTOCOL,
|
|
431
463
|
revision: SESSION_REVISION,
|
|
432
464
|
version: runtimeVersion(),
|
|
465
|
+
heartbeatAt: new Date().toISOString(),
|
|
466
|
+
...(ownerLiveness ? { livenessPort: ownerLiveness.port } : {}),
|
|
467
|
+
},
|
|
468
|
+
// A live pid that neither beats nor answers is a reused pid, not a peer.
|
|
469
|
+
isStale: (owner) => {
|
|
470
|
+
const stale = daemonOwnerIsStale(owner, { proof, staleMs: timings.staleMs });
|
|
471
|
+
if (stale) {
|
|
472
|
+
log(
|
|
473
|
+
`reclaiming stale owner record pid=${owner.pid} claimedAt=${owner.claimedAt}` +
|
|
474
|
+
` heartbeatAt=${owner.heartbeatAt ?? '-'} probe=${proof?.verdict ?? '-'}`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return stale;
|
|
433
478
|
},
|
|
434
479
|
});
|
|
435
480
|
if (!claim.owned) {
|
|
436
481
|
log(`live peer holds owner lock (pid=${claim.owner?.pid}) — exiting for attach`);
|
|
482
|
+
void ownerLiveness?.close();
|
|
483
|
+
ownerLiveness = null;
|
|
437
484
|
exitRecorder.setReason(`lost singleton claim to live peer pid=${claim.owner?.pid}`, 0);
|
|
438
485
|
process.exit(0);
|
|
439
486
|
}
|
|
440
487
|
bootPhases.mark('owner-claimed');
|
|
488
|
+
// Beats from here on, through boot: a slow boot is never mistaken for a
|
|
489
|
+
// reused pid. Losing the record to another pid, or the lease running out
|
|
490
|
+
// unrefreshed, means another daemon owns (or may now claim) this data dir —
|
|
491
|
+
// at most one may keep serving.
|
|
492
|
+
ownerHeartbeat = createOwnerHeartbeat({
|
|
493
|
+
ownerPath: OWNER_PATH,
|
|
494
|
+
owner: claim.owner,
|
|
495
|
+
heartbeatMs: timings.heartbeatMs,
|
|
496
|
+
fenceMs: timings.fenceMs,
|
|
497
|
+
tickMs: timings.tickMs,
|
|
498
|
+
stallMs: timings.stallMs,
|
|
499
|
+
log,
|
|
500
|
+
onBeat: republishMissingDiscovery,
|
|
501
|
+
onSuperseded: (record) => fenceDaemon(`owner claim superseded by pid=${record?.pid}`),
|
|
502
|
+
onFenced: ({ lastRefreshAt, sinceMs }) =>
|
|
503
|
+
fenceDaemon(`owner lease expired (no refresh for ${sinceMs}ms, last ${lastRefreshAt})`),
|
|
504
|
+
});
|
|
505
|
+
ownerHeartbeat.start();
|
|
441
506
|
process.on('exit', () => {
|
|
442
507
|
try {
|
|
443
508
|
releaseSingletonOwner(OWNER_PATH, process.pid);
|
|
@@ -498,6 +563,8 @@ function daemonStatusSnapshot() {
|
|
|
498
563
|
/** The file every attacher reads to find this daemon: wire identity plus both
|
|
499
564
|
* loopback endpoints and their privileged tokens. */
|
|
500
565
|
function publishDaemonDiscovery({ channel, session }) {
|
|
566
|
+
// A re-publish keeps the first publication's startedAt.
|
|
567
|
+
publishedEndpoints = { channel, session, startedAt: publishedEndpoints?.startedAt ?? Date.now() };
|
|
501
568
|
writeJsonAtomicSync(
|
|
502
569
|
DAEMON_DISCOVERY_PATH,
|
|
503
570
|
{
|
|
@@ -506,7 +573,7 @@ function publishDaemonDiscovery({ channel, session }) {
|
|
|
506
573
|
version: runtimeVersion(),
|
|
507
574
|
capabilityFingerprint: SESSION_CAPABILITY_FINGERPRINT,
|
|
508
575
|
pid: process.pid,
|
|
509
|
-
startedAt:
|
|
576
|
+
startedAt: publishedEndpoints.startedAt,
|
|
510
577
|
endpoints: {
|
|
511
578
|
channel: { port: channel.port, token: channel.token },
|
|
512
579
|
session: { port: session.port, token: session.token },
|
|
@@ -516,6 +583,42 @@ function publishDaemonDiscovery({ channel, session }) {
|
|
|
516
583
|
);
|
|
517
584
|
}
|
|
518
585
|
|
|
586
|
+
/** Asked by both front doors before every register/call: an expired lease
|
|
587
|
+
* fences here (answered 503), and after an event-loop stall nothing is
|
|
588
|
+
* served until the record is re-verified as ours. */
|
|
589
|
+
function admitOwnerWork() {
|
|
590
|
+
return ownerHeartbeat ? ownerHeartbeat.admit() : true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** This process no longer holds (or is about to lose) the owner lease: refuse
|
|
594
|
+
* new work at once, unpublish its endpoint so launchers stop finding it, then
|
|
595
|
+
* shut down within the shutdown budget — before a contender may reclaim. */
|
|
596
|
+
function fenceDaemon(reason) {
|
|
597
|
+
if (shuttingDown) return;
|
|
598
|
+
log(`${reason} — refusing new work and stopping so only one daemon serves`);
|
|
599
|
+
sessionTransport?.commitDrain?.(reason);
|
|
600
|
+
transport?.commitDrain?.(reason);
|
|
601
|
+
try {
|
|
602
|
+
if (discoveryPid(DAEMON_DISCOVERY_PATH) === process.pid) rmSync(DAEMON_DISCOVERY_PATH, { force: true });
|
|
603
|
+
} catch (e) {
|
|
604
|
+
log(`daemon.json unpublish failed: ${e?.message || e}`);
|
|
605
|
+
}
|
|
606
|
+
void shutdown(reason);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Heartbeat follow-up while this process still owns the lock: a daemon.json
|
|
610
|
+
* deleted (TEMP cleanup) or overwritten while it runs is written again, so
|
|
611
|
+
* new clients keep finding the one daemon that serves this data dir. */
|
|
612
|
+
function republishMissingDiscovery() {
|
|
613
|
+
if (!publishedEndpoints || shuttingDown || discoveryPid(DAEMON_DISCOVERY_PATH) === process.pid) return;
|
|
614
|
+
try {
|
|
615
|
+
publishDaemonDiscovery(publishedEndpoints);
|
|
616
|
+
log('daemon.json was missing or named another pid — re-published');
|
|
617
|
+
} catch (error) {
|
|
618
|
+
log(`daemon.json re-publish failed: ${error?.message || error}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
519
622
|
/** Ready handshake for the spawner, plus the log-sink handoff pinned to the
|
|
520
623
|
* same boundary. Transport is already listening; signal ready before the heavy
|
|
521
624
|
* channel-worker connect so the spawner's ready wait never blocks on service
|
|
@@ -569,6 +672,7 @@ async function startChannelFrontDoor({ channelsRuntime, bootPhases }) {
|
|
|
569
672
|
transport = createChannelTransport({
|
|
570
673
|
handleCall: channelsRuntime.handleCall,
|
|
571
674
|
agentBroker: agentDispatchBroker,
|
|
675
|
+
admitWork: admitOwnerWork,
|
|
572
676
|
log,
|
|
573
677
|
// The durable channel link lives in this file. Without it a restart loses
|
|
574
678
|
// the pinned session (nothing to restore) and every catalog reports Remote
|
|
@@ -617,8 +721,27 @@ async function main() {
|
|
|
617
721
|
// are per-user and fail closed if another account owns the configured path.
|
|
618
722
|
ensurePrivateRuntimeRoot(RUNTIME_ROOT);
|
|
619
723
|
|
|
620
|
-
claimDaemonOwnership(bootPhases);
|
|
724
|
+
await claimDaemonOwnership(bootPhases);
|
|
621
725
|
registerMemoryRuntimeLazy();
|
|
726
|
+
// Every in-process session runtime removes managed worktrees through the
|
|
727
|
+
// shared manager; its root cleanups drop the path-keyed caches (turn
|
|
728
|
+
// snapshots, code graph) of a removed worktree. Modules load on first use.
|
|
729
|
+
registerWorktreeCacheCleanups();
|
|
730
|
+
// Managed worktrees, off the boot path: finish interrupted deletions and
|
|
731
|
+
// creations, release stale leases and, with autoCleanup on, remove clean,
|
|
732
|
+
// merged worktrees no stored session retains (never one a session keeps).
|
|
733
|
+
void import('../runtime/shared/worktrees/maintenance.mjs')
|
|
734
|
+
.then((maintenance) => maintenance.reconcileWorktreesAtStartup())
|
|
735
|
+
.then((report) => {
|
|
736
|
+
const changed = report.removed.length + report.recovered.length + report.pruned.length;
|
|
737
|
+
if (changed || report.errors.length) {
|
|
738
|
+
log(
|
|
739
|
+
`worktree reconcile: removed ${report.removed.length}, recovered ${report.recovered.length}, ` +
|
|
740
|
+
`pruned ${report.pruned.length}, errors ${report.errors.length}`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
})
|
|
744
|
+
.catch((error) => log(`worktree reconcile failed: ${error?.message || error}`));
|
|
622
745
|
// The daemon owns prompt attachments; its census runs off the boot path
|
|
623
746
|
// (first attempt after the store's start delay, then per its interval).
|
|
624
747
|
void import('../runtime/attachments/store.mjs').then((store) => store.startAttachmentGc()).catch(() => {});
|
|
@@ -764,8 +887,19 @@ async function main() {
|
|
|
764
887
|
} catch {}
|
|
765
888
|
},
|
|
766
889
|
onUpgradeRequested: requestDaemonReplacement,
|
|
890
|
+
admitWork: admitOwnerWork,
|
|
767
891
|
});
|
|
768
892
|
const sessionEndpoint = await bootPhases.measure('session-transport-start', () => sessionTransport.start());
|
|
893
|
+
// A boot whose holder stopped answering may have been superseded by a
|
|
894
|
+
// contender between beats; never publish over the winner's endpoint. An
|
|
895
|
+
// unreadable record (an antivirus lock) is not a takeover.
|
|
896
|
+
const ownerPid = Number(readSingletonOwner(OWNER_PATH).owner?.pid) || 0;
|
|
897
|
+
if (shuttingDown) return;
|
|
898
|
+
if (ownerPid && ownerPid !== process.pid) {
|
|
899
|
+
log(`owner record now names pid=${ownerPid} — exiting for attach before publishing`);
|
|
900
|
+
exitRecorder.setReason(`owner claim superseded by pid=${ownerPid} before publish`, 0);
|
|
901
|
+
process.exit(0);
|
|
902
|
+
}
|
|
769
903
|
publishDaemonDiscovery({ channel: { port, token }, session: sessionEndpoint });
|
|
770
904
|
bootPhases.mark('discovery-published');
|
|
771
905
|
log(`session front door on 127.0.0.1:${sessionEndpoint.port}`);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { sanitizeForWire } from './session-wire-values.mjs';
|
|
4
|
+
import { modelProxyClientMode } from '../runtime/shared/model-proxy-mode.mjs';
|
|
5
|
+
import { getModelProxyChannel } from '../runtime/agent/orchestrator/providers/model-proxy-channel.mjs';
|
|
4
6
|
|
|
5
7
|
const requireDesktopModule = createRequire(import.meta.url);
|
|
6
8
|
|
|
@@ -185,9 +187,24 @@ export class DesktopServiceRegistry {
|
|
|
185
187
|
if (this.#closed) throw new Error('session service is closed');
|
|
186
188
|
const token = subscriberToken(ctx);
|
|
187
189
|
if (token) service.subscribers.add(token);
|
|
190
|
+
this.#syncModelProxyPresence();
|
|
188
191
|
return { desktopId: service.desktopId };
|
|
189
192
|
}
|
|
190
193
|
|
|
194
|
+
// Proxied model calls need a live desktop subscriber; tell the channel the
|
|
195
|
+
// moment the last one leaves instead of waiting for its claim timeout.
|
|
196
|
+
#syncModelProxyPresence() {
|
|
197
|
+
if (!modelProxyClientMode()) return;
|
|
198
|
+
let present = false;
|
|
199
|
+
for (const service of this.#servicesById.values()) {
|
|
200
|
+
if (service.subscribers.size) {
|
|
201
|
+
present = true;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
getModelProxyChannel().setClientPresent(present);
|
|
206
|
+
}
|
|
207
|
+
|
|
191
208
|
// Every addressed call also (re)subscribes its caller to the service lane.
|
|
192
209
|
#requireSubscribed(desktopId, ctx) {
|
|
193
210
|
const service = this.#require(desktopId);
|
|
@@ -211,6 +228,7 @@ export class DesktopServiceRegistry {
|
|
|
211
228
|
|
|
212
229
|
ready({ desktopId } = {}, ctx = null) {
|
|
213
230
|
const { service, token } = this.#requireSubscribed(desktopId, ctx);
|
|
231
|
+
this.#syncModelProxyPresence();
|
|
214
232
|
this.#onReady({ desktopId: service.desktopId, clientToken: token || null });
|
|
215
233
|
return { ok: true };
|
|
216
234
|
}
|
|
@@ -219,6 +237,7 @@ export class DesktopServiceRegistry {
|
|
|
219
237
|
const service = this.#require(desktopId);
|
|
220
238
|
const token = subscriberToken(ctx);
|
|
221
239
|
if (token) service.subscribers.delete(token);
|
|
240
|
+
this.#syncModelProxyPresence();
|
|
222
241
|
return { ok: true, unsubscribed: true };
|
|
223
242
|
}
|
|
224
243
|
|
|
@@ -228,6 +247,7 @@ export class DesktopServiceRegistry {
|
|
|
228
247
|
for (const service of this.#servicesById.values()) {
|
|
229
248
|
service.subscribers.delete(token);
|
|
230
249
|
}
|
|
250
|
+
this.#syncModelProxyPresence();
|
|
231
251
|
}
|
|
232
252
|
|
|
233
253
|
notifySessionRuntimeReleased(sessionId, reason) {
|
|
@@ -122,14 +122,17 @@ export async function createLocalSessionRuntime({
|
|
|
122
122
|
model,
|
|
123
123
|
effort,
|
|
124
124
|
fast,
|
|
125
|
+
autoEffort,
|
|
125
126
|
modelParameters,
|
|
126
127
|
contextPercent,
|
|
128
|
+
contextTokens,
|
|
127
129
|
selectedContextWindow,
|
|
128
130
|
toolMode = 'full',
|
|
129
131
|
remote = false,
|
|
130
132
|
cwd,
|
|
131
133
|
desktopSession,
|
|
132
134
|
sessionProfile,
|
|
135
|
+
worktree,
|
|
133
136
|
executeAgentControl,
|
|
134
137
|
} = {}) {
|
|
135
138
|
const startedAt = performance.now();
|
|
@@ -150,14 +153,18 @@ export async function createLocalSessionRuntime({
|
|
|
150
153
|
model,
|
|
151
154
|
effort,
|
|
152
155
|
fast,
|
|
156
|
+
autoEffort,
|
|
153
157
|
modelParameters,
|
|
154
158
|
contextPercent,
|
|
159
|
+
contextTokens,
|
|
155
160
|
selectedContextWindow,
|
|
156
161
|
toolMode,
|
|
157
162
|
remote,
|
|
158
163
|
...(cwd ? { cwd } : {}),
|
|
159
164
|
...(desktopSession ? { desktopSession } : {}),
|
|
160
165
|
...(sessionProfile && typeof sessionProfile === 'object' ? { sessionProfile } : {}),
|
|
166
|
+
// Session creation worktree intent; consumed by the workspace transition scope.
|
|
167
|
+
...(worktree && typeof worktree === 'object' ? { worktree } : {}),
|
|
161
168
|
...(typeof executeAgentControl === 'function' ? { executeAgentControl } : {}),
|
|
162
169
|
});
|
|
163
170
|
bootProfile('session:create:runtime-ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
@@ -396,6 +403,10 @@ export async function createLocalSessionRuntime({
|
|
|
396
403
|
subscribeRuntimeNotifications,
|
|
397
404
|
});
|
|
398
405
|
Object.assign(bag, createSessionFlow(bag));
|
|
406
|
+
// Runtime-driven completion wakes (no attached surface acked the completion)
|
|
407
|
+
// get the same idle auto-clear as a user submit.
|
|
408
|
+
lifecycle.unsubscribeCompletionWakeGate =
|
|
409
|
+
runtime.setCompletionWakeGate?.(() => bag.autoClearBeforeWake()) || null;
|
|
399
410
|
Object.assign(
|
|
400
411
|
bag,
|
|
401
412
|
createGoalContinuation({
|
|
@@ -13,7 +13,7 @@ const URGENT_CALLS = new Set([
|
|
|
13
13
|
'desktop.unsubscribe',
|
|
14
14
|
]);
|
|
15
15
|
|
|
16
|
-
export function createAttachmentCalls({ request, port, serverToken, clientToken, stopStream, log }) {
|
|
16
|
+
export function createAttachmentCalls({ request, port, serverToken, clientToken, stopStream, stopHeartbeat, log }) {
|
|
17
17
|
let closed = false;
|
|
18
18
|
|
|
19
19
|
async function call(name, args = {}, { timeoutMs = 300_000, callId = null } = {}) {
|
|
@@ -38,6 +38,7 @@ export function createAttachmentCalls({ request, port, serverToken, clientToken,
|
|
|
38
38
|
// Preserve the daemon's machine-readable classification across the wire.
|
|
39
39
|
const err = new Error(out.error);
|
|
40
40
|
if (out.code) err.code = String(out.code);
|
|
41
|
+
if (out.details && typeof out.details === 'object') err.details = out.details;
|
|
41
42
|
throw err;
|
|
42
43
|
}
|
|
43
44
|
return out?.result;
|
|
@@ -47,6 +48,7 @@ export function createAttachmentCalls({ request, port, serverToken, clientToken,
|
|
|
47
48
|
if (closed) return;
|
|
48
49
|
closed = true;
|
|
49
50
|
stopStream();
|
|
51
|
+
stopHeartbeat?.();
|
|
50
52
|
try {
|
|
51
53
|
await request({
|
|
52
54
|
port,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The error ensureDaemon throws when no session endpoint appeared before its
|
|
2
|
+
// deadline. The first line is the stable message callers and tests match; the
|
|
3
|
+
// lines below carry what the last pass observed — an unanswered discovery, an
|
|
4
|
+
// owner that never published, the newest candidate's exit and first stderr
|
|
5
|
+
// error — so the desktop error card's details name the cause, not only the
|
|
6
|
+
// symptom.
|
|
7
|
+
export const DAEMON_UNAVAILABLE_MESSAGE = 'daemon session endpoint is unavailable';
|
|
8
|
+
|
|
9
|
+
function exitCodeText(code) {
|
|
10
|
+
// Windows reports NTSTATUS crash codes as large unsigned values.
|
|
11
|
+
return Number.isSafeInteger(code) && code > 0x7fffffff ? `0x${code.toString(16).toUpperCase()}` : String(code);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function candidateLines(candidate, { attempt, maxAttempts, now }) {
|
|
15
|
+
const head = `last daemon candidate (attempt ${attempt} of ${maxAttempts})`;
|
|
16
|
+
if (!candidate.pid) return [`${head} failed to spawn: ${candidate.error || 'unknown error'}`];
|
|
17
|
+
let state;
|
|
18
|
+
if (candidate.exitedAt) {
|
|
19
|
+
state =
|
|
20
|
+
`exited code=${candidate.exitCode === null ? '-' : exitCodeText(candidate.exitCode)}` +
|
|
21
|
+
` signal=${candidate.exitSignal || '-'} after ${candidate.uptimeMs ?? '?'}ms` +
|
|
22
|
+
(candidate.exitReason ? ` (${candidate.exitReason})` : '');
|
|
23
|
+
} else if (candidate.ready) {
|
|
24
|
+
state = 'reported ready but its endpoint never answered';
|
|
25
|
+
} else {
|
|
26
|
+
const spawnedAt = Date.parse(candidate.spawnedAt || '');
|
|
27
|
+
state = `still running, not ready ${Number.isFinite(spawnedAt) ? now - spawnedAt : '?'}ms after spawn`;
|
|
28
|
+
}
|
|
29
|
+
const lines = [`${head}: pid=${candidate.pid} ${state}`];
|
|
30
|
+
if (candidate.stderrErrorLine) lines.push(`first stderr error: ${candidate.stderrErrorLine}`);
|
|
31
|
+
else if (candidate.stderrLastLine) lines.push(`last stderr line: ${candidate.stderrLastLine}`);
|
|
32
|
+
if (candidate.stderrPath) lines.push(`stderr capture: ${candidate.stderrPath}`);
|
|
33
|
+
return lines;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function daemonUnavailableError({
|
|
37
|
+
discovery = null,
|
|
38
|
+
owner = null,
|
|
39
|
+
candidate = null,
|
|
40
|
+
spawnAttempts = 0,
|
|
41
|
+
maxSpawnAttempts = 0,
|
|
42
|
+
captureDir,
|
|
43
|
+
now = Date.now(),
|
|
44
|
+
}) {
|
|
45
|
+
const lines = [DAEMON_UNAVAILABLE_MESSAGE];
|
|
46
|
+
if (discovery) lines.push(`discovery: pid=${discovery.pid} port=${discovery.port} did not answer its health check`);
|
|
47
|
+
if (owner) {
|
|
48
|
+
lines.push(
|
|
49
|
+
`daemon owner: pid=${owner.pid} (claimed ${owner.claimedAt || 'at an unknown time'}) held the lock` +
|
|
50
|
+
' but never published an endpoint'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (candidate) {
|
|
54
|
+
lines.push(...candidateLines(candidate, { attempt: spawnAttempts, maxAttempts: maxSpawnAttempts, now }));
|
|
55
|
+
} else {
|
|
56
|
+
lines.push(`no daemon candidate was spawned (spawn attempts allowed: ${maxSpawnAttempts})`);
|
|
57
|
+
}
|
|
58
|
+
lines.push(`daemon crash captures: ${captureDir}`);
|
|
59
|
+
const error = new Error(lines.join('\n'));
|
|
60
|
+
error.daemonUnavailable = { discovery, owner, candidate, spawnAttempts, captureDir };
|
|
61
|
+
return error;
|
|
62
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// session-client/lease-heartbeat.mjs
|
|
2
|
+
// A remote attachment has no local pid the daemon can probe, so its liveness
|
|
3
|
+
// is a lease the client renews: POST /client/heartbeat every `heartbeatMs`,
|
|
4
|
+
// one request in flight at a time, each bounded by heartbeatMs / 2. A failed
|
|
5
|
+
// beat is only logged — the next tick retries, and the stream's own recovery
|
|
6
|
+
// decides when the attachment is truly lost.
|
|
7
|
+
import { clampLeaseMs } from '../session-transport/client-registry/client-grace.mjs';
|
|
8
|
+
|
|
9
|
+
export function createLeaseHeartbeat({ request, port, serverToken, clientToken, heartbeatMs = 10_000, leaseMs, log }) {
|
|
10
|
+
// Always beat at least three times per (server-clamped) lease.
|
|
11
|
+
heartbeatMs = Math.min(heartbeatMs, Math.floor(clampLeaseMs(leaseMs) / 3));
|
|
12
|
+
let timer = null;
|
|
13
|
+
let inFlight = false;
|
|
14
|
+
|
|
15
|
+
async function beat() {
|
|
16
|
+
if (inFlight) return;
|
|
17
|
+
inFlight = true;
|
|
18
|
+
try {
|
|
19
|
+
await request({
|
|
20
|
+
port,
|
|
21
|
+
token: serverToken,
|
|
22
|
+
method: 'POST',
|
|
23
|
+
path: '/client/heartbeat',
|
|
24
|
+
body: { token: clientToken },
|
|
25
|
+
timeoutMs: heartbeatMs / 2,
|
|
26
|
+
control: true,
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
log(`heartbeat failed: ${error?.message || error}`);
|
|
30
|
+
} finally {
|
|
31
|
+
inFlight = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function start() {
|
|
36
|
+
if (timer) return;
|
|
37
|
+
timer = setInterval(beat, heartbeatMs);
|
|
38
|
+
timer.unref?.();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stop() {
|
|
42
|
+
if (timer) clearInterval(timer);
|
|
43
|
+
timer = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { start, stop };
|
|
47
|
+
}
|