@team-agent/installer 0.5.50 → 0.5.52

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 (93) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +46 -115
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
  25. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  26. package/crates/team-agent/src/cli/types.rs +1 -0
  27. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  29. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  30. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  31. package/crates/team-agent/src/db/message_store.rs +154 -44
  32. package/crates/team-agent/src/event_log.rs +73 -0
  33. package/crates/team-agent/src/leader/lease.rs +202 -16
  34. package/crates/team-agent/src/leader/start.rs +28 -4
  35. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  36. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
  37. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  38. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  39. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  41. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  42. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  43. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  44. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  45. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  46. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  48. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  49. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  50. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  52. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  53. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  54. package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
  55. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  56. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
  57. package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
  58. package/crates/team-agent/src/lifecycle/restart.rs +26 -2
  59. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
  60. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
  61. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  62. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  64. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  65. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  66. package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
  67. package/crates/team-agent/src/mcp_server/tests.rs +11 -4
  68. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  69. package/crates/team-agent/src/messaging/activity.rs +4 -2
  70. package/crates/team-agent/src/messaging/address.rs +86 -0
  71. package/crates/team-agent/src/messaging/delivery.rs +288 -183
  72. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  73. package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
  74. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  75. package/crates/team-agent/src/messaging/mod.rs +15 -2
  76. package/crates/team-agent/src/messaging/persist.rs +309 -0
  77. package/crates/team-agent/src/messaging/results.rs +16 -24
  78. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  79. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  80. package/crates/team-agent/src/messaging/send.rs +101 -49
  81. package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
  84. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  85. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  86. package/crates/team-agent/src/redaction.rs +72 -2
  87. package/crates/team-agent/src/state/persist.rs +284 -21
  88. package/crates/team-agent/src/state/projection.rs +245 -30
  89. package/crates/team-agent/src/state/repository/intent.rs +13 -0
  90. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  91. package/crates/team-agent/src/state/repository.rs +94 -27
  92. package/crates/team-agent/src/state/selector.rs +9 -18
  93. package/package.json +4 -4
@@ -45,20 +45,28 @@ pub fn deliver_stored_message(
45
45
  ) -> Result<DeliveryOutcome, MessagingError> {
46
46
  let _ = (wait_visible, timeout);
47
47
  let recipient = target.unwrap_or("leader");
48
- let store = MessageStore::open(workspace)?;
49
- let message_id = store.create_message(
48
+ let super::PersistResolution::Persisted(persisted) = super::persist::persist_internal_send(
49
+ workspace,
50
+ super::InternalSendKind::Delivery,
51
+ team.map(TeamKey::as_str),
50
52
  task_id.map(crate::model::ids::TaskId::as_str),
51
53
  sender,
52
54
  recipient,
53
55
  content,
54
56
  None,
55
57
  requires_ack,
56
- team.map(TeamKey::as_str),
57
- )?;
58
+ None,
59
+ super::InitialDisposition::Accepted,
60
+ )?
61
+ else {
62
+ unreachable!("internal delivery does not accept caller-supplied ids")
63
+ };
64
+ let message_status = persisted.row_status.as_str().to_string();
65
+ let message_id = persisted.message_id;
58
66
  Ok(DeliveryOutcome {
59
67
  ok: true,
60
68
  status: DeliveryStatus::Queued,
61
- message_status: MessageStatusShadow("accepted".to_string()),
69
+ message_status: MessageStatusShadow(message_status),
62
70
  message_id: Some(message_id),
63
71
  verification: None,
64
72
  stage: None,
@@ -67,6 +75,17 @@ pub fn deliver_stored_message(
67
75
  })
68
76
  }
69
77
 
78
+ pub fn deliver_persisted_message(
79
+ workspace: &Path,
80
+ transport: &dyn Transport,
81
+ message_id: &str,
82
+ event_log: &EventLog,
83
+ state: &serde_json::Value,
84
+ ) -> Result<DeliveryOutcome, MessagingError> {
85
+ let store = MessageStore::open(workspace)?;
86
+ deliver_pending_message(workspace, &store, transport, message_id, event_log, state)
87
+ }
88
+
70
89
  // ===========================================================================
71
90
  // delivery.py — 单条 tmux 注入投递 + trust 有界重试 + turn-open arm (card §16)
72
91
  // ===========================================================================
@@ -200,6 +219,47 @@ pub fn deliver_pending_message(
200
219
  }
201
220
  _ => state,
202
221
  };
