@team-agent/installer 0.5.50 → 0.5.51
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 +5 -3
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +32 -14
- package/crates/team-agent/src/cli/named_address.rs +11 -48
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +44 -1169
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
- package/crates/team-agent/src/mcp_server/tools.rs +14 -5
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +10 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +101 -49
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
use crate::cli::CliError;
|
|
2
|
+
use serde_json::{json, Value};
|
|
3
|
+
|
|
4
|
+
/// `format_status` agent 分支(`queries.py:135-162`)。
|
|
5
|
+
pub(super) fn format_agent_status(
|
|
6
|
+
status: &Value,
|
|
7
|
+
agent_id: &str,
|
|
8
|
+
inbox_rows: &[Value],
|
|
9
|
+
) -> Result<String, CliError> {
|
|
10
|
+
ensure_agent_known(status, agent_id)?;
|
|
11
|
+
let agents = status.get("agents").and_then(Value::as_object);
|
|
12
|
+
let health = status.get("agent_health").and_then(Value::as_object);
|
|
13
|
+
let empty = json!({});
|
|
14
|
+
let agent = agents.and_then(|map| map.get(agent_id)).unwrap_or(&empty);
|
|
15
|
+
let row = health.and_then(|map| map.get(agent_id)).unwrap_or(&empty);
|
|
16
|
+
let status_text = row
|
|
17
|
+
.get("status")
|
|
18
|
+
.and_then(Value::as_str)
|
|
19
|
+
.map(str::to_string)
|
|
20
|
+
.unwrap_or_else(|| {
|
|
21
|
+
agent_health_status_text(agent.get("status").and_then(Value::as_str).unwrap_or(""))
|
|
22
|
+
});
|
|
23
|
+
let tasks = status
|
|
24
|
+
.get("tasks")
|
|
25
|
+
.and_then(Value::as_array)
|
|
26
|
+
.cloned()
|
|
27
|
+
.unwrap_or_default();
|
|
28
|
+
let task_id = current_task_for_agent(&tasks, agent_id).unwrap_or_else(|| "-".to_string());
|
|
29
|
+
let mut lines = vec![
|
|
30
|
+
format!("{agent_id} {status_text}"),
|
|
31
|
+
format!(" provider: {}", py_get(agent, "provider")),
|
|
32
|
+
format!(" model: {}", py_get(agent, "model")),
|
|
33
|
+
format!(" profile: {}", py_get(agent, "profile")),
|
|
34
|
+
format!(" session_id: {}", py_get_or_dash(agent, "session_id")),
|
|
35
|
+
format!(" captured_via: {}", py_get_or_dash(agent, "captured_via")),
|
|
36
|
+
format!(
|
|
37
|
+
" attribution_confidence: {}",
|
|
38
|
+
py_get_or_dash(agent, "attribution_confidence")
|
|
39
|
+
),
|
|
40
|
+
format!(" task: {task_id}"),
|
|
41
|
+
format!(" handoff: {}", py_get(agent, "handoff_path")),
|
|
42
|
+
" recent messages:".to_string(),
|
|
43
|
+
];
|
|
44
|
+
if inbox_rows.is_empty() {
|
|
45
|
+
lines.push(" none".to_string());
|
|
46
|
+
} else {
|
|
47
|
+
for item in inbox_rows {
|
|
48
|
+
let content = item.get("content").and_then(Value::as_str).unwrap_or("");
|
|
49
|
+
let content: String = content.chars().take(120).collect();
|
|
50
|
+
lines.push(format!(
|
|
51
|
+
" {} {} -> {} {}: {content}",
|
|
52
|
+
py_get_or_dash(item, "created_at"),
|
|
53
|
+
py_get_or_dash(item, "sender"),
|
|
54
|
+
py_get_or_dash(item, "recipient"),
|
|
55
|
+
py_get_or_dash(item, "status"),
|
|
56
|
+
));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
Ok(lines.join("\n"))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub(super) fn ensure_agent_known(status: &Value, agent_id: &str) -> Result<(), CliError> {
|
|
63
|
+
let agents = status.get("agents").and_then(Value::as_object);
|
|
64
|
+
let health = status.get("agent_health").and_then(Value::as_object);
|
|
65
|
+
let known = agents.is_some_and(|map| map.contains_key(agent_id))
|
|
66
|
+
|| health.is_some_and(|map| map.contains_key(agent_id));
|
|
67
|
+
if !known {
|
|
68
|
+
return Err(CliError::Runtime(format!("unknown agent id: {agent_id}")));
|
|
69
|
+
}
|
|
70
|
+
Ok(())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// `current_task_for_agent`(`approvals/status.py:127-132`)。
|
|
74
|
+
pub(super) fn current_task_for_agent(tasks: &[Value], agent_id: &str) -> Option<String> {
|
|
75
|
+
const ACTIVE: [&str; 5] = ["pending", "ready", "running", "blocked", "needs_retry"];
|
|
76
|
+
for task in tasks.iter().rev() {
|
|
77
|
+
let assignee = task.get("assignee").and_then(Value::as_str);
|
|
78
|
+
let status = task
|
|
79
|
+
.get("status")
|
|
80
|
+
.and_then(Value::as_str)
|
|
81
|
+
.unwrap_or("pending");
|
|
82
|
+
if assignee == Some(agent_id) && ACTIVE.contains(&status) {
|
|
83
|
+
return task.get("id").and_then(Value::as_str).map(str::to_string);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
None
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub(super) fn agent_health_status_text(status: &str) -> String {
|
|
90
|
+
serde_json::to_value(crate::provider::agent_health_status(status))
|
|
91
|
+
.ok()
|
|
92
|
+
.and_then(|v| v.as_str().map(str::to_string))
|
|
93
|
+
.unwrap_or_else(|| "-".to_string())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Python `agent.get(key, '-')`:键缺失 → `-`;键存在但为 null → 打印 `None`。
|
|
97
|
+
pub(super) fn py_get(agent: &Value, key: &str) -> String {
|
|
98
|
+
match agent.get(key) {
|
|
99
|
+
None => "-".to_string(),
|
|
100
|
+
Some(Value::Null) => "None".to_string(),
|
|
101
|
+
Some(Value::String(s)) => s.clone(),
|
|
102
|
+
Some(other) => other.to_string(),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Python `agent.get(key) or '-'`:缺失/null/空串都落 `-`。
|
|
107
|
+
pub(super) fn py_get_or_dash(agent: &Value, key: &str) -> String {
|
|
108
|
+
match agent.get(key) {
|
|
109
|
+
Some(Value::String(s)) if !s.is_empty() => s.clone(),
|
|
110
|
+
Some(Value::Number(n)) => n.to_string(),
|
|
111
|
+
_ => "-".to_string(),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// `status.approvals(workspace, agent_id)`(JSON)/`format_approvals`(人读)。
|
|
116
|
+
pub fn format_approvals(value: &Value) -> String {
|
|
117
|
+
let approvals = value
|
|
118
|
+
.get("approvals")
|
|
119
|
+
.and_then(Value::as_array)
|
|
120
|
+
.map(Vec::as_slice)
|
|
121
|
+
.unwrap_or(&[]);
|
|
122
|
+
if approvals.is_empty() {
|
|
123
|
+
return "No pending approvals.".to_string();
|
|
124
|
+
}
|
|
125
|
+
approvals
|
|
126
|
+
.iter()
|
|
127
|
+
.map(|approval| {
|
|
128
|
+
let agent = approval
|
|
129
|
+
.get("agent_id")
|
|
130
|
+
.and_then(Value::as_str)
|
|
131
|
+
.unwrap_or("-");
|
|
132
|
+
let kind = approval
|
|
133
|
+
.get("kind")
|
|
134
|
+
.and_then(Value::as_str)
|
|
135
|
+
.unwrap_or("unknown");
|
|
136
|
+
let prompt = approval
|
|
137
|
+
.get("prompt")
|
|
138
|
+
.and_then(Value::as_str)
|
|
139
|
+
.or_else(|| approval.get("subject").and_then(Value::as_str))
|
|
140
|
+
.unwrap_or("-");
|
|
141
|
+
format!("{agent}: {kind} {prompt}")
|
|
142
|
+
})
|
|
143
|
+
.collect::<Vec<_>>()
|
|
144
|
+
.join("\n")
|
|
145
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
pub fn inbox(
|
|
4
|
+
workspace: &Path,
|
|
5
|
+
agent: &str,
|
|
6
|
+
limit: usize,
|
|
7
|
+
since: Option<&str>,
|
|
8
|
+
as_json: bool,
|
|
9
|
+
owner_team_id: Option<&str>,
|
|
10
|
+
) -> Result<Value, CliError> {
|
|
11
|
+
let _ = as_json;
|
|
12
|
+
let store = crate::message_store::MessageStore::open(workspace)
|
|
13
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
14
|
+
let mut messages = store
|
|
15
|
+
.inbox(agent, limit, owner_team_id)
|
|
16
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
17
|
+
if let Some(cutoff) = since.and_then(parse_rfc3339) {
|
|
18
|
+
messages.retain(|message| {
|
|
19
|
+
message
|
|
20
|
+
.get("created_at")
|
|
21
|
+
.and_then(Value::as_str)
|
|
22
|
+
.and_then(parse_rfc3339)
|
|
23
|
+
.is_some_and(|created| created >= cutoff)
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
Ok(json!({
|
|
27
|
+
"ok": true,
|
|
28
|
+
"agent_id": agent,
|
|
29
|
+
"messages": messages,
|
|
30
|
+
"since": since,
|
|
31
|
+
}))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub(super) fn parse_rfc3339(value: &str) -> Option<chrono::DateTime<chrono::FixedOffset>> {
|
|
35
|
+
chrono::DateTime::parse_from_rfc3339(value).ok()
|
|
36
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
/// 0.5.41 Slice 3 (fault-invisibility-locate.md §5): single-source
|
|
4
|
+
/// runtime freshness projection consumed by status/diagnose. Read-
|
|
5
|
+
/// only over existing sources (`coordinator_health`, heartbeat
|
|
6
|
+
/// sidecar, watch state); computes NO transport calls of its own.
|
|
7
|
+
#[derive(Default, Clone)]
|
|
8
|
+
pub(crate) struct RuntimeFreshness {
|
|
9
|
+
pub coordinator_service_available: bool,
|
|
10
|
+
pub host_boot_stale: bool,
|
|
11
|
+
pub host_boot_recorded: Option<String>,
|
|
12
|
+
pub host_boot_current: Option<String>,
|
|
13
|
+
pub provider_exited_agents: std::collections::BTreeSet<String>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl RuntimeFreshness {
|
|
17
|
+
pub(super) fn host_boot_stale_reason(&self) -> Option<&'static str> {
|
|
18
|
+
self.host_boot_stale.then_some("host_boot_mismatch")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub(crate) fn compute_runtime_freshness(
|
|
23
|
+
workspace: &Path,
|
|
24
|
+
state: &Value,
|
|
25
|
+
health: &crate::coordinator::HealthReport,
|
|
26
|
+
) -> RuntimeFreshness {
|
|
27
|
+
let workspace_path = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
28
|
+
let heartbeat = crate::coordinator::read_coordinator_heartbeat(&workspace_path);
|
|
29
|
+
let host_boot_recorded = heartbeat
|
|
30
|
+
.as_ref()
|
|
31
|
+
.and_then(|hb| hb.get("host_boot_id"))
|
|
32
|
+
.and_then(Value::as_str)
|
|
33
|
+
.filter(|s| !s.is_empty() && *s != "unknown")
|
|
34
|
+
.map(str::to_string);
|
|
35
|
+
let host_boot_current = crate::coordinator::probe_host_boot_id();
|
|
36
|
+
let host_boot_stale = match (host_boot_recorded.as_deref(), host_boot_current.as_deref()) {
|
|
37
|
+
(Some(recorded), Some(current)) => recorded != current,
|
|
38
|
+
_ => false,
|
|
39
|
+
};
|
|
40
|
+
// Collect worker-provider-exited agents from the coordinator
|
|
41
|
+
// abnormal_exit_watch payload. Only the typed positive provider-exit
|
|
42
|
+
// marker participates here: generic process/pane death diagnostics are
|
|
43
|
+
// not proof that the provider exited. Read from top-level
|
|
44
|
+
// `coordinator.abnormal_exit_watch`
|
|
45
|
+
// OR the team-scoped mirror `teams.<key>.coordinator...`.
|
|
46
|
+
let mut provider_exited_agents = std::collections::BTreeSet::new();
|
|
47
|
+
for path in [
|
|
48
|
+
"/coordinator/abnormal_exit_watch",
|
|
49
|
+
&format!(
|
|
50
|
+
"/teams/{}/coordinator/abnormal_exit_watch",
|
|
51
|
+
state
|
|
52
|
+
.get("active_team_key")
|
|
53
|
+
.and_then(Value::as_str)
|
|
54
|
+
.unwrap_or("")
|
|
55
|
+
),
|
|
56
|
+
] {
|
|
57
|
+
if let Some(watch) = state.pointer(path).and_then(Value::as_object) {
|
|
58
|
+
for (agent_id, entry) in watch {
|
|
59
|
+
let exited =
|
|
60
|
+
entry.get("worker_provider_exited").and_then(Value::as_bool) == Some(true);
|
|
61
|
+
if exited {
|
|
62
|
+
provider_exited_agents.insert(agent_id.clone());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
RuntimeFreshness {
|
|
68
|
+
coordinator_service_available: health.service_available,
|
|
69
|
+
host_boot_stale,
|
|
70
|
+
host_boot_recorded,
|
|
71
|
+
host_boot_current,
|
|
72
|
+
provider_exited_agents,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub(super) fn build_runtime_status_block(
|
|
77
|
+
coordinator_running: bool,
|
|
78
|
+
undelivered: i64,
|
|
79
|
+
tmux_session_missing: bool,
|
|
80
|
+
freshness: &RuntimeFreshness,
|
|
81
|
+
) -> Value {
|
|
82
|
+
let mut runtime = serde_json::Map::new();
|
|
83
|
+
runtime.insert(
|
|
84
|
+
"coordinator".to_string(),
|
|
85
|
+
json!({"ok": coordinator_running}),
|
|
86
|
+
);
|
|
87
|
+
runtime.insert("undelivered".to_string(), json!(undelivered));
|
|
88
|
+
// 0.5.41 Slice 3 (fault-invisibility-locate.md §5/§6.3): expose
|
|
89
|
+
// typed issues on the runtime block so `status --json --detail`
|
|
90
|
+
// can be scanned for `runtime_bindings_stale_after_boot` the
|
|
91
|
+
// same way `diagnose` surfaces it. Hint precedence: session-
|
|
92
|
+
// missing > host-boot-stale > coordinator-not-running-with-
|
|
93
|
+
// backlog — most-actionable-first.
|
|
94
|
+
let mut issues: Vec<Value> = Vec::new();
|
|
95
|
+
if freshness.host_boot_stale {
|
|
96
|
+
issues.push(json!({
|
|
97
|
+
"id": "runtime_bindings_stale_after_boot",
|
|
98
|
+
"recorded_host_boot_id": freshness.host_boot_recorded,
|
|
99
|
+
"current_host_boot_id": freshness.host_boot_current,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
if !issues.is_empty() {
|
|
103
|
+
runtime.insert("issues".to_string(), Value::Array(issues));
|
|
104
|
+
}
|
|
105
|
+
if tmux_session_missing {
|
|
106
|
+
runtime.insert(
|
|
107
|
+
"hint".to_string(),
|
|
108
|
+
json!("tmux session missing — run team-agent restart"),
|
|
109
|
+
);
|
|
110
|
+
} else if freshness.host_boot_stale {
|
|
111
|
+
runtime.insert(
|
|
112
|
+
"hint".to_string(),
|
|
113
|
+
json!("runtime_bindings_stale_after_boot — run team-agent restart"),
|
|
114
|
+
);
|
|
115
|
+
} else if !coordinator_running && undelivered > 0 {
|
|
116
|
+
runtime.insert(
|
|
117
|
+
"hint".to_string(),
|
|
118
|
+
json!(format!(
|
|
119
|
+
"coordinator not running with {undelivered} undelivered — run team-agent restart"
|
|
120
|
+
)),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
Value::Object(runtime)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Whether the coordinator HealthReport reflects a running tick loop. Used by the
|
|
127
|
+
/// runtime block + the hint gate.
|
|
128
|
+
pub(super) fn coordinator_status_running(health: &crate::coordinator::HealthReport) -> bool {
|
|
129
|
+
// 0.5.41 Slice 3 (fault-invisibility-locate.md §5/§6.3): runtime
|
|
130
|
+
// service predicate is `service_available`, not any-`Running` pid.
|
|
131
|
+
// A stale-pid daemon is CoordinatorHealthStatus::Stale (or Running
|
|
132
|
+
// with metadata_ok=false); a service-compatible newer daemon is
|
|
133
|
+
// Running + service_available=true — send/diagnose already use
|
|
134
|
+
// this same truth source, and status must agree.
|
|
135
|
+
health.service_available
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Count of messages currently sitting in delivery-able backlog
|
|
139
|
+
/// (accepted/pending/queued forms — not delivered / not failed / not refused).
|
|
140
|
+
/// owner_team_id scope honored when present.
|
|
141
|
+
pub(super) fn coordinator_health_value(health: crate::coordinator::HealthReport) -> Value {
|
|
142
|
+
let expose_binary_drift = health.service_available && !health.metadata_ok;
|
|
143
|
+
let binary_identity_relation = health.binary_identity_relation.as_str();
|
|
144
|
+
let mut value = json!({
|
|
145
|
+
"ok": health.ok,
|
|
146
|
+
"status": coordinator_status_wire(health.status),
|
|
147
|
+
"pid": health.pid.map(|p| p.get()),
|
|
148
|
+
"metadata": health.metadata.map(|m| json!({
|
|
149
|
+
"pid": m.pid.get(),
|
|
150
|
+
"protocol_version": m.protocol_version,
|
|
151
|
+
"message_store_schema_version": m.message_store_schema_version,
|
|
152
|
+
"binary_path": m.binary_path,
|
|
153
|
+
"binary_version": m.binary_version,
|
|
154
|
+
"source": m.source,
|
|
155
|
+
"updated_at": m.updated_at,
|
|
156
|
+
})),
|
|
157
|
+
"metadata_ok": health.metadata_ok,
|
|
158
|
+
"metadata_mismatch_reason": health.metadata_mismatch_reason,
|
|
159
|
+
"binary_path": health.current_binary_identity.binary_path,
|
|
160
|
+
"binary_version": health.current_binary_identity.binary_version,
|
|
161
|
+
"schema_ok": health.schema.ok,
|
|
162
|
+
"schema_error": health.schema.error.map(|e| format!("{e:?}")),
|
|
163
|
+
"schema": {
|
|
164
|
+
"message_store_schema_version": health.schema.schema_version,
|
|
165
|
+
},
|
|
166
|
+
// 0.5.41 Slice 3 (fault-invisibility-locate.md §5/§6.3):
|
|
167
|
+
// `service_available` is now always exposed alongside `ok`
|
|
168
|
+
// and `status` so status/diagnose consumers can share one
|
|
169
|
+
// service-availability truth without keying on the legacy
|
|
170
|
+
// `expose_binary_drift` conditional (that path only fired
|
|
171
|
+
// for the newer-daemon-preserved shape). `binary_identity_
|
|
172
|
+
// relation` moves to always-on for the same reason — RED3
|
|
173
|
+
// scans the summary for `daemon_newer_than_caller`.
|
|
174
|
+
"service_available": health.service_available,
|
|
175
|
+
"binary_identity_relation": binary_identity_relation,
|
|
176
|
+
});
|
|
177
|
+
if expose_binary_drift {
|
|
178
|
+
if let Some(obj) = value.as_object_mut() {
|
|
179
|
+
obj.insert("wire_metadata_ok".to_string(), Value::Bool(true));
|
|
180
|
+
obj.insert("binary_identity_ok".to_string(), Value::Bool(false));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
value
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
pub(super) fn coordinator_status_wire(
|
|
187
|
+
status: crate::coordinator::CoordinatorHealthStatus,
|
|
188
|
+
) -> &'static str {
|
|
189
|
+
match status {
|
|
190
|
+
crate::coordinator::CoordinatorHealthStatus::Missing => "missing",
|
|
191
|
+
crate::coordinator::CoordinatorHealthStatus::InvalidPid => "invalid_pid",
|
|
192
|
+
crate::coordinator::CoordinatorHealthStatus::Running => "running",
|
|
193
|
+
crate::coordinator::CoordinatorHealthStatus::Stale => "stale",
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
use super::agents::{enrich_agents, tmux_session_present};
|
|
2
|
+
use super::compact::compact_status;
|
|
3
|
+
use super::runtime::{
|
|
4
|
+
build_runtime_status_block, compute_runtime_freshness, coordinator_health_value,
|
|
5
|
+
coordinator_status_running,
|
|
6
|
+
};
|
|
7
|
+
use super::store::{
|
|
8
|
+
agent_health, count_undelivered_backlog, latest_result_summaries, message_counts,
|
|
9
|
+
pending_leader_notifications, queued_messages, result_counts,
|
|
10
|
+
};
|
|
11
|
+
use crate::cli::CliError;
|
|
12
|
+
use crate::state::projection::OwnerTeamResolution;
|
|
13
|
+
use serde_json::{json, Value};
|
|
14
|
+
use std::path::Path;
|
|
15
|
+
|
|
16
|
+
pub(crate) struct RuntimeSnapshot {
|
|
17
|
+
full: Value,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl RuntimeSnapshot {
|
|
21
|
+
pub(crate) fn assemble(
|
|
22
|
+
workspace: &Path,
|
|
23
|
+
state: &Value,
|
|
24
|
+
owner_team_id: Option<&str>,
|
|
25
|
+
) -> Result<Self, CliError> {
|
|
26
|
+
let resolved_owner_team_id = resolve_status_owner_team(workspace, owner_team_id)?;
|
|
27
|
+
let owner_team_id = resolved_owner_team_id.as_deref().or(owner_team_id);
|
|
28
|
+
let health = crate::coordinator::coordinator_health(
|
|
29
|
+
&crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
|
|
30
|
+
);
|
|
31
|
+
let store = crate::message_store::MessageStore::open(workspace)
|
|
32
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
33
|
+
let conn = crate::db::schema::open_db(store.db_path())
|
|
34
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
35
|
+
// B-5 / 036b N38 explicable — status 出口 runtime 块:把 coordinator_health
|
|
36
|
+
// (现状)+ undelivered backlog count 一起暴露;coordinator not running ∧
|
|
37
|
+
// backlog>0 才挂 down-hint(anti-nag)。auto-recovery 不做(user 已裁)。
|
|
38
|
+
let coordinator_running = coordinator_status_running(&health);
|
|
39
|
+
let undelivered_backlog = count_undelivered_backlog(&conn, owner_team_id)?;
|
|
40
|
+
let session_name = state.get("session_name").cloned().unwrap_or(Value::Null);
|
|
41
|
+
let tmux_present = tmux_session_present(workspace, state, session_name.as_str());
|
|
42
|
+
// 0.5.41 Slice 3 (fault-invisibility-locate.md §5/§6.3): resolve
|
|
43
|
+
// RuntimeFreshness once and thread it through the runtime block
|
|
44
|
+
// and per-agent enrichment. This is the single-source read that
|
|
45
|
+
// makes host-boot / coordinator / provider-exit staleness win
|
|
46
|
+
// over cached state.agents and DB agent_health.
|
|
47
|
+
let freshness = compute_runtime_freshness(workspace, state, &health);
|
|
48
|
+
let runtime_block = build_runtime_status_block(
|
|
49
|
+
coordinator_running,
|
|
50
|
+
undelivered_backlog,
|
|
51
|
+
!tmux_present,
|
|
52
|
+
&freshness,
|
|
53
|
+
);
|
|
54
|
+
let agents = enrich_agents(workspace, state, tmux_present, &freshness);
|
|
55
|
+
let tasks = state.get("tasks").cloned().unwrap_or_else(|| json!([]));
|
|
56
|
+
let leader_receiver = state
|
|
57
|
+
.get("leader_receiver")
|
|
58
|
+
.cloned()
|
|
59
|
+
.unwrap_or_else(|| json!({}));
|
|
60
|
+
let is_external_leader = crate::state::projection::state_is_external_leader(state);
|
|
61
|
+
let leader_topology = if is_external_leader {
|
|
62
|
+
"external"
|
|
63
|
+
} else {
|
|
64
|
+
"managed"
|
|
65
|
+
};
|
|
66
|
+
let leader_attach_command = if is_external_leader {
|
|
67
|
+
None
|
|
68
|
+
} else {
|
|
69
|
+
let window_name = state
|
|
70
|
+
.pointer("/leader_receiver/window_name")
|
|
71
|
+
.and_then(Value::as_str)
|
|
72
|
+
.unwrap_or("leader");
|
|
73
|
+
session_name.as_str().and_then(|session| {
|
|
74
|
+
// Bug #7 (gate review §6): build the attach command from the
|
|
75
|
+
// SAME endpoint the readiness probe uses (state's persisted
|
|
76
|
+
// tmux_endpoint/tmux_socket), so the printed command matches
|
|
77
|
+
// where the session actually lives.
|
|
78
|
+
crate::tmux_backend::attach_command_for_runtime_state_or_workspace(
|
|
79
|
+
workspace,
|
|
80
|
+
Some(state),
|
|
81
|
+
&crate::transport::SessionName::new(session.to_string()),
|
|
82
|
+
window_name,
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
};
|
|
86
|
+
let mut readiness_state = state.clone();
|
|
87
|
+
if let Some(obj) = readiness_state.as_object_mut() {
|
|
88
|
+
obj.insert(
|
|
89
|
+
"tmux_session_present".to_string(),
|
|
90
|
+
serde_json::json!(tmux_present),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
let readiness = crate::cli::diagnose::wait_readiness(&readiness_state);
|
|
94
|
+
let full = crate::redaction::redact_external_value(&json!({
|
|
95
|
+
"ok": true,
|
|
96
|
+
"team": state.pointer("/leader/id").cloned().unwrap_or_else(|| json!("leader")),
|
|
97
|
+
"session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
|
|
98
|
+
"leader_topology": leader_topology,
|
|
99
|
+
"is_external_leader": is_external_leader,
|
|
100
|
+
"leader_attach_command": leader_attach_command,
|
|
101
|
+
"leader_client": state.get("leader_client").cloned().unwrap_or(Value::Null),
|
|
102
|
+
"tmux_session_present": tmux_present,
|
|
103
|
+
"all_spawned": readiness.get("all_spawned").cloned().unwrap_or(Value::Bool(false)),
|
|
104
|
+
"all_attached_receiver": readiness.get("all_attached_receiver").cloned().unwrap_or(Value::Bool(true)),
|
|
105
|
+
"all_resumable_have_session": readiness.get("all_resumable_have_session").cloned().unwrap_or(Value::Bool(true)),
|
|
106
|
+
"session_capture_complete": readiness.get("session_capture_complete").cloned().unwrap_or(Value::Bool(true)),
|
|
107
|
+
"session_capture_incomplete": readiness.get("session_capture_incomplete").cloned().unwrap_or(Value::Bool(false)),
|
|
108
|
+
"incomplete_session_capture_agents": readiness.get("incomplete_session_capture_agents").cloned().unwrap_or_else(|| json!([])),
|
|
109
|
+
"pending_session_agent_ids": readiness.get("pending_session_agent_ids").cloned().unwrap_or_else(|| json!([])),
|
|
110
|
+
"leader_receiver": leader_receiver,
|
|
111
|
+
"teams": state.get("teams").cloned().unwrap_or_else(|| json!({})),
|
|
112
|
+
"agents": agents,
|
|
113
|
+
"agent_health": agent_health(&conn, owner_team_id)?,
|
|
114
|
+
"tasks": tasks,
|
|
115
|
+
"messages": message_counts(&conn, owner_team_id)?,
|
|
116
|
+
"queued_messages": queued_messages(&conn, owner_team_id, 8)?,
|
|
117
|
+
"pending_leader_notifications": pending_leader_notifications(&conn, owner_team_id, 8)?,
|
|
118
|
+
"results": result_counts(&conn, owner_team_id)?,
|
|
119
|
+
"latest_results": latest_result_summaries(&store, owner_team_id)?,
|
|
120
|
+
"readiness": readiness,
|
|
121
|
+
"coordinator": coordinator_health_value(health),
|
|
122
|
+
"runtime": runtime_block,
|
|
123
|
+
"reminder": crate::cli::STATUS_REMINDER,
|
|
124
|
+
"last_events": Value::Array(
|
|
125
|
+
crate::event_log::EventLog::new(workspace)
|
|
126
|
+
.tail(10)
|
|
127
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
128
|
+
),
|
|
129
|
+
}));
|
|
130
|
+
Ok(Self { full })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pub(crate) fn full(&self) -> &Value {
|
|
134
|
+
&self.full
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pub(crate) fn into_full(self) -> Value {
|
|
138
|
+
self.full
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
pub(crate) fn compact(&self) -> Value {
|
|
142
|
+
compact_status(self.full.clone())
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pub(super) fn read_runtime_state(workspace: &Path) -> Value {
|
|
147
|
+
crate::state::repository::StateRepository::new(workspace)
|
|
148
|
+
.load_workspace_if_exists_without_migrations()
|
|
149
|
+
.ok()
|
|
150
|
+
.flatten()
|
|
151
|
+
.unwrap_or_else(|| json!({}))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pub(super) fn resolve_status_owner_team(
|
|
155
|
+
workspace: &Path,
|
|
156
|
+
owner_team_id: Option<&str>,
|
|
157
|
+
) -> Result<Option<String>, CliError> {
|
|
158
|
+
let Some(requested) = owner_team_id.filter(|team| !team.is_empty()) else {
|
|
159
|
+
return Ok(None);
|
|
160
|
+
};
|
|
161
|
+
let state = read_runtime_state(workspace);
|
|
162
|
+
match crate::state::projection::resolve_owner_team_id(&state, requested) {
|
|
163
|
+
OwnerTeamResolution::Canonical(canonical) => Ok(Some(canonical)),
|
|
164
|
+
OwnerTeamResolution::LegacyAlias {
|
|
165
|
+
requested,
|
|
166
|
+
canonical,
|
|
167
|
+
} => {
|
|
168
|
+
let log = crate::event_log::EventLog::new(workspace);
|
|
169
|
+
crate::messaging::delivery::normalize_owner_team_id_rows(
|
|
170
|
+
workspace,
|
|
171
|
+
&requested,
|
|
172
|
+
&canonical,
|
|
173
|
+
None,
|
|
174
|
+
Some(&log),
|
|
175
|
+
)
|
|
176
|
+
.map_err(CliError::from)?;
|
|
177
|
+
Ok(Some(canonical))
|
|
178
|
+
}
|
|
179
|
+
OwnerTeamResolution::Unresolved { .. } | OwnerTeamResolution::Ambiguous { .. } => Ok(None),
|
|
180
|
+
}
|
|
181
|
+
}
|