@team-agent/installer 0.5.100 → 0.5.102
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/diagnose.rs +29 -7
- package/crates/team-agent/src/cli/mod.rs +24 -8
- package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
- package/crates/team-agent/src/compiler/tests.rs +20 -23
- package/crates/team-agent/src/compiler.rs +52 -65
- package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
- package/crates/team-agent/src/leader/start.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +11 -3
- package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +115 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +8 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +10 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
- package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
- package/crates/team-agent/src/mcp_server/mod.rs +7 -12
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
- package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
- package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
- package/crates/team-agent/src/mcp_server/tests.rs +4 -200
- package/crates/team-agent/src/mcp_server/tools.rs +5 -396
- package/crates/team-agent/src/mcp_server/types.rs +3 -36
- package/crates/team-agent/src/mcp_server/wire.rs +2 -223
- package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/scheduler.rs +2 -205
- package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
- package/crates/team-agent/src/messaging/types.rs +0 -20
- package/crates/team-agent/src/model/spec.rs +15 -18
- package/crates/team-agent/src/provider/types.rs +2 -0
- package/crates/team-agent/src/state/persist.rs +58 -2
- package/crates/team-agent/src/state/projection.rs +5 -1
- package/crates/team-agent/src/state/repository/tests.rs +1 -16
- package/crates/team-agent/src/state/repository.rs +126 -37
- package/crates/team-agent/src/transport/test_support.rs +23 -3
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +18 -47
- package/skills/team-agent/command-coverage.json +7 -0
- package/skills/team-agent/references/team-agent-operator.md +28 -118
- package/skills/team-agent/references/team-in-team.md +4 -19
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
|
@@ -25,19 +25,9 @@ use super::types::{
|
|
|
25
25
|
/// schema cannot drift from the enum. Returned verbatim by `tools/list`.
|
|
26
26
|
pub fn tools_contract() -> Vec<Value> {
|
|
27
27
|
let tools = [
|
|
28
|
-
McpTool::AssignTask,
|
|
29
28
|
McpTool::SendMessage,
|
|
30
29
|
McpTool::ReportResult,
|
|
31
|
-
McpTool::UpdateState,
|
|
32
30
|
McpTool::GetTeamStatus,
|
|
33
|
-
McpTool::StopAgent,
|
|
34
|
-
McpTool::ResetAgent,
|
|
35
|
-
McpTool::AddAgent,
|
|
36
|
-
McpTool::CloneAgent,
|
|
37
|
-
McpTool::ForkAgent,
|
|
38
|
-
McpTool::RequestHuman,
|
|
39
|
-
McpTool::StuckList,
|
|
40
|
-
McpTool::StuckCancel,
|
|
41
31
|
];
|
|
42
32
|
tools.into_iter().map(tool_contract).collect()
|
|
43
33
|
}
|
|
@@ -397,21 +387,11 @@ fn tool_contract(tool: McpTool) -> Value {
|
|
|
397
387
|
"Send a message to a teammate, the leader, or '*' for all other team members. mailbox=true stores durably without live injection; the default is live delivery.",
|
|
398
388
|
vec!["to", "content"],
|
|
399
389
|
),
|
|
400
|
-
McpTool::AssignTask => ("Assign or update a task in the team graph and deliver it to its assignee.", vec!["task"]),
|
|
401
390
|
McpTool::ReportResult => (
|
|
402
391
|
"Report task completion with a durable result envelope. Optional presentation routing controls live leader display, not persistence.",
|
|
403
392
|
Vec::new(),
|
|
404
393
|
),
|
|
405
|
-
McpTool::UpdateState => ("Append a note to team state and rewrite team_state.md.", vec!["note"]),
|
|
406
394
|
McpTool::GetTeamStatus => ("Return machine-readable team status.", Vec::new()),
|
|
407
|
-
McpTool::StopAgent => ("Stop a running worker.", vec!["agent_id"]),
|
|
408
|
-
McpTool::ResetAgent => ("Reset one worker to a fresh session.", vec!["agent_id", "discard_session"]),
|
|
409
|
-
McpTool::AddAgent => ("Add a first-class worker from a role file.", vec!["new_agent_id", "role_file_path"]),
|
|
410
|
-
McpTool::CloneAgent => ("Clone a worker role into a fresh provider session.", vec!["source_agent_id", "as_agent_id"]),
|
|
411
|
-
McpTool::ForkAgent => ("Fork a running worker.", vec!["source_agent_id", "as_agent_id"]),
|
|
412
|
-
McpTool::RequestHuman => ("Ask the leader or user for human input.", vec!["question"]),
|
|
413
|
-
McpTool::StuckList => ("List manually suppressed idle alerts.", Vec::new()),
|
|
414
|
-
McpTool::StuckCancel => ("Suppress repeated stuck or idle alerts.", vec!["agent_id"]),
|
|
415
395
|
};
|
|
416
396
|
serde_json::json!({
|
|
417
397
|
"name": tool.wire_name(),
|
|
@@ -428,14 +408,6 @@ fn tool_contract(tool: McpTool) -> Value {
|
|
|
428
408
|
fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
429
409
|
let mut properties = serde_json::Map::new();
|
|
430
410
|
match tool {
|
|
431
|
-
McpTool::AssignTask => {
|
|
432
|
-
insert_property(&mut properties, "task", task_property());
|
|
433
|
-
insert_property(
|
|
434
|
-
&mut properties,
|
|
435
|
-
"message",
|
|
436
|
-
string_property("Optional message to deliver with the task."),
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
411
|
McpTool::SendMessage => {
|
|
440
412
|
insert_property(
|
|
441
413
|
&mut properties,
|
|
@@ -506,108 +478,7 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
506
478
|
presentation_property("Optional durable presentation routing."),
|
|
507
479
|
);
|
|
508
480
|
}
|
|
509
|
-
McpTool::
|
|
510
|
-
insert_property(
|
|
511
|
-
&mut properties,
|
|
512
|
-
"note",
|
|
513
|
-
string_property("Note to append to team state."),
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
McpTool::GetTeamStatus | McpTool::StuckList => {}
|
|
517
|
-
McpTool::StopAgent => {
|
|
518
|
-
insert_property(
|
|
519
|
-
&mut properties,
|
|
520
|
-
"agent_id",
|
|
521
|
-
string_property("Agent id to stop."),
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
McpTool::ResetAgent => {
|
|
525
|
-
insert_property(
|
|
526
|
-
&mut properties,
|
|
527
|
-
"agent_id",
|
|
528
|
-
string_property("Agent id to reset."),
|
|
529
|
-
);
|
|
530
|
-
insert_property(
|
|
531
|
-
&mut properties,
|
|
532
|
-
"discard_session",
|
|
533
|
-
boolean_property("Whether to discard the existing provider session."),
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
McpTool::AddAgent => {
|
|
537
|
-
insert_property(
|
|
538
|
-
&mut properties,
|
|
539
|
-
"new_agent_id",
|
|
540
|
-
string_property("New agent id."),
|
|
541
|
-
);
|
|
542
|
-
insert_property(
|
|
543
|
-
&mut properties,
|
|
544
|
-
"role_file_path",
|
|
545
|
-
string_property("Workspace-relative role file path."),
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
McpTool::ForkAgent => {
|
|
549
|
-
insert_property(
|
|
550
|
-
&mut properties,
|
|
551
|
-
"source_agent_id",
|
|
552
|
-
string_property("Agent id to fork from."),
|
|
553
|
-
);
|
|
554
|
-
insert_property(
|
|
555
|
-
&mut properties,
|
|
556
|
-
"as_agent_id",
|
|
557
|
-
string_property("Agent id for the forked worker."),
|
|
558
|
-
);
|
|
559
|
-
insert_property(
|
|
560
|
-
&mut properties,
|
|
561
|
-
"label",
|
|
562
|
-
string_property("Optional display label."),
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
McpTool::CloneAgent => {
|
|
566
|
-
insert_property(
|
|
567
|
-
&mut properties,
|
|
568
|
-
"source_agent_id",
|
|
569
|
-
string_property("Agent id to clone from."),
|
|
570
|
-
);
|
|
571
|
-
insert_property(
|
|
572
|
-
&mut properties,
|
|
573
|
-
"as_agent_id",
|
|
574
|
-
string_property("Agent id for the cloned worker."),
|
|
575
|
-
);
|
|
576
|
-
insert_property(
|
|
577
|
-
&mut properties,
|
|
578
|
-
"label",
|
|
579
|
-
string_property("Optional display label."),
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
McpTool::RequestHuman => {
|
|
583
|
-
insert_property(
|
|
584
|
-
&mut properties,
|
|
585
|
-
"question",
|
|
586
|
-
string_property("Question to ask the human."),
|
|
587
|
-
);
|
|
588
|
-
insert_property(
|
|
589
|
-
&mut properties,
|
|
590
|
-
"task_id",
|
|
591
|
-
string_property("Optional related task id."),
|
|
592
|
-
);
|
|
593
|
-
insert_property(
|
|
594
|
-
&mut properties,
|
|
595
|
-
"agent_id",
|
|
596
|
-
string_property("Optional requesting agent id."),
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
McpTool::StuckCancel => {
|
|
600
|
-
insert_property(
|
|
601
|
-
&mut properties,
|
|
602
|
-
"agent_id",
|
|
603
|
-
string_property("Agent id whose stuck alerts should be suppressed."),
|
|
604
|
-
);
|
|
605
|
-
insert_property(
|
|
606
|
-
&mut properties,
|
|
607
|
-
"alert_type",
|
|
608
|
-
string_property("Alert type to suppress, or all."),
|
|
609
|
-
);
|
|
610
|
-
}
|
|
481
|
+
McpTool::GetTeamStatus => {}
|
|
611
482
|
}
|
|
612
483
|
properties
|
|
613
484
|
}
|
|
@@ -628,25 +499,6 @@ fn object_property(description: &str) -> Value {
|
|
|
628
499
|
serde_json::json!({"type": "object", "description": description, "additionalProperties": true})
|
|
629
500
|
}
|
|
630
501
|
|
|
631
|
-
fn task_property() -> Value {
|
|
632
|
-
serde_json::json!({
|
|
633
|
-
"type": "object",
|
|
634
|
-
"description": "Task object to add or update.",
|
|
635
|
-
"properties": {
|
|
636
|
-
"result_route": {
|
|
637
|
-
"type": "string",
|
|
638
|
-
"enum": crate::messaging::results::ResultRoute::ALL
|
|
639
|
-
.iter()
|
|
640
|
-
.map(|route| route.as_str())
|
|
641
|
-
.collect::<Vec<_>>(),
|
|
642
|
-
"default": crate::messaging::results::ResultRoute::default().as_str(),
|
|
643
|
-
"description": "Result destination chosen by the task assigner."
|
|
644
|
-
}
|
|
645
|
-
},
|
|
646
|
-
"additionalProperties": true
|
|
647
|
-
})
|
|
648
|
-
}
|
|
649
|
-
|
|
650
502
|
fn presentation_property(description: &str) -> Value {
|
|
651
503
|
serde_json::json!({
|
|
652
504
|
"type": "object",
|
|
@@ -690,14 +542,8 @@ pub(crate) fn dispatch_tool(
|
|
|
690
542
|
}
|
|
691
543
|
_ => {}
|
|
692
544
|
}
|
|
693
|
-
|
|
694
|
-
tools.validate_rpc_scope_args(tool.wire_name(), args)?;
|
|
695
|
-
}
|
|
545
|
+
tools.validate_rpc_scope_args(tool.wire_name(), args)?;
|
|
696
546
|
match tool {
|
|
697
|
-
McpTool::AssignTask => tools.assign_task(
|
|
698
|
-
args.get("task").unwrap_or(args),
|
|
699
|
-
args.get("message").and_then(Value::as_str),
|
|
700
|
-
),
|
|
701
547
|
McpTool::SendMessage => {
|
|
702
548
|
let target = message_target_from_value(args.get("to"));
|
|
703
549
|
let content = args.get("content").and_then(Value::as_str).unwrap_or("");
|
|
@@ -740,77 +586,10 @@ pub(crate) fn dispatch_tool(
|
|
|
740
586
|
args.get("agent_id").and_then(Value::as_str),
|
|
741
587
|
args.get("presentation"),
|
|
742
588
|
),
|
|
743
|
-
McpTool::UpdateState => {
|
|
744
|
-
tools.update_state(args.get("note").and_then(Value::as_str).unwrap_or(""))
|
|
745
|
-
}
|
|
746
589
|
McpTool::GetTeamStatus => tools.get_team_status(),
|
|
747
|
-
McpTool::StopAgent => {
|
|
748
|
-
tools.stop_agent(args.get("agent_id").and_then(Value::as_str).unwrap_or(""))
|
|
749
|
-
}
|
|
750
|
-
McpTool::ResetAgent => tools.reset_agent(
|
|
751
|
-
args.get("agent_id").and_then(Value::as_str).unwrap_or(""),
|
|
752
|
-
args.get("discard_session")
|
|
753
|
-
.and_then(Value::as_bool)
|
|
754
|
-
.unwrap_or(false),
|
|
755
|
-
),
|
|
756
|
-
McpTool::AddAgent => tools.add_agent(
|
|
757
|
-
args.get("new_agent_id")
|
|
758
|
-
.and_then(Value::as_str)
|
|
759
|
-
.unwrap_or(""),
|
|
760
|
-
args.get("role_file_path")
|
|
761
|
-
.and_then(Value::as_str)
|
|
762
|
-
.unwrap_or(""),
|
|
763
|
-
),
|
|
764
|
-
McpTool::ForkAgent => tools.fork_agent(
|
|
765
|
-
args.get("source_agent_id")
|
|
766
|
-
.and_then(Value::as_str)
|
|
767
|
-
.unwrap_or(""),
|
|
768
|
-
args.get("as_agent_id")
|
|
769
|
-
.and_then(Value::as_str)
|
|
770
|
-
.unwrap_or(""),
|
|
771
|
-
args.get("label").and_then(Value::as_str),
|
|
772
|
-
),
|
|
773
|
-
McpTool::CloneAgent => tools.clone_agent(
|
|
774
|
-
args.get("source_agent_id")
|
|
775
|
-
.and_then(Value::as_str)
|
|
776
|
-
.unwrap_or(""),
|
|
777
|
-
args.get("as_agent_id")
|
|
778
|
-
.and_then(Value::as_str)
|
|
779
|
-
.unwrap_or(""),
|
|
780
|
-
args.get("label").and_then(Value::as_str),
|
|
781
|
-
),
|
|
782
|
-
McpTool::RequestHuman => tools.request_human(
|
|
783
|
-
args.get("question").and_then(Value::as_str).unwrap_or(""),
|
|
784
|
-
args.get("task_id").and_then(Value::as_str),
|
|
785
|
-
args.get("agent_id").and_then(Value::as_str),
|
|
786
|
-
),
|
|
787
|
-
McpTool::StuckList => tools.stuck_list(),
|
|
788
|
-
McpTool::StuckCancel => tools.stuck_cancel(
|
|
789
|
-
args.get("agent_id").and_then(Value::as_str).unwrap_or(""),
|
|
790
|
-
// tools.py:351 — the MCP default alert_type is "stuck", not "all".
|
|
791
|
-
args.get("alert_type")
|
|
792
|
-
.and_then(Value::as_str)
|
|
793
|
-
.unwrap_or("stuck"),
|
|
794
|
-
),
|
|
795
590
|
}
|
|
796
591
|
}
|
|
797
592
|
|
|
798
|
-
fn scope_ceiling_tool(tool: McpTool) -> bool {
|
|
799
|
-
matches!(
|
|
800
|
-
tool,
|
|
801
|
-
McpTool::SendMessage
|
|
802
|
-
| McpTool::ReportResult
|
|
803
|
-
| McpTool::RequestHuman
|
|
804
|
-
| McpTool::AssignTask
|
|
805
|
-
| McpTool::UpdateState
|
|
806
|
-
| McpTool::GetTeamStatus
|
|
807
|
-
| McpTool::StopAgent
|
|
808
|
-
| McpTool::ResetAgent
|
|
809
|
-
| McpTool::CloneAgent
|
|
810
|
-
| McpTool::ForkAgent
|
|
811
|
-
)
|
|
812
|
-
}
|
|
813
|
-
|
|
814
593
|
fn message_target_from_value(value: Option<&Value>) -> MessageTarget {
|
|
815
594
|
match value {
|
|
816
595
|
Some(Value::String(s)) if s == "*" => MessageTarget::Broadcast,
|
|
@@ -22,7 +22,7 @@ use super::{
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/// `_send_to_leader_receiver` (`leader.py:69`) — **N31/N32 funnel primitive**:所有 leader-bound
|
|
25
|
-
/// caller(send_message(to=leader) / report_result /
|
|
25
|
+
/// caller(send_message(to=leader) / report_result / idle reminder /
|
|
26
26
|
/// broadcast-to-leader / peer-mirror / worker.abnormal_exit)统一经过这里。
|
|
27
27
|
///
|
|
28
28
|
/// 职责 = create_message + leader_notification_log dedup(result_id 时)+ audit + emit
|
|
@@ -105,14 +105,14 @@ pub use results::{
|
|
|
105
105
|
collect, collect_for_team, report_result, report_result_for_owner_team,
|
|
106
106
|
report_result_for_owner_team_with_primary_error,
|
|
107
107
|
};
|
|
108
|
-
pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events
|
|
108
|
+
pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events};
|
|
109
109
|
pub use selftest::{run_comms_selftest, CommsSelftestDriver};
|
|
110
110
|
pub use send::{
|
|
111
111
|
apply_worker_sender_bypass, send_message, MessageTarget, SendOptions, TrustedSender,
|
|
112
112
|
};
|
|
113
113
|
pub use trust::{attempt_trust_auto_answer, TrustAnswerOutcome};
|
|
114
114
|
pub use types::{
|
|
115
|
-
ActivityStatus, AgentActivity,
|
|
115
|
+
ActivityStatus, AgentActivity, CheckEvidence, CheckKind, CheckStatus,
|
|
116
116
|
ContractSuiteCheck, DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus,
|
|
117
117
|
IdleEvaluation, LeaderNotificationKey, LeaderReceiver, PaneWidthQuery,
|
|
118
118
|
ProviderSdkCalls, ReceiverMode, ScheduledKind, SelftestCheck, SelftestReport, SendEventPayload,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//!
|
|
2
|
-
//!
|
|
2
|
+
//! Scheduled-event dispatch and coordinator stuck detection (card §18/§68/§73).
|
|
3
3
|
|
|
4
4
|
use std::path::Path;
|
|
5
5
|
|
|
@@ -11,9 +11,7 @@ use crate::transport::{PaneId, Transport};
|
|
|
11
11
|
|
|
12
12
|
use super::delivery::{deliver_stored_message, handle_trust_retry_needed};
|
|
13
13
|
use super::helpers::{parse_scheduled_kind, status_wire};
|
|
14
|
-
use super::{
|
|
15
|
-
AlertType, MessagingError, ScheduledKind, TrustRetryPayload, TRUST_RETRY_MAX_ATTEMPTS,
|
|
16
|
-
};
|
|
14
|
+
use super::{MessagingError, ScheduledKind, TrustRetryPayload, TRUST_RETRY_MAX_ATTEMPTS};
|
|
17
15
|
|
|
18
16
|
/// `_fire_due_scheduled_events` (`scheduler.py:41`):coordinator tick 的调度器心脏 —— 分派到期
|
|
19
17
|
/// `send`/`health_ping`/`trust_retry` ([`ScheduledKind`] 穷尽 match),send 失败有界重试。
|
|
@@ -209,204 +207,3 @@ fn output_is_stale(last_output_at: &str, timeout_seconds: i64) -> bool {
|
|
|
209
207
|
.num_seconds()
|
|
210
208
|
>= timeout_seconds
|
|
211
209
|
}
|
|
212
|
-
|
|
213
|
-
/// stuck/idle 告警列表 (`scheduler.py:222`)。team-scoped 抑制查询。CLI 调。
|
|
214
|
-
pub fn stuck_list(workspace: &Path) -> Result<serde_json::Value, MessagingError> {
|
|
215
|
-
let state = crate::state::persist::load_runtime_state(workspace)?;
|
|
216
|
-
let team = active_team_key(workspace, &state);
|
|
217
|
-
let suppressed = state
|
|
218
|
-
.get("coordinator")
|
|
219
|
-
.and_then(|v| v.get("suppressed_idle_alerts"))
|
|
220
|
-
.and_then(|v| v.get(&team))
|
|
221
|
-
.cloned()
|
|
222
|
-
.unwrap_or_else(|| serde_json::json!({}));
|
|
223
|
-
Ok(serde_json::json!({"ok": true, "suppressed_idle_alerts": suppressed}))
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/// 取消 idle 告警抑制 (`scheduler.py:262`)。`alert_type` 为 [`AlertType`] 或 `all` (展开全集);
|
|
227
|
-
/// owner-gate 校验 (check_team_owner)。CLI 调。
|
|
228
|
-
pub fn stuck_cancel(
|
|
229
|
-
workspace: &Path,
|
|
230
|
-
agent_id: &str,
|
|
231
|
-
alert_type: Option<AlertType>,
|
|
232
|
-
suppressed_by: &str,
|
|
233
|
-
) -> Result<serde_json::Value, MessagingError> {
|
|
234
|
-
let alert_types: Vec<&str> = match alert_type {
|
|
235
|
-
Some(AlertType::Stuck) => vec!["stuck"],
|
|
236
|
-
Some(AlertType::IdleFallback) => vec!["idle_fallback"],
|
|
237
|
-
Some(AlertType::CrossWorkerDeadlock) => vec!["cross_worker_deadlock"],
|
|
238
|
-
None => vec!["cross_worker_deadlock", "idle_fallback", "stuck"],
|
|
239
|
-
};
|
|
240
|
-
let mut state = crate::state::persist::load_runtime_state(workspace)?;
|
|
241
|
-
let team = active_team_key(workspace, &state);
|
|
242
|
-
let now = chrono::Utc::now().to_rfc3339();
|
|
243
|
-
let assigned = assigned_task_ids(&state, agent_id);
|
|
244
|
-
let store = MessageStore::open(workspace)?;
|
|
245
|
-
let delivered = delivered_message_ids(&store, &team, agent_id)?;
|
|
246
|
-
for kind in &alert_types {
|
|
247
|
-
upsert_suppression(
|
|
248
|
-
&mut state,
|
|
249
|
-
SuppressionRecord {
|
|
250
|
-
team: &team,
|
|
251
|
-
agent_id,
|
|
252
|
-
alert_type: kind,
|
|
253
|
-
suppressed_by,
|
|
254
|
-
suppressed_at: &now,
|
|
255
|
-
assigned_task_ids: assigned.clone(),
|
|
256
|
-
delivered_message_ids: delivered.clone(),
|
|
257
|
-
},
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
crate::state::repository::StateRepository::new(workspace).save_reapplying(
|
|
261
|
-
crate::state::repository::StateWriteIntent::SchedulerSuppression {
|
|
262
|
-
owner_team_id: Some(&team),
|
|
263
|
-
},
|
|
264
|
-
&state,
|
|
265
|
-
|latest| {
|
|
266
|
-
for kind in &alert_types {
|
|
267
|
-
upsert_suppression(
|
|
268
|
-
latest,
|
|
269
|
-
SuppressionRecord {
|
|
270
|
-
team: &team,
|
|
271
|
-
agent_id,
|
|
272
|
-
alert_type: kind,
|
|
273
|
-
suppressed_by,
|
|
274
|
-
suppressed_at: &now,
|
|
275
|
-
assigned_task_ids: assigned.clone(),
|
|
276
|
-
delivered_message_ids: delivered.clone(),
|
|
277
|
-
},
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
)?;
|
|
282
|
-
crate::event_log::EventLog::new(workspace).write(
|
|
283
|
-
"coordinator.idle_alert_suppressed",
|
|
284
|
-
serde_json::json!({
|
|
285
|
-
"agent_id": agent_id,
|
|
286
|
-
"team": team,
|
|
287
|
-
"alert_types": alert_types,
|
|
288
|
-
"suppressed_by": suppressed_by,
|
|
289
|
-
}),
|
|
290
|
-
)?;
|
|
291
|
-
let suppressed = state
|
|
292
|
-
.get("coordinator")
|
|
293
|
-
.and_then(|v| v.get("suppressed_idle_alerts"))
|
|
294
|
-
.and_then(|v| v.get(&team))
|
|
295
|
-
.and_then(|v| v.get(agent_id))
|
|
296
|
-
.cloned()
|
|
297
|
-
.unwrap_or_else(|| serde_json::json!({}));
|
|
298
|
-
Ok(serde_json::json!({
|
|
299
|
-
"ok": true,
|
|
300
|
-
"agent_id": agent_id,
|
|
301
|
-
"alert_types": alert_types,
|
|
302
|
-
"suppressed": suppressed
|
|
303
|
-
}))
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
fn active_team_key(workspace: &Path, state: &serde_json::Value) -> String {
|
|
307
|
-
state
|
|
308
|
-
.get("active_team_key")
|
|
309
|
-
.and_then(serde_json::Value::as_str)
|
|
310
|
-
.filter(|team| !team.is_empty())
|
|
311
|
-
.map(ToString::to_string)
|
|
312
|
-
.or_else(|| {
|
|
313
|
-
workspace
|
|
314
|
-
.file_name()
|
|
315
|
-
.map(|name| name.to_string_lossy().to_string())
|
|
316
|
-
})
|
|
317
|
-
.unwrap_or_else(|| "current".to_string())
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
fn assigned_task_ids(state: &serde_json::Value, agent_id: &str) -> Vec<String> {
|
|
321
|
-
let mut ids: Vec<String> = state
|
|
322
|
-
.get("tasks")
|
|
323
|
-
.and_then(serde_json::Value::as_array)
|
|
324
|
-
.map(|tasks| {
|
|
325
|
-
tasks
|
|
326
|
-
.iter()
|
|
327
|
-
.filter(|task| {
|
|
328
|
-
task.get("assignee").and_then(serde_json::Value::as_str) == Some(agent_id)
|
|
329
|
-
})
|
|
330
|
-
.filter_map(|task| {
|
|
331
|
-
task.get("id")
|
|
332
|
-
.and_then(serde_json::Value::as_str)
|
|
333
|
-
.map(ToString::to_string)
|
|
334
|
-
})
|
|
335
|
-
.collect()
|
|
336
|
-
})
|
|
337
|
-
.unwrap_or_default();
|
|
338
|
-
ids.sort();
|
|
339
|
-
ids
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
fn delivered_message_ids(
|
|
343
|
-
store: &MessageStore,
|
|
344
|
-
team: &str,
|
|
345
|
-
agent_id: &str,
|
|
346
|
-
) -> Result<Vec<String>, MessagingError> {
|
|
347
|
-
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
348
|
-
let mut stmt = conn.prepare(
|
|
349
|
-
"select message_id from messages
|
|
350
|
-
where (owner_team_id = ?1 or owner_team_id is null)
|
|
351
|
-
and recipient = ?2
|
|
352
|
-
and status in ('visible', 'submitted', 'delivered', 'acknowledged')
|
|
353
|
-
order by message_id",
|
|
354
|
-
)?;
|
|
355
|
-
let rows = stmt.query_map(params![team, agent_id], |row| row.get::<_, String>(0))?;
|
|
356
|
-
Ok(rows.collect::<Result<Vec<_>, _>>()?)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
struct SuppressionRecord<'a> {
|
|
360
|
-
team: &'a str,
|
|
361
|
-
agent_id: &'a str,
|
|
362
|
-
alert_type: &'a str,
|
|
363
|
-
suppressed_by: &'a str,
|
|
364
|
-
suppressed_at: &'a str,
|
|
365
|
-
assigned_task_ids: Vec<String>,
|
|
366
|
-
delivered_message_ids: Vec<String>,
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
fn upsert_suppression(state: &mut serde_json::Value, record: SuppressionRecord<'_>) {
|
|
370
|
-
let Some(root) = state.as_object_mut() else {
|
|
371
|
-
return;
|
|
372
|
-
};
|
|
373
|
-
let Some(coordinator) = root
|
|
374
|
-
.entry("coordinator")
|
|
375
|
-
.or_insert_with(|| serde_json::json!({}))
|
|
376
|
-
.as_object_mut()
|
|
377
|
-
else {
|
|
378
|
-
return;
|
|
379
|
-
};
|
|
380
|
-
let Some(all) = coordinator
|
|
381
|
-
.entry("suppressed_idle_alerts")
|
|
382
|
-
.or_insert_with(|| serde_json::json!({}))
|
|
383
|
-
.as_object_mut()
|
|
384
|
-
else {
|
|
385
|
-
return;
|
|
386
|
-
};
|
|
387
|
-
let Some(team_map) = all
|
|
388
|
-
.entry(record.team.to_string())
|
|
389
|
-
.or_insert_with(|| serde_json::json!({}))
|
|
390
|
-
.as_object_mut()
|
|
391
|
-
else {
|
|
392
|
-
return;
|
|
393
|
-
};
|
|
394
|
-
let Some(agent_map) = team_map
|
|
395
|
-
.entry(record.agent_id.to_string())
|
|
396
|
-
.or_insert_with(|| serde_json::json!({}))
|
|
397
|
-
.as_object_mut()
|
|
398
|
-
else {
|
|
399
|
-
return;
|
|
400
|
-
};
|
|
401
|
-
agent_map.insert(
|
|
402
|
-
record.alert_type.to_string(),
|
|
403
|
-
serde_json::json!({
|
|
404
|
-
"suppressed_at": record.suppressed_at,
|
|
405
|
-
"suppressed_by": record.suppressed_by,
|
|
406
|
-
"snapshot": {
|
|
407
|
-
"assigned_task_ids": record.assigned_task_ids,
|
|
408
|
-
"delivered_message_ids": record.delivered_message_ids
|
|
409
|
-
},
|
|
410
|
-
}),
|
|
411
|
-
);
|
|
412
|
-
}
|
|
@@ -140,23 +140,6 @@ fn activity_status_serde_byte_locked() {
|
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
#[test]
|
|
144
|
-
fn alert_type_serde_byte_locked() {
|
|
145
|
-
// scheduler.py:38 _ALERT_TYPES set members.
|
|
146
|
-
assert_eq!(
|
|
147
|
-
serde_json::to_string(&AlertType::Stuck).unwrap(),
|
|
148
|
-
"\"stuck\""
|
|
149
|
-
);
|
|
150
|
-
assert_eq!(
|
|
151
|
-
serde_json::to_string(&AlertType::IdleFallback).unwrap(),
|
|
152
|
-
"\"idle_fallback\""
|
|
153
|
-
);
|
|
154
|
-
assert_eq!(
|
|
155
|
-
serde_json::to_string(&AlertType::CrossWorkerDeadlock).unwrap(),
|
|
156
|
-
"\"cross_worker_deadlock\""
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
143
|
#[test]
|
|
161
144
|
fn check_status_serde_byte_locked() {
|
|
162
145
|
assert_eq!(
|
|
@@ -266,20 +249,6 @@ fn provider_sdk_calls_is_zero_only_when_all_three_zero() {
|
|
|
266
249
|
.is_zero());
|
|
267
250
|
}
|
|
268
251
|
|
|
269
|
-
#[test]
|
|
270
|
-
fn alert_type_all_is_sorted_full_set() {
|
|
271
|
-
// scheduler.py:269 sorted(_ALERT_TYPES) =
|
|
272
|
-
// ['cross_worker_deadlock','idle_fallback','stuck'].
|
|
273
|
-
assert_eq!(
|
|
274
|
-
AlertType::all(),
|
|
275
|
-
[
|
|
276
|
-
AlertType::CrossWorkerDeadlock,
|
|
277
|
-
AlertType::IdleFallback,
|
|
278
|
-
AlertType::Stuck
|
|
279
|
-
]
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
252
|
#[test]
|
|
284
253
|
fn activity_status_idle_takeover_gate_uncertain_never_idle() {
|
|
285
254
|
// THE IRON LAW (bug-071/077/085): only Idle passes; Uncertain/Working/Stuck
|
|
@@ -1,78 +1,5 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
|
-
// ── F1 [P1 byte-shape] — stuck_cancel suppression snapshot.delivered_message_ids must use golden's
|
|
4
|
-
// _DELIVERED_MESSAGE_STATUSES = {visible, submitted, delivered, acknowledged} (scheduler.py:27,376-383),
|
|
5
|
-
// sorted. The Rust delivered_message_ids (scheduler.rs:243) used {injected, visible, submitted,
|
|
6
|
-
// submitted_unverified, delivered} — the WRONG set (reviewer's explicit warning: don't copy the
|
|
7
|
-
// activity.rs deadlock query set): it EXCLUDES 'acknowledged' and INCLUDES 'injected'/'submitted_unverified'.
|
|
8
|
-
// (The snapshot also must carry BOTH assigned_task_ids AND delivered_message_ids — that structural half
|
|
9
|
-
// is already present; this pins the STATUS SET.)
|
|
10
|
-
#[test]
|
|
11
|
-
fn stuck_cancel_snapshot_delivered_message_ids_uses_golden_status_set() {
|
|
12
|
-
let ws = tmp_ws("f1_delivered");
|
|
13
|
-
crate::state::persist::save_runtime_state(
|
|
14
|
-
&ws,
|
|
15
|
-
&serde_json::json!({
|
|
16
|
-
"active_team_key": "teamX",
|
|
17
|
-
"agents": {"w1": {"status": "running", "provider": "codex"}}
|
|
18
|
-
}),
|
|
19
|
-
)
|
|
20
|
-
.unwrap();
|
|
21
|
-
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
22
|
-
// golden-delivered (must be IN delivered_message_ids): acknowledged + visible.
|
|
23
|
-
let m_ack = store
|
|
24
|
-
.create_message(None, "leader", "w1", "ack-me", None, true, Some("teamX"))
|
|
25
|
-
.unwrap();
|
|
26
|
-
store.mark(&m_ack, "delivered", None).unwrap();
|
|
27
|
-
store.mark(&m_ack, "acknowledged", None).unwrap();
|
|
28
|
-
let m_vis = store
|
|
29
|
-
.create_message(None, "leader", "w1", "vis", None, true, Some("teamX"))
|
|
30
|
-
.unwrap();
|
|
31
|
-
store.mark(&m_vis, "visible", None).unwrap();
|
|
32
|
-
// NOT golden-delivered (must be EXCLUDED): injected.
|
|
33
|
-
let m_inj = store
|
|
34
|
-
.create_message(None, "leader", "w1", "inj", None, true, Some("teamX"))
|
|
35
|
-
.unwrap();
|
|
36
|
-
store.mark(&m_inj, "injected", None).unwrap();
|
|
37
|
-
drop(store);
|
|
38
|
-
let _ = stuck_cancel(&ws, "w1", None, "leader").unwrap();
|
|
39
|
-
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
40
|
-
let snapshot =
|
|
41
|
-
&state["coordinator"]["suppressed_idle_alerts"]["teamX"]["w1"]["stuck"]["snapshot"];
|
|
42
|
-
assert!(
|
|
43
|
-
snapshot.get("assigned_task_ids").is_some(),
|
|
44
|
-
"F1: snapshot must carry assigned_task_ids; got {snapshot}"
|
|
45
|
-
);
|
|
46
|
-
let delivered = snapshot
|
|
47
|
-
.get("delivered_message_ids")
|
|
48
|
-
.and_then(serde_json::Value::as_array)
|
|
49
|
-
.map(|a| {
|
|
50
|
-
a.iter()
|
|
51
|
-
.filter_map(|v| v.as_str().map(str::to_string))
|
|
52
|
-
.collect::<Vec<_>>()
|
|
53
|
-
})
|
|
54
|
-
.unwrap_or_else(|| panic!("F1: snapshot must carry delivered_message_ids; got {snapshot}"));
|
|
55
|
-
assert!(
|
|
56
|
-
delivered.contains(&m_ack),
|
|
57
|
-
"F1: golden _DELIVERED_MESSAGE_STATUSES includes 'acknowledged' — the acknowledged message must be \
|
|
58
|
-
in delivered_message_ids; the Rust set omits it. got {delivered:?}"
|
|
59
|
-
);
|
|
60
|
-
assert!(
|
|
61
|
-
delivered.contains(&m_vis),
|
|
62
|
-
"F1: 'visible' is delivered (golden); got {delivered:?}"
|
|
63
|
-
);
|
|
64
|
-
assert!(
|
|
65
|
-
!delivered.contains(&m_inj),
|
|
66
|
-
"F1: 'injected' is NOT in golden _DELIVERED_MESSAGE_STATUSES — it must be EXCLUDED; the Rust set \
|
|
67
|
-
wrongly includes it. got {delivered:?}"
|
|
68
|
-
);
|
|
69
|
-
let mut sorted = delivered.clone();
|
|
70
|
-
sorted.sort();
|
|
71
|
-
assert_eq!(
|
|
72
|
-
delivered, sorted,
|
|
73
|
-
"F1: delivered_message_ids must be sorted (golden sorts); got {delivered:?}"
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
3
|
// ── collect --result-file INGEST SEMANTIC (golden results.py:58-73) ──────────────────────────────
|
|
77
4
|
// Golden `collect(result_file=…)` INDEPENDENTLY INGESTS a standalone result: a valid result_envelope_v1
|
|
78
5
|
// is `store.add_result(envelope)`'d (results.py:73) regardless of any in-flight delivery, then the
|