@team-agent/installer 0.5.59 → 0.5.61

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 (68) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +44 -1
  4. package/crates/team-agent/src/cli/diagnose.rs +183 -0
  5. package/crates/team-agent/src/cli/emit.rs +63 -14
  6. package/crates/team-agent/src/cli/leader.rs +8 -1
  7. package/crates/team-agent/src/cli/mod.rs +263 -5
  8. package/crates/team-agent/src/cli/named_address.rs +18 -1
  9. package/crates/team-agent/src/cli/send/presentation.rs +2 -0
  10. package/crates/team-agent/src/cli/spec.rs +4 -1
  11. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  12. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  13. package/crates/team-agent/src/cli/types.rs +33 -0
  14. package/crates/team-agent/src/communication_mode/mod.rs +53 -0
  15. package/crates/team-agent/src/compiler/tests.rs +5 -5
  16. package/crates/team-agent/src/compiler.rs +53 -1
  17. package/crates/team-agent/src/coordinator/tick.rs +21 -8
  18. package/crates/team-agent/src/db/message_store.rs +45 -1
  19. package/crates/team-agent/src/fake_worker.rs +21 -1
  20. package/crates/team-agent/src/leader/start.rs +732 -94
  21. package/crates/team-agent/src/leader/tests/identity.rs +64 -57
  22. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  23. package/crates/team-agent/src/lib.rs +4 -0
  24. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
  25. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +57 -51
  26. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
  27. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
  28. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/launch.rs +1 -1
  30. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  31. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  32. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
  33. package/crates/team-agent/src/lifecycle/types.rs +1 -0
  34. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  35. package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
  36. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  37. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  38. package/crates/team-agent/src/mcp_server/tools.rs +100 -66
  39. package/crates/team-agent/src/mcp_server/types.rs +5 -0
  40. package/crates/team-agent/src/mcp_server/wire.rs +27 -21
  41. package/crates/team-agent/src/messaging/delivery.rs +105 -38
  42. package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
  43. package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
  44. package/crates/team-agent/src/messaging/presentation.rs +109 -0
  45. package/crates/team-agent/src/messaging/results.rs +165 -17
  46. package/crates/team-agent/src/messaging/send.rs +94 -81
  47. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  48. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  49. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  50. package/crates/team-agent/src/messaging/types.rs +5 -0
  51. package/crates/team-agent/src/messaging/watchers.rs +5 -0
  52. package/crates/team-agent/src/model/mod.rs +1 -0
  53. package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
  54. package/crates/team-agent/src/model/spec.rs +16 -0
  55. package/crates/team-agent/src/provider/adapter.rs +57 -0
  56. package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
  57. package/crates/team-agent/src/provider/session/capture.rs +69 -34
  58. package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
  59. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
  60. package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
  61. package/crates/team-agent/src/provider/session/mod.rs +3 -2
  62. package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
  63. package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
  64. package/crates/team-agent/src/provider/session_scan/common.rs +34 -8
  65. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  66. package/crates/team-agent/src/topology.rs +3 -0
  67. package/package.json +4 -4
  68. package/skills/team-agent/command-coverage.json +379 -0
@@ -14,6 +14,95 @@ use super::MessagingError;
14
14
  use crate::model::ids::TaskId;
15
15
  use crate::state::projection::OwnerTeamResolution;
16
16
 
