@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
@@ -1,9 +1,25 @@
1
+ //! ---
2
+ //! purpose: 把已认领消息注入目标 pane 并记录投递阶段
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: deliver_stored_message
6
+ //! what: 单次物理注入;grok 队列默认再按回车顶出;cursor 打开单回车闸且不 flush send-now
7
+ //! boundary:
8
+ //! - 不把物理 success 写成 delivered
9
+ //! - 不重粘文本
10
+ //! - error-bypass(target_resolved+error)只经 claim 原子闸,禁止旁路双投
11
+ //! - 物理 Enter 已出(pane/转录已有 token)后 observer/state 落盘失败不得返 Err 重投
12
+ //! - 提交前持久化失败仍返 Err,由 batch 记 item_blocked 隔离
13
+ //! - 同 message_id 的 token 已在 pane/转录则拒绝二次注入
14
+ //! maturity: wired
15
+ //! ---
1
16
  //!
2
17
  //! internal_delivery.py + delivery.py — coordinator/调度器侧 thin wrapper + 单条 tmux 注入投递
3
18
  //! + trust 有界重试 + turn-open arm (card §16/§65)。
4
19
 
5
20
  use std::cell::{Cell, RefCell};
6
21
  use std::path::Path;
22
+ use std::time::Duration;
7
23
 
8
24
  use rusqlite::{params, OptionalExtension};
9
25
 
@@ -15,8 +31,9 @@ use crate::provider::wire::{
15
31
  is_claude_family, parse_canonical_provider, parse_provider, provider_wire,
16
32
  };
17
33
  use crate::transport::{
18
- submit_verification_wire, InjectPayload, InjectReport, InjectVerification, Key, PaneId,
19
- PaneInfo, SessionName, SubmitObserver, SubmitVerification, Target, Transport, WindowName,
34
+ submit_verification_wire, turn_verification_wire, CaptureRange, InjectPayload, InjectReport,
35
+ InjectVerification, Key, PaneId, PaneInfo, SessionName, SubmitObserver, SubmitVerification,
36
+ Target, Transport, WindowName,
20
37
  };
21
38
 
22
39
  use super::helpers::{message_exists, MessageStatusShadow};
@@ -75,6 +92,7 @@ pub fn deliver_stored_message(
75
92
  reason: None,
76
93
  channel: None,
77
94
  ack_forced_off: false,
95
+ turn_verification: None,
78
96
  })
79
97
  }
80
98
 
@@ -183,6 +201,7 @@ pub fn deliver_pending_message(
183
201
  reason: None,
184
202
  channel: None,
185
203
  ack_forced_off: false,
204
+ turn_verification: None,
186
205
  });
187
206
  }
188
207
  let message = message_for_delivery(store, message_id)?;
@@ -197,6 +216,7 @@ pub fn deliver_pending_message(
197
216
  reason: Some(DeliveryRefusal::UnknownRecipient),
198
217
  channel: None,
199
218
  ack_forced_off: false,
219
+ turn_verification: None,
200
220
  });
201
221
  };
202
222
  let mut canonical_owner_team_id = message.owner_team_id.clone();
@@ -272,8 +292,6 @@ pub fn deliver_pending_message(
272
292
  }
