@gajae-code/coding-agent 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/notify-cli.d.ts +1 -0
  4. package/dist/types/cli/session-picker.d.ts +2 -1
  5. package/dist/types/cli/setup-cli.d.ts +1 -1
  6. package/dist/types/commands/mcp-serve.d.ts +33 -0
  7. package/dist/types/commands/mcp.d.ts +1 -1
  8. package/dist/types/commands/notify.d.ts +0 -2
  9. package/dist/types/commands/sdk.d.ts +1 -1
  10. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  11. package/dist/types/config/file-lock.d.ts +14 -4
  12. package/dist/types/config/keybindings.d.ts +42 -1
  13. package/dist/types/config/model-auth.d.ts +3 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  15. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  16. package/dist/types/config/model-equivalence.d.ts +20 -0
  17. package/dist/types/config/model-registry.d.ts +16 -15
  18. package/dist/types/config/model-resolver.d.ts +28 -18
  19. package/dist/types/config/models-config-schema.d.ts +11 -0
  20. package/dist/types/config/settings-schema.d.ts +56 -1
  21. package/dist/types/config/settings.d.ts +14 -11
  22. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  23. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  24. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  25. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  26. package/dist/types/discovery/helpers.d.ts +0 -6
  27. package/dist/types/edit/index.d.ts +3 -0
  28. package/dist/types/eval/py/env.d.ts +9 -0
  29. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  30. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  31. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  32. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  33. package/dist/types/extensibility/shared-events.d.ts +2 -0
  34. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  35. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  36. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  37. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  38. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  39. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  40. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  41. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  42. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  43. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  44. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  45. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  46. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  47. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  49. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  50. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  51. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  52. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  53. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  54. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  55. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  56. package/dist/types/goals/runtime.d.ts +1 -0
  57. package/dist/types/goals/state.d.ts +8 -0
  58. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  59. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  60. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  61. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  62. package/dist/types/hindsight/state.d.ts +11 -4
  63. package/dist/types/lsp/lspmux.d.ts +5 -3
  64. package/dist/types/lsp/path-trust.d.ts +1 -0
  65. package/dist/types/main.d.ts +3 -2
  66. package/dist/types/memories/storage.d.ts +1 -0
  67. package/dist/types/modes/action-registry.d.ts +29 -0
  68. package/dist/types/modes/components/chrome.d.ts +14 -0
  69. package/dist/types/modes/components/command-palette.d.ts +27 -0
  70. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  71. package/dist/types/modes/components/index.d.ts +2 -1
  72. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  73. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  74. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  75. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  76. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  77. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  78. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  79. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  80. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  81. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  82. package/dist/types/modes/components/status-line.d.ts +1 -94
  83. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  84. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  85. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  86. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  87. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  88. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  89. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  90. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  91. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  92. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  93. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  94. package/dist/types/modes/interactive-mode.d.ts +19 -17
  95. package/dist/types/modes/jobs-observer.d.ts +2 -0
  96. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  97. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  98. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  99. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  100. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  101. package/dist/types/modes/types.d.ts +20 -13
  102. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  103. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  104. package/dist/types/sdk/broker/broker.d.ts +78 -12
  105. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  106. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  107. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  108. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  109. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  110. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  111. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  112. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  113. package/dist/types/sdk/bus/config.d.ts +11 -0
  114. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  115. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  116. package/dist/types/sdk/bus/index.d.ts +27 -0
  117. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  118. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  119. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  120. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  121. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  122. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  123. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  124. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  125. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  126. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  127. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  128. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  129. package/dist/types/sdk/host/host.d.ts +2 -0
  130. package/dist/types/sdk/index.d.ts +1 -0
  131. package/dist/types/sdk/session-directory.d.ts +56 -0
  132. package/dist/types/session/agent-session.d.ts +82 -47
  133. package/dist/types/session/agent-storage.d.ts +7 -0
  134. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  135. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  136. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  137. package/dist/types/session/messages.d.ts +16 -0
  138. package/dist/types/session/session-manager-internal.d.ts +14 -0
  139. package/dist/types/session/session-manager.d.ts +82 -12
  140. package/dist/types/session/session-storage.d.ts +43 -3
  141. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  142. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  143. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  144. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  145. package/dist/types/system-prompt.d.ts +2 -11
  146. package/dist/types/task/executor.d.ts +3 -1
  147. package/dist/types/thinking.d.ts +7 -0
  148. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  149. package/dist/types/tools/ask.d.ts +64 -2
  150. package/dist/types/tools/bash.d.ts +3 -0
  151. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  152. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  153. package/dist/types/tools/cron.d.ts +2 -0
  154. package/dist/types/tools/fetch.d.ts +1 -0
  155. package/dist/types/tools/index.d.ts +30 -15
  156. package/dist/types/tools/output-meta.d.ts +1 -0
  157. package/dist/types/tools/read.d.ts +3 -0
  158. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  159. package/dist/types/utils/event-bus.d.ts +1 -1
  160. package/dist/types/web/search/index.d.ts +3 -1
  161. package/package.json +21 -9
  162. package/scripts/build-sdk-package-smoke.ts +34 -1
  163. package/scripts/generate-hotkeys-docs.ts +74 -0
  164. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  165. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  166. package/src/async/job-manager.ts +49 -6
  167. package/src/cli/config-cli.ts +75 -2
  168. package/src/cli/notify-cli.ts +92 -26
  169. package/src/cli/session-picker.ts +20 -3
  170. package/src/cli/setup-cli.ts +1 -1
  171. package/src/cli.ts +1 -1
  172. package/src/commands/config.ts +1 -1
  173. package/src/commands/harness.ts +34 -106
  174. package/src/commands/launch.ts +6 -4
  175. package/src/commands/mcp-serve.ts +85 -8
  176. package/src/commands/mcp.ts +2 -2
  177. package/src/commands/notify.ts +8 -4
  178. package/src/commands/sdk.ts +61 -40
  179. package/src/commands/ultragoal.ts +4 -1
  180. package/src/config/atomic-yaml-patch.ts +80 -13
  181. package/src/config/file-lock.ts +105 -43
  182. package/src/config/keybindings.ts +64 -20
  183. package/src/config/model-auth.ts +9 -0
  184. package/src/config/model-bindings-applier.ts +139 -0
  185. package/src/config/model-discovery-manager.ts +224 -0
  186. package/src/config/model-equivalence.ts +66 -0
  187. package/src/config/model-registry.ts +169 -302
  188. package/src/config/model-resolver.ts +361 -369
  189. package/src/config/models-config-schema.ts +5 -4
  190. package/src/config/settings-schema.ts +159 -2
  191. package/src/config/settings.ts +458 -482
  192. package/src/coordinator-mcp/policy.ts +76 -11
  193. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  194. package/src/coordinator-mcp/question-state.ts +778 -0
  195. package/src/coordinator-mcp/server.ts +1702 -238
  196. package/src/debug/index.ts +1 -1
  197. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  198. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  199. package/src/discovery/helpers.ts +1 -50
  200. package/src/edit/index.ts +64 -0
  201. package/src/eval/py/env.ts +26 -0
  202. package/src/eval/py/kernel.ts +5 -3
  203. package/src/extensibility/extensions/loader.ts +9 -1
  204. package/src/extensibility/extensions/runner.ts +12 -3
  205. package/src/extensibility/extensions/types.ts +39 -1
  206. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  207. package/src/extensibility/hooks/runner.ts +2 -2
  208. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  209. package/src/extensibility/shared-events.ts +2 -0
  210. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  211. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  212. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  213. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  214. package/src/gjc-runtime/launch-tmux.ts +2 -5
  215. package/src/gjc-runtime/launch-worktree.ts +5 -5
  216. package/src/gjc-runtime/linux-proc.ts +71 -0
  217. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  218. package/src/gjc-runtime/session-resolution.ts +18 -3
  219. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  220. package/src/gjc-runtime/state-migrations.ts +1 -1
  221. package/src/gjc-runtime/state-runtime.ts +320 -249
  222. package/src/gjc-runtime/state-writer.ts +36 -40
  223. package/src/gjc-runtime/team-launch.ts +319 -0
  224. package/src/gjc-runtime/team-notify.ts +326 -0
  225. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  226. package/src/gjc-runtime/team-store.ts +828 -0
  227. package/src/gjc-runtime/team-workers.ts +962 -0
  228. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  229. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  230. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  231. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  232. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  233. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  234. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  235. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  236. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  237. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  238. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  239. package/src/goals/continuation-timeout-guard.ts +79 -0
  240. package/src/goals/runtime.ts +7 -5
  241. package/src/goals/state.ts +18 -0
  242. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  243. package/src/harness-control-plane/phase-rollup.ts +4 -7
  244. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  245. package/src/harness-control-plane/receipt-spool.ts +51 -7
  246. package/src/harness-control-plane/session-lease.ts +12 -130
  247. package/src/hindsight/backend.ts +26 -27
  248. package/src/hindsight/state.ts +101 -32
  249. package/src/hooks/skill-state.ts +17 -11
  250. package/src/internal-urls/docs-index.generated.ts +21 -18
  251. package/src/lsp/client.ts +1 -1
  252. package/src/lsp/config.ts +145 -26
  253. package/src/lsp/index.ts +1 -1
  254. package/src/lsp/lspmux.ts +39 -10
  255. package/src/lsp/path-trust.ts +81 -0
  256. package/src/main.ts +28 -8
  257. package/src/memories/index.ts +1 -0
  258. package/src/memories/storage.ts +6 -2
  259. package/src/modes/action-registry.ts +165 -0
  260. package/src/modes/components/chrome.ts +40 -0
  261. package/src/modes/components/command-palette.ts +168 -0
  262. package/src/modes/components/custom-editor.ts +1 -1
  263. package/src/modes/components/custom-provider-wizard.ts +51 -14
  264. package/src/modes/components/index.ts +2 -1
  265. package/src/modes/components/pet-selector.ts +11 -17
  266. package/src/modes/components/plan-preview-overlay.ts +295 -0
  267. package/src/modes/components/queue-mode-selector.ts +10 -27
  268. package/src/modes/components/queue-pane.ts +79 -0
  269. package/src/modes/components/read-tool-group.ts +13 -0
  270. package/src/modes/components/session-observer-overlay.ts +207 -772
  271. package/src/modes/components/sessions-dashboard.ts +181 -0
  272. package/src/modes/components/settings-selector.ts +1 -1
  273. package/src/modes/components/show-images-selector.ts +10 -24
  274. package/src/modes/components/status-line/types.ts +1 -1
  275. package/src/modes/components/status-line.ts +1 -969
  276. package/src/modes/components/tasks-pane.ts +73 -0
  277. package/src/modes/components/theme-selector.ts +11 -31
  278. package/src/modes/components/tool-execution.ts +22 -0
  279. package/src/modes/components/tool-status-header.ts +1056 -0
  280. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  281. package/src/modes/controllers/btw-controller.ts +1 -0
  282. package/src/modes/controllers/event-controller.ts +3 -2
  283. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  284. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  285. package/src/modes/controllers/input-controller.ts +764 -229
  286. package/src/modes/controllers/mode-gate.ts +24 -0
  287. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  288. package/src/modes/controllers/selector-controller.ts +225 -10
  289. package/src/modes/controllers/stt-controller.ts +125 -0
  290. package/src/modes/interactive-mode.ts +289 -1150
  291. package/src/modes/jobs-observer.ts +3 -0
  292. package/src/modes/runtime-init.ts +8 -0
  293. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  294. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  295. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  296. package/src/modes/tasks-aggregator.ts +199 -0
  297. package/src/modes/transcript-item-registry.ts +229 -0
  298. package/src/modes/types.ts +45 -18
  299. package/src/modes/utils/ui-helpers.ts +17 -9
  300. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  301. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  302. package/src/prompts/agents/architect.md +2 -8
  303. package/src/prompts/agents/critic.md +2 -8
  304. package/src/prompts/agents/executor.md +3 -0
  305. package/src/prompts/agents/planner.md +4 -12
  306. package/src/prompts/system/custom-system-prompt.md +1 -23
  307. package/src/prompts/system/plan-mode-approved.md +8 -0
  308. package/src/prompts/system/system-prompt.md +17 -96
  309. package/src/prompts/tools/bash.md +4 -4
  310. package/src/prompts/tools/browser.md +15 -52
  311. package/src/prompts/tools/find.md +2 -2
  312. package/src/prompts/tools/hashline.md +13 -55
  313. package/src/prompts/tools/read.md +3 -6
  314. package/src/prompts/tools/search-tool-bm25.md +0 -2
  315. package/src/prompts/tools/search.md +2 -4
  316. package/src/prompts/tools/skill-discovery.md +1 -2
  317. package/src/prompts/tools/task.md +1 -1
  318. package/src/runtime-mcp/manager.ts +1 -1
  319. package/src/sdk/broker/broker.ts +206 -78
  320. package/src/sdk/broker/discovery.ts +1 -1
  321. package/src/sdk/broker/ensure.ts +3 -1
  322. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  323. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  324. package/src/sdk/broker/lifecycle.ts +1399 -142
  325. package/src/sdk/broker/process-incarnation.ts +5 -13
  326. package/src/sdk/broker/session-index.ts +145 -56
  327. package/src/sdk/broker/state-version.ts +21 -0
  328. package/src/sdk/broker/transport.ts +1 -1
  329. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  330. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  331. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  332. package/src/sdk/bus/config-commands.ts +5 -4
  333. package/src/sdk/bus/config.ts +98 -0
  334. package/src/sdk/bus/conversation-store.ts +5 -29
  335. package/src/sdk/bus/daemon-paths.ts +2 -0
  336. package/src/sdk/bus/html-format.ts +20 -3
  337. package/src/sdk/bus/index.ts +778 -93
  338. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  339. package/src/sdk/bus/natives-augment.d.ts +26 -1
  340. package/src/sdk/bus/notification-service.ts +168 -20
  341. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  342. package/src/sdk/bus/recent-activity.ts +188 -74
  343. package/src/sdk/bus/rich-document.ts +258 -0
  344. package/src/sdk/bus/rich-render.ts +62 -70
  345. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  346. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  347. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  348. package/src/sdk/bus/threaded-inbound.ts +8 -3
  349. package/src/sdk/bus/threaded-render.ts +57 -1
  350. package/src/sdk/bus/topic-registry.ts +120 -13
  351. package/src/sdk/host/control/dispatch.ts +14 -2
  352. package/src/sdk/host/control/operations.ts +6 -1
  353. package/src/sdk/host/host.ts +31 -1
  354. package/src/sdk/host/query/handlers.ts +20 -22
  355. package/src/sdk/host/query/revision-store.ts +80 -57
  356. package/src/sdk/index.ts +1 -0
  357. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  358. package/src/sdk/session-directory.ts +141 -0
  359. package/src/sdk/session.ts +458 -479
  360. package/src/session/agent-session.ts +1700 -583
  361. package/src/session/agent-storage.ts +18 -0
  362. package/src/session/cache-economics.ts +25 -8
  363. package/src/session/fallback-chain-controller.ts +53 -3
  364. package/src/session/internal/managed-session-scope.ts +2226 -0
  365. package/src/session/internal/managed-session-storage.ts +453 -0
  366. package/src/session/messages.ts +91 -2
  367. package/src/session/session-manager-internal.ts +33 -0
  368. package/src/session/session-manager.ts +1112 -333
  369. package/src/session/session-storage.ts +444 -39
  370. package/src/session/volatile-context-pruning.ts +78 -0
  371. package/src/setup/credential-auto-import.ts +2 -1
  372. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  373. package/src/setup/provider-onboarding.ts +36 -61
  374. package/src/setup/provider-presets.json +5 -0
  375. package/src/skill-state/active-state.ts +3 -13
  376. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  377. package/src/skill-state/workflow-state-contract.ts +6 -41
  378. package/src/slash-commands/builtin-registry.ts +46 -11
  379. package/src/system-prompt.ts +37 -44
  380. package/src/task/agents.ts +19 -1
  381. package/src/task/executor.ts +90 -6
  382. package/src/task/index.ts +36 -21
  383. package/src/thinking.ts +21 -6
  384. package/src/tool-discovery/tool-index.ts +2 -134
  385. package/src/tools/ask.ts +157 -21
  386. package/src/tools/ast-edit.ts +1 -1
  387. package/src/tools/bash.ts +38 -0
  388. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  389. package/src/tools/browser/tab-supervisor.ts +282 -36
  390. package/src/tools/computer.ts +10 -3
  391. package/src/tools/cron.ts +30 -16
  392. package/src/tools/eval.ts +1 -2
  393. package/src/tools/fetch.ts +11 -4
  394. package/src/tools/index.ts +76 -24
  395. package/src/tools/output-meta.ts +13 -5
  396. package/src/tools/read.ts +37 -1
  397. package/src/tools/resource-gc.ts +2 -2
  398. package/src/tools/search-tool-bm25.ts +11 -74
  399. package/src/utils/event-bus.ts +9 -4
  400. package/src/web/search/index.ts +3 -3
  401. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  402. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  403. package/src/discovery/plugin-dir-roots.ts +0 -28
  404. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -1,5 +1,6 @@
