@team-agent/installer 0.5.66 → 0.5.69

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 (178) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +156 -0
  6. package/crates/team-agent/src/cli/emit.rs +6 -5
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +22 -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 +3 -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/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +121 -5
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +31 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +3 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +973 -75
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +398 -26
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +80 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +60 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +548 -0
  51. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
  52. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +208 -456
  53. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  54. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  55. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  56. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  57. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  58. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +482 -1
  59. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  60. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
  61. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  62. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  63. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +87 -3
  64. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  65. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  66. package/crates/team-agent/src/lifecycle/launch/spawn.rs +127 -25
  67. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +140 -0
  68. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  69. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  70. package/crates/team-agent/src/lifecycle/launch.rs +107 -29
  71. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  72. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  73. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  74. package/crates/team-agent/src/lifecycle/profile_launch.rs +99 -0
  75. package/crates/team-agent/src/lifecycle/profile_smoke.rs +52 -1
  76. package/crates/team-agent/src/lifecycle/restart/agent.rs +214 -160
  77. package/crates/team-agent/src/lifecycle/restart/common.rs +513 -37
  78. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  79. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  80. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  81. package/crates/team-agent/src/lifecycle/restart/remove.rs +233 -6
  82. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  83. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  84. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  85. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  86. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
  87. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  88. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  89. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  90. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  91. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +464 -0
  92. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +247 -0
  93. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  94. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
  95. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +787 -0
  96. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  97. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  98. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +485 -0
  99. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  100. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  101. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  102. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  103. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  104. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  105. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  106. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  107. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  108. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
  109. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
  110. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
  111. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
  112. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
  113. package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
  114. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
  115. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -0
  116. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  117. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  118. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  119. package/crates/team-agent/src/lifecycle/tests.rs +30 -13
  120. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  121. package/crates/team-agent/src/lifecycle/worker_command_context.rs +143 -12
  122. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  123. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  124. package/crates/team-agent/src/messaging/delivery.rs +430 -41
  125. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  126. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  127. package/crates/team-agent/src/messaging/results.rs +4 -0
  128. package/crates/team-agent/src/messaging/send.rs +34 -0
  129. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  130. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  131. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  132. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  133. package/crates/team-agent/src/messaging/types.rs +4 -1
  134. package/crates/team-agent/src/model/enums.rs +17 -6
  135. package/crates/team-agent/src/model/permissions.rs +3 -0
  136. package/crates/team-agent/src/os_probe.rs +73 -2
  137. package/crates/team-agent/src/provider/adapter.rs +259 -6
  138. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +89 -0
  139. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  140. package/crates/team-agent/src/provider/adapters/mod.rs +3 -0
  141. package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
  142. package/crates/team-agent/src/provider/bypass_flags.rs +47 -2
  143. package/crates/team-agent/src/provider/classify.rs +4 -2
  144. package/crates/team-agent/src/provider/faults.rs +2 -1
  145. package/crates/team-agent/src/provider/mod.rs +11 -0
  146. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  147. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  148. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  149. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  150. package/crates/team-agent/src/provider/session/context_fork.rs +78 -3
  151. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  152. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  153. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  154. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  155. package/crates/team-agent/src/provider/session_scan/common.rs +205 -2
  156. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  157. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  158. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  159. package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
  160. package/crates/team-agent/src/provider/session_scan.rs +12 -0
  161. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  162. package/crates/team-agent/src/provider/tests/adapter.rs +384 -0
  163. package/crates/team-agent/src/provider/types.rs +4 -2
  164. package/crates/team-agent/src/provider/wire.rs +29 -1
  165. package/crates/team-agent/src/state/persist.rs +301 -8
  166. package/crates/team-agent/src/state/repository.rs +5 -0
  167. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  168. package/crates/team-agent/src/tmux_backend.rs +1116 -75
  169. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  170. package/crates/team-agent/src/transport.rs +248 -1
  171. package/npm/install.mjs +129 -73
  172. package/package.json +4 -4
  173. package/skills/team-agent/SKILL.md +32 -239
  174. package/skills/team-agent/command-coverage.json +31 -0
  175. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  176. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  177. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  178. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -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,56 @@ 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_single_enter = recipient_requires_single_enter(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_single_enter, || {
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()
596
+ && recipient_allows_explicit_queue_flush(state, &message.recipient)
597
+ {
598
+ match crate::provider::submit_now::flush_explicit_queue(transport, &target) {
599
+ Ok(flush) if flush.extra_enters > 0 => {
600
+ let _ = event_log.write(
601
+ "send.grok_send_now",
602
+ serde_json::json!({
603
+ "message_id": message_id,
604
+ "recipient": message.recipient,
605
+ "extra_enters": flush.extra_enters,
606
+ "mark_cleared": flush.mark_cleared,
607
+ }),
608
+ );
609
+ }
610
+ Ok(_) => {}
611
+ Err(error) => {
612
+ let _ = event_log.write(
613
+ "send.grok_send_now_failed",
614
+ serde_json::json!({
615
+ "message_id": message_id,
616
+ "recipient": message.recipient,
617
+ "error": error.to_string(),
618
+ }),
619
+ );
620
+ }
621
+ }
622
+ }
623
+ report
624
+ }
514
625
  Err(error) => {
515
626
  let reason = format!("inject_failed:{error}");
516
627
  if message.recipient == "leader" {
@@ -538,6 +649,7 @@ pub fn deliver_pending_message(
538
649
  reason: Some(DeliveryRefusal::LeaderNotAttached),
539
650
  channel: Some("rebind_required".to_string()),
540
651
  ack_forced_off: false,
652
+ turn_verification: None,
541
653
  });
542
654
  }
543
655
  if transport_error_is_target_missing(&error) {
@@ -582,6 +694,7 @@ pub fn deliver_pending_message(
582
694
  reason: None,
583
695
  channel: None,
584
696
  ack_forced_off: false,
697
+ turn_verification: None,
585
698
  });
586
699
  }