17
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
18
+ pub enum ResultRoute {
19
+ Leader,
20
+ Pipeline,
21
+ }
22
+
23
+ impl ResultRoute {
24
+ pub const ALL: [Self; 2] = [Self::Leader, Self::Pipeline];
25
+
26
+ pub const fn as_str(self) -> &'static str {
27
+ match self {
28
+ Self::Leader => "leader",
29
+ Self::Pipeline => "pipeline",
30
+ }
31
+ }
32
+
33
+ pub fn parse(value: &str) -> Option<Self> {
34
+ match value {
35
+ "leader" => Some(Self::Leader),
36
+ "pipeline" => Some(Self::Pipeline),
37
+ _ => None,
38
+ }
39
+ }
40
+ }
41
+
42
+ impl Default for ResultRoute {
43
+ fn default() -> Self {
44
+ Self::Leader
45
+ }
46
+ }
47
+
48
+ pub fn results_for_case(
49
+ workspace: &Path,
50
+ case_id: &str,
51
+ owner_team_id: Option<&str>,
52
+ requested_owner_team_id: Option<&str>,
53
+ ) -> Result<Vec<serde_json::Value>, MessagingError> {
54
+ let current_db = workspace.join(".team/runtime/team.db");
55
+ let legacy_db = workspace.join(".team/team.db");
56
+ if current_db.exists() {
57
+ return results_for_case_in_db(
58
+ &current_db,
59
+ case_id,
60
+ owner_team_id,
61
+ requested_owner_team_id,
62
+ );
63
+ }
64
+ if legacy_db.exists() {
65
+ return results_for_case_in_db(&legacy_db, case_id, owner_team_id, requested_owner_team_id);
66
+ }
67
+ Ok(Vec::new())
68
+ }
69
+
70
+ fn results_for_case_in_db(
71
+ db_path: &Path,
72
+ case_id: &str,
73
+ owner_team_id: Option<&str>,
74
+ requested_owner_team_id: Option<&str>,
75
+ ) -> Result<Vec<serde_json::Value>, MessagingError> {
76
+ let conn = crate::db::schema::open_db(db_path)?;
77
+ // The second predicate works around
78
+ // `.team/bugs/BUG-caseid-writable-when-route-undeclared-20260730.md`;
79
+ // it is not design intent.
80
+ let case_predicate = "task_id = ?1 or json_extract(envelope,'$.presentation.case_id') = ?1";
81
+ let sql = match owner_team_id {
82
+ Some(_) => format!(
83
+ "select envelope from results where ({case_predicate}) \
84
+ and (owner_team_id = ?2 or owner_team_id = ?3) \
85
+ order by created_at, result_id"
86
+ ),
87
+ None => format!(
88
+ "select envelope from results where {case_predicate} order by created_at, result_id"
89
+ ),
90
+ };
91
+ let mut stmt = conn.prepare(&sql)?;
92
+ let row_mapper = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
93
+ let rows = match (owner_team_id, requested_owner_team_id) {
94
+ (Some(team), Some(requested_team)) => {
95
+ stmt.query_map(params![case_id, team, requested_team], row_mapper)
96
+ }
97
+ (Some(team), None) => stmt.query_map(params![case_id, team, team], row_mapper),
98
+ (None, _) => stmt.query_map(params![case_id], row_mapper),
99
+ }?
100
+ .collect::<Result<Vec<_>, _>>()?;
101
+ rows.into_iter()
102
+ .map(|envelope| serde_json::from_str(&envelope).map_err(MessagingError::from))
103
+ .collect()
104
+ }
105
+
17
106
  /// `collect` (`results.py:45`):投递 pending、捞 uncollected results、校验 envelope、更新任务态、
18
107
  /// 写 team_state、ensure coordinator。CLI `collect` + coordinator tick 调。
