@team-agent/installer 0.5.49 → 0.5.51

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 (90) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +6 -4
  4. package/crates/team-agent/src/cli/emit.rs +91 -39
  5. package/crates/team-agent/src/cli/mod.rs +33 -15
  6. package/crates/team-agent/src/cli/named_address.rs +82 -53
  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 +103 -1308
  13. package/crates/team-agent/src/cli/spec.rs +2 -2
  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/leader_watch.rs +1 -1
  25. package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
  26. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
  27. package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
  28. package/crates/team-agent/src/cli/types.rs +5 -8
  29. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  30. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  31. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  32. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  33. package/crates/team-agent/src/db/message_store.rs +154 -44
  34. package/crates/team-agent/src/event_log.rs +73 -0
  35. package/crates/team-agent/src/leader/start.rs +28 -4
  36. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  37. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
  38. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  39. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  41. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  42. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  43. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  44. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  45. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  46. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  48. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  49. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  50. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  51. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  52. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  53. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  54. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  55. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
  56. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  57. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
  58. package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
  59. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  60. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
  61. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
  62. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  63. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
  64. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  65. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  66. package/crates/team-agent/src/mcp_server/mod.rs +2 -2
  67. package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
  68. package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
  69. package/crates/team-agent/src/mcp_server/tools.rs +26 -15
  70. package/crates/team-agent/src/mcp_server/wire.rs +2 -18
  71. package/crates/team-agent/src/messaging/activity.rs +4 -2
  72. package/crates/team-agent/src/messaging/address.rs +86 -0
  73. package/crates/team-agent/src/messaging/delivery.rs +165 -39
  74. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  75. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  76. package/crates/team-agent/src/messaging/mod.rs +11 -2
  77. package/crates/team-agent/src/messaging/persist.rs +309 -0
  78. package/crates/team-agent/src/messaging/results.rs +16 -24
  79. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  80. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  81. package/crates/team-agent/src/messaging/send.rs +133 -58
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +1 -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 +2 -1
  88. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  89. package/crates/team-agent/src/state/repository.rs +59 -16
  90. 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,22 @@ 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
+ if leader_receiver_status(state) != Some("attached") {
227
+ return Ok(leader_acceptance_pending_outcome(
228
+ message_id,
229
+ "queued_until_leader_attach",
230
+ ));
231
+ }
232
+ store.mark(message_id, "accepted", None)?;
233
+ event_log.write(
234
+ "leader_receiver.mailbox_requeued",
235
+ serde_json::json!({"message_id": message_id}),
236
+ )?;
237
+ }
203
238
  let attempt = if store.claim_for_delivery(message_id)? {
204
239
  message.delivery_attempts.saturating_add(1)
205
240
  } else if message.status == "target_resolved" && message.error.is_some() {
@@ -524,14 +559,10 @@ pub fn deliver_pending_message(
524
559
  }
525
560
  let submit_verified = inject_submit_verified(&inject_report);
526
561
  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
- };
562
+ // A successful tmux command is never provider-acceptance proof. Leader and
563
+ // worker delivery both require post-submit evidence; leader additionally
564
+ // waits for its authoritative provider transcript below.
565
+ let verified = submit_verified;
535
566
  if !verified {
536
567
  let reason = if !readback_verified {
537
568
  "pane_readback_unverified:capture_missing_token".to_string()
@@ -658,7 +689,26 @@ pub fn deliver_pending_message(
658
689
  });
659
690
  }
660
691
  }
661
- store.mark(message_id, "delivered", None)?;
692
+ if is_leader_recipient {
693
+ store.record_delivery_submission(message_id, readback_verified)?;
694
+ if !leader_transcript_has_token(state, message_id) {
695
+ store.mark(message_id, "submitted_pending_acceptance", None)?;
696
+ event_log.write(
697
+ "leader_receiver.acceptance_pending",
698
+ serde_json::json!({
699
+ "message_id": message_id,
700
+ "reason": "provider_receipt_not_observed",
701
+ }),
702
+ )?;
703
+ return Ok(leader_acceptance_pending_outcome(
704
+ message_id,
705
+ "submitted_pending_acceptance",
706
+ ));
707
+ }
708
+ store.mark_delivered_with_receipt(message_id)?;
709
+ } else {
710
+ store.mark(message_id, "delivered", None)?;
711
+ }
662
712
  let mut delivered_event = serde_json::json!({"message_id": message_id});
