@team-agent/installer 0.5.72 → 0.5.74
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 +19 -13
- package/crates/team-agent/src/cli/emit.rs +6 -3
- package/crates/team-agent/src/cli/mod.rs +151 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
- package/crates/team-agent/src/cli/types.rs +6 -0
- package/crates/team-agent/src/compiler.rs +14 -1
- package/crates/team-agent/src/leader/lease.rs +106 -21
- package/crates/team-agent/src/leader/registry.rs +0 -89
- package/crates/team-agent/src/leader/start.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +43 -7
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +457 -73
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -11
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +71 -1
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +34 -1
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +396 -3
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +3 -1
- package/crates/team-agent/src/messaging/delivery.rs +26 -8
- package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
- package/crates/team-agent/src/model/spec.rs +81 -7
- package/crates/team-agent/src/provider/adapters/pi.rs +26 -6
- package/crates/team-agent/src/state/persist.rs +272 -3
- package/crates/team-agent/src/state/repository.rs +9 -2
- package/crates/team-agent/src/tmux_backend/tests.rs +64 -0
- package/crates/team-agent/src/tmux_backend.rs +57 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +4 -1
- package/skills/team-agent/command-coverage.json +21 -0
- package/skills/team-agent/references/team-agent-operator.md +9 -2
|
@@ -243,12 +243,6 @@ pub fn register_binding_from_state_best_effort(
|
|
|
243
243
|
);
|
|
244
244
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
245
245
|
let write_result = write_entry_best_effort(&entry);
|
|
246
|
-
// Fresh quick-start is acquisition-only: it must never displace another
|
|
247
|
-
// same-pane row. Its caller performs a validated-LIVE check before this
|
|
248
|
-
// write; manual/restart ownership paths keep existing behavior.
|
|
249
|
-
if write_result.is_some() && source != "quick-start" {
|
|
250
|
-
let _ = mark_displaced_same_pane_entries_unbound(&entry);
|
|
251
|
-
}
|
|
252
246
|
let status = if let Some(path) = &write_result {
|
|
253
247
|
let _ = event_log.write(
|
|
254
248
|
EVENT_REGISTERED,
|
|
@@ -281,68 +275,6 @@ pub fn register_binding_from_state_best_effort(
|
|
|
281
275
|
})
|
|
282
276
|
}
|
|
283
277
|
|
|
284
|
-
/// Stored registry status after another workspace claimed the same pane.
|
|
285
|
-
pub const REGISTRY_STATUS_UNBOUND: &str = "unbound";
|
|
286
|
-
|
|
287
|
-
/// When workspace B claims a pane, every other attached registry row that
|
|
288
|
-
/// still names that pane is no longer deliverable. Mark those rows unbound.
|
|
289
|
-
/// Does not change pane-side authorization storage.
|
|
290
|
-
pub fn mark_displaced_same_pane_entries_unbound(winner: &LeaderRegistryEntry) -> Vec<PathBuf> {
|
|
291
|
-
let Some(winner_pane) = winner
|
|
292
|
-
.channel
|
|
293
|
-
.get("pane_id")
|
|
294
|
-
.and_then(Value::as_str)
|
|
295
|
-
.filter(|pane| !pane.is_empty())
|
|
296
|
-
else {
|
|
297
|
-
return Vec::new();
|
|
298
|
-
};
|
|
299
|
-
let winner_socket = winner
|
|
300
|
-
.channel
|
|
301
|
-
.get("tmux_socket")
|
|
302
|
-
.and_then(Value::as_str)
|
|
303
|
-
.filter(|socket| !socket.is_empty());
|
|
304
|
-
let mut displaced = Vec::new();
|
|
305
|
-
for (path, mut entry) in read_all_entries() {
|
|
306
|
-
if entry.workspace_hash == winner.workspace_hash && entry.team_key == winner.team_key {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
if entry.status != "attached" {
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
let Some(pane) = entry
|
|
313
|
-
.channel
|
|
314
|
-
.get("pane_id")
|
|
315
|
-
.and_then(Value::as_str)
|
|
316
|
-
.filter(|pane| !pane.is_empty())
|
|
317
|
-
else {
|
|
318
|
-
continue;
|
|
319
|
-
};
|
|
320
|
-
if pane != winner_pane {
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
let Some(socket) = entry
|
|
324
|
-
.channel
|
|
325
|
-
.get("tmux_socket")
|
|
326
|
-
.and_then(Value::as_str)
|
|
327
|
-
.filter(|socket| !socket.is_empty())
|
|
328
|
-
else {
|
|
329
|
-
continue;
|
|
330
|
-
};
|
|
331
|
-
let Some(winner_socket) = winner_socket else {
|
|
332
|
-
continue;
|
|
333
|
-
};
|
|
334
|
-
if socket != winner_socket {
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
entry.status = REGISTRY_STATUS_UNBOUND.to_string();
|
|
338
|
-
entry.updated_at = chrono::Utc::now().to_rfc3339();
|
|
339
|
-
if write_entry_best_effort(&entry).is_some() {
|
|
340
|
-
displaced.push(path);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
displaced
|
|
344
|
-
}
|
|
345
|
-
|
|
346
278
|
fn entry_filename(entry: &LeaderRegistryEntry) -> String {
|
|
347
279
|
format!("{}__{}.json", entry.workspace_hash, entry.team_key)
|
|
348
280
|
}
|
|
@@ -546,27 +478,6 @@ pub fn list_validated_with_gc() -> Vec<(LeaderRegistryEntry, &'static str, Optio
|
|
|
546
478
|
out
|
|
547
479
|
}
|
|
548
480
|
|
|
549
|
-
/// Refuse a fresh quick-start binding when the caller pane already backs a
|
|
550
|
-
/// canonically LIVE receiver for another workspace/team. This is read-only:
|
|
551
|
-
/// quick-start never displaces or takes over an existing binding.
|
|
552
|
-
#[must_use]
|
|
553
|
-
pub(crate) fn live_same_pane_binding_elsewhere(
|
|
554
|
-
pane_id: &str,
|
|
555
|
-
workspace: &Path,
|
|
556
|
-
team_key: &str,
|
|
557
|
-
) -> bool {
|
|
558
|
-
let workspace = std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf());
|
|
559
|
-
list_validated_no_gc().into_iter().any(|(entry, status, _)| {
|
|
560
|
-
status == "LIVE"
|
|
561
|
-
&& entry
|
|
562
|
-
.channel
|
|
563
|
-
.get("pane_id")
|
|
564
|
-
.and_then(Value::as_str)
|
|
565
|
-
== Some(pane_id)
|
|
566
|
-
&& (entry.workspace != workspace || entry.team_key != team_key)
|
|
567
|
-
})
|
|
568
|
-
}
|
|
569
|
-
|
|
570
481
|
/// Same as `list_validated_with_gc` but preserves all entries. `send
|
|
571
482
|
/// --to-leader` needs to see stale entries so it can refuse with
|
|
572
483
|
/// `registry_stale` (and never silently fall through to
|
|
@@ -162,6 +162,7 @@ pub(crate) fn prepare_leader_start_with_nested_attach(
|
|
|
162
162
|
std::env::var_os("TMUX").is_some(),
|
|
163
163
|
managed_client_attach_mode,
|
|
164
164
|
managed_exec_provider,
|
|
165
|
+
allow_nested_attach,
|
|
165
166
|
)?;
|
|
166
167
|
Ok(PreparedLeaderStart {
|
|
167
168
|
plan,
|
|
@@ -196,6 +197,7 @@ pub(crate) fn leader_start_plan_after_ambient_authority_check(
|
|
|
196
197
|
in_tmux,
|
|
197
198
|
managed_client_attach_mode,
|
|
198
199
|
false,
|
|
200
|
+
false,
|
|
199
201
|
)
|
|
200
202
|
}
|
|
201
203
|
|
|
@@ -210,6 +212,7 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
210
212
|
in_tmux: bool,
|
|
211
213
|
managed_client_attach_mode: Option<ManagedClientAttachMode>,
|
|
212
214
|
managed_exec_provider: bool,
|
|
215
|
+
allow_nested_attach: bool,
|
|
213
216
|
) -> Result<LeaderStartPlan, LeaderError> {
|
|
214
217
|
if attach_session.is_some() && !confirm_attach {
|
|
215
218
|
return Err(LeaderError::Start(
|
|
@@ -327,6 +330,11 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
327
330
|
tool_categories: &tools,
|
|
328
331
|
team_mcp_tools: &team_mcp_tools,
|
|
329
332
|
mcp_config: &mcp_config,
|
|
333
|
+
session_scope: crate::lifecycle::launch::pi_mcp::pi_leader_session_scope(
|
|
334
|
+
provider_args,
|
|
335
|
+
external_path,
|
|
336
|
+
allow_nested_attach,
|
|
337
|
+
),
|
|
330
338
|
},
|
|
331
339
|
)
|
|
332
340
|
.map_err(|error| LeaderError::Start(error.to_string()))?
|
|
@@ -816,6 +816,27 @@ pub(crate) fn resolve_pi_executable_chain(
|
|
|
816
816
|
Ok((chain, models))
|
|
817
817
|
}
|
|
818
818
|
|
|
819
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
820
|
+
pub(crate) enum PiSessionScope {
|
|
821
|
+
Isolated,
|
|
822
|
+
NativeDefault,
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/// Zero-argument managed Pi is the interactive leader entry. Only that route
|
|
826
|
+
/// may use Pi's cwd-keyed native session root; every explicit or worker route
|
|
827
|
+
/// remains Team Agent session-isolated.
|
|
828
|
+
pub(crate) fn pi_leader_session_scope(
|
|
829
|
+
provider_args: &[String],
|
|
830
|
+
external_path: bool,
|
|
831
|
+
allow_nested_attach: bool,
|
|
832
|
+
) -> PiSessionScope {
|
|
833
|
+
if provider_args.is_empty() && !external_path && !allow_nested_attach {
|
|
834
|
+
PiSessionScope::NativeDefault
|
|
835
|
+
} else {
|
|
836
|
+
PiSessionScope::Isolated
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
819
840
|
pub(crate) struct PiMaterializeRequest<'a> {
|
|
820
841
|
pub workspace: &'a Path,
|
|
821
842
|
pub team_id: &'a str,
|
|
@@ -826,11 +847,13 @@ pub(crate) struct PiMaterializeRequest<'a> {
|
|
|
826
847
|
pub tool_categories: &'a [&'a str],
|
|
827
848
|
pub team_mcp_tools: &'a [&'a str],
|
|
828
849
|
pub mcp_config: &'a McpConfig,
|
|
850
|
+
pub session_scope: PiSessionScope,
|
|
829
851
|
}
|
|
830
852
|
|
|
831
853
|
/// ---
|
|
832
854
|
/// purpose: 共享物化 Pi leader 与 TeamMate 的 wrapper、UUID 和 CommandPlan
|
|
833
|
-
/// returns: fresh Pi CommandPlan
|
|
855
|
+
/// returns: fresh Pi CommandPlan;isolated seats carry their session root, while the
|
|
856
|
+
/// interactive leader preserves Pi's native default root
|
|
834
857
|
/// errors: resolver、exact model、wrapper 或 argv 构造失败时返回 ProviderError
|
|
835
858
|
/// ---
|
|
836
859
|
pub(crate) fn materialize_pi_plan(
|
|
@@ -863,8 +886,18 @@ fn materialize_pi_plan_with_session(
|
|
|
863
886
|
.map(|model| select_exact_pi_model(&catalog, model))
|
|
864
887
|
.transpose()?;
|
|
865
888
|
let paths = pi_seat_paths(request.workspace, request.team_id, request.agent_id);
|
|
889
|
+
let session_dir = match request.session_scope {
|
|
890
|
+
PiSessionScope::Isolated => Some(paths.sessions.as_path()),
|
|
891
|
+
PiSessionScope::NativeDefault => None,
|
|
892
|
+
};
|
|
866
893
|
let resume = if let Some((session_id, session_path, spawn_cwd)) = resume {
|
|
867
|
-
let
|
|
894
|
+
let session_dir = session_dir.ok_or_else(|| {
|
|
895
|
+
ProviderError::ResumeUnavailable(format!(
|
|
896
|
+
"Pi session root is unavailable {}",
|
|
897
|
+
paths.sessions.display()
|
|
898
|
+
))
|
|
899
|
+
})?;
|
|
900
|
+
let session_root = std::fs::canonicalize(session_dir).map_err(|error| {
|
|
868
901
|
ProviderError::ResumeUnavailable(format!(
|
|
869
902
|
"Pi session root is unavailable {}: {error}",
|
|
870
903
|
paths.sessions.display()
|
|
@@ -885,8 +918,11 @@ fn materialize_pi_plan_with_session(
|
|
|
885
918
|
let _ = spawn_cwd;
|
|
886
919
|
Some((session_id, exact_path))
|
|
887
920
|
} else {
|
|
888
|
-
|
|
889
|
-
.map_err(|error|
|
|
921
|
+
if let Some(session_dir) = session_dir {
|
|
922
|
+
std::fs::create_dir_all(session_dir).map_err(|error| {
|
|
923
|
+
ProviderError::Io(format!("{}: {error}", session_dir.display()))
|
|
924
|
+
})?;
|
|
925
|
+
}
|
|
890
926
|
None
|
|
891
927
|
};
|
|
892
928
|
let candidate = candidate_from_mcp_config(request.mcp_config)?;
|
|
@@ -908,7 +944,7 @@ fn materialize_pi_plan_with_session(
|
|
|
908
944
|
effort: request.effort,
|
|
909
945
|
system_prompt: request.system_prompt,
|
|
910
946
|
tool_categories: request.tool_categories,
|
|
911
|
-
session_dir
|
|
947
|
+
session_dir,
|
|
912
948
|
session: PiSessionSelector::Resume { path: &exact_path },
|
|
913
949
|
agent_id: request.agent_id,
|
|
914
950
|
})?;
|
|
@@ -922,7 +958,7 @@ fn materialize_pi_plan_with_session(
|
|
|
922
958
|
effort: request.effort,
|
|
923
959
|
system_prompt: request.system_prompt,
|
|
924
960
|
tool_categories: request.tool_categories,
|
|
925
|
-
session_dir
|
|
961
|
+
session_dir,
|
|
926
962
|
session: PiSessionSelector::Fresh {
|
|
927
963
|
session_id: session_id.as_str(),
|
|
928
964
|
},
|
|
@@ -933,7 +969,7 @@ fn materialize_pi_plan_with_session(
|
|
|
933
969
|
Ok(CommandPlan {
|
|
934
970
|
argv,
|
|
935
971
|
expected_session_id,
|
|
936
|
-
provider_projects_root:
|
|
972
|
+
provider_projects_root: session_dir.map(Path::to_path_buf),
|
|
937
973
|
managed_mcp_config: false,
|
|
938
974
|
})
|
|
939
975
|
}
|