@sayknow-cli/coding-agent 0.4.6 → 0.5.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 (428) hide show
  1. package/CHANGELOG.md +112 -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 +27 -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
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Kind-aware invocation reconciliation (prompt | skill) with optional durable store.
3
+ * Preserves Q26 admit/first-terminal/capacity/TTL semantics; indexes and caps are per-kind.
4
+ */
5
+ import {
6
+ PROMPT_RECONCILIATION_ACTIVE_CAPACITY,
7
+ PROMPT_RECONCILIATION_TERMINAL_CAPACITY,
8
+ PROMPT_RECONCILIATION_TERMINAL_TTL_MS,
9
+ type PromptCorrelation,
10
+ type PromptReconciliationStatus,
11
+ sanitizePromptFailure,
12
+ type TurnPromptReconciliation,
13
+ } from "./prompt-reconciliation";
14
+ import type { DurableReconciliationRecord, ReconciliationKind, ReconciliationStore } from "./reconciliation-store";
15
+
16
+ export type { ReconciliationKind };
17
+
18
+ export interface KindCorrelation extends PromptCorrelation {
19
+ kind: ReconciliationKind;
20
+ }
21
+
22
+ export interface KindAwareReconciliation {
23
+ admit(kind: ReconciliationKind, clientRef?: string): void;
24
+ releaseAdmission(kind: ReconciliationKind, clientRef?: string): void;
25
+ noteAccepted(
26
+ kind: ReconciliationKind,
27
+ correlation: PromptCorrelation,
28
+ clientRef?: string,
29
+ extra?: { skillName?: string },
30
+ ): Promise<void>;
31
+ noteTransition(
32
+ kind: ReconciliationKind,
33
+ correlation: PromptCorrelation | undefined,
34
+ frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
35
+ ): Promise<void>;
36
+ lookup(
37
+ kind: ReconciliationKind,
38
+ selector: { commandId?: string; turnId?: string; clientRef?: string },
39
+ ): TurnPromptReconciliation;
40
+ cleanup(): void;
41
+ activeCount(kind: ReconciliationKind): number;
42
+ /** Hydrate from durable store (call once at session host start). */
43
+ hydrateFromStore(): Promise<void>;
44
+ }
45
+
46
+ export function createKindAwareReconciliation(
47
+ options: { now?: () => number; store?: ReconciliationStore | null } = {},
48
+ ): KindAwareReconciliation {
49
+ const now = options.now ?? Date.now;
50
+ const store = options.store ?? null;
51
+ const records = new Map<string, DurableReconciliationRecord>();
52
+ const clientRefIndex = new Map<string, string>(); // `${kind}\0${clientRef}` -> key
53
+ const reservedClientRefs = new Map<ReconciliationKind, Set<string>>();
54
+ const reservations: Array<{ kind: ReconciliationKind; clientRef?: string }> = [];
55
+
56
+ const keyOf = (kind: ReconciliationKind, correlation: PromptCorrelation) =>
57
+ `${kind}:${correlation.commandId}:${correlation.turnId}`;
58
+ const refKey = (kind: ReconciliationKind, clientRef: string) => `${kind}\0${clientRef}`;
59
+
60
+ const reservedFor = (kind: ReconciliationKind) => {
61
+ let set = reservedClientRefs.get(kind);
62
+ if (!set) {
63
+ set = new Set();
64
+ reservedClientRefs.set(kind, set);
65
+ }
66
+ return set;
67
+ };
68
+
69
+ const remove = (key: string) => {
70
+ const record = records.get(key);
71
+ if (!record) return;
72
+ records.delete(key);
73
+ if (record.clientRef !== undefined) {
74
+ const rk = refKey(record.kind, record.clientRef);
75
+ if (clientRefIndex.get(rk) === key) clientRefIndex.delete(rk);
76
+ }
77
+ };
78
+
79
+ const cleanup = () => {
80
+ const at = now();
81
+ for (const [key, record] of records)
82
+ if (record.terminalAt !== undefined && record.terminalAt + PROMPT_RECONCILIATION_TERMINAL_TTL_MS <= at)
83
+ remove(key);
84
+ for (const kind of ["prompt", "skill"] as const) {
85
+ const terminalEntries = [...records.entries()].filter(
86
+ ([, record]) => record.kind === kind && record.terminalAt !== undefined,
87
+ );
88
+ if (terminalEntries.length <= PROMPT_RECONCILIATION_TERMINAL_CAPACITY) continue;
89
+ terminalEntries.sort((a, b) => (a[1].terminalAt as number) - (b[1].terminalAt as number));
90
+ for (const [key] of terminalEntries.slice(0, terminalEntries.length - PROMPT_RECONCILIATION_TERMINAL_CAPACITY))
91
+ remove(key);
92
+ }
93
+ };
94
+
95
+ const activeCount = (kind: ReconciliationKind) => {
96
+ let count = 0;
97
+ for (const record of records.values()) if (record.kind === kind && record.terminalAt === undefined) count++;
98
+ return count;
99
+ };
100
+
101
+ const reservationCount = (kind: ReconciliationKind) => reservations.filter(r => r.kind === kind).length;
102
+
103
+ const consumeReservation = (kind: ReconciliationKind, clientRef?: string) => {
104
+ const index = reservations.findIndex(r => r.kind === kind && r.clientRef === clientRef);
105
+ if (index === -1) return;
106
+ reservations.splice(index, 1);
107
+ if (clientRef !== undefined && !reservations.some(r => r.kind === kind && r.clientRef === clientRef))
108
+ reservedFor(kind).delete(clientRef);
109
+ };
110
+
111
+ const persist = async () => {
112
+ if (!store) return;
113
+ await store.transact(() => [...records.values()].map(r => ({ ...r })));
114
+ };
115
+
116
+ const admit = (kind: ReconciliationKind, clientRef?: string) => {
117
+ cleanup();
118
+ const reserved = reservedFor(kind);
119
+ if (clientRef !== undefined && (clientRefIndex.has(refKey(kind, clientRef)) || reserved.has(clientRef)))
120
+ throw Object.assign(
121
+ new Error("A submission with this clientRef is already retained; never reuse a clientRef for retry."),
122
+ { code: "client_ref_conflict" },
123
+ );
124
+ if (activeCount(kind) + reservationCount(kind) >= PROMPT_RECONCILIATION_ACTIVE_CAPACITY)
125
+ throw Object.assign(new Error("Too many active submissions; reconcile or await terminal state."), {
126
+ code: "reconciliation_capacity",
127
+ });
128
+ reservations.push({ kind, clientRef });
129
+ if (clientRef !== undefined) reserved.add(clientRef);
130
+ };
131
+
132
+ const releaseAdmission = (kind: ReconciliationKind, clientRef?: string) => {
133
+ consumeReservation(kind, clientRef);
134
+ };
135
+
136
+ const noteAccepted = async (
137
+ kind: ReconciliationKind,
138
+ correlation: PromptCorrelation,
139
+ clientRef?: string,
140
+ extra?: { skillName?: string },
141
+ ) => {
142
+ cleanup();
143
+ consumeReservation(kind, clientRef);
144
+ const at = now();
145
+ const key = keyOf(kind, correlation);
146
+ const record: DurableReconciliationRecord = {
147
+ kind,
148
+ commandId: correlation.commandId,
149
+ turnId: correlation.turnId,
150
+ ...(clientRef !== undefined ? { clientRef } : {}),
151
+ status: "accepted",
152
+ acceptedAt: at,
153
+ ...(extra?.skillName ? { skillName: extra.skillName } : {}),
154
+ };
155
+ records.set(key, record);
156
+ if (clientRef !== undefined) clientRefIndex.set(refKey(kind, clientRef), key);
157
+ await persist();
158
+ };
159
+
160
+ const noteTransition = async (
161
+ kind: ReconciliationKind,
162
+ correlation: PromptCorrelation | undefined,
163
+ frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
164
+ ) => {
165
+ if (!correlation) return;
166
+ const record = records.get(keyOf(kind, correlation));
167
+ if (!record || record.terminalAt !== undefined) return;
168
+ if (frame.type === "agent_start") {
169
+ if (record.status === "accepted") {
170
+ record.status = "in_flight";
171
+ record.startedAt = now();
172
+ await persist();
173
+ }
174
+ return;
175
+ }
176
+ record.terminalAt = now();
177
+ if (frame.type === "agent_failed") {
178
+ record.status = "failed";
179
+ record.error = sanitizePromptFailure(frame.error);
180
+ } else {
181
+ record.status = "terminal_ok";
182
+ }
183
+ cleanup();
184
+ await persist();
185
+ };
186
+
187
+ const lookup = (
188
+ kind: ReconciliationKind,
189
+ selector: { commandId?: string; turnId?: string; clientRef?: string },
190
+ ): TurnPromptReconciliation => {
191
+ cleanup();
192
+ const key =
193
+ selector.clientRef !== undefined
194
+ ? clientRefIndex.get(refKey(kind, selector.clientRef))
195
+ : selector.commandId !== undefined && selector.turnId !== undefined
196
+ ? keyOf(kind, { commandId: selector.commandId, turnId: selector.turnId })
197
+ : undefined;
198
+ const record = key === undefined ? undefined : records.get(key);
199
+ if (!record) return { status: "unknown" };
200
+ const identity = {
201
+ commandId: record.commandId,
202
+ turnId: record.turnId,
203
+ ...(record.clientRef !== undefined ? { clientRef: record.clientRef } : {}),
204
+ acceptedAt: record.acceptedAt,
205
+ };
206
+ if (record.status === "accepted") return { status: "accepted", ...identity };
207
+ if (record.status === "in_flight")
208
+ return { status: "in_flight", ...identity, startedAt: record.startedAt as number };
209
+ const terminal = {
210
+ ...identity,
211
+ ...(record.startedAt !== undefined ? { startedAt: record.startedAt } : {}),
212
+ terminalAt: record.terminalAt as number,
213
+ };
214
+ if (record.status === "terminal_ok") return { status: "terminal_ok", ...terminal };
215
+ return { status: "failed", ...terminal, error: record.error ?? sanitizePromptFailure(undefined) };
216
+ };
217
+
218
+ const hydrateFromStore = async () => {
219
+ if (!store) return;
220
+ const loaded = await store.load();
221
+ records.clear();
222
+ clientRefIndex.clear();
223
+ for (const record of loaded) {
224
+ const key = keyOf(record.kind, record);
225
+ records.set(key, record);
226
+ if (record.clientRef !== undefined) clientRefIndex.set(refKey(record.kind, record.clientRef), key);
227
+ }
228
+ };
229
+
230
+ return {
231
+ admit,
232
+ releaseAdmission,
233
+ noteAccepted,
234
+ noteTransition,
235
+ lookup,
236
+ cleanup,
237
+ activeCount,
238
+ hydrateFromStore,
239
+ };
240
+ }
241
+
242
+ export type { PromptReconciliationStatus };
@@ -49,6 +49,49 @@ const USAGE = [
49
49
  "/session_recent [create|resume]",
50
50
  ].join("\n");
