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
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
openSync,
|
|
8
8
|
readFileSync,
|
|
9
9
|
renameSync,
|
|
10
|
+
statSync,
|
|
10
11
|
unlinkSync,
|
|
11
12
|
writeFileSync,
|
|
12
13
|
} from 'node:fs';
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
readFile as readFileAsync,
|
|
18
19
|
rename as renameAsync,
|
|
19
20
|
unlink as unlinkAsync,
|
|
20
|
-
writeFile as writeFileAsync,
|
|
21
21
|
} from 'node:fs/promises';
|
|
22
22
|
import { tmpdir } from 'node:os';
|
|
23
23
|
import { dirname, basename, join, relative, isAbsolute, resolve } from 'node:path';
|
|
@@ -82,10 +82,23 @@ function renameBackoffs(opts) {
|
|
|
82
82
|
return Array.isArray(opts.backoffs) && opts.backoffs.length > 0 ? opts.backoffs : DEFAULT_BACKOFFS_MS;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Windows answers EPERM/EACCES both for a sharing violation (AV/indexer holds
|
|
86
|
+
// the destination: clears in moments) and for renaming over a read-only file
|
|
87
|
+
// (never clears). Only the first is worth waiting for.
|
|
88
|
+
function isReadOnlyFile(path) {
|
|
89
|
+
try {
|
|
90
|
+
const stat = statSync(path);
|
|
91
|
+
return stat.isFile() && (stat.mode & 0o200) === 0;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
// Jittered delay before the next rename attempt, or null when this error or
|
|
86
98
|
// attempt count ends the loop. Shared so the sync and async paths cannot drift.
|
|
87
|
-
function nextRenameDelayMs(err, attempt, backoffs) {
|
|
99
|
+
function nextRenameDelayMs(err, attempt, backoffs, dst) {
|
|
88
100
|
if (!RETRY_CODES.has(err?.code) || attempt >= backoffs.length) return null;
|
|
101
|
+
if ((err.code === 'EPERM' || err.code === 'EACCES') && isReadOnlyFile(dst)) return null;
|
|
89
102
|
return backoffs[attempt] + Math.floor(Math.random() * Math.min(50, Math.max(1, backoffs[attempt])));
|
|
90
103
|
}
|
|
91
104
|
|
|
@@ -101,7 +114,7 @@ export function renameAttempt(src, dst, attempt, opts = {}) {
|
|
|
101
114
|
renameSync(src, dst);
|
|
102
115
|
return null;
|
|
103
116
|
} catch (err) {
|
|
104
|
-
const delayMs = nextRenameDelayMs(err, attempt, renameBackoffs(opts));
|
|
117
|
+
const delayMs = nextRenameDelayMs(err, attempt, renameBackoffs(opts), dst);
|
|
105
118
|
if (delayMs === null) throw err;
|
|
106
119
|
return delayMs;
|
|
107
120
|
}
|
|
@@ -124,7 +137,7 @@ async function renameWithRetry(src, dst, opts = {}) {
|
|
|
124
137
|
return true;
|
|
125
138
|
} catch (err) {
|
|
126
139
|
lastErr = err;
|
|
127
|
-
const delayMs = nextRenameDelayMs(err, attempt, backoffs);
|
|
140
|
+
const delayMs = nextRenameDelayMs(err, attempt, backoffs, dst);
|
|
128
141
|
if (delayMs === null) break;
|
|
129
142
|
await sleep(Math.max(1, Number(delayMs) || 1));
|
|
130
143
|
}
|
|
@@ -137,6 +150,14 @@ async function renameWithRetry(src, dst, opts = {}) {
|
|
|
137
150
|
const FILE_FSYNC_TOLERATED = ['EPERM', 'ENOTSUP', 'EINVAL'];
|
|
138
151
|
const DIR_FSYNC_TOLERATED = ['EPERM', 'ENOTSUP', 'EINVAL', 'EACCES'];
|
|
139
152
|
|
|
153
|
+
/** True when `err` is a filesystem refusing fsync on a file (network and
|
|
154
|
+
* cloud-sync drives), not a real I/O error: durability is best-effort there. */
|
|
155
|
+
export function isFileFsyncUnsupported(err) {
|
|
156
|
+
return FILE_FSYNC_TOLERATED.includes(err?.code);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Directory fsync (POSIX durability of the rename). Windows refuses it with
|
|
160
|
+
// EPERM, which is tolerated.
|
|
140
161
|
function fsyncPathSync(path, tolerated) {
|
|
141
162
|
let fd = null;
|
|
142
163
|
try {
|
|
@@ -165,6 +186,81 @@ async function fsyncPathAsync(path, tolerated) {
|
|
|
165
186
|
}
|
|
166
187
|
}
|
|
167
188
|
|
|
189
|
+
// Writes a file and flushes it through the handle that wrote it. Reopening to
|
|
190
|
+
// flush does not work: Windows refuses fsync on a read-only handle (EPERM) and
|
|
191
|
+
// may refuse a read-write reopen while AV scans the new file, and a 0o400 file
|
|
192
|
+
// cannot be reopened for writing on POSIX. Unflushed, a crash can publish the
|
|
193
|
+
// renamed file with its size committed but every byte NUL.
|
|
194
|
+
function writeFileDurableSync(path, data, opts) {
|
|
195
|
+
const fd = openSync(path, opts.flag || 'wx', opts.mode !== undefined ? opts.mode : 0o600);
|
|
196
|
+
try {
|
|
197
|
+
writeFileSync(fd, data, { encoding: opts.encoding || 'utf8' });
|
|
198
|
+
if (opts.fsync !== false) {
|
|
199
|
+
try {
|
|
200
|
+
fsyncSync(fd);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (!isFileFsyncUnsupported(err)) throw err;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} finally {
|
|
206
|
+
closeSync(fd);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function writeFileDurableAsync(path, data, opts) {
|
|
211
|
+
const handle = await openAsync(path, opts.flag || 'wx', opts.mode !== undefined ? opts.mode : 0o600);
|
|
212
|
+
try {
|
|
213
|
+
await handle.writeFile(data, { encoding: opts.encoding || 'utf8' });
|
|
214
|
+
if (opts.fsync !== false) {
|
|
215
|
+
try {
|
|
216
|
+
await handle.sync();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (!isFileFsyncUnsupported(err)) throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
await handle.close();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** True when `content` (string or Buffer) holds nothing but NUL bytes and
|
|
227
|
+
* whitespace — what a crash leaves of a file whose data never reached disk. */
|
|
228
|
+
export function holdsNoData(content) {
|
|
229
|
+
if (typeof content === 'string') return /^[\0\s]*$/.test(content);
|
|
230
|
+
for (const byte of content) {
|
|
231
|
+
if (byte !== 0 && byte !== 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0d) return false;
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// `backup: true` keeps the version a write replaces as `<file>.bak`, the last
|
|
237
|
+
// good copy readJsonState* restores when the file is later found damaged.
|
|
238
|
+
// Damaged bytes are never promoted to the backup.
|
|
239
|
+
const keepsAsBackup = (opts, content) => (opts.backupAccepts ? opts.backupAccepts(content) : !holdsNoData(content));
|
|
240
|
+
const backupWriteOpts = (opts) => ({ mode: opts.mode, secret: opts.secret, fsync: opts.fsync });
|
|
241
|
+
|
|
242
|
+
function keepBackupSync(filePath, opts) {
|
|
243
|
+
let previous;
|
|
244
|
+
try {
|
|
245
|
+
previous = readFileSync(filePath);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (err?.code === 'ENOENT') return;
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
if (keepsAsBackup(opts, previous)) publishFileSync(`${filePath}.bak`, previous, backupWriteOpts(opts));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function keepBackupAsync(filePath, opts) {
|
|
254
|
+
let previous;
|
|
255
|
+
try {
|
|
256
|
+
previous = await readFileAsync(filePath);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err?.code === 'ENOENT') return;
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
if (keepsAsBackup(opts, previous)) await publishFileAsync(`${filePath}.bak`, previous, backupWriteOpts(opts));
|
|
262
|
+
}
|
|
263
|
+
|
|
168
264
|
// Node sets NODE_TEST_CONTEXT in every `--test` process. A secret (credential)
|
|
169
265
|
// write from a test must land under the temp dir; anything else is a fixture
|
|
170
266
|
// about to overwrite a real profile, so fail loudly instead.
|
|
@@ -177,88 +273,95 @@ function assertTestSecretWriteSafe(filePath, opts) {
|
|
|
177
273
|
|
|
178
274
|
export function writeFileAtomicSync(filePath, data, opts = {}) {
|
|
179
275
|
assertTestSecretWriteSafe(filePath, opts);
|
|
180
|
-
const run = () =>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
276
|
+
const run = () => publishFileSync(filePath, data, opts);
|
|
277
|
+
if (opts.lock === true) {
|
|
278
|
+
return withFileLockSync(`${filePath}.lock`, run, opts);
|
|
279
|
+
}
|
|
280
|
+
return run();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Windows cloud-sync drives (Naver MYBOX, #138) refuse to create a file whose
|
|
284
|
+
// name starts with '.', so neither a temp file nor a quarantined copy may
|
|
285
|
+
// carry one. The temp shape is the edit tool's `<name>.mixdog-tmp-<hex>`.
|
|
286
|
+
function publishTempPath(filePath) {
|
|
287
|
+
return join(dirname(filePath), `${basename(filePath)}.mixdog-tmp-${randomBytes(8).toString('hex')}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function publishFileSync(filePath, data, opts) {
|
|
291
|
+
const dir = dirname(filePath);
|
|
292
|
+
mkdirSync(dir, { recursive: true });
|
|
293
|
+
const tmp = publishTempPath(filePath);
|
|
294
|
+
try {
|
|
295
|
+
writeFileDurableSync(tmp, data, { ...opts, flag: 'wx' });
|
|
296
|
+
// Secure the file before publication. Rename and hard links retain this
|
|
297
|
+
// file's ACL; resetting permissions afterward would expose a published
|
|
298
|
+
// secret if the subsequent owner grant failed.
|
|
299
|
+
if (opts.secret === true) _enforceOwnerOnlyAclWin32(tmp, { fresh: true });
|
|
300
|
+
if (opts.backup === true && opts.createOnly !== true) keepBackupSync(filePath, opts);
|
|
301
|
+
if (opts.createOnly === true) {
|
|
302
|
+
// Atomic create-if-absent: linkSync fails with EEXIST if the
|
|
303
|
+
// target already exists at link time, so a concurrent
|
|
304
|
+
// non-locking writer (user/editor) that creates the file
|
|
305
|
+
// between any prior existsSync gate and this call still
|
|
306
|
+
// wins — we drop the temp and report `false` so the caller
|
|
307
|
+
// can record the path as skipped rather than overwriting.
|
|
308
|
+
try {
|
|
309
|
+
linkSync(tmp, filePath);
|
|
310
|
+
} catch (err) {
|
|
203
311
|
try {
|
|
204
|
-
|
|
205
|
-
} catch
|
|
312
|
+
unlinkSync(tmp);
|
|
313
|
+
} catch {}
|
|
314
|
+
if (err?.code === 'EEXIST') return false;
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
unlinkSync(tmp);
|
|
319
|
+
} catch {}
|
|
320
|
+
} else {
|
|
321
|
+
try {
|
|
322
|
+
renameWithRetrySync(tmp, filePath, opts);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
// Opt-in fallback for callers whose target is a rename-lock hot
|
|
325
|
+
// spot on win32 (e.g. AV/indexer briefly holding the destination
|
|
326
|
+
// handle open), where rename-over keeps failing with EPERM/EACCES
|
|
327
|
+
// /EBUSY even after renameWithRetrySync's backoff loop. The write
|
|
328
|
+
// is already serialized by the caller's own withFileLockSync
|
|
329
|
+
// (opts.lock / updateJsonAtomicSync's lock), so a direct
|
|
330
|
+
// truncate+write of the target here is still race-safe against
|
|
331
|
+
// other writers using the same lock path; it just loses the
|
|
332
|
+
// rename's fully-atomic-swap guarantee for THIS write. Default
|
|
333
|
+
// behavior (throw) is unchanged for every other caller.
|
|
334
|
+
if (
|
|
335
|
+
opts.renameFallback === 'truncate' &&
|
|
336
|
+
opts.secret !== true &&
|
|
337
|
+
process.platform === 'win32' &&
|
|
338
|
+
RETRY_CODES.has(err?.code)
|
|
339
|
+
) {
|
|
340
|
+
writeFileDurableSync(filePath, readFileSync(tmp), { ...opts, flag: 'w', encoding: undefined });
|
|
206
341
|
try {
|
|
207
342
|
unlinkSync(tmp);
|
|
208
343
|
} catch {}
|
|
209
|
-
|
|
344
|
+
} else {
|
|
210
345
|
throw err;
|
|
211
346
|
}
|
|
212
|
-
try {
|
|
213
|
-
unlinkSync(tmp);
|
|
214
|
-
} catch {}
|
|
215
|
-
} else {
|
|
216
|
-
try {
|
|
217
|
-
renameWithRetrySync(tmp, filePath, opts);
|
|
218
|
-
} catch (err) {
|
|
219
|
-
// Opt-in fallback for callers whose target is a rename-lock hot
|
|
220
|
-
// spot on win32 (e.g. AV/indexer briefly holding the destination
|
|
221
|
-
// handle open), where rename-over keeps failing with EPERM/EACCES
|
|
222
|
-
// /EBUSY even after renameWithRetrySync's backoff loop. The write
|
|
223
|
-
// is already serialized by the caller's own withFileLockSync
|
|
224
|
-
// (opts.lock / updateJsonAtomicSync's lock), so a direct
|
|
225
|
-
// truncate+write of the target here is still race-safe against
|
|
226
|
-
// other writers using the same lock path; it just loses the
|
|
227
|
-
// rename's fully-atomic-swap guarantee for THIS write. Default
|
|
228
|
-
// behavior (throw) is unchanged for every other caller.
|
|
229
|
-
if (
|
|
230
|
-
opts.renameFallback === 'truncate' &&
|
|
231
|
-
opts.secret !== true &&
|
|
232
|
-
process.platform === 'win32' &&
|
|
233
|
-
RETRY_CODES.has(err?.code)
|
|
234
|
-
) {
|
|
235
|
-
const data = readFileSync(tmp);
|
|
236
|
-
writeFileSync(filePath, data, { mode: opts.mode !== undefined ? opts.mode : 0o600 });
|
|
237
|
-
try {
|
|
238
|
-
unlinkSync(tmp);
|
|
239
|
-
} catch {}
|
|
240
|
-
} else {
|
|
241
|
-
throw err;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
347
|
}
|
|
245
|
-
if (opts.fsyncDir === true) fsyncPathSync(dir, DIR_FSYNC_TOLERATED);
|
|
246
|
-
return true;
|
|
247
|
-
} catch (err) {
|
|
248
|
-
try {
|
|
249
|
-
if (existsSync(tmp)) unlinkSync(tmp);
|
|
250
|
-
} catch {}
|
|
251
|
-
throw err;
|
|
252
348
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
349
|
+
if (opts.fsyncDir === true) fsyncPathSync(dir, DIR_FSYNC_TOLERATED);
|
|
350
|
+
return true;
|
|
351
|
+
} catch (err) {
|
|
352
|
+
try {
|
|
353
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
354
|
+
} catch {}
|
|
355
|
+
throw err;
|
|
256
356
|
}
|
|
257
|
-
return run();
|
|
258
357
|
}
|
|
259
358
|
|
|
359
|
+
// A JSON write's backup takes only a replaced version that still parses.
|
|
360
|
+
const jsonBackupOpts = (opts) =>
|
|
361
|
+
opts.backup === true && !opts.backupAccepts ? { ...opts, backupAccepts: isJsonContent } : opts;
|
|
362
|
+
|
|
260
363
|
export function writeJsonAtomicSync(filePath, value, opts = {}) {
|
|
261
|
-
return writeFileAtomicSync(filePath, `${JSON.stringify(value, null, opts.compact ? 0 : 2)}\n`, opts);
|
|
364
|
+
return writeFileAtomicSync(filePath, `${JSON.stringify(value, null, opts.compact ? 0 : 2)}\n`, jsonBackupOpts(opts));
|
|
262
365
|
}
|
|
263
366
|
|
|
264
367
|
// Windows editors and PowerShell 5 save UTF-8 with a leading byte order mark,
|
|
@@ -267,12 +370,182 @@ export function parseJsonText(text) {
|
|
|
267
370
|
return JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
|
|
268
371
|
}
|
|
269
372
|
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
373
|
+
function isJsonContent(content) {
|
|
374
|
+
try {
|
|
375
|
+
parseJsonText(String(content));
|
|
376
|
+
return true;
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Damaged JSON state ──────────────────────────────────────────────
|
|
383
|
+
// A state file whose text does not parse, or that the caller's `validate`
|
|
384
|
+
// rejects (throws), is damaged: empty or NUL-filled after a crash, truncated,
|
|
385
|
+
// or broken by hand. Its feature must keep working, so the damaged file is
|
|
386
|
+
// moved aside to `<file>.corrupt-<ts>` (its bytes are kept, never overwritten)
|
|
387
|
+
// and the last good copy `<file>.bak` is restored when there is one. A file
|
|
388
|
+
// that cannot be READ (EACCES, EBUSY…) is not damaged and still throws.
|
|
389
|
+
// `decode` turns the file's bytes into JSON text (e.g. unsealing an encrypted
|
|
390
|
+
// file); throwing there marks the file damaged too.
|
|
391
|
+
// The rename that moves a damaged file aside replaces an existing destination,
|
|
392
|
+
// so the name is the first `<file>.corrupt-<ms>` that does not exist yet: two
|
|
393
|
+
// recoveries in one millisecond (or after a clock rollback) never overwrite a
|
|
394
|
+
// kept copy, and the name is no longer than the original shape. Recovery runs
|
|
395
|
+
// under the file's lock, so nothing else creates a quarantine name between
|
|
396
|
+
// this check and the rename.
|
|
397
|
+
export const quarantinePath = (filePath) => {
|
|
398
|
+
let stamp = Date.now();
|
|
399
|
+
while (existsSync(`${filePath}.corrupt-${stamp}`)) stamp += 1;
|
|
400
|
+
return `${filePath}.corrupt-${stamp}`;
|
|
401
|
+
};
|
|
402
|
+
const LOCK_BUSY_CODES = new Set(['ELOCKCONTENDED', 'ELOCKTIMEOUT']);
|
|
403
|
+
const STATE_UNSUPPORTED = 'ESTATEUNSUPPORTED';
|
|
404
|
+
const acceptAny = (value) => value;
|
|
405
|
+
const utf8Text = (content) => content.toString('utf8');
|
|
406
|
+
|
|
407
|
+
/** For `validate`: the file is intact but in a format a newer build wrote.
|
|
408
|
+
* It is not damage — the file stays in place, untouched, and the read
|
|
409
|
+
* answers the fallback, so a downgrade never discards a newer build's data. */
|
|
410
|
+
export function unsupportedState(message) {
|
|
411
|
+
return Object.assign(new Error(message), { code: STATE_UNSUPPORTED });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const stateParser =
|
|
415
|
+
(validate = acceptAny, decode = utf8Text) =>
|
|
416
|
+
(content) => {
|
|
417
|
+
if (content === null) return { state: 'absent' };
|
|
418
|
+
try {
|
|
419
|
+
return { state: 'ok', value: validate(parseJsonText(decode(content))), content };
|
|
420
|
+
} catch (error) {
|
|
421
|
+
return { state: error?.code === STATE_UNSUPPORTED ? 'unsupported' : 'damaged', error };
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
function readContentOrNullSync(filePath) {
|
|
426
|
+
try {
|
|
427
|
+
return readFileSync(filePath);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
if (err?.code === 'ENOENT') return null;
|
|
430
|
+
throw err;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function readContentOrNullAsync(filePath) {
|
|
435
|
+
try {
|
|
436
|
+
return await readFileAsync(filePath);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (err?.code === 'ENOENT') return null;
|
|
439
|
+
throw err;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function reportDamaged(filePath, error, outcome) {
|
|
444
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: escapes C0 control characters (U+0000-U+001F) so a damaged-file message stays on one log line.
|
|
445
|
+
const reason = String(error?.message || error).replace(/[\0-\x1f]/g, (c) => `\\x${c.charCodeAt(0).toString(16)}`);
|
|
446
|
+
process.stderr.write(`[state] ${filePath} is damaged (${reason}); ${outcome}\n`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const restoreOpts = (opts) => ({ mode: opts.mode, secret: opts.secret });
|
|
450
|
+
const KEPT_IN_PLACE = 'left in place: no good copy to restore';
|
|
451
|
+
|
|
452
|
+
const NO_BACKUP = { state: 'absent' };
|
|
453
|
+
|
|
454
|
+
// Caller holds `${filePath}.lock`. The file is re-read under the lock, so a
|
|
455
|
+
// file another process has repaired meanwhile is kept. `keepWithoutBackup` leaves a
|
|
456
|
+
// damaged file in place when there is no good copy: for a file other readers
|
|
457
|
+
// treat "missing" as permissive (e.g. a deny list), moving it would fail open.
|
|
458
|
+
// `restoreBackup: false` never restores `.bak` — for files where an older copy
|
|
459
|
+
// is worse than none (e.g. permission grants a later revocation removed).
|
|
460
|
+
function recoverDamagedSync(filePath, parse, opts) {
|
|
461
|
+
const current = parse(readContentOrNullSync(filePath));
|
|
462
|
+
if (current.state !== 'damaged') return current;
|
|
463
|
+
const backup = opts.restoreBackup === false ? NO_BACKUP : parse(readContentOrNullSync(`${filePath}.bak`));
|
|
464
|
+
if (backup.state !== 'ok' && opts.keepWithoutBackup) {
|
|
465
|
+
reportDamaged(filePath, current.error, KEPT_IN_PLACE);
|
|
466
|
+
return { state: 'absent' };
|
|
467
|
+
}
|
|
468
|
+
const aside = quarantinePath(filePath);
|
|
469
|
+
renameWithRetrySync(filePath, aside, opts);
|
|
470
|
+
if (backup.state === 'ok') publishFileSync(filePath, backup.content, restoreOpts(opts));
|
|
471
|
+
reportDamaged(filePath, current.error, recoveredOutcome(aside, backup));
|
|
472
|
+
return backup.state === 'ok' ? backup : { state: 'absent' };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function recoverDamagedAsync(filePath, parse, opts) {
|
|
476
|
+
const current = parse(await readContentOrNullAsync(filePath));
|
|
477
|
+
if (current.state !== 'damaged') return current;
|
|
478
|
+
const backup = opts.restoreBackup === false ? NO_BACKUP : parse(await readContentOrNullAsync(`${filePath}.bak`));
|
|
479
|
+
if (backup.state !== 'ok' && opts.keepWithoutBackup) {
|
|
480
|
+
reportDamaged(filePath, current.error, KEPT_IN_PLACE);
|
|
481
|
+
return { state: 'absent' };
|
|
482
|
+
}
|
|
483
|
+
const aside = quarantinePath(filePath);
|
|
484
|
+
await renameWithRetry(filePath, aside, opts);
|
|
485
|
+
if (backup.state === 'ok') await publishFileAsync(filePath, backup.content, restoreOpts(opts));
|
|
486
|
+
reportDamaged(filePath, current.error, recoveredOutcome(aside, backup));
|
|
487
|
+
return backup.state === 'ok' ? backup : { state: 'absent' };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const recoveredOutcome = (aside, backup) =>
|
|
491
|
+
`kept as ${aside}${backup.state === 'ok' ? '; restored the last good copy' : '; starting empty'}`;
|
|
492
|
+
|
|
493
|
+
/** Reads a JSON state file that must never block its feature. Returns the
|
|
494
|
+
* validated value, or `fallback` (as is) when the file is missing or damaged
|
|
495
|
+
* and has no good backup. `validate` maps the parsed value and throws to mark
|
|
496
|
+
* it damaged; `decode` maps the raw bytes to JSON text. Other opts (`secret`,
|
|
497
|
+
* `mode`, lock timing) apply to the lock and to a restored file. */
|
|
498
|
+
export function readJsonStateSync(filePath, { validate, decode, fallback = null, ...opts } = {}) {
|
|
499
|
+
const parse = stateParser(validate, decode);
|
|
500
|
+
const first = parse(readContentOrNullSync(filePath));
|
|
501
|
+
if (first.state === 'ok') return first.value;
|
|
502
|
+
if (first.state !== 'damaged') return fallback;
|
|
503
|
+
let recovered;
|
|
504
|
+
try {
|
|
505
|
+
recovered = withFileLockSync(`${filePath}.lock`, () => recoverDamagedSync(filePath, parse, opts), opts);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
if (!LOCK_BUSY_CODES.has(err?.code)) throw err;
|
|
508
|
+
reportDamaged(filePath, first.error, 'left in place while another writer holds its lock');
|
|
509
|
+
return fallback;
|
|
510
|
+
}
|
|
511
|
+
return recovered.state === 'ok' ? recovered.value : fallback;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export async function readJsonState(filePath, { validate, decode, fallback = null, ...opts } = {}) {
|
|
515
|
+
const parse = stateParser(validate, decode);
|
|
516
|
+
const first = parse(await readContentOrNullAsync(filePath));
|
|
517
|
+
if (first.state === 'ok') return first.value;
|
|
518
|
+
if (first.state !== 'damaged') return fallback;
|
|
519
|
+
let recovered;
|
|
520
|
+
try {
|
|
521
|
+
recovered = await withFileLock(`${filePath}.lock`, () => recoverDamagedAsync(filePath, parse, opts), opts);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
if (!LOCK_BUSY_CODES.has(err?.code)) throw err;
|
|
524
|
+
reportDamaged(filePath, first.error, 'left in place while another writer holds its lock');
|
|
525
|
+
return fallback;
|
|
526
|
+
}
|
|
527
|
+
return recovered.state === 'ok' ? recovered.value : fallback;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Read-modify-write start: a missing file is null; a damaged one is kept
|
|
531
|
+
// aside (never silently overwritten) and its last good copy, if any, is the
|
|
532
|
+
// mutator's input. I/O failures throw — an unreadable file is not empty.
|
|
533
|
+
const parseAnyJson = stateParser();
|
|
534
|
+
|
|
535
|
+
function readForMutationSync(filePath, opts) {
|
|
536
|
+
const current = parseAnyJson(readContentOrNullSync(filePath));
|
|
537
|
+
if (current.state === 'ok') return current.value;
|
|
538
|
+
if (current.state === 'absent') return null;
|
|
539
|
+
const recovered = recoverDamagedSync(filePath, parseAnyJson, opts);
|
|
540
|
+
return recovered.state === 'ok' ? recovered.value : null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function readForMutationAsync(filePath, opts) {
|
|
544
|
+
const current = parseAnyJson(await readContentOrNullAsync(filePath));
|
|
545
|
+
if (current.state === 'ok') return current.value;
|
|
546
|
+
if (current.state === 'absent') return null;
|
|
547
|
+
const recovered = await recoverDamagedAsync(filePath, parseAnyJson, opts);
|
|
548
|
+
return recovered.state === 'ok' ? recovered.value : null;
|
|
276
549
|
}
|
|
277
550
|
|
|
278
551
|
export function updateJsonAtomicSync(filePath, mutator, opts = {}) {
|
|
@@ -280,12 +553,7 @@ export function updateJsonAtomicSync(filePath, mutator, opts = {}) {
|
|
|
280
553
|
return withFileLockSync(
|
|
281
554
|
`${filePath}.lock`,
|
|
282
555
|
() => {
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
cur = parseJsonText(readFileSync(filePath, 'utf8'));
|
|
286
|
-
} catch (error) {
|
|
287
|
-
cur = recoverJsonMutationRead(error);
|
|
288
|
-
}
|
|
556
|
+
const cur = readForMutationSync(filePath, writeOpts);
|
|
289
557
|
const next = mutator(cur);
|
|
290
558
|
if (next === undefined) return cur;
|
|
291
559
|
writeJsonAtomicSync(filePath, next, { ...writeOpts, lock: false });
|
|
@@ -302,45 +570,7 @@ export function updateJsonAtomicSync(filePath, mutator, opts = {}) {
|
|
|
302
570
|
// Async writes retain atomic rename publication; truncate recovery is sync-only.
|
|
303
571
|
export async function writeFileAtomicAsync(filePath, data, opts = {}) {
|
|
304
572
|
assertTestSecretWriteSafe(filePath, opts);
|
|
305
|
-
const run =
|
|
306
|
-
const dir = dirname(filePath);
|
|
307
|
-
await mkdirAsync(dir, { recursive: true });
|
|
308
|
-
const tmp = join(dir, `.${basename(filePath)}.${randomBytes(12).toString('hex')}.tmp`);
|
|
309
|
-
try {
|
|
310
|
-
const writeOpts = {
|
|
311
|
-
encoding: opts.encoding || 'utf8',
|
|
312
|
-
flag: 'wx',
|
|
313
|
-
mode: opts.mode !== undefined ? opts.mode : 0o600,
|
|
314
|
-
};
|
|
315
|
-
await writeFileAsync(tmp, data, writeOpts);
|
|
316
|
-
// Both publication paths retain the secured file's existing ACL.
|
|
317
|
-
if (opts.secret === true) await _enforceOwnerOnlyAclWin32Async(tmp, { fresh: true });
|
|
318
|
-
if (opts.fsync !== false) await fsyncPathAsync(tmp, FILE_FSYNC_TOLERATED);
|
|
319
|
-
if (opts.createOnly === true) {
|
|
320
|
-
try {
|
|
321
|
-
await linkAsync(tmp, filePath);
|
|
322
|
-
} catch (err) {
|
|
323
|
-
try {
|
|
324
|
-
await unlinkAsync(tmp);
|
|
325
|
-
} catch {}
|
|
326
|
-
if (err?.code === 'EEXIST') return false;
|
|
327
|
-
throw err;
|
|
328
|
-
}
|
|
329
|
-
try {
|
|
330
|
-
await unlinkAsync(tmp);
|
|
331
|
-
} catch {}
|
|
332
|
-
} else {
|
|
333
|
-
await renameWithRetry(tmp, filePath, opts);
|
|
334
|
-
}
|
|
335
|
-
if (opts.fsyncDir === true) await fsyncPathAsync(dir, DIR_FSYNC_TOLERATED);
|
|
336
|
-
return true;
|
|
337
|
-
} catch (err) {
|
|
338
|
-
try {
|
|
339
|
-
await unlinkAsync(tmp);
|
|
340
|
-
} catch {}
|
|
341
|
-
throw err;
|
|
342
|
-
}
|
|
343
|
-
};
|
|
573
|
+
const run = () => publishFileAsync(filePath, data, opts);
|
|
344
574
|
// Acquire the file lock before occupying an I/O slot. Otherwise a waiter can
|
|
345
575
|
// exhaust the gate while the current lock owner needs it to finish its write.
|
|
346
576
|
if (opts.lock !== true) return _runAdaptiveFileIo(run);
|
|
@@ -369,8 +599,45 @@ export async function writeFileAtomicAsync(filePath, data, opts = {}) {
|
|
|
369
599
|
);
|
|
370
600
|
}
|
|
371
601
|
|
|
602
|
+
// Never re-enters the I/O gate: a backup or restore runs inside a slot the
|
|
603
|
+
// caller already holds.
|
|
604
|
+
async function publishFileAsync(filePath, data, opts) {
|
|
605
|
+
const dir = dirname(filePath);
|
|
606
|
+
await mkdirAsync(dir, { recursive: true });
|
|
607
|
+
const tmp = publishTempPath(filePath);
|
|
608
|
+
try {
|
|
609
|
+
await writeFileDurableAsync(tmp, data, { ...opts, flag: 'wx' });
|
|
610
|
+
// Both publication paths retain the secured file's existing ACL.
|
|
611
|
+
if (opts.secret === true) await _enforceOwnerOnlyAclWin32Async(tmp, { fresh: true });
|
|
612
|
+
if (opts.backup === true && opts.createOnly !== true) await keepBackupAsync(filePath, opts);
|
|
613
|
+
if (opts.createOnly === true) {
|
|
614
|
+
try {
|
|
615
|
+
await linkAsync(tmp, filePath);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
try {
|
|
618
|
+
await unlinkAsync(tmp);
|
|
619
|
+
} catch {}
|
|
620
|
+
if (err?.code === 'EEXIST') return false;
|
|
621
|
+
throw err;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
await unlinkAsync(tmp);
|
|
625
|
+
} catch {}
|
|
626
|
+
} else {
|
|
627
|
+
await renameWithRetry(tmp, filePath, opts);
|
|
628
|
+
}
|
|
629
|
+
if (opts.fsyncDir === true) await fsyncPathAsync(dir, DIR_FSYNC_TOLERATED);
|
|
630
|
+
return true;
|
|
631
|
+
} catch (err) {
|
|
632
|
+
try {
|
|
633
|
+
await unlinkAsync(tmp);
|
|
634
|
+
} catch {}
|
|
635
|
+
throw err;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
372
639
|
export function writeJsonAtomicAsync(filePath, value, opts = {}) {
|
|
373
|
-
return writeFileAtomicAsync(filePath, `${JSON.stringify(value, null, opts.compact ? 0 : 2)}\n`, opts);
|
|
640
|
+
return writeFileAtomicAsync(filePath, `${JSON.stringify(value, null, opts.compact ? 0 : 2)}\n`, jsonBackupOpts(opts));
|
|
374
641
|
}
|
|
375
642
|
|
|
376
643
|
// Async read-modify-write. Same lock path (`${filePath}.lock`) and protocol
|
|
@@ -383,12 +650,7 @@ export async function updateJsonAtomic(filePath, mutator, opts = {}) {
|
|
|
383
650
|
return withFileLock(
|
|
384
651
|
`${filePath}.lock`,
|
|
385
652
|
async () => {
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
cur = parseJsonText(await readFileAsync(filePath, 'utf8'));
|
|
389
|
-
} catch (error) {
|
|
390
|
-
cur = recoverJsonMutationRead(error);
|
|
391
|
-
}
|
|
653
|
+
const cur = await readForMutationAsync(filePath, writeOpts);
|
|
392
654
|
const next = mutator(cur);
|
|
393
655
|
if (next === undefined) return cur;
|
|
394
656
|
await writeJsonAtomicAsync(filePath, next, { ...writeOpts, lock: false });
|
|
@@ -306,12 +306,12 @@ export function cleanupBackgroundTasks(options = {}) {
|
|
|
306
306
|
// `notify`: true notifies immediately, false never notifies (owner teardown).
|
|
307
307
|
// Left unset, a `notifyOnCancel` task defers its single `cancelled`
|
|
308
308
|
// notification until its run settles (see settleCancelledBackgroundTask); any
|
|
309
|
-
// other task stays silent.
|
|
310
|
-
function cancelRunningTask(task, reason, notify) {
|
|
309
|
+
// other task stays silent. `terminalReason` records why it was cancelled.
|
|
310
|
+
function cancelRunningTask(task, reason, notify, terminalReason = null) {
|
|
311
311
|
try {
|
|
312
312
|
task.cancel?.();
|
|
313
313
|
} catch {}
|
|
314
|
-
completeBackgroundTask(task.taskId, { status: 'cancelled', error: reason, notify: notify === true });
|
|
314
|
+
completeBackgroundTask(task.taskId, { status: 'cancelled', error: reason, notify: notify === true, terminalReason });
|
|
315
315
|
if (task.notifyOnCancel !== true || notify === true || task.status !== 'cancelled' || task.cancelNotify) return;
|
|
316
316
|
if (notify === false) {
|
|
317
317
|
task.cancelNotify = 'silent';
|
|
@@ -362,8 +362,11 @@ export function settleCancelledBackgroundTask(taskId, { result } = {}) {
|
|
|
362
362
|
return task;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
/** Cancel the running tasks `options` selects. `terminalReason` (optional) is
|
|
366
|
+
* stamped on each one cancelled, so its run's settlement can tell why. */
|
|
365
367
|
export function cancelBackgroundTasks(options = {}) {
|
|
366
368
|
const reason = clean(options.reason) || 'cancelled';
|
|
369
|
+
const terminalReason = clean(options.terminalReason) || null;
|
|
367
370
|
const wanted = clean(options.surface);
|
|
368
371
|
const executionSessionId = clean(options.executionSessionId);
|
|
369
372
|
const scope = normalizeTaskScope(options);
|
|
@@ -382,7 +385,7 @@ export function cancelBackgroundTasks(options = {}) {
|
|
|
382
385
|
if (options.notify === false) silenceCancelledNotification(task);
|
|
383
386
|
continue;
|
|
384
387
|
}
|
|
385
|
-
cancelRunningTask(task, reason, options.notify);
|
|
388
|
+
cancelRunningTask(task, reason, options.notify, terminalReason);
|
|
386
389
|
cancelled += 1;
|
|
387
390
|
}
|
|
388
391
|
return { cancelled };
|