@team-agent/installer 0.5.110 → 0.5.112
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/adapters.rs +231 -812
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +493 -451
- package/crates/team-agent/src/cli/leader.rs +4 -9
- package/crates/team-agent/src/cli/leaders.rs +90 -29
- package/crates/team-agent/src/cli/mod.rs +348 -169
- package/crates/team-agent/src/cli/models.rs +80 -24
- package/crates/team-agent/src/cli/named_address.rs +114 -25
- package/crates/team-agent/src/cli/route.rs +10 -5
- package/crates/team-agent/src/cli/send/presentation.rs +60 -41
- package/crates/team-agent/src/cli/send/resolve.rs +25 -18
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +53 -109
- package/crates/team-agent/src/cli/tests/base.rs +189 -82
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
- package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
- package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
- package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
- package/crates/team-agent/src/cli/types.rs +26 -121
- package/crates/team-agent/src/coordinator/health.rs +3 -283
- package/crates/team-agent/src/coordinator/mod.rs +0 -5
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
- package/crates/team-agent/src/coordinator/types.rs +1 -21
- package/crates/team-agent/src/db/message_store.rs +0 -42
- package/crates/team-agent/src/db/migration.rs +49 -9
- package/crates/team-agent/src/db/schema.rs +27 -20
- package/crates/team-agent/src/leader/owner_bind.rs +0 -24
- package/crates/team-agent/src/leader/registry.rs +3 -3
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +0 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
- package/crates/team-agent/src/messaging/mod.rs +0 -2
- package/crates/team-agent/src/messaging/results.rs +0 -58
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
- package/crates/team-agent/src/state/repository.rs +1 -5
- package/package.json +4 -4
- package/templates/team_state.md +1 -1
- package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
- package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
- package/crates/team-agent/src/messaging/peers.rs +0 -34
|
@@ -37,10 +37,17 @@ pub enum CliError {
|
|
|
37
37
|
/// 委派的 runtime/lifecycle/state 错误(对应 Python `TeamAgentError`)。message == `str(exc)`。
|
|
38
38
|
#[error("{0}")]
|
|
39
39
|
Runtime(String),
|
|
40
|
-
/// argparse 风格用法错误 / 互斥违反(如 `--summary` + `--json`、`--fix` 缺 `--gate
|
|
41
|
-
///
|
|
40
|
+
/// argparse 风格用法错误 / 互斥违反(如 `--summary` + `--json`、`--fix` 缺 `--gate`)。
|
|
41
|
+
/// 对应 Python 抛 `TeamAgentError` 或 parser.error。
|
|
42
42
|
#[error("usage error: {0}")]
|
|
43
43
|
Usage(String),
|
|
44
|
+
/// Preserve the original refusal for audit while carrying safe human guidance.
|
|
45
|
+
#[error("usage error: {audit}")]
|
|
46
|
+
AddressRefusal {
|
|
47
|
+
audit: String,
|
|
48
|
+
error: String,
|
|
49
|
+
action: String,
|
|
50
|
+
},
|
|
44
51
|
/// state 解析失败(歧义/未找到 team 等)。透传 step 5。
|
|
45
52
|
#[error("{0}")]
|
|
46
53
|
State(#[from] crate::state::StateError),
|
|
@@ -76,7 +83,12 @@ impl CliError {
|
|
|
76
83
|
let mut payload = CliErrorPayload {
|
|
77
84
|
ok: false,
|
|
78
85
|
error: error.clone(),
|
|
79
|
-
action:
|
|
86
|
+
action: if super::spec::command_spec(command).is_some() {
|
|
87
|
+
"先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
|
|
88
|
+
} else {
|
|
89
|
+
"run `team-agent doctor` or inspect the log path shown here"
|
|
90
|
+
}
|
|
91
|
+
.to_string(),
|
|
80
92
|
log: log_path.to_string_lossy().to_string(),
|
|
81
93
|
reason: None,
|
|
82
94
|
session_name: None,
|
|
@@ -87,30 +99,27 @@ impl CliError {
|
|
|
87
99
|
payload.session_name = Some(session.clone());
|
|
88
100
|
if command == "quick-start" {
|
|
89
101
|
payload.action = format!(
|
|
90
|
-
"
|
|
102
|
+
"终端会话 `{session}` 已存在,可能是你的已有队伍。恢复用 team-agent restart;只有明确同意丢弃旧对话才用 --allow-fresh。另建队伍请修改 TEAM.md 的 name 后再 quick-start;不要关闭已有队伍凑成功。"
|
|
91
103
|
);
|
|
92
104
|
payload.next_actions = Some(vec![
|
|
93
|
-
"
|
|
94
|
-
"
|
|
105
|
+
"如果是自己的已有队伍,使用 team-agent restart 恢复。".to_string(),
|
|
106
|
+
"如要另建队伍,修改 TEAM.md 的 name 后再运行 team-agent quick-start。"
|
|
107
|
+
.to_string(),
|
|
95
108
|
]);
|
|
96
109
|
} else {
|
|
97
110
|
payload.action = format!(
|
|
98
|
-
"
|
|
111
|
+
"终端会话 `{session}` 已存在,可能属于运行中的队伍。不要关闭它;请使用另一个队伍名称后再启动。"
|
|
99
112
|
);
|
|
100
|
-
payload.next_actions = Some(vec![
|
|
101
|
-
"Use a different team name or runtime.session_name before starting again."
|
|
102
|
-
.to_string(),
|
|
103
|
-
]);
|
|
113
|
+
payload.next_actions = Some(vec!["请使用另一个队伍名称后再启动。".to_string()]);
|
|
104
114
|
}
|
|
105
115
|
} else if error.contains("Team Agent launcher flag")
|
|
106
116
|
&& error.contains("must appear before --")
|
|
107
117
|
{
|
|
108
|
-
payload.action =
|
|
109
|
-
"
|
|
110
|
-
);
|
|
118
|
+
payload.action =
|
|
119
|
+
String::from("将主控启动选项放在 -- 前面;-- 后面只能是工具自己的参数。");
|
|
111
120
|
} else if error.contains("managed launcher refuses a different ambient tmux server") {
|
|
112
121
|
payload.action = String::from(
|
|
113
|
-
"
|
|
122
|
+
"先退出当前 tmux 客户端再重试;只有明确需要嵌套连接时才在 -- 前加 --allow-nested-attach。",
|
|
114
123
|
);
|
|
115
124
|
}
|
|
116
125
|
payload
|
|
@@ -118,7 +127,6 @@ impl CliError {
|
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
/// CLI 进程退出码(`main`:`result.ok is False` 或异常 → `SystemExit(1)`,否则 0)。
|
|
121
|
-
/// `watch` 路径直接 `SystemExit(0)`(`cmd_watch`)。
|
|
122
130
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
123
131
|
pub enum ExitCode {
|
|
124
132
|
Ok,
|
|
@@ -144,14 +152,14 @@ impl ExitCode {
|
|
|
144
152
|
/// Python `cmd_*` 返回 `dict | str | None`:
|
|
145
153
|
/// - `Json(Value)` ← 委派 runtime 的稳定 `{ok, ...}` dict(`--json` 序列化 sort_keys+indent=2)。
|
|
146
154
|
/// - `Human(String)` ← 人读渲染(`format_status`/`cmd_advanced`/五行 summary/comms boundary text)。
|
|
147
|
-
/// - `None` ← passthrough
|
|
155
|
+
/// - `None` ← passthrough 命令(`cmd_codex`/`cmd_claude`,直接 SystemExit,无 emit)。
|
|
148
156
|
#[derive(Debug, Clone, PartialEq)]
|
|
149
157
|
pub enum CmdOutput {
|
|
150
158
|
/// 机器可读稳定 dict(`--json` 路径或 `result.get("ok") is False` 路径)。
|
|
151
159
|
Json(Value),
|
|
152
160
|
/// 人读字符串(`emit` 非 dict 分支直接 print)。
|
|
153
161
|
Human(String),
|
|
154
|
-
/// 无输出(passthrough
|
|
162
|
+
/// 无输出(passthrough;不经 `emit`)。
|
|
155
163
|
None,
|
|
156
164
|
}
|
|
157
165
|
|
|
@@ -184,19 +192,6 @@ impl CmdResult {
|
|
|
184
192
|
preserve_json_order: false,
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
|
-
pub fn from_ordered_json(value: Value) -> Self {
|
|
188
|
-
let exit = if value.get("ok").and_then(Value::as_bool) == Some(false) {
|
|
189
|
-
ExitCode::Error
|
|
190
|
-
} else {
|
|
191
|
-
ExitCode::Ok
|
|
192
|
-
};
|
|
193
|
-
Self {
|
|
194
|
-
output: CmdOutput::Json(value),
|
|
195
|
-
exit,
|
|
196
|
-
as_json: true,
|
|
197
|
-
preserve_json_order: true,
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
195
|
pub fn human(text: impl Into<String>) -> Self {
|
|
201
196
|
Self {
|
|
202
197
|
output: CmdOutput::Human(text.into()),
|
|
@@ -348,18 +343,6 @@ pub struct SendArgs {
|
|
|
348
343
|
pub to_leader: Option<String>,
|
|
349
344
|
}
|
|
350
345
|
|
|
351
|
-
/// `allow-peer-talk`(`parser.py`): allow direct peer communication between two agents.
|
|
352
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
353
|
-
pub struct AllowPeerTalkArgs {
|
|
354
|
-
pub a: String,
|
|
355
|
-
pub b: String,
|
|
356
|
-
pub workspace: PathBuf,
|
|
357
|
-
pub json: bool,
|
|
358
|
-
/// Parsed but rejected until peer allowlist storage grows a team-scoped
|
|
359
|
-
/// backend column.
|
|
360
|
-
pub team: Option<String>,
|
|
361
|
-
}
|
|
362
|
-
|
|
363
346
|
/// `status`(`parser.py:182`)。`--summary`/`--json`/`--detail` 三态(summary xor json,见 `cmd_status`)。
|
|
364
347
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
365
348
|
pub struct StatusArgs {
|
|
@@ -373,13 +356,6 @@ pub struct StatusArgs {
|
|
|
373
356
|
pub team: Option<String>,
|
|
374
357
|
}
|
|
375
358
|
|
|
376
|
-
/// `watch`(`parser.py:190`)。无 `--json`(纯 stream 到 SystemExit)。
|
|
377
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
378
|
-
pub struct WatchArgs {
|
|
379
|
-
pub workspace: PathBuf,
|
|
380
|
-
pub team: Option<String>,
|
|
381
|
-
}
|
|
382
|
-
|
|
383
359
|
/// `approvals`(`parser.py:195`)。
|
|
384
360
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
385
361
|
pub struct ApprovalsArgs {
|
|
@@ -443,14 +419,6 @@ pub struct AttachAppServerLeaderArgs {
|
|
|
443
419
|
pub json: bool,
|
|
444
420
|
}
|
|
445
421
|
|
|
446
|
-
/// `identity`(`parser.py:256`)。
|
|
447
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
448
|
-
pub struct IdentityArgs {
|
|
449
|
-
pub workspace: PathBuf,
|
|
450
|
-
pub team: Option<String>,
|
|
451
|
-
pub json: bool,
|
|
452
|
-
}
|
|
453
|
-
|
|
454
422
|
/// `shutdown`(`parser.py:355`)。`--keep-logs` 默认 true。
|
|
455
423
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
456
424
|
pub struct ShutdownArgs {
|
|
@@ -575,15 +543,6 @@ pub enum DoctorGate {
|
|
|
575
543
|
Unknown(String),
|
|
576
544
|
}
|
|
577
545
|
|
|
578
|
-
/// `sessions`(`parser.py:230`)。
|
|
579
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
580
|
-
pub struct SessionsArgs {
|
|
581
|
-
pub workspace: PathBuf,
|
|
582
|
-
pub json: bool,
|
|
583
|
-
/// Stage 4: explicit `--team` scope (read-only enumeration).
|
|
584
|
-
pub team: Option<String>,
|
|
585
|
-
}
|
|
586
|
-
|
|
587
546
|
/// E7 (0.5.9 host-leader-registry-design §4.1): `team-agent leaders`
|
|
588
547
|
/// enumerates the host discovery index and classifies each entry as
|
|
589
548
|
/// LIVE, STALE, or AMBIGUOUS after re-validating against canonical
|
|
@@ -612,13 +571,6 @@ pub struct ModelsArgs {
|
|
|
612
571
|
pub json: bool,
|
|
613
572
|
}
|
|
614
573
|
|
|
615
|
-
/// `validate [spec=team.spec.yaml] --json`(`parser.py:120`)。
|
|
616
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
617
|
-
pub struct ValidateArgs {
|
|
618
|
-
pub spec: PathBuf,
|
|
619
|
-
pub json: bool,
|
|
620
|
-
}
|
|
621
|
-
|
|
622
574
|
/// `profile {init,doctor,show}`(`parser.py:131`)。
|
|
623
575
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
624
576
|
pub struct ProfileArgs {
|
|
@@ -631,56 +583,9 @@ pub struct ProfileArgs {
|
|
|
631
583
|
pub json: bool,
|
|
632
584
|
}
|
|
633
585
|
|
|
634
|
-
/// Read-only lookup of all stored result envelopes for one case.
|
|
635
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
636
|
-
pub struct ResultsArgs {
|
|
637
|
-
pub case_id: String,
|
|
638
|
-
pub workspace: PathBuf,
|
|
639
|
-
pub team: Option<String>,
|
|
640
|
-
pub json: bool,
|
|
641
|
-
}
|
|
642
|
-
|
|
643
586
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
644
587
|
pub struct WaitArgs {
|
|
645
588
|
pub task_id: String,
|
|
646
589
|
pub workspace: PathBuf,
|
|
647
590
|
pub json: bool,
|
|
648
591
|
}
|
|
649
|
-
|
|
650
|
-
/// `preflight`(`parser.py:160`)。
|
|
651
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
652
|
-
pub struct PreflightArgs {
|
|
653
|
-
pub team: PathBuf,
|
|
654
|
-
pub json: bool,
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/// `wait-ready`(`parser.py:171`)。
|
|
658
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
659
|
-
pub struct WaitReadyArgs {
|
|
660
|
-
pub workspace: PathBuf,
|
|
661
|
-
pub timeout: f64,
|
|
662
|
-
pub json: bool,
|
|
663
|
-
/// Stage 4: explicit `--team` scope (read-only readiness probe).
|
|
664
|
-
pub team: Option<String>,
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/// `e2e`(`parser.py:449`)。
|
|
668
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
669
|
-
pub struct E2eArgs {
|
|
670
|
-
pub workspace: PathBuf,
|
|
671
|
-
pub providers: Vec<String>,
|
|
672
|
-
pub real: bool,
|
|
673
|
-
pub json: bool,
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/// `peek`(`parser.py:201`)。
|
|
677
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
678
|
-
pub struct PeekArgs {
|
|
679
|
-
pub agent: String,
|
|
680
|
-
pub workspace: PathBuf,
|
|
681
|
-
pub tail: usize,
|
|
682
|
-
pub head: Option<usize>,
|
|
683
|
-
pub search: Option<String>,
|
|
684
|
-
pub allow_raw_screen: bool,
|
|
685
|
-
pub json: bool,
|
|
686
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//! ---
|
|
2
|
-
//! purpose: coordinator daemon 的健康判定、幂等启停与只读观测面——pid/metadata/schema 三合一健康、spawn
|
|
2
|
+
//! purpose: coordinator daemon 的健康判定、幂等启停与只读观测面——pid/metadata/schema 三合一健康、spawn 与终止与 runtime 路径
|
|
3
3
|
//! contract:
|
|
4
4
|
//! provides:
|
|
5
5
|
//! - name: coordinator_health
|
|
@@ -10,12 +10,6 @@
|
|
|
10
10
|
//! what: 同上,并把 team_key 以 --team 传给子进程,免得 daemon 自己从 state 推
|
|
11
11
|
//! - name: stop_coordinator
|
|
12
12
|
//! what: 终止 daemon 并清 pid/meta;pid 文件缺失时用 ps 扫描发现流浪 coordinator
|
|
13
|
-
//! - name: collect_watch_lines
|
|
14
|
-
//! what: 从 events.jsonl 与结果表增量取出可渲染行,并推进 WatchCursor
|
|
15
|
-
//! - name: render_event_line
|
|
16
|
-
//! what: 把一条结构化事件渲染成人类可读行,不认识的事件返回 None
|
|
17
|
-
//! - name: run_watch
|
|
18
|
-
//! what: team-agent watch 主循环:反复 collect 后输出并 sleep
|
|
19
13
|
//! - name: coordinator_pid_path
|
|
20
14
|
//! what: coordinator.pid 的位置
|
|
21
15
|
//! - name: coordinator_meta_path
|
|
@@ -35,29 +29,24 @@
|
|
|
35
29
|
//! - 不做 tick 编排,也不投递任何消息
|
|
36
30
|
//! - 不读 provider 凭据、不碰 .env;身份只取自当前可执行文件与已落盘 metadata
|
|
37
31
|
//! - 终止进程限定本 workspace:优先按本次判定拿到的精确 pid;pid 文件缺失时的流浪回收会按 ps 命令行匹配「coordinator --workspace <本 ws>」发现目标,仍不做跨 workspace 的 pkill/killall 泛清
|
|
38
|
-
//! - watch 侧只读:不重放已归档段,rotation 只插一条 marker 并重置 offset
|
|
39
32
|
//! maturity: wired
|
|
40
33
|
//! ---
|
|
41
34
|
//!
|
|
42
|
-
//! coordinator 健康/身份 & 只读可观测面:metadata 身份原语 + coordinator
|
|
35
|
+
//! coordinator 健康/身份 & 只读可观测面:metadata 身份原语 + coordinator 路径。
|
|
43
36
|
|
|
44
|
-
use std::io::{Read, Seek, SeekFrom};
|
|
45
37
|
#[cfg(unix)]
|
|
46
38
|
use std::os::unix::process::CommandExt;
|
|
47
39
|
use std::path::{Path, PathBuf};
|
|
48
40
|
use std::process::{Command, Stdio};
|
|
49
41
|
use std::time::Duration;
|
|
50
42
|
|
|
51
|
-
use serde_json::Value;
|
|
52
|
-
use thiserror::Error;
|
|
53
|
-
|
|
54
43
|
use crate::message_store::MessageStore;
|
|
55
44
|
|
|
56
45
|
use super::types::{
|
|
57
46
|
CoordinatorBinaryIdentity, CoordinatorBinaryIdentityRelation, CoordinatorHealthStatus,
|
|
58
47
|
CoordinatorMetadata, CoordinatorMetadataMismatchReason, HealthReport, MetadataSource, Pid,
|
|
59
48
|
SchemaError, SchemaHealth, StartError, StartOutcome, StartReport, StopError, StopOutcome,
|
|
60
|
-
StopReport,
|
|
49
|
+
StopReport, WorkspacePath, PROTOCOL_VERSION,
|
|
61
50
|
};
|
|
62
51
|
|
|
63
52
|
// ===========================================================================
|
|
@@ -1157,275 +1146,6 @@ pub fn coordinator_log_path(workspace: &WorkspacePath) -> PathBuf {
|
|
|
1157
1146
|
crate::model::paths::runtime_dir(workspace.as_path()).join("coordinator.log")
|
|
1158
1147
|
}
|
|
1159
1148
|
|
|
1160
|
-
// ===========================================================================
|
|
1161
|
-
// watch 实时流(watch/__init__.py)—— `team-agent watch`
|
|
1162
|
-
// ===========================================================================
|
|
1163
|
-
|
|
1164
|
-
/// `collect_watch_lines`(`watch.py:40`)。tail events.jsonl(过滤 team)+ latest_results,
|
|
1165
|
-
/// 渲染人类可读行;处理 log rotation(ROTATION_MARKER + offset 重置,不重放历史段)。
|
|
1166
|
-
/// 推进 `cursor`。
|
|
1167
|
-
/// ---
|
|
1168
|
-
/// purpose: 增量取出自上次游标以来的可渲染 watch 行(事件 + 结果两路)
|
|
1169
|
-
/// params:
|
|
1170
|
-
/// workspace: workspace 根
|
|
1171
|
-
/// cursor: 可变游标,函数会推进 offset、已见结果 id 集合与归档签名
|
|
1172
|
-
/// store: 已打开的 message store,用于取结果行
|
|
1173
|
-
/// team: 只看这个 team 的事件;None 表示不过滤
|
|
1174
|
-
/// returns: 本次新增的渲染行,事件行在前、结果行在后;无新内容时为空 Vec
|
|
1175
|
-
/// errors: 读事件文件或查库失败时返回 WatchError
|
|
1176
|
-
/// ---
|
|
1177
|
-
pub fn collect_watch_lines(
|
|
1178
|
-
workspace: &WorkspacePath,
|
|
1179
|
-
cursor: &mut WatchCursor,
|
|
1180
|
-
store: &MessageStore,
|
|
1181
|
-
team: Option<&str>,
|
|
1182
|
-
) -> Result<Vec<String>, WatchError> {
|
|
1183
|
-
let mut lines = collect_event_lines(workspace, cursor, team)?;
|
|
1184
|
-
lines.extend(collect_result_lines(workspace, cursor, store, team)?);
|
|
1185
|
-
Ok(lines)
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
/// `_collect_event_lines`(`watch.py:66-97`):tail events.jsonl,按 team 过滤。
|
|
1189
|
-
fn collect_event_lines(
|
|
1190
|
-
workspace: &WorkspacePath,
|
|
1191
|
-
cursor: &mut WatchCursor,
|
|
1192
|
-
team: Option<&str>,
|
|
1193
|
-
) -> Result<Vec<String>, WatchError> {
|
|
1194
|
-
let logs = crate::model::paths::logs_dir(workspace.as_path());
|
|
1195
|
-
let events_path = logs.join("events.jsonl");
|
|
1196
|
-
let archive_path = logs.join("events.jsonl.1");
|
|
1197
|
-
let archive_signature = file_signature(&archive_path)?;
|
|
1198
|
-
let mut lines = Vec::new();
|
|
1199
|
-
|
|
1200
|
-
let size = std::fs::metadata(&events_path)
|
|
1201
|
-
.map(|m| m.len())
|
|
1202
|
-
.unwrap_or(0);
|
|
1203
|
-
let rotated = cursor.initialized
|
|
1204
|
-
&& (cursor.archive_signature != archive_signature || cursor.event_offset > size);
|
|
1205
|
-
if rotated {
|
|
1206
|
-
lines.push(ROTATION_MARKER.to_string());
|
|
1207
|
-
cursor.event_offset = 0;
|
|
1208
|
-
}
|
|
1209
|
-
cursor.archive_signature = archive_signature;
|
|
1210
|
-
|
|
1211
|
-
let mut file = match std::fs::File::open(&events_path) {
|
|
1212
|
-
Ok(file) => file,
|
|
1213
|
-
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
|
1214
|
-
cursor.initialized = true;
|
|
1215
|
-
return Ok(lines);
|
|
1216
|
-
}
|
|
1217
|
-
Err(e) => return Err(WatchError::Io(e)),
|
|
1218
|
-
};
|
|
1219
|
-
file.seek(SeekFrom::Start(cursor.event_offset))?;
|
|
1220
|
-
let mut text = String::new();
|
|
1221
|
-
file.read_to_string(&mut text)?;
|
|
1222
|
-
cursor.event_offset = file.stream_position()?;
|
|
1223
|
-
cursor.initialized = true;
|
|
1224
|
-
for line in text.lines() {
|
|
1225
|
-
if let Ok(event) = serde_json::from_str::<Value>(line) {
|
|
1226
|
-
// watch.py:91 — `if team and _event_team_id(event) != team: continue`.
|
|
1227
|
-
if team.is_some() && event_team_id(&event).as_deref() != team {
|
|
1228
|
-
continue;
|
|
1229
|
-
}
|
|
1230
|
-
if let Some(rendered) = render_event_line(&event) {
|
|
1231
|
-
lines.push(rendered);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
Ok(lines)
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
/// `_event_team_id`(`watch.py:132-134`)。
|
|
1239
|
-
fn event_team_id(event: &Value) -> Option<String> {
|
|
1240
|
-
["team_id", "owner_team_id", "team"]
|
|
1241
|
-
.iter()
|
|
1242
|
-
.find_map(|key| event.get(*key))
|
|
1243
|
-
.and_then(|value| match value {
|
|
1244
|
-
Value::String(s) if !s.is_empty() => Some(s.clone()),
|
|
1245
|
-
Value::Number(n) => Some(n.to_string()),
|
|
1246
|
-
_ => None,
|
|
1247
|
-
})
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
/// `_collect_result_lines`(`watch.py:100-112`):store.latest_results(owner_team_id=team)
|
|
1251
|
-
/// 出 `result_received: {agent} -> {summary}` 行;按 cursor.seen_result_ids 去重。
|
|
1252
|
-
fn collect_result_lines(
|
|
1253
|
-
workspace: &WorkspacePath,
|
|
1254
|
-
cursor: &mut WatchCursor,
|
|
1255
|
-
store: &MessageStore,
|
|
1256
|
-
team: Option<&str>,
|
|
1257
|
-
) -> Result<Vec<String>, WatchError> {
|
|
1258
|
-
let db_path = crate::model::paths::runtime_dir(workspace.as_path()).join("team.db");
|
|
1259
|
-
if !db_path.exists() {
|
|
1260
|
-
return Ok(Vec::new());
|
|
1261
|
-
}
|
|
1262
|
-
let mut lines = Vec::new();
|
|
1263
|
-
for row in store.latest_results(20, team)? {
|
|
1264
|
-
let Some(result_id) = row
|
|
1265
|
-
.get("result_id")
|
|
1266
|
-
.and_then(Value::as_str)
|
|
1267
|
-
.filter(|id| !id.is_empty())
|
|
1268
|
-
.map(str::to_string)
|
|
1269
|
-
else {
|
|
1270
|
-
continue;
|
|
1271
|
-
};
|
|
1272
|
-
if !cursor.seen_result_ids.insert(result_id) {
|
|
1273
|
-
continue;
|
|
1274
|
-
}
|
|
1275
|
-
let mut summary = crate::message_store::result_summary_from_row(&row)
|
|
1276
|
-
.unwrap_or_else(|| serde_json::json!({}));
|
|
1277
|
-
if let Some(obj) = summary.as_object_mut() {
|
|
1278
|
-
obj.insert(
|
|
1279
|
-
"event".to_string(),
|
|
1280
|
-
Value::String("result_received".to_string()),
|
|
1281
|
-
);
|
|
1282
|
-
}
|
|
1283
|
-
if let Some(rendered) = render_event_line(&summary) {
|
|
1284
|
-
lines.push(rendered);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
Ok(lines)
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
/// `render_event_line`(`watch.py:46-63`)。把一条 step 4 事件渲染成人类可读行;非可渲染事件 → `None`。
|
|
1291
|
-
/// 消费的事件类型:`result_received` / `leader_receiver.{injected,submitted}` / `send.failed` /
|
|
1292
|
-
/// `leader_receiver.rebind_required` / `leader.api_error`(card 表)。
|
|
1293
|
-
/// ---
|
|
1294
|
-
/// purpose: 把一条结构化事件渲染成一行人类可读文本
|
|
1295
|
-
/// params:
|
|
1296
|
-
/// event: 事件 JSON 对象;靠其中的 event 字段分派
|
|
1297
|
-
/// returns: 已知事件类型返回渲染行,其余一律 None(不猜、不打印原始 JSON)。摘要字段做长度截断
|
|
1298
|
-
/// ---
|
|
1299
|
-
pub fn render_event_line(event: &Value) -> Option<String> {
|
|
1300
|
-
let event_name = event.get("event").and_then(Value::as_str)?;
|
|
1301
|
-
match event_name {
|
|
1302
|
-
"result_received" => Some(format!(
|
|
1303
|
-
"result_received: {} -> {}",
|
|
1304
|
-
clean_field(event, &["agent_id"], "-"),
|
|
1305
|
-
prefix_chars(&clean_field(event, &["summary"], "-"), 80)
|
|
1306
|
-
)),
|
|
1307
|
-
"leader_receiver.injected" | "leader_receiver.submitted" => {
|
|
1308
|
-
let id = first_field(event, &["message_id", "msg_id"]).unwrap_or("-");
|
|
1309
|
-
let id = prefix_chars(id, 12);
|
|
1310
|
-
Some(format!(
|
|
1311
|
-
"leader_receiver.injected: {} -> {}",
|
|
1312
|
-
id,
|
|
1313
|
-
clean_field(event, &["recipient", "to"], "-")
|
|
1314
|
-
))
|
|
1315
|
-
}
|
|
1316
|
-
"send.failed" => Some(format!(
|
|
1317
|
-
"send.failed: {} reason={}",
|
|
1318
|
-
clean_field(event, &["recipient", "to", "target"], "-"),
|
|
1319
|
-
clean_field(event, &["reason", "error"], "-")
|
|
1320
|
-
)),
|
|
1321
|
-
"leader_receiver.rebind_required" => Some(format!(
|
|
1322
|
-
"leader_receiver.rebind_required: pane={} reason={}",
|
|
1323
|
-
clean_field(event, &["old_pane_id", "pane_id", "target"], "-"),
|
|
1324
|
-
clean_field(event, &["reason", "rediscovery_status"], "-")
|
|
1325
|
-
)),
|
|
1326
|
-
"leader.api_error" => Some(format!(
|
|
1327
|
-
"leader.api_error: {} provider={} snippet={}",
|
|
1328
|
-
clean_field(event, &["error_class"], "Unknown"),
|
|
1329
|
-
clean_field(event, &["provider"], "-"),
|
|
1330
|
-
clean_field(event, &["matched_pattern_snippet", "snippet"], "-")
|
|
1331
|
-
)),
|
|
1332
|
-
"result_wake.registered" => Some(format!(
|
|
1333
|
-
"result_wake.registered: task={} watcher={}",
|
|
1334
|
-
clean_field(event, &["task_id"], "-"),
|
|
1335
|
-
clean_field(event, &["watcher_id"], "-")
|
|
1336
|
-
)),
|
|
1337
|
-
"result_wake.notified" => Some(format!(
|
|
1338
|
-
"result_wake.notified: task={} result={} watcher={}",
|
|
1339
|
-
clean_field(event, &["task_id"], "-"),
|
|
1340
|
-
clean_field(event, &["result_id"], "-"),
|
|
1341
|
-
clean_field(event, &["watcher_id"], "-")
|
|
1342
|
-
)),
|
|
1343
|
-
"result_wake.notify_failed" => Some(format!(
|
|
1344
|
-
"result_wake.notify_failed: task={} watcher={} reason={}",
|
|
1345
|
-
clean_field(event, &["task_id"], "-"),
|
|
1346
|
-
clean_field(event, &["watcher_id"], "-"),
|
|
1347
|
-
clean_field(event, &["reason", "error"], "-")
|
|
1348
|
-
)),
|
|
1349
|
-
_ => None,
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
/// `run_watch`(`watch.py:25`)。`team-agent watch` 主循环:反复 `collect_watch_lines` + 输出 + sleep。
|
|
1354
|
-
/// `output`/`sleep` 注入便于测试。§10 返 Result。
|
|
1355
|
-
/// ---
|
|
1356
|
-
/// purpose: team-agent watch 的主循环:反复增量收集、输出、休眠
|
|
1357
|
-
/// params:
|
|
1358
|
-
/// workspace: workspace 根
|
|
1359
|
-
/// team: 只看这个 team;None 表示不过滤
|
|
1360
|
-
/// interval_sec: 轮询间隔;非有限值或非正数时回落到内置默认
|
|
1361
|
-
/// output: 输出回调,注入以便测试;本函数自己不写 stdout
|
|
1362
|
-
/// returns: 循环结束时为 Ok。这是个长跑循环,正常运行期间不返回
|
|
1363
|
-
/// errors: 打开 message store 或某轮收集失败时返回 WatchError
|
|
1364
|
-
/// ---
|
|
1365
|
-
pub fn run_watch(
|
|
1366
|
-
workspace: &WorkspacePath,
|
|
1367
|
-
team: Option<&str>,
|
|
1368
|
-
interval_sec: f64,
|
|
1369
|
-
output: &mut dyn FnMut(&str),
|
|
1370
|
-
) -> Result<(), WatchError> {
|
|
1371
|
-
let store = MessageStore::open(workspace.as_path())?;
|
|
1372
|
-
let mut cursor = WatchCursor::default();
|
|
1373
|
-
let interval = if interval_sec.is_finite() && interval_sec > 0.0 {
|
|
1374
|
-
std::time::Duration::from_secs_f64(interval_sec)
|
|
1375
|
-
} else {
|
|
1376
|
-
std::time::Duration::from_millis(100)
|
|
1377
|
-
};
|
|
1378
|
-
loop {
|
|
1379
|
-
for line in collect_watch_lines(workspace, &mut cursor, &store, team)? {
|
|
1380
|
-
output(&line);
|
|
1381
|
-
}
|
|
1382
|
-
std::thread::sleep(interval);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
/// watch 错误(读 events.jsonl / latest_results)。
|
|
1387
|
-
#[derive(Debug, Error)]
|
|
1388
|
-
pub enum WatchError {
|
|
1389
|
-
#[error("io: {0}")]
|
|
1390
|
-
Io(#[from] std::io::Error),
|
|
1391
|
-
#[error("message store: {0}")]
|
|
1392
|
-
MessageStore(#[from] crate::message_store::MessageStoreError),
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
fn file_signature(path: &Path) -> Result<Option<(u64, i128)>, WatchError> {
|
|
1396
|
-
let meta = match std::fs::metadata(path) {
|
|
1397
|
-
Ok(meta) => meta,
|
|
1398
|
-
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
|
1399
|
-
Err(e) => return Err(WatchError::Io(e)),
|
|
1400
|
-
};
|
|
1401
|
-
let modified = meta.modified().ok();
|
|
1402
|
-
let nanos = modified
|
|
1403
|
-
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
|
1404
|
-
.and_then(|d| i128::try_from(d.as_nanos()).ok())
|
|
1405
|
-
.unwrap_or(0);
|
|
1406
|
-
Ok(Some((meta.len(), nanos)))
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
fn first_field<'a>(event: &'a Value, keys: &[&str]) -> Option<&'a str> {
|
|
1410
|
-
keys.iter()
|
|
1411
|
-
.find_map(|key| event.get(*key).and_then(Value::as_str))
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
fn clean_field(event: &Value, keys: &[&str], default: &str) -> String {
|
|
1415
|
-
first_field(event, keys)
|
|
1416
|
-
.map(clean_text)
|
|
1417
|
-
.filter(|s| !s.is_empty())
|
|
1418
|
-
.unwrap_or_else(|| default.to_string())
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
fn clean_text(text: &str) -> String {
|
|
1422
|
-
text.split_whitespace().collect::<Vec<_>>().join(" ")
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
fn prefix_chars(text: &str, max: usize) -> String {
|
|
1426
|
-
text.chars().take(max).collect()
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
1149
|
#[cfg(all(test, unix))]
|
|
1430
1150
|
mod tests {
|
|
1431
1151
|
use super::*;
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
//! what: 幂等启动 daemon 子进程
|
|
11
11
|
//! - name: stop_coordinator
|
|
12
12
|
//! what: 停 daemon 并清 pid/meta
|
|
13
|
-
//! - name: run_watch
|
|
14
|
-
//! what: team-agent watch 的只读事件流主循环
|
|
15
13
|
//! - name: detect_whole_team_gone
|
|
16
14
|
//! what: 不依赖 coordinator 存活的整队消失判定
|
|
17
15
|
//! depends:
|
|
@@ -41,7 +39,6 @@
|
|
|
41
39
|
//! - `coordinator/lifecycle.py` (health / start / stop / tick orchestration + schema health)
|
|
42
40
|
//! - `coordinator/metadata.py` (COORDINATOR_PROTOCOL_VERSION=2, pid_is_running, read/write/ok meta)
|
|
43
41
|
//! - `coordinator/paths.py` (coordinator.pid / coordinator.json / coordinator.log paths)
|
|
44
|
-
//! - `watch/__init__.py` (run_watch / collect_watch_lines / render_event_line / WatchCursor)
|
|
45
42
|
//! - `abnormal_track.py` (Gap 32 §4 provider-neutral whole-team-gone detection)
|
|
46
43
|
//!
|
|
47
44
|
//! 职责(card §职责):per-workspace daemon 生命周期 + 单次 tick 编排。tick 按固定顺序把
|
|
@@ -70,8 +67,6 @@
|
|
|
70
67
|
//! 仅 unexpected exit 写 durable marker + 延迟到下条 leader 命令再 escalate。
|
|
71
68
|
//! - **abnormal track 不读屏/不命名 provider**:只消费结构化 fault fact + 进程身份;
|
|
72
69
|
//! `(signature, turn_id)` 去重,turn_id 缺失退化为 per-record fingerprint 桶。
|
|
73
|
-
//! - **watch rotation**:archive_signature 变化或 offset>size 即插 ROTATION_MARKER 并重置 offset,
|
|
74
|
-
//! 不重放历史段。
|
|
75
70
|
//!
|
|
76
71
|
//! ROUND-0:仅类型 + struct/enum + fn/trait/method 签名。所有 body =
|
|
77
72
|
//! `unimplemented!("step12 port: <what>")`。fallible 路径返 `Result`/`Option`(§10 实现层禁
|
|
@@ -18,15 +18,6 @@ fn default_tick_interval_is_five_seconds() {
|
|
|
18
18
|
assert_eq!(BACKOFF_MAX_SEC, 60.0);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
#[test]
|
|
22
|
-
fn rotation_marker_is_byte_exact() {
|
|
23
|
-
// watch.py:22 — byte-for-byte (note the U+2014 em dash).
|
|
24
|
-
assert_eq!(
|
|
25
|
-
ROTATION_MARKER,
|
|
26
|
-
"[watch] log rotated; archived segment events.jsonl.1 not replayed — historical replay deferred to a future --replay flag"
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
21
|
#[test]
|
|
31
22
|
fn status_enums_serialize_to_exact_python_strings() {
|
|
32
23
|
let cases: &[(String, &str)] = &[
|
|
@@ -235,20 +235,3 @@ fn p2_pid_is_running_false_for_cross_user_pid() {
|
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
-
|
|
239
|
-
// P1 — render_event_line(result_received) truncates the summary to 80 chars
|
|
240
|
-
// (watch.py:115-116 `_clean(summary)[:80]`).
|
|
241
|
-
#[test]
|
|
242
|
-
fn p2_render_result_received_truncates_summary_to_80_chars() {
|
|
243
|
-
let long = "x".repeat(200);
|
|
244
|
-
let e = serde_json::json!({"event":"result_received","agent_id":"w1","summary": long});
|
|
245
|
-
let line = render_event_line(&e).expect("renders");
|
|
246
|
-
assert!(
|
|
247
|
-
line.contains(&"x".repeat(80)),
|
|
248
|
-
"first 80 summary chars are kept"
|
|
249
|
-
);
|
|
250
|
-
assert!(
|
|
251
|
-
!line.contains(&"x".repeat(81)),
|
|
252
|
-
"summary must be truncated to 80 chars"
|
|
253
|
-
);
|
|
254
|
-
}
|