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
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* loop each read stretched to seconds. This worker runs exactly that pipeline
|
|
9
9
|
* (stat → read → stat, digest, readTopLevelLifecycleRecord →
|
|
10
10
|
* projectStoredTranscript with no turn checkpoint) and hands back the
|
|
11
|
-
* digest and the
|
|
12
|
-
*
|
|
11
|
+
* digest and one slice of the session's display history: the history itself
|
|
12
|
+
* is built once per content and kept here with its base, and only the window
|
|
13
|
+
* or page a request names (`slice`: `{ limit, skip }`) crosses back, with its
|
|
14
|
+
* serialized size measured here.
|
|
13
15
|
*
|
|
14
16
|
* Inside the worker, one projection base (recovery, prepared session and
|
|
15
17
|
* whole-transcript context gauge) per session content is shared by every
|
|
@@ -60,8 +62,14 @@ function clientFor(sessionId) {
|
|
|
60
62
|
* the full file stamp the bytes provably came from, or null. Rejects when the
|
|
61
63
|
* worker cannot answer; callers then run the pipeline on their own thread.
|
|
62
64
|
*/
|
|
63
|
-
export function projectStoredTranscriptOffThread({
|
|
64
|
-
|
|
65
|
+
export function projectStoredTranscriptOffThread({
|
|
66
|
+
sessionId,
|
|
67
|
+
recordPath,
|
|
68
|
+
known = null,
|
|
69
|
+
fingerprint = '',
|
|
70
|
+
slice = null,
|
|
71
|
+
}) {
|
|
72
|
+
return clientFor(sessionId)({ sessionId, recordPath, known, fingerprint, slice });
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/** Whether the projection worker of `sessionId` is attached (a request now
|
|
@@ -321,15 +329,108 @@ async function buildBase(sessionId, text, previousMessages, timing) {
|
|
|
321
329
|
|
|
322
330
|
const baseBuilds = createBaseBuilds({ build: buildBase });
|
|
323
331
|
|
|
324
|
-
|
|
332
|
+
// Serialized bytes of the display histories this worker keeps for slicing:
|
|
333
|
+
// one larger than the per-session bound is rebuilt per request instead, and
|
|
334
|
+
// the oldest go once all of them exceed the worker's bound. The worker's idle
|
|
335
|
+
// exit releases every one.
|
|
336
|
+
// Measured: a history is about as large as its record (74 MB record: 75 MB
|
|
337
|
+
// history) and a page of a retained one costs ~0.2 s, a rebuilt one ~1 s. 96
|
|
338
|
+
// MB keeps the largest sessions seen (~78 MB records) smooth; 128 MB holds
|
|
339
|
+
// one of those beside a few ordinary ones.
|
|
340
|
+
export const WORKER_HISTORY_SESSION_MAX_BYTES = 96 * 1024 * 1024;
|
|
341
|
+
export const WORKER_HISTORY_MAX_BYTES = 128 * 1024 * 1024;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The display histories one worker retains (`{ value, rowBytes, bytes }`,
|
|
345
|
+
* keyed by session, Map order LRU) within `sessionMaxBytes` each and
|
|
346
|
+
* `maxBytes` together. A history of other content (`owner`, the record
|
|
347
|
+
* content it was built from, and its sidecar fingerprint) is never served.
|
|
348
|
+
*/
|
|
349
|
+
export function createHistoryRetention({
|
|
350
|
+
sessionMaxBytes = WORKER_HISTORY_SESSION_MAX_BYTES,
|
|
351
|
+
maxBytes = WORKER_HISTORY_MAX_BYTES,
|
|
352
|
+
} = {}) {
|
|
353
|
+
const histories = new Map();
|
|
354
|
+
let retainedBytes = 0;
|
|
355
|
+
const drop = (sessionId) => {
|
|
356
|
+
const entry = histories.get(sessionId);
|
|
357
|
+
if (!entry) return;
|
|
358
|
+
histories.delete(sessionId);
|
|
359
|
+
retainedBytes -= entry.history.bytes;
|
|
360
|
+
};
|
|
361
|
+
return {
|
|
362
|
+
get(sessionId, owner, fingerprint) {
|
|
363
|
+
const entry = histories.get(sessionId);
|
|
364
|
+
if (!entry || entry.owner !== owner || entry.fingerprint !== fingerprint) return null;
|
|
365
|
+
histories.delete(sessionId);
|
|
366
|
+
histories.set(sessionId, entry);
|
|
367
|
+
return entry.history;
|
|
368
|
+
},
|
|
369
|
+
/** Keeps `history` when it fits; whether it was kept. */
|
|
370
|
+
retain(sessionId, owner, fingerprint, history) {
|
|
371
|
+
drop(sessionId);
|
|
372
|
+
if (history.bytes > sessionMaxBytes) return false;
|
|
373
|
+
histories.set(sessionId, { owner, fingerprint, history });
|
|
374
|
+
retainedBytes += history.bytes;
|
|
375
|
+
while (retainedBytes > maxBytes && histories.size > 1) drop(histories.keys().next().value);
|
|
376
|
+
return true;
|
|
377
|
+
},
|
|
378
|
+
forget: drop,
|
|
379
|
+
stats: () => ({ histories: histories.size, retainedBytes }),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const historyRetention = createHistoryRetention();
|
|
384
|
+
/** sessionId -> { owner, fingerprint, promise }: histories being built. */
|
|
385
|
+
const historyBuilds = new Map();
|
|
386
|
+
|
|
387
|
+
/** The display history of `built` (one base) under sidecar `fingerprint`,
|
|
388
|
+
* with each row's serialized bytes: retained, joined while another request
|
|
389
|
+
* builds it, or built now. */
|
|
390
|
+
async function displayHistory(sessionId, built, content, fingerprint, projection) {
|
|
391
|
+
const retained = historyRetention.get(sessionId, content, fingerprint);
|
|
392
|
+
if (retained) return { history: retained, historyBuilt: false };
|
|
393
|
+
const pending = historyBuilds.get(sessionId);
|
|
394
|
+
if (pending && pending.owner === content && pending.fingerprint === fingerprint) {
|
|
395
|
+
return { history: await pending.promise, historyBuilt: false };
|
|
396
|
+
}
|
|
397
|
+
const promise = (async () => {
|
|
398
|
+
const value = await projection.windowStoredTranscript(sessionId, built.base, {
|
|
399
|
+
history: true,
|
|
400
|
+
includeMessages: false,
|
|
401
|
+
});
|
|
402
|
+
const rowBytes = new Float64Array(value.items.length);
|
|
403
|
+
let bytes = 0;
|
|
404
|
+
for (let index = 0; index < value.items.length; index += 1) {
|
|
405
|
+
rowBytes[index] = Buffer.byteLength(JSON.stringify(value.items[index] ?? null));
|
|
406
|
+
bytes += rowBytes[index];
|
|
407
|
+
}
|
|
408
|
+
return { value, rowBytes, bytes };
|
|
409
|
+
})();
|
|
410
|
+
const record = { owner: content, fingerprint, promise };
|
|
411
|
+
historyBuilds.set(sessionId, record);
|
|
412
|
+
try {
|
|
413
|
+
const history = await promise;
|
|
414
|
+
if (historyBuilds.get(sessionId) === record) historyRetention.retain(sessionId, content, fingerprint, history);
|
|
415
|
+
return { history, historyBuilt: true };
|
|
416
|
+
} finally {
|
|
417
|
+
if (historyBuilds.get(sessionId) === record) historyBuilds.delete(sessionId);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function project({ sessionId, recordPath, known, fingerprint = '', slice = null }) {
|
|
325
422
|
const importStartedAt = performance.now();
|
|
326
|
-
const [
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
423
|
+
const [
|
|
424
|
+
{ statSessionStamp, sameSessionStamp },
|
|
425
|
+
{ readTextFile, PROBE_PRESENT },
|
|
426
|
+
{ transcriptTextHash, historyStampOf },
|
|
427
|
+
projection,
|
|
428
|
+
] = await Promise.all([
|
|
429
|
+
import('./store/canonical-reader.mjs'),
|
|
430
|
+
import('./store/fs-probe.mjs'),
|
|
431
|
+
import('./store-transcript-cache.mjs'),
|
|
432
|
+
import('./store-transcript-projection.mjs'),
|
|
433
|
+
]);
|
|
333
434
|
const timing = { importMs: performance.now() - importStartedAt };
|
|
334
435
|
const observe = () => {
|
|
335
436
|
try {
|
|
@@ -374,12 +475,32 @@ async function project({ sessionId, recordPath, itemLimit, known }) {
|
|
|
374
475
|
const built = await pending;
|
|
375
476
|
if (built === RESTART_READ) continue;
|
|
376
477
|
if (built.unreadable) return { ...digest, unreadable: true };
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
478
|
+
// The whole display history stays here (within its budgets); only the
|
|
479
|
+
// requested slice crosses back.
|
|
480
|
+
const { history, historyBuilt } = await displayHistory(
|
|
481
|
+
sessionId,
|
|
482
|
+
built,
|
|
483
|
+
`${digest.textHash}:${digest.textChars}`,
|
|
484
|
+
fingerprint,
|
|
485
|
+
projection
|
|
486
|
+
);
|
|
487
|
+
const request = slice || {};
|
|
488
|
+
const historyStamp = historyStampOf(sessionId, digest.textHash, digest.textChars, fingerprint, request.epoch);
|
|
489
|
+
const proven = Boolean(request.held) && request.baseHistory === historyStamp;
|
|
490
|
+
const value = projection.storedHistorySlice(
|
|
491
|
+
history.value,
|
|
492
|
+
{ ...request, proven },
|
|
493
|
+
{
|
|
494
|
+
digestOf: request.held && !proven ? await projection.heldRowsDigester() : null,
|
|
495
|
+
rowBytes: (index) => history.rowBytes[index],
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
const { windowStamp } = value;
|
|
499
|
+
delete value.windowStamp;
|
|
500
|
+
value.projectionStamp = `${historyStamp}/${windowStamp}`;
|
|
501
|
+
const bytes = Buffer.byteLength(JSON.stringify(value));
|
|
381
502
|
timing.projectMs = performance.now() - projectStartedAt;
|
|
382
|
-
return { ...digest, value, lifecycle: built.lifecycle };
|
|
503
|
+
return { ...digest, value, bytes, historyBuilt, lifecycle: built.lifecycle };
|
|
383
504
|
} finally {
|
|
384
505
|
if (!submitted) baseBuilds.release(ticket);
|
|
385
506
|
}
|
|
@@ -388,6 +509,9 @@ async function project({ sessionId, recordPath, itemLimit, known }) {
|
|
|
388
509
|
|
|
389
510
|
if (!isMainThread && workerData?.kind === WORKER_KIND) {
|
|
390
511
|
serveWorkerRequests((payload) => {
|
|
391
|
-
|
|
512
|
+
if (!payload?.forget) return project(payload);
|
|
513
|
+
historyRetention.forget(payload.forget);
|
|
514
|
+
historyBuilds.delete(payload.forget);
|
|
515
|
+
return baseBuilds.forget(payload.forget);
|
|
392
516
|
});
|
|
393
517
|
}
|
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
* when the last observed survivor exits, `cancel` terminates them. */
|
|
26
26
|
function trackSurvivingDescendants(handle, { command, cwd, options, startedAtMs } = {}) {
|
|
27
27
|
if (!handle?.taskId) return null;
|
|
28
|
+
// Once cancelled, every observation pass terminates survivors that ignored
|
|
29
|
+
// the first kill, so a retry is not needed to finish the job.
|
|
30
|
+
let cancelled = false;
|
|
28
31
|
try {
|
|
29
32
|
const task = startBackgroundTask({
|
|
30
33
|
taskId: handle.taskId,
|
|
@@ -36,10 +39,10 @@ function trackSurvivingDescendants(handle, { command, cwd, options, startedAtMs
|
|
|
36
39
|
meta: { task_id: handle.taskId, stdout: null, stderr: null, cwd, timeoutMs: 0 },
|
|
37
40
|
resultType: 'shell_task_result',
|
|
38
41
|
run: async () => {
|
|
39
|
-
await waitForShellDescendants(handle);
|
|
42
|
+
await waitForShellDescendants(handle, { sweep: () => cancelled });
|
|
40
43
|
// The folder reservation the finished command handed to its
|
|
41
44
|
// survivors ends with the last of them.
|
|
42
|
-
releaseShellJobWorkspace(handle.taskId);
|
|
45
|
+
await releaseShellJobWorkspace(handle.taskId);
|
|
43
46
|
return {
|
|
44
47
|
task_id: handle.taskId,
|
|
45
48
|
status: 'completed',
|
|
@@ -48,6 +51,7 @@ function trackSurvivingDescendants(handle, { command, cwd, options, startedAtMs
|
|
|
48
51
|
},
|
|
49
52
|
// The kill's own re-observation is what `task cancel` reports.
|
|
50
53
|
cancel: () => {
|
|
54
|
+
cancelled = true;
|
|
51
55
|
task.cancelOutcome = killShellDescendants(handle);
|
|
52
56
|
},
|
|
53
57
|
});
|
|
@@ -9,11 +9,12 @@ import { withBuiltinPathLocks } from './path-locks.mjs';
|
|
|
9
9
|
import { withAdvisoryLocks } from './advisory-lock.mjs';
|
|
10
10
|
import { gitRepoLockKey, withGitWorktreeWriteLock } from './git-repo-rw-lock.mjs';
|
|
11
11
|
import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { tryAdmitWorkspaceActivityAsync } from '../../../../shared/worktrees/activity-gate.mjs';
|
|
13
13
|
import { WORKTREE_SNAPSHOT_KIND } from '../../../../shared/worktrees/contract.mjs';
|
|
14
14
|
import { isWithin } from '../../../../shared/worktrees/paths.mjs';
|
|
15
15
|
import { readRegistrySync } from '../../../../shared/worktrees/registry.mjs';
|
|
16
16
|
import { runAbortable } from '../../../../shared/abort-race.mjs';
|
|
17
|
+
import { startGit } from '../../../../shared/git-launch.mjs';
|
|
17
18
|
import { sleep } from '../../../../shared/sleep.mjs';
|
|
18
19
|
import { capMarker } from '../../../../shared/tool-output-marker.mjs';
|
|
19
20
|
import { drainCodeGraphCache } from '../code-graph-state.mjs';
|
|
@@ -437,7 +438,24 @@ function parseCommand(rawCommand, workDir) {
|
|
|
437
438
|
return { command: String(command), cwd, globalArgs, operation, args };
|
|
438
439
|
}
|
|
439
440
|
|
|
440
|
-
|
|
441
|
+
function gitToolEnvironment(optionalLocks) {
|
|
442
|
+
return {
|
|
443
|
+
...process.env,
|
|
444
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
445
|
+
GIT_PAGER: 'cat',
|
|
446
|
+
GIT_EDITOR: 'true',
|
|
447
|
+
GIT_SEQUENCE_EDITOR: 'true',
|
|
448
|
+
LC_ALL: 'C',
|
|
449
|
+
// Reads run beside writes without a lock (git-repo-rw-lock.mjs).
|
|
450
|
+
// With optional locks off `git status` never takes index.lock to
|
|
451
|
+
// write back a refreshed index, so it cannot fail a concurrent
|
|
452
|
+
// write on that lock. (`git diff` ignores the setting and may still
|
|
453
|
+
// refresh the index briefly; the write retry absorbs that.)
|
|
454
|
+
...(optionalLocks ? {} : { GIT_OPTIONAL_LOCKS: '0' }),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function runProcess(program, argv, { cwd, signal, maxBytes = MAX_CAPTURE_BYTES, env } = {}) {
|
|
441
459
|
let child;
|
|
442
460
|
try {
|
|
443
461
|
await ensureNativeSpawnServer();
|
|
@@ -446,20 +464,7 @@ async function runProcess(program, argv, { cwd, signal, maxBytes = MAX_CAPTURE_B
|
|
|
446
464
|
argv,
|
|
447
465
|
spawnOptions: {
|
|
448
466
|
cwd,
|
|
449
|
-
env
|
|
450
|
-
...process.env,
|
|
451
|
-
GIT_TERMINAL_PROMPT: '0',
|
|
452
|
-
GIT_PAGER: 'cat',
|
|
453
|
-
GIT_EDITOR: 'true',
|
|
454
|
-
GIT_SEQUENCE_EDITOR: 'true',
|
|
455
|
-
LC_ALL: 'C',
|
|
456
|
-
// Reads run beside writes without a lock (git-repo-rw-lock.mjs).
|
|
457
|
-
// With optional locks off `git status` never takes index.lock to
|
|
458
|
-
// write back a refreshed index, so it cannot fail a concurrent
|
|
459
|
-
// write on that lock. (`git diff` ignores the setting and may still
|
|
460
|
-
// refresh the index briefly; the write retry absorbs that.)
|
|
461
|
-
...(optionalLocks ? {} : { GIT_OPTIONAL_LOCKS: '0' }),
|
|
462
|
-
},
|
|
467
|
+
env,
|
|
463
468
|
outputLimit: maxBytes,
|
|
464
469
|
// Bytes as git wrote them, decoded once by the caller: a multibyte
|
|
465
470
|
// character split across two pipe reads must not become U+FFFD.
|
|
@@ -573,11 +578,11 @@ function commandFailure(plan, result, limit = GIT_OUTPUT_LINES_DEFAULT) {
|
|
|
573
578
|
}
|
|
574
579
|
|
|
575
580
|
function runGit(plan, argv, options = {}) {
|
|
576
|
-
return
|
|
577
|
-
|
|
578
|
-
signal: options.signal,
|
|
579
|
-
|
|
580
|
-
|
|
581
|
+
return startGit(
|
|
582
|
+
gitToolEnvironment(!options.readOnly),
|
|
583
|
+
(command, env) => runProcess(command, [...plan.globalArgs, ...argv], { cwd: plan.cwd, signal: options.signal, env }),
|
|
584
|
+
(result) => result.error
|
|
585
|
+
);
|
|
581
586
|
}
|
|
582
587
|
|
|
583
588
|
// Another process (an editor, a terminal, an IDE) holding one of git's lock
|
|
@@ -585,6 +590,10 @@ function runGit(plan, argv, options = {}) {
|
|
|
585
590
|
// config.lock. Commands take that lock before changing anything, so a brief
|
|
586
591
|
// retry with backoff turns the collision into a short wait; a command that had
|
|
587
592
|
// already done part of its work reports its own state on the retry.
|
|
593
|
+
// Git reports lock contention only as text: lockfile.c's "Unable to create
|
|
594
|
+
// '<path>.lock': File exists" (and config.c's "could not lock config file"),
|
|
595
|
+
// stable English because runProcess pins LC_ALL=C. The delays are a bounded
|
|
596
|
+
// backoff (3.1 s in all), a policy choice rather than anything git reports.
|
|
588
597
|
const GIT_LOCK_CONTENTION_RE =
|
|
589
598
|
/Unable to create '[^'\n]*\.lock': File exists|could not lock config file [^\n]*: File exists/;
|
|
590
599
|
const GIT_LOCK_RETRY_DELAYS_MS = [100, 200, 400, 800, 1600];
|
|
@@ -884,12 +893,13 @@ function snapshotContaining(repo) {
|
|
|
884
893
|
// lease it holds (activity-gate, the same as a background shell's) keeps the
|
|
885
894
|
// snapshot from being removed in any process until it ends, and a snapshot
|
|
886
895
|
// whose removal is under way refuses it. Returns the release.
|
|
887
|
-
function admitSnapshotCommand(repo, sessionId) {
|
|
896
|
+
async function admitSnapshotCommand(repo, sessionId) {
|
|
888
897
|
const snapshot = snapshotContaining(repo);
|
|
889
898
|
if (!snapshot) return () => {};
|
|
890
|
-
const leave =
|
|
899
|
+
const leave = await tryAdmitWorkspaceActivityAsync(sessionId || 'git-tool', repo, { kind: 'command' });
|
|
891
900
|
if (!leave) throw new Error(`the snapshot worktree ${snapshot.path} is being removed; nothing ran`);
|
|
892
|
-
|
|
901
|
+
// Fails only when workspace admission stopped (reported there), which keeps the snapshot held.
|
|
902
|
+
return () => leave().catch(() => {});
|
|
893
903
|
}
|
|
894
904
|
|
|
895
905
|
function invalidateAfterGitMutation() {
|
|
@@ -1057,14 +1067,14 @@ async function executeSingleGitTool(input, workDir, options = {}) {
|
|
|
1057
1067
|
// under it, and a snapshot being removed refuses it.
|
|
1058
1068
|
let leaveSnapshot;
|
|
1059
1069
|
try {
|
|
1060
|
-
leaveSnapshot = admitSnapshotCommand(repo, options.sessionId);
|
|
1070
|
+
leaveSnapshot = await admitSnapshotCommand(repo, options.sessionId);
|
|
1061
1071
|
} catch (error) {
|
|
1062
1072
|
return rejected(error.message);
|
|
1063
1073
|
}
|
|
1064
1074
|
try {
|
|
1065
1075
|
return await runInRepository(input, plan, { repo, lockKey, limit, signal, workDir });
|
|
1066
1076
|
} finally {
|
|
1067
|
-
leaveSnapshot();
|
|
1077
|
+
await leaveSnapshot();
|
|
1068
1078
|
}
|
|
1069
1079
|
}
|
|
1070
1080
|
|
|
@@ -334,11 +334,9 @@ async function rawAdd(plan, input, options) {
|
|
|
334
334
|
const txId = txIdOf(options, plan.command);
|
|
335
335
|
const mapped = { mapped: plan.command, requestedPath: resolve(plan.cwd, path) };
|
|
336
336
|
// A detached checkout, or a commit-ish that names no local branch: a snapshot.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
? commitish
|
|
341
|
-
: null;
|
|
337
|
+
let snapshotCommit = null;
|
|
338
|
+
if (detach) snapshotCommit = commitish ?? 'HEAD';
|
|
339
|
+
else if (!branch && commitish && !(await localBranchExists(plan.cwd, commitish))) snapshotCommit = commitish;
|
|
342
340
|
if (snapshotCommit) {
|
|
343
341
|
const commit = await commitIn(plan.cwd, snapshotCommit);
|
|
344
342
|
if (!commit) throw new Error(`git worktree add: ${snapshotCommit} is not a commit`);
|
|
@@ -27,24 +27,40 @@ import { join } from 'node:path';
|
|
|
27
27
|
import { holdsNoData, writeJsonAtomicAsync } from '../../../../../shared/atomic-file.mjs';
|
|
28
28
|
import { isPidAlive } from '../../../../../shared/pid-liveness.mjs';
|
|
29
29
|
import { displayShellCommand } from '../../../../../shared/shell-display.mjs';
|
|
30
|
-
import {
|
|
30
|
+
import { tryAdmitWorkspaceActivityAsync } from '../../../../../shared/worktrees/activity-gate.mjs';
|
|
31
31
|
import { worktreeError } from '../../../../../shared/worktrees/contract.mjs';
|
|
32
32
|
import { workspaceSees } from '../../../../../shared/worktrees/workspace-visibility.mjs';
|
|
33
33
|
|
|
34
34
|
const workspaceLeases = new Map();
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Calls both releases in order; settles once both did. Never goes unhandled
|
|
38
|
+
* (an asynchronous release rejects only when workspace admission stopped).
|
|
39
|
+
*/
|
|
40
|
+
export function bothReleased(first, second) {
|
|
41
|
+
const both = Promise.all([first(), second()]);
|
|
42
|
+
both.catch(() => {});
|
|
43
|
+
return both;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A workspace grant requested now, for a release handle needed before it is
|
|
48
|
+
* answered: the release, whenever called, follows the grant (and releases
|
|
49
|
+
* nothing if it was refused or failed).
|
|
50
|
+
*/
|
|
51
|
+
export function pendingWorkspaceAdmission(granting) {
|
|
52
|
+
granting.catch(() => {});
|
|
53
|
+
return () =>
|
|
54
|
+
granting.then(
|
|
55
|
+
(leave) => leave?.(),
|
|
56
|
+
() => {}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
/** Transfer a foreground reader to a surviving-descendant task without a lease gap. */
|
|
37
61
|
export function adoptShellWorkspaceAdmission(jobId, leave) {
|
|
38
62
|
const prior = workspaceLeases.get(jobId);
|
|
39
|
-
workspaceLeases.set(
|
|
40
|
-
jobId,
|
|
41
|
-
prior
|
|
42
|
-
? () => {
|
|
43
|
-
prior();
|
|
44
|
-
leave();
|
|
45
|
-
}
|
|
46
|
-
: leave
|
|
47
|
-
);
|
|
63
|
+
workspaceLeases.set(jobId, prior ? () => bothReleased(prior, leave) : leave);
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
// A record whose owner process is gone is garbage. Age alone never deletes a
|
|
@@ -88,8 +104,13 @@ function validJobId(jobId) {
|
|
|
88
104
|
|
|
89
105
|
function terminalRecord(base, detail) {
|
|
90
106
|
const finishedAt = detail?.finishedAt || new Date().toISOString();
|
|
107
|
+
// A completed record no longer claims an unconfirmed termination: it is
|
|
108
|
+
// written once the processes are confirmed gone, or explicitly released.
|
|
109
|
+
// A completed record no longer claims an unconfirmed termination: it is
|
|
110
|
+
// written once the processes are confirmed gone, or explicitly released.
|
|
111
|
+
const { terminationUnconfirmed: _held, ...open } = base || {};
|
|
91
112
|
return {
|
|
92
|
-
...
|
|
113
|
+
...open,
|
|
93
114
|
jobId: String(detail?.jobId || base?.jobId || ''),
|
|
94
115
|
kind: 'bash',
|
|
95
116
|
status: String(detail?.status || 'failed'),
|
|
@@ -172,7 +193,14 @@ async function sweepOrphanRecords(dir) {
|
|
|
172
193
|
}
|
|
173
194
|
}
|
|
174
195
|
|
|
175
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Publish the record for a job that has just started or been promoted. Its
|
|
198
|
+
* workspace lease (under the job's pid) is requested at the call and adopted
|
|
199
|
+
* under the job id at once, so a later release of the job — or of a
|
|
200
|
+
* predecessor's lease — is ordered after it. Resolves once the lease is in
|
|
201
|
+
* both ledger copies; rejects (and writes no record) when it is refused or
|
|
202
|
+
* cannot be written. The returned promise never goes unhandled.
|
|
203
|
+
*/
|
|
176
204
|
export function publishShellJobRecord(
|
|
177
205
|
task,
|
|
178
206
|
{
|
|
@@ -184,16 +212,20 @@ export function publishShellJobRecord(
|
|
|
184
212
|
} = {}
|
|
185
213
|
) {
|
|
186
214
|
const jobId = String(task?.jobId || '').trim();
|
|
187
|
-
if (!jobId) return;
|
|
215
|
+
if (!jobId) return Promise.resolve();
|
|
216
|
+
let admitted = Promise.resolve();
|
|
188
217
|
if (task?.cwd) {
|
|
189
|
-
const
|
|
218
|
+
const granting = tryAdmitWorkspaceActivityAsync(ownerSessionId || jobId, task.cwd, {
|
|
190
219
|
pid: Number(task.pid) || process.pid,
|
|
191
220
|
kind: 'command',
|
|
192
221
|
});
|
|
193
|
-
|
|
222
|
+
admitted = granting.then((leave) => {
|
|
223
|
+
if (!leave) throw worktreeError('WORKTREE_BUSY_JOBS', 'The command folder is being managed.');
|
|
224
|
+
});
|
|
225
|
+
admitted.catch(() => {});
|
|
194
226
|
// A descendant handoff can reuse the job id with a different live pid.
|
|
195
|
-
//
|
|
196
|
-
adoptShellWorkspaceAdmission(jobId,
|
|
227
|
+
// Its reader is requested before any predecessor's admission is released.
|
|
228
|
+
adoptShellWorkspaceAdmission(jobId, pendingWorkspaceAdmission(granting));
|
|
197
229
|
}
|
|
198
230
|
const ownerPid = Number(clientHostPid ?? task?.clientHostPid) || process.pid;
|
|
199
231
|
const dir = jobsDir();
|
|
@@ -202,6 +234,8 @@ export function publishShellJobRecord(
|
|
|
202
234
|
paths.set(jobId, [jsonPath, ownerPath]);
|
|
203
235
|
void enqueue(jobId, async () => {
|
|
204
236
|
try {
|
|
237
|
+
// No record for a job whose workspace lease was not granted.
|
|
238
|
+
await admitted;
|
|
205
239
|
await mkdir(dir, { recursive: true });
|
|
206
240
|
if (!sweepStarted) {
|
|
207
241
|
sweepStarted = true;
|
|
@@ -244,6 +278,7 @@ export function publishShellJobRecord(
|
|
|
244
278
|
/* the record is presentation only; never fail a job for it */
|
|
245
279
|
}
|
|
246
280
|
});
|
|
281
|
+
return admitted;
|
|
247
282
|
}
|
|
248
283
|
|
|
249
284
|
/** Preserve a bounded terminal snapshot for manual recovery after restart. */
|
|
@@ -335,8 +370,9 @@ export function releaseShellJobWorkspace(jobId) {
|
|
|
335
370
|
const key = String(jobId || '').trim();
|
|
336
371
|
const leave = workspaceLeases.get(key);
|
|
337
372
|
if (!leave) return;
|
|
338
|
-
leave();
|
|
373
|
+
const released = leave();
|
|
339
374
|
if (workspaceLeases.get(key) === leave) workspaceLeases.delete(key);
|
|
375
|
+
return released;
|
|
340
376
|
}
|
|
341
377
|
|
|
342
378
|
/** Flag a still-open job record as "termination unconfirmed" for the readouts. */
|
|
@@ -390,8 +426,10 @@ export function completeShellJobRecord(jobId, detail) {
|
|
|
390
426
|
// Callers complete a record only once its processes are confirmed gone
|
|
391
427
|
// (or explicitly released): the workspace admission follows that, not
|
|
392
428
|
// the record write, so a failed write cannot strand it with no handle.
|
|
393
|
-
leaveWorkspace?.();
|
|
429
|
+
const released = leaveWorkspace?.();
|
|
394
430
|
if (workspaceLeases.get(key) === leaveWorkspace) workspaceLeases.delete(key);
|
|
431
|
+
// Fails only when workspace admission stopped (reported there), which keeps the folder held.
|
|
432
|
+
await Promise.resolve(released).catch(() => {});
|
|
395
433
|
}
|
|
396
434
|
await removePaths(ownerPaths);
|
|
397
435
|
paths.set(key, [jsonPath, donePath]);
|
|
@@ -486,6 +524,6 @@ export function retireShellJobRecord(jobId) {
|
|
|
486
524
|
records.delete(key);
|
|
487
525
|
incarnations.delete(key);
|
|
488
526
|
await removePaths(list);
|
|
489
|
-
leaveWorkspace?.();
|
|
527
|
+
await leaveWorkspace?.();
|
|
490
528
|
});
|
|
491
529
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { readdir as readdirAsync } from 'node:fs/promises';
|
|
4
|
+
import { startGitSync } from '../../../../shared/git-launch.mjs';
|
|
4
5
|
import { runGitOffThread } from '../../../../shared/git-runner.mjs';
|
|
5
6
|
import { findPathExecutable } from '../../../../shared/path-executable.mjs';
|
|
6
7
|
import { dirname as pathDirname, join as pathJoin, resolve as pathResolveAbsolute } from 'node:path';
|
|
@@ -161,13 +162,15 @@ function _changeStateArgs(root) {
|
|
|
161
162
|
|
|
162
163
|
function _probeRepositoryChangeStateSync(root) {
|
|
163
164
|
try {
|
|
164
|
-
const result =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
const result = startGitSync({ ...process.env, GIT_OPTIONAL_LOCKS: '0' }, (command, env) =>
|
|
166
|
+
spawnSync(command, _changeStateArgs(root), {
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
env,
|
|
169
|
+
maxBuffer: 64 * 1024,
|
|
170
|
+
timeout: 1500,
|
|
171
|
+
windowsHide: true,
|
|
172
|
+
})
|
|
173
|
+
);
|
|
171
174
|
const output = String(result.stdout || '').trim();
|
|
172
175
|
if (result.status === 0) return output ? 'changes present' : 'clean';
|
|
173
176
|
if (result.error?.code === 'ENOBUFS' && output) return 'changes present';
|
|
@@ -271,14 +274,16 @@ function _checkIgnoreArgs(directory) {
|
|
|
271
274
|
|
|
272
275
|
function _probeGitIgnoredEntriesSync(directory, names) {
|
|
273
276
|
try {
|
|
274
|
-
const result =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
277
|
+
const result = startGitSync({ ...process.env, GIT_OPTIONAL_LOCKS: '0' }, (command, env) =>
|
|
278
|
+
spawnSync(command, _checkIgnoreArgs(directory), {
|
|
279
|
+
encoding: 'utf8',
|
|
280
|
+
input: `${names.join('\0')}\0`,
|
|
281
|
+
env,
|
|
282
|
+
maxBuffer: 1024 * 1024,
|
|
283
|
+
timeout: 1500,
|
|
284
|
+
windowsHide: true,
|
|
285
|
+
})
|
|
286
|
+
);
|
|
282
287
|
// Exit 1 is "nothing ignored", not a failure.
|
|
283
288
|
if (result.status !== 0 && result.status !== 1) return new Set();
|
|
284
289
|
return new Set(
|