@team-agent/installer 0.3.1 → 0.3.3
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 +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +234 -26
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +289 -54
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +1281 -196
- package/crates/team-agent/src/cli/status_port.rs +195 -46
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
- package/crates/team-agent/src/cli/types.rs +18 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +95 -17
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
- package/crates/team-agent/src/lifecycle/mod.rs +2 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +470 -59
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +353 -63
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +564 -63
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +170 -1
- package/crates/team-agent/src/state/projection.rs +141 -8
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +161 -64
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//! Shared coordinator runtime observation seam.
|
|
2
|
+
//!
|
|
3
|
+
//! S0 only defines the typed capture/result surface. Lane 1 fills capture facts;
|
|
4
|
+
//! Lane 2 fills detector results from those facts.
|
|
5
|
+
|
|
6
|
+
use std::collections::BTreeMap;
|
|
7
|
+
use std::path::Path;
|
|
8
|
+
|
|
9
|
+
use serde_json::Value;
|
|
10
|
+
|
|
11
|
+
use crate::model::enums::Provider;
|
|
12
|
+
use crate::model::ids::{AgentId, TeamKey};
|
|
13
|
+
use crate::provider::{ProcessLiveness, RolloutPath};
|
|
14
|
+
use crate::transport::{PaneId, PaneInfo, SessionName, WindowName};
|
|
15
|
+
|
|
16
|
+
use super::types::{CompactionResult, LeaderApiError, SessionDriftResult};
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
19
|
+
pub struct CapturedRuntimeFact {
|
|
20
|
+
pub team_key: Option<TeamKey>,
|
|
21
|
+
pub agent_id: AgentId,
|
|
22
|
+
pub provider: Option<Provider>,
|
|
23
|
+
pub session_name: Option<SessionName>,
|
|
24
|
+
pub window: Option<WindowName>,
|
|
25
|
+
pub pane_id: Option<PaneId>,
|
|
26
|
+
pub scrollback_tail: String,
|
|
27
|
+
pub pane_info: Option<PaneInfo>,
|
|
28
|
+
pub agent_state_snapshot: Value,
|
|
29
|
+
pub stored_session_id: Option<String>,
|
|
30
|
+
pub last_output_at: Option<String>,
|
|
31
|
+
pub rollout_path: Option<RolloutPath>,
|
|
32
|
+
pub process_liveness: Option<ProcessLiveness>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
36
|
+
pub struct LeaderCaptureFact {
|
|
37
|
+
pub team_key: Option<TeamKey>,
|
|
38
|
+
pub leader_receiver: Option<Value>,
|
|
39
|
+
pub pane_id: Option<PaneId>,
|
|
40
|
+
pub scrollback_tail: String,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
44
|
+
pub struct RuntimeObservationResults {
|
|
45
|
+
pub captures_by_agent: BTreeMap<AgentId, CapturedRuntimeFact>,
|
|
46
|
+
pub compaction: Vec<CompactionResult>,
|
|
47
|
+
pub session_drift: Vec<SessionDriftResult>,
|
|
48
|
+
pub api_errors: Vec<LeaderApiError>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn observe(
|
|
52
|
+
workspace: &Path,
|
|
53
|
+
state: &mut Value,
|
|
54
|
+
captures_by_agent: BTreeMap<AgentId, CapturedRuntimeFact>,
|
|
55
|
+
leader_capture: Option<LeaderCaptureFact>,
|
|
56
|
+
) -> RuntimeObservationResults {
|
|
57
|
+
super::runtime_detectors::observe_runtime(workspace, state, captures_by_agent, leader_capture)
|
|
58
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//! Coordinator core:daemon lifecycle 宿主 + 单次 tick 编排(19 步固定顺序)+ health/start/stop。
|
|
2
2
|
|
|
3
|
+
use std::collections::BTreeMap;
|
|
3
4
|
use std::path::{Path, PathBuf};
|
|
4
5
|
|
|
5
6
|
use serde_json::Value;
|
|
@@ -17,6 +18,7 @@ use super::health::{
|
|
|
17
18
|
coordinator_log_path, coordinator_meta_path, coordinator_metadata_ok, coordinator_pid_path,
|
|
18
19
|
pid_is_running, read_coordinator_metadata, write_coordinator_metadata,
|
|
19
20
|
};
|
|
21
|
+
use super::runtime_observation::{self, CapturedRuntimeFact};
|
|
20
22
|
use super::types::{
|
|
21
23
|
AgentId, CoordinatorHealthStatus, HealthReport, MetadataSource, Pid, ProviderRegistry,
|
|
22
24
|
SchemaHealth, StartError, StartOutcome, StartReport, StopError, StopOutcome, StopReport,
|
|
@@ -206,7 +208,7 @@ impl Coordinator {
|
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
self.record_step("capture_missing");
|
|
209
|
-
self.capture_missing_sessions(&mut state)?;
|
|
211
|
+
self.capture_missing_sessions(&mut state, &event_log)?;
|
|
210
212
|
|
|
211
213
|
self.record_step("refresh_statuses");
|
|
212
214
|
// TODO(spine slice 2b): split lightweight runtime status refresh from health sync.
|
|
@@ -230,7 +232,7 @@ impl Coordinator {
|
|
|
230
232
|
self.handle_runtime_approval_prompts(&mut state, &event_log)?;
|
|
231
233
|
|
|
232
234
|
self.record_step("sync_health");
|
|
233
|
-
self.sync_agent_health(&mut state, &store, &event_log)?;
|
|
235
|
+
let captures_by_agent = self.sync_agent_health(&mut state, &store, &event_log)?;
|
|
234
236
|
self.detect_abnormal_exits(&mut state, &event_log)?;
|
|
235
237
|
|
|
236
238
|
self.record_step("deliver_pending");
|
|
@@ -269,17 +271,34 @@ impl Coordinator {
|
|
|
269
271
|
let idle_alerts: Vec<IdleAlert> = Vec::new();
|
|
270
272
|
self.record_step("detect_deadlocks");
|
|
271
273
|
let deadlock_alerts: Vec<DeadlockAlert> = Vec::new();
|
|
272
|
-
let _ =
|
|
274
|
+
let _ = &store;
|
|
273
275
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
self.record_step("detect_compaction");
|
|
277
|
+
self.record_step("detect_drift");
|
|
278
|
+
self.record_step("detect_api_errors");
|
|
279
|
+
let leader_capture = self.capture_leader_receiver(&state);
|
|
280
|
+
let observations = runtime_observation::observe(
|
|
281
|
+
self.workspace.as_path(),
|
|
282
|
+
&mut state,
|
|
283
|
+
captures_by_agent,
|
|
284
|
+
leader_capture,
|
|
285
|
+
);
|
|
286
|
+
let mut collections = TickCollections {
|
|
287
|
+
delivered,
|
|
288
|
+
scheduled,
|
|
289
|
+
stuck,
|
|
290
|
+
idle_alerts,
|
|
291
|
+
deadlock_alerts,
|
|
292
|
+
compaction: observations.compaction,
|
|
293
|
+
session_drift: observations.session_drift,
|
|
294
|
+
api_errors: observations.api_errors,
|
|
295
|
+
results: Vec::new(),
|
|
296
|
+
};
|
|
278
297
|
|
|
279
298
|
self.record_step("atomic_save");
|
|
280
299
|
let saved = match &self.save_hook {
|
|
281
300
|
Some(hook) => hook(&self.workspace, &state),
|
|
282
|
-
None => crate::state::
|
|
301
|
+
None => crate::state::projection::save_team_scoped_state(self.workspace.as_path(), &state),
|
|
283
302
|
};
|
|
284
303
|
if saved.is_err() {
|
|
285
304
|
return Ok(base_tick_report(
|
|
@@ -287,19 +306,12 @@ impl Coordinator {
|
|
|
287
306
|
false,
|
|
288
307
|
Some(TickStopReason::PersistenceDegraded),
|
|
289
308
|
Some(false),
|
|
290
|
-
|
|
291
|
-
delivered,
|
|
292
|
-
scheduled,
|
|
293
|
-
stuck,
|
|
294
|
-
idle_alerts,
|
|
295
|
-
deadlock_alerts,
|
|
296
|
-
results: Vec::new(),
|
|
297
|
-
},
|
|
309
|
+
collections,
|
|
298
310
|
));
|
|
299
311
|
}
|
|
300
312
|
|
|
301
313
|
self.record_step("collect_results");
|
|
302
|
-
|
|
314
|
+
collections.results = collect_results(
|
|
303
315
|
crate::messaging::collect_results_and_notify_watchers(self.workspace.as_path(), &event_log)?,
|
|
304
316
|
);
|
|
305
317
|
self.record_step("prune_dedupe_log");
|
|
@@ -308,7 +320,7 @@ impl Coordinator {
|
|
|
308
320
|
false,
|
|
309
321
|
None,
|
|
310
322
|
Some(true),
|
|
311
|
-
|
|
323
|
+
collections,
|
|
312
324
|
))
|
|
313
325
|
}
|
|
314
326
|
|
|
@@ -317,44 +329,21 @@ impl Coordinator {
|
|
|
317
329
|
// were removed by design. Delivery primitives still flow through the rest of
|
|
318
330
|
// the tick body unchanged.
|
|
319
331
|
|
|
320
|
-
fn capture_missing_sessions(&self, state: &mut Value) -> Result<(), TickError> {
|
|
321
|
-
let
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
let Some(provider) = agent_obj
|
|
335
|
-
.get("provider")
|
|
336
|
-
.and_then(Value::as_str)
|
|
337
|
-
.and_then(parse_provider)
|
|
338
|
-
else {
|
|
339
|
-
continue;
|
|
340
|
-
};
|
|
341
|
-
let adapter = self.provider_registry.adapter_for(provider);
|
|
342
|
-
let captured = adapter.capture_session_id(
|
|
343
|
-
agent_id,
|
|
344
|
-
std::path::Path::new(spawn_cwd),
|
|
345
|
-
0,
|
|
332
|
+
fn capture_missing_sessions(&self, state: &mut Value, event_log: &EventLog) -> Result<(), TickError> {
|
|
333
|
+
let report = crate::session_capture::capture_missing_provider_sessions_once(
|
|
334
|
+
state,
|
|
335
|
+
&mut |provider| self.provider_registry.adapter_for(provider),
|
|
336
|
+
true,
|
|
337
|
+
0,
|
|
338
|
+
)?;
|
|
339
|
+
for ambiguous in report.ambiguous {
|
|
340
|
+
event_log.write(
|
|
341
|
+
"provider.session.attribution_ambiguous",
|
|
342
|
+
serde_json::json!({
|
|
343
|
+
"agent_id": ambiguous.agent_id,
|
|
344
|
+
"spawn_cwd": ambiguous.spawn_cwd,
|
|
345
|
+
}),
|
|
346
346
|
)?;
|
|
347
|
-
if let Some(captured) = captured {
|
|
348
|
-
if let Some(session_id) = captured.session_id {
|
|
349
|
-
agent_obj.insert("session_id".to_string(), serde_json::json!(session_id.as_str()));
|
|
350
|
-
}
|
|
351
|
-
if let Some(rollout_path) = captured.rollout_path {
|
|
352
|
-
agent_obj.insert(
|
|
353
|
-
"rollout_path".to_string(),
|
|
354
|
-
serde_json::json!(rollout_path.as_path().to_string_lossy()),
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
347
|
}
|
|
359
348
|
Ok(())
|
|
360
349
|
}
|
|
@@ -364,12 +353,15 @@ impl Coordinator {
|
|
|
364
353
|
state: &mut Value,
|
|
365
354
|
store: &crate::message_store::MessageStore,
|
|
366
355
|
event_log: &EventLog,
|
|
367
|
-
) -> Result<
|
|
356
|
+
) -> Result<BTreeMap<AgentId, CapturedRuntimeFact>, TickError> {
|
|
357
|
+
let mut captures = BTreeMap::new();
|
|
368
358
|
let snapshot = state.clone();
|
|
369
359
|
let team = crate::state::projection::team_state_key(&snapshot);
|
|
360
|
+
let team_key = Some(crate::model::ids::TeamKey::new(team.clone()));
|
|
370
361
|
let session_name = state.get("session_name").and_then(Value::as_str).map(str::to_string);
|
|
362
|
+
let pane_infos = self.transport.list_targets().unwrap_or_default();
|
|
371
363
|
let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
|
|
372
|
-
return Ok(
|
|
364
|
+
return Ok(captures);
|
|
373
365
|
};
|
|
374
366
|
for (agent_id, agent) in agents {
|
|
375
367
|
let Some((session, window, target)) = capture_window_target(agent, session_name.as_deref()) else {
|
|
@@ -417,18 +409,72 @@ impl Coordinator {
|
|
|
417
409
|
.get("pane_current_command")
|
|
418
410
|
.or_else(|| agent.get("current_command"))
|
|
419
411
|
.and_then(Value::as_str);
|
|
420
|
-
let
|
|
412
|
+
let last_output_at_before = agent.get("last_output_at").and_then(Value::as_str);
|
|
421
413
|
let activity = crate::messaging::classify_agent_activity(
|
|
422
414
|
&snapshot,
|
|
423
415
|
&captured.text,
|
|
424
416
|
pane_in_mode,
|
|
425
417
|
current_command,
|
|
426
|
-
|
|
418
|
+
last_output_at_before,
|
|
427
419
|
);
|
|
428
420
|
let last_output_at = write_activity(agent, &activity, !captured.text.is_empty());
|
|
429
421
|
write_agent_health(store, &team, agent_id, agent, &activity, last_output_at.as_deref())?;
|
|
422
|
+
let pane_info = matching_capture_pane_info(agent, &session, &window, &pane_infos);
|
|
423
|
+
let pane_id = pane_info
|
|
424
|
+
.as_ref()
|
|
425
|
+
.map(|info| info.pane_id.clone())
|
|
426
|
+
.or_else(|| agent_pane_id(agent));
|
|
427
|
+
let rollout_path = agent_rollout_path(agent).map(crate::provider::RolloutPath::new);
|
|
428
|
+
captures.insert(
|
|
429
|
+
AgentId::new(agent_id.clone()),
|
|
430
|
+
CapturedRuntimeFact {
|
|
431
|
+
team_key: team_key.clone(),
|
|
432
|
+
agent_id: AgentId::new(agent_id.clone()),
|
|
433
|
+
provider: agent.get("provider").and_then(Value::as_str).and_then(parse_provider),
|
|
434
|
+
session_name: Some(session),
|
|
435
|
+
window: Some(window),
|
|
436
|
+
pane_id,
|
|
437
|
+
scrollback_tail: captured.text,
|
|
438
|
+
pane_info,
|
|
439
|
+
agent_state_snapshot: agent.clone(),
|
|
440
|
+
stored_session_id: agent
|
|
441
|
+
.get("session_id")
|
|
442
|
+
.and_then(Value::as_str)
|
|
443
|
+
.map(str::to_string),
|
|
444
|
+
last_output_at,
|
|
445
|
+
rollout_path,
|
|
446
|
+
process_liveness: explicit_process_liveness(agent),
|
|
447
|
+
},
|
|
448
|
+
);
|
|
430
449
|
}
|
|
431
|
-
Ok(
|
|
450
|
+
Ok(captures)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
fn capture_leader_receiver(
|
|
454
|
+
&self,
|
|
455
|
+
state: &Value,
|
|
456
|
+
) -> Option<runtime_observation::LeaderCaptureFact> {
|
|
457
|
+
let receiver = state.get("leader_receiver")?.clone();
|
|
458
|
+
let pane_id = receiver
|
|
459
|
+
.get("pane_id")
|
|
460
|
+
.and_then(Value::as_str)
|
|
461
|
+
.filter(|pane_id| !pane_id.is_empty())
|
|
462
|
+
.map(crate::transport::PaneId::new)?;
|
|
463
|
+
let captured = self
|
|
464
|
+
.transport
|
|
465
|
+
.capture(
|
|
466
|
+
&crate::transport::Target::Pane(pane_id.clone()),
|
|
467
|
+
crate::transport::CaptureRange::Tail(40),
|
|
468
|
+
)
|
|
469
|
+
.ok()?;
|
|
470
|
+
Some(runtime_observation::LeaderCaptureFact {
|
|
471
|
+
team_key: Some(crate::model::ids::TeamKey::new(
|
|
472
|
+
crate::state::projection::team_state_key(state),
|
|
473
|
+
)),
|
|
474
|
+
leader_receiver: Some(receiver),
|
|
475
|
+
pane_id: Some(pane_id),
|
|
476
|
+
scrollback_tail: captured.text,
|
|
477
|
+
})
|
|
432
478
|
}
|
|
433
479
|
|
|
434
480
|
/// #236 `worker.abnormal_exit` watcher.
|
|
@@ -692,13 +738,14 @@ impl Coordinator {
|
|
|
692
738
|
let snapshot = state.clone();
|
|
693
739
|
let team = crate::state::projection::team_state_key(&snapshot);
|
|
694
740
|
let session_name = snapshot.get("session_name").and_then(Value::as_str).map(str::to_string);
|
|
695
|
-
let auto_answer_allowed = runtime_approval_auto_answer_allowed();
|
|
696
741
|
let mut dedup_updates = Vec::new();
|
|
697
742
|
{
|
|
698
743
|
let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
|
|
699
744
|
return Ok(());
|
|
700
745
|
};
|
|
701
746
|
for (agent_id, agent) in agents {
|
|
747
|
+
let approval_policy = runtime_approval_policy_from_agent(agent);
|
|
748
|
+
let auto_answer_allowed = approval_policy.auto_answer_allowed();
|
|
702
749
|
let Some(target) = runtime_approval_target(agent, session_name.as_deref()) else {
|
|
703
750
|
clear_awaiting_human_confirm(agent);
|
|
704
751
|
dedup_updates.push(AwaitingDedupUpdate::Clear {
|
|
@@ -753,13 +800,17 @@ impl Coordinator {
|
|
|
753
800
|
let cleared = after
|
|
754
801
|
.as_ref()
|
|
755
802
|
.is_none_or(|after| after.prompt != prompt.prompt || after.tool != prompt.tool);
|
|
756
|
-
|
|
803
|
+
event_log.write(
|
|
757
804
|
"runtime_approval.auto_approved",
|
|
758
805
|
serde_json::json!({
|
|
759
806
|
"agent_id": agent_id,
|
|
760
807
|
"tool": prompt.tool,
|
|
761
808
|
"choice": choice,
|
|
762
809
|
"cleared": cleared,
|
|
810
|
+
"policy_source": approval_policy.source,
|
|
811
|
+
"inherited": approval_policy.inherited,
|
|
812
|
+
"explicit_yes_confirmed": approval_policy.explicit_yes_confirmed,
|
|
813
|
+
"worker_capability_above_leader": approval_policy.worker_capability_above_leader,
|
|
763
814
|
}),
|
|
764
815
|
)?;
|
|
765
816
|
}
|
|
@@ -820,6 +871,18 @@ impl Coordinator {
|
|
|
820
871
|
}),
|
|
821
872
|
)?;
|
|
822
873
|
}
|
|
874
|
+
"command_approval_requires_human" => {
|
|
875
|
+
event_log.write(
|
|
876
|
+
"runtime_approval.command_approval_requires_human",
|
|
877
|
+
serde_json::json!({
|
|
878
|
+
"agent_id": agent_id,
|
|
879
|
+
"tool": prompt.tool,
|
|
880
|
+
"command": prompt.command,
|
|
881
|
+
"kind": prompt.kind,
|
|
882
|
+
"prompt": prompt.prompt,
|
|
883
|
+
}),
|
|
884
|
+
)?;
|
|
885
|
+
}
|
|
823
886
|
_ => {}
|
|
824
887
|
}
|
|
825
888
|
}
|
|
@@ -963,9 +1026,9 @@ fn base_tick_report(
|
|
|
963
1026
|
stuck: collections.stuck,
|
|
964
1027
|
idle_alerts: collections.idle_alerts,
|
|
965
1028
|
deadlock_alerts: collections.deadlock_alerts,
|
|
966
|
-
compaction:
|
|
967
|
-
session_drift:
|
|
968
|
-
api_errors:
|
|
1029
|
+
compaction: collections.compaction,
|
|
1030
|
+
session_drift: collections.session_drift,
|
|
1031
|
+
api_errors: collections.api_errors,
|
|
969
1032
|
results: collections.results,
|
|
970
1033
|
}
|
|
971
1034
|
}
|
|
@@ -977,6 +1040,9 @@ struct TickCollections {
|
|
|
977
1040
|
stuck: Vec<AgentId>,
|
|
978
1041
|
idle_alerts: Vec<IdleAlert>,
|
|
979
1042
|
deadlock_alerts: Vec<DeadlockAlert>,
|
|
1043
|
+
compaction: Vec<CompactionResult>,
|
|
1044
|
+
session_drift: Vec<SessionDriftResult>,
|
|
1045
|
+
api_errors: Vec<LeaderApiError>,
|
|
980
1046
|
results: Vec<CollectedResult>,
|
|
981
1047
|
}
|
|
982
1048
|
|
|
@@ -1750,6 +1816,45 @@ fn capture_window_target(
|
|
|
1750
1816
|
))
|
|
1751
1817
|
}
|
|
1752
1818
|
|
|
1819
|
+
fn matching_capture_pane_info(
|
|
1820
|
+
agent: &Value,
|
|
1821
|
+
session: &crate::transport::SessionName,
|
|
1822
|
+
window: &crate::transport::WindowName,
|
|
1823
|
+
pane_infos: &[crate::transport::PaneInfo],
|
|
1824
|
+
) -> Option<crate::transport::PaneInfo> {
|
|
1825
|
+
if let Some(pane_id) = agent_pane_id(agent) {
|
|
1826
|
+
if let Some(info) = pane_infos.iter().find(|info| info.pane_id == pane_id) {
|
|
1827
|
+
return Some(info.clone());
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
pane_infos
|
|
1831
|
+
.iter()
|
|
1832
|
+
.find(|info| {
|
|
1833
|
+
&info.session == session
|
|
1834
|
+
&& info
|
|
1835
|
+
.window_name
|
|
1836
|
+
.as_ref()
|
|
1837
|
+
.is_some_and(|known_window| known_window == window)
|
|
1838
|
+
})
|
|
1839
|
+
.cloned()
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
fn agent_pane_id(agent: &Value) -> Option<crate::transport::PaneId> {
|
|
1843
|
+
agent
|
|
1844
|
+
.get("pane_id")
|
|
1845
|
+
.and_then(Value::as_str)
|
|
1846
|
+
.filter(|pane_id| !pane_id.is_empty())
|
|
1847
|
+
.map(crate::transport::PaneId::new)
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
fn agent_rollout_path(agent: &Value) -> Option<PathBuf> {
|
|
1851
|
+
["rollout_path", "transcript_path", "session_log_path"]
|
|
1852
|
+
.into_iter()
|
|
1853
|
+
.find_map(|key| agent.get(key).and_then(Value::as_str))
|
|
1854
|
+
.filter(|path| !path.is_empty())
|
|
1855
|
+
.map(PathBuf::from)
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1753
1858
|
fn runtime_approval_target(agent: &Value, session_name: Option<&str>) -> Option<crate::transport::Target> {
|
|
1754
1859
|
if let Some(pane_id) = agent
|
|
1755
1860
|
.get("pane_id")
|
|
@@ -1780,10 +1885,58 @@ fn runtime_approval_key(raw: String) -> Option<crate::transport::Key> {
|
|
|
1780
1885
|
}
|
|
1781
1886
|
}
|
|
1782
1887
|
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1888
|
+
#[derive(Debug, Clone)]
|
|
1889
|
+
struct RuntimeApprovalPolicy {
|
|
1890
|
+
enabled: bool,
|
|
1891
|
+
source: String,
|
|
1892
|
+
inherited: bool,
|
|
1893
|
+
explicit_yes_confirmed: bool,
|
|
1894
|
+
worker_capability_above_leader: bool,
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
impl RuntimeApprovalPolicy {
|
|
1898
|
+
fn auto_answer_allowed(&self) -> bool {
|
|
1899
|
+
if !self.enabled {
|
|
1900
|
+
return false;
|
|
1901
|
+
}
|
|
1902
|
+
let source_allows = match self.source.as_str() {
|
|
1903
|
+
"leader_process" => self.inherited,
|
|
1904
|
+
"runtime_config" => self.explicit_yes_confirmed,
|
|
1905
|
+
_ => false,
|
|
1906
|
+
};
|
|
1907
|
+
source_allows
|
|
1908
|
+
&& (!self.worker_capability_above_leader
|
|
1909
|
+
|| (self.source == "runtime_config" && self.explicit_yes_confirmed))
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
fn runtime_approval_policy_from_agent(agent: &Value) -> RuntimeApprovalPolicy {
|
|
1914
|
+
let policy = agent
|
|
1915
|
+
.get("effective_approval_policy")
|
|
1916
|
+
.and_then(Value::as_object);
|
|
1917
|
+
RuntimeApprovalPolicy {
|
|
1918
|
+
enabled: policy
|
|
1919
|
+
.and_then(|p| p.get("enabled"))
|
|
1920
|
+
.and_then(Value::as_bool)
|
|
1921
|
+
.unwrap_or(false),
|
|
1922
|
+
source: policy
|
|
1923
|
+
.and_then(|p| p.get("source"))
|
|
1924
|
+
.and_then(Value::as_str)
|
|
1925
|
+
.unwrap_or("disabled")
|
|
1926
|
+
.to_string(),
|
|
1927
|
+
inherited: policy
|
|
1928
|
+
.and_then(|p| p.get("inherited"))
|
|
1929
|
+
.and_then(Value::as_bool)
|
|
1930
|
+
.unwrap_or(false),
|
|
1931
|
+
explicit_yes_confirmed: policy
|
|
1932
|
+
.and_then(|p| p.get("explicit_yes_confirmed"))
|
|
1933
|
+
.and_then(Value::as_bool)
|
|
1934
|
+
.unwrap_or(false),
|
|
1935
|
+
worker_capability_above_leader: policy
|
|
1936
|
+
.and_then(|p| p.get("worker_capability_above_leader"))
|
|
1937
|
+
.and_then(Value::as_bool)
|
|
1938
|
+
.unwrap_or(false),
|
|
1939
|
+
}
|
|
1787
1940
|
}
|
|
1788
1941
|
|
|
1789
1942
|
fn awaiting_human_confirm_payload(
|
|
@@ -152,6 +152,10 @@ pub enum OrphanReason {
|
|
|
152
152
|
WorkspaceAlive,
|
|
153
153
|
/// 无法解析 cmdline → 无法判定 workspace。
|
|
154
154
|
CmdlineUnparsed,
|
|
155
|
+
/// workspace 仍存在,但 coordinator metadata 不指向该 pid/schema。
|
|
156
|
+
MetadataMismatch,
|
|
157
|
+
/// ps 列表仍有命令残留,但 pid 已不再存活。
|
|
158
|
+
PidNotRunning,
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
// ===========================================================================
|
|
@@ -553,20 +557,28 @@ pub struct DeadlockAlert {
|
|
|
553
557
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
554
558
|
pub struct CompactionResult {
|
|
555
559
|
pub agent_id: AgentId,
|
|
556
|
-
pub
|
|
560
|
+
pub provider: Option<Provider>,
|
|
561
|
+
pub observed: bool,
|
|
562
|
+
pub reason: Option<String>,
|
|
563
|
+
pub recommendation: Option<String>,
|
|
557
564
|
}
|
|
558
565
|
|
|
559
566
|
/// `detect_session_drift` 结果(`messaging/session_drift.py`,step 11 拥有)。**PLACEHOLDER**。
|
|
560
567
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
561
568
|
pub struct SessionDriftResult {
|
|
562
569
|
pub agent_id: AgentId,
|
|
563
|
-
pub
|
|
570
|
+
pub stored_session_id: Option<String>,
|
|
571
|
+
pub observed_session_id: Option<String>,
|
|
572
|
+
pub status: String,
|
|
564
573
|
}
|
|
565
574
|
|
|
566
575
|
/// `detect_leader_api_errors` 结果(`messaging/leader_api_errors.py`,step 11 拥有)。**PLACEHOLDER**。
|
|
567
576
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
568
577
|
pub struct LeaderApiError {
|
|
569
|
-
pub
|
|
578
|
+
pub provider: Option<Provider>,
|
|
579
|
+
pub pane_id: Option<String>,
|
|
580
|
+
pub fingerprint: String,
|
|
581
|
+
pub message: String,
|
|
570
582
|
}
|
|
571
583
|
|
|
572
584
|
/// `_collect_results_and_notify_watchers` 结果(`results.py:430`,step 11 拥有)。**PLACEHOLDER**。
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
//! slice 2(待做):列**顺序**漂移的整表 rebuild(`ensure_table_layout`/`_rebuild_tables`)
|
|
6
6
|
//! 与 `schema_diagnosis`,验 legacy_team_db_fixture + schema_migration 契约。
|
|
7
7
|
|
|
8
|
+
use std::time::Duration;
|
|
9
|
+
|
|
8
10
|
use rusqlite::Connection;
|
|
9
11
|
|
|
10
12
|
use crate::db::DbError;
|
|
@@ -66,11 +68,44 @@ const LEADER_NOTIFICATION_LOG_COLUMNS: &[&str] = &[
|
|
|
66
68
|
|
|
67
69
|
/// 打开 `team.db` 并设 pragmas(`core.py:60-61`:busy_timeout=30000 + WAL)。
|
|
68
70
|
pub fn open_db(path: &std::path::Path) -> Result<Connection, DbError> {
|
|
69
|
-
let
|
|
70
|
-
conn
|
|
71
|
+
let existed = path.exists();
|
|
72
|
+
let conn = retry_sqlite(|| Connection::open(path))?;
|
|
73
|
+
conn.busy_timeout(Duration::from_millis(30_000))?;
|
|
74
|
+
if !existed {
|
|
75
|
+
retry_sqlite(|| conn.execute_batch("PRAGMA journal_mode=WAL;"))?;
|
|
76
|
+
}
|
|
71
77
|
Ok(conn)
|
|
72
78
|
}
|
|
73
79
|
|
|
80
|
+
fn retry_sqlite<T>(mut op: impl FnMut() -> rusqlite::Result<T>) -> Result<T, DbError> {
|
|
81
|
+
let mut last_error = None;
|
|
82
|
+
for attempt in 0..8 {
|
|
83
|
+
match op() {
|
|
84
|
+
Ok(value) => return Ok(value),
|
|
85
|
+
Err(error) if sqlite_busy_or_locked(&error) => {
|
|
86
|
+
last_error = Some(error);
|
|
87
|
+
std::thread::sleep(Duration::from_millis(25 * (attempt + 1)));
|
|
88
|
+
}
|
|
89
|
+
Err(error) => return Err(error.into()),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
match last_error {
|
|
93
|
+
Some(error) => Err(error.into()),
|
|
94
|
+
None => Err(DbError::Schema("sqlite retry exhausted without an error".to_string())),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn sqlite_busy_or_locked(error: &rusqlite::Error) -> bool {
|
|
99
|
+
matches!(
|
|
100
|
+
error,
|
|
101
|
+
rusqlite::Error::SqliteFailure(err, _)
|
|
102
|
+
if matches!(
|
|
103
|
+
err.code,
|
|
104
|
+
rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
74
109
|
/// `pragma table_info(table)` 的列名序(`schema_migration.py:table_layout`)。
|
|
75
110
|
pub fn table_layout(conn: &Connection, table: &str) -> Result<Vec<String>, DbError> {
|
|
76
111
|
// table 来自固定常量名,非用户输入 → format 安全。
|