@team-agent/installer 0.5.52 → 0.5.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +18 -2
  4. package/crates/team-agent/src/cli/emit.rs +17 -0
  5. package/crates/team-agent/src/cli/mod.rs +155 -4
  6. package/crates/team-agent/src/cli/spec.rs +3 -0
  7. package/crates/team-agent/src/cli/types.rs +11 -0
  8. package/crates/team-agent/src/diagnose/orphans.rs +24 -3
  9. package/crates/team-agent/src/kill_audit.rs +67 -0
  10. package/crates/team-agent/src/leader/lease.rs +324 -57
  11. package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
  12. package/crates/team-agent/src/leader/rediscover.rs +6 -0
  13. package/crates/team-agent/src/leader/start.rs +144 -23
  14. package/crates/team-agent/src/leader/tests/idle.rs +3 -0
  15. package/crates/team-agent/src/leader/types.rs +6 -0
  16. package/crates/team-agent/src/lib.rs +1 -0
  17. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
  18. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +5 -0
  19. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
  20. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
  21. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
  22. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
  23. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
  24. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
  25. package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
  26. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
  27. package/crates/team-agent/src/lifecycle/launch.rs +14 -2
  28. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
  30. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -3
  31. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +110 -12
  32. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +8 -2
  33. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
  34. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
  35. package/crates/team-agent/src/lifecycle/types.rs +17 -0
  36. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
  37. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
  38. package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
  39. package/crates/team-agent/src/mcp_server/tools.rs +18 -4
  40. package/crates/team-agent/src/mcp_server/types.rs +3 -0
  41. package/crates/team-agent/src/mcp_server/wire.rs +30 -7
  42. package/crates/team-agent/src/messaging/leader_channel.rs +32 -5
  43. package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
  44. package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
  45. package/crates/team-agent/src/provider/adapter.rs +54 -13
  46. package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
  47. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
  48. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  49. package/crates/team-agent/src/provider/session/capture.rs +321 -52
  50. package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
  51. package/crates/team-agent/src/provider/session/mod.rs +6 -0
  52. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
  53. package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
  54. package/crates/team-agent/src/provider/session_scan/common.rs +39 -6
  55. package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
  56. package/crates/team-agent/src/provider/session_scan.rs +3 -3
  57. package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
  58. package/crates/team-agent/src/provider/tests.rs +1 -0
  59. package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
  60. package/crates/team-agent/src/tmux_backend.rs +29 -2
  61. package/package.json +4 -4
  62. package/skills/team-agent/SKILL.md +8 -3
