@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.
Files changed (54) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +231 -812
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +493 -451
  6. package/crates/team-agent/src/cli/leader.rs +4 -9
  7. package/crates/team-agent/src/cli/leaders.rs +90 -29
  8. package/crates/team-agent/src/cli/mod.rs +348 -169
  9. package/crates/team-agent/src/cli/models.rs +80 -24
  10. package/crates/team-agent/src/cli/named_address.rs +114 -25
  11. package/crates/team-agent/src/cli/route.rs +10 -5
  12. package/crates/team-agent/src/cli/send/presentation.rs +60 -41
  13. package/crates/team-agent/src/cli/send/resolve.rs +25 -18
  14. package/crates/team-agent/src/cli/send.rs +4 -4
  15. package/crates/team-agent/src/cli/spec.rs +53 -109
  16. package/crates/team-agent/src/cli/tests/base.rs +189 -82
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
  21. package/crates/team-agent/src/cli/tests/mod.rs +0 -2
  22. package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
  23. package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
  24. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
  25. package/crates/team-agent/src/cli/types.rs +26 -121
  26. package/crates/team-agent/src/coordinator/health.rs +3 -283
  27. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  28. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  29. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  30. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  31. package/crates/team-agent/src/coordinator/types.rs +1 -21
  32. package/crates/team-agent/src/db/message_store.rs +0 -42
  33. package/crates/team-agent/src/db/migration.rs +49 -9
  34. package/crates/team-agent/src/db/schema.rs +27 -20
  35. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  36. package/crates/team-agent/src/leader/registry.rs +3 -3
  37. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  38. package/crates/team-agent/src/leader/tests/identity.rs +0 -32
  39. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  40. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
  41. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  42. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
  43. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  44. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  45. package/crates/team-agent/src/messaging/mod.rs +0 -2
  46. package/crates/team-agent/src/messaging/results.rs +0 -58
  47. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  48. package/crates/team-agent/src/state/repository.rs +1 -5
  49. package/package.json +4 -4
  50. package/templates/team_state.md +1 -1
  51. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  52. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  53. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  54. 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
- /// `peek` 缺 `--allow-raw-screen`)。对应 Python 抛 `TeamAgentError` 或 parser.error。
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: "run `team-agent doctor` or inspect the log path shown here".to_string(),
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
- "tmux session `{session}` already exists. It may be your own existing team. To resume it use `team-agent restart`. If recovery is impossible, use `team-agent restart --allow-fresh` only after explicit context-loss consent. Only if you want a separate team, change `name:` in TEAM.md and run quick-start again. Never terminate existing tmux sessions from quick-start."
102
+ "终端会话 `{session}` 已存在,可能是你的已有队伍。恢复用 team-agent restart;只有明确同意丢弃旧对话才用 --allow-fresh。另建队伍请修改 TEAM.md 的 name 后再 quick-start;不要关闭已有队伍凑成功。"
91
103
  );
92
104
  payload.next_actions = Some(vec![
93
- "If this is your existing team, resume it with `team-agent restart`.".to_string(),
94
- "If you want a separate team, change `name:` in TEAM.md and run `team-agent quick-start` again.".to_string(),
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
- "tmux session `{session}` already exists. It may be an active team. Do not terminate existing tmux sessions from startup; use a different team name or runtime.session_name and start again."
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 = String::from(
109
- "move the Team Agent launcher flag before `--`; only provider flags belong after `--`",
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
- "leave the current tmux client and retry, or pass `--allow-nested-attach` before `--` to opt in to a nested managed attach",
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/watch 命令(`cmd_codex`/`cmd_claude`/`cmd_watch`,直接 SystemExit,无 emit)。
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/watch;不经 `emit`)。
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 与终止、runtime 路径、以及 team-agent watch 的事件渲染
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 路径 + watch 实时流。
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, WatchCursor, WorkspacePath, PROTOCOL_VERSION, ROTATION_MARKER,
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)] = &[
@@ -293,4 +293,3 @@ mod main_preserved;
293
293
  mod spine;
294
294
  mod takeover;
295
295
  mod tick_core;
296
- mod watch;
@@ -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
- }