@team-agent/installer 0.5.74 → 0.5.77
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 +9 -0
- package/README.zh.md +8 -0
- package/crates/team-agent/src/cli/adapters.rs +23 -96
- package/crates/team-agent/src/cli/emit.rs +35 -7
- package/crates/team-agent/src/cli/mod.rs +63 -42
- package/crates/team-agent/src/cli/models.rs +138 -27
- package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
- package/crates/team-agent/src/cli/status_port.rs +8 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
- package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
- package/crates/team-agent/src/layout/worker_env.rs +366 -0
- package/crates/team-agent/src/leader/lease.rs +274 -40
- package/crates/team-agent/src/leader/registry.rs +177 -19
- package/crates/team-agent/src/leader/types.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +93 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1820 -60
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
- package/crates/team-agent/src/lifecycle/restart/common.rs +3 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +24 -28
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
- package/crates/team-agent/src/lifecycle/types.rs +4 -0
- package/crates/team-agent/src/messaging/delivery.rs +200 -29
- package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
- package/crates/team-agent/src/messaging/watchers.rs +48 -4
- package/crates/team-agent/src/state/paths.rs +17 -2
- package/crates/team-agent/src/state/persist.rs +166 -6
- package/crates/team-agent/src/state/projection.rs +18 -2
- package/crates/team-agent/src/state/repository.rs +36 -0
- package/crates/team-agent/src/state/selector.rs +23 -3
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +122 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +350 -7
- package/crates/team-agent/src/tmux_backend.rs +217 -26
- package/crates/team-agent/src/transport/test_support.rs +18 -3
- package/crates/team-agent/src/transport.rs +99 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -82,6 +82,15 @@ Subagents are one-shot, fire-and-forget, and can't talk to each other. Team Agen
|
|
|
82
82
|
|
|
83
83
|
## Status
|
|
84
84
|
|
|
85
|
+
`team-agent status` prints one concise row per registered node with seven fields:
|
|
86
|
+
`name`, `provider`, `runtime_status`, `activity`, `health`, `session_name`, and `tmux_command`.
|
|
87
|
+
`--json` exposes the same projection as structured JSON. Missing or conflicting evidence is
|
|
88
|
+
reported as `unknown`; a non-null `tmux_command` is safe to copy for the observed socket,
|
|
89
|
+
session, window, and pane. `--summary` and `--detail` remain accepted for compatibility and
|
|
90
|
+
do not add diagnostic or history fields. The accepted nodeprobe pairing and
|
|
91
|
+
operator-trusted capability receipt are documented in
|
|
92
|
+
[`docs/status-nodeprobe.md`](docs/status-nodeprobe.md).
|
|
93
|
+
|
|
85
94
|
**Beta.** Used daily in real projects. Rough edges possible in uncommon configurations. Issues and PRs welcome.
|
|
86
95
|
|
|
87
96
|
## License
|
package/README.zh.md
CHANGED
|
@@ -82,6 +82,14 @@ subagent 是一次性的、干完即焚、彼此不通信。Team Agent 的队友
|
|
|
82
82
|
|
|
83
83
|
## 状态
|
|
84
84
|
|
|
85
|
+
`team-agent status` 为每个已登记节点输出七个简要字段:
|
|
86
|
+
`name`、`provider`、`runtime_status`、`activity`、`health`、`session_name`、`tmux_command`。
|
|
87
|
+
`--json` 与人读输出使用同一信息投影。证据缺失或冲突时显示 `unknown`;非空
|
|
88
|
+
`tmux_command` 可复制到已观测的 socket、session、window 和 pane。`--summary` 与
|
|
89
|
+
`--detail` 保留兼容性,但不会增加诊断或历史字段。已验收 nodeprobe 的配对方式、
|
|
90
|
+
operator-trusted 能力 receipt 与 fail-closed 边界见
|
|
91
|
+
[`docs/status-nodeprobe.md`](docs/status-nodeprobe.md)。
|
|
92
|
+
|
|
85
93
|
**Beta。** 已在真实项目里日常使用,小众配置下可能有毛边。欢迎 issue 和 PR。
|
|
86
94
|
|
|
87
95
|
## License
|
|
@@ -280,8 +280,8 @@ fn warn_ignored_owner_team_id(team_dir: &std::path::Path) {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
/// `cmd_status`(`commands.py:90`)
|
|
284
|
-
/// `--json
|
|
283
|
+
/// `cmd_status`(`commands.py:90`)。CLI status 统一为只读七字段 brief;
|
|
284
|
+
/// `--json` 与人读路径共享 nodeprobe-backed projection;`--summary`/`--detail` 保留参数兼容性但不暴露诊断。
|
|
285
285
|
#[cfg(test)]
|
|
286
286
|
pub(crate) fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
|
|
287
287
|
cmd_status_for_team(args, args.team.as_deref())
|
|
@@ -300,123 +300,50 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
300
300
|
}
|
|
301
301
|
// S4QR-001 (0.4.8): selected-team ambiguity gate. status is a selected-team
|
|
302
302
|
// command: when the workspace has 2+ alive teams and no --team was passed,
|
|
303
|
-
// refuse instead of silently defaulting to the active team. Uses the
|
|
304
|
-
// CommandScope
|
|
305
|
-
// (cli/emit.rs:964-979) so the destructive-command ambiguity gate and the
|
|
306
|
-
// selected-team-read ambiguity gate share a single source of truth.
|
|
303
|
+
// refuse instead of silently defaulting to the active team. Uses the
|
|
304
|
+
// read-only CommandScope helper so the gate cannot migrate/write state.
|
|
307
305
|
if team.is_none() {
|
|
308
|
-
let scope = crate::state::paths::CommandScope::
|
|
306
|
+
let scope = crate::state::paths::CommandScope::resolve_readonly(&args.workspace, None);
|
|
309
307
|
if scope.is_ambiguous() {
|
|
310
308
|
let candidates: Vec<String> = scope.candidates().to_vec();
|
|
311
309
|
let message = format!(
|
|
312
310
|
"status: workspace has multiple alive teams ({}); pass `--team <key>` to choose one",
|
|
313
311
|
candidates.join(", ")
|
|
314
312
|
);
|
|
315
|
-
if args.json {
|
|
316
|
-
let payload = serde_json::json!({
|
|
317
|
-
"ok": false,
|
|
318
|
-
"status": "refused",
|
|
319
|
-
"reason": "team_target_ambiguous",
|
|
320
|
-
"candidates": candidates,
|
|
321
|
-
"message": message,
|
|
322
|
-
});
|
|
323
|
-
return Ok(CmdResult::from_json(payload, args.json));
|
|
324
|
-
}
|
|
325
313
|
return Err(CliError::Usage(message));
|
|
326
314
|
}
|
|
327
315
|
}
|
|
328
|
-
let selected =
|
|
316
|
+
let selected = crate::state::selector::resolve_active_team_readonly(
|
|
329
317
|
&args.workspace,
|
|
330
318
|
team,
|
|
331
319
|
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return
|
|
336
|
-
status_selector_error_payload(&error.to_string(), &args.workspace),
|
|
337
|
-
args.json,
|
|
338
|
-
));
|
|
320
|
+
)?;
|
|
321
|
+
if let Some(agent) = args.agent.as_deref() {
|
|
322
|
+
if !status_port::registered_agent_exists(&selected.state, agent) {
|
|
323
|
+
return Err(CliError::Runtime(format!("unknown agent id: {agent}")));
|
|
339
324
|
}
|
|
340
|
-
};
|
|
341
|
-
if args.summary {
|
|
342
|
-
// 0.4.x compact slimming: `--summary` renders the five-line triage
|
|
343
|
-
// text via `format_status_summary`, which needs the full payload
|
|
344
|
-
// fields (coordinator, leader_receiver, agent_health, queued_messages,
|
|
345
|
-
// latest_results). The slim compact projection drops those, so the
|
|
346
|
-
// summary path must request the full payload (compact=false).
|
|
347
|
-
let value = status_port::status_scoped(
|
|
348
|
-
&selected.run_workspace,
|
|
349
|
-
&selected.state,
|
|
350
|
-
Some(&selected.team_key),
|
|
351
|
-
false,
|
|
352
|
-
false,
|
|
353
|
-
)?;
|
|
354
|
-
return Ok(CmdResult::human(append_reminder(
|
|
355
|
-
format_status_summary(&value),
|
|
356
|
-
crate::cli::STATUS_REMINDER,
|
|
357
|
-
)));
|
|
358
325
|
}
|
|
326
|
+
// Status brief is deliberately independent of the legacy RuntimeSnapshot:
|
|
327
|
+
// it performs one bounded nodeprobe sample and exposes exactly seven
|
|
328
|
+
// fields. `--summary` and `--detail` remain parser-compatible but do not
|
|
329
|
+
// re-enable history, runtime diagnostics, or reminder text.
|
|
359
330
|
if args.json {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
331
|
+
return Ok(CmdResult::from_json(
|
|
332
|
+
status_port::status_brief_scoped(
|
|
333
|
+
&selected.run_workspace,
|
|
334
|
+
&selected.state,
|
|
335
|
+
args.agent.as_deref(),
|
|
336
|
+
),
|
|
337
|
+
true,
|
|
338
|
+
));
|
|
368
339
|
}
|
|
369
|
-
|
|
340
|
+
Ok(CmdResult::human(status_port::format_status_brief(
|
|
370
341
|
&selected.run_workspace,
|
|
371
342
|
&selected.state,
|
|
372
|
-
Some(&selected.team_key),
|
|
373
343
|
args.agent.as_deref(),
|
|
374
|
-
)?;
|
|
375
|
-
if args.detail {
|
|
376
|
-
let value = status_port::status_scoped(
|
|
377
|
-
&selected.run_workspace,
|
|
378
|
-
&selected.state,
|
|
379
|
-
Some(&selected.team_key),
|
|
380
|
-
false,
|
|
381
|
-
true,
|
|
382
|
-
)?;
|
|
383
|
-
if let Some(hint) = value
|
|
384
|
-
.pointer("/runtime/hint")
|
|
385
|
-
.and_then(serde_json::Value::as_str)
|
|
386
|
-
.filter(|hint| !hint.is_empty())
|
|
387
|
-
{
|
|
388
|
-
if !text.is_empty() {
|
|
389
|
-
text.push('\n');
|
|
390
|
-
}
|
|
391
|
-
text.push_str(hint);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
Ok(CmdResult::human(append_reminder(
|
|
395
|
-
text,
|
|
396
|
-
crate::cli::STATUS_REMINDER,
|
|
397
344
|
)))
|
|
398
345
|
}
|
|
399
346
|
|
|
400
|
-
fn status_selector_error_payload(error: &str, workspace: &Path) -> Value {
|
|
401
|
-
let stamp = chrono::Utc::now().format("%Y%m%d-%H%M%S%.6f");
|
|
402
|
-
let log_path = std::env::temp_dir()
|
|
403
|
-
.join("team-agent")
|
|
404
|
-
.join("cli-errors")
|
|
405
|
-
.join(format!("status-{stamp}.log"));
|
|
406
|
-
if let Some(parent) = log_path.parent() {
|
|
407
|
-
let _ = std::fs::create_dir_all(parent);
|
|
408
|
-
}
|
|
409
|
-
let _ = std::fs::write(&log_path, format!("{error}\n"));
|
|
410
|
-
json!({
|
|
411
|
-
"ok": false,
|
|
412
|
-
"error": error,
|
|
413
|
-
"action": "run `team-agent doctor` or inspect the log path shown here",
|
|
414
|
-
"log": log_path.to_string_lossy().to_string(),
|
|
415
|
-
"workspace": workspace.to_string_lossy().to_string(),
|
|
416
|
-
"reminder": crate::cli::STATUS_REMINDER,
|
|
417
|
-
})
|
|
418
|
-
}
|
|
419
|
-
|
|
420
347
|
/// `cmd_watch`(`commands.py:103`)。委派 `coordinator::run_watch`;KeyboardInterrupt/正常 → `SystemExit(0)`。
|
|
421
348
|
/// 返回 [`CmdResult::none`](不经 emit)。
|
|
422
349
|
pub fn cmd_watch(args: &WatchArgs) -> Result<CmdResult, CliError> {
|
|
@@ -85,10 +85,31 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
|
|
|
85
85
|
}
|
|
86
86
|
match dispatch(command, &argv[1..], cwd) {
|
|
87
87
|
Ok(exit) => exit,
|
|
88
|
-
Err(error) =>
|
|
88
|
+
Err(error) => emit_cli_error_for_command(command, &argv[1..], cwd, &error),
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
fn emit_cli_error_for_command(
|
|
93
|
+
command: &str,
|
|
94
|
+
args: &[String],
|
|
95
|
+
cwd: &Path,
|
|
96
|
+
error: &CliError,
|
|
97
|
+
) -> ExitCode {
|
|
98
|
+
if command == "status" {
|
|
99
|
+
emit_status_cli_error(error)
|
|
100
|
+
} else {
|
|
101
|
+
emit_cli_error(command, args, cwd, error)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn emit_status_cli_error(error: &CliError) -> ExitCode {
|
|
106
|
+
let normalized = normalize_cli_error(error);
|
|
107
|
+
let payload_error = normalized.as_ref().unwrap_or(error);
|
|
108
|
+
let safe_error = crate::redaction::redact_external_text(&payload_error.to_string());
|
|
109
|
+
eprintln!("error: {safe_error}");
|
|
110
|
+
ExitCode::Error
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
/// Print a handler's CmdResult to stdout (emit formats json/human), then surface its exit code.
|
|
93
114
|
/// (parser.py: `print(emit(result, as_json))` then the ok→exit mapping.)
|
|
94
115
|
fn emit_result(r: CmdResult) -> ExitCode {
|
|
@@ -387,7 +408,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
387
408
|
)
|
|
388
409
|
.to_string(),
|
|
389
410
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
390
|
-
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n
|
|
411
|
+
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出七字段:name/provider/runtime_status/activity/health/session_name/tmux_command;人读与 --json 使用同一投影。缺少可靠定位或 nodeprobe 证据时显示 unknown;tmux_command 可复制到对应目标。--summary/--detail 仅保留兼容性,不增加诊断字段。".to_string(),
|
|
391
412
|
Some("models") => "usage: team-agent models --provider pi [--search TEXT] [--json]\n\nPrints models.v1 exact role_model entries; each entry includes current=true|false. Catalog readiness is reported as auth=ok|not_ready with auth_basis=catalog_visibility.".to_string(),
|
|
392
413
|
Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
|
|
393
414
|
Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
|
|
@@ -2184,11 +2205,18 @@ mod tests {
|
|
|
2184
2205
|
}
|
|
2185
2206
|
|
|
2186
2207
|
#[test]
|
|
2187
|
-
fn
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
"
|
|
2191
|
-
|
|
2208
|
+
fn status_help_describes_brief_projection_and_unknown_boundary() {
|
|
2209
|
+
let help = command_help(Some("status"));
|
|
2210
|
+
for marker in [
|
|
2211
|
+
"name/provider/runtime_status/activity/health/session_name/tmux_command",
|
|
2212
|
+
"人读与 --json 使用同一投影",
|
|
2213
|
+
"显示 unknown",
|
|
2214
|
+
"tmux_command",
|
|
2215
|
+
"不增加诊断字段",
|
|
2216
|
+
] {
|
|
2217
|
+
assert!(help.contains(marker), "status help missing {marker}: {help}");
|
|
2218
|
+
}
|
|
2219
|
+
assert!(!help.contains("错误细分走 status --summary"));
|
|
2192
2220
|
}
|
|
2193
2221
|
|
|
2194
2222
|
#[test]
|
|
@@ -3633,17 +3633,24 @@ pub mod lifecycle_port {
|
|
|
3633
3633
|
coordinator,
|
|
3634
3634
|
next_actions,
|
|
3635
3635
|
attach_commands,
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3636
|
+
attach_window_failures,
|
|
3637
|
+
} => {
|
|
3638
|
+
let mut value = json!({
|
|
3639
|
+
"ok": true,
|
|
3640
|
+
"status": "restarted",
|
|
3641
|
+
"session_name": session_name.as_str(),
|
|
3642
|
+
"agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
|
|
3643
|
+
"coordinator_started": coordinator_started,
|
|
3644
|
+
"coordinator": crate::lifecycle::coordinator_start_summary_value(&coordinator),
|
|
3645
|
+
"next_actions": next_actions,
|
|
3646
|
+
"attach_commands": attach_commands,
|
|
3647
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
3648
|
+
});
|
|
3649
|
+
if let Some(debt) = attach_window_failures {
|
|
3650
|
+
value["attach_window_failures"] = debt;
|
|
3651
|
+
}
|
|
3652
|
+
value
|
|
3653
|
+
},
|
|
3647
3654
|
crate::lifecycle::RestartReport::Partial {
|
|
3648
3655
|
session_name,
|
|
3649
3656
|
agents,
|
|
@@ -3652,35 +3659,42 @@ pub mod lifecycle_port {
|
|
|
3652
3659
|
coordinator,
|
|
3653
3660
|
next_actions,
|
|
3654
3661
|
attach_commands,
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
"
|
|
3663
|
-
"
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3662
|
+
attach_window_failures,
|
|
3663
|
+
} => {
|
|
3664
|
+
let mut value = json!({
|
|
3665
|
+
"ok": false,
|
|
3666
|
+
"status": "partial",
|
|
3667
|
+
"reason": "restart_agent_failed",
|
|
3668
|
+
"session_name": session_name.as_str(),
|
|
3669
|
+
"agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
|
|
3670
|
+
"failed_agents": failed_agents.iter().map(|failure| json!({
|
|
3671
|
+
"agent_id": failure.agent_id.as_str(),
|
|
3672
|
+
"restart_mode": failure.restart_mode,
|
|
3673
|
+
"decision": failure.decision,
|
|
3674
|
+
"session_id": failure.session_id.as_ref().map(|session| session.as_str()),
|
|
3675
|
+
"phase": failure.phase,
|
|
3676
|
+
"error": failure.error,
|
|
3677
|
+
"action": crate::lifecycle::restart_failure_action(
|
|
3678
|
+
&failure.agent_id,
|
|
3679
|
+
&failure.phase,
|
|
3680
|
+
failure.session_id.as_ref(),
|
|
3681
|
+
),
|
|
3682
|
+
"log": format!(
|
|
3683
|
+
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
3684
|
+
failure.agent_id
|
|
3685
|
+
),
|
|
3686
|
+
})).collect::<Vec<_>>(),
|
|
3687
|
+
"coordinator_started": coordinator_started,
|
|
3688
|
+
"coordinator": crate::lifecycle::coordinator_start_summary_value(&coordinator),
|
|
3689
|
+
"next_actions": next_actions,
|
|
3690
|
+
"attach_commands": attach_commands,
|
|
3691
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
3692
|
+
});
|
|
3693
|
+
if let Some(debt) = attach_window_failures {
|
|
3694
|
+
value["attach_window_failures"] = debt;
|
|
3695
|
+
}
|
|
3696
|
+
value
|
|
3697
|
+
},
|
|
3684
3698
|
crate::lifecycle::RestartReport::Failed {
|
|
3685
3699
|
session_name,
|
|
3686
3700
|
failed_agents,
|
|
@@ -5260,11 +5274,14 @@ pub mod leader_port {
|
|
|
5260
5274
|
if let Some(topology_convergence) = result.topology_convergence {
|
|
5261
5275
|
out.insert("topology_convergence".to_string(), topology_convergence);
|
|
5262
5276
|
}
|
|
5277
|
+
if let Some(attach_window_failures) = result.attach_window_failures {
|
|
5278
|
+
out.insert("attach_window_failures".to_string(), attach_window_failures);
|
|
5279
|
+
}
|
|
5263
5280
|
Value::Object(out)
|
|
5264
5281
|
}
|
|
5265
5282
|
|
|
5266
5283
|
fn attach_lease_value(result: crate::leader::LeaseResult, requeued: Value) -> Value {
|
|
5267
|
-
json!({
|
|
5284
|
+
let mut value = json!({
|
|
5268
5285
|
"ok": result.ok,
|
|
5269
5286
|
"leader_receiver": result
|
|
5270
5287
|
.receiver
|
|
@@ -5277,7 +5294,11 @@ pub mod leader_port {
|
|
|
5277
5294
|
"action": result.action,
|
|
5278
5295
|
},
|
|
5279
5296
|
"requeued_exhausted_watchers": requeued,
|
|
5280
|
-
})
|
|
5297
|
+
});
|
|
5298
|
+
if let Some(attach_window_failures) = result.attach_window_failures {
|
|
5299
|
+
value["attach_window_failures"] = attach_window_failures;
|
|
5300
|
+
}
|
|
5301
|
+
value
|
|
5281
5302
|
}
|
|
5282
5303
|
|
|
5283
5304
|
fn attach_requeued_exhausted_watchers(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//! Read-only Pi model catalog discovery for `team-agent models`.
|
|
2
2
|
use super::{CliError, CmdOutput, CmdResult, ExitCode, ModelsArgs};
|
|
3
3
|
use serde_json::{json, Value};
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
use std::collections::BTreeMap;
|
|
4
6
|
use std::path::Path;
|
|
5
7
|
use std::process::Command;
|
|
6
8
|
use std::time::Duration;
|
|
@@ -272,6 +274,66 @@ mod tests {
|
|
|
272
274
|
let _ = std::fs::remove_dir_all(path.parent().unwrap());
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
#[cfg(unix)]
|
|
278
|
+
fn native_timeout_fixture() -> std::path::PathBuf {
|
|
279
|
+
let path = std::path::PathBuf::from(env!("TEAM_AGENT_MODELS_TIMEOUT_FIXTURE"));
|
|
280
|
+
assert!(path.is_file(), "prebuilt models timeout fixture is missing");
|
|
281
|
+
path
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[cfg(unix)]
|
|
285
|
+
fn native_timeout_receipt() -> std::path::PathBuf {
|
|
286
|
+
let stamp = std::time::SystemTime::now()
|
|
287
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
288
|
+
.unwrap()
|
|
289
|
+
.as_nanos();
|
|
290
|
+
std::path::PathBuf::from("/tmp").join(format!(
|
|
291
|
+
"team-agent-models-timeout-{}-{stamp}.receipt",
|
|
292
|
+
std::process::id()
|
|
293
|
+
))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[cfg(unix)]
|
|
297
|
+
fn read_native_timeout_receipt(path: &Path) -> BTreeMap<String, String> {
|
|
298
|
+
let mut fields = BTreeMap::new();
|
|
299
|
+
for line in std::fs::read_to_string(path).unwrap().lines() {
|
|
300
|
+
let (key, value) = line.split_once('=').expect("receipt key/value");
|
|
301
|
+
assert!(fields.insert(key.to_string(), value.to_string()).is_none());
|
|
302
|
+
}
|
|
303
|
+
assert_eq!(
|
|
304
|
+
fields.keys().map(String::as_str).collect::<Vec<_>>(),
|
|
305
|
+
vec![
|
|
306
|
+
"child_argv_exact",
|
|
307
|
+
"child_pid",
|
|
308
|
+
"child_spawn_attempts",
|
|
309
|
+
"descendant_pid",
|
|
310
|
+
"descendant_started",
|
|
311
|
+
"descendant_stdout_inherited",
|
|
312
|
+
"parent_argv_exact",
|
|
313
|
+
"parent_pid",
|
|
314
|
+
"parent_started_once",
|
|
315
|
+
]
|
|
316
|
+
);
|
|
317
|
+
fields
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#[cfg(unix)]
|
|
321
|
+
fn with_native_mode<T>(
|
|
322
|
+
mode: &str,
|
|
323
|
+
action: impl FnOnce() -> T,
|
|
324
|
+
) -> (T, crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation) {
|
|
325
|
+
let receipt = native_timeout_receipt();
|
|
326
|
+
assert!(!receipt.exists());
|
|
327
|
+
let result = crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
328
|
+
&receipt,
|
|
329
|
+
Some(mode),
|
|
330
|
+
action,
|
|
331
|
+
);
|
|
332
|
+
assert!(!receipt.exists());
|
|
333
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
334
|
+
result
|
|
335
|
+
}
|
|
336
|
+
|
|
275
337
|
#[cfg(unix)]
|
|
276
338
|
#[test]
|
|
277
339
|
fn runner_invokes_exact_argv_once_and_drains() {
|
|
@@ -291,12 +353,38 @@ mod tests {
|
|
|
291
353
|
#[cfg(unix)]
|
|
292
354
|
#[test]
|
|
293
355
|
fn runner_timeout_is_bounded_when_descendant_keeps_stdout() {
|
|
294
|
-
let path =
|
|
356
|
+
let path = native_timeout_fixture();
|
|
357
|
+
let receipt = native_timeout_receipt();
|
|
358
|
+
assert!(!receipt.exists());
|
|
295
359
|
let started = Instant::now();
|
|
296
|
-
let result
|
|
360
|
+
let (result, observation) =
|
|
361
|
+
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
362
|
+
&receipt,
|
|
363
|
+
Some("descendant"),
|
|
364
|
+
|| run_catalog(&path, Duration::from_millis(40), 1024),
|
|
365
|
+
);
|
|
297
366
|
assert!(started.elapsed() < Duration::from_millis(500));
|
|
298
367
|
assert!(result.unwrap_err().contains("timed out"));
|
|
299
|
-
|
|
368
|
+
assert_eq!(observation.spawn_count, 1);
|
|
369
|
+
assert_eq!(observation.argv, vec!["--list-models"]);
|
|
370
|
+
assert_eq!(observation.parent_exit_success, Some(true));
|
|
371
|
+
assert_eq!(observation.parent_exit_code, Some(0));
|
|
372
|
+
assert!(observation.reader_timeout);
|
|
373
|
+
|
|
374
|
+
let fields = read_native_timeout_receipt(&receipt);
|
|
375
|
+
assert_eq!(fields["parent_started_once"], "1");
|
|
376
|
+
assert_eq!(fields["parent_argv_exact"], "1");
|
|
377
|
+
assert_eq!(fields["child_spawn_attempts"], "1");
|
|
378
|
+
assert_eq!(fields["child_argv_exact"], "1");
|
|
379
|
+
assert_eq!(fields["descendant_started"], "1");
|
|
380
|
+
assert_eq!(fields["descendant_stdout_inherited"], "1");
|
|
381
|
+
assert_eq!(
|
|
382
|
+
fields["parent_pid"],
|
|
383
|
+
observation.parent_pid.unwrap().to_string()
|
|
384
|
+
);
|
|
385
|
+
assert_eq!(fields["child_pid"], fields["descendant_pid"]);
|
|
386
|
+
let _ = std::fs::remove_file(&receipt);
|
|
387
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
300
388
|
}
|
|
301
389
|
|
|
302
390
|
#[cfg(unix)]
|
|
@@ -352,35 +440,61 @@ mod tests {
|
|
|
352
440
|
#[cfg(unix)]
|
|
353
441
|
#[test]
|
|
354
442
|
fn runner_fails_closed_for_nonzero_oversize_timeout_and_unavailable() {
|
|
355
|
-
let fail =
|
|
443
|
+
let fail = native_timeout_fixture();
|
|
444
|
+
let (failed_result, fail_observation) = with_native_mode("exit7", || {
|
|
445
|
+
run_catalog(&fail, Duration::from_secs(1), 1024)
|
|
446
|
+
});
|
|
356
447
|
assert_eq!(
|
|
357
|
-
|
|
448
|
+
failed_result.unwrap_err(),
|
|
358
449
|
"Pi model catalog command failed"
|
|
359
450
|
);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
451
|
+
assert_eq!(fail_observation.spawn_count, 1);
|
|
452
|
+
assert_eq!(fail_observation.argv, vec!["--list-models"]);
|
|
453
|
+
assert_eq!(fail_observation.parent_exit_success, Some(false));
|
|
454
|
+
assert_eq!(fail_observation.parent_exit_code, Some(7));
|
|
455
|
+
assert!(!fail_observation.reader_timeout);
|
|
456
|
+
let (failed, failed_observation) = with_native_mode("exit7", || {
|
|
457
|
+
cmd_models_with(
|
|
458
|
+
&ModelsArgs {
|
|
459
|
+
provider: "pi".into(),
|
|
460
|
+
search: None,
|
|
461
|
+
json: false,
|
|
462
|
+
},
|
|
463
|
+
&fail,
|
|
464
|
+
Duration::from_secs(1),
|
|
465
|
+
1024,
|
|
466
|
+
)
|
|
467
|
+
});
|
|
468
|
+
let failed = failed.unwrap();
|
|
469
|
+
assert_eq!(failed_observation.spawn_count, 1);
|
|
470
|
+
assert_eq!(failed_observation.argv, vec!["--list-models"]);
|
|
471
|
+
assert_eq!(failed_observation.parent_exit_success, Some(false));
|
|
472
|
+
assert_eq!(failed_observation.parent_exit_code, Some(7));
|
|
473
|
+
assert!(!failed_observation.reader_timeout);
|
|
371
474
|
let CmdOutput::Human(text) = failed.output else {
|
|
372
475
|
panic!("expected failure projection")
|
|
373
476
|
};
|
|
374
477
|
assert_eq!(failed.exit, ExitCode::Error);
|
|
375
478
|
assert!(!text.contains("sensitive-token"));
|
|
376
|
-
let big =
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
479
|
+
let big = native_timeout_fixture();
|
|
480
|
+
let (big_result, big_observation) = with_native_mode("oversize", || {
|
|
481
|
+
run_catalog(&big, Duration::from_secs(1), 8)
|
|
482
|
+
});
|
|
483
|
+
assert!(big_result.unwrap_err().contains("bounded output"));
|
|
484
|
+
assert_eq!(big_observation.spawn_count, 1);
|
|
485
|
+
assert_eq!(big_observation.argv, vec!["--list-models"]);
|
|
486
|
+
assert_eq!(big_observation.parent_exit_success, Some(true));
|
|
487
|
+
assert_eq!(big_observation.parent_exit_code, Some(0));
|
|
488
|
+
assert!(!big_observation.reader_timeout);
|
|
489
|
+
let slow = native_timeout_fixture();
|
|
490
|
+
let (slow_result, slow_observation) = with_native_mode("sleep", || {
|
|
491
|
+
run_catalog(&slow, Duration::from_millis(20), 1024)
|
|
492
|
+
});
|
|
493
|
+
assert!(slow_result.unwrap_err().contains("timed out"));
|
|
494
|
+
assert_eq!(slow_observation.spawn_count, 1);
|
|
495
|
+
assert_eq!(slow_observation.argv, vec!["--list-models"]);
|
|
496
|
+
assert_eq!(slow_observation.parent_exit_success, None);
|
|
497
|
+
assert!(!slow_observation.reader_timeout);
|
|
384
498
|
assert!(run_catalog(
|
|
385
499
|
std::path::Path::new("/does/not/exist"),
|
|
386
500
|
Duration::from_secs(1),
|
|
@@ -388,9 +502,6 @@ mod tests {
|
|
|
388
502
|
)
|
|
389
503
|
.unwrap_err()
|
|
390
504
|
.contains("unavailable"));
|
|
391
|
-
for path in [fail, big, slow] {
|
|
392
|
-
cleanup_fixture(&path);
|
|
393
|
-
}
|
|
394
505
|
}
|
|
395
506
|
|
|
396
507
|
#[test]
|