@team-agent/installer 0.3.4 → 0.3.6
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 +8 -0
- package/crates/team-agent/src/cli/diagnose.rs +51 -10
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +217 -80
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/status_port.rs +135 -7
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +8 -1
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +39 -0
- package/crates/team-agent/src/cli/types.rs +5 -1
- package/crates/team-agent/src/coordinator/backoff.rs +57 -9
- package/crates/team-agent/src/coordinator/health.rs +65 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +28 -16
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +87 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/tick.rs +195 -43
- package/crates/team-agent/src/leader/helpers.rs +2 -0
- package/crates/team-agent/src/leader/rediscover.rs +1 -0
- package/crates/team-agent/src/leader/start.rs +9 -1
- package/crates/team-agent/src/leader/takeover.rs +18 -1
- package/crates/team-agent/src/lifecycle/launch.rs +434 -29
- package/crates/team-agent/src/lifecycle/profile_launch.rs +110 -4
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +4 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +19 -2
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +44 -9
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +2 -1
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +14 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +15 -1
- package/crates/team-agent/src/mcp_server/tools.rs +65 -9
- package/crates/team-agent/src/mcp_server/wire.rs +2 -1
- package/crates/team-agent/src/message_store.rs +80 -0
- package/crates/team-agent/src/messaging/results.rs +76 -5
- package/crates/team-agent/src/messaging/send.rs +3 -1
- package/crates/team-agent/src/messaging/types.rs +15 -1
- package/crates/team-agent/src/messaging/watchers.rs +68 -30
- package/crates/team-agent/src/model/enums.rs +7 -1
- package/crates/team-agent/src/model/permissions.rs +7 -0
- package/crates/team-agent/src/model/spec.rs +3 -1
- package/crates/team-agent/src/provider/adapter.rs +472 -7
- package/crates/team-agent/src/provider/classify.rs +6 -2
- package/crates/team-agent/src/provider/faults.rs +3 -2
- package/crates/team-agent/src/provider/startup_prompt.rs +25 -7
- package/crates/team-agent/src/provider/types.rs +11 -0
- package/crates/team-agent/src/session_capture.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +95 -19
- package/crates/team-agent/src/tmux_backend/tests.rs +8 -7
- package/crates/team-agent/src/tmux_backend.rs +80 -6
- package/crates/team-agent/src/transport.rs +32 -0
- package/npm/install.mjs +21 -0
- package/package.json +4 -4
|
@@ -184,7 +184,7 @@ impl Coordinator {
|
|
|
184
184
|
let mut state = crate::state::persist::load_runtime_state(self.workspace.as_path())?;
|
|
185
185
|
let store = crate::message_store::MessageStore::open(self.workspace.as_path())?;
|
|
186
186
|
let event_log = EventLog::new(self.workspace.as_path());
|
|
187
|
-
increment_coordinator_tick_iteration_count(&
|
|
187
|
+
increment_coordinator_tick_iteration_count(&self.workspace);
|
|
188
188
|
|
|
189
189
|
self.record_step("tmux_session_gate");
|
|
190
190
|
if let Some(session_name) = state
|
|
@@ -232,8 +232,37 @@ impl Coordinator {
|
|
|
232
232
|
self.handle_runtime_approval_prompts(&mut state, &event_log)?;
|
|
233
233
|
|
|
234
234
|
self.record_step("sync_health");
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
// P5 (C-P5-1, N3): ONE pane snapshot per tick, shared by sync_health and the
|
|
236
|
+
// abnormal-exit pass (same-tick reuse only — the snapshot does not outlive
|
|
237
|
+
// this tick; every tick re-reads).
|
|
238
|
+
let pane_snapshot = self.transport.list_targets().unwrap_or_default();
|
|
239
|
+
let captures_by_agent =
|
|
240
|
+
self.sync_agent_health(&mut state, &store, &event_log, &pane_snapshot)?;
|
|
241
|
+
// C-3-4 cr verdict — copilot 一期 classify→None(Unknown);为防 silent,
|
|
242
|
+
// tick 每次发现 copilot agent(从 state.agents 直接扫,不依赖 captures —
|
|
243
|
+
// 离线/未起 tmux 场景仍能写)就发 `provider.classify.unsupported` 事件
|
|
244
|
+
// (字面 reason=`phase1_unknown_pending_sample`,含 provider="copilot" + "classify"
|
|
245
|
+
// 串)。二期接 sqlite turns 表后这条删/降级,届时改 reason 区分。
|
|
246
|
+
if let Some(agents) = state.get("agents").and_then(Value::as_object) {
|
|
247
|
+
for (agent_id, agent) in agents {
|
|
248
|
+
let is_copilot = agent
|
|
249
|
+
.get("provider")
|
|
250
|
+
.and_then(Value::as_str)
|
|
251
|
+
.and_then(parse_provider)
|
|
252
|
+
.is_some_and(|p| matches!(p, crate::model::enums::Provider::Copilot));
|
|
253
|
+
if is_copilot {
|
|
254
|
+
let _ = event_log.write(
|
|
255
|
+
"provider.classify.unsupported",
|
|
256
|
+
serde_json::json!({
|
|
257
|
+
"provider": "copilot",
|
|
258
|
+
"agent_id": agent_id,
|
|
259
|
+
"reason": "phase1_unknown_pending_sample",
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
self.detect_abnormal_exits(&mut state, &event_log, &pane_snapshot)?;
|
|
237
266
|
|
|
238
267
|
self.record_step("deliver_pending");
|
|
239
268
|
let delivered = crate::messaging::deliver_pending_messages(
|
|
@@ -353,13 +382,17 @@ impl Coordinator {
|
|
|
353
382
|
state: &mut Value,
|
|
354
383
|
store: &crate::message_store::MessageStore,
|
|
355
384
|
event_log: &EventLog,
|
|
385
|
+
pane_infos: &[crate::transport::PaneInfo],
|
|
356
386
|
) -> Result<BTreeMap<AgentId, CapturedRuntimeFact>, TickError> {
|
|
357
387
|
let mut captures = BTreeMap::new();
|
|
358
388
|
let snapshot = state.clone();
|
|
359
389
|
let team = crate::state::projection::team_state_key(&snapshot);
|
|
360
390
|
let team_key = Some(crate::model::ids::TeamKey::new(team.clone()));
|
|
361
391
|
let session_name = state.get("session_name").and_then(Value::as_str).map(str::to_string);
|
|
362
|
-
|
|
392
|
+
// P5 (C-P5-2): one list-windows per SESSION per tick — memoized across the
|
|
393
|
+
// agent loop instead of one fork per agent.
|
|
394
|
+
let mut windows_by_session: BTreeMap<String, Result<Vec<crate::transport::WindowName>, String>> =
|
|
395
|
+
BTreeMap::new();
|
|
363
396
|
let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
|
|
364
397
|
return Ok(captures);
|
|
365
398
|
};
|
|
@@ -367,15 +400,21 @@ impl Coordinator {
|
|
|
367
400
|
let Some((session, window, target)) = capture_window_target(agent, session_name.as_deref()) else {
|
|
368
401
|
continue;
|
|
369
402
|
};
|
|
370
|
-
let windows = match
|
|
371
|
-
|
|
403
|
+
let windows = match windows_by_session
|
|
404
|
+
.entry(session.as_str().to_string())
|
|
405
|
+
.or_insert_with(|| {
|
|
406
|
+
self.transport
|
|
407
|
+
.list_windows(&session)
|
|
408
|
+
.map_err(|error| error.to_string())
|
|
409
|
+
}) {
|
|
410
|
+
Ok(windows) => windows.clone(),
|
|
372
411
|
Err(error) => {
|
|
373
412
|
event_log.write(
|
|
374
413
|
"coordinator.agent_capture_failed",
|
|
375
414
|
serde_json::json!({
|
|
376
415
|
"agent_id": agent_id,
|
|
377
416
|
"target": format!("{target:?}"),
|
|
378
|
-
"error": error.
|
|
417
|
+
"error": error.clone(),
|
|
379
418
|
}),
|
|
380
419
|
)?;
|
|
381
420
|
continue;
|
|
@@ -408,18 +447,38 @@ impl Coordinator {
|
|
|
408
447
|
let current_command = agent
|
|
409
448
|
.get("pane_current_command")
|
|
410
449
|
.or_else(|| agent.get("current_command"))
|
|
411
|
-
.and_then(Value::as_str)
|
|
412
|
-
|
|
450
|
+
.and_then(Value::as_str)
|
|
451
|
+
.map(str::to_string);
|
|
452
|
+
// Python approvals/status.py:68-73 — last_output_at advances ONLY when the
|
|
453
|
+
// scrollback sha256 digest changed (last_output_hash gate), and it is
|
|
454
|
+
// refreshed BEFORE classification (the classifier sees the updated value).
|
|
455
|
+
// A non-empty but UNCHANGED capture must not dirty the state every tick
|
|
456
|
+
// (P3 umbrella: steady second tick is a zero state write).
|
|
457
|
+
let output_advanced =
|
|
458
|
+
!captured.text.is_empty() && scrollback_digest_advanced(agent, &captured.text);
|
|
459
|
+
if output_advanced {
|
|
460
|
+
if let Some(agent_obj) = agent.as_object_mut() {
|
|
461
|
+
agent_obj.insert(
|
|
462
|
+
"last_output_at".to_string(),
|
|
463
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
let last_output_at_now = agent
|
|
468
|
+
.get("last_output_at")
|
|
469
|
+
.and_then(Value::as_str)
|
|
470
|
+
.map(str::to_string);
|
|
413
471
|
let activity = crate::messaging::classify_agent_activity(
|
|
414
472
|
&snapshot,
|
|
415
473
|
&captured.text,
|
|
416
474
|
pane_in_mode,
|
|
417
|
-
current_command,
|
|
418
|
-
|
|
475
|
+
current_command.as_deref(),
|
|
476
|
+
last_output_at_now.as_deref(),
|
|
419
477
|
);
|
|
420
|
-
|
|
478
|
+
write_activity(agent, &activity, false);
|
|
479
|
+
let last_output_at = last_output_at_now;
|
|
421
480
|
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,
|
|
481
|
+
let pane_info = matching_capture_pane_info(agent, &session, &window, pane_infos);
|
|
423
482
|
let pane_id = pane_info
|
|
424
483
|
.as_ref()
|
|
425
484
|
.map(|info| info.pane_id.clone())
|
|
@@ -488,11 +547,11 @@ impl Coordinator {
|
|
|
488
547
|
&self,
|
|
489
548
|
state: &mut Value,
|
|
490
549
|
event_log: &EventLog,
|
|
550
|
+
targets: &[crate::transport::PaneInfo],
|
|
491
551
|
) -> Result<(), TickError> {
|
|
492
552
|
let snapshot = state.clone();
|
|
493
553
|
let team = crate::state::projection::team_state_key(&snapshot);
|
|
494
554
|
let session_name = snapshot.get("session_name").and_then(Value::as_str);
|
|
495
|
-
let targets = self.transport.list_targets().unwrap_or_default();
|
|
496
555
|
for agent in abnormal_watch_agents(&snapshot) {
|
|
497
556
|
let rollout_path = resolve_agent_rollout_path(self.workspace.as_path(), &agent.rollout_path);
|
|
498
557
|
let metadata = match std::fs::metadata(&rollout_path) {
|
|
@@ -508,7 +567,21 @@ impl Coordinator {
|
|
|
508
567
|
};
|
|
509
568
|
let size = metadata.len();
|
|
510
569
|
let mtime_ns = metadata_mtime_ns(&metadata);
|
|
511
|
-
|
|
570
|
+
// P1 (C-P1-2/3): (size, mtime_ns) pair gate — an unchanged transcript is not
|
|
571
|
+
// read at all (live sample: 332MB whole-file read per agent per 2s tick).
|
|
572
|
+
// ANY field change (including a size shrink / truncate) falls through to the
|
|
573
|
+
// re-read below.
|
|
574
|
+
if let (Some(mtime), Some(stored)) =
|
|
575
|
+
(mtime_ns, abnormal_watch_stored_metadata(&snapshot, &agent.agent_id))
|
|
576
|
+
{
|
|
577
|
+
if stored == (size, mtime) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// P1 (C-P1-1): bounded tail read — the abnormal decision only consumes the
|
|
582
|
+
// LATEST transcript record; window matches Python `_TAIL_BYTES` (131072,
|
|
583
|
+
// idle_takeover_wiring.py:13), never less.
|
|
584
|
+
let text = match read_tail_text(&rollout_path, ABNORMAL_TAIL_BYTES) {
|
|
512
585
|
Ok(text) => text,
|
|
513
586
|
Err(error) => {
|
|
514
587
|
upsert_abnormal_watch(
|
|
@@ -522,7 +595,7 @@ impl Coordinator {
|
|
|
522
595
|
let liveness = agent_process_liveness(
|
|
523
596
|
&agent,
|
|
524
597
|
session_name,
|
|
525
|
-
|
|
598
|
+
targets,
|
|
526
599
|
self.transport.as_ref(),
|
|
527
600
|
);
|
|
528
601
|
let fact = crate::provider::latest_explicit_error_fact(agent.provider, &text);
|
|
@@ -643,7 +716,21 @@ impl Coordinator {
|
|
|
643
716
|
continue;
|
|
644
717
|
};
|
|
645
718
|
let adapter = self.provider_registry.adapter_for(provider);
|
|
646
|
-
let
|
|
719
|
+
let outcome =
|
|
720
|
+
adapter.handle_startup_prompts_outcome(self.transport.as_ref(), &target, 1, 0.0);
|
|
721
|
+
// swallow batch 2 ② (A1): an unobservable pane is a surfaced failure, not a
|
|
722
|
+
// silent "no prompts" — the agent's startup_prompts state stays un-handled.
|
|
723
|
+
if let Some(error) = &outcome.capture_error {
|
|
724
|
+
let _ = event_log.write(
|
|
725
|
+
"provider.startup_prompt_failed",
|
|
726
|
+
serde_json::json!({
|
|
727
|
+
"agent_id": agent_id,
|
|
728
|
+
"target": format!("{target:?}"),
|
|
729
|
+
"error": error,
|
|
730
|
+
}),
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
let handled = outcome.handled;
|
|
647
734
|
if handled.is_empty() {
|
|
648
735
|
continue;
|
|
649
736
|
}
|
|
@@ -791,7 +878,21 @@ impl Coordinator {
|
|
|
791
878
|
.into_iter()
|
|
792
879
|
.filter_map(runtime_approval_key)
|
|
793
880
|
.collect::<Vec<_>>();
|
|
794
|
-
|
|
881
|
+
// A-6 / Python approvals/runtime_prompts.py:21-43: prompts are handled
|
|
882
|
+
// per-agent with run_cmd(check=False) — one agent's tmux failure must
|
|
883
|
+
// not abort the whole tick for the rest.
|
|
884
|
+
if let Err(error) = self.transport.send_keys(&target, &keys) {
|
|
885
|
+
event_log.write(
|
|
886
|
+
"runtime_approval.send_keys_failed",
|
|
887
|
+
serde_json::json!({
|
|
888
|
+
"agent_id": agent_id,
|
|
889
|
+
"target": format!("{target:?}"),
|
|
890
|
+
"tool": prompt.tool,
|
|
891
|
+
"error": error.to_string(),
|
|
892
|
+
}),
|
|
893
|
+
)?;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
795
896
|
let after = self
|
|
796
897
|
.transport
|
|
797
898
|
.capture(&target, crate::transport::CaptureRange::Tail(80))
|
|
@@ -992,12 +1093,10 @@ impl Coordinator {
|
|
|
992
1093
|
/// `message_store_schema_health`(`lifecycle.py:197`)。DB 列兼容门:区分 pre-init 必需列缺失
|
|
993
1094
|
/// (拒启)vs migratable 列缺失(可迁移)。`advanced repair-state --schema` 用其 action hint。
|
|
994
1095
|
pub fn schema_health(&self) -> SchemaHealth {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
action: None,
|
|
1000
|
-
}
|
|
1096
|
+
// A-8: the gate must inspect the REAL team.db (Python lifecycle.py:197+
|
|
1097
|
+
// message_store_schema_health); a hardcoded ok:true left the card §89
|
|
1098
|
+
// restart_incompatible door permanently dead.
|
|
1099
|
+
super::health::message_store_schema_health(&self.workspace)
|
|
1001
1100
|
}
|
|
1002
1101
|
|
|
1003
1102
|
fn record_step(&self, step: &'static str) {
|
|
@@ -1092,27 +1191,27 @@ impl TurnStateClassifier for ProviderTurnClassifier {
|
|
|
1092
1191
|
}
|
|
1093
1192
|
}
|
|
1094
1193
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1194
|
+
/// P3 (C-P3-1, N1): the tick counter is a transient diagnostic, NOT source-of-truth
|
|
1195
|
+
/// state — keeping it in state.json made EVERY tick dirty and defeated both save
|
|
1196
|
+
/// short-circuits. It lives in its own metadata file; old state files still carrying
|
|
1197
|
+
/// `coordinator.coordinator_tick_iteration_count` load fine (read-compat, C-P3-3) —
|
|
1198
|
+
/// new versions simply stop writing it.
|
|
1199
|
+
fn increment_coordinator_tick_iteration_count(workspace: &WorkspacePath) {
|
|
1200
|
+
let path =
|
|
1201
|
+
crate::model::paths::runtime_dir(workspace.as_path()).join("coordinator_tick.json");
|
|
1202
|
+
let next = std::fs::read_to_string(&path)
|
|
1203
|
+
.ok()
|
|
1204
|
+
.and_then(|text| serde_json::from_str::<Value>(&text).ok())
|
|
1205
|
+
.and_then(|value| {
|
|
1206
|
+
value
|
|
1207
|
+
.get("coordinator_tick_iteration_count")
|
|
1208
|
+
.and_then(Value::as_u64)
|
|
1209
|
+
})
|
|
1111
1210
|
.unwrap_or(0)
|
|
1112
1211
|
.saturating_add(1);
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
serde_json::json!(next),
|
|
1212
|
+
let _ = std::fs::write(
|
|
1213
|
+
&path,
|
|
1214
|
+
serde_json::json!({"coordinator_tick_iteration_count": next}).to_string(),
|
|
1116
1215
|
);
|
|
1117
1216
|
}
|
|
1118
1217
|
|
|
@@ -1143,6 +1242,7 @@ fn provider_wire(provider: crate::model::enums::Provider) -> &'static str {
|
|
|
1143
1242
|
crate::model::enums::Provider::Claude => "claude",
|
|
1144
1243
|
crate::model::enums::Provider::ClaudeCode => "claude_code",
|
|
1145
1244
|
crate::model::enums::Provider::Codex => "codex",
|
|
1245
|
+
crate::model::enums::Provider::Copilot => "copilot",
|
|
1146
1246
|
crate::model::enums::Provider::GeminiCli => "gemini_cli",
|
|
1147
1247
|
crate::model::enums::Provider::Fake => "fake",
|
|
1148
1248
|
}
|
|
@@ -1430,6 +1530,7 @@ fn provider_command_matches(provider: crate::model::enums::Provider, command: &s
|
|
|
1430
1530
|
lower.contains("claude")
|
|
1431
1531
|
}
|
|
1432
1532
|
crate::model::enums::Provider::Codex => lower.contains("codex"),
|
|
1533
|
+
crate::model::enums::Provider::Copilot => lower.contains("copilot"),
|
|
1433
1534
|
crate::model::enums::Provider::GeminiCli => lower.contains("gemini"),
|
|
1434
1535
|
crate::model::enums::Provider::Fake => lower.contains("fake"),
|
|
1435
1536
|
}
|
|
@@ -1559,6 +1660,35 @@ fn abnormal_last_check_key(state: &Value, agent_id: &str) -> Option<String> {
|
|
|
1559
1660
|
abnormal_watch_str(state, agent_id, "last_check_key")
|
|
1560
1661
|
}
|
|
1561
1662
|
|
|
1663
|
+
/// P1: Python `_TAIL_BYTES` parity (idle_takeover_wiring.py:13) — RS must not read less.
|
|
1664
|
+
const ABNORMAL_TAIL_BYTES: u64 = 131_072;
|
|
1665
|
+
|
|
1666
|
+
/// P1: bounded tail read; a partial first line is harmless (the consumer only parses
|
|
1667
|
+
/// the latest complete JSONL record) and lossy UTF-8 keeps a mid-codepoint seek safe.
|
|
1668
|
+
fn read_tail_text(path: &Path, max_bytes: u64) -> std::io::Result<String> {
|
|
1669
|
+
use std::io::{Read, Seek, SeekFrom};
|
|
1670
|
+
let mut file = std::fs::File::open(path)?;
|
|
1671
|
+
let len = file.metadata()?.len();
|
|
1672
|
+
if len > max_bytes {
|
|
1673
|
+
file.seek(SeekFrom::Start(len - max_bytes))?;
|
|
1674
|
+
}
|
|
1675
|
+
let mut bytes = Vec::new();
|
|
1676
|
+
file.read_to_end(&mut bytes)?;
|
|
1677
|
+
Ok(String::from_utf8_lossy(&bytes).into_owned())
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/// P1: the previous tick's `(size, mtime_ns)` pair from the abnormal watch payload.
|
|
1681
|
+
fn abnormal_watch_stored_metadata(state: &Value, agent_id: &str) -> Option<(u64, u64)> {
|
|
1682
|
+
let watch = state
|
|
1683
|
+
.get("coordinator")?
|
|
1684
|
+
.get("abnormal_exit_watch")?
|
|
1685
|
+
.get(agent_id)?;
|
|
1686
|
+
Some((
|
|
1687
|
+
watch.get("size")?.as_u64()?,
|
|
1688
|
+
watch.get("mtime_ns")?.as_u64()?,
|
|
1689
|
+
))
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1562
1692
|
fn abnormal_watch_str(state: &Value, agent_id: &str, field: &str) -> Option<String> {
|
|
1563
1693
|
state
|
|
1564
1694
|
.get("coordinator")
|
|
@@ -1791,6 +1921,7 @@ fn parse_provider(raw: &str) -> Option<crate::model::enums::Provider> {
|
|
|
1791
1921
|
"claude" => Some(crate::model::enums::Provider::Claude),
|
|
1792
1922
|
"claude_code" => Some(crate::model::enums::Provider::ClaudeCode),
|
|
1793
1923
|
"codex" => Some(crate::model::enums::Provider::Codex),
|
|
1924
|
+
"copilot" => Some(crate::model::enums::Provider::Copilot),
|
|
1794
1925
|
"gemini_cli" => Some(crate::model::enums::Provider::GeminiCli),
|
|
1795
1926
|
"fake" => Some(crate::model::enums::Provider::Fake),
|
|
1796
1927
|
_ => None,
|
|
@@ -2081,6 +2212,27 @@ fn clear_awaiting_human_confirm(agent: &mut Value) {
|
|
|
2081
2212
|
}
|
|
2082
2213
|
}
|
|
2083
2214
|
|
|
2215
|
+
/// Python approvals/status.py:68-72 — sha256 the scrollback, compare to the stored
|
|
2216
|
+
/// `last_output_hash`; only a CHANGED digest counts as advanced output (and stores
|
|
2217
|
+
/// the new digest).
|
|
2218
|
+
fn scrollback_digest_advanced(agent: &mut Value, text: &str) -> bool {
|
|
2219
|
+
use sha2::Digest;
|
|
2220
|
+
let mut hasher = sha2::Sha256::new();
|
|
2221
|
+
hasher.update(text.as_bytes());
|
|
2222
|
+
let digest = format!("{:x}", hasher.finalize());
|
|
2223
|
+
let unchanged = agent
|
|
2224
|
+
.get("last_output_hash")
|
|
2225
|
+
.and_then(Value::as_str)
|
|
2226
|
+
.is_some_and(|stored| stored == digest);
|
|
2227
|
+
if unchanged {
|
|
2228
|
+
return false;
|
|
2229
|
+
}
|
|
2230
|
+
if let Some(obj) = agent.as_object_mut() {
|
|
2231
|
+
obj.insert("last_output_hash".to_string(), serde_json::json!(digest));
|
|
2232
|
+
}
|
|
2233
|
+
true
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2084
2236
|
fn write_activity(
|
|
2085
2237
|
agent: &mut Value,
|
|
2086
2238
|
activity: &crate::messaging::AgentActivity,
|
|
@@ -14,6 +14,7 @@ pub(crate) fn provider_wire(provider: Provider) -> &'static str {
|
|
|
14
14
|
Provider::Claude => "claude",
|
|
15
15
|
Provider::ClaudeCode => "claude_code",
|
|
16
16
|
Provider::Codex => "codex",
|
|
17
|
+
Provider::Copilot => "copilot",
|
|
17
18
|
Provider::GeminiCli => "gemini_cli",
|
|
18
19
|
Provider::Fake => "fake",
|
|
19
20
|
}
|
|
@@ -24,6 +25,7 @@ pub(crate) fn parse_provider(s: &str) -> Option<Provider> {
|
|
|
24
25
|
"claude" => Some(Provider::Claude),
|
|
25
26
|
"claude_code" => Some(Provider::ClaudeCode),
|
|
26
27
|
"codex" => Some(Provider::Codex),
|
|
28
|
+
"copilot" => Some(Provider::Copilot),
|
|
27
29
|
"gemini_cli" => Some(Provider::GeminiCli),
|
|
28
30
|
"fake" => Some(Provider::Fake),
|
|
29
31
|
_ => None,
|
|
@@ -898,6 +898,7 @@ fn provider_wire(provider: Provider) -> &'static str {
|
|
|
898
898
|
Provider::Claude => "claude",
|
|
899
899
|
Provider::ClaudeCode => "claude_code",
|
|
900
900
|
Provider::Codex => "codex",
|
|
901
|
+
Provider::Copilot => "copilot",
|
|
901
902
|
Provider::GeminiCli => "gemini_cli",
|
|
902
903
|
Provider::Fake => "fake",
|
|
903
904
|
}
|
|
@@ -187,6 +187,10 @@ pub fn execute_leader_plan(
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/// B5: the deterministic leader-session naming prefix IS the ownership truth source —
|
|
191
|
+
/// shutdown's socket teardown spares sessions carrying it (no separate registry).
|
|
192
|
+
pub const LEADER_SESSION_PREFIX: &str = "team-agent-leader-";
|
|
193
|
+
|
|
190
194
|
/// `leader_session_name`(card §48;`__init__.py:186`)。确定派生 tmux session 名
|
|
191
195
|
/// `team-agent-leader-<provider>-<folder>-<sha1[:8]>`(workspace.resolve() 的 sha1 前 8 hex)。
|
|
192
196
|
pub fn leader_session_name(provider: Provider, workspace: &Path) -> SessionName {
|
|
@@ -198,7 +202,7 @@ pub fn leader_session_name(provider: Provider, workspace: &Path) -> SessionName
|
|
|
198
202
|
let folder = sanitize_session_folder(folder_raw);
|
|
199
203
|
let hash = sha1_hex_prefix(resolved.to_string_lossy().as_bytes(), 8);
|
|
200
204
|
SessionName::new(format!(
|
|
201
|
-
"
|
|
205
|
+
"{LEADER_SESSION_PREFIX}{}-{folder}-{hash}",
|
|
202
206
|
provider_wire(provider)
|
|
203
207
|
))
|
|
204
208
|
}
|
|
@@ -309,6 +313,10 @@ fn provider_command_name(provider: Provider) -> &'static str {
|
|
|
309
313
|
match provider {
|
|
310
314
|
Provider::Claude | Provider::ClaudeCode => "claude",
|
|
311
315
|
Provider::Codex => "codex",
|
|
316
|
+
// §B leader 入口接缝(设计 design.md line 40):`team-agent copilot` 启 leader
|
|
317
|
+
// 即 spawn 真 copilot 命令;B5 session 名前缀 `team-agent-leader-copilot-*`
|
|
318
|
+
// (leader/start.rs:192-204 派生)自动覆盖前缀保护。
|
|
319
|
+
Provider::Copilot => "copilot",
|
|
312
320
|
Provider::GeminiCli => "gemini",
|
|
313
321
|
Provider::Fake => "fake",
|
|
314
322
|
}
|
|
@@ -130,7 +130,6 @@ pub fn evaluate_takeover_reminder(
|
|
|
130
130
|
nodes: &[IdleNode],
|
|
131
131
|
arm_state: &Value,
|
|
132
132
|
) -> Result<TakeoverReminderResult, LeaderError> {
|
|
133
|
-
let _ = arm_state;
|
|
134
133
|
if nodes.is_empty() {
|
|
135
134
|
return Ok(TakeoverReminderResult {
|
|
136
135
|
should_ping: false,
|
|
@@ -147,6 +146,24 @@ pub fn evaluate_takeover_reminder(
|
|
|
147
146
|
reason: Some(format!("node_{}", turn_state_wire(blocking.state))),
|
|
148
147
|
});
|
|
149
148
|
}
|
|
149
|
+
// idle_predicate.py:55-62 (C1): only a real worker turn-open arms the watch — an
|
|
150
|
+
// un-armed monitor must never ping. The facade honors both its own write-side key
|
|
151
|
+
// (`armed`, record_turn_open_after_delivery) and the classify-layer monitor_state
|
|
152
|
+
// key (`opened_worker_turn_since_ack`); debounce/episode tiers stay at the classify
|
|
153
|
+
// layer (provider/classify.rs evaluate_takeover_reminder).
|
|
154
|
+
let armed = arm_state.get("armed").and_then(Value::as_bool) == Some(true)
|
|
155
|
+
|| arm_state
|
|
156
|
+
.get("opened_worker_turn_since_ack")
|
|
157
|
+
.and_then(Value::as_bool)
|
|
158
|
+
== Some(true);
|
|
159
|
+
if !armed {
|
|
160
|
+
return Ok(TakeoverReminderResult {
|
|
161
|
+
should_ping: false,
|
|
162
|
+
message: None,
|
|
163
|
+
interrupted_nodes: Vec::new(),
|
|
164
|
+
reason: Some("not_armed_no_worker_turn".to_string()),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
150
167
|
let interrupted_nodes = nodes
|
|
151
168
|
.iter()
|
|
152
169
|
.filter(|n| n.state == TurnState::IdleInterrupted)
|