mixdog 1.4.1 → 1.4.2
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/package.json +3 -2
- package/scripts/build-shipped-history.mjs +115 -33
- package/scripts/gate-local.mjs +172 -61
- package/scripts/lib/gate-scheduler.mjs +138 -32
- package/scripts/lib/gate-timing-reporter.mjs +16 -0
- package/scripts/lib/run-node-tests.mjs +54 -29
- package/scripts/lib/scoped-temp.mjs +1 -0
- package/scripts/lib/test-failure-records.mjs +14 -1
- package/scripts/lib/test-failure-summary.mjs +16 -4
- package/scripts/lib/test-spawn-ledger.mjs +12 -3
- package/scripts/pptx-kit-index.mjs +5 -6
- package/scripts/suite-health.mjs +110 -28
- package/scripts/test.mjs +9 -14
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/explore/AGENT.md +22 -0
- package/src/agents/explore/agent.json +6 -0
- package/src/cli.mjs +7 -0
- package/src/defaults/agents.json +7 -0
- package/src/defaults/shipped-history.json +18 -2
- package/src/defaults/skills/browser-use/SKILL.md +4 -1
- package/src/defaults/skills/code-tidy/SKILL.md +71 -102
- package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
- package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
- package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
- package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
- package/src/defaults/skills/computer-use/SKILL.md +12 -2
- package/src/defaults/skills/goal-management/SKILL.md +1 -1
- package/src/defaults/skills/setup/references/actions.md +12 -6
- package/src/defaults/skills/setup/references/surfaces.md +2 -2
- package/src/defaults/skills/setup-guide/SKILL.md +5 -2
- package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
- package/src/defaults/skills/skill-creator/SKILL.md +85 -190
- package/src/defaults/skills/skill-creator/references/design.md +70 -162
- package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
- package/src/defaults/skills/test-suite/SKILL.md +194 -0
- package/src/defaults/skills/test-suite/references/case-management.md +51 -0
- package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
- package/src/defaults/skills/test-suite/references/reliability.md +49 -0
- package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
- package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
- package/src/defaults/skills/visualize/SKILL.md +6 -2
- package/src/lib/rules-builder.cjs +6 -30
- package/src/rules/lead/LEAD.md +3 -3
- package/src/rules/shared/20-research.md +3 -6
- package/src/rules/shared/40-editing.md +1 -0
- package/src/rules/shared/60-verification.md +10 -9
- package/src/rules/shared/70-delivery.md +4 -3
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
- package/src/runtime/agent/orchestrator/config.mjs +12 -7
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
- package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
- package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
- package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
- package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
- package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/channels/lib/whisper-port.mjs +19 -10
- package/src/runtime/channels/lib/whisper-server.mjs +10 -1
- package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
- package/src/runtime/connectors/gmail-app-password.mjs +3 -3
- package/src/runtime/explore/explorer.mjs +93 -0
- package/src/runtime/explore/tool-defs.mjs +35 -0
- package/src/runtime/explore/tool.mjs +123 -0
- package/src/runtime/memory/index.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
- package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
- package/src/runtime/shared/agent-route-config.mjs +17 -2
- package/src/runtime/shared/atomic-file.mjs +103 -2
- package/src/runtime/shared/background-tasks.mjs +15 -6
- package/src/runtime/shared/connector-accounts.mjs +5 -5
- package/src/runtime/shared/connector-catalog.d.mts +11 -4
- package/src/runtime/shared/connector-catalog.mjs +60 -52
- package/src/runtime/shared/custom-provider-keys.mjs +20 -0
- package/src/runtime/shared/file-has-data.mjs +26 -2
- package/src/runtime/shared/file-lock.mjs +13 -15
- package/src/runtime/shared/git-launch.d.mts +9 -0
- package/src/runtime/shared/git-launch.mjs +213 -0
- package/src/runtime/shared/git-runner-worker.mjs +2 -1
- package/src/runtime/shared/llm/cost.mjs +6 -1
- package/src/runtime/shared/llm/http-agent.mjs +313 -29
- package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
- package/src/runtime/shared/llm/model-catalog.mjs +46 -6
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
- package/src/runtime/shared/llm/system-proxy.mjs +452 -0
- package/src/runtime/shared/provider-accounts.mjs +52 -2
- package/src/runtime/shared/staged-install-worker.mjs +5 -0
- package/src/runtime/shared/staged-update.mjs +47 -2
- package/src/runtime/shared/tool-surface.mjs +110 -36
- package/src/runtime/shared/tool-work-units.mjs +4 -0
- package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
- package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
- package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
- package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
- package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
- package/src/runtime/shared/worktrees/git.mjs +57 -0
- package/src/runtime/shared/worktrees/manager.mjs +205 -137
- package/src/runtime/tidy/install.mjs +95 -15
- package/src/runtime/tidy/languages.mjs +17 -15
- package/src/runtime/tidy/process.mjs +7 -4
- package/src/runtime/tidy/resolve.mjs +19 -16
- package/src/runtime/tidy/runners/biome.mjs +3 -3
- package/src/runtime/tidy/tool.mjs +1 -1
- package/src/runtime/tidy/worktree.mjs +10 -6
- package/src/runtime/visualize-bridge/client.mjs +4 -1
- package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
- package/src/runtime/web-search/index.mjs +1 -1
- package/src/session-runtime/agent-task-sessions.mjs +410 -353
- package/src/session-runtime/boot/tools.mjs +2 -0
- package/src/session-runtime/bot-status.mjs +103 -103
- package/src/session-runtime/connector-merge-migration.mjs +18 -15
- package/src/session-runtime/conversation-import/discovery.mjs +159 -40
- package/src/session-runtime/conversation-import/service.mjs +9 -3
- package/src/session-runtime/handoff-attachments.mjs +1 -1
- package/src/session-runtime/held-requests.mjs +3 -1
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
- package/src/session-runtime/lifecycle/teardown.mjs +1 -1
- package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
- package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
- package/src/session-runtime/mixy-heartbeat.mjs +1 -1
- package/src/session-runtime/provider-auth/login.mjs +2 -2
- package/src/session-runtime/resource-mcp-api.mjs +98 -35
- package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
- package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
- package/src/session-runtime/resource-mcp-servers.mjs +5 -4
- package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
- package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
- package/src/session-runtime/services/daemon-candidate.mjs +4 -1
- package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
- package/src/session-runtime/services/plugin-admin.mjs +10 -6
- package/src/session-runtime/services/provider-admin.mjs +281 -10
- package/src/session-runtime/session-turn-api.mjs +2 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
- package/src/session-runtime/settings-system-api.mjs +1 -1
- package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
- package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
- package/src/session-runtime/setup-tool/executor.mjs +6 -2
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
- package/src/session-runtime/turn/agent-ops.mjs +23 -0
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
- package/src/session-runtime/workspace/activity-guards.mjs +176 -0
- package/src/session-runtime/workspace/session-view.mjs +149 -0
- package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
- package/src/session-runtime/workspace/transition-runner.mjs +188 -0
- package/src/session-runtime/workspace/transitions.mjs +108 -1066
- package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
- package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
- package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
- package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
- package/src/standalone/daemon-desktop-runtime.mjs +2 -0
- package/src/standalone/daemon-session-control.mjs +151 -136
- package/src/standalone/daemon.mjs +7 -0
- package/src/standalone/desktop-service-registry.mjs +9 -0
- package/src/standalone/local-session-runtime.mjs +5 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
- package/src/standalone/session-client.mjs +5 -1
- package/src/standalone/session-frame-stream.mjs +83 -11
- package/src/standalone/session-protocol.mjs +4 -0
- package/src/standalone/session-service/agent-tree.mjs +45 -9
- package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
- package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
- package/src/standalone/session-service/stored-reader.mjs +127 -40
- package/src/tui/App.jsx +154 -164
- package/src/tui/app/app-view/floating-panel.jsx +3 -0
- package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
- package/src/tui/app/clipboard.mjs +21 -10
- package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
- package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
- package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
- package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
- package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
- package/src/tui/app/provider-setup-picker.mjs +20 -13
- package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
- package/src/tui/app/slash-dispatch.mjs +1 -1
- package/src/tui/app/use-btw-panel.mjs +29 -0
- package/src/tui/app/use-context-panel-refresh.mjs +31 -0
- package/src/tui/app/use-model-catalog-cache.mjs +13 -10
- package/src/tui/app/use-mouse-input.mjs +11 -0
- package/src/tui/app/use-onboarding-start.mjs +8 -3
- package/src/tui/app/use-panel-transition.mjs +10 -1
- package/src/tui/app/use-pasted-buffers.mjs +28 -20
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
- package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
- package/src/tui/app/use-prompt-queue-history.mjs +3 -2
- package/src/tui/app/use-selection-focus.mjs +11 -2
- package/src/tui/app/use-selection-geometry.mjs +18 -9
- package/src/tui/app/use-selection-paint.mjs +4 -4
- package/src/tui/app/use-selection-stitch.mjs +3 -3
- package/src/tui/app/use-slash-palette.mjs +62 -0
- package/src/tui/app/use-terminal-chrome.mjs +2 -2
- package/src/tui/app/use-transcript-activity.mjs +47 -32
- package/src/tui/app/use-transcript-follow.mjs +12 -4
- package/src/tui/app/use-transcript-scroll.mjs +46 -17
- package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
- package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
- package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
- package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
- package/src/tui/app/use-transcript-window.mjs +21 -11
- package/src/tui/app/use-tui-ready.mjs +18 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
- package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
- package/src/tui/components/Picker.jsx +27 -3
- package/src/tui/components/SignInUrl.jsx +68 -0
- package/src/tui/components/TextEntryPanel.jsx +76 -46
- package/src/tui/components/sign-in-url-layout.mjs +24 -0
- package/src/tui/dist/index.mjs +1896 -1341
- package/src/tui/session/goal-turn-state.mjs +6 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +8 -0
- package/src/tui/session/session-api/intake/abort.mjs +2 -1
- package/src/tui/session/session-api/intake.mjs +2 -0
- package/src/tui/session/session-api/integrations.mjs +2 -2
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/session-api/settings.mjs +2 -2
- package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
- package/src/tui/session/transcript-spill.mjs +92 -4
- package/src/tui/session/transcript-store.mjs +2 -0
- package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
|
@@ -1,334 +1,262 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Workspace admission: the turn, command and management leases of a Git
|
|
3
|
+
* repository (activity-ledger.mjs keeps their ledger). One admission thread
|
|
4
|
+
* per process (activity-ledger-worker.mjs) owns every lease the process holds
|
|
5
|
+
* and runs the ledger's locked read-modify-publish, one request at a time in
|
|
6
|
+
* the order the requests were made. The fsynced writes therefore never block
|
|
7
|
+
* this thread's event loop, and no two threads of the process ever update the
|
|
8
|
+
* ledger at once.
|
|
9
|
+
*
|
|
10
|
+
* The asynchronous API posts its request and settles with the outcome. The
|
|
11
|
+
* synchronous API posts the same request and blocks (Atomics.wait) until it
|
|
12
|
+
* is answered, for at most MIXDOG_ADMISSION_SYNC_TIMEOUT_MS: a request still
|
|
13
|
+
* running then is reported with an unknown outcome — a grant is withdrawn by
|
|
14
|
+
* the admission thread once it finishes, a release still lands. A grant or
|
|
15
|
+
* release is answered only once both copies hold its outcome (or it was
|
|
16
|
+
* refused). If the admission thread stops, every outstanding and later
|
|
17
|
+
* request fails (workspace-admission-unreadable) until the process restarts:
|
|
18
|
+
* it is never restarted, since a ledger lock it held still names this live
|
|
19
|
+
* process. The exit waits (bounded the same way) for the requests already
|
|
20
|
+
* posted, so a release made just before exiting still lands.
|
|
21
|
+
*/
|
|
3
22
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { findGitLayout } from './git-head.mjs';
|
|
8
|
-
import { canonicalPath, isWithin, samePath } from './paths.mjs';
|
|
9
|
-
import { worktreeError, worktreeRefusal } from './contract.mjs';
|
|
10
|
-
import { readRegistrySync } from './registry.mjs';
|
|
23
|
+
import { MessageChannel, receiveMessageOnPort, SHARE_ENV, Worker } from 'node:worker_threads';
|
|
24
|
+
import { WorktreeError, worktreeRefusal } from './contract.mjs';
|
|
25
|
+
import { envMs, LEDGER_SLOTS as SLOT } from './activity-ledger.mjs';
|
|
11
26
|
|
|
12
|
-
|
|
13
|
-
const overlaps = (a, b) => isWithin(a, b) || isWithin(b, a);
|
|
27
|
+
export { isExternalCheckout } from './activity-ledger.mjs';
|
|
14
28
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* recovered empty) still refuses its readers.
|
|
23
|
-
*/
|
|
24
|
-
export function isExternalCheckout(folder) {
|
|
25
|
-
const layout = findGitLayout(folder, { strict: true });
|
|
26
|
-
if (!layout || samePath(layout.gitDir, layout.commonDir)) return false;
|
|
27
|
-
let entries;
|
|
28
|
-
try {
|
|
29
|
-
entries = readRegistrySync().entries;
|
|
30
|
-
} catch {
|
|
31
|
-
// A damaged registry is kept aside and reads as a restored or empty one;
|
|
32
|
-
// only a real I/O failure (access denied, in use) lands here.
|
|
33
|
-
throw worktreeRefusal('workspace-admission-unreadable');
|
|
34
|
-
}
|
|
35
|
-
return !Object.values(entries).some(
|
|
36
|
-
(record) => typeof record?.path === 'string' && samePath(record.path, layout.topLevel)
|
|
37
|
-
);
|
|
38
|
-
}
|
|
29
|
+
// The longest a synchronous caller (or the exit) waits for the admission
|
|
30
|
+
// thread, the requests queued ahead of it included: each is bounded by the
|
|
31
|
+
// ledger lock timeout (file-lock.mjs, MIXDOG_LOCK_TIMEOUT_MS) and the atomic
|
|
32
|
+
// writes' rename retries. A chosen ceiling, not a measurement: reaching it is
|
|
33
|
+
// the typed `outcome: 'unknown'` refusal, and MIXDOG_ADMISSION_SYNC_TIMEOUT_MS
|
|
34
|
+
// sets it.
|
|
35
|
+
const syncTimeoutMs = () => envMs('MIXDOG_ADMISSION_SYNC_TIMEOUT_MS', 60_000);
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* that may be missing leases because no complete copy survived: readers still
|
|
48
|
-
* run, management refuses, and only a quiescent reset that removes both
|
|
49
|
-
* copies clears it (docs/git-github-integration.md). Time, healing and
|
|
50
|
-
* releases never do.
|
|
51
|
-
*/
|
|
52
|
-
const VERSION = 2;
|
|
53
|
-
const isLease = (lease) =>
|
|
54
|
-
!!lease &&
|
|
55
|
-
['turn', 'command', 'management'].includes(lease.kind) &&
|
|
56
|
-
typeof lease.token === 'string' &&
|
|
57
|
-
Number.isSafeInteger(lease.pid) &&
|
|
58
|
-
lease.pid > 0 &&
|
|
59
|
-
typeof lease.path === 'string' &&
|
|
60
|
-
isAbsolute(lease.path);
|
|
61
|
-
// Only a pid PROVEN dead (ESRCH) drops its lease, whatever its kind; an
|
|
62
|
-
// unprobeable pid still counts as running. A live runtime keeps a command
|
|
63
|
-
// whose tree it cannot verify reserved under its own (live) pid until the
|
|
64
|
-
// tree is confirmed gone, so a lease that outlives every pid belongs to a
|
|
65
|
-
// restarted runtime that can no longer release it.
|
|
66
|
-
const aliveLease = (lease) => isPidAliveOrUnknown(lease.pid);
|
|
67
|
-
// A management hold shares one token across its folders, so a lease is
|
|
68
|
-
// identified by token and path.
|
|
69
|
-
const leaseKey = (lease) => `${lease.token}\0${lease.path}`;
|
|
70
|
-
const unverifiable = () => worktreeRefusal('workspace-admission-unreadable');
|
|
71
|
-
const copyPaths = (commonDir) => {
|
|
72
|
-
const primary = join(commonDir, FILE);
|
|
73
|
-
return [primary, `${primary}.management`];
|
|
74
|
-
};
|
|
75
|
-
const envMs = (name, fallback) => {
|
|
76
|
-
const value = Number(process.env[name]);
|
|
77
|
-
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
78
|
-
};
|
|
79
|
-
const healIntervalMs = () => envMs('MIXDOG_ADMISSION_HEAL_MS', 2_000);
|
|
37
|
+
// `outcome: 'unknown'`: the request was posted and may have landed.
|
|
38
|
+
const unknownOutcome = (details = {}) =>
|
|
39
|
+
worktreeRefusal('workspace-admission-unreadable', { ...details, outcome: 'unknown' });
|
|
40
|
+
const stoppedError = (posted) =>
|
|
41
|
+
posted
|
|
42
|
+
? unknownOutcome({ admission: 'stopped' })
|
|
43
|
+
: worktreeRefusal('workspace-admission-unreadable', { admission: 'stopped' });
|
|
80
44
|
|
|
81
|
-
|
|
82
|
-
// the tokens it released or failed to grant whose removal from both copies is
|
|
83
|
-
// not confirmed yet (token -> common dir), retried until it is.
|
|
84
|
-
const held = new Map();
|
|
85
|
-
const pending = new Map();
|
|
86
|
-
let healTimer = null;
|
|
45
|
+
let gate = null;
|
|
87
46
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* valid records still count. A copy is complete when it accounts for every
|
|
94
|
-
* acknowledged lease: intact, not uncertain, and version 2 — or the version 1
|
|
95
|
-
* primary, since the version 1 mirror held management leases only.
|
|
96
|
-
*/
|
|
97
|
-
function readCopy(path, primary) {
|
|
98
|
-
let bytes;
|
|
47
|
+
function startGate() {
|
|
48
|
+
if (gate) return gate;
|
|
49
|
+
const slots = new Int32Array(new SharedArrayBuffer(SLOT.COUNT * Int32Array.BYTES_PER_ELEMENT));
|
|
50
|
+
const { port1, port2 } = new MessageChannel();
|
|
51
|
+
let worker;
|
|
99
52
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
53
|
+
worker = new Worker(new URL('./activity-ledger-worker.mjs', import.meta.url), {
|
|
54
|
+
env: SHARE_ENV,
|
|
55
|
+
execArgv: [],
|
|
56
|
+
workerData: {
|
|
57
|
+
slots: slots.buffer,
|
|
58
|
+
syncPort: port2,
|
|
59
|
+
// Tests only: load the fixed fault hook beside the worker (never shipped).
|
|
60
|
+
faultHook: process.env.MIXDOG_ADMISSION_TEST_FAULT_HOOK === '1',
|
|
61
|
+
},
|
|
62
|
+
transferList: [port2],
|
|
63
|
+
});
|
|
64
|
+
} catch (cause) {
|
|
65
|
+
// Nothing ran: the next request tries again.
|
|
66
|
+
throw Object.assign(worktreeRefusal('workspace-admission-unreadable', { admission: 'unavailable' }), { cause });
|
|
104
67
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const leases = ledger.leases.filter(isLease);
|
|
116
|
-
const damaged = leases.length !== ledger.leases.length;
|
|
117
|
-
const uncertain = Object.hasOwn(ledger, 'recoveredAt');
|
|
118
|
-
const complete = !damaged && !uncertain && (ledger.version === VERSION || primary);
|
|
119
|
-
return { path, bytes, leases, damaged, uncertain, complete, recoveredAt: ledger.recoveredAt };
|
|
68
|
+
const current = { worker, slots, syncPort: port1, lastId: 0, posted: 0, waiting: new Map(), stopped: null };
|
|
69
|
+
gate = current;
|
|
70
|
+
worker.on('message', (answer) => settleAnswer(current, answer));
|
|
71
|
+
worker.on('error', (error) => stopGate(current, error));
|
|
72
|
+
worker.on('exit', (code) => stopGate(current, new Error(`exit code ${code}`)));
|
|
73
|
+
// Kept alive by outstanding asynchronous requests only (request); after the
|
|
74
|
+
// listeners, since attaching a 'message' listener refs the worker again.
|
|
75
|
+
worker.unref();
|
|
76
|
+
process.on('exit', () => drainOnExit(current));
|
|
77
|
+
return current;
|
|
120
78
|
}
|
|
121
79
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
const leases = new Map();
|
|
132
|
-
for (const lease of [...copies.flatMap((copy) => copy.leases), ...own]) {
|
|
133
|
-
const key = leaseKey(lease);
|
|
134
|
-
if (!leases.has(key) && pending.get(lease.token) !== commonDir && aliveLease(lease)) leases.set(key, lease);
|
|
135
|
-
}
|
|
136
|
-
const ledger = { version: VERSION, leases: [...leases.values()] };
|
|
137
|
-
const uncertain =
|
|
138
|
-
copies.some((copy) => copy.uncertain) ||
|
|
139
|
-
(copies.some((copy) => copy.bytes) && !copies.some((copy) => copy.complete));
|
|
140
|
-
if (uncertain) ledger.recoveredAt = copies.map((copy) => copy.recoveredAt).find(Number.isFinite) ?? Date.now();
|
|
141
|
-
return ledger;
|
|
80
|
+
function stopGate(current, cause) {
|
|
81
|
+
if (current.stopped) return;
|
|
82
|
+
current.stopped = cause;
|
|
83
|
+
process.stderr.write(
|
|
84
|
+
`[state] the workspace admission thread stopped (${cause?.message ?? cause}); workspace activity is refused until restart\n`
|
|
85
|
+
);
|
|
86
|
+
const waiting = [...current.waiting.values()];
|
|
87
|
+
current.waiting.clear();
|
|
88
|
+
for (const entry of waiting) entry.reject(stoppedError(true));
|
|
142
89
|
}
|
|
143
90
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
91
|
+
function revive(error) {
|
|
92
|
+
if (error.name === 'WorktreeError') return new WorktreeError(error.code, error.message, error.details);
|
|
93
|
+
const revived = new ({ TypeError, RangeError }[error.name] ?? Error)(error.message);
|
|
94
|
+
if (error.code !== undefined) revived.code = error.code;
|
|
95
|
+
return revived;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function post(current, op, args, sync) {
|
|
99
|
+
current.lastId = current.lastId >= 0x3fffffff ? 1 : current.lastId + 1;
|
|
100
|
+
const id = current.lastId;
|
|
101
|
+
if (sync) Atomics.store(current.slots, SLOT.SYNC, id);
|
|
102
|
+
current.worker.postMessage({ id, op, args, sync });
|
|
103
|
+
current.posted = (current.posted + 1) | 0;
|
|
104
|
+
return id;
|
|
159
105
|
}
|
|
160
106
|
|
|
161
|
-
/**
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
copy.bytes = text;
|
|
107
|
+
/** Posts the request at the call, so it runs after every earlier request of this process. */
|
|
108
|
+
function request(op, args) {
|
|
109
|
+
let current;
|
|
110
|
+
try {
|
|
111
|
+
current = startGate();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return Promise.reject(error);
|
|
169
114
|
}
|
|
115
|
+
if (current.stopped) return Promise.reject(stoppedError(false));
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const id = post(current, op, args, false);
|
|
118
|
+
current.waiting.set(id, { resolve, reject });
|
|
119
|
+
if (current.waiting.size === 1) current.worker.ref();
|
|
120
|
+
});
|
|
170
121
|
}
|
|
171
122
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
123
|
+
function settleAnswer(current, { id, value, error }) {
|
|
124
|
+
const entry = current.waiting.get(id);
|
|
125
|
+
if (!entry) return;
|
|
126
|
+
current.waiting.delete(id);
|
|
127
|
+
if (!current.waiting.size) current.worker.unref();
|
|
128
|
+
if (error) entry.reject(revive(error));
|
|
129
|
+
else entry.resolve(value);
|
|
175
130
|
}
|
|
176
131
|
|
|
177
132
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* refused: its leases become pending removals and the ledger without them is
|
|
183
|
-
* published again; whatever that leaves behind is withdrawn by the next
|
|
184
|
-
* mutation or heal tick, which also blocks every later grant until it lands.
|
|
133
|
+
* The request answered while this thread blocks. The admission thread never
|
|
134
|
+
* waits for this one, so the wait ends with its answer, its stop, or the
|
|
135
|
+
* deadline — after which the request no longer counts as this caller's: the
|
|
136
|
+
* admission thread withdraws a grant it then completes (activity-ledger-worker).
|
|
185
137
|
*/
|
|
186
|
-
function
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
settle(withdrawn);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
process.stderr.write(`[state] ${paths[0]} could not be published (${error?.message})\n`);
|
|
209
|
-
if (refusal) throw refusal;
|
|
210
|
-
for (const lease of granted) pending.set(lease.token, commonDir);
|
|
211
|
-
if (granted.length) {
|
|
212
|
-
try {
|
|
213
|
-
publish(copies, base);
|
|
214
|
-
settle([...withdrawn, ...granted.map((lease) => lease.token)]);
|
|
215
|
-
} catch {
|
|
216
|
-
syncHealTimer();
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
throw unverifiable();
|
|
220
|
-
}
|
|
221
|
-
if (refusal) throw refusal;
|
|
222
|
-
return result;
|
|
223
|
-
},
|
|
224
|
-
{ strictOwner: true }
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Without the lock, a cheap check that both copies are the same current
|
|
229
|
-
// ledger holding every live lease this process holds there and none of the
|
|
230
|
-
// tokens it is withdrawing. Repair, never proof that the ledger is complete.
|
|
231
|
-
function inSync(commonDir) {
|
|
232
|
-
let ledger;
|
|
233
|
-
try {
|
|
234
|
-
const [primary, mirror] = copyPaths(commonDir).map((path) => readFileSync(path));
|
|
235
|
-
if (!primary.equals(mirror)) return false;
|
|
236
|
-
ledger = parseJsonText(primary.toString('utf8'));
|
|
237
|
-
} catch {
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
if (ledger?.version !== VERSION || !Array.isArray(ledger.leases)) return false;
|
|
241
|
-
const stored = new Set(ledger.leases.filter(isLease).map(leaseKey));
|
|
242
|
-
return (
|
|
243
|
-
[...held].every(([key, entry]) => entry.commonDir !== commonDir || stored.has(key) || !aliveLease(entry.lease)) &&
|
|
244
|
-
!ledger.leases.some((lease) => pending.get(lease?.token) === commonDir)
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function healHeld() {
|
|
249
|
-
const dirs = new Set([...[...held.values()].map((entry) => entry.commonDir), ...pending.values()]);
|
|
250
|
-
for (const commonDir of dirs) {
|
|
251
|
-
if (inSync(commonDir)) continue;
|
|
252
|
-
try {
|
|
253
|
-
mutate(commonDir);
|
|
254
|
-
} catch {
|
|
255
|
-
// Retried on the next tick.
|
|
138
|
+
function callSync(op, args) {
|
|
139
|
+
const current = startGate();
|
|
140
|
+
if (current.stopped) throw stoppedError(false);
|
|
141
|
+
const { slots } = current;
|
|
142
|
+
const id = post(current, op, args, true);
|
|
143
|
+
const deadline = Date.now() + syncTimeoutMs();
|
|
144
|
+
for (;;) {
|
|
145
|
+
const seen = Atomics.load(slots, SLOT.ANSWERED);
|
|
146
|
+
for (
|
|
147
|
+
let received = receiveMessageOnPort(current.syncPort);
|
|
148
|
+
received;
|
|
149
|
+
received = receiveMessageOnPort(current.syncPort)
|
|
150
|
+
) {
|
|
151
|
+
if (received.message.id !== id) continue;
|
|
152
|
+
if (received.message.error) throw revive(received.message.error);
|
|
153
|
+
return received.message.value;
|
|
154
|
+
}
|
|
155
|
+
if (Atomics.load(slots, SLOT.STOPPED) === 1) {
|
|
156
|
+
stopGate(current, new Error('stopped while a synchronous request waited'));
|
|
157
|
+
throw stoppedError(true);
|
|
256
158
|
}
|
|
159
|
+
let left = deadline - Date.now();
|
|
160
|
+
if (left <= 0) {
|
|
161
|
+
if (Atomics.compareExchange(slots, SLOT.SYNC, id, 0) === id) throw unknownOutcome();
|
|
162
|
+
// The admission thread already claimed the slot (SYNC is -id): the
|
|
163
|
+
// answer is on its way and its notify wakes this wait; 1 s only bounds
|
|
164
|
+
// a missed wake.
|
|
165
|
+
left = 1_000;
|
|
166
|
+
}
|
|
167
|
+
Atomics.wait(slots, SLOT.ANSWERED, seen, left);
|
|
257
168
|
}
|
|
258
|
-
syncHealTimer();
|
|
259
169
|
}
|
|
260
170
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
171
|
+
// `exit` listeners run synchronously while the admission thread still runs:
|
|
172
|
+
// the requests already posted (a release made just before exiting) are let
|
|
173
|
+
// finish. Not a durability mechanism — every grant was answered only once it
|
|
174
|
+
// was written — and a forced termination skips it: a release then left
|
|
175
|
+
// behind belongs to a dead pid, which the next reader drops.
|
|
176
|
+
function drainOnExit(current) {
|
|
177
|
+
const { slots } = current;
|
|
178
|
+
const deadline = Date.now() + syncTimeoutMs();
|
|
179
|
+
for (;;) {
|
|
180
|
+
const done = Atomics.load(slots, SLOT.DONE);
|
|
181
|
+
if (done === current.posted || current.stopped || Atomics.load(slots, SLOT.STOPPED) === 1) return;
|
|
182
|
+
const left = deadline - Date.now();
|
|
183
|
+
if (left <= 0) return;
|
|
184
|
+
Atomics.wait(slots, SLOT.DONE, done, left);
|
|
268
185
|
}
|
|
269
186
|
}
|
|
270
187
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
188
|
+
const handled = (promise) => {
|
|
189
|
+
promise.catch(() => {});
|
|
190
|
+
return promise;
|
|
191
|
+
};
|
|
275
192
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
193
|
+
/**
|
|
194
|
+
* The release of a synchronous grant: returns once the removal was attempted
|
|
195
|
+
* (one that did not land is retried by the admission thread, as always).
|
|
196
|
+
* Throws only when the admission thread stopped.
|
|
197
|
+
*/
|
|
198
|
+
function syncRelease(leases) {
|
|
279
199
|
let done = false;
|
|
280
200
|
return () => {
|
|
281
|
-
if (done) return;
|
|
201
|
+
if (done || !leases.length) return;
|
|
282
202
|
done = true;
|
|
283
|
-
for (const [key, entry] of held) if (entry.lease.token === token) held.delete(key);
|
|
284
|
-
pending.set(token, commonDir);
|
|
285
203
|
try {
|
|
286
|
-
|
|
287
|
-
} catch {
|
|
288
|
-
|
|
204
|
+
callSync('release', { leases });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error?.details?.outcome !== 'unknown' || error.details.admission === 'stopped') throw error;
|
|
207
|
+
process.stderr.write('[state] a workspace release outlasted the synchronous wait; it lands in its turn\n');
|
|
289
208
|
}
|
|
290
|
-
syncHealTimer();
|
|
291
209
|
};
|
|
292
210
|
}
|
|
293
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The release of an asynchronous grant, requested at the call (so it lands
|
|
214
|
+
* before any later request of this process). Settles once the removal was
|
|
215
|
+
* attempted; rejects only when the admission thread stopped. Every call
|
|
216
|
+
* returns the same promise, which never goes unhandled.
|
|
217
|
+
*/
|
|
218
|
+
function asyncRelease(leases) {
|
|
219
|
+
let released = null;
|
|
220
|
+
return () => (released ??= leases.length ? handled(request('release', { leases })) : Promise.resolve());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readerArgs(sessionId, cwd, { pid = process.pid, kind = 'turn' } = {}) {
|
|
224
|
+
if (!['turn', 'command'].includes(kind)) throw new TypeError('Invalid workspace reader kind.');
|
|
225
|
+
return { sessionId, cwd, pid, kind };
|
|
226
|
+
}
|
|
227
|
+
|
|
294
228
|
/**
|
|
295
229
|
* Readers share a checkout freely. Null means a management writer currently
|
|
296
230
|
* owns it; a reader in an external checkout is admitted whatever is held
|
|
297
231
|
* outside that checkout, but refused by a management lease inside it. Throws
|
|
298
232
|
* `workspace-admission-unreadable` when the ledger cannot be read, was
|
|
299
233
|
* written by a newer build, or the grant could not be made durable in both
|
|
300
|
-
* copies: no work may run unrecorded.
|
|
234
|
+
* copies: no work may run unrecorded. Blocks until answered;
|
|
235
|
+
* tryAdmitWorkspaceActivityAsync does not.
|
|
301
236
|
*/
|
|
302
|
-
export function tryAdmitWorkspaceActivity(sessionId, cwd,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
// a registry recovered empty) published inside this very checkout still holds.
|
|
312
|
-
const checkout = canonicalPath(layout.topLevel);
|
|
313
|
-
const held = (other) => (external ? isWithin(checkout, other.path) : overlaps(other.path, path));
|
|
314
|
-
const admitted = mutate(layout.commonDir, (leases) => {
|
|
315
|
-
if (leases.some((other) => other.kind === 'management' && held(other))) return false;
|
|
316
|
-
leases.push(lease);
|
|
317
|
-
return true;
|
|
318
|
-
});
|
|
319
|
-
if (!admitted) return null;
|
|
320
|
-
track(layout.commonDir, lease);
|
|
321
|
-
return release(layout.commonDir, token);
|
|
237
|
+
export function tryAdmitWorkspaceActivity(sessionId, cwd, options) {
|
|
238
|
+
const granted = callSync('admit', readerArgs(sessionId, cwd, options));
|
|
239
|
+
return granted && syncRelease(granted.leases);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** tryAdmitWorkspaceActivity without blocking; its release settles once the removal was attempted. */
|
|
243
|
+
export async function tryAdmitWorkspaceActivityAsync(sessionId, cwd, options) {
|
|
244
|
+
const granted = await request('admit', readerArgs(sessionId, cwd, options));
|
|
245
|
+
return granted && asyncRelease(granted.leases);
|
|
322
246
|
}
|
|
323
247
|
|
|
248
|
+
/** Waits (polling) until a reader is admitted; an abort registers nothing. Its release is asynchronous. */
|
|
324
249
|
export async function admitWorkspaceActivity(sessionId, cwd, signal, options) {
|
|
325
250
|
for (;;) {
|
|
326
251
|
signal?.throwIfAborted();
|
|
327
|
-
const leave =
|
|
252
|
+
const leave = await tryAdmitWorkspaceActivityAsync(sessionId, cwd, options);
|
|
328
253
|
if (leave) {
|
|
329
254
|
if (signal?.aborted) {
|
|
330
|
-
|
|
331
|
-
|
|
255
|
+
try {
|
|
256
|
+
await leave();
|
|
257
|
+
} finally {
|
|
258
|
+
signal.throwIfAborted();
|
|
259
|
+
}
|
|
332
260
|
}
|
|
333
261
|
return leave;
|
|
334
262
|
}
|
|
@@ -341,46 +269,15 @@ export async function admitWorkspaceActivity(sessionId, cwd, signal, options) {
|
|
|
341
269
|
* exclude later readers until the returned release is called. External
|
|
342
270
|
* checkouts are only checked, never held. `sessionId`: the acting session,
|
|
343
271
|
* whose own readers in this process do not count (its idleness is the
|
|
344
|
-
* caller's check).
|
|
272
|
+
* caller's check). Blocks until answered; holdWorkspaceActivityAsync does not.
|
|
345
273
|
*/
|
|
346
274
|
export function holdWorkspaceActivity(folders, { sessionId = null } = {}) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
groups.set(layout.commonDir, paths);
|
|
354
|
-
}
|
|
355
|
-
const releases = [];
|
|
356
|
-
try {
|
|
357
|
-
for (const [commonDir, paths] of [...groups].sort(([a], [b]) => a.localeCompare(b))) {
|
|
358
|
-
const token = randomUUID();
|
|
359
|
-
const taken = [];
|
|
360
|
-
mutate(commonDir, (leases, { unknown }) => {
|
|
361
|
-
const conflicts = leases.filter(
|
|
362
|
-
(lease) =>
|
|
363
|
-
!(sessionId && lease.sessionId === sessionId && lease.pid === process.pid) &&
|
|
364
|
-
paths.some(({ path }) => overlaps(path, lease.path))
|
|
365
|
-
);
|
|
366
|
-
if (conflicts.length) throw worktreeRefusal('folder-in-use');
|
|
367
|
-
// The ledger may be missing a live foreign lease: refused until reset.
|
|
368
|
-
if (unknown) throw unverifiable();
|
|
369
|
-
for (const { path, external } of paths) {
|
|
370
|
-
if (external) continue;
|
|
371
|
-
const lease = { kind: 'management', pid: process.pid, token, path };
|
|
372
|
-
leases.push(lease);
|
|
373
|
-
taken.push(lease);
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
for (const lease of taken) track(commonDir, lease);
|
|
377
|
-
releases.push(release(commonDir, token));
|
|
378
|
-
}
|
|
379
|
-
} catch (error) {
|
|
380
|
-
releaseAll(releases);
|
|
381
|
-
throw error;
|
|
382
|
-
}
|
|
383
|
-
return () => releaseAll(releases);
|
|
275
|
+
return syncRelease(callSync('hold', { folders: [...folders], sessionId }).leases);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** holdWorkspaceActivity without blocking; its release settles once every removal was attempted. */
|
|
279
|
+
export async function holdWorkspaceActivityAsync(folders, { sessionId = null } = {}) {
|
|
280
|
+
return asyncRelease((await request('hold', { folders: [...folders], sessionId })).leases);
|
|
384
281
|
}
|
|
385
282
|
|
|
386
283
|
/** Runs every release in reverse order even when one throws; the first failure is rethrown after all ran. */
|