@team-agent/installer 0.2.11 → 0.3.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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,591 @@
1
+ //! result_delivery.py — watcher 通知/去重/有界重试/claim-leader requeue (card §69-71)。
2
+
3
+ use std::path::Path;
4
+
5
+ use rusqlite::{params, OptionalExtension};
6
+
7
+ use crate::event_log::EventLog;
8
+ use crate::message_store::{MessageStore, NotificationClaimParams};
9
+ use crate::model::ids::{TaskId, TeamKey};
10
+ use crate::transport::PaneId;
11
+
12
+ use super::{MessagingError, WatcherNotice, RESULT_DELIVERY_MAX_ATTEMPTS};
13
+
14
+ /// `notify_result_watchers` (`result_delivery.py:38`):匹配 + 去重 + (有界) 投递 result 给 leader
15
+ /// watcher。**恰好一次** (Gap 32/38):同 result_id 多 watcher → 1 次注入,余 `superseded`。
16
+ /// 去重唯一原语 = [`MessageStore::claim_leader_notification_delivery`]。`collect`/coordinator tick 调。
17
+ pub fn notify_result_watchers(
18
+ workspace: &Path,
19
+ result: &serde_json::Value,
20
+ event_log: &EventLog,
21
+ watchers: Option<&[serde_json::Value]>,
22
+ dedupe_reason: Option<&str>,
23
+ ) -> Result<Vec<WatcherNotice>, MessagingError> {
24
+ let _ = dedupe_reason;
25
+ let Some(watchers) = watchers else {
26
+ return Ok(Vec::new());
27
+ };
28
+ let store = MessageStore::open(workspace)?;
29
+ let conn = crate::db::schema::open_db(store.db_path())?;
30
+ let result_task = result.get("task_id").and_then(|v| v.as_str());
31
+ let result_agent = result.get("agent_id").and_then(|v| v.as_str());
32
+ let result_id = result.get("result_id").and_then(|v| v.as_str()).map(ToString::to_string);
33
+ let mut matched: Vec<&serde_json::Value> = watchers
34
+ .iter()
35
+ .filter(|w| watcher_matches(w, result_task, result_agent))
36
+ .collect();
37
+ matched.sort_by(|a, b| {
38
+ let ac = a.get("created_at").and_then(|v| v.as_str()).unwrap_or("");
39
+ let bc = b.get("created_at").and_then(|v| v.as_str()).unwrap_or("");
40
+ ac.cmp(bc)
41
+ });
42
+ let mut notices = Vec::new();
43
+ let mut primary_watcher_id = None;
44
+ for (idx, watcher) in matched.iter().enumerate() {
45
+ if let Some(watcher_id) = watcher.get("watcher_id").and_then(|v| v.as_str()) {
46
+ if idx == 0 {
47
+ primary_watcher_id = Some(watcher_id.to_string());
48
+ notices.push(deliver_primary_watcher(
49
+ &conn,
50
+ &store,
51
+ event_log,
52
+ watcher,
53
+ result,
54
+ result_id.as_deref(),
55
+ result_task,
56
+ )?);
57
+ } else {
58
+ let primary = primary_watcher_id.clone().unwrap_or_default();
59
+ let error = "superseded by earlier watcher for same (task_id, agent_id, result_id)";
60
+ let now = chrono::Utc::now().to_rfc3339();
61
+ conn.execute(
62
+ "update result_watchers
63
+ set status = 'superseded', completed_at = ?2, result_id = ?3, error = ?4
64
+ where watcher_id = ?1",
65
+ params![watcher_id, now, result_id, error],
66
+ )?;
67
+ event_log.write(
68
+ "result_watcher.superseded",
69
+ serde_json::json!({
70
+ "watcher_id": watcher_id,
71
+ "result_id": result_id,
72
+ "task_id": result_task,
73
+ "agent_id": result_agent,
74
+ "primary_watcher_id": primary,
75
+ }),
76
+ )?;
77
+ notices.push(WatcherNotice {
78
+ watcher_id: watcher_id.to_string(),
79
+ result_id: result_id.clone(),
80
+ ok: false,
81
+ status: Some("superseded".to_string()),
82
+ notified_message_id: None,
83
+ primary_watcher_id: Some(primary),
84
+ prior_state: None,
85
+ error: Some(error.to_string()),
86
+ });
87
+ }
88
+ }
89
+ }
90
+ Ok(notices)
91
+ }
92
+
93
+ fn watcher_matches(
94
+ watcher: &serde_json::Value,
95
+ result_task: Option<&str>,
96
+ result_agent: Option<&str>,
97
+ ) -> bool {
98
+ let task_matches = watcher
99
+ .get("task_id")
100
+ .and_then(|v| v.as_str())
101
+ .filter(|s| !s.is_empty())
102
+ .is_none_or(|task| Some(task) == result_task);
103
+ let agent_matches = watcher
104
+ .get("agent_id")
105
+ .and_then(|v| v.as_str())
106
+ .filter(|s| !s.is_empty())
107
+ .is_none_or(|agent| Some(agent) == result_agent);
108
+ task_matches && agent_matches
109
+ }
110
+
111
+ fn deliver_primary_watcher(
112
+ conn: &rusqlite::Connection,
113
+ store: &MessageStore,
114
+ event_log: &EventLog,
115
+ watcher: &serde_json::Value,
116
+ result: &serde_json::Value,
117
+ result_id: Option<&str>,
118
+ result_task: Option<&str>,
119
+ ) -> Result<WatcherNotice, MessagingError> {
120
+ let watcher_id = watcher
121
+ .get("watcher_id")
122
+ .and_then(|v| v.as_str())
123
+ .unwrap_or_default();
124
+ let Some(result_id) = result_id else {
125
+ return update_watcher_failure(
126
+ conn,
127
+ event_log,
128
+ watcher_id,
129
+ None,
130
+ "notify_failed",
131
+ "missing_result_id",
132
+ );
133
+ };
134
+ if let Some(existing) = delivered_result_message(
135
+ store,
136
+ result_id,
137
+ result_task.map(TaskId::new).as_ref(),
138
+ watcher
139
+ .get("owner_team_id")
140
+ .and_then(|v| v.as_str())
141
+ .map(TeamKey::new)
142
+ .as_ref(),
143
+ )? {
144
+ let message_id = existing
145
+ .get("message_id")
146
+ .and_then(|v| v.as_str())
147
+ .map(ToString::to_string);
148
+ mark_watcher_notified(conn, event_log, watcher_id, result_id, message_id.as_deref())?;
149
+ return Ok(WatcherNotice {
150
+ watcher_id: watcher_id.to_string(),
151
+ result_id: Some(result_id.to_string()),
152
+ ok: true,
153
+ status: Some("notified".to_string()),
154
+ notified_message_id: message_id,
155
+ primary_watcher_id: None,
156
+ prior_state: None,
157
+ error: None,
158
+ });
159
+ }
160
+ let attempts = result_delivery_attempts(event_log, watcher_id, result_id)?;
161
+ if attempts >= u64::from(RESULT_DELIVERY_MAX_ATTEMPTS) {
162
+ return update_watcher_failure(
163
+ conn,
164
+ event_log,
165
+ watcher_id,
166
+ Some(result_id),
167
+ "delivery_exhausted",
168
+ "delivery_exhausted",
169
+ );
170
+ }
171
+ let content = format_result_watcher_notification(result);
172
+ let message_id = store.create_message(
173
+ result_task,
174
+ watcher.get("leader_id").and_then(|v| v.as_str()).unwrap_or("team-agent"),
175
+ "leader",
176
+ &content,
177
+ None,
178
+ false,
179
+ watcher.get("owner_team_id").and_then(|v| v.as_str()),
180
+ )?;
181
+ let claim = store.claim_leader_notification_delivery(NotificationClaimParams {
182
+ result_id,
183
+ owner_team_id: watcher.get("owner_team_id").and_then(|v| v.as_str()),
184
+ owner_epoch: None,
185
+ leader_session_uuid: None,
186
+ proposed_message_id: &message_id,
187
+ envelope_hash: "",
188
+ pane_id: None,
189
+ })?;
190
+ if claim.status == "claimed_by_you" {
191
+ mark_watcher_notified(
192
+ conn,
193
+ event_log,
194
+ watcher_id,
195
+ result_id,
196
+ Some(&claim.notified_message_id),
197
+ )?;
198
+ Ok(WatcherNotice {
199
+ watcher_id: watcher_id.to_string(),
200
+ result_id: Some(result_id.to_string()),
201
+ ok: true,
202
+ status: Some("notified".to_string()),
203
+ notified_message_id: Some(claim.notified_message_id),
204
+ primary_watcher_id: None,
205
+ prior_state: None,
206
+ error: None,
207
+ })
208
+ } else {
209
+ update_watcher_failure(
210
+ conn,
211
+ event_log,
212
+ watcher_id,
213
+ Some(result_id),
214
+ "notify_failed",
215
+ "already_notified_by",
216
+ )
217
+ }
218
+ }
219
+
220
+ fn mark_watcher_notified(
221
+ conn: &rusqlite::Connection,
222
+ event_log: &EventLog,
223
+ watcher_id: &str,
224
+ result_id: &str,
225
+ message_id: Option<&str>,
226
+ ) -> Result<(), MessagingError> {
227
+ let now = chrono::Utc::now().to_rfc3339();
228
+ conn.execute(
229
+ "update result_watchers
230
+ set status = 'notified', notified_message_id = ?3, completed_at = ?4, result_id = ?2, error = null
231
+ where watcher_id = ?1",
232
+ params![watcher_id, result_id, message_id, now],
233
+ )?;
234
+ event_log.write(
235
+ "result_watcher.notified",
236
+ serde_json::json!({"watcher_id": watcher_id, "result_id": result_id, "message_id": message_id}),
237
+ )?;
238
+ Ok(())
239
+ }
240
+
241
+ fn update_watcher_failure(
242
+ conn: &rusqlite::Connection,
243
+ event_log: &EventLog,
244
+ watcher_id: &str,
245
+ result_id: Option<&str>,
246
+ status: &str,
247
+ error: &str,
248
+ ) -> Result<WatcherNotice, MessagingError> {
249
+ let now = chrono::Utc::now().to_rfc3339();
250
+ conn.execute(
251
+ "update result_watchers
252
+ set status = ?2, completed_at = ?3, result_id = ?4, error = ?5
253
+ where watcher_id = ?1",
254
+ params![watcher_id, status, now, result_id, error],
255
+ )?;
256
+ event_log.write(
257
+ "result_watcher.notify_failed",
258
+ serde_json::json!({"watcher_id": watcher_id, "result_id": result_id, "status": status, "error": error}),
259
+ )?;
260
+ Ok(WatcherNotice {
261
+ watcher_id: watcher_id.to_string(),
262
+ result_id: result_id.map(ToString::to_string),
263
+ ok: false,
264
+ status: Some(status.to_string()),
265
+ notified_message_id: None,
266
+ primary_watcher_id: None,
267
+ prior_state: None,
268
+ error: Some(error.to_string()),
269
+ })
270
+ }
271
+
272
+ fn result_delivery_attempts(
273
+ event_log: &EventLog,
274
+ watcher_id: &str,
275
+ result_id: &str,
276
+ ) -> Result<u64, MessagingError> {
277
+ let events = event_log.tail(0)?;
278
+ Ok(events
279
+ .iter()
280
+ .filter(|event| {
281
+ event.get("watcher_id").and_then(|v| v.as_str()) == Some(watcher_id)
282
+ && event.get("result_id").and_then(|v| v.as_str()) == Some(result_id)
283
+ && matches!(
284
+ event.get("event").and_then(|v| v.as_str()),
285
+ Some("result_watcher.notify_failed" | "result_watcher.retry_notified")
286
+ )
287
+ })
288
+ .count()
289
+ .try_into()
290
+ .unwrap_or(u64::MAX))
291
+ }
292
+
293
+ /// `retry_result_deliveries` (`result_delivery.py:19`):重投 `notify_failed` watcher。
294
+ /// coordinator tick + claim-leader 调。daemon-path → Result。
295
+ pub fn retry_result_deliveries(
296
+ workspace: &Path,
297
+ event_log: &EventLog,
298
+ ) -> Result<Vec<WatcherNotice>, MessagingError> {
299
+ let store = MessageStore::open(workspace)?;
300
+ let conn = crate::db::schema::open_db(store.db_path())?;
301
+ let mut stmt = conn.prepare(
302
+ "select watcher_id, result_id from result_watchers
303
+ where status in ('pending', 'notify_failed') and result_id is not null and notified_message_id is null
304
+ order by created_at, watcher_id",
305
+ )?;
306
+ let rows = stmt.query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)))?;
307
+ let mut notices = Vec::new();
308
+ for row in rows {
309
+ let (watcher_id, result_id) = row?;
310
+ let result: Option<String> = conn
311
+ .query_row(
312
+ "select envelope from results where result_id = ?1",
313
+ params![result_id],
314
+ |r| r.get(0),
315
+ )
316
+ .optional()?;
317
+ if let Some(envelope) = result {
318
+ let parsed: serde_json::Value = serde_json::from_str(&envelope)?;
319
+ conn.execute(
320
+ "update result_watchers set status = 'notified', completed_at = ?2 where watcher_id = ?1",
321
+ params![watcher_id, chrono::Utc::now().to_rfc3339()],
322
+ )?;
323
+ event_log.write(
324
+ "result_watcher.retry_notified",
325
+ serde_json::json!({"watcher_id": watcher_id, "result_id": result_id}),
326
+ )?;
327
+ notices.push(WatcherNotice {
328
+ watcher_id,
329
+ result_id: Some(result_id),
330
+ ok: true,
331
+ status: Some("notified".to_string()),
332
+ notified_message_id: delivered_result_message(&store, parsed.get("result_id").and_then(|v| v.as_str()).unwrap_or(""), None, None)?
333
+ .and_then(|v| v.get("message_id").and_then(|id| id.as_str()).map(ToString::to_string)),
334
+ primary_watcher_id: None,
335
+ prior_state: None,
336
+ error: None,
337
+ });
338
+ }
339
+ }
340
+ Ok(notices)
341
+ }
342
+
343
+ /// `requeue_after_claim_leader` (`result_delivery.py:428`):Gap 26 —— 认领新 leader pane 后把
344
+ /// 未投递 watcher 重路由到新 pane。**`notified_message_id` 必须存活** (Gap 32,清空会二次注入)。
345
+ /// step 10 claim-leader 调。
346
+ pub fn requeue_after_claim_leader(
347
+ workspace: &Path,
348
+ store: &MessageStore,
349
+ event_log: &EventLog,
350
+ owner_team_id: &TeamKey,
351
+ claimed_pane_id: &PaneId,
352
+ incident_ts: Option<&str>,
353
+ ) -> Result<Vec<WatcherNotice>, MessagingError> {
354
+ let conn = crate::db::schema::open_db(store.db_path())?;
355
+ let mut stmt = conn.prepare(
356
+ "select watcher_id, result_id, status, coalesce(completed_at, created_at) from result_watchers
357
+ where owner_team_id = ?1 and notified_message_id is null
358
+ order by created_at, watcher_id",
359
+ )?;
360
+ let rows = stmt.query_map(params![owner_team_id.as_str()], |row| {
361
+ Ok((
362
+ row.get::<_, String>(0)?,
363
+ row.get::<_, Option<String>>(1)?,
364
+ row.get::<_, String>(2)?,
365
+ row.get::<_, String>(3)?,
366
+ ))
367
+ })?;
368
+ let mut out = Vec::new();
369
+ for row in rows {
370
+ let (watcher_id, result_id, prior_state, latest_ts) = row?;
371
+ if incident_ts.is_some_and(|incident| latest_ts.as_str() < incident) {
372
+ continue;
373
+ }
374
+ let requeued_at = chrono::Utc::now().to_rfc3339();
375
+ conn.execute(
376
+ "update result_watchers set status = 'notify_failed', completed_at = ?2 where watcher_id = ?1",
377
+ params![watcher_id.as_str(), requeued_at.as_str()],
378
+ )?;
379
+ out.push(WatcherNotice {
380
+ watcher_id: watcher_id.clone(),
381
+ result_id: result_id.clone(),
382
+ ok: true,
383
+ status: Some("notify_failed".to_string()),
384
+ notified_message_id: None,
385
+ primary_watcher_id: None,
386
+ prior_state: Some(prior_state.clone()),
387
+ error: None,
388
+ });
389
+ event_log.write(
390
+ "leader_receiver.claim_requeue",
391
+ serde_json::json!({
392
+ "result_id": result_id,
393
+ "watcher_id": watcher_id,
394
+ "prior_state": prior_state,
395
+ "requeued_at": requeued_at,
396
+ "claimed_pane_id": claimed_pane_id.as_str(),
397
+ "team_id": owner_team_id.as_str(),
398
+ }),
399
+ )?;
400
+ }
401
+ // #231 C-5: requeue blocked leader-bound messages that hit I-4 `rebind_required`
402
+ // while no leader pane was attached. Same row, same message_id — flip status back
403
+ // from `failed`/`leader_not_attached` to `accepted` so `deliver_pending_messages`
404
+ // replays it through the SAME pipeline. The leader_notification_log PK is already
405
+ // there (primitive wrote it before the unbound check), so this replay can't create
406
+ // a duplicate notification — exactly-once across rebind, no new send/notify rows.
407
+ let requeued_blocked = conn.execute(
408
+ "update messages
409
+ set status = 'accepted',
410
+ error = null,
411
+ updated_at = ?3
412
+ where recipient = 'leader'
413
+ and status = 'failed'
414
+ and error = 'leader_not_attached'
415
+ and owner_team_id = ?1",
416
+ params![
417
+ owner_team_id.as_str(),
418
+ claimed_pane_id.as_str(),
419
+ chrono::Utc::now().to_rfc3339(),
420
+ ],
421
+ )?;
422
+ if requeued_blocked > 0 {
423
+ event_log.write(
424
+ "leader_receiver.blocked_messages_requeued",
425
+ serde_json::json!({
426
+ "team_id": owner_team_id.as_str(),
427
+ "claimed_pane_id": claimed_pane_id.as_str(),
428
+ "count": requeued_blocked,
429
+ }),
430
+ )?;
431
+ }
432
+ if !out.is_empty() {
433
+ let _ = retry_result_deliveries(workspace, event_log)?;
434
+ }
435
+ Ok(out)
436
+ }
437
+
438
+ /// `requeue_delivery_exhausted_watchers`: attach-leader 成功后把已经耗尽投递
439
+ /// 重试的 watcher 放回 notify_failed, 留给 coordinator tick 重试投递。
440
+ pub fn requeue_delivery_exhausted_watchers(
441
+ _workspace: &Path,
442
+ store: &MessageStore,
443
+ event_log: &EventLog,
444
+ owner_team_id: &TeamKey,
445
+ claimed_pane_id: &PaneId,
446
+ ) -> Result<Vec<WatcherNotice>, MessagingError> {
447
+ let conn = crate::db::schema::open_db(store.db_path())?;
448
+ let mut stmt = conn.prepare(
449
+ "select watcher_id, result_id, status from result_watchers
450
+ where owner_team_id = ?1 and status = 'delivery_exhausted' and notified_message_id is null
451
+ order by created_at, watcher_id",
452
+ )?;
453
+ let rows = stmt.query_map(params![owner_team_id.as_str()], |row| {
454
+ Ok((
455
+ row.get::<_, String>(0)?,
456
+ row.get::<_, Option<String>>(1)?,
457
+ row.get::<_, String>(2)?,
458
+ ))
459
+ })?;
460
+ let mut out = Vec::new();
461
+ for row in rows {
462
+ let (watcher_id, result_id, prior_state) = row?;
463
+ conn.execute(
464
+ "update result_watchers set status = 'notify_failed', completed_at = null, error = null where watcher_id = ?1",
465
+ params![watcher_id.as_str()],
466
+ )?;
467
+ out.push(WatcherNotice {
468
+ watcher_id: watcher_id.clone(),
469
+ result_id: result_id.clone(),
470
+ ok: true,
471
+ status: Some("notify_failed".to_string()),
472
+ notified_message_id: None,
473
+ primary_watcher_id: None,
474
+ prior_state: Some(prior_state.clone()),
475
+ error: None,
476
+ });
477
+ event_log.write(
478
+ "result_watcher.requeued",
479
+ serde_json::json!({
480
+ "watcher_id": watcher_id,
481
+ "trigger": "attach_leader",
482
+ "new_pane_id": claimed_pane_id.as_str(),
483
+ }),
484
+ )?;
485
+ }
486
+ drop(stmt);
487
+ Ok(out)
488
+ }
489
+
490
+ /// `delivered_result_message` (`result_delivery.py:394`):内容级去重 —— 查某 result_id 是否已有
491
+ /// 投递的 leader 通知消息。
492
+ pub fn delivered_result_message(
493
+ store: &MessageStore,
494
+ result_id: &str,
495
+ task_id: Option<&TaskId>,
496
+ owner_team_id: Option<&TeamKey>,
497
+ ) -> Result<Option<serde_json::Value>, MessagingError> {
498
+ if result_id.trim().is_empty() {
499
+ return Ok(None);
500
+ }
501
+ let conn = crate::db::schema::open_db(store.db_path())?;
502
+ let mut sql = "select message_id, content from messages
503
+ where recipient = 'leader'
504
+ and status in ('visible', 'submitted', 'submitted_unverified', 'delivered', 'acknowledged')"
505
+ .to_string();
506
+ if task_id.is_some() {
507
+ sql.push_str(" and task_id = ?1");
508
+ }
509
+ if owner_team_id.is_some() {
510
+ sql.push_str(if task_id.is_some() {
511
+ " and owner_team_id = ?2"
512
+ } else {
513
+ " and owner_team_id = ?1"
514
+ });
515
+ }
516
+ let mut stmt = conn.prepare(&sql)?;
517
+ let rows: Vec<(String, String)> = match (task_id, owner_team_id) {
518
+ (Some(task), Some(team)) => stmt
519
+ .query_map(params![task.as_str(), team.as_str()], message_content_row)?
520
+ .collect::<Result<Vec<_>, _>>()?,
521
+ (Some(task), None) => stmt
522
+ .query_map(params![task.as_str()], message_content_row)?
523
+ .collect::<Result<Vec<_>, _>>()?,
524
+ (None, Some(team)) => stmt
525
+ .query_map(params![team.as_str()], message_content_row)?
526
+ .collect::<Result<Vec<_>, _>>()?,
527
+ (None, None) => stmt
528
+ .query_map([], message_content_row)?
529
+ .collect::<Result<Vec<_>, _>>()?,
530
+ };
531
+ for row in rows.into_iter().rev() {
532
+ let (message_id, content) = row;
533
+ if result_id_from_text(&content).as_deref() == Some(result_id) {
534
+ return Ok(Some(serde_json::json!({"message_id": message_id, "content": content})));
535
+ }
536
+ }
537
+ Ok(None)
538
+ }
539
+
540
+ fn message_content_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<(String, String)> {
541
+ Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
542
+ }
543
+
544
+ /// `result_id_from_text` (`result_delivery.py:415`):解析通知文案的 `Result id: <id>` 行做内容级
545
+ /// 去重。**格式字节级稳定** (golden fixture,card §53)。
546
+ pub fn result_id_from_text(content: &str) -> Option<String> {
547
+ for line in content.lines() {
548
+ if let Some(rest) = line.strip_prefix("Result id: ") {
549
+ let id = rest.trim();
550
+ if id.is_empty() {
551
+ return None;
552
+ }
553
+ return Some(id.to_string());
554
+ }
555
+ }
556
+ None
557
+ }
558
+
559
+ /// `format_result_watcher_notification` (`result_delivery.py:521`):拼 watcher 通知文案 +
560
+ /// `Result id: <id>` 行。**格式必须字节级稳定** (golden fixture,与 [`result_id_from_text`] 对偶)。
561
+ pub fn format_result_watcher_notification(result: &serde_json::Value) -> String {
562
+ let task_id = result.get("task_id").and_then(|v| v.as_str()).unwrap_or("unknown task");
563
+ let agent_id = result.get("agent_id").and_then(|v| v.as_str()).unwrap_or("unknown agent");
564
+ let status = result.get("status").and_then(|v| v.as_str()).unwrap_or("unknown");
565
+ let summary = result.get("summary").and_then(|v| v.as_str()).unwrap_or("completed");
566
+ let mut lines = vec![format!("Task {task_id} reported {status} from {agent_id}: {summary}")];
567
+ if let Some(tests) = result.get("tests").and_then(|v| v.as_array()) {
568
+ let rendered: Vec<String> = tests
569
+ .iter()
570
+ .take(3)
571
+ .filter_map(|test| {
572
+ let command = test.get("command").and_then(|v| v.as_str())?;
573
+ let status = test.get("status").and_then(|v| v.as_str())?;
574
+ Some(format!("{command}={status}"))
575
+ })
576
+ .collect();
577
+ if !rendered.is_empty() {
578
+ lines.push(format!("Tests: {}", rendered.join("; ")));
579
+ }
580
+ }
581
+ if let Some(result_id) = result.get("result_id").and_then(|v| v.as_str()) {
582
+ if !result_id.is_empty() {
583
+ lines.push(format!("Result id: {result_id}"));
584
+ }
585
+ }
586
+ lines.push(
587
+ "Team Agent has collected this result and updated team_state.md. No manual polling is needed."
588
+ .to_string(),
589
+ );
590
+ lines.join("\n")
591
+ }