@team-agent/installer 0.3.23 → 0.3.24
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/leader/lease.rs +34 -13
- package/crates/team-agent/src/leader/tests/lease_api.rs +79 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +11 -3
- package/crates/team-agent/src/lifecycle/launch.rs +2 -16
- package/crates/team-agent/src/messaging/delivery.rs +74 -27
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/wave2.rs +689 -0
- package/crates/team-agent/src/state/projection.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +46 -0
- package/crates/team-agent/src/tmux_backend.rs +17 -5
- package/crates/team-agent/src/transport/test_support.rs +79 -6
- package/package.json +4 -4
|
@@ -298,6 +298,11 @@ pub fn project_top_level_view(state: &Value, team_key: &str) -> Value {
|
|
|
298
298
|
team_entry_from_state(state, team_key).and_then(Value::as_object).cloned().unwrap_or_default();
|
|
299
299
|
let mut p = entry_obj.clone(); // projection = deepcopy(entry)
|
|
300
300
|
// setdefault session_name/team_dir ← entry.get(...)(缺则插,值可为 null)。
|
|
301
|
+
// **0.3.24 excision** (U1-A real-machine RED v2): the ff38ab9 root-state
|
|
302
|
+
// `or_else` fallback was reverted along with the wave-2 U1-A drift fallback,
|
|
303
|
+
// since this projection change had no other consumer. v0.3.25 will re-introduce
|
|
304
|
+
// this together with the writer-shape + rediscover-writer fix — see
|
|
305
|
+
// `.team/artifacts/u1-a-realmachine-v2-fix-or-excise.md`.
|
|
301
306
|
if !p.contains_key("session_name") {
|
|
302
307
|
p.insert("session_name".to_string(), entry_obj.get("session_name").cloned().unwrap_or(Value::Null));
|
|
303
308
|
}
|
|
@@ -526,6 +526,52 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
|
|
|
526
526
|
);
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
#[test]
|
|
530
|
+
fn inject_waits_for_token_visibility_before_enter() {
|
|
531
|
+
let text = "hello [team-agent-token:e31]".to_string();
|
|
532
|
+
let marker_visible = format!("{text}\n");
|
|
533
|
+
let (be, rec) = backend_with(
|
|
534
|
+
MockResp::Out(ok("")),
|
|
535
|
+
vec![
|
|
536
|
+
MockResp::Out(ok("")), // set-buffer
|
|
537
|
+
MockResp::Out(ok("")), // paste-buffer
|
|
538
|
+
MockResp::Out(ok("")), // delete-buffer
|
|
539
|
+
MockResp::Out(ok("")), // pasted-content prompt poll 1
|
|
540
|
+
MockResp::Out(ok("")), // pasted-content prompt poll 2
|
|
541
|
+
MockResp::Out(ok("")), // pasted-content prompt poll 3
|
|
542
|
+
MockResp::Out(ok("")), // pasted-content prompt poll 4
|
|
543
|
+
MockResp::Out(ok("")), // pasted-content prompt poll 5
|
|
544
|
+
MockResp::Out(ok("")), // token gate: not visible yet
|
|
545
|
+
MockResp::Out(ok("")), // token gate: still not visible
|
|
546
|
+
MockResp::Out(ok(&marker_visible)), // token gate: visible, Enter may fire
|
|
547
|
+
MockResp::Out(ok("")), // send-keys Enter
|
|
548
|
+
],
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
let report = be
|
|
552
|
+
.inject(&Target::Pane(PaneId::new("%7")), &InjectPayload::Text(text), Key::Enter, true)
|
|
553
|
+
.expect("inject");
|
|
554
|
+
let calls = rec.lock().unwrap().clone();
|
|
555
|
+
let submit_index = calls
|
|
556
|
+
.iter()
|
|
557
|
+
.position(|argv| argv.get(1).map(String::as_str) == Some("send-keys"))
|
|
558
|
+
.expect("inject must eventually send Enter");
|
|
559
|
+
let captures_before_submit = calls[..submit_index]
|
|
560
|
+
.iter()
|
|
561
|
+
.filter(|argv| argv.get(1).map(String::as_str) == Some("capture-pane"))
|
|
562
|
+
.count();
|
|
563
|
+
|
|
564
|
+
assert!(
|
|
565
|
+
captures_before_submit >= super::PASTED_CONTENT_APPEAR_POLLS as usize + 3,
|
|
566
|
+
"Enter must wait until the pasted token is visible; calls={calls:?}"
|
|
567
|
+
);
|
|
568
|
+
assert_eq!(report.inject_verification, InjectVerification::CaptureContainsToken);
|
|
569
|
+
assert_eq!(
|
|
570
|
+
report.submit_verification,
|
|
571
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
529
575
|
#[test]
|
|
530
576
|
fn send_keys_cancel_mode_queries_mode_and_dispatches_cancel_argv() {
|
|
531
577
|
let (be, rec) = backend_with(
|
|
@@ -845,16 +845,28 @@ fn token_visible_in_capture(
|
|
|
845
845
|
}
|
|
846
846
|
}
|
|
847
847
|
|
|
848
|
-
/// U1 #7
|
|
849
|
-
///
|
|
850
|
-
///
|
|
851
|
-
/// the static `inject_verification_for_payload` would have reported as success.
|
|
848
|
+
/// U1 #7 / E31: wait briefly for the just-pasted token marker before submitting.
|
|
849
|
+
/// `Ok(None)` for non-token payloads (nothing to check). `Ok(Some(false))` means the
|
|
850
|
+
/// paste did not become visible before the Python-parity fallback delay.
|
|
852
851
|
fn pre_submit_token_visible(
|
|
853
852
|
backend: &TmuxBackend,
|
|
854
853
|
target: &Target,
|
|
855
854
|
payload: &InjectPayload,
|
|
856
855
|
) -> Result<Option<bool>, TransportError> {
|
|
857
|
-
|
|
856
|
+
if payload_token_marker(payload).is_none() {
|
|
857
|
+
return Ok(None);
|
|
858
|
+
}
|
|
859
|
+
for attempt in 0..PASTED_CONTENT_APPEAR_POLLS {
|
|
860
|
+
if let Some(true) = token_visible_in_capture(backend, target, payload)? {
|
|
861
|
+
return Ok(Some(true));
|
|
862
|
+
}
|
|
863
|
+
if attempt + 1 < PASTED_CONTENT_APPEAR_POLLS {
|
|
864
|
+
std::thread::sleep(Duration::from_millis(25));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// Python waits 250ms between paste-buffer and Enter to let bracketed paste settle.
|
|
868
|
+
std::thread::sleep(Duration::from_millis(250));
|
|
869
|
+
Ok(Some(false))
|
|
858
870
|
}
|
|
859
871
|
|
|
860
872
|
const TOKEN_POST_SUBMIT_READBACK_POLLS: u32 = 5;
|
|
@@ -37,6 +37,18 @@ struct OfflineState {
|
|
|
37
37
|
inject_targets: Vec<Target>,
|
|
38
38
|
inject_payloads: Vec<String>,
|
|
39
39
|
tmux_endpoint: Option<String>,
|
|
40
|
+
/// U1-B contract: when set, `list_targets()` returns `TransportError::MuxUnavailable`
|
|
41
|
+
/// — used to model tmux server jitter (subprocess-fork-failed level). The whole
|
|
42
|
+
/// resolve must DEFER, not silently coerce to an empty vec.
|
|
43
|
+
list_targets_error: Option<String>,
|
|
44
|
+
/// U1-C / general: pre-staged `capture()` payload, keyed by target stringification.
|
|
45
|
+
/// `Target::Pane(p)` keys as `p.as_str()`; `Target::SessionWindow{session,window}`
|
|
46
|
+
/// keys as `format!("{session}:{window}")`. A miss returns empty text (current
|
|
47
|
+
/// default behaviour).
|
|
48
|
+
capture_text: BTreeMap<String, String>,
|
|
49
|
+
/// U1-C Tail-peek contract: every `capture()` call records its `CaptureRange`,
|
|
50
|
+
/// in order, so a test can prove the delivery peek site narrowed Full → Tail(80).
|
|
51
|
+
capture_ranges: Vec<CaptureRange>,
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
impl Default for OfflineState {
|
|
@@ -57,6 +69,9 @@ impl Default for OfflineState {
|
|
|
57
69
|
inject_targets: Vec::new(),
|
|
58
70
|
inject_payloads: Vec::new(),
|
|
59
71
|
tmux_endpoint: None,
|
|
72
|
+
list_targets_error: None,
|
|
73
|
+
capture_text: BTreeMap::new(),
|
|
74
|
+
capture_ranges: Vec::new(),
|
|
60
75
|
}
|
|
61
76
|
}
|
|
62
77
|
}
|
|
@@ -127,6 +142,40 @@ impl OfflineTransport {
|
|
|
127
142
|
self
|
|
128
143
|
}
|
|
129
144
|
|
|
145
|
+
/// U1-B: stage a `list_targets()` error to model tmux server jitter. While set,
|
|
146
|
+
/// every call returns `Err(TransportError::MuxUnavailable{detail})`. Pass an
|
|
147
|
+
/// empty string to clear via re-builder if needed.
|
|
148
|
+
pub fn with_list_targets_error(self, detail: impl Into<String>) -> Self {
|
|
149
|
+
self.with_state(|state| state.list_targets_error = Some(detail.into()));
|
|
150
|
+
self
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Pre-stage `capture()` output for a `Target::Pane(pane_id)` key.
|
|
154
|
+
pub fn with_capture_for_pane(
|
|
155
|
+
self,
|
|
156
|
+
pane_id: impl Into<String>,
|
|
157
|
+
text: impl Into<String>,
|
|
158
|
+
) -> Self {
|
|
159
|
+
self.with_state(|state| {
|
|
160
|
+
state.capture_text.insert(pane_id.into(), text.into());
|
|
161
|
+
});
|
|
162
|
+
self
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Pre-stage `capture()` output for a `Target::SessionWindow{session,window}` key.
|
|
166
|
+
pub fn with_capture_for_session_window(
|
|
167
|
+
self,
|
|
168
|
+
session: impl Into<String>,
|
|
169
|
+
window: impl Into<String>,
|
|
170
|
+
text: impl Into<String>,
|
|
171
|
+
) -> Self {
|
|
172
|
+
let key = format!("{}:{}", session.into(), window.into());
|
|
173
|
+
self.with_state(|state| {
|
|
174
|
+
state.capture_text.insert(key, text.into());
|
|
175
|
+
});
|
|
176
|
+
self
|
|
177
|
+
}
|
|
178
|
+
|
|
130
179
|
pub fn calls(&self) -> Vec<&'static str> {
|
|
131
180
|
self.with_state(|state| state.calls.clone())
|
|
132
181
|
}
|
|
@@ -163,6 +212,12 @@ impl OfflineTransport {
|
|
|
163
212
|
self.with_state(|state| state.inject_payloads.clone())
|
|
164
213
|
}
|
|
165
214
|
|
|
215
|
+
/// All `capture()` ranges observed, in call order. Used by U1-C Tail-peek
|
|
216
|
+
/// contracts to prove the delivery peek site requested Tail rather than Full.
|
|
217
|
+
pub fn capture_ranges(&self) -> Vec<CaptureRange> {
|
|
218
|
+
self.with_state(|state| state.capture_ranges.clone())
|
|
219
|
+
}
|
|
220
|
+
|
|
166
221
|
fn record(&self, call: &'static str) {
|
|
167
222
|
self.with_state(|state| state.calls.push(call));
|
|
168
223
|
}
|
|
@@ -294,11 +349,21 @@ impl Transport for OfflineTransport {
|
|
|
294
349
|
|
|
295
350
|
fn capture(
|
|
296
351
|
&self,
|
|
297
|
-
|
|
352
|
+
target: &Target,
|
|
298
353
|
range: CaptureRange,
|
|
299
354
|
) -> Result<CapturedText, TransportError> {
|
|
300
|
-
|
|
301
|
-
|
|
355
|
+
let key = match target {
|
|
356
|
+
Target::Pane(p) => p.as_str().to_string(),
|
|
357
|
+
Target::SessionWindow { session, window } => {
|
|
358
|
+
format!("{}:{}", session.as_str(), window.as_str())
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
let text = self.with_state(|state| {
|
|
362
|
+
state.calls.push("capture");
|
|
363
|
+
state.capture_ranges.push(range);
|
|
364
|
+
state.capture_text.get(&key).cloned().unwrap_or_default()
|
|
365
|
+
});
|
|
366
|
+
Ok(CapturedText { text, range })
|
|
302
367
|
}
|
|
303
368
|
|
|
304
369
|
fn query(
|
|
@@ -329,10 +394,18 @@ impl Transport for OfflineTransport {
|
|
|
329
394
|
}
|
|
330
395
|
|
|
331
396
|
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
332
|
-
|
|
397
|
+
// U1-B: when `with_list_targets_error` is set, return a real Err so the
|
|
398
|
+
// caller sees server jitter rather than a coerced empty vec.
|
|
399
|
+
if let Some(detail) = self.with_state(|state| {
|
|
333
400
|
state.calls.push("list_targets");
|
|
334
|
-
state.
|
|
335
|
-
})
|
|
401
|
+
state.list_targets_error.clone()
|
|
402
|
+
}) {
|
|
403
|
+
return Err(TransportError::MuxUnavailable {
|
|
404
|
+
backend: BackendKind::Tmux,
|
|
405
|
+
detail,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
Ok(self.with_state(|state| state.targets.clone()))
|
|
336
409
|
}
|
|
337
410
|
|
|
338
411
|
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
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.24",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.24",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.24"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|