@team-agent/installer 0.5.86 → 0.5.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +104 -164
  4. package/crates/team-agent/src/cli/emit.rs +68 -87
  5. package/crates/team-agent/src/cli/mod.rs +1 -1
  6. package/crates/team-agent/src/cli/spec.rs +1 -4
  7. package/crates/team-agent/src/cli/status_port/brief.rs +187 -7
  8. package/crates/team-agent/src/cli/status_port/inbox.rs +42 -17
  9. package/crates/team-agent/src/cli/tests/lane_c.rs +34 -26
  10. package/crates/team-agent/src/cli/tests/main_preserved.rs +4 -70
  11. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +0 -68
  12. package/crates/team-agent/src/cli/types.rs +3 -16
  13. package/crates/team-agent/src/compiler.rs +4 -7
  14. package/crates/team-agent/src/coordinator/tests/tick_core.rs +5 -6
  15. package/crates/team-agent/src/coordinator/tick.rs +1 -16
  16. package/crates/team-agent/src/coordinator/types.rs +1 -1
  17. package/crates/team-agent/src/db/message_store.rs +10 -5
  18. package/crates/team-agent/src/fake_worker.rs +4 -4
  19. package/crates/team-agent/src/lifecycle/launch/identity.rs +39 -21
  20. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  21. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  22. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +3 -3
  23. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +2 -2
  24. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -10
  25. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -9
  26. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -26
  27. package/crates/team-agent/src/main.rs +7 -2
  28. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  29. package/crates/team-agent/src/messaging/mod.rs +2 -2
  30. package/crates/team-agent/src/messaging/results.rs +234 -386
  31. package/crates/team-agent/src/messaging/tests/runtime.rs +91 -135
  32. package/crates/team-agent/src/messaging/watchers.rs +1 -1
  33. package/crates/team-agent/src/model/enums.rs +17 -3
  34. package/crates/team-agent/src/model/spec.rs +8 -7
  35. package/crates/team-agent/src/provider/wire.rs +1 -1
  36. package/package.json +4 -4
  37. package/skills/team-agent/SKILL.md +3 -3
  38. package/skills/team-agent/command-coverage.json +2 -9
  39. package/skills/team-agent/references/team-agent-operator.md +661 -0
@@ -1,5 +1,5 @@
1
1
  //!
2
- //! results.py — collect + report_result + envelope 校验编排 (card §66/§67)。
2
+ //! results.py — report_result + envelope 校验编排 (card §66/§67)。
3
3
 
4
4
  use std::path::{Path, PathBuf};
5
5
 
@@ -7,12 +7,11 @@ use rusqlite::params;
7
7
 
8
8
  use crate::event_log::EventLog;
9
9
  use crate::message_store::MessageStore;
10
+ use crate::model::ids::TaskId;
10
11
 
11
12
  use super::helpers::{next_result_id, required_str, validate_result_envelope};
12
13
  use super::types::SEND_RETRY_MAX_ATTEMPTS;
13
- use super::watchers::retry_result_deliveries;
14
14
  use super::MessagingError;
15
- use crate::model::ids::TaskId;
16
15
  use crate::state::projection::OwnerTeamResolution;
17
16
 
18
17
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -660,6 +659,14 @@ fn task_exists(state: &serde_json::Value, task_id: &str) -> bool {
660
659
  })
661
660
  }
662
661
 
662
+ fn task_exists_for_owner(state: &serde_json::Value, owner_team: &str, task_id: &str) -> bool {
663
+ state
664
+ .get("teams")
665
+ .and_then(|teams| teams.get(owner_team))
666
+ .is_some_and(|team| task_exists(team, task_id))
667
+ || task_exists(state, task_id)
668
+ }
669
+
663
670
  fn is_message_scoped_result(
664
671
  conn: &rusqlite::Connection,
665
672
  task_id: &str,
@@ -752,8 +759,7 @@ fn count_results(
752
759
  }
753
760
  }
754
761
 
