@team-agent/installer 0.5.65 → 0.5.68

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 (208) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +6 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/compile.rs +1 -1
  17. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  18. package/crates/team-agent/src/cli/tests/mod.rs +2 -2
  19. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  20. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  21. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  22. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  23. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  24. package/crates/team-agent/src/compiler/tests.rs +23 -20
  25. package/crates/team-agent/src/compiler.rs +84 -5
  26. package/crates/team-agent/src/conpty/backend.rs +16 -0
  27. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  28. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  29. package/crates/team-agent/src/coordinator/health.rs +173 -0
  30. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  31. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  32. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  33. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  34. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  35. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  36. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  37. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  38. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  39. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  40. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  41. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  42. package/crates/team-agent/src/coordinator/types.rs +49 -0
  43. package/crates/team-agent/src/db/message_store.rs +39 -5
  44. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  45. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  46. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  47. package/crates/team-agent/src/leader/registry.rs +65 -0
  48. package/crates/team-agent/src/leader/start.rs +1080 -58
  49. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  50. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  51. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  52. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
  53. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
  54. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
  55. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  56. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  57. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  58. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
  59. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  60. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  61. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  62. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  63. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  64. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  65. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  66. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  67. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  68. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  69. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  70. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  71. package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
  72. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
  73. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
  74. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  75. package/crates/team-agent/src/lifecycle/launch.rs +209 -43
  76. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  77. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  78. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  79. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  80. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  81. package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
  82. package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
  83. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  84. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  85. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
  86. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  87. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  88. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  89. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  90. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  91. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
  92. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  93. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  95. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  96. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
  97. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
  99. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
  100. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  102. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  103. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  104. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  105. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  106. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  107. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  108. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  109. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  110. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  111. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  112. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  113. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  114. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
  115. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
  116. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
  117. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  118. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
  119. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
  120. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  121. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  122. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  123. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
  124. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  125. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  126. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  127. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
  128. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  129. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  130. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  131. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  132. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  133. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  134. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  135. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
  136. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  137. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  138. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  139. package/crates/team-agent/src/lifecycle/tests.rs +23 -15
  140. package/crates/team-agent/src/lifecycle/types.rs +61 -2
  141. package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
  142. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  143. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  144. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  145. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  146. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  147. package/crates/team-agent/src/messaging/results.rs +4 -0
  148. package/crates/team-agent/src/messaging/send.rs +34 -0
  149. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  150. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  151. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  152. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  153. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  154. package/crates/team-agent/src/messaging/types.rs +4 -1
  155. package/crates/team-agent/src/model/enums.rs +14 -5
  156. package/crates/team-agent/src/model/permissions.rs +3 -0
  157. package/crates/team-agent/src/model/spec.rs +129 -6
  158. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  159. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  160. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  161. package/crates/team-agent/src/os_probe.rs +73 -2
  162. package/crates/team-agent/src/provider/adapter.rs +231 -4
  163. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  164. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  165. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  166. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  167. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  168. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  169. package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
  170. package/crates/team-agent/src/provider/classify.rs +4 -2
  171. package/crates/team-agent/src/provider/faults.rs +2 -1
  172. package/crates/team-agent/src/provider/mod.rs +13 -0
  173. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  174. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  175. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  176. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  177. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  178. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  179. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  180. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  181. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  182. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  183. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  184. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  185. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  186. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  187. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  188. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  189. package/crates/team-agent/src/provider/types.rs +4 -2
  190. package/crates/team-agent/src/provider/wire.rs +23 -1
  191. package/crates/team-agent/src/state/persist.rs +301 -8
  192. package/crates/team-agent/src/state/repository.rs +5 -0
  193. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  194. package/crates/team-agent/src/tmux_backend.rs +1083 -75
  195. package/crates/team-agent/src/transport/test_support.rs +20 -0
  196. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  197. package/crates/team-agent/src/transport.rs +265 -1
  198. package/npm/install.mjs +129 -73
  199. package/package.json +4 -4
  200. package/skills/team-agent/SKILL.md +33 -238
  201. package/skills/team-agent/command-coverage.json +31 -0
  202. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  203. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  204. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
  205. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  206. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
  207. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  208. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -295,6 +295,7 @@ pub(crate) fn fail_leader_delivery(
295
295
  reason: Some(reason),
296
296
  channel: Some("fallback_log".to_string()),
297
297
  ack_forced_off: false,
298
+ turn_verification: None,
298
299
  });
