@gajae-code/coding-agent 0.10.2 → 0.11.0

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 (465) hide show
  1. package/CHANGELOG.md +50 -2
  2. package/dist/types/append-only-mode.d.ts +1 -1
  3. package/dist/types/cli/args.d.ts +4 -5
  4. package/dist/types/cli/daemon-cli.d.ts +12 -1
  5. package/dist/types/cli/notify-cli.d.ts +21 -1
  6. package/dist/types/cli.d.ts +3 -0
  7. package/dist/types/commands/daemon.d.ts +39 -0
  8. package/dist/types/commands/launch.d.ts +3 -0
  9. package/dist/types/commands/notify.d.ts +28 -1
  10. package/dist/types/commands/sdk.d.ts +33 -0
  11. package/dist/types/config/atomic-yaml-patch.d.ts +85 -0
  12. package/dist/types/config/model-profile-activation.d.ts +35 -15
  13. package/dist/types/config/model-profiles.d.ts +11 -5
  14. package/dist/types/config/model-registry.d.ts +3 -3
  15. package/dist/types/config/model-resolver.d.ts +39 -6
  16. package/dist/types/config/model-selector-value.d.ts +17 -0
  17. package/dist/types/config/models-config-schema.d.ts +10 -6
  18. package/dist/types/config/settings-schema.d.ts +133 -6
  19. package/dist/types/config/settings.d.ts +63 -35
  20. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -6
  21. package/dist/types/coordinator-mcp/server.d.ts +6 -49
  22. package/dist/types/daemon/builtin.d.ts +6 -5
  23. package/dist/types/daemon/control-types.d.ts +1 -1
  24. package/dist/types/discovery/mcp-json.d.ts +12 -1
  25. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/compact-handler.d.ts +4 -2
  27. package/dist/types/extensibility/extensions/get-commands-handler.d.ts +2 -2
  28. package/dist/types/extensibility/extensions/loader.d.ts +10 -1
  29. package/dist/types/extensibility/extensions/runner.d.ts +3 -3
  30. package/dist/types/extensibility/extensions/types.d.ts +141 -16
  31. package/dist/types/extensibility/hooks/types.d.ts +1 -1
  32. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -2
  33. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
  35. package/dist/types/gjc-runtime/team-runtime.d.ts +3 -4
  36. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +13 -0
  37. package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -1
  38. package/dist/types/gjc-runtime/windows-powershell-command.d.ts +9 -0
  39. package/dist/types/harness-control-plane/operate.d.ts +5 -5
  40. package/dist/types/harness-control-plane/owner.d.ts +18 -4
  41. package/dist/types/harness-control-plane/receipts.d.ts +0 -1
  42. package/dist/types/harness-control-plane/sdk-transport.d.ts +43 -0
  43. package/dist/types/harness-control-plane/session-transport.d.ts +48 -0
  44. package/dist/types/harness-control-plane/types.d.ts +3 -4
  45. package/dist/types/lsp/index.d.ts +1 -1
  46. package/dist/types/main.d.ts +32 -38
  47. package/dist/types/modes/acp/acp-agent.d.ts +224 -53
  48. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -0
  49. package/dist/types/modes/acp/acp-mode.d.ts +6 -15
  50. package/dist/types/modes/acp/permission-mode.d.ts +4 -0
  51. package/dist/types/modes/acp/startup-options.d.ts +6 -0
  52. package/dist/types/modes/components/agent-dashboard.d.ts +3 -0
  53. package/dist/types/modes/components/model-selector.d.ts +1 -0
  54. package/dist/types/modes/components/notifications-settings-editor.d.ts +135 -0
  55. package/dist/types/modes/components/settings-selector.d.ts +6 -4
  56. package/dist/types/modes/components/welcome.d.ts +1 -1
  57. package/dist/types/modes/controllers/selector-controller.d.ts +35 -1
  58. package/dist/types/modes/index.d.ts +0 -4
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/runtime-init.d.ts +2 -2
  61. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +6 -9
  62. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -6
  63. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +6 -6
  64. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +4 -9
  65. package/dist/types/modes/shared/agent-wire/event-observation.d.ts +1 -1
  66. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +169 -30
  67. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +7 -7
  68. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +105 -0
  69. package/dist/types/modes/types.d.ts +5 -0
  70. package/dist/types/runtime-mcp/client.d.ts +2 -0
  71. package/dist/types/runtime-mcp/config.d.ts +5 -1
  72. package/dist/types/runtime-mcp/manager.d.ts +8 -1
  73. package/dist/types/runtime-mcp/types.d.ts +4 -0
  74. package/dist/types/sdk/acp/adapter.d.ts +67 -0
  75. package/dist/types/sdk/acp/index.d.ts +2 -0
  76. package/dist/types/sdk/broker/broker.d.ts +53 -0
  77. package/dist/types/sdk/broker/discovery.d.ts +30 -0
  78. package/dist/types/sdk/broker/ensure.d.ts +33 -0
  79. package/dist/types/sdk/broker/identity.d.ts +3 -0
  80. package/dist/types/sdk/broker/index.d.ts +8 -0
  81. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +95 -0
  82. package/dist/types/sdk/broker/lifecycle.d.ts +85 -0
  83. package/dist/types/sdk/broker/process-incarnation.d.ts +14 -0
  84. package/dist/types/sdk/broker/runtime.d.ts +33 -0
  85. package/dist/types/sdk/broker/session-index.d.ts +57 -0
  86. package/dist/types/sdk/broker/state-version.d.ts +8 -0
  87. package/dist/types/sdk/broker/transport.d.ts +9 -0
  88. package/dist/types/sdk/bus/chat-command-policy.d.ts +49 -0
  89. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +21 -0
  90. package/dist/types/sdk/bus/chat-daemon-control.d.ts +111 -0
  91. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +77 -0
  92. package/dist/types/sdk/bus/chat-effect-journal.d.ts +84 -0
  93. package/dist/types/{notifications → sdk/bus}/config-commands.d.ts +7 -2
  94. package/dist/types/sdk/bus/config.d.ts +274 -0
  95. package/dist/types/sdk/bus/conversation-store.d.ts +72 -0
  96. package/dist/types/sdk/bus/discord-conversation.d.ts +58 -0
  97. package/dist/types/sdk/bus/discord-daemon.d.ts +45 -0
  98. package/dist/types/sdk/bus/discord-live-provider.d.ts +70 -0
  99. package/dist/types/sdk/bus/discord-provider.d.ts +85 -0
  100. package/dist/types/{notifications → sdk/bus}/html-format.d.ts +2 -2
  101. package/dist/types/{notifications → sdk/bus}/index.d.ts +113 -13
  102. package/dist/types/{notifications → sdk/bus}/lifecycle-control-runtime.d.ts +4 -2
  103. package/dist/types/{notifications → sdk/bus}/lifecycle-orchestrator.d.ts +18 -21
  104. package/dist/types/{notifications → sdk/bus}/managed-daemon.d.ts +1 -1
  105. package/dist/types/sdk/bus/native-runtime-compatibility.d.ts +13 -0
  106. package/dist/types/sdk/bus/notification-orchestration.d.ts +170 -0
  107. package/dist/types/{notifications → sdk/bus}/notification-service.d.ts +7 -1
  108. package/dist/types/sdk/bus/session-control.d.ts +83 -0
  109. package/dist/types/sdk/bus/slack-conversation.d.ts +56 -0
  110. package/dist/types/sdk/bus/slack-daemon.d.ts +60 -0
  111. package/dist/types/sdk/bus/slack-live-provider.d.ts +59 -0
  112. package/dist/types/sdk/bus/slack-provider.d.ts +55 -0
  113. package/dist/types/{notifications → sdk/bus}/telegram-cli.d.ts +4 -4
  114. package/dist/types/{notifications → sdk/bus}/telegram-daemon-cli.d.ts +14 -5
  115. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -0
  116. package/dist/types/{notifications → sdk/bus}/telegram-daemon-control.d.ts +2 -2
  117. package/dist/types/{notifications → sdk/bus}/telegram-daemon.d.ts +55 -17
  118. package/dist/types/{notifications → sdk/bus}/telegram-reference.d.ts +2 -2
  119. package/dist/types/sdk/bus/telegram-setup.d.ts +128 -0
  120. package/dist/types/{notifications → sdk/bus}/topic-registry.d.ts +11 -0
  121. package/dist/types/sdk/cli/index.d.ts +1 -0
  122. package/dist/types/sdk/cli/session-cli.d.ts +19 -0
  123. package/dist/types/sdk/client/client.d.ts +1 -0
  124. package/dist/types/sdk/client/discovery.d.ts +27 -0
  125. package/dist/types/sdk/client/index.d.ts +2 -0
  126. package/dist/types/sdk/host/control/dispatch.d.ts +34 -0
  127. package/dist/types/sdk/host/control/index.d.ts +2 -0
  128. package/dist/types/sdk/host/control/operations.d.ts +65 -0
  129. package/dist/types/sdk/host/events.d.ts +35 -0
  130. package/dist/types/sdk/host/host.d.ts +35 -0
  131. package/dist/types/sdk/host/index.d.ts +4 -0
  132. package/dist/types/sdk/host/query/cursor.d.ts +62 -0
  133. package/dist/types/sdk/host/query/handlers.d.ts +70 -0
  134. package/dist/types/sdk/host/query/index.d.ts +3 -0
  135. package/dist/types/sdk/host/query/revision-store.d.ts +60 -0
  136. package/dist/types/sdk/host/reverse-leases.d.ts +45 -0
  137. package/dist/types/sdk/host/types.d.ts +32 -0
  138. package/dist/types/sdk/index.d.ts +6 -0
  139. package/dist/types/sdk/lifecycle-session.d.ts +14 -0
  140. package/dist/types/sdk/mcp/index.d.ts +1 -0
  141. package/dist/types/sdk/mcp/server.d.ts +37 -0
  142. package/dist/types/sdk/models.d.ts +44 -0
  143. package/dist/types/sdk/protocol/adapter-validation.d.ts +14 -0
  144. package/dist/types/sdk/protocol/operation-registry.d.ts +24 -0
  145. package/dist/types/{sdk.d.ts → sdk/session.d.ts} +44 -38
  146. package/dist/types/sdk/startup-capability.d.ts +50 -0
  147. package/dist/types/session/agent-session.d.ts +114 -21
  148. package/dist/types/session/default-model-selection.d.ts +25 -0
  149. package/dist/types/session/fallback-chain-controller.d.ts +52 -0
  150. package/dist/types/session/session-manager.d.ts +58 -1
  151. package/dist/types/setup/credential-auto-import.d.ts +36 -6
  152. package/dist/types/tools/ask-answer-registry.d.ts +1 -1
  153. package/dist/types/tools/browser/benchmark/detector-report.d.ts +63 -0
  154. package/dist/types/tools/browser/benchmark/run-suite.d.ts +18 -0
  155. package/dist/types/tools/browser/launch.d.ts +22 -0
  156. package/dist/types/tools/browser/profile-discovery.d.ts +31 -0
  157. package/dist/types/tools/browser/profile-posture.d.ts +37 -0
  158. package/dist/types/tools/browser/profile-reuse.d.ts +39 -0
  159. package/dist/types/tools/browser/profile-warmup.d.ts +39 -0
  160. package/dist/types/tools/browser/registry.d.ts +19 -0
  161. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  162. package/dist/types/tools/index.d.ts +8 -8
  163. package/package.json +34 -13
  164. package/scripts/build-sdk-package-smoke.ts +94 -0
  165. package/scripts/capture-notifications-settings-showcase.ts +435 -0
  166. package/scripts/compile-args.ts +4 -2
  167. package/scripts/g005-daemon-smoke.ts +4 -5
  168. package/scripts/g011-daemon-path-smoke.ts +5 -5
  169. package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
  170. package/scripts/generate-sdk-operation-inventory.ts +867 -0
  171. package/scripts/generate-telegram-baseline-manifest.ts +128 -0
  172. package/scripts/manifest-schema.ts +124 -0
  173. package/scripts/run-test-manifest.ts +153 -0
  174. package/scripts/run-threat-model-check.ts +157 -0
  175. package/scripts/verify-gjc-sdk-canonicalization.ts +3467 -0
  176. package/scripts/verify-gjc-sdk-rename.ts +90 -0
  177. package/src/append-only-mode.ts +1 -1
  178. package/src/cli/args.ts +43 -12
  179. package/src/cli/auth-gateway-cli.ts +2 -2
  180. package/src/cli/config-cli.ts +2 -1
  181. package/src/cli/daemon-cli.ts +63 -14
  182. package/src/cli/list-models.ts +1 -1
  183. package/src/cli/notify-cli.ts +288 -286
  184. package/src/cli.ts +21 -2
  185. package/src/commands/daemon.ts +50 -6
  186. package/src/commands/harness.ts +93 -36
  187. package/src/commands/launch.ts +5 -2
  188. package/src/commands/mcp-serve.ts +19 -12
  189. package/src/commands/notify.ts +36 -3
  190. package/src/commands/sdk.ts +426 -0
  191. package/src/commit/agentic/agent.ts +1 -0
  192. package/src/config/atomic-yaml-patch.ts +423 -0
  193. package/src/config/model-profile-activation.ts +240 -89
  194. package/src/config/model-profiles.ts +18 -15
  195. package/src/config/model-registry.ts +51 -26
  196. package/src/config/model-resolver.ts +138 -51
  197. package/src/config/model-selector-value.ts +50 -0
  198. package/src/config/models-config-schema.ts +14 -21
  199. package/src/config/settings-schema.ts +128 -10
  200. package/src/config/settings.ts +1120 -158
  201. package/src/coordinator-mcp/model-preset.ts +74 -20
  202. package/src/coordinator-mcp/server.ts +1597 -3052
  203. package/src/daemon/builtin.ts +8 -5
  204. package/src/daemon/control-types.ts +1 -1
  205. package/src/defaults/gjc/skills/team/SKILL.md +4 -4
  206. package/src/discovery/mcp-json.ts +327 -20
  207. package/src/extensibility/custom-tools/types.ts +1 -1
  208. package/src/extensibility/extensions/compact-handler.ts +3 -3
  209. package/src/extensibility/extensions/get-commands-handler.ts +2 -2
  210. package/src/extensibility/extensions/loader.ts +69 -5
  211. package/src/extensibility/extensions/runner.ts +112 -4
  212. package/src/extensibility/extensions/types.ts +176 -17
  213. package/src/extensibility/hooks/types.ts +1 -1
  214. package/src/gjc-runtime/launch-tmux.ts +170 -86
  215. package/src/gjc-runtime/psmux-detect.ts +82 -4
  216. package/src/gjc-runtime/session-state-sidecar.ts +41 -31
  217. package/src/gjc-runtime/team-runtime.ts +54 -35
  218. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +53 -17
  219. package/src/gjc-runtime/tmux-owner-isolation.ts +1 -1
  220. package/src/gjc-runtime/tmux-sessions.ts +19 -13
  221. package/src/gjc-runtime/ultragoal-guard.ts +53 -6
  222. package/src/gjc-runtime/ultragoal-runtime.ts +155 -3
  223. package/src/gjc-runtime/windows-powershell-command.ts +58 -0
  224. package/src/harness-control-plane/operate.ts +9 -9
  225. package/src/harness-control-plane/owner.ts +283 -39
  226. package/src/harness-control-plane/receipts.ts +0 -1
  227. package/src/harness-control-plane/sdk-transport.ts +395 -0
  228. package/src/harness-control-plane/session-transport.ts +94 -0
  229. package/src/harness-control-plane/types.ts +1 -1
  230. package/src/internal-urls/docs-index.generated.ts +34 -35
  231. package/src/lsp/client.ts +42 -6
  232. package/src/lsp/index.ts +2 -2
  233. package/src/main.ts +299 -204
  234. package/src/modes/DESIGN.md +258 -0
  235. package/src/modes/acp/acp-agent.ts +1228 -2612
  236. package/src/modes/acp/acp-client-bridge.ts +2 -20
  237. package/src/modes/acp/acp-event-mapper.ts +18 -2
  238. package/src/modes/acp/acp-mode.ts +8 -36
  239. package/src/modes/acp/permission-mode.ts +25 -0
  240. package/src/modes/acp/startup-options.ts +6 -0
  241. package/src/modes/components/agent-dashboard.ts +37 -17
  242. package/src/modes/components/model-selector.ts +93 -35
  243. package/src/modes/components/notifications-settings-editor.ts +1379 -0
  244. package/src/modes/components/settings-selector.ts +60 -11
  245. package/src/modes/components/welcome.ts +4 -4
  246. package/src/modes/controllers/command-controller.ts +1 -2
  247. package/src/modes/controllers/event-controller.ts +11 -16
  248. package/src/modes/controllers/extension-ui-controller.ts +348 -4
  249. package/src/modes/controllers/selector-controller.ts +674 -36
  250. package/src/modes/index.ts +0 -22
  251. package/src/modes/interactive-mode.ts +59 -20
  252. package/src/modes/runtime-init.ts +298 -6
  253. package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
  254. package/src/modes/shared/agent-wire/deep-interview-gate.ts +10 -11
  255. package/src/modes/shared/agent-wire/event-contract.ts +7 -8
  256. package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
  257. package/src/modes/shared/agent-wire/event-observation.ts +16 -7
  258. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1254 -87
  259. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +15 -15
  260. package/src/modes/shared/agent-wire/workflow-gate-types.ts +113 -0
  261. package/src/modes/types.ts +5 -0
  262. package/src/runtime-mcp/client.ts +94 -19
  263. package/src/runtime-mcp/config.ts +27 -15
  264. package/src/runtime-mcp/manager.ts +343 -105
  265. package/src/runtime-mcp/oauth-flow.ts +42 -12
  266. package/src/runtime-mcp/transports/http.ts +49 -19
  267. package/src/runtime-mcp/transports/stdio.ts +47 -28
  268. package/src/runtime-mcp/types.ts +10 -0
  269. package/src/sdk/acp/adapter.ts +418 -0
  270. package/src/sdk/acp/index.ts +2 -0
  271. package/src/sdk/broker/broker.ts +642 -0
  272. package/src/sdk/broker/discovery.ts +94 -0
  273. package/src/sdk/broker/ensure.ts +249 -0
  274. package/src/sdk/broker/identity.ts +42 -0
  275. package/src/sdk/broker/index.ts +8 -0
  276. package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
  277. package/src/sdk/broker/internal-source.bunfig.toml +1 -0
  278. package/src/sdk/broker/lifecycle-ledger.ts +286 -0
  279. package/src/sdk/broker/lifecycle.ts +2157 -0
  280. package/src/sdk/broker/process-incarnation.ts +159 -0
  281. package/src/sdk/broker/runtime.ts +164 -0
  282. package/src/sdk/broker/session-index.ts +318 -0
  283. package/src/sdk/broker/state-version.ts +23 -0
  284. package/src/sdk/broker/transport.ts +125 -0
  285. package/src/sdk/bus/chat-command-policy.ts +107 -0
  286. package/src/sdk/bus/chat-daemon-cli.ts +214 -0
  287. package/src/sdk/bus/chat-daemon-control.ts +713 -0
  288. package/src/sdk/bus/chat-daemon-runtime.ts +526 -0
  289. package/src/sdk/bus/chat-effect-journal.ts +353 -0
  290. package/src/{notifications → sdk/bus}/config-commands.ts +39 -10
  291. package/src/sdk/bus/config.ts +433 -0
  292. package/src/sdk/bus/conversation-store.ts +391 -0
  293. package/src/sdk/bus/discord-conversation.ts +85 -0
  294. package/src/sdk/bus/discord-daemon.ts +2275 -0
  295. package/src/sdk/bus/discord-live-provider.ts +521 -0
  296. package/src/sdk/bus/discord-provider.ts +56 -0
  297. package/src/{notifications → sdk/bus}/html-format.ts +2 -2
  298. package/src/sdk/bus/index.ts +3943 -0
  299. package/src/{notifications → sdk/bus}/lifecycle-commands.ts +2 -0
  300. package/src/{notifications → sdk/bus}/lifecycle-control-runtime.ts +225 -196
  301. package/src/{notifications → sdk/bus}/lifecycle-orchestrator.ts +161 -74
  302. package/src/{notifications → sdk/bus}/managed-daemon.ts +2 -2
  303. package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
  304. package/src/sdk/bus/natives-augment.d.ts +38 -0
  305. package/src/sdk/bus/notification-orchestration.ts +535 -0
  306. package/src/{notifications → sdk/bus}/notification-service.ts +106 -12
  307. package/src/sdk/bus/session-control.ts +333 -0
  308. package/src/sdk/bus/slack-conversation.ts +74 -0
  309. package/src/sdk/bus/slack-daemon.ts +2144 -0
  310. package/src/sdk/bus/slack-live-provider.ts +380 -0
  311. package/src/sdk/bus/slack-provider.ts +76 -0
  312. package/src/{notifications → sdk/bus}/telegram-cli.ts +8 -8
  313. package/src/sdk/bus/telegram-daemon-cli.ts +330 -0
  314. package/src/sdk/bus/telegram-daemon-contract.ts +14 -0
  315. package/src/{notifications → sdk/bus}/telegram-daemon-control.ts +4 -4
  316. package/src/{notifications → sdk/bus}/telegram-daemon.ts +502 -54
  317. package/src/{notifications → sdk/bus}/telegram-reference.ts +2 -2
  318. package/src/sdk/bus/telegram-setup.ts +663 -0
  319. package/src/{notifications → sdk/bus}/topic-registry.ts +32 -0
  320. package/src/sdk/cli/index.ts +1 -0
  321. package/src/sdk/cli/session-cli.ts +275 -0
  322. package/src/sdk/client/client.ts +1 -0
  323. package/src/sdk/client/discovery.ts +108 -0
  324. package/src/sdk/client/index.ts +2 -0
  325. package/src/sdk/host/control/dispatch.ts +367 -0
  326. package/src/sdk/host/control/index.ts +10 -0
  327. package/src/sdk/host/control/operations.ts +63 -0
  328. package/src/sdk/host/events.ts +91 -0
  329. package/src/sdk/host/host.ts +296 -0
  330. package/src/sdk/host/index.ts +4 -0
  331. package/src/sdk/host/query/cursor.ts +189 -0
  332. package/src/sdk/host/query/handlers.ts +533 -0
  333. package/src/sdk/host/query/index.ts +3 -0
  334. package/src/sdk/host/query/revision-store.ts +1126 -0
  335. package/src/sdk/host/reverse-leases.ts +325 -0
  336. package/src/sdk/host/types.ts +29 -0
  337. package/src/sdk/index.ts +13 -0
  338. package/src/sdk/lifecycle-session.ts +39 -0
  339. package/src/sdk/mcp/index.ts +1 -0
  340. package/src/sdk/mcp/server.ts +332 -0
  341. package/src/sdk/models.ts +168 -0
  342. package/src/sdk/protocol/adapter-validation.ts +48 -0
  343. package/src/sdk/protocol/operation-inventory.generated.json +5654 -0
  344. package/src/sdk/protocol/operation-registry.ts +312 -0
  345. package/src/{sdk.ts → sdk/session.ts} +339 -212
  346. package/src/sdk/startup-capability.ts +189 -0
  347. package/src/session/agent-session.ts +2453 -894
  348. package/src/session/default-model-selection.ts +93 -0
  349. package/src/session/fallback-chain-controller.ts +136 -0
  350. package/src/session/session-manager.ts +438 -11
  351. package/src/session/session-storage.ts +8 -1
  352. package/src/setup/credential-auto-import.ts +290 -35
  353. package/src/setup/hermes/templates/operator-instructions.v1.md +3 -5
  354. package/src/slash-commands/builtin-registry.ts +2 -1
  355. package/src/task/executor.ts +62 -13
  356. package/src/tools/ask-answer-registry.ts +1 -1
  357. package/src/tools/ask.ts +13 -8
  358. package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
  359. package/src/tools/browser/benchmark/detector-report.ts +157 -0
  360. package/src/tools/browser/benchmark/run-suite.ts +39 -0
  361. package/src/tools/browser/launch.ts +124 -19
  362. package/src/tools/browser/profile-discovery.ts +63 -0
  363. package/src/tools/browser/profile-posture.ts +59 -0
  364. package/src/tools/browser/profile-reuse.ts +102 -0
  365. package/src/tools/browser/profile-warmup.ts +109 -0
  366. package/src/tools/browser/registry.ts +87 -11
  367. package/src/tools/browser/tab-protocol.ts +1 -0
  368. package/src/tools/browser/tab-supervisor.ts +1 -0
  369. package/src/tools/browser/tab-worker.ts +1 -1
  370. package/src/tools/browser.ts +6 -0
  371. package/src/tools/index.ts +8 -8
  372. package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
  373. package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
  374. package/src/tools/telegram-send.ts +2 -2
  375. package/vendor/insane-search/MANIFEST.json +2 -1
  376. package/vendor/insane-search/engine/bias_check.py +4 -1
  377. package/vendor/insane-search/engine/phase0.py +26 -1
  378. package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
  379. package/dist/types/modes/bridge/auth.d.ts +0 -12
  380. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
  381. package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
  382. package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
  383. package/dist/types/modes/bridge/event-stream.d.ts +0 -8
  384. package/dist/types/modes/rpc/host-tools.d.ts +0 -1
  385. package/dist/types/modes/rpc/host-uris.d.ts +0 -1
  386. package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
  387. package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
  388. package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
  389. package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
  390. package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
  391. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
  392. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
  393. package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
  394. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
  395. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
  396. package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
  397. package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
  398. package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
  399. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
  400. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
  401. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
  402. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
  403. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
  404. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
  405. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
  406. package/dist/types/notifications/config.d.ts +0 -132
  407. package/src/harness-control-plane/rpc-adapter.ts +0 -290
  408. package/src/modes/bridge/auth.ts +0 -41
  409. package/src/modes/bridge/bridge-client-bridge.ts +0 -47
  410. package/src/modes/bridge/bridge-mode.ts +0 -675
  411. package/src/modes/bridge/bridge-ui-context.ts +0 -200
  412. package/src/modes/bridge/event-stream.ts +0 -70
  413. package/src/modes/rpc/host-tools.ts +0 -1
  414. package/src/modes/rpc/host-uris.ts +0 -1
  415. package/src/modes/rpc/rpc-client.ts +0 -1166
  416. package/src/modes/rpc/rpc-mode.ts +0 -1029
  417. package/src/modes/rpc/rpc-socket-security.ts +0 -115
  418. package/src/modes/rpc/rpc-types.ts +0 -637
  419. package/src/modes/shared/agent-wire/command-contract.ts +0 -18
  420. package/src/modes/shared/agent-wire/command-dispatch.ts +0 -498
  421. package/src/modes/shared/agent-wire/command-validation.ts +0 -181
  422. package/src/modes/shared/agent-wire/handshake.ts +0 -157
  423. package/src/modes/shared/agent-wire/host-tool-bridge.ts +0 -197
  424. package/src/modes/shared/agent-wire/host-uri-bridge.ts +0 -236
  425. package/src/modes/shared/agent-wire/protocol.ts +0 -46
  426. package/src/modes/shared/agent-wire/responses.ts +0 -17
  427. package/src/modes/shared/agent-wire/scopes.ts +0 -94
  428. package/src/modes/shared/agent-wire/session-registry.ts +0 -109
  429. package/src/modes/shared/agent-wire/ui-request-broker.ts +0 -150
  430. package/src/modes/shared/agent-wire/ui-result.ts +0 -48
  431. package/src/modes/shared/agent-wire/unattended-action-policy.ts +0 -365
  432. package/src/modes/shared/agent-wire/unattended-audit.ts +0 -176
  433. package/src/modes/shared/agent-wire/unattended-run-controller.ts +0 -426
  434. package/src/modes/shared/agent-wire/unattended-session.ts +0 -528
  435. package/src/notifications/config.ts +0 -219
  436. package/src/notifications/index.ts +0 -1835
  437. package/src/notifications/telegram-daemon-cli.ts +0 -243
  438. package/src/tools/puppeteer/09_stealth_locale.txt +0 -46
  439. /package/dist/types/{notifications → sdk/bus}/attachment-registry.d.ts +0 -0
  440. /package/dist/types/{notifications → sdk/bus}/chat-adapters.d.ts +0 -0
  441. /package/dist/types/{notifications → sdk/bus}/daemon-paths.d.ts +0 -0
  442. /package/dist/types/{notifications → sdk/bus}/engine.d.ts +0 -0
  443. /package/dist/types/{notifications → sdk/bus}/helpers.d.ts +0 -0
  444. /package/dist/types/{notifications → sdk/bus}/lifecycle-commands.d.ts +0 -0
  445. /package/dist/types/{notifications → sdk/bus}/operator-runtime.d.ts +0 -0
  446. /package/dist/types/{notifications → sdk/bus}/rate-limit-pool.d.ts +0 -0
  447. /package/dist/types/{notifications → sdk/bus}/recent-activity.d.ts +0 -0
  448. /package/dist/types/{notifications → sdk/bus}/reply-sent-store.d.ts +0 -0
  449. /package/dist/types/{notifications → sdk/bus}/rich-draft.d.ts +0 -0
  450. /package/dist/types/{notifications → sdk/bus}/rich-render.d.ts +0 -0
  451. /package/dist/types/{notifications → sdk/bus}/threaded-inbound.d.ts +0 -0
  452. /package/dist/types/{notifications → sdk/bus}/threaded-render.d.ts +0 -0
  453. /package/src/{notifications → sdk/bus}/attachment-registry.ts +0 -0
  454. /package/src/{notifications → sdk/bus}/chat-adapters.ts +0 -0
  455. /package/src/{notifications → sdk/bus}/daemon-paths.ts +0 -0
  456. /package/src/{notifications → sdk/bus}/engine.ts +0 -0
  457. /package/src/{notifications → sdk/bus}/helpers.ts +0 -0
  458. /package/src/{notifications → sdk/bus}/operator-runtime.ts +0 -0
  459. /package/src/{notifications → sdk/bus}/rate-limit-pool.ts +0 -0
  460. /package/src/{notifications → sdk/bus}/recent-activity.ts +0 -0
  461. /package/src/{notifications → sdk/bus}/reply-sent-store.ts +0 -0
  462. /package/src/{notifications → sdk/bus}/rich-draft.ts +0 -0
  463. /package/src/{notifications → sdk/bus}/rich-render.ts +0 -0
  464. /package/src/{notifications → sdk/bus}/threaded-inbound.ts +0 -0
  465. /package/src/{notifications → sdk/bus}/threaded-render.ts +0 -0
