@sayknow-cli/coding-agent 0.4.7 → 0.5.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 (428) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/cli/read-cli.d.ts +1 -0
  6. package/dist/types/cli.d.ts +9 -1
  7. package/dist/types/commands/daemon.d.ts +2 -2
  8. package/dist/types/commands/deep-interview.d.ts +9 -0
  9. package/dist/types/commands/harness.d.ts +22 -0
  10. package/dist/types/commands/read.d.ts +6 -0
  11. package/dist/types/commands/sdk.d.ts +20 -2
  12. package/dist/types/config/keybindings.d.ts +34 -4
  13. package/dist/types/config/model-profile-contract.d.ts +40 -0
  14. package/dist/types/config/model-profiles.d.ts +2 -3
  15. package/dist/types/config/model-registry.d.ts +5 -0
  16. package/dist/types/config/provider-auth-health.d.ts +14 -0
  17. package/dist/types/config/provider-ranking.d.ts +59 -0
  18. package/dist/types/config/settings-schema.d.ts +156 -10
  19. package/dist/types/config/settings.d.ts +3 -0
  20. package/dist/types/edit/modes/patch.d.ts +13 -0
  21. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  22. package/dist/types/eval/py/executor.d.ts +1 -1
  23. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  24. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  25. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  26. package/dist/types/extensibility/shared-events.d.ts +6 -0
  27. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  28. package/dist/types/main.d.ts +33 -2
  29. package/dist/types/memories/index.d.ts +2 -0
  30. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  31. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  32. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  35. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  36. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  37. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  38. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  39. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  41. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  42. package/dist/types/modes/components/welcome.d.ts +5 -0
  43. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  45. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  46. package/dist/types/modes/interactive-mode.d.ts +13 -2
  47. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  48. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  49. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  50. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  51. package/dist/types/modes/types.d.ts +6 -0
  52. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  53. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  54. package/dist/types/runtime/memory-domain.d.ts +9 -0
  55. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  56. package/dist/types/runtime/memory-guard.d.ts +39 -0
  57. package/dist/types/runtime/memory-limit.d.ts +11 -0
  58. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  59. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
  60. package/dist/types/runtime-mcp/manager.d.ts +9 -1
  61. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  62. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  63. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  64. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  65. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  66. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  67. package/dist/types/sdk/broker/broker.d.ts +2 -0
  68. package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
  69. package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
  70. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  71. package/dist/types/sdk/bus/config.d.ts +2 -0
  72. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  73. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  74. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  75. package/dist/types/sdk/bus/index.d.ts +52 -4
  76. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  77. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  78. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  79. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  80. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  81. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
  82. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  83. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  84. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
  85. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  86. package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
  87. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  88. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  89. package/dist/types/sdk/client/discovery.d.ts +2 -0
  90. package/dist/types/sdk/client/liveness.d.ts +7 -0
  91. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  92. package/dist/types/sdk/host/host.d.ts +3 -1
  93. package/dist/types/sdk/host/query/handlers.d.ts +14 -0
  94. package/dist/types/sdk/index.d.ts +2 -0
  95. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  96. package/dist/types/sdk/prompt-status.d.ts +88 -0
  97. package/dist/types/sdk/session-directory.d.ts +1 -1
  98. package/dist/types/sdk/session.d.ts +7 -0
  99. package/dist/types/sdk/startup-capability.d.ts +9 -0
  100. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  101. package/dist/types/sdk/transport/index.d.ts +12 -0
  102. package/dist/types/sdk/transport/relay.d.ts +43 -0
  103. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  104. package/dist/types/sdk/transport/socket.d.ts +7 -0
  105. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  106. package/dist/types/session/agent-session.d.ts +83 -24
  107. package/dist/types/session/btw-contract.d.ts +16 -0
  108. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  109. package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
  110. package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
  111. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  112. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  113. package/dist/types/session/session-manager.d.ts +89 -18
  114. package/dist/types/session/session-storage.d.ts +31 -0
  115. package/dist/types/session/streaming-output.d.ts +60 -5
  116. package/dist/types/setup/credential-import.d.ts +5 -1
  117. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  118. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
  119. package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
  120. package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
  121. package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
  122. package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
  123. package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
  124. package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
  125. package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
  126. package/dist/types/skc-runtime/state-writer.d.ts +5 -1
  127. package/dist/types/skc-runtime/team-launch.d.ts +1 -1
  128. package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
  129. package/dist/types/skc-runtime/team-store.d.ts +12 -0
  130. package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
  131. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
  132. package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
  133. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
  134. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
  135. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  136. package/dist/types/ssh/utils.d.ts +1 -0
  137. package/dist/types/task/discovery.d.ts +3 -1
  138. package/dist/types/task/executor.d.ts +6 -0
  139. package/dist/types/task/index.d.ts +3 -1
  140. package/dist/types/task/provider-retry-status.d.ts +13 -0
  141. package/dist/types/task/receipt.d.ts +2 -0
  142. package/dist/types/task/types.d.ts +133 -0
  143. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  144. package/dist/types/tools/browser/launch.d.ts +7 -0
  145. package/dist/types/tools/fetch.d.ts +22 -9
  146. package/dist/types/tools/image-gen.d.ts +63 -1
  147. package/dist/types/tools/index.d.ts +7 -1
  148. package/dist/types/tools/output-meta.d.ts +21 -1
  149. package/dist/types/tools/read-internals.d.ts +12 -0
  150. package/dist/types/tools/read.d.ts +45 -1
  151. package/dist/types/tools/resource-gc.d.ts +23 -0
  152. package/dist/types/tools/skill-discovery.d.ts +7 -0
  153. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  154. package/dist/types/tools/tool-result.d.ts +7 -1
  155. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  156. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  157. package/dist/types/web/insane/bridge.d.ts +1 -0
  158. package/dist/types/web/insane/url-guard.d.ts +17 -0
  159. package/package.json +8 -12
  160. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  161. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  162. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  163. package/scripts/compile-args.ts +0 -1
  164. package/scripts/dogfood-repository-binding.ts +209 -0
  165. package/scripts/generate-docs-index.ts +4 -1
  166. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  167. package/scripts/run-test-manifest.ts +6 -4
  168. package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
  169. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  170. package/src/capability/index.ts +58 -42
  171. package/src/capability/types.ts +3 -0
  172. package/src/cli/args.ts +16 -100
  173. package/src/cli/fast-help.ts +1 -1
  174. package/src/cli/mcp-cli.ts +4 -21
  175. package/src/cli/notify-cli.ts +6 -38
  176. package/src/cli/read-cli.ts +2 -1
  177. package/src/cli/setup-cli.ts +10 -4
  178. package/src/cli.ts +133 -11
  179. package/src/commands/daemon.ts +22 -5
  180. package/src/commands/deep-interview.ts +26 -1
  181. package/src/commands/harness.ts +35 -12
  182. package/src/commands/read.ts +11 -2
  183. package/src/commands/sdk.ts +134 -17
  184. package/src/commands/team.ts +1 -1
  185. package/src/commit/agentic/index.ts +3 -3
  186. package/src/commit/map-reduce/index.ts +2 -2
  187. package/src/config/keybindings.ts +158 -25
  188. package/src/config/model-profile-contract.ts +174 -0
  189. package/src/config/model-profiles.ts +51 -10
  190. package/src/config/model-registry.ts +24 -4
  191. package/src/config/model-resolver.ts +2 -2
  192. package/src/config/provider-auth-health.ts +42 -0
  193. package/src/config/provider-ranking.ts +119 -0
  194. package/src/config/settings-schema.ts +157 -10
  195. package/src/config/settings.ts +254 -108
  196. package/src/coordinator-mcp/model-preset.ts +21 -74
  197. package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
  198. package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
  199. package/src/defaults/skc/skills/team/SKILL.md +1 -1
  200. package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
  201. package/src/discovery/ssh.ts +12 -1
  202. package/src/edit/index.ts +3 -6
  203. package/src/edit/modes/patch.ts +38 -1
  204. package/src/edit/modes/replace.ts +42 -0
  205. package/src/edit/path-mutation-lock.ts +112 -0
  206. package/src/eval/js/tool-bridge.ts +1 -1
  207. package/src/eval/py/executor.ts +37 -13
  208. package/src/eval/py/kernel.ts +1 -21
  209. package/src/eval/py/prelude.py +2 -2
  210. package/src/eval/py/runner-artifact.ts +84 -0
  211. package/src/eval/py/tool-bridge.ts +34 -18
  212. package/src/extensibility/extensions/runner.ts +4 -0
  213. package/src/extensibility/extensions/types.ts +42 -4
  214. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  215. package/src/extensibility/shared-events.ts +5 -0
  216. package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
  217. package/src/hooks/native-skill-hook.ts +17 -4
  218. package/src/internal-urls/docs-index.generated.ts +17 -16
  219. package/src/internal-urls/local-protocol.ts +87 -25
  220. package/src/internal-urls/local-root-gc.ts +127 -0
  221. package/src/internal-urls/mcp-protocol.ts +73 -11
  222. package/src/main.ts +120 -42
  223. package/src/memories/index.ts +10 -0
  224. package/src/modes/DESIGN.md +52 -0
  225. package/src/modes/acp/acp-agent.ts +647 -83
  226. package/src/modes/acp/acp-event-mapper.ts +106 -12
  227. package/src/modes/acp/acp-mode.ts +39 -3
  228. package/src/modes/action-registry.ts +1 -1
  229. package/src/modes/components/assistant-message.ts +123 -22
  230. package/src/modes/components/btw-panel.ts +82 -16
  231. package/src/modes/components/irc-sidebar.ts +176 -62
  232. package/src/modes/components/model-selector.ts +61 -4
  233. package/src/modes/components/notifications-settings-editor.ts +1 -1
  234. package/src/modes/components/oauth-selector.ts +42 -12
  235. package/src/modes/components/plan-preview-overlay.ts +1 -1
  236. package/src/modes/components/queue-pane.ts +55 -9
  237. package/src/modes/components/queued-message-selector.ts +53 -10
  238. package/src/modes/components/read-tool-group.ts +82 -14
  239. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  240. package/src/modes/components/sayknow-pet-widget.ts +20 -8
  241. package/src/modes/components/session-observer-overlay.ts +187 -32
  242. package/src/modes/components/settings-selector.ts +60 -18
  243. package/src/modes/components/tool-execution.ts +138 -27
  244. package/src/modes/components/tool-status-header.ts +26 -8
  245. package/src/modes/components/welcome.ts +50 -23
  246. package/src/modes/controllers/btw-controller.ts +119 -36
  247. package/src/modes/controllers/command-controller.ts +101 -54
  248. package/src/modes/controllers/event-controller.ts +119 -27
  249. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  250. package/src/modes/controllers/input-controller.ts +100 -8
  251. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  252. package/src/modes/controllers/selector-controller.ts +4 -1
  253. package/src/modes/interactive-mode.ts +201 -60
  254. package/src/modes/irc-observation-ledger.ts +71 -15
  255. package/src/modes/prompt-action-autocomplete.ts +32 -13
  256. package/src/modes/prompt-suggestion-controller.ts +96 -0
  257. package/src/modes/runtime-init.ts +20 -2
  258. package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
  259. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  260. package/src/modes/types.ts +6 -0
  261. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  262. package/src/modes/utils/ui-helpers.ts +99 -4
  263. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  264. package/src/prompts/agents/architect.md +17 -3
  265. package/src/prompts/agents/critic.md +17 -2
  266. package/src/prompts/agents/executor.md +1 -1
  267. package/src/prompts/agents/planner.md +8 -1
  268. package/src/prompts/system/btw-user.md +3 -8
  269. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  270. package/src/prompts/system/system-prompt.md +6 -3
  271. package/src/prompts/tools/cron.md +3 -1
  272. package/src/prompts/tools/read.md +4 -2
  273. package/src/prompts/tools/skill-discovery.md +1 -0
  274. package/src/runtime/memory-domain.ts +66 -0
  275. package/src/runtime/memory-guard-contract.ts +73 -0
  276. package/src/runtime/memory-guard.ts +292 -0
  277. package/src/runtime/memory-limit.ts +50 -0
  278. package/src/runtime-mcp/client.ts +41 -55
  279. package/src/runtime-mcp/content-limits.ts +71 -0
  280. package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
  281. package/src/runtime-mcp/json-rpc.ts +28 -11
  282. package/src/runtime-mcp/manager.ts +77 -59
  283. package/src/runtime-mcp/oauth-flow.ts +0 -72
  284. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  285. package/src/runtime-mcp/redaction.ts +30 -0
  286. package/src/runtime-mcp/smithery-auth.ts +17 -2
  287. package/src/runtime-mcp/smithery-connect.ts +11 -1
  288. package/src/runtime-mcp/transports/http.ts +63 -22
  289. package/src/sdk/acp/adapter.ts +8 -1
  290. package/src/sdk/acp/final-text.ts +40 -0
  291. package/src/sdk/acp/mcp.ts +26 -0
  292. package/src/sdk/broker/broker.ts +34 -20
  293. package/src/sdk/broker/identity.ts +22 -12
  294. package/src/sdk/broker/lifecycle.ts +293 -43
  295. package/src/sdk/broker/session-index.ts +11 -1
  296. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  297. package/src/sdk/bus/chat-daemon-control.ts +22 -5
  298. package/src/sdk/bus/config-commands.ts +2 -2
  299. package/src/sdk/bus/config.ts +10 -2
  300. package/src/sdk/bus/control-drain-lease.ts +104 -0
  301. package/src/sdk/bus/conversation-store.ts +40 -6
  302. package/src/sdk/bus/daemon-paths.ts +2 -0
  303. package/src/sdk/bus/index.ts +793 -121
  304. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  305. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  306. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  307. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  308. package/src/sdk/bus/notification-orchestration.ts +12 -0
  309. package/src/sdk/bus/notification-service.ts +87 -25
  310. package/src/sdk/bus/operator-runtime.ts +31 -5
  311. package/src/sdk/bus/prompt-reconciliation.ts +240 -0
  312. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  313. package/src/sdk/bus/recent-activity.ts +90 -26
  314. package/src/sdk/bus/reconciliation-store.ts +224 -0
  315. package/src/sdk/bus/telegram-cli.ts +6 -13
  316. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  317. package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
  318. package/src/sdk/bus/telegram-daemon-control.ts +161 -40
  319. package/src/sdk/bus/telegram-daemon.ts +3649 -593
  320. package/src/sdk/bus/telegram-reference.ts +20 -2
  321. package/src/sdk/bus/topic-registry.ts +451 -12
  322. package/src/sdk/cli/session-cli.ts +15 -2
  323. package/src/sdk/client/discovery.ts +19 -3
  324. package/src/sdk/client/liveness.ts +49 -0
  325. package/src/sdk/host/control/dispatch.ts +8 -2
  326. package/src/sdk/host/control/operations.ts +3 -2
  327. package/src/sdk/host/host.ts +27 -9
  328. package/src/sdk/host/query/handlers.ts +125 -5
  329. package/src/sdk/index.ts +12 -0
  330. package/src/sdk/lifecycle-session.ts +19 -3
  331. package/src/sdk/mcp/server.ts +46 -18
  332. package/src/sdk/prompt-status.ts +97 -0
  333. package/src/sdk/protocol/operation-inventory.generated.json +145 -0
  334. package/src/sdk/protocol/operation-registry.ts +24 -4
  335. package/src/sdk/session-directory.ts +5 -1
  336. package/src/sdk/session.ts +69 -8
  337. package/src/sdk/startup-capability.ts +15 -1
  338. package/src/sdk/transport/auth-preface.ts +82 -0
  339. package/src/sdk/transport/index.ts +18 -0
  340. package/src/sdk/transport/relay.ts +252 -0
  341. package/src/sdk/transport/serve-cli.ts +105 -0
  342. package/src/sdk/transport/socket.ts +197 -0
  343. package/src/sdk/transport/stdio.ts +17 -0
  344. package/src/session/agent-session.ts +1112 -314
  345. package/src/session/blob-store.ts +25 -7
  346. package/src/session/btw-contract.ts +59 -0
  347. package/src/session/fallback-chain-controller.ts +47 -8
  348. package/src/session/internal/managed-session-scope.ts +1426 -259
  349. package/src/session/internal/managed-session-storage.ts +414 -238
  350. package/src/session/internal/native-publish-outcome.ts +271 -0
  351. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  352. package/src/session/session-manager.ts +2079 -469
  353. package/src/session/session-storage.ts +151 -11
  354. package/src/session/streaming-output.ts +262 -43
  355. package/src/setup/credential-import.ts +28 -3
  356. package/src/setup/provider-onboarding.ts +49 -5
  357. package/src/setup/provider-presets.json +2 -1
  358. package/src/skc-runtime/deep-interview-runtime.ts +303 -36
  359. package/src/skc-runtime/deep-interview-stage.ts +1016 -0
  360. package/src/skc-runtime/deep-interview-state.ts +8 -5
  361. package/src/skc-runtime/gc-render.ts +1 -0
  362. package/src/skc-runtime/gc-runtime.ts +11 -1
  363. package/src/skc-runtime/linux-proc.ts +70 -36
  364. package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
  365. package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
  366. package/src/skc-runtime/ralplan-runtime.ts +1057 -75
  367. package/src/skc-runtime/repository-binding.ts +267 -0
  368. package/src/skc-runtime/session-state-sidecar.ts +25 -9
  369. package/src/skc-runtime/state-runtime.ts +50 -12
  370. package/src/skc-runtime/state-writer.ts +68 -56
  371. package/src/skc-runtime/team-launch.ts +34 -5
  372. package/src/skc-runtime/team-runtime.ts +41 -1
  373. package/src/skc-runtime/team-store.ts +32 -0
  374. package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
  375. package/src/skc-runtime/team-workers.ts +1 -0
  376. package/src/skc-runtime/tmux-common.ts +13 -2
  377. package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
  378. package/src/skc-runtime/tmux-sessions.ts +45 -6
  379. package/src/skc-runtime/ultragoal-guard.ts +93 -15
  380. package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
  381. package/src/skc-runtime/ultragoal-runtime.ts +469 -38
  382. package/src/skc-runtime/workflow-manifest.generated.json +195 -5
  383. package/src/skc-runtime/workflow-manifest.ts +67 -3
  384. package/src/skill-state/workflow-hud.ts +25 -2
  385. package/src/skill-state/workflow-mutation-guard.ts +104 -12
  386. package/src/slash-commands/builtin-registry.ts +1 -1
  387. package/src/ssh/utils.ts +14 -0
  388. package/src/task/discovery.ts +8 -2
  389. package/src/task/executor.ts +165 -47
  390. package/src/task/index.ts +123 -15
  391. package/src/task/provider-retry-status.ts +88 -0
  392. package/src/task/receipt.ts +3 -0
  393. package/src/task/render.ts +45 -26
  394. package/src/task/skc-command.ts +2 -2
  395. package/src/task/types.ts +55 -10
  396. package/src/task/ultragoal-redteam-activation.ts +75 -0
  397. package/src/tools/ask.ts +1 -1
  398. package/src/tools/ast-edit.ts +2 -2
  399. package/src/tools/bash-allowed-prefixes.ts +45 -1
  400. package/src/tools/bash-pty-selection.ts +2 -2
  401. package/src/tools/browser/launch.ts +41 -6
  402. package/src/tools/browser/screenshot-format.ts +6 -1
  403. package/src/tools/cron.ts +1 -1
  404. package/src/tools/fetch.ts +127 -154
  405. package/src/tools/image-gen.ts +476 -23
  406. package/src/tools/index.ts +7 -1
  407. package/src/tools/output-meta.ts +176 -9
  408. package/src/tools/path-utils.ts +5 -1
  409. package/src/tools/read-internals.ts +44 -0
  410. package/src/tools/read.ts +1544 -310
  411. package/src/tools/resource-gc.ts +665 -125
  412. package/src/tools/skill-discovery.ts +39 -3
  413. package/src/tools/sqlite-reader.ts +63 -5
  414. package/src/tools/subagent-render.ts +74 -16
  415. package/src/tools/subagent.ts +2 -0
  416. package/src/tools/tool-result.ts +14 -1
  417. package/src/utils/edit-mode.ts +2 -2
  418. package/src/utils/pasted-image-path.ts +15 -6
  419. package/src/utils/prompt-suggestion.ts +265 -0
  420. package/src/utils/shell-snapshot.ts +173 -63
  421. package/src/utils/sixel.ts +3 -3
  422. package/src/web/insane/bridge.ts +5 -1
  423. package/src/web/insane/url-guard.ts +140 -27
  424. package/src/web/scrapers/docs-rs.ts +30 -5
  425. package/src/web/scrapers/types.ts +19 -49
  426. package/src/web/scrapers/utils.ts +12 -26
  427. package/src/web/search/providers/codex.ts +1 -1
  428. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -156,6 +156,18 @@ export function createAliasTable(): AliasTable {
156
156
  },
