@gajae-code/coding-agent 0.11.11 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/cli/read-cli.d.ts +1 -0
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/provider-auth-health.d.ts +14 -0
  9. package/dist/types/config/provider-ranking.d.ts +59 -0
  10. package/dist/types/config/settings-schema.d.ts +83 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/exa/mcp-client.d.ts +10 -1
  13. package/dist/types/exec/bash-executor.d.ts +45 -12
  14. package/dist/types/extensibility/extensions/types.d.ts +38 -4
  15. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  17. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  18. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  19. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  20. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  22. package/dist/types/extensibility/shared-events.d.ts +6 -0
  23. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  24. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  25. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  26. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  27. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  29. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  30. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
  31. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  32. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  33. package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
  34. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  35. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  36. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  37. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
  38. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  39. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  40. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  41. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  42. package/dist/types/main.d.ts +1 -1
  43. package/dist/types/memories/index.d.ts +2 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +10 -19
  45. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  46. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  47. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  48. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  49. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  50. package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
  51. package/dist/types/modes/components/settings-selector.d.ts +11 -0
  52. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  53. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  54. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  55. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  56. package/dist/types/modes/interactive-mode.d.ts +2 -0
  57. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  58. package/dist/types/modes/types.d.ts +2 -0
  59. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  60. package/dist/types/reminders/star-reminder.d.ts +3 -13
  61. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  62. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  64. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  65. package/dist/types/sdk/broker/transport.d.ts +2 -0
  66. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  68. package/dist/types/sdk/bus/config.d.ts +6 -2
  69. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  70. package/dist/types/sdk/bus/index.d.ts +44 -1
  71. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
  72. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  73. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  74. package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
  75. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  76. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  77. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  78. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  79. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  80. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  81. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  82. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  83. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  84. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  85. package/dist/types/sdk/host/host.d.ts +2 -0
  86. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  87. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  88. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  89. package/dist/types/sdk/prompt-status.d.ts +23 -3
  90. package/dist/types/sdk/session.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +82 -12
  92. package/dist/types/session/artifacts.d.ts +8 -0
  93. package/dist/types/session/blob-store.d.ts +57 -4
  94. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  95. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  96. package/dist/types/session/session-manager.d.ts +48 -19
  97. package/dist/types/session/session-storage.d.ts +4 -0
  98. package/dist/types/session/streaming-output.d.ts +96 -12
  99. package/dist/types/setup/credential-import.d.ts +12 -0
  100. package/dist/types/skill-state/active-state.d.ts +12 -4
  101. package/dist/types/skill-state/workflow-hud.d.ts +9 -0
  102. package/dist/types/task/executor.d.ts +3 -2
  103. package/dist/types/task/index.d.ts +3 -1
  104. package/dist/types/task/provider-retry-status.d.ts +8 -0
  105. package/dist/types/task/receipt.d.ts +7 -1
  106. package/dist/types/task/types.d.ts +23 -0
  107. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  108. package/dist/types/tools/bash-interactive.d.ts +4 -1
  109. package/dist/types/tools/bash.d.ts +3 -1
  110. package/dist/types/tools/browser/launch.d.ts +7 -0
  111. package/dist/types/tools/fetch.d.ts +17 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -0
  113. package/dist/types/tools/index.d.ts +4 -0
  114. package/dist/types/tools/output-meta.d.ts +44 -3
  115. package/dist/types/tools/path-utils.d.ts +1 -0
  116. package/dist/types/tools/read-internals.d.ts +12 -0
  117. package/dist/types/tools/read.d.ts +45 -7
  118. package/dist/types/tools/resource-gc.d.ts +4 -0
  119. package/dist/types/tools/review.d.ts +5 -7
  120. package/dist/types/tools/subagent.d.ts +2 -0
  121. package/dist/types/tools/tool-result.d.ts +7 -1
  122. package/dist/types/utils/gh.d.ts +13 -0
  123. package/dist/types/utils/image-resize.d.ts +1 -0
  124. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  125. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  126. package/dist/types/web/search/providers/xai.d.ts +2 -0
  127. package/package.json +11 -13
  128. package/scripts/acp-conformance-agent.ts +218 -0
  129. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  130. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  131. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  132. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  133. package/scripts/resident-memory-bench.ts +1341 -0
  134. package/scripts/run-acp-conformance.ts +285 -0
  135. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  136. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  137. package/src/async/job-manager.ts +78 -20
  138. package/src/capability/index.ts +8 -0
  139. package/src/cli/fast-help.ts +1 -1
  140. package/src/cli/plugin-cli.ts +176 -18
  141. package/src/cli/read-cli.ts +2 -1
  142. package/src/cli.ts +1 -0
  143. package/src/commands/deep-interview.ts +29 -9
  144. package/src/commands/harness.ts +35 -12
  145. package/src/commands/read.ts +11 -2
  146. package/src/commands/team.ts +20 -2
  147. package/src/config/file-lock.ts +11 -2
  148. package/src/config/model-registry.ts +21 -2
  149. package/src/config/provider-auth-health.ts +42 -0
  150. package/src/config/provider-ranking.ts +119 -0
  151. package/src/config/settings-schema.ts +74 -1
  152. package/src/config/settings.ts +246 -108
  153. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  154. package/src/coordinator-mcp/server.ts +5 -12
  155. package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
  156. package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
  157. package/src/defaults/gjc/skills/team/SKILL.md +3 -1
  158. package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
  159. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  160. package/src/defaults/gjc-defaults.ts +4 -4
  161. package/src/eval/py/executor.ts +7 -2
  162. package/src/exa/mcp-client.ts +12 -3
  163. package/src/exec/bash-executor.ts +144 -27
  164. package/src/extensibility/extensions/runner.ts +15 -0
  165. package/src/extensibility/extensions/types.ts +46 -5
  166. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  167. package/src/extensibility/gjc-plugins/index.ts +19 -2
  168. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  169. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  170. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  171. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  172. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  173. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  174. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  175. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  176. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  177. package/src/extensibility/gjc-plugins/types.ts +141 -0
  178. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  179. package/src/extensibility/shared-events.ts +5 -0
  180. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  181. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  182. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  183. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  184. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  185. package/src/gjc-runtime/gc-render.ts +1 -0
  186. package/src/gjc-runtime/gc-runtime.ts +11 -1
  187. package/src/gjc-runtime/launch-tmux.ts +25 -6
  188. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  189. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  190. package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
  191. package/src/gjc-runtime/state-runtime.ts +2 -1
  192. package/src/gjc-runtime/state-writer.ts +70 -222
  193. package/src/gjc-runtime/team-launch.ts +42 -5
  194. package/src/gjc-runtime/team-runtime.ts +1085 -40
  195. package/src/gjc-runtime/team-store.ts +32 -0
  196. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  197. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  198. package/src/gjc-runtime/team-workers.ts +1 -0
  199. package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
  200. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  201. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  202. package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
  203. package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
  204. package/src/gjc-runtime/workflow-manifest.ts +38 -247
  205. package/src/hooks/native-skill-hook.ts +17 -4
  206. package/src/hooks/skill-state.ts +1 -23
  207. package/src/internal-urls/docs-index.generated.ts +21 -22
  208. package/src/internal-urls/local-root-gc.ts +127 -0
  209. package/src/main.ts +9 -5
  210. package/src/memories/index.ts +10 -0
  211. package/src/modes/DESIGN.md +98 -0
  212. package/src/modes/acp/acp-agent.ts +339 -191
  213. package/src/modes/acp/acp-event-mapper.ts +14 -7
  214. package/src/modes/acp/acp-mode.ts +39 -3
  215. package/src/modes/components/assistant-message.ts +123 -22
  216. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  217. package/src/modes/components/irc-sidebar.ts +176 -62
  218. package/src/modes/components/model-selector.ts +61 -4
  219. package/src/modes/components/notifications-settings-editor.ts +12 -1
  220. package/src/modes/components/oauth-selector.ts +96 -28
  221. package/src/modes/components/read-tool-group.ts +82 -14
  222. package/src/modes/components/session-observer-overlay.ts +457 -83
  223. package/src/modes/components/settings-selector.ts +116 -23
  224. package/src/modes/components/status-line/gh.ts +16 -0
  225. package/src/modes/components/tool-execution.ts +138 -27
  226. package/src/modes/components/tool-status-header.ts +3 -14
  227. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  228. package/src/modes/controllers/command-controller.ts +4 -0
  229. package/src/modes/controllers/event-controller.ts +109 -27
  230. package/src/modes/controllers/extension-ui-controller.ts +26 -2
  231. package/src/modes/controllers/input-controller.ts +18 -3
  232. package/src/modes/controllers/selector-controller.ts +127 -36
  233. package/src/modes/interactive-mode.ts +62 -3
  234. package/src/modes/irc-observation-ledger.ts +71 -15
  235. package/src/modes/runtime-init.ts +22 -2
  236. package/src/modes/types.ts +2 -0
  237. package/src/modes/utils/ui-helpers.ts +94 -1
  238. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  239. package/src/prompts/agents/architect.md +9 -1
  240. package/src/prompts/agents/critic.md +9 -0
  241. package/src/prompts/system/eager-todo.md +8 -4
  242. package/src/prompts/system/system-prompt.md +11 -5
  243. package/src/prompts/tools/bash.md +5 -5
  244. package/src/prompts/tools/read.md +4 -2
  245. package/src/prompts/tools/search-tool-bm25.md +5 -0
  246. package/src/prompts/tools/task.md +1 -1
  247. package/src/reminders/star-reminder.ts +4 -42
  248. package/src/runtime-mcp/manager.ts +19 -4
  249. package/src/runtime-mcp/smithery-auth.ts +17 -2
  250. package/src/runtime-mcp/smithery-connect.ts +11 -1
  251. package/src/sdk/acp/adapter.ts +22 -6
  252. package/src/sdk/broker/lifecycle.ts +25 -6
  253. package/src/sdk/broker/transport.ts +35 -3
  254. package/src/sdk/bus/attachment-registry.ts +5 -1
  255. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  256. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  257. package/src/sdk/bus/config.ts +18 -2
  258. package/src/sdk/bus/control-drain-lease.ts +104 -0
  259. package/src/sdk/bus/index.ts +984 -146
  260. package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
  261. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  262. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  263. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  264. package/src/sdk/bus/reconciliation-store.ts +303 -0
  265. package/src/sdk/bus/rich-draft.ts +1 -1
  266. package/src/sdk/bus/rich-render.ts +2 -2
  267. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  268. package/src/sdk/bus/telegram-cli.ts +33 -8
  269. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  270. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  271. package/src/sdk/bus/telegram-daemon.ts +916 -23
  272. package/src/sdk/bus/telegram-reference.ts +40 -3
  273. package/src/sdk/bus/threaded-render.ts +48 -3
  274. package/src/sdk/bus/topic-registry.ts +20 -0
  275. package/src/sdk/host/control/dispatch.ts +5 -1
  276. package/src/sdk/host/control/operations.ts +1 -1
  277. package/src/sdk/host/host.ts +23 -1
  278. package/src/sdk/host/query/handlers.ts +55 -0
  279. package/src/sdk/host/query/revision-store.ts +36 -2
  280. package/src/sdk/host/reverse-leases.ts +44 -7
  281. package/src/sdk/mcp/server.ts +46 -18
  282. package/src/sdk/prompt-status.ts +16 -3
  283. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  284. package/src/sdk/protocol/operation-registry.ts +7 -2
  285. package/src/sdk/session.ts +101 -10
  286. package/src/session/agent-session.ts +1286 -220
  287. package/src/session/artifacts.ts +22 -0
  288. package/src/session/auth-broker-config.ts +10 -3
  289. package/src/session/blob-store.ts +915 -24
  290. package/src/session/internal/managed-session-scope.ts +44 -6
  291. package/src/session/internal/managed-session-storage.ts +48 -6
  292. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  293. package/src/session/session-manager.ts +2364 -438
  294. package/src/session/session-storage.ts +9 -1
  295. package/src/session/streaming-output.ts +500 -111
  296. package/src/setup/credential-import.ts +75 -13
  297. package/src/setup/provider-onboarding.ts +19 -4
  298. package/src/skill-state/active-state.ts +32 -10
  299. package/src/skill-state/workflow-hud.ts +42 -2
  300. package/src/skill-state/workflow-mutation-guard.ts +560 -8
  301. package/src/task/executor.ts +122 -39
  302. package/src/task/index.ts +23 -14
  303. package/src/task/provider-retry-status.ts +16 -0
  304. package/src/task/receipt.ts +23 -7
  305. package/src/task/render.ts +6 -2
  306. package/src/task/types.ts +108 -0
  307. package/src/tools/ask-answer-registry.ts +12 -6
  308. package/src/tools/ask.ts +169 -118
  309. package/src/tools/ast-edit.ts +1 -0
  310. package/src/tools/ast-grep.ts +1 -0
  311. package/src/tools/bash-interactive.ts +8 -3
  312. package/src/tools/bash.ts +503 -60
  313. package/src/tools/browser/launch.ts +41 -6
  314. package/src/tools/fetch.ts +114 -23
  315. package/src/tools/find.ts +1 -0
  316. package/src/tools/image-gen.ts +29 -5
  317. package/src/tools/index.ts +4 -0
  318. package/src/tools/output-meta.ts +281 -20
  319. package/src/tools/path-utils.ts +8 -3
  320. package/src/tools/read-internals.ts +44 -0
  321. package/src/tools/read.ts +1560 -310
  322. package/src/tools/resource-gc.ts +182 -18
  323. package/src/tools/review.ts +5 -7
  324. package/src/tools/search.ts +1 -0
  325. package/src/tools/subagent-render.ts +6 -2
  326. package/src/tools/subagent.ts +6 -0
  327. package/src/tools/telegram-send.ts +6 -3
  328. package/src/tools/tool-result.ts +14 -1
  329. package/src/utils/gh.ts +39 -0
  330. package/src/utils/image-resize.ts +3 -0
  331. package/src/web/search/providers/anthropic.ts +4 -4
  332. package/src/web/search/providers/codex.ts +1 -1
  333. package/src/web/search/providers/kimi.ts +15 -5
  334. package/src/web/search/providers/searxng.ts +47 -4
  335. package/src/web/search/providers/xai.ts +7 -2
  336. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  337. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  338. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  339. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  340. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  341. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  342. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  343. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
  344. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -209,7 +209,12 @@ export function buildActionMarkdown(action: {
209
209
  if (action.kind === "idle") {
210
210
  return action.summary ? `🟢 Agent idle\n${action.summary}` : "🟢 Agent idle";
211
211
  }
212
- const heading = `❓ **${action.question ?? "Question"}**`;
212
+ const question = (action.question ?? "Question")
213
+ .split(/\r\n|\r|\n/)
214
+ .map(line => line.trimEnd())
215
+ .map(line => (line ? `**${line}**` : ""))
216
+ .join(" \n");
217
+ const heading = `❓ ${question}`;
213
218
  const options = action.options ?? [];
214
219
  const displayOptions = withRecommendedOptionLabel(options, action.recommendedIndex);
215
220
  if (options.length === 0) return `${heading}\n\n(reply with text)`;
@@ -217,19 +222,43 @@ export function buildActionMarkdown(action: {
217
222
  return `${heading}\n\n${list}`;
218
223
  }
219
224
 
225
+ export type TelegramNotificationSound = "all" | "important" | "none";
226
+
227
+ export type TelegramDeliveryLane = "ask" | "idle" | "live" | "finalized";
228
+
229
+ /**
230
+ * Resolve Telegram's optional silent-delivery flag. `finalChunk` silences
231
+ * non-final actionable chunks under the `important` policy; `undefined`
232
+ * intentionally omits the field so Telegram uses its normal audible delivery
233
+ * behavior.
234
+ */
235
+ export function telegramDisableNotification(
236
+ sound: TelegramNotificationSound | undefined,
237
+ lane: TelegramDeliveryLane,
238
+ finalChunk = true,
239
+ ): true | undefined {
240
+ if (sound === "none") return true;
241
+ if (sound !== "important") return undefined;
242
+ return (lane === "ask" || lane === "idle") && finalChunk ? undefined : true;
243
+ }
244
+
220
245
  /** Send Telegram HTML text chunks sequentially so long messages preserve order. */
221
246
  export async function sendTelegramHtmlChunks(
222
247
  send: TelegramSend,
223
248
  chatId: string,
224
249
  text: string,
225
250
  inlineKeyboard?: InlineButton[][],
251
+ sound?: TelegramNotificationSound,
252
+ lane: TelegramDeliveryLane = "ask",
226
253
  ): Promise<void> {
227
254
  const chunks = splitTelegramHtml(text);
228
255
  for (let i = 0; i < chunks.length; i++) {
256
+ const disableNotification = telegramDisableNotification(sound, lane, i === chunks.length - 1);
229
257
  await send("sendMessage", {
230
258
  chat_id: chatId,
231
259
  text: chunks[i]!,
232
260
  parse_mode: TELEGRAM_PARSE_MODE,
261
+ ...(disableNotification === true ? { disable_notification: true } : {}),
233
262
  ...(i === chunks.length - 1 && inlineKeyboard ? { reply_markup: { inline_keyboard: inlineKeyboard } } : {}),
234
263
  });
235
264
  }
@@ -348,6 +377,7 @@ export interface TelegramReferenceOptions {
348
377
  endpointFile: string;
349
378
  apiBase?: string;
350
379
  fetchImpl?: typeof fetch;
380
+ sound?: TelegramNotificationSound;
351
381
  }
352
382
 
353
383
  /**
@@ -395,7 +425,14 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
395
425
  controls: msg.controls,
396
426
  summary: msg.summary,
397
427
  });
398
- await sendTelegramHtmlChunks(send, opts.chatId, rendered.text, rendered.inline_keyboard);
428
+ await sendTelegramHtmlChunks(
429
+ send,
430
+ opts.chatId,
431
+ rendered.text,
432
+ rendered.inline_keyboard,
433
+ opts.sound,
434
+ msg.kind ?? "ask",
435
+ );
399
436
  } else if (msg.type === "action_unavailable") {
400
437
  const requiredCapabilities = Array.isArray(msg.requiredCapabilities)
401
438
  ? msg.requiredCapabilities
@@ -416,7 +453,7 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
416
453
  // session's forum topic; this reference shows the minimal handling.
417
454
  const threaded = renderThreadedFrame(msg as never);
418
455
  if (threaded?.text) {
419
- await sendTelegramHtmlChunks(send, opts.chatId, threaded.text);
456
+ await sendTelegramHtmlChunks(send, opts.chatId, threaded.text, undefined, opts.sound, threaded.lane);
420
457
  }
421
458
  }
422
459
  };
@@ -18,13 +18,13 @@ export interface ThreadedSend {
18
18
  method: "sendMessage" | "sendPhoto" | "sendDocument";
19
19
  /** Rate-limit lane for prioritisation/fairness. */
20
20
  lane: RateLimitLane;
21
- /** Message text (sendMessage) or photo caption (sendPhoto). */
21
+ /** Message text (sendMessage) or media caption (sendPhoto/sendDocument). */
22
22
  text?: string;
23
23
  /** Base64 image bytes for sendPhoto. */
24
24
  photoBase64?: string;
25
25
  /** Base64 file bytes for sendDocument. */
26
26
  documentBase64?: string;
27
- /** Image MIME type for sendPhoto. */
27
+ /** Uploaded media MIME type. */
28
28
  mime?: string;
29
29
  /** Suggested document filename. */
30
30
  fileName?: string;
@@ -100,6 +100,40 @@ function str(v: unknown): string | undefined {
100
100
  return typeof v === "string" && v.length > 0 ? v : undefined;
101
101
  }
102
102
 
103
+ function baseMimeType(mime: string): string {
104
+ return mime.split(";", 1)[0]!.trim().toLowerCase();
105
+ }
106
+
107
+ export function supportsTelegramPhotoUpload(mime: string | undefined): boolean {
108
+ if (mime === undefined) return true;
109
+ const normalized = baseMimeType(mime);
110
+ return normalized === "image/jpeg" || normalized === "image/png";
111
+ }
112
+
113
+ function imageDocumentFileName(mime: string | undefined): string {
114
+ switch (mime === undefined ? "" : baseMimeType(mime)) {
115
+ case "image/webp":
116
+ return "image.webp";
117
+ case "image/gif":
118
+ return "image.gif";
119
+ case "image/svg+xml":
120
+ return "image.svg";
121
+ case "image/avif":
122
+ return "image.avif";
123
+ case "image/bmp":
124
+ case "image/x-ms-bmp":
125
+ return "image.bmp";
126
+ case "image/tiff":
127
+ return "image.tiff";
128
+ case "image/heic":
129
+ return "image.heic";
130
+ case "image/heif":
131
+ return "image.heif";
132
+ default:
133
+ return "image";
134
+ }
135
+ }
136
+
103
137
  function isDotOnlyText(value: string): boolean {
104
138
  return /^[.\s]+$/.test(value);
105
139
  }
@@ -244,11 +278,22 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
244
278
  const data = str(frame.data);
245
279
  if (!data) return undefined;
246
280
  const caption = str(frame.caption);
281
+ const mime = str(frame.mime);
282
+ if (!supportsTelegramPhotoUpload(mime)) {
283
+ return {
284
+ method: "sendDocument",
285
+ lane: "finalized",
286
+ documentBase64: data,
287
+ mime,
288
+ fileName: imageDocumentFileName(mime),
289
+ text: finalizeTelegramHtml(caption === undefined ? undefined : escapeHtml(caption)),
290
+ };
291
+ }
247
292
  return {
248
293
  method: "sendPhoto",
249
294
  lane: "finalized",
250
295
  photoBase64: data,
251
- mime: str(frame.mime),
296
+ mime,
252
297
  text: finalizeTelegramHtml(caption === undefined ? undefined : escapeHtml(caption)),
253
298
  };
254
299
  }
@@ -43,6 +43,8 @@ export interface TopicRecord {
43
43
  creationLeaseEpoch?: number;
44
44
  /** An uncertain delete fences future creation and inbound routing. */
45
45
  authorityState?: "active" | "delete_pending";
46
+ /** Provenance for topics created by the user and adopted by GJC. Missing means daemon-created. */
47
+ topicOrigin?: "user_created";
46
48
  /** Telegram chat and endpoint authority last proven to use this topic. */
47
49
  chatId?: string;
48
50
  /** Canonical endpoint tuple (URL + token) that currently holds the lease. */
@@ -234,6 +236,7 @@ export class TopicRegistry {
234
236
  ...(raw.authorityState === "delete_pending" || fenceSupersedesRecord
235
237
  ? { authorityState: "delete_pending" as const }
236
238
  : {}),
239
+ ...(raw.topicOrigin === "user_created" ? { topicOrigin: "user_created" as const } : {}),
237
240
  ...(isValidBindingString(raw.chatId) ? { chatId: raw.chatId } : {}),
238
241
  ...(isValidBindingString(raw.endpointKey) ? { endpointKey: raw.endpointKey } : {}),
239
242
  ...(isValidBindingString(raw.endpointDigest) ? { endpointDigest: raw.endpointDigest } : {}),
@@ -307,6 +310,21 @@ export class TopicRegistry {
307
310
  const record = this.topics.get(sessionId);
308
311
  return record?.authorityState !== "delete_pending" && this.byTopic.get(record?.topicId ?? "") === sessionId;
309
312
  }
313
+ /**
314
+ * Pure read-only availability check for user-topic adoption. Rejects invalid
315
+ * ids and any id already committed, delete-pending/fenced, staged, or
316
+ * ambiguous, without mutating maps, epochs, or persistence. Used inside the
317
+ * {@link getOrCreateTopic} create callback to fail-closed before adopting a
318
+ * user-created topic id.
319
+ */
320
+ isTopicIdAvailable(topicId: string): boolean {
321
+ if (!isValidTopicId(topicId)) return false;
322
+ if (this.#ambiguousTopicIds.has(topicId)) return false;
323
+ if (this.byTopic.has(topicId)) return false;
324
+ for (const record of this.topics.values()) if (record.topicId === topicId) return false;
325
+ for (const record of this.staged.values()) if (record.topicId === topicId) return false;
326
+ return true;
327
+ }
310
328
 
311
329
  /**
312
330
  * Resolve endpoint authority for identity-less replay. An endpoint may bootstrap
@@ -455,6 +473,7 @@ export class TopicRegistry {
455
473
  binding?: TopicEndpointBinding,
456
474
  commit?: () => Promise<void>,
457
475
  transientClaimant?: object,
476
+ topicOrigin?: TopicRecord["topicOrigin"],
458
477
  ): Promise<TopicRecord> {
459
478
  const existing = this.topics.get(sessionId);
460
479
  if (existing?.authorityState === "delete_pending") throw new Error("topic authority is deletion-fenced");
@@ -491,6 +510,7 @@ export class TopicRegistry {
491
510
  }
492
511
  : {}),
493
512
  ...(revoked ? { authorityState: "delete_pending" as const } : {}),
513
+ ...(topicOrigin === "user_created" ? { topicOrigin: "user_created" as const } : {}),
494
514
  };
495
515
  if (revoked) {
496
516
  this.topics.set(sessionId, record);
@@ -158,7 +158,11 @@ function invoke(
158
158
  case "workflow.plan_approve":
159
159
  return surface.approvePlan(text(input, "id"), input.choice, input.expectedSessionId as string | undefined);
160
160
  case "skill.invoke":
161
- return surface.invokeSkill(text(input, "name"), input.args);
161
+ return surface.invokeSkill(
162
+ text(input, "name"),
163
+ input.args,
164
+ typeof input.clientRef === "string" ? input.clientRef : undefined,
165
+ );
162
166
  case "mode.plan.set":
163
167
  return surface.setPlanMode(input.on as boolean);
164
168
  case "mode.goal.operate":
@@ -19,7 +19,7 @@ export interface ControlSurface {
19
19
  idempotencyKey?: string,
20
20
  ): Promise<ControlValue> | ControlValue;
21
21
  approvePlan(id: string, choice: ControlValue, expectedSessionId?: string): Promise<ControlValue> | ControlValue;
22
- invokeSkill(name: string, args: ControlValue): Promise<ControlValue> | ControlValue;
22
+ invokeSkill(name: string, args: ControlValue, clientRef?: string): Promise<ControlValue> | ControlValue;
23
23
  setPlanMode(on: boolean): Promise<ControlValue> | ControlValue;
24
24
  operateGoal(op: string, objective?: string): Promise<ControlValue> | ControlValue;
25
25
  replaceTodo(items: ControlValue): Promise<ControlValue> | ControlValue;
@@ -9,6 +9,13 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
9
9
  query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
10
10
  /** Best-effort diagnostic observation of accepted control/query frames. */
11
11
  onRequest?: SdkRequestObserver;
12
+ /** Runs before a control response is sent; identity transitions use sendTerminal. */
13
+ beforeControlResponse?: (
14
+ connectionId: string,
15
+ request: SdkFrame,
16
+ response: SdkFrame,
17
+ sendTerminal: () => Promise<void>,
18
+ ) => void | Promise<void>;
12
19
  /** Runs only after a successful control response has been sent to the client. */
13
20
  afterControlResponse?: (connectionId: string, request: SdkFrame, response: SdkFrame) => void | Promise<void>;
14
21
  installProviderDefinitions?: (capability: string, definitions: unknown) => void;
@@ -36,6 +43,14 @@ function errorFrame(connectionId: string, frame: SdkFrame, error: unknown): SdkF
36
43
  ? candidate.code
37
44
  : "internal";
38
45
  const message = typeof candidate?.message === "string" ? candidate.message : "SDK host operation failed.";
46
+ if (frame.type === "control_request") {
47
+ return {
48
+ type: "control_response",
49
+ id: typeof frame.id === "string" ? frame.id : "",
50
+ ok: false,
51
+ error: { code, message },
52
+ };
53
+ }
39
54
  return {
40
55
  type: "reverse_response",
41
56
  id: typeof frame.id === "string" ? frame.id : "",
@@ -214,7 +229,14 @@ export class SessionSdkHost {
214
229
  const result = await this.#options.control?.(connectionId, frame);
215
230
  if (result !== undefined) {
216
231
  const response = { type: "control_response", ...(result as SdkFrame) };
217
- await this.#send(connectionId, response);
232
+ let terminalSent = false;
233
+ const sendTerminal = async (): Promise<void> => {
234
+ if (terminalSent) return;
235
+ terminalSent = true;
236
+ await this.#send(connectionId, response);
237
+ };
238
+ await this.#options.beforeControlResponse?.(connectionId, frame, response, sendTerminal);
239
+ await sendTerminal();
218
240
  await this.#options.afterControlResponse?.(connectionId, frame, response);
219
241
  }
220
242
  break;
@@ -46,6 +46,11 @@ export interface SessionSurface {
46
46
  getJobs(): unknown | Promise<unknown>;
47
47
  /** Q26 keyed lookup of a submitted prompt's authoritative reconciliation status. */
48
48
  getPromptStatus?(selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown | Promise<unknown>;
49
+ getSkillInvokeStatus?(selector: {
50
+ commandId?: string;
51
+ turnId?: string;
52
+ clientRef?: string;
53
+ }): unknown | Promise<unknown>;
49
54
  /** Q27 effective model-profile catalog from the live session registry. */
50
55
  getModelProfiles?(): unknown[] | Promise<unknown[]>;
51
56
  /** Query rows backed by the session's installed binding map. */
@@ -127,6 +132,7 @@ const names = [
127
132
  "runtime.jobs.list",
128
133
  "turn.prompt_status",
129
134
  "models.profiles.list",
135
+ "skill.invoke_status",
130
136
  ];
131
137
 
132
138
  export class QueryHandlers {
@@ -156,6 +162,7 @@ export class QueryHandlers {
156
162
  if (query === "Q02") return await this.#transcriptBody(request);
157
163
  if (query === "Q23") return await this.#resourceBody(request);
158
164
  if (query === "Q26") return await this.#promptStatus(request);
165
+ if (query === "Q28") return await this.#skillInvokeStatus(request);
159
166
  if (query === "Q24") return await this.#artifact(request);
160
167
  if (query === "Q27" && request.input && Object.keys(request.input).length > 0)
161
168
  return this.#error(request, "invalid_request", false, "models.profiles.list does not accept input fields.");
@@ -517,6 +524,54 @@ export class QueryHandlers {
517
524
  return { id: request.id, ok: true, page };
518
525
  }
519
526
 
527
+ async #skillInvokeStatus(request: QueryRequest): Promise<QueryResponse> {
528
+ if (request.cursor)
529
+ return this.#error(request, "invalid_request", false, "skill.invoke_status does not support cursors.");
530
+ const input = request.input ?? {};
531
+ for (const key of Object.keys(input))
532
+ if (key !== "commandId" && key !== "turnId" && key !== "clientRef")
533
+ return this.#error(
534
+ request,
535
+ "invalid_request",
536
+ false,
537
+ `skill.invoke_status does not accept selector field "${key}".`,
538
+ );
539
+ const commandId = typeof input.commandId === "string" && input.commandId ? input.commandId : undefined;
540
+ const turnId = typeof input.turnId === "string" && input.turnId ? input.turnId : undefined;
541
+ const rawClientRef = typeof input.clientRef === "string" ? input.clientRef : undefined;
542
+ const trimmedClientRef = rawClientRef?.trim();
543
+ if (rawClientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH))
544
+ return this.#error(
545
+ request,
546
+ "invalid_request",
547
+ false,
548
+ "clientRef must be a non-empty string of at most 128 characters.",
549
+ );
550
+ const clientRef = trimmedClientRef || undefined;
551
+ if ((commandId === undefined) !== (turnId === undefined))
552
+ return this.#error(request, "invalid_request", false, "commandId and turnId must be provided together.");
553
+ if (commandId !== undefined && clientRef !== undefined)
554
+ return this.#error(
555
+ request,
556
+ "invalid_request",
557
+ false,
558
+ "Provide exactly one selector: a commandId/turnId pair or a clientRef.",
559
+ );
560
+ if (commandId === undefined && clientRef === undefined)
561
+ return this.#error(
562
+ request,
563
+ "invalid_request",
564
+ false,
565
+ "skill.invoke_status requires a commandId/turnId pair or a clientRef.",
566
+ );
567
+ if (typeof this.surface.getSkillInvokeStatus !== "function")
568
+ return this.#error(request, "unavailable", false, "skill.invoke_status is unavailable for this session.");
569
+ const result = await this.surface.getSkillInvokeStatus(
570
+ clientRef !== undefined ? { clientRef } : { commandId: commandId!, turnId: turnId! },
571
+ );
572
+ return { id: request.id, ok: true, result };
573
+ }
574
+
520
575
  async #promptStatus(request: QueryRequest): Promise<QueryResponse> {
521
576
  if (request.cursor)
522
577
  return this.#error(request, "invalid_request", false, "turn.prompt_status does not support cursors.");
@@ -2,6 +2,32 @@ import { createHash } from "node:crypto";
2
2
  import { chmod, mkdir, mkdtemp, open, readFile, rename, rm, unlink } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { postmortem } from "@gajae-code/utils";
6
+
7
+ /**
8
+ * Spill directories this process created under the system temp dir.
9
+ *
10
+ * `close()` removes a store's own directory, but an abnormal exit never reaches
11
+ * it, and these hold real spilled snapshot data rather than a marker. The two
12
+ * sibling temp-artifact caches (`shell-snapshot`, `python-runner-artifact`)
13
+ * already register a postmortem sweep; this does the same.
14
+ *
15
+ * Only `mkdtemp` directories are tracked. A caller-supplied `storageDir` is
16
+ * durable session storage and must never be swept here.
17
+ */
18
+ const processSpillDirectories = new Set<string>();
19
+
20
+ /** Test seam: spill directories currently tracked for postmortem cleanup. */
21
+ export function trackedSpillDirectoriesForTest(): string[] {
22
+ return [...processSpillDirectories];
23
+ }
24
+
25
+ postmortem.register("sdk-revision-store:spill", async () => {
26
+ for (const dir of [...processSpillDirectories]) {
27
+ processSpillDirectories.delete(dir);
28
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
29
+ }
30
+ });
5
31
 
6
32
  export const MAX_REVISIONS_PER_RESOURCE = 8;
7
33
  export const MAX_PINNED_REVISIONS = 128;
@@ -512,7 +538,10 @@ export class RevisionStore {
512
538
  this.#memoryBytes = 0;
513
539
  this.#chunkRefs.clear();
514
540
  this.#manifestRefs.clear();
515
- if (this.#directory) await rm(this.#directory, { recursive: true, force: true });
541
+ if (this.#directory) {
542
+ processSpillDirectories.delete(this.#directory);
543
+ await rm(this.#directory, { recursive: true, force: true });
544
+ }
516
545
  })();
517
546
  return this.#closePromise;
518
547
  }
@@ -1040,7 +1069,12 @@ export class RevisionStore {
1040
1069
  }
1041
1070
 
1042
1071
  async #spillDirectory(): Promise<string> {
1043
- if (!this.#directory) this.#directory = await mkdtemp(join(tmpdir(), "gjc-sdk-snapshots-"));
1072
+ if (!this.#directory) {
1073
+ this.#directory = await mkdtemp(join(tmpdir(), "gjc-sdk-snapshots-"));
1074
+ // Only temp spill directories are swept on abnormal exit; a caller-supplied
1075
+ // storageDir is durable and never enters this set.
1076
+ processSpillDirectories.add(this.#directory);
1077
+ }
1044
1078
  await mkdir(join(this.#directory, "objects"), { recursive: true, mode: 0o700 });
1045
1079
  await mkdir(join(this.#directory, "manifests"), { recursive: true, mode: 0o700 });
1046
1080
  await chmod(this.#directory, 0o700);
@@ -41,6 +41,8 @@ interface Outstanding {
41
41
  leaseId: string;
42
42
  resolve: (value: unknown) => void;
43
43
  reject: (reason: Error) => void;
44
+ signal?: AbortSignal;
45
+ onAbort?: () => void;
44
46
  }
45
47
 
46
48
  export interface ReverseLeaseOptions {
@@ -188,7 +190,7 @@ export class ReverseLeaseRuntime {
188
190
  this.#cancelForConnection(connectionId, "provider_disconnected");
189
191
  }
190
192
 
191
- request(capability: string, method: string, payload: unknown): Promise<unknown> {
193
+ request(capability: string, method: string, payload: unknown, signal?: AbortSignal): Promise<unknown> {
192
194
  if (this.#disposing) throw new Error("reverse runtime is disposing");
193
195
  this.#assertPayload(payload);
194
196
  const lease = this.#liveLease(capability);
@@ -198,16 +200,42 @@ export class ReverseLeaseRuntime {
198
200
  throw new ReverseLeaseError("lease_unavailable");
199
201
  throw new ReverseLeaseError("provider_required");
200
202
  }
203
+ if (signal?.aborted)
204
+ return Promise.reject(Object.assign(new Error("request_cancelled"), { name: "request_cancelled" }));
201
205
  if (this.#outstanding.size >= MAX_REVERSE_OUTSTANDING) throw new ReverseLeaseError("too_many_outstanding");
202
206
  const id = randomUUID();
203
207
  return new Promise((resolve, reject) => {
204
- this.#outstanding.set(id, {
208
+ const outstanding: Outstanding = {
205
209
  connectionId: lease.connectionId,
206
210
  capability,
207
211
  leaseId: lease.leaseId,
208
212
  resolve,
209
213
  reject,
210
- });
214
+ ...(signal ? { signal } : {}),
215
+ };
216
+ this.#outstanding.set(id, outstanding);
217
+ if (signal) {
218
+ outstanding.onAbort = () => {
219
+ if (this.#takeOutstanding(id) !== outstanding) return;
220
+ try {
221
+ const cancellation = this.#sendFrame(lease.connectionId, {
222
+ type: "reverse_cancel",
223
+ id,
224
+ connectionId: lease.connectionId,
225
+ leaseId: lease.leaseId,
226
+ });
227
+ void Promise.resolve(cancellation).catch(() => {});
228
+ } catch {
229
+ // Cancellation is best effort; the caller is already settled locally.
230
+ }
231
+ reject(Object.assign(new Error("request_cancelled"), { name: "request_cancelled" }));
232
+ };
233
+ signal.addEventListener("abort", outstanding.onAbort, { once: true });
234
+ if (signal.aborted) {
235
+ outstanding.onAbort();
236
+ return;
237
+ }
238
+ }
211
239
  let delivery: void | Promise<void>;
212
240
  try {
213
241
  delivery = this.#sendFrame(lease.connectionId, {
@@ -219,12 +247,12 @@ export class ReverseLeaseRuntime {
219
247
  payload: { method, payload },
220
248
  });
221
249
  } catch (error) {
222
- this.#outstanding.delete(id);
250
+ this.#takeOutstanding(id);
223
251
  reject(error instanceof Error ? error : new Error(String(error)));
224
252
  return;
225
253
  }
226
254
  Promise.resolve(delivery).catch(error => {
227
- this.#outstanding.delete(id);
255
+ if (this.#takeOutstanding(id) !== outstanding) return;
228
256
  reject(error instanceof Error ? error : new Error(String(error)));
229
257
  });
230
258
  });
@@ -242,7 +270,7 @@ export class ReverseLeaseRuntime {
242
270
  if (!request) throw new ReverseLeaseError("unknown_request");
243
271
  if (request.connectionId !== connectionId || request.leaseId !== leaseId)
244
272
  throw new ReverseLeaseError("not_lease_owner");
245
- this.#outstanding.delete(id);
273
+ this.#takeOutstanding(id);
246
274
  if (error) {
247
275
  const rejection = new Error(error.message);
248
276
  rejection.name = error.code;
@@ -270,6 +298,8 @@ export class ReverseLeaseRuntime {
270
298
  this.#installedCapabilities.clear();
271
299
  this.#leases.clear();
272
300
  this.#idempotency.clear();
301
+ for (const request of outstanding.map(([, request]) => request))
302
+ if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
273
303
  for (const [id, request] of outstanding) {
274
304
  request.reject(new Error("request_cancelled"));
275
305
  try {
@@ -304,11 +334,18 @@ export class ReverseLeaseRuntime {
304
334
  #cancelForConnection(connectionId: string, reason: "provider_disconnected" | "lease_released"): void {
305
335
  for (const [id, request] of this.#outstanding)
306
336
  if (request.connectionId === connectionId) {
307
- this.#outstanding.delete(id);
337
+ this.#takeOutstanding(id);
308
338
  request.reject(new Error("request_cancelled"));
309
339
  this.#onCancel?.(id, reason);
310
340
  }
311
341
  }
342
+ #takeOutstanding(id: string): Outstanding | undefined {
343
+ const request = this.#outstanding.get(id);
344
+ if (!request) return undefined;
345
+ this.#outstanding.delete(id);
346
+ if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
347
+ return request;
348
+ }
312
349
  #installDefinitionsFor(capability: string, definitions: unknown): void {
313
350
  this.#installDefinitions?.(capability, definitions);
314
351
  this.#installedCapabilities.add(capability);
@@ -301,9 +301,16 @@ export function createSdkMcpServer(options: SdkMcpServerOptions = {}) {
301
301
  export async function runSdkMcpStdio(options: SdkMcpServerOptions = {}): Promise<void> {
302
302
  const server = createSdkMcpServer(options);
303
303
  let buffer = "";
304
+ const inflight = new Set<Promise<void>>();
304
305
  process.stdin.setEncoding("utf8");
305
- await new Promise<void>(resolve => {
306
- process.stdin.on("data", (chunk: string) => {
306
+
307
+ const track = (work: Promise<void>): void => {
308
+ inflight.add(work);
309
+ void work.finally(() => inflight.delete(work));
310
+ };
311
+
312
+ await new Promise<void>((resolve, reject) => {
313
+ const onData = (chunk: string) => {
307
314
  buffer += chunk;
308
315
  let index = buffer.indexOf("\n");
309
316
  while (index >= 0) {
@@ -311,22 +318,43 @@ export async function runSdkMcpStdio(options: SdkMcpServerOptions = {}): Promise
311
318
  buffer = buffer.slice(index + 1);
312
319
  index = buffer.indexOf("\n");
313
320
  if (!line) continue;
314
- void (async () => {
315
- let request: JsonRpcRequest;
316
- try {
317
- request = JSON.parse(line) as JsonRpcRequest;
318
- } catch {
319
- process.stdout.write(
320
- `${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse_error" } })}\n`,
321
- );
322
- return;
323
- }
324
- const response = await server.handleJsonRpc(request);
325
- // JSON-RPC notifications (no id) receive no response.
326
- if (request.id !== undefined) process.stdout.write(`${JSON.stringify(response)}\n`);
327
- })();
321
+ track(
322
+ (async () => {
323
+ let request: JsonRpcRequest;
324
+ try {
325
+ request = JSON.parse(line) as JsonRpcRequest;
326
+ } catch {
327
+ process.stdout.write(
328
+ `${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse_error" } })}\n`,
329
+ );
330
+ return;
331
+ }
332
+ const response = await server.handleJsonRpc(request);
333
+ // JSON-RPC notifications (no id) receive no response.
334
+ if (request.id !== undefined) process.stdout.write(`${JSON.stringify(response)}\n`);
335
+ })(),
336
+ );
328
337
  }
329
- });
330
- process.stdin.on("end", () => resolve());
338
+ };
339
+ const onEnd = () => {
340
+ process.stdin.off("data", onData);
341
+ process.stdin.off("end", onEnd);
342
+ process.stdin.off("error", onError);
343
+ resolve();
344
+ };
345
+ const onError = (error: Error) => {
346
+ process.stdin.off("data", onData);
347
+ process.stdin.off("end", onEnd);
348
+ process.stdin.off("error", onError);
349
+ reject(error);
350
+ };
351
+ process.stdin.on("data", onData);
352
+ process.stdin.on("end", onEnd);
353
+ process.stdin.on("error", onError);
331
354
  });
355
+
356
+ // Stdin EOF must not drop in-flight tools/call handlers (WS connect/query).
357
+ // Awaiting them also prevents the process from exiting before responses flush,
358
+ // and lets clients close so the event loop can drain deterministically.
359
+ await Promise.allSettled([...inflight]);
332
360
  }