587
700
  store.mark(message_id, "target_resolved", Some(&reason))?;
@@ -595,12 +708,26 @@ pub fn deliver_pending_message(
595
708
  reason: None,
596
709
  channel: None,
597
710
  ack_forced_off: false,
711
+ turn_verification: None,
598
712
  });
599
713
  }
600
714
  };
601
715
  if let Some(error) = submit_observer.take_error() {
602
- return Err(error);
716
+ // Token on pane/transcript = physical Enter already out → degrade.
717
+ // Poison before submit (no token) still returns Err → item_blocked.
718
+ absorb_state_error_only_after_physical_submit(
719
+ transport,
720
+ state,
721
+ &message.recipient,
722
+ &target,
723
+ message_id,
724
+ event_log,
725
+ "submit_observer",
726
+ error,
727
+ )?;
603
728
  }
729
+ persist_submit_verification(workspace, message_id, &inject_report);
730
+ let turn_verification = inject_report.turn_verification;
604
731
  let submit_verified = inject_submit_verified(&inject_report);
605
732
  let readback_verified = pane_readback_verified(&inject_report);
606
733
  // A successful tmux command is never provider-acceptance proof. Leader and
@@ -655,6 +782,7 @@ pub fn deliver_pending_message(
655
782
  reason: None,
656
783
  channel: None,
657
784
  ack_forced_off: false,
785
+ turn_verification: Some(turn_verification),
658
786
  });
659
787
  }
660
788
  store.mark(message_id, "submitted_unverified", Some(&reason))?;
@@ -668,6 +796,7 @@ pub fn deliver_pending_message(
668
796
  reason: None,
669
797
  channel: None,
670
798
  ack_forced_off: false,
799
+ turn_verification: Some(turn_verification),
671
800
  });
672
801
  }
673
802
  // S1-CAPTURE-001 (0.4.8, CR M4 Claude phase-1): for Claude/ClaudeCode
@@ -733,6 +862,7 @@ pub fn deliver_pending_message(
733
862
  reason: None,
734
863
  channel: None,
735
864
  ack_forced_off: false,
865
+ turn_verification: Some(turn_verification),
736
866
  });
737
867
  }
738
868
  }
