@team-agent/installer 0.3.26 → 0.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/mod.rs +14 -0
- package/crates/team-agent/src/layout/manager.rs +241 -0
- package/crates/team-agent/src/layout/mod.rs +29 -0
- package/crates/team-agent/src/layout/overlay.rs +122 -0
- package/crates/team-agent/src/layout/placement.rs +47 -0
- package/crates/team-agent/src/layout/recovery.rs +101 -0
- package/crates/team-agent/src/layout/sessions.rs +277 -0
- package/crates/team-agent/src/layout/worker_env.rs +267 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +35 -0
- package/crates/team-agent/src/leader/start.rs +29 -13
- package/crates/team-agent/src/leader/tests/identity.rs +30 -15
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/launch.rs +55 -64
- package/crates/team-agent/src/lifecycle/restart/agent.rs +18 -10
- package/crates/team-agent/src/lifecycle/restart/common.rs +16 -9
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +26 -75
- package/crates/team-agent/src/messaging/delivery.rs +4 -2
- package/crates/team-agent/src/messaging/leader_receiver.rs +61 -24
- package/crates/team-agent/src/tmux_backend/tests.rs +64 -22
- package/crates/team-agent/src/tmux_backend.rs +185 -132
- package/package.json +4 -4
|
@@ -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(
|
|
@@ -3292,6 +3267,13 @@ fn dangerous_leader_flags() -> &'static [(&'static str, &'static str)] {
|
|
|
3292
3267
|
("claude", "--dangerously-skip-permissions"),
|
|
3293
3268
|
("claude", "--dangerously-skip-permission"),
|
|
3294
3269
|
("codex", "--dangerously-bypass-approvals-and-sandbox"),
|
|
3270
|
+
// 0.3.27 P1 (E54 symptom 2): copilot's full-permission flags. Without
|
|
3271
|
+
// these entries, a copilot leader running with --allow-all / --yolo would
|
|
3272
|
+
// NOT propagate bypass to claude/codex workers because
|
|
3273
|
+
// detect_dangerous_approval scans the coordinator's ancestor argv and
|
|
3274
|
+
// only matches entries in THIS table.
|
|
3275
|
+
("copilot", "--allow-all"),
|
|
3276
|
+
("copilot", "--yolo"),
|
|
3295
3277
|
]
|
|
3296
3278
|
}
|
|
3297
3279
|
|
|
@@ -3299,6 +3281,8 @@ fn binary_matches_provider(provider: &str, binary: Option<&str>) -> bool {
|
|
|
3299
3281
|
match (provider, binary) {
|
|
3300
3282
|
("codex", Some("codex")) => true,
|
|
3301
3283
|
("claude", Some("claude" | "claude-code" | "claude_code")) => true,
|
|
3284
|
+
// 0.3.27 P1 (E54): copilot binary detection for dangerous-approval scan.
|
|
3285
|
+
("copilot", Some("copilot" | "github-copilot" | "gh-copilot")) => true,
|
|
3302
3286
|
_ => false,
|
|
3303
3287
|
}
|
|
3304
3288
|
}
|
|
@@ -4778,6 +4762,13 @@ fn spec_session_name(spec: &Value) -> SessionName {
|
|
|
4778
4762
|
SessionName::new(format!("team-{team_name}"))
|
|
4779
4763
|
}
|
|
4780
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
|
+
|
|
4781
4772
|
fn spec_agents(spec: &Value) -> Vec<AgentId> {
|
|
4782
4773
|
spec_agent_values(spec)
|
|
4783
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));
|
|
@@ -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(
|