@team-agent/installer 0.5.74 → 0.5.77

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 (45) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/README.zh.md +8 -0
  5. package/crates/team-agent/src/cli/adapters.rs +23 -96
  6. package/crates/team-agent/src/cli/emit.rs +35 -7
  7. package/crates/team-agent/src/cli/mod.rs +63 -42
  8. package/crates/team-agent/src/cli/models.rs +138 -27
  9. package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
  10. package/crates/team-agent/src/cli/status_port.rs +8 -0
  11. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  12. package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
  13. package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
  15. package/crates/team-agent/src/layout/worker_env.rs +366 -0
  16. package/crates/team-agent/src/leader/lease.rs +274 -40
  17. package/crates/team-agent/src/leader/registry.rs +177 -19
  18. package/crates/team-agent/src/leader/types.rs +4 -0
  19. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +93 -5
  20. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1820 -60
  21. package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
  22. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
  23. package/crates/team-agent/src/lifecycle/restart/common.rs +3 -0
  24. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +11 -4
  25. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
  26. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -0
  27. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  28. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +24 -28
  29. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
  30. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
  31. package/crates/team-agent/src/lifecycle/types.rs +4 -0
  32. package/crates/team-agent/src/messaging/delivery.rs +200 -29
  33. package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
  34. package/crates/team-agent/src/messaging/watchers.rs +48 -4
  35. package/crates/team-agent/src/state/paths.rs +17 -2
  36. package/crates/team-agent/src/state/persist.rs +166 -6
  37. package/crates/team-agent/src/state/projection.rs +18 -2
  38. package/crates/team-agent/src/state/repository.rs +36 -0
  39. package/crates/team-agent/src/state/selector.rs +23 -3
  40. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +122 -0
  41. package/crates/team-agent/src/tmux_backend/tests.rs +350 -7
  42. package/crates/team-agent/src/tmux_backend.rs +217 -26
  43. package/crates/team-agent/src/transport/test_support.rs +18 -3
  44. package/crates/team-agent/src/transport.rs +99 -0
  45. package/package.json +4 -4
@@ -61,13 +61,15 @@ use std::process::Stdio;
61
61
  use std::time::{Duration, Instant};
62
62
 
63
63
  use crate::model::enums::PaneLiveness;
64
+ use crate::provider::wire::{parse_provider, provider_wire};
64
65
  use crate::transport::{
65
- normalize_capture, tmux_capture_argv, tmux_empty_inject_argv, tmux_inject_text_argv,
66
- tmux_query_argv, tmux_send_keys_argv, tmux_send_submit_argv, tmux_spawn_argv, AttachOutcome,
67
- BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport, InjectStage,
68
- InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneMode, SessionName, SetEnvOutcome,
69
- SpawnResult, SubmitAttemptObservation, SubmitObserver, SubmitVerification, Target, Transport,
70
- TransportError, TurnVerification, WindowName,
66
+ command_basename, normalize_capture, tmux_capture_argv, tmux_empty_inject_argv,
67
+ tmux_inject_text_argv, tmux_query_argv, tmux_send_keys_argv, tmux_send_submit_argv,
68
+ tmux_spawn_argv, AttachOutcome, BackendKind, CaptureRange, CaptureSampleOutcome, CapturedText,
69
+ InjectPayload, InjectReport, InjectStage, InjectVerification, InputSurfaceProbe, Key,
70
+ PaneField, PaneId, PaneInfo, PaneMode, QueryOutcome, SessionName, SetEnvOutcome, SpawnResult,
71
+ SubmitAttemptObservation, SubmitDiagnostics, SubmitObserver, SubmitVerification, Target,
72
+ Transport, TransportError, TurnVerification, WindowName,
71
73
  };
72
74
 
73
75
  pub const PANE_BINDING_NONCE_METADATA_KEY: &str = "TEAM_AGENT_PANE_BINDING_NONCE";
@@ -1446,6 +1448,107 @@ fn pane_from_target(target: &Target) -> PaneId {
1446
1448
  }
1447
1449
  }
1448
1450
 
