@team-agent/installer 0.5.60 → 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 (54) 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/db/message_store.rs +45 -1
  18. package/crates/team-agent/src/fake_worker.rs +21 -1
  19. package/crates/team-agent/src/leader/start.rs +732 -94
  20. package/crates/team-agent/src/leader/tests/identity.rs +64 -57
  21. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  22. package/crates/team-agent/src/lib.rs +4 -0
  23. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
  24. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  25. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  27. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  28. package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
  29. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  30. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  31. package/crates/team-agent/src/mcp_server/tools.rs +100 -66
  32. package/crates/team-agent/src/mcp_server/types.rs +5 -0
  33. package/crates/team-agent/src/mcp_server/wire.rs +27 -21
  34. package/crates/team-agent/src/messaging/delivery.rs +105 -38
  35. package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
  36. package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
  37. package/crates/team-agent/src/messaging/presentation.rs +109 -0
  38. package/crates/team-agent/src/messaging/results.rs +165 -17
  39. package/crates/team-agent/src/messaging/send.rs +94 -81
  40. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  41. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  42. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  43. package/crates/team-agent/src/messaging/types.rs +5 -0
  44. package/crates/team-agent/src/messaging/watchers.rs +5 -0
  45. package/crates/team-agent/src/model/mod.rs +1 -0
  46. package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
  47. package/crates/team-agent/src/model/spec.rs +16 -0
  48. package/crates/team-agent/src/provider/session/capture.rs +40 -18
  49. package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
  50. package/crates/team-agent/src/provider/session_scan/common.rs +23 -7
  51. package/crates/team-agent/src/provider/session_scan.rs +5 -0
  52. package/crates/team-agent/src/topology.rs +3 -0
  53. package/package.json +4 -4
  54. package/skills/team-agent/command-coverage.json +379 -0
@@ -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
  }
@@ -99,6 +99,141 @@ fn attached_leader_mailbox_is_rechecked_by_the_normal_delivery_tick() {
99
99
  assert!(case.events().contains("leader_receiver.mailbox_requeued"));
100
100
  }
101
101
 
