@team-agent/installer 0.5.50 → 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 (84) 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 +11 -48
  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/run_delegation.rs +1 -0
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  27. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  28. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  29. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  30. package/crates/team-agent/src/db/message_store.rs +154 -44
  31. package/crates/team-agent/src/event_log.rs +73 -0
  32. package/crates/team-agent/src/leader/start.rs +28 -4
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
  35. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  36. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  37. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  38. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  39. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  40. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  41. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  42. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  43. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  44. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  45. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  46. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  47. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  48. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  49. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  50. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  51. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  52. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
  53. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
  55. package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
  56. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  57. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
  58. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
  59. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  60. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  61. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  62. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  63. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  64. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  65. package/crates/team-agent/src/messaging/activity.rs +4 -2
  66. package/crates/team-agent/src/messaging/address.rs +86 -0
  67. package/crates/team-agent/src/messaging/delivery.rs +165 -39
  68. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  69. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  70. package/crates/team-agent/src/messaging/mod.rs +10 -2
  71. package/crates/team-agent/src/messaging/persist.rs +309 -0
  72. package/crates/team-agent/src/messaging/results.rs +16 -24
  73. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  74. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  75. package/crates/team-agent/src/messaging/send.rs +101 -49
  76. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  77. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  78. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  79. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  80. package/crates/team-agent/src/redaction.rs +72 -2
  81. package/crates/team-agent/src/state/persist.rs +2 -1
  82. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  83. package/crates/team-agent/src/state/repository.rs +59 -16
  84. package/package.json +4 -4
