@team-agent/installer 0.5.42 → 0.5.43
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 +129 -54
- package/crates/team-agent/src/cli/diagnose.rs +1 -2
- package/crates/team-agent/src/cli/emit.rs +1 -7
- package/crates/team-agent/src/cli/helpers.rs +3 -1
- package/crates/team-agent/src/cli/leader.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +27 -7
- package/crates/team-agent/src/cli/named_address.rs +14 -5
- package/crates/team-agent/src/cli/profile.rs +19 -7
- package/crates/team-agent/src/cli/send.rs +9 -3
- package/crates/team-agent/src/cli/status.rs +8 -30
- package/crates/team-agent/src/cli/status_port.rs +1341 -1272
- package/crates/team-agent/src/cli/tests/base.rs +738 -660
- package/crates/team-agent/src/cli/tests/compile.rs +45 -18
- package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
- package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
- package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
- package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
- package/crates/team-agent/src/cli/tests/mod.rs +17 -7
- package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
- package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
- package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
- package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
- package/crates/team-agent/src/codex_app_server.rs +2 -5
- package/crates/team-agent/src/compiler/tests.rs +139 -33
- package/crates/team-agent/src/compiler.rs +55 -22
- package/crates/team-agent/src/conpty/backend.rs +23 -33
- package/crates/team-agent/src/coordinator/backoff.rs +2 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
- package/crates/team-agent/src/coordinator/health.rs +46 -31
- package/crates/team-agent/src/coordinator/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/orphan.rs +22 -10
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +47 -52
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
- package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
- package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
- package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
- package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
- package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
- package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
- package/crates/team-agent/src/db/message_store.rs +138 -30
- package/crates/team-agent/src/db/migration.rs +249 -61
- package/crates/team-agent/src/db/schema.rs +303 -82
- package/crates/team-agent/src/diagnose/comms.rs +9 -2
- package/crates/team-agent/src/diagnose/mod.rs +1 -3
- package/crates/team-agent/src/diagnose/orphans.rs +79 -61
- package/crates/team-agent/src/event_log.rs +70 -16
- package/crates/team-agent/src/layout/manager.rs +15 -4
- package/crates/team-agent/src/layout/mod.rs +4 -4
- package/crates/team-agent/src/layout/overlay.rs +10 -3
- package/crates/team-agent/src/layout/placement.rs +5 -1
- package/crates/team-agent/src/layout/recovery.rs +4 -2
- package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
- package/crates/team-agent/src/layout/sessions.rs +17 -9
- package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +52 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +166 -80
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
- package/crates/team-agent/src/leader/rediscover.rs +74 -25
- package/crates/team-agent/src/leader/start.rs +75 -54
- package/crates/team-agent/src/leader/takeover.rs +46 -11
- package/crates/team-agent/src/leader/tests/basics.rs +320 -167
- package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
- package/crates/team-agent/src/leader/tests/identity.rs +428 -356
- package/crates/team-agent/src/leader/tests/idle.rs +285 -254
- package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
- package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
- package/crates/team-agent/src/leader/tests/mod.rs +115 -99
- package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
- package/crates/team-agent/src/lib.rs +4 -4
- package/crates/team-agent/src/lifecycle/display.rs +7 -3
- package/crates/team-agent/src/lifecycle/launch.rs +6 -1
- package/crates/team-agent/src/lifecycle/mod.rs +9 -1
- package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
- package/crates/team-agent/src/lifecycle/tests.rs +2 -2
- package/crates/team-agent/src/main.rs +4 -4
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
- package/crates/team-agent/src/mcp_server/mod.rs +11 -2
- package/crates/team-agent/src/mcp_server/types.rs +14 -3
- package/crates/team-agent/src/mcp_server/wire.rs +230 -68
- package/crates/team-agent/src/messaging/delivery.rs +20 -18
- package/crates/team-agent/src/messaging/helpers.rs +25 -4
- package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
- package/crates/team-agent/src/messaging/mod.rs +2 -3
- package/crates/team-agent/src/messaging/selftest.rs +46 -26
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
- package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
- package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
- package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
- package/crates/team-agent/src/messaging/trust.rs +25 -2
- package/crates/team-agent/src/messaging/watchers.rs +37 -9
- package/crates/team-agent/src/model/enums.rs +65 -16
- package/crates/team-agent/src/model/ids.rs +12 -3
- package/crates/team-agent/src/model/paths.rs +28 -7
- package/crates/team-agent/src/model/permissions.rs +176 -33
- package/crates/team-agent/src/model/routing.rs +66 -20
- package/crates/team-agent/src/model/spec.rs +365 -69
- package/crates/team-agent/src/model/task_graph.rs +36 -9
- package/crates/team-agent/src/model/yaml/tests.rs +24 -6
- package/crates/team-agent/src/model/yaml.rs +7 -6
- package/crates/team-agent/src/packaging/install.rs +23 -9
- package/crates/team-agent/src/packaging/migrate.rs +5 -7
- package/crates/team-agent/src/packaging/mod.rs +9 -1
- package/crates/team-agent/src/packaging/repair.rs +13 -6
- package/crates/team-agent/src/packaging/tests.rs +63 -16
- package/crates/team-agent/src/packaging/types.rs +22 -7
- package/crates/team-agent/src/platform/argv.rs +4 -1
- package/crates/team-agent/src/platform/file_lock.rs +22 -8
- package/crates/team-agent/src/platform/process.rs +21 -22
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
- package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
- package/crates/team-agent/src/provider/classify.rs +127 -42
- package/crates/team-agent/src/provider/faults.rs +17 -5
- package/crates/team-agent/src/provider/helpers.rs +6 -5
- package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
- package/crates/team-agent/src/state/repository.rs +1 -3
- package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
- package/crates/team-agent/src/tmux_backend.rs +80 -44
- package/crates/team-agent/src/topology.rs +27 -20
- package/crates/team-agent/src/transport/test_support.rs +20 -23
- package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
- package/crates/team-agent/src/transport/tests/mod.rs +178 -187
- package/crates/team-agent/src/transport/tests/wire.rs +561 -525
- package/crates/team-agent/src/transport.rs +12 -17
- package/crates/team-agent/src/transport_factory.rs +29 -14
- package/package.json +4 -4
|
@@ -114,7 +114,9 @@ fn is_stripped(key: &str) -> bool {
|
|
|
114
114
|
|
|
115
115
|
fn is_posix_shell_identifier(s: &str) -> bool {
|
|
116
116
|
let mut bytes = s.bytes();
|
|
117
|
-
let Some(first) = bytes.next() else {
|
|
117
|
+
let Some(first) = bytes.next() else {
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
118
120
|
if !(first.is_ascii_alphabetic() || first == b'_') {
|
|
119
121
|
return false;
|
|
120
122
|
}
|
|
@@ -147,12 +149,24 @@ mod tests {
|
|
|
147
149
|
("OPENAI_API_KEY".to_string(), "sk-test".to_string()),
|
|
148
150
|
("COPILOT_TOKEN".to_string(), "tok".to_string()),
|
|
149
151
|
// Leader identity — must be stripped.
|
|
150
|
-
(
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
(
|
|
153
|
+
"TEAM_AGENT_LEADER_PROVIDER".to_string(),
|
|
154
|
+
"claude".to_string(),
|
|
155
|
+
),
|
|
156
|
+
(
|
|
157
|
+
"TEAM_AGENT_LEADER_SESSION_UUID".to_string(),
|
|
158
|
+
"leader-uuid".to_string(),
|
|
159
|
+
),
|
|
160
|
+
(
|
|
161
|
+
"TEAM_AGENT_MACHINE_FINGERPRINT".to_string(),
|
|
162
|
+
"fp".to_string(),
|
|
163
|
+
),
|
|
153
164
|
("TEAM_AGENT_TEAM_ID".to_string(), "alpha".to_string()),
|
|
154
165
|
("TEAM_AGENT_LEADER_BYPASS".to_string(), "1".to_string()),
|
|
155
|
-
(
|
|
166
|
+
(
|
|
167
|
+
"COPILOT_DISABLE_TERMINAL_TITLE".to_string(),
|
|
168
|
+
"1".to_string(),
|
|
169
|
+
),
|
|
156
170
|
// Random noise — non-whitelisted prefix.
|
|
157
171
|
("RANDOM_NOISE".to_string(), "x".to_string()),
|
|
158
172
|
("TMUX".to_string(), "leader-pane".to_string()),
|
|
@@ -197,20 +211,27 @@ mod tests {
|
|
|
197
211
|
"developer",
|
|
198
212
|
Some("alpha"),
|
|
199
213
|
);
|
|
200
|
-
assert_eq!(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
214
|
+
assert_eq!(
|
|
215
|
+
env.get("TEAM_AGENT_ID").map(String::as_str),
|
|
216
|
+
Some("developer")
|
|
217
|
+
);
|
|
218
|
+
assert_eq!(
|
|
219
|
+
env.get("TEAM_AGENT_AGENT_ID").map(String::as_str),
|
|
220
|
+
Some("developer")
|
|
221
|
+
);
|
|
222
|
+
assert_eq!(
|
|
223
|
+
env.get("TEAM_AGENT_WORKSPACE").map(String::as_str),
|
|
224
|
+
Some("/ws")
|
|
225
|
+
);
|
|
226
|
+
assert_eq!(
|
|
227
|
+
env.get("TEAM_AGENT_OWNER_TEAM_ID").map(String::as_str),
|
|
228
|
+
Some("alpha")
|
|
229
|
+
);
|
|
204
230
|
}
|
|
205
231
|
|
|
206
232
|
#[test]
|
|
207
233
|
fn worker_spawn_env_inherits_parent_env_minus_strip_list() {
|
|
208
|
-
let env = worker_spawn_env(
|
|
209
|
-
make_parent_env(&[]),
|
|
210
|
-
Path::new("/ws"),
|
|
211
|
-
"developer",
|
|
212
|
-
None,
|
|
213
|
-
);
|
|
234
|
+
let env = worker_spawn_env(make_parent_env(&[]), Path::new("/ws"), "developer", None);
|
|
214
235
|
// Inherit-then-strip: every parent key passes through EXCEPT the
|
|
215
236
|
// strip list. PATH-like + provider creds + random user vars all
|
|
216
237
|
// present.
|
|
@@ -240,15 +261,24 @@ mod tests {
|
|
|
240
261
|
#[test]
|
|
241
262
|
fn worker_spawn_cwd_returns_workspace_when_no_spec_override() {
|
|
242
263
|
// YAML agent with no spawn_cwd field.
|
|
243
|
-
let agent = YamlValue::Map(vec![(
|
|
244
|
-
|
|
264
|
+
let agent = YamlValue::Map(vec![(
|
|
265
|
+
"id".to_string(),
|
|
266
|
+
YamlValue::Str("developer".to_string()),
|
|
267
|
+
)]);
|
|
268
|
+
assert_eq!(
|
|
269
|
+
worker_spawn_cwd(&agent, Path::new("/ws")),
|
|
270
|
+
PathBuf::from("/ws")
|
|
271
|
+
);
|
|
245
272
|
}
|
|
246
273
|
|
|
247
274
|
#[test]
|
|
248
275
|
fn worker_spawn_cwd_returns_yaml_override_when_present() {
|
|
249
276
|
let agent = YamlValue::Map(vec![
|
|
250
277
|
("id".to_string(), YamlValue::Str("developer".to_string())),
|
|
251
|
-
(
|
|
278
|
+
(
|
|
279
|
+
"spawn_cwd".to_string(),
|
|
280
|
+
YamlValue::Str("/explicit/cwd".to_string()),
|
|
281
|
+
),
|
|
252
282
|
]);
|
|
253
283
|
assert_eq!(
|
|
254
284
|
worker_spawn_cwd(&agent, Path::new("/ws")),
|
|
@@ -262,6 +292,9 @@ mod tests {
|
|
|
262
292
|
("id".to_string(), YamlValue::Str("developer".to_string())),
|
|
263
293
|
("spawn_cwd".to_string(), YamlValue::Str("".to_string())),
|
|
264
294
|
]);
|
|
265
|
-
assert_eq!(
|
|
295
|
+
assert_eq!(
|
|
296
|
+
worker_spawn_cwd(&agent, Path::new("/ws")),
|
|
297
|
+
PathBuf::from("/ws")
|
|
298
|
+
);
|
|
266
299
|
}
|
|
267
300
|
}
|
|
@@ -63,7 +63,13 @@ pub(crate) fn resolve_workspace_for_hash(workspace: &Path) -> PathBuf {
|
|
|
63
63
|
pub(crate) fn sanitize_session_folder(raw: &str) -> String {
|
|
64
64
|
let replaced: String = raw
|
|
65
65
|
.chars()
|
|
66
|
-
.map(|c|
|
|
66
|
+
.map(|c| {
|
|
67
|
+
if c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.') {
|
|
68
|
+
c
|
|
69
|
+
} else {
|
|
70
|
+
'_'
|
|
71
|
+
}
|
|
72
|
+
})
|
|
67
73
|
.take(48)
|
|
68
74
|
.collect();
|
|
69
75
|
let trimmed = replaced.trim_matches(|c| matches!(c, '_' | '-' | '.'));
|
|
@@ -40,20 +40,38 @@ pub fn attach_leader(
|
|
|
40
40
|
let targets = attach_leader_targets(workspace, &state);
|
|
41
41
|
let pane_id = pane
|
|
42
42
|
.cloned()
|
|
43
|
-
.or_else(||
|
|
43
|
+
.or_else(|| {
|
|
44
|
+
std::env::var("TMUX_PANE")
|
|
45
|
+
.ok()
|
|
46
|
+
.filter(|p| !p.is_empty())
|
|
47
|
+
.map(PaneId::new)
|
|
48
|
+
})
|
|
44
49
|
.ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
|
|
45
50
|
let non_empty_pane_id = NonEmptyPaneId::try_from_pane(&pane_id)?;
|
|
46
51
|
let Some(target) = targets.iter().find(|target| target.info.pane_id == pane_id) else {
|
|
47
|
-
return Err(LeaderError::Validation(format!(
|
|
52
|
+
return Err(LeaderError::Validation(format!(
|
|
53
|
+
"tmux pane not found: {pane_id}"
|
|
54
|
+
)));
|
|
48
55
|
};
|
|
49
|
-
let mut receiver = receiver_for_attach_target(
|
|
56
|
+
let mut receiver = receiver_for_attach_target(
|
|
57
|
+
workspace,
|
|
58
|
+
&state,
|
|
59
|
+
&target.info,
|
|
60
|
+
provider,
|
|
61
|
+
Discovery::ExplicitPane,
|
|
62
|
+
)?;
|
|
50
63
|
if let Some(endpoint) = target.endpoint.as_ref() {
|
|
51
64
|
receiver.tmux_socket = Some(endpoint.clone());
|
|
52
65
|
}
|
|
53
66
|
let validation = validate_attach_target(workspace, &state, &target.info, provider);
|
|
54
67
|
if validation.is_err() {
|
|
55
68
|
let pane_info = pane_info_value(&target.info);
|
|
56
|
-
let targets_value = Value::Array(
|
|
69
|
+
let targets_value = Value::Array(
|
|
70
|
+
targets
|
|
71
|
+
.iter()
|
|
72
|
+
.map(|target| pane_info_value(&target.info))
|
|
73
|
+
.collect(),
|
|
74
|
+
);
|
|
57
75
|
let owner_record = state_owner(&state);
|
|
58
76
|
if let Some((readopted, validation)) = crate::leader::try_readopt_leader_pane(
|
|
59
77
|
workspace,
|
|
@@ -66,8 +84,11 @@ pub fn attach_leader(
|
|
|
66
84
|
LeaseSource::Manual,
|
|
67
85
|
&event_log,
|
|
68
86
|
)? {
|
|
69
|
-
if let Some(endpoint) = target.endpoint.as_deref() {
|
|
70
|
-
|
|
87
|
+
if let Some(endpoint) = target.endpoint.as_deref() {
|
|
88
|
+
quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
|
|
89
|
+
}
|
|
90
|
+
let _ =
|
|
91
|
+
requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
|
|
71
92
|
return Ok(LeaseResult {
|
|
72
93
|
ok: true,
|
|
73
94
|
status: LeaseStatus::Claimed,
|
|
@@ -90,7 +111,9 @@ pub fn attach_leader(
|
|
|
90
111
|
"reason": validation.err().unwrap_or("leader_pane_validation_failed"),
|
|
91
112
|
}),
|
|
92
113
|
)?;
|
|
93
|
-
return Err(LeaderError::Validation(format!(
|
|
114
|
+
return Err(LeaderError::Validation(format!(
|
|
115
|
+
"leader pane validation failed: {pane_id}"
|
|
116
|
+
)));
|
|
94
117
|
}
|
|
95
118
|
let epoch = current_owner_epoch(&state);
|
|
96
119
|
if state.get("team_owner").is_some() {
|
|
@@ -100,7 +123,9 @@ pub fn attach_leader(
|
|
|
100
123
|
super::LeaderEvent::ReceiverAttached.name(),
|
|
101
124
|
json!({"pane_id": pane_id.as_str(), "owner_epoch": epoch.0}),
|
|
102
125
|
)?;
|
|
103
|
-
if let Some(endpoint) = target.endpoint.as_deref() {
|
|
126
|
+
if let Some(endpoint) = target.endpoint.as_deref() {
|
|
127
|
+
quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
|
|
128
|
+
}
|
|
104
129
|
let _ = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
|
|
105
130
|
return Ok(LeaseResult {
|
|
106
131
|
ok: true,
|
|
@@ -125,7 +150,9 @@ pub fn attach_leader(
|
|
|
125
150
|
super::LeaderEvent::ReceiverAttached.name(),
|
|
126
151
|
json!({"pane_id": pane_id.as_str(), "owner_epoch": next_epoch.0}),
|
|
127
152
|
)?;
|
|
128
|
-
if let Some(endpoint) = target.endpoint.as_deref() {
|
|
153
|
+
if let Some(endpoint) = target.endpoint.as_deref() {
|
|
154
|
+
quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
|
|
155
|
+
}
|
|
129
156
|
let _ = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
|
|
130
157
|
Ok(LeaseResult {
|
|
131
158
|
ok: true,
|
|
@@ -279,7 +306,10 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
|
|
|
279
306
|
.list_targets()
|
|
280
307
|
.unwrap_or_default()
|
|
281
308
|
.into_iter()
|
|
282
|
-
.map(|info| AttachLeaderTarget {
|
|
309
|
+
.map(|info| AttachLeaderTarget {
|
|
310
|
+
info,
|
|
311
|
+
endpoint: None,
|
|
312
|
+
}),
|
|
283
313
|
);
|
|
284
314
|
targets
|
|
285
315
|
}
|
|
@@ -336,11 +366,7 @@ fn requeue_exhausted_watchers_after_attach(
|
|
|
336
366
|
let store = MessageStore::open(workspace)?;
|
|
337
367
|
let team_id = TeamKey::new(crate::state::projection::team_state_key(state));
|
|
338
368
|
let notices = crate::messaging::requeue_delivery_exhausted_watchers(
|
|
339
|
-
workspace,
|
|
340
|
-
&store,
|
|
341
|
-
event_log,
|
|
342
|
-
&team_id,
|
|
343
|
-
pane_id,
|
|
369
|
+
workspace, &store, event_log, &team_id, pane_id,
|
|
344
370
|
)?;
|
|
345
371
|
event_log.write(
|
|
346
372
|
super::LeaderEvent::ReceiverRequeuedExhaustedWatchers.name(),
|
|
@@ -359,7 +385,10 @@ pub(crate) fn requeued_exhausted_watchers_event_payload(
|
|
|
359
385
|
_team_id: &TeamKey,
|
|
360
386
|
notices: &[crate::messaging::WatcherNotice],
|
|
361
387
|
) -> serde_json::Value {
|
|
362
|
-
let watcher_ids: Vec<&str> = notices
|
|
388
|
+
let watcher_ids: Vec<&str> = notices
|
|
389
|
+
.iter()
|
|
390
|
+
.map(|notice| notice.watcher_id.as_str())
|
|
391
|
+
.collect();
|
|
363
392
|
json!({
|
|
364
393
|
"watcher_ids": watcher_ids,
|
|
365
394
|
"count": watcher_ids.len(),
|
|
@@ -382,16 +411,32 @@ pub fn attach_leader_to_state(
|
|
|
382
411
|
require_current: bool,
|
|
383
412
|
) -> Result<(LeaderReceiver, Value), LeaderError> {
|
|
384
413
|
let _ = (source, require_current);
|
|
385
|
-
let pane_id = pane
|
|
414
|
+
let pane_id = pane
|
|
415
|
+
.cloned()
|
|
416
|
+
.ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
|
|
386
417
|
let non_empty_pane_id = NonEmptyPaneId::try_from_pane(&pane_id)?;
|
|
387
418
|
let identity = leader_identity_context(workspace, None, Some(state))?;
|
|
388
419
|
let epoch = current_owner_epoch(state);
|
|
389
|
-
let receiver = make_receiver(
|
|
420
|
+
let receiver = make_receiver(
|
|
421
|
+
provider,
|
|
422
|
+
&non_empty_pane_id,
|
|
423
|
+
&identity.leader_session_uuid,
|
|
424
|
+
epoch,
|
|
425
|
+
Discovery::EnvPane,
|
|
426
|
+
None,
|
|
427
|
+
);
|
|
390
428
|
if state.get("team_owner").is_some() {
|
|
391
429
|
write_receiver_to_state(state, &receiver)?;
|
|
392
430
|
} else {
|
|
393
431
|
let next_epoch = OwnerEpoch(epoch.0.saturating_add(1));
|
|
394
|
-
let receiver = make_receiver(
|
|
432
|
+
let receiver = make_receiver(
|
|
433
|
+
provider,
|
|
434
|
+
&non_empty_pane_id,
|
|
435
|
+
&identity.leader_session_uuid,
|
|
436
|
+
next_epoch,
|
|
437
|
+
Discovery::EnvPane,
|
|
438
|
+
None,
|
|
439
|
+
);
|
|
395
440
|
let owner = make_owner(provider, &non_empty_pane_id, &identity, next_epoch);
|
|
396
441
|
write_binding_to_state(state, &receiver, &owner)?;
|
|
397
442
|
write_lease_dual_state(workspace, state)?;
|
|
@@ -436,7 +481,11 @@ pub fn claim_leader(
|
|
|
436
481
|
let caller = std::env::var("TMUX_PANE")
|
|
437
482
|
.ok()
|
|
438
483
|
.filter(|pane| !pane.is_empty())
|
|
439
|
-
.or_else(||
|
|
484
|
+
.or_else(|| {
|
|
485
|
+
std::env::var("TEAM_AGENT_LEADER_PANE_ID")
|
|
486
|
+
.ok()
|
|
487
|
+
.filter(|pane| !pane.is_empty())
|
|
488
|
+
})
|
|
440
489
|
.unwrap_or_default();
|
|
441
490
|
let raw_state = crate::state::persist::load_runtime_state(workspace)?;
|
|
442
491
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
@@ -458,13 +507,15 @@ pub fn claim_leader(
|
|
|
458
507
|
let explicit_team = team.filter(|team| !team.is_empty());
|
|
459
508
|
let requested_team = explicit_team
|
|
460
509
|
.filter(|team| !team.is_empty())
|
|
461
|
-
.or_else(||
|
|
510
|
+
.or_else(|| {
|
|
511
|
+
caller_target
|
|
512
|
+
.as_ref()
|
|
513
|
+
.and_then(|target| target.team_id.as_deref())
|
|
514
|
+
})
|
|
462
515
|
.or(env_team.as_deref());
|
|
463
|
-
let team_id = TeamKey::new(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
.unwrap_or_else(|| crate::messaging::leader_receiver::active_team_key(workspace, &raw_state)),
|
|
467
|
-
);
|
|
516
|
+
let team_id = TeamKey::new(requested_team.map(str::to_string).unwrap_or_else(|| {
|
|
517
|
+
crate::messaging::leader_receiver::active_team_key(workspace, &raw_state)
|
|
518
|
+
}));
|
|
468
519
|
let active_team = crate::messaging::leader_receiver::active_team_key(workspace, &raw_state);
|
|
469
520
|
let scoped_team = explicit_team.filter(|team| {
|
|
470
521
|
*team == active_team
|
|
@@ -504,12 +555,7 @@ pub fn claim_leader(
|
|
|
504
555
|
if let Some(pane) = result.bound_pane_id.as_ref() {
|
|
505
556
|
let store = MessageStore::open(workspace)?;
|
|
506
557
|
crate::messaging::watchers::requeue_after_claim_leader(
|
|
507
|
-
workspace,
|
|
508
|
-
&store,
|
|
509
|
-
&event_log,
|
|
510
|
-
&team_id,
|
|
511
|
-
pane,
|
|
512
|
-
None,
|
|
558
|
+
workspace, &store, &event_log, &team_id, pane, None,
|
|
513
559
|
)?;
|
|
514
560
|
}
|
|
515
561
|
}
|
|
@@ -568,7 +614,9 @@ struct NonEmptyPaneId(PaneId);
|
|
|
568
614
|
impl NonEmptyPaneId {
|
|
569
615
|
fn try_from_pane(pane: &PaneId) -> Result<Self, LeaderError> {
|
|
570
616
|
if pane.as_str().trim().is_empty() {
|
|
571
|
-
return Err(LeaderError::Validation(
|
|
617
|
+
return Err(LeaderError::Validation(
|
|
618
|
+
"leader pane id is empty".to_string(),
|
|
619
|
+
));
|
|
572
620
|
}
|
|
573
621
|
Ok(Self(pane.clone()))
|
|
574
622
|
}
|
|
@@ -640,14 +688,13 @@ fn claim_lease_no_incident_with_target(
|
|
|
640
688
|
} else {
|
|
641
689
|
None
|
|
642
690
|
};
|
|
643
|
-
let (mut topology_convergence, converged) =
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
);
|
|
691
|
+
let (mut topology_convergence, converged) = apply_endpoint_convergence(
|
|
692
|
+
state,
|
|
693
|
+
team_id,
|
|
694
|
+
convergence_candidate,
|
|
695
|
+
candidate_source,
|
|
696
|
+
pre_epoch,
|
|
697
|
+
);
|
|
651
698
|
if converged {
|
|
652
699
|
write_claim_state(workspace, state, scoped_team, team)?;
|
|
653
700
|
topology_convergence = verify_persisted_topology_convergence(
|
|
@@ -681,14 +728,12 @@ fn claim_lease_no_incident_with_target(
|
|
|
681
728
|
topology_convergence,
|
|
682
729
|
});
|
|
683
730
|
}
|
|
684
|
-
let owner_live = bound_pane_id
|
|
685
|
-
.
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
liveness.liveness(pane) == PaneLiveness::Live
|
|
691
|
-
});
|
|
731
|
+
let owner_live = bound_pane_id.as_deref().is_some_and(|pane| {
|
|
732
|
+
if pane == caller_pane.as_str() && !bound_endpoint_matches_caller {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
liveness.liveness(pane) == PaneLiveness::Live
|
|
736
|
+
});
|
|
692
737
|
if owner_live && !confirm {
|
|
693
738
|
emit_lease_refusal(
|
|
694
739
|
event_log,
|
|
@@ -724,9 +769,9 @@ fn claim_lease_no_incident_with_target(
|
|
|
724
769
|
));
|
|
725
770
|
}
|
|
726
771
|
let locked_bound_pane = bound_pane(&locked);
|
|
727
|
-
let locked_owner_live = locked_bound_pane
|
|
728
|
-
.
|
|
729
|
-
|
|
772
|
+
let locked_owner_live = locked_bound_pane.as_deref().is_some_and(|pane| {
|
|
773
|
+
pane != caller_pane.as_str() && liveness.liveness(pane) == PaneLiveness::Live
|
|
774
|
+
});
|
|
730
775
|
if locked_owner_live && !confirm {
|
|
731
776
|
emit_lease_refusal(
|
|
732
777
|
event_log,
|
|
@@ -750,10 +795,8 @@ fn claim_lease_no_incident_with_target(
|
|
|
750
795
|
// pane → delivery routes worker messages to itself (loop) and the leader
|
|
751
796
|
// loses its handle (the macmini "hand-handle mapping 灾难" truth source).
|
|
752
797
|
if let Some(caller_pane_info) = caller_pane_info {
|
|
753
|
-
match crate::topology::classify_registered_worker_for_observed_pane(
|
|
754
|
-
|
|
755
|
-
caller_pane_info,
|
|
756
|
-
) {
|
|
798
|
+
match crate::topology::classify_registered_worker_for_observed_pane(state, caller_pane_info)
|
|
799
|
+
{
|
|
757
800
|
crate::topology::WorkerPaneBindingMatch::LiveSameWorker { agent_id } => {
|
|
758
801
|
emit_lease_refusal(
|
|
759
802
|
event_log,
|
|
@@ -829,14 +872,13 @@ fn claim_lease_no_incident_with_target(
|
|
|
829
872
|
} else {
|
|
830
873
|
None
|
|
831
874
|
};
|
|
832
|
-
let (mut topology_convergence, converged) =
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
);
|
|
875
|
+
let (mut topology_convergence, converged) = apply_endpoint_convergence(
|
|
876
|
+
state,
|
|
877
|
+
team_id,
|
|
878
|
+
receiver.tmux_socket.as_deref(),
|
|
879
|
+
candidate_source,
|
|
880
|
+
next_epoch,
|
|
881
|
+
);
|
|
840
882
|
write_claim_state(workspace, state, scoped_team, team)?;
|
|
841
883
|
if converged {
|
|
842
884
|
topology_convergence = verify_persisted_topology_convergence(
|
|
@@ -858,7 +900,12 @@ fn claim_lease_no_incident_with_target(
|
|
|
858
900
|
));
|
|
859
901
|
}
|
|
860
902
|
}
|
|
861
|
-
let uuid_prefix = identity
|
|
903
|
+
let uuid_prefix = identity
|
|
904
|
+
.leader_session_uuid
|
|
905
|
+
.as_str()
|
|
906
|
+
.chars()
|
|
907
|
+
.take(8)
|
|
908
|
+
.collect::<String>();
|
|
862
909
|
if reason == LeaseReason::PreviousOwnerPaneDead {
|
|
863
910
|
event_log.write(
|
|
864
911
|
super::LeaderEvent::OwnerAdoptedOnRestart.name(),
|
|
@@ -1105,7 +1152,10 @@ fn mark_convergence_persisted(metadata: &mut Value, team_id: &str) {
|
|
|
1105
1152
|
return;
|
|
1106
1153
|
};
|
|
1107
1154
|
obj.insert("persisted".to_string(), json!(true));
|
|
1108
|
-
obj.insert(
|
|
1155
|
+
obj.insert(
|
|
1156
|
+
"checked_paths".to_string(),
|
|
1157
|
+
json!(convergence_checked_paths(team_id)),
|
|
1158
|
+
);
|
|
1109
1159
|
}
|
|
1110
1160
|
|
|
1111
1161
|
fn mark_convergence_persistence_conflict(metadata: &mut Value, team_id: &str) {
|
|
@@ -1114,7 +1164,10 @@ fn mark_convergence_persistence_conflict(metadata: &mut Value, team_id: &str) {
|
|
|
1114
1164
|
};
|
|
1115
1165
|
obj.insert("status".to_string(), json!("persistence_conflict"));
|
|
1116
1166
|
obj.insert("persisted".to_string(), json!(false));
|
|
1117
|
-
obj.insert(
|
|
1167
|
+
obj.insert(
|
|
1168
|
+
"checked_paths".to_string(),
|
|
1169
|
+
json!(convergence_checked_paths(team_id)),
|
|
1170
|
+
);
|
|
1118
1171
|
obj.insert(
|
|
1119
1172
|
"action".to_string(),
|
|
1120
1173
|
json!("endpoint convergence was not durably persisted; retry claim-leader or takeover"),
|
|
@@ -1170,7 +1223,11 @@ fn refused(
|
|
|
1170
1223
|
owner: None,
|
|
1171
1224
|
owner_epoch: epoch,
|
|
1172
1225
|
reason: Some(reason),
|
|
1173
|
-
action: if action.is_empty() {
|
|
1226
|
+
action: if action.is_empty() {
|
|
1227
|
+
None
|
|
1228
|
+
} else {
|
|
1229
|
+
Some(action.to_string())
|
|
1230
|
+
},
|
|
1174
1231
|
bound_pane_id,
|
|
1175
1232
|
topology_convergence: None,
|
|
1176
1233
|
}
|
|
@@ -1178,7 +1235,8 @@ fn refused(
|
|
|
1178
1235
|
|
|
1179
1236
|
fn current_owner_epoch(state: &Value) -> OwnerEpoch {
|
|
1180
1237
|
let owner_epoch = get_path_u64(state, &["team_owner", "owner_epoch"]).filter(|v| *v != 0);
|
|
1181
|
-
let receiver_epoch =
|
|
1238
|
+
let receiver_epoch =
|
|
1239
|
+
get_path_u64(state, &["leader_receiver", "owner_epoch"]).filter(|v| *v != 0);
|
|
1182
1240
|
OwnerEpoch(owner_epoch.or(receiver_epoch).unwrap_or(0))
|
|
1183
1241
|
}
|
|
1184
1242
|
|
|
@@ -1189,7 +1247,9 @@ fn bound_pane(state: &Value) -> Option<String> {
|
|
|
1189
1247
|
}
|
|
1190
1248
|
|
|
1191
1249
|
fn bound_endpoint_matches_current_process(state: &Value) -> bool {
|
|
1192
|
-
let Some(bound) =
|
|
1250
|
+
let Some(bound) =
|
|
1251
|
+
get_path_str(state, &["leader_receiver", "tmux_socket"]).filter(|v| !v.is_empty())
|
|
1252
|
+
else {
|
|
1193
1253
|
return true;
|
|
1194
1254
|
};
|
|
1195
1255
|
let Some(current) = crate::tmux_backend::socket_name_from_tmux_env() else {
|
|
@@ -1316,7 +1376,11 @@ fn claim_target_from_pane_info(workspace: &Path, target: &PaneInfo) -> Option<Le
|
|
|
1316
1376
|
Some(LeaderClaimTarget {
|
|
1317
1377
|
provider,
|
|
1318
1378
|
leader_session_uuid: target_leader_session_uuid(target),
|
|
1319
|
-
team_id: target
|
|
1379
|
+
team_id: target
|
|
1380
|
+
.leader_env
|
|
1381
|
+
.get("TEAM_AGENT_TEAM_ID")
|
|
1382
|
+
.filter(|raw| !raw.is_empty())
|
|
1383
|
+
.cloned(),
|
|
1320
1384
|
pane_info: Some(target.clone()),
|
|
1321
1385
|
endpoint: None,
|
|
1322
1386
|
})
|
|
@@ -1359,7 +1423,10 @@ fn validate_attach_target(
|
|
|
1359
1423
|
.or_else(|| get_path_str(state, &["leader_receiver", "leader_session_uuid"]));
|
|
1360
1424
|
if let (Some(recorded), Some(target_uuid)) = (
|
|
1361
1425
|
recorded_uuid.as_deref(),
|
|
1362
|
-
claim_target
|
|
1426
|
+
claim_target
|
|
1427
|
+
.leader_session_uuid
|
|
1428
|
+
.as_ref()
|
|
1429
|
+
.map(|u| u.as_str()),
|
|
1363
1430
|
) {
|
|
1364
1431
|
if recorded != target_uuid {
|
|
1365
1432
|
return Err("leader_session_uuid_mismatch");
|
|
@@ -1472,7 +1539,10 @@ impl crate::state::owner_gate::PaneLivenessProbe for TargetScanLiveness {
|
|
|
1472
1539
|
}
|
|
1473
1540
|
}
|
|
1474
1541
|
|
|
1475
|
-
fn locked_runtime_state(
|
|
1542
|
+
fn locked_runtime_state(
|
|
1543
|
+
workspace: &Path,
|
|
1544
|
+
scoped_team: Option<&str>,
|
|
1545
|
+
) -> Result<Option<Value>, LeaderError> {
|
|
1476
1546
|
let path = crate::state::persist::runtime_state_path(workspace);
|
|
1477
1547
|
if !path.exists() {
|
|
1478
1548
|
return Ok(None);
|
|
@@ -1537,9 +1607,13 @@ fn make_receiver(
|
|
|
1537
1607
|
provider,
|
|
1538
1608
|
pane_id: pane.as_pane_id().clone(),
|
|
1539
1609
|
session_name: target.as_ref().map(|t| t.session.clone()),
|
|
1540
|
-
window_index: target
|
|
1610
|
+
window_index: target
|
|
1611
|
+
.as_ref()
|
|
1612
|
+
.and_then(|t| t.window_index.map(|v| v.to_string())),
|
|
1541
1613
|
window_name: target.as_ref().and_then(|t| t.window_name.clone()),
|
|
1542
|
-
pane_index: target
|
|
1614
|
+
pane_index: target
|
|
1615
|
+
.as_ref()
|
|
1616
|
+
.and_then(|t| t.pane_index.map(|v| v.to_string())),
|
|
1543
1617
|
pane_tty: target.as_ref().and_then(|t| t.tty.clone()),
|
|
1544
1618
|
pane_current_command: target.as_ref().and_then(|t| t.current_command.clone()),
|
|
1545
1619
|
tmux_socket: crate::tmux_backend::socket_name_from_tmux_env(),
|
|
@@ -1557,8 +1631,12 @@ fn receiver_fingerprint(target: &PaneInfo) -> String {
|
|
|
1557
1631
|
format!(
|
|
1558
1632
|
"{}|{}|{}|{}",
|
|
1559
1633
|
target.session.as_str(),
|
|
1560
|
-
target
|
|
1561
|
-
|
|
1634
|
+
target
|
|
1635
|
+
.window_index
|
|
1636
|
+
.map_or_else(String::new, |v| v.to_string()),
|
|
1637
|
+
target
|
|
1638
|
+
.pane_index
|
|
1639
|
+
.map_or_else(String::new, |v| v.to_string()),
|
|
1562
1640
|
target.tty.as_deref().unwrap_or("")
|
|
1563
1641
|
)
|
|
1564
1642
|
}
|
|
@@ -1605,7 +1683,9 @@ fn write_binding_to_state(
|
|
|
1605
1683
|
*state = json!({});
|
|
1606
1684
|
}
|
|
1607
1685
|
if !state.is_object() {
|
|
1608
|
-
return Err(LeaderError::Validation(
|
|
1686
|
+
return Err(LeaderError::Validation(
|
|
1687
|
+
"state root is not an object".to_string(),
|
|
1688
|
+
));
|
|
1609
1689
|
}
|
|
1610
1690
|
let team_key = canonical_owner_write_key(state);
|
|
1611
1691
|
let record = crate::state::ownership::OwnershipWrite::new()
|
|
@@ -1624,7 +1704,9 @@ fn write_receiver_to_state(
|
|
|
1624
1704
|
*state = json!({});
|
|
1625
1705
|
}
|
|
1626
1706
|
if !state.is_object() {
|
|
1627
|
-
return Err(LeaderError::Validation(
|
|
1707
|
+
return Err(LeaderError::Validation(
|
|
1708
|
+
"state root is not an object".to_string(),
|
|
1709
|
+
));
|
|
1628
1710
|
}
|
|
1629
1711
|
let team_key = canonical_owner_write_key(state);
|
|
1630
1712
|
let record = crate::state::ownership::OwnershipWrite::new()
|
|
@@ -1787,7 +1869,11 @@ fn write_claim_state(
|
|
|
1787
1869
|
}
|
|
1788
1870
|
}
|
|
1789
1871
|
|
|
1790
|
-
fn save_claim_team_scoped_state(
|
|
1872
|
+
fn save_claim_team_scoped_state(
|
|
1873
|
+
workspace: &Path,
|
|
1874
|
+
state: &Value,
|
|
1875
|
+
target_key: &str,
|
|
1876
|
+
) -> Result<(), LeaderError> {
|
|
1791
1877
|
let existing = crate::state::persist::load_runtime_state(workspace)?;
|
|
1792
1878
|
let mut teams = existing
|
|
1793
1879
|
.get("teams")
|