@team-agent/installer 0.5.2 → 0.5.4
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 +3 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +20 -0
- package/crates/team-agent/src/cli/diagnose.rs +191 -31
- package/crates/team-agent/src/cli/emit.rs +5 -0
- package/crates/team-agent/src/cli/mod.rs +213 -89
- package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
- package/crates/team-agent/src/codex_app_server.rs +62 -1
- package/crates/team-agent/src/conpty/backend.rs +120 -8
- package/crates/team-agent/src/coordinator/backoff.rs +88 -2
- package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
- package/crates/team-agent/src/coordinator/health.rs +97 -11
- package/crates/team-agent/src/coordinator/mod.rs +8 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
- package/crates/team-agent/src/coordinator/tick.rs +13 -0
- package/crates/team-agent/src/diagnose/orphans.rs +18 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
- package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
- package/crates/team-agent/src/lib.rs +14 -1
- package/crates/team-agent/src/lifecycle/launch.rs +89 -92
- package/crates/team-agent/src/lifecycle/lock.rs +40 -33
- package/crates/team-agent/src/lifecycle/restart/agent.rs +25 -19
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -6
- package/crates/team-agent/src/lifecycle/restart/selection.rs +47 -18
- package/crates/team-agent/src/lifecycle/restart.rs +16 -6
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +287 -53
- package/crates/team-agent/src/lifecycle/tests/restart.rs +144 -5
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- package/crates/team-agent/src/mcp_server/wire.rs +6 -7
- package/crates/team-agent/src/messaging/tests/runtime.rs +5 -0
- package/crates/team-agent/src/packaging/tests.rs +41 -6
- package/crates/team-agent/src/packaging/types.rs +31 -3
- package/crates/team-agent/src/platform/argv.rs +324 -0
- package/crates/team-agent/src/platform/errors.rs +95 -0
- package/crates/team-agent/src/platform/file_lock.rs +418 -0
- package/crates/team-agent/src/platform/mod.rs +66 -0
- package/crates/team-agent/src/platform/process.rs +555 -0
- package/crates/team-agent/src/provider/adapter.rs +103 -41
- package/crates/team-agent/src/provider/session/capture.rs +328 -66
- package/crates/team-agent/src/provider/session/resume.rs +30 -28
- package/crates/team-agent/src/provider/session_scan/common.rs +117 -1
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +222 -25
- package/crates/team-agent/src/state/projection.rs +59 -4
- package/crates/team-agent/src/tmux_backend.rs +63 -13
- package/crates/team-agent/src/transport_factory.rs +124 -5
- package/package.json +4 -4
|
@@ -3,11 +3,11 @@ use std::path::{Path, PathBuf};
|
|
|
3
3
|
|
|
4
4
|
use serde_json::Value;
|
|
5
5
|
|
|
6
|
+
use crate::provider::wire::{is_claude_family, parse_provider};
|
|
6
7
|
use crate::provider::{
|
|
7
|
-
CapturedSession, CapturedSessionCandidate,
|
|
8
|
+
CaptureSessionContext, CapturedSession, CapturedSessionCandidate, Provider, ProviderAdapter,
|
|
8
9
|
ProviderError, SessionId,
|
|
9
10
|
};
|
|
10
|
-
use crate::provider::wire::{is_claude_family, parse_provider};
|
|
11
11
|
|
|
12
12
|
pub const SESSION_CAPTURE_CONVERGENCE_DEADLINE_MS: u64 = 12_000;
|
|
13
13
|
pub const SESSION_CAPTURE_CONVERGENCE_POLL_MS: u64 = 250;
|
|
@@ -20,6 +20,7 @@ pub struct CapturePassReport {
|
|
|
20
20
|
pub pending: Vec<String>,
|
|
21
21
|
pub assigned: Vec<String>,
|
|
22
22
|
pub ambiguous: Vec<AmbiguousSessionCapture>,
|
|
23
|
+
pub identity_mismatches: Vec<SessionIdentityMismatch>,
|
|
23
24
|
pub capture_failures: Vec<SessionCaptureFailure>,
|
|
24
25
|
pub candidate_count_by_agent: BTreeMap<String, usize>,
|
|
25
26
|
/// 0.4.6 Stage 3: agents that transitioned into the
|
|
@@ -41,6 +42,16 @@ pub struct AmbiguousSessionCapture {
|
|
|
41
42
|
pub spawn_cwd: String,
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
46
|
+
pub struct SessionIdentityMismatch {
|
|
47
|
+
pub agent_id: String,
|
|
48
|
+
pub expected_agent_id: String,
|
|
49
|
+
pub embedded_agent_id: String,
|
|
50
|
+
pub session_id: Option<String>,
|
|
51
|
+
pub rollout_path: Option<String>,
|
|
52
|
+
pub spawn_cwd: String,
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
/// 0.4.6 Stage 3: information emitted when a pending agent transitions
|
|
45
56
|
/// into the `transcript_missing` capture_state. Carries the diagnostic
|
|
46
57
|
/// fields the caller uses to write the
|
|
@@ -123,8 +134,9 @@ where
|
|
|
123
134
|
})?;
|
|
124
135
|
if missing.is_empty() {
|
|
125
136
|
if !report.ambiguous.is_empty() {
|
|
126
|
-
let final_report =
|
|
127
|
-
|
|
137
|
+
let final_report =
|
|
138
|
+
capture_missing_provider_sessions_once(state, adapter_for, true, timeout_s)
|
|
139
|
+
.map_err(|e| e.to_string())?;
|
|
128
140
|
changed |= final_report.changed;
|
|
129
141
|
}
|
|
130
142
|
return Ok(SessionConvergence {
|
|
@@ -209,10 +221,50 @@ where
|
|
|
209
221
|
..CapturePassReport::default()
|
|
210
222
|
};
|
|
211
223
|
for item in pending {
|
|
212
|
-
let Some(agent_obj) = agents
|
|
224
|
+
let Some(agent_obj) = agents
|
|
225
|
+
.get_mut(&item.agent_id)
|
|
226
|
+
.and_then(Value::as_object_mut)
|
|
227
|
+
else {
|
|
213
228
|
continue;
|
|
214
229
|
};
|
|
215
230
|
if let Some(candidate) = assignments.get(&item.agent_id) {
|
|
231
|
+
if let Some(embedded_agent_id) = candidate
|
|
232
|
+
.embedded_agent_id
|
|
233
|
+
.as_deref()
|
|
234
|
+
.filter(|embedded| *embedded != item.agent_id.as_str())
|
|
235
|
+
{
|
|
236
|
+
report.identity_mismatches.push(SessionIdentityMismatch {
|
|
237
|
+
agent_id: item.agent_id.clone(),
|
|
238
|
+
expected_agent_id: item.agent_id.clone(),
|
|
239
|
+
embedded_agent_id: embedded_agent_id.to_string(),
|
|
240
|
+
session_id: candidate
|
|
241
|
+
.captured
|
|
242
|
+
.session_id
|
|
243
|
+
.as_ref()
|
|
244
|
+
.map(|session| session.as_str().to_string()),
|
|
245
|
+
rollout_path: candidate
|
|
246
|
+
.captured
|
|
247
|
+
.rollout_path
|
|
248
|
+
.as_ref()
|
|
249
|
+
.map(|path| path.as_path().to_string_lossy().to_string()),
|
|
250
|
+
spawn_cwd: item.context.spawn_cwd.to_string_lossy().to_string(),
|
|
251
|
+
});
|
|
252
|
+
if finalize_ambiguous {
|
|
253
|
+
agent_obj.insert(
|
|
254
|
+
"capture_state".to_string(),
|
|
255
|
+
serde_json::json!("session_identity_mismatch"),
|
|
256
|
+
);
|
|
257
|
+
agent_obj.insert(
|
|
258
|
+
"session_identity_mismatch".to_string(),
|
|
259
|
+
serde_json::json!({
|
|
260
|
+
"expected_agent_id": item.agent_id,
|
|
261
|
+
"embedded_agent_id": embedded_agent_id,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
report.changed = true;
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
216
268
|
// Stage 1 (identity-boundary unified plan, architect direction
|
|
217
269
|
// 2026-06-23): defensive expected-id guard. The adapter scanner
|
|
218
270
|
// is the primary defence (Claude no longer falls back to same-
|
|
@@ -235,10 +287,7 @@ where
|
|
|
235
287
|
spawn_cwd: item.context.spawn_cwd.to_string_lossy().to_string(),
|
|
236
288
|
});
|
|
237
289
|
if finalize_ambiguous {
|
|
238
|
-
agent_obj.insert(
|
|
239
|
-
"attribution_ambiguous".to_string(),
|
|
240
|
-
serde_json::json!(true),
|
|
241
|
-
);
|
|
290
|
+
agent_obj.insert("attribution_ambiguous".to_string(), serde_json::json!(true));
|
|
242
291
|
// 0.4.6 tuple-atomic contract (audit:93): do NOT write
|
|
243
292
|
// `captured_at` for ambiguity diagnostics. `captured_at`
|
|
244
293
|
// is part of the authoritative tuple and may only be
|
|
@@ -308,19 +357,21 @@ where
|
|
|
308
357
|
// _pending_session_id (or _pending is absent, meaning no rebind in
|
|
309
358
|
// flight).
|
|
310
359
|
let pending_mismatch = match (
|
|
311
|
-
agent_obj
|
|
312
|
-
|
|
360
|
+
agent_obj
|
|
361
|
+
.get("_pending_session_id")
|
|
362
|
+
.and_then(Value::as_str)
|
|
363
|
+
.filter(|s| !s.is_empty()),
|
|
364
|
+
agent_obj
|
|
365
|
+
.get("session_id")
|
|
366
|
+
.and_then(Value::as_str)
|
|
367
|
+
.filter(|s| !s.is_empty()),
|
|
313
368
|
) {
|
|
314
369
|
(Some(pending), Some(current)) => pending != current,
|
|
315
370
|
_ => false,
|
|
316
371
|
};
|
|
317
372
|
if has_session && !pending_mismatch {
|
|
318
373
|
// Captured already — fix state field if drifted.
|
|
319
|
-
if agent_obj
|
|
320
|
-
.get("capture_state")
|
|
321
|
-
.and_then(Value::as_str)
|
|
322
|
-
!= Some("captured")
|
|
323
|
-
{
|
|
374
|
+
if agent_obj.get("capture_state").and_then(Value::as_str) != Some("captured") {
|
|
324
375
|
agent_obj.insert("capture_state".to_string(), serde_json::json!("captured"));
|
|
325
376
|
report.changed = true;
|
|
326
377
|
}
|
|
@@ -337,10 +388,7 @@ where
|
|
|
337
388
|
.and_then(Value::as_str)
|
|
338
389
|
.map(str::to_string);
|
|
339
390
|
if prev_state.as_deref() != Some(next_state) {
|
|
340
|
-
agent_obj.insert(
|
|
341
|
-
"capture_state".to_string(),
|
|
342
|
-
serde_json::json!(next_state),
|
|
343
|
-
);
|
|
391
|
+
agent_obj.insert("capture_state".to_string(), serde_json::json!(next_state));
|
|
344
392
|
report.changed = true;
|
|
345
393
|
if next_state == "transcript_missing" {
|
|
346
394
|
report.transcript_missing.push(TranscriptMissing {
|
|
@@ -439,7 +487,8 @@ pub fn incomplete_resumable_agent_ids(state: &Value) -> Vec<String> {
|
|
|
439
487
|
let mut out = agents
|
|
440
488
|
.iter()
|
|
441
489
|
.filter_map(|(agent_id, agent)| {
|
|
442
|
-
if pending_session_capture(agent_id, agent, &mut crate::provider::get_adapter).is_some()
|
|
490
|
+
if pending_session_capture(agent_id, agent, &mut crate::provider::get_adapter).is_some()
|
|
491
|
+
{
|
|
443
492
|
Some(agent_id.clone())
|
|
444
493
|
} else {
|
|
445
494
|
None
|
|
@@ -508,7 +557,10 @@ pub fn recover_resume_session_from_events(
|
|
|
508
557
|
// release-engineer via captured_via=event_log_repair). The check is
|
|
509
558
|
// a no-op for non-Claude providers.
|
|
510
559
|
if let Some(p) = provider {
|
|
511
|
-
if crate::provider::session_scan::rollout_path_has_claude_leader_marker(
|
|
560
|
+
if crate::provider::session_scan::rollout_path_has_claude_leader_marker(
|
|
561
|
+
p,
|
|
562
|
+
&rollout_path,
|
|
563
|
+
) {
|
|
512
564
|
continue;
|
|
513
565
|
}
|
|
514
566
|
}
|
|
@@ -874,19 +926,9 @@ fn allocate_session_candidates(
|
|
|
874
926
|
if assignments.contains_key(&item.agent_id) {
|
|
875
927
|
continue;
|
|
876
928
|
}
|
|
877
|
-
// P0 (lane-046-capture-gap): Claude
|
|
878
|
-
//
|
|
879
|
-
|
|
880
|
-
// Without this guard, a same-cwd leader transcript (no positive
|
|
881
|
-
// agent-id match, no path match) becomes the sole candidate via
|
|
882
|
-
// the time window and gets attributed to a worker. Only
|
|
883
|
-
// positive-agent-id / path-agent-id can authoritatively bind a
|
|
884
|
-
// Claude no-expected worker session.
|
|
885
|
-
let claude_no_expected = matches!(
|
|
886
|
-
item.provider,
|
|
887
|
-
Provider::Claude | Provider::ClaudeCode
|
|
888
|
-
) && item.context.expected_session_id.is_none();
|
|
889
|
-
if claude_no_expected {
|
|
929
|
+
// P0 (lane-046-capture-gap): Claude without a framework expected
|
|
930
|
+
// session id must not accept the weak `Any` fallback.
|
|
931
|
+
if provider_no_expected_requires_positive_attribution(item) {
|
|
890
932
|
if candidates_by_agent
|
|
891
933
|
.get(&item.agent_id)
|
|
892
934
|
.is_some_and(|candidates| !candidates.is_empty())
|
|
@@ -895,6 +937,18 @@ fn allocate_session_candidates(
|
|
|
895
937
|
}
|
|
896
938
|
continue;
|
|
897
939
|
}
|
|
940
|
+
// 0.5.x Codex cross-bind: a single provider-specific candidate may be
|
|
941
|
+
// captured when it has no conflicting embedded identity, but a same-cwd
|
|
942
|
+
// multi-candidate set must remain ambiguous instead of using weak
|
|
943
|
+
// sorted zip / Any attribution.
|
|
944
|
+
if codex_no_expected(item)
|
|
945
|
+
&& candidates_by_agent
|
|
946
|
+
.get(&item.agent_id)
|
|
947
|
+
.is_some_and(|candidates| candidates.len() > 1)
|
|
948
|
+
{
|
|
949
|
+
ambiguous.insert(item.agent_id.clone());
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
898
952
|
match unique_available_candidate(
|
|
899
953
|
candidates_by_agent.get(&item.agent_id),
|
|
900
954
|
claimed,
|
|
@@ -923,16 +977,14 @@ fn allocate_global_one_to_one(
|
|
|
923
977
|
claimed: &mut BTreeSet<String>,
|
|
924
978
|
assignments: &mut BTreeMap<String, CapturedSessionCandidate>,
|
|
925
979
|
) {
|
|
926
|
-
// P0 (lane-046-capture-gap
|
|
927
|
-
// global one-to-one weak allocator. They must
|
|
928
|
-
// PathAgentId
|
|
980
|
+
// P0 (lane-046-capture-gap + 0.5.x Codex cross-bind): exclude
|
|
981
|
+
// no-expected agents from the global one-to-one weak allocator. They must
|
|
982
|
+
// use PositiveAgentId, PathAgentId, or the per-agent unique-candidate path
|
|
983
|
+
// only.
|
|
929
984
|
let remaining_agents = pending
|
|
930
985
|
.iter()
|
|
931
986
|
.filter(|item| !assignments.contains_key(&item.agent_id))
|
|
932
|
-
.filter(|item|
|
|
933
|
-
!(matches!(item.provider, Provider::Claude | Provider::ClaudeCode)
|
|
934
|
-
&& item.context.expected_session_id.is_none())
|
|
935
|
-
})
|
|
987
|
+
.filter(|item| !provider_no_expected_disables_global_one_to_one(item))
|
|
936
988
|
.map(|item| item.agent_id.clone())
|
|
937
989
|
.collect::<Vec<_>>();
|
|
938
990
|
if remaining_agents.is_empty() {
|
|
@@ -963,6 +1015,22 @@ fn allocate_global_one_to_one(
|
|
|
963
1015
|
}
|
|
964
1016
|
}
|
|
965
1017
|
|
|
1018
|
+
fn provider_no_expected_requires_positive_attribution(item: &PendingSessionCapture) -> bool {
|
|
1019
|
+
matches!(item.provider, Provider::Claude | Provider::ClaudeCode)
|
|
1020
|
+
&& item.context.expected_session_id.is_none()
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
fn provider_no_expected_disables_global_one_to_one(item: &PendingSessionCapture) -> bool {
|
|
1024
|
+
matches!(
|
|
1025
|
+
item.provider,
|
|
1026
|
+
Provider::Codex | Provider::Claude | Provider::ClaudeCode
|
|
1027
|
+
) && item.context.expected_session_id.is_none()
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
fn codex_no_expected(item: &PendingSessionCapture) -> bool {
|
|
1031
|
+
matches!(item.provider, Provider::Codex) && item.context.expected_session_id.is_none()
|
|
1032
|
+
}
|
|
1033
|
+
|
|
966
1034
|
fn unique_available_candidate(
|
|
967
1035
|
candidates: Option<&Vec<CapturedSessionCandidate>>,
|
|
968
1036
|
claimed: &BTreeSet<String>,
|
|
@@ -992,7 +1060,10 @@ enum CandidateMatchKind {
|
|
|
992
1060
|
Any,
|
|
993
1061
|
}
|
|
994
1062
|
|
|
995
|
-
fn candidate_keys_collide(
|
|
1063
|
+
fn candidate_keys_collide(
|
|
1064
|
+
candidate: &CapturedSessionCandidate,
|
|
1065
|
+
claimed: &BTreeSet<String>,
|
|
1066
|
+
) -> bool {
|
|
996
1067
|
captured_provider_session_keys(&candidate.captured)
|
|
997
1068
|
.iter()
|
|
998
1069
|
.any(|key| claimed.contains(key))
|
|
@@ -1049,10 +1120,7 @@ fn apply_captured_session(
|
|
|
1049
1120
|
// agent_session_complete. Also stamp capture_state="captured" for
|
|
1050
1121
|
// diagnose/status observability.
|
|
1051
1122
|
agent_obj.remove("_pending_session_id");
|
|
1052
|
-
agent_obj.insert(
|
|
1053
|
-
"capture_state".to_string(),
|
|
1054
|
-
serde_json::json!("captured"),
|
|
1055
|
-
);
|
|
1123
|
+
agent_obj.insert("capture_state".to_string(), serde_json::json!("captured"));
|
|
1056
1124
|
true
|
|
1057
1125
|
}
|
|
1058
1126
|
|
|
@@ -1357,6 +1425,7 @@ mod u1_tests {
|
|
|
1357
1425
|
attribution_confidence: Confidence::High,
|
|
1358
1426
|
spawn_cwd: PathBuf::from("/tmp/u1-cwd"),
|
|
1359
1427
|
},
|
|
1428
|
+
embedded_agent_id: None,
|
|
1360
1429
|
positive_agent_id_match: false,
|
|
1361
1430
|
agent_path_match: false,
|
|
1362
1431
|
}
|
|
@@ -1371,6 +1440,7 @@ mod u1_tests {
|
|
|
1371
1440
|
attribution_confidence: Confidence::High,
|
|
1372
1441
|
spawn_cwd: PathBuf::from("/tmp/u1-cwd"),
|
|
1373
1442
|
},
|
|
1443
|
+
embedded_agent_id: None,
|
|
1374
1444
|
positive_agent_id_match: true,
|
|
1375
1445
|
agent_path_match: false,
|
|
1376
1446
|
}
|
|
@@ -1424,6 +1494,204 @@ mod u1_tests {
|
|
|
1424
1494
|
);
|
|
1425
1495
|
}
|
|
1426
1496
|
|
|
1497
|
+
#[test]
|
|
1498
|
+
fn codex_no_expected_same_cwd_candidates_without_identity_do_not_global_zip_assign() {
|
|
1499
|
+
let mut state = serde_json::json!({
|
|
1500
|
+
"agents": {
|
|
1501
|
+
"frontend": {
|
|
1502
|
+
"provider": "codex",
|
|
1503
|
+
"status": "running",
|
|
1504
|
+
"spawn_cwd": "/tmp/u1-cwd"
|
|
1505
|
+
},
|
|
1506
|
+
"ios-dev": {
|
|
1507
|
+
"provider": "codex",
|
|
1508
|
+
"status": "running",
|
|
1509
|
+
"spawn_cwd": "/tmp/u1-cwd"
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
let candidates = vec![
|
|
1514
|
+
leader_like_candidate(
|
|
1515
|
+
"019f3327-c35a-7023-b3cd-1bea93a7a157",
|
|
1516
|
+
"/Users/alauda/.codex/sessions/2026/07/06/rollout-frontend-poison.jsonl",
|
|
1517
|
+
),
|
|
1518
|
+
leader_like_candidate(
|
|
1519
|
+
"019f3327-d6d7-7c80-846a-0db4821714fb",
|
|
1520
|
+
"/Users/alauda/.codex/sessions/2026/07/06/rollout-ios-poison.jsonl",
|
|
1521
|
+
),
|
|
1522
|
+
];
|
|
1523
|
+
let mut canned = BTreeMap::new();
|
|
1524
|
+
canned.insert("frontend".to_string(), candidates.clone());
|
|
1525
|
+
canned.insert("ios-dev".to_string(), candidates);
|
|
1526
|
+
let canned_for_adapter = canned.clone();
|
|
1527
|
+
let mut adapter_for = move |provider| {
|
|
1528
|
+
Box::new(
|
|
1529
|
+
test_support::CaptureCandidatesAdapter::new(provider, None, "")
|
|
1530
|
+
.with_candidates(canned_for_adapter.clone()),
|
|
1531
|
+
) as Box<dyn ProviderAdapter>
|
|
1532
|
+
};
|
|
1533
|
+
let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
|
|
1534
|
+
.expect("capture pass succeeds");
|
|
1535
|
+
assert!(
|
|
1536
|
+
report.assigned.is_empty(),
|
|
1537
|
+
"Codex no-expected same-cwd candidates with no positive identity must stay ambiguous, not global one-to-one assigned; report={report:?}"
|
|
1538
|
+
);
|
|
1539
|
+
assert_eq!(
|
|
1540
|
+
report
|
|
1541
|
+
.ambiguous
|
|
1542
|
+
.iter()
|
|
1543
|
+
.map(|item| item.agent_id.as_str())
|
|
1544
|
+
.collect::<Vec<_>>(),
|
|
1545
|
+
vec!["frontend", "ios-dev"],
|
|
1546
|
+
"both workers should be marked ambiguous instead of receiving weak zipped tuples"
|
|
1547
|
+
);
|
|
1548
|
+
let agents = state["agents"].as_object().expect("agents object");
|
|
1549
|
+
assert!(agents["frontend"].get("session_id").is_none());
|
|
1550
|
+
assert!(agents["ios-dev"].get("session_id").is_none());
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
#[test]
|
|
1554
|
+
fn codex_no_expected_single_candidate_without_identity_still_captures() {
|
|
1555
|
+
let mut state = serde_json::json!({
|
|
1556
|
+
"agents": {
|
|
1557
|
+
"subgreeter": {
|
|
1558
|
+
"provider": "codex",
|
|
1559
|
+
"status": "running",
|
|
1560
|
+
"spawn_cwd": "/tmp/u1-cwd"
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
let mut canned = BTreeMap::new();
|
|
1565
|
+
canned.insert(
|
|
1566
|
+
"subgreeter".to_string(),
|
|
1567
|
+
vec![leader_like_candidate(
|
|
1568
|
+
"sess-sub-captured",
|
|
1569
|
+
"/Users/alauda/.codex/sessions/2026/07/06/rollout-sub.jsonl",
|
|
1570
|
+
)],
|
|
1571
|
+
);
|
|
1572
|
+
let canned_for_adapter = canned.clone();
|
|
1573
|
+
let mut adapter_for = move |provider| {
|
|
1574
|
+
Box::new(
|
|
1575
|
+
test_support::CaptureCandidatesAdapter::new(provider, None, "")
|
|
1576
|
+
.with_candidates(canned_for_adapter.clone()),
|
|
1577
|
+
) as Box<dyn ProviderAdapter>
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
|
|
1581
|
+
.expect("capture pass succeeds");
|
|
1582
|
+
|
|
1583
|
+
assert_eq!(report.assigned, vec!["subgreeter".to_string()]);
|
|
1584
|
+
assert!(report.ambiguous.is_empty(), "report={report:?}");
|
|
1585
|
+
assert_eq!(
|
|
1586
|
+
state["agents"]["subgreeter"]["session_id"].as_str(),
|
|
1587
|
+
Some("sess-sub-captured")
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
#[test]
|
|
1592
|
+
fn codex_embedded_identity_assigns_each_same_cwd_worker_to_own_rollout() {
|
|
1593
|
+
let mut state = serde_json::json!({
|
|
1594
|
+
"agents": {
|
|
1595
|
+
"frontend": {
|
|
1596
|
+
"provider": "codex",
|
|
1597
|
+
"status": "running",
|
|
1598
|
+
"spawn_cwd": "/tmp/u1-cwd"
|
|
1599
|
+
},
|
|
1600
|
+
"ios-dev": {
|
|
1601
|
+
"provider": "codex",
|
|
1602
|
+
"status": "running",
|
|
1603
|
+
"spawn_cwd": "/tmp/u1-cwd"
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
let frontend = CapturedSessionCandidate {
|
|
1608
|
+
embedded_agent_id: Some("frontend".to_string()),
|
|
1609
|
+
positive_agent_id_match: true,
|
|
1610
|
+
..leader_like_candidate(
|
|
1611
|
+
"019f3327-frontend",
|
|
1612
|
+
"/Users/alauda/.codex/sessions/2026/07/06/rollout-frontend.jsonl",
|
|
1613
|
+
)
|
|
1614
|
+
};
|
|
1615
|
+
let ios = CapturedSessionCandidate {
|
|
1616
|
+
embedded_agent_id: Some("ios-dev".to_string()),
|
|
1617
|
+
positive_agent_id_match: true,
|
|
1618
|
+
..leader_like_candidate(
|
|
1619
|
+
"019f3327-ios-dev",
|
|
1620
|
+
"/Users/alauda/.codex/sessions/2026/07/06/rollout-ios-dev.jsonl",
|
|
1621
|
+
)
|
|
1622
|
+
};
|
|
1623
|
+
let mut canned = BTreeMap::new();
|
|
1624
|
+
canned.insert("frontend".to_string(), vec![frontend]);
|
|
1625
|
+
canned.insert("ios-dev".to_string(), vec![ios]);
|
|
1626
|
+
let canned_for_adapter = canned.clone();
|
|
1627
|
+
let mut adapter_for = move |provider| {
|
|
1628
|
+
Box::new(
|
|
1629
|
+
test_support::CaptureCandidatesAdapter::new(provider, None, "")
|
|
1630
|
+
.with_candidates(canned_for_adapter.clone()),
|
|
1631
|
+
) as Box<dyn ProviderAdapter>
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1634
|
+
let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
|
|
1635
|
+
.expect("capture pass succeeds");
|
|
1636
|
+
|
|
1637
|
+
assert_eq!(
|
|
1638
|
+
report.assigned,
|
|
1639
|
+
vec!["frontend".to_string(), "ios-dev".to_string()]
|
|
1640
|
+
);
|
|
1641
|
+
assert!(report.ambiguous.is_empty());
|
|
1642
|
+
assert_eq!(
|
|
1643
|
+
state["agents"]["frontend"]["session_id"].as_str(),
|
|
1644
|
+
Some("019f3327-frontend")
|
|
1645
|
+
);
|
|
1646
|
+
assert_eq!(
|
|
1647
|
+
state["agents"]["ios-dev"]["session_id"].as_str(),
|
|
1648
|
+
Some("019f3327-ios-dev")
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
#[test]
|
|
1653
|
+
fn apply_time_identity_guard_refuses_mismatched_candidate() {
|
|
1654
|
+
let mut state = serde_json::json!({
|
|
1655
|
+
"agents": {
|
|
1656
|
+
"frontend": {
|
|
1657
|
+
"provider": "codex",
|
|
1658
|
+
"status": "running",
|
|
1659
|
+
"spawn_cwd": "/tmp/u1-cwd"
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
let candidate = CapturedSessionCandidate {
|
|
1664
|
+
embedded_agent_id: Some("ios-dev".to_string()),
|
|
1665
|
+
positive_agent_id_match: true,
|
|
1666
|
+
..leader_like_candidate(
|
|
1667
|
+
"019f3327-c35a-7023-b3cd-1bea93a7a157",
|
|
1668
|
+
"/Users/alauda/.codex/sessions/2026/07/06/rollout-ios-dev.jsonl",
|
|
1669
|
+
)
|
|
1670
|
+
};
|
|
1671
|
+
let mut canned = BTreeMap::new();
|
|
1672
|
+
canned.insert("frontend".to_string(), vec![candidate]);
|
|
1673
|
+
let canned_for_adapter = canned.clone();
|
|
1674
|
+
let mut adapter_for = move |provider| {
|
|
1675
|
+
Box::new(
|
|
1676
|
+
test_support::CaptureCandidatesAdapter::new(provider, None, "")
|
|
1677
|
+
.with_candidates(canned_for_adapter.clone()),
|
|
1678
|
+
) as Box<dyn ProviderAdapter>
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
|
|
1682
|
+
.expect("capture pass succeeds");
|
|
1683
|
+
|
|
1684
|
+
assert!(report.assigned.is_empty());
|
|
1685
|
+
assert_eq!(report.identity_mismatches.len(), 1);
|
|
1686
|
+
assert_eq!(report.identity_mismatches[0].agent_id, "frontend");
|
|
1687
|
+
assert_eq!(report.identity_mismatches[0].embedded_agent_id, "ios-dev");
|
|
1688
|
+
assert!(state["agents"]["frontend"].get("session_id").is_none());
|
|
1689
|
+
assert_eq!(
|
|
1690
|
+
state["agents"]["frontend"]["capture_state"].as_str(),
|
|
1691
|
+
Some("session_identity_mismatch")
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1427
1695
|
/// P0 RED-2: same shape but with a positive_agent_id_match candidate →
|
|
1428
1696
|
/// must capture (positive agent id is authoritative for Claude
|
|
1429
1697
|
/// no-expected).
|
|
@@ -1639,10 +1907,7 @@ mod u1_tests {
|
|
|
1639
1907
|
// only one left that honors framework-supplied --session-id). Switch
|
|
1640
1908
|
// this test to Copilot to preserve the pre-pass invariant coverage.
|
|
1641
1909
|
use crate::provider::{CaptureVia, Confidence, RolloutPath};
|
|
1642
|
-
let dir = std::env::temp_dir().join(format!(
|
|
1643
|
-
"ta-stage1-allocator-{}",
|
|
1644
|
-
std::process::id()
|
|
1645
|
-
));
|
|
1910
|
+
let dir = std::env::temp_dir().join(format!("ta-stage1-allocator-{}", std::process::id()));
|
|
1646
1911
|
std::fs::create_dir_all(&dir).unwrap();
|
|
1647
1912
|
let rollout_a = dir.join("9a2d1668.jsonl");
|
|
1648
1913
|
let rollout_b = dir.join("3e824e89.jsonl");
|
|
@@ -1650,27 +1915,25 @@ mod u1_tests {
|
|
|
1650
1915
|
std::fs::write(&rollout_b, b"{}\n").unwrap();
|
|
1651
1916
|
let candidate_a = CapturedSessionCandidate {
|
|
1652
1917
|
captured: CapturedSession {
|
|
1653
|
-
session_id: Some(SessionId::new(
|
|
1654
|
-
"9a2d1668-8987-4c36-8bde-a5135b10da02",
|
|
1655
|
-
)),
|
|
1918
|
+
session_id: Some(SessionId::new("9a2d1668-8987-4c36-8bde-a5135b10da02")),
|
|
1656
1919
|
rollout_path: Some(RolloutPath::new(rollout_a.clone())),
|
|
1657
1920
|
captured_via: CaptureVia::FsWatch,
|
|
1658
1921
|
attribution_confidence: Confidence::High,
|
|
1659
1922
|
spawn_cwd: dir.clone(),
|
|
1660
1923
|
},
|
|
1924
|
+
embedded_agent_id: None,
|
|
1661
1925
|
positive_agent_id_match: false,
|
|
1662
1926
|
agent_path_match: false,
|
|
1663
1927
|
};
|
|
1664
1928
|
let candidate_b = CapturedSessionCandidate {
|
|
1665
1929
|
captured: CapturedSession {
|
|
1666
|
-
session_id: Some(SessionId::new(
|
|
1667
|
-
"3e824e89-25ac-4b3f-b272-b4f733f6403c",
|
|
1668
|
-
)),
|
|
1930
|
+
session_id: Some(SessionId::new("3e824e89-25ac-4b3f-b272-b4f733f6403c")),
|
|
1669
1931
|
rollout_path: Some(RolloutPath::new(rollout_b.clone())),
|
|
1670
1932
|
captured_via: CaptureVia::FsWatch,
|
|
1671
1933
|
attribution_confidence: Confidence::High,
|
|
1672
1934
|
spawn_cwd: dir.clone(),
|
|
1673
1935
|
},
|
|
1936
|
+
embedded_agent_id: None,
|
|
1674
1937
|
positive_agent_id_match: false,
|
|
1675
1938
|
agent_path_match: false,
|
|
1676
1939
|
};
|
|
@@ -1698,9 +1961,8 @@ mod u1_tests {
|
|
|
1698
1961
|
});
|
|
1699
1962
|
let adapter = test_support::CaptureCandidatesAdapter::new(Provider::Copilot, None, "")
|
|
1700
1963
|
.with_candidates(candidates_by_agent);
|
|
1701
|
-
let mut adapter_for =
|
|
1702
|
-
Box::new(adapter.clone()) as Box<dyn ProviderAdapter
|
|
1703
|
-
};
|
|
1964
|
+
let mut adapter_for =
|
|
1965
|
+
move |_provider| Box::new(adapter.clone()) as Box<dyn ProviderAdapter>;
|
|
1704
1966
|
|
|
1705
1967
|
let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
|
|
1706
1968
|
.expect("allocator pass should succeed");
|
|
@@ -1762,7 +2024,8 @@ mod u1_tests {
|
|
|
1762
2024
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
1763
2025
|
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
1764
2026
|
let uniq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
1765
|
-
let workspace =
|
|
2027
|
+
let workspace =
|
|
2028
|
+
std::env::temp_dir().join(format!("ta-e57-recover-{}-{}", std::process::id(), uniq));
|
|
1766
2029
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
1767
2030
|
std::fs::create_dir_all(&workspace).expect("workspace");
|
|
1768
2031
|
|
|
@@ -1832,10 +2095,7 @@ mod u1_tests {
|
|
|
1832
2095
|
/// the stale rollout look authoritative.
|
|
1833
2096
|
#[test]
|
|
1834
2097
|
fn agent_session_complete_returns_false_on_pending_mismatch() {
|
|
1835
|
-
let dir = std::env::temp_dir().join(format!(
|
|
1836
|
-
"ta-pending-mismatch-{}",
|
|
1837
|
-
std::process::id()
|
|
1838
|
-
));
|
|
2098
|
+
let dir = std::env::temp_dir().join(format!("ta-pending-mismatch-{}", std::process::id()));
|
|
1839
2099
|
let _ = std::fs::create_dir_all(&dir);
|
|
1840
2100
|
let rollout = dir.join("old.jsonl");
|
|
1841
2101
|
std::fs::write(&rollout, "{\"type\":\"assistant\"}\n").unwrap();
|
|
@@ -1898,7 +2158,9 @@ mod u1_tests {
|
|
|
1898
2158
|
(agent={agent:?})"
|
|
1899
2159
|
);
|
|
1900
2160
|
assert_eq!(
|
|
1901
|
-
agent
|
|
2161
|
+
agent
|
|
2162
|
+
.get("capture_state")
|
|
2163
|
+
.and_then(serde_json::Value::as_str),
|
|
1902
2164
|
Some("captured"),
|
|
1903
2165
|
"S1-CAPTURE-001: capture_state must be stamped 'captured'"
|
|
1904
2166
|
);
|