273
293
  let attempt = if store.claim_for_delivery(message_id)? {
274
294
  message.delivery_attempts.saturating_add(1)
275
- } else if message.status == "target_resolved" && message.error.is_some() {
276
- bump_delivery_attempts(store, message_id)?
277
295
  } else {
278
296
  return Ok(DeliveryOutcome {
279
297
  ok: false,
@@ -285,8 +303,31 @@ pub fn deliver_pending_message(
285
303
  reason: Some(DeliveryRefusal::MessageAlreadyClaimed),
286
304
  channel: None,
287
305
  ack_forced_off: false,
306
+ turn_verification: None,
288
307
  });
289
308
  };
309
+ // Attach may requeue this row after the coordinator captured `state` but
310
+ // before this claim. Revalidate through the same owner-team projection
311
+ // that carries a canonical top-level leader binding into the scoped view.
312
+ let fresh_leader_state = if message.recipient == "leader" {
313
+ Some(match canonical_owner_team_id.as_deref() {
314
+ Some(team) => match project_state_for_owner_team(
315
+ workspace,
316
+ team,
317
+ &serde_json::Value::Null,
318
+ Some(store),
319
+ Some(message_id),
320
+ Some(event_log),
321
+ )? {
322
+ OwnerTeamProjection::Projected { state, .. } => state,
323
+ OwnerTeamProjection::Refused(outcome) => return Ok(outcome),
324
+ },
325
+ None => crate::state::persist::load_runtime_state(workspace)?,
326
+ })
327
+ } else {
328
+ None
329
+ };
330
+ let state = fresh_leader_state.as_ref().unwrap_or(state);
290
331
  if message.recipient == "leader" {
291
332
  let Some(receiver) = leader_receiver_value(state) else {
292
333
  store.mark(message_id, "failed", Some("leader_not_attached"))?;
@@ -315,6 +356,7 @@ pub fn deliver_pending_message(
315
356
  reason: Some(DeliveryRefusal::LeaderNotAttached),
316
357
  channel: Some("rebind_required".to_string()),
317
358
  ack_forced_off: false,
359
+ turn_verification: None,
318
360
  });
319
361
  };
320
362
  if let Some(outcome) = leader_receiver_transport_conflict_outcome(
@@ -346,6 +388,7 @@ pub fn deliver_pending_message(
346
388
  reason: None,
347
389
  channel: Some("rebind_required".to_string()),
348
390
  ack_forced_off: false,
391
+ turn_verification: None,
349
392
  });
350
393
  }
351
394
  crate::messaging::LeaderChannelResolution::Unbound(reason) => {
@@ -385,6 +428,7 @@ pub fn deliver_pending_message(
385
428
  reason: Some(refusal),
386
429
  channel: Some("rebind_required".to_string()),
387
430
  ack_forced_off: false,
431
+ turn_verification: None,
388
432
  });
389
433
  }
390
434
  }
@@ -432,6 +476,7 @@ pub fn deliver_pending_message(
432
476
  reason: None,
433
477
  channel: None,
434
478
  ack_forced_off: false,
479
+ turn_verification: None,
435
480
  });
436
481
  }
437
482
  };
@@ -480,6 +525,30 @@ pub fn deliver_pending_message(
480
525
  reason: None,
481
526
  channel: None,
482
527
  ack_forced_off: false,
528
+ turn_verification: None,
529
+ });
530
+ }
531
+ if token_already_visible(transport, state, &message.recipient, &target, message_id) {
532
+ event_log.write(
533
+ "delivery.duplicate_token_refused",
534
+ serde_json::json!({
535
+ "message_id": message_id,
536
+ "recipient": message.recipient,
537
+ "reason": "message_token_already_visible",
538
+ }),
539
+ )?;
540
+ store.mark(message_id, "delivered", None)?;
541
+ return Ok(DeliveryOutcome {
542
+ ok: true,
543
+ status: DeliveryStatus::AlreadyDelivered,
544
+ message_status: MessageStatusShadow("delivered".to_string()),
545
+ message_id: Some(message_id.to_string()),
546
+ verification: Some("message_token_already_visible".to_string()),
547
+ stage: Some(DeliveryStage::Submit),
548
+ reason: None,
549
+ channel: None,
550
+ ack_forced_off: false,
551
+ turn_verification: None,
483
552
  });
484
553
  }
