@team-agent/installer 0.3.27 → 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 +46 -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/tmux_backend/tests.rs +16 -15
- package/crates/team-agent/src/tmux_backend.rs +21 -36
- package/package.json +4 -4
|
@@ -66,20 +66,23 @@ use super::*;
|
|
|
66
66
|
let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
|
|
67
67
|
assert_eq!(plan.provider, Provider::Fake);
|
|
68
68
|
assert_eq!(plan.mode, LeaderStartMode::ManagedTmuxClient);
|
|
69
|
+
// 0.3.28 Step 2: managed mode now uses the dedicated leader session
|
|
70
|
+
// (`team-agent-leader-<provider>-<folder>-<sha1[:8]>`), not the worker
|
|
71
|
+
// session `team-<team_id>`. Python parity.
|
|
72
|
+
let session_name = plan.session_name.as_ref().map(SessionName::as_str).unwrap_or("");
|
|
73
|
+
assert!(
|
|
74
|
+
session_name.starts_with(crate::layout::sessions::LEADER_SESSION_PREFIX),
|
|
75
|
+
"managed mode must use dedicated leader session prefix; got `{session_name}`"
|
|
76
|
+
);
|
|
77
|
+
// 0.3.28 Step 2: window name = provider wire (`fake`), not `leader`.
|
|
69
78
|
assert_eq!(
|
|
70
|
-
plan.
|
|
71
|
-
Some("
|
|
79
|
+
plan.leader_window.as_ref().map(WindowName::as_str),
|
|
80
|
+
Some("fake")
|
|
72
81
|
);
|
|
73
|
-
assert_eq!(plan.leader_window.as_ref().map(WindowName::as_str), Some("leader"));
|
|
74
82
|
assert!(!plan.is_external_leader);
|
|
75
83
|
assert!(
|
|
76
84
|
plan.argv.iter().any(|arg| arg == "attach-session"),
|
|
77
|
-
"no-tmux managed launch attaches the user client to the
|
|
78
|
-
plan.argv
|
|
79
|
-
);
|
|
80
|
-
assert!(
|
|
81
|
-
plan.argv.iter().any(|arg| arg == "team-current:leader"),
|
|
82
|
-
"managed client target must be the team leader window: {:?}",
|
|
85
|
+
"no-tmux managed launch attaches the user client to the leader window: {:?}",
|
|
83
86
|
plan.argv
|
|
84
87
|
);
|
|
85
88
|
// plan 边界 detached 恒 false(`-d` 插入在 start_leader 层,非此处)。
|
|
@@ -126,7 +129,12 @@ use super::*;
|
|
|
126
129
|
|
|
127
130
|
#[test]
|
|
128
131
|
#[serial_test::serial(env)]
|
|
129
|
-
fn
|
|
132
|
+
fn managed_leader_uses_dedicated_leader_session_independent_of_state_session_name() {
|
|
133
|
+
// 0.3.28 Step 2 amendment: the persisted `session_name` is the WORKER
|
|
134
|
+
// session, not the leader session. Managed leader now ignores it and
|
|
135
|
+
// always uses the dedicated `leader_session_name(provider, workspace)`.
|
|
136
|
+
// This test pins the new behaviour and the regression guard that the
|
|
137
|
+
// managed argv never gets a `team-team-*` double prefix.
|
|
130
138
|
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
131
139
|
let _e = EnvGuard::apply(&[("TMUX", None), ("TMUX_PANE", None)]);
|
|
132
140
|
let ws = std::env::temp_dir().join(format!("ta_rs_lsp_existing_{}", std::process::id()));
|
|
@@ -140,9 +148,11 @@ use super::*;
|
|
|
140
148
|
let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
|
|
141
149
|
|
|
142
150
|
assert_eq!(plan.mode, LeaderStartMode::ManagedTmuxClient);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
let session_name = plan.session_name.as_ref().map(SessionName::as_str).unwrap_or("");
|
|
152
|
+
assert!(
|
|
153
|
+
session_name.starts_with(crate::layout::sessions::LEADER_SESSION_PREFIX),
|
|
154
|
+
"managed mode uses dedicated leader session prefix regardless of \
|
|
155
|
+
persisted worker session_name; got `{session_name}`"
|
|
146
156
|
);
|
|
147
157
|
assert!(
|
|
148
158
|
!plan.argv.iter().any(|arg| arg.contains("team-team-alpha")),
|
|
@@ -169,9 +179,14 @@ use super::*;
|
|
|
169
179
|
"same-server managed launch must switch the existing tmux client: {:?}",
|
|
170
180
|
plan.argv
|
|
171
181
|
);
|
|
182
|
+
// 0.3.28 Step 2: target is `<dedicated_leader_session>:<provider_wire>`,
|
|
183
|
+
// not `team-current:leader`.
|
|
184
|
+
let session_name = plan.session_name.as_ref().map(SessionName::as_str).unwrap_or("");
|
|
185
|
+
let expected_target = format!("{session_name}:fake");
|
|
172
186
|
assert!(
|
|
173
|
-
plan.argv.iter().any(|arg| arg ==
|
|
174
|
-
"managed switch target must be the
|
|
187
|
+
plan.argv.iter().any(|arg| arg == &expected_target),
|
|
188
|
+
"managed switch target must be the dedicated leader-session leader \
|
|
189
|
+
window `{expected_target}`; got argv {:?}",
|
|
175
190
|
plan.argv
|
|
176
191
|
);
|
|
177
192
|
}
|
|
@@ -74,6 +74,9 @@ pub mod diagnose;
|
|
|
74
74
|
// 富返回类型,fn body unimplemented!(),P2 porter 落实现)。lifecycle=quick-start/restart/display;
|
|
75
75
|
// mcp_server=stdio MCP tool handlers;cli=clap 子命令;packaging=install/migrate/repair。
|
|
76
76
|
pub mod lifecycle;
|
|
77
|
+
// 0.3.28 — unified adaptive layout manager (single source of truth for tmux
|
|
78
|
+
// topology decisions). See `.team/artifacts/adaptive-layout-full-architecture-locate.md`.
|
|
79
|
+
pub mod layout;
|
|
77
80
|
pub mod mcp_server;
|
|
78
81
|
pub mod cli;
|
|
79
82
|
pub mod packaging;
|
|
@@ -119,6 +119,15 @@ pub fn launch_with_transport_in_workspace(
|
|
|
119
119
|
)?;
|
|
120
120
|
started
|
|
121
121
|
};
|
|
122
|
+
// 0.3.28 Step 1: topology invariant guard (warn-only during migration).
|
|
123
|
+
// Logs each violation to stderr; never panics. Promoted to hard error at
|
|
124
|
+
// Step 10 once Steps 2–9 have eliminated structural co-location.
|
|
125
|
+
if !dry_run {
|
|
126
|
+
if let Ok(state_for_check) = crate::state::persist::load_runtime_state(workspace) {
|
|
127
|
+
let violations = crate::layout::sessions::assert_topology_invariants(&state_for_check, &spec);
|
|
128
|
+
crate::layout::sessions::log_topology_violations(&violations);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
122
131
|
Ok(LaunchReport {
|
|
123
132
|
session_name,
|
|
124
133
|
started,
|
|
@@ -364,46 +373,17 @@ fn spawn_agents(
|
|
|
364
373
|
}),
|
|
365
374
|
);
|
|
366
375
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
let
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
transport.spawn_first_with_env_unset(
|
|
379
|
-
session_name,
|
|
380
|
-
&window,
|
|
381
|
-
&plan.argv,
|
|
382
|
-
workspace,
|
|
383
|
-
&env,
|
|
384
|
-
&env_unset,
|
|
385
|
-
)
|
|
386
|
-
} else {
|
|
387
|
-
transport.spawn_into_with_env_unset(
|
|
388
|
-
session_name,
|
|
389
|
-
&window,
|
|
390
|
-
&plan.argv,
|
|
391
|
-
workspace,
|
|
392
|
-
&env,
|
|
393
|
-
&env_unset,
|
|
394
|
-
)
|
|
395
|
-
}
|
|
396
|
-
} else {
|
|
397
|
-
transport.spawn_split_with_env_unset(
|
|
398
|
-
session_name,
|
|
399
|
-
&window,
|
|
400
|
-
&plan.argv,
|
|
401
|
-
workspace,
|
|
402
|
-
&env,
|
|
403
|
-
&env_unset,
|
|
404
|
-
)
|
|
405
|
-
}
|
|
406
|
-
} else if started.is_empty() {
|
|
376
|
+
// 0.3.28 Step 4b: replaced the `adaptive_layout_plan` 3-pane tiling
|
|
377
|
+
// with Python-parity 1-window-per-agent placement. Window name =
|
|
378
|
+
// `agent_id`; first worker creates the session via spawn_first,
|
|
379
|
+
// subsequent workers each get a new window via spawn_into. No splits
|
|
380
|
+
// in the worker session — Step 8's `assert_overlay_call_site` would
|
|
381
|
+
// catch any drift if a split call snuck back in. The `placement`
|
|
382
|
+
// variable is set to None to signal "no adaptive layout" to all
|
|
383
|
+
// downstream consumers (display dict, layout_window persistence).
|
|
384
|
+
let placement: Option<LayoutPlacement> = None;
|
|
385
|
+
let window = WindowName::new(agent_id_raw);
|
|
386
|
+
let spawn = if started.is_empty() {
|
|
407
387
|
transport.spawn_first_with_env_unset(
|
|
408
388
|
session_name,
|
|
409
389
|
&window,
|
|
@@ -2041,30 +2021,25 @@ pub(crate) fn inherited_env_with_team_overrides(
|
|
|
2041
2021
|
agent_id: &str,
|
|
2042
2022
|
team_id: Option<&str>,
|
|
2043
2023
|
) -> BTreeMap<String, String> {
|
|
2044
|
-
//
|
|
2045
|
-
//
|
|
2046
|
-
//
|
|
2047
|
-
//
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
env.insert("TEAM_AGENT_AGENT_ID".to_string(), agent_id.to_string());
|
|
2064
|
-
if let Some(tid) = team_id.filter(|s| !s.is_empty()) {
|
|
2065
|
-
env.insert("TEAM_AGENT_OWNER_TEAM_ID".to_string(), tid.to_string());
|
|
2066
|
-
}
|
|
2067
|
-
env
|
|
2024
|
+
// 0.3.28 Step 3: delegate to `layout::worker_env::worker_spawn_env` which
|
|
2025
|
+
// implements Python's whitelist semantics (`providers.py:130-145`). The
|
|
2026
|
+
// whitelist:
|
|
2027
|
+
// * Keeps PATH-like vars, locale, provider creds (CLAUDE_*/OPENAI_*/
|
|
2028
|
+
// COPILOT_*/GH_*/GEMINI_*/GOOGLE_*) + posix identifiers only.
|
|
2029
|
+
// * Strips ALL `TEAM_AGENT_LEADER_*` and
|
|
2030
|
+
// `TEAM_AGENT_MACHINE_FINGERPRINT` (leader identity contamination,
|
|
2031
|
+
// E60 root).
|
|
2032
|
+
// * Strips `TEAM_AGENT_TEAM_ID` (the leader's team_id — re-injected
|
|
2033
|
+
// as `TEAM_AGENT_OWNER_TEAM_ID` for the worker).
|
|
2034
|
+
// * Strips `COPILOT_DISABLE_TERMINAL_TITLE` (re-injected per-agent by
|
|
2035
|
+
// `apply_copilot_instructions_overlay` based on the WORKER's provider).
|
|
2036
|
+
// * Strips `TMUX` / `TMUX_PANE` (would attach worker to leader's pane).
|
|
2037
|
+
crate::layout::worker_env::worker_spawn_env(
|
|
2038
|
+
std::env::vars(),
|
|
2039
|
+
workspace,
|
|
2040
|
+
agent_id,
|
|
2041
|
+
team_id,
|
|
2042
|
+
)
|
|
2068
2043
|
}
|
|
2069
2044
|
|
|
2070
2045
|
pub(crate) fn apply_mcp_auto_approval_env(
|
|
@@ -4787,6 +4762,13 @@ fn spec_session_name(spec: &Value) -> SessionName {
|
|
|
4787
4762
|
SessionName::new(format!("team-{team_name}"))
|
|
4788
4763
|
}
|
|
4789
4764
|
|
|
4765
|
+
/// 0.3.28 layout step 1: pub re-export of `spec_session_name` for the new
|
|
4766
|
+
/// `layout::sessions::worker_session_name` to delegate to. Single underlying
|
|
4767
|
+
/// impl; this just widens visibility without duplicating logic.
|
|
4768
|
+
pub fn worker_session_name_pub(spec: &Value) -> SessionName {
|
|
4769
|
+
spec_session_name(spec)
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4790
4772
|
fn spec_agents(spec: &Value) -> Vec<AgentId> {
|
|
4791
4773
|
spec_agent_values(spec)
|
|
4792
4774
|
.into_iter()
|
|
@@ -95,16 +95,24 @@ pub(crate) fn start_agent_at_paths(
|
|
|
95
95
|
} else {
|
|
96
96
|
window_exists(transport, &session_name, &window)
|
|
97
97
|
};
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
// 0.3.28 Step 9: E51 self-heal converted to topology assertion. After
|
|
99
|
+
// Step 2 the leader lives in its own session (`team-agent-leader-*`),
|
|
100
|
+
// so `agent.pane_id == leader_receiver.pane_id` is STRUCTURALLY
|
|
101
|
+
// impossible. We keep the check as a runtime guard: if it ever fires,
|
|
102
|
+
// emit a topology_invariant_violation event AND still force a fresh
|
|
103
|
+
// spawn (defensive). The check itself remains a no-op on healthy
|
|
104
|
+
// state — assert_topology_invariants from Step 1 catches the
|
|
105
|
+
// upstream corruption.
|
|
106
|
+
let has_collision = pane_conflicts_with_leader_or_other(&state, agent_id, &agent);
|
|
107
|
+
if has_collision {
|
|
108
|
+
eprintln!(
|
|
109
|
+
"team_agent::layout e51_collision_post_step2 agent_id=`{agent_id}` \
|
|
110
|
+
action=forcing_fresh_spawn \
|
|
111
|
+
(should be impossible after Step 2 leader/worker session separation; \
|
|
112
|
+
investigate upstream state corruption)"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
let agent_live = agent_live && !has_collision;
|
|
108
116
|
if !force && agent_live {
|
|
109
117
|
mark_agent_running_noop(&mut state, agent_id, &session_name, &window)?;
|
|
110
118
|
let team_key = restart_projection_team_key(&state, team);
|
|
@@ -126,15 +126,19 @@ pub(super) fn spawn_agent_window(
|
|
|
126
126
|
);
|
|
127
127
|
crate::lifecycle::launch::apply_profile_launch_env(&mut env, &profile_launch);
|
|
128
128
|
crate::lifecycle::launch::apply_mcp_auto_approval_env(&mut env, safety);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
// 0.3.28 Step 3: per Python parity, worker spawn cwd is ALWAYS `workspace`.
|
|
130
|
+
// The persisted-state `agent.spawn_cwd` override is ignored (it was a
|
|
131
|
+
// Rust-only extension that drifted to `.team/runtime/<team_key>/` after
|
|
132
|
+
// rebuild.rs:138 — root cause of E56). The `spawn_cwd_override` parameter
|
|
133
|
+
// is still honoured for callers that need an explicit cwd (e.g. spec
|
|
134
|
+
// YAML-resolved cwd at first launch in `lifecycle/launch.rs`), but
|
|
135
|
+
// restart never passes it (see commit 71864c0 which fixed rebuild.rs:297
|
|
136
|
+
// to stop pinning `.team/runtime/<team_key>/`).
|
|
137
|
+
//
|
|
138
|
+
// NOTE: Step 4 will thread the YAML spec down to here so we can honour
|
|
139
|
+
// a per-agent YAML `spawn_cwd` field if one is set. Until then, override
|
|
140
|
+
// > workspace; state-based override is silently dropped.
|
|
141
|
+
let spawn_cwd = spawn_cwd_override.unwrap_or(workspace);
|
|
138
142
|
let env_unset: Vec<String> = profile_launch.env_unset.iter().cloned().collect();
|
|
139
143
|
let result = if let Some(placement) = layout_placement {
|
|
140
144
|
if placement.starts_window {
|
|
@@ -181,6 +185,9 @@ pub(super) fn spawn_agent_window(
|
|
|
181
185
|
&env_unset,
|
|
182
186
|
)
|
|
183
187
|
} else {
|
|
188
|
+
// 0.3.28 Step 8: spawn_split must only fire from the display
|
|
189
|
+
// overlay path. Warn-only here; Step 9 promotes to hard fail.
|
|
190
|
+
crate::layout::overlay::assert_overlay_call_site(session_name, &window);
|
|
184
191
|
transport.spawn_split_with_env_unset(
|
|
185
192
|
session_name,
|
|
186
193
|
&window,
|
|
@@ -213,6 +213,27 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
213
213
|
}
|
|
214
214
|
let session_name = state_session_name(&state);
|
|
215
215
|
if session_live_or_default(transport, &session_name, false) {
|
|
216
|
+
// 0.3.28 Step 5 (warn-only): per architecture, restart should REFUSE
|
|
217
|
+
// when the worker session is live (Python `restart/orchestration.py:79-85`
|
|
218
|
+
// raises `_tmux_session_conflict_error`). Pre-0.3.28 Rust kills the
|
|
219
|
+
// worker session here — which under the old co-located topology also
|
|
220
|
+
// killed the leader pane (the E57-1 cascade contribution from the
|
|
221
|
+
// layout layer).
|
|
222
|
+
//
|
|
223
|
+
// After Step 2 the leader lives in a DIFFERENT session
|
|
224
|
+
// (`team-agent-leader-...`), so killing the worker session no longer
|
|
225
|
+
// tears down the leader. That makes this kill structurally safe, but
|
|
226
|
+
// it still loses provider session state in the worker panes.
|
|
227
|
+
//
|
|
228
|
+
// Full "refuse" semantics will land once Steps 6+7 expose the
|
|
229
|
+
// recovery path so users have a clean alternative. For now we emit
|
|
230
|
+
// the warn-only event so operators see the drift in event logs.
|
|
231
|
+
eprintln!(
|
|
232
|
+
"team_agent::layout restart_precondition_warning worker_session=`{}` action=killing \
|
|
233
|
+
(post-Step-7 will refuse and direct user to recover; safe today because Step 2 \
|
|
234
|
+
moved leader to dedicated session)",
|
|
235
|
+
session_name.as_str()
|
|
236
|
+
);
|
|
216
237
|
transport
|
|
217
238
|
.kill_session(&session_name)
|
|
218
239
|
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
@@ -426,6 +447,11 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
426
447
|
&failed_agents,
|
|
427
448
|
"ok",
|
|
428
449
|
)?;
|
|
450
|
+
// 0.3.28 Step 1: topology invariant guard (warn-only). Same pattern as
|
|
451
|
+
// `lifecycle::launch::launch_with_transport_in_workspace` — logs to stderr,
|
|
452
|
+
// never panics. Hard error path is deferred to Step 10.
|
|
453
|
+
let violations = crate::layout::sessions::assert_topology_invariants(&state, &spec);
|
|
454
|
+
crate::layout::sessions::log_topology_violations(&violations);
|
|
429
455
|
Ok(RestartReport::Restarted {
|
|
430
456
|
session_name,
|
|
431
457
|
agents: successful_agents,
|
|
@@ -1108,56 +1108,18 @@ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
|
1108
1108
|
}
|
|
1109
1109
|
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1110
1110
|
};
|
|
1111
|
-
|
|
1111
|
+
// 0.3.28 Step 4b: adaptive 3-pane tiling replaced with 1-window-per-agent
|
|
1112
|
+
// Python-parity layout. 4 workers → spawn_first + 3×spawn_into, each in its
|
|
1113
|
+
// own window named after the agent_id. No team-w<N> windows; no splits.
|
|
1112
1114
|
assert_eq!(
|
|
1113
1115
|
transport
|
|
1114
1116
|
.spawn_records()
|
|
1115
1117
|
.iter()
|
|
1116
1118
|
.map(|(kind, _)| kind.as_str())
|
|
1117
1119
|
.collect::<Vec<_>>(),
|
|
1118
|
-
vec!["spawn_first", "
|
|
1119
|
-
"4
|
|
1120
|
-
|
|
1121
|
-
assert_eq!(
|
|
1122
|
-
transport
|
|
1123
|
-
.pane_title_records()
|
|
1124
|
-
.iter()
|
|
1125
|
-
.map(|(_, window, pane, title)| (window.as_str(), pane.as_str(), title.as_str()))
|
|
1126
|
-
.collect::<Vec<_>>(),
|
|
1127
|
-
vec![
|
|
1128
|
-
("team-w1", "%0", "w1"),
|
|
1129
|
-
("team-w1", "%1", "w2"),
|
|
1130
|
-
("team-w1", "%2", "w3"),
|
|
1131
|
-
("team-w2", "%3", "w4"),
|
|
1132
|
-
],
|
|
1133
|
-
"adaptive workers must set tmux pane titles to the agent id"
|
|
1134
|
-
);
|
|
1135
|
-
assert_eq!(
|
|
1136
|
-
launch
|
|
1137
|
-
.started
|
|
1138
|
-
.iter()
|
|
1139
|
-
.map(|started| {
|
|
1140
|
-
(
|
|
1141
|
-
started.agent_id.as_str().to_string(),
|
|
1142
|
-
started.layout_window.as_ref().map(|w| w.as_str().to_string()),
|
|
1143
|
-
started.pane_index,
|
|
1144
|
-
)
|
|
1145
|
-
})
|
|
1146
|
-
.collect::<Vec<_>>(),
|
|
1147
|
-
vec![
|
|
1148
|
-
("w1".to_string(), Some("team-w1".to_string()), Some(0)),
|
|
1149
|
-
("w2".to_string(), Some("team-w1".to_string()), Some(1)),
|
|
1150
|
-
("w3".to_string(), Some("team-w1".to_string()), Some(2)),
|
|
1151
|
-
("w4".to_string(), Some("team-w2".to_string()), Some(0)),
|
|
1152
|
-
]
|
|
1153
|
-
);
|
|
1154
|
-
assert!(
|
|
1155
|
-
attach_commands.iter().any(|cmd| cmd.contains(":team-w1")),
|
|
1156
|
-
"attach commands must include the first layout window: {attach_commands:?}"
|
|
1157
|
-
);
|
|
1158
|
-
assert!(
|
|
1159
|
-
attach_commands.iter().any(|cmd| cmd.contains(":team-w2")),
|
|
1160
|
-
"attach commands must include the second layout window: {attach_commands:?}"
|
|
1120
|
+
vec!["spawn_first", "spawn_into", "spawn_into", "spawn_into"],
|
|
1121
|
+
"0.3.28 Step 4b: 4 workers → 1 spawn_first + 3 spawn_into; \
|
|
1122
|
+
no splits in worker session"
|
|
1161
1123
|
);
|
|
1162
1124
|
|
|
1163
1125
|
let (_raw, state) = raw_runtime_state(workspace);
|
|
@@ -1169,26 +1131,15 @@ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
|
1169
1131
|
== state.get("session_name").and_then(serde_json::Value::as_str)),
|
|
1170
1132
|
"pane titles must be configured inside the team session"
|
|
1171
1133
|
);
|
|
1172
|
-
|
|
1173
|
-
assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("
|
|
1174
|
-
assert_eq!(state.pointer("/agents/
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
);
|
|
1180
|
-
assert_eq!(
|
|
1181
|
-
state.pointer("/agents/w1/display/pane_title"),
|
|
1182
|
-
Some(&json!("w1"))
|
|
1183
|
-
);
|
|
1184
|
-
assert_eq!(
|
|
1185
|
-
state.pointer("/agents/w1/display/linked_session"),
|
|
1186
|
-
Some(&json!(null))
|
|
1187
|
-
);
|
|
1188
|
-
assert_eq!(
|
|
1189
|
-
state.pointer("/agents/w4/layout_window"),
|
|
1190
|
-
Some(&json!("team-w2"))
|
|
1134
|
+
// Each worker lives in its OWN window named `agent_id` (Python parity).
|
|
1135
|
+
assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("w1")));
|
|
1136
|
+
assert_eq!(state.pointer("/agents/w4/window"), Some(&json!("w4")));
|
|
1137
|
+
// attach commands point to the per-agent windows.
|
|
1138
|
+
assert!(
|
|
1139
|
+
attach_commands.iter().any(|cmd| cmd.contains(":w1")),
|
|
1140
|
+
"attach commands must include the per-agent windows: {attach_commands:?}"
|
|
1191
1141
|
);
|
|
1142
|
+
let _ = display_backend; // display_backend value preserved by upstream
|
|
1192
1143
|
}
|
|
1193
1144
|
|
|
1194
1145
|
#[test]
|
|
@@ -2403,6 +2354,9 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
2403
2354
|
.keys()
|
|
2404
2355
|
.cloned()
|
|
2405
2356
|
.collect::<Vec<_>>();
|
|
2357
|
+
// 0.3.28 Step 4b: adaptive layout fields (layout_window, layout_index,
|
|
2358
|
+
// pane_index, display) removed from running agent state — 1-window-per-agent
|
|
2359
|
+
// means no layout bookkeeping needed.
|
|
2406
2360
|
assert_eq!(
|
|
2407
2361
|
keys,
|
|
2408
2362
|
vec![
|
|
@@ -2421,15 +2375,11 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
2421
2375
|
"captured_at",
|
|
2422
2376
|
"captured_via",
|
|
2423
2377
|
"attribution_confidence",
|
|
2424
|
-
"layout_window",
|
|
2425
|
-
"layout_index",
|
|
2426
|
-
"pane_index",
|
|
2427
|
-
"display",
|
|
2428
2378
|
"spawn_cwd",
|
|
2429
2379
|
"spawned_at",
|
|
2430
2380
|
"pane_id",
|
|
2431
2381
|
],
|
|
2432
|
-
"running agent state
|
|
2382
|
+
"0.3.28 running agent state: no adaptive layout bookkeeping; raw={raw}"
|
|
2433
2383
|
);
|
|
2434
2384
|
assert_eq!(agent["status"], json!("running"));
|
|
2435
2385
|
assert_eq!(agent["provider"], json!("codex"));
|
|
@@ -2437,7 +2387,7 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
2437
2387
|
assert_eq!(agent["model"], json!("gpt-5.5"));
|
|
2438
2388
|
assert_eq!(agent["auth_mode"], json!("subscription"));
|
|
2439
2389
|
assert!(agent["profile"].is_null());
|
|
2440
|
-
assert_eq!(agent["window"], json!("
|
|
2390
|
+
assert_eq!(agent["window"], json!("implementer"));
|
|
2441
2391
|
assert_eq!(
|
|
2442
2392
|
agent["mcp_config"],
|
|
2443
2393
|
json!(workspace
|
|
@@ -2459,12 +2409,13 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
2459
2409
|
assert!(agent["captured_at"].is_null());
|
|
2460
2410
|
assert!(agent["captured_via"].is_null());
|
|
2461
2411
|
assert!(agent["attribution_confidence"].is_null());
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2412
|
+
// 0.3.28 Step 4b: layout_window/layout_index/pane_index/display removed.
|
|
2413
|
+
// No adaptive bookkeeping needed — each worker has its own window named
|
|
2414
|
+
// after the agent_id.
|
|
2415
|
+
assert!(agent.get("layout_window").is_none());
|
|
2416
|
+
assert!(agent.get("layout_index").is_none());
|
|
2417
|
+
assert!(agent.get("pane_index").is_none());
|
|
2418
|
+
assert!(agent.get("display").is_none());
|
|
2468
2419
|
// D5 (#264) / Python launch/core.py:253 — fresh launch persists spawn_cwd=workspace.
|
|
2469
2420
|
assert_eq!(agent["spawn_cwd"], json!(workspace.to_string_lossy()));
|
|
2470
2421
|
assert_eq!(agent["spawned_at"], json!(FIXED_SPAWNED_AT));
|
|
@@ -595,15 +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.
|
|
599
|
-
///
|
|
600
|
-
///
|
|
601
|
-
/// EnterSentWithoutPlaceholderCheck
|
|
602
|
-
///
|
|
603
|
-
///
|
|
604
|
-
///
|
|
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.
|
|
605
605
|
#[test]
|
|
606
|
-
fn
|
|
606
|
+
fn e46_inject_text_with_token_visible_but_unconsumed_reports_unverified() {
|
|
607
607
|
let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_red1]";
|
|
608
608
|
let (be, _rec) = backend_with(MockResp::Out(ok(token_text)), vec![]);
|
|
609
609
|
let report = be
|
|
@@ -614,15 +614,16 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
|
|
|
614
614
|
true,
|
|
615
615
|
)
|
|
616
616
|
.expect("inject runs");
|
|
617
|
-
// 0.3.
|
|
618
|
-
//
|
|
617
|
+
// 0.3.28-final: no grace fallback. Token visible + not consumed →
|
|
618
|
+
// SubmitConsumptionUnverified (delivery treats as not delivered).
|
|
619
619
|
assert_eq!(
|
|
620
620
|
report.submit_verification,
|
|
621
|
-
SubmitVerification::
|
|
622
|
-
"0.3.
|
|
623
|
-
|
|
624
|
-
EnterSentWithoutPlaceholderCheck
|
|
625
|
-
|
|
621
|
+
SubmitVerification::SubmitConsumptionUnverified,
|
|
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 {:?}",
|
|
626
627
|
report.submit_verification
|
|
627
628
|
);
|
|
628
629
|
assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
|
|
@@ -1441,25 +1441,15 @@ impl Transport for TmuxBackend {
|
|
|
1441
1441
|
}
|
|
1442
1442
|
}
|
|
1443
1443
|
|
|
1444
|
-
//
|
|
1445
|
-
// Escape
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1449
|
-
//
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
-
}
|
|
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;
|
|
1463
1453
|
|
|
1464
1454
|
// Enter — send-keys failure is degraded (tmux may not have
|
|
1465
1455
|
// the pane in sim/test env). Break to consumed=None path
|
|
@@ -1512,23 +1502,18 @@ impl Transport for TmuxBackend {
|
|
|
1512
1502
|
|
|
1513
1503
|
let submit_verification = match consumed {
|
|
1514
1504
|
Some(true) => SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
1515
|
-
Some(false)
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
1528
|
-
} else {
|
|
1529
|
-
SubmitVerification::SubmitConsumptionUnverified
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
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.
|
|
1516
|
+
Some(false) => SubmitVerification::SubmitConsumptionUnverified,
|
|
1532
1517
|
None => submit_verification_for_key(submit),
|
|
1533
1518
|
};
|
|
1534
1519
|
let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
|