@gajae-code/coding-agent 0.11.1 → 0.11.3

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