@team-agent/installer 0.4.0 → 0.4.2
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 +82 -22
- package/crates/team-agent/src/cli/diagnose.rs +6 -2
- package/crates/team-agent/src/cli/emit.rs +301 -38
- package/crates/team-agent/src/cli/mod.rs +7 -14
- package/crates/team-agent/src/cli/status_port.rs +12 -1
- package/crates/team-agent/src/cli/tests/base.rs +6 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- package/crates/team-agent/src/cli/tests/leader_watch.rs +5 -3
- package/crates/team-agent/src/cli/tests/main_preserved.rs +28 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +19 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +72 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +35 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +7 -2
- package/crates/team-agent/src/coordinator/tick.rs +86 -16
- package/crates/team-agent/src/diagnose/comms.rs +10 -2
- package/crates/team-agent/src/leader/lease.rs +98 -19
- package/crates/team-agent/src/leader/rediscover/tests.rs +21 -7
- package/crates/team-agent/src/leader/rediscover.rs +16 -7
- package/crates/team-agent/src/leader/start.rs +25 -12
- package/crates/team-agent/src/leader/tests/byte_findings.rs +11 -2
- package/crates/team-agent/src/leader/tests/lease_claim.rs +248 -7
- package/crates/team-agent/src/lifecycle/launch.rs +116 -100
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +5 -12
- package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +16 -15
- package/crates/team-agent/src/lifecycle/types.rs +1 -1
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -8
- package/crates/team-agent/src/messaging/send.rs +21 -4
- package/crates/team-agent/src/provider/adapter.rs +22 -0
- package/crates/team-agent/src/provider/session/capture.rs +228 -0
- package/crates/team-agent/src/state/identity.rs +42 -12
- package/crates/team-agent/src/state/identity_keys.rs +134 -0
- package/crates/team-agent/src/state/mod.rs +8 -0
- package/crates/team-agent/src/state/owner_gate.rs +11 -1
- package/crates/team-agent/src/state/ownership.rs +556 -0
- package/crates/team-agent/src/state/paths.rs +358 -0
- package/crates/team-agent/src/state/persist.rs +43 -5
- package/crates/team-agent/src/state/projection.rs +13 -5
- package/crates/team-agent/src/tmux_backend.rs +12 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -3
|
@@ -1031,6 +1031,17 @@ fn scan_agents_for_pane(agents: Option<&Value>, caller_pane: &PaneId) -> Option<
|
|
|
1031
1031
|
None
|
|
1032
1032
|
}
|
|
1033
1033
|
|
|
1034
|
+
/// Stage 3a (identity-boundary unified plan, architect direction 2026-06-23)
|
|
1035
|
+
/// + Stage 3 owner persist fix (architect direction 2026-06-24): route the
|
|
1036
|
+
/// in-memory binding write through the `state::ownership` API. The
|
|
1037
|
+
/// canonical team_key is derived from `active_team_key` first (the explicit
|
|
1038
|
+
/// scope set by `project_top_level_view` when an explicit team was passed
|
|
1039
|
+
/// to claim-leader) and only falls back to `team_state_key(state)` for the
|
|
1040
|
+
/// legacy unscoped flow. Without the `active_team_key` preference, an
|
|
1041
|
+
/// explicit `claim-leader --team alpha` whose projected state was emptied
|
|
1042
|
+
/// of team_dir/session_name would derive "current" and write the owner to
|
|
1043
|
+
/// `teams.current`, while `save_claim_team_scoped_state` expected
|
|
1044
|
+
/// `teams.alpha` — the S3-OWNER-001 persistence loss shape.
|
|
1034
1045
|
fn write_binding_to_state(
|
|
1035
1046
|
state: &mut Value,
|
|
1036
1047
|
receiver: &LeaderReceiver,
|
|
@@ -1039,12 +1050,15 @@ fn write_binding_to_state(
|
|
|
1039
1050
|
if !state.is_object() {
|
|
1040
1051
|
*state = json!({});
|
|
1041
1052
|
}
|
|
1042
|
-
|
|
1053
|
+
if !state.is_object() {
|
|
1043
1054
|
return Err(LeaderError::Validation("state root is not an object".to_string()));
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1055
|
+
}
|
|
1056
|
+
let team_key = canonical_owner_write_key(state);
|
|
1057
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1058
|
+
.with_leader_receiver(serde_json::to_value(receiver)?)
|
|
1059
|
+
.with_team_owner(serde_json::to_value(owner)?)
|
|
1060
|
+
.with_owner_epoch(owner.owner_epoch.0);
|
|
1061
|
+
crate::state::ownership::write_owner(state, &team_key, record);
|
|
1048
1062
|
Ok(())
|
|
1049
1063
|
}
|
|
1050
1064
|
|
|
@@ -1055,13 +1069,34 @@ fn write_receiver_to_state(
|
|
|
1055
1069
|
if !state.is_object() {
|
|
1056
1070
|
*state = json!({});
|
|
1057
1071
|
}
|
|
1058
|
-
|
|
1072
|
+
if !state.is_object() {
|
|
1059
1073
|
return Err(LeaderError::Validation("state root is not an object".to_string()));
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1074
|
+
}
|
|
1075
|
+
let team_key = canonical_owner_write_key(state);
|
|
1076
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1077
|
+
.with_leader_receiver(serde_json::to_value(receiver)?);
|
|
1078
|
+
crate::state::ownership::write_owner(state, &team_key, record);
|
|
1062
1079
|
Ok(())
|
|
1063
1080
|
}
|
|
1064
1081
|
|
|
1082
|
+
/// Stage 3 owner persist fix (architect direction 2026-06-24): determine
|
|
1083
|
+
/// the canonical team_key for an in-memory ownership write. When the state
|
|
1084
|
+
/// carries an `active_team_key` (set by `project_top_level_view` whenever
|
|
1085
|
+
/// an explicit team was scoped into the claim path), trust it — that's the
|
|
1086
|
+
/// requested team that `save_claim_team_scoped_state` will read back. Only
|
|
1087
|
+
/// fall back to `team_state_key(state)` for the legacy unscoped flow where
|
|
1088
|
+
/// the writer must derive team identity from session/dir/spec_path fields.
|
|
1089
|
+
fn canonical_owner_write_key(state: &Value) -> String {
|
|
1090
|
+
if let Some(active) = state
|
|
1091
|
+
.get("active_team_key")
|
|
1092
|
+
.and_then(Value::as_str)
|
|
1093
|
+
.filter(|key| !key.is_empty())
|
|
1094
|
+
{
|
|
1095
|
+
return active.to_string();
|
|
1096
|
+
}
|
|
1097
|
+
crate::state::projection::team_state_key(state)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1065
1100
|
fn state_receiver(state: &Value) -> Option<LeaderReceiver> {
|
|
1066
1101
|
state
|
|
1067
1102
|
.get("leader_receiver")
|
|
@@ -1139,9 +1174,16 @@ fn save_claim_team_scoped_state(workspace: &Path, state: &Value, target_key: &st
|
|
|
1139
1174
|
.entry(existing_key)
|
|
1140
1175
|
.or_insert_with(|| crate::state::projection::compact_team_state(&existing));
|
|
1141
1176
|
}
|
|
1177
|
+
// Stage 3 owner persist fix (architect direction 2026-06-24,
|
|
1178
|
+
// .team/artifacts/stage3-owner-persist-fix.md): after Stage 3d removed
|
|
1179
|
+
// top-level dual-write, the canonical ownership record lives ONLY at
|
|
1180
|
+
// `state.teams[target_key].{team_owner, leader_receiver, owner_epoch}`.
|
|
1181
|
+
// `compact_team_state(state)` strips the `teams` key entirely — that
|
|
1182
|
+
// dropped the just-written nested ownership record on disk. Use the
|
|
1183
|
+
// preserving helper so the canonical fields survive the compaction.
|
|
1142
1184
|
teams.insert(
|
|
1143
1185
|
target_key.to_string(),
|
|
1144
|
-
|
|
1186
|
+
compact_team_state_preserving_ownership(state, target_key),
|
|
1145
1187
|
);
|
|
1146
1188
|
let existing_primary_key = existing
|
|
1147
1189
|
.get("session_name")
|
|
@@ -1154,16 +1196,18 @@ fn save_claim_team_scoped_state(workspace: &Path, state: &Value, target_key: &st
|
|
|
1154
1196
|
} else {
|
|
1155
1197
|
value_object(&existing)
|
|
1156
1198
|
};
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1199
|
+
// Stage 3 top-level cleanup (architect direction 2026-06-24,
|
|
1200
|
+
// .team/artifacts/stage3-toplevel-cleanup-fix.md + stage3-save-strip-fix.md):
|
|
1201
|
+
// the legacy top-level owner copy loop is gone; the canonical-aware
|
|
1202
|
+
// strip is now performed at the persistence boundary
|
|
1203
|
+
// (`state::persist::save_runtime_state_with_merge_exceptions`), which
|
|
1204
|
+
// every save_runtime_state* path funnels through. This per-call-site
|
|
1205
|
+
// strip call has been removed — the save output handles cleanup
|
|
1206
|
+
// uniformly for claim / restart / shutdown / start-agent / stop-agent
|
|
1207
|
+
// / coordinator tick / promote sibling. `had_existing_teams` /
|
|
1208
|
+
// primary/active key computations are retained as dead writes only
|
|
1209
|
+
// via _ binding — they no longer gate any owner promote.
|
|
1210
|
+
let _ = (had_existing_teams, existing_primary_key, existing_active_key);
|
|
1167
1211
|
merged.insert("teams".to_string(), Value::Object(teams));
|
|
1168
1212
|
crate::state::persist::save_runtime_state(workspace, &Value::Object(merged))?;
|
|
1169
1213
|
Ok(())
|
|
@@ -1173,6 +1217,41 @@ fn value_object(value: &Value) -> serde_json::Map<String, Value> {
|
|
|
1173
1217
|
value.as_object().cloned().unwrap_or_default()
|
|
1174
1218
|
}
|
|
1175
1219
|
|
|
1220
|
+
/// Stage 3 owner persist fix (architect direction 2026-06-24,
|
|
1221
|
+
/// .team/artifacts/stage3-owner-persist-fix.md): build the per-team
|
|
1222
|
+
/// snapshot used by `save_claim_team_scoped_state` so the canonical
|
|
1223
|
+
/// `teams.<target_key>.{team_owner, leader_receiver, owner_epoch}` record
|
|
1224
|
+
/// survives the `compact_team_state` strip. Without this helper the
|
|
1225
|
+
/// claim-leader save loses the just-written nested ownership record (the
|
|
1226
|
+
/// S3-OWNER-001 evidence shape: `persisted_owner_locations=[]`).
|
|
1227
|
+
///
|
|
1228
|
+
/// Behaviour:
|
|
1229
|
+
/// - Start from the existing `compact_team_state(state)` (strips `teams`).
|
|
1230
|
+
/// - Look up `state.teams[target_key].{team_owner, leader_receiver, owner_epoch}`
|
|
1231
|
+
/// and copy each present field into the compacted entry.
|
|
1232
|
+
/// - Top-level owner fields stay UNTOUCHED — Stage 3d's canonical-only
|
|
1233
|
+
/// shape is preserved on disk.
|
|
1234
|
+
fn compact_team_state_preserving_ownership(state: &Value, target_key: &str) -> Value {
|
|
1235
|
+
let mut entry = crate::state::projection::compact_team_state(state);
|
|
1236
|
+
let Some(entry_obj) = entry.as_object_mut() else {
|
|
1237
|
+
return entry;
|
|
1238
|
+
};
|
|
1239
|
+
let Some(owner_obj) = state
|
|
1240
|
+
.get("teams")
|
|
1241
|
+
.and_then(Value::as_object)
|
|
1242
|
+
.and_then(|teams| teams.get(target_key))
|
|
1243
|
+
.and_then(Value::as_object)
|
|
1244
|
+
else {
|
|
1245
|
+
return entry;
|
|
1246
|
+
};
|
|
1247
|
+
for key in ["team_owner", "leader_receiver", "owner_epoch"] {
|
|
1248
|
+
if let Some(value) = owner_obj.get(key) {
|
|
1249
|
+
entry_obj.insert(key.to_string(), value.clone());
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
entry
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1176
1255
|
/// `_detect_dual_state_divergence`(card §85 C18;`__init__.py:556`)。workspace-level 与
|
|
1177
1256
|
/// team-level snapshot 在 owner_uuid/receiver_pane_id/owner_epoch 上是否分叉 → `Some(详情)`。
|
|
1178
1257
|
/// **CROSS-LANE**:snapshot 读经 step 13 restart。
|
|
@@ -127,10 +127,14 @@ fn try_readopt_writes_owner_receiver_dual_state_and_events() {
|
|
|
127
127
|
assert_eq!(got.1["pane"]["pane_id"], json!("%new"));
|
|
128
128
|
assert_eq!(got.1["readopted"], json!(true));
|
|
129
129
|
assert!(got.1.get("warning").is_some_and(Value::is_null));
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
// Stage 3d: owner + receiver live at canonical teams.<team_key>.
|
|
131
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
132
|
+
let owner_view = &state["teams"][&team_key]["team_owner"];
|
|
133
|
+
let receiver_view = &state["teams"][&team_key]["leader_receiver"];
|
|
134
|
+
assert_eq!(owner_view["pane_id"], json!("%new"));
|
|
135
|
+
assert_eq!(owner_view["claimed_via"], json!("attach-leader"));
|
|
136
|
+
assert_eq!(owner_view["owner_epoch"], json!(4));
|
|
137
|
+
assert_eq!(receiver_view["discovery"], json!("attach_readopt"));
|
|
134
138
|
assert!(crate::model::paths::runtime_dir(&ws)
|
|
135
139
|
.join("teams")
|
|
136
140
|
.join("sess")
|
|
@@ -185,6 +189,10 @@ fn try_readopt_refuses_when_different_live_owner_uuid_mismatches() {
|
|
|
185
189
|
.unwrap();
|
|
186
190
|
|
|
187
191
|
assert!(got.is_none());
|
|
192
|
+
// Stage 3d: no owner anywhere — check both legacy top-level and the
|
|
193
|
+
// canonical teams.<key> location via the ownership repository.
|
|
194
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
195
|
+
assert!(crate::state::ownership::read_owner_value(&state, &team_key).is_none());
|
|
188
196
|
assert!(state.get("team_owner").is_none());
|
|
189
197
|
}
|
|
190
198
|
|
|
@@ -227,7 +235,10 @@ fn rediscover_updates_unique_env_triple_match() {
|
|
|
227
235
|
assert_eq!(out["status"], json!("updated"));
|
|
228
236
|
assert!(out.get("ok").is_none(), "golden rediscover result has no ok wrapper");
|
|
229
237
|
assert_eq!(out["owner_identity"]["pane_id"], json!("%r"));
|
|
230
|
-
|
|
238
|
+
// Stage 3d: receiver lives at canonical teams.<team_key>.leader_receiver.
|
|
239
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
240
|
+
let receiver = &state["teams"][&team_key]["leader_receiver"];
|
|
241
|
+
assert_eq!(receiver["pane_id"], json!("%r"));
|
|
231
242
|
let events = event_log.tail(10).unwrap();
|
|
232
243
|
let rediscovered = event_named(&events, "leader_receiver.rediscovered");
|
|
233
244
|
assert_eq!(rediscovered["provider"], json!("codex"));
|
|
@@ -241,7 +252,7 @@ fn rediscover_updates_unique_env_triple_match() {
|
|
|
241
252
|
assert_eq!(rebound["new_pane_id"], json!("%r"));
|
|
242
253
|
assert_eq!(rebound["owner_identity"]["machine_fingerprint"], json!("fp"));
|
|
243
254
|
assert_eq!(rebound["uuid_prefix"], json!("OWNERUUI"));
|
|
244
|
-
assert_eq!(
|
|
255
|
+
assert_eq!(receiver["discovery"], json!("stale_rediscovery_unique_candidate"));
|
|
245
256
|
}
|
|
246
257
|
|
|
247
258
|
#[test]
|
|
@@ -286,7 +297,10 @@ fn rediscover_owner_identity_passthrough_sets_discovery_and_reason() {
|
|
|
286
297
|
|
|
287
298
|
assert_eq!(out["status"], json!("updated"));
|
|
288
299
|
assert_eq!(out["owner_identity"], raw_identity);
|
|
289
|
-
|
|
300
|
+
// Stage 3d: receiver lives at canonical teams.<team_key>.leader_receiver.
|
|
301
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
302
|
+
let receiver = &state["teams"][&team_key]["leader_receiver"];
|
|
303
|
+
assert_eq!(receiver["discovery"], json!("stale_rediscovery_owner_identity"));
|
|
290
304
|
let rebound = event_named(&event_log.tail(10).unwrap(), "leader_receiver.rebind_applied");
|
|
291
305
|
assert_eq!(rebound["reason"], json!("stale_receiver"));
|
|
292
306
|
assert_eq!(rebound["owner_identity"], raw_identity);
|
|
@@ -1064,6 +1064,9 @@ fn state_receiver(state: &Value) -> Option<LeaderReceiver> {
|
|
|
1064
1064
|
.and_then(|value| serde_json::from_value(value).ok())
|
|
1065
1065
|
}
|
|
1066
1066
|
|
|
1067
|
+
/// Stage 3a (identity-boundary unified plan, architect direction 2026-06-23):
|
|
1068
|
+
/// route the readopt binding write through the ownership repository.
|
|
1069
|
+
/// Behaviour byte-identical to the pre-3a hand-rolled inserts.
|
|
1067
1070
|
fn write_readopt_state(
|
|
1068
1071
|
workspace: &Path,
|
|
1069
1072
|
state: &mut Value,
|
|
@@ -1073,11 +1076,14 @@ fn write_readopt_state(
|
|
|
1073
1076
|
if !state.is_object() {
|
|
1074
1077
|
*state = json!({});
|
|
1075
1078
|
}
|
|
1076
|
-
|
|
1079
|
+
if !state.is_object() {
|
|
1077
1080
|
return Err(LeaderError::Validation("state root is not an object".to_string()));
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
+
}
|
|
1082
|
+
let team_key = crate::state::projection::team_state_key(state);
|
|
1083
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1084
|
+
.with_leader_receiver(serde_json::to_value(receiver)?)
|
|
1085
|
+
.with_team_owner(serde_json::to_value(owner)?);
|
|
1086
|
+
crate::state::ownership::write_owner(state, &team_key, record);
|
|
1081
1087
|
crate::leader::write_lease_dual_state(workspace, state)
|
|
1082
1088
|
}
|
|
1083
1089
|
|
|
@@ -1089,10 +1095,13 @@ fn write_receiver_state(
|
|
|
1089
1095
|
if !state.is_object() {
|
|
1090
1096
|
*state = json!({});
|
|
1091
1097
|
}
|
|
1092
|
-
|
|
1098
|
+
if !state.is_object() {
|
|
1093
1099
|
return Err(LeaderError::Validation("state root is not an object".to_string()));
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1100
|
+
}
|
|
1101
|
+
let team_key = crate::state::projection::team_state_key(state);
|
|
1102
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1103
|
+
.with_leader_receiver(serde_json::to_value(receiver)?);
|
|
1104
|
+
crate::state::ownership::write_owner(state, &team_key, record);
|
|
1096
1105
|
crate::leader::write_lease_dual_state(workspace, state)
|
|
1097
1106
|
}
|
|
1098
1107
|
|
|
@@ -587,10 +587,13 @@ fn persist_managed_leader_binding(
|
|
|
587
587
|
"tmux": std::env::var("TMUX").ok(),
|
|
588
588
|
}),
|
|
589
589
|
);
|
|
590
|
-
obj.insert("leader_receiver".to_string(), receiver);
|
|
591
|
-
obj.insert("team_owner".to_string(), owner);
|
|
592
|
-
obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
|
|
593
590
|
}
|
|
591
|
+
// Stage 3a/d (identity-boundary unified plan, architect direction
|
|
592
|
+
// 2026-06-23): build the teams.<key> entry from the non-owner fields
|
|
593
|
+
// of state (compact_team_state strips the `teams` key), then publish
|
|
594
|
+
// the canonical owner record AFTER teams.insert so write_owner's
|
|
595
|
+
// teams.<key>.{team_owner,leader_receiver,owner_epoch} is the final
|
|
596
|
+
// word and doesn't get overwritten by the compacted entry.
|
|
594
597
|
let entry = crate::state::projection::compact_team_state(&state);
|
|
595
598
|
if let Some(obj) = state.as_object_mut() {
|
|
596
599
|
let teams = obj
|
|
@@ -600,6 +603,11 @@ fn persist_managed_leader_binding(
|
|
|
600
603
|
teams.insert(identity.team_id.as_str().to_string(), entry);
|
|
601
604
|
}
|
|
602
605
|
}
|
|
606
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
607
|
+
.with_leader_receiver(receiver)
|
|
608
|
+
.with_team_owner(owner)
|
|
609
|
+
.with_owner_epoch(owner_epoch);
|
|
610
|
+
crate::state::ownership::write_owner(&mut state, identity.team_id.as_str(), record);
|
|
603
611
|
crate::state::persist::save_runtime_state(workspace, &state)?;
|
|
604
612
|
Ok(())
|
|
605
613
|
}
|
|
@@ -689,10 +697,11 @@ fn persist_exec_provider_leader_binding(
|
|
|
689
697
|
"tmux": std::env::var("TMUX").ok(),
|
|
690
698
|
}),
|
|
691
699
|
);
|
|
692
|
-
obj.insert("leader_receiver".to_string(), receiver);
|
|
693
|
-
obj.insert("team_owner".to_string(), owner);
|
|
694
|
-
obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
|
|
695
700
|
}
|
|
701
|
+
// Stage 3a/d (identity-boundary unified plan, architect direction
|
|
702
|
+
// 2026-06-23): compact-then-write-owner ordering as in
|
|
703
|
+
// persist_managed_leader_binding above. write_owner must be the final
|
|
704
|
+
// write so the canonical teams.<key> owner record survives.
|
|
696
705
|
let entry = crate::state::projection::compact_team_state(&state);
|
|
697
706
|
if let Some(obj) = state.as_object_mut() {
|
|
698
707
|
let teams = obj
|
|
@@ -702,6 +711,11 @@ fn persist_exec_provider_leader_binding(
|
|
|
702
711
|
teams.insert(identity.team_id.as_str().to_string(), entry);
|
|
703
712
|
}
|
|
704
713
|
}
|
|
714
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
715
|
+
.with_leader_receiver(receiver)
|
|
716
|
+
.with_team_owner(owner)
|
|
717
|
+
.with_owner_epoch(owner_epoch);
|
|
718
|
+
crate::state::ownership::write_owner(&mut state, identity.team_id.as_str(), record);
|
|
705
719
|
crate::state::persist::save_runtime_state(workspace, &state)?;
|
|
706
720
|
Ok(())
|
|
707
721
|
}
|
|
@@ -1351,16 +1365,15 @@ mod tests {
|
|
|
1351
1365
|
assert_eq!(outcome.status, crate::leader::LeaderLaunchStatus::Exited);
|
|
1352
1366
|
let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
1353
1367
|
assert_eq!(state["is_external_leader"], serde_json::json!(false));
|
|
1354
|
-
|
|
1355
|
-
assert_eq!(
|
|
1356
|
-
state["leader_receiver"]["tmux_socket"],
|
|
1357
|
-
serde_json::json!("/private/tmp/tmux-501/default")
|
|
1358
|
-
);
|
|
1359
|
-
assert_eq!(state["team_owner"]["pane_id"], serde_json::json!("%77"));
|
|
1368
|
+
// Stage 3d: canonical owner/receiver at teams.<team_key>.
|
|
1360
1369
|
assert_eq!(
|
|
1361
1370
|
state["teams"]["current"]["leader_receiver"]["pane_id"],
|
|
1362
1371
|
serde_json::json!("%77")
|
|
1363
1372
|
);
|
|
1373
|
+
assert_eq!(
|
|
1374
|
+
state["teams"]["current"]["leader_receiver"]["tmux_socket"],
|
|
1375
|
+
serde_json::json!("/private/tmp/tmux-501/default")
|
|
1376
|
+
);
|
|
1364
1377
|
assert_eq!(
|
|
1365
1378
|
state["teams"]["current"]["team_owner"]["pane_id"],
|
|
1366
1379
|
serde_json::json!("%77")
|
|
@@ -10,7 +10,12 @@ use super::*;
|
|
|
10
10
|
let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
11
11
|
&PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
|
|
12
12
|
assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
|
|
13
|
-
|
|
13
|
+
// Stage 3d (identity-boundary unified plan, architect direction
|
|
14
|
+
// 2026-06-23): canonical owner now at teams.<team_key>.team_owner.
|
|
15
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
16
|
+
let owner = state["teams"][&team_key]["team_owner"]
|
|
17
|
+
.as_object()
|
|
18
|
+
.expect("Stage 3d: team_owner at canonical teams.<team_key>");
|
|
14
19
|
assert!(owner.contains_key("os_user"),
|
|
15
20
|
"golden claim-path team_owner carries os_user even when empty; keys={:?}", owner.keys().collect::<Vec<_>>());
|
|
16
21
|
let keys: Vec<&str> = owner.keys().map(String::as_str).collect();
|
|
@@ -37,7 +42,11 @@ use super::*;
|
|
|
37
42
|
let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
|
|
38
43
|
&PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
|
|
39
44
|
assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
|
|
40
|
-
|
|
45
|
+
// Stage 3d: canonical receiver at teams.<team_key>.leader_receiver.
|
|
46
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
47
|
+
let recv = state["teams"][&team_key]["leader_receiver"]
|
|
48
|
+
.as_object()
|
|
49
|
+
.expect("Stage 3d: leader_receiver at canonical teams.<team_key>");
|
|
41
50
|
for extra in ["fingerprint", "requested_provider", "warning"] {
|
|
42
51
|
assert!(!recv.contains_key(extra),
|
|
43
52
|
"golden leader_receiver has NO '{extra}' key (Rust must add skip_serializing_if); keys={:?}", recv.keys().collect::<Vec<_>>());
|