@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +44 -1
- package/crates/team-agent/src/cli/diagnose.rs +183 -0
- package/crates/team-agent/src/cli/emit.rs +63 -14
- package/crates/team-agent/src/cli/leader.rs +8 -1
- package/crates/team-agent/src/cli/mod.rs +263 -5
- package/crates/team-agent/src/cli/named_address.rs +18 -1
- package/crates/team-agent/src/cli/send/presentation.rs +2 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/types.rs +33 -0
- package/crates/team-agent/src/communication_mode/mod.rs +53 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +53 -1
- package/crates/team-agent/src/db/message_store.rs +45 -1
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/leader/start.rs +732 -94
- package/crates/team-agent/src/leader/tests/identity.rs +64 -57
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +100 -66
- package/crates/team-agent/src/mcp_server/types.rs +5 -0
- package/crates/team-agent/src/mcp_server/wire.rs +27 -21
- package/crates/team-agent/src/messaging/delivery.rs +105 -38
- package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
- package/crates/team-agent/src/messaging/presentation.rs +109 -0
- package/crates/team-agent/src/messaging/results.rs +165 -17
- package/crates/team-agent/src/messaging/send.rs +94 -81
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +5 -0
- package/crates/team-agent/src/messaging/watchers.rs +5 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
- package/crates/team-agent/src/model/spec.rs +16 -0
- package/crates/team-agent/src/provider/session/capture.rs +40 -18
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/common.rs +23 -7
- package/crates/team-agent/src/provider/session_scan.rs +5 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- 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::
|
|
12
|
+
use super::leader_receiver::send_to_leader_receiver_with_presentation;
|
|
13
13
|
use super::presentation::{
|
|
14
|
-
decide_presentation, PresentationRequest, PresentationSink,
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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:
|
|
886
|
+
verification: stored_only.then(|| "durable_without_live_inject".to_string()),
|
|
878
887
|
stage: None,
|
|
879
888
|
reason: None,
|
|
880
|
-
channel: Some(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2980
|
-
//
|
|
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,
|
|
3017
|
-
"gate054
|
|
3018
|
-
|
|
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;
|