@gajae-code/coding-agent 0.11.1 → 0.11.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/CHANGELOG.md +54 -0
- package/dist/types/cli/config-cli.d.ts +11 -1
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/cli/session-picker.d.ts +2 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/mcp-serve.d.ts +33 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/commands/notify.d.ts +0 -2
- package/dist/types/commands/sdk.d.ts +1 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +14 -4
- package/dist/types/config/keybindings.d.ts +42 -1
- package/dist/types/config/model-auth.d.ts +3 -0
- package/dist/types/config/model-bindings-applier.d.ts +13 -0
- package/dist/types/config/model-discovery-manager.d.ts +49 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-registry.d.ts +16 -15
- package/dist/types/config/model-resolver.d.ts +28 -18
- package/dist/types/config/models-config-schema.d.ts +11 -0
- package/dist/types/config/settings-schema.d.ts +56 -1
- package/dist/types/config/settings.d.ts +14 -11
- package/dist/types/coordinator-mcp/policy.d.ts +5 -0
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +0 -6
- package/dist/types/edit/index.d.ts +3 -0
- package/dist/types/eval/py/env.d.ts +9 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -3
- package/dist/types/extensibility/hooks/runner.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
- package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
- package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
- package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
- package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
- package/dist/types/gjc-runtime/team-store.d.ts +160 -0
- package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
- package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
- package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/goals/state.d.ts +8 -0
- package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/state.d.ts +11 -4
- package/dist/types/lsp/lspmux.d.ts +5 -3
- package/dist/types/lsp/path-trust.d.ts +1 -0
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memories/storage.d.ts +1 -0
- package/dist/types/modes/action-registry.d.ts +29 -0
- package/dist/types/modes/components/chrome.d.ts +14 -0
- package/dist/types/modes/components/command-palette.d.ts +27 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
- package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
- package/dist/types/modes/components/queue-pane.d.ts +12 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
- package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/show-images-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +1 -94
- package/dist/types/modes/components/tasks-pane.d.ts +15 -0
- package/dist/types/modes/components/theme-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/tool-status-header.d.ts +111 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -1
- package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
- package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
- package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
- package/dist/types/modes/interactive-mode.d.ts +19 -17
- package/dist/types/modes/jobs-observer.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
- package/dist/types/modes/tasks-aggregator.d.ts +39 -0
- package/dist/types/modes/transcript-item-registry.d.ts +60 -0
- package/dist/types/modes/types.d.ts +20 -13
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +78 -12
- package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
- package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
- package/dist/types/sdk/broker/session-index.d.ts +0 -1
- package/dist/types/sdk/broker/state-version.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
- package/dist/types/sdk/bus/config-commands.d.ts +1 -1
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/html-format.d.ts +3 -2
- package/dist/types/sdk/bus/index.d.ts +27 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/notification-service.d.ts +20 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
- package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
- package/dist/types/sdk/bus/rich-document.d.ts +7 -0
- package/dist/types/sdk/bus/rich-render.d.ts +18 -41
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
- package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/session-directory.d.ts +56 -0
- package/dist/types/session/agent-session.d.ts +82 -47
- package/dist/types/session/agent-storage.d.ts +7 -0
- package/dist/types/session/fallback-chain-controller.d.ts +10 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
- package/dist/types/session/messages.d.ts +16 -0
- package/dist/types/session/session-manager-internal.d.ts +14 -0
- package/dist/types/session/session-manager.d.ts +82 -12
- package/dist/types/session/session-storage.d.ts +43 -3
- package/dist/types/session/volatile-context-pruning.d.ts +15 -0
- package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
- package/dist/types/system-prompt.d.ts +2 -11
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/thinking.d.ts +7 -0
- package/dist/types/tool-discovery/tool-index.d.ts +1 -50
- package/dist/types/tools/ask.d.ts +64 -2
- package/dist/types/tools/bash.d.ts +3 -0
- package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
- package/dist/types/tools/cron.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -0
- package/dist/types/tools/index.d.ts +30 -15
- package/dist/types/tools/output-meta.d.ts +1 -0
- package/dist/types/tools/read.d.ts +3 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/utils/event-bus.d.ts +1 -1
- package/dist/types/web/search/index.d.ts +3 -1
- package/package.json +21 -9
- package/scripts/build-sdk-package-smoke.ts +34 -1
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-operation-inventory.ts +64 -20
- package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
- package/src/async/job-manager.ts +49 -6
- package/src/cli/config-cli.ts +75 -2
- package/src/cli/notify-cli.ts +92 -26
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/harness.ts +34 -106
- package/src/commands/launch.ts +6 -4
- package/src/commands/mcp-serve.ts +85 -8
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +8 -4
- package/src/commands/sdk.ts +61 -40
- package/src/commands/ultragoal.ts +4 -1
- package/src/config/atomic-yaml-patch.ts +80 -13
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-registry.ts +169 -302
- package/src/config/model-resolver.ts +361 -369
- package/src/config/models-config-schema.ts +5 -4
- package/src/config/settings-schema.ts +159 -2
- package/src/config/settings.ts +458 -482
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +1702 -238
- package/src/debug/index.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
- package/src/defaults/gjc/skills/team/SKILL.md +16 -0
- package/src/discovery/helpers.ts +1 -50
- package/src/edit/index.ts +64 -0
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/kernel.ts +5 -3
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +12 -3
- package/src/extensibility/extensions/types.ts +39 -1
- package/src/extensibility/gjc-plugins/installer.ts +16 -16
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
- package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
- package/src/gjc-runtime/deep-interview-state.ts +317 -5
- package/src/gjc-runtime/goal-mode-request.ts +55 -8
- package/src/gjc-runtime/launch-tmux.ts +2 -5
- package/src/gjc-runtime/launch-worktree.ts +5 -5
- package/src/gjc-runtime/linux-proc.ts +71 -0
- package/src/gjc-runtime/ralplan-runtime.ts +74 -90
- package/src/gjc-runtime/session-resolution.ts +18 -3
- package/src/gjc-runtime/session-state-sidecar.ts +6 -107
- package/src/gjc-runtime/state-migrations.ts +1 -1
- package/src/gjc-runtime/state-runtime.ts +320 -249
- package/src/gjc-runtime/state-writer.ts +36 -40
- package/src/gjc-runtime/team-launch.ts +319 -0
- package/src/gjc-runtime/team-notify.ts +326 -0
- package/src/gjc-runtime/team-runtime.ts +1099 -1482
- package/src/gjc-runtime/team-store.ts +828 -0
- package/src/gjc-runtime/team-workers.ts +962 -0
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
- package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
- package/src/gjc-runtime/tmux-sessions.ts +4 -21
- package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
- package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
- package/src/gjc-runtime/ultragoal-guard.ts +29 -5
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
- package/src/gjc-runtime/workflow-cli-common.ts +30 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +18 -0
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/lsp/client.ts +1 -1
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +28 -8
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/custom-provider-wizard.ts +51 -14
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/pet-selector.ts +11 -17
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/session-observer-overlay.ts +207 -772
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/transcript-viewer-overlay.ts +384 -0
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +24 -1
- package/src/modes/controllers/goal-mode-controller.ts +474 -0
- package/src/modes/controllers/input-controller.ts +764 -229
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +517 -0
- package/src/modes/controllers/selector-controller.ts +225 -10
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/interactive-mode.ts +289 -1150
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/runtime-init.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +45 -18
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +2 -8
- package/src/prompts/agents/critic.md +2 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +1 -23
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +17 -96
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/manager.ts +1 -1
- package/src/sdk/broker/broker.ts +206 -78
- package/src/sdk/broker/discovery.ts +1 -1
- package/src/sdk/broker/ensure.ts +3 -1
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +371 -74
- package/src/sdk/broker/lifecycle.ts +1399 -142
- package/src/sdk/broker/process-incarnation.ts +5 -13
- package/src/sdk/broker/session-index.ts +145 -56
- package/src/sdk/broker/state-version.ts +21 -0
- package/src/sdk/broker/transport.ts +1 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -28
- package/src/sdk/bus/chat-effect-journal.ts +10 -2
- package/src/sdk/bus/config-commands.ts +5 -4
- package/src/sdk/bus/config.ts +98 -0
- package/src/sdk/bus/conversation-store.ts +5 -29
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +778 -93
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/natives-augment.d.ts +26 -1
- package/src/sdk/bus/notification-service.ts +168 -20
- package/src/sdk/bus/rate-limit-pool.ts +58 -14
- package/src/sdk/bus/recent-activity.ts +188 -74
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-render.ts +62 -70
- package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon.ts +1389 -237
- package/src/sdk/bus/threaded-inbound.ts +8 -3
- package/src/sdk/bus/threaded-render.ts +57 -1
- package/src/sdk/bus/topic-registry.ts +120 -13
- package/src/sdk/host/control/dispatch.ts +14 -2
- package/src/sdk/host/control/operations.ts +6 -1
- package/src/sdk/host/host.ts +31 -1
- package/src/sdk/host/query/handlers.ts +20 -22
- package/src/sdk/host/query/revision-store.ts +80 -57
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +458 -479
- package/src/session/agent-session.ts +1700 -583
- package/src/session/agent-storage.ts +18 -0
- package/src/session/cache-economics.ts +25 -8
- package/src/session/fallback-chain-controller.ts +53 -3
- package/src/session/internal/managed-session-scope.ts +2226 -0
- package/src/session/internal/managed-session-storage.ts +453 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +1112 -333
- package/src/session/session-storage.ts +444 -39
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/setup/credential-auto-import.ts +2 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +46 -11
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +90 -6
- package/src/task/index.ts +36 -21
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +2 -134
- package/src/tools/ask.ts +157 -21
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash.ts +38 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/tab-supervisor.ts +282 -36
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/eval.ts +1 -2
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +76 -24
- package/src/tools/output-meta.ts +13 -5
- package/src/tools/read.ts +37 -1
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/utils/event-bus.ts +9 -4
- package/src/web/search/index.ts +3 -3
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/src/discovery/plugin-dir-roots.ts +0 -28
- package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
package/src/config/settings.ts
CHANGED
|
@@ -28,13 +28,18 @@ import { type Settings as SettingsCapabilityItem, settingsCapability } from "../
|
|
|
28
28
|
import type { ModelRole } from "../config/model-registry";
|
|
29
29
|
import { loadCapability } from "../discovery";
|
|
30
30
|
import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset } from "../modes/theme/theme";
|
|
31
|
-
import
|
|
31
|
+
import {
|
|
32
|
+
type NotificationSettingsReader,
|
|
33
|
+
type NotificationSettingsSnapshot,
|
|
34
|
+
parseNotificationSettingsSnapshot,
|
|
35
|
+
} from "../sdk/bus/config";
|
|
32
36
|
import { AgentStorage } from "../session/agent-storage";
|
|
33
37
|
import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
|
|
34
38
|
import {
|
|
35
39
|
type AtomicYamlPatch,
|
|
36
40
|
applyAtomicYamlPatches,
|
|
37
41
|
applyAtomicYamlPatchesWithCurrent,
|
|
42
|
+
atomicYamlPathHash,
|
|
38
43
|
type CasReceipt,
|
|
39
44
|
deleteByPath,
|
|
40
45
|
reserveAtomicYamlUpdateSlot,
|
|
@@ -44,11 +49,14 @@ import { isModelSelectorValue, type ModelSelectorValue, normalizeModelSelectorVa
|
|
|
44
49
|
|
|
45
50
|
import {
|
|
46
51
|
type BashInterceptorRule,
|
|
52
|
+
CONFIG_SCHEMA_VERSION,
|
|
47
53
|
type GroupPrefix,
|
|
48
54
|
type GroupTypeMap,
|
|
49
55
|
getDefault,
|
|
56
|
+
reconcileSettingsSchema,
|
|
50
57
|
SETTINGS_SCHEMA,
|
|
51
58
|
type SettingPath,
|
|
59
|
+
type SettingsSchemaReport,
|
|
52
60
|
type SettingValue,
|
|
53
61
|
} from "./settings-schema";
|
|
54
62
|
|
|
@@ -71,8 +79,6 @@ type SettingsPatch = {
|
|
|
71
79
|
readonly generation: number;
|
|
72
80
|
readonly revision: number;
|
|
73
81
|
readonly modelRole?: string;
|
|
74
|
-
readonly modelRoleRevision?: number;
|
|
75
|
-
readonly configVersion?: string;
|
|
76
82
|
readonly legacyFallbackMigration?: boolean;
|
|
77
83
|
};
|
|
78
84
|
|
|
@@ -88,17 +94,19 @@ type DurableBatchRevision = {
|
|
|
88
94
|
previousRevision: number | undefined;
|
|
89
95
|
revision: number;
|
|
90
96
|
};
|
|
97
|
+
type NotificationValidationState = {
|
|
98
|
+
malformedConfigRoot: boolean;
|
|
99
|
+
invalidNotificationConfiguration: boolean;
|
|
100
|
+
generation: number;
|
|
101
|
+
};
|
|
102
|
+
type NotificationValidationRestoreGuard = {
|
|
103
|
+
readonly state: NotificationValidationState;
|
|
104
|
+
restoreGeneration: number | undefined;
|
|
105
|
+
};
|
|
91
106
|
|
|
92
107
|
export type SettingsAtomicPatch = { path: SettingPath; op: "set"; value: unknown } | { path: SettingPath; op: "unset" };
|
|
93
108
|
export type SettingsAtomicReceipt = CasReceipt;
|
|
94
109
|
|
|
95
|
-
export interface GlobalDefaultModelRoleCommit {
|
|
96
|
-
readonly previousDefault: ModelSelectorValue | undefined;
|
|
97
|
-
readonly previousModelRolesExisted: boolean;
|
|
98
|
-
readonly committedDefault: ModelSelectorValue | undefined;
|
|
99
|
-
readonly committedConfigVersion?: string;
|
|
100
|
-
readonly defaultRevision: number;
|
|
101
|
-
}
|
|
102
110
|
export interface SettingsOptions {
|
|
103
111
|
/** Current working directory for project settings discovery */
|
|
104
112
|
cwd?: string;
|
|
@@ -110,6 +118,17 @@ export interface SettingsOptions {
|
|
|
110
118
|
overrides?: Partial<Record<SettingPath, unknown>>;
|
|
111
119
|
}
|
|
112
120
|
|
|
121
|
+
function summarizeSettingsOptions(options: SettingsOptions | null): {
|
|
122
|
+
optionKeys: string[];
|
|
123
|
+
overrideKeys: string[];
|
|
124
|
+
} {
|
|
125
|
+
if (!options) return { optionKeys: [], overrideKeys: [] };
|
|
126
|
+
return {
|
|
127
|
+
optionKeys: Object.keys(options).sort(),
|
|
128
|
+
overrideKeys: Object.keys(options.overrides ?? {}).sort(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
/** Additional layer setup for {@link Settings.isolated}. */
|
|
114
133
|
export interface IsolatedSettingsOptions {
|
|
115
134
|
/** Initial runtime overrides. Notification paths are rejected. */
|
|
@@ -135,6 +154,13 @@ function isNotificationSettingsPath(path: string): boolean {
|
|
|
135
154
|
);
|
|
136
155
|
}
|
|
137
156
|
|
|
157
|
+
function isAtomicSettingsPath(path: string): boolean {
|
|
158
|
+
return (
|
|
159
|
+
Object.hasOwn(SETTINGS_SCHEMA, path) ||
|
|
160
|
+
(path.startsWith("modelRoles.") && path.split(".").every(segment => segment.length > 0))
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
138
164
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
139
165
|
// Path Utilities
|
|
140
166
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -191,6 +217,14 @@ function stringArrayFromUnknown(value: unknown): string[] {
|
|
|
191
217
|
return [];
|
|
192
218
|
}
|
|
193
219
|
|
|
220
|
+
function normalizeSessionDirectoryMigration(raw: RawSettings): void {
|
|
221
|
+
const session = rawSettingsRecord(raw.session);
|
|
222
|
+
if (!session) return;
|
|
223
|
+
if (session.directoryMigration !== "copy-retain" && session.directoryMigration !== "disabled") {
|
|
224
|
+
delete session.directoryMigration;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
194
228
|
function rawSettingsRecord(value: unknown): RawSettings | undefined {
|
|
195
229
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
196
230
|
return value as RawSettings;
|
|
@@ -259,27 +293,6 @@ function resolvePathScopedStringArray(settingPath: SettingPath, value: unknown,
|
|
|
259
293
|
|
|
260
294
|
return resolved;
|
|
261
295
|
}
|
|
262
|
-
type DefaultModelRoleOwnership = {
|
|
263
|
-
generation: number;
|
|
264
|
-
configVersion?: string;
|
|
265
|
-
defaultConfigVersion?: string;
|
|
266
|
-
defaultLineageKnown: boolean;
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
function readConfigVersion(filePath: string): string | undefined {
|
|
270
|
-
try {
|
|
271
|
-
const stat = fs.statSync(filePath, { bigint: true });
|
|
272
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}`;
|
|
273
|
-
} catch (error) {
|
|
274
|
-
if (isEnoent(error)) return undefined;
|
|
275
|
-
throw error;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function defaultModelRoleFrom(raw: RawSettings): ModelSelectorValue | undefined {
|
|
280
|
-
const value = rawSettingsRecord(raw.modelRoles)?.default;
|
|
281
|
-
return isModelSelectorValue(value) ? (Array.isArray(value) ? [...value] : value) : undefined;
|
|
282
|
-
}
|
|
283
296
|
|
|
284
297
|
function setRawModelRole(
|
|
285
298
|
raw: RawSettings,
|
|
@@ -300,17 +313,6 @@ function setRawModelRole(
|
|
|
300
313
|
raw.modelRoles = { ...roles, [role]: modelId };
|
|
301
314
|
}
|
|
302
315
|
|
|
303
|
-
function updateModelRolesPatch(
|
|
304
|
-
patch: SettingsPatch,
|
|
305
|
-
role: string,
|
|
306
|
-
modelId: ModelSelectorValue | undefined,
|
|
307
|
-
removeContainerWhenEmpty = false,
|
|
308
|
-
): SettingsPatch {
|
|
309
|
-
const raw: RawSettings = { modelRoles: structuredClone(patch.value) };
|
|
310
|
-
setRawModelRole(raw, role, modelId, removeContainerWhenEmpty);
|
|
311
|
-
return { ...patch, value: raw.modelRoles };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
316
|
function settingsPatchKey(patch: SettingsPatch): string {
|
|
315
317
|
return patch.modelRole ? `modelRoles.${patch.modelRole}` : patch.path;
|
|
316
318
|
}
|
|
@@ -339,6 +341,13 @@ export class Settings implements NotificationSettingsReader {
|
|
|
339
341
|
|
|
340
342
|
/** Global settings from config.yml */
|
|
341
343
|
#global: RawSettings = {};
|
|
344
|
+
/**
|
|
345
|
+
* Raw notification syntax retained across schema reconciliation so notification
|
|
346
|
+
* validation matches the lightweight config reader until each leaf is repaired.
|
|
347
|
+
*/
|
|
348
|
+
#rawNotificationConfig: RawSettings | undefined = {};
|
|
349
|
+
/** Raw notification syntax from the last durable config read, before local replay. */
|
|
350
|
+
#durableRawNotificationConfig: RawSettings | undefined = {};
|
|
342
351
|
/** Project settings from .Anthropic model/settings.yml etc */
|
|
343
352
|
#project: RawSettings = {};
|
|
344
353
|
/** Runtime overrides (not persisted) */
|
|
@@ -351,16 +360,23 @@ export class Settings implements NotificationSettingsReader {
|
|
|
351
360
|
#nextGeneration = 0;
|
|
352
361
|
#pathRevisions = new Map<string, number>();
|
|
353
362
|
#nextRevision = 0;
|
|
354
|
-
#defaultModelRoleOwnership: DefaultModelRoleOwnership = { generation: 0, defaultLineageKnown: true };
|
|
355
363
|
/** Pending debounced ordinary save; its queue slot is reserved immediately. */
|
|
356
364
|
#saveTimer?: NodeJS.Timeout;
|
|
357
365
|
#savePromise?: Promise<void>;
|
|
358
366
|
#pendingSaveSlot?: PendingSaveSlot;
|
|
359
|
-
#globalModelRoleTail: Promise<void> = Promise.resolve();
|
|
360
367
|
|
|
361
368
|
/** Legacy fallback migration warnings emitted once per settings instance. */
|
|
362
369
|
#legacyFallbackMigrationWarnings = 0;
|
|
363
370
|
#legacyFallbackMigrationGlobalFingerprint: string | undefined;
|
|
371
|
+
#schemaReport: SettingsSchemaReport = { issues: [], valid: true };
|
|
372
|
+
#schemaMigrationPending = false;
|
|
373
|
+
/** A newer config schema must never be rewritten by legacy migrations. */
|
|
374
|
+
#futureSchemaVersion = false;
|
|
375
|
+
#hasMalformedConfigRoot = false;
|
|
376
|
+
#hasInvalidNotificationConfiguration = false;
|
|
377
|
+
#notificationValidationGeneration = 0;
|
|
378
|
+
/** Notification subtree fingerprint from the last raw durable config read. */
|
|
379
|
+
#durableNotificationFingerprint: string | undefined;
|
|
364
380
|
|
|
365
381
|
/** Whether to persist changes */
|
|
366
382
|
#persist: boolean;
|
|
@@ -377,6 +393,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
377
393
|
setByPath(this.#overrides, key.split("."), structuredClone(value));
|
|
378
394
|
}
|
|
379
395
|
}
|
|
396
|
+
normalizeSessionDirectoryMigration(this.#overrides);
|
|
380
397
|
}
|
|
381
398
|
|
|
382
399
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -388,8 +405,17 @@ export class Settings implements NotificationSettingsReader {
|
|
|
388
405
|
* Call once at startup before accessing `settings`.
|
|
389
406
|
*/
|
|
390
407
|
static init(options: SettingsOptions = {}): Promise<Settings> {
|
|
391
|
-
if (globalInstancePromise)
|
|
408
|
+
if (globalInstancePromise) {
|
|
409
|
+
if (JSON.stringify(options) !== JSON.stringify(globalInitOptions)) {
|
|
410
|
+
logger.warn("Settings.init called again with different options; reusing existing settings instance", {
|
|
411
|
+
initialOptions: summarizeSettingsOptions(globalInitOptions),
|
|
412
|
+
requestedOptions: summarizeSettingsOptions(options),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return globalInstancePromise;
|
|
416
|
+
}
|
|
392
417
|
|
|
418
|
+
globalInitOptions = structuredClone(options);
|
|
393
419
|
const instance = new Settings(options);
|
|
394
420
|
const promise = instance.#load();
|
|
395
421
|
globalInstancePromise = promise;
|
|
@@ -407,6 +433,15 @@ export class Settings implements NotificationSettingsReader {
|
|
|
407
433
|
);
|
|
408
434
|
}
|
|
409
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Load settings for an explicit workspace without changing the global singleton.
|
|
438
|
+
* Managed-session policy resolution must be bound to the workspace being opened.
|
|
439
|
+
*/
|
|
440
|
+
static loadForScope(options: { cwd: string; agentDir?: string }): Promise<Settings> {
|
|
441
|
+
const instance = new Settings(options);
|
|
442
|
+
return instance.#load();
|
|
443
|
+
}
|
|
444
|
+
|
|
410
445
|
/**
|
|
411
446
|
* Create an isolated instance for testing with explicit user/global settings.
|
|
412
447
|
* Does not affect the global singleton.
|
|
@@ -419,8 +454,10 @@ export class Settings implements NotificationSettingsReader {
|
|
|
419
454
|
for (const [key, value] of Object.entries(globalSettings)) {
|
|
420
455
|
setByPath(instance.#global, key.split("."), structuredClone(value));
|
|
421
456
|
}
|
|
457
|
+
normalizeSessionDirectoryMigration(instance.#global);
|
|
422
458
|
|
|
423
459
|
instance.#rebuildMerged();
|
|
460
|
+
instance.#captureRawNotificationConfig(instance.#global);
|
|
424
461
|
return instance;
|
|
425
462
|
}
|
|
426
463
|
|
|
@@ -470,105 +507,9 @@ export class Settings implements NotificationSettingsReader {
|
|
|
470
507
|
* are deliberately excluded from this trust boundary.
|
|
471
508
|
*/
|
|
472
509
|
getNotificationSettingsSnapshot(): NotificationSettingsSnapshot {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const activation = this.#getGlobalResolved("notifications.telegram.activation");
|
|
477
|
-
const activationSnapshot =
|
|
478
|
-
activation && Object.keys(activation).length > 0 ? structuredClone(activation) : undefined;
|
|
479
|
-
const richEnabled = this.#getGlobalResolved("notifications.telegram.rich.enabled");
|
|
480
|
-
const richDraftEnabled = this.#getGlobalResolved("notifications.telegram.richDraft.enabled");
|
|
481
|
-
const nameTemplate = this.#getGlobalResolved("notifications.telegram.topics.nameTemplate");
|
|
482
|
-
const discordBotToken = this.#getGlobalResolved("notifications.discord.botToken");
|
|
483
|
-
const discordApplicationId = this.#getGlobalResolved("notifications.discord.applicationId");
|
|
484
|
-
const discordGuildId = this.#getGlobalResolved("notifications.discord.guildId");
|
|
485
|
-
const discordParentChannelId = this.#getGlobalResolved("notifications.discord.parentChannelId");
|
|
486
|
-
const slackBotToken = this.#getGlobalResolved("notifications.slack.botToken");
|
|
487
|
-
const slackAppToken = this.#getGlobalResolved("notifications.slack.appToken");
|
|
488
|
-
const slackWorkspaceId = this.#getGlobalResolved("notifications.slack.workspaceId");
|
|
489
|
-
const slackChannelId = this.#getGlobalResolved("notifications.slack.channelId");
|
|
490
|
-
const slackAuthorizedUserId = this.#getGlobalResolved("notifications.slack.authorizedUserId");
|
|
491
|
-
const redact = this.#getGlobalResolved("notifications.redact");
|
|
492
|
-
const verbosity = this.#getGlobalResolved("notifications.verbosity");
|
|
493
|
-
const sessionScope = this.#getGlobalResolved("notifications.sessionScope");
|
|
494
|
-
const idleTimeoutMs = this.#getGlobalResolved("notifications.daemon.idleTimeoutMs");
|
|
495
|
-
|
|
496
|
-
return {
|
|
497
|
-
enabled: typeof enabled === "boolean" ? enabled : getDefault("notifications.enabled"),
|
|
498
|
-
telegram: {
|
|
499
|
-
botToken:
|
|
500
|
-
typeof botToken === "string" && botToken.length > 0
|
|
501
|
-
? botToken
|
|
502
|
-
: getDefault("notifications.telegram.botToken"),
|
|
503
|
-
chatId:
|
|
504
|
-
typeof chatId === "string" && chatId.length > 0 ? chatId : getDefault("notifications.telegram.chatId"),
|
|
505
|
-
...(activationSnapshot === undefined ? {} : { activation: activationSnapshot }),
|
|
506
|
-
rich: {
|
|
507
|
-
enabled:
|
|
508
|
-
typeof richEnabled === "boolean" ? richEnabled : getDefault("notifications.telegram.rich.enabled"),
|
|
509
|
-
},
|
|
510
|
-
richDraft: {
|
|
511
|
-
enabled:
|
|
512
|
-
typeof richDraftEnabled === "boolean"
|
|
513
|
-
? richDraftEnabled
|
|
514
|
-
: getDefault("notifications.telegram.richDraft.enabled"),
|
|
515
|
-
},
|
|
516
|
-
topics: {
|
|
517
|
-
nameTemplate:
|
|
518
|
-
typeof nameTemplate === "string" && nameTemplate.length > 0
|
|
519
|
-
? nameTemplate
|
|
520
|
-
: getDefault("notifications.telegram.topics.nameTemplate"),
|
|
521
|
-
},
|
|
522
|
-
},
|
|
523
|
-
discord: {
|
|
524
|
-
botToken:
|
|
525
|
-
typeof discordBotToken === "string" && discordBotToken.length > 0
|
|
526
|
-
? discordBotToken
|
|
527
|
-
: getDefault("notifications.discord.botToken"),
|
|
528
|
-
applicationId:
|
|
529
|
-
typeof discordApplicationId === "string" && discordApplicationId.length > 0
|
|
530
|
-
? discordApplicationId
|
|
531
|
-
: getDefault("notifications.discord.applicationId"),
|
|
532
|
-
guildId:
|
|
533
|
-
typeof discordGuildId === "string" && discordGuildId.length > 0
|
|
534
|
-
? discordGuildId
|
|
535
|
-
: getDefault("notifications.discord.guildId"),
|
|
536
|
-
parentChannelId:
|
|
537
|
-
typeof discordParentChannelId === "string" && discordParentChannelId.length > 0
|
|
538
|
-
? discordParentChannelId
|
|
539
|
-
: getDefault("notifications.discord.parentChannelId"),
|
|
540
|
-
},
|
|
541
|
-
slack: {
|
|
542
|
-
botToken:
|
|
543
|
-
typeof slackBotToken === "string" && slackBotToken.length > 0
|
|
544
|
-
? slackBotToken
|
|
545
|
-
: getDefault("notifications.slack.botToken"),
|
|
546
|
-
appToken:
|
|
547
|
-
typeof slackAppToken === "string" && slackAppToken.length > 0
|
|
548
|
-
? slackAppToken
|
|
549
|
-
: getDefault("notifications.slack.appToken"),
|
|
550
|
-
workspaceId:
|
|
551
|
-
typeof slackWorkspaceId === "string" && slackWorkspaceId.length > 0
|
|
552
|
-
? slackWorkspaceId
|
|
553
|
-
: getDefault("notifications.slack.workspaceId"),
|
|
554
|
-
channelId:
|
|
555
|
-
typeof slackChannelId === "string" && slackChannelId.length > 0
|
|
556
|
-
? slackChannelId
|
|
557
|
-
: getDefault("notifications.slack.channelId"),
|
|
558
|
-
authorizedUserId:
|
|
559
|
-
typeof slackAuthorizedUserId === "string" && slackAuthorizedUserId.length > 0
|
|
560
|
-
? slackAuthorizedUserId
|
|
561
|
-
: getDefault("notifications.slack.authorizedUserId"),
|
|
562
|
-
},
|
|
563
|
-
redact: typeof redact === "boolean" ? redact : getDefault("notifications.redact"),
|
|
564
|
-
verbosity: verbosity === "verbose" || getDefault("notifications.verbosity") === "verbose" ? "verbose" : "lean",
|
|
565
|
-
sessionScope:
|
|
566
|
-
sessionScope === "primary" || getDefault("notifications.sessionScope") === "primary" ? "primary" : "all",
|
|
567
|
-
idleTimeoutMs:
|
|
568
|
-
typeof idleTimeoutMs === "number" && Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0
|
|
569
|
-
? idleTimeoutMs
|
|
570
|
-
: getDefault("notifications.daemon.idleTimeoutMs"),
|
|
571
|
-
};
|
|
510
|
+
return parseNotificationSettingsSnapshot(
|
|
511
|
+
this.#hasMalformedConfigRoot || this.#hasInvalidNotificationConfiguration ? null : this.#global,
|
|
512
|
+
);
|
|
572
513
|
}
|
|
573
514
|
|
|
574
515
|
/** Check whether a setting is present in loaded settings/overrides rather than coming from schema defaults. */
|
|
@@ -576,6 +517,11 @@ export class Settings implements NotificationSettingsReader {
|
|
|
576
517
|
return getByPath(this.#merged, path.split(".")) !== undefined;
|
|
577
518
|
}
|
|
578
519
|
|
|
520
|
+
/** Diagnostics from schema reconciliation during the most recent load. */
|
|
521
|
+
getSchemaReport(): SettingsSchemaReport {
|
|
522
|
+
return structuredClone(this.#schemaReport);
|
|
523
|
+
}
|
|
524
|
+
|
|
579
525
|
/**
|
|
580
526
|
* Set a setting value (sync).
|
|
581
527
|
* Updates global settings and reserves its background persistence slot before
|
|
@@ -589,27 +535,22 @@ export class Settings implements NotificationSettingsReader {
|
|
|
589
535
|
this.#set(path, value, true);
|
|
590
536
|
}
|
|
591
537
|
|
|
592
|
-
#set<P extends SettingPath>(path: P, value: SettingValue<P>,
|
|
538
|
+
#set<P extends SettingPath>(path: P, value: SettingValue<P>, _defaultModelRoleMayHaveChanged: boolean): void {
|
|
593
539
|
const prev = this.get(path);
|
|
594
540
|
const clonedValue = structuredClone(value);
|
|
595
|
-
let modelRoleRevision: number | undefined;
|
|
596
|
-
if (path === "modelRoles" && defaultModelRoleMayHaveChanged) {
|
|
597
|
-
this.#defaultModelRoleOwnership.generation += 1;
|
|
598
|
-
modelRoleRevision = this.#defaultModelRoleOwnership.generation;
|
|
599
|
-
}
|
|
600
541
|
const patch: SettingsPatch = {
|
|
601
542
|
path,
|
|
602
543
|
value: clonedValue,
|
|
603
544
|
generation: ++this.#nextGeneration,
|
|
604
545
|
revision: ++this.#nextRevision,
|
|
605
|
-
modelRoleRevision,
|
|
606
|
-
configVersion: this.#defaultModelRoleOwnership.configVersion,
|
|
607
546
|
};
|
|
608
547
|
setByPath(this.#global, path.split("."), structuredClone(clonedValue));
|
|
548
|
+
this.#applyNotificationMutationToRaw(path, clonedValue);
|
|
609
549
|
this.#pathRevisions.set(path, patch.revision);
|
|
610
550
|
this.#modified.set(path, patch);
|
|
611
551
|
|
|
612
552
|
this.#rebuildMerged();
|
|
553
|
+
this.#revalidateNotificationSettingsAfterMutation([path]);
|
|
613
554
|
this.#queueSave();
|
|
614
555
|
|
|
615
556
|
const hook = SETTING_HOOKS[path];
|
|
@@ -622,23 +563,18 @@ export class Settings implements NotificationSettingsReader {
|
|
|
622
563
|
*/
|
|
623
564
|
unset<P extends SettingPath>(path: P): void {
|
|
624
565
|
const prev = this.get(path);
|
|
625
|
-
let modelRoleRevision: number | undefined;
|
|
626
|
-
if (path === "modelRoles") {
|
|
627
|
-
this.#defaultModelRoleOwnership.generation += 1;
|
|
628
|
-
modelRoleRevision = this.#defaultModelRoleOwnership.generation;
|
|
629
|
-
}
|
|
630
566
|
const patch: SettingsPatch = {
|
|
631
567
|
path,
|
|
632
568
|
value: undefined,
|
|
633
569
|
generation: ++this.#nextGeneration,
|
|
634
570
|
revision: ++this.#nextRevision,
|
|
635
|
-
modelRoleRevision,
|
|
636
|
-
configVersion: this.#defaultModelRoleOwnership.configVersion,
|
|
637
571
|
};
|
|
638
572
|
deleteByPath(this.#global, path.split("."));
|
|
573
|
+
this.#applyNotificationMutationToRaw(path, undefined);
|
|
639
574
|
this.#pathRevisions.set(path, patch.revision);
|
|
640
575
|
this.#modified.set(path, patch);
|
|
641
576
|
this.#rebuildMerged();
|
|
577
|
+
this.#revalidateNotificationSettingsAfterMutation([path]);
|
|
642
578
|
this.#queueSave();
|
|
643
579
|
|
|
644
580
|
const hook = SETTING_HOOKS[path];
|
|
@@ -651,11 +587,84 @@ export class Settings implements NotificationSettingsReader {
|
|
|
651
587
|
*/
|
|
652
588
|
async commitAtomicBatch(patches: readonly SettingsAtomicPatch[]): Promise<CasReceipt> {
|
|
653
589
|
if (!this.#persist || !this.#configPath) {
|
|
654
|
-
|
|
590
|
+
const notificationValidationGuard = this.#notificationValidationRestoreGuard();
|
|
591
|
+
const changes = new Map<string, { before: unknown; beforeHash: string; afterHash: string }>();
|
|
592
|
+
for (const patch of patches) {
|
|
593
|
+
if (!isAtomicSettingsPath(patch.path)) {
|
|
594
|
+
throw new Error(`Unknown setting path for atomic batch: ${patch.path}`);
|
|
595
|
+
}
|
|
596
|
+
if (patch.op === "set" && patch.value === undefined) {
|
|
597
|
+
throw new TypeError(`Settings set patch for ${patch.path} cannot carry undefined; use unset instead.`);
|
|
598
|
+
}
|
|
599
|
+
if (!changes.has(patch.path)) {
|
|
600
|
+
changes.set(patch.path, {
|
|
601
|
+
before: structuredClone(getByPath(this.#global, patch.path.split("."))),
|
|
602
|
+
beforeHash: atomicYamlPathHash(this.#global, patch.path),
|
|
603
|
+
afterHash: "",
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
for (const patch of patches) {
|
|
608
|
+
if (patch.op === "set") {
|
|
609
|
+
setByPath(this.#global, patch.path.split("."), structuredClone(patch.value));
|
|
610
|
+
this.#applyNotificationMutationToRaw(patch.path, patch.value);
|
|
611
|
+
} else {
|
|
612
|
+
deleteByPath(this.#global, patch.path.split("."));
|
|
613
|
+
this.#applyNotificationMutationToRaw(patch.path, undefined);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
for (const [patchPath, change] of changes) {
|
|
617
|
+
change.afterHash = atomicYamlPathHash(this.#global, patchPath);
|
|
618
|
+
}
|
|
619
|
+
this.#rebuildMerged();
|
|
620
|
+
this.#revalidateNotificationSettingsAfterMutation(patches.map(patch => patch.path));
|
|
621
|
+
this.#recordNotificationValidationBatchApply(
|
|
622
|
+
notificationValidationGuard,
|
|
623
|
+
patches.map(patch => patch.path),
|
|
624
|
+
);
|
|
625
|
+
let discarded = false;
|
|
626
|
+
let receipt: CasReceipt;
|
|
627
|
+
receipt = {
|
|
628
|
+
revisions: [],
|
|
629
|
+
discard: () => {
|
|
630
|
+
discarded = true;
|
|
631
|
+
},
|
|
632
|
+
restore: async () => {
|
|
633
|
+
if (discarded) return { status: "discarded" } as const;
|
|
634
|
+
const conflicts = [...changes].flatMap(([patchPath, change]) =>
|
|
635
|
+
atomicYamlPathHash(this.#global, patchPath) === change.afterHash ? [] : [patchPath],
|
|
636
|
+
);
|
|
637
|
+
if (conflicts.length > 0) return { status: "conflict", paths: conflicts } as const;
|
|
638
|
+
const restoreNotificationValidationState = this.#canRestoreNotificationValidationState(
|
|
639
|
+
notificationValidationGuard,
|
|
640
|
+
changes.keys(),
|
|
641
|
+
);
|
|
642
|
+
for (const [patchPath, change] of changes) {
|
|
643
|
+
if (change.beforeHash === atomicYamlPathHash({}, patchPath)) {
|
|
644
|
+
deleteByPath(this.#global, patchPath.split("."));
|
|
645
|
+
this.#applyNotificationMutationToRaw(patchPath, undefined);
|
|
646
|
+
} else {
|
|
647
|
+
setByPath(this.#global, patchPath.split("."), structuredClone(change.before));
|
|
648
|
+
this.#applyNotificationMutationToRaw(patchPath, change.before);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const modelRoles = rawSettingsRecord(this.#global.modelRoles);
|
|
652
|
+
if (changes.has("modelRoles.default") && modelRoles && Object.keys(modelRoles).length === 0) {
|
|
653
|
+
delete this.#global.modelRoles;
|
|
654
|
+
}
|
|
655
|
+
this.#rebuildMerged();
|
|
656
|
+
this.#revalidateNotificationSettingsAfterMutation(changes.keys());
|
|
657
|
+
if (restoreNotificationValidationState) {
|
|
658
|
+
this.#restoreNotificationValidationState(notificationValidationGuard.state);
|
|
659
|
+
}
|
|
660
|
+
return { status: "restored", receipt } as const;
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
return receipt;
|
|
655
664
|
}
|
|
656
665
|
|
|
657
666
|
const durablePatches: AtomicYamlPatch[] = patches.map(patch => {
|
|
658
|
-
if (!
|
|
667
|
+
if (!isAtomicSettingsPath(patch.path)) {
|
|
659
668
|
throw new Error(`Unknown setting path for atomic batch: ${patch.path}`);
|
|
660
669
|
}
|
|
661
670
|
if (patch.op === "unset") return { path: patch.path, op: "unset" };
|
|
@@ -668,6 +677,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
668
677
|
// A durable batch is a causal barrier: close the earlier ordinary debounce
|
|
669
678
|
// inside its already-reserved slot before queueing this batch.
|
|
670
679
|
this.#releasePendingSaveSlot();
|
|
680
|
+
const notificationValidationGuard = this.#notificationValidationRestoreGuard();
|
|
671
681
|
|
|
672
682
|
const revisions = durablePatches.map(patch => ({
|
|
673
683
|
patch,
|
|
@@ -678,9 +688,13 @@ export class Settings implements NotificationSettingsReader {
|
|
|
678
688
|
|
|
679
689
|
try {
|
|
680
690
|
const receipt = await applyAtomicYamlPatches(this.#configPath, durablePatches, {
|
|
681
|
-
|
|
691
|
+
validateRoot: (root, currentPatches) =>
|
|
692
|
+
this.#rejectAtomicNotificationRepairForMalformedRoot(currentPatches, root),
|
|
693
|
+
onRestored: restoredPatches =>
|
|
694
|
+
this.#applyRestoredDurableBatch(revisions, restoredPatches, notificationValidationGuard),
|
|
682
695
|
});
|
|
683
|
-
this.#applyDurableBatch(revisions);
|
|
696
|
+
const appliedNotificationMutation = this.#applyDurableBatch(revisions);
|
|
697
|
+
this.#recordNotificationValidationBatchApply(notificationValidationGuard, appliedNotificationMutation);
|
|
684
698
|
return receipt;
|
|
685
699
|
} catch (error) {
|
|
686
700
|
for (const entry of revisions) {
|
|
@@ -707,13 +721,14 @@ export class Settings implements NotificationSettingsReader {
|
|
|
707
721
|
|
|
708
722
|
this.#releasePendingSaveSlot();
|
|
709
723
|
let revisions: DurableBatchRevision[] = [];
|
|
724
|
+
const notificationValidationGuard = this.#notificationValidationRestoreGuard();
|
|
710
725
|
try {
|
|
711
726
|
const receipt = await applyAtomicYamlPatchesWithCurrent(
|
|
712
727
|
this.#configPath,
|
|
713
728
|
async current => {
|
|
714
729
|
const patches = await buildPatches(structuredClone(current));
|
|
715
730
|
const durablePatches: AtomicYamlPatch[] = patches.map(patch => {
|
|
716
|
-
if (!
|
|
731
|
+
if (!isAtomicSettingsPath(patch.path)) {
|
|
717
732
|
throw new Error(`Unknown setting path for atomic batch: ${patch.path}`);
|
|
718
733
|
}
|
|
719
734
|
if (patch.op === "unset") return { path: patch.path, op: "unset" };
|
|
@@ -733,10 +748,14 @@ export class Settings implements NotificationSettingsReader {
|
|
|
733
748
|
return durablePatches;
|
|
734
749
|
},
|
|
735
750
|
{
|
|
736
|
-
|
|
751
|
+
validateRoot: (root, currentPatches) =>
|
|
752
|
+
this.#rejectAtomicNotificationRepairForMalformedRoot(currentPatches, root),
|
|
753
|
+
onRestored: restoredPatches =>
|
|
754
|
+
this.#applyRestoredDurableBatch(revisions, restoredPatches, notificationValidationGuard),
|
|
737
755
|
},
|
|
738
756
|
);
|
|
739
|
-
this.#applyDurableBatch(revisions);
|
|
757
|
+
const appliedNotificationMutation = this.#applyDurableBatch(revisions);
|
|
758
|
+
this.#recordNotificationValidationBatchApply(notificationValidationGuard, appliedNotificationMutation);
|
|
740
759
|
return receipt;
|
|
741
760
|
} catch (error) {
|
|
742
761
|
for (const entry of revisions) {
|
|
@@ -821,9 +840,12 @@ export class Settings implements NotificationSettingsReader {
|
|
|
821
840
|
inMemory: !this.#persist,
|
|
822
841
|
});
|
|
823
842
|
cloned.#storage = this.#storage;
|
|
824
|
-
cloned.#
|
|
843
|
+
cloned.#futureSchemaVersion = this.#futureSchemaVersion;
|
|
825
844
|
|
|
826
845
|
cloned.#global = structuredClone(this.#global);
|
|
846
|
+
cloned.#rawNotificationConfig = structuredClone(this.#rawNotificationConfig);
|
|
847
|
+
cloned.#durableRawNotificationConfig = structuredClone(this.#durableRawNotificationConfig);
|
|
848
|
+
cloned.#durableNotificationFingerprint = this.#durableNotificationFingerprint;
|
|
827
849
|
cloned.#project = this.#persist ? await cloned.#loadProjectSettings() : structuredClone(this.#project);
|
|
828
850
|
cloned.#overrides = structuredClone(this.#overrides);
|
|
829
851
|
await cloned.#normalizeAfterLoad();
|
|
@@ -918,11 +940,6 @@ export class Settings implements NotificationSettingsReader {
|
|
|
918
940
|
}
|
|
919
941
|
|
|
920
942
|
setGlobalModelRole(role: ModelRole | string, modelId: ModelSelectorValue | undefined): void {
|
|
921
|
-
let modelRoleRevision: number | undefined;
|
|
922
|
-
if (role === "default") {
|
|
923
|
-
this.#defaultModelRoleOwnership.generation += 1;
|
|
924
|
-
modelRoleRevision = this.#defaultModelRoleOwnership.generation;
|
|
925
|
-
}
|
|
926
943
|
const revision = ++this.#nextRevision;
|
|
927
944
|
const patch: SettingsPatch = {
|
|
928
945
|
path: "modelRoles",
|
|
@@ -930,8 +947,6 @@ export class Settings implements NotificationSettingsReader {
|
|
|
930
947
|
generation: ++this.#nextGeneration,
|
|
931
948
|
revision,
|
|
932
949
|
modelRole: role,
|
|
933
|
-
modelRoleRevision,
|
|
934
|
-
configVersion: this.#defaultModelRoleOwnership.configVersion,
|
|
935
950
|
};
|
|
936
951
|
setRawModelRole(this.#global, role, modelId);
|
|
937
952
|
this.#pathRevisions.set("modelRoles", revision);
|
|
@@ -940,182 +955,31 @@ export class Settings implements NotificationSettingsReader {
|
|
|
940
955
|
this.#queueSave();
|
|
941
956
|
}
|
|
942
957
|
|
|
943
|
-
setGlobalModelRoleAndFlush(
|
|
958
|
+
async setGlobalModelRoleAndFlush(
|
|
944
959
|
role: ModelRole | string,
|
|
945
960
|
modelId: ModelSelectorValue | undefined,
|
|
946
|
-
): Promise<
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
()
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
restoreGlobalDefaultModelRoleIfCurrent(commit: GlobalDefaultModelRoleCommit): Promise<boolean> {
|
|
956
|
-
const transaction = this.#globalModelRoleTail.then(() => this.#restoreGlobalDefaultModelRoleIfCurrent(commit));
|
|
957
|
-
this.#globalModelRoleTail = transaction.then(
|
|
958
|
-
() => undefined,
|
|
959
|
-
() => undefined,
|
|
960
|
-
);
|
|
961
|
-
return transaction;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
async #commitGlobalModelRoleAndFlush(
|
|
965
|
-
role: ModelRole | string,
|
|
966
|
-
modelId: ModelSelectorValue | undefined,
|
|
967
|
-
): Promise<GlobalDefaultModelRoleCommit> {
|
|
968
|
-
if (this.#persist) await this.flushOrThrow();
|
|
969
|
-
const previousDefault = defaultModelRoleFrom(this.#global);
|
|
970
|
-
const previousModelRolesExisted = Object.hasOwn(this.#global, "modelRoles");
|
|
971
|
-
const previousDefaultRevision = this.#defaultModelRoleOwnership.generation;
|
|
972
|
-
let defaultRevision = previousDefaultRevision;
|
|
973
|
-
if (role === "default") {
|
|
974
|
-
defaultRevision += 1;
|
|
975
|
-
this.#defaultModelRoleOwnership.generation = defaultRevision;
|
|
976
|
-
}
|
|
977
|
-
this.#setGlobalModelRoleInMemory(role, modelId, false, true);
|
|
978
|
-
|
|
979
|
-
if (!this.#persist || !this.#configPath) {
|
|
980
|
-
return {
|
|
981
|
-
previousDefault,
|
|
982
|
-
previousModelRolesExisted,
|
|
983
|
-
committedDefault: defaultModelRoleFrom(this.#global),
|
|
984
|
-
defaultRevision,
|
|
985
|
-
committedConfigVersion: undefined,
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
let durableBeforeWrite: RawSettings | undefined;
|
|
990
|
-
let durableVersionBeforeWrite: string | undefined;
|
|
991
|
-
try {
|
|
992
|
-
const result = await reserveAtomicYamlUpdateSlot(this.#configPath, () => ({
|
|
993
|
-
apply: current => {
|
|
994
|
-
durableBeforeWrite = structuredClone(current);
|
|
995
|
-
durableVersionBeforeWrite = readConfigVersion(this.#configPath!);
|
|
996
|
-
const durablePreviousDefault = defaultModelRoleFrom(current);
|
|
997
|
-
const durablePreviousModelRolesExisted = Object.hasOwn(current, "modelRoles");
|
|
998
|
-
setRawModelRole(current, role, modelId);
|
|
999
|
-
return {
|
|
1000
|
-
durablePreviousDefault,
|
|
1001
|
-
durablePreviousModelRolesExisted,
|
|
1002
|
-
committedDefault: defaultModelRoleFrom(current),
|
|
1003
|
-
committedConfigVersion: undefined as string | undefined,
|
|
1004
|
-
defaultRevision,
|
|
1005
|
-
};
|
|
1006
|
-
},
|
|
1007
|
-
committed: (current, result) => {
|
|
1008
|
-
const committedConfigVersion = readConfigVersion(this.#configPath!);
|
|
1009
|
-
this.#replaceGlobalWithDurable(
|
|
1010
|
-
current,
|
|
1011
|
-
committedConfigVersion,
|
|
1012
|
-
role === "default",
|
|
1013
|
-
durableVersionBeforeWrite,
|
|
1014
|
-
);
|
|
1015
|
-
result.committedConfigVersion = committedConfigVersion;
|
|
1016
|
-
},
|
|
1017
|
-
}));
|
|
1018
|
-
return {
|
|
1019
|
-
previousDefault: result.durablePreviousDefault,
|
|
1020
|
-
previousModelRolesExisted: result.durablePreviousModelRolesExisted,
|
|
1021
|
-
committedDefault: result.committedDefault,
|
|
1022
|
-
committedConfigVersion: result.committedConfigVersion,
|
|
1023
|
-
defaultRevision: result.defaultRevision,
|
|
1024
|
-
};
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
if (role === "default" && this.#defaultModelRoleOwnership.generation === defaultRevision) {
|
|
1027
|
-
if (durableBeforeWrite) {
|
|
1028
|
-
this.#replaceGlobalWithDurable(durableBeforeWrite, durableVersionBeforeWrite, false);
|
|
1029
|
-
} else {
|
|
1030
|
-
this.#setGlobalModelRoleInMemory("default", previousDefault, !previousModelRolesExisted, true);
|
|
1031
|
-
}
|
|
1032
|
-
this.#defaultModelRoleOwnership.generation = previousDefaultRevision;
|
|
1033
|
-
}
|
|
1034
|
-
throw error;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
async #restoreGlobalDefaultModelRoleIfCurrent(commit: GlobalDefaultModelRoleCommit): Promise<boolean> {
|
|
1039
|
-
if (this.#defaultModelRoleOwnership.generation !== commit.defaultRevision) return false;
|
|
1040
|
-
|
|
1041
|
-
if (!this.#persist || !this.#configPath) {
|
|
1042
|
-
if (defaultModelRoleFrom(this.#global) !== commit.committedDefault) return false;
|
|
1043
|
-
this.#setGlobalModelRoleInMemory("default", commit.previousDefault, !commit.previousModelRolesExisted);
|
|
1044
|
-
this.#defaultModelRoleOwnership.generation += 1;
|
|
1045
|
-
return true;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
const restored = await reserveAtomicYamlUpdateSlot(this.#configPath, () => ({
|
|
1049
|
-
apply: current => {
|
|
1050
|
-
if (this.#defaultModelRoleOwnership.generation !== commit.defaultRevision) return false;
|
|
1051
|
-
const currentConfigVersion = readConfigVersion(this.#configPath!);
|
|
1052
|
-
if (defaultModelRoleFrom(current) !== commit.committedDefault) return false;
|
|
1053
|
-
if (
|
|
1054
|
-
commit.committedConfigVersion !== undefined &&
|
|
1055
|
-
currentConfigVersion !== commit.committedConfigVersion &&
|
|
1056
|
-
!(
|
|
1057
|
-
this.#defaultModelRoleOwnership.defaultConfigVersion === commit.committedConfigVersion &&
|
|
1058
|
-
this.#defaultModelRoleOwnership.defaultLineageKnown &&
|
|
1059
|
-
this.#defaultModelRoleOwnership.configVersion === currentConfigVersion
|
|
1060
|
-
)
|
|
1061
|
-
) {
|
|
1062
|
-
return false;
|
|
1063
|
-
}
|
|
1064
|
-
setRawModelRole(current, "default", commit.previousDefault, !commit.previousModelRolesExisted);
|
|
1065
|
-
return { currentConfigVersion };
|
|
1066
|
-
},
|
|
1067
|
-
shouldWrite: result => result !== false,
|
|
1068
|
-
committed: (current, result) => {
|
|
1069
|
-
if (result === false) return;
|
|
1070
|
-
const restoredConfigVersion = readConfigVersion(this.#configPath!);
|
|
1071
|
-
this.#replaceGlobalWithDurable(current, restoredConfigVersion, true, result.currentConfigVersion);
|
|
1072
|
-
this.#defaultModelRoleOwnership.generation += 1;
|
|
1073
|
-
},
|
|
1074
|
-
}));
|
|
1075
|
-
if (!restored) return false;
|
|
1076
|
-
await this.flushOrThrow();
|
|
1077
|
-
return true;
|
|
961
|
+
): Promise<CasReceipt> {
|
|
962
|
+
return this.commitAtomicBatchWithCurrent(current => {
|
|
963
|
+
const roles = rawSettingsRecord(current.modelRoles) ?? {};
|
|
964
|
+
const next = { ...roles };
|
|
965
|
+
if (modelId === undefined) delete next[role];
|
|
966
|
+
else next[role] = modelId;
|
|
967
|
+
return [{ path: "modelRoles", op: "set", value: next }];
|
|
968
|
+
});
|
|
1078
969
|
}
|
|
1079
970
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
modelId: ModelSelectorValue | undefined,
|
|
1083
|
-
removeContainerWhenEmpty: boolean,
|
|
1084
|
-
updatePendingPatch = false,
|
|
1085
|
-
): void {
|
|
1086
|
-
setRawModelRole(this.#global, role, modelId, removeContainerWhenEmpty);
|
|
1087
|
-
if (updatePendingPatch) {
|
|
1088
|
-
const rootPatch = this.#modified.get("modelRoles");
|
|
1089
|
-
if (rootPatch) {
|
|
1090
|
-
this.#modified.set("modelRoles", updateModelRolesPatch(rootPatch, role, modelId, removeContainerWhenEmpty));
|
|
1091
|
-
}
|
|
1092
|
-
const rolePatch = this.#modified.get(`modelRoles.${role}`);
|
|
1093
|
-
if (rolePatch) {
|
|
1094
|
-
this.#modified.set(`modelRoles.${role}`, { ...rolePatch, value: modelId });
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
this.#rebuildMerged();
|
|
971
|
+
async restoreGlobalDefaultModelRoleIfCurrent(commit: CasReceipt): Promise<boolean> {
|
|
972
|
+
return (await commit.restore()).status === "restored";
|
|
1098
973
|
}
|
|
1099
974
|
|
|
1100
|
-
#replaceGlobalWithDurable(
|
|
1101
|
-
current: RawSettings,
|
|
1102
|
-
configVersion?: string,
|
|
1103
|
-
defaultChanged = false,
|
|
1104
|
-
predecessorConfigVersion = configVersion,
|
|
1105
|
-
): void {
|
|
975
|
+
#replaceGlobalWithDurable(current: RawSettings): void {
|
|
1106
976
|
this.#global = current;
|
|
1107
977
|
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const externalLineageBreak = predecessorConfigVersion !== this.#defaultModelRoleOwnership.configVersion;
|
|
1111
|
-
this.#defaultModelRoleOwnership.configVersion = configVersion;
|
|
1112
|
-
if (defaultChanged) {
|
|
1113
|
-
this.#defaultModelRoleOwnership.defaultConfigVersion = configVersion;
|
|
1114
|
-
this.#defaultModelRoleOwnership.defaultLineageKnown = true;
|
|
1115
|
-
} else if (externalLineageBreak) {
|
|
1116
|
-
this.#defaultModelRoleOwnership.defaultLineageKnown = false;
|
|
978
|
+
applySettingsPatch(this.#global, { ...patch, value: structuredClone(patch.value) });
|
|
979
|
+
this.#applyNotificationMutationToRaw(patch.path, patch.value);
|
|
1117
980
|
}
|
|
1118
981
|
this.#rebuildMerged();
|
|
982
|
+
this.#recomputeNotificationValidationFromRaw();
|
|
1119
983
|
}
|
|
1120
984
|
/**
|
|
1121
985
|
* Set an agent model override while keeping any live runtime override aligned.
|
|
@@ -1185,33 +1049,83 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1185
1049
|
// #loadYaml then reads; migration's db fallback needs #storage opened.
|
|
1186
1050
|
const projectPromise = this.#loadProjectSettings();
|
|
1187
1051
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
this.#
|
|
1195
|
-
|
|
1052
|
+
try {
|
|
1053
|
+
if (this.#persist) {
|
|
1054
|
+
this.#storage = await AgentStorage.open(getAgentDbPath(this.#agentDir));
|
|
1055
|
+
await this.#migrateFromLegacy();
|
|
1056
|
+
this.#global = await this.#loadYaml(this.#configPath!);
|
|
1057
|
+
}
|
|
1058
|
+
if (this.#schemaMigrationPending)
|
|
1059
|
+
this.#recordLegacyFallbackMigrationPatch("configSchemaVersion", CONFIG_SCHEMA_VERSION);
|
|
1196
1060
|
|
|
1197
|
-
|
|
1061
|
+
this.#project = await projectPromise;
|
|
1198
1062
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1063
|
+
await this.#normalizeAfterLoad();
|
|
1064
|
+
if (this.#schemaReport.issues.length > 0) {
|
|
1065
|
+
logger.warn("Settings: schema reconciliation found configuration issues", {
|
|
1066
|
+
issues: this.#schemaReport.issues.map(issue => `${issue.kind}:${issue.path}`),
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
return this;
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
this.#storage?.close();
|
|
1072
|
+
throw error;
|
|
1073
|
+
}
|
|
1201
1074
|
}
|
|
1202
1075
|
|
|
1203
1076
|
async #loadYaml(filePath: string): Promise<RawSettings> {
|
|
1077
|
+
this.#hasMalformedConfigRoot = false;
|
|
1078
|
+
this.#hasInvalidNotificationConfiguration = false;
|
|
1079
|
+
this.#captureRawNotificationConfig({});
|
|
1204
1080
|
try {
|
|
1205
1081
|
const content = await Bun.file(filePath).text();
|
|
1206
1082
|
const parsed = YAML.parse(content);
|
|
1083
|
+
if (parsed === undefined) return {};
|
|
1207
1084
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1085
|
+
this.#hasMalformedConfigRoot = true;
|
|
1086
|
+
this.#captureRawNotificationConfig(undefined);
|
|
1208
1087
|
return {};
|
|
1209
1088
|
}
|
|
1210
|
-
|
|
1089
|
+
const parsedRaw = parsed as RawSettings;
|
|
1090
|
+
if (filePath === this.#configPath) this.#captureRawNotificationConfig(parsedRaw);
|
|
1091
|
+
if (filePath === this.#configPath) {
|
|
1092
|
+
try {
|
|
1093
|
+
parseNotificationSettingsSnapshot(parsedRaw);
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
if (!(error instanceof Error) || error.message !== "gjc_notify_daemon_invalid_configuration")
|
|
1096
|
+
throw error;
|
|
1097
|
+
this.#hasInvalidNotificationConfiguration = true;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
this.#futureSchemaVersion =
|
|
1101
|
+
filePath === this.#configPath &&
|
|
1102
|
+
typeof parsedRaw.configSchemaVersion === "number" &&
|
|
1103
|
+
parsedRaw.configSchemaVersion > CONFIG_SCHEMA_VERSION;
|
|
1104
|
+
|
|
1105
|
+
const configSchemaVersion = parsedRaw.configSchemaVersion;
|
|
1106
|
+
if (
|
|
1107
|
+
filePath === this.#configPath &&
|
|
1108
|
+
(typeof configSchemaVersion !== "number" || configSchemaVersion < CONFIG_SCHEMA_VERSION)
|
|
1109
|
+
) {
|
|
1110
|
+
this.#schemaMigrationPending = true;
|
|
1111
|
+
}
|
|
1112
|
+
const migrated = this.#migrateRawSettings(parsedRaw);
|
|
1113
|
+
const reconciled = reconcileSettingsSchema(migrated);
|
|
1114
|
+
if (typeof configSchemaVersion === "number" && configSchemaVersion > CONFIG_SCHEMA_VERSION) {
|
|
1115
|
+
reconciled.report.issues.push({
|
|
1116
|
+
path: "configSchemaVersion",
|
|
1117
|
+
kind: "pending-migration",
|
|
1118
|
+
detail: `Configuration requires schema version ${configSchemaVersion}.`,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
this.#schemaReport = reconciled.report;
|
|
1122
|
+
return reconciled.settings;
|
|
1211
1123
|
} catch (error) {
|
|
1212
|
-
if (isEnoent(error))
|
|
1213
|
-
|
|
1214
|
-
|
|
1124
|
+
if (isEnoent(error)) {
|
|
1125
|
+
this.#captureRawNotificationConfig({});
|
|
1126
|
+
return {};
|
|
1127
|
+
}
|
|
1128
|
+
throw error;
|
|
1215
1129
|
}
|
|
1216
1130
|
}
|
|
1217
1131
|
|
|
@@ -1238,13 +1152,15 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1238
1152
|
async #normalizeAfterLoad(): Promise<void> {
|
|
1239
1153
|
this.#sanitizeModelSelectorRecords();
|
|
1240
1154
|
this.#rebuildMerged();
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1155
|
+
if (!this.#futureSchemaVersion) {
|
|
1156
|
+
this.#legacyFallbackMigrationGlobalFingerprint = YAML.stringify(this.#global, null, 2);
|
|
1157
|
+
this.#migrateRetryFallbackChains();
|
|
1158
|
+
if (
|
|
1159
|
+
!this.#modified.has("modelRoles") &&
|
|
1160
|
+
![...this.#modified.keys()].some(path => path.startsWith("retry.fallback"))
|
|
1161
|
+
) {
|
|
1162
|
+
this.#legacyFallbackMigrationGlobalFingerprint = undefined;
|
|
1163
|
+
}
|
|
1248
1164
|
}
|
|
1249
1165
|
await this.flush();
|
|
1250
1166
|
this.#sanitizeModelSelectorRecords();
|
|
@@ -1367,7 +1283,6 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1367
1283
|
value: structuredClone(value),
|
|
1368
1284
|
generation: ++this.#nextGeneration,
|
|
1369
1285
|
revision,
|
|
1370
|
-
configVersion: this.#defaultModelRoleOwnership.configVersion,
|
|
1371
1286
|
legacyFallbackMigration: true,
|
|
1372
1287
|
});
|
|
1373
1288
|
}
|
|
@@ -1449,20 +1364,23 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1449
1364
|
return isLightTheme(name, this.#agentDir) ? "light" : "dark";
|
|
1450
1365
|
}
|
|
1451
1366
|
|
|
1452
|
-
/** Apply schema migrations
|
|
1367
|
+
/** Apply registered schema migrations once, using configSchemaVersion as the durable marker. */
|
|
1453
1368
|
#migrateRawSettings(raw: RawSettings): RawSettings {
|
|
1369
|
+
const configuredVersion = raw.configSchemaVersion;
|
|
1370
|
+
if (configuredVersion === CONFIG_SCHEMA_VERSION) return raw;
|
|
1371
|
+
if (typeof configuredVersion === "number" && configuredVersion > CONFIG_SCHEMA_VERSION) return raw;
|
|
1372
|
+
|
|
1373
|
+
// Migration registry v0 -> v1.
|
|
1454
1374
|
// queueMode -> steeringMode
|
|
1375
|
+
normalizeSessionDirectoryMigration(raw);
|
|
1455
1376
|
if ("queueMode" in raw && !("steeringMode" in raw)) {
|
|
1456
1377
|
raw.steeringMode = raw.queueMode;
|
|
1457
1378
|
delete raw.queueMode;
|
|
1458
1379
|
}
|
|
1459
|
-
|
|
1460
|
-
// ask.timeout: ms -> seconds (if value > 1000, it's old ms format)
|
|
1380
|
+
// ask.timeout: v0 stored milliseconds; v1 stores seconds.
|
|
1461
1381
|
if (raw.ask && typeof (raw.ask as Record<string, unknown>).timeout === "number") {
|
|
1462
1382
|
const oldValue = (raw.ask as Record<string, unknown>).timeout as number;
|
|
1463
|
-
if (oldValue > 1000)
|
|
1464
|
-
(raw.ask as Record<string, unknown>).timeout = Math.round(oldValue / 1000);
|
|
1465
|
-
}
|
|
1383
|
+
if (oldValue > 1000) (raw.ask as Record<string, unknown>).timeout = Math.round(oldValue / 1000);
|
|
1466
1384
|
}
|
|
1467
1385
|
|
|
1468
1386
|
// Migrate old flat "theme" string to nested theme.dark/theme.light
|
|
@@ -1591,6 +1509,8 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1591
1509
|
}
|
|
1592
1510
|
}
|
|
1593
1511
|
|
|
1512
|
+
raw.configSchemaVersion = CONFIG_SCHEMA_VERSION;
|
|
1513
|
+
|
|
1594
1514
|
return raw;
|
|
1595
1515
|
}
|
|
1596
1516
|
|
|
@@ -1620,7 +1540,6 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1620
1540
|
|
|
1621
1541
|
let captured: SettingsPatch[] = [];
|
|
1622
1542
|
let durableBeforeWrite: RawSettings | undefined;
|
|
1623
|
-
let durableVersionBeforeWrite: string | undefined;
|
|
1624
1543
|
const save = reserveAtomicYamlUpdateSlot(this.#configPath, async () => {
|
|
1625
1544
|
await slot.wait;
|
|
1626
1545
|
slot.captured = true;
|
|
@@ -1635,8 +1554,6 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1635
1554
|
this.#global = structuredClone(current);
|
|
1636
1555
|
this.#rebuildMerged();
|
|
1637
1556
|
if (getByPath(current, ["retry", "fallbackChains"]) !== undefined) {
|
|
1638
|
-
this.#defaultModelRoleOwnership.configVersion = readConfigVersion(this.#configPath!);
|
|
1639
|
-
this.#defaultModelRoleOwnership.defaultLineageKnown = false;
|
|
1640
1557
|
this.#migrateRetryFallbackChains();
|
|
1641
1558
|
captured = this.#pendingPatchesInGenerationOrder();
|
|
1642
1559
|
} else {
|
|
@@ -1648,86 +1565,44 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1648
1565
|
captured = captured.filter(patch => !patch.legacyFallbackMigration);
|
|
1649
1566
|
}
|
|
1650
1567
|
}
|
|
1651
|
-
|
|
1568
|
+
this.#fenceNotificationValidationForExternalDurableDelta(current, captured);
|
|
1652
1569
|
durableBeforeWrite = structuredClone(current);
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
const applicablePatches = captured.filter(
|
|
1656
|
-
patch =>
|
|
1657
|
-
!this.#isStaleDefaultModelRolePatch(patch, currentConfigVersion) || patch.modelRole !== "default",
|
|
1658
|
-
);
|
|
1659
|
-
const appliesDefault = applicablePatches.some(
|
|
1660
|
-
patch =>
|
|
1661
|
-
!this.#isStaleDefaultModelRolePatch(patch, currentConfigVersion) &&
|
|
1662
|
-
(patch.modelRole === "default" || (patch.path === "modelRoles" && !patch.modelRole)),
|
|
1663
|
-
);
|
|
1664
|
-
for (const patch of applicablePatches) {
|
|
1665
|
-
this.#applyPatchWithDefaultOwnership(current, patch, currentConfigVersion);
|
|
1666
|
-
}
|
|
1667
|
-
return { appliesDefault, externalLineageBreak, shouldWrite: applicablePatches.length > 0 };
|
|
1570
|
+
for (const patch of captured) applySettingsPatch(current, patch);
|
|
1571
|
+
return { shouldWrite: captured.length > 0 };
|
|
1668
1572
|
},
|
|
1669
1573
|
shouldWrite: result => result.shouldWrite,
|
|
1670
|
-
committed:
|
|
1671
|
-
const savedConfigVersion = readConfigVersion(this.#configPath!);
|
|
1672
|
-
this.#defaultModelRoleOwnership.configVersion = savedConfigVersion;
|
|
1673
|
-
if (result.appliesDefault) {
|
|
1674
|
-
this.#defaultModelRoleOwnership.defaultConfigVersion = savedConfigVersion;
|
|
1675
|
-
this.#defaultModelRoleOwnership.defaultLineageKnown = true;
|
|
1676
|
-
} else if (result.externalLineageBreak) {
|
|
1677
|
-
this.#defaultModelRoleOwnership.defaultLineageKnown = false;
|
|
1678
|
-
}
|
|
1574
|
+
committed: current => {
|
|
1679
1575
|
for (const patch of captured) {
|
|
1680
1576
|
const key = settingsPatchKey(patch);
|
|
1681
|
-
if (this.#modified.get(key)?.generation === patch.generation)
|
|
1682
|
-
this.#modified.delete(key);
|
|
1683
|
-
}
|
|
1577
|
+
if (this.#modified.get(key)?.generation === patch.generation) this.#modified.delete(key);
|
|
1684
1578
|
}
|
|
1685
1579
|
this.#global = current;
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
this.#modified.delete(key);
|
|
1691
|
-
continue;
|
|
1692
|
-
}
|
|
1693
|
-
this.#applyPatchWithDefaultOwnership(
|
|
1694
|
-
this.#global,
|
|
1695
|
-
{ ...patch, value: structuredClone(patch.value) },
|
|
1696
|
-
savedConfigVersion,
|
|
1697
|
-
);
|
|
1580
|
+
this.#captureRawNotificationConfig(current);
|
|
1581
|
+
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
1582
|
+
applySettingsPatch(this.#global, { ...patch, value: structuredClone(patch.value) });
|
|
1583
|
+
this.#applyNotificationMutationToRaw(patch.path, patch.value);
|
|
1698
1584
|
}
|
|
1699
1585
|
this.#rebuildMerged();
|
|
1586
|
+
this.#recomputeNotificationValidationFromRaw();
|
|
1700
1587
|
},
|
|
1701
1588
|
};
|
|
1702
1589
|
}).then(() => undefined);
|
|
1703
1590
|
this.#savePromise = save;
|
|
1704
1591
|
void save.catch(error => {
|
|
1705
1592
|
logger.warn("Settings: background save failed", { error: String(error) });
|
|
1706
|
-
let droppedStaleDefault = false;
|
|
1707
1593
|
for (const patch of captured) {
|
|
1708
1594
|
const key = settingsPatchKey(patch);
|
|
1709
|
-
|
|
1710
|
-
if (currentPatch?.generation !== patch.generation) continue;
|
|
1711
|
-
if (
|
|
1712
|
-
this.#isStaleDefaultModelRolePatch(patch, readConfigVersion(this.#configPath!)) &&
|
|
1713
|
-
patch.modelRole === "default"
|
|
1714
|
-
) {
|
|
1715
|
-
this.#modified.delete(key);
|
|
1716
|
-
droppedStaleDefault = true;
|
|
1717
|
-
} else {
|
|
1718
|
-
this.#modified.set(key, patch);
|
|
1719
|
-
}
|
|
1595
|
+
if (this.#modified.get(key)?.generation === patch.generation) this.#modified.set(key, patch);
|
|
1720
1596
|
}
|
|
1721
|
-
if (
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
this.#defaultModelRoleOwnership.configVersion = durableVersionBeforeWrite;
|
|
1729
|
-
this.#defaultModelRoleOwnership.defaultLineageKnown = false;
|
|
1597
|
+
if (durableBeforeWrite) {
|
|
1598
|
+
this.#global = durableBeforeWrite;
|
|
1599
|
+
this.#captureRawNotificationConfig(durableBeforeWrite);
|
|
1600
|
+
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
1601
|
+
applySettingsPatch(this.#global, { ...patch, value: structuredClone(patch.value) });
|
|
1602
|
+
this.#applyNotificationMutationToRaw(patch.path, patch.value);
|
|
1603
|
+
}
|
|
1730
1604
|
this.#rebuildMerged();
|
|
1605
|
+
this.#recomputeNotificationValidationFromRaw();
|
|
1731
1606
|
}
|
|
1732
1607
|
});
|
|
1733
1608
|
this.#armSaveTimer(slot);
|
|
@@ -1743,44 +1618,9 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1743
1618
|
}, 100);
|
|
1744
1619
|
}
|
|
1745
1620
|
|
|
1746
|
-
#isStaleDefaultModelRolePatch(
|
|
1747
|
-
patch: SettingsPatch,
|
|
1748
|
-
currentConfigVersion = this.#defaultModelRoleOwnership.configVersion,
|
|
1749
|
-
): boolean {
|
|
1750
|
-
const changesDefault = patch.modelRole === "default" || (patch.path === "modelRoles" && !patch.modelRole);
|
|
1751
|
-
const generationChanged =
|
|
1752
|
-
patch.modelRoleRevision !== undefined &&
|
|
1753
|
-
patch.modelRoleRevision !== this.#defaultModelRoleOwnership.generation;
|
|
1754
|
-
const externalConfigChanged =
|
|
1755
|
-
patch.configVersion !== currentConfigVersion &&
|
|
1756
|
-
(this.#defaultModelRoleOwnership.configVersion !== currentConfigVersion ||
|
|
1757
|
-
!this.#defaultModelRoleOwnership.defaultLineageKnown);
|
|
1758
|
-
return changesDefault && (generationChanged || externalConfigChanged);
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
1621
|
#pendingPatchesInGenerationOrder(): SettingsPatch[] {
|
|
1762
1622
|
return [...this.#modified.values()].sort((left, right) => left.generation - right.generation);
|
|
1763
1623
|
}
|
|
1764
|
-
|
|
1765
|
-
#applyPatchWithDefaultOwnership(
|
|
1766
|
-
raw: RawSettings,
|
|
1767
|
-
patch: SettingsPatch,
|
|
1768
|
-
currentConfigVersion = this.#defaultModelRoleOwnership.configVersion,
|
|
1769
|
-
): boolean {
|
|
1770
|
-
if (!this.#isStaleDefaultModelRolePatch(patch, currentConfigVersion)) {
|
|
1771
|
-
applySettingsPatch(raw, patch);
|
|
1772
|
-
return true;
|
|
1773
|
-
}
|
|
1774
|
-
if (patch.modelRole === "default") return false;
|
|
1775
|
-
|
|
1776
|
-
const durableDefault = defaultModelRoleFrom(raw);
|
|
1777
|
-
const roles = shallowModelSelectorRecord(patch.value);
|
|
1778
|
-
if (durableDefault === undefined) delete roles.default;
|
|
1779
|
-
else roles.default = durableDefault;
|
|
1780
|
-
setByPath(raw, ["modelRoles"], roles);
|
|
1781
|
-
return true;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
1624
|
#releasePendingSaveSlot(): void {
|
|
1785
1625
|
if (this.#saveTimer) {
|
|
1786
1626
|
clearTimeout(this.#saveTimer);
|
|
@@ -1792,8 +1632,8 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1792
1632
|
slot.release();
|
|
1793
1633
|
}
|
|
1794
1634
|
|
|
1795
|
-
#applyDurableBatch(revisions: readonly DurableBatchRevision[]):
|
|
1796
|
-
this.#applyDurablePatches(
|
|
1635
|
+
#applyDurableBatch(revisions: readonly DurableBatchRevision[]): boolean {
|
|
1636
|
+
return this.#applyDurablePatches(
|
|
1797
1637
|
revisions,
|
|
1798
1638
|
revisions.map(entry => entry.patch),
|
|
1799
1639
|
true,
|
|
@@ -1803,15 +1643,22 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1803
1643
|
#applyRestoredDurableBatch(
|
|
1804
1644
|
revisions: readonly DurableBatchRevision[],
|
|
1805
1645
|
restoredPatches: readonly AtomicYamlPatch[],
|
|
1646
|
+
notificationValidationGuard: NotificationValidationRestoreGuard,
|
|
1806
1647
|
): void {
|
|
1807
|
-
this.#
|
|
1648
|
+
const restoreNotificationValidationState = this.#canRestoreNotificationValidationState(
|
|
1649
|
+
notificationValidationGuard,
|
|
1650
|
+
restoredPatches.map(patch => patch.path),
|
|
1651
|
+
);
|
|
1652
|
+
if (this.#applyDurablePatches(revisions, restoredPatches, false) && restoreNotificationValidationState) {
|
|
1653
|
+
this.#restoreNotificationValidationState(notificationValidationGuard.state);
|
|
1654
|
+
}
|
|
1808
1655
|
}
|
|
1809
1656
|
|
|
1810
1657
|
#applyDurablePatches(
|
|
1811
1658
|
revisions: readonly DurableBatchRevision[],
|
|
1812
1659
|
patches: readonly AtomicYamlPatch[],
|
|
1813
1660
|
clearStagedMutations: boolean,
|
|
1814
|
-
):
|
|
1661
|
+
): boolean {
|
|
1815
1662
|
const revisionsByPath = new Map<string, DurableBatchRevision>();
|
|
1816
1663
|
for (const entry of revisions) revisionsByPath.set(entry.patch.path, entry);
|
|
1817
1664
|
const finalPatches = new Map<string, AtomicYamlPatch>();
|
|
@@ -1820,17 +1667,19 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1820
1667
|
const revision = revisionsByPath.get(patch.path);
|
|
1821
1668
|
return revision !== undefined && this.#pathRevisions.get(patch.path) === revision.revision;
|
|
1822
1669
|
});
|
|
1823
|
-
if (applicable.length === 0) return;
|
|
1670
|
+
if (applicable.length === 0) return false;
|
|
1824
1671
|
|
|
1825
|
-
const previous = new Map<
|
|
1672
|
+
const previous = new Map<string, unknown>();
|
|
1826
1673
|
for (const patch of applicable) {
|
|
1827
|
-
const settingPath = patch.path
|
|
1674
|
+
const settingPath = patch.path;
|
|
1828
1675
|
const revision = revisionsByPath.get(patch.path)!;
|
|
1829
|
-
previous.set(settingPath, this.
|
|
1676
|
+
previous.set(settingPath, getByPath(this.#global, settingPath.split(".")));
|
|
1830
1677
|
if (patch.op === "set") {
|
|
1831
1678
|
setByPath(this.#global, settingPath.split("."), structuredClone(patch.value));
|
|
1679
|
+
this.#applyNotificationMutationToRaw(settingPath, patch.value);
|
|
1832
1680
|
} else {
|
|
1833
1681
|
deleteByPath(this.#global, settingPath.split("."));
|
|
1682
|
+
this.#applyNotificationMutationToRaw(settingPath, undefined);
|
|
1834
1683
|
}
|
|
1835
1684
|
if (clearStagedMutations) {
|
|
1836
1685
|
for (const [key, staged] of this.#modified) {
|
|
@@ -1840,24 +1689,153 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1840
1689
|
}
|
|
1841
1690
|
}
|
|
1842
1691
|
}
|
|
1692
|
+
for (const patch of applicable) this.#applyDurableNotificationMutation(patch);
|
|
1693
|
+
const modelRoles = rawSettingsRecord(this.#global.modelRoles);
|
|
1694
|
+
if (
|
|
1695
|
+
applicable.some(patch => patch.path === "modelRoles.default" && patch.op === "unset") &&
|
|
1696
|
+
modelRoles &&
|
|
1697
|
+
Object.keys(modelRoles).length === 0
|
|
1698
|
+
) {
|
|
1699
|
+
delete this.#global.modelRoles;
|
|
1700
|
+
}
|
|
1843
1701
|
this.#rebuildMerged();
|
|
1702
|
+
this.#revalidateNotificationSettingsAfterMutation(applicable.map(patch => patch.path));
|
|
1844
1703
|
for (const patch of applicable) {
|
|
1845
1704
|
const settingPath = patch.path as SettingPath;
|
|
1846
1705
|
const hook = SETTING_HOOKS[settingPath];
|
|
1847
1706
|
if (hook) hook(this.get(settingPath), previous.get(settingPath)!);
|
|
1848
1707
|
}
|
|
1708
|
+
return applicable.some(patch => isNotificationSettingsPath(patch.path));
|
|
1849
1709
|
}
|
|
1850
1710
|
|
|
1851
1711
|
async #refreshDurableSettings(): Promise<void> {
|
|
1852
1712
|
if (!this.#persist || !this.#configPath) return;
|
|
1713
|
+
const previousFingerprint = this.#durableNotificationFingerprint;
|
|
1853
1714
|
const current = await this.#loadYaml(this.#configPath);
|
|
1854
|
-
|
|
1715
|
+
if (previousFingerprint !== this.#durableNotificationFingerprint) this.#notificationValidationGeneration++;
|
|
1716
|
+
this.#replaceGlobalWithDurable(current);
|
|
1855
1717
|
}
|
|
1856
1718
|
|
|
1857
1719
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1858
1720
|
// Utilities
|
|
1859
1721
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1860
1722
|
|
|
1723
|
+
#notificationValidationRestoreGuard(): NotificationValidationRestoreGuard {
|
|
1724
|
+
return {
|
|
1725
|
+
state: this.#notificationValidationState(),
|
|
1726
|
+
restoreGeneration: undefined,
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
#notificationValidationState(): NotificationValidationState {
|
|
1730
|
+
return {
|
|
1731
|
+
malformedConfigRoot: this.#hasMalformedConfigRoot,
|
|
1732
|
+
invalidNotificationConfiguration: this.#hasInvalidNotificationConfiguration,
|
|
1733
|
+
generation: this.#notificationValidationGeneration,
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
#recordNotificationValidationBatchApply(
|
|
1737
|
+
guard: NotificationValidationRestoreGuard,
|
|
1738
|
+
pathsOrAppliedNotificationMutation: Iterable<string> | boolean,
|
|
1739
|
+
): void {
|
|
1740
|
+
const appliedNotificationMutation =
|
|
1741
|
+
typeof pathsOrAppliedNotificationMutation === "boolean"
|
|
1742
|
+
? pathsOrAppliedNotificationMutation
|
|
1743
|
+
: [...pathsOrAppliedNotificationMutation].some(isNotificationSettingsPath);
|
|
1744
|
+
if (appliedNotificationMutation && this.#notificationValidationGeneration === guard.state.generation + 1) {
|
|
1745
|
+
guard.restoreGeneration = this.#notificationValidationGeneration;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
#canRestoreNotificationValidationState(guard: NotificationValidationRestoreGuard, paths: Iterable<string>): boolean {
|
|
1749
|
+
return (
|
|
1750
|
+
[...paths].some(isNotificationSettingsPath) &&
|
|
1751
|
+
guard.restoreGeneration !== undefined &&
|
|
1752
|
+
this.#notificationValidationGeneration === guard.restoreGeneration
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
#restoreNotificationValidationState(state: NotificationValidationState): void {
|
|
1756
|
+
this.#hasMalformedConfigRoot = state.malformedConfigRoot;
|
|
1757
|
+
this.#hasInvalidNotificationConfiguration = state.invalidNotificationConfiguration;
|
|
1758
|
+
}
|
|
1759
|
+
#rejectAtomicNotificationRepairForMalformedRoot(patches: readonly AtomicYamlPatch[], root: unknown): void {
|
|
1760
|
+
if (
|
|
1761
|
+
root !== undefined &&
|
|
1762
|
+
!rawSettingsRecord(root) &&
|
|
1763
|
+
patches.some(patch => isNotificationSettingsPath(patch.path))
|
|
1764
|
+
) {
|
|
1765
|
+
throw new Error("Cannot atomically repair notification settings while config.yml has a malformed root.");
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
#captureRawNotificationConfig(raw: RawSettings | undefined): void {
|
|
1770
|
+
this.#rawNotificationConfig = raw === undefined ? undefined : structuredClone(raw);
|
|
1771
|
+
this.#durableRawNotificationConfig = raw === undefined ? undefined : structuredClone(raw);
|
|
1772
|
+
this.#durableNotificationFingerprint =
|
|
1773
|
+
raw === undefined ? "malformed-root" : YAML.stringify(getByPath(raw, ["notifications"]), null, 2);
|
|
1774
|
+
}
|
|
1775
|
+
#applyNotificationMutationToRaw(path: string, value: unknown | undefined): void {
|
|
1776
|
+
if (!isNotificationSettingsPath(path)) return;
|
|
1777
|
+
if (!this.#rawNotificationConfig) this.#rawNotificationConfig = {};
|
|
1778
|
+
if (value === undefined) deleteByPath(this.#rawNotificationConfig, path.split("."));
|
|
1779
|
+
else setByPath(this.#rawNotificationConfig, path.split("."), structuredClone(value));
|
|
1780
|
+
}
|
|
1781
|
+
#applyDurableNotificationMutation(patch: AtomicYamlPatch): void {
|
|
1782
|
+
if (!isNotificationSettingsPath(patch.path)) return;
|
|
1783
|
+
if (!this.#durableRawNotificationConfig) this.#durableRawNotificationConfig = {};
|
|
1784
|
+
if (patch.op === "unset") deleteByPath(this.#durableRawNotificationConfig, patch.path.split("."));
|
|
1785
|
+
else setByPath(this.#durableRawNotificationConfig, patch.path.split("."), structuredClone(patch.value));
|
|
1786
|
+
this.#durableNotificationFingerprint = YAML.stringify(
|
|
1787
|
+
getByPath(this.#durableRawNotificationConfig, ["notifications"]),
|
|
1788
|
+
null,
|
|
1789
|
+
2,
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
#fenceNotificationValidationForExternalDurableDelta(current: RawSettings, captured: readonly SettingsPatch[]): void {
|
|
1793
|
+
const expected = structuredClone(this.#durableRawNotificationConfig);
|
|
1794
|
+
for (const patch of captured) {
|
|
1795
|
+
if (!isNotificationSettingsPath(patch.path)) continue;
|
|
1796
|
+
if (!expected) break;
|
|
1797
|
+
if (patch.value === undefined) deleteByPath(expected, patch.path.split("."));
|
|
1798
|
+
else setByPath(expected, patch.path.split("."), structuredClone(patch.value));
|
|
1799
|
+
}
|
|
1800
|
+
const expectedFingerprint =
|
|
1801
|
+
expected === undefined ? "malformed-root" : YAML.stringify(getByPath(expected, ["notifications"]), null, 2);
|
|
1802
|
+
const currentFingerprint = YAML.stringify(getByPath(current, ["notifications"]), null, 2);
|
|
1803
|
+
if (expectedFingerprint !== currentFingerprint) this.#notificationValidationGeneration++;
|
|
1804
|
+
}
|
|
1805
|
+
#recomputeNotificationValidationFromRaw(): void {
|
|
1806
|
+
if (this.#rawNotificationConfig === undefined) {
|
|
1807
|
+
this.#hasMalformedConfigRoot = true;
|
|
1808
|
+
this.#hasInvalidNotificationConfiguration = false;
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
try {
|
|
1812
|
+
parseNotificationSettingsSnapshot(this.#rawNotificationConfig);
|
|
1813
|
+
this.#hasMalformedConfigRoot = false;
|
|
1814
|
+
this.#hasInvalidNotificationConfiguration = false;
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
if (error instanceof Error && error.message === "gjc_notify_daemon_invalid_configuration") {
|
|
1817
|
+
this.#hasMalformedConfigRoot = false;
|
|
1818
|
+
this.#hasInvalidNotificationConfiguration = true;
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
throw error;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
#revalidateNotificationSettingsAfterMutation(paths: Iterable<string>): void {
|
|
1825
|
+
if (![...paths].some(isNotificationSettingsPath)) return;
|
|
1826
|
+
this.#notificationValidationGeneration++;
|
|
1827
|
+
try {
|
|
1828
|
+
parseNotificationSettingsSnapshot(this.#rawNotificationConfig);
|
|
1829
|
+
this.#hasMalformedConfigRoot = false;
|
|
1830
|
+
this.#hasInvalidNotificationConfiguration = false;
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
if (error instanceof Error && error.message === "gjc_notify_daemon_invalid_configuration") {
|
|
1833
|
+
this.#hasInvalidNotificationConfiguration = true;
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
throw error;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1861
1839
|
#rebuildMerged(): void {
|
|
1862
1840
|
this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), this.#project);
|
|
1863
1841
|
this.#merged = this.#deepMerge(this.#merged, this.#overrides);
|
|
@@ -1873,11 +1851,6 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1873
1851
|
}
|
|
1874
1852
|
}
|
|
1875
1853
|
|
|
1876
|
-
#getGlobalResolved<P extends SettingPath>(path: P): SettingValue<P> {
|
|
1877
|
-
const value = getByPath(this.#global, path.split("."));
|
|
1878
|
-
return value === undefined ? getDefault(path) : (value as SettingValue<P>);
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
1854
|
#stripProjectNotificationSettings(settings: RawSettings): {
|
|
1882
1855
|
settings: RawSettings;
|
|
1883
1856
|
rejectedNotifications: boolean;
|
|
@@ -1994,6 +1967,7 @@ export function onAppendOnlyModeChanged(cb: (value: string) => void): () => void
|
|
|
1994
1967
|
|
|
1995
1968
|
let globalInstance: Settings | null = null;
|
|
1996
1969
|
let globalInstancePromise: Promise<Settings> | null = null;
|
|
1970
|
+
let globalInitOptions: SettingsOptions | null = null;
|
|
1997
1971
|
|
|
1998
1972
|
export function isSettingsInitialized(): boolean {
|
|
1999
1973
|
return globalInstance !== null;
|
|
@@ -2004,8 +1978,10 @@ export function isSettingsInitialized(): boolean {
|
|
|
2004
1978
|
* @internal
|
|
2005
1979
|
*/
|
|
2006
1980
|
export function resetSettingsForTest(): void {
|
|
1981
|
+
globalInstance?.getStorage()?.close();
|
|
2007
1982
|
globalInstance = null;
|
|
2008
1983
|
globalInstancePromise = null;
|
|
1984
|
+
globalInitOptions = null;
|
|
2009
1985
|
}
|
|
2010
1986
|
|
|
2011
1987
|
/**
|