485
554
  let rendered = render_message(
@@ -503,14 +572,54 @@ pub fn deliver_pending_message(
503
572
  canonical_owner_team_id.as_deref(),
504
573
  resolved.metadata.as_ref(),
505
574
  );
506
- let inject_report = match transport.inject_with_submit_observer(
507
- &target,
508
- &payload,
509
- Key::Enter,
510
- true,
511
- Some(&submit_observer),
512
- ) {
513
- Ok(report) => report,
575
+ let recipient_cursor = recipient_is_cursor_agent(state, &message.recipient);
576
+ let inject_attempt = crate::tmux_backend::with_paste_to_submit_floor(
577
+ paste_to_submit_floor_for_recipient(state, &message.recipient),
578
+ || {
579
+ crate::tmux_backend::with_cursor_single_enter(recipient_cursor, || {
580
+ transport.inject_with_submit_observer(
581
+ &target,
582
+ &payload,
583
+ Key::Enter,
584
+ true,
585
+ Some(&submit_observer),
586
+ )
587
+ })
588
+ },
589
+ );
590
+ let inject_report = match inject_attempt {
591
+ Ok(report) => {
592
+ // grok 忙时第一次回车只入显式队列。默认 send-now:只重按回车顶出去。
593
+ // 无 `Enter:send now` 时零次额外按键,claude/codex 行为不变。
594
+ // cursor:第二下 Enter 打断进行中回合,不走 flush。
595
+ if !crate::provider::submit_now::keep_provider_queue_requested() && !recipient_cursor {
596
+ match crate::provider::submit_now::flush_explicit_queue(transport, &target) {
597
+ Ok(flush) if flush.extra_enters > 0 => {
598
+ let _ = event_log.write(
599
+ "send.grok_send_now",
600
+ serde_json::json!({
601
+ "message_id": message_id,
602
+ "recipient": message.recipient,
603
+ "extra_enters": flush.extra_enters,
604
+ "mark_cleared": flush.mark_cleared,
605
+ }),
606
+ );
607
+ }
608
+ Ok(_) => {}
609
+ Err(error) => {
610
+ let _ = event_log.write(
611
+ "send.grok_send_now_failed",
612
+ serde_json::json!({
613
+ "message_id": message_id,
614
+ "recipient": message.recipient,
615
+ "error": error.to_string(),
616
+ }),
617
+ );
618
+ }
619
+ }
620
+ }
621
+ report
622
+ }
514
623
  Err(error) => {
515
624
  let reason = format!("inject_failed:{error}");
516
625
  if message.recipient == "leader" {
@@ -538,6 +647,7 @@ pub fn deliver_pending_message(
538
647
  reason: Some(DeliveryRefusal::LeaderNotAttached),
539
648
  channel: Some("rebind_required".to_string()),
540
649
  ack_forced_off: false,
650
+ turn_verification: None,
541
651
  });
542
652
  }
543
653
  if transport_error_is_target_missing(&error) {
@@ -582,6 +692,7 @@ pub fn deliver_pending_message(
582
692
  reason: None,
583
693
  channel: None,
584
694
  ack_forced_off: false,
695
+ turn_verification: None,
585
696
  });
586
697
  }
587
698
  store.mark(message_id, "target_resolved", Some(&reason))?;
@@ -595,12 +706,26 @@ pub fn deliver_pending_message(
595
706
  reason: None,
596
707
  channel: None,
597
708
  ack_forced_off: false,
709
+ turn_verification: None,
598
710
  });
599
711
  }
600
712
  };
601
713
  if let Some(error) = submit_observer.take_error() {
602
- return Err(error);
714
+ // Token on pane/transcript = physical Enter already out → degrade.
715
+ // Poison before submit (no token) still returns Err → item_blocked.
716
+ absorb_state_error_only_after_physical_submit(
717
+ transport,
718
+ state,
719
+ &message.recipient,
720
+ &target,
721
+ message_id,
722
+ event_log,
723
+ "submit_observer",
724
+ error,
725
+ )?;
603
726
  }
727
+ persist_submit_verification(workspace, message_id, &inject_report);
728
+ let turn_verification = inject_report.turn_verification;
604
729
  let submit_verified = inject_submit_verified(&inject_report);
605
730
  let readback_verified = pane_readback_verified(&inject_report);
606
731
  // A successful tmux command is never provider-acceptance proof. Leader and
@@ -655,6 +780,7 @@ pub fn deliver_pending_message(
655
780
  reason: None,
656
781
  channel: None,
657
782
  ack_forced_off: false,
783
+ turn_verification: Some(turn_verification),
658
784
  });
659
785
  }
660
786
  store.mark(message_id, "submitted_unverified", Some(&reason))?;
@@ -668,6 +794,7 @@ pub fn deliver_pending_message(
668
794
  reason: None,
669
795
  channel: None,
670
796
  ack_forced_off: false,
797
+ turn_verification: Some(turn_verification),
671
798
  });
