ADR-0014: Session Counter Forward-Progression Semantics¶
Status: Accepted Enforced by: tests/test_state_machine.py Date: 2026-03-01
Context¶
SessionCounters (introduced in PR #1689, issue #1542) tracks per-session
completion counts for each pipeline stage: triaged, planned, implemented,
reviewed, and merged. These counters drive the dashboard's
completed_session metric via build_pipeline_stats in orchestrator.py.
Three stages — triage, plan, and implement — follow a consistent forward-progression pattern: the counter increments exactly once per issue, only when the issue successfully exits that stage and transitions to the next one. Failures, escalations to HITL, and retries do not increment.
| Stage | Increments when | Guard |
|---|---|---|
triaged |
Issue transitions from triage to plan or ready |
Transition call succeeds |
planned |
Plan posted and label swapped to ready, or issue closed as already satisfied |
Plan completion path |
implemented |
PR created and issue transitions to review |
result.success is True |
The review stage counter in _record_review_outcome (review_phase.py:_record_review_outcome)
increments only when the verdict is APPROVE. This is semantically correct
for forward-progression — non-approved reviews (REQUEST_CHANGES, NEEDS_CHANGES)
represent retry loops, not successful exits from the stage. An earlier version
of the code (noted in memory #1697) called increment_session_counter for all
verdicts, which inflated the count when issues cycled through multiple review
rounds. The current code guards on APPROVE, aligning with the
forward-progression pattern.
A secondary risk exists in session_counter_map inside build_pipeline_stats
(orchestrator.py:build_pipeline_stats). This dict maps dashboard stage names to
SessionCounters field names. If an unknown stage is added and mapped to
another stage's field name (e.g., mapping "hitl" to "reviewed" instead of
""), that stage's count leaks into the wrong display column. The current code
correctly maps "hitl" to "" so the if counter_field else 0 guard returns
zero.
Decision¶
Adopt forward-progression-only as the canonical semantics for all
SessionCounters stage counters:
-
Increment once per issue, on successful stage exit. A counter increments when the issue irreversibly transitions to the next pipeline stage. Retries, failures, and intermediate states do not increment.
-
Guard on transition, not on attempt. The increment must be co-located with the actual state transition (label swap, PR creation, merge) — not in shared outcome-recording paths that fire for all verdicts or results.
-
Map unknown stages to empty string in
session_counter_map. Any stage without a dedicatedSessionCountersfield must map to""so theif counter_field else 0guard produces zero. Never map an unknown stage to another stage's field name. -
New counters follow the same pattern. When adding a stage counter (e.g., for HITL completions), place the
increment_session_countercall at the point where the issue exits that stage successfully, not where the stage records any outcome.
Consequences¶
Positive:
- Dashboard completed_session metrics accurately reflect unique issues that
passed through each stage, enabling reliable throughput measurement.
- Counter cardinality matches issue cardinality: each issue contributes at most
one increment per stage, making counts directly comparable across stages.
- Pattern is simple to follow and audit: find the transition call, confirm the
counter increment is adjacent to it.
Trade-offs:
- Retries and failures are invisible in session counters. Operators who want
attempt-level metrics must use record_review_verdict or the event bus, not
SessionCounters.
- The reviewed counter does not distinguish between normal PR approvals and
ADR-specific review completions (both increment on success). This is
acceptable because both represent a successful exit from the review stage.
- Adding new stages requires updating both SessionCounters and
session_counter_map in lockstep; forgetting the map entry silently shows
zero rather than erroring.
Alternatives considered¶
-
Attempt-counting semantics (increment on every review outcome). Rejected: inflates counts when issues cycle through REQUEST_CHANGES rounds, making
completed_sessionunreliable for throughput measurement. Attempt counts are available through other mechanisms (record_review_verdict). -
Separate attempt and completion counters per stage. Rejected for now: doubles the counter surface area without a clear dashboard consumer. Can be revisited if operators need attempt-level visibility in the dashboard.
-
Strict enum-based mapping (error on unknown stage). Rejected: the silent-zero behavior is safer for forward compatibility. New stages can be added to the orchestrator loop before their counters exist without crashing the dashboard.
Related¶
- Source memory: #1697
- Implementation: PR #1689, issue #1542
src/models.py:SessionCounters— counter model definitionsrc/state.py:StateTracker.increment_session_counter— increment logicsrc/triage_phase.py:_triage_single— triaged counter (forward-progression)src/plan_phase.py:_handle_already_satisfied,src/plan_phase.py:_handle_plan_success— planned counter (forward-progression)src/implement_phase.py:_handle_implementation_result— implemented counter (forward-progression)src/review_phase.py:_record_review_outcome— reviewed counter (guarded on APPROVE)src/post_merge_handler.py:handle_approved— merged counter (forward-progression)src/orchestrator.py:build_pipeline_stats— session_counter_map (stage-to-field mapping)