@team-agent/installer 0.5.0 → 0.5.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 +118 -9
- package/Cargo.toml +2 -2
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/app_server_test_support.rs +258 -0
- package/crates/team-agent/src/cli/adapters.rs +32 -3
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
- package/crates/team-agent/src/cli/diagnose.rs +14 -5
- package/crates/team-agent/src/cli/emit.rs +111 -3
- package/crates/team-agent/src/cli/mod.rs +72 -25
- package/crates/team-agent/src/cli/named_address.rs +975 -0
- package/crates/team-agent/src/cli/send.rs +200 -1
- package/crates/team-agent/src/cli/status_port.rs +44 -6
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +586 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +21 -0
- package/crates/team-agent/src/codex_app_server.rs +488 -0
- package/crates/team-agent/src/conpty/backend.rs +710 -0
- package/crates/team-agent/src/conpty/mod.rs +32 -0
- package/crates/team-agent/src/coordinator/backoff.rs +45 -6
- package/crates/team-agent/src/diagnose/orphans.rs +11 -3
- package/crates/team-agent/src/leader/lease.rs +144 -7
- package/crates/team-agent/src/leader/owner_bind.rs +5 -2
- package/crates/team-agent/src/leader/start.rs +18 -5
- package/crates/team-agent/src/leader/tests/lease_api.rs +172 -0
- package/crates/team-agent/src/lib.rs +23 -0
- package/crates/team-agent/src/lifecycle/launch.rs +127 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +90 -0
- package/crates/team-agent/src/messaging/delivery.rs +329 -9
- package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +78 -21
- package/crates/team-agent/src/messaging/tests/runtime.rs +232 -0
- package/crates/team-agent/src/tmux_backend.rs +31 -0
- package/crates/team-agent/src/transport_factory.rs +632 -0
- package/package.json +4 -4
|
@@ -724,6 +724,19 @@ fn report_result_for_owner_team_inner(
|
|
|
724
724
|
&event_log,
|
|
725
725
|
) {
|
|
726
726
|
Ok(outcome) => outcome,
|
|
727
|
+
Err(error) if report_state_has_app_server_receiver(&state) => {
|
|
728
|
+
let message_id = format!("fallback:{result_id}");
|
|
729
|
+
crate::messaging::DeliveryOutcome {
|
|
730
|
+
ok: false,
|
|
731
|
+
status: crate::messaging::DeliveryStatus::Failed,
|
|
732
|
+
message_status: super::helpers::MessageStatusShadow("failed".to_string()),
|
|
733
|
+
message_id: Some(message_id),
|
|
734
|
+
verification: Some(format!("leader_funnel_error:{error}")),
|
|
735
|
+
stage: None,
|
|
736
|
+
reason: Some(crate::messaging::DeliveryRefusal::LeaderNotAttached),
|
|
737
|
+
channel: Some("codex_app_server".to_string()),
|
|
738
|
+
}
|
|
739
|
+
}
|
|
727
740
|
Err(error) => {
|
|
728
741
|
let message_id = format!("fallback:{result_id}");
|
|
729
742
|
let fallback_error = fallback_primary_error_text(
|
|
@@ -744,17 +757,32 @@ fn report_result_for_owner_team_inner(
|
|
|
744
757
|
};
|
|
745
758
|
if let Some(message_id) = outcome.message_id.clone() {
|
|
746
759
|
let store = MessageStore::open(workspace)?;
|
|
747
|
-
|
|
760
|
+
// 0.5.x Phase 1d Batch 4: report_result retry uses the runtime
|
|
761
|
+
// factory transport so a conpty team's report notification
|
|
762
|
+
// goes to the shim (design §Batch 4). Previously we always
|
|
763
|
+
// built `TmuxBackend::for_workspace`, which for a conpty team
|
|
764
|
+
// silently no-op'd the retry. Factory refusal falls back to
|
|
765
|
+
// tmux workspace (byte-equivalent tmux teams; honest failure
|
|
766
|
+
// for conpty teams without a resolvable factory input).
|
|
748
767
|
let delivery_state_raw = crate::state::persist::load_runtime_state(workspace)
|
|
749
768
|
.unwrap_or_else(|_| state_for_owner.clone());
|
|
750
769
|
let delivery_state = report_owner_state(&delivery_state_raw, &owner_team);
|
|
770
|
+
let transport: Box<dyn crate::transport::Transport> =
|
|
771
|
+
match crate::transport_factory::resolve_read_only_transport(
|
|
772
|
+
workspace,
|
|
773
|
+
Some(&delivery_state),
|
|
774
|
+
crate::transport_factory::TransportPurpose::MessageDelivery,
|
|
775
|
+
) {
|
|
776
|
+
Ok(r) => r.backend,
|
|
777
|
+
Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(workspace)),
|
|
778
|
+
};
|
|
751
779
|
let mut primary_error: Option<String> = None;
|
|
752
780
|
for attempt in 0..3 {
|
|
753
781
|
let _ = store.mark(&message_id, "accepted", None);
|
|
754
782
|
match super::delivery::deliver_pending_message(
|
|
755
783
|
workspace,
|
|
756
784
|
&store,
|
|
757
|
-
|
|
785
|
+
transport.as_ref(),
|
|
758
786
|
&message_id,
|
|
759
787
|
&event_log,
|
|
760
788
|
&delivery_state,
|
|
@@ -781,7 +809,7 @@ fn report_result_for_owner_team_inner(
|
|
|
781
809
|
match super::delivery::deliver_pending_messages(
|
|
782
810
|
workspace,
|
|
783
811
|
&delivery_state,
|
|
784
|
-
|
|
812
|
+
transport.as_ref(),
|
|
785
813
|
&event_log,
|
|
786
814
|
) {
|
|
787
815
|
Ok(delivered)
|
|
@@ -824,24 +852,26 @@ fn report_result_for_owner_team_inner(
|
|
|
824
852
|
}
|
|
825
853
|
}
|
|
826
854
|
if !outcome.ok {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
855
|
+
if !report_state_has_app_server_receiver(&delivery_state) {
|
|
856
|
+
let fallback_error = primary_error.unwrap_or_else(|| {
|
|
857
|
+
format!(
|
|
858
|
+
"leader_notification_primary_failed:{}",
|
|
859
|
+
super::helpers::status_wire(outcome.status)
|
|
860
|
+
)
|
|
861
|
+
});
|
|
862
|
+
let fallback_error =
|
|
863
|
+
fallback_primary_error_text(fallback_primary_error, fallback_error);
|
|
864
|
+
outcome = super::leader_receiver::deliver_to_leader_fallback_pane(
|
|
865
|
+
workspace,
|
|
866
|
+
&delivery_state,
|
|
867
|
+
&message_id,
|
|
868
|
+
Some(&result_id),
|
|
869
|
+
&content,
|
|
870
|
+
false,
|
|
871
|
+
Some(&fallback_error),
|
|
872
|
+
&event_log,
|
|
873
|
+
)?;
|
|
874
|
+
}
|
|
845
875
|
}
|
|
846
876
|
}
|
|
847
877
|
let leader_notified = outcome.ok;
|
|
@@ -937,6 +967,33 @@ fn report_owner_state(state: &serde_json::Value, owner_team: &str) -> serde_json
|
|
|
937
967
|
state
|
|
938
968
|
}
|
|
939
969
|
|
|
970
|
+
fn report_state_has_app_server_receiver(state: &serde_json::Value) -> bool {
|
|
971
|
+
report_leader_receiver_value(state).is_some_and(crate::codex_app_server::receiver_is_app_server)
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
fn report_leader_receiver_value(state: &serde_json::Value) -> Option<&serde_json::Value> {
|
|
975
|
+
state
|
|
976
|
+
.get("leader_receiver")
|
|
977
|
+
.or_else(|| {
|
|
978
|
+
state
|
|
979
|
+
.get("active_team_key")
|
|
980
|
+
.and_then(serde_json::Value::as_str)
|
|
981
|
+
.and_then(|team| state.get("teams").and_then(|teams| teams.get(team)))
|
|
982
|
+
.and_then(|team| team.get("leader_receiver"))
|
|
983
|
+
})
|
|
984
|
+
.or_else(|| {
|
|
985
|
+
let teams = state.get("teams").and_then(serde_json::Value::as_object)?;
|
|
986
|
+
if teams.len() == 1 {
|
|
987
|
+
teams
|
|
988
|
+
.values()
|
|
989
|
+
.next()
|
|
990
|
+
.and_then(|team| team.get("leader_receiver"))
|
|
991
|
+
} else {
|
|
992
|
+
None
|
|
993
|
+
}
|
|
994
|
+
})
|
|
995
|
+
}
|
|
996
|
+
|
|
940
997
|
fn insert_result_if_absent(
|
|
941
998
|
conn: &rusqlite::Connection,
|
|
942
999
|
result_id: &str,
|
|
@@ -1894,6 +1894,238 @@ fn e51_delivery_keeps_same_socket_pane_conflict_guard() {
|
|
|
1894
1894
|
);
|
|
1895
1895
|
}
|
|
1896
1896
|
|
|
1897
|
+
fn app_server_state(
|
|
1898
|
+
ws: &Path,
|
|
1899
|
+
endpoint: &str,
|
|
1900
|
+
thread_id: &str,
|
|
1901
|
+
session_id: &str,
|
|
1902
|
+
) -> serde_json::Value {
|
|
1903
|
+
serde_json::json!({
|
|
1904
|
+
"active_team_key": "team-a",
|
|
1905
|
+
"teams": {
|
|
1906
|
+
"team-a": {
|
|
1907
|
+
"team_key": "team-a",
|
|
1908
|
+
"agents": {},
|
|
1909
|
+
"leader_receiver": {
|
|
1910
|
+
"mode": "codex_app_server",
|
|
1911
|
+
"transport_kind": "codex_app_server",
|
|
1912
|
+
"status": "attached",
|
|
1913
|
+
"provider": "codex",
|
|
1914
|
+
"owner_epoch": 7,
|
|
1915
|
+
"app_server": {
|
|
1916
|
+
"socket": endpoint,
|
|
1917
|
+
"thread_id": thread_id,
|
|
1918
|
+
"session_id": session_id,
|
|
1919
|
+
"cwd": ws.to_string_lossy(),
|
|
1920
|
+
"cli_version": "codex-appserver-team-agent-test/0.139.0",
|
|
1921
|
+
"bound_at": "2026-07-05T00:00:00Z"
|
|
1922
|
+
}
|
|
1923
|
+
},
|
|
1924
|
+
"team_owner": {
|
|
1925
|
+
"provider": "codex",
|
|
1926
|
+
"transport_kind": "codex_app_server",
|
|
1927
|
+
"owner_epoch": 7
|
|
1928
|
+
},
|
|
1929
|
+
"owner_epoch": 7
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
})
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
#[test]
|
|
1936
|
+
fn app_server_leader_delivery_marks_delivered_without_tmux_transport() {
|
|
1937
|
+
let ws = tmp_ws("appdeliver");
|
|
1938
|
+
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
1939
|
+
"deliver-ok",
|
|
1940
|
+
crate::app_server_test_support::FakeAppServerScript::happy(
|
|
1941
|
+
"thread-live",
|
|
1942
|
+
"session-live",
|
|
1943
|
+
ws.to_str().unwrap(),
|
|
1944
|
+
),
|
|
1945
|
+
);
|
|
1946
|
+
let state = app_server_state(&ws, fake.endpoint(), "thread-live", "session-live");
|
|
1947
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
1948
|
+
let store = store_for(&ws);
|
|
1949
|
+
let log = EventLog::new(&ws);
|
|
1950
|
+
let message_id = store
|
|
1951
|
+
.create_message(
|
|
1952
|
+
None,
|
|
1953
|
+
"worker_a",
|
|
1954
|
+
"leader",
|
|
1955
|
+
"app server hello",
|
|
1956
|
+
None,
|
|
1957
|
+
false,
|
|
1958
|
+
Some("team-a"),
|
|
1959
|
+
)
|
|
1960
|
+
.unwrap();
|
|
1961
|
+
|
|
1962
|
+
let out = deliver_pending_message(&ws, &store, &NoopTransport, &message_id, &log, &state)
|
|
1963
|
+
.expect("app-server delivery should not require tmux transport");
|
|
1964
|
+
|
|
1965
|
+
assert!(
|
|
1966
|
+
out.ok,
|
|
1967
|
+
"app-server turn/start acceptance is delivery proof: {out:?}"
|
|
1968
|
+
);
|
|
1969
|
+
assert_eq!(out.message_status.0, "delivered");
|
|
1970
|
+
assert_eq!(out.channel.as_deref(), Some("codex_app_server"));
|
|
1971
|
+
let turns = fake.received_turns();
|
|
1972
|
+
assert_eq!(turns.len(), 1);
|
|
1973
|
+
assert_eq!(
|
|
1974
|
+
turns[0]["params"]["clientUserMessageId"],
|
|
1975
|
+
serde_json::json!(message_id)
|
|
1976
|
+
);
|
|
1977
|
+
let events = log.tail(0).unwrap();
|
|
1978
|
+
assert!(
|
|
1979
|
+
events.iter().any(|event| {
|
|
1980
|
+
event.get("event").and_then(serde_json::Value::as_str)
|
|
1981
|
+
== Some("leader_receiver.app_server_submitted")
|
|
1982
|
+
}),
|
|
1983
|
+
"delivery must emit app-server submission event; events={events:?}"
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
#[test]
|
|
1988
|
+
fn app_server_leader_delivery_stale_tuple_fails_closed_with_event() {
|
|
1989
|
+
let ws = tmp_ws("appstale");
|
|
1990
|
+
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
1991
|
+
"deliver-stale",
|
|
1992
|
+
crate::app_server_test_support::FakeAppServerScript::happy(
|
|
1993
|
+
"thread-live",
|
|
1994
|
+
"session-new",
|
|
1995
|
+
ws.to_str().unwrap(),
|
|
1996
|
+
),
|
|
1997
|
+
);
|
|
1998
|
+
let state = app_server_state(&ws, fake.endpoint(), "thread-live", "session-old");
|
|
1999
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
2000
|
+
let store = store_for(&ws);
|
|
2001
|
+
let log = EventLog::new(&ws);
|
|
2002
|
+
let message_id = store
|
|
2003
|
+
.create_message(
|
|
2004
|
+
None,
|
|
2005
|
+
"worker_a",
|
|
2006
|
+
"leader",
|
|
2007
|
+
"stale",
|
|
2008
|
+
None,
|
|
2009
|
+
false,
|
|
2010
|
+
Some("team-a"),
|
|
2011
|
+
)
|
|
2012
|
+
.unwrap();
|
|
2013
|
+
|
|
2014
|
+
let out = deliver_pending_message(&ws, &store, &NoopTransport, &message_id, &log, &state)
|
|
2015
|
+
.expect("stale app-server binding should be a business failure, not panic");
|
|
2016
|
+
|
|
2017
|
+
assert!(!out.ok);
|
|
2018
|
+
assert_eq!(out.channel.as_deref(), Some("rebind_required"));
|
|
2019
|
+
assert_eq!(out.message_status.0, "failed");
|
|
2020
|
+
let events = log.tail(0).unwrap();
|
|
2021
|
+
assert!(
|
|
2022
|
+
events.iter().any(|event| {
|
|
2023
|
+
event.get("event").and_then(serde_json::Value::as_str)
|
|
2024
|
+
== Some("leader_receiver.app_server_thread_stale")
|
|
2025
|
+
&& event.get("message_id").and_then(serde_json::Value::as_str)
|
|
2026
|
+
== Some(message_id.as_str())
|
|
2027
|
+
}),
|
|
2028
|
+
"stale app-server tuple must emit app_server_thread_stale; events={events:?}"
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
#[test]
|
|
2033
|
+
fn app_server_leader_delivery_fails_closed_on_mode_transport_conflict() {
|
|
2034
|
+
let ws = tmp_ws("appconflict");
|
|
2035
|
+
let mut state = app_server_state(
|
|
2036
|
+
&ws,
|
|
2037
|
+
"unix:///tmp/team-agent-should-not-connect.sock",
|
|
2038
|
+
"thread-live",
|
|
2039
|
+
"session-live",
|
|
2040
|
+
);
|
|
2041
|
+
state["teams"]["team-a"]["leader_receiver"]["mode"] = serde_json::json!("direct_tmux");
|
|
2042
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
2043
|
+
let store = store_for(&ws);
|
|
2044
|
+
let log = EventLog::new(&ws);
|
|
2045
|
+
let message_id = store
|
|
2046
|
+
.create_message(
|
|
2047
|
+
None,
|
|
2048
|
+
"worker_a",
|
|
2049
|
+
"leader",
|
|
2050
|
+
"conflict",
|
|
2051
|
+
None,
|
|
2052
|
+
false,
|
|
2053
|
+
Some("team-a"),
|
|
2054
|
+
)
|
|
2055
|
+
.unwrap();
|
|
2056
|
+
|
|
2057
|
+
let out = deliver_pending_message(&ws, &store, &NoopTransport, &message_id, &log, &state)
|
|
2058
|
+
.expect("transport conflict should be a business failure");
|
|
2059
|
+
|
|
2060
|
+
assert!(!out.ok);
|
|
2061
|
+
assert_eq!(out.channel.as_deref(), Some("rebind_required"));
|
|
2062
|
+
assert_eq!(out.message_status.0, "failed");
|
|
2063
|
+
assert_eq!(
|
|
2064
|
+
out.verification.as_deref(),
|
|
2065
|
+
Some("run team-agent claim-leader, takeover, or attach-app-server-leader")
|
|
2066
|
+
);
|
|
2067
|
+
let events = log.tail(0).unwrap();
|
|
2068
|
+
assert!(
|
|
2069
|
+
events.iter().any(|event| {
|
|
2070
|
+
event.get("event").and_then(serde_json::Value::as_str)
|
|
2071
|
+
== Some("leader_receiver.delivery_blocked")
|
|
2072
|
+
&& event.get("reason").and_then(serde_json::Value::as_str)
|
|
2073
|
+
== Some("leader_receiver_transport_conflict")
|
|
2074
|
+
}),
|
|
2075
|
+
"transport conflict must emit explicit fail-closed event; events={events:?}"
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
#[test]
|
|
2080
|
+
fn app_server_leader_busy_is_retryable_and_does_not_steer() {
|
|
2081
|
+
let ws = tmp_ws("appbusy");
|
|
2082
|
+
let mut script = crate::app_server_test_support::FakeAppServerScript::happy(
|
|
2083
|
+
"thread-live",
|
|
2084
|
+
"session-live",
|
|
2085
|
+
ws.to_str().unwrap(),
|
|
2086
|
+
);
|
|
2087
|
+
script.turn_error = Some("thread already has an active turn".to_string());
|
|
2088
|
+
let fake = crate::app_server_test_support::FakeAppServer::start("deliver-busy", script);
|
|
2089
|
+
let state = app_server_state(&ws, fake.endpoint(), "thread-live", "session-live");
|
|
2090
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
2091
|
+
let store = store_for(&ws);
|
|
2092
|
+
let log = EventLog::new(&ws);
|
|
2093
|
+
let message_id = store
|
|
2094
|
+
.create_message(
|
|
2095
|
+
None,
|
|
2096
|
+
"worker_a",
|
|
2097
|
+
"leader",
|
|
2098
|
+
"busy",
|
|
2099
|
+
None,
|
|
2100
|
+
false,
|
|
2101
|
+
Some("team-a"),
|
|
2102
|
+
)
|
|
2103
|
+
.unwrap();
|
|
2104
|
+
|
|
2105
|
+
let out = deliver_pending_message(&ws, &store, &NoopTransport, &message_id, &log, &state)
|
|
2106
|
+
.expect("leader busy should be retryable");
|
|
2107
|
+
|
|
2108
|
+
assert!(!out.ok);
|
|
2109
|
+
assert_eq!(out.status, DeliveryStatus::RetryScheduled);
|
|
2110
|
+
assert_eq!(out.channel.as_deref(), Some("leader_busy"));
|
|
2111
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
2112
|
+
let status: String = conn
|
|
2113
|
+
.query_row(
|
|
2114
|
+
"select status from messages where message_id = ?1",
|
|
2115
|
+
[&message_id],
|
|
2116
|
+
|row| row.get(0),
|
|
2117
|
+
)
|
|
2118
|
+
.unwrap();
|
|
2119
|
+
assert_eq!(status, "target_resolved", "busy leader remains retryable");
|
|
2120
|
+
let turns = fake.received_turns();
|
|
2121
|
+
assert_eq!(
|
|
2122
|
+
turns.len(),
|
|
2123
|
+
1,
|
|
2124
|
+
"must only call turn/start, never turn/steer"
|
|
2125
|
+
);
|
|
2126
|
+
assert_eq!(turns[0]["method"], serde_json::json!("turn/start"));
|
|
2127
|
+
}
|
|
2128
|
+
|
|
1897
2129
|
#[test]
|
|
1898
2130
|
fn u1_multi_team_send_does_not_backfill_top_level_leader_binding() {
|
|
1899
2131
|
let ws = tmp_ws("u1backfill");
|
|
@@ -430,6 +430,37 @@ fn runtime_tmux_endpoint_from_state(
|
|
|
430
430
|
/// stable FNV-1a hash over the canonicalized path. AF_UNIX `sun_path` is ~104 chars and the socket
|
|
431
431
|
/// lives at `/tmp/tmux-<uid>/<name>`, so we must NOT use the (~88-char) session name. §10: a
|
|
432
432
|
/// canonicalize failure falls back to the raw path (never panics).
|
|
433
|
+
/// Public re-export of the crate-private canonical workspace hash used
|
|
434
|
+
/// by the tmux short-socket derivation. `transport_factory` uses this
|
|
435
|
+
/// same hash for the ConPTY `workspace_hash` so both backends see
|
|
436
|
+
/// identical workspace identity. Adding a wrapper (not calling
|
|
437
|
+
/// `socket_name_for_workspace` directly outside this file) keeps the
|
|
438
|
+
/// existing internal API stable.
|
|
439
|
+
pub fn workspace_short_hash_pub(workspace: &Path) -> String {
|
|
440
|
+
// The tmux short-socket name is `ta-<12 hex>`; the workspace-hash
|
|
441
|
+
// used by the ConPTY workspace identity is the raw 12-hex portion
|
|
442
|
+
// (no `ta-` prefix), which is what the shim/pipe name derivation
|
|
443
|
+
// expects. Reuse the exact same hash so drift is impossible.
|
|
444
|
+
let name = socket_name_for_workspace(workspace);
|
|
445
|
+
name.strip_prefix("ta-").unwrap_or(&name).to_string()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/// Public re-export of the crate-private `runtime_tmux_endpoint_from_state`
|
|
449
|
+
/// probe. `transport_factory` uses this to spot the legacy tmux endpoint
|
|
450
|
+
/// in `state` without duplicating the field-precedence logic.
|
|
451
|
+
pub fn runtime_tmux_endpoint_from_state_pub(
|
|
452
|
+
state: Option<&serde_json::Value>,
|
|
453
|
+
) -> Option<(String, &'static str)> {
|
|
454
|
+
runtime_tmux_endpoint_from_state(state).map(|(endpoint, source)| {
|
|
455
|
+
let src = match source {
|
|
456
|
+
RuntimeTmuxEndpointSource::StateTmuxEndpoint => "state.tmux_endpoint",
|
|
457
|
+
RuntimeTmuxEndpointSource::StateTmuxSocket => "state.tmux_socket",
|
|
458
|
+
RuntimeTmuxEndpointSource::WorkspaceFallback => "workspace_fallback",
|
|
459
|
+
};
|
|
460
|
+
(endpoint.to_string(), src)
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
|
|
433
464
|
pub(crate) fn socket_name_for_workspace(workspace: &Path) -> String {
|
|
434
465
|
let canonical = workspace
|
|
435
466
|
.canonicalize()
|