222
+ if message.recipient == "leader" && message.status == "submitted_pending_acceptance" {
223
+ return observe_pending_leader_acceptance(store, event_log, state, message_id);
224
+ }
225
+ if message.recipient == "leader" && message.status == "queued_until_leader_attach" {
226
+ let Some(receiver) = leader_receiver_value(state) else {
227
+ return Ok(leader_acceptance_pending_outcome(
228
+ message_id,
229
+ "queued_until_leader_attach",
230
+ ));
231
+ };
232
+ let channel_transport =
233
+ delivery_transport_for_recipient(workspace, transport, state, "leader");
234
+ let resolution = crate::messaging::resolve_live_leader_channel(
235
+ workspace,
236
+ receiver,
237
+ channel_transport.as_transport(),
238
+ );
239
+ let crate::messaging::LeaderChannelResolution::Live(channel) = resolution else {
240
+ return Ok(leader_acceptance_pending_outcome(
241
+ message_id,
242
+ "queued_until_leader_attach",
243
+ ));
244
+ };
245
+ let metadata_drift = match &channel {
246
+ crate::messaging::LiveLeaderChannel::DirectTmux(channel) => {
247
+ channel.metadata_drift.clone()
248
+ }
249
+ crate::messaging::LiveLeaderChannel::CodexAppServer(_) => Vec::new(),
250
+ };
251
+ store.mark(message_id, "accepted", None)?;
252
+ event_log.write(
253
+ "leader_receiver.mailbox_requeued",
254
+ serde_json::json!({
255
+ "message_id": message_id,
256
+ "channel_live": true,
257
+ "metadata_drift": metadata_drift,
258
+ "requeue_trigger": "delivery_revalidation",
259
+ "revalidated_at": chrono::Utc::now().to_rfc3339(),
260
+ }),
261
+ )?;
262
+ }
203
263
  let attempt = if store.claim_for_delivery(message_id)? {
204
264
  message.delivery_attempts.saturating_add(1)
205
265
  } else if message.status == "target_resolved" && message.error.is_some() {
@@ -217,88 +277,108 @@ pub fn deliver_pending_message(
217
277
  });
218
278
  };
