@team-agent/installer 0.5.3 → 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.
@@ -95,9 +95,14 @@ pub enum ResumeRefusalReason {
95
95
  SessionCaptureIncomplete,
96
96
  /// State session_id differs from the provider's observed session
97
97
  /// (T6 L5.5 drift). Caller should reconcile before resuming.
98
- SessionDrift {
99
- expected: String,
100
- actual: String,
98
+ SessionDrift { expected: String, actual: String },
99
+ /// The persisted provider backing exists, but the transcript itself
100
+ /// declares a different Team Agent worker identity.
101
+ SessionIdentityMismatch {
102
+ expected_agent_id: String,
103
+ embedded_agent_id: String,
104
+ session_id: String,
105
+ rollout_path: Option<PathBuf>,
101
106
  },
102
107
  /// Catch-all for refusals the structured shape hasn't taxonomized
103
108
  /// yet. Carries the legacy free-form string. This variant exists so
@@ -118,11 +123,10 @@ impl ResumeRefusalReason {
118
123
  ResumeRefusalReason::SessionBackingStoreMissing { .. } => {
119
124
  "session_backing_store_missing"
120
125
  }
121
- ResumeRefusalReason::ProviderResumeUnsupported { .. } => {
122
- "provider_resume_unsupported"
123
- }
126
+ ResumeRefusalReason::ProviderResumeUnsupported { .. } => "provider_resume_unsupported",
124
127
  ResumeRefusalReason::SessionCaptureIncomplete => "session_capture_incomplete",
125
128
  ResumeRefusalReason::SessionDrift { .. } => "session_drift",
129
+ ResumeRefusalReason::SessionIdentityMismatch { .. } => "session_identity_mismatch",
126
130
  // For Other we still report the legacy wire so the existing
127
131
  // `session_unresumable` JSON shape is preserved end-to-end.
128
132
  ResumeRefusalReason::Other { .. } => "session_unresumable",
@@ -134,16 +138,20 @@ impl ResumeRefusalReason {
134
138
  pub fn from_legacy(reason: &str) -> Self {
135
139
  match reason {
136
140
  "no_persisted_session_id" => ResumeRefusalReason::NoSessionId,
137
- "session_backing_store_missing" => {
138
- ResumeRefusalReason::SessionBackingStoreMissing {
139
- checked_paths: Vec::new(),
140
- recovery_hint: None,
141
- }
142
- }
141
+ "session_backing_store_missing" => ResumeRefusalReason::SessionBackingStoreMissing {
142
+ checked_paths: Vec::new(),
143
+ recovery_hint: None,
144
+ },
143
145
  "provider_resume_unsupported" => ResumeRefusalReason::ProviderResumeUnsupported {
144
146
  provider: String::new(),
145
147
  },
146
148
  "session_capture_incomplete" => ResumeRefusalReason::SessionCaptureIncomplete,
149
+ "session_identity_mismatch" => ResumeRefusalReason::SessionIdentityMismatch {
150
+ expected_agent_id: String::new(),
151
+ embedded_agent_id: String::new(),
152
+ session_id: String::new(),
153
+ rollout_path: None,
154
+ },
147
155
  other => ResumeRefusalReason::Other {
148
156
  legacy_reason: other.to_string(),
149
157
  },
@@ -155,15 +163,11 @@ impl ResumeRefusalReason {
155
163
  #[derive(Debug, Clone, PartialEq, Eq)]
156
164
  pub enum ResumePreflightOutcome {
157
165
  /// Worker can resume from the named session.
158
- Resume {
159
- session_id: String,
160
- },
166
+ Resume { session_id: String },
161
167
  /// `--allow-fresh` was set; worker will start fresh.
162
168
  FreshStart,
163
169
  /// Resume refused. Caller MUST NOT proceed with teardown/spawn.
164
- Refuse {
165
- reason: ResumeRefusalReason,
166
- },
170
+ Refuse { reason: ResumeRefusalReason },
167
171
  }
168
172
 
169
173
  /// Information about whether a provider backing file is present on disk.
@@ -239,9 +243,7 @@ impl ResumePreflight {
239
243
  } else {
240
244
  ResumePreflightOutcome::Refuse {
241
245
  reason: ResumeRefusalReason::SessionBackingStoreMissing {
242
- checked_paths: backing
243
- .map(|b| b.paths.clone())
244
- .unwrap_or_default(),
246
+ checked_paths: backing.map(|b| b.paths.clone()).unwrap_or_default(),
245
247
  recovery_hint,
246
248
  },
247
249
  }
@@ -272,6 +274,7 @@ mod tests {
272
274
  "session_backing_store_missing",
273
275
  "provider_resume_unsupported",
274
276
  "session_capture_incomplete",
277
+ "session_identity_mismatch",
275
278
  ] {
276
279
  assert_eq!(ResumeRefusalReason::from_legacy(wire).wire(), wire);
277
280
  }
@@ -383,10 +386,11 @@ mod tests {
383
386
  );
384
387
  match out {
385
388
  ResumePreflightOutcome::Refuse {
386
- reason: ResumeRefusalReason::SessionBackingStoreMissing {
387
- recovery_hint: Some(h),
388
- ..
389
- },
389
+ reason:
390
+ ResumeRefusalReason::SessionBackingStoreMissing {
391
+ recovery_hint: Some(h),
392
+ ..
393
+ },
390
394
  } => {
391
395
  assert_eq!(h, hint);
392
396
  }
@@ -405,9 +409,7 @@ mod tests {
405
409
  let out = ResumePreflight::check(Some("sess-x"), true, Some(&backing), "codex", false);
406
410
  match out {
407
411
  ResumePreflightOutcome::Refuse {
408
- reason: ResumeRefusalReason::SessionBackingStoreMissing {
409
- recovery_hint, ..
410
- },
412
+ reason: ResumeRefusalReason::SessionBackingStoreMissing { recovery_hint, .. },
411
413
  } => {
412
414
  assert!(recovery_hint.is_none());
413
415
  }
@@ -98,7 +98,15 @@ pub(super) fn parse_candidate_files(
98
98
  } else {
99
99
  Confidence::Low
100
100
  };
101
- let positive_agent_id_match = candidate_text_has_team_agent_id(&text, context);
101
+ let embedded_agent_id = embedded_team_agent_worker_id_from_text(&text);
102
+ if embedded_agent_id
103
+ .as_deref()
104
+ .is_some_and(|id| id != context.agent_id.as_str())
105
+ {
106
+ continue;
107
+ }
108
+ let positive_agent_id_match = candidate_text_has_team_agent_id(&text, context)
109
+ || embedded_agent_id.as_deref() == Some(context.agent_id.as_str());
102
110
  let agent_path_match = candidate_path_matches_agent_id(&path, context);
103
111
  if matches!(provider, Provider::Claude | Provider::ClaudeCode)
104
112
  && super::claude::records_have_leader_marker(&records)
@@ -113,6 +121,7 @@ pub(super) fn parse_candidate_files(
113
121
  attribution_confidence,
114
122
  spawn_cwd: context.spawn_cwd.clone(),
115
123
  },
124
+ embedded_agent_id,
116
125
  positive_agent_id_match,
117
126
  agent_path_match,
118
127
  });
@@ -308,6 +317,28 @@ fn candidate_text_has_team_agent_id(text: &str, context: &CaptureSessionContext)
308
317
  .any(|needle| text.contains(needle))
309
318
  }
310
319
 
320
+ pub(crate) fn embedded_team_agent_worker_id_from_text(text: &str) -> Option<String> {
321
+ const PREFIX: &str = "You are Team Agent worker `";
322
+ let start = text.find(PREFIX)? + PREFIX.len();
323
+ let rest = &text[start..];
324
+ let end = rest.find('`')?;
325
+ let id = &rest[..end];
326
+ if id.is_empty()
327
+ || !id
328
+ .chars()
329
+ .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
330
+ {
331
+ return None;
332
+ }
333
+ Some(id.to_string())
334
+ }
335
+
336
+ pub(crate) fn rollout_path_embedded_team_agent_worker_id(path: &Path) -> Option<String> {
337
+ read_head_text(path, CAPTURE_HEAD_BYTES)
338
+ .ok()
339
+ .and_then(|text| embedded_team_agent_worker_id_from_text(&text))
340
+ }
341
+
311
342
  fn candidate_path_matches_agent_id(path: &Path, context: &CaptureSessionContext) -> bool {
312
343
  let id = context.agent_id.as_str();
313
344
  if id.is_empty() {
@@ -348,3 +379,88 @@ fn path_is_under_team_runtime(path: &Path) -> bool {
348
379
  path.components()
349
380
  .any(|c| c.as_os_str() == std::ffi::OsStr::new(".team"))
350
381
  }
382
+
383
+ #[cfg(test)]
384
+ mod tests {
385
+ use super::*;
386
+
387
+ fn temp_dir(name: &str) -> PathBuf {
388
+ static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
389
+ let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
390
+ let dir =
391
+ std::env::temp_dir().join(format!("ta-session-scan-{name}-{}-{n}", std::process::id()));
392
+ std::fs::create_dir_all(&dir).expect("create temp dir");
393
+ dir
394
+ }
395
+
396
+ fn write_codex_rollout(path: &Path, cwd: &Path, session_id: &str, embedded_agent_id: &str) {
397
+ let text = format!(
398
+ "{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}}}}\n\
399
+ {{\"type\":\"turn_context\",\"payload\":{{}}}}\n\
400
+ {{\"type\":\"response_item\",\"payload\":{{\"content\":[{{\"type\":\"input_text\",\"text\":\"You are Team Agent worker `{embedded_agent_id}` with role `fixture`.\"}}]}}}}\n",
401
+ cwd.to_string_lossy()
402
+ );
403
+ std::fs::write(path, text).expect("write codex rollout");
404
+ }
405
+
406
+ #[test]
407
+ fn codex_prompt_worker_identity_is_a_positive_match() {
408
+ let dir = temp_dir("positive");
409
+ let rollout = dir.join("rollout-frontend.jsonl");
410
+ write_codex_rollout(&rollout, &dir, "sess-frontend", "frontend");
411
+ let context = CaptureSessionContext {
412
+ agent_id: "frontend".to_string(),
413
+ spawn_cwd: dir.clone(),
414
+ pane_id: None,
415
+ pane_pid: None,
416
+ spawned_at: None,
417
+ expected_session_id: None,
418
+ provider_projects_root: None,
419
+ };
420
+ let candidates = parse_candidate_files(
421
+ Provider::Codex,
422
+ &context,
423
+ vec![SessionCandidate {
424
+ path: rollout.clone(),
425
+ requires_cwd_match: false,
426
+ }],
427
+ );
428
+ assert_eq!(candidates.len(), 1);
429
+ assert!(
430
+ candidates[0].positive_agent_id_match,
431
+ "Codex transcript prompt identity must be treated as a positive worker id source"
432
+ );
433
+ let _ = std::fs::remove_file(&rollout);
434
+ let _ = std::fs::remove_dir_all(&dir);
435
+ }
436
+
437
+ #[test]
438
+ fn codex_prompt_worker_identity_mismatch_is_rejected_for_that_agent() {
439
+ let dir = temp_dir("mismatch");
440
+ let rollout = dir.join("rollout-ios-dev.jsonl");
441
+ write_codex_rollout(&rollout, &dir, "sess-ios-dev", "ios-dev");
442
+ let context = CaptureSessionContext {
443
+ agent_id: "frontend".to_string(),
444
+ spawn_cwd: dir.clone(),
445
+ pane_id: None,
446
+ pane_pid: None,
447
+ spawned_at: None,
448
+ expected_session_id: None,
449
+ provider_projects_root: None,
450
+ };
451
+ let candidates = parse_candidate_files(
452
+ Provider::Codex,
453
+ &context,
454
+ vec![SessionCandidate {
455
+ path: rollout.clone(),
456
+ requires_cwd_match: false,
457
+ }],
458
+ );
459
+ assert!(
460
+ candidates.is_empty(),
461
+ "state agent=frontend must not accept a Codex rollout whose prompt says ios-dev"
462
+ );
463
+ let _ = std::fs::remove_file(&rollout);
464
+ let _ = std::fs::remove_dir_all(&dir);
465
+ }
466
+ }
@@ -48,6 +48,7 @@ fn copilot_candidate(
48
48
  attribution_confidence: Confidence::High,
49
49
  spawn_cwd: context.spawn_cwd.clone(),
50
50
  },
51
+ embedded_agent_id: None,
51
52
  positive_agent_id_match: false,
52
53
  agent_path_match: false,
53
54
  }
@@ -49,6 +49,7 @@ pub struct CaptureSessionContext {
49
49
  #[derive(Debug, Clone, PartialEq, Eq)]
50
50
  pub struct CapturedSessionCandidate {
51
51
  pub captured: CapturedSession,
52
+ pub embedded_agent_id: Option<String>,
52
53
  pub positive_agent_id_match: bool,
53
54
  pub agent_path_match: bool,
54
55
  }
@@ -234,6 +234,23 @@ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority(
234
234
  save_runtime_state_with_merge_options(workspace, state, &[], None, &[], agent_ids)
235
235
  }
236
236
 
237
+ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
238
+ workspace: &Path,
239
+ state: &Value,
240
+ skip_capture_backfill_team_key: &str,
241
+ skip_capture_backfill_agent_ids: &[&str],
242
+ topology_agent_ids: &[&str],
243
+ ) -> Result<(), StateError> {
244
+ save_runtime_state_with_merge_options(
245
+ workspace,
246
+ state,
247
+ &[],
248
+ Some(skip_capture_backfill_team_key),
249
+ skip_capture_backfill_agent_ids,
250
+ topology_agent_ids,
251
+ )
252
+ }
253
+
237
254
  pub(crate) fn save_runtime_state_with_deleted_agents(
238
255
  workspace: &Path,
239
256
  state: &Value,
@@ -17,6 +17,7 @@ use super::StateError;
17
17
  use crate::state::persist::{
18
18
  load_runtime_state, save_runtime_state_with_deleted_agents,
19
19
  save_runtime_state_with_lifecycle_topology_authority,
20
+ save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip,
20
21
  save_runtime_state_with_team_tombstone_lifecycle_topology_authority,
21
22
  save_runtime_state_with_team_tombstoned_agents,
22
23
  };
@@ -595,7 +596,7 @@ pub(crate) fn save_team_scoped_state_with_tombstone_lifecycle_topology_authority
595
596
  team_state: &Value,
596
597
  agent_ids: &[&str],
597
598
  ) -> Result<(), StateError> {
598
- save_team_scoped_state_with_merge_options(workspace, team_state, &[], agent_ids, agent_ids)
599
+ save_team_scoped_state_with_merge_options(workspace, team_state, &[], agent_ids, &[], agent_ids)
599
600
  }
600
601
 
601
602
  pub(crate) fn save_team_scoped_state_with_lifecycle_topology_authority(
@@ -603,7 +604,23 @@ pub(crate) fn save_team_scoped_state_with_lifecycle_topology_authority(
603
604
  team_state: &Value,
604
605
  agent_ids: &[&str],
605
606
  ) -> Result<(), StateError> {
606
- save_team_scoped_state_with_merge_options(workspace, team_state, &[], &[], agent_ids)
607
+ save_team_scoped_state_with_merge_options(workspace, team_state, &[], &[], &[], agent_ids)
608
+ }
609
+
610
+ pub(crate) fn save_team_scoped_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
611
+ workspace: &Path,
612
+ team_state: &Value,
613
+ skip_capture_backfill_agent_ids: &[&str],
614
+ topology_agent_ids: &[&str],
615
+ ) -> Result<(), StateError> {
616
+ save_team_scoped_state_with_merge_options(
617
+ workspace,
618
+ team_state,
619
+ &[],
620
+ &[],
621
+ skip_capture_backfill_agent_ids,
622
+ topology_agent_ids,
623
+ )
607
624
  }
608
625
 
609
626
  fn save_team_scoped_state_with_merge_exceptions(
@@ -618,6 +635,7 @@ fn save_team_scoped_state_with_merge_exceptions(
618
635
  deleted_agent_ids,
619
636
  tombstoned_agent_ids,
620
637
  &[],
638
+ &[],
621
639
  )
622
640
  }
623
641
 
@@ -626,6 +644,7 @@ fn save_team_scoped_state_with_merge_options(
626
644
  team_state: &Value,
627
645
  deleted_agent_ids: &[&str],
628
646
  tombstoned_agent_ids: &[&str],
647
+ skip_capture_backfill_agent_ids: &[&str],
629
648
  topology_agent_ids: &[&str],
630
649
  ) -> Result<(), StateError> {
631
650
  let target_key = team_state_key(team_state);
@@ -669,12 +688,30 @@ fn save_team_scoped_state_with_merge_options(
669
688
  topology_agent_ids,
670
689
  );
671
690
  }
691
+ if !skip_capture_backfill_agent_ids.is_empty() {
692
+ return save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
693
+ workspace,
694
+ &merged,
695
+ &target_key,
696
+ skip_capture_backfill_agent_ids,
697
+ topology_agent_ids,
698
+ );
699
+ }
672
700
  return save_runtime_state_with_lifecycle_topology_authority(
673
701
  workspace,
674
702
  &merged,
675
703
  topology_agent_ids,
676
704
  );
677
705
  }
706
+ if !skip_capture_backfill_agent_ids.is_empty() {
707
+ return save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
708
+ workspace,
709
+ &merged,
710
+ &target_key,
711
+ skip_capture_backfill_agent_ids,
712
+ &[],
713
+ );
714
+ }
678
715
  if tombstoned_agent_ids.is_empty() {
679
716
  return save_runtime_state_with_deleted_agents(workspace, &merged, deleted_agent_ids);
680
717
  }
@@ -709,10 +746,28 @@ fn save_team_scoped_state_with_merge_options(
709
746
  }
710
747
  if tombstoned_agent_ids.is_empty() {
711
748
  if !topology_agent_ids.is_empty() {
712
- save_runtime_state_with_lifecycle_topology_authority(
749
+ if !skip_capture_backfill_agent_ids.is_empty() {
750
+ save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
751
+ workspace,
752
+ &Value::Object(merged),
753
+ &target_key,
754
+ skip_capture_backfill_agent_ids,
755
+ topology_agent_ids,
756
+ )
757
+ } else {
758
+ save_runtime_state_with_lifecycle_topology_authority(
759
+ workspace,
760
+ &Value::Object(merged),
761
+ topology_agent_ids,
762
+ )
763
+ }
764
+ } else if !skip_capture_backfill_agent_ids.is_empty() {
765
+ save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
713
766
  workspace,
714
767
  &Value::Object(merged),
715
- topology_agent_ids,
768
+ &target_key,
769
+ skip_capture_backfill_agent_ids,
770
+ &[],
716
771
  )
717
772
  } else {
718
773
  save_runtime_state_with_deleted_agents(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.3",
24
- "@team-agent/cli-darwin-x64": "0.5.3",
25
- "@team-agent/cli-linux-x64": "0.5.3"
23
+ "@team-agent/cli-darwin-arm64": "0.5.4",
24
+ "@team-agent/cli-darwin-x64": "0.5.4",
25
+ "@team-agent/cli-linux-x64": "0.5.4"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",