@team-agent/installer 0.5.51 → 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 (79) 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/named_address.rs +35 -67
  7. package/crates/team-agent/src/cli/spec.rs +3 -0
  8. package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
  9. package/crates/team-agent/src/cli/types.rs +11 -0
  10. package/crates/team-agent/src/diagnose/orphans.rs +24 -3
  11. package/crates/team-agent/src/kill_audit.rs +67 -0
  12. package/crates/team-agent/src/leader/lease.rs +526 -73
  13. package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
  14. package/crates/team-agent/src/leader/rediscover.rs +6 -0
  15. package/crates/team-agent/src/leader/start.rs +144 -23
  16. package/crates/team-agent/src/leader/tests/idle.rs +3 -0
  17. package/crates/team-agent/src/leader/types.rs +6 -0
  18. package/crates/team-agent/src/lib.rs +1 -0
  19. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
  20. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +12 -1
  21. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
  22. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
  23. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
  24. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
  25. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
  26. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
  27. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1 -1
  28. package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
  29. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -8
  30. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
  31. package/crates/team-agent/src/lifecycle/launch.rs +14 -2
  32. package/crates/team-agent/src/lifecycle/restart/agent.rs +20 -4
  33. package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
  34. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +113 -4
  35. package/crates/team-agent/src/lifecycle/restart/remove.rs +73 -10
  36. package/crates/team-agent/src/lifecycle/restart.rs +25 -1
  37. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +441 -12
  38. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -2
  39. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
  40. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
  41. package/crates/team-agent/src/lifecycle/types.rs +17 -0
  42. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
  43. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
  44. package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
  45. package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
  46. package/crates/team-agent/src/mcp_server/tests.rs +11 -4
  47. package/crates/team-agent/src/mcp_server/tools.rs +18 -4
  48. package/crates/team-agent/src/mcp_server/types.rs +3 -0
  49. package/crates/team-agent/src/mcp_server/wire.rs +30 -7
  50. package/crates/team-agent/src/messaging/delivery.rs +126 -147
  51. package/crates/team-agent/src/messaging/leader_channel.rs +198 -0
  52. package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
  53. package/crates/team-agent/src/messaging/mod.rs +5 -0
  54. package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
  55. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  56. package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
  57. package/crates/team-agent/src/provider/adapter.rs +54 -13
  58. package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
  59. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
  60. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  61. package/crates/team-agent/src/provider/session/capture.rs +321 -52
  62. package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
  63. package/crates/team-agent/src/provider/session/mod.rs +6 -0
  64. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
  65. package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
  66. package/crates/team-agent/src/provider/session_scan/common.rs +39 -6
  67. package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
  68. package/crates/team-agent/src/provider/session_scan.rs +3 -3
  69. package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
  70. package/crates/team-agent/src/provider/tests.rs +1 -0
  71. package/crates/team-agent/src/state/persist.rs +282 -20
  72. package/crates/team-agent/src/state/projection.rs +245 -30
  73. package/crates/team-agent/src/state/repository/intent.rs +13 -0
  74. package/crates/team-agent/src/state/repository.rs +36 -12
  75. package/crates/team-agent/src/state/selector.rs +9 -18
  76. package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
  77. package/crates/team-agent/src/tmux_backend.rs +29 -2
  78. package/package.json +4 -4
  79. 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