@team-agent/installer 0.2.11 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -1,554 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import copy
4
- from datetime import datetime, timezone
5
- from pathlib import Path
6
- from typing import Any
7
-
8
- from team_agent.events import EventLog
9
- from team_agent.message_store import MessageStore
10
- from team_agent.permissions import resolve_permissions
11
- from team_agent.display.backend import display_backend_has_worker_views, display_backend_opens_before_leader_rebind, resolve_restart_display_backend
12
- from team_agent.display.close import close_team_display_backends
13
- from team_agent.display.rebuild import rebuild_restart_display_after_rebind
14
- from team_agent.restart.selection import select_restart_state
15
- from team_agent.restart.snapshot import save_team_runtime_snapshot
16
- from team_agent.spec import load_spec
17
- from team_agent.state import (
18
- check_team_owner,
19
- compact_team_state,
20
- populate_team_owner_from_env,
21
- save_runtime_state,
22
- team_state_key,
23
- write_team_state,
24
- )
25
-
26
- def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None) -> dict[str, Any]:
27
- # Lazy-import everything from team_agent.runtime so existing tests that
28
- # patch runtime.shell_resume_command_for_agent / runtime.run_cmd /
29
- # runtime.start_coordinator / runtime.get_adapter continue to take effect
30
- # at call time. Runtime re-exports the provider helpers, so this also
31
- # routes through the providers module without binding it directly.
32
- from team_agent.runtime import (
33
- ResumeUnavailable,
34
- RuntimeError,
35
- _attach_profile_resume_root,
36
- _attach_team_profile_dirs,
37
- _capture_agent_session,
38
- _clear_session_capture_fields,
39
- _close_ghostty_display,
40
- _compile_team_dir_spec,
41
- _effective_runtime_config,
42
- _ensure_agent_start_requirements,
43
- _handle_startup_prompts_and_verify_window,
44
- _is_team_doc_dir,
45
- _open_worker_displays,
46
- _prepare_resume_state,
47
- _spec_team_dir,
48
- _tmux_session_conflict_error,
49
- _tmux_session_exists,
50
- _tmux_start_command_for_agent_window,
51
- _tmux_window_exists,
52
- ensure_workspace_dirs,
53
- get_adapter,
54
- run_cmd,
55
- shell_command_for_agent,
56
- shell_resume_command_for_agent,
57
- start_coordinator,
58
- )
59
- state = select_restart_state(workspace, team)
60
- gate = check_team_owner(state)
61
- if gate:
62
- return gate
63
- spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
64
- team_dir = Path(str(state.get("team_dir"))) if state.get("team_dir") else _spec_team_dir(spec_path, workspace)
65
- if _is_team_doc_dir(team_dir):
66
- compiled = _compile_team_dir_spec(team_dir, workspace)
67
- spec = compiled["spec"]
68
- spec_path = team_dir / "team.spec.yaml"
69
- state["spec_path"] = str(spec_path)
70
- else:
71
- if not spec_path.exists():
72
- raise RuntimeError(f"missing spec for restart: {spec_path}")
73
- spec = load_spec(spec_path)
74
- _attach_team_profile_dirs(spec, spec_path, workspace, team_dir)
75
- ensure_workspace_dirs(workspace)
76
- event_log = EventLog(workspace)
77
- session_name = state.get("session_name") or spec.get("runtime", {}).get("session_name") or f"team-{spec['team']['name']}"
78
- state.setdefault("team_dir", str(team_dir))
79
- if _tmux_session_exists(session_name):
80
- event_log.write(
81
- "restart.session_conflict",
82
- session=session_name,
83
- action="use a different team name or runtime.session_name; do not terminate existing tmux sessions from restart",
84
- )
85
- raise RuntimeError(_tmux_session_conflict_error(session_name))
86
- runtime_cfg = _effective_runtime_config(spec.get("runtime", {}))
87
- display_backend = resolve_restart_display_backend(spec, state, event_log)
88
- # Stage 7 S5 — Slice 6 lifecycle atomicity contract: compute restart_agents
89
- # early so we can pre-validate resumability BEFORE any destructive teardown
90
- # (ghostty close, tmux session creation). Without --allow-fresh, every
91
- # non-paused worker MUST be resumable; if any is not, refuse the operation
92
- # atomically with a structured result and a restart.atomic_refusal event.
93
- # No rollback path is needed because nothing has been created yet.
94
- restart_agents = [
95
- agent
96
- for agent in spec.get("agents", [])
97
- if state.get("agents", {}).get(agent["id"], {}).get("status") != "paused" and not agent.get("paused")
98
- ]
99
- # cr strict-typing (2026-05-27): refuse the operation deterministically
100
- # before any decision logic if any persisted first_send_at is corrupt
101
- # (empty string, 0, False, literal "null", any non-ISO garbage). This
102
- # avoids silent misclassification through Python truthiness and gives the
103
- # operator a clear audit signal that state.json is damaged.
104
- invalid_first_send_at = _collect_corrupt_first_send_at(restart_agents, state)
105
- if invalid_first_send_at:
106
- for entry in invalid_first_send_at:
107
- event_log.write(
108
- "restart.first_send_at_invalid",
109
- worker_id=entry["worker_id"],
110
- raw_first_send_at=entry["raw_first_send_at"],
111
- raw_first_send_at_type=entry["raw_first_send_at_type"],
112
- )
113
- invalid_names = [entry["worker_id"] for entry in invalid_first_send_at]
114
- return {
115
- "ok": False,
116
- "status": "refused",
117
- "reason": "invalid_first_send_at",
118
- "invalid_first_send_at": invalid_first_send_at,
119
- "allow_fresh": bool(allow_fresh),
120
- "error": (
121
- f"Cannot restart: workers {invalid_names} have a corrupt "
122
- "first_send_at in state.json (only null/missing or a valid "
123
- "ISO-8601 UTC timestamp string is accepted). Inspect the "
124
- "restart.first_send_at_invalid audit events for raw values "
125
- "and repair state.json before retrying."
126
- ),
127
- }
128
- # cr C2: emit one restart.resume_decision event per non-paused worker so
129
- # every restart attempt produces an auditable per-worker classification.
130
- # The function returns only refused workers — populated when
131
- # allow_fresh=False AND at least one interacted worker cannot be repaired.
132
- refused = _emit_resume_decisions(
133
- workspace, restart_agents, state, get_adapter, event_log, allow_fresh,
134
- )
135
- if refused:
136
- event_log.write(
137
- "restart.atomic_refusal",
138
- unresumable=refused,
139
- allow_fresh=bool(allow_fresh),
140
- reason="resume_atomicity",
141
- )
142
- return {
143
- "ok": False,
144
- "status": "refused",
145
- "reason": "resume_atomicity",
146
- "unresumable": refused,
147
- "allow_fresh": bool(allow_fresh),
148
- "error": _format_atomic_refusal_error(refused),
149
- }
150
- close_team_display_backends(state, event_log)
151
- for agent_id, agent_state in state.get("agents", {}).items():
152
- _close_ghostty_display(agent_id, agent_state, event_log)
153
- state["display_backend"] = display_backend
154
- _ensure_agent_start_requirements(workspace, restart_agents, event_log, "restart")
155
- first = True
156
- restarted: list[dict[str, Any]] = []
157
- new_agents: dict[str, Any] = {}
158
- display_jobs: list[tuple[str, dict[str, Any]]] = []
159
- for agent in spec.get("agents", []):
160
- previous = state.get("agents", {}).get(agent["id"], {})
161
- if previous.get("status") == "paused" or agent.get("paused"):
162
- new_agents[agent["id"]] = dict(previous or {"status": "paused", "provider": agent["provider"]})
163
- new_agents[agent["id"]]["status"] = "paused"
164
- continue
165
- adapter = get_adapter(agent["provider"])
166
- if not adapter.is_installed():
167
- event_log.write(
168
- "restart.provider_missing",
169
- agent_id=agent["id"],
170
- provider=agent["provider"],
171
- command=adapter.command_name,
172
- )
173
- raise RuntimeError(
174
- f"Provider {agent['provider']} command {adapter.command_name!r} not found for agent {agent['id']}"
175
- )
176
- mcp_config = adapter.mcp_config(workspace, agent["id"])
177
- mcp_path = adapter.install_mcp(workspace, agent["id"], mcp_config)
178
- command_agent = copy.deepcopy(agent)
179
- command_agent["_runtime"] = runtime_cfg
180
- previous = _attach_profile_resume_root(workspace, command_agent, previous)
181
- known_session_ids = {
182
- str(item.get("session_id"))
183
- for aid, item in {**state.get("agents", {}), **new_agents}.items()
184
- if aid != agent["id"] and item.get("session_id")
185
- }
186
- try:
187
- previous = _prepare_resume_state(
188
- workspace,
189
- agent["id"],
190
- previous,
191
- adapter,
192
- event_log,
193
- known_session_ids,
194
- allow_fresh_on_resume_failure=allow_fresh,
195
- )
196
- except ResumeUnavailable as exc:
197
- try:
198
- adapter.cleanup_mcp(workspace, agent["id"], mcp_path)
199
- except Exception as cleanup_exc:
200
- event_log.write(
201
- "restart.mcp_cleanup_failed",
202
- agent_id=agent["id"],
203
- provider=agent["provider"],
204
- mcp_config=str(mcp_path),
205
- error=str(cleanup_exc),
206
- )
207
- raise RuntimeError(str(exc)) from exc
208
- restart_mode = "resumed" if previous.get("session_id") else "fresh"
209
- if restart_mode == "resumed":
210
- try:
211
- command = shell_resume_command_for_agent(command_agent, previous, workspace, mcp_config)
212
- except ResumeUnavailable as exc:
213
- event_log.write("restart.resume_unavailable", agent_id=agent["id"], error=str(exc))
214
- if not allow_fresh:
215
- try:
216
- adapter.cleanup_mcp(workspace, agent["id"], mcp_path)
217
- except Exception as cleanup_exc:
218
- event_log.write(
219
- "restart.mcp_cleanup_failed",
220
- agent_id=agent["id"],
221
- provider=agent["provider"],
222
- mcp_config=str(mcp_path),
223
- error=str(cleanup_exc),
224
- )
225
- raise RuntimeError(
226
- f"Cannot resume agent {agent['id']}: {exc}. "
227
- "Use team-agent restart --allow-fresh only if losing that worker context is acceptable."
228
- ) from exc
229
- command = shell_command_for_agent(command_agent, workspace, mcp_config)
230
- restart_mode = "fresh"
231
- else:
232
- command = shell_command_for_agent(command_agent, workspace, mcp_config)
233
- event_log.write("restart.fresh_spawn", agent_id=agent["id"], provider=agent["provider"], reason="session_id_missing")
234
- event_log.write(
235
- "restart.agent_start",
236
- agent_id=agent["id"],
237
- provider=agent["provider"],
238
- restart_mode=restart_mode,
239
- session_id=previous.get("session_id"),
240
- session=session_name,
241
- window=agent["id"],
242
- tmux_start_mode="new-session" if first else "new-window",
243
- command=command,
244
- mcp_config=str(mcp_path),
245
- )
246
- if first:
247
- proc = run_cmd(["tmux", "new-session", "-d", "-s", session_name, "-n", agent["id"], "sh", "-lc", command])
248
- first = False
249
- else:
250
- proc = run_cmd(["tmux", "new-window", "-t", session_name, "-n", agent["id"], "sh", "-lc", command])
251
- if proc.returncode != 0:
252
- raise RuntimeError(f"Failed to restart agent {agent['id']}: {proc.stderr.strip()}")
253
- if not _handle_startup_prompts_and_verify_window(
254
- adapter, event_log, "restart", agent["id"], agent["provider"], session_name, restart_mode
255
- ):
256
- if restart_mode != "resumed":
257
- raise RuntimeError(f"Failed to restart agent {agent['id']}: tmux window exited after start")
258
- if not allow_fresh:
259
- try:
260
- adapter.cleanup_mcp(workspace, agent["id"], mcp_path)
261
- except Exception as cleanup_exc:
262
- event_log.write(
263
- "restart.mcp_cleanup_failed",
264
- agent_id=agent["id"],
265
- provider=agent["provider"],
266
- mcp_config=str(mcp_path),
267
- error=str(cleanup_exc),
268
- )
269
- raise RuntimeError(
270
- f"Cannot resume agent {agent['id']}: resume window exited or did not become visible. "
271
- "Use team-agent restart --allow-fresh only if losing that worker context is acceptable."
272
- )
273
- event_log.write(
274
- "restart.resume_window_missing_fallback_fresh",
275
- agent_id=agent["id"],
276
- provider=agent["provider"],
277
- session_id=previous.get("session_id"),
278
- )
279
- command = shell_command_for_agent(command_agent, workspace, mcp_config)
280
- restart_mode = "fresh"
281
- tmux_cmd, tmux_start_mode = _tmux_start_command_for_agent_window(session_name, agent["id"], command)
282
- event_log.write(
283
- "restart.agent_start",
284
- agent_id=agent["id"],
285
- provider=agent["provider"],
286
- restart_mode=restart_mode,
287
- session_id=None,
288
- session=session_name,
289
- window=agent["id"],
290
- tmux_start_mode=tmux_start_mode,
291
- command=command,
292
- mcp_config=str(mcp_path),
293
- )
294
- proc = run_cmd(tmux_cmd)
295
- if proc.returncode != 0:
296
- raise RuntimeError(f"Failed to restart agent {agent['id']} fresh after resume exit: {proc.stderr.strip()}")
297
- if not _handle_startup_prompts_and_verify_window(
298
- adapter, event_log, "restart", agent["id"], agent["provider"], session_name, restart_mode
299
- ):
300
- raise RuntimeError(f"Failed to restart agent {agent['id']} fresh: tmux window exited after start")
301
- spawn_time = datetime.now(timezone.utc)
302
- agent_state = dict(previous)
303
- agent_state.update(
304
- {
305
- "status": "running",
306
- "provider": agent["provider"],
307
- "agent_id": agent["id"],
308
- "model": agent.get("model"),
309
- "auth_mode": agent.get("auth_mode"),
310
- "profile": agent.get("profile"),
311
- "window": agent["id"],
312
- "mcp_config": str(mcp_path),
313
- "permissions": resolve_permissions(agent),
314
- "spawn_cwd": str(workspace),
315
- "spawned_at": spawn_time.isoformat(),
316
- }
317
- )
318
- profile_launch = command_agent.get("_provider_profile") or {}
319
- if profile_launch.get("claude_projects_root"):
320
- agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
321
- if restart_mode == "fresh":
322
- _clear_session_capture_fields(agent_state)
323
- if command_agent.get("_session_id"):
324
- agent_state["_pending_session_id"] = command_agent["_session_id"]
325
- _capture_agent_session(
326
- workspace,
327
- agent["id"],
328
- agent_state,
329
- event_log,
330
- timeout_s=1.5,
331
- exclude_session_ids=known_session_ids,
332
- raise_on_missed=False,
333
- )
334
- if display_backend_has_worker_views(display_backend):
335
- display_jobs.append((agent["id"], agent))
336
- new_agents[agent["id"]] = agent_state
337
- restarted.append(
338
- {
339
- "agent_id": agent["id"],
340
- "restart_mode": restart_mode,
341
- "session_id": agent_state.get("session_id"),
342
- "display_target": None,
343
- }
344
- )
345
- display_results = _open_worker_displays(workspace, session_name, display_jobs, event_log, display_backend) if display_backend_opens_before_leader_rebind(display_backend) else {}
346
- for agent_id, display in display_results.items():
347
- if agent_id in new_agents:
348
- new_agents[agent_id]["display"] = display
349
- for item in restarted:
350
- agent_id = item["agent_id"]
351
- if agent_id in display_results:
352
- item["display_target"] = display_results[agent_id]
353
- missing_after_start = [item["agent_id"] for item in restarted if not _tmux_window_exists(session_name, item["agent_id"])]
354
- if missing_after_start:
355
- for agent_id in missing_after_start:
356
- event_log.write("restart.agent_missing_after_start", agent_id=agent_id, target=f"{session_name}:{agent_id}")
357
- rollback = rollback_restart_session(session_name, event_log)
358
- raise RuntimeError(
359
- f"Failed to restart agent {missing_after_start[0]}: tmux window exited after start; "
360
- f"rollback_session_ok={rollback.get('ok')}"
361
- )
362
- state["session_name"] = session_name
363
- state["agents"] = new_agents
364
- populate_team_owner_from_env(state, source="restart")
365
- _save_restart_selected_team_state(workspace, state)
366
- save_team_runtime_snapshot(workspace, state)
367
- MessageStore(workspace)
368
- write_team_state(workspace, spec, state)
369
- from team_agent.leader import autobind_leader_receiver_from_env
370
- leader_provider = str(spec.get("leader", {}).get("provider") or "codex")
371
- rebound_receiver = autobind_leader_receiver_from_env(workspace, leader_provider, source="restart")
372
- if rebound_receiver is None and state.get("leader_receiver"):
373
- stale = state.pop("leader_receiver", None)
374
- event_log.write(
375
- "leader_receiver.rebind_required",
376
- reason="restart_autobind_unresolved",
377
- old_pane_id=(stale or {}).get("pane_id") if isinstance(stale, dict) else None,
378
- old_session_name=(stale or {}).get("session_name") if isinstance(stale, dict) else None,
379
- source="restart",
380
- )
381
- _save_restart_selected_team_state(workspace, state)
382
- save_team_runtime_snapshot(workspace, state)
383
- write_team_state(workspace, spec, state)
384
- rebuild_restart_display_after_rebind(display_backend, workspace, session_name, spec, event_log, restarted, receiver=rebound_receiver)
385
- coordinator = start_coordinator(workspace)
386
- event_log.write("restart.complete", session=session_name, agents=restarted, coordinator=coordinator)
387
- return {"ok": True, "session_name": session_name, "agents": restarted, "coordinator": coordinator}
388
-
389
-
390
- def _save_restart_selected_team_state(workspace: Path, state: dict[str, Any]) -> None:
391
- team_key = str(state.get("active_team_key") or team_state_key(state))
392
- teams = copy.deepcopy(state.get("teams") if isinstance(state.get("teams"), dict) else {})
393
- state["active_team_key"] = team_key
394
- state["teams"] = teams
395
- teams[team_key] = compact_team_state(state)
396
- save_runtime_state(workspace, state)
397
-
398
-
399
- _FIRST_SEND_AT_ABSENT = "absent"
400
- _FIRST_SEND_AT_VALID = "valid"
401
- _FIRST_SEND_AT_CORRUPT = "corrupt"
402
-
403
-
404
- def _classify_first_send_at(value: Any) -> str:
405
- """Strict first_send_at typing (cr verdict, 2026-05-27).
406
-
407
- Returns one of:
408
- "absent" — None or missing field (worker never-interacted).
409
- "valid" — non-empty ISO-8601 UTC string parseable by datetime.fromisoformat.
410
- "corrupt" — anything else: empty string, 0, False, literal "null", garbage.
411
-
412
- The contract requires that corrupt values be detected deterministically
413
- before any restart decision so we never silent-misclassify a worker's
414
- interaction state via Python truthiness.
415
- """
416
- if value is None:
417
- return _FIRST_SEND_AT_ABSENT
418
- if not isinstance(value, str):
419
- return _FIRST_SEND_AT_CORRUPT
420
- if not value:
421
- return _FIRST_SEND_AT_CORRUPT
422
- try:
423
- datetime.fromisoformat(value)
424
- except (ValueError, TypeError):
425
- return _FIRST_SEND_AT_CORRUPT
426
- return _FIRST_SEND_AT_VALID
427
-
428
-
429
- def _collect_corrupt_first_send_at(
430
- restart_agents: list[dict[str, Any]],
431
- state: dict[str, Any],
432
- ) -> list[dict[str, Any]]:
433
- """Walk every non-paused worker and flag any whose persisted first_send_at
434
- is corrupt. Returns the list of invalid records ready for the
435
- `restart.first_send_at_invalid` event and the refusal envelope."""
436
- invalid: list[dict[str, Any]] = []
437
- for agent in restart_agents:
438
- agent_id = agent["id"]
439
- previous = state.get("agents", {}).get(agent_id, {})
440
- raw = previous.get("first_send_at") if isinstance(previous, dict) else None
441
- if _classify_first_send_at(raw) != _FIRST_SEND_AT_CORRUPT:
442
- continue
443
- invalid.append({
444
- "worker_id": agent_id,
445
- "raw_first_send_at": raw,
446
- "raw_first_send_at_type": type(raw).__name__,
447
- })
448
- return invalid
449
-
450
-
451
- def _emit_resume_decisions(
452
- workspace: Path,
453
- restart_agents: list[dict[str, Any]],
454
- state: dict[str, Any],
455
- get_adapter_fn: Any,
456
- event_log: EventLog,
457
- allow_fresh: bool,
458
- ) -> list[dict[str, Any]]:
459
- """Route B audit-events contract (cr C2, 2026-05-27). For every non-paused
460
- worker considered by restart, derive the resume decision per the Route B
461
- matrix and emit ONE `restart.resume_decision` event:
462
-
463
- resumable AND ... -> decision = "resume"
464
- not resumable AND not interacted -> decision = "fresh_start"
465
- not resumable AND interacted AND fresh -> decision = "fresh_start"
466
- not resumable AND interacted AND not fresh -> decision = "refuse"
467
-
468
- Resumability mirrors sessions.resume.prepare_resume_state's repair chain
469
- so workers the runtime would legitimately repair are NOT flagged. Returns
470
- the subset of refused workers — populated only when allow_fresh=False AND
471
- some interacted worker cannot be repaired — for use by atomic_refusal.
472
- """
473
- from team_agent.sessions.resume import recover_resume_session_from_events
474
- refused: list[dict[str, Any]] = []
475
- for agent in restart_agents:
476
- agent_id = agent["id"]
477
- previous = state.get("agents", {}).get(agent_id, {})
478
- session_id = previous.get("session_id")
479
- first_send_at = previous.get("first_send_at")
480
- has_first_send_at = _classify_first_send_at(first_send_at) == _FIRST_SEND_AT_VALID
481
- has_session_id = bool(session_id)
482
- adapter = get_adapter_fn(agent["provider"])
483
- resumable = bool(session_id) and adapter.session_is_resumable(previous, workspace)
484
- if not resumable:
485
- known_session_ids = {
486
- str(item.get("session_id"))
487
- for aid, item in state.get("agents", {}).items()
488
- if aid != agent_id and item.get("session_id")
489
- }
490
- repaired = recover_resume_session_from_events(
491
- workspace, agent_id, previous, adapter, known_session_ids,
492
- )
493
- if not repaired:
494
- repaired = adapter.recover_session_id(
495
- agent_id, previous, workspace, known_session_ids,
496
- )
497
- resumable = bool(repaired)
498
- if resumable:
499
- decision = "resume"
500
- elif not has_first_send_at:
501
- decision = "fresh_start"
502
- elif allow_fresh:
503
- decision = "fresh_start"
504
- else:
505
- decision = "refuse"
506
- event_log.write(
507
- "restart.resume_decision",
508
- worker_id=agent_id,
509
- has_first_send_at=has_first_send_at,
510
- has_session_id=has_session_id,
511
- allow_fresh=bool(allow_fresh),
512
- decision=decision,
513
- first_send_at=first_send_at if has_first_send_at else None,
514
- session_id=session_id,
515
- )
516
- if decision == "refuse":
517
- refused.append({
518
- "agent_id": agent_id,
519
- "reason": "no_persisted_session_id" if not session_id else "session_unresumable",
520
- "session_id": session_id,
521
- "first_send_at": first_send_at,
522
- })
523
- return refused
524
-
525
-
526
- def _format_atomic_refusal_error(refused: list[dict[str, Any]]) -> str:
527
- """C4 (cr verdict, 2026-05-27): the human-readable refusal error must
528
- name every refused worker AND its first_send_at timestamp so an operator
529
- can decide whether to pass --allow-fresh and accept losing that
530
- interaction history."""
531
- names = [item["agent_id"] for item in refused]
532
- details = ". ".join(
533
- f"{item['agent_id']} was first interacted with at {item.get('first_send_at')}; "
534
- "its persisted session is missing"
535
- for item in refused
536
- )
537
- return (
538
- f"Cannot restart: workers {names} have no resumable session despite "
539
- f"previous interaction. {details}. "
540
- "Pass --allow-fresh if you accept losing that interaction history."
541
- )
542
-
543
-
544
- def rollback_restart_session(session_name: str, event_log: EventLog) -> dict[str, Any]:
545
- from team_agent.runtime import run_cmd
546
- proc = run_cmd(["tmux", "kill-session", "-t", session_name], timeout=10)
547
- result = {
548
- "ok": proc.returncode == 0,
549
- "session": session_name,
550
- "stdout": proc.stdout.strip(),
551
- "stderr": proc.stderr.strip(),
552
- }
553
- event_log.write("restart.rollback_session", **result)
554
- return result
@@ -1,89 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import copy
4
- from pathlib import Path
5
- from typing import Any
6
-
7
- from team_agent.paths import runtime_dir
8
- from team_agent.state import load_runtime_state, runtime_state_path
9
- from team_agent.restart.snapshot import load_snapshot_state, state_team_name
10
-
11
-
12
- def restart_candidates(workspace: Path) -> list[dict[str, Any]]:
13
- by_session: dict[str, dict[str, Any]] = {}
14
- snapshots_root = runtime_dir(workspace) / "teams"
15
- for path in sorted(snapshots_root.glob("*/state.json")) if snapshots_root.exists() else []:
16
- state = load_snapshot_state(path)
17
- if not state or not state.get("session_name"):
18
- continue
19
- session_name = str(state["session_name"])
20
- by_session[session_name] = restart_candidate_from_state(state, path)
21
- active = load_runtime_state(workspace)
22
- if active.get("session_name"):
23
- by_session[str(active["session_name"])] = restart_candidate_from_state(active, runtime_state_path(workspace))
24
- return sorted(by_session.values(), key=lambda item: item.get("session_name") or "")
25
-
26
-
27
- def restart_candidate_from_state(state: dict[str, Any], state_path: Path) -> dict[str, Any]:
28
- session_name = str(state.get("session_name") or "")
29
- return {
30
- "session_name": session_name,
31
- "team_name": state_team_name(state),
32
- "state_path": str(state_path),
33
- "spec_path": state.get("spec_path"),
34
- "agents": sorted(state.get("agents", {}).keys()),
35
- "has_context": state_has_restart_context(state),
36
- "state": state,
37
- }
38
-
39
-
40
- def state_has_restart_context(state: dict[str, Any]) -> bool:
41
- for agent_state in state.get("agents", {}).values():
42
- if not isinstance(agent_state, dict):
43
- continue
44
- if agent_state.get("session_id") or agent_state.get("rollout_path") or agent_state.get("captured_at"):
45
- return True
46
- return bool(state.get("agents"))
47
-
48
-
49
- def select_restart_state(workspace: Path, team: str | None = None) -> dict[str, Any]:
50
- from team_agent.runtime import RuntimeError
51
- candidates = [item for item in restart_candidates(workspace) if item.get("has_context")]
52
- if team:
53
- matches = [
54
- item
55
- for item in candidates
56
- if team in {item.get("session_name"), item.get("team_name"), Path(str(item.get("state_path"))).parent.name}
57
- ]
58
- if len(matches) == 1:
59
- return copy.deepcopy(matches[0]["state"])
60
- if len(matches) > 1:
61
- raise RuntimeError("restart team selector is ambiguous. " + format_restart_candidates(matches))
62
- raise RuntimeError(f"restart team {team!r} not found. " + format_restart_candidates(candidates))
63
- if len(candidates) == 1:
64
- return copy.deepcopy(candidates[0]["state"])
65
- if len(candidates) > 1:
66
- raise RuntimeError(
67
- "multiple restartable teams found in this workspace; pass --team <session_name> to choose. "
68
- + format_restart_candidates(candidates)
69
- )
70
- return load_runtime_state(workspace)
71
-
72
-
73
- def format_restart_candidates(candidates: list[dict[str, Any]]) -> str:
74
- if not candidates:
75
- return "No restartable team state was found."
76
- parts = []
77
- for item in candidates:
78
- parts.append(
79
- f"{item.get('session_name')} team={item.get('team_name') or '-'} "
80
- f"agents={','.join(item.get('agents') or []) or '-'}"
81
- )
82
- return "Candidates: " + "; ".join(parts)
83
-
84
-
85
- def quick_start_existing_context(workspace: Path, session_name: str) -> dict[str, Any] | None:
86
- for item in restart_candidates(workspace):
87
- if item.get("session_name") == session_name and item.get("has_context"):
88
- return item
89
- return None