@team-agent/installer 0.5.50 → 0.5.52
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 +5 -3
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +32 -14
- package/crates/team-agent/src/cli/named_address.rs +46 -115
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +44 -1169
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/lease.rs +202 -16
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
- package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
- package/crates/team-agent/src/lifecycle/restart.rs +26 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +14 -5
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +288 -183
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +15 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +101 -49
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +284 -21
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +94 -27
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/package.json +4 -4
|
@@ -197,27 +197,15 @@ fn collect_scoped(
|
|
|
197
197
|
collected_results.push(summary);
|
|
198
198
|
}
|
|
199
199
|
if state_dirty {
|
|
200
|
-
|
|
201
|
-
crate::state::
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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.
|
|
896
|
-
let notification_status_wire = if
|
|
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::
|
|
260
|
-
|
|
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(|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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!(
|
|
@@ -10,7 +10,11 @@ use crate::transport::{PaneId, Transport};
|
|
|
10
10
|
|
|
11
11
|
use super::helpers::{status_wire, MessageStatusShadow};
|
|
12
12
|
use super::leader_receiver::{send_to_leader_receiver, send_to_leader_receiver_with_message_id};
|
|
13
|
-
use super::{
|
|
13
|
+
use super::{
|
|
14
|
+
persist_resolved_send, DeliveryBlocker, DeliveryOutcome, DeliveryRefusal, DeliveryStatus,
|
|
15
|
+
InitialDisposition, LogicalRecipient, MessagingError, PersistResolution, ResolvedSendIntent,
|
|
16
|
+
SendOrigin,
|
|
17
|
+
};
|
|
14
18
|
|
|
15
19
|
/// 发件目标:单 target / 广播 `*` / 扇出 list (`send.py:36` `target: str|list[str]|None`)。
|
|
16
20
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -49,6 +53,7 @@ impl TrustedSender {
|
|
|
49
53
|
/// `send_message` 选项 (`send.py:36`:Python 大量默认参数 → typed 选项 struct)。
|
|
50
54
|
#[derive(Debug, Clone)]
|
|
51
55
|
pub struct SendOptions {
|
|
56
|
+
pub origin: SendOrigin,
|
|
52
57
|
pub task_id: Option<TaskId>,
|
|
53
58
|
/// `route_task_id`(golden `send.py:190`,默认 `True`):仅 **routing**(CLI `send --task`)时把
|
|
54
59
|
/// `task_id` 当真任务校验/路由。**投递/fanout/internal/coordinator** 路径传 `false`
|
|
@@ -73,6 +78,7 @@ impl Default for SendOptions {
|
|
|
73
78
|
fn default() -> Self {
|
|
74
79
|
// 默认值对齐 Python 签名 (`send.py:38-40`)。
|
|
75
80
|
Self {
|
|
81
|
+
origin: SendOrigin::Internal(super::InternalSendKind::Delivery),
|
|
76
82
|
task_id: None,
|
|
77
83
|
route_task_id: true,
|
|
78
84
|
sender: TrustedSender::leader(),
|
|
@@ -207,46 +213,47 @@ pub fn send_message(
|
|
|
207
213
|
}
|
|
208
214
|
let stale_worker_target_missing =
|
|
209
215
|
stale_worker_target_missing_preflight(workspace, &state, recipient)?;
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
let coordinator_unavailable = if stale_worker_target_missing {
|
|
217
|
+
None
|
|
218
|
+
} else {
|
|
219
|
+
coordinator_unavailable_outcome(workspace, recipient, opts, &event_log)?
|
|
220
|
+
};
|
|
221
|
+
if let Some(gate) = coordinator_unavailable
|
|
222
|
+
.as_ref()
|
|
223
|
+
.filter(|gate| !gate.persist)
|
|
224
|
+
{
|
|
225
|
+
return Ok(gate.outcome.clone());
|
|
216
226
|
}
|
|
217
|
-
let
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
let mut intent = ResolvedSendIntent::accepted(
|
|
228
|
+
opts.origin,
|
|
229
|
+
workspace,
|
|
230
|
+
opts.team.clone(),
|
|
231
|
+
LogicalRecipient::from_resolved(recipient),
|
|
232
|
+
opts.sender.clone(),
|
|
233
|
+
opts.task_id.clone(),
|
|
234
|
+
content,
|
|
235
|
+
None,
|
|
236
|
+
opts.requires_ack,
|
|
237
|
+
opts.message_id.clone(),
|
|
238
|
+
);
|
|
239
|
+
if coordinator_unavailable
|
|
240
|
+
.as_ref()
|
|
241
|
+
.is_some_and(|gate| gate.persist)
|
|
242
|
+
{
|
|
243
|
+
intent.initial_disposition =
|
|
244
|
+
InitialDisposition::Blocked(DeliveryBlocker::CoordinatorUnavailable);
|
|
245
|
+
}
|
|
246
|
+
let persisted = match persist_resolved_send(&intent)? {
|
|
247
|
+
PersistResolution::Persisted(persisted) => persisted,
|
|
248
|
+
PersistResolution::Duplicate(message_id) => {
|
|
224
249
|
return Ok(refused_outcome_with_id(
|
|
225
250
|
DeliveryRefusal::Duplicate,
|
|
226
|
-
Some(
|
|
251
|
+
Some(message_id),
|
|
227
252
|
));
|
|
228
253
|
}
|
|
229
|
-
store.create_message_with_id(
|
|
230
|
-
requested,
|
|
231
|
-
task_id,
|
|
232
|
-
opts.sender.as_str(),
|
|
233
|
-
recipient,
|
|
234
|
-
content,
|
|
235
|
-
None,
|
|
236
|
-
opts.requires_ack,
|
|
237
|
-
owner_team_id,
|
|
238
|
-
)?
|
|
239
|
-
} else {
|
|
240
|
-
store.create_message(
|
|
241
|
-
task_id,
|
|
242
|
-
opts.sender.as_str(),
|
|
243
|
-
recipient,
|
|
244
|
-
content,
|
|
245
|
-
None,
|
|
246
|
-
opts.requires_ack,
|
|
247
|
-
owner_team_id,
|
|
248
|
-
)?
|
|
249
254
|
};
|
|
255
|
+
let message_id = persisted.message_id.clone();
|
|
256
|
+
let store = crate::message_store::MessageStore::open(workspace)?;
|
|
250
257
|
if stale_worker_target_missing {
|
|
251
258
|
return super::delivery::mark_worker_target_missing(
|
|
252
259
|
&store,
|
|
@@ -256,10 +263,37 @@ pub fn send_message(
|
|
|
256
263
|
None,
|
|
257
264
|
);
|
|
258
265
|
}
|
|
266
|
+
if let Some(CoordinatorUnavailableGate {
|
|
267
|
+
mut outcome,
|
|
268
|
+
persist: true,
|
|
269
|
+
}) = coordinator_unavailable
|
|
270
|
+
{
|
|
271
|
+
let Some(blocker) = persisted.blocker else {
|
|
272
|
+
return Err(MessagingError::Validation(
|
|
273
|
+
"coordinator-blocked persistence returned no blocker".to_string(),
|
|
274
|
+
));
|
|
275
|
+
};
|
|
276
|
+
let message_status = persisted.row_status.as_str();
|
|
277
|
+
event_log.write(
|
|
278
|
+
"send.message_queued",
|
|
279
|
+
serde_json::json!({
|
|
280
|
+
"message_id": message_id,
|
|
281
|
+
"recipient": recipient,
|
|
282
|
+
"sender": opts.sender,
|
|
283
|
+
"message_status": message_status,
|
|
284
|
+
"blocker": blocker.as_str(),
|
|
285
|
+
}),
|
|
286
|
+
)?;
|
|
287
|
+
outcome.ok = true;
|
|
288
|
+
outcome.status = DeliveryStatus::Blocked;
|
|
289
|
+
outcome.message_status = MessageStatusShadow(message_status.to_string());
|
|
290
|
+
outcome.message_id = Some(message_id);
|
|
291
|
+
return Ok(outcome);
|
|
292
|
+
}
|
|
259
293
|
Ok(DeliveryOutcome {
|
|
260
294
|
ok: true,
|
|
261
295
|
status: DeliveryStatus::Queued,
|
|
262
|
-
message_status: MessageStatusShadow(
|
|
296
|
+
message_status: MessageStatusShadow(persisted.row_status.as_str().to_string()),
|
|
263
297
|
message_id: Some(message_id),
|
|
264
298
|
verification: None,
|
|
265
299
|
stage: None,
|
|
@@ -331,12 +365,17 @@ fn refused_outcome_with_verification(
|
|
|
331
365
|
}
|
|
332
366
|
}
|
|
333
367
|
|
|
368
|
+
struct CoordinatorUnavailableGate {
|
|
369
|
+
outcome: DeliveryOutcome,
|
|
370
|
+
persist: bool,
|
|
371
|
+
}
|
|
372
|
+
|
|
334
373
|
fn coordinator_unavailable_outcome(
|
|
335
374
|
workspace: &Path,
|
|
336
375
|
recipient: &str,
|
|
337
376
|
opts: &SendOptions,
|
|
338
377
|
event_log: &EventLog,
|
|
339
|
-
) -> Result<Option<
|
|
378
|
+
) -> Result<Option<CoordinatorUnavailableGate>, MessagingError> {
|
|
340
379
|
let coordinator_workspace = WorkspacePath::new(workspace.to_path_buf());
|
|
341
380
|
let health = crate::coordinator::coordinator_health(&coordinator_workspace);
|
|
342
381
|
if health.ok || matches!(health.status, CoordinatorHealthStatus::Missing) {
|
|
@@ -366,9 +405,19 @@ fn coordinator_unavailable_outcome(
|
|
|
366
405
|
)?;
|
|
367
406
|
return Ok(None);
|
|
368
407
|
}
|
|
369
|
-
let
|
|
370
|
-
|
|
408
|
+
let persist = !matches!(
|
|
409
|
+
health.metadata_mismatch_reason.as_deref(),
|
|
410
|
+
Some("protocol_version_mismatch" | "message_store_schema_version_mismatch")
|
|
371
411
|
);
|
|
412
|
+
let warning = if persist {
|
|
413
|
+
format!(
|
|
414
|
+
"coordinator is not running; message was queued for {recipient} and will retry. Run `team-agent diagnose` or restart the team."
|
|
415
|
+
)
|
|
416
|
+
} else {
|
|
417
|
+
format!(
|
|
418
|
+
"coordinator protocol or schema is incompatible; message was not queued for {recipient}. Run `team-agent diagnose` or upgrade the runtime."
|
|
419
|
+
)
|
|
420
|
+
};
|
|
372
421
|
event_log.write(
|
|
373
422
|
"send.coordinator_unavailable",
|
|
374
423
|
serde_json::json!({
|
|
@@ -377,22 +426,25 @@ fn coordinator_unavailable_outcome(
|
|
|
377
426
|
"coordinator_status": health.status,
|
|
378
427
|
"coordinator_pid": health.pid.map(|pid| pid.get()),
|
|
379
428
|
"metadata_mismatch_reason": health.metadata_mismatch_reason,
|
|
380
|
-
"message_queued":
|
|
429
|
+
"message_queued": persist,
|
|
381
430
|
"warning": warning,
|
|
382
431
|
"coordinator_log": crate::coordinator::coordinator_log_path(&coordinator_workspace)
|
|
383
432
|
.display()
|
|
384
433
|
.to_string(),
|
|
385
434
|
}),
|
|
386
435
|
)?;
|
|
387
|
-
Ok(Some(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
436
|
+
Ok(Some(CoordinatorUnavailableGate {
|
|
437
|
+
persist,
|
|
438
|
+
outcome: DeliveryOutcome {
|
|
439
|
+
ok: false,
|
|
440
|
+
status: DeliveryStatus::Degraded,
|
|
441
|
+
message_status: MessageStatusShadow("degraded".to_string()),
|
|
442
|
+
message_id: None,
|
|
443
|
+
verification: Some(warning),
|
|
444
|
+
stage: None,
|
|
445
|
+
reason: Some(DeliveryRefusal::CoordinatorUnavailable),
|
|
446
|
+
channel: Some("coordinator_unavailable".to_string()),
|
|
447
|
+
},
|
|
396
448
|
}))
|
|
397
449
|
}
|
|
398
450
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
use crate::transport::test_support::OfflineTransport;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
fn pane(session: &str, tty: &str) -> PaneInfo {
|
|
6
|
+
PaneInfo {
|
|
7
|
+
pane_id: PaneId::new("%7"),
|
|
8
|
+
session: SessionName::new(session),
|
|
9
|
+
window_index: Some(0),
|
|
10
|
+
window_name: Some(WindowName::new("leader")),
|
|
11
|
+
pane_index: Some(0),
|
|
12
|
+
tty: Some(tty.to_string()),
|
|
13
|
+
current_command: Some("codex".to_string()),
|
|
14
|
+
current_path: None,
|
|
15
|
+
active: true,
|
|
16
|
+
pane_pid: Some(77),
|
|
17
|
+
leader_env: BTreeMap::new(),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn receiver(socket: &str) -> serde_json::Value {
|
|
22
|
+
serde_json::json!({
|
|
23
|
+
"mode": "direct_tmux",
|
|
24
|
+
"status": "attached",
|
|
25
|
+
"pane_id": "%7",
|
|
26
|
+
"tmux_socket": socket,
|
|
27
|
+
"session_name": "old-session",
|
|
28
|
+
"window_name": "leader",
|
|
29
|
+
"pane_tty": "/dev/old",
|
|
30
|
+
"fingerprint": "old-session|0|0|/dev/old"
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[test]
|
|
35
|
+
fn live_direct_channel_accepts_session_and_tty_drift_as_diagnostics() {
|
|
36
|
+
let socket = "/tmp/team-agent-channel-a";
|
|
37
|
+
let workspace = Path::new("/tmp/team-agent-workspace-a");
|
|
38
|
+
let transport = OfflineTransport::new()
|
|
39
|
+
.with_tmux_endpoint(socket)
|
|
40
|
+
.with_targets(vec![pane("new-session", "/dev/new")]);
|
|
41
|
+
|
|
42
|
+
let LeaderChannelResolution::Live(LiveLeaderChannel::DirectTmux(channel)) =
|
|
43
|
+
resolve_live_leader_channel(workspace, &receiver(socket), &transport)
|
|
44
|
+
else {
|
|
45
|
+
panic!("same socket and pane must remain a live channel");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
assert_eq!(channel.pane_id, "%7");
|
|
49
|
+
assert_eq!(channel.tmux_socket.as_deref(), Some(socket));
|
|
50
|
+
assert_eq!(
|
|
51
|
+
channel.metadata_drift,
|
|
52
|
+
vec!["session_name", "pane_tty", "fingerprint"]
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn legacy_receiver_without_status_still_uses_live_channel_facts() {
|
|
58
|
+
let socket = "/tmp/team-agent-channel-a";
|
|
59
|
+
let workspace = Path::new("/tmp/team-agent-workspace-a");
|
|
60
|
+
let mut receiver = receiver(socket);
|
|
61
|
+
if let Some(receiver) = receiver.as_object_mut() {
|
|
62
|
+
receiver.remove("status");
|
|
63
|
+
}
|
|
64
|
+
let transport = OfflineTransport::new()
|
|
65
|
+
.with_tmux_endpoint(socket)
|
|
66
|
+
.with_targets(vec![pane("new-session", "/dev/new")]);
|
|
67
|
+
|
|
68
|
+
assert!(matches!(
|
|
69
|
+
resolve_live_leader_channel(workspace, &receiver, &transport),
|
|
70
|
+
LeaderChannelResolution::Live(LiveLeaderChannel::DirectTmux(_))
|
|
71
|
+
));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[test]
|
|
75
|
+
fn explicit_non_attached_status_still_refuses_before_liveness() {
|
|
76
|
+
let socket = "/tmp/team-agent-channel-a";
|
|
77
|
+
let workspace = Path::new("/tmp/team-agent-workspace-a");
|
|
78
|
+
let mut receiver = receiver(socket);
|
|
79
|
+
receiver["status"] = serde_json::json!("rebind_required");
|
|
80
|
+
let transport = OfflineTransport::new()
|
|
81
|
+
.with_tmux_endpoint(socket)
|
|
82
|
+
.with_targets(vec![pane("new-session", "/dev/new")]);
|
|
83
|
+
|
|
84
|
+
assert_eq!(
|
|
85
|
+
resolve_live_leader_channel(workspace, &receiver, &transport),
|
|
86
|
+
LeaderChannelResolution::Unbound(LeaderChannelUnbound::ReceiverNotAttached)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[test]
|
|
91
|
+
fn legacy_receiver_without_status_still_rejects_a_foreign_workspace() {
|
|
92
|
+
let socket = "/tmp/team-agent-channel-a";
|
|
93
|
+
let workspace = Path::new("/tmp/team-agent-workspace-a");
|
|
94
|
+
let mut receiver = receiver(socket);
|
|
95
|
+
if let Some(receiver) = receiver.as_object_mut() {
|
|
96
|
+
receiver.remove("status");
|
|
97
|
+
}
|
|
98
|
+
let mut observed = pane("foreign-session", "/dev/foreign");
|
|
99
|
+
observed.current_path = Some(Path::new("/tmp/team-agent-workspace-b").to_path_buf());
|
|
100
|
+
let transport = OfflineTransport::new()
|
|
101
|
+
.with_tmux_endpoint(socket)
|
|
102
|
+
.with_targets(vec![observed]);
|
|
103
|
+
|
|
104
|
+
assert_eq!(
|
|
105
|
+
resolve_live_leader_channel(workspace, &receiver, &transport),
|
|
106
|
+
LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn same_pane_id_on_foreign_socket_is_not_a_live_channel() {
|
|
112
|
+
let workspace = Path::new("/tmp/team-agent-workspace-a");
|
|
113
|
+
let transport = OfflineTransport::new()
|
|
114
|
+
.with_tmux_endpoint("/tmp/team-agent-channel-b")
|
|
115
|
+
.with_targets(vec![pane("old-session", "/dev/old")]);
|
|
116
|
+
|
|
117
|
+
assert_eq!(
|
|
118
|
+
resolve_live_leader_channel(
|
|
119
|
+
workspace,
|
|
120
|
+
&receiver("/tmp/team-agent-channel-a"),
|
|
121
|
+
&transport,
|
|
122
|
+
),
|
|
123
|
+
LeaderChannelResolution::Unbound(LeaderChannelUnbound::EndpointMismatch)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn attached_record_without_live_pane_is_not_a_live_channel() {
|
|
129
|
+
let socket = "/tmp/team-agent-channel-a";
|
|
130
|
+
let workspace = Path::new("/tmp/team-agent-workspace-a");
|
|
131
|
+
let transport = OfflineTransport::new().with_tmux_endpoint(socket);
|
|
132
|
+
|
|
133
|
+
assert_eq!(
|
|
134
|
+
resolve_live_leader_channel(workspace, &receiver(socket), &transport),
|
|
135
|
+
LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneNotLive)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn live_direct_channel_accepts_a_pane_inside_the_target_workspace() {
|
|
141
|
+
let socket = "/tmp/team-agent-channel-a";
|
|
142
|
+
let workspace = Path::new("/tmp/team-agent-workspace-a");
|
|
143
|
+
let mut observed = pane("new-session", "/dev/new");
|
|
144
|
+
observed.current_path = Some(workspace.join("nested"));
|
|
145
|
+
let transport = OfflineTransport::new()
|
|
146
|
+
.with_tmux_endpoint(socket)
|
|
147
|
+
.with_targets(vec![observed]);
|
|
148
|
+
|
|
149
|
+
assert!(matches!(
|
|
150
|
+
resolve_live_leader_channel(workspace, &receiver(socket), &transport),
|
|
151
|
+
LeaderChannelResolution::Live(LiveLeaderChannel::DirectTmux(_))
|
|
152
|
+
));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[test]
|
|
156
|
+
fn same_socket_and_pane_in_a_foreign_workspace_is_not_a_live_leader() {
|
|
157
|
+
let socket = "/tmp/team-agent-channel-a";
|
|
158
|
+
let workspace = Path::new("/tmp/team-agent-workspace-a");
|
|
159
|
+
let mut observed = pane("foreign-session", "/dev/foreign");
|
|
160
|
+
observed.current_path = Some(Path::new("/tmp/team-agent-workspace-b").to_path_buf());
|
|
161
|
+
let transport = OfflineTransport::new()
|
|
162
|
+
.with_tmux_endpoint(socket)
|
|
163
|
+
.with_targets(vec![observed]);
|
|
164
|
+
|
|
165
|
+
assert_eq!(
|
|
166
|
+
resolve_live_leader_channel(workspace, &receiver(socket), &transport),
|
|
167
|
+
LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch)
|
|
168
|
+
);
|
|
169
|
+
}
|