@team-agent/installer 0.5.3 → 0.5.5

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.
Files changed (31) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +9 -1
  4. package/crates/team-agent/src/cli/diagnose.rs +191 -31
  5. package/crates/team-agent/src/cli/mod.rs +93 -34
  6. package/crates/team-agent/src/cli/send.rs +69 -2
  7. package/crates/team-agent/src/cli/status_port.rs +62 -0
  8. package/crates/team-agent/src/cli/tests/status_send.rs +12 -19
  9. package/crates/team-agent/src/coordinator/tick.rs +13 -0
  10. package/crates/team-agent/src/lifecycle/launch.rs +14 -6
  11. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -1
  12. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -2
  13. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -6
  14. package/crates/team-agent/src/lifecycle/restart/selection.rs +47 -18
  15. package/crates/team-agent/src/lifecycle/restart.rs +16 -6
  16. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +287 -53
  17. package/crates/team-agent/src/lifecycle/tests/restart.rs +144 -5
  18. package/crates/team-agent/src/lifecycle/types.rs +1 -0
  19. package/crates/team-agent/src/messaging/delivery.rs +209 -30
  20. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -25
  21. package/crates/team-agent/src/messaging/tests/runtime.rs +446 -0
  22. package/crates/team-agent/src/messaging/watchers.rs +36 -19
  23. package/crates/team-agent/src/provider/adapter.rs +103 -41
  24. package/crates/team-agent/src/provider/session/capture.rs +328 -66
  25. package/crates/team-agent/src/provider/session/resume.rs +30 -28
  26. package/crates/team-agent/src/provider/session_scan/common.rs +117 -1
  27. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  28. package/crates/team-agent/src/provider/session_scan.rs +1 -0
  29. package/crates/team-agent/src/state/persist.rs +17 -0
  30. package/crates/team-agent/src/state/projection.rs +59 -4
  31. 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, CaptureSessionContext, Provider, ProviderAdapter,
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 = capture_missing_provider_sessions_once(state, adapter_for, true, timeout_s)
127
- .map_err(|e| e.to_string())?;
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.get_mut(&item.agent_id).and_then(Value::as_object_mut) else {
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.get("_pending_session_id").and_then(Value::as_str).filter(|s| !s.is_empty()),
312
- agent_obj.get("session_id").and_then(Value::as_str).filter(|s| !s.is_empty()),
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(p, &rollout_path) {
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/ClaudeCode with no
878
- // expected_session_id (natural fresh Stage 1 expected-id miss
879
- // guard cannot fire) must NOT accept the weak `Any` fallback.
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): exclude Claude no-expected agents from the
927
- // global one-to-one weak allocator. They must use PositiveAgentId or
928
- // PathAgentId only see allocate_session_candidates Any-block.
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(candidate: &CapturedSessionCandidate, claimed: &BTreeSet<String>) -> bool {
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 = move |_provider| {
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 = std::env::temp_dir().join(format!("ta-e57-recover-{}-{}", std::process::id(), uniq));
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.get("capture_state").and_then(serde_json::Value::as_str),
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
  );