157
157
  };
158
158
  }
159
+ /** Copy labels for display and annotate one valid recommended option only. */
160
+ function withRecommendedOptionLabel(options: readonly string[], recommendedIndex: unknown): string[] {
161
+ if (
162
+ typeof recommendedIndex !== "number" ||
163
+ !Number.isFinite(recommendedIndex) ||
164
+ !Number.isInteger(recommendedIndex) ||
165
+ recommendedIndex < 0 ||
166
+ recommendedIndex >= options.length
167
+ )
168
+ return [...options];
169
+ return options.map((label, index) => (index === recommendedIndex ? `${label} (Recommended)` : label));
170
+ }
159
171
 
160
172
  /** Render an `action_needed` payload into a Telegram message. */
161
173
  export function buildActionMessage(action: {
@@ -163,6 +175,7 @@ export function buildActionMessage(action: {
163
175
  id: string;
164
176
  question?: string;
165
177
  options?: string[];
178
+ recommendedIndex?: unknown;
166
179
  controls?: readonly TelegramActionControl[];
167
180
  summary?: string;
168
181
  }): RenderedMessage {
@@ -172,9 +185,10 @@ export function buildActionMessage(action: {
172
185
  }
173
186
  const text = `❓ ${bold(action.question ?? "Question")}`;
174
187
  const options = action.options ?? [];
188
+ const displayOptions = withRecommendedOptionLabel(options, action.recommendedIndex);
175
189
  const controls = (action.controls ?? []).filter(control => control.enabled);
176
190
  if (options.length === 0 && controls.length === 0) return { text: `${text}\n\n(reply with text)` };
177
- const body = options.length ? `${text}\n\n${numberedOptionList(options)}` : text;
191
+ const body = options.length ? `${text}\n\n${numberedOptionList(displayOptions)}` : text;
178
192
  const inline_keyboard = [
179
193
  ...(options.length ? buildCompactChoiceGrid(options, i => encodeCallbackData(action.id, i)) : []),
180
194
  ...controls.map(control => [
@@ -189,6 +203,7 @@ export function buildActionMarkdown(action: {
189
203
  kind: "ask" | "idle";
190
204
  question?: string;
191
205
  options?: string[];
206
+ recommendedIndex?: unknown;
192
207
  summary?: string;
193
208
  }): string {
194
209
  if (action.kind === "idle") {
@@ -196,8 +211,9 @@ export function buildActionMarkdown(action: {
196
211
  }
197
212
  const heading = `❓ **${action.question ?? "Question"}**`;
198
213
  const options = action.options ?? [];
214
+ const displayOptions = withRecommendedOptionLabel(options, action.recommendedIndex);
199
215
  if (options.length === 0) return `${heading}\n\n(reply with text)`;
200
- const list = options.map((label, i) => `${i + 1}. ${label.replace(/^\s*\d+[.)]\s+/, "")}`).join("\n");
216
+ const list = displayOptions.map((label, i) => `${i + 1}. ${label.replace(/^\s*\d+[.)]\s+/, "")}`).join("\n");
201
217
  return `${heading}\n\n${list}`;
202
218
  }
203
219
 
@@ -362,6 +378,7 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
362
378
  id?: string;
363
379
  question?: string;
364
380
  options?: string[];
381
+ recommendedIndex?: unknown;
365
382
  controls?: TelegramActionControl[];
366
383
  summary?: string;
367
384
  reason?: string;
@@ -374,6 +391,7 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
374
391
  id: msg.id,
375
392
  question: msg.question,
376
393
  options: msg.options,
394
+ recommendedIndex: msg.recommendedIndex,
377
395
  controls: msg.controls,
378
396
  summary: msg.summary,
379
397
  });
@@ -39,8 +39,22 @@ export interface TopicRecord {
39
39
  replaySeq?: number;
40
40
  /** Serialized authority epoch; a late create may commit only in its starting epoch. */
41
41
  authorityEpoch?: number;
42
+ /** Immutable authority epoch held when this remote topic create began. */
43
+ creationLeaseEpoch?: number;
42
44
  /** An uncertain delete fences future creation and inbound routing. */
43
45
  authorityState?: "active" | "delete_pending";
46
+ /** Telegram chat and endpoint authority last proven to use this topic. */
47
+ chatId?: string;
48
+ /** Canonical endpoint tuple (URL + token) that currently holds the lease. */
49
+ endpointKey?: string;
50
+ /** Authenticated endpoint authority digest, excluding transport presentation. */
51
+ endpointDigest?: string;
52
+ /** SDK event generation associated with the current endpoint lease. */
53
+ endpointGeneration?: number;
54
+ /** Monotonic authenticated endpoint handoffs; legacy bindings begin at zero. */
55
+ endpointIncarnation?: number;
56
+ /** True when persisted binding fields were present but malformed; recovery must fail closed. */
57
+ bindingMalformed?: true;
44
58
  }
45
59
 
46
60
  /** Serialisable shape persisted to disk. */
@@ -49,6 +63,69 @@ export interface TopicRegistryState {
49
63
  topics: Record<string, TopicRecord>;
50
64
  /** Durable deletion epochs retained after a definite delete. */
51
65
  fences?: Record<string, number>;
66
+ /** Closed transport endpoint leases; unchanged endpoint discovery remains fenced across restart. */
67
+ closedEndpoints?: Record<string, TopicEndpointBinding>;
68
+ }
69
+
70
+ /** Authenticated runtime binding for a durable topic lease. */
71
+ export interface TopicEndpointBinding {
72
+ chatId: string;
73
+ endpointKey: string;
74
+ endpointDigest: string;
75
+ endpointGeneration?: number;
76
+ }
77
+
78
+ /** Discriminated durable endpoint authority for identity-less replay admission. */
79
+ export type TopicEndpointAuthority =
80
+ | { state: "none" }
81
+ | { state: "unique"; sessionId: string }
82
+ | { state: "ambiguous" };
83
+
84
+ /** Conditional rollback token for a delete fence publication. */
85
+ export interface TopicDeleteAuthoritySnapshot {
86
+ sessionId: string;
87
+ topicId?: string;
88
+ authorityEpoch?: number;
89
+ authorityState?: TopicRecord["authorityState"];
90
+ fenceEpoch?: number;
91
+ /** Exact fenced record, retained to restore an in-memory tombstone after a failed clear publication. */
92
+ record?: TopicRecord;
93
+ }
94
+
95
+ function isValidBindingString(value: unknown): value is string {
96
+ return typeof value === "string" && value.trim().length > 0;
97
+ }
98
+
99
+ function isValidBindingGeneration(value: unknown): value is number {
100
+ return Number.isSafeInteger(value) && (value as number) >= 0;
101
+ }
102
+ function hasAnyBinding(record: TopicRecord): boolean {
103
+ return (
104
+ record.chatId !== undefined ||
105
+ record.endpointKey !== undefined ||
106
+ record.endpointDigest !== undefined ||
107
+ record.endpointGeneration !== undefined ||
108
+ record.endpointIncarnation !== undefined
109
+ );
110
+ }
111
+
112
+ function hasCompleteBinding(record: TopicRecord): boolean {
113
+ return (
114
+ isValidBindingString(record.chatId) &&
115
+ isValidBindingString(record.endpointKey) &&
116
+ isValidBindingString(record.endpointDigest) &&
117
+ (record.endpointGeneration === undefined || isValidBindingGeneration(record.endpointGeneration)) &&
118
+ (record.endpointIncarnation === undefined || isValidBindingGeneration(record.endpointIncarnation))
119
+ );
120
+ }
121
+
122
+ function hasValidBinding(binding: TopicEndpointBinding): boolean {
123
+ return (
124
+ isValidBindingString(binding.chatId) &&
125
+ isValidBindingString(binding.endpointKey) &&
126
+ isValidBindingString(binding.endpointDigest) &&
127
+ (binding.endpointGeneration === undefined || isValidBindingGeneration(binding.endpointGeneration))
128
+ );
52
129
  }
53
130
 
54
131
  function isValidTopicId(value: unknown): value is string {
@@ -74,6 +151,12 @@ export class TopicRegistry {
74
151
  readonly #ambiguousTopicIds = new Set<string>();
75
152
  /** In-flight create promises, keyed by session, to dedupe concurrent creates. */
76
153
  private readonly inflight = new Map<string, Promise<TopicRecord>>();
154
+ /** Newly-created records being durably published; never routable until committed. */
155
+ private readonly staged = new Map<string, TopicRecord>();
156
+ /** Socket-specific provenance for transient endpoint claims. */
157
+ private readonly transientClaimants = new Map<string, object | undefined>();
158
+ /** Endpoint claims registered before a remote topic create can publish a record. */
159
+ private readonly creatingBindings = new Map<string, TopicEndpointBinding>();
77
160
  /** Monotonic authority epochs, including deletion fences for absent records. */
78
161
  private readonly epochs = new Map<string, number>();
79
162
 
@@ -82,6 +165,15 @@ export class TopicRegistry {
82
165
  this.load(state);
83
166
  }
84
167
 
168
+ /** Replace all runtime state after a successfully persisted staged publication. */
169
+ replace(state: TopicRegistryState): void {
170
+ this.topics.clear();
171
+ this.byTopic.clear();
172
+ this.#ambiguousTopicIds.clear();
173
+ this.epochs.clear();
174
+ this.load(state);
175
+ }
176
+
85
177
  /** Merge serialized state and normalize authority fields from older releases. */
86
178
  load(state: TopicRegistryState): void {
87
179
  for (const [sessionId, epoch] of Object.entries(state.fences ?? {})) {
@@ -94,9 +186,10 @@ export class TopicRegistry {
94
186
  raw.nameOwner === "user" &&
95
187
  typeof raw.name === "string" &&
96
188
  raw.name.trim().length > 0 &&
97
- typeof raw.userNameUpdateId === "number" &&
98
- Number.isSafeInteger(raw.userNameUpdateId) &&
99
- raw.userNameUpdateId >= 0;
189
+ (raw.userNameUpdateId === undefined ||
190
+ (typeof raw.userNameUpdateId === "number" &&
191
+ Number.isSafeInteger(raw.userNameUpdateId) &&
192
+ raw.userNameUpdateId >= 0));
100
193
  const hasValidReplayCursor =
101
194
  typeof raw.replayGeneration === "number" &&
102
195
  Number.isSafeInteger(raw.replayGeneration) &&
@@ -104,6 +197,19 @@ export class TopicRegistry {
104
197
  typeof raw.replaySeq === "number" &&
105
198
  Number.isSafeInteger(raw.replaySeq) &&
106
199
  raw.replaySeq >= 0;
200
+ const legacyUnbound = !hasAnyBinding(raw);
201
+ const bindingMalformed = raw.bindingMalformed === true || (!legacyUnbound && !hasCompleteBinding(raw));
202
+ const candidateAuthorityEpoch = raw.authorityEpoch;
203
+ const rawAuthorityEpoch: number =
204
+ typeof candidateAuthorityEpoch === "number" &&
205
+ Number.isSafeInteger(candidateAuthorityEpoch) &&
206
+ candidateAuthorityEpoch >= 0
207
+ ? candidateAuthorityEpoch
208
+ : 0;
209
+ // A fence is the durable authority source. A mixed snapshot can contain an
210
+ // older active record alongside a newer fence; never rebuild its inbound route.
211
+ const fenceEpoch = this.epochs.get(sessionId) ?? 0;
212
+ const fenceSupersedesRecord = fenceEpoch > rawAuthorityEpoch;
107
213
  const record: TopicRecord = {
108
214
  topicId: raw.topicId,
109
215
  identitySent: raw.identitySent === true,
@@ -114,15 +220,33 @@ export class TopicRegistry {
114
220
  : {}),
115
221
  ...(hasValidUserAuthority ? { nameOwner: "user" as const } : {}),
116
222
  ...(hasValidUserAuthority && raw.nameReconcilePending === true ? { nameReconcilePending: true } : {}),
117
- ...(hasValidUserAuthority ? { userNameUpdateId: raw.userNameUpdateId } : {}),
223
+ ...(hasValidUserAuthority && typeof raw.userNameUpdateId === "number"
224
+ ? { userNameUpdateId: raw.userNameUpdateId }
225
+ : {}),
118
226
  ...(typeof raw.identityKey === "string" ? { identityKey: raw.identityKey } : {}),
227
+ ...(typeof raw.creationLeaseEpoch === "number" &&
228
+ Number.isSafeInteger(raw.creationLeaseEpoch) &&
229
+ raw.creationLeaseEpoch >= 0
230
+ ? { creationLeaseEpoch: raw.creationLeaseEpoch }
231
+ : {}),
119
232
  ...(hasValidReplayCursor ? { replayGeneration: raw.replayGeneration, replaySeq: raw.replaySeq } : {}),
120
- ...(Number.isSafeInteger(raw.authorityEpoch) && raw.authorityEpoch! >= 0
121
- ? { authorityEpoch: raw.authorityEpoch }
233
+ authorityEpoch: Math.max(rawAuthorityEpoch, fenceEpoch),
234
+ ...(raw.authorityState === "delete_pending" || fenceSupersedesRecord
235
+ ? { authorityState: "delete_pending" as const }
236
+ : {}),
237
+ ...(isValidBindingString(raw.chatId) ? { chatId: raw.chatId } : {}),
238
+ ...(isValidBindingString(raw.endpointKey) ? { endpointKey: raw.endpointKey } : {}),
239
+ ...(isValidBindingString(raw.endpointDigest) ? { endpointDigest: raw.endpointDigest } : {}),
240
+ ...(isValidBindingGeneration(raw.endpointGeneration) ? { endpointGeneration: raw.endpointGeneration } : {}),
241
+ ...(isValidBindingGeneration(raw.endpointIncarnation)
242
+ ? { endpointIncarnation: raw.endpointIncarnation }
122
243
  : {}),
123
- ...(raw.authorityState === "delete_pending" ? { authorityState: "delete_pending" as const } : {}),
244
+ ...(bindingMalformed ? { bindingMalformed: true as const } : {}),
124
245
  };
125
- this.epochs.set(sessionId, Math.max(this.epochs.get(sessionId) ?? 0, record.authorityEpoch ?? 0));
246
+ this.epochs.set(sessionId, Math.max(fenceEpoch, record.authorityEpoch ?? 0));
247
+ // Pre-generation-17 records have no endpoint authority. Retire them locally:
248
+ // their unknown remote topic must neither be rebound nor deleted cross-chat.
249
+ if (legacyUnbound) continue;
126
250
 
127
251
  this.topics.set(sessionId, record);
128
252
  }
@@ -135,7 +259,7 @@ export class TopicRegistry {
135
259
  const activeByTopic = new Map<string, string>();
136
260
 
137
261
  for (const [sessionId, record] of this.topics) {
138
- if (record.authorityState === "delete_pending") {
262
+ if (record.authorityState === "delete_pending" || record.bindingMalformed) {
139
263
  this.#ambiguousTopicIds.add(record.topicId);
140
264
  continue;
141
265
  }
@@ -161,11 +285,164 @@ export class TopicRegistry {
161
285
  return [...this.topics.keys()];
162
286
  }
163
287
 
288
+ /** Persisted remote deletes that must be reconciled before recovery can proceed. */
289
+ deletePendingSessionIds(): string[] {
290
+ return [...this.topics].flatMap(([sessionId, record]) =>
291
+ record.authorityState === "delete_pending" ? [sessionId] : [],
292
+ );
293
+ }
294
+
164
295
  /** The existing topic record for a session, if any. */
165
296
  get(sessionId: string): TopicRecord | undefined {
166
297
  return this.topics.get(sessionId);
167
298
  }
168
299
 
300
+ /** Current immutable authority epoch for a creation lease. */
301
+ authorityEpoch(sessionId: string): number {
302
+ return Math.max(this.epochs.get(sessionId) ?? 0, this.topics.get(sessionId)?.authorityEpoch ?? 0);
303
+ }
304
+
305
+ /** Whether this session has an active, unambiguous topic authority. */
306
+ isActiveUnambiguous(sessionId: string): boolean {
307
+ const record = this.topics.get(sessionId);
308
+ return record?.authorityState !== "delete_pending" && this.byTopic.get(record?.topicId ?? "") === sessionId;
309
+ }
310
+
311
+ /**
312
+ * Resolve endpoint authority for identity-less replay. An endpoint may bootstrap
313
+ * only when no committed, staged, or pre-create claim can own it; malformed
314
+ * partial bindings and deletion fences deliberately fail closed.
315
+ */
316
+ endpointAuthority(binding: TopicEndpointBinding, excludedTransientClaimant?: object): TopicEndpointAuthority {
317
+ if (!hasValidBinding(binding)) return { state: "ambiguous" };
318
+ const canClaim = (record: Pick<TopicRecord, "chatId" | "endpointKey" | "endpointDigest">): boolean =>
319
+ (record.chatId === undefined || record.chatId === binding.chatId) &&
320
+ (record.endpointKey === undefined || record.endpointKey === binding.endpointKey) &&
321
+ (record.endpointDigest === undefined || record.endpointDigest === binding.endpointDigest);
322
+ const committed = [...this.topics].filter(([, record]) => canClaim(record));
323
+ const excludesClaimant = (sessionId: string): boolean =>
324
+ excludedTransientClaimant !== undefined &&
325
+ this.transientClaimants.get(sessionId) === excludedTransientClaimant;
326
+ const staged = [...this.staged].filter(([sessionId, record]) => canClaim(record) && !excludesClaimant(sessionId));
327
+ const creating = [...this.creatingBindings].filter(
328
+ ([sessionId, claim]) => canClaim(claim) && !excludesClaimant(sessionId),
329
+ );
330
+ if (committed.length === 0 && staged.length === 0 && creating.length === 0) return { state: "none" };
331
+ if (committed.length !== 1 || staged.length !== 0 || creating.length !== 0) return { state: "ambiguous" };
332
+ const [sessionId, record] = committed[0]!;
333
+ return record.chatId === binding.chatId &&
334
+ record.endpointKey === binding.endpointKey &&
335
+ record.endpointDigest === binding.endpointDigest &&
336
+ this.isActiveUnambiguous(sessionId) &&
337
+ !record.bindingMalformed
338
+ ? { state: "unique", sessionId }
339
+ : { state: "ambiguous" };
340
+ }
341
+
342
+ /** Resolve a uniquely bound logical owner for callers that only need the owner. */
343
+ uniqueSessionForEndpoint(binding: TopicEndpointBinding): string | undefined {
344
+ const authority = this.endpointAuthority(binding);
345
+ return authority.state === "unique" ? authority.sessionId : undefined;
346
+ }
347
+
348
+ /** Whether this exact session owns the complete durable endpoint binding. */
349
+ matchesEndpoint(sessionId: string, binding: TopicEndpointBinding): boolean {
350
+ const record = this.topics.get(sessionId);
351
+ return (
352
+ this.isActiveUnambiguous(sessionId) &&
353
+ !record?.bindingMalformed &&
354
+ record?.chatId === binding.chatId &&
355
+ record.endpointKey === binding.endpointKey &&
356
+ record.endpointDigest === binding.endpointDigest
357
+ );
358
+ }
359
+
360
+ /**
361
+ * Rebind an existing topic to an authenticated successor endpoint. The exact
362
+ * logical session id is proved by replay before this method is called. A
363
+ * rotated credential may replace only an inactive incumbent; concurrent
364
+ * incumbents, cross-chat records, malformed evidence, collisions, and delete
365
+ * fences remain fail-closed.
366
+ */
367
+ bindEndpoint(
368
+ sessionId: string,
369
+ binding: TopicEndpointBinding,
370
+ activeEndpointKeys: ReadonlySet<string> = new Set(),
371
+ allowEndpointRotation = false,
372
+ ): "bound" | "unchanged" | "rejected" {
373
+ const record = this.topics.get(sessionId);
374
+ if (
375
+ !record ||
376
+ record.authorityState === "delete_pending" ||
377
+ record.bindingMalformed ||
378
+ !hasValidBinding(binding) ||
379
+ !this.isActiveUnambiguous(sessionId)
380
+ )
381
+ return "rejected";
382
+ if (hasAnyBinding(record) && !hasCompleteBinding(record)) return "rejected";
383
+ // A topic id without chat affinity may belong to any prior paired chat.
384
+ // Do not bind it to the current chat merely because a resumed endpoint
385
+ // authenticated its logical session id.
386
+ if (record.chatId === undefined || record.chatId !== binding.chatId) return "rejected";
387
+
388
+ const sameEndpoint =
389
+ record.endpointKey === binding.endpointKey && record.endpointDigest === binding.endpointDigest;
390
+ if (
391
+ sameEndpoint &&
392
+ record.endpointGeneration !== undefined &&
393
+ binding.endpointGeneration !== undefined &&
394
+ binding.endpointGeneration < record.endpointGeneration
395
+ )
396
+ return "rejected";
397
+ if (
398
+ !sameEndpoint &&
399
+ hasAnyBinding(record) &&
400
+ (!allowEndpointRotation || (record.endpointKey !== undefined && activeEndpointKeys.has(record.endpointKey)))
401
+ )
402
+ return "rejected";
403
+
404
+ const changed =
405
+ record.chatId !== binding.chatId ||
406
+ record.endpointKey !== binding.endpointKey ||
407
+ record.endpointDigest !== binding.endpointDigest ||
408
+ record.endpointGeneration !== binding.endpointGeneration;
409
+ if (!changed) return "unchanged";
410
+ record.chatId = binding.chatId;
411
+ record.endpointKey = binding.endpointKey;
412
+ record.endpointDigest = binding.endpointDigest;
413
+ record.endpointGeneration = binding.endpointGeneration;
414
+ if (!sameEndpoint) record.endpointIncarnation = (record.endpointIncarnation ?? 0) + 1;
415
+ return "bound";
416
+ }
417
+
418
+ /** Undo a failed durable endpoint migration without disturbing concurrent metadata writers. */
419
+ restoreEndpointBinding(
420
+ sessionId: string,
421
+ expected: TopicEndpointBinding,
422
+ previous: Pick<
423
+ TopicRecord,
424
+ "chatId" | "endpointKey" | "endpointDigest" | "endpointGeneration" | "endpointIncarnation"
425
+ >,
426
+ ): boolean {
427
+ const record = this.topics.get(sessionId);
428
+ if (
429
+ !record ||
430
+ record.chatId !== expected.chatId ||
431
+ record.endpointKey !== expected.endpointKey ||
432
+ record.endpointDigest !== expected.endpointDigest ||
433
+ record.endpointGeneration !== expected.endpointGeneration
434
+ )
435
+ return false;
436
+ record.chatId = previous.chatId;
437
+ record.endpointKey = previous.endpointKey;
438
+ record.endpointDigest = previous.endpointDigest;
439
+ if (previous.endpointGeneration === undefined) delete record.endpointGeneration;
440
+ else record.endpointGeneration = previous.endpointGeneration;
441
+ if (previous.endpointIncarnation === undefined) delete record.endpointIncarnation;
442
+ else record.endpointIncarnation = previous.endpointIncarnation;
443
+ return true;
444
+ }
445
+
169
446
  /**
170
447
  * Return the existing active topic for `sessionId`, or create one via
171
448
  * `create` (called only on first use).
@@ -175,13 +452,22 @@ export class TopicRegistry {
175
452
  create: () => Promise<unknown>,
176
453
  now: () => number = Date.now,
177
454
  name?: string,
455
+ binding?: TopicEndpointBinding,
456
+ commit?: () => Promise<void>,
457
+ transientClaimant?: object,
178
458
  ): Promise<TopicRecord> {
179
459
  const existing = this.topics.get(sessionId);
180
460
  if (existing?.authorityState === "delete_pending") throw new Error("topic authority is deletion-fenced");
461
+ if (existing?.bindingMalformed) throw new Error("topic authority binding is quarantined");
181
462
  if (existing) return existing;
182
463
  const pending = this.inflight.get(sessionId);
183
464
  if (pending) return pending;
184
465
  const epoch = this.epochs.get(sessionId) ?? 0;
466
+ // Publish the compatible endpoint claim before invoking `create`: the callback
467
+ // may immediately begin a remote create and identity-less recovery must never
468
+ // observe a false absence during that await.
469
+ if (binding) this.creatingBindings.set(sessionId, binding);
470
+ this.transientClaimants.set(sessionId, transientClaimant);
185
471
  const promise = (async () => {
186
472
  const topicId = await create();
187
473
  if (!isValidTopicId(topicId)) throw new Error("createForumTopic: invalid message_thread_id");
@@ -192,10 +478,39 @@ export class TopicRegistry {
192
478
  identitySent: false,
193
479
  createdAt: now(),
194
480
  authorityEpoch: revoked ? (this.epochs.get(sessionId) ?? 0) : epoch,
481
+ creationLeaseEpoch: epoch,
482
+ ...(binding
483
+ ? {
484
+ chatId: binding.chatId,
485
+ endpointKey: binding.endpointKey,
486
+ endpointDigest: binding.endpointDigest,
487
+ endpointIncarnation: 0,
488
+ ...(binding.endpointGeneration === undefined
489
+ ? {}
490
+ : { endpointGeneration: binding.endpointGeneration }),
491
+ }
492
+ : {}),
195
493
  ...(revoked ? { authorityState: "delete_pending" as const } : {}),
196
494
  };
495
+ if (revoked) {
496
+ this.topics.set(sessionId, record);
497
+ throw new Error("topic authority was revoked during creation");
498
+ }
499
+ this.staged.set(sessionId, record);
500
+ try {
501
+ await commit?.();
502
+ } catch (error) {
503
+ this.staged.delete(sessionId);
504
+ throw error;
505
+ }
506
+ this.staged.delete(sessionId);
507
+ if ((this.epochs.get(sessionId) ?? 0) !== epoch) {
508
+ record.authorityEpoch = this.epochs.get(sessionId) ?? 0;
509
+ record.authorityState = "delete_pending";
510
+ this.topics.set(sessionId, record);
511
+ throw new Error("topic authority was revoked during creation");
512
+ }
197
513
  this.topics.set(sessionId, record);
198
- if (revoked) throw new Error("topic authority was revoked during creation");
199
514
  if (this.#ambiguousTopicIds.has(topicId)) return record;
200
515
  if (this.byTopic.has(topicId)) {
201
516
  this.byTopic.delete(topicId);
@@ -210,6 +525,8 @@ export class TopicRegistry {
210
525
  return await promise;
211
526
  } finally {
212
527
  this.inflight.delete(sessionId);
528
+ this.creatingBindings.delete(sessionId);
529
+ this.transientClaimants.delete(sessionId);
213
530
  }
214
531
  }
215
532
 
@@ -323,6 +640,67 @@ export class TopicRegistry {
323
640
  record.nameReconcilePending = false;
324
641
  }
325
642
 
643
+ /** Capture only authority fields that a failed delete publication may restore. */
644
+ captureDeleteAuthority(sessionId: string): TopicDeleteAuthoritySnapshot {
645
+ const record = this.topics.get(sessionId);
646
+ return {
647
+ sessionId,
648
+ topicId: record?.topicId,
649
+ authorityEpoch: record?.authorityEpoch,
650
+ authorityState: record?.authorityState,
651
+ fenceEpoch: this.epochs.get(sessionId),
652
+ ...(record ? { record: { ...record } } : {}),
653
+ };
654
+ }
655
+
656
+ /** Restore a failed delete fence only while its exact authority mutation remains current. */
657
+ restoreDeleteAuthority(snapshot: TopicDeleteAuthoritySnapshot): boolean {
658
+ const record = this.topics.get(snapshot.sessionId);
659
+ const deleteEpoch = Math.max(snapshot.fenceEpoch ?? 0, snapshot.authorityEpoch ?? 0) + 1;
660
+ if (this.epochs.get(snapshot.sessionId) !== deleteEpoch) return false;
661
+ if (snapshot.topicId === undefined) {
662
+ if (record) return false;
663
+ } else if (
664
+ !record ||
665
+ record.topicId !== snapshot.topicId ||
666
+ record.authorityState !== "delete_pending" ||
667
+ record.authorityEpoch !== deleteEpoch
668
+ ) {
669
+ return false;
670
+ } else {
671
+ record.authorityEpoch = snapshot.authorityEpoch;
672
+ record.authorityState = snapshot.authorityState;
673
+ this.rebuildInboundRoutes();
674
+ }
675
+ if (snapshot.fenceEpoch === undefined) this.epochs.delete(snapshot.sessionId);
676
+ else this.epochs.set(snapshot.sessionId, snapshot.fenceEpoch);
677
+ return true;
678
+ }
679
+
680
+ /** Restore the exact delete fence after a failed compensation publication. */
681
+ restoreDeleteFence(snapshot: TopicDeleteAuthoritySnapshot): boolean {
682
+ const record = this.topics.get(snapshot.sessionId);
683
+ const deleteEpoch = Math.max(snapshot.fenceEpoch ?? 0, snapshot.authorityEpoch ?? 0) + 1;
684
+ if (snapshot.topicId === undefined) {
685
+ if (record) return false;
686
+ } else if (!record) {
687
+ if (!snapshot.record) return false;
688
+ this.topics.set(snapshot.sessionId, {
689
+ ...snapshot.record,
690
+ authorityEpoch: deleteEpoch,
691
+ authorityState: "delete_pending",
692
+ });
693
+ } else if (record.topicId !== snapshot.topicId) {
694
+ return false;
695
+ } else {
696
+ record.authorityEpoch = deleteEpoch;
697
+ record.authorityState = "delete_pending";
698
+ if (this.byTopic.get(record.topicId) === snapshot.sessionId) this.byTopic.delete(record.topicId);
699
+ }
700
+ this.epochs.set(snapshot.sessionId, deleteEpoch);
701
+ return true;
702
+ }
703
+
326
704
  /** Fence new work before the remote delete starts, including an absent in-flight create. */
327
705
  beginDelete(sessionId: string): TopicRecord | undefined {
328
706
  const record = this.topics.get(sessionId);
@@ -335,6 +713,67 @@ export class TopicRegistry {
335
713
  return record;
336
714
  }
337
715
 
716
+ /** Retain an accepted create as deletion-fenced before remote compensation can begin. */
717
+ fenceAcceptedCreate(
718
+ sessionId: string,
719
+ topicId: string,
720
+ now: () => number = Date.now,
721
+ name?: string,
722
+ binding?: TopicEndpointBinding,
723
+ ): TopicRecord {
724
+ const epoch = Math.max(this.epochs.get(sessionId) ?? 0, this.topics.get(sessionId)?.authorityEpoch ?? 0);
725
+ const record: TopicRecord = {
726
+ topicId,
727
+ name,
728
+ identitySent: false,
729
+ createdAt: now(),
730
+ authorityEpoch: epoch,
731
+ authorityState: "delete_pending",
732
+ ...(binding
733
+ ? {
734
+ chatId: binding.chatId,
735
+ endpointKey: binding.endpointKey,
736
+ endpointDigest: binding.endpointDigest,
737
+ endpointIncarnation: 0,
738
+ ...(binding.endpointGeneration === undefined
739
+ ? {}
740
+ : { endpointGeneration: binding.endpointGeneration }),
741
+ }
742
+ : {}),
743
+ };
744
+ this.topics.set(sessionId, record);
745
+ if (this.byTopic.get(topicId) === sessionId) this.byTopic.delete(topicId);
746
+ this.#ambiguousTopicIds.add(topicId);
747
+ return record;
748
+ }
749
+
750
+ /** Fence an accepted create only when its exact creator lease still owns the record. */
751
+ fenceAcceptedCreateForLease(
752
+ sessionId: string,
753
+ topicId: string,
754
+ creationLeaseEpoch: number,
755
+ now: () => number = Date.now,
756
+ name?: string,
757
+ binding?: TopicEndpointBinding,
758
+ ): TopicRecord | undefined {
759
+ const record = this.topics.get(sessionId);
760
+ const matchesBinding =
761
+ record?.chatId === binding?.chatId &&
762
+ record?.endpointKey === binding?.endpointKey &&
763
+ record?.endpointDigest === binding?.endpointDigest &&
764
+ record?.endpointGeneration === binding?.endpointGeneration;
765
+ if (
766
+ record
767
+ ? record.topicId !== topicId || record.creationLeaseEpoch !== creationLeaseEpoch || !matchesBinding
768
+ : (this.epochs.get(sessionId) ?? 0) !== creationLeaseEpoch
769
+ )
770
+ return undefined;
771
+ this.beginDelete(sessionId);
772
+ const fenced = this.fenceAcceptedCreate(sessionId, topicId, now, name, binding);
773
+ fenced.creationLeaseEpoch = creationLeaseEpoch;
774
+ return fenced;
775
+ }
776
+
338
777
  /** Wait for a revoked create to settle before admitting a later lifecycle epoch. */
339
778
  async awaitInflight(sessionId: string): Promise<void> {
340
779
  await this.inflight.get(sessionId)?.catch(() => undefined);
@@ -357,8 +796,8 @@ export class TopicRegistry {
357
796
  return this.topics.delete(sessionId);
358
797
  }
359
798
 
360
- /** Serialise for atomic persistence beside the daemon state. */
799
+ /** Serialise active records plus unpublished staged creates for atomic commit. */
361
800
  serialize(): TopicRegistryState {
362
- return { topics: Object.fromEntries(this.topics), fences: Object.fromEntries(this.epochs) };
801
+ return { topics: Object.fromEntries([...this.topics, ...this.staged]), fences: Object.fromEntries(this.epochs) };
363
802
  }
364
803
  }
@@ -40,6 +40,7 @@ class SdkSessionCliError extends Error {
40
40
  readonly code: string,
41
41
  message: string,
42
42
  readonly exitCode: 1 | 2,
43
+ readonly details?: unknown,
43
44
  ) {
44
45
  super(message);
45
46
  }
@@ -261,7 +262,12 @@ export async function runSdkSessionCli(
261
262
  error instanceof SdkSessionCliError
262
263
  ? error
263
264
  : error instanceof SdkClientError
264
- ? new SdkSessionCliError(error.code, error.message, 1)
265
+ ? new SdkSessionCliError(
266
+ error.code,
267
+ error.message,
268
+ 1,
269
+ (error.details as { details?: unknown } | undefined)?.details,
270
+ )
265
271
  : error instanceof SdkDiscoveryError
266
272
  ? new SdkSessionCliError(error.code, error.message, 1)
267
273
  : new SdkSessionCliError(
@@ -269,7 +275,14 @@ export async function runSdkSessionCli(
269
275
  error instanceof Error ? error.message : "SDK operation failed.",
270
276
  1,
271
277
  );
272
- writeOutput({ ok: false, error: { code: cliError.code, message: cliError.message } });
278
+ writeOutput({
279
+ ok: false,
280
+ error: {
281
+ code: cliError.code,
282
+ message: cliError.message,
283
+ ...(cliError.details ? { details: cliError.details } : {}),
284
+ },
285
+ });
273
286
  setExitCode(cliError.exitCode);
274
287
  }
275
288
  }