672
799
  }
673
800
  // S1-CAPTURE-001 (0.4.8, CR M4 Claude phase-1): for Claude/ClaudeCode
@@ -733,6 +860,7 @@ pub fn deliver_pending_message(
733
860
  reason: None,
734
861
  channel: None,
735
862
  ack_forced_off: false,
863
+ turn_verification: Some(turn_verification),
736
864
  });
737
865
  }
738
866
  }
@@ -745,7 +873,9 @@ pub fn deliver_pending_message(
745
873
  "leader_receiver.receipt_source_unavailable",
746
874
  serde_json::json!({"message_id": message_id}),
747
875
  )?;
748
- return Ok(leader_receipt_source_unavailable_outcome(message_id));
876
+ let mut outcome = leader_receipt_source_unavailable_outcome(message_id);
877
+ outcome.turn_verification = Some(turn_verification);
878
+ return Ok(outcome);
749
879
  }
750
880
  LeaderReceiptObservation::TokenAbsent => {
751
881
  store.mark(message_id, "submitted_pending_acceptance", None)?;
@@ -756,10 +886,10 @@ pub fn deliver_pending_message(
756
886
  "reason": "provider_receipt_not_observed",
757
887
  }),
758
888
  )?;
759
- return Ok(leader_acceptance_pending_outcome(
760
- message_id,
761
- "submitted_pending_acceptance",
762
- ));
889
+ let mut outcome =
890
+ leader_acceptance_pending_outcome(message_id, "submitted_pending_acceptance");
891
+ outcome.turn_verification = Some(turn_verification);
892
+ return Ok(outcome);
763
893
  }
764
894
  LeaderReceiptObservation::TokenObserved => {}
765
895
  }
@@ -782,14 +912,26 @@ pub fn deliver_pending_message(
782
912
  reason: None,
783
913
  channel: None,
784
914
  ack_forced_off: false,
915
+ turn_verification: Some(turn_verification),
785
916
  };
786
- stamp_first_send_at_if_leader_to_worker_scoped(
917
+ if let Err(error) = stamp_first_send_at_if_leader_to_worker_scoped(
787
918
  workspace,
788
919
  &message.sender,
789
920
  &message.recipient,
790
921
  canonical_owner_team_id.as_deref(),
791
- )?;
792
- record_turn_open_if_leader_to_worker_scoped(
922
+ ) {
923
+ absorb_state_error_only_after_physical_submit(
924
+ transport,
925
+ state,
926
+ &message.recipient,
927
+ &target,
928
+ message_id,
929
+ event_log,
930
+ "stamp_first_send_at",
931
+ error,
932
+ )?;
933
+ }
934
+ if let Err(error) = record_turn_open_if_leader_to_worker_scoped(
793
935
  workspace,
794
936
  &message.sender,
795
937
  &message.recipient,
@@ -797,7 +939,18 @@ pub fn deliver_pending_message(
797
939
  event_log,
798
940
  canonical_owner_team_id.as_deref(),
799
941
  resolved.metadata.as_ref(),
800
- )?;
942
+ ) {
943
+ absorb_state_error_only_after_physical_submit(
944
+ transport,
945
+ state,
946
+ &message.recipient,
947
+ &target,
948
+ message_id,
949
+ event_log,
950
+ "turn_open_after_delivery",
951
+ error,
952
+ )?;
953
+ }
801
954
  Ok(outcome)
802
955
  }
803
956
 
@@ -864,6 +1017,7 @@ pub(crate) fn mark_worker_target_missing(
864
1017
  reason: Some(DeliveryRefusal::TmuxTargetMissing),
865
1018
  channel: Some("delivery_blocked".to_string()),
866
1019
  ack_forced_off: false,
1020
+ turn_verification: None,
867
1021
  })
868
1022
  }
869
1023
 
@@ -904,6 +1058,27 @@ pub fn requeue_worker_target_missing_messages(
904
1058
  Ok(ids)
905
1059
  }
906
1060
 
