@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.
- 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 +117 -1
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/watchers.rs +13 -7
- 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
|
@@ -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
|
|
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
|
|
473
|
-
///
|
|
474
|
-
///
|
|
475
|
-
///
|
|
476
|
-
///
|
|
477
|
-
///
|
|
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(
|
|
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(&
|
|
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
|
-
|
|
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.
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
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.
|