@@ -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,
@@ -0,0 +1,309 @@
1
+ //! Durable boundary between resolved logical addressing and delivery/recovery.
2
+
3
+ use std::path::{Path, PathBuf};
4
+
5
+ use crate::db::message_store::{MessageRowStatus, MessageStore, PersistMessageInput};
6
+ use crate::model::ids::{AgentId, TaskId, TeamKey};
7
+
8
+ use super::send::TrustedSender;
9
+ use super::MessagingError;
10
+
11
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12
+ pub enum InternalSendKind {
13
+ Delivery,
14
+ LeaderNotification,
15
+ OfflineMailbox,
16
+ Selftest,
17
+ Watcher,
18
+ }
19
+
20
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
21
+ pub enum SendOrigin {
22
+ Cli,
23
+ Mcp,
24
+ Internal(InternalSendKind),
25
+ }
26
+
27
+ #[derive(Debug, Clone, PartialEq, Eq)]
28
+ pub enum LogicalRecipient {
29
+ Worker(AgentId),
30
+ Leader,
31
+ }
32
+
33
+ impl LogicalRecipient {
34
+ pub fn from_resolved(value: impl Into<String>) -> Self {
35
+ let value = value.into();
36
+ if value == "leader" {
37
+ Self::Leader
38
+ } else {
39
+ Self::Worker(AgentId::new(value))
40
+ }
41
+ }
42
+
43
+ pub fn as_str(&self) -> &str {
44
+ match self {
45
+ Self::Worker(agent) => agent.as_str(),
46
+ Self::Leader => "leader",
47
+ }
48
+ }
49
+ }
50
+
51
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
52
+ pub enum DeliveryBlocker {
53
+ CoordinatorUnavailable,
54
+ }
55
+
56
+ impl DeliveryBlocker {
57
+ pub const fn as_str(self) -> &'static str {
58
+ match self {
59
+ Self::CoordinatorUnavailable => "coordinator_unavailable",
60
+ }
61
+ }
62
+ }
63
+
64
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
65
+ pub enum InitialDisposition {
66
+ Accepted,
67
+ QueuedUntilLeaderAttach,
68
+ Blocked(DeliveryBlocker),
69
+ }
70
+
71
+ impl InitialDisposition {
72
+ fn row_status(self) -> MessageRowStatus {
73
+ match self {
74
+ Self::Accepted => MessageRowStatus::Accepted,
75
+ Self::QueuedUntilLeaderAttach => MessageRowStatus::QueuedUntilLeaderAttach,
76
+ Self::Blocked(DeliveryBlocker::CoordinatorUnavailable) => {
77
+ MessageRowStatus::QueuedCoordinatorUnavailable
78
+ }
79
+ }
80
+ }
81
+
82
+ fn error(self) -> Option<&'static str> {
83
+ match self {
84
+ Self::Blocked(blocker) => Some(blocker.as_str()),
85
+ Self::Accepted | Self::QueuedUntilLeaderAttach => None,
86
+ }
87
+ }
88
+ }
89
+
90
+ #[derive(Debug, Clone)]
91
+ pub struct ResolvedSendIntent {
92
+ pub origin: SendOrigin,
93
+ pub target_workspace: PathBuf,
94
+ pub owner_team_id: Option<TeamKey>,
95
+ pub recipient: LogicalRecipient,
96
+ pub sender: TrustedSender,
97
+ pub task_id: Option<TaskId>,
98
+ pub content: String,
99
+ pub reply_to: Option<String>,
100
+ pub requires_ack: bool,
101
+ pub requested_message_id: Option<String>,
102
+ pub initial_disposition: InitialDisposition,
103
+ }
104
+
105
+ impl ResolvedSendIntent {
106
+ #[allow(clippy::too_many_arguments)]
107
+ pub fn accepted(
108
+ origin: SendOrigin,
109
+ workspace: &Path,
110
+ owner_team_id: Option<TeamKey>,
111
+ recipient: LogicalRecipient,
112
+ sender: TrustedSender,
113
+ task_id: Option<TaskId>,
114
+ content: impl Into<String>,
115
+ reply_to: Option<String>,
116
+ requires_ack: bool,
117
+ requested_message_id: Option<String>,
118
+ ) -> Self {
119
+ Self {
120
+ origin,
121
+ target_workspace: workspace.to_path_buf(),
122
+ owner_team_id,
123
+ recipient,
124
+ sender,
125
+ task_id,
126
+ content: content.into(),
127
+ reply_to,
128
+ requires_ack,
129
+ requested_message_id,
130
+ initial_disposition: InitialDisposition::Accepted,
131
+ }
132
+ }
133
+ }
134
+
135
+ #[derive(Debug, Clone, PartialEq, Eq)]
136
+ pub struct PersistedSend {
137
+ pub message_id: String,
138
+ pub owner_team_id: Option<TeamKey>,
139
+ pub recipient: LogicalRecipient,
140
+ pub row_status: MessageRowStatus,
141
+ pub blocker: Option<DeliveryBlocker>,
142
+ }
143
+
144
+ #[derive(Debug, Clone, PartialEq, Eq)]
145
+ pub enum PersistResolution {
146
+ Persisted(PersistedSend),
147
+ Duplicate(String),
148
+ }
149
+
150
+ /// The only production entry point that creates a durable message row.
151
+ pub fn persist_resolved_send(
152
+ intent: &ResolvedSendIntent,
153
+ ) -> Result<PersistResolution, MessagingError> {
154
+ let store = MessageStore::open(&intent.target_workspace)?;
155
+ if let Some(requested) = intent.requested_message_id.as_deref() {
156
+ if store.message_exists(requested)? {
157
+ return Ok(PersistResolution::Duplicate(requested.to_string()));
158
+ }
159
+ }
160
+ let status = intent.initial_disposition.row_status();
161
+ let message_id = store.persist_message(PersistMessageInput {
162
+ message_id: intent.requested_message_id.as_deref(),
163
+ owner_team_id: intent.owner_team_id.as_ref().map(TeamKey::as_str),
164
+ task_id: intent.task_id.as_ref().map(TaskId::as_str),
165
+ sender: intent.sender.as_str(),
166
+ recipient: intent.recipient.as_str(),
167
+ reply_to: intent.reply_to.as_deref(),
168
+ requires_ack: intent.requires_ack,
169
+ status,
170
+ content: &intent.content,
171
+ error: intent.initial_disposition.error(),
172
+ })?;
173
+ Ok(PersistResolution::Persisted(PersistedSend {
174
+ message_id,
175
+ owner_team_id: intent.owner_team_id.clone(),
176
+ recipient: intent.recipient.clone(),
177
+ row_status: status,
178
+ blocker: match intent.initial_disposition {
179
+ InitialDisposition::Blocked(blocker) => Some(blocker),
180
+ InitialDisposition::Accepted | InitialDisposition::QueuedUntilLeaderAttach => None,
181
+ },
182
+ }))
183
+ }
184
+
185
+ #[allow(clippy::too_many_arguments)]
186
+ pub(crate) fn persist_internal_send(
187
+ workspace: &Path,
188
+ kind: InternalSendKind,
189
+ owner_team_id: Option<&str>,
190
+ task_id: Option<&str>,
191
+ sender: &str,
192
+ recipient: &str,
193
+ content: &str,
194
+ reply_to: Option<&str>,
195
+ requires_ack: bool,
196
+ requested_message_id: Option<&str>,
197
+ initial_disposition: InitialDisposition,
198
+ ) -> Result<PersistResolution, MessagingError> {
199
+ let mut intent = ResolvedSendIntent::accepted(
200
+ SendOrigin::Internal(kind),
201
+ workspace,
202
+ owner_team_id.map(TeamKey::new),
203
+ LogicalRecipient::from_resolved(recipient),
204
+ TrustedSender::from_runtime_identity(AgentId::new(sender)),
205
+ task_id.map(TaskId::new),
206
+ content,
207
+ reply_to.map(ToOwned::to_owned),
208
+ requires_ack,
209
+ requested_message_id.map(ToOwned::to_owned),
210
+ );
211
+ intent.initial_disposition = initial_disposition;
212
+ persist_resolved_send(&intent)
213
+ }
214
+
215
+ #[cfg(test)]
216
+ mod tests {
217
+ use super::*;
218
+
219
+ fn workspace(tag: &str) -> PathBuf {
220
+ let path =
221
+ std::env::temp_dir().join(format!("ta-send-persist-{tag}-{}", std::process::id()));
222
+ std::fs::create_dir_all(&path).unwrap();
223
+ path
224
+ }
225
+
226
+ #[test]
227
+ fn coordinator_blocker_is_durable_and_not_delivered() {
228
+ let workspace = workspace("coordinator-blocker");
229
+ let mut intent = ResolvedSendIntent::accepted(
230
+ SendOrigin::Cli,
231
+ &workspace,
232
+ Some(TeamKey::new("carc")),
233
+ LogicalRecipient::from_resolved("w1"),
234
+ TrustedSender::leader(),
235
+ None,
236
+ "probe",
237
+ None,
238
+ true,
239
+ None,
240
+ );
241
+ intent.initial_disposition =
242
+ InitialDisposition::Blocked(DeliveryBlocker::CoordinatorUnavailable);
243
+ let PersistResolution::Persisted(persisted) = persist_resolved_send(&intent).unwrap()
244
+ else {
245
+ panic!("fresh intent must persist")
246
+ };
247
+ assert_eq!(
248
+ persisted.row_status,
249
+ MessageRowStatus::QueuedCoordinatorUnavailable
250
+ );
251
+ assert_eq!(
252
+ persisted.blocker,
253
+ Some(DeliveryBlocker::CoordinatorUnavailable)
254
+ );
255
+ let store = MessageStore::open(&workspace).unwrap();
256
+ let connection = crate::db::schema::open_db(store.db_path()).unwrap();
257
+ let (status, delivered_at, error): (String, Option<String>, Option<String>) = connection
258
+ .query_row(
259
+ "select status, delivered_at, error from messages where message_id = ?1",
260
+ [&persisted.message_id],
261
+ |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
262
+ )
263
+ .unwrap();
264
+ assert_eq!(status, "queued_coordinator_unavailable");
265
+ assert_eq!(delivered_at, None);
266
+ assert_eq!(error.as_deref(), Some("coordinator_unavailable"));
267
+ }
268
+
269
+ #[test]
270
+ fn production_row_creation_is_owned_by_persisted_primitive() {
271
+ for (name, source) in [
272
+ ("send", include_str!("send.rs")),
273
+ ("leader_receiver", include_str!("leader_receiver.rs")),
274
+ ("delivery", include_str!("delivery.rs")),
275
+ ("watchers", include_str!("watchers.rs")),
276
+ ("helpers", include_str!("helpers.rs")),
277
+ ("selftest", include_str!("selftest.rs")),
278
+ ] {
279
+ assert!(
280
+ !source.contains(".create_message(")
281
+ && !source.contains(".create_message_with_id("),
282
+ "{name} still creates message rows outside persist_resolved_send"
283
+ );
284
+ }
285
+ }
286
+
287
+ #[test]
288
+ fn persisted_truth_and_workspace_identity_have_forward_consumers() {
289
+ for (name, source) in [
290
+ ("send", include_str!("send.rs")),
291
+ ("delivery", include_str!("delivery.rs")),
292
+ ("leader_receiver", include_str!("leader_receiver.rs")),
293
+ ] {
294
+ assert!(
295
+ source.contains("persisted.row_status"),
296
+ "{name} rebuilds wire truth instead of consuming PersistedSend"
297
+ );
298
+ }
299
+ let watchers = include_str!("watchers.rs");
300
+ assert!(
301
+ !watchers.contains(".and_then(std::path::Path::parent)"),
302
+ "watcher identity must not be derived from the team.db layout"
303
+ );
304
+ assert!(
305
+ watchers.contains("deliver_primary_watcher(\n workspace,"),
306
+ "watcher must forward the caller's canonical workspace"
307
+ );
308
+ }
309
+ }
@@ -197,27 +197,15 @@ fn collect_scoped(
197
197
  collected_results.push(summary);
198
198
  }