51
51
 
52
+ function tokenizeLifecycleCommand(value: string): string[] | undefined {
53
+ const tokens: string[] = [];
54
+ let token = "";
55
+ let quote: '"' | "'" | undefined;
56
+ let started = false;
57
+ for (let index = 0; index < value.length; index++) {
58
+ const character = value[index]!;
59
+ if (character === "\\") {
60
+ const next = value[index + 1];
61
+ if (next !== undefined && (next === "\\" || next === '"' || next === "'" || /\s/u.test(next))) {
62
+ token += next;
63
+ index++;
64
+ } else token += character;
65
+ started = true;
66
+ continue;
67
+ }
68
+ if (quote) {
69
+ if (character === quote) quote = undefined;
70
+ else token += character;
71
+ started = true;
72
+ continue;
73
+ }
74
+ if (character === '"' || character === "'") {
75
+ quote = character;
76
+ started = true;
77
+ continue;
78
+ }
79
+ if (/\s/u.test(character)) {
80
+ if (started) {
81
+ tokens.push(token);
82
+ token = "";
83
+ started = false;
84
+ }
85
+ continue;
86
+ }
87
+ token += character;
88
+ started = true;
89
+ }
90
+ if (quote) return undefined;
91
+ if (started) tokens.push(token);
92
+ return tokens;
93
+ }
94
+
52
95
  /** True when the text begins with any /session_* token, regardless of addressability. */
