@team-agent/installer 0.5.8 → 0.5.9
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/attach_app_server_leader.rs +82 -1
- package/crates/team-agent/src/cli/emit.rs +30 -1
- package/crates/team-agent/src/cli/leaders.rs +94 -0
- package/crates/team-agent/src/cli/mod.rs +138 -2
- package/crates/team-agent/src/cli/named_address.rs +234 -34
- package/crates/team-agent/src/cli/send.rs +230 -0
- package/crates/team-agent/src/cli/status_port.rs +20 -6
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +14 -0
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/registry.rs +402 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +79 -0
- package/crates/team-agent/src/mcp_server/tools.rs +8 -1
- package/crates/team-agent/src/messaging/leader_receiver.rs +58 -0
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +53 -0
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -5,12 +5,93 @@ use super::*;
|
|
|
5
5
|
pub fn cmd_attach_app_server_leader(
|
|
6
6
|
args: &AttachAppServerLeaderArgs,
|
|
7
7
|
) -> Result<CmdResult, CliError> {
|
|
8
|
-
let value = crate::leader::attach_app_server_leader(
|
|
8
|
+
let mut value = crate::leader::attach_app_server_leader(
|
|
9
9
|
&args.workspace,
|
|
10
10
|
args.team.as_deref(),
|
|
11
11
|
&args.socket,
|
|
12
12
|
&args.thread_id,
|
|
13
13
|
)
|
|
14
14
|
.map_err(|error| CliError::Runtime(error.to_string()))?;
|
|
15
|
+
// E7 register-after-success (host-leader-registry-design §8.4): mirror
|
|
16
|
+
// the tmux binding flow so app-server bindings appear in the host
|
|
17
|
+
// discovery index too. Registry write failure is discoverability
|
|
18
|
+
// degradation, not binding failure.
|
|
19
|
+
if value.get("ok").and_then(serde_json::Value::as_bool) == Some(true) {
|
|
20
|
+
register_app_server_binding_in_registry(&args.workspace, args.team.as_deref(), &mut value);
|
|
21
|
+
}
|
|
15
22
|
Ok(CmdResult::from_json(value, args.json))
|
|
16
23
|
}
|
|
24
|
+
|
|
25
|
+
fn register_app_server_binding_in_registry(
|
|
26
|
+
workspace: &std::path::Path,
|
|
27
|
+
team: Option<&str>,
|
|
28
|
+
response: &mut serde_json::Value,
|
|
29
|
+
) {
|
|
30
|
+
let Ok(state) = crate::state::persist::load_runtime_state(workspace) else {
|
|
31
|
+
return;
|
|
32
|
+
};
|
|
33
|
+
let team_key = match team.filter(|t| !t.is_empty()) {
|
|
34
|
+
Some(t) => t.to_string(),
|
|
35
|
+
None => crate::state::projection::team_state_key(&state),
|
|
36
|
+
};
|
|
37
|
+
let receiver = state
|
|
38
|
+
.get("teams")
|
|
39
|
+
.and_then(|v| v.as_object())
|
|
40
|
+
.and_then(|teams| teams.get(&team_key))
|
|
41
|
+
.and_then(|t| t.get("leader_receiver"))
|
|
42
|
+
.cloned();
|
|
43
|
+
let Some(receiver) = receiver else {
|
|
44
|
+
return;
|
|
45
|
+
};
|
|
46
|
+
let transport_kind = receiver
|
|
47
|
+
.get("transport_kind")
|
|
48
|
+
.and_then(serde_json::Value::as_str)
|
|
49
|
+
.unwrap_or("codex_app_server")
|
|
50
|
+
.to_string();
|
|
51
|
+
let channel = receiver
|
|
52
|
+
.get("app_server")
|
|
53
|
+
.cloned()
|
|
54
|
+
.unwrap_or(serde_json::Value::Null);
|
|
55
|
+
let owner_epoch = receiver
|
|
56
|
+
.get("owner_epoch")
|
|
57
|
+
.and_then(serde_json::Value::as_u64)
|
|
58
|
+
.unwrap_or(0);
|
|
59
|
+
let entry = crate::leader::registry::build_entry(
|
|
60
|
+
workspace,
|
|
61
|
+
&team_key,
|
|
62
|
+
&transport_kind,
|
|
63
|
+
channel,
|
|
64
|
+
owner_epoch,
|
|
65
|
+
"attach-app-server-leader",
|
|
66
|
+
chrono::Utc::now().to_rfc3339(),
|
|
67
|
+
);
|
|
68
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
69
|
+
let write_result = crate::leader::registry::write_entry_best_effort(&entry);
|
|
70
|
+
let registry_status = match &write_result {
|
|
71
|
+
Some(path) => {
|
|
72
|
+
let _ = event_log.write(
|
|
73
|
+
crate::leader::registry::EVENT_REGISTERED,
|
|
74
|
+
serde_json::json!({
|
|
75
|
+
"path": path.display().to_string(),
|
|
76
|
+
"team_key": team_key,
|
|
77
|
+
"workspace_hash": entry.workspace_hash,
|
|
78
|
+
"source": "attach-app-server-leader",
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
serde_json::json!({"status": "registered", "path": path.display().to_string()})
|
|
82
|
+
}
|
|
83
|
+
None => {
|
|
84
|
+
let _ = event_log.write(
|
|
85
|
+
crate::leader::registry::EVENT_WRITE_FAILED,
|
|
86
|
+
serde_json::json!({
|
|
87
|
+
"team_key": team_key,
|
|
88
|
+
"source": "attach-app-server-leader",
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
serde_json::json!({"status": "write_failed"})
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
if let Some(obj) = response.as_object_mut() {
|
|
95
|
+
obj.insert("leader_registry".to_string(), registry_status);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -122,6 +122,14 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
122
122
|
"doctor" => cmd_doctor(&doctor_args(args, cwd)).map(emit_result),
|
|
123
123
|
"watch" => cmd_watch(&watch_args(args, cwd)).map(emit_result),
|
|
124
124
|
"sessions" => cmd_sessions(&sessions_args(args, cwd)).map(emit_result),
|
|
125
|
+
// 0.5.9 E7 host-leader-registry: `leaders` is the host-level derived
|
|
126
|
+
// discovery command. It reads ~/.team-agent/leaders, validates each
|
|
127
|
+
// entry against canonical state, and reports LIVE/STALE/AMBIGUOUS
|
|
128
|
+
// status. `--to-leader NAME` on `send` uses the same registry to
|
|
129
|
+
// resolve short/qualified/hash-qualified names to a canonical
|
|
130
|
+
// (workspace, team_key) tuple and delegates to the E6 named-leader
|
|
131
|
+
// delivery path — no separate route authority.
|
|
132
|
+
"leaders" => cmd_leaders(&leaders_args(args, cwd)).map(emit_result),
|
|
125
133
|
"validate" => cmd_validate(&validate_args(args, cwd)).map(emit_result),
|
|
126
134
|
"install-skill" => cmd_install_skill(&install_skill_args(args)?).map(emit_result),
|
|
127
135
|
"profile" => cmd_profile(&profile_args(args, cwd)?).map(emit_result),
|
|
@@ -178,6 +186,8 @@ const DISPATCH_COMMANDS: &[&str] = &[
|
|
|
178
186
|
"doctor",
|
|
179
187
|
"watch",
|
|
180
188
|
"sessions",
|
|
189
|
+
// 0.5.9 E7: host leader discovery command surface.
|
|
190
|
+
"leaders",
|
|
181
191
|
"validate",
|
|
182
192
|
"install-skill",
|
|
183
193
|
"profile",
|
|
@@ -692,6 +702,11 @@ struct ParsedArgs {
|
|
|
692
702
|
auth_mode: Option<String>,
|
|
693
703
|
pane: Option<String>,
|
|
694
704
|
to_name: Option<String>,
|
|
705
|
+
/// E7 (0.5.9 host-leader-registry-design §4.2): `send --to-leader NAME`
|
|
706
|
+
/// resolves NAME through `~/.team-agent/leaders` to a canonical target
|
|
707
|
+
/// (workspace, team_key) then delegates to the E6 leader delivery path.
|
|
708
|
+
/// Mutually exclusive with `--to-name`, TARGET, `--pane`, and `--to`.
|
|
709
|
+
to_leader: Option<String>,
|
|
695
710
|
provider: Option<String>,
|
|
696
711
|
socket: Option<String>,
|
|
697
712
|
thread_id: Option<String>,
|
|
@@ -782,6 +797,7 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
782
797
|
"--auth-mode" => parsed.auth_mode = next_arg(args, &mut i),
|
|
783
798
|
"--pane" => parsed.pane = next_arg(args, &mut i),
|
|
784
799
|
"--to-name" => parsed.to_name = next_arg(args, &mut i),
|
|
800
|
+
"--to-leader" => parsed.to_leader = next_arg(args, &mut i),
|
|
785
801
|
"--provider" => parsed.provider = next_arg(args, &mut i),
|
|
786
802
|
"--socket" => parsed.socket = next_arg(args, &mut i),
|
|
787
803
|
"--thread-id" => parsed.thread_id = next_arg(args, &mut i),
|
|
@@ -799,6 +815,9 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
799
815
|
other if other.starts_with("--to-name=") => {
|
|
800
816
|
parsed.to_name = Some(other.trim_start_matches("--to-name=").to_string());
|
|
801
817
|
}
|
|
818
|
+
other if other.starts_with("--to-leader=") => {
|
|
819
|
+
parsed.to_leader = Some(other.trim_start_matches("--to-leader=").to_string());
|
|
820
|
+
}
|
|
802
821
|
other if other.starts_with("--provider=") => {
|
|
803
822
|
parsed.provider = Some(other.trim_start_matches("--provider=").to_string());
|
|
804
823
|
}
|
|
@@ -933,7 +952,11 @@ fn resolve_cli_path(cwd: &Path, path: &Path) -> PathBuf {
|
|
|
933
952
|
|
|
934
953
|
fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
935
954
|
let parsed = parse_args(args);
|
|
936
|
-
let target = if parsed.targets.is_some()
|
|
955
|
+
let target = if parsed.targets.is_some()
|
|
956
|
+
|| parsed.pane.is_some()
|
|
957
|
+
|| parsed.to_name.is_some()
|
|
958
|
+
|| parsed.to_leader.is_some()
|
|
959
|
+
{
|
|
937
960
|
None
|
|
938
961
|
} else {
|
|
939
962
|
parsed.positionals.first().cloned()
|
|
@@ -962,6 +985,7 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
|
962
985
|
message_id: parsed.message_id,
|
|
963
986
|
pane: parsed.pane.clone(),
|
|
964
987
|
to_name: parsed.to_name.clone(),
|
|
988
|
+
to_leader: parsed.to_leader.clone(),
|
|
965
989
|
})
|
|
966
990
|
}
|
|
967
991
|
|
|
@@ -1351,6 +1375,11 @@ fn sessions_args(args: &[String], cwd: &Path) -> SessionsArgs {
|
|
|
1351
1375
|
}
|
|
1352
1376
|
}
|
|
1353
1377
|
|
|
1378
|
+
fn leaders_args(args: &[String], _cwd: &Path) -> LeadersArgs {
|
|
1379
|
+
let parsed = parse_args(args);
|
|
1380
|
+
LeadersArgs { json: parsed.json }
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1354
1383
|
fn validate_args(args: &[String], cwd: &Path) -> ValidateArgs {
|
|
1355
1384
|
let parsed = parse_args(args);
|
|
1356
1385
|
ValidateArgs {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//! E7 (0.5.9 host-leader-registry-design §4.1): `team-agent leaders` command.
|
|
2
|
+
//!
|
|
3
|
+
//! Enumerates entries under `~/.team-agent/leaders`, re-validates each
|
|
4
|
+
//! against the target workspace's canonical runtime state, and emits a
|
|
5
|
+
//! LIVE / STALE / AMBIGUOUS classification. Leaders-only — worker rows,
|
|
6
|
+
//! task rows, and result rows are deliberately absent from the output.
|
|
7
|
+
//!
|
|
8
|
+
//! The command is a **discovery** surface, not a route authority. Callers
|
|
9
|
+
//! that consume `send_hint` must still re-validate via
|
|
10
|
+
//! `named_address::resolve_name_for_cli` before delivery — this file only
|
|
11
|
+
//! reports what registry sees now.
|
|
12
|
+
|
|
13
|
+
use super::{types::LeadersArgs, CliError, CmdResult};
|
|
14
|
+
use serde_json::{json, Value};
|
|
15
|
+
|
|
16
|
+
/// E7 CLI entry point (E7 test 2 marker: `cmd_leaders`).
|
|
17
|
+
///
|
|
18
|
+
/// Reads registry entries, canonical-validates each, and groups by
|
|
19
|
+
/// delivery_name to classify each as LIVE / STALE / AMBIGUOUS. Terminal
|
|
20
|
+
/// stale entries (canonical workspace/team gone) are pruned as GC side
|
|
21
|
+
/// effect. Leader-detached-but-team-alive stays visible as STALE so the
|
|
22
|
+
/// operator can decide.
|
|
23
|
+
pub fn cmd_leaders(args: &LeadersArgs) -> Result<CmdResult, CliError> {
|
|
24
|
+
let dir = crate::leader::registry::registry_dir()
|
|
25
|
+
.map(|p| p.display().to_string())
|
|
26
|
+
.unwrap_or_else(|| "~/.team-agent/leaders".to_string());
|
|
27
|
+
let classified = crate::leader::registry::list_validated_with_gc();
|
|
28
|
+
let mut name_counts: std::collections::HashMap<String, Vec<Value>> =
|
|
29
|
+
std::collections::HashMap::new();
|
|
30
|
+
for (entry, _status, _reason) in &classified {
|
|
31
|
+
name_counts
|
|
32
|
+
.entry(entry.delivery_name.clone())
|
|
33
|
+
.or_default()
|
|
34
|
+
.push(json!({
|
|
35
|
+
"name": entry.qualified_name,
|
|
36
|
+
"workspace": entry.workspace.display().to_string(),
|
|
37
|
+
"team_key": entry.team_key,
|
|
38
|
+
"workspace_hash": entry.workspace_hash,
|
|
39
|
+
"stable_qualified_name": entry.stable_qualified_name,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
let ambiguous_map: std::collections::HashMap<String, Vec<Value>> = name_counts
|
|
43
|
+
.iter()
|
|
44
|
+
.filter(|(_, list)| list.len() > 1)
|
|
45
|
+
.map(|(k, v)| (k.clone(), v.clone()))
|
|
46
|
+
.collect();
|
|
47
|
+
let mut leaders: Vec<Value> = Vec::new();
|
|
48
|
+
for (entry, status, reason) in &classified {
|
|
49
|
+
let final_status = if ambiguous_map.contains_key(&entry.delivery_name) {
|
|
50
|
+
"AMBIGUOUS"
|
|
51
|
+
} else {
|
|
52
|
+
*status
|
|
53
|
+
};
|
|
54
|
+
let send_hint = format!(
|
|
55
|
+
"team-agent send --to-leader {} MESSAGE",
|
|
56
|
+
entry.qualified_name
|
|
57
|
+
);
|
|
58
|
+
leaders.push(json!({
|
|
59
|
+
"name": entry.delivery_name,
|
|
60
|
+
"delivery_name": entry.delivery_name,
|
|
61
|
+
"qualified_name": entry.qualified_name,
|
|
62
|
+
"stable_qualified_name": entry.stable_qualified_name,
|
|
63
|
+
"workspace": entry.workspace.display().to_string(),
|
|
64
|
+
"workspace_hash": entry.workspace_hash,
|
|
65
|
+
"workspace_short": entry.workspace_short,
|
|
66
|
+
"team_key": entry.team_key,
|
|
67
|
+
"transport_kind": entry.transport_kind,
|
|
68
|
+
"owner_epoch": entry.owner_epoch,
|
|
69
|
+
"status": final_status,
|
|
70
|
+
"stale_reason": reason.clone(),
|
|
71
|
+
"send_hint": send_hint,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
let ambiguous_names: Vec<Value> = ambiguous_map
|
|
75
|
+
.into_iter()
|
|
76
|
+
.map(|(name, candidates)| {
|
|
77
|
+
json!({
|
|
78
|
+
"name": name,
|
|
79
|
+
"candidates": candidates,
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
.collect();
|
|
83
|
+
let value = json!({
|
|
84
|
+
"ok": true,
|
|
85
|
+
"registry_dir": dir,
|
|
86
|
+
"leaders": leaders,
|
|
87
|
+
"ambiguous_names": ambiguous_names,
|
|
88
|
+
// Status vocabulary pinned by E7 RED: LIVE / STALE / AMBIGUOUS.
|
|
89
|
+
// "stale_reason" is null for LIVE and carries a machine-readable
|
|
90
|
+
// reason for STALE/AMBIGUOUS entries.
|
|
91
|
+
"status_wire_values": ["LIVE", "STALE", "AMBIGUOUS"],
|
|
92
|
+
});
|
|
93
|
+
Ok(CmdResult::from_json(value, args.json))
|
|
94
|
+
}
|
|
@@ -58,6 +58,7 @@ pub mod diagnose;
|
|
|
58
58
|
pub mod emit;
|
|
59
59
|
pub mod helpers;
|
|
60
60
|
pub mod leader;
|
|
61
|
+
pub mod leaders;
|
|
61
62
|
pub mod named_address;
|
|
62
63
|
pub mod profile;
|
|
63
64
|
pub mod send;
|
|
@@ -69,6 +70,7 @@ pub use attach_app_server_leader::*;
|
|
|
69
70
|
pub use diagnose::*;
|
|
70
71
|
pub use emit::*;
|
|
71
72
|
pub use leader::*;
|
|
73
|
+
pub use leaders::*;
|
|
72
74
|
pub use named_address::*;
|
|
73
75
|
pub use profile::*;
|
|
74
76
|
pub use send::*;
|
|
@@ -989,6 +991,14 @@ pub mod lifecycle_port {
|
|
|
989
991
|
{
|
|
990
992
|
let _ = &run_workspace;
|
|
991
993
|
}
|
|
994
|
+
// E7 unregister-after-success (host-leader-registry-design §14 step 6):
|
|
995
|
+
// Only prune the derived registry entry when the canonical shutdown
|
|
996
|
+
// succeeded and there was no dirty_state / verification degradation.
|
|
997
|
+
// Failed/degraded shutdowns leave the entry STALE so operators can
|
|
998
|
+
// decide.
|
|
999
|
+
if ok && !verification_degraded && !probe_degraded {
|
|
1000
|
+
let _ = crate::cli::leader_port::unregister_after_shutdown_success(&run_workspace, team);
|
|
1001
|
+
}
|
|
992
1002
|
Ok(response)
|
|
993
1003
|
}
|
|
994
1004
|
|
|
@@ -3939,7 +3949,11 @@ pub mod leader_port {
|
|
|
3939
3949
|
}
|
|
3940
3950
|
let result = crate::leader::claim_leader(workspace, team, true)
|
|
3941
3951
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
3942
|
-
|
|
3952
|
+
let mut value = lease_value(result);
|
|
3953
|
+
if value.get("ok").and_then(Value::as_bool) == Some(true) {
|
|
3954
|
+
register_after_binding_success(workspace, team, "takeover", &mut value);
|
|
3955
|
+
}
|
|
3956
|
+
Ok(value)
|
|
3943
3957
|
}
|
|
3944
3958
|
/// `runtime.claim_leader(...)` 的 CLI `--json` 投影(`cmd_claim_leader`;含 inbox_hint)。
|
|
3945
3959
|
pub fn claim_leader(
|
|
@@ -3968,7 +3982,11 @@ pub mod leader_port {
|
|
|
3968
3982
|
}
|
|
3969
3983
|
let result = crate::leader::claim_leader(workspace, team, confirm)
|
|
3970
3984
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
3971
|
-
|
|
3985
|
+
let mut value = lease_value(result);
|
|
3986
|
+
if value.get("ok").and_then(Value::as_bool) == Some(true) {
|
|
3987
|
+
register_after_binding_success(workspace, team, "claim-leader", &mut value);
|
|
3988
|
+
}
|
|
3989
|
+
Ok(value)
|
|
3972
3990
|
}
|
|
3973
3991
|
|
|
3974
3992
|
/// `runtime.attach_leader(...)` 的 CLI `--json` 投影。
|
|
@@ -3990,6 +4008,11 @@ pub mod leader_port {
|
|
|
3990
4008
|
obj.insert("team_key".to_string(), json!(team));
|
|
3991
4009
|
}
|
|
3992
4010
|
}
|
|
4011
|
+
// E7 register-after-success: canonical lease write already
|
|
4012
|
+
// finished inside `crate::leader::attach_leader`.
|
|
4013
|
+
if value.get("ok").and_then(Value::as_bool) == Some(true) {
|
|
4014
|
+
register_after_binding_success(workspace, team, "attach-leader", &mut value);
|
|
4015
|
+
}
|
|
3993
4016
|
Ok(value)
|
|
3994
4017
|
}
|
|
3995
4018
|
|
|
@@ -3999,6 +4022,119 @@ pub mod leader_port {
|
|
|
3999
4022
|
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
4000
4023
|
}
|
|
4001
4024
|
|
|
4025
|
+
/// E7 (0.5.9 host-leader-registry-design §14 step 3): after a
|
|
4026
|
+
/// canonical binding command succeeds, write a derived discovery
|
|
4027
|
+
/// entry to `~/.team-agent/leaders`. Registry write failure is
|
|
4028
|
+
/// **never** a binding failure — we only append `leader_registry`
|
|
4029
|
+
/// status to the JSON response so operators can see the degradation.
|
|
4030
|
+
///
|
|
4031
|
+
/// The receiver payload is read from the JUST-persisted runtime
|
|
4032
|
+
/// state so we capture the canonical fields (pane_id, socket,
|
|
4033
|
+
/// owner_epoch) after the lease writers finished.
|
|
4034
|
+
fn register_after_binding_success(
|
|
4035
|
+
workspace: &Path,
|
|
4036
|
+
team: Option<&str>,
|
|
4037
|
+
source: &str,
|
|
4038
|
+
response: &mut Value,
|
|
4039
|
+
) {
|
|
4040
|
+
let Ok(state) = crate::state::persist::load_runtime_state(workspace) else {
|
|
4041
|
+
return;
|
|
4042
|
+
};
|
|
4043
|
+
let team_key = match team.filter(|t| !t.is_empty()) {
|
|
4044
|
+
Some(t) => t.to_string(),
|
|
4045
|
+
None => crate::state::projection::team_state_key(&state),
|
|
4046
|
+
};
|
|
4047
|
+
let receiver = state
|
|
4048
|
+
.get("teams")
|
|
4049
|
+
.and_then(|v| v.as_object())
|
|
4050
|
+
.and_then(|teams| teams.get(&team_key))
|
|
4051
|
+
.and_then(|t| t.get("leader_receiver"))
|
|
4052
|
+
.or_else(|| state.get("leader_receiver"))
|
|
4053
|
+
.cloned();
|
|
4054
|
+
let Some(receiver) = receiver else {
|
|
4055
|
+
return;
|
|
4056
|
+
};
|
|
4057
|
+
let transport_kind = receiver
|
|
4058
|
+
.get("transport_kind")
|
|
4059
|
+
.and_then(Value::as_str)
|
|
4060
|
+
.unwrap_or("direct_tmux")
|
|
4061
|
+
.to_string();
|
|
4062
|
+
let owner_epoch = receiver
|
|
4063
|
+
.get("owner_epoch")
|
|
4064
|
+
.and_then(Value::as_u64)
|
|
4065
|
+
.or_else(|| {
|
|
4066
|
+
state
|
|
4067
|
+
.get("teams")
|
|
4068
|
+
.and_then(|v| v.as_object())
|
|
4069
|
+
.and_then(|teams| teams.get(&team_key))
|
|
4070
|
+
.and_then(|t| t.get("owner_epoch"))
|
|
4071
|
+
.and_then(Value::as_u64)
|
|
4072
|
+
})
|
|
4073
|
+
.unwrap_or(0);
|
|
4074
|
+
let entry = crate::leader::registry::build_entry(
|
|
4075
|
+
workspace,
|
|
4076
|
+
&team_key,
|
|
4077
|
+
&transport_kind,
|
|
4078
|
+
receiver,
|
|
4079
|
+
owner_epoch,
|
|
4080
|
+
source,
|
|
4081
|
+
chrono::Utc::now().to_rfc3339(),
|
|
4082
|
+
);
|
|
4083
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
4084
|
+
let write_result = crate::leader::registry::write_entry_best_effort(&entry);
|
|
4085
|
+
let registry_status = match &write_result {
|
|
4086
|
+
Some(path) => {
|
|
4087
|
+
let _ = event_log.write(
|
|
4088
|
+
crate::leader::registry::EVENT_REGISTERED,
|
|
4089
|
+
json!({
|
|
4090
|
+
"path": path.display().to_string(),
|
|
4091
|
+
"team_key": team_key,
|
|
4092
|
+
"workspace_hash": entry.workspace_hash,
|
|
4093
|
+
"source": source,
|
|
4094
|
+
"owner_epoch": entry.owner_epoch,
|
|
4095
|
+
}),
|
|
4096
|
+
);
|
|
4097
|
+
json!({"status": "registered", "path": path.display().to_string()})
|
|
4098
|
+
}
|
|
4099
|
+
None => {
|
|
4100
|
+
let _ = event_log.write(
|
|
4101
|
+
crate::leader::registry::EVENT_WRITE_FAILED,
|
|
4102
|
+
json!({
|
|
4103
|
+
"team_key": team_key,
|
|
4104
|
+
"workspace_hash": entry.workspace_hash,
|
|
4105
|
+
"source": source,
|
|
4106
|
+
}),
|
|
4107
|
+
);
|
|
4108
|
+
json!({"status": "write_failed"})
|
|
4109
|
+
}
|
|
4110
|
+
};
|
|
4111
|
+
if let Some(obj) = response.as_object_mut() {
|
|
4112
|
+
obj.insert("leader_registry".to_string(), registry_status);
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
/// E7 GC hook: called from shutdown/unbind success paths.
|
|
4117
|
+
pub(crate) fn unregister_after_shutdown_success(
|
|
4118
|
+
workspace: &Path,
|
|
4119
|
+
team: Option<&str>,
|
|
4120
|
+
) -> Option<PathBuf> {
|
|
4121
|
+
let team_key = team.filter(|t| !t.is_empty()).map(str::to_string).or_else(|| {
|
|
4122
|
+
crate::state::persist::load_runtime_state(workspace)
|
|
4123
|
+
.ok()
|
|
4124
|
+
.map(|s| crate::state::projection::team_state_key(&s))
|
|
4125
|
+
})?;
|
|
4126
|
+
let path = crate::leader::registry::unregister_entry(workspace, &team_key)?;
|
|
4127
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
4128
|
+
let _ = event_log.write(
|
|
4129
|
+
crate::leader::registry::EVENT_UNREGISTERED,
|
|
4130
|
+
json!({
|
|
4131
|
+
"path": path.display().to_string(),
|
|
4132
|
+
"team_key": team_key,
|
|
4133
|
+
}),
|
|
4134
|
+
);
|
|
4135
|
+
Some(path)
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4002
4138
|
fn owner_bind_value(result: crate::leader::OwnerBindResult) -> Value {
|
|
4003
4139
|
json!({
|
|
4004
4140
|
"ok": result.ok,
|