199
199
  if state_dirty {
200
- if owner_team_id.is_some() {
201
- crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
202
- &paths.run_workspace,
203
- &state,
204
- |latest| {
205
- for (task_id, result_id) in &task_updates {
206
- mark_task_done(latest, task_id, result_id);
207
- }
208
- },
209
- )?;
210
- } else {
211
- crate::state::persist::save_runtime_state_reapplying_after_conflict(
212
- &paths.run_workspace,
213
- &state,
214
- |latest| {
215
- for (task_id, result_id) in &task_updates {
216
- mark_task_done(latest, task_id, result_id);
217
- }
218
- },
219
- )?;
220
- }
200
+ crate::state::repository::StateRepository::new(&paths.run_workspace).save_reapplying(
201
+ crate::state::repository::StateWriteIntent::ResultCollection { owner_team_id },
202
+ &state,
203
+ |latest| {
204
+ for (task_id, result_id) in &task_updates {
205
+ mark_task_done(latest, task_id, result_id);
206
+ }
207
+ },
208
+ )?;
221
209
  }
222
210
  let counts = result_counts(&conn, owner_team_id)?;
223
211
  // results.py:157 — ensure_coordinator=true runs the REAL ensure step; the
@@ -870,7 +858,9 @@ fn report_result_for_owner_team_inner(
870
858
  }
