@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +9 -0
- package/README.zh.md +8 -0
- package/crates/team-agent/src/cli/adapters.rs +23 -96
- package/crates/team-agent/src/cli/emit.rs +35 -7
- package/crates/team-agent/src/cli/mod.rs +63 -42
- package/crates/team-agent/src/cli/models.rs +138 -27
- package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
- package/crates/team-agent/src/cli/status_port.rs +8 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
- package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
- package/crates/team-agent/src/layout/worker_env.rs +366 -0
- package/crates/team-agent/src/leader/lease.rs +274 -40
- package/crates/team-agent/src/leader/registry.rs +177 -19
- package/crates/team-agent/src/leader/types.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +93 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1820 -60
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
- package/crates/team-agent/src/lifecycle/restart/common.rs +3 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +24 -28
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
- package/crates/team-agent/src/lifecycle/types.rs +4 -0
- package/crates/team-agent/src/messaging/delivery.rs +200 -29
- package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
- package/crates/team-agent/src/messaging/watchers.rs +48 -4
- package/crates/team-agent/src/state/paths.rs +17 -2
- package/crates/team-agent/src/state/persist.rs +166 -6
- package/crates/team-agent/src/state/projection.rs +18 -2
- package/crates/team-agent/src/state/repository.rs +36 -0
- package/crates/team-agent/src/state/selector.rs +23 -3
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +122 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +350 -7
- package/crates/team-agent/src/tmux_backend.rs +217 -26
- package/crates/team-agent/src/transport/test_support.rs +18 -3
- package/crates/team-agent/src/transport.rs +99 -0
- 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,
|
|
66
|
-
tmux_query_argv, tmux_send_keys_argv, tmux_send_submit_argv,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1656
|
-
|
|
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(_) =>
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
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(_) =>
|
|
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(
|
|
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|
|
|
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|
|
|
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.
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
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",
|