@team-agent/installer 0.3.26 → 0.3.28
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/crates/team-agent/src/cli/mod.rs +14 -0
- package/crates/team-agent/src/layout/manager.rs +241 -0
- package/crates/team-agent/src/layout/mod.rs +29 -0
- package/crates/team-agent/src/layout/overlay.rs +122 -0
- package/crates/team-agent/src/layout/placement.rs +47 -0
- package/crates/team-agent/src/layout/recovery.rs +101 -0
- package/crates/team-agent/src/layout/sessions.rs +277 -0
- package/crates/team-agent/src/layout/worker_env.rs +267 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +35 -0
- package/crates/team-agent/src/leader/start.rs +29 -13
- package/crates/team-agent/src/leader/tests/identity.rs +30 -15
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/launch.rs +55 -64
- package/crates/team-agent/src/lifecycle/restart/agent.rs +18 -10
- package/crates/team-agent/src/lifecycle/restart/common.rs +16 -9
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +26 -75
- package/crates/team-agent/src/messaging/delivery.rs +4 -2
- package/crates/team-agent/src/messaging/leader_receiver.rs +61 -24
- package/crates/team-agent/src/tmux_backend/tests.rs +64 -22
- package/crates/team-agent/src/tmux_backend.rs +185 -132
- package/package.json +4 -4
|
@@ -595,11 +595,15 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
|
|
|
595
595
|
/// report `SubmitConsumptionUnverified`, NOT
|
|
596
596
|
/// `EnterSentWithoutPlaceholderCheck`. Otherwise delivery emits delivered
|
|
597
597
|
/// + message.delivered (假阳) even though the worker never consumed it.
|
|
598
|
+
/// 0.3.28-final E55 truth source: grace fallback DELETED.
|
|
599
|
+
/// `consumed=Some(false)` (token still in bottom 5 after all retries)
|
|
600
|
+
/// MUST report SubmitConsumptionUnverified unconditionally. Pre-final
|
|
601
|
+
/// returned EnterSentWithoutPlaceholderCheck when Phase-1 saw the token
|
|
602
|
+
/// at all (token_visible_for_report=Some(true)) — but Phase-1 visibility
|
|
603
|
+
/// only proves the paste LANDED, not that the provider CONSUMED it.
|
|
604
|
+
/// That masked E55 busy-agent false positives as delivered=true.
|
|
598
605
|
#[test]
|
|
599
|
-
fn
|
|
600
|
-
// Mock always returns the token in the captured tail — simulates the
|
|
601
|
-
// macmini fresh claude TUI where the paste lands but the input never
|
|
602
|
-
// clears (Enter swallowed by bracketed-paste).
|
|
606
|
+
fn e46_inject_text_with_token_visible_but_unconsumed_reports_unverified() {
|
|
603
607
|
let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_red1]";
|
|
604
608
|
let (be, _rec) = backend_with(MockResp::Out(ok(token_text)), vec![]);
|
|
605
609
|
let report = be
|
|
@@ -610,19 +614,51 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
|
|
|
610
614
|
true,
|
|
611
615
|
)
|
|
612
616
|
.expect("inject runs");
|
|
617
|
+
// 0.3.28-final: no grace fallback. Token visible + not consumed →
|
|
618
|
+
// SubmitConsumptionUnverified (delivery treats as not delivered).
|
|
613
619
|
assert_eq!(
|
|
614
620
|
report.submit_verification,
|
|
615
621
|
SubmitVerification::SubmitConsumptionUnverified,
|
|
616
|
-
"
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
622
|
+
"0.3.28-final: when token still in bottom 5 after all retries, \
|
|
623
|
+
MUST be SubmitConsumptionUnverified — grace fallback masking \
|
|
624
|
+
this as EnterSentWithoutPlaceholderCheck caused E55 false \
|
|
625
|
+
positives (paste landed but busy agent never consumed). \
|
|
626
|
+
Got {:?}",
|
|
620
627
|
report.submit_verification
|
|
621
628
|
);
|
|
622
|
-
// turn_verification stays NotYetObserved (Gap42: metadata only).
|
|
623
629
|
assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
|
|
624
630
|
}
|
|
625
631
|
|
|
632
|
+
/// 0.3.27: empty pane captures → consumption poll sees "no token in
|
|
633
|
+
/// bottom 5" → consumed=true → EnterSentWithoutPlaceholderCheck. This is
|
|
634
|
+
/// the generous default for panes where the capture can't distinguish
|
|
635
|
+
/// "token consumed" from "token never landed" (empty mock, MCP sim).
|
|
636
|
+
/// SubmitConsumptionUnverified only fires when the grace fallback
|
|
637
|
+
/// explicitly rejects (token_visible_for_report=false AND consumed=false
|
|
638
|
+
/// at the same time — a state that requires the token to be in the pane
|
|
639
|
+
/// during consumption poll but absent during Phase 1, which is a
|
|
640
|
+
/// contradictory mock state that doesn't arise in production).
|
|
641
|
+
#[test]
|
|
642
|
+
fn e46_inject_text_with_empty_pane_defaults_to_consumed() {
|
|
643
|
+
let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_empty]";
|
|
644
|
+
let (be, _rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
645
|
+
let report = be
|
|
646
|
+
.inject(
|
|
647
|
+
&Target::Pane(PaneId::new("%7")),
|
|
648
|
+
&InjectPayload::Text(token_text.to_string()),
|
|
649
|
+
Key::Enter,
|
|
650
|
+
true,
|
|
651
|
+
)
|
|
652
|
+
.expect("inject runs");
|
|
653
|
+
assert_eq!(
|
|
654
|
+
report.submit_verification,
|
|
655
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
656
|
+
"0.3.27: empty pane → consumption poll sees no token in bottom → \
|
|
657
|
+
consumed=true → EnterSentWithoutPlaceholderCheck. Got {:?}",
|
|
658
|
+
report.submit_verification
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
626
662
|
/// **E46 RED-2 (正向消费确认 → delivered)**: token-bearing Text + Enter +
|
|
627
663
|
/// bracketed paste. Token VISIBLE on first capture (pre/post-submit token
|
|
628
664
|
/// readback) BUT then GONE from the post-submit input-consumption probe
|
|
@@ -763,8 +799,13 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
|
|
|
763
799
|
/// mode on stuck composer). Non-Enter submits (e.g. Key::Down for codex
|
|
764
800
|
/// menu) must NOT receive the Escape pre-step.
|
|
765
801
|
#[test]
|
|
766
|
-
|
|
802
|
+
/// 0.3.27 amendment: Escape is now RETRY-ONLY (attempt > 0). The first
|
|
803
|
+
/// attempt sends Enter directly (Python parity — Python never sends Escape).
|
|
804
|
+
/// Escape fires only if the first Enter failed to consume the token and a
|
|
805
|
+
/// retry is needed. This test now asserts the first attempt has NO Escape.
|
|
806
|
+
fn e46_inject_text_first_attempt_no_escape_retry_only() {
|
|
767
807
|
let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_esc]";
|
|
808
|
+
// Mock returns empty — token not in tail → consumed=true on first attempt.
|
|
768
809
|
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
769
810
|
let _report = be
|
|
770
811
|
.inject(
|
|
@@ -775,21 +816,22 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
|
|
|
775
816
|
)
|
|
776
817
|
.expect("inject runs");
|
|
777
818
|
let calls = rec.lock().unwrap().clone();
|
|
778
|
-
let
|
|
779
|
-
argv.get(1).map(String::as_str) == Some("send-keys")
|
|
780
|
-
&& argv.contains(&"Escape".to_string())
|
|
781
|
-
});
|
|
782
|
-
let enter_idx = calls.iter().position(|argv| {
|
|
819
|
+
let enter_count = calls.iter().filter(|argv| {
|
|
783
820
|
argv.get(1).map(String::as_str) == Some("send-keys")
|
|
784
821
|
&& argv.contains(&"Enter".to_string())
|
|
785
|
-
});
|
|
786
|
-
let
|
|
787
|
-
|
|
822
|
+
}).count();
|
|
823
|
+
let escape_count = calls.iter().filter(|argv| {
|
|
824
|
+
argv.get(1).map(String::as_str) == Some("send-keys")
|
|
825
|
+
&& argv.contains(&"Escape".to_string())
|
|
826
|
+
}).count();
|
|
788
827
|
assert!(
|
|
789
|
-
|
|
790
|
-
"
|
|
791
|
-
|
|
792
|
-
|
|
828
|
+
enter_count >= 1,
|
|
829
|
+
"0.3.27: first attempt must send Enter; calls={calls:?}"
|
|
830
|
+
);
|
|
831
|
+
assert_eq!(
|
|
832
|
+
escape_count, 0,
|
|
833
|
+
"0.3.27: first attempt (consumed=true) must NOT send Escape \
|
|
834
|
+
(Escape is retry-only); calls={calls:?}"
|
|
793
835
|
);
|
|
794
836
|
}
|
|
795
837
|
|
|
@@ -929,6 +929,31 @@ fn capture_has_pasted_content_prompt(text: &str) -> bool {
|
|
|
929
929
|
pasted_prompt_match(text).is_some()
|
|
930
930
|
}
|
|
931
931
|
|
|
932
|
+
/// 0.3.27: check if a token marker is present in the bottom N non-empty lines.
|
|
933
|
+
/// Used by the unified submit_and_verify to detect whether the provider consumed
|
|
934
|
+
/// the pasted message (token scrolls out of composer region on successful submit).
|
|
935
|
+
fn token_in_bottom_n(text: &str, marker: &str, n: usize) -> bool {
|
|
936
|
+
text.lines()
|
|
937
|
+
.rev()
|
|
938
|
+
.filter(|line| !line.trim().is_empty())
|
|
939
|
+
.take(n)
|
|
940
|
+
.any(|line| line.contains(marker))
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/// 0.3.27: check if a pasted-content prompt literal (`pasted content` / `pasted text`)
|
|
944
|
+
/// appears in the bottom N non-empty lines. Narrower than the full-Tail(80) check
|
|
945
|
+
/// that caused scrollback ghost matches (E50 defect B).
|
|
946
|
+
fn pasted_prompt_in_bottom(text: &str, n: usize) -> bool {
|
|
947
|
+
text.lines()
|
|
948
|
+
.rev()
|
|
949
|
+
.filter(|line| !line.trim().is_empty())
|
|
950
|
+
.take(n)
|
|
951
|
+
.any(|line| {
|
|
952
|
+
let lower = line.to_ascii_lowercase();
|
|
953
|
+
lower.contains("pasted content") || lower.contains("pasted text")
|
|
954
|
+
})
|
|
955
|
+
}
|
|
956
|
+
|
|
932
957
|
/// E50 PR-1 (0.3.24 P0, pasted-prompt 假阴诊断): factor `capture_has_pasted_content_prompt`
|
|
933
958
|
/// so the diagnostic layer can recover the MATCHED LITERAL and its position
|
|
934
959
|
/// in the tail. Returns `(literal, line_index_from_bottom)` on a match.
|
|
@@ -1318,152 +1343,180 @@ impl Transport for TmuxBackend {
|
|
|
1318
1343
|
self.run_inject_stage(&argv, stage)?;
|
|
1319
1344
|
}
|
|
1320
1345
|
}
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1323
|
-
// existing matcher / windows / actions are byte-identical.
|
|
1324
|
-
let appear_gate_start = std::time::Instant::now();
|
|
1325
|
-
let mut saw_pasted_prompt = false;
|
|
1326
|
-
for _ in 0..PASTED_CONTENT_APPEAR_POLLS {
|
|
1327
|
-
let captured = self.capture(target, CaptureRange::Tail(80))?;
|
|
1328
|
-
if capture_has_pasted_content_prompt(&captured.text) {
|
|
1329
|
-
saw_pasted_prompt = true;
|
|
1330
|
-
break;
|
|
1331
|
-
}
|
|
1332
|
-
std::thread::sleep(Duration::from_millis(25));
|
|
1333
|
-
}
|
|
1334
|
-
let appear_gate_elapsed_ms = appear_gate_start.elapsed().as_millis() as u64;
|
|
1335
|
-
let submit_argv = tmux_send_keys_argv(&pane, &[submit]);
|
|
1336
|
-
// E50 PR-2 Fix-B (0.3.26 P0): when the payload carries a token
|
|
1337
|
-
// marker (ALL delivery messages do via render_message), SKIP the
|
|
1338
|
-
// legacy pasted-prompt weak loop even if `saw_pasted_prompt` is
|
|
1339
|
-
// true. Fall through to the E46 token-based consumption gate
|
|
1340
|
-
// (Escape pre-Enter + post_submit_input_consumed + resend cap
|
|
1341
|
-
// with C3 re-check), which has NONE of the two defects:
|
|
1342
|
-
// A. E46 gate uses 60ms sleep + 3 retries (not zero-sleep).
|
|
1343
|
-
// B. E46 gate anchors on the token marker in the bottom 5
|
|
1344
|
-
// lines (composer region), not a substring match on
|
|
1345
|
-
// Tail(80) that catches scrollback residue.
|
|
1346
|
+
// ═══════════════════════════════════════════════════════════
|
|
1347
|
+
// 0.3.27 UNIFIED submit_and_verify
|
|
1346
1348
|
//
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
//
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1349
|
+
// Replaces the dual-branch split (saw_pasted_prompt weak loop
|
|
1350
|
+
// + E46 token consumption gate) with a single pipeline:
|
|
1351
|
+
//
|
|
1352
|
+
// Phase 1 — token visibility poll (dynamic timeout based on
|
|
1353
|
+
// payload size, 50ms interval, replaces the fixed 125ms
|
|
1354
|
+
// appear_gate)
|
|
1355
|
+
// Phase 2 — Escape (if bracketed+Text+Enter) + Enter + poll
|
|
1356
|
+
// token disappeared from bottom 3 lines. On failure:
|
|
1357
|
+
// re-check → Escape+Enter → poll. Up to 3 attempts.
|
|
1358
|
+
//
|
|
1359
|
+
// Design truth source: .team/artifacts/E55-delivery-architecture-design.html
|
|
1360
|
+
// Python parity: dynamic timeout max(2s, bytes/25000), poll 50ms.
|
|
1361
|
+
// ═══════════════════════════════════════════════════════════
|
|
1362
|
+
let inject_start = std::time::Instant::now();
|
|
1363
|
+
let submit_argv = tmux_send_keys_argv(&pane, &[submit]);
|
|
1364
|
+
|
|
1365
|
+
// Phase 1: token visibility poll — wait for the pasted text to
|
|
1366
|
+
// become visible in the pane before submitting. Dynamic timeout
|
|
1367
|
+
// based on payload size (large codex pastes can take seconds to
|
|
1368
|
+
// render the bracketed-paste block).
|
|
1369
|
+
let token_poll_timeout_ms = {
|
|
1370
|
+
let size_based = (text.len() as u64) / 25;
|
|
1371
|
+
size_based.max(2000)
|
|
1372
|
+
};
|
|
1373
|
+
let poll_start = std::time::Instant::now();
|
|
1374
|
+
token_visible_for_report =
|
|
1375
|
+
if payload_token_marker(payload).is_some() {
|
|
1376
|
+
let mut visible = false;
|
|
1377
|
+
while poll_start.elapsed().as_millis() < token_poll_timeout_ms as u128 {
|
|
1378
|
+
match token_visible_in_capture(self, target, payload) {
|
|
1379
|
+
Ok(Some(true)) => { visible = true; break; }
|
|
1380
|
+
Err(_) => break, // tmux unavailable, skip poll
|
|
1381
|
+
_ => {}
|
|
1382
|
+
}
|
|
1383
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
1362
1384
|
}
|
|
1363
|
-
|
|
1385
|
+
Some(visible)
|
|
1386
|
+
} else {
|
|
1387
|
+
None
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
// Phase 2: submit_and_verify — unified Escape+Enter+poll loop.
|
|
1391
|
+
let use_escape = bracketed
|
|
1392
|
+
&& matches!(payload, InjectPayload::Text(_))
|
|
1393
|
+
&& matches!(submit, Key::Enter);
|
|
1394
|
+
let escape_argv = if use_escape {
|
|
1395
|
+
Some(tmux_send_keys_argv(&pane, &[Key::Escape]))
|
|
1396
|
+
} else {
|
|
1397
|
+
None
|
|
1398
|
+
};
|
|
1399
|
+
// Non-Enter submit keys (Key::Down for codex menu, etc.) skip
|
|
1400
|
+
// the entire submit_and_verify loop — single send, no consumption
|
|
1401
|
+
// check, KeySentAfterVisibleToken verification.
|
|
1402
|
+
if !matches!(submit, Key::Enter) {
|
|
1403
|
+
self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
|
|
1404
|
+
let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
|
|
1364
1405
|
return Ok(InjectReport {
|
|
1365
1406
|
stage_reached: InjectStage::Submit,
|
|
1366
|
-
inject_verification:
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
SubmitVerification::PastedContentPromptStillPresentAfterSubmit
|
|
1372
|
-
},
|
|
1407
|
+
inject_verification: inject_verification_after_readback(
|
|
1408
|
+
payload,
|
|
1409
|
+
token_visible_for_report,
|
|
1410
|
+
),
|
|
1411
|
+
submit_verification: submit_verification_for_key(submit),
|
|
1373
1412
|
turn_verification: TurnVerification::NotYetObserved,
|
|
1374
|
-
attempts,
|
|
1375
|
-
submit_diagnostics:
|
|
1413
|
+
attempts: 1,
|
|
1414
|
+
submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
|
|
1415
|
+
appear_gate_elapsed_ms: 0,
|
|
1416
|
+
appear_gate_matched: false,
|
|
1417
|
+
total_elapsed_ms,
|
|
1418
|
+
attempts_detail: Vec::new(),
|
|
1419
|
+
}),
|
|
1376
1420
|
});
|
|
1377
1421
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
//
|
|
1402
|
-
//
|
|
1403
|
-
//
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1422
|
+
|
|
1423
|
+
let marker = payload_token_marker(payload);
|
|
1424
|
+
let max_submit_attempts: u32 = 3;
|
|
1425
|
+
let mut consumption_attempts: u32 = 0;
|
|
1426
|
+
let mut consumed: Option<bool> = None;
|
|
1427
|
+
|
|
1428
|
+
for attempt in 0..max_submit_attempts {
|
|
1429
|
+
// Before resending (attempt > 0), re-check if the token
|
|
1430
|
+
// already disappeared — guards against double-submit (C3).
|
|
1431
|
+
// Capture failures are non-fatal (tmux may not be running
|
|
1432
|
+
// in MCP sim / test env).
|
|
1433
|
+
if attempt > 0 {
|
|
1434
|
+
if let Some(m) = marker {
|
|
1435
|
+
if let Ok(cap) = self.capture(target, CaptureRange::Tail(30)) {
|
|
1436
|
+
if !token_in_bottom_n(&cap.text, m, 5) {
|
|
1437
|
+
consumed = Some(true);
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// 0.3.28-final (E55 false-positive truth source):
|
|
1445
|
+
// Escape retry is DELETED. The researcher established
|
|
1446
|
+
// Escape on Claude TUI with [Pasted content] visible
|
|
1447
|
+
// may CLEAR the composer content rather than exit paste
|
|
1448
|
+
// mode — sending Escape+Enter on retry would submit an
|
|
1449
|
+
// empty message and hide the genuine consumption failure
|
|
1450
|
+
// under a fake-success path. Python parity: ONLY ever
|
|
1451
|
+
// send Enter, never Escape.
|
|
1452
|
+
let _ = escape_argv;
|
|
1453
|
+
|
|
1454
|
+
// Enter — send-keys failure is degraded (tmux may not have
|
|
1455
|
+
// the pane in sim/test env). Break to consumed=None path
|
|
1456
|
+
// which returns EnterSentWithoutPlaceholderCheck.
|
|
1457
|
+
if self.run_inject_stage(&submit_argv, InjectStage::Submit).is_err() {
|
|
1458
|
+
consumed = None;
|
|
1459
|
+
break;
|
|
1460
|
+
}
|
|
1461
|
+
consumption_attempts = attempt + 1;
|
|
1462
|
+
|
|
1463
|
+
// Post-submit token readback (U1 #7 parity: check token
|
|
1464
|
+
// visible after Enter for no-echo panes).
|
|
1465
|
+
if attempt == 0 && matches!(token_visible_for_report, Some(false)) {
|
|
1466
|
+
token_visible_for_report =
|
|
1467
|
+
post_submit_token_visible(self, target, payload)
|
|
1468
|
+
.unwrap_or(Some(false));
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Poll: token disappeared from bottom 5 lines = consumed.
|
|
1472
|
+
// Capture failures → consumed=None (non-blocking).
|
|
1473
|
+
if let Some(m) = marker {
|
|
1474
|
+
let mut found_consumed = false;
|
|
1475
|
+
let mut capture_failed = false;
|
|
1476
|
+
for _ in 0..6 {
|
|
1477
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
1478
|
+
match self.capture(target, CaptureRange::Tail(30)) {
|
|
1479
|
+
Ok(cap) => {
|
|
1480
|
+
if !token_in_bottom_n(&cap.text, m, 5) {
|
|
1481
|
+
found_consumed = true;
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
Err(_) => { capture_failed = true; break; }
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if capture_failed {
|
|
1489
|
+
consumed = None;
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
consumed = Some(found_consumed);
|
|
1493
|
+
if found_consumed {
|
|
1441
1494
|
break;
|
|
1442
1495
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1496
|
+
} else {
|
|
1497
|
+
// Non-token payload: single Enter, no consumption check.
|
|
1498
|
+
consumed = None;
|
|
1499
|
+
break;
|
|
1445
1500
|
}
|
|
1446
1501
|
}
|
|
1502
|
+
|
|
1447
1503
|
let submit_verification = match consumed {
|
|
1448
|
-
// Consumption confirmed → MUST-10 path: keep the canonical
|
|
1449
|
-
// EnterSentWithoutPlaceholderCheck so the existing
|
|
1450
|
-
// provider_submit_verification_red contract holds.
|
|
1451
1504
|
Some(true) => SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1505
|
+
// 0.3.28-final (E55 truth source): consumed=Some(false)
|
|
1506
|
+
// means we ran ALL retries and the token never left
|
|
1507
|
+
// bottom 5 lines. That is a genuine consumption failure;
|
|
1508
|
+
// it MUST NOT be masked. Pre-final used a grace fallback
|
|
1509
|
+
// that returned EnterSentWithoutPlaceholderCheck whenever
|
|
1510
|
+
// token_visible_for_report=Some(true) — but Phase-1 token
|
|
1511
|
+
// visibility only proves the paste landed, NOT that the
|
|
1512
|
+
// provider consumed it. The fallback caused
|
|
1513
|
+
// delivered=true under E55 (busy-agent paste-landed-not-
|
|
1514
|
+
// consumed false positive). Always return Unverified now;
|
|
1515
|
+
// delivery treats it as not delivered.
|
|
1455
1516
|
Some(false) => SubmitVerification::SubmitConsumptionUnverified,
|
|
1456
|
-
// No structural anchor (non-token payload) → preserve the
|
|
1457
|
-
// historic non-checked variant; the pre-existing
|
|
1458
|
-
// pre/post-submit token readback + U1 #7 cover the other
|
|
1459
|
-
// false-positive (silent paste drop).
|
|
1460
1517
|
None => submit_verification_for_key(submit),
|
|
1461
1518
|
};
|
|
1462
|
-
|
|
1463
|
-
// saw_pasted_prompt=false here; the placeholder never appeared
|
|
1464
|
-
// within PASTED_CONTENT_APPEAR_POLLS, so the route is E46. The
|
|
1465
|
-
// gate elapsed_ms still helps operators see how long we waited
|
|
1466
|
-
// (e.g. codex slow-render).
|
|
1519
|
+
let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
|
|
1467
1520
|
return Ok(InjectReport {
|
|
1468
1521
|
stage_reached: InjectStage::Submit,
|
|
1469
1522
|
inject_verification: inject_verification_after_readback(
|
|
@@ -1477,9 +1530,9 @@ impl Transport for TmuxBackend {
|
|
|
1477
1530
|
},
|
|
1478
1531
|
attempts: consumption_attempts,
|
|
1479
1532
|
submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
|
|
1480
|
-
appear_gate_elapsed_ms,
|
|
1533
|
+
appear_gate_elapsed_ms: 0,
|
|
1481
1534
|
appear_gate_matched: false,
|
|
1482
|
-
total_elapsed_ms
|
|
1535
|
+
total_elapsed_ms,
|
|
1483
1536
|
attempts_detail: Vec::new(),
|
|
1484
1537
|
}),
|
|
1485
1538
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.28",
|
|
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.28",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.28",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.28"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|