299
300
  };
300
301
  store.mark(&message_id, "failed", error)?;
@@ -312,6 +313,7 @@ pub(crate) fn fail_leader_delivery(
312
313
  reason: Some(reason),
313
314
  channel: Some("fallback_inbox".to_string()),
314
315
  ack_forced_off: false,
316
+ turn_verification: None,
315
317
  })
316
318
  }
317
319
 
@@ -134,6 +134,7 @@ pub fn send_to_leader_receiver_with_presentation(
134
134
  reason: Some(DeliveryRefusal::Duplicate),
135
135
  channel: Some("leader_receiver".to_string()),
136
136
  ack_forced_off: requires_ack,
137
+ turn_verification: None,
137
138
  });
138
139
  }
139
140
  PersistResolution::Persisted(persisted) => persisted.message_id,
@@ -176,6 +177,7 @@ pub fn send_to_leader_receiver_with_presentation(
176
177
  reason: None,
177
178
  channel: Some("leader_receiver".to_string()),
178
179
  ack_forced_off: requires_ack,
180
+ turn_verification: None,
179
181
  });
180
182
  }
181
183
  }
@@ -213,6 +215,7 @@ pub fn send_to_leader_receiver_with_presentation(
213
215
  reason: None,
214
216
  channel: Some("leader_receiver".to_string()),
215
217
  ack_forced_off: requires_ack,
218
+ turn_verification: None,
216
219
  })
217
220
  }
218
221
 
@@ -259,6 +262,7 @@ pub fn deliver_to_leader_fallback_pane(
259
262
  reason: None,
260
263
  channel: Some("leader_receiver".to_string()),
261
264
  ack_forced_off: false,
265
+ turn_verification: None,
262
266
  });
263
267
  }
264
268
 
@@ -274,6 +278,7 @@ pub fn deliver_to_leader_fallback_pane(
274
278
  reason: None,
275
279
  channel: Some("leader_receiver".to_string()),
276
280
  ack_forced_off: false,
281
+ turn_verification: None,
277
282
  });
278
283
  }
279
284
 
@@ -316,6 +321,7 @@ pub fn deliver_to_leader_fallback_pane(
316
321
  reason: Some(DeliveryRefusal::LeaderNotAttached),
317
322
  channel: Some("fallback_pane".to_string()),
318
323
  ack_forced_off: false,
324
+ turn_verification: None,
319
325
  });
320
326
  };
321
327
 
@@ -390,9 +396,40 @@ pub fn deliver_to_leader_fallback_pane(
390
396
  reason: Some(DeliveryRefusal::LeaderNotAttached),
391
397
  channel: Some("rebind_required".to_string()),
392
398
  ack_forced_off: false,
399
+ turn_verification: None,
393
400
  });
394
401
  };
395
402
  let target = Target::Pane(PaneId::new(channel.pane_id));
403
+ if super::delivery::token_already_visible(
404
+ transport.as_ref(),
405
+ state,
406
+ "leader",
407
+ &target,
408
+ message_id,
409
+ ) {
410
+ event_log.write(
411
+ "leader_receiver.duplicate_token_refused",
412
+ serde_json::json!({
413
+ "message_id": message_id,
414
+ "result_id": result_id,
415
+ "pane_id": pane_id,
416
+ "reason": "message_token_already_visible",
417
+ "delivered_via": "fallback_pane",
418
+ }),
419
+ )?;
420
+ return Ok(DeliveryOutcome {
421
+ ok: true,
422
+ status: DeliveryStatus::AlreadyDelivered,
423
+ message_status: MessageStatusShadow("delivered".to_string()),
424
+ message_id: Some(message_id.to_string()),
425
+ verification: Some("message_token_already_visible".to_string()),
426
+ stage: Some(DeliveryStage::Submit),
427
+ reason: None,
428
+ channel: Some("fallback_pane".to_string()),
429
+ ack_forced_off: false,
430
+ turn_verification: None,
431
+ });
432
+ }
396
433
  let inject_result: Result<InjectReport, TransportError> =
397
434
  transport.inject(&target, &payload, Key::Enter, true);
398
435
 
