@team-agent/installer 0.2.10 → 0.3.0
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 +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1077 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1141 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +436 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1063 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
- package/crates/team-agent/src/leader/rediscover.rs +1099 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +271 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +487 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +685 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +388 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +542 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +537 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +582 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +656 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
- package/crates/team-agent/src/tmux_backend.rs +758 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +90 -106
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -83
- package/src/team_agent/coordinator/lifecycle.py +0 -363
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -200
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -111
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -254
- package/src/team_agent/messaging/delivery.py +0 -473
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -457
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -86
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1239
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -143
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -602
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- package/src/team_agent/watch/__init__.py +0 -145
|
@@ -1,554 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import copy
|
|
4
|
-
from datetime import datetime, timezone
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from team_agent.events import EventLog
|
|
9
|
-
from team_agent.message_store import MessageStore
|
|
10
|
-
from team_agent.permissions import resolve_permissions
|
|
11
|
-
from team_agent.display.backend import display_backend_has_worker_views, display_backend_opens_before_leader_rebind, resolve_restart_display_backend
|
|
12
|
-
from team_agent.display.close import close_team_display_backends
|
|
13
|
-
from team_agent.display.rebuild import rebuild_restart_display_after_rebind
|
|
14
|
-
from team_agent.restart.selection import select_restart_state
|
|
15
|
-
from team_agent.restart.snapshot import save_team_runtime_snapshot
|
|
16
|
-
from team_agent.spec import load_spec
|
|
17
|
-
from team_agent.state import (
|
|
18
|
-
check_team_owner,
|
|
19
|
-
compact_team_state,
|
|
20
|
-
populate_team_owner_from_env,
|
|
21
|
-
save_runtime_state,
|
|
22
|
-
team_state_key,
|
|
23
|
-
write_team_state,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None) -> dict[str, Any]:
|
|
27
|
-
# Lazy-import everything from team_agent.runtime so existing tests that
|
|
28
|
-
# patch runtime.shell_resume_command_for_agent / runtime.run_cmd /
|
|
29
|
-
# runtime.start_coordinator / runtime.get_adapter continue to take effect
|
|
30
|
-
# at call time. Runtime re-exports the provider helpers, so this also
|
|
31
|
-
# routes through the providers module without binding it directly.
|
|
32
|
-
from team_agent.runtime import (
|
|
33
|
-
ResumeUnavailable,
|
|
34
|
-
RuntimeError,
|
|
35
|
-
_attach_profile_resume_root,
|
|
36
|
-
_attach_team_profile_dirs,
|
|
37
|
-
_capture_agent_session,
|
|
38
|
-
_clear_session_capture_fields,
|
|
39
|
-
_close_ghostty_display,
|
|
40
|
-
_compile_team_dir_spec,
|
|
41
|
-
_effective_runtime_config,
|
|
42
|
-
_ensure_agent_start_requirements,
|
|
43
|
-
_handle_startup_prompts_and_verify_window,
|
|
44
|
-
_is_team_doc_dir,
|
|
45
|
-
_open_worker_displays,
|
|
46
|
-
_prepare_resume_state,
|
|
47
|
-
_spec_team_dir,
|
|
48
|
-
_tmux_session_conflict_error,
|
|
49
|
-
_tmux_session_exists,
|
|
50
|
-
_tmux_start_command_for_agent_window,
|
|
51
|
-
_tmux_window_exists,
|
|
52
|
-
ensure_workspace_dirs,
|
|
53
|
-
get_adapter,
|
|
54
|
-
run_cmd,
|
|
55
|
-
shell_command_for_agent,
|
|
56
|
-
shell_resume_command_for_agent,
|
|
57
|
-
start_coordinator,
|
|
58
|
-
)
|
|
59
|
-
state = select_restart_state(workspace, team)
|
|
60
|
-
gate = check_team_owner(state)
|
|
61
|
-
if gate:
|
|
62
|
-
return gate
|
|
63
|
-
spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
|
|
64
|
-
team_dir = Path(str(state.get("team_dir"))) if state.get("team_dir") else _spec_team_dir(spec_path, workspace)
|
|
65
|
-
if _is_team_doc_dir(team_dir):
|
|
66
|
-
compiled = _compile_team_dir_spec(team_dir, workspace)
|
|
67
|
-
spec = compiled["spec"]
|
|
68
|
-
spec_path = team_dir / "team.spec.yaml"
|
|
69
|
-
state["spec_path"] = str(spec_path)
|
|
70
|
-
else:
|
|
71
|
-
if not spec_path.exists():
|
|
72
|
-
raise RuntimeError(f"missing spec for restart: {spec_path}")
|
|
73
|
-
spec = load_spec(spec_path)
|
|
74
|
-
_attach_team_profile_dirs(spec, spec_path, workspace, team_dir)
|
|
75
|
-
ensure_workspace_dirs(workspace)
|
|
76
|
-
event_log = EventLog(workspace)
|
|
77
|
-
session_name = state.get("session_name") or spec.get("runtime", {}).get("session_name") or f"team-{spec['team']['name']}"
|
|
78
|
-
state.setdefault("team_dir", str(team_dir))
|
|
79
|
-
if _tmux_session_exists(session_name):
|
|
80
|
-
event_log.write(
|
|
81
|
-
"restart.session_conflict",
|
|
82
|
-
session=session_name,
|
|
83
|
-
action="use a different team name or runtime.session_name; do not terminate existing tmux sessions from restart",
|
|
84
|
-
)
|
|
85
|
-
raise RuntimeError(_tmux_session_conflict_error(session_name))
|
|
86
|
-
runtime_cfg = _effective_runtime_config(spec.get("runtime", {}))
|
|
87
|
-
display_backend = resolve_restart_display_backend(spec, state, event_log)
|
|
88
|
-
# Stage 7 S5 — Slice 6 lifecycle atomicity contract: compute restart_agents
|
|
89
|
-
# early so we can pre-validate resumability BEFORE any destructive teardown
|
|
90
|
-
# (ghostty close, tmux session creation). Without --allow-fresh, every
|
|
91
|
-
# non-paused worker MUST be resumable; if any is not, refuse the operation
|
|
92
|
-
# atomically with a structured result and a restart.atomic_refusal event.
|
|
93
|
-
# No rollback path is needed because nothing has been created yet.
|
|
94
|
-
restart_agents = [
|
|
95
|
-
agent
|
|
96
|
-
for agent in spec.get("agents", [])
|
|
97
|
-
if state.get("agents", {}).get(agent["id"], {}).get("status") != "paused" and not agent.get("paused")
|
|
98
|
-
]
|
|
99
|
-
# cr strict-typing (2026-05-27): refuse the operation deterministically
|
|
100
|
-
# before any decision logic if any persisted first_send_at is corrupt
|
|
101
|
-
# (empty string, 0, False, literal "null", any non-ISO garbage). This
|
|
102
|
-
# avoids silent misclassification through Python truthiness and gives the
|
|
103
|
-
# operator a clear audit signal that state.json is damaged.
|
|
104
|
-
invalid_first_send_at = _collect_corrupt_first_send_at(restart_agents, state)
|
|
105
|
-
if invalid_first_send_at:
|
|
106
|
-
for entry in invalid_first_send_at:
|
|
107
|
-
event_log.write(
|
|
108
|
-
"restart.first_send_at_invalid",
|
|
109
|
-
worker_id=entry["worker_id"],
|
|
110
|
-
raw_first_send_at=entry["raw_first_send_at"],
|
|
111
|
-
raw_first_send_at_type=entry["raw_first_send_at_type"],
|
|
112
|
-
)
|
|
113
|
-
invalid_names = [entry["worker_id"] for entry in invalid_first_send_at]
|
|
114
|
-
return {
|
|
115
|
-
"ok": False,
|
|
116
|
-
"status": "refused",
|
|
117
|
-
"reason": "invalid_first_send_at",
|
|
118
|
-
"invalid_first_send_at": invalid_first_send_at,
|
|
119
|
-
"allow_fresh": bool(allow_fresh),
|
|
120
|
-
"error": (
|
|
121
|
-
f"Cannot restart: workers {invalid_names} have a corrupt "
|
|
122
|
-
"first_send_at in state.json (only null/missing or a valid "
|
|
123
|
-
"ISO-8601 UTC timestamp string is accepted). Inspect the "
|
|
124
|
-
"restart.first_send_at_invalid audit events for raw values "
|
|
125
|
-
"and repair state.json before retrying."
|
|
126
|
-
),
|
|
127
|
-
}
|
|
128
|
-
# cr C2: emit one restart.resume_decision event per non-paused worker so
|
|
129
|
-
# every restart attempt produces an auditable per-worker classification.
|
|
130
|
-
# The function returns only refused workers — populated when
|
|
131
|
-
# allow_fresh=False AND at least one interacted worker cannot be repaired.
|
|
132
|
-
refused = _emit_resume_decisions(
|
|
133
|
-
workspace, restart_agents, state, get_adapter, event_log, allow_fresh,
|
|
134
|
-
)
|
|
135
|
-
if refused:
|
|
136
|
-
event_log.write(
|
|
137
|
-
"restart.atomic_refusal",
|
|
138
|
-
unresumable=refused,
|
|
139
|
-
allow_fresh=bool(allow_fresh),
|
|
140
|
-
reason="resume_atomicity",
|
|
141
|
-
)
|
|
142
|
-
return {
|
|
143
|
-
"ok": False,
|
|
144
|
-
"status": "refused",
|
|
145
|
-
"reason": "resume_atomicity",
|
|
146
|
-
"unresumable": refused,
|
|
147
|
-
"allow_fresh": bool(allow_fresh),
|
|
148
|
-
"error": _format_atomic_refusal_error(refused),
|
|
149
|
-
}
|
|
150
|
-
close_team_display_backends(state, event_log)
|
|
151
|
-
for agent_id, agent_state in state.get("agents", {}).items():
|
|
152
|
-
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
153
|
-
state["display_backend"] = display_backend
|
|
154
|
-
_ensure_agent_start_requirements(workspace, restart_agents, event_log, "restart")
|
|
155
|
-
first = True
|
|
156
|
-
restarted: list[dict[str, Any]] = []
|
|
157
|
-
new_agents: dict[str, Any] = {}
|
|
158
|
-
display_jobs: list[tuple[str, dict[str, Any]]] = []
|
|
159
|
-
for agent in spec.get("agents", []):
|
|
160
|
-
previous = state.get("agents", {}).get(agent["id"], {})
|
|
161
|
-
if previous.get("status") == "paused" or agent.get("paused"):
|
|
162
|
-
new_agents[agent["id"]] = dict(previous or {"status": "paused", "provider": agent["provider"]})
|
|
163
|
-
new_agents[agent["id"]]["status"] = "paused"
|
|
164
|
-
continue
|
|
165
|
-
adapter = get_adapter(agent["provider"])
|
|
166
|
-
if not adapter.is_installed():
|
|
167
|
-
event_log.write(
|
|
168
|
-
"restart.provider_missing",
|
|
169
|
-
agent_id=agent["id"],
|
|
170
|
-
provider=agent["provider"],
|
|
171
|
-
command=adapter.command_name,
|
|
172
|
-
)
|
|
173
|
-
raise RuntimeError(
|
|
174
|
-
f"Provider {agent['provider']} command {adapter.command_name!r} not found for agent {agent['id']}"
|
|
175
|
-
)
|
|
176
|
-
mcp_config = adapter.mcp_config(workspace, agent["id"])
|
|
177
|
-
mcp_path = adapter.install_mcp(workspace, agent["id"], mcp_config)
|
|
178
|
-
command_agent = copy.deepcopy(agent)
|
|
179
|
-
command_agent["_runtime"] = runtime_cfg
|
|
180
|
-
previous = _attach_profile_resume_root(workspace, command_agent, previous)
|
|
181
|
-
known_session_ids = {
|
|
182
|
-
str(item.get("session_id"))
|
|
183
|
-
for aid, item in {**state.get("agents", {}), **new_agents}.items()
|
|
184
|
-
if aid != agent["id"] and item.get("session_id")
|
|
185
|
-
}
|
|
186
|
-
try:
|
|
187
|
-
previous = _prepare_resume_state(
|
|
188
|
-
workspace,
|
|
189
|
-
agent["id"],
|
|
190
|
-
previous,
|
|
191
|
-
adapter,
|
|
192
|
-
event_log,
|
|
193
|
-
known_session_ids,
|
|
194
|
-
allow_fresh_on_resume_failure=allow_fresh,
|
|
195
|
-
)
|
|
196
|
-
except ResumeUnavailable as exc:
|
|
197
|
-
try:
|
|
198
|
-
adapter.cleanup_mcp(workspace, agent["id"], mcp_path)
|
|
199
|
-
except Exception as cleanup_exc:
|
|
200
|
-
event_log.write(
|
|
201
|
-
"restart.mcp_cleanup_failed",
|
|
202
|
-
agent_id=agent["id"],
|
|
203
|
-
provider=agent["provider"],
|
|
204
|
-
mcp_config=str(mcp_path),
|
|
205
|
-
error=str(cleanup_exc),
|
|
206
|
-
)
|
|
207
|
-
raise RuntimeError(str(exc)) from exc
|
|
208
|
-
restart_mode = "resumed" if previous.get("session_id") else "fresh"
|
|
209
|
-
if restart_mode == "resumed":
|
|
210
|
-
try:
|
|
211
|
-
command = shell_resume_command_for_agent(command_agent, previous, workspace, mcp_config)
|
|
212
|
-
except ResumeUnavailable as exc:
|
|
213
|
-
event_log.write("restart.resume_unavailable", agent_id=agent["id"], error=str(exc))
|
|
214
|
-
if not allow_fresh:
|
|
215
|
-
try:
|
|
216
|
-
adapter.cleanup_mcp(workspace, agent["id"], mcp_path)
|
|
217
|
-
except Exception as cleanup_exc:
|
|
218
|
-
event_log.write(
|
|
219
|
-
"restart.mcp_cleanup_failed",
|
|
220
|
-
agent_id=agent["id"],
|
|
221
|
-
provider=agent["provider"],
|
|
222
|
-
mcp_config=str(mcp_path),
|
|
223
|
-
error=str(cleanup_exc),
|
|
224
|
-
)
|
|
225
|
-
raise RuntimeError(
|
|
226
|
-
f"Cannot resume agent {agent['id']}: {exc}. "
|
|
227
|
-
"Use team-agent restart --allow-fresh only if losing that worker context is acceptable."
|
|
228
|
-
) from exc
|
|
229
|
-
command = shell_command_for_agent(command_agent, workspace, mcp_config)
|
|
230
|
-
restart_mode = "fresh"
|
|
231
|
-
else:
|
|
232
|
-
command = shell_command_for_agent(command_agent, workspace, mcp_config)
|
|
233
|
-
event_log.write("restart.fresh_spawn", agent_id=agent["id"], provider=agent["provider"], reason="session_id_missing")
|
|
234
|
-
event_log.write(
|
|
235
|
-
"restart.agent_start",
|
|
236
|
-
agent_id=agent["id"],
|
|
237
|
-
provider=agent["provider"],
|
|
238
|
-
restart_mode=restart_mode,
|
|
239
|
-
session_id=previous.get("session_id"),
|
|
240
|
-
session=session_name,
|
|
241
|
-
window=agent["id"],
|
|
242
|
-
tmux_start_mode="new-session" if first else "new-window",
|
|
243
|
-
command=command,
|
|
244
|
-
mcp_config=str(mcp_path),
|
|
245
|
-
)
|
|
246
|
-
if first:
|
|
247
|
-
proc = run_cmd(["tmux", "new-session", "-d", "-s", session_name, "-n", agent["id"], "sh", "-lc", command])
|
|
248
|
-
first = False
|
|
249
|
-
else:
|
|
250
|
-
proc = run_cmd(["tmux", "new-window", "-t", session_name, "-n", agent["id"], "sh", "-lc", command])
|
|
251
|
-
if proc.returncode != 0:
|
|
252
|
-
raise RuntimeError(f"Failed to restart agent {agent['id']}: {proc.stderr.strip()}")
|
|
253
|
-
if not _handle_startup_prompts_and_verify_window(
|
|
254
|
-
adapter, event_log, "restart", agent["id"], agent["provider"], session_name, restart_mode
|
|
255
|
-
):
|
|
256
|
-
if restart_mode != "resumed":
|
|
257
|
-
raise RuntimeError(f"Failed to restart agent {agent['id']}: tmux window exited after start")
|
|
258
|
-
if not allow_fresh:
|
|
259
|
-
try:
|
|
260
|
-
adapter.cleanup_mcp(workspace, agent["id"], mcp_path)
|
|
261
|
-
except Exception as cleanup_exc:
|
|
262
|
-
event_log.write(
|
|
263
|
-
"restart.mcp_cleanup_failed",
|
|
264
|
-
agent_id=agent["id"],
|
|
265
|
-
provider=agent["provider"],
|
|
266
|
-
mcp_config=str(mcp_path),
|
|
267
|
-
error=str(cleanup_exc),
|
|
268
|
-
)
|
|
269
|
-
raise RuntimeError(
|
|
270
|
-
f"Cannot resume agent {agent['id']}: resume window exited or did not become visible. "
|
|
271
|
-
"Use team-agent restart --allow-fresh only if losing that worker context is acceptable."
|
|
272
|
-
)
|
|
273
|
-
event_log.write(
|
|
274
|
-
"restart.resume_window_missing_fallback_fresh",
|
|
275
|
-
agent_id=agent["id"],
|
|
276
|
-
provider=agent["provider"],
|
|
277
|
-
session_id=previous.get("session_id"),
|
|
278
|
-
)
|
|
279
|
-
command = shell_command_for_agent(command_agent, workspace, mcp_config)
|
|
280
|
-
restart_mode = "fresh"
|
|
281
|
-
tmux_cmd, tmux_start_mode = _tmux_start_command_for_agent_window(session_name, agent["id"], command)
|
|
282
|
-
event_log.write(
|
|
283
|
-
"restart.agent_start",
|
|
284
|
-
agent_id=agent["id"],
|
|
285
|
-
provider=agent["provider"],
|
|
286
|
-
restart_mode=restart_mode,
|
|
287
|
-
session_id=None,
|
|
288
|
-
session=session_name,
|
|
289
|
-
window=agent["id"],
|
|
290
|
-
tmux_start_mode=tmux_start_mode,
|
|
291
|
-
command=command,
|
|
292
|
-
mcp_config=str(mcp_path),
|
|
293
|
-
)
|
|
294
|
-
proc = run_cmd(tmux_cmd)
|
|
295
|
-
if proc.returncode != 0:
|
|
296
|
-
raise RuntimeError(f"Failed to restart agent {agent['id']} fresh after resume exit: {proc.stderr.strip()}")
|
|
297
|
-
if not _handle_startup_prompts_and_verify_window(
|
|
298
|
-
adapter, event_log, "restart", agent["id"], agent["provider"], session_name, restart_mode
|
|
299
|
-
):
|
|
300
|
-
raise RuntimeError(f"Failed to restart agent {agent['id']} fresh: tmux window exited after start")
|
|
301
|
-
spawn_time = datetime.now(timezone.utc)
|
|
302
|
-
agent_state = dict(previous)
|
|
303
|
-
agent_state.update(
|
|
304
|
-
{
|
|
305
|
-
"status": "running",
|
|
306
|
-
"provider": agent["provider"],
|
|
307
|
-
"agent_id": agent["id"],
|
|
308
|
-
"model": agent.get("model"),
|
|
309
|
-
"auth_mode": agent.get("auth_mode"),
|
|
310
|
-
"profile": agent.get("profile"),
|
|
311
|
-
"window": agent["id"],
|
|
312
|
-
"mcp_config": str(mcp_path),
|
|
313
|
-
"permissions": resolve_permissions(agent),
|
|
314
|
-
"spawn_cwd": str(workspace),
|
|
315
|
-
"spawned_at": spawn_time.isoformat(),
|
|
316
|
-
}
|
|
317
|
-
)
|
|
318
|
-
profile_launch = command_agent.get("_provider_profile") or {}
|
|
319
|
-
if profile_launch.get("claude_projects_root"):
|
|
320
|
-
agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
|
|
321
|
-
if restart_mode == "fresh":
|
|
322
|
-
_clear_session_capture_fields(agent_state)
|
|
323
|
-
if command_agent.get("_session_id"):
|
|
324
|
-
agent_state["_pending_session_id"] = command_agent["_session_id"]
|
|
325
|
-
_capture_agent_session(
|
|
326
|
-
workspace,
|
|
327
|
-
agent["id"],
|
|
328
|
-
agent_state,
|
|
329
|
-
event_log,
|
|
330
|
-
timeout_s=1.5,
|
|
331
|
-
exclude_session_ids=known_session_ids,
|
|
332
|
-
raise_on_missed=False,
|
|
333
|
-
)
|
|
334
|
-
if display_backend_has_worker_views(display_backend):
|
|
335
|
-
display_jobs.append((agent["id"], agent))
|
|
336
|
-
new_agents[agent["id"]] = agent_state
|
|
337
|
-
restarted.append(
|
|
338
|
-
{
|
|
339
|
-
"agent_id": agent["id"],
|
|
340
|
-
"restart_mode": restart_mode,
|
|
341
|
-
"session_id": agent_state.get("session_id"),
|
|
342
|
-
"display_target": None,
|
|
343
|
-
}
|
|
344
|
-
)
|
|
345
|
-
display_results = _open_worker_displays(workspace, session_name, display_jobs, event_log, display_backend) if display_backend_opens_before_leader_rebind(display_backend) else {}
|
|
346
|
-
for agent_id, display in display_results.items():
|
|
347
|
-
if agent_id in new_agents:
|
|
348
|
-
new_agents[agent_id]["display"] = display
|
|
349
|
-
for item in restarted:
|
|
350
|
-
agent_id = item["agent_id"]
|
|
351
|
-
if agent_id in display_results:
|
|
352
|
-
item["display_target"] = display_results[agent_id]
|
|
353
|
-
missing_after_start = [item["agent_id"] for item in restarted if not _tmux_window_exists(session_name, item["agent_id"])]
|
|
354
|
-
if missing_after_start:
|
|
355
|
-
for agent_id in missing_after_start:
|
|
356
|
-
event_log.write("restart.agent_missing_after_start", agent_id=agent_id, target=f"{session_name}:{agent_id}")
|
|
357
|
-
rollback = rollback_restart_session(session_name, event_log)
|
|
358
|
-
raise RuntimeError(
|
|
359
|
-
f"Failed to restart agent {missing_after_start[0]}: tmux window exited after start; "
|
|
360
|
-
f"rollback_session_ok={rollback.get('ok')}"
|
|
361
|
-
)
|
|
362
|
-
state["session_name"] = session_name
|
|
363
|
-
state["agents"] = new_agents
|
|
364
|
-
populate_team_owner_from_env(state, source="restart")
|
|
365
|
-
_save_restart_selected_team_state(workspace, state)
|
|
366
|
-
save_team_runtime_snapshot(workspace, state)
|
|
367
|
-
MessageStore(workspace)
|
|
368
|
-
write_team_state(workspace, spec, state)
|
|
369
|
-
from team_agent.leader import autobind_leader_receiver_from_env
|
|
370
|
-
leader_provider = str(spec.get("leader", {}).get("provider") or "codex")
|
|
371
|
-
rebound_receiver = autobind_leader_receiver_from_env(workspace, leader_provider, source="restart")
|
|
372
|
-
if rebound_receiver is None and state.get("leader_receiver"):
|
|
373
|
-
stale = state.pop("leader_receiver", None)
|
|
374
|
-
event_log.write(
|
|
375
|
-
"leader_receiver.rebind_required",
|
|
376
|
-
reason="restart_autobind_unresolved",
|
|
377
|
-
old_pane_id=(stale or {}).get("pane_id") if isinstance(stale, dict) else None,
|
|
378
|
-
old_session_name=(stale or {}).get("session_name") if isinstance(stale, dict) else None,
|
|
379
|
-
source="restart",
|
|
380
|
-
)
|
|
381
|
-
_save_restart_selected_team_state(workspace, state)
|
|
382
|
-
save_team_runtime_snapshot(workspace, state)
|
|
383
|
-
write_team_state(workspace, spec, state)
|
|
384
|
-
rebuild_restart_display_after_rebind(display_backend, workspace, session_name, spec, event_log, restarted, receiver=rebound_receiver)
|
|
385
|
-
coordinator = start_coordinator(workspace)
|
|
386
|
-
event_log.write("restart.complete", session=session_name, agents=restarted, coordinator=coordinator)
|
|
387
|
-
return {"ok": True, "session_name": session_name, "agents": restarted, "coordinator": coordinator}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
def _save_restart_selected_team_state(workspace: Path, state: dict[str, Any]) -> None:
|
|
391
|
-
team_key = str(state.get("active_team_key") or team_state_key(state))
|
|
392
|
-
teams = copy.deepcopy(state.get("teams") if isinstance(state.get("teams"), dict) else {})
|
|
393
|
-
state["active_team_key"] = team_key
|
|
394
|
-
state["teams"] = teams
|
|
395
|
-
teams[team_key] = compact_team_state(state)
|
|
396
|
-
save_runtime_state(workspace, state)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
_FIRST_SEND_AT_ABSENT = "absent"
|
|
400
|
-
_FIRST_SEND_AT_VALID = "valid"
|
|
401
|
-
_FIRST_SEND_AT_CORRUPT = "corrupt"
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def _classify_first_send_at(value: Any) -> str:
|
|
405
|
-
"""Strict first_send_at typing (cr verdict, 2026-05-27).
|
|
406
|
-
|
|
407
|
-
Returns one of:
|
|
408
|
-
"absent" — None or missing field (worker never-interacted).
|
|
409
|
-
"valid" — non-empty ISO-8601 UTC string parseable by datetime.fromisoformat.
|
|
410
|
-
"corrupt" — anything else: empty string, 0, False, literal "null", garbage.
|
|
411
|
-
|
|
412
|
-
The contract requires that corrupt values be detected deterministically
|
|
413
|
-
before any restart decision so we never silent-misclassify a worker's
|
|
414
|
-
interaction state via Python truthiness.
|
|
415
|
-
"""
|
|
416
|
-
if value is None:
|
|
417
|
-
return _FIRST_SEND_AT_ABSENT
|
|
418
|
-
if not isinstance(value, str):
|
|
419
|
-
return _FIRST_SEND_AT_CORRUPT
|
|
420
|
-
if not value:
|
|
421
|
-
return _FIRST_SEND_AT_CORRUPT
|
|
422
|
-
try:
|
|
423
|
-
datetime.fromisoformat(value)
|
|
424
|
-
except (ValueError, TypeError):
|
|
425
|
-
return _FIRST_SEND_AT_CORRUPT
|
|
426
|
-
return _FIRST_SEND_AT_VALID
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def _collect_corrupt_first_send_at(
|
|
430
|
-
restart_agents: list[dict[str, Any]],
|
|
431
|
-
state: dict[str, Any],
|
|
432
|
-
) -> list[dict[str, Any]]:
|
|
433
|
-
"""Walk every non-paused worker and flag any whose persisted first_send_at
|
|
434
|
-
is corrupt. Returns the list of invalid records ready for the
|
|
435
|
-
`restart.first_send_at_invalid` event and the refusal envelope."""
|
|
436
|
-
invalid: list[dict[str, Any]] = []
|
|
437
|
-
for agent in restart_agents:
|
|
438
|
-
agent_id = agent["id"]
|
|
439
|
-
previous = state.get("agents", {}).get(agent_id, {})
|
|
440
|
-
raw = previous.get("first_send_at") if isinstance(previous, dict) else None
|
|
441
|
-
if _classify_first_send_at(raw) != _FIRST_SEND_AT_CORRUPT:
|
|
442
|
-
continue
|
|
443
|
-
invalid.append({
|
|
444
|
-
"worker_id": agent_id,
|
|
445
|
-
"raw_first_send_at": raw,
|
|
446
|
-
"raw_first_send_at_type": type(raw).__name__,
|
|
447
|
-
})
|
|
448
|
-
return invalid
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
def _emit_resume_decisions(
|
|
452
|
-
workspace: Path,
|
|
453
|
-
restart_agents: list[dict[str, Any]],
|
|
454
|
-
state: dict[str, Any],
|
|
455
|
-
get_adapter_fn: Any,
|
|
456
|
-
event_log: EventLog,
|
|
457
|
-
allow_fresh: bool,
|
|
458
|
-
) -> list[dict[str, Any]]:
|
|
459
|
-
"""Route B audit-events contract (cr C2, 2026-05-27). For every non-paused
|
|
460
|
-
worker considered by restart, derive the resume decision per the Route B
|
|
461
|
-
matrix and emit ONE `restart.resume_decision` event:
|
|
462
|
-
|
|
463
|
-
resumable AND ... -> decision = "resume"
|
|
464
|
-
not resumable AND not interacted -> decision = "fresh_start"
|
|
465
|
-
not resumable AND interacted AND fresh -> decision = "fresh_start"
|
|
466
|
-
not resumable AND interacted AND not fresh -> decision = "refuse"
|
|
467
|
-
|
|
468
|
-
Resumability mirrors sessions.resume.prepare_resume_state's repair chain
|
|
469
|
-
so workers the runtime would legitimately repair are NOT flagged. Returns
|
|
470
|
-
the subset of refused workers — populated only when allow_fresh=False AND
|
|
471
|
-
some interacted worker cannot be repaired — for use by atomic_refusal.
|
|
472
|
-
"""
|
|
473
|
-
from team_agent.sessions.resume import recover_resume_session_from_events
|
|
474
|
-
refused: list[dict[str, Any]] = []
|
|
475
|
-
for agent in restart_agents:
|
|
476
|
-
agent_id = agent["id"]
|
|
477
|
-
previous = state.get("agents", {}).get(agent_id, {})
|
|
478
|
-
session_id = previous.get("session_id")
|
|
479
|
-
first_send_at = previous.get("first_send_at")
|
|
480
|
-
has_first_send_at = _classify_first_send_at(first_send_at) == _FIRST_SEND_AT_VALID
|
|
481
|
-
has_session_id = bool(session_id)
|
|
482
|
-
adapter = get_adapter_fn(agent["provider"])
|
|
483
|
-
resumable = bool(session_id) and adapter.session_is_resumable(previous, workspace)
|
|
484
|
-
if not resumable:
|
|
485
|
-
known_session_ids = {
|
|
486
|
-
str(item.get("session_id"))
|
|
487
|
-
for aid, item in state.get("agents", {}).items()
|
|
488
|
-
if aid != agent_id and item.get("session_id")
|
|
489
|
-
}
|
|
490
|
-
repaired = recover_resume_session_from_events(
|
|
491
|
-
workspace, agent_id, previous, adapter, known_session_ids,
|
|
492
|
-
)
|
|
493
|
-
if not repaired:
|
|
494
|
-
repaired = adapter.recover_session_id(
|
|
495
|
-
agent_id, previous, workspace, known_session_ids,
|
|
496
|
-
)
|
|
497
|
-
resumable = bool(repaired)
|
|
498
|
-
if resumable:
|
|
499
|
-
decision = "resume"
|
|
500
|
-
elif not has_first_send_at:
|
|
501
|
-
decision = "fresh_start"
|
|
502
|
-
elif allow_fresh:
|
|
503
|
-
decision = "fresh_start"
|
|
504
|
-
else:
|
|
505
|
-
decision = "refuse"
|
|
506
|
-
event_log.write(
|
|
507
|
-
"restart.resume_decision",
|
|
508
|
-
worker_id=agent_id,
|
|
509
|
-
has_first_send_at=has_first_send_at,
|
|
510
|
-
has_session_id=has_session_id,
|
|
511
|
-
allow_fresh=bool(allow_fresh),
|
|
512
|
-
decision=decision,
|
|
513
|
-
first_send_at=first_send_at if has_first_send_at else None,
|
|
514
|
-
session_id=session_id,
|
|
515
|
-
)
|
|
516
|
-
if decision == "refuse":
|
|
517
|
-
refused.append({
|
|
518
|
-
"agent_id": agent_id,
|
|
519
|
-
"reason": "no_persisted_session_id" if not session_id else "session_unresumable",
|
|
520
|
-
"session_id": session_id,
|
|
521
|
-
"first_send_at": first_send_at,
|
|
522
|
-
})
|
|
523
|
-
return refused
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
def _format_atomic_refusal_error(refused: list[dict[str, Any]]) -> str:
|
|
527
|
-
"""C4 (cr verdict, 2026-05-27): the human-readable refusal error must
|
|
528
|
-
name every refused worker AND its first_send_at timestamp so an operator
|
|
529
|
-
can decide whether to pass --allow-fresh and accept losing that
|
|
530
|
-
interaction history."""
|
|
531
|
-
names = [item["agent_id"] for item in refused]
|
|
532
|
-
details = ". ".join(
|
|
533
|
-
f"{item['agent_id']} was first interacted with at {item.get('first_send_at')}; "
|
|
534
|
-
"its persisted session is missing"
|
|
535
|
-
for item in refused
|
|
536
|
-
)
|
|
537
|
-
return (
|
|
538
|
-
f"Cannot restart: workers {names} have no resumable session despite "
|
|
539
|
-
f"previous interaction. {details}. "
|
|
540
|
-
"Pass --allow-fresh if you accept losing that interaction history."
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
def rollback_restart_session(session_name: str, event_log: EventLog) -> dict[str, Any]:
|
|
545
|
-
from team_agent.runtime import run_cmd
|
|
546
|
-
proc = run_cmd(["tmux", "kill-session", "-t", session_name], timeout=10)
|
|
547
|
-
result = {
|
|
548
|
-
"ok": proc.returncode == 0,
|
|
549
|
-
"session": session_name,
|
|
550
|
-
"stdout": proc.stdout.strip(),
|
|
551
|
-
"stderr": proc.stderr.strip(),
|
|
552
|
-
}
|
|
553
|
-
event_log.write("restart.rollback_session", **result)
|
|
554
|
-
return result
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import copy
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from team_agent.paths import runtime_dir
|
|
8
|
-
from team_agent.state import load_runtime_state, runtime_state_path
|
|
9
|
-
from team_agent.restart.snapshot import load_snapshot_state, state_team_name
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def restart_candidates(workspace: Path) -> list[dict[str, Any]]:
|
|
13
|
-
by_session: dict[str, dict[str, Any]] = {}
|
|
14
|
-
snapshots_root = runtime_dir(workspace) / "teams"
|
|
15
|
-
for path in sorted(snapshots_root.glob("*/state.json")) if snapshots_root.exists() else []:
|
|
16
|
-
state = load_snapshot_state(path)
|
|
17
|
-
if not state or not state.get("session_name"):
|
|
18
|
-
continue
|
|
19
|
-
session_name = str(state["session_name"])
|
|
20
|
-
by_session[session_name] = restart_candidate_from_state(state, path)
|
|
21
|
-
active = load_runtime_state(workspace)
|
|
22
|
-
if active.get("session_name"):
|
|
23
|
-
by_session[str(active["session_name"])] = restart_candidate_from_state(active, runtime_state_path(workspace))
|
|
24
|
-
return sorted(by_session.values(), key=lambda item: item.get("session_name") or "")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def restart_candidate_from_state(state: dict[str, Any], state_path: Path) -> dict[str, Any]:
|
|
28
|
-
session_name = str(state.get("session_name") or "")
|
|
29
|
-
return {
|
|
30
|
-
"session_name": session_name,
|
|
31
|
-
"team_name": state_team_name(state),
|
|
32
|
-
"state_path": str(state_path),
|
|
33
|
-
"spec_path": state.get("spec_path"),
|
|
34
|
-
"agents": sorted(state.get("agents", {}).keys()),
|
|
35
|
-
"has_context": state_has_restart_context(state),
|
|
36
|
-
"state": state,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def state_has_restart_context(state: dict[str, Any]) -> bool:
|
|
41
|
-
for agent_state in state.get("agents", {}).values():
|
|
42
|
-
if not isinstance(agent_state, dict):
|
|
43
|
-
continue
|
|
44
|
-
if agent_state.get("session_id") or agent_state.get("rollout_path") or agent_state.get("captured_at"):
|
|
45
|
-
return True
|
|
46
|
-
return bool(state.get("agents"))
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def select_restart_state(workspace: Path, team: str | None = None) -> dict[str, Any]:
|
|
50
|
-
from team_agent.runtime import RuntimeError
|
|
51
|
-
candidates = [item for item in restart_candidates(workspace) if item.get("has_context")]
|
|
52
|
-
if team:
|
|
53
|
-
matches = [
|
|
54
|
-
item
|
|
55
|
-
for item in candidates
|
|
56
|
-
if team in {item.get("session_name"), item.get("team_name"), Path(str(item.get("state_path"))).parent.name}
|
|
57
|
-
]
|
|
58
|
-
if len(matches) == 1:
|
|
59
|
-
return copy.deepcopy(matches[0]["state"])
|
|
60
|
-
if len(matches) > 1:
|
|
61
|
-
raise RuntimeError("restart team selector is ambiguous. " + format_restart_candidates(matches))
|
|
62
|
-
raise RuntimeError(f"restart team {team!r} not found. " + format_restart_candidates(candidates))
|
|
63
|
-
if len(candidates) == 1:
|
|
64
|
-
return copy.deepcopy(candidates[0]["state"])
|
|
65
|
-
if len(candidates) > 1:
|
|
66
|
-
raise RuntimeError(
|
|
67
|
-
"multiple restartable teams found in this workspace; pass --team <session_name> to choose. "
|
|
68
|
-
+ format_restart_candidates(candidates)
|
|
69
|
-
)
|
|
70
|
-
return load_runtime_state(workspace)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def format_restart_candidates(candidates: list[dict[str, Any]]) -> str:
|
|
74
|
-
if not candidates:
|
|
75
|
-
return "No restartable team state was found."
|
|
76
|
-
parts = []
|
|
77
|
-
for item in candidates:
|
|
78
|
-
parts.append(
|
|
79
|
-
f"{item.get('session_name')} team={item.get('team_name') or '-'} "
|
|
80
|
-
f"agents={','.join(item.get('agents') or []) or '-'}"
|
|
81
|
-
)
|
|
82
|
-
return "Candidates: " + "; ".join(parts)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def quick_start_existing_context(workspace: Path, session_name: str) -> dict[str, Any] | None:
|
|
86
|
-
for item in restart_candidates(workspace):
|
|
87
|
-
if item.get("session_name") == session_name and item.get("has_context"):
|
|
88
|
-
return item
|
|
89
|
-
return None
|