@@ -745,7 +875,9 @@ pub fn deliver_pending_message(
745
875
  "leader_receiver.receipt_source_unavailable",
746
876
  serde_json::json!({"message_id": message_id}),
747
877
  )?;
748
- return Ok(leader_receipt_source_unavailable_outcome(message_id));
878
+ let mut outcome = leader_receipt_source_unavailable_outcome(message_id);
879
+ outcome.turn_verification = Some(turn_verification);
880
+ return Ok(outcome);
749
881
  }
750
882
  LeaderReceiptObservation::TokenAbsent => {
751
883
  store.mark(message_id, "submitted_pending_acceptance", None)?;
@@ -756,10 +888,10 @@ pub fn deliver_pending_message(
756
888
  "reason": "provider_receipt_not_observed",
757
889
  }),
758
890
  )?;
759
- return Ok(leader_acceptance_pending_outcome(
760
- message_id,
761
- "submitted_pending_acceptance",
762
- ));
891
+ let mut outcome =
892
+ leader_acceptance_pending_outcome(message_id, "submitted_pending_acceptance");
893
+ outcome.turn_verification = Some(turn_verification);
894
+ return Ok(outcome);
763
895
  }
764
896
  LeaderReceiptObservation::TokenObserved => {}
765
897
  }
@@ -782,14 +914,26 @@ pub fn deliver_pending_message(
782
914
  reason: None,
783
915
  channel: None,
784
916
  ack_forced_off: false,
917
+ turn_verification: Some(turn_verification),
785
918
  };
786
- stamp_first_send_at_if_leader_to_worker_scoped(
919
+ if let Err(error) = stamp_first_send_at_if_leader_to_worker_scoped(
787
920
  workspace,
788
921
  &message.sender,
789
922
  &message.recipient,
790
923
  canonical_owner_team_id.as_deref(),
791
- )?;
792
- record_turn_open_if_leader_to_worker_scoped(
924
+ ) {
925
+ absorb_state_error_only_after_physical_submit(
926
+ transport,
927
+ state,
928
+ &message.recipient,
929
+ &target,
930
+ message_id,
931
+ event_log,
932
+ "stamp_first_send_at",
933
+ error,
934
+ )?;
935
+ }
936
+ if let Err(error) = record_turn_open_if_leader_to_worker_scoped(
793
937
  workspace,
794
938
  &message.sender,
795
939
  &message.recipient,
@@ -797,7 +941,18 @@ pub fn deliver_pending_message(
797
941
  event_log,
798
942
  canonical_owner_team_id.as_deref(),
799
943
  resolved.metadata.as_ref(),
800
- )?;
944
+ ) {
945
+ absorb_state_error_only_after_physical_submit(
946
+ transport,
947
+ state,
948
+ &message.recipient,
949
+ &target,
950
+ message_id,
951
+ event_log,
952
+ "turn_open_after_delivery",
953
+ error,
954
+ )?;
955
+ }
801
956
  Ok(outcome)
802
957
  }
803
958
 
@@ -864,6 +1019,7 @@ pub(crate) fn mark_worker_target_missing(
864
1019
  reason: Some(DeliveryRefusal::TmuxTargetMissing),
865
1020
  channel: Some("delivery_blocked".to_string()),
866
1021
  ack_forced_off: false,
1022
+ turn_verification: None,
867
1023
  })
868
1024
  }
869
1025
 
@@ -904,6 +1060,27 @@ pub fn requeue_worker_target_missing_messages(
904
1060
  Ok(ids)
905
1061
  }
906
1062
 
1063
+ /// 把 post-Enter 消费判定落到 workspace,供确认面扫盘。
1064
+ ///
1065
+ /// //! purpose: persist SubmitVerification after inject
1066
+ /// //! contract: file contains Debug variant name (SubmitConsumptionUnverified / EnterSentWithoutPlaceholderCheck)
1067
+ /// //! boundary: write failure must not fail delivery
1068
+ fn persist_submit_verification(workspace: &Path, message_id: &str, report: &InjectReport) {
1069
+ let dir = workspace.join(".team").join("runtime");
1070
+ if std::fs::create_dir_all(&dir).is_err() {
1071
+ return;
1072
+ }
1073
+ let body = serde_json::json!({
1074
+ "message_id": message_id,
1075
+ "submit_verification": format!("{:?}", report.submit_verification),
1076
+ "inject_verification": format!("{:?}", report.inject_verification),
1077
+ "turn_verification": turn_verification_wire(report.turn_verification),
1078
+ "submit_verified": inject_submit_verified(report),
1079
+ "attempts": report.attempts,
1080
+ });
1081
+ let _ = std::fs::write(dir.join("submit-verification.json"), format!("{body}\n"));
1082
+ }
1083
+
907
1084
  /// 0.3.27: promoted to pub(crate) for leader_receiver.rs verification gate.
