@team-agent/installer 0.5.57 → 0.5.58

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
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.57"
578
+ version = "0.5.58"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.57"
12
+ version = "0.5.58"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -2484,6 +2484,7 @@ pub mod lifecycle_port {
2484
2484
  "new_agent_id": report.new_agent_id.as_str(),
2485
2485
  "session_id": report.session_id.as_ref().map(|session| session.as_str()),
2486
2486
  "new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
2487
+ "backing_state": report.backing_state,
2487
2488
  })),
2488
2489
  Err(e) => Ok(error_value(e)),
2489
2490
  }
@@ -57,66 +57,13 @@ pub fn fork_agent_with_transport(
57
57
  let text = std::fs::read_to_string(&read_spec_path)
58
58
  .map_err(|e| LifecycleError::Compile(format!("{}: {e}", read_spec_path.display())))?;
59
59
  let spec = yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))?;
60
- if find_spec_agent(&spec, as_agent_id).is_some() || leader_id_matches(&spec, as_agent_id) {
60
+ if fork_spec_agent(&spec, as_agent_id).is_some() || leader_id_matches(&spec, as_agent_id) {
61
61
  return Err(LifecycleError::RequirementUnmet(format!(
62
62
  "agent id already exists: {as_agent_id}"
63
63
  )));
64
64
  }
65
- let source_agent = find_spec_agent(&spec, source_agent_id).ok_or_else(|| {
66
- LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
67
- })?;
68
- // Fork requires the complete source tuple before treating session_id as
69
- // resumable truth; a scalar-only row has no confirmed backing.
70
- let source_agent_state = state
71
- .get("agents")
72
- .and_then(|v| v.get(source_agent_id.as_str()))
73
- .ok_or_else(|| {
74
- LifecycleError::Provider(format!(
75
- "cannot fork {source_agent_id}: source agent row not in state"
76
- ))
77
- })?;
78
- let tuple_field_ok = |field: &str| -> bool {
79
- source_agent_state
80
- .get(field)
81
- .and_then(|v| v.as_str())
82
- .is_some_and(|s| !s.is_empty())
83
- };
84
- let session_id_str = source_agent_state
85
- .get("session_id")
86
- .and_then(|v| v.as_str())
87
- .filter(|s| !s.is_empty());
88
- let rollout_path_str = source_agent_state
89
- .get("rollout_path")
90
- .and_then(|v| v.as_str())
91
- .filter(|s| !s.is_empty());
92
- if session_id_str.is_none()
93
- || rollout_path_str.is_none()
94
- || !tuple_field_ok("captured_at")
95
- || !tuple_field_ok("captured_via")
96
- {
97
- return Err(LifecycleError::Provider(format!(
98
- "cannot fork {source_agent_id}: source session backing is missing or incomplete \
99
- (session_id+rollout_path+captured_at+captured_via required)"
100
- )));
101
- }
102
- let Some(source_backing_raw) = rollout_path_str else {
103
- return Err(LifecycleError::Provider(format!(
104
- "cannot fork {source_agent_id}: source session backing is missing"
105
- )));
106
- };
107
- let source_backing = Path::new(source_backing_raw);
108
- if !source_backing.is_file() {
109
- return Err(LifecycleError::Provider(format!(
110
- "cannot fork {source_agent_id}: source session backing is not readable: {}",
111
- source_backing.display()
112
- )));
113
- }
114
- let Some(source_session_id) = session_id_str else {
115
- return Err(LifecycleError::Provider(format!(
116
- "cannot fork {source_agent_id}: source session id is missing"
117
- )));
118
- };
119
- let session_id = crate::provider::SessionId::new(source_session_id.to_string());
65
+ // Source existence authority: state.get("agents"), matching clone-agent.
66
+ let (session_id, source_backing) = fork_source_tuple(&state, source_agent_id)?;
120
67
  let session_name = state
121
68
  .get("session_name")
122
69
  .and_then(|v| v.as_str())
@@ -158,7 +105,7 @@ pub fn fork_agent_with_transport(
158
105
  crate::model::spec::validate_spec(&new_spec, &validate_ws)
159
106
  .map_err(|e| LifecycleError::Compile(e.to_string()))?;
160
107
  write_spec_atomic(&spec_path, &new_spec)?;
161
- let new_agent = find_spec_agent(&new_spec, as_agent_id).ok_or_else(|| {
108
+ let new_agent = fork_spec_agent(&new_spec, as_agent_id).ok_or_else(|| {
162
109
  LifecycleError::RequirementUnmet(format!("unknown worker agent id: {as_agent_id}"))
163
110
  })?;
164
111
  let provider = new_agent
@@ -296,13 +243,21 @@ pub fn fork_agent_with_transport(
296
243
  plan.provider_projects_root = source_backing.parent().map(Path::to_path_buf);
297
244
  }
298
245
  let window = WindowName::new(as_agent_id.as_str());
246
+ let mut claude_fork = prepare_claude_fork_backing(
247
+ provider,
248
+ &plan,
249
+ &source_backing,
250
+ &session_id,
251
+ )
252
+ .map_err(|error| {
253
+ let _ = std::fs::write(&spec_path, text.as_bytes());
254
+ cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
255
+ error
256
+ })?;
257
+ // The framework-created Claude snapshot is only fork input, not provider
258
+ // proof. Observe changes made after materialization so a spawn-only
259
+ // provider cannot turn the copied source backing into a Verified result.
299
260
  let backing_before = crate::provider::session::ContextBackingSnapshot::capture(provider, &plan);
300
- let mut claude_fork = prepare_claude_fork_backing(provider, &plan, source_backing, &session_id)
301
- .map_err(|error| {
302
- let _ = std::fs::write(&spec_path, text.as_bytes());
303
- cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
304
- error
305
- })?;
306
261
  let mut env =
307
262
  inherited_env_with_team_overrides(&workspace, as_agent_id.as_str(), Some(&fork_team));
308
263
  apply_profile_launch_env(&mut env, &profile_launch);
@@ -401,7 +356,7 @@ pub fn fork_agent_with_transport(
401
356
  })?;
402
357
  let convergence_deadline =
403
358
  crate::provider::session::context_fork_convergence_deadline(provider);
404
- let context_proof = match crate::provider::session::verify_context_fork(
359
+ let context_outcome = crate::provider::session::observe_context_fork(
405
360
  provider,
406
361
  &session_id,
407
362
  &plan,
@@ -411,9 +366,40 @@ pub fn fork_agent_with_transport(
411
366
  &workspace,
412
367
  &spawned_at,
413
368
  convergence_deadline,
414
- ) {
415
- Ok(proof) => proof,
416
- Err(error) => {
369
+ );
370
+ let context_proof = match context_outcome {
371
+ crate::provider::session::ContextForkOutcome::Verified(proof) => Some(proof),
372
+ crate::provider::session::ContextForkOutcome::Pending(pending) => {
373
+ if let Err(error) = finalize_pending_fork_state(ForkPendingFinalizeInput {
374
+ workspace: &workspace,
375
+ team_key: &fork_team,
376
+ source_agent_id,
377
+ agent_id: as_agent_id,
378
+ spec_agent: new_agent,
379
+ safety: &safety,
380
+ plan: &plan,
381
+ profile_launch: &profile_launch,
382
+ spawn: &spawn,
383
+ profile_dir: &profile_dir,
384
+ dynamic_role_file: materialized_role.path(),
385
+ pending: &pending,
386
+ spawn_epoch,
387
+ }) {
388
+ rollback_fork_after_spawn(
389
+ &workspace,
390
+ transport,
391
+ &session_name,
392
+ &window,
393
+ &mcp_config_path,
394
+ as_agent_id,
395
+ &profile_launch,
396
+ &fork_team,
397
+ );
398
+ return Err(error);
399
+ }
400
+ None
401
+ }
402
+ crate::provider::session::ContextForkOutcome::Rejected(error) => {
417
403
  rollback_fork_after_spawn(
418
404
  &workspace,
419
405
  transport,
@@ -427,33 +413,35 @@ pub fn fork_agent_with_transport(
427
413
  return Err(LifecycleError::Provider(error.to_string()));
428
414
  }
429
415
  };
430
- if let Err(error) = finalize_fork_state(ForkFinalizeInput {
431
- workspace: &workspace,
432
- team_key: &fork_team,
433
- source_agent_id,
434
- agent_id: as_agent_id,
435
- spec_agent: new_agent,
436
- safety: &safety,
437
- plan: &plan,
438
- profile_launch: &profile_launch,
439
- spawn: &spawn,
440
- profile_dir: &profile_dir,
441
- dynamic_role_file: materialized_role.path(),
442
- context_proof: &context_proof,
443
- spawned_at: &spawned_at,
444
- spawn_epoch,
445
- }) {
446
- rollback_fork_after_spawn(
447
- &workspace,
448
- transport,
449
- &session_name,
450
- &window,
451
- &mcp_config_path,
452
- as_agent_id,
453
- &profile_launch,
454
- &fork_team,
455
- );
456
- return Err(error);
416
+ if let Some(context_proof) = context_proof.as_ref() {
417
+ if let Err(error) = finalize_fork_state(ForkFinalizeInput {
418
+ workspace: &workspace,
419
+ team_key: &fork_team,
420
+ source_agent_id,
421
+ agent_id: as_agent_id,
422
+ spec_agent: new_agent,
423
+ safety: &safety,
424
+ plan: &plan,
425
+ profile_launch: &profile_launch,
426
+ spawn: &spawn,
427
+ profile_dir: &profile_dir,
428
+ dynamic_role_file: materialized_role.path(),
429
+ context_proof: &context_proof,
430
+ spawned_at: &spawned_at,
431
+ spawn_epoch,
432
+ }) {
433
+ rollback_fork_after_spawn(
434
+ &workspace,
435
+ transport,
436
+ &session_name,
437
+ &window,
438
+ &mcp_config_path,
439
+ as_agent_id,
440
+ &profile_launch,
441
+ &fork_team,
442
+ );
443
+ return Err(error);
444
+ }
457
445
  }
458
446
  if let Err(error) =
459
447
  verify_fork_registration(&workspace, &fork_team, as_agent_id, &spawn, &window)
@@ -487,6 +475,11 @@ pub fn fork_agent_with_transport(
487
475
  if let Some(materialized) = copilot_fork.as_mut() {
488
476
  materialized.keep();
489
477
  }
478
+ let backing_state = if context_proof.is_some() {
479
+ ForkBackingState::Verified
480
+ } else {
481
+ ForkBackingState::PendingContextFork
482
+ };
490
483
  Ok(ForkAgentReport {
491
484
  source_agent_id: source_agent_id.clone(),
492
485
  new_agent_id: as_agent_id.clone(),
@@ -495,6 +488,7 @@ pub fn fork_agent_with_transport(
495
488
  state_file: crate::state::persist::runtime_state_path(&workspace),
496
489
  coordinator_started,
497
490
  },
498
- session_id: Some(context_proof.new_session_id),
491
+ session_id: context_proof.map(|proof| proof.new_session_id),
492
+ backing_state,
499
493
  })
500
494
  }
@@ -172,6 +172,110 @@ pub(super) fn finalize_fork_state(input: ForkFinalizeInput<'_>) -> Result<(), Li
172
172
  .map_err(|error| LifecycleError::StatePersist(error.to_string()))
173
173
  }
174
174
 
175
+ pub(super) struct ForkPendingFinalizeInput<'a> {
176
+ pub workspace: &'a Path,
177
+ pub team_key: &'a str,
178
+ pub source_agent_id: &'a AgentId,
179
+ pub agent_id: &'a AgentId,
180
+ pub spec_agent: &'a Value,
181
+ pub safety: &'a DangerousApproval,
182
+ pub plan: &'a crate::provider::CommandPlan,
183
+ pub profile_launch: &'a crate::provider::ProviderProfileLaunch,
184
+ pub spawn: &'a crate::transport::SpawnResult,
185
+ pub profile_dir: &'a Path,
186
+ pub dynamic_role_file: &'a Path,
187
+ pub pending: &'a crate::provider::session::PendingContextFork,
188
+ pub spawn_epoch: u64,
189
+ }
190
+
191
+ pub(super) fn finalize_pending_fork_state(
192
+ input: ForkPendingFinalizeInput<'_>,
193
+ ) -> Result<(), LifecycleError> {
194
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
195
+ workspace: input.workspace,
196
+ operation: "fork-agent-pending",
197
+ team: Some(input.team_key),
198
+ agent_id: Some(input.agent_id),
199
+ })?;
200
+ let mut next_state = crate::state::selector::resolve_active_team(
201
+ input.workspace,
202
+ Some(input.team_key),
203
+ crate::state::selector::SelectorMode::RequireSpec,
204
+ )
205
+ .map_err(|error| LifecycleError::TeamSelect(error.to_string()))?
206
+ .state;
207
+ upsert_pending_forked_agent_state(
208
+ &mut next_state,
209
+ input.source_agent_id,
210
+ input.agent_id,
211
+ input.spec_agent,
212
+ input.safety,
213
+ input.plan,
214
+ input.profile_launch,
215
+ input.spawn,
216
+ Some(input.profile_dir),
217
+ input.dynamic_role_file,
218
+ input.pending,
219
+ input.spawn_epoch,
220
+ )?;
221
+ maybe_fail_fork_after_spawn("save_runtime_state")?;
222
+ crate::state::repository::StateRepository::new(input.workspace)
223
+ .save(
224
+ crate::state::repository::StateWriteIntent::ForkAgent {
225
+ team_key: input.team_key,
226
+ agent_id: input.agent_id.as_str(),
227
+ },
228
+ &next_state,
229
+ )
230
+ .map_err(|error| LifecycleError::StatePersist(error.to_string()))
231
+ }
232
+
233
+ pub(crate) fn finalize_pending_fork_capture(
234
+ agent: &mut serde_json::Map<String, serde_json::Value>,
235
+ captured: &crate::provider::CapturedSession,
236
+ ) -> bool {
237
+ let Some(session_id) = captured.session_id.as_ref() else {
238
+ return false;
239
+ };
240
+ let Some(rollout_path) = captured.rollout_path.as_ref() else {
241
+ return false;
242
+ };
243
+ if agent
244
+ .get("fork_source_session_id")
245
+ .and_then(serde_json::Value::as_str)
246
+ == Some(session_id.as_str())
247
+ {
248
+ return false;
249
+ }
250
+ agent.insert(
251
+ "session_id".to_string(),
252
+ serde_json::json!(session_id.as_str()),
253
+ );
254
+ agent.insert(
255
+ "rollout_path".to_string(),
256
+ serde_json::json!(rollout_path.as_path().to_string_lossy()),
257
+ );
258
+ agent.insert(
259
+ "captured_at".to_string(),
260
+ serde_json::json!(chrono::Utc::now().to_rfc3339()),
261
+ );
262
+ agent.insert(
263
+ "captured_via".to_string(),
264
+ serde_json::to_value(captured.captured_via).unwrap_or(serde_json::Value::Null),
265
+ );
266
+ agent.insert(
267
+ "attribution_confidence".to_string(),
268
+ serde_json::to_value(captured.attribution_confidence).unwrap_or(serde_json::Value::Null),
269
+ );
270
+ agent.remove("_pending_session_id");
271
+ agent.remove("attribution_ambiguous");
272
+ agent.remove("fork_source_session_id");
273
+ agent.remove("pending_target_agent");
274
+ agent.remove("pending_grace_secs");
275
+ agent.insert("capture_state".to_string(), serde_json::json!("captured"));
276
+ true
277
+ }
278
+
175
279
  pub(super) fn verify_fork_registration(
176
280
  workspace: &Path,
177
281
  team_key: &str,
@@ -0,0 +1,109 @@
1
+ use super::*;
2
+
3
+ #[allow(clippy::too_many_arguments)]
4
+ pub(crate) fn upsert_pending_forked_agent_state(
5
+ state: &mut serde_json::Value,
6
+ source_agent_id: &AgentId,
7
+ as_agent_id: &AgentId,
8
+ spec_agent: &Value,
9
+ safety: &DangerousApproval,
10
+ plan: &crate::provider::CommandPlan,
11
+ profile_launch: &crate::provider::ProviderProfileLaunch,
12
+ spawn: &crate::transport::SpawnResult,
13
+ profile_dir: Option<&Path>,
14
+ dynamic_role_file: &Path,
15
+ pending: &crate::provider::session::PendingContextFork,
16
+ spawn_epoch: u64,
17
+ ) -> Result<(), LifecycleError> {
18
+ let root = state.as_object_mut().ok_or_else(|| {
19
+ LifecycleError::StatePersist("runtime state root is not an object".to_string())
20
+ })?;
21
+ let agents = root
22
+ .entry("agents".to_string())
23
+ .or_insert_with(|| serde_json::json!({}))
24
+ .as_object_mut()
25
+ .ok_or_else(|| {
26
+ LifecycleError::StatePersist("runtime state agents is not an object".to_string())
27
+ })?;
28
+ let mut entry = serde_json::Map::new();
29
+ entry.insert("status".to_string(), serde_json::json!("running"));
30
+ entry.insert(
31
+ "capture_state".to_string(),
32
+ serde_json::json!("pending_context_fork"),
33
+ );
34
+ entry.insert(
35
+ "agent_id".to_string(),
36
+ serde_json::json!(as_agent_id.as_str()),
37
+ );
38
+ entry.insert(
39
+ "window".to_string(),
40
+ serde_json::json!(as_agent_id.as_str()),
41
+ );
42
+ entry.insert(
43
+ "forked_from".to_string(),
44
+ serde_json::json!(source_agent_id.as_str()),
45
+ );
46
+ entry.insert(
47
+ "fork_source_session_id".to_string(),
48
+ serde_json::json!(pending.source_session_id.as_str()),
49
+ );
50
+ entry.insert(
51
+ "pending_target_agent".to_string(),
52
+ serde_json::json!(pending.target_agent),
53
+ );
54
+ entry.insert(
55
+ "dynamic_role_file".to_string(),
56
+ serde_json::json!(dynamic_role_file.to_string_lossy().to_string()),
57
+ );
58
+ entry.insert(
59
+ "role_source_ownership".to_string(),
60
+ serde_json::json!("managed"),
61
+ );
62
+ entry.insert(
63
+ "spawn_cwd".to_string(),
64
+ serde_json::json!(pending
65
+ .scanner_context
66
+ .spawn_cwd
67
+ .to_string_lossy()
68
+ .to_string()),
69
+ );
70
+ entry.insert(
71
+ "pane_id".to_string(),
72
+ serde_json::json!(spawn.pane_id.as_str()),
73
+ );
74
+ entry.insert(
75
+ "spawned_at".to_string(),
76
+ serde_json::json!(pending.spawned_at),
77
+ );
78
+ entry.insert("spawn_epoch".to_string(), serde_json::json!(spawn_epoch));
79
+ if let Some(pid) = spawn.child_pid {
80
+ entry.insert("pane_pid".to_string(), serde_json::json!(pid));
81
+ }
82
+ for key in [
83
+ "provider",
84
+ "auth_mode",
85
+ "model",
86
+ "profile",
87
+ "role",
88
+ "effort",
89
+ ] {
90
+ if let Some(value) = spec_agent.get(key) {
91
+ entry.insert(key.to_string(), yaml_value_to_json(value));
92
+ }
93
+ }
94
+ if spec_agent.get("profile").is_some() {
95
+ if let Some(profile_dir) = profile_dir {
96
+ entry.insert(
97
+ "_profile_dir".to_string(),
98
+ serde_json::json!(profile_dir.to_string_lossy().to_string()),
99
+ );
100
+ }
101
+ }
102
+ persist_command_plan_state(&mut entry, plan, profile_launch);
103
+ persist_effective_approval_policy(&mut entry, safety);
104
+ agents.insert(
105
+ as_agent_id.as_str().to_string(),
106
+ serde_json::Value::Object(entry),
107
+ );
108
+ Ok(())
109
+ }
@@ -14,6 +14,10 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
14
 
15
15
  use super::*;
16
16
 
17
+ #[path = "fork_pending.rs"]
18
+ mod fork_pending;
19
+ pub(super) use fork_pending::upsert_pending_forked_agent_state;
20
+
17
21
  pub(super) fn reserve_forked_agent_state(
18
22
  state: &mut serde_json::Value,
19
23
  source_agent_id: &AgentId,
@@ -134,6 +138,51 @@ pub(super) fn find_spec_agent<'a>(spec: &'a Value, agent_id: &AgentId) -> Option
134
138
  })
135
139
  }
136
140
 
141
+ pub(super) fn fork_spec_agent<'a>(spec: &'a Value, agent_id: &AgentId) -> Option<&'a Value> {
142
+ find_spec_agent(spec, agent_id)
143
+ }
144
+
145
+ pub(super) fn fork_source_tuple(
146
+ state: &serde_json::Value,
147
+ source_agent_id: &AgentId,
148
+ ) -> Result<(crate::provider::SessionId, PathBuf), LifecycleError> {
149
+ let source = state
150
+ .get("agents")
151
+ .and_then(|agents| agents.get(source_agent_id.as_str()))
152
+ .ok_or_else(|| {
153
+ LifecycleError::Provider(format!("unknown worker agent id: {source_agent_id}"))
154
+ })?;
155
+ let field = |name: &str| {
156
+ source
157
+ .get(name)
158
+ .and_then(serde_json::Value::as_str)
159
+ .filter(|value| !value.is_empty())
160
+ };
161
+ let (Some(session_id), Some(backing)) = (field("session_id"), field("rollout_path")) else {
162
+ return Err(LifecycleError::Provider(format!(
163
+ "cannot fork {source_agent_id}: source session backing is missing or incomplete \
164
+ (session_id+rollout_path+captured_at+captured_via required)"
165
+ )));
166
+ };
167
+ if field("captured_at").is_none() || field("captured_via").is_none() {
168
+ return Err(LifecycleError::Provider(format!(
169
+ "cannot fork {source_agent_id}: source session backing is missing or incomplete \
170
+ (session_id+rollout_path+captured_at+captured_via required)"
171
+ )));
172
+ }
173
+ let backing = PathBuf::from(backing);
174
+ if !backing.is_file() {
175
+ return Err(LifecycleError::Provider(format!(
176
+ "cannot fork {source_agent_id}: source session backing is not readable: {}",
177
+ backing.display()
178
+ )));
179
+ }
180
+ Ok((
181
+ crate::provider::SessionId::new(session_id.to_string()),
182
+ backing,
183
+ ))
184
+ }
185
+
137
186
  pub(super) fn append_forked_agent(
138
187
  spec: &Value,
139
188
  source_agent: &Value,
@@ -295,6 +295,7 @@ mod fork_state;
295
295
  pub(super) use fork_state::*;
296
296
 
297
297
  mod fork_finalize;
298
+ pub(crate) use fork_finalize::finalize_pending_fork_capture;
298
299
  pub(super) use fork_finalize::*;
299
300
 
300
301
  mod role_source;
@@ -664,6 +664,14 @@ pub struct ForkAgentReport {
664
664
  pub new_agent_id: AgentId,
665
665
  pub env: AgentActionEnvelope,
666
666
  pub session_id: Option<SessionId>,
667
+ pub backing_state: ForkBackingState,
668
+ }
669
+
670
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
671
+ #[serde(rename_all = "snake_case")]
672
+ pub enum ForkBackingState {
673
+ Verified,
674
+ PendingContextFork,
667
675
  }
668
676
 
669
677
  #[derive(Debug, Clone, PartialEq, Eq)]
@@ -130,6 +130,7 @@ pub(crate) fn fork_agent(
130
130
  "state_file": report.env.state_file.to_string_lossy().to_string(),
131
131
  "coordinator_started": report.env.coordinator_started,
132
132
  "session_id": report.session_id.as_ref().map(|session| session.as_str()),
133
+ "backing_state": report.backing_state,
133
134
  })),
134
135
  })
