mixdog 1.3.0 → 1.3.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/NOTICE.md +52 -9
- package/README.md +8 -4
- package/package.json +2 -2
- package/scripts/native-release-status.mjs +238 -0
- package/src/defaults/shipped-history.json +8 -1
- package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
- package/src/defaults/skills/setup/SKILL.md +23 -1
- package/src/defaults/skills/setup/references/actions.md +54 -4
- package/src/defaults/skills/setup/references/surfaces.md +23 -3
- package/src/defaults/skills/setup-guide/SKILL.md +17 -10
- package/src/defaults/skills/visualize/SKILL.md +9 -6
- package/src/defaults/skills/visualize/references/craft.md +8 -1
- package/src/help.mjs +1 -1
- package/src/lib/keychain-cjs.cjs +4 -1
- package/src/lib/rules-builder.cjs +14 -7
- package/src/output-styles/common.md +13 -8
- package/src/rules/agent-mode/AGENT_MODE.md +9 -6
- package/src/rules/agent-mode/MIXY.md +11 -6
- package/src/rules/lead/LEAD.md +2 -2
- package/src/rules/shared/10-tool-workflow.md +4 -0
- package/src/rules/shared/20-research.md +2 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
- package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
- package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
- package/src/runtime/agent/orchestrator/config.mjs +11 -0
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +81 -16
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -4
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
- package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +34 -16
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
- package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
- package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +5 -5
- package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
- package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +17 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +41 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +6 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +21 -2
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +149 -45
- package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
- package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
- package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
- package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +156 -25
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
- package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
- package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +111 -9
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -14
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
- package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
- package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
- package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +62 -47
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +6 -8
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +35 -4
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +179 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
- package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +49 -14
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +134 -17
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
- package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
- package/src/runtime/attachments/attachment-reference.mjs +155 -0
- package/src/runtime/attachments/limits.mjs +5 -1
- package/src/runtime/attachments/office-extract.mjs +3 -6
- package/src/runtime/attachments/pdf-extract.mjs +84 -10
- package/src/runtime/attachments/store.mjs +55 -30
- package/src/runtime/browser-bridge/client.mjs +73 -20
- package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +14 -7
- package/src/runtime/channels/lib/state-file.mjs +15 -15
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
- package/src/runtime/channels/lib/worker-main.mjs +3 -2
- package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
- package/src/runtime/effort-judge/judge-client.mjs +6 -3
- package/src/runtime/effort-judge/model-install.mjs +16 -8
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/local-provider/asset-installer.mjs +10 -4
- package/src/runtime/local-provider/catalog.mjs +5 -2
- package/src/runtime/local-provider/registered-models.mjs +64 -27
- package/src/runtime/local-provider/server.mjs +2 -3
- package/src/runtime/media/adapters/image-api.mjs +89 -0
- package/src/runtime/media/auth.mjs +64 -1
- package/src/runtime/media/catalog.mjs +66 -4
- package/src/runtime/media/jobs.mjs +4 -0
- package/src/runtime/media/lanes.mjs +46 -5
- package/src/runtime/media/media-usage.mjs +17 -0
- package/src/runtime/media/renditions.mjs +15 -15
- package/src/runtime/media/store.mjs +36 -35
- package/src/runtime/media/tool.mjs +8 -1
- package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
- package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
- package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
- package/src/runtime/memory/lib/pg/process.mjs +8 -4
- package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
- package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
- package/src/runtime/office/core/journal.mjs +7 -14
- package/src/runtime/office/design/library/design-library-core.mjs +4 -16
- package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
- package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
- package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
- package/src/runtime/office/pdf/pdf-render.mjs +30 -18
- package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
- package/src/runtime/office/portable/portable-soffice.mjs +4 -0
- package/src/runtime/shared/agent-kind-migration.mjs +200 -0
- package/src/runtime/shared/agent-kind.mjs +152 -0
- package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
- package/src/runtime/shared/agent-message-envelope.mjs +44 -0
- package/src/runtime/shared/agent-reap-state.d.mts +4 -0
- package/src/runtime/shared/agent-route-config.d.mts +1 -0
- package/src/runtime/shared/agent-route-config.mjs +22 -8
- package/src/runtime/shared/agent-store.mjs +149 -0
- package/src/runtime/shared/agent-task-starts.mjs +27 -0
- package/src/runtime/shared/atomic-file.d.mts +50 -0
- package/src/runtime/shared/atomic-file.mjs +394 -132
- package/src/runtime/shared/background-tasks.mjs +7 -4
- package/src/runtime/shared/bot-hops.mjs +30 -0
- package/src/runtime/shared/bot-split-migration.mjs +457 -0
- package/src/runtime/shared/bounded-download.mjs +4 -2
- package/src/runtime/shared/config-patch.mjs +20 -1
- package/src/runtime/shared/config.mjs +39 -9
- package/src/runtime/shared/connector-catalog.d.mts +1 -2
- package/src/runtime/shared/connector-catalog.mjs +2 -4
- package/src/runtime/shared/connector-triggers.mjs +2 -0
- package/src/runtime/shared/defaults-separation.mjs +6 -2
- package/src/runtime/shared/delegated-approval-host.mjs +74 -0
- package/src/runtime/shared/err-text.mjs +7 -1
- package/src/runtime/shared/error-presentation.mjs +113 -10
- package/src/runtime/shared/file-has-data.mjs +28 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
- package/src/runtime/shared/handoff-artifacts.mjs +61 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
- package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
- package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
- package/src/runtime/shared/model-route-fallback.mjs +79 -7
- package/src/runtime/shared/pending-agent-work.mjs +54 -0
- package/src/runtime/shared/plain-preview.mjs +22 -17
- package/src/runtime/shared/process-lifecycle.mjs +8 -12
- package/src/runtime/shared/projects.mjs +50 -41
- package/src/runtime/shared/provider-accounts.mjs +17 -12
- package/src/runtime/shared/routine-trigger.d.mts +6 -1
- package/src/runtime/shared/routine-trigger.mjs +14 -6
- package/src/runtime/shared/run-child.d.mts +3 -0
- package/src/runtime/shared/run-child.mjs +24 -3
- package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
- package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
- package/src/runtime/shared/session-baseline-store.mjs +8 -2
- package/src/runtime/shared/singleton-owner.mjs +7 -6
- package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
- package/src/runtime/shared/staged-update.mjs +4 -2
- package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
- package/src/runtime/shared/tool-execution-contract.mjs +6 -0
- package/src/runtime/shared/transcript-metadata.mjs +28 -3
- package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
- package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
- package/src/runtime/shared/turn-snapshot.mjs +44 -2
- package/src/runtime/shared/usage-readout-totals.mjs +15 -13
- package/src/runtime/shared/user-cwd.mjs +9 -4
- package/src/runtime/shared/user-data-guard.mjs +46 -44
- package/src/runtime/shared/utf8-cut.mjs +10 -0
- package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
- package/src/runtime/shared/worktrees/registry.mjs +26 -26
- package/src/runtime/tidy/core-install.mjs +26 -4
- package/src/runtime/tidy/install.mjs +91 -23
- package/src/runtime/web-search/lib/config.mjs +4 -16
- package/src/session-runtime/agent-home-delivery.mjs +859 -136
- package/src/session-runtime/agent-room-delivery.mjs +167 -19
- package/src/session-runtime/agent-task-sessions.mjs +831 -69
- package/src/session-runtime/agent-tool-routing.mjs +1 -0
- package/src/session-runtime/boot/apis.mjs +58 -24
- package/src/session-runtime/boot/config.mjs +1 -0
- package/src/session-runtime/boot/facade.mjs +11 -1
- package/src/session-runtime/boot/infrastructure.mjs +14 -2
- package/src/session-runtime/boot/providers.mjs +16 -6
- package/src/session-runtime/boot/shared.mjs +12 -6
- package/src/session-runtime/boot/tools.mjs +2 -2
- package/src/session-runtime/bot-status.mjs +432 -0
- package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
- package/src/session-runtime/config-lifecycle.mjs +78 -32
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
- package/src/session-runtime/handoff-attachments.mjs +593 -0
- package/src/session-runtime/held-requests.mjs +36 -33
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +45 -6
- package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
- package/src/session-runtime/lifecycle/teardown.mjs +11 -3
- package/src/session-runtime/mcp-glue/connect.mjs +100 -186
- package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
- package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
- package/src/session-runtime/media-api.mjs +5 -2
- package/src/session-runtime/model-route/route-persist.mjs +2 -2
- package/src/session-runtime/model-route/route-tuning.mjs +11 -2
- package/src/session-runtime/model-route/set-route.mjs +82 -2
- package/src/session-runtime/native-web-search.mjs +2 -2
- package/src/session-runtime/new-session-config.mjs +6 -1
- package/src/session-runtime/owned-automation-run.mjs +8 -1
- package/src/session-runtime/room-member-gate.mjs +16 -3
- package/src/session-runtime/route-resolution.mjs +21 -19
- package/src/session-runtime/route-state.mjs +1 -59
- package/src/session-runtime/routine-home-turn.mjs +5 -3
- package/src/session-runtime/runtime-bootstrap.mjs +5 -3
- package/src/session-runtime/runtime-paths.mjs +11 -5
- package/src/session-runtime/runtime-tunables.mjs +6 -0
- package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
- package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
- package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
- package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
- package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
- package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +36 -24
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +12 -6
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
- package/src/session-runtime/services/agent-tool.mjs +6 -0
- package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
- package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
- package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
- package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
- package/src/session-runtime/services/plugin-admin.mjs +26 -26
- package/src/session-runtime/services/provider-admin.mjs +14 -4
- package/src/session-runtime/services/schedule-access.mjs +33 -4
- package/src/session-runtime/services/seeds.mjs +4 -3
- package/src/session-runtime/services/usage-dashboard.mjs +1 -1
- package/src/session-runtime/services/usage-stats-model.mjs +2 -1
- package/src/session-runtime/session-create/materialize.mjs +2 -3
- package/src/session-runtime/session-create/session-options.mjs +15 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
- package/src/session-runtime/settings-compaction-api.mjs +86 -39
- package/src/session-runtime/setup-tool/executor.mjs +20 -3
- package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
- package/src/session-runtime/setup-tool/settings-contract.mjs +37 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
- package/src/session-runtime/skills-api.mjs +12 -5
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
- package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
- package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
- package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
- package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
- package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
- package/src/session-runtime/tool-policy-refresh.mjs +47 -358
- package/src/session-runtime/tool-surface.mjs +5 -3
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +99 -81
- package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
- package/src/session-runtime/turn/turn-run.mjs +2 -1
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +578 -301
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +113 -53
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
- package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
- package/src/session-runtime/workspace/workspace-state.mjs +7 -6
- package/src/standalone/daemon-agent-control.mjs +4 -1
- package/src/standalone/daemon-session-control.mjs +60 -11
- package/src/standalone/daemon.mjs +34 -4
- package/src/standalone/local-session-runtime.mjs +22 -14
- package/src/standalone/session-client.mjs +7 -2
- package/src/standalone/session-protocol.mjs +7 -0
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
- package/src/standalone/session-service/agent-tree.mjs +4 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
- package/src/standalone/session-service.mjs +11 -0
- package/src/standalone/session-state-projection.mjs +19 -9
- package/src/standalone/session-tool-output.mjs +35 -9
- package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
- package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
- package/src/tui/app/slash-commands.mjs +1 -1
- package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
- package/src/tui/dist/index.mjs +599 -342
- package/src/tui/prompt-history-store.mjs +16 -21
- package/src/tui/session/context-state.mjs +4 -0
- package/src/tui/session/notice-surface.mjs +3 -4
- package/src/tui/session/session-api/intake/abort.mjs +19 -1
- package/src/tui/session/session-api/media.mjs +2 -1
- package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
- package/src/tui/session/session-api/routes-write.mjs +203 -0
- package/src/tui/session/session-api/routes.mjs +23 -236
- package/src/tui/session/session-api/settings.mjs +14 -1
- package/src/tui/session/session-flow/drain.mjs +28 -3
- package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
- package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
- package/src/tui/session/session-flow/steering.mjs +8 -3
- package/src/tui/session/tool-approval.mjs +185 -20
- package/src/tui/session/transcript-intake.mjs +4 -0
- package/src/tui/session/turn-live-call.mjs +11 -3
- package/src/tui/session/turn.mjs +36 -17
- package/src/tui/transcript-tool-failures.mjs +8 -2
- package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
- package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
- package/src/rules/agent-mode/ROUTINE.md +0 -6
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
23
|
-
import { readFileSync, statSync
|
|
23
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
24
24
|
import { join, resolve } from 'node:path';
|
|
25
25
|
import { homedir } from 'node:os';
|
|
26
|
+
import { holdsNoData, writeFileAtomicSync } from './atomic-file.mjs';
|
|
26
27
|
import { resolvePluginData, mixdogRoot } from './plugin-paths.mjs';
|
|
27
28
|
|
|
28
29
|
const _cwdOverride = new AsyncLocalStorage();
|
|
@@ -112,7 +113,10 @@ export function explicitSessionCwd() {
|
|
|
112
113
|
if (sessionDir) return sessionDir;
|
|
113
114
|
}
|
|
114
115
|
try {
|
|
115
|
-
const
|
|
116
|
+
const raw = readFileSync(_dataFile('user-cwd.txt'), 'utf8');
|
|
117
|
+
// A crash can leave NUL bytes in place of the path: that is no path.
|
|
118
|
+
if (holdsNoData(raw)) return null;
|
|
119
|
+
const txt = raw.trim();
|
|
116
120
|
return (txt && _normalizePlatformCwd(txt)) || null;
|
|
117
121
|
} catch {
|
|
118
122
|
return null;
|
|
@@ -173,7 +177,7 @@ function _lastSessionCwdFile(keyPid) {
|
|
|
173
177
|
*/
|
|
174
178
|
export function writeLastSessionCwd(cwd, keyPid) {
|
|
175
179
|
try {
|
|
176
|
-
|
|
180
|
+
writeFileAtomicSync(_lastSessionCwdFile(keyPid), String(cwd));
|
|
177
181
|
} catch {
|
|
178
182
|
/* best-effort */
|
|
179
183
|
}
|
|
@@ -188,7 +192,8 @@ export function writeLastSessionCwd(cwd, keyPid) {
|
|
|
188
192
|
*/
|
|
189
193
|
export function readLastSessionCwd(keyPid) {
|
|
190
194
|
try {
|
|
191
|
-
|
|
195
|
+
const raw = readFileSync(_lastSessionCwdFile(keyPid), 'utf8');
|
|
196
|
+
return holdsNoData(raw) ? null : _existingDirectory(raw.trim());
|
|
192
197
|
} catch {
|
|
193
198
|
return null;
|
|
194
199
|
}
|
|
@@ -1,23 +1,15 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
1
2
|
import {
|
|
2
|
-
copyFileSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readdirSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
rmSync,
|
|
8
|
-
statSync,
|
|
9
|
-
writeFileSync,
|
|
10
|
-
} from 'node:fs';
|
|
11
|
-
import {
|
|
12
|
-
copyFile as copyFileP,
|
|
13
3
|
mkdir as mkdirP,
|
|
4
|
+
readFile as readFileP,
|
|
14
5
|
readdir as readdirP,
|
|
15
6
|
rm as rmP,
|
|
16
7
|
stat as statP,
|
|
17
8
|
} from 'node:fs/promises';
|
|
18
|
-
import { basename,
|
|
9
|
+
import { basename, join, resolve } from 'node:path';
|
|
19
10
|
import { homedir } from 'node:os';
|
|
20
11
|
import { createHash, randomUUID } from 'node:crypto';
|
|
12
|
+
import { writeFileAtomicAsync, writeFileAtomicSync, writeJsonAtomicSync } from './atomic-file.mjs';
|
|
21
13
|
import { isPlainObject } from './object.mjs';
|
|
22
14
|
import { mixdogHome } from './plugin-paths.mjs';
|
|
23
15
|
|
|
@@ -155,15 +147,27 @@ function safeReason(reason) {
|
|
|
155
147
|
);
|
|
156
148
|
}
|
|
157
149
|
|
|
158
|
-
/** instanceId of this data dir's init marker; null when absent
|
|
150
|
+
/** instanceId of this data dir's init marker; null only when absent. */
|
|
159
151
|
function readInitInstanceId(dataDir) {
|
|
160
152
|
if (!dataDir) return null;
|
|
153
|
+
const path = join(dataDir, INIT_MARKER);
|
|
154
|
+
let raw;
|
|
161
155
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
raw = readFileSync(path, 'utf8');
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error?.code === 'ENOENT') return null;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
let marker;
|
|
162
|
+
try {
|
|
163
|
+
marker = JSON.parse(raw);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new SyntaxError(`[user-data] invalid initialization marker at ${path}`, { cause: error });
|
|
166
|
+
}
|
|
167
|
+
if (typeof marker?.instanceId !== 'string' || !marker.instanceId) {
|
|
168
|
+
throw new SyntaxError(`[user-data] initialization marker at ${path} must have a non-empty instanceId`);
|
|
166
169
|
}
|
|
170
|
+
return marker.instanceId;
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
export function hasUserDataInitMarker(dataDir) {
|
|
@@ -379,8 +383,9 @@ function copyTree(src, dst, copied) {
|
|
|
379
383
|
return;
|
|
380
384
|
}
|
|
381
385
|
if (!st.isFile()) return;
|
|
382
|
-
|
|
383
|
-
|
|
386
|
+
// A copy goes through an fsynced handle: a plain copy can leave a backup that
|
|
387
|
+
// is NUL exactly when a crash makes it needed.
|
|
388
|
+
writeFileAtomicSync(dst, readFileSync(src), { mode: st.mode & 0o777 });
|
|
384
389
|
copied.push(dst);
|
|
385
390
|
}
|
|
386
391
|
|
|
@@ -394,22 +399,20 @@ function pruneBackups(scopeDir, names, own) {
|
|
|
394
399
|
}
|
|
395
400
|
|
|
396
401
|
/**
|
|
397
|
-
* Create
|
|
398
|
-
*
|
|
402
|
+
* Create a missing init marker; return its instanceId, or null if the data dir
|
|
403
|
+
* is gone or the missing marker cannot be created. Never creates the data dir.
|
|
404
|
+
* Read/validation failures throw; an existing marker is never replaced.
|
|
399
405
|
*/
|
|
400
406
|
export function markUserDataInitialized(dataDir) {
|
|
401
407
|
const existing = readInitInstanceId(dataDir);
|
|
402
408
|
if (existing || !dataDir || !existsSync(dataDir)) return existing;
|
|
403
409
|
try {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
)}\n`,
|
|
411
|
-
// An unreadable marker is replaced; a concurrent creator's valid one wins.
|
|
412
|
-
{ encoding: 'utf8', flag: existsSync(join(dataDir, INIT_MARKER)) ? 'w' : 'wx' }
|
|
410
|
+
const marker = join(dataDir, INIT_MARKER);
|
|
411
|
+
writeJsonAtomicSync(
|
|
412
|
+
marker,
|
|
413
|
+
{ instanceId: randomUUID(), dataDir: resolve(dataDir), createdAt: new Date().toISOString() },
|
|
414
|
+
// Another creator may have published since the absent read; it wins.
|
|
415
|
+
{ createOnly: true }
|
|
413
416
|
);
|
|
414
417
|
} catch {}
|
|
415
418
|
return readInitInstanceId(dataDir);
|
|
@@ -427,8 +430,8 @@ export function ensureUserDataInitialized(dataDir) {
|
|
|
427
430
|
if (!dataDir) return;
|
|
428
431
|
const key = normalizeDataDir(dataDir);
|
|
429
432
|
if (ensuredDataDirs.has(key)) return;
|
|
430
|
-
ensuredDataDirs.add(key);
|
|
431
433
|
if (!hasUserDataInitMarker(dataDir)) backupUserData(dataDir, 'initial-snapshot');
|
|
434
|
+
ensuredDataDirs.add(key);
|
|
432
435
|
}
|
|
433
436
|
|
|
434
437
|
function skipBackups() {
|
|
@@ -439,20 +442,20 @@ function skipBackups() {
|
|
|
439
442
|
// manifest goes last, so an interrupted copy is never restorable.
|
|
440
443
|
function finishBackup(dataDir, backupDir, reason) {
|
|
441
444
|
const instanceId = markUserDataInitialized(dataDir);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
)}\n`,
|
|
449
|
-
'utf8'
|
|
450
|
-
);
|
|
445
|
+
writeJsonAtomicSync(join(backupDir, BACKUP_MANIFEST), {
|
|
446
|
+
dataDir: resolve(dataDir),
|
|
447
|
+
instanceId,
|
|
448
|
+
reason,
|
|
449
|
+
createdAt: new Date().toISOString(),
|
|
450
|
+
});
|
|
451
451
|
restoreLookupCache.delete(normalizeDataDir(dataDir));
|
|
452
452
|
legacyEvidenceCache.delete(normalizeDataDir(dataDir));
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
|
|
455
|
+
/** `dirs`: more data-dir folders this one snapshot also carries (a one-time
|
|
456
|
+
* migration's, e.g. agents/ and bots/); routine snapshots keep to
|
|
457
|
+
* USER_DATA_DIRS so the ring stays small. */
|
|
458
|
+
export function backupUserData(dataDir, reason = 'snapshot', { dirs = [] } = {}) {
|
|
456
459
|
if (skipBackups()) return { dir: null, copied: [] };
|
|
457
460
|
if (!dataDir || !existsSync(dataDir)) return { dir: null, copied: [] };
|
|
458
461
|
const scopeDir = scopeDirFor(dataDir);
|
|
@@ -462,7 +465,7 @@ export function backupUserData(dataDir, reason = 'snapshot') {
|
|
|
462
465
|
const src = join(dataDir, rel);
|
|
463
466
|
if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
|
|
464
467
|
}
|
|
465
|
-
for (const rel of USER_DATA_DIRS) {
|
|
468
|
+
for (const rel of [...USER_DATA_DIRS, ...dirs]) {
|
|
466
469
|
const src = join(dataDir, rel);
|
|
467
470
|
if (existsSync(src)) copyTree(src, join(backupDir, rel), copied);
|
|
468
471
|
}
|
|
@@ -491,8 +494,7 @@ async function copyTreeAsync(src, dst, copied) {
|
|
|
491
494
|
return;
|
|
492
495
|
}
|
|
493
496
|
if (!st.isFile()) return;
|
|
494
|
-
await
|
|
495
|
-
await copyFileP(src, dst);
|
|
497
|
+
await writeFileAtomicAsync(dst, await readFileP(src), { mode: st.mode & 0o777 });
|
|
496
498
|
copied.push(dst);
|
|
497
499
|
}
|
|
498
500
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text` without its trailing U+FFFD characters: what a byte cut through a
|
|
3
|
+
* multi-byte sequence leaves behind. A loop, not `/\uFFFD+$/`, which is
|
|
4
|
+
* quadratic on a long run of them.
|
|
5
|
+
*/
|
|
6
|
+
export function stripTrailingReplacementChars(text) {
|
|
7
|
+
let end = text.length;
|
|
8
|
+
while (end > 0 && text[end - 1] === '\uFFFD') end -= 1;
|
|
9
|
+
return end === text.length ? text : text.slice(0, end);
|
|
10
|
+
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
-
import { readFileSync, renameSync } from 'node:fs';
|
|
4
4
|
import { isAbsolute, join } from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
parseJsonText,
|
|
7
|
+
quarantinePath,
|
|
8
|
+
readJsonStateSync,
|
|
9
|
+
unsupportedState,
|
|
10
|
+
updateJsonAtomicSync,
|
|
11
|
+
writeFileAtomicSync,
|
|
12
|
+
writeJsonAtomicSync,
|
|
13
|
+
} from '../atomic-file.mjs';
|
|
6
14
|
import { isPidAliveOrUnknown } from '../pid-liveness.mjs';
|
|
7
15
|
import { findGitLayout } from './git-head.mjs';
|
|
8
16
|
import { canonicalPath, isWithin, samePath } from './paths.mjs';
|
|
9
17
|
import { worktreeError, worktreeRefusal } from './contract.mjs';
|
|
10
|
-
import {
|
|
18
|
+
import { readRegistrySync } from './registry.mjs';
|
|
11
19
|
|
|
12
20
|
const FILE = 'mixdog-workspace-admission.json';
|
|
13
21
|
const overlaps = (a, b) => isWithin(a, b) || isWithin(b, a);
|
|
@@ -24,91 +32,273 @@ export function isExternalCheckout(folder) {
|
|
|
24
32
|
if (!layout || samePath(layout.gitDir, layout.commonDir)) return false;
|
|
25
33
|
let entries;
|
|
26
34
|
try {
|
|
27
|
-
entries =
|
|
28
|
-
} catch
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
entries = readRegistrySync().entries;
|
|
36
|
+
} catch {
|
|
37
|
+
// A damaged registry is kept aside and reads as a restored or empty one;
|
|
38
|
+
// only a real I/O failure (access denied, in use) lands here.
|
|
39
|
+
throw worktreeRefusal('workspace-admission-unreadable');
|
|
31
40
|
}
|
|
32
41
|
return !Object.values(entries).some(
|
|
33
42
|
(record) => typeof record?.path === 'string' && samePath(record.path, layout.topLevel)
|
|
34
43
|
);
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
const VERSION = 1;
|
|
47
|
+
const UNRECORDED = Symbol('unrecorded');
|
|
48
|
+
const emptyLedger = () => ({ version: VERSION, leases: [] });
|
|
49
|
+
const isNewer = (state) => Number.isSafeInteger(state?.version) && state.version > VERSION;
|
|
50
|
+
const isLease = (lease) =>
|
|
51
|
+
!!lease &&
|
|
52
|
+
['turn', 'command', 'management'].includes(lease.kind) &&
|
|
53
|
+
typeof lease.token === 'string' &&
|
|
54
|
+
Number.isSafeInteger(lease.pid) &&
|
|
55
|
+
lease.pid > 0 &&
|
|
56
|
+
typeof lease.path === 'string' &&
|
|
57
|
+
isAbsolute(lease.path);
|
|
58
|
+
const hasLedgerShape = (state) => state?.version === VERSION && Array.isArray(state.leases);
|
|
59
|
+
|
|
60
|
+
// The whole-file check: a newer build's ledger is intact data in a format this
|
|
61
|
+
// build cannot honour (left in place), anything else off-shape is damage. One
|
|
62
|
+
// bad lease record is not a damaged file; mutate() drops just that record.
|
|
63
|
+
function validateLedger(state) {
|
|
64
|
+
if (isNewer(state)) throw unsupportedState('Workspace admission ledger was written by a newer build.');
|
|
65
|
+
if (!hasLedgerShape(state)) throw new TypeError('Workspace admission ledger is malformed.');
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const refuseUnreadable = () => {
|
|
70
|
+
throw worktreeRefusal('workspace-admission-unreadable');
|
|
71
|
+
};
|
|
72
|
+
const envMs = (name, fallback) => {
|
|
73
|
+
const value = Number(process.env[name]);
|
|
74
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
75
|
+
};
|
|
76
|
+
// How long the leases of a recovered ledger stay unknown. Every process
|
|
77
|
+
// holding a lease republishes it within one heal period, so after this window
|
|
78
|
+
// the ledger is whole again and destructive operations resume on their own.
|
|
79
|
+
const recoveryWindowMs = () => envMs('MIXDOG_ADMISSION_RECOVERY_MS', 15_000);
|
|
80
|
+
const healIntervalMs = () => envMs('MIXDOG_ADMISSION_HEAL_MS', 2_000);
|
|
81
|
+
|
|
82
|
+
// A path that cannot be read at all (access denied, in use, a directory) is an
|
|
83
|
+
// I/O failure, not damage: it refuses everything and is never touched.
|
|
84
|
+
function assertLedgerReadable(path) {
|
|
85
|
+
try {
|
|
86
|
+
readFileSync(path);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error?.code !== 'ENOENT') throw worktreeRefusal('workspace-admission-unreadable');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
37
92
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
93
|
+
* A damaged ledger (NUL, truncated, malformed) must not block every prompt and
|
|
94
|
+
* shell command of the workspace, nor may it make a destructive operation fail
|
|
95
|
+
* open. Called under the ledger lock: its bytes are COPIED aside as
|
|
96
|
+
* `<ledger>.corrupt-<ts>` and the same locked update replaces it with a ledger
|
|
97
|
+
* carrying `recoveredAt`. The ledger is never moved away: a missing ledger
|
|
98
|
+
* reads as "no leases", so any gap between the move and the publish (another
|
|
99
|
+
* process taking the lock, or a crash) would admit management beside a live
|
|
100
|
+
* foreign turn. A crash before the publish leaves the damaged file in place,
|
|
101
|
+
* which the next writer recovers the same way. If the copy cannot be kept the
|
|
102
|
+
* ledger is left untouched and everything refuses.
|
|
47
103
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
104
|
+
function keepDamagedAside(path) {
|
|
105
|
+
const aside = quarantinePath(path);
|
|
50
106
|
try {
|
|
51
|
-
|
|
52
|
-
} catch
|
|
53
|
-
if (error?.code === 'ENOENT') return { version: 1, leases: [] };
|
|
107
|
+
writeFileAtomicSync(aside, readFileSync(path));
|
|
108
|
+
} catch {
|
|
54
109
|
throw worktreeRefusal('workspace-admission-unreadable');
|
|
55
110
|
}
|
|
111
|
+
process.stderr.write(`[state] ${path} is damaged; kept as ${aside}\n`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function keepAside(path, value) {
|
|
115
|
+
const aside = quarantinePath(path);
|
|
116
|
+
try {
|
|
117
|
+
writeFileAtomicSync(aside, JSON.stringify(value));
|
|
118
|
+
process.stderr.write(`[state] ${path} held invalid lease records; kept as ${aside}\n`);
|
|
119
|
+
} catch {
|
|
120
|
+
// Best effort: the valid records carry on either way.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const aliveLease = (lease) => isPidAliveOrUnknown(lease.pid);
|
|
125
|
+
|
|
126
|
+
// Management leases live in a mirror file as well as the ledger: damage to
|
|
127
|
+
// one never hides an operation in progress from the readers.
|
|
128
|
+
function readMirror(mirror) {
|
|
56
129
|
try {
|
|
57
|
-
|
|
130
|
+
const state = readJsonStateSync(mirror, { validate: validateLedger, strictOwner: true });
|
|
131
|
+
return (state?.leases ?? []).filter((lease) => isLease(lease) && lease.kind === 'management' && aliveLease(lease));
|
|
58
132
|
} catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function writeMirror(mirror, leases) {
|
|
138
|
+
try {
|
|
139
|
+
writeJsonAtomicSync(mirror, { version: VERSION, leases }, { compact: true });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
process.stderr.write(`[state] ${mirror} could not be written (${error?.message})\n`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Leases this process holds, republished into any ledger that lost them.
|
|
146
|
+
const held = new Map();
|
|
147
|
+
let healTimer = null;
|
|
148
|
+
|
|
149
|
+
// A management hold shares one token across its folders, so a lease is
|
|
150
|
+
// identified by token and path.
|
|
151
|
+
const leaseKey = (lease) => `${lease.token}\0${lease.path}`;
|
|
152
|
+
|
|
153
|
+
function track(commonDir, lease) {
|
|
154
|
+
held.set(leaseKey(lease), { commonDir, lease });
|
|
155
|
+
healTimer ??= setInterval(healHeld, healIntervalMs());
|
|
156
|
+
healTimer.unref?.();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function untrack(token) {
|
|
160
|
+
for (const [key, { lease }] of held) if (lease.token === token) held.delete(key);
|
|
161
|
+
if (!held.size && healTimer) {
|
|
162
|
+
clearInterval(healTimer);
|
|
163
|
+
healTimer = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function ledgerLacks(commonDir, keys) {
|
|
168
|
+
let state;
|
|
169
|
+
try {
|
|
170
|
+
state = parseJsonText(readFileSync(join(commonDir, FILE), 'utf8'));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return error?.code === 'ENOENT' || error instanceof SyntaxError;
|
|
173
|
+
}
|
|
174
|
+
if (isNewer(state)) return false;
|
|
175
|
+
if (!hasLedgerShape(state)) return true;
|
|
176
|
+
const stored = new Set(state.leases.filter(Boolean).map(leaseKey));
|
|
177
|
+
return keys.some((key) => !stored.has(key));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function healHeld() {
|
|
181
|
+
const byDir = new Map();
|
|
182
|
+
for (const [key, { commonDir }] of held) byDir.set(commonDir, [...(byDir.get(commonDir) ?? []), key]);
|
|
183
|
+
for (const [commonDir, keys] of byDir) {
|
|
184
|
+
if (!ledgerLacks(commonDir, keys)) continue;
|
|
59
185
|
try {
|
|
60
|
-
|
|
186
|
+
mutate(
|
|
187
|
+
commonDir,
|
|
188
|
+
() => {},
|
|
189
|
+
() => {}
|
|
190
|
+
);
|
|
61
191
|
} catch {
|
|
62
|
-
|
|
192
|
+
// Retried on the next tick.
|
|
63
193
|
}
|
|
64
|
-
return { version: 1, leases: [] };
|
|
65
194
|
}
|
|
66
195
|
}
|
|
67
196
|
|
|
68
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Runs `update(leases, { unknown })` under the ledger lock. `unknown`: the
|
|
199
|
+
* ledger was recovered recently and may be missing other processes' leases.
|
|
200
|
+
* `onNewer` answers for a ledger of a newer build, which is left untouched:
|
|
201
|
+
* it refuses by default; readers pass an answer that admits them unrecorded.
|
|
202
|
+
*/
|
|
203
|
+
function mutate(commonDir, update, onNewer = refuseUnreadable) {
|
|
69
204
|
const path = join(commonDir, FILE);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
205
|
+
const mirror = `${path}.management`;
|
|
206
|
+
assertLedgerReadable(path);
|
|
207
|
+
let result;
|
|
208
|
+
let refusal = null;
|
|
209
|
+
updateJsonAtomicSync(
|
|
210
|
+
path,
|
|
211
|
+
(current) => {
|
|
212
|
+
if (isNewer(current)) {
|
|
213
|
+
result = onNewer();
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
let state;
|
|
218
|
+
let recovered = false;
|
|
219
|
+
if (current === null) {
|
|
220
|
+
state = emptyLedger();
|
|
221
|
+
// `keepWithoutBackup` leaves a ledger whose text does not parse in
|
|
222
|
+
// place, so null with a file present is damage; no file is a new ledger.
|
|
223
|
+
if (existsSync(path)) {
|
|
224
|
+
keepDamagedAside(path);
|
|
225
|
+
recovered = true;
|
|
226
|
+
}
|
|
227
|
+
} else if (!hasLedgerShape(current)) {
|
|
228
|
+
keepAside(path, current);
|
|
229
|
+
state = emptyLedger();
|
|
230
|
+
recovered = true;
|
|
231
|
+
} else {
|
|
232
|
+
state = { ...current, leases: current.leases.filter(isLease) };
|
|
233
|
+
if (state.leases.length !== current.leases.length) {
|
|
234
|
+
keepAside(path, current);
|
|
235
|
+
recovered = true;
|
|
236
|
+
}
|
|
89
237
|
}
|
|
238
|
+
if (recovered) state.recoveredAt = now;
|
|
90
239
|
// Only a pid PROVEN dead (ESRCH) drops its lease, whatever its kind; an
|
|
91
240
|
// unprobeable pid still counts as running. A live runtime keeps a command
|
|
92
241
|
// whose tree it cannot verify reserved under its own (live) pid until the
|
|
93
242
|
// tree is confirmed gone, so a lease that outlives every pid belongs to a
|
|
94
243
|
// restarted runtime that can no longer release it.
|
|
95
|
-
state.leases = state.leases.filter(
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
244
|
+
state.leases = state.leases.filter(aliveLease);
|
|
245
|
+
const present = new Set(state.leases.map(leaseKey));
|
|
246
|
+
const restore = [
|
|
247
|
+
...(recovered ? readMirror(mirror) : []),
|
|
248
|
+
...[...held.values()].filter((own) => own.commonDir === commonDir).map((own) => own.lease),
|
|
249
|
+
];
|
|
250
|
+
for (const lease of restore) {
|
|
251
|
+
if (present.has(leaseKey(lease)) || !aliveLease(lease)) continue;
|
|
252
|
+
present.add(leaseKey(lease));
|
|
253
|
+
state.leases.push(lease);
|
|
254
|
+
}
|
|
255
|
+
const unknown = Number.isFinite(state.recoveredAt) && now - state.recoveredAt < recoveryWindowMs();
|
|
256
|
+
if (!unknown) delete state.recoveredAt;
|
|
257
|
+
const managementTokens = () =>
|
|
258
|
+
state.leases
|
|
259
|
+
.filter((lease) => lease.kind === 'management')
|
|
260
|
+
.map((lease) => lease.token)
|
|
261
|
+
.join();
|
|
262
|
+
const before = managementTokens();
|
|
263
|
+
const unchanged = [...state.leases];
|
|
264
|
+
try {
|
|
265
|
+
result = update(state.leases, { unknown });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// A refusal still publishes the recovery: dropping the write would
|
|
268
|
+
// leave the damaged file, and every retry would recover it again and
|
|
269
|
+
// restart the window, so management would never resume.
|
|
270
|
+
if (!recovered) throw error;
|
|
271
|
+
refusal = error;
|
|
272
|
+
state.leases = unchanged;
|
|
273
|
+
}
|
|
274
|
+
if (recovered || managementTokens() !== before) {
|
|
275
|
+
writeMirror(
|
|
276
|
+
mirror,
|
|
277
|
+
state.leases.filter((lease) => lease.kind === 'management')
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return state;
|
|
99
281
|
},
|
|
100
|
-
|
|
282
|
+
// No backup restore: an older ledger would resurrect leases long released.
|
|
283
|
+
{ strictOwner: true, keepWithoutBackup: true, restoreBackup: false }
|
|
101
284
|
);
|
|
285
|
+
if (refusal) throw refusal;
|
|
286
|
+
return result;
|
|
102
287
|
}
|
|
103
288
|
|
|
104
289
|
function release(commonDir, token) {
|
|
105
290
|
let done = false;
|
|
106
291
|
return () => {
|
|
107
292
|
if (done) return;
|
|
108
|
-
mutate(
|
|
109
|
-
|
|
110
|
-
|
|
293
|
+
mutate(
|
|
294
|
+
commonDir,
|
|
295
|
+
(leases) => {
|
|
296
|
+
for (let i = leases.length - 1; i >= 0; i--) if (leases[i].token === token) leases.splice(i, 1);
|
|
297
|
+
},
|
|
298
|
+
() => {}
|
|
299
|
+
);
|
|
111
300
|
done = true;
|
|
301
|
+
untrack(token);
|
|
112
302
|
};
|
|
113
303
|
}
|
|
114
304
|
|
|
@@ -123,12 +313,22 @@ export function tryAdmitWorkspaceActivity(sessionId, cwd, { pid = process.pid, k
|
|
|
123
313
|
const external = isExternalCheckout(cwd);
|
|
124
314
|
const path = canonicalPath(cwd);
|
|
125
315
|
const token = randomUUID();
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
316
|
+
const lease = { kind, sessionId, pid, token, path };
|
|
317
|
+
const admitted = mutate(
|
|
318
|
+
layout.commonDir,
|
|
319
|
+
(leases) => {
|
|
320
|
+
if (!external && leases.some((other) => other.kind === 'management' && overlaps(other.path, path))) return false;
|
|
321
|
+
leases.push(lease);
|
|
322
|
+
return true;
|
|
323
|
+
},
|
|
324
|
+
// A newer build's ledger cannot be written: the reader goes unrecorded
|
|
325
|
+
// (prompts never wait on it) while management refuses on it.
|
|
326
|
+
() => UNRECORDED
|
|
327
|
+
);
|
|
328
|
+
if (admitted === UNRECORDED) return () => {};
|
|
329
|
+
if (!admitted) return null;
|
|
330
|
+
track(layout.commonDir, lease);
|
|
331
|
+
return release(layout.commonDir, token);
|
|
132
332
|
}
|
|
133
333
|
|
|
134
334
|
export async function admitWorkspaceActivity(sessionId, cwd, signal, options) {
|
|
@@ -166,17 +366,25 @@ export function holdWorkspaceActivity(folders, { sessionId = null } = {}) {
|
|
|
166
366
|
try {
|
|
167
367
|
for (const [commonDir, paths] of [...groups].sort(([a], [b]) => a.localeCompare(b))) {
|
|
168
368
|
const token = randomUUID();
|
|
169
|
-
|
|
369
|
+
const taken = [];
|
|
370
|
+
mutate(commonDir, (leases, { unknown }) => {
|
|
170
371
|
const conflicts = leases.filter(
|
|
171
372
|
(lease) =>
|
|
172
373
|
!(sessionId && lease.sessionId === sessionId && lease.pid === process.pid) &&
|
|
173
374
|
paths.some(({ path }) => overlaps(path, lease.path))
|
|
174
375
|
);
|
|
175
376
|
if (conflicts.length) throw worktreeRefusal('folder-in-use');
|
|
377
|
+
// A recovered ledger may be missing a live foreign turn: wait for the
|
|
378
|
+
// holders to republish (bounded by the recovery window).
|
|
379
|
+
if (unknown) throw worktreeRefusal('workspace-admission-unreadable');
|
|
176
380
|
for (const { path, external } of paths) {
|
|
177
|
-
if (
|
|
381
|
+
if (external) continue;
|
|
382
|
+
const lease = { kind: 'management', pid: process.pid, token, path };
|
|
383
|
+
leases.push(lease);
|
|
384
|
+
taken.push(lease);
|
|
178
385
|
}
|
|
179
386
|
});
|
|
387
|
+
for (const lease of taken) track(commonDir, lease);
|
|
180
388
|
releases.push(release(commonDir, token));
|
|
181
389
|
}
|
|
182
390
|
} catch (error) {
|