@team-agent/installer 0.5.5 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/mcp_server/helpers.rs +144 -34
- package/crates/team-agent/src/mcp_server/mod.rs +3 -3
- package/crates/team-agent/src/mcp_server/tests/tools.rs +128 -0
- package/crates/team-agent/src/mcp_server/tools.rs +6 -7
- package/crates/team-agent/src/messaging/delivery.rs +46 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +11 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
use std::io::Write as _;
|
|
4
4
|
use std::path::Path;
|
|
5
5
|
|
|
6
|
+
use rusqlite::{params, OptionalExtension};
|
|
6
7
|
use serde::Serialize;
|
|
7
8
|
use serde_json::Value;
|
|
8
9
|
|
|
@@ -274,12 +275,7 @@ fn find_latest_nonterminal_task_for(tasks: &[Value], agent_id: &str) -> Option<S
|
|
|
274
275
|
None
|
|
275
276
|
}
|
|
276
277
|
|
|
277
|
-
|
|
278
|
-
/// is empty/null AND for which no result row exists yet. Used by
|
|
279
|
-
/// `report_result` as a message-scoped fallback before defaulting to
|
|
280
|
-
/// `"manual"`, so `collect` can correlate via the existing message-scope path
|
|
281
|
-
/// (`messaging::results::is_message_scoped_result`).
|
|
282
|
-
pub(crate) fn latest_uncorrelated_delivered_message_for(
|
|
278
|
+
pub(crate) fn latest_reportable_message_for(
|
|
283
279
|
workspace: &Path,
|
|
284
280
|
agent_id: &str,
|
|
285
281
|
owner_team_id: Option<&str>,
|
|
@@ -287,33 +283,147 @@ pub(crate) fn latest_uncorrelated_delivered_message_for(
|
|
|
287
283
|
use crate::db::message_store::MessageStore;
|
|
288
284
|
let store = MessageStore::open(workspace).ok()?;
|
|
289
285
|
let conn = crate::db::schema::open_db(store.db_path()).ok()?;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
let mut stmt = conn.prepare(sql).ok()?;
|
|
310
|
-
let id: Option<String> = match owner_team_id {
|
|
311
|
-
Some(team) => stmt
|
|
312
|
-
.query_row(rusqlite::params![agent_id, team], |row| row.get::<_, String>(0))
|
|
313
|
-
.ok(),
|
|
314
|
-
None => stmt
|
|
315
|
-
.query_row(rusqlite::params![agent_id], |row| row.get::<_, String>(0))
|
|
316
|
-
.ok(),
|
|
286
|
+
current_turn_message_for(workspace, &conn, agent_id, owner_team_id)
|
|
287
|
+
.or_else(|| latest_reportable_message_from_db(&conn, agent_id, owner_team_id))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn current_turn_message_for(
|
|
291
|
+
workspace: &Path,
|
|
292
|
+
conn: &rusqlite::Connection,
|
|
293
|
+
agent_id: &str,
|
|
294
|
+
owner_team_id: Option<&str>,
|
|
295
|
+
) -> Option<String> {
|
|
296
|
+
let state = report_scope_state(workspace, owner_team_id)?;
|
|
297
|
+
current_turn_id_from_state(&state, agent_id)
|
|
298
|
+
.filter(|message_id| message_is_reportable(conn, message_id, agent_id, owner_team_id))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fn report_scope_state(workspace: &Path, owner_team_id: Option<&str>) -> Option<Value> {
|
|
302
|
+
let state = load_runtime_state(workspace).ok()?;
|
|
303
|
+
let Some(team) = owner_team_id.filter(|team| !team.is_empty()) else {
|
|
304
|
+
return Some(state);
|
|
317
305
|
};
|
|
318
|
-
|
|
306
|
+
let canonical = crate::state::projection::resolve_owner_team_id(&state, team)
|
|
307
|
+
.canonical_key()
|
|
308
|
+
.map(str::to_string)
|
|
309
|
+
.unwrap_or_else(|| team.to_string());
|
|
310
|
+
if let Some(projected) = state
|
|
311
|
+
.get("teams")
|
|
312
|
+
.and_then(Value::as_object)
|
|
313
|
+
.and_then(|teams| teams.get(&canonical))
|
|
314
|
+
.cloned()
|
|
315
|
+
{
|
|
316
|
+
Some(projected)
|
|
317
|
+
} else {
|
|
318
|
+
Some(state)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fn current_turn_id_from_state(state: &Value, agent_id: &str) -> Option<String> {
|
|
323
|
+
if let Some(turn) = state.get("coordinator").and_then(|v| v.get("turn_open")) {
|
|
324
|
+
let armed = turn.get("armed").and_then(Value::as_bool).unwrap_or(false);
|
|
325
|
+
let node_matches = turn
|
|
326
|
+
.get("node_id")
|
|
327
|
+
.and_then(Value::as_str)
|
|
328
|
+
.is_some_and(|node| node == agent_id);
|
|
329
|
+
if armed && node_matches {
|
|
330
|
+
if let Some(turn_id) = turn
|
|
331
|
+
.get("turn_id")
|
|
332
|
+
.and_then(Value::as_str)
|
|
333
|
+
.and_then(non_empty_string)
|
|
334
|
+
{
|
|
335
|
+
return Some(turn_id.to_string());
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
state
|
|
340
|
+
.get("agents")
|
|
341
|
+
.and_then(|agents| agents.get(agent_id))
|
|
342
|
+
.and_then(|agent| agent.get("current_task_id"))
|
|
343
|
+
.and_then(Value::as_str)
|
|
344
|
+
.and_then(non_empty_string)
|
|
345
|
+
.map(ToString::to_string)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn message_is_reportable(
|
|
349
|
+
conn: &rusqlite::Connection,
|
|
350
|
+
message_id: &str,
|
|
351
|
+
agent_id: &str,
|
|
352
|
+
owner_team_id: Option<&str>,
|
|
353
|
+
) -> bool {
|
|
354
|
+
conn.query_row(
|
|
355
|
+
"select 1 from messages m
|
|
356
|
+
where m.message_id = ?1
|
|
357
|
+
and m.recipient = ?2
|
|
358
|
+
and (?3 is null or m.owner_team_id = ?3)
|
|
359
|
+
and (m.task_id is null or m.task_id = '')
|
|
360
|
+
and m.status in ('delivered', 'target_resolved', 'submitted', 'injected', 'visible')
|
|
361
|
+
and (m.status = 'delivered' or m.error is null)
|
|
362
|
+
and m.created_at >= coalesce((
|
|
363
|
+
select max(r.created_at) from results r
|
|
364
|
+
where r.agent_id = m.recipient
|
|
365
|
+
and (?3 is null or r.owner_team_id = m.owner_team_id)
|
|
366
|
+
), '0000')
|
|
367
|
+
and not exists (
|
|
368
|
+
select 1 from results r
|
|
369
|
+
where r.task_id = m.message_id and r.agent_id = m.recipient
|
|
370
|
+
)
|
|
371
|
+
limit 1",
|
|
372
|
+
params![message_id, agent_id, owner_team_id],
|
|
373
|
+
|_| Ok(()),
|
|
374
|
+
)
|
|
375
|
+
.optional()
|
|
376
|
+
.ok()
|
|
377
|
+
.flatten()
|
|
378
|
+
.is_some()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
fn latest_reportable_message_from_db(
|
|
382
|
+
conn: &rusqlite::Connection,
|
|
383
|
+
agent_id: &str,
|
|
384
|
+
owner_team_id: Option<&str>,
|
|
385
|
+
) -> Option<String> {
|
|
386
|
+
let row = conn
|
|
387
|
+
.query_row(
|
|
388
|
+
"select m.message_id, m.status, m.error from messages m
|
|
389
|
+
where m.recipient = ?1
|
|
390
|
+
and (?2 is null or m.owner_team_id = ?2)
|
|
391
|
+
and (m.task_id is null or m.task_id = '')
|
|
392
|
+
and m.created_at >= coalesce((
|
|
393
|
+
select max(r.created_at) from results r
|
|
394
|
+
where r.agent_id = m.recipient
|
|
395
|
+
and (?2 is null or r.owner_team_id = m.owner_team_id)
|
|
396
|
+
), '0000')
|
|
397
|
+
and not exists (
|
|
398
|
+
select 1 from results r
|
|
399
|
+
where r.task_id = m.message_id and r.agent_id = m.recipient
|
|
400
|
+
)
|
|
401
|
+
order by m.created_at desc,
|
|
402
|
+
case when m.status = 'delivered' then 0 else 1 end
|
|
403
|
+
limit 1",
|
|
404
|
+
params![agent_id, owner_team_id],
|
|
405
|
+
|row| {
|
|
406
|
+
Ok((
|
|
407
|
+
row.get::<_, String>(0)?,
|
|
408
|
+
row.get::<_, String>(1)?,
|
|
409
|
+
row.get::<_, Option<String>>(2)?,
|
|
410
|
+
))
|
|
411
|
+
},
|
|
412
|
+
)
|
|
413
|
+
.optional()
|
|
414
|
+
.ok()
|
|
415
|
+
.flatten()?;
|
|
416
|
+
let (message_id, status, error) = row;
|
|
417
|
+
if reportable_message_status(&status, error.as_deref()) {
|
|
418
|
+
Some(message_id)
|
|
419
|
+
} else {
|
|
420
|
+
None
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
fn reportable_message_status(status: &str, error: Option<&str>) -> bool {
|
|
425
|
+
matches!(
|
|
426
|
+
status,
|
|
427
|
+
"delivered" | "target_resolved" | "submitted" | "injected" | "visible"
|
|
428
|
+
) && (status == "delivered" || error.is_none())
|
|
319
429
|
}
|
|
@@ -99,9 +99,9 @@ pub use wire::*;
|
|
|
99
99
|
// pub(crate) 子项 (normalize 的 list helpers、wire 的 dispatch_tool 等) 经此再导出,
|
|
100
100
|
// 使 `#[cfg(test)] mod tests` 的 `use super::*` 与跨子模块引用解析不变。
|
|
101
101
|
pub(crate) use helpers::{
|
|
102
|
-
delivery_outcome_value, ensure_object, enum_value, insert_array,
|
|
103
|
-
non_empty_string, normalize_token, normalized_envelope_value,
|
|
104
|
-
text_of_value, tool_error_reason_wire, tool_runtime_error,
|
|
102
|
+
delivery_outcome_value, ensure_object, enum_value, insert_array, latest_reportable_message_for,
|
|
103
|
+
latest_task_for_assignee, non_empty_string, normalize_token, normalized_envelope_value,
|
|
104
|
+
object_fields, text_field, text_of_value, tool_error_reason_wire, tool_runtime_error,
|
|
105
105
|
};
|
|
106
106
|
pub(crate) use normalize::{
|
|
107
107
|
normalize_artifacts, normalize_changes, normalize_next_actions, normalize_risks, normalize_tests,
|
|
@@ -61,6 +61,134 @@
|
|
|
61
61
|
assert_eq!(env.task_id, TaskId::new("manual"));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
fn seed_report_message(
|
|
65
|
+
ws: &std::path::Path,
|
|
66
|
+
message_id: &str,
|
|
67
|
+
owner_team_id: &str,
|
|
68
|
+
status: &str,
|
|
69
|
+
created_at: &str,
|
|
70
|
+
) {
|
|
71
|
+
let store = MessageStore::open(ws).unwrap();
|
|
72
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
73
|
+
conn.execute(
|
|
74
|
+
"insert into messages(
|
|
75
|
+
message_id, owner_team_id, task_id, sender, recipient, reply_to, requires_ack,
|
|
76
|
+
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
77
|
+
acknowledged_at, error, delivery_attempts
|
|
78
|
+
) values (?1, ?2, null, 'leader', 'probe-worker', null, 0,
|
|
79
|
+
?3, 'task', '[]', ?4, ?4, case when ?3 = 'delivered' then ?4 else null end,
|
|
80
|
+
null, null, 0)",
|
|
81
|
+
rusqlite::params![message_id, owner_team_id, status, created_at],
|
|
82
|
+
)
|
|
83
|
+
.unwrap();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn seed_report_result(
|
|
87
|
+
ws: &std::path::Path,
|
|
88
|
+
result_id: &str,
|
|
89
|
+
owner_team_id: &str,
|
|
90
|
+
task_id: &str,
|
|
91
|
+
created_at: &str,
|
|
92
|
+
) {
|
|
93
|
+
let store = MessageStore::open(ws).unwrap();
|
|
94
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
95
|
+
let envelope = json!({
|
|
96
|
+
"schema_version": "result_envelope_v1",
|
|
97
|
+
"result_id": result_id,
|
|
98
|
+
"task_id": task_id,
|
|
99
|
+
"agent_id": "probe-worker",
|
|
100
|
+
"status": "success",
|
|
101
|
+
"summary": "seed",
|
|
102
|
+
"changes": [], "tests": [], "risks": [], "artifacts": [], "next_actions": []
|
|
103
|
+
});
|
|
104
|
+
conn.execute(
|
|
105
|
+
"insert into results(
|
|
106
|
+
result_id, owner_team_id, task_id, agent_id, envelope, status, created_at
|
|
107
|
+
) values (?1, ?2, ?3, 'probe-worker', ?4, 'success', ?5)",
|
|
108
|
+
rusqlite::params![result_id, owner_team_id, task_id, envelope.to_string(), created_at],
|
|
109
|
+
)
|
|
110
|
+
.unwrap();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn report_result_prefers_current_inflight_message_over_old_delivered_fallback() {
|
|
115
|
+
let ws = unique_ws("report-current-message");
|
|
116
|
+
seed_report_message(
|
|
117
|
+
&ws,
|
|
118
|
+
"msg_old",
|
|
119
|
+
"gate055",
|
|
120
|
+
"delivered",
|
|
121
|
+
"2026-07-06T13:24:25.000000+00:00",
|
|
122
|
+
);
|
|
123
|
+
seed_report_result(
|
|
124
|
+
&ws,
|
|
125
|
+
"res_seed",
|
|
126
|
+
"gate055",
|
|
127
|
+
"task_initial",
|
|
128
|
+
"2026-07-06T13:25:00.000000+00:00",
|
|
129
|
+
);
|
|
130
|
+
seed_report_message(
|
|
131
|
+
&ws,
|
|
132
|
+
"msg_new",
|
|
133
|
+
"gate055",
|
|
134
|
+
"target_resolved",
|
|
135
|
+
"2026-07-06T13:27:35.000000+00:00",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
139
|
+
&ws,
|
|
140
|
+
Some(AgentId::new("probe-worker")),
|
|
141
|
+
Some(TeamKey::new("gate055")),
|
|
142
|
+
);
|
|
143
|
+
let ok = tools.report_result(
|
|
144
|
+
None, Some("S3_RESTART_TOKEN"), ResultStatus::Success,
|
|
145
|
+
None, None, None, None, None,
|
|
146
|
+
None, None,
|
|
147
|
+
).expect("report ok");
|
|
148
|
+
let v = serde_json::to_value(&ok).unwrap();
|
|
149
|
+
assert_eq!(
|
|
150
|
+
v.get("task_id"),
|
|
151
|
+
Some(&json!("msg_new")),
|
|
152
|
+
"current in-flight message must beat stale delivered fallback"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn report_result_does_not_backfill_old_delivered_message_after_latest_result() {
|
|
158
|
+
let ws = unique_ws("report-no-old-backfill");
|
|
159
|
+
seed_report_message(
|
|
160
|
+
&ws,
|
|
161
|
+
"msg_old",
|
|
162
|
+
"gate055",
|
|
163
|
+
"delivered",
|
|
164
|
+
"2026-07-06T13:24:25.000000+00:00",
|
|
165
|
+
);
|
|
166
|
+
seed_report_result(
|
|
167
|
+
&ws,
|
|
168
|
+
"res_latest",
|
|
169
|
+
"gate055",
|
|
170
|
+
"task_initial",
|
|
171
|
+
"2026-07-06T13:25:00.000000+00:00",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
175
|
+
&ws,
|
|
176
|
+
Some(AgentId::new("probe-worker")),
|
|
177
|
+
Some(TeamKey::new("gate055")),
|
|
178
|
+
);
|
|
179
|
+
let ok = tools.report_result(
|
|
180
|
+
None, Some("manual follow-up"), ResultStatus::Success,
|
|
181
|
+
None, None, None, None, None,
|
|
182
|
+
None, None,
|
|
183
|
+
).expect("report ok");
|
|
184
|
+
let v = serde_json::to_value(&ok).unwrap();
|
|
185
|
+
assert_eq!(
|
|
186
|
+
v.get("task_id"),
|
|
187
|
+
Some(&json!("manual")),
|
|
188
|
+
"old delivered messages older than latest result must not be reused"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
64
192
|
// ════════════════════════════════════════════════════════════════════════
|
|
65
193
|
// CONTROL-PLANE: request_human creates a requires_ack leader message → needs_human
|
|
66
194
|
// (tools.py:342-346). sender = explicit > env > "unknown" (never leader).
|
|
@@ -21,9 +21,8 @@ use crate::messaging::{self, MessageTarget, SendOptions};
|
|
|
21
21
|
|
|
22
22
|
use super::helpers::{
|
|
23
23
|
delivery_outcome_value, ensure_object, enum_value, insert_array, is_worker_recipient,
|
|
24
|
-
json_dumps_default, latest_task_for_assignee,
|
|
25
|
-
|
|
26
|
-
tool_runtime_error,
|
|
24
|
+
json_dumps_default, latest_reportable_message_for, latest_task_for_assignee, non_empty_string,
|
|
25
|
+
normalized_envelope_value, object_fields, requires_ack_for_target, tool_runtime_error,
|
|
27
26
|
};
|
|
28
27
|
use super::normalize::{
|
|
29
28
|
compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
|
|
@@ -326,9 +325,9 @@ impl TeamOrchestratorTools {
|
|
|
326
325
|
// 1. explicit arg
|
|
327
326
|
// 2. latest nonterminal task assigned to this agent in
|
|
328
327
|
// teams.<owner>.tasks (scoped) → top-level tasks
|
|
329
|
-
// 3.
|
|
330
|
-
// task id and no result yet (message-scope
|
|
331
|
-
// collect path: is_message_scoped_result)
|
|
328
|
+
// 3. current/in-flight reportable direct message to this agent
|
|
329
|
+
// with no task id and no result yet (message-scope
|
|
330
|
+
// correlation — collect path: is_message_scoped_result)
|
|
332
331
|
// 4. "manual" — truly uncorrelated; collect still rejects
|
|
333
332
|
let owner_team_id_str = self.owner_team_id.as_ref().map(|t| t.as_str().to_string());
|
|
334
333
|
let resolved = task_id
|
|
@@ -344,7 +343,7 @@ impl TeamOrchestratorTools {
|
|
|
344
343
|
})
|
|
345
344
|
.or_else(|| {
|
|
346
345
|
self.agent_id.as_ref().and_then(|agent| {
|
|
347
|
-
|
|
346
|
+
latest_reportable_message_for(
|
|
348
347
|
&self.workspace,
|
|
349
348
|
agent.as_str(),
|
|
350
349
|
owner_team_id_str.as_deref(),
|
|
@@ -460,6 +460,14 @@ pub fn deliver_pending_message(
|
|
|
460
460
|
});
|
|
461
461
|
}
|
|
462
462
|
};
|
|
463
|
+
record_current_turn_after_inject_if_leader_to_worker_scoped(
|
|
464
|
+
workspace,
|
|
465
|
+
&message.sender,
|
|
466
|
+
&message.recipient,
|
|
467
|
+
message_id,
|
|
468
|
+
event_log,
|
|
469
|
+
canonical_owner_team_id.as_deref(),
|
|
470
|
+
)?;
|
|
463
471
|
let submit_verified = inject_submit_verified(&inject_report);
|
|
464
472
|
let readback_verified = pane_readback_verified(&inject_report);
|
|
465
473
|
// Leader pane: inject success is delivery proof. Worker pane: post-submit
|
|
@@ -1892,7 +1900,7 @@ pub fn execute_trust_retry(
|
|
|
1892
1900
|
}
|
|
1893
1901
|
|
|
1894
1902
|
/// `_record_turn_open_if_leader_to_worker` (`delivery.py:430`):**take-over arm 来自真实投递**
|
|
1895
|
-
/// (card §121) —— 仅 leader→worker
|
|
1903
|
+
/// (card §121) —— 仅 leader→worker 注入/投递成功后才 arm,绝不凭空 arm。
|
|
1896
1904
|
pub fn record_turn_open_if_leader_to_worker(
|
|
1897
1905
|
workspace: &Path,
|
|
1898
1906
|
state: &serde_json::Value,
|
|
@@ -1907,6 +1915,30 @@ pub fn record_turn_open_if_leader_to_worker(
|
|
|
1907
1915
|
)
|
|
1908
1916
|
}
|
|
1909
1917
|
|
|
1918
|
+
fn record_current_turn_after_inject_if_leader_to_worker_scoped(
|
|
1919
|
+
workspace: &Path,
|
|
1920
|
+
sender: &str,
|
|
1921
|
+
recipient: &str,
|
|
1922
|
+
message_id: &str,
|
|
1923
|
+
event_log: &EventLog,
|
|
1924
|
+
owner_team_id: Option<&str>,
|
|
1925
|
+
) -> Result<(), MessagingError> {
|
|
1926
|
+
if !matches!(sender, "leader" | "Leader") || recipient == "leader" {
|
|
1927
|
+
return Ok(());
|
|
1928
|
+
}
|
|
1929
|
+
let message_id = Some(message_id.to_string());
|
|
1930
|
+
let mut state = scoped_state_for_write(workspace, owner_team_id)?;
|
|
1931
|
+
arm_turn_open(&mut state, recipient, &message_id);
|
|
1932
|
+
save_scoped_state_reapplying_after_conflict(workspace, &state, owner_team_id, |latest| {
|
|
1933
|
+
arm_turn_open(latest, recipient, &message_id);
|
|
1934
|
+
})?;
|
|
1935
|
+
event_log.write(
|
|
1936
|
+
"turn_open.armed_after_inject",
|
|
1937
|
+
serde_json::json!({"agent_id": recipient, "message_id": message_id}),
|
|
1938
|
+
)?;
|
|
1939
|
+
Ok(())
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1910
1942
|
fn record_turn_open_if_leader_to_worker_scoped(
|
|
1911
1943
|
workspace: &Path,
|
|
1912
1944
|
sender: &str,
|
|
@@ -1943,6 +1975,19 @@ fn arm_turn_open(state: &mut serde_json::Value, recipient: &str, message_id: &Op
|
|
|
1943
1975
|
serde_json::json!({"armed": true, "node_id": recipient, "turn_id": message_id}),
|
|
1944
1976
|
);
|
|
1945
1977
|
}
|
|
1978
|
+
if let Some(agent) = root
|
|
1979
|
+
.get_mut("agents")
|
|
1980
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
1981
|
+
.and_then(|agents| agents.get_mut(recipient))
|
|
1982
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
1983
|
+
{
|
|
1984
|
+
agent.insert(
|
|
1985
|
+
"current_task_id".to_string(),
|
|
1986
|
+
message_id.as_ref().map_or(serde_json::Value::Null, |id| {
|
|
1987
|
+
serde_json::Value::String(id.clone())
|
|
1988
|
+
}),
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1946
1991
|
}
|
|
1947
1992
|
|
|
1948
1993
|
/// `_stamp_first_send_at_if_leader_to_worker` (`delivery.py:380`):首次 leader→worker 投递戳
|
|
@@ -1575,6 +1575,17 @@ fn deliver_pending_submit_unverified_is_not_saved_by_readback() {
|
|
|
1575
1575
|
"0.3.30: positive readback alone must not mark delivered when submit is unverified"
|
|
1576
1576
|
);
|
|
1577
1577
|
assert_ne!(out.message_status.0, "delivered");
|
|
1578
|
+
let updated = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1579
|
+
assert_eq!(
|
|
1580
|
+
updated.pointer("/coordinator/turn_open/turn_id"),
|
|
1581
|
+
Some(&serde_json::json!(message_id)),
|
|
1582
|
+
"current-turn fact is armed immediately after physical inject succeeds"
|
|
1583
|
+
);
|
|
1584
|
+
assert_eq!(
|
|
1585
|
+
updated.pointer("/agents/w1/current_task_id"),
|
|
1586
|
+
Some(&serde_json::json!(message_id)),
|
|
1587
|
+
"agent current_task_id is armed without marking the message delivered"
|
|
1588
|
+
);
|
|
1578
1589
|
}
|
|
1579
1590
|
|
|
1580
1591
|
#[test]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
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.6",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.6",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.6"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|