135
136
  }
@@ -469,13 +469,25 @@ fn classify_pending_capture_state(
469
469
  const TRIGGER_GRACE_SECS: i64 = 15;
470
470
  let past_spawn_grace = elapsed_since_spawn >= SPAWN_GRACE_SECS;
471
471
  let past_trigger_grace = has_trigger && elapsed_since_trigger >= TRIGGER_GRACE_SECS;
472
+ let fork_pending_expired = agent_obj.get("capture_state").and_then(Value::as_str)
473
+ == Some("pending_context_fork")
474
+ && crate::provider::session::transition_pending_context_fork(
475
+ has_trigger,
476
+ past_spawn_grace && past_trigger_grace,
477
+ )
478
+ .is_some();
472
479
  // transcript_missing requires BOTH: a trigger occurred AND we've
473
480
  // waited past the spawn grace (so we're not blamed for slow startup).
474
481
  // candidate_count == 0 is the "no backing" signal; non-zero
475
482
  // candidates that didn't satisfy assignment go through the ambiguous
476
483
  // path above and don't reach here.
477
- if has_trigger && past_spawn_grace && past_trigger_grace && candidate_count == 0 {
484
+ if (fork_pending_expired || has_trigger && past_spawn_grace && past_trigger_grace)
485
+ && candidate_count == 0
486
+ {
478
487
  "transcript_missing"
488
+ } else if agent_obj.get("capture_state").and_then(Value::as_str) == Some("pending_context_fork")
489
+ {
490
+ "pending_context_fork"
479
491
  } else {
480
492
  "pending_first_turn"
481
493
  }
@@ -1143,6 +1155,9 @@ fn apply_captured_session(
1143
1155
  agent_obj: &mut serde_json::Map<String, Value>,
1144
1156
  captured: &CapturedSession,
1145
1157
  ) -> bool {
1158
+ if agent_obj.get("capture_state").and_then(Value::as_str) == Some("pending_context_fork") {
1159
+ return crate::lifecycle::finalize_pending_fork_capture(agent_obj, captured);
1160
+ }
1146
1161
  let Some(session_id) = captured.session_id.as_ref() else {
1147
1162
  return false;
1148
1163
  };
@@ -1984,6 +1999,27 @@ mod u1_tests {
1984
1999
  );
1985
2000
  }
1986
2001
 
2002
+ #[test]
2003
+ fn pending_context_fork_keeps_typed_state_until_trigger_grace_expires() {
2004
+ let old = (chrono::Utc::now() - chrono::Duration::seconds(60)).to_rfc3339();
2005
+ let mut agent = serde_json::Map::new();
2006
+ agent.insert("spawned_at".to_string(), serde_json::json!(old));
2007
+ agent.insert(
2008
+ "capture_state".to_string(),
2009
+ serde_json::json!("pending_context_fork"),
2010
+ );
2011
+ assert_eq!(
2012
+ super::classify_pending_capture_state(&agent, 0),
2013
+ "pending_context_fork"
2014
+ );
2015
+ let trigger = (chrono::Utc::now() - chrono::Duration::seconds(20)).to_rfc3339();
2016
+ agent.insert("first_send_at".to_string(), serde_json::json!(trigger));
2017
+ assert_eq!(
2018
+ super::classify_pending_capture_state(&agent, 0),
2019
+ "transcript_missing"
2020
+ );
2021
+ }
2022
+
1987
2023
  #[test]
1988
2024
  fn canonical_pending_capture_preserves_legacy_missing_boundary() {
1989
2025
  let agent = serde_json::json!({
@@ -0,0 +1,85 @@
1
+ use super::*;
2
+
3
+ pub(super) fn verify_claude_fork(
4
+ provider: Provider,
5
+ source_session_id: &SessionId,
6
+ plan: &CommandPlan,
7
+ before: &ContextBackingSnapshot,
8
+ expected_backing_path: Option<&Path>,
9
+ spawn_cwd: &Path,
10
+ deadline: Duration,
11
+ ) -> Result<ContextForkProof, ContextForkTermination> {
12
+ let expected = plan.expected_session_id.as_ref().ok_or_else(|| {
13
+ ProviderError::CaptureFailed(
14
+ "context_fork_unverified: Claude plan has no expected session id".to_string(),
15
+ )
16
+ })?;
17
+ let path = expected_backing_path.ok_or_else(|| {
18
+ ProviderError::CaptureFailed(
19
+ "context_fork_unverified: Claude plan has no exact snapshot backing".to_string(),
20
+ )
21
+ })?;
22
+ let expected_name = format!("{}.jsonl", expected.as_str());
23
+ if path.file_name().and_then(|name| name.to_str()) != Some(expected_name.as_str()) {
24
+ return Err(ProviderError::CaptureFailed(format!(
25
+ "context_fork_unverified: Claude snapshot backing does not match expected session {}",
26
+ expected.as_str()
27
+ ))
28
+ .into());
29
+ }
30
+ let started = std::time::Instant::now();
31
+ loop {
32
+ if let Some(stamp) = file_stamp(path) {
33
+ let changed = before.files.get(path).is_none_or(|old| *old != stamp);
34
+ let observed_matches =
35
+ session_id_from_jsonl(path).is_none_or(|observed| observed == expected.as_str());
36
+ if changed && readable_jsonl(path) && observed_matches && expected != source_session_id
37
+ {
38
+ return Ok(ContextForkProof {
39
+ provider,
40
+ source_session_id: source_session_id.clone(),
41
+ new_session_id: expected.clone(),
42
+ backing_path: path.to_path_buf(),
43
+ captured_via: "context_fork_verified".to_string(),
44
+ attribution_confidence: "high".to_string(),
45
+ managed_backing_root: None,
46
+ });
47
+ }
48
+ }
49
+ if let Some(provider_path) = std::env::var_os("HOME")
50
+ .map(PathBuf::from)
51
+ .and_then(|home| {
52
+ crate::provider::session_scan::claude::projects_dir_for_cwd(&home, spawn_cwd)
53
+ })
54
+ .map(|root| root.join(&expected_name))
55
+ {
56
+ let observed_matches = session_id_from_jsonl(&provider_path)
57
+ .is_some_and(|observed| observed == expected.as_str());
58
+ let changed = file_stamp(&provider_path)
59
+ .is_some_and(|stamp| before.files.get(&provider_path) != Some(&stamp));
60
+ if changed
61
+ && readable_jsonl(&provider_path)
62
+ && observed_matches
63
+ && expected != source_session_id
64
+ {
65
+ return Ok(ContextForkProof {
66
+ provider,
67
+ source_session_id: source_session_id.clone(),
68
+ new_session_id: expected.clone(),
69
+ backing_path: provider_path,
70
+ captured_via: "context_fork_verified".to_string(),
71
+ attribution_confidence: "high".to_string(),
72
+ managed_backing_root: None,
73
+ });
74
+ }
75
+ }
76
+ if started.elapsed() >= deadline {
77
+ break;
78
+ }
79
+ std::thread::sleep(Duration::from_millis(50));
80
+ }
81
+ Err(ContextForkTermination::Timeout {
82
+ provider,
83
+ deadline_ms: deadline.as_millis(),
84
+ })
85
+ }
@@ -0,0 +1,65 @@
1
+ use super::*;
2
+
3
+ pub(super) fn verify_codex_fork(
4
+ source_session_id: &SessionId,
5
+ plan: &CommandPlan,
6
+ before: &ContextBackingSnapshot,
7
+ agent_id: &str,
8
+ spawn_cwd: &Path,
9
+ spawned_at: &str,
10
+ deadline: Duration,
11
+ ) -> Result<ContextForkProof, ContextForkTermination> {
12
+ let context = crate::provider::session_scan::CaptureSessionContext {
13
+ agent_id: agent_id.to_string(),
14
+ spawn_cwd: spawn_cwd.to_path_buf(),
15
+ pane_id: None,
16
+ pane_pid: None,
17
+ spawned_at: Some(spawned_at.to_string()),
18
+ expected_session_id: plan.expected_session_id.clone(),
19
+ provider_projects_root: plan.provider_projects_root.clone(),
20
+ };
21
+ let excluded = outcome::source_exclusions(before, source_session_id);
22
+ let started = std::time::Instant::now();
23
+ loop {
24
+ let current = jsonl_files(&before.root);
25
+ for candidate in
26
+ crate::provider::session_scan::scan_session_candidates_once(Provider::Codex, &context)?
27
+ {
28
+ let Some(path) = candidate.captured.rollout_path.as_ref() else {
29
+ continue;
30
+ };
31
+ let Some(stamp) = current.get(path.as_path()) else {
32
+ continue;
33
+ };
34
+ let snapshot_changed = before.files.get(path.as_path()) != Some(stamp);
35
+ if !snapshot_changed {
36
+ continue;
37
+ }
38
+ let Some(new_session_id) = candidate.captured.session_id else {
39
+ continue;
40
+ };
41
+ if excluded.contains(new_session_id.as_str())
42
+ || excluded.contains(&path.as_path().to_string_lossy().to_string())
43
+ {
44
+ continue;
45
+ }
46
+ return Ok(ContextForkProof {
47
+ provider: Provider::Codex,
48
+ source_session_id: source_session_id.clone(),
49
+ new_session_id,
50
+ backing_path: path.as_path().to_path_buf(),
51
+ captured_via: "context_fork_verified".to_string(),
52
+ attribution_confidence: "high".to_string(),
53
+ managed_backing_root: None,
54
+ });
55
+ }
56
+ if started.elapsed() >= deadline {
57
+ break;
58
+ }
59
+ std::thread::sleep(Duration::from_millis(50));
60
+ }
61
+ Err(ContextForkTermination::Timeout {
62
+ provider: Provider::Codex,
63
+ deadline_ms: deadline.as_millis(),
64
+ })
65
+ }
@@ -0,0 +1,138 @@
1
+ use super::*;
2
+ use std::collections::BTreeSet;
3
+
4
+ pub(super) fn source_exclusions(
5
+ before: &ContextBackingSnapshot,
6
+ source_session_id: &SessionId,
7
+ ) -> BTreeSet<String> {
8
+ let mut excluded = BTreeSet::from([source_session_id.as_str().to_string()]);
9
+ for path in before.files.keys() {
10
+ if session_id_from_jsonl(path).as_deref() == Some(source_session_id.as_str()) {
11
+ excluded.insert(path.to_string_lossy().to_string());
12
+ }
13
+ }
14
+ excluded
15
+ }
16
+
17
+ #[derive(Debug, Clone)]
18
+ pub(crate) struct PendingContextFork {
19
+ pub source_session_id: SessionId,
20
+ pub target_agent: String,
21
+ pub spawned_at: String,
22
+ pub scanner_context: crate::provider::session_scan::CaptureSessionContext,
23
+ }
24
+
25
+ #[derive(Debug)]
26
+ pub(crate) enum ContextForkOutcome {
27
+ Verified(ContextForkProof),
28
+ Pending(PendingContextFork),
29
+ Rejected(ProviderError),
30
+ }
31
+
32
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
33
+ pub(crate) enum ContextForkPendingFailure {
34
+ TranscriptMissing,
35
+ }
36
+
37
+ pub(crate) fn transition_pending_context_fork(
38
+ triggered: bool,
39
+ grace_expired: bool,
40
+ ) -> Option<ContextForkPendingFailure> {
41
+ (triggered && grace_expired).then_some(ContextForkPendingFailure::TranscriptMissing)
42
+ }
43
+
44
+ pub(crate) fn observe_context_fork(
45
+ provider: Provider,
46
+ source_session_id: &SessionId,
47
+ plan: &CommandPlan,
48
+ before: &ContextBackingSnapshot,
49
+ expected_backing_path: Option<&Path>,
50
+ agent_id: &str,
51
+ spawn_cwd: &Path,
52
+ spawned_at: &str,
53
+ deadline: Duration,
54
+ ) -> ContextForkOutcome {
55
+ match verify_context_fork(
56
+ provider,
57
+ source_session_id,
58
+ plan,
59
+ before,
60
+ expected_backing_path,
61
+ agent_id,
62
+ spawn_cwd,
63
+ spawned_at,
64
+ deadline,
65
+ ) {
66
+ Ok(proof) => ContextForkOutcome::Verified(proof),
67
+ Err(ContextForkTermination::Timeout { .. }) => {
68
+ ContextForkOutcome::Pending(PendingContextFork {
69
+ source_session_id: source_session_id.clone(),
70
+ target_agent: agent_id.to_string(),
71
+ spawned_at: spawned_at.to_string(),
72
+ scanner_context: crate::provider::session_scan::CaptureSessionContext {
73
+ agent_id: agent_id.to_string(),
74
+ spawn_cwd: spawn_cwd.to_path_buf(),
75
+ pane_id: None,
76
+ pane_pid: None,
77
+ spawned_at: Some(spawned_at.to_string()),
78
+ expected_session_id: plan.expected_session_id.clone(),
79
+ provider_projects_root: plan.provider_projects_root.clone(),
80
+ },
81
+ })
82
+ }
83
+ Err(ContextForkTermination::Rejected(error)) => ContextForkOutcome::Rejected(error),
84
+ }
85
+ }
86
+
87
+ #[cfg(test)]
88
+ mod tests {
89
+ use super::*;
90
+
91
+ #[test]
92
+ fn convergence_deadlines_are_provider_specific() {
93
+ assert_eq!(
94
+ context_fork_convergence_deadline(Provider::Claude),
95
+ Duration::from_secs(45)
96
+ );
97
+ assert_eq!(
98
+ context_fork_convergence_deadline(Provider::Codex),
99
+ Duration::from_secs(10)
100
+ );
101
+ assert_eq!(
102
+ context_fork_convergence_deadline(Provider::Copilot),
103
+ Duration::from_secs(5)
104
+ );
105
+ }
106
+
107
+ #[test]
108
+ fn codex_without_new_backing_is_typed_pending() {
109
+ let root =
110
+ std::env::temp_dir().join(format!("ta-codex-fork-pending-{}", std::process::id()));
111
+ let _ = std::fs::remove_dir_all(&root);
112
+ std::fs::create_dir_all(&root).unwrap();
113
+ let plan = CommandPlan {
114
+ argv: Vec::new(),
115
+ expected_session_id: None,
116
+ provider_projects_root: Some(root.clone()),
117
+ managed_mcp_config: false,
118
+ };
119
+ let before = ContextBackingSnapshot::capture(Provider::Codex, &plan);
120
+ let outcome = observe_context_fork(
121
+ Provider::Codex,
122
+ &SessionId::new("source-session"),
123
+ &plan,
124
+ &before,
125
+ None,
126
+ "fork",
127
+ &root,
128
+ "2026-07-24T00:00:00Z",
129
+ Duration::ZERO,
130
+ );
131
+ let ContextForkOutcome::Pending(pending) = outcome else {
132
+ panic!("missing backing must remain a typed pending fork")
133
+ };
134
+ assert_eq!(pending.source_session_id.as_str(), "source-session");
135
+ assert_eq!(pending.target_agent, "fork");
136
+ let _ = std::fs::remove_dir_all(&root);
137
+ }
138
+ }
@@ -5,7 +5,28 @@ use std::time::{Duration, SystemTime};
5
5
  use crate::model::enums::Provider;
6
6
  use crate::provider::{CommandPlan, ProviderError, SessionId};
7
7
 
8
+ #[derive(Debug, thiserror::Error)]
9
+ pub(crate) enum ContextForkTermination {
10
+ #[error(
11
+ "context_fork_unverified: {provider:?} produced no readable NEW session backing within {deadline_ms}ms"
12
+ )]
13
+ Timeout {
14
+ provider: Provider,
15
+ deadline_ms: u128,
16
+ },
17
+ #[error(transparent)]
18
+ Rejected(#[from] ProviderError),
19
+ }
20
+
21
+ mod claude;
22
+ mod codex;
23
+ mod outcome;
24
+ pub(crate) use outcome::{
25
+ observe_context_fork, transition_pending_context_fork, ContextForkOutcome, PendingContextFork,
26
+ };
27
+
8
28
  pub(crate) fn context_fork_convergence_deadline(provider: Provider) -> Duration {
29
+ // Expiration is consumed by ContextForkOutcome::Pending(PendingContextFork).
9
30
  match provider {
10
31
  Provider::Claude | Provider::ClaudeCode => Duration::from_secs(45),
11
32
  Provider::Codex => Duration::from_secs(10),
@@ -54,12 +75,12 @@ pub(crate) fn verify_context_fork(
54
75
  spawn_cwd: &Path,
55
76
  spawned_at: &str,
56
77
  deadline: Duration,
57
- ) -> Result<ContextForkProof, ProviderError> {
78
+ ) -> Result<ContextForkProof, ContextForkTermination> {
58
79
  if provider == Provider::Copilot {
59
- return verify_copilot_fork(source_session_id, plan);
80
+ return verify_copilot_fork(source_session_id, plan).map_err(Into::into);
60
81
  }
61
82
  if provider == Provider::Codex {
62
- return verify_codex_fork(
83
+ return codex::verify_codex_fork(
63
84
  source_session_id,
64
85
  plan,
65
86
  before,
@@ -70,132 +91,20 @@ pub(crate) fn verify_context_fork(
70
91
  );
71
92
  }
72
93
  if matches!(provider, Provider::Claude | Provider::ClaudeCode) {
73
- return verify_claude_fork(
94
+ return claude::verify_claude_fork(
74
95
  provider,
75
96
  source_session_id,
76
97
  plan,
77
98
  before,
78
99
  expected_backing_path,
100
+ spawn_cwd,
79
101
  deadline,
80
102
  );
81
103
  }
82
104
  Err(ProviderError::CaptureFailed(format!(
83
105
  "context_fork_unverified: {provider:?} has no verifiable fork backing"
84
- )))
85
- }
86
- fn verify_claude_fork(
87
- provider: Provider,
88
- source_session_id: &SessionId,
89
- plan: &CommandPlan,
90
- before: &ContextBackingSnapshot,
91
- expected_backing_path: Option<&Path>,
92
- deadline: Duration,
93
- ) -> Result<ContextForkProof, ProviderError> {
94
- let expected = plan.expected_session_id.as_ref().ok_or_else(|| {
95
- ProviderError::CaptureFailed(
96
- "context_fork_unverified: Claude plan has no expected session id".to_string(),
97
- )
98
- })?;
99
- let path = expected_backing_path.ok_or_else(|| {
100
- ProviderError::CaptureFailed(
101
- "context_fork_unverified: Claude plan has no exact snapshot backing".to_string(),
102
- )
103
- })?;
104
- let expected_name = format!("{}.jsonl", expected.as_str());
105
- if path.file_name().and_then(|name| name.to_str()) != Some(expected_name.as_str()) {
106
- return Err(ProviderError::CaptureFailed(format!(
107
- "context_fork_unverified: Claude snapshot backing does not match expected session {}",
108
- expected.as_str()
109
- )));
110
- }
111
- let started = std::time::Instant::now();
112
- loop {
113
- if let Some(stamp) = file_stamp(path) {
114
- let changed = before.files.get(path).is_none_or(|old| *old != stamp);
115
- let observed_matches =
116
- session_id_from_jsonl(path).is_none_or(|observed| observed == expected.as_str());
117
- if changed && readable_jsonl(path) && observed_matches && expected != source_session_id
118
- {
119
- return Ok(ContextForkProof {
120
- provider,
121
- source_session_id: source_session_id.clone(),
122
- new_session_id: expected.clone(),
123
- backing_path: path.to_path_buf(),
124
- captured_via: "context_fork_verified".to_string(),
125
- attribution_confidence: "high".to_string(),
126
- managed_backing_root: None,
127
- });
128
- }
129
- }
130
- if started.elapsed() >= deadline {
131
- break;
132
- }
133
- std::thread::sleep(Duration::from_millis(50));
134
- }
135
- Err(ProviderError::CaptureFailed(format!(
136
- "context_fork_unverified: {provider:?} produced no readable NEW session backing within {}ms",
137
- deadline.as_millis()
138
- )))
139
- }
140
-
141
- fn verify_codex_fork(
142
- source_session_id: &SessionId,
143
- plan: &CommandPlan,
144
- before: &ContextBackingSnapshot,
145
- agent_id: &str,
146
- spawn_cwd: &Path,
147
- spawned_at: &str,
148
- deadline: Duration,
149
- ) -> Result<ContextForkProof, ProviderError> {
150
- let context = crate::provider::session_scan::CaptureSessionContext {
151
- agent_id: agent_id.to_string(),
152
- spawn_cwd: spawn_cwd.to_path_buf(),
153
- pane_id: None,
154
- pane_pid: None,
155
- spawned_at: Some(spawned_at.to_string()),
156
- expected_session_id: plan.expected_session_id.clone(),
157
- provider_projects_root: plan.provider_projects_root.clone(),
158
- };
159
- let started = std::time::Instant::now();
160
- loop {
161
- let current = jsonl_files(&before.root);
162
- for candidate in
163
- crate::provider::session_scan::scan_session_candidates_once(Provider::Codex, &context)?
164
- {
165
- let Some(path) = candidate.captured.rollout_path.as_ref() else {
166
- continue;
167
- };
168
- let Some(stamp) = current.get(path.as_path()) else {
169
- continue;
170
- };
171
- if before.files.get(path.as_path()) == Some(stamp) {
172
- continue;
173
- }
174
- let Some(new_session_id) = candidate.captured.session_id else {
175
- continue;
176
- };
177
- if new_session_id == *source_session_id {
178
- continue;
179
- }
180
- return Ok(ContextForkProof {
181
- provider: Provider::Codex,
182
- source_session_id: source_session_id.clone(),
183
- new_session_id,
184
- backing_path: path.as_path().to_path_buf(),
185
- captured_via: "context_fork_verified".to_string(),
186
- attribution_confidence: "high".to_string(),
187
- managed_backing_root: None,
188
- });
189
- }
190
- if started.elapsed() >= deadline {
191
- break;
192
- }
193
- std::thread::sleep(Duration::from_millis(50));
194
- }
195
- Err(ProviderError::CaptureFailed(format!(
196
- "context_fork_unverified: Codex produced no readable NEW session backing within {}ms",
197
- deadline.as_millis()
198
- )))
106
+ ))
107
+ .into())
199
108
  }
200
109
 
201
110
  fn verify_copilot_fork(
@@ -242,6 +151,8 @@ fn verify_copilot_fork(
242
151
  "context_fork_unverified: copilot NEW session is absent from session-store".to_string(),
243
152
  ));
244
153
  }
154
+ // Copilot's isolated database row is its synchronous fork proof; the
155
+ // pre-spawn materialization baseline is never accepted by transcript scan.
245
156
  Ok(ContextForkProof {
246
157
  provider: Provider::Copilot,
247
158
  source_session_id: source_session_id.clone(),
@@ -335,22 +246,6 @@ mod tests {
335
246
  use super::*;
336
247
  use std::io::Write;
337
248
 
338
- #[test]
339
- fn context_fork_deadlines_are_provider_specific() {
340
- assert_eq!(
341
- context_fork_convergence_deadline(Provider::Claude),
342
- Duration::from_secs(45)
343
- );
344
- assert_eq!(
345
- context_fork_convergence_deadline(Provider::Codex),
346
- Duration::from_secs(10)
347
- );
348
- assert_eq!(
349
- context_fork_convergence_deadline(Provider::Copilot),
350
- Duration::from_secs(5)
351
- );
352
- }
353
-
354
249
  #[test]
355
250
  fn codex_fork_proof_ignores_changed_stale_and_foreign_rollouts() {
356
251
  let root =
@@ -10,7 +10,8 @@ pub mod resume;
10
10
 
11
11
  pub use context_fork::ContextForkProof;
12
12
  pub(crate) use context_fork::{
13
- context_fork_convergence_deadline, verify_context_fork, ContextBackingSnapshot,
13
+ context_fork_convergence_deadline, observe_context_fork, transition_pending_context_fork,
14
+ ContextBackingSnapshot, ContextForkOutcome, PendingContextFork,
14
15
  };
15
16
 
16
17
  pub use resume::{
@@ -5,7 +5,7 @@ use crate::provider::Provider;
5
5
 
6
6
  use super::{CaptureSessionContext, CapturedSessionCandidate};
7
7
 
8
- pub(super) fn projects_dir_for_cwd(home: &Path, spawn_cwd: &Path) -> Option<PathBuf> {
8
+ pub(crate) fn projects_dir_for_cwd(home: &Path, spawn_cwd: &Path) -> Option<PathBuf> {
9
9
  let canonical = std::fs::canonicalize(spawn_cwd).unwrap_or_else(|_| spawn_cwd.to_path_buf());
10
10
  let encoded = encode_projects_dir(&canonical.to_string_lossy());
11
11
  if encoded.is_empty() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.57",
3
+ "version": "0.5.58",
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.57",
24
- "@team-agent/cli-darwin-x64": "0.5.57",
25
- "@team-agent/cli-linux-x64": "0.5.57"
23
+ "@team-agent/cli-darwin-arm64": "0.5.58",
24
+ "@team-agent/cli-darwin-x64": "0.5.58",
25
+ "@team-agent/cli-linux-x64": "0.5.58"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",