@team-agent/installer 0.3.33 → 0.3.34

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 CHANGED
@@ -566,7 +566,7 @@ dependencies = [
566
566
 
567
567
  [[package]]
568
568
  name = "team-agent"
569
- version = "0.3.33"
569
+ version = "0.3.34"
570
570
  dependencies = [
571
571
  "anyhow",
572
572
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent"]
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.3.33"
12
+ version = "0.3.34"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -471,7 +471,10 @@ pub(super) fn resume_backing_exists_for_agent(
471
471
  let _ = (workspace, agent_id, agent, session_id, rollout_path);
472
472
  false
473
473
  }
474
- Provider::Codex => rollout_path_exists(rollout_path),
474
+ Provider::Codex => {
475
+ rollout_path_exists(rollout_path)
476
+ || codex_session_transcript_exists(agent, session_id.as_str(), rollout_path)
477
+ }
475
478
  Provider::Claude | Provider::ClaudeCode => {
476
479
  rollout_path_exists(rollout_path)
477
480
  || event_log_transcript_exists(workspace, agent_id.as_str(), session_id.as_str())
@@ -557,6 +560,68 @@ fn claude_project_transcript_exists(agent: &serde_json::Value, session_id: &str)
557
560
  .any(|entry| entry.path().join(&transcript_name).is_file())
558
561
  }
559
562
 
563
+ fn codex_session_transcript_exists(
564
+ agent: &serde_json::Value,
565
+ session_id: &str,
566
+ rollout_path: Option<&RolloutPath>,
567
+ ) -> bool {
568
+ if session_id.is_empty() {
569
+ return false;
570
+ }
571
+ let mut roots = Vec::new();
572
+ if let Some(parent) = rollout_path
573
+ .map(RolloutPath::as_path)
574
+ .and_then(Path::parent)
575
+ .filter(|path| path.is_dir())
576
+ {
577
+ roots.push(parent.to_path_buf());
578
+ }
579
+ if let Some(root) = agent
580
+ .get("codex_sessions_root")
581
+ .and_then(serde_json::Value::as_str)
582
+ .filter(|path| !path.is_empty())
583
+ .map(PathBuf::from)
584
+ .filter(|path| path.is_dir())
585
+ {
586
+ roots.push(root);
587
+ }
588
+ if let Some(root) = std::env::var_os("HOME")
589
+ .map(PathBuf::from)
590
+ .map(|home| home.join(".codex").join("sessions"))
591
+ .filter(|path| path.is_dir())
592
+ {
593
+ roots.push(root);
594
+ }
595
+ roots.sort();
596
+ roots.dedup();
597
+ roots
598
+ .iter()
599
+ .any(|root| session_transcript_exists_under(root, session_id, 4))
600
+ }
601
+
602
+ fn session_transcript_exists_under(root: &Path, session_id: &str, max_depth: usize) -> bool {
603
+ let Ok(entries) = std::fs::read_dir(root) else {
604
+ return false;
605
+ };
606
+ for entry in entries.flatten() {
607
+ let path = entry.path();
608
+ if path.is_file() {
609
+ let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
610
+ continue;
611
+ };
612
+ if name.ends_with(".jsonl") && name.contains(session_id) {
613
+ return true;
614
+ }
615
+ } else if max_depth > 0
616
+ && path.is_dir()
617
+ && session_transcript_exists_under(&path, session_id, max_depth.saturating_sub(1))
618
+ {
619
+ return true;
620
+ }
621
+ }
622
+ false
623
+ }
624
+
560
625
  fn copilot_session_store_has_session(session_id: &str) -> bool {
561
626
  let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
562
627
  return false;
@@ -1082,4 +1147,41 @@ mod e36_transcript_backing_tests {
1082
1147
  let agent = serde_json::json!({});
1083
1148
  assert!(!claude_project_transcript_exists(&agent, ""));
1084
1149
  }
1150
+
1151
+ #[test]
1152
+ fn codex_session_transcript_is_recognized_when_rollout_path_is_stale() {
1153
+ let scratch = ScratchDir::new("codex-recognized");
1154
+ let sessions_root = scratch.path().join("sessions");
1155
+ let dated = sessions_root.join("2026").join("06").join("20");
1156
+ std::fs::create_dir_all(&dated).expect("mkdir dated sessions");
1157
+ let session_id = "019ee540-37ed-7a20-a141-1d654224d209";
1158
+ std::fs::write(
1159
+ dated.join(format!("rollout-2026-06-20T21-37-31-{session_id}.jsonl")),
1160
+ b"{}\n",
1161
+ )
1162
+ .expect("codex transcript");
1163
+
1164
+ let stale = RolloutPath::new(scratch.path().join("old").join("missing.jsonl"));
1165
+ let agent = serde_json::json!({
1166
+ "codex_sessions_root": sessions_root.to_string_lossy(),
1167
+ });
1168
+ assert!(
1169
+ codex_session_transcript_exists(&agent, session_id, Some(&stale)),
1170
+ "matching codex transcript under codex_sessions_root must count as resume backing"
1171
+ );
1172
+ }
1173
+
1174
+ #[test]
1175
+ fn missing_codex_session_transcript_is_not_backing() {
1176
+ let scratch = ScratchDir::new("codex-missing");
1177
+ let sessions_root = scratch.path().join("sessions");
1178
+ std::fs::create_dir_all(&sessions_root).expect("mkdir sessions");
1179
+ let agent = serde_json::json!({
1180
+ "codex_sessions_root": sessions_root.to_string_lossy(),
1181
+ });
1182
+ assert!(
1183
+ !codex_session_transcript_exists(&agent, "019ee540-ffff-7a20-a141-1d654224d209", None,),
1184
+ "no matching codex transcript => no backing"
1185
+ );
1186
+ }
1085
1187
  }
@@ -172,7 +172,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
172
172
  if !convergence.converged && convergence.changed {
173
173
  save_restart_state(&selected.run_workspace, &mut state, &selected.team_key)?;
174
174
  }
175
- let mut forced_fresh_missing = if convergence.converged {
175
+ let forced_fresh_missing = if convergence.converged {
176
176
  std::collections::BTreeSet::new()
177
177
  } else {
178
178
  convergence.missing.iter().cloned().collect()
@@ -183,12 +183,6 @@ pub fn restart_with_transport_with_session_convergence_deadline(
183
183
  &state,
184
184
  allow_fresh,
185
185
  )?;
186
- for decision in &plan.decisions {
187
- if matches!(decision.decision, ResumeDecision::FreshStart) && decision.session_id.is_some()
188
- {
189
- forced_fresh_missing.insert(decision.agent_id.as_str().to_string());
190
- }
191
- }
192
186
  write_restart_resume_decision_events(
193
187
  &selected.run_workspace,
194
188
  &state,
@@ -43,6 +43,7 @@ pub use rebuild::{
43
43
  restart, restart_candidates, restart_with_session_convergence_deadline, restart_with_transport,
44
44
  restart_with_transport_with_readiness_deadline, select_restart_state,
45
45
  };
46
+ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline;
46
47
  pub use remove::{remove_agent, remove_agent_with_transport};
47
48
  pub use selection::{classify_first_send_at, classify_restart_plan, decide_start_mode, python_type_name};
48
49
  pub(crate) use team_state::write_team_state;
@@ -1533,6 +1533,107 @@ fn restart_with_transport_spawns_resumable_workers_not_stub() {
1533
1533
  );
1534
1534
  }
1535
1535
 
1536
+ #[test]
1537
+ fn restart_allow_fresh_does_not_force_fresh_other_agents_when_one_session_capture_times_out() {
1538
+ let ws = temp_ws().join("restartatomic");
1539
+ std::fs::create_dir_all(ws.join("agents")).unwrap();
1540
+ std::fs::write(
1541
+ ws.join("TEAM.md"),
1542
+ "---\nname: restartatomic\nobjective: Restart atomicity probe.\nprovider: codex\n---\n\nteam.\n",
1543
+ )
1544
+ .unwrap();
1545
+ std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
1546
+ std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
1547
+
1548
+ let sessions_root = ws.join("codex-sessions");
1549
+ let dated = sessions_root.join("2026").join("06").join("20");
1550
+ std::fs::create_dir_all(&dated).unwrap();
1551
+ let alpha_session = "019ee540-37ed-7a20-a141-1d654224d209";
1552
+ std::fs::write(
1553
+ dated.join(format!(
1554
+ "rollout-2026-06-20T21-37-31-{alpha_session}.jsonl"
1555
+ )),
1556
+ "{}\n",
1557
+ )
1558
+ .unwrap();
1559
+
1560
+ let spec = crate::compiler::compile_team(&ws).expect("compile restart team");
1561
+ std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
1562
+ crate::state::persist::save_runtime_state(
1563
+ &ws,
1564
+ &json!({
1565
+ "session_name": "team-restartatomic",
1566
+ "agents": {
1567
+ "alpha": {
1568
+ "status": "running",
1569
+ "provider": "codex",
1570
+ "session_id": alpha_session,
1571
+ "rollout_path": ws.join("stale").join("missing-alpha.jsonl").to_string_lossy(),
1572
+ "codex_sessions_root": sessions_root.to_string_lossy(),
1573
+ "first_send_at": "2026-05-27T10:00:00+00:00"
1574
+ },
1575
+ "bravo": {
1576
+ "status": "running",
1577
+ "provider": "codex",
1578
+ "session_id": null,
1579
+ "spawn_cwd": ws.to_string_lossy(),
1580
+ "first_send_at": "2026-05-27T10:00:00+00:00"
1581
+ }
1582
+ }
1583
+ }),
1584
+ )
1585
+ .unwrap();
1586
+ seed_healthy_coordinator(&ws);
1587
+ let transport = OfflineTransport::new();
1588
+
1589
+ let result = restart_with_transport_with_session_convergence_deadline(
1590
+ &ws,
1591
+ true,
1592
+ None,
1593
+ &transport,
1594
+ Some(0),
1595
+ None,
1596
+ );
1597
+
1598
+ assert!(
1599
+ matches!(result, Ok(RestartReport::Restarted { .. })),
1600
+ "allow-fresh restart should complete with alpha resumed and bravo fresh; got {result:?}"
1601
+ );
1602
+ let events = crate::event_log::EventLog::new(&ws).tail(80).unwrap();
1603
+ let decisions = events
1604
+ .iter()
1605
+ .filter(|event| {
1606
+ event.get("event").and_then(|v| v.as_str()) == Some("restart.resume_decision")
1607
+ })
1608
+ .collect::<Vec<_>>();
1609
+ let alpha = decisions
1610
+ .iter()
1611
+ .find(|event| event.get("worker_id").and_then(|v| v.as_str()) == Some("alpha"))
1612
+ .expect("alpha decision");
1613
+ assert_eq!(
1614
+ alpha.get("decision").and_then(|v| v.as_str()),
1615
+ Some("resume"),
1616
+ "alpha has session_id plus matching codex transcript and must not be forced fresh: {alpha}"
1617
+ );
1618
+ assert!(
1619
+ alpha.get("forced_fresh").is_none(),
1620
+ "alpha must not inherit bravo's convergence timeout: {alpha}"
1621
+ );
1622
+ let bravo = decisions
1623
+ .iter()
1624
+ .find(|event| event.get("worker_id").and_then(|v| v.as_str()) == Some("bravo"))
1625
+ .expect("bravo decision");
1626
+ assert_eq!(
1627
+ bravo.get("decision").and_then(|v| v.as_str()),
1628
+ Some("fresh_start")
1629
+ );
1630
+ assert_eq!(
1631
+ bravo.get("forced_fresh").and_then(|v| v.as_bool()),
1632
+ Some(true),
1633
+ "only the missing-session agent should carry forced_fresh: {bravo}"
1634
+ );
1635
+ }
1636
+
1536
1637
  #[test]
1537
1638
  fn restart_spawn_failure_isolated_to_partial_report() {
1538
1639
  let ws = restart_ws_two_resumable_workers();
@@ -1476,7 +1476,6 @@ fn claude_base_command(
1476
1476
  };
1477
1477
  argv.push("--mcp-config".to_string());
1478
1478
  argv.push(raw.to_string());
1479
- argv.push("--strict-mcp-config".to_string());
1480
1479
  }
1481
1480
  for tool in claude_disallowed_tools(tools) {
1482
1481
  argv.push("--disallowedTools".to_string());
@@ -215,6 +215,31 @@ fn build_command_includes_model_and_system_prompt() {
215
215
  );
216
216
  }
217
217
 
218
+ #[test]
219
+ fn claude_mcp_config_does_not_enable_strict_mode() {
220
+ let adapter = get_adapter(Provider::ClaudeCode);
221
+ let config = adapter
222
+ .mcp_config(AuthMode::Subscription)
223
+ .expect("mcp config");
224
+ let argv = adapter
225
+ .build_command(
226
+ AuthMode::Subscription,
227
+ Some(&config),
228
+ Some("worker"),
229
+ Some("opus"),
230
+ )
231
+ .expect("build command with mcp config");
232
+
233
+ assert!(
234
+ argv_contains_adjacent(&argv, &["--mcp-config"]),
235
+ "Claude worker argv must still pass Team Agent MCP config: {argv:?}"
236
+ );
237
+ assert!(
238
+ !argv.iter().any(|arg| arg == "--strict-mcp-config"),
239
+ "--mcp-config must merge with user-level Claude MCP; strict mode hides user MCP: {argv:?}"
240
+ );
241
+ }
242
+
218
243
  #[test]
219
244
  fn unsupported_provider_capability_error_message_shape() {
220
245
  // unsupported.py:31 ProviderCapabilityError — placeholder providers reject
@@ -236,4 +261,3 @@ fn unsupported_provider_capability_error_message_shape() {
236
261
  // ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
237
262
  // Lock the CORRECT Python v0.2.11 behavior the strengthened contracts missed.
238
263
  // Golden re-probed via /tmp/probe_p2_provider.py vs team-agent-public @ 439bef8.
239
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.33",
3
+ "version": "0.3.34",
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.3.33",
24
- "@team-agent/cli-darwin-x64": "0.3.33",
25
- "@team-agent/cli-linux-x64": "0.3.33"
23
+ "@team-agent/cli-darwin-arm64": "0.3.34",
24
+ "@team-agent/cli-darwin-x64": "0.3.34",
25
+ "@team-agent/cli-linux-x64": "0.3.34"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",