@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
@@ -4,19 +4,40 @@ import * as path from "node:path";
4
4
  import { YAML } from "bun";
5
5
  import { withFileLock } from "./file-lock";
6
6
 
7
+ export interface AtomicYamlExpectedPrecondition {
8
+ path: string;
9
+ hash: string;
10
+ }
11
+
7
12
  export interface AtomicYamlSetPatch {
8
13
  path: string;
9
14
  op: "set";
10
15
  value: unknown;
16
+ expected?: AtomicYamlExpectedPrecondition;
11
17
  }
12
18
 
13
19
  export interface AtomicYamlUnsetPatch {
14
20
  path: string;
15
21
  op: "unset";
22
+ expected?: AtomicYamlExpectedPrecondition;
16
23
  }
17
24
 
18
25
  export type AtomicYamlPatch = AtomicYamlSetPatch | AtomicYamlUnsetPatch;
19
26
 
27
+ /** Raised when a compare-and-swap precondition no longer matches durable YAML. */
28
+ export class AtomicYamlConflictError extends Error {
29
+ readonly code = "ATOMIC_YAML_CONFLICT";
30
+
31
+ constructor(
32
+ readonly path: string,
33
+ readonly expectedHash: string,
34
+ readonly actualHash: string,
35
+ ) {
36
+ super(`Atomic YAML precondition failed for ${path}.`);
37
+ this.name = "AtomicYamlConflictError";
38
+ }
39
+ }
40
+
20
41
  export interface AtomicYamlPatchRevision {
21
42
  path: string;
22
43
  beforeHash: string;
@@ -53,6 +74,8 @@ export interface AtomicYamlPatchOptions {
53
74
  sleep?: (ms: number) => Promise<void>;
54
75
  /** Test seam for Windows rename retry behavior. */
55
76
  platform?: NodeJS.Platform;
77
+ /** Called under the config lock before patches are applied. */
78
+ validateRoot?: (root: unknown, patches: readonly AtomicYamlPatch[]) => void | Promise<void>;
56
79
  /** Called under the config lock after a successful CAS restore. */
57
80
  onRestored?: (patches: readonly AtomicYamlPatch[]) => void | Promise<void>;
58
81
  }
@@ -98,6 +121,16 @@ function assertPatch(patch: AtomicYamlPatch): void {
98
121
  ) {
99
122
  throw new Error("Atomic YAML patches require a non-empty dotted path.");
100
123
  }
124
+ if (
125
+ patch.expected &&
126
+ (typeof patch.expected.path !== "string" ||
127
+ patch.expected.path.length === 0 ||
128
+ patch.expected.path.split(".").some(part => !part) ||
129
+ typeof patch.expected.hash !== "string" ||
130
+ patch.expected.hash.length === 0)
131
+ ) {
132
+ throw new Error("Atomic YAML patch preconditions require a non-empty dotted path and hash.");
133
+ }
101
134
  if (patch.op === "set") {
102
135
  if (patch.value === undefined) {
103
136
  throw new TypeError(`Atomic YAML set patch for ${patch.path} cannot carry undefined; use unset instead.`);
@@ -148,6 +181,12 @@ export function deleteByPath(value: Record<string, unknown>, segments: readonly
148
181
  }
149
182
 
150
183
  function stableValue(value: unknown): string {
184
+ if (typeof value === "number") {
185
+ if (Number.isNaN(value)) return "NaN";
186
+ if (value === Infinity) return "Infinity";
187
+ if (value === -Infinity) return "-Infinity";
188
+ if (Object.is(value, -0)) return "-0";
189
+ }
151
190
  if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
152
191
  if (Array.isArray(value)) return `[${value.map(stableValue).join(",")}]`;
153
192
  const object = value as Record<string, unknown>;
@@ -167,12 +206,22 @@ function cloneState(state: PathState): PathState {
167
206
  return state.exists ? { exists: true, value: structuredClone(state.value) } : state;
168
207
  }
169
208
 
170
- async function readYaml(configPath: string): Promise<Record<string, unknown>> {
209
+ /** Hash a dotted YAML path state for an expected-hash patch precondition. */
210
+ export function atomicYamlPathHash(value: Record<string, unknown>, path: string): string {
211
+ return stateHash(stateAtPath(value, path.split(".")));
212
+ }
213
+
214
+ type YamlReadResult = {
215
+ current: Record<string, unknown>;
216
+ root: unknown;
217
+ };
218
+
219
+ async function readYaml(configPath: string): Promise<YamlReadResult> {
171
220
  try {
172
- const parsed = YAML.parse(await fs.readFile(configPath, "utf8"));
173
- return record(parsed) ?? {};
221
+ const root = YAML.parse(await fs.readFile(configPath, "utf8"));
222
+ return { current: record(root) ?? {}, root };
174
223
  } catch (error) {
175
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
224
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { current: {}, root: undefined };
176
225
  throw error;
177
226
  }
178
227
  }
@@ -254,8 +303,9 @@ function createReceipt(
254
303
  async restore(): Promise<CasRestoreResult> {
255
304
  if (discarded) return { status: "discarded" };
256
305
  return await enqueueAtomicYamlOperation(configPath, async canonicalPath => {
306
+ if (discarded) return { status: "discarded" };
257
307
  return await withFileLock(canonicalPath, async () => {
258
- const current = await readYaml(canonicalPath);
308
+ const { current, root } = await readYaml(canonicalPath);
259
309
  const conflicts = changes
260
310
  .filter(
261
311
  change =>
@@ -269,6 +319,7 @@ function createReceipt(
269
319
  ? { path: change.path, op: "set", value: structuredClone(change.before.value) }
270
320
  : { path: change.path, op: "unset" },
271
321
  );
322
+ await options.validateRoot?.(root, restorePatches);
272
323
  const receipt = await applyPatchesUnderLock(canonicalPath, current, restorePatches, options);
273
324
  await options.onRestored?.(restorePatches);
274
325
  return { status: "restored", receipt };
@@ -286,6 +337,14 @@ async function applyPatchesUnderLock(
286
337
  ): Promise<CasReceipt> {
287
338
  if (patches.length === 0) return createReceipt(configPath, [], options);
288
339
 
340
+ for (const patch of patches) {
341
+ if (!patch.expected) continue;
342
+ const actualHash = stateHash(stateAtPath(current, patch.expected.path.split(".")));
343
+ if (actualHash !== patch.expected.hash) {
344
+ throw new AtomicYamlConflictError(patch.expected.path, patch.expected.hash, actualHash);
345
+ }
346
+ }
347
+
289
348
  const changesByPath = new Map<string, ReceiptChange>();
290
349
  for (const patch of patches) {
291
350
  const segments = patch.path.split(".");
@@ -329,9 +388,10 @@ export function applyAtomicYamlPatchesWithCurrent(
329
388
  return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
330
389
  await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
331
390
  return await withFileLock(canonicalPath, async () => {
332
- const current = await readYaml(canonicalPath);
391
+ const { current, root } = await readYaml(canonicalPath);
333
392
  const patches = await buildPatches(current);
334
393
  for (const patch of patches) assertPatch(patch);
394
+ await options.validateRoot?.(root, patches);
335
395
  return await applyPatchesUnderLock(canonicalPath, current, patches, options);
336
396
  });
337
397
  });
@@ -374,7 +434,8 @@ export function reserveAtomicYamlPatchSlot(
374
434
  for (const patch of nextPatches) assertPatch(patch);
375
435
  await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
376
436
  return await withFileLock(canonicalPath, async () => {
377
- const current = await readYaml(canonicalPath);
437
+ const { current, root } = await readYaml(canonicalPath);
438
+ await options.validateRoot?.(root, nextPatches);
378
439
  return await applyPatchesUnderLock(canonicalPath, current, nextPatches, options);
379
440
  });
380
441
  });
@@ -393,7 +454,7 @@ export function reserveAtomicYamlUpdateSlot<T>(
393
454
  const atomicUpdate = await update();
394
455
  await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
395
456
  return await withFileLock(canonicalPath, async () => {
396
- const current = await readYaml(canonicalPath);
457
+ const { current } = await readYaml(canonicalPath);
397
458
  const result = await atomicUpdate.apply(current);
398
459
  if (atomicUpdate.shouldWrite?.(result) !== false) {
399
460
  await writeAtomicYaml(canonicalPath, current, options);
@@ -414,10 +475,16 @@ export function applyAtomicYamlPatches(
414
475
  options: AtomicYamlPatchOptions = {},
415
476
  ): Promise<CasReceipt> {
416
477
  for (const patch of patches) assertPatch(patch);
417
- const immutablePatches = patches.map(patch =>
418
- patch.op === "set"
419
- ? ({ path: patch.path, op: "set", value: structuredClone(patch.value) } as const)
420
- : ({ path: patch.path, op: "unset" } as const),
421
- );
478
+ const immutablePatches = patches.map(patch => {
479
+ const expected = patch.expected ? { ...patch.expected } : undefined;
480
+ return patch.op === "set"
481
+ ? ({
482
+ path: patch.path,
483
+ op: "set",
484
+ value: structuredClone(patch.value),
485
+ ...(expected ? { expected } : {}),
486
+ } as const)
487
+ : ({ path: patch.path, op: "unset", ...(expected ? { expected } : {}) } as const);
488
+ });
422
489
  return reserveAtomicYamlPatchSlot(configPath, () => immutablePatches, options);
423
490
  }
@@ -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
+ }