@@ -443,6 +480,7 @@ pub fn deliver_to_leader_fallback_pane(
443
480
  reason: None,
444
481
  channel: Some("leader_acceptance_pending".to_string()),
445
482
  ack_forced_off: false,
483
+ turn_verification: None,
446
484
  });
447
485
  }
448
486
  super::delivery::LeaderReceiptObservation::TokenObserved => {}
@@ -470,6 +508,7 @@ pub fn deliver_to_leader_fallback_pane(
470
508
  reason: None,
471
509
  channel: Some("fallback_pane".to_string()),
472
510
  ack_forced_off: false,
511
+ turn_verification: None,
473
512
  })
474
513
  } else {
475
514
  let reason = format!(
@@ -496,6 +535,7 @@ pub fn deliver_to_leader_fallback_pane(
496
535
  reason: None,
497
536
  channel: Some("fallback_pane".to_string()),
498
537
  ack_forced_off: false,
538
+ turn_verification: None,
499
539
  })
500
540
  }
501
541
  }
@@ -538,6 +578,7 @@ pub fn deliver_to_leader_fallback_pane(
538
578
  reason: Some(DeliveryRefusal::TmuxTargetMissing),
539
579
  channel: Some(channel),
540
580
  ack_forced_off: false,
581
+ turn_verification: None,
541
582
  })
542
583
  }
543
584
  }
@@ -656,7 +697,17 @@ fn message_already_delivered(
656
697
  .optional()?;
657
698
  Ok(matches!(
658
699
  status.as_deref(),
659
- Some("delivered" | "acknowledged" | "submitted" | "submitted_unverified")
700
+ Some(
701
+ "delivered"
702
+ | "acknowledged"
703
+ | "submitted"
704
+ | "submitted_unverified"
705
+ | "submitted_pending_acceptance"
706
+ | "injected_awaiting_receipt"
707
+ | "visible"
708
+ | "injected"
709
+ | "consumed"
710
+ )
660
711
  ))
661
712
  }
662
713
 
@@ -730,5 +781,6 @@ pub fn enqueue_leader_mailbox_until_attach(
730
781
  reason: None,
731
782
  channel: Some("leader_mailbox".to_string()),
732
783
  ack_forced_off: false,
784
+ turn_verification: None,
733
785
  })
734
786
  }
@@ -940,6 +940,7 @@ fn report_result_for_owner_team_inner(
940
940
  reason: Some(crate::messaging::DeliveryRefusal::LeaderNotAttached),
941
941
  channel: Some("codex_app_server".to_string()),
942
942
  ack_forced_off: false,
943
+ turn_verification: None,
943
944
  }
944
945
  }
945
946
  Err(error) => {
@@ -1005,6 +1006,7 @@ fn report_result_for_owner_team_inner(
1005
1006
  reason: None,
1006
1007
  channel: Some("leader_receiver".to_string()),
1007
1008
  ack_forced_off: false,
1009
+ turn_verification: None,
1008
1010
  };
1009
1011
  break;
1010
1012
  }
@@ -1035,6 +1037,7 @@ fn report_result_for_owner_team_inner(
1035
1037
  reason: None,
1036
1038
  channel: Some("leader_receiver".to_string()),
1037
1039
  ack_forced_off: false,
1040
+ turn_verification: None,
1038
1041
  };
1039
1042
  break;
1040
1043
  }
@@ -1051,6 +1054,7 @@ fn report_result_for_owner_team_inner(
1051
1054
  reason: None,
1052
1055
  channel: Some("leader_receiver".to_string()),
1053
1056
  ack_forced_off: false,
1057
+ turn_verification: None,
1054
1058
  };
1055
1059
  break;
1056
1060
  }
@@ -215,6 +215,7 @@ pub fn send_message(
215
215
  reason: Some(crate::messaging::DeliveryRefusal::EmptyTargetList),
216
216
  channel: None,
217
217
  ack_forced_off: false,
218
+ turn_verification: None,
218
219
  });
219
220
  }
220
221
  MessageTarget::Fanout(recipients) => {
@@ -316,6 +317,7 @@ pub fn send_message(
316
317
  None,
317
318
  );
318
319
  }
320
+ capture_missing_sessions_after_send(workspace);
319
321
  if let Some(CoordinatorUnavailableGate {
320
322
  mut outcome,
321
323
  persist: true,
@@ -353,9 +355,34 @@ pub fn send_message(
353
355
  reason: None,
354
356
  channel: None,
355
357
  ack_forced_off: false,
358
+ turn_verification: None,
356
359
  })
357
360
  }
