@team-agent/installer 0.5.58 → 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.
Files changed (27) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +30 -2
  4. package/crates/team-agent/src/cli/send/mailbox.rs +68 -25
  5. package/crates/team-agent/src/cli/spec.rs +1 -1
  6. package/crates/team-agent/src/coordinator/tick.rs +21 -8
  7. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
  8. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +56 -50
  9. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
  10. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
  11. package/crates/team-agent/src/lifecycle/launch.rs +1 -1
  12. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
  13. package/crates/team-agent/src/lifecycle/types.rs +1 -0
  14. package/crates/team-agent/src/mcp_server/normalize.rs +53 -1
  15. package/crates/team-agent/src/mcp_server/tools.rs +8 -1
  16. package/crates/team-agent/src/provider/adapter.rs +57 -0
  17. package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
  18. package/crates/team-agent/src/provider/session/capture.rs +29 -16
  19. package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
  20. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
  21. package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
  22. package/crates/team-agent/src/provider/session/mod.rs +3 -2
  23. package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
  24. package/crates/team-agent/src/provider/session_scan/common.rs +11 -1
  25. package/crates/team-agent/src/provider/session_scan.rs +3 -0
  26. package/package.json +4 -4
  27. package/skills/team-agent/SKILL.md +1 -0
@@ -1,14 +1,319 @@
1
1
  use super::*;