1
1
  import type { Stats } from "node:fs";
2
2
  import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
3
4
  import { isEnoent } from "@gajae-code/utils/fs-error";
4
5
 
5
6
  export interface FileLockOptions {
@@ -14,45 +15,93 @@ const DEFAULT_OPTIONS: Required<FileLockOptions> = {
14
15
  retryDelayMs: 100,
15
16
  };
16
17
 
17
- interface LockInfo {
18
- pid: number;
19
- timestamp: number;
18
+ type LockInfo = FileLockOwnerToken;
19
+
20
+ /**
21
+ * Returns the OS-provided process start timestamp for PID-reuse detection.
22
+ * `ps` is available on the supported Unix hosts (macOS and Linux), unlike
23
+ * Linux's `/proc/<pid>/stat` pseudo-file.
24
+ */
25
+ export function processStartTime(pid: number): string | null {
26
+ try {
27
+ const result = Bun.spawnSync(["ps", "-o", "lstart=", "-p", String(pid)], { stdout: "pipe", stderr: "ignore" });
28
+ if (result.exitCode !== 0) return null;
29
+ const startTime = new TextDecoder().decode(result.stdout).trim();
30
+ return startTime || null;
31
+ } catch {
32
+ return null;
33
+ }
20
34
  }
21
35
 
22
- function getLockPath(filePath: string): string {
23
- return `${filePath}.lock`;
36
+ let ownProcessStartTime: string | undefined;
37
+
38
+ function currentProcessStartTime(): string {
39
+ if (ownProcessStartTime === undefined) ownProcessStartTime = processStartTime(process.pid) ?? "unknown";
40
+ return ownProcessStartTime;
41
+ }
42
+
43
+ function cachedProcessStartTime(owner: FileLockOwnerToken, cache?: Map<string, string | null>): string | null {
44
+ if (!cache) return processStartTime(owner.pid);
45
+ const key = `${owner.pid}:${owner.start_time ?? ""}`;
46
+ const cached = cache.get(key);
47
+ if (cached !== undefined || cache.has(key)) return cached ?? null;
48
+ const startTime = processStartTime(owner.pid);
49
+ cache.set(key, startTime);
50
+ return startTime;
51
+ }
52
+
53
+ function ownerIsAlive(owner: FileLockOwnerToken, startTimeCache?: Map<string, string | null>): boolean {
54
+ if (ownerLiveness(owner.pid) !== "alive") return false;
55
+ if (!owner.start_time) return true;
56
+ const currentStartTime = cachedProcessStartTime(owner, startTimeCache);
57
+ return currentStartTime === null || currentStartTime === owner.start_time;
24
58
  }
25
59
 
26
- async function writeLockInfo(lockPath: string): Promise<LockInfo> {
27
- const info: LockInfo = { pid: process.pid, timestamp: Date.now() };
28
- await Bun.write(`${lockPath}/info`, JSON.stringify(info));
29
- return info;
60
+ function writeLockInfo(lockPath: string): Promise<LockInfo> {
61
+ const info: LockInfo = { pid: process.pid, start_time: currentProcessStartTime(), timestamp: Date.now() };
62
+ return Bun.write(`${lockPath}/info`, JSON.stringify(info)).then(() => info);
30
63
  }
31
64
 
32
65
  async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
66
+ let parsed: unknown;
33
67
  try {
34
- const content = await fs.readFile(`${lockPath}/info`, "utf-8");
35
- return JSON.parse(content) as LockInfo;
36
- } catch {
37
- return null;
68
+ parsed = JSON.parse(await fs.readFile(`${lockPath}/info`, "utf-8"));
69
+ } catch (error) {
70
+ if (isEnoent(error) || error instanceof SyntaxError) return null;
71
+ throw error;
38
72
  }
73
+
74
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
75
+ const { pid, start_time, timestamp } = parsed as Partial<LockInfo>;
76
+ if (
77
+ typeof pid !== "number" ||
78
+ !Number.isInteger(pid) ||
79
+ pid <= 0 ||
80
+ typeof timestamp !== "number" ||
81
+ !Number.isFinite(timestamp) ||
82
+ (start_time !== undefined && (typeof start_time !== "string" || !start_time))
83
+ )
84
+ return null;
85
+ return { pid, start_time, timestamp };
39
86
  }
40
87
 
41
88
  /** @internal */
42
- export async function readFileLockInfoForGc(lockDir: string): Promise<{ pid: number; timestamp: number } | null> {
43
- const info = await readLockInfo(lockDir);
44
- if (!info) return null;
45
- if (!Number.isFinite(info.pid) || info.pid <= 0) return null;
46
- if (!Number.isFinite(info.timestamp)) return null;
47
- return info;
89
+ export async function readFileLockInfoForGc(lockDir: string): Promise<FileLockOwnerToken | null> {
90
+ return await readLockInfo(lockDir);
48
91
  }
49
92
 
50
93
  /** Owner identity stamped into a `<file>.lock/info` record. */
51
94
  export interface FileLockOwnerToken {
52
95
  pid: number;
96
+ start_time?: string;
97
+
53
98
  timestamp: number;
54
99
  }
55
100
 
101
+ function getLockPath(filePath: string): string {
102
+ return `${filePath}.lock`;
103
+ }
104
+
56
105
  /** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
57
106
  export type FileLockGcRemoval = "removed" | "owner_changed" | "missing";
58
107
 
@@ -91,7 +140,11 @@ export async function removeFileLockDirForGc(
91
140
  ): Promise<FileLockGcRemoval> {
92
141
  const current = await readLockInfo(lockDir);
93
142
  if (!current) return "missing";
94
- if (current.pid !== expected.pid || current.timestamp !== expected.timestamp) {
143
+ if (
144
+ current.pid !== expected.pid ||
145
+ (expected.start_time !== undefined && current.start_time !== expected.start_time) ||
146
+ current.timestamp !== expected.timestamp
147
+ ) {
95
148
  return "owner_changed";
96
149
  }
97
150
  await fs.rm(lockDir, { recursive: true, force: true });
@@ -127,7 +180,11 @@ function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
127
180
  return a.dev === b.dev && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
128
181
  }
129
182
 
130
- async function staleLockSnapshot(lockPath: string, staleMs: number): Promise<LockStaleSnapshot> {
183
+ async function staleLockSnapshot(
184
+ lockPath: string,
185
+ staleMs: number,
186
+ startTimeCache?: Map<string, string | null>,
187
+ ): Promise<LockStaleSnapshot> {
131
188
  const info = await readLockInfo(lockPath);
132
189
  if (!info) {
133
190
  try {
@@ -143,10 +200,9 @@ async function staleLockSnapshot(lockPath: string, staleMs: number): Promise<Loc
143
200
  // Never reap a live owner by elapsed time: a long legitimate critical section must
144
201
  // not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
145
202
  // liveness is indeterminate do we fall back to the staleMs elapsed-time heuristic.
146
- const liveness = ownerLiveness(info.pid);
147
- if (liveness === "alive") return { stale: false };
148
- if (liveness === "dead" || Date.now() - info.timestamp > staleMs) {
149
- return { stale: true, owner: { pid: info.pid, timestamp: info.timestamp } };
203
+ if (ownerIsAlive(info, startTimeCache)) return { stale: false };
204
+ if (ownerLiveness(info.pid) === "dead" || Date.now() - info.timestamp > staleMs) {
205
+ return { stale: true, owner: info };
150
206
  }
151
207
  return { stale: false };
152
208
  }
@@ -171,6 +227,7 @@ async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSn
171
227
  }
172
228
 
173
229
  async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
230
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
174
231
  try {
175
232
  await fs.mkdir(lockPath);
176
233
  return await writeLockInfo(lockPath);
@@ -183,42 +240,47 @@ async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
183
240
  }
184
241
 
185
242
  async function releaseLock(lockPath: string, owner: FileLockOwnerToken): Promise<void> {
186
- try {
187
- await removeFileLockDirForGc(lockPath, owner);
188
- } catch {
189
- // Ignore errors on release
190
- }
243
+ const outcome = await removeFileLockDirForGc(lockPath, owner);
244
+ if (outcome !== "removed") throw new Error(`Failed to release file lock: ${outcome}.`);
191
245
  }
192
246
  async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
193
247
  const opts = { ...DEFAULT_OPTIONS, ...options };
194
248
  const lockPath = getLockPath(filePath);
195
-
249
+ const contentionStartTimes = new Map<string, string | null>();
196
250
  for (let attempt = 0; attempt < opts.retries; attempt++) {
197
251
  const owner = await tryAcquireLock(lockPath);
198
- if (owner) {
199
- return () => releaseLock(lockPath, owner);
200
- }
201
-
202
- const stale = await staleLockSnapshot(lockPath, opts.staleMs);
203
- if (await removeStaleLockForAcquire(lockPath, stale)) {
204
- continue;
205
- }
252
+ if (owner) return () => releaseLock(lockPath, owner);
206
253
 
254
+ const stale = await staleLockSnapshot(lockPath, opts.staleMs, contentionStartTimes);
255
+ if (await removeStaleLockForAcquire(lockPath, stale)) continue;
207
256
  await Bun.sleep(opts.retryDelayMs);
208
257
  }
209
-
210
258
  throw new Error(`Failed to acquire lock for ${filePath} after ${opts.retries} attempts`);
211
259
  }
212
260
 
261
+ /**
262
+ * Serializes all contenders, including callers in the same process. Because this
263
+ * API exposes no ownership token, recursive acquisition is indistinguishable
264
+ * from independent async contention; code that already holds the lock must pass
265
+ * that fact through its own `lockHeld` path instead of acquiring it again.
266
+ */
213
267
  export async function withFileLock<T>(
214
268
  filePath: string,
215
269
  fn: () => Promise<T>,
216
270
  options: FileLockOptions = {},
217
271
  ): Promise<T> {
218
272
  const release = await acquireLock(filePath, options);
273
+ let result: T;
219
274
  try {
220
- return await fn();
221
- } finally {
222
- await release();
275
+ result = await fn();
276
+ } catch (operationError) {
277
+ try {
278
+ await release();
279
+ } catch (releaseError) {
280
+ throw new AggregateError([operationError, releaseError], "File lock operation and release both failed.");
281
+ }
282
+ throw operationError;
223
283
  }
284
+ await release();
285
+ return result;
224
286
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import {
4
4
  type Keybinding,
@@ -41,6 +41,7 @@ interface AppKeybindings {
41
41
  "app.session.fork": true;
42
42
  "app.session.resume": true;
43
43
  "app.session.observe": true;
44
+ "app.session.dashboard": true;
44
45
  "app.jobs.open": true;
45
46
  "app.session.togglePath": true;
46
47
  "app.session.toggleSort": true;
@@ -53,6 +54,13 @@ interface AppKeybindings {
53
54
  "app.history.search": true;
54
55
  "app.stt.toggle": true;
55
56
  "app.irc.sidebar.toggle": true;
57
+ "app.transcript.browse": true;
58
+ "app.transcript.prevTurn": true;
59
+ "app.transcript.nextTurn": true;
60
+ "app.mode.cycle": true;
61
+ "app.tasks.toggle": true;
62
+ "app.queue.togglePane": true;
63
+ "app.message.sendNow": true;
56
64
  }
57
65
 
58
66
  export type AppKeybinding = keyof AppKeybindings;
@@ -65,6 +73,10 @@ export function defaultMessageQueueKeysForPlatform(platform: NodeJS.Platform = p
65
73
  return platform === "win32" || platform === "darwin" ? "alt+q" : "alt+enter";
66
74
  }
67
75
 
76
+ export function defaultClipboardPasteImageKeysForPlatform(platform: NodeJS.Platform = process.platform): KeyId {
77
+ return platform === "win32" ? "alt+v" : "ctrl+v";
78
+ }
79
+
68
80
  /**
69
81
  * All keybindings definitions: TUI + app-specific.
70
82
  */
@@ -139,7 +151,7 @@ export const KEYBINDINGS = {
139
151
  description: "Select queued message to edit",
140
152
  },
141
153
  "app.clipboard.pasteImage": {
142
- defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v",
154
+ defaultKeys: defaultClipboardPasteImageKeysForPlatform(),
143
155
  description: "Paste image from clipboard",
144
156
  },
145
157
  "app.clipboard.copyLine": {
@@ -170,6 +182,10 @@ export const KEYBINDINGS = {
170
182
  defaultKeys: "ctrl+s",
171
183
  description: "Observe subagent sessions",
172
184
  },
185
+ "app.session.dashboard": {
186
+ defaultKeys: [],
187
+ description: "Show all persisted sessions dashboard",
188
+ },
173
189
 
174
190
  "app.jobs.open": {
175
191
  defaultKeys: "alt+j",
@@ -219,6 +235,34 @@ export const KEYBINDINGS = {
219
235
  defaultKeys: "alt+i",
220
236
  description: "Toggle IRC sidebar",
221
237
  },
238
+ "app.transcript.browse": {
239
+ defaultKeys: [],
240
+ description: "Browse the transcript",
241
+ },
242
+ "app.transcript.prevTurn": {
243
+ defaultKeys: [],
244
+ description: "Jump to previous user turn",
245
+ },
246
+ "app.transcript.nextTurn": {
247
+ defaultKeys: [],
248
+ description: "Jump to next user turn",
249
+ },
250
+ "app.mode.cycle": {
251
+ defaultKeys: [],
252
+ description: "Cycle interaction mode",
253
+ },
254
+ "app.tasks.toggle": {
255
+ defaultKeys: "alt+t",
256
+ description: "Toggle tasks pane",
257
+ },
258
+ "app.queue.togglePane": {
259
+ defaultKeys: [],
260
+ description: "Toggle message queue pane",
261
+ },
262
+ "app.message.sendNow": {
263
+ defaultKeys: [],
264
+ description: "Send message immediately",
265
+ },
222
266
  } as const satisfies KeybindingDefinitions;
223
267
 
224
268
  /**
@@ -304,13 +348,14 @@ function toKeybindingsConfig(value: unknown): KeybindingsConfig {
304
348
 
305
349
  const config: KeybindingsConfig = {};
306
350
  for (const [key, val] of Object.entries(value)) {
307
- if (val === undefined) {
308
- config[key] = undefined;
309
- } else if (typeof val === "string") {
310
- config[key] = val as KeyId;
311
- } else if (Array.isArray(val) && val.every(v => typeof v === "string")) {
312
- config[key] = val as KeyId[];
351
+ if (!(key in KEYBINDINGS) && !isLegacyKeybindingName(key)) {
352
+ logger.info("Ignoring unknown keybinding entry", { key });
353
+ continue;
313
354
  }
355
+ if (val === undefined) config[key] = undefined;
356
+ else if (typeof val === "string") config[key] = val as KeyId;
357
+ else if (Array.isArray(val) && val.every(v => typeof v === "string")) config[key] = val as KeyId[];
358
+ else logger.info("Ignoring malformed keybinding entry", { key });
314
359
  }
315
360
  return config;
316
361
  }
@@ -381,28 +426,27 @@ function loadRawConfig(filePath: string): unknown {
381
426
  }
382
427
  }
383
428
 
384
- /**
385
- * Migrate keybindings config file from old format to new.
386
- * Reads from agentDir/keybindings.json, migrates old names, and writes back.
387
- */
429
+ /** Migrate legacy keybindings once, preserving the original file as .bak. */
388
430
  function loadKeybindingsConfig(filePath: string, writeBack: boolean): KeybindingsConfig {
389
431
  const rawConfig = loadRawConfig(filePath);
390
-
391
- if (rawConfig === null) {
392
- return {};
393
- }
394
-
395
- const { config: migratedConfig, migrated } = migrateKeybindingNames(rawConfig);
432
+ if (rawConfig === null) return {};
433
+ const markerPath = `${filePath}.migration-v1`;
434
+ const { config: migratedConfig, migrated } = existsSync(markerPath)
435
+ ? { config: toKeybindingsConfig(rawConfig), migrated: false }
436
+ : migrateKeybindingNames(rawConfig);
396
437
  if (writeBack && migrated) {
397
438
  const ordered = orderKeybindingsConfig(migratedConfig);
439
+ const tempPath = `${filePath}.${process.pid}.tmp`;
398
440
  try {
399
- writeFileSync(filePath, `${JSON.stringify(ordered, null, 2)}\n`, "utf-8");
441
+ copyFileSync(filePath, `${filePath}.bak`);
442
+ writeFileSync(tempPath, `${JSON.stringify(ordered, null, 2)}\n`, "utf-8");
443
+ renameSync(tempPath, filePath);
444
+ writeFileSync(markerPath, "v1\n", "utf-8");
400
445
  logger.debug("Migrated keybindings config", { path: filePath });
401
446
  } catch (error) {
402
447
  logger.warn("Failed to write migrated keybindings config", { path: filePath, error: String(error) });
403
448
  }
404
449
  }
405
-
406
450
  return migratedConfig;
407
451
  }
408
452
 
@@ -0,0 +1,9 @@
1
+ export const kNoAuth = "N/A";
2
+
3
+ export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
4
+ return Boolean(apiKey) && apiKey !== kNoAuth;
5
+ }
6
+
7
+ export function isAuthenticatedOrKeyless(apiKey: string | undefined | null): boolean {
8
+ return apiKey === kNoAuth || isAuthenticated(apiKey);
9
+ }
@@ -0,0 +1,139 @@
1
+ import { type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value";
2
+ import type { Settings } from "./settings";
3
+
4
+ export interface ConfiguredModelBindings {
5
+ modelRoles?: Record<string, ModelSelectorValue>;
6
+ agentModelOverrides?: Record<string, ModelSelectorValue>;
7
+ }
8
+
9
+ /** Synchronizes config-owned model bindings while preserving user edits. */
10
+ export class ModelBindingsApplier {
11
+ #targetSettings: Settings | undefined;
12
+ #bindings: ConfiguredModelBindings | undefined;
13
+ #appliedRoles = new Set<string>();
14
+ #appliedAgentOverrides = new Set<string>();
15
+ #roleBaselines = new Map<string, ModelSelectorValue | undefined>();
16
+ #agentBaselines = new Map<string, ModelSelectorValue | undefined>();
17
+ #lastAppliedRoles = new Map<string, ModelSelectorValue>();
18
+ #lastAppliedAgentOverrides = new Map<string, ModelSelectorValue>();
19
+
20
+ setBindings(bindings: ConfiguredModelBindings | undefined): void {
21
+ this.#bindings = bindings && {
22
+ modelRoles: this.#cloneBindings(bindings.modelRoles),
23
+ agentModelOverrides: this.#cloneBindings(bindings.agentModelOverrides),
24
+ };
25
+ }
26
+
27
+ applyTo(targetSettings: Settings): void {
28
+ if (this.#targetSettings && this.#targetSettings !== targetSettings) {
29
+ this.#restoreTarget(this.#targetSettings);
30
+ this.#clearTargetLifecycle();
31
+ }
32
+ this.#targetSettings = targetSettings;
33
+ this.apply();
34
+ }
35
+
36
+ apply(): void {
37
+ const targetSettings = this.#targetSettings;
38
+ if (!targetSettings) return;
39
+ const bindings = this.#bindings;
40
+ const nextModelRoles = { ...(targetSettings.get("modelRoles") ?? {}) };
41
+ this.#sync(
42
+ nextModelRoles,
43
+ bindings?.modelRoles ?? {},
44
+ this.#appliedRoles,
45
+ this.#roleBaselines,
46
+ this.#lastAppliedRoles,
47
+ );
48
+ targetSettings.override("modelRoles", nextModelRoles);
49
+
50
+ const nextAgentOverrides = { ...(targetSettings.get("task.agentModelOverrides") ?? {}) };
51
+ this.#sync(
52
+ nextAgentOverrides,
53
+ bindings?.agentModelOverrides ?? {},
54
+ this.#appliedAgentOverrides,
55
+ this.#agentBaselines,
56
+ this.#lastAppliedAgentOverrides,
57
+ );
58
+ targetSettings.override("task.agentModelOverrides", nextAgentOverrides);
59
+ }
60
+
61
+ #restoreTarget(targetSettings: Settings): void {
62
+ const modelRoles = { ...(targetSettings.get("modelRoles") ?? {}) };
63
+ this.#sync(modelRoles, {}, this.#appliedRoles, this.#roleBaselines, this.#lastAppliedRoles);
64
+ targetSettings.override("modelRoles", modelRoles);
65
+
66
+ const agentOverrides = { ...(targetSettings.get("task.agentModelOverrides") ?? {}) };
67
+ this.#sync(
68
+ agentOverrides,
69
+ {},
70
+ this.#appliedAgentOverrides,
71
+ this.#agentBaselines,
72
+ this.#lastAppliedAgentOverrides,
73
+ );
74
+ targetSettings.override("task.agentModelOverrides", agentOverrides);
75
+ }
76
+
77
+ #clearTargetLifecycle(): void {
78
+ this.#appliedRoles.clear();
79
+ this.#appliedAgentOverrides.clear();
80
+ this.#roleBaselines.clear();
81
+ this.#agentBaselines.clear();
82
+ this.#lastAppliedRoles.clear();
83
+ this.#lastAppliedAgentOverrides.clear();
84
+ }
85
+
86
+ #sync(
87
+ target: Record<string, ModelSelectorValue>,
88
+ configured: Record<string, ModelSelectorValue>,
89
+ applied: Set<string>,
90
+ baselines: Map<string, ModelSelectorValue | undefined>,
91
+ lastApplied: Map<string, ModelSelectorValue>,
92
+ ): void {
93
+ const configuredKeys = new Set(Object.keys(configured));
94
+ for (const key of applied) {
95
+ if (configuredKeys.has(key)) continue;
96
+ const previous = lastApplied.get(key);
97
+ if (previous !== undefined && this.#equal(target[key], previous)) {
98
+ const baseline = baselines.get(key);
99
+ if (baseline === undefined) delete target[key];
100
+ else target[key] = this.#clone(baseline)!;
101
+ }
102
+ baselines.delete(key);
103
+ lastApplied.delete(key);
104
+ }
105
+ for (const [key, value] of Object.entries(configured)) {
106
+ const previous = lastApplied.get(key);
107
+ if (!baselines.has(key)) baselines.set(key, this.#clone(target[key]));
108
+ if (previous === undefined || this.#equal(target[key], previous)) {
109
+ const appliedValue = this.#clone(value)!;
110
+ target[key] = appliedValue;
111
+ lastApplied.set(key, this.#clone(appliedValue)!);
112
+ }
113
+ }
114
+ applied.clear();
115
+ for (const key of configuredKeys) applied.add(key);
116
+ }
117
+
118
+ #cloneBindings(
119
+ bindings: Record<string, ModelSelectorValue> | undefined,
120
+ ): Record<string, ModelSelectorValue> | undefined {
121
+ if (!bindings) return undefined;
122
+ const copy: Record<string, ModelSelectorValue> = {};
123
+ for (const [key, value] of Object.entries(bindings)) copy[key] = this.#clone(value)!;
124
+ return copy;
125
+ }
126
+
127
+ #clone(value: ModelSelectorValue | undefined): ModelSelectorValue | undefined {
128
+ return Array.isArray(value) ? [...value] : value;
129
+ }
130
+
131
+ #equal(left: ModelSelectorValue | undefined, right: ModelSelectorValue | undefined): boolean {
132
+ const leftSelectors = normalizeModelSelectorValue(left);
133
+ const rightSelectors = normalizeModelSelectorValue(right);
134
+ return (
135
+ leftSelectors.length === rightSelectors.length &&
136
+ leftSelectors.every((value, index) => value === rightSelectors[index])
137
+ );
138
+ }
139
+ }