19
108
  pub fn collect(
@@ -627,18 +716,12 @@ fn report_result_for_owner_team_inner(
627
716
  explicit_owner_team: Option<&str>,
628
717
  fallback_primary_error: Option<&str>,
629
718
  ) -> Result<serde_json::Value, MessagingError> {
630
- validate_result_envelope(envelope)?;
631
- let (presentation_request, presentation_error) =
632
- super::presentation::normalize_report_presentation(envelope.get("presentation"));
633
- if let Some(error) = presentation_error {
634
- return Err(MessagingError::Validation(format!(
635
- "invalid presentation: {error}"
636
- )));
719
+ if envelope.get("result_route").is_some() {
720
+ return Err(MessagingError::Validation(
721
+ "report_result result_route is task-owned and cannot be set by a worker".to_string(),
722
+ ));
637
723
  }
638
- let presentation = super::presentation::decide_presentation(
639
- &presentation_request,
640
- super::presentation::PresentationSource::ReportResult,
641
- );
724
+ validate_result_envelope(envelope)?;
642
725
  let store = MessageStore::open(workspace)?;
643
726
  let result_id = envelope
644
727
  .get("result_id")
@@ -656,12 +739,6 @@ fn report_result_for_owner_team_inner(
656
739
  );
657
740
  }
658
741
  }
659
- if let Some(obj) = stored.as_object_mut() {
660
- obj.insert(
661
- "presentation".to_string(),
662
- serde_json::to_value(&presentation)?,
663
- );
664
- }
665
742
  let conn = crate::db::schema::open_db(store.db_path())?;
666
743
  let state_for_owner =
667
744
  crate::state::persist::load_runtime_state(workspace).unwrap_or(serde_json::json!({}));
@@ -669,6 +746,42 @@ fn report_result_for_owner_team_inner(
669
746
  .filter(|team| !team.is_empty())
670
747
  .map(str::to_string)
671
748
  .unwrap_or_else(|| super::leader_receiver::active_team_key(workspace, &state_for_owner));
749
+ let (result_route, route_declared) = task_result_route(&state_for_owner, &owner_team, task_id)?;
750
+ let presentation = if route_declared {
751
+ let request = super::presentation::PresentationRequest {
752
+ sink: match result_route {
753
+ ResultRoute::Leader => super::presentation::PresentationSink::Leader,
754
+ ResultRoute::Pipeline => super::presentation::PresentationSink::Casefile,
755
+ },
756
+ class: match result_route {
757
+ ResultRoute::Leader => super::presentation::PresentationClass::Message,
758
+ ResultRoute::Pipeline => super::presentation::PresentationClass::StageResult,
759
+ },
760
+ case_id: (result_route == ResultRoute::Pipeline).then(|| task_id.to_string()),
761
+ };
762
+ super::presentation::decide_presentation(
763
+ &request,
764
+ super::presentation::PresentationSource::ReportResult,
765
+ )
766
+ } else {
767
+ let (request, error) =
768
+ super::presentation::normalize_report_presentation(envelope.get("presentation"));
769
+ if let Some(error) = error {
770
+ return Err(MessagingError::Validation(format!(
771
+ "invalid presentation: {error}"
772
+ )));
773
+ }
774
+ super::presentation::decide_presentation(
775
+ &request,
776
+ super::presentation::PresentationSource::ReportResult,
777
+ )
778
+ };
779
+ if let Some(obj) = stored.as_object_mut() {
780
+ obj.insert(
781
+ "presentation".to_string(),
782
+ serde_json::to_value(&presentation)?,
783
+ );
784
+ }
672
785
  let inserted = insert_result_if_absent(
673
786
  &conn,
674
787
  &result_id,
@@ -968,6 +1081,8 @@ fn report_result_for_owner_team_inner(
968
1081
  let leader_notified = matches!(outcome.status, crate::messaging::DeliveryStatus::Delivered);
969
1082
  let notification_status_wire = if leader_notified {
970
1083
  "delivered"
1084
+ } else if outcome.channel.as_deref() == Some("leader_receipt_source_unavailable") {
1085
+ "injected_awaiting_receipt"
971
1086
  } else if outcome.channel.as_deref() == Some("leader_acceptance_pending") {
972
1087
  "submitted_pending_acceptance"
973
1088
  } else if outcome.channel.as_deref() == Some("rebind_required")
@@ -1045,6 +1160,39 @@ fn report_result_for_owner_team_inner(
1045
1160
  Ok(serde_json::Value::Object(out))
1046
1161
  }
1047
1162
 
1163
+ fn task_result_route(
1164
+ state: &serde_json::Value,
1165
+ owner_team: &str,
1166
+ task_id: &str,
1167
+ ) -> Result<(ResultRoute, bool), MessagingError> {
1168
+ let scoped_tasks = state
1169
+ .get("teams")
1170
+ .and_then(|teams| teams.get(owner_team))
1171
+ .and_then(|team| team.get("tasks"))
1172
+ .and_then(serde_json::Value::as_array);
1173
+ let top_level_tasks = state.get("tasks").and_then(serde_json::Value::as_array);
1174
+ let task = scoped_tasks
1175
+ .into_iter()
1176
+ .flatten()
1177
+ .chain(top_level_tasks.into_iter().flatten())
1178
+ .find(|task| task.get("id").and_then(serde_json::Value::as_str) == Some(task_id));
1179
+ let Some(route) = task.and_then(|task| task.get("result_route")) else {
1180
+ return Ok((ResultRoute::default(), false));
1181
+ };
1182
+ let route = route.as_str().ok_or_else(|| {
1183
+ MessagingError::Validation(format!(
1184
+ "task {task_id} has non-string result_route and cannot accept results"
1185
+ ))
1186
+ })?;
1187
+ ResultRoute::parse(route)
1188
+ .map(|route| (route, true))
1189
+ .ok_or_else(|| {
1190
+ MessagingError::Validation(format!(
1191
+ "task {task_id} has unknown result_route {route:?} and cannot accept results"
1192
+ ))
1193
+ })
1194
+ }
1195
+
1048
1196
  fn copy_report_attribution_fields(
1049
1197
  envelope: &serde_json::Value,
1050
1198
  out: &mut serde_json::Map<String, serde_json::Value>,
@@ -9,9 +9,10 @@ use crate::model::ids::{AgentId, TaskId, TeamKey};
9
9
  use crate::transport::{PaneId, Transport};
10
10
 
11
11
  use super::helpers::{status_wire, MessageStatusShadow};
12
- use super::leader_receiver::{send_to_leader_receiver, send_to_leader_receiver_with_presentation};
12
+ use super::leader_receiver::send_to_leader_receiver_with_presentation;
13
13
  use super::presentation::{
14
- decide_presentation, PresentationRequest, PresentationSink, PresentationSource,
14
+ decide_presentation, PresentationDecision, PresentationRequest, PresentationSink,
15
+ PresentationSource,
15
16
  };
16
17
  use super::{
17
18
  persist_resolved_send, DeliveryBlocker, DeliveryOutcome, DeliveryRefusal, DeliveryStatus,
@@ -125,49 +126,14 @@ pub fn send_message(
125
126
  MessageTarget::Single(target) if target == "leader" => {
126
127
  let presentation = decide_presentation(&opts.presentation, PresentationSource::Send);
127
128
  if presentation.effective_sink != PresentationSink::Leader {
128
- let mut intent = ResolvedSendIntent::accepted(
129
- opts.origin,
129
+ return persist_stored_only_send(
130
130
  workspace,
131
- opts.team.clone(),
132
131
  LogicalRecipient::Leader,
133
- opts.sender.clone(),
134
- opts.task_id.clone(),
135
132
  content,
136
- None,
137
- false,
138
- opts.message_id.clone(),
133
+ opts,
134
+ &presentation,
135
+ &event_log,
139
136
  );
140
- intent.initial_disposition = InitialDisposition::StoredOnly;
141
- intent.presentation = presentation.clone();
142
- let persisted = match persist_resolved_send(&intent)? {
143
- PersistResolution::Persisted(persisted) => persisted,
144
- PersistResolution::Duplicate(message_id) => {
145
- return Ok(refused_outcome_with_id(
146
- DeliveryRefusal::Duplicate,
147
- Some(message_id),
148
- ));
149
- }
150
- };
151
- event_log.write(
152
- "presentation.stored_without_live_inject",
153
- serde_json::json!({
154
- "message_id": persisted.message_id,
155
- "requested_sink": presentation.requested_sink,
156
- "effective_sink": presentation.effective_sink,
157
- "class": presentation.class,
158
- "policy_reason": presentation.policy_reason,
159
- }),
160
- )?;
161
- return Ok(DeliveryOutcome {
162
- ok: true,
163
- status: DeliveryStatus::StoredOnly,
164
- message_status: MessageStatusShadow("stored_only".to_string()),
165
- message_id: Some(persisted.message_id),
166
- verification: Some("durable_without_live_inject".to_string()),
167
- stage: None,
168
- reason: None,
169
- channel: Some(presentation.effective_sink.as_str().to_string()),
170
- });
171
137
  }
172
138
  let outcome = send_to_leader_receiver_with_presentation(
173
139
  workspace,
@@ -201,15 +167,7 @@ pub fn send_message(
201
167
  MessageTarget::Single(target) => target,
202
168
  MessageTarget::Broadcast => {
203
169
  let recipients = broadcast_recipients(&state, opts.sender.as_str(), opts.team.as_ref());
204
- return fanout_send(
205
- workspace,
206
- &state,
207
- &recipients,
208
- content,
209
- opts,
210
- &event_log,
211
- "*",
212
- );
170
+ return fanout_send(workspace, &recipients, content, opts, "*");
213
171
  }
214
172
  MessageTarget::Fanout(recipients) if recipients.is_empty() => {
215
173
  // swallow batch 3 ②: a failed send carries its reason (Python send error
@@ -226,9 +184,7 @@ pub fn send_message(
226
184
  });
227
185
  }
228
186
  MessageTarget::Fanout(recipients) => {
229
- return fanout_send(
230
- workspace, &state, recipients, content, opts, &event_log, "fanout",
231
- );
187
+ return fanout_send(workspace, recipients, content, opts, "fanout");
232
188
  }
233
189
  };
234
190
  // send.py:259-261 — a non-leader target that is NOT a known team agent is refused
@@ -240,6 +196,17 @@ pub fn send_message(
240
196
  if !in_team {
241
197
  return Ok(refused_outcome(DeliveryRefusal::TargetNotInTeam));
242
198
  }
199
+ let presentation = decide_presentation(&opts.presentation, PresentationSource::Send);
200
+ if presentation.effective_sink != PresentationSink::Leader {
201
+ return persist_stored_only_send(
202
+ workspace,
203
+ LogicalRecipient::from_resolved(recipient),
204
+ content,
205
+ opts,
206
+ &presentation,
207
+ &event_log,
208
+ );
209
+ }
243
210
  if let Some(outcome) = session_drift_refusal(
244
211
  &state,
245
212
  recipient,
@@ -354,6 +321,59 @@ pub fn send_message(
354
321
  })
355
322
  }
356
323
 
324
+ fn persist_stored_only_send(
325
+ workspace: &Path,
326
+ recipient: LogicalRecipient,
327
+ content: &str,
328
+ opts: &SendOptions,
329
+ presentation: &PresentationDecision,
330
+ event_log: &EventLog,
331
+ ) -> Result<DeliveryOutcome, MessagingError> {
332
+ let mut intent = ResolvedSendIntent::accepted(
333
+ opts.origin,
334
+ workspace,
335
+ opts.team.clone(),
336
+ recipient,
337
+ opts.sender.clone(),
338
+ opts.task_id.clone(),
339
+ content,
340
+ None,
341
+ opts.requires_ack,
342
+ opts.message_id.clone(),
343
+ );
344
+ intent.initial_disposition = InitialDisposition::StoredOnly;
345
+ intent.presentation = presentation.clone();
346
+ let persisted = match persist_resolved_send(&intent)? {
347
+ PersistResolution::Persisted(persisted) => persisted,
348
+ PersistResolution::Duplicate(message_id) => {
349
+ return Ok(refused_outcome_with_id(
350
+ DeliveryRefusal::Duplicate,
351
+ Some(message_id),
352
+ ));
353
+ }
354
+ };
355
+ event_log.write(
356
+ "presentation.stored_without_live_inject",
357
+ serde_json::json!({
358
+ "message_id": persisted.message_id,
359
+ "requested_sink": presentation.requested_sink,
360
+ "effective_sink": presentation.effective_sink,
361
+ "class": presentation.class,
362
+ "policy_reason": presentation.policy_reason,
363
+ }),
364
+ )?;
365
+ Ok(DeliveryOutcome {
366
+ ok: true,
367
+ status: DeliveryStatus::StoredOnly,
368
+ message_status: MessageStatusShadow("stored_only".to_string()),
369
+ message_id: Some(persisted.message_id),
370
+ verification: Some("durable_without_live_inject".to_string()),
371
+ stage: None,
372
+ reason: None,
373
+ channel: Some(presentation.effective_sink.as_str().to_string()),
374
+ })
375
+ }
376
+
357
377
  fn stale_worker_target_missing_preflight(
358
378
  workspace: &Path,
359
379
  state: &serde_json::Value,
@@ -804,11 +824,9 @@ fn broadcast_recipients(
804
824
 
805
825
  fn fanout_send(
806
826
  workspace: &Path,
807
- state: &serde_json::Value,
808
827
  recipients: &[String],
809
828
  content: &str,
810
829
  opts: &SendOptions,
811
- event_log: &EventLog,
812
830
  channel_label: &str,
813
831
  ) -> Result<DeliveryOutcome, MessagingError> {
814
832
  let mut last_message_id: Option<String> = None;
@@ -821,30 +839,17 @@ fn fanout_send(
821
839
  continue;
822
840
  }
823
841
  attempted_count = attempted_count.saturating_add(1);
824
- let outcome = if recipient == "leader" {
825
- send_to_leader_receiver(
826
- workspace,
827
- state,
828
- recipient,
829
- content,
830
- opts.task_id.as_ref(),
831
- opts.sender.as_str(),
832
- opts.requires_ack,
833
- None,
834
- event_log,
835
- )?
836
- } else {
837
- // single-recipient re-entry — strip fanout metadata to avoid recursion + ensure
838
- // each row gets its own caller-supplied message_id (none) so SQLite PK doesn't clash.
839
- let mut inner_opts = opts.clone();
840
- inner_opts.message_id = None;
841
- super::send::send_message(
842
- workspace,
843
- &MessageTarget::Single(recipient.clone()),
844
- content,
845
- &inner_opts,
846
- )?
847
- };
842
+ // Every expanded recipient re-enters the same presentation-aware send
843
+ // funnel. Strip the caller idempotency key so each durable fanout row
844
+ // receives its own stable message id.
845
+ let mut inner_opts = opts.clone();
846
+ inner_opts.message_id = None;
847
+ let outcome = super::send::send_message(
848
+ workspace,
849
+ &MessageTarget::Single(recipient.clone()),
850
+ content,
851
+ &inner_opts,
852
+ )?;
848
853
  if outcome.ok {
849
854
  delivered_count = delivered_count.saturating_add(1);
850
855
  if let Some(mid) = outcome.message_id.clone() {
@@ -862,8 +867,12 @@ fn fanout_send(
862
867
  return Ok(outcome);
863
868
  }
864
869
  }
870
+ let presentation = decide_presentation(&opts.presentation, PresentationSource::Send);
871
+ let stored_only = presentation.effective_sink != PresentationSink::Leader;
865
872
  let status = if any_failure {
866
873
  DeliveryStatus::FanoutPartial
874
+ } else if delivered_count > 0 && stored_only {
875
+ DeliveryStatus::StoredOnly
867
876
  } else if delivered_count > 0 {
868
877
  DeliveryStatus::FanoutDelivered
869
878
  } else {
@@ -874,9 +883,13 @@ fn fanout_send(
874
883
  status,
875
884
  message_status: MessageStatusShadow(status_wire(status).to_string()),
876
885
  message_id: last_message_id,
877
- verification: None,
886
+ verification: stored_only.then(|| "durable_without_live_inject".to_string()),
878
887
  stage: None,
879
888
  reason: None,
880
- channel: Some(channel_label.to_string()),
889
+ channel: Some(if stored_only {
890
+ presentation.effective_sink.as_str().to_string()
891
+ } else {
892
+ channel_label.to_string()
893
+ }),
881
894
  })
882
895
  }
@@ -101,10 +101,10 @@ fn legacy_receiver_without_status_still_rejects_a_foreign_workspace() {
101
101
  .with_tmux_endpoint(socket)
102
102
  .with_targets(vec![observed]);
103
103
 
104
- assert_eq!(
104
+ assert!(matches!(
105
105
  resolve_live_leader_channel(workspace, &receiver, &transport),
106
- LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch)
107
- );
106
+ LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch(_))
107
+ ));
108
108
  }
109
109
 
110
110
  #[test]
@@ -162,8 +162,8 @@ fn same_socket_and_pane_in_a_foreign_workspace_is_not_a_live_leader() {
162
162
  .with_tmux_endpoint(socket)
163
163
  .with_targets(vec![observed]);
164
164
 
165
- assert_eq!(
165
+ assert!(matches!(
166
166
  resolve_live_leader_channel(workspace, &receiver(socket), &transport),
167
- LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch)
168
- );
167
+ LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch(_))
168
+ ));
169
169
  }