2
+ use std::collections::BTreeSet;
3
+ use std::fs::{File, OpenOptions};
4
+ use std::io::{Read, Write};
5
+
6
+ #[derive(Debug)]
7
+ pub(crate) struct CodexForkMaterialization {
8
+ path: PathBuf,
9
+ keep: bool,
10
+ }
11
+
12
+ impl CodexForkMaterialization {
13
+ pub(crate) fn path(&self) -> &Path {
14
+ &self.path
15
+ }
16
+
17
+ fn mark_handoff(&mut self) {
18
+ self.keep = true;
19
+ }
20
+ }
21
+
22
+ impl crate::provider::adapter::ForkBackingMaterialization for CodexForkMaterialization {
23
+ fn path(&self) -> &Path {
24
+ self.path()
25
+ }
26
+
27
+ fn handoff(&mut self) {
28
+ self.mark_handoff();
29
+ }
30
+ }
31
+
32
+ impl Drop for CodexForkMaterialization {
33
+ fn drop(&mut self) {
34
+ if !self.keep {
35
+ let _ = std::fs::remove_file(&self.path);
36
+ }
37
+ }
38
+ }
39
+
40
+ pub(crate) fn materialize_codex_fork(
41
+ source_path: &Path,
42
+ source_session_id: &SessionId,
43
+ source_agent_id: &str,
44
+ target_agent_id: &str,
45
+ plan: &mut CommandPlan,
46
+ ) -> Result<CodexForkMaterialization, ProviderError> {
47
+ let target_session_id = SessionId::new(codex_session_v7());
48
+ let parent = source_path.parent().ok_or_else(|| {
49
+ ProviderError::Io(format!(
50
+ "codex source backing has no parent: {}",
51
+ source_path.display()
52
+ ))
53
+ })?;
54
+ let timestamp = chrono::Utc::now().format("%Y-%m-%dT%H-%M-%S");
55
+ let target_path = parent.join(format!(
56
+ "rollout-{timestamp}-{}.jsonl",
57
+ target_session_id.as_str()
58
+ ));
59
+ let source = complete_source_snapshot(source_path)?;
60
+ let rewritten = rewrite_snapshot(
61
+ &source,
62
+ source_session_id,
63
+ &target_session_id,
64
+ source_agent_id,
65
+ target_agent_id,
66
+ )?;
67
+ publish_target_no_clobber(&target_path, rewritten.as_bytes())?;
68
+ if let Err(error) =
69
+ validate_materialized_target(&target_path, &target_session_id, target_agent_id)
70
+ {
71
+ let _ = std::fs::remove_file(&target_path);
72
+ return Err(error);
73
+ }
74
+ apply_resume_target(plan, source_session_id, &target_session_id)?;
75
+ Ok(CodexForkMaterialization {
76
+ path: target_path,
77
+ keep: false,
78
+ })
79
+ }
80
+
81
+ fn complete_source_snapshot(path: &Path) -> Result<String, ProviderError> {
82
+ let file = File::open(path).map_err(|error| ProviderError::Io(error.to_string()))?;
83
+ let visible_len = file
84
+ .metadata()
85
+ .map_err(|error| ProviderError::Io(error.to_string()))?
86
+ .len();
87
+ let mut bytes = Vec::new();
88
+ file.take(visible_len)
89
+ .read_to_end(&mut bytes)
90
+ .map_err(|error| ProviderError::Io(error.to_string()))?;
91
+ let Some(boundary) = bytes.iter().rposition(|byte| *byte == b'\n') else {
92
+ return Err(ProviderError::Io(
93
+ "codex fork source has no complete JSONL record".to_string(),
94
+ ));
95
+ };
96
+ bytes.truncate(boundary + 1);
97
+ String::from_utf8(bytes).map_err(|error| ProviderError::Io(error.to_string()))
98
+ }
99
+
100
+ fn rewrite_snapshot(
101
+ source: &str,
102
+ source_session_id: &SessionId,
103
+ target_session_id: &SessionId,
104
+ source_agent_id: &str,
105
+ target_agent_id: &str,
106
+ ) -> Result<String, ProviderError> {
107
+ let source_marker = format!("You are Team Agent worker `{source_agent_id}`");
108
+ let target_marker = format!("You are Team Agent worker `{target_agent_id}`");
109
+ let mut meta_count = 0_usize;
110
+ let mut marker_count = 0_usize;
111
+ let mut output = String::new();
112
+ for (index, line) in source.lines().enumerate() {
113
+ if line.trim().is_empty() {
114
+ continue;
115
+ }
116
+ let mut record = serde_json::from_str::<serde_json::Value>(line).map_err(|error| {
117
+ ProviderError::Io(format!(
118
+ "codex fork source has invalid JSONL at line {}: {error}",
119
+ index + 1
120
+ ))
121
+ })?;
122
+ if record.get("type").and_then(serde_json::Value::as_str) == Some("session_meta") {
123
+ let id = record
124
+ .get_mut("payload")
125
+ .and_then(serde_json::Value::as_object_mut)
126
+ .and_then(|payload| payload.get_mut("id"))
127
+ .and_then(|id| id.as_str())
128
+ .ok_or_else(|| {
129
+ ProviderError::Io(
130
+ "codex fork session_meta has no string payload.id".to_string(),
131
+ )
132
+ })?;
133
+ if id != source_session_id.as_str() {
134
+ return Err(ProviderError::Io(
135
+ "codex fork session_meta id does not match source".to_string(),
136
+ ));
137
+ }
138
+ record["payload"]["id"] =
139
+ serde_json::Value::String(target_session_id.as_str().to_string());
140
+ meta_count += 1;
141
+ }
142
+ marker_count += replace_exact_marker(&mut record, &source_marker, &target_marker);
143
+ output.push_str(
144
+ &serde_json::to_string(&record)
145
+ .map_err(|error| ProviderError::Io(error.to_string()))?,
146
+ );
147
+ output.push('\n');
148
+ }
149
+ if meta_count != 1 || marker_count != 1 {
150
+ return Err(ProviderError::Io(format!(
151
+ "codex fork source identity is ambiguous: session_meta={meta_count}, marker={marker_count}"
152
+ )));
153
+ }
154
+ Ok(output)
155
+ }
156
+
157
+ fn replace_exact_marker(value: &mut serde_json::Value, source: &str, target: &str) -> usize {
158
+ match value {
159
+ serde_json::Value::String(text) => {
160
+ let count = text.matches(source).count();
161
+ if count == 1 {
162
+ *text = text.replacen(source, target, 1);
163
+ }
164
+ count
165
+ }
166
+ serde_json::Value::Array(items) => items
167
+ .iter_mut()
168
+ .map(|item| replace_exact_marker(item, source, target))
169
+ .sum(),
170
+ serde_json::Value::Object(fields) => fields
171
+ .values_mut()
172
+ .map(|item| replace_exact_marker(item, source, target))
173
+ .sum(),
174
+ _ => 0,
175
+ }
176
+ }
177
+
178
+ fn publish_target_no_clobber(path: &Path, bytes: &[u8]) -> Result<(), ProviderError> {
179
+ let parent = path.parent().ok_or_else(|| {
180
+ ProviderError::Io(format!("codex target has no parent: {}", path.display()))
181
+ })?;
182
+ let temp = parent.join(format!(
183
+ ".{}.tmp-{}",
184
+ path.file_name()
185
+ .and_then(|name| name.to_str())
186
+ .unwrap_or("codex-fork"),
187
+ std::process::id()
188
+ ));
189
+ let result = (|| -> std::io::Result<()> {
190
+ let mut file = OpenOptions::new()
191
+ .write(true)
192
+ .create_new(true)
193
+ .open(&temp)?;
194
+ file.write_all(bytes)?;
195
+ file.sync_all()?;
196
+ std::fs::hard_link(&temp, path)?;
197
+ File::open(parent)?.sync_all()?;
198
+ std::fs::remove_file(&temp)?;
199
+ Ok(())
200
+ })();
201
+ if let Err(error) = result {
202
+ let _ = std::fs::remove_file(&temp);
203
+ return Err(ProviderError::Io(error.to_string()));
204
+ }
205
+ Ok(())
206
+ }
207
+
208
+ fn validate_materialized_target(
209
+ path: &Path,
210
+ session_id: &SessionId,
211
+ target_agent_id: &str,
212
+ ) -> Result<(), ProviderError> {
213
+ let text =
214
+ std::fs::read_to_string(path).map_err(|error| ProviderError::Io(error.to_string()))?;
215
+ let actual = session_id_from_jsonl(path);
216
+ let marker = format!("You are Team Agent worker `{target_agent_id}`");
217
+ if actual.as_deref() != Some(session_id.as_str())
218
+ || !path
219
+ .file_stem()
220
+ .and_then(|name| name.to_str())
221
+ .is_some_and(|name| name.ends_with(session_id.as_str()))
222
+ || text.matches(&marker).count() != 1
223
+ {
224
+ return Err(ProviderError::Io(
225
+ "codex fork target read-back identity mismatch".to_string(),
226
+ ));
227
+ }
228
+ Ok(())
229
+ }
230
+
231
+ fn apply_resume_target(
232
+ plan: &mut CommandPlan,
233
+ source_session_id: &SessionId,
234
+ target_session_id: &SessionId,
235
+ ) -> Result<(), ProviderError> {
236
+ if plan.argv.first().map(String::as_str) != Some("codex")
237
+ || plan.argv.get(1).map(String::as_str) != Some("fork")
238
+ || plan.argv.last().map(String::as_str) != Some(source_session_id.as_str())
239
+ {
240
+ return Err(ProviderError::Command(
241
+ "codex fork command shape cannot be converted to exact resume".to_string(),
242
+ ));
243
+ }
244
+ plan.argv[1] = "resume".to_string();
245
+ *plan.argv.last_mut().expect("validated non-empty argv") =
246
+ target_session_id.as_str().to_string();
247
+ plan.expected_session_id = Some(target_session_id.clone());
248
+ Ok(())
249
+ }
250
+
251
+ fn codex_session_v7() -> String {
252
+ use sha2::Digest;
253
+
254
+ static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
255
+
256
+ let millis = std::time::SystemTime::now()
257
+ .duration_since(std::time::UNIX_EPOCH)
258
+ .map_or(0, |duration| duration.as_millis() as u64);
259
+ let mut hasher = sha2::Sha256::new();
260
+ hasher.update(millis.to_le_bytes());
261
+ hasher.update(std::process::id().to_le_bytes());
262
+ hasher.update(
263
+ COUNTER
264
+ .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
265
+ .to_le_bytes(),
266
+ );
267
+ let digest = hasher.finalize();
268
+ let mut bytes = [0_u8; 16];
269
+ bytes[..6].copy_from_slice(&millis.to_be_bytes()[2..]);
270
+ bytes[6..].copy_from_slice(&digest[..10]);
271
+ bytes[6] = (bytes[6] & 0x0f) | 0x70;
272
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
273
+ format!(
274
+ "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
275
+ u32::from_be_bytes(bytes[0..4].try_into().expect("fixed slice")),
276
+ u16::from_be_bytes(bytes[4..6].try_into().expect("fixed slice")),
277
+ u16::from_be_bytes(bytes[6..8].try_into().expect("fixed slice")),
278
+ u16::from_be_bytes(bytes[8..10].try_into().expect("fixed slice")),
279
+ u64::from_be_bytes([
280
+ 0, 0, bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
281
+ ])
282
+ )
283
+ }
2
284
 
