@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
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
//! step 7 · message_store — core message lifecycle over `team.db`.
|
|
2
|
+
//!
|
|
3
|
+
//! Truth source (READ-ONLY): `team-agent-public` @ v0.2.11,
|
|
4
|
+
//! `team_agent/message_store/core.py` + `leader_notification_log.py`.
|
|
5
|
+
//! Builds on step 3 [`crate::db::schema`] (DDL/migration already create all 8
|
|
6
|
+
//! tables incl. `messages` + `leader_notification_log`).
|
|
7
|
+
//!
|
|
8
|
+
//! SCOPE (this slice) = the semantic-tricky core lifecycle:
|
|
9
|
+
//! 1. [`MessageStore::create_message`] — insert a fresh `msg_<hex12>` row,
|
|
10
|
+
//! status `accepted` (`core.py:71-114`).
|
|
11
|
+
//! 2. [`MessageStore::claim_for_delivery`] — atomic single-winner claim; flips
|
|
12
|
+
//! an eligible row to `target_resolved`, bumps `delivery_attempts`, returns
|
|
13
|
+
//! whether THIS caller won (`rowcount == 1`) (`core.py:190-205`).
|
|
14
|
+
//! 3. [`MessageStore::mark`] — status state machine; the only guard is that
|
|
15
|
+
//! `acknowledged` is STICKY against delivery statuses (injected/visible/
|
|
16
|
+
//! submitted/submitted_unverified/delivered) but NOT against others
|
|
17
|
+
//! (e.g. `failed` overwrites) (`core.py:116-138`, the SQL CASE).
|
|
18
|
+
//! 4. [`MessageStore::claim_leader_notification_delivery`] — exactly-once dedup
|
|
19
|
+
//! at the leader-injection boundary. Dedup key = `(result_id, owner_team_id,
|
|
20
|
+
//! owner_epoch)` via the PK + `INSERT OR IGNORE`; **`leader_session_uuid` is
|
|
21
|
+
//! NOT part of the key** (nullable audit metadata only). When `owner_epoch`
|
|
22
|
+
//! is `None` it is derived from the uuid via [`legacy_epoch_from_uuid`]
|
|
23
|
+
//! (`leader_notification_log.py:30-101,145-147`).
|
|
24
|
+
//!
|
|
25
|
+
//! DEFERRED (note, don't build) — follow-on RED slices: scheduled events, token
|
|
26
|
+
//! accounting (incl. the `delivery_tokens` side-effect of `mark`), agent health,
|
|
27
|
+
//! result watchers, results store, `artifact_refs` payloads, busy-retry timing.
|
|
28
|
+
//!
|
|
29
|
+
//! §10: pure-ish lib over SQLite — no panic on malformed input; every path
|
|
30
|
+
//! returns `Result<_, MessageStoreError>`.
|
|
31
|
+
|
|
32
|
+
use std::path::Path;
|
|
33
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
34
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
35
|
+
|
|
36
|
+
use rusqlite::{params, OptionalExtension};
|
|
37
|
+
use thiserror::Error;
|
|
38
|
+
|
|
39
|
+
static MESSAGE_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
40
|
+
|
|
41
|
+
#[derive(Debug, Error)]
|
|
42
|
+
pub enum MessageStoreError {
|
|
43
|
+
#[error("db: {0}")]
|
|
44
|
+
Db(#[from] crate::db::DbError),
|
|
45
|
+
#[error("sqlite: {0}")]
|
|
46
|
+
Sqlite(#[from] rusqlite::Error),
|
|
47
|
+
#[error("io: {0}")]
|
|
48
|
+
Io(#[from] std::io::Error),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Outcome of [`MessageStore::claim_leader_notification_delivery`]
|
|
52
|
+
/// (`leader_notification_log.py:73-101`). `status` is `"claimed_by_you"` for the
|
|
53
|
+
/// winner, `"already_notified_by"` for a deduped loser; `notified_message_id` is
|
|
54
|
+
/// always the WINNER's proposed id (a loser sees the first winner's id).
|
|
55
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
56
|
+
pub struct NotificationClaim {
|
|
57
|
+
pub status: String,
|
|
58
|
+
pub notified_message_id: String,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Args for [`MessageStore::claim_leader_notification_delivery`]
|
|
62
|
+
/// (`leader_notification_log.py:30-40`).
|
|
63
|
+
#[derive(Debug, Clone)]
|
|
64
|
+
pub struct NotificationClaimParams<'a> {
|
|
65
|
+
pub result_id: &'a str,
|
|
66
|
+
pub owner_team_id: Option<&'a str>,
|
|
67
|
+
/// `None` → derived from `leader_session_uuid` via [`legacy_epoch_from_uuid`].
|
|
68
|
+
pub owner_epoch: Option<i64>,
|
|
69
|
+
pub leader_session_uuid: Option<&'a str>,
|
|
70
|
+
pub proposed_message_id: &'a str,
|
|
71
|
+
pub envelope_hash: &'a str,
|
|
72
|
+
pub pane_id: Option<&'a str>,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// `leader_notification_log._legacy_epoch_from_uuid` (line 145-147):
|
|
76
|
+
/// `int(zlib.crc32(str(uuid or "").encode("utf-8")) & 0x7FFFFFFF)`.
|
|
77
|
+
pub fn legacy_epoch_from_uuid(leader_session_uuid: Option<&str>) -> i64 {
|
|
78
|
+
let mut crc = 0xFFFF_FFFFu32;
|
|
79
|
+
for byte in leader_session_uuid.unwrap_or("").as_bytes() {
|
|
80
|
+
crc ^= u32::from(*byte);
|
|
81
|
+
for _ in 0..8 {
|
|
82
|
+
let mask = 0u32.wrapping_sub(crc & 1);
|
|
83
|
+
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
i64::from((!crc) & 0x7FFF_FFFF)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// SQLite-backed message store (`core.py:MessageStore`). `open` mirrors
|
|
90
|
+
/// `__init__`: `runtime_dir(workspace)/team.db`, mkdir parents, init schema.
|
|
91
|
+
pub struct MessageStore {
|
|
92
|
+
#[allow(dead_code)]
|
|
93
|
+
path: std::path::PathBuf,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl MessageStore {
|
|
97
|
+
/// `MessageStore.__init__` (`core.py:51-55`): `workspace/.team/runtime/team.db`,
|
|
98
|
+
/// create parents, `initialize_schema`.
|
|
99
|
+
pub fn open(workspace: &Path) -> Result<Self, MessageStoreError> {
|
|
100
|
+
let runtime_dir = workspace.join(".team").join("runtime");
|
|
101
|
+
std::fs::create_dir_all(&runtime_dir)?;
|
|
102
|
+
let path = runtime_dir.join("team.db");
|
|
103
|
+
let conn = crate::db::schema::open_db(&path)?;
|
|
104
|
+
crate::db::schema::initialize_schema(&conn, Some(&path))?;
|
|
105
|
+
Ok(Self { path })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Absolute path to the backing `team.db` (test/diagnostic accessor).
|
|
109
|
+
pub fn db_path(&self) -> &Path {
|
|
110
|
+
&self.path
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// `create_message` (`core.py:71-114`). Returns `msg_<uuid4 hex[:12]>`; inserts
|
|
114
|
+
/// a row with `status='accepted'`, `requires_ack` as 0/1 int, `artifact_refs`
|
|
115
|
+
/// defaulting to `'[]'`, `delivery_attempts=0`, timestamps = now.
|
|
116
|
+
#[allow(clippy::too_many_arguments)]
|
|
117
|
+
pub fn create_message(
|
|
118
|
+
&self,
|
|
119
|
+
task_id: Option<&str>,
|
|
120
|
+
sender: &str,
|
|
121
|
+
recipient: &str,
|
|
122
|
+
content: &str,
|
|
123
|
+
reply_to: Option<&str>,
|
|
124
|
+
requires_ack: bool,
|
|
125
|
+
owner_team_id: Option<&str>,
|
|
126
|
+
) -> Result<String, MessageStoreError> {
|
|
127
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
128
|
+
let message_id = next_message_id();
|
|
129
|
+
let now = now_ts();
|
|
130
|
+
conn.execute(
|
|
131
|
+
"insert into messages(
|
|
132
|
+
message_id, owner_team_id, task_id, sender, recipient, reply_to, requires_ack,
|
|
133
|
+
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
134
|
+
acknowledged_at, error, delivery_attempts
|
|
135
|
+
) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'accepted', ?8, '[]', ?9, ?9, null, null, null, 0)",
|
|
136
|
+
params![
|
|
137
|
+
message_id,
|
|
138
|
+
owner_team_id,
|
|
139
|
+
task_id,
|
|
140
|
+
sender,
|
|
141
|
+
recipient,
|
|
142
|
+
reply_to,
|
|
143
|
+
if requires_ack { 1 } else { 0 },
|
|
144
|
+
content,
|
|
145
|
+
now,
|
|
146
|
+
],
|
|
147
|
+
)?;
|
|
148
|
+
Ok(message_id)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Caller-supplied-id variant of [`create_message`] (CR-015/054 — `--message-id`).
|
|
152
|
+
/// Inserts exactly the given `message_id` instead of generating one. The store
|
|
153
|
+
/// PK is `message_id`, so a repeat with the same id is rejected by SQLite; the
|
|
154
|
+
/// caller is expected to gate via [`message_exists`] first to map collision to
|
|
155
|
+
/// a typed `Duplicate` refusal rather than an opaque sqlite error.
|
|
156
|
+
///
|
|
157
|
+
/// [`message_exists`]: Self::message_exists
|
|
158
|
+
#[allow(clippy::too_many_arguments)]
|
|
159
|
+
pub fn create_message_with_id(
|
|
160
|
+
&self,
|
|
161
|
+
message_id: &str,
|
|
162
|
+
task_id: Option<&str>,
|
|
163
|
+
sender: &str,
|
|
164
|
+
recipient: &str,
|
|
165
|
+
content: &str,
|
|
166
|
+
reply_to: Option<&str>,
|
|
167
|
+
requires_ack: bool,
|
|
168
|
+
owner_team_id: Option<&str>,
|
|
169
|
+
) -> Result<String, MessageStoreError> {
|
|
170
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
171
|
+
let now = now_ts();
|
|
172
|
+
conn.execute(
|
|
173
|
+
"insert into messages(
|
|
174
|
+
message_id, owner_team_id, task_id, sender, recipient, reply_to, requires_ack,
|
|
175
|
+
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
176
|
+
acknowledged_at, error, delivery_attempts
|
|
177
|
+
) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'accepted', ?8, '[]', ?9, ?9, null, null, null, 0)",
|
|
178
|
+
params![
|
|
179
|
+
message_id,
|
|
180
|
+
owner_team_id,
|
|
181
|
+
task_id,
|
|
182
|
+
sender,
|
|
183
|
+
recipient,
|
|
184
|
+
reply_to,
|
|
185
|
+
if requires_ack { 1 } else { 0 },
|
|
186
|
+
content,
|
|
187
|
+
now,
|
|
188
|
+
],
|
|
189
|
+
)?;
|
|
190
|
+
Ok(message_id.to_string())
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// `true` iff a `messages` row with this `message_id` already exists. Used by
|
|
194
|
+
/// the send path to map a caller-key collision (CR-015/054) to a `Duplicate`
|
|
195
|
+
/// refusal before attempting an insert that would otherwise fail on the PK.
|
|
196
|
+
pub fn message_exists(&self, message_id: &str) -> Result<bool, MessageStoreError> {
|
|
197
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
198
|
+
let row: Option<i64> = conn
|
|
199
|
+
.query_row(
|
|
200
|
+
"select 1 from messages where message_id = ?1",
|
|
201
|
+
params![message_id],
|
|
202
|
+
|row| row.get(0),
|
|
203
|
+
)
|
|
204
|
+
.optional()?;
|
|
205
|
+
Ok(row.is_some())
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// `mark` (`core.py:116-138`) — the messages.status state machine (this slice
|
|
209
|
+
/// excludes the `delivery_tokens` side-effect, which is deferred).
|
|
210
|
+
pub fn mark(&self, message_id: &str, status: &str, error: Option<&str>) -> Result<(), MessageStoreError> {
|
|
211
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
212
|
+
let now = now_ts();
|
|
213
|
+
conn.execute(
|
|
214
|
+
"update messages
|
|
215
|
+
set status = case
|
|
216
|
+
when status = 'acknowledged'
|
|
217
|
+
and ?2 in ('injected', 'visible', 'submitted', 'submitted_unverified', 'delivered')
|
|
218
|
+
then status
|
|
219
|
+
else ?2
|
|
220
|
+
end,
|
|
221
|
+
updated_at = ?3,
|
|
222
|
+
delivered_at = case
|
|
223
|
+
when ?2 in ('injected', 'visible', 'submitted', 'submitted_unverified', 'delivered')
|
|
224
|
+
then ?3
|
|
225
|
+
else delivered_at
|
|
226
|
+
end,
|
|
227
|
+
acknowledged_at = case when ?2 = 'acknowledged' then ?3 else acknowledged_at end,
|
|
228
|
+
error = coalesce(?4, error)
|
|
229
|
+
where message_id = ?1",
|
|
230
|
+
params![message_id, status, now, error],
|
|
231
|
+
)?;
|
|
232
|
+
Ok(())
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// `claim_for_delivery` (`core.py:190-205`): atomic single-winner claim. Flips an
|
|
236
|
+
/// eligible row (status ∈ pending/accepted/queued_until_idle/queued_until_start/
|
|
237
|
+
/// queued_stopped/queued_pane_missing) to `target_resolved`, `delivery_attempts +=
|
|
238
|
+
/// 1`. Returns `true` iff THIS update matched exactly one row.
|
|
239
|
+
pub fn claim_for_delivery(&self, message_id: &str) -> Result<bool, MessageStoreError> {
|
|
240
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
241
|
+
let rows = conn.execute(
|
|
242
|
+
"update messages
|
|
243
|
+
set status = 'target_resolved',
|
|
244
|
+
delivery_attempts = delivery_attempts + 1,
|
|
245
|
+
updated_at = ?2
|
|
246
|
+
where message_id = ?1
|
|
247
|
+
and status in (
|
|
248
|
+
'pending', 'accepted', 'queued_until_idle', 'queued_until_start',
|
|
249
|
+
'queued_stopped', 'queued_pane_missing'
|
|
250
|
+
)",
|
|
251
|
+
params![message_id, now_ts()],
|
|
252
|
+
)?;
|
|
253
|
+
Ok(rows == 1)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Read inbox rows for an agent. This projection intentionally has no owner-team
|
|
257
|
+
/// filter when the caller does not provide one: legacy/CLI inbox must surface
|
|
258
|
+
/// NULL-owner messages stored for the agent.
|
|
259
|
+
pub fn inbox(
|
|
260
|
+
&self,
|
|
261
|
+
agent_id: &str,
|
|
262
|
+
limit: usize,
|
|
263
|
+
owner_team_id: Option<&str>,
|
|
264
|
+
) -> Result<Vec<serde_json::Value>, MessageStoreError> {
|
|
265
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
266
|
+
let limit = i64::try_from(limit).unwrap_or(i64::MAX);
|
|
267
|
+
let sql = match owner_team_id {
|
|
268
|
+
Some(_) => {
|
|
269
|
+
"select message_id, owner_team_id, task_id, sender, recipient, reply_to, requires_ack,
|
|
270
|
+
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
271
|
+
acknowledged_at, error, delivery_attempts
|
|
272
|
+
from messages
|
|
273
|
+
where (sender = ?1 or recipient = ?1) and owner_team_id = ?3
|
|
274
|
+
order by created_at desc
|
|
275
|
+
limit ?2"
|
|
276
|
+
}
|
|
277
|
+
None => {
|
|
278
|
+
"select message_id, owner_team_id, task_id, sender, recipient, reply_to, requires_ack,
|
|
279
|
+
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
280
|
+
acknowledged_at, error, delivery_attempts
|
|
281
|
+
from messages
|
|
282
|
+
where sender = ?1 or recipient = ?1
|
|
283
|
+
order by created_at desc
|
|
284
|
+
limit ?2"
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
let rows = match owner_team_id {
|
|
288
|
+
Some(team) => {
|
|
289
|
+
let mut stmt = conn.prepare(sql)?;
|
|
290
|
+
let values = stmt
|
|
291
|
+
.query_map(params![agent_id, limit, team], row_to_message_value)?
|
|
292
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
293
|
+
values
|
|
294
|
+
}
|
|
295
|
+
None => {
|
|
296
|
+
let mut stmt = conn.prepare(sql)?;
|
|
297
|
+
let values = stmt
|
|
298
|
+
.query_map(params![agent_id, limit], row_to_message_value)?
|
|
299
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
300
|
+
values
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
Ok(rows.into_iter().rev().collect())
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// Allow direct peer messages in both directions. Golden stores `(a,b)` and
|
|
307
|
+
/// `(b,a)` so either sender/recipient lookup can use a single ordered key.
|
|
308
|
+
pub fn allow_peer(&self, a: &str, b: &str) -> Result<(), MessageStoreError> {
|
|
309
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
310
|
+
let now = now_ts();
|
|
311
|
+
conn.execute(
|
|
312
|
+
"insert or ignore into peer_allowlist(a, b, created_at) values (?1, ?2, ?3)",
|
|
313
|
+
params![a, b, now.as_str()],
|
|
314
|
+
)?;
|
|
315
|
+
conn.execute(
|
|
316
|
+
"insert or ignore into peer_allowlist(a, b, created_at) values (?1, ?2, ?3)",
|
|
317
|
+
params![b, a, now.as_str()],
|
|
318
|
+
)?;
|
|
319
|
+
Ok(())
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// `claim_leader_notification_delivery` (`leader_notification_log.py:30-101`):
|
|
323
|
+
/// `INSERT OR IGNORE` on PK `(result_id, owner_team_id, owner_epoch)`. rowcount==1
|
|
324
|
+
/// → `claimed_by_you`; else read the existing winner row → `already_notified_by`.
|
|
325
|
+
/// `owner_team_id` defaults to `""`; `owner_epoch=None` → [`legacy_epoch_from_uuid`].
|
|
326
|
+
pub fn claim_leader_notification_delivery(
|
|
327
|
+
&self,
|
|
328
|
+
params: NotificationClaimParams<'_>,
|
|
329
|
+
) -> Result<NotificationClaim, MessageStoreError> {
|
|
330
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
331
|
+
let owner_team_id = params.owner_team_id.unwrap_or("");
|
|
332
|
+
let owner_epoch = match params.owner_epoch {
|
|
333
|
+
Some(epoch) => epoch,
|
|
334
|
+
None => legacy_epoch_from_uuid(params.leader_session_uuid),
|
|
335
|
+
};
|
|
336
|
+
let rows = conn.execute(
|
|
337
|
+
"insert or ignore into leader_notification_log(
|
|
338
|
+
result_id, owner_team_id, owner_epoch, leader_session_uuid, notified_message_id,
|
|
339
|
+
notified_at, leader_pane_id_at_notify, envelope_content_hash
|
|
340
|
+
) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
|
341
|
+
params![
|
|
342
|
+
params.result_id,
|
|
343
|
+
owner_team_id,
|
|
344
|
+
owner_epoch,
|
|
345
|
+
params.leader_session_uuid,
|
|
346
|
+
params.proposed_message_id,
|
|
347
|
+
now_ts(),
|
|
348
|
+
params.pane_id,
|
|
349
|
+
params.envelope_hash,
|
|
350
|
+
],
|
|
351
|
+
)?;
|
|
352
|
+
if rows == 1 {
|
|
353
|
+
return Ok(NotificationClaim {
|
|
354
|
+
status: "claimed_by_you".to_string(),
|
|
355
|
+
notified_message_id: params.proposed_message_id.to_string(),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let notified_message_id = conn
|
|
360
|
+
.query_row(
|
|
361
|
+
"select notified_message_id from leader_notification_log
|
|
362
|
+
where result_id = ?1 and owner_team_id = ?2 and owner_epoch = ?3",
|
|
363
|
+
params![params.result_id, owner_team_id, owner_epoch],
|
|
364
|
+
|row| row.get::<_, String>(0),
|
|
365
|
+
)
|
|
366
|
+
.optional()?
|
|
367
|
+
.ok_or(rusqlite::Error::QueryReturnedNoRows)?;
|
|
368
|
+
Ok(NotificationClaim {
|
|
369
|
+
status: "already_notified_by".to_string(),
|
|
370
|
+
notified_message_id,
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fn row_to_message_value(row: &rusqlite::Row<'_>) -> rusqlite::Result<serde_json::Value> {
|
|
376
|
+
Ok(serde_json::json!({
|
|
377
|
+
"message_id": row.get::<_, String>(0)?,
|
|
378
|
+
"owner_team_id": row.get::<_, Option<String>>(1)?,
|
|
379
|
+
"task_id": row.get::<_, Option<String>>(2)?,
|
|
380
|
+
"sender": row.get::<_, Option<String>>(3)?,
|
|
381
|
+
"recipient": row.get::<_, Option<String>>(4)?,
|
|
382
|
+
"reply_to": row.get::<_, Option<String>>(5)?,
|
|
383
|
+
"requires_ack": row.get::<_, Option<i64>>(6)?,
|
|
384
|
+
"status": row.get::<_, Option<String>>(7)?,
|
|
385
|
+
"content": row.get::<_, Option<String>>(8)?,
|
|
386
|
+
"artifact_refs": row.get::<_, Option<String>>(9)?,
|
|
387
|
+
"created_at": row.get::<_, Option<String>>(10)?,
|
|
388
|
+
"updated_at": row.get::<_, Option<String>>(11)?,
|
|
389
|
+
"delivered_at": row.get::<_, Option<String>>(12)?,
|
|
390
|
+
"acknowledged_at": row.get::<_, Option<String>>(13)?,
|
|
391
|
+
"error": row.get::<_, Option<String>>(14)?,
|
|
392
|
+
"delivery_attempts": row.get::<_, Option<i64>>(15)?,
|
|
393
|
+
}))
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fn now_ts() -> String {
|
|
397
|
+
chrono::Utc::now().to_rfc3339()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
fn next_message_id() -> String {
|
|
401
|
+
let nanos = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
|
402
|
+
Ok(duration) => {
|
|
403
|
+
let low = duration.as_nanos() & u128::from(u64::MAX);
|
|
404
|
+
u64::try_from(low).unwrap_or(0)
|
|
405
|
+
}
|
|
406
|
+
Err(_) => 0,
|
|
407
|
+
};
|
|
408
|
+
let counter = MESSAGE_COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
409
|
+
let pid = u64::from(std::process::id());
|
|
410
|
+
let value = nanos ^ counter.rotate_left(17) ^ pid.rotate_left(32);
|
|
411
|
+
format!("msg_{:012x}", value & 0xFFFF_FFFF_FFFF)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[cfg(test)]
|
|
415
|
+
mod tests {
|
|
416
|
+
#![allow(clippy::unwrap_used)]
|
|
417
|
+
use super::*;
|
|
418
|
+
use crate::db;
|
|
419
|
+
use rusqlite::Connection;
|
|
420
|
+
use std::path::PathBuf;
|
|
421
|
+
use std::sync::atomic::{AtomicU32, Ordering};
|
|
422
|
+
|
|
423
|
+
static COUNTER: AtomicU32 = AtomicU32::new(0);
|
|
424
|
+
|
|
425
|
+
fn temp_workspace() -> PathBuf {
|
|
426
|
+
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
427
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_msgstore_{}_{}", std::process::id(), n));
|
|
428
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
429
|
+
ws
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
fn store() -> MessageStore {
|
|
433
|
+
MessageStore::open(&temp_workspace()).unwrap()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/// Fresh read connection onto the store's team.db (asserts DB-STATE parity
|
|
437
|
+
/// independently of the store's own connection).
|
|
438
|
+
fn read(store: &MessageStore) -> Connection {
|
|
439
|
+
db::schema::open_db(store.db_path()).unwrap()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
fn col_str(conn: &Connection, mid: &str, col: &str) -> Option<String> {
|
|
443
|
+
// `col` is a fixed test literal, never user input.
|
|
444
|
+
conn.query_row(
|
|
445
|
+
&format!("select {col} from messages where message_id = ?1"),
|
|
446
|
+
[mid],
|
|
447
|
+
|r| r.get::<_, Option<String>>(0),
|
|
448
|
+
)
|
|
449
|
+
.unwrap()
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
fn col_i64(conn: &Connection, mid: &str, col: &str) -> i64 {
|
|
453
|
+
conn.query_row(
|
|
454
|
+
&format!("select {col} from messages where message_id = ?1"),
|
|
455
|
+
[mid],
|
|
456
|
+
|r| r.get(0),
|
|
457
|
+
)
|
|
458
|
+
.unwrap()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
fn status_of(conn: &Connection, mid: &str) -> String {
|
|
462
|
+
col_str(conn, mid, "status").unwrap()
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/// (result_id, owner_team_id, owner_epoch, leader_session_uuid, notified_message_id)
|
|
466
|
+
/// ordered by notified_at — the dedup-relevant projection of leader_notification_log.
|
|
467
|
+
fn notif_rows(conn: &Connection) -> Vec<(String, String, i64, Option<String>, String)> {
|
|
468
|
+
let mut stmt = conn
|
|
469
|
+
.prepare(
|
|
470
|
+
"select result_id, owner_team_id, owner_epoch, leader_session_uuid, notified_message_id \
|
|
471
|
+
from leader_notification_log order by notified_at",
|
|
472
|
+
)
|
|
473
|
+
.unwrap();
|
|
474
|
+
stmt.query_map([], |r| {
|
|
475
|
+
Ok((
|
|
476
|
+
r.get::<_, String>(0)?,
|
|
477
|
+
r.get::<_, String>(1)?,
|
|
478
|
+
r.get::<_, i64>(2)?,
|
|
479
|
+
r.get::<_, Option<String>>(3)?,
|
|
480
|
+
r.get::<_, String>(4)?,
|
|
481
|
+
))
|
|
482
|
+
})
|
|
483
|
+
.unwrap()
|
|
484
|
+
.collect::<Result<_, _>>()
|
|
485
|
+
.unwrap()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ───────────────────────────── create_message ─────────────────────────────
|
|
489
|
+
|
|
490
|
+
#[test]
|
|
491
|
+
fn create_message_inserts_accepted_row() {
|
|
492
|
+
let s = store();
|
|
493
|
+
let mid = s
|
|
494
|
+
.create_message(Some("task_1"), "leader", "alice", "hello", None, true, Some("team_A"))
|
|
495
|
+
.unwrap();
|
|
496
|
+
assert!(mid.starts_with("msg_"), "id: {mid}");
|
|
497
|
+
assert_eq!(mid.len(), 16, "msg_ + 12 hex");
|
|
498
|
+
|
|
499
|
+
let c = read(&s);
|
|
500
|
+
assert_eq!(status_of(&c, &mid), "accepted");
|
|
501
|
+
assert_eq!(col_str(&c, &mid, "task_id").as_deref(), Some("task_1"));
|
|
502
|
+
assert_eq!(col_str(&c, &mid, "sender").as_deref(), Some("leader"));
|
|
503
|
+
assert_eq!(col_str(&c, &mid, "recipient").as_deref(), Some("alice"));
|
|
504
|
+
assert_eq!(col_str(&c, &mid, "owner_team_id").as_deref(), Some("team_A"));
|
|
505
|
+
assert_eq!(col_str(&c, &mid, "content").as_deref(), Some("hello"));
|
|
506
|
+
assert_eq!(col_str(&c, &mid, "artifact_refs").as_deref(), Some("[]"));
|
|
507
|
+
assert_eq!(col_str(&c, &mid, "reply_to"), None);
|
|
508
|
+
assert_eq!(col_i64(&c, &mid, "requires_ack"), 1);
|
|
509
|
+
assert_eq!(col_i64(&c, &mid, "delivery_attempts"), 0);
|
|
510
|
+
assert_eq!(col_str(&c, &mid, "delivered_at"), None);
|
|
511
|
+
assert_eq!(col_str(&c, &mid, "acknowledged_at"), None);
|
|
512
|
+
assert_eq!(col_str(&c, &mid, "error"), None);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#[test]
|
|
516
|
+
fn create_message_no_task_no_owner_ack_false() {
|
|
517
|
+
let s = store();
|
|
518
|
+
let mid = s.create_message(None, "leader", "bob", "hi", None, false, None).unwrap();
|
|
519
|
+
let c = read(&s);
|
|
520
|
+
assert_eq!(col_str(&c, &mid, "task_id"), None);
|
|
521
|
+
assert_eq!(col_str(&c, &mid, "owner_team_id"), None);
|
|
522
|
+
assert_eq!(col_i64(&c, &mid, "requires_ack"), 0);
|
|
523
|
+
assert_eq!(status_of(&c, &mid), "accepted");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ───────────────────────── claim_for_delivery (atomic claim) ─────────────────────────
|
|
527
|
+
|
|
528
|
+
#[test]
|
|
529
|
+
fn claim_for_delivery_first_caller_wins() {
|
|
530
|
+
let s = store();
|
|
531
|
+
let mid = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
532
|
+
|
|
533
|
+
assert!(s.claim_for_delivery(&mid).unwrap(), "accepted is eligible → claim wins");
|
|
534
|
+
let c = read(&s);
|
|
535
|
+
assert_eq!(status_of(&c, &mid), "target_resolved");
|
|
536
|
+
assert_eq!(col_i64(&c, &mid, "delivery_attempts"), 1);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
#[test]
|
|
540
|
+
fn claim_for_delivery_second_caller_loses_and_state_unchanged() {
|
|
541
|
+
// Atomic single-winner: once target_resolved, a re-claim returns false and
|
|
542
|
+
// must NOT bump delivery_attempts again.
|
|
543
|
+
let s = store();
|
|
544
|
+
let mid = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
545
|
+
assert!(s.claim_for_delivery(&mid).unwrap());
|
|
546
|
+
assert!(!s.claim_for_delivery(&mid).unwrap(), "already target_resolved → no second winner");
|
|
547
|
+
|
|
548
|
+
let c = read(&s);
|
|
549
|
+
assert_eq!(status_of(&c, &mid), "target_resolved");
|
|
550
|
+
assert_eq!(col_i64(&c, &mid, "delivery_attempts"), 1);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#[test]
|
|
554
|
+
fn claim_for_delivery_nonexistent_is_false() {
|
|
555
|
+
let s = store();
|
|
556
|
+
assert!(!s.claim_for_delivery("msg_doesnotexist").unwrap());
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#[test]
|
|
560
|
+
fn claim_for_delivery_ineligible_status_is_false() {
|
|
561
|
+
// 'failed' is not in the eligible set → claim returns false, status unchanged.
|
|
562
|
+
let s = store();
|
|
563
|
+
let mid = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
564
|
+
s.mark(&mid, "failed", Some("boom")).unwrap();
|
|
565
|
+
assert!(!s.claim_for_delivery(&mid).unwrap());
|
|
566
|
+
assert_eq!(status_of(&read(&s), &mid), "failed");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ───────────────────────────── mark state machine ─────────────────────────────
|
|
570
|
+
|
|
571
|
+
#[test]
|
|
572
|
+
fn mark_injected_sets_status_and_delivered_at() {
|
|
573
|
+
let s = store();
|
|
574
|
+
let mid = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
575
|
+
s.mark(&mid, "injected", None).unwrap();
|
|
576
|
+
let c = read(&s);
|
|
577
|
+
assert_eq!(status_of(&c, &mid), "injected");
|
|
578
|
+
assert!(col_str(&c, &mid, "delivered_at").is_some(), "delivered_at set for injected");
|
|
579
|
+
assert_eq!(col_str(&c, &mid, "acknowledged_at"), None);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
#[test]
|
|
583
|
+
fn mark_acknowledged_is_sticky_against_delivery_statuses() {
|
|
584
|
+
// CASE guard: once acknowledged, marks to injected/visible/... are ignored.
|
|
585
|
+
let s = store();
|
|
586
|
+
let mid = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
587
|
+
s.mark(&mid, "acknowledged", None).unwrap();
|
|
588
|
+
assert_eq!(status_of(&read(&s), &mid), "acknowledged");
|
|
589
|
+
assert!(col_str(&read(&s), &mid, "acknowledged_at").is_some());
|
|
590
|
+
|
|
591
|
+
s.mark(&mid, "injected", None).unwrap();
|
|
592
|
+
assert_eq!(status_of(&read(&s), &mid), "acknowledged", "injected ignored after ack");
|
|
593
|
+
s.mark(&mid, "visible", None).unwrap();
|
|
594
|
+
assert_eq!(status_of(&read(&s), &mid), "acknowledged", "visible ignored after ack");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
#[test]
|
|
598
|
+
fn mark_acknowledged_then_failed_overwrites() {
|
|
599
|
+
// 'failed' is NOT in the guarded delivery set → it overwrites acknowledged.
|
|
600
|
+
let s = store();
|
|
601
|
+
let mid = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
602
|
+
s.mark(&mid, "acknowledged", None).unwrap();
|
|
603
|
+
s.mark(&mid, "failed", Some("x")).unwrap();
|
|
604
|
+
assert_eq!(status_of(&read(&s), &mid), "failed");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#[test]
|
|
608
|
+
fn mark_overwrites_for_non_acknowledged() {
|
|
609
|
+
let s = store();
|
|
610
|
+
let mid = s.create_message(Some("t"), "s", "r", "c", None, true, None).unwrap();
|
|
611
|
+
s.mark(&mid, "injected", None).unwrap();
|
|
612
|
+
s.mark(&mid, "visible", None).unwrap();
|
|
613
|
+
assert_eq!(status_of(&read(&s), &mid), "visible");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ───────────────────── leader_notification_log dedup ─────────────────────
|
|
617
|
+
|
|
618
|
+
fn params<'a>(
|
|
619
|
+
result_id: &'a str,
|
|
620
|
+
owner_team_id: Option<&'a str>,
|
|
621
|
+
owner_epoch: Option<i64>,
|
|
622
|
+
uuid: Option<&'a str>,
|
|
623
|
+
proposed: &'a str,
|
|
624
|
+
) -> NotificationClaimParams<'a> {
|
|
625
|
+
NotificationClaimParams {
|
|
626
|
+
result_id,
|
|
627
|
+
owner_team_id,
|
|
628
|
+
owner_epoch,
|
|
629
|
+
leader_session_uuid: uuid,
|
|
630
|
+
proposed_message_id: proposed,
|
|
631
|
+
envelope_hash: "h1",
|
|
632
|
+
pane_id: Some("%1"),
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#[test]
|
|
637
|
+
fn leader_notification_dedup_key_excludes_session_uuid() {
|
|
638
|
+
// SAME (result_id, owner_team_id, owner_epoch) but DIFFERENT leader_session_uuid
|
|
639
|
+
// and proposed id → second is deduped to the first winner.
|
|
640
|
+
let s = store();
|
|
641
|
+
let r1 = s
|
|
642
|
+
.claim_leader_notification_delivery(params("res_1", Some("team_A"), Some(7), Some("uuid-AAA"), "msg_notif_1"))
|
|
643
|
+
.unwrap();
|
|
644
|
+
assert_eq!(r1.status, "claimed_by_you");
|
|
645
|
+
assert_eq!(r1.notified_message_id, "msg_notif_1");
|
|
646
|
+
|
|
647
|
+
let r2 = s
|
|
648
|
+
.claim_leader_notification_delivery(params("res_1", Some("team_A"), Some(7), Some("uuid-BBB"), "msg_notif_2"))
|
|
649
|
+
.unwrap();
|
|
650
|
+
assert_eq!(r2.status, "already_notified_by");
|
|
651
|
+
assert_eq!(r2.notified_message_id, "msg_notif_1", "loser sees first winner's id, not its own");
|
|
652
|
+
|
|
653
|
+
// Exactly ONE row — carrying the FIRST caller's uuid (uuid not in the key).
|
|
654
|
+
assert_eq!(
|
|
655
|
+
notif_rows(&read(&s)),
|
|
656
|
+
vec![(
|
|
657
|
+
"res_1".to_string(),
|
|
658
|
+
"team_A".to_string(),
|
|
659
|
+
7,
|
|
660
|
+
Some("uuid-AAA".to_string()),
|
|
661
|
+
"msg_notif_1".to_string()
|
|
662
|
+
)]
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
#[test]
|
|
667
|
+
fn leader_notification_different_epoch_is_a_new_claim() {
|
|
668
|
+
let s = store();
|
|
669
|
+
s.claim_leader_notification_delivery(params("res_1", Some("team_A"), Some(7), Some("uuid-AAA"), "msg_notif_1"))
|
|
670
|
+
.unwrap();
|
|
671
|
+
let r3 = s
|
|
672
|
+
.claim_leader_notification_delivery(params("res_1", Some("team_A"), Some(8), Some("uuid-AAA"), "msg_notif_3"))
|
|
673
|
+
.unwrap();
|
|
674
|
+
assert_eq!(r3.status, "claimed_by_you", "different owner_epoch → different PK → new claim");
|
|
675
|
+
|
|
676
|
+
let rows = notif_rows(&read(&s));
|
|
677
|
+
assert_eq!(rows.len(), 2);
|
|
678
|
+
assert_eq!(rows[0].2, 7);
|
|
679
|
+
assert_eq!(rows[1].2, 8);
|
|
680
|
+
assert_eq!(rows[1].4, "msg_notif_3");
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#[test]
|
|
684
|
+
fn leader_notification_none_epoch_derives_from_uuid() {
|
|
685
|
+
// owner_epoch=None → epoch derived from uuid. Same uuid dedups; different uuid
|
|
686
|
+
// yields a different derived epoch → a new claim.
|
|
687
|
+
let s = store();
|
|
688
|
+
let a1 = s
|
|
689
|
+
.claim_leader_notification_delivery(params("res_2", Some("team_B"), None, Some("uuid-AAA"), "m1"))
|
|
690
|
+
.unwrap();
|
|
691
|
+
assert_eq!(a1.status, "claimed_by_you");
|
|
692
|
+
|
|
693
|
+
let a2 = s
|
|
694
|
+
.claim_leader_notification_delivery(params("res_2", Some("team_B"), None, Some("uuid-AAA"), "m2"))
|
|
695
|
+
.unwrap();
|
|
696
|
+
assert_eq!(a2.status, "already_notified_by");
|
|
697
|
+
assert_eq!(a2.notified_message_id, "m1");
|
|
698
|
+
|
|
699
|
+
let a3 = s
|
|
700
|
+
.claim_leader_notification_delivery(params("res_2", Some("team_B"), None, Some("uuid-BBB"), "m3"))
|
|
701
|
+
.unwrap();
|
|
702
|
+
assert_eq!(a3.status, "claimed_by_you", "different uuid → different derived epoch → new claim");
|
|
703
|
+
|
|
704
|
+
// Stored epochs equal the crc32 derivation of each uuid.
|
|
705
|
+
let rows = notif_rows(&read(&s));
|
|
706
|
+
assert_eq!(rows.len(), 2);
|
|
707
|
+
assert_eq!(rows[0].2, 926068568); // crc32('uuid-AAA') & 0x7FFFFFFF
|
|
708
|
+
assert_eq!(rows[1].2, 122688376); // crc32('uuid-BBB') & 0x7FFFFFFF
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
#[test]
|
|
712
|
+
fn legacy_epoch_from_uuid_crc32_golden() {
|
|
713
|
+
assert_eq!(legacy_epoch_from_uuid(None), 0);
|
|
714
|
+
assert_eq!(legacy_epoch_from_uuid(Some("")), 0);
|
|
715
|
+
assert_eq!(legacy_epoch_from_uuid(Some("uuid-AAA")), 926068568);
|
|
716
|
+
assert_eq!(legacy_epoch_from_uuid(Some("uuid-BBB")), 122688376);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
#[test]
|
|
720
|
+
fn allow_peer_inserts_bidirectional_rows_idempotently() {
|
|
721
|
+
let s = store();
|
|
722
|
+
s.allow_peer("alice", "bob").unwrap();
|
|
723
|
+
s.allow_peer("alice", "bob").unwrap();
|
|
724
|
+
|
|
725
|
+
let c = read(&s);
|
|
726
|
+
let mut rows = c
|
|
727
|
+
.prepare("select a, b from peer_allowlist order by a, b")
|
|
728
|
+
.unwrap()
|
|
729
|
+
.query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)))
|
|
730
|
+
.unwrap()
|
|
731
|
+
.collect::<Result<Vec<_>, _>>()
|
|
732
|
+
.unwrap();
|
|
733
|
+
rows.sort();
|
|
734
|
+
assert_eq!(
|
|
735
|
+
rows,
|
|
736
|
+
vec![
|
|
737
|
+
("alice".to_string(), "bob".to_string()),
|
|
738
|
+
("bob".to_string(), "alice".to_string()),
|
|
739
|
+
]
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ════════════════════════ FIX-LOOP (wave-1) RED test ════════════════════════
|
|
744
|
+
// B1: mark() error column = coalesce(?, error) — a NEW error overwrites, but a
|
|
745
|
+
// mark WITHOUT an error must PRESERVE the existing error (current impl clobbers
|
|
746
|
+
// it to NULL). Golden /tmp/probe_b1.py vs team-agent-public v0.2.11.
|
|
747
|
+
|
|
748
|
+
#[test]
|
|
749
|
+
fn fix_b1_mark_preserves_existing_error_when_none_given() {
|
|
750
|
+
let s = store();
|
|
751
|
+
let mid = s.create_message(Some("t"), "a", "b", "c", None, true, None).unwrap();
|
|
752
|
+
|
|
753
|
+
s.mark(&mid, "failed", Some("boom")).unwrap();
|
|
754
|
+
assert_eq!(col_str(&read(&s), &mid, "error").as_deref(), Some("boom"));
|
|
755
|
+
|
|
756
|
+
// Non-error mark to a delivery status: status/delivered_at advance, but error
|
|
757
|
+
// must remain 'boom' (coalesce(NULL, error)), NOT be clobbered to NULL.
|
|
758
|
+
s.mark(&mid, "injected", None).unwrap();
|
|
759
|
+
let c = read(&s);
|
|
760
|
+
assert_eq!(status_of(&c, &mid), "injected");
|
|
761
|
+
assert_eq!(col_str(&c, &mid, "error").as_deref(), Some("boom"), "existing error must survive a no-error mark");
|
|
762
|
+
|
|
763
|
+
// A NEW error overwrites.
|
|
764
|
+
s.mark(&mid, "failed", Some("second")).unwrap();
|
|
765
|
+
assert_eq!(col_str(&read(&s), &mid, "error").as_deref(), Some("second"));
|
|
766
|
+
}
|
|
767
|
+
}
|