@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,1099 @@
1
+ //! leader::rediscover — attach-leader readopt and leader receiver rediscovery.
2
+
3
+ use std::collections::BTreeMap;
4
+ use std::path::{Path, PathBuf};
5
+
6
+ use serde_json::{json, Value};
7
+
8
+ use crate::model::ids::{LeaderSessionUuid, OwnerEpoch};
9
+ use crate::provider::Provider;
10
+ use crate::transport::{PaneId, PaneInfo, SessionName, Transport, WindowName};
11
+
12
+ use super::helpers::{get_path_str, now_ts, parse_provider, prefix, resolve_workspace_for_hash, sha1_hex_prefix};
13
+ use super::{
14
+ ClaimedVia, Discovery, LeaderError, LeaderEvent, LeaderReceiver, LeaseSource, ReceiverMode,
15
+ ReceiverStatus, TeamOwner,
16
+ };
17
+
18
+ /// `_try_readopt_leader_pane`: attach-leader can re-adopt a live leader pane when
19
+ /// the strict uuid validation failed but the pane is still a usable leader in this
20
+ /// workspace. Returns the same `(receiver, validation)` shape that attach wiring
21
+ /// needs; `None` means caller must continue to the normal refusal/takeover path.
22
+ #[allow(clippy::too_many_arguments)]
23
+ pub fn try_readopt_leader_pane(
24
+ workspace: &Path,
25
+ state: &mut Value,
26
+ receiver: &mut LeaderReceiver,
27
+ pane_info: &Value,
28
+ targets: &Value,
29
+ owner_record: Option<&TeamOwner>,
30
+ receiver_provider: Provider,
31
+ source: LeaseSource,
32
+ event_log: &crate::event_log::EventLog,
33
+ ) -> Result<Option<(LeaderReceiver, Value)>, LeaderError> {
34
+ let Some(candidate) = target_from_value(pane_info) else {
35
+ event_log.write(
36
+ LeaderEvent::ReceiverRebindRequired.name(),
37
+ json!({"reason": "leader_pane_missing"}),
38
+ )?;
39
+ return Ok(None);
40
+ };
41
+ if !readopt_candidate_is_usable_leader(workspace, &candidate) {
42
+ event_log.write(
43
+ LeaderEvent::ReceiverRebindRequired.name(),
44
+ json!({"reason": "leader_pane_unusable"}),
45
+ )?;
46
+ return Ok(None);
47
+ }
48
+ if different_live_owner_uuid_mismatch(owner_record, &candidate, targets) {
49
+ return Ok(None);
50
+ }
51
+ let owner_epoch = next_owner_epoch(state, receiver, owner_record);
52
+ let uuid = candidate
53
+ .leader_session_uuid
54
+ .clone()
55
+ .or_else(|| owner_record.and_then(|owner| owner.leader_session_uuid.clone()))
56
+ .or_else(|| receiver.leader_session_uuid.clone());
57
+ let provider = candidate.provider.unwrap_or(receiver_provider);
58
+ let rebound = receiver_from_candidate(&candidate, receiver, provider, uuid.clone(), owner_epoch, Discovery::AttachReadopt);
59
+ let owner = owner_from_candidate(&candidate, provider, uuid, owner_epoch, ClaimedVia::AttachLeader);
60
+ let mut owner_identity = OwnerIdentity::from_state(workspace, state)?;
61
+ if let Some(record) = owner_record {
62
+ owner_identity.pane_id = Some(record.pane_id.as_str().to_string());
63
+ owner_identity.leader_session_uuid = record.leader_session_uuid.clone();
64
+ owner_identity.machine_fingerprint = record.machine_fingerprint.clone();
65
+ owner_identity.provider = Some(record.provider);
66
+ }
67
+ let old_pane_id = old_pane_id(receiver, owner_record);
68
+ let uuid_prefix = uuid_prefix(owner.leader_session_uuid.as_ref());
69
+ write_readopt_state(workspace, state, &rebound, &owner)?;
70
+ *receiver = rebound.clone();
71
+ event_log.write(
72
+ LeaderEvent::OwnerAdoptedOnRestart.name(),
73
+ json!({
74
+ "reason": "attach_readopt",
75
+ "old_pane_id": old_pane_id.as_deref(),
76
+ "new_pane_id": rebound.pane_id.as_str(),
77
+ "owner_epoch": owner_epoch.0,
78
+ "uuid_prefix": uuid_prefix.as_str(),
79
+ "team_id": owner_identity.team_id.as_str(),
80
+ }),
81
+ )?;
82
+ event_log.write(
83
+ LeaderEvent::ReceiverRebindApplied.name(),
84
+ json!({
85
+ "old_pane_id": old_pane_id.as_deref(),
86
+ "new_pane_id": rebound.pane_id.as_str(),
87
+ "reason": "attach_readopt",
88
+ "owner_epoch": owner_epoch.0,
89
+ "uuid_prefix": uuid_prefix.as_str(),
90
+ "team_id": owner_identity.team_id.as_str(),
91
+ }),
92
+ )?;
93
+ event_log.write(
94
+ LeaderEvent::ReceiverAttached.name(),
95
+ json!({
96
+ "target": rebound.pane_id.as_str(),
97
+ "session_name": rebound.session_name.as_ref().map(SessionName::as_str),
98
+ "provider": provider_wire(provider),
99
+ "discovery": "attach_readopt",
100
+ "source": serde_json::to_value(source)?,
101
+ "owner_epoch": owner_epoch.0,
102
+ "uuid_prefix": uuid_prefix.as_str(),
103
+ }),
104
+ )?;
105
+ Ok(Some((
106
+ rebound.clone(),
107
+ json!({
108
+ "ok": true,
109
+ "pane": pane_info,
110
+ "readopted": true,
111
+ "warning": Value::Null,
112
+ }),
113
+ )))
114
+ }
115
+
116
+ /// `_rediscover_leader_receiver`: scan live targets and repair the receiver when
117
+ /// exactly one usable pane matches the recorded owner identity.
118
+ pub fn rediscover_leader_receiver(
119
+ workspace: &Path,
120
+ state: &mut Value,
121
+ transport: &dyn Transport,
122
+ event_log: &crate::event_log::EventLog,
123
+ ) -> Result<Value, LeaderError> {
124
+ let identity = OwnerIdentity::from_state(workspace, state)?;
125
+ rediscover_leader_receiver_with_identity(workspace, state, transport, event_log, &identity, None)
126
+ }
127
+
128
+ pub fn rediscover_leader_receiver_with_owner_identity(
129
+ workspace: &Path,
130
+ state: &mut Value,
131
+ transport: &dyn Transport,
132
+ event_log: &crate::event_log::EventLog,
133
+ owner_identity: &Value,
134
+ invalidation_reason: Option<&str>,
135
+ ) -> Result<Value, LeaderError> {
136
+ let identity = OwnerIdentity::from_raw(owner_identity, true);
137
+ rediscover_leader_receiver_with_identity(
138
+ workspace,
139
+ state,
140
+ transport,
141
+ event_log,
142
+ &identity,
143
+ invalidation_reason,
144
+ )
145
+ }
146
+
147
+ fn rediscover_leader_receiver_with_identity(
148
+ workspace: &Path,
149
+ state: &mut Value,
150
+ transport: &dyn Transport,
151
+ event_log: &crate::event_log::EventLog,
152
+ identity: &OwnerIdentity,
153
+ invalidation_reason: Option<&str>,
154
+ ) -> Result<Value, LeaderError> {
155
+ let targets = match transport.list_targets() {
156
+ Ok(targets) => targets,
157
+ Err(err) => {
158
+ let error = err.to_string();
159
+ event_log.write(
160
+ "leader_receiver.rediscover_failed",
161
+ json!({
162
+ "provider": identity.provider.map(provider_wire),
163
+ "error": error.as_str(),
164
+ }),
165
+ )?;
166
+ emit_failed_rebind_required(event_log, state, identity, invalidation_reason, error.as_str())?;
167
+ return Ok(json!({
168
+ "status": "failed",
169
+ "error": error,
170
+ }));
171
+ }
172
+ };
173
+ rediscover_leader_receiver_from_targets_with_identity(
174
+ workspace,
175
+ state,
176
+ &targets,
177
+ event_log,
178
+ identity,
179
+ invalidation_reason,
180
+ )
181
+ }
182
+
183
+ pub fn rediscover_leader_receiver_from_targets(
184
+ workspace: &Path,
185
+ state: &mut Value,
186
+ targets: &[PaneInfo],
187
+ event_log: &crate::event_log::EventLog,
188
+ ) -> Result<Value, LeaderError> {
189
+ let identity = OwnerIdentity::from_state(workspace, state)?;
190
+ rediscover_leader_receiver_from_targets_with_identity(
191
+ workspace,
192
+ state,
193
+ targets,
194
+ event_log,
195
+ &identity,
196
+ None,
197
+ )
198
+ }
199
+
200
+ pub fn rediscover_leader_receiver_from_targets_with_owner_identity(
201
+ workspace: &Path,
202
+ state: &mut Value,
203
+ targets: &[PaneInfo],
204
+ event_log: &crate::event_log::EventLog,
205
+ owner_identity: &Value,
206
+ invalidation_reason: Option<&str>,
207
+ ) -> Result<Value, LeaderError> {
208
+ let identity = OwnerIdentity::from_raw(owner_identity, true);
209
+ rediscover_leader_receiver_from_targets_with_identity(
210
+ workspace,
211
+ state,
212
+ targets,
213
+ event_log,
214
+ &identity,
215
+ invalidation_reason,
216
+ )
217
+ }
218
+
219
+ fn rediscover_leader_receiver_from_targets_with_identity(
220
+ workspace: &Path,
221
+ state: &mut Value,
222
+ targets: &[PaneInfo],
223
+ event_log: &crate::event_log::EventLog,
224
+ identity: &OwnerIdentity,
225
+ invalidation_reason: Option<&str>,
226
+ ) -> Result<Value, LeaderError> {
227
+ let mut command_candidates: Vec<LeaderTarget> = targets
228
+ .iter()
229
+ .filter_map(LeaderTarget::from_pane_info)
230
+ .filter(rediscover_candidate_is_usable_leader)
231
+ .collect();
232
+ command_candidates.sort_by(|a, b| a.pane_id.as_str().cmp(b.pane_id.as_str()));
233
+ let has_owner_identity = identity.has_match_identity();
234
+ let mut candidates: Vec<LeaderTarget> = if has_owner_identity {
235
+ command_candidates
236
+ .iter()
237
+ .filter(|target| target_matches_owner_identity(target, identity))
238
+ .cloned()
239
+ .collect()
240
+ } else {
241
+ command_candidates.clone()
242
+ };
243
+ if candidates.len() > 1 {
244
+ candidates.sort_by(|a, b| a.pane_id.as_str().cmp(b.pane_id.as_str()));
245
+ let panes = candidate_pane_ids(&candidates);
246
+ let incident_id = ambiguous_incident_id(identity, &panes);
247
+ let deduped = ambiguous_candidates_already_broadcast(event_log, incident_id.as_str())?;
248
+ event_log.write(
249
+ "leader_receiver.rediscover_ambiguous",
250
+ json!({
251
+ "provider": event_provider(identity),
252
+ "old_target": old_target_from_state(state).or_else(|| identity.pane_id.clone()),
253
+ "candidates": panes,
254
+ "owner_identity": owner_identity_value(identity),
255
+ "incident_id": incident_id.as_str(),
256
+ "deduped": deduped,
257
+ }),
258
+ )?;
259
+ if !deduped {
260
+ emit_ambiguous_candidates(
261
+ event_log,
262
+ state,
263
+ identity,
264
+ &candidates,
265
+ incident_id.as_str(),
266
+ identity.from_caller,
267
+ )?;
268
+ }
269
+ if has_owner_identity {
270
+ emit_rebind_required(event_log, "ambiguous", state, identity, invalidation_reason, "confirm rediscover leader receiver")?;
271
+ return Ok(json!({
272
+ "status": "ambiguous",
273
+ "owner_candidates": candidate_values(&candidates, identity.from_caller),
274
+ "owner_identity": owner_identity_value(identity),
275
+ "incident_id": incident_id,
276
+ "deduped": deduped,
277
+ }));
278
+ }
279
+ emit_no_owner_rebind_required(event_log, "ambiguous", state, identity, None)?;
280
+ return Ok(json!({
281
+ "status": "ambiguous",
282
+ "candidates": candidate_values(&candidates, true),
283
+ "incident_id": incident_id,
284
+ "deduped": deduped,
285
+ }));
286
+ }
287
+ let Some(candidate) = candidates.pop() else {
288
+ if has_owner_identity {
289
+ event_log.write(
290
+ "leader_receiver.rediscover_missing",
291
+ json!({
292
+ "provider": event_provider(identity),
293
+ "old_target": old_target_from_state(state).or_else(|| identity.pane_id.clone()),
294
+ "owner_identity": owner_identity_value(identity),
295
+ "candidate_count": command_candidates.len(),
296
+ }),
297
+ )?;
298
+ emit_owner_missing_rebind_required(event_log, state, identity, invalidation_reason)?;
299
+ } else {
300
+ event_log.write(
301
+ "leader_receiver.rediscover_missing",
302
+ json!({
303
+ "provider": event_provider(identity),
304
+ "old_target": old_target_from_state(state),
305
+ }),
306
+ )?;
307
+ emit_no_owner_rebind_required(event_log, "missing", state, identity, None)?;
308
+ }
309
+ return Ok(json!({
310
+ "status": "missing",
311
+ "owner_identity": owner_identity_value(identity),
312
+ }));
313
+ };
314
+ let prior = state_receiver(state);
315
+ let epoch = prior
316
+ .as_ref()
317
+ .and_then(|receiver| receiver.owner_epoch)
318
+ .or_else(|| state_owner_epoch(state))
319
+ .unwrap_or(OwnerEpoch::FIRST);
320
+ let provider = candidate
321
+ .provider
322
+ .or_else(|| prior.as_ref().map(|receiver| receiver.provider))
323
+ .unwrap_or(Provider::Codex);
324
+ let uuid = candidate
325
+ .leader_session_uuid
326
+ .clone()
327
+ .or(identity.leader_session_uuid.clone());
328
+ let old_pane_id = prior
329
+ .as_ref()
330
+ .map(|receiver| receiver.pane_id.as_str().to_string())
331
+ .or_else(|| identity.pane_id.clone());
332
+ let uuid_prefix = uuid_prefix(uuid.as_ref());
333
+ let discovery = if identity.from_caller {
334
+ Discovery::StaleRediscoveryOwnerIdentity
335
+ } else {
336
+ Discovery::StaleRediscoveryUniqueCandidate
337
+ };
338
+ let receiver = receiver_from_candidate(
339
+ &candidate,
340
+ prior.as_ref().unwrap_or(&empty_prior(provider, epoch)),
341
+ provider,
342
+ uuid,
343
+ epoch,
344
+ discovery,
345
+ );
346
+ write_receiver_state(workspace, state, &receiver)?;
347
+ event_log.write(
348
+ LeaderEvent::ReceiverRebindApplied.name(),
349
+ json!({
350
+ "old_pane_id": old_pane_id.as_deref(),
351
+ "new_pane_id": receiver.pane_id.as_str(),
352
+ "reason": invalidation_reason,
353
+ "owner_identity": owner_identity_value(identity),
354
+ "uuid_prefix": uuid_prefix.as_str(),
355
+ }),
356
+ )?;
357
+ event_log.write(
358
+ "leader_receiver.rediscovered",
359
+ json!({
360
+ "provider": provider_wire(provider),
361
+ "old_target": old_pane_id.as_deref(),
362
+ "new_target": receiver.pane_id.as_str(),
363
+ "candidate_count": 1,
364
+ "owner_identity": owner_identity_value(identity),
365
+ }),
366
+ )?;
367
+ Ok(json!({
368
+ "status": "updated",
369
+ "receiver": receiver,
370
+ "owner_identity": owner_identity_value(identity),
371
+ }))
372
+ }
373
+
374
+ #[derive(Clone)]
375
+ struct LeaderTarget {
376
+ raw: Value,
377
+ pane_id: PaneId,
378
+ session: Option<SessionName>,
379
+ window_index: Option<String>,
380
+ window_name: Option<WindowName>,
381
+ pane_index: Option<String>,
382
+ tty: Option<String>,
383
+ current_command: Option<String>,
384
+ current_path: Option<PathBuf>,
385
+ active: bool,
386
+ leader_env: BTreeMap<String, String>,
387
+ provider: Option<Provider>,
388
+ leader_session_uuid: Option<LeaderSessionUuid>,
389
+ fingerprint: Option<String>,
390
+ }
391
+
392
+ impl LeaderTarget {
393
+ fn from_pane_info(info: &PaneInfo) -> Option<Self> {
394
+ if !info.active {
395
+ return None;
396
+ }
397
+ let provider = leader_command_provider(info.current_command.as_deref().unwrap_or(""));
398
+ let leader_session_uuid = info
399
+ .leader_env
400
+ .get("TEAM_AGENT_LEADER_SESSION_UUID")
401
+ .filter(|raw| !raw.is_empty())
402
+ .and_then(|raw| serde_json::from_value(Value::String(raw.clone())).ok());
403
+ Some(Self {
404
+ raw: pane_info_value(info, provider, leader_session_uuid.as_ref()),
405
+ pane_id: info.pane_id.clone(),
406
+ session: Some(info.session.clone()),
407
+ window_index: info.window_index.map(|value| value.to_string()),
408
+ window_name: info.window_name.clone(),
409
+ pane_index: info.pane_index.map(|value| value.to_string()),
410
+ tty: info.tty.clone(),
411
+ current_command: info.current_command.clone(),
412
+ current_path: info.current_path.clone(),
413
+ active: info.active,
414
+ leader_env: info.leader_env.clone(),
415
+ provider,
416
+ leader_session_uuid,
417
+ fingerprint: info
418
+ .leader_env
419
+ .get("TEAM_AGENT_MACHINE_FINGERPRINT")
420
+ .filter(|raw| !raw.is_empty())
421
+ .cloned(),
422
+ })
423
+ }
424
+ }
425
+
426
+ struct OwnerIdentity {
427
+ raw: Value,
428
+ from_caller: bool,
429
+ pane_id: Option<String>,
430
+ leader_session_uuid: Option<LeaderSessionUuid>,
431
+ machine_fingerprint: String,
432
+ provider: Option<Provider>,
433
+ team_id: String,
434
+ }
435
+
436
+ impl OwnerIdentity {
437
+ fn from_state(workspace: &Path, state: &Value) -> Result<Self, LeaderError> {
438
+ let identity = super::owner_bind::leader_identity_context(workspace, None, Some(state))?;
439
+ let pane_id = get_path_str(state, &["team_owner", "pane_id"])
440
+ .or_else(|| get_path_str(state, &["leader_receiver", "pane_id"]));
441
+ let leader_session_uuid = get_path_str(state, &["team_owner", "leader_session_uuid"])
442
+ .or_else(|| get_path_str(state, &["leader_receiver", "leader_session_uuid"]))
443
+ .and_then(|raw| serde_json::from_value(Value::String(raw)).ok());
444
+ let provider = get_path_str(state, &["team_owner", "provider"])
445
+ .or_else(|| get_path_str(state, &["leader_receiver", "provider"]))
446
+ .and_then(|raw| parse_provider(&raw));
447
+ let machine_fingerprint = identity.machine_fingerprint;
448
+ let team_id = identity.team_id.as_str().to_string();
449
+ Ok(Self {
450
+ raw: json!({
451
+ "pane_id": pane_id.as_deref(),
452
+ "leader_session_uuid": leader_session_uuid.as_ref().map(LeaderSessionUuid::as_str),
453
+ "machine_fingerprint": machine_fingerprint.as_str(),
454
+ "provider": provider.map(provider_wire),
455
+ "team_id": team_id.as_str(),
456
+ }),
457
+ from_caller: false,
458
+ pane_id,
459
+ leader_session_uuid,
460
+ machine_fingerprint,
461
+ provider,
462
+ team_id,
463
+ })
464
+ }
465
+
466
+ fn from_raw(raw: &Value, from_caller: bool) -> Self {
467
+ let pane_id = raw
468
+ .get("pane_id")
469
+ .or_else(|| raw.get("leader_pane_id"))
470
+ .and_then(Value::as_str)
471
+ .filter(|s| !s.is_empty())
472
+ .map(str::to_string);
473
+ let leader_session_uuid = raw
474
+ .get("leader_session_uuid")
475
+ .and_then(Value::as_str)
476
+ .filter(|s| !s.is_empty())
477
+ .and_then(|value| serde_json::from_value(Value::String(value.to_string())).ok());
478
+ let machine_fingerprint = raw
479
+ .get("machine_fingerprint")
480
+ .and_then(Value::as_str)
481
+ .unwrap_or("")
482
+ .to_string();
483
+ let provider = raw
484
+ .get("provider")
485
+ .and_then(Value::as_str)
486
+ .and_then(parse_provider);
487
+ let team_id = raw
488
+ .get("team_id")
489
+ .and_then(Value::as_str)
490
+ .unwrap_or("")
491
+ .to_string();
492
+ Self {
493
+ raw: raw.clone(),
494
+ from_caller,
495
+ pane_id,
496
+ leader_session_uuid,
497
+ machine_fingerprint,
498
+ provider,
499
+ team_id,
500
+ }
501
+ }
502
+
503
+ fn has_match_identity(&self) -> bool {
504
+ self.pane_id.is_some() || self.leader_session_uuid.is_some() || !self.machine_fingerprint.is_empty()
505
+ }
506
+ }
507
+
508
+ fn target_from_value(value: &Value) -> Option<LeaderTarget> {
509
+ let pane_id = get_str(value, "pane_id").or_else(|| get_str(value, "pane"))?;
510
+ let current_command = get_str(value, "pane_current_command").or_else(|| get_str(value, "current_command"));
511
+ let provider = current_command.as_deref().and_then(leader_command_provider);
512
+ let leader_env = map_env(value.get("leader_env").or_else(|| value.get("env")));
513
+ let leader_session_uuid = get_str(value, "leader_session_uuid")
514
+ .or_else(|| leader_env.get("TEAM_AGENT_LEADER_SESSION_UUID").cloned())
515
+ .and_then(|raw| serde_json::from_value(Value::String(raw)).ok());
516
+ Some(LeaderTarget {
517
+ raw: value.clone(),
518
+ pane_id: PaneId::new(pane_id),
519
+ session: get_str(value, "session_name")
520
+ .or_else(|| get_str(value, "session"))
521
+ .map(SessionName::new),
522
+ window_index: get_str(value, "window_index"),
523
+ window_name: get_str(value, "window_name").map(WindowName::new),
524
+ pane_index: get_str(value, "pane_index"),
525
+ tty: get_str(value, "pane_tty").or_else(|| get_str(value, "tty")),
526
+ current_command,
527
+ current_path: get_str(value, "pane_current_path")
528
+ .or_else(|| get_str(value, "current_path"))
529
+ .map(PathBuf::from),
530
+ active: value.get("active").and_then(Value::as_bool).unwrap_or(true),
531
+ leader_env,
532
+ provider,
533
+ leader_session_uuid,
534
+ fingerprint: get_str(value, "fingerprint").or_else(|| get_str(value, "machine_fingerprint")),
535
+ })
536
+ }
537
+
538
+ fn map_env(value: Option<&Value>) -> BTreeMap<String, String> {
539
+ let mut out = BTreeMap::new();
540
+ let Some(obj) = value.and_then(Value::as_object) else {
541
+ return out;
542
+ };
543
+ for (key, value) in obj {
544
+ if let Some(text) = value.as_str().filter(|s| !s.is_empty()) {
545
+ out.insert(key.clone(), text.to_string());
546
+ }
547
+ }
548
+ out
549
+ }
550
+
551
+ fn target_iter(targets: &Value) -> Vec<LeaderTarget> {
552
+ if let Some(items) = targets.as_array() {
553
+ return items.iter().filter_map(target_from_value).collect();
554
+ }
555
+ for key in ["targets", "panes"] {
556
+ if let Some(items) = targets.get(key).and_then(Value::as_array) {
557
+ return items.iter().filter_map(target_from_value).collect();
558
+ }
559
+ }
560
+ Vec::new()
561
+ }
562
+
563
+ fn readopt_candidate_is_usable_leader(workspace: &Path, target: &LeaderTarget) -> bool {
564
+ if !target.active || target.provider.is_none() {
565
+ return false;
566
+ }
567
+ target
568
+ .current_path
569
+ .as_deref()
570
+ .is_some_and(|path| path_in_workspace(path, workspace))
571
+ }
572
+
573
+ fn rediscover_candidate_is_usable_leader(target: &LeaderTarget) -> bool {
574
+ target.active
575
+ && target
576
+ .current_command
577
+ .as_deref()
578
+ .is_some_and(|command| !command.trim().is_empty())
579
+ }
580
+
581
+ fn path_in_workspace(path: &Path, workspace: &Path) -> bool {
582
+ let cwd = resolve_workspace_for_hash(path);
583
+ let root = resolve_workspace_for_hash(workspace);
584
+ cwd == root || cwd.starts_with(root)
585
+ }
586
+
587
+ fn leader_command_provider(command: &str) -> Option<Provider> {
588
+ let lower = command.to_ascii_lowercase();
589
+ if lower.contains("claude") {
590
+ Some(Provider::ClaudeCode)
591
+ } else if lower.contains("codex") {
592
+ Some(Provider::Codex)
593
+ } else if lower.contains("fake") {
594
+ Some(Provider::Fake)
595
+ } else {
596
+ None
597
+ }
598
+ }
599
+
600
+ fn target_matches_owner_identity(target: &LeaderTarget, identity: &OwnerIdentity) -> bool {
601
+ identity
602
+ .pane_id
603
+ .as_deref()
604
+ .is_some_and(|pane| pane == target.pane_id.as_str())
605
+ || identity
606
+ .leader_session_uuid
607
+ .as_ref()
608
+ .is_some_and(|uuid| target.leader_session_uuid.as_ref() == Some(uuid))
609
+ || env_triple_matches(target, identity)
610
+ }
611
+
612
+ fn pane_info_value(
613
+ info: &PaneInfo,
614
+ provider: Option<Provider>,
615
+ leader_session_uuid: Option<&LeaderSessionUuid>,
616
+ ) -> Value {
617
+ json!({
618
+ "pane_id": info.pane_id.as_str(),
619
+ "session_name": info.session.as_str(),
620
+ "window_index": info.window_index,
621
+ "window_name": info.window_name.as_ref().map(WindowName::as_str),
622
+ "pane_index": info.pane_index,
623
+ "pane_tty": info.tty,
624
+ "pane_current_command": info.current_command,
625
+ "pane_current_path": info.current_path.as_ref().map(|path| path.to_string_lossy().to_string()),
626
+ "fingerprint": info.leader_env.get("TEAM_AGENT_MACHINE_FINGERPRINT"),
627
+ "provider": provider.map(provider_wire),
628
+ "leader_session_uuid": leader_session_uuid.map(LeaderSessionUuid::as_str),
629
+ })
630
+ }
631
+
632
+ fn candidate_pane_ids(candidates: &[LeaderTarget]) -> Vec<String> {
633
+ candidates
634
+ .iter()
635
+ .map(|candidate| candidate.pane_id.as_str().to_string())
636
+ .collect()
637
+ }
638
+
639
+ fn ambiguous_incident_id(identity: &OwnerIdentity, panes: &[String]) -> String {
640
+ let mut bytes = Vec::new();
641
+ bytes.extend_from_slice(identity.provider.map(provider_wire).unwrap_or("").as_bytes());
642
+ bytes.push(0);
643
+ bytes.extend_from_slice(identity.team_id.as_bytes());
644
+ bytes.push(0);
645
+ if let Some(uuid) = &identity.leader_session_uuid {
646
+ bytes.extend_from_slice(uuid.as_str().as_bytes());
647
+ }
648
+ for pane in panes {
649
+ bytes.push(0);
650
+ bytes.extend_from_slice(pane.as_bytes());
651
+ }
652
+ format!("rediscover_{}", sha1_hex_prefix(&bytes, 12))
653
+ }
654
+
655
+ fn old_target_from_state(state: &Value) -> Option<String> {
656
+ state
657
+ .get("leader_receiver")
658
+ .and_then(|receiver| receiver.get("pane_id"))
659
+ .and_then(Value::as_str)
660
+ .filter(|pane| !pane.is_empty())
661
+ .map(str::to_string)
662
+ }
663
+
664
+ fn emit_failed_rebind_required(
665
+ event_log: &crate::event_log::EventLog,
666
+ state: &Value,
667
+ identity: &OwnerIdentity,
668
+ invalidation_reason: Option<&str>,
669
+ error: &str,
670
+ ) -> Result<(), LeaderError> {
671
+ event_log.write(
672
+ LeaderEvent::ReceiverRebindRequired.name(),
673
+ json!({
674
+ "old_pane_id": old_target_from_state(state).or_else(|| identity.pane_id.clone()),
675
+ "reason": invalidation_reason,
676
+ "provider": identity.provider.map(provider_wire),
677
+ "team_id": empty_to_null(identity.team_id.as_str()),
678
+ "rediscovery_status": "failed",
679
+ "error": error,
680
+ }),
681
+ )?;
682
+ Ok(())
683
+ }
684
+
685
+ fn emit_ambiguous_candidates(
686
+ event_log: &crate::event_log::EventLog,
687
+ state: &Value,
688
+ identity: &OwnerIdentity,
689
+ candidates: &[LeaderTarget],
690
+ incident_id: &str,
691
+ golden_shape: bool,
692
+ ) -> Result<(), LeaderError> {
693
+ let panes = candidate_pane_ids(candidates);
694
+ if !golden_shape {
695
+ event_log.write(
696
+ LeaderEvent::ReceiverAmbiguousCandidates.name(),
697
+ json!({
698
+ "incident_id": incident_id,
699
+ "pane_ids": panes,
700
+ "provider": identity.provider.map(provider_wire),
701
+ "team_id": empty_to_null(identity.team_id.as_str()),
702
+ "uuid_prefix": uuid_prefix(identity.leader_session_uuid.as_ref()).as_str(),
703
+ "debounce_bucket": incident_id,
704
+ "reason": "force_confirm_required",
705
+ "old_pane_id": old_target_from_state(state).or_else(|| identity.pane_id.clone()),
706
+ "owner_identity": owner_identity_value(identity),
707
+ "invalidation_reason": Value::Null,
708
+ "queued": candidate_queue_values(candidates),
709
+ }),
710
+ )?;
711
+ return Ok(());
712
+ }
713
+ event_log.write(
714
+ LeaderEvent::ReceiverAmbiguousCandidates.name(),
715
+ json!({
716
+ "incident_id": incident_id,
717
+ "candidates": panes,
718
+ "provider": event_provider(identity),
719
+ "team_id": empty_to_null(identity.team_id.as_str()),
720
+ "uuid_prefix": uuid_prefix(identity.leader_session_uuid.as_ref()).as_str(),
721
+ "debounce_bucket": incident_id,
722
+ "reason": "force_confirm_required",
723
+ "old_pane_id": old_target_from_state(state).or_else(|| identity.pane_id.clone()),
724
+ }),
725
+ )?;
726
+ for candidate in candidates {
727
+ event_log.write(
728
+ "leader_receiver.ambiguous_candidate_queued",
729
+ json!({
730
+ "incident_id": incident_id,
731
+ "pane_id": candidate.pane_id.as_str(),
732
+ "ok": true,
733
+ "error": Value::Null,
734
+ }),
735
+ )?;
736
+ }
737
+ Ok(())
738
+ }
739
+
740
+ fn ambiguous_candidates_already_broadcast(
741
+ event_log: &crate::event_log::EventLog,
742
+ incident_id: &str,
743
+ ) -> Result<bool, LeaderError> {
744
+ Ok(event_log.tail(200)?.iter().any(|event| {
745
+ event.get("event").and_then(Value::as_str) == Some(LeaderEvent::ReceiverAmbiguousCandidates.name())
746
+ && event.get("incident_id").and_then(Value::as_str) == Some(incident_id)
747
+ }))
748
+ }
749
+
750
+ fn emit_no_owner_rebind_required(
751
+ event_log: &crate::event_log::EventLog,
752
+ rediscovery_status: &str,
753
+ state: &Value,
754
+ identity: &OwnerIdentity,
755
+ reason: Option<&str>,
756
+ ) -> Result<(), LeaderError> {
757
+ event_log.write(
758
+ LeaderEvent::ReceiverRebindRequired.name(),
759
+ json!({
760
+ "old_pane_id": old_target_from_state(state),
761
+ "reason": reason,
762
+ "provider": event_provider(identity),
763
+ "team_id": empty_to_null(identity.team_id.as_str()),
764
+ "rediscovery_status": rediscovery_status,
765
+ }),
766
+ )?;
767
+ Ok(())
768
+ }
769
+
770
+ fn emit_owner_missing_rebind_required(
771
+ event_log: &crate::event_log::EventLog,
772
+ state: &Value,
773
+ identity: &OwnerIdentity,
774
+ invalidation_reason: Option<&str>,
775
+ ) -> Result<(), LeaderError> {
776
+ let recovery_action = if identity.from_caller {
777
+ "open the owning leader pane or run team-agent claim-leader --confirm from a matching pane"
778
+ } else {
779
+ "run team-agent attach-leader or claim-leader"
780
+ };
781
+ event_log.write(
782
+ LeaderEvent::ReceiverRebindRequired.name(),
783
+ json!({
784
+ "old_pane_id": old_target_from_state(state).or_else(|| identity.pane_id.clone()),
785
+ "reason": invalidation_reason,
786
+ "provider": event_provider(identity),
787
+ "team_id": empty_to_null(identity.team_id.as_str()),
788
+ "owner_identity": owner_identity_value(identity),
789
+ "uuid_prefix": uuid_prefix(identity.leader_session_uuid.as_ref()).as_str(),
790
+ "recovery_action": recovery_action,
791
+ }),
792
+ )?;
793
+ Ok(())
794
+ }
795
+
796
+ fn emit_rebind_required(
797
+ event_log: &crate::event_log::EventLog,
798
+ reason: &str,
799
+ state: &Value,
800
+ identity: &OwnerIdentity,
801
+ invalidation_reason: Option<&str>,
802
+ recovery_action: &str,
803
+ ) -> Result<(), LeaderError> {
804
+ event_log.write(
805
+ LeaderEvent::ReceiverRebindRequired.name(),
806
+ json!({
807
+ "old_pane_id": old_target_from_state(state).or_else(|| identity.pane_id.clone()),
808
+ "reason": invalidation_reason,
809
+ "provider": identity.provider.map(provider_wire),
810
+ "team_id": empty_to_null(identity.team_id.as_str()),
811
+ "owner_identity": owner_identity_value(identity),
812
+ "uuid_prefix": uuid_prefix(identity.leader_session_uuid.as_ref()).as_str(),
813
+ "rediscovery_status": reason,
814
+ "recovery_action": recovery_action,
815
+ }),
816
+ )?;
817
+ Ok(())
818
+ }
819
+
820
+ fn owner_identity_value(identity: &OwnerIdentity) -> Value {
821
+ identity.raw.clone()
822
+ }
823
+
824
+ fn candidate_values(candidates: &[LeaderTarget], golden_spelling: bool) -> Value {
825
+ Value::Array(
826
+ candidates
827
+ .iter()
828
+ .map(|candidate| {
829
+ if golden_spelling {
830
+ candidate.raw.clone()
831
+ } else {
832
+ legacy_candidate_value(candidate)
833
+ }
834
+ })
835
+ .collect(),
836
+ )
837
+ }
838
+
839
+ fn legacy_candidate_value(candidate: &LeaderTarget) -> Value {
840
+ json!({
841
+ "pane_id": candidate.pane_id.as_str(),
842
+ "session_name": candidate.session.as_ref().map(SessionName::as_str),
843
+ "window_index": optional_numeric_string_value(candidate.window_index.as_deref()),
844
+ "window_name": candidate.window_name.as_ref().map(WindowName::as_str),
845
+ "pane_index": optional_numeric_string_value(candidate.pane_index.as_deref()),
846
+ "pane_tty": candidate.tty,
847
+ "current_command": candidate.current_command,
848
+ "current_path": candidate.current_path.as_ref().map(|path| path.to_string_lossy().to_string()),
849
+ "fingerprint": candidate.fingerprint,
850
+ "provider": candidate.provider.map(provider_wire),
851
+ "leader_session_uuid": candidate.leader_session_uuid.as_ref().map(LeaderSessionUuid::as_str),
852
+ })
853
+ }
854
+
855
+ fn optional_numeric_string_value(value: Option<&str>) -> Value {
856
+ match value {
857
+ Some(raw) => raw
858
+ .parse::<u64>()
859
+ .map_or_else(|_| Value::String(raw.to_string()), |parsed| json!(parsed)),
860
+ None => Value::Null,
861
+ }
862
+ }
863
+
864
+ fn candidate_queue_values(candidates: &[LeaderTarget]) -> Value {
865
+ Value::Array(
866
+ candidates
867
+ .iter()
868
+ .map(|candidate| {
869
+ json!({
870
+ "pane_id": candidate.pane_id.as_str(),
871
+ "queued": true,
872
+ })
873
+ })
874
+ .collect(),
875
+ )
876
+ }
877
+
878
+ fn empty_to_null(value: &str) -> Option<&str> {
879
+ if value.is_empty() {
880
+ None
881
+ } else {
882
+ Some(value)
883
+ }
884
+ }
885
+
886
+ fn old_pane_id(receiver: &LeaderReceiver, owner_record: Option<&TeamOwner>) -> Option<String> {
887
+ owner_record
888
+ .map(|owner| owner.pane_id.as_str().to_string())
889
+ .or_else(|| Some(receiver.pane_id.as_str().to_string()).filter(|pane| !pane.is_empty()))
890
+ }
891
+
892
+ fn uuid_prefix(uuid: Option<&LeaderSessionUuid>) -> String {
893
+ uuid.map_or_else(String::new, |value| prefix(value.as_str(), 8).to_string())
894
+ }
895
+
896
+ fn provider_wire(provider: Provider) -> &'static str {
897
+ match provider {
898
+ Provider::Claude => "claude",
899
+ Provider::ClaudeCode => "claude_code",
900
+ Provider::Codex => "codex",
901
+ Provider::GeminiCli => "gemini_cli",
902
+ Provider::Fake => "fake",
903
+ }
904
+ }
905
+
906
+ fn event_provider(identity: &OwnerIdentity) -> &'static str {
907
+ provider_wire(identity.provider.unwrap_or(Provider::Codex))
908
+ }
909
+
910
+ fn env_triple_matches(target: &LeaderTarget, identity: &OwnerIdentity) -> bool {
911
+ target
912
+ .leader_env
913
+ .get("TEAM_AGENT_LEADER_PANE_ID")
914
+ .is_some_and(|value| identity.pane_id.as_deref() == Some(value.as_str()))
915
+ && target
916
+ .leader_env
917
+ .get("TEAM_AGENT_LEADER_PROVIDER")
918
+ .is_some_and(|value| identity.provider.is_some_and(|provider| value == provider_wire(provider)))
919
+ && target
920
+ .leader_env
921
+ .get("TEAM_AGENT_MACHINE_FINGERPRINT")
922
+ .is_some_and(|value| value == &identity.machine_fingerprint)
923
+ }
924
+
925
+ fn different_live_owner_uuid_mismatch(
926
+ owner_record: Option<&TeamOwner>,
927
+ candidate: &LeaderTarget,
928
+ targets: &Value,
929
+ ) -> bool {
930
+ let Some(owner) = owner_record else {
931
+ return false;
932
+ };
933
+ if owner.pane_id.as_str() == candidate.pane_id.as_str() {
934
+ return false;
935
+ }
936
+ let Some(owner_uuid) = &owner.leader_session_uuid else {
937
+ return false;
938
+ };
939
+ if candidate.leader_session_uuid.as_ref() == Some(owner_uuid) {
940
+ return false;
941
+ }
942
+ target_iter(targets)
943
+ .iter()
944
+ .any(|target| target.active && target.pane_id.as_str() == owner.pane_id.as_str())
945
+ }
946
+
947
+ fn next_owner_epoch(
948
+ state: &Value,
949
+ receiver: &LeaderReceiver,
950
+ owner_record: Option<&TeamOwner>,
951
+ ) -> OwnerEpoch {
952
+ let current = owner_record
953
+ .map(|owner| owner.owner_epoch.0)
954
+ .or_else(|| state_owner_epoch(state).map(|epoch| epoch.0))
955
+ .or_else(|| receiver.owner_epoch.map(|epoch| epoch.0))
956
+ .unwrap_or(0);
957
+ OwnerEpoch(current.saturating_add(1))
958
+ }
959
+
960
+ fn state_owner_epoch(state: &Value) -> Option<OwnerEpoch> {
961
+ state
962
+ .get("team_owner")
963
+ .and_then(|owner| owner.get("owner_epoch"))
964
+ .and_then(Value::as_u64)
965
+ .or_else(|| {
966
+ state
967
+ .get("leader_receiver")
968
+ .and_then(|receiver| receiver.get("owner_epoch"))
969
+ .and_then(Value::as_u64)
970
+ })
971
+ .map(OwnerEpoch)
972
+ }
973
+
974
+ fn owner_from_candidate(
975
+ candidate: &LeaderTarget,
976
+ provider: Provider,
977
+ uuid: Option<LeaderSessionUuid>,
978
+ epoch: OwnerEpoch,
979
+ claimed_via: ClaimedVia,
980
+ ) -> TeamOwner {
981
+ TeamOwner {
982
+ pane_id: candidate.pane_id.clone(),
983
+ provider,
984
+ machine_fingerprint: candidate
985
+ .fingerprint
986
+ .clone()
987
+ .or_else(|| candidate.leader_env.get("TEAM_AGENT_MACHINE_FINGERPRINT").cloned())
988
+ .unwrap_or_default(),
989
+ leader_session_uuid: uuid,
990
+ owner_epoch: epoch,
991
+ claimed_at: now_ts(),
992
+ claimed_via,
993
+ os_user: None,
994
+ }
995
+ }
996
+
997
+ fn receiver_from_candidate(
998
+ target: &LeaderTarget,
999
+ prior: &LeaderReceiver,
1000
+ provider: Provider,
1001
+ uuid: Option<LeaderSessionUuid>,
1002
+ epoch: OwnerEpoch,
1003
+ discovery: Discovery,
1004
+ ) -> LeaderReceiver {
1005
+ LeaderReceiver {
1006
+ mode: ReceiverMode::DirectTmux,
1007
+ status: ReceiverStatus::Attached,
1008
+ provider,
1009
+ pane_id: target.pane_id.clone(),
1010
+ session_name: target.session.clone(),
1011
+ window_index: target.window_index.clone(),
1012
+ window_name: target.window_name.clone(),
1013
+ pane_index: target.pane_index.clone(),
1014
+ pane_tty: target.tty.clone(),
1015
+ pane_current_command: target.current_command.clone(),
1016
+ fingerprint: target.fingerprint.clone(),
1017
+ leader_session_uuid: uuid,
1018
+ owner_epoch: Some(epoch),
1019
+ attached_at: Some(now_ts()),
1020
+ discovery: Some(discovery),
1021
+ requested_provider: prior.requested_provider,
1022
+ warning: prior.warning.clone(),
1023
+ }
1024
+ }
1025
+
1026
+ fn empty_prior(provider: Provider, epoch: OwnerEpoch) -> LeaderReceiver {
1027
+ LeaderReceiver {
1028
+ mode: ReceiverMode::DirectTmux,
1029
+ status: ReceiverStatus::Attached,
1030
+ provider,
1031
+ pane_id: PaneId::new(""),
1032
+ session_name: None,
1033
+ window_index: None,
1034
+ window_name: None,
1035
+ pane_index: None,
1036
+ pane_tty: None,
1037
+ pane_current_command: None,
1038
+ fingerprint: None,
1039
+ leader_session_uuid: None,
1040
+ owner_epoch: Some(epoch),
1041
+ attached_at: None,
1042
+ discovery: None,
1043
+ requested_provider: None,
1044
+ warning: None,
1045
+ }
1046
+ }
1047
+
1048
+ fn state_receiver(state: &Value) -> Option<LeaderReceiver> {
1049
+ state
1050
+ .get("leader_receiver")
1051
+ .cloned()
1052
+ .and_then(|value| serde_json::from_value(value).ok())
1053
+ }
1054
+
1055
+ fn write_readopt_state(
1056
+ workspace: &Path,
1057
+ state: &mut Value,
1058
+ receiver: &LeaderReceiver,
1059
+ owner: &TeamOwner,
1060
+ ) -> Result<(), LeaderError> {
1061
+ if !state.is_object() {
1062
+ *state = json!({});
1063
+ }
1064
+ let Some(root) = state.as_object_mut() else {
1065
+ return Err(LeaderError::Validation("state root is not an object".to_string()));
1066
+ };
1067
+ root.insert("leader_receiver".to_string(), serde_json::to_value(receiver)?);
1068
+ root.insert("team_owner".to_string(), serde_json::to_value(owner)?);
1069
+ crate::leader::write_lease_dual_state(workspace, state)
1070
+ }
1071
+
1072
+ fn write_receiver_state(
1073
+ workspace: &Path,
1074
+ state: &mut Value,
1075
+ receiver: &LeaderReceiver,
1076
+ ) -> Result<(), LeaderError> {
1077
+ if !state.is_object() {
1078
+ *state = json!({});
1079
+ }
1080
+ let Some(root) = state.as_object_mut() else {
1081
+ return Err(LeaderError::Validation("state root is not an object".to_string()));
1082
+ };
1083
+ root.insert("leader_receiver".to_string(), serde_json::to_value(receiver)?);
1084
+ crate::leader::write_lease_dual_state(workspace, state)
1085
+ }
1086
+
1087
+ fn get_str(value: &Value, key: &str) -> Option<String> {
1088
+ value
1089
+ .get(key)
1090
+ .and_then(|raw| {
1091
+ raw.as_str()
1092
+ .map(str::to_string)
1093
+ .or_else(|| raw.as_u64().map(|n| n.to_string()))
1094
+ })
1095
+ .filter(|s| !s.is_empty())
1096
+ }
1097
+
1098
+ #[cfg(test)]
1099
+ mod tests;