@team-agent/installer 0.2.11 → 0.3.1
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 +1204 -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 +1207 -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 +557 -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 +1084 -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 +526 -0
- package/crates/team-agent/src/leader/rediscover.rs +1101 -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 +237 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +272 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -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 +489 -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 +2109 -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 +985 -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 +710 -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 +187 -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 +468 -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 +743 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +329 -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 +553 -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 +578 -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 +659 -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 +765 -0
- package/crates/team-agent/src/tmux_backend.rs +810 -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 +118 -112
- 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
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from team_agent.events import EventLog
|
|
6
|
-
from team_agent.state import worker_sender_bypasses_owner_gate
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def apply_worker_sender_bypass(
|
|
10
|
-
state: dict[str, Any],
|
|
11
|
-
sender: str | None,
|
|
12
|
-
target: Any,
|
|
13
|
-
task_id: str | None,
|
|
14
|
-
event_log: EventLog,
|
|
15
|
-
) -> bool:
|
|
16
|
-
via = worker_sender_bypasses_owner_gate(state, sender)
|
|
17
|
-
if not via:
|
|
18
|
-
return False
|
|
19
|
-
event_log.write(
|
|
20
|
-
"send.bypassed_owner_gate_worker_sender",
|
|
21
|
-
sender=sender,
|
|
22
|
-
env_team_agent_id=via,
|
|
23
|
-
target=target if isinstance(target, str) else None,
|
|
24
|
-
task_id=task_id,
|
|
25
|
-
)
|
|
26
|
-
return True
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
__all__ = ["apply_worker_sender_bypass"]
|
|
@@ -1,539 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
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.message_store.leader_notification_log import peek_leader_notification
|
|
11
|
-
from team_agent.message_store.result_watchers import leader_notified_message_id_for_result
|
|
12
|
-
from team_agent.messaging.deps import send_message
|
|
13
|
-
from team_agent.messaging.internal_delivery import deliver_stored_message
|
|
14
|
-
|
|
15
|
-
_RESULT_DELIVERY_MAX_ATTEMPTS = 5
|
|
16
|
-
_DELIVERED_RESULT_MESSAGE_STATUSES = {"visible", "submitted", "submitted_unverified", "delivered", "acknowledged"}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def retry_result_deliveries(workspace: Path, event_log: EventLog) -> list[dict[str, Any]]:
|
|
20
|
-
store = MessageStore(workspace)
|
|
21
|
-
notified: list[dict[str, Any]] = []
|
|
22
|
-
for watcher in store.retryable_result_watchers():
|
|
23
|
-
if watcher.get("status") != "notify_failed" or not watcher.get("result_id"):
|
|
24
|
-
continue
|
|
25
|
-
row = store.result_by_id(str(watcher["result_id"]))
|
|
26
|
-
if not row:
|
|
27
|
-
continue
|
|
28
|
-
notified.extend(notify_result_watchers(
|
|
29
|
-
workspace,
|
|
30
|
-
_result_entry_from_row(row),
|
|
31
|
-
event_log,
|
|
32
|
-
watchers=[watcher],
|
|
33
|
-
dedupe_reason="rebind_retry",
|
|
34
|
-
))
|
|
35
|
-
return notified
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def notify_result_watchers(
|
|
39
|
-
workspace: Path,
|
|
40
|
-
result: dict[str, Any],
|
|
41
|
-
event_log: EventLog,
|
|
42
|
-
watchers: list[dict[str, Any]] | None = None,
|
|
43
|
-
dedupe_reason: str | None = None,
|
|
44
|
-
) -> list[dict[str, Any]]:
|
|
45
|
-
store = MessageStore(workspace)
|
|
46
|
-
candidates = [
|
|
47
|
-
watcher
|
|
48
|
-
for watcher in (watchers if watchers is not None else store.pending_result_watchers())
|
|
49
|
-
if watcher_matches_result(watcher, result)
|
|
50
|
-
]
|
|
51
|
-
if not candidates:
|
|
52
|
-
return []
|
|
53
|
-
primary, superseded = _dedupe_watchers_for_result(candidates)
|
|
54
|
-
notified: list[dict[str, Any]] = []
|
|
55
|
-
for stale in superseded:
|
|
56
|
-
store.mark_result_watcher(
|
|
57
|
-
stale["watcher_id"],
|
|
58
|
-
"superseded",
|
|
59
|
-
result_id=result.get("result_id"),
|
|
60
|
-
error="superseded by earlier watcher for same (task_id, agent_id, result_id)",
|
|
61
|
-
)
|
|
62
|
-
event_log.write(
|
|
63
|
-
"result_watcher.superseded",
|
|
64
|
-
watcher_id=stale["watcher_id"],
|
|
65
|
-
result_id=result.get("result_id"),
|
|
66
|
-
task_id=result.get("task_id"),
|
|
67
|
-
agent_id=result.get("agent_id"),
|
|
68
|
-
primary_watcher_id=primary["watcher_id"],
|
|
69
|
-
)
|
|
70
|
-
notified.append(
|
|
71
|
-
{
|
|
72
|
-
"watcher_id": stale["watcher_id"],
|
|
73
|
-
"result_id": result.get("result_id"),
|
|
74
|
-
"ok": False,
|
|
75
|
-
"status": "superseded",
|
|
76
|
-
"primary_watcher_id": primary["watcher_id"],
|
|
77
|
-
}
|
|
78
|
-
)
|
|
79
|
-
attempts = result_delivery_attempts(event_log, primary["watcher_id"], str(result.get("result_id") or ""))
|
|
80
|
-
# Stage 12 (Gap 26 ∩ Gap 32 roundtable consolidation 2026-05-26): exactly-once dedupe
|
|
81
|
-
# lives in leader_notification_log keyed by (result_id, leader_session_uuid) and is
|
|
82
|
-
# consulted atomically at the injection boundary inside _send_to_leader_receiver. Here
|
|
83
|
-
# we add a read-only fast-path peek so concurrent notify_result_watchers calls for the
|
|
84
|
-
# same result short-circuit without spinning up a deliver_stored_message round-trip.
|
|
85
|
-
# The peek is NOT the dedupe primitive — the atomic INSERT OR IGNORE at injection is.
|
|
86
|
-
result_id_str = str(result.get("result_id") or "") or None
|
|
87
|
-
if result_id_str:
|
|
88
|
-
leader_identity = _resolve_leader_notification_identity(workspace, primary.get("owner_team_id"))
|
|
89
|
-
if leader_identity:
|
|
90
|
-
prior = peek_leader_notification(
|
|
91
|
-
store,
|
|
92
|
-
result_id=result_id_str,
|
|
93
|
-
leader_session_uuid=leader_identity.get("leader_session_uuid"),
|
|
94
|
-
owner_team_id=primary.get("owner_team_id"),
|
|
95
|
-
owner_epoch=leader_identity.get("owner_epoch"),
|
|
96
|
-
)
|
|
97
|
-
if prior:
|
|
98
|
-
notified.append(_mark_watcher_dedupe_skip(
|
|
99
|
-
store, event_log, primary, result, attempts,
|
|
100
|
-
prior["notified_message_id"],
|
|
101
|
-
dedupe_reason or "injection_log_already_notified",
|
|
102
|
-
notified_at=prior.get("notified_at"),
|
|
103
|
-
leader_session_uuid=leader_identity.get("leader_session_uuid"),
|
|
104
|
-
))
|
|
105
|
-
return notified
|
|
106
|
-
# Legacy compat: watcher.notified_message_id set by a prior path (Gap 32 reversal of
|
|
107
|
-
# 78055bc, or any pre-Stage-12 code) also blocks redelivery. This preserves the
|
|
108
|
-
# Stage 11.9-11.12 era contract while the new gate (leader_notification_log) is the
|
|
109
|
-
# authoritative dedupe primitive going forward.
|
|
110
|
-
legacy_canonical = leader_notified_message_id_for_result(
|
|
111
|
-
store, primary.get("owner_team_id"), result_id_str,
|
|
112
|
-
)
|
|
113
|
-
if legacy_canonical:
|
|
114
|
-
notified.append(_mark_watcher_dedupe_skip(
|
|
115
|
-
store, event_log, primary, result, attempts,
|
|
116
|
-
legacy_canonical,
|
|
117
|
-
dedupe_reason or "rebind_retry",
|
|
118
|
-
))
|
|
119
|
-
return notified
|
|
120
|
-
existing = delivered_result_message(
|
|
121
|
-
store, str(result.get("result_id") or ""),
|
|
122
|
-
task_id=result.get("task_id"),
|
|
123
|
-
owner_team_id=primary.get("owner_team_id"),
|
|
124
|
-
)
|
|
125
|
-
if existing:
|
|
126
|
-
notified.append(_mark_watcher_already_delivered(store, event_log, primary, result, attempts, existing))
|
|
127
|
-
return notified
|
|
128
|
-
if attempts >= _RESULT_DELIVERY_MAX_ATTEMPTS:
|
|
129
|
-
notified.append(_mark_delivery_exhausted(store, event_log, primary, result, attempts))
|
|
130
|
-
else:
|
|
131
|
-
notified.append(_deliver_result_to_watcher(workspace, store, event_log, primary, result, attempts))
|
|
132
|
-
return notified
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _resolve_leader_session_uuid(workspace: Path, owner_team_id: str | None) -> str | None:
|
|
136
|
-
"""Helper: read the team's leader_session_uuid from runtime state for gate lookups."""
|
|
137
|
-
try:
|
|
138
|
-
from team_agent.messaging.deps import load_runtime_state, team_state_key
|
|
139
|
-
state = load_runtime_state(workspace)
|
|
140
|
-
if owner_team_id and isinstance(state.get("teams"), dict):
|
|
141
|
-
scoped = state["teams"].get(owner_team_id)
|
|
142
|
-
if isinstance(scoped, dict):
|
|
143
|
-
state = scoped
|
|
144
|
-
elif owner_team_id and team_state_key(state) != owner_team_id:
|
|
145
|
-
return None
|
|
146
|
-
owner = state.get("team_owner") or {}
|
|
147
|
-
return str(owner.get("leader_session_uuid") or "") or None
|
|
148
|
-
except Exception:
|
|
149
|
-
return None
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _resolve_leader_notification_identity(workspace: Path, owner_team_id: str | None) -> dict[str, Any] | None:
|
|
153
|
-
try:
|
|
154
|
-
from team_agent.messaging.deps import load_runtime_state, team_state_key
|
|
155
|
-
state = load_runtime_state(workspace)
|
|
156
|
-
if owner_team_id and isinstance(state.get("teams"), dict):
|
|
157
|
-
scoped = state["teams"].get(owner_team_id)
|
|
158
|
-
if isinstance(scoped, dict):
|
|
159
|
-
state = scoped
|
|
160
|
-
elif owner_team_id and team_state_key(state) != owner_team_id:
|
|
161
|
-
return None
|
|
162
|
-
owner = state.get("team_owner") or {}
|
|
163
|
-
receiver = state.get("leader_receiver") or {}
|
|
164
|
-
return {
|
|
165
|
-
"leader_session_uuid": str(owner.get("leader_session_uuid") or receiver.get("leader_session_uuid") or "") or None,
|
|
166
|
-
"owner_epoch": int(owner.get("owner_epoch") or receiver.get("owner_epoch") or 0),
|
|
167
|
-
}
|
|
168
|
-
except Exception:
|
|
169
|
-
return None
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _infer_dedupe_reason(primary: dict[str, Any], store: MessageStore) -> str:
|
|
173
|
-
if primary.get("notified_message_id"):
|
|
174
|
-
return "rebind_retry"
|
|
175
|
-
return "watcher_duplicate"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _mark_watcher_dedupe_skip(
|
|
179
|
-
store: MessageStore,
|
|
180
|
-
event_log: EventLog,
|
|
181
|
-
watcher: dict[str, Any],
|
|
182
|
-
result: dict[str, Any],
|
|
183
|
-
attempts: int,
|
|
184
|
-
canonical_message_id: str,
|
|
185
|
-
reason: str,
|
|
186
|
-
*,
|
|
187
|
-
notified_at: str | None = None,
|
|
188
|
-
leader_session_uuid: str | None = None,
|
|
189
|
-
) -> dict[str, Any]:
|
|
190
|
-
original_message_id = watcher.get("notified_message_id")
|
|
191
|
-
# Stage 12: the canonical message_id (or sentinel from the gate) is auditing metadata
|
|
192
|
-
# here. The authoritative dedupe gate is leader_notification_log; this mark just keeps
|
|
193
|
-
# the watcher row from being re-picked by retry scans.
|
|
194
|
-
store.mark_result_watcher(
|
|
195
|
-
watcher["watcher_id"],
|
|
196
|
-
"notified",
|
|
197
|
-
result_id=result.get("result_id"),
|
|
198
|
-
notified_message_id=canonical_message_id,
|
|
199
|
-
)
|
|
200
|
-
event_log.write(
|
|
201
|
-
"leader_receiver.notification_dedupe_skip",
|
|
202
|
-
result_id=result.get("result_id"),
|
|
203
|
-
original_message_id=original_message_id,
|
|
204
|
-
suppressed_message_id=canonical_message_id,
|
|
205
|
-
reason=reason,
|
|
206
|
-
team_id=watcher.get("owner_team_id"),
|
|
207
|
-
watcher_id=watcher["watcher_id"],
|
|
208
|
-
task_id=result.get("task_id"),
|
|
209
|
-
agent_id=result.get("agent_id"),
|
|
210
|
-
attempt=attempts + 1,
|
|
211
|
-
leader_session_uuid=leader_session_uuid,
|
|
212
|
-
prior_notified_at=notified_at,
|
|
213
|
-
)
|
|
214
|
-
return {
|
|
215
|
-
"watcher_id": watcher["watcher_id"],
|
|
216
|
-
"result_id": result.get("result_id"),
|
|
217
|
-
"ok": True,
|
|
218
|
-
"message_id": canonical_message_id,
|
|
219
|
-
"deduped": True,
|
|
220
|
-
"dedupe_reason": reason,
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def _dedupe_watchers_for_result(
|
|
225
|
-
watchers: list[dict[str, Any]],
|
|
226
|
-
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
227
|
-
ordered = sorted(watchers, key=lambda w: (str(w.get("created_at") or ""), str(w.get("watcher_id") or "")))
|
|
228
|
-
return ordered[0], ordered[1:]
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _deliver_result_to_watcher(
|
|
232
|
-
workspace: Path,
|
|
233
|
-
store: MessageStore,
|
|
234
|
-
event_log: EventLog,
|
|
235
|
-
watcher: dict[str, Any],
|
|
236
|
-
result: dict[str, Any],
|
|
237
|
-
attempts: int,
|
|
238
|
-
) -> dict[str, Any]:
|
|
239
|
-
try:
|
|
240
|
-
deliver = deliver_stored_message if watcher.get("owner_team_id") else send_message
|
|
241
|
-
delivery = deliver(
|
|
242
|
-
workspace,
|
|
243
|
-
watcher.get("leader_id") or "leader",
|
|
244
|
-
format_result_watcher_notification(result),
|
|
245
|
-
task_id=result.get("task_id"),
|
|
246
|
-
sender="coordinator",
|
|
247
|
-
requires_ack=False,
|
|
248
|
-
wait_visible=False,
|
|
249
|
-
team=watcher.get("owner_team_id"),
|
|
250
|
-
)
|
|
251
|
-
except Exception as exc:
|
|
252
|
-
return _mark_delivery_failed(store, event_log, watcher, result, attempts, str(exc))
|
|
253
|
-
status = "notified" if delivery.get("ok") else "notify_failed"
|
|
254
|
-
error = delivery.get("reason") or delivery.get("error")
|
|
255
|
-
# Stage 12: notified_message_id is now auditing metadata. The exactly-once contract
|
|
256
|
-
# lives in the leader_notification_log table consulted by _send_to_leader_receiver;
|
|
257
|
-
# whatever the gate suppresses comes back as ok=true deduped=true, and the watcher row
|
|
258
|
-
# records this as a successful notification with the canonical message_id.
|
|
259
|
-
persisted_message_id = (
|
|
260
|
-
delivery.get("canonical_message_id") if delivery.get("deduped")
|
|
261
|
-
else (delivery.get("message_id") if delivery.get("ok") else None)
|
|
262
|
-
)
|
|
263
|
-
store.mark_result_watcher(
|
|
264
|
-
watcher["watcher_id"],
|
|
265
|
-
status,
|
|
266
|
-
result_id=result.get("result_id"),
|
|
267
|
-
notified_message_id=persisted_message_id,
|
|
268
|
-
error=error,
|
|
269
|
-
)
|
|
270
|
-
event_log.write(
|
|
271
|
-
"result_watcher.notified",
|
|
272
|
-
watcher_id=watcher["watcher_id"],
|
|
273
|
-
result_id=result.get("result_id"),
|
|
274
|
-
task_id=result.get("task_id"),
|
|
275
|
-
agent_id=result.get("agent_id"),
|
|
276
|
-
ok=bool(delivery.get("ok")),
|
|
277
|
-
delivery_status=delivery.get("status"),
|
|
278
|
-
message_id=delivery.get("message_id"),
|
|
279
|
-
error=error,
|
|
280
|
-
attempt=attempts + 1,
|
|
281
|
-
)
|
|
282
|
-
return {
|
|
283
|
-
"watcher_id": watcher["watcher_id"],
|
|
284
|
-
"result_id": result.get("result_id"),
|
|
285
|
-
"ok": bool(delivery.get("ok")),
|
|
286
|
-
"message_id": delivery.get("message_id"),
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def _mark_delivery_failed(
|
|
291
|
-
store: MessageStore,
|
|
292
|
-
event_log: EventLog,
|
|
293
|
-
watcher: dict[str, Any],
|
|
294
|
-
result: dict[str, Any],
|
|
295
|
-
attempts: int,
|
|
296
|
-
error: str,
|
|
297
|
-
) -> dict[str, Any]:
|
|
298
|
-
store.mark_result_watcher(watcher["watcher_id"], "notify_failed", result_id=result.get("result_id"), error=error)
|
|
299
|
-
event_log.write(
|
|
300
|
-
"result_watcher.notify_failed",
|
|
301
|
-
watcher_id=watcher["watcher_id"],
|
|
302
|
-
result_id=result.get("result_id"),
|
|
303
|
-
attempt=attempts + 1,
|
|
304
|
-
error=error,
|
|
305
|
-
)
|
|
306
|
-
return {"watcher_id": watcher["watcher_id"], "result_id": result.get("result_id"), "ok": False, "error": error}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def _mark_watcher_already_delivered(
|
|
310
|
-
store: MessageStore,
|
|
311
|
-
event_log: EventLog,
|
|
312
|
-
watcher: dict[str, Any],
|
|
313
|
-
result: dict[str, Any],
|
|
314
|
-
attempts: int,
|
|
315
|
-
message: dict[str, Any],
|
|
316
|
-
) -> dict[str, Any]:
|
|
317
|
-
store.mark_result_watcher(
|
|
318
|
-
watcher["watcher_id"],
|
|
319
|
-
"notified",
|
|
320
|
-
result_id=result.get("result_id"),
|
|
321
|
-
notified_message_id=message.get("message_id"),
|
|
322
|
-
)
|
|
323
|
-
event_log.write(
|
|
324
|
-
"result_watcher.notified",
|
|
325
|
-
watcher_id=watcher["watcher_id"],
|
|
326
|
-
result_id=result.get("result_id"),
|
|
327
|
-
task_id=result.get("task_id"),
|
|
328
|
-
agent_id=result.get("agent_id"),
|
|
329
|
-
ok=True,
|
|
330
|
-
delivery_status="already_delivered",
|
|
331
|
-
message_id=message.get("message_id"),
|
|
332
|
-
deduped=True,
|
|
333
|
-
attempt=attempts,
|
|
334
|
-
)
|
|
335
|
-
return {
|
|
336
|
-
"watcher_id": watcher["watcher_id"],
|
|
337
|
-
"result_id": result.get("result_id"),
|
|
338
|
-
"ok": True,
|
|
339
|
-
"message_id": message.get("message_id"),
|
|
340
|
-
"deduped": True,
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def _mark_delivery_exhausted(
|
|
345
|
-
store: MessageStore,
|
|
346
|
-
event_log: EventLog,
|
|
347
|
-
watcher: dict[str, Any],
|
|
348
|
-
result: dict[str, Any],
|
|
349
|
-
attempts: int,
|
|
350
|
-
) -> dict[str, Any]:
|
|
351
|
-
error = "result delivery retry budget exhausted"
|
|
352
|
-
store.mark_result_watcher(watcher["watcher_id"], "delivery_exhausted", result_id=result.get("result_id"), error=error)
|
|
353
|
-
event_log.write(
|
|
354
|
-
"result_delivery_exhausted",
|
|
355
|
-
watcher_id=watcher["watcher_id"],
|
|
356
|
-
result_id=result.get("result_id"),
|
|
357
|
-
task_id=result.get("task_id"),
|
|
358
|
-
agent_id=result.get("agent_id"),
|
|
359
|
-
attempts=attempts,
|
|
360
|
-
last_error=watcher.get("error"),
|
|
361
|
-
)
|
|
362
|
-
return {"watcher_id": watcher["watcher_id"], "result_id": result.get("result_id"), "ok": False, "error": error}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def _result_entry_from_row(row: dict[str, Any]) -> dict[str, Any]:
|
|
366
|
-
envelope = json.loads(row["envelope"])
|
|
367
|
-
return {
|
|
368
|
-
"result_id": row["result_id"],
|
|
369
|
-
"task_id": envelope.get("task_id"),
|
|
370
|
-
"agent_id": envelope.get("agent_id"),
|
|
371
|
-
"status": envelope.get("status"),
|
|
372
|
-
"summary": envelope.get("summary"),
|
|
373
|
-
"tests": envelope.get("tests", []),
|
|
374
|
-
"created_at": row.get("created_at"),
|
|
375
|
-
"scope": "task",
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def result_delivery_attempts(event_log: EventLog, watcher_id: str, result_id: str) -> int:
|
|
380
|
-
attempts = 0
|
|
381
|
-
for event in event_log.tail(500):
|
|
382
|
-
if event.get("watcher_id") != watcher_id:
|
|
383
|
-
continue
|
|
384
|
-
if event.get("event") == "result_watcher.requeued":
|
|
385
|
-
attempts = 0
|
|
386
|
-
continue
|
|
387
|
-
if event.get("result_id") != result_id:
|
|
388
|
-
continue
|
|
389
|
-
if event.get("event") in {"result_watcher.notified", "result_watcher.notify_failed"}:
|
|
390
|
-
attempts += 1
|
|
391
|
-
return attempts
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def delivered_result_message(
|
|
395
|
-
store: MessageStore,
|
|
396
|
-
result_id: str,
|
|
397
|
-
*,
|
|
398
|
-
task_id: str | None = None,
|
|
399
|
-
owner_team_id: str | None = None,
|
|
400
|
-
) -> dict[str, Any] | None:
|
|
401
|
-
if not result_id:
|
|
402
|
-
return None
|
|
403
|
-
for message in reversed(store.messages(owner_team_id=owner_team_id)):
|
|
404
|
-
if message.get("recipient") != "leader":
|
|
405
|
-
continue
|
|
406
|
-
if task_id and message.get("task_id") != task_id:
|
|
407
|
-
continue
|
|
408
|
-
if message.get("status") not in _DELIVERED_RESULT_MESSAGE_STATUSES:
|
|
409
|
-
continue
|
|
410
|
-
if f"Result id: {result_id}" in str(message.get("content") or ""):
|
|
411
|
-
return message
|
|
412
|
-
return None
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
def result_id_from_text(content: str) -> str | None:
|
|
416
|
-
for line in content.splitlines():
|
|
417
|
-
if line.startswith("Result id: "):
|
|
418
|
-
return line.removeprefix("Result id: ").strip() or None
|
|
419
|
-
return None
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
def watcher_matches_result(watcher: dict[str, Any], result: dict[str, Any]) -> bool:
|
|
423
|
-
task_id = watcher.get("task_id")
|
|
424
|
-
agent_id = watcher.get("agent_id")
|
|
425
|
-
return (not task_id or task_id == result.get("task_id")) and (not agent_id or agent_id == result.get("agent_id"))
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
def requeue_after_claim_leader(
|
|
429
|
-
workspace: Path,
|
|
430
|
-
store: MessageStore,
|
|
431
|
-
event_log: EventLog,
|
|
432
|
-
owner_team_id: str,
|
|
433
|
-
claimed_pane_id: str,
|
|
434
|
-
*,
|
|
435
|
-
incident_ts: str | None = None,
|
|
436
|
-
) -> list[dict[str, Any]]:
|
|
437
|
-
"""Post-claim hook (Gap 26 / Mac mini Stage 11 Scenarios 3, 11.10): re-route every
|
|
438
|
-
not-yet-delivered leader-bound notification to the newly claimed pane. Returns the
|
|
439
|
-
list of requeued watcher records (may be empty).
|
|
440
|
-
|
|
441
|
-
Stage 11.10 semantic reframe: claim-leader means "all not-yet-delivered leader-bound
|
|
442
|
-
notifications for this team_id reroute to the claimed pane". Watcher status is
|
|
443
|
-
irrelevant — `notified_message_id` is the only dedupe gate. Gap 32 exactly-once
|
|
444
|
-
contract still holds: notified_message_id non-null blocks redelivery.
|
|
445
|
-
|
|
446
|
-
Selection rules:
|
|
447
|
-
- watcher is scoped to this team (owner_team_id match)
|
|
448
|
-
- watcher has no notified_message_id (Gap 32 once-only)
|
|
449
|
-
- watcher's latest activity timestamp (completed_at fallback created_at) is
|
|
450
|
-
at-or-after incident_ts when provided; without an incident_ts every
|
|
451
|
-
un-notified watcher is requeued.
|
|
452
|
-
- watcher status is otherwise ignored (pending / delivery_blocked /
|
|
453
|
-
delivery_exhausted / notify_failed all become candidates).
|
|
454
|
-
|
|
455
|
-
Atomicity vs coordinator's own scheduled retry: just before flipping a watcher's
|
|
456
|
-
status, re-fetch the row from the store. If notified_message_id became non-null
|
|
457
|
-
in the gap (the scheduled retry beat us), emit a benign
|
|
458
|
-
leader_receiver.claim_requeue_already_in_flight event and skip. If the race
|
|
459
|
-
leaks past this check, Gap 32 dedupe inside notify_result_watchers still
|
|
460
|
-
guarantees exactly-once injection.
|
|
461
|
-
"""
|
|
462
|
-
# Stage 11.12: CAS re-fetch + claim_requeue_already_in_flight event retired. The atomic
|
|
463
|
-
# UPSERT in notify_result_watchers (claim_leader_notification) is now the single race
|
|
464
|
-
# gate. We mark eligible watchers to notify_failed and let retry_result_deliveries route
|
|
465
|
-
# through the UPSERT — concurrent claim/scheduled-retry paths both pass through the
|
|
466
|
-
# same atomic claim and only one fires deliver_attempt.
|
|
467
|
-
incident_dt = _parse_iso(incident_ts)
|
|
468
|
-
requeued: list[dict[str, Any]] = []
|
|
469
|
-
for watcher in store.result_watchers(owner_team_id=owner_team_id):
|
|
470
|
-
if watcher.get("notified_message_id"):
|
|
471
|
-
continue
|
|
472
|
-
latest_ts = _parse_iso(watcher.get("completed_at")) or _parse_iso(watcher.get("created_at"))
|
|
473
|
-
if incident_dt and latest_ts and latest_ts < incident_dt:
|
|
474
|
-
continue
|
|
475
|
-
watcher_id = watcher["watcher_id"]
|
|
476
|
-
prior_state = str(watcher.get("status") or "")
|
|
477
|
-
store.mark_result_watcher(
|
|
478
|
-
watcher_id, "notify_failed",
|
|
479
|
-
result_id=watcher.get("result_id"),
|
|
480
|
-
)
|
|
481
|
-
event_log.write(
|
|
482
|
-
"leader_receiver.claim_requeue",
|
|
483
|
-
result_id=watcher.get("result_id"),
|
|
484
|
-
watcher_id=watcher_id,
|
|
485
|
-
prior_state=prior_state,
|
|
486
|
-
requeued_at=datetime.now(timezone.utc).isoformat(),
|
|
487
|
-
claimed_pane_id=claimed_pane_id,
|
|
488
|
-
team_id=owner_team_id,
|
|
489
|
-
)
|
|
490
|
-
requeued.append({
|
|
491
|
-
"watcher_id": watcher_id,
|
|
492
|
-
"result_id": watcher.get("result_id"),
|
|
493
|
-
"prior_state": prior_state,
|
|
494
|
-
})
|
|
495
|
-
if requeued:
|
|
496
|
-
try:
|
|
497
|
-
retry_result_deliveries(workspace, event_log)
|
|
498
|
-
except Exception as exc:
|
|
499
|
-
event_log.write(
|
|
500
|
-
"leader_receiver.claim_requeue_delivery_failed",
|
|
501
|
-
error=str(exc),
|
|
502
|
-
watcher_ids=[r["watcher_id"] for r in requeued],
|
|
503
|
-
team_id=owner_team_id,
|
|
504
|
-
claimed_pane_id=claimed_pane_id,
|
|
505
|
-
)
|
|
506
|
-
return requeued
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
def _parse_iso(text: Any) -> datetime | None:
|
|
510
|
-
if not isinstance(text, str) or not text:
|
|
511
|
-
return None
|
|
512
|
-
try:
|
|
513
|
-
dt = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
514
|
-
except ValueError:
|
|
515
|
-
return None
|
|
516
|
-
if dt.tzinfo is None:
|
|
517
|
-
dt = dt.replace(tzinfo=timezone.utc)
|
|
518
|
-
return dt
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
def format_result_watcher_notification(result: dict[str, Any]) -> str:
|
|
522
|
-
task_id = result.get("task_id") or "unknown task"
|
|
523
|
-
agent_id = result.get("agent_id") or "unknown agent"
|
|
524
|
-
status = result.get("status") or "unknown"
|
|
525
|
-
summary = result.get("summary") or "completed"
|
|
526
|
-
lines = [
|
|
527
|
-
f"Task {task_id} reported {status} from {agent_id}: {summary}",
|
|
528
|
-
"Team Agent has collected this result and updated team_state.md. No manual polling is needed.",
|
|
529
|
-
]
|
|
530
|
-
if result.get("result_id"):
|
|
531
|
-
lines.insert(1, f"Result id: {result['result_id']}")
|
|
532
|
-
rendered_tests = [
|
|
533
|
-
f"{test.get('command') or 'test'}={test.get('status') or 'unknown'}"
|
|
534
|
-
for test in (result.get("tests") or [])[:3]
|
|
535
|
-
if isinstance(test, dict)
|
|
536
|
-
]
|
|
537
|
-
if rendered_tests:
|
|
538
|
-
lines.insert(1, "Tests: " + "; ".join(rendered_tests))
|
|
539
|
-
return "\n".join(lines)
|