@team-agent/installer 0.5.111 → 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 (38) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -743
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +31 -275
  6. package/crates/team-agent/src/cli/mod.rs +2 -9
  7. package/crates/team-agent/src/cli/spec.rs +0 -3
  8. package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
  9. package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
  10. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
  11. package/crates/team-agent/src/cli/tests/mod.rs +0 -2
  12. package/crates/team-agent/src/cli/types.rs +4 -108
  13. package/crates/team-agent/src/coordinator/health.rs +3 -283
  14. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  15. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  16. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  17. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  18. package/crates/team-agent/src/coordinator/types.rs +1 -21
  19. package/crates/team-agent/src/db/message_store.rs +0 -42
  20. package/crates/team-agent/src/db/migration.rs +49 -9
  21. package/crates/team-agent/src/db/schema.rs +27 -20
  22. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  23. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  24. package/crates/team-agent/src/leader/tests/identity.rs +0 -32
  25. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  26. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  27. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  28. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  29. package/crates/team-agent/src/messaging/mod.rs +0 -2
  30. package/crates/team-agent/src/messaging/results.rs +0 -58
  31. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  32. package/crates/team-agent/src/state/repository.rs +1 -5
  33. package/package.json +4 -4
  34. package/templates/team_state.md +1 -1
  35. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  36. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  37. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  38. package/crates/team-agent/src/messaging/peers.rs +0 -34
@@ -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
- }
@@ -20,9 +20,8 @@
20
20
  //! ---
21
21
  //!
22
22
  //! coordinator 共享数据面:常量 / newtype / 穷尽 enum / metadata / schema health /
23
- //! report 结构 / abnormal-track 数据型 / WatchCursor / cross-dep 占位。
23
+ //! report 结构 / abnormal-track 数据型 / cross-dep 占位。
24
24
 
25
- use std::collections::BTreeSet;
26
25
  use std::path::{Path, PathBuf};
27
26
 
28
27
  use serde::{Deserialize, Serialize};
@@ -49,10 +48,6 @@ pub const DEFAULT_TICK_INTERVAL_SEC: f64 = 5.0;
49
48
  /// 指数退避封顶(`__main__.py:65` `min(.., 60.0)`)。bug-084 崩溃循环防护。
50
49
  pub const BACKOFF_MAX_SEC: f64 = 60.0;
51
50
 
52
- /// watch log rotation marker(`watch.py:22`,字节级一致 —— 测试钉死)。
53
- pub const ROTATION_MARKER: &str =
54
- "[watch] log rotated; archived segment events.jsonl.1 not replayed — historical replay deferred to a future --replay flag";
55
-
56
51
  // ===========================================================================
57
52
  // NEWTYPES(§3:Pid / WorkspacePath 不与裸 int/PathBuf 混传)
58
53
  // ===========================================================================
@@ -445,21 +440,6 @@ pub struct TeamPresenceSnapshot {
445
440
  pub restart_in_progress: bool,
446
441
  }
447
442
 
448
- // ===========================================================================
449
- // WatchCursor(watch/__init__.py)
450
- // ===========================================================================
451
-
452
- /// watch tail 游标(`watch.py:14-19`)。rotation 检测靠 `archive_signature` 变化 + `offset > size`
453
- /// (`watch.py:73-83`)。`archive_signature = Some((size, mtime_ns))`。
454
- #[derive(Debug, Clone, Default, PartialEq, Eq)]
455
- pub struct WatchCursor {
456
- pub event_offset: u64,
457
- pub seen_result_ids: BTreeSet<String>,
458
- pub initialized: bool,
459
- /// `(size, mtime_ns)`;`mtime_ns: i128`(防溢出)。
460
- pub archive_signature: Option<(u64, i128)>,
461
- }
462
-
463
443
  // ===========================================================================
464
444
  // CROSS-DEP placeholders(其他并行 lane 拥有真名;leader 集成时 reconcile)
465
445
  // ===========================================================================
@@ -830,22 +830,6 @@ impl MessageStore {
830
830
  Ok(rows.into_iter().rev().collect())
831
831
  }
832
832
 
833
- /// Allow direct peer messages in both directions. Golden stores `(a,b)` and
834
- /// `(b,a)` so either sender/recipient lookup can use a single ordered key.
835
- pub fn allow_peer(&self, a: &str, b: &str) -> Result<(), MessageStoreError> {
836
- let conn = crate::db::schema::open_db(&self.path)?;
837
- let now = now_ts();
838
- conn.execute(
839
- "insert or ignore into peer_allowlist(a, b, created_at) values (?1, ?2, ?3)",
840
- params![a, b, now.as_str()],
841
- )?;
842
- conn.execute(
843
- "insert or ignore into peer_allowlist(a, b, created_at) values (?1, ?2, ?3)",
844
- params![b, a, now.as_str()],
845
- )?;
846
- Ok(())
847
- }
848
-
849
833
  /// `claim_leader_notification_delivery` (`leader_notification_log.py:30-101`):