102
+ #[test]
103
+ fn leader_submit_without_receipt_source_cannot_park_forever_without_a_completion_writer() {
104
+ let case = Case::new_without_rollout("missing-receipt-source");
105
+ let transport = RecordingTransport::new("");
106
+ let message_id = case.seed_message("missing receipt source canary");
107
+
108
+ let first = deliver_pending_message(
109
+ &case.workspace,
110
+ &case.store,
111
+ &transport,
112
+ &message_id,
113
+ &case.event_log,
114
+ &case.state,
115
+ )
116
+ .expect("verified leader transport submission");
117
+
118
+ assert_ne!(
119
+ case.message_status(&message_id),
120
+ "submitted_pending_acceptance",
121
+ "F4 acceptance-stall: a verified leader transport submit with no reachable \
122
+ receipt source must not enter submitted_pending_acceptance, because that \
123
+ protected state has no completion writer. The runtime may close the \
124
+ transport-acceptance fact or expose a provider-session receipt writer, \
125
+ but it must not report an indefinitely retryable fact-terminal. outcome={first:?}"
126
+ );
127
+ assert_eq!(
128
+ transport.inject_count(),
129
+ 1,
130
+ "the acceptance repair must not obtain progress by physically injecting twice"
131
+ );
132
+
133
+ let _ = deliver_pending_message(
134
+ &case.workspace,
135
+ &case.store,
136
+ &transport,
137
+ &message_id,
138
+ &case.event_log,
139
+ &case.state,
140
+ );
141
+ assert_eq!(
142
+ transport.inject_count(),
143
+ 1,
144
+ "a second coordinator observation must not replay the already-submitted leader message"
145
+ );
146
+ }
147
+
148
+ #[test]
149
+ fn nonempty_missing_receipt_source_is_not_token_absent() {
150
+ assert_unavailable_rollout_converges(
151
+ "missing-rollout",
152
+ PathBuf::from("definitely-missing-leader-rollout.jsonl"),
153
+ false,
154
+ );
155
+ }
156
+
157
+ #[test]
158
+ fn nonempty_unreadable_receipt_source_is_not_token_absent() {
159
+ assert_unavailable_rollout_converges(
160
+ "unreadable-rollout",
161
+ PathBuf::from("leader-rollout-is-a-directory"),
162
+ true,
163
+ );
164
+ }
165
+
166
+ fn assert_unavailable_rollout_converges(tag: &str, rollout: PathBuf, directory: bool) {
167
+ let case = Case::new_with_unavailable_rollout(tag, rollout, directory);
168
+ let transport = RecordingTransport::new("");
169
+ let message_id = case.seed_message("unavailable receipt source canary");
170
+
171
+ let first = deliver_pending_message(
172
+ &case.workspace,
173
+ &case.store,
174
+ &transport,
175
+ &message_id,
176
+ &case.event_log,
177
+ &case.state,
178
+ )
179
+ .expect("verified leader transport submission");
180
+
181
+ assert_eq!(
182
+ case.message_status(&message_id),
183
+ "injected_awaiting_receipt",
184
+ "F4 receipt observation must distinguish an unreadable nonempty rollout_path \
185
+ from a readable rollout that lacks the token. The former is \
186
+ receipt-source-unavailable and must converge to the typed parked state, \
187
+ not remain submitted_pending_acceptance. shape={tag} outcome={first:?}"
188
+ );
189
+ assert_eq!(
190
+ first.verification.as_deref(),
191
+ Some("leader_receipt_source_unavailable"),
192
+ "the outcome must expose read-unavailable rather than TokenAbsent; shape={tag}"
193
+ );
194
+ assert_eq!(transport.inject_count(), 1);
195
+
196
+ let _ = deliver_pending_message(
197
+ &case.workspace,
198
+ &case.store,
199
+ &transport,
200
+ &message_id,
201
+ &case.event_log,
202
+ &case.state,
203
+ );
204
+ assert_eq!(
205
+ transport.inject_count(),
206
+ 1,
207
+ "read-unavailable convergence must not obtain progress by a second physical \
208
+ injection; shape={tag}"
209
+ );
210
+ }
211
+
212
+ #[test]
213
+ fn worker_consumption_writer_remains_a_positive_control() {
214
+ let case = Case::new("worker-positive-control");
215
+ let message_id = case
216
+ .store
217
+ .create_message(
218
+ None,
219
+ "leader",
220
+ "worker",
221
+ "worker consume canary",
222
+ None,
223
+ false,
224
+ None,
225
+ )
226
+ .unwrap();
227
+
228
+ case.store.mark(&message_id, "consumed", None).unwrap();
229
+
230
+ assert_eq!(
231
+ case.message_status(&message_id),
232
+ "consumed",
233
+ "positive control: the independent worker consumption writer must remain reachable"
234
+ );
235
+ }
236
+
102
237
  struct Case {
103
238
  workspace: PathBuf,
104
239
  rollout: PathBuf,
@@ -134,6 +269,58 @@ impl Case {
134
269
  }
135
270
  }
136
271
 