3
285
  pub(super) fn verify_codex_fork(
4
286
  source_session_id: &SessionId,
5
287
  plan: &CommandPlan,
6
288
  before: &ContextBackingSnapshot,
289
+ expected_backing_path: Option<&Path>,
7
290
  agent_id: &str,
8
291
  spawn_cwd: &Path,
9
292
  spawned_at: &str,
10
293
  deadline: Duration,
11
294
  ) -> Result<ContextForkProof, ContextForkTermination> {
295
+ let Some(expected) = plan.expected_session_id.as_ref() else {
296
+ return verify_legacy_codex_fork(
297
+ source_session_id,
298
+ plan,
299
+ before,
300
+ agent_id,
301
+ spawn_cwd,
302
+ spawned_at,
303
+ deadline,
304
+ );
305
+ };
306
+ if expected == source_session_id {
307
+ return Err(ProviderError::CaptureFailed(
308
+ "context_fork_unverified: codex target session equals source".to_string(),
309
+ )
310
+ .into());
311
+ }
312
+ let expected_path = expected_backing_path.ok_or_else(|| {
313
+ ProviderError::CaptureFailed(
314
+ "context_fork_unverified: codex plan has no materialized target backing".to_string(),
315
+ )
316
+ })?;
12
317
  let context = crate::provider::session_scan::CaptureSessionContext {
13
318
  agent_id: agent_id.to_string(),
14
319
  spawn_cwd: spawn_cwd.to_path_buf(),
@@ -18,28 +323,25 @@ pub(super) fn verify_codex_fork(
18
323
  expected_session_id: plan.expected_session_id.clone(),
19
324
  provider_projects_root: plan.provider_projects_root.clone(),
20
325
  };
21
- let excluded = outcome::source_exclusions(before, source_session_id);
22
326
  let started = std::time::Instant::now();
23
327
  loop {
24
- let current = jsonl_files(&before.root);
25
328
  for candidate in
26
329
  crate::provider::session_scan::scan_session_candidates_once(Provider::Codex, &context)?
27
330
  {
28
331
  let Some(path) = candidate.captured.rollout_path.as_ref() else {
29
332
  continue;
30
333
  };
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 {
334
+ if path.as_path() != expected_path {
36
335
  continue;
37
336
  }
38
337
  let Some(new_session_id) = candidate.captured.session_id else {
39
338
  continue;
40
339
  };
41
- if excluded.contains(new_session_id.as_str())
42
- || excluded.contains(&path.as_path().to_string_lossy().to_string())
340
+ if &new_session_id != expected || &new_session_id == source_session_id {
341
+ continue;
342
+ }
343
+ if !candidate.positive_agent_id_match
344
+ || candidate.embedded_agent_id.as_deref() != Some(agent_id)
43
345
  {
44
346
  continue;
45
347
  }
@@ -63,3 +365,78 @@ pub(super) fn verify_codex_fork(
63
365
  deadline_ms: deadline.as_millis(),
64
366
  })
65
367
  }
368
+
369
+ fn verify_legacy_codex_fork(
370
+ source_session_id: &SessionId,
371
+ plan: &CommandPlan,
372
+ before: &ContextBackingSnapshot,
373
+ agent_id: &str,
374
+ spawn_cwd: &Path,
375
+ spawned_at: &str,
376
+ deadline: Duration,
377
+ ) -> Result<ContextForkProof, ContextForkTermination> {
378
+ let mut excluded_session_ids = BTreeSet::new();
379
+ excluded_session_ids.insert(source_session_id.as_str().to_string());
380
+ let mut excluded_backing_paths = BTreeSet::new();
381
+ for path in before.files.keys() {
382
+ if session_id_from_jsonl(path).as_deref() == Some(source_session_id.as_str()) {
383
+ excluded_backing_paths.insert(path.clone());
384
+ }
385
+ }
386
+ let context = crate::provider::session_scan::CaptureSessionContext {
387
+ agent_id: agent_id.to_string(),
388
+ spawn_cwd: spawn_cwd.to_path_buf(),
389
+ pane_id: None,
390
+ pane_pid: None,
391
+ spawned_at: Some(spawned_at.to_string()),
392
+ expected_session_id: None,
393
+ provider_projects_root: plan.provider_projects_root.clone(),
394
+ };
395
+ let started = std::time::Instant::now();
396
+ loop {
397
+ let current = jsonl_files(&provider_backing_root(Provider::Codex, plan));
398
+ let mut matches = Vec::new();
399
+ for candidate in
400
+ crate::provider::session_scan::scan_session_candidates_once(Provider::Codex, &context)?
401
+ {
402
+ let Some(path) = candidate.captured.rollout_path.as_ref() else {
403
+ continue;
404
+ };
405
+ if excluded_backing_paths.contains(path.as_path()) {
406
+ continue;
407
+ }
408
+ let Some(stamp) = current.get(path.as_path()) else {
409
+ continue;
410
+ };
411
+ if before.files.get(path.as_path()) == Some(stamp) {
412
+ continue;
413
+ }
414
+ let Some(session_id) = candidate.captured.session_id else {
415
+ continue;
416
+ };
417
+ if excluded_session_ids.contains(session_id.as_str()) {
418
+ continue;
419
+ }
420
+ matches.push((session_id, path.as_path().to_path_buf()));
421
+ }
422
+ if let [(new_session_id, backing_path)] = matches.as_slice() {
423
+ return Ok(ContextForkProof {
424
+ provider: Provider::Codex,
425
+ source_session_id: source_session_id.clone(),
426
+ new_session_id: new_session_id.clone(),
427
+ backing_path: backing_path.clone(),
428
+ captured_via: "context_fork_verified".to_string(),
429
+ attribution_confidence: "high".to_string(),
430
+ managed_backing_root: None,
431
+ });
432
+ }
433
+ if started.elapsed() >= deadline {
434
+ break;
435
+ }
436
+ std::thread::sleep(Duration::from_millis(50));
437
+ }
438
+ Err(ContextForkTermination::Timeout {
439
+ provider: Provider::Codex,
440
+ deadline_ms: deadline.as_millis(),
441
+ })
442
+ }
@@ -1,18 +1,4 @@
1
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
2
 
17
3
  #[derive(Debug, Clone)]
18
4
  pub(crate) struct PendingContextFork {
@@ -47,6 +33,7 @@ pub(crate) fn observe_context_fork(
47
33
  plan: &CommandPlan,
48
34
  before: &ContextBackingSnapshot,
49
35
  expected_backing_path: Option<&Path>,
36
+ source_agent_id: &str,
50
37
  agent_id: &str,
51
38
  spawn_cwd: &Path,
52
39
  spawned_at: &str,
@@ -58,6 +45,7 @@ pub(crate) fn observe_context_fork(
58
45
  plan,
59
46
  before,
60
47
  expected_backing_path,
48
+ source_agent_id,
61
49
  agent_id,
62
50
  spawn_cwd,
63
51
  spawned_at,
@@ -123,6 +111,7 @@ mod tests {
123
111
  &plan,
124
112
  &before,
125
113
  None,
114
+ "source",
126
115
  "fork",
127
116
  &root,
128
117
  "2026-07-24T00:00:00Z",
@@ -21,6 +21,7 @@ pub(crate) enum ContextForkTermination {
21
21
  mod claude;
22
22
  mod codex;
23
23
  mod outcome;
24
+ pub(crate) use codex::materialize_codex_fork;
24
25
  pub(crate) use outcome::{
25
26
  observe_context_fork, transition_pending_context_fork, ContextForkOutcome, PendingContextFork,
26
27
  };
@@ -47,7 +48,6 @@ pub struct ContextForkProof {
47
48
 
48
49
  #[derive(Debug, Clone)]
49
50
  pub(crate) struct ContextBackingSnapshot {
50
- root: PathBuf,
51
51
  files: BTreeMap<PathBuf, FileStamp>,
52
52
  }
53
53
 
@@ -61,7 +61,7 @@ impl ContextBackingSnapshot {
61
61
  pub(crate) fn capture(provider: Provider, plan: &CommandPlan) -> Self {
62
62
  let root = provider_backing_root(provider, plan);
63
63
  let files = jsonl_files(&root);
64
- Self { root, files }
64
+ Self { files }
65
65
  }
66
66
  }
67
67
 
@@ -71,6 +71,7 @@ pub(crate) fn verify_context_fork(
71
71
  plan: &CommandPlan,
72
72
  before: &ContextBackingSnapshot,
73
73
  expected_backing_path: Option<&Path>,
74
+ _source_agent_id: &str,
74
75
  agent_id: &str,
75
76
  spawn_cwd: &Path,
76
77
  spawned_at: &str,
@@ -84,6 +85,7 @@ pub(crate) fn verify_context_fork(
84
85
  source_session_id,
85
86
  plan,
86
87
  before,
88
+ expected_backing_path,
87
89
  agent_id,
88
90
  spawn_cwd,
89
91
  spawned_at,
@@ -298,6 +300,7 @@ mod tests {
298
300
  &plan,
299
301
  &before,
300
302
  None,
303
+ "source",
301
304
  "fork",
302
305
  &current_cwd,
303
306
  "2026-07-21T20:00:00+00:00",
@@ -344,6 +347,7 @@ mod tests {
344
347
  &plan,
345
348
  &before,
346
349
  Some(&expected_path),
350
+ "source",
347
351
  "fork",
348
352
  &root,
349
353
  "2026-07-22T00:00:00Z",
@@ -382,6 +386,7 @@ mod tests {
382
386
  &plan,
383
387
  &before,
384
388
  Some(&missing),
389
+ "source",
385
390
  "fork",
386
391
  &root,
387
392
  "2026-07-22T00:00:00Z",
@@ -10,8 +10,9 @@ pub mod resume;
10
10
 
11
11
  pub use context_fork::ContextForkProof;
12
12
  pub(crate) use context_fork::{
13
- context_fork_convergence_deadline, observe_context_fork, transition_pending_context_fork,
14
- ContextBackingSnapshot, ContextForkOutcome, PendingContextFork,
13
+ context_fork_convergence_deadline, materialize_codex_fork, observe_context_fork,
14
+ transition_pending_context_fork, ContextBackingSnapshot, ContextForkOutcome,
15
+ PendingContextFork,
15
16
  };
16
17
 
17
18
  pub use resume::{
@@ -4,12 +4,40 @@ pub(super) fn parse_spawned_at(raw: &str) -> Option<std::time::SystemTime> {
4
4
  .map(|dt| std::time::SystemTime::from(dt.with_timezone(&chrono::Utc)))
5
5
  }
6
6
 
7
+ pub(super) fn truncate_to_uuid_precision(
8
+ timestamp: std::time::SystemTime,
9
+ ) -> Option<std::time::SystemTime> {
10
+ let since_epoch = timestamp
11
+ .duration_since(std::time::SystemTime::UNIX_EPOCH)
12
+ .ok()?;
13
+ std::time::SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::from_millis(
14
+ u64::try_from(since_epoch.as_millis()).ok()?,
15
+ ))
16
+ }
17
+
7
18
  pub(super) fn rollout_created_at(path: &std::path::Path) -> Option<std::time::SystemTime> {
8
19
  created_at_from_rollout_uuid(path)
9
20
  .or_else(|| created_at_from_rollout_head(path))
10
21
  .or_else(|| created_at_from_rollout_filename(path))
11
22
  }
12
23
 
24
+ pub(super) fn apply_expected_session_filter(
25
+ context: &super::CaptureSessionContext,
26
+ mut out: Vec<super::CapturedSessionCandidate>,
27
+ ) -> Vec<super::CapturedSessionCandidate> {
28
+ let Some(expected) = context.expected_session_id.as_ref() else {
29
+ return out;
30
+ };
31
+ out.retain(|candidate| {
32
+ candidate
33
+ .captured
34
+ .session_id
35
+ .as_ref()
36
+ .is_some_and(|session| session.as_str() == expected.as_str())
37
+ });
38
+ out
39
+ }
40
+
13
41
  pub(super) fn retain_spawn_cwd(
14
42
  context: &super::CaptureSessionContext,
15
43
  out: &mut Vec<super::CapturedSessionCandidate>,
@@ -146,13 +146,23 @@ pub(super) fn apply_spawned_at_filter(
146
146
  out.clear();
147
147
  return;
148
148
  };
149
+ let codex_spawned_at =
150
+ if matches!(provider, Provider::Codex) && context.expected_session_id.is_some() {
151
+ let Some(timestamp) = super::codex::truncate_to_uuid_precision(spawned_at) else {
152
+ out.clear();
153
+ return;
154
+ };
155
+ timestamp
156
+ } else {
157
+ spawned_at
158
+ };
149
159
  out.retain(|candidate| {
150
160
  let Some(path) = candidate.captured.rollout_path.as_ref() else {
151
161
  return false;
152
162
  };
153
163
  if matches!(provider, Provider::Codex) {
154
164
  return super::codex::rollout_created_at(path.as_path())
155
- .is_some_and(|created_at| created_at >= spawned_at);
165
+ .is_some_and(|created_at| created_at >= codex_spawned_at);
156
166
  }
157
167
  candidate_mtime(path.as_path()).is_some_and(|mtime| mtime >= spawned_at)
158
168
  });
@@ -72,6 +72,9 @@ pub(crate) fn scan_session_candidates_once(
72
72
  if matches!(provider, Provider::Claude | Provider::ClaudeCode) {
73
73
  return claude::apply_expected_session_filter(context, out);
74
74
  }
75
+ if matches!(provider, Provider::Codex) {
76
+ out = codex::apply_expected_session_filter(context, out);
77
+ }
75
78
 
76
79
  common::apply_spawn_time_window_if_unique(provider, context, &mut out);
77
80
  common::sort_expected_first_if_needed(context, &mut out);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.58",
3
+ "version": "0.5.60",
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.58",
24
- "@team-agent/cli-darwin-x64": "0.5.58",
25
- "@team-agent/cli-linux-x64": "0.5.58"
23
+ "@team-agent/cli-darwin-arm64": "0.5.60",
24
+ "@team-agent/cli-darwin-x64": "0.5.60",
25
+ "@team-agent/cli-linux-x64": "0.5.60"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -166,6 +166,7 @@ For diagnosis, run `team-agent profile show deepseek --workspace . --json`; neve
166
166
  - `quick-start` is only for first-time team creation from role docs. If that team already has runtime state, use `team-agent restart . --team <session_name_or_team_name>` to resume it. If restart cannot recover context, explain the loss and wait for explicit user consent before using `team-agent restart . --allow-fresh`; never reset context through quick-start.
167
167
  - If the user explicitly asks a worker to create or operate a nested child team, first read `references/team-in-team.md`. Child teams must use an independent child workspace, never the parent `.team/current`.
168
168
  - `team-agent send --watch-result coder "Do the bounded task"` sends a direct worker message, returns after delivery, and lets the coordinator collect/report completion asynchronously.
169
+ - Positional `TO` has two co-equal forms: an in-team short name, for example `team-agent send reviewer "Review this change"`, and a fully-qualified logical name, `<workspace>::<team>/<agent>`. Use the fully-qualified form across workspaces or when the local team scope is ambiguous.
169
170
  - Advanced orchestration callers may add `--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]`. All sinks remain durable and pullable; `casefile`/`silent` suppress only live leader injection. Missing presentation metadata preserves the normal leader-visible behavior.
170
171
  - After `send --watch-result` succeeds, do not run `sleep`, `status`, `inbox`, or `collect` polling loops unless the user explicitly asks for diagnosis; the coordinator will notify the leader when the result arrives.
171
172
  - `team-agent send --task task_initial "Start"` routes by task.