Repository navigation
fix(OMN-16808): the autoclose sweep must not repeat itself on a ticket it already commented on - #2954
Conversation
|
Warning Review limit reachedNext included review available in 11 minutes. View limit detailsLimit details: You’ve used the included review currently available. Your 131 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (9)
Comment |
|
| Verdict | Meaning | Blocks merge? |
|---|---|---|
passed |
No critical findings | No |
blocked |
CRITICAL findings found | Yes |
degraded |
All models unavailable (infra) | No (pilot) |
Powered by omniintelligence.review_pairing.cli_review — multi-model adversarial review (OMN-8468/OMN-8524)
…t it has already commented on The sweep enumerated OCC companions by a bare `now - lookback_hours` window with no cursor or watermark, kept `seen_tickets` as a per-run local, and let all three gap paths call `create_comment` unconditionally under `apply=True` through a `commentCreate` mutation that never read what was already on the ticket. Under the shipped schedule (`*/30` cron, 2h lookback) one merged companion sits inside four consecutive windows, so a gapped ticket accrues an identical comment per run, forever. Proven, not inferred: OMN-16037 / OMN-16373 / OMN-16757 / OMN-16759 were classified gap in BOTH run 33098307405 and run 33128661860. Dry-run masked it entirely (`linear_comment_posted: false` everywhere), which is why arming `--apply` was held. Fix - read-before-write, fail closed: * Every sweep gap comment ends with a stable marker keyed on (gap class, verdict fingerprint). Markdown hides it in the UI; the raw body is exact-matchable. State lives on the projection (the ticket), not in the job. * All three gap classes now funnel through one `_emit_gap_comment` write path, so the rule cannot hold on two paths and be forgotten on the third - which is exactly how the defect existed. * The key deliberately excludes the companion PR: the same verdict re-derived from a later companion is the same statement, so each (ticket, gap class) carries at most one live comment. A CHANGED verdict (3/6 -> 5/6, GAP_POSTED -> GAP_NO_BEHAVIOR_PROOF) fingerprints differently and IS posted. * An unreadable comment history is ERROR_LINEAR_API and writes nothing. "I could not check" must never resolve to "so I will post". The paginated read fails closed on an API error, an uninterpretable payload, a missing cursor, and a history longer than the page cap. * The dedup read runs in DRY-RUN too. It is a read, so DRY-RUN stays zero-write, and the report becomes an honest preview of what --apply would actually post rather than promising a comment the write path would suppress. * The FLIPPED audit comment is deliberately ungated: it is reached only after `issueUpdate` has moved the ticket to a completed state, which the next run short-circuits at SKIPPED_ALREADY_DONE. The flip path is self-limiting by its own terminal state; the gap paths have none, which is why they need the gate. Also OMN-16792 (in scope, one line): lookback 2h -> 6h. GitHub drops scheduled ticks under load rather than queueing them - 194 of 238 expected runs over 2026-08-22..27 (81%), with createdAt->startedAt 0s median AND 0s max across all 194, so nothing was slow; the missing runs were never created. Observed inter-run gap p100 was 236 min, larger than the 2h lookback, so a companion merged in the uncovered window was scanned by no run, ever. Widening was previously unsafe because more overlap meant more duplicate comments; with the dedup above, re-scanning a companion costs nothing observable. Residual recorded on the workflow and on OMN-16792: `max_companions` is capped at 50 and a 10h window hit it (run 33128661860), so the cap - not the lookback - bounds how wide the window can usefully go. Tests: tests/unit/nodes/node_evidence_autoclose_sweep_effect/test_omn_16808_comment_idempotency.py runs the same fixture window twice against one stateful fake Linear and asserts exactly one comment per (ticket, gap class, verdict), across all three gap classes, plus the changed-verdict, cross-companion, unreadable-history and dry-run directions. The two existing doubles now serve their own write log back through `fetch_comment_bodies` - a fake that forgets cannot see this defect. Refs OMN-16808, OMN-16792, OMN-16106.
be9f9e2 to
96da7b9
Compare
OMN-16808 — the autoclose sweep has no cross-run comment idempotency
Blocks arming OMN-16106 to
--apply. Also lands the in-scope half of OMN-16792.The defect (read from merged
origin/dev@35d97fc51)node_evidence_autoclose_sweep_effectenumerates OCC companions by a barenow - lookback_hourswindow and keeps no cross-run state:handle()computessince_isofrom the clock — no cursor, no watermark, no"already processed" record.
seen_ticketsis a local declared insidehandle(). It suppresses a secondcompanion for the same ticket within one run and is discarded when the run
ends.
_behavior_proof_outcome,_ac_coverage_outcome, theGAP_POSTEDtail of_process_ticket) calledself._linear.create_comment(...)whenever
applywas true, through a barecommentCreatethat never readsexisting comments.
Under the shipped schedule (
cron: '*/30',lookback_hours2) a companionmerged at T sits inside four consecutive windows. Each posts an identical
comment on the bound ticket, forever while the ticket stays gapped.
Proven empirically, not inferred. Two DRY-RUN dispatches ~7h apart, both
lookback_hours=10: OMN-16037, OMN-16373, OMN-16757 and OMN-16759 wereclassified gap in both run
33098307405and run33128661860. Under--applyeach would have taken two identical comments from those two runsalone. Run
33128661860classified 16 tickets as gap in one window. Dry-runmasked the whole thing (
linear_comment_posted: falseeverywhere), which iswhy it had never been observed live.
The fix — read-before-write, fail closed
(gap class, verdict fingerprint). It is an HTML comment, so Linear'smarkdown renderer hides it and the raw body stays exact-matchable. The
sweep's record that it has already made a statement lives on the projection
(the ticket), not in the job.
_emit_gap_comment, so the rule cannot hold on two paths and be forgotten onthe third — which is precisely how the defect existed: three call sites,
three unconditional writes, no shared gate.
re-derived from a later companion is the same statement; keying on the
companion would let every new OCC merge re-open the same gap comment. Each
(ticket, gap class)therefore carries at most one live statement — refreshedwhen the verdict moves, silent when it does not.
GAP_POSTED→GAP_NO_BEHAVIOR_PROOF, fingerprints differently and is posted. ForGAP_AC_COVERAGEthe uncovered criteria are part of the fingerprint.ERROR_LINEAR_API. "I could not check" must never resolve to "so I willpost". The paginated read returns
None(not an empty tuple) on an APIfailure, an uninterpretable payload, a missing cursor, and a history longer
than the page cap.
a read — which also makes the DRY-RUN report an honest preview of what
--applywould post, instead of promising a comment the write path wouldsuppress.
issueUpdatemoved the ticket to a completed state, which the next runshort-circuits at
SKIPPED_ALREADY_DONE. The flip path is self-limiting byits own terminal state; the gap paths have no terminal state, which is why
they need the gate and this does not.
New decision
SKIPPED_DUPLICATE_COMMENT, counted undertickets_skipped— thegap is real and still open, but the run added no information, and counting it
under
tickets_gap_postedwould report comments that were never written.OMN-16792, in scope: lookback 2h → 6h
GitHub drops scheduled ticks under load rather than queueing them. Over
2026-08-22T20:03Z → 2026-08-27T19:05Z: 194 scheduled runs against 238 expected
at
*/30(81%), withcreatedAt → startedAt0s median and 0s max across all194 — nothing was slow, the missing runs were never created. Observed
inter-run gaps: median 30 min, p90 43 min, p100 236 min. A 2h lookback is
smaller than that p100, so a companion merged in the uncovered window was
scanned by no run, ever.
Widening was previously unsafe for exactly the reason this PR fixes: more
overlap meant more duplicate comments. With the dedup above, re-scanning a
companion costs nothing observable, so the window can be sized for the worst
measured gap. That is OMN-16792 option 1; options 2 (event trigger) and 3
(runtime scheduler) stay open on that ticket.
Residual, recorded not fixed:
max_companionsis capped at 50 and a 10hwindow hit that cap (run
33128661860,companions_scanned=50), so the cap —not the lookback — bounds how wide the window can usefully go. Sizing it
belongs to OMN-16792. Its AC3 (kill-switch wiring end to end) is untouched here.
Tests (TDD, red first)
tests/unit/nodes/node_evidence_autoclose_sweep_effect/test_omn_16808_comment_idempotency.py— 10 tests, all red before the fix (8 on the missing enum member / behavior, 2
green by construction):
exactly one comment; the second run records
SKIPPED_DUPLICATE_COMMENT;class=,fingerprint=);marker exists;
The two pre-existing doubles now serve their own write log back through
fetch_comment_bodies. A fake that forgets what it was told cannot see thisdefect — which is why the suite stayed green while production repeated itself.
Local verification
uv run pytest tests/unit/nodes/node_evidence_autoclose_sweep_effect/ -q→ 71 passeduv run mypy src/ --strict→ Success, 2858 source filesuv run ruff format/ruff check --fix→ cleanpre-commit run --all-files→ all hooks pass (the OMN-14350 ratchet reportstwo soft, report-only, non-gate-blocking
_LinearClientcandidates, one ofthem in a file this PR does not touch; it passes on re-run after regenerating
its own baseline)
prepush_smart_tests.sh) ranas-is. No
PREPUSH_FULL_SUITE, noENABLE_SMART_TESTStoggle, no--no-verify, no skip tokens.Refs OMN-16808, OMN-16792, OMN-16106.
Evidence-Ticket: OMN-16808
Evidence-Source: OCC#7360