@team-agent/installer 0.5.105 → 0.5.107
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/README.md +5 -3
- package/README.zh.md +4 -3
- package/crates/team-agent/src/cli/adapters.rs +15 -1
- package/crates/team-agent/src/cli/emit.rs +33 -13
- package/crates/team-agent/src/cli/mod.rs +148 -21
- package/crates/team-agent/src/cli/status_port/brief.rs +62 -3
- package/crates/team-agent/src/cli/tests/base.rs +112 -225
- package/crates/team-agent/src/cli/tests/run_delegation.rs +4 -2
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -2
- package/crates/team-agent/src/compiler/tests.rs +46 -59
- package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
- package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +24 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +22 -9
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +43 -66
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +71 -392
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
- package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +6 -2
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
- package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
- package/crates/team-agent/src/model/name_similarity.rs +52 -67
- package/crates/team-agent/src/model/task_graph.rs +37 -124
- package/crates/team-agent/src/model/yaml/tests.rs +46 -165
- package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
- package/crates/team-agent/src/provider/tests/classify.rs +106 -198
- package/crates/team-agent/src/state/projection.rs +15 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +315 -501
- package/crates/team-agent/src/tmux_backend.rs +110 -0
- package/crates/team-agent/src/transport/test_support.rs +35 -0
- package/crates/team-agent/src/transport/tests/wire.rs +32 -58
- package/crates/team-agent/src/transport.rs +26 -12
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +2 -2
|
@@ -373,6 +373,8 @@ pub(crate) fn start_agent_at_paths(
|
|
|
373
373
|
};
|
|
374
374
|
let into_existing_session =
|
|
375
375
|
session_live_or_default(transport, &session_name, session_name_present(&state));
|
|
376
|
+
let inherited_generation =
|
|
377
|
+
crate::state::projection::session_generation(&state).map(str::to_string);
|
|
376
378
|
let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn_json(
|
|
377
379
|
&agent,
|
|
378
380
|
provider,
|
|
@@ -441,6 +443,24 @@ pub(crate) fn start_agent_at_paths(
|
|
|
441
443
|
&safety,
|
|
442
444
|
start_mode,
|
|
443
445
|
)?;
|
|
446
|
+
let generation = if into_existing_session {
|
|
447
|
+
inherited_generation.as_deref()
|
|
448
|
+
} else {
|
|
449
|
+
Some(spawn.spawned_at.as_str())
|
|
450
|
+
};
|
|
451
|
+
if let Some(generation) = generation {
|
|
452
|
+
state["generation"] = serde_json::json!(generation);
|
|
453
|
+
if !into_existing_session {
|
|
454
|
+
transport
|
|
455
|
+
.set_session_owner_with_generation(
|
|
456
|
+
&session_name,
|
|
457
|
+
workspace,
|
|
458
|
+
&resolved_team_key,
|
|
459
|
+
generation,
|
|
460
|
+
)
|
|
461
|
+
.map_err(|error| LifecycleError::Transport(error.to_string()))?;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
444
464
|
// 0.5.66 bypass 单源 §2.7:启动即带 bypass → 审计留痕。
|
|
445
465
|
if safety.enabled {
|
|
446
466
|
let state_agent = state
|
|
@@ -1156,6 +1176,30 @@ pub(super) fn stop_agent_at_paths(
|
|
|
1156
1176
|
|| !same_role_panes.is_empty()
|
|
1157
1177
|
|| (pane_id.is_none() && window_exists(transport, &session_name, &window));
|
|
1158
1178
|
if stopped {
|
|
1179
|
+
let stop_targets =
|
|
1180
|
+
if let Some(pane) = pane_id.as_ref().filter(|_| stored_pane_live) {
|
|
1181
|
+
vec![Target::Pane(pane.clone())]
|
|
1182
|
+
} else if !same_role_panes.is_empty() {
|
|
1183
|
+
same_role_panes
|
|
1184
|
+
.iter()
|
|
1185
|
+
.map(|pane| Target::Pane(pane.pane_id.clone()))
|
|
1186
|
+
.collect()
|
|
1187
|
+
} else {
|
|
1188
|
+
vec![Target::SessionWindow {
|
|
1189
|
+
session: session_name.clone(),
|
|
1190
|
+
window: WindowName::new(&window),
|
|
1191
|
+
}]
|
|
1192
|
+
};
|
|
1193
|
+
// Explicit kill-pane/kill-window ignores remain-on-exit on the worker.
|
|
1194
|
+
// Preserve the last window BEFORE killing it, or the coordinator sees
|
|
1195
|
+
// a missing session and stops while the seat is merely being stopped.
|
|
1196
|
+
transport
|
|
1197
|
+
.preserve_session_before_stop(&session_name, &stop_targets)
|
|
1198
|
+
.map_err(|error| {
|
|
1199
|
+
LifecycleError::Transport(format!(
|
|
1200
|
+
"failed to preserve session before stopping agent {agent_id}: {error}"
|
|
1201
|
+
))
|
|
1202
|
+
})?;
|
|
1159
1203
|
// golden operations.py:84-86: a non-zero kill-window raises
|
|
1160
1204
|
// RuntimeError(f"failed to stop agent {agent_id}: {proc.stderr.strip()}").
|
|
1161
1205
|
//
|
|
@@ -509,6 +509,8 @@ fn restart_with_selected_team_and_transport(
|
|
|
509
509
|
let live_worker_session_deferred_teardown =
|
|
510
510
|
session_live_or_default(transport, &session_name, false)
|
|
511
511
|
&& !collect_live_agents_from_state(&state).is_empty();
|
|
512
|
+
let inherited_generation =
|
|
513
|
+
crate::state::projection::session_generation(&state).map(str::to_string);
|
|
512
514
|
let same_role_cohort_targets =
|
|
513
515
|
same_role_cohort_targets_from_decisions(&state, spec_workspace, &plan.decisions);
|
|
514
516
|
let pre_spawn_pane_ids = if live_worker_session_deferred_teardown {
|
|
@@ -998,6 +1000,25 @@ fn restart_with_selected_team_and_transport(
|
|
|
998
1000
|
}
|
|
999
1001
|
}
|
|
1000
1002
|
}
|
|
1003
|
+
if !fatal_resume_failure && !successful_agents.is_empty() {
|
|
1004
|
+
let generation = if live_worker_session_deferred_teardown {
|
|
1005
|
+
inherited_generation
|
|
1006
|
+
} else {
|
|
1007
|
+
// A recreated session gets the first successful NEW worker's birth,
|
|
1008
|
+
// not an arbitrary old stopped worker still present in the roster.
|
|
1009
|
+
successful_agents.first().and_then(|agent| {
|
|
1010
|
+
state
|
|
1011
|
+
.get("agents")?
|
|
1012
|
+
.get(agent.agent_id.as_str())?
|
|
1013
|
+
.get("spawned_at")?
|
|
1014
|
+
.as_str()
|
|
1015
|
+
.map(str::to_string)
|
|
1016
|
+
})
|
|
1017
|
+
};
|
|
1018
|
+
if let Some(generation) = generation {
|
|
1019
|
+
state["generation"] = serde_json::json!(generation);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1001
1022
|
save_restart_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
|
|
1002
1023
|
&selected.run_workspace,
|
|
1003
1024
|
&mut state,
|
|
@@ -1040,23 +1061,9 @@ fn restart_with_selected_team_and_transport(
|
|
|
1040
1061
|
attach_commands,
|
|
1041
1062
|
});
|
|
1042
1063
|
}
|
|
1043
|
-
//
|
|
1044
|
-
//
|
|
1045
|
-
|
|
1046
|
-
if let Some(generation) = state
|
|
1047
|
-
.get("generation")
|
|
1048
|
-
.and_then(serde_json::Value::as_str)
|
|
1049
|
-
.or_else(|| {
|
|
1050
|
-
state
|
|
1051
|
-
.get("agents")
|
|
1052
|
-
.and_then(serde_json::Value::as_object)
|
|
1053
|
-
.and_then(|agents| {
|
|
1054
|
-
agents.values().find_map(|agent| {
|
|
1055
|
-
agent.get("spawned_at").and_then(serde_json::Value::as_str)
|
|
1056
|
-
})
|
|
1057
|
-
})
|
|
1058
|
-
})
|
|
1059
|
-
{
|
|
1064
|
+
// Stamp the physical session birth after durable state. Replacing workers
|
|
1065
|
+
// in a retained session does not create a new session generation.
|
|
1066
|
+
if let Some(generation) = crate::state::projection::session_generation(&state) {
|
|
1060
1067
|
transport
|
|
1061
1068
|
.set_session_owner_with_generation(
|
|
1062
1069
|
&session_name,
|
|
@@ -365,15 +365,6 @@ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
|
|
|
365
365
|
);
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
// REAL-MACHINE e2e boundary (rt-host-a verifies): after `add-agent w2`, the coordinator delivers
|
|
369
|
-
// `send w2` and w2 reports a result (full round-trip). #[ignore] — needs a live tmux session + worker.
|
|
370
|
-
#[test]
|
|
371
|
-
#[ignore = "real-machine: add-agent then send w2 round-trips"]
|
|
372
|
-
fn add_agent_then_send_w2_round_trips() {
|
|
373
|
-
// The framework asserts: add-agent w2 -> w2 in the live session + roster -> `send w2 <msg>` ->
|
|
374
|
-
// coordinator delivers -> w2 emits a result_envelope (results row). Not unit-testable in-process.
|
|
375
|
-
}
|
|
376
|
-
|
|
377
368
|
#[derive(Clone)]
|
|
378
369
|
struct SocketRecordingRunner {
|
|
379
370
|
recorded: std::sync::Arc<std::sync::Mutex<Vec<Vec<String>>>>,
|
|
@@ -505,6 +496,7 @@ session="${TEAM_AGENT_E27_SESSION_NAME}"
|
|
|
505
496
|
pane="${TEAM_AGENT_E27_PANE_ID}"
|
|
506
497
|
killed_marker="${log}.killed"
|
|
507
498
|
spawned_marker="${log}.spawned"
|
|
499
|
+
anchor_marker="${log}.anchor"
|
|
508
500
|
track=0
|
|
509
501
|
case "$*" in
|
|
510
502
|
*"$expected"*|*"$session"*|*"$pane"*) track=1 ;;
|
|
@@ -544,6 +536,23 @@ case "$*" in
|
|
|
544
536
|
;;
|
|
545
537
|
esac
|
|
546
538
|
case "$*" in
|
|
539
|
+
*"list-panes -s -t =$session -F"*)
|
|
540
|
+
if [ -f "$anchor_marker" ]; then
|
|
541
|
+
printf '%%9299\t[team-agent keepalive]\n'
|
|
542
|
+
fi
|
|
543
|
+
if [ ! -f "$killed_marker" ]; then
|
|
544
|
+
printf '%s\talpha\n' "$pane"
|
|
545
|
+
fi
|
|
546
|
+
if [ -f "$spawned_marker" ]; then
|
|
547
|
+
printf '%%9288\talpha\n'
|
|
548
|
+
fi
|
|
549
|
+
exit 0
|
|
550
|
+
;;
|
|
551
|
+
*"new-window "*"[team-agent keepalive]"*)
|
|
552
|
+
: > "$anchor_marker"
|
|
553
|
+
printf '%%9299\n'
|
|
554
|
+
exit 0
|
|
555
|
+
;;
|
|
547
556
|
*"new-window "*|*"new-session "*)
|
|
548
557
|
: > "$spawned_marker"
|
|
549
558
|
printf '%%9288\n'
|
|
@@ -651,6 +660,10 @@ fn e27_stop_agent_uses_attached_explicit_state_socket() {
|
|
|
651
660
|
"attached explicit-socket worker should be stopped"
|
|
652
661
|
);
|
|
653
662
|
assert_only_expected_socket_used(&shim.log, endpoint);
|
|
663
|
+
let raw = std::fs::read_to_string(&shim.log).unwrap();
|
|
664
|
+
assert!(raw.contains(&format!("list-panes -s -t ={session_name}")));
|
|
665
|
+
assert!(raw.contains("set-window-option -t %9299 remain-on-exit on"));
|
|
666
|
+
assert!(raw.contains("respawn-pane -k -t %9299 exit 0"));
|
|
654
667
|
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
655
668
|
assert_eq!(
|
|
656
669
|
state
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
const [wrapperPath, receiptPath, cwd] = process.argv.slice(2);
|
|
5
|
+
if (!wrapperPath || !receiptPath || !cwd) throw new Error("expected wrapper, receipt, and cwd");
|
|
6
|
+
|
|
7
|
+
const registrations = [];
|
|
8
|
+
const unregistrations = [];
|
|
9
|
+
const handlers = new Map();
|
|
10
|
+
const extensionCommands = [{
|
|
11
|
+
name: "mcp",
|
|
12
|
+
source: "extension",
|
|
13
|
+
sourceInfo: { path: "builtin:mcp", source: "builtin", scope: "user", origin: "top-level" },
|
|
14
|
+
}];
|
|
15
|
+
const pi = {
|
|
16
|
+
on(event, handler) {
|
|
17
|
+
const list = handlers.get(event) ?? [];
|
|
18
|
+
list.push(handler);
|
|
19
|
+
handlers.set(event, list);
|
|
20
|
+
},
|
|
21
|
+
getCommands() {
|
|
22
|
+
return extensionCommands;
|
|
23
|
+
},
|
|
24
|
+
getMcpServers() {
|
|
25
|
+
return registrations.map(({ name, config }) => ({ name, config, extensionPath: "<native-n1-fixture>" }));
|
|
26
|
+
},
|
|
27
|
+
getAllTools() {
|
|
28
|
+
return registrations.flatMap(({ name, config }) => Object.entries(config.toolExposure ?? {})
|
|
29
|
+
.map(([tool, exposure]) => ({ name: `mcp__${name}__${tool}`, namespace: { name: `mcp__${name}` }, exposure })));
|
|
30
|
+
},
|
|
31
|
+
getActiveTools() {
|
|
32
|
+
return this.getAllTools().filter((tool) => tool.exposure !== "hidden").map((tool) => tool.name);
|
|
33
|
+
},
|
|
34
|
+
registerMcpServer(name, config) {
|
|
35
|
+
registrations.push({ name, config });
|
|
36
|
+
},
|
|
37
|
+
unregisterMcpServer(name) {
|
|
38
|
+
unregistrations.push(name);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const extension = await import(pathToFileURL(wrapperPath).href);
|
|
43
|
+
if (typeof extension.default !== "function") throw new Error("generated wrapper has no default extension factory");
|
|
44
|
+
await extension.default(pi);
|
|
45
|
+
const context = {
|
|
46
|
+
cwd,
|
|
47
|
+
sessionManager: {
|
|
48
|
+
getSessionFile: () => undefined,
|
|
49
|
+
getLeafId: () => undefined,
|
|
50
|
+
getEntries: () => [],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
for (const handler of handlers.get("session_start") ?? []) {
|
|
54
|
+
await handler({ type: "session_start" }, context);
|
|
55
|
+
}
|
|
56
|
+
if (registrations.length !== 1) throw new Error(`expected one native MCP registration, saw ${registrations.length}`);
|
|
57
|
+
const expectedTools = Object.keys(registrations[0].config.toolExposure ?? {})
|
|
58
|
+
.map((tool) => `mcp__${registrations[0].name}__${tool}`);
|
|
59
|
+
const deadline = Date.now() + 3_000;
|
|
60
|
+
while (!expectedTools.every((name) => pi.getActiveTools().includes(name))) {
|
|
61
|
+
if (Date.now() >= deadline) throw new Error(`native MCP tools never became active: ${pi.getActiveTools()}`);
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
63
|
+
}
|
|
64
|
+
const beforeAgentStart = handlers.get("before_agent_start")?.[0];
|
|
65
|
+
if (typeof beforeAgentStart !== "function") throw new Error("generated wrapper omitted before_agent_start wire binding");
|
|
66
|
+
const binding = await beforeAgentStart({ systemPrompt: "N1 fixture" }, context);
|
|
67
|
+
const wirePrompt = binding?.systemPrompt;
|
|
68
|
+
if (typeof wirePrompt !== "string" || !expectedTools.every((name) => wirePrompt.includes(name))) {
|
|
69
|
+
throw new Error(`native Team tool wire binding is incomplete: ${wirePrompt}`);
|
|
70
|
+
}
|
|
71
|
+
await writeFile(receiptPath, `${JSON.stringify({ registrations, unregistrations, activeTools: pi.getActiveTools(), expectedTools, wirePrompt }, null, 2)}\n`, "utf8");
|
|
@@ -73,6 +73,8 @@ pub(super) struct LaneTransport {
|
|
|
73
73
|
killed: LaneKills,
|
|
74
74
|
spawns: LaneSpawns,
|
|
75
75
|
spawn_observed_at: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
|
|
76
|
+
stop_preservation: std::sync::Mutex<Vec<(Vec<crate::transport::Target>, Vec<String>)>>,
|
|
77
|
+
fail_stop_preservation: bool,
|
|
76
78
|
}
|
|
77
79
|
impl LaneTransport {
|
|
78
80
|
pub(super) fn new(session: &str, windows: &[&str]) -> Self {
|
|
@@ -85,6 +87,8 @@ impl LaneTransport {
|
|
|
85
87
|
killed: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
86
88
|
spawns: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
87
89
|
spawn_observed_at: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
90
|
+
stop_preservation: std::sync::Mutex::new(Vec::new()),
|
|
91
|
+
fail_stop_preservation: false,
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
pub(super) fn killed(&self) -> Vec<String> {
|
|
@@ -297,6 +301,21 @@ impl crate::transport::Transport for LaneTransport {
|
|
|
297
301
|
) -> Result<crate::transport::SetEnvOutcome, crate::transport::TransportError> {
|
|
298
302
|
Ok(crate::transport::SetEnvOutcome::Applied)
|
|
299
303
|
}
|
|
304
|
+
fn preserve_session_before_stop(
|
|
305
|
+
&self,
|
|
306
|
+
session: &crate::transport::SessionName,
|
|
307
|
+
targets: &[crate::transport::Target],
|
|
308
|
+
) -> Result<(), crate::transport::TransportError> {
|
|
309
|
+
self.stop_preservation
|
|
310
|
+
.lock()
|
|
311
|
+
.unwrap()
|
|
312
|
+
.push((targets.to_vec(), self.killed()));
|
|
313
|
+
if self.fail_stop_preservation {
|
|
314
|
+
Err(crate::transport::TransportError::TargetNotFound { target: session.as_str().to_string() })
|
|
315
|
+
} else {
|
|
316
|
+
Ok(())
|
|
317
|
+
}
|
|
318
|
+
}
|
|
300
319
|
fn kill_session(
|
|
301
320
|
&self,
|
|
302
321
|
_s: &crate::transport::SessionName,
|
|
@@ -408,6 +427,30 @@ fn lanea_one_agent_ws(alpha_status: &str) -> PathBuf {
|
|
|
408
427
|
ws
|
|
409
428
|
}
|
|
410
429
|
|
|
430
|
+
#[test]
|
|
431
|
+
fn stop_session_preservation_runs_before_kill_and_failure_preserves_worker_state() {
|
|
432
|
+
for fail in [false, true] {
|
|
433
|
+
let ws = lanea_one_agent_ws("running");
|
|
434
|
+
let mut tx = LaneTransport::new("team-laneateam", &["alpha"]);
|
|
435
|
+
tx.fail_stop_preservation = fail;
|
|
436
|
+
let result = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx);
|
|
437
|
+
let calls = tx.stop_preservation.lock().unwrap();
|
|
438
|
+
assert_eq!(calls.len(), 1);
|
|
439
|
+
assert_eq!(calls[0].1, Vec::<String>::new(), "preserve must precede all worker kills");
|
|
440
|
+
assert_eq!(calls[0].0, vec![crate::transport::Target::Pane(crate::transport::PaneId::new("%alpha"))]);
|
|
441
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
442
|
+
if fail {
|
|
443
|
+
assert!(result.is_err());
|
|
444
|
+
assert!(tx.killed().is_empty());
|
|
445
|
+
assert_eq!(state["agents"]["alpha"]["status"], "running");
|
|
446
|
+
} else {
|
|
447
|
+
assert!(result.unwrap().stopped);
|
|
448
|
+
assert_eq!(tx.killed(), vec!["%alpha".to_string()]);
|
|
449
|
+
assert_eq!(state["agents"]["alpha"]["status"], "stopped");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
411
454
|
fn selected_spec_path(ws: &std::path::Path) -> std::path::PathBuf {
|
|
412
455
|
crate::state::selector::resolve_active_team(
|
|
413
456
|
ws,
|
|
@@ -1278,33 +1321,6 @@ fn lanea_stop_window_present_kills_and_stopped_true() {
|
|
|
1278
1321
|
);
|
|
1279
1322
|
}
|
|
1280
1323
|
|
|
1281
|
-
// ── STOP #2 (stop-display-noop-2) [RED] — close ghostty_workspace slot: persist display.status/pane_title
|
|
1282
|
-
// Golden operations.py:88-92 -> display/close.py:84-85 relabels the slot: display["status"]="stopped",
|
|
1283
|
-
// display["pane_title"]=f"stopped: {agent_id}", written back into the persisted agent entry. Rust never
|
|
1284
|
-
// touches the display (mark_agent_stopped leaves it as-is) and hardcodes display_closed:false. RED: the
|
|
1285
|
-
// persisted display.status/pane_title are the in-process observable.
|
|
1286
|
-
#[test]
|
|
1287
|
-
#[ignore = "legacy external display contract removed; runtime is silent tmux only"]
|
|
1288
|
-
fn lanea_stop_ghostty_workspace_relabels_slot_to_stopped() {
|
|
1289
|
-
let ws = lanea_ws_agents(json!({
|
|
1290
|
-
"alpha": { "status": "running", "provider": "codex", "window": "alpha",
|
|
1291
|
-
"display": { "backend": "ghostty_workspace", "pane_id": "%5", "linked_session": "disp-alpha", "status": "running", "pane_title": "alpha" } }
|
|
1292
|
-
}));
|
|
1293
|
-
let tx = LaneTransport::new("team-laneateam", &["alpha"]);
|
|
1294
|
-
let _ = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx).expect("stop ok");
|
|
1295
|
-
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
1296
|
-
assert_eq!(
|
|
1297
|
-
state.pointer("/agents/alpha/display/status").and_then(serde_json::Value::as_str),
|
|
1298
|
-
Some("stopped"),
|
|
1299
|
-
"stop must relabel a ghostty_workspace slot: display.status='stopped' (close.py:84); Rust leaves it 'running'"
|
|
1300
|
-
);
|
|
1301
|
-
assert_eq!(
|
|
1302
|
-
state.pointer("/agents/alpha/display/pane_title").and_then(serde_json::Value::as_str),
|
|
1303
|
-
Some("stopped: alpha"),
|
|
1304
|
-
"stop must set display.pane_title='stopped: <id>' (close.py:85); Rust never touches the display"
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
1324
|
// ── RESET #3 (reset-paused-restart-2) [RED] — reset of a PAUSED agent returns ok=true (NOT an Err) ───
|
|
1309
1325
|
// Golden operations.py:126-140: after discard, reset re-spawns via start_agent(force,allow_fresh).
|
|
1310
1326
|
// discard does NOT clear `paused`, so start_agent returns the refusal-shaped {ok:False,status:paused,
|
|
@@ -1808,42 +1824,3 @@ fn fresh_start_persists_boundary_from_before_transport_spawn() {
|
|
|
1808
1824
|
spawned_at={spawned_at}, transport_observed_at={transport_observed_at}"
|
|
1809
1825
|
);
|
|
1810
1826
|
}
|
|
1811
|
-
|
|
1812
|
-
// ── REMOVE #6/#12 (remove-rollback-no-agent-health-3 / remove-rollback-health-1) [SEAM #[ignore]] ────
|
|
1813
|
-
// Golden _RemoveRollback captures `self.health = copy.deepcopy(store.agent_health().get(agent_id))`
|
|
1814
|
-
// (agents.py:185) and restore() re-upserts it via _restore_agent_health (agents.py:215-218,268-278). The
|
|
1815
|
-
// Rust RemoveRollback has NO health field and never restores it (restart.rs:972-1067). This is only
|
|
1816
|
-
// observable when a step AFTER the agent_health delete fails — but Rust's only post-delete step is the
|
|
1817
|
-
// snapshot, which golden runs OUTSIDE the rollback-protected region (agents.py:135). Exercising it
|
|
1818
|
-
// golden-faithfully needs a production failure-injection seam at an in-try step after the delete (mirror
|
|
1819
|
-
// the coordinator SaveHook). PORTER: add `health: Option<Value>` to RemoveRollback (capture the row
|
|
1820
|
-
// before delete; restore re-upserts status||"IDLE"/last_output_at/context_usage_pct/current_task_id, or
|
|
1821
|
-
// deletes if None) AND move save_team_runtime_snapshot OUTSIDE the rollback region (golden agents.py:135).
|
|
1822
|
-
#[test]
|
|
1823
|
-
#[ignore = "seam: agent_health rollback restore needs a failure-injection hook (post-delete, in-try) to \
|
|
1824
|
-
exercise in-process; golden agents.py:185/215-218/268-278. Porter adds RemoveRollback.health + \
|
|
1825
|
-
moves save_team_runtime_snapshot outside the rollback region."]
|
|
1826
|
-
fn lanea_remove_rollback_restores_agent_health() {
|
|
1827
|
-
// Golden contract (verified by reading agents.py): on a mid-remove failure after the agent_health
|
|
1828
|
-
// row is deleted, rollback re-upserts the captured row so the health history is not lost.
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
// ── FORK (fork-incomplete-rollback) [SEAM #[ignore]] — post-spawn rollback arms ─────────────────────
|
|
1832
|
-
// Golden operations.py:384-394 wraps spec-mutation..start_coordinator in try/except; on ANY failure it
|
|
1833
|
-
// (1) kills the spawned tmux window if present, (2) adapter.cleanup_mcp, (3) restores old spec text, and
|
|
1834
|
-
// (4) restores prior state. Rust only restores the spec on the spawn_into arm (launch.rs:481); the
|
|
1835
|
-
// save_runtime_state (486-487) and start_coordinator (488-493) failure arms leave the spec mutated, the
|
|
1836
|
-
// already-spawned window un-killed, and the state un-rolled-back; install_mcp/cleanup_mcp are absent.
|
|
1837
|
-
// The adapter.fork_plan arm IS covered HARD above (lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm).
|
|
1838
|
-
// The post-SPAWN arms need a failure-injection seam after spawn_into (codex+subscription forks past
|
|
1839
|
-
// adapter.fork_plan, so the spawn succeeds and there is no in-process way to fail save/coordinator cleanly).
|
|
1840
|
-
// PORTER: a Drop guard armed after the spec write, disarmed on success — kills the window, restores spec
|
|
1841
|
-
// + state, runs cleanup_mcp on every post-write error arm.
|
|
1842
|
-
#[test]
|
|
1843
|
-
#[ignore = "seam: fork post-spawn rollback arms (save_runtime_state / start_coordinator failure) need a \
|
|
1844
|
-
failure-injection hook after spawn_into; golden operations.py:384-394. Porter wires a Drop \
|
|
1845
|
-
guard (kill window + restore spec/state + cleanup_mcp) armed after the spec write."]
|
|
1846
|
-
fn lanea_fork_rollback_complete_on_post_spawn_failure() {
|
|
1847
|
-
// Golden contract (operations.py:384-394): a post-spawn failure kills the spawned window, restores
|
|
1848
|
-
// the old spec text + prior state, and runs cleanup_mcp before re-raising.
|
|
1849
|
-
}
|