@team-agent/installer 0.5.61 → 0.5.63

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 (250) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -2
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +1 -0
  6. package/crates/team-agent/src/cli/emit.rs +41 -7
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +6 -1
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +33 -3
  11. package/crates/team-agent/src/cli/named_address.rs +8 -1
  12. package/crates/team-agent/src/cli/profile.rs +2 -1
  13. package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
  14. package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
  15. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  16. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  17. package/crates/team-agent/src/cli/send/resolve.rs +206 -1
  18. package/crates/team-agent/src/cli/send.rs +2 -1
  19. package/crates/team-agent/src/cli/spec.rs +1 -0
  20. package/crates/team-agent/src/cli/status.rs +2 -1
  21. package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
  22. package/crates/team-agent/src/cli/status_port/format.rs +1 -0
  23. package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  25. package/crates/team-agent/src/cli/types.rs +3 -2
  26. package/crates/team-agent/src/communication_mode/mod.rs +1 -0
  27. package/crates/team-agent/src/compiler.rs +1 -0
  28. package/crates/team-agent/src/conpty/backend.rs +1 -0
  29. package/crates/team-agent/src/conpty/mod.rs +1 -0
  30. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  31. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  32. package/crates/team-agent/src/coordinator/health.rs +1 -0
  33. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  34. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  35. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  36. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  37. package/crates/team-agent/src/coordinator/steps/abnormal.rs +15 -866
  38. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  39. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  41. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  44. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  45. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +139 -375
  46. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  47. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -49
  49. package/crates/team-agent/src/coordinator/tick.rs +23 -40
  50. package/crates/team-agent/src/coordinator/types.rs +3 -176
  51. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  52. package/crates/team-agent/src/db/message_store.rs +2 -1
  53. package/crates/team-agent/src/db/migration.rs +5 -4
  54. package/crates/team-agent/src/db/mod.rs +1 -0
  55. package/crates/team-agent/src/db/schema.rs +1 -0
  56. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  57. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  58. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  59. package/crates/team-agent/src/layout/manager.rs +3 -7
  60. package/crates/team-agent/src/layout/mod.rs +2 -2
  61. package/crates/team-agent/src/layout/overlay.rs +6 -1
  62. package/crates/team-agent/src/layout/placement.rs +1 -0
  63. package/crates/team-agent/src/layout/recovery.rs +3 -0
  64. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  65. package/crates/team-agent/src/layout/sessions.rs +8 -1
  66. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  67. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  68. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  69. package/crates/team-agent/src/leader/helpers.rs +1 -0
  70. package/crates/team-agent/src/leader/incident.rs +1 -0
  71. package/crates/team-agent/src/leader/lease.rs +27 -22
  72. package/crates/team-agent/src/leader/mod.rs +2 -3
  73. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  74. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  75. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  76. package/crates/team-agent/src/leader/registry.rs +3 -15
  77. package/crates/team-agent/src/leader/start.rs +373 -125
  78. package/crates/team-agent/src/leader/takeover.rs +10 -232
  79. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  80. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  81. package/crates/team-agent/src/leader/tests/identity.rs +7 -2
  82. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  83. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  84. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  85. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  86. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  87. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  88. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  89. package/crates/team-agent/src/leader/types.rs +1 -28
  90. package/crates/team-agent/src/lib.rs +3 -0
  91. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  92. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  93. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  95. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  96. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  97. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  99. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  100. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
  101. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  102. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  104. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  105. package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -66
  106. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  107. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  108. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  109. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  110. package/crates/team-agent/src/lifecycle/restart.rs +13 -15
  111. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  112. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  113. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  114. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  115. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  116. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  117. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  118. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  119. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  120. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  121. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  122. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  123. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  124. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  125. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  126. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  127. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  128. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  129. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  130. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  131. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  132. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  133. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  134. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  135. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  136. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  137. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  138. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  139. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  140. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  141. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  142. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  143. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  144. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  145. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  146. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  147. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  148. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  149. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  150. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  151. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  152. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  153. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  154. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  155. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  156. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  157. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  158. package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
  159. package/crates/team-agent/src/mcp_server/tools.rs +1 -0
  160. package/crates/team-agent/src/mcp_server/types.rs +1 -18
  161. package/crates/team-agent/src/mcp_server/wire.rs +5 -5
  162. package/crates/team-agent/src/messaging/activity.rs +1 -0
  163. package/crates/team-agent/src/messaging/address.rs +1 -0
  164. package/crates/team-agent/src/messaging/delivery.rs +4 -3
  165. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  166. package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
  167. package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
  168. package/crates/team-agent/src/messaging/mod.rs +10 -14
  169. package/crates/team-agent/src/messaging/peers.rs +2 -0
  170. package/crates/team-agent/src/messaging/persist.rs +3 -1
  171. package/crates/team-agent/src/messaging/presentation.rs +2 -1
  172. package/crates/team-agent/src/messaging/results.rs +1 -0
  173. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  174. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  175. package/crates/team-agent/src/messaging/send.rs +41 -8
  176. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  177. package/crates/team-agent/src/messaging/types.rs +2 -2
  178. package/crates/team-agent/src/messaging/watchers.rs +19 -13
  179. package/crates/team-agent/src/model/enums.rs +1 -0
  180. package/crates/team-agent/src/model/errors.rs +1 -0
  181. package/crates/team-agent/src/model/ids.rs +1 -0
  182. package/crates/team-agent/src/model/mod.rs +1 -0
  183. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  184. package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
  185. package/crates/team-agent/src/model/paths.rs +1 -0
  186. package/crates/team-agent/src/model/permissions.rs +3 -2
  187. package/crates/team-agent/src/model/routing.rs +1 -0
  188. package/crates/team-agent/src/model/spec.rs +1 -0
  189. package/crates/team-agent/src/model/task_graph.rs +1 -0
  190. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  191. package/crates/team-agent/src/model/yaml.rs +1 -0
  192. package/crates/team-agent/src/packaging/install.rs +9 -147
  193. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  194. package/crates/team-agent/src/packaging/mod.rs +1 -0
  195. package/crates/team-agent/src/packaging/repair.rs +2 -0
  196. package/crates/team-agent/src/packaging/tests.rs +29 -218
  197. package/crates/team-agent/src/packaging/types.rs +8 -15
  198. package/crates/team-agent/src/platform/argv.rs +4 -0
  199. package/crates/team-agent/src/platform/errors.rs +10 -1
  200. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  201. package/crates/team-agent/src/platform/mod.rs +0 -61
  202. package/crates/team-agent/src/platform/process.rs +1 -0
  203. package/crates/team-agent/src/provider/adapter.rs +1 -0
  204. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  205. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  206. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  207. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  208. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  209. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  210. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  211. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  212. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  213. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  214. package/crates/team-agent/src/provider/classify.rs +1 -0
  215. package/crates/team-agent/src/provider/faults.rs +1 -26
  216. package/crates/team-agent/src/provider/helpers.rs +1 -0
  217. package/crates/team-agent/src/provider/mod.rs +1 -1
  218. package/crates/team-agent/src/provider/session/capture.rs +1 -4
  219. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  220. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  221. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  222. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  223. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  224. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  225. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
  226. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  227. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  228. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  229. package/crates/team-agent/src/provider/session_scan.rs +1 -42
  230. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  231. package/crates/team-agent/src/provider/types.rs +1 -0
  232. package/crates/team-agent/src/provider/wire.rs +1 -0
  233. package/crates/team-agent/src/state/identity.rs +16 -5
  234. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  235. package/crates/team-agent/src/state/mod.rs +2 -0
  236. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  237. package/crates/team-agent/src/state/ownership.rs +10 -4
  238. package/crates/team-agent/src/state/paths.rs +12 -0
  239. package/crates/team-agent/src/state/persist.rs +9 -3
  240. package/crates/team-agent/src/state/projection.rs +15 -2
  241. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  242. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  243. package/crates/team-agent/src/state/repository.rs +7 -13
  244. package/crates/team-agent/src/state/selector.rs +1 -0
  245. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  246. package/crates/team-agent/src/tmux_backend.rs +63 -0
  247. package/crates/team-agent/src/transport.rs +15 -0
  248. package/package.json +4 -4
  249. package/crates/team-agent/src/leader/inject.rs +0 -33
  250. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,651 @@