358
361
 
362
+ fn capture_missing_sessions_after_send(workspace: &Path) {
363
+ let Ok(mut state) = crate::state::persist::load_runtime_state(workspace) else {
364
+ return;
365
+ };
366
+ let Ok(report) = crate::session_capture::capture_missing_provider_sessions_once(
367
+ &mut state,
368
+ &mut crate::provider::get_adapter,
369
+ false,
370
+ 0,
371
+ ) else {
372
+ return;
373
+ };
374
+ if report.changed || !report.assigned.is_empty() {
375
+ // The post-send capture backfill routes through `StateRepository` with
376
+ // the `MessagingSessionCapture` intent; dispatch is the same root
377
+ // helper this callsite used directly before, and the error is still
378
+ // ignored because the capture is best-effort.
379
+ let _ = crate::state::repository::StateRepository::new(workspace).save(
380
+ crate::state::repository::StateWriteIntent::MessagingSessionCapture,
381
+ &state,
382
+ );
383
+ }
384
+ }
385
+
359
386
  fn persist_stored_only_send(
360
387
  workspace: &Path,
361
388
  recipient: LogicalRecipient,
@@ -407,6 +434,7 @@ fn persist_stored_only_send(
407
434
  reason: None,
408
435
  channel: Some(presentation.effective_sink.as_str().to_string()),
409
436
  ack_forced_off: false,
437
+ turn_verification: None,
410
438
  })
411
439
  }
412
440
 
@@ -455,6 +483,7 @@ fn refused_outcome_with_id(reason: DeliveryRefusal, message_id: Option<String>)
455
483
  reason: Some(reason),
456
484
  channel: None,
457
485
  ack_forced_off: false,
486
+ turn_verification: None,
458
487
  }
459
488
  }
460
489
 
@@ -472,6 +501,7 @@ fn refused_outcome_with_verification(
472
501
  reason: Some(reason),
473
502
  channel: None,
474
503
  ack_forced_off: false,
504
+ turn_verification: None,
475
505
  }
476
506
  }
477
507
 
@@ -555,6 +585,7 @@ fn coordinator_unavailable_outcome(
555
585
  reason: Some(DeliveryRefusal::CoordinatorUnavailable),
556
586
  channel: Some("coordinator_unavailable".to_string()),
557
587
  ack_forced_off: false,
588
+ turn_verification: None,
558
589
  },
559
590
  }))
560
591
  }
@@ -580,6 +611,7 @@ fn rebind_required_outcome_with_verification(
580
611
  reason: Some(DeliveryRefusal::LeaderNotAttached),
581
612
  channel: Some("rebind_required".to_string()),
582
613
  ack_forced_off: false,
614
+ turn_verification: None,
583
615
  }
584
616
  }
585
617
 
@@ -812,6 +844,7 @@ pub(crate) fn session_drift_refusal(
812
844
  reason: Some(DeliveryRefusal::SessionDrift),
813
845
  channel: None,
814
846
  ack_forced_off: false,
847
+ turn_verification: None,
815
848
  }))
816
849
  }
817
850
 
@@ -933,5 +966,6 @@ fn fanout_send(
933
966
  channel_label.to_string()
934
967
  }),
935
968
  ack_forced_off: false,
969
+ turn_verification: None,
936
970
  })
937
971
  }