663
713
  if let Some(metadata) = resolved.metadata.as_ref() {
664
714
  append_target_metadata(&mut delivered_event, metadata);
@@ -908,7 +958,8 @@ fn deliver_leader_via_app_server(
908
958
  );
909
959
  match crate::codex_app_server::submit_to_bound_thread(&binding, message_id, &rendered) {
910
960
  Ok(submit) => {
911
- store.mark(message_id, "delivered", None)?;
961
+ store.record_delivery_submission(message_id, true)?;
962
+ store.mark_delivered_with_receipt(message_id)?;
912
963
  event_log.write(
913
964
  "message.delivered",
914
965
  serde_json::json!({"message_id": message_id}),
@@ -1800,10 +1851,16 @@ pub fn deliver_pending_messages(
1800
1851
  let conn = crate::db::schema::open_db(store.db_path())?;
1801
1852
  let mut stmt = conn.prepare(
1802
1853
  "select message_id from messages
1803
- where status in ('pending', 'accepted', 'target_resolved')
1854
+ where status in (
1855
+ 'pending', 'accepted', 'target_resolved',
1856
+ 'submitted_pending_acceptance', 'queued_until_leader_attach', ?1
1857
+ )
1804
1858
  order by created_at, message_id",
1805
1859
  )?;
1806
- let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1860
+ let rows = stmt.query_map(
1861
+ [crate::message_store::MessageRowStatus::QueuedCoordinatorUnavailable.as_str()],
1862
+ |row| row.get::<_, String>(0),
1863
+ )?;
1807
1864
  rows.collect::<Result<Vec<_>, _>>()?
1808
1865
  };
1809
1866
  let mut delivered = Vec::new();
@@ -1874,7 +1931,7 @@ pub fn deliver_pending_messages(
1874
1931
  continue;
1875
1932
  }
1876
1933
  };
1877
- if outcome.ok {
1934
+ if matches!(outcome.status, DeliveryStatus::Delivered) {
1878
1935
  delivered.push(message_id);
1879
1936
  }
1880
1937
  }
@@ -2017,6 +2074,73 @@ fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
2017
2074
  == Some("busy")
2018
2075
  }
2019
2076
 
2077
+ fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf> {
2078
+ state
2079
+ .get("leader")
2080
+ .and_then(|leader| leader.get("rollout_path"))
2081
+ .and_then(serde_json::Value::as_str)
2082
+ .or_else(|| {
2083
+ leader_receiver_value(state)
2084
+ .and_then(|receiver| receiver.get("rollout_path"))
2085
+ .and_then(serde_json::Value::as_str)
2086
+ })
2087
+ .filter(|path| !path.is_empty())
2088
+ .map(std::path::PathBuf::from)
2089
+ }
2090
+
2091
+ pub(crate) fn leader_transcript_has_token(state: &serde_json::Value, message_id: &str) -> bool {
2092
+ let Some(path) = leader_rollout_path(state) else {
2093
+ return false;
2094
+ };
2095
+ rollout_tail_contains(
2096
+ &path,
2097
+ &format!("[team-agent-token:{message_id}]"),
2098
+ 64 * 1024,
2099
+ )
2100
+ }
2101
+
2102
+ fn leader_acceptance_pending_outcome(message_id: &str, status: &str) -> DeliveryOutcome {
2103
+ DeliveryOutcome {
2104
+ ok: true,
2105
+ status: DeliveryStatus::RetryScheduled,
2106
+ message_status: MessageStatusShadow(status.to_string()),
2107
+ message_id: Some(message_id.to_string()),
2108
+ verification: Some("provider_receipt_not_observed".to_string()),
2109
+ stage: Some(DeliveryStage::Submit),
2110
+ reason: None,
2111
+ channel: Some("leader_acceptance_pending".to_string()),
2112
+ }
2113
+ }
2114
+
2115
+ fn observe_pending_leader_acceptance(
2116
+ store: &MessageStore,
2117
+ event_log: &EventLog,
2118
+ state: &serde_json::Value,
2119
+ message_id: &str,
2120
+ ) -> Result<DeliveryOutcome, MessagingError> {
2121
+ if !leader_transcript_has_token(state, message_id) {
2122
+ return Ok(leader_acceptance_pending_outcome(
2123
+ message_id,
2124
+ "submitted_pending_acceptance",
2125
+ ));
2126
+ }
2127
+ store.mark_delivered_with_receipt(message_id)?;
2128
+ event_log.write(
2129
+ "message.delivered",
2130
+ serde_json::json!({"message_id": message_id}),
2131
+ )?;
2132
+ Ok(DeliveryOutcome {
2133
+ ok: true,
2134
+ status: DeliveryStatus::Delivered,
2135
+ message_status: MessageStatusShadow("delivered".to_string()),
2136
+ message_id: Some(message_id.to_string()),
2137
+ verification: Some("provider_transcript_receipt".to_string()),
2138
+ stage: None,
2139
+ reason: None,
2140
+ channel: Some("leader_receiver".to_string()),
2141
+ })
2142
+ }
2143
+
2020
2144
  /// `_handle_trust_retry_needed` (`delivery.py:221`):trust 应答失败时调度有界退避重试
2021
2145
  /// (`attempt < MAX` → schedule;`>= MAX` → 终态 mark failed + `trust_auto_answer_exhausted`)。
2022
2146
  pub fn handle_trust_retry_needed(
@@ -2341,8 +2465,10 @@ fn save_scoped_state(
2341
2465
  state: &serde_json::Value,
2342
2466
  owner_team_id: Option<&str>,
2343
2467
  ) -> Result<(), MessagingError> {
2344
- if owner_team_id.filter(|team| !team.is_empty()).is_some() {
2345
- if state
2468
+ let scoped_owner_team_id = owner_team_id
2469
+ .filter(|team| !team.is_empty())
2470
+ .filter(|_| {
2471
+ state
2346
2472
  .get("teams")
2347
2473
  .and_then(serde_json::Value::as_object)
2348
2474
  .is_some_and(|teams| {
@@ -2354,14 +2480,13 @@ fn save_scoped_state(
2354
2480
  })
2355
2481
  .is_some_and(|team| teams.contains_key(&team))
2356
2482
  })
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
- }
2483
+ });
2484
+ crate::state::repository::StateRepository::new(workspace).save(
2485
+ crate::state::repository::StateWriteIntent::MessagingDeliveryState {
2486
+ owner_team_id: scoped_owner_team_id,
2487
+ },
2488
+ state,
2489
+ )?;
2365
2490
  Ok(())
2366
2491
  }
2367
2492
 
@@ -2374,8 +2499,10 @@ fn save_scoped_state_reapplying_after_conflict<F>(
2374
2499
  where
2375
2500
  F: FnOnce(&mut serde_json::Value),
2376
2501
  {
2377
- if owner_team_id.filter(|team| !team.is_empty()).is_some()
2378
- && state
2502
+ let scoped_owner_team_id = owner_team_id
2503
+ .filter(|team| !team.is_empty())
2504
+ .filter(|_| {
2505
+ state
2379
2506
  .get("teams")
2380
2507
  .and_then(serde_json::Value::as_object)
2381
2508
  .is_some_and(|teams| {
@@ -2387,15 +2514,14 @@ where
2387
2514
  })
2388
2515
  .is_some_and(|team| teams.contains_key(&team))
2389
2516
  })
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
- }
2517
+ });
2518
+ crate::state::repository::StateRepository::new(workspace).save_reapplying(
2519
+ crate::state::repository::StateWriteIntent::MessagingDeliveryState {
2520
+ owner_team_id: scoped_owner_team_id,
2521
+ },
2522
+ state,
2523
+ reapply,
2524
+ )?;
2399
2525
  Ok(())
2400
2526
  }
