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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session workspace record helpers: reading the durable `workspace` field,
|
|
3
|
+
* keeping it consistent when the cwd tool switches Projects, restoring it on
|
|
4
|
+
* resume (missing worktree → recovery), and carrying the Project's memory
|
|
5
|
+
* identity into a new worktree.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { isWorktreeBound, nextRevision, normalizeSessionWorkspace } from '../../runtime/shared/worktrees/contract.mjs';
|
|
10
|
+
import { isWithin, samePath } from '../../runtime/shared/worktrees/paths.mjs';
|
|
11
|
+
import { resolveProjectId } from '../../runtime/memory/lib/project-id-resolver.mjs';
|
|
12
|
+
import { setWorkspaceCwdLock } from './admission-gate.mjs';
|
|
13
|
+
|
|
14
|
+
/** The session's persisted workspace, or null when absent or malformed. */
|
|
15
|
+
export function readSessionWorkspace(session) {
|
|
16
|
+
if (!session?.workspace) return null;
|
|
17
|
+
try {
|
|
18
|
+
return normalizeSessionWorkspace(session.workspace);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A manual Project selection (cwd tool / setCwd) while a workspace record
|
|
26
|
+
* exists. Moving inside the bound worktree keeps the record; any other
|
|
27
|
+
* directory is a Project switch and leaves the worktree (still retained) at a
|
|
28
|
+
* new revision. Returns the record to store and the Project path the desktop
|
|
29
|
+
* metadata must carry.
|
|
30
|
+
* @returns {{ workspace: object|null, projectPath: string }}
|
|
31
|
+
*/
|
|
32
|
+
export function workspaceForProjectSelection(session, cwd) {
|
|
33
|
+
const current = readSessionWorkspace(session);
|
|
34
|
+
if (!current) return { workspace: session?.workspace ?? null, projectPath: cwd };
|
|
35
|
+
if (isWorktreeBound(current) && isWithin(current.cwd, cwd)) {
|
|
36
|
+
return { workspace: session.workspace, projectPath: current.projectPath };
|
|
37
|
+
}
|
|
38
|
+
if (!isWorktreeBound(current) && samePath(current.cwd, cwd)) {
|
|
39
|
+
return { workspace: session.workspace, projectPath: cwd };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
workspace: normalizeSessionWorkspace({ projectPath: cwd, cwd, revision: nextRevision(current) }),
|
|
43
|
+
projectPath: cwd,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Where a session that replaces `session` (/new, /clear) starts: the Project
|
|
49
|
+
* folder when `session` is bound to a worktree — that worktree belongs to the
|
|
50
|
+
* session being left — else null (keep the current cwd).
|
|
51
|
+
* @returns {string|null}
|
|
52
|
+
*/
|
|
53
|
+
export function replacementSessionCwd(session) {
|
|
54
|
+
const workspace = readSessionWorkspace(session);
|
|
55
|
+
return isWorktreeBound(workspace) ? workspace.projectPath : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execution root of a worktree-bound session for resume: the worktree when it
|
|
60
|
+
* exists, else the Project folder flagged `missing`. Null when not bound.
|
|
61
|
+
* @returns {{ cwd: string, missing: boolean }|null}
|
|
62
|
+
*/
|
|
63
|
+
export function boundWorkspaceCwd(session) {
|
|
64
|
+
const workspace = readSessionWorkspace(session);
|
|
65
|
+
if (!isWorktreeBound(workspace)) return null;
|
|
66
|
+
if (existsSync(workspace.cwd)) return { cwd: workspace.cwd, missing: false };
|
|
67
|
+
return { cwd: workspace.projectPath, missing: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* After a resume: a bound session whose worktree vanished enters `recovery`
|
|
72
|
+
* (turns are refused with WORKTREE_MISSING until the user picks a branch or
|
|
73
|
+
* removes the worktree); an intact one re-locks its workers to the worktree.
|
|
74
|
+
*/
|
|
75
|
+
export function restoreWorkspaceAfterResume(session) {
|
|
76
|
+
const workspace = readSessionWorkspace(session);
|
|
77
|
+
if (!session?.id) return workspace;
|
|
78
|
+
if (!isWorktreeBound(workspace)) {
|
|
79
|
+
setWorkspaceCwdLock(session.id, null);
|
|
80
|
+
return workspace;
|
|
81
|
+
}
|
|
82
|
+
if (existsSync(workspace.cwd)) {
|
|
83
|
+
setWorkspaceCwdLock(session.id, workspace.cwd);
|
|
84
|
+
if (workspace.transition.state !== 'idle') {
|
|
85
|
+
session.workspace = normalizeSessionWorkspace({ ...workspace, transition: { state: 'idle', txId: null } });
|
|
86
|
+
}
|
|
87
|
+
return session.workspace;
|
|
88
|
+
}
|
|
89
|
+
setWorkspaceCwdLock(session.id, null);
|
|
90
|
+
session.workspace = normalizeSessionWorkspace({
|
|
91
|
+
...workspace,
|
|
92
|
+
transition: { state: 'recovery', txId: `recovery:${workspace.worktreeId}` },
|
|
93
|
+
});
|
|
94
|
+
return session.workspace;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Carry the Project's memory identity into a worktree that lives outside the
|
|
99
|
+
* Project tree. Runs before anything resolves memory for the worktree (the
|
|
100
|
+
* resolver memoizes per root). No-op when the checkout already has the file
|
|
101
|
+
* or the Project has no identity.
|
|
102
|
+
* @returns {boolean} Whether a file was written.
|
|
103
|
+
*/
|
|
104
|
+
export function copyProjectIdentity(projectPath, worktreePath) {
|
|
105
|
+
const target = join(worktreePath, '.mixdog', 'project.id');
|
|
106
|
+
if (existsSync(target)) return false;
|
|
107
|
+
const id = resolveProjectId(projectPath);
|
|
108
|
+
if (!id) return false;
|
|
109
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
110
|
+
writeFileSync(target, `${id}\n`, 'utf8');
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
@@ -20,6 +20,7 @@ export function createChannelRoutes({
|
|
|
20
20
|
onClientsEmpty,
|
|
21
21
|
registry,
|
|
22
22
|
dispatch,
|
|
23
|
+
admitWork = () => true,
|
|
23
24
|
}) {
|
|
24
25
|
const { clients } = state;
|
|
25
26
|
|
|
@@ -27,6 +28,16 @@ export function createChannelRoutes({
|
|
|
27
28
|
sendError(res, `daemon is draining: ${state.drainingReason}`, 503);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// Checked on every register/call/agent dispatch, first and again right
|
|
32
|
+
// before the work is handed on (the lease may have been fenced while the
|
|
33
|
+
// body was still arriving): a daemon that lost (or cannot re-verify) its
|
|
34
|
+
// owner lease serves nothing.
|
|
35
|
+
function refuseUnadmitted(res) {
|
|
36
|
+
if (admitWork()) return false;
|
|
37
|
+
sendError(res, 'daemon owner lease is not held', 503);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
function health(res) {
|
|
31
42
|
sendJson(res, {
|
|
32
43
|
status: 'ok',
|
|
@@ -43,8 +54,10 @@ export function createChannelRoutes({
|
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
async function register(req, res) {
|
|
57
|
+
if (refuseUnadmitted(res)) return;
|
|
46
58
|
if (state.drainCommitted) return sendDraining(res);
|
|
47
59
|
const body = await readChannelBody(req);
|
|
60
|
+
if (refuseUnadmitted(res)) return;
|
|
48
61
|
const clientToken = registry.registerClient({
|
|
49
62
|
leadPid: body.leadPid,
|
|
50
63
|
cwd: body.cwd,
|
|
@@ -83,9 +96,11 @@ export function createChannelRoutes({
|
|
|
83
96
|
// channels client registry alive. The broker itself owns a parallel fair
|
|
84
97
|
// scheduler and per-call cancellation.
|
|
85
98
|
async function agentDispatch(req, res) {
|
|
99
|
+
if (refuseUnadmitted(res)) return;
|
|
86
100
|
if (state.drainCommitted) return sendDraining(res);
|
|
87
101
|
if (!agentBroker?.dispatch) return sendError(res, 'agent broker unavailable', 503);
|
|
88
102
|
const body = await readChannelBody(req);
|
|
103
|
+
if (refuseUnadmitted(res)) return;
|
|
89
104
|
const callId = String(body.callId || '').trim();
|
|
90
105
|
if (!callId) return sendError(res, 'callId required', 400);
|
|
91
106
|
res.on('close', () => {
|
|
@@ -115,8 +130,10 @@ export function createChannelRoutes({
|
|
|
115
130
|
}
|
|
116
131
|
|
|
117
132
|
async function call(req, res) {
|
|
133
|
+
if (refuseUnadmitted(res)) return;
|
|
118
134
|
if (state.drainCommitted) return sendDraining(res);
|
|
119
135
|
const body = await readChannelBody(req);
|
|
136
|
+
if (refuseUnadmitted(res)) return;
|
|
120
137
|
const clientToken = body.token || null;
|
|
121
138
|
const c = clientToken ? clients.get(clientToken) : null;
|
|
122
139
|
if (!c) return sendError(res, 'unknown client token', 404);
|
|
@@ -41,10 +41,21 @@ export function createChannelTransport({
|
|
|
41
41
|
onClientRegistered = null,
|
|
42
42
|
onRemoteStateChange = null,
|
|
43
43
|
agentBroker = null,
|
|
44
|
+
// Asked before every register/call/agent dispatch; false answers 503 (the
|
|
45
|
+
// daemon's owner lease is not held — daemon-owner.mjs).
|
|
46
|
+
admitWork = () => true,
|
|
44
47
|
} = {}) {
|
|
45
48
|
if (typeof handleCall !== 'function') throw new Error('handleCall is required');
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
let remoteIntent = null;
|
|
51
|
+
try {
|
|
52
|
+
remoteIntent = readRemoteIntent(remoteIntentPath);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// An unreadable pin (permissions, an antivirus lock) only costs the channel
|
|
55
|
+
// its restored session; it must never keep the whole daemon from booting.
|
|
56
|
+
// The file stays in place.
|
|
57
|
+
log(`channel remote intent unreadable, starting unpinned: ${error?.message || error}`);
|
|
58
|
+
}
|
|
48
59
|
const state = {
|
|
49
60
|
// token -> { token, leadPid, cwd, sse, lastSeen, registeredAt }
|
|
50
61
|
clients: new Map(),
|
|
@@ -111,6 +122,7 @@ export function createChannelTransport({
|
|
|
111
122
|
onClientsEmpty,
|
|
112
123
|
registry,
|
|
113
124
|
dispatch,
|
|
125
|
+
admitWork,
|
|
114
126
|
});
|
|
115
127
|
|
|
116
128
|
function beginDrain(reason = 'daemon replacement') {
|
|
@@ -16,6 +16,7 @@ export function createDesktopRuntime({ getLocalSessionBridge }) {
|
|
|
16
16
|
return bridge.attach(options);
|
|
17
17
|
},
|
|
18
18
|
loadProjects: () => import('../runtime/shared/projects.mjs'),
|
|
19
|
+
loadWorktrees: () => import('../runtime/shared/worktrees/manager.mjs'),
|
|
19
20
|
loadSessionStore: () => import('../runtime/agent/orchestrator/session/store-summary-reader.mjs'),
|
|
20
21
|
loadSessionSearch: () => import('../runtime/session-search/session-search-db.mjs'),
|
|
21
22
|
loadStatuslineSegments: () => import('../runtime/shared/statusline/statusline-segments.mjs'),
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// Whether the daemon-owner.json record still names a daemon, and the lease a
|
|
2
|
+
// daemon holds on it.
|
|
3
|
+
//
|
|
4
|
+
// A live pid proves nothing on its own. A daemon terminated without running
|
|
5
|
+
// its exit hooks (Windows shutdown, an installer or Task Manager ending the
|
|
6
|
+
// process tree) leaves its owner record behind, and the OS soon hands that pid
|
|
7
|
+
// to an unrelated process. A pid-only check then reported a live owner for as
|
|
8
|
+
// long as that process ran: every launcher waited for it to publish, every
|
|
9
|
+
// spawned candidate yielded to it, and the desktop failed every call with
|
|
10
|
+
// "daemon session endpoint is unavailable".
|
|
11
|
+
//
|
|
12
|
+
// Single-owner safety never rests on the wall clock:
|
|
13
|
+
// - Holder: keeps its lease in monotonic time since its last successful
|
|
14
|
+
// refresh. An expired lease fences it (refuse work, shut down) instead of
|
|
15
|
+
// being renewed, and every register/call asks `admit()` first. After an
|
|
16
|
+
// event-loop stall nothing is served until a refresh under the claim lock
|
|
17
|
+
// proves the record still names it — a contender's claim takes that lock.
|
|
18
|
+
// - Holder's record names `livenessPort` from its very first claim: a tiny
|
|
19
|
+
// loopback listener started BEFORE the claim (startOwnerLivenessListener)
|
|
20
|
+
// answers /health with the pid, through boot and for the process lifetime.
|
|
21
|
+
// - Contender: may reclaim a live-pid record only when its heartbeat looks
|
|
22
|
+
// stale (wall clock: a reused pid never beats) AND that port, probed for the
|
|
23
|
+
// full OWNER_PROBE_TIMEOUT_MS, does not answer with that pid — never on
|
|
24
|
+
// wall-clock staleness alone, so a clock jump never reclaims a holder that
|
|
25
|
+
// answers, booting or not; a holder silent for that long has stalled past
|
|
26
|
+
// `stallMs` and re-verifies before serving again.
|
|
27
|
+
//
|
|
28
|
+
// Records without a heartbeat come from v1.1.2 and older (and records with a
|
|
29
|
+
// heartbeat but no liveness port only from unreleased intermediate builds,
|
|
30
|
+
// judged the same way). Their pid matching the leftover daemon.json proves
|
|
31
|
+
// nothing — a crashed daemon leaves both files behind — so only the endpoint
|
|
32
|
+
// daemon.json publishes, answering with that pid, proves them alive.
|
|
33
|
+
import http from 'node:http';
|
|
34
|
+
import { readFileSync } from 'node:fs';
|
|
35
|
+
import { isPidAlive } from '../runtime/shared/pid-liveness.mjs';
|
|
36
|
+
import { readSingletonOwner, refreshSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
|
|
37
|
+
|
|
38
|
+
export const DAEMON_OWNER_STALE_MS = 60_000;
|
|
39
|
+
export const OWNER_PROBE_TIMEOUT_MS = 3_000;
|
|
40
|
+
const PROBE_BODY_MAX_BYTES = 1024 * 1024;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Lease timings. MIXDOG_DAEMON_OWNER_STALE_MS may only shorten the stale
|
|
44
|
+
* threshold (tests); every process on one data dir must see the same value.
|
|
45
|
+
* Six beats per threshold; fenced at two thirds of it (40 s of 60 s, leaving
|
|
46
|
+
* the 15 s shutdown budget inside it); the lease is checked every `tickMs`
|
|
47
|
+
* and a turn gap over `stallMs` (< OWNER_PROBE_TIMEOUT_MS) counts as a stall.
|
|
48
|
+
*/
|
|
49
|
+
export function daemonOwnerTimings(env = process.env) {
|
|
50
|
+
const configured = Number(env.MIXDOG_DAEMON_OWNER_STALE_MS);
|
|
51
|
+
const staleMs =
|
|
52
|
+
Number.isFinite(configured) && configured > 0
|
|
53
|
+
? Math.min(DAEMON_OWNER_STALE_MS, Math.max(600, configured))
|
|
54
|
+
: DAEMON_OWNER_STALE_MS;
|
|
55
|
+
const heartbeatMs = Math.round(staleMs / 6);
|
|
56
|
+
const tickMs = Math.min(250, Math.round(heartbeatMs / 4));
|
|
57
|
+
return { staleMs, heartbeatMs, fenceMs: Math.round((staleMs * 2) / 3), tickMs, stallMs: tickMs * 4 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** daemon.json's pid and session endpoint, or null when missing/unreadable. */
|
|
61
|
+
export function readDiscovery(discoveryPath) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(readFileSync(discoveryPath, 'utf8'));
|
|
64
|
+
const session = parsed?.endpoints?.session;
|
|
65
|
+
return { pid: Number(parsed?.pid) || 0, port: Number(session?.port) || 0, token: session?.token || '' };
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The pid daemon.json names, or 0 when it is missing or unreadable. */
|
|
72
|
+
export function discoveryPid(discoveryPath) {
|
|
73
|
+
return readDiscovery(discoveryPath)?.pid || 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The pid a session endpoint's /health reports (0 when it does not answer
|
|
78
|
+
* with one), bounded by `timeoutMs` of TOTAL duration — a response trickling
|
|
79
|
+
* in byte by byte cannot stretch it. `timedOut` tells a probe cut off by the
|
|
80
|
+
* bound from one refused or answered.
|
|
81
|
+
*/
|
|
82
|
+
export function probeDaemonPid({ port, token = '', timeoutMs = OWNER_PROBE_TIMEOUT_MS }) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
if (!(Number.isInteger(port) && port > 0 && port < 65_536)) {
|
|
85
|
+
resolve({ pid: 0, timedOut: false });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let settled = false;
|
|
89
|
+
let timer = null;
|
|
90
|
+
let req = null;
|
|
91
|
+
const finish = (pid, timedOut = false) => {
|
|
92
|
+
if (settled) return;
|
|
93
|
+
settled = true;
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
resolve({ pid, timedOut });
|
|
96
|
+
req?.destroy();
|
|
97
|
+
};
|
|
98
|
+
timer = setTimeout(() => finish(0, true), Math.max(1, timeoutMs));
|
|
99
|
+
req = http.get(
|
|
100
|
+
{
|
|
101
|
+
hostname: '127.0.0.1',
|
|
102
|
+
port,
|
|
103
|
+
path: '/health',
|
|
104
|
+
agent: false,
|
|
105
|
+
headers: token ? { 'X-Mixdog-Daemon-Token': token } : {},
|
|
106
|
+
},
|
|
107
|
+
(res) => {
|
|
108
|
+
let body = '';
|
|
109
|
+
res.setEncoding('utf8');
|
|
110
|
+
res.on('data', (chunk) => {
|
|
111
|
+
body += chunk;
|
|
112
|
+
if (body.length > PROBE_BODY_MAX_BYTES) finish(0);
|
|
113
|
+
});
|
|
114
|
+
res.on('end', () => {
|
|
115
|
+
let health = null;
|
|
116
|
+
try {
|
|
117
|
+
health = JSON.parse(body);
|
|
118
|
+
} catch {}
|
|
119
|
+
finish(res.statusCode === 200 && health?.status === 'ok' ? Number(health.pid) || 0 : 0);
|
|
120
|
+
});
|
|
121
|
+
res.on('error', () => finish(0));
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
req.on('error', () => finish(0));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function olderThan(stamp, now, staleMs) {
|
|
129
|
+
const at = Date.parse(String(stamp ?? ''));
|
|
130
|
+
// A clock step in either direction must not keep a dead record fresh.
|
|
131
|
+
return !Number.isFinite(at) || Math.abs(now - at) > staleMs;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validPort(value) {
|
|
135
|
+
const port = Number(value);
|
|
136
|
+
return Number.isInteger(port) && port > 0 && port < 65_536 ? port : 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The liveness port of a leased record (heartbeat + port), else 0. */
|
|
140
|
+
function leasedPort(owner) {
|
|
141
|
+
return owner?.heartbeatAt !== undefined ? validPort(owner.livenessPort) : 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The holder's owner-liveness listener: loopback /health answering this pid
|
|
146
|
+
* (unauthenticated, like the session /health). Started before the claim so
|
|
147
|
+
* the very first record names its port; it answers whenever this process's
|
|
148
|
+
* event loop runs. Resolves null when loopback cannot be bound — the record
|
|
149
|
+
* then carries no port and is judged like a legacy one.
|
|
150
|
+
*/
|
|
151
|
+
export function startOwnerLivenessListener({ pid = process.pid, log = () => {} } = {}) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
const server = http.createServer((req, res) => {
|
|
154
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
155
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
156
|
+
res.end(JSON.stringify({ status: 'ok', pid }));
|
|
157
|
+
} else {
|
|
158
|
+
res.writeHead(404);
|
|
159
|
+
res.end();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
server.once('error', (error) => {
|
|
163
|
+
log(`owner liveness listener unavailable: ${error?.message || error}`);
|
|
164
|
+
resolve(null);
|
|
165
|
+
});
|
|
166
|
+
server.listen(0, '127.0.0.1', () => {
|
|
167
|
+
// Never what keeps the process alive.
|
|
168
|
+
server.unref();
|
|
169
|
+
resolve({
|
|
170
|
+
port: server.address().port,
|
|
171
|
+
close: () => new Promise((done) => server.close(() => done())),
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Liveness proof for a live-pid owner record that may be reclaimable.
|
|
179
|
+
* Leased record (heartbeat + liveness port): null while the heartbeat looks
|
|
180
|
+
* fresh, else a probe of that port. Any other record (v1.1.2 and older, or an
|
|
181
|
+
* intermediate build's record without a port): a probe of the endpoint
|
|
182
|
+
* daemon.json publishes for that pid. Verdicts: `live` (answered with the
|
|
183
|
+
* pid), `dead` (refused, answered otherwise, or silent for the full probe
|
|
184
|
+
* timeout), `unknown` (silent for a probe cut short by `budgetMs`),
|
|
185
|
+
* `unpublished` (legacy only: no endpoint to probe).
|
|
186
|
+
*/
|
|
187
|
+
export async function proveDaemonOwner(
|
|
188
|
+
owner,
|
|
189
|
+
{
|
|
190
|
+
discoveryPath,
|
|
191
|
+
now = Date.now(),
|
|
192
|
+
staleMs = daemonOwnerTimings().staleMs,
|
|
193
|
+
probe = probeDaemonPid,
|
|
194
|
+
timeoutMs = OWNER_PROBE_TIMEOUT_MS,
|
|
195
|
+
budgetMs = Number.POSITIVE_INFINITY,
|
|
196
|
+
} = {}
|
|
197
|
+
) {
|
|
198
|
+
const pid = Number(owner?.pid) || 0;
|
|
199
|
+
if (!pid || !isPidAlive(pid)) return null;
|
|
200
|
+
const limit = Math.max(1, Math.min(timeoutMs, budgetMs));
|
|
201
|
+
const verdictOf = (answer) => {
|
|
202
|
+
if (answer.pid === pid) return 'live';
|
|
203
|
+
return answer.timedOut && limit < timeoutMs ? 'unknown' : 'dead';
|
|
204
|
+
};
|
|
205
|
+
const livenessPort = leasedPort(owner);
|
|
206
|
+
if (livenessPort) {
|
|
207
|
+
if (!olderThan(owner.heartbeatAt, now, staleMs)) return null;
|
|
208
|
+
return { pid, verdict: verdictOf(await probe({ port: livenessPort, timeoutMs: limit })) };
|
|
209
|
+
}
|
|
210
|
+
const discovery = readDiscovery(discoveryPath);
|
|
211
|
+
if (discovery?.pid !== pid || !validPort(discovery.port)) return { pid, verdict: 'unpublished' };
|
|
212
|
+
return { pid, verdict: verdictOf(await probe({ port: discovery.port, token: discovery.token, timeoutMs: limit })) };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** proveDaemonOwner for the record currently at `ownerPath`. */
|
|
216
|
+
export function proveDaemonOwnerRecord(ownerPath, options) {
|
|
217
|
+
return proveDaemonOwner(readSingletonOwner(ownerPath).owner, options);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Whether a live-pid owner record may be reclaimed (the pid check itself is
|
|
222
|
+
* the caller's: readSingletonOwner / claimSingletonOwner), given `proof` from
|
|
223
|
+
* proveDaemonOwner for its pid. Without that proof it stands.
|
|
224
|
+
*/
|
|
225
|
+
export function daemonOwnerIsStale(
|
|
226
|
+
owner,
|
|
227
|
+
{ proof = null, now = Date.now(), staleMs = daemonOwnerTimings().staleMs } = {}
|
|
228
|
+
) {
|
|
229
|
+
const pid = Number(owner?.pid) || 0;
|
|
230
|
+
if (!pid || proof?.pid !== pid) return false;
|
|
231
|
+
if (leasedPort(owner)) {
|
|
232
|
+
// Only a failed probe of its liveness port, never staleness alone. The
|
|
233
|
+
// heartbeat is re-checked: under the claim lock the holder may just have
|
|
234
|
+
// beaten.
|
|
235
|
+
return olderThan(owner.heartbeatAt, now, staleMs) && proof.verdict === 'dead';
|
|
236
|
+
}
|
|
237
|
+
if (proof.verdict === 'dead') return true;
|
|
238
|
+
return proof.verdict === 'unpublished' && olderThan(owner.claimedAt, now, staleMs);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The holder's side of the lease, in monotonic time (`monoNow`). Every
|
|
243
|
+
* `tickMs` it checks the lease, notices stalls, and every `heartbeatMs`
|
|
244
|
+
* rewrites `owner` with a fresh wall-clock `heartbeatAt` under the claim lock
|
|
245
|
+
* (recreating a deleted record), then calls `onBeat`.
|
|
246
|
+
* - No successful refresh for `fenceMs`: fenced, never renewed (`onFenced`).
|
|
247
|
+
* - The record names another pid: `onSuperseded(record)`.
|
|
248
|
+
* - A turn gap over `stallMs`: `admit()` refuses work until a refresh succeeds.
|
|
249
|
+
* Both callbacks must make this process stop serving.
|
|
250
|
+
*/
|
|
251
|
+
export function createOwnerHeartbeat({
|
|
252
|
+
ownerPath,
|
|
253
|
+
owner,
|
|
254
|
+
heartbeatMs,
|
|
255
|
+
fenceMs,
|
|
256
|
+
tickMs,
|
|
257
|
+
stallMs,
|
|
258
|
+
now = Date.now,
|
|
259
|
+
monoNow = () => performance.now(),
|
|
260
|
+
refresh = refreshSingletonOwner,
|
|
261
|
+
log = () => {},
|
|
262
|
+
onBeat = () => {},
|
|
263
|
+
onSuperseded,
|
|
264
|
+
onFenced,
|
|
265
|
+
}) {
|
|
266
|
+
const record = { ...owner };
|
|
267
|
+
let state = 'active';
|
|
268
|
+
let leaseFrom = monoNow();
|
|
269
|
+
let lastRefreshAt = record.heartbeatAt ?? null;
|
|
270
|
+
let lastBeatAt = leaseFrom;
|
|
271
|
+
let lastTurnAt = leaseFrom;
|
|
272
|
+
let needsVerify = false;
|
|
273
|
+
let timer = null;
|
|
274
|
+
|
|
275
|
+
function halt(next) {
|
|
276
|
+
state = next;
|
|
277
|
+
if (timer) clearInterval(timer);
|
|
278
|
+
timer = null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function leaseExpired(t) {
|
|
282
|
+
return t - leaseFrom >= fenceMs;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function fence(t) {
|
|
286
|
+
if (state !== 'active') return;
|
|
287
|
+
halt('fenced');
|
|
288
|
+
log(`owner lease not refreshed for ${Math.round(t - leaseFrom)}ms (last ${lastRefreshAt}) — fencing`);
|
|
289
|
+
onFenced({ lastRefreshAt, sinceMs: Math.round(t - leaseFrom) });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function noteTurn(t) {
|
|
293
|
+
if (t - lastTurnAt > stallMs && !needsVerify) {
|
|
294
|
+
needsVerify = true;
|
|
295
|
+
log(`event loop stalled ${Math.round(t - lastTurnAt)}ms — owner lease must be re-verified before serving`);
|
|
296
|
+
}
|
|
297
|
+
lastTurnAt = t;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function beat() {
|
|
301
|
+
if (state !== 'active') return null;
|
|
302
|
+
const t = monoNow();
|
|
303
|
+
lastBeatAt = t;
|
|
304
|
+
// Checked before every renewal: an overdue beat after a stall must fence,
|
|
305
|
+
// not extend a lease a contender may already have reclaimed.
|
|
306
|
+
if (leaseExpired(t)) {
|
|
307
|
+
fence(t);
|
|
308
|
+
return 'fenced';
|
|
309
|
+
}
|
|
310
|
+
const heartbeatAt = new Date(now()).toISOString();
|
|
311
|
+
let result;
|
|
312
|
+
try {
|
|
313
|
+
result = refresh(ownerPath, { ...record, heartbeatAt });
|
|
314
|
+
} catch (error) {
|
|
315
|
+
log(`owner heartbeat failed: ${error?.message || error}`);
|
|
316
|
+
result = { status: 'failed' };
|
|
317
|
+
}
|
|
318
|
+
if (result.status === 'refreshed') {
|
|
319
|
+
leaseFrom = t;
|
|
320
|
+
lastRefreshAt = heartbeatAt;
|
|
321
|
+
needsVerify = false;
|
|
322
|
+
onBeat();
|
|
323
|
+
} else if (result.status === 'superseded') {
|
|
324
|
+
halt('superseded');
|
|
325
|
+
onSuperseded(result.owner);
|
|
326
|
+
} else if (result.status === 'busy') {
|
|
327
|
+
log('owner heartbeat skipped: claim lock busy');
|
|
328
|
+
}
|
|
329
|
+
return result.status;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function tick() {
|
|
333
|
+
if (state !== 'active') return;
|
|
334
|
+
const t = monoNow();
|
|
335
|
+
noteTurn(t);
|
|
336
|
+
if (leaseExpired(t)) fence(t);
|
|
337
|
+
else if (needsVerify ? t - lastBeatAt >= tickMs : t - lastBeatAt >= heartbeatMs) beat();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
start() {
|
|
342
|
+
if (timer || state !== 'active') return;
|
|
343
|
+
timer = setInterval(tick, tickMs);
|
|
344
|
+
timer.unref?.();
|
|
345
|
+
},
|
|
346
|
+
/** Gate for every register/call: true while the lease is held and verified. */
|
|
347
|
+
admit() {
|
|
348
|
+
if (state === 'fenced' || state === 'superseded') return false;
|
|
349
|
+
const t = monoNow();
|
|
350
|
+
// Shutting down: no ticks run to measure stalls; the lease still bounds it.
|
|
351
|
+
if (state === 'stopped') return !leaseExpired(t);
|
|
352
|
+
noteTurn(t);
|
|
353
|
+
if (leaseExpired(t)) {
|
|
354
|
+
fence(t);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
// Re-verified at most once per tick: a busy claim lock must not block
|
|
358
|
+
// the loop on every request.
|
|
359
|
+
if (needsVerify && t - lastBeatAt >= tickMs) beat();
|
|
360
|
+
return state === 'active' && !needsVerify;
|
|
361
|
+
},
|
|
362
|
+
beat,
|
|
363
|
+
tick,
|
|
364
|
+
/** Stop beating (shutdown). admit() keeps refusing once the lease runs out. */
|
|
365
|
+
stop() {
|
|
366
|
+
if (state === 'active') halt('stopped');
|
|
367
|
+
},
|
|
368
|
+
get state() {
|
|
369
|
+
return state;
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
@@ -37,7 +37,10 @@ export function createStoredSessionViews({ desktopRuntime, dataDir }) {
|
|
|
37
37
|
const { createRuntimeReviewApi } = await import('../session-runtime/runtime-review-api.mjs');
|
|
38
38
|
const review = createRuntimeReviewApi({ getCwd: () => metadata.cwd || '', getSessionId: () => sessionId });
|
|
39
39
|
// The runtime review API answers `known`/`summary` itself (turnReviewReply).
|
|
40
|
-
|
|
40
|
+
if (action === 'getSessionReviewDiff') return review.getSessionReviewDiff();
|
|
41
|
+
// A cold session runs no turn: its change record is final.
|
|
42
|
+
if (action === 'getTurnChanges') return review.getTurnChanges(args[0]);
|
|
43
|
+
return review.getTurnReviewDiff(args[0]);
|
|
41
44
|
},
|
|
42
45
|
forgetStoredSession: async (sessionId) => {
|
|
43
46
|
const store = await desktopRuntime.loadSessionStore();
|