@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.
Files changed (84) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +11 -48
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  27. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  28. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  29. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  30. package/crates/team-agent/src/db/message_store.rs +154 -44
  31. package/crates/team-agent/src/event_log.rs +73 -0
  32. package/crates/team-agent/src/leader/start.rs +28 -4
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
  35. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  36. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  37. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  38. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  39. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  40. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  41. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  42. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  43. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  44. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  45. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  46. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  47. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  48. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  49. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  50. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  51. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  52. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
  53. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
  55. package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
  56. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  57. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
  58. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
  59. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  60. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  61. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  62. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  63. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  64. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  65. package/crates/team-agent/src/messaging/activity.rs +4 -2
  66. package/crates/team-agent/src/messaging/address.rs +86 -0
  67. package/crates/team-agent/src/messaging/delivery.rs +165 -39
  68. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  69. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  70. package/crates/team-agent/src/messaging/mod.rs +10 -2
  71. package/crates/team-agent/src/messaging/persist.rs +309 -0
  72. package/crates/team-agent/src/messaging/results.rs +16 -24
  73. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  74. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  75. package/crates/team-agent/src/messaging/send.rs +101 -49
  76. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  77. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  78. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  79. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  80. package/crates/team-agent/src/redaction.rs +72 -2
  81. package/crates/team-agent/src/state/persist.rs +2 -1
  82. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  83. package/crates/team-agent/src/state/repository.rs +59 -16
  84. 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
+ }