@team-agent/installer 0.5.59 → 0.5.60

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.59"
578
+ version = "0.5.60"
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.59"
12
+ version = "0.5.60"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -269,12 +269,17 @@ impl Coordinator {
269
269
  }
270
270
 
271
271
  self.record_step("capture_missing");
272
- if let Err(error) = self.capture_missing_sessions(&mut state, &event_log) {
273
- let _ = event_log.write(
274
- "coordinator.tick.capture_missing_failed",
275
- serde_json::json!({"error": error.to_string()}),
276
- );
277
- }
272
+ let pending_context_fork_audits =
273
+ match self.capture_missing_sessions(&mut state, &event_log) {
274
+ Ok(audits) => audits,
275
+ Err(error) => {
276
+ let _ = event_log.write(
277
+ "coordinator.tick.capture_missing_failed",
278
+ serde_json::json!({"error": error.to_string()}),
279
+ );
280
+ Vec::new()
281
+ }
282
+ };
278
283
 
279
284
  // Slice 1 energy gate: one pane snapshot per tick feeds probe eligibility,
280
285
  // health sync, and abnormal-exit detection. Missing panes are filtered
@@ -454,6 +459,14 @@ impl Coordinator {
454
459
  collections,
455
460
  ));
456
461
  }
462
+ for context_fork in &pending_context_fork_audits {
463
+ context_fork.write_audit(&event_log).map_err(|error| {
464
+ eprintln!(
465
+ "[coordinator] context_fork audit publish failed after state commit: {error}"
466
+ );
467
+ TickError::EventLog(error)
468
+ })?;
469
+ }
457
470
 
458
471
  // 0.5.36 (`.team/artifacts/supermarket-api-error-recovery-locate.md` §7.3):
459
472
  // post-save recovery step. Reloads fresh state, consumes due
@@ -487,7 +500,7 @@ impl Coordinator {
487
500
  &self,
488
501
  state: &mut Value,
489
502
  event_log: &EventLog,
490
- ) -> Result<(), TickError> {
503
+ ) -> Result<Vec<crate::lifecycle::launch::ContextForkFinalized>, TickError> {
491
504
  let report = crate::session_capture::capture_missing_provider_sessions_once(
492
505
  state,
493
506
  &mut |provider| self.provider_registry.adapter_for(provider),
@@ -651,7 +664,7 @@ impl Coordinator {
651
664
  }),
652
665
  )?;
653
666
  }
654
- Ok(())
667
+ Ok(report.context_forks)
655
668
  }
656
669
 
657
670
  fn sync_agent_health(
@@ -0,0 +1,58 @@
1
+ use super::super::*;
2
+
3
+ pub(super) struct CompleteForkInput<'a> {
4
+ pub workspace: &'a Path,
5
+ pub team_key: &'a str,
6
+ pub agent_id: &'a AgentId,
7
+ pub spawn: &'a crate::transport::SpawnResult,
8
+ pub window: &'a WindowName,
9
+ pub transport: &'a dyn Transport,
10
+ pub session_name: &'a SessionName,
11
+ pub mcp_config_path: &'a Path,
12
+ pub profile_launch: &'a crate::provider::ProviderProfileLaunch,
13
+ pub materialized_role: &'a mut MaterializedRole,
14
+ pub claude_fork:
15
+ &'a mut Option<crate::provider::adapters::claude_fork::ClaudeForkMaterialization>,
16
+ pub copilot_fork:
17
+ &'a mut Option<crate::provider::adapters::copilot_fork::CopilotForkMaterialization>,
18
+ }
19
+
20
+ pub(super) fn complete_fork(input: CompleteForkInput<'_>) -> Result<bool, LifecycleError> {
21
+ if let Err(error) = verify_fork_registration(
22
+ input.workspace,
23
+ input.team_key,
24
+ input.agent_id,
25
+ input.spawn,
26
+ input.window,
27
+ ) {
28
+ rollback_fork_after_spawn(
29
+ input.workspace,
30
+ input.transport,
31
+ input.session_name,
32
+ input.window,
33
+ input.mcp_config_path,
34
+ input.agent_id,
35
+ input.profile_launch,
36
+ input.team_key,
37
+ );
38
+ return Err(error);
39
+ }
40
+ let coordinator_started = start_fork_coordinator(ForkCoordinatorInput {
41
+ workspace: input.workspace,
42
+ team_key: input.team_key,
43
+ agent_id: input.agent_id,
44
+ transport: input.transport,
45
+ session_name: input.session_name,
46
+ window: input.window,
47
+ mcp_config_path: input.mcp_config_path,
48
+ profile_launch: input.profile_launch,
49
+ })?;
50
+ input.materialized_role.keep();
51
+ if let Some(materialized) = input.claude_fork.as_mut() {
52
+ materialized.keep();
53
+ }
54
+ if let Some(materialized) = input.copilot_fork.as_mut() {
55
+ materialized.keep();
56
+ }
57
+ Ok(coordinator_started)
58
+ }
@@ -1,16 +1,7 @@
1
1
  use super::*;
