@team-agent/installer 0.5.53 → 0.5.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +94 -3
  4. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  5. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  6. package/crates/team-agent/src/cli/send.rs +1 -0
  7. package/crates/team-agent/src/cli/spec.rs +1 -1
  8. package/crates/team-agent/src/cli/tests/lane_c.rs +3 -3
  9. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  10. package/crates/team-agent/src/cli/tests/named_address.rs +1 -0
  11. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  12. package/crates/team-agent/src/cli/types.rs +1 -0
  13. package/crates/team-agent/src/coordinator/tests/basics.rs +4 -4
  14. package/crates/team-agent/src/db/message_store.rs +31 -2
  15. package/crates/team-agent/src/db/migration.rs +7 -6
  16. package/crates/team-agent/src/db/schema.rs +18 -5
  17. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
  18. package/crates/team-agent/src/mcp_server/normalize.rs +8 -0
  19. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  20. package/crates/team-agent/src/mcp_server/tools.rs +71 -0
  21. package/crates/team-agent/src/mcp_server/types.rs +4 -0
  22. package/crates/team-agent/src/mcp_server/wire.rs +42 -4
  23. package/crates/team-agent/src/messaging/delivery.rs +2 -0
  24. package/crates/team-agent/src/messaging/helpers.rs +1 -0
  25. package/crates/team-agent/src/messaging/leader_receiver.rs +32 -0
  26. package/crates/team-agent/src/messaging/mod.rs +2 -1
  27. package/crates/team-agent/src/messaging/persist.rs +68 -2
  28. package/crates/team-agent/src/messaging/presentation.rs +307 -0
  29. package/crates/team-agent/src/messaging/results.rs +130 -3
  30. package/crates/team-agent/src/messaging/selftest.rs +1 -0
  31. package/crates/team-agent/src/messaging/send.rs +54 -2
  32. package/crates/team-agent/src/messaging/tests/runtime.rs +117 -1
  33. package/crates/team-agent/src/messaging/types.rs +2 -0
  34. package/crates/team-agent/src/messaging/watchers.rs +13 -7
  35. package/crates/team-agent/src/provider/session/capture.rs +74 -0
  36. package/crates/team-agent/src/provider/session_scan/common/tests.rs +112 -0
  37. package/crates/team-agent/src/provider/session_scan/common.rs +14 -86
  38. package/package.json +4 -4
  39. package/schemas/result-envelope.schema.json +10 -0
  40. package/skills/team-agent/SKILL.md +3 -0
