@team-agent/installer 0.2.11 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +1204 -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 +1207 -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 +557 -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 +1084 -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 +526 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1101 -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 +237 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +272 -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 +410 -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 +489 -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 +2109 -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 +985 -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 +710 -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 +187 -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 +468 -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 +743 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +329 -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 +553 -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 +578 -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 +659 -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 +765 -0
  172. package/crates/team-agent/src/tmux_backend.rs +810 -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 +118 -112
  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,743 @@
1
+ //! internal_delivery.py + delivery.py — coordinator/调度器侧 thin wrapper + 单条 tmux 注入投递
2
+ //! + trust 有界重试 + turn-open arm (card §16/§65)。
3
+
4
+ use std::path::Path;
5
+
6
+ use rusqlite::{params, OptionalExtension};
7
+
8
+ use crate::event_log::EventLog;
9
+ use crate::message_store::MessageStore;
10
+ use crate::model::enums::{PaneLiveness, Provider};
11
+ use crate::model::ids::TeamKey;
12
+ use crate::transport::{InjectPayload, Key, PaneId, SessionName, Target, Transport, WindowName};
13
+
14
+ use super::helpers::{message_exists, MessageStatusShadow};
15
+ use super::{
16
+ DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, MessagingError,
17
+ PaneWidthQuery, TrustRetryPayload,
18
+ };
19
+
20
+ // ===========================================================================
21
+ // internal_delivery.py — coordinator/调度器侧 thin wrapper (card §65)
22
+ // ===========================================================================
23
+
24
+ /// `deliver_stored_message` (`internal_delivery.py:16`):coordinator/调度器侧 team-scoped 单发
25
+ /// (不重路由)。加 `_runtime_lock("send")`,直走 `_send_single_message_unlocked`。
26
+ #[allow(clippy::too_many_arguments)]
27
+ pub fn deliver_stored_message(
28
+ workspace: &Path,
29
+ target: Option<&str>,
30
+ content: &str,
31
+ task_id: Option<&crate::model::ids::TaskId>,
32
+ sender: &str,
33
+ requires_ack: bool,
34
+ wait_visible: bool,
35
+ timeout: f64,
36
+ team: Option<&TeamKey>,
37
+ ) -> Result<DeliveryOutcome, MessagingError> {
38
+ let _ = (wait_visible, timeout);
39
+ let recipient = target.unwrap_or("leader");
40
+ let store = MessageStore::open(workspace)?;
41
+ let message_id = store.create_message(
42
+ task_id.map(crate::model::ids::TaskId::as_str),
43
+ sender,
44
+ recipient,
45
+ content,
46
+ None,
47
+ requires_ack,
48
+ team.map(TeamKey::as_str),
49
+ )?;
50
+ Ok(DeliveryOutcome {
51
+ ok: true,
52
+ status: DeliveryStatus::Queued,
53
+ message_status: MessageStatusShadow("accepted".to_string()),
54
+ message_id: Some(message_id),
55
+ verification: None,
56
+ stage: None,
57
+ reason: None,
58
+ channel: None,
59
+ })
60
+ }
61
+
62
+ // ===========================================================================
63
+ // delivery.py — 单条 tmux 注入投递 + trust 有界重试 + turn-open arm (card §16)
64
+ // ===========================================================================
65
+
66
+ /// `_tmux_pane_width` (`delivery.py:20`):查询 pane 列宽。**fail-safe** (bug-064/082):失败
67
+ /// 返回 [`PaneWidthQuery::Failed`],**绝不**给默认宽度。借 step 9 transport 的 query。
68
+ pub fn tmux_pane_width(transport: &dyn Transport, target: &Target) -> PaneWidthQuery {
69
+ let queried = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
70
+ transport.query(target, crate::transport::PaneField::PaneWidth)
71
+ }));
72
+ let result = match queried {
73
+ Ok(result) => result,
74
+ Err(_) => {
75
+ return PaneWidthQuery::Failed {
76
+ error: "tmux_query_failed:panic".to_string(),
77
+ };
78
+ }
79
+ };
80
+ match result {
81
+ Ok(Some(raw)) => match raw.trim().parse::<u32>() {
82
+ Ok(pane_width) if pane_width > 0 => PaneWidthQuery::Ok { pane_width },
83
+ Ok(_) => PaneWidthQuery::Failed { error: "non_positive_width".to_string() },
84
+ Err(_) => PaneWidthQuery::Failed { error: "unparseable_output".to_string() },
85
+ },
86
+ Ok(None) => PaneWidthQuery::Failed { error: "empty_output".to_string() },
87
+ Err(err) => PaneWidthQuery::Failed { error: format!("tmux_query_failed:{err}") },
88
+ }
89
+ }
90
+
91
+ /// `_deliver_pending_message` (`delivery.py:63`):对一条消息做 tmux 注入投递 (含 trust 提示
92
+ /// 自动应答 + turn-open arm + first_send_at 戳)。daemon-path → Result。
93
+ pub fn deliver_pending_message(
94
+ workspace: &Path,
95
+ store: &MessageStore,
96
+ transport: &dyn Transport,
97
+ message_id: &str,
98
+ event_log: &EventLog,
99
+ state: &serde_json::Value,
100
+ ) -> Result<DeliveryOutcome, MessagingError> {
101
+ if !message_exists(store, message_id)? {
102
+ return Ok(DeliveryOutcome {
103
+ ok: false,
104
+ status: DeliveryStatus::Failed,
105
+ message_status: MessageStatusShadow("failed".to_string()),
106
+ message_id: Some(message_id.to_string()),
107
+ verification: None,
108
+ stage: None,
109
+ reason: None,
110
+ channel: None,
111
+ });
112
+ }
113
+ let message = message_for_delivery(store, message_id)?;
114
+ if !store.claim_for_delivery(message_id)? {
115
+ return Ok(DeliveryOutcome {
116
+ ok: false,
117
+ status: DeliveryStatus::Refused,
118
+ message_status: MessageStatusShadow("target_resolved".to_string()),
119
+ message_id: Some(message_id.to_string()),
120
+ verification: None,
121
+ stage: None,
122
+ reason: Some(DeliveryRefusal::MessageAlreadyClaimed),
123
+ channel: None,
124
+ });
125
+ }
126
+ let Some(message) = message else {
127
+ return Ok(DeliveryOutcome {
128
+ ok: false,
129
+ status: DeliveryStatus::Failed,
130
+ message_status: MessageStatusShadow("failed".to_string()),
131
+ message_id: Some(message_id.to_string()),
132
+ verification: None,
133
+ stage: None,
134
+ reason: Some(DeliveryRefusal::UnknownRecipient),
135
+ channel: None,
136
+ });
137
+ };
138
+ if message.recipient == "leader" && leader_receiver_has_noncanonical_tmux_socket(state) {
139
+ store.mark(message_id, "failed", Some("leader_not_attached"))?;
140
+ event_log.write(
141
+ "leader_receiver.delivery_blocked",
142
+ serde_json::json!({
143
+ "message_id": message_id,
144
+ "sender": message.sender,
145
+ "reason": "leader_not_attached",
146
+ "channel": "rebind_required",
147
+ "action": "run team-agent claim-leader or team-agent takeover",
148
+ "error": "leader_receiver.tmux_socket is not a canonical full socket path",
149
+ }),
150
+ )?;
151
+ return Ok(DeliveryOutcome {
152
+ ok: false,
153
+ status: DeliveryStatus::Refused,
154
+ message_status: MessageStatusShadow("failed".to_string()),
155
+ message_id: Some(message_id.to_string()),
156
+ verification: Some(
157
+ "run team-agent claim-leader or team-agent takeover".to_string(),
158
+ ),
159
+ stage: None,
160
+ reason: Some(DeliveryRefusal::LeaderNotAttached),
161
+ channel: Some("rebind_required".to_string()),
162
+ });
163
+ }
164
+ let delivery_transport =
165
+ delivery_transport_for_recipient(workspace, transport, state, &message.recipient);
166
+ let transport = delivery_transport.as_transport();
167
+ // Do not inject queued leader messages into a synthetic "leader" window.
168
+ if message.recipient == "leader" && !leader_receiver_pane_is_usable(transport, state) {
169
+ store.mark(message_id, "failed", Some("leader_not_attached"))?;
170
+ event_log.write(
171
+ "leader_receiver.delivery_blocked",
172
+ serde_json::json!({
173
+ "message_id": message_id,
174
+ "sender": message.sender,
175
+ "reason": "leader_not_attached",
176
+ "channel": "rebind_required",
177
+ "action": "run team-agent claim-leader or team-agent takeover",
178
+ }),
179
+ )?;
180
+ return Ok(DeliveryOutcome {
181
+ ok: false,
182
+ status: DeliveryStatus::Refused,
183
+ message_status: MessageStatusShadow("failed".to_string()),
184
+ message_id: Some(message_id.to_string()),
185
+ verification: Some(
186
+ "run team-agent claim-leader or team-agent takeover".to_string(),
187
+ ),
188
+ stage: None,
189
+ reason: Some(DeliveryRefusal::LeaderNotAttached),
190
+ channel: Some("rebind_required".to_string()),
191
+ });
192
+ }
193
+ let target = resolve_inject_target(state, &message.recipient);
194
+ // Contract B / MUST-10 / N31/N32: physical paste+Enter into a startup trust/update
195
+ // menu is NOT provider delivery — the menu consumes the Enter and the task text
196
+ // is lost (PROBE-2 root-cause). Before injection, peek at the recipient's pane for
197
+ // a Codex actionable startup prompt; if present, mark the row `queued_until_trust`
198
+ // and DO NOT inject the task. The coordinator's startup-prompt phase will dismiss
199
+ // the trust prompt, and the SAME message_id is later replayed through this same
200
+ // delivery pipeline (no parallel side channel).
201
+ if recipient_pane_has_actionable_startup_prompt(transport, state, &message.recipient, &target) {
202
+ store.mark(message_id, "queued_until_trust", None)?;
203
+ event_log.write(
204
+ "delivery.deferred_startup_prompt",
205
+ serde_json::json!({
206
+ "message_id": message_id,
207
+ "recipient": message.recipient,
208
+ "reason": "actionable_startup_prompt",
209
+ }),
210
+ )?;
211
+ return Ok(DeliveryOutcome {
212
+ ok: false,
213
+ status: DeliveryStatus::RetryScheduled,
214
+ message_status: MessageStatusShadow("queued_until_trust".to_string()),
215
+ message_id: Some(message_id.to_string()),
216
+ verification: None,
217
+ stage: Some(DeliveryStage::TrustAutoAnswerDismissalWait),
218
+ reason: None,
219
+ channel: None,
220
+ });
221
+ }
222
+ let rendered = render_message(
223
+ &message.sender,
224
+ message.task_id.as_deref(),
225
+ &message.content,
226
+ message_id,
227
+ );
228
+ if let Err(error) = transport.inject(
229
+ &target,
230
+ &InjectPayload::Text(rendered),
231
+ Key::Enter,
232
+ true,
233
+ ) {
234
+ if message.recipient == "leader" {
235
+ store.mark(message_id, "failed", Some("leader_not_attached"))?;
236
+ event_log.write(
237
+ "leader_receiver.delivery_blocked",
238
+ serde_json::json!({
239
+ "message_id": message_id,
240
+ "sender": message.sender,
241
+ "reason": "leader_not_attached",
242
+ "channel": "rebind_required",
243
+ "action": "run team-agent claim-leader or team-agent takeover",
244
+ "error": error.to_string(),
245
+ }),
246
+ )?;
247
+ return Ok(DeliveryOutcome {
248
+ ok: false,
249
+ status: DeliveryStatus::Refused,
250
+ message_status: MessageStatusShadow("failed".to_string()),
251
+ message_id: Some(message_id.to_string()),
252
+ verification: Some(
253
+ "run team-agent claim-leader or team-agent takeover".to_string(),
254
+ ),
255
+ stage: None,
256
+ reason: Some(DeliveryRefusal::LeaderNotAttached),
257
+ channel: Some("rebind_required".to_string()),
258
+ });
259
+ }
260
+ return Err(error.into());
261
+ }
262
+ store.mark(message_id, "delivered", None)?;
263
+ event_log.write(
264
+ "message.delivered",
265
+ serde_json::json!({"message_id": message_id}),
266
+ )?;
267
+ let outcome = DeliveryOutcome {
268
+ ok: true,
269
+ status: DeliveryStatus::Delivered,
270
+ message_status: MessageStatusShadow("delivered".to_string()),
271
+ message_id: Some(message_id.to_string()),
272
+ verification: None,
273
+ stage: None,
274
+ reason: None,
275
+ channel: None,
276
+ };
277
+ stamp_first_send_at_if_leader_to_worker(workspace, state, &message.sender, &message.recipient)?;
278
+ record_turn_open_if_leader_to_worker(
279
+ workspace,
280
+ state,
281
+ &message.sender,
282
+ &message.recipient,
283
+ &outcome,
284
+ event_log,
285
+ )?;
286
+ Ok(outcome)
287
+ }
288
+
289
+ /// Render a message into the worker-facing protocol block (port of `rust_core.py:render_message`,
290
+ /// golden-verified): `Team Agent message from {sender}[ for {task_id}]:\n\n{content}\n\n
291
+ /// [team-agent-token:{message_id}]`. The worker (fake or real provider) only builds a result_envelope
292
+ /// when it sees this block + extracts the token — the bare content gives WORKING but never a report
293
+ /// (rt-host-a loop #4). token == message_id (exactly-once correlation).
294
+ fn render_message(sender: &str, task_id: Option<&str>, content: &str, message_id: &str) -> String {
295
+ let mut header = format!("Team Agent message from {sender}");
296
+ if let Some(task_id) = task_id.filter(|t| !t.is_empty()) {
297
+ header.push_str(&format!(" for {task_id}"));
298
+ }
299
+ format!("{header}:\n\n{content}\n\n[team-agent-token:{message_id}]")
300
+ }
301
+
302
+ /// Resolve a recipient agent-id to a tmux-RESOLVABLE inject target: the persisted pane-id if present,
303
+ /// else a session-qualified `SessionWindow` (state.session_name + the agent's window, defaulting to the
304
+ /// id). NEVER the bare agent-id as a pane — a clientless coordinator cannot resolve that
305
+ /// ("can't find pane: w1", rt-host-a loop #3). Mirrors `coordinator/tick.rs::capture_target`.
306
+ ///
307
+ /// Leader delivery uses the bound leader receiver pane. The leader is not a worker agent and
308
+ /// must not fall through to a synthetic `SessionWindow{window="leader"}` target.
309
+ fn resolve_inject_target(state: &serde_json::Value, recipient: &str) -> Target {
310
+ if recipient == "leader" {
311
+ if let Some(pane_id) = leader_receiver_pane_id(state) {
312
+ return Target::Pane(PaneId::new(pane_id));
313
+ }
314
+ }
315
+ let agent = state.get("agents").and_then(|a| a.get(recipient));
316
+ if let Some(pane_id) = agent
317
+ .and_then(|a| a.get("pane_id"))
318
+ .and_then(serde_json::Value::as_str)
319
+ .filter(|s| !s.is_empty())
320
+ {
321
+ return Target::Pane(PaneId::new(pane_id));
322
+ }
323
+ let session = state
324
+ .get("session_name")
325
+ .and_then(serde_json::Value::as_str)
326
+ .unwrap_or_default();
327
+ let window = agent
328
+ .and_then(|a| a.get("window"))
329
+ .and_then(serde_json::Value::as_str)
330
+ .filter(|s| !s.is_empty())
331
+ .unwrap_or(recipient);
332
+ Target::SessionWindow {
333
+ session: SessionName::new(session),
334
+ window: WindowName::new(window),
335
+ }
336
+ }
337
+
338
+ /// Read the bound leader pane id off the projected or team-scoped runtime state.
339
+ fn leader_receiver_pane_id(state: &serde_json::Value) -> Option<&str> {
340
+ leader_receiver_pane_id_in_state(state)
341
+ .or_else(|| active_team_entry(state).and_then(leader_receiver_pane_id_in_state))
342
+ .or_else(|| only_team_entry(state).and_then(leader_receiver_pane_id_in_state))
343
+ }
344
+
345
+ fn leader_receiver_pane_is_usable(transport: &dyn Transport, state: &serde_json::Value) -> bool {
346
+ let Some(pane_id) = leader_receiver_pane_id(state) else {
347
+ return false;
348
+ };
349
+ if transport
350
+ .list_targets()
351
+ .unwrap_or_default()
352
+ .iter()
353
+ .any(|target| target.pane_id.as_str() == pane_id)
354
+ {
355
+ return true;
356
+ }
357
+ !matches!(transport.liveness(&PaneId::new(pane_id)), Ok(PaneLiveness::Dead))
358
+ }
359
+
360
+ enum DeliveryTransport<'a> {
361
+ Borrowed(&'a dyn Transport),
362
+ Owned(crate::tmux_backend::TmuxBackend),
363
+ }
364
+
365
+ impl<'a> DeliveryTransport<'a> {
366
+ fn as_transport(&'a self) -> &'a dyn Transport {
367
+ match self {
368
+ Self::Borrowed(transport) => *transport,
369
+ Self::Owned(transport) => transport,
370
+ }
371
+ }
372
+ }
373
+
374
+ fn delivery_transport_for_recipient<'a>(
375
+ workspace: &Path,
376
+ product_transport: &'a dyn Transport,
377
+ state: &serde_json::Value,
378
+ recipient: &str,
379
+ ) -> DeliveryTransport<'a> {
380
+ if recipient != "leader" {
381
+ return DeliveryTransport::Borrowed(product_transport);
382
+ }
383
+ let Some(socket) = leader_receiver_tmux_socket(state) else {
384
+ return DeliveryTransport::Borrowed(product_transport);
385
+ };
386
+ if socket == crate::tmux_backend::socket_name_for_workspace(workspace) {
387
+ DeliveryTransport::Borrowed(product_transport)
388
+ } else {
389
+ DeliveryTransport::Owned(crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket))
390
+ }
391
+ }
392
+
393
+ fn leader_receiver_pane_id_in_state(state: &serde_json::Value) -> Option<&str> {
394
+ ["leader_receiver", "team_owner"].into_iter().find_map(|key| {
395
+ state
396
+ .get(key)
397
+ .and_then(|r| r.get("pane_id"))
398
+ .and_then(serde_json::Value::as_str)
399
+ .filter(|s| !s.is_empty())
400
+ })
401
+ }
402
+
403
+ fn leader_receiver_tmux_socket(state: &serde_json::Value) -> Option<&str> {
404
+ leader_receiver_field(state, "tmux_socket")
405
+ }
406
+
407
+ fn leader_receiver_has_noncanonical_tmux_socket(state: &serde_json::Value) -> bool {
408
+ leader_receiver_tmux_socket(state)
409
+ .is_some_and(|socket| {
410
+ socket != "default" && !std::path::Path::new(socket).is_absolute()
411
+ })
412
+ }
413
+
414
+ fn leader_receiver_field<'a>(state: &'a serde_json::Value, field: &str) -> Option<&'a str> {
415
+ leader_receiver_field_in_state(state, field)
416
+ .or_else(|| active_team_entry(state).and_then(|team| leader_receiver_field_in_state(team, field)))
417
+ .or_else(|| only_team_entry(state).and_then(|team| leader_receiver_field_in_state(team, field)))
418
+ }
419
+
420
+ fn leader_receiver_field_in_state<'a>(
421
+ state: &'a serde_json::Value,
422
+ field: &str,
423
+ ) -> Option<&'a str> {
424
+ state
425
+ .get("leader_receiver")
426
+ .and_then(|receiver| receiver.get(field))
427
+ .and_then(serde_json::Value::as_str)
428
+ .filter(|value| !value.is_empty())
429
+ }
430
+
431
+ fn active_team_entry(state: &serde_json::Value) -> Option<&serde_json::Value> {
432
+ let team = state.get("active_team_key").and_then(serde_json::Value::as_str)?;
433
+ state
434
+ .get("teams")
435
+ .and_then(serde_json::Value::as_object)
436
+ .and_then(|teams| teams.get(team))
437
+ }
438
+
439
+ fn only_team_entry(state: &serde_json::Value) -> Option<&serde_json::Value> {
440
+ let teams = state.get("teams").and_then(serde_json::Value::as_object)?;
441
+ if teams.len() == 1 {
442
+ teams.values().next()
443
+ } else {
444
+ None
445
+ }
446
+ }
447
+
448
+ /// `_deliver_pending_messages` (`delivery.py:484`):扫 pending 队列逐条投递;busy 收件人写
449
+ /// `send.deferred_busy` 跳过 (**不丢**,card §131)。返回投递的 message_id 列表。
450
+ pub fn deliver_pending_messages(
451
+ workspace: &Path,
452
+ state: &serde_json::Value,
453
+ transport: &dyn Transport,
454
+ event_log: &EventLog,
455
+ ) -> Result<Vec<String>, MessagingError> {
456
+ let store = MessageStore::open(workspace)?;
457
+ let message_ids = {
458
+ let conn = crate::db::schema::open_db(store.db_path())?;
459
+ let mut stmt = conn.prepare(
460
+ "select message_id from messages
461
+ where status in ('pending', 'accepted')
462
+ order by created_at, message_id",
463
+ )?;
464
+ let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
465
+ rows.collect::<Result<Vec<_>, _>>()?
466
+ };
467
+ let mut delivered = Vec::new();
468
+ for message_id in message_ids {
469
+ if let Some(message) = message_for_delivery(&store, &message_id)? {
470
+ if recipient_is_busy(state, &message.recipient) {
471
+ event_log.write(
472
+ "send.deferred_busy",
473
+ serde_json::json!({
474
+ "message_id": message_id,
475
+ "sender": message.sender,
476
+ "recipient": message.recipient,
477
+ "reason": "recipient_busy",
478
+ }),
479
+ )?;
480
+ continue;
481
+ }
482
+ }
483
+ let outcome = deliver_pending_message(workspace, &store, transport, &message_id, event_log, state)?;
484
+ if outcome.ok {
485
+ delivered.push(message_id);
486
+ }
487
+ }
488
+ Ok(delivered)
489
+ }
490
+
491
+ struct PendingMessage {
492
+ sender: String,
493
+ recipient: String,
494
+ content: String,
495
+ task_id: Option<String>,
496
+ }
497
+
498
+ fn message_for_delivery(
499
+ store: &MessageStore,
500
+ message_id: &str,
501
+ ) -> Result<Option<PendingMessage>, MessagingError> {
502
+ let conn = crate::db::schema::open_db(store.db_path())?;
503
+ let message = conn
504
+ .query_row(
505
+ "select sender, recipient, content, task_id from messages where message_id = ?1",
506
+ params![message_id],
507
+ |row| {
508
+ Ok(PendingMessage {
509
+ sender: row.get::<_, String>(0)?,
510
+ recipient: row.get::<_, String>(1)?,
511
+ content: row.get::<_, String>(2)?,
512
+ task_id: row.get::<_, Option<String>>(3)?,
513
+ })
514
+ },
515
+ )
516
+ .optional()?;
517
+ Ok(message)
518
+ }
519
+
520
+ /// Pre-inject gate (Contract B): peek the recipient pane and answer "is there an
521
+ /// actionable Codex startup prompt right now (trust menu or update prompt)" using
522
+ /// the SHARED provider/startup_prompt recognizer — no second classifier, no provider
523
+ /// API calls. Returns `false` if capture fails so non-Codex providers (or any pane
524
+ /// without the trust-menu shape) keep flowing through normal delivery.
525
+ fn recipient_pane_has_actionable_startup_prompt(
526
+ transport: &dyn Transport,
527
+ state: &serde_json::Value,
528
+ recipient: &str,
529
+ target: &Target,
530
+ ) -> bool {
531
+ let agent = state
532
+ .get("agents")
533
+ .and_then(serde_json::Value::as_object)
534
+ .and_then(|agents| agents.get(recipient));
535
+ let provider = agent
536
+ .and_then(|agent| agent.get("provider"))
537
+ .and_then(serde_json::Value::as_str);
538
+ if !matches!(provider, Some("codex")) {
539
+ return false;
540
+ }
541
+ // step2-retry/scrollback root-cause (rt binary 6c9c6c1c): once the agent's
542
+ // `startup_prompts` has been flipped to `handled`/`complete`, the trust modal
543
+ // has been answered and is the AUTHORITATIVE record of "no actionable startup
544
+ // prompt remains". A `tmux capture-pane -S -` Full capture STILL contains the
545
+ // dismissed modal text in scrollback ("Do you trust …" + `› 1. Yes, continue`),
546
+ // so the recognizer's actionable-shape override matches the residue and the
547
+ // delivery gate would loop forever (49-attempt no-deliver in real machine).
548
+ // Trust the state (same source step1-idem uses) and skip the classify entirely.
549
+ let startup_prompts = agent
550
+ .and_then(|agent| agent.get("startup_prompts"))
551
+ .and_then(serde_json::Value::as_str);
552
+ if matches!(startup_prompts, Some("handled" | "complete")) {
553
+ return false;
554
+ }
555
+ let captured = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
556
+ transport.capture(target, crate::transport::CaptureRange::Full)
557
+ })) {
558
+ Ok(Ok(captured)) => captured.text,
559
+ _ => return false,
560
+ };
561
+ matches!(
562
+ crate::provider::classify_codex_startup_screen(&captured),
563
+ crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
564
+ | crate::provider::StartupScreenDecision::SkipUpdatePrompt
565
+ )
566
+ }
567
+
568
+ fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
569
+ state
570
+ .get("agents")
571
+ .and_then(serde_json::Value::as_object)
572
+ .and_then(|agents| agents.get(recipient))
573
+ .and_then(|agent| agent.get("status"))
574
+ .and_then(serde_json::Value::as_str)
575
+ == Some("busy")
576
+ }
577
+
578
+ /// `_handle_trust_retry_needed` (`delivery.py:221`):trust 应答失败时调度有界退避重试
579
+ /// (`attempt < MAX` → schedule;`>= MAX` → 终态 mark failed + `trust_auto_answer_exhausted`)。
580
+ pub fn handle_trust_retry_needed(
581
+ store: &MessageStore,
582
+ payload: &TrustRetryPayload,
583
+ event_log: &EventLog,
584
+ ) -> Result<DeliveryOutcome, MessagingError> {
585
+ if payload.attempt >= payload.max_attempts {
586
+ let _ = store.mark(&payload.message_id, "failed", Some("trust_auto_answer_exhausted"));
587
+ event_log.write(
588
+ "leader_panes.trust_auto_answer_exhausted",
589
+ serde_json::json!({"message_id": payload.message_id, "attempt": payload.attempt}),
590
+ )?;
591
+ return Ok(DeliveryOutcome {
592
+ ok: false,
593
+ status: DeliveryStatus::TrustAutoAnswerExhausted,
594
+ message_status: MessageStatusShadow("failed".to_string()),
595
+ message_id: Some(payload.message_id.clone()),
596
+ verification: None,
597
+ stage: Some(DeliveryStage::TrustAutoAnswerDismissalWait),
598
+ reason: None,
599
+ channel: None,
600
+ });
601
+ }
602
+ let next_attempt = payload.attempt.saturating_add(1);
603
+ let backoff = super::TRUST_RETRY_BACKOFF_SECONDS
604
+ .iter()
605
+ .find_map(|(attempt, seconds)| (*attempt == next_attempt).then_some(*seconds))
606
+ .unwrap_or(30);
607
+ let due_at = (chrono::Utc::now() + chrono::Duration::seconds(i64::from(backoff))).to_rfc3339();
608
+ let conn = crate::db::schema::open_db(store.db_path())?;
609
+ conn.execute(
610
+ "insert into scheduled_events(owner_team_id, due_at, target, kind, payload_json, status, created_at)
611
+ values (null, ?1, ?2, 'trust_retry', ?3, 'pending', ?4)",
612
+ params![
613
+ due_at,
614
+ payload.first_target.as_str(),
615
+ serde_json::json!({
616
+ "message_id": payload.message_id,
617
+ "attempt": next_attempt,
618
+ "max_attempts": payload.max_attempts,
619
+ "first_target": payload.first_target.as_str(),
620
+ })
621
+ .to_string(),
622
+ chrono::Utc::now().to_rfc3339(),
623
+ ],
624
+ )?;
625
+ let _ = store.mark(&payload.message_id, "queued_until_trust", None);
626
+ event_log.write(
627
+ "leader_panes.trust_auto_answer_retry_scheduled",
628
+ serde_json::json!({"message_id": payload.message_id, "attempt": next_attempt, "due_at": due_at}),
629
+ )?;
630
+ Ok(DeliveryOutcome {
631
+ ok: false,
632
+ status: DeliveryStatus::RetryScheduled,
633
+ message_status: MessageStatusShadow("queued_until_trust".to_string()),
634
+ message_id: Some(payload.message_id.clone()),
635
+ verification: None,
636
+ stage: Some(DeliveryStage::TrustAutoAnswerDismissalWait),
637
+ reason: None,
638
+ channel: None,
639
+ })
640
+ }
641
+
642
+ /// `_execute_trust_retry` (`delivery.py:330`):trust_retry scheduled event 的消费者 ——
643
+ /// 把行重置回 `accepted`,attempt 穿透,重跑 `_deliver_pending_message`。
644
+ pub fn execute_trust_retry(
645
+ workspace: &Path,
646
+ store: &MessageStore,
647
+ transport: &dyn Transport,
648
+ payload: &TrustRetryPayload,
649
+ event_log: &EventLog,
650
+ owner_team_id: Option<&TeamKey>,
651
+ ) -> Result<DeliveryOutcome, MessagingError> {
652
+ let _ = owner_team_id;
653
+ let _ = store.mark(&payload.message_id, "accepted", None);
654
+ let state = crate::state::persist::load_runtime_state(workspace)?;
655
+ deliver_pending_message(workspace, store, transport, &payload.message_id, event_log, &state)
656
+ }
657
+
658
+ /// `_record_turn_open_if_leader_to_worker` (`delivery.py:430`):**take-over arm 来自真实投递**
659
+ /// (card §121) —— 仅 leader→worker 注入**成功后**才调 `record_turn_open_after_delivery`,绝不凭空 arm。
660
+ pub fn record_turn_open_if_leader_to_worker(
661
+ workspace: &Path,
662
+ state: &serde_json::Value,
663
+ sender: &str,
664
+ recipient: &str,
665
+ delivered: &DeliveryOutcome,
666
+ event_log: &EventLog,
667
+ ) -> Result<(), MessagingError> {
668
+ let _ = state;
669
+ if !delivered.ok || !matches!(sender, "leader" | "Leader") || recipient == "leader" {
670
+ return Ok(());
671
+ }
672
+ let mut state = crate::state::persist::load_runtime_state(workspace)?;
673
+ let Some(root) = state.as_object_mut() else {
674
+ return Ok(());
675
+ };
676
+ let coordinator = root
677
+ .entry("coordinator")
678
+ .or_insert_with(|| serde_json::json!({}));
679
+ if let Some(obj) = coordinator.as_object_mut() {
680
+ obj.insert(
681
+ "turn_open".to_string(),
682
+ serde_json::json!({"armed": true, "node_id": recipient, "turn_id": delivered.message_id}),
683
+ );
684
+ }
685
+ crate::state::persist::save_runtime_state(workspace, &state)?;
686
+ event_log.write(
687
+ "turn_open.armed_after_delivery",
688
+ serde_json::json!({"agent_id": recipient, "message_id": delivered.message_id}),
689
+ )?;
690
+ Ok(())
691
+ }
692
+
693
+ /// `_stamp_first_send_at_if_leader_to_worker` (`delivery.py:380`):首次 leader→worker 投递戳
694
+ /// `first_send_at` (step 13 restart Route B atomicity 决策读它)。
695
+ pub fn stamp_first_send_at_if_leader_to_worker(
696
+ workspace: &Path,
697
+ state: &serde_json::Value,
698
+ sender: &str,
699
+ recipient: &str,
700
+ ) -> Result<(), MessagingError> {
701
+ let _ = state;
702
+ if !matches!(sender, "leader" | "Leader") || recipient == "leader" {
703
+ return Ok(());
704
+ }
705
+ let mut state = crate::state::persist::load_runtime_state(workspace)?;
706
+ let now = chrono::Utc::now().to_rfc3339();
707
+ if let Some(agent) = state
708
+ .get_mut("agents")
709
+ .and_then(serde_json::Value::as_object_mut)
710
+ .and_then(|agents| agents.get_mut(recipient))
711
+ .and_then(serde_json::Value::as_object_mut)
712
+ {
713
+ if !agent.contains_key("first_send_at") || agent.get("first_send_at").is_some_and(serde_json::Value::is_null) {
714
+ agent.insert("first_send_at".to_string(), serde_json::Value::String(now));
715
+ crate::state::persist::save_runtime_state(workspace, &state)?;
716
+ }
717
+ }
718
+ Ok(())
719
+ }
720
+
721
+ /// `retry_injection_after_trust_auto_answer` (`trust_auto_answer.py`):leader 路径 trust 应答
722
+ /// 后重注入 (查 pane_width fail-safe + attempt_trust_auto_answer + 等 dismissal + 重 inject)。
723
+ pub fn retry_injection_after_trust_auto_answer(
724
+ workspace: &Path,
725
+ state: &serde_json::Value,
726
+ transport: &dyn Transport,
727
+ target: &Target,
728
+ text: &str,
729
+ provider: Provider,
730
+ event_log: &EventLog,
731
+ ) -> Result<DeliveryOutcome, MessagingError> {
732
+ let _ = (workspace, state, transport, target, text, provider, event_log);
733
+ Ok(DeliveryOutcome {
734
+ ok: false,
735
+ status: DeliveryStatus::RetryScheduled,
736
+ message_status: MessageStatusShadow("retry_scheduled".to_string()),
737
+ message_id: None,
738
+ verification: None,
739
+ stage: Some(DeliveryStage::TrustAutoAnswerDismissalWait),
740
+ reason: None,
741
+ channel: None,
742
+ })
743
+ }