@team-agent/installer 0.5.77 → 0.5.78
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 +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +362 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +151 -14
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +65 -18
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +67 -8
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/state/persist.rs +97 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
- package/crates/team-agent/src/tmux_backend.rs +208 -118
- package/crates/team-agent/src/transport.rs +72 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -142,7 +142,28 @@ pub fn bind_owner_from_caller_pane(
|
|
|
142
142
|
.as_ref()
|
|
143
143
|
.and_then(|info| info.current_command.clone())
|
|
144
144
|
.unwrap_or_else(|| tmux_pane_current_command(workspace, &pane).unwrap_or_default());
|
|
145
|
-
let provider = bind_provider_from_env_or_pane(&caller_current_command, caller_info)
|
|
145
|
+
let Some(provider) = bind_provider_from_env_or_pane(&caller_current_command, caller_info) else {
|
|
146
|
+
let hint = "set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; unknown commands are not Codex";
|
|
147
|
+
event_log.write(
|
|
148
|
+
LeaderEvent::OwnerBindRefused.name(),
|
|
149
|
+
json!({
|
|
150
|
+
"reason": serde_json::to_value(LeaseReason::CallerNotLeaderShaped)?,
|
|
151
|
+
"caller_pane_id": pane,
|
|
152
|
+
"caller_current_command": caller_current_command.clone(),
|
|
153
|
+
"team_id": team_id.as_str(),
|
|
154
|
+
"hint": hint,
|
|
155
|
+
}),
|
|
156
|
+
)?;
|
|
157
|
+
return Ok(OwnerBindResult {
|
|
158
|
+
ok: false,
|
|
159
|
+
owner: None,
|
|
160
|
+
caller_pane_id: PaneId::new(pane),
|
|
161
|
+
caller_current_command,
|
|
162
|
+
team_id: team_id.clone(),
|
|
163
|
+
reason: Some(LeaseReason::CallerNotLeaderShaped),
|
|
164
|
+
hint: Some(hint.to_string()),
|
|
165
|
+
});
|
|
166
|
+
};
|
|
146
167
|
let machine_fingerprint = std::env::var("TEAM_AGENT_MACHINE_FINGERPRINT").unwrap_or_default();
|
|
147
168
|
let os_user = std::env::var("USER")
|
|
148
169
|
.or_else(|_| std::env::var("USERNAME"))
|
|
@@ -207,17 +228,13 @@ fn emit_owner_bound_event(
|
|
|
207
228
|
Ok(())
|
|
208
229
|
}
|
|
209
230
|
|
|
210
|
-
fn bind_provider_from_env_or_pane(command: &str, pane: Option<PaneInfo>) -> Provider {
|
|
231
|
+
fn bind_provider_from_env_or_pane(command: &str, pane: Option<PaneInfo>) -> Option<Provider> {
|
|
211
232
|
let env_provider = std::env::var("TEAM_AGENT_LEADER_PROVIDER")
|
|
212
233
|
.ok()
|
|
213
234
|
.and_then(|raw| super::helpers::parse_provider(&raw));
|
|
214
235
|
env_provider
|
|
215
236
|
.or_else(|| pane.as_ref().and_then(super::attribute_pane_provider))
|
|
216
237
|
.or_else(|| super::attribute_command_provider(command))
|
|
217
|
-
// E11 层2:未知命令不再静默默认 codex(会误绑任意 provider + 喂错分类器)。
|
|
218
|
-
// 无法识别时回落 Codex 仅作最末兜底,且该路径已被统一归因入口的显式 None 收窄
|
|
219
|
-
// (调用方理应只在已知 leader 命令上 bind);保留以不改 fn 签名/上游 panic 面。
|
|
220
|
-
.unwrap_or(Provider::Codex)
|
|
221
238
|
}
|
|
222
239
|
|
|
223
240
|
pub(crate) fn strict_owner_bind_provider(
|
|
@@ -352,13 +369,8 @@ mod e11_provider_bind_tests {
|
|
|
352
369
|
}
|
|
353
370
|
|
|
354
371
|
#[test]
|
|
355
|
-
fn
|
|
356
|
-
assert_eq!(
|
|
357
|
-
strict_owner_bind_provider(Some("pi"), "codex"),
|
|
358
|
-
Some(Provider::Pi)
|
|
359
|
-
);
|
|
372
|
+
fn provider_independent_rejects_unknown_or_node_without_fallback() {
|
|
360
373
|
assert_eq!(strict_owner_bind_provider(Some("unknown"), "codex"), None);
|
|
361
|
-
assert_eq!(strict_owner_bind_provider(None, "pi"), Some(Provider::Pi));
|
|
362
374
|
assert_eq!(strict_owner_bind_provider(None, "node"), None);
|
|
363
375
|
}
|
|
364
376
|
|
|
@@ -394,3 +406,19 @@ mod e11_provider_bind_tests {
|
|
|
394
406
|
assert_eq!(owner_bind_provider_wire("totally-unknown"), "");
|
|
395
407
|
}
|
|
396
408
|
}
|
|
409
|
+
|
|
410
|
+
#[cfg(test)]
|
|
411
|
+
mod claim_pi_carrier_tests {
|
|
412
|
+
use super::*;
|
|
413
|
+
|
|
414
|
+
#[test]
|
|
415
|
+
fn strict_provider_never_falls_back_from_unknown_explicit_or_command() {
|
|
416
|
+
assert_eq!(
|
|
417
|
+
strict_owner_bind_provider(Some("pi"), "codex"),
|
|
418
|
+
Some(Provider::Pi)
|
|
419
|
+
);
|
|
420
|
+
assert_eq!(strict_owner_bind_provider(Some("unknown"), "codex"), None);
|
|
421
|
+
assert_eq!(strict_owner_bind_provider(None, "pi"), Some(Provider::Pi));
|
|
422
|
+
assert_eq!(strict_owner_bind_provider(None, "node"), None);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -204,6 +204,71 @@ fn try_readopt_refuses_when_different_live_owner_uuid_mismatches() {
|
|
|
204
204
|
assert!(state.get("team_owner").is_none());
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
#[test]
|
|
208
|
+
fn try_readopt_inactive_bash_wrapper_with_matching_owner_uuid() {
|
|
209
|
+
let ws = ws("readopt-bash-ok");
|
|
210
|
+
let mut state = json!({"session_name": "sess"});
|
|
211
|
+
let uuid = "c1e3f7a7aeaed95491667b4d9c3ce6fb";
|
|
212
|
+
let mut recv = receiver("%0", uuid, 1);
|
|
213
|
+
let owner = owner("%0", uuid, 1);
|
|
214
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
215
|
+
let pane = json!({
|
|
216
|
+
"pane_id": "%0",
|
|
217
|
+
"session_name": "sess",
|
|
218
|
+
"window_name": "pi",
|
|
219
|
+
"pane_current_command": "bash",
|
|
220
|
+
"pane_current_path": ws.join("sub").to_string_lossy(),
|
|
221
|
+
"active": false,
|
|
222
|
+
"leader_session_uuid": uuid,
|
|
223
|
+
"leader_env": {
|
|
224
|
+
"TEAM_AGENT_LEADER_SESSION_UUID": uuid,
|
|
225
|
+
"TEAM_AGENT_WORKSPACE": ws.to_string_lossy(),
|
|
226
|
+
"TEAM_AGENT_TEAM_ID": "sess"
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
let got = try_readopt_leader_pane(
|
|
230
|
+
&ws,
|
|
231
|
+
&mut state,
|
|
232
|
+
&mut recv,
|
|
233
|
+
&pane,
|
|
234
|
+
&json!({"targets": [pane.clone()]}),
|
|
235
|
+
Some(&owner),
|
|
236
|
+
Provider::Codex,
|
|
237
|
+
LeaseSource::Manual,
|
|
238
|
+
&event_log,
|
|
239
|
+
)
|
|
240
|
+
.unwrap();
|
|
241
|
+
assert!(got.is_some(), "matching inactive wrapper must readopt");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[test]
|
|
245
|
+
fn try_readopt_bash_without_recorded_owner_uuid_refuses() {
|
|
246
|
+
let ws = ws("readopt-bash-no-id");
|
|
247
|
+
let mut state = json!({});
|
|
248
|
+
let mut recv = receiver("%0", "c1e3f7a7aeaed95491667b4d9c3ce6fb", 1);
|
|
249
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
250
|
+
let pane = json!({
|
|
251
|
+
"pane_id": "%0",
|
|
252
|
+
"session_name": "sess",
|
|
253
|
+
"pane_current_command": "bash",
|
|
254
|
+
"pane_current_path": ws.join("sub").to_string_lossy(),
|
|
255
|
+
"active": false
|
|
256
|
+
});
|
|
257
|
+
let got = try_readopt_leader_pane(
|
|
258
|
+
&ws,
|
|
259
|
+
&mut state,
|
|
260
|
+
&mut recv,
|
|
261
|
+
&pane,
|
|
262
|
+
&json!({"targets": [pane.clone()]}),
|
|
263
|
+
None,
|
|
264
|
+
Provider::Codex,
|
|
265
|
+
LeaseSource::Manual,
|
|
266
|
+
&event_log,
|
|
267
|
+
)
|
|
268
|
+
.unwrap();
|
|
269
|
+
assert!(got.is_none());
|
|
270
|
+
}
|
|
271
|
+
|
|
207
272
|
#[test]
|
|
208
273
|
fn rediscover_updates_unique_env_triple_match() {
|
|
209
274
|
let ws = ws("rediscover");
|
|
@@ -42,7 +42,12 @@ pub fn try_readopt_leader_pane(
|
|
|
42
42
|
)?;
|
|
43
43
|
return Ok(None);
|
|
44
44
|
};
|
|
45
|
-
if !readopt_candidate_is_usable_leader(
|
|
45
|
+
if !readopt_candidate_is_usable_leader(
|
|
46
|
+
workspace,
|
|
47
|
+
&candidate,
|
|
48
|
+
owner_record,
|
|
49
|
+
receiver_provider,
|
|
50
|
+
) {
|
|
46
51
|
event_log.write(
|
|
47
52
|
LeaderEvent::ReceiverRebindRequired.name(),
|
|
48
53
|
json!({"reason": "leader_pane_unusable"}),
|
|
@@ -598,14 +603,39 @@ fn target_iter(targets: &Value) -> Vec<LeaderTarget> {
|
|
|
598
603
|
Vec::new()
|
|
599
604
|
}
|
|
600
605
|
|
|
601
|
-
fn readopt_candidate_is_usable_leader(
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
+
fn readopt_candidate_is_usable_leader(
|
|
607
|
+
workspace: &Path,
|
|
608
|
+
target: &LeaderTarget,
|
|
609
|
+
owner_record: Option<&TeamOwner>,
|
|
610
|
+
receiver_provider: Provider,
|
|
611
|
+
) -> bool {
|
|
612
|
+
if !target
|
|
606
613
|
.current_path
|
|
607
614
|
.as_deref()
|
|
608
615
|
.is_some_and(|path| path_in_workspace(path, workspace))
|
|
616
|
+
{
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
if target.provider.is_some() {
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
let Some(owner) = owner_record else {
|
|
623
|
+
return false;
|
|
624
|
+
};
|
|
625
|
+
if owner.pane_id.as_str() != target.pane_id.as_str() {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
if owner.provider != receiver_provider {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
match (
|
|
632
|
+
owner.leader_session_uuid.as_ref(),
|
|
633
|
+
target.leader_session_uuid.as_ref(),
|
|
634
|
+
) {
|
|
635
|
+
(Some(recorded), Some(live)) => recorded == live,
|
|
636
|
+
(Some(_), None) => true,
|
|
637
|
+
(None, _) => false,
|
|
638
|
+
}
|
|
609
639
|
}
|
|
610
640
|
|
|
611
641
|
fn rediscover_candidate_is_usable_leader(target: &LeaderTarget) -> bool {
|
|
@@ -1859,6 +1859,17 @@ fn persist_managed_leader_binding(
|
|
|
1859
1859
|
&serde_json::json!(socket),
|
|
1860
1860
|
);
|
|
1861
1861
|
let bound_panes = merge_bound_panes(existing_recv.as_ref(), new_binding);
|
|
1862
|
+
let existing_owner = state
|
|
1863
|
+
.get("teams")
|
|
1864
|
+
.and_then(|teams| teams.get(&team_key))
|
|
1865
|
+
.and_then(|team| team.get("team_owner"))
|
|
1866
|
+
.cloned();
|
|
1867
|
+
let keep_field = |existing: Option<&serde_json::Value>, key: &str, fallback: serde_json::Value| {
|
|
1868
|
+
existing
|
|
1869
|
+
.and_then(|value| value.get(key))
|
|
1870
|
+
.cloned()
|
|
1871
|
+
.unwrap_or(fallback)
|
|
1872
|
+
};
|
|
1862
1873
|
let (scalar_pane, scalar_session, scalar_window, scalar_attached_at) =
|
|
1863
1874
|
match existing_recv.as_ref() {
|
|
1864
1875
|
Some(recv) if keep_existing_scalar => (
|
|
@@ -1867,20 +1878,41 @@ fn persist_managed_leader_binding(
|
|
|
1867
1878
|
.unwrap_or(pane.as_str())
|
|
1868
1879
|
.to_string(),
|
|
1869
1880
|
recv.get("session_name")
|
|
1870
|
-
.
|
|
1871
|
-
.
|
|
1872
|
-
.to_string(),
|
|
1881
|
+
.cloned()
|
|
1882
|
+
.unwrap_or_else(|| serde_json::json!(session)),
|
|
1873
1883
|
recv.get("window_name")
|
|
1874
|
-
.
|
|
1875
|
-
.
|
|
1876
|
-
.to_string(),
|
|
1884
|
+
.cloned()
|
|
1885
|
+
.unwrap_or_else(|| serde_json::json!(window)),
|
|
1877
1886
|
recv.get("attached_at")
|
|
1878
1887
|
.and_then(serde_json::Value::as_str)
|
|
1879
1888
|
.unwrap_or(now.as_str())
|
|
1880
1889
|
.to_string(),
|
|
1881
1890
|
),
|
|
1882
|
-
_ => (
|
|
1891
|
+
_ => (
|
|
1892
|
+
pane.clone(),
|
|
1893
|
+
serde_json::json!(session),
|
|
1894
|
+
serde_json::json!(window),
|
|
1895
|
+
now.clone(),
|
|
1896
|
+
),
|
|
1883
1897
|
};
|
|
1898
|
+
let leader_session_uuid = if keep_existing_scalar {
|
|
1899
|
+
keep_field(
|
|
1900
|
+
existing_recv.as_ref(),
|
|
1901
|
+
"leader_session_uuid",
|
|
1902
|
+
serde_json::json!(identity.leader_session_uuid),
|
|
1903
|
+
)
|
|
1904
|
+
} else {
|
|
1905
|
+
serde_json::json!(identity.leader_session_uuid)
|
|
1906
|
+
};
|
|
1907
|
+
let discovery = if keep_existing_scalar {
|
|
1908
|
+
keep_field(
|
|
1909
|
+
existing_recv.as_ref(),
|
|
1910
|
+
"discovery",
|
|
1911
|
+
serde_json::json!("managed_launcher"),
|
|
1912
|
+
)
|
|
1913
|
+
} else {
|
|
1914
|
+
serde_json::json!("managed_launcher")
|
|
1915
|
+
};
|
|
1884
1916
|
let receiver = serde_json::json!({
|
|
1885
1917
|
"mode": "direct_tmux",
|
|
1886
1918
|
"status": "attached",
|
|
@@ -1890,20 +1922,43 @@ fn persist_managed_leader_binding(
|
|
|
1890
1922
|
"session_name": scalar_session,
|
|
1891
1923
|
"window_name": scalar_window,
|
|
1892
1924
|
"tmux_socket": socket,
|
|
1893
|
-
"leader_session_uuid":
|
|
1925
|
+
"leader_session_uuid": leader_session_uuid.clone(),
|
|
1894
1926
|
"owner_epoch": owner_epoch,
|
|
1895
1927
|
"attached_at": scalar_attached_at,
|
|
1896
|
-
"discovery":
|
|
1928
|
+
"discovery": discovery,
|
|
1897
1929
|
"bound_panes": bound_panes,
|
|
1898
1930
|
});
|
|
1931
|
+
let owner_uuid = if keep_existing_scalar {
|
|
1932
|
+
keep_field(
|
|
1933
|
+
existing_owner.as_ref(),
|
|
1934
|
+
"leader_session_uuid",
|
|
1935
|
+
leader_session_uuid.clone(),
|
|
1936
|
+
)
|
|
1937
|
+
} else {
|
|
1938
|
+
leader_session_uuid.clone()
|
|
1939
|
+
};
|
|
1940
|
+
let claimed_via = if keep_existing_scalar {
|
|
1941
|
+
keep_field(
|
|
1942
|
+
existing_owner.as_ref(),
|
|
1943
|
+
"claimed_via",
|
|
1944
|
+
serde_json::json!("claim-leader"),
|
|
1945
|
+
)
|
|
1946
|
+
} else {
|
|
1947
|
+
serde_json::json!("claim-leader")
|
|
1948
|
+
};
|
|
1949
|
+
let claimed_at = if keep_existing_scalar {
|
|
1950
|
+
keep_field(existing_owner.as_ref(), "claimed_at", serde_json::json!(now))
|
|
1951
|
+
} else {
|
|
1952
|
+
serde_json::json!(now)
|
|
1953
|
+
};
|
|
1899
1954
|
let owner = serde_json::json!({
|
|
1900
1955
|
"pane_id": scalar_pane,
|
|
1901
1956
|
"provider": provider.clone(),
|
|
1902
1957
|
"machine_fingerprint": identity.machine_fingerprint,
|
|
1903
|
-
"leader_session_uuid":
|
|
1958
|
+
"leader_session_uuid": owner_uuid,
|
|
1904
1959
|
"owner_epoch": owner_epoch,
|
|
1905
|
-
"claimed_at":
|
|
1906
|
-
"claimed_via":
|
|
1960
|
+
"claimed_at": claimed_at,
|
|
1961
|
+
"claimed_via": claimed_via,
|
|
1907
1962
|
"os_user": identity.os_user,
|
|
1908
1963
|
});
|
|
1909
1964
|
if let Some(obj) = state.as_object_mut() {
|
|
@@ -3881,18 +3936,22 @@ mod tests {
|
|
|
3881
3936
|
}
|
|
3882
3937
|
|
|
3883
3938
|
fn persist_test_identity(workspace: &Path) -> LeaderIdentity {
|
|
3939
|
+
persist_test_identity_named(workspace, "tester")
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
fn persist_test_identity_named(workspace: &Path, os_user: &str) -> LeaderIdentity {
|
|
3884
3943
|
LeaderIdentity {
|
|
3885
3944
|
leader_session_uuid: LeaderSessionUuid::derive(
|
|
3886
3945
|
"fp",
|
|
3887
3946
|
&workspace.to_string_lossy(),
|
|
3888
|
-
|
|
3947
|
+
os_user,
|
|
3889
3948
|
"current",
|
|
3890
3949
|
)
|
|
3891
3950
|
.unwrap(),
|
|
3892
3951
|
leader_session_uuid_source: LeaderSessionUuidSource::Derived,
|
|
3893
3952
|
machine_fingerprint: "fp".to_string(),
|
|
3894
3953
|
workspace_abspath: workspace.to_path_buf(),
|
|
3895
|
-
os_user:
|
|
3954
|
+
os_user: os_user.to_string(),
|
|
3896
3955
|
team_id: TeamKey::new("current"),
|
|
3897
3956
|
}
|
|
3898
3957
|
}
|
|
@@ -3995,6 +4054,84 @@ mod tests {
|
|
|
3995
4054
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
3996
4055
|
}
|
|
3997
4056
|
|
|
4057
|
+
#[test]
|
|
4058
|
+
fn multi_leader_persist_keeps_first_owner_uuid() {
|
|
4059
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
4060
|
+
"ta-multi-leader-uuid-{}",
|
|
4061
|
+
std::process::id()
|
|
4062
|
+
));
|
|
4063
|
+
std::fs::create_dir_all(workspace.join(".team").join("runtime")).unwrap();
|
|
4064
|
+
let original_uuid = "b2b230cfd645e78cf49e8147ba32ff3b";
|
|
4065
|
+
let state = serde_json::json!({
|
|
4066
|
+
"active_team_key": "current",
|
|
4067
|
+
"teams": {
|
|
4068
|
+
"current": {
|
|
4069
|
+
"leader_receiver": {
|
|
4070
|
+
"status": "attached",
|
|
4071
|
+
"pane_id": "%0",
|
|
4072
|
+
"discovery": "env_pane",
|
|
4073
|
+
"leader_session_uuid": original_uuid,
|
|
4074
|
+
"owner_epoch": 1,
|
|
4075
|
+
"attached_at": "2026-09-09T17:08:31.965311+00:00",
|
|
4076
|
+
"tmux_socket": "/private/tmp/tmux-501/ta-33bddc6e7687",
|
|
4077
|
+
"session_name": null,
|
|
4078
|
+
"provider": "codex"
|
|
4079
|
+
},
|
|
4080
|
+
"team_owner": {
|
|
4081
|
+
"pane_id": "%0",
|
|
4082
|
+
"leader_session_uuid": original_uuid,
|
|
4083
|
+
"owner_epoch": 1,
|
|
4084
|
+
"claimed_via": "quick-start"
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
std::fs::write(
|
|
4090
|
+
crate::state::persist::runtime_state_path(&workspace),
|
|
4091
|
+
serde_json::to_vec_pretty(&state).unwrap(),
|
|
4092
|
+
)
|
|
4093
|
+
.unwrap();
|
|
4094
|
+
let mut plan = persist_test_plan(&workspace);
|
|
4095
|
+
plan.identity = Some(persist_test_identity_named(&workspace, "second-launcher"));
|
|
4096
|
+
let second_uuid = plan
|
|
4097
|
+
.identity
|
|
4098
|
+
.as_ref()
|
|
4099
|
+
.unwrap()
|
|
4100
|
+
.leader_session_uuid
|
|
4101
|
+
.as_str()
|
|
4102
|
+
.to_string();
|
|
4103
|
+
assert_ne!(second_uuid, original_uuid);
|
|
4104
|
+
let spawned = SpawnResult {
|
|
4105
|
+
pane_id: PaneId::new("%2"),
|
|
4106
|
+
session: SessionName::new(
|
|
4107
|
+
"team-agent-leader-codex-team-agent-live-0578-r4-19450903",
|
|
4108
|
+
),
|
|
4109
|
+
window: WindowName::new("codex-second"),
|
|
4110
|
+
child_pid: Some(2),
|
|
4111
|
+
};
|
|
4112
|
+
persist_managed_leader_binding(&plan, &workspace, &spawned).expect("second persist");
|
|
4113
|
+
let loaded = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4114
|
+
let recv = &loaded["teams"]["current"]["leader_receiver"];
|
|
4115
|
+
let owner = &loaded["teams"]["current"]["team_owner"];
|
|
4116
|
+
assert_eq!(recv["pane_id"], serde_json::json!("%0"));
|
|
4117
|
+
assert_eq!(recv["leader_session_uuid"], serde_json::json!(original_uuid));
|
|
4118
|
+
assert_eq!(recv["discovery"], serde_json::json!("env_pane"));
|
|
4119
|
+
assert_eq!(recv["session_name"], serde_json::Value::Null);
|
|
4120
|
+
assert_eq!(recv["owner_epoch"], serde_json::json!(1));
|
|
4121
|
+
assert_eq!(
|
|
4122
|
+
owner["leader_session_uuid"],
|
|
4123
|
+
serde_json::json!(original_uuid)
|
|
4124
|
+
);
|
|
4125
|
+
let panes = recv["bound_panes"].as_array().expect("bound_panes");
|
|
4126
|
+
let ids: Vec<&str> = panes
|
|
4127
|
+
.iter()
|
|
4128
|
+
.filter_map(|pane| pane.get("pane_id").and_then(serde_json::Value::as_str))
|
|
4129
|
+
.collect();
|
|
4130
|
+
assert!(ids.contains(&"%0"), "bound_panes missing first: {recv}");
|
|
4131
|
+
assert!(ids.contains(&"%2"), "bound_panes missing second: {recv}");
|
|
4132
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
3998
4135
|
#[test]
|
|
3999
4136
|
fn multi_leader_clear_promotes_remaining() {
|
|
4000
4137
|
let workspace =
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
4
|
+
mod hermetic;
|
|
5
|
+
|
|
3
6
|
// =====================================================================
|
|
4
7
|
// 5. leader_session_name — sha1 派生 + 文件夹消毒(unimplemented → RED)
|
|
5
8
|
// =====================================================================
|
|
@@ -111,18 +114,21 @@ fn owner_bind_refused_event_name_is_owner_bind_refused() {
|
|
|
111
114
|
// old uuid 为 None → old_uuid_prefix == ""(空串,非缺省);全 32 hex uuid 绝不出现在任何字段。
|
|
112
115
|
// unimplemented → RED。
|
|
113
116
|
#[test]
|
|
117
|
+
#[serial_test::serial(env)]
|
|
114
118
|
fn emit_owner_bound_event_logs_prefix_only_never_full_uuid() {
|
|
115
119
|
let _lock = ENV_LOCK.lock().unwrap();
|
|
116
|
-
let
|
|
117
|
-
|
|
118
|
-
let
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
("TEAM_AGENT_LEADER_PROVIDER", Some("codex")),
|
|
122
|
-
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
123
|
-
]);
|
|
120
|
+
let hermetic = hermetic::HermeticTestEnv::enter("owner-bound-uuid");
|
|
121
|
+
let ws = hermetic.workspace("emit");
|
|
122
|
+
let _pane = hermetic.with_env("TMUX_PANE", "%7");
|
|
123
|
+
let _fingerprint = hermetic.with_env("TEAM_AGENT_MACHINE_FINGERPRINT", "fp");
|
|
124
|
+
let _provider = hermetic.with_env("TEAM_AGENT_LEADER_PROVIDER", "codex");
|
|
124
125
|
let result = bind_owner_from_caller_pane(&ws, &TeamKey::new("default"), None).unwrap();
|
|
125
|
-
assert!(
|
|
126
|
+
assert!(
|
|
127
|
+
result.ok,
|
|
128
|
+
"caller pane must produce an owner binding; reason={:?} hint={:?}",
|
|
129
|
+
result.reason,
|
|
130
|
+
result.hint
|
|
131
|
+
);
|
|
126
132
|
let owner = result.owner.as_ref().expect("successful bind has owner");
|
|
127
133
|
let full = owner
|
|
128
134
|
.leader_session_uuid
|