@@ -0,0 +1,499 @@
1
+ use std::collections::BTreeMap;
2
+ use std::path::{Path, PathBuf};
3
+ use std::time::{Duration, SystemTime};
4
+
5
+ use crate::model::enums::Provider;
6
+ use crate::provider::{CommandPlan, ProviderError, SessionId};
7
+
8
+ pub(crate) fn context_fork_convergence_deadline(provider: Provider) -> Duration {
9
+ match provider {
10
+ Provider::Claude | Provider::ClaudeCode => Duration::from_secs(45),
11
+ Provider::Codex => Duration::from_secs(10),
12
+ Provider::Copilot | Provider::GeminiCli | Provider::Fake => Duration::from_secs(5),
13
+ }
14
+ }
15
+
16
+ #[derive(Debug, Clone, PartialEq, Eq)]
17
+ pub struct ContextForkProof {
18
+ pub provider: Provider,
19
+ pub source_session_id: SessionId,
20
+ pub new_session_id: SessionId,
21
+ pub backing_path: PathBuf,
22
+ pub captured_via: String,
23
+ pub attribution_confidence: String,
24
+ pub managed_backing_root: Option<PathBuf>,
25
+ }
26
+
27
+ #[derive(Debug, Clone)]
28
+ pub(crate) struct ContextBackingSnapshot {
29
+ root: PathBuf,
30
+ files: BTreeMap<PathBuf, FileStamp>,
31
+ }
32
+
33
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
34
+ struct FileStamp {
35
+ len: u64,
36
+ modified: Option<SystemTime>,
37
+ }
38
+
39
+ impl ContextBackingSnapshot {
40
+ pub(crate) fn capture(provider: Provider, plan: &CommandPlan) -> Self {
41
+ let root = provider_backing_root(provider, plan);
42
+ let files = jsonl_files(&root);
43
+ Self { root, files }
44
+ }
45
+ }
46
+
47
+ pub(crate) fn verify_context_fork(
48
+ provider: Provider,
49
+ source_session_id: &SessionId,
50
+ plan: &CommandPlan,
51
+ before: &ContextBackingSnapshot,
52
+ expected_backing_path: Option<&Path>,
53
+ agent_id: &str,
54
+ spawn_cwd: &Path,
55
+ spawned_at: &str,
56
+ deadline: Duration,
57
+ ) -> Result<ContextForkProof, ProviderError> {
58
+ if provider == Provider::Copilot {
59
+ return verify_copilot_fork(source_session_id, plan);
60
+ }
61
+ if provider == Provider::Codex {
62
+ return verify_codex_fork(
63
+ source_session_id,
64
+ plan,
65
+ before,
66
+ agent_id,
67
+ spawn_cwd,
68
+ spawned_at,
69
+ deadline,
70
+ );
71
+ }
72
+ if matches!(provider, Provider::Claude | Provider::ClaudeCode) {
73
+ return verify_claude_fork(
74
+ provider,
75
+ source_session_id,
76
+ plan,
77
+ before,
78
+ expected_backing_path,
79
+ deadline,
80
+ );
81
+ }
82
+ Err(ProviderError::CaptureFailed(format!(
83
+ "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
+ )))
199
+ }
200
+
201
+ fn verify_copilot_fork(
202
+ source_session_id: &SessionId,
203
+ plan: &CommandPlan,
204
+ ) -> Result<ContextForkProof, ProviderError> {
205
+ let expected = plan.expected_session_id.as_ref().ok_or_else(|| {
206
+ ProviderError::CaptureFailed(
207
+ "context_fork_unverified: copilot plan has no expected session id".to_string(),
208
+ )
209
+ })?;
210
+ if expected == source_session_id {
211
+ return Err(ProviderError::CaptureFailed(
212
+ "context_fork_unverified: copilot NEW session equals source".to_string(),
213
+ ));
214
+ }
215
+ let root = plan.provider_projects_root.as_ref().ok_or_else(|| {
216
+ ProviderError::CaptureFailed(
217
+ "context_fork_unverified: copilot plan has no isolated backing root".to_string(),
218
+ )
219
+ })?;
220
+ let state_dir = root.join("session-state").join(expected.as_str());
221
+ let db_path = root.join("session-store.db");
222
+ if !state_dir.is_dir() || !db_path.is_file() {
223
+ return Err(ProviderError::CaptureFailed(format!(
224
+ "context_fork_unverified: copilot backing missing under {}",
225
+ root.display()
226
+ )));
227
+ }
228
+ let conn = rusqlite::Connection::open_with_flags(
229
+ &db_path,
230
+ rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
231
+ )
232
+ .map_err(|error| ProviderError::CaptureFailed(error.to_string()))?;
233
+ let found: i64 = conn
234
+ .query_row(
235
+ "select count(*) from sessions where id = ?1",
236
+ [expected.as_str()],
237
+ |row| row.get(0),
238
+ )
239
+ .map_err(|error| ProviderError::CaptureFailed(error.to_string()))?;
240
+ if found != 1 {
241
+ return Err(ProviderError::CaptureFailed(
242
+ "context_fork_unverified: copilot NEW session is absent from session-store".to_string(),
243
+ ));
244
+ }
245
+ Ok(ContextForkProof {
246
+ provider: Provider::Copilot,
247
+ source_session_id: source_session_id.clone(),
248
+ new_session_id: expected.clone(),
249
+ backing_path: db_path,
250
+ captured_via: "copilot_store_fork_verified".to_string(),
251
+ attribution_confidence: "high".to_string(),
252
+ managed_backing_root: Some(root.clone()),
253
+ })
254
+ }
255
+
256
+ fn provider_backing_root(provider: Provider, plan: &CommandPlan) -> PathBuf {
257
+ if let Some(root) = plan.provider_projects_root.as_ref() {
258
+ return root.clone();
259
+ }
260
+ let home = std::env::var_os("HOME")
261
+ .map(PathBuf::from)
262
+ .unwrap_or_else(|| PathBuf::from("."));
263
+ match provider {
264
+ Provider::Claude | Provider::ClaudeCode => home.join(".claude").join("projects"),
265
+ Provider::Codex => home.join(".codex").join("sessions"),
266
+ Provider::Copilot => home.join(".copilot").join("session-state"),
267
+ Provider::GeminiCli | Provider::Fake => home,
268
+ }
269
+ }
270
+
271
+ fn jsonl_files(root: &Path) -> BTreeMap<PathBuf, FileStamp> {
272
+ let mut files = BTreeMap::new();
273
+ let mut pending = vec![root.to_path_buf()];
274
+ while let Some(dir) = pending.pop() {
275
+ let Ok(entries) = std::fs::read_dir(&dir) else {
276
+ continue;
277
+ };
278
+ for entry in entries.flatten() {
279
+ let path = entry.path();
280
+ if path.is_dir() {
281
+ pending.push(path);
282
+ } else if path.extension().and_then(|ext| ext.to_str()) == Some("jsonl") {
283
+ if let Some(stamp) = file_stamp(&path) {
284
+ files.insert(path, stamp);
285
+ }
286
+ }
287
+ }
288
+ }
289
+ files
290
+ }
291
+
292
+ fn file_stamp(path: &Path) -> Option<FileStamp> {
293
+ let meta = std::fs::metadata(path).ok()?;
294
+ Some(FileStamp {
295
+ len: meta.len(),
296
+ modified: meta.modified().ok(),
297
+ })
298
+ }
299
+
300
+ fn readable_jsonl(path: &Path) -> bool {
301
+ std::fs::read_to_string(path)
302
+ .ok()
303
+ .and_then(|text| {
304
+ text.lines()
305
+ .find(|line| !line.trim().is_empty())
306
+ .map(str::to_string)
307
+ })
308
+ .is_some_and(|line| serde_json::from_str::<serde_json::Value>(&line).is_ok())
309
+ }
310
+
311
+ fn session_id_from_jsonl(path: &Path) -> Option<String> {
312
+ let text = std::fs::read_to_string(path).ok()?;
313
+ text.lines()
314
+ .filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
315
+ .find_map(|record| {
316
+ record
317
+ .get("sessionId")
318
+ .or_else(|| record.get("session_id"))
319
+ .and_then(serde_json::Value::as_str)
320
+ .map(ToString::to_string)
321
+ .or_else(|| {
322
+ record
323
+ .get("session_meta")
324
+ .and_then(|value| value.get("payload"))
325
+ .or_else(|| record.get("payload"))
326
+ .and_then(|value| value.get("id"))
327
+ .and_then(serde_json::Value::as_str)
328
+ .map(ToString::to_string)
329
+ })
330
+ })
331
+ }
332
+
333
+ #[cfg(test)]
334
+ mod tests {
335
+ use super::*;
336
+ use std::io::Write;
337
+
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
+ #[test]
355
+ fn codex_fork_proof_ignores_changed_stale_and_foreign_rollouts() {
356
+ let root =
357
+ std::env::temp_dir().join(format!("ta-codex-fork-proof-dirty-{}", std::process::id()));
358
+ let _ = std::fs::remove_dir_all(&root);
359
+ let current_cwd = root.join("current");
360
+ let foreign_cwd = root.join("foreign");
361
+ std::fs::create_dir_all(&current_cwd).unwrap();
362
+ std::fs::create_dir_all(&foreign_cwd).unwrap();
363
+ let stale = root.join(
364
+ "2026/07/16/rollout-2026-07-16T08-05-43-019f6686-e2b6-7000-8000-000000000001.jsonl",
365
+ );
366
+ let fresh = root.join(
367
+ "2026/07/22/rollout-2026-07-22T04-01-01-019f8644-7450-7000-8000-000000000002.jsonl",
368
+ );
369
+ let foreign = root.join(
370
+ "2026/07/22/rollout-2026-07-22T04-01-02-019f8644-7838-7000-8000-000000000003.jsonl",
371
+ );
372
+ let write_rollout = |path: &Path, cwd: &Path, id: &str| {
373
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
374
+ std::fs::write(
375
+ path,
376
+ format!(
377
+ "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"{}\"}}}}\n",
378
+ cwd.to_string_lossy()
379
+ ),
380
+ )
381
+ .unwrap();
382
+ };
383
+ write_rollout(&stale, &current_cwd, "stale-session");
384
+ let plan = CommandPlan {
385
+ argv: Vec::new(),
386
+ expected_session_id: None,
387
+ provider_projects_root: Some(root.clone()),
388
+ managed_mcp_config: false,
389
+ };
390
+ let before = ContextBackingSnapshot::capture(Provider::Codex, &plan);
391
+ std::fs::OpenOptions::new()
392
+ .append(true)
393
+ .open(&stale)
394
+ .unwrap()
395
+ .write_all(b"{\"type\":\"event_msg\"}\n")
396
+ .unwrap();
397
+ write_rollout(&foreign, &foreign_cwd, "foreign-session");
398
+ write_rollout(&fresh, &current_cwd, "fresh-session");
399
+
400
+ let proof = verify_context_fork(
401
+ Provider::Codex,
402
+ &SessionId::new("source-session"),
403
+ &plan,
404
+ &before,
405
+ None,
406
+ "fork",
407
+ &current_cwd,
408
+ "2026-07-21T20:00:00+00:00",
409
+ Duration::ZERO,
410
+ )
411
+ .unwrap();
412
+
413
+ assert_eq!(proof.new_session_id.as_str(), "fresh-session");
414
+ assert_eq!(proof.backing_path, fresh);
415
+ let _ = std::fs::remove_dir_all(&root);
416
+ }
417
+
418
+ #[test]
419
+ fn claude_fork_proof_is_partitioned_by_exact_snapshot_path() {
420
+ let root = std::env::temp_dir().join(format!(
421
+ "ta-claude-fork-proof-partition-{}",
422
+ std::process::id()
423
+ ));
424
+ let _ = std::fs::remove_dir_all(&root);
425
+ std::fs::create_dir_all(root.join(".team/logs")).unwrap();
426
+ let expected = SessionId::new("22222222-2222-4222-8222-222222222222");
427
+ let plan = CommandPlan {
428
+ argv: Vec::new(),
429
+ expected_session_id: Some(expected.clone()),
430
+ provider_projects_root: Some(root.clone()),
431
+ managed_mcp_config: false,
432
+ };
433
+ let before = ContextBackingSnapshot::capture(Provider::Claude, &plan);
434
+ std::fs::write(
435
+ root.join(".team/logs/events.jsonl"),
436
+ "{\"event\":\"noise\"}\n",
437
+ )
438
+ .unwrap();
439
+ std::fs::write(
440
+ root.join("11111111-1111-4111-8111-111111111111.jsonl"),
441
+ "{\"type\":\"sibling-snapshot\"}\n",
442
+ )
443
+ .unwrap();
444
+ let expected_path = root.join(format!("{}.jsonl", expected.as_str()));
445
+ std::fs::write(&expected_path, "{\"type\":\"own-snapshot\"}\n").unwrap();
446
+ let proof = verify_context_fork(
447
+ Provider::Claude,
448
+ &SessionId::new("source-session"),
449
+ &plan,
450
+ &before,
451
+ Some(&expected_path),
452
+ "fork",
453
+ &root,
454
+ "2026-07-22T00:00:00Z",
455
+ Duration::ZERO,
456
+ )
457
+ .unwrap();
458
+ assert_eq!(proof.new_session_id, expected);
459
+ assert_eq!(proof.backing_path, expected_path);
460
+ let _ = std::fs::remove_dir_all(&root);
461
+ }
462
+ #[test]
463
+ fn claude_fork_proof_never_uses_events_jsonl_as_backing() {
464
+ let root = std::env::temp_dir().join(format!(
465
+ "ta-claude-fork-proof-no-events-{}",
466
+ std::process::id()
467
+ ));
468
+ let _ = std::fs::remove_dir_all(&root);
469
+ std::fs::create_dir_all(root.join(".team/logs")).unwrap();
470
+ let expected = SessionId::new("33333333-3333-4333-8333-333333333333");
471
+ let plan = CommandPlan {
472
+ argv: Vec::new(),
473
+ expected_session_id: Some(expected.clone()),
474
+ provider_projects_root: Some(root.clone()),
475
+ managed_mcp_config: false,
476
+ };
477
+ let before = ContextBackingSnapshot::capture(Provider::Claude, &plan);
478
+ std::fs::write(
479
+ root.join(".team/logs/events.jsonl"),
480
+ "{\"event\":\"noise\"}\n",
481
+ )
482
+ .unwrap();
483
+ let missing = root.join(format!("{}.jsonl", expected.as_str()));
484
+ let error = verify_context_fork(
485
+ Provider::Claude,
486
+ &SessionId::new("source-session"),
487
+ &plan,
488
+ &before,
489
+ Some(&missing),
490
+ "fork",
491
+ &root,
492
+ "2026-07-22T00:00:00Z",
493
+ Duration::ZERO,
494
+ )
495
+ .unwrap_err();
496
+ assert!(error.to_string().contains("context_fork_unverified"));
497
+ let _ = std::fs::remove_dir_all(&root);
498
+ }
499
+ }
@@ -5,8 +5,14 @@
5
5
  //! session backing checks, etc.
6
6
 
7
7
  pub mod capture;
8
+ mod context_fork;
8
9
  pub mod resume;
9
10
 
11
+ pub use context_fork::ContextForkProof;
12
+ pub(crate) use context_fork::{
13
+ context_fork_convergence_deadline, verify_context_fork, ContextBackingSnapshot,
14
+ };
15
+
10
16
  pub use resume::{
11
17
  ProviderBackingCheck, RecoveryHint, ResumeDecisionDetail, ResumePreflight,
12
18
  ResumePreflightOutcome, ResumeRefusalReason,
@@ -75,7 +75,7 @@ pub(super) fn apply_expected_session_filter(
75
75
  .collect();
76
76
  return Ok(positive_only);
77
77
  }
78
- super::common::apply_spawn_time_window_if_unique(context, &mut out);
78
+ super::common::apply_spawn_time_window_if_unique(Provider::Claude, context, &mut out);
79
79
  Ok(out)
80
80
  }
81
81