908
1085
  pub(crate) fn inject_submit_verified(report: &InjectReport) -> bool {
909
1086
  match report.submit_verification {
@@ -1018,6 +1195,7 @@ fn deliver_leader_via_app_server(
1018
1195
  reason: None,
1019
1196
  channel: Some("codex_app_server".to_string()),
1020
1197
  ack_forced_off: false,
1198
+ turn_verification: None,
1021
1199
  })
1022
1200
  }
1023
1201
  Err(error) => app_server_delivery_failure(
@@ -1064,6 +1242,7 @@ fn app_server_delivery_failure(
1064
1242
  reason: Some(DeliveryRefusal::RecipientBusy),
1065
1243
  channel: Some("leader_busy".to_string()),
1066
1244
  ack_forced_off: false,
1245
+ turn_verification: None,
1067
1246
  })
1068
1247
  }
1069
1248
  crate::codex_app_server::AppServerError::ThreadStale { expected, actual } => {
@@ -1103,6 +1282,7 @@ fn app_server_delivery_failure(
1103
1282
  reason: Some(DeliveryRefusal::MissingPermissions),
1104
1283
  channel: Some("codex_app_server".to_string()),
1105
1284
  ack_forced_off: false,
1285
+ turn_verification: None,
1106
1286
  })
1107
1287
  }
1108
1288
  crate::codex_app_server::AppServerError::ProtocolMismatch(_)
@@ -1155,6 +1335,7 @@ fn rebind_required_outcome(message_id: &str, action: &str) -> DeliveryOutcome {
1155
1335
  reason: Some(DeliveryRefusal::LeaderNotAttached),
1156
1336
  channel: Some("rebind_required".to_string()),
1157
1337
  ack_forced_off: false,
1338
+ turn_verification: None,
1158
1339
  }
1159
1340
  }
1160
1341
 
@@ -1558,6 +1739,7 @@ fn leader_receiver_transport_conflict_outcome(
1558
1739
  reason: Some(DeliveryRefusal::LeaderNotAttached),
1559
1740
  channel: Some("rebind_required".to_string()),
1560
1741
  ack_forced_off: false,
1742
+ turn_verification: None,
1561
1743
  }));
1562
1744
  }
1563
1745
  }
@@ -1973,23 +2155,6 @@ fn message_for_delivery(
1973
2155
  Ok(message)
1974
2156
  }
1975
2157
 
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
2158
  /// Pre-inject gate (Contract B): peek the recipient pane and answer "is there an
1994
2159
  /// actionable provider startup prompt right now (trust menu or update prompt)" using
1995
2160
  /// the SHARED provider/startup_prompt recognizers — no second classifier, no provider
@@ -2058,11 +2223,67 @@ fn recipient_pane_has_actionable_startup_prompt(
2058
2223
  crate::provider::classify_copilot_startup_screen(&captured),
2059
2224
  crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
2060
2225
  ),
2061
- Provider::GeminiCli | Provider::Fake => false,
2226
+ Provider::Pi
2227
+ | Provider::Grok
2228
+ | Provider::CursorAgent
2229
+ | Provider::GeminiCli
2230
+ | Provider::Fake => false,
2062
2231
  }
2063
2232
  }
2064
2233
 