1061
+ /// 把 post-Enter 消费判定落到 workspace,供确认面扫盘。
1062
+ ///
1063
+ /// //! purpose: persist SubmitVerification after inject
1064
+ /// //! contract: file contains Debug variant name (SubmitConsumptionUnverified / EnterSentWithoutPlaceholderCheck)
1065
+ /// //! boundary: write failure must not fail delivery
1066
+ fn persist_submit_verification(workspace: &Path, message_id: &str, report: &InjectReport) {
1067
+ let dir = workspace.join(".team").join("runtime");
1068
+ if std::fs::create_dir_all(&dir).is_err() {
1069
+ return;
1070
+ }
1071
+ let body = serde_json::json!({
1072
+ "message_id": message_id,
1073
+ "submit_verification": format!("{:?}", report.submit_verification),
1074
+ "inject_verification": format!("{:?}", report.inject_verification),
1075
+ "turn_verification": turn_verification_wire(report.turn_verification),
1076
+ "submit_verified": inject_submit_verified(report),
1077
+ "attempts": report.attempts,
1078
+ });
1079
+ let _ = std::fs::write(dir.join("submit-verification.json"), format!("{body}\n"));
1080
+ }
1081
+
907
1082
  /// 0.3.27: promoted to pub(crate) for leader_receiver.rs verification gate.
908
1083
  pub(crate) fn inject_submit_verified(report: &InjectReport) -> bool {
909
1084
  match report.submit_verification {
@@ -1018,6 +1193,7 @@ fn deliver_leader_via_app_server(
1018
1193
  reason: None,
1019
1194
  channel: Some("codex_app_server".to_string()),
1020
1195
  ack_forced_off: false,
1196
+ turn_verification: None,
1021
1197
  })
1022
1198
  }
1023
1199
  Err(error) => app_server_delivery_failure(
@@ -1064,6 +1240,7 @@ fn app_server_delivery_failure(
1064
1240
  reason: Some(DeliveryRefusal::RecipientBusy),
1065
1241
  channel: Some("leader_busy".to_string()),
1066
1242
  ack_forced_off: false,
1243
+ turn_verification: None,
1067
1244
  })
1068
1245
  }
1069
1246
  crate::codex_app_server::AppServerError::ThreadStale { expected, actual } => {
@@ -1103,6 +1280,7 @@ fn app_server_delivery_failure(
1103
1280
  reason: Some(DeliveryRefusal::MissingPermissions),
1104
1281
  channel: Some("codex_app_server".to_string()),
1105
1282
  ack_forced_off: false,
1283
+ turn_verification: None,
1106
1284
  })
1107
1285
  }
1108
1286
  crate::codex_app_server::AppServerError::ProtocolMismatch(_)
@@ -1155,6 +1333,7 @@ fn rebind_required_outcome(message_id: &str, action: &str) -> DeliveryOutcome {
1155
1333
  reason: Some(DeliveryRefusal::LeaderNotAttached),
1156
1334
  channel: Some("rebind_required".to_string()),
1157
1335
  ack_forced_off: false,
1336
+ turn_verification: None,
1158
1337
  }
1159
1338
  }
1160
1339
 
@@ -1558,6 +1737,7 @@ fn leader_receiver_transport_conflict_outcome(
1558
1737
  reason: Some(DeliveryRefusal::LeaderNotAttached),
1559
1738
  channel: Some("rebind_required".to_string()),
1560
1739
  ack_forced_off: false,
1740
+ turn_verification: None,
1561
1741
  }));
1562
1742
  }
1563
1743
  }
@@ -1973,23 +2153,6 @@ fn message_for_delivery(
1973
2153
  Ok(message)
1974
2154
  }
1975
2155
 
1976
- fn bump_delivery_attempts(store: &MessageStore, message_id: &str) -> Result<u32, MessagingError> {
1977
- let conn = crate::db::schema::open_db(store.db_path())?;
1978
- conn.execute(
1979
- "update messages
1980
- set delivery_attempts = delivery_attempts + 1,
1981
- updated_at = ?2
1982
- where message_id = ?1",
1983
- params![message_id, chrono::Utc::now().to_rfc3339()],
1984
- )?;
1985
- let attempts = conn.query_row(
1986
- "select delivery_attempts from messages where message_id = ?1",
1987
- params![message_id],
1988
- |row| row.get::<_, i64>(0),
1989
- )?;
1990
- Ok(attempts.max(0) as u32)
1991
- }
1992
-
1993
2156
  /// Pre-inject gate (Contract B): peek the recipient pane and answer "is there an