2401
2527
 
@@ -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(
@@ -13,7 +13,11 @@ use crate::transport::{
13
13
  };
14
14
 
15
15
  use super::helpers::MessageStatusShadow;
16
- use super::{DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, MessagingError};
16
+ use super::persist::persist_internal_send;
17
+ use super::{
18
+ DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, InitialDisposition,
19
+ InternalSendKind, MessagingError, PersistResolution,
20
+ };
17
21
 
18
22
  /// `_send_to_leader_receiver` (`leader.py:69`) — **N31/N32 funnel primitive**:所有 leader-bound
19
23
  /// caller(send_message(to=leader) / report_result / request_human / idle reminder /
@@ -74,39 +78,32 @@ pub fn send_to_leader_receiver_with_message_id(
74
78
  serde_json::json!({"sender": sender, "leader_id": leader_id, "result_id": result_id}),
75
79
  )?;
76
80
  }
77
- let message_id = if let Some(requested) = requested_message_id {
78
- if store.message_exists(requested)? {
81
+ let message_id = match persist_internal_send(
82
+ workspace,
83
+ InternalSendKind::LeaderNotification,
84
+ Some(&owner_team),
85
+ task_id.map(TaskId::as_str),
86
+ sender,
87
+ leader_id,
88
+ content,
89
+ None,
90
+ false,
91
+ requested_message_id,
92
+ InitialDisposition::Accepted,
93
+ )? {
94
+ PersistResolution::Duplicate(requested) => {
79
95
  return Ok(DeliveryOutcome {
80
96
  ok: false,
81
97
  status: DeliveryStatus::Refused,
82
98
  message_status: MessageStatusShadow("refused".to_string()),
83
- message_id: Some(requested.to_string()),
99
+ message_id: Some(requested),
84
100
  verification: None,
85
101
  stage: None,
86
102
  reason: Some(DeliveryRefusal::Duplicate),
87
103
  channel: Some("leader_receiver".to_string()),
88
104
  });
89
105
  }
90
- store.create_message_with_id(
91
- requested,
92
- task_id.map(TaskId::as_str),
93
- sender,
94
- leader_id,
95
- content,
96
- None,
97
- false,
98
- Some(&owner_team),
99
- )?
100
- } else {
101
- store.create_message(
102
- task_id.map(TaskId::as_str),
103
- sender,
104
- leader_id,
105
- content,
106
- None,
107
- false,
108
- Some(&owner_team),
109
- )?
106
+ PersistResolution::Persisted(persisted) => persisted.message_id,
110
107
  };
111
108
  // #231 exactly-once across rebind: insert the leader_notification_log PK BEFORE the
112
109
  // unbound-pane check. That way, if the result row gets blocked (I-4) and later the
@@ -332,7 +329,32 @@ pub fn deliver_to_leader_fallback_pane(
332
329
  let submit_ok = super::delivery::inject_submit_verified(&report);
333
330
  let readback_ok = super::delivery::pane_readback_verified(&report);
334
331
  if submit_ok {
335
- store.mark(message_id, "delivered", None)?;
332
+ store.record_delivery_submission(message_id, readback_ok)?;
333
+ if !super::delivery::leader_transcript_has_token(state, message_id) {
334
+ store.mark(message_id, "submitted_pending_acceptance", None)?;
335
+ event_log.write(
336
+ "leader_receiver.acceptance_pending",
337
+ serde_json::json!({
338
+ "message_id": message_id,
339
+ "result_id": result_id,
340
+ "reason": "provider_receipt_not_observed",
341
+ "channel": "fallback_pane",
342
+ }),
343
+ )?;
344
+ return Ok(DeliveryOutcome {
345
+ ok: true,
346
+ status: DeliveryStatus::RetryScheduled,
347
+ message_status: MessageStatusShadow(
348
+ "submitted_pending_acceptance".to_string(),
349
+ ),
350
+ message_id: Some(message_id.to_string()),
351
+ verification: Some("provider_receipt_not_observed".to_string()),
352
+ stage: Some(DeliveryStage::Submit),
353
+ reason: None,
354
+ channel: Some("leader_acceptance_pending".to_string()),
355
+ });
356
+ }
357
+ store.mark_delivered_with_receipt(message_id)?;
336
358
  event_log.write(
337
359
  "leader_receiver.fallback_pane_submitted",
338
360
  serde_json::json!({
@@ -562,21 +584,24 @@ pub fn enqueue_leader_mailbox_until_attach(
562
584
  sender: &str,
563
585
  event_log: &EventLog,
564
586
  ) -> Result<DeliveryOutcome, MessagingError> {
565
- let store = MessageStore::open(target_workspace)?;
566
- let message_id = store.create_message(
587
+ let PersistResolution::Persisted(persisted) = persist_internal_send(
588
+ target_workspace,
589
+ InternalSendKind::OfflineMailbox,
590
+ Some(canonical_team_key),
567
591
  task_id.map(TaskId::as_str),
568
592
  sender,
569
593
  "leader",
570
594
  content,
571
595
  None,
572
596
  false,
573
- Some(canonical_team_key),
574
- )?;
575
- // The default row status after `create_message` is `accepted`. Flip it to
576
- // `queued_until_leader_attach` so it stays out of `claim_for_delivery`
577
- // eligible set and out of `deliver_pending_messages` scan until an
578
- // attach/claim explicitly requeues it (see requeue_blocked_leader_messages).
579
- store.mark(&message_id, "queued_until_leader_attach", None)?;
597
+ None,
598
+ InitialDisposition::QueuedUntilLeaderAttach,
599
+ )?
600
+ else {
601
+ unreachable!("offline mailbox does not accept caller-supplied ids")
602
+ };
603
+ let message_status = persisted.row_status.as_str().to_string();
604
+ let message_id = persisted.message_id;
580
605
  event_log.write(
581
606
  "leader_mailbox.queued_until_attach",
582
607
  serde_json::json!({
@@ -590,7 +615,7 @@ pub fn enqueue_leader_mailbox_until_attach(
590
615
  Ok(DeliveryOutcome {
591
616
  ok: true,
592
617
  status: DeliveryStatus::Queued,
593
- message_status: MessageStatusShadow("queued_until_leader_attach".to_string()),
618
+ message_status: MessageStatusShadow(message_status),
594
619
  message_id: Some(message_id),
595
620
  verification: None,
596
621
  stage: None,
@@ -61,10 +61,12 @@ use crate::message_store::MessageStore;
61
61
  use crate::transport::{PaneId, Target, Transport};
62
62
 
63
63
  pub mod activity;
64
+ pub mod address;
64
65
  pub mod delivery;
65
66
  pub mod helpers;
66
67
  pub mod leader_receiver;
67
68
  pub mod peers;
69
+ pub mod persist;
68
70
  pub mod results;
69
71
  pub mod scheduler;
70
72
  pub mod selftest;
@@ -75,9 +77,11 @@ pub mod watchers;
75
77
 
76
78
  // ── re-export: 保持 `crate::messaging::X` 与 test `super::X` 解析不变 ──────────
77
79
  pub use activity::{classify_agent_activity, detect_cross_worker_deadlocks, detect_idle_fallbacks};
80
+ pub use address::{parse_logical_address, LogicalAddress, LogicalAddressTarget};
78
81
  pub use delivery::{
79
- deliver_pending_message, deliver_pending_messages, deliver_stored_message, execute_trust_retry,
80
- handle_trust_retry_needed, record_turn_open_if_leader_to_worker,
82
+ deliver_pending_message, deliver_pending_messages, deliver_persisted_message,
83
+ deliver_stored_message, execute_trust_retry, handle_trust_retry_needed,
84
+ record_turn_open_if_leader_to_worker,
81
85
  retry_injection_after_trust_auto_answer, stamp_first_send_at_if_leader_to_worker,
82
86
  tmux_pane_width,
83
87
  };
@@ -88,6 +92,10 @@ pub use leader_receiver::{
88
92
  send_to_leader_receiver_with_message_id,
89
93
  };
90
94
  pub use peers::allow_peer_talk;
95
+ pub use persist::{
96
+ persist_resolved_send, DeliveryBlocker, InitialDisposition, InternalSendKind,
97
+ LogicalRecipient, PersistResolution, PersistedSend, ResolvedSendIntent, SendOrigin,
98
+ };
91
99
  pub use results::{
92
100
  collect, collect_for_team, collect_results_and_notify_watchers, report_result,
93
101
  report_result_for_owner_team, report_result_for_owner_team_with_primary_error,
@@ -96,6 +104,7 @@ pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events, stuck_cancel
96
104
  pub use selftest::{evaluate_idle_behavior, run_comms_selftest, CommsSelftestDriver};
97
105
  pub use send::{
98
106
  apply_worker_sender_bypass, send_message, session_drift_refusal, MessageTarget, SendOptions,
107
+ TrustedSender,
99
108
  };
100
109
  pub use trust::{attempt_trust_auto_answer, TrustAnswerOutcome};
101
110
  pub use types::{