@team-agent/installer 0.5.111 → 0.5.113
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 +93 -779
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +158 -373
- package/crates/team-agent/src/cli/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +8 -15
- package/crates/team-agent/src/cli/named_address.rs +17 -16
- package/crates/team-agent/src/cli/route.rs +4 -4
- package/crates/team-agent/src/cli/send/presentation.rs +19 -18
- package/crates/team-agent/src/cli/send/resolve.rs +11 -11
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +29 -32
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/tests/base.rs +6 -6
- package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- 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 +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +12 -116
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +0 -9
- 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/start.rs +5 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +21 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
- package/crates/team-agent/src/lifecycle/launch.rs +2 -2
- package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- 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 -2
- 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/provider/adapter.rs +8 -59
- package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
- package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
- package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
- package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
- package/crates/team-agent/src/provider/command_helpers.rs +45 -0
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +21 -28
- package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
- package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
- 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
|
@@ -7,6 +7,18 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
|
|
7
7
|
|
|
8
8
|
static COUNTER: AtomicU32 = AtomicU32::new(0);
|
|
9
9
|
|
|
10
|
+
#[test]
|
|
11
|
+
fn cursor_effort_is_rejected_by_shared_admission_for_every_level() {
|
|
12
|
+
for effort in ["low", "medium", "high", "xhigh", "max", "ultra"] {
|
|
13
|
+
let role = write_tmp("cursor.md", &format!("---\nprovider: cursor_agent\neffort: {effort}\n---\nRole\n"));
|
|
14
|
+
let error = compile_role_agent(&role, &Value::Map(Vec::new()), "/workspace")
|
|
15
|
+
.err()
|
|
16
|
+
.expect("Cursor must reject effort before command construction");
|
|
17
|
+
assert!(error.to_string().contains("cursor_agent does not support effort; the Cursor CLI has no --effort flag"));
|
|
18
|
+
fs::remove_dir_all(role.parent().unwrap()).unwrap();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
fn unique_base() -> PathBuf {
|
|
11
23
|
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
12
24
|
std::env::temp_dir().join(format!("ta_rs_compile_{}_{}", std::process::id(), n))
|
|
@@ -590,15 +590,6 @@ fn compile_role_agent_with_mode(
|
|
|
590
590
|
effort.as_str()
|
|
591
591
|
)));
|
|
592
592
|
}
|
|
593
|
-
if provider_enum == Provider::CursorAgent {
|
|
594
|
-
return Err(ModelError::Validation(format!(
|
|
595
|
-
"{}: cursor_agent does not support effort '{}'. \
|
|
596
|
-
The Cursor CLI has no `--effort` flag; the framework must not drop the field and still launch. \
|
|
597
|
-
Remove effort from the role file (do not map it into `--model[effort=]` until that form is measured).",
|
|
598
|
-
role_path.display(),
|
|
599
|
-
effort.as_str()
|
|
600
|
-
)));
|
|
601
|
-
}
|
|
602
593
|
agent_items.push(("effort", Value::Str(effort.as_str().to_string())));
|
|
603
594
|
}
|
|
604
595
|
Ok(CompiledRole {
|
|
@@ -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
|
-
}
|
|
@@ -20,9 +20,8 @@
|
|
|
20
20
|
//! ---
|
|
21
21
|
//!
|
|
22
22
|
//! coordinator 共享数据面:常量 / newtype / 穷尽 enum / metadata / schema health /
|
|
23
|
-
//! report 结构 / abnormal-track 数据型 /
|
|
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,全
|
|
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(),
|
|
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(),
|
|
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(),
|
|
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(),
|
|
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
|
-
|
|
925
|
-
"
|
|
964
|
+
MANAGED_TABLE_LAYOUTS.len(),
|
|
965
|
+
"每张 managed 表各一条 schema.layout_rebuild"
|
|
926
966
|
);
|
|
927
967
|
for e in &rebuilds {
|
|
928
968
|
assert_eq!(
|