2
- use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
3
2
  use crate::lifecycle::profile_launch::parse_provider;
4
- use crate::lifecycle::*;
5
- use crate::model::enums::{AuthMode, DisplayBackend, Provider, ProviderEffort};
6
- use crate::model::ids::AgentId;
7
- use crate::model::permissions::{self, AgentPermissionInput};
8
- use crate::model::yaml::{self, Value};
9
- use crate::state::persist::load_runtime_state;
10
- use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
11
- use std::collections::{BTreeMap, BTreeSet};
12
- use std::path::{Path, PathBuf};
13
- use std::process::Command;
3
+
4
+ mod completion;
14
5
 
15
6
  pub fn fork_agent_with_transport(
16
7
  workspace: &Path,
@@ -239,6 +230,23 @@ pub fn fork_agent_with_transport(
239
230
  as_agent_id.as_str(),
240
231
  Some(&fork_team),
241
232
  );
233
+ let spawned_at = spawn_timestamp_for_agent(1);
234
+ let mut fork_backing = adapter
235
+ .materialize_fork_backing(
236
+ &source_backing,
237
+ &session_id,
238
+ source_agent_id.as_str(),
239
+ as_agent_id.as_str(),
240
+ &mut plan,
241
+ )
242
+ .map_err(|error| {
243
+ let _ = std::fs::write(&spec_path, text.as_bytes());
244
+ cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
245
+ LifecycleError::Provider(error.to_string())
246
+ })?;
247
+ let mut expected_backing_path = fork_backing
248
+ .as_ref()
249
+ .map(|materialized| materialized.path().to_path_buf());
242
250
  if matches!(provider, Provider::Claude | Provider::ClaudeCode) {
243
251
  plan.provider_projects_root = source_backing.parent().map(Path::to_path_buf);
244
252
  }
@@ -248,12 +256,19 @@ pub fn fork_agent_with_transport(
248
256
  &plan,
249
257
  &source_backing,
250
258
  &session_id,
259
+ source_agent_id,
260
+ as_agent_id,
251
261
  )
252
262
  .map_err(|error| {
253
263
  let _ = std::fs::write(&spec_path, text.as_bytes());
254
264
  cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
255
265
  error
256
266
  })?;
267
+ if expected_backing_path.is_none() {
268
+ expected_backing_path = claude_fork
269
+ .as_ref()
270
+ .map(|materialized| materialized.path().to_path_buf());
271
+ }
257
272
  // The framework-created Claude snapshot is only fork input, not provider
258
273
  // proof. Observe changes made after materialization so a spawn-only
259
274
  // provider cannot turn the copied source backing into a Verified result.
@@ -307,7 +322,6 @@ pub fn fork_agent_with_transport(
307
322
  // Release the metadata lock before per-seat provider convergence; finalize reacquires it.
308
323
  drop(_lock);
309
324
  let spawn_epoch = 1_u64;
310
- let spawned_at = spawn_timestamp_for_agent(u32::try_from(spawn_epoch).unwrap_or(u32::MAX));
311
325
  let spawn_result = if session_live {
312
326
  transport.spawn_into_with_env_unset(
313
327
  &session_name,
@@ -343,6 +357,9 @@ pub fn fork_agent_with_transport(
343
357
  return Err(LifecycleError::Transport(error.to_string()));
344
358
  }
345
359
  };
360
+ if let Some(materialized) = fork_backing.as_mut() {
361
+ materialized.handoff();
362
+ }
346
363
  ensure_fork_spawn_live(ForkPostSpawnInput {
347
364
  workspace: &workspace,
348
365
  transport,
@@ -361,7 +378,8 @@ pub fn fork_agent_with_transport(
361
378
  &session_id,
362
379
  &plan,
363
380
  &backing_before,
364
- claude_fork.as_ref().map(|materialized| materialized.path()),
381
+ expected_backing_path.as_deref(),
382
+ source_agent_id.as_str(),
365
383
  as_agent_id.as_str(),
366
384
  &workspace,
367
385
  &spawned_at,
@@ -414,7 +432,7 @@ pub fn fork_agent_with_transport(
414
432
  }
415
433
  };
416
434
  if let Some(context_proof) = context_proof.as_ref() {
417
- if let Err(error) = finalize_fork_state(ForkFinalizeInput {
435
+ let finalized = match finalize_fork_state(ForkFinalizeInput {
418
436
  workspace: &workspace,
419
437
  team_key: &fork_team,
420
438
  source_agent_id,
@@ -430,51 +448,39 @@ pub fn fork_agent_with_transport(
430
448
  spawned_at: &spawned_at,
431
449
  spawn_epoch,
432
450
  }) {
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
- }
445
- }
446
- if let Err(error) =
447
- verify_fork_registration(&workspace, &fork_team, as_agent_id, &spawn, &window)
448
- {
449
- rollback_fork_after_spawn(
450
- &workspace,
451
- transport,
452
- &session_name,
453
- &window,
454
- &mcp_config_path,
455
- as_agent_id,
456
- &profile_launch,
457
- &fork_team,
458
- );
459
- return Err(error);
451
+ Ok(finalized) => finalized,
452
+ Err(error) => {
453
+ rollback_fork_after_spawn(
454
+ &workspace,
455
+ transport,
456
+ &session_name,
457
+ &window,
458
+ &mcp_config_path,
459
+ as_agent_id,
460
+ &profile_launch,
461
+ &fork_team,
462
+ );
463
+ return Err(error);
464
+ }
465
+ };
466
+ finalized
467
+ .write_audit(&crate::event_log::EventLog::new(&workspace))
468
+ .map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
460
469
  }
461
- let coordinator_started = start_fork_coordinator(ForkCoordinatorInput {
470
+ let coordinator_started = completion::complete_fork(completion::CompleteForkInput {
462
471
  workspace: &workspace,
463
472
  team_key: &fork_team,
464
473
  agent_id: as_agent_id,
474
+ spawn: &spawn,
475
+ window: &window,
465
476
  transport,
466
477
  session_name: &session_name,
467
- window: &window,
468
478
  mcp_config_path: &mcp_config_path,
469
479
  profile_launch: &profile_launch,
480
+ materialized_role: &mut materialized_role,
481
+ claude_fork: &mut claude_fork,
482
+ copilot_fork: &mut copilot_fork,
470
483
  })?;
471
- materialized_role.keep();
472
- if let Some(materialized) = claude_fork.as_mut() {
473
- materialized.keep();
474
- }
475
- if let Some(materialized) = copilot_fork.as_mut() {
476
- materialized.keep();
477
- }
478
484
  let backing_state = if context_proof.is_some() {
479
485
  ForkBackingState::Verified
480
486
  } else {
@@ -44,6 +44,8 @@ pub(super) fn prepare_claude_fork_backing(
44
44
  plan: &crate::provider::CommandPlan,
45
45
  source_backing: &Path,
46
46
  source_session_id: &crate::provider::SessionId,
47
+ source_agent_id: &AgentId,
48
+ target_agent_id: &AgentId,
47
49
  ) -> Result<Option<crate::provider::adapters::claude_fork::ClaudeForkMaterialization>, LifecycleError>
48
50
  {
49
51
  if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
@@ -56,6 +58,8 @@ pub(super) fn prepare_claude_fork_backing(
56
58
  source_backing,
57
59
  source_session_id,
58
60
  target_session_id,
61
+ source_agent_id.as_str(),
62
+ target_agent_id.as_str(),
59
63
  )
60
64
  .map(Some)
61
65
  .map_err(|error| {
@@ -122,7 +126,75 @@ pub(super) struct ForkFinalizeInput<'a> {
122
126
  pub spawn_epoch: u64,
123
127
  }
124
128
 
125
- pub(super) fn finalize_fork_state(input: ForkFinalizeInput<'_>) -> Result<(), LifecycleError> {
129
+ #[derive(Debug, Clone, PartialEq, Eq)]
130
+ pub(crate) struct ContextForkFinalized {
131
+ source_agent_id: String,
132
+ agent_id: String,
133
+ session_id: String,
134
+ rollout_path: String,
135
+ captured_via: String,
136
+ attribution_confidence: String,
137
+ }
138
+
139
+ impl ContextForkFinalized {
140
+ fn new(
141
+ source_agent_id: &str,
142
+ agent_id: &str,
143
+ session_id: &str,
144
+ rollout_path: &Path,
145
+ captured_via: &str,
146
+ attribution_confidence: &str,
147
+ ) -> Self {
148
+ Self {
149
+ source_agent_id: source_agent_id.to_string(),
150
+ agent_id: agent_id.to_string(),
151
+ session_id: session_id.to_string(),
152
+ rollout_path: rollout_path.to_string_lossy().to_string(),
153
+ captured_via: captured_via.to_string(),
154
+ attribution_confidence: attribution_confidence.to_string(),
155
+ }
156
+ }
157
+
158
+ fn from_captured(
159
+ source_agent_id: &str,
160
+ agent_id: &str,
161
+ captured: &crate::provider::CapturedSession,
162
+ ) -> Option<Self> {
163
+ let captured_via = serde_json::to_value(captured.captured_via).ok()?;
164
+ let attribution_confidence = serde_json::to_value(captured.attribution_confidence).ok()?;
165
+ Some(Self::new(
166
+ source_agent_id,
167
+ agent_id,
168
+ captured.session_id.as_ref()?.as_str(),
169
+ captured.rollout_path.as_ref()?.as_path(),
170
+ captured_via.as_str()?,
171
+ attribution_confidence.as_str()?,
172
+ ))
173
+ }
174
+
175
+ pub(crate) fn write_audit(
176
+ &self,
177
+ event_log: &crate::event_log::EventLog,
178
+ ) -> Result<(), crate::event_log::EventLogError> {
179
+ event_log
180
+ .write(
181
+ crate::lifecycle::types::event_names::CONTEXT_FORK,
182
+ serde_json::json!({
183
+ "source_agent_id": self.source_agent_id,
184
+ "agent_id": self.agent_id,
185
+ "session_id": self.session_id,
186
+ "rollout_path": self.rollout_path,
187
+ "captured_via": self.captured_via,
188
+ "attribution_confidence": self.attribution_confidence,
189
+ }),
190
+ )
191
+ .map(|_| ())
192
+ }
193
+ }
194
+
195
+ pub(super) fn finalize_fork_state(
196
+ input: ForkFinalizeInput<'_>,
197
+ ) -> Result<ContextForkFinalized, LifecycleError> {
126
198
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
127
199
  workspace: input.workspace,
128
200
  operation: "fork-agent-finalize",
@@ -169,7 +241,15 @@ pub(super) fn finalize_fork_state(input: ForkFinalizeInput<'_>) -> Result<(), Li
169
241
  },
170
242
  &next_state,
171
243
  )
172
- .map_err(|error| LifecycleError::StatePersist(error.to_string()))
244
+ .map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
245
+ Ok(ContextForkFinalized::new(
246
+ input.source_agent_id.as_str(),
247
+ input.agent_id.as_str(),
248
+ input.context_proof.new_session_id.as_str(),
249
+ &input.context_proof.backing_path,
250
+ &input.context_proof.captured_via,
251
+ &input.context_proof.attribution_confidence,
252
+ ))
173
253
  }
174
254
 
175
255
  pub(super) struct ForkPendingFinalizeInput<'a> {
@@ -233,20 +313,36 @@ pub(super) fn finalize_pending_fork_state(
233
313
  pub(crate) fn finalize_pending_fork_capture(
234
314
  agent: &mut serde_json::Map<String, serde_json::Value>,
235
315
  captured: &crate::provider::CapturedSession,
236
- ) -> bool {
316
+ ) -> Option<ContextForkFinalized> {
237
317
  let Some(session_id) = captured.session_id.as_ref() else {
238
- return false;
318
+ return None;
239
319
  };
240
320
  let Some(rollout_path) = captured.rollout_path.as_ref() else {
241
- return false;
321
+ return None;
242
322
  };
243
323
  if agent
244
324
  .get("fork_source_session_id")
245
325
  .and_then(serde_json::Value::as_str)
246
326
  == Some(session_id.as_str())
247
327
  {
248
- return false;
328
+ return None;
249
329
  }
330
+ let finalized = ContextForkFinalized::from_captured(
331
+ agent
332
+ .get("forked_from")
333
+ .and_then(serde_json::Value::as_str)
334
+ .unwrap_or_default(),
335
+ agent
336
+ .get("agent_id")
337
+ .and_then(serde_json::Value::as_str)
338
+ .or_else(|| {
339
+ agent
340
+ .get("pending_target_agent")
341
+ .and_then(serde_json::Value::as_str)
342
+ })
343
+ .unwrap_or_default(),
344
+ captured,
345
+ )?;
250
346
  agent.insert(
251
347
  "session_id".to_string(),
252
348
  serde_json::json!(session_id.as_str()),
@@ -269,11 +365,55 @@ pub(crate) fn finalize_pending_fork_capture(
269
365
  );
270
366
  agent.remove("_pending_session_id");
271
367
  agent.remove("attribution_ambiguous");
272
- agent.remove("fork_source_session_id");
273
368
  agent.remove("pending_target_agent");
274
369
  agent.remove("pending_grace_secs");
275
370
  agent.insert("capture_state".to_string(), serde_json::json!("captured"));
276
- true
371
+ Some(finalized)
372
+ }
373
+
374
+ #[cfg(test)]
375
+ mod tests {
376
+ use super::*;
377
+
378
+ #[test]
379
+ fn context_fork_finalized_writes_catalog_event_and_canonical_fields() {
380
+ let workspace = std::env::temp_dir().join(format!(
381
+ "team-agent-context-fork-audit-{}",
382
+ std::process::id()
383
+ ));
384
+ let _ = std::fs::remove_dir_all(&workspace);
385
+ std::fs::create_dir_all(&workspace).expect("create audit workspace");
386
+ let event_log = crate::event_log::EventLog::new(&workspace);
387
+ ContextForkFinalized::new(
388
+ "source",
389
+ "target",
390
+ "target-session",
391
+ Path::new("/tmp/target.jsonl"),
392
+ "context_fork_verified",
393
+ "high",
394
+ )
395
+ .write_audit(&event_log)
396
+ .expect("write context fork audit");
397
+
398
+ let events = event_log.tail(0).expect("read context fork audit");
399
+ let event = events.last().expect("context fork audit event");
400
+ assert_eq!(
401
+ event.get("event").and_then(serde_json::Value::as_str),
402
+ Some(crate::lifecycle::types::event_names::CONTEXT_FORK)
403
+ );
404
+ assert_eq!(
405
+ event
406
+ .get("source_agent_id")
407
+ .and_then(serde_json::Value::as_str),
408
+ Some("source")
409
+ );
410
+ assert_eq!(
411
+ event.get("agent_id").and_then(serde_json::Value::as_str),
412
+ Some("target")
413
+ );
414
+ assert!(event.get("prompt").is_none());
415
+ let _ = std::fs::remove_dir_all(workspace);
416
+ }
277
417
  }
278
418
 
279
419
  pub(super) fn verify_fork_registration(
@@ -354,6 +354,11 @@ pub(super) fn upsert_forked_agent_state(
354
354
  "forked_from".to_string(),
355
355
  serde_json::json!(source_agent_id.as_str()),
356
356
  );
357
+ entry.insert(
358
+ "fork_source_session_id".to_string(),
359
+ serde_json::json!(context_proof.source_session_id.as_str()),
360
+ );
361
+ entry.insert("capture_state".to_string(), serde_json::json!("captured"));
357
362
  entry.insert(
358
363
  "dynamic_role_file".to_string(),
359
364
  serde_json::json!(dynamic_role_file.to_string_lossy().to_string()),
@@ -295,8 +295,8 @@ 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;
299
298
  pub(super) use fork_finalize::*;
299
+ pub(crate) use fork_finalize::{finalize_pending_fork_capture, ContextForkFinalized};
300
300
 
301
301
  mod role_source;
302
302
  pub(super) use role_source::*;
@@ -1178,7 +1178,29 @@ pub(super) fn fork_ws(alpha_role: &str) -> PathBuf {
1178
1178
  std::process::id(),
1179
1179
  n
1180
1180
  ));
1181
- std::fs::write(&rollout, b"{}\n").expect("seed fork source rollout");
1181
+ std::fs::write(
1182
+ &rollout,
1183
+ format!(
1184
+ "{}\n{}\n",
1185
+ json!({
1186
+ "type": "session_meta",
1187
+ "payload": {
1188
+ "id": "sess-a",
1189
+ "cwd": ws,
1190
+ }
1191
+ }),
1192
+ json!({
1193
+ "type": "response_item",
1194
+ "payload": {
1195
+ "content": [{
1196
+ "type": "input_text",
1197
+ "text": "You are Team Agent worker `alpha` with role `fixture`."
1198
+ }]
1199
+ }
1200
+ })
1201
+ ),
1202
+ )
1203
+ .expect("seed identity-complete fork source rollout");
1182
1204
  crate::state::persist::save_runtime_state(
1183
1205
  &ws,
1184
1206
  &json!({
@@ -1669,6 +1691,18 @@ fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
1669
1691
  fn lanea_fork_report_session_id_is_not_pane_id() {
1670
1692
  let _home = LaneHomeGuard::enter("fork-report");
1671
1693
  let ws = fork_ws(DELEG_ROLE_ALPHA); // codex+subscription -> native fork supported -> full success path
1694
+ let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
1695
+ let source = PathBuf::from(state["agents"]["alpha"]["rollout_path"].as_str().unwrap());
1696
+ let codex_root = std::env::var_os("HOME")
1697
+ .map(PathBuf::from)
1698
+ .unwrap()
1699
+ .join(".codex/sessions/lane-fixture");
1700
+ std::fs::create_dir_all(&codex_root).unwrap();
1701
+ let discoverable_source = codex_root.join("rollout-sess-a.jsonl");
1702
+ std::fs::copy(source, &discoverable_source).unwrap();
1703
+ state["agents"]["alpha"]["rollout_path"] =
1704
+ json!(discoverable_source.to_string_lossy().to_string());
1705
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
1672
1706
  let tx = LaneTransport::new("team-laneateam", &[]);
1673
1707
  let report =
1674
1708
  fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx)
@@ -36,6 +36,7 @@ pub mod event_names {
36
36
  pub const ADD_FAILED: &str = "lifecycle.add_failed";
37
37
  pub const REMOVE_STEP_COMPLETED: &str = "lifecycle.remove_step_completed";
38
38
  pub const REMOVE_ROLLED_BACK: &str = "lifecycle.remove_rolled_back";
39
+ pub const CONTEXT_FORK: &str = "context_fork";
39
40
  // restart 决策事件(Route B audit 契约必发)。
40
41
  pub const RESTART_RESUME_DECISION: &str = "restart.resume_decision";
41
42
  pub const RESTART_ATOMIC_REFUSAL: &str = "restart.atomic_refusal";
@@ -12,6 +12,11 @@ use super::{AuthMode, Provider};
12
12
 
13
13
  pub use crate::provider::session_scan::{CaptureSessionContext, CapturedSessionCandidate};
14
14
 
15
+ pub trait ForkBackingMaterialization {
16
+ fn path(&self) -> &Path;
17
+ fn handoff(&mut self);
18
+ }
19
+
15
20
  // ===========================================================================
16
21
  // TRAIT: ProviderAdapter (method SIGNATURES only — 无 body)
17
22
  // ===========================================================================
@@ -187,6 +192,24 @@ pub trait ProviderAdapter {
187
192
  .map(CommandPlan::argv_only)
188
193
  }
189
194
 
195
+ fn materialize_fork_backing(
196
+ &self,
197
+ source_path: &Path,
198
+ source_session_id: &SessionId,
199
+ source_agent_id: &str,
200
+ target_agent_id: &str,
201
+ plan: &mut CommandPlan,
202
+ ) -> Result<Option<Box<dyn ForkBackingMaterialization>>, ProviderError> {
203
+ let _ = (
204
+ source_path,
205
+ source_session_id,
206
+ source_agent_id,
207
+ target_agent_id,
208
+ plan,
209
+ );
210
+ Ok(None)
211
+ }
212
+
190
213
  /// 计算本 provider 该用的 MCP server 配置(`adapter.py` mcp_config;claude
191
214
  /// compatible_api 走 `ensure_compatible_claude_mcp_config`)。
192
215
  fn mcp_config(&self, auth_mode: AuthMode) -> Result<McpConfig, ProviderError>;
@@ -549,6 +572,29 @@ impl ProviderAdapter for BasicProviderAdapter {
549
572
  }
550
573
  }
551
574
 
575
+ fn materialize_fork_backing(
576
+ &self,
577
+ source_path: &Path,
578
+ source_session_id: &SessionId,
579
+ source_agent_id: &str,
580
+ target_agent_id: &str,
581
+ plan: &mut CommandPlan,
582
+ ) -> Result<Option<Box<dyn ForkBackingMaterialization>>, ProviderError> {
583
+ match self.provider {
584
+ Provider::Codex => crate::provider::session::materialize_codex_fork(
585
+ source_path,
586
+ source_session_id,
587
+ source_agent_id,
588
+ target_agent_id,
589
+ plan,
590
+ )
591
+ .map(|materialized| {
592
+ Some(Box::new(materialized) as Box<dyn ForkBackingMaterialization>)
593
+ }),
594
+ _ => Ok(None),
595
+ }
596
+ }
597
+
552
598
  fn capture_session_id(
553
599
  &self,
554
600
  agent_id: &str,
@@ -1198,3 +1244,14 @@ pub(crate) fn next_session_token() -> String {
1198
1244
  bytes[15],
1199
1245
  )
1200
1246
  }
1247
+
1248
+ #[cfg(test)]
1249
+ mod fork_materialization_source_guard {
1250
+ #[test]
1251
+ fn lifecycle_uses_provider_neutral_fork_materialization_dispatch() {
1252
+ let source = include_str!("../lifecycle/launch/fork_agent.rs");
1253
+ assert!(source.contains(".materialize_fork_backing("));
1254
+ assert!(!source.contains("materialize_codex_fork"));
1255
+ assert!(!source.contains("codex_fork"));
1256
+ }
1257
+ }