@team-agent/installer 0.5.2 → 0.5.3
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 +3 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +20 -0
- package/crates/team-agent/src/cli/emit.rs +5 -0
- package/crates/team-agent/src/cli/mod.rs +121 -56
- package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
- package/crates/team-agent/src/codex_app_server.rs +62 -1
- package/crates/team-agent/src/conpty/backend.rs +120 -8
- package/crates/team-agent/src/coordinator/backoff.rs +88 -2
- package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
- package/crates/team-agent/src/coordinator/health.rs +97 -11
- package/crates/team-agent/src/coordinator/mod.rs +8 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
- package/crates/team-agent/src/diagnose/orphans.rs +18 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
- package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
- package/crates/team-agent/src/lib.rs +14 -1
- package/crates/team-agent/src/lifecycle/launch.rs +80 -91
- package/crates/team-agent/src/lifecycle/lock.rs +40 -33
- package/crates/team-agent/src/lifecycle/restart/agent.rs +10 -18
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
- package/crates/team-agent/src/mcp_server/wire.rs +6 -7
- package/crates/team-agent/src/messaging/tests/runtime.rs +5 -0
- package/crates/team-agent/src/packaging/tests.rs +41 -6
- package/crates/team-agent/src/packaging/types.rs +31 -3
- package/crates/team-agent/src/platform/argv.rs +324 -0
- package/crates/team-agent/src/platform/errors.rs +95 -0
- package/crates/team-agent/src/platform/file_lock.rs +418 -0
- package/crates/team-agent/src/platform/mod.rs +66 -0
- package/crates/team-agent/src/platform/process.rs +555 -0
- package/crates/team-agent/src/state/persist.rs +205 -25
- package/crates/team-agent/src/tmux_backend.rs +63 -13
- package/crates/team-agent/src/transport_factory.rs +124 -5
- package/package.json +4 -4
|
@@ -82,7 +82,19 @@ pub struct ConPtyBackend {
|
|
|
82
82
|
/// Present when a `PipeClient` is wired. When present, all
|
|
83
83
|
/// Required trait methods forward through the client; when absent,
|
|
84
84
|
/// they degrade to `MuxUnavailable` honest-fail.
|
|
85
|
-
|
|
85
|
+
///
|
|
86
|
+
/// 0.5.x CR C-5 fix (leader msg_a89b5a03bbf0): behind a Mutex so
|
|
87
|
+
/// the lazy `ensure_pipe_client` path can populate it on first
|
|
88
|
+
/// Op call. Prior shape kept it in `Option<...>` outside a lock,
|
|
89
|
+
/// which was fine when the factory pre-wired via
|
|
90
|
+
/// `with_pipe_client` but is racy under lazy semantics.
|
|
91
|
+
pipe_client: std::sync::Mutex<Option<Box<dyn PipeClientTrait>>>,
|
|
92
|
+
/// Optional pipe-name hint used by lazy connect. The factory
|
|
93
|
+
/// stashes this when it sees `state.transport.shim.pipe_name`
|
|
94
|
+
/// (Batch 5+), and `ensure_pipe_client` uses it on first Op that
|
|
95
|
+
/// needs the pipe. `None` = factory had no hint → backend stays
|
|
96
|
+
/// unwired.
|
|
97
|
+
pipe_hint: Option<String>,
|
|
86
98
|
/// Monotonic request id counter — never persisted, never surfaced
|
|
87
99
|
/// to callers.
|
|
88
100
|
request_counter: AtomicU64,
|
|
@@ -90,22 +102,51 @@ pub struct ConPtyBackend {
|
|
|
90
102
|
|
|
91
103
|
impl ConPtyBackend {
|
|
92
104
|
/// New backend for `(workspace_hash, team_key)`. The pipe client is
|
|
93
|
-
/// left unset; callers wire it via `with_pipe_client
|
|
105
|
+
/// left unset; callers wire it via `with_pipe_client` (Batch 6
|
|
106
|
+
/// direct-handoff) or `with_pipe_hint` (Batch 5+ lazy connect).
|
|
94
107
|
pub fn new(workspace_hash: impl Into<String>, team_key: impl Into<String>) -> Self {
|
|
95
108
|
Self {
|
|
96
109
|
workspace_hash: workspace_hash.into(),
|
|
97
110
|
team_key: team_key.into(),
|
|
98
111
|
pipe_token: std::sync::Mutex::new(None),
|
|
99
|
-
pipe_client: None,
|
|
112
|
+
pipe_client: std::sync::Mutex::new(None),
|
|
113
|
+
pipe_hint: None,
|
|
100
114
|
request_counter: AtomicU64::new(0),
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
117
|
|
|
118
|
+
/// 0.5.x CR C-5 fix (leader msg_a89b5a03bbf0): stash a pipe-name
|
|
119
|
+
/// hint for lazy connect. This is what the factory uses in
|
|
120
|
+
/// `build_conpty` — no I/O at resolve time; the connect is
|
|
121
|
+
/// deferred to the first Op that needs the pipe.
|
|
122
|
+
///
|
|
123
|
+
/// Idempotent: calling twice with the same hint is a no-op;
|
|
124
|
+
/// calling with a different hint overrides the previous. The
|
|
125
|
+
/// factory calls this ONCE per resolve.
|
|
126
|
+
pub fn with_pipe_hint(mut self, pipe_name: impl Into<String>) -> Self {
|
|
127
|
+
self.pipe_hint = Some(pipe_name.into());
|
|
128
|
+
self
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Read the current pipe-name hint (for factory logic that
|
|
132
|
+
/// derives a fallback hint from env override).
|
|
133
|
+
pub fn pipe_hint_ref(&self) -> Option<&str> {
|
|
134
|
+
self.pipe_hint.as_deref()
|
|
135
|
+
}
|
|
136
|
+
|
|
104
137
|
/// Wire a live PipeClient. Immediately performs `hello` to negotiate
|
|
105
138
|
/// the pipe_token; on failure, drops the client so subsequent calls
|
|
106
139
|
/// degrade honestly.
|
|
140
|
+
///
|
|
141
|
+
/// Used by `lifecycle/launch.rs` Batch 6 direct-handoff path
|
|
142
|
+
/// (quick-start conpty spawn on Windows), where the coordinator
|
|
143
|
+
/// already has a Hello-connected client from
|
|
144
|
+
/// `coordinator::conpty_shim::spawn_shim_and_handshake`. The
|
|
145
|
+
/// client becomes the backend's initial `pipe_client` cache
|
|
146
|
+
/// value, unifying with the lazy path (subsequent Ops just find
|
|
147
|
+
/// a wired client and skip the lazy `ensure_pipe_client` cost).
|
|
107
148
|
pub fn with_pipe_client(
|
|
108
|
-
|
|
149
|
+
self,
|
|
109
150
|
client: Box<dyn PipeClientTrait>,
|
|
110
151
|
) -> Result<Self, TransportError> {
|
|
111
152
|
// Hello does NOT require token pre-match; use a placeholder.
|
|
@@ -129,10 +170,74 @@ impl ConPtyBackend {
|
|
|
129
170
|
detail: format!("hello response malformed: {e}"),
|
|
130
171
|
})?;
|
|
131
172
|
*self.pipe_token.lock().unwrap() = Some(hello.pipe_token);
|
|
132
|
-
self.pipe_client = Some(client);
|
|
173
|
+
*self.pipe_client.lock().unwrap() = Some(client);
|
|
133
174
|
Ok(self)
|
|
134
175
|
}
|
|
135
176
|
|
|
177
|
+
/// 0.5.x CR C-5 fix (leader msg_a89b5a03bbf0): the lazy-connect
|
|
178
|
+
/// helper. Called from `dispatch` on the first Op that needs the
|
|
179
|
+
/// pipe.
|
|
180
|
+
///
|
|
181
|
+
/// Semantics:
|
|
182
|
+
/// 1. If `pipe_client` is already set (Batch 6 direct-handoff or
|
|
183
|
+
/// a prior lazy connect), return Ok immediately.
|
|
184
|
+
/// 2. Else if `pipe_hint` is set, attempt
|
|
185
|
+
/// `NamedPipeClient::connect(&hint, 2000)` (the same 2000ms
|
|
186
|
+
/// timeout the factory used pre-C-5-fix — leader constraint
|
|
187
|
+
/// "沿用 2000ms 语义"), perform Hello, cache the client.
|
|
188
|
+
/// 3. Else return `MuxUnavailable::no_pipe_client`.
|
|
189
|
+
///
|
|
190
|
+
/// On Unix this fn is a no-op stub — `NamedPipeClient` doesn't
|
|
191
|
+
/// exist there. Callers that reach this path on Unix already
|
|
192
|
+
/// have `pipe_client == None` and fall to the error branch,
|
|
193
|
+
/// which is the pre-C-5-fix behavior.
|
|
194
|
+
fn ensure_pipe_client(&self) -> Result<(), TransportError> {
|
|
195
|
+
// Fast path: already wired.
|
|
196
|
+
if self.pipe_client.lock().unwrap().is_some() {
|
|
197
|
+
return Ok(());
|
|
198
|
+
}
|
|
199
|
+
// Lazy path: need a hint + Windows backend.
|
|
200
|
+
#[cfg(windows)]
|
|
201
|
+
{
|
|
202
|
+
let Some(pipe_name) = self.pipe_hint.as_ref() else {
|
|
203
|
+
return Err(self.mux_unavailable("no_pipe_hint"));
|
|
204
|
+
};
|
|
205
|
+
let client = conpty_transport::NamedPipeClient::connect(pipe_name, 2000)
|
|
206
|
+
.map_err(|e| self.mux_unavailable(&format!("connect_failed: {e}")))?;
|
|
207
|
+
let adapter = crate::conpty::PipeClientAdapter(client);
|
|
208
|
+
// Perform Hello inline to negotiate pipe_token.
|
|
209
|
+
let req = Request::new(
|
|
210
|
+
self.next_request_id(),
|
|
211
|
+
&self.workspace_hash,
|
|
212
|
+
&self.team_key,
|
|
213
|
+
"PENDING",
|
|
214
|
+
Op::Hello,
|
|
215
|
+
);
|
|
216
|
+
let resp = adapter.request(&req)?;
|
|
217
|
+
if !resp.ok {
|
|
218
|
+
return Err(TransportError::MuxUnavailable {
|
|
219
|
+
backend: BackendKind::ConPty,
|
|
220
|
+
detail: format!("hello_failed: {:?}", resp.error),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
let hello: HelloResult = serde_json::from_value(resp.result).map_err(|e| {
|
|
224
|
+
TransportError::MuxUnavailable {
|
|
225
|
+
backend: BackendKind::ConPty,
|
|
226
|
+
detail: format!("hello_response_malformed: {e}"),
|
|
227
|
+
}
|
|
228
|
+
})?;
|
|
229
|
+
*self.pipe_token.lock().unwrap() = Some(hello.pipe_token);
|
|
230
|
+
*self.pipe_client.lock().unwrap() = Some(Box::new(adapter));
|
|
231
|
+
Ok(())
|
|
232
|
+
}
|
|
233
|
+
#[cfg(not(windows))]
|
|
234
|
+
{
|
|
235
|
+
// Unix has no NamedPipeClient. If we got here without a
|
|
236
|
+
// wired client, degrade honestly.
|
|
237
|
+
Err(self.mux_unavailable("no_pipe_client"))
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
136
241
|
fn next_request_id(&self) -> String {
|
|
137
242
|
let n = self.request_counter.fetch_add(1, Ordering::Relaxed);
|
|
138
243
|
format!("req-{n}")
|
|
@@ -156,10 +261,17 @@ impl ConPtyBackend {
|
|
|
156
261
|
}
|
|
157
262
|
|
|
158
263
|
fn dispatch(&self, op: Op, payload: serde_json::Value) -> Result<Response, TransportError> {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
264
|
+
// 0.5.x CR C-5 fix (leader msg_a89b5a03bbf0): trigger the
|
|
265
|
+
// lazy connect + Hello on first Op that needs the pipe. If a
|
|
266
|
+
// client was pre-wired (Batch 6 direct-handoff) or a prior
|
|
267
|
+
// Op already connected, `ensure_pipe_client` is a fast
|
|
268
|
+
// no-op.
|
|
269
|
+
self.ensure_pipe_client()?;
|
|
162
270
|
let req = self.build_request(op, payload)?;
|
|
271
|
+
let client_guard = self.pipe_client.lock().unwrap();
|
|
272
|
+
let client = client_guard
|
|
273
|
+
.as_ref()
|
|
274
|
+
.ok_or_else(|| self.mux_unavailable(&format!("{op:?}").to_lowercase()))?;
|
|
163
275
|
let resp = client.request(&req)?;
|
|
164
276
|
if !resp.ok {
|
|
165
277
|
return Err(map_protocol_error(resp.error.as_ref()));
|
|
@@ -27,6 +27,14 @@ pub struct DaemonArgs {
|
|
|
27
27
|
pub once: bool,
|
|
28
28
|
/// `--tick-interval`(`__main__.py:29`)。`None` → 读 spec `runtime.tick_interval_sec`。
|
|
29
29
|
pub tick_interval_sec: Option<f64>,
|
|
30
|
+
/// 0.5.x Windows portability Batch 9 F8 (leader msg_2a4cc1fa54c0):
|
|
31
|
+
/// team_key passed via `--team` CLI so the coordinator daemon
|
|
32
|
+
/// doesn't have to derive from state at boot time. That derivation
|
|
33
|
+
/// (Batch 7 F5) got trapped by Batch 8's seed-state pattern
|
|
34
|
+
/// (F8 root cause). Preserving the derivation as a fallback when
|
|
35
|
+
/// `team_key` is None keeps Unix daemons and pre-existing test
|
|
36
|
+
/// harnesses byte-preserving.
|
|
37
|
+
pub team_key: Option<String>,
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
/// daemon 主循环(`main`,`__main__.py:25-98`)。写 pid/meta(source=boot)、装信号→STOP、孤儿自检、
|
|
@@ -44,11 +52,35 @@ pub fn run_daemon(args: DaemonArgs) -> Result<(), DaemonError> {
|
|
|
44
52
|
// legacy `tmux_endpoint` → same `tmux_backend_for_runtime_state_or_workspace`
|
|
45
53
|
// shape inside the factory's `build_tmux`).
|
|
46
54
|
let state = crate::state::persist::load_runtime_state(args.workspace.as_path()).ok();
|
|
47
|
-
|
|
55
|
+
// 0.5.x Windows portability Batch 9 F8 (leader msg_2a4cc1fa54c0):
|
|
56
|
+
// team_key priority is now `args.team_key` (from `--team` CLI)
|
|
57
|
+
// > `state.active_team_key` fallback. The Batch 7 F5 derivation
|
|
58
|
+
// stays as the fallback so pre-Batch-9 tests + Unix daemons
|
|
59
|
+
// continue byte-preserving. Callers that CAN pass `--team`
|
|
60
|
+
// (Batch 9 quick-start on Windows) do — that avoids the F8
|
|
61
|
+
// seed-state trap where writing active_team_key to state ahead
|
|
62
|
+
// of coord spawn made downstream launch code think "existing
|
|
63
|
+
// runtime, use restart" and skip spec compile.
|
|
64
|
+
let team_key_from_state = args
|
|
65
|
+
.team_key
|
|
66
|
+
.clone()
|
|
67
|
+
.filter(|s| !s.is_empty())
|
|
68
|
+
.or_else(|| {
|
|
69
|
+
state
|
|
70
|
+
.as_ref()
|
|
71
|
+
.and_then(|s| s.get("active_team_key"))
|
|
72
|
+
.and_then(|v| v.as_str())
|
|
73
|
+
.filter(|s| !s.is_empty())
|
|
74
|
+
.map(str::to_string)
|
|
75
|
+
});
|
|
76
|
+
let mut factory_input = crate::transport_factory::TransportFactoryInput::new(
|
|
48
77
|
args.workspace.as_path(),
|
|
49
78
|
crate::transport_factory::TransportPurpose::Coordinator,
|
|
50
79
|
)
|
|
51
80
|
.with_state(state.as_ref());
|
|
81
|
+
if let Some(ref team_key) = team_key_from_state {
|
|
82
|
+
factory_input = factory_input.with_team_key(Some(team_key));
|
|
83
|
+
}
|
|
52
84
|
let resolved = match crate::transport_factory::resolve_transport(factory_input) {
|
|
53
85
|
Ok(resolved) => resolved,
|
|
54
86
|
Err(e) => {
|
|
@@ -137,6 +169,57 @@ fn run_daemon_with_coordinator_and_boot_tmux(
|
|
|
137
169
|
}
|
|
138
170
|
}
|
|
139
171
|
event_log.write("coordinator.boot", boot_event)?;
|
|
172
|
+
|
|
173
|
+
// 0.5.x Windows portability Batch 8 F7 (leader msg_590b4dce0f68):
|
|
174
|
+
// coordinator takes ownership of shim lifecycle. Idempotent:
|
|
175
|
+
// spawns a fresh shim if none is recorded, reconnects if
|
|
176
|
+
// `state.transport.shim.pid` names a living shim (post-crash
|
|
177
|
+
// restart path). Failures are non-fatal for the coord daemon —
|
|
178
|
+
// the tick loop's transport calls will surface honest
|
|
179
|
+
// MuxUnavailable errors and `mark_transport_unavailable`
|
|
180
|
+
// emits the C-3 stale-family event.
|
|
181
|
+
//
|
|
182
|
+
// On non-Windows this call is cfg'd out (there's no shim
|
|
183
|
+
// concept on Unix).
|
|
184
|
+
#[cfg(windows)]
|
|
185
|
+
{
|
|
186
|
+
let state_for_team_key =
|
|
187
|
+
crate::state::persist::load_runtime_state(args.workspace.as_path()).ok();
|
|
188
|
+
let team_key_opt = state_for_team_key
|
|
189
|
+
.as_ref()
|
|
190
|
+
.and_then(|s| s.get("active_team_key"))
|
|
191
|
+
.and_then(|v| v.as_str())
|
|
192
|
+
.filter(|s| !s.is_empty())
|
|
193
|
+
.map(str::to_string);
|
|
194
|
+
if let Some(team_key) = team_key_opt {
|
|
195
|
+
let workspace_hash =
|
|
196
|
+
crate::tmux_backend::workspace_short_hash_pub(args.workspace.as_path());
|
|
197
|
+
match crate::coordinator::conpty_shim::ensure_shim_running(
|
|
198
|
+
args.workspace.as_path(),
|
|
199
|
+
&team_key,
|
|
200
|
+
&workspace_hash,
|
|
201
|
+
) {
|
|
202
|
+
Ok(handle) => {
|
|
203
|
+
// Detach so the shim survives beyond this
|
|
204
|
+
// coordinator instance (F7 core invariant).
|
|
205
|
+
let _shim_pid = handle.detach();
|
|
206
|
+
eprintln!("coordinator: shim ensured (pid={_shim_pid})");
|
|
207
|
+
}
|
|
208
|
+
Err(e) => {
|
|
209
|
+
// Honest degradation: emit the stale-family
|
|
210
|
+
// event, coord continues without a shim. Tick
|
|
211
|
+
// loop will surface `mux_unavailable` on any
|
|
212
|
+
// transport call.
|
|
213
|
+
eprintln!("coordinator: shim ensure failed: {e}");
|
|
214
|
+
let _ = crate::coordinator::conpty_shim::mark_transport_unavailable(
|
|
215
|
+
args.workspace.as_path(),
|
|
216
|
+
&format!("boot_ensure_failed: {e}"),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
140
223
|
let tick_interval = match args.tick_interval_sec {
|
|
141
224
|
Some(v) if v > 0.0 => v,
|
|
142
225
|
_ => resolve_tick_interval(&args.workspace)?,
|
|
@@ -254,8 +337,11 @@ fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
|
|
|
254
337
|
}
|
|
255
338
|
|
|
256
339
|
/// 当前 ppid(`os.getppid()`,孤儿自检输入)。
|
|
340
|
+
///
|
|
341
|
+
/// 0.5.x Windows portability Batch 3: uses `platform::process::current_parent_pid`
|
|
342
|
+
/// so Windows sees a real Toolhelp32-derived ppid instead of `0`.
|
|
257
343
|
fn current_ppid() -> u32 {
|
|
258
|
-
|
|
344
|
+
crate::platform::process::current_parent_pid().unwrap_or(0)
|
|
259
345
|
}
|
|
260
346
|
|
|
261
347
|
/// 计算 tick 间隔(`_tick_interval`,`__main__.py:104-115`)。读 spec `runtime.tick_interval_sec`,
|