2065
- fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
2234
+ fn recipient_provider(state: &serde_json::Value, recipient: &str) -> Option<Provider> {
2235
+ state
2236
+ .get("agents")
2237
+ .and_then(serde_json::Value::as_object)
2238
+ .and_then(|agents| agents.get(recipient))
2239
+ .and_then(|agent| agent.get("provider"))
2240
+ .and_then(serde_json::Value::as_str)
2241
+ .and_then(parse_canonical_provider)
2242
+ }
2243
+
2244
+ /// ---
2245
+ /// purpose: paste→Enter 地板按收件人 provider 选择
2246
+ /// contract: CursorAgent 与 Grok 为 1s;其余(含 claude)为 ZERO
2247
+ /// boundary: 不改 cursor 单回车闸;不把地板套到 claude/codex
2248
+ /// ---
2249
+ pub(crate) fn paste_to_submit_floor_for_recipient(
2250
+ state: &serde_json::Value,
2251
+ recipient: &str,
2252
+ ) -> Duration {
2253
+ match recipient_provider(state, recipient) {
2254
+ Some(Provider::CursorAgent) | Some(Provider::Grok) => {
2255
+ crate::tmux_backend::CURSOR_PASTE_TO_SUBMIT_FLOOR
2256
+ }
2257
+ _ => Duration::ZERO,
2258
+ }
2259
+ }
2260
+
2261
+ /// ---
2262
+ /// purpose: 判断收件 provider 是否必须禁止 submit observer 重按 Enter
2263
+ /// returns: CursorAgent 与 Pi 为 true,其余为 false
2264
+ /// ---
2265
+ pub(crate) fn recipient_requires_single_enter(state: &serde_json::Value, recipient: &str) -> bool {
2266
+ matches!(
2267
+ recipient_provider(state, recipient),
2268
+ Some(Provider::CursorAgent | Provider::Pi)
2269
+ )
2270
+ }
2271
+
2272
+ /// ---
2273
+ /// purpose: 判断收件 provider 是否允许 Grok send-now 显式队列 flush
2274
+ /// returns: CursorAgent 与 Pi 为 false,其余为 true
2275
+ /// ---
2276
+ pub(crate) fn recipient_allows_explicit_queue_flush(
2277
+ state: &serde_json::Value,
2278
+ recipient: &str,
2279
+ ) -> bool {
2280
+ !matches!(
2281
+ recipient_provider(state, recipient),
2282
+ Some(Provider::CursorAgent | Provider::Pi)
2283
+ )
2284
+ }
2285
+
2286
+ pub(crate) fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
2066
2287
  state
2067
2288
  .get("agents")
2068
2289
  .and_then(serde_json::Value::as_object)
@@ -2129,6 +2350,7 @@ fn leader_acceptance_pending_outcome(message_id: &str, status: &str) -> Delivery
2129
2350
  reason: None,
2130
2351
  channel: Some("leader_acceptance_pending".to_string()),
2131
2352
  ack_forced_off: false,
2353
+ turn_verification: None,
2132
2354
  }
2133
2355
  }
2134
2356
 
@@ -2143,6 +2365,7 @@ pub(crate) fn leader_receipt_source_unavailable_outcome(message_id: &str) -> Del
2143
2365
  reason: None,
2144
2366
  channel: Some("leader_receipt_source_unavailable".to_string()),
2145
2367
  ack_forced_off: false,
2368
+ turn_verification: None,
2146
2369
  }
2147
2370
  }
2148
2371
 
@@ -2184,6 +2407,7 @@ fn observe_pending_leader_acceptance(
2184
2407
  reason: None,
2185
2408
  channel: Some("leader_receiver".to_string()),
2186
2409
  ack_forced_off: false,
2410
+ turn_verification: None,
2187
2411
  })
2188
2412
  }
2189
2413
 
@@ -2214,6 +2438,7 @@ pub fn handle_trust_retry_needed(
2214
2438
  reason: None,
2215
2439
  channel: None,
2216
2440
  ack_forced_off: false,
2441
+ turn_verification: None,
2217
2442
  });
2218
2443
  }
2219
2444
  let next_attempt = payload.attempt.saturating_add(1);
@@ -2254,6 +2479,7 @@ pub fn handle_trust_retry_needed(
2254
2479
  reason: None,
2255
2480
  channel: None,
2256
2481
  ack_forced_off: false,
2482
+ turn_verification: None,
2257
2483
  })
