@team-agent/installer 0.2.11 → 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 -119
- package/src/team_agent/coordinator/lifecycle.py +0 -411
- 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 -218
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -114
- 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 -262
- package/src/team_agent/messaging/delivery.py +0 -504
- 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 -503
- 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 -91
- 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 -1243
- 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 -144
- 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 -693
- 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
package/src/team_agent/state.py
DELETED
|
@@ -1,693 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
import errno
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import copy
|
|
8
|
-
import subprocess
|
|
9
|
-
import time
|
|
10
|
-
import uuid
|
|
11
|
-
from datetime import datetime, timezone
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
from team_agent.paths import runtime_dir
|
|
16
|
-
from team_agent.simple_yaml import dumps
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
SESSION_CAPTURE_FIELDS = [
|
|
20
|
-
"session_id",
|
|
21
|
-
"rollout_path",
|
|
22
|
-
"captured_at",
|
|
23
|
-
"captured_via",
|
|
24
|
-
"attribution_confidence",
|
|
25
|
-
]
|
|
26
|
-
SESSION_STATE_FIELDS = [
|
|
27
|
-
*SESSION_CAPTURE_FIELDS,
|
|
28
|
-
"spawn_cwd",
|
|
29
|
-
]
|
|
30
|
-
_UUID_SEPARATOR = "\0"
|
|
31
|
-
_RUNTIME_STATE_CACHE: dict[str, dict[str, Any]] = {}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def derive_leader_session_uuid(machine_fingerprint: str, workspace_abspath: str, os_user: str, team_id: str) -> str:
|
|
35
|
-
parts = [machine_fingerprint, workspace_abspath, os_user, team_id]
|
|
36
|
-
if any(_UUID_SEPARATOR in part for part in parts):
|
|
37
|
-
raise ValueError("leader_session_uuid inputs must not contain NUL")
|
|
38
|
-
return hashlib.sha256(_UUID_SEPARATOR.join(parts).encode("utf-8")).hexdigest()[:32]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def runtime_state_path(workspace: Path) -> Path:
|
|
42
|
-
return runtime_dir(workspace) / "state.json"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def normalize_agent_session_state(state: dict[str, Any]) -> None:
|
|
46
|
-
agents = state.get("agents", {})
|
|
47
|
-
if not isinstance(agents, dict):
|
|
48
|
-
return
|
|
49
|
-
for agent_state in agents.values():
|
|
50
|
-
if isinstance(agent_state, dict):
|
|
51
|
-
for field in SESSION_STATE_FIELDS:
|
|
52
|
-
agent_state.setdefault(field, None)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def load_runtime_state(workspace: Path) -> dict[str, Any]:
|
|
56
|
-
path = runtime_state_path(workspace)
|
|
57
|
-
if not path.exists():
|
|
58
|
-
cached = _RUNTIME_STATE_CACHE.get(str(path))
|
|
59
|
-
if cached is not None:
|
|
60
|
-
return copy.deepcopy(cached)
|
|
61
|
-
return {"agents": {}, "tasks": [], "session_name": None, "active_team_key": None}
|
|
62
|
-
state = json.loads(path.read_text(encoding="utf-8"))
|
|
63
|
-
normalize_agent_session_state(state)
|
|
64
|
-
changed = _migrate_state_identity(state, workspace)
|
|
65
|
-
if _migrate_active_team_key(state):
|
|
66
|
-
changed = True
|
|
67
|
-
if changed:
|
|
68
|
-
save_runtime_state(workspace, state)
|
|
69
|
-
_RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
|
|
70
|
-
return state
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _migrate_active_team_key(state: dict[str, Any]) -> bool:
|
|
74
|
-
"""0.2.6 Family B (C6): legacy states with a top-level ``session_name``
|
|
75
|
-
but no ``active_team_key`` get the active pointer seeded once. After
|
|
76
|
-
this, ``active_team_key`` is the single explicit source of truth and
|
|
77
|
-
callers mutate it through CLI verbs (claim-leader / takeover /
|
|
78
|
-
shutdown / restart)."""
|
|
79
|
-
if "active_team_key" in state:
|
|
80
|
-
return False
|
|
81
|
-
teams = state.get("teams") if isinstance(state.get("teams"), dict) else {}
|
|
82
|
-
if state.get("session_name"):
|
|
83
|
-
seed = team_state_key(state)
|
|
84
|
-
state["active_team_key"] = seed if seed in teams or not teams else seed
|
|
85
|
-
return True
|
|
86
|
-
if isinstance(teams, dict) and len(teams) == 1:
|
|
87
|
-
state["active_team_key"] = next(iter(teams))
|
|
88
|
-
return True
|
|
89
|
-
state["active_team_key"] = None
|
|
90
|
-
return True
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def team_state_key(state: dict[str, Any]) -> str:
|
|
94
|
-
for field in ("team_dir", "spec_path"):
|
|
95
|
-
value = state.get(field)
|
|
96
|
-
if not value:
|
|
97
|
-
continue
|
|
98
|
-
path = Path(str(value))
|
|
99
|
-
key = path.name if field == "team_dir" else path.parent.name
|
|
100
|
-
if key and key not in {".team", "runtime"}:
|
|
101
|
-
return key
|
|
102
|
-
return str(state.get("session_name") or "current")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def compact_team_state(state: dict[str, Any]) -> dict[str, Any]:
|
|
106
|
-
compact = copy.deepcopy(state)
|
|
107
|
-
compact.pop("teams", None)
|
|
108
|
-
return compact
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def merge_workspace_team_state(existing: dict[str, Any], launched: dict[str, Any]) -> dict[str, Any]:
|
|
112
|
-
launched_key = team_state_key(launched)
|
|
113
|
-
if not existing.get("session_name"):
|
|
114
|
-
merged = copy.deepcopy(launched)
|
|
115
|
-
merged.setdefault("teams", {})[launched_key] = compact_team_state(launched)
|
|
116
|
-
return merged
|
|
117
|
-
existing_key = team_state_key(existing)
|
|
118
|
-
if existing_key == launched_key:
|
|
119
|
-
merged = copy.deepcopy(launched)
|
|
120
|
-
teams = copy.deepcopy(existing.get("teams") or {})
|
|
121
|
-
teams[launched_key] = compact_team_state(launched)
|
|
122
|
-
merged["teams"] = teams
|
|
123
|
-
return merged
|
|
124
|
-
merged = copy.deepcopy(existing)
|
|
125
|
-
teams = merged.setdefault("teams", {})
|
|
126
|
-
teams.setdefault(existing_key, compact_team_state(existing))
|
|
127
|
-
teams[launched_key] = compact_team_state(launched)
|
|
128
|
-
return merged
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def team_state_candidates(state: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
132
|
-
"""0.2.6 Family B (C7): the only candidate source is ``state.teams``
|
|
133
|
-
filtered by ``status == "alive"``. Top-level ``session_name`` /
|
|
134
|
-
``team_dir`` are a derived view of the active team and never count as
|
|
135
|
-
an independent candidate. Shutdown/legacy entries with non-alive
|
|
136
|
-
status are excluded."""
|
|
137
|
-
out: dict[str, dict[str, Any]] = {}
|
|
138
|
-
teams = state.get("teams") if isinstance(state.get("teams"), dict) else {}
|
|
139
|
-
for key, value in teams.items():
|
|
140
|
-
if not isinstance(value, dict):
|
|
141
|
-
continue
|
|
142
|
-
if str(value.get("status") or "alive").lower() != "alive":
|
|
143
|
-
continue
|
|
144
|
-
out[str(key)] = value
|
|
145
|
-
return out
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def format_team_candidates(team_states: dict[str, dict[str, Any]]) -> str:
|
|
149
|
-
if not team_states:
|
|
150
|
-
return "No team state was found."
|
|
151
|
-
parts = []
|
|
152
|
-
for key in sorted(team_states):
|
|
153
|
-
st = team_states[key]
|
|
154
|
-
agents = ",".join(sorted(st.get("agents", {}).keys())) or "-"
|
|
155
|
-
parts.append(f"{key} session={st.get('session_name') or '-'} agents={agents}")
|
|
156
|
-
return "Candidates: " + "; ".join(parts)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _team_entry_from_state(state: dict[str, Any], team_key: str) -> dict[str, Any] | None:
|
|
160
|
-
teams = state.get("teams") if isinstance(state.get("teams"), dict) else {}
|
|
161
|
-
entry = teams.get(team_key)
|
|
162
|
-
if not isinstance(entry, dict):
|
|
163
|
-
return None
|
|
164
|
-
return entry
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def _project_top_level_view(state: dict[str, Any], team_key: str) -> dict[str, Any]:
|
|
168
|
-
"""0.2.6 Family B (C8): when picking a team for use, the top-level
|
|
169
|
-
keys (``session_name`` / ``team_dir`` / ``agents`` / ``tasks``) are a
|
|
170
|
-
derived view of ``teams[team_key]``. We copy the team entry into a
|
|
171
|
-
flat dict and preserve any auxiliary state (``team_owner`` /
|
|
172
|
-
``leader_receiver`` / ``coordinator`` already pinned to the team)."""
|
|
173
|
-
entry = _team_entry_from_state(state, team_key) or {}
|
|
174
|
-
projection = copy.deepcopy(entry)
|
|
175
|
-
projection.setdefault("session_name", entry.get("session_name"))
|
|
176
|
-
projection.setdefault("team_dir", entry.get("team_dir"))
|
|
177
|
-
projection["active_team_key"] = team_key
|
|
178
|
-
# Preserve the full teams dict so consumers can introspect siblings.
|
|
179
|
-
projection["teams"] = copy.deepcopy(state.get("teams") or {})
|
|
180
|
-
if "team_owner" in entry:
|
|
181
|
-
projection["team_owner"] = copy.deepcopy(entry["team_owner"])
|
|
182
|
-
elif state.get("team_owner") is not None:
|
|
183
|
-
projection["team_owner"] = copy.deepcopy(state["team_owner"])
|
|
184
|
-
if "leader_receiver" in entry:
|
|
185
|
-
projection["leader_receiver"] = copy.deepcopy(entry["leader_receiver"])
|
|
186
|
-
elif state.get("leader_receiver") is not None:
|
|
187
|
-
projection["leader_receiver"] = copy.deepcopy(state["leader_receiver"])
|
|
188
|
-
if "coordinator" in state:
|
|
189
|
-
projection.setdefault("coordinator", copy.deepcopy(state["coordinator"]))
|
|
190
|
-
return projection
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def select_runtime_state(workspace: Path, team: str | None = None) -> dict[str, Any]:
|
|
194
|
-
state = load_runtime_state(workspace)
|
|
195
|
-
alive = team_state_candidates(state)
|
|
196
|
-
if team:
|
|
197
|
-
if not alive and team in {str(state.get("active_team_key") or ""), team_state_key(state)}:
|
|
198
|
-
projection = copy.deepcopy(state)
|
|
199
|
-
projection["active_team_key"] = str(team)
|
|
200
|
-
return projection
|
|
201
|
-
matches = [
|
|
202
|
-
(key, value)
|
|
203
|
-
for key, value in alive.items()
|
|
204
|
-
if team in {key, str(value.get("session_name") or ""), str(value.get("team_dir") or "")}
|
|
205
|
-
]
|
|
206
|
-
if len(matches) == 1:
|
|
207
|
-
return _project_top_level_view(state, matches[0][0])
|
|
208
|
-
from team_agent.errors import RuntimeError
|
|
209
|
-
if len(matches) > 1:
|
|
210
|
-
raise RuntimeError("team selector is ambiguous. " + format_team_candidates(alive))
|
|
211
|
-
raise RuntimeError(f"team {team!r} not found. " + format_team_candidates(alive))
|
|
212
|
-
active = state.get("active_team_key")
|
|
213
|
-
if active and active in alive:
|
|
214
|
-
return _project_top_level_view(state, str(active))
|
|
215
|
-
if len(alive) == 1:
|
|
216
|
-
return _project_top_level_view(state, next(iter(alive)))
|
|
217
|
-
if not alive:
|
|
218
|
-
return copy.deepcopy(state)
|
|
219
|
-
from team_agent.errors import RuntimeError
|
|
220
|
-
raise RuntimeError(
|
|
221
|
-
"multiple teams found in this workspace; pass --team <team> to choose. "
|
|
222
|
-
+ format_team_candidates(alive)
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def ambiguous_team_target_result(state: dict[str, Any]) -> dict[str, Any] | None:
|
|
227
|
-
alive = team_state_candidates(state)
|
|
228
|
-
active = state.get("active_team_key")
|
|
229
|
-
if active and active in alive:
|
|
230
|
-
return None
|
|
231
|
-
if len(alive) <= 1:
|
|
232
|
-
return None
|
|
233
|
-
return {
|
|
234
|
-
"ok": False,
|
|
235
|
-
"status": "refused",
|
|
236
|
-
"reason": "team_target_ambiguous",
|
|
237
|
-
"candidates": sorted(alive.keys()),
|
|
238
|
-
"message": "multiple teams found in this workspace; pass --team <team> to choose. "
|
|
239
|
-
+ format_team_candidates(alive),
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def resolve_team_scoped_state(
|
|
244
|
-
workspace: Path,
|
|
245
|
-
team: str | None,
|
|
246
|
-
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
247
|
-
if team is None:
|
|
248
|
-
ambiguous = ambiguous_team_target_result(load_runtime_state(workspace))
|
|
249
|
-
if ambiguous:
|
|
250
|
-
return None, ambiguous
|
|
251
|
-
try:
|
|
252
|
-
from team_agent.errors import RuntimeError as _TeamAgentRuntimeError
|
|
253
|
-
return select_runtime_state(workspace, team), None
|
|
254
|
-
except _TeamAgentRuntimeError as exc:
|
|
255
|
-
return None, {
|
|
256
|
-
"ok": False,
|
|
257
|
-
"status": "refused",
|
|
258
|
-
"reason": "team_target_unresolved",
|
|
259
|
-
"team": team,
|
|
260
|
-
"error": str(exc),
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def _identity_workspace_abspath(state: dict[str, Any], workspace: Path | None = None) -> str:
|
|
265
|
-
if state.get("workspace"):
|
|
266
|
-
return str(Path(str(state["workspace"])).resolve())
|
|
267
|
-
if state.get("team_dir"):
|
|
268
|
-
return str(Path(str(state["team_dir"])).resolve().parent.parent)
|
|
269
|
-
if state.get("spec_path"):
|
|
270
|
-
spec_path = Path(str(state["spec_path"])).resolve()
|
|
271
|
-
return str(spec_path.parent.parent.parent if spec_path.parent.parent.name == ".team" else spec_path.parent)
|
|
272
|
-
return str((workspace or Path(os.environ.get("TEAM_AGENT_WORKSPACE") or os.getcwd())).resolve())
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def _identity_os_user() -> str:
|
|
276
|
-
return os.environ.get("USER") or os.environ.get("USERNAME") or ""
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def _identity_machine_fingerprint(state: dict[str, Any]) -> str:
|
|
280
|
-
for record in (state.get("team_owner"), state.get("leader_receiver")):
|
|
281
|
-
if isinstance(record, dict) and record.get("machine_fingerprint"):
|
|
282
|
-
return str(record["machine_fingerprint"])
|
|
283
|
-
return os.environ.get("TEAM_AGENT_MACHINE_FINGERPRINT") or ""
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _leader_session_uuid_for_state(state: dict[str, Any], workspace: Path | None = None, team_id: str | None = None) -> str:
|
|
287
|
-
return derive_leader_session_uuid(
|
|
288
|
-
_identity_machine_fingerprint(state),
|
|
289
|
-
_identity_workspace_abspath(state, workspace),
|
|
290
|
-
_identity_os_user(),
|
|
291
|
-
team_id or team_state_key(state),
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def _migrate_team_identity(state: dict[str, Any], workspace: Path, team_id: str | None = None) -> bool:
|
|
296
|
-
leader_uuid = _leader_session_uuid_for_state(state, workspace, team_id)
|
|
297
|
-
changed = False
|
|
298
|
-
for key in ("team_owner", "leader_receiver"):
|
|
299
|
-
record = state.get(key)
|
|
300
|
-
if isinstance(record, dict) and not record.get("leader_session_uuid"):
|
|
301
|
-
record["leader_session_uuid"] = leader_uuid
|
|
302
|
-
changed = True
|
|
303
|
-
return changed
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def _migrate_state_identity(state: dict[str, Any], workspace: Path) -> bool:
|
|
307
|
-
changed = _migrate_team_identity(state, workspace) if state.get("session_name") else False
|
|
308
|
-
teams = state.get("teams")
|
|
309
|
-
if isinstance(teams, dict):
|
|
310
|
-
for team_id, team_state in teams.items():
|
|
311
|
-
if isinstance(team_state, dict):
|
|
312
|
-
changed = _migrate_team_identity(team_state, workspace, str(team_id)) or changed
|
|
313
|
-
return changed
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def _caller_identity_from_env(state: dict[str, Any] | None = None, team_id: str | None = None, workspace: Path | None = None) -> dict[str, str]:
|
|
317
|
-
state = state or {}
|
|
318
|
-
machine_fingerprint = os.environ.get("TEAM_AGENT_MACHINE_FINGERPRINT") or ""
|
|
319
|
-
override = os.environ.get("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE") or ""
|
|
320
|
-
env_uuid = os.environ.get("TEAM_AGENT_LEADER_SESSION_UUID") or ""
|
|
321
|
-
leader_uuid = override or env_uuid or derive_leader_session_uuid(
|
|
322
|
-
machine_fingerprint,
|
|
323
|
-
_identity_workspace_abspath(state, workspace),
|
|
324
|
-
_identity_os_user(),
|
|
325
|
-
team_id or os.environ.get("TEAM_AGENT_TEAM_ID") or team_state_key(state),
|
|
326
|
-
)
|
|
327
|
-
return {
|
|
328
|
-
"pane_id": os.environ.get("TEAM_AGENT_LEADER_PANE_ID") or os.environ.get("TMUX_PANE") or "",
|
|
329
|
-
"provider": os.environ.get("TEAM_AGENT_LEADER_PROVIDER") or "",
|
|
330
|
-
"machine_fingerprint": machine_fingerprint,
|
|
331
|
-
"leader_session_uuid": leader_uuid,
|
|
332
|
-
"leader_session_uuid_source": "explicit-override" if override else ("env" if env_uuid else "derived"),
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
_TMUX_PANE_LIVE = "live"
|
|
337
|
-
_TMUX_PANE_DEAD = "dead"
|
|
338
|
-
_TMUX_PANE_UNKNOWN = "unknown"
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def _tmux_pane_liveness(pane_id: str) -> str:
|
|
342
|
-
if not pane_id:
|
|
343
|
-
return _TMUX_PANE_UNKNOWN
|
|
344
|
-
try:
|
|
345
|
-
from team_agent.runtime import run_cmd
|
|
346
|
-
proc = run_cmd(["tmux", "display-message", "-p", "-t", pane_id, "#{pane_id}"], timeout=3)
|
|
347
|
-
except Exception:
|
|
348
|
-
try:
|
|
349
|
-
proc = subprocess.run(
|
|
350
|
-
["tmux", "display-message", "-p", "-t", pane_id, "#{pane_id}"],
|
|
351
|
-
text=True,
|
|
352
|
-
capture_output=True,
|
|
353
|
-
timeout=3,
|
|
354
|
-
check=False,
|
|
355
|
-
)
|
|
356
|
-
except Exception:
|
|
357
|
-
return _TMUX_PANE_UNKNOWN
|
|
358
|
-
if proc.returncode == 0:
|
|
359
|
-
return _TMUX_PANE_LIVE
|
|
360
|
-
stderr = str(getattr(proc, "stderr", "") or "").lower()
|
|
361
|
-
if "can't find pane" in stderr or "can't find window" in stderr or "can't find session" in stderr:
|
|
362
|
-
return _TMUX_PANE_DEAD
|
|
363
|
-
return _TMUX_PANE_UNKNOWN
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
def check_team_owner(state: dict[str, Any]) -> dict[str, Any] | None:
|
|
367
|
-
owner = state.get("team_owner") or {}
|
|
368
|
-
if not owner:
|
|
369
|
-
return None
|
|
370
|
-
_migrate_team_identity(state, Path(_identity_workspace_abspath(state)), team_state_key(state))
|
|
371
|
-
caller = _caller_identity_from_env(state, team_state_key(state))
|
|
372
|
-
owner_uuid = str(owner.get("leader_session_uuid") or "")
|
|
373
|
-
caller_uuid = caller["leader_session_uuid"]
|
|
374
|
-
owner_pane = str(owner.get("pane_id") or "")
|
|
375
|
-
caller_pane = caller.get("pane_id") or ""
|
|
376
|
-
if caller_pane and caller_pane == owner_pane:
|
|
377
|
-
return None
|
|
378
|
-
if (
|
|
379
|
-
caller_pane
|
|
380
|
-
and not os.environ.get("TEAM_AGENT_ID")
|
|
381
|
-
and owner_pane
|
|
382
|
-
and _tmux_pane_liveness(owner_pane) != _TMUX_PANE_LIVE
|
|
383
|
-
):
|
|
384
|
-
return None
|
|
385
|
-
if caller_uuid == owner_uuid and (not caller_pane or caller_pane == owner_pane):
|
|
386
|
-
return None
|
|
387
|
-
same_uuid = caller_uuid == owner_uuid
|
|
388
|
-
return {
|
|
389
|
-
"ok": False,
|
|
390
|
-
"status": "refused",
|
|
391
|
-
"reason": "team_owner_mismatch",
|
|
392
|
-
"reason_kind": "sticky_bind_collision" if same_uuid else "owner_takeover_required",
|
|
393
|
-
"error": "not_owner",
|
|
394
|
-
"action": "team-agent claim-leader --confirm" if same_uuid else "team-agent takeover --confirm",
|
|
395
|
-
"team_owner": owner,
|
|
396
|
-
"caller": caller,
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
def worker_sender_bypasses_owner_gate(state: dict[str, Any], sender: str | None) -> str | None:
|
|
401
|
-
if not sender:
|
|
402
|
-
return None
|
|
403
|
-
leader_id = (state.get("leader") or {}).get("id") or "leader"
|
|
404
|
-
if sender == leader_id or sender in {"leader", "Leader"}:
|
|
405
|
-
return None
|
|
406
|
-
if sender not in (state.get("agents") or {}):
|
|
407
|
-
return None
|
|
408
|
-
env_agent_id = os.environ.get("TEAM_AGENT_ID") or ""
|
|
409
|
-
if env_agent_id and env_agent_id != sender:
|
|
410
|
-
return None
|
|
411
|
-
return env_agent_id or sender
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
def populate_team_owner_from_env(state: dict[str, Any], source: str = "autopopulate") -> dict[str, Any] | None:
|
|
415
|
-
# Lease mutation convergence marker: _write_lease_dual_state.
|
|
416
|
-
if state.get("team_owner"):
|
|
417
|
-
_migrate_team_identity(state, Path(_identity_workspace_abspath(state)), team_state_key(state))
|
|
418
|
-
return state["team_owner"]
|
|
419
|
-
caller = _caller_identity_from_env(state, team_state_key(state))
|
|
420
|
-
if not caller["pane_id"]:
|
|
421
|
-
return None
|
|
422
|
-
owner = {
|
|
423
|
-
"pane_id": caller["pane_id"],
|
|
424
|
-
"provider": caller["provider"],
|
|
425
|
-
"machine_fingerprint": caller["machine_fingerprint"],
|
|
426
|
-
"leader_session_uuid": caller["leader_session_uuid"],
|
|
427
|
-
"claimed_at": datetime.now(timezone.utc).isoformat(),
|
|
428
|
-
"claimed_via": source,
|
|
429
|
-
}
|
|
430
|
-
state["team_owner"] = owner
|
|
431
|
-
return owner
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def apply_first_time_leader_binding(
|
|
435
|
-
workspace: Path,
|
|
436
|
-
state: dict[str, Any],
|
|
437
|
-
receiver: dict[str, Any],
|
|
438
|
-
pane_info: dict[str, Any],
|
|
439
|
-
identity: dict[str, Any],
|
|
440
|
-
source: str,
|
|
441
|
-
) -> dict[str, Any]:
|
|
442
|
-
# Lease mutation convergence marker: _write_lease_dual_state.
|
|
443
|
-
from team_agent.messaging.leader_panes import _leader_command_looks_usable
|
|
444
|
-
command = pane_info.get("pane_current_command", "")
|
|
445
|
-
provider = str(receiver.get("provider") or "")
|
|
446
|
-
if not _leader_command_looks_usable(command, provider):
|
|
447
|
-
return {"ok": False, "reason": "leader_pane_wrong_command", "error": f"pane command {command!r} is not a leader host", "pane": pane_info}
|
|
448
|
-
current_path = pane_info.get("pane_current_path")
|
|
449
|
-
if not current_path or os.path.realpath(current_path) != os.path.realpath(str(workspace.resolve())):
|
|
450
|
-
return {"ok": False, "reason": "leader_pane_wrong_workspace", "error": f"pane cwd {current_path!r} does not match workspace {str(workspace.resolve())!r}", "pane": pane_info}
|
|
451
|
-
receiver.update({
|
|
452
|
-
"leader_session_uuid": identity["leader_session_uuid"],
|
|
453
|
-
"machine_fingerprint": identity["machine_fingerprint"],
|
|
454
|
-
"owner_epoch": 0,
|
|
455
|
-
})
|
|
456
|
-
state["team_owner"] = {
|
|
457
|
-
"pane_id": receiver["pane_id"],
|
|
458
|
-
"provider": provider,
|
|
459
|
-
"machine_fingerprint": identity["machine_fingerprint"],
|
|
460
|
-
"leader_session_uuid": identity["leader_session_uuid"],
|
|
461
|
-
"owner_epoch": 0,
|
|
462
|
-
"claimed_at": datetime.now(timezone.utc).isoformat(),
|
|
463
|
-
"claimed_via": source,
|
|
464
|
-
}
|
|
465
|
-
state["leader_receiver"] = receiver
|
|
466
|
-
return {"ok": True, "pane": pane_info, "warning": None, "first_time": True}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
def leader_env_exports(receiver: dict[str, Any], identity: dict[str, Any]) -> dict[str, str]:
|
|
470
|
-
return {
|
|
471
|
-
"TEAM_AGENT_LEADER_PANE_ID": str(receiver.get("pane_id") or ""),
|
|
472
|
-
"TEAM_AGENT_LEADER_PROVIDER": str(receiver.get("provider") or ""),
|
|
473
|
-
"TEAM_AGENT_LEADER_SESSION_UUID": str(identity.get("leader_session_uuid") or ""),
|
|
474
|
-
"TEAM_AGENT_MACHINE_FINGERPRINT": str(identity.get("machine_fingerprint") or ""),
|
|
475
|
-
"TEAM_AGENT_WORKSPACE": str(identity.get("workspace_abspath") or ""),
|
|
476
|
-
"TEAM_AGENT_TEAM_ID": str(identity.get("team_id") or ""),
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
def validate_leader_uuid_from_targets(receiver: dict[str, Any], targets: dict[str, Any]) -> dict[str, Any]:
|
|
481
|
-
if receiver.get("provider") == "fake":
|
|
482
|
-
return {"ok": True}
|
|
483
|
-
if not targets.get("ok"):
|
|
484
|
-
return {"ok": False, "reason": "leader_uuid_lookup_failed", "error": targets.get("error") or "tmux target scan failed"}
|
|
485
|
-
pane_id = receiver.get("pane_id")
|
|
486
|
-
target = next((item for item in targets.get("targets", []) if item.get("pane_id") == pane_id), None)
|
|
487
|
-
if not target:
|
|
488
|
-
return {"ok": False, "reason": "leader_pane_missing", "error": "tmux pane does not exist"}
|
|
489
|
-
return {"ok": True, "pane": target}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
def save_runtime_state(workspace: Path, state: dict[str, Any]) -> None:
|
|
493
|
-
path = runtime_state_path(workspace)
|
|
494
|
-
cached = _RUNTIME_STATE_CACHE.get(str(path))
|
|
495
|
-
if cached is not None and state == cached:
|
|
496
|
-
return
|
|
497
|
-
_migrate_state_identity(state, workspace)
|
|
498
|
-
cached = _RUNTIME_STATE_CACHE.get(str(path))
|
|
499
|
-
if cached is not None and state == cached:
|
|
500
|
-
return
|
|
501
|
-
if path.exists():
|
|
502
|
-
try:
|
|
503
|
-
existing = json.loads(path.read_text(encoding="utf-8"))
|
|
504
|
-
normalize_agent_session_state(existing)
|
|
505
|
-
_migrate_state_identity(existing, workspace)
|
|
506
|
-
if state == existing:
|
|
507
|
-
_RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
|
|
508
|
-
return
|
|
509
|
-
except Exception:
|
|
510
|
-
pass
|
|
511
|
-
from team_agent.runtime import _runtime_lock
|
|
512
|
-
with _runtime_lock(workspace, "state-save", timeout=2.0):
|
|
513
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
514
|
-
payload = json.dumps(state, indent=2, ensure_ascii=False)
|
|
515
|
-
delays = [0.05, 0.2, 0.5]
|
|
516
|
-
for attempt in range(len(delays) + 1):
|
|
517
|
-
tmp_path = path.with_name(f"{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
|
|
518
|
-
try:
|
|
519
|
-
tmp_path.write_text(payload, encoding="utf-8")
|
|
520
|
-
os.replace(tmp_path, path)
|
|
521
|
-
_RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
|
|
522
|
-
return
|
|
523
|
-
except (PermissionError, OSError) as exc:
|
|
524
|
-
if not _retryable_replace_error(exc) or attempt >= len(delays):
|
|
525
|
-
if _retryable_replace_error(exc):
|
|
526
|
-
_self_heal_runtime_state(workspace, path, payload, state, attempt + 1, exc)
|
|
527
|
-
return
|
|
528
|
-
raise
|
|
529
|
-
from team_agent.events import EventLog
|
|
530
|
-
EventLog(workspace).write(
|
|
531
|
-
"runtime.state.save_retry",
|
|
532
|
-
attempt=attempt + 1,
|
|
533
|
-
errno=getattr(exc, "errno", None),
|
|
534
|
-
errno_name=errno.errorcode.get(getattr(exc, "errno", 0), None),
|
|
535
|
-
error=str(exc),
|
|
536
|
-
)
|
|
537
|
-
time.sleep(delays[attempt])
|
|
538
|
-
finally:
|
|
539
|
-
tmp_path.unlink(missing_ok=True)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
def _retryable_replace_error(exc: BaseException) -> bool:
|
|
543
|
-
return isinstance(exc, PermissionError) or (
|
|
544
|
-
isinstance(exc, OSError) and getattr(exc, "errno", None) in {errno.EACCES, errno.EPERM, errno.EBUSY}
|
|
545
|
-
)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
def _self_heal_runtime_state(
|
|
549
|
-
workspace: Path,
|
|
550
|
-
path: Path,
|
|
551
|
-
payload: str,
|
|
552
|
-
state: dict[str, Any],
|
|
553
|
-
attempts_used: int,
|
|
554
|
-
original_exc: BaseException,
|
|
555
|
-
) -> None:
|
|
556
|
-
from team_agent.events import EventLog
|
|
557
|
-
event_log = EventLog(workspace)
|
|
558
|
-
heal_tmp = path.with_name(f"{path.name}.{os.getpid()}.{uuid.uuid4().hex}.heal.tmp")
|
|
559
|
-
backup = path.with_name(f"{path.name}.bak.{os.getpid()}")
|
|
560
|
-
backup_created = False
|
|
561
|
-
try:
|
|
562
|
-
heal_tmp.write_text(payload, encoding="utf-8")
|
|
563
|
-
try:
|
|
564
|
-
os.replace(path, backup)
|
|
565
|
-
backup_created = True
|
|
566
|
-
except FileNotFoundError:
|
|
567
|
-
backup_created = False
|
|
568
|
-
os.replace(heal_tmp, path)
|
|
569
|
-
_RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
|
|
570
|
-
event_log.write(
|
|
571
|
-
"runtime.state.self_healed",
|
|
572
|
-
inode_rebuilt=True,
|
|
573
|
-
attempts_used=attempts_used,
|
|
574
|
-
replace_retries=max(0, attempts_used - 1),
|
|
575
|
-
)
|
|
576
|
-
except Exception as exc:
|
|
577
|
-
if backup_created:
|
|
578
|
-
try:
|
|
579
|
-
os.replace(backup, path)
|
|
580
|
-
except Exception as restore_exc:
|
|
581
|
-
event_log.write("runtime.state.self_heal_restore_failed", error=str(restore_exc))
|
|
582
|
-
event_log.write(
|
|
583
|
-
"runtime.state.save_failed",
|
|
584
|
-
phase="save_runtime_state",
|
|
585
|
-
final_errno=getattr(exc, "errno", getattr(original_exc, "errno", None)),
|
|
586
|
-
error=str(exc),
|
|
587
|
-
retries_used=max(0, attempts_used - 1),
|
|
588
|
-
)
|
|
589
|
-
raise
|
|
590
|
-
finally:
|
|
591
|
-
heal_tmp.unlink(missing_ok=True)
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
def save_team_scoped_state(workspace: Path, team_state: dict[str, Any]) -> None:
|
|
595
|
-
target_key = team_state_key(team_state)
|
|
596
|
-
existing = load_runtime_state(workspace)
|
|
597
|
-
existing_primary_key = team_state_key(existing) if existing.get("session_name") else None
|
|
598
|
-
if (
|
|
599
|
-
existing_primary_key is not None
|
|
600
|
-
and existing_primary_key != target_key
|
|
601
|
-
and existing.get("session_name")
|
|
602
|
-
and existing.get("session_name") == team_state.get("session_name")
|
|
603
|
-
):
|
|
604
|
-
existing_primary_key = target_key
|
|
605
|
-
existing_teams = existing.get("teams") or {}
|
|
606
|
-
incoming_teams = team_state.get("teams") if isinstance(team_state.get("teams"), dict) else None
|
|
607
|
-
if not existing_teams and existing_primary_key == target_key:
|
|
608
|
-
merged = copy.deepcopy(team_state)
|
|
609
|
-
merged.pop("teams", None)
|
|
610
|
-
save_runtime_state(workspace, merged)
|
|
611
|
-
return
|
|
612
|
-
teams = copy.deepcopy(incoming_teams or existing_teams)
|
|
613
|
-
teams[target_key] = compact_team_state(team_state)
|
|
614
|
-
if existing_primary_key is None or existing_primary_key == target_key:
|
|
615
|
-
merged = copy.deepcopy(team_state)
|
|
616
|
-
merged["teams"] = teams
|
|
617
|
-
else:
|
|
618
|
-
merged = copy.deepcopy(existing)
|
|
619
|
-
merged["teams"] = teams
|
|
620
|
-
if not merged.get("teams"):
|
|
621
|
-
merged.pop("teams", None)
|
|
622
|
-
save_runtime_state(workspace, merged)
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
def write_team_state(workspace: Path, spec: dict[str, Any], runtime: dict[str, Any], results: list[dict[str, Any]] | None = None) -> Path:
|
|
626
|
-
path = workspace / spec.get("context", {}).get("state_file", "team_state.md")
|
|
627
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
628
|
-
lines = [
|
|
629
|
-
"# Team State",
|
|
630
|
-
"",
|
|
631
|
-
f"Updated: {datetime.now(timezone.utc).isoformat()}",
|
|
632
|
-
"",
|
|
633
|
-
"## Objective",
|
|
634
|
-
"",
|
|
635
|
-
spec.get("team", {}).get("objective", ""),
|
|
636
|
-
"",
|
|
637
|
-
"## Team",
|
|
638
|
-
"",
|
|
639
|
-
f"- Name: {spec.get('team', {}).get('name')}",
|
|
640
|
-
f"- Runtime session: {runtime.get('session_name')}",
|
|
641
|
-
]
|
|
642
|
-
receiver = runtime.get("leader_receiver") or {}
|
|
643
|
-
if receiver:
|
|
644
|
-
if receiver.get("mode") == "direct_tmux":
|
|
645
|
-
lines.append(
|
|
646
|
-
f"- Leader receiver: direct tmux {receiver.get('pane_id')} "
|
|
647
|
-
f"({receiver.get('provider')}, {receiver.get('status')})"
|
|
648
|
-
)
|
|
649
|
-
else:
|
|
650
|
-
lines.append(f"- Leader inbox fallback: {receiver.get('session')}:{receiver.get('window')} ({receiver.get('status')})")
|
|
651
|
-
lines.append(f"- Leader inbox log: {receiver.get('path')}")
|
|
652
|
-
lines.extend(["", "## Agents", ""])
|
|
653
|
-
for agent in spec.get("agents", []):
|
|
654
|
-
status = runtime.get("agents", {}).get(agent["id"], {}).get("status", "unknown")
|
|
655
|
-
lines.append(f"- {agent['id']}: {agent['role']} on {agent['provider']} ({status})")
|
|
656
|
-
lines.extend(["", "## Task Graph", ""])
|
|
657
|
-
for task in runtime.get("tasks", spec.get("tasks", [])):
|
|
658
|
-
deps = ", ".join(task.get("deps", [])) or "none"
|
|
659
|
-
assignee = task.get("assignee") or "unassigned"
|
|
660
|
-
lines.append(f"- {task['id']} [{task.get('status', 'pending')}], assignee={assignee}, deps={deps}: {task['title']}")
|
|
661
|
-
if task.get("last_result_summary"):
|
|
662
|
-
lines.append(f" Summary: {task['last_result_summary']}")
|
|
663
|
-
if task.get("artifact_refs"):
|
|
664
|
-
for ref in task["artifact_refs"]:
|
|
665
|
-
if isinstance(ref, dict):
|
|
666
|
-
lines.append(f" Artifact: {ref.get('path')} - {ref.get('description', '')}")
|
|
667
|
-
else:
|
|
668
|
-
lines.append(f" Artifact: INVALID artifact ref {ref!r}")
|
|
669
|
-
lines.extend(["", "## Latest Results", ""])
|
|
670
|
-
for result in results or []:
|
|
671
|
-
envelope = json.loads(result["envelope"]) if isinstance(result.get("envelope"), str) else result
|
|
672
|
-
lines.append(f"- {envelope.get('task_id')} from {envelope.get('agent_id')}: {envelope.get('status')} - {envelope.get('summary')}")
|
|
673
|
-
lines.extend(["", "## Blockers", ""])
|
|
674
|
-
blockers = [
|
|
675
|
-
task
|
|
676
|
-
for task in runtime.get("tasks", spec.get("tasks", []))
|
|
677
|
-
if task.get("status") in {"blocked", "failed", "needs_retry"}
|
|
678
|
-
]
|
|
679
|
-
if blockers:
|
|
680
|
-
for task in blockers:
|
|
681
|
-
lines.append(f"- {task['id']}: {task.get('last_result_summary', task.get('title'))}")
|
|
682
|
-
else:
|
|
683
|
-
lines.append("- None")
|
|
684
|
-
lines.extend(["", "## Next Step", "", "- Continue routing ready tasks and collect result envelopes."])
|
|
685
|
-
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
686
|
-
return path
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
def write_spec(path: Path, spec: dict[str, Any]) -> None:
|
|
690
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
691
|
-
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
692
|
-
tmp_path.write_text(dumps(spec), encoding="utf-8")
|
|
693
|
-
os.replace(tmp_path, path)
|