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
|
@@ -72,6 +72,27 @@ export function createAgentOps(deps) {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// `/stop`: the background shells this session would stop, then stopping all
|
|
76
|
+
// of them through the same per-task cancel.
|
|
77
|
+
async function listStoppableShellJobs() {
|
|
78
|
+
const session = getSession();
|
|
79
|
+
const { listStoppableShellJobs: list } = await import(
|
|
80
|
+
'../../runtime/agent/orchestrator/tools/builtin/task-tool.mjs'
|
|
81
|
+
);
|
|
82
|
+
return {
|
|
83
|
+
jobs: list({ callerSessionId: session?.id || null, clientHostPid: session?.clientHostPid || process.pid }),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function stopShellJobs() {
|
|
88
|
+
const session = getSession();
|
|
89
|
+
const { stopBackgroundShellJobs } = await import('../../runtime/agent/orchestrator/tools/builtin/task-tool.mjs');
|
|
90
|
+
return stopBackgroundShellJobs({
|
|
91
|
+
callerSessionId: session?.id || null,
|
|
92
|
+
clientHostPid: session?.clientHostPid || process.pid,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
// Explicit user force-release ("Release anyway") of a shell job whose
|
|
76
97
|
// termination is unconfirmed: drops its workspace hold. The surface has
|
|
77
98
|
// already asked the user to confirm. The caller session's own holds, and
|
|
@@ -147,6 +168,8 @@ export function createAgentOps(deps) {
|
|
|
147
168
|
agentControl,
|
|
148
169
|
taskControl,
|
|
149
170
|
stopShellJob,
|
|
171
|
+
listStoppableShellJobs,
|
|
172
|
+
stopShellJobs,
|
|
150
173
|
releaseShellJob,
|
|
151
174
|
listShellJobs,
|
|
152
175
|
onNotification,
|
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
assertConnectorPolicyNotWidened,
|
|
5
5
|
CONNECTOR_PERMISSIONS,
|
|
6
6
|
connectorEnabled,
|
|
7
|
+
connectorGroupId,
|
|
8
|
+
connectorToolEntries,
|
|
9
|
+
connectorToolPermission,
|
|
7
10
|
DENY_ALL_CONNECTORS,
|
|
8
11
|
migrateLegacyConnectorPolicy,
|
|
9
12
|
NO_WIDEN_MESSAGE,
|
|
@@ -108,8 +111,8 @@ export function createAgentConnectorsApi(deps, editor) {
|
|
|
108
111
|
return getAgentConnectors(agentId);
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
//
|
|
112
|
-
// `permission` null drops the tool's entry, so the tool is
|
|
114
|
+
// One tool's permission (the profile's tool list, getAgentConnectorTools):
|
|
115
|
+
// `permission` null drops the tool's entry, so the tool is on its default again.
|
|
113
116
|
async function setAgentConnectorToolPermission(agentId, connectorId, toolName, permission, options) {
|
|
114
117
|
if (permission != null && !CONNECTOR_PERMISSIONS.includes(permission)) {
|
|
115
118
|
throw new Error(`permission must be one of ${CONNECTOR_PERMISSIONS.join(', ')}`);
|
|
@@ -117,19 +120,25 @@ export function createAgentConnectorsApi(deps, editor) {
|
|
|
117
120
|
const server = requireId(connectorId, 'connector id');
|
|
118
121
|
const tool = requireId(toolName, 'tool name');
|
|
119
122
|
const policy = editablePolicy(agentId);
|
|
120
|
-
|
|
123
|
+
// The accounts of one connector (`gmail`, `gmail-2`…) share the permission,
|
|
124
|
+
// stored once under the connector's id; an entry an account kept of its own
|
|
125
|
+
// for this tool goes in the same write.
|
|
126
|
+
const group = connectorGroupId(server);
|
|
127
|
+
const tools = Object.fromEntries(
|
|
128
|
+
Object.entries(policy.tools).map(([id, entries]) => {
|
|
129
|
+
if (id === group || connectorGroupId(id) !== group) return [id, entries];
|
|
130
|
+
const { [tool]: _dropped, ...rest } = entries;
|
|
131
|
+
return [id, rest];
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
const own = { ...tools[group] };
|
|
121
135
|
if (permission == null) delete own[tool];
|
|
122
136
|
else own[tool] = permission;
|
|
137
|
+
tools[group] = own;
|
|
123
138
|
if (options?.noWiden) {
|
|
124
|
-
assertConnectorPolicyNotWidened(editor.getAgentConnectorPolicy(agentId), {
|
|
125
|
-
off: policy.off,
|
|
126
|
-
tools: { ...policy.tools, [server]: own },
|
|
127
|
-
});
|
|
139
|
+
assertConnectorPolicyNotWidened(editor.getAgentConnectorPolicy(agentId), { off: policy.off, tools });
|
|
128
140
|
}
|
|
129
|
-
return editor.setAgentConnectorPolicy(
|
|
130
|
-
agentId,
|
|
131
|
-
storedConnectorPolicy({ off: policy.off, tools: { ...policy.tools, [server]: own } })
|
|
132
|
-
);
|
|
141
|
+
return editor.setAgentConnectorPolicy(agentId, storedConnectorPolicy({ off: policy.off, tools }));
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
// A card's "항상 허용" for a connector tool saves through this same writer.
|
|
@@ -149,6 +158,34 @@ export function createAgentConnectorsApi(deps, editor) {
|
|
|
149
158
|
.sort((a, b) => a.connector.localeCompare(b.connector) || a.tool.localeCompare(b.tool));
|
|
150
159
|
}
|
|
151
160
|
|
|
161
|
+
/** `[{ connector, tool, permission, explicit }]`: each installed connector's
|
|
162
|
+
* tools (the connected server's own, and any the agent's policy names) with
|
|
163
|
+
* the permission the agent has for the tool itself, so a connector switched
|
|
164
|
+
* off does not change it, and whether that is an entry of its own policy
|
|
165
|
+
* rather than the tool's default. The profile's per-tool editor reads it. */
|
|
166
|
+
function getAgentConnectorTools(agentId) {
|
|
167
|
+
const { tools } = editablePolicy(agentId);
|
|
168
|
+
return installedIds().flatMap((connector) => {
|
|
169
|
+
const group = connectorGroupId(connector);
|
|
170
|
+
const accounts = Object.entries(tools).filter(([id]) => connectorGroupId(id) === group);
|
|
171
|
+
const names = new Set([
|
|
172
|
+
...listConnectorTools(connector).map(({ name }) => name),
|
|
173
|
+
...accounts.flatMap(([, entries]) => Object.keys(entries)),
|
|
174
|
+
]);
|
|
175
|
+
// Accounts share one permission (stored under the connector); entries an
|
|
176
|
+
// account kept of its own follow the topmost.
|
|
177
|
+
return [...names].map((tool) => ({
|
|
178
|
+
connector,
|
|
179
|
+
tool,
|
|
180
|
+
permission: connectorToolPermission(
|
|
181
|
+
{ off: [], tools },
|
|
182
|
+
{ mcpServerName: connector, mcpToolName: tool }
|
|
183
|
+
),
|
|
184
|
+
explicit: connectorToolEntries(tools, group, tool).length > 0,
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
152
189
|
/** Replace the agent's connector policy (null removes it). With `options.noWiden`
|
|
153
190
|
* the replacement may not give any tool more authority than the stored policy. */
|
|
154
191
|
async function setAgentConnectors(agentId, connectors, options) {
|
|
@@ -279,6 +316,7 @@ export function createAgentConnectorsApi(deps, editor) {
|
|
|
279
316
|
setAgentConnectorEnabled,
|
|
280
317
|
setAgentConnectorToolPermission,
|
|
281
318
|
getAgentAlwaysAllowedTools,
|
|
319
|
+
getAgentConnectorTools,
|
|
282
320
|
getProjectConnectors,
|
|
283
321
|
setProjectConnectorEnabled,
|
|
284
322
|
migrateConnectorPolicies,
|
|
@@ -7,6 +7,7 @@ import { ensureProviderEnabled } from '../../runtime/agent/orchestrator/runtime-
|
|
|
7
7
|
import { resolveRouteContextState, selectionFastCapable } from '../route-state.mjs';
|
|
8
8
|
import {
|
|
9
9
|
canonicalizeAgentRouteStorage,
|
|
10
|
+
EXPLORE_AGENT_ID,
|
|
10
11
|
isAgentDisabled,
|
|
11
12
|
withAgentDisabled,
|
|
12
13
|
} from '../../runtime/shared/agent-route-config.mjs';
|
|
@@ -226,5 +227,13 @@ export function createAgentRouteApi(deps) {
|
|
|
226
227
|
return stayOff ? { ...routeToSave, id, disabled: true } : routeToSave;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
|
|
230
|
+
// Explore's route decides whether the explore tool exists, so the empty
|
|
231
|
+
// session rebuilds its tool surface on any change of it.
|
|
232
|
+
async function setAgentRouteAndRefresh(agentId, next) {
|
|
233
|
+
const saved = await setAgentRoute(agentId, next);
|
|
234
|
+
if (agentEditorId(agentId) === EXPLORE_AGENT_ID) await deps.refreshEmptySessionToolPolicy?.();
|
|
235
|
+
return saved;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { setAgentRoute: setAgentRouteAndRefresh };
|
|
230
239
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The activity checks every workspace change runs first: whether the hosted
|
|
3
|
+
* Lead (its turn, workers and background shells) is idle, and whether other
|
|
4
|
+
* sessions work in a shared checkout — then holding that checkout so new
|
|
5
|
+
* turns there wait until the change settles (admission gate).
|
|
6
|
+
*/
|
|
7
|
+
import { worktreeError, worktreeRefusal } from '../../runtime/shared/worktrees/contract.mjs';
|
|
8
|
+
import { isWithin } from '../../runtime/shared/worktrees/paths.mjs';
|
|
9
|
+
import {
|
|
10
|
+
holdWorkspaceActivity,
|
|
11
|
+
isExternalCheckout,
|
|
12
|
+
releaseAll,
|
|
13
|
+
} from '../../runtime/shared/worktrees/activity-gate.mjs';
|
|
14
|
+
import { AGENT_OWNER } from '../../runtime/agent/orchestrator/agent-owner.mjs';
|
|
15
|
+
import { holdCheckoutFolder } from './admission-gate.mjs';
|
|
16
|
+
|
|
17
|
+
const ACTIVE_STAGES = new Set(['connecting', 'requesting', 'streaming', 'tool_running', 'running', 'cancelling']);
|
|
18
|
+
const BUSY_WORKER_STATES = new Set([...ACTIVE_STAGES, 'starting', 'spawning', 'queued', 'pending']);
|
|
19
|
+
const BUSY_JOB_STATES = new Set(['starting', 'spawning', 'queued', 'pending', 'running']);
|
|
20
|
+
|
|
21
|
+
const lower = (value) => String(value || '').toLowerCase();
|
|
22
|
+
|
|
23
|
+
/** @param {object} deps The activity sources of createWorkspaceTransitions. */
|
|
24
|
+
export function createActivityGuards({
|
|
25
|
+
getActiveTurnCount,
|
|
26
|
+
agentStatusState,
|
|
27
|
+
countOwnShellJobs,
|
|
28
|
+
listLiveSessions,
|
|
29
|
+
sessionManager,
|
|
30
|
+
listWorkspaceConsumers,
|
|
31
|
+
listWorkspaceJobs,
|
|
32
|
+
}) {
|
|
33
|
+
function isSessionActive(session) {
|
|
34
|
+
const runtime = sessionManager?.getSessionRuntime?.(session.id);
|
|
35
|
+
if (runtime?.controller?.signal && !runtime.controller.signal.aborted) return true;
|
|
36
|
+
return ACTIVE_STAGES.has(lower(runtime?.stage || session.status));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const workersOf = (session) =>
|
|
40
|
+
listLiveSessions().filter(
|
|
41
|
+
(candidate) =>
|
|
42
|
+
candidate?.id &&
|
|
43
|
+
candidate.id !== session.id &&
|
|
44
|
+
candidate.owner === AGENT_OWNER &&
|
|
45
|
+
candidate.closed !== true &&
|
|
46
|
+
(candidate.ownerSessionId === session.id || candidate.parentSessionId === session.id)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* WORKTREE_BUSY_JOBS while this Lead has a turn, workers or background
|
|
51
|
+
* shells running (`shellJobs: false` tolerates background shells).
|
|
52
|
+
*/
|
|
53
|
+
async function assertLeadIdle(session, { ownTurn = false, shellJobs: checkShellJobs = true } = {}) {
|
|
54
|
+
if (!ownTurn && getActiveTurnCount() > 0) {
|
|
55
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', 'The session is running a turn; wait for it to finish.', {
|
|
56
|
+
reason: 'turn',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const status = agentStatusState?.() || {};
|
|
60
|
+
const workers = Array.isArray(status.agentWorkers) ? status.agentWorkers : [];
|
|
61
|
+
const jobs = Array.isArray(status.agentJobs) ? status.agentJobs : [];
|
|
62
|
+
const busyWorkers = workers.filter(
|
|
63
|
+
(row) => BUSY_WORKER_STATES.has(lower(row?.status)) || BUSY_WORKER_STATES.has(lower(row?.stage))
|
|
64
|
+
).length;
|
|
65
|
+
const busyJobs = jobs.filter((row) => BUSY_JOB_STATES.has(lower(row?.status))).length;
|
|
66
|
+
if (busyWorkers || busyJobs) {
|
|
67
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', undefined, { reason: 'workers', workers: busyWorkers, jobs: busyJobs });
|
|
68
|
+
}
|
|
69
|
+
if (!checkShellJobs) return;
|
|
70
|
+
const ids = new Set([session.id, ...workers.map((row) => row?.sessionId).filter(Boolean)]);
|
|
71
|
+
for (const worker of workersOf(session)) ids.add(worker.id);
|
|
72
|
+
const shellJobs = await countOwnShellJobs([...ids]);
|
|
73
|
+
if (shellJobs > 0) throw worktreeError('WORKTREE_BUSY_JOBS', undefined, { reason: 'shell-jobs', shellJobs });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A checkout or merge in the shared checkout `folder` refuses while another
|
|
78
|
+
* session (not this Lead's own workers) works there, then holds the folder
|
|
79
|
+
* until the returned release: other sessions' turns that arrive meanwhile
|
|
80
|
+
* wait (admission gate). `sessionId` null: a session-less (draft) change.
|
|
81
|
+
* @returns {() => void}
|
|
82
|
+
*/
|
|
83
|
+
function holdProjectFolder(sessionId, folder, { hold = true } = {}) {
|
|
84
|
+
const users = listLiveSessions().filter(
|
|
85
|
+
(candidate) =>
|
|
86
|
+
candidate?.id &&
|
|
87
|
+
candidate.id !== sessionId &&
|
|
88
|
+
candidate.closed !== true &&
|
|
89
|
+
(!sessionId || (candidate.ownerSessionId !== sessionId && candidate.parentSessionId !== sessionId)) &&
|
|
90
|
+
typeof candidate.cwd === 'string' &&
|
|
91
|
+
isWithin(folder, candidate.cwd) &&
|
|
92
|
+
isSessionActive(candidate)
|
|
93
|
+
);
|
|
94
|
+
if (users.length) throw worktreeRefusal('folder-in-use', { sessions: users.map((candidate) => candidate.id) });
|
|
95
|
+
return holdCheckoutFolder(sessionId, folder, { hold });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Run `fn` holding the checkout `folder` (holdProjectFolder); released when it settles. */
|
|
99
|
+
async function withProjectFolder(sessionId, folder, fn) {
|
|
100
|
+
const release = holdProjectFolder(sessionId, folder);
|
|
101
|
+
try {
|
|
102
|
+
return await fn();
|
|
103
|
+
} finally {
|
|
104
|
+
release();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Before a carry clears the shared checkout `folder` (held by the caller,
|
|
110
|
+
* so no new turn starts there): WORKTREE_BUSY_JOBS while any other session
|
|
111
|
+
* working in it — idle or not, this Lead's workers included — owns a
|
|
112
|
+
* running background shell (holdProjectFolder already refused running
|
|
113
|
+
* turns and agents there, assertLeadIdle this Lead's own). Processes
|
|
114
|
+
* outside Mixdog (editors, watchers) cannot be excluded.
|
|
115
|
+
*/
|
|
116
|
+
async function assertCheckoutQuiet(session, folder) {
|
|
117
|
+
const sharing = listLiveSessions()
|
|
118
|
+
.filter(
|
|
119
|
+
(candidate) =>
|
|
120
|
+
candidate?.id &&
|
|
121
|
+
candidate.id !== session.id &&
|
|
122
|
+
candidate.closed !== true &&
|
|
123
|
+
typeof candidate.cwd === 'string' &&
|
|
124
|
+
isWithin(folder, candidate.cwd)
|
|
125
|
+
)
|
|
126
|
+
.map((candidate) => candidate.id);
|
|
127
|
+
if (!sharing.length) return;
|
|
128
|
+
const shellJobs = await countOwnShellJobs(sharing);
|
|
129
|
+
if (shellJobs > 0) throw worktreeRefusal('folder-shell-jobs', { shellJobs });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The shared admission of every merge and folder cleanup (manageProjectBranch):
|
|
134
|
+
* refuses while a session or worker runs in an affected folder, or a
|
|
135
|
+
* background command does; `removing` refuses while any session still uses
|
|
136
|
+
* one. Then holds the folders — Mixdog checkouts only; an external checkout
|
|
137
|
+
* takes no lease from this profile — so new Mixdog work there waits until `run`
|
|
138
|
+
* settles. `actor`: the session acting on its own checkout, whose own turn
|
|
139
|
+
* and workers its caller already checked (assertLeadIdle).
|
|
140
|
+
*/
|
|
141
|
+
async function withManagementFolders(folders, removing, run, { actor = null } = {}) {
|
|
142
|
+
const release = [];
|
|
143
|
+
try {
|
|
144
|
+
for (const folder of [...new Set(folders)].sort()) {
|
|
145
|
+
release.push(holdProjectFolder(actor, folder, { hold: !isExternalCheckout(folder) }));
|
|
146
|
+
}
|
|
147
|
+
release.push(holdWorkspaceActivity(folders, { sessionId: actor }));
|
|
148
|
+
const stored = await listWorkspaceConsumers();
|
|
149
|
+
const consumers = new Map(stored.map((session) => [session.id, session]));
|
|
150
|
+
for (const session of listLiveSessions()) if (session?.closed !== true) consumers.set(session.id, session);
|
|
151
|
+
const affected = [...consumers.values()].filter((session) =>
|
|
152
|
+
folders.some((folder) => session.cwd && isWithin(folder, session.cwd))
|
|
153
|
+
);
|
|
154
|
+
const blocking = removing
|
|
155
|
+
? affected
|
|
156
|
+
: affected.filter(
|
|
157
|
+
(session) =>
|
|
158
|
+
!(actor && [session.id, session.ownerSessionId, session.parentSessionId].includes(actor)) &&
|
|
159
|
+
isSessionActive(session)
|
|
160
|
+
);
|
|
161
|
+
if (blocking.length) throw worktreeRefusal('folder-in-use', { sessions: blocking.map((session) => session.id) });
|
|
162
|
+
const jobs = await listWorkspaceJobs();
|
|
163
|
+
if (jobs.some((job) => !job.terminal && folders.some((folder) => isWithin(folder, job.cwd)))) {
|
|
164
|
+
throw worktreeRefusal('folder-shell-jobs');
|
|
165
|
+
}
|
|
166
|
+
if ((await countOwnShellJobs(affected.map((session) => session.id))) > 0) {
|
|
167
|
+
throw worktreeRefusal('folder-shell-jobs');
|
|
168
|
+
}
|
|
169
|
+
return await run([...consumers.keys()]);
|
|
170
|
+
} finally {
|
|
171
|
+
releaseAll(release);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { workersOf, assertLeadIdle, withProjectFolder, assertCheckoutQuiet, withManagementFolders };
|
|
176
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hosted Lead's workspace as the transitions read it: the session this
|
|
3
|
+
* runtime hosts, its stored (or project-folder) workspace record, the live
|
|
4
|
+
* view the session snapshot publishes and the branch menu.
|
|
5
|
+
*/
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
7
|
+
import {
|
|
8
|
+
branchMenuEntries,
|
|
9
|
+
isWorktreeBound,
|
|
10
|
+
normalizeSessionWorkspace,
|
|
11
|
+
worktreeError,
|
|
12
|
+
} from '../../runtime/shared/worktrees/contract.mjs';
|
|
13
|
+
import { samePath } from '../../runtime/shared/worktrees/paths.mjs';
|
|
14
|
+
import { readGitHead } from '../../runtime/shared/worktrees/git-head.mjs';
|
|
15
|
+
import { readSessionWorkspace } from './workspace-state.mjs';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} deps The host hooks of createWorkspaceTransitions plus its
|
|
19
|
+
* `backend()` loader and `pendingWorktree()` (the armed worktree intent).
|
|
20
|
+
*/
|
|
21
|
+
export function createSessionWorkspaceView({
|
|
22
|
+
getSession,
|
|
23
|
+
getCurrentCwd,
|
|
24
|
+
getDesktopSession,
|
|
25
|
+
createCurrentSession,
|
|
26
|
+
inspectProject,
|
|
27
|
+
backend,
|
|
28
|
+
pendingWorktree,
|
|
29
|
+
}) {
|
|
30
|
+
function hostedSession(sessionRef) {
|
|
31
|
+
const session = getSession();
|
|
32
|
+
const requested = String(sessionRef?.sessionId || '').trim();
|
|
33
|
+
if (!session?.id || (requested && requested !== session.id)) {
|
|
34
|
+
throw new Error(`workspace: session ${requested || '(none)'} is not hosted by this runtime.`);
|
|
35
|
+
}
|
|
36
|
+
return session;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function ensureSession() {
|
|
40
|
+
if (!getSession()?.id && typeof createCurrentSession === 'function') await createCurrentSession('workspace');
|
|
41
|
+
return hostedSession(null);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function projectPathOf(session) {
|
|
45
|
+
const desktop = getDesktopSession?.() || session?.desktopSession;
|
|
46
|
+
if (desktop?.classification === 'project' && desktop.projectPath) return desktop.projectPath;
|
|
47
|
+
return session?.cwd || getCurrentCwd();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The stored record, or the project-folder workspace (revision 0) of a session that never moved. */
|
|
51
|
+
function currentWorkspace(session) {
|
|
52
|
+
const stored = readSessionWorkspace(session);
|
|
53
|
+
if (stored) return stored;
|
|
54
|
+
const projectPath = projectPathOf(session);
|
|
55
|
+
return normalizeSessionWorkspace({ projectPath, cwd: getCurrentCwd() || projectPath, revision: 0 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let liveWorkspace = null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The workspace the session publishes: a worktree-bound record as stored; a
|
|
62
|
+
* project-folder session (never moved, back from a worktree, or carrying a
|
|
63
|
+
* transition stub) with the folder's checked-out branch / detached commit and
|
|
64
|
+
* repoCommonDir read straight from HEAD — no git process. Null outside a git
|
|
65
|
+
* repository for a session without a record. Identity-stable while equal.
|
|
66
|
+
*/
|
|
67
|
+
function getLiveWorkspace() {
|
|
68
|
+
const session = getSession();
|
|
69
|
+
const stored = readSessionWorkspace(session);
|
|
70
|
+
if (isWorktreeBound(stored)) return session.workspace;
|
|
71
|
+
let base;
|
|
72
|
+
try {
|
|
73
|
+
base = stored ?? currentWorkspace(session);
|
|
74
|
+
} catch {
|
|
75
|
+
return session?.workspace ?? null;
|
|
76
|
+
}
|
|
77
|
+
const head = readGitHead(base.cwd);
|
|
78
|
+
if (!head) return session?.workspace ?? null;
|
|
79
|
+
const next = {
|
|
80
|
+
...base,
|
|
81
|
+
repoCommonDir: head.repoCommonDir,
|
|
82
|
+
branch: head.branch,
|
|
83
|
+
baseCommit: head.branch ? base.baseCommit : head.commit,
|
|
84
|
+
};
|
|
85
|
+
if (!liveWorkspace || !isDeepStrictEqual(liveWorkspace, next)) liveWorkspace = next;
|
|
86
|
+
return liveWorkspace;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** WORKTREE_PROJECT_MISMATCH unless `repoCommonDir` (when given) is the session Project's repository. */
|
|
90
|
+
async function sessionProject(session, repoCommonDir) {
|
|
91
|
+
const workspace = currentWorkspace(session);
|
|
92
|
+
const project = await inspectProject(workspace.projectPath);
|
|
93
|
+
if (repoCommonDir && !samePath(repoCommonDir, project.repoCommonDir)) {
|
|
94
|
+
throw worktreeError(
|
|
95
|
+
'WORKTREE_PROJECT_MISMATCH',
|
|
96
|
+
"Managed worktrees belong to this session's Project repository; run the command there.",
|
|
97
|
+
{ projectPath: workspace.projectPath, repoCommonDir }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return { workspace, project };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function getBranchMenu() {
|
|
104
|
+
const session = getSession();
|
|
105
|
+
const workspace = session?.id
|
|
106
|
+
? currentWorkspace(session)
|
|
107
|
+
: normalizeSessionWorkspace({ projectPath: projectPathOf(null), cwd: getCurrentCwd(), revision: 0 });
|
|
108
|
+
const menu = await (await backend()).listBranchOccupancy({
|
|
109
|
+
projectPath: workspace.projectPath,
|
|
110
|
+
sessionId: session?.id || null,
|
|
111
|
+
});
|
|
112
|
+
const selectedEntry = workspace.worktreeId ? await (await backend()).findWorktreeContaining(workspace.cwd) : null;
|
|
113
|
+
// A session outside a managed worktree (the project folder — never moved,
|
|
114
|
+
// or back from a worktree — or a linked checkout of the selected branch)
|
|
115
|
+
// is on whatever that folder has checked out now.
|
|
116
|
+
// Its repository is the project's (a never-moved session has no stored
|
|
117
|
+
// record), and a detached checkout shows branch null with the commit in
|
|
118
|
+
// baseCommit — never mistaken for "not a repository".
|
|
119
|
+
let view = workspace;
|
|
120
|
+
if (!isWorktreeBound(workspace)) {
|
|
121
|
+
const project = await inspectProject(workspace.cwd);
|
|
122
|
+
const checkedOut = project.currentBranch ?? null;
|
|
123
|
+
view = {
|
|
124
|
+
...workspace,
|
|
125
|
+
repoCommonDir: project.repoCommonDir ?? workspace.repoCommonDir,
|
|
126
|
+
branch: checkedOut,
|
|
127
|
+
baseCommit: checkedOut ? workspace.baseCommit : (project.headCommit ?? null),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
workspace: view,
|
|
132
|
+
pendingWorktree: pendingWorktree(),
|
|
133
|
+
branches: branchMenuEntries(menu.branches, menu.occupancy),
|
|
134
|
+
occupancy: menu.occupancy,
|
|
135
|
+
retainedWorktree: selectedEntry ?? menu.retainedWorktree ?? null,
|
|
136
|
+
// Creation notes of the retained worktree (submodules, include entries).
|
|
137
|
+
warnings: Array.isArray(menu.retainedWorktree?.warnings) ? [...menu.retainedWorktree.warnings] : [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
hostedSession,
|
|
143
|
+
ensureSession,
|
|
144
|
+
currentWorkspace,
|
|
145
|
+
getLiveWorkspace,
|
|
146
|
+
sessionProject,
|
|
147
|
+
getBranchMenu,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed checkouts the hosted Lead makes without moving into them: a branch
|
|
3
|
+
* worktree (`git worktree add`) and the read-only snapshots of its turns,
|
|
4
|
+
* which the turn's admission release removes once nothing runs in them.
|
|
5
|
+
*/
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
import { WORKTREE_SNAPSHOT_KIND, worktreeError } from '../../runtime/shared/worktrees/contract.mjs';
|
|
8
|
+
import { admitTurn } from './admission-gate.mjs';
|
|
9
|
+
|
|
10
|
+
// A settled turn's snapshot cleanup waits at most 2^5 times its first delay between attempts.
|
|
11
|
+
const SNAPSHOT_BACKOFF_STEPS = 5;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A managed worktree for `branch` without moving the session (`git
|
|
15
|
+
* worktree add`): retained by this session like a branch it selected, so
|
|
16
|
+
* entering it later (selectBranch) reuses it. A branch already checked out
|
|
17
|
+
* elsewhere is WORKTREE_BRANCH_OCCUPIED, except in this session's own
|
|
18
|
+
* managed worktree, which is returned as it is. `startPoint` (with
|
|
19
|
+
* `create`): the commit the new branch starts at.
|
|
20
|
+
*/
|
|
21
|
+
async function addBranchWorktree(
|
|
22
|
+
{ hostedSession, sessionProject, backend, runtimeToken, newTxId },
|
|
23
|
+
{ branch, create = false, startPoint = null, confirmLarge = false, txId = '', repoCommonDir = null } = {}
|
|
24
|
+
) {
|
|
25
|
+
const session = hostedSession(null);
|
|
26
|
+
const { workspace } = await sessionProject(session, repoCommonDir);
|
|
27
|
+
const backendApi = await backend();
|
|
28
|
+
const { occupancy } = await backendApi.listBranchOccupancy({
|
|
29
|
+
projectPath: workspace.projectPath,
|
|
30
|
+
sessionId: session.id,
|
|
31
|
+
});
|
|
32
|
+
const held = occupancy.find((row) => row.branch === branch && row.kind !== 'free');
|
|
33
|
+
if (held && !(held.kind === 'session' && held.sessionId === session.id)) {
|
|
34
|
+
throw worktreeError('WORKTREE_BRANCH_OCCUPIED', `Branch ${branch} is checked out in ${held.path}.`, {
|
|
35
|
+
branch,
|
|
36
|
+
path: held.path,
|
|
37
|
+
kind: held.kind,
|
|
38
|
+
...(held.sessionId ? { sessionId: held.sessionId } : {}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const resolved = await backendApi.resolveBranchWorkspace({
|
|
42
|
+
projectPath: workspace.projectPath,
|
|
43
|
+
branch,
|
|
44
|
+
create: create === true,
|
|
45
|
+
startPoint,
|
|
46
|
+
sessionId: session.id,
|
|
47
|
+
runtimeToken,
|
|
48
|
+
txId: String(txId || '').trim() || newTxId(),
|
|
49
|
+
confirmLarge: confirmLarge === true,
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
path: resolved.path,
|
|
53
|
+
branch,
|
|
54
|
+
worktreeId: resolved.entry?.id ?? null,
|
|
55
|
+
created: resolved.created === true,
|
|
56
|
+
baseCommit: resolved.entry?.baseCommit ?? null,
|
|
57
|
+
mergeTarget: resolved.entry?.mergeTarget ?? null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Remove this session's snapshot at `path` now; a dirty one only with
|
|
63
|
+
* `force`, and none while a worker or background shell runs in it.
|
|
64
|
+
*/
|
|
65
|
+
async function removeSnapshot({ hostedSession, backend }, { path, force = false } = {}) {
|
|
66
|
+
const session = hostedSession(null);
|
|
67
|
+
const backendApi = await backend();
|
|
68
|
+
const entry = await backendApi.getWorktreeByPath(path);
|
|
69
|
+
if (entry?.kind !== WORKTREE_SNAPSHOT_KIND) throw worktreeError('WORKTREE_MISSING', undefined, { path });
|
|
70
|
+
if (entry.holderSessionId !== session.id) {
|
|
71
|
+
throw worktreeError(
|
|
72
|
+
'WORKTREE_BUSY_JOBS',
|
|
73
|
+
"This snapshot belongs to another session's turn; it is removed when that turn ends, or by a prune.",
|
|
74
|
+
{ reason: 'snapshot-held', sessionId: entry.holderSessionId, entryId: entry.id }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const removed = await backendApi.removeSnapshotWorktree({ entryId: entry.id, force: force === true });
|
|
78
|
+
return { ...removed, entryId: entry.id, path: entry.path };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {object} deps The session view (hostedSession, sessionProject), the
|
|
83
|
+
* `backend()` loader, the runtime identity and the snapshot retry bounds.
|
|
84
|
+
*/
|
|
85
|
+
export function createSideCheckouts({
|
|
86
|
+
getSession,
|
|
87
|
+
getCurrentCwd,
|
|
88
|
+
hostedSession,
|
|
89
|
+
sessionProject,
|
|
90
|
+
backend,
|
|
91
|
+
runtimeToken,
|
|
92
|
+
newTxId,
|
|
93
|
+
snapshotRetryMs,
|
|
94
|
+
snapshotRetryLimit,
|
|
95
|
+
}) {
|
|
96
|
+
// The creation txIds of the read-only snapshots the current admitted turn
|
|
97
|
+
// made. Each turn owns its own set, so its cleanup never touches a later
|
|
98
|
+
// turn's snapshots (a txId, unlike a freed slug, is never reused).
|
|
99
|
+
let turnSnapshots = null;
|
|
100
|
+
// The running cleanups of settled turns' snapshots (controller → session id).
|
|
101
|
+
const snapshotCleanups = new Map();
|
|
102
|
+
let disposed = false;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Turn admission (see admission-gate admitTurn); resolves the release. The
|
|
106
|
+
* release also starts the removal of the read-only snapshots this turn
|
|
107
|
+
* made — only those (removeTurnSnapshots).
|
|
108
|
+
*/
|
|
109
|
+
async function admitTurnWorkspace({ signal } = {}) {
|
|
110
|
+
const release = await admitTurn({ sessionId: getSession()?.id || '', cwd: () => getCurrentCwd() || '', signal });
|
|
111
|
+
const made = new Set();
|
|
112
|
+
turnSnapshots = made;
|
|
113
|
+
return () => {
|
|
114
|
+
release();
|
|
115
|
+
if (turnSnapshots === made) turnSnapshots = null;
|
|
116
|
+
const sessionId = getSession()?.id;
|
|
117
|
+
if (made.size && sessionId) void removeTurnSnapshots(sessionId, [...made]);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Remove a settled turn's snapshots (by creation `txIds`) once nothing runs
|
|
123
|
+
* in them: a worker turn or background shell holding a workspace lease
|
|
124
|
+
* inside one keeps it (manager removeSessionSnapshots reports it `in-use`).
|
|
125
|
+
* Those are retried with a doubling delay (`snapshotRetryMs`, at most
|
|
126
|
+
* 2^SNAPSHOT_BACKOFF_STEPS times it) for at most `snapshotRetryLimit`
|
|
127
|
+
* attempts; a snapshot removal that fails otherwise is not retried. What is
|
|
128
|
+
* left then — or when the runtime is disposed, the session closes, is
|
|
129
|
+
* deleted or is no longer the one hosted here — is the prune's and the
|
|
130
|
+
* startup reconcile's. Admission is not held meanwhile: the removal only
|
|
131
|
+
* ever names this turn's snapshots.
|
|
132
|
+
*/
|
|
133
|
+
async function removeTurnSnapshots(sessionId, txIds) {
|
|
134
|
+
if (disposed) return;
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
snapshotCleanups.set(controller, sessionId);
|
|
137
|
+
try {
|
|
138
|
+
let pending = txIds;
|
|
139
|
+
for (let attempt = 0; pending.length && attempt < snapshotRetryLimit; attempt++) {
|
|
140
|
+
if (attempt > 0) {
|
|
141
|
+
const wait = snapshotRetryMs * 2 ** Math.min(attempt - 1, SNAPSHOT_BACKOFF_STEPS);
|
|
142
|
+
await delay(wait, undefined, { ref: false, signal: controller.signal });
|
|
143
|
+
}
|
|
144
|
+
const session = getSession();
|
|
145
|
+
if (session?.id !== sessionId || session.closed === true) return;
|
|
146
|
+
const report = await (await backend()).removeSessionSnapshots(sessionId, { txIds: pending });
|
|
147
|
+
pending = report.kept.filter((row) => row.reason === 'in-use').map((row) => row.txId);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Cancelled, or the backend failed: the rest is the maintenance's.
|
|
151
|
+
} finally {
|
|
152
|
+
snapshotCleanups.delete(controller);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Stop the snapshot cleanups of `sessionId` (every session's when null). */
|
|
157
|
+
function cancelSnapshotCleanups(sessionId = null) {
|
|
158
|
+
for (const [controller, owner] of snapshotCleanups) {
|
|
159
|
+
if (sessionId === null || owner === sessionId) controller.abort();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Runtime teardown: no snapshot cleanup runs on (prune / startup reconcile finish them). */
|
|
164
|
+
function dispose() {
|
|
165
|
+
disposed = true;
|
|
166
|
+
cancelSnapshotCleanups();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A read-only snapshot of `commit` for this session's turn (`git worktree
|
|
171
|
+
* add --detach`): a detached managed checkout no session works in,
|
|
172
|
+
* removed when the turn settles (and nothing runs in it any more). The
|
|
173
|
+
* session does not move. Only inside an admitted turn: a snapshot always
|
|
174
|
+
* belongs to the turn that made it.
|
|
175
|
+
*/
|
|
176
|
+
async function createSnapshot({ commit = 'HEAD', confirmLarge = false, txId = '', repoCommonDir = null } = {}) {
|
|
177
|
+
const session = hostedSession(null);
|
|
178
|
+
const made = turnSnapshots;
|
|
179
|
+
if (!made) throw new Error('A read-only snapshot belongs to a turn; none is running in this session.');
|
|
180
|
+
const { workspace } = await sessionProject(session, repoCommonDir);
|
|
181
|
+
const tx = String(txId || '').trim() || newTxId();
|
|
182
|
+
const entry = await (await backend()).createSnapshotWorktree({
|
|
183
|
+
projectPath: workspace.projectPath,
|
|
184
|
+
sessionId: session.id,
|
|
185
|
+
runtimeToken,
|
|
186
|
+
commit,
|
|
187
|
+
txId: tx,
|
|
188
|
+
confirmLarge: confirmLarge === true,
|
|
189
|
+
});
|
|
190
|
+
made.add(tx);
|
|
191
|
+
return entry;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
admitTurnWorkspace,
|
|
196
|
+
addBranchWorktree: (input) =>
|
|
197
|
+
addBranchWorktree({ hostedSession, sessionProject, backend, runtimeToken, newTxId }, input),
|
|
198
|
+
createSnapshot,
|
|
199
|
+
removeSnapshot: (input) => removeSnapshot({ hostedSession, backend }, input),
|
|
200
|
+
cancelSnapshotCleanups,
|
|
201
|
+
dispose,
|
|
202
|
+
};
|
|
203
|
+
}
|