@gajae-code/coding-agent 0.10.2 → 0.11.1
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 +58 -2
- package/dist/types/append-only-mode.d.ts +1 -1
- package/dist/types/async/job-manager.d.ts +46 -7
- package/dist/types/cli/args.d.ts +4 -5
- package/dist/types/cli/daemon-cli.d.ts +12 -1
- package/dist/types/cli/notify-cli.d.ts +21 -1
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +39 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commands/notify.d.ts +28 -1
- package/dist/types/commands/sdk.d.ts +33 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +85 -0
- package/dist/types/config/model-profile-activation.d.ts +35 -15
- package/dist/types/config/model-profiles.d.ts +11 -5
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +39 -6
- package/dist/types/config/model-selector-value.d.ts +17 -0
- package/dist/types/config/models-config-schema.d.ts +10 -6
- package/dist/types/config/settings-schema.d.ts +133 -6
- package/dist/types/config/settings.d.ts +63 -35
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -6
- package/dist/types/coordinator-mcp/server.d.ts +6 -49
- package/dist/types/daemon/builtin.d.ts +6 -5
- package/dist/types/daemon/control-types.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +12 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/compact-handler.d.ts +4 -2
- package/dist/types/extensibility/extensions/get-commands-handler.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +10 -1
- package/dist/types/extensibility/extensions/runner.d.ts +3 -3
- package/dist/types/extensibility/extensions/types.d.ts +141 -16
- package/dist/types/extensibility/hooks/types.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -2
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +3 -4
- package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +13 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -1
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
- package/dist/types/gjc-runtime/windows-powershell-command.d.ts +9 -0
- package/dist/types/harness-control-plane/operate.d.ts +5 -5
- package/dist/types/harness-control-plane/owner.d.ts +18 -4
- package/dist/types/harness-control-plane/receipts.d.ts +0 -1
- package/dist/types/harness-control-plane/sdk-transport.d.ts +43 -0
- package/dist/types/harness-control-plane/session-transport.d.ts +48 -0
- package/dist/types/harness-control-plane/types.d.ts +3 -4
- package/dist/types/lsp/index.d.ts +1 -1
- package/dist/types/main.d.ts +32 -38
- package/dist/types/modes/acp/acp-agent.d.ts +224 -53
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -15
- package/dist/types/modes/acp/permission-mode.d.ts +4 -0
- package/dist/types/modes/acp/startup-options.d.ts +6 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +3 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +135 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -4
- package/dist/types/modes/components/welcome.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +35 -1
- package/dist/types/modes/index.d.ts +0 -4
- package/dist/types/modes/interactive-mode.d.ts +5 -2
- package/dist/types/modes/runtime-init.d.ts +2 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +6 -9
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -6
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +6 -6
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +4 -9
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +170 -30
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +7 -7
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +105 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/runtime-mcp/client.d.ts +2 -0
- package/dist/types/runtime-mcp/config.d.ts +5 -1
- package/dist/types/runtime-mcp/manager.d.ts +8 -1
- package/dist/types/runtime-mcp/types.d.ts +4 -0
- package/dist/types/sdk/acp/adapter.d.ts +67 -0
- package/dist/types/sdk/acp/index.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +53 -0
- package/dist/types/sdk/broker/discovery.d.ts +30 -0
- package/dist/types/sdk/broker/ensure.d.ts +42 -0
- package/dist/types/sdk/broker/identity.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +8 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +95 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +85 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +14 -0
- package/dist/types/sdk/broker/runtime.d.ts +33 -0
- package/dist/types/sdk/broker/session-index.d.ts +57 -0
- package/dist/types/sdk/broker/state-version.d.ts +8 -0
- package/dist/types/sdk/broker/transport.d.ts +9 -0
- package/dist/types/sdk/bus/chat-command-policy.d.ts +49 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +21 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +111 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +77 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +84 -0
- package/dist/types/{notifications → sdk/bus}/config-commands.d.ts +7 -2
- package/dist/types/sdk/bus/config.d.ts +274 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +72 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +58 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +50 -0
- package/dist/types/sdk/bus/discord-live-provider.d.ts +70 -0
- package/dist/types/sdk/bus/discord-provider.d.ts +85 -0
- package/dist/types/{notifications → sdk/bus}/html-format.d.ts +2 -2
- package/dist/types/{notifications → sdk/bus}/index.d.ts +113 -13
- package/dist/types/{notifications → sdk/bus}/lifecycle-control-runtime.d.ts +4 -2
- package/dist/types/{notifications → sdk/bus}/lifecycle-orchestrator.d.ts +18 -21
- package/dist/types/{notifications → sdk/bus}/managed-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/native-runtime-compatibility.d.ts +13 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +170 -0
- package/dist/types/{notifications → sdk/bus}/notification-service.d.ts +7 -1
- package/dist/types/sdk/bus/session-control.d.ts +83 -0
- package/dist/types/sdk/bus/slack-conversation.d.ts +56 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +60 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +59 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +55 -0
- package/dist/types/{notifications → sdk/bus}/telegram-cli.d.ts +4 -4
- package/dist/types/{notifications → sdk/bus}/telegram-daemon-cli.d.ts +14 -5
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -0
- package/dist/types/{notifications → sdk/bus}/telegram-daemon-control.d.ts +2 -2
- package/dist/types/{notifications → sdk/bus}/telegram-daemon.d.ts +78 -17
- package/dist/types/{notifications → sdk/bus}/telegram-reference.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-setup.d.ts +128 -0
- package/dist/types/{notifications → sdk/bus}/topic-registry.d.ts +11 -0
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/session-cli.d.ts +19 -0
- package/dist/types/sdk/client/client.d.ts +1 -0
- package/dist/types/sdk/client/discovery.d.ts +27 -0
- package/dist/types/sdk/client/index.d.ts +2 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +34 -0
- package/dist/types/sdk/host/control/index.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +65 -0
- package/dist/types/sdk/host/events.d.ts +35 -0
- package/dist/types/sdk/host/host.d.ts +35 -0
- package/dist/types/sdk/host/index.d.ts +4 -0
- package/dist/types/sdk/host/query/cursor.d.ts +62 -0
- package/dist/types/sdk/host/query/handlers.d.ts +70 -0
- package/dist/types/sdk/host/query/index.d.ts +3 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +60 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +45 -0
- package/dist/types/sdk/host/types.d.ts +32 -0
- package/dist/types/sdk/index.d.ts +6 -0
- package/dist/types/sdk/lifecycle-session.d.ts +14 -0
- package/dist/types/sdk/mcp/index.d.ts +1 -0
- package/dist/types/sdk/mcp/server.d.ts +37 -0
- package/dist/types/sdk/models.d.ts +44 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +14 -0
- package/dist/types/sdk/protocol/operation-registry.d.ts +24 -0
- package/dist/types/{sdk.d.ts → sdk/session.d.ts} +44 -38
- package/dist/types/sdk/startup-capability.d.ts +50 -0
- package/dist/types/session/agent-session.d.ts +114 -21
- package/dist/types/session/default-model-selection.d.ts +25 -0
- package/dist/types/session/fallback-chain-controller.d.ts +52 -0
- package/dist/types/session/session-manager.d.ts +58 -1
- package/dist/types/setup/credential-auto-import.d.ts +36 -6
- package/dist/types/tools/ask-answer-registry.d.ts +1 -1
- package/dist/types/tools/browser/benchmark/detector-report.d.ts +63 -0
- package/dist/types/tools/browser/benchmark/run-suite.d.ts +18 -0
- package/dist/types/tools/browser/launch.d.ts +22 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +31 -0
- package/dist/types/tools/browser/profile-posture.d.ts +37 -0
- package/dist/types/tools/browser/profile-reuse.d.ts +39 -0
- package/dist/types/tools/browser/profile-warmup.d.ts +39 -0
- package/dist/types/tools/browser/registry.d.ts +19 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/index.d.ts +8 -8
- package/dist/types/tools/subagent-render.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +6 -1
- package/package.json +34 -13
- package/scripts/build-sdk-package-smoke.ts +99 -0
- package/scripts/capture-notifications-settings-showcase.ts +435 -0
- package/scripts/compile-args.ts +4 -2
- package/scripts/g005-daemon-smoke.ts +4 -5
- package/scripts/g011-daemon-path-smoke.ts +5 -5
- package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
- package/scripts/generate-sdk-operation-inventory.ts +867 -0
- package/scripts/generate-telegram-baseline-manifest.ts +128 -0
- package/scripts/manifest-schema.ts +124 -0
- package/scripts/run-test-manifest.ts +153 -0
- package/scripts/run-threat-model-check.ts +157 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +3467 -0
- package/scripts/verify-gjc-sdk-rename.ts +90 -0
- package/src/append-only-mode.ts +1 -1
- package/src/async/job-manager.ts +406 -63
- package/src/cli/args.ts +43 -12
- package/src/cli/auth-gateway-cli.ts +2 -2
- package/src/cli/config-cli.ts +2 -1
- package/src/cli/daemon-cli.ts +63 -14
- package/src/cli/list-models.ts +1 -1
- package/src/cli/notify-cli.ts +288 -286
- package/src/cli.ts +21 -2
- package/src/commands/daemon.ts +50 -6
- package/src/commands/harness.ts +93 -36
- package/src/commands/launch.ts +5 -2
- package/src/commands/mcp-serve.ts +19 -12
- package/src/commands/notify.ts +36 -3
- package/src/commands/sdk.ts +426 -0
- package/src/commit/agentic/agent.ts +1 -0
- package/src/config/atomic-yaml-patch.ts +423 -0
- package/src/config/model-profile-activation.ts +240 -89
- package/src/config/model-profiles.ts +18 -15
- package/src/config/model-registry.ts +51 -26
- package/src/config/model-resolver.ts +138 -51
- package/src/config/model-selector-value.ts +50 -0
- package/src/config/models-config-schema.ts +14 -21
- package/src/config/settings-schema.ts +128 -10
- package/src/config/settings.ts +1120 -158
- package/src/coordinator-mcp/model-preset.ts +74 -20
- package/src/coordinator-mcp/server.ts +1597 -3052
- package/src/daemon/builtin.ts +8 -5
- package/src/daemon/control-types.ts +1 -1
- package/src/defaults/gjc/skills/team/SKILL.md +4 -4
- package/src/discovery/mcp-json.ts +327 -20
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/compact-handler.ts +3 -3
- package/src/extensibility/extensions/get-commands-handler.ts +2 -2
- package/src/extensibility/extensions/loader.ts +69 -5
- package/src/extensibility/extensions/runner.ts +112 -4
- package/src/extensibility/extensions/types.ts +176 -17
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +170 -86
- package/src/gjc-runtime/psmux-detect.ts +82 -4
- package/src/gjc-runtime/session-state-sidecar.ts +41 -31
- package/src/gjc-runtime/team-runtime.ts +54 -35
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +53 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +1 -1
- package/src/gjc-runtime/tmux-sessions.ts +19 -13
- package/src/gjc-runtime/ultragoal-guard.ts +108 -14
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +227 -11
- package/src/gjc-runtime/windows-powershell-command.ts +58 -0
- package/src/harness-control-plane/operate.ts +9 -9
- package/src/harness-control-plane/owner.ts +283 -39
- package/src/harness-control-plane/receipts.ts +0 -1
- package/src/harness-control-plane/sdk-transport.ts +395 -0
- package/src/harness-control-plane/session-transport.ts +94 -0
- package/src/harness-control-plane/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +34 -35
- package/src/lsp/client.ts +42 -6
- package/src/lsp/index.ts +2 -2
- package/src/main.ts +299 -204
- package/src/modes/DESIGN.md +258 -0
- package/src/modes/acp/acp-agent.ts +1228 -2612
- package/src/modes/acp/acp-client-bridge.ts +2 -20
- package/src/modes/acp/acp-event-mapper.ts +18 -2
- package/src/modes/acp/acp-mode.ts +8 -36
- package/src/modes/acp/permission-mode.ts +25 -0
- package/src/modes/acp/startup-options.ts +6 -0
- package/src/modes/components/agent-dashboard.ts +37 -17
- package/src/modes/components/model-selector.ts +93 -35
- package/src/modes/components/notifications-settings-editor.ts +1379 -0
- package/src/modes/components/settings-selector.ts +60 -11
- package/src/modes/components/welcome.ts +4 -4
- package/src/modes/controllers/command-controller.ts +10 -17
- package/src/modes/controllers/event-controller.ts +11 -16
- package/src/modes/controllers/extension-ui-controller.ts +396 -28
- package/src/modes/controllers/selector-controller.ts +674 -36
- package/src/modes/index.ts +0 -22
- package/src/modes/interactive-mode.ts +87 -36
- package/src/modes/runtime-init.ts +298 -6
- package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +10 -11
- package/src/modes/shared/agent-wire/event-contract.ts +7 -8
- package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
- package/src/modes/shared/agent-wire/event-observation.ts +16 -7
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1258 -87
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +15 -15
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +113 -0
- package/src/modes/types.ts +7 -2
- package/src/runtime-mcp/client.ts +94 -19
- package/src/runtime-mcp/config.ts +27 -15
- package/src/runtime-mcp/manager.ts +343 -105
- package/src/runtime-mcp/oauth-flow.ts +42 -12
- package/src/runtime-mcp/transports/http.ts +49 -19
- package/src/runtime-mcp/transports/stdio.ts +47 -28
- package/src/runtime-mcp/types.ts +10 -0
- package/src/sdk/acp/adapter.ts +418 -0
- package/src/sdk/acp/index.ts +2 -0
- package/src/sdk/broker/broker.ts +642 -0
- package/src/sdk/broker/discovery.ts +113 -0
- package/src/sdk/broker/ensure.ts +307 -0
- package/src/sdk/broker/identity.ts +42 -0
- package/src/sdk/broker/index.ts +8 -0
- package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
- package/src/sdk/broker/internal-source.bunfig.toml +1 -0
- package/src/sdk/broker/lifecycle-ledger.ts +286 -0
- package/src/sdk/broker/lifecycle.ts +2157 -0
- package/src/sdk/broker/process-incarnation.ts +159 -0
- package/src/sdk/broker/runtime.ts +164 -0
- package/src/sdk/broker/session-index.ts +318 -0
- package/src/sdk/broker/state-version.ts +23 -0
- package/src/sdk/broker/transport.ts +125 -0
- package/src/sdk/bus/chat-command-policy.ts +107 -0
- package/src/sdk/bus/chat-daemon-cli.ts +214 -0
- package/src/sdk/bus/chat-daemon-control.ts +713 -0
- package/src/sdk/bus/chat-daemon-runtime.ts +526 -0
- package/src/sdk/bus/chat-effect-journal.ts +355 -0
- package/src/{notifications → sdk/bus}/config-commands.ts +39 -10
- package/src/sdk/bus/config.ts +433 -0
- package/src/sdk/bus/conversation-store.ts +391 -0
- package/src/sdk/bus/discord-conversation.ts +85 -0
- package/src/sdk/bus/discord-daemon.ts +2292 -0
- package/src/sdk/bus/discord-live-provider.ts +521 -0
- package/src/sdk/bus/discord-provider.ts +56 -0
- package/src/{notifications → sdk/bus}/html-format.ts +2 -2
- package/src/sdk/bus/index.ts +3979 -0
- package/src/{notifications → sdk/bus}/lifecycle-commands.ts +2 -0
- package/src/{notifications → sdk/bus}/lifecycle-control-runtime.ts +225 -196
- package/src/{notifications → sdk/bus}/lifecycle-orchestrator.ts +161 -74
- package/src/{notifications → sdk/bus}/managed-daemon.ts +2 -2
- package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
- package/src/sdk/bus/natives-augment.d.ts +38 -0
- package/src/sdk/bus/notification-orchestration.ts +535 -0
- package/src/{notifications → sdk/bus}/notification-service.ts +106 -12
- package/src/sdk/bus/session-control.ts +333 -0
- package/src/sdk/bus/slack-conversation.ts +74 -0
- package/src/sdk/bus/slack-daemon.ts +2144 -0
- package/src/sdk/bus/slack-live-provider.ts +380 -0
- package/src/sdk/bus/slack-provider.ts +76 -0
- package/src/{notifications → sdk/bus}/telegram-cli.ts +8 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +330 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +14 -0
- package/src/{notifications → sdk/bus}/telegram-daemon-control.ts +4 -4
- package/src/{notifications → sdk/bus}/telegram-daemon.ts +603 -63
- package/src/{notifications → sdk/bus}/telegram-reference.ts +2 -2
- package/src/sdk/bus/telegram-setup.ts +663 -0
- package/src/{notifications → sdk/bus}/topic-registry.ts +32 -0
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/session-cli.ts +275 -0
- package/src/sdk/client/client.ts +1 -0
- package/src/sdk/client/discovery.ts +108 -0
- package/src/sdk/client/index.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +367 -0
- package/src/sdk/host/control/index.ts +10 -0
- package/src/sdk/host/control/operations.ts +63 -0
- package/src/sdk/host/events.ts +91 -0
- package/src/sdk/host/host.ts +296 -0
- package/src/sdk/host/index.ts +4 -0
- package/src/sdk/host/query/cursor.ts +189 -0
- package/src/sdk/host/query/handlers.ts +533 -0
- package/src/sdk/host/query/index.ts +3 -0
- package/src/sdk/host/query/revision-store.ts +1126 -0
- package/src/sdk/host/reverse-leases.ts +325 -0
- package/src/sdk/host/types.ts +29 -0
- package/src/sdk/index.ts +13 -0
- package/src/sdk/lifecycle-session.ts +39 -0
- package/src/sdk/mcp/index.ts +1 -0
- package/src/sdk/mcp/server.ts +332 -0
- package/src/sdk/models.ts +168 -0
- package/src/sdk/protocol/adapter-validation.ts +48 -0
- package/src/sdk/protocol/operation-inventory.generated.json +5654 -0
- package/src/sdk/protocol/operation-registry.ts +312 -0
- package/src/{sdk.ts → sdk/session.ts} +339 -212
- package/src/sdk/startup-capability.ts +189 -0
- package/src/session/agent-session.ts +2650 -944
- package/src/session/default-model-selection.ts +93 -0
- package/src/session/fallback-chain-controller.ts +136 -0
- package/src/session/session-manager.ts +438 -11
- package/src/session/session-storage.ts +8 -1
- package/src/setup/credential-auto-import.ts +290 -35
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -5
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/task/executor.ts +62 -13
- package/src/task/index.ts +26 -16
- package/src/tools/ask-answer-registry.ts +1 -1
- package/src/tools/ask.ts +13 -8
- package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
- package/src/tools/browser/benchmark/detector-report.ts +157 -0
- package/src/tools/browser/benchmark/run-suite.ts +39 -0
- package/src/tools/browser/launch.ts +124 -19
- package/src/tools/browser/profile-discovery.ts +63 -0
- package/src/tools/browser/profile-posture.ts +59 -0
- package/src/tools/browser/profile-reuse.ts +102 -0
- package/src/tools/browser/profile-warmup.ts +109 -0
- package/src/tools/browser/registry.ts +87 -11
- package/src/tools/browser/tab-protocol.ts +1 -0
- package/src/tools/browser/tab-supervisor.ts +1 -0
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/browser.ts +6 -0
- package/src/tools/index.ts +8 -8
- package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
- package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
- package/src/tools/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +84 -32
- package/src/tools/telegram-send.ts +2 -2
- package/vendor/insane-search/MANIFEST.json +2 -1
- package/vendor/insane-search/engine/bias_check.py +4 -1
- package/vendor/insane-search/engine/phase0.py +26 -1
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
- package/dist/types/modes/bridge/auth.d.ts +0 -12
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
- package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
- package/dist/types/modes/bridge/event-stream.d.ts +0 -8
- package/dist/types/modes/rpc/host-tools.d.ts +0 -1
- package/dist/types/modes/rpc/host-uris.d.ts +0 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
- package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
- package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
- package/dist/types/notifications/config.d.ts +0 -132
- package/src/harness-control-plane/rpc-adapter.ts +0 -290
- package/src/modes/bridge/auth.ts +0 -41
- package/src/modes/bridge/bridge-client-bridge.ts +0 -47
- package/src/modes/bridge/bridge-mode.ts +0 -675
- package/src/modes/bridge/bridge-ui-context.ts +0 -200
- package/src/modes/bridge/event-stream.ts +0 -70
- package/src/modes/rpc/host-tools.ts +0 -1
- package/src/modes/rpc/host-uris.ts +0 -1
- package/src/modes/rpc/rpc-client.ts +0 -1166
- package/src/modes/rpc/rpc-mode.ts +0 -1029
- package/src/modes/rpc/rpc-socket-security.ts +0 -115
- package/src/modes/rpc/rpc-types.ts +0 -637
- package/src/modes/shared/agent-wire/command-contract.ts +0 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +0 -498
- package/src/modes/shared/agent-wire/command-validation.ts +0 -181
- package/src/modes/shared/agent-wire/handshake.ts +0 -157
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +0 -197
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +0 -236
- package/src/modes/shared/agent-wire/protocol.ts +0 -46
- package/src/modes/shared/agent-wire/responses.ts +0 -17
- package/src/modes/shared/agent-wire/scopes.ts +0 -94
- package/src/modes/shared/agent-wire/session-registry.ts +0 -109
- package/src/modes/shared/agent-wire/ui-request-broker.ts +0 -150
- package/src/modes/shared/agent-wire/ui-result.ts +0 -48
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +0 -365
- package/src/modes/shared/agent-wire/unattended-audit.ts +0 -176
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +0 -426
- package/src/modes/shared/agent-wire/unattended-session.ts +0 -528
- package/src/notifications/config.ts +0 -219
- package/src/notifications/index.ts +0 -1835
- package/src/notifications/telegram-daemon-cli.ts +0 -243
- package/src/tools/puppeteer/09_stealth_locale.txt +0 -46
- /package/dist/types/{notifications → sdk/bus}/attachment-registry.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/chat-adapters.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/daemon-paths.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/engine.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/helpers.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/lifecycle-commands.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/operator-runtime.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rate-limit-pool.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/recent-activity.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/reply-sent-store.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rich-draft.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rich-render.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/threaded-inbound.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/threaded-render.d.ts +0 -0
- /package/src/{notifications → sdk/bus}/attachment-registry.ts +0 -0
- /package/src/{notifications → sdk/bus}/chat-adapters.ts +0 -0
- /package/src/{notifications → sdk/bus}/daemon-paths.ts +0 -0
- /package/src/{notifications → sdk/bus}/engine.ts +0 -0
- /package/src/{notifications → sdk/bus}/helpers.ts +0 -0
- /package/src/{notifications → sdk/bus}/operator-runtime.ts +0 -0
- /package/src/{notifications → sdk/bus}/rate-limit-pool.ts +0 -0
- /package/src/{notifications → sdk/bus}/recent-activity.ts +0 -0
- /package/src/{notifications → sdk/bus}/reply-sent-store.ts +0 -0
- /package/src/{notifications → sdk/bus}/rich-draft.ts +0 -0
- /package/src/{notifications → sdk/bus}/rich-render.ts +0 -0
- /package/src/{notifications → sdk/bus}/threaded-inbound.ts +0 -0
- /package/src/{notifications → sdk/bus}/threaded-render.ts +0 -0
package/src/config/settings.ts
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
* const enabled = settings.get("compaction.enabled"); // sync read
|
|
8
8
|
* settings.set("theme.dark", "red-claw"); // sync write, saves in background
|
|
9
9
|
*
|
|
10
|
-
* For tests:
|
|
10
|
+
* For tests, `Settings.isolated()` seeds explicit user/global settings:
|
|
11
11
|
* const isolated = Settings.isolated({ "compaction.enabled": false });
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
13
|
import * as fs from "node:fs";
|
|
15
14
|
import * as os from "node:os";
|
|
16
15
|
import * as path from "node:path";
|
|
@@ -29,9 +28,20 @@ import { type Settings as SettingsCapabilityItem, settingsCapability } from "../
|
|
|
29
28
|
import type { ModelRole } from "../config/model-registry";
|
|
30
29
|
import { loadCapability } from "../discovery";
|
|
31
30
|
import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset } from "../modes/theme/theme";
|
|
31
|
+
import type { NotificationSettingsReader, NotificationSettingsSnapshot } from "../sdk/bus/config";
|
|
32
32
|
import { AgentStorage } from "../session/agent-storage";
|
|
33
33
|
import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
type AtomicYamlPatch,
|
|
36
|
+
applyAtomicYamlPatches,
|
|
37
|
+
applyAtomicYamlPatchesWithCurrent,
|
|
38
|
+
type CasReceipt,
|
|
39
|
+
deleteByPath,
|
|
40
|
+
reserveAtomicYamlUpdateSlot,
|
|
41
|
+
setByPath,
|
|
42
|
+
} from "./atomic-yaml-patch";
|
|
43
|
+
import { isModelSelectorValue, type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value";
|
|
44
|
+
|
|
35
45
|
import {
|
|
36
46
|
type BashInterceptorRule,
|
|
37
47
|
type GroupPrefix,
|
|
@@ -57,10 +67,38 @@ export interface RawSettings {
|
|
|
57
67
|
|
|
58
68
|
type SettingsPatch = {
|
|
59
69
|
readonly path: string;
|
|
60
|
-
readonly value: unknown;
|
|
70
|
+
readonly value: unknown | undefined;
|
|
61
71
|
readonly generation: number;
|
|
72
|
+
readonly revision: number;
|
|
73
|
+
readonly modelRole?: string;
|
|
74
|
+
readonly modelRoleRevision?: number;
|
|
75
|
+
readonly configVersion?: string;
|
|
76
|
+
readonly legacyFallbackMigration?: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type PendingSaveSlot = {
|
|
80
|
+
captured: boolean;
|
|
81
|
+
released: boolean;
|
|
82
|
+
release: () => void;
|
|
83
|
+
wait: Promise<void>;
|
|
62
84
|
};
|
|
63
85
|
|
|
86
|
+
type DurableBatchRevision = {
|
|
87
|
+
patch: AtomicYamlPatch;
|
|
88
|
+
previousRevision: number | undefined;
|
|
89
|
+
revision: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type SettingsAtomicPatch = { path: SettingPath; op: "set"; value: unknown } | { path: SettingPath; op: "unset" };
|
|
93
|
+
export type SettingsAtomicReceipt = CasReceipt;
|
|
94
|
+
|
|
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
|
+
}
|
|
64
102
|
export interface SettingsOptions {
|
|
65
103
|
/** Current working directory for project settings discovery */
|
|
66
104
|
cwd?: string;
|
|
@@ -72,6 +110,31 @@ export interface SettingsOptions {
|
|
|
72
110
|
overrides?: Partial<Record<SettingPath, unknown>>;
|
|
73
111
|
}
|
|
74
112
|
|
|
113
|
+
/** Additional layer setup for {@link Settings.isolated}. */
|
|
114
|
+
export interface IsolatedSettingsOptions {
|
|
115
|
+
/** Initial runtime overrides. Notification paths are rejected. */
|
|
116
|
+
overrides?: Partial<Record<SettingPath, unknown>>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Raised when an ephemeral override attempts to change global-only notification settings. */
|
|
120
|
+
export class NotificationSettingsOverrideError extends Error {
|
|
121
|
+
constructor(readonly path: SettingPath) {
|
|
122
|
+
super(`Runtime overrides are not allowed for global notification setting ${path}.`);
|
|
123
|
+
this.name = "NotificationSettingsOverrideError";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const LOCAL_NOTIFICATION_SETTING_KEYS = new Set(["terminalBell", "bellOnComplete", "bellOnApproval", "bellOnAsk"]);
|
|
128
|
+
const LOCAL_NOTIFICATION_SETTING_PATHS = new Set(
|
|
129
|
+
[...LOCAL_NOTIFICATION_SETTING_KEYS].map(key => `notifications.${key}`),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
function isNotificationSettingsPath(path: string): boolean {
|
|
133
|
+
return (
|
|
134
|
+
(path === "notifications" || path.startsWith("notifications.")) && !LOCAL_NOTIFICATION_SETTING_PATHS.has(path)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
75
138
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
76
139
|
// Path Utilities
|
|
77
140
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -90,22 +153,6 @@ function getByPath(obj: RawSettings, segments: string[]): unknown {
|
|
|
90
153
|
return current;
|
|
91
154
|
}
|
|
92
155
|
|
|
93
|
-
/**
|
|
94
|
-
* Set a nested value in an object by path segments.
|
|
95
|
-
* Creates intermediate objects as needed.
|
|
96
|
-
*/
|
|
97
|
-
function setByPath(obj: RawSettings, segments: string[], value: unknown): void {
|
|
98
|
-
let current = obj;
|
|
99
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
100
|
-
const segment = segments[i];
|
|
101
|
-
if (!(segment in current) || typeof current[segment] !== "object" || current[segment] === null) {
|
|
102
|
-
current[segment] = {};
|
|
103
|
-
}
|
|
104
|
-
current = current[segment] as RawSettings;
|
|
105
|
-
}
|
|
106
|
-
current[segments[segments.length - 1]] = value;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
156
|
const PATH_SCOPED_ARRAY_SETTINGS = new Set<SettingPath>(["enabledModels", "disabledProviders"]);
|
|
110
157
|
const LEGACY_THEME_NAME_REPLACEMENTS = {
|
|
111
158
|
dark: "red-claw",
|
|
@@ -144,18 +191,37 @@ function stringArrayFromUnknown(value: unknown): string[] {
|
|
|
144
191
|
return [];
|
|
145
192
|
}
|
|
146
193
|
|
|
147
|
-
function
|
|
148
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
|
194
|
+
function rawSettingsRecord(value: unknown): RawSettings | undefined {
|
|
195
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
196
|
+
return value as RawSettings;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function shallowModelSelectorRecord(value: unknown): Record<string, ModelSelectorValue> {
|
|
200
|
+
const record = rawSettingsRecord(value);
|
|
201
|
+
if (!record) return {};
|
|
149
202
|
|
|
150
|
-
const result: Record<string,
|
|
151
|
-
for (const [key, item] of Object.entries(
|
|
152
|
-
if (
|
|
153
|
-
result[key] = item;
|
|
154
|
-
}
|
|
203
|
+
const result: Record<string, ModelSelectorValue> = {};
|
|
204
|
+
for (const [key, item] of Object.entries(record)) {
|
|
205
|
+
if (isModelSelectorValue(item)) result[key] = Array.isArray(item) ? [...item] : item;
|
|
155
206
|
}
|
|
156
207
|
return result;
|
|
157
208
|
}
|
|
158
209
|
|
|
210
|
+
function legacyFallbackChains(value: unknown): Record<string, unknown> {
|
|
211
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function hasOwnModelRole(source: RawSettings, role: string): boolean {
|
|
215
|
+
const roles = getByPath(source, ["modelRoles"]);
|
|
216
|
+
return !!roles && typeof roles === "object" && !Array.isArray(roles) && Object.hasOwn(roles, role);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function selectorChain(value: unknown): string[] {
|
|
220
|
+
if (typeof value === "string") return normalizeModelSelectorValue(value);
|
|
221
|
+
if (!Array.isArray(value) || !value.every(item => typeof item === "string")) return [];
|
|
222
|
+
return normalizeModelSelectorValue(value);
|
|
223
|
+
}
|
|
224
|
+
|
|
159
225
|
function resolvePathScopedStringArray(settingPath: SettingPath, value: unknown, cwd: string): string[] | undefined {
|
|
160
226
|
if (!PATH_SCOPED_ARRAY_SETTINGS.has(settingPath) || !Array.isArray(value)) return undefined;
|
|
161
227
|
|
|
@@ -193,12 +259,79 @@ function resolvePathScopedStringArray(settingPath: SettingPath, value: unknown,
|
|
|
193
259
|
|
|
194
260
|
return resolved;
|
|
195
261
|
}
|
|
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
|
+
|
|
284
|
+
function setRawModelRole(
|
|
285
|
+
raw: RawSettings,
|
|
286
|
+
role: string,
|
|
287
|
+
modelId: ModelSelectorValue | undefined,
|
|
288
|
+
removeContainerWhenEmpty = false,
|
|
289
|
+
): void {
|
|
290
|
+
const roles = { ...rawSettingsRecord(raw.modelRoles) };
|
|
291
|
+
if (modelId === undefined) {
|
|
292
|
+
delete roles[role];
|
|
293
|
+
if (removeContainerWhenEmpty && Object.keys(roles).length === 0) {
|
|
294
|
+
delete raw.modelRoles;
|
|
295
|
+
} else {
|
|
296
|
+
raw.modelRoles = roles;
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
raw.modelRoles = { ...roles, [role]: modelId };
|
|
301
|
+
}
|
|
302
|
+
|
|
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
|
+
function settingsPatchKey(patch: SettingsPatch): string {
|
|
315
|
+
return patch.modelRole ? `modelRoles.${patch.modelRole}` : patch.path;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function applySettingsPatch(raw: RawSettings, patch: SettingsPatch): void {
|
|
319
|
+
if (patch.modelRole) {
|
|
320
|
+
setRawModelRole(raw, patch.modelRole, patch.value as ModelSelectorValue | undefined);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (patch.value === undefined) {
|
|
324
|
+
deleteByPath(raw, patch.path.split("."));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
setByPath(raw, patch.path.split("."), patch.value);
|
|
328
|
+
}
|
|
196
329
|
|
|
197
330
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
198
331
|
// Settings Class
|
|
199
332
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
200
333
|
|
|
201
|
-
export class Settings {
|
|
334
|
+
export class Settings implements NotificationSettingsReader {
|
|
202
335
|
#configPath: string | null;
|
|
203
336
|
#cwd: string;
|
|
204
337
|
#agentDir: string;
|
|
@@ -216,12 +349,19 @@ export class Settings {
|
|
|
216
349
|
/** Latest dirty patch for each path, owned by its generation. */
|
|
217
350
|
#modified = new Map<string, SettingsPatch>();
|
|
218
351
|
#nextGeneration = 0;
|
|
219
|
-
|
|
220
|
-
|
|
352
|
+
#pathRevisions = new Map<string, number>();
|
|
353
|
+
#nextRevision = 0;
|
|
354
|
+
#defaultModelRoleOwnership: DefaultModelRoleOwnership = { generation: 0, defaultLineageKnown: true };
|
|
355
|
+
/** Pending debounced ordinary save; its queue slot is reserved immediately. */
|
|
221
356
|
#saveTimer?: NodeJS.Timeout;
|
|
222
|
-
#
|
|
357
|
+
#savePromise?: Promise<void>;
|
|
358
|
+
#pendingSaveSlot?: PendingSaveSlot;
|
|
223
359
|
#globalModelRoleTail: Promise<void> = Promise.resolve();
|
|
224
360
|
|
|
361
|
+
/** Legacy fallback migration warnings emitted once per settings instance. */
|
|
362
|
+
#legacyFallbackMigrationWarnings = 0;
|
|
363
|
+
#legacyFallbackMigrationGlobalFingerprint: string | undefined;
|
|
364
|
+
|
|
225
365
|
/** Whether to persist changes */
|
|
226
366
|
#persist: boolean;
|
|
227
367
|
|
|
@@ -233,7 +373,8 @@ export class Settings {
|
|
|
233
373
|
|
|
234
374
|
if (options.overrides) {
|
|
235
375
|
for (const [key, value] of Object.entries(options.overrides)) {
|
|
236
|
-
|
|
376
|
+
if (isNotificationSettingsPath(key)) throw new NotificationSettingsOverrideError(key as SettingPath);
|
|
377
|
+
setByPath(this.#overrides, key.split("."), structuredClone(value));
|
|
237
378
|
}
|
|
238
379
|
}
|
|
239
380
|
}
|
|
@@ -267,11 +408,18 @@ export class Settings {
|
|
|
267
408
|
}
|
|
268
409
|
|
|
269
410
|
/**
|
|
270
|
-
* Create an isolated instance for testing.
|
|
411
|
+
* Create an isolated instance for testing with explicit user/global settings.
|
|
271
412
|
* Does not affect the global singleton.
|
|
272
413
|
*/
|
|
273
|
-
static isolated(
|
|
274
|
-
|
|
414
|
+
static isolated(
|
|
415
|
+
globalSettings: Partial<Record<SettingPath, unknown>> = {},
|
|
416
|
+
options: IsolatedSettingsOptions = {},
|
|
417
|
+
): Settings {
|
|
418
|
+
const instance = new Settings({ inMemory: true, overrides: options.overrides });
|
|
419
|
+
for (const [key, value] of Object.entries(globalSettings)) {
|
|
420
|
+
setByPath(instance.#global, key.split("."), structuredClone(value));
|
|
421
|
+
}
|
|
422
|
+
|
|
275
423
|
instance.#rebuildMerged();
|
|
276
424
|
return instance;
|
|
277
425
|
}
|
|
@@ -316,6 +464,113 @@ export class Settings {
|
|
|
316
464
|
return value === undefined ? undefined : (value as SettingValue<P>);
|
|
317
465
|
}
|
|
318
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Read the remote-notification settings from the user/global layer only.
|
|
469
|
+
* Schema defaults are applied per path; project settings and runtime overrides
|
|
470
|
+
* are deliberately excluded from this trust boundary.
|
|
471
|
+
*/
|
|
472
|
+
getNotificationSettingsSnapshot(): NotificationSettingsSnapshot {
|
|
473
|
+
const enabled = this.#getGlobalResolved("notifications.enabled");
|
|
474
|
+
const botToken = this.#getGlobalResolved("notifications.telegram.botToken");
|
|
475
|
+
const chatId = this.#getGlobalResolved("notifications.telegram.chatId");
|
|
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
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
319
574
|
/** Check whether a setting is present in loaded settings/overrides rather than coming from schema defaults. */
|
|
320
575
|
has(path: SettingPath): boolean {
|
|
321
576
|
return getByPath(this.#merged, path.split(".")) !== undefined;
|
|
@@ -323,25 +578,175 @@ export class Settings {
|
|
|
323
578
|
|
|
324
579
|
/**
|
|
325
580
|
* Set a setting value (sync).
|
|
326
|
-
* Updates global settings and
|
|
327
|
-
*
|
|
581
|
+
* Updates global settings and reserves its background persistence slot before
|
|
582
|
+
* returning, so later durable batches cannot overtake this mutation.
|
|
328
583
|
*/
|
|
329
|
-
set<P extends SettingPath>(path: P, value: SettingValue<P>): void {
|
|
584
|
+
set<P extends SettingPath>(path: P, value: SettingValue<P> | undefined): void {
|
|
585
|
+
if (value === undefined) {
|
|
586
|
+
this.unset(path);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this.#set(path, value, true);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#set<P extends SettingPath>(path: P, value: SettingValue<P>, defaultModelRoleMayHaveChanged: boolean): void {
|
|
330
593
|
const prev = this.get(path);
|
|
594
|
+
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
|
+
}
|
|
331
600
|
const patch: SettingsPatch = {
|
|
332
601
|
path,
|
|
333
|
-
value:
|
|
602
|
+
value: clonedValue,
|
|
334
603
|
generation: ++this.#nextGeneration,
|
|
604
|
+
revision: ++this.#nextRevision,
|
|
605
|
+
modelRoleRevision,
|
|
606
|
+
configVersion: this.#defaultModelRoleOwnership.configVersion,
|
|
335
607
|
};
|
|
336
|
-
setByPath(this.#global, path.split("."), structuredClone(
|
|
608
|
+
setByPath(this.#global, path.split("."), structuredClone(clonedValue));
|
|
609
|
+
this.#pathRevisions.set(path, patch.revision);
|
|
337
610
|
this.#modified.set(path, patch);
|
|
611
|
+
|
|
338
612
|
this.#rebuildMerged();
|
|
339
613
|
this.#queueSave();
|
|
340
614
|
|
|
341
|
-
// Trigger hook if exists
|
|
342
615
|
const hook = SETTING_HOOKS[path];
|
|
343
|
-
if (hook)
|
|
344
|
-
|
|
616
|
+
if (hook) hook(value, prev);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Delete a global setting (sync), rather than serializing an ambiguous YAML
|
|
621
|
+
* `undefined` value. Defaults/project settings become visible immediately.
|
|
622
|
+
*/
|
|
623
|
+
unset<P extends SettingPath>(path: P): void {
|
|
624
|
+
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
|
+
const patch: SettingsPatch = {
|
|
631
|
+
path,
|
|
632
|
+
value: undefined,
|
|
633
|
+
generation: ++this.#nextGeneration,
|
|
634
|
+
revision: ++this.#nextRevision,
|
|
635
|
+
modelRoleRevision,
|
|
636
|
+
configVersion: this.#defaultModelRoleOwnership.configVersion,
|
|
637
|
+
};
|
|
638
|
+
deleteByPath(this.#global, path.split("."));
|
|
639
|
+
this.#pathRevisions.set(path, patch.revision);
|
|
640
|
+
this.#modified.set(path, patch);
|
|
641
|
+
this.#rebuildMerged();
|
|
642
|
+
this.#queueSave();
|
|
643
|
+
|
|
644
|
+
const hook = SETTING_HOOKS[path];
|
|
645
|
+
if (hook) hook(this.get(path), prev);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Persist a tagged batch as one atomic YAML replacement. Unlike ordinary
|
|
650
|
+
* {@link set}, canonical state and hooks change only after the rename succeeds.
|
|
651
|
+
*/
|
|
652
|
+
async commitAtomicBatch(patches: readonly SettingsAtomicPatch[]): Promise<CasReceipt> {
|
|
653
|
+
if (!this.#persist || !this.#configPath) {
|
|
654
|
+
throw new Error("commitAtomicBatch requires persistent Settings with a config.yml path.");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const durablePatches: AtomicYamlPatch[] = patches.map(patch => {
|
|
658
|
+
if (!Object.hasOwn(SETTINGS_SCHEMA, patch.path)) {
|
|
659
|
+
throw new Error(`Unknown setting path for atomic batch: ${patch.path}`);
|
|
660
|
+
}
|
|
661
|
+
if (patch.op === "unset") return { path: patch.path, op: "unset" };
|
|
662
|
+
if (patch.value === undefined) {
|
|
663
|
+
throw new TypeError(`Settings set patch for ${patch.path} cannot carry undefined; use unset instead.`);
|
|
664
|
+
}
|
|
665
|
+
return { path: patch.path, op: "set", value: structuredClone(patch.value) };
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// A durable batch is a causal barrier: close the earlier ordinary debounce
|
|
669
|
+
// inside its already-reserved slot before queueing this batch.
|
|
670
|
+
this.#releasePendingSaveSlot();
|
|
671
|
+
|
|
672
|
+
const revisions = durablePatches.map(patch => ({
|
|
673
|
+
patch,
|
|
674
|
+
revision: ++this.#nextRevision,
|
|
675
|
+
previousRevision: this.#pathRevisions.get(patch.path),
|
|
676
|
+
}));
|
|
677
|
+
for (const entry of revisions) this.#pathRevisions.set(entry.patch.path, entry.revision);
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const receipt = await applyAtomicYamlPatches(this.#configPath, durablePatches, {
|
|
681
|
+
onRestored: restoredPatches => this.#applyRestoredDurableBatch(revisions, restoredPatches),
|
|
682
|
+
});
|
|
683
|
+
this.#applyDurableBatch(revisions);
|
|
684
|
+
return receipt;
|
|
685
|
+
} catch (error) {
|
|
686
|
+
for (const entry of revisions) {
|
|
687
|
+
if (this.#pathRevisions.get(entry.patch.path) === entry.revision) {
|
|
688
|
+
if (entry.previousRevision === undefined) this.#pathRevisions.delete(entry.patch.path);
|
|
689
|
+
else this.#pathRevisions.set(entry.patch.path, entry.previousRevision);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Build a durable batch from the current on-disk YAML under the shared queue and file lock. */
|
|
698
|
+
async commitAtomicBatchWithCurrent(
|
|
699
|
+
buildPatches: (
|
|
700
|
+
current: Readonly<RawSettings>,
|
|
701
|
+
) => Promise<readonly SettingsAtomicPatch[]> | readonly SettingsAtomicPatch[],
|
|
702
|
+
): Promise<CasReceipt> {
|
|
703
|
+
if (!this.#persist || !this.#configPath) {
|
|
704
|
+
const patches = await buildPatches(structuredClone(this.#global));
|
|
705
|
+
return this.commitAtomicBatch(patches);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
this.#releasePendingSaveSlot();
|
|
709
|
+
let revisions: DurableBatchRevision[] = [];
|
|
710
|
+
try {
|
|
711
|
+
const receipt = await applyAtomicYamlPatchesWithCurrent(
|
|
712
|
+
this.#configPath,
|
|
713
|
+
async current => {
|
|
714
|
+
const patches = await buildPatches(structuredClone(current));
|
|
715
|
+
const durablePatches: AtomicYamlPatch[] = patches.map(patch => {
|
|
716
|
+
if (!Object.hasOwn(SETTINGS_SCHEMA, patch.path)) {
|
|
717
|
+
throw new Error(`Unknown setting path for atomic batch: ${patch.path}`);
|
|
718
|
+
}
|
|
719
|
+
if (patch.op === "unset") return { path: patch.path, op: "unset" };
|
|
720
|
+
if (patch.value === undefined) {
|
|
721
|
+
throw new TypeError(
|
|
722
|
+
`Settings set patch for ${patch.path} cannot carry undefined; use unset instead.`,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
return { path: patch.path, op: "set", value: structuredClone(patch.value) };
|
|
726
|
+
});
|
|
727
|
+
revisions = durablePatches.map(patch => ({
|
|
728
|
+
patch,
|
|
729
|
+
revision: ++this.#nextRevision,
|
|
730
|
+
previousRevision: this.#pathRevisions.get(patch.path),
|
|
731
|
+
}));
|
|
732
|
+
for (const entry of revisions) this.#pathRevisions.set(entry.patch.path, entry.revision);
|
|
733
|
+
return durablePatches;
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
onRestored: restoredPatches => this.#applyRestoredDurableBatch(revisions, restoredPatches),
|
|
737
|
+
},
|
|
738
|
+
);
|
|
739
|
+
this.#applyDurableBatch(revisions);
|
|
740
|
+
return receipt;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
for (const entry of revisions) {
|
|
743
|
+
if (this.#pathRevisions.get(entry.patch.path) === entry.revision) {
|
|
744
|
+
if (entry.previousRevision === undefined) this.#pathRevisions.delete(entry.patch.path);
|
|
745
|
+
else this.#pathRevisions.set(entry.patch.path, entry.previousRevision);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
749
|
+
throw error;
|
|
345
750
|
}
|
|
346
751
|
}
|
|
347
752
|
|
|
@@ -349,8 +754,9 @@ export class Settings {
|
|
|
349
754
|
* Apply runtime overrides (not persisted).
|
|
350
755
|
*/
|
|
351
756
|
override<P extends SettingPath>(path: P, value: SettingValue<P>): void {
|
|
352
|
-
|
|
353
|
-
|
|
757
|
+
if (isNotificationSettingsPath(path)) throw new NotificationSettingsOverrideError(path);
|
|
758
|
+
const clonedValue = structuredClone(value);
|
|
759
|
+
setByPath(this.#overrides, path.split("."), clonedValue);
|
|
354
760
|
this.#rebuildMerged();
|
|
355
761
|
}
|
|
356
762
|
|
|
@@ -369,45 +775,58 @@ export class Settings {
|
|
|
369
775
|
this.#rebuildMerged();
|
|
370
776
|
}
|
|
371
777
|
|
|
372
|
-
/**
|
|
373
|
-
* Flush any pending saves to disk.
|
|
374
|
-
* Call before exit to ensure all changes are persisted.
|
|
375
|
-
*/
|
|
778
|
+
/** Flush a reserved debounced save without allowing it to be overtaken. */
|
|
376
779
|
async flush(): Promise<void> {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
780
|
+
this.#releasePendingSaveSlot();
|
|
781
|
+
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
782
|
+
this.#releasePendingSaveSlot();
|
|
783
|
+
const observedSave = this.#savePromise;
|
|
784
|
+
try {
|
|
785
|
+
await observedSave;
|
|
786
|
+
} catch {
|
|
787
|
+
// Historical flush() behavior logs background failures but does not reject.
|
|
788
|
+
}
|
|
789
|
+
// A failed predecessor may settle just before a new mutation observes its
|
|
790
|
+
// still-reserved slot. Explicit flush owns one fresh attempt for remaining
|
|
791
|
+
// dirty patches instead of leaving them stranded or retrying forever.
|
|
792
|
+
if (this.#modified.size > 0 && this.#savePromise === observedSave) {
|
|
793
|
+
if (!this.#pendingSaveSlot) this.#queueSave();
|
|
794
|
+
this.#releasePendingSaveSlot();
|
|
795
|
+
try {
|
|
796
|
+
await this.#savePromise;
|
|
797
|
+
} catch {
|
|
798
|
+
// Keep dirty state for a later explicit flush or mutation.
|
|
799
|
+
}
|
|
380
800
|
}
|
|
381
|
-
|
|
382
|
-
await save;
|
|
801
|
+
await this.#refreshDurableSettings();
|
|
383
802
|
}
|
|
384
803
|
|
|
385
|
-
/**
|
|
386
|
-
* Like {@link flush}, but rejects if the durable save fails instead of
|
|
387
|
-
* swallowing the error. Use where the caller must confirm persistence before
|
|
388
|
-
* reporting success (e.g. the Telegram `/rich` toggle). In-memory instances
|
|
389
|
-
* ({@link isolated}) short-circuit in {@link #saveNow} and never throw.
|
|
390
|
-
*/
|
|
804
|
+
/** Like {@link flush}, but reports a durable save failure to the caller. */
|
|
391
805
|
async flushOrThrow(): Promise<void> {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
await save;
|
|
806
|
+
this.#releasePendingSaveSlot();
|
|
807
|
+
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
808
|
+
this.#releasePendingSaveSlot();
|
|
809
|
+
await this.#savePromise;
|
|
810
|
+
await this.#refreshDurableSettings();
|
|
398
811
|
}
|
|
399
812
|
|
|
400
813
|
async cloneForCwd(cwd: string): Promise<Settings> {
|
|
814
|
+
// A clone shares the same config queue. Settle an already-reserved local
|
|
815
|
+
// debounce before the clone can enqueue a durable selector, preventing it
|
|
816
|
+
// from waiting behind a slot only this instance can open.
|
|
817
|
+
await this.flush();
|
|
401
818
|
const cloned = new Settings({
|
|
402
819
|
cwd,
|
|
403
820
|
agentDir: this.#agentDir,
|
|
404
821
|
inMemory: !this.#persist,
|
|
405
822
|
});
|
|
406
823
|
cloned.#storage = this.#storage;
|
|
824
|
+
cloned.#defaultModelRoleOwnership = this.#defaultModelRoleOwnership;
|
|
825
|
+
|
|
407
826
|
cloned.#global = structuredClone(this.#global);
|
|
408
827
|
cloned.#project = this.#persist ? await cloned.#loadProjectSettings() : structuredClone(this.#project);
|
|
409
828
|
cloned.#overrides = structuredClone(this.#overrides);
|
|
410
|
-
cloned.#
|
|
829
|
+
await cloned.#normalizeAfterLoad();
|
|
411
830
|
cloned.#fireAllHooks();
|
|
412
831
|
return cloned;
|
|
413
832
|
}
|
|
@@ -483,7 +902,7 @@ export class Settings {
|
|
|
483
902
|
/**
|
|
484
903
|
* Set a model role (helper for modelRoles record).
|
|
485
904
|
*/
|
|
486
|
-
setModelRole(role: ModelRole | string, modelId:
|
|
905
|
+
setModelRole(role: ModelRole | string, modelId: ModelSelectorValue): void {
|
|
487
906
|
const runtimeOverrides = getByPath(this.#overrides, ["modelRoles"]);
|
|
488
907
|
const updateRuntimeOverride =
|
|
489
908
|
!!runtimeOverrides &&
|
|
@@ -494,52 +913,210 @@ export class Settings {
|
|
|
494
913
|
this.setGlobalModelRole(role, modelId);
|
|
495
914
|
|
|
496
915
|
if (updateRuntimeOverride) {
|
|
497
|
-
this.override("modelRoles", { ...
|
|
916
|
+
this.override("modelRoles", { ...shallowModelSelectorRecord(runtimeOverrides), [role]: modelId });
|
|
498
917
|
}
|
|
499
918
|
}
|
|
500
919
|
|
|
501
|
-
setGlobalModelRole(role: ModelRole | string, modelId:
|
|
502
|
-
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
this.
|
|
506
|
-
return;
|
|
920
|
+
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;
|
|
507
925
|
}
|
|
508
|
-
|
|
926
|
+
const revision = ++this.#nextRevision;
|
|
927
|
+
const patch: SettingsPatch = {
|
|
928
|
+
path: "modelRoles",
|
|
929
|
+
value: modelId,
|
|
930
|
+
generation: ++this.#nextGeneration,
|
|
931
|
+
revision,
|
|
932
|
+
modelRole: role,
|
|
933
|
+
modelRoleRevision,
|
|
934
|
+
configVersion: this.#defaultModelRoleOwnership.configVersion,
|
|
935
|
+
};
|
|
936
|
+
setRawModelRole(this.#global, role, modelId);
|
|
937
|
+
this.#pathRevisions.set("modelRoles", revision);
|
|
938
|
+
this.#modified.set(settingsPatchKey(patch), patch);
|
|
939
|
+
this.#rebuildMerged();
|
|
940
|
+
this.#queueSave();
|
|
509
941
|
}
|
|
510
942
|
|
|
511
|
-
setGlobalModelRoleAndFlush(
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
this.setGlobalModelRole(role, modelId);
|
|
517
|
-
const generation = this.#modified.get("modelRoles")?.generation;
|
|
518
|
-
if (this.#saveTimer) {
|
|
519
|
-
clearTimeout(this.#saveTimer);
|
|
520
|
-
this.#saveTimer = undefined;
|
|
521
|
-
}
|
|
522
|
-
const save = this.#saveNow({ throwOnError: true });
|
|
523
|
-
try {
|
|
524
|
-
await save;
|
|
525
|
-
} catch (error) {
|
|
526
|
-
const currentPatch = this.#modified.get("modelRoles");
|
|
527
|
-
if (currentPatch?.generation === generation) {
|
|
528
|
-
if (hadModelRoles) this.#global.modelRoles = previousModelRoles;
|
|
529
|
-
else delete this.#global.modelRoles;
|
|
530
|
-
if (previousPatch) this.#modified.set("modelRoles", previousPatch);
|
|
531
|
-
else this.#modified.delete("modelRoles");
|
|
532
|
-
this.#rebuildMerged();
|
|
533
|
-
}
|
|
534
|
-
throw error;
|
|
535
|
-
}
|
|
536
|
-
});
|
|
943
|
+
setGlobalModelRoleAndFlush(
|
|
944
|
+
role: ModelRole | string,
|
|
945
|
+
modelId: ModelSelectorValue | undefined,
|
|
946
|
+
): Promise<GlobalDefaultModelRoleCommit> {
|
|
947
|
+
const transaction = this.#globalModelRoleTail.then(() => this.#commitGlobalModelRoleAndFlush(role, modelId));
|
|
537
948
|
this.#globalModelRoleTail = transaction.then(
|
|
538
949
|
() => undefined,
|
|
539
950
|
() => undefined,
|
|
540
951
|
);
|
|
541
952
|
return transaction;
|
|
542
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;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
#setGlobalModelRoleInMemory(
|
|
1081
|
+
role: string,
|
|
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();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
#replaceGlobalWithDurable(
|
|
1101
|
+
current: RawSettings,
|
|
1102
|
+
configVersion?: string,
|
|
1103
|
+
defaultChanged = false,
|
|
1104
|
+
predecessorConfigVersion = configVersion,
|
|
1105
|
+
): void {
|
|
1106
|
+
this.#global = current;
|
|
1107
|
+
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
1108
|
+
this.#applyPatchWithDefaultOwnership(this.#global, { ...patch, value: structuredClone(patch.value) });
|
|
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;
|
|
1117
|
+
}
|
|
1118
|
+
this.#rebuildMerged();
|
|
1119
|
+
}
|
|
543
1120
|
/**
|
|
544
1121
|
* Set an agent model override while keeping any live runtime override aligned.
|
|
545
1122
|
*
|
|
@@ -547,8 +1124,8 @@ export class Settings {
|
|
|
547
1124
|
* session. A user-selected role assignment must win immediately in that same
|
|
548
1125
|
* session, but only the explicit agent change should be persisted.
|
|
549
1126
|
*/
|
|
550
|
-
setAgentModelOverride(agentName: string, modelId:
|
|
551
|
-
const current =
|
|
1127
|
+
setAgentModelOverride(agentName: string, modelId: ModelSelectorValue): void {
|
|
1128
|
+
const current = shallowModelSelectorRecord(getByPath(this.#global, ["task", "agentModelOverrides"]));
|
|
552
1129
|
const runtimeOverrides = getByPath(this.#overrides, ["task", "agentModelOverrides"]);
|
|
553
1130
|
const updateRuntimeOverride =
|
|
554
1131
|
!!runtimeOverrides && typeof runtimeOverrides === "object" && !Array.isArray(runtimeOverrides);
|
|
@@ -557,7 +1134,7 @@ export class Settings {
|
|
|
557
1134
|
|
|
558
1135
|
if (updateRuntimeOverride) {
|
|
559
1136
|
this.override("task.agentModelOverrides", {
|
|
560
|
-
...
|
|
1137
|
+
...shallowModelSelectorRecord(runtimeOverrides),
|
|
561
1138
|
[agentName]: modelId,
|
|
562
1139
|
});
|
|
563
1140
|
}
|
|
@@ -566,7 +1143,7 @@ export class Settings {
|
|
|
566
1143
|
/**
|
|
567
1144
|
* Get a model role (helper for modelRoles record).
|
|
568
1145
|
*/
|
|
569
|
-
getModelRole(role: ModelRole | string):
|
|
1146
|
+
getModelRole(role: ModelRole | string): ModelSelectorValue | undefined {
|
|
570
1147
|
const roles = this.get("modelRoles");
|
|
571
1148
|
return roles[role];
|
|
572
1149
|
}
|
|
@@ -574,19 +1151,17 @@ export class Settings {
|
|
|
574
1151
|
/**
|
|
575
1152
|
* Get all model roles (helper for modelRoles record).
|
|
576
1153
|
*/
|
|
577
|
-
getModelRoles():
|
|
1154
|
+
getModelRoles(): Readonly<Record<string, ModelSelectorValue>> {
|
|
578
1155
|
return { ...this.get("modelRoles") };
|
|
579
1156
|
}
|
|
580
1157
|
|
|
581
1158
|
/*
|
|
582
1159
|
* Override model roles (helper for modelRoles record).
|
|
583
1160
|
*/
|
|
584
|
-
overrideModelRoles(roles:
|
|
585
|
-
const next =
|
|
1161
|
+
overrideModelRoles(roles: Readonly<Record<string, ModelSelectorValue>>): void {
|
|
1162
|
+
const next = shallowModelSelectorRecord(getByPath(this.#overrides, ["modelRoles"]));
|
|
586
1163
|
for (const [role, modelId] of Object.entries(roles)) {
|
|
587
|
-
if (modelId)
|
|
588
|
-
next[role] = modelId;
|
|
589
|
-
}
|
|
1164
|
+
if (modelId) next[role] = Array.isArray(modelId) ? [...modelId] : modelId;
|
|
590
1165
|
}
|
|
591
1166
|
this.override("modelRoles", next);
|
|
592
1167
|
}
|
|
@@ -614,13 +1189,14 @@ export class Settings {
|
|
|
614
1189
|
this.#storage = await AgentStorage.open(getAgentDbPath(this.#agentDir));
|
|
615
1190
|
await this.#migrateFromLegacy();
|
|
616
1191
|
this.#global = await this.#loadYaml(this.#configPath!);
|
|
1192
|
+
const configVersion = readConfigVersion(this.#configPath!);
|
|
1193
|
+
this.#defaultModelRoleOwnership.configVersion = configVersion;
|
|
1194
|
+
this.#defaultModelRoleOwnership.defaultConfigVersion = configVersion;
|
|
617
1195
|
}
|
|
618
1196
|
|
|
619
1197
|
this.#project = await projectPromise;
|
|
620
1198
|
|
|
621
|
-
|
|
622
|
-
this.#rebuildMerged();
|
|
623
|
-
this.#fireAllHooks();
|
|
1199
|
+
await this.#normalizeAfterLoad();
|
|
624
1200
|
return this;
|
|
625
1201
|
}
|
|
626
1202
|
|
|
@@ -644,9 +1220,14 @@ export class Settings {
|
|
|
644
1220
|
const result = await loadCapability(settingsCapability.id, { cwd: this.#cwd });
|
|
645
1221
|
let merged: RawSettings = {};
|
|
646
1222
|
for (const item of result.items as SettingsCapabilityItem[]) {
|
|
647
|
-
if (item.level
|
|
648
|
-
|
|
1223
|
+
if (item.level !== "project") continue;
|
|
1224
|
+
const { settings, rejectedNotifications } = this.#stripProjectNotificationSettings(
|
|
1225
|
+
item.data as RawSettings,
|
|
1226
|
+
);
|
|
1227
|
+
if (rejectedNotifications) {
|
|
1228
|
+
logger.warn("Settings: ignoring project notification settings", { path: item.path });
|
|
649
1229
|
}
|
|
1230
|
+
merged = this.#deepMerge(merged, settings);
|
|
650
1231
|
}
|
|
651
1232
|
return this.#migrateRawSettings(merged);
|
|
652
1233
|
} catch {
|
|
@@ -654,6 +1235,149 @@ export class Settings {
|
|
|
654
1235
|
}
|
|
655
1236
|
}
|
|
656
1237
|
|
|
1238
|
+
async #normalizeAfterLoad(): Promise<void> {
|
|
1239
|
+
this.#sanitizeModelSelectorRecords();
|
|
1240
|
+
this.#rebuildMerged();
|
|
1241
|
+
this.#legacyFallbackMigrationGlobalFingerprint = YAML.stringify(this.#global, null, 2);
|
|
1242
|
+
this.#migrateRetryFallbackChains();
|
|
1243
|
+
if (
|
|
1244
|
+
!this.#modified.has("modelRoles") &&
|
|
1245
|
+
![...this.#modified.keys()].some(path => path.startsWith("retry.fallback"))
|
|
1246
|
+
) {
|
|
1247
|
+
this.#legacyFallbackMigrationGlobalFingerprint = undefined;
|
|
1248
|
+
}
|
|
1249
|
+
await this.flush();
|
|
1250
|
+
this.#sanitizeModelSelectorRecords();
|
|
1251
|
+
this.#rebuildMerged();
|
|
1252
|
+
this.#fireAllHooks();
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
#sanitizeModelSelectorRecords(): void {
|
|
1256
|
+
for (const source of [this.#global, this.#project, this.#overrides]) {
|
|
1257
|
+
for (const pathSegments of [["modelRoles"], ["task", "agentModelOverrides"]]) {
|
|
1258
|
+
const raw = getByPath(source, pathSegments);
|
|
1259
|
+
if (raw === undefined) continue;
|
|
1260
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1261
|
+
logger.warn("Settings: replaced malformed model selector record", { path: pathSegments.join(".") });
|
|
1262
|
+
setByPath(source, pathSegments, {});
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
const sanitized = shallowModelSelectorRecord(raw);
|
|
1266
|
+
if (Object.keys(sanitized).length !== Object.keys(raw).length) {
|
|
1267
|
+
logger.warn("Settings: dropped invalid model selector values", {
|
|
1268
|
+
path: pathSegments.join("."),
|
|
1269
|
+
dropped: Object.keys(raw).filter(key => !(key in sanitized)),
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
setByPath(source, pathSegments, sanitized);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
#migrateRetryFallbackChains(): void {
|
|
1278
|
+
const globalChains = legacyFallbackChains(getByPath(this.#global, ["retry", "fallbackChains"]));
|
|
1279
|
+
const projectChains = legacyFallbackChains(getByPath(this.#project, ["retry", "fallbackChains"]));
|
|
1280
|
+
const overrideChains = legacyFallbackChains(getByPath(this.#overrides, ["retry", "fallbackChains"]));
|
|
1281
|
+
const roles = new Set([
|
|
1282
|
+
...Object.keys(globalChains),
|
|
1283
|
+
...Object.keys(projectChains),
|
|
1284
|
+
...Object.keys(overrideChains),
|
|
1285
|
+
]);
|
|
1286
|
+
const retainedGlobalChains: Record<string, unknown> = {};
|
|
1287
|
+
const effectiveRoles = shallowModelSelectorRecord(getByPath(this.#merged, ["modelRoles"]));
|
|
1288
|
+
for (const role of roles) {
|
|
1289
|
+
const source = Object.hasOwn(overrideChains, role)
|
|
1290
|
+
? "override"
|
|
1291
|
+
: Object.hasOwn(projectChains, role)
|
|
1292
|
+
? "project"
|
|
1293
|
+
: "global";
|
|
1294
|
+
const tailValue =
|
|
1295
|
+
source === "override"
|
|
1296
|
+
? overrideChains[role]
|
|
1297
|
+
: source === "project"
|
|
1298
|
+
? projectChains[role]
|
|
1299
|
+
: globalChains[role];
|
|
1300
|
+
const primary = selectorChain(effectiveRoles[role]);
|
|
1301
|
+
const tail = selectorChain(tailValue);
|
|
1302
|
+
const chain = [...new Set([...primary, ...tail])];
|
|
1303
|
+
if (primary.length === 0 || tail.length === 0) {
|
|
1304
|
+
this.#warnLegacyFallbackMigration(
|
|
1305
|
+
`retry.fallbackChains.${role} could not be migrated because it lacks a valid primary selector or tail.`,
|
|
1306
|
+
);
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
const target =
|
|
1310
|
+
source === "override" || hasOwnModelRole(this.#overrides, role)
|
|
1311
|
+
? this.#overrides
|
|
1312
|
+
: source === "project" || hasOwnModelRole(this.#project, role)
|
|
1313
|
+
? this.#project
|
|
1314
|
+
: this.#global;
|
|
1315
|
+
const targetRoles = shallowModelSelectorRecord(getByPath(target, ["modelRoles"]));
|
|
1316
|
+
setByPath(target, ["modelRoles"], { ...targetRoles, [role]: chain });
|
|
1317
|
+
if (target === this.#global) {
|
|
1318
|
+
this.#recordLegacyFallbackMigrationPatch("modelRoles", getByPath(this.#global, ["modelRoles"]));
|
|
1319
|
+
}
|
|
1320
|
+
if (target !== this.#global && Object.hasOwn(globalChains, role))
|
|
1321
|
+
retainedGlobalChains[role] = globalChains[role];
|
|
1322
|
+
if (source === "project") {
|
|
1323
|
+
this.#warnLegacyFallbackMigration(
|
|
1324
|
+
`retry.fallbackChains.${role} is project-owned and was migrated in memory only.`,
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
for (const source of [this.#project, this.#overrides]) {
|
|
1329
|
+
deleteByPath(source, ["retry", "fallbackChains"]);
|
|
1330
|
+
deleteByPath(source, ["retry", "fallbackRevertPolicy"]);
|
|
1331
|
+
}
|
|
1332
|
+
if (Object.keys(retainedGlobalChains).length > 0) {
|
|
1333
|
+
setByPath(this.#global, ["retry", "fallbackChains"], retainedGlobalChains);
|
|
1334
|
+
this.#recordLegacyFallbackMigrationPatch("retry.fallbackChains", retainedGlobalChains);
|
|
1335
|
+
} else if (getByPath(this.#global, ["retry", "fallbackChains"]) !== undefined) {
|
|
1336
|
+
deleteByPath(this.#global, ["retry", "fallbackChains"]);
|
|
1337
|
+
this.#recordLegacyFallbackMigrationPatch("retry.fallbackChains", undefined);
|
|
1338
|
+
}
|
|
1339
|
+
if (
|
|
1340
|
+
Object.keys(retainedGlobalChains).length === 0 &&
|
|
1341
|
+
getByPath(this.#global, ["retry", "fallbackRevertPolicy"]) !== undefined
|
|
1342
|
+
) {
|
|
1343
|
+
deleteByPath(this.#global, ["retry", "fallbackRevertPolicy"]);
|
|
1344
|
+
this.#recordLegacyFallbackMigrationPatch("retry.fallbackRevertPolicy", undefined);
|
|
1345
|
+
}
|
|
1346
|
+
if (
|
|
1347
|
+
Object.keys(retainedGlobalChains).length === 0 &&
|
|
1348
|
+
this.#global.retry !== undefined &&
|
|
1349
|
+
Object.keys(rawSettingsRecord(this.#global.retry) ?? {}).length === 0
|
|
1350
|
+
) {
|
|
1351
|
+
delete this.#global.retry;
|
|
1352
|
+
this.#recordLegacyFallbackMigrationPatch("retry", undefined);
|
|
1353
|
+
}
|
|
1354
|
+
this.#rebuildMerged();
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
#recordLegacyFallbackMigrationPatch(path: string, value: unknown): void {
|
|
1358
|
+
const existing = this.#modified.get(path);
|
|
1359
|
+
if (existing && !existing.legacyFallbackMigration) {
|
|
1360
|
+
this.#modified.set(path, { ...existing, value: structuredClone(value) });
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const revision = ++this.#nextRevision;
|
|
1364
|
+
this.#pathRevisions.set(path, revision);
|
|
1365
|
+
this.#modified.set(path, {
|
|
1366
|
+
path,
|
|
1367
|
+
value: structuredClone(value),
|
|
1368
|
+
generation: ++this.#nextGeneration,
|
|
1369
|
+
revision,
|
|
1370
|
+
configVersion: this.#defaultModelRoleOwnership.configVersion,
|
|
1371
|
+
legacyFallbackMigration: true,
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
#warnLegacyFallbackMigration(message: string): void {
|
|
1376
|
+
if (this.#legacyFallbackMigrationWarnings >= 10) return;
|
|
1377
|
+
this.#legacyFallbackMigrationWarnings++;
|
|
1378
|
+
logger.warn(`Settings: ${message}`);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
657
1381
|
async #migrateFromLegacy(): Promise<void> {
|
|
658
1382
|
if (!this.#configPath) return;
|
|
659
1383
|
|
|
@@ -690,10 +1414,17 @@ export class Settings {
|
|
|
690
1414
|
}
|
|
691
1415
|
} catch {}
|
|
692
1416
|
|
|
693
|
-
// 3. Write merged settings
|
|
1417
|
+
// 3. Write merged settings through the shared atomic YAML pipeline.
|
|
694
1418
|
if (migrated && Object.keys(settings).length > 0) {
|
|
695
1419
|
try {
|
|
696
|
-
await
|
|
1420
|
+
await applyAtomicYamlPatches(
|
|
1421
|
+
this.#configPath,
|
|
1422
|
+
Object.entries(settings).map(([settingPath, value]) => ({
|
|
1423
|
+
path: settingPath,
|
|
1424
|
+
op: "set" as const,
|
|
1425
|
+
value,
|
|
1426
|
+
})),
|
|
1427
|
+
);
|
|
697
1428
|
logger.debug("Settings: migrated to config.yml", { path: this.#configPath });
|
|
698
1429
|
} catch {}
|
|
699
1430
|
}
|
|
@@ -870,59 +1601,257 @@ export class Settings {
|
|
|
870
1601
|
#queueSave(): void {
|
|
871
1602
|
if (!this.#persist || !this.#configPath) return;
|
|
872
1603
|
|
|
1604
|
+
const currentSlot = this.#pendingSaveSlot;
|
|
1605
|
+
if (currentSlot && !currentSlot.captured && !currentSlot.released) {
|
|
1606
|
+
this.#armSaveTimer(currentSlot);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
let release!: () => void;
|
|
1611
|
+
const slot: PendingSaveSlot = {
|
|
1612
|
+
captured: false,
|
|
1613
|
+
released: false,
|
|
1614
|
+
release: () => release(),
|
|
1615
|
+
wait: new Promise<void>(resolve => {
|
|
1616
|
+
release = resolve;
|
|
1617
|
+
}),
|
|
1618
|
+
};
|
|
1619
|
+
this.#pendingSaveSlot = slot;
|
|
1620
|
+
|
|
1621
|
+
let captured: SettingsPatch[] = [];
|
|
1622
|
+
let durableBeforeWrite: RawSettings | undefined;
|
|
1623
|
+
let durableVersionBeforeWrite: string | undefined;
|
|
1624
|
+
const save = reserveAtomicYamlUpdateSlot(this.#configPath, async () => {
|
|
1625
|
+
await slot.wait;
|
|
1626
|
+
slot.captured = true;
|
|
1627
|
+
if (this.#pendingSaveSlot === slot) this.#pendingSaveSlot = undefined;
|
|
1628
|
+
captured = this.#pendingPatchesInGenerationOrder();
|
|
1629
|
+
return {
|
|
1630
|
+
apply: current => {
|
|
1631
|
+
this.#migrateRawSettings(current);
|
|
1632
|
+
const migrationFingerprint = this.#legacyFallbackMigrationGlobalFingerprint;
|
|
1633
|
+
this.#legacyFallbackMigrationGlobalFingerprint = undefined;
|
|
1634
|
+
if (migrationFingerprint !== undefined && YAML.stringify(current, null, 2) !== migrationFingerprint) {
|
|
1635
|
+
this.#global = structuredClone(current);
|
|
1636
|
+
this.#rebuildMerged();
|
|
1637
|
+
if (getByPath(current, ["retry", "fallbackChains"]) !== undefined) {
|
|
1638
|
+
this.#defaultModelRoleOwnership.configVersion = readConfigVersion(this.#configPath!);
|
|
1639
|
+
this.#defaultModelRoleOwnership.defaultLineageKnown = false;
|
|
1640
|
+
this.#migrateRetryFallbackChains();
|
|
1641
|
+
captured = this.#pendingPatchesInGenerationOrder();
|
|
1642
|
+
} else {
|
|
1643
|
+
for (const patch of captured) {
|
|
1644
|
+
if (!patch.legacyFallbackMigration) continue;
|
|
1645
|
+
const key = settingsPatchKey(patch);
|
|
1646
|
+
if (this.#modified.get(key)?.generation === patch.generation) this.#modified.delete(key);
|
|
1647
|
+
}
|
|
1648
|
+
captured = captured.filter(patch => !patch.legacyFallbackMigration);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
const currentConfigVersion = readConfigVersion(this.#configPath!);
|
|
1652
|
+
durableBeforeWrite = structuredClone(current);
|
|
1653
|
+
durableVersionBeforeWrite = currentConfigVersion;
|
|
1654
|
+
const externalLineageBreak = currentConfigVersion !== this.#defaultModelRoleOwnership.configVersion;
|
|
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 };
|
|
1668
|
+
},
|
|
1669
|
+
shouldWrite: result => result.shouldWrite,
|
|
1670
|
+
committed: (current, result) => {
|
|
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
|
+
}
|
|
1679
|
+
for (const patch of captured) {
|
|
1680
|
+
const key = settingsPatchKey(patch);
|
|
1681
|
+
if (this.#modified.get(key)?.generation === patch.generation) {
|
|
1682
|
+
this.#modified.delete(key);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
this.#global = current;
|
|
1686
|
+
for (const [key, patch] of [...this.#modified].sort(
|
|
1687
|
+
(left, right) => left[1].generation - right[1].generation,
|
|
1688
|
+
)) {
|
|
1689
|
+
if (this.#isStaleDefaultModelRolePatch(patch, savedConfigVersion) && patch.modelRole === "default") {
|
|
1690
|
+
this.#modified.delete(key);
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
this.#applyPatchWithDefaultOwnership(
|
|
1694
|
+
this.#global,
|
|
1695
|
+
{ ...patch, value: structuredClone(patch.value) },
|
|
1696
|
+
savedConfigVersion,
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
this.#rebuildMerged();
|
|
1700
|
+
},
|
|
1701
|
+
};
|
|
1702
|
+
}).then(() => undefined);
|
|
1703
|
+
this.#savePromise = save;
|
|
1704
|
+
void save.catch(error => {
|
|
1705
|
+
logger.warn("Settings: background save failed", { error: String(error) });
|
|
1706
|
+
let droppedStaleDefault = false;
|
|
1707
|
+
for (const patch of captured) {
|
|
1708
|
+
const key = settingsPatchKey(patch);
|
|
1709
|
+
const currentPatch = this.#modified.get(key);
|
|
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
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (droppedStaleDefault && durableBeforeWrite) {
|
|
1722
|
+
setRawModelRole(
|
|
1723
|
+
this.#global,
|
|
1724
|
+
"default",
|
|
1725
|
+
defaultModelRoleFrom(durableBeforeWrite),
|
|
1726
|
+
!Object.hasOwn(durableBeforeWrite, "modelRoles"),
|
|
1727
|
+
);
|
|
1728
|
+
this.#defaultModelRoleOwnership.configVersion = durableVersionBeforeWrite;
|
|
1729
|
+
this.#defaultModelRoleOwnership.defaultLineageKnown = false;
|
|
1730
|
+
this.#rebuildMerged();
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
this.#armSaveTimer(slot);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
#armSaveTimer(slot: PendingSaveSlot): void {
|
|
873
1737
|
if (this.#saveTimer) clearTimeout(this.#saveTimer);
|
|
874
1738
|
this.#saveTimer = setTimeout(() => {
|
|
875
1739
|
this.#saveTimer = undefined;
|
|
876
|
-
|
|
1740
|
+
if (slot.released) return;
|
|
1741
|
+
slot.released = true;
|
|
1742
|
+
slot.release();
|
|
877
1743
|
}, 100);
|
|
878
1744
|
}
|
|
879
1745
|
|
|
880
|
-
|
|
881
|
-
|
|
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
|
+
}
|
|
882
1760
|
|
|
883
|
-
|
|
884
|
-
|
|
1761
|
+
#pendingPatchesInGenerationOrder(): SettingsPatch[] {
|
|
1762
|
+
return [...this.#modified.values()].sort((left, right) => left.generation - right.generation);
|
|
1763
|
+
}
|
|
885
1764
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
+
#releasePendingSaveSlot(): void {
|
|
1785
|
+
if (this.#saveTimer) {
|
|
1786
|
+
clearTimeout(this.#saveTimer);
|
|
1787
|
+
this.#saveTimer = undefined;
|
|
1788
|
+
}
|
|
1789
|
+
const slot = this.#pendingSaveSlot;
|
|
1790
|
+
if (!slot || slot.released) return;
|
|
1791
|
+
slot.released = true;
|
|
1792
|
+
slot.release();
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
#applyDurableBatch(revisions: readonly DurableBatchRevision[]): void {
|
|
1796
|
+
this.#applyDurablePatches(
|
|
1797
|
+
revisions,
|
|
1798
|
+
revisions.map(entry => entry.patch),
|
|
1799
|
+
true,
|
|
907
1800
|
);
|
|
1801
|
+
}
|
|
908
1802
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1803
|
+
#applyRestoredDurableBatch(
|
|
1804
|
+
revisions: readonly DurableBatchRevision[],
|
|
1805
|
+
restoredPatches: readonly AtomicYamlPatch[],
|
|
1806
|
+
): void {
|
|
1807
|
+
this.#applyDurablePatches(revisions, restoredPatches, false);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
#applyDurablePatches(
|
|
1811
|
+
revisions: readonly DurableBatchRevision[],
|
|
1812
|
+
patches: readonly AtomicYamlPatch[],
|
|
1813
|
+
clearStagedMutations: boolean,
|
|
1814
|
+
): void {
|
|
1815
|
+
const revisionsByPath = new Map<string, DurableBatchRevision>();
|
|
1816
|
+
for (const entry of revisions) revisionsByPath.set(entry.patch.path, entry);
|
|
1817
|
+
const finalPatches = new Map<string, AtomicYamlPatch>();
|
|
1818
|
+
for (const patch of patches) finalPatches.set(patch.path, patch);
|
|
1819
|
+
const applicable = [...finalPatches.values()].filter(patch => {
|
|
1820
|
+
const revision = revisionsByPath.get(patch.path);
|
|
1821
|
+
return revision !== undefined && this.#pathRevisions.get(patch.path) === revision.revision;
|
|
1822
|
+
});
|
|
1823
|
+
if (applicable.length === 0) return;
|
|
1824
|
+
|
|
1825
|
+
const previous = new Map<SettingPath, SettingValue<SettingPath>>();
|
|
1826
|
+
for (const patch of applicable) {
|
|
1827
|
+
const settingPath = patch.path as SettingPath;
|
|
1828
|
+
const revision = revisionsByPath.get(patch.path)!;
|
|
1829
|
+
previous.set(settingPath, this.get(settingPath));
|
|
1830
|
+
if (patch.op === "set") {
|
|
1831
|
+
setByPath(this.#global, settingPath.split("."), structuredClone(patch.value));
|
|
1832
|
+
} else {
|
|
1833
|
+
deleteByPath(this.#global, settingPath.split("."));
|
|
918
1834
|
}
|
|
919
|
-
if (
|
|
920
|
-
this.#
|
|
921
|
-
|
|
1835
|
+
if (clearStagedMutations) {
|
|
1836
|
+
for (const [key, staged] of this.#modified) {
|
|
1837
|
+
if (staged.path === settingPath && staged.revision <= revision.revision) {
|
|
1838
|
+
this.#modified.delete(key);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
922
1841
|
}
|
|
923
1842
|
}
|
|
924
|
-
|
|
925
1843
|
this.#rebuildMerged();
|
|
1844
|
+
for (const patch of applicable) {
|
|
1845
|
+
const settingPath = patch.path as SettingPath;
|
|
1846
|
+
const hook = SETTING_HOOKS[settingPath];
|
|
1847
|
+
if (hook) hook(this.get(settingPath), previous.get(settingPath)!);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
async #refreshDurableSettings(): Promise<void> {
|
|
1852
|
+
if (!this.#persist || !this.#configPath) return;
|
|
1853
|
+
const current = await this.#loadYaml(this.#configPath);
|
|
1854
|
+
this.#replaceGlobalWithDurable(current, readConfigVersion(this.#configPath), false);
|
|
926
1855
|
}
|
|
927
1856
|
|
|
928
1857
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -944,6 +1873,39 @@ export class Settings {
|
|
|
944
1873
|
}
|
|
945
1874
|
}
|
|
946
1875
|
|
|
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
|
+
#stripProjectNotificationSettings(settings: RawSettings): {
|
|
1882
|
+
settings: RawSettings;
|
|
1883
|
+
rejectedNotifications: boolean;
|
|
1884
|
+
} {
|
|
1885
|
+
let rejectedNotifications = false;
|
|
1886
|
+
const sanitized: RawSettings = {};
|
|
1887
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
1888
|
+
if (key === "notifications" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
1889
|
+
const localNotifications: Record<string, unknown> = {};
|
|
1890
|
+
for (const [notificationKey, notificationValue] of Object.entries(value)) {
|
|
1891
|
+
if (LOCAL_NOTIFICATION_SETTING_KEYS.has(notificationKey)) {
|
|
1892
|
+
localNotifications[notificationKey] = notificationValue;
|
|
1893
|
+
} else {
|
|
1894
|
+
rejectedNotifications = true;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (Object.keys(localNotifications).length > 0) sanitized[key] = localNotifications;
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
if (isNotificationSettingsPath(key)) {
|
|
1901
|
+
rejectedNotifications = true;
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
sanitized[key] = value;
|
|
1905
|
+
}
|
|
1906
|
+
return { settings: sanitized, rejectedNotifications };
|
|
1907
|
+
}
|
|
1908
|
+
|
|
947
1909
|
#deepMerge(base: RawSettings, overrides: RawSettings): RawSettings {
|
|
948
1910
|
const result = { ...base };
|
|
949
1911
|
for (const key of Object.keys(overrides)) {
|