@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
@@ -9,6 +9,8 @@
9
9
  import { createHash, randomBytes } from "node:crypto";
10
10
  import * as fs from "node:fs/promises";
11
11
  import * as path from "node:path";
12
+ import { withFileLock } from "../config/file-lock";
13
+
12
14
  import { controlSocketPath, sessionPaths } from "./storage";
13
15
 
14
16
  export interface SessionLease {
@@ -51,21 +53,8 @@ async function writeLeaseAtomic(file: string, lease: SessionLease): Promise<void
51
53
  await fs.writeFile(tmp, `${JSON.stringify(lease, null, 2)}\n`, "utf8");
52
54
  await fs.rename(tmp, file);
53
55
  }
54
- const LEASE_LOCK_RETRIES = 100;
55
- const LEASE_LOCK_RETRY_DELAY_MS = 20;
56
- const LEASE_LOCK_FILE_SUFFIX = ".json";
57
-
58
- interface LeaseLockInfo {
59
- pid: number;
60
- token: string;
61
- }
62
-
63
56
  function leaseMutationLockPath(root: string, sessionId: string): string {
64
- return `${sessionPaths(root, sessionId).lease}.lock`;
65
- }
66
-
67
- function leaseMutationLockFile(lockPath: string, token: string): string {
68
- return path.join(lockPath, `${token}${LEASE_LOCK_FILE_SUFFIX}`);
57
+ return sessionPaths(root, sessionId).lease;
69
58
  }
70
59
 
71
60
  function isOwnedUnixEndpointPath(root: string, sessionId: string, endpointPath: string, fallbackPath: string): boolean {
@@ -77,128 +66,21 @@ function isOwnedUnixEndpointPath(root: string, sessionId: string, endpointPath:
77
66
  return false;
78
67
  }
79
68
  }