219
279
  if message.recipient == "leader" {
220
- if let Some(receiver) = leader_receiver_value(state) {
221
- if let Some(outcome) = leader_receiver_transport_conflict_outcome(
280
+ let Some(receiver) = leader_receiver_value(state) else {
281
+ store.mark(message_id, "failed", Some("leader_not_attached"))?;
282
+ event_log.write(
283
+ "leader_receiver.delivery_blocked",
284
+ serde_json::json!({
285
+ "message_id": message_id,
286
+ "sender": message.sender,
287
+ "reason": "leader_receiver_not_attached",
288
+ "error": "leader_not_attached",
289
+ "channel_reason": "receiver_missing",
290
+ "channel": "rebind_required",
291
+ "action": "run team-agent attach-leader or team-agent takeover",
292
+ "status": "unbound",
293
+ }),
294
+ )?;
295
+ return Ok(DeliveryOutcome {
296
+ ok: false,
297
+ status: DeliveryStatus::Refused,
298
+ message_status: MessageStatusShadow("failed".to_string()),
299
+ message_id: Some(message_id.to_string()),
300
+ verification: Some(
301
+ "run team-agent attach-leader or team-agent takeover".to_string(),
302
+ ),
303
+ stage: None,
304
+ reason: Some(DeliveryRefusal::LeaderNotAttached),
305
+ channel: Some("rebind_required".to_string()),
306
+ });
307
+ };
308
+ if let Some(outcome) = leader_receiver_transport_conflict_outcome(
309
+ store,
310
+ event_log,
311
+ message_id,
312
+ receiver,
313
+ &message.sender,
314
+ )? {
315
+ return Ok(outcome);
316
+ }
317
+ let channel_transport =
318
+ delivery_transport_for_recipient(workspace, transport, state, "leader");
319
+ match crate::messaging::resolve_live_leader_channel(
320
+ workspace,
321
+ receiver,
322
+ channel_transport.as_transport(),
323
+ ) {
324
+ crate::messaging::LeaderChannelResolution::Live(_) => {}
325
+ crate::messaging::LeaderChannelResolution::ProbeFailed(error) => {
326
+ store.mark(message_id, "target_resolved", Some(&error))?;
327
+ return Ok(DeliveryOutcome {
328
+ ok: false,
329
+ status: DeliveryStatus::Degraded,
330
+ message_status: MessageStatusShadow("target_resolved".to_string()),
331
+ message_id: Some(message_id.to_string()),
332
+ verification: Some(error),
333
+ stage: None,
334
+ reason: None,
335
+ channel: Some("rebind_required".to_string()),
336
+ });
337
+ }
338
+ crate::messaging::LeaderChannelResolution::Unbound(reason) => {
339
+ let receiver_status = receiver
340
+ .get("status")
341
+ .and_then(serde_json::Value::as_str)
342
+ .unwrap_or("unbound");
343
+ store.mark(message_id, "failed", Some("leader_not_attached"))?;
344
+ event_log.write(
345
+ "leader_receiver.delivery_blocked",
346
+ serde_json::json!({
347
+ "message_id": message_id,
348
+ "sender": message.sender,
349
+ "reason": "leader_receiver_not_attached",
350
+ "error": "leader_not_attached",
351
+ "channel_reason": format!("{reason:?}"),
352
+ "channel": "rebind_required",
353
+ "action": "run team-agent attach-leader or team-agent takeover",
354
+ "status": receiver_status,
355
+ }),
356
+ )?;
357
+ return Ok(DeliveryOutcome {
358
+ ok: false,
359
+ status: DeliveryStatus::Refused,
360
+ message_status: MessageStatusShadow("failed".to_string()),
361
+ message_id: Some(message_id.to_string()),
362
+ verification: Some(
363
+ "run team-agent attach-leader or team-agent takeover".to_string(),
364
+ ),
365
+ stage: None,
366
+ reason: Some(DeliveryRefusal::LeaderNotAttached),
367
+ channel: Some("rebind_required".to_string()),
368
+ });
369
+ }
370
+ }
371
+ if crate::codex_app_server::receiver_is_app_server(receiver) {
372
+ return deliver_leader_via_app_server(
222
373
  store,
223
374
  event_log,
375
+ state,
224
376
  message_id,
225
- receiver,
226
- &message.sender,
227
- )? {
228
- return Ok(outcome);
229
- }
230
- // 0.5.4 gate054 cross-team leak fix: a `leader_receiver.status`
231
- // other than `"attached"` (restart demotes killed-session receivers
232
- // to `"rebind_required"` — lifecycle/restart/rebuild.rs:1311) MUST
233
- // refuse delivery before any pane injection or cross-server pane-id
234
- // probing. Without this gate, a stale receiver on team A's socket
235
- // could otherwise fall back to the same pane id on the main tmux
236
- // server (delivery.rs:1150-1201) and inject A's leader traffic into
237
- // team B's leader pane. See .team/artifacts/0.5.4-gate-crossteam-notify-triage.md.
238
- if let Some(status) = leader_receiver_status(state) {
239
- if status != "attached" {
240
- store.mark(message_id, "failed", Some("leader_not_attached"))?;
241
- event_log.write(
242
- "leader_receiver.delivery_blocked",
243
- serde_json::json!({
244
- "message_id": message_id,
245
- "sender": message.sender,
246
- "reason": "leader_receiver_not_attached",
247
- "channel": "rebind_required",
248
- "action": "run team-agent attach-leader or team-agent takeover",
249
- "status": status,
250
- }),
251
- )?;
252
- return Ok(DeliveryOutcome {
253
- ok: false,
254
- status: DeliveryStatus::Refused,
255
- message_status: MessageStatusShadow("failed".to_string()),
256
- message_id: Some(message_id.to_string()),
257
- verification: Some(
258
- "run team-agent attach-leader or team-agent takeover".to_string(),
259
- ),
260
- stage: None,
261
- reason: Some(DeliveryRefusal::LeaderNotAttached),
262
- channel: Some("rebind_required".to_string()),
263
- });
264
- }
265
- }
266
- if crate::codex_app_server::receiver_is_app_server(receiver) {
267
- return deliver_leader_via_app_server(
268
- store,
269
- event_log,
270
- state,
271
- message_id,
272
- &message,
273
- canonical_owner_team_id.as_deref(),
274
- );
275
- }
377
+ &message,
378
+ canonical_owner_team_id.as_deref(),
379
+ );
276
380
  }
277
381
  }
278
- if message.recipient == "leader" && leader_receiver_has_noncanonical_tmux_socket(state) {
279
- store.mark(message_id, "failed", Some("leader_not_attached"))?;
280
- event_log.write(
281
- "leader_receiver.delivery_blocked",
282
- serde_json::json!({
283
- "message_id": message_id,
284
- "sender": message.sender,
285
- "reason": "leader_not_attached",
286
- "channel": "rebind_required",
287
- "action": "run team-agent claim-leader or team-agent takeover",
288
- "error": "leader_receiver.tmux_socket is not a canonical full socket path",
289
- }),
290
- )?;
291
- return Ok(DeliveryOutcome {
292
- ok: false,
293
- status: DeliveryStatus::Refused,
294
- message_status: MessageStatusShadow("failed".to_string()),
295
- message_id: Some(message_id.to_string()),
296
- verification: Some("run team-agent claim-leader or team-agent takeover".to_string()),
297
- stage: None,
298
- reason: Some(DeliveryRefusal::LeaderNotAttached),
299
- channel: Some("rebind_required".to_string()),
300
- });
301
- }
302
382
  let delivery_transport =
303
383
  delivery_transport_for_recipient(workspace, transport, state, &message.recipient);
304
384
  let transport = delivery_transport.as_transport();
@@ -334,32 +414,6 @@ pub fn deliver_pending_message(
334
414
  });
