@team-agent/installer 0.5.50 → 0.5.52
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 +5 -3
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +32 -14
- package/crates/team-agent/src/cli/named_address.rs +46 -115
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +44 -1169
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/lease.rs +202 -16
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
- package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
- package/crates/team-agent/src/lifecycle/restart.rs +26 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +14 -5
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +288 -183
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +15 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +101 -49
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +284 -21
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +94 -27
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/package.json +4 -4
|
@@ -3,6 +3,29 @@ use crate::transport::test_support::OfflineTransport;
|
|
|
3
3
|
use serde_json::json;
|
|
4
4
|
use serial_test::serial;
|
|
5
5
|
|
|
6
|
+
fn launch_source() -> String {
|
|
7
|
+
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/lifecycle");
|
|
8
|
+
let mut files = vec![src.join("launch.rs")];
|
|
9
|
+
let mut siblings = std::fs::read_dir(src.join("launch"))
|
|
10
|
+
.expect("read launch module directory")
|
|
11
|
+
.map(|entry| entry.expect("read launch module entry").path())
|
|
12
|
+
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("rs"))
|
|
13
|
+
.collect::<Vec<_>>();
|
|
14
|
+
siblings.sort();
|
|
15
|
+
files.extend(siblings);
|
|
16
|
+
|
|
17
|
+
files
|
|
18
|
+
.into_iter()
|
|
19
|
+
.map(|path| {
|
|
20
|
+
format!(
|
|
21
|
+
"\n// @source {}\n{}",
|
|
22
|
+
path.strip_prefix(&src).unwrap().display(),
|
|
23
|
+
std::fs::read_to_string(&path).expect("read launch source")
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
.collect()
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
#[allow(dead_code)]
|
|
7
30
|
struct HermeticTestEnv;
|
|
8
31
|
|
|
@@ -682,6 +705,7 @@ fn spine_add_agent_rejects_duplicate_agent_id() {
|
|
|
682
705
|
#[test]
|
|
683
706
|
fn e5_add_agent_does_not_copy_role_into_platform_dir_and_injects_into_spec() {
|
|
684
707
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
708
|
+
seed_canonical_team_identity(&team, "teamdir");
|
|
685
709
|
// External role file OUTSIDE team_dir/agents, with recognizable body content.
|
|
686
710
|
let role = team.parent().unwrap().join("w2-external-role.md");
|
|
687
711
|
let role_body = "---\nname: w2\nrole: Second Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nUNIQUE-E5-BUG1-MARKER body for w2.\n";
|
|
@@ -968,7 +992,7 @@ fn e5_add_agent_resolves_team_dir_to_role_dir_when_runtime_spec_exists() {
|
|
|
968
992
|
// never write team.spec.yaml into the user team_dir/agents_dir. Pins the spec-demote invariant.
|
|
969
993
|
#[test]
|
|
970
994
|
fn e5_guard_g1_writers_use_runtime_spec_path_not_user_dir() {
|
|
971
|
-
let source =
|
|
995
|
+
let source = launch_source();
|
|
972
996
|
let runtime_writes = source.matches("runtime_spec_path(").count();
|
|
973
997
|
assert!(
|
|
974
998
|
runtime_writes >= 2,
|
|
@@ -989,7 +1013,7 @@ fn e5_guard_g1_writers_use_runtime_spec_path_not_user_dir() {
|
|
|
989
1013
|
// Pins the Bug1 fix: no `fs::copy` of a role file + no `materialize_added_role_file` reborn.
|
|
990
1014
|
#[test]
|
|
991
1015
|
fn e5_guard_g2_no_copy_role_into_platform_dir() {
|
|
992
|
-
let source =
|
|
1016
|
+
let source = launch_source();
|
|
993
1017
|
assert!(
|
|
994
1018
|
!source.contains("materialize_added_role_file"),
|
|
995
1019
|
"G2: materialize_added_role_file (role copy anti-pattern) must stay deleted"
|
|
@@ -1131,6 +1155,12 @@ pub(super) fn seed_healthy_coordinator(workspace: &std::path::Path) {
|
|
|
1131
1155
|
.unwrap();
|
|
1132
1156
|
}
|
|
1133
1157
|
|
|
1158
|
+
fn seed_canonical_team_identity(workspace: &std::path::Path, team_key: &str) {
|
|
1159
|
+
let mut state = crate::state::persist::load_runtime_state(workspace).unwrap();
|
|
1160
|
+
state["active_team_key"] = json!(team_key);
|
|
1161
|
+
crate::state::persist::save_runtime_state(workspace, &state).unwrap();
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1134
1164
|
// RED — quick_start_with_transport must drive the REAL spawn path: launch.dry_run==false, started
|
|
1135
1165
|
// non-empty (one per compiled agent), and the transport records >=1 spawn carrying that agent's
|
|
1136
1166
|
// provider build_command. Today the dry_run=true bug -> launch.started empty + ZERO spawns recorded ->
|
|
@@ -2240,6 +2270,7 @@ fn add_agent_with_transport_spawns_new_worker_not_stub() {
|
|
|
2240
2270
|
let role_file = team.join("worker2-role.md"); // OUTSIDE agents/ -> not a duplicate of an existing agent
|
|
2241
2271
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
2242
2272
|
seed_healthy_coordinator(&team);
|
|
2273
|
+
seed_canonical_team_identity(&team, "addteam");
|
|
2243
2274
|
let transport = OfflineTransport::new();
|
|
2244
2275
|
|
|
2245
2276
|
let _result = add_agent_with_transport(
|
|
@@ -3117,6 +3148,36 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
3117
3148
|
assert_eq!(spawn_event["spawned_at"], json!(FIXED_SPAWNED_AT));
|
|
3118
3149
|
assert_eq!(spawn_event["source"], json!("launch"));
|
|
3119
3150
|
assert_eq!(spawn_event["spawn_epoch"], json!(0));
|
|
3151
|
+
let actual_keys = spawn_event
|
|
3152
|
+
.as_object()
|
|
3153
|
+
.expect("spawn event object")
|
|
3154
|
+
.keys()
|
|
3155
|
+
.map(String::as_str)
|
|
3156
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
3157
|
+
let expected_keys = [
|
|
3158
|
+
"agent_id",
|
|
3159
|
+
"argv",
|
|
3160
|
+
"env_overlay_keys",
|
|
3161
|
+
"env_unset",
|
|
3162
|
+
"event",
|
|
3163
|
+
"expected_session_id",
|
|
3164
|
+
"provider",
|
|
3165
|
+
"session_id_in_argv",
|
|
3166
|
+
"source",
|
|
3167
|
+
"spawn_cwd",
|
|
3168
|
+
"spawn_epoch",
|
|
3169
|
+
"spawned_at",
|
|
3170
|
+
"tmux_endpoint",
|
|
3171
|
+
"tmux_endpoint_source",
|
|
3172
|
+
"tmux_start_mode",
|
|
3173
|
+
"ts",
|
|
3174
|
+
]
|
|
3175
|
+
.into_iter()
|
|
3176
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
3177
|
+
assert_eq!(
|
|
3178
|
+
actual_keys, expected_keys,
|
|
3179
|
+
"spawn event schema must be canonical"
|
|
3180
|
+
);
|
|
3120
3181
|
}
|
|
3121
3182
|
|
|
3122
3183
|
// Stage B2 — golden launch/core.py:171-173 writes paused workers as exactly
|
|
@@ -3278,6 +3339,7 @@ fn add_agent_reachability_gate_returns_error_when_spawn_pane_not_addressable() {
|
|
|
3278
3339
|
let role_file = team.join("worker2-role.md");
|
|
3279
3340
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
3280
3341
|
seed_healthy_coordinator(&team);
|
|
3342
|
+
seed_canonical_team_identity(&team, "reachgate");
|
|
3281
3343
|
|
|
3282
3344
|
// Models "spawn to wrong socket" / "spawn succeeded but pane orphan": the
|
|
3283
3345
|
// OfflineTransport records the spawn but reports the new pane is NOT
|
|
@@ -3337,6 +3399,7 @@ fn add_agent_reachability_gate_passes_when_spawn_pane_addressable() {
|
|
|
3337
3399
|
let role_file = team.join("worker2-role.md");
|
|
3338
3400
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
3339
3401
|
seed_healthy_coordinator(&team);
|
|
3402
|
+
seed_canonical_team_identity(&team, "gatepass");
|
|
3340
3403
|
|
|
3341
3404
|
// Default OfflineTransport: spawned_panes_addressable=true, liveness=Unknown
|
|
3342
3405
|
// (treated as not-Dead by the gate's fallthrough).
|
|
@@ -4096,6 +4159,7 @@ fn e42_re_add_and_remove_agree_on_existence_after_add() {
|
|
|
4096
4159
|
let role_file = team.join("newagent.md");
|
|
4097
4160
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
4098
4161
|
seed_healthy_coordinator(&team);
|
|
4162
|
+
seed_canonical_team_identity(&team, "e42sym");
|
|
4099
4163
|
let transport = OfflineTransport::new();
|
|
4100
4164
|
// (1) first add
|
|
4101
4165
|
let r1 = crate::lifecycle::add_agent_with_transport(
|
|
@@ -4152,6 +4216,7 @@ fn e42_add_writes_to_canonical_runtime_spec_and_state_consistently() {
|
|
|
4152
4216
|
let role_file = team.join("newagent.md");
|
|
4153
4217
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
4154
4218
|
seed_healthy_coordinator(&team);
|
|
4219
|
+
seed_canonical_team_identity(&team, "e42proj");
|
|
4155
4220
|
let transport = OfflineTransport::new();
|
|
4156
4221
|
let _ = crate::lifecycle::add_agent_with_transport(
|
|
4157
4222
|
&team,
|
|
@@ -233,7 +233,7 @@ fn r2_lifecycle_lock_exists_precondition() {
|
|
|
233
233
|
"remove rollback runs while remove-agent holds the lifecycle lock; it must use lock-free start_agent_at_paths"
|
|
234
234
|
);
|
|
235
235
|
|
|
236
|
-
let launch =
|
|
236
|
+
let launch = launch_source();
|
|
237
237
|
assert_public_operation(&launch, "add-agent");
|
|
238
238
|
assert_public_operation(&launch, "fork-agent");
|
|
239
239
|
assert_body_is_unlocked(&launch, "fn add_agent_with_transport_at_paths");
|
|
@@ -279,6 +279,29 @@ fn read_src(path: &str) -> String {
|
|
|
279
279
|
std::fs::read_to_string(manifest_src().join(path)).unwrap()
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
fn launch_source() -> String {
|
|
283
|
+
let src = manifest_src().join("lifecycle");
|
|
284
|
+
let mut files = vec![src.join("launch.rs")];
|
|
285
|
+
let mut siblings = std::fs::read_dir(src.join("launch"))
|
|
286
|
+
.expect("read launch module directory")
|
|
287
|
+
.map(|entry| entry.expect("read launch module entry").path())
|
|
288
|
+
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("rs"))
|
|
289
|
+
.collect::<Vec<_>>();
|
|
290
|
+
siblings.sort();
|
|
291
|
+
files.extend(siblings);
|
|
292
|
+
|
|
293
|
+
files
|
|
294
|
+
.into_iter()
|
|
295
|
+
.map(|path| {
|
|
296
|
+
format!(
|
|
297
|
+
"\n// @source {}\n{}",
|
|
298
|
+
path.strip_prefix(&src).unwrap().display(),
|
|
299
|
+
std::fs::read_to_string(&path).expect("read launch source")
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
.collect()
|
|
303
|
+
}
|
|
304
|
+
|
|
282
305
|
fn source_files(root: &Path) -> Vec<PathBuf> {
|
|
283
306
|
let mut out = Vec::new();
|
|
284
307
|
collect_rs_files(root, &mut out);
|
|
@@ -252,12 +252,28 @@ fn state_spec_workspace_from_entry(state: &Value) -> Option<PathBuf> {
|
|
|
252
252
|
|
|
253
253
|
fn load_local_runtime_state(workspace: &Path) -> Result<Value, super::super::ToolError> {
|
|
254
254
|
let path = crate::state::persist::runtime_state_path(workspace);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
tool_runtime_error(format!(
|
|
260
|
-
|
|
255
|
+
match crate::state::repository::StateRepository::new(workspace)
|
|
256
|
+
.load_workspace_if_exists_without_migrations()
|
|
257
|
+
{
|
|
258
|
+
Ok(Some(state)) => Ok(state),
|
|
259
|
+
Ok(None) => Err(tool_runtime_error(format!(
|
|
260
|
+
"read local runtime state {}: {}",
|
|
261
|
+
path.display(),
|
|
262
|
+
std::io::Error::from(std::io::ErrorKind::NotFound)
|
|
263
|
+
))),
|
|
264
|
+
Err(crate::state::StateError::Json(error)) => Err(tool_runtime_error(format!(
|
|
265
|
+
"parse local runtime state {}: {error}",
|
|
266
|
+
path.display()
|
|
267
|
+
))),
|
|
268
|
+
Err(crate::state::StateError::Io(error)) => Err(tool_runtime_error(format!(
|
|
269
|
+
"read local runtime state {}: {error}",
|
|
270
|
+
path.display()
|
|
271
|
+
))),
|
|
272
|
+
Err(error) => Err(tool_runtime_error(format!(
|
|
273
|
+
"read local runtime state {}: {error}",
|
|
274
|
+
path.display()
|
|
275
|
+
))),
|
|
276
|
+
}
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
fn materialize_mcp_lifecycle_spec(
|
|
@@ -416,7 +432,14 @@ fn prepare_selected_team_state(
|
|
|
416
432
|
}
|
|
417
433
|
}
|
|
418
434
|
}
|
|
419
|
-
crate::state::
|
|
435
|
+
crate::state::repository::StateRepository::new(workspace)
|
|
436
|
+
.save(
|
|
437
|
+
crate::state::repository::StateWriteIntent::McpLifecycleAgentOps {
|
|
438
|
+
team_key: Some(team),
|
|
439
|
+
},
|
|
440
|
+
state,
|
|
441
|
+
)
|
|
442
|
+
.map_err(|e| {
|
|
420
443
|
tool_runtime_error(format!(
|
|
421
444
|
"save MCP lifecycle scoped state {}: {e}",
|
|
422
445
|
workspace.display()
|
|
@@ -26,8 +26,10 @@ pub(crate) fn update_state(
|
|
|
26
26
|
let mut state = selected.state;
|
|
27
27
|
ensure_object(&mut state);
|
|
28
28
|
append_note(&mut state, note);
|
|
29
|
-
crate::state::
|
|
30
|
-
|
|
29
|
+
crate::state::repository::StateRepository::new(&selected.run_workspace).save_reapplying(
|
|
30
|
+
crate::state::repository::StateWriteIntent::McpUpdateStateNote {
|
|
31
|
+
team_key: Some(&selected.team_key),
|
|
32
|
+
},
|
|
31
33
|
&state,
|
|
32
34
|
|latest| {
|
|
33
35
|
ensure_object(latest);
|
|
@@ -66,8 +68,10 @@ fn update_state_without_spec(
|
|
|
66
68
|
ensure_object(&mut state);
|
|
67
69
|
seed_legacy_team_key(&mut state, &selected.run_workspace, &selected.team_key);
|
|
68
70
|
append_note(&mut state, note);
|
|
69
|
-
crate::state::
|
|
70
|
-
|
|
71
|
+
crate::state::repository::StateRepository::new(&selected.run_workspace).save_reapplying(
|
|
72
|
+
crate::state::repository::StateWriteIntent::McpUpdateStateNote {
|
|
73
|
+
team_key: Some(&selected.team_key),
|
|
74
|
+
},
|
|
71
75
|
&state,
|
|
72
76
|
|latest| {
|
|
73
77
|
ensure_object(latest);
|
|
@@ -85,7 +85,7 @@ use crate::event_log::EventLog;
|
|
|
85
85
|
use crate::message_store::MessageStore;
|
|
86
86
|
|
|
87
87
|
// ── REUSE: step 5 state persist / projection ────────────────────────────────
|
|
88
|
-
use crate::state::persist::
|
|
88
|
+
use crate::state::persist::load_runtime_state;
|
|
89
89
|
|
|
90
90
|
// ── REUSE: step 11 messaging delegate surface ───────────────────────────────
|
|
91
91
|
use crate::messaging::{self, DeliveryOutcome, MessageTarget, SendOptions, TrustedSender};
|
|
@@ -319,20 +319,16 @@ fn send_message_worker_recipient_surfaces_dead_coordinator_warning() {
|
|
|
319
319
|
None,
|
|
320
320
|
None,
|
|
321
321
|
)
|
|
322
|
-
.expect("send
|
|
322
|
+
.expect("send persists a durable blocker, not an MCP error");
|
|
323
323
|
let v = outcome.to_value();
|
|
324
|
-
assert_eq!(v.get("status"), Some(&json!("
|
|
325
|
-
assert_eq!(v.get("reason"), Some(&json!("coordinator_unavailable")));
|
|
324
|
+
assert_eq!(v.get("status"), Some(&json!("accepted")));
|
|
326
325
|
assert!(
|
|
327
|
-
v.get("
|
|
326
|
+
v.get("message_id")
|
|
328
327
|
.and_then(Value::as_str)
|
|
329
|
-
.is_some_and(|
|
|
330
|
-
"
|
|
331
|
-
);
|
|
332
|
-
assert!(
|
|
333
|
-
v.get("delivery_pending").is_none(),
|
|
334
|
-
"dead coordinator must not return the old accepted async envelope"
|
|
328
|
+
.is_some_and(|id| id.starts_with("msg_")),
|
|
329
|
+
"worker MCP send must expose the durable row id; value={v}"
|
|
335
330
|
);
|
|
331
|
+
assert_eq!(v.get("delivery_pending"), Some(&json!(true)));
|
|
336
332
|
}
|
|
337
333
|
|
|
338
334
|
#[test]
|
|
@@ -311,8 +311,25 @@
|
|
|
311
311
|
// ════════════════════════════════════════════════════════════════════════
|
|
312
312
|
#[test]
|
|
313
313
|
fn update_state_returns_ok_and_state_file_path() {
|
|
314
|
+
let ws = seed_state_ws(
|
|
315
|
+
"update-state",
|
|
316
|
+
&json!({
|
|
317
|
+
"active_team_key": "teamA",
|
|
318
|
+
"team_key": "teamA",
|
|
319
|
+
"session_name": "teamA",
|
|
320
|
+
"status": "alive",
|
|
321
|
+
"teams": {
|
|
322
|
+
"teamA": {
|
|
323
|
+
"team_key": "teamA",
|
|
324
|
+
"session_name": "teamA",
|
|
325
|
+
"status": "alive",
|
|
326
|
+
"agents": {}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
314
331
|
let tools = TeamOrchestratorTools::with_identity(
|
|
315
|
-
&
|
|
332
|
+
&ws,
|
|
316
333
|
Some(AgentId::new("leader")),
|
|
317
334
|
Some(TeamKey::new("teamA")),
|
|
318
335
|
);
|
|
@@ -22,12 +22,19 @@ fn keys(v: &Value) -> Vec<String> {
|
|
|
22
22
|
/// `/tmp/ws`, or they flake under parallel cargo (sqlite "database is locked" / NotFound).
|
|
23
23
|
/// Pure-function / dispatch-shape tests that never touch fs/db keep a dummy fixed path.
|
|
24
24
|
fn unique_ws(tag: &str) -> std::path::PathBuf {
|
|
25
|
+
use std::io::ErrorKind;
|
|
25
26
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
26
27
|
static N: AtomicU64 = AtomicU64::new(0);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
loop {
|
|
29
|
+
let n = N.fetch_add(1, Ordering::Relaxed);
|
|
30
|
+
let p =
|
|
31
|
+
std::env::temp_dir().join(format!("ta-rs-mcp-{tag}-{}-{n}", std::process::id()));
|
|
32
|
+
match std::fs::create_dir(&p) {
|
|
33
|
+
Ok(()) => return p,
|
|
34
|
+
Err(error) if error.kind() == ErrorKind::AlreadyExists => {}
|
|
35
|
+
Err(error) => panic!("create unique workspace {}: {error}", p.display()),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
include!("tests/normalize.rs");
|
|
@@ -126,11 +126,19 @@ impl TeamOrchestratorTools {
|
|
|
126
126
|
.map(|team| team.as_str().to_string())
|
|
127
127
|
.or_else(|| assignment_team_key(&state));
|
|
128
128
|
reconcile_assigned_task(&mut state, team_key.as_deref(), &task_value);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
crate::state::repository::StateRepository::new(&self.workspace)
|
|
130
|
+
.save_reapplying(
|
|
131
|
+
crate::state::repository::StateWriteIntent::McpAssignTask {
|
|
132
|
+
team_key: team_key.as_deref(),
|
|
133
|
+
task_id,
|
|
134
|
+
},
|
|
135
|
+
&state,
|
|
136
|
+
|latest| {
|
|
137
|
+
ensure_object(latest);
|
|
138
|
+
let latest_team_key = team_key.clone().or_else(|| assignment_team_key(latest));
|
|
139
|
+
reconcile_assigned_task(latest, latest_team_key.as_deref(), &task_value);
|
|
140
|
+
},
|
|
141
|
+
)
|
|
134
142
|
.map_err(tool_runtime_error)?;
|
|
135
143
|
|
|
136
144
|
let content = assignment_message(task, message);
|
|
@@ -213,6 +221,7 @@ impl TeamOrchestratorTools {
|
|
|
213
221
|
)
|
|
214
222
|
.map_err(tool_runtime_error)?;
|
|
215
223
|
let opts = SendOptions {
|
|
224
|
+
origin: crate::messaging::SendOrigin::Mcp,
|
|
216
225
|
task_id: task_id.map(TaskId::new),
|
|
217
226
|
route_task_id: true,
|
|
218
227
|
sender,
|
|
@@ -80,8 +80,10 @@ pub fn detect_idle_fallbacks(
|
|
|
80
80
|
let suppression_snapshots =
|
|
81
81
|
idle_fallback_suppression_snapshots(&next_state, store, &team, &idle_workers)?;
|
|
82
82
|
register_idle_fallback_suppression(&mut next_state, &team, &now, &suppression_snapshots);
|
|
83
|
-
crate::state::
|
|
84
|
-
|
|
83
|
+
crate::state::repository::StateRepository::new(workspace).save_reapplying(
|
|
84
|
+
crate::state::repository::StateWriteIntent::MessagingTurnArm {
|
|
85
|
+
owner_team_id: Some(&team),
|
|
86
|
+
},
|
|
85
87
|
&next_state,
|
|
86
88
|
|latest| {
|
|
87
89
|
register_idle_fallback_suppression(latest, &team, &now, &suppression_snapshots);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//! Pure logical-address grammar shared by public entry-point adapters.
|
|
2
|
+
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
6
|
+
pub struct LogicalAddress {
|
|
7
|
+
pub workspace: Option<PathBuf>,
|
|
8
|
+
pub target: LogicalAddressTarget,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
12
|
+
pub enum LogicalAddressTarget {
|
|
13
|
+
Worker(String),
|
|
14
|
+
TeamEntity { team: String, entity: String },
|
|
15
|
+
SessionWindow { session: String, window: String },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn parse_logical_address(raw_name: &str) -> Result<LogicalAddress, &'static str> {
|
|
19
|
+
let raw = raw_name.trim();
|
|
20
|
+
if raw.is_empty() {
|
|
21
|
+
return Err("name is empty");
|
|
22
|
+
}
|
|
23
|
+
let (workspace, name) = if let Some((workspace, rest)) = raw.split_once("::") {
|
|
24
|
+
if workspace.trim().is_empty() || rest.trim().is_empty() {
|
|
25
|
+
return Err("workspace-qualified name must include workspace and target");
|
|
26
|
+
}
|
|
27
|
+
(Some(PathBuf::from(workspace)), rest.trim())
|
|
28
|
+
} else {
|
|
29
|
+
(None, raw)
|
|
30
|
+
};
|
|
31
|
+
if name.contains("//") {
|
|
32
|
+
return Err("name contains an empty path segment");
|
|
33
|
+
}
|
|
34
|
+
let target = if name.contains('/') {
|
|
35
|
+
let parts = name.split('/').collect::<Vec<_>>();
|
|
36
|
+
if parts.len() != 2 || !valid_component(parts[0]) || !valid_component(parts[1]) {
|
|
37
|
+
return Err("expected <team>/<agent> or <team>/leader");
|
|
38
|
+
}
|
|
39
|
+
LogicalAddressTarget::TeamEntity {
|
|
40
|
+
team: parts[0].to_string(),
|
|
41
|
+
entity: parts[1].to_string(),
|
|
42
|
+
}
|
|
43
|
+
} else if name.contains(':') {
|
|
44
|
+
let parts = name.split(':').collect::<Vec<_>>();
|
|
45
|
+
if parts.len() != 2 || parts[0].trim().is_empty() || parts[1].trim().is_empty() {
|
|
46
|
+
return Err("expected <session>:<window>");
|
|
47
|
+
}
|
|
48
|
+
LogicalAddressTarget::SessionWindow {
|
|
49
|
+
session: parts[0].to_string(),
|
|
50
|
+
window: parts[1].to_string(),
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
if !valid_component(name) {
|
|
54
|
+
return Err("expected a non-empty agent id");
|
|
55
|
+
}
|
|
56
|
+
LogicalAddressTarget::Worker(name.to_string())
|
|
57
|
+
};
|
|
58
|
+
Ok(LogicalAddress { workspace, target })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn valid_component(raw: &str) -> bool {
|
|
62
|
+
!raw.trim().is_empty()
|
|
63
|
+
&& !raw.contains(char::is_whitespace)
|
|
64
|
+
&& !raw.contains('/')
|
|
65
|
+
&& !raw.contains(':')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[cfg(test)]
|
|
69
|
+
mod tests {
|
|
70
|
+
use super::*;
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn parses_canonical_send_grammar_without_runtime_state() {
|
|
74
|
+
assert_eq!(
|
|
75
|
+
parse_logical_address("/tmp/ws::team/w1").unwrap(),
|
|
76
|
+
LogicalAddress {
|
|
77
|
+
workspace: Some(PathBuf::from("/tmp/ws")),
|
|
78
|
+
target: LogicalAddressTarget::TeamEntity {
|
|
79
|
+
team: "team".into(),
|
|
80
|
+
entity: "w1".into(),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
assert!(parse_logical_address("team//w1").is_err());
|
|
85
|
+
}
|
|
86
|
+
}
|