80
-
81
- function isPathExistsError(error: unknown): boolean {
82
- const code = (error as NodeJS.ErrnoException).code;
83
- return code === "EEXIST" || code === "ENOTEMPTY" || code === "EISDIR" || code === "ENOTDIR";
84
- }
85
-
86
- function isIgnorableLockDirRemoveError(error: unknown): boolean {
87
- const code = (error as NodeJS.ErrnoException).code;
88
- return code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST";
89
- }
90
-
91
- async function removeEmptyLockDir(lockPath: string): Promise<void> {
92
- try {
93
- await fs.rmdir(lockPath);
94
- } catch (error) {
95
- if (!isIgnorableLockDirRemoveError(error)) throw error;
96
- }
97
- }
98
-
99
- function parseLeaseLockInfo(raw: string): LeaseLockInfo | null {
100
- try {
101
- const parsed = JSON.parse(raw) as unknown;
102
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
103
- const info = parsed as Record<string, unknown>;
104
- return typeof info.pid === "number" && typeof info.token === "string"
105
- ? { pid: info.pid, token: info.token }
106
- : null;
107
- } catch {
108
- return null;
109
- }
110
- }
111
-
112
- async function readLeaseLockInfo(lockFile: string): Promise<LeaseLockInfo | null> {
113
- try {
114
- return parseLeaseLockInfo(await fs.readFile(lockFile, "utf8"));
115
- } catch {
116
- return null;
117
- }
118
- }
119
-
120
- async function createLeaseMutationLock(lockPath: string, info: LeaseLockInfo): Promise<boolean> {
121
- const tmpPath = `${lockPath}.tmp-${info.token}`;
122
- await fs.rm(tmpPath, { recursive: true, force: true });
123
- await fs.mkdir(tmpPath, { mode: 0o700 });
69
+ async function withLeaseMutationLock<T>(root: string, sessionId: string, fn: () => Promise<T>): Promise<T> {
70
+ const lockPath = leaseMutationLockPath(root, sessionId);
71
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
124
72
  try {
125
- await fs.writeFile(leaseMutationLockFile(tmpPath, info.token), `${JSON.stringify(info)}\n`, {
126
- encoding: "utf8",
127
- mode: 0o600,
73
+ return await withFileLock(lockPath, fn, {
74
+ staleMs: 30_000,
75
+ retries: 100,
76
+ retryDelayMs: 20,
128
77
  });
129
- await fs.rename(tmpPath, lockPath);
130
- return true;
131
- } catch (error) {
132
- await fs.rm(tmpPath, { recursive: true, force: true });
133
- if (isPathExistsError(error)) return false;
134
- throw error;
135
- }
136
- }
137
-
138
- async function recoverLegacyStaleLeaseLock(lockPath: string): Promise<void> {
139
- const info = await readLeaseLockInfo(lockPath);
140
- if (!info || defaultPidStatusProbe(info.pid) !== "dead") return;
141
- try {
142
- await fs.unlink(lockPath);
143
78
  } catch (error) {
144
- const code = (error as NodeJS.ErrnoException).code;
145
- if (code !== "ENOENT" && code !== "EISDIR") throw error;
146
- }
147
- }
148
-
149
- async function recoverStaleLeaseMutationLock(lockPath: string): Promise<void> {
150
- let entries: string[];
151
- try {
152
- entries = await fs.readdir(lockPath);
153
- } catch (error) {
154
- const code = (error as NodeJS.ErrnoException).code;
155
- if (code === "ENOENT") return;
156
- if (code === "ENOTDIR") {
157
- await recoverLegacyStaleLeaseLock(lockPath);
158
- return;
79
+ if (error instanceof Error && error.message.startsWith("Failed to acquire lock")) {
80
+ throw new LeaseError(`lease_lock_timeout:${sessionId}`, "lease_lock_timeout");
159
81
  }
160
82
  throw error;
161
83
  }
162
- const lockFiles = entries.filter(entry => entry.endsWith(LEASE_LOCK_FILE_SUFFIX));
163
- if (lockFiles.length === 0) {
164
- await removeEmptyLockDir(lockPath);
165
- return;
166
- }
167
- if (lockFiles.length !== 1) return;
168
- const lockFile = lockFiles[0];
169
- const info = await readLeaseLockInfo(path.join(lockPath, lockFile));
170
- if (!info || lockFile !== `${info.token}${LEASE_LOCK_FILE_SUFFIX}`) return;
171
- if (defaultPidStatusProbe(info.pid) !== "dead") return;
172
- // Delete only the stale owner's token-named file; a fresh lock uses a different file name.
173
- await fs.rm(path.join(lockPath, lockFile), { force: true });
174
- await removeEmptyLockDir(lockPath);
175
- }
176
-
177
- async function releaseLeaseMutationLock(lockPath: string, token: string): Promise<void> {
178
- await fs.rm(leaseMutationLockFile(lockPath, token), { force: true });
179
- await removeEmptyLockDir(lockPath);
180
- }
181
-
182
- async function acquireLeaseMutationLock(root: string, sessionId: string): Promise<() => Promise<void>> {
183
- const lockPath = leaseMutationLockPath(root, sessionId);
184
- await fs.mkdir(path.dirname(lockPath), { recursive: true });
185
- for (let attempt = 0; attempt < LEASE_LOCK_RETRIES; attempt++) {
186
- const token = randomBytes(16).toString("hex");
187
- const info: LeaseLockInfo = { pid: process.pid, token };
188
- if (await createLeaseMutationLock(lockPath, info)) return () => releaseLeaseMutationLock(lockPath, token);
189
- await recoverStaleLeaseMutationLock(lockPath);
190
- await Bun.sleep(LEASE_LOCK_RETRY_DELAY_MS);
191
- }
192
- throw new LeaseError(`lease_lock_timeout:${sessionId}`, "lease_lock_timeout");
193
- }
194
-
195
- async function withLeaseMutationLock<T>(root: string, sessionId: string, fn: () => Promise<T>): Promise<T> {
196
- const release = await acquireLeaseMutationLock(root, sessionId);
197
- try {
198
- return await fn();
199
- } finally {
200
- await release();
201
- }
202
84
  }
203
85
 
204
86
  export async function readLease(root: string, sessionId: string): Promise<SessionLease | null> {
@@ -48,23 +48,23 @@ export const hindsightBackend: MemoryBackend = {
48
48
  if (options.taskDepth > 0) {
49
49
  const parent = options.parentHindsightSessionState;
50
50
  if (!parent) return;
51
- const previous = session.setHindsightSessionState(
52
- new HindsightSessionState({
53
- sessionId,
54
- client: parent.client,
55
- bankId: parent.bankId,
56
- retainTags: parent.retainTags,
57
- recallTags: parent.recallTags,
58
- recallTagsMatch: parent.recallTagsMatch,
59
- config: parent.config,
60
- session,
61
- missionsSet: parent.missionsSet,
62
- lastRetainedTurn: 0,
63
- hasRecalledForFirstTurn: true,
64
- aliasOf: parent,
65
- }),
66
- );
67
- previous?.dispose();
51
+ const replacement = new HindsightSessionState({
52
+ sessionId,
53
+ client: parent.client,
54
+ bankId: parent.bankId,
55
+ retainTags: parent.retainTags,
56
+ recallTags: parent.recallTags,
57
+ recallTagsMatch: parent.recallTagsMatch,
58
+ config: parent.config,
59
+ session,
60
+ missionsSet: parent.missionsSet,
61
+ lastRetainedTurn: 0,
62
+ hasRecalledForFirstTurn: true,
63
+ aliasOf: parent,
64
+ });
65
+ const previous = session.getHindsightSessionState();
66
+ await previous?.dispose();
67
+ session.setHindsightSessionState(replacement);
68
68
  return;
69
69
  }
70
70
 
@@ -91,10 +91,12 @@ export const hindsightBackend: MemoryBackend = {
91
91
  hasRecalledForFirstTurn: false,
92
92
  });
93
93
 
94
- // Cleanup any stale state for this session (defensive — prevents leaks
95
- // when a session is reused without going through dispose).
96
- const previous = session.setHindsightSessionState(state);
97
- previous?.dispose();
94
+ // Close and drain the old queue while it still owns this session. Closing
95
+ // rejects concurrent enqueues, so no retain can land between the final flush
96
+ // and replacement.
97
+ const previous = session.getHindsightSessionState();
98
+ await previous?.dispose();
99
+ session.setHindsightSessionState(state);
98
100
  state.attachSessionListeners();
99
101
 
100
102
  // Kick off mental-model bootstrap. Resolves asynchronously; the first
@@ -114,15 +116,12 @@ export const hindsightBackend: MemoryBackend = {
114
116
 
115
117
  const state = session?.getHindsightSessionState();
116
118
  const primary = state?.aliasOf ?? state;
117
- const recallSnippet = primary?.lastRecallSnippet;
118
119
  const mentalModelsSnippet = primary?.mentalModelsSnippet;
119
120
 
120
- // Order: static instructions → mental models (stable, curated) → recall
121
- // (volatile per turn). Stable context first so the LLM's prior is
122
- // anchored on curated knowledge.
121
+ // Static instructions and curated mental models are prefix-stable. Recall
122
+ // is injected by AgentSession as volatile user-role context instead.
123
123
  const parts = [STATIC_INSTRUCTIONS];
124
124
  if (mentalModelsSnippet) parts.push(mentalModelsSnippet);
125
- if (recallSnippet) parts.push(recallSnippet);
126
125
  return parts.join("\n\n");
127
126
  },
128
127
 
@@ -141,7 +140,7 @@ export const hindsightBackend: MemoryBackend = {
141
140
  const state = session?.getHindsightSessionState();
142
141
  if (state) await state.flushRetainQueue();
143
142
  const previous = session?.setHindsightSessionState(undefined);
144
- previous?.dispose();
143
+ await previous?.dispose();
145
144
  logger.warn(
146
145
  "Hindsight memory is server-side; only the local recall cache was cleared. " +
147
146
  "Delete the Hindsight bank from the UI to wipe upstream state.",
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  import { logger } from "@gajae-code/utils";
2
4
  import type { AgentSession } from "../session/agent-session";
3
5
  import { type BankScope, ensureBankMission } from "./bank";
@@ -5,7 +7,6 @@ import type { HindsightApi, MemoryItemInput } from "./client";
5
7
  import type { HindsightConfig } from "./config";
6
8
  import {
7
9
  composeRecallQuery,
8
- formatCurrentTime,
9
10
  formatMemories,
10
11
  type HindsightMessage,
11
12
  prepareRetentionTranscript,
@@ -105,33 +106,31 @@ export class HindsightRetainQueue {
105
106
  this.#timer = undefined;
106
107
  }
107
108
 
108
- if (this.#flushing) {
109
- // Coalesce: wait for the in-flight flush, then drain anything that
110
- // landed after it started so we don't strand items.
111
- await this.#flushing;
112
- if (this.#items.length > 0) await this.flush();
113
- return;
114
- }
115
-
109
+ if (this.#flushing) return this.#flushing;
116
110
  if (this.#items.length === 0) return;
117
111
 
118
- const items = this.#items.splice(0);
119
- const flushPromise = this.#doFlush(items);
120
- this.#flushing = flushPromise;
112
+ this.#flushing = this.#flushLoop();
121
113
  try {
122
- await flushPromise;
114
+ await this.#flushing;
123
115
  } finally {
124
116
  this.#flushing = undefined;
125
117
  }
126
118
  }
127
119
 
128
- dispose(): void {
120
+ async #flushLoop(): Promise<void> {
121
+ while (this.#items.length > 0) {
122
+ const items = this.#items.splice(0);
123
+ await this.#doFlush(items);
124
+ }
125
+ }
126
+
127
+ async dispose(): Promise<void> {
129
128
  this.#closed = true;
130
129
  if (this.#timer) {
131
130
  clearTimeout(this.#timer);
132
131
  this.#timer = undefined;
133
132
  }
134
- this.#items = [];
133
+ await this.flush();
135
134
  }
136
135
 
137
136
  async #doFlush(items: PendingRetainItem[]): Promise<void> {
@@ -202,6 +201,7 @@ export class HindsightSessionState {
202
201
  lastRetainedTurn: number;
203
202
  hasRecalledForFirstTurn: boolean;
204
203
  lastRecallSnippet?: string;
204
+ lastInjectedRecallSnippetHash?: string;
205
205
  /** Cached `<mental_models>` block injected into developer instructions. */
206
206
  mentalModelsSnippet?: string;
207
207
  /** When the cached snippet was last refreshed; gates the agent_end re-list. */
@@ -215,6 +215,9 @@ export class HindsightSessionState {
215
215
  unsubscribe?: () => void;
216
216
  /** Alias states delegate persistence config to a primary parent state. */
217
217
  aliasOf?: HindsightSessionState;
218
+ /** One auto-retain may run at a time; one later agent_end is coalesced. */
219
+ #autoRetainInFlight?: Promise<void>;
220
+ #autoRetainPending = false;
218
221
  readonly retainQueue: HindsightRetainQueue;
219
222
 
220
223
  constructor(options: HindsightSessionStateOptions) {
@@ -241,6 +244,30 @@ export class HindsightSessionState {
241
244
  this.lastRetainedTurn = 0;
242
245
  this.hasRecalledForFirstTurn = false;
243
246
  this.lastRecallSnippet = undefined;
247
+ this.lastInjectedRecallSnippetHash = undefined;
248
+ }
249
+
250
+ /** Return the current recall payload, resolving subagent aliases to their primary state. */
251
+ getRecallSnippet(): string | undefined {
252
+ return this.aliasOf ? this.aliasOf.getRecallSnippet() : this.lastRecallSnippet;
253
+ }
254
+ /** Return the current recall payload without consuming its one-shot injection eligibility. */
255
+ getRecallSnippetForInjection(): string | undefined {
256
+ if (this.aliasOf) return this.aliasOf.getRecallSnippetForInjection();
257
+ const snippet = this.lastRecallSnippet;
258
+ if (!snippet) return undefined;
259
+ const hash = createHash("sha256").update(snippet).digest("hex");
260
+ return hash === this.lastInjectedRecallSnippetHash ? undefined : snippet;
261
+ }
262
+
263
+ /** Mark a recall payload consumed only after it has been accepted for provider injection. */
264
+ markRecallSnippetInjected(snippet: string): boolean {
265
+ if (this.aliasOf) return this.aliasOf.markRecallSnippetInjected(snippet);
266
+ if (snippet !== this.lastRecallSnippet) return false;
267
+ const hash = createHash("sha256").update(snippet).digest("hex");
268
+ if (hash === this.lastInjectedRecallSnippetHash) return false;
269
+ this.lastInjectedRecallSnippetHash = hash;
270
+ return true;
244
271
  }
245
272
 
246
273
  enqueueRetain(content: string, context?: string): void {
@@ -264,7 +291,7 @@ export class HindsightSessionState {
264
291
  const results = response.results ?? [];
265
292
  if (results.length === 0) return { context: null, ok: true };
266
293
  const formatted = formatMemories(results);
267
- const block = `<memories>\n${this.config.recallPromptPreamble}\nCurrent time: ${formatCurrentTime()} UTC\n\n${formatted}\n</memories>`;
294
+ const block = `<memories>\n${this.config.recallPromptPreamble}\n\n${formatted}\n</memories>`;
268
295
  return { context: block, ok: true };
269
296
  } catch (err) {
270
297
  if (this.config.debug) {
@@ -274,13 +301,14 @@ export class HindsightSessionState {
274
301
  }
275
302
  }
276
303
 
277
- async retainSession(messages: HindsightMessage[]): Promise<void> {
304
+ async retainSession(messages: HindsightMessage[]): Promise<boolean> {
278
305
  const retainFullWindow = this.config.retainMode === "full-session";
279
306
  let target: HindsightMessage[];
280
307
  let documentId: string;
281
308
 
282
309
  if (retainFullWindow) {
283
- target = messages;
310
+ target = sliceMessagesAfterUserTurns(messages, this.lastRetainedTurn);
311
+ if (target.length === 0) return false;
284
312
  documentId = this.sessionId;
285
313
  } else {
286
314
  const windowTurns = this.config.retainEveryNTurns + this.config.retainOverlapTurns;
@@ -289,19 +317,40 @@ export class HindsightSessionState {
289
317
  }
290
318
 
291
319
  const { transcript } = prepareRetentionTranscript(target, true);
292
- if (!transcript) return;
320
+ if (!transcript) return false;
293
321
 
294
322
  await ensureBankMission(this.client, this.bankId, this.config, this.missionsSet);
295
323
  await this.client.retain(this.bankId, transcript, {
296
324
  documentId,
325
+ updateMode: "append",
297
326
  context: this.config.retainContext,
298
327
  metadata: { session_id: this.sessionId },
299
328
  tags: this.retainTags,
300
329
  async: true,
301
330
  });
331
+ return true;
302
332
  }
303
333
 
304
334
  async maybeRetainOnAgentEnd(): Promise<void> {
335
+ if (this.#autoRetainInFlight) {
336
+ this.#autoRetainPending = true;
337
+ return;
338
+ }
339
+
340
+ const retain = this.#maybeRetainOnAgentEnd();
341
+ this.#autoRetainInFlight = retain;
342
+ try {
343
+ await retain;
344
+ } finally {
345
+ this.#autoRetainInFlight = undefined;
346
+ if (this.#autoRetainPending) {
347
+ this.#autoRetainPending = false;
348
+ void this.maybeRetainOnAgentEnd();
349
+ }
350
+ }
351
+ }
352
+
353
+ async #maybeRetainOnAgentEnd(): Promise<void> {
305
354
  if (!this.config.autoRetain) return;
306
355
  const messages = extractMessages(this.session.sessionManager);
307
356
  if (messages.length === 0) return;
@@ -309,7 +358,8 @@ export class HindsightSessionState {
309
358
  if (userTurns - this.lastRetainedTurn < this.config.retainEveryNTurns) return;
310
359
 
311
360
  try {
312
- await this.retainSession(messages);
361
+ const retained = await this.retainSession(messages);
362
+ if (!retained) return;
313
363
  this.lastRetainedTurn = userTurns;
314
364
  if (this.config.debug) {
315
365
  logger.debug("Hindsight: auto-retain succeeded", {
@@ -332,7 +382,8 @@ export class HindsightSessionState {
332
382
  const messages = extractMessages(this.session.sessionManager);
333
383
  if (messages.length === 0) return;
334
384
  try {
335
- await this.retainSession(messages);
385
+ const retained = await this.retainSession(messages);
386
+ if (!retained) return;
336
387
  this.lastRetainedTurn = messages.filter(m => m.role === "user").length;
337
388
  } catch (err) {
338
389
  logger.warn("Hindsight: forced retain failed", {
@@ -358,7 +409,6 @@ export class HindsightSessionState {
358
409
  if (!context) return;
359
410
 
360
411
  this.lastRecallSnippet = context;
361
- await this.#refreshBaseSystemPromptAfter("recall");
362
412
  }
363
413
 
364
414
  async beforeAgentStartPrompt(promptText: string): Promise<string | undefined> {
@@ -409,21 +459,23 @@ export class HindsightSessionState {
409
459
  }
410
460
  }
411
461
 
412
- await this.refreshMentalModelsSnippet();
413
- await this.#refreshBaseSystemPromptAfter("MM load");
462
+ const changed = await this.refreshMentalModelsSnippet();
463
+ if (changed) await this.#refreshBaseSystemPromptAfter("MM load");
414
464
  }
415
465
 
416
- async refreshMentalModelsSnippet(): Promise<void> {
466
+ async refreshMentalModelsSnippet(): Promise<boolean> {
417
467
  const snippet = await loadMentalModelsBlock(this.client, this.bankId, this.config.mentalModelMaxRenderChars);
468
+ const changed = contentHash(snippet) !== contentHash(this.mentalModelsSnippet);
418
469
  this.mentalModelsSnippet = snippet;
419
470
  this.mentalModelsLoadedAt = Date.now();
471
+ return changed;
420
472
  }
421
473
 
422
474
  async reloadMentalModels(): Promise<boolean> {
423
475
  if (this.aliasOf) return false;
424
476
  if (!this.config.mentalModelsEnabled) return false;
425
- await this.refreshMentalModelsSnippet();
426
- await this.#refreshBaseSystemPromptAfter("MM reload");
477
+ const changed = await this.refreshMentalModelsSnippet();
478
+ if (changed) await this.#refreshBaseSystemPromptAfter("MM reload");
427
479
  return true;
428
480
  }
429
481
 
@@ -445,21 +497,21 @@ export class HindsightSessionState {
445
497
  this.mentalModelsLoadedAt !== undefined &&
446
498
  Date.now() - this.mentalModelsLoadedAt >= this.config.mentalModelRefreshIntervalMs
447
499
  ) {
448
- void this.refreshMentalModelsSnippet().then(async () => {
449
- await this.#refreshBaseSystemPromptAfter("MM TTL reload");
500
+ void this.refreshMentalModelsSnippet().then(async changed => {
501
+ if (changed) await this.#refreshBaseSystemPromptAfter("MM TTL reload");
450
502
  });
451
503
  }
452
504
  }
453
505
  });
454
506
  }
455
507
 
456
- dispose(): void {
508
+ async dispose(): Promise<void> {
457
509
  this.unsubscribe?.();
458
510
  this.unsubscribe = undefined;
459
- this.retainQueue.dispose();
511
+ await this.retainQueue.dispose();
460
512
  }
461
513
 
462
- async #refreshBaseSystemPromptAfter(reason: "recall" | "MM load" | "MM reload" | "MM TTL reload"): Promise<void> {
514
+ async #refreshBaseSystemPromptAfter(reason: "MM load" | "MM reload" | "MM TTL reload"): Promise<void> {
463
515
  try {
464
516
  await this.session.refreshBaseSystemPrompt();
465
517
  } catch (err) {
@@ -467,3 +519,20 @@ export class HindsightSessionState {
467
519
  }
468
520
  }
469
521
  }
522
+
523
+ /** Return the tail beginning with the first user turn after `retainedTurns`. */
524
+ function sliceMessagesAfterUserTurns(messages: HindsightMessage[], retainedTurns: number): HindsightMessage[] {
525
+ if (retainedTurns <= 0) return messages;
526
+
527
+ let seenTurns = 0;
528
+ for (let i = 0; i < messages.length; i++) {
529
+ if (messages[i].role !== "user") continue;
530
+ seenTurns += 1;
531
+ if (seenTurns > retainedTurns) return messages.slice(i);
532
+ }
533
+ return [];
534
+ }
535
+
536
+ function contentHash(content: string | undefined): string {
537
+ return Bun.hash(content ?? "").toString(16);
538
+ }
@@ -12,6 +12,7 @@ import {
12
12
  writeGuardedWorkflowEnvelopeAtomic,
13
13
  } from "../gjc-runtime/state-writer";
14
14
  import { isUltragoalBypassPrompt, verifyUltragoalDurableCompletionState } from "../gjc-runtime/ultragoal-guard";
15
+ import { getSkillManifest } from "../gjc-runtime/workflow-manifest";
15
16
  import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
16
17
  import {
17
18
  readVisibleSkillActiveState as readCanonicalVisibleSkillActiveState,
@@ -20,6 +21,7 @@ import {
20
21
  syncSkillActiveState,
21
22
  } from "../skill-state/active-state";
22
23
  import { initialPhaseForSkill } from "../skill-state/initial-phase";
24
+ import { readWorkflowGuardContext } from "../skill-state/workflow-mutation-guard";
23
25
 
24
26
  // Re-export for existing callers and tests that imported it from this module.
25
27
  export { initialPhaseForSkill };
@@ -531,7 +533,6 @@ function isTerminalModeState(state: ModeState | null): boolean {
531
533
  * declared it is ready to chain but has not yet been demoted/cleared, so it
532
534
  * must keep blocking until the chain (or an explicit clear) removes it.
533
535
  */
534
- const STOP_RELEASING_PHASES = ["complete", "completed", "failed", "cancelled", "canceled", "inactive"] as const;
535
536
 
536
537
  /**
537
538
  * Handoff workflows must never stop silently — they always have to offer the
@@ -553,13 +554,13 @@ function isHandoffRequiredSkill(skill: GjcWorkflowSkill): boolean {
553
554
  * mode-state preserves the historical fail-open behavior so a broken state file
554
555
  * cannot lock a session.
555
556
  */
556
- function modeStateReleasesStop(state: ModeState | null, handoffRequired: boolean): boolean {
557
+ function modeStateReleasesStop(state: ModeState | null, handoffRequired: boolean, skill: GjcWorkflowSkill): boolean {
557
558
  if (!state) return !handoffRequired;
558
559
  if (state.active !== true) return true;
559
560
  const phase = String(state.current_phase ?? "")
560
561
  .trim()
561
562
  .toLowerCase();
562
- if ((STOP_RELEASING_PHASES as readonly string[]).includes(phase)) return true;
563
+ if (getSkillManifest(skill).stopReleasingPhases.includes(phase)) return true;
563
564
  if (!handoffRequired && phase === "handoff") return true;
564
565
  return false;
565
566
  }
@@ -743,12 +744,21 @@ function buildHandoffModeStateRecoveryMessage(skill: GjcWorkflowSkill, phase: st
743
744
  }
744
745
 
745
746
  function buildHandoffForceAskMessage(skill: GjcWorkflowSkill, phase: string, statePath: string): string {
747
+ if (skill === "deep-interview" && phase.trim().toLowerCase() === "interviewing") {
748
+ return `GJC deep-interview is still interviewing and must not stop (${statePath}). Continue the active round immediately: score and persist an answered round, then report progress. Use the ask tool for the next question. Only stop after crystallizing, recording a handoff, or explicitly cancelling the workflow. ${buildHandoffStopReleaseGuidance(skill)}`;
749
+ }
746
750
  return `GJC handoff skill "${skill}" must not stop without offering a next step (phase: ${phase}; state: ${statePath}). ${buildHandoffStopReleaseGuidance(skill)}`;
747
751
  }
748
752
 
749
753
  export async function buildSkillStopOutput(input: StopHookInput): Promise<Record<string, unknown> | null> {
750
- const resolvedSessionId = await resolveBoundarySessionId(input.cwd, input.sessionId);
751
- const skillState = await readVisibleSkillActiveState(input.cwd, resolvedSessionId, input.stateDir);
754
+ const guardContext = await readWorkflowGuardContext(input.cwd, {
755
+ sessionId: input.sessionId,
756
+ threadId: input.threadId,
757
+ });
758
+ const resolvedSessionId = guardContext.sessionId;
759
+ if (!resolvedSessionId) return null;
760
+ const skillState = guardContext.activeState;
761
+
752
762
  const activeEntries = listActiveSkills(skillState)
753
763
  .filter(isWorkflowActiveEntry)
754
764
  .filter(entry =>
@@ -757,11 +767,7 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
757
767
  if (!skillState || activeEntries.length === 0) return null;
758
768
 
759
769
  for (const entry of activeEntries) {
760
- const modeState = await readValidatedJsonFile<ModeState>(
761
- modeStatePath(input.cwd, entry.skill, resolvedSessionId),
762
- "mode-state",
763
- ModeStateSchema,
764
- );
770
+ const modeState = guardContext.modeStates.get(entry.skill) ?? null;
765
771
  const handoffRequired = isHandoffRequiredSkill(entry.skill);
766
772
  if (!modeState && handoffRequired) {
767
773
  const phase = String(entry.phase ?? skillState.phase ?? "active");
@@ -784,7 +790,7 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
784
790
  systemMessage: rescueMessage,
785
791
  };
786
792
  }
787
- if (modeStateReleasesStop(modeState, handoffRequired)) {
793
+ if (modeStateReleasesStop(modeState, handoffRequired, entry.skill)) {
788
794
  // A mode-state that claims it releases the Stop block must agree with
789
795
  // authoritative durable state. If a stale/incoherent mode-state would
790
796
  // release while the plan/ledger still shows pending work, block instead