@@ -446,7 +446,10 @@ fn message_not_silently_stuck_accepted_when_coordinator_dead() {
446
446
  assert!(out.ok, "durable persistence is the send success boundary");
447
447
  assert_eq!(out.status, DeliveryStatus::Blocked);
448
448
  assert_eq!(out.message_status.0, "queued_coordinator_unavailable");
449
- assert!(out.message_id.as_deref().is_some_and(|id| id.starts_with("msg_")));
449
+ assert!(out
450
+ .message_id
451
+ .as_deref()
452
+ .is_some_and(|id| id.starts_with("msg_")));
450
453
  assert_eq!(out.reason, Some(DeliveryRefusal::CoordinatorUnavailable));
451
454
  assert!(
452
455
  out.verification
@@ -2290,6 +2293,53 @@ fn u1_multi_team_send_does_not_backfill_top_level_leader_binding() {
2290
2293
  assert_eq!(owner_team_id.as_deref(), Some("team-b"));
2291
2294
  }
2292
2295
 
2296
+ #[test]
2297
+ fn casefile_leader_send_is_durable_without_entering_leader_funnel() {
2298
+ let ws = tmp_ws("casefile-send");
2299
+ crate::state::persist::save_runtime_state(
2300
+ &ws,
2301
+ &serde_json::json!({
2302
+ "session_name": "team-a",
2303
+ "active_team_key": "team-a",
2304
+ "agents": {}
2305
+ }),
2306
+ )
2307
+ .unwrap();
2308
+ let opts = SendOptions {
2309
+ team: Some(TeamKey::new("team-a")),
2310
+ requires_ack: false,
2311
+ presentation: crate::messaging::presentation::PresentationRequest {
2312
+ sink: crate::messaging::presentation::PresentationSink::Casefile,
2313
+ class: crate::messaging::presentation::PresentationClass::Progress,
2314
+ case_id: Some("case-1".to_string()),
2315
+ },
2316
+ ..SendOptions::default()
2317
+ };
2318
+ let out = send_message(
2319
+ &ws,
2320
+ &MessageTarget::Single("leader".to_string()),
2321
+ "internal progress",
2322
+ &opts,
2323
+ )
2324
+ .unwrap();
2325
+ assert_eq!(out.status, DeliveryStatus::StoredOnly);
2326
+ assert_eq!(out.message_status.0, "stored_only");
2327
+ let store = store_for(&ws);
2328
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
2329
+ let (status, presentation): (String, String) = conn
2330
+ .query_row(
2331
+ "select status, presentation from messages where message_id = ?1",
2332
+ [out.message_id.as_deref().unwrap()],
2333
+ |row| Ok((row.get(0)?, row.get(1)?)),
2334
+ )
2335
+ .unwrap();
2336
+ assert_eq!(status, "stored_only");
2337
+ assert_eq!(
2338
+ serde_json::from_str::<serde_json::Value>(&presentation).unwrap()["effective_sink"],
2339
+ "casefile"
2340
+ );
2341
+ }
2342
+
2293
2343
  // ════════════════════════════════════════════════════════════════════════
2294
2344
  // GROUP V — retry_result_deliveries: re-route notify_failed watchers with
2295
2345
  // dedupe_reason rebind_retry. result_delivery.py:19-35.
@@ -3158,6 +3208,72 @@ fn gate054_rebind_replay_requeues_failed_leader_message_on_attach() {
3158
3208
  );
3159
3209
  }
3160
3210
 
3211
+ #[test]
3212
+ fn claim_requeues_pending_acceptance_for_leader_only() {
3213
+ let ws = tmp_ws("claim-pending-acceptance");
3214
+ let store = store_for(&ws);
3215
+ let log = EventLog::new(&ws);
3216
+ let team_id = "mailbox-team";
3217
+ let leader_message = store
3218
+ .create_message(
3219
+ None,
3220
+ "worker",
3221
+ "leader",
3222
+ "leader canary",
3223
+ None,
3224
+ false,
3225
+ Some(team_id),
3226
+ )
3227
+ .unwrap();
3228
+ let worker_message = store
3229
+ .create_message(
3230
+ None,
3231
+ "worker",
3232
+ "worker-b",
3233
+ "worker canary",
3234
+ None,
3235
+ false,
3236
+ Some(team_id),
3237
+ )
3238
+ .unwrap();
3239
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
3240
+ for message_id in [&leader_message, &worker_message] {
3241
+ conn.execute(
3242
+ "update messages set status = 'submitted_pending_acceptance' where message_id = ?1",
3243
+ [message_id],
3244
+ )
3245
+ .unwrap();
3246
+ }
3247
+
3248
+ let team = TeamKey::new(team_id);
3249
+ let pane = PaneId::new("%leader");
3250
+ requeue_after_claim_leader(&ws, &store, &log, &team, &pane, None).unwrap();
3251
+
3252
+ let row = |message_id: &str| {
3253
+ conn.query_row(
3254
+ "select status, delivered_at, error from messages where message_id = ?1",
3255
+ [message_id],
3256
+ |row| {
3257
+ Ok((
3258
+ row.get::<_, String>(0)?,
3259
+ row.get::<_, Option<String>>(1)?,
3260
+ row.get::<_, Option<String>>(2)?,
3261
+ ))
3262
+ },
3263
+ )
3264
+ .unwrap()
3265
+ };
3266
+ assert_eq!(row(&leader_message), ("accepted".to_string(), None, None));
3267
+ assert_eq!(
3268
+ row(&worker_message),
3269
+ (
3270
+ "submitted_pending_acceptance".to_string(),
3271
+ None,
3272
+ None
3273
+ )
3274
+ );
3275
+ }
3276
+
3161
3277
  #[test]
3162
3278
  fn gate054_status_surfaces_pending_leader_notifications() {
3163
3279
  // Round-2: user-facing visibility — status must show the blocked leader
@@ -20,6 +20,8 @@ use super::helpers::MessageStatusShadow;
20
20
  #[serde(rename_all = "snake_case")]
21
21
  pub enum DeliveryStatus {
22
22
  Delivered,
23
+ /// Durable presentation obligation intentionally did not enter physical injection.
24
+ StoredOnly,
23
25
  Failed,
24
26
  /// busy → 延后不丢 (card §131:不 mark failed,留队列)。
25
27
  Queued,
@@ -195,6 +195,7 @@ fn deliver_primary_watcher(
195
195
  false,
196
196
  None,
197
197
  super::InitialDisposition::Accepted,
198
+ None,
198
199
  )?
199
200
  else {
200
201
  unreachable!("watcher notifications do not accept caller-supplied ids")
@@ -469,12 +470,13 @@ pub fn requeue_after_claim_leader(
469
470
  /// 0.5.5 gate054 round-2: attach-leader (and claim-leader) requeue for leader messages
470
471
  /// that were refused with `rebind_required` while no leader pane was attached.
471
472
  ///
472
- /// #231 C-5 semantics: same row, same message_id — flip `status` back from
473
- /// `failed`/`leader_not_attached` to `accepted` so `deliver_pending_messages`
474
- /// replays it through the SAME pipeline. The `leader_notification_log` PK is
475
- /// already there (primitive wrote it before the unbound check), so this replay
476
- /// cannot create a duplicate notification exactly-once across rebind, no new
477
- /// send/notify rows and no second replay mechanism.
473
+ /// #231 C-5 semantics: same row, same message_id — flip an eligible status back
474
+ /// to `accepted` so `deliver_pending_messages` replays it through the SAME
475
+ /// pipeline. The `leader_notification_log` PK prevents a second notification
476
+ /// row for watcher-backed messages. A `submitted_pending_acceptance` row has
477
+ /// already crossed the transport boundary, so its recovery is intentionally
478
+ /// at-least-once: the stable message id/receipt token is preserved, but a replay
479
+ /// can repeat the physical submit if a same-pane claim races the receipt window.
478
480
  pub(crate) fn requeue_blocked_leader_messages(
479
481
  conn: &rusqlite::Connection,
480
482
  event_log: &EventLog,
@@ -488,7 +490,10 @@ pub(crate) fn requeue_blocked_leader_messages(
488
490
  // `deliver_pending_messages` picks them up as `accepted` and injects
489
491
  // exactly once. status `queued_until_leader_attach` is deliberately NOT
490
492
  // in the `claim_for_delivery` eligible set (see message_store.rs) so it
491
- // could not have churned while the leader was unattached.
493
+ // could not have churned while the leader was unattached. In contrast,
494
+ // `submitted_pending_acceptance` is an explicit at-least-once recovery arm:
495
+ // claim convergence favors an eventual receipt over preserving an
496
+ // unobservable in-flight submit.
492
497
  let requeued = conn.execute(
493
498
  "update messages
494
499
  set status = 'accepted',
@@ -499,6 +504,7 @@ pub(crate) fn requeue_blocked_leader_messages(
499
504
  and (
500
505
  (status = 'failed' and error = 'leader_not_attached')
501
506
  or status = 'queued_until_leader_attach'
507
+ or status = 'submitted_pending_acceptance'
502
508
  )",
503
509
  params![owner_team_id.as_str(), chrono::Utc::now().to_rfc3339()],
504
510
  )?;
@@ -968,6 +968,17 @@ fn allocate_session_candidates(
968
968
  CandidateMatchKind::Any,
969
969
  item,
970
970
  ) {
971
+ Some(candidate)
972
+ if codex_no_expected(item)
973
+ && candidate_is_visible_to_pending_peer(
974
+ item,
975
+ &candidate,
976
+ pending,
977
+ candidates_by_agent,
978
+ ) =>
979
+ {
980
+ ambiguous.insert(item.agent_id.clone());
981
+ }
971
982
  Some(candidate) => {
972
983
  claimed.extend(captured_provider_session_keys(item, &candidate.captured));
973
984
  assignments.insert(item.agent_id.clone(), candidate);
@@ -985,6 +996,30 @@ fn allocate_session_candidates(
985
996
  (assignments, ambiguous)
986
997
  }
987
998
 
999
+ fn candidate_is_visible_to_pending_peer(
1000
+ owner: &PendingSessionCapture,
1001
+ candidate: &CapturedSessionCandidate,
1002
+ pending: &[PendingSessionCapture],
1003
+ candidates_by_agent: &BTreeMap<String, Vec<CapturedSessionCandidate>>,
1004
+ ) -> bool {
1005
+ let owner_keys = captured_provider_session_keys(owner, &candidate.captured);
1006
+ pending.iter().any(|peer| {
1007
+ peer.agent_id != owner.agent_id
1008
+ && peer.provider == owner.provider
1009
+ && peer.team_key == owner.team_key
1010
+ && candidates_by_agent
1011
+ .get(&peer.agent_id)
1012
+ .is_some_and(|candidates| {
1013
+ candidates.iter().any(|peer_candidate| {
1014
+ !owner_keys.is_disjoint(&captured_provider_session_keys(
1015
+ peer,
1016
+ &peer_candidate.captured,
1017
+ ))
1018
+ })
1019
+ })
1020
+ })
1021
+ }
1022
+
988
1023
  fn allocate_global_one_to_one(
989
1024
  pending: &[PendingSessionCapture],
990
1025
  candidates_by_agent: &BTreeMap<String, Vec<CapturedSessionCandidate>>,
@@ -1702,6 +1737,45 @@ mod u1_tests {
1702
1737
  );
1703
1738
  }
1704
1739
 
1740
+ #[test]
1741
+ fn codex_shared_single_candidate_without_identity_is_deferred() {
1742
+ let mut state = serde_json::json!({
1743
+ "agents": {
1744
+ "parent-peer": {
1745
+ "provider": "codex",
1746
+ "status": "running",
1747
+ "spawn_cwd": "/tmp/u1-cwd"
1748
+ },
1749
+ "worker-a": {
1750
+ "provider": "codex",
1751
+ "status": "running",
1752
+ "spawn_cwd": "/tmp/u1-cwd"
1753
+ }
1754
+ }
1755
+ });
1756
+ let candidate = leader_like_candidate(
1757
+ "019f3327-shared",
1758
+ "/Users/alauda/.codex/sessions/2026/07/06/rollout-shared.jsonl",
1759
+ );
1760
+ let mut canned = BTreeMap::new();
1761
+ canned.insert("parent-peer".to_string(), vec![candidate.clone()]);
1762
+ canned.insert("worker-a".to_string(), vec![candidate]);
1763
+ let canned_for_adapter = canned.clone();
1764
+ let mut adapter_for = move |provider| {
1765
+ Box::new(
1766
+ test_support::CaptureCandidatesAdapter::new(provider, None, "")
1767
+ .with_candidates(canned_for_adapter.clone()),
1768
+ ) as Box<dyn ProviderAdapter>
1769
+ };
1770
+
1771
+ let report = capture_missing_provider_sessions_once(&mut state, &mut adapter_for, true, 0)
1772
+ .expect("capture pass succeeds");
1773
+
1774
+ assert!(report.assigned.is_empty(), "report={report:?}");
1775
+ assert!(state["agents"]["parent-peer"].get("session_id").is_none());
1776
+ assert!(state["agents"]["worker-a"].get("session_id").is_none());
1777
+ }
1778
+
1705
1779
  #[test]
1706
1780
  fn codex_embedded_identity_assigns_each_same_cwd_worker_to_own_rollout() {
1707
1781
  let mut state = serde_json::json!({
@@ -0,0 +1,112 @@
1
+ use super::*;
2
+
3
+ fn temp_dir(name: &str) -> PathBuf {
4
+ static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
5
+ let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6
+ let dir =
7
+ std::env::temp_dir().join(format!("ta-session-scan-{name}-{}-{n}", std::process::id()));
8
+ std::fs::create_dir_all(&dir).expect("create temp dir");
9
+ dir
10
+ }
11
+
12
+ fn write_codex_rollout(path: &Path, cwd: &Path, session_id: &str, embedded_agent_id: &str) {
13
+ let text = format!(
14
+ "{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}}}}\n\
15
+ {{\"type\":\"turn_context\",\"payload\":{{}}}}\n\
16
+ {{\"type\":\"response_item\",\"payload\":{{\"content\":[{{\"type\":\"input_text\",\"text\":\"You are Team Agent worker `{embedded_agent_id}` with role `fixture`.\"}}]}}}}\n",
17
+ cwd.to_string_lossy()
18
+ );
19
+ std::fs::write(path, text).expect("write codex rollout");
20
+ }
21
+
22
+ #[test]
23
+ fn codex_prompt_worker_identity_is_a_positive_match() {
24
+ let dir = temp_dir("positive");
25
+ let rollout = dir.join("rollout-frontend.jsonl");
26
+ write_codex_rollout(&rollout, &dir, "sess-frontend", "frontend");
27
+ let context = CaptureSessionContext {
28
+ agent_id: "frontend".to_string(),
29
+ spawn_cwd: dir.clone(),
30
+ pane_id: None,
31
+ pane_pid: None,
32
+ spawned_at: None,
33
+ expected_session_id: None,
34
+ provider_projects_root: None,
35
+ };
36
+ let candidates = parse_candidate_files(
37
+ Provider::Codex,
38
+ &context,
39
+ vec![SessionCandidate {
40
+ path: rollout.clone(),
41
+ requires_cwd_match: false,
42
+ }],
43
+ );
44
+ assert_eq!(candidates.len(), 1);
45
+ assert!(candidates[0].positive_agent_id_match);
46
+ let _ = std::fs::remove_dir_all(&dir);
47
+ }
48
+
49
+ #[test]
50
+ fn codex_identity_in_incomplete_final_head_record_is_positive() {
51
+ let dir = temp_dir("cross-cap-positive");
52
+ let rollout = dir.join("rollout-worker-a.jsonl");
53
+ let meta = format!(
54
+ "{{\"session_meta\":{{\"payload\":{{\"id\":\"sess-worker-a\",\"cwd\":\"{}\"}}}}}}\n",
55
+ dir.to_string_lossy()
56
+ );
57
+ let marker = "You are Team Agent worker `worker-a` with role `fixture`.";
58
+ let body = format!("{meta}{{\"text\":\"{marker}{}\"}}\n", "x".repeat(70_000));
59
+ std::fs::write(&rollout, body).expect("write cross-cap rollout");
60
+ assert!(!read_head_text(&rollout, CAPTURE_HEAD_BYTES)
61
+ .expect("read complete records")
62
+ .contains(marker));
63
+ let context = CaptureSessionContext {
64
+ agent_id: "worker-a".to_string(),
65
+ spawn_cwd: dir.clone(),
66
+ pane_id: None,
67
+ pane_pid: None,
68
+ spawned_at: None,
69
+ expected_session_id: None,
70
+ provider_projects_root: None,
71
+ };
72
+
73
+ let candidates = parse_candidate_files(
74
+ Provider::Codex,
75
+ &context,
76
+ vec![SessionCandidate {
77
+ path: rollout,
78
+ requires_cwd_match: false,
79
+ }],
80
+ );
81
+
82
+ assert_eq!(candidates.len(), 1);
83
+ assert_eq!(candidates[0].embedded_agent_id.as_deref(), Some("worker-a"));
84
+ assert!(candidates[0].positive_agent_id_match);
85
+ let _ = std::fs::remove_dir_all(&dir);
86
+ }
87
+
88
+ #[test]
89
+ fn codex_prompt_worker_identity_mismatch_is_rejected_for_that_agent() {
90
+ let dir = temp_dir("mismatch");
91
+ let rollout = dir.join("rollout-ios-dev.jsonl");
92
+ write_codex_rollout(&rollout, &dir, "sess-ios-dev", "ios-dev");
93
+ let context = CaptureSessionContext {
94
+ agent_id: "frontend".to_string(),
95
+ spawn_cwd: dir.clone(),
96
+ pane_id: None,
97
+ pane_pid: None,
98
+ spawned_at: None,
99
+ expected_session_id: None,
100
+ provider_projects_root: None,
101
+ };
102
+ let candidates = parse_candidate_files(
103
+ Provider::Codex,
104
+ &context,
105
+ vec![SessionCandidate {
106
+ path: rollout,
107
+ requires_cwd_match: false,
108
+ }],
109
+ );
110
+ assert!(candidates.is_empty());
111
+ let _ = std::fs::remove_dir_all(&dir);
112
+ }
@@ -69,9 +69,11 @@ pub(super) fn parse_candidate_files(
69
69
  let mut out = Vec::new();
70
70
  for candidate in candidates {
71
71
  let path = candidate.path;
72
- let Ok(text) = read_head_text(&path, CAPTURE_HEAD_BYTES) else {
72
+ let Ok(head_bytes) = read_head_bytes(&path, CAPTURE_HEAD_BYTES) else {
73
73
  continue;
74
74
  };
75
+ let text = complete_head_text(&head_bytes);
76
+ let identity_text = String::from_utf8_lossy(&head_bytes);
75
77
  let records = parse_session_records(&text);
76
78
  if records.is_empty() {
77
79
  continue;
@@ -98,7 +100,7 @@ pub(super) fn parse_candidate_files(
98
100
  } else {
99
101
  Confidence::Low
100
102
  };
101
- let embedded_agent_id = embedded_team_agent_worker_id_from_text(&text);
103
+ let embedded_agent_id = embedded_team_agent_worker_id_from_text(&identity_text);
102
104
  if embedded_agent_id
103
105
  .as_deref()
104
106
  .is_some_and(|id| id != context.agent_id.as_str())
@@ -241,15 +243,23 @@ fn cap_candidates_by_mtime(out: &mut Vec<SessionCandidate>, cap: usize) {
241
243
  }
242
244
 
243
245
  pub(super) fn read_head_text(path: &Path, max_bytes: u64) -> std::io::Result<String> {
246
+ read_head_bytes(path, max_bytes).map(|bytes| complete_head_text(&bytes))
247
+ }
248
+
249
+ fn read_head_bytes(path: &Path, max_bytes: u64) -> std::io::Result<Vec<u8>> {
244
250
  use std::io::Read;
245
251
  let file = std::fs::File::open(path)?;
246
252
  let mut bytes = Vec::new();
247
253
  file.take(max_bytes).read_to_end(&mut bytes)?;
254
+ Ok(bytes)
255
+ }
256
+
257
+ fn complete_head_text(bytes: &[u8]) -> String {
248
258
  let complete = match bytes.iter().rposition(|byte| *byte == b'\n') {
249
259
  Some(last_newline) => &bytes[..=last_newline],
250
260
  None => &bytes[..],
251
261
  };
252
- Ok(String::from_utf8_lossy(complete).into_owned())
262
+ String::from_utf8_lossy(complete).into_owned()
253
263
  }
254
264
 
255
265
  fn collect_optional_candidate_files(
@@ -414,86 +424,4 @@ fn path_is_under_team_runtime(path: &Path) -> bool {
414
424
  }
415
425
 
416
426
  #[cfg(test)]
417
- mod tests {
418
- use super::*;
419
-
420
- fn temp_dir(name: &str) -> PathBuf {
421
- static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
422
- let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
423
- let dir =
424
- std::env::temp_dir().join(format!("ta-session-scan-{name}-{}-{n}", std::process::id()));
425
- std::fs::create_dir_all(&dir).expect("create temp dir");
426
- dir
427
- }
428
-
429
- fn write_codex_rollout(path: &Path, cwd: &Path, session_id: &str, embedded_agent_id: &str) {
430
- let text = format!(
431
- "{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}}}}\n\
432
- {{\"type\":\"turn_context\",\"payload\":{{}}}}\n\
433
- {{\"type\":\"response_item\",\"payload\":{{\"content\":[{{\"type\":\"input_text\",\"text\":\"You are Team Agent worker `{embedded_agent_id}` with role `fixture`.\"}}]}}}}\n",
434
- cwd.to_string_lossy()
435
- );
436
- std::fs::write(path, text).expect("write codex rollout");
437
- }
438
-
439
- #[test]
440
- fn codex_prompt_worker_identity_is_a_positive_match() {
441
- let dir = temp_dir("positive");
442
- let rollout = dir.join("rollout-frontend.jsonl");
443
- write_codex_rollout(&rollout, &dir, "sess-frontend", "frontend");
444
- let context = CaptureSessionContext {
445
- agent_id: "frontend".to_string(),
446
- spawn_cwd: dir.clone(),
447
- pane_id: None,
448
- pane_pid: None,
449
- spawned_at: None,
450
- expected_session_id: None,
451
- provider_projects_root: None,
452
- };
453
- let candidates = parse_candidate_files(
454
- Provider::Codex,
455
- &context,
456
- vec![SessionCandidate {
457
- path: rollout.clone(),
458
- requires_cwd_match: false,
459
- }],
460
- );
461
- assert_eq!(candidates.len(), 1);
462
- assert!(
463
- candidates[0].positive_agent_id_match,
464
- "Codex transcript prompt identity must be treated as a positive worker id source"
465
- );
466
- let _ = std::fs::remove_file(&rollout);
467
- let _ = std::fs::remove_dir_all(&dir);
468
- }
469
-
470
- #[test]
471
- fn codex_prompt_worker_identity_mismatch_is_rejected_for_that_agent() {
472
- let dir = temp_dir("mismatch");
473
- let rollout = dir.join("rollout-ios-dev.jsonl");
474
- write_codex_rollout(&rollout, &dir, "sess-ios-dev", "ios-dev");
475
- let context = CaptureSessionContext {
476
- agent_id: "frontend".to_string(),
477
- spawn_cwd: dir.clone(),
478
- pane_id: None,
479
- pane_pid: None,
480
- spawned_at: None,
481
- expected_session_id: None,
482
- provider_projects_root: None,
483
- };
484
- let candidates = parse_candidate_files(
485
- Provider::Codex,
486
- &context,
487
- vec![SessionCandidate {
488
- path: rollout.clone(),
489
- requires_cwd_match: false,
490
- }],
491
- );
492
- assert!(
493
- candidates.is_empty(),
494
- "state agent=frontend must not accept a Codex rollout whose prompt says ios-dev"
495
- );
496
- let _ = std::fs::remove_file(&rollout);
497
- let _ = std::fs::remove_dir_all(&dir);
498
- }
499
- }
427
+ mod tests;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.53",
3
+ "version": "0.5.55",
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.53",
24
- "@team-agent/cli-darwin-x64": "0.5.53",
25
- "@team-agent/cli-linux-x64": "0.5.53"
23
+ "@team-agent/cli-darwin-arm64": "0.5.55",
24
+ "@team-agent/cli-darwin-x64": "0.5.55",
25
+ "@team-agent/cli-linux-x64": "0.5.55"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -11,6 +11,16 @@
11
11
  "agent_id": { "type": "string", "minLength": 1 },
12
12
  "status": { "enum": ["success", "blocked", "failed", "partial"] },
13
13
  "summary": { "type": "string" },
14
+ "presentation": {
15
+ "type": "object",
16
+ "required": ["sink", "class"],
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "sink": { "enum": ["leader", "casefile", "silent"] },
20
+ "class": { "enum": ["message", "progress", "stage_result", "stage_pass", "bounce", "blocking", "final_review", "timeout"] },
21
+ "case_id": { "type": "string", "minLength": 1 }
22
+ }
23
+ },
14
24
  "changes": {
15
25
  "type": "array",
16
26
  "items": {
@@ -166,6 +166,7 @@ For diagnosis, run `team-agent profile show deepseek --workspace . --json`; neve
166
166
  - `quick-start` is only for first-time team creation from role docs. If that team already has runtime state, use `team-agent restart . --team <session_name_or_team_name>` to resume it. If restart cannot recover context, explain the loss and wait for explicit user consent before using `team-agent restart . --allow-fresh`; never reset context through quick-start.
167
167
  - If the user explicitly asks a worker to create or operate a nested child team, first read `references/team-in-team.md`. Child teams must use an independent child workspace, never the parent `.team/current`.
168
168
  - `team-agent send --watch-result coder "Do the bounded task"` sends a direct worker message, returns after delivery, and lets the coordinator collect/report completion asynchronously.
169
+ - Advanced orchestration callers may add `--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]`. All sinks remain durable and pullable; `casefile`/`silent` suppress only live leader injection. Missing presentation metadata preserves the normal leader-visible behavior.
169
170
  - After `send --watch-result` succeeds, do not run `sleep`, `status`, `inbox`, or `collect` polling loops unless the user explicitly asks for diagnosis; the coordinator will notify the leader when the result arrives.
170
171
  - `team-agent send --task task_initial "Start"` routes by task.
171
172
  - `team-agent status` shows team, worker health, result-store counts, `session_id`, `captured_via`, and attribution confidence. `team-agent status --json` is compact and context-safe by default; use `team-agent status --detail --json` only for raw runtime-state diagnostics.
@@ -237,6 +238,8 @@ team_orchestrator.send_message(to="*", content="short broadcast")
237
238
  team_orchestrator.report_result(summary="short completion", status="success", tests=[{"command":"command","status":"passed"}])
238
239
  ```
239
240
 
241
+ For typed orchestration traffic, both `send_message` and `report_result` accept `presentation={"sink":"leader|casefile|silent","class":"message|progress|stage_result|stage_pass|bounce|blocking|final_review|timeout","case_id":"optional-case"}`. If the object is present, `sink` and `class` are required and unknown values fail closed. `casefile` and `silent` are durable-only, not deletion. The fixed critical classes `stage_pass`, `bounce`, `blocking`, `final_review`, and `timeout` always appear on the leader screen even when another sink is requested. Routing uses the typed class, never words in the content or summary.
242
+
240
243
  Do not pass `sender`, `task_id`, `requires_ack`, `schema_version`, or `agent_id` unless doing a low-level compatibility diagnostic. The MCP runtime fills those fields and keeps delivery metadata in runtime state and event logs. If provider env loses the worker id, MCP infers it from active task/message state and falls back to an explicit `unknown` sender instead of treating the worker as leader.
241
244
 
242
245
  Message targets are team-scoped. Use `leader`, another teammate agent id, or `*` for all other team members. The runtime excludes the sender from `*` broadcasts and never scans unrelated terminal windows for recipients.