@team-agent/installer 0.5.7 → 0.5.9
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/attach_app_server_leader.rs +82 -1
- package/crates/team-agent/src/cli/diagnose.rs +46 -17
- package/crates/team-agent/src/cli/emit.rs +30 -1
- package/crates/team-agent/src/cli/leaders.rs +94 -0
- package/crates/team-agent/src/cli/mod.rs +138 -2
- package/crates/team-agent/src/cli/named_address.rs +288 -28
- package/crates/team-agent/src/cli/send.rs +230 -0
- package/crates/team-agent/src/cli/status_port.rs +117 -14
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +14 -0
- package/crates/team-agent/src/coordinator/tick.rs +7 -1
- package/crates/team-agent/src/db/agent_health_capture.rs +96 -0
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/registry.rs +402 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +13 -61
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +18 -1
- package/crates/team-agent/src/mcp_server/normalize.rs +79 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +125 -13
- package/crates/team-agent/src/mcp_server/tools.rs +24 -2
- package/crates/team-agent/src/messaging/delivery.rs +12 -5
- package/crates/team-agent/src/messaging/leader_receiver.rs +58 -0
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +53 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +7 -2
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/package.json +4 -4
|
@@ -112,29 +112,43 @@ fn send_outcome_direct_renders_compact_body() {
|
|
|
112
112
|
// ════════════════════════════════════════════════════════════════════════
|
|
113
113
|
// CONTROL-PLANE: send_message worker recipient → WorkerAccepted (tools.py:135-183)
|
|
114
114
|
// ════════════════════════════════════════════════════════════════════════
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// A worker recipient w/ a delivered message_id → async accepted carrying the
|
|
118
|
-
// byte-stable poll hint. Identity anchored on injected env (no candidate scan).
|
|
119
|
-
// golden: a leader WITH owner_team_id on an unseeded ws would hit the C23 cross-team
|
|
120
|
-
// refusal first (worker-1 not in visible peers) -> PeerNotInScope. owner_team_id=None
|
|
121
|
-
// (legacy single-team) bypasses that, isolating the worker-recipient accepted path.
|
|
122
|
-
// The cross-team refusal has its own tests (refuse_cross_team_peer_* / send_message_cross_team_*).
|
|
123
|
-
// A-7: the accepted path needs a REAL stored message_id (tools.py:175-181 —
|
|
124
|
-
// fabricated ids are gone), so the workspace seeds a running worker-1 the
|
|
125
|
-
// delivery layer can actually queue for.
|
|
126
|
-
let ws = unique_ws("send-worker");
|
|
115
|
+
fn seed_current_worker_state(tag: &str) -> std::path::PathBuf {
|
|
116
|
+
let ws = unique_ws(tag);
|
|
127
117
|
crate::state::persist::save_runtime_state(
|
|
128
118
|
&ws,
|
|
129
119
|
&serde_json::json!({
|
|
130
120
|
"session_name": "team-x",
|
|
121
|
+
"active_team_key": "current",
|
|
131
122
|
"agents": {
|
|
132
123
|
"worker-1": {"status": "running", "agent_id": "worker-1", "window": "worker-1"},
|
|
133
124
|
},
|
|
125
|
+
"teams": {
|
|
126
|
+
"current": {
|
|
127
|
+
"status": "alive",
|
|
128
|
+
"agents": {
|
|
129
|
+
"worker-1": {"status": "running", "agent_id": "worker-1", "window": "worker-1"},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
134
133
|
}),
|
|
135
134
|
)
|
|
136
135
|
.unwrap();
|
|
137
|
-
|
|
136
|
+
ws
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn send_message_worker_recipient_returns_accepted_with_poll_hint() {
|
|
141
|
+
// A worker recipient w/ a delivered message_id → async accepted carrying the
|
|
142
|
+
// byte-stable poll hint. Identity anchored on injected env (no candidate scan).
|
|
143
|
+
// A-7: the accepted path needs a REAL stored message_id (tools.py:175-181 —
|
|
144
|
+
// fabricated ids are gone), so the workspace seeds a running worker-1 the
|
|
145
|
+
// delivery layer can actually queue for.
|
|
146
|
+
let ws = seed_current_worker_state("send-worker");
|
|
147
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
148
|
+
&ws,
|
|
149
|
+
Some(AgentId::new("leader")),
|
|
150
|
+
Some(TeamKey::new("current")),
|
|
151
|
+
);
|
|
138
152
|
let outcome = tools.send_message(
|
|
139
153
|
&MessageTarget::Single("worker-1".to_string()),
|
|
140
154
|
"do the thing",
|
|
@@ -155,6 +169,104 @@ fn send_message_worker_recipient_returns_accepted_with_poll_hint() {
|
|
|
155
169
|
}
|
|
156
170
|
}
|
|
157
171
|
|
|
172
|
+
#[test]
|
|
173
|
+
fn ordinary_send_assign_shape_has_no_recovery_marker() {
|
|
174
|
+
let ws = seed_current_worker_state("ordinary-no-recovery");
|
|
175
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
176
|
+
&ws,
|
|
177
|
+
Some(AgentId::new("leader")),
|
|
178
|
+
Some(TeamKey::new("current")),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
let sent = tools
|
|
182
|
+
.send_message(
|
|
183
|
+
&MessageTarget::Single("worker-1".to_string()),
|
|
184
|
+
"ordinary send",
|
|
185
|
+
None,
|
|
186
|
+
None,
|
|
187
|
+
None,
|
|
188
|
+
None,
|
|
189
|
+
)
|
|
190
|
+
.expect("ordinary send ok")
|
|
191
|
+
.to_value();
|
|
192
|
+
assert!(
|
|
193
|
+
!sent.as_object().unwrap().contains_key("recovery"),
|
|
194
|
+
"ordinary send must not carry recovery marker: {sent}"
|
|
195
|
+
);
|
|
196
|
+
assert!(
|
|
197
|
+
sent.get("acceptance_marker").is_none(),
|
|
198
|
+
"ordinary send must not carry acceptance marker: {sent}"
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
let assigned = tools
|
|
202
|
+
.assign_task(
|
|
203
|
+
&json!({"id": "ordinary-task", "assignee": "worker-1", "title": "ordinary"}),
|
|
204
|
+
None,
|
|
205
|
+
)
|
|
206
|
+
.expect("ordinary assign ok");
|
|
207
|
+
let assigned_value = serde_json::to_value(&assigned).expect("serialize ordinary assign");
|
|
208
|
+
assert!(
|
|
209
|
+
!assigned_value.as_object().unwrap().contains_key("recovery"),
|
|
210
|
+
"ordinary assign must not carry recovery marker: {assigned_value}"
|
|
211
|
+
);
|
|
212
|
+
assert!(
|
|
213
|
+
assigned_value.get("acceptance_marker").is_none(),
|
|
214
|
+
"ordinary assign must not carry acceptance marker: {assigned_value}"
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
let recovery_false_assigned = tools
|
|
218
|
+
.assign_task(
|
|
219
|
+
&json!({
|
|
220
|
+
"id": "ordinary-recovery-false-task",
|
|
221
|
+
"assignee": "worker-1",
|
|
222
|
+
"title": "ordinary recovery false",
|
|
223
|
+
"recovery": false,
|
|
224
|
+
}),
|
|
225
|
+
None,
|
|
226
|
+
)
|
|
227
|
+
.expect("recovery=false assign ok");
|
|
228
|
+
let recovery_false_value =
|
|
229
|
+
serde_json::to_value(&recovery_false_assigned).expect("serialize recovery=false assign");
|
|
230
|
+
assert!(
|
|
231
|
+
!recovery_false_value
|
|
232
|
+
.as_object()
|
|
233
|
+
.unwrap()
|
|
234
|
+
.contains_key("recovery"),
|
|
235
|
+
"recovery=false assign must not carry recovery marker: {recovery_false_value}"
|
|
236
|
+
);
|
|
237
|
+
assert!(
|
|
238
|
+
recovery_false_value.get("acceptance_marker").is_none(),
|
|
239
|
+
"recovery=false assign must not carry acceptance marker: {recovery_false_value}"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[test]
|
|
244
|
+
fn recovery_assign_shape_has_structured_marker() {
|
|
245
|
+
let ws = seed_current_worker_state("recovery-marker");
|
|
246
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
247
|
+
&ws,
|
|
248
|
+
Some(AgentId::new("leader")),
|
|
249
|
+
Some(TeamKey::new("current")),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
let assigned = tools
|
|
253
|
+
.assign_task(
|
|
254
|
+
&json!({
|
|
255
|
+
"id": "recovery-task",
|
|
256
|
+
"assignee": "worker-1",
|
|
257
|
+
"title": "recover",
|
|
258
|
+
"recovery": true,
|
|
259
|
+
}),
|
|
260
|
+
None,
|
|
261
|
+
)
|
|
262
|
+
.expect("recovery assign ok");
|
|
263
|
+
assert_eq!(assigned.fields.get("recovery"), Some(&json!(true)));
|
|
264
|
+
assert_eq!(
|
|
265
|
+
assigned.fields.get("acceptance_marker"),
|
|
266
|
+
Some(&json!("recovery"))
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
158
270
|
#[test]
|
|
159
271
|
fn send_message_worker_recipient_surfaces_dead_coordinator_warning() {
|
|
160
272
|
let ws = unique_ws("send-worker-dead-coord");
|
|
@@ -26,6 +26,7 @@ use super::helpers::{
|
|
|
26
26
|
};
|
|
27
27
|
use super::normalize::{
|
|
28
28
|
compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
|
|
29
|
+
report_result_integrity_warnings,
|
|
29
30
|
};
|
|
30
31
|
use super::types::{
|
|
31
32
|
Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers,
|
|
@@ -116,6 +117,7 @@ impl TeamOrchestratorTools {
|
|
|
116
117
|
};
|
|
117
118
|
|
|
118
119
|
let task_value = Value::Object(task_obj.clone());
|
|
120
|
+
let recovery = task_recovery_marker(&task_value);
|
|
119
121
|
let mut state = load_runtime_state(&self.workspace).map_err(tool_runtime_error)?;
|
|
120
122
|
ensure_object(&mut state);
|
|
121
123
|
let team_key = self
|
|
@@ -139,7 +141,16 @@ impl TeamOrchestratorTools {
|
|
|
139
141
|
None,
|
|
140
142
|
None,
|
|
141
143
|
)?;
|
|
142
|
-
compact_tool_result(&out.to_value())
|
|
144
|
+
let mut ok = compact_tool_result(&out.to_value())?;
|
|
145
|
+
if recovery {
|
|
146
|
+
ok.fields
|
|
147
|
+
.insert("recovery".to_string(), serde_json::json!(true));
|
|
148
|
+
ok.fields.insert(
|
|
149
|
+
"acceptance_marker".to_string(),
|
|
150
|
+
Value::String("recovery".to_string()),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
Ok(ok)
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
/// `send_message` (`tools.py:135-183`): C14/C15/C17 scope resolution.
|
|
@@ -389,7 +400,13 @@ impl TeamOrchestratorTools {
|
|
|
389
400
|
self.note_unknown_result_status(&raw);
|
|
390
401
|
}
|
|
391
402
|
let normalized = normalize_report_envelope(&base);
|
|
392
|
-
let
|
|
403
|
+
let warnings = report_result_integrity_warnings(&base, &normalized);
|
|
404
|
+
let mut env_value = normalized_envelope_value(&normalized);
|
|
405
|
+
if !warnings.is_empty() {
|
|
406
|
+
if let Some(obj) = env_value.as_object_mut() {
|
|
407
|
+
obj.insert("warnings".to_string(), Value::Array(warnings));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
393
410
|
let owner_team = self.canonical_owner_team_key()?;
|
|
394
411
|
messaging::report_result_for_owner_team(
|
|
395
412
|
&self.workspace,
|
|
@@ -989,6 +1006,11 @@ fn assignment_message(task: &Value, explicit: Option<&str>) -> String {
|
|
|
989
1006
|
json_dumps_default(task)
|
|
990
1007
|
}
|
|
991
1008
|
|
|
1009
|
+
fn task_recovery_marker(task: &Value) -> bool {
|
|
1010
|
+
task.get("recovery").and_then(Value::as_bool) == Some(true)
|
|
1011
|
+
|| task.get("acceptance_marker").and_then(Value::as_str) == Some("recovery")
|
|
1012
|
+
}
|
|
1013
|
+
|
|
992
1014
|
fn scope_override_name(scope: Scope) -> Option<&'static str> {
|
|
993
1015
|
match scope {
|
|
994
1016
|
Scope::Team => Some("team"),
|
|
@@ -1981,12 +1981,19 @@ fn arm_turn_open(state: &mut serde_json::Value, recipient: &str, message_id: &Op
|
|
|
1981
1981
|
.and_then(|agents| agents.get_mut(recipient))
|
|
1982
1982
|
.and_then(serde_json::Value::as_object_mut)
|
|
1983
1983
|
{
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1984
|
+
// Phase-DX E2 (CR P0 red line #5): the leader→worker turn message id.
|
|
1985
|
+
// Renamed from a misleading legacy field name (which was never a task
|
|
1986
|
+
// FSM id — task FSM is A1) to `current_turn_message_id`, matching its
|
|
1987
|
+
// actual semantic. Reader compatibility lives in `coordinator/tick.rs`
|
|
1988
|
+
// (whitelisted for the transition) and `mcp_server/helpers.rs`; other
|
|
1989
|
+
// messaging/lifecycle/mcp_server code MUST NOT treat this as
|
|
1990
|
+
// authoritative task state.
|
|
1991
|
+
let field = message_id
|
|
1992
|
+
.as_ref()
|
|
1993
|
+
.map_or(serde_json::Value::Null, |id| {
|
|
1987
1994
|
serde_json::Value::String(id.clone())
|
|
1988
|
-
})
|
|
1989
|
-
);
|
|
1995
|
+
});
|
|
1996
|
+
agent.insert("current_turn_message_id".to_string(), field);
|
|
1990
1997
|
}
|
|
1991
1998
|
}
|
|
1992
1999
|
|
|
@@ -541,3 +541,61 @@ fn render_fallback_pane_message(content: &str, message_id: &str, primary_error:
|
|
|
541
541
|
[team-agent-token:{message_id}]"
|
|
542
542
|
)
|
|
543
543
|
}
|
|
544
|
+
|
|
545
|
+
/// E6 (0.5.9 offline-mailbox-toname-design §6.3): enqueue a leader-bound message
|
|
546
|
+
/// row in the TARGET workspace `team.db` with status `queued_until_leader_attach`.
|
|
547
|
+
///
|
|
548
|
+
/// This is a durable "leader mailbox" write, NOT a delivery. Coordinator delivery
|
|
549
|
+
/// never claims this status (§3.3), so the row will not churn or be marked
|
|
550
|
+
/// `failed` while the leader is unattached. When the target owner runs
|
|
551
|
+
/// `attach-leader` / `claim-leader`, the existing `requeue_blocked_leader_messages`
|
|
552
|
+
/// helper flips the same row to `accepted` and the standard delivery pipeline
|
|
553
|
+
/// injects it exactly once.
|
|
554
|
+
///
|
|
555
|
+
/// Safety boundary (§5): only the `messages` table is touched. Owner identity
|
|
556
|
+
/// (`leader_receiver` / `team_owner` / `owner_epoch`) is never written by this
|
|
557
|
+
/// path, and no provider/worker process is spawned.
|
|
558
|
+
pub fn enqueue_leader_mailbox_until_attach(
|
|
559
|
+
target_workspace: &Path,
|
|
560
|
+
canonical_team_key: &str,
|
|
561
|
+
content: &str,
|
|
562
|
+
task_id: Option<&TaskId>,
|
|
563
|
+
sender: &str,
|
|
564
|
+
event_log: &EventLog,
|
|
565
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
566
|
+
let store = MessageStore::open(target_workspace)?;
|
|
567
|
+
let message_id = store.create_message(
|
|
568
|
+
task_id.map(TaskId::as_str),
|
|
569
|
+
sender,
|
|
570
|
+
"leader",
|
|
571
|
+
content,
|
|
572
|
+
None,
|
|
573
|
+
false,
|
|
574
|
+
Some(canonical_team_key),
|
|
575
|
+
)?;
|
|
576
|
+
// The default row status after `create_message` is `accepted`. Flip it to
|
|
577
|
+
// `queued_until_leader_attach` so it stays out of `claim_for_delivery`
|
|
578
|
+
// eligible set and out of `deliver_pending_messages` scan until an
|
|
579
|
+
// attach/claim explicitly requeues it (see requeue_blocked_leader_messages).
|
|
580
|
+
store.mark(&message_id, "queued_until_leader_attach", None)?;
|
|
581
|
+
event_log.write(
|
|
582
|
+
"leader_mailbox.queued_until_attach",
|
|
583
|
+
serde_json::json!({
|
|
584
|
+
"message_id": message_id,
|
|
585
|
+
"owner_team_id": canonical_team_key,
|
|
586
|
+
"sender": sender,
|
|
587
|
+
"target_workspace": target_workspace.display().to_string(),
|
|
588
|
+
"channel": "leader_mailbox",
|
|
589
|
+
}),
|
|
590
|
+
)?;
|
|
591
|
+
Ok(DeliveryOutcome {
|
|
592
|
+
ok: true,
|
|
593
|
+
status: DeliveryStatus::Queued,
|
|
594
|
+
message_status: MessageStatusShadow("queued_until_leader_attach".to_string()),
|
|
595
|
+
message_id: Some(message_id),
|
|
596
|
+
verification: None,
|
|
597
|
+
stage: None,
|
|
598
|
+
reason: None,
|
|
599
|
+
channel: Some("leader_mailbox".to_string()),
|
|
600
|
+
})
|
|
601
|
+
}
|
|
@@ -83,7 +83,8 @@ pub use delivery::{
|
|
|
83
83
|
};
|
|
84
84
|
pub use helpers::fail_leader_delivery;
|
|
85
85
|
pub use leader_receiver::{
|
|
86
|
-
deliver_to_leader_fallback_pane,
|
|
86
|
+
deliver_to_leader_fallback_pane, enqueue_leader_mailbox_until_attach,
|
|
87
|
+
mirror_peer_message_to_leader, send_to_leader_receiver,
|
|
87
88
|
send_to_leader_receiver_with_message_id,
|
|
88
89
|
};
|
|
89
90
|
pub use peers::allow_peer_talk;
|
|
@@ -263,11 +263,21 @@ fn ensure_coordinator_after_collect(
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
/// `_coordinator_should_run`(`results.py:187-188`)。
|
|
266
|
+
///
|
|
267
|
+
/// The top-level `state.leader_receiver` read below is a coordinator-scope
|
|
268
|
+
/// legacy compat bridge (pre-teams-map single-team layout) — deliberately
|
|
269
|
+
/// NOT team-scoped because this predicate answers "is a coordinator worth
|
|
270
|
+
/// starting for this workspace at all?" and does not itself route any
|
|
271
|
+
/// message. The `ALLOWED-LEGACY-READ` marker documents the exception and
|
|
272
|
+
/// keeps grep sweeps honest; delete once B1 canonical layout lands and
|
|
273
|
+
/// every state carries a `teams` map
|
|
274
|
+
/// (`.team/artifacts/next-version-staged-plan.md §5 Phase-Foundation-1`).
|
|
266
275
|
fn coordinator_should_run(state: &serde_json::Value) -> bool {
|
|
267
276
|
let has_session = state
|
|
268
277
|
.get("session_name")
|
|
269
278
|
.and_then(serde_json::Value::as_str)
|
|
270
279
|
.is_some_and(|s| !s.is_empty());
|
|
280
|
+
// ALLOWED-LEGACY-READ: pre-teams-map coordinator-scope existence probe; never used for routing.
|
|
271
281
|
has_session || leader_receiver_is_direct(state.get("leader_receiver"))
|
|
272
282
|
}
|
|
273
283
|
|
|
@@ -942,6 +952,12 @@ fn report_result_for_owner_team_inner(
|
|
|
942
952
|
);
|
|
943
953
|
}
|
|
944
954
|
out.insert("notification_event_id".to_string(), serde_json::Value::Null);
|
|
955
|
+
if let Some(warnings) = report_result_array(envelope, "warnings") {
|
|
956
|
+
out.insert(
|
|
957
|
+
"warnings".to_string(),
|
|
958
|
+
serde_json::Value::Array(warnings.clone()),
|
|
959
|
+
);
|
|
960
|
+
}
|
|
945
961
|
Ok(serde_json::Value::Object(out))
|
|
946
962
|
}
|
|
947
963
|
|
|
@@ -1036,6 +1052,9 @@ fn format_report_result_notification(
|
|
|
1036
1052
|
if let Some(tests) = format_report_result_tests(envelope) {
|
|
1037
1053
|
lines.push(tests);
|
|
1038
1054
|
}
|
|
1055
|
+
if let Some(warnings) = format_report_result_warnings(envelope) {
|
|
1056
|
+
lines.push(warnings);
|
|
1057
|
+
}
|
|
1039
1058
|
if let Some(changes) = format_report_result_changes(envelope) {
|
|
1040
1059
|
lines.push(changes);
|
|
1041
1060
|
}
|
|
@@ -1075,6 +1094,29 @@ fn format_report_result_tests(envelope: &serde_json::Value) -> Option<String> {
|
|
|
1075
1094
|
}
|
|
1076
1095
|
}
|
|
1077
1096
|
|
|
1097
|
+
fn format_report_result_warnings(envelope: &serde_json::Value) -> Option<String> {
|
|
1098
|
+
let parts = report_result_array(envelope, "warnings")?
|
|
1099
|
+
.iter()
|
|
1100
|
+
.filter_map(|warning| {
|
|
1101
|
+
let code = report_field(warning, "code")?;
|
|
1102
|
+
let field = report_field(warning, "field").unwrap_or("result");
|
|
1103
|
+
let severity = report_field(warning, "severity").unwrap_or("warning");
|
|
1104
|
+
let advisory =
|
|
1105
|
+
if warning.get("advisory").and_then(serde_json::Value::as_bool) == Some(true) {
|
|
1106
|
+
" advisory"
|
|
1107
|
+
} else {
|
|
1108
|
+
""
|
|
1109
|
+
};
|
|
1110
|
+
Some(format!("{severity}{advisory} {field}: {code}"))
|
|
1111
|
+
})
|
|
1112
|
+
.collect::<Vec<_>>();
|
|
1113
|
+
if parts.is_empty() {
|
|
1114
|
+
None
|
|
1115
|
+
} else {
|
|
1116
|
+
Some(format!("Verification warnings: {}", parts.join(", ")))
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1078
1120
|
fn report_result_array<'a>(
|
|
1079
1121
|
envelope: &'a serde_json::Value,
|
|
1080
1122
|
key: &str,
|
|
@@ -1203,6 +1245,14 @@ mod tests {
|
|
|
1203
1245
|
"tests": [
|
|
1204
1246
|
{"command": "cargo test", "status": "passed"}
|
|
1205
1247
|
],
|
|
1248
|
+
"warnings": [
|
|
1249
|
+
{
|
|
1250
|
+
"code": "result_success_without_executed_tests",
|
|
1251
|
+
"field": "tests",
|
|
1252
|
+
"severity": "warning",
|
|
1253
|
+
"advisory": true
|
|
1254
|
+
}
|
|
1255
|
+
],
|
|
1206
1256
|
"risks": [
|
|
1207
1257
|
{"severity": "low", "description": "none known"}
|
|
1208
1258
|
],
|
|
@@ -1217,6 +1267,9 @@ mod tests {
|
|
|
1217
1267
|
format_report_result_notification("res_1", "task-1", "worker", "success", &envelope);
|
|
1218
1268
|
assert!(notification.contains("Task task-1 reported success from worker: done"));
|
|
1219
1269
|
assert!(notification.contains("Tests: cargo test=passed"));
|
|
1270
|
+
assert!(notification.contains(
|
|
1271
|
+
"Verification warnings: warning advisory tests: result_success_without_executed_tests"
|
|
1272
|
+
));
|
|
1220
1273
|
assert!(notification.contains("Changes: modified src/a.rs: patched delivery"));
|
|
1221
1274
|
assert!(notification.contains("Risks: low: none known"));
|
|
1222
1275
|
assert!(notification.contains("Artifacts: .team/artifacts/evidence.md: evidence"));
|
|
@@ -1581,10 +1581,15 @@ fn deliver_pending_submit_unverified_is_not_saved_by_readback() {
|
|
|
1581
1581
|
Some(&serde_json::json!(message_id)),
|
|
1582
1582
|
"current-turn fact is armed immediately after physical inject succeeds"
|
|
1583
1583
|
);
|
|
1584
|
+
// Phase-DX E2 rename: the leader→worker turn message id lives under
|
|
1585
|
+
// `current_turn_message_id` (the previous name misleadingly implied a task
|
|
1586
|
+
// FSM id — that is A1 territory). Same value written by
|
|
1587
|
+
// `delivery::arm_turn_open`; the invariant of arming-without-delivered is
|
|
1588
|
+
// unchanged.
|
|
1584
1589
|
assert_eq!(
|
|
1585
|
-
updated.pointer("/agents/w1/
|
|
1590
|
+
updated.pointer("/agents/w1/current_turn_message_id"),
|
|
1586
1591
|
Some(&serde_json::json!(message_id)),
|
|
1587
|
-
"agent
|
|
1592
|
+
"agent current-turn message id is armed without marking the message delivered"
|
|
1588
1593
|
);
|
|
1589
1594
|
}
|
|
1590
1595
|
|
|
@@ -459,15 +459,25 @@ pub(crate) fn requeue_blocked_leader_messages(
|
|
|
459
459
|
owner_team_id: &TeamKey,
|
|
460
460
|
claimed_pane_id: &PaneId,
|
|
461
461
|
) -> Result<usize, MessagingError> {
|
|
462
|
+
// E6 (0.5.9 offline-mailbox §6.5): also requeue rows that a third-party
|
|
463
|
+
// sender left in `queued_until_leader_attach` via the leader mailbox.
|
|
464
|
+
// Same idempotent requeue funnel (row/message_id/leader_notification_log
|
|
465
|
+
// PK are all preserved) — after attach/claim the coordinator's normal
|
|
466
|
+
// `deliver_pending_messages` picks them up as `accepted` and injects
|
|
467
|
+
// exactly once. status `queued_until_leader_attach` is deliberately NOT
|
|
468
|
+
// in the `claim_for_delivery` eligible set (see message_store.rs) so it
|
|
469
|
+
// could not have churned while the leader was unattached.
|
|
462
470
|
let requeued = conn.execute(
|
|
463
471
|
"update messages
|
|
464
472
|
set status = 'accepted',
|
|
465
473
|
error = null,
|
|
466
474
|
updated_at = ?2
|
|
467
475
|
where recipient = 'leader'
|
|
468
|
-
and
|
|
469
|
-
and
|
|
470
|
-
|
|
476
|
+
and owner_team_id = ?1
|
|
477
|
+
and (
|
|
478
|
+
(status = 'failed' and error = 'leader_not_attached')
|
|
479
|
+
or status = 'queued_until_leader_attach'
|
|
480
|
+
)",
|
|
471
481
|
params![owner_team_id.as_str(), chrono::Utc::now().to_rfc3339()],
|
|
472
482
|
)?;
|
|
473
483
|
if requeued > 0 {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.9",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.9",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.9"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|