53
96
  export function isLifecycleCommandLikeText(text: string | undefined): boolean {
54
97
  if (!text) return false;
@@ -98,8 +141,10 @@ export function parseLifecycleCommand(
98
141
  text: string | undefined,
99
142
  ctx: { chatType?: string; botUsername?: string } = {},
100
143
  ): ParsedLifecycleCommand {
144
+ const tokens = tokenizeLifecycleCommand((text ?? "").trim());
145
+ if (!tokens) return { kind: "usage", message: USAGE };
146
+ const [rawCommand, ...args] = tokens;
101
147
  const raw = (text ?? "").trim();
102
- const [rawCommand, ...args] = raw.split(/\s+/);
103
148
  const command = normalizeLifecycleCommandToken(rawCommand ?? "", ctx);
104
149
  if (command === undefined) return { kind: "none" };
105
150
 
@@ -140,9 +185,8 @@ export function parseLifecycleCommand(
140
185
 
141
186
  // /session_create <kind> ... [--mpreset <profile>]
142
187
  const { positional, modelPreset } = extractModelPreset(args);
143
- if (modelPreset !== undefined && !isSafeIdentifier(modelPreset)) {
188
+ if (modelPreset !== undefined && modelPreset.length === 0)
144
189
  return { kind: "reject", reason: "invalid_target", message: `Invalid model preset name.\n\n${USAGE}` };
145
- }
146
190
  const kind = positional[0];
147
191
  if (kind === "path") {
148
192
  if (positional.length !== 2) return { kind: "usage", message: USAGE };
@@ -238,11 +282,12 @@ function isSafeBranch(value: string): boolean {
238
282
  /**
239
283
  * Map a lifecycle response/error to a user-facing Telegram message (G010).
240
284
  *
241
- * Only derives text from sessionId, mode, reason, a safe message, and candidate
242
- * {sessionId,path} — never a token or prompt. Each error reason gets tailored,
243
- * actionable copy; an "in progress" pending response is surfaced distinctly.
285
+ * Only derives text from sessionId, mode, reason, a safe message, the originating
286
+ * lifecycle verb, and candidate {sessionId,path} — never a token or prompt. Each
287
+ * error reason gets tailored, actionable copy; an "in progress" pending response
288
+ * is surfaced distinctly.
244
289
  */
245
- export function formatLifecycleOutcome(r: SessionLifecycleResponse): string {
290
+ export function formatLifecycleOutcome(r: SessionLifecycleResponse, verb?: LifecycleCommandVerb): string {
246
291
  switch (r.type) {
247
292
  case "session_create_response":
248
293
  return `\u{1f680} Launching session ${r.sessionId} in tmux. It will appear once ready \u2014 check /session_recent.`;
@@ -282,9 +327,17 @@ export function formatLifecycleOutcome(r: SessionLifecycleResponse): string {
282
327
  case "unsupported_platform":
283
328
  return "Remote session lifecycle is unavailable on this psmux host because SKC cannot prove immutable session identity. No lifecycle action was performed. Use a local SKC terminal with a supported tmux provider.";
284
329
  case "terminal_uncertain":
285
- return /in progress/i.test(r.message)
286
- ? "\u23f3 That request is already in progress \u2014 hold on."
287
- : "\u26a0\ufe0f Outcome uncertain. Check /session_recent before retrying so you don't double-spawn.";
330
+ if (/in progress/i.test(r.message)) return "\u23f3 That request is already in progress \u2014 hold on.";
331
+ switch (verb) {
332
+ case "session_create":
333
+ return "\u26a0\ufe0f Create outcome uncertain. The session may already be starting \u2014 check /session_recent before retrying to avoid starting it twice.";
334
+ case "session_close":
335
+ return "\u26a0\ufe0f Close outcome uncertain. The session may already be closed \u2014 check /session_recent before retrying.";
336
+ case "session_resume":
337
+ return "\u26a0\ufe0f Resume outcome uncertain. The session may already be reattached or restarting \u2014 check /session_recent before retrying.";
338
+ default:
339
+ return "\u26a0\ufe0f Outcome uncertain. Check /session_recent before retrying.";
340
+ }
288
341
  default:
289
342
  return `\u26a0\ufe0f ${r.reason}: ${r.message}`;
290
343
  }
@@ -1178,16 +1178,25 @@ export function outcomeToResponse(frame: SessionLifecycleRequest, outcome: Lifec
1178
1178
  if (frame.type === "session_close") {
1179
1179
  if (typeof e.sessionId !== "string" || typeof e.processGone !== "boolean")
1180
1180
  throw new Error("invalid close success ledger entry");
1181
+ if (!e.processGone) {
1182
+ return {
1183
+ type: "session_lifecycle_error",
1184
+ requestId: frame.requestId,
1185
+ status: "error",
1186
+ reason: "terminal_uncertain",
1187
+ message: "close did not confirm process termination",
1188
+ };
1189
+ }
1181
1190
  return {
1182
1191
  type: "session_close_response",
1183
1192
  requestId: frame.requestId,
1184
1193
  status: "ok",
1185
1194
  sessionId: e.sessionId,
1186
- processGone: e.processGone,
1195
+ processGone: true,
1187
1196
  historyPreserved: true,
1188
1197
  // The killed session's per-session endpoint record is reaped by the
1189
1198
  // daemon's dead-PID scan (scanRoots), so it is effectively stale.
1190
- endpointStale: e.processGone,
1199
+ endpointStale: true,
1191
1200
  };
1192
1201
  }
1193
1202
  if (
@@ -309,6 +309,21 @@ export async function handleLifecycleRequest(
309
309
 
310
310
  // 4. Durable idempotency.
311
311
  const doc = await deps.store.read();
312
+ const existing = doc.entries[key];
313
+ if (
314
+ frame.type === "session_close" &&
315
+ existing?.verb === "session_close" &&
316
+ existing.requestHash === hash &&
317
+ existing.state === "success" &&
318
+ typeof existing.sessionId === "string" &&
319
+ existing.processGone === false
320
+ ) {
321
+ existing.state = "terminal_uncertain";
322
+ existing.reason = "terminal_uncertain";
323
+ existing.updatedAt = deps.now();
324
+ delete existing.processGone;
325
+ await deps.store.write(doc);
326
+ }
312
327
  const dup = classifyDuplicate(doc.entries[key], hash);
313
328
  switch (dup.kind) {
314
329
  case "conflict":
@@ -394,12 +409,28 @@ export async function handleLifecycleRequest(
394
409
 
395
410
  if (frame.type === "session_close") {
396
411
  const closed = await deps.closeSession(frame.target);
412
+ if (!closed.processGone) {
413
+ Object.assign(entry, {
414
+ state: "terminal_uncertain",
415
+ updatedAt: deps.now(),
416
+ reason: "terminal_uncertain",
417
+ sessionId: frame.target.sessionId,
418
+ tmuxSession: frame.target.tmuxSession,
419
+ });
420
+ await deps.store.write(doc);
421
+ await deps.audit({ ...baseAudit, event: "terminal_uncertain", reason: "terminal_uncertain" });
422
+ return {
423
+ status: "error",
424
+ reason: "terminal_uncertain",
425
+ message: "session_close could not confirm process disappearance; manual check",
426
+ };
427
+ }
397
428
  Object.assign(entry, {
398
429
  state: "success",
399
430
  updatedAt: deps.now(),
400
431
  sessionId: frame.target.sessionId,
401
432
  tmuxSession: frame.target.tmuxSession,
402
- processGone: closed.processGone,
433
+ processGone: true,
403
434
  });
404
435
  await deps.store.write(doc);
405
436
  await deps.audit({ ...baseAudit, event: "success" });
@@ -1,5 +1,6 @@
1
1
  import type { CasRestoreResult } from "../../config/atomic-yaml-patch";
2
2
  import type { RawSettings, SettingsAtomicPatch, SettingsAtomicReceipt } from "../../config/settings";
3
+ import { isProcessIncarnation, processIncarnation } from "../broker/process-incarnation";
3
4
  import {
4
5
  getNotificationConfig,
5
6
  hasNonBlankValue,
@@ -53,6 +54,7 @@ export interface ProposedTelegramIdentityPreflightInput {
53
54
  deps?: {
54
55
  fs?: TelegramDaemonFs;
55
56
  pidAlive?: (pid: number) => boolean;
57
+ pidIncarnation?: (pid: number) => string | undefined;
56
58
  };
57
59
  }
58
60
 
@@ -109,6 +111,16 @@ export async function proposedTelegramIdentity(
109
111
 
110
112
  const pidAlive = input.deps?.pidAlive ?? defaultPidAlive;
111
113
  if (!pidAlive(metadata.pid)) return { status: "absent" };
114
+ const pidIncarnation = input.deps?.pidIncarnation ?? processIncarnation;
115
+ const persistedIncarnation = state.incarnation;
116
+ const currentIncarnation = pidIncarnation(metadata.pid);
117
+ if (
118
+ !isProcessIncarnation(persistedIncarnation) ||
119
+ !isProcessIncarnation(currentIncarnation) ||
120
+ persistedIncarnation !== currentIncarnation
121
+ ) {
122
+ return { status: "absent" };
123
+ }
112
124
 
113
125
  if (state.tokenFingerprint === tokenFingerprint(input.botToken) && state.chatId === input.chatId) {
114
126
  return {
@@ -28,6 +28,7 @@ import {
28
28
  tokenFingerprint,
29
29
  } from "./config";
30
30
  import { type DaemonPaths, daemonPaths, HEARTBEAT_TTL_MS } from "./daemon-paths";
31
+ import { type OwnerFreshnessSnapshot, readOwnerFreshnessSnapshot, type TelegramDaemonFs } from "./telegram-daemon";
31
32
  import { DAEMON_GENERATION } from "./telegram-daemon-contract";
32
33
 
33
34
  const DEFAULT_API_BASE = "https://api.telegram.org";
@@ -251,19 +252,29 @@ export function formatNotificationStatusReport(report: NotificationStatusReport)
251
252
 
252
253
  // --- endpoint / daemon file readers -------------------------------------
253
254
 
254
- interface EndpointView {
255
+ export interface NotificationEndpointView {
255
256
  sessionId: string;
256
257
  pid: number | undefined;
257
258
  stale: boolean;
258
259
  }
259
260
 
260
- type EndpointFileRead =
261
- | { kind: "endpoint"; view: EndpointView; identity: NotificationEndpointFileIdentity }
261
+ export type NotificationEndpointLiveness = "live" | "dead" | "unknown";
262
+
263
+ /**
264
+ * Classification used by recovery and startup takeover. A file is an endpoint
265
+ * only when it has endpoint authority fields; lifecycle/audit records are never
266
+ * candidates for endpoint cleanup.
267
+ */
268
+ export type NotificationEndpointClassification =
269
+ | {
270
+ kind: "endpoint";
271
+ view: NotificationEndpointView;
272
+ liveness: NotificationEndpointLiveness;
273
+ identity: NotificationEndpointFileIdentity;
274
+ }
262
275
  | { kind: "non-endpoint" }
263
276
  | { kind: "unreadable" };
264
277
 
265
- type EndpointLiveness = "live" | "dead" | "unknown";
266
-
267
278
  /**
268
279
  * Classify an endpoint using owner-proof semantics. An endpoint is only `dead`
269
280
  * with positive proof: an explicit `stale` tombstone, or a recorded pid that is
@@ -271,7 +282,10 @@ type EndpointLiveness = "live" | "dead" | "unknown";
271
282
  * must never be treated as dead — removing it could delete a live session's
272
283
  * discovery file that simply omitted a pid.
273
284
  */
274
- function endpointLiveness(view: EndpointView, pidAlive: (pid: number) => boolean): EndpointLiveness {
285
+ export function notificationEndpointLiveness(
286
+ view: NotificationEndpointView,
287
+ pidAlive: (pid: number) => boolean,
288
+ ): NotificationEndpointLiveness {
275
289
  if (view.stale) return "dead";
276
290
  if (view.pid === undefined) return "unknown";
277
291
  return pidAlive(view.pid) ? "live" : "dead";
@@ -314,11 +328,19 @@ function isCanonicalLifecycleArtifactName(name: string): boolean {
314
328
  );
315
329
  }
316
330
 
317
- function unreadableEndpointResult(file: string): EndpointFileRead {
331
+ function unreadableEndpointResult(file: string): NotificationEndpointClassification {
318
332
  return isCanonicalLifecycleArtifactName(path.basename(file)) ? { kind: "non-endpoint" } : { kind: "unreadable" };
319
333
  }
320
334
 
321
- async function readEndpointView(fs: NotificationServiceFs, file: string): Promise<EndpointFileRead> {
335
+ /**
336
+ * Read and classify one endpoint candidate. The returned identity belongs to
337
+ * exactly the bytes inspected and is required for any later deletion.
338
+ */
339
+ export async function classifyNotificationEndpoint(
340
+ fs: Pick<NotificationServiceFs, "readEndpointFile">,
341
+ file: string,
342
+ pidAlive: (pid: number) => boolean,
343
+ ): Promise<NotificationEndpointClassification> {
322
344
  let endpoint: NotificationEndpointFile;
323
345
  let raw: string;
324
346
  try {
@@ -334,16 +356,19 @@ async function readEndpointView(fs: NotificationServiceFs, file: string): Promis
334
356
  return unreadableEndpointResult(file);
335
357
  }
336
358
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "non-endpoint" };
359
+ if (path.basename(file) === "broker.json") return { kind: "non-endpoint" };
337
360
  const rec = parsed as Record<string, unknown>;
338
361
  if (isLifecycleArtifact(rec) || typeof rec.url !== "string" || typeof rec.token !== "string")
339
362
  return { kind: "non-endpoint" };
363
+ const view: NotificationEndpointView = {
364
+ sessionId: typeof rec.sessionId === "string" ? rec.sessionId : path.basename(file, ".json"),
365
+ pid: safePositiveInteger(rec.pid),
366
+ stale: rec.stale === true,
367
+ };
340
368
  return {
341
369
  kind: "endpoint",
342
- view: {
343
- sessionId: typeof rec.sessionId === "string" ? rec.sessionId : path.basename(file, ".json"),
344
- pid: safePositiveInteger(rec.pid),
345
- stale: rec.stale === true,
346
- },
370
+ view,
371
+ liveness: notificationEndpointLiveness(view, pidAlive),
347
372
  identity: endpoint.identity,
348
373
  };
349
374
  }
@@ -554,10 +579,21 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
554
579
  checks.push({ name: "config", level: "ok", detail: "enabled with at least one configured adapter" });
555
580
  }
556
581
 
557
- // Daemon ownership state (offline; read the persisted state file directly).
558
- const paths = daemonPaths(opts.settings.getAgentDir());
559
- const state = await readDaemonStateFile(fs, paths.state);
560
- const heartbeatAt = state?.heartbeatAt;
582
+ // Daemon ownership state (offline; corrupt or unreadable state degrades to a
583
+ // warning instead of making the health command itself fail).
584
+ let daemonStateUnreadable = false;
585
+ let snapshot: OwnerFreshnessSnapshot;
586
+ try {
587
+ snapshot = await readOwnerFreshnessSnapshot({
588
+ settings: opts.settings,
589
+ fs: fs as unknown as TelegramDaemonFs,
590
+ });
591
+ } catch {
592
+ daemonStateUnreadable = true;
593
+ snapshot = { ownerTag: null, effectiveHeartbeatAt: undefined, legacyEmbedded: false, state: undefined };
594
+ }
595
+ const state = snapshot.state ? parseDaemonState(JSON.stringify(snapshot.state)) : undefined;
596
+ const heartbeatAt = finiteNonNegativeNumber(snapshot.effectiveHeartbeatAt);
561
597
  const daemon: DaemonHealth = {
562
598
  present: Boolean(state),
563
599
  ownerId: state?.ownerId,
@@ -576,7 +612,9 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
576
612
  currentGeneration: DAEMON_GENERATION,
577
613
  generationRelation: daemonGenerationRelation(state),
578
614
  };
579
- if (!state) {
615
+ if (daemonStateUnreadable) {
616
+ checks.push({ name: "daemon", level: "warn", detail: "daemon ownership record is corrupt or unreadable" });
617
+ } else if (!state) {
580
618
  checks.push({ name: "daemon", level: "ok", detail: "no daemon ownership record (none running)" });
581
619
  } else if (!daemon.alive) {
582
620
  checks.push({
@@ -604,14 +642,13 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
604
642
  let unknownEndpoints = 0;
605
643
  let unreadable = 0;
606
644
  for (const name of files) {
607
- const record = await readEndpointView(fs, path.join(dir, name));
645
+ const record = await classifyNotificationEndpoint(fs, path.join(dir, name), pidAlive);
608
646
  if (record.kind === "non-endpoint") continue;
609
647
  if (record.kind === "unreadable") {
610
648
  unreadable += 1;
611
649
  continue;
612
650
  }
613
- const view = record.view;
614
- switch (endpointLiveness(view, pidAlive)) {
651
+ switch (record.liveness) {
615
652
  case "live":
616
653
  live += 1;
617
654
  break;
@@ -877,7 +914,16 @@ async function detachTransitionMarker(
877
914
  ): Promise<boolean> {
878
915
  if (!snapshot.identity || !fs.exactUnlink) return false;
879
916
  try {
880
- return (await fs.exactUnlink(path, snapshot.identity)).ok;
917
+ const removed = await fs.exactUnlink(path, snapshot.identity);
918
+ if (removed.ok) return true;
919
+ // Accept only typed retained authority: a concrete detached quarantine plus
920
+ // a proven-absent canonical marker pathname. Anything else stays fail-closed.
921
+ return (
922
+ removed.code === "cleanup_pending" &&
923
+ typeof removed.detachedPath === "string" &&
924
+ removed.detachedPath.length > 0 &&
925
+ (await fs.readFile(path, "utf8").catch(() => undefined)) === undefined
926
+ );
881
927
  } catch {
882
928
  return false;
883
929
  }
@@ -1040,7 +1086,7 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
1040
1086
  let unreadable = 0;
1041
1087
  for (const name of files) {
1042
1088
  const file = path.join(dir, name);
1043
- const record = await readEndpointView(fs, file);
1089
+ const record = await classifyNotificationEndpoint(fs, file, pidAlive);
1044
1090
  if (record.kind === "non-endpoint") continue;
1045
1091
  if (record.kind === "unreadable") {
1046
1092
  // Leave unparseable files untouched: they may be mid-write by a live server.
@@ -1048,7 +1094,7 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
1048
1094
  continue;
1049
1095
  }
1050
1096
  const view = record.view;
1051
- if (endpointLiveness(view, pidAlive) !== "dead") {
1097
+ if (record.liveness !== "dead") {
1052
1098
  // Keep live AND unknown (PID-less) endpoints: only positive proof of
1053
1099
  // death (a stale tombstone or a dead pid) authorizes removal.
1054
1100
  kept += 1;
@@ -1057,11 +1103,27 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
1057
1103
  try {
1058
1104
  const result = await fs.exactUnlink(file, record.identity);
1059
1105
  if (!result.ok) {
1060
- recoveryFailures += 1;
1061
1106
  if (result.detachedPath) detached.push(result.detachedPath);
1062
1107
  if (result.retainedSuccessorPath) retainedSuccessors.push(result.retainedSuccessorPath);
1063
1108
  if (result.retainedPlaceholderPath) retainedPlaceholders.push(result.retainedPlaceholderPath);
1064
1109
  if (result.retainedUnknownPath) retainedUnknown.push(result.retainedUnknownPath);
1110
+ // Typed retained authority with a concrete quarantine and a proven-absent
1111
+ // canonical endpoint counts as removed; the detached path above is the
1112
+ // durable evidence. Anything else is a genuine recovery failure.
1113
+ const retainedRemoved =
1114
+ result.code === "cleanup_pending" &&
1115
+ typeof result.detachedPath === "string" &&
1116
+ result.detachedPath.length > 0 &&
1117
+ (await fs.readFile(file, "utf8").catch(() => undefined)) === undefined;
1118
+ if (retainedRemoved) {
1119
+ removed.push({
1120
+ sessionId: view.sessionId,
1121
+ pid: view.pid,
1122
+ reason: view.stale ? "stale-flag" : "dead-pid",
1123
+ });
1124
+ continue;
1125
+ }
1126
+ recoveryFailures += 1;
1065
1127
  if (
1066
1128
  !result.detachedPath &&
1067
1129
  !result.retainedSuccessorPath &&