850
834
  /// `INSERT OR IGNORE` on PK `(result_id, owner_team_id, owner_epoch)`. rowcount==1
851
835
  /// → `claimed_by_you`; else read the existing winner row → `already_notified_by`.
@@ -1421,32 +1405,6 @@ mod tests {
1421
1405
  assert_eq!(legacy_epoch_from_uuid(Some("uuid-BBB")), 122688376);
1422
1406
  }
1423
1407
 
1424
- #[test]
1425
- fn allow_peer_inserts_bidirectional_rows_idempotently() {
1426
- let s = store();
1427
- s.allow_peer("alice", "bob").unwrap();
1428
- s.allow_peer("alice", "bob").unwrap();
1429
-
1430
- let c = read(&s);
1431
- let mut rows = c
1432
- .prepare("select a, b from peer_allowlist order by a, b")
1433
- .unwrap()
1434
- .query_map([], |row| {
1435
- Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1436
- })
1437
- .unwrap()
1438
- .collect::<Result<Vec<_>, _>>()
1439
- .unwrap();
1440
- rows.sort();
1441
- assert_eq!(
1442
- rows,
1443
- vec![
1444
- ("alice".to_string(), "bob".to_string()),
1445
- ("bob".to_string(), "alice".to_string()),
1446
- ]
1447
- );
1448
- }
1449
-
1450
1408
  // ════════════════════════ FIX-LOOP (wave-1) RED test ════════════════════════
1451
1409
  // B1: mark() error column = coalesce(?, error) — a NEW error overwrites, but a
1452
1410
  // mark WITHOUT an error must PRESERVE the existing error (current impl clobbers
@@ -88,7 +88,6 @@ pub const MANAGED_TABLE_LAYOUTS: &[(&str, &[&str])] = &[
88
88
  "updated_at",
89
89
  ],
90
90
  ),