1
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2
+
3
+ #[path = "../../../tests/support/hermetic.rs"]
4
+ mod hermetic_guard;
5
+ #[allow(dead_code)]
6
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
7
+
8
+ use std::collections::{BTreeMap, BTreeSet};
9
+ use std::path::{Path, PathBuf};
10
+ use std::sync::atomic::{AtomicU64, Ordering};
11
+ use std::sync::{Arc, Mutex};
12
+
13
+ use rusqlite::params;
14
+ use serde_json::json;
15
+ use serial_test::serial;
16
+ use team_agent::coordinator::{
17
+ coordinator_pid_path, write_coordinator_metadata, MetadataSource, Pid, WorkspacePath,
18
+ };
19
+ use team_agent::db::schema::open_db;
20
+ use team_agent::lifecycle::{
21
+ add_agent_with_transport, fork_agent_with_transport, remove_agent_with_transport,
22
+ };
23
+ use team_agent::message_store::MessageStore;
24
+ use team_agent::model::ids::AgentId;
25
+ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
26
+ use team_agent::transport::{
27
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
28
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
29
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
30
+ TurnVerification, WindowName,
31
+ };
32
+
33
+ const FAIL_REMOVE_AFTER_HEALTH_DELETE: &str =
34
+ "TEAM_AGENT_TEST_FAIL_REMOVE_AFTER_AGENT_HEALTH_DELETE";
35
+ const FAIL_FORK_AFTER_SPAWN: &str = "TEAM_AGENT_TEST_FAIL_FORK_AFTER_SPAWN";
36
+
37
+ #[test]
38
+ #[serial(env)]
39
+ fn remove_agent_rollback_restores_agent_health_after_post_delete_failure() {
40
+ let _env = EnvGuard::set(FAIL_REMOVE_AFTER_HEALTH_DELETE, "rollback-contract");
41
+ let fixture = RollbackFixture::new("remove-health");
42
+ fixture.seed_team();
43
+ fixture.seed_agent_health(
44
+ "rollbackteam",
45
+ "alpha",
46
+ "BUSY",
47
+ Some("2026-06-09T01:02:03Z"),
48
+ Some(77),
49
+ Some("task-alpha"),
50
+ );
51
+ let before = fixture.agent_health_row("alpha");
52
+ let transport = RecordingTransport::new("team-rollbackteam", &["bravo"]);
53
+
54
+ let result =
55
+ remove_agent_with_transport(&fixture.team, &aid("alpha"), true, false, None, &transport);
56
+ let after = fixture.agent_health_row("alpha");
57
+
58
+ assert!(
59
+ result.is_err(),
60
+ "failure-injection hook {FAIL_REMOVE_AFTER_HEALTH_DELETE}=rollback-contract must fail after deleting agent_health so rollback is exercised; result={result:?} before={before:?} after={after:?}"
61
+ );
62
+ assert_eq!(
63
+ after, before,
64
+ "remove-agent rollback must restore the exact pre-operation agent_health row, including owner_team_id/status/last_output/context/task; result={result:?}"
65
+ );
66
+ }
67
+
68
+ #[test]
69
+ #[serial(env)]
70
+ fn fork_agent_rollback_cleans_spawned_window_spec_state_and_mcp_after_post_spawn_failure() {
71
+ let _home = hermetic_guard::HermeticTestEnv::enter("fork-rollback");
72
+ let env = EnvGuard::set(FAIL_FORK_AFTER_SPAWN, "save_runtime_state");
73
+ let fixture = RollbackFixture::new("fork-post-spawn");
74
+ fixture.seed_team();
75
+ fixture.seed_sibling_team_with_same_fork_id();
76
+ fixture.seed_healthy_coordinator();
77
+ let spec_before = fixture.spec_text();
78
+ let transport = RecordingTransport::new("team-rollbackteam", &["alpha", "bravo"]);
79
+
80
+ let result = fork_agent_with_transport(
81
+ &fixture.team,
82
+ &aid("alpha"),
83
+ &aid("newfork"),
84
+ None,
85
+ false,
86
+ None,
87
+ &transport,
88
+ );
89
+ let spec_after = fixture.spec_text();
90
+ let state_after = load_runtime_state(&fixture.team).unwrap();
91
+ let mcp_path = fixture.team.join(".team/runtime/mcp/newfork.json");
92
+
93
+ assert!(
94
+ result.is_err(),
95
+ "failure-injection hook {FAIL_FORK_AFTER_SPAWN}=save_runtime_state must fail after spawn so post-spawn rollback is exercised; result={result:?}"
96
+ );
97
+ assert!(
98
+ transport
99
+ .killed()
100
+ .contains(&"team-rollbackteam:newfork".to_string()),
101
+ "fork post-spawn rollback must kill the already-spawned window; killed={:?} result={result:?}",
102
+ transport.killed()
103
+ );
104
+ assert_eq!(
105
+ spec_after, spec_before,
106
+ "fork post-spawn rollback must restore team.spec.yaml byte-for-byte; result={result:?}"
107
+ );
108
+ assert_failed_fork_has_only_target_tombstone(&state_after, "newfork", "rollbackteam");
109
+ assert!(
110
+ !mcp_path.exists(),
111
+ "fork post-spawn rollback must cleanup the MCP config written for the half-created agent; leaked={}",
112
+ mcp_path.display()
113
+ );
114
+
115
+ // A rollback tombstone prevents stale-writer resurrection, but must not
116
+ // permanently reserve the name. The next legitimate add clears it.
117
+ drop(env);
118
+ let role = fixture.team.join("agents/newfork.md");
119
+ std::fs::write(&role, agent_doc("newfork", "Replacement worker")).unwrap();
120
+ let retry = add_agent_with_transport(
121
+ &fixture.team,
122
+ &aid("newfork"),
123
+ &role,
124
+ false,
125
+ None,
126
+ &transport,
127
+ );
128
+ assert!(
129
+ retry.is_ok(),
130
+ "same-name retry must clear tombstone: {retry:?}"
131
+ );
132
+ let retried = load_runtime_state(&fixture.team).unwrap();
133
+ assert!(
134
+ retried
135
+ .get("agents")
136
+ .and_then(|agents| agents.get("newfork"))
137
+ .is_some(),
138
+ "same-name retry must recreate the target row"
139
+ );
140
+ assert_ne!(
141
+ retried
142
+ .get("agent_lifecycle")
143
+ .and_then(|rows| rows.get("newfork"))
144
+ .and_then(|row| row.get("state"))
145
+ .and_then(serde_json::Value::as_str),
146
+ Some("retired"),
147
+ "same-name retry must clear the target retirement tombstone"
148
+ );
149
+ }
150
+
151
+ fn assert_failed_fork_has_only_target_tombstone(
152
+ state: &serde_json::Value,
153
+ target: &str,
154
+ team_key: &str,
155
+ ) {
156
+ assert!(
157
+ state
158
+ .get("agents")
159
+ .and_then(|agents| agents.get(target))
160
+ .is_none(),
161
+ "failed fork must leave no target agent row"
162
+ );
163
+ let lifecycle = state
164
+ .get("agent_lifecycle")
165
+ .and_then(serde_json::Value::as_object)
166
+ .expect("rollback must retain the target tombstone");
167
+ assert_eq!(lifecycle.len(), 1, "only target may be tombstoned");
168
+ assert_eq!(
169
+ lifecycle
170
+ .get(target)
171
+ .and_then(|row| row.get("state"))
172
+ .and_then(serde_json::Value::as_str),
173
+ Some("retired")
174
+ );
175
+ let team = state
176
+ .get("teams")
177
+ .and_then(|teams| teams.get(team_key))
178
+ .expect("selected team projection");
179
+ assert!(
180
+ team.get("agents")
181
+ .and_then(|agents| agents.get(target))
182
+ .is_none(),
183
+ "selected team must contain no failed target row"
184
+ );
185
+ assert_eq!(
186
+ team.get("agent_lifecycle")
187
+ .and_then(|rows| rows.get(target))
188
+ .and_then(|row| row.get("state"))
189
+ .and_then(serde_json::Value::as_str),
190
+ Some("retired")
191
+ );
192
+ assert!(
193
+ state.pointer("/teams/sibling/agents/newfork").is_some(),
194
+ "same-named sibling row must survive target rollback"
195
+ );
196
+ }
197
+
198
+ fn aid(id: &str) -> AgentId {
199
+ AgentId::new(id)
200
+ }
201
+
202
+ #[derive(Debug, Clone, PartialEq, Eq)]
203
+ struct HealthRow {
204
+ owner_team_id: Option<String>,
205
+ status: String,
206
+ last_output_at: Option<String>,
207
+ context_usage_pct: Option<i64>,
208
+ current_task_id: Option<String>,
209
+ }
210
+
211
+ struct RollbackFixture {
212
+ team: PathBuf,
213
+ }
214
+
215
+ impl RollbackFixture {
216
+ fn new(label: &str) -> Self {
217
+ static SEQ: AtomicU64 = AtomicU64::new(0);
218
+ let n = SEQ.fetch_add(1, Ordering::Relaxed);
219
+ let root = std::env::temp_dir().join(format!(
220
+ "ta_lifecycle_rollback_{label}_{}_{}",
221
+ std::process::id(),
222
+ n
223
+ ));
224
+ if root.exists() {
225
+ let _ = std::fs::remove_dir_all(&root);
226
+ }
227
+ std::fs::create_dir_all(&root).unwrap();
228
+ Self {
229
+ team: root.join("team"),
230
+ }
231
+ }
232
+
233
+ fn seed_team(&self) {
234
+ std::fs::create_dir_all(self.team.join("agents")).unwrap();
235
+ std::fs::write(
236
+ self.team.join("TEAM.md"),
237
+ "---\nname: rollbackteam\nobjective: Lifecycle rollback contract.\nprovider: codex\n---\n\nRollback team.\n",
238
+ )
239
+ .unwrap();
240
+ std::fs::write(
241
+ self.team.join("agents/alpha.md"),
242
+ agent_doc("alpha", "Alpha worker"),
243
+ )
244
+ .unwrap();
245
+ std::fs::write(
246
+ self.team.join("agents/bravo.md"),
247
+ agent_doc("bravo", "Bravo worker"),
248
+ )
249
+ .unwrap();
250
+ let spec = team_agent::compiler::compile_team(&self.team).expect("compile rollback team");
251
+ let yaml = team_agent::model::yaml::dumps(&spec)
252
+ .replace("default_assignee: \"alpha\"", "default_assignee: \"bravo\"")
253
+ .replace("assign_to: \"alpha\"", "assign_to: \"bravo\"")
254
+ .replace("assignee: \"alpha\"", "assignee: \"bravo\"");
255
+ assert!(
256
+ yaml.contains("id: \"alpha\"") && yaml.contains("id: \"bravo\""),
257
+ "fixture must compile agent ids alpha/bravo before lifecycle calls; spec={yaml}"
258
+ );
259
+ std::fs::write(self.team.join("team.spec.yaml"), yaml).unwrap();
260
+ // Seed identity-complete Codex source backings so rollback tests reach
261
+ // their post-spawn failure injection instead of stopping at materialization.
262
+ let rollout_root = PathBuf::from(std::env::var_os("HOME").expect("hermetic HOME"))
263
+ .join(".codex/sessions/rollback-source");
264
+ std::fs::create_dir_all(&rollout_root).unwrap();
265
+ for (agent, session) in [("alpha", "sess-alpha"), ("bravo", "sess-bravo")] {
266
+ std::fs::write(
267
+ rollout_root.join(format!("rollout-{session}.jsonl")),
268
+ format!(
269
+ "{}\n{}\n",
270
+ json!({
271
+ "type": "session_meta",
272
+ "payload": {"id": session, "cwd": self.team}
273
+ }),
274
+ json!({
275
+ "type": "response_item",
276
+ "payload": {"content": [{
277
+ "type": "input_text",
278
+ "text": format!("You are Team Agent worker `{agent}` with role `fixture`.")
279
+ }]}
280
+ })
281
+ ),
282
+ )
283
+ .unwrap();
284
+ }
285
+ save_runtime_state(
286
+ &self.team,
287
+ &json!({
288
+ "session_name": "team-rollbackteam",
289
+ "active_team_key": "rollbackteam",
290
+ // 0.4.6 tuple-atomic contract: fork now requires complete
291
+ // source tuple. Seed real rollout files + captured_at/via
292
+ // so the fork-source backing guard passes and the test
293
+ // reaches the rollback behaviour it actually asserts.
294
+ "agents": {
295
+ "alpha": {
296
+ "status": "stopped",
297
+ "provider": "codex",
298
+ "auth_mode": "subscription",
299
+ "window": "alpha",
300
+ "session_id": "sess-alpha",
301
+ "rollout_path": rollout_root.join("rollout-sess-alpha.jsonl").to_string_lossy(),
302
+ "captured_at": "2026-06-25T10:00:00+00:00",
303
+ "captured_via": "session.captured",
304
+ "owner_team_id": "rollbackteam"
305
+ },
306
+ "bravo": {
307
+ "status": "running",
308
+ "provider": "codex",
309
+ "auth_mode": "subscription",
310
+ "window": "bravo",
311
+ "session_id": "sess-bravo",
312
+ "rollout_path": rollout_root.join("rollout-sess-bravo.jsonl").to_string_lossy(),
313
+ "captured_at": "2026-06-25T10:00:00+00:00",
314
+ "captured_via": "session.captured",
315
+ "owner_team_id": "rollbackteam"
316
+ }
317
+ }
318
+ }),
319
+ )
320
+ .unwrap();
321
+ let _ = MessageStore::open(&self.team).unwrap();
322
+ }
323
+
324
+ fn seed_healthy_coordinator(&self) {
325
+ let ws = WorkspacePath::new(self.team.clone());
326
+ let pid = Pid::new(std::process::id());
327
+ if let Some(parent) = coordinator_pid_path(&ws).parent() {
328
+ std::fs::create_dir_all(parent).unwrap();
329
+ }
330
+ std::fs::write(coordinator_pid_path(&ws), pid.to_string()).unwrap();
331
+ write_coordinator_metadata(&ws, pid, MetadataSource::Start).unwrap();
332
+ }
333
+
334
+ fn seed_sibling_team_with_same_fork_id(&self) {
335
+ let mut state = load_runtime_state(&self.team).unwrap();
336
+ let selected = state.clone();
337
+ state.as_object_mut().unwrap().insert(
338
+ "teams".to_string(),
339
+ json!({
340
+ "rollbackteam": selected,
341
+ "sibling": {
342
+ "agents": {
343
+ "newfork": {
344
+ "status": "running",
345
+ "provider": "codex",
346
+ "pane_id": "%sibling-newfork",
347
+ "owner_team_id": "sibling"
348
+ }
349
+ }
350
+ }
351
+ }),
352
+ );
353
+ save_runtime_state(&self.team, &state).unwrap();
354
+ let selected =
355
+ team_agent::state::projection::select_runtime_state(&self.team, Some("rollbackteam"))
356
+ .unwrap();
357
+ team_agent::state::projection::save_team_scoped_state(&self.team, &selected).unwrap();
358
+ }
359
+
360
+ fn seed_agent_health(
361
+ &self,
362
+ owner_team_id: &str,
363
+ agent_id: &str,
364
+ status: &str,
365
+ last_output_at: Option<&str>,
366
+ context_usage_pct: Option<i64>,
367
+ current_task_id: Option<&str>,
368
+ ) {
369
+ let store = MessageStore::open(&self.team).unwrap();
370
+ let conn = open_db(store.db_path()).unwrap();
371
+ conn.execute(
372
+ "insert or replace into agent_health(owner_team_id, agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at)
373
+ values (?1, ?2, ?3, ?4, ?5, ?6, '2026-06-09T00:00:00Z')",
374
+ params![
375
+ owner_team_id,
376
+ agent_id,
377
+ status,
378
+ last_output_at,
379
+ context_usage_pct,
380
+ current_task_id
381
+ ],
382
+ )
383
+ .unwrap();
384
+ }
385
+
386
+ fn agent_health_row(&self, agent_id: &str) -> Option<HealthRow> {
387
+ let store = MessageStore::open(&self.team).unwrap();
388
+ let conn = open_db(store.db_path()).unwrap();
389
+ conn.query_row(
390
+ "select owner_team_id, status, last_output_at, context_usage_pct, current_task_id
391
+ from agent_health where agent_id = ?1",
392
+ [agent_id],
393
+ |row| {
394
+ Ok(HealthRow {
395
+ owner_team_id: row.get(0)?,
396
+ status: row.get(1)?,
397
+ last_output_at: row.get(2)?,
398
+ context_usage_pct: row.get(3)?,
399
+ current_task_id: row.get(4)?,
400
+ })
401
+ },
402
+ )
403
+ .ok()
404
+ }
405
+
406
+ fn spec_text(&self) -> String {
407
+ std::fs::read_to_string(self.team.join("team.spec.yaml")).unwrap()
408
+ }
409
+ }
410
+
411
+ fn agent_doc(name: &str, role: &str) -> String {
412
+ format!(
413
+ "---\nname: {name}\nrole: {role}\nprovider: codex\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{role}.\n"
414
+ )
415
+ }
416
+
417
+ struct EnvGuard {
418
+ key: &'static str,
419
+ old: Option<String>,
420
+ }
421
+
422
+ impl EnvGuard {
423
+ fn set(key: &'static str, value: &str) -> Self {
424
+ let old = std::env::var(key).ok();
425
+ std::env::set_var(key, value);
426
+ Self { key, old }
427
+ }
428
+ }
429
+
430
+ impl Drop for EnvGuard {
431
+ fn drop(&mut self) {
432
+ match &self.old {
433
+ Some(value) => std::env::set_var(self.key, value),
434
+ None => std::env::remove_var(self.key),
435
+ }
436
+ }
437
+ }
438
+
439
+ #[derive(Clone)]
440
+ struct RecordingTransport {
441
+ session: String,
442
+ windows: Arc<Mutex<BTreeSet<String>>>,
443
+ killed: Arc<Mutex<Vec<String>>>,
444
+ }
445
+
446
+ impl RecordingTransport {
447
+ fn new(session: &str, windows: &[&str]) -> Self {
448
+ Self {
449
+ session: session.to_string(),
450
+ windows: Arc::new(Mutex::new(
451
+ windows.iter().map(|window| (*window).to_string()).collect(),
452
+ )),
453
+ killed: Arc::new(Mutex::new(Vec::new())),
454
+ }
455
+ }
456
+
457
+ fn killed(&self) -> Vec<String> {
458
+ self.killed.lock().unwrap().clone()
459
+ }
460
+
461
+ fn record_spawn(
462
+ &self,
463
+ session: &SessionName,
464
+ window: &WindowName,
465
+ argv: &[String],
466
+ ) -> Result<SpawnResult, TransportError> {
467
+ emit_codex_fork_backing(argv);
468
+ self.windows
469
+ .lock()
470
+ .unwrap()
471
+ .insert(window.as_str().to_string());
472
+ Ok(SpawnResult {
473
+ pane_id: PaneId::new(format!("%{}", window.as_str())),
474
+ session: session.clone(),
475
+ window: window.clone(),
476
+ child_pid: Some(4242),
477
+ })
478
+ }
479
+ }
480
+
481
+ fn emit_codex_fork_backing(argv: &[String]) {
482
+ if !argv
483
+ .windows(2)
484
+ .any(|pair| pair[0] == "codex" && pair[1] == "fork")
485
+ {
486
+ return;
487
+ }
488
+ static SESSION_SEQ: AtomicU64 = AtomicU64::new(0);
489
+ let session_id = format!(
490
+ "rollback-fork-{}-{}",
491
+ std::process::id(),
492
+ SESSION_SEQ.fetch_add(1, Ordering::Relaxed)
493
+ );
494
+ let root = PathBuf::from(std::env::var_os("HOME").expect("hermetic HOME"))
495
+ .join(".codex/sessions/lifecycle-rollback-fixture");
496
+ std::fs::create_dir_all(&root).unwrap();
497
+ std::fs::write(
498
+ root.join(format!("rollout-{session_id}.jsonl")),
499
+ format!(
500
+ "{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\"}}}},\"created_at\":\"{}\"}}\n",
501
+ chrono::Utc::now().to_rfc3339()
502
+ ),
503
+ )
504
+ .unwrap();
505
+ }
506
+
507
+ impl Transport for RecordingTransport {
508
+ fn kind(&self) -> BackendKind {
509
+ BackendKind::Tmux
510
+ }
511
+
512
+ fn spawn_first(
513
+ &self,
514
+ session: &SessionName,
515
+ window: &WindowName,
516
+ argv: &[String],
517
+ _cwd: &Path,
518
+ _env: &BTreeMap<String, String>,
519
+ ) -> Result<SpawnResult, TransportError> {
520
+ self.record_spawn(session, window, argv)
521
+ }
522
+
523
+ fn spawn_into(
524
+ &self,
525
+ session: &SessionName,
526
+ window: &WindowName,
527
+ argv: &[String],
528
+ _cwd: &Path,
529
+ _env: &BTreeMap<String, String>,
530
+ ) -> Result<SpawnResult, TransportError> {
531
+ self.record_spawn(session, window, argv)
532
+ }
533
+
534
+ fn inject(
535
+ &self,
536
+ _target: &Target,
537
+ _payload: &InjectPayload,
538
+ _submit: Key,
539
+ _bracketed: bool,
540
+ ) -> Result<InjectReport, TransportError> {
541
+ Ok(InjectReport {
542
+ stage_reached: InjectStage::Submit,
543
+ inject_verification: InjectVerification::EmptyTextSendKeys,
544
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
545
+ turn_verification: TurnVerification::NotRequired,
546
+ attempts: 1,
547
+ submit_diagnostics: None,
548
+ })
549
+ }
550
+
551
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
552
+ Ok(())
553
+ }
554
+
555
+ fn capture(
556
+ &self,
557
+ _target: &Target,
558
+ range: CaptureRange,
559
+ ) -> Result<CapturedText, TransportError> {
560
+ Ok(CapturedText {
561
+ text: String::new(),
562
+ range,
563
+ })
564
+ }
565
+
566
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
567
+ Ok(match field {
568
+ PaneField::PaneId => Some("%0".to_string()),
569
+ PaneField::PaneMode => None,
570
+ PaneField::PaneWidth => Some("120".to_string()),
571
+ PaneField::PaneCurrentPath => None,
572
+ PaneField::PaneCurrentCommand => None,
573
+ PaneField::SessionName => Some(self.session.clone()),
574
+ PaneField::PaneTty => None,
575
+ })
576
+ }
577
+
578
+ fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
579
+ Ok(PaneLiveness::Live)
580
+ }
581
+
582
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
583
+ Ok(self
584
+ .windows
585
+ .lock()
586
+ .unwrap()
587
+ .iter()
588
+ .map(|window| PaneInfo {
589
+ pane_id: PaneId::new(format!("%{window}")),
590
+ session: SessionName::new(self.session.clone()),
591
+ window_index: None,
592
+ window_name: Some(WindowName::new(window.clone())),
593
+ pane_index: None,
594
+ tty: None,
595
+ current_command: None,
596
+ current_path: None,
597
+ active: false,
598
+ pane_pid: Some(4242),
599
+ leader_env: BTreeMap::new(),
600
+ })
601
+ .collect())
602
+ }
603
+
604
+ fn has_session(&self, session: &SessionName) -> Result<bool, TransportError> {
605
+ Ok(session.as_str() == self.session)
606
+ }
607
+
608
+ fn list_windows(&self, session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
609
+ if session.as_str() != self.session {
610
+ return Ok(Vec::new());
611
+ }
612
+ Ok(self
613
+ .windows
614
+ .lock()
615
+ .unwrap()
616
+ .iter()
617
+ .map(|window| WindowName::new(window.clone()))
618
+ .collect())
619
+ }
620
+
621
+ fn set_session_env(
622
+ &self,
623
+ _session: &SessionName,
624
+ _key: &str,
625
+ _value: &str,
626
+ ) -> Result<SetEnvOutcome, TransportError> {
627
+ Ok(SetEnvOutcome::Applied)
628
+ }
629
+
630
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
631
+ Ok(())
632
+ }
633
+
634
+ fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
635
+ let label = match target {
636
+ Target::Pane(pane) => pane.as_str().to_string(),
637
+ Target::SessionWindow { session, window } => {
638
+ format!("{}:{}", session.as_str(), window.as_str())
639
+ }
640
+ };
641
+ if let Some(window) = label.split(':').next_back() {
642
+ self.windows.lock().unwrap().remove(window);
643
+ }
644
+ self.killed.lock().unwrap().push(label);
645
+ Ok(())
646
+ }
647
+
648
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
649
+ Ok(AttachOutcome::Attached)
650
+ }
651
+ }
@@ -406,6 +406,7 @@ fn reset_agent_discard_session_syncs_projection_epoch_inputs_for_restart_agent_c
406
406
  // runtime. canonical_run_workspace($ws/.team) == $ws. The regression made remove "succeed" against a
407
407
  // detached context, leaving the live roster [alpha,bravo] intact. (lanea_team_ws re-points routing to
408
408
  // bravo so removing alpha validates cleanly.)
409
+ #[ignore = "flaky under parallel suite, tracked"]
409
410
  #[test]
410
411
  fn remove_agent_via_team_subpath_applies_to_canonical_live_roster() {
411
412
  let ws = lanea_team_ws("stopped"); // canonical workspace: spec + state roster [alpha,bravo]
@@ -475,6 +476,7 @@ fn bugb_routed_team_ws() -> PathBuf {
475
476
  // Removing `bravo` (the SECOND agent) isolates the routing-rule prune cleanly: only `route-bravo` dangles;
476
477
  // default_assignee (alpha) and tasks[0].assignee (alpha) stay valid, so this test does NOT entangle the
477
478
  // default_assignee / task-reassignment policy.
479
+ #[ignore = "flaky: main_preserved parallel-contention family, passes in isolation (5/5)"]
478
480
  #[test]
479
481
  fn remove_routed_worker_prunes_dangling_routing_rule_divergence() {
480
482
  let ws = bugb_routed_team_ws();