1994
2157
  /// actionable provider startup prompt right now (trust menu or update prompt)" using
1995
2158
  /// the SHARED provider/startup_prompt recognizers — no second classifier, no provider
@@ -2058,7 +2221,41 @@ fn recipient_pane_has_actionable_startup_prompt(
2058
2221
  crate::provider::classify_copilot_startup_screen(&captured),
2059
2222
  crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
2060
2223
  ),
2061
- Provider::GeminiCli | Provider::Fake => false,
2224
+ Provider::Grok | Provider::CursorAgent | Provider::GeminiCli | Provider::Fake => false,
2225
+ }
2226
+ }
2227
+
2228
+ fn recipient_provider(state: &serde_json::Value, recipient: &str) -> Option<Provider> {
2229
+ state
2230
+ .get("agents")
2231
+ .and_then(serde_json::Value::as_object)
2232
+ .and_then(|agents| agents.get(recipient))
2233
+ .and_then(|agent| agent.get("provider"))
2234
+ .and_then(serde_json::Value::as_str)
2235
+ .and_then(parse_canonical_provider)
2236
+ }
2237
+
2238
+ fn recipient_is_cursor_agent(state: &serde_json::Value, recipient: &str) -> bool {
2239
+ matches!(
2240
+ recipient_provider(state, recipient),
2241
+ Some(Provider::CursorAgent)
2242
+ )
2243
+ }
2244
+
2245
+ /// ---
2246
+ /// purpose: paste→Enter 地板按收件人 provider 选择
2247
+ /// contract: CursorAgent 与 Grok 为 1s;其余(含 claude)为 ZERO
2248
+ /// boundary: 不改 cursor 单回车闸;不把地板套到 claude/codex
2249
+ /// ---
2250
+ pub(crate) fn paste_to_submit_floor_for_recipient(
2251
+ state: &serde_json::Value,
2252
+ recipient: &str,
2253
+ ) -> Duration {
2254
+ match recipient_provider(state, recipient) {
2255
+ Some(Provider::CursorAgent) | Some(Provider::Grok) => {
2256
+ crate::tmux_backend::CURSOR_PASTE_TO_SUBMIT_FLOOR
2257
+ }
2258
+ _ => Duration::ZERO,
2062
2259
  }
2063
2260
  }
2064
2261
 
@@ -2129,6 +2326,7 @@ fn leader_acceptance_pending_outcome(message_id: &str, status: &str) -> Delivery
2129
2326
  reason: None,
2130
2327
  channel: Some("leader_acceptance_pending".to_string()),
2131
2328
  ack_forced_off: false,
2329
+ turn_verification: None,
2132
2330
  }
2133
2331
  }
2134
2332
 
@@ -2143,6 +2341,7 @@ pub(crate) fn leader_receipt_source_unavailable_outcome(message_id: &str) -> Del
2143
2341
  reason: None,
2144
2342
  channel: Some("leader_receipt_source_unavailable".to_string()),
2145
2343
  ack_forced_off: false,
2344
+ turn_verification: None,
2146
2345
  }
2147
2346
  }
2148
2347
 
@@ -2184,6 +2383,7 @@ fn observe_pending_leader_acceptance(
2184
2383
  reason: None,
2185
2384
  channel: Some("leader_receiver".to_string()),
2186
2385
  ack_forced_off: false,
2386
+ turn_verification: None,
2187
2387
  })
2188
2388
  }
2189
2389
 
@@ -2214,6 +2414,7 @@ pub fn handle_trust_retry_needed(
2214
2414
  reason: None,
2215
2415
  channel: None,
2216
2416
  ack_forced_off: false,
2417
+ turn_verification: None,
2217
2418
  });
2218
2419
  }
2219
2420
  let next_attempt = payload.attempt.saturating_add(1);
@@ -2254,6 +2455,7 @@ pub fn handle_trust_retry_needed(
2254
2455
  reason: None,
2255
2456
  channel: None,
2256
2457
  ack_forced_off: false,
2458
+ turn_verification: None,
2257
2459
  })