@@ -4,14 +4,21 @@ import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { logger } from "@gajae-code/utils";
7
- import { withFileLock } from "../config/file-lock";
8
- import type { Settings } from "../config/settings";
9
- import type { DaemonRuntimeInfo } from "../daemon/control-types";
10
- import { resolveGjcRuntimeSpawnInfo } from "../daemon/runtime";
7
+ import { withFileLock } from "../../config/file-lock";
8
+ import type { Settings } from "../../config/settings";
9
+ import type { DaemonRuntimeInfo } from "../../daemon/control-types";
10
+ import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
11
11
  import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
12
12
  import { parseInThreadConfigCommand, parseRichToggleCommand, parseTelegramControlCommand } from "./config-commands";
13
13
  import { daemonPaths, HEARTBEAT_TTL_MS } from "./daemon-paths";
14
+ import { sanitizeDiagnostic } from "./notification-service";
15
+ import { DAEMON_GENERATION, NOTIFICATION_PROTOCOL_VERSION } from "./telegram-daemon-contract";
16
+ import { withTelegramSetupLease } from "./telegram-setup";
17
+
18
+ export { DAEMON_GENERATION, NOTIFICATION_PROTOCOL_VERSION } from "./telegram-daemon-contract";
19
+
14
20
  import {
21
+ buildButtonGrid,
15
22
  buildCompactChoiceGrid,
16
23
  code,
17
24
  splitTelegramHtml,
@@ -41,6 +48,7 @@ import {
41
48
  type LifecycleControlServer,
42
49
  type LifecycleControlServerFactory,
43
50
  } from "./lifecycle-control-runtime";
51
+ import type { OrchestratorDeps } from "./lifecycle-orchestrator";
44
52
  import { NotificationOperatorRuntime, OperatorBackoffPolicy, OperatorEventRouter } from "./operator-runtime";
45
53
  import { RateLimitPool } from "./rate-limit-pool";
46
54
  import { listRecentSessions } from "./recent-activity";
@@ -61,6 +69,8 @@ import { renderThreadedFrame, type ThreadedSend } from "./threaded-render";
61
69
  import { TopicRegistry, type TopicRegistryState } from "./topic-registry";
62
70
 
63
71
  export type EnsureDaemonResult = "owner_spawned" | "attached" | "disabled" | "blocked";
72
+ /** Detailed result for orchestration that must distinguish a #2028 handoff from a fresh spawn. */
73
+ export type EnsureTelegramDaemonDetailedResult = "spawned" | "reloaded" | "attached" | "disabled" | "blocked_identity";
64
74
 
65
75
  export interface DaemonState {
66
76
  pid: number;
@@ -123,21 +133,9 @@ export { HEARTBEAT_TTL_MS };
123
133
  export const DAEMON_VERSION = 1;
124
134
  /** Capability token advertised when the server supports app-level ping/pong. */
125
135
  export const CLIENT_PING_PONG_CAPABILITY = "client_ping_pong";
126
- /** Protocol version the daemon advertises in its ClientHello. */
127
- export const NOTIFICATION_PROTOCOL_VERSION = 3;
128
136
  /** Capability required for typed controls and semantic Selected acknowledgement frames. */
129
137
  export const ASK_SELECTED_ACK_CAPABILITY = "ask_selected_ack_v1";
130
138
  export const ASK_CONTROLS_CAPABILITY = "ask_controls_v1";
131
- /**
132
- * Operational generation the current daemon build speaks, persisted into
133
- * {@link DaemonState.generation} on ownership acquisition. It is tied to the
134
- * wire {@link NOTIFICATION_PROTOCOL_VERSION} so any protocol bump (which is what
135
- * gates capabilities like {@link ASK_SELECTED_ACK_CAPABILITY}) also bumps the
136
- * generation, letting a freshly-upgraded host recognise an older, still-live
137
- * daemon and reload it instead of silently attaching to it. Distinct from the
138
- * persisted schema {@link DAEMON_VERSION}, which did not change across #1999.
139
- */
140
- export const DAEMON_GENERATION = NOTIFICATION_PROTOCOL_VERSION;
141
139
 
142
140
  const nodeFs: TelegramDaemonFs = fs.promises as unknown as TelegramDaemonFs;
143
141
 
@@ -177,6 +175,36 @@ const PENDING_TOPIC_FRAME_LIMIT = 20;
177
175
  const SEEN_UPDATE_ID_LIMIT = 1_000;
178
176
  const ORPHAN_TOPIC_GRACE_MS = 60_000;
179
177
  const CONSUMED_REACTION = "✅";
178
+ const MODEL_CALLBACK_PREFIX = "m:";
179
+ const MODEL_CHOICE_TTL_MS = 10 * 60 * 1_000;
180
+
181
+ const MODEL_BUTTON_LABEL_MAX_BYTES = 48;
182
+ const SENSITIVE_MODEL_LABEL =
183
+ /(?:\b(?:https?|wss?):\/\/|\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b|\b(?:api[-_ ]?key|access[-_ ]?token|bearer|secret|password|account(?:\s*id)?|email|exception|stack trace)\b|\b(?:sk|pk|rk)-[A-Za-z0-9_-]{12,}\b)/i;
184
+
185
+ /** Inline-keyboard labels are plain text, bounded, and stripped of visual control characters. */
186
+ function safeModelButtonLabel(value: unknown): string | undefined {
187
+ if (typeof value !== "string") return undefined;
188
+ const normalized = value
189
+ .replace(/[\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069]/g, " ")
190
+ .replace(/\s+/g, " ")
191
+ .trim();
192
+ if (!normalized) return undefined;
193
+ if (SENSITIVE_MODEL_LABEL.test(normalized)) return undefined;
194
+ if (Buffer.byteLength(normalized, "utf8") <= MODEL_BUTTON_LABEL_MAX_BYTES) return normalized;
195
+
196
+ const marker = "…";
197
+ const markerBytes = Buffer.byteLength(marker, "utf8");
198
+ let label = "";
199
+ let labelBytes = 0;
200
+ for (const char of normalized) {
201
+ const charBytes = Buffer.byteLength(char, "utf8");
202
+ if (labelBytes + charBytes + markerBytes > MODEL_BUTTON_LABEL_MAX_BYTES) break;
203
+ label += char;
204
+ labelBytes += charBytes;
205
+ }
206
+ return `${label}${marker}`;
207
+ }
180
208
 
181
209
  function splitTelegramPlainText(text: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
182
210
  if (text.length <= max) return [text];
@@ -254,6 +282,9 @@ async function fetchWithRetry(
254
282
  }
255
283
 
256
284
  export { type DaemonPaths, daemonPaths } from "./daemon-paths";
285
+ export function deriveLifecycleAuditRedactionKey(botToken: string): Uint8Array {
286
+ return crypto.createHmac("sha256", botToken).update("gjc.lifecycle.audit.v2.key", "utf8").digest();
287
+ }
257
288
 
258
289
  /**
259
290
  * Attach session-lifecycle control (create/close/resume) to the running daemon.
@@ -265,19 +296,23 @@ export { type DaemonPaths, daemonPaths } from "./daemon-paths";
265
296
  * (NotificationControlServer) is owned/started by the daemon process; this
266
297
  * function only connects it to policy. Returns the orchestrator deps for tests.
267
298
  */
299
+
268
300
  export function startDaemonLifecycleControl(input: {
269
301
  controlServer: ControlServerLike;
270
302
  pairedChatId: string;
271
303
  agentDir: string;
304
+ auditRedactionKey: Uint8Array;
272
305
  env?: NodeJS.ProcessEnv;
273
- }): void {
306
+ }): OrchestratorDeps {
274
307
  const deps = buildOrchestratorDeps({
275
308
  pairedChatId: input.pairedChatId,
276
309
  agentNotificationsDir: daemonPaths(input.agentDir).dir,
277
310
  sessionsRoot: path.join(input.agentDir, "sessions"),
311
+ auditRedactionKey: input.auditRedactionKey,
278
312
  env: input.env,
279
313
  });
280
314
  attachLifecycleControl(input.controlServer, deps);
315
+ return deps;
281
316
  }
282
317
 
283
318
  async function ensureDir(fsImpl: TelegramDaemonFs, dir: string): Promise<void> {
@@ -340,6 +375,44 @@ export async function registerNotificationRoot(input: {
340
375
  return root;
341
376
  }
342
377
 
378
+ export interface UnregisterNotificationRootResult {
379
+ root: string;
380
+ remainingRoots: number;
381
+ }
382
+
383
+ /** Remove one session's Telegram scan root without disturbing other live session roots. */
384
+ export async function unregisterNotificationRoot(input: {
385
+ settings: Settings;
386
+ cwd: string;
387
+ sessionId: string;
388
+ fs?: TelegramDaemonFs;
389
+ }): Promise<UnregisterNotificationRootResult> {
390
+ const fsImpl = input.fs ?? nodeFs;
391
+ const paths = daemonPaths(input.settings.getAgentDir());
392
+ const root = notificationRootForCwd(input.cwd);
393
+ await ensureDir(fsImpl, paths.dir);
394
+ let remainingRoots = 0;
395
+ await withFileLock(
396
+ paths.roots,
397
+ async () => {
398
+ const current = await readJson<{ roots?: string[]; sessions?: Record<string, string> }>(fsImpl, paths.roots);
399
+ if (!current) return;
400
+ const sessions = { ...(current.sessions ?? {}) };
401
+ delete sessions[input.sessionId];
402
+ const rootStillReferenced = Object.values(sessions).includes(root);
403
+ const roots = (current.roots ?? []).filter(candidate => candidate !== root || rootStillReferenced);
404
+ remainingRoots = roots.length;
405
+ await writeJsonAtomic(fsImpl, paths.roots, {
406
+ version: 1,
407
+ roots: Array.from(new Set(roots)).sort(),
408
+ sessions,
409
+ });
410
+ },
411
+ { staleMs: 10_000 },
412
+ );
413
+ return { root, remainingRoots };
414
+ }
415
+
343
416
  function notificationRootForCwd(cwd: string): string {
344
417
  return path.join(cwd, ".gjc", "state");
345
418
  }
@@ -541,14 +614,17 @@ export async function releaseDaemonOwnership(input: {
541
614
 
542
615
  /** Read the persisted daemon ownership state (or undefined when absent). */
543
616
  export async function readDaemonState(
544
- settings: Settings,
617
+ settings: Pick<Settings, "getAgentDir">,
545
618
  fs: TelegramDaemonFs = nodeFs,
546
619
  ): Promise<DaemonState | undefined> {
547
620
  return readJson<DaemonState>(fs, daemonPaths(settings.getAgentDir()).state);
548
621
  }
549
622
 
550
623
  /** Read the persisted notification roots list. */
551
- export async function readDaemonRoots(settings: Settings, fs: TelegramDaemonFs = nodeFs): Promise<string[]> {
624
+ export async function readDaemonRoots(
625
+ settings: Pick<Settings, "getAgentDir">,
626
+ fs: TelegramDaemonFs = nodeFs,
627
+ ): Promise<string[]> {
552
628
  const roots = await readJson<{ roots?: string[] }>(fs, daemonPaths(settings.getAgentDir()).roots);
553
629
  return roots?.roots ?? [];
554
630
  }
@@ -688,35 +764,57 @@ export async function spawnTelegramDaemonOwner(
688
764
  return { result: "owner_spawned", ownerId: ownership.ownerId, runtime, warnings: [] };
689
765
  }
690
766
 
691
- export async function ensureTelegramDaemonRunning(
767
+ /**
768
+ * Ensure a configured daemon owns this session root, preserving ownership safety
769
+ * while exposing whether a #2028 generation handoff was required.
770
+ */
771
+ export async function ensureTelegramDaemonRunningDetailed(
692
772
  input: { settings: Settings; cwd: string; sessionId: string },
693
773
  deps: TelegramDaemonDeps = {},
694
- ): Promise<EnsureDaemonResult> {
774
+ ): Promise<EnsureTelegramDaemonDetailedResult> {
695
775
  const cfg = getNotificationConfig(input.settings);
696
776
  if (!isTelegramConfigured(cfg)) return "disabled";
697
- const root = notificationRootForCwd(input.cwd);
698
- const fp = tokenFingerprint(cfg.botToken);
699
- const spawned = await spawnTelegramDaemonOwner(
700
- { settings: input.settings, roots: [root], tokenFingerprint: fp, chatId: cfg.chatId },
701
- deps,
702
- );
703
- if (spawned.result === "blocked") {
704
- logger.warn(`notifications: failed to ensure Telegram daemon: ${spawned.warnings.join("; ")}`);
705
- return spawned.result;
706
- }
707
- if (spawned.reloadRequired) {
708
- // A still-live owner is running an OLDER daemon generation than this host
709
- // (e.g. a pre-upgrade daemon reused in place across an in-place upgrade).
710
- // Attaching would silently drop this host's newer ask-ack / control frames,
711
- // so register this session's root first — so the replacement daemon serves
712
- // it — then hand off through the tested SIGTERM/control reload path to bring
713
- // up a fresh current-generation daemon.
777
+ return await withTelegramSetupLease(cfg.botToken, async () => {
778
+ const root = notificationRootForCwd(input.cwd);
779
+ const fp = tokenFingerprint(cfg.botToken);
780
+ const spawned = await spawnTelegramDaemonOwner(
781
+ { settings: input.settings, roots: [root], tokenFingerprint: fp, chatId: cfg.chatId },
782
+ deps,
783
+ );
784
+ if (spawned.result === "blocked") {
785
+ logger.warn(`notifications: failed to ensure Telegram daemon: ${spawned.warnings.join("; ")}`);
786
+ return "blocked_identity";
787
+ }
788
+ if (spawned.reloadRequired) {
789
+ await registerNotificationRoot({ ...input, fs: deps.fs });
790
+ await reloadStaleGenerationOwner(input.settings, deps);
791
+ return "reloaded";
792
+ }
714
793
  await registerNotificationRoot({ ...input, fs: deps.fs });
715
- await reloadStaleGenerationOwner(input.settings, deps);
716
- return "owner_spawned";
794
+ return spawned.result === "owner_spawned" ? "spawned" : "attached";
795
+ });
796
+ }
797
+
798
+ /**
799
+ * Legacy compatibility mapping for callers that only distinguish ownership from
800
+ * attachment. New orchestration should use {@link ensureTelegramDaemonRunningDetailed}.
801
+ */
802
+ export async function ensureTelegramDaemonRunning(
803
+ input: { settings: Settings; cwd: string; sessionId: string },
804
+ deps: TelegramDaemonDeps = {},
805
+ ): Promise<EnsureDaemonResult> {
806
+ const result = await ensureTelegramDaemonRunningDetailed(input, deps);
807
+ switch (result) {
808
+ case "spawned":
809
+ case "reloaded":
810
+ return "owner_spawned";
811
+ case "attached":
812
+ return "attached";
813
+ case "disabled":
814
+ return "disabled";
815
+ case "blocked_identity":
816
+ return "blocked";
717
817
  }
718
- await registerNotificationRoot({ ...input, fs: deps.fs });
719
- return spawned.result;
720
818
  }
721
819
 
722
820
  /**
@@ -876,7 +974,7 @@ export class TelegramUpdatePoller {
876
974
  // A cooperative stop aborts the in-flight long poll; treat as a clean wake.
877
975
  if (isAbortError(err)) return 0;
878
976
  // A transient Telegram API failure must never crash the daemon.
879
- logger.error("notifications daemon: getUpdates failed", { error: String(err) });
977
+ logger.error("notifications daemon: getUpdates failed", { error: sanitizeDiagnostic(String(err)) });
880
978
  await this.#opts.runtime.sleep(POLL_BACKOFF_MS, signal);
881
979
  return 0;
882
980
  }
@@ -955,6 +1053,16 @@ export interface TelegramDaemonOptions {
955
1053
  * default applies (e.g. lifecycle control disabled), no control server starts.
956
1054
  */
957
1055
  createLifecycleControlServer?: LifecycleControlServerFactory | null;
1056
+ /**
1057
+ * Test seam for observing the exact production lifecycle dependency input.
1058
+ * Production defaults to {@link buildOrchestratorDeps}.
1059
+ */
1060
+ createLifecycleOrchestratorDeps?: (input: {
1061
+ pairedChatId: string;
1062
+ agentNotificationsDir: string;
1063
+ sessionsRoot: string;
1064
+ auditRedactionKey: Uint8Array;
1065
+ }) => OrchestratorDeps;
958
1066
  /** Rich text promotion (enabled by default; see rich-render.ts). */
959
1067
  rich?: { enabled: boolean };
960
1068
  /** Opt-in rich-draft streaming of live turn previews (off by default; see rich-draft.ts). */
@@ -968,7 +1076,10 @@ export interface TelegramDaemonOptions {
968
1076
  }
969
1077
 
970
1078
  interface SessionSocket {
1079
+ /** Immutable key of the transport endpoint that owns this socket. */
971
1080
  sessionId: string;
1081
+ /** Current logical session carried by valid threaded frames on this transport. */
1082
+ logicalSessionId: string;
972
1083
  token: string;
973
1084
  endpointKey: string;
974
1085
  ws: WebSocket;
@@ -981,6 +1092,25 @@ interface SessionSocket {
981
1092
  awaitingNonce: string | undefined;
982
1093
  /** Per-session liveness interval handle (only set for capable sessions). */
983
1094
  pingTimer: ReturnType<typeof setInterval> | undefined;
1095
+ /** Correlation id for the startup replay barrier. */
1096
+ replayId: string;
1097
+ /** Queues live frames until startup replay is applied. */
1098
+ replayPending: boolean;
1099
+ replayQueue: Record<string, unknown>[];
1100
+ }
1101
+
1102
+ interface ModelChoiceRoute {
1103
+ /** Exact transport socket that rendered this choice. */
1104
+ session: SessionSocket;
1105
+ /** Logical session current when this choice was rendered. */
1106
+ sessionId: string;
1107
+ selector: string;
1108
+ expiresAt: number;
1109
+ }
1110
+
1111
+ interface RenderedModelChoice {
1112
+ selector: string;
1113
+ label: string;
984
1114
  }
985
1115
 
986
1116
  interface PendingThreadedFrame {
@@ -1017,6 +1147,9 @@ export class TelegramNotificationDaemon {
1017
1147
  /** Telegram message id backing each streamed `${sessionId}:${coalesceKey}`, for in-place edits. */
1018
1148
  private readonly liveMessages = new Map<string, number>();
1019
1149
  readonly sessions = new Map<string, SessionSocket>();
1150
+ /** Ephemeral aliases for model choices; deliberately never serialized across daemon restarts. */
1151
+ #modelChoiceAliases = new Map<string, ModelChoiceRoute>();
1152
+
1020
1153
  private readonly runtime: NotificationOperatorRuntime;
1021
1154
  private readonly sessionRouter: OperatorEventRouter<SessionSocket>;
1022
1155
  private readonly pollConflictBackoff = new OperatorBackoffPolicy({ initialMs: 500, maxMs: 5_000 });
@@ -1140,10 +1273,11 @@ export class TelegramNotificationDaemon {
1140
1273
  const token = crypto.randomBytes(32).toString("base64url");
1141
1274
  const agentDir = this.opts.settings.getAgentDir();
1142
1275
  server = factory({ token, ownerId: this.opts.ownerId, agentDir });
1143
- const deps = buildOrchestratorDeps({
1276
+ const deps = (this.opts.createLifecycleOrchestratorDeps ?? buildOrchestratorDeps)({
1144
1277
  pairedChatId: this.opts.chatId,
1145
1278
  agentNotificationsDir: daemonPaths(agentDir).dir,
1146
1279
  sessionsRoot: path.join(agentDir, "sessions"),
1280
+ auditRedactionKey: deriveLifecycleAuditRedactionKey(this.opts.botToken),
1147
1281
  });
1148
1282
  // Register the lifecycle-request handler BEFORE start(): the native
1149
1283
  // control server captures the callback at start time, so wiring must
@@ -1697,6 +1831,26 @@ export class TelegramNotificationDaemon {
1697
1831
  logger.warn(`notifications: failed to persist Telegram update id ${updateId}: ${String(err)}`);
1698
1832
  }
1699
1833
  }
1834
+ private async rememberSeenUpdateIdForUnavailableNotice(updateId: number): Promise<boolean> {
1835
+ if (!Number.isSafeInteger(updateId) || updateId < 0) return false;
1836
+ const candidate = new Set(this.dispatchState.seenUpdateIds);
1837
+ candidate.add(updateId);
1838
+ while (candidate.size > SEEN_UPDATE_ID_LIMIT) candidate.delete(candidate.values().next().value!);
1839
+ try {
1840
+ const paths = daemonPaths(this.opts.settings.getAgentDir());
1841
+ await ensureDir(this.fsImpl, paths.dir);
1842
+ await writeJsonAtomic(this.fsImpl, paths.seenUpdates, {
1843
+ version: 1,
1844
+ updateIds: [...candidate],
1845
+ });
1846
+ } catch {
1847
+ logger.warn("notifications: unavailable-control notice state publication failed");
1848
+ return false;
1849
+ }
1850
+ this.dispatchState.seenUpdateIds.clear();
1851
+ for (const seenId of candidate) this.dispatchState.seenUpdateIds.add(seenId);
1852
+ return true;
1853
+ }
1700
1854
 
1701
1855
  async scanRoots(): Promise<void> {
1702
1856
  const paths = daemonPaths(this.opts.settings.getAgentDir());
@@ -1704,7 +1858,7 @@ export class TelegramNotificationDaemon {
1704
1858
  const endpointSessionIds = new Set<string>();
1705
1859
  let allRootsReadable = true;
1706
1860
  for (const root of rootState?.roots ?? []) {
1707
- const dir = path.join(root, "notifications");
1861
+ const dir = path.join(root, "sdk");
1708
1862
  let files: string[];
1709
1863
  try {
1710
1864
  files = await this.fsImpl.readdir(dir);
@@ -1748,8 +1902,14 @@ export class TelegramNotificationDaemon {
1748
1902
  const ws = new WS(`${url}/?token=${encodeURIComponent(token)}`);
1749
1903
  const endpointKey = endpointGenerationKey(url, token);
1750
1904
  this.closedEndpointKeys.delete(sessionId);
1905
+ const existing = this.sessions.get(sessionId);
1906
+ if (existing) this.#clearModelChoiceAliasesForSocket(existing);
1907
+ this.#clearModelChoiceAliases(sessionId);
1908
+
1751
1909
  const session: SessionSocket = {
1752
1910
  sessionId,
1911
+ logicalSessionId: sessionId,
1912
+
1753
1913
  token,
1754
1914
  endpointKey,
1755
1915
  ws,
@@ -1758,12 +1918,18 @@ export class TelegramNotificationDaemon {
1758
1918
  lastPongAt: 0,
1759
1919
  awaitingNonce: undefined,
1760
1920
  pingTimer: undefined,
1921
+ replayId: `telegram-startup-replay:${sessionId}`,
1922
+ replayPending: false,
1923
+ replayQueue: [],
1761
1924
  };
1762
1925
  this.sessions.set(sessionId, session);
1763
1926
  // Bidirectional capability advertisement: announce client_ping_pong once the
1764
1927
  // socket is open. Sent on "open" only — a real WHATWG WebSocket cannot send
1765
1928
  // while CONNECTING — and liveness starts only after a capable ServerHello.
1766
1929
  ws.addEventListener("open", () => {
1930
+ session.replayPending = true;
1931
+ session.replayQueue = [];
1932
+ const replayCursor = this.topics.replayCursor(sessionId);
1767
1933
  if (session.ws.readyState === WebSocket.OPEN) {
1768
1934
  try {
1769
1935
  session.ws.send(
@@ -1774,6 +1940,16 @@ export class TelegramNotificationDaemon {
1774
1940
  }),
1775
1941
  );
1776
1942
  } catch {}
1943
+ try {
1944
+ session.ws.send(
1945
+ JSON.stringify({
1946
+ type: "event_replay",
1947
+ id: session.replayId,
1948
+ sinceGeneration: replayCursor?.generation ?? 1,
1949
+ sinceSeq: replayCursor?.seq ?? 0,
1950
+ }),
1951
+ );
1952
+ } catch {}
1777
1953
  }
1778
1954
  // Eagerly create the session's Telegram topic as soon as it connects, so
1779
1955
  // a thread exists the moment a notifications-enabled session is live —
@@ -1843,8 +2019,10 @@ export class TelegramNotificationDaemon {
1843
2019
  this.deleteMessageRoutes(session.sessionId);
1844
2020
  }
1845
2021
  if (isCurrentSession) {
2022
+ this.#clearModelChoiceAliasesForSocket(session);
1846
2023
  this.sessions.delete(session.sessionId);
1847
2024
  }
2025
+
1848
2026
  for (const item of new Set(this.selectedAckPending.values())) {
1849
2027
  if (item.session !== session) continue;
1850
2028
  if (item.state === "queued") this.pool.removeById(item.itemId);
@@ -1866,6 +2044,39 @@ export class TelegramNotificationDaemon {
1866
2044
  }
1867
2045
  }
1868
2046
 
2047
+ #logicalSessionId(session: SessionSocket): string {
2048
+ return session.logicalSessionId ?? session.sessionId;
2049
+ }
2050
+
2051
+ #clearModelChoiceAliases(sessionId: string): void {
2052
+ for (const [alias, route] of this.#modelChoiceAliases) {
2053
+ if (route.sessionId === sessionId) this.#modelChoiceAliases.delete(alias);
2054
+ }
2055
+ }
2056
+
2057
+ #clearModelChoiceAliasesForSocket(session: SessionSocket): void {
2058
+ for (const [alias, route] of this.#modelChoiceAliases) {
2059
+ if (route.session === session) this.#modelChoiceAliases.delete(alias);
2060
+ }
2061
+ }
2062
+
2063
+ #sweepExpiredModelChoiceAliases(): void {
2064
+ const now = this.runtime.now();
2065
+ for (const [alias, route] of this.#modelChoiceAliases) {
2066
+ if (route.expiresAt <= now) this.#modelChoiceAliases.delete(alias);
2067
+ }
2068
+ }
2069
+
2070
+ #putModelChoiceAlias(route: Omit<ModelChoiceRoute, "expiresAt">): string {
2071
+ this.#sweepExpiredModelChoiceAliases();
2072
+ let alias: string;
2073
+ do {
2074
+ alias = `${MODEL_CALLBACK_PREFIX}${crypto.randomBytes(16).toString("base64url")}`;
2075
+ } while (this.#modelChoiceAliases.has(alias));
2076
+ this.#modelChoiceAliases.set(alias, { ...route, expiresAt: this.runtime.now() + MODEL_CHOICE_TTL_MS });
2077
+ return alias;
2078
+ }
2079
+
1869
2080
  private static readonly THREADED_FRAMES = new Set([
1870
2081
  "identity_header",
1871
2082
  "context_update",
@@ -1876,6 +2087,22 @@ export class TelegramNotificationDaemon {
1876
2087
  "control_command_result",
1877
2088
  ]);
1878
2089
 
2090
+ /** Rekey model controls before a valid threaded frame can render or a silent config update can route output. */
2091
+ #updateLogicalSessionForThreadedFrame(session: SessionSocket, msg: Record<string, unknown>): void {
2092
+ if (
2093
+ typeof msg.type !== "string" ||
2094
+ !TelegramNotificationDaemon.THREADED_FRAMES.has(msg.type) ||
2095
+ typeof msg.sessionId !== "string" ||
2096
+ !msg.sessionId.trim() ||
2097
+ msg.sessionId === this.#logicalSessionId(session) ||
2098
+ (msg.type !== "config_update" && !renderThreadedFrame(msg))
2099
+ )
2100
+ return;
2101
+ this.#clearModelChoiceAliasesForSocket(session);
2102
+ this.#clearModelChoiceAliases(msg.sessionId);
2103
+ session.logicalSessionId = msg.sessionId;
2104
+ }
2105
+
1879
2106
  private topicNameFor(sessionId: string, msg: { title?: unknown; repo?: unknown; branch?: unknown }): string {
1880
2107
  const repo = typeof msg?.repo === "string" && msg.repo ? msg.repo : undefined;
1881
2108
  const branch = typeof msg?.branch === "string" && msg.branch ? msg.branch : undefined;
@@ -2134,7 +2361,7 @@ export class TelegramNotificationDaemon {
2134
2361
  if (!res.ok) return undefined;
2135
2362
  return Buffer.from(await res.arrayBuffer());
2136
2363
  } catch (e) {
2137
- logger.warn(`notifications: file download failed: ${String(e)}`);
2364
+ logger.warn(`notifications: file download failed: ${sanitizeDiagnostic(String(e))}`);
2138
2365
  return undefined;
2139
2366
  }
2140
2367
  }
@@ -2664,8 +2891,129 @@ export class TelegramNotificationDaemon {
2664
2891
  this.runtime.stopInterval("telegram-typing");
2665
2892
  }
2666
2893
 
2894
+ /** Render successful `/model` lists as session-bound, one-shot inline choices. */
2895
+ async #renderModelChoices(session: SessionSocket, msg: Record<string, unknown>): Promise<boolean> {
2896
+ const logicalSessionId = this.#logicalSessionId(session);
2897
+ if (
2898
+ msg.type !== "control_command_result" ||
2899
+ msg.status !== "ok" ||
2900
+ msg.sessionId !== logicalSessionId ||
2901
+ !Array.isArray(msg.modelChoices) ||
2902
+ this.sessions.get(session.sessionId) !== session
2903
+ )
2904
+ return false;
2905
+
2906
+ const choices: RenderedModelChoice[] = [];
2907
+ for (const choice of msg.modelChoices) {
2908
+ if (!choice || typeof choice !== "object") continue;
2909
+ const { selector, label } = choice as { selector?: unknown; label?: unknown };
2910
+ const safeLabel = safeModelButtonLabel(label);
2911
+ if (typeof selector !== "string" || !selector.trim() || !safeLabel) continue;
2912
+ choices.push({ selector, label: safeLabel });
2913
+ }
2914
+ if (choices.length === 0) return false;
2915
+
2916
+ const rendered = renderThreadedFrame({ ...msg, type: "control_command_result" });
2917
+ if (!rendered?.text) return false;
2918
+ const topicId =
2919
+ (await this.existingTopicForPrivateChat(session.sessionId)) ??
2920
+ (await this.ensureTopic(session.sessionId, this.topicNameFor(session.sessionId, msg)));
2921
+ if (!topicId) return false;
2922
+
2923
+ // Each logical session owns only its most recently rendered menu.
2924
+ this.#clearModelChoiceAliases(logicalSessionId);
2925
+ const aliases = choices.map(choice =>
2926
+ this.#putModelChoiceAlias({ session, sessionId: logicalSessionId, selector: choice.selector }),
2927
+ );
2928
+ const inline_keyboard = buildButtonGrid(
2929
+ choices.map(choice => choice.label),
2930
+ index => aliases[index]!,
2931
+ );
2932
+ try {
2933
+ const response = await this.botApi.call("sendMessage", {
2934
+ chat_id: this.opts.chatId,
2935
+ message_thread_id: Number(topicId),
2936
+ text: rendered.text,
2937
+ parse_mode: TELEGRAM_PARSE_MODE,
2938
+ reply_markup: { inline_keyboard },
2939
+ });
2940
+ if (response && typeof response === "object" && (response as { ok?: unknown }).ok === false) {
2941
+ for (const alias of aliases) this.#modelChoiceAliases.delete(alias);
2942
+ logger.warn("notifications: failed to send model selection keyboard");
2943
+ return false;
2944
+ }
2945
+ } catch {
2946
+ for (const alias of aliases) this.#modelChoiceAliases.delete(alias);
2947
+ logger.warn("notifications: failed to send model selection keyboard");
2948
+ return false;
2949
+ }
2950
+ return true;
2951
+ }
2952
+
2667
2953
  async handleSessionMessage(session: SessionSocket, msg: any): Promise<void> {
2954
+ if (session.replayPending) {
2955
+ const matchingReplay = msg?.type === "event_replay_result" && msg.id === session.replayId;
2956
+ if (!matchingReplay) {
2957
+ session.replayQueue.push(msg as Record<string, unknown>);
2958
+ return;
2959
+ }
2960
+ session.replayPending = false;
2961
+ const replayValid =
2962
+ Number.isSafeInteger(msg.generation) &&
2963
+ msg.generation >= 1 &&
2964
+ Number.isSafeInteger(msg.lastSeq) &&
2965
+ msg.lastSeq >= 0 &&
2966
+ Array.isArray(msg.events);
2967
+ const replayed: Record<string, unknown>[] = replayValid
2968
+ ? (msg.events as unknown[]).flatMap((event: unknown): Record<string, unknown>[] => {
2969
+ if (!event || typeof event !== "object" || Array.isArray(event)) return [];
2970
+ const envelope = event as Record<string, unknown>;
2971
+ const payload = envelope.payload;
2972
+ return [
2973
+ payload && typeof payload === "object" && !Array.isArray(payload)
2974
+ ? (payload as Record<string, unknown>)
2975
+ : envelope,
2976
+ ];
2977
+ })
2978
+ : [];
2979
+ // Replay restores durable attachment state only. Live notification effects
2980
+ // (turn streams, context updates, lifecycle messages) may already have been
2981
+ // delivered before a reconnect and must never be rendered a second time.
2982
+ const identityIndex = replayed.findLastIndex(frame => frame.type === "identity_header");
2983
+ const currentGeneration = identityIndex < 0 ? replayed : replayed.slice(identityIndex);
2984
+ const latestIdentity = identityIndex < 0 ? undefined : replayed[identityIndex];
2985
+ const latestActions = new Map<string, Record<string, unknown>>();
2986
+ for (const frame of currentGeneration) {
2987
+ if ((frame.type === "action_needed" || frame.type === "action_resolved") && typeof frame.id === "string")
2988
+ latestActions.set(frame.id, frame);
2989
+ }
2990
+ const replayState = [...(latestIdentity ? [latestIdentity] : []), ...latestActions.values()];
2991
+ const replayCounts = new Map<string, number>();
2992
+ for (const frame of replayState) {
2993
+ const fingerprint = JSON.stringify(frame);
2994
+ replayCounts.set(fingerprint, (replayCounts.get(fingerprint) ?? 0) + 1);
2995
+ await this.handleSessionMessage(session, frame);
2996
+ }
2997
+ const queued = session.replayQueue.splice(0);
2998
+ for (const frame of queued) {
2999
+ const fingerprint = JSON.stringify(frame);
3000
+ const remaining = replayCounts.get(fingerprint) ?? 0;
3001
+ if (remaining > 0) {
3002
+ if (remaining === 1) replayCounts.delete(fingerprint);
3003
+ else replayCounts.set(fingerprint, remaining - 1);
3004
+ continue;
3005
+ }
3006
+ await this.handleSessionMessage(session, frame);
3007
+ }
3008
+ if (replayValid && this.topics.markReplayCursor(session.sessionId, msg.generation, msg.lastSeq))
3009
+ await this.persistTopics();
3010
+ return;
3011
+ }
3012
+ if (msg?.type === "event_replay_result") return;
3013
+ if (msg && typeof msg === "object") this.#updateLogicalSessionForThreadedFrame(session, msg);
2668
3014
  if (await this.sessionRouter.dispatch(session, msg as Record<string, unknown>)) return;
3015
+ if (await this.#renderModelChoices(session, msg as Record<string, unknown>)) return;
3016
+
2669
3017
  if (typeof msg?.type === "string" && TelegramNotificationDaemon.THREADED_FRAMES.has(msg.type)) {
2670
3018
  const send = renderThreadedFrame(msg);
2671
3019
  if (!send) return;
@@ -2864,6 +3212,80 @@ export class TelegramNotificationDaemon {
2864
3212
  }
2865
3213
  }
2866
3214
 
3215
+ /** Claim and forward one session-bound model alias before acknowledging its callback. */
3216
+ async #handleModelChoiceCallback(update: unknown, callbackId: unknown): Promise<boolean> {
3217
+ const callback = (
3218
+ update as {
3219
+ callback_query?: { data?: unknown; message?: { chat?: { id?: unknown } } };
3220
+ }
3221
+ ).callback_query;
3222
+ const alias = callback?.data;
3223
+ if (typeof alias !== "string" || !alias.startsWith(MODEL_CALLBACK_PREFIX)) return false;
3224
+ this.#sweepExpiredModelChoiceAliases();
3225
+ if (String(callback?.message?.chat?.id) !== String(this.opts.chatId)) {
3226
+ await this.answerCallbackQueryBestEffort(callbackId, "Not authorized");
3227
+ return true;
3228
+ }
3229
+ if (!(await this.pairedChatIsPrivate())) {
3230
+ await this.answerCallbackQueryBestEffort(callbackId, "Not authorized");
3231
+ return true;
3232
+ }
3233
+
3234
+ const route = this.#modelChoiceAliases.get(alias);
3235
+ if (!route) {
3236
+ await this.#sendModelStaleGuidance(callbackId);
3237
+ return true;
3238
+ }
3239
+ const session = route.session;
3240
+ if (
3241
+ this.sessions.get(session.sessionId) !== session ||
3242
+ this.#logicalSessionId(session) !== route.sessionId ||
3243
+ session.ws.readyState !== WebSocket.OPEN
3244
+ ) {
3245
+ this.#modelChoiceAliases.delete(alias);
3246
+ await this.#sendModelStaleGuidance(callbackId);
3247
+ return true;
3248
+ }
3249
+
3250
+ // Delete before send so a duplicate tap can only become stale, never duplicate a control command.
3251
+ this.#modelChoiceAliases.delete(alias);
3252
+ const updateId = (update as { update_id?: unknown }).update_id;
3253
+ const safeUpdateId =
3254
+ typeof updateId === "number" && Number.isSafeInteger(updateId) && updateId >= 0 ? updateId : undefined;
3255
+ try {
3256
+ session.ws.send(
3257
+ JSON.stringify({
3258
+ type: "control_command",
3259
+ sessionId: route.sessionId,
3260
+ token: session.token,
3261
+ requestId: safeUpdateId === undefined ? `tg:model:${alias}` : `tg:model:${safeUpdateId}`,
3262
+ ...(safeUpdateId === undefined ? {} : { updateId: safeUpdateId }),
3263
+ command: { name: "model", action: "set", selector: route.selector },
3264
+ }),
3265
+ );
3266
+ } catch {
3267
+ await this.#sendModelStaleGuidance(callbackId);
3268
+ return true;
3269
+ }
3270
+ await this.answerCallbackQueryBestEffort(callbackId);
3271
+ return true;
3272
+ }
3273
+
3274
+ async #sendModelStaleGuidance(callbackId: unknown): Promise<void> {
3275
+ const text = "Button is stale. Run /model again.";
3276
+ await this.answerCallbackQueryBestEffort(callbackId, text);
3277
+ if (!(await this.pairedChatIsPrivate())) return;
3278
+ try {
3279
+ await this.botApi.call("sendMessage", {
3280
+ chat_id: this.opts.chatId,
3281
+ text,
3282
+ parse_mode: TELEGRAM_PARSE_MODE,
3283
+ });
3284
+ } catch {
3285
+ // Best-effort stale guidance must not reject a callback.
3286
+ }
3287
+ }
3288
+
2867
3289
  private async sendStaleGuidance(callbackId: unknown): Promise<void> {
2868
3290
  await this.answerCallbackQueryBestEffort(callbackId, "Button is stale");
2869
3291
  if (!(await this.pairedChatIsPrivate())) return;
@@ -3040,7 +3462,7 @@ export class TelegramNotificationDaemon {
3040
3462
  // update_id dedupe are all enforced by decideThreadedInbound.
3041
3463
  const raw = update as {
3042
3464
  callback_query?: unknown;
3043
- message?: { reply_to_message?: { message_id?: unknown } };
3465
+ message?: { text?: unknown; reply_to_message?: { message_id?: unknown } };
3044
3466
  };
3045
3467
  // A reply to a known ask message routes to that ask (below). Any OTHER
3046
3468
  // message in a topic (plain text, or a reply to a non-ask message) is a
@@ -3048,7 +3470,12 @@ export class TelegramNotificationDaemon {
3048
3470
  const replyTo = raw.message?.reply_to_message?.message_id;
3049
3471
  const isAskReply =
3050
3472
  replyTo !== undefined && (this.messageRoutes.has(String(replyTo)) || this.messageRoutes.has(Number(replyTo)));
3051
- if (!raw.callback_query && !isAskReply) {
3473
+ const directControl =
3474
+ typeof raw.message?.text === "string"
3475
+ ? parseTelegramControlCommand(raw.message.text, this.botUsername)
3476
+ : ({ kind: "none" } as const);
3477
+ const interceptAskControl = isAskReply && directControl.kind !== "none";
3478
+ if (!raw.callback_query && (!isAskReply || interceptAskControl)) {
3052
3479
  const inbound = decideThreadedInbound(update as never, {
3053
3480
  pairedChatId: this.opts.chatId,
3054
3481
  topicToSession: t => this.topics.sessionForTopic(t),
@@ -3056,7 +3483,27 @@ export class TelegramNotificationDaemon {
3056
3483
  });
3057
3484
  if (inbound.kind === "duplicate") return;
3058
3485
  if (inbound.kind === "inject") {
3486
+ if (!(await this.pairedChatIsPrivate())) return;
3487
+ const preliminaryControl = inbound.attachment
3488
+ ? { kind: "none" as const }
3489
+ : parseTelegramControlCommand(inbound.text, this.botUsername);
3490
+ if (preliminaryControl.kind === "ignored") return;
3059
3491
  const session = this.sessions.get(inbound.sessionId);
3492
+ if (preliminaryControl.kind === "invalid" && session?.ws.readyState !== WebSocket.OPEN) return;
3493
+ if (preliminaryControl.kind === "command" && session?.ws.readyState !== WebSocket.OPEN) {
3494
+ if (await this.rememberSeenUpdateIdForUnavailableNotice(inbound.updateId)) {
3495
+ try {
3496
+ await this.botApi.call("sendMessage", {
3497
+ chat_id: this.opts.chatId,
3498
+ message_thread_id: Number(inbound.threadId),
3499
+ text: "Session control unavailable: this local GJC session is disconnected.",
3500
+ });
3501
+ } catch {
3502
+ logger.warn("notifications: unavailable-control notice delivery failed");
3503
+ }
3504
+ }
3505
+ return;
3506
+ }
3060
3507
  if (session?.ws.readyState === WebSocket.OPEN) {
3061
3508
  const attachmentResult = inbound.attachment
3062
3509
  ? await this.resolveInboundAttachment(inbound.attachment, inbound.sessionId)
@@ -3075,9 +3522,7 @@ export class TelegramNotificationDaemon {
3075
3522
  const injectedText = repliedOriginal
3076
3523
  ? `> replied-to message:\n${repliedOriginal}\n\n${baseInjectedText}`
3077
3524
  : baseInjectedText;
3078
- const control = hasMedia
3079
- ? { kind: "none" as const }
3080
- : parseTelegramControlCommand(inbound.text, this.botUsername);
3525
+ const control = hasMedia ? { kind: "none" as const } : preliminaryControl;
3081
3526
  if (control.kind !== "none") {
3082
3527
  await this.rememberSeenUpdateId(inbound.updateId);
3083
3528
  const sendControlNotice = async (body: string): Promise<void> => {
@@ -3092,7 +3537,6 @@ export class TelegramNotificationDaemon {
3092
3537
  // Best-effort control feedback; never convert to user input.
3093
3538
  }
3094
3539
  };
3095
- if (control.kind === "ignored") return;
3096
3540
  if (control.kind === "invalid") {
3097
3541
  await sendControlNotice(control.usage);
3098
3542
  return;
@@ -3104,7 +3548,7 @@ export class TelegramNotificationDaemon {
3104
3548
  session.ws.send(
3105
3549
  JSON.stringify({
3106
3550
  type: "control_command",
3107
- sessionId: inbound.sessionId,
3551
+ sessionId: this.#logicalSessionId(session),
3108
3552
  token: session.token,
3109
3553
  requestId: `tg:${inbound.updateId}`,
3110
3554
  updateId: inbound.updateId,
@@ -3161,6 +3605,7 @@ export class TelegramNotificationDaemon {
3161
3605
  }
3162
3606
  }
3163
3607
  const callbackId = (update as { callback_query?: { id?: unknown } }).callback_query?.id;
3608
+ if (await this.#handleModelChoiceCallback(update, callbackId)) return;
3164
3609
  const decision = routeInboundUpdate(update, {
3165
3610
  aliasTable: this.aliasTable,
3166
3611
  messageRoutes: this.messageRoutes,
@@ -3196,6 +3641,7 @@ export class TelegramNotificationDaemon {
3196
3641
  { command: "rich", description: "Toggle rich Telegram delivery: /rich <on|off>" },
3197
3642
  { command: "reasoning", description: "Show or change reasoning effort in this session" },
3198
3643
  { command: "usage", description: "Show provider/local usage for this session" },
3644
+ { command: "model", description: "Select a model for this session" },
3199
3645
  { command: "context", description: "Show current context usage for this session" },
3200
3646
  { command: "compact", description: "Compact this session: /compact [instructions]" },
3201
3647
  { command: "session_create", description: "Create a GJC session: path, worktree, or dir [--mpreset]" },
@@ -3268,7 +3714,9 @@ export class TelegramNotificationDaemon {
3268
3714
  // Back off (bounded, below the heartbeat TTL) and keep renewing
3269
3715
  // ownership at the loop top.
3270
3716
  const backoffMs = this.loopBackoff.next();
3271
- logger.warn(`notifications: getUpdates failed, backing off ${backoffMs}ms: ${String(e)}`);
3717
+ logger.warn(
3718
+ `notifications: getUpdates failed, backing off ${backoffMs}ms: ${sanitizeDiagnostic(String(e))}`,
3719
+ );
3272
3720
  await this.runtime.sleep(backoffMs);
3273
3721
  continue;
3274
3722
  } finally {