@team-agent/installer 0.5.12 → 0.5.13
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
CHANGED
package/Cargo.toml
CHANGED
|
@@ -3321,8 +3321,9 @@ pub mod lifecycle_port {
|
|
|
3321
3321
|
.map(|id| json!({"id": id}))
|
|
3322
3322
|
.collect::<Vec<_>>(),
|
|
3323
3323
|
"next_actions": [
|
|
3324
|
-
"
|
|
3325
|
-
"
|
|
3324
|
+
"team-agent diagnose --json",
|
|
3325
|
+
"team-agent claim-leader --confirm --json",
|
|
3326
|
+
"team-agent takeover --confirm --json"
|
|
3326
3327
|
],
|
|
3327
3328
|
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
3328
3329
|
}),
|
|
@@ -160,7 +160,9 @@ pub fn assert_topology_invariants(state: &JsonValue, spec: &YamlValue) -> Vec<To
|
|
|
160
160
|
kind: TopologyViolationKind::LeaderPaneIdCollidesWithAgent,
|
|
161
161
|
detail: format!(
|
|
162
162
|
"leader_receiver.pane_id `{leader_pane}` is also bound to \
|
|
163
|
-
agent `{agent_id}` —
|
|
163
|
+
agent `{agent_id}` — legacy bare-pane advisory only; validate \
|
|
164
|
+
endpoint/session/window/pane_pid tuple before using this as an \
|
|
165
|
+
identity blocker"
|
|
164
166
|
),
|
|
165
167
|
});
|
|
166
168
|
}
|
|
@@ -447,10 +447,11 @@ pub fn claim_leader(
|
|
|
447
447
|
.list_targets()
|
|
448
448
|
.unwrap_or_default(),
|
|
449
449
|
);
|
|
450
|
-
let
|
|
450
|
+
let caller_pane_info = targets
|
|
451
451
|
.iter()
|
|
452
|
-
.find(|target| target.pane_id.as_str() == caller)
|
|
453
|
-
|
|
452
|
+
.find(|target| target.pane_id.as_str() == caller);
|
|
453
|
+
let caller_target =
|
|
454
|
+
caller_pane_info.and_then(|target| claim_target_from_pane_info(workspace, target));
|
|
454
455
|
let env_team = std::env::var("TEAM_AGENT_TEAM_ID")
|
|
455
456
|
.ok()
|
|
456
457
|
.filter(|team| !team.is_empty());
|
|
@@ -496,6 +497,7 @@ pub fn claim_leader(
|
|
|
496
497
|
&event_log,
|
|
497
498
|
&liveness,
|
|
498
499
|
caller_target.as_ref(),
|
|
500
|
+
caller_pane_info,
|
|
499
501
|
scoped_team.map(|_| team_id.as_str()),
|
|
500
502
|
)?;
|
|
501
503
|
if result.ok {
|
|
@@ -556,6 +558,7 @@ pub fn claim_lease_no_incident(
|
|
|
556
558
|
event_log,
|
|
557
559
|
liveness,
|
|
558
560
|
None,
|
|
561
|
+
None,
|
|
559
562
|
scoped_team,
|
|
560
563
|
)
|
|
561
564
|
}
|
|
@@ -586,6 +589,7 @@ fn claim_lease_no_incident_with_target(
|
|
|
586
589
|
event_log: &crate::event_log::EventLog,
|
|
587
590
|
liveness: &dyn crate::state::owner_gate::PaneLivenessProbe,
|
|
588
591
|
caller_target: Option<&LeaderClaimTarget>,
|
|
592
|
+
caller_pane_info: Option<&PaneInfo>,
|
|
589
593
|
scoped_team: Option<&str>,
|
|
590
594
|
) -> Result<LeaseResult, LeaderError> {
|
|
591
595
|
let _ = team;
|
|
@@ -705,25 +709,53 @@ fn claim_lease_no_incident_with_target(
|
|
|
705
709
|
// worker's tmux pane overwrites leader_receiver.pane_id with the worker's
|
|
706
710
|
// pane → delivery routes worker messages to itself (loop) and the leader
|
|
707
711
|
// loses its handle (the macmini "hand-handle mapping 灾难" truth source).
|
|
708
|
-
if let Some(
|
|
709
|
-
|
|
710
|
-
event_log,
|
|
711
|
-
LeaseReason::CallerNotLeaderShaped,
|
|
712
|
+
if let Some(caller_pane_info) = caller_pane_info {
|
|
713
|
+
match crate::topology::classify_registered_worker_for_observed_pane(
|
|
712
714
|
state,
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
715
|
+
caller_pane_info,
|
|
716
|
+
) {
|
|
717
|
+
crate::topology::WorkerPaneBindingMatch::LiveSameWorker { agent_id } => {
|
|
718
|
+
emit_lease_refusal(
|
|
719
|
+
event_log,
|
|
720
|
+
LeaseReason::CallerNotLeaderShaped,
|
|
721
|
+
state,
|
|
722
|
+
bound_pane_id.as_deref(),
|
|
723
|
+
Some(caller_pane.as_str()),
|
|
724
|
+
team_id,
|
|
725
|
+
)?;
|
|
726
|
+
return Ok(refused(
|
|
727
|
+
LeaseReason::CallerNotLeaderShaped,
|
|
728
|
+
&format!(
|
|
729
|
+
"pane {} is registered as worker {agent_id}; \
|
|
730
|
+
run claim-leader from the leader's own pane, not a worker pane",
|
|
731
|
+
caller_pane.as_str()
|
|
732
|
+
),
|
|
733
|
+
None,
|
|
734
|
+
bound_pane_id.clone().map(PaneId::new),
|
|
735
|
+
));
|
|
736
|
+
}
|
|
737
|
+
crate::topology::WorkerPaneBindingMatch::Stale { agent_id, reason } => {
|
|
738
|
+
event_log.write(
|
|
739
|
+
"leader_receiver.worker_pane_binding_ignored",
|
|
740
|
+
json!({
|
|
741
|
+
"agent_id": agent_id,
|
|
742
|
+
"pane_id": caller_pane.as_str(),
|
|
743
|
+
"reason": reason,
|
|
744
|
+
}),
|
|
745
|
+
)?;
|
|
746
|
+
}
|
|
747
|
+
crate::topology::WorkerPaneBindingMatch::IncompleteLegacy { agent_id } => {
|
|
748
|
+
event_log.write(
|
|
749
|
+
"leader_receiver.worker_pane_binding_ignored",
|
|
750
|
+
json!({
|
|
751
|
+
"agent_id": agent_id,
|
|
752
|
+
"pane_id": caller_pane.as_str(),
|
|
753
|
+
"reason": "incomplete_legacy_tuple",
|
|
754
|
+
}),
|
|
755
|
+
)?;
|
|
756
|
+
}
|
|
757
|
+
crate::topology::WorkerPaneBindingMatch::NoMatch => {}
|
|
758
|
+
}
|
|
727
759
|
}
|
|
728
760
|
let reason = if bound_pane_id.is_some() {
|
|
729
761
|
LeaseReason::PreviousOwnerPaneDead
|
|
@@ -1135,38 +1167,6 @@ fn make_owner(
|
|
|
1135
1167
|
}
|
|
1136
1168
|
}
|
|
1137
1169
|
|
|
1138
|
-
/// E51 (0.3.26 P0, lease guard): scan state.agents + teams[*].agents for a worker
|
|
1139
|
-
/// whose pane_id matches the caller. Returns the first matching agent_id, or None
|
|
1140
|
-
/// if no registered worker owns this pane. The check is O(N agents) — negligible
|
|
1141
|
-
/// on a team-agent team which rarely exceeds ~20 workers.
|
|
1142
|
-
fn registered_worker_for_pane(state: &Value, caller_pane: &PaneId) -> Option<String> {
|
|
1143
|
-
if let Some(agent_id) = scan_agents_for_pane(state.get("agents"), caller_pane) {
|
|
1144
|
-
return Some(agent_id);
|
|
1145
|
-
}
|
|
1146
|
-
if let Some(teams) = state.get("teams").and_then(Value::as_object) {
|
|
1147
|
-
for (_, team_state) in teams {
|
|
1148
|
-
if let Some(agent_id) = scan_agents_for_pane(team_state.get("agents"), caller_pane) {
|
|
1149
|
-
return Some(agent_id);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
None
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
fn scan_agents_for_pane(agents: Option<&Value>, caller_pane: &PaneId) -> Option<String> {
|
|
1157
|
-
let map = agents?.as_object()?;
|
|
1158
|
-
for (agent_id, agent) in map {
|
|
1159
|
-
if agent
|
|
1160
|
-
.get("pane_id")
|
|
1161
|
-
.and_then(Value::as_str)
|
|
1162
|
-
.is_some_and(|pane| pane == caller_pane.as_str())
|
|
1163
|
-
{
|
|
1164
|
-
return Some(agent_id.clone());
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
None
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
1170
|
/// Stage 3a (identity-boundary unified plan, architect direction 2026-06-23)
|
|
1171
1171
|
/// + Stage 3 owner persist fix (architect direction 2026-06-24): route the
|
|
1172
1172
|
/// in-memory binding write through the `state::ownership` API. The
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use serde_json::{json, Value};
|
|
2
2
|
|
|
3
|
-
use crate::transport::{SessionName, Transport};
|
|
3
|
+
use crate::transport::{PaneInfo, SessionName, Transport};
|
|
4
4
|
|
|
5
5
|
pub const WORKER_PANE_BINDING_STALE: &str = "worker_pane_binding_stale";
|
|
6
6
|
pub const TMUX_ENDPOINT_SOCKET_CONFLICT: &str = "tmux_endpoint_socket_conflict";
|
|
@@ -30,6 +30,103 @@ pub fn issue_id(issue: &Value) -> Option<&str> {
|
|
|
30
30
|
issue.get("id").and_then(Value::as_str)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
34
|
+
pub enum WorkerPaneBindingMatch {
|
|
35
|
+
LiveSameWorker { agent_id: String },
|
|
36
|
+
Stale { agent_id: String, reason: &'static str },
|
|
37
|
+
IncompleteLegacy { agent_id: String },
|
|
38
|
+
NoMatch,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub fn classify_worker_pane_binding(
|
|
42
|
+
agent_id: &str,
|
|
43
|
+
agent: &Value,
|
|
44
|
+
expected_session: &str,
|
|
45
|
+
observed: &PaneInfo,
|
|
46
|
+
) -> WorkerPaneBindingMatch {
|
|
47
|
+
let Some(cached_pane_id) = non_empty_str(agent, "pane_id") else {
|
|
48
|
+
return WorkerPaneBindingMatch::NoMatch;
|
|
49
|
+
};
|
|
50
|
+
if cached_pane_id != observed.pane_id.as_str() {
|
|
51
|
+
return WorkerPaneBindingMatch::NoMatch;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let Some(observed_window) = observed.window_name.as_ref().map(|window| window.as_str()) else {
|
|
55
|
+
return WorkerPaneBindingMatch::IncompleteLegacy {
|
|
56
|
+
agent_id: agent_id.to_string(),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
let expected_window = non_empty_str(agent, "window").unwrap_or(agent_id);
|
|
60
|
+
if expected_session.is_empty() || expected_window.is_empty() {
|
|
61
|
+
return WorkerPaneBindingMatch::IncompleteLegacy {
|
|
62
|
+
agent_id: agent_id.to_string(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if let (Some(expected_pid), Some(observed_pid)) = (agent_pane_pid(agent), observed.pane_pid) {
|
|
67
|
+
if expected_pid != observed_pid {
|
|
68
|
+
return WorkerPaneBindingMatch::Stale {
|
|
69
|
+
agent_id: agent_id.to_string(),
|
|
70
|
+
reason: "pane_pid_mismatch",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if observed.session.as_str() == expected_session && observed_window == expected_window {
|
|
76
|
+
WorkerPaneBindingMatch::LiveSameWorker {
|
|
77
|
+
agent_id: agent_id.to_string(),
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
WorkerPaneBindingMatch::Stale {
|
|
81
|
+
agent_id: agent_id.to_string(),
|
|
82
|
+
reason: "session_window_mismatch",
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn classify_registered_worker_for_observed_pane(
|
|
88
|
+
state: &Value,
|
|
89
|
+
observed: &PaneInfo,
|
|
90
|
+
) -> WorkerPaneBindingMatch {
|
|
91
|
+
let mut fallback = WorkerPaneBindingMatch::NoMatch;
|
|
92
|
+
classify_agents_for_observed_pane(state, observed, &mut fallback);
|
|
93
|
+
if matches!(fallback, WorkerPaneBindingMatch::LiveSameWorker { .. }) {
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
if let Some(teams) = state.get("teams").and_then(Value::as_object) {
|
|
97
|
+
for team_state in teams.values() {
|
|
98
|
+
classify_agents_for_observed_pane(team_state, observed, &mut fallback);
|
|
99
|
+
if matches!(fallback, WorkerPaneBindingMatch::LiveSameWorker { .. }) {
|
|
100
|
+
return fallback;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
fallback
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn classify_agents_for_observed_pane(
|
|
108
|
+
state: &Value,
|
|
109
|
+
observed: &PaneInfo,
|
|
110
|
+
fallback: &mut WorkerPaneBindingMatch,
|
|
111
|
+
) {
|
|
112
|
+
let expected_session = non_empty_str(state, "session_name").unwrap_or_default();
|
|
113
|
+
let Some(agents) = state.get("agents").and_then(Value::as_object) else {
|
|
114
|
+
return;
|
|
115
|
+
};
|
|
116
|
+
for (agent_id, agent) in agents {
|
|
117
|
+
let candidate = classify_worker_pane_binding(agent_id, agent, expected_session, observed);
|
|
118
|
+
if matches!(candidate, WorkerPaneBindingMatch::LiveSameWorker { .. }) {
|
|
119
|
+
*fallback = candidate;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if matches!(fallback, WorkerPaneBindingMatch::NoMatch)
|
|
123
|
+
&& !matches!(candidate, WorkerPaneBindingMatch::NoMatch)
|
|
124
|
+
{
|
|
125
|
+
*fallback = candidate;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
33
130
|
fn append_socket_split_issues(state: &Value, issues: &mut Vec<Value>, include_readiness: bool) {
|
|
34
131
|
let endpoint = non_empty_str(state, "tmux_endpoint");
|
|
35
132
|
let socket = non_empty_str(state, "tmux_socket");
|
|
@@ -111,9 +208,20 @@ fn append_worker_pane_binding_issues(
|
|
|
111
208
|
.as_ref()
|
|
112
209
|
.map(|window| window.as_str())
|
|
113
210
|
.unwrap_or_default();
|
|
114
|
-
|
|
211
|
+
let classification = classify_worker_pane_binding(agent_id, agent, &session, observed);
|
|
212
|
+
if matches!(
|
|
213
|
+
classification,
|
|
214
|
+
WorkerPaneBindingMatch::LiveSameWorker { .. } | WorkerPaneBindingMatch::NoMatch
|
|
215
|
+
) {
|
|
115
216
|
continue;
|
|
116
217
|
}
|
|
218
|
+
let reason = match classification {
|
|
219
|
+
WorkerPaneBindingMatch::Stale { reason, .. } => reason,
|
|
220
|
+
WorkerPaneBindingMatch::IncompleteLegacy { .. } => "incomplete_legacy_tuple",
|
|
221
|
+
WorkerPaneBindingMatch::LiveSameWorker { .. } | WorkerPaneBindingMatch::NoMatch => {
|
|
222
|
+
"unknown"
|
|
223
|
+
}
|
|
224
|
+
};
|
|
117
225
|
issues.push(json!({
|
|
118
226
|
"id": WORKER_PANE_BINDING_STALE,
|
|
119
227
|
"agent_id": agent_id,
|
|
@@ -123,10 +231,18 @@ fn append_worker_pane_binding_issues(
|
|
|
123
231
|
"observed_session": observed.session.as_str(),
|
|
124
232
|
"observed_window": observed_window,
|
|
125
233
|
"observed_pane_pid": observed.pane_pid,
|
|
234
|
+
"reason": reason,
|
|
126
235
|
}));
|
|
127
236
|
}
|
|
128
237
|
}
|
|
129
238
|
|
|
239
|
+
fn agent_pane_pid(agent: &Value) -> Option<u32> {
|
|
240
|
+
agent
|
|
241
|
+
.get("pane_pid")
|
|
242
|
+
.and_then(Value::as_u64)
|
|
243
|
+
.and_then(|value| u32::try_from(value).ok())
|
|
244
|
+
}
|
|
245
|
+
|
|
130
246
|
fn session_exists_on_endpoint(endpoint: &str, session: &str) -> bool {
|
|
131
247
|
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
|
|
132
248
|
.has_session(&SessionName::new(session.to_string()))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
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.13",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.13",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.13"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|