272
+ fn new_without_rollout(tag: &str) -> Self {
273
+ let workspace = tmp_ws(&format!("leader-inject-acceptance-{tag}"));
274
+ let rollout = workspace.join("absent-leader.jsonl");
275
+ let state = serde_json::json!({
276
+ "active_team_key": "acceptance-team",
277
+ "session_name": "team-acceptance",
278
+ "leader_receiver": {
279
+ "pane_id": "%leader",
280
+ "status": "attached",
281
+ "provider": "claude"
282
+ }
283
+ });
284
+ crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
285
+ let store = MessageStore::open(&workspace).unwrap();
286
+ let event_log = EventLog::new(&workspace);
287
+ Self {
288
+ workspace,
289
+ rollout,
290
+ store,
291
+ event_log,
292
+ state,
293
+ }
294
+ }
295
+
296
+ fn new_with_unavailable_rollout(tag: &str, relative: PathBuf, directory: bool) -> Self {
297
+ let workspace = tmp_ws(&format!("leader-inject-acceptance-{tag}"));
298
+ let rollout = workspace.join(relative);
299
+ if directory {
300
+ std::fs::create_dir_all(&rollout).unwrap();
301
+ }
302
+ let state = serde_json::json!({
303
+ "active_team_key": "acceptance-team",
304
+ "session_name": "team-acceptance",
305
+ "leader_receiver": {
306
+ "pane_id": "%leader",
307
+ "status": "attached",
308
+ "provider": "claude",
309
+ "rollout_path": rollout,
310
+ }
311
+ });
312
+ crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
313
+ let store = MessageStore::open(&workspace).unwrap();
314
+ let event_log = EventLog::new(&workspace);
315
+ Self {
316
+ workspace,
317
+ rollout,
318
+ store,
319
+ event_log,
320
+ state,
321
+ }
322
+ }
323
+
137
324
  fn seed_message(&self, content: &str) -> String {
138
325
  self.store
139
326
  .create_message(None, "worker", "leader", content, None, false, None)
@@ -2976,9 +2976,8 @@ fn gate054_attached_status_still_accepts_when_pane_is_live() {
2976
2976
  // Injection-case revision: transport submit success is NOT a provider
2977
2977
  // receipt, so the old `delivered` pin was a false-positive lock
2978
2978
  // (MUST-10: physical submit is the framework's fact; acceptance is the
2979
- // provider's). The un-refused path now parks the SAME row in
2980
- // submitted_pending_acceptance until a provider receipt or retry
2981
- // schedule advances it.
2979
+ // provider's). The un-refused path now records the SAME row in the typed
2980
+ // post-injection fact state until a provider receipt advances it.
2982
2981
  let ws = tmp_ws("gate054ok");
2983
2982
  let store = store_for(&ws);
2984
2983
  let log = EventLog::new(&ws);
@@ -3012,10 +3011,18 @@ fn gate054_attached_status_still_accepts_when_pane_is_live() {
3012
3011
  1,
3013
3012
  "gate054: exactly one physical inject must have happened (MUST-10 anchor)"
3014
3013
  );
3014
+ let injected_awaiting_receipt = crate::db::message_store::MessageRowStatus::ALL
3015
+ .iter()
3016
+ .map(|status| status.as_str())
3017
+ .find(|status| *status == "injected_awaiting_receipt")
3018
+ .expect(
3019
+ "gate054 signed reanchor: typed status directory must expose \
3020
+ injected_awaiting_receipt",
3021
+ );
3015
3022
  assert_eq!(
3016
- out.message_status.0, "submitted_pending_acceptance",
3017
- "gate054 (injection-case revision): a successful physical submit without a provider \
3018
- receipt must park as submitted_pending_acceptance, never claim delivered: {out:?}"
3023
+ out.message_status.0, injected_awaiting_receipt,
3024
+ "gate054 signed reanchor: a successful physical inject without a provider receipt \
3025
+ must remain non-delivered as injected_awaiting_receipt, never claim delivered: {out:?}"
3019
3026
  );
3020
3027
  }
3021
3028
 
@@ -53,6 +53,11 @@ pub enum DeliveryRefusal {
53
53
  TmuxTargetMissing,
54
54
  MessageAlreadyClaimed,
55
55
  LeaderNotAttached,
56
+ /// The recorded leader pane is live but belongs to another workspace.
57
+ /// Keep this typed cause distinct from an actually unattached leader so
58
+ /// the send surface can return the existing recovery action.
59
+ #[serde(rename = "PaneWorkspaceMismatch")]
60
+ PaneWorkspaceMismatch,
56
61
  NoCallerPane,
57
62
  TeamOwnerMismatch,
58
63
  Ambiguous,
@@ -529,6 +529,11 @@ pub(crate) fn requeue_blocked_leader_messages(
529
529
  "blocked_leader_unbound": counts.blocked_leader_unbound,
530
530
  "queued_until_leader_attach": counts.queued_until_leader_attach,
531
531
  },
532
+ "attach_window_failures": {
533
+ "reason": "leader_not_attached",
534
+ "count": counts.blocked_leader_unbound,
535
+ "status": "requeued_pending_physical_retry",
536
+ },
532
537
  }),
533
538
  )?;
534
539
  }
@@ -18,6 +18,7 @@ pub mod ids;
18
18
  // cli/send.rs (positional adapter), mcp_server/tools.rs (owner-scoped
19
19
  // peer suggestions). Never a routing authority — advisory only.
20
20
  pub(crate) mod name_similarity;
21
+ pub mod pane_authority_refusal;
21
22
  pub mod paths;
22
23
  pub mod permissions;
23
24
  pub mod routing;