@@ -0,0 +1,406 @@
1
+ //! ---
2
+ //! purpose: 消息双投三机制回归——claim 闸、observer 落盘失败、token 幂等
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: dup-inject-unit
6
+ //! what: 同 message_id 二次 deliver 不得再 inject;observer 失败不返 Err 重投
7
+ //! requires:
8
+ //! - name: recording-transport
9
+ //! what: 捕获 pane 文本与 inject 次数
10
+ //! boundary:
11
+ //! - 不作为活体验收;活体三臂装置在 scripts/repro/repro_dup_inject.sh
12
+ //! maturity: wired
13
+ //! ---
14
+
15
+ use super::*;
16
+ use std::collections::BTreeMap;
17
+ use std::path::{Path, PathBuf};
18
+ use std::sync::atomic::{AtomicUsize, Ordering};
19
+ use std::sync::{Arc, Mutex};
20
+
21
+ use crate::event_log::EventLog;
22
+ use crate::message_store::MessageStore;
23
+ use crate::messaging::leader_receiver::deliver_to_leader_fallback_pane;
24
+ use crate::transport::{
25
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
26
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
27
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
28
+ TurnVerification, WindowName,
29
+ };
30
+
31
+ #[test]
32
+ fn error_retry_claim_is_single_winner_across_two_delivers() {
33
+ let case = WorkerCase::new("claim-gate", "fake");
34
+ let transport = RecordingTransport::new("");
35
+ let message_id = case.seed_leader_to_worker("claim canary");
36
+ case.store
37
+ .mark(&message_id, "target_resolved", Some("probe_failed"))
38
+ .unwrap();
39
+
40
+ let first = deliver_pending_message(
41
+ &case.workspace,
42
+ &case.store,
43
+ &transport,
44
+ &message_id,
45
+ &case.event_log,
46
+ &case.state,
47
+ )
48
+ .expect("first retry after error");
49
+ assert!(
50
+ first.ok || first.status == DeliveryStatus::AlreadyDelivered,
51
+ "{first:?}"
52
+ );
53
+ assert_eq!(
54
+ transport.inject_count(),
55
+ 1,
56
+ "first claim winner injects once"
57
+ );
58
+
59
+ case.store
60
+ .mark(&message_id, "target_resolved", Some("probe_failed"))
61
+ .unwrap();
62
+ let second = deliver_pending_message(
63
+ &case.workspace,
64
+ &case.store,
65
+ &transport,
66
+ &message_id,
67
+ &case.event_log,
68
+ &case.state,
69
+ )
70
+ .expect("second retry after plant");
71
+ assert_eq!(
72
+ transport.inject_count(),
73
+ 1,
74
+ "token already in pane must refuse the planted second inject: {second:?}"
75
+ );
76
+ }
77
+
78
+ #[test]
79
+ fn observer_state_save_failure_after_enter_does_not_return_err() {
80
+ let case = WorkerCase::new("observer-degrade", "fake");
81
+ let transport = RecordingTransport::new("");
82
+ let message_id = case.seed_leader_to_worker("observer canary");
83
+ poison_state_file(&case.workspace);
84
+
85
+ let outcome = deliver_pending_message(
86
+ &case.workspace,
87
+ &case.store,
88
+ &transport,
89
+ &message_id,
90
+ &case.event_log,
91
+ &case.state,
92
+ )
93
+ .expect("physical Enter already sent; observer save must not reverse that into Err");
94
+ assert_eq!(transport.inject_count(), 1);
95
+ assert!(
96
+ case.events().contains("delivery.post_submit_state_degraded")
97
+ || outcome.ok
98
+ || outcome.status == DeliveryStatus::AlreadyDelivered,
99
+ "observer/state save failure must degrade, not reopen inject. outcome={outcome:?} events={}",
100
+ case.events()
101
+ );
102
+ }
103
+
104
+ #[test]
105
+ fn pane_token_refuses_second_inject_for_claude_grok_cursor() {
106
+ for provider in ["claude", "grok", "cursor"] {
107
+ let case = WorkerCase::new(&format!("token-{provider}"), provider);
108
+ let transport = RecordingTransport::new("");
109
+ let message_id = case.seed_leader_to_worker(&format!("{provider} canary"));
110
+ let first = deliver_pending_message(
111
+ &case.workspace,
112
+ &case.store,
113
+ &transport,
114
+ &message_id,
115
+ &case.event_log,
116
+ &case.state,
117
+ )
118
+ .expect("first inject");
119
+ assert_eq!(
120
+ transport.inject_count(),
121
+ 1,
122
+ "provider={provider} first inject failed: {first:?}"
123
+ );
124
+
125
+ case.store
126
+ .mark(&message_id, "target_resolved", Some("probe_failed"))
127
+ .unwrap();
128
+ let second = deliver_pending_message(
129
+ &case.workspace,
130
+ &case.store,
131
+ &transport,
132
+ &message_id,
133
+ &case.event_log,
134
+ &case.state,
135
+ )
136
+ .expect("second deliver");
137
+ assert_eq!(
138
+ transport.inject_count(),
139
+ 1,
140
+ "provider={provider} pane token must refuse second inject: {second:?}"
141
+ );
142
+ assert!(
143
+ second.status == DeliveryStatus::AlreadyDelivered
144
+ || second.reason == Some(DeliveryRefusal::MessageAlreadyClaimed)
145
+ || case.events().contains("delivery.duplicate_token_refused"),
146
+ "provider={provider} missing duplicate refuse. outcome={second:?} events={}",
147
+ case.events()
148
+ );
149
+ }
150
+ }
151
+
152
+ #[test]
153
+ fn transcript_token_refuses_second_inject() {
154
+ let case = WorkerCase::new("transcript", "claude");
155
+ let message_id = case.seed_leader_to_worker("transcript canary");
156
+ std::fs::write(
157
+ &case.rollout,
158
+ format!("user\n[team-agent-token:{message_id}]\n"),
159
+ )
160
+ .unwrap();
161
+ let transport = RecordingTransport::new("unrelated pane text");
162
+ case.store
163
+ .mark(&message_id, "target_resolved", Some("probe_failed"))
164
+ .unwrap();
165
+ let outcome = deliver_pending_message(
166
+ &case.workspace,
167
+ &case.store,
168
+ &transport,
169
+ &message_id,
170
+ &case.event_log,
171
+ &case.state,
172
+ )
173
+ .expect("transcript hit");
174
+ assert_eq!(
175
+ transport.inject_count(),
176
+ 0,
177
+ "transcript token must refuse inject: {outcome:?}"
178
+ );
179
+ }
180
+
181
+ #[test]
182
+ fn fallback_already_delivered_uses_live_status_set() {
183
+ let workspace = tmp_ws("fallback-status");
184
+ let store = MessageStore::open(&workspace).unwrap();
185
+ let mid = store
186
+ .create_message(None, "w1", "leader", "fallback canary", None, false, None)
187
+ .unwrap();
188
+ store
189
+ .mark(&mid, "submitted_pending_acceptance", None)
190
+ .unwrap();
191
+
192
+ let event_log = EventLog::new(&workspace);
193
+ let state = serde_json::json!({
194
+ "active_team_key": "t",
195
+ "leader_receiver": {"pane_id": "%leader", "status": "attached"}
196
+ });
197
+ crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
198
+ let outcome = deliver_to_leader_fallback_pane(
199
+ &workspace,
200
+ &state,
201
+ &mid,
202
+ None,
203
+ "fallback canary",
204
+ false,
205
+ Some("primary failed"),
206
+ &event_log,
207
+ )
208
+ .expect("fallback short-circuit");
209
+ assert_eq!(outcome.status, DeliveryStatus::AlreadyDelivered);
210
+ }
211
+
212
+ struct WorkerCase {
213
+ workspace: PathBuf,
214
+ rollout: PathBuf,
215
+ store: MessageStore,
216
+ event_log: EventLog,
217
+ state: serde_json::Value,
218
+ }
219
+
220
+ impl WorkerCase {
221
+ fn new(tag: &str, provider: &str) -> Self {
222
+ let workspace = tmp_ws(&format!("dup-inject-{tag}"));
223
+ let rollout = workspace.join(format!("{provider}-rollout.jsonl"));
224
+ std::fs::write(&rollout, "").unwrap();
225
+ let state = serde_json::json!({
226
+ "active_team_key": "dup-team",
227
+ "session_name": "team-dup",
228
+ "agents": {
229
+ "w1": {
230
+ "pane_id": "%w1",
231
+ "window": "w1",
232
+ "provider": provider,
233
+ "rollout_path": rollout,
234
+ }
235
+ }
236
+ });
237
+ crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
238
+ let store = MessageStore::open(&workspace).unwrap();
239
+ let event_log = EventLog::new(&workspace);
240
+ Self {
241
+ workspace,
242
+ rollout,
243
+ store,
244
+ event_log,
245
+ state,
246
+ }
247
+ }
248
+
249
+ fn seed_leader_to_worker(&self, content: &str) -> String {
250
+ self.store
251
+ .create_message(None, "leader", "w1", content, None, false, None)
252
+ .unwrap()
253
+ }
254
+
255
+ fn events(&self) -> String {
256
+ self.event_log
257
+ .tail(0)
258
+ .unwrap()
259
+ .into_iter()
260
+ .map(|event| event.to_string())
261
+ .collect::<Vec<_>>()
262
+ .join("\n")
263
+ }
264
+ }
265
+
266
+ fn poison_state_file(workspace: &Path) {
267
+ let path = crate::state::persist::runtime_state_path(workspace);
268
+ let _ = std::fs::remove_file(&path);
269
+ std::fs::create_dir_all(&path).unwrap();
270
+ }
271
+
272
+ #[derive(Clone)]
273
+ struct RecordingTransport {
274
+ injects: Arc<AtomicUsize>,
275
+ capture: Arc<Mutex<String>>,
276
+ }
277
+
278
+ impl RecordingTransport {
279
+ fn new(capture: &str) -> Self {
280
+ Self {
281
+ injects: Arc::new(AtomicUsize::new(0)),
282
+ capture: Arc::new(Mutex::new(capture.to_string())),
283
+ }
284
+ }
285
+
286
+ fn inject_count(&self) -> usize {
287
+ self.injects.load(Ordering::Relaxed)
288
+ }
289
+ }
290
+
291
+ impl Transport for RecordingTransport {
292
+ fn kind(&self) -> BackendKind {
293
+ BackendKind::Tmux
294
+ }
295
+
296
+ fn spawn_first(
297
+ &self,
298
+ _session: &SessionName,
299
+ _window: &WindowName,
300
+ _argv: &[String],
301
+ _cwd: &Path,
302
+ _env: &BTreeMap<String, String>,
303
+ ) -> Result<SpawnResult, TransportError> {
304
+ unreachable!("dup-inject tests do not spawn")
305
+ }
306
+
307
+ fn spawn_into(
308
+ &self,
309
+ _session: &SessionName,
310
+ _window: &WindowName,
311
+ _argv: &[String],
312
+ _cwd: &Path,
313
+ _env: &BTreeMap<String, String>,
314
+ ) -> Result<SpawnResult, TransportError> {
315
+ unreachable!("dup-inject tests do not spawn")
316
+ }
317
+
318
+ fn inject(
319
+ &self,
320
+ _target: &Target,
321
+ payload: &InjectPayload,
322
+ _submit: Key,
323
+ _bracketed_paste: bool,
324
+ ) -> Result<InjectReport, TransportError> {
325
+ self.injects.fetch_add(1, Ordering::Relaxed);
326
+ if let Some(text) = payload.text() {
327
+ self.capture.lock().unwrap().push_str(text);
328
+ }
329
+ Ok(InjectReport {
330
+ stage_reached: InjectStage::Submit,
331
+ inject_verification: InjectVerification::CaptureContainsToken,
332
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
333
+ turn_verification: TurnVerification::NotYetObserved,
334
+ attempts: 1,
335
+ submit_diagnostics: None,
336
+ })
337
+ }
338
+
339
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
340
+ Ok(())
341
+ }
342
+
343
+ fn capture(
344
+ &self,
345
+ _target: &Target,
346
+ range: CaptureRange,
347
+ ) -> Result<CapturedText, TransportError> {
348
+ Ok(CapturedText {
349
+ text: self.capture.lock().unwrap().clone(),
350
+ range,
351
+ })
352
+ }
353
+
354
+ fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
355
+ Ok(None)
356
+ }
357
+
358
+ fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
359
+ Ok(PaneLiveness::Live)
360
+ }
361
+
362
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
363
+ Ok(vec![PaneInfo {
364
+ pane_id: PaneId::new("%w1"),
365
+ session: SessionName::new("team-dup"),
366
+ window_index: None,
367
+ window_name: Some(WindowName::new("w1")),
368
+ pane_index: None,
369
+ tty: None,
370
+ current_command: Some("sleep".to_string()),
371
+ current_path: None,
372
+ active: true,
373
+ pane_pid: None,
374
+ leader_env: BTreeMap::new(),
375
+ }])
376
+ }
377
+
378
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
379
+ Ok(true)
380
+ }
381
+
382
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
383
+ Ok(vec![WindowName::new("w1")])
384
+ }
385
+
386
+ fn set_session_env(
387
+ &self,
388
+ _session: &SessionName,
389
+ _key: &str,
390
+ _value: &str,
391
+ ) -> Result<SetEnvOutcome, TransportError> {
392
+ Ok(SetEnvOutcome::Applied)
393
+ }
394
+
395
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
396
+ Ok(())
397
+ }
398
+
399
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
400
+ Ok(())
401
+ }
402
+
403
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
404
+ Ok(AttachOutcome::Attached)
405
+ }
406
+ }