@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.
@@ -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 e46_inject_text_with_token_unconsumed_in_tail_reports_submit_consumption_unverified() {
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
- "E46 RED-1: when token remains in the pane input region after \
617
- Enter + resend cap, transport must report SubmitConsumptionUnverified \
618
- (not EnterSentWithoutPlaceholderCheck which delivery treats as \
619
- delivered). Got {:?}",
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
- fn e46_inject_text_enter_emits_escape_before_enter_on_bracketed_paste() {
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 escape_idx = calls.iter().position(|argv| {
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 escape_idx = escape_idx.expect("E46: bracketed text + Enter must send Escape pre-step");
787
- let enter_idx = enter_idx.expect("E46: must still send the final Enter");
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
- escape_idx < enter_idx,
790
- "E46: Escape must be sent BEFORE Enter (closes bracketed-paste \
791
- so Enter is interpreted as submit). escape_idx={escape_idx} \
792
- enter_idx={enter_idx} calls={calls:?}"
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
- // E50 PR-1 (0.3.24 P0): record appear-gate timing + per-attempt
1322
- // observations. Pure diagnostic — no behavioural change. The
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
- // Non-token payloads (rare: test harness / trust prompt Empty)
1348
- // still use the legacy loop for backward compatibility with
1349
- // provider_submit_verification_red.rs::PastePromptRunner tests.
1350
- let has_token = payload_token_marker(payload).is_some();
1351
- if saw_pasted_prompt && !has_token {
1352
- // Legacy weak loop preserved for non-token payloads (test harness).
1353
- let mut attempts = 0;
1354
- let mut cleared = false;
1355
- for _ in 0..PASTED_CONTENT_SUBMIT_ATTEMPTS {
1356
- attempts += 1;
1357
- self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
1358
- let captured = self.capture(target, CaptureRange::Tail(80))?;
1359
- if !capture_has_pasted_content_prompt(&captured.text) {
1360
- cleared = true;
1361
- break;
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
- InjectVerification::CaptureContainsNewPastedContentPrompt,
1368
- submit_verification: if cleared {
1369
- SubmitVerification::PastedContentPromptAbsentAfterSubmit
1370
- } else {
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: None,
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
- // U1 #7 (efd189b redo, canonical-native): around the final submit, read
1379
- // back the pane and confirm the just-pasted token is actually visible.
1380
- // The static `inject_verification_for_payload` returns CaptureContainsToken
1381
- // for any token payload WITHOUT checking the pane — a false positive when
1382
- // the paste silently dropped. A no-echo pane may only render after Enter,
1383
- // so a pre-submit miss gets one bounded post-submit readback before we
1384
- // downgrade to CaptureMissingToken.
1385
- token_visible_for_report =
1386
- pre_submit_token_visible(self, target, payload).unwrap_or(None);
1387
- // E46 (0.3.24 bug#5, demo-director卡 bracketed paste root cause):
1388
- // bracketed-paste-mode on a fresh provider TUI swallows the framework's
1389
- // Enter as paste content. Send `Escape` first to exit the paste bracket
1390
- // so the subsequent Enter is interpreted as submit. Safe across
1391
- // claude/codex/copilot: Escape on an empty composer is a no-op
1392
- // (cancel any pending mode), it only matters when bracketed-paste is
1393
- // actually open. Architect-approved + macmini real-machine verified.
1394
- // Only Enter benefits — explicit menu navigation (Key::Down etc.)
1395
- // should not pre-cancel mode.
1396
- if bracketed
1397
- && matches!(payload, InjectPayload::Text(_))
1398
- && matches!(submit, Key::Enter)
1399
- {
1400
- let escape_argv = tmux_send_keys_argv(&pane, &[Key::Escape]);
1401
- // We don't fail the whole inject on Escape error — Escape failure
1402
- // would surface downstream as the same SubmitConsumptionUnverified
1403
- // when post-Enter probe still sees the token in the input region.
1404
- let _ = self.run_inject_stage(&escape_argv, InjectStage::Submit);
1405
- }
1406
- self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
1407
- if matches!(token_visible_for_report, Some(false)) {
1408
- token_visible_for_report =
1409
- post_submit_token_visible(self, target, payload).unwrap_or(Some(false));
1410
- }
1411
- // E46 C2 + C3: post-Enter consumption gate with bounded resend cap.
1412
- // Only fires for:
1413
- // - submit key == Enter (other keys like Down/Up are explicit
1414
- // menu navigation; codex uses `Down Enter` to dismiss a
1415
- // prompt — those carry their own verification semantics
1416
- // via KeySentAfterVisibleToken),
1417
- // - token-bearing Text payloads (the structural input-empty
1418
- // signal needs a token marker to anchor on).
1419
- // Each iteration RE-CHECKS that the input still contains the
1420
- // token BEFORE resending Enter guards against the C3
1421
- // double-submit hazard when the first Enter was consumed but
1422
- // our readback was slow to catch the empty-input state.
1423
- let mut consumption_attempts: u32 = 1;
1424
- let mut consumed = if matches!(submit, Key::Enter) {
1425
- post_submit_input_consumed(self, target, payload).unwrap_or(None)
1426
- } else {
1427
- None
1428
- };
1429
- if matches!(consumed, Some(false)) {
1430
- for _ in 1..POST_SUBMIT_CONSUMPTION_ATTEMPTS {
1431
- std::thread::sleep(Duration::from_millis(
1432
- POST_SUBMIT_CONSUMPTION_POLL_MS,
1433
- ));
1434
- // C3 critical: re-check BEFORE resending. If the input
1435
- // is now empty (= first Enter actually consumed, just
1436
- // observable now), DO NOT resend — bumping a spurious
1437
- // empty Enter would open an empty turn.
1438
- consumed = post_submit_input_consumed(self, target, payload)
1439
- .unwrap_or(None);
1440
- if !matches!(consumed, Some(false)) {
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
- consumption_attempts += 1;
1444
- let _ = self.run_inject_stage(&submit_argv, InjectStage::Submit);
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
- // Consumption explicitly NOT observed (token still in tail
1453
- // after resend cap) E46's new variant. Delivery treats
1454
- // as not delivered.
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
- // E50 PR-1: surface appear-gate timing on the E46 token path too.
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: appear_gate_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.26",
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.26",
24
- "@team-agent/cli-darwin-x64": "0.3.26",
25
- "@team-agent/cli-linux-x64": "0.3.26"
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",