1451
+ #[derive(Default)]
1452
+ struct CaptureTally {
1453
+ ok: u32,
1454
+ err: u32,
1455
+ last: CaptureSampleOutcome,
1456
+ }
1457
+
1458
+ impl CaptureTally {
1459
+ fn record_ok(&mut self, text: &str, marker: Option<&str>) {
1460
+ self.ok = self.ok.saturating_add(1);
1461
+ self.last = if marker.is_some_and(|token| text.contains(token)) {
1462
+ CaptureSampleOutcome::OkTokenSeen
1463
+ } else {
1464
+ CaptureSampleOutcome::OkTokenMissing
1465
+ };
1466
+ }
1467
+
1468
+ fn record_err(&mut self) {
1469
+ self.err = self.err.saturating_add(1);
1470
+ self.last = CaptureSampleOutcome::Failed;
1471
+ }
1472
+
1473
+ fn seen_after_ok_samples(&self, visible: bool) -> Option<bool> {
1474
+ if self.ok == 0 {
1475
+ None
1476
+ } else {
1477
+ Some(visible)
1478
+ }
1479
+ }
1480
+ }
1481
+
1482
+ struct InjectTargetSurface {
1483
+ pane_command_basename: Option<String>,
1484
+ pane_command_query: QueryOutcome,
1485
+ input_surface: InputSurfaceProbe,
1486
+ target_pane_id: String,
1487
+ target_pane_query_matched: Option<bool>,
1488
+ }
1489
+
1490
+ fn probe_inject_target_surface(backend: &TmuxBackend, target: &Target) -> InjectTargetSurface {
1491
+ let pane = pane_from_target(target);
1492
+ let (pane_command_basename, pane_command_query) =
1493
+ match backend.query(target, PaneField::PaneCurrentCommand) {
1494
+ Ok(Some(raw)) => match command_basename(&raw) {
1495
+ Some(name) => (Some(name), QueryOutcome::Observed),
1496
+ None => (None, QueryOutcome::Unavailable),
1497
+ },
1498
+ _ => (None, QueryOutcome::Unavailable),
1499
+ };
1500
+ let input_surface = match backend.query(target, PaneField::PaneMode) {
1501
+ Ok(Some(raw)) => match pane_mode_from_raw(Some(raw)) {
1502
+ None => InputSurfaceProbe::Input,
1503
+ Some(PaneMode::Copy) => InputSurfaceProbe::Copy,
1504
+ Some(PaneMode::Tree) => InputSurfaceProbe::Tree,
1505
+ Some(PaneMode::View) => InputSurfaceProbe::View,
1506
+ Some(PaneMode::Client) => InputSurfaceProbe::Client,
1507
+ Some(PaneMode::Unknown) => InputSurfaceProbe::UnknownMode,
1508
+ },
1509
+ _ => InputSurfaceProbe::Unavailable,
1510
+ };
1511
+ let target_pane_query_matched = match backend.query(target, PaneField::PaneId) {
1512
+ Ok(Some(id)) if !id.is_empty() => Some(id == pane.as_str()),
1513
+ _ => None,
1514
+ };
1515
+ InjectTargetSurface {
1516
+ pane_command_basename,
1517
+ pane_command_query,
1518
+ input_surface,
1519
+ target_pane_id: pane.as_str().to_string(),
1520
+ target_pane_query_matched,
1521
+ }
1522
+ }
1523
+
1524
+ fn submit_diagnostics_for_inject(
1525
+ total_elapsed_ms: u64,
1526
+ attempts_detail: Vec<SubmitAttemptObservation>,
1527
+ tally: &CaptureTally,
1528
+ token_seen_after_paste: Option<bool>,
1529
+ token_seen_after_enter: Option<bool>,
1530
+ surface: InjectTargetSurface,
1531
+ ) -> SubmitDiagnostics {
1532
+ SubmitDiagnostics {
1533
+ appear_gate_elapsed_ms: 0,
1534
+ appear_gate_matched: false,
1535
+ total_elapsed_ms,
1536
+ attempts_detail,
1537
+ token_seen_before_paste: None,
1538
+ token_seen_after_paste,
1539
+ token_seen_after_enter,
1540
+ capture_ok_count: tally.ok,
1541
+ capture_err_count: tally.err,
1542
+ capture_sample_count: tally.ok.saturating_add(tally.err),
1543
+ last_capture_outcome: tally.last,
1544
+ pane_command_basename: surface.pane_command_basename,
1545
+ pane_command_query: surface.pane_command_query,
1546
+ input_surface: surface.input_surface,
1547
+ target_pane_id: Some(surface.target_pane_id),
1548
+ target_pane_query_matched: surface.target_pane_query_matched,
1549
+ }
1550
+ }
1551
+
1449
1552
  fn target_name(target: &Target) -> String {
1450
1553
  match target {
1451
1554
  Target::Pane(pane) => pane.as_str().to_string(),
@@ -1647,13 +1750,23 @@ fn post_submit_token_visible(
1647
1750
  backend: &TmuxBackend,
1648
1751
  target: &Target,
1649
1752
  payload: &InjectPayload,
1753
+ tally: &mut CaptureTally,
1650
1754
  ) -> Result<Option<bool>, TransportError> {
1651
- if payload_token_marker(payload).is_none() {
1755
+ let Some(marker) = payload_token_marker(payload) else {
1652
1756
  return Ok(None);
1653
- }
1757
+ };
1654
1758
  for attempt in 0..TOKEN_POST_SUBMIT_READBACK_POLLS {
1655
- if let Some(true) = token_visible_in_capture(backend, target, payload)? {
1656
- return Ok(Some(true));
1759
+ match backend.capture(target, CaptureRange::Tail(80)) {
1760
+ Ok(cap) => {
1761
+ tally.record_ok(&cap.text, Some(marker));
1762
+ if cap.text.contains(marker) {
1763
+ return Ok(Some(true));
1764
+ }
1765
+ }
1766
+ Err(_) => {
1767
+ tally.record_err();
1768
+ return Ok(None);
1769
+ }
1657
1770
  }
1658
1771
  if attempt + 1 < TOKEN_POST_SUBMIT_READBACK_POLLS {
1659
1772
  std::thread::sleep(Duration::from_millis(25));
@@ -2445,6 +2558,7 @@ fn shell_command(
2445
2558
  ) -> String {
2446
2559
  let unset_set: std::collections::BTreeSet<&str> =
2447
2560
  env_unset.iter().map(String::as_str).collect();
2561
+ let caller_provider = caller_provider_for_invocation(env);
2448
2562
  let mut parts = Vec::new();
2449
2563
  parts.push("cd".to_string());
2450
2564
  parts.push(shell_quote(&cwd.to_string_lossy()));
@@ -2464,16 +2578,47 @@ fn shell_command(
2464
2578
  // contains the very keys we want to scrub (e.g. CLAUDE_EFFORT carried
2465
2579
  // forward from the launching shell into the env map).
2466
2580
  for (key, value) in env {
2467
- if unset_set.contains(key.as_str()) {
2581
+ if unset_set.contains(key.as_str())
2582
+ || key.starts_with(crate::layout::worker_env::CALLER_CONTEXT_PREFIX)
2583
+ {
2468
2584
  continue;
2469
2585
  }
2470
2586
  parts.push(format!("{key}={}", shell_quote(value)));
2471
2587
  }
2588
+ append_caller_context_assignments(&mut parts, caller_provider);
2472
2589
  parts.push("exec".to_string());
2473
2590
  parts.extend(argv.iter().map(|arg| shell_quote(arg)));
2474
2591
  parts.join(" ")
2475
2592
  }
2476
2593
 
2594
+ fn caller_provider_for_invocation(env: &BTreeMap<String, String>) -> Option<&'static str> {
2595
+ env.get(crate::layout::worker_env::CALLER_PROVIDER_ENV)
2596
+ .and_then(|value| parse_provider(value))
2597
+ .map(provider_wire)
2598
+ }
2599
+
2600
+ fn append_caller_context_assignments(parts: &mut Vec<String>, provider: Option<&str>) {
2601
+ let Some(provider) = provider else {
2602
+ return;
2603
+ };
2604
+ parts.push(format!(
2605
+ "{}={}",
2606
+ crate::layout::worker_env::CALLER_PROVIDER_ENV,
2607
+ shell_quote(provider)
2608
+ ));
2609
+ // These are expanded by the shell created inside the target pane. They are
2610
+ // assignments on the provider command only, so the exit marker/inert tail
2611
+ // cannot retain a usable caller context.
2612
+ parts.push(format!(
2613
+ "{}=\"${{TMUX_PANE-}}\"",
2614
+ crate::layout::worker_env::CALLER_PANE_ENV
2615
+ ));
2616
+ parts.push(format!(
2617
+ "{}=\"${{TMUX%%,*}}\"",
2618
+ crate::layout::worker_env::CALLER_ENDPOINT_ENV
2619
+ ));
2620
+ }
2621
+
2477
2622
  /// 0.4.x (CR R6): single-source marker prefix. The exit marker emitted by
2478
2623
  /// `leader_shell_wrapper_command` and the substring detected by
2479
2624
  /// `leader_provider_health` MUST share this prefix exactly. Format:
@@ -2643,6 +2788,7 @@ pub fn worker_shell_wrapper_command(
2643
2788
  ) -> String {
2644
2789
  let unset_set: std::collections::BTreeSet<&str> =
2645
2790
  env_unset.iter().map(String::as_str).collect();
2791
+ let caller_provider = caller_provider_for_invocation(env);
2646
2792
  let mut parts = Vec::new();
2647
2793
  parts.push("cd".to_string());
2648
2794
  parts.push(shell_quote(&cwd.to_string_lossy()));
@@ -2653,11 +2799,14 @@ pub fn worker_shell_wrapper_command(
2653
2799
  parts.push("&&".to_string());
2654
2800
  }
2655
2801
  for (key, value) in env {
2656
- if unset_set.contains(key.as_str()) {
2802
+ if unset_set.contains(key.as_str())
2803
+ || key.starts_with(crate::layout::worker_env::CALLER_CONTEXT_PREFIX)
2804
+ {
2657
2805
  continue;
2658
2806
  }
2659
2807
  parts.push(format!("{key}={}", shell_quote(value)));
2660
2808
  }
2809
+ append_caller_context_assignments(&mut parts, caller_provider);
2661
2810
  parts.extend(argv.iter().map(|arg| shell_quote(arg)));
2662
2811
  parts.push(";".to_string());
2663
2812
  parts.push("rc=$?;".to_string());
@@ -3002,11 +3151,15 @@ impl Transport for TmuxBackend {
3002
3151
  let poll_start = std::time::Instant::now();
3003
3152
  let mut token_ever_visible = false;
3004
3153
  let mut tracked_paste: Option<PasteLatch> = None;
3154
+ let mut capture_tally = CaptureTally::default();
3155
+ let mut token_seen_after_paste = None;
3156
+ let mut token_seen_after_enter = None;
3005
3157
  token_visible_for_report = if let Some(m) = payload_token_marker(payload) {
3006
3158
  let mut visible = false;
3007
3159
  while poll_start.elapsed().as_millis() < token_poll_timeout_ms as u128 {
3008
3160
  match self.capture(target, CaptureRange::Tail(80)) {
3009
3161
  Ok(cap) => {
3162
+ capture_tally.record_ok(&cap.text, Some(m));
3010
3163
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3011
3164
  if cap.text.contains(m) {
3012
3165
  visible = true;
@@ -3017,10 +3170,14 @@ impl Transport for TmuxBackend {
3017
3170
  break;
3018
3171
  }
3019
3172
  }
3020
- Err(_) => break, // tmux unavailable, skip poll
3173
+ Err(_) => {
3174
+ capture_tally.record_err();
3175
+ break; // tmux unavailable, skip poll
3176
+ }
3021
3177
  }
3022
3178
  std::thread::sleep(Duration::from_millis(50));
3023
3179
  }
3180
+ token_seen_after_paste = capture_tally.seen_after_ok_samples(visible);
3024
3181
  Some(visible)
3025
3182
  } else {
3026
3183
  None
@@ -3042,6 +3199,7 @@ impl Transport for TmuxBackend {
3042
3199
  self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
3043
3200
  notify_submit_observer(observer);
3044
3201
  let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
3202
+ let surface = probe_inject_target_surface(self, target);
3045
3203
  return Ok(InjectReport {
3046
3204
  stage_reached: InjectStage::Submit,
3047
3205
  inject_verification: inject_verification_after_readback(
@@ -3051,12 +3209,14 @@ impl Transport for TmuxBackend {
3051
3209
  submit_verification: submit_verification_for_key(submit),
3052
3210
  turn_verification: TurnVerification::NotYetObserved,
3053
3211
  attempts: 1,
3054
- submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
3055
- appear_gate_elapsed_ms: 0,
3056
- appear_gate_matched: false,
3212
+ submit_diagnostics: Some(submit_diagnostics_for_inject(
3057
3213
  total_elapsed_ms,
3058
- attempts_detail: Vec::new(),
3059
- }),
3214
+ Vec::new(),
3215
+ &capture_tally,
3216
+ token_seen_after_paste,
3217
+ None,
3218
+ surface,
3219
+ )),
3060
3220
  });
3061
3221
  }
3062
3222
 
@@ -3099,8 +3259,12 @@ impl Transport for TmuxBackend {
3099
3259
  if attempt > 0 {
3100
3260
  if let Some(m) = marker {
3101
3261
  if let Ok(cap) = self.capture(target, CaptureRange::Tail(40)) {
3262
+ capture_tally.record_ok(&cap.text, Some(m));
3102
3263
  if cap.text.contains(m) {
3103
3264
  token_ever_visible = true;
3265
+ token_seen_after_enter = Some(true);
3266
+ } else if token_seen_after_enter.is_none() {
3267
+ token_seen_after_enter = Some(false);
3104
3268
  }
3105
3269
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3106
3270
  let obs = submit_attempt_observation(
@@ -3172,10 +3336,22 @@ impl Transport for TmuxBackend {
3172
3336
  // Post-submit token readback (U1 #7 parity: check token
3173
3337
  // visible after Enter for no-echo panes).
3174
3338
  if attempt == 0 && matches!(token_visible_for_report, Some(false)) {
3175
- token_visible_for_report =
3176
- post_submit_token_visible(self, target, payload).unwrap_or(Some(false));
3177
- if token_visible_for_report == Some(true) {
3339
+ let post = post_submit_token_visible(
3340
+ self,
3341
+ target,
3342
+ payload,
3343
+ &mut capture_tally,
3344
+ )
3345
+ .unwrap_or(None);
3346
+ if post == Some(true) {
3347
+ token_visible_for_report = Some(true);
3178
3348
  token_ever_visible = true;
3349
+ token_seen_after_enter = Some(true);
3350
+ } else {
3351
+ token_visible_for_report = Some(false);
3352
+ if post == Some(false) {
3353
+ token_seen_after_enter = Some(false);
3354
+ }
3179
3355
  }
3180
3356
  }
3181
3357
 
@@ -3189,10 +3365,14 @@ impl Transport for TmuxBackend {
3189
3365
  std::thread::sleep(Duration::from_millis(100));
3190
3366
  match self.capture(target, CaptureRange::Tail(40)) {
3191
3367
  Ok(cap) => {
3368
+ capture_tally.record_ok(&cap.text, Some(m));
3192
3369
  consecutive_capture_failures = 0;
3193
3370
  saw_capture = true;
3194
3371
  if cap.text.contains(m) {
3195
3372
  token_ever_visible = true;
3373
+ token_seen_after_enter = Some(true);
3374
+ } else if token_seen_after_enter.is_none() {
3375
+ token_seen_after_enter = Some(false);
3196
3376
  }
3197
3377
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3198
3378
  let obs = submit_attempt_observation(
@@ -3223,6 +3403,7 @@ impl Transport for TmuxBackend {
3223
3403
  }
3224
3404
  }
3225
3405
  Err(_) => {
3406
+ capture_tally.record_err();
3226
3407
  capture_retries = capture_retries.saturating_add(1);
3227
3408
  consecutive_capture_failures =
3228
3409
  consecutive_capture_failures.saturating_add(1);
@@ -3322,8 +3503,12 @@ impl Transport for TmuxBackend {
3322
3503
  std::thread::sleep(Duration::from_millis(100));
3323
3504
  match self.capture(target, CaptureRange::Tail(40)) {
3324
3505
  Ok(cap) => {
3506
+ capture_tally.record_ok(&cap.text, Some(m));
3325
3507
  if cap.text.contains(m) {
3326
3508
  token_ever_visible = true;
3509
+ token_seen_after_enter = Some(true);
3510
+ } else if token_seen_after_enter.is_none() {
3511
+ token_seen_after_enter = Some(false);
3327
3512
  }
3328
3513
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3329
3514
  let obs = submit_attempt_observation(
@@ -3361,7 +3546,10 @@ impl Transport for TmuxBackend {
3361
3546
  break;
3362
3547
  }
3363
3548
  }
3364
- Err(_) => break,
3549
+ Err(_) => {
3550
+ capture_tally.record_err();
3551
+ break;
3552
+ }
3365
3553
  }
3366
3554
  }
3367
3555
  if found_consumed {
@@ -3373,6 +3561,7 @@ impl Transport for TmuxBackend {
3373
3561
  }
3374
3562
  }
3375
3563
  let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
3564
+ let surface = probe_inject_target_surface(self, target);
3376
3565
  return Ok(InjectReport {
3377
3566
  stage_reached: InjectStage::Submit,
3378
3567
  inject_verification: inject_verification_after_readback(
@@ -3385,12 +3574,14 @@ impl Transport for TmuxBackend {
3385
3574
  last_turn_text.as_deref(),
3386
3575
  ),
3387
3576
  attempts: consumption_attempts.saturating_add(capture_retries),
3388
- submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
3389
- appear_gate_elapsed_ms: 0,
3390
- appear_gate_matched: false,
3577
+ submit_diagnostics: Some(submit_diagnostics_for_inject(
3391
3578
  total_elapsed_ms,
3392
3579
  attempts_detail,
3393
- }),
3580
+ &capture_tally,
3581
+ token_seen_after_paste,
3582
+ token_seen_after_enter,
3583
+ surface,
3584
+ )),
3394
3585
  });
3395
3586
  }
3396
3587
  }
@@ -1,7 +1,7 @@
1
1
  //! 测试默认零真-spawn、零真-tmux:lifecycle/CLI spawn 路径必经 *_with_transport 注入离线
2
2
  //! mock;确需真 tmux 者必须 provider:fake + RAII kill_server 守卫 + #[ignore=real-machine].
3
3
 
4
- use std::collections::BTreeMap;
4
+ use std::collections::{BTreeMap, VecDeque};
5
5
  use std::path::Path;
6
6
  use std::sync::{Arc, Mutex};
7
7
 
@@ -26,6 +26,7 @@ struct OfflineState {
26
26
  session_present: bool,
27
27
  session_absent_after_spawn_first: bool,
28
28
  targets: Vec<PaneInfo>,
29
+ target_snapshots: VecDeque<Vec<PaneInfo>>,
29
30
  windows: Vec<WindowName>,
30
31
  pane_presence: BTreeMap<String, bool>,
31
32
  spawn_failures: BTreeMap<String, String>,
@@ -62,6 +63,7 @@ impl Default for OfflineState {
62
63
  session_present: false,
63
64
  session_absent_after_spawn_first: false,
64
65
  targets: Vec::new(),
66
+ target_snapshots: VecDeque::new(),
65
67
  windows: Vec::new(),
66
68
  pane_presence: BTreeMap::new(),
67
69
  spawn_failures: BTreeMap::new(),
@@ -104,7 +106,15 @@ impl OfflineTransport {
104
106
  }
105
107
 
106
108
  pub fn with_targets(self, targets: Vec<PaneInfo>) -> Self {
107
- self.with_state(|state| state.targets = targets);
109
+ self.with_state(|state| {
110
+ state.targets = targets;
111
+ state.target_snapshots.clear();
112
+ });
113
+ self
114
+ }
115
+
116
+ pub fn with_target_snapshots(self, snapshots: Vec<Vec<PaneInfo>>) -> Self {
117
+ self.with_state(|state| state.target_snapshots = snapshots.into());
108
118
  self
109
119
  }
110
120
 
@@ -507,7 +517,12 @@ impl Transport for OfflineTransport {
507
517
  detail,
508
518
  });
509
519
  }
510
- Ok(self.with_state(|state| state.targets.clone()))
520
+ Ok(self.with_state(|state| {
521
+ state
522
+ .target_snapshots
523
+ .pop_front()
524
+ .unwrap_or_else(|| state.targets.clone())
525
+ }))
511
526
  }
512
527
 
513
528
  fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
@@ -483,6 +483,105 @@ pub struct SubmitDiagnostics {
483
483
  /// prompt loop. Empty when `saw_pasted_prompt == false` and the inject
484
484
  /// went through the E46 token path.
485
485
  pub attempts_detail: Vec<SubmitAttemptObservation>,
486
+ /// Token visibility before paste. `None` means this inject did not sample
487
+ /// (honest unknown). Never inferred from later captures.
488
+ pub token_seen_before_paste: Option<bool>,
489
+ /// Token visibility from successful post-paste captures. `None` if every
490
+ /// post-paste capture failed or none ran.
491
+ pub token_seen_after_paste: Option<bool>,
492
+ /// Token visibility from successful post-Enter captures. `None` if none ran
493
+ /// or all failed.
494
+ pub token_seen_after_enter: Option<bool>,
495
+ pub capture_ok_count: u32,
496
+ pub capture_err_count: u32,
497
+ pub capture_sample_count: u32,
498
+ pub last_capture_outcome: CaptureSampleOutcome,
499
+ /// Basename only of `#{pane_current_command}` at inject; never argv.
500
+ pub pane_command_basename: Option<String>,
501
+ pub pane_command_query: QueryOutcome,
502
+ pub input_surface: InputSurfaceProbe,
503
+ pub target_pane_id: Option<String>,
504
+ /// Inject target pane id vs `#{pane_id}` queried on that same target.
505
+ pub target_pane_query_matched: Option<bool>,
506
+ }
507
+
508
+ /// Last capture sample of the inject token path. Distinguishes "read failed"
509
+ /// from "read succeeded and token was absent". Does not change delivery gates.
510
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
511
+ pub enum CaptureSampleOutcome {
512
+ #[default]
513
+ NotAttempted,
514
+ OkTokenSeen,
515
+ OkTokenMissing,
516
+ Failed,
517
+ }
518
+
519
+ impl CaptureSampleOutcome {
520
+ pub fn as_str(self) -> &'static str {
521
+ match self {
522
+ Self::NotAttempted => "not_attempted",
523
+ Self::OkTokenSeen => "ok_token_seen",
524
+ Self::OkTokenMissing => "ok_token_missing",
525
+ Self::Failed => "failed",
526
+ }
527
+ }
528
+ }
529
+
530
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
531
+ pub enum QueryOutcome {
532
+ #[default]
533
+ Unavailable,
534
+ Observed,
535
+ }
536
+
537
+ impl QueryOutcome {
538
+ pub fn as_str(self) -> &'static str {
539
+ match self {
540
+ Self::Unavailable => "unavailable",
541
+ Self::Observed => "observed",
542
+ }
543
+ }
544
+ }
545
+
546
+ /// Typed pane input surface from existing `pane_mode` probe. `Unavailable`
547
+ /// means the query failed; it is not a pass.
548
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
549
+ pub enum InputSurfaceProbe {
550
+ #[default]
551
+ Unavailable,
552
+ Input,
553
+ Copy,
554
+ Tree,
555
+ View,
556
+ Client,
557
+ UnknownMode,
558
+ }
559
+
560
+ impl InputSurfaceProbe {
561
+ pub fn as_str(self) -> &'static str {
562
+ match self {
563
+ Self::Unavailable => "unavailable",
564
+ Self::Input => "input",
565
+ Self::Copy => "copy",
566
+ Self::Tree => "tree",
567
+ Self::View => "view",
568
+ Self::Client => "client",
569
+ Self::UnknownMode => "unknown_mode",
570
+ }
571
+ }
572
+ }
573
+
574
+ /// First token of a pane command, basename only. Empty/missing → `None`.
575
+ pub(crate) fn command_basename(raw: &str) -> Option<String> {
576
+ let first = raw.split_whitespace().next()?.trim();
577
+ if first.is_empty() {
578
+ return None;
579
+ }
580
+ std::path::Path::new(first)
581
+ .file_name()
582
+ .and_then(|name| name.to_str())
583
+ .filter(|name| !name.is_empty())
584
+ .map(str::to_string)
486
585
  }
487
586
 
488
587
  /// E50 PR-1 single attempt observation (one Enter + one capture). All fields
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.74",
3
+ "version": "0.5.77",
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.74",
24
- "@team-agent/cli-darwin-x64": "0.5.74",
25
- "@team-agent/cli-linux-x64": "0.5.74"
23
+ "@team-agent/cli-darwin-arm64": "0.5.77",
24
+ "@team-agent/cli-darwin-x64": "0.5.77",
25
+ "@team-agent/cli-linux-x64": "0.5.77"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",