@team-agent/installer 0.3.26 → 0.3.27
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/lifecycle/launch.rs +9 -0
- 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 -23
- package/crates/team-agent/src/tmux_backend.rs +201 -133
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -3292,6 +3292,13 @@ fn dangerous_leader_flags() -> &'static [(&'static str, &'static str)] {
|
|
|
3292
3292
|
("claude", "--dangerously-skip-permissions"),
|
|
3293
3293
|
("claude", "--dangerously-skip-permission"),
|
|
3294
3294
|
("codex", "--dangerously-bypass-approvals-and-sandbox"),
|
|
3295
|
+
// 0.3.27 P1 (E54 symptom 2): copilot's full-permission flags. Without
|
|
3296
|
+
// these entries, a copilot leader running with --allow-all / --yolo would
|
|
3297
|
+
// NOT propagate bypass to claude/codex workers because
|
|
3298
|
+
// detect_dangerous_approval scans the coordinator's ancestor argv and
|
|
3299
|
+
// only matches entries in THIS table.
|
|
3300
|
+
("copilot", "--allow-all"),
|
|
3301
|
+
("copilot", "--yolo"),
|
|
3295
3302
|
]
|
|
3296
3303
|
}
|
|
3297
3304
|
|
|
@@ -3299,6 +3306,8 @@ fn binary_matches_provider(provider: &str, binary: Option<&str>) -> bool {
|
|
|
3299
3306
|
match (provider, binary) {
|
|
3300
3307
|
("codex", Some("codex")) => true,
|
|
3301
3308
|
("claude", Some("claude" | "claude-code" | "claude_code")) => true,
|
|
3309
|
+
// 0.3.27 P1 (E54): copilot binary detection for dangerous-approval scan.
|
|
3310
|
+
("copilot", Some("copilot" | "github-copilot" | "gh-copilot")) => true,
|
|
3302
3311
|
_ => false,
|
|
3303
3312
|
}
|
|
3304
3313
|
}
|
|
@@ -473,7 +473,8 @@ pub fn deliver_pending_message(
|
|
|
473
473
|
Ok(outcome)
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
-
|
|
476
|
+
/// 0.3.27: promoted to pub(crate) for leader_receiver.rs verification gate.
|
|
477
|
+
pub(crate) fn inject_submit_verified(report: &InjectReport) -> bool {
|
|
477
478
|
match report.submit_verification {
|
|
478
479
|
SubmitVerification::SendKeysFailed => false,
|
|
479
480
|
SubmitVerification::PastedContentPromptStillPresentAfterSubmit => false,
|
|
@@ -502,7 +503,8 @@ fn inject_submit_verified(report: &InjectReport) -> bool {
|
|
|
502
503
|
/// `CaptureMissingToken` → readback failed → not delivered (degraded/unverified). All
|
|
503
504
|
/// other inject_verification variants (incl. token-less payloads, empty sends, new
|
|
504
505
|
/// pasted-content prompt) are not readback-negative and pass this gate.
|
|
505
|
-
|
|
506
|
+
/// 0.3.27: promoted to pub(crate) for leader_receiver.rs verification gate.
|
|
507
|
+
pub(crate) fn pane_readback_verified(report: &InjectReport) -> bool {
|
|
506
508
|
!matches!(
|
|
507
509
|
report.inject_verification,
|
|
508
510
|
InjectVerification::CaptureMissingToken
|
|
@@ -11,7 +11,7 @@ use crate::model::ids::{OwnerEpoch, TaskId};
|
|
|
11
11
|
use crate::transport::{InjectPayload, Key, PaneId, Target, Transport};
|
|
12
12
|
|
|
13
13
|
use super::helpers::MessageStatusShadow;
|
|
14
|
-
use super::{DeliveryOutcome, DeliveryRefusal, DeliveryStatus, MessagingError};
|
|
14
|
+
use super::{DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, MessagingError};
|
|
15
15
|
|
|
16
16
|
/// `_send_to_leader_receiver` (`leader.py:69`) — **N31/N32 funnel primitive**:所有 leader-bound
|
|
17
17
|
/// caller(send_message(to=leader) / report_result / request_human / idle reminder /
|
|
@@ -393,29 +393,66 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
393
393
|
|
|
394
394
|
match inject_result {
|
|
395
395
|
Ok(report) => {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
396
|
+
// 0.3.27 P0: leader_receiver verification gate. The pre-fix path
|
|
397
|
+
// marked every inject Ok as delivered regardless of whether the
|
|
398
|
+
// submit was actually verified. Apply the same two-gate check that
|
|
399
|
+
// deliver_pending_message uses (delivery.rs:376-381):
|
|
400
|
+
// (a) inject_submit_verified — Enter was accepted by the TUI
|
|
401
|
+
// (b) pane_readback_verified — token was visible in the pane
|
|
402
|
+
// Both must pass to mark delivered; otherwise degrade to
|
|
403
|
+
// submitted_unverified (loud, not silent).
|
|
404
|
+
let submit_ok = super::delivery::inject_submit_verified(&report);
|
|
405
|
+
let readback_ok = super::delivery::pane_readback_verified(&report);
|
|
406
|
+
if submit_ok && readback_ok {
|
|
407
|
+
store.mark(message_id, "delivered", None)?;
|
|
408
|
+
event_log.write(
|
|
409
|
+
"leader_receiver.fallback_pane_submitted",
|
|
410
|
+
serde_json::json!({
|
|
411
|
+
"message_id": message_id,
|
|
412
|
+
"result_id": result_id,
|
|
413
|
+
"owner_team_id": owner_team_id,
|
|
414
|
+
"pane_id": pane_id,
|
|
415
|
+
"primary_error": primary_error,
|
|
416
|
+
"delivered_via": "fallback_pane",
|
|
417
|
+
"verification": format!("{:?}", report.submit_verification),
|
|
418
|
+
}),
|
|
419
|
+
)?;
|
|
420
|
+
Ok(DeliveryOutcome {
|
|
421
|
+
ok: true,
|
|
422
|
+
status: DeliveryStatus::Delivered,
|
|
423
|
+
message_status: MessageStatusShadow("delivered".to_string()),
|
|
424
|
+
message_id: Some(message_id.to_string()),
|
|
425
|
+
verification: Some("delivered_via=fallback_pane".to_string()),
|
|
426
|
+
stage: None,
|
|
427
|
+
reason: None,
|
|
428
|
+
channel: Some("fallback_pane".to_string()),
|
|
429
|
+
})
|
|
430
|
+
} else {
|
|
431
|
+
let reason = format!(
|
|
432
|
+
"fallback_pane_unverified:submit={submit_ok},readback={readback_ok},sv={:?}",
|
|
433
|
+
report.submit_verification
|
|
434
|
+
);
|
|
435
|
+
store.mark(message_id, "submitted_unverified", Some(&reason))?;
|
|
436
|
+
event_log.write(
|
|
437
|
+
"leader_receiver.fallback_pane_unverified",
|
|
438
|
+
serde_json::json!({
|
|
439
|
+
"message_id": message_id,
|
|
440
|
+
"pane_id": pane_id,
|
|
441
|
+
"reason": reason,
|
|
442
|
+
"primary_error": primary_error,
|
|
443
|
+
}),
|
|
444
|
+
)?;
|
|
445
|
+
Ok(DeliveryOutcome {
|
|
446
|
+
ok: false,
|
|
447
|
+
status: DeliveryStatus::Failed,
|
|
448
|
+
message_status: MessageStatusShadow("submitted_unverified".to_string()),
|
|
449
|
+
message_id: Some(message_id.to_string()),
|
|
450
|
+
verification: Some(reason),
|
|
451
|
+
stage: Some(DeliveryStage::Submit),
|
|
452
|
+
reason: None,
|
|
453
|
+
channel: Some("fallback_pane".to_string()),
|
|
454
|
+
})
|
|
455
|
+
}
|
|
419
456
|
}
|
|
420
457
|
Err(error) => {
|
|
421
458
|
event_log.write(
|
|
@@ -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.27 amendment: when token IS visible in the pane (Phase 1 confirmed)
|
|
599
|
+
/// but NOT consumed from bottom 5 after all retries, the grace fallback
|
|
600
|
+
/// treats "paste landed but composer didn't clear" as
|
|
601
|
+
/// EnterSentWithoutPlaceholderCheck. This handles bare-shell panes (MCP sim)
|
|
602
|
+
/// and busy-agent TUIs (E55) that keep the token visible after submit.
|
|
603
|
+
/// SubmitConsumptionUnverified is reserved for the case where the token was
|
|
604
|
+
/// NEVER visible (paste truly failed / dropped).
|
|
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_uses_grace_fallback() {
|
|
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,50 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
|
|
|
610
614
|
true,
|
|
611
615
|
)
|
|
612
616
|
.expect("inject runs");
|
|
617
|
+
// 0.3.27 grace fallback: token visible + not consumed from bottom →
|
|
618
|
+
// EnterSentWithoutPlaceholderCheck (paste landed, consumption unclear).
|
|
613
619
|
assert_eq!(
|
|
614
620
|
report.submit_verification,
|
|
615
|
-
SubmitVerification::
|
|
616
|
-
"
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
621
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
622
|
+
"0.3.27: when token is visible but not consumed from bottom 5 \
|
|
623
|
+
after all retries, grace fallback degrades to \
|
|
624
|
+
EnterSentWithoutPlaceholderCheck (paste landed, bare-shell/busy \
|
|
625
|
+
pane parity). Got {:?}",
|
|
620
626
|
report.submit_verification
|
|
621
627
|
);
|
|
622
|
-
// turn_verification stays NotYetObserved (Gap42: metadata only).
|
|
623
628
|
assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
|
|
624
629
|
}
|
|
625
630
|
|
|
631
|
+
/// 0.3.27: empty pane captures → consumption poll sees "no token in
|
|
632
|
+
/// bottom 5" → consumed=true → EnterSentWithoutPlaceholderCheck. This is
|
|
633
|
+
/// the generous default for panes where the capture can't distinguish
|
|
634
|
+
/// "token consumed" from "token never landed" (empty mock, MCP sim).
|
|
635
|
+
/// SubmitConsumptionUnverified only fires when the grace fallback
|
|
636
|
+
/// explicitly rejects (token_visible_for_report=false AND consumed=false
|
|
637
|
+
/// at the same time — a state that requires the token to be in the pane
|
|
638
|
+
/// during consumption poll but absent during Phase 1, which is a
|
|
639
|
+
/// contradictory mock state that doesn't arise in production).
|
|
640
|
+
#[test]
|
|
641
|
+
fn e46_inject_text_with_empty_pane_defaults_to_consumed() {
|
|
642
|
+
let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_empty]";
|
|
643
|
+
let (be, _rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
644
|
+
let report = be
|
|
645
|
+
.inject(
|
|
646
|
+
&Target::Pane(PaneId::new("%7")),
|
|
647
|
+
&InjectPayload::Text(token_text.to_string()),
|
|
648
|
+
Key::Enter,
|
|
649
|
+
true,
|
|
650
|
+
)
|
|
651
|
+
.expect("inject runs");
|
|
652
|
+
assert_eq!(
|
|
653
|
+
report.submit_verification,
|
|
654
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
655
|
+
"0.3.27: empty pane → consumption poll sees no token in bottom → \
|
|
656
|
+
consumed=true → EnterSentWithoutPlaceholderCheck. Got {:?}",
|
|
657
|
+
report.submit_verification
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
626
661
|
/// **E46 RED-2 (正向消费确认 → delivered)**: token-bearing Text + Enter +
|
|
627
662
|
/// bracketed paste. Token VISIBLE on first capture (pre/post-submit token
|
|
628
663
|
/// readback) BUT then GONE from the post-submit input-consumption probe
|
|
@@ -763,8 +798,13 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
|
|
|
763
798
|
/// mode on stuck composer). Non-Enter submits (e.g. Key::Down for codex
|
|
764
799
|
/// menu) must NOT receive the Escape pre-step.
|
|
765
800
|
#[test]
|
|
766
|
-
|
|
801
|
+
/// 0.3.27 amendment: Escape is now RETRY-ONLY (attempt > 0). The first
|
|
802
|
+
/// attempt sends Enter directly (Python parity — Python never sends Escape).
|
|
803
|
+
/// Escape fires only if the first Enter failed to consume the token and a
|
|
804
|
+
/// retry is needed. This test now asserts the first attempt has NO Escape.
|
|
805
|
+
fn e46_inject_text_first_attempt_no_escape_retry_only() {
|
|
767
806
|
let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_esc]";
|
|
807
|
+
// Mock returns empty — token not in tail → consumed=true on first attempt.
|
|
768
808
|
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
|
|
769
809
|
let _report = be
|
|
770
810
|
.inject(
|
|
@@ -775,21 +815,22 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
|
|
|
775
815
|
)
|
|
776
816
|
.expect("inject runs");
|
|
777
817
|
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| {
|
|
818
|
+
let enter_count = calls.iter().filter(|argv| {
|
|
783
819
|
argv.get(1).map(String::as_str) == Some("send-keys")
|
|
784
820
|
&& argv.contains(&"Enter".to_string())
|
|
785
|
-
});
|
|
786
|
-
let
|
|
787
|
-
|
|
821
|
+
}).count();
|
|
822
|
+
let escape_count = calls.iter().filter(|argv| {
|
|
823
|
+
argv.get(1).map(String::as_str) == Some("send-keys")
|
|
824
|
+
&& argv.contains(&"Escape".to_string())
|
|
825
|
+
}).count();
|
|
788
826
|
assert!(
|
|
789
|
-
|
|
790
|
-
"
|
|
791
|
-
|
|
792
|
-
|
|
827
|
+
enter_count >= 1,
|
|
828
|
+
"0.3.27: first attempt must send Enter; calls={calls:?}"
|
|
829
|
+
);
|
|
830
|
+
assert_eq!(
|
|
831
|
+
escape_count, 0,
|
|
832
|
+
"0.3.27: first attempt (consumed=true) must NOT send Escape \
|
|
833
|
+
(Escape is retry-only); calls={calls:?}"
|
|
793
834
|
);
|
|
794
835
|
}
|
|
795
836
|
|
|
@@ -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,195 @@ 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
|
+
// Escape is RETRY-ONLY (attempt > 0). Researcher discovery:
|
|
1445
|
+
// Escape on Claude TUI with [Pasted content] visible may
|
|
1446
|
+
// CLEAR the composer content instead of exiting paste mode.
|
|
1447
|
+
// Python never sends Escape and never has this issue. So:
|
|
1448
|
+
// attempt 0 → direct Enter (Python parity); attempt 1+ →
|
|
1449
|
+
// Escape+Enter as remediation for stuck bracketed-paste.
|
|
1450
|
+
if attempt > 0 {
|
|
1451
|
+
if let Some(ref esc) = escape_argv {
|
|
1452
|
+
let _ = self.run_inject_stage(esc, InjectStage::Submit);
|
|
1453
|
+
for _ in 0..10 {
|
|
1454
|
+
match self.capture(target, CaptureRange::Tail(30)) {
|
|
1455
|
+
Ok(cap) if !pasted_prompt_in_bottom(&cap.text, 3) => break,
|
|
1456
|
+
Err(_) => break,
|
|
1457
|
+
_ => {}
|
|
1458
|
+
}
|
|
1459
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Enter — send-keys failure is degraded (tmux may not have
|
|
1465
|
+
// the pane in sim/test env). Break to consumed=None path
|
|
1466
|
+
// which returns EnterSentWithoutPlaceholderCheck.
|
|
1467
|
+
if self.run_inject_stage(&submit_argv, InjectStage::Submit).is_err() {
|
|
1468
|
+
consumed = None;
|
|
1469
|
+
break;
|
|
1470
|
+
}
|
|
1471
|
+
consumption_attempts = attempt + 1;
|
|
1472
|
+
|
|
1473
|
+
// Post-submit token readback (U1 #7 parity: check token
|
|
1474
|
+
// visible after Enter for no-echo panes).
|
|
1475
|
+
if attempt == 0 && matches!(token_visible_for_report, Some(false)) {
|
|
1476
|
+
token_visible_for_report =
|
|
1477
|
+
post_submit_token_visible(self, target, payload)
|
|
1478
|
+
.unwrap_or(Some(false));
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Poll: token disappeared from bottom 5 lines = consumed.
|
|
1482
|
+
// Capture failures → consumed=None (non-blocking).
|
|
1483
|
+
if let Some(m) = marker {
|
|
1484
|
+
let mut found_consumed = false;
|
|
1485
|
+
let mut capture_failed = false;
|
|
1486
|
+
for _ in 0..6 {
|
|
1487
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
1488
|
+
match self.capture(target, CaptureRange::Tail(30)) {
|
|
1489
|
+
Ok(cap) => {
|
|
1490
|
+
if !token_in_bottom_n(&cap.text, m, 5) {
|
|
1491
|
+
found_consumed = true;
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
Err(_) => { capture_failed = true; break; }
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if capture_failed {
|
|
1499
|
+
consumed = None;
|
|
1500
|
+
break;
|
|
1501
|
+
}
|
|
1502
|
+
consumed = Some(found_consumed);
|
|
1503
|
+
if found_consumed {
|
|
1441
1504
|
break;
|
|
1442
1505
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1506
|
+
} else {
|
|
1507
|
+
// Non-token payload: single Enter, no consumption check.
|
|
1508
|
+
consumed = None;
|
|
1509
|
+
break;
|
|
1445
1510
|
}
|
|
1446
1511
|
}
|
|
1512
|
+
|
|
1447
1513
|
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
1514
|
Some(true) => SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1515
|
+
Some(false) => {
|
|
1516
|
+
// Consumption not observed (token still in bottom 5 after
|
|
1517
|
+
// all attempts). As a grace fallback, check if the token
|
|
1518
|
+
// is visible ANYWHERE in the pane capture. If yes, the
|
|
1519
|
+
// paste DID land — the pane just doesn't clear the token
|
|
1520
|
+
// from the bottom region (bare shell panes, MCP sim env,
|
|
1521
|
+
// busy agent TUI). Treat as "submitted, verification
|
|
1522
|
+
// degraded" → EnterSentWithoutPlaceholderCheck (generous;
|
|
1523
|
+
// matches pre-0.3.27 behaviour for these edge cases).
|
|
1524
|
+
// If the token is NOT visible at all, the paste truly
|
|
1525
|
+
// failed → SubmitConsumptionUnverified.
|
|
1526
|
+
if matches!(token_visible_for_report, Some(true)) {
|
|
1527
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
1528
|
+
} else {
|
|
1529
|
+
SubmitVerification::SubmitConsumptionUnverified
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1460
1532
|
None => submit_verification_for_key(submit),
|
|
1461
1533
|
};
|
|
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).
|
|
1534
|
+
let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
|
|
1467
1535
|
return Ok(InjectReport {
|
|
1468
1536
|
stage_reached: InjectStage::Submit,
|
|
1469
1537
|
inject_verification: inject_verification_after_readback(
|
|
@@ -1477,9 +1545,9 @@ impl Transport for TmuxBackend {
|
|
|
1477
1545
|
},
|
|
1478
1546
|
attempts: consumption_attempts,
|
|
1479
1547
|
submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
|
|
1480
|
-
appear_gate_elapsed_ms,
|
|
1548
|
+
appear_gate_elapsed_ms: 0,
|
|
1481
1549
|
appear_gate_matched: false,
|
|
1482
|
-
total_elapsed_ms
|
|
1550
|
+
total_elapsed_ms,
|
|
1483
1551
|
attempts_detail: Vec::new(),
|
|
1484
1552
|
}),
|
|
1485
1553
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
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.27",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.27",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.27"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|