@team-agent/installer 0.5.100 → 0.5.102
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/diagnose.rs +29 -7
- package/crates/team-agent/src/cli/mod.rs +24 -8
- package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
- package/crates/team-agent/src/compiler/tests.rs +20 -23
- package/crates/team-agent/src/compiler.rs +52 -65
- package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
- package/crates/team-agent/src/leader/start.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +11 -3
- package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +115 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +8 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +10 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
- package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
- package/crates/team-agent/src/mcp_server/mod.rs +7 -12
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
- package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
- package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
- package/crates/team-agent/src/mcp_server/tests.rs +4 -200
- package/crates/team-agent/src/mcp_server/tools.rs +5 -396
- package/crates/team-agent/src/mcp_server/types.rs +3 -36
- package/crates/team-agent/src/mcp_server/wire.rs +2 -223
- package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/scheduler.rs +2 -205
- package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
- package/crates/team-agent/src/messaging/types.rs +0 -20
- package/crates/team-agent/src/model/spec.rs +15 -18
- package/crates/team-agent/src/provider/types.rs +2 -0
- package/crates/team-agent/src/state/persist.rs +58 -2
- package/crates/team-agent/src/state/projection.rs +5 -1
- package/crates/team-agent/src/state/repository/tests.rs +1 -16
- package/crates/team-agent/src/state/repository.rs +126 -37
- package/crates/team-agent/src/transport/test_support.rs +23 -3
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +18 -47
- package/skills/team-agent/command-coverage.json +7 -0
- package/skills/team-agent/references/team-agent-operator.md +28 -118
- package/skills/team-agent/references/team-in-team.md +4 -19
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
|
@@ -979,32 +979,6 @@ fn requeue_delivery_exhausted_watchers_reopens_only_exhausted() {
|
|
|
979
979
|
);
|
|
980
980
|
}
|
|
981
981
|
|
|
982
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
983
|
-
// GROUP P — stuck_cancel owner-gate + invalid alert type refusal.
|
|
984
|
-
// scheduler.py:262-294.
|
|
985
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
986
|
-
|
|
987
|
-
#[test]
|
|
988
|
-
fn stuck_cancel_none_alert_type_expands_to_all() {
|
|
989
|
-
// alert_type None == Python "all" → sorted(_ALERT_TYPES) expansion.
|
|
990
|
-
let ws = tmp_ws("stuckcancel");
|
|
991
|
-
let out = stuck_cancel(&ws, "w1", None, "leader").unwrap();
|
|
992
|
-
// The suppression result must enumerate all three alert types.
|
|
993
|
-
let types = out.get("alert_types").and_then(|v| v.as_array()).map(|a| {
|
|
994
|
-
a.iter()
|
|
995
|
-
.filter_map(|x| x.as_str().map(str::to_string))
|
|
996
|
-
.collect::<Vec<_>>()
|
|
997
|
-
});
|
|
998
|
-
assert_eq!(
|
|
999
|
-
types,
|
|
1000
|
-
Some(vec![
|
|
1001
|
-
"cross_worker_deadlock".to_string(),
|
|
1002
|
-
"idle_fallback".to_string(),
|
|
1003
|
-
"stuck".to_string()
|
|
1004
|
-
])
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
982
|
// ════════════════════════════════════════════════════════════════════════
|
|
1009
983
|
// GROUP Q — collect intake (results.py:45-167): valid result advances task,
|
|
1010
984
|
// returns collected_results + delivered_messages + results counts shape.
|
|
@@ -219,26 +219,6 @@ impl WorkerRuntimeState {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
/// 告警类型 (card §49;`scheduler.py:38` `_ALERT_TYPES`)。`stuck_cancel` 还接 `all` (展开全集)。
|
|
223
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
224
|
-
#[serde(rename_all = "snake_case")]
|
|
225
|
-
pub enum AlertType {
|
|
226
|
-
Stuck,
|
|
227
|
-
IdleFallback,
|
|
228
|
-
CrossWorkerDeadlock,
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
impl AlertType {
|
|
232
|
-
/// `stuck_cancel(alert_type="all")` → `sorted(_ALERT_TYPES)` 全集 (`scheduler.py:269`)。
|
|
233
|
-
pub fn all() -> [AlertType; 3] {
|
|
234
|
-
[
|
|
235
|
-
AlertType::CrossWorkerDeadlock,
|
|
236
|
-
AlertType::IdleFallback,
|
|
237
|
-
AlertType::Stuck,
|
|
238
|
-
]
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
222
|
/// selftest check 状态 (card §47;`diagnose/comms.py`)。§19 必变 enum。
|
|
243
223
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
244
224
|
#[serde(rename_all = "snake_case")]
|
|
@@ -406,6 +406,7 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
|
406
406
|
];
|
|
407
407
|
let allowed = &[
|
|
408
408
|
"id",
|
|
409
|
+
"label",
|
|
409
410
|
"role",
|
|
410
411
|
"provider",
|
|
411
412
|
"model",
|
|
@@ -1134,19 +1135,10 @@ mod tests {
|
|
|
1134
1135
|
);
|
|
1135
1136
|
}
|
|
1136
1137
|
|
|
1137
|
-
// 0.5.66 bypass 单源 §4.1:7
|
|
1138
|
-
//
|
|
1139
|
-
// compile 级校验确实 fail-loud,即各触发面(经 compile_team)都被覆盖。
|
|
1138
|
+
// 0.5.66 bypass 单源 §4.1:7 个生命周期入口共享编译后的 spec。
|
|
1139
|
+
// 缺失 role frontmatter 值时由编译器注入 true;此处锁定公共编译入口的默认结果。
|
|
1140
1140
|
#[test]
|
|
1141
|
-
fn
|
|
1142
|
-
// compile_team 是 7 触发面的 spec 校验必经点:
|
|
1143
|
-
// quick-start(quick_start.rs:233 compile_team)
|
|
1144
|
-
// restart(rebuild.rs:3245 rebuild_runtime_spec_from_roles → compile_team)
|
|
1145
|
-
// add-agent(add_agent.rs:331 compile_role_agent)
|
|
1146
|
-
// clone-agent(clone_agent.rs:62 → add_agent)
|
|
1147
|
-
// fork-agent(fork_agent.rs:96 validate_spec)
|
|
1148
|
-
// start-agent / reset-agent(操作已编译 spec,团队创建时经 compile_team 校验)
|
|
1149
|
-
// 验证 compile_team 对缺字段的角色文档 fail-loud。
|
|
1141
|
+
fn test_all_7_triggers_use_compiled_default_bypass() {
|
|
1150
1142
|
let team = std::env::temp_dir().join(format!(
|
|
1151
1143
|
"ta-spec-7trigger-{}",
|
|
1152
1144
|
std::process::id()
|
|
@@ -1158,17 +1150,22 @@ mod tests {
|
|
|
1158
1150
|
"---\nname: t7\nobjective: 7 trigger.\nprovider: codex\n---\n\nteam.\n",
|
|
1159
1151
|
)
|
|
1160
1152
|
.unwrap();
|
|
1161
|
-
//
|
|
1153
|
+
// 角色文档故意省略该字段;纯文本默认必须写入 compiled spec。
|
|
1162
1154
|
std::fs::write(
|
|
1163
1155
|
team.join("agents").join("w.md"),
|
|
1164
1156
|
"---\nname: w\nrole: Worker\nprovider: codex\ntools:\n - mcp_team\n---\n\nw.\n",
|
|
1165
1157
|
)
|
|
1166
1158
|
.unwrap();
|
|
1167
|
-
let
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1159
|
+
let spec = crate::compiler::compile_team(&team)
|
|
1160
|
+
.expect("compile_team must materialize the default bypass value");
|
|
1161
|
+
let agent = spec
|
|
1162
|
+
.get("agents")
|
|
1163
|
+
.and_then(Yaml::as_list)
|
|
1164
|
+
.and_then(|agents| agents.first())
|
|
1165
|
+
.expect("compiled spec has a worker");
|
|
1166
|
+
assert_eq!(
|
|
1167
|
+
agent.get("dangerously_skip_permissions"),
|
|
1168
|
+
Some(&Yaml::Bool(true))
|
|
1172
1169
|
);
|
|
1173
1170
|
let _ = std::fs::remove_dir_all(&team);
|
|
1174
1171
|
}
|
|
@@ -232,6 +232,7 @@ pub fn save_runtime_state(workspace: &Path, state: &Value) -> Result<(), StateEr
|
|
|
232
232
|
&[],
|
|
233
233
|
None,
|
|
234
234
|
None,
|
|
235
|
+
false,
|
|
235
236
|
)
|
|
236
237
|
}
|
|
237
238
|
|
|
@@ -251,6 +252,7 @@ pub(crate) fn save_runtime_state_with_receiver_authority(
|
|
|
251
252
|
&[],
|
|
252
253
|
Some(receiver_team_key),
|
|
253
254
|
exact_owner_seed_to_clear,
|
|
255
|
+
false,
|
|
254
256
|
)
|
|
255
257
|
}
|
|
256
258
|
|
|
@@ -289,6 +291,7 @@ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority(
|
|
|
289
291
|
agent_ids,
|
|
290
292
|
None,
|
|
291
293
|
None,
|
|
294
|
+
false,
|
|
292
295
|
)
|
|
293
296
|
}
|
|
294
297
|
|
|
@@ -309,6 +312,7 @@ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority_and_capture_b
|
|
|
309
312
|
topology_agent_ids,
|
|
310
313
|
None,
|
|
311
314
|
None,
|
|
315
|
+
false,
|
|
312
316
|
)
|
|
313
317
|
}
|
|
314
318
|
|
|
@@ -327,6 +331,7 @@ pub(crate) fn save_runtime_state_with_deleted_agents(
|
|
|
327
331
|
&[],
|
|
328
332
|
None,
|
|
329
333
|
None,
|
|
334
|
+
true,
|
|
330
335
|
)
|
|
331
336
|
}
|
|
332
337
|
|
|
@@ -346,6 +351,7 @@ pub(crate) fn save_runtime_state_with_team_tombstoned_agents(
|
|
|
346
351
|
&[],
|
|
347
352
|
None,
|
|
348
353
|
None,
|
|
354
|
+
false,
|
|
349
355
|
)
|
|
350
356
|
}
|
|
351
357
|
|
|
@@ -365,6 +371,7 @@ pub(crate) fn save_runtime_state_with_team_tombstone_lifecycle_topology_authorit
|
|
|
365
371
|
agent_ids,
|
|
366
372
|
None,
|
|
367
373
|
None,
|
|
374
|
+
false,
|
|
368
375
|
)
|
|
369
376
|
}
|
|
370
377
|
|
|
@@ -378,6 +385,7 @@ fn save_runtime_state_with_merge_options(
|
|
|
378
385
|
topology_update_agent_ids: &[&str],
|
|
379
386
|
receiver_update_team_key: Option<&str>,
|
|
380
387
|
exact_owner_seed_to_clear: Option<&Value>,
|
|
388
|
+
preserve_latest_fork_targets: bool,
|
|
381
389
|
) -> Result<(), StateError> {
|
|
382
390
|
persist_runtime_state_with_merge_options_and_expected(
|
|
383
391
|
workspace,
|
|
@@ -392,6 +400,7 @@ fn save_runtime_state_with_merge_options(
|
|
|
392
400
|
None,
|
|
393
401
|
None,
|
|
394
402
|
None,
|
|
403
|
+
preserve_latest_fork_targets,
|
|
395
404
|
)
|
|
396
405
|
}
|
|
397
406
|
|
|
@@ -415,6 +424,7 @@ pub(crate) fn save_runtime_state_with_receiver_authority_and_expected(
|
|
|
415
424
|
Some((expected_owner, expected_receiver)),
|
|
416
425
|
None,
|
|
417
426
|
None,
|
|
427
|
+
false,
|
|
418
428
|
)
|
|
419
429
|
}
|
|
420
430
|
|
|
@@ -438,6 +448,7 @@ pub(crate) fn save_runtime_state_with_exact_owner_receiver_cleanup(
|
|
|
438
448
|
None,
|
|
439
449
|
Some((team_key, expected_owner, expected_receiver)),
|
|
440
450
|
None,
|
|
451
|
+
false,
|
|
441
452
|
)
|
|
442
453
|
}
|
|
443
454
|
|
|
@@ -469,6 +480,7 @@ pub(crate) fn save_runtime_state_with_exact_owner_receiver_restore(
|
|
|
469
480
|
previous_owner,
|
|
470
481
|
previous_receiver,
|
|
471
482
|
)),
|
|
483
|
+
false,
|
|
472
484
|
)
|
|
473
485
|
}
|
|
474
486
|
|
|
@@ -485,6 +497,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
485
497
|
expected_owner_receiver: Option<(&Value, &Value)>,
|
|
486
498
|
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
487
499
|
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
500
|
+
preserve_latest_fork_targets: bool,
|
|
488
501
|
) -> Result<(), StateError> {
|
|
489
502
|
let path = runtime_state_path(workspace);
|
|
490
503
|
// Python `state.py:497`:先对入参 state 跑 `_migrate_state_identity`(就地填缺失 leader uuid)。
|
|
@@ -585,7 +598,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
585
598
|
.filter(|team_key| !team_key.is_empty())
|
|
586
599
|
.map(str::to_string)
|
|
587
600
|
.collect::<BTreeSet<_>>();
|
|
588
|
-
|
|
601
|
+
apply_persist_merge_contract_with_fork_target_preservation(
|
|
589
602
|
&mut migrated,
|
|
590
603
|
latest,
|
|
591
604
|
&deleted,
|
|
@@ -596,6 +609,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
596
609
|
receiver_update_team_key.zip(exact_owner_seed_to_clear),
|
|
597
610
|
exact_owner_receiver_to_clear,
|
|
598
611
|
exact_owner_receiver_to_restore,
|
|
612
|
+
preserve_latest_fork_targets,
|
|
599
613
|
)?;
|
|
600
614
|
}
|
|
601
615
|
super::abnormal_watch::compact_after_merge(&mut migrated, latest.as_ref(), deleted_agent_ids);
|
|
@@ -707,6 +721,34 @@ fn apply_persist_merge_contract(
|
|
|
707
721
|
exact_owner_seed_to_clear: Option<(&str, &Value)>,
|
|
708
722
|
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
709
723
|
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
724
|
+
) -> Result<(), StateError> {
|
|
725
|
+
apply_persist_merge_contract_with_fork_target_preservation(
|
|
726
|
+
incoming,
|
|
727
|
+
latest,
|
|
728
|
+
deleted_agent_ids,
|
|
729
|
+
skip_capture_backfill_team_key,
|
|
730
|
+
skip_capture_backfill_agent_ids,
|
|
731
|
+
topology_updates,
|
|
732
|
+
receiver_updates,
|
|
733
|
+
exact_owner_seed_to_clear,
|
|
734
|
+
exact_owner_receiver_to_clear,
|
|
735
|
+
exact_owner_receiver_to_restore,
|
|
736
|
+
false,
|
|
737
|
+
)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
fn apply_persist_merge_contract_with_fork_target_preservation(
|
|
741
|
+
incoming: &mut Value,
|
|
742
|
+
latest: &Value,
|
|
743
|
+
deleted_agent_ids: &BTreeSet<String>,
|
|
744
|
+
skip_capture_backfill_team_key: Option<&str>,
|
|
745
|
+
skip_capture_backfill_agent_ids: &BTreeSet<String>,
|
|
746
|
+
topology_updates: &BTreeSet<(String, String)>,
|
|
747
|
+
receiver_updates: &BTreeSet<String>,
|
|
748
|
+
exact_owner_seed_to_clear: Option<(&str, &Value)>,
|
|
749
|
+
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
750
|
+
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
751
|
+
preserve_latest_fork_targets: bool,
|
|
710
752
|
) -> Result<(), StateError> {
|
|
711
753
|
// A0/R1: the projection gate only guards the TOP-LEVEL passes (top-level agents and
|
|
712
754
|
// the top-level<->active-team cross projections depend on which team is active); the
|
|
@@ -740,6 +782,7 @@ fn apply_persist_merge_contract(
|
|
|
740
782
|
skip_capture_backfill_agent_ids,
|
|
741
783
|
&topology_update_agent_ids,
|
|
742
784
|
None,
|
|
785
|
+
preserve_latest_fork_targets,
|
|
743
786
|
)?;
|
|
744
787
|
// Stage 3c (identity-boundary unified plan, architect direction
|
|
745
788
|
// 2026-06-23): top-level owner copy-back removed. Pre-3c this
|
|
@@ -818,6 +861,7 @@ fn apply_persist_merge_contract(
|
|
|
818
861
|
skip_capture_backfill_agent_ids,
|
|
819
862
|
&topology_update_agent_ids,
|
|
820
863
|
team_alive,
|
|
864
|
+
preserve_latest_fork_targets,
|
|
821
865
|
)?;
|
|
822
866
|
preserve_latest_ownership_fields(
|
|
823
867
|
incoming_entry,
|
|
@@ -1196,6 +1240,7 @@ fn merge_agent_projection(
|
|
|
1196
1240
|
// the merge must NOT protect stale topology so a live sibling's write
|
|
1197
1241
|
// can go through.
|
|
1198
1242
|
team_alive: Option<bool>,
|
|
1243
|
+
preserve_latest_fork_targets: bool,
|
|
1199
1244
|
) -> Result<(), StateError> {
|
|
1200
1245
|
let Some(incoming_agents) = incoming_agents else {
|
|
1201
1246
|
return Ok(());
|
|
@@ -1212,8 +1257,17 @@ fn merge_agent_projection(
|
|
|
1212
1257
|
}
|
|
1213
1258
|
let tombstoned_for_projection =
|
|
1214
1259
|
skip_capture_backfill && skip_capture_backfill_agent_ids.contains(agent_id);
|
|
1260
|
+
let fork_target = preserve_latest_fork_targets
|
|
1261
|
+
&& latest_agent
|
|
1262
|
+
.get("forked_from")
|
|
1263
|
+
.and_then(Value::as_str)
|
|
1264
|
+
.is_some_and(|source| !source.is_empty());
|
|
1215
1265
|
match incoming_map.entry(agent_id.clone()) {
|
|
1216
1266
|
serde_json::map::Entry::Vacant(slot) => {
|
|
1267
|
+
if fork_target {
|
|
1268
|
+
slot.insert(latest_agent.clone());
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1217
1271
|
if topology_update_agent_ids.contains(agent_id) {
|
|
1218
1272
|
continue;
|
|
1219
1273
|
}
|
|
@@ -1244,7 +1298,9 @@ fn merge_agent_projection(
|
|
|
1244
1298
|
if !tombstoned_for_projection && !topology_update_agent_ids.contains(agent_id) {
|
|
1245
1299
|
let fields = topology_conflict_fields(existing.get(), latest_agent, team_alive);
|
|
1246
1300
|
if !fields.is_empty() {
|
|
1247
|
-
if
|
|
1301
|
+
if fork_target {
|
|
1302
|
+
preserve_latest_topology(existing.get_mut(), latest_agent);
|
|
1303
|
+
} else if !topology_update_agent_ids.is_empty()
|
|
1248
1304
|
&& lifecycle_peer_snapshot_is_stale(existing.get(), latest_agent)
|
|
1249
1305
|
{
|
|
1250
1306
|
// A lifecycle writer may have loaded a complete
|
|
@@ -972,7 +972,11 @@ fn save_team_scoped_state_with_merge_options(
|
|
|
972
972
|
);
|
|
973
973
|
}
|
|
974
974
|
if tombstoned_agent_ids.is_empty() {
|
|
975
|
-
return save_runtime_state_with_deleted_agents(
|
|
975
|
+
return save_runtime_state_with_deleted_agents(
|
|
976
|
+
workspace,
|
|
977
|
+
&merged,
|
|
978
|
+
deleted_agent_ids,
|
|
979
|
+
);
|
|
976
980
|
}
|
|
977
981
|
return save_runtime_state_with_team_tombstoned_agents(
|
|
978
982
|
workspace,
|
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
use super::
|
|
3
|
-
|
|
4
|
-
#[test]
|
|
5
|
-
fn mcp_reapply_scope_matches_legacy_helpers() {
|
|
6
|
-
assert!(
|
|
7
|
-
reapply_scope(&StateWriteIntent::McpUpdateStateNote {
|
|
8
|
-
team_key: Some("team-a"),
|
|
9
|
-
}) == ReapplyScope::Team
|
|
10
|
-
);
|
|
11
|
-
assert!(
|
|
12
|
-
reapply_scope(&StateWriteIntent::McpAssignTask {
|
|
13
|
-
team_key: Some("team-a"),
|
|
14
|
-
task_id: "task-a",
|
|
15
|
-
}) == ReapplyScope::Root
|
|
16
|
-
);
|
|
17
|
-
}
|
|
2
|
+
use super::StateWriteIntent;
|
|
18
3
|
|
|
19
4
|
#[test]
|
|
20
5
|
fn optional_unmigrated_read_preserves_missing_corrupt_and_valid_shapes(
|
|
@@ -128,6 +128,9 @@ impl<'a> StateRepository<'a> {
|
|
|
128
128
|
/// forward call to the same legacy helper family that the caller used
|
|
129
129
|
/// before S1a; the intent selects the family, not the merge semantics.
|
|
130
130
|
pub fn save(&self, intent: StateWriteIntent<'_>, state: &Value) -> Result<(), StateError> {
|
|
131
|
+
if let StateWriteIntent::CoordinatorTick { team_key } = &intent {
|
|
132
|
+
return self.save_coordinator_tick(team_key, state);
|
|
133
|
+
}
|
|
131
134
|
route_direct(self.workspace, intent, state)
|
|
132
135
|
}
|
|
133
136
|
|
|
@@ -149,8 +152,8 @@ impl<'a> StateRepository<'a> {
|
|
|
149
152
|
route_reapply(self.workspace, intent, state, reapply)
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
/// Commit a
|
|
153
|
-
/// the committed Team view for callers that produce Markdown afterwards.
|
|
155
|
+
/// Commit a bounded Team delta once against latest under state-save.
|
|
156
|
+
/// Return the committed Team view for callers that produce Markdown afterwards.
|
|
154
157
|
pub fn commit<F>(&self, intent: StateWriteIntent<'_>, update: F) -> Result<Value, StateError>
|
|
155
158
|
where
|
|
156
159
|
F: FnOnce(&mut Value),
|
|
@@ -158,12 +161,13 @@ impl<'a> StateRepository<'a> {
|
|
|
158
161
|
#[cfg(test)]
|
|
159
162
|
test_support::before_commit();
|
|
160
163
|
let team_key = match intent {
|
|
161
|
-
// Assignment's existing reconcile helper updates the root task
|
|
162
|
-
// array and the explicit Team array independently (legacy parity).
|
|
163
|
-
StateWriteIntent::McpAssignTask { .. } => None,
|
|
164
|
-
StateWriteIntent::McpUpdateStateNote { team_key } => team_key,
|
|
165
164
|
StateWriteIntent::ResultCollection { owner_team_id } => owner_team_id,
|
|
166
|
-
|
|
165
|
+
StateWriteIntent::ForkAgent { team_key, .. } => Some(team_key),
|
|
166
|
+
_ => {
|
|
167
|
+
return Err(StateError::SaveFailed(
|
|
168
|
+
"intent does not accept a bounded Team delta".into(),
|
|
169
|
+
))
|
|
170
|
+
}
|
|
167
171
|
};
|
|
168
172
|
let committed = super::persist::update_runtime_state(self.workspace, |latest| {
|
|
169
173
|
let mut selected = bounded_team_view(latest, team_key)?;
|
|
@@ -176,6 +180,64 @@ impl<'a> StateRepository<'a> {
|
|
|
176
180
|
bounded_team_view(&committed, team_key)
|
|
177
181
|
}
|
|
178
182
|
|
|
183
|
+
/// Atomically replace only the fork target row in the selected team.
|
|
184
|
+
pub(crate) fn commit_fork_agent(
|
|
185
|
+
&self,
|
|
186
|
+
intent: StateWriteIntent<'_>,
|
|
187
|
+
target_row: &Value,
|
|
188
|
+
) -> Result<(), StateError> {
|
|
189
|
+
let (team_key, agent_id) = match &intent {
|
|
190
|
+
StateWriteIntent::ForkAgent {
|
|
191
|
+
team_key,
|
|
192
|
+
agent_id,
|
|
193
|
+
} => (*team_key, *agent_id),
|
|
194
|
+
_ => {
|
|
195
|
+
return Err(StateError::SaveFailed(
|
|
196
|
+
"fork target delta requires ForkAgent intent".into(),
|
|
197
|
+
))
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
let target_row = target_row.clone();
|
|
201
|
+
let mut found = false;
|
|
202
|
+
self.commit(
|
|
203
|
+
StateWriteIntent::ForkAgent { team_key, agent_id },
|
|
204
|
+
|selected| {
|
|
205
|
+
if let Some(row) = selected
|
|
206
|
+
.get_mut("agents")
|
|
207
|
+
.and_then(Value::as_object_mut)
|
|
208
|
+
.and_then(|agents| agents.get_mut(agent_id))
|
|
209
|
+
{
|
|
210
|
+
*row = target_row;
|
|
211
|
+
found = true;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
)?;
|
|
215
|
+
if found {
|
|
216
|
+
Ok(())
|
|
217
|
+
} else {
|
|
218
|
+
Err(StateError::SaveFailed(
|
|
219
|
+
"ForkAgent target row is missing from selected team".into(),
|
|
220
|
+
))
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fn save_coordinator_tick(&self, team_key: &str, state: &Value) -> Result<(), StateError> {
|
|
225
|
+
let incoming = state.clone();
|
|
226
|
+
super::persist::update_runtime_state(self.workspace, |latest| {
|
|
227
|
+
let mut incoming_team = bounded_team_view(&incoming, Some(team_key))?;
|
|
228
|
+
let latest_team = bounded_team_view(latest, Some(team_key))?;
|
|
229
|
+
preserve_latest_fork_rows(&mut incoming_team, &latest_team);
|
|
230
|
+
let mut checked =
|
|
231
|
+
super::projection::merge_committed_team(latest, &incoming_team, team_key);
|
|
232
|
+
super::persist::merge_ordinary_state(&mut checked, latest)?;
|
|
233
|
+
let checked = bounded_team_view(&checked, Some(team_key))?;
|
|
234
|
+
Ok(super::projection::merge_committed_team(
|
|
235
|
+
latest, &checked, team_key,
|
|
236
|
+
))
|
|
237
|
+
})?;
|
|
238
|
+
Ok(())
|
|
239
|
+
}
|
|
240
|
+
|
|
179
241
|
/// Fence the sampled incarnation with the existing persist guards, then
|
|
180
242
|
/// commit only this tick's agent observations and coordinator bookkeeping.
|
|
181
243
|
pub fn commit_observations(
|
|
@@ -201,6 +263,7 @@ impl<'a> StateRepository<'a> {
|
|
|
201
263
|
));
|
|
202
264
|
}
|
|
203
265
|
let mut checked = observed.clone();
|
|
266
|
+
preserve_latest_fork_rows(&mut checked, latest);
|
|
204
267
|
super::persist::merge_ordinary_state(&mut checked, latest)?;
|
|
205
268
|
if !legacy_single_team_state(&checked)
|
|
206
269
|
|| sampled_identity != legacy_team_identity(&checked)
|
|
@@ -222,7 +285,13 @@ impl<'a> StateRepository<'a> {
|
|
|
222
285
|
}
|
|
223
286
|
return Ok(selected);
|
|
224
287
|
}
|
|
225
|
-
let mut
|
|
288
|
+
let mut observed_with_delta = observed.clone();
|
|
289
|
+
sync_team_observation_delta(before, &mut observed_with_delta, team_key);
|
|
290
|
+
let mut observed_team = bounded_team_view(&observed_with_delta, Some(team_key))?;
|
|
291
|
+
let latest_team = bounded_team_view(latest, Some(team_key))?;
|
|
292
|
+
preserve_latest_fork_rows(&mut observed_team, &latest_team);
|
|
293
|
+
let mut checked =
|
|
294
|
+
super::projection::merge_committed_team(latest, &observed_team, team_key);
|
|
226
295
|
super::persist::merge_ordinary_state(&mut checked, latest)?;
|
|
227
296
|
let checked = bounded_team_view(&checked, Some(team_key))?;
|
|
228
297
|
let mut selected = bounded_team_view(latest, Some(team_key))?;
|
|
@@ -231,12 +300,32 @@ impl<'a> StateRepository<'a> {
|
|
|
231
300
|
apply_observation_delta(&before[field], &checked[field], &mut selected[field]);
|
|
232
301
|
}
|
|
233
302
|
}
|
|
303
|
+
sync_team_observation_delta(before, &mut selected, team_key);
|
|
234
304
|
Ok(super::projection::merge_committed_team(latest, &selected, team_key))
|
|
235
305
|
})?;
|
|
236
306
|
Ok(())
|
|
237
307
|
}
|
|
238
308
|
}
|
|
239
309
|
|
|
310
|
+
// Coordinator observations do not own fork-target lifecycle rows.
|
|
311
|
+
fn preserve_latest_fork_rows(incoming: &mut Value, latest: &Value) {
|
|
312
|
+
let Some(latest_agents) = latest.get("agents").and_then(Value::as_object) else {
|
|
313
|
+
return;
|
|
314
|
+
};
|
|
315
|
+
let Some(incoming_agents) = incoming.get_mut("agents").and_then(Value::as_object_mut) else {
|
|
316
|
+
return;
|
|
317
|
+
};
|
|
318
|
+
for (agent_id, latest_agent) in latest_agents {
|
|
319
|
+
if latest_agent
|
|
320
|
+
.get("forked_from")
|
|
321
|
+
.and_then(Value::as_str)
|
|
322
|
+
.is_some_and(|source| !source.is_empty())
|
|
323
|
+
{
|
|
324
|
+
incoming_agents.insert(agent_id.clone(), latest_agent.clone());
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
240
329
|
fn legacy_single_team_state(state: &Value) -> bool {
|
|
241
330
|
match state.get("teams") {
|
|
242
331
|
None => true,
|
|
@@ -292,6 +381,34 @@ fn apply_observation_delta(before: &Value, observed: &Value, latest: &mut Value)
|
|
|
292
381
|
}
|
|
293
382
|
}
|
|
294
383
|
|
|
384
|
+
// The coordinator observes a projected team view, while its nested `teams`
|
|
385
|
+
// snapshot remains stale. Carry only observation deltas back to the canonical
|
|
386
|
+
// team entry before `bounded_team_view` projects it again.
|
|
387
|
+
fn sync_team_observation_delta(before: &Value, observed: &mut Value, team_key: &str) {
|
|
388
|
+
for field in ["agents", "coordinator"] {
|
|
389
|
+
let before_value = before.get(field).cloned().unwrap_or(Value::Null);
|
|
390
|
+
let observed_value = observed.get(field).cloned().unwrap_or(Value::Null);
|
|
391
|
+
if before_value == observed_value {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
let Some(team_entry) = observed
|
|
395
|
+
.get_mut("teams")
|
|
396
|
+
.and_then(Value::as_object_mut)
|
|
397
|
+
.and_then(|teams| teams.get_mut(team_key))
|
|
398
|
+
.and_then(Value::as_object_mut)
|
|
399
|
+
else {
|
|
400
|
+
continue;
|
|
401
|
+
};
|
|
402
|
+
if field == "coordinator" && !team_entry.contains_key(field) {
|
|
403
|
+
team_entry.insert(field.to_string(), serde_json::json!({}));
|
|
404
|
+
}
|
|
405
|
+
let slot = team_entry
|
|
406
|
+
.entry(field.to_string())
|
|
407
|
+
.or_insert_with(|| before_value.clone());
|
|
408
|
+
apply_observation_delta(&before_value, &observed_value, slot);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
295
412
|
/// Closed semantic catalog of workspace state writes. Every allowlisted
|
|
296
413
|
/// callsite maps to exactly one variant, and there is intentionally no
|
|
297
414
|
/// generic escape bucket. Any new state write must earn its own variant so
|
|
@@ -396,16 +513,6 @@ pub enum StateWriteIntent<'a> {
|
|
|
396
513
|
CoordinatorConptyShim {
|
|
397
514
|
team_key: Option<&'a str>,
|
|
398
515
|
},
|
|
399
|
-
McpAssignTask {
|
|
400
|
-
team_key: Option<&'a str>,
|
|
401
|
-
task_id: &'a str,
|
|
402
|
-
},
|
|
403
|
-
McpLifecycleAgentOps {
|
|
404
|
-
team_key: Option<&'a str>,
|
|
405
|
-
},
|
|
406
|
-
McpUpdateStateNote {
|
|
407
|
-
team_key: Option<&'a str>,
|
|
408
|
-
},
|
|
409
516
|
MessagingDeliveryState {
|
|
410
517
|
owner_team_id: Option<&'a str>,
|
|
411
518
|
},
|
|
@@ -416,9 +523,6 @@ pub enum StateWriteIntent<'a> {
|
|
|
416
523
|
ResultCollection {
|
|
417
524
|
owner_team_id: Option<&'a str>,
|
|
418
525
|
},
|
|
419
|
-
SchedulerSuppression {
|
|
420
|
-
owner_team_id: Option<&'a str>,
|
|
421
|
-
},
|
|
422
526
|
TaskRepair {
|
|
423
527
|
team_key: Option<&'a str>,
|
|
424
528
|
},
|
|
@@ -614,15 +718,6 @@ fn route_direct(
|
|
|
614
718
|
// CoordinatorConptyShim -> root save
|
|
615
719
|
// (coordinator/conpty_shim.rs:426/674).
|
|
616
720
|
StateWriteIntent::CoordinatorConptyShim { .. } => helper_write_root(workspace, state),
|
|
617
|
-
// McpAssignTask uses the reapply variant today; direct save routes
|
|
618
|
-
// to the root helper for parity.
|
|
619
|
-
StateWriteIntent::McpAssignTask { .. } => helper_write_root(workspace, state),
|
|
620
|
-
// McpLifecycleAgentOps -> root save
|
|
621
|
-
// (mcp_server/lifecycle_tools/agent_ops.rs:385).
|
|
622
|
-
StateWriteIntent::McpLifecycleAgentOps { .. } => helper_write_root(workspace, state),
|
|
623
|
-
// McpUpdateStateNote -> root save
|
|
624
|
-
// (mcp_server/lifecycle_tools/state_status.rs:29/69).
|
|
625
|
-
StateWriteIntent::McpUpdateStateNote { .. } => helper_write_root(workspace, state),
|
|
626
721
|
// MessagingDeliveryState direct writes cover team-scoped, root
|
|
627
722
|
// fallback, and root (messaging/delivery.rs:2356/2358/2361).
|
|
628
723
|
StateWriteIntent::MessagingDeliveryState { owner_team_id } => match owner_team_id {
|
|
@@ -645,8 +740,6 @@ fn route_direct(
|
|
|
645
740
|
Some(_) => helper_write_team_scoped(workspace, state),
|
|
646
741
|
None => helper_write_root(workspace, state),
|
|
647
742
|
},
|
|
648
|
-
// SchedulerSuppression -> root save (messaging/scheduler.rs:259).
|
|
649
|
-
StateWriteIntent::SchedulerSuppression { .. } => helper_write_root(workspace, state),
|
|
650
743
|
// TaskRepair -> `save_team_scoped_state` (cli/adapters.rs:675).
|
|
651
744
|
StateWriteIntent::TaskRepair { .. } => helper_write_team_scoped(workspace, state),
|
|
652
745
|
// FakeE2eSeed / SelfMigration are diagnostic seams. The current
|
|
@@ -677,9 +770,6 @@ fn reapply_scope(intent: &StateWriteIntent<'_>) -> ReapplyScope {
|
|
|
677
770
|
owner_team_id: Some(_),
|
|
678
771
|
}
|
|
679
772
|
| StateWriteIntent::CoordinatorTick { .. }
|
|
680
|
-
| StateWriteIntent::McpUpdateStateNote {
|
|
681
|
-
team_key: Some(_),
|
|
682
|
-
}
|
|
683
773
|
) {
|
|
684
774
|
ReapplyScope::Team
|
|
685
775
|
} else {
|
|
@@ -696,8 +786,7 @@ fn route_reapply<F>(
|
|
|
696
786
|
where
|
|
697
787
|
F: FnOnce(&mut Value),
|
|
698
788
|
{
|
|
699
|
-
if matches!(&intent, StateWriteIntent::
|
|
700
|
-
| StateWriteIntent::McpUpdateStateNote { .. } | StateWriteIntent::ResultCollection { .. })
|
|
789
|
+
if matches!(&intent, StateWriteIntent::ResultCollection { .. })
|
|
701
790
|
{
|
|
702
791
|
return StateRepository::new(workspace).commit(intent, reapply).map(|_| ());
|
|
703
792
|
}
|
|
@@ -360,7 +360,20 @@ impl OfflineTransport {
|
|
|
360
360
|
if kind == "spawn_first" && !state.session_absent_after_spawn_first {
|
|
361
361
|
state.session_present = true;
|
|
362
362
|
}
|
|
363
|
-
let pane_index = state.spawns.len().saturating_sub(1);
|
|
363
|
+
let mut pane_index = state.spawns.len().saturating_sub(1);
|
|
364
|
+
// Existing sessions reserve `%0` for the already-running pane.
|
|
365
|
+
if kind == "spawn_into" {
|
|
366
|
+
pane_index = pane_index.max(1);
|
|
367
|
+
}
|
|
368
|
+
while state.pane_presence.contains_key(&format!("%{pane_index}"))
|
|
369
|
+
|| state
|
|
370
|
+
.targets
|
|
371
|
+
.iter()
|
|
372
|
+
.chain(state.target_snapshots.iter().flatten())
|
|
373
|
+
.any(|target| target.pane_id.as_str() == format!("%{pane_index}"))
|
|
374
|
+
{
|
|
375
|
+
pane_index = pane_index.saturating_add(1);
|
|
376
|
+
}
|
|
364
377
|
state
|
|
365
378
|
.pane_presence
|
|
366
379
|
.insert(format!("%{pane_index}"), state.spawned_panes_addressable);
|
|
@@ -541,10 +554,17 @@ impl Transport for OfflineTransport {
|
|
|
541
554
|
});
|
|
542
555
|
}
|
|
543
556
|
Ok(self.with_state(|state| {
|
|
544
|
-
state
|
|
557
|
+
let targets = state
|
|
545
558
|
.target_snapshots
|
|
546
559
|
.pop_front()
|
|
547
|
-
.unwrap_or_else(|| state.targets.clone())
|
|
560
|
+
.unwrap_or_else(|| state.targets.clone());
|
|
561
|
+
for target in &targets {
|
|
562
|
+
state
|
|
563
|
+
.pane_presence
|
|
564
|
+
.entry(target.pane_id.as_str().to_string())
|
|
565
|
+
.or_insert(true);
|
|
566
|
+
}
|
|
567
|
+
targets
|
|
548
568
|
}))
|
|
549
569
|
}
|
|
550
570
|
|