755
- /// `report_result` (`results.py:191`):worker 报结果 —— 校验 envelope、存 result、ack 任务消息、
756
- /// **排队** send 事件通知 leader、推进 orchestrator (软依赖,失败仅记 `orchestrator.advance_skipped`)。
762
+ /// `report_result` (`results.py:191`): worker 报结果 —— 校验 envelope、持久化并自动结案。
757
763
  /// MCP `report_result` 工具调。
758
764
  pub fn report_result(
759
765
  workspace: &Path,
@@ -788,7 +794,7 @@ fn report_result_for_owner_team_inner(
788
794
  workspace: &Path,
789
795
  envelope: &serde_json::Value,
790
796
  explicit_owner_team: Option<&str>,
791
- fallback_primary_error: Option<&str>,
797
+ _fallback_primary_error: Option<&str>,
792
798
  ) -> Result<serde_json::Value, MessagingError> {
793
799
  if envelope.get("result_route").is_some() {
794
800
  return Err(MessagingError::Validation(
@@ -865,327 +871,95 @@ fn report_result_for_owner_team_inner(
865
871
  status,
866
872
  Some(&owner_team),
867
873
  )?;
868
- if !inserted {
869
- let log = EventLog::new(workspace);
870
- log.write(
871
- "mcp.report_result_duplicate_ignored",
872
- serde_json::json!({
873
- "notification_status": "duplicate_ignored",
874
- "owner_team_id": owner_team,
875
- "result_id": result_id,
876
- }),
877
- )?;
878
- let mut out = serde_json::Map::new();
879
- out.insert("ok".to_string(), serde_json::Value::Bool(true));
880
- out.insert(
881
- "status".to_string(),
882
- serde_json::Value::String("duplicate_ignored".to_string()),
883
- );
884
- out.insert(
885
- "result_id".to_string(),
886
- serde_json::Value::String(result_id),
887
- );
888
- out.insert(
889
- "task_id".to_string(),
890
- serde_json::Value::String(task_id.to_string()),
891
- );
892
- copy_report_attribution_fields(envelope, &mut out);
893
- out.insert(
894
- "agent_id".to_string(),
895
- serde_json::Value::String(agent_id.to_string()),
896
- );
897
- out.insert("acknowledged_messages".to_string(), serde_json::json!([]));
898
- out.insert(
899
- "leader_notified".to_string(),
900
- serde_json::Value::Bool(false),
901
- );
902
- out.insert(
903
- "notification_message_id".to_string(),
904
- serde_json::Value::Null,
905
- );
906
- out.insert(
907
- "notification_status".to_string(),
908
- serde_json::Value::String("duplicate_ignored".to_string()),
909
- );
910
- out.insert(
911
- "notification_channel".to_string(),
912
- serde_json::Value::String("coordinator".to_string()),
913
- );
914
- out.insert("notification_event_id".to_string(), serde_json::Value::Null);
915
- return Ok(serde_json::Value::Object(out));
916
- }
917
874
  let event_log = EventLog::new(workspace);
918
- super::watchers::notify_fifo_result_watchers(&conn, &event_log, task_id, &result_id)?;
919
- if presentation.effective_sink != super::presentation::PresentationSink::Leader {
920
- event_log.write(
921
- "presentation.stored_without_live_inject",
922
- serde_json::json!({
923
- "result_id": result_id,
924
- "owner_team_id": owner_team,
925
- "requested_sink": presentation.requested_sink,
926
- "effective_sink": presentation.effective_sink,
927
- "class": presentation.class,
928
- "policy_reason": presentation.policy_reason,
929
- }),
930
- )?;
875
+ let finalized_scope = auto_finalize_result(
876
+ workspace,
877
+ &conn,
878
+ &state_for_owner,
879
+ &owner_team,
880
+ &result_id,
881
+ task_id,
882
+ agent_id,
883
+ )?;
884
+ if inserted {
885
+ super::watchers::notify_fifo_result_watchers(&conn, &event_log, task_id, &result_id)?;
886
+ } else {
931
887
  event_log.write(
932
- "mcp.report_result",
888
+ "mcp.report_result_duplicate_ignored",
933
889
  serde_json::json!({
934
- "leader_notified": false,
935
- "notification_channel": presentation.effective_sink,
936
- "notification_message_id": serde_json::Value::Null,
937
- "notification_status": "stored_not_presented",
938
890
  "owner_team_id": owner_team,
939
891
  "result_id": result_id,
940
892
  }),
941
893
  )?;
942
- let mut out = serde_json::Map::new();
943
- out.insert("ok".to_string(), serde_json::Value::Bool(true));
944
- out.insert(
945
- "result_id".to_string(),
946
- serde_json::Value::String(result_id),
947
- );
948
- out.insert(
949
- "task_id".to_string(),
950
- serde_json::Value::String(task_id.to_string()),
951
- );
952
- copy_report_attribution_fields(envelope, &mut out);
953
- out.insert(
954
- "agent_id".to_string(),
955
- serde_json::Value::String(agent_id.to_string()),
956
- );
957
- out.insert("acknowledged_messages".to_string(), serde_json::json!([]));
958
- out.insert(
959
- "leader_notified".to_string(),
960
- serde_json::Value::Bool(false),
961
- );
962
- out.insert(
963
- "notification_message_id".to_string(),
964
- serde_json::Value::Null,
965
- );
966
- out.insert(
967
- "notification_status".to_string(),
968
- serde_json::Value::String("stored_not_presented".to_string()),
969
- );
970
- out.insert(
971
- "notification_channel".to_string(),
972
- serde_json::Value::String(presentation.effective_sink.as_str().to_string()),
973
- );
974
- out.insert("notification_event_id".to_string(), serde_json::Value::Null);
975
- if let Some(warnings) = report_result_array(envelope, "warnings") {
976
- out.insert(
977
- "warnings".to_string(),
978
- serde_json::Value::Array(warnings.clone()),
979
- );
980
- }
981
- return Ok(serde_json::Value::Object(out));
982
894
  }
983
- // #230 N31/N32 funnel: report_result must go through the shared leader-delivery
984
- // primitive synchronously, NOT via a parallel queued scheduled_events row. The
985
- // legacy path was MUST-8 / I-3 violating (the deferred notification status was returned
986
- // to the caller as "success" while leader actually never saw the result text).
987
- let content =
988
- format_report_result_notification(&result_id, task_id, agent_id, status, envelope);
989
- let state = report_owner_state(&state_for_owner, &owner_team);
990
- let mut outcome = match super::leader_receiver::send_to_leader_receiver_with_presentation(
991
- workspace,
992
- &state,
993
- "leader",
994
- &content,
995
- Some(&TaskId::new(task_id.to_string())),
996
- agent_id,
997
- false,
998
- Some(&result_id),
999
- None,
1000
- &presentation,
1001
- &event_log,
1002
- ) {
1003
- Ok(outcome) => outcome,
1004
- Err(error) if report_state_has_app_server_receiver(&state) => {
1005
- let message_id = format!("fallback:{result_id}");
1006
- crate::messaging::DeliveryOutcome {
1007
- ok: false,
1008
- status: crate::messaging::DeliveryStatus::Failed,
1009
- message_status: super::helpers::MessageStatusShadow("failed".to_string()),
1010
- message_id: Some(message_id),
1011
- verification: Some(format!("leader_funnel_error:{error}")),
1012
- stage: None,
1013
- reason: Some(crate::messaging::DeliveryRefusal::LeaderNotAttached),
1014
- channel: Some("codex_app_server".to_string()),
1015
- ack_forced_off: false,
1016
- turn_verification: None,
1017
- }
1018
- }
1019
- Err(error) => {
1020
- let message_id = format!("fallback:{result_id}");
1021
- let fallback_error = fallback_primary_error_text(
1022
- fallback_primary_error,
1023
- format!("leader_funnel_error:{error}"),
1024
- );
1025
- super::leader_receiver::deliver_to_leader_fallback_pane(
1026
- workspace,
1027
- &state,
1028
- &message_id,
1029
- Some(&result_id),
1030
- &content,
1031
- false,
1032
- Some(&fallback_error),
1033
- &event_log,
1034
- )?
1035
- }
895
+ let mut leader_notified = false;
896
+ let mut notification_message_id = None;
897
+ let mut notification_channel = "none".to_string();
898
+ let mut notification_status = if finalized_scope.is_some() {
899
+ "auto_finalized".to_string()
900
+ } else {
901
+ "already_finalized".to_string()
1036
902
  };
1037
- if let Some(message_id) = outcome.message_id.clone() {
1038
- let store = MessageStore::open(workspace)?;
1039
- // 0.5.x Phase 1d Batch 4: report_result retry uses the runtime
1040
- // factory transport so a conpty team's report notification
1041
- // goes to the shim (design §Batch 4). Previously we always
1042
- // built `TmuxBackend::for_workspace`, which for a conpty team
1043
- // silently no-op'd the retry. Factory refusal falls back to
1044
- // tmux workspace (byte-equivalent tmux teams; honest failure
1045
- // for conpty teams without a resolvable factory input).
1046
- let delivery_state_raw = crate::state::persist::load_runtime_state(workspace)
1047
- .unwrap_or_else(|_| state_for_owner.clone());
1048
- let delivery_state = report_owner_state(&delivery_state_raw, &owner_team);
1049
- let transport: Box<dyn crate::transport::Transport> =
1050
- match crate::transport_factory::resolve_read_only_transport(
1051
- workspace,
1052
- Some(&delivery_state),
1053
- crate::transport_factory::TransportPurpose::MessageDelivery,
1054
- ) {
1055
- Ok(r) => r.backend,
1056
- Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(workspace)),
1057
- };
1058
- let mut primary_error: Option<String> = None;
1059
- for attempt in 0..3 {
1060
- let _ = store.mark(&message_id, "accepted", None);
1061
- match super::delivery::deliver_pending_message(
1062
- workspace,
1063
- &store,
1064
- transport.as_ref(),
1065
- &message_id,
1066
- &event_log,
1067
- &delivery_state,
1068
- ) {
1069
- Ok(next) => outcome = next,
1070
- Err(error) => {
1071
- primary_error = Some(format!("primary_delivery_error:{error}"));
1072
- outcome = crate::messaging::DeliveryOutcome {
1073
- ok: false,
1074
- status: crate::messaging::DeliveryStatus::Failed,
1075
- message_status: super::helpers::MessageStatusShadow("failed".to_string()),
1076
- message_id: Some(message_id.clone()),
1077
- verification: Some(error.to_string()),
1078
- stage: None,
1079
- reason: None,
1080
- channel: Some("leader_receiver".to_string()),
1081
- ack_forced_off: false,
1082
- turn_verification: None,
1083
- };
1084
- break;
1085
- }
1086
- }
1087
- if outcome.ok {
1088
- break;
1089
- }
1090
- match super::delivery::deliver_pending_messages(
1091
- workspace,
1092
- &delivery_state,
1093
- transport.as_ref(),
1094
- &event_log,
1095
- ) {
1096
- Ok(delivered)
1097
- if delivered
1098
- .iter()
1099
- .any(|delivered_id| delivered_id == &message_id) =>
1100
- {
1101
- outcome = crate::messaging::DeliveryOutcome {
1102
- ok: true,
1103
- status: crate::messaging::DeliveryStatus::Delivered,
1104
- message_status: super::helpers::MessageStatusShadow(
1105
- "delivered".to_string(),
1106
- ),
1107
- message_id: Some(message_id.clone()),
1108
- verification: None,
1109
- stage: None,
1110
- reason: None,
1111
- channel: Some("leader_receiver".to_string()),
1112
- ack_forced_off: false,
1113
- turn_verification: None,
1114
- };
1115
- break;
1116
- }
1117
- Ok(_) => {}
1118
- Err(error) => {
1119
- primary_error = Some(format!("primary_delivery_error:{error}"));
1120
- outcome = crate::messaging::DeliveryOutcome {
1121
- ok: false,
1122
- status: crate::messaging::DeliveryStatus::Failed,
1123
- message_status: super::helpers::MessageStatusShadow("failed".to_string()),
1124
- message_id: Some(message_id.clone()),
1125
- verification: Some(error.to_string()),
1126
- stage: None,
1127
- reason: None,
1128
- channel: Some("leader_receiver".to_string()),
1129
- ack_forced_off: false,
1130
- turn_verification: None,
1131
- };
1132
- break;
1133
- }
1134
- }
1135
- if attempt < 2 {
1136
- std::thread::sleep(std::time::Duration::from_millis(50));
903
+ if inserted && presentation.effective_sink == super::presentation::PresentationSink::Leader {
904
+ let content = format_report_result_leader_summary(&result_id, task_id, agent_id, status, envelope);
905
+ let leader_state = report_owner_state(&state_for_owner, &owner_team);
906
+ let leader_presentation = leader_result_presentation(task_id);
907
+ match super::leader_receiver::send_to_leader_receiver_with_presentation(
908
+ workspace,
909
+ &leader_state,
910
+ "leader",
911
+ &content,
912
+ Some(&TaskId::new(task_id.to_string())),
913
+ agent_id,
914
+ false,
915
+ Some(&result_id),
916
+ None,
917
+ &leader_presentation,
918
+ &event_log,
919
+ ) {
920
+ Ok(outcome) => {
921
+ leader_notified = matches!(
922
+ outcome.status,
923
+ super::DeliveryStatus::Delivered
924
+ );
925
+ notification_message_id = outcome.message_id;
926
+ notification_channel = outcome
927
+ .channel
928
+ .unwrap_or_else(|| "leader_receiver".to_string());
929
+ notification_status = if leader_notified {
930
+ "delivered".to_string()
931
+ } else if outcome.ok {
932
+ "queued".to_string()
933
+ } else {
934
+ "refused".to_string()
935
+ };
1137
936
  }
1138
- }
1139
- if !outcome.ok {
1140
- if outcome.channel.as_deref() != Some("leader_acceptance_pending")
1141
- && !report_state_has_app_server_receiver(&delivery_state)
1142
- {
1143
- let fallback_error = primary_error.unwrap_or_else(|| {
1144
- format!(
1145
- "leader_notification_primary_failed:{}",
1146
- super::helpers::status_wire(outcome.status)
1147
- )
1148
- });
1149
- let fallback_error =
1150
- fallback_primary_error_text(fallback_primary_error, fallback_error);
1151
- outcome = super::leader_receiver::deliver_to_leader_fallback_pane(
1152
- workspace,
1153
- &delivery_state,
1154
- &message_id,
1155
- Some(&result_id),
1156
- &content,
1157
- false,
1158
- Some(&fallback_error),
1159
- &event_log,
937
+ Err(error) => {
938
+ event_log.write(
939
+ "leader_receiver.result_notification_failed",
940
+ serde_json::json!({
941
+ "owner_team_id": owner_team,
942
+ "result_id": result_id,
943
+ "error": error.to_string(),
944
+ }),
1160
945
  )?;
946
+ notification_channel = "leader_receiver".to_string();
947
+ notification_status = "refused".to_string();
1161
948
  }
1162
949
  }
1163
950
  }
1164
- let leader_notified = matches!(outcome.status, crate::messaging::DeliveryStatus::Delivered);
1165
- let notification_status_wire = if leader_notified {
1166
- "delivered"
1167
- } else if outcome.channel.as_deref() == Some("leader_receipt_source_unavailable") {
1168
- "injected_awaiting_receipt"
1169
- } else if outcome.channel.as_deref() == Some("leader_acceptance_pending") {
1170
- "submitted_pending_acceptance"
1171
- } else if outcome.channel.as_deref() == Some("rebind_required")
1172
- || matches!(outcome.status, crate::messaging::DeliveryStatus::Blocked)
1173
- {
1174
- "rebind_required"
1175
- } else {
1176
- "refused"
1177
- };
1178
- let channel = outcome
1179
- .channel
1180
- .clone()
1181
- .unwrap_or_else(|| "leader_receiver".to_string());
1182
951
  event_log.write(
1183
952
  "mcp.report_result",
1184
953
  serde_json::json!({
1185
954
  "leader_notified": leader_notified,
1186
- "notification_channel": channel,
1187
- "notification_message_id": outcome.message_id,
1188
- "notification_status": notification_status_wire,
955
+ "notification_channel": notification_channel.clone(),
956
+ "notification_message_id": notification_message_id.clone(),
957
+ "notification_status": notification_status,
958
+ "finalization_status": if finalized_scope.is_some() {
959
+ "auto_finalized"
960
+ } else {
961
+ "already_finalized"
962
+ },
1189
963
  "owner_team_id": owner_team,
1190
964
  "result_id": result_id,
1191
965
  }),
@@ -1213,26 +987,28 @@ fn report_result_for_owner_team_inner(
1213
987
  );
1214
988
  out.insert(
1215
989
  "notification_message_id".to_string(),
1216
- outcome
1217
- .message_id
1218
- .map_or(serde_json::Value::Null, serde_json::Value::String),
990
+ notification_message_id.map_or(serde_json::Value::Null, serde_json::Value::String),
1219
991
  );
1220
992
  out.insert(
1221
993
  "notification_status".to_string(),
1222
- serde_json::Value::String(notification_status_wire.to_string()),
994
+ serde_json::Value::String(if inserted {
995
+ notification_status
996
+ } else {
997
+ "duplicate_ignored".to_string()
998
+ }),
999
+ );
1000
+ out.insert(
1001
+ "finalization_status".to_string(),
1002
+ serde_json::Value::String(if finalized_scope.is_some() {
1003
+ "auto_finalized".to_string()
1004
+ } else {
1005
+ "already_finalized".to_string()
1006
+ }),
1223
1007
  );
1224
1008
  out.insert(
1225
1009
  "notification_channel".to_string(),
1226
- serde_json::Value::String(channel.clone()),
1010
+ serde_json::Value::String(notification_channel),
1227
1011
  );
1228
- if channel == "rebind_required" {
1229
- out.insert(
1230
- "notification_action".to_string(),
1231
- serde_json::Value::String(
1232
- "run team-agent claim-leader or team-agent takeover".to_string(),
1233
- ),
1234
- );
1235
- }
1236
1012
  out.insert("notification_event_id".to_string(), serde_json::Value::Null);
1237
1013
  if let Some(warnings) = report_result_array(envelope, "warnings") {
1238
1014
  out.insert(
@@ -1243,6 +1019,122 @@ fn report_result_for_owner_team_inner(
1243
1019
  Ok(serde_json::Value::Object(out))
1244
1020
  }
1245
1021
 
1022
+ fn report_owner_state(state: &serde_json::Value, owner_team: &str) -> serde_json::Value {
1023
+ let mut state = match crate::state::projection::resolve_owner_team_id(state, owner_team)
1024
+ .canonical_key()
1025
+ {
1026
+ Some(team) => crate::state::projection::project_top_level_view(state, team),
1027
+ None => state.clone(),
1028
+ };
1029
+ if let Some(obj) = state.as_object_mut() {
1030
+ obj.insert(
1031
+ "active_team_key".to_string(),
1032
+ serde_json::Value::String(owner_team.to_string()),
1033
+ );
1034
+ }
1035
+ state
1036
+ }
1037
+
1038
+ fn leader_result_presentation(task_id: &str) -> super::presentation::PresentationDecision {
1039
+ super::presentation::decide_presentation(
1040
+ &super::presentation::PresentationRequest {
1041
+ sink: super::presentation::PresentationSink::Leader,
1042
+ class: super::presentation::PresentationClass::StageResult,
1043
+ case_id: Some(task_id.to_string()),
1044
+ },
1045
+ super::presentation::PresentationSource::ReportResult,
1046
+ )
1047
+ }
1048
+
1049
+ const LEADER_NOTIFICATION_LIMIT_BYTES: usize = 160;
1050
+
1051
+ fn truncate_utf8(text: &str, max_bytes: usize) -> String {
1052
+ if text.len() <= max_bytes {
1053
+ return text.to_string();
1054
+ }
1055
+ let suffix = "...";
1056
+ let budget = max_bytes.saturating_sub(suffix.len());
1057
+ let mut end = 0;
1058
+ for (index, ch) in text.char_indices() {
1059
+ let next = index + ch.len_utf8();
1060
+ if next > budget {
1061
+ break;
1062
+ }
1063
+ end = next;
1064
+ }
1065
+ format!("{}{}", &text[..end], suffix)
1066
+ }
1067
+
1068
+ fn format_report_result_leader_summary(
1069
+ result_id: &str,
1070
+ task_id: &str,
1071
+ agent_id: &str,
1072
+ status: &str,
1073
+ envelope: &serde_json::Value,
1074
+ ) -> String {
1075
+ let summary = envelope
1076
+ .get("summary")
1077
+ .and_then(serde_json::Value::as_str)
1078
+ .unwrap_or("")
1079
+ .lines()
1080
+ .next()
1081
+ .unwrap_or("")
1082
+ .chars()
1083
+ .filter(|ch| !ch.is_control())
1084
+ .collect::<String>();
1085
+ truncate_utf8(
1086
+ &format!(
1087
+ "Task {task_id} reported {status} from {agent_id}: {summary}; Result id: {result_id}"
1088
+ ),
1089
+ LEADER_NOTIFICATION_LIMIT_BYTES,
1090
+ )
1091
+ }
1092
+
1093
+ fn auto_finalize_result(
1094
+ workspace: &Path,
1095
+ conn: &rusqlite::Connection,
1096
+ state: &serde_json::Value,
1097
+ owner_team: &str,
1098
+ result_id: &str,
1099
+ task_id: &str,
1100
+ agent_id: &str,
1101
+ ) -> Result<Option<&'static str>, MessagingError> {
1102
+ let message_scoped = is_message_scoped_result(conn, task_id, agent_id, Some(owner_team))?;
1103
+ let scope = if message_scoped { "message" } else { "task" };
1104
+ let projected_task_id = if message_scoped {
1105
+ crate::mcp_server::latest_task_for_assignee(workspace, agent_id, Some(owner_team))
1106
+ } else if task_exists_for_owner(state, owner_team, task_id) {
1107
+ Some(task_id.to_string())
1108
+ } else {
1109
+ None
1110
+ };
1111
+ if let Some(projected_task_id) = projected_task_id {
1112
+ crate::state::repository::StateRepository::new(workspace).commit(
1113
+ crate::state::repository::StateWriteIntent::ResultCollection {
1114
+ owner_team_id: Some(owner_team),
1115
+ },
1116
+ |latest| mark_task_done(latest, &projected_task_id, result_id),
1117
+ )?;
1118
+ }
1119
+ let finalized = conn.execute(
1120
+ "update results set status = 'collected' where result_id = ?1 and owner_team_id = ?2 and status not in ('collected', 'invalid')",
1121
+ params![result_id, owner_team],
1122
+ )?;
1123
+ if finalized == 0 {
1124
+ return Ok(None);
1125
+ }
1126
+ EventLog::new(workspace).write(
1127
+ "collect.result",
1128
+ serde_json::json!({
1129
+ "result_id": result_id,
1130
+ "task_id": task_id,
1131
+ "agent_id": agent_id,
1132
+ "scope": scope,
1133
+ }),
1134
+ )?;
1135
+ Ok(Some(scope))
1136
+ }
1137
+
1246
1138
  fn task_result_route(
1247
1139
  state: &serde_json::Value,
1248
1140
  owner_team: &str,
@@ -1291,55 +1183,6 @@ fn copy_report_attribution_fields(
1291
1183
  }
1292
1184
  }
1293
1185
 
1294
- fn fallback_primary_error_text(cli_primary_error: Option<&str>, observed: String) -> String {
1295
- match cli_primary_error.filter(|error| !error.trim().is_empty()) {
1296
- Some(error) => format!("{error}; {observed}"),
1297
- None => observed,
1298
- }
1299
- }
1300
-
1301
- fn report_owner_state(state: &serde_json::Value, owner_team: &str) -> serde_json::Value {
1302
- let mut state =
1303
- match crate::state::projection::resolve_owner_team_id(state, owner_team).canonical_key() {
1304
- Some(team) => crate::state::projection::project_top_level_view(state, team),
1305
- None => state.clone(),
1306
- };
1307
- if let Some(obj) = state.as_object_mut() {
1308
- obj.insert(
1309
- "active_team_key".to_string(),
1310
- serde_json::Value::String(owner_team.to_string()),
1311
- );
1312
- }
1313
- state
1314
- }
1315
-
1316
- fn report_state_has_app_server_receiver(state: &serde_json::Value) -> bool {
1317
- report_leader_receiver_value(state).is_some_and(crate::codex_app_server::receiver_is_app_server)
1318
- }
1319
-
1320
- fn report_leader_receiver_value(state: &serde_json::Value) -> Option<&serde_json::Value> {
1321
- state
1322
- .get("leader_receiver")
1323
- .or_else(|| {
1324
- state
1325
- .get("active_team_key")
1326
- .and_then(serde_json::Value::as_str)
1327
- .and_then(|team| state.get("teams").and_then(|teams| teams.get(team)))
1328
- .and_then(|team| team.get("leader_receiver"))
1329
- })
1330
- .or_else(|| {
1331
- let teams = state.get("teams").and_then(serde_json::Value::as_object)?;
1332
- if teams.len() == 1 {
1333
- teams
1334
- .values()
1335
- .next()
1336
- .and_then(|team| team.get("leader_receiver"))
1337
- } else {
1338
- None
1339
- }
1340
- })
1341
- }
1342
-
1343
1186
  fn insert_result_if_absent(
1344
1187
  conn: &rusqlite::Connection,
1345
1188
  result_id: &str,
@@ -1543,20 +1386,6 @@ fn format_report_result_next_actions(envelope: &serde_json::Value) -> Option<Str
1543
1386
  }
1544
1387
  }
1545
1388
 
1546
- /// `_collect_results_and_notify_watchers` (`results.py:430`):coordinator tick 调用 —— collect +
1547
- /// notify_result_watchers 编排。daemon-path → Result。
1548
- pub fn collect_results_and_notify_watchers(
1549
- workspace: &Path,
1550
- event_log: &EventLog,
1551
- ) -> Result<serde_json::Value, MessagingError> {
1552
- let notified = retry_result_deliveries(workspace, event_log)?;
1553
- Ok(serde_json::json!({
1554
- "ok": true,
1555
- "collected": 0,
1556
- "notified": notified
1557
- }))
1558
- }
1559
-
1560
1389
  #[cfg(test)]
1561
1390
  mod collection_tests;
1562
1391
 
@@ -1611,6 +1440,21 @@ mod tests {
1611
1440
  assert!(notification.contains("Result id: res_1"));
1612
1441
  }
1613
1442
 
1443
+ #[test]
1444
+ fn leader_result_summary_is_single_line_and_byte_bounded() {
1445
+ let envelope = serde_json::json!({"summary": "中".repeat(4096)});
1446
+ let summary = super::format_report_result_leader_summary(
1447
+ "res-1",
1448
+ "task-1",
1449
+ "worker",
1450
+ "success",
1451
+ &envelope,
1452
+ );
1453
+ assert!(summary.len() <= 160);
1454
+ assert!(!summary.contains('\n'));
1455
+ assert!(summary.ends_with("..."));
1456
+ }
1457
+
1614
1458
  #[test]
1615
1459
  fn casefile_result_is_stored_without_a_leader_notification_row() {
1616
1460
  let workspace = std::env::temp_dir().join(format!(
@@ -1640,7 +1484,7 @@ mod tests {
1640
1484
  }),
1641
1485
  )
1642
1486
  .unwrap();
1643
- assert_eq!(out["notification_status"], "stored_not_presented");
1487
+ assert_eq!(out["notification_status"], "auto_finalized");
1644
1488
  assert_eq!(out["leader_notified"], false);
1645
1489
  let store = MessageStore::open(&workspace).unwrap();
1646
1490
  let conn = crate::db::schema::open_db(store.db_path()).unwrap();
@@ -1650,7 +1494,11 @@ mod tests {
1650
1494
  let message_count: i64 = conn
1651
1495
  .query_row("select count(*) from messages", [], |row| row.get(0))
1652
1496
  .unwrap();
1497
+ let status: String = conn
1498
+ .query_row("select status from results", [], |row| row.get(0))
1499
+ .unwrap();
1653
1500
  assert_eq!(result_count, 1);
1501
+ assert_eq!(status, "collected");
1654
1502
  assert_eq!(message_count, 0);
1655
1503
  }
1656
1504
  }