@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,412 @@
|
|
|
1
|
+
use crate::cli::CliError;
|
|
2
|
+
use rusqlite::params;
|
|
3
|
+
use serde_json::{json, Map, Value};
|
|
4
|
+
use std::path::Path;
|
|
5
|
+
|
|
6
|
+
pub(super) fn recent_agent_messages(
|
|
7
|
+
workspace: &Path,
|
|
8
|
+
agent_id: &str,
|
|
9
|
+
) -> Result<Vec<Value>, CliError> {
|
|
10
|
+
crate::message_store::MessageStore::open(workspace)
|
|
11
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?
|
|
12
|
+
.inbox(agent_id, 3, None)
|
|
13
|
+
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// `latest_result_summaries`(`queries.py:83-89`)。
|
|
17
|
+
pub(super) fn latest_result_summaries(
|
|
18
|
+
store: &crate::message_store::MessageStore,
|
|
19
|
+
owner_team_id: Option<&str>,
|
|
20
|
+
) -> Result<Value, CliError> {
|
|
21
|
+
let rows = store
|
|
22
|
+
.latest_results(5, owner_team_id)
|
|
23
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
24
|
+
Ok(Value::Array(
|
|
25
|
+
rows.iter()
|
|
26
|
+
.filter_map(crate::message_store::result_summary_from_row)
|
|
27
|
+
.collect(),
|
|
28
|
+
))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub(super) fn message_counts(
|
|
32
|
+
conn: &rusqlite::Connection,
|
|
33
|
+
owner_team_id: Option<&str>,
|
|
34
|
+
) -> Result<Value, CliError> {
|
|
35
|
+
status_counts(conn, "messages", owner_team_id)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub(super) fn result_counts(
|
|
39
|
+
conn: &rusqlite::Connection,
|
|
40
|
+
owner_team_id: Option<&str>,
|
|
41
|
+
) -> Result<Value, CliError> {
|
|
42
|
+
let by_status = result_status_counts(conn, owner_team_id)?;
|
|
43
|
+
let total = count_rows(conn, "results", owner_team_id)?;
|
|
44
|
+
let invalid = count_where_status(conn, "results", owner_team_id, "invalid")?;
|
|
45
|
+
let collected = count_where_status(conn, "results", owner_team_id, "collected")?;
|
|
46
|
+
let uncollected = total.saturating_sub(collected).saturating_sub(invalid);
|
|
47
|
+
Ok(json!({
|
|
48
|
+
"total": total,
|
|
49
|
+
"uncollected": uncollected,
|
|
50
|
+
"collected": collected,
|
|
51
|
+
"invalid": invalid,
|
|
52
|
+
"by_status": by_status,
|
|
53
|
+
}))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub(super) fn status_counts(
|
|
57
|
+
conn: &rusqlite::Connection,
|
|
58
|
+
table: &str,
|
|
59
|
+
owner_team_id: Option<&str>,
|
|
60
|
+
) -> Result<Value, CliError> {
|
|
61
|
+
let sql = match owner_team_id {
|
|
62
|
+
Some(_) => format!(
|
|
63
|
+
"select status, count(*) from {table}
|
|
64
|
+
where owner_team_id = ?1
|
|
65
|
+
group by status order by status"
|
|
66
|
+
),
|
|
67
|
+
None => format!("select status, count(*) from {table} group by status order by status"),
|
|
68
|
+
};
|
|
69
|
+
let mut stmt = conn
|
|
70
|
+
.prepare(&sql)
|
|
71
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
72
|
+
let mut rows = match owner_team_id {
|
|
73
|
+
Some(team) => stmt
|
|
74
|
+
.query(params![team])
|
|
75
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
76
|
+
None => stmt
|
|
77
|
+
.query([])
|
|
78
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
79
|
+
};
|
|
80
|
+
let mut out = Map::new();
|
|
81
|
+
while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
|
|
82
|
+
let status: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
83
|
+
let count: i64 = row.get(1).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
84
|
+
out.insert(status, json!(count));
|
|
85
|
+
}
|
|
86
|
+
Ok(Value::Object(out))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub(super) fn result_status_counts(
|
|
90
|
+
conn: &rusqlite::Connection,
|
|
91
|
+
owner_team_id: Option<&str>,
|
|
92
|
+
) -> Result<Value, CliError> {
|
|
93
|
+
let sql = match owner_team_id {
|
|
94
|
+
Some(_) => {
|
|
95
|
+
"select status, count(*) from results
|
|
96
|
+
where status not in ('collected', 'invalid') and owner_team_id = ?1
|
|
97
|
+
group by status
|
|
98
|
+
order by status"
|
|
99
|
+
}
|
|
100
|
+
None => {
|
|
101
|
+
"select status, count(*) from results
|
|
102
|
+
where status not in ('collected', 'invalid')
|
|
103
|
+
group by status
|
|
104
|
+
order by status"
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
let mut stmt = conn
|
|
108
|
+
.prepare(sql)
|
|
109
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
110
|
+
let mut rows = match owner_team_id {
|
|
111
|
+
Some(team) => stmt
|
|
112
|
+
.query(params![team])
|
|
113
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
114
|
+
None => stmt
|
|
115
|
+
.query([])
|
|
116
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
117
|
+
};
|
|
118
|
+
let mut out = Map::new();
|
|
119
|
+
while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
|
|
120
|
+
let status: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
121
|
+
let count: i64 = row.get(1).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
122
|
+
out.insert(status, json!(count));
|
|
123
|
+
}
|
|
124
|
+
Ok(Value::Object(out))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pub(super) fn queued_messages(
|
|
128
|
+
conn: &rusqlite::Connection,
|
|
129
|
+
owner_team_id: Option<&str>,
|
|
130
|
+
limit: usize,
|
|
131
|
+
) -> Result<Value, CliError> {
|
|
132
|
+
let limit = i64::try_from(limit).unwrap_or(i64::MAX);
|
|
133
|
+
let sql = match owner_team_id {
|
|
134
|
+
Some(_) => {
|
|
135
|
+
"select message_id, recipient, status, created_at, delivery_attempts
|
|
136
|
+
from messages
|
|
137
|
+
where status like 'queued%' and owner_team_id = ?1
|
|
138
|
+
order by created_at desc
|
|
139
|
+
limit ?2"
|
|
140
|
+
}
|
|
141
|
+
None => {
|
|
142
|
+
"select message_id, recipient, status, created_at, delivery_attempts
|
|
143
|
+
from messages
|
|
144
|
+
where status like 'queued%'
|
|
145
|
+
order by created_at desc
|
|
146
|
+
limit ?1"
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
let mut stmt = conn
|
|
150
|
+
.prepare(sql)
|
|
151
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
152
|
+
let map_row = |row: &rusqlite::Row<'_>| {
|
|
153
|
+
Ok(json!({
|
|
154
|
+
"message_id": row.get::<_, String>(0)?,
|
|
155
|
+
"recipient": row.get::<_, Option<String>>(1)?,
|
|
156
|
+
"status": row.get::<_, String>(2)?,
|
|
157
|
+
"created_at": row.get::<_, Option<String>>(3)?,
|
|
158
|
+
"delivery_attempts": row.get::<_, i64>(4)?,
|
|
159
|
+
}))
|
|
160
|
+
};
|
|
161
|
+
let rows = match owner_team_id {
|
|
162
|
+
Some(team) => stmt.query_map(params![team, limit], map_row),
|
|
163
|
+
None => stmt.query_map(params![limit], map_row),
|
|
164
|
+
}
|
|
165
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
166
|
+
let values = rows
|
|
167
|
+
.collect::<Result<Vec<_>, _>>()
|
|
168
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
169
|
+
Ok(Value::Array(values))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// 0.5.5 gate054 round-2: leader notifications that were refused with
|
|
173
|
+
/// `rebind_required` (status=failed, error=leader_not_attached) sit as
|
|
174
|
+
/// failed rows in the store; without a dedicated status field the
|
|
175
|
+
/// operator sees only `messages.failed=N` and cannot tell that the
|
|
176
|
+
/// notifications are waiting for a rebind. This field surfaces them
|
|
177
|
+
/// alongside `queued_messages` so `attach-leader` / `takeover` is
|
|
178
|
+
/// visibly the fix. Once the pane is rebound the requeue path flips
|
|
179
|
+
/// each row back to `status=accepted` and it drops out of this list.
|
|
180
|
+
pub(super) fn pending_leader_notifications(
|
|
181
|
+
conn: &rusqlite::Connection,
|
|
182
|
+
owner_team_id: Option<&str>,
|
|
183
|
+
limit: usize,
|
|
184
|
+
) -> Result<Value, CliError> {
|
|
185
|
+
let limit = i64::try_from(limit).unwrap_or(i64::MAX);
|
|
186
|
+
// E6 (0.5.9 offline-mailbox §6.6): also surface `queued_until_leader_attach`
|
|
187
|
+
// rows (third-party sends into the leader mailbox) so the target owner can
|
|
188
|
+
// see them alongside the rebind-required failures. Channel wire label
|
|
189
|
+
// distinguishes the two so the operator can tell the two shapes apart.
|
|
190
|
+
let sql = match owner_team_id {
|
|
191
|
+
Some(_) => {
|
|
192
|
+
"select message_id, sender, status, error, created_at, delivery_attempts
|
|
193
|
+
from messages
|
|
194
|
+
where recipient = 'leader'
|
|
195
|
+
and owner_team_id = ?1
|
|
196
|
+
and (
|
|
197
|
+
(status = 'failed' and error = 'leader_not_attached')
|
|
198
|
+
or status = 'queued_until_leader_attach'
|
|
199
|
+
)
|
|
200
|
+
order by created_at desc
|
|
201
|
+
limit ?2"
|
|
202
|
+
}
|
|
203
|
+
None => {
|
|
204
|
+
"select message_id, sender, status, error, created_at, delivery_attempts
|
|
205
|
+
from messages
|
|
206
|
+
where recipient = 'leader'
|
|
207
|
+
and (
|
|
208
|
+
(status = 'failed' and error = 'leader_not_attached')
|
|
209
|
+
or status = 'queued_until_leader_attach'
|
|
210
|
+
)
|
|
211
|
+
order by created_at desc
|
|
212
|
+
limit ?1"
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
let mut stmt = conn
|
|
216
|
+
.prepare(sql)
|
|
217
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
218
|
+
let map_row = |row: &rusqlite::Row<'_>| {
|
|
219
|
+
let status: String = row.get(2)?;
|
|
220
|
+
let channel = if status == "queued_until_leader_attach" {
|
|
221
|
+
"leader_mailbox"
|
|
222
|
+
} else {
|
|
223
|
+
"rebind_required"
|
|
224
|
+
};
|
|
225
|
+
Ok(json!({
|
|
226
|
+
"message_id": row.get::<_, String>(0)?,
|
|
227
|
+
"sender": row.get::<_, Option<String>>(1)?,
|
|
228
|
+
"status": status,
|
|
229
|
+
"error": row.get::<_, Option<String>>(3)?,
|
|
230
|
+
"created_at": row.get::<_, Option<String>>(4)?,
|
|
231
|
+
"delivery_attempts": row.get::<_, i64>(5)?,
|
|
232
|
+
"channel": channel,
|
|
233
|
+
"action": "run team-agent attach-leader or team-agent takeover",
|
|
234
|
+
}))
|
|
235
|
+
};
|
|
236
|
+
let rows = match owner_team_id {
|
|
237
|
+
Some(team) => stmt.query_map(params![team, limit], map_row),
|
|
238
|
+
None => stmt.query_map(params![limit], map_row),
|
|
239
|
+
}
|
|
240
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
241
|
+
let values = rows
|
|
242
|
+
.collect::<Result<Vec<_>, _>>()
|
|
243
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
244
|
+
Ok(Value::Array(values))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// 0.4.x: slim default compact payload — exactly 7 top-level fields.
|
|
248
|
+
/// Diagnostic detail moves to `--detail`. Plan:
|
|
249
|
+
/// /Users/alauda/Documents/code/team-agent-public/.team/artifacts/status-compact-plan.md
|
|
250
|
+
pub(super) fn count_rows(
|
|
251
|
+
conn: &rusqlite::Connection,
|
|
252
|
+
table: &str,
|
|
253
|
+
owner_team_id: Option<&str>,
|
|
254
|
+
) -> Result<i64, CliError> {
|
|
255
|
+
match owner_team_id {
|
|
256
|
+
Some(team) => {
|
|
257
|
+
let sql = format!("select count(*) from {table} where owner_team_id = ?1");
|
|
258
|
+
conn.query_row(&sql, [team], |row| row.get::<_, i64>(0))
|
|
259
|
+
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
260
|
+
}
|
|
261
|
+
None => {
|
|
262
|
+
let sql = format!("select count(*) from {table}");
|
|
263
|
+
conn.query_row(&sql, [], |row| row.get::<_, i64>(0))
|
|
264
|
+
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
pub(super) fn count_where_status(
|
|
270
|
+
conn: &rusqlite::Connection,
|
|
271
|
+
table: &str,
|
|
272
|
+
owner_team_id: Option<&str>,
|
|
273
|
+
status: &str,
|
|
274
|
+
) -> Result<i64, CliError> {
|
|
275
|
+
match owner_team_id {
|
|
276
|
+
Some(team) => {
|
|
277
|
+
let sql =
|
|
278
|
+
format!("select count(*) from {table} where status = ?1 and owner_team_id = ?2");
|
|
279
|
+
conn.query_row(&sql, params![status, team], |row| row.get::<_, i64>(0))
|
|
280
|
+
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
281
|
+
}
|
|
282
|
+
None => {
|
|
283
|
+
let sql = format!("select count(*) from {table} where status = ?1");
|
|
284
|
+
conn.query_row(&sql, [status], |row| row.get::<_, i64>(0))
|
|
285
|
+
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
pub(super) fn agent_health(
|
|
291
|
+
conn: &rusqlite::Connection,
|
|
292
|
+
owner_team_id: Option<&str>,
|
|
293
|
+
) -> Result<Value, CliError> {
|
|
294
|
+
let sql = match owner_team_id {
|
|
295
|
+
Some(_) => {
|
|
296
|
+
"select agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at, owner_team_id
|
|
297
|
+
from agent_health where owner_team_id = ?1 order by agent_id"
|
|
298
|
+
}
|
|
299
|
+
None => {
|
|
300
|
+
"select agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at, owner_team_id
|
|
301
|
+
from agent_health order by agent_id"
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
let mut stmt = conn
|
|
305
|
+
.prepare(sql)
|
|
306
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
307
|
+
let mut rows = match owner_team_id {
|
|
308
|
+
Some(team) => stmt
|
|
309
|
+
.query(params![team])
|
|
310
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
311
|
+
None => stmt
|
|
312
|
+
.query([])
|
|
313
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
314
|
+
};
|
|
315
|
+
let mut out = Map::new();
|
|
316
|
+
while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
|
|
317
|
+
let agent_id: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
318
|
+
let status: String = row.get(1).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
319
|
+
let mut item = Map::new();
|
|
320
|
+
item.insert("status".to_string(), json!(status));
|
|
321
|
+
item.insert(
|
|
322
|
+
"health_status".to_string(),
|
|
323
|
+
json!(crate::provider::agent_health_status(
|
|
324
|
+
item.get("status").and_then(Value::as_str).unwrap_or("")
|
|
325
|
+
)),
|
|
326
|
+
);
|
|
327
|
+
insert_optional_string(
|
|
328
|
+
&mut item,
|
|
329
|
+
"last_output_at",
|
|
330
|
+
row.get(2).map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
331
|
+
);
|
|
332
|
+
insert_optional_i64(
|
|
333
|
+
&mut item,
|
|
334
|
+
"context_usage_pct",
|
|
335
|
+
row.get(3).map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
336
|
+
);
|
|
337
|
+
let current_task_id: Option<String> =
|
|
338
|
+
row.get(4).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
339
|
+
let has_current_task = current_task_id.is_some();
|
|
340
|
+
insert_optional_string(&mut item, "current_task_id", current_task_id);
|
|
341
|
+
let updated_at: String = row.get(5).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
342
|
+
// Phase-DX E2 (plan §4 / CR supplement A): expose the last agent_health
|
|
343
|
+
// observation timestamp as `health_updated_at` alongside the legacy
|
|
344
|
+
// `updated_at` alias. Two names for one column keep old scrapers working
|
|
345
|
+
// while surfacing the semantic (heartbeat, not row bookkeeping).
|
|
346
|
+
item.insert("updated_at".to_string(), json!(updated_at.clone()));
|
|
347
|
+
item.insert("health_updated_at".to_string(), json!(updated_at));
|
|
348
|
+
// Phase-DX E2 (CR P0 red line #6, supplements A/B): current_task is a
|
|
349
|
+
// best-effort *display* field until A1 makes task FSM authoritative.
|
|
350
|
+
// The structured source/confidence markers stop downstream code from
|
|
351
|
+
// treating agent_health.current_task_id as authority. `current_task_source`
|
|
352
|
+
// records where the display value came from (only "health" today —
|
|
353
|
+
// Phase-DX never merges state tasks into this projection); the
|
|
354
|
+
// `current_task_confidence` enum stays "best_effort" for the whole
|
|
355
|
+
// Phase-DX slice (A1 will later flip it to "authoritative" when the
|
|
356
|
+
// task FSM lands). Field is written unconditionally so consumers can
|
|
357
|
+
// switch on it even when `current_task_id` is null.
|
|
358
|
+
item.insert(
|
|
359
|
+
"current_task_source".to_string(),
|
|
360
|
+
json!(if has_current_task { "health" } else { "none" }),
|
|
361
|
+
);
|
|
362
|
+
item.insert("current_task_confidence".to_string(), json!("best_effort"));
|
|
363
|
+
insert_optional_string(
|
|
364
|
+
&mut item,
|
|
365
|
+
"owner_team_id",
|
|
366
|
+
row.get(6).map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
367
|
+
);
|
|
368
|
+
out.insert(agent_id, Value::Object(item));
|
|
369
|
+
}
|
|
370
|
+
Ok(Value::Object(out))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
pub(super) fn insert_optional_string(
|
|
374
|
+
map: &mut Map<String, Value>,
|
|
375
|
+
key: &str,
|
|
376
|
+
value: Option<String>,
|
|
377
|
+
) {
|
|
378
|
+
if let Some(value) = value {
|
|
379
|
+
map.insert(key.to_string(), Value::String(value));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
pub(super) fn insert_optional_i64(map: &mut Map<String, Value>, key: &str, value: Option<i64>) {
|
|
384
|
+
if let Some(value) = value {
|
|
385
|
+
map.insert(key.to_string(), json!(value));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// B-5 / 036b N38 — status 出口的 runtime 块:把 coordinator_health 与
|
|
390
|
+
/// undelivered backlog 合体暴露。down-hint 只在【coordinator 不在跑 ∧ 有 backlog】
|
|
391
|
+
/// 两条件同时满足才挂(anti-nag);健康状态下不挂提示。auto-recovery 不做。
|
|
392
|
+
pub(super) fn count_undelivered_backlog(
|
|
393
|
+
conn: &rusqlite::Connection,
|
|
394
|
+
owner_team_id: Option<&str>,
|
|
395
|
+
) -> Result<i64, CliError> {
|
|
396
|
+
// Backlog statuses chosen to mirror what `deliver_pending` would pick up.
|
|
397
|
+
let sql = match owner_team_id {
|
|
398
|
+
Some(_) => "select count(*) from messages
|
|
399
|
+
where owner_team_id = ?1 and status in ('accepted','pending','queued','queued_until_trust')",
|
|
400
|
+
None => "select count(*) from messages
|
|
401
|
+
where status in ('accepted','pending','queued','queued_until_trust')",
|
|
402
|
+
};
|
|
403
|
+
let count: i64 = match owner_team_id {
|
|
404
|
+
Some(team) => conn
|
|
405
|
+
.query_row(sql, params![team], |row| row.get(0))
|
|
406
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
407
|
+
None => conn
|
|
408
|
+
.query_row(sql, [], |row| row.get(0))
|
|
409
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
410
|
+
};
|
|
411
|
+
Ok(count)
|
|
412
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
fn health_fixture() -> crate::coordinator::HealthReport {
|
|
4
|
+
crate::coordinator::HealthReport {
|
|
5
|
+
ok: true,
|
|
6
|
+
status: crate::coordinator::CoordinatorHealthStatus::Running,
|
|
7
|
+
pid: Some(crate::coordinator::Pid::new(std::process::id())),
|
|
8
|
+
metadata: None,
|
|
9
|
+
metadata_ok: true,
|
|
10
|
+
process_running: true,
|
|
11
|
+
wire_metadata_ok: true,
|
|
12
|
+
binary_identity_ok: true,
|
|
13
|
+
binary_identity_relation: crate::coordinator::CoordinatorBinaryIdentityRelation::Same,
|
|
14
|
+
service_available: true,
|
|
15
|
+
metadata_mismatch_reason: None,
|
|
16
|
+
current_binary_identity: crate::coordinator::CoordinatorBinaryIdentity {
|
|
17
|
+
binary_path: "/test/team-agent".to_string(),
|
|
18
|
+
binary_version: env!("CARGO_PKG_VERSION").to_string(),
|
|
19
|
+
},
|
|
20
|
+
schema: crate::coordinator::SchemaHealth {
|
|
21
|
+
ok: true,
|
|
22
|
+
schema_version: crate::db::schema::SCHEMA_VERSION,
|
|
23
|
+
error: None,
|
|
24
|
+
action: None,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[test]
|
|
30
|
+
fn runtime_freshness_provider_exit_requires_typed_positive_fact() {
|
|
31
|
+
let state = serde_json::json!({
|
|
32
|
+
"coordinator": {
|
|
33
|
+
"abnormal_exit_watch": {
|
|
34
|
+
"pane_only": {
|
|
35
|
+
"provider_process_dead": true,
|
|
36
|
+
"worker_provider_exited": false
|
|
37
|
+
},
|
|
38
|
+
"typed_exit": {
|
|
39
|
+
"provider_process_dead": true,
|
|
40
|
+
"worker_provider_exited": true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let freshness = compute_runtime_freshness(
|
|
47
|
+
Path::new("/nonexistent/status-port-typed-provider-exit-test"),
|
|
48
|
+
&state,
|
|
49
|
+
&health_fixture(),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
assert!(!freshness.provider_exited_agents.contains("pane_only"));
|
|
53
|
+
assert!(freshness.provider_exited_agents.contains("typed_exit"));
|
|
54
|
+
}
|