@team-agent/installer 0.3.20 → 0.3.21
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
package/Cargo.toml
CHANGED
|
@@ -116,6 +116,25 @@ fn detect_session_drift(
|
|
|
116
116
|
.filter(|s| !s.trim().is_empty())?;
|
|
117
117
|
let actual = extract_thread_id_from_scrollback(&fact.scrollback_tail)?;
|
|
118
118
|
if actual.eq_ignore_ascii_case(stored) {
|
|
119
|
+
if agent_has_session_drift(state, &fact.agent_id)
|
|
120
|
+
|| fact
|
|
121
|
+
.agent_state_snapshot
|
|
122
|
+
.get("status")
|
|
123
|
+
.and_then(Value::as_str)
|
|
124
|
+
== Some("session_drift")
|
|
125
|
+
{
|
|
126
|
+
clear_agent_session_drift(state, &fact.agent_id);
|
|
127
|
+
let _ = event_log.write(
|
|
128
|
+
"coordinator.session_drift_cleared",
|
|
129
|
+
json!({
|
|
130
|
+
"agent_id": fact.agent_id.as_str(),
|
|
131
|
+
"stored_session_id": stored,
|
|
132
|
+
"actual_thread_id": actual,
|
|
133
|
+
"status": "running",
|
|
134
|
+
"provider": "codex",
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
119
138
|
return None;
|
|
120
139
|
}
|
|
121
140
|
if fact
|
|
@@ -278,7 +297,9 @@ fn extract_thread_id_from_scrollback(scrollback: &str) -> Option<String> {
|
|
|
278
297
|
while let Some(pos) = lower.get(offset..).and_then(|tail| tail.find(needle)) {
|
|
279
298
|
let start = offset + pos + needle.len();
|
|
280
299
|
if let Some(token) = first_token(scrollback.get(start..).unwrap_or_default()) {
|
|
281
|
-
|
|
300
|
+
if is_uuid_token(&token) {
|
|
301
|
+
found = Some(token.to_ascii_lowercase());
|
|
302
|
+
}
|
|
282
303
|
}
|
|
283
304
|
offset = start;
|
|
284
305
|
}
|
|
@@ -297,11 +318,33 @@ fn first_token(text: &str) -> Option<String> {
|
|
|
297
318
|
.unwrap_or(trimmed);
|
|
298
319
|
let token: String = trimmed
|
|
299
320
|
.chars()
|
|
300
|
-
.take_while(|c| c.is_ascii_alphanumeric() ||
|
|
321
|
+
.take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
|
|
301
322
|
.collect();
|
|
302
323
|
(!token.is_empty()).then_some(token)
|
|
303
324
|
}
|
|
304
325
|
|
|
326
|
+
fn is_uuid_token(token: &str) -> bool {
|
|
327
|
+
let bytes = token.as_bytes();
|
|
328
|
+
if bytes.len() != 36 {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
for (idx, byte) in bytes.iter().enumerate() {
|
|
332
|
+
match idx {
|
|
333
|
+
8 | 13 | 18 | 23 => {
|
|
334
|
+
if *byte != b'-' {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
_ => {
|
|
339
|
+
if !byte.is_ascii_hexdigit() {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
true
|
|
346
|
+
}
|
|
347
|
+
|
|
305
348
|
fn mark_agent_session_drift(
|
|
306
349
|
state: &mut Value,
|
|
307
350
|
agent_id: &AgentId,
|
|
@@ -341,6 +384,49 @@ fn mark_agent_session_drift(
|
|
|
341
384
|
}
|
|
342
385
|
}
|
|
343
386
|
|
|
387
|
+
fn clear_agent_session_drift(state: &mut Value, agent_id: &AgentId) {
|
|
388
|
+
if let Some(agent) = agent_object_mut(state, agent_id) {
|
|
389
|
+
agent.insert("status".to_string(), Value::String("running".to_string()));
|
|
390
|
+
agent.remove("session_drift");
|
|
391
|
+
}
|
|
392
|
+
if let Some(teams) = state.get_mut("teams").and_then(Value::as_object_mut) {
|
|
393
|
+
for team in teams.values_mut() {
|
|
394
|
+
if let Some(agent) = team
|
|
395
|
+
.get_mut("agents")
|
|
396
|
+
.and_then(Value::as_object_mut)
|
|
397
|
+
.and_then(|agents| agents.get_mut(agent_id.as_str()))
|
|
398
|
+
.and_then(Value::as_object_mut)
|
|
399
|
+
{
|
|
400
|
+
agent.insert("status".to_string(), Value::String("running".to_string()));
|
|
401
|
+
agent.remove("session_drift");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
fn agent_has_session_drift(state: &Value, agent_id: &AgentId) -> bool {
|
|
408
|
+
state
|
|
409
|
+
.get("agents")
|
|
410
|
+
.and_then(Value::as_object)
|
|
411
|
+
.and_then(|agents| agents.get(agent_id.as_str()))
|
|
412
|
+
.and_then(|agent| agent.get("status"))
|
|
413
|
+
.and_then(Value::as_str)
|
|
414
|
+
== Some("session_drift")
|
|
415
|
+
|| state
|
|
416
|
+
.get("teams")
|
|
417
|
+
.and_then(Value::as_object)
|
|
418
|
+
.is_some_and(|teams| {
|
|
419
|
+
teams.values().any(|team| {
|
|
420
|
+
team.get("agents")
|
|
421
|
+
.and_then(Value::as_object)
|
|
422
|
+
.and_then(|agents| agents.get(agent_id.as_str()))
|
|
423
|
+
.and_then(|agent| agent.get("status"))
|
|
424
|
+
.and_then(Value::as_str)
|
|
425
|
+
== Some("session_drift")
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
|
|
344
430
|
fn match_api_error(scrollback: &str) -> Option<(String, String)> {
|
|
345
431
|
let lines: Vec<String> = scrollback
|
|
346
432
|
.lines()
|
|
@@ -510,3 +596,166 @@ fn tail_chars(text: &str, max_chars: usize) -> String {
|
|
|
510
596
|
let start = chars.len().saturating_sub(max_chars);
|
|
511
597
|
chars[start..].iter().collect()
|
|
512
598
|
}
|
|
599
|
+
|
|
600
|
+
#[cfg(test)]
|
|
601
|
+
mod tests {
|
|
602
|
+
use super::*;
|
|
603
|
+
use std::collections::BTreeMap;
|
|
604
|
+
use std::path::PathBuf;
|
|
605
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
606
|
+
|
|
607
|
+
use crate::model::ids::{AgentId, TeamKey};
|
|
608
|
+
|
|
609
|
+
const STORED: &str = "11111111-1111-4111-8111-111111111111";
|
|
610
|
+
const OTHER: &str = "22222222-2222-4222-8222-222222222222";
|
|
611
|
+
|
|
612
|
+
#[test]
|
|
613
|
+
fn session_drift_ignores_plain_text_after_marker_words() {
|
|
614
|
+
let workspace = temp_workspace("plain-text");
|
|
615
|
+
let mut state = state_with_agent("running", None);
|
|
616
|
+
let captures = captures("w1", STORED, "Need resume confirmation before changing anything.\nSeveral threads mention evidence.\n");
|
|
617
|
+
|
|
618
|
+
let observed = observe_runtime(&workspace, &mut state, captures, None);
|
|
619
|
+
|
|
620
|
+
assert!(
|
|
621
|
+
observed.session_drift.is_empty(),
|
|
622
|
+
"plain prose must not be treated as a runtime id observation: {observed:?}"
|
|
623
|
+
);
|
|
624
|
+
assert_eq!(
|
|
625
|
+
state.pointer("/agents/w1/status").and_then(Value::as_str),
|
|
626
|
+
Some("running")
|
|
627
|
+
);
|
|
628
|
+
assert!(state.pointer("/agents/w1/session_drift").is_none());
|
|
629
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
#[test]
|
|
633
|
+
fn session_drift_marks_real_uuid_mismatch() {
|
|
634
|
+
let workspace = temp_workspace("uuid-mismatch");
|
|
635
|
+
let mut state = state_with_agent("running", None);
|
|
636
|
+
let captures = captures(
|
|
637
|
+
"w1",
|
|
638
|
+
STORED,
|
|
639
|
+
&format!("Codex resumed. Switched to thread {OTHER}\n"),
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
let observed = observe_runtime(&workspace, &mut state, captures, None);
|
|
643
|
+
|
|
644
|
+
assert_eq!(observed.session_drift.len(), 1, "{observed:?}");
|
|
645
|
+
assert_eq!(
|
|
646
|
+
state.pointer("/agents/w1/status").and_then(Value::as_str),
|
|
647
|
+
Some("session_drift")
|
|
648
|
+
);
|
|
649
|
+
assert_eq!(
|
|
650
|
+
state
|
|
651
|
+
.pointer("/agents/w1/session_drift/actual_thread_id")
|
|
652
|
+
.and_then(Value::as_str),
|
|
653
|
+
Some(OTHER)
|
|
654
|
+
);
|
|
655
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
#[test]
|
|
659
|
+
fn session_drift_clears_existing_marker_when_uuid_matches() {
|
|
660
|
+
let workspace = temp_workspace("uuid-match-clear");
|
|
661
|
+
let mut state = state_with_agent(
|
|
662
|
+
"session_drift",
|
|
663
|
+
Some(json!({
|
|
664
|
+
"stored_session_id": STORED,
|
|
665
|
+
"actual_thread_id": OTHER,
|
|
666
|
+
"detected_at": "2026-06-13T00:00:00Z",
|
|
667
|
+
"remediation": "old"
|
|
668
|
+
})),
|
|
669
|
+
);
|
|
670
|
+
let captures = captures("w1", STORED, &format!("resume {STORED}\n"));
|
|
671
|
+
|
|
672
|
+
let observed = observe_runtime(&workspace, &mut state, captures, None);
|
|
673
|
+
|
|
674
|
+
assert!(
|
|
675
|
+
observed.session_drift.is_empty(),
|
|
676
|
+
"matching observation clears state instead of reporting a new drift: {observed:?}"
|
|
677
|
+
);
|
|
678
|
+
assert_eq!(
|
|
679
|
+
state.pointer("/agents/w1/status").and_then(Value::as_str),
|
|
680
|
+
Some("running")
|
|
681
|
+
);
|
|
682
|
+
assert!(state.pointer("/agents/w1/session_drift").is_none());
|
|
683
|
+
assert_eq!(
|
|
684
|
+
state
|
|
685
|
+
.pointer("/teams/current/agents/w1/status")
|
|
686
|
+
.and_then(Value::as_str),
|
|
687
|
+
Some("running")
|
|
688
|
+
);
|
|
689
|
+
assert!(state
|
|
690
|
+
.pointer("/teams/current/agents/w1/session_drift")
|
|
691
|
+
.is_none());
|
|
692
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
fn captures(
|
|
696
|
+
agent_id: &str,
|
|
697
|
+
stored_session_id: &str,
|
|
698
|
+
scrollback_tail: &str,
|
|
699
|
+
) -> BTreeMap<AgentId, CapturedRuntimeFact> {
|
|
700
|
+
let agent_id = AgentId::new(agent_id);
|
|
701
|
+
let snapshot = json!({
|
|
702
|
+
"provider": "codex",
|
|
703
|
+
"status": "running",
|
|
704
|
+
"session_id": stored_session_id,
|
|
705
|
+
});
|
|
706
|
+
BTreeMap::from([(
|
|
707
|
+
agent_id.clone(),
|
|
708
|
+
CapturedRuntimeFact {
|
|
709
|
+
team_key: Some(TeamKey::new("current")),
|
|
710
|
+
agent_id,
|
|
711
|
+
provider: Some(Provider::Codex),
|
|
712
|
+
session_name: None,
|
|
713
|
+
window: None,
|
|
714
|
+
pane_id: None,
|
|
715
|
+
scrollback_tail: scrollback_tail.to_string(),
|
|
716
|
+
pane_info: None,
|
|
717
|
+
agent_state_snapshot: snapshot,
|
|
718
|
+
stored_session_id: Some(stored_session_id.to_string()),
|
|
719
|
+
last_output_at: None,
|
|
720
|
+
rollout_path: None,
|
|
721
|
+
process_liveness: None,
|
|
722
|
+
},
|
|
723
|
+
)])
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
fn state_with_agent(status: &str, session_drift: Option<Value>) -> Value {
|
|
727
|
+
let mut agent = json!({
|
|
728
|
+
"provider": "codex",
|
|
729
|
+
"status": status,
|
|
730
|
+
"session_id": STORED,
|
|
731
|
+
});
|
|
732
|
+
if let Some(drift) = session_drift {
|
|
733
|
+
agent["session_drift"] = drift;
|
|
734
|
+
}
|
|
735
|
+
json!({
|
|
736
|
+
"active_team_key": "current",
|
|
737
|
+
"agents": {
|
|
738
|
+
"w1": agent.clone()
|
|
739
|
+
},
|
|
740
|
+
"teams": {
|
|
741
|
+
"current": {
|
|
742
|
+
"agents": {
|
|
743
|
+
"w1": agent
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
fn temp_workspace(tag: &str) -> PathBuf {
|
|
751
|
+
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
752
|
+
let dir = std::env::temp_dir().join(format!(
|
|
753
|
+
"ta-rs-e28-{tag}-{}-{}",
|
|
754
|
+
std::process::id(),
|
|
755
|
+
COUNTER.fetch_add(1, Ordering::Relaxed)
|
|
756
|
+
));
|
|
757
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
758
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
759
|
+
std::fs::canonicalize(dir).unwrap()
|
|
760
|
+
}
|
|
761
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
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.3.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.3.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.3.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.3.21",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.21",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.21"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|