2258
2484
  }
2259
2485
 
@@ -2751,6 +2977,7 @@ fn refuse_owner_team_resolution(
2751
2977
  reason: Some(refusal),
2752
2978
  channel: Some("owner_team_resolution".to_string()),
2753
2979
  ack_forced_off: false,
2980
+ turn_verification: None,
2754
2981
  })
2755
2982
  }
2756
2983
 
@@ -2808,6 +3035,7 @@ pub fn retry_injection_after_trust_auto_answer(
2808
3035
  reason: None,
2809
3036
  channel: None,
2810
3037
  ack_forced_off: false,
3038
+ turn_verification: None,
2811
3039
  })
2812
3040
  }
2813
3041
 
@@ -2844,6 +3072,134 @@ fn rollout_tail_contains(path: &std::path::Path, needle: &str, tail_bytes: u64)
2844
3072
  rollout_tail_contains_result(path, needle, tail_bytes).unwrap_or(false)
2845
3073
  }
2846
3074
 
3075
+ fn message_token_marker(message_id: &str) -> String {
3076
+ format!("[team-agent-token:{message_id}]")
3077
+ }
3078
+
3079
+ fn text_contains_message_token(text: &str, message_id: &str) -> bool {
3080
+ !message_id.is_empty() && text.contains(&message_token_marker(message_id))
3081
+ }
3082
+
3083
+ /// ---
3084
+ /// purpose: 同 message_id 已在 pane 或转录出现则视为已注入
3085
+ /// contract:
3086
+ /// provides:
3087
+ /// - name: token_already_visible
3088
+ /// what: capture Tail(80) 或 recipient rollout_path 尾含 `[team-agent-token:{id}]` 则为 true
3089
+ /// boundary:
3090
+ /// - capture 失败不当已见(倒向允许注入,避免丢信)
3091
+ /// - 不请求 Full(wave-2:滚出屏的 trust 残渣不当成已见/可行动)
3092
+ /// - 不把 JSON 字段里的 message_id 当 pane token
3093
+ /// ---
3094
+ pub(crate) fn token_already_visible(
3095
+ transport: &dyn Transport,
3096
+ state: &serde_json::Value,
3097
+ recipient: &str,
3098
+ target: &Target,
3099
+ message_id: &str,
3100
+ ) -> bool {
3101
+ if message_id.is_empty() {
3102
+ return false;
3103
+ }
3104
+ let marker = message_token_marker(message_id);
3105
+ match transport.capture(target, CaptureRange::Tail(80)) {
3106
+ Ok(captured) if text_contains_message_token(&captured.text, message_id) => return true,
3107
+ _ => {}
3108
+ }
3109
+ recipient_transcript_contains(state, recipient, &marker)
3110
+ }
3111
+
3112
+ fn recipient_transcript_contains(state: &serde_json::Value, recipient: &str, marker: &str) -> bool {
3113
+ for path in recipient_transcript_paths(state, recipient) {
3114
+ if rollout_tail_contains(&path, marker, 64 * 1024) {
3115
+ return true;
3116
+ }
3117
+ }
3118
+ false
3119
+ }
3120
+
3121
+ fn recipient_transcript_paths(
3122
+ state: &serde_json::Value,
3123
+ recipient: &str,
3124
+ ) -> Vec<std::path::PathBuf> {
3125
+ let mut paths = Vec::new();
3126
+ let push = |paths: &mut Vec<std::path::PathBuf>, value: Option<&serde_json::Value>| {
3127
+ if let Some(path) = value
3128
+ .and_then(serde_json::Value::as_str)
3129
+ .filter(|p| !p.is_empty())
3130
+ {
3131
+ paths.push(std::path::PathBuf::from(path));
3132
+ }
3133
+ };
3134
+ if recipient == "leader" {
3135
+ push(
3136
+ &mut paths,
3137
+ leader_receiver_record_path(state, "rollout_path"),
3138
+ );
3139
+ }
3140
+ let agent = state.get("agents").and_then(|agents| agents.get(recipient));
3141
+ push(
3142
+ &mut paths,
3143
+ agent.and_then(|agent| agent.get("rollout_path")),
3144
+ );
3145
+ paths
3146
+ }
3147
+
3148
+ fn leader_receiver_record_path<'a>(
3149
+ state: &'a serde_json::Value,
3150
+ field: &str,
3151
+ ) -> Option<&'a serde_json::Value> {
3152
+ state
3153
+ .get("leader_receiver")
3154
+ .and_then(|receiver| receiver.get(field))
3155
+ .or_else(|| {
3156
+ let active = state
3157
+ .get("active_team_key")
3158
+ .and_then(serde_json::Value::as_str)?;
3159
+ state
3160
+ .get("teams")
3161
+ .and_then(serde_json::Value::as_object)
3162
+ .and_then(|teams| teams.get(active))
3163
+ .and_then(|team| team.get("leader_receiver"))
3164
+ .and_then(|receiver| receiver.get(field))
3165
+ })
3166
+ }
3167
+
3168
+ fn absorb_state_error_only_after_physical_submit(
3169
+ transport: &dyn Transport,
3170
+ state: &serde_json::Value,
3171
+ recipient: &str,
3172
+ target: &Target,
3173
+ message_id: &str,
3174
+ event_log: &EventLog,
3175
+ stage: &str,
3176
+ error: MessagingError,
3177
+ ) -> Result<(), MessagingError> {
3178
+ if token_already_visible(transport, state, recipient, target, message_id) {
3179
+ log_post_physical_submit_state_error(event_log, message_id, stage, &error)
3180
+ } else {
3181
+ Err(error)
3182
+ }
3183
+ }
3184
+
3185
+ fn log_post_physical_submit_state_error(
3186
+ event_log: &EventLog,
3187
+ message_id: &str,
3188
+ stage: &str,
3189
+ error: &MessagingError,
3190
+ ) -> Result<(), MessagingError> {
3191
+ event_log.write(
3192
+ "delivery.post_submit_state_degraded",
3193
+ serde_json::json!({
3194
+ "message_id": message_id,
3195
+ "stage": stage,
3196
+ "error": error.to_string(),
3197
+ "physical_submit": true,
3198
+ }),
3199
+ )?;
3200
+ Ok(())
3201
+ }
3202
+
2847
3203
  fn rollout_tail_contains_result(
2848
3204
  path: &std::path::Path,
2849
3205
  needle: &str,
@@ -2860,3 +3216,36 @@ fn rollout_tail_contains_result(
2860
3216
  let haystack = String::from_utf8_lossy(&buf);
2861
3217
  Ok(haystack.contains(needle))
2862
3218
  }
3219
+
3220
+ #[cfg(test)]
3221
+ mod paste_floor_tests {
3222
+ use super::*;
3223
+ use std::time::Duration;
3224
+
3225
+ fn state_with_provider(provider: &str) -> serde_json::Value {
3226
+ serde_json::json!({
3227
+ "agents": { "w1": { "provider": provider } }
3228
+ })
3229
+ }
3230
+
3231
+ #[test]
3232
+ fn grok_paste_to_submit_floor_matches_cursor() {
3233
+ let grok = paste_to_submit_floor_for_recipient(&state_with_provider("grok"), "w1");
3234
+ let cursor = paste_to_submit_floor_for_recipient(&state_with_provider("cursor_agent"), "w1");
3235
+ assert_eq!(grok, crate::tmux_backend::CURSOR_PASTE_TO_SUBMIT_FLOOR);
3236
+ assert_eq!(cursor, crate::tmux_backend::CURSOR_PASTE_TO_SUBMIT_FLOOR);
3237
+ assert_eq!(grok, cursor);
3238
+ }
3239
+
3240
+ #[test]
3241
+ fn claude_and_codex_paste_to_submit_floor_stays_zero() {
3242
+ assert_eq!(
3243
+ paste_to_submit_floor_for_recipient(&state_with_provider("claude"), "w1"),
3244
+ Duration::ZERO
3245
+ );
3246
+ assert_eq!(
3247
+ paste_to_submit_floor_for_recipient(&state_with_provider("codex"), "w1"),
3248
+ Duration::ZERO
3249
+ );
3250
+ }
3251
+ }