@team-agent/installer 0.5.61 → 0.5.63
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 +5 -2
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +41 -7
- package/crates/team-agent/src/cli/helpers.rs +1 -0
- package/crates/team-agent/src/cli/leader.rs +6 -1
- package/crates/team-agent/src/cli/leaders.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +33 -3
- package/crates/team-agent/src/cli/named_address.rs +8 -1
- package/crates/team-agent/src/cli/profile.rs +2 -1
- package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send/resolve.rs +206 -1
- package/crates/team-agent/src/cli/send.rs +2 -1
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/cli/status.rs +2 -1
- package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
- package/crates/team-agent/src/cli/status_port/format.rs +1 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/communication_mode/mod.rs +1 -0
- package/crates/team-agent/src/compiler.rs +1 -0
- package/crates/team-agent/src/conpty/backend.rs +1 -0
- package/crates/team-agent/src/conpty/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/backoff.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
- package/crates/team-agent/src/coordinator/health.rs +1 -0
- package/crates/team-agent/src/coordinator/mod.rs +2 -2
- package/crates/team-agent/src/coordinator/orphan.rs +4 -116
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +15 -866
- package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
- package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +139 -375
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
- package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -49
- package/crates/team-agent/src/coordinator/tick.rs +23 -40
- package/crates/team-agent/src/coordinator/types.rs +3 -176
- package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
- package/crates/team-agent/src/db/message_store.rs +2 -1
- package/crates/team-agent/src/db/migration.rs +5 -4
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/db/schema.rs +1 -0
- package/crates/team-agent/src/diagnose/comms.rs +2 -1
- package/crates/team-agent/src/diagnose/mod.rs +1 -0
- package/crates/team-agent/src/diagnose/orphans.rs +1 -0
- package/crates/team-agent/src/layout/manager.rs +3 -7
- package/crates/team-agent/src/layout/mod.rs +2 -2
- package/crates/team-agent/src/layout/overlay.rs +6 -1
- package/crates/team-agent/src/layout/placement.rs +1 -0
- package/crates/team-agent/src/layout/recovery.rs +3 -0
- package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
- package/crates/team-agent/src/layout/sessions.rs +8 -1
- package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
- package/crates/team-agent/src/layout/worker_env.rs +3 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/helpers.rs +1 -0
- package/crates/team-agent/src/leader/incident.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +27 -22
- package/crates/team-agent/src/leader/mod.rs +2 -3
- package/crates/team-agent/src/leader/owner_bind.rs +13 -3
- package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +4 -17
- package/crates/team-agent/src/leader/registry.rs +3 -15
- package/crates/team-agent/src/leader/start.rs +373 -125
- package/crates/team-agent/src/leader/takeover.rs +10 -232
- package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
- package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
- package/crates/team-agent/src/leader/tests/identity.rs +7 -2
- package/crates/team-agent/src/leader/tests/idle.rs +0 -193
- package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
- package/crates/team-agent/src/leader/tests/mod.rs +4 -41
- package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
- package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
- package/crates/team-agent/src/leader/types.rs +1 -28
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +7 -1
- package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +11 -12
- package/crates/team-agent/src/lifecycle/mod.rs +2 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -66
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
- package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart.rs +13 -15
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
- package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
- package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
- package/crates/team-agent/src/lifecycle/tests.rs +59 -0
- package/crates/team-agent/src/lifecycle/types.rs +0 -9
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
- package/crates/team-agent/src/mcp_server/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
- package/crates/team-agent/src/mcp_server/tools.rs +1 -0
- package/crates/team-agent/src/mcp_server/types.rs +1 -18
- package/crates/team-agent/src/mcp_server/wire.rs +5 -5
- package/crates/team-agent/src/messaging/activity.rs +1 -0
- package/crates/team-agent/src/messaging/address.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +4 -3
- package/crates/team-agent/src/messaging/helpers.rs +2 -1
- package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
- package/crates/team-agent/src/messaging/mod.rs +10 -14
- package/crates/team-agent/src/messaging/peers.rs +2 -0
- package/crates/team-agent/src/messaging/persist.rs +3 -1
- package/crates/team-agent/src/messaging/presentation.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +1 -0
- package/crates/team-agent/src/messaging/selftest.rs +1 -1
- package/crates/team-agent/src/messaging/send.rs +41 -8
- package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
- package/crates/team-agent/src/messaging/types.rs +2 -2
- package/crates/team-agent/src/messaging/watchers.rs +19 -13
- package/crates/team-agent/src/model/enums.rs +1 -0
- package/crates/team-agent/src/model/errors.rs +1 -0
- package/crates/team-agent/src/model/ids.rs +1 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/name_similarity.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
- package/crates/team-agent/src/model/paths.rs +1 -0
- package/crates/team-agent/src/model/permissions.rs +3 -2
- package/crates/team-agent/src/model/routing.rs +1 -0
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/model/task_graph.rs +1 -0
- package/crates/team-agent/src/model/yaml/tests.rs +1 -0
- package/crates/team-agent/src/model/yaml.rs +1 -0
- package/crates/team-agent/src/packaging/install.rs +9 -147
- package/crates/team-agent/src/packaging/migrate.rs +2 -0
- package/crates/team-agent/src/packaging/mod.rs +1 -0
- package/crates/team-agent/src/packaging/repair.rs +2 -0
- package/crates/team-agent/src/packaging/tests.rs +29 -218
- package/crates/team-agent/src/packaging/types.rs +8 -15
- package/crates/team-agent/src/platform/argv.rs +4 -0
- package/crates/team-agent/src/platform/errors.rs +10 -1
- package/crates/team-agent/src/platform/file_lock.rs +4 -157
- package/crates/team-agent/src/platform/mod.rs +0 -61
- package/crates/team-agent/src/platform/process.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
- package/crates/team-agent/src/provider/classify.rs +1 -0
- package/crates/team-agent/src/provider/faults.rs +1 -26
- package/crates/team-agent/src/provider/helpers.rs +1 -0
- package/crates/team-agent/src/provider/mod.rs +1 -1
- package/crates/team-agent/src/provider/session/capture.rs +1 -4
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
- package/crates/team-agent/src/provider/session/mod.rs +2 -4
- package/crates/team-agent/src/provider/session/resume.rs +2 -209
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -42
- package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +1 -0
- package/crates/team-agent/src/provider/wire.rs +1 -0
- package/crates/team-agent/src/state/identity.rs +16 -5
- package/crates/team-agent/src/state/identity_keys.rs +1 -0
- package/crates/team-agent/src/state/mod.rs +2 -0
- package/crates/team-agent/src/state/owner_gate.rs +5 -0
- package/crates/team-agent/src/state/ownership.rs +10 -4
- package/crates/team-agent/src/state/paths.rs +12 -0
- package/crates/team-agent/src/state/persist.rs +9 -3
- package/crates/team-agent/src/state/projection.rs +15 -2
- package/crates/team-agent/src/state/repository/intent.rs +1 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +7 -13
- package/crates/team-agent/src/state/selector.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
- package/crates/team-agent/src/tmux_backend.rs +63 -0
- package/crates/team-agent/src/transport.rs +15 -0
- package/package.json +4 -4
- package/crates/team-agent/src/leader/inject.rs +0 -33
- package/crates/team-agent/src/provider/command.rs +0 -80
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
//!
|
|
1
|
+
//!
|
|
2
|
+
//! leader::start — leader_start_plan / leader_session_name(派生 tmux session 名)。
|
|
2
3
|
|
|
3
4
|
use std::collections::BTreeMap;
|
|
4
5
|
use std::io::{IsTerminal, Read, Write};
|
|
@@ -26,7 +27,7 @@ use super::{
|
|
|
26
27
|
LeaderStartMode, LeaderStartPlan,
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
// ── leader::start — leader_start_plan /
|
|
30
|
+
// ── leader::start — leader_start_plan / session 名 ──
|
|
30
31
|
|
|
31
32
|
pub(crate) struct PreparedLeaderStart {
|
|
32
33
|
plan: LeaderStartPlan,
|
|
@@ -74,7 +75,7 @@ impl From<LeaderError> for PrepareLeaderStartError {
|
|
|
74
75
|
|
|
75
76
|
/// `leader_start_plan`(card §46;`__init__.py:82`)。计算 leader 启动计划
|
|
76
77
|
/// (exec in-TMUX / new tmux session / attach existing)。provider 未安装 → `Err(Start)`。
|
|
77
|
-
pub fn leader_start_plan(
|
|
78
|
+
pub(crate) fn leader_start_plan(
|
|
78
79
|
provider: Provider,
|
|
79
80
|
provider_args: &[String],
|
|
80
81
|
workspace: &Path,
|
|
@@ -232,20 +233,16 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
232
233
|
// `managed_team_session_name(identity) = team-<team_id>` which is the
|
|
233
234
|
// worker session — that co-location is the structural root of
|
|
234
235
|
// E49/E51/E53/E57-3/E60.
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
// time. Pre-fix the managed path used the workspace-keyed name (same
|
|
240
|
-
// session for every entry) and silently attached the second client →
|
|
241
|
-
// UI mirror. The external/attach paths keep the stable workspace-keyed
|
|
242
|
-
// name (per-launch nonce would break `--attach-session` reattach).
|
|
236
|
+
// A-22: managed mode reuses a live provider leader session by prefix;
|
|
237
|
+
// when no candidate exists, it falls back to a per-launch nonce. The
|
|
238
|
+
// external/attach paths keep the stable workspace-keyed name because a
|
|
239
|
+
// nonce would break `--attach-session` reattach.
|
|
243
240
|
let session_name = if external_path {
|
|
244
241
|
attach_session
|
|
245
242
|
.cloned()
|
|
246
243
|
.or_else(|| Some(leader_session_name(provider, workspace)))
|
|
247
244
|
} else {
|
|
248
|
-
Some(
|
|
245
|
+
Some(managed_leader_session_for_launch(provider, workspace))
|
|
249
246
|
};
|
|
250
247
|
if !in_tmux {
|
|
251
248
|
ensure_tmux_installed()?;
|
|
@@ -348,66 +345,6 @@ pub(crate) fn leader_env_for_identity(
|
|
|
348
345
|
leader_env
|
|
349
346
|
}
|
|
350
347
|
|
|
351
|
-
/// `start_leader`(card §46;`__init__.py:60`)。计算并执行 leader 启动计划(spawn + 信号处理)。
|
|
352
|
-
/// 进程退出后 `Err`/退出码经 caller 处理(此处返 `Result` 替代 Python 的 `SystemExit`)。
|
|
353
|
-
pub fn start_leader(
|
|
354
|
-
provider: Provider,
|
|
355
|
-
provider_args: &[String],
|
|
356
|
-
workspace: &Path,
|
|
357
|
-
attach_existing: bool,
|
|
358
|
-
confirm_attach: bool,
|
|
359
|
-
attach_session: Option<&SessionName>,
|
|
360
|
-
external_leader: bool,
|
|
361
|
-
) -> Result<(), LeaderError> {
|
|
362
|
-
let prepared = prepare_leader_start(
|
|
363
|
-
provider,
|
|
364
|
-
provider_args,
|
|
365
|
-
workspace,
|
|
366
|
-
attach_existing,
|
|
367
|
-
confirm_attach,
|
|
368
|
-
attach_session,
|
|
369
|
-
external_leader,
|
|
370
|
-
)
|
|
371
|
-
.map_err(PrepareLeaderStartError::into_leader_error)?;
|
|
372
|
-
let plan = prepared.plan();
|
|
373
|
-
crate::event_log::EventLog::new(workspace).write(
|
|
374
|
-
super::LeaderEvent::LeaderStart.name(),
|
|
375
|
-
serde_json::json!({
|
|
376
|
-
"provider": super::helpers::provider_wire(plan.provider),
|
|
377
|
-
"mode": serde_json::to_value(plan.mode)?,
|
|
378
|
-
"session_name": plan.session_name.as_ref().map(|s| s.as_str().to_string()),
|
|
379
|
-
}),
|
|
380
|
-
)?;
|
|
381
|
-
execute_prepared_leader_start(&prepared, workspace).map(|_| ())
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/// Execute a precomputed leader launch plan.
|
|
385
|
-
///
|
|
386
|
-
/// S0 exposes the seam and return model only. Lane 2 owns the real provider/tmux
|
|
387
|
-
/// execution and workspace-socket enforcement.
|
|
388
|
-
pub fn execute_leader_plan(
|
|
389
|
-
plan: &LeaderStartPlan,
|
|
390
|
-
workspace: &Path,
|
|
391
|
-
) -> Result<LeaderLaunchOutcome, LeaderError> {
|
|
392
|
-
let ambient_authority = if plan.mode == LeaderStartMode::ExecProvider {
|
|
393
|
-
match ambient_pane_authority_preflight(workspace) {
|
|
394
|
-
Ok(Some(authority)) => Some(authority),
|
|
395
|
-
Ok(None) => {
|
|
396
|
-
return Err(LeaderError::Validation(
|
|
397
|
-
"exec provider ambient pane authority missing".to_string(),
|
|
398
|
-
));
|
|
399
|
-
}
|
|
400
|
-
Err(PrepareLeaderStartError::PaneAuthorityRefused(refusal)) => {
|
|
401
|
-
return Ok(LeaderLaunchOutcome::not_started(refusal.reason().as_str()));
|
|
402
|
-
}
|
|
403
|
-
Err(PrepareLeaderStartError::Leader(error)) => return Err(error),
|
|
404
|
-
}
|
|
405
|
-
} else {
|
|
406
|
-
None
|
|
407
|
-
};
|
|
408
|
-
execute_leader_plan_after_ambient_authority(plan, workspace, ambient_authority.as_ref())
|
|
409
|
-
}
|
|
410
|
-
|
|
411
348
|
pub(crate) fn execute_prepared_leader_start(
|
|
412
349
|
prepared: &PreparedLeaderStart,
|
|
413
350
|
workspace: &Path,
|
|
@@ -917,7 +854,7 @@ pub fn leader_session_name(provider: Provider, workspace: &Path) -> SessionName
|
|
|
917
854
|
/// External / attach paths keep the stable `leader_session_name` —
|
|
918
855
|
/// per-launch nonce would break `--attach-session <name>` reattach
|
|
919
856
|
/// semantics.
|
|
920
|
-
|
|
857
|
+
fn managed_leader_session_name(provider: Provider, workspace: &Path) -> SessionName {
|
|
921
858
|
let resolved = resolve_workspace_for_hash(workspace);
|
|
922
859
|
let folder_raw = resolved
|
|
923
860
|
.file_name()
|
|
@@ -936,6 +873,32 @@ pub fn managed_leader_session_name(provider: Provider, workspace: &Path) -> Sess
|
|
|
936
873
|
))
|
|
937
874
|
}
|
|
938
875
|
|
|
876
|
+
fn managed_leader_session_for_launch(provider: Provider, workspace: &Path) -> SessionName {
|
|
877
|
+
let candidates = crate::transport_factory::tmux_workspace_transport(workspace)
|
|
878
|
+
.list_targets()
|
|
879
|
+
.unwrap_or_default()
|
|
880
|
+
.into_iter()
|
|
881
|
+
.map(|target| target.session.as_str().to_string());
|
|
882
|
+
managed_leader_session_from_candidates(provider, workspace, candidates)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
fn managed_leader_session_from_candidates(
|
|
886
|
+
provider: Provider,
|
|
887
|
+
workspace: &Path,
|
|
888
|
+
candidates: impl IntoIterator<Item = String>,
|
|
889
|
+
) -> SessionName {
|
|
890
|
+
let prefix = format!("{LEADER_SESSION_PREFIX}{}-", provider_wire(provider));
|
|
891
|
+
let existing = candidates
|
|
892
|
+
.into_iter()
|
|
893
|
+
.filter(|name| name.starts_with(&prefix))
|
|
894
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
895
|
+
existing
|
|
896
|
+
.into_iter()
|
|
897
|
+
.next()
|
|
898
|
+
.map(SessionName::new)
|
|
899
|
+
.unwrap_or_else(|| managed_leader_session_name(provider, workspace))
|
|
900
|
+
}
|
|
901
|
+
|
|
939
902
|
fn start_argv(
|
|
940
903
|
mode: LeaderStartMode,
|
|
941
904
|
provider: Provider,
|
|
@@ -1025,11 +988,10 @@ fn normalized_provider_args(provider_args: &[String]) -> impl Iterator<Item = St
|
|
|
1025
988
|
.cloned()
|
|
1026
989
|
}
|
|
1027
990
|
|
|
1028
|
-
// 0.3.28 Step 2: `managed_team_session_name` deleted. Both
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
// root of E49/E51/E53/E57-3/E60.
|
|
991
|
+
// 0.3.28 Step 2: `managed_team_session_name` deleted. Both paths use the
|
|
992
|
+
// dedicated leader-session namespace; managed launches select a live prefix
|
|
993
|
+
// candidate before falling back to the nonce helper, while external/attach
|
|
994
|
+
// launches use the stable `leader_session_name`.
|
|
1033
995
|
|
|
1034
996
|
fn managed_client_argv(
|
|
1035
997
|
workspace: &Path,
|
|
@@ -1061,6 +1023,14 @@ fn managed_client_argv(
|
|
|
1061
1023
|
fn execute_managed_leader_plan(
|
|
1062
1024
|
plan: &LeaderStartPlan,
|
|
1063
1025
|
workspace: &Path,
|
|
1026
|
+
) -> Result<LeaderLaunchOutcome, LeaderError> {
|
|
1027
|
+
execute_managed_leader_plan_attempt(plan, workspace, true)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
fn execute_managed_leader_plan_attempt(
|
|
1031
|
+
plan: &LeaderStartPlan,
|
|
1032
|
+
workspace: &Path,
|
|
1033
|
+
allow_attach_recovery: bool,
|
|
1064
1034
|
) -> Result<LeaderLaunchOutcome, LeaderError> {
|
|
1065
1035
|
let Some(session) = plan.session_name.as_ref() else {
|
|
1066
1036
|
return Err(LeaderError::Start(
|
|
@@ -1078,19 +1048,76 @@ fn execute_managed_leader_plan(
|
|
|
1078
1048
|
// site. Semantics unchanged; managed leader hosting stays
|
|
1079
1049
|
// tmux-only per design §Batch 6.
|
|
1080
1050
|
let transport = crate::transport_factory::tmux_workspace_transport(workspace);
|
|
1081
|
-
let
|
|
1082
|
-
|
|
1051
|
+
let session_existed_before = transport.has_session(session).map_err(|error| {
|
|
1052
|
+
LeaderError::Start(format!("managed leader session probe failed: {error}"))
|
|
1053
|
+
})?;
|
|
1054
|
+
let spawned = ensure_managed_leader_pane(
|
|
1055
|
+
&transport,
|
|
1056
|
+
session,
|
|
1057
|
+
window,
|
|
1058
|
+
plan,
|
|
1059
|
+
workspace,
|
|
1060
|
+
session_existed_before,
|
|
1061
|
+
)?;
|
|
1062
|
+
if let Err(error) = persist_managed_leader_binding(plan, workspace, &spawned) {
|
|
1063
|
+
cleanup_managed_leader_resources(
|
|
1064
|
+
&transport,
|
|
1065
|
+
session,
|
|
1066
|
+
&spawned,
|
|
1067
|
+
session_existed_before,
|
|
1068
|
+
workspace,
|
|
1069
|
+
"binding_persist_failed",
|
|
1070
|
+
);
|
|
1071
|
+
return Err(error);
|
|
1072
|
+
}
|
|
1083
1073
|
spawn_managed_provider_startup_prompt_handler(
|
|
1084
1074
|
plan.provider,
|
|
1085
1075
|
workspace.to_path_buf(),
|
|
1086
1076
|
spawned.pane_id.as_str().to_string(),
|
|
1087
1077
|
);
|
|
1088
|
-
let process = run_leader_argv(&plan.argv, &BTreeMap::new(), plan, workspace)
|
|
1078
|
+
let process = match run_leader_argv(&plan.argv, &BTreeMap::new(), plan, workspace) {
|
|
1079
|
+
Ok(process) => process,
|
|
1080
|
+
Err(error) => {
|
|
1081
|
+
cleanup_managed_leader_resources(
|
|
1082
|
+
&transport,
|
|
1083
|
+
session,
|
|
1084
|
+
&spawned,
|
|
1085
|
+
session_existed_before,
|
|
1086
|
+
workspace,
|
|
1087
|
+
"launcher_spawn_failed",
|
|
1088
|
+
);
|
|
1089
|
+
return Err(error);
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1089
1092
|
let code = process.status.code();
|
|
1090
1093
|
if !process.status.success() {
|
|
1091
|
-
|
|
1094
|
+
cleanup_managed_leader_resources(
|
|
1095
|
+
&transport,
|
|
1096
|
+
session,
|
|
1097
|
+
&spawned,
|
|
1098
|
+
session_existed_before,
|
|
1099
|
+
workspace,
|
|
1100
|
+
"launcher_exit_failed",
|
|
1101
|
+
);
|
|
1102
|
+
let failure = LeaderError::Start(leader_launcher_failure(&process));
|
|
1103
|
+
if allow_attach_recovery {
|
|
1104
|
+
if let Ok(retry_plan) = managed_leader_attach_recovery_plan(plan, workspace) {
|
|
1105
|
+
return execute_managed_leader_plan_attempt(&retry_plan, workspace, false);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return Err(failure);
|
|
1109
|
+
}
|
|
1110
|
+
if let Err(error) = ensure_managed_provider_live_after_attach(&transport, &spawned) {
|
|
1111
|
+
cleanup_managed_leader_resources(
|
|
1112
|
+
&transport,
|
|
1113
|
+
session,
|
|
1114
|
+
&spawned,
|
|
1115
|
+
session_existed_before,
|
|
1116
|
+
workspace,
|
|
1117
|
+
"provider_not_live_after_attach",
|
|
1118
|
+
);
|
|
1119
|
+
return Err(error);
|
|
1092
1120
|
}
|
|
1093
|
-
ensure_managed_provider_live_after_attach(&transport, &spawned)?;
|
|
1094
1121
|
Ok(LeaderLaunchOutcome {
|
|
1095
1122
|
status: LeaderLaunchStatus::Exited,
|
|
1096
1123
|
exit_code: code,
|
|
@@ -1099,21 +1126,36 @@ fn execute_managed_leader_plan(
|
|
|
1099
1126
|
})
|
|
1100
1127
|
}
|
|
1101
1128
|
|
|
1129
|
+
fn managed_leader_attach_recovery_plan(
|
|
1130
|
+
plan: &LeaderStartPlan,
|
|
1131
|
+
workspace: &Path,
|
|
1132
|
+
) -> Result<LeaderStartPlan, LeaderError> {
|
|
1133
|
+
let session = managed_leader_session_for_launch(plan.provider, workspace);
|
|
1134
|
+
let attach_mode = if plan.argv.iter().any(|arg| arg == "switch-client") {
|
|
1135
|
+
ManagedClientAttachMode::SwitchClient
|
|
1136
|
+
} else {
|
|
1137
|
+
ManagedClientAttachMode::AttachSession
|
|
1138
|
+
};
|
|
1139
|
+
let argv = managed_client_argv(workspace, &session, plan.provider, attach_mode)?;
|
|
1140
|
+
let mut retry_plan = plan.clone();
|
|
1141
|
+
retry_plan.session_name = Some(session);
|
|
1142
|
+
retry_plan.argv = argv;
|
|
1143
|
+
Ok(retry_plan)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1102
1146
|
fn ensure_managed_leader_pane(
|
|
1103
1147
|
transport: &dyn Transport,
|
|
1104
1148
|
session: &SessionName,
|
|
1105
1149
|
window: &WindowName,
|
|
1106
1150
|
plan: &LeaderStartPlan,
|
|
1107
1151
|
workspace: &Path,
|
|
1152
|
+
session_existed_before: bool,
|
|
1108
1153
|
) -> Result<SpawnResult, LeaderError> {
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
|
|
1114
|
-
// tmux's own duplicate-session error will surface — better than
|
|
1115
|
-
// silently attaching a second client.
|
|
1116
|
-
if transport.has_session(session).unwrap_or(false) {
|
|
1154
|
+
// A-22: a managed launch may reuse an existing provider leader session;
|
|
1155
|
+
// only a newly selected nonce session is absent before this probe.
|
|
1156
|
+
// Reusing the existing session adds one pane; a new session uses the
|
|
1157
|
+
// first-spawn path.
|
|
1158
|
+
if session_existed_before {
|
|
1117
1159
|
// 0.4.x (CR C-1 + C-2): leader env_unset reuses the worker
|
|
1118
1160
|
// provider_env_unsets (single source of truth) + spawn through the
|
|
1119
1161
|
// leader shell wrapper so provider exit returns to a shell, not
|
|
@@ -1148,6 +1190,45 @@ fn ensure_managed_leader_pane(
|
|
|
1148
1190
|
}
|
|
1149
1191
|
}
|
|
1150
1192
|
|
|
1193
|
+
fn cleanup_managed_leader_resources(
|
|
1194
|
+
transport: &dyn Transport,
|
|
1195
|
+
session: &SessionName,
|
|
1196
|
+
spawned: &SpawnResult,
|
|
1197
|
+
session_existed_before: bool,
|
|
1198
|
+
workspace: &Path,
|
|
1199
|
+
reason: &str,
|
|
1200
|
+
) {
|
|
1201
|
+
let (action, result) = if session_existed_before {
|
|
1202
|
+
(
|
|
1203
|
+
"kill_pane",
|
|
1204
|
+
transport
|
|
1205
|
+
.kill_pane(&spawned.pane_id)
|
|
1206
|
+
.map(|_| "ok".to_string())
|
|
1207
|
+
.unwrap_or_else(|error| error.to_string()),
|
|
1208
|
+
)
|
|
1209
|
+
} else {
|
|
1210
|
+
(
|
|
1211
|
+
"kill_session",
|
|
1212
|
+
transport
|
|
1213
|
+
.kill_session(session)
|
|
1214
|
+
.map(|_| "ok".to_string())
|
|
1215
|
+
.unwrap_or_else(|error| error.to_string()),
|
|
1216
|
+
)
|
|
1217
|
+
};
|
|
1218
|
+
write_leader_startup_prompt_event(
|
|
1219
|
+
workspace,
|
|
1220
|
+
"leader.launcher.rollback",
|
|
1221
|
+
serde_json::json!({
|
|
1222
|
+
"reason": reason,
|
|
1223
|
+
"session": session.as_str(),
|
|
1224
|
+
"pane_id": spawned.pane_id.as_str(),
|
|
1225
|
+
"session_preexisted": session_existed_before,
|
|
1226
|
+
"action": action,
|
|
1227
|
+
"result": result,
|
|
1228
|
+
}),
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1151
1232
|
/// 0.4.x (CR C-1): leader provider env-unset list — SINGLE SOURCE OF TRUTH
|
|
1152
1233
|
/// reused from worker spawn (`profile_launch::provider_env_unsets`).
|
|
1153
1234
|
/// Audit grep guard: this function MUST be the only place in
|
|
@@ -1156,7 +1237,7 @@ fn ensure_managed_leader_pane(
|
|
|
1156
1237
|
/// or the underlying `provider_env_unsets`. Use `AuthMode::Subscription` —
|
|
1157
1238
|
/// the leader is the user's interactive provider, never CompatibleApi/
|
|
1158
1239
|
/// OfficialApi which are worker-only auth modes today.
|
|
1159
|
-
|
|
1240
|
+
fn leader_env_unset_for_provider(provider: Provider) -> Vec<String> {
|
|
1160
1241
|
crate::lifecycle::profile_launch::provider_env_unsets(
|
|
1161
1242
|
provider,
|
|
1162
1243
|
crate::model::enums::AuthMode::Subscription,
|
|
@@ -1270,7 +1351,7 @@ pub enum LeaderProviderHealth {
|
|
|
1270
1351
|
/// common interactive shells; missing entries here are false negatives
|
|
1271
1352
|
/// (shell looks like provider absent → health says Alive) which is the
|
|
1272
1353
|
/// safe default per the CR R6 conservative-Alive rule.
|
|
1273
|
-
|
|
1354
|
+
fn is_interactive_shell_basename(name: &str) -> bool {
|
|
1274
1355
|
let trimmed = name.trim().to_ascii_lowercase();
|
|
1275
1356
|
let basename = std::path::Path::new(&trimmed)
|
|
1276
1357
|
.file_name()
|
|
@@ -1823,6 +1904,13 @@ fn run_leader_argv(
|
|
|
1823
1904
|
"leader launch argv is empty".to_string(),
|
|
1824
1905
|
));
|
|
1825
1906
|
};
|
|
1907
|
+
let diagnostics_path = launcher_diagnostics_path(workspace);
|
|
1908
|
+
if let Err(error) = write_launcher_diagnostics_header(&diagnostics_path, plan, argv) {
|
|
1909
|
+
return Err(LeaderError::Start(format!(
|
|
1910
|
+
"leader launcher diagnostics unavailable; startup_stage=diagnostics_create; launcher_diagnostics={}; error={error}",
|
|
1911
|
+
diagnostics_path.display()
|
|
1912
|
+
)));
|
|
1913
|
+
}
|
|
1826
1914
|
// 0.4.x regression fix (env-leak scenario 1, in-tmux ExecProvider path):
|
|
1827
1915
|
// the managed-tmux path got the provider env-unset block via the shell
|
|
1828
1916
|
// wrapper (cb9c217), but the ExecProvider in-tmux path here is a direct
|
|
@@ -1835,11 +1923,15 @@ fn run_leader_argv(
|
|
|
1835
1923
|
// leader_env_unset_for_provider).
|
|
1836
1924
|
let env_unset = leader_env_unset_for_provider(plan.provider);
|
|
1837
1925
|
let mut command = Command::new(program);
|
|
1838
|
-
command
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1926
|
+
command.args(argv.iter().skip(1)).stdin(Stdio::inherit());
|
|
1927
|
+
let capture_stdout = !std::io::stdout().is_terminal();
|
|
1928
|
+
command.stdout(if capture_stdout {
|
|
1929
|
+
Stdio::piped()
|
|
1930
|
+
} else {
|
|
1931
|
+
Stdio::inherit()
|
|
1932
|
+
});
|
|
1933
|
+
let capture_stderr =
|
|
1934
|
+
plan.mode != LeaderStartMode::ExecProvider || !std::io::stderr().is_terminal();
|
|
1843
1935
|
command.stderr(if capture_stderr {
|
|
1844
1936
|
Stdio::piped()
|
|
1845
1937
|
} else {
|
|
@@ -1862,7 +1954,35 @@ fn run_leader_argv(
|
|
|
1862
1954
|
command.env_remove("TMUX");
|
|
1863
1955
|
command.env_remove("TMUX_PANE");
|
|
1864
1956
|
}
|
|
1865
|
-
let mut child = command.spawn()
|
|
1957
|
+
let mut child = command.spawn().map_err(|error| {
|
|
1958
|
+
LeaderError::Start(format!(
|
|
1959
|
+
"leader launcher spawn failed; startup_stage=spawn; launcher_diagnostics={}; error={error}",
|
|
1960
|
+
diagnostics_path.display()
|
|
1961
|
+
))
|
|
1962
|
+
})?;
|
|
1963
|
+
append_launcher_diagnostics(&diagnostics_path, "startup_stage=spawned\n").map_err(|error| {
|
|
1964
|
+
LeaderError::Start(format!(
|
|
1965
|
+
"leader launcher diagnostics write failed; startup_stage=spawned; launcher_diagnostics={}; error={error}",
|
|
1966
|
+
diagnostics_path.display()
|
|
1967
|
+
))
|
|
1968
|
+
})?;
|
|
1969
|
+
let stdout_reader = child.stdout.take().map(|mut child_stdout| {
|
|
1970
|
+
std::thread::spawn(move || {
|
|
1971
|
+
let mut captured = Vec::new();
|
|
1972
|
+
let mut buf = [0_u8; 4096];
|
|
1973
|
+
loop {
|
|
1974
|
+
let Ok(read) = child_stdout.read(&mut buf) else {
|
|
1975
|
+
break;
|
|
1976
|
+
};
|
|
1977
|
+
if read == 0 {
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
let _ = std::io::stdout().write_all(&buf[..read]);
|
|
1981
|
+
captured.extend_from_slice(&buf[..read]);
|
|
1982
|
+
}
|
|
1983
|
+
captured
|
|
1984
|
+
})
|
|
1985
|
+
});
|
|
1866
1986
|
let stderr_reader = child.stderr.take().map(|mut child_stderr| {
|
|
1867
1987
|
std::thread::spawn(move || {
|
|
1868
1988
|
let mut captured = Vec::new();
|
|
@@ -1875,7 +1995,7 @@ fn run_leader_argv(
|
|
|
1875
1995
|
break;
|
|
1876
1996
|
}
|
|
1877
1997
|
let _ = std::io::stderr().write_all(&buf[..read]);
|
|
1878
|
-
|
|
1998
|
+
captured.extend_from_slice(&buf[..read]);
|
|
1879
1999
|
}
|
|
1880
2000
|
captured
|
|
1881
2001
|
})
|
|
@@ -1884,6 +2004,13 @@ fn run_leader_argv(
|
|
|
1884
2004
|
spawn_exec_provider_startup_prompt_handler(plan.provider, workspace.to_path_buf());
|
|
1885
2005
|
}
|
|
1886
2006
|
let status = child.wait().map_err(LeaderError::Io)?;
|
|
2007
|
+
let stdout = match stdout_reader {
|
|
2008
|
+
Some(reader) => match reader.join() {
|
|
2009
|
+
Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
|
|
2010
|
+
Err(_) => String::new(),
|
|
2011
|
+
},
|
|
2012
|
+
None => String::new(),
|
|
2013
|
+
};
|
|
1887
2014
|
let stderr = match stderr_reader {
|
|
1888
2015
|
Some(reader) => match reader.join() {
|
|
1889
2016
|
Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
|
|
@@ -1891,7 +2018,62 @@ fn run_leader_argv(
|
|
|
1891
2018
|
},
|
|
1892
2019
|
None => String::new(),
|
|
1893
2020
|
};
|
|
1894
|
-
|
|
2021
|
+
append_launcher_diagnostics(
|
|
2022
|
+
&diagnostics_path,
|
|
2023
|
+
&format!(
|
|
2024
|
+
"startup_stage=exited\nexit_code={}\nchild_stdout={}\nchild_stderr={}\n",
|
|
2025
|
+
status
|
|
2026
|
+
.code()
|
|
2027
|
+
.map(|code| code.to_string())
|
|
2028
|
+
.unwrap_or_else(|| "signal".to_string()),
|
|
2029
|
+
crate::redaction::redact_external_text(&stdout),
|
|
2030
|
+
crate::redaction::redact_external_text(&stderr),
|
|
2031
|
+
),
|
|
2032
|
+
)
|
|
2033
|
+
.map_err(|error| {
|
|
2034
|
+
LeaderError::Start(format!(
|
|
2035
|
+
"leader launcher diagnostics write failed; startup_stage=exited; launcher_diagnostics={}; error={error}",
|
|
2036
|
+
diagnostics_path.display()
|
|
2037
|
+
))
|
|
2038
|
+
})?;
|
|
2039
|
+
Ok(LeaderProcessExit {
|
|
2040
|
+
status,
|
|
2041
|
+
stderr,
|
|
2042
|
+
diagnostics_path,
|
|
2043
|
+
})
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
fn launcher_diagnostics_path(workspace: &Path) -> PathBuf {
|
|
2047
|
+
let stamp = chrono::Utc::now().format("%Y%m%d-%H%M%S%.6f");
|
|
2048
|
+
workspace.join(".team").join("logs").join(format!(
|
|
2049
|
+
"launcher-diagnostics-{stamp}-{}.log",
|
|
2050
|
+
std::process::id()
|
|
2051
|
+
))
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
fn write_launcher_diagnostics_header(
|
|
2055
|
+
path: &Path,
|
|
2056
|
+
plan: &LeaderStartPlan,
|
|
2057
|
+
argv: &[String],
|
|
2058
|
+
) -> Result<(), std::io::Error> {
|
|
2059
|
+
if let Some(parent) = path.parent() {
|
|
2060
|
+
std::fs::create_dir_all(parent)?;
|
|
2061
|
+
}
|
|
2062
|
+
std::fs::write(
|
|
2063
|
+
path,
|
|
2064
|
+
format!(
|
|
2065
|
+
"startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n",
|
|
2066
|
+
plan.mode,
|
|
2067
|
+
provider_wire(plan.provider),
|
|
2068
|
+
crate::redaction::redact_external_text(&argv.join(" ")),
|
|
2069
|
+
),
|
|
2070
|
+
)
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
fn append_launcher_diagnostics(path: &Path, text: &str) -> Result<(), std::io::Error> {
|
|
2074
|
+
use std::io::Write as _;
|
|
2075
|
+
let mut file = std::fs::OpenOptions::new().append(true).open(path)?;
|
|
2076
|
+
file.write_all(text.as_bytes())
|
|
1895
2077
|
}
|
|
1896
2078
|
|
|
1897
2079
|
const LEADER_STDERR_LIMIT: usize = 8192;
|
|
@@ -1899,6 +2081,7 @@ const LEADER_STDERR_LIMIT: usize = 8192;
|
|
|
1899
2081
|
struct LeaderProcessExit {
|
|
1900
2082
|
status: std::process::ExitStatus,
|
|
1901
2083
|
stderr: String,
|
|
2084
|
+
diagnostics_path: PathBuf,
|
|
1902
2085
|
}
|
|
1903
2086
|
|
|
1904
2087
|
fn push_bounded_stderr(captured: &mut Vec<u8>, chunk: &[u8]) {
|
|
@@ -1923,7 +2106,11 @@ fn leader_launcher_failure(process: &LeaderProcessExit) -> String {
|
|
|
1923
2106
|
.code()
|
|
1924
2107
|
.map(|code| code.to_string())
|
|
1925
2108
|
.unwrap_or_else(|| "signal".to_string());
|
|
1926
|
-
|
|
2109
|
+
format!(
|
|
2110
|
+
"{}; startup_stage=exited; launcher_diagnostics={}",
|
|
2111
|
+
format_launcher_failure(&status, process.status.code().is_none(), &process.stderr),
|
|
2112
|
+
process.diagnostics_path.display()
|
|
2113
|
+
)
|
|
1927
2114
|
}
|
|
1928
2115
|
|
|
1929
2116
|
fn format_launcher_failure(status: &str, signaled: bool, raw_stderr: &str) -> String {
|
|
@@ -2017,7 +2204,7 @@ fn tmux_transport_for_current_pane() -> TmuxBackend {
|
|
|
2017
2204
|
.unwrap_or_else(crate::transport_factory::tmux_default_transport)
|
|
2018
2205
|
}
|
|
2019
2206
|
|
|
2020
|
-
|
|
2207
|
+
fn handle_exec_provider_startup_prompts(
|
|
2021
2208
|
provider: Provider,
|
|
2022
2209
|
workspace: &Path,
|
|
2023
2210
|
pane_id: &str,
|
|
@@ -2161,9 +2348,9 @@ mod tests {
|
|
|
2161
2348
|
};
|
|
2162
2349
|
|
|
2163
2350
|
use super::{
|
|
2164
|
-
ensure_managed_provider_live_after_attach,
|
|
2165
|
-
|
|
2166
|
-
|
|
2351
|
+
ensure_managed_provider_live_after_attach, execute_leader_plan_after_ambient_authority,
|
|
2352
|
+
format_launcher_failure, handle_exec_provider_startup_prompts,
|
|
2353
|
+
is_interactive_shell_basename, push_bounded_stderr, shlex_quote,
|
|
2167
2354
|
VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
|
|
2168
2355
|
};
|
|
2169
2356
|
|
|
@@ -2606,12 +2793,26 @@ mod tests {
|
|
|
2606
2793
|
assert_eq!(sent[0].1, vec![Key::Enter]);
|
|
2607
2794
|
}
|
|
2608
2795
|
|
|
2796
|
+
#[test]
|
|
2797
|
+
fn interactive_shell_basename_recognizes_supported_shells() {
|
|
2798
|
+
let shells = [
|
|
2799
|
+
"zsh", "bash", "sh", "fish", "dash", "ksh", "tcsh", "csh", "ash", "mksh", "yash",
|
|
2800
|
+
"elvish", "nu", "nushell", "xonsh",
|
|
2801
|
+
];
|
|
2802
|
+
for shell in shells {
|
|
2803
|
+
assert!(is_interactive_shell_basename(shell));
|
|
2804
|
+
assert!(is_interactive_shell_basename(&shell.to_uppercase()));
|
|
2805
|
+
}
|
|
2806
|
+
for provider in ["claude", "codex", "copilot", "gemini"] {
|
|
2807
|
+
assert!(!is_interactive_shell_basename(provider));
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2609
2811
|
// ═══════════════════════════════════════════════════════════════════
|
|
2610
2812
|
// 0.4.10+ mirror-session fix v2 (option B: independent session per launch).
|
|
2611
2813
|
//
|
|
2612
|
-
//
|
|
2613
|
-
//
|
|
2614
|
-
// tmux session, matching `tmux new-session + claude` semantics.
|
|
2814
|
+
// The managed selector reuses a live provider leader prefix when present;
|
|
2815
|
+
// its no-candidate fallback appends a per-launch nonce.
|
|
2615
2816
|
// ═══════════════════════════════════════════════════════════════════
|
|
2616
2817
|
|
|
2617
2818
|
#[test]
|
|
@@ -2684,37 +2885,84 @@ mod tests {
|
|
|
2684
2885
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
2685
2886
|
}
|
|
2686
2887
|
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
/// keep `leader_session_name`.
|
|
2888
|
+
// Tombstone for `managed_path_uses_nonce_session_name_grep_guard`: A-22
|
|
2889
|
+
// replaced that implementation-shape assertion with the contract guard
|
|
2890
|
+
// below. Keep the old name here for audit/search provenance.
|
|
2691
2891
|
#[test]
|
|
2692
|
-
fn
|
|
2892
|
+
fn a22_launch_session_reuse_contract() {
|
|
2693
2893
|
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
2694
2894
|
let start_rs = manifest.join("src").join("leader").join("start.rs");
|
|
2695
2895
|
let contents = std::fs::read_to_string(&start_rs).expect("read leader/start.rs");
|
|
2696
|
-
// leader_start_plan must dispatch to managed_leader_session_name
|
|
2697
|
-
// on the managed path.
|
|
2698
2896
|
let start = contents
|
|
2699
|
-
.find("
|
|
2700
|
-
.expect("
|
|
2897
|
+
.find("fn leader_start_plan_with_ambient_authority(")
|
|
2898
|
+
.expect("leader_start_plan_with_ambient_authority must exist");
|
|
2701
2899
|
let end = contents[start + 1..]
|
|
2702
|
-
.find("\
|
|
2703
|
-
.
|
|
2704
|
-
.map(|p| start + 1 + p)
|
|
2900
|
+
.find("\nfn ")
|
|
2901
|
+
.map(|offset| start + 1 + offset)
|
|
2705
2902
|
.unwrap_or(contents.len());
|
|
2706
2903
|
let body = &contents[start..end];
|
|
2707
2904
|
assert!(
|
|
2708
|
-
body.contains("
|
|
2709
|
-
"
|
|
2710
|
-
the managed path (mirror-session fix v2); body excerpt: {body}"
|
|
2905
|
+
body.contains("managed_leader_session_for_launch(provider, workspace)"),
|
|
2906
|
+
"managed launch must select from the A-22 candidate contract; body excerpt: {body}"
|
|
2711
2907
|
);
|
|
2712
|
-
// The external_path branch must still use the stable name.
|
|
2713
2908
|
assert!(
|
|
2714
2909
|
body.contains("leader_session_name(provider, workspace)"),
|
|
2715
2910
|
"leader_start_plan must keep leader_session_name on the \
|
|
2716
2911
|
external/attach paths; body excerpt: {body}"
|
|
2717
2912
|
);
|
|
2913
|
+
let workspace =
|
|
2914
|
+
std::env::temp_dir().join(format!("ta_rs_a22_contract_{}", std::process::id()));
|
|
2915
|
+
let reused = super::managed_leader_session_from_candidates(
|
|
2916
|
+
Provider::ClaudeCode,
|
|
2917
|
+
&workspace,
|
|
2918
|
+
vec![
|
|
2919
|
+
"team-worker-not-a-leader".to_string(),
|
|
2920
|
+
"team-agent-leader-claude_code-z".to_string(),
|
|
2921
|
+
"team-agent-leader-codex-other".to_string(),
|
|
2922
|
+
"team-agent-leader-claude_code-a".to_string(),
|
|
2923
|
+
],
|
|
2924
|
+
);
|
|
2925
|
+
assert_eq!(
|
|
2926
|
+
reused.as_str(),
|
|
2927
|
+
"team-agent-leader-claude_code-a",
|
|
2928
|
+
"managed launch must deterministically reuse a matching leader prefix session"
|
|
2929
|
+
);
|
|
2930
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
#[test]
|
|
2934
|
+
fn a22_launch_session_without_candidate_gets_nonce() {
|
|
2935
|
+
let workspace =
|
|
2936
|
+
std::env::temp_dir().join(format!("ta_rs_a22_nonce_{}", std::process::id()));
|
|
2937
|
+
let fresh = super::managed_leader_session_from_candidates(
|
|
2938
|
+
Provider::ClaudeCode,
|
|
2939
|
+
&workspace,
|
|
2940
|
+
std::iter::empty(),
|
|
2941
|
+
);
|
|
2942
|
+
assert!(
|
|
2943
|
+
fresh.as_str().starts_with("team-agent-leader-claude_code-"),
|
|
2944
|
+
"no candidate must create a leader-prefixed managed session: {}",
|
|
2945
|
+
fresh.as_str()
|
|
2946
|
+
);
|
|
2947
|
+
assert_ne!(
|
|
2948
|
+
fresh.as_str(),
|
|
2949
|
+
super::leader_session_name(Provider::ClaudeCode, &workspace).as_str(),
|
|
2950
|
+
"managed fallback must carry a per-launch nonce"
|
|
2951
|
+
);
|
|
2952
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
#[test]
|
|
2956
|
+
fn a22_external_attach_session_name_remains_stable() {
|
|
2957
|
+
let workspace =
|
|
2958
|
+
std::env::temp_dir().join(format!("ta_rs_a22_external_{}", std::process::id()));
|
|
2959
|
+
let stable = super::leader_session_name(Provider::ClaudeCode, &workspace);
|
|
2960
|
+
assert_eq!(
|
|
2961
|
+
stable.as_str(),
|
|
2962
|
+
super::leader_session_name(Provider::ClaudeCode, &workspace).as_str(),
|
|
2963
|
+
"external/attach paths must keep the stable workspace-keyed session name"
|
|
2964
|
+
);
|
|
2965
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
2718
2966
|
}
|
|
2719
2967
|
|
|
2720
2968
|
#[test]
|