@team-agent/installer 0.5.51 → 0.5.53
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 +18 -2
- package/crates/team-agent/src/cli/emit.rs +17 -0
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/named_address.rs +35 -67
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +11 -0
- package/crates/team-agent/src/diagnose/orphans.rs +24 -3
- package/crates/team-agent/src/kill_audit.rs +67 -0
- package/crates/team-agent/src/leader/lease.rs +526 -73
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
- package/crates/team-agent/src/leader/rediscover.rs +6 -0
- package/crates/team-agent/src/leader/start.rs +144 -23
- package/crates/team-agent/src/leader/tests/idle.rs +3 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +12 -1
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -8
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart/agent.rs +20 -4
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +113 -4
- package/crates/team-agent/src/lifecycle/restart/remove.rs +73 -10
- package/crates/team-agent/src/lifecycle/restart.rs +25 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +441 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -2
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
- package/crates/team-agent/src/lifecycle/types.rs +17 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +18 -4
- package/crates/team-agent/src/mcp_server/types.rs +3 -0
- package/crates/team-agent/src/mcp_server/wire.rs +30 -7
- package/crates/team-agent/src/messaging/delivery.rs +126 -147
- package/crates/team-agent/src/messaging/leader_channel.rs +198 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
- package/crates/team-agent/src/messaging/mod.rs +5 -0
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
- package/crates/team-agent/src/provider/adapter.rs +54 -13
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/session/capture.rs +321 -52
- package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
- package/crates/team-agent/src/provider/session/mod.rs +6 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
- package/crates/team-agent/src/provider/session_scan/common.rs +39 -6
- package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
- package/crates/team-agent/src/provider/session_scan.rs +3 -3
- package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
- package/crates/team-agent/src/provider/tests.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +282 -20
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository.rs +36 -12
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +8 -3
|
@@ -335,7 +335,7 @@ where
|
|
|
335
335
|
// * `transcript_missing` — trigger event has fired (report_result /
|
|
336
336
|
// first_send_at / pane_output_advanced) AND elapsed > threshold
|
|
337
337
|
// AND zero candidates / no backing
|
|
338
|
-
// * `
|
|
338
|
+
// * `pending_first_turn` — no verified backing yet, still in grace
|
|
339
339
|
// The state field surfaces in status/diagnose without changing the
|
|
340
340
|
// authoritative tuple contract (we never write session_id without
|
|
341
341
|
// backing). Throttled event emission is done by the caller (tick).
|
|
@@ -415,11 +415,11 @@ where
|
|
|
415
415
|
/// 0.4.6 Stage 3: classify a pending agent's capture state into one of:
|
|
416
416
|
/// * `transcript_missing` — has had a strong trigger event AND elapsed
|
|
417
417
|
/// beyond grace threshold AND still no backing
|
|
418
|
-
/// * `
|
|
418
|
+
/// * `pending_first_turn` — still in grace period or no trigger yet
|
|
419
419
|
///
|
|
420
420
|
/// Trigger events (CR M2): any of `first_send_at`, `last_result_at`, or a
|
|
421
421
|
/// recent `last_pane_output_at` indicates the worker has been interactively
|
|
422
|
-
/// used / done something. Without ANY trigger, we stay
|
|
422
|
+
/// used / done something. Without ANY trigger, we stay pending
|
|
423
423
|
/// even past the grace window (silent worker is not a failure to capture).
|
|
424
424
|
///
|
|
425
425
|
/// Threshold: 30 seconds from spawned_at OR 15 seconds from the first
|
|
@@ -477,7 +477,7 @@ fn classify_pending_capture_state(
|
|
|
477
477
|
if has_trigger && past_spawn_grace && past_trigger_grace && candidate_count == 0 {
|
|
478
478
|
"transcript_missing"
|
|
479
479
|
} else {
|
|
480
|
-
"
|
|
480
|
+
"pending_first_turn"
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
483
|
|
|
@@ -488,8 +488,7 @@ pub fn incomplete_resumable_agent_ids(state: &Value) -> Vec<String> {
|
|
|
488
488
|
let mut out = agents
|
|
489
489
|
.iter()
|
|
490
490
|
.filter_map(|(agent_id, agent)| {
|
|
491
|
-
if
|
|
492
|
-
{
|
|
491
|
+
if incomplete_resumable_provider(agent, &mut crate::provider::get_adapter).is_some() {
|
|
493
492
|
Some(agent_id.clone())
|
|
494
493
|
} else {
|
|
495
494
|
None
|
|
@@ -651,27 +650,20 @@ fn pending_session_capture<F>(
|
|
|
651
650
|
where
|
|
652
651
|
F: FnMut(Provider) -> Box<dyn ProviderAdapter>,
|
|
653
652
|
{
|
|
654
|
-
|
|
655
|
-
.get("status")
|
|
656
|
-
.and_then(Value::as_str)
|
|
657
|
-
.is_some_and(|status| status != "running")
|
|
658
|
-
{
|
|
659
|
-
return None;
|
|
660
|
-
}
|
|
661
|
-
if agent_session_complete(agent) {
|
|
662
|
-
return None;
|
|
663
|
-
}
|
|
664
|
-
let provider = agent
|
|
665
|
-
.get("provider")
|
|
666
|
-
.and_then(Value::as_str)
|
|
667
|
-
.and_then(parse_provider)?;
|
|
653
|
+
let provider = incomplete_resumable_provider(agent, adapter_for)?;
|
|
668
654
|
let spawn_cwd = agent
|
|
669
655
|
.get("spawn_cwd")
|
|
670
656
|
.and_then(Value::as_str)
|
|
671
657
|
.filter(|cwd| !cwd.is_empty())?;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
658
|
+
// New cohorts carry a boundary and are filtered fail-closed by the
|
|
659
|
+
// canonical scanner. Legacy rows may predate spawned_at; keep their
|
|
660
|
+
// boundary absent so they can still use the allocator's identity,
|
|
661
|
+
// collision, and unique-candidate guards instead of being skipped.
|
|
662
|
+
let spawned_at = agent
|
|
663
|
+
.get("spawned_at")
|
|
664
|
+
.and_then(Value::as_str)
|
|
665
|
+
.filter(|value| !value.is_empty())
|
|
666
|
+
.map(str::to_string);
|
|
675
667
|
Some(PendingSessionCapture {
|
|
676
668
|
agent_id: agent_id.to_string(),
|
|
677
669
|
provider,
|
|
@@ -694,25 +686,7 @@ where
|
|
|
694
686
|
.get("pane_pid")
|
|
695
687
|
.and_then(Value::as_u64)
|
|
696
688
|
.and_then(|pid| u32::try_from(pid).ok()),
|
|
697
|
-
spawned_at
|
|
698
|
-
.get("spawned_at")
|
|
699
|
-
.and_then(Value::as_str)
|
|
700
|
-
.filter(|value| !value.is_empty())
|
|
701
|
-
.map(str::to_string),
|
|
702
|
-
// 0.3.31 Codex capture correction: Codex does NOT honor
|
|
703
|
-
// `--session-id`, so any `_pending_session_id` stored for a Codex
|
|
704
|
-
// agent (stale 0.3.30 state, or the framework's pre-spawn token)
|
|
705
|
-
// is a local-only token and must NOT be used as expected_session_id
|
|
706
|
-
// — that would trigger the Stage 1 mismatch guard against Codex's
|
|
707
|
-
// real session_meta.payload.id and permanently reject the correct
|
|
708
|
-
// transcript. Codex capture anchors purely on (cwd, spawned_at).
|
|
709
|
-
// 0.4.7 (B1 verified, partial-resume revert of 9feafc31):
|
|
710
|
-
// Claude --session-id was restored in adapter.rs build_command_plan
|
|
711
|
-
// because Claude ≥ 2.1.185 DOES honour framework-supplied session
|
|
712
|
-
// id and DOES create a transcript at that id. So `_pending_session_id`
|
|
713
|
-
// is once again valid for Claude — re-enable the Stage 1 pre-pass
|
|
714
|
-
// for Claude/ClaudeCode. Codex remains excluded (Codex CLI ignores
|
|
715
|
-
// --session-id, capture must anchor purely on cwd+spawned_at).
|
|
689
|
+
spawned_at,
|
|
716
690
|
expected_session_id: if matches!(provider, Provider::Codex) {
|
|
717
691
|
None
|
|
718
692
|
} else {
|
|
@@ -731,6 +705,34 @@ where
|
|
|
731
705
|
})
|
|
732
706
|
}
|
|
733
707
|
|
|
708
|
+
fn incomplete_resumable_provider<F>(agent: &Value, adapter_for: &mut F) -> Option<Provider>
|
|
709
|
+
where
|
|
710
|
+
F: FnMut(Provider) -> Box<dyn ProviderAdapter>,
|
|
711
|
+
{
|
|
712
|
+
if agent
|
|
713
|
+
.get("status")
|
|
714
|
+
.and_then(Value::as_str)
|
|
715
|
+
.is_some_and(|status| status != "running")
|
|
716
|
+
{
|
|
717
|
+
return None;
|
|
718
|
+
}
|
|
719
|
+
if agent_session_complete(agent) {
|
|
720
|
+
return None;
|
|
721
|
+
}
|
|
722
|
+
let provider = agent
|
|
723
|
+
.get("provider")
|
|
724
|
+
.and_then(Value::as_str)
|
|
725
|
+
.and_then(parse_provider)?;
|
|
726
|
+
agent
|
|
727
|
+
.get("spawn_cwd")
|
|
728
|
+
.and_then(Value::as_str)
|
|
729
|
+
.filter(|cwd| !cwd.is_empty())?;
|
|
730
|
+
if !adapter_for(provider).caps().resume {
|
|
731
|
+
return None;
|
|
732
|
+
}
|
|
733
|
+
Some(provider)
|
|
734
|
+
}
|
|
735
|
+
|
|
734
736
|
fn agent_session_complete(agent: &Value) -> bool {
|
|
735
737
|
// RM-039-SESS-001 step 1 (architect verdict 2026-06-22): historically
|
|
736
738
|
// this returned true the moment `rollout_path` was non-empty — even when
|
|
@@ -1150,13 +1152,26 @@ fn claimed_provider_session_keys(
|
|
|
1150
1152
|
) -> BTreeSet<String> {
|
|
1151
1153
|
let mut keys = BTreeSet::new();
|
|
1152
1154
|
let team_key = crate::state::projection::team_state_key(state);
|
|
1153
|
-
// 1.
|
|
1155
|
+
// 1. Every other already-known worker session. The pending row itself is
|
|
1156
|
+
// excluded so an incomplete tuple can still be repaired in place.
|
|
1154
1157
|
for (agent_id, agent) in agents {
|
|
1155
1158
|
if pending_ids.contains(agent_id) {
|
|
1156
1159
|
continue;
|
|
1157
1160
|
}
|
|
1158
1161
|
push_provider_session_keys(&mut keys, &team_key, agent_id, agent);
|
|
1159
1162
|
}
|
|
1163
|
+
if let Some(teams) = state.get("teams").and_then(Value::as_object) {
|
|
1164
|
+
for (nested_team_key, team_state) in teams {
|
|
1165
|
+
if let Some(nested_agents) = team_state.get("agents").and_then(Value::as_object) {
|
|
1166
|
+
for (agent_id, agent) in nested_agents {
|
|
1167
|
+
if nested_team_key == &team_key && pending_ids.contains(agent_id) {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
push_provider_session_keys(&mut keys, nested_team_key, agent_id, agent);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1160
1175
|
// 2. P0 (lane-046-capture-gap): leader anchor sessions. The leader's
|
|
1161
1176
|
// own provider transcript must never be attributed to a worker. Scan
|
|
1162
1177
|
// state.leader_receiver, state.team_owner, and the same fields under
|
|
@@ -1207,6 +1222,12 @@ fn push_provider_session_keys(
|
|
|
1207
1222
|
keys.insert(global_session_attribution_key_string(session_id));
|
|
1208
1223
|
}
|
|
1209
1224
|
}
|
|
1225
|
+
// Copilot stores every session in one shared session-store.db. Session id
|
|
1226
|
+
// is the attribution identity; treating the shared DB path as globally
|
|
1227
|
+
// exclusive makes an exact expected id collide with every sibling.
|
|
1228
|
+
if matches!(provider, Provider::Copilot) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1210
1231
|
for field in ["rollout_path", "transcript_path"] {
|
|
1211
1232
|
if let Some(path) = value
|
|
1212
1233
|
.get(field)
|
|
@@ -1246,15 +1267,17 @@ fn captured_provider_session_keys(
|
|
|
1246
1267
|
}
|
|
1247
1268
|
keys.insert(global_session_attribution_key_string(session_id.as_str()));
|
|
1248
1269
|
}
|
|
1249
|
-
if
|
|
1250
|
-
let
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1270
|
+
if !matches!(owner.provider, Provider::Copilot) {
|
|
1271
|
+
if let Some(rollout_path) = &captured.rollout_path {
|
|
1272
|
+
let path = rollout_path.as_path().to_string_lossy();
|
|
1273
|
+
keys.insert(transcript_attribution_key_string(
|
|
1274
|
+
owner.provider,
|
|
1275
|
+
team_key,
|
|
1276
|
+
owner.agent_id.as_str(),
|
|
1277
|
+
&path,
|
|
1278
|
+
));
|
|
1279
|
+
keys.insert(global_transcript_attribution_key_string(&path));
|
|
1280
|
+
}
|
|
1258
1281
|
}
|
|
1259
1282
|
keys
|
|
1260
1283
|
}
|
|
@@ -1869,6 +1892,187 @@ mod u1_tests {
|
|
|
1869
1892
|
);
|
|
1870
1893
|
}
|
|
1871
1894
|
|
|
1895
|
+
#[test]
|
|
1896
|
+
fn pending_capture_state_stays_pending_until_a_trigger_expires() {
|
|
1897
|
+
let old = (chrono::Utc::now() - chrono::Duration::seconds(60)).to_rfc3339();
|
|
1898
|
+
let mut agent = serde_json::Map::new();
|
|
1899
|
+
agent.insert("spawned_at".to_string(), serde_json::json!(old));
|
|
1900
|
+
assert_eq!(
|
|
1901
|
+
super::classify_pending_capture_state(&agent, 0),
|
|
1902
|
+
"pending_first_turn"
|
|
1903
|
+
);
|
|
1904
|
+
|
|
1905
|
+
let trigger = (chrono::Utc::now() - chrono::Duration::seconds(20)).to_rfc3339();
|
|
1906
|
+
agent.insert("first_send_at".to_string(), serde_json::json!(trigger));
|
|
1907
|
+
assert_eq!(
|
|
1908
|
+
super::classify_pending_capture_state(&agent, 0),
|
|
1909
|
+
"transcript_missing"
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
#[test]
|
|
1914
|
+
fn canonical_pending_capture_preserves_legacy_missing_boundary() {
|
|
1915
|
+
let agent = serde_json::json!({
|
|
1916
|
+
"provider": "codex",
|
|
1917
|
+
"status": "running",
|
|
1918
|
+
"spawn_cwd": "/tmp/u1-cwd",
|
|
1919
|
+
"capture_state": "pending_first_turn"
|
|
1920
|
+
});
|
|
1921
|
+
let mut adapter_for = crate::provider::get_adapter;
|
|
1922
|
+
let pending = pending_session_capture("clone", &agent, &mut adapter_for)
|
|
1923
|
+
.expect("legacy row still participates in canonical capture");
|
|
1924
|
+
assert_eq!(pending.context.spawned_at, None);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
#[test]
|
|
1928
|
+
fn canonical_delayed_capture_rejects_prior_round_codex_rollout() {
|
|
1929
|
+
let dir =
|
|
1930
|
+
std::env::temp_dir().join(format!("ta-delayed-stale-codex-{}", std::process::id()));
|
|
1931
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
1932
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
1933
|
+
let stale =
|
|
1934
|
+
dir.join("rollout-2026-07-21T23-18-28-019f8541-c986-7f50-b9e6-6503c89df98c.jsonl");
|
|
1935
|
+
std::fs::write(
|
|
1936
|
+
&stale,
|
|
1937
|
+
format!(
|
|
1938
|
+
"{{\"session_meta\":{{\"payload\":{{\"id\":\"019f8541-c986-7f50-b9e6-6503c89df98c\",\"cwd\":\"{}\",\"created_at\":\"2026-07-21T23:18:28+00:00\"}}}}}}\n",
|
|
1939
|
+
dir.to_string_lossy()
|
|
1940
|
+
),
|
|
1941
|
+
)
|
|
1942
|
+
.unwrap();
|
|
1943
|
+
let mut state = serde_json::json!({
|
|
1944
|
+
"agents": {
|
|
1945
|
+
"clone": {
|
|
1946
|
+
"provider": "codex",
|
|
1947
|
+
"status": "running",
|
|
1948
|
+
"spawn_cwd": dir.to_string_lossy(),
|
|
1949
|
+
"spawned_at": "2026-07-22T02:12:27+00:00",
|
|
1950
|
+
"capture_state": "pending_first_turn"
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
let mut adapter_for = crate::provider::get_adapter;
|
|
1955
|
+
|
|
1956
|
+
let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
|
|
1957
|
+
.expect("delayed capture pass");
|
|
1958
|
+
|
|
1959
|
+
assert!(report.assigned.is_empty());
|
|
1960
|
+
assert!(state["agents"]["clone"]["session_id"].is_null());
|
|
1961
|
+
assert_ne!(
|
|
1962
|
+
state["agents"]["clone"]["capture_state"],
|
|
1963
|
+
serde_json::json!("captured")
|
|
1964
|
+
);
|
|
1965
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
#[test]
|
|
1969
|
+
fn canonical_delayed_capture_ignores_active_foreign_cwd_rollouts() {
|
|
1970
|
+
use std::io::Write;
|
|
1971
|
+
|
|
1972
|
+
let root =
|
|
1973
|
+
std::env::temp_dir().join(format!("ta-codex-dirty-domain-{}", std::process::id()));
|
|
1974
|
+
let _ = std::fs::remove_dir_all(&root);
|
|
1975
|
+
let current_cwd = root.join("current");
|
|
1976
|
+
let foreign_cwd = root.join("foreign");
|
|
1977
|
+
std::fs::create_dir_all(¤t_cwd).unwrap();
|
|
1978
|
+
std::fs::create_dir_all(&foreign_cwd).unwrap();
|
|
1979
|
+
let stale_current = current_cwd.join(
|
|
1980
|
+
"cache/current/rollout-2026-07-21T23-18-28-019f8541-c986-7f50-b9e6-6503c89df98c.jsonl",
|
|
1981
|
+
);
|
|
1982
|
+
let fresh_foreign = current_cwd.join(
|
|
1983
|
+
"cache/foreign/rollout-2026-07-22T04-01-00-019f8644-7060-7000-8000-000000000001.jsonl",
|
|
1984
|
+
);
|
|
1985
|
+
let fresh_current = current_cwd.join(
|
|
1986
|
+
"cache/current/rollout-2026-07-22T04-01-01-019f8644-7450-7000-8000-000000000002.jsonl",
|
|
1987
|
+
);
|
|
1988
|
+
let write_rollout = |path: &std::path::Path, cwd: &std::path::Path, id: &str| {
|
|
1989
|
+
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
1990
|
+
std::fs::write(
|
|
1991
|
+
path,
|
|
1992
|
+
format!(
|
|
1993
|
+
"{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"{}\"}}}}\n",
|
|
1994
|
+
cwd.to_string_lossy()
|
|
1995
|
+
),
|
|
1996
|
+
)
|
|
1997
|
+
.unwrap();
|
|
1998
|
+
};
|
|
1999
|
+
write_rollout(&stale_current, ¤t_cwd, "stale-current");
|
|
2000
|
+
write_rollout(&fresh_foreign, &foreign_cwd, "fresh-foreign");
|
|
2001
|
+
write_rollout(&fresh_current, ¤t_cwd, "fresh-current");
|
|
2002
|
+
std::fs::OpenOptions::new()
|
|
2003
|
+
.append(true)
|
|
2004
|
+
.open(&stale_current)
|
|
2005
|
+
.unwrap()
|
|
2006
|
+
.write_all(b"{\"type\":\"event_msg\"}\n")
|
|
2007
|
+
.unwrap();
|
|
2008
|
+
let mut state = serde_json::json!({
|
|
2009
|
+
"agents": {"clone": {
|
|
2010
|
+
"provider": "codex", "status": "running",
|
|
2011
|
+
"spawn_cwd": current_cwd.to_string_lossy(),
|
|
2012
|
+
"spawned_at": "2026-07-21T20:00:00+00:00",
|
|
2013
|
+
"capture_state": "pending_first_turn"
|
|
2014
|
+
}}
|
|
2015
|
+
});
|
|
2016
|
+
let mut adapter_for = crate::provider::get_adapter;
|
|
2017
|
+
|
|
2018
|
+
let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
|
|
2019
|
+
.expect("canonical delayed capture");
|
|
2020
|
+
|
|
2021
|
+
assert_eq!(report.candidate_count_by_agent.get("clone"), Some(&1));
|
|
2022
|
+
assert_eq!(state["agents"]["clone"]["session_id"], "fresh-current");
|
|
2023
|
+
assert_eq!(state["agents"]["clone"]["capture_state"], "captured");
|
|
2024
|
+
let _ = std::fs::remove_dir_all(&root);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
#[test]
|
|
2028
|
+
fn existing_team_session_is_rejected_while_fresh_candidate_is_accepted() {
|
|
2029
|
+
let mut state = serde_json::json!({
|
|
2030
|
+
"session_name": "current",
|
|
2031
|
+
"agents": {
|
|
2032
|
+
"clone": {
|
|
2033
|
+
"provider": "codex",
|
|
2034
|
+
"status": "running",
|
|
2035
|
+
"spawn_cwd": "/tmp/u1-cwd"
|
|
2036
|
+
}
|
|
2037
|
+
},
|
|
2038
|
+
"teams": {
|
|
2039
|
+
"archived": {
|
|
2040
|
+
"agents": {
|
|
2041
|
+
"old-seat": {
|
|
2042
|
+
"provider": "codex",
|
|
2043
|
+
"session_id": "stale-session",
|
|
2044
|
+
"rollout_path": "/tmp/stale.jsonl"
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
let mut canned = BTreeMap::new();
|
|
2051
|
+
canned.insert(
|
|
2052
|
+
"clone".to_string(),
|
|
2053
|
+
vec![
|
|
2054
|
+
worker_owned_candidate("stale-session", "/tmp/stale.jsonl"),
|
|
2055
|
+
worker_owned_candidate("fresh-session", "/tmp/fresh.jsonl"),
|
|
2056
|
+
],
|
|
2057
|
+
);
|
|
2058
|
+
let canned_for_adapter = canned.clone();
|
|
2059
|
+
let mut adapter_for = move |provider| {
|
|
2060
|
+
Box::new(
|
|
2061
|
+
test_support::CaptureCandidatesAdapter::new(provider, None, "")
|
|
2062
|
+
.with_candidates(canned_for_adapter.clone()),
|
|
2063
|
+
) as Box<dyn ProviderAdapter>
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
|
|
2067
|
+
.expect("capture pass succeeds");
|
|
2068
|
+
|
|
2069
|
+
assert_eq!(report.assigned, vec!["clone".to_string()]);
|
|
2070
|
+
assert_eq!(
|
|
2071
|
+
state["agents"]["clone"]["session_id"].as_str(),
|
|
2072
|
+
Some("fresh-session")
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1872
2076
|
#[test]
|
|
1873
2077
|
fn capture_pass_keeps_pending_agent_when_one_adapter_capture_fails() {
|
|
1874
2078
|
let mut state = serde_json::json!({
|
|
@@ -2102,6 +2306,71 @@ mod u1_tests {
|
|
|
2102
2306
|
let _ = std::fs::remove_dir(&dir);
|
|
2103
2307
|
}
|
|
2104
2308
|
|
|
2309
|
+
#[test]
|
|
2310
|
+
fn copilot_exact_session_id_does_not_collide_on_shared_store_path() {
|
|
2311
|
+
use crate::provider::{CaptureVia, Confidence, RolloutPath};
|
|
2312
|
+
let dir =
|
|
2313
|
+
std::env::temp_dir().join(format!("ta-copilot-shared-store-{}", std::process::id()));
|
|
2314
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
2315
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
2316
|
+
let store = dir.join("session-store.db");
|
|
2317
|
+
std::fs::write(&store, b"fixture").unwrap();
|
|
2318
|
+
let expected = "22222222-2222-4222-8222-222222222222";
|
|
2319
|
+
let candidate = CapturedSessionCandidate {
|
|
2320
|
+
captured: CapturedSession {
|
|
2321
|
+
session_id: Some(SessionId::new(expected)),
|
|
2322
|
+
rollout_path: Some(RolloutPath::new(store.clone())),
|
|
2323
|
+
captured_via: CaptureVia::FsWatch,
|
|
2324
|
+
attribution_confidence: Confidence::High,
|
|
2325
|
+
spawn_cwd: dir.clone(),
|
|
2326
|
+
},
|
|
2327
|
+
embedded_agent_id: None,
|
|
2328
|
+
positive_agent_id_match: false,
|
|
2329
|
+
agent_path_match: false,
|
|
2330
|
+
};
|
|
2331
|
+
let mut by_agent = BTreeMap::new();
|
|
2332
|
+
by_agent.insert("clone".to_string(), vec![candidate]);
|
|
2333
|
+
let mut state = serde_json::json!({
|
|
2334
|
+
"agents": {
|
|
2335
|
+
"source": {
|
|
2336
|
+
"provider": "copilot",
|
|
2337
|
+
"status": "running",
|
|
2338
|
+
"spawn_cwd": dir.to_string_lossy(),
|
|
2339
|
+
"session_id": "11111111-1111-4111-8111-111111111111",
|
|
2340
|
+
"rollout_path": store.to_string_lossy(),
|
|
2341
|
+
"captured_at": "2026-07-22T01:00:00+00:00",
|
|
2342
|
+
"captured_via": "fs_watch"
|
|
2343
|
+
},
|
|
2344
|
+
"clone": {
|
|
2345
|
+
"provider": "copilot",
|
|
2346
|
+
"status": "running",
|
|
2347
|
+
"spawn_cwd": dir.to_string_lossy(),
|
|
2348
|
+
"spawned_at": "2026-07-22T02:00:00+00:00",
|
|
2349
|
+
"_pending_session_id": expected
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
let adapter = test_support::CaptureCandidatesAdapter::new(Provider::Copilot, None, "")
|
|
2354
|
+
.with_candidates(by_agent);
|
|
2355
|
+
let mut adapter_for =
|
|
2356
|
+
move |_provider| Box::new(adapter.clone()) as Box<dyn ProviderAdapter>;
|
|
2357
|
+
|
|
2358
|
+
let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
|
|
2359
|
+
.expect("capture shared Copilot store");
|
|
2360
|
+
|
|
2361
|
+
assert_eq!(report.assigned, vec!["clone".to_string()]);
|
|
2362
|
+
assert_eq!(state["agents"]["clone"]["session_id"], expected);
|
|
2363
|
+
assert_eq!(
|
|
2364
|
+
state["agents"]["clone"]["rollout_path"],
|
|
2365
|
+
store.to_string_lossy().as_ref()
|
|
2366
|
+
);
|
|
2367
|
+
assert_eq!(
|
|
2368
|
+
state["agents"]["source"]["session_id"],
|
|
2369
|
+
"11111111-1111-4111-8111-111111111111"
|
|
2370
|
+
);
|
|
2371
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2105
2374
|
/// E57 RED postflight (lane-046-capture-gap): `recover_resume_session_from_events`
|
|
2106
2375
|
/// must refuse to repair a worker's session from a stale `session.captured`
|
|
2107
2376
|
/// event whose rollout file is a Claude LEADER transcript
|