Summary
Phases 2 and 3 of the Canary Invariant Harness (#411), shipped together in PR #884. Follow-up to PR #653 (Phase 1 — S-01, E-02, L-03).
Adds seven invariants in two waves. The split reflects internal complexity — Phase 2 is the four single-source SQL/Redis checks, Phase 3 adds the three "moderate" ones that each need one new piece of plumbing.
Context
Phase 1 landed the canary library + service + fleet manifest + admin endpoint. The orchestration invariant catalog (docs/testing/orchestration-invariant-catalog.md) identifies 12 high-impact invariants for the starting subset; 3 are now in production. This issue covers the next 7. After it lands, the 2 remaining catalog invariants (E-06, G-01) require either HTTP fan-out or one-shot restart probes — different shape, deferred to a future ticket.
Acceptance Criteria
Phase 2 — single-source SQL/Redis (no new sources)
Phase 3 — moderate complexity (one new piece of plumbing each)
Common
Technical Notes
- Phase 2 stays scope-pure to
canary/ (+ architecture doc). Phase 3's B-02 plumbing touches services/capacity_manager.py:run_maintenance — a one-line heartbeat write at the END of the sweep (so a crash mid-sweep doesn't claim success).
- R-01's
docker exec per cycle is bounded by agent count (not execution count). Cost ≈ 1 exec per running agent per 5 min — negligible.
Out of scope
- Phase 4 — E-06 (HTTP fan-out across agents to
/api/executions/running) and G-01 (one-shot post-restart probe — different shape, doesn't fit the 5-min loop).
Refs
Summary
Phases 2 and 3 of the Canary Invariant Harness (#411), shipped together in PR #884. Follow-up to PR #653 (Phase 1 — S-01, E-02, L-03).
Adds seven invariants in two waves. The split reflects internal complexity — Phase 2 is the four single-source SQL/Redis checks, Phase 3 adds the three "moderate" ones that each need one new piece of plumbing.
Context
Phase 1 landed the canary library + service + fleet manifest + admin endpoint. The orchestration invariant catalog (
docs/testing/orchestration-invariant-catalog.md) identifies 12 high-impact invariants for the starting subset; 3 are now in production. This issue covers the next 7. After it lands, the 2 remaining catalog invariants (E-06, G-01) require either HTTP fan-out or one-shot restart probes — different shape, deferred to a future ticket.Acceptance Criteria
Phase 2 — single-source SQL/Redis (no new sources)
ZCARD(agent:slots:A) ≤ agent_ownership.max_parallel_tasks. Critical. Tier A.status='running' AND started_at < now() - (timeout_seconds + 300s)must be 0. Critical. Tier B. Usesiso_cutoff()(architecture invariant #16).status='running' AND started_at < now() - 60s ⇒ claude_session_id IS NOT NULL. Major. Tier B. Guards bug: Cleanup service misses 'skipped' executions and slow to detect no-session failures #106.db.get_queued_countmatches snapshot's independently-collected queued id-list count. Critical. Tier A. Regression guard against future cache layer / status-filter drift.Phase 3 — moderate complexity (one new piece of plumbing each)
agent:slots:A, the companionagent:slot:A:{eid}HASH hasTTL ≥ execution_timeout_seconds + 300s(SLOT_TTL_BUFFER). Critical. Tier A. Guards bug: Stale slot cleanup uses fixed 20-min TTL regardless of agent timeout #226.len(queued) > 0, then eitherlen(slots excluding sentinels) == max_parallelOR a drain-tick fired in the last 60s. Critical. Tier B. Catches stalled drain callbacks. RequiresCapacityManager.run_maintenance()to write acanary:drain_tick_atheartbeat Redis key on each successful sweep.trinity.platform=agentcontainer,ps -eo stat,comm | grep ' Z.*claude' | wc -lreturns 0. Critical. Tier A. Guards bug: agent-server.py spins at ~83% CPU after claude CLI subprocess becomes defunct #407. New source type (Docker exec) — failures recorded insources_unavailableso an unhealthy container doesn't kill the cycle.Common
CanaryService.run_cycle().POST /api/canary/run-cyclereports it, revert, observe green.docs/memory/architecture.mdCANARY-001 section updated with Phase 2 + Phase 3 invariant blocks.Technical Notes
canary/(+ architecture doc). Phase 3's B-02 plumbing touchesservices/capacity_manager.py:run_maintenance— a one-line heartbeat write at the END of the sweep (so a crash mid-sweep doesn't claim success).docker execper cycle is bounded by agent count (not execution count). Cost ≈ 1 exec per running agent per 5 min — negligible.Out of scope
/api/executions/running) and G-01 (one-shot post-restart probe — different shape, doesn't fit the 5-min loop).Refs
docs/testing/orchestration-invariant-catalog.mdRecommended Starting Subsetdocs/memory/architecture.md