@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 CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.5"
578
+ version = "0.5.6"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.5"
12
+ version = "0.5.6"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -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
- /// Find the most recent delivered direct message to `agent_id` whose `task_id`
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
- let sql = match owner_team_id {
291
- Some(_) => "select m.message_id from messages m \
292
- where m.recipient = ?1 and m.status = 'delivered' \
293
- and (m.task_id is null or m.task_id = '') \
294
- and m.owner_team_id = ?2 \
295
- and not exists ( \
296
- select 1 from results r \
297
- where r.task_id = m.message_id and r.agent_id = m.recipient \
298
- ) \
299
- order by m.created_at desc limit 1",
300
- None => "select m.message_id from messages m \
301
- where m.recipient = ?1 and m.status = 'delivered' \
302
- and (m.task_id is null or m.task_id = '') \
303
- and not exists ( \
304
- select 1 from results r \
305
- where r.task_id = m.message_id and r.agent_id = m.recipient \
306
- ) \
307
- order by m.created_at desc limit 1",
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
- id
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, latest_task_for_assignee,
103
- non_empty_string, normalize_token, normalized_envelope_value, object_fields, text_field,
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, latest_uncorrelated_delivered_message_for,
25
- non_empty_string, normalized_envelope_value, object_fields, requires_ack_for_target,
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. latest delivered direct message to this agent with no
330
- // task id and no result yet (message-scope correlation —
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
- latest_uncorrelated_delivered_message_for(
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 注入**成功后**才调 `record_turn_open_after_delivery`,绝不凭空 arm。
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.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.5",
24
- "@team-agent/cli-darwin-x64": "0.5.5",
25
- "@team-agent/cli-linux-x64": "0.5.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",