@team-agent/installer 0.5.10 → 0.5.11
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/diagnose.rs +18 -0
- package/crates/team-agent/src/cli/mod.rs +9 -0
- package/crates/team-agent/src/cli/named_address.rs +16 -1
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +100 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +2 -0
- package/crates/team-agent/src/messaging/delivery.rs +244 -45
- package/crates/team-agent/src/messaging/send.rs +36 -1
- package/crates/team-agent/src/topology.rs +152 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -6,6 +6,12 @@ use crate::transport::Transport;
|
|
|
6
6
|
pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value, Value) {
|
|
7
7
|
let mut issues = Vec::new();
|
|
8
8
|
let mut repairs = Vec::new();
|
|
9
|
+
for issue in crate::topology::diagnose_topology_issues(state, backend) {
|
|
10
|
+
if let Some(id) = crate::topology::issue_id(&issue) {
|
|
11
|
+
repairs.push(topology_repair_hint(id));
|
|
12
|
+
}
|
|
13
|
+
issues.push(issue);
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
if let Some(session_name) = state
|
|
11
17
|
.get("session_name")
|
|
@@ -159,6 +165,18 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
|
|
|
159
165
|
(Value::Array(issues), Value::Array(repairs))
|
|
160
166
|
}
|
|
161
167
|
|
|
168
|
+
fn topology_repair_hint(issue: &str) -> Value {
|
|
169
|
+
json!({
|
|
170
|
+
"issue": issue,
|
|
171
|
+
"action_required": true,
|
|
172
|
+
"advisory": true,
|
|
173
|
+
"broken_class": issue,
|
|
174
|
+
"hint_action": "team-agent diagnose --json",
|
|
175
|
+
"dedupe_key": issue,
|
|
176
|
+
"action": "repair the tmux topology mismatch, then rerun team-agent restart",
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
162
180
|
/// 0.4.x (CR R2): pull (pane_id, provider_label) from leader_receiver. Used
|
|
163
181
|
/// by `leader_provider_health` reconcile in diagnose. Returns None when the
|
|
164
182
|
/// leader is not attached or any field is missing.
|
|
@@ -3309,12 +3309,21 @@ pub mod lifecycle_port {
|
|
|
3309
3309
|
session_name,
|
|
3310
3310
|
reason,
|
|
3311
3311
|
error,
|
|
3312
|
+
issue_ids,
|
|
3312
3313
|
} => json!({
|
|
3313
3314
|
"ok": false,
|
|
3314
3315
|
"status": "refused_dirty_topology",
|
|
3315
3316
|
"reason": reason,
|
|
3316
3317
|
"session_name": session_name,
|
|
3317
3318
|
"error": error,
|
|
3319
|
+
"issues": issue_ids
|
|
3320
|
+
.iter()
|
|
3321
|
+
.map(|id| json!({"id": id}))
|
|
3322
|
+
.collect::<Vec<_>>(),
|
|
3323
|
+
"next_actions": [
|
|
3324
|
+
"run team-agent diagnose --json from the intended leader socket",
|
|
3325
|
+
"repair the tmux endpoint/socket split before retrying restart"
|
|
3326
|
+
],
|
|
3318
3327
|
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
3319
3328
|
}),
|
|
3320
3329
|
}
|
|
@@ -721,8 +721,20 @@ fn resolve_leader(
|
|
|
721
721
|
let targets = list_targets(transport)?;
|
|
722
722
|
let matches = targets
|
|
723
723
|
.iter()
|
|
724
|
-
.filter(|target|
|
|
724
|
+
.filter(|target| {
|
|
725
|
+
target.pane_id.as_str() == pane_id
|
|
726
|
+
&& session.is_none_or(|expected| target.session.as_str() == expected)
|
|
727
|
+
&& window.is_none_or(|expected| {
|
|
728
|
+
target
|
|
729
|
+
.window_name
|
|
730
|
+
.as_ref()
|
|
731
|
+
.is_some_and(|name| name.as_str() == expected)
|
|
732
|
+
})
|
|
733
|
+
})
|
|
725
734
|
.collect::<Vec<_>>();
|
|
735
|
+
let stale_pane_seen = targets
|
|
736
|
+
.iter()
|
|
737
|
+
.any(|target| target.pane_id.as_str() == pane_id);
|
|
726
738
|
match matches.len() {
|
|
727
739
|
1 => Ok(ResolvedNamedAddress {
|
|
728
740
|
raw_name: parsed.display_name(),
|
|
@@ -751,6 +763,9 @@ fn resolve_leader(
|
|
|
751
763
|
window,
|
|
752
764
|
socket,
|
|
753
765
|
);
|
|
766
|
+
if stale_pane_seen {
|
|
767
|
+
err.log = format!("{} stale_tuple_mismatch=true", err.log);
|
|
768
|
+
}
|
|
754
769
|
err.candidates =
|
|
755
770
|
leader_advisory_candidates(state, team, transport, "state_recorded_socket");
|
|
756
771
|
Err(err)
|
|
@@ -88,6 +88,7 @@ pub mod platform;
|
|
|
88
88
|
// 0.3.28 — unified adaptive layout manager (single source of truth for tmux
|
|
89
89
|
// topology decisions). See `.team/artifacts/adaptive-layout-full-architecture-locate.md`.
|
|
90
90
|
pub mod layout;
|
|
91
|
+
pub mod topology;
|
|
91
92
|
pub mod mcp_server;
|
|
92
93
|
pub mod cli;
|
|
93
94
|
pub mod packaging;
|
|
@@ -127,7 +127,12 @@ pub(crate) fn start_agent_at_paths(
|
|
|
127
127
|
// state — assert_topology_invariants from Step 1 catches the
|
|
128
128
|
// upstream corruption.
|
|
129
129
|
let has_collision = pane_conflicts_with_leader_or_other(&state, agent_id, &raw_agent);
|
|
130
|
-
if
|
|
130
|
+
let noop_pane = if adaptive_layout {
|
|
131
|
+
None
|
|
132
|
+
} else {
|
|
133
|
+
single_live_pane_for_window(transport, &session_name, &window)
|
|
134
|
+
};
|
|
135
|
+
if has_collision && noop_pane.is_none() {
|
|
131
136
|
eprintln!(
|
|
132
137
|
"team_agent::layout e51_collision_post_step2 agent_id=`{agent_id}` \
|
|
133
138
|
action=forcing_fresh_spawn \
|
|
@@ -135,9 +140,17 @@ pub(crate) fn start_agent_at_paths(
|
|
|
135
140
|
investigate upstream state corruption)"
|
|
136
141
|
);
|
|
137
142
|
}
|
|
138
|
-
let agent_live = agent_live && !has_collision;
|
|
143
|
+
let agent_live = agent_live && (!has_collision || noop_pane.is_some());
|
|
139
144
|
if !force && agent_live {
|
|
140
|
-
|
|
145
|
+
let old_binding = pane_binding_snapshot(&raw_agent);
|
|
146
|
+
let refreshed_binding = noop_pane.as_ref().map(pane_binding_from_live);
|
|
147
|
+
mark_agent_running_noop(
|
|
148
|
+
&mut state,
|
|
149
|
+
agent_id,
|
|
150
|
+
&session_name,
|
|
151
|
+
&window,
|
|
152
|
+
noop_pane.as_ref(),
|
|
153
|
+
)?;
|
|
141
154
|
let team_key = restart_projection_team_key(&state, team);
|
|
142
155
|
save_restart_projected_state(workspace, &mut state, &team_key, &[agent_id.as_str()])?;
|
|
143
156
|
if let Ok(spec) = load_team_spec(spec_workspace) {
|
|
@@ -146,6 +159,18 @@ pub(crate) fn start_agent_at_paths(
|
|
|
146
159
|
replay_worker_target_missing_messages(workspace, agent_id, &team_key, &state, transport)?;
|
|
147
160
|
let coordinator_started = start_coordinator_for_workspace(workspace)?;
|
|
148
161
|
let target = format!("{}:{window}", session_name.as_str());
|
|
162
|
+
if let Some(new_binding) = refreshed_binding.as_ref() {
|
|
163
|
+
if old_binding.as_ref() != Some(new_binding) {
|
|
164
|
+
write_agent_pane_binding_refreshed_event(
|
|
165
|
+
workspace,
|
|
166
|
+
agent_id,
|
|
167
|
+
&session_name,
|
|
168
|
+
&window,
|
|
169
|
+
old_binding.as_ref(),
|
|
170
|
+
new_binding,
|
|
171
|
+
)?;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
149
174
|
write_start_agent_noop_event(workspace, agent_id, &target, coordinator_started)?;
|
|
150
175
|
return Ok(StartAgentOutcome::Noop {
|
|
151
176
|
env: AgentActionEnvelope {
|
|
@@ -373,6 +398,78 @@ fn verify_spawned_pane_matches_target(
|
|
|
373
398
|
Ok(())
|
|
374
399
|
}
|
|
375
400
|
|
|
401
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
402
|
+
struct PaneBindingSnapshot {
|
|
403
|
+
pane_id: String,
|
|
404
|
+
pane_pid: Option<u32>,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
fn pane_binding_snapshot(agent: &serde_json::Value) -> Option<PaneBindingSnapshot> {
|
|
408
|
+
let pane_id = agent
|
|
409
|
+
.get("pane_id")
|
|
410
|
+
.and_then(serde_json::Value::as_str)
|
|
411
|
+
.filter(|pane| !pane.is_empty())?;
|
|
412
|
+
Some(PaneBindingSnapshot {
|
|
413
|
+
pane_id: pane_id.to_string(),
|
|
414
|
+
pane_pid: agent
|
|
415
|
+
.get("pane_pid")
|
|
416
|
+
.and_then(serde_json::Value::as_u64)
|
|
417
|
+
.map(|pid| pid as u32),
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
fn pane_binding_from_live(pane: &crate::transport::PaneInfo) -> PaneBindingSnapshot {
|
|
422
|
+
PaneBindingSnapshot {
|
|
423
|
+
pane_id: pane.pane_id.as_str().to_string(),
|
|
424
|
+
pane_pid: pane.pane_pid,
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
fn single_live_pane_for_window(
|
|
429
|
+
transport: &dyn crate::transport::Transport,
|
|
430
|
+
session: &crate::transport::SessionName,
|
|
431
|
+
window: &str,
|
|
432
|
+
) -> Option<crate::transport::PaneInfo> {
|
|
433
|
+
let targets = transport.list_targets().ok()?;
|
|
434
|
+
let mut matches = targets.into_iter().filter(|target| {
|
|
435
|
+
target.session.as_str() == session.as_str()
|
|
436
|
+
&& target
|
|
437
|
+
.window_name
|
|
438
|
+
.as_ref()
|
|
439
|
+
.is_some_and(|name| name.as_str() == window)
|
|
440
|
+
});
|
|
441
|
+
let pane = matches.next()?;
|
|
442
|
+
if matches.next().is_some() {
|
|
443
|
+
return None;
|
|
444
|
+
}
|
|
445
|
+
Some(pane)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
fn write_agent_pane_binding_refreshed_event(
|
|
449
|
+
workspace: &Path,
|
|
450
|
+
agent_id: &AgentId,
|
|
451
|
+
session: &crate::transport::SessionName,
|
|
452
|
+
window: &str,
|
|
453
|
+
old: Option<&PaneBindingSnapshot>,
|
|
454
|
+
new: &PaneBindingSnapshot,
|
|
455
|
+
) -> Result<(), LifecycleError> {
|
|
456
|
+
crate::event_log::EventLog::new(workspace)
|
|
457
|
+
.write(
|
|
458
|
+
"agent_pane_binding_refreshed",
|
|
459
|
+
serde_json::json!({
|
|
460
|
+
"agent_id": agent_id.as_str(),
|
|
461
|
+
"session": session.as_str(),
|
|
462
|
+
"window": window,
|
|
463
|
+
"old_pane_id": old.map(|binding| binding.pane_id.as_str()),
|
|
464
|
+
"old_pane_pid": old.and_then(|binding| binding.pane_pid),
|
|
465
|
+
"pane_id": new.pane_id.as_str(),
|
|
466
|
+
"pane_pid": new.pane_pid,
|
|
467
|
+
}),
|
|
468
|
+
)
|
|
469
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
470
|
+
Ok(())
|
|
471
|
+
}
|
|
472
|
+
|
|
376
473
|
/// E51 (0.3.26 P0, restart self-heal): returns `true` when the agent's pane_id
|
|
377
474
|
/// is the same as the leader_receiver/team_owner pane_id OR is owned by a
|
|
378
475
|
/// different agent in the state. In both cases `start_agent` must NOT treat the
|
|
@@ -1284,6 +1284,7 @@ pub(super) fn mark_agent_running_noop(
|
|
|
1284
1284
|
agent_id: &AgentId,
|
|
1285
1285
|
session_name: &SessionName,
|
|
1286
1286
|
window: &str,
|
|
1287
|
+
pane: Option<&crate::transport::PaneInfo>,
|
|
1287
1288
|
) -> Result<(), LifecycleError> {
|
|
1288
1289
|
if !state.is_object() {
|
|
1289
1290
|
*state = serde_json::json!({});
|
|
@@ -1322,6 +1323,17 @@ pub(super) fn mark_agent_running_noop(
|
|
|
1322
1323
|
obj.insert("status".to_string(), serde_json::json!("running"));
|
|
1323
1324
|
obj.insert("agent_id".to_string(), serde_json::json!(agent_id.as_str()));
|
|
1324
1325
|
obj.insert("window".to_string(), serde_json::json!(window));
|
|
1326
|
+
if let Some(pane) = pane {
|
|
1327
|
+
obj.insert(
|
|
1328
|
+
"pane_id".to_string(),
|
|
1329
|
+
serde_json::json!(pane.pane_id.as_str()),
|
|
1330
|
+
);
|
|
1331
|
+
if let Some(pane_pid) = pane.pane_pid {
|
|
1332
|
+
obj.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
1333
|
+
} else {
|
|
1334
|
+
obj.remove("pane_pid");
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1325
1337
|
Ok(())
|
|
1326
1338
|
}
|
|
1327
1339
|
|
|
@@ -137,6 +137,32 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
137
137
|
})?;
|
|
138
138
|
let mut state = selected.state;
|
|
139
139
|
crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, None)?;
|
|
140
|
+
let topology_issue_ids = crate::topology::restart_dirty_topology_issue_ids(&state);
|
|
141
|
+
if !topology_issue_ids.is_empty() {
|
|
142
|
+
let session_name = state
|
|
143
|
+
.get("session_name")
|
|
144
|
+
.and_then(serde_json::Value::as_str)
|
|
145
|
+
.unwrap_or_default()
|
|
146
|
+
.to_string();
|
|
147
|
+
crate::event_log::EventLog::new(&selected.run_workspace)
|
|
148
|
+
.write(
|
|
149
|
+
"restart.refused_dirty_topology",
|
|
150
|
+
serde_json::json!({
|
|
151
|
+
"session_name": session_name,
|
|
152
|
+
"issues": topology_issue_ids.iter().map(|id| serde_json::json!({"id": id})).collect::<Vec<_>>(),
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
156
|
+
return Ok(RestartReport::RefusedDirtyTopology {
|
|
157
|
+
session_name,
|
|
158
|
+
reason: topology_issue_ids
|
|
159
|
+
.first()
|
|
160
|
+
.cloned()
|
|
161
|
+
.unwrap_or_else(|| "dirty_topology".to_string()),
|
|
162
|
+
error: "restart refused: tmux endpoint/socket topology is inconsistent; run diagnose from the intended leader socket before restarting".to_string(),
|
|
163
|
+
issue_ids: topology_issue_ids,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
140
166
|
// E5 task#3 / RC-A6a + E4(leader 裁定:每次 restart 都从角色定义重建 runtime spec,覆盖):
|
|
141
167
|
// 角色定义=第一真相源。角色齐 → compile_team 重建 + 保留运行期 override(session_name)+
|
|
142
168
|
// 写 runtime spec。角色缺(TEAM.md/agents 不在)→ 显式拒(列缺哪些),旧 spec 原地保留不删不用。
|
|
@@ -278,6 +304,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
278
304
|
launcher session ({reason}); aborting before any tmux kill. Repair \
|
|
279
305
|
state.session_name to the worker session and re-run."
|
|
280
306
|
),
|
|
307
|
+
issue_ids: vec![reason.clone()],
|
|
281
308
|
});
|
|
282
309
|
}
|
|
283
310
|
}
|
|
@@ -103,6 +103,42 @@ pub fn tmux_pane_width(transport: &dyn Transport, target: &Target) -> PaneWidthQ
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
pub(crate) fn worker_target_missing_due_to_stale_binding(
|
|
107
|
+
state: &serde_json::Value,
|
|
108
|
+
recipient: &str,
|
|
109
|
+
transport: &dyn Transport,
|
|
110
|
+
) -> bool {
|
|
111
|
+
let Some(agent) = state.get("agents").and_then(|agents| agents.get(recipient)) else {
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
let session = state
|
|
115
|
+
.get("session_name")
|
|
116
|
+
.and_then(serde_json::Value::as_str)
|
|
117
|
+
.unwrap_or_default();
|
|
118
|
+
let window = agent
|
|
119
|
+
.get("window")
|
|
120
|
+
.and_then(serde_json::Value::as_str)
|
|
121
|
+
.filter(|s| !s.is_empty())
|
|
122
|
+
.unwrap_or(recipient);
|
|
123
|
+
let Some(cached_pane) = agent
|
|
124
|
+
.get("pane_id")
|
|
125
|
+
.and_then(serde_json::Value::as_str)
|
|
126
|
+
.filter(|s| !s.is_empty())
|
|
127
|
+
else {
|
|
128
|
+
return false;
|
|
129
|
+
};
|
|
130
|
+
let Ok(live_targets) = transport.list_targets() else {
|
|
131
|
+
return false;
|
|
132
|
+
};
|
|
133
|
+
if live_pane_for_session_window(&live_targets, session, window).is_some() {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
live_targets.iter().any(|target| {
|
|
137
|
+
target.pane_id.as_str() == cached_pane
|
|
138
|
+
&& stale_worker_pane_binding(target, session, window).is_some()
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
/// `_deliver_pending_message` (`delivery.py:63`):对一条消息做 tmux 注入投递 (含 trust 提示
|
|
107
143
|
/// 自动应答 + turn-open arm + first_send_at 戳)。daemon-path → Result。
|
|
108
144
|
pub fn deliver_pending_message(
|
|
@@ -323,7 +359,11 @@ pub fn deliver_pending_message(
|
|
|
323
359
|
channel: Some("rebind_required".to_string()),
|
|
324
360
|
});
|
|
325
361
|
}
|
|
326
|
-
let
|
|
362
|
+
let resolved = resolve_inject_target(state, &message.recipient, transport, &live_targets);
|
|
363
|
+
if let Some(stale) = resolved.stale_binding.as_ref() {
|
|
364
|
+
event_log.write("worker_pane_binding_stale", stale.as_event_payload(message_id, &message.recipient))?;
|
|
365
|
+
}
|
|
366
|
+
let target = resolved.target.clone();
|
|
327
367
|
if let Some(outcome) = block_missing_worker_target(
|
|
328
368
|
store,
|
|
329
369
|
event_log,
|
|
@@ -467,6 +507,7 @@ pub fn deliver_pending_message(
|
|
|
467
507
|
message_id,
|
|
468
508
|
event_log,
|
|
469
509
|
canonical_owner_team_id.as_deref(),
|
|
510
|
+
resolved.metadata.as_ref(),
|
|
470
511
|
)?;
|
|
471
512
|
let submit_verified = inject_submit_verified(&inject_report);
|
|
472
513
|
let readback_verified = pane_readback_verified(&inject_report);
|
|
@@ -554,14 +595,13 @@ pub fn deliver_pending_message(
|
|
|
554
595
|
let token_marker = format!("[team-agent-token:{message_id}]");
|
|
555
596
|
let grace = std::time::Duration::from_millis(200);
|
|
556
597
|
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
|
|
557
|
-
let
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
break;
|
|
598
|
+
let transcript_has_token = loop {
|
|
599
|
+
let found = rollout_tail_contains(&rollout_path, &token_marker, 64 * 1024);
|
|
600
|
+
if found || std::time::Instant::now() >= deadline {
|
|
601
|
+
break found;
|
|
562
602
|
}
|
|
563
603
|
std::thread::sleep(grace);
|
|
564
|
-
}
|
|
604
|
+
};
|
|
565
605
|
if !transcript_has_token {
|
|
566
606
|
// Loud but non-fatal: emit a mismatch event for diagnose/status
|
|
567
607
|
// observability. Do NOT mark delivered — degrade to
|
|
@@ -606,10 +646,11 @@ pub fn deliver_pending_message(
|
|
|
606
646
|
}
|
|
607
647
|
}
|
|
608
648
|
store.mark(message_id, "delivered", None)?;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
649
|
+
let mut delivered_event = serde_json::json!({"message_id": message_id});
|
|
650
|
+
if let Some(metadata) = resolved.metadata.as_ref() {
|
|
651
|
+
append_target_metadata(&mut delivered_event, metadata);
|
|
652
|
+
}
|
|
653
|
+
event_log.write("message.delivered", delivered_event)?;
|
|
613
654
|
let outcome = DeliveryOutcome {
|
|
614
655
|
ok: true,
|
|
615
656
|
status: DeliveryStatus::Delivered,
|
|
@@ -633,6 +674,7 @@ pub fn deliver_pending_message(
|
|
|
633
674
|
&outcome,
|
|
634
675
|
event_log,
|
|
635
676
|
canonical_owner_team_id.as_deref(),
|
|
677
|
+
resolved.metadata.as_ref(),
|
|
636
678
|
)?;
|
|
637
679
|
Ok(outcome)
|
|
638
680
|
}
|
|
@@ -1056,15 +1098,61 @@ fn app_server_rebind_action(owner_team_id: Option<&str>, receiver: &serde_json::
|
|
|
1056
1098
|
/// pane. U1-A real-machine fix is deferred to v0.3.25 (writer-shape + projection +
|
|
1057
1099
|
/// rediscover-writer triad). U1-B jitter defer at try_deliver_message:213-237 and
|
|
1058
1100
|
/// U1-C-Tail at the startup-prompt peek site are unchanged.
|
|
1101
|
+
#[derive(Clone)]
|
|
1102
|
+
struct ResolvedInjectTarget {
|
|
1103
|
+
target: Target,
|
|
1104
|
+
metadata: Option<TargetMetadata>,
|
|
1105
|
+
stale_binding: Option<WorkerPaneBindingStale>,
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
#[derive(Clone)]
|
|
1109
|
+
struct TargetMetadata {
|
|
1110
|
+
tmux_endpoint: Option<String>,
|
|
1111
|
+
target_session: String,
|
|
1112
|
+
target_window: String,
|
|
1113
|
+
target_pane_id: String,
|
|
1114
|
+
target_pane_pid: Option<u32>,
|
|
1115
|
+
resolved_from: &'static str,
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
#[derive(Clone)]
|
|
1119
|
+
struct WorkerPaneBindingStale {
|
|
1120
|
+
cached_pane_id: String,
|
|
1121
|
+
expected_session: String,
|
|
1122
|
+
expected_window: String,
|
|
1123
|
+
observed_session: String,
|
|
1124
|
+
observed_window: String,
|
|
1125
|
+
observed_pane_pid: Option<u32>,
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
impl WorkerPaneBindingStale {
|
|
1129
|
+
fn as_event_payload(&self, message_id: &str, recipient: &str) -> serde_json::Value {
|
|
1130
|
+
serde_json::json!({
|
|
1131
|
+
"message_id": message_id,
|
|
1132
|
+
"recipient": recipient,
|
|
1133
|
+
"cached_pane_id": self.cached_pane_id,
|
|
1134
|
+
"expected_session": self.expected_session,
|
|
1135
|
+
"expected_window": self.expected_window,
|
|
1136
|
+
"observed_session": self.observed_session,
|
|
1137
|
+
"observed_window": self.observed_window,
|
|
1138
|
+
"observed_pane_pid": self.observed_pane_pid,
|
|
1139
|
+
})
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1059
1143
|
fn resolve_inject_target(
|
|
1060
1144
|
state: &serde_json::Value,
|
|
1061
1145
|
recipient: &str,
|
|
1062
1146
|
transport: &dyn Transport,
|
|
1063
1147
|
live_targets: &[PaneInfo],
|
|
1064
|
-
) ->
|
|
1148
|
+
) -> ResolvedInjectTarget {
|
|
1065
1149
|
if recipient == "leader" {
|
|
1066
1150
|
if let Some(pane_id) = leader_receiver_pane_id(state) {
|
|
1067
|
-
return
|
|
1151
|
+
return ResolvedInjectTarget {
|
|
1152
|
+
target: Target::Pane(PaneId::new(pane_id)),
|
|
1153
|
+
metadata: None,
|
|
1154
|
+
stale_binding: None,
|
|
1155
|
+
};
|
|
1068
1156
|
}
|
|
1069
1157
|
}
|
|
1070
1158
|
let agent = state.get("agents").and_then(|a| a.get(recipient));
|
|
@@ -1096,37 +1184,71 @@ fn resolve_inject_target(
|
|
|
1096
1184
|
if leader_receiver_pane_binding(state).is_some_and(|leader| {
|
|
1097
1185
|
pane_conflicts_on_same_socket(pane.as_str(), worker_socket.as_deref(), leader)
|
|
1098
1186
|
}) {
|
|
1099
|
-
return
|
|
1100
|
-
|
|
1101
|
-
|
|
1187
|
+
return ResolvedInjectTarget {
|
|
1188
|
+
target: Target::SessionWindow {
|
|
1189
|
+
session: SessionName::new(session),
|
|
1190
|
+
window: WindowName::new(format!("{recipient}_pane_conflicts_with_leader")),
|
|
1191
|
+
},
|
|
1192
|
+
metadata: None,
|
|
1193
|
+
stale_binding: None,
|
|
1102
1194
|
};
|
|
1103
1195
|
}
|
|
1104
|
-
if live_targets
|
|
1105
|
-
.iter()
|
|
1106
|
-
.any(|target| target.pane_id.as_str() == pane.as_str())
|
|
1107
|
-
{
|
|
1108
|
-
return Target::Pane(pane.clone());
|
|
1109
|
-
}
|
|
1110
1196
|
}
|
|
1197
|
+
let stale_binding = cached_pane.as_ref().and_then(|pane| {
|
|
1198
|
+
live_targets
|
|
1199
|
+
.iter()
|
|
1200
|
+
.find(|target| target.pane_id.as_str() == pane.as_str())
|
|
1201
|
+
.and_then(|target| stale_worker_pane_binding(target, session, window))
|
|
1202
|
+
});
|
|
1111
1203
|
if let Some(live_pane) = live_pane_for_session_window(live_targets, session, window) {
|
|
1112
|
-
return
|
|
1204
|
+
return ResolvedInjectTarget {
|
|
1205
|
+
target: Target::Pane(live_pane.pane_id.clone()),
|
|
1206
|
+
metadata: Some(target_metadata(
|
|
1207
|
+
transport,
|
|
1208
|
+
live_pane,
|
|
1209
|
+
"session_window_lookup",
|
|
1210
|
+
)),
|
|
1211
|
+
stale_binding,
|
|
1212
|
+
};
|
|
1113
1213
|
}
|
|
1114
1214
|
if let Some(pane) = cached_pane {
|
|
1115
|
-
if
|
|
1116
|
-
|
|
1215
|
+
if let Some(live_pane) = live_targets.iter().find(|target| {
|
|
1216
|
+
target.pane_id.as_str() == pane.as_str()
|
|
1217
|
+
&& target.session.as_str() == session
|
|
1218
|
+
&& target
|
|
1219
|
+
.window_name
|
|
1220
|
+
.as_ref()
|
|
1221
|
+
.is_some_and(|name| name.as_str() == window)
|
|
1222
|
+
}) {
|
|
1223
|
+
return ResolvedInjectTarget {
|
|
1224
|
+
target: Target::Pane(pane),
|
|
1225
|
+
metadata: Some(target_metadata(transport, live_pane, "validated_cached_pane")),
|
|
1226
|
+
stale_binding: None,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
if live_targets.is_empty() {
|
|
1230
|
+
return ResolvedInjectTarget {
|
|
1231
|
+
target: Target::Pane(pane),
|
|
1232
|
+
metadata: None,
|
|
1233
|
+
stale_binding: None,
|
|
1234
|
+
};
|
|
1117
1235
|
}
|
|
1118
1236
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1237
|
+
ResolvedInjectTarget {
|
|
1238
|
+
target: Target::SessionWindow {
|
|
1239
|
+
session: SessionName::new(session),
|
|
1240
|
+
window: WindowName::new(window),
|
|
1241
|
+
},
|
|
1242
|
+
metadata: None,
|
|
1243
|
+
stale_binding,
|
|
1122
1244
|
}
|
|
1123
1245
|
}
|
|
1124
1246
|
|
|
1125
|
-
fn live_pane_for_session_window(
|
|
1126
|
-
targets: &[PaneInfo],
|
|
1247
|
+
fn live_pane_for_session_window<'a>(
|
|
1248
|
+
targets: &'a [PaneInfo],
|
|
1127
1249
|
session: &str,
|
|
1128
1250
|
window: &str,
|
|
1129
|
-
) -> Option
|
|
1251
|
+
) -> Option<&'a PaneInfo> {
|
|
1130
1252
|
targets
|
|
1131
1253
|
.iter()
|
|
1132
1254
|
.find(|target| {
|
|
@@ -1136,14 +1258,86 @@ fn live_pane_for_session_window(
|
|
|
1136
1258
|
.as_ref()
|
|
1137
1259
|
.is_some_and(|name| name.as_str() == window)
|
|
1138
1260
|
})
|
|
1139
|
-
.map(|target| target.pane_id.clone())
|
|
1140
1261
|
}
|
|
1141
1262
|
|
|
1142
|
-
fn
|
|
1143
|
-
|
|
1144
|
-
|
|
1263
|
+
fn stale_worker_pane_binding(
|
|
1264
|
+
observed: &PaneInfo,
|
|
1265
|
+
expected_session: &str,
|
|
1266
|
+
expected_window: &str,
|
|
1267
|
+
) -> Option<WorkerPaneBindingStale> {
|
|
1268
|
+
let observed_window = observed
|
|
1269
|
+
.window_name
|
|
1270
|
+
.as_ref()
|
|
1271
|
+
.map(|window| window.as_str())
|
|
1272
|
+
.unwrap_or_default();
|
|
1273
|
+
if observed.session.as_str() == expected_session && observed_window == expected_window {
|
|
1274
|
+
return None;
|
|
1275
|
+
}
|
|
1276
|
+
Some(WorkerPaneBindingStale {
|
|
1277
|
+
cached_pane_id: observed.pane_id.as_str().to_string(),
|
|
1278
|
+
expected_session: expected_session.to_string(),
|
|
1279
|
+
expected_window: expected_window.to_string(),
|
|
1280
|
+
observed_session: observed.session.as_str().to_string(),
|
|
1281
|
+
observed_window: observed_window.to_string(),
|
|
1282
|
+
observed_pane_pid: observed.pane_pid,
|
|
1283
|
+
})
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
fn target_metadata(
|
|
1287
|
+
transport: &dyn Transport,
|
|
1288
|
+
pane: &PaneInfo,
|
|
1289
|
+
resolved_from: &'static str,
|
|
1290
|
+
) -> TargetMetadata {
|
|
1291
|
+
TargetMetadata {
|
|
1292
|
+
tmux_endpoint: transport.tmux_endpoint(),
|
|
1293
|
+
target_session: pane.session.as_str().to_string(),
|
|
1294
|
+
target_window: pane
|
|
1295
|
+
.window_name
|
|
1296
|
+
.as_ref()
|
|
1297
|
+
.map(|window| window.as_str().to_string())
|
|
1298
|
+
.unwrap_or_default(),
|
|
1299
|
+
target_pane_id: pane.pane_id.as_str().to_string(),
|
|
1300
|
+
target_pane_pid: pane.pane_pid,
|
|
1301
|
+
resolved_from,
|
|
1145
1302
|
}
|
|
1146
|
-
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
fn append_target_metadata(event: &mut serde_json::Value, metadata: &TargetMetadata) {
|
|
1306
|
+
let Some(obj) = event.as_object_mut() else {
|
|
1307
|
+
return;
|
|
1308
|
+
};
|
|
1309
|
+
obj.insert("target_kind".to_string(), serde_json::json!("pane"));
|
|
1310
|
+
obj.insert(
|
|
1311
|
+
"tmux_endpoint".to_string(),
|
|
1312
|
+
metadata
|
|
1313
|
+
.tmux_endpoint
|
|
1314
|
+
.as_ref()
|
|
1315
|
+
.map(|endpoint| serde_json::json!(endpoint))
|
|
1316
|
+
.unwrap_or(serde_json::Value::Null),
|
|
1317
|
+
);
|
|
1318
|
+
obj.insert(
|
|
1319
|
+
"target_session".to_string(),
|
|
1320
|
+
serde_json::json!(metadata.target_session),
|
|
1321
|
+
);
|
|
1322
|
+
obj.insert(
|
|
1323
|
+
"target_window".to_string(),
|
|
1324
|
+
serde_json::json!(metadata.target_window),
|
|
1325
|
+
);
|
|
1326
|
+
obj.insert(
|
|
1327
|
+
"target_pane_id".to_string(),
|
|
1328
|
+
serde_json::json!(metadata.target_pane_id),
|
|
1329
|
+
);
|
|
1330
|
+
obj.insert(
|
|
1331
|
+
"target_pane_pid".to_string(),
|
|
1332
|
+
metadata
|
|
1333
|
+
.target_pane_pid
|
|
1334
|
+
.map(|pid| serde_json::json!(pid))
|
|
1335
|
+
.unwrap_or(serde_json::Value::Null),
|
|
1336
|
+
);
|
|
1337
|
+
obj.insert(
|
|
1338
|
+
"resolved_from".to_string(),
|
|
1339
|
+
serde_json::json!(metadata.resolved_from),
|
|
1340
|
+
);
|
|
1147
1341
|
}
|
|
1148
1342
|
|
|
1149
1343
|
#[derive(Clone, Copy)]
|
|
@@ -1911,7 +2105,7 @@ pub fn record_turn_open_if_leader_to_worker(
|
|
|
1911
2105
|
) -> Result<(), MessagingError> {
|
|
1912
2106
|
let _ = state;
|
|
1913
2107
|
record_turn_open_if_leader_to_worker_scoped(
|
|
1914
|
-
workspace, sender, recipient, delivered, event_log, None,
|
|
2108
|
+
workspace, sender, recipient, delivered, event_log, None, None,
|
|
1915
2109
|
)
|
|
1916
2110
|
}
|
|
1917
2111
|
|
|
@@ -1922,6 +2116,7 @@ fn record_current_turn_after_inject_if_leader_to_worker_scoped(
|
|
|
1922
2116
|
message_id: &str,
|
|
1923
2117
|
event_log: &EventLog,
|
|
1924
2118
|
owner_team_id: Option<&str>,
|
|
2119
|
+
metadata: Option<&TargetMetadata>,
|
|
1925
2120
|
) -> Result<(), MessagingError> {
|
|
1926
2121
|
if !matches!(sender, "leader" | "Leader") || recipient == "leader" {
|
|
1927
2122
|
return Ok(());
|
|
@@ -1932,10 +2127,11 @@ fn record_current_turn_after_inject_if_leader_to_worker_scoped(
|
|
|
1932
2127
|
save_scoped_state_reapplying_after_conflict(workspace, &state, owner_team_id, |latest| {
|
|
1933
2128
|
arm_turn_open(latest, recipient, &message_id);
|
|
1934
2129
|
})?;
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2130
|
+
let mut event = serde_json::json!({"agent_id": recipient, "message_id": message_id});
|
|
2131
|
+
if let Some(metadata) = metadata {
|
|
2132
|
+
append_target_metadata(&mut event, metadata);
|
|
2133
|
+
}
|
|
2134
|
+
event_log.write("turn_open.armed_after_inject", event)?;
|
|
1939
2135
|
Ok(())
|
|
1940
2136
|
}
|
|
1941
2137
|
|
|
@@ -1946,6 +2142,7 @@ fn record_turn_open_if_leader_to_worker_scoped(
|
|
|
1946
2142
|
delivered: &DeliveryOutcome,
|
|
1947
2143
|
event_log: &EventLog,
|
|
1948
2144
|
owner_team_id: Option<&str>,
|
|
2145
|
+
metadata: Option<&TargetMetadata>,
|
|
1949
2146
|
) -> Result<(), MessagingError> {
|
|
1950
2147
|
if !delivered.ok || !matches!(sender, "leader" | "Leader") || recipient == "leader" {
|
|
1951
2148
|
return Ok(());
|
|
@@ -1955,10 +2152,12 @@ fn record_turn_open_if_leader_to_worker_scoped(
|
|
|
1955
2152
|
save_scoped_state_reapplying_after_conflict(workspace, &state, owner_team_id, |latest| {
|
|
1956
2153
|
arm_turn_open(latest, recipient, &delivered.message_id);
|
|
1957
2154
|
})?;
|
|
1958
|
-
|
|
1959
|
-
"
|
|
1960
|
-
|
|
1961
|
-
|
|
2155
|
+
let mut event =
|
|
2156
|
+
serde_json::json!({"agent_id": recipient, "message_id": delivered.message_id});
|
|
2157
|
+
if let Some(metadata) = metadata {
|
|
2158
|
+
append_target_metadata(&mut event, metadata);
|
|
2159
|
+
}
|
|
2160
|
+
event_log.write("turn_open.armed_after_delivery", event)?;
|
|
1962
2161
|
Ok(())
|
|
1963
2162
|
}
|
|
1964
2163
|
|
|
@@ -10,7 +10,7 @@ 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::{DeliveryOutcome, DeliveryRefusal, DeliveryStatus, MessagingError};
|
|
13
|
+
use super::{DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, MessagingError};
|
|
14
14
|
|
|
15
15
|
/// 发件目标:单 target / 广播 `*` / 扇出 list (`send.py:36` `target: str|list[str]|None`)。
|
|
16
16
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -182,6 +182,9 @@ pub fn send_message(
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
+
if let Some(outcome) = stale_worker_target_preflight(workspace, &state, recipient, opts)? {
|
|
186
|
+
return Ok(outcome);
|
|
187
|
+
}
|
|
185
188
|
if let Some(outcome) = coordinator_unavailable_outcome(workspace, recipient, opts, &event_log)?
|
|
186
189
|
{
|
|
187
190
|
return Ok(outcome);
|
|
@@ -231,6 +234,38 @@ pub fn send_message(
|
|
|
231
234
|
})
|
|
232
235
|
}
|
|
233
236
|
|
|
237
|
+
fn stale_worker_target_preflight(
|
|
238
|
+
workspace: &Path,
|
|
239
|
+
state: &serde_json::Value,
|
|
240
|
+
recipient: &str,
|
|
241
|
+
opts: &SendOptions,
|
|
242
|
+
) -> Result<Option<DeliveryOutcome>, MessagingError> {
|
|
243
|
+
let Ok(resolved) = crate::transport_factory::resolve_read_only_transport(
|
|
244
|
+
workspace,
|
|
245
|
+
Some(state),
|
|
246
|
+
crate::transport_factory::TransportPurpose::MessageDelivery,
|
|
247
|
+
) else {
|
|
248
|
+
return Ok(None);
|
|
249
|
+
};
|
|
250
|
+
if !super::delivery::worker_target_missing_due_to_stale_binding(
|
|
251
|
+
state,
|
|
252
|
+
recipient,
|
|
253
|
+
resolved.backend.as_ref(),
|
|
254
|
+
) {
|
|
255
|
+
return Ok(None);
|
|
256
|
+
}
|
|
257
|
+
Ok(Some(DeliveryOutcome {
|
|
258
|
+
ok: false,
|
|
259
|
+
status: DeliveryStatus::Blocked,
|
|
260
|
+
message_status: MessageStatusShadow("queued_pane_missing".to_string()),
|
|
261
|
+
message_id: opts.message_id.clone(),
|
|
262
|
+
verification: Some(format!("run team-agent start-agent {recipient} --allow-fresh")),
|
|
263
|
+
stage: Some(DeliveryStage::Inject),
|
|
264
|
+
reason: Some(DeliveryRefusal::TmuxTargetMissing),
|
|
265
|
+
channel: Some("delivery_blocked".to_string()),
|
|
266
|
+
}))
|
|
267
|
+
}
|
|
268
|
+
|
|
234
269
|
fn task_exists(state: &serde_json::Value, task_id: &TaskId) -> bool {
|
|
235
270
|
state
|
|
236
271
|
.get("tasks")
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
use serde_json::{json, Value};
|
|
2
|
+
|
|
3
|
+
use crate::transport::{SessionName, Transport};
|
|
4
|
+
|
|
5
|
+
pub const WORKER_PANE_BINDING_STALE: &str = "worker_pane_binding_stale";
|
|
6
|
+
pub const TMUX_ENDPOINT_SOCKET_CONFLICT: &str = "tmux_endpoint_socket_conflict";
|
|
7
|
+
pub const LEADER_RECEIVER_SOCKET_MISMATCH: &str = "leader_receiver_socket_mismatch";
|
|
8
|
+
pub const ORPHAN_TEAM_SESSION_ON_IGNORED_SOCKET: &str = "orphan_team_session_on_ignored_socket";
|
|
9
|
+
pub const TEAM_SESSION_MISSING_ON_CANONICAL_SOCKET: &str =
|
|
10
|
+
"team_session_missing_on_canonical_socket";
|
|
11
|
+
pub const RECENT_COORDINATOR_SESSION_MISSING: &str = "recent_coordinator_session_missing";
|
|
12
|
+
|
|
13
|
+
pub fn diagnose_topology_issues(state: &Value, backend: &dyn Transport) -> Vec<Value> {
|
|
14
|
+
let mut issues = Vec::new();
|
|
15
|
+
append_socket_split_issues(state, &mut issues, true);
|
|
16
|
+
append_worker_pane_binding_issues(state, backend, &mut issues);
|
|
17
|
+
issues
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn restart_dirty_topology_issue_ids(state: &Value) -> Vec<String> {
|
|
21
|
+
let mut issues = Vec::new();
|
|
22
|
+
append_socket_split_issues(state, &mut issues, false);
|
|
23
|
+
issues
|
|
24
|
+
.into_iter()
|
|
25
|
+
.filter_map(|issue| issue.get("id").and_then(Value::as_str).map(str::to_string))
|
|
26
|
+
.collect()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn issue_id(issue: &Value) -> Option<&str> {
|
|
30
|
+
issue.get("id").and_then(Value::as_str)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn append_socket_split_issues(state: &Value, issues: &mut Vec<Value>, include_readiness: bool) {
|
|
34
|
+
let endpoint = non_empty_str(state, "tmux_endpoint");
|
|
35
|
+
let socket = non_empty_str(state, "tmux_socket");
|
|
36
|
+
let (Some(endpoint), Some(socket)) = (endpoint, socket) else {
|
|
37
|
+
return;
|
|
38
|
+
};
|
|
39
|
+
if same_endpoint(endpoint, socket) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let session = non_empty_str(state, "session_name").unwrap_or_default();
|
|
44
|
+
issues.push(json!({
|
|
45
|
+
"id": TMUX_ENDPOINT_SOCKET_CONFLICT,
|
|
46
|
+
"tmux_endpoint": endpoint,
|
|
47
|
+
"tmux_socket": socket,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
if state
|
|
51
|
+
.get("leader_receiver")
|
|
52
|
+
.and_then(|receiver| non_empty_str(receiver, "tmux_socket"))
|
|
53
|
+
.is_some_and(|leader_socket| !same_endpoint(leader_socket, endpoint))
|
|
54
|
+
{
|
|
55
|
+
issues.push(json!({
|
|
56
|
+
"id": LEADER_RECEIVER_SOCKET_MISMATCH,
|
|
57
|
+
"tmux_endpoint": endpoint,
|
|
58
|
+
"leader_receiver_tmux_socket": state
|
|
59
|
+
.get("leader_receiver")
|
|
60
|
+
.and_then(|receiver| non_empty_str(receiver, "tmux_socket")),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if !session.is_empty() && session_exists_on_endpoint(endpoint, session) {
|
|
65
|
+
issues.push(json!({
|
|
66
|
+
"id": ORPHAN_TEAM_SESSION_ON_IGNORED_SOCKET,
|
|
67
|
+
"ignored_tmux_endpoint": endpoint,
|
|
68
|
+
"session_name": session,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if include_readiness && !session.is_empty() && !session_exists_on_endpoint(socket, session) {
|
|
73
|
+
issues.push(json!({
|
|
74
|
+
"id": TEAM_SESSION_MISSING_ON_CANONICAL_SOCKET,
|
|
75
|
+
"tmux_endpoint": socket,
|
|
76
|
+
"session_name": session,
|
|
77
|
+
}));
|
|
78
|
+
issues.push(json!({
|
|
79
|
+
"id": RECENT_COORDINATOR_SESSION_MISSING,
|
|
80
|
+
"tmux_endpoint": socket,
|
|
81
|
+
"session_name": session,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn append_worker_pane_binding_issues(
|
|
87
|
+
state: &Value,
|
|
88
|
+
backend: &dyn Transport,
|
|
89
|
+
issues: &mut Vec<Value>,
|
|
90
|
+
) {
|
|
91
|
+
let session = non_empty_str(state, "session_name").unwrap_or_default();
|
|
92
|
+
let Some(agents) = state.get("agents").and_then(Value::as_object) else {
|
|
93
|
+
return;
|
|
94
|
+
};
|
|
95
|
+
let Ok(live_targets) = backend.list_targets() else {
|
|
96
|
+
return;
|
|
97
|
+
};
|
|
98
|
+
for (agent_id, agent) in agents {
|
|
99
|
+
let Some(cached_pane_id) = non_empty_str(agent, "pane_id") else {
|
|
100
|
+
continue;
|
|
101
|
+
};
|
|
102
|
+
let window = non_empty_str(agent, "window").unwrap_or(agent_id.as_str());
|
|
103
|
+
let Some(observed) = live_targets
|
|
104
|
+
.iter()
|
|
105
|
+
.find(|pane| pane.pane_id.as_str() == cached_pane_id)
|
|
106
|
+
else {
|
|
107
|
+
continue;
|
|
108
|
+
};
|
|
109
|
+
let observed_window = observed
|
|
110
|
+
.window_name
|
|
111
|
+
.as_ref()
|
|
112
|
+
.map(|window| window.as_str())
|
|
113
|
+
.unwrap_or_default();
|
|
114
|
+
if observed.session.as_str() == session && observed_window == window {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
issues.push(json!({
|
|
118
|
+
"id": WORKER_PANE_BINDING_STALE,
|
|
119
|
+
"agent_id": agent_id,
|
|
120
|
+
"cached_pane_id": cached_pane_id,
|
|
121
|
+
"expected_session": session,
|
|
122
|
+
"expected_window": window,
|
|
123
|
+
"observed_session": observed.session.as_str(),
|
|
124
|
+
"observed_window": observed_window,
|
|
125
|
+
"observed_pane_pid": observed.pane_pid,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn session_exists_on_endpoint(endpoint: &str, session: &str) -> bool {
|
|
131
|
+
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
|
|
132
|
+
.has_session(&SessionName::new(session.to_string()))
|
|
133
|
+
.unwrap_or(false)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn same_endpoint(left: &str, right: &str) -> bool {
|
|
137
|
+
normalize_endpoint(left) == normalize_endpoint(right)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn normalize_endpoint(value: &str) -> String {
|
|
141
|
+
if std::path::Path::new(value).is_absolute() {
|
|
142
|
+
value.to_string()
|
|
143
|
+
} else if let Some(path) = crate::tmux_backend::socket_path_for_name(value) {
|
|
144
|
+
path.to_string_lossy().into_owned()
|
|
145
|
+
} else {
|
|
146
|
+
value.to_string()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn non_empty_str<'a>(value: &'a Value, key: &str) -> Option<&'a str> {
|
|
151
|
+
value.get(key).and_then(Value::as_str).filter(|s| !s.is_empty())
|
|
152
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.11",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.11",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.11"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|