@team-agent/installer 0.5.61 → 0.5.62

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 +6 -5
  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 +0 -9
  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 -42
  49. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  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 -3
  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 -11
  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 -0
  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,544 @@
1
+ //! RED contract (REVISED, inverted): a successful leader claim — including the
2
+ //! `already_bound` branch — requeues only rows that never crossed the
3
+ //! transport; it must NEVER flush `submitted_pending_acceptance` rows.
4
+ //!
5
+ //! REVISION LINEAGE (verifier signature, leader ruling msg_2bc80666aef8):
6
+ //! - 5027d533…6157 (original freeze at 1b2848a): asserted the OPPOSITE — that
7
+ //! an `already_bound` claim flushes a `submitted_pending_acceptance` row.
8
+ //! That premise was wrong at scale (leader co-signed the same error): those
9
+ //! rows are the DESIGNED PARKED STATE of messages that already crossed the
10
+ //! transport and merely lack a provider receipt. Shipping that semantics
11
+ //! (d3a4966, 0.5.55) caused the P0 replay storm of 2026-07-23: one
12
+ //! attach-triggered claim flipped 346 historical parked rows back to
13
+ //! `accepted` and physically re-injected all of them (attempts 1→2),
14
+ //! O(history) with no time/attempt bound (storm-p0-locate.md).
15
+ //! - THIS revision (new SHA): inverts the tooth (`already_bound` never
16
+ //! flushes parked rows), adds the storm tooth (>=3 historical parked rows,
17
+ //! zero flips on claim — the stored-data shape a fresh-environment gate
18
+ //! misses), keeps the positive control (a newly claimed leader still
19
+ //! flushes `queued_until_leader_attach`) and the non-leader negative
20
+ //! control.
21
+ //!
22
+ //! DOCUMENTED RESIDUAL (explicit, not silent): after the 0.5.56 revert, a row
23
+ //! whose physical submit was genuinely LOST (submitted, no receipt, transport
24
+ //! evaporated) parks forever again — the original stranding this contract's
25
+ //! V1 tried to fix. That recovery belongs to the blocked-before-submit /
26
+ //! submitted-awaiting-receipt state-machine separation car (0.5.57+): a typed
27
+ //! recovery arm with attempt budget and age/owner-epoch bounds, never a
28
+ //! blanket claim-time requeue.
29
+ //!
30
+ //! Requirements: N31 (leader-bound delivery cannot remain stuck) applies to
31
+ //! rows that never crossed the transport; N32 (single delivery/requeue
32
+ //! funnel) is unchanged.
33
+
34
+ #![cfg(unix)]
35
+ #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
36
+
37
+ use crate as team_agent;
38
+
39
+ use std::path::PathBuf;
40
+ use std::sync::atomic::{AtomicU64, Ordering};
41
+
42
+ use rusqlite::params;
43
+ use serial_test::serial;
44
+ use team_agent::event_log::EventLog;
45
+ use team_agent::leader::{claim_lease_no_incident, LeaseStatus};
46
+ use team_agent::message_store::MessageStore;
47
+ use team_agent::messaging::enqueue_leader_mailbox_until_attach;
48
+ use team_agent::messaging::watchers::requeue_after_claim_leader;
49
+ use team_agent::model::enums::PaneLiveness;
50
+ use team_agent::model::ids::TeamKey;
51
+ use team_agent::state::owner_gate::PaneLivenessProbe;
52
+ use team_agent::transport::PaneId;
53
+
54
+ const TEAM: &str = "current";
55
+ const PANE: &str = "%leader";
56
+
57
+ struct LiveCaller;
58
+
59
+ impl PaneLivenessProbe for LiveCaller {
60
+ fn liveness(&self, pane_id: &str) -> PaneLiveness {
61
+ if pane_id == PANE {
62
+ PaneLiveness::Live
63
+ } else {
64
+ PaneLiveness::Dead
65
+ }
66
+ }
67
+ }
68
+
69
+ /// INVERTED RED TOOTH — a `submitted_pending_acceptance` row already crossed
70
+ /// the transport; it is the designed parked state awaiting a provider
71
+ /// receipt. A claim (any branch, `already_bound` included) must leave it
72
+ /// COMPLETELY untouched: status, attempts, error, delivered_at, updated_at
73
+ /// and its delivery token row. Baseline 6a4b7a5 red: the claim flips it to
74
+ /// `accepted` and the coordinator physically re-injects (the 2026-07-23
75
+ /// storm, 346 rows, attempts 1->2).
76
+ #[test]
77
+ #[serial(env)]
78
+ fn already_bound_claim_never_flushes_submitted_pending_acceptance_row() {
79
+ let case = Case::new("parked-untouched");
80
+ let message_id = case.seed_incident_row("incident canary");
81
+ case.seed_delivery_token(&message_id, "TOKEN-PARKED-1");
82
+ let before_row = case.full_row(&message_id);
83
+ let before_token = case.token_row(&message_id);
84
+ let mut state = bound_state();
85
+
86
+ let status = case.claim_and_run_success_hook(&mut state);
87
+
88
+ assert_eq!(
89
+ status,
90
+ LeaseStatus::AlreadyBound,
91
+ "fixture must exercise the already_bound enum branch"
92
+ );
93
+ assert_eq!(
94
+ case.full_row(&message_id),
95
+ before_row,
96
+ "a parked (submitted_pending_acceptance) row must be byte-stable across a claim: \
97
+ status/attempts/error/delivered_at/updated_at all unchanged. Flipping it to accepted \
98
+ re-injects a message that already crossed the transport (storm-p0-locate.md B2)"
99
+ );
100
+ assert_eq!(
101
+ case.token_row(&message_id),
102
+ before_token,
103
+ "the delivery token row of a parked message must survive a claim unchanged \
104
+ (stable receipt identity)"
105
+ );
106
+ }
107
+
108
+ /// STORM TOOTH — the stored-data face a fresh-environment gate misses:
109
+ /// three historical parked rows (created on earlier days) + one truly
110
+ /// blocked row + one delivered row. A claim may requeue ONLY the blocked
111
+ /// row; parked and delivered rows see zero flips, zero attempt growth, and
112
+ /// the requeue audit event reports exactly the blocked count (per-status
113
+ /// classification, locate §D3). Baseline red: eligible SQL flips all
114
+ /// parked rows too (O(history), no time/attempt bound).
115
+ #[test]
116
+ #[serial(env)]
117
+ fn claim_with_historical_backlog_requeues_only_truly_blocked_rows() {
118
+ let case = Case::new("storm-backlog");
119
+ let parked: Vec<String> = (0..3)
120
+ .map(|i| {
121
+ let id = case.seed_incident_row(&format!("parked history {i}"));
122
+ case.backdate_created_at(&id, &format!("2026-07-2{}T0{}:00:00Z", 1 + i % 2, i));
123
+ id
124
+ })
125
+ .collect();
126
+ let blocked = case.seed_blocked_row("truly blocked canary");
127
+ let delivered = case.seed_delivered_row("delivered history canary");
128
+ let parked_before: Vec<FullRow> = parked.iter().map(|id| case.full_row(id)).collect();
129
+ let delivered_before = case.full_row(&delivered);
130
+ let mut state = bound_state();
131
+
132
+ let status = case.claim_and_run_success_hook(&mut state);
133
+ assert_eq!(status, LeaseStatus::AlreadyBound);
134
+
135
+ let parked_after: Vec<FullRow> = parked.iter().map(|id| case.full_row(id)).collect();
136
+ assert_eq!(
137
+ parked_after, parked_before,
138
+ "N parked rows must be fully unchanged by a claim — zero flips, zero attempts, \
139
+ zero updated_at churn (the 346-row replay was exactly this set)"
140
+ );
141
+ assert_eq!(
142
+ case.full_row(&delivered).row,
143
+ delivered_before.row,
144
+ "K delivered rows must never re-enter the delivery funnel via a claim"
145
+ );
146
+ assert_eq!(
147
+ case.full_row(&blocked).row,
148
+ Row::accepted(0),
149
+ "M truly blocked rows (failed/leader_not_attached) must still be requeued — \
150
+ the residual N31 face this contract keeps alive"
151
+ );
152
+ assert_eq!(
153
+ case.last_blocked_requeue_count(),
154
+ Some(1),
155
+ "the requeue audit event must report exactly the truly-blocked count (M=1), \
156
+ not parked+blocked — per-original-status classification (locate §D3)"
157
+ );
158
+ }
159
+
160
+ /// WATCHER AMPLIFICATION TOOTH — historical watchers with `result_id=NULL`
161
+ /// can never be retried (the retry arm skips them), so rewriting them to
162
+ /// `notify_failed` + one audit event each on every claim is pure O(history)
163
+ /// write amplification (the 18:22 window: 108 rewrites before the process
164
+ /// was killed). A claim must leave null-result watchers untouched and not
165
+ /// return them as notices. Baseline red: the claim sweep selects every
166
+ /// `notified_message_id is null` watcher unbounded (incident_ts=None).
167
+ #[test]
168
+ #[serial(env)]
169
+ fn claim_does_not_rewrite_null_result_historical_watchers() {
170
+ let case = Case::new("watcher-amp");
171
+ let watchers: Vec<String> = (0..3)
172
+ .map(|i| case.seed_null_result_watcher(i, "2026-07-21T00:00:00Z"))
173
+ .collect();
174
+ let before: Vec<(String, Option<String>)> =
175
+ watchers.iter().map(|id| case.watcher_row(id)).collect();
176
+ let mut state = bound_state();
177
+
178
+ let notices = case.claim_and_collect_notices(&mut state);
179
+
180
+ let after: Vec<(String, Option<String>)> =
181
+ watchers.iter().map(|id| case.watcher_row(id)).collect();
182
+ assert_eq!(
183
+ after, before,
184
+ "null-result historical watchers must not be rewritten by a claim: they are \
185
+ unretryable (retry skips result_id=NULL), so per-claim rewrites are unbounded \
186
+ write amplification (storm-p0-locate.md §4)"
187
+ );
188
+ assert!(
189
+ notices
190
+ .iter()
191
+ .all(|notice| !watchers.contains(&notice.watcher_id)),
192
+ "null-result watchers must not surface as claim notices; notices={notices:?}"
193
+ );
194
+ }
195
+
196
+ #[test]
197
+ #[serial(env)]
198
+ fn newly_claimed_path_still_flushes_queued_leader_mailbox_row() {
199
+ let case = Case::new("new-binding");
200
+ let message_id = case.seed_queued_row("positive-control canary");
201
+ let mut state = serde_json::json!({"session_name": "team-mailbox-positive"});
202
+
203
+ let status = case.claim_and_run_success_hook(&mut state);
204
+
205
+ assert_eq!(
206
+ status,
207
+ LeaseStatus::Claimed,
208
+ "positive control must exercise the newly-bound enum branch"
209
+ );
210
+ assert_eq!(
211
+ case.row(&message_id),
212
+ Row::accepted(0),
213
+ "existing new-binding mailbox flush must remain green"
214
+ );
215
+ }
216
+
217
+ #[test]
218
+ #[serial(env)]
219
+ fn claim_does_not_flush_non_leader_submitted_row() {
220
+ let case = Case::new("negative-control");
221
+ let message_id = case.seed_incident_row("negative-control canary");
222
+ case.set_recipient(&message_id, "worker-a");
223
+ let mut state = bound_state();
224
+
225
+ let status = case.claim_and_run_success_hook(&mut state);
226
+
227
+ assert_eq!(status, LeaseStatus::AlreadyBound);
228
+ assert_eq!(
229
+ case.row(&message_id),
230
+ Row::submitted(1),
231
+ "negative control: claim flush is scoped to recipient=leader"
232
+ );
233
+ }
234
+
235
+ struct Case {
236
+ workspace: PathBuf,
237
+ store: MessageStore,
238
+ event_log: EventLog,
239
+ }
240
+
241
+ impl Case {
242
+ fn new(tag: &str) -> Self {
243
+ static NEXT: AtomicU64 = AtomicU64::new(0);
244
+ let workspace = std::env::temp_dir().join(format!(
245
+ "ta-claim-mailbox-flush-{tag}-{}-{}",
246
+ std::process::id(),
247
+ NEXT.fetch_add(1, Ordering::Relaxed)
248
+ ));
249
+ std::fs::create_dir_all(&workspace).unwrap();
250
+ let store = MessageStore::open(&workspace).unwrap();
251
+ let event_log = EventLog::new(&workspace);
252
+ Self {
253
+ workspace,
254
+ store,
255
+ event_log,
256
+ }
257
+ }
258
+
259
+ fn seed_queued_row(&self, content: &str) -> String {
260
+ enqueue_leader_mailbox_until_attach(
261
+ &self.workspace,
262
+ TEAM,
263
+ content,
264
+ None,
265
+ "leader",
266
+ &self.event_log,
267
+ )
268
+ .unwrap()
269
+ .message_id
270
+ .expect("mailbox enqueue returns message id")
271
+ }
272
+
273
+ fn seed_incident_row(&self, content: &str) -> String {
274
+ let message_id = self.seed_queued_row(content);
275
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
276
+ conn.execute(
277
+ "update messages
278
+ set status = 'submitted_pending_acceptance',
279
+ delivered_at = null,
280
+ delivery_attempts = 1,
281
+ error = null
282
+ where message_id = ?1",
283
+ params![message_id],
284
+ )
285
+ .unwrap();
286
+ message_id
287
+ }
288
+
289
+ fn seed_blocked_row(&self, content: &str) -> String {
290
+ let message_id = self.seed_queued_row(content);
291
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
292
+ conn.execute(
293
+ "update messages
294
+ set status = 'failed', error = 'leader_not_attached', delivery_attempts = 0
295
+ where message_id = ?1",
296
+ params![message_id],
297
+ )
298
+ .unwrap();
299
+ message_id
300
+ }
301
+
302
+ fn seed_delivered_row(&self, content: &str) -> String {
303
+ let message_id = self.seed_queued_row(content);
304
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
305
+ conn.execute(
306
+ "update messages
307
+ set status = 'delivered', delivered_at = '2026-07-21T12:00:00Z',
308
+ delivery_attempts = 1, error = null
309
+ where message_id = ?1",
310
+ params![message_id],
311
+ )
312
+ .unwrap();
313
+ message_id
314
+ }
315
+
316
+ fn backdate_created_at(&self, message_id: &str, created_at: &str) {
317
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
318
+ conn.execute(
319
+ "update messages set created_at = ?2, updated_at = ?2 where message_id = ?1",
320
+ params![message_id, created_at],
321
+ )
322
+ .unwrap();
323
+ }
324
+
325
+ fn seed_delivery_token(&self, message_id: &str, token: &str) {
326
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
327
+ conn.execute(
328
+ "insert into delivery_tokens (message_id, unique_token, injected_at)
329
+ values (?1, ?2, '2026-07-21T12:00:00Z')",
330
+ params![message_id, token],
331
+ )
332
+ .unwrap();
333
+ }
334
+
335
+ fn token_row(&self, message_id: &str) -> Option<(String, String)> {
336
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
337
+ use rusqlite::OptionalExtension;
338
+ conn.query_row(
339
+ "select unique_token, injected_at from delivery_tokens where message_id = ?1",
340
+ params![message_id],
341
+ |row| Ok((row.get(0)?, row.get(1)?)),
342
+ )
343
+ .optional()
344
+ .unwrap()
345
+ }
346
+
347
+ fn seed_null_result_watcher(&self, index: usize, created_at: &str) -> String {
348
+ let watcher_id = format!("watch-null-{index}");
349
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
350
+ conn.execute(
351
+ "insert into result_watchers
352
+ (watcher_id, owner_team_id, leader_id, status, created_at, completed_at,
353
+ result_id, notified_message_id)
354
+ values (?1, ?2, 'leader', 'notify_failed', ?3, ?3, null, null)",
355
+ params![watcher_id, TEAM, created_at],
356
+ )
357
+ .unwrap();
358
+ watcher_id
359
+ }
360
+
361
+ fn watcher_row(&self, watcher_id: &str) -> (String, Option<String>) {
362
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
363
+ conn.query_row(
364
+ "select status, completed_at from result_watchers where watcher_id = ?1",
365
+ params![watcher_id],
366
+ |row| Ok((row.get(0)?, row.get(1)?)),
367
+ )
368
+ .unwrap()
369
+ }
370
+
371
+ fn last_blocked_requeue_count(&self) -> Option<i64> {
372
+ let path = self
373
+ .workspace
374
+ .join(".team")
375
+ .join("logs")
376
+ .join("events.jsonl");
377
+ let text = std::fs::read_to_string(path).ok()?;
378
+ text.lines()
379
+ .rev()
380
+ .filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
381
+ .find(|event| {
382
+ event.get("event").and_then(serde_json::Value::as_str)
383
+ == Some("leader_receiver.blocked_messages_requeued")
384
+ })
385
+ .and_then(|event| event.get("count").and_then(serde_json::Value::as_i64))
386
+ }
387
+
388
+ fn set_recipient(&self, message_id: &str, recipient: &str) {
389
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
390
+ conn.execute(
391
+ "update messages set recipient = ?2 where message_id = ?1",
392
+ params![message_id, recipient],
393
+ )
394
+ .unwrap();
395
+ }
396
+
397
+ fn claim_and_run_success_hook(&self, state: &mut serde_json::Value) -> LeaseStatus {
398
+ let (status, _) = self.claim_inner(state);
399
+ status
400
+ }
401
+
402
+ fn claim_and_collect_notices(
403
+ &self,
404
+ state: &mut serde_json::Value,
405
+ ) -> Vec<team_agent::messaging::WatcherNotice> {
406
+ let (_, notices) = self.claim_inner(state);
407
+ notices
408
+ }
409
+
410
+ fn claim_inner(
411
+ &self,
412
+ state: &mut serde_json::Value,
413
+ ) -> (LeaseStatus, Vec<team_agent::messaging::WatcherNotice>) {
414
+ let team = TeamKey::new(TEAM);
415
+ let pane = PaneId::new(PANE);
416
+ let result = claim_lease_no_incident(
417
+ &self.workspace,
418
+ state,
419
+ None,
420
+ &team,
421
+ &pane,
422
+ true,
423
+ &self.event_log,
424
+ &LiveCaller,
425
+ )
426
+ .unwrap();
427
+ let mut notices = Vec::new();
428
+ if result.ok {
429
+ notices = requeue_after_claim_leader(
430
+ &self.workspace,
431
+ &self.store,
432
+ &self.event_log,
433
+ &team,
434
+ result
435
+ .bound_pane_id
436
+ .as_ref()
437
+ .expect("successful claim has pane"),
438
+ None,
439
+ )
440
+ .unwrap();
441
+ }
442
+ (result.status, notices)
443
+ }
444
+
445
+ /// Row plus updated_at — the storm/inverted teeth require byte-stability
446
+ /// of the WHOLE observable row, not just the status.
447
+ fn full_row(&self, message_id: &str) -> FullRow {
448
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
449
+ conn.query_row(
450
+ "select status, delivered_at, delivery_attempts, error, updated_at
451
+ from messages where message_id = ?1",
452
+ params![message_id],
453
+ |row| {
454
+ Ok(FullRow {
455
+ row: Row {
456
+ status: row.get(0)?,
457
+ delivered_at: row.get(1)?,
458
+ attempts: row.get(2)?,
459
+ error: row.get(3)?,
460
+ },
461
+ updated_at: row.get(4)?,
462
+ })
463
+ },
464
+ )
465
+ .unwrap()
466
+ }
467
+
468
+ fn row(&self, message_id: &str) -> Row {
469
+ let conn = team_agent::db::schema::open_db(self.store.db_path()).unwrap();
470
+ conn.query_row(
471
+ "select status, delivered_at, delivery_attempts, error
472
+ from messages where message_id = ?1",
473
+ params![message_id],
474
+ |row| {
475
+ Ok(Row {
476
+ status: row.get(0)?,
477
+ delivered_at: row.get(1)?,
478
+ attempts: row.get(2)?,
479
+ error: row.get(3)?,
480
+ })
481
+ },
482
+ )
483
+ .unwrap()
484
+ }
485
+ }
486
+
487
+ impl Drop for Case {
488
+ fn drop(&mut self) {
489
+ let _ = std::fs::remove_dir_all(&self.workspace);
490
+ }
491
+ }
492
+
493
+ #[derive(Debug, PartialEq, Eq)]
494
+ struct FullRow {
495
+ row: Row,
496
+ updated_at: Option<String>,
497
+ }
498
+
499
+ #[derive(Debug, PartialEq, Eq)]
500
+ struct Row {
501
+ status: String,
502
+ delivered_at: Option<String>,
503
+ attempts: i64,
504
+ error: Option<String>,
505
+ }
506
+
507
+ impl Row {
508
+ fn accepted(attempts: i64) -> Self {
509
+ Self {
510
+ status: "accepted".to_string(),
511
+ delivered_at: None,
512
+ attempts,
513
+ error: None,
514
+ }
515
+ }
516
+
517
+ fn submitted(attempts: i64) -> Self {
518
+ Self {
519
+ status: "submitted_pending_acceptance".to_string(),
520
+ delivered_at: None,
521
+ attempts,
522
+ error: None,
523
+ }
524
+ }
525
+ }
526
+
527
+ fn bound_state() -> serde_json::Value {
528
+ serde_json::json!({
529
+ "session_name": "team-mailbox-already-bound",
530
+ "team_owner": {
531
+ "pane_id": PANE,
532
+ "provider": "codex",
533
+ "owner_epoch": 7,
534
+ "claimed_at": "2026-07-23T05:13:42Z",
535
+ "claimed_via": "claim-leader"
536
+ },
537
+ "leader_receiver": {
538
+ "pane_id": PANE,
539
+ "provider": "codex",
540
+ "owner_epoch": 7,
541
+ "status": "attached"
542
+ }
543
+ })
544
+ }