@team-agent/installer 0.5.86 → 0.5.91
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/adapters.rs +104 -164
- package/crates/team-agent/src/cli/emit.rs +68 -87
- package/crates/team-agent/src/cli/mod.rs +1 -1
- package/crates/team-agent/src/cli/spec.rs +1 -4
- package/crates/team-agent/src/cli/status_port/brief.rs +187 -7
- package/crates/team-agent/src/cli/status_port/inbox.rs +42 -17
- package/crates/team-agent/src/cli/tests/lane_c.rs +34 -26
- package/crates/team-agent/src/cli/tests/main_preserved.rs +4 -70
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +0 -68
- package/crates/team-agent/src/cli/types.rs +3 -16
- package/crates/team-agent/src/compiler.rs +4 -7
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +5 -6
- package/crates/team-agent/src/coordinator/tick.rs +1 -16
- package/crates/team-agent/src/coordinator/types.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +10 -5
- package/crates/team-agent/src/fake_worker.rs +4 -4
- package/crates/team-agent/src/lifecycle/launch/identity.rs +39 -21
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -10
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -9
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -26
- package/crates/team-agent/src/main.rs +7 -2
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +234 -386
- package/crates/team-agent/src/messaging/tests/runtime.rs +91 -135
- package/crates/team-agent/src/messaging/watchers.rs +1 -1
- package/crates/team-agent/src/model/enums.rs +17 -3
- package/crates/team-agent/src/model/spec.rs +8 -7
- package/crates/team-agent/src/provider/wire.rs +1 -1
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -3
- package/skills/team-agent/command-coverage.json +2 -9
- package/skills/team-agent/references/team-agent-operator.md +661 -0
|
@@ -208,7 +208,7 @@ fn inbox_returns_stored_message_for_recipient() {
|
|
|
208
208
|
let mid = store
|
|
209
209
|
.create_message(None, "leader", "w1", "hello w1", None, true, None)
|
|
210
210
|
.unwrap();
|
|
211
|
-
let v = status_port::inbox(&ws, "w1", 20, None
|
|
211
|
+
let v = status_port::inbox(&ws, "w1", 20, None).expect("inbox");
|
|
212
212
|
let messages = v["messages"].as_array().expect("messages array");
|
|
213
213
|
assert_eq!(
|
|
214
214
|
messages.len(),
|
|
@@ -223,30 +223,12 @@ fn inbox_returns_stored_message_for_recipient() {
|
|
|
223
223
|
);
|
|
224
224
|
assert_eq!(m["recipient"], json!("w1"));
|
|
225
225
|
assert_eq!(m["sender"], json!("leader"));
|
|
226
|
-
assert_eq!(m["
|
|
226
|
+
assert_eq!(m["summary"], json!("hello w1"));
|
|
227
227
|
assert_eq!(
|
|
228
228
|
m["status"],
|
|
229
229
|
json!("accepted"),
|
|
230
230
|
"create_message persists status='accepted'"
|
|
231
231
|
);
|
|
232
|
-
// NULL owner_team_id semantics: status.inbox() calls MessageStore.inbox(agent) with
|
|
233
|
-
// owner_team_id=None (no team clause), so a NULL-owner message MUST surface for its recipient.
|
|
234
|
-
assert_eq!(
|
|
235
|
-
m["owner_team_id"],
|
|
236
|
-
json!(null),
|
|
237
|
-
"the stored message's owner_team_id is NULL and still returned"
|
|
238
|
-
);
|
|
239
|
-
// byte-faithful raw-row columns: requires_ack is the 0/1 INT; artifact_refs the literal text "[]".
|
|
240
|
-
assert_eq!(
|
|
241
|
-
m["requires_ack"],
|
|
242
|
-
json!(1),
|
|
243
|
-
"requires_ack is the 0/1 int, not a bool"
|
|
244
|
-
);
|
|
245
|
-
assert_eq!(
|
|
246
|
-
m["artifact_refs"],
|
|
247
|
-
json!("[]"),
|
|
248
|
-
"artifact_refs is the raw text column, not parsed"
|
|
249
|
-
);
|
|
250
232
|
let _ = std::fs::remove_dir_all(&ws);
|
|
251
233
|
}
|
|
252
234
|
// ── BUG-2 (match scope) — inbox(agent) returns rows where sender==agent OR recipient==agent, and
|
|
@@ -265,11 +247,11 @@ fn inbox_matches_sender_or_recipient_and_excludes_others() {
|
|
|
265
247
|
store
|
|
266
248
|
.create_message(None, "leader", "w2", "unrelated to w2", None, true, None)
|
|
267
249
|
.unwrap();
|
|
268
|
-
let v = status_port::inbox(&ws, "w1", 20, None
|
|
250
|
+
let v = status_port::inbox(&ws, "w1", 20, None).expect("inbox");
|
|
269
251
|
let messages = v["messages"].as_array().expect("messages array");
|
|
270
252
|
let mut contents: Vec<String> = messages
|
|
271
253
|
.iter()
|
|
272
|
-
.map(|m| m["
|
|
254
|
+
.map(|m| m["summary"].as_str().unwrap().to_string())
|
|
273
255
|
.collect();
|
|
274
256
|
contents.sort();
|
|
275
257
|
assert_eq!(
|
|
@@ -691,54 +673,6 @@ fn remove_agent_from_spec_refusal_is_not_success_envelope() {
|
|
|
691
673
|
);
|
|
692
674
|
}
|
|
693
675
|
#[test]
|
|
694
|
-
fn collect_uncollected_result_marks_db_and_outputs_result() {
|
|
695
|
-
let ws = tmp_workspace();
|
|
696
|
-
seed_collect_state(&ws);
|
|
697
|
-
seed_uncollected_result(&ws, "res_collect_red");
|
|
698
|
-
let out = json_output(
|
|
699
|
-
cmd_collect(&CollectArgs {
|
|
700
|
-
workspace: ws.clone(),
|
|
701
|
-
result_file: None,
|
|
702
|
-
json: true,
|
|
703
|
-
team: None,
|
|
704
|
-
})
|
|
705
|
-
.unwrap(),
|
|
706
|
-
);
|
|
707
|
-
assert_eq!(out["ok"], json!(true));
|
|
708
|
-
assert_eq!(
|
|
709
|
-
out["collected_results"][0]["result_id"],
|
|
710
|
-
json!("res_collect_red")
|
|
711
|
-
);
|
|
712
|
-
assert_eq!(out["collected_results"][0]["scope"], json!("task"));
|
|
713
|
-
assert_eq!(
|
|
714
|
-
out["results"],
|
|
715
|
-
json!({"total": 1, "uncollected": 0, "collected": 1, "invalid": 0, "by_status": {}})
|
|
716
|
-
);
|
|
717
|
-
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
718
|
-
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
719
|
-
let status: String = conn
|
|
720
|
-
.query_row(
|
|
721
|
-
"select status from results where result_id = 'res_collect_red'",
|
|
722
|
-
[],
|
|
723
|
-
|row| row.get(0),
|
|
724
|
-
)
|
|
725
|
-
.unwrap();
|
|
726
|
-
assert_eq!(status, "collected");
|
|
727
|
-
let state = read_state(&ws);
|
|
728
|
-
assert_eq!(state["tasks"][0]["status"], json!("done"));
|
|
729
|
-
assert_eq!(
|
|
730
|
-
state["tasks"][0]["accepted_result_id"],
|
|
731
|
-
json!("res_collect_red")
|
|
732
|
-
);
|
|
733
|
-
assert!(
|
|
734
|
-
read_events(&ws)
|
|
735
|
-
.iter()
|
|
736
|
-
.any(|e| e["event"] == json!("collect.result")
|
|
737
|
-
&& e["result_id"] == json!("res_collect_red")),
|
|
738
|
-
"collect must emit collect.result for the stored result"
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
#[test]
|
|
742
676
|
fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
|
|
743
677
|
let ws = tmp_workspace();
|
|
744
678
|
seed_collect_state(&ws);
|
|
@@ -275,36 +275,6 @@ fn seed_team_spec(ws: &std::path::Path) {
|
|
|
275
275
|
std::fs::write(ws.join("team.spec.yaml"), spec).unwrap();
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
fn seed_collect_runtime_state(ws: &std::path::Path) {
|
|
279
|
-
crate::state::persist::save_runtime_state(
|
|
280
|
-
ws,
|
|
281
|
-
&json!({
|
|
282
|
-
"active_team_key": "fake-e2e",
|
|
283
|
-
"team_dir": ws.to_string_lossy().to_string(),
|
|
284
|
-
"spec_path": ws.join("team.spec.yaml").to_string_lossy().to_string(),
|
|
285
|
-
"session_name": "team-agent-fake-e2e",
|
|
286
|
-
"leader": {"id": "leader"},
|
|
287
|
-
"agents": {
|
|
288
|
-
"fake_impl": {
|
|
289
|
-
"status": "running",
|
|
290
|
-
"provider": "fake",
|
|
291
|
-
"role": "implementation_engineer",
|
|
292
|
-
"window": "fake_impl",
|
|
293
|
-
"owner_team_id": "fake-e2e"
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
"tasks": [{
|
|
297
|
-
"id": "task_impl",
|
|
298
|
-
"title": "Fake implementation",
|
|
299
|
-
"type": "implementation",
|
|
300
|
-
"status": "pending",
|
|
301
|
-
"assignee": "fake_impl"
|
|
302
|
-
}]
|
|
303
|
-
}),
|
|
304
|
-
)
|
|
305
|
-
.unwrap();
|
|
306
|
-
}
|
|
307
|
-
|
|
308
278
|
// ── sessions ── golden cli/parser.py:230 `cmd_sessions` -> runtime.sessions(ws). EXIT 0.
|
|
309
279
|
// `team-agent sessions --workspace <ws> --json` on an empty ws ->
|
|
310
280
|
// {"ok":true,"sessions":[],"workspace":"<ws>"} (--json sort_keys). RED: unrouted -> Error.
|
|
@@ -324,44 +294,6 @@ fn dispatch_routes_sessions_subcommand() {
|
|
|
324
294
|
let _ = std::fs::remove_dir_all(&ws);
|
|
325
295
|
}
|
|
326
296
|
|
|
327
|
-
// ── collect ── golden parser.py:292 `cmd_collect` -> runtime.collect(ws). With a valid
|
|
328
|
-
// team.spec.yaml present and nothing to collect -> EXIT 0, golden:
|
|
329
|
-
// {"collected":[],"collected_results":[],"coordinator":{"ok":false,"status":"not_required"},
|
|
330
|
-
// "delivered_messages":[],"invalid_results":[],"ok":true,"results":{...},"state_file":"<ws>/team_state.md"}
|
|
331
|
-
// RED: unrouted -> Error.
|
|
332
|
-
#[test]
|
|
333
|
-
#[serial(env)]
|
|
334
|
-
fn dispatch_routes_collect_with_spec() {
|
|
335
|
-
let _env = EnvUnsetGuard::unset(&[
|
|
336
|
-
"TEAM_AGENT_WORKSPACE",
|
|
337
|
-
"TEAM_AGENT_TEAM_ID",
|
|
338
|
-
"TEAM_AGENT_OWNER_TEAM_ID",
|
|
339
|
-
"TEAM_AGENT_ACTIVE_TEAM",
|
|
340
|
-
"TEAM_AGENT_ID",
|
|
341
|
-
"TEAM_AGENT_LEADER_PANE_ID",
|
|
342
|
-
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
343
|
-
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
344
|
-
"TEAM_AGENT_LEADER_PROVIDER",
|
|
345
|
-
"TMUX",
|
|
346
|
-
"TMUX_PANE",
|
|
347
|
-
]);
|
|
348
|
-
let ws = tmp_workspace();
|
|
349
|
-
let _cleanup = WorkspaceCleanup(ws.clone());
|
|
350
|
-
seed_team_spec(&ws);
|
|
351
|
-
seed_collect_runtime_state(&ws);
|
|
352
|
-
let code = run(
|
|
353
|
-
&cli_argv(&["collect", "--workspace", &ws.to_string_lossy(), "--json"]),
|
|
354
|
-
&ws,
|
|
355
|
-
);
|
|
356
|
-
assert_eq!(
|
|
357
|
-
code,
|
|
358
|
-
ExitCode::Ok,
|
|
359
|
-
"`collect` must ROUTE to cmd_collect (parser.py:292); with a valid spec golden exits 0 \
|
|
360
|
-
{{collected,collected_results,coordinator,delivered_messages,invalid_results,ok,results,state_file}}; \
|
|
361
|
-
today -> unknown-subcommand Error"
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
297
|
// ── diagnose ── attached is host-registry authority, not state.json self-sign.
|
|
366
298
|
// Seed leader_receiver=attached + a ~/.team-agent/leaders/ row for this workspace
|
|
367
299
|
// + NO session_name + NO agents -> issues=[] -> EXIT 0.
|
|
@@ -44,10 +44,10 @@ pub enum CliError {
|
|
|
44
44
|
/// state 解析失败(歧义/未找到 team 等)。透传 step 5。
|
|
45
45
|
#[error("{0}")]
|
|
46
46
|
State(#[from] crate::state::StateError),
|
|
47
|
-
/// messaging 委派失败(send/
|
|
47
|
+
/// messaging 委派失败(send/stuck)。透传 step 11。
|
|
48
48
|
#[error("{0}")]
|
|
49
49
|
Messaging(crate::messaging::MessagingError),
|
|
50
|
-
/// I/O(cli-error
|
|
50
|
+
/// I/O(cli-error 落盘)。bug-084:写路径降级,不裸 panic。
|
|
51
51
|
#[error("io: {0}")]
|
|
52
52
|
Io(#[from] std::io::Error),
|
|
53
53
|
/// JSON 解析。
|
|
@@ -407,13 +407,12 @@ pub struct ApprovalsArgs {
|
|
|
407
407
|
pub team: Option<String>,
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
/// `inbox`(`parser.py:217`)
|
|
410
|
+
/// `inbox`(`parser.py:217`)。极简消息兜底查看器。
|
|
411
411
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
412
412
|
pub struct InboxArgs {
|
|
413
413
|
pub agent: String,
|
|
414
414
|
pub workspace: PathBuf,
|
|
415
415
|
pub limit: usize,
|
|
416
|
-
pub since: Option<String>,
|
|
417
416
|
pub json: bool,
|
|
418
417
|
/// Stage 4: explicit `--team` scope.
|
|
419
418
|
pub team: Option<String>,
|
|
@@ -682,18 +681,6 @@ pub struct ProfileArgs {
|
|
|
682
681
|
pub json: bool,
|
|
683
682
|
}
|
|
684
683
|
|
|
685
|
-
/// `collect`(`parser.py:292`)。
|
|
686
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
687
|
-
pub struct CollectArgs {
|
|
688
|
-
pub workspace: PathBuf,
|
|
689
|
-
pub result_file: Option<PathBuf>,
|
|
690
|
-
pub json: bool,
|
|
691
|
-
/// Stage 4: explicit `--team` scope. Destructive (consumes result
|
|
692
|
-
/// envelopes); CLI dispatch refuses bare invocation in a
|
|
693
|
-
/// multi-alive-team workspace.
|
|
694
|
-
pub team: Option<String>,
|
|
695
|
-
}
|
|
696
|
-
|
|
697
684
|
/// Read-only lookup of all stored result envelopes for one case.
|
|
698
685
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
699
686
|
pub struct ResultsArgs {
|
|
@@ -32,8 +32,8 @@ use crate::model::enums::{Provider, ProviderEffort};
|
|
|
32
32
|
use crate::model::yaml::Value;
|
|
33
33
|
use crate::model::{paths, spec, yaml, ModelError};
|
|
34
34
|
use crate::provider::wire::{
|
|
35
|
-
builtin_provider_model as wire_builtin_provider_model,
|
|
36
|
-
|
|
35
|
+
builtin_provider_model as wire_builtin_provider_model, parse_canonical_provider,
|
|
36
|
+
provider_model_keys,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
pub const IGNORED_OWNER_TEAM_ID_FIELD: &str = "owner_team_id";
|
|
@@ -602,12 +602,9 @@ fn compile_role_agent_with_mode(
|
|
|
602
602
|
})
|
|
603
603
|
.unwrap_or("");
|
|
604
604
|
let provider_enum = parse_canonical_provider(provider_str).unwrap_or(Provider::Codex);
|
|
605
|
-
if effort.
|
|
606
|
-
&& !is_claude_family(provider_enum)
|
|
607
|
-
&& provider_enum != Provider::Pi
|
|
608
|
-
{
|
|
605
|
+
if let Err(reason) = effort.resolve_for_provider(provider_enum) {
|
|
609
606
|
return Err(ModelError::Validation(format!(
|
|
610
|
-
"{}: effort
|
|
607
|
+
"{}: {reason} (effort: {}; provider: {provider_str})",
|
|
611
608
|
role_path.display(),
|
|
612
609
|
effort.as_str()
|
|
613
610
|
)));
|
|
@@ -82,7 +82,7 @@ fn tick_side_effect_order_is_the_fixed_sequence() {
|
|
|
82
82
|
// startup_prompts -> runtime_prompts -> sync_health -> deliver_pending ->
|
|
83
83
|
// fire_scheduled -> detect_stuck -> record_unknown_idle -> evaluate_takeover ->
|
|
84
84
|
// detect_deadlocks -> detect_compaction -> detect_drift -> detect_api_errors ->
|
|
85
|
-
// ATOMIC_save (bug-084 wrap) ->
|
|
85
|
+
// ATOMIC_save (bug-084 wrap) -> prune_dedupe_log.
|
|
86
86
|
// The porter must push each step name into the injected recorder at its call site.
|
|
87
87
|
let recorder: OrderRecorder = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
88
88
|
let (coord, _calls) = coord_for_test(true, None, Some(std::sync::Arc::clone(&recorder)));
|
|
@@ -106,19 +106,18 @@ fn tick_side_effect_order_is_the_fixed_sequence() {
|
|
|
106
106
|
"detect_drift",
|
|
107
107
|
"detect_api_errors",
|
|
108
108
|
"atomic_save",
|
|
109
|
-
"collect_results",
|
|
110
109
|
"prune_dedupe_log",
|
|
111
110
|
];
|
|
112
111
|
assert_eq!(
|
|
113
112
|
order, expected,
|
|
114
113
|
"tick side-effect ORDER must match the fixed sequence"
|
|
115
114
|
);
|
|
116
|
-
// ATOMIC save is the LAST mutation before read-only
|
|
115
|
+
// ATOMIC save is the LAST mutation before the read-only prune (bug-084 wrap point).
|
|
117
116
|
let save_idx = order.iter().position(|s| *s == "atomic_save").unwrap();
|
|
118
|
-
let
|
|
117
|
+
let prune_idx = order.iter().position(|s| *s == "prune_dedupe_log").unwrap();
|
|
119
118
|
assert!(
|
|
120
|
-
save_idx <
|
|
121
|
-
"save precedes
|
|
119
|
+
save_idx < prune_idx,
|
|
120
|
+
"save precedes prune (bug-084 wrap is the last mutation)"
|
|
122
121
|
);
|
|
123
122
|
}
|
|
124
123
|
|
|
@@ -104,7 +104,7 @@ pub struct TickReport {
|
|
|
104
104
|
pub session_drift: Vec<SessionDriftResult>,
|
|
105
105
|
/// `detect_leader_api_errors`(`lifecycle.py:344`)——cross-dep step 11。
|
|
106
106
|
pub api_errors: Vec<LeaderApiError>,
|
|
107
|
-
///
|
|
107
|
+
/// Reserved result projection slot; report_result now finalizes directly.
|
|
108
108
|
pub results: Vec<CollectedResult>,
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -528,12 +528,6 @@ impl Coordinator {
|
|
|
528
528
|
collections,
|
|
529
529
|
));
|
|
530
530
|
}
|
|
531
|
-
self.record_step(TickStepGroup::Delivery, "collect_results");
|
|
532
|
-
collections.results =
|
|
533
|
-
collect_results(crate::messaging::collect_results_and_notify_watchers(
|
|
534
|
-
self.workspace.as_path(),
|
|
535
|
-
&event_log,
|
|
536
|
-
)?);
|
|
537
531
|
self.record_step(TickStepGroup::Persist, "prune_dedupe_log");
|
|
538
532
|
Ok(base_tick_report(true, false, None, Some(true), collections))
|
|
539
533
|
}
|
|
@@ -1554,15 +1548,6 @@ fn empty_tick_report(
|
|
|
1554
1548
|
base_tick_report(ok, stop, reason, persisted, TickCollections::default())
|
|
1555
1549
|
}
|
|
1556
1550
|
|
|
1557
|
-
fn collect_results(value: Value) -> Vec<CollectedResult> {
|
|
1558
|
-
let Some(result_id) = value.get("result_id").and_then(Value::as_str) else {
|
|
1559
|
-
return Vec::new();
|
|
1560
|
-
};
|
|
1561
|
-
vec![CollectedResult {
|
|
1562
|
-
result_id: result_id.to_string(),
|
|
1563
|
-
}]
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
1551
|
struct ProviderTurnClassifier;
|
|
1567
1552
|
|
|
1568
1553
|
impl TurnStateClassifier for ProviderTurnClassifier {
|
|
@@ -543,7 +543,7 @@ pub struct LeaderApiError {
|
|
|
543
543
|
pub message: String,
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
-
///
|
|
546
|
+
/// Result projection reported by a coordinator tick. Report results now finalize inline.
|
|
547
547
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
548
548
|
pub struct CollectedResult {
|
|
549
549
|
pub result_id: String,
|
|
@@ -727,9 +727,11 @@ impl MessageStore {
|
|
|
727
727
|
Ok(changed == 1)
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
-
/// Read inbox rows for an agent.
|
|
731
|
-
///
|
|
732
|
-
///
|
|
730
|
+
/// Read ordinary inbox rows for an agent. Leader stage-result notifications are
|
|
731
|
+
/// delivered through the leader receiver, not exposed as mailbox fallback rows.
|
|
732
|
+
/// This projection intentionally has no owner-team filter when the caller does
|
|
733
|
+
/// not provide one: legacy/CLI inbox must surface NULL-owner messages stored for
|
|
734
|
+
/// the agent.
|
|
733
735
|
pub fn inbox(
|
|
734
736
|
&self,
|
|
735
737
|
agent_id: &str,
|
|
@@ -744,7 +746,9 @@ impl MessageStore {
|
|
|
744
746
|
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
745
747
|
acknowledged_at, error, delivery_attempts
|
|
746
748
|
from messages
|
|
747
|
-
where (sender = ?1 or recipient = ?1)
|
|
749
|
+
where (sender = ?1 or recipient = ?1)
|
|
750
|
+
and owner_team_id = ?3
|
|
751
|
+
and coalesce(json_extract(presentation, '$.class'), 'message') != 'stage_result'
|
|
748
752
|
order by created_at desc
|
|
749
753
|
limit ?2"
|
|
750
754
|
}
|
|
@@ -753,7 +757,8 @@ impl MessageStore {
|
|
|
753
757
|
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
754
758
|
acknowledged_at, error, delivery_attempts
|
|
755
759
|
from messages
|
|
756
|
-
where sender = ?1 or recipient = ?1
|
|
760
|
+
where (sender = ?1 or recipient = ?1)
|
|
761
|
+
and coalesce(json_extract(presentation, '$.class'), 'message') != 'stage_result'
|
|
757
762
|
order by created_at desc
|
|
758
763
|
limit ?2"
|
|
759
764
|
}
|
|
@@ -220,8 +220,8 @@ mod tests {
|
|
|
220
220
|
let (task_id, agent_id, status, env) = stored_result(ws);
|
|
221
221
|
assert_eq!(
|
|
222
222
|
(task_id.as_str(), agent_id.as_str(), status.as_str()),
|
|
223
|
-
("t1", "w1", "
|
|
224
|
-
"stored result row must be task_id=t1 agent_id=w1 status=
|
|
223
|
+
("t1", "w1", "collected"),
|
|
224
|
+
"stored result row must be task_id=t1 agent_id=w1 status=collected"
|
|
225
225
|
);
|
|
226
226
|
assert_eq!(
|
|
227
227
|
env["schema_version"],
|
|
@@ -291,8 +291,8 @@ mod tests {
|
|
|
291
291
|
let (task_id, agent_id, status, envelope) = stored_result(&ws);
|
|
292
292
|
assert_eq!(
|
|
293
293
|
(task_id.as_str(), agent_id.as_str(), status.as_str()),
|
|
294
|
-
("m1", "w1", "
|
|
295
|
-
"a direct-message result must use its message id for message-scope
|
|
294
|
+
("m1", "w1", "collected"),
|
|
295
|
+
"a direct-message result must use its message id for message-scope auto-finalization"
|
|
296
296
|
);
|
|
297
297
|
assert_ne!(
|
|
298
298
|
envelope["task_id"],
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//! - name: transport_has_session
|
|
10
10
|
//! what: 探测 tmux session 是否存在,探测本身出错一律当不存在
|
|
11
11
|
//! - name: provider_effort_from_raw
|
|
12
|
-
//! what: 解析 effort
|
|
12
|
+
//! what: 解析 effort,保留兼容忽略项并拒绝非法 provider 组合
|
|
13
13
|
//! - name: quick_start_depth_guard
|
|
14
14
|
//! what: 定出本次 quick-start 的父团队与层级,推不出父团队时拒绝
|
|
15
15
|
//! - name: annotate_persisted_team_depth
|
|
@@ -135,24 +135,24 @@ pub(super) fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
|
135
135
|
/// purpose: 从原始字符串解析 effort,并要求当前 provider 支持它
|
|
136
136
|
/// params:
|
|
137
137
|
/// raw: effort 原文,None 或空白返回 None
|
|
138
|
-
/// returns:
|
|
138
|
+
/// returns: 支持的档位或兼容忽略的 None;已知非法 provider/effort 组合返回错误
|
|
139
139
|
/// ---
|
|
140
140
|
/// 0.4.x provider effort MVP step 4: low-level from a raw string. Returns
|
|
141
|
-
/// `Some(effort)`
|
|
141
|
+
/// `Some(effort)` for supported levels, None for legacy ignored values,
|
|
142
|
+
/// and an error for provider/effort combinations rejected by configuration.
|
|
142
143
|
pub(crate) fn provider_effort_from_raw(
|
|
143
144
|
raw: Option<&str>,
|
|
144
145
|
provider: Provider,
|
|
145
|
-
) -> Option<ProviderEffort> {
|
|
146
|
-
let
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
146
|
+
) -> Result<Option<ProviderEffort>, LifecycleError> {
|
|
147
|
+
let Some(effort) = raw.and_then(ProviderEffort::parse) else {
|
|
148
|
+
return Ok(None);
|
|
149
|
+
};
|
|
150
|
+
effort.resolve_for_provider(provider).map_err(|reason| {
|
|
151
|
+
LifecycleError::RequirementUnmet(format!(
|
|
152
|
+
"{reason} (effort: {}; provider: {provider:?})",
|
|
153
|
+
effort.as_str()
|
|
154
|
+
))
|
|
155
|
+
})
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/// ---
|
|
@@ -171,7 +171,7 @@ pub(crate) fn provider_effort_event_payload(
|
|
|
171
171
|
return None;
|
|
172
172
|
}
|
|
173
173
|
let effort = ProviderEffort::parse(raw)?;
|
|
174
|
-
if effort.
|
|
174
|
+
if !matches!(effort.resolve_for_provider(provider), Ok(None)) {
|
|
175
175
|
return None;
|
|
176
176
|
}
|
|
177
177
|
Some(serde_json::json!({
|
|
@@ -221,7 +221,7 @@ pub(crate) fn extend_worker_env_unset_for_effort(
|
|
|
221
221
|
pub(crate) fn provider_effort_for_spawn(
|
|
222
222
|
agent: &crate::model::yaml::Value,
|
|
223
223
|
provider: Provider,
|
|
224
|
-
) -> Option<ProviderEffort> {
|
|
224
|
+
) -> Result<Option<ProviderEffort>, LifecycleError> {
|
|
225
225
|
provider_effort_from_raw(agent.get("effort").and_then(|v| v.as_str()), provider)
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -252,7 +252,7 @@ pub(crate) fn provider_effort_event_if_dropped(
|
|
|
252
252
|
pub(crate) fn provider_effort_for_spawn_json(
|
|
253
253
|
agent: &serde_json::Value,
|
|
254
254
|
provider: Provider,
|
|
255
|
-
) -> Option<ProviderEffort> {
|
|
255
|
+
) -> Result<Option<ProviderEffort>, LifecycleError> {
|
|
256
256
|
provider_effort_from_raw(
|
|
257
257
|
agent.get("effort").and_then(serde_json::Value::as_str),
|
|
258
258
|
provider,
|
|
@@ -307,10 +307,16 @@ pub(super) fn quick_start_depth_guard(
|
|
|
307
307
|
requested_team: Option<&str>,
|
|
308
308
|
_strict_real_runtime: bool,
|
|
309
309
|
) -> Result<QuickStartDepth, LifecycleError> {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
310
|
+
// OWNER_TEAM_ID is injected into every worker process. It is parent
|
|
311
|
+
// context only when the target quick-start workspace is the same workspace
|
|
312
|
+
// that produced the environment; otherwise a worker launching a fresh
|
|
313
|
+
// external workspace would inherit a false nesting parent.
|
|
314
|
+
let env_parent = owner_context_matches_workspace(workspace).then(|| {
|
|
315
|
+
std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
|
|
316
|
+
.ok()
|
|
317
|
+
.map(|value| value.trim().to_string())
|
|
318
|
+
.filter(|value| !value.is_empty())
|
|
319
|
+
}).flatten();
|
|
314
320
|
let parent = env_parent;
|
|
315
321
|
let Some(parent) = parent else {
|
|
316
322
|
let state = crate::state::persist::load_runtime_state(workspace)
|
|
@@ -365,6 +371,18 @@ pub(super) fn quick_start_depth_guard(
|
|
|
365
371
|
/// purpose: 从 runtime state 推断父团队键
|
|
366
372
|
/// returns: 活跃团队键,且该团队确有 running 席位时返回它,否则 None
|
|
367
373
|
/// ---
|
|
374
|
+
fn owner_context_matches_workspace(workspace: &Path) -> bool {
|
|
375
|
+
let Some(env_workspace) = std::env::var_os("TEAM_AGENT_WORKSPACE") else {
|
|
376
|
+
return false;
|
|
377
|
+
};
|
|
378
|
+
let env_workspace = PathBuf::from(env_workspace);
|
|
379
|
+
env_workspace == workspace
|
|
380
|
+
|| std::fs::canonicalize(&env_workspace)
|
|
381
|
+
.ok()
|
|
382
|
+
.zip(std::fs::canonicalize(workspace).ok())
|
|
383
|
+
.is_some_and(|(env_path, target_path)| env_path == target_path)
|
|
384
|
+
}
|
|
385
|
+
|
|
368
386
|
pub(super) fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
|
|
369
387
|
let active = explicit_active_team_key(state)?;
|
|
370
388
|
let team = state
|
|
@@ -152,7 +152,7 @@ pub(super) fn spawn_agents(
|
|
|
152
152
|
// 0.4.x provider effort MVP step 4 + 7: resolve effort and emit
|
|
153
153
|
// unsupported warning event when the spec asked for an effort the
|
|
154
154
|
// provider can't satisfy.
|
|
155
|
-
let agent_effort = provider_effort_for_spawn(agent, provider)
|
|
155
|
+
let agent_effort = provider_effort_for_spawn(agent, provider)?;
|
|
156
156
|
if let Some(event_value) = provider_effort_event_if_dropped(agent, provider, agent_id_raw) {
|
|
157
157
|
let _ = crate::event_log::EventLog::new(workspace)
|
|
158
158
|
.write("provider.effort_unsupported", event_value);
|
|
@@ -355,7 +355,7 @@ pub(super) fn spawn_agent_window(
|
|
|
355
355
|
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
356
356
|
// 0.4.x provider effort MVP: restart/resume preserves effort from the
|
|
357
357
|
// persisted agent JSON (state's agent["effort"] field, set by launch).
|
|
358
|
-
let restart_effort = crate::lifecycle::launch::provider_effort_for_spawn_json(agent, provider)
|
|
358
|
+
let restart_effort = crate::lifecycle::launch::provider_effort_for_spawn_json(agent, provider)?;
|
|
359
359
|
if let Some(event_value) = crate::lifecycle::launch::provider_effort_event_if_dropped_json(
|
|
360
360
|
agent,
|
|
361
361
|
provider,
|
|
@@ -99,17 +99,17 @@ fn stale_snapshot_cannot_flip_status_or_diagnose_ok_readiness() {
|
|
|
99
99
|
let case = B0HideOneCase::new("status-diagnose-hide-one");
|
|
100
100
|
let root = case.current_state();
|
|
101
101
|
save_runtime_state(&case.workspace, &root).expect("seed current root state");
|
|
102
|
-
let
|
|
102
|
+
let tmux = brief_probe::install_native_tmux(
|
|
103
103
|
&case.workspace.join("probe"),
|
|
104
104
|
NEW_ENDPOINT,
|
|
105
105
|
SESSION,
|
|
106
106
|
WORKER,
|
|
107
107
|
"%new",
|
|
108
|
-
"
|
|
108
|
+
"bash",
|
|
109
109
|
);
|
|
110
110
|
let _path = case.env.with_env(
|
|
111
111
|
"PATH",
|
|
112
|
-
&brief_probe::path_with_probe(&
|
|
112
|
+
&brief_probe::path_with_probe(&tmux, std::env::var_os("PATH").as_deref()),
|
|
113
113
|
);
|
|
114
114
|
|
|
115
115
|
let status_before = case.run_json(&[
|
|
@@ -255,8 +255,8 @@ fn t3_unknown_business_status_is_preserved_without_normalized_event() {
|
|
|
255
255
|
let envelope: serde_json::Value =
|
|
256
256
|
serde_json::from_str(&row.envelope).expect("T3-1 durable envelope JSON");
|
|
257
257
|
assert_eq!(
|
|
258
|
-
row.status,
|
|
259
|
-
"T3-1 ingress:
|
|
258
|
+
row.status, "collected",
|
|
259
|
+
"T3-1 ingress: report_result auto-finalization must mark the durable row collected"
|
|
260
260
|
);
|
|
261
261
|
assert_eq!(
|
|
262
262
|
envelope["status"],
|
|
@@ -410,16 +410,6 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
410
410
|
"status should normalize teamdir to project-root runtime state: {status:?}"
|
|
411
411
|
);
|
|
412
412
|
|
|
413
|
-
let collect = crate::cli::cmd_collect(&crate::cli::CollectArgs {
|
|
414
|
-
result_file: None,
|
|
415
|
-
workspace: team.clone(),
|
|
416
|
-
json: true,
|
|
417
|
-
team: None,
|
|
418
|
-
});
|
|
419
|
-
assert!(
|
|
420
|
-
collect.is_ok(),
|
|
421
|
-
"collect should normalize teamdir to the same project-root state/spec: {collect:?}"
|
|
422
|
-
);
|
|
423
413
|
}
|
|
424
414
|
|
|
425
415
|
#[test]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
use super::launch_spawn::seed_healthy_coordinator;
|
|
2
2
|
use super::*;
|
|
3
3
|
use crate::cli::{
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
cmd_diagnose, cmd_send, cmd_status, lifecycle_port, CmdOutput, CmdResult, DiagnoseArgs,
|
|
5
|
+
SendArgs, StatusArgs,
|
|
6
6
|
};
|
|
7
7
|
use crate::transport::test_support::OfflineTransport;
|
|
8
8
|
use crate::transport::WindowName;
|
|
@@ -275,12 +275,6 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
275
275
|
to_name: None,
|
|
276
276
|
to_leader: None,
|
|
277
277
|
});
|
|
278
|
-
let collect = cmd_collect(&CollectArgs {
|
|
279
|
-
workspace: workspace.clone(),
|
|
280
|
-
result_file: None,
|
|
281
|
-
json: true,
|
|
282
|
-
team: Some(spec.team_key.to_string()),
|
|
283
|
-
});
|
|
284
278
|
let lifecycle_transport = codex_ready_transport()
|
|
285
279
|
.with_session_present(true)
|
|
286
280
|
.with_windows(vec![WindowName::new("w1"), WindowName::new("w2")]);
|
|
@@ -303,7 +297,6 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
303
297
|
{ "step": "status-json", "result": cmd_value(status_compact) },
|
|
304
298
|
{ "step": "status-detail-json", "result": cmd_value(status_detail) },
|
|
305
299
|
{ "step": "send", "result": cmd_value(send) },
|
|
306
|
-
{ "step": "collect", "result": cmd_value(collect) },
|
|
307
300
|
{ "step": "phase-lifecycle-op", "output": lifecycle },
|
|
308
301
|
{
|
|
309
302
|
"step": "shutdown-keep-logs",
|