@team-agent/installer 0.5.53 → 0.5.54
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/emit.rs +94 -3
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/lane_c.rs +3 -3
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +1 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +4 -4
- package/crates/team-agent/src/db/message_store.rs +31 -2
- package/crates/team-agent/src/db/migration.rs +7 -6
- package/crates/team-agent/src/db/schema.rs +18 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +8 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +71 -0
- package/crates/team-agent/src/mcp_server/types.rs +4 -0
- package/crates/team-agent/src/mcp_server/wire.rs +42 -4
- package/crates/team-agent/src/messaging/delivery.rs +2 -0
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +32 -0
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/persist.rs +68 -2
- package/crates/team-agent/src/messaging/presentation.rs +307 -0
- package/crates/team-agent/src/messaging/results.rs +130 -3
- package/crates/team-agent/src/messaging/selftest.rs +1 -0
- package/crates/team-agent/src/messaging/send.rs +54 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +51 -1
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/watchers.rs +1 -0
- package/crates/team-agent/src/provider/session/capture.rs +74 -0
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +112 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +14 -86
- package/package.json +4 -4
- package/schemas/result-envelope.schema.json +10 -0
- package/skills/team-agent/SKILL.md +3 -0
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -317,8 +317,11 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
317
317
|
Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
|
|
318
318
|
Some("send") => concat!(
|
|
319
319
|
"usage: team-agent send TO MESSAGE... ",
|
|
320
|
-
"[--workspace WORKSPACE] [--team TEAM]
|
|
321
|
-
"
|
|
320
|
+
"[--workspace WORKSPACE] [--team TEAM] ",
|
|
321
|
+
"[--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]] ",
|
|
322
|
+
"[--json]\n\n",
|
|
323
|
+
"TO is a logical recipient; send returns after the message is persisted. ",
|
|
324
|
+
"Presentation changes live display only; every sink remains durable."
|
|
322
325
|
)
|
|
323
326
|
.to_string(),
|
|
324
327
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
@@ -775,6 +778,9 @@ struct ParsedArgs {
|
|
|
775
778
|
socket: Option<String>,
|
|
776
779
|
thread_id: Option<String>,
|
|
777
780
|
message_id: Option<String>,
|
|
781
|
+
presentation_sink: Option<String>,
|
|
782
|
+
message_class: Option<String>,
|
|
783
|
+
case_id: Option<String>,
|
|
778
784
|
content: Option<String>,
|
|
779
785
|
primary_error: Option<String>,
|
|
780
786
|
agent_id: Option<String>,
|
|
@@ -865,6 +871,9 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
865
871
|
"--socket" => parsed.socket = next_arg(args, &mut i),
|
|
866
872
|
"--thread-id" => parsed.thread_id = next_arg(args, &mut i),
|
|
867
873
|
"--message-id" => parsed.message_id = next_arg(args, &mut i),
|
|
874
|
+
"--presentation-sink" => parsed.presentation_sink = next_arg(args, &mut i),
|
|
875
|
+
"--message-class" => parsed.message_class = next_arg(args, &mut i),
|
|
876
|
+
"--case-id" => parsed.case_id = next_arg(args, &mut i),
|
|
868
877
|
"--content" => parsed.content = next_arg(args, &mut i),
|
|
869
878
|
"--primary-error" => parsed.primary_error = next_arg(args, &mut i),
|
|
870
879
|
"--result-json" => parsed.result_json = next_arg(args, &mut i),
|
|
@@ -881,6 +890,17 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
881
890
|
other if other.starts_with("--to-leader=") => {
|
|
882
891
|
parsed.to_leader = Some(other.trim_start_matches("--to-leader=").to_string());
|
|
883
892
|
}
|
|
893
|
+
other if other.starts_with("--presentation-sink=") => {
|
|
894
|
+
parsed.presentation_sink =
|
|
895
|
+
Some(other.trim_start_matches("--presentation-sink=").to_string());
|
|
896
|
+
}
|
|
897
|
+
other if other.starts_with("--message-class=") => {
|
|
898
|
+
parsed.message_class =
|
|
899
|
+
Some(other.trim_start_matches("--message-class=").to_string());
|
|
900
|
+
}
|
|
901
|
+
other if other.starts_with("--case-id=") => {
|
|
902
|
+
parsed.case_id = Some(other.trim_start_matches("--case-id=").to_string());
|
|
903
|
+
}
|
|
884
904
|
other if other.starts_with("--provider=") => {
|
|
885
905
|
parsed.provider = Some(other.trim_start_matches("--provider=").to_string());
|
|
886
906
|
}
|
|
@@ -1029,6 +1049,23 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
|
1029
1049
|
};
|
|
1030
1050
|
let message_start = usize::from(target.is_some());
|
|
1031
1051
|
let workspace = workspace(&parsed, cwd);
|
|
1052
|
+
let presentation_value = if parsed.presentation_sink.is_some()
|
|
1053
|
+
|| parsed.message_class.is_some()
|
|
1054
|
+
|| parsed.case_id.is_some()
|
|
1055
|
+
{
|
|
1056
|
+
Some(serde_json::json!({
|
|
1057
|
+
"sink": parsed.presentation_sink.clone(),
|
|
1058
|
+
"class": parsed.message_class.clone(),
|
|
1059
|
+
"case_id": parsed.case_id.clone(),
|
|
1060
|
+
}))
|
|
1061
|
+
} else {
|
|
1062
|
+
None
|
|
1063
|
+
};
|
|
1064
|
+
let (presentation, presentation_error) =
|
|
1065
|
+
crate::messaging::presentation::normalize_presentation(presentation_value.as_ref());
|
|
1066
|
+
if let Some(error) = presentation_error {
|
|
1067
|
+
return Err(CliError::Usage(format!("invalid presentation: {error}")));
|
|
1068
|
+
}
|
|
1032
1069
|
Ok(SendArgs {
|
|
1033
1070
|
target,
|
|
1034
1071
|
message: parsed
|
|
@@ -1049,6 +1086,7 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
|
1049
1086
|
confirm_human: false,
|
|
1050
1087
|
json: parsed.json,
|
|
1051
1088
|
message_id: None,
|
|
1089
|
+
presentation,
|
|
1052
1090
|
pane: parsed.pane.clone(),
|
|
1053
1091
|
to_name: parsed.to_name.clone(),
|
|
1054
1092
|
to_leader: parsed.to_leader.clone(),
|
|
@@ -1111,11 +1149,22 @@ fn validate_send_flags(args: &[String]) -> Result<(), CliError> {
|
|
|
1111
1149
|
"--timeout",
|
|
1112
1150
|
"--confirm-human",
|
|
1113
1151
|
"--message-id",
|
|
1152
|
+
"--presentation-sink",
|
|
1153
|
+
"--message-class",
|
|
1154
|
+
"--case-id",
|
|
1114
1155
|
"--json",
|
|
1115
1156
|
"-h",
|
|
1116
1157
|
"--help",
|
|
1117
1158
|
];
|
|
1118
|
-
const ALLOWED_PREFIXES: &[&str] = &[
|
|
1159
|
+
const ALLOWED_PREFIXES: &[&str] = &[
|
|
1160
|
+
"--team=",
|
|
1161
|
+
"--pane=",
|
|
1162
|
+
"--to-name=",
|
|
1163
|
+
"--to-leader=",
|
|
1164
|
+
"--presentation-sink=",
|
|
1165
|
+
"--message-class=",
|
|
1166
|
+
"--case-id=",
|
|
1167
|
+
];
|
|
1119
1168
|
if let Some(flag) = args.iter().find(|arg| {
|
|
1120
1169
|
arg.starts_with('-')
|
|
1121
1170
|
&& !ALLOWED.contains(&arg.as_str())
|
|
@@ -2015,6 +2064,48 @@ mod tests {
|
|
|
2015
2064
|
let _ = std::fs::remove_dir_all(&cwd);
|
|
2016
2065
|
}
|
|
2017
2066
|
|
|
2067
|
+
#[test]
|
|
2068
|
+
fn send_presentation_flags_map_to_the_typed_request() {
|
|
2069
|
+
let cwd = tmp_workspace();
|
|
2070
|
+
let args = send_args(
|
|
2071
|
+
&cli_argv(&[
|
|
2072
|
+
"leader",
|
|
2073
|
+
"progress",
|
|
2074
|
+
"--presentation-sink",
|
|
2075
|
+
"casefile",
|
|
2076
|
+
"--message-class",
|
|
2077
|
+
"progress",
|
|
2078
|
+
"--case-id",
|
|
2079
|
+
"case-9",
|
|
2080
|
+
]),
|
|
2081
|
+
&cwd,
|
|
2082
|
+
)
|
|
2083
|
+
.unwrap();
|
|
2084
|
+
assert_eq!(
|
|
2085
|
+
args.presentation.sink,
|
|
2086
|
+
crate::messaging::presentation::PresentationSink::Casefile
|
|
2087
|
+
);
|
|
2088
|
+
assert_eq!(
|
|
2089
|
+
args.presentation.class,
|
|
2090
|
+
crate::messaging::presentation::PresentationClass::Progress
|
|
2091
|
+
);
|
|
2092
|
+
assert_eq!(args.presentation.case_id.as_deref(), Some("case-9"));
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
#[test]
|
|
2096
|
+
fn send_presentation_flags_fail_closed_when_incomplete() {
|
|
2097
|
+
let cwd = tmp_workspace();
|
|
2098
|
+
let error = send_args(
|
|
2099
|
+
&cli_argv(&["leader", "progress", "--presentation-sink", "casefile"]),
|
|
2100
|
+
&cwd,
|
|
2101
|
+
)
|
|
2102
|
+
.unwrap_err();
|
|
2103
|
+
assert!(matches!(
|
|
2104
|
+
error,
|
|
2105
|
+
CliError::Usage(message) if message == "invalid presentation: missing_class"
|
|
2106
|
+
));
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2018
2109
|
#[test]
|
|
2019
2110
|
fn send_to_name_positionals_are_message_not_target() {
|
|
2020
2111
|
let cwd = tmp_workspace();
|
|
@@ -25,6 +25,7 @@ pub fn send_options_from_args(args: &SendArgs) -> SendOptions {
|
|
|
25
25
|
watch_result: args.watch_result,
|
|
26
26
|
team: args.team.as_ref().map(|s| TeamKey::new(s.clone())),
|
|
27
27
|
message_id: args.message_id.clone(),
|
|
28
|
+
presentation: args.presentation.clone(),
|
|
28
29
|
..SendOptions::default()
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -285,6 +285,7 @@ pub(super) fn first_target(target: &MessageTarget) -> String {
|
|
|
285
285
|
pub(super) fn delivery_status_wire(status: DeliveryStatus) -> &'static str {
|
|
286
286
|
match status {
|
|
287
287
|
DeliveryStatus::Delivered => "delivered",
|
|
288
|
+
DeliveryStatus::StoredOnly => "stored_only",
|
|
288
289
|
DeliveryStatus::Failed => "failed",
|
|
289
290
|
DeliveryStatus::Queued => "queued",
|
|
290
291
|
DeliveryStatus::Blocked => "blocked",
|
|
@@ -155,7 +155,7 @@ pub(crate) struct CommandSpec {
|
|
|
155
155
|
#[rustfmt::skip]
|
|
156
156
|
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
157
157
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
158
|
-
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for a logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
158
|
+
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for a logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
159
159
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
160
160
|
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
161
161
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -361,7 +361,7 @@ fn cleanup_orphans_dryrun_golden_envelope() {
|
|
|
361
361
|
// ── fix_schema: golden fix_schema_layout diagnosis envelope (schema_migration.py:258) ─────────────
|
|
362
362
|
// {ok,status,db_path,schema_version,user_version,layout_diffs,recommended_action,would_backup_path,
|
|
363
363
|
// fixed,rebuilds}. RUST mod.rs:538-540 stub {ok,fixed:false}. db_path/would_backup_path are
|
|
364
|
-
// path/clock-derived; lock the deterministic fields
|
|
364
|
+
// path/clock-derived; lock the deterministic fields against the canonical SCHEMA_VERSION. ────────
|
|
365
365
|
#[test]
|
|
366
366
|
fn fix_schema_golden_layout_diagnosis_envelope() {
|
|
367
367
|
let ws = tmp_workspace();
|
|
@@ -370,8 +370,8 @@ fn fix_schema_golden_layout_diagnosis_envelope() {
|
|
|
370
370
|
assert_eq!(v["status"], json!("missing"), "missing db: status missing");
|
|
371
371
|
assert_eq!(
|
|
372
372
|
v["schema_version"],
|
|
373
|
-
json!(
|
|
374
|
-
"golden schema_version == SCHEMA_VERSION
|
|
373
|
+
json!(crate::db::schema::SCHEMA_VERSION),
|
|
374
|
+
"golden schema_version == canonical SCHEMA_VERSION"
|
|
375
375
|
);
|
|
376
376
|
assert_eq!(v["user_version"], json!(0), "missing db user_version == 0");
|
|
377
377
|
assert_eq!(
|
|
@@ -157,6 +157,7 @@ fn named_send_args(
|
|
|
157
157
|
confirm_human: false,
|
|
158
158
|
json: true,
|
|
159
159
|
message_id: None,
|
|
160
|
+
presentation: crate::messaging::presentation::PresentationRequest::default(),
|
|
160
161
|
pane: pane.map(str::to_string),
|
|
161
162
|
to_name: to_name.map(str::to_string),
|
|
162
163
|
to_leader: None,
|
|
@@ -323,6 +323,7 @@ pub struct SendArgs {
|
|
|
323
323
|
/// When set, the store insert uses this id verbatim; a repeat with the same
|
|
324
324
|
/// id returns a `Duplicate` refusal instead of creating a second row.
|
|
325
325
|
pub message_id: Option<String>,
|
|
326
|
+
pub presentation: crate::messaging::presentation::PresentationRequest,
|
|
326
327
|
/// Deprecated compatibility input. Public send refuses pane identity and
|
|
327
328
|
/// requires a logical recipient so persistence always precedes delivery.
|
|
328
329
|
pub pane: Option<String>,
|
|
@@ -365,16 +365,16 @@ fn orphan_self_terminate_false_when_workspace_exists() {
|
|
|
365
365
|
|
|
366
366
|
#[test]
|
|
367
367
|
fn metadata_ok_requires_all_three_to_match() {
|
|
368
|
-
// metadata.py:37-43 — pid
|
|
369
|
-
let good = meta(555, PROTOCOL_VERSION,
|
|
368
|
+
// metadata.py:37-43 — pid, protocol version, and current message schema all match.
|
|
369
|
+
let good = meta(555, PROTOCOL_VERSION, crate::db::schema::SCHEMA_VERSION);
|
|
370
370
|
assert!(coordinator_metadata_ok(Some(&good), Pid(555)));
|
|
371
371
|
// pid 不符 → false。
|
|
372
372
|
assert!(!coordinator_metadata_ok(Some(&good), Pid(999)));
|
|
373
373
|
// protocol_version 不符(bump 触发 restart_incompatible)→ false。
|
|
374
|
-
let bad_proto = meta(555, 1,
|
|
374
|
+
let bad_proto = meta(555, 1, crate::db::schema::SCHEMA_VERSION);
|
|
375
375
|
assert!(!coordinator_metadata_ok(Some(&bad_proto), Pid(555)));
|
|
376
376
|
// schema_version 不符 → false(不可静默继续旧 schema 写库,card §89)。
|
|
377
|
-
let bad_schema = meta(555, PROTOCOL_VERSION,
|
|
377
|
+
let bad_schema = meta(555, PROTOCOL_VERSION, crate::db::schema::SCHEMA_VERSION - 1);
|
|
378
378
|
assert!(!coordinator_metadata_ok(Some(&bad_schema), Pid(555)));
|
|
379
379
|
}
|
|
380
380
|
|
|
@@ -85,6 +85,7 @@ pub struct NotificationClaimParams<'a> {
|
|
|
85
85
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
86
86
|
pub enum MessageRowStatus {
|
|
87
87
|
Accepted,
|
|
88
|
+
StoredOnly,
|
|
88
89
|
QueuedUntilLeaderAttach,
|
|
89
90
|
QueuedCoordinatorUnavailable,
|
|
90
91
|
}
|
|
@@ -93,6 +94,7 @@ impl MessageRowStatus {
|
|
|
93
94
|
pub const fn as_str(self) -> &'static str {
|
|
94
95
|
match self {
|
|
95
96
|
Self::Accepted => "accepted",
|
|
97
|
+
Self::StoredOnly => "stored_only",
|
|
96
98
|
Self::QueuedUntilLeaderAttach => "queued_until_leader_attach",
|
|
97
99
|
Self::QueuedCoordinatorUnavailable => "queued_coordinator_unavailable",
|
|
98
100
|
}
|
|
@@ -111,6 +113,7 @@ pub struct PersistMessageInput<'a> {
|
|
|
111
113
|
pub requires_ack: bool,
|
|
112
114
|
pub status: MessageRowStatus,
|
|
113
115
|
pub content: &'a str,
|
|
116
|
+
pub presentation: &'a str,
|
|
114
117
|
pub error: Option<&'a str>,
|
|
115
118
|
}
|
|
116
119
|
|
|
@@ -182,9 +185,9 @@ impl MessageStore {
|
|
|
182
185
|
conn.execute(
|
|
183
186
|
"insert into messages(
|
|
184
187
|
message_id, owner_team_id, task_id, sender, recipient, reply_to, requires_ack,
|
|
185
|
-
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
188
|
+
status, content, presentation, artifact_refs, created_at, updated_at, delivered_at,
|
|
186
189
|
acknowledged_at, error, delivery_attempts
|
|
187
|
-
) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, '[]', ?
|
|
190
|
+
) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, '[]', ?11, ?11, null, null, ?12, 0)",
|
|
188
191
|
params![
|
|
189
192
|
message_id,
|
|
190
193
|
input.owner_team_id,
|
|
@@ -195,6 +198,7 @@ impl MessageStore {
|
|
|
195
198
|
if input.requires_ack { 1 } else { 0 },
|
|
196
199
|
input.status.as_str(),
|
|
197
200
|
input.content,
|
|
201
|
+
input.presentation,
|
|
198
202
|
now,
|
|
199
203
|
input.error,
|
|
200
204
|
],
|
|
@@ -226,6 +230,7 @@ impl MessageStore {
|
|
|
226
230
|
requires_ack,
|
|
227
231
|
status: MessageRowStatus::Accepted,
|
|
228
232
|
content,
|
|
233
|
+
presentation: r#"{"sink":"leader","class":"message"}"#,
|
|
229
234
|
error: None,
|
|
230
235
|
})
|
|
231
236
|
}
|
|
@@ -259,6 +264,7 @@ impl MessageStore {
|
|
|
259
264
|
requires_ack,
|
|
260
265
|
status: MessageRowStatus::Accepted,
|
|
261
266
|
content,
|
|
267
|
+
presentation: r#"{"sink":"leader","class":"message"}"#,
|
|
262
268
|
error: None,
|
|
263
269
|
})
|
|
264
270
|
}
|
|
@@ -809,6 +815,29 @@ mod tests {
|
|
|
809
815
|
assert_eq!(status_of(&read(&s), &mid), "failed");
|
|
810
816
|
}
|
|
811
817
|
|
|
818
|
+
#[test]
|
|
819
|
+
fn claim_for_delivery_never_claims_stored_only_presentation() {
|
|
820
|
+
let s = store();
|
|
821
|
+
let mid = s
|
|
822
|
+
.persist_message(PersistMessageInput {
|
|
823
|
+
message_id: None,
|
|
824
|
+
owner_team_id: Some("team-a"),
|
|
825
|
+
task_id: None,
|
|
826
|
+
sender: "worker",
|
|
827
|
+
recipient: "leader",
|
|
828
|
+
reply_to: None,
|
|
829
|
+
requires_ack: false,
|
|
830
|
+
status: MessageRowStatus::StoredOnly,
|
|
831
|
+
content: "casefile evidence",
|
|
832
|
+
presentation: r#"{"sink":"casefile","class":"stage_result"}"#,
|
|
833
|
+
error: None,
|
|
834
|
+
})
|
|
835
|
+
.unwrap();
|
|
836
|
+
assert!(!s.claim_for_delivery(&mid).unwrap());
|
|
837
|
+
assert_eq!(status_of(&read(&s), &mid), "stored_only");
|
|
838
|
+
assert_eq!(col_i64(&read(&s), &mid, "delivery_attempts"), 0);
|
|
839
|
+
}
|
|
840
|
+
|
|
812
841
|
// ───────────────────────────── mark state machine ─────────────────────────────
|
|
813
842
|
|
|
814
843
|
#[test]
|
|
@@ -26,6 +26,7 @@ pub const MANAGED_TABLE_LAYOUTS: &[(&str, &[&str])] = &[
|
|
|
26
26
|
"requires_ack",
|
|
27
27
|
"status",
|
|
28
28
|
"content",
|
|
29
|
+
"presentation",
|
|
29
30
|
"artifact_refs",
|
|
30
31
|
"created_at",
|
|
31
32
|
"updated_at",
|
|
@@ -121,7 +122,7 @@ pub const MANAGED_TABLE_LAYOUTS: &[(&str, &[&str])] = &[
|
|
|
121
122
|
|
|
122
123
|
/// rebuild / 建缺表用的 DDL 模板(`schema_migration.py:CREATE_TABLE_SQL`,`__TABLE__` 占位)。
|
|
123
124
|
const CREATE_TABLE_TEMPLATES: &[(&str, &str)] = &[
|
|
124
|
-
("messages", "create table if not exists __TABLE__ (\n message_id text primary key,\n owner_team_id text,\n task_id text,\n sender text,\n recipient text,\n reply_to text,\n requires_ack integer,\n status text,\n content text,\n artifact_refs text,\n created_at text,\n updated_at text,\n delivered_at text,\n acknowledged_at text,\n error text,\n delivery_attempts integer not null default 0\n )"),
|
|
125
|
+
("messages", "create table if not exists __TABLE__ (\n message_id text primary key,\n owner_team_id text,\n task_id text,\n sender text,\n recipient text,\n reply_to text,\n requires_ack integer,\n status text,\n content text,\n presentation text not null default '{\"sink\":\"leader\",\"class\":\"message\"}',\n artifact_refs text,\n created_at text,\n updated_at text,\n delivered_at text,\n acknowledged_at text,\n error text,\n delivery_attempts integer not null default 0\n )"),
|
|
125
126
|
("results", "create table if not exists __TABLE__ (\n result_id text primary key,\n owner_team_id text,\n task_id text not null,\n agent_id text not null,\n envelope text not null,\n status text not null,\n created_at text not null\n )"),
|
|
126
127
|
("scheduled_events", "create table if not exists __TABLE__ (\n id integer primary key,\n owner_team_id text,\n due_at text not null,\n target text not null,\n kind text not null,\n payload_json text not null,\n status text not null,\n created_at text not null,\n fired_at text,\n result_json text\n )"),
|
|
127
128
|
("delivery_tokens", "create table if not exists __TABLE__ (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )"),
|
|
@@ -641,11 +642,11 @@ mod tests {
|
|
|
641
642
|
assert_eq!(m1, "t");
|
|
642
643
|
drop(conn);
|
|
643
644
|
|
|
644
|
-
// 迁移后 diagnosis = ok / user_version
|
|
645
|
+
// 迁移后 diagnosis = ok / current user_version。
|
|
645
646
|
let after = schema_diagnosis(&path, SCHEMA_VERSION).unwrap();
|
|
646
647
|
assert!(after.ok);
|
|
647
648
|
assert_eq!(after.status, "ok");
|
|
648
|
-
assert_eq!(after.user_version,
|
|
649
|
+
assert_eq!(after.user_version, SCHEMA_VERSION);
|
|
649
650
|
|
|
650
651
|
// 备份文件已写(team.db.pre-migration-*-from-v1.bak)。
|
|
651
652
|
let runtime = path.parent().unwrap();
|
|
@@ -670,7 +671,7 @@ mod tests {
|
|
|
670
671
|
let d = schema_diagnosis(&path, SCHEMA_VERSION).unwrap();
|
|
671
672
|
assert!(d.ok);
|
|
672
673
|
assert_eq!(d.status, "ok");
|
|
673
|
-
assert_eq!(d.user_version,
|
|
674
|
+
assert_eq!(d.user_version, SCHEMA_VERSION);
|
|
674
675
|
}
|
|
675
676
|
|
|
676
677
|
#[test]
|
|
@@ -788,7 +789,7 @@ mod tests {
|
|
|
788
789
|
initialize_schema(&conn, Some(&path)).unwrap();
|
|
789
790
|
let cols = table_layout(&conn, "messages").unwrap();
|
|
790
791
|
assert!(!cols.iter().any(|c| c == "legacy_junk"), "废列应被丢弃");
|
|
791
|
-
assert_eq!(cols.len(),
|
|
792
|
+
assert_eq!(cols.len(), 17);
|
|
792
793
|
assert_eq!(table_count(&conn, "messages").unwrap(), 1);
|
|
793
794
|
}
|
|
794
795
|
|
|
@@ -846,7 +847,7 @@ mod tests {
|
|
|
846
847
|
rebuilds,
|
|
847
848
|
} => {
|
|
848
849
|
assert!(diagnosis.ok);
|
|
849
|
-
assert_eq!(diagnosis.user_version,
|
|
850
|
+
assert_eq!(diagnosis.user_version, SCHEMA_VERSION);
|
|
850
851
|
assert_eq!(rebuilds.len(), 8);
|
|
851
852
|
}
|
|
852
853
|
other => panic!("expected Fixed, got {other:?}"),
|
|
@@ -12,11 +12,11 @@ use rusqlite::Connection;
|
|
|
12
12
|
use crate::db::DbError;
|
|
13
13
|
|
|
14
14
|
/// `schema.py:90`。
|
|
15
|
-
pub const SCHEMA_VERSION: i64 =
|
|
15
|
+
pub const SCHEMA_VERSION: i64 = 4;
|
|
16
16
|
|
|
17
17
|
/// 8 张表的 DDL(逐字照搬 `schema.py:initialize_schema` 的内联建表;含 `if not exists`)。
|
|
18
18
|
/// 顺序与 Python 一致(leader_notification_log 在 ensure 块后创建)。
|
|
19
|
-
const CREATE_MESSAGES: &str = "create table if not exists messages (\n message_id text primary key,\n owner_team_id text,\n task_id text,\n sender text,\n recipient text,\n reply_to text,\n requires_ack integer,\n status text,\n content text,\n artifact_refs text,\n created_at text,\n updated_at text,\n delivered_at text,\n acknowledged_at text,\n error text,\n delivery_attempts integer not null default 0\n )";
|
|
19
|
+
const CREATE_MESSAGES: &str = "create table if not exists messages (\n message_id text primary key,\n owner_team_id text,\n task_id text,\n sender text,\n recipient text,\n reply_to text,\n requires_ack integer,\n status text,\n content text,\n presentation text not null default '{\"sink\":\"leader\",\"class\":\"message\"}',\n artifact_refs text,\n created_at text,\n updated_at text,\n delivered_at text,\n acknowledged_at text,\n error text,\n delivery_attempts integer not null default 0\n )";
|
|
20
20
|
const CREATE_RESULTS: &str = "create table if not exists results (\n result_id text primary key,\n owner_team_id text,\n task_id text not null,\n agent_id text not null,\n envelope text not null,\n status text not null,\n created_at text not null\n )";
|
|
21
21
|
const CREATE_SCHEDULED_EVENTS: &str = "create table if not exists scheduled_events (\n id integer primary key,\n owner_team_id text,\n due_at text not null,\n target text not null,\n kind text not null,\n payload_json text not null,\n status text not null,\n created_at text not null,\n fired_at text,\n result_json text\n )";
|
|
22
22
|
const CREATE_DELIVERY_TOKENS: &str = "create table if not exists delivery_tokens (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )";
|
|
@@ -47,6 +47,7 @@ const MESSAGE_COLUMNS: &[&str] = &[
|
|
|
47
47
|
"requires_ack",
|
|
48
48
|
"status",
|
|
49
49
|
"content",
|
|
50
|
+
"presentation",
|
|
50
51
|
"artifact_refs",
|
|
51
52
|
"created_at",
|
|
52
53
|
"updated_at",
|
|
@@ -264,6 +265,10 @@ pub fn initialize_schema(
|
|
|
264
265
|
"owner_team_id",
|
|
265
266
|
"alter table messages add column owner_team_id text",
|
|
266
267
|
),
|
|
268
|
+
(
|
|
269
|
+
"presentation",
|
|
270
|
+
"alter table messages add column presentation text not null default '{\"sink\":\"leader\",\"class\":\"message\"}'",
|
|
271
|
+
),
|
|
267
272
|
],
|
|
268
273
|
)?;
|
|
269
274
|
ensure_table_columns(
|
|
@@ -354,13 +359,13 @@ mod tests {
|
|
|
354
359
|
}
|
|
355
360
|
|
|
356
361
|
#[test]
|
|
357
|
-
fn
|
|
362
|
+
fn user_version_is_four() {
|
|
358
363
|
let conn = fresh();
|
|
359
364
|
let v: i64 = conn
|
|
360
365
|
.query_row("pragma user_version", [], |r| r.get(0))
|
|
361
366
|
.unwrap();
|
|
362
367
|
assert_eq!(v, SCHEMA_VERSION);
|
|
363
|
-
assert_eq!(v,
|
|
368
|
+
assert_eq!(v, 4);
|
|
364
369
|
}
|
|
365
370
|
|
|
366
371
|
#[test]
|
|
@@ -378,6 +383,7 @@ mod tests {
|
|
|
378
383
|
"requires_ack",
|
|
379
384
|
"status",
|
|
380
385
|
"content",
|
|
386
|
+
"presentation",
|
|
381
387
|
"artifact_refs",
|
|
382
388
|
"created_at",
|
|
383
389
|
"updated_at",
|
|
@@ -394,7 +400,7 @@ mod tests {
|
|
|
394
400
|
// initialize_schema 二次调用(if not exists / 无缺列)→ 不报错、schema 不变 + 索引仍在。
|
|
395
401
|
let conn = fresh();
|
|
396
402
|
initialize_schema(&conn, None).unwrap();
|
|
397
|
-
assert_eq!(table_layout(&conn, "messages").unwrap().len(),
|
|
403
|
+
assert_eq!(table_layout(&conn, "messages").unwrap().len(), 17);
|
|
398
404
|
let idx: i64 = conn
|
|
399
405
|
.query_row(
|
|
400
406
|
"select count(*) from sqlite_master where type='index' and sql is not null",
|
|
@@ -422,6 +428,13 @@ mod tests {
|
|
|
422
428
|
("requires_ack", "INTEGER", 0, None, 0),
|
|
423
429
|
("status", "TEXT", 0, None, 0),
|
|
424
430
|
("content", "TEXT", 0, None, 0),
|
|
431
|
+
(
|
|
432
|
+
"presentation",
|
|
433
|
+
"TEXT",
|
|
434
|
+
1,
|
|
435
|
+
Some("'{\"sink\":\"leader\",\"class\":\"message\"}'"),
|
|
436
|
+
0,
|
|
437
|
+
),
|
|
425
438
|
("artifact_refs", "TEXT", 0, None, 0),
|
|
426
439
|
("created_at", "TEXT", 0, None, 0),
|
|
427
440
|
("updated_at", "TEXT", 0, None, 0),
|
|
@@ -267,6 +267,7 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
267
267
|
confirm_human: false,
|
|
268
268
|
json: true,
|
|
269
269
|
message_id: Some("phase-golden-message".to_string()),
|
|
270
|
+
presentation: crate::messaging::presentation::PresentationRequest::default(),
|
|
270
271
|
pane: None,
|
|
271
272
|
to_name: None,
|
|
272
273
|
to_leader: None,
|
|
@@ -104,6 +104,12 @@ pub fn normalize_report_envelope(env: &Value) -> NormalizedReportEnvelope {
|
|
|
104
104
|
let summary = text_field(env, "summary").unwrap_or_else(|| "completed".to_string());
|
|
105
105
|
let task_id = text_field(env, "task_id").unwrap_or_else(|| "manual".to_string());
|
|
106
106
|
let agent_id = text_field(env, "agent_id").unwrap_or_else(|| "unknown".to_string());
|
|
107
|
+
let (presentation_request, presentation_error) =
|
|
108
|
+
crate::messaging::presentation::normalize_report_presentation(env.get("presentation"));
|
|
109
|
+
let presentation = crate::messaging::presentation::decide_presentation(
|
|
110
|
+
&presentation_request,
|
|
111
|
+
crate::messaging::presentation::PresentationSource::ReportResult,
|
|
112
|
+
);
|
|
107
113
|
NormalizedReportEnvelope {
|
|
108
114
|
schema_version: "result_envelope_v1".to_string(),
|
|
109
115
|
task_id: TaskId::new(task_id),
|
|
@@ -115,6 +121,8 @@ pub fn normalize_report_envelope(env: &Value) -> NormalizedReportEnvelope {
|
|
|
115
121
|
risks: normalize_risks(env.get("risks")),
|
|
116
122
|
artifacts: normalize_artifacts(env.get("artifacts")),
|
|
117
123
|
next_actions: normalize_next_actions(env.get("next_actions")),
|
|
124
|
+
presentation,
|
|
125
|
+
presentation_error,
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
@@ -79,7 +79,7 @@ fn tools_contract_has_thirteen_tools_in_order() {
|
|
|
79
79
|
.unwrap();
|
|
80
80
|
assert_eq!(
|
|
81
81
|
send["description"],
|
|
82
|
-
json!("Send a message to a teammate, the leader, or '*' for all other team members.
|
|
82
|
+
json!("Send a message to a teammate, the leader, or '*' for all other team members. Team Agent fills identity and delivery metadata; optional presentation routing is durable and never drops the message.")
|
|
83
83
|
);
|
|
84
84
|
assert_eq!(send["inputSchema"]["additionalProperties"], json!(false));
|
|
85
85
|
assert_eq!(send["inputSchema"]["required"], json!(["to", "content"]));
|