871
859
  }
872
860
  if !outcome.ok {
873
- if !report_state_has_app_server_receiver(&delivery_state) {
861
+ if outcome.channel.as_deref() != Some("leader_acceptance_pending")
862
+ && !report_state_has_app_server_receiver(&delivery_state)
863
+ {
874
864
  let fallback_error = primary_error.unwrap_or_else(|| {
875
865
  format!(
876
866
  "leader_notification_primary_failed:{}",
@@ -892,9 +882,11 @@ fn report_result_for_owner_team_inner(
892
882
  }
893
883
  }
894
884
  }
895
- let leader_notified = outcome.ok;
896
- let notification_status_wire = if outcome.ok {
885
+ let leader_notified = matches!(outcome.status, crate::messaging::DeliveryStatus::Delivered);
886
+ let notification_status_wire = if leader_notified {
897
887
  "delivered"
888
+ } else if outcome.channel.as_deref() == Some("leader_acceptance_pending") {
889
+ "submitted_pending_acceptance"
898
890
  } else if outcome.channel.as_deref() == Some("rebind_required")
899
891
  || matches!(outcome.status, crate::messaging::DeliveryStatus::Blocked)
900
892
  {
@@ -256,8 +256,10 @@ pub fn stuck_cancel(
256
256
  },
257
257
  );
258
258
  }
259
- crate::state::persist::save_runtime_state_reapplying_after_conflict(
260
- workspace,
259
+ crate::state::repository::StateRepository::new(workspace).save_reapplying(
260
+ crate::state::repository::StateWriteIntent::SchedulerSuppression {
261
+ owner_team_id: Some(&team),
262
+ },
261
263
  &state,
262
264
  |latest| {
263
265
  for kind in &alert_types {
@@ -129,18 +129,25 @@ fn run_contract_suite(
129
129
  }
130
130
  };
131
131
 
132
- match scratch_store.as_ref().and_then(|store| {
133
- store
134
- .create_message(
135
- None,
136
- "doctor",
137
- "worker",
138
- "comms contract probe",
139
- None,
140
- false,
141
- Some("contract-team"),
142
- )
143
- .ok()
132
+ match scratch_store.as_ref().and_then(|_| {
133
+ match super::persist::persist_internal_send(
134
+ &scratch,
135
+ super::InternalSendKind::Selftest,
136
+ Some("contract-team"),
137
+ None,
138
+ "doctor",
139
+ "worker",
140
+ "comms contract probe",
141
+ None,
142
+ false,
143
+ None,
144
+ super::InitialDisposition::Accepted,
145
+ )
146
+ .ok()?
147
+ {
148
+ super::PersistResolution::Persisted(persisted) => Some(persisted.message_id),
149
+ super::PersistResolution::Duplicate(_) => None,
150
+ }
144
151
  }) {
145
152
  Some(message_id) if message_id.starts_with("msg_") && message_id.len() > 4 => {
146
153
  let rendered = format!(