91
- ("peer_allowlist", &["a", "b", "created_at"]),
92
91
  (
93
92
  "result_watchers",
94
93
  &[
@@ -129,7 +128,6 @@ const CREATE_TABLE_TEMPLATES: &[(&str, &str)] = &[
129
128
  ("scheduled_events", "create table if not exists __TABLE__ (\n id integer primary key,\n owner_team_id text,\n due_at text not null,\n target text not null,\n kind text not null,\n payload_json text not null,\n status text not null,\n created_at text not null,\n fired_at text,\n result_json text\n )"),
130
129
  ("delivery_tokens", "create table if not exists __TABLE__ (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )"),
131
130
  ("agent_health", "create table if not exists __TABLE__ (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )"),
132
- ("peer_allowlist", "create table if not exists __TABLE__ (\n a text not null,\n b text not null,\n created_at text not null,\n primary key (a, b)\n )"),
133
131
  ("result_watchers", "create table if not exists __TABLE__ (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n recipient text,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )"),
134
132
  ("leader_notification_log", "create table if not exists __TABLE__ (\n result_id text not null,\n owner_team_id text not null default '',\n owner_epoch integer not null default 0,\n leader_session_uuid text,\n notified_message_id text not null,\n notified_at text not null,\n leader_pane_id_at_notify text,\n envelope_content_hash text,\n primary key (result_id, owner_team_id, owner_epoch)\n )"),
135
133
  ];
@@ -630,12 +628,12 @@ mod tests {
630
628
  let path = temp_db();
631
629
  build_legacy(&path);
632
630
 
633
- // 迁移前:diagnosis = schema_repair_available,全 8 表 diff(4 漂移 + 4 缺失)。
631
+ // 迁移前:diagnosis = schema_repair_available,全 7 表 diff(4 漂移 + 3 缺失)。
634
632
  let before = schema_diagnosis(&path, SCHEMA_VERSION).unwrap();
635
633
  assert_eq!(before.status, "schema_repair_available");
636
634
  assert!(!before.ok);
637
635
  assert_eq!(before.user_version, 1);
638
- assert_eq!(before.layout_diffs.len(), 8);
636
+ assert_eq!(before.layout_diffs.len(), MANAGED_TABLE_LAYOUTS.len());
639
637
 
640
638
  // initialize_schema 走 ensure_table_layout(rebuild)+ 建表 + user_version=3。
641
639
  let conn = crate::db::schema::open_db(&path).unwrap();
@@ -697,6 +695,48 @@ mod tests {
697
695
  assert_eq!(d.user_version, SCHEMA_VERSION);
698
696
  }
699
697
 
698
+ // A drifted legacy peer table is inert: no diagnosis, rebuild, backup or row loss.
699
+ #[test]
700
+ fn legacy_peer_allowlist_is_inert_and_preserved() {
701
+ let path = temp_db();
702
+ let conn = crate::db::schema::open_db(&path).unwrap();
703
+ initialize_schema(&conn, Some(&path)).unwrap();
704
+ conn.execute_batch(
705
+ "create table peer_allowlist (created_at text not null, b text not null, a text not null, primary key (a, b));
706
+ insert into peer_allowlist(a, b, created_at) values ('worker', 'reviewer', 'v4');",
707
+ )
708
+ .unwrap();
709
+ assert!(layout_diffs(&conn).unwrap().is_empty());
710
+ initialize_schema(&conn, Some(&path)).unwrap();
711
+ assert_eq!(layout(&conn, "peer_allowlist"), ["created_at", "b", "a"]);
712
+ let row: (String, String, String) = conn
713
+ .query_row("select a, b, created_at from peer_allowlist", [], |r| {
714
+ Ok((r.get(0)?, r.get(1)?, r.get(2)?))
715
+ })
716
+ .unwrap();
717
+ assert_eq!(
718
+ row,
719
+ (
720
+ "worker".to_string(),
721
+ "reviewer".to_string(),
722
+ "v4".to_string()
723
+ )
724
+ );
725
+ drop(conn);
726
+ let diagnosis = schema_diagnosis(&path, SCHEMA_VERSION).unwrap();
727
+ assert!(
728
+ diagnosis.ok,
729
+ "legacy peer table must remain inert: {diagnosis:?}"
730
+ );
731
+ assert!(
732
+ !std::fs::read_dir(path.parent().unwrap())
733
+ .unwrap()
734
+ .filter_map(|entry| entry.ok())
735
+ .any(|entry| entry.file_name().to_string_lossy().contains("pre-migration")),
736
+ "legacy peer table must not trigger a migration backup"
737
+ );
738
+ }
739
+
700
740
  #[test]
701
741
  fn diagnosis_missing_db() {
702
742
  let path = temp_db(); // 未创建文件
@@ -713,7 +753,7 @@ mod tests {
713
753
  build_legacy(&path);
714
754
  let conn = crate::db::schema::open_db(&path).unwrap();
715
755
  let events1 = ensure_table_layout(&conn, SCHEMA_VERSION, Some(&path)).unwrap();
716
- assert_eq!(events1.len(), 8); // 4 漂移 rebuild + 4 缺失建表
756
+ assert_eq!(events1.len(), MANAGED_TABLE_LAYOUTS.len()); // 4 漂移 rebuild + 3 缺失建表
717
757
  for e in &events1 {
718
758
  assert_eq!(e.row_count_before, e.row_count_after);
719
759
  }
@@ -759,7 +799,7 @@ mod tests {
759
799
  build_legacy(&path);
760
800
  let conn = crate::db::schema::open_db(&path).unwrap();
761
801
  let events = ensure_table_layout(&conn, SCHEMA_VERSION, None).unwrap(); // None 不再 Err
762
- assert_eq!(events.len(), 8);
802
+ assert_eq!(events.len(), MANAGED_TABLE_LAYOUTS.len());
763
803
  assert_eq!(
764
804
  table_layout(&conn, "messages").unwrap()[..2],
765
805
  ["message_id", "owner_team_id"]
@@ -871,7 +911,7 @@ mod tests {
871
911
  } => {
872
912
  assert!(diagnosis.ok);
873
913
  assert_eq!(diagnosis.user_version, SCHEMA_VERSION);
874
- assert_eq!(rebuilds.len(), 8);
914
+ assert_eq!(rebuilds.len(), MANAGED_TABLE_LAYOUTS.len());
875
915
  }
876
916
  other => panic!("expected Fixed, got {other:?}"),
877
917
  }
@@ -921,8 +961,8 @@ mod tests {
921
961
  .collect();
922
962
  assert_eq!(
923
963
  rebuilds.len(),
924
- 8,
925
- "8 张 managed 表各一条 schema.layout_rebuild"
964
+ MANAGED_TABLE_LAYOUTS.len(),
965
+ "每张 managed 表各一条 schema.layout_rebuild"
926
966
  );
927
967
  for e in &rebuilds {
928
968
  assert_eq!(