@team-agent/installer 0.5.7 → 0.5.9
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/attach_app_server_leader.rs +82 -1
- package/crates/team-agent/src/cli/diagnose.rs +46 -17
- package/crates/team-agent/src/cli/emit.rs +30 -1
- package/crates/team-agent/src/cli/leaders.rs +94 -0
- package/crates/team-agent/src/cli/mod.rs +138 -2
- package/crates/team-agent/src/cli/named_address.rs +288 -28
- package/crates/team-agent/src/cli/send.rs +230 -0
- package/crates/team-agent/src/cli/status_port.rs +117 -14
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +14 -0
- package/crates/team-agent/src/coordinator/tick.rs +7 -1
- package/crates/team-agent/src/db/agent_health_capture.rs +96 -0
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/registry.rs +402 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +13 -61
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +18 -1
- package/crates/team-agent/src/mcp_server/normalize.rs +79 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +125 -13
- package/crates/team-agent/src/mcp_server/tools.rs +24 -2
- package/crates/team-agent/src/messaging/delivery.rs +12 -5
- package/crates/team-agent/src/messaging/leader_receiver.rs +58 -0
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +53 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +7 -2
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/package.json +4 -4
|
@@ -38,7 +38,9 @@ use rusqlite::params;
|
|
|
38
38
|
coordinator_running,
|
|
39
39
|
undelivered_backlog,
|
|
40
40
|
);
|
|
41
|
-
let
|
|
41
|
+
let session_name = state.get("session_name").cloned().unwrap_or(Value::Null);
|
|
42
|
+
let tmux_present = tmux_session_present(workspace, state, session_name.as_str());
|
|
43
|
+
let agents = enrich_agents(state.get("agents"), tmux_present);
|
|
42
44
|
let tasks = state
|
|
43
45
|
.get("tasks")
|
|
44
46
|
.cloned()
|
|
@@ -47,7 +49,6 @@ use rusqlite::params;
|
|
|
47
49
|
.get("leader_receiver")
|
|
48
50
|
.cloned()
|
|
49
51
|
.unwrap_or_else(|| json!({}));
|
|
50
|
-
let session_name = state.get("session_name").cloned().unwrap_or(Value::Null);
|
|
51
52
|
let is_external_leader = crate::state::projection::state_is_external_leader(state);
|
|
52
53
|
let leader_topology = if is_external_leader { "external" } else { "managed" };
|
|
53
54
|
let leader_attach_command = if is_external_leader {
|
|
@@ -70,7 +71,6 @@ use rusqlite::params;
|
|
|
70
71
|
)
|
|
71
72
|
})
|
|
72
73
|
};
|
|
73
|
-
let tmux_present = tmux_session_present(workspace, state, session_name.as_str());
|
|
74
74
|
let mut readiness_state = state.clone();
|
|
75
75
|
if let Some(obj) = readiness_state.as_object_mut() {
|
|
76
76
|
obj.insert("tmux_session_present".to_string(), serde_json::json!(tmux_present));
|
|
@@ -437,7 +437,7 @@ use rusqlite::params;
|
|
|
437
437
|
.to_string()
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
fn enrich_agents(agents: Option<&Value
|
|
440
|
+
fn enrich_agents(agents: Option<&Value>, tmux_session_present: bool) -> Value {
|
|
441
441
|
let Some(Value::Object(input)) = agents else {
|
|
442
442
|
return json!({});
|
|
443
443
|
};
|
|
@@ -450,6 +450,15 @@ use rusqlite::params;
|
|
|
450
450
|
"interacted".to_string(),
|
|
451
451
|
Value::String(interacted_marker(obj.get("first_send_at"))),
|
|
452
452
|
);
|
|
453
|
+
if let Some(reason) =
|
|
454
|
+
stale_reason_for_agent(&Value::Object(obj.clone()), tmux_session_present)
|
|
455
|
+
{
|
|
456
|
+
enriched.insert("stale".to_string(), Value::Bool(true));
|
|
457
|
+
enriched.insert(
|
|
458
|
+
"stale_reason".to_string(),
|
|
459
|
+
Value::String(reason.to_string()),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
453
462
|
out.insert(agent_id.clone(), Value::Object(enriched));
|
|
454
463
|
}
|
|
455
464
|
_ => {
|
|
@@ -460,6 +469,53 @@ use rusqlite::params;
|
|
|
460
469
|
Value::Object(out)
|
|
461
470
|
}
|
|
462
471
|
|
|
472
|
+
fn stale_reason_for_agent(agent: &Value, tmux_session_present: bool) -> Option<&'static str> {
|
|
473
|
+
let pane_dead = !tmux_session_present && agent_has_pane_fact(agent);
|
|
474
|
+
let process_dead =
|
|
475
|
+
agent_process_dead(agent) || (!tmux_session_present && agent_has_process_fact(agent));
|
|
476
|
+
match (pane_dead, process_dead) {
|
|
477
|
+
(true, true) => Some("both"),
|
|
478
|
+
(false, true) => Some("process_dead"),
|
|
479
|
+
(true, false) => Some("pane_dead"),
|
|
480
|
+
(false, false) => None,
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
fn agent_has_pane_fact(agent: &Value) -> bool {
|
|
485
|
+
["pane_id", "window", "window_name"].iter().any(|key| {
|
|
486
|
+
agent
|
|
487
|
+
.get(*key)
|
|
488
|
+
.and_then(Value::as_str)
|
|
489
|
+
.is_some_and(|value| !value.is_empty())
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
fn agent_has_process_fact(agent: &Value) -> bool {
|
|
494
|
+
agent.get("pid").and_then(Value::as_i64).is_some()
|
|
495
|
+
|| agent.get("process_started").and_then(Value::as_bool) == Some(true)
|
|
496
|
+
|| agent.get("provider_process_dead").and_then(Value::as_bool).is_some()
|
|
497
|
+
|| agent.get("process_liveness").and_then(Value::as_str).is_some()
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
fn agent_process_dead(agent: &Value) -> bool {
|
|
501
|
+
if agent.get("provider_process_dead").and_then(Value::as_bool) == Some(true) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
["process_liveness", "worker_state"].iter().any(|key| {
|
|
505
|
+
agent
|
|
506
|
+
.get(*key)
|
|
507
|
+
.and_then(Value::as_str)
|
|
508
|
+
.is_some_and(is_dead_process_state)
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
fn is_dead_process_state(value: &str) -> bool {
|
|
513
|
+
matches!(
|
|
514
|
+
value,
|
|
515
|
+
"dead" | "missing" | "stopped" | "exited" | "terminated"
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
463
519
|
fn interacted_marker(value: Option<&Value>) -> String {
|
|
464
520
|
let Some(raw) = value.and_then(Value::as_str) else {
|
|
465
521
|
return "never".to_string();
|
|
@@ -654,14 +710,20 @@ use rusqlite::params;
|
|
|
654
710
|
limit: usize,
|
|
655
711
|
) -> Result<Value, CliError> {
|
|
656
712
|
let limit = i64::try_from(limit).unwrap_or(i64::MAX);
|
|
713
|
+
// E6 (0.5.9 offline-mailbox §6.6): also surface `queued_until_leader_attach`
|
|
714
|
+
// rows (third-party sends into the leader mailbox) so the target owner can
|
|
715
|
+
// see them alongside the rebind-required failures. Channel wire label
|
|
716
|
+
// distinguishes the two so the operator can tell the two shapes apart.
|
|
657
717
|
let sql = match owner_team_id {
|
|
658
718
|
Some(_) => {
|
|
659
719
|
"select message_id, sender, status, error, created_at, delivery_attempts
|
|
660
720
|
from messages
|
|
661
721
|
where recipient = 'leader'
|
|
662
|
-
and status = 'failed'
|
|
663
|
-
and error = 'leader_not_attached'
|
|
664
722
|
and owner_team_id = ?1
|
|
723
|
+
and (
|
|
724
|
+
(status = 'failed' and error = 'leader_not_attached')
|
|
725
|
+
or status = 'queued_until_leader_attach'
|
|
726
|
+
)
|
|
665
727
|
order by created_at desc
|
|
666
728
|
limit ?2"
|
|
667
729
|
}
|
|
@@ -669,8 +731,10 @@ use rusqlite::params;
|
|
|
669
731
|
"select message_id, sender, status, error, created_at, delivery_attempts
|
|
670
732
|
from messages
|
|
671
733
|
where recipient = 'leader'
|
|
672
|
-
and
|
|
673
|
-
|
|
734
|
+
and (
|
|
735
|
+
(status = 'failed' and error = 'leader_not_attached')
|
|
736
|
+
or status = 'queued_until_leader_attach'
|
|
737
|
+
)
|
|
674
738
|
order by created_at desc
|
|
675
739
|
limit ?1"
|
|
676
740
|
}
|
|
@@ -679,14 +743,20 @@ use rusqlite::params;
|
|
|
679
743
|
.prepare(sql)
|
|
680
744
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
681
745
|
let map_row = |row: &rusqlite::Row<'_>| {
|
|
746
|
+
let status: String = row.get(2)?;
|
|
747
|
+
let channel = if status == "queued_until_leader_attach" {
|
|
748
|
+
"leader_mailbox"
|
|
749
|
+
} else {
|
|
750
|
+
"rebind_required"
|
|
751
|
+
};
|
|
682
752
|
Ok(json!({
|
|
683
753
|
"message_id": row.get::<_, String>(0)?,
|
|
684
754
|
"sender": row.get::<_, Option<String>>(1)?,
|
|
685
|
-
"status":
|
|
755
|
+
"status": status,
|
|
686
756
|
"error": row.get::<_, Option<String>>(3)?,
|
|
687
757
|
"created_at": row.get::<_, Option<String>>(4)?,
|
|
688
758
|
"delivery_attempts": row.get::<_, i64>(5)?,
|
|
689
|
-
"channel":
|
|
759
|
+
"channel": channel,
|
|
690
760
|
"action": "run team-agent attach-leader or team-agent takeover",
|
|
691
761
|
}))
|
|
692
762
|
};
|
|
@@ -846,7 +916,15 @@ use rusqlite::params;
|
|
|
846
916
|
// 0.4.x Phase 1: add `worker_state` (canonical 5-state product
|
|
847
917
|
// surface). `activity` is preserved alongside as the deprecated
|
|
848
918
|
// legacy classifier output (CR R3 same-source contract).
|
|
849
|
-
for key in [
|
|
919
|
+
for key in [
|
|
920
|
+
"status",
|
|
921
|
+
"provider",
|
|
922
|
+
"worker_state",
|
|
923
|
+
"activity",
|
|
924
|
+
"last_output_at",
|
|
925
|
+
"stale",
|
|
926
|
+
"stale_reason",
|
|
927
|
+
] {
|
|
850
928
|
if let Some(value) = input.get(key) {
|
|
851
929
|
out.insert(key.to_string(), value.clone());
|
|
852
930
|
}
|
|
@@ -964,10 +1042,35 @@ use rusqlite::params;
|
|
|
964
1042
|
);
|
|
965
1043
|
insert_optional_string(&mut item, "last_output_at", row.get(2).map_err(|e| CliError::Runtime(e.to_string()))?);
|
|
966
1044
|
insert_optional_i64(&mut item, "context_usage_pct", row.get(3).map_err(|e| CliError::Runtime(e.to_string()))?);
|
|
967
|
-
|
|
1045
|
+
let current_task_id: Option<String> =
|
|
1046
|
+
row.get(4).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
1047
|
+
let has_current_task = current_task_id.is_some();
|
|
1048
|
+
insert_optional_string(&mut item, "current_task_id", current_task_id);
|
|
1049
|
+
let updated_at: String =
|
|
1050
|
+
row.get(5).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
1051
|
+
// Phase-DX E2 (plan §4 / CR supplement A): expose the last agent_health
|
|
1052
|
+
// observation timestamp as `health_updated_at` alongside the legacy
|
|
1053
|
+
// `updated_at` alias. Two names for one column keep old scrapers working
|
|
1054
|
+
// while surfacing the semantic (heartbeat, not row bookkeeping).
|
|
1055
|
+
item.insert("updated_at".to_string(), json!(updated_at.clone()));
|
|
1056
|
+
item.insert("health_updated_at".to_string(), json!(updated_at));
|
|
1057
|
+
// Phase-DX E2 (CR P0 red line #6, supplements A/B): current_task is a
|
|
1058
|
+
// best-effort *display* field until A1 makes task FSM authoritative.
|
|
1059
|
+
// The structured source/confidence markers stop downstream code from
|
|
1060
|
+
// treating agent_health.current_task_id as authority. `current_task_source`
|
|
1061
|
+
// records where the display value came from (only "health" today —
|
|
1062
|
+
// Phase-DX never merges state tasks into this projection); the
|
|
1063
|
+
// `current_task_confidence` enum stays "best_effort" for the whole
|
|
1064
|
+
// Phase-DX slice (A1 will later flip it to "authoritative" when the
|
|
1065
|
+
// task FSM lands). Field is written unconditionally so consumers can
|
|
1066
|
+
// switch on it even when `current_task_id` is null.
|
|
1067
|
+
item.insert(
|
|
1068
|
+
"current_task_source".to_string(),
|
|
1069
|
+
json!(if has_current_task { "health" } else { "none" }),
|
|
1070
|
+
);
|
|
968
1071
|
item.insert(
|
|
969
|
-
"
|
|
970
|
-
json!(
|
|
1072
|
+
"current_task_confidence".to_string(),
|
|
1073
|
+
json!("best_effort"),
|
|
971
1074
|
);
|
|
972
1075
|
insert_optional_string(&mut item, "owner_team_id", row.get(6).map_err(|e| CliError::Runtime(e.to_string()))?);
|
|
973
1076
|
out.insert(agent_id, Value::Object(item));
|
|
@@ -159,6 +159,7 @@ fn named_send_args(
|
|
|
159
159
|
message_id: None,
|
|
160
160
|
pane: pane.map(str::to_string),
|
|
161
161
|
to_name: to_name.map(str::to_string),
|
|
162
|
+
to_leader: None,
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -328,11 +329,16 @@ fn resolve_leader_name_not_live() {
|
|
|
328
329
|
.with_targets(vec![pane("other", "codex", "%other")]);
|
|
329
330
|
|
|
330
331
|
let err = resolve_name_with_transport(&ws, "alpha/leader", &transport).unwrap_err();
|
|
331
|
-
|
|
332
|
+
// 0.5.9 E6 taxonomy split: `--to-name <team>/leader` when the team
|
|
333
|
+
// exists but the leader receiver's pane is missing is now
|
|
334
|
+
// `leader_not_attached` (not the coarser `name_not_live`), so a
|
|
335
|
+
// third-party sender can be told what to do without being pushed
|
|
336
|
+
// toward `claim-leader`/`takeover` (owner-only actions).
|
|
337
|
+
assert_eq!(err.kind, NamedAddressErrorKind::LeaderNotAttached);
|
|
332
338
|
let n38 = err.n38_message();
|
|
333
|
-
assert!(n38.contains("claim-leader"), "{n38}");
|
|
334
339
|
assert!(n38.contains("attach-leader"), "{n38}");
|
|
335
|
-
assert!(n38.contains("
|
|
340
|
+
assert!(!n38.contains("claim-leader"), "third-party copy must not suggest claim-leader: {n38}");
|
|
341
|
+
assert!(!n38.contains("takeover"), "third-party copy must not suggest takeover: {n38}");
|
|
336
342
|
let _ = std::fs::remove_dir_all(&ws);
|
|
337
343
|
}
|
|
338
344
|
|
|
@@ -334,6 +334,11 @@ pub struct SendArgs {
|
|
|
334
334
|
/// `target` / `targets` / `--pane`; resolves workspace/team/agent or leader
|
|
335
335
|
/// name to the current live pane before using direct pane injection.
|
|
336
336
|
pub to_name: Option<String>,
|
|
337
|
+
/// E7 (0.5.9 host-leader-registry-design): `--to-leader NAME` resolves
|
|
338
|
+
/// NAME through `~/.team-agent/leaders`, canonical-validates, and then
|
|
339
|
+
/// delegates to the E6 leader delivery path (live inject or offline
|
|
340
|
+
/// mailbox with `queued_until_leader_attach`).
|
|
341
|
+
pub to_leader: Option<String>,
|
|
337
342
|
}
|
|
338
343
|
|
|
339
344
|
/// E23 worker-side emergency fallback for `team_orchestrator.send_message`
|
|
@@ -617,6 +622,15 @@ pub struct SessionsArgs {
|
|
|
617
622
|
pub team: Option<String>,
|
|
618
623
|
}
|
|
619
624
|
|
|
625
|
+
/// E7 (0.5.9 host-leader-registry-design §4.1): `team-agent leaders`
|
|
626
|
+
/// enumerates the host discovery index and classifies each entry as
|
|
627
|
+
/// LIVE, STALE, or AMBIGUOUS after re-validating against canonical
|
|
628
|
+
/// workspace state.
|
|
629
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
630
|
+
pub struct LeadersArgs {
|
|
631
|
+
pub json: bool,
|
|
632
|
+
}
|
|
633
|
+
|
|
620
634
|
/// `validate [spec=team.spec.yaml] --json`(`parser.py:120`)。
|
|
621
635
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
622
636
|
pub struct ValidateArgs {
|
|
@@ -2366,8 +2366,14 @@ fn write_agent_health(
|
|
|
2366
2366
|
.get("context_usage_pct")
|
|
2367
2367
|
.or_else(|| agent.get("context_usage_percent"))
|
|
2368
2368
|
.and_then(Value::as_i64);
|
|
2369
|
+
// Phase-DX E2: read the renamed `current_turn_message_id` (leader→worker turn
|
|
2370
|
+
// proxy, written by delivery::arm_turn_open) with fallbacks to the legacy field
|
|
2371
|
+
// names for backwards state compatibility. The SQL column stays `current_task_id`
|
|
2372
|
+
// (agent_health schema) — a rename would require a DB migration, which Phase-DX
|
|
2373
|
+
// forbids.
|
|
2369
2374
|
let current_task_id = agent
|
|
2370
|
-
.get("
|
|
2375
|
+
.get("current_turn_message_id")
|
|
2376
|
+
.or_else(|| agent.get("current_task_id"))
|
|
2371
2377
|
.or_else(|| agent.get("task_id"))
|
|
2372
2378
|
.and_then(Value::as_str);
|
|
2373
2379
|
conn.execute(
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//! Phase-DX E2: `agent_health` row capture/restore for the remove-agent flow.
|
|
2
|
+
//!
|
|
3
|
+
//! Extracted from `lifecycle/restart/remove.rs` so the SQL column reference to
|
|
4
|
+
//! `current_task_id` sits in the persistence layer (whitelisted by the E2 grep
|
|
5
|
+
//! guard) rather than in lifecycle policy code. Semantics are the golden Python
|
|
6
|
+
//! `_capture_agent_health` / `_restore_agent_health` — a plain
|
|
7
|
+
//! backup-across-delete that never treats the stored column as authoritative
|
|
8
|
+
//! task state.
|
|
9
|
+
|
|
10
|
+
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
11
|
+
|
|
12
|
+
use std::path::Path;
|
|
13
|
+
|
|
14
|
+
use crate::model::ids::AgentId;
|
|
15
|
+
|
|
16
|
+
#[derive(Clone, Debug)]
|
|
17
|
+
pub struct CapturedHealth {
|
|
18
|
+
pub owner_team_id: Option<String>,
|
|
19
|
+
pub status: Option<String>,
|
|
20
|
+
pub last_output_at: Option<String>,
|
|
21
|
+
pub context_usage_pct: Option<i64>,
|
|
22
|
+
pub current_task_id: Option<String>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// golden agents.py:185 `copy.deepcopy(store.agent_health().get(agent_id))` — read the row BEFORE
|
|
26
|
+
/// delete so the rollback can re-upsert it. Returns the captured columns, or `None` if the row is
|
|
27
|
+
/// absent.
|
|
28
|
+
pub fn select_agent_health(
|
|
29
|
+
workspace: &Path,
|
|
30
|
+
agent_id: &AgentId,
|
|
31
|
+
) -> Result<Option<CapturedHealth>, crate::db::DbError> {
|
|
32
|
+
let store = crate::message_store::MessageStore::open(workspace)
|
|
33
|
+
.map_err(|e| crate::db::DbError::Schema(e.to_string()))?;
|
|
34
|
+
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
35
|
+
let row = conn
|
|
36
|
+
.query_row(
|
|
37
|
+
"select owner_team_id, status, last_output_at, context_usage_pct, current_task_id \
|
|
38
|
+
from agent_health where agent_id = ?1",
|
|
39
|
+
[agent_id.as_str()],
|
|
40
|
+
|r| {
|
|
41
|
+
Ok(CapturedHealth {
|
|
42
|
+
owner_team_id: r.get::<_, Option<String>>(0)?,
|
|
43
|
+
status: r.get::<_, Option<String>>(1)?,
|
|
44
|
+
last_output_at: r.get::<_, Option<String>>(2)?,
|
|
45
|
+
context_usage_pct: r.get::<_, Option<i64>>(3)?,
|
|
46
|
+
current_task_id: r.get::<_, Option<String>>(4)?,
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
.ok();
|
|
51
|
+
Ok(row)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// golden agents.py:268-278 `_restore_agent_health`: re-upsert the captured row (status||"IDLE"),
|
|
55
|
+
/// or delete the row when there was nothing to restore.
|
|
56
|
+
pub fn restore_agent_health(
|
|
57
|
+
workspace: &Path,
|
|
58
|
+
agent_id: &AgentId,
|
|
59
|
+
row: &Option<CapturedHealth>,
|
|
60
|
+
) -> Result<(), crate::db::DbError> {
|
|
61
|
+
let Some(row) = row else {
|
|
62
|
+
return delete_agent_health(workspace, agent_id);
|
|
63
|
+
};
|
|
64
|
+
let store = crate::message_store::MessageStore::open(workspace)
|
|
65
|
+
.map_err(|e| crate::db::DbError::Schema(e.to_string()))?;
|
|
66
|
+
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
67
|
+
let status = row.status.clone().unwrap_or_else(|| "IDLE".to_string());
|
|
68
|
+
let now = chrono::Utc::now()
|
|
69
|
+
.format("%Y-%m-%dT%H:%M:%S%.6f+00:00")
|
|
70
|
+
.to_string();
|
|
71
|
+
conn.execute(
|
|
72
|
+
"insert into agent_health (owner_team_id, agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at) \
|
|
73
|
+
values (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
|
74
|
+
rusqlite::params![
|
|
75
|
+
row.owner_team_id,
|
|
76
|
+
agent_id.as_str(),
|
|
77
|
+
status,
|
|
78
|
+
row.last_output_at,
|
|
79
|
+
row.context_usage_pct,
|
|
80
|
+
row.current_task_id,
|
|
81
|
+
now,
|
|
82
|
+
],
|
|
83
|
+
)?;
|
|
84
|
+
Ok(())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn delete_agent_health(workspace: &Path, agent_id: &AgentId) -> Result<(), crate::db::DbError> {
|
|
88
|
+
let store = crate::message_store::MessageStore::open(workspace)
|
|
89
|
+
.map_err(|e| crate::db::DbError::Schema(e.to_string()))?;
|
|
90
|
+
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
91
|
+
conn.execute(
|
|
92
|
+
"delete from agent_health where agent_id = ?1",
|
|
93
|
+
[agent_id.as_str()],
|
|
94
|
+
)?;
|
|
95
|
+
Ok(())
|
|
96
|
+
}
|