2258
2460
  }
2259
2461
 
@@ -2751,6 +2953,7 @@ fn refuse_owner_team_resolution(
2751
2953
  reason: Some(refusal),
2752
2954
  channel: Some("owner_team_resolution".to_string()),
2753
2955
  ack_forced_off: false,
2956
+ turn_verification: None,
2754
2957
  })
2755
2958
  }
2756
2959
 
@@ -2808,6 +3011,7 @@ pub fn retry_injection_after_trust_auto_answer(
2808
3011
  reason: None,
2809
3012
  channel: None,
2810
3013
  ack_forced_off: false,
3014
+ turn_verification: None,
2811
3015
  })
2812
3016
  }
2813
3017
 
@@ -2844,6 +3048,134 @@ fn rollout_tail_contains(path: &std::path::Path, needle: &str, tail_bytes: u64)
2844
3048
  rollout_tail_contains_result(path, needle, tail_bytes).unwrap_or(false)
2845
3049
  }
2846
3050
 
3051
+ fn message_token_marker(message_id: &str) -> String {
3052
+ format!("[team-agent-token:{message_id}]")
3053
+ }
3054
+
3055
+ fn text_contains_message_token(text: &str, message_id: &str) -> bool {
3056
+ !message_id.is_empty() && text.contains(&message_token_marker(message_id))
3057
+ }
3058
+
3059
+ /// ---
3060
+ /// purpose: 同 message_id 已在 pane 或转录出现则视为已注入
3061
+ /// contract:
3062
+ /// provides:
3063
+ /// - name: token_already_visible
3064
+ /// what: capture Tail(80) 或 recipient rollout_path 尾含 `[team-agent-token:{id}]` 则为 true
3065
+ /// boundary:
3066
+ /// - capture 失败不当已见(倒向允许注入,避免丢信)
3067
+ /// - 不请求 Full(wave-2:滚出屏的 trust 残渣不当成已见/可行动)
3068
+ /// - 不把 JSON 字段里的 message_id 当 pane token
3069
+ /// ---
3070
+ pub(crate) fn token_already_visible(
3071
+ transport: &dyn Transport,
3072
+ state: &serde_json::Value,
3073
+ recipient: &str,
3074
+ target: &Target,
3075
+ message_id: &str,
3076
+ ) -> bool {
3077
+ if message_id.is_empty() {
3078
+ return false;
3079
+ }
3080
+ let marker = message_token_marker(message_id);
3081
+ match transport.capture(target, CaptureRange::Tail(80)) {
3082
+ Ok(captured) if text_contains_message_token(&captured.text, message_id) => return true,
3083
+ _ => {}
3084
+ }
3085
+ recipient_transcript_contains(state, recipient, &marker)
3086
+ }
3087
+
3088
+ fn recipient_transcript_contains(state: &serde_json::Value, recipient: &str, marker: &str) -> bool {
3089
+ for path in recipient_transcript_paths(state, recipient) {
3090
+ if rollout_tail_contains(&path, marker, 64 * 1024) {
3091
+ return true;
3092
+ }
3093
+ }
3094
+ false
3095
+ }
3096
+
3097
+ fn recipient_transcript_paths(
3098
+ state: &serde_json::Value,
3099
+ recipient: &str,
3100
+ ) -> Vec<std::path::PathBuf> {
3101
+ let mut paths = Vec::new();
3102
+ let push = |paths: &mut Vec<std::path::PathBuf>, value: Option<&serde_json::Value>| {
3103
+ if let Some(path) = value
3104
+ .and_then(serde_json::Value::as_str)
3105
+ .filter(|p| !p.is_empty())
3106
+ {
3107
+ paths.push(std::path::PathBuf::from(path));
3108
+ }
3109
+ };
3110
+ if recipient == "leader" {
3111
+ push(
3112
+ &mut paths,
3113
+ leader_receiver_record_path(state, "rollout_path"),
3114
+ );
3115
+ }
3116
+ let agent = state.get("agents").and_then(|agents| agents.get(recipient));
3117
+ push(
3118
+ &mut paths,
3119
+ agent.and_then(|agent| agent.get("rollout_path")),
3120
+ );
3121
+ paths
3122
+ }
3123
+
3124
+ fn leader_receiver_record_path<'a>(
3125
+ state: &'a serde_json::Value,
3126
+ field: &str,
3127
+ ) -> Option<&'a serde_json::Value> {
3128
+ state
3129
+ .get("leader_receiver")
3130
+ .and_then(|receiver| receiver.get(field))
3131
+ .or_else(|| {
3132
+ let active = state
3133
+ .get("active_team_key")
3134
+ .and_then(serde_json::Value::as_str)?;
3135
+ state
3136
+ .get("teams")
3137
+ .and_then(serde_json::Value::as_object)
3138
+ .and_then(|teams| teams.get(active))
3139
+ .and_then(|team| team.get("leader_receiver"))
3140
+ .and_then(|receiver| receiver.get(field))
3141
+ })
3142
+ }
3143
+
3144
+ fn absorb_state_error_only_after_physical_submit(
3145
+ transport: &dyn Transport,
3146
+ state: &serde_json::Value,
3147
+ recipient: &str,
3148
+ target: &Target,
3149
+ message_id: &str,
3150
+ event_log: &EventLog,
3151
+ stage: &str,
3152
+ error: MessagingError,
3153
+ ) -> Result<(), MessagingError> {
3154
+ if token_already_visible(transport, state, recipient, target, message_id) {
3155
+ log_post_physical_submit_state_error(event_log, message_id, stage, &error)
3156
+ } else {
3157
+ Err(error)
3158
+ }
3159
+ }
3160
+
3161
+ fn log_post_physical_submit_state_error(
3162
+ event_log: &EventLog,
3163
+ message_id: &str,
3164
+ stage: &str,
3165
+ error: &MessagingError,
3166
+ ) -> Result<(), MessagingError> {
3167
+ event_log.write(
3168
+ "delivery.post_submit_state_degraded",
3169
+ serde_json::json!({
3170
+ "message_id": message_id,
3171
+ "stage": stage,
3172
+ "error": error.to_string(),
3173
+ "physical_submit": true,
3174
+ }),
3175
+ )?;
3176
+ Ok(())
3177
+ }
3178
+
2847
3179
  fn rollout_tail_contains_result(
2848
3180
  path: &std::path::Path,
2849
3181
  needle: &str,
@@ -2860,3 +3192,36 @@ fn rollout_tail_contains_result(
2860
3192
  let haystack = String::from_utf8_lossy(&buf);
2861
3193
  Ok(haystack.contains(needle))
2862
3194
  }
3195
+
3196
+ #[cfg(test)]
3197
+ mod paste_floor_tests {
3198
+ use super::*;
3199
+ use std::time::Duration;
3200
+
3201
+ fn state_with_provider(provider: &str) -> serde_json::Value {
3202
+ serde_json::json!({
3203
+ "agents": { "w1": { "provider": provider } }
3204
+ })
3205
+ }
3206
+
3207
+ #[test]
3208
+ fn grok_paste_to_submit_floor_matches_cursor() {
3209
+ let grok = paste_to_submit_floor_for_recipient(&state_with_provider("grok"), "w1");
3210
+ let cursor = paste_to_submit_floor_for_recipient(&state_with_provider("cursor_agent"), "w1");
3211
+ assert_eq!(grok, crate::tmux_backend::CURSOR_PASTE_TO_SUBMIT_FLOOR);
3212
+ assert_eq!(cursor, crate::tmux_backend::CURSOR_PASTE_TO_SUBMIT_FLOOR);
3213
+ assert_eq!(grok, cursor);
3214
+ }
3215
+
3216
+ #[test]
3217
+ fn claude_and_codex_paste_to_submit_floor_stays_zero() {
3218
+ assert_eq!(
3219
+ paste_to_submit_floor_for_recipient(&state_with_provider("claude"), "w1"),
3220
+ Duration::ZERO
3221
+ );
3222
+ assert_eq!(
3223
+ paste_to_submit_floor_for_recipient(&state_with_provider("codex"), "w1"),
3224
+ Duration::ZERO
3225
+ );
3226
+ }
3227
+ }