335
415
  }
336
416
  };
337
- // Do not inject queued leader messages into a synthetic "leader" window.
338
- if message.recipient == "leader"
339
- && !leader_receiver_pane_is_usable(transport, state, &live_targets)
340
- {
341
- store.mark(message_id, "failed", Some("leader_not_attached"))?;
342
- event_log.write(
343
- "leader_receiver.delivery_blocked",
344
- serde_json::json!({
345
- "message_id": message_id,
346
- "sender": message.sender,
347
- "reason": "leader_not_attached",
348
- "channel": "rebind_required",
349
- "action": "run team-agent claim-leader or team-agent takeover",
350
- }),
351
- )?;
352
- return Ok(DeliveryOutcome {
353
- ok: false,
354
- status: DeliveryStatus::Refused,
355
- message_status: MessageStatusShadow("failed".to_string()),
356
- message_id: Some(message_id.to_string()),
357
- verification: Some("run team-agent claim-leader or team-agent takeover".to_string()),
358
- stage: None,
359
- reason: Some(DeliveryRefusal::LeaderNotAttached),
360
- channel: Some("rebind_required".to_string()),
361
- });
362
- }
363
417
  let resolved = resolve_inject_target(state, &message.recipient, transport, &live_targets);
364
418
  if let Some(stale) = resolved.stale_binding.as_ref() {
365
419
  event_log.write(
@@ -524,14 +578,10 @@ pub fn deliver_pending_message(
524
578
  }
525
579
  let submit_verified = inject_submit_verified(&inject_report);
526
580
  let readback_verified = pane_readback_verified(&inject_report);
527
- // Leader pane: inject success is delivery proof. Worker pane: post-submit
528
- // evidence is the delivery proof; stale Phase-1 readback must not veto it
529
- // and cannot independently prove the current Enter submitted.
530
- let verified = if is_leader_recipient {
531
- true
532
- } else {
533
- submit_verified
534
- };
581
+ // A successful tmux command is never provider-acceptance proof. Leader and
582
+ // worker delivery both require post-submit evidence; leader additionally
583
+ // waits for its authoritative provider transcript below.
584
+ let verified = submit_verified;
535
585
  if !verified {
536
586
  let reason = if !readback_verified {
537
587
  "pane_readback_unverified:capture_missing_token".to_string()
@@ -658,7 +708,26 @@ pub fn deliver_pending_message(
658
708
  });
659
709
  }
660
710
  }
661
- store.mark(message_id, "delivered", None)?;
711
+ if is_leader_recipient {
712
+ store.record_delivery_submission(message_id, readback_verified)?;
713
+ if !leader_transcript_has_token(state, message_id) {
714
+ store.mark(message_id, "submitted_pending_acceptance", None)?;
715
+ event_log.write(
716
+ "leader_receiver.acceptance_pending",
717
+ serde_json::json!({
718
+ "message_id": message_id,
719
+ "reason": "provider_receipt_not_observed",
720
+ }),
721
+ )?;
722
+ return Ok(leader_acceptance_pending_outcome(
723
+ message_id,
724
+ "submitted_pending_acceptance",
725
+ ));
726
+ }
727
+ store.mark_delivered_with_receipt(message_id)?;
728
+ } else {
729
+ store.mark(message_id, "delivered", None)?;
730
+ }
662
731
  let mut delivered_event = serde_json::json!({"message_id": message_id});
663
732
  if let Some(metadata) = resolved.metadata.as_ref() {
664
733
  append_target_metadata(&mut delivered_event, metadata);
@@ -908,7 +977,8 @@ fn deliver_leader_via_app_server(
908
977
  );
909
978
  match crate::codex_app_server::submit_to_bound_thread(&binding, message_id, &rendered) {
910
979
  Ok(submit) => {
911
- store.mark(message_id, "delivered", None)?;
980
+ store.record_delivery_submission(message_id, true)?;
981
+ store.mark_delivered_with_receipt(message_id)?;
912
982
  event_log.write(
913
983
  "message.delivered",
914
984
  serde_json::json!({"message_id": message_id}),
@@ -1482,32 +1552,6 @@ fn leader_receiver_pane_binding(state: &serde_json::Value) -> Option<PaneSocketB
1482
1552
  .or_else(|| only_team_entry(state).and_then(leader_receiver_pane_binding_in_state))
1483
1553
  }
1484
1554
 
1485
- /// `state` is "usable" for leader delivery when the bound `leader_receiver.pane_id`
1486
- /// is present and alive (in `live_targets` or `liveness` probe returns not-Dead).
1487
- ///
1488
- /// **0.3.24 excision (U1-A real-machine RED v2)**: the wave-2 session+window drift
1489
- /// fallback was removed. Drift now fails loudly as `leader_not_attached` —
1490
- /// see resolve_inject_target and `.team/artifacts/u1-a-realmachine-v2-fix-or-excise.md`.
1491
- fn leader_receiver_pane_is_usable(
1492
- transport: &dyn Transport,
1493
- state: &serde_json::Value,
1494
- live_targets: &[PaneInfo],
1495
- ) -> bool {
1496
- let Some(pane_id) = leader_receiver_pane_id(state) else {
1497
- return false;
1498
- };
1499
- if live_targets
1500
- .iter()
1501
- .any(|target| target.pane_id.as_str() == pane_id)
1502
- {
1503
- return true;
1504
- }
1505
- !matches!(
1506
- transport.liveness(&PaneId::new(pane_id)),
1507
- Ok(PaneLiveness::Dead)
1508
- )
1509
- }
1510
-
1511
1555
  /// 0.5.x Phase 1d Batch 4: the `Owned` variant now carries a boxed
1512
1556
  /// `Transport` trait object rather than a concrete `TmuxBackend`. All
1513
1557
  /// existing leader-fallback sites still supply a `TmuxBackend` here
@@ -1580,7 +1624,7 @@ fn delivery_transport_for_recipient<'a>(
1580
1624
  // switching transports on a match can cross the team/tmux boundary and
1581
1625
  // inject A's leader traffic into team B's leader pane. If the pane is
1582
1626
  // absent on the recorded endpoint, let the downstream
1583
- // `leader_receiver_pane_is_usable` guard produce `rebind_required`
1627
+ // the shared leader-channel resolver produce `rebind_required`
1584
1628
  // instead of falling back to another server.
1585
1629
  let endpoint_backend = crate::transport_factory::tmux_endpoint_transport(socket);
1586
1630
  DeliveryTransport::Owned(Box::new(endpoint_backend))
@@ -1622,20 +1666,6 @@ fn leader_receiver_tmux_socket(state: &serde_json::Value) -> Option<&str> {
1622
1666
  leader_receiver_field(state, "tmux_socket")
1623
1667
  }
1624
1668
 
1625
- /// 0.5.4 gate054 cross-team leak fix: read the bound `leader_receiver.status`
1626
- /// through the same team-scope resolution as [`leader_receiver_tmux_socket`].
1627
- /// Restart marks demoted receivers as `"rebind_required"`
1628
- /// (lifecycle/restart/rebuild.rs:1311); delivery must refuse before injecting
1629
- /// or cross-server pane-id probing (delivery.rs:1150-1201).
1630
- fn leader_receiver_status(state: &serde_json::Value) -> Option<&str> {
1631
- leader_receiver_field(state, "status")
1632
- }
1633
-
1634
- fn leader_receiver_has_noncanonical_tmux_socket(state: &serde_json::Value) -> bool {
1635
- leader_receiver_tmux_socket(state)
1636
- .is_some_and(|socket| socket != "default" && !std::path::Path::new(socket).is_absolute())
1637
- }
1638
-
1639
1669
  fn leader_receiver_field<'a>(state: &'a serde_json::Value, field: &str) -> Option<&'a str> {
1640
1670
  leader_receiver_field_in_state(state, field)
1641
1671
  .or_else(|| {
@@ -1800,10 +1830,16 @@ pub fn deliver_pending_messages(
1800
1830
  let conn = crate::db::schema::open_db(store.db_path())?;
1801
1831
  let mut stmt = conn.prepare(
1802
1832
  "select message_id from messages
1803
- where status in ('pending', 'accepted', 'target_resolved')
1833
+ where status in (
1834
+ 'pending', 'accepted', 'target_resolved',
1835
+ 'submitted_pending_acceptance', 'queued_until_leader_attach', ?1
1836
+ )
1804
1837
  order by created_at, message_id",
1805
1838
  )?;
1806
- let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1839
+ let rows = stmt.query_map(
1840
+ [crate::message_store::MessageRowStatus::QueuedCoordinatorUnavailable.as_str()],
1841
+ |row| row.get::<_, String>(0),
1842
+ )?;
1807
1843
  rows.collect::<Result<Vec<_>, _>>()?
1808
1844
  };
1809
1845
  let mut delivered = Vec::new();
@@ -1874,7 +1910,7 @@ pub fn deliver_pending_messages(
1874
1910
  continue;
1875
1911
  }
1876
1912
  };
1877
- if outcome.ok {
1913
+ if matches!(outcome.status, DeliveryStatus::Delivered) {
1878
1914
  delivered.push(message_id);
1879
1915
  }
1880
1916
  }
@@ -2017,6 +2053,73 @@ fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
2017
2053
  == Some("busy")
2018
2054
  }
2019
2055
 
2056
+ fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf> {
2057
+ state
2058
+ .get("leader")
2059
+ .and_then(|leader| leader.get("rollout_path"))
2060
+ .and_then(serde_json::Value::as_str)
2061
+ .or_else(|| {
2062
+ leader_receiver_value(state)
2063
+ .and_then(|receiver| receiver.get("rollout_path"))
2064
+ .and_then(serde_json::Value::as_str)
2065
+ })
2066
+ .filter(|path| !path.is_empty())
2067
+ .map(std::path::PathBuf::from)
2068
+ }
2069
+
2070
+ pub(crate) fn leader_transcript_has_token(state: &serde_json::Value, message_id: &str) -> bool {
2071
+ let Some(path) = leader_rollout_path(state) else {
2072
+ return false;
2073
+ };
2074
+ rollout_tail_contains(
2075
+ &path,
2076
+ &format!("[team-agent-token:{message_id}]"),
2077
+ 64 * 1024,
2078
+ )
2079
+ }
2080
+
2081
+ fn leader_acceptance_pending_outcome(message_id: &str, status: &str) -> DeliveryOutcome {
2082
+ DeliveryOutcome {
2083
+ ok: true,
2084
+ status: DeliveryStatus::RetryScheduled,
2085
+ message_status: MessageStatusShadow(status.to_string()),
2086
+ message_id: Some(message_id.to_string()),
2087
+ verification: Some("provider_receipt_not_observed".to_string()),
2088
+ stage: Some(DeliveryStage::Submit),
2089
+ reason: None,
2090
+ channel: Some("leader_acceptance_pending".to_string()),
2091
+ }
2092
+ }
2093
+
2094
+ fn observe_pending_leader_acceptance(
2095
+ store: &MessageStore,
2096
+ event_log: &EventLog,
2097
+ state: &serde_json::Value,
2098
+ message_id: &str,
2099
+ ) -> Result<DeliveryOutcome, MessagingError> {
2100
+ if !leader_transcript_has_token(state, message_id) {
2101
+ return Ok(leader_acceptance_pending_outcome(
2102
+ message_id,
2103
+ "submitted_pending_acceptance",
2104
+ ));
2105
+ }
2106
+ store.mark_delivered_with_receipt(message_id)?;
2107
+ event_log.write(
2108
+ "message.delivered",
2109
+ serde_json::json!({"message_id": message_id}),
2110
+ )?;
2111
+ Ok(DeliveryOutcome {
2112
+ ok: true,
2113
+ status: DeliveryStatus::Delivered,
2114
+ message_status: MessageStatusShadow("delivered".to_string()),
2115
+ message_id: Some(message_id.to_string()),
2116
+ verification: Some("provider_transcript_receipt".to_string()),
2117
+ stage: None,
2118
+ reason: None,
2119
+ channel: Some("leader_receiver".to_string()),
2120
+ })
2121
+ }
2122
+
2020
2123
  /// `_handle_trust_retry_needed` (`delivery.py:221`):trust 应答失败时调度有界退避重试
2021
2124
  /// (`attempt < MAX` → schedule;`>= MAX` → 终态 mark failed + `trust_auto_answer_exhausted`)。
2022
2125
  pub fn handle_trust_retry_needed(
@@ -2341,8 +2444,10 @@ fn save_scoped_state(
2341
2444
  state: &serde_json::Value,
2342
2445
  owner_team_id: Option<&str>,
2343
2446
  ) -> Result<(), MessagingError> {
2344
- if owner_team_id.filter(|team| !team.is_empty()).is_some() {
2345
- if state
2447
+ let scoped_owner_team_id = owner_team_id
2448
+ .filter(|team| !team.is_empty())
2449
+ .filter(|_| {
2450
+ state
2346
2451
  .get("teams")
2347
2452
  .and_then(serde_json::Value::as_object)
2348
2453
  .is_some_and(|teams| {
@@ -2354,14 +2459,13 @@ fn save_scoped_state(
2354
2459
  })
2355
2460
  .is_some_and(|team| teams.contains_key(&team))
2356
2461
  })
2357
- {
2358
- crate::state::projection::save_team_scoped_state(workspace, state)?;
2359
- } else {
2360
- crate::state::persist::save_runtime_state(workspace, state)?;
2361
- }
2362
- } else {
2363
- crate::state::persist::save_runtime_state(workspace, state)?;
2364
- }
2462
+ });
2463
+ crate::state::repository::StateRepository::new(workspace).save(
2464
+ crate::state::repository::StateWriteIntent::MessagingDeliveryState {
2465
+ owner_team_id: scoped_owner_team_id,
2466
+ },
2467
+ state,
2468
+ )?;
2365
2469
  Ok(())
2366
2470
  }
2367
2471
 
@@ -2374,8 +2478,10 @@ fn save_scoped_state_reapplying_after_conflict<F>(
2374
2478
  where
2375
2479
  F: FnOnce(&mut serde_json::Value),
2376
2480
  {
2377
- if owner_team_id.filter(|team| !team.is_empty()).is_some()
2378
- && state
2481
+ let scoped_owner_team_id = owner_team_id
2482
+ .filter(|team| !team.is_empty())
2483
+ .filter(|_| {
2484
+ state
2379
2485
  .get("teams")
2380
2486
  .and_then(serde_json::Value::as_object)
2381
2487
  .is_some_and(|teams| {
@@ -2387,15 +2493,14 @@ where
2387
2493
  })
2388
2494
  .is_some_and(|team| teams.contains_key(&team))
2389
2495
  })
2390
- {
2391
- crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
2392
- workspace, state, reapply,
2393
- )?;
2394
- } else {
2395
- crate::state::persist::save_runtime_state_reapplying_after_conflict(
2396
- workspace, state, reapply,
2397
- )?;
2398
- }
2496
+ });
2497
+ crate::state::repository::StateRepository::new(workspace).save_reapplying(
2498
+ crate::state::repository::StateWriteIntent::MessagingDeliveryState {
2499
+ owner_team_id: scoped_owner_team_id,
2500
+ },
2501
+ state,
2502
+ reapply,
2503
+ )?;
2399
2504
  Ok(())
2400
2505
  }
2401
2506
 
@@ -274,21 +274,25 @@ pub fn fail_leader_delivery(
274
274
  error: Option<&str>,
275
275
  ) -> Result<DeliveryOutcome, MessagingError> {
276
276
  let store = MessageStore::open(workspace)?;
277
- let sender = payload
278
- .get("sender")
279
- .and_then(serde_json::Value::as_str)
280
- .unwrap_or("system");
281
- let content = payload
282
- .get("content")
283
- .and_then(serde_json::Value::as_str)
284
- .unwrap_or("");
285
- let task_id = payload.get("task_id").and_then(serde_json::Value::as_str);
286
- let message_id = match payload
277
+ let Some(message_id) = payload
287
278
  .get("message_id")
288
279
  .and_then(serde_json::Value::as_str)
289
- {
290
- Some(existing) => existing.to_string(),
291
- None => store.create_message(task_id, sender, "leader", content, None, false, None)?,
280
+ .map(ToOwned::to_owned)
281
+ else {
282
+ crate::event_log::EventLog::new(workspace).write(
283
+ "leader_receiver.delivery_failed_without_message",
284
+ serde_json::json!({"reason": serde_json::to_value(reason).ok(), "error": error}),
285
+ )?;
286
+ return Ok(DeliveryOutcome {
287
+ ok: false,
288
+ status: DeliveryStatus::FallbackLog,
289
+ message_status: MessageStatusShadow("failed".to_string()),
290
+ message_id: None,
291
+ verification: None,
292
+ stage: None,
293
+ reason: Some(reason),
294
+ channel: Some("fallback_log".to_string()),
295
+ });
292
296
  };
293
297
  store.mark(&message_id, "failed", error)?;
294
298
  crate::event_log::EventLog::new(workspace).write(