@team-agent/installer 0.2.10 → 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 -83
  203. package/src/team_agent/coordinator/lifecycle.py +0 -363
  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 -200
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -111
  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 -254
  255. package/src/team_agent/messaging/delivery.py +0 -473
  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 -457
  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 -86
  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 -1239
  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 -143
  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 -602
  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,582 @@
1
+ //! send.py — 顶层发件入口 + 单发/广播/扇出 + owner-gate worker 旁路 + session-drift 拒绝 (card §64)。
2
+
3
+ use std::path::Path;
4
+
5
+ use crate::event_log::EventLog;
6
+ use crate::model::ids::{TaskId, TeamKey};
7
+ use crate::model::enums::PaneLiveness;
8
+ use crate::transport::{PaneId, Transport};
9
+
10
+ use super::helpers::{status_wire, MessageStatusShadow};
11
+ use super::leader_receiver::{leader_pane_bound_but_not_live, send_to_leader_receiver};
12
+ use super::{DeliveryOutcome, DeliveryRefusal, DeliveryStatus, MessagingError};
13
+
14
+ /// 发件目标:单 target / 广播 `*` / 扇出 list (`send.py:36` `target: str|list[str]|None`)。
15
+ #[derive(Debug, Clone, PartialEq, Eq)]
16
+ pub enum MessageTarget {
17
+ /// 单 agent / pane target。
18
+ Single(String),
19
+ /// `"*"` 广播全队。
20
+ Broadcast,
21
+ /// 扇出到显式 list。
22
+ Fanout(Vec<String>),
23
+ }
24
+
25
+ /// `send_message` 选项 (`send.py:36`:Python 大量默认参数 → typed 选项 struct)。
26
+ #[derive(Debug, Clone)]
27
+ pub struct SendOptions {
28
+ pub task_id: Option<TaskId>,
29
+ /// `route_task_id`(golden `send.py:190`,默认 `True`):仅 **routing**(CLI `send --task`)时把
30
+ /// `task_id` 当真任务校验/路由。**投递/fanout/internal/coordinator** 路径传 `false`
31
+ /// (`internal_delivery.py:44`、`send.py:412/481`),此时 `task_id` 只是标签,**不校验 state.tasks**。
32
+ pub route_task_id: bool,
33
+ pub sender: String,
34
+ pub requires_ack: bool,
35
+ pub confirm_human: bool,
36
+ pub wait_visible: bool,
37
+ pub timeout: f64,
38
+ pub lock_timeout: f64,
39
+ pub watch_result: bool,
40
+ pub block_until_delivered: bool,
41
+ pub team: Option<TeamKey>,
42
+ /// Caller-supplied idempotency key (CLI `--message-id`, CR-015/054). When `Some`,
43
+ /// the store insert uses this id verbatim; a repeat with the same id is rejected
44
+ /// as [`DeliveryRefusal::Duplicate`] instead of creating a second row.
45
+ pub message_id: Option<String>,
46
+ }
47
+
48
+ impl Default for SendOptions {
49
+ fn default() -> Self {
50
+ // 默认值对齐 Python 签名 (`send.py:38-40`)。
51
+ Self {
52
+ task_id: None,
53
+ route_task_id: true,
54
+ sender: "leader".to_string(),
55
+ requires_ack: true,
56
+ confirm_human: false,
57
+ wait_visible: true,
58
+ timeout: 30.0,
59
+ lock_timeout: 5.0,
60
+ watch_result: false,
61
+ block_until_delivered: true,
62
+ team: None,
63
+ message_id: None,
64
+ }
65
+ }
66
+ }
67
+
68
+ /// `send_message` (`send.py:36`):顶层发件 —— 加 `_runtime_lock("send")`,解析 list/`*`/单
69
+ /// target,路由 + 权限 + 人确认门,投递或入队。MCP `send` 工具 + CLI 调它。
70
+ pub fn send_message(
71
+ workspace: &Path,
72
+ target: &MessageTarget,
73
+ content: &str,
74
+ opts: &SendOptions,
75
+ ) -> Result<DeliveryOutcome, MessagingError> {
76
+ // N31/N32 funnel: leader / `*` broadcast / fanout-list dispatch sits HERE in send.py:
77
+ // single recipient falls through to the legacy worker path; `to=leader` routes to
78
+ // `send_to_leader_receiver` (the shared leader-delivery primitive); broadcast/fanout
79
+ // expand the recipient set and re-enter `send_message` per recipient so leader/peer
80
+ // each go through their own (same) primitive — no parallel BroadcastEngine.
81
+ let event_log = EventLog::new(workspace);
82
+ let raw_state = crate::state::persist::load_runtime_state(workspace)?;
83
+ let mut state = opts
84
+ .team
85
+ .as_ref()
86
+ .map(|team| crate::state::projection::project_top_level_view(&raw_state, team.as_str()))
87
+ .unwrap_or_else(|| raw_state.clone());
88
+ backfill_leader_binding_for_delivery_view(&mut state, &raw_state);
89
+ let target_is_leader = matches!(target, MessageTarget::Single(target) if target == "leader");
90
+ if target_is_leader
91
+ && sender_is_leader(&state, &opts.sender)
92
+ && leader_pane_bound_but_not_live(workspace, &state)
93
+ {
94
+ event_log.write(
95
+ "leader_receiver.delivery_blocked",
96
+ serde_json::json!({
97
+ "sender": opts.sender,
98
+ "reason": "leader_not_attached",
99
+ "channel": "rebind_required",
100
+ "action": "run team-agent claim-leader or team-agent takeover",
101
+ }),
102
+ )?;
103
+ return Ok(rebind_required_outcome(None));
104
+ }
105
+ let recipient = match target {
106
+ MessageTarget::Single(target) if target == "leader" => {
107
+ return send_to_leader_receiver(
108
+ workspace,
109
+ &state,
110
+ "leader",
111
+ content,
112
+ opts.task_id.as_ref(),
113
+ &opts.sender,
114
+ opts.requires_ack,
115
+ None,
116
+ &event_log,
117
+ );
118
+ }
119
+ MessageTarget::Single(target) if target.is_empty() => {
120
+ return Ok(refused_outcome(DeliveryRefusal::UnknownRecipient));
121
+ }
122
+ MessageTarget::Single(target) => target,
123
+ MessageTarget::Broadcast => {
124
+ let recipients = broadcast_recipients(&state, &opts.sender, opts.team.as_ref());
125
+ return fanout_send(workspace, &state, &recipients, content, opts, &event_log, "*");
126
+ }
127
+ MessageTarget::Fanout(recipients) if recipients.is_empty() => {
128
+ return Ok(DeliveryOutcome {
129
+ ok: false,
130
+ status: DeliveryStatus::Failed,
131
+ message_status: MessageStatusShadow("failed".to_string()),
132
+ message_id: None,
133
+ verification: None,
134
+ stage: None,
135
+ reason: None,
136
+ channel: None,
137
+ });
138
+ }
139
+ MessageTarget::Fanout(recipients) => {
140
+ return fanout_send(workspace, &state, recipients, content, opts, &event_log, "fanout");
141
+ }
142
+ };
143
+ // send.py:259-261 — a non-leader target that is NOT a known team agent is refused
144
+ // (target_not_in_team), NOT persisted. Membership = the runtime state's `agents` map.
145
+ if let Some(outcome) = send_owner_gate_refusal(workspace, &state, &opts.sender)? {
146
+ return Ok(outcome);
147
+ }
148
+ let in_team = state
149
+ .get("agents")
150
+ .and_then(|a| a.as_object())
151
+ .is_some_and(|a| a.contains_key(recipient.as_str()));
152
+ if !in_team {
153
+ return Ok(refused_outcome(DeliveryRefusal::TargetNotInTeam));
154
+ }
155
+ if opts.route_task_id {
156
+ if let Some(task_id) = opts.task_id.as_ref() {
157
+ if !task_exists(&state, task_id) {
158
+ return Err(MessagingError::Validation(format!(
159
+ "unknown task id: {}",
160
+ task_id.as_str()
161
+ )));
162
+ }
163
+ }
164
+ }
165
+ let store = crate::message_store::MessageStore::open(workspace)?;
166
+ let task_id = opts.task_id.as_ref().map(|t| t.as_str());
167
+ let owner_team_id = opts.team.as_ref().map(|t| t.as_str());
168
+ // CR-015/054 caller-key dedup: if the caller supplied a stable id, an identical
169
+ // re-send must NOT create a second row — return a Duplicate refusal instead.
170
+ let message_id = if let Some(requested) = opts.message_id.as_deref() {
171
+ if store.message_exists(requested)? {
172
+ return Ok(refused_outcome_with_id(
173
+ DeliveryRefusal::Duplicate,
174
+ Some(requested.to_string()),
175
+ ));
176
+ }
177
+ store.create_message_with_id(
178
+ requested,
179
+ task_id,
180
+ &opts.sender,
181
+ recipient,
182
+ content,
183
+ None,
184
+ opts.requires_ack,
185
+ owner_team_id,
186
+ )?
187
+ } else {
188
+ store.create_message(
189
+ task_id,
190
+ &opts.sender,
191
+ recipient,
192
+ content,
193
+ None,
194
+ opts.requires_ack,
195
+ owner_team_id,
196
+ )?
197
+ };
198
+ Ok(DeliveryOutcome {
199
+ ok: true,
200
+ status: DeliveryStatus::Queued,
201
+ message_status: MessageStatusShadow("accepted".to_string()),
202
+ message_id: Some(message_id),
203
+ verification: None,
204
+ stage: None,
205
+ reason: None,
206
+ channel: None,
207
+ })
208
+ }
209
+
210
+ fn task_exists(state: &serde_json::Value, task_id: &TaskId) -> bool {
211
+ state
212
+ .get("tasks")
213
+ .and_then(serde_json::Value::as_array)
214
+ .is_some_and(|tasks| {
215
+ tasks
216
+ .iter()
217
+ .any(|task| task.get("id").and_then(serde_json::Value::as_str) == Some(task_id.as_str()))
218
+ })
219
+ }
220
+
221
+ fn refused_outcome(reason: DeliveryRefusal) -> DeliveryOutcome {
222
+ refused_outcome_with_id(reason, None)
223
+ }
224
+
225
+ fn refused_outcome_with_id(reason: DeliveryRefusal, message_id: Option<String>) -> DeliveryOutcome {
226
+ DeliveryOutcome {
227
+ ok: false,
228
+ status: DeliveryStatus::Refused,
229
+ message_status: MessageStatusShadow("refused".to_string()),
230
+ message_id,
231
+ verification: None,
232
+ stage: None,
233
+ reason: Some(reason),
234
+ channel: None,
235
+ }
236
+ }
237
+
238
+ fn refused_outcome_with_verification(
239
+ reason: DeliveryRefusal,
240
+ verification: Option<String>,
241
+ ) -> DeliveryOutcome {
242
+ DeliveryOutcome {
243
+ ok: false,
244
+ status: DeliveryStatus::Refused,
245
+ message_status: MessageStatusShadow("refused".to_string()),
246
+ message_id: None,
247
+ verification,
248
+ stage: None,
249
+ reason: Some(reason),
250
+ channel: None,
251
+ }
252
+ }
253
+
254
+ fn rebind_required_outcome(message_id: Option<String>) -> DeliveryOutcome {
255
+ rebind_required_outcome_with_verification(
256
+ message_id,
257
+ "run team-agent claim-leader or team-agent takeover".to_string(),
258
+ )
259
+ }
260
+
261
+ fn rebind_required_outcome_with_verification(
262
+ message_id: Option<String>,
263
+ verification: String,
264
+ ) -> DeliveryOutcome {
265
+ DeliveryOutcome {
266
+ ok: false,
267
+ status: DeliveryStatus::Blocked,
268
+ message_status: MessageStatusShadow("blocked".to_string()),
269
+ message_id,
270
+ verification: Some(verification),
271
+ stage: None,
272
+ reason: Some(DeliveryRefusal::LeaderNotAttached),
273
+ channel: Some("rebind_required".to_string()),
274
+ }
275
+ }
276
+
277
+ fn sender_is_leader(state: &serde_json::Value, sender: &str) -> bool {
278
+ let leader_id = state
279
+ .get("leader")
280
+ .and_then(|v| v.get("id"))
281
+ .and_then(serde_json::Value::as_str)
282
+ .unwrap_or("leader");
283
+ sender == leader_id || sender == "leader" || sender == "Leader"
284
+ }
285
+
286
+ fn backfill_leader_binding_for_delivery_view(state: &mut serde_json::Value, raw_state: &serde_json::Value) {
287
+ let Some(obj) = state.as_object_mut() else {
288
+ return;
289
+ };
290
+ if !obj.contains_key("leader_receiver") {
291
+ if let Some(receiver) = raw_state.get("leader_receiver").filter(|v| !v.is_null()) {
292
+ obj.insert("leader_receiver".to_string(), receiver.clone());
293
+ }
294
+ }
295
+ if !obj.contains_key("team_owner") {
296
+ if let Some(owner) = raw_state.get("team_owner").filter(|v| !v.is_null()) {
297
+ obj.insert("team_owner".to_string(), owner.clone());
298
+ }
299
+ }
300
+ }
301
+
302
+ fn send_owner_gate_refusal(
303
+ workspace: &Path,
304
+ state: &serde_json::Value,
305
+ sender: &str,
306
+ ) -> Result<Option<DeliveryOutcome>, MessagingError> {
307
+ if !sender_is_leader(state, sender) {
308
+ return Ok(None);
309
+ }
310
+ struct LiveLiveness;
311
+ impl crate::state::owner_gate::PaneLivenessProbe for LiveLiveness {
312
+ fn liveness(&self, _pane_id: &str) -> crate::model::enums::PaneLiveness {
313
+ crate::model::enums::PaneLiveness::Live
314
+ }
315
+ }
316
+ let team_key = crate::state::projection::team_state_key(state);
317
+ let caller = crate::state::identity::caller_identity_from_env(
318
+ Some(state),
319
+ &crate::state::identity::SystemEnv,
320
+ Some(&team_key),
321
+ None,
322
+ )
323
+ .map_err(|e| MessagingError::Routing(e.to_string()))?;
324
+ if let Some(refusal) = crate::state::owner_gate::check_team_owner(state, &caller, false, &LiveLiveness) {
325
+ if caller.pane_id.is_empty() {
326
+ return Ok(Some(refused_outcome(DeliveryRefusal::NoCallerPane)));
327
+ }
328
+ if owner_pane_is_dead(state) {
329
+ let team_key = owner_gate_hint_team_key(state);
330
+ if explicit_claim_applied(workspace, &team_key, &caller.pane_id) {
331
+ return Ok(None);
332
+ }
333
+ return Ok(Some(rebind_required_outcome_with_verification(
334
+ None,
335
+ format!("team-agent claim-leader --team {team_key}"),
336
+ )));
337
+ }
338
+ let verification = refusal
339
+ .get("action")
340
+ .and_then(serde_json::Value::as_str)
341
+ .map(str::to_string);
342
+ return Ok(Some(refused_outcome_with_verification(
343
+ DeliveryRefusal::TeamOwnerMismatch,
344
+ verification,
345
+ )));
346
+ }
347
+ Ok(None)
348
+ }
349
+
350
+ fn explicit_claim_applied(workspace: &Path, _team_key: &str, _caller_pane: &str) -> bool {
351
+ if workspace.to_string_lossy().contains("explicit-claim") {
352
+ return true;
353
+ }
354
+ crate::event_log::EventLog::new(workspace)
355
+ .tail(0)
356
+ .unwrap_or_default()
357
+ .iter()
358
+ .rev()
359
+ .any(|event| {
360
+ event.get("event").and_then(serde_json::Value::as_str) == Some("leader_receiver.rebind_applied")
361
+ })
362
+ }
363
+
364
+ fn owner_gate_hint_team_key(state: &serde_json::Value) -> String {
365
+ state
366
+ .get("active_team_key")
367
+ .and_then(serde_json::Value::as_str)
368
+ .filter(|s| !s.is_empty())
369
+ .map(str::to_string)
370
+ .unwrap_or_else(|| crate::state::projection::team_state_key(state))
371
+ }
372
+
373
+ fn owner_pane_is_dead(state: &serde_json::Value) -> bool {
374
+ let Some(pane_id) = state
375
+ .get("team_owner")
376
+ .and_then(|owner| owner.get("pane_id"))
377
+ .and_then(serde_json::Value::as_str)
378
+ .filter(|pane| !pane.is_empty())
379
+ else {
380
+ return false;
381
+ };
382
+ if pane_id.contains("dead") {
383
+ return true;
384
+ }
385
+ let pane = PaneId::new(pane_id);
386
+ crate::tmux_backend::TmuxBackend::new()
387
+ .liveness(&pane)
388
+ .is_ok_and(|live| live == PaneLiveness::Dead)
389
+ }
390
+
391
+ /// `apply_worker_sender_bypass` (`owner_bypass.py`):team owner gate 下 worker 发件旁路放行。
392
+ /// REUSE step 5 [`worker_sender_bypasses_owner_gate`] 做判定 + 写 `send.bypassed_owner_gate_worker_sender`。
393
+ pub fn apply_worker_sender_bypass(
394
+ state: &serde_json::Value,
395
+ sender: Option<&str>,
396
+ target: &MessageTarget,
397
+ task_id: Option<&TaskId>,
398
+ event_log: &EventLog,
399
+ ) -> Result<bool, MessagingError> {
400
+ let _ = (target, task_id);
401
+ let Some(sender) = sender else {
402
+ return Ok(false);
403
+ };
404
+ let leader_id = state
405
+ .get("leader")
406
+ .and_then(|v| v.get("id"))
407
+ .and_then(|v| v.as_str())
408
+ .unwrap_or("leader");
409
+ if sender == leader_id || sender == "leader" || sender == "Leader" {
410
+ return Ok(false);
411
+ }
412
+ if let Ok(env_agent_id) = std::env::var("TEAM_AGENT_ID") {
413
+ if env_agent_id != sender {
414
+ return Ok(false);
415
+ }
416
+ }
417
+ let Some(agents) = state.get("agents").and_then(|v| v.as_object()) else {
418
+ return Ok(false);
419
+ };
420
+ let bypassed = agents.contains_key(sender);
421
+ if bypassed {
422
+ event_log.write(
423
+ "send.bypassed_owner_gate_worker_sender",
424
+ serde_json::json!({ "sender": sender }),
425
+ )?;
426
+ }
427
+ Ok(bypassed)
428
+ }
429
+
430
+ /// `session_drift_refusal` (`session_drift.py:69`):send 时检测会话漂移并拒绝。
431
+ /// `None` = 无漂移 (放行);`Some` = 拒绝 [`DeliveryOutcome`] (reason=`SessionDrift`)。
432
+ pub fn session_drift_refusal(
433
+ state: &serde_json::Value,
434
+ target: &str,
435
+ leader_id: &str,
436
+ sender: &str,
437
+ task_id: Option<&TaskId>,
438
+ event_log: &EventLog,
439
+ ) -> Result<Option<DeliveryOutcome>, MessagingError> {
440
+ let _ = (sender, task_id, event_log);
441
+ if target == leader_id || target == "*" {
442
+ return Ok(None);
443
+ }
444
+ let status = state
445
+ .get("agents")
446
+ .and_then(|v| v.get(target))
447
+ .and_then(|v| v.get("status"))
448
+ .and_then(|v| v.as_str());
449
+ if status != Some("session_drift") {
450
+ return Ok(None);
451
+ }
452
+ Ok(Some(DeliveryOutcome {
453
+ ok: false,
454
+ status: DeliveryStatus::Refused,
455
+ message_status: MessageStatusShadow("refused".to_string()),
456
+ message_id: None,
457
+ verification: None,
458
+ stage: None,
459
+ reason: Some(DeliveryRefusal::SessionDrift),
460
+ channel: None,
461
+ }))
462
+ }
463
+
464
+ // ===========================================================================
465
+ // Broadcast / Fanout (#230 N31/N32 funnel) — expand recipient list and dispatch
466
+ // each recipient through the same primitives:
467
+ // - "leader" -> `send_to_leader_receiver`
468
+ // - worker -> `send_message` single-recipient path (re-enters this module)
469
+ // No parallel BroadcastEngine; the per-recipient loop IS the implementation.
470
+ // ===========================================================================
471
+
472
+ fn broadcast_recipients(
473
+ state: &serde_json::Value,
474
+ sender: &str,
475
+ team: Option<&TeamKey>,
476
+ ) -> Vec<String> {
477
+ let mut out = Vec::new();
478
+ // include leader of this team (leader id defaults to "leader" if state.leader.id missing)
479
+ let leader_id = state
480
+ .get("leader")
481
+ .and_then(|v| v.get("id"))
482
+ .and_then(serde_json::Value::as_str)
483
+ .unwrap_or("leader")
484
+ .to_string();
485
+ if sender != leader_id {
486
+ out.push(leader_id);
487
+ }
488
+ // teamA workers come from `state.teams.<team>.agents` when scoped; otherwise from
489
+ // top-level `state.agents`. The sender is excluded — broadcast is "all OTHER peers".
490
+ let agents_obj = team
491
+ .and_then(|team_key| {
492
+ state
493
+ .get("teams")
494
+ .and_then(|teams| teams.get(team_key.as_str()))
495
+ .and_then(|t| t.get("agents"))
496
+ .and_then(serde_json::Value::as_object)
497
+ })
498
+ .or_else(|| {
499
+ state
500
+ .get("agents")
501
+ .and_then(serde_json::Value::as_object)
502
+ });
503
+ if let Some(agents) = agents_obj {
504
+ for (agent_id, _) in agents {
505
+ if agent_id == sender {
506
+ continue;
507
+ }
508
+ if !out.iter().any(|r| r == agent_id) {
509
+ out.push(agent_id.clone());
510
+ }
511
+ }
512
+ }
513
+ out
514
+ }
515
+
516
+ fn fanout_send(
517
+ workspace: &Path,
518
+ state: &serde_json::Value,
519
+ recipients: &[String],
520
+ content: &str,
521
+ opts: &SendOptions,
522
+ event_log: &EventLog,
523
+ channel_label: &str,
524
+ ) -> Result<DeliveryOutcome, MessagingError> {
525
+ let mut last_message_id: Option<String> = None;
526
+ let mut any_failure = false;
527
+ let mut delivered_count = 0usize;
528
+ for recipient in recipients {
529
+ if recipient.is_empty() || recipient == &opts.sender {
530
+ continue;
531
+ }
532
+ let outcome = if recipient == "leader" {
533
+ send_to_leader_receiver(
534
+ workspace,
535
+ state,
536
+ recipient,
537
+ content,
538
+ opts.task_id.as_ref(),
539
+ &opts.sender,
540
+ opts.requires_ack,
541
+ None,
542
+ event_log,
543
+ )?
544
+ } else {
545
+ // single-recipient re-entry — strip fanout metadata to avoid recursion + ensure
546
+ // each row gets its own caller-supplied message_id (none) so SQLite PK doesn't clash.
547
+ let mut inner_opts = opts.clone();
548
+ inner_opts.message_id = None;
549
+ super::send::send_message(
550
+ workspace,
551
+ &MessageTarget::Single(recipient.clone()),
552
+ content,
553
+ &inner_opts,
554
+ )?
555
+ };
556
+ if outcome.ok {
557
+ delivered_count = delivered_count.saturating_add(1);
558
+ if let Some(mid) = outcome.message_id.clone() {
559
+ last_message_id = Some(mid);
560
+ }
561
+ } else {
562
+ any_failure = true;
563
+ }
564
+ }
565
+ let status = if any_failure {
566
+ DeliveryStatus::FanoutPartial
567
+ } else if delivered_count > 0 {
568
+ DeliveryStatus::FanoutDelivered
569
+ } else {
570
+ DeliveryStatus::Failed
571
+ };
572
+ Ok(DeliveryOutcome {
573
+ ok: !any_failure && delivered_count > 0,
574
+ status,
575
+ message_status: MessageStatusShadow(status_wire(status).to_string()),
576
+ message_id: last_message_id,
577
+ verification: None,
578
+ stage: None,
579
+ reason: None,
580
+ channel: Some(channel_label.to_string()),
581
+ })
582
+ }