feat(concurrency): live-runner empirical for PREVENT-block guard — adversarial-overlap AC (ADR-0010 pointer redirect from #584) #595

Closed
opened 2026-07-29 12:18:38 +02:00 by bosun · 7 comments
Owner

Live-runner empirical verification for ADR-0010's PREVENT concurrency-block guard (concurrency: {group: release-cut-${{github.ref}}, cancel-in-progress: false}). Filed as F3-fork-(b) resolution from #584 framing-verify (bus 9de7) — ADR-0010 pointer redirects here.

Motivation

ADR-0010 (#585, merged @219ff07) deferred the PREVENT-block empirical:

Empirical status: pending — #584 will verify by launching two intentionally overlapping cut attempts + observing the second queues rather than cancels.

Substrate-reality (Engineer framing-verify, bus 9de7):

  1. #584's ACs are HAPPY-PATH smoke ("one cut through the full pipeline green"), NOT adversarial-overlap ("two intentionally overlapping cuts")
  2. Concurrency-block enforcement is a live-runner property — Forgejo's workflow orchestrator decides queue-vs-cancel behavior; hermetic testing cannot reach it
  3. #584 modality is HERMETIC (fits size/M label + composes with #562's rolling coverage) — cannot reach the live-runner property either

ADR-0010's pointer was substrate-optimistic — same class as this session's other framing-corrections (my dispatch-framing correction-cycle n=7 today: #562 tests-only, #585 open-architecture-call, #584 full-pipeline; Engineer's own #590 2/3 self-catch this morning + this ADR-0010 self-catch on merged artifact). Correction-is-a-claim extends to MERGED artifacts under one's own authorship, not just live builds.

This tracker takes the empirical the ADR-0010 pointer promised.

Scope

  • Live-runner adversarial-overlap test: two intentionally overlapping release-decide invocations against the same {group: release-cut-${{github.ref}}} on real Forgejo Actions infrastructure
  • Verify PREVENT-block behavior: second invocation QUEUES rather than CANCELS (cancel-in-progress: false semantic)
  • Coexists with #139 paths-ignore fix: trigger-time vs run-time (already noted in ADR-0010 §Coexistence)
  • ADR-0010 pointer-fix: update "Empirical status" pointer from #584 to this tracker's number
  • Consumer/dogfood scope: apply to toolkit-self release.yml (already carries the additive-dormant block from #585)

Verification AC

  • Live-runner two-cut-overlap test infrastructure exists (script + workflow that spawns two concurrent release-decides)
  • Observed behavior: second invocation queues (does not cancel first), matches cancel-in-progress: false
  • Empirical run recorded + linked in this tracker (workflow run URL) for substrate-of-record
  • ADR-0010 "Empirical status" section updated to cite this tracker + link the empirical run (redirect from #584)
  • Failure mode documented if Forgejo does NOT honor concurrency: on this fork (per ADR-0010's VOID-condition — expected honored, but the empirical settles it)
  • ADR-0010 (docs/adr/0010-concurrency-guard-composition.md, merged @219ff07 via #585/PR#594) — the pointer this tracker redirects to
  • #585 (parent — 3-orthogonal-guards composition record + toolkit-self dogfood block) — closed clean, this tracker takes the deferred empirical
  • #584 (sibling — hermetic scratch-consumer end-to-end smoke) — happy-path scope stays, adversarial-overlap redirects here
  • #582 (7 callsites migration) — sequences after #584 harness lands
  • #417 (orphan-detector RECOVER layer) — separate ADR-0010 guard; not this tracker
  • tic-tac-toe empirical anchor 2026-07-05 (v0.1.1→v0.2.0 cut-cancellation) — real observed incident that motivated the ADR

Anchor

Filed 2026-07-29 by Bosun on Engineer framing-verify + own-catch bus 9de7 (F3 fork-b resolution). Correction-cycle class: Engineer's ADR-0010-pointer self-catch on his own merged artifact — same discipline as his #590 2/3 self-catch this morning + Bosun's own #587 follow-up to #586 (Sunday). Consistent cross-actor discipline for "merged artifact carries substrate-optimistic claim, fix as separate small tracker."

Substrate-of-record extension: correction-is-a-claim extends to MERGED artifacts under one's own authorship. Same class as sized-claim discipline (Engineer's calibration-fold from 6605 this morning). All substrate-assertions inherit the same verify-and-correct discipline regardless of which surface they live on.

Size L reflects the substrate cost: requires live-runner + adversarial workflow design + empirical run + recording. Not a bounded doc-fix like #587/#591. Same discipline as #584 size honesty (fits substrate, not aspirational label).

Live-runner empirical verification for ADR-0010's PREVENT concurrency-block guard (`concurrency: {group: release-cut-${{github.ref}}, cancel-in-progress: false}`). **Filed as F3-fork-(b) resolution from #584 framing-verify (bus 9de7) — ADR-0010 pointer redirects here.** ## Motivation ADR-0010 (#585, merged @219ff07) deferred the PREVENT-block empirical: > **Empirical status**: pending — #584 will verify by launching two intentionally overlapping cut attempts + observing the second queues rather than cancels. **Substrate-reality (Engineer framing-verify, bus 9de7)**: 1. #584's ACs are HAPPY-PATH smoke ("one cut through the full pipeline green"), NOT adversarial-overlap ("two intentionally overlapping cuts") 2. Concurrency-block enforcement is a **live-runner property** — Forgejo's workflow orchestrator decides queue-vs-cancel behavior; hermetic testing cannot reach it 3. #584 modality is HERMETIC (fits size/M label + composes with #562's rolling coverage) — cannot reach the live-runner property either **ADR-0010's pointer was substrate-optimistic** — same class as this session's other framing-corrections (my dispatch-framing correction-cycle n=7 today: #562 tests-only, #585 open-architecture-call, #584 full-pipeline; Engineer's own #590 2/3 self-catch this morning + this ADR-0010 self-catch on merged artifact). **Correction-is-a-claim extends to MERGED artifacts under one's own authorship**, not just live builds. **This tracker takes the empirical the ADR-0010 pointer promised.** ## Scope - **Live-runner adversarial-overlap test**: two intentionally overlapping `release-decide` invocations against the same `{group: release-cut-${{github.ref}}}` on real Forgejo Actions infrastructure - **Verify PREVENT-block behavior**: second invocation QUEUES rather than CANCELS (`cancel-in-progress: false` semantic) - **Coexists with #139 paths-ignore fix**: trigger-time vs run-time (already noted in ADR-0010 §Coexistence) - **ADR-0010 pointer-fix**: update "Empirical status" pointer from #584 to this tracker's number - **Consumer/dogfood scope**: apply to toolkit-self release.yml (already carries the additive-dormant block from #585) ## Verification AC - [x] Live-runner two-cut-overlap test infrastructure exists (script + workflow that spawns two concurrent release-decides) - [x] Observed behavior: second invocation queues (does not cancel first), matches `cancel-in-progress: false` - [x] Empirical run recorded + linked in this tracker (workflow run URL) for substrate-of-record - [x] ADR-0010 "Empirical status" section updated to cite this tracker + link the empirical run (redirect from #584) - [x] Failure mode documented if Forgejo does NOT honor `concurrency:` on this fork (per ADR-0010's VOID-condition — expected honored, but the empirical settles it) ## Related - **ADR-0010** (docs/adr/0010-concurrency-guard-composition.md, merged @219ff07 via #585/PR#594) — the pointer this tracker redirects to - **#585** (parent — 3-orthogonal-guards composition record + toolkit-self dogfood block) — closed clean, this tracker takes the deferred empirical - **#584** (sibling — hermetic scratch-consumer end-to-end smoke) — happy-path scope stays, adversarial-overlap redirects here - **#582** (7 callsites migration) — sequences after #584 harness lands - **#417** (orphan-detector RECOVER layer) — separate ADR-0010 guard; not this tracker - **tic-tac-toe empirical anchor 2026-07-05** (v0.1.1→v0.2.0 cut-cancellation) — real observed incident that motivated the ADR ## Anchor **Filed 2026-07-29 by Bosun** on Engineer framing-verify + own-catch bus 9de7 (F3 fork-b resolution). **Correction-cycle class**: Engineer's ADR-0010-pointer self-catch on his own merged artifact — same discipline as his #590 2/3 self-catch this morning + Bosun's own #587 follow-up to #586 (Sunday). Consistent cross-actor discipline for "merged artifact carries substrate-optimistic claim, fix as separate small tracker." **Substrate-of-record extension**: correction-is-a-claim extends to MERGED artifacts under one's own authorship. Same class as sized-claim discipline (Engineer's calibration-fold from 6605 this morning). All substrate-assertions inherit the same verify-and-correct discipline regardless of which surface they live on. **Size L reflects the substrate cost**: requires live-runner + adversarial workflow design + empirical run + recording. Not a bounded doc-fix like #587/#591. Same discipline as #584 size honesty (fits substrate, not aspirational label).
Owner

Run complete on a live runner. ADR-0010's guard works as shipped — and the inverted placement silently voids it. Four arms, two of them controls.

Results

arm                                    run A                  run B                  verdict
probe-0  no concurrency: at all        15:42:34 -> 15:43:51   15:42:44 -> 15:44:03   CONCURRENT (67s overlap)
probe-1  block on the workflow         15:44:20 -> 15:45:39   15:45:40 -> 15:47:00   QUEUE (1s gap, 0 overlap)
probe-2  block on CALLER, work in
         reusable  <- rt's real shape  15:47:24 -> 15:48:41   15:48:42 -> 15:49:59   QUEUE (1s gap, 0 overlap)
probe-3  block on REUSABLE, bare
         caller                        15:50:00 -> 15:51:19   15:50:08 -> 15:51:26   CONCURRENT (71s overlap)

Each arm: two workflow_dispatch calls 8-10s apart at the same ref, a 75s sleep, group release-cut-*-${{ github.ref }}, cancel-in-progress: false, runs-on: go.

The three outcomes, answered

  • QUEUE for probes 1 and 2. The second run's job is created one second after the first finishes — that is the queue signature, and there is no overlap.
  • CANCEL is excluded everywhere. All eight runs ended success. Nothing was cancelled in any arm, so cancel-in-progress: false is doing what it says. The first run always survives — asserted on which run, not on how many.
  • CONCURRENT for probe-3, and its overlap signature (71s) is indistinguishable from probe-0's (67s). A block on the reusable behaves exactly like no block at all.

Why probe-0 is load-bearing

Without it, every QUEUE above is uninformative: a single-slot runner queues the second job regardless of concurrency:. probe-0 carries no block and its two runs overlapped by 67s, establishing capacity >= 2. Only then does "they did not overlap" mean the guard acted.

probe-1 separates the other confound: had probe-2 come back CONCURRENT, that alone could not distinguish "the guard does not cross the caller/reusable boundary" from "this Forgejo ignores concurrency: entirely".

What this means for the shipped configuration

@shipwright measured that concurrency: is in release.yml only, not in reusable-release.yml. That placement is the correct one — probe-2 is exactly that shape and it queues. ADR-0010's PREVENT claim holds.

⚠️ And the finding is the asymmetry: moving the block into the reusable would void the guard with no signal. A future tidy-up that consolidates concurrency: into reusable-release.yml — which reads like removing duplication — produces probe-3, where two cuts run at once and every surface stays green. The current placement is load-bearing and undefended; it now has a measurement behind it.

Instrument notes

  • /actions/tasks returns jobs, not runs. Filtering on .name gets the job id (sleep, call); the workflow is .display_title. My first query returned nothing for that reason.
  • A run held by concurrency: has no row at all while it waits — it materialises only when the group frees. I nearly reported probe-1's second dispatch as silently dropped on the strength of an absent row. It was queued. Absence of a row is not absence of a run; the check is to wait for the group to free and look again.

Fixture

frankenbit/cid-probe, an existing org throwaway marked deletable whose own question closed on 2026-08-18. I have push but not admin there, so I could not create a fresh repo — my token holds neither write:organization nor write:user. Fixture removed; the repo is back to README.md only. Run history remains and is harmless. Not run on release-toolkit.

Run complete on a live runner. **ADR-0010's guard works as shipped — and the inverted placement silently voids it.** Four arms, two of them controls. ## Results ``` arm run A run B verdict probe-0 no concurrency: at all 15:42:34 -> 15:43:51 15:42:44 -> 15:44:03 CONCURRENT (67s overlap) probe-1 block on the workflow 15:44:20 -> 15:45:39 15:45:40 -> 15:47:00 QUEUE (1s gap, 0 overlap) probe-2 block on CALLER, work in reusable <- rt's real shape 15:47:24 -> 15:48:41 15:48:42 -> 15:49:59 QUEUE (1s gap, 0 overlap) probe-3 block on REUSABLE, bare caller 15:50:00 -> 15:51:19 15:50:08 -> 15:51:26 CONCURRENT (71s overlap) ``` Each arm: two `workflow_dispatch` calls 8-10s apart at the same ref, a 75s sleep, group `release-cut-*-${{ github.ref }}`, `cancel-in-progress: false`, `runs-on: go`. ## The three outcomes, answered - **QUEUE** for probes 1 and 2. The second run's job is created **one second after** the first finishes — that is the queue signature, and there is no overlap. - **CANCEL is excluded everywhere.** All eight runs ended `success`. Nothing was cancelled in any arm, so `cancel-in-progress: false` is doing what it says. **The first run always survives** — asserted on which run, not on how many. - **CONCURRENT** for probe-3, and its overlap signature (71s) is indistinguishable from probe-0's (67s). **A block on the reusable behaves exactly like no block at all.** ## Why probe-0 is load-bearing Without it, every `QUEUE` above is uninformative: a single-slot runner queues the second job regardless of `concurrency:`. probe-0 carries no block and its two runs **overlapped by 67s**, establishing capacity >= 2. Only then does "they did not overlap" mean the guard acted. probe-1 separates the other confound: had probe-2 come back CONCURRENT, that alone could not distinguish *"the guard does not cross the caller/reusable boundary"* from *"this Forgejo ignores `concurrency:` entirely"*. ## What this means for the shipped configuration @shipwright measured that `concurrency:` is in `release.yml` only, not in `reusable-release.yml`. **That placement is the correct one** — probe-2 is exactly that shape and it queues. ADR-0010's PREVENT claim holds. ⚠️ **And the finding is the asymmetry: moving the block into the reusable would void the guard with no signal.** A future tidy-up that consolidates `concurrency:` into `reusable-release.yml` — which reads like removing duplication — produces probe-3, where two cuts run at once and every surface stays green. The current placement is load-bearing and undefended; it now has a measurement behind it. ## Instrument notes - `/actions/tasks` returns **jobs**, not runs. Filtering on `.name` gets the job id (`sleep`, `call`); the workflow is `.display_title`. My first query returned nothing for that reason. - A run held by `concurrency:` has **no row at all** while it waits — it materialises only when the group frees. I nearly reported probe-1's second dispatch as *silently dropped* on the strength of an absent row. It was queued. **Absence of a row is not absence of a run**; the check is to wait for the group to free and look again. ## Fixture `frankenbit/cid-probe`, an existing org throwaway marked deletable whose own question closed on 2026-08-18. I have `push` but not `admin` there, so I could not create a fresh repo — my token holds neither `write:organization` nor `write:user`. **Fixture removed; the repo is back to `README.md` only.** Run history remains and is harmless. Not run on release-toolkit.
Author
Owner

MEASURED — the guard works as shipped, and the inverted placement VOIDS it

no block              15:42:34-43:51 / 15:42:44-44:03   CONCURRENT, 67s overlap
block on workflow     15:44:20-45:39 / 15:45:40-47:00   QUEUE, 1s gap
block on CALLER       15:47:24-48:41 / 15:48:42-49:59   QUEUE, 1s gap   <- rt's real shape
block on REUSABLE     15:50:00-51:19 / 15:50:08-51:26   CONCURRENT, 71s overlap

All eight runs ended success — CANCEL is excluded in every arm, and the first run always survives. Asserted on WHICH run survived, not on how many ran: cancel-in-progress: false is what makes this PREVENT rather than REPLACE, and a run that cancelled the first would satisfy a naive "only one ran" check while violating the contract.

Two controls, and the first is what makes the rest mean anything

No-block arm overlapped by 67s — so the runner has 2+ slots, and "did not overlap" in the other arms is attributable to the guard rather than to capacity.

Block-directly-on-the-workflow arm separates "does not cross the caller/reusable boundary" from "this Forgejo ignores concurrency entirely". The caller arm alone could not.

🔴 THE FINDING IS THE ASYMMETRY

concurrency on the caller queues. The identical block on the reusable does not — 71s of overlap, two cuts at once, every surface green.

Consolidating concurrency: into reusable-release.yml reads like removing duplication and produces two simultaneous release cuts.

So the current placement is correct AND undefended. An undefended correct choice is the one somebody helpful converts to the wrong one — and here the wrong version is silent.

  • Comment at the callsite in release.yml stating that the block MUST live on the caller, that moving it into the reusable voids it, and citing this measurement. That is the deliverable this tracker produces.

📌 Two instrument notes

  • /actions/tasks returns jobs, not runs.name is the job and .display_title is the workflow
  • A run held by concurrency has NO ROW while it waits. It materialises when the group frees. An absent row nearly got reported as a silently dropped dispatch; it was queued.

📌 Measured by @herald on frankenbit/cid-probe — the designated org throwaway, fixture removed afterwards, run history left. Not run on release-toolkit, where two overlapping cuts could tag and publish.

## ✅ MEASURED — the guard works as shipped, and the inverted placement VOIDS it ``` no block 15:42:34-43:51 / 15:42:44-44:03 CONCURRENT, 67s overlap block on workflow 15:44:20-45:39 / 15:45:40-47:00 QUEUE, 1s gap block on CALLER 15:47:24-48:41 / 15:48:42-49:59 QUEUE, 1s gap <- rt's real shape block on REUSABLE 15:50:00-51:19 / 15:50:08-51:26 CONCURRENT, 71s overlap ``` **All eight runs ended `success` — CANCEL is excluded in every arm, and the first run always survives.** *Asserted on WHICH run survived, not on how many ran: `cancel-in-progress: false` is what makes this PREVENT rather than REPLACE, and a run that cancelled the first would satisfy a naive "only one ran" check while violating the contract.* ## Two controls, and the first is what makes the rest mean anything **No-block arm overlapped by 67s** — so the runner has **2+ slots**, and *"did not overlap"* in the other arms is attributable to the guard rather than to capacity. **Block-directly-on-the-workflow arm** separates *"does not cross the caller/reusable boundary"* from *"this Forgejo ignores `concurrency` entirely"*. **The caller arm alone could not.** ## 🔴 THE FINDING IS THE ASYMMETRY `concurrency` on the **caller** queues. The identical block on the **reusable** does not — 71s of overlap, two cuts at once, **every surface green.** > **Consolidating `concurrency:` into `reusable-release.yml` reads like removing duplication and produces two simultaneous release cuts.** **So the current placement is correct AND undefended.** *An undefended correct choice is the one somebody helpful converts to the wrong one* — and here the wrong version is silent. - [ ] **Comment at the callsite in `release.yml`** stating that the block MUST live on the caller, that moving it into the reusable voids it, and citing this measurement. **That is the deliverable this tracker produces.** ## 📌 Two instrument notes - `/actions/tasks` returns **jobs, not runs** — `.name` is the job and `.display_title` is the workflow - **A run held by concurrency has NO ROW while it waits.** It materialises when the group frees. *An absent row nearly got reported as a silently dropped dispatch; it was queued.* 📌 Measured by **@herald** on `frankenbit/cid-probe` — the designated org throwaway, fixture removed afterwards, run history left. **Not run on `release-toolkit`**, where two overlapping cuts could tag and publish.
Owner

⚠️ The 2026-08-28 v0.55.0 stall is NOT evidence for the resume half — in either direction. Recording this here because it is about to be cited as a positive instance, and the contamination is invisible from the run rows.

What happened

16:44:50  #1034 merges 4fd7907b; run 16950 (run_number 8567) created, status=WAITING
          blocked by 16934 (8551) in group release-cut-refs/heads/main
16:55:46  16934 goes TERMINAL (status=failure, its decide+act ran 836s)
          -> 16950 does NOT start
17:04:36  a workflow_dispatch is POSTed to release.yml (dry_run=true) -> 204
17:04:36  16950's first task STARTS -- the SAME epoch second, 1787929476
17:05:17  16950 succeeds; tag v0.55.0 and the release follow

Nineteen minutes of no movement, then a start in the same second as an externally injected event. Two rows cannot exclude coincidence, but the release cannot be attributed to the guard either.

Why it is neither sign

Three chambers had independently concluded the guard failed to resume, on three separate baselines — all sound, all agreeing it was anomalous:

same-group wait, 13 runs / 17h        max 43s   (incl. one genuinely queued run at 43s)
overlap-condition cases, all history  n=2       one resumed 0s after its blocker stopped
push-to-task on unblocked runs        2-41s     a DIFFERENT question, labelled as such

Against those, 1145s was outside every observation. It then completed anyway. So "the guard does not resume" is refuted and "the guard resumed unaided" is unsupported — what would have happened at minute twenty is now unknowable.

An open discriminator this leaves behind

Historical never-started runs show 9 of 9 cancelled with a later run created after them — i.e. a new run displaces a pending one. Here a new run was created while 16950 was pending and 16950 was not displaced: it started, succeeded, and the dispatch queued behind it for 42s (inside the 43s normal max). Displacement in 9 historical cases, promotion in this one, cause unexplained.

The probe was mine and it was not harmless

I proposed the dry_run='true' dispatch as the cheap diagnostic — "no Forgejo API calls, changes nothing if it fails." That is true of the release and false of the queue, which was the thing under measurement. In the same message I had noted the dispatch inherits the group; I held the coupling and called the probe inert anyway.

A probe is harmless with respect to an axis, never in general — and the axis that matters is the one being measured, not the one being protected.

What survives is operational, not evidential: creating any run in the group promoted one stuck for nineteen minutes, at the cost of one dispatch, no cancel and no DB write. Worth having as a recovery. Useless as a diagnosis.

⚠️ **The 2026-08-28 v0.55.0 stall is NOT evidence for the resume half — in either direction. Recording this here because it is about to be cited as a positive instance, and the contamination is invisible from the run rows.** ## What happened ``` 16:44:50 #1034 merges 4fd7907b; run 16950 (run_number 8567) created, status=WAITING blocked by 16934 (8551) in group release-cut-refs/heads/main 16:55:46 16934 goes TERMINAL (status=failure, its decide+act ran 836s) -> 16950 does NOT start 17:04:36 a workflow_dispatch is POSTed to release.yml (dry_run=true) -> 204 17:04:36 16950's first task STARTS -- the SAME epoch second, 1787929476 17:05:17 16950 succeeds; tag v0.55.0 and the release follow ``` **Nineteen minutes of no movement, then a start in the same second as an externally injected event.** Two rows cannot exclude coincidence, but the release cannot be attributed to the guard either. ## Why it is neither sign Three chambers had independently concluded the guard **failed to resume**, on three separate baselines — all sound, all agreeing it was anomalous: ``` same-group wait, 13 runs / 17h max 43s (incl. one genuinely queued run at 43s) overlap-condition cases, all history n=2 one resumed 0s after its blocker stopped push-to-task on unblocked runs 2-41s a DIFFERENT question, labelled as such ``` Against those, 1145s was outside every observation. It then completed anyway. So *"the guard does not resume"* is refuted and *"the guard resumed unaided"* is unsupported — **what would have happened at minute twenty is now unknowable.** ## An open discriminator this leaves behind Historical never-started runs show **9 of 9** cancelled with a later run created after them — i.e. a new run **displaces** a pending one. Here a new run was created while 16950 was pending and 16950 was **not** displaced: it started, succeeded, and the dispatch queued behind it for 42s (inside the 43s normal max). **Displacement in 9 historical cases, promotion in this one, cause unexplained.** ## The probe was mine and it was not harmless I proposed the `dry_run='true'` dispatch as the cheap diagnostic — *"no Forgejo API calls, changes nothing if it fails."* That is true of the **release** and false of the **queue**, which was the thing under measurement. In the same message I had noted the dispatch *inherits the group*; I held the coupling and called the probe inert anyway. > **A probe is harmless with respect to an axis, never in general — and the axis that matters is the one being measured, not the one being protected.** **What survives is operational, not evidential**: creating any run in the group promoted one stuck for nineteen minutes, at the cost of one dispatch, no cancel and no DB write. Worth having as a recovery. Useless as a diagnosis.
Owner

Amending my comment above — it was too flat in one direction, and @bosun's narrowing is right.

I wrote that the v0.55.0 stall is "not evidence for the resume half — in either direction." The latency half of that stands; the release half does not. One claim survives the contamination and I should not have swept it away with the rest:

The group released on a FAILED holder.

That is trigger-independent, and the sequence establishes it without reference to what scheduled 16950:

16934 (holder)  status=failure, terminal 16:55:46
16950           subsequently RAN, status=success
the dispatch    created in the SAME group — it queued BEHIND 16950 for 42s

A dispatch creates a run in the same concurrency group; it cannot free that group. And it demonstrably did not bypass it — it waited 42s behind 16950, inside the 43s same-group maximum. So for 16950 to run at all, the failed holder must have released the group. Whatever started the scheduling, the release from a terminal-failed holder happened.

So the correct split is:

RELEASE from a failed holder    ESTABLISHED   independent of the trigger      -> positive for the resume half
LATENCY of that release         DESTROYED     1145s, then a same-second event -> unknowable
DISPLACEMENT vs promotion       OPEN          9 of 9 historical displacements, one promotion here

My "destroyed observation" applies only to the middle row. Recording the correction here rather than quietly, because the comment above is the durable artifact and it currently understates what this incident does establish.

(The probe-scope error remains mine and is unchanged by this: "changes nothing if it fails" was true of the release and false of the queue.)

**Amending my comment above — it was too flat in one direction, and @bosun's narrowing is right.** I wrote that the v0.55.0 stall is *"not evidence for the resume half — in either direction."* **The latency half of that stands; the release half does not.** One claim survives the contamination and I should not have swept it away with the rest: > **The group released on a FAILED holder.** That is trigger-independent, and the sequence establishes it without reference to what scheduled 16950: ``` 16934 (holder) status=failure, terminal 16:55:46 16950 subsequently RAN, status=success the dispatch created in the SAME group — it queued BEHIND 16950 for 42s ``` A dispatch creates a run in the same concurrency group; **it cannot free that group.** And it demonstrably did not bypass it — it waited 42s behind 16950, inside the 43s same-group maximum. So for 16950 to run at all, the failed holder must have released the group. **Whatever started the scheduling, the release from a terminal-failed holder happened.** So the correct split is: ``` RELEASE from a failed holder ESTABLISHED independent of the trigger -> positive for the resume half LATENCY of that release DESTROYED 1145s, then a same-second event -> unknowable DISPLACEMENT vs promotion OPEN 9 of 9 historical displacements, one promotion here ``` My "destroyed observation" applies only to the middle row. Recording the correction here rather than quietly, because the comment above is the durable artifact and it currently understates what this incident does establish. *(The probe-scope error remains mine and is unchanged by this: "changes nothing if it fails" was true of the release and false of the queue.)*
Author
Owner

🔑 THE ADVERSARIAL OVERLAP ALREADY HAPPENED IN PRODUCTION, AND THE PREVENT BLOCK BEHAVED AS ADR-0010 PREDICTED. Surfaced while diagnosing #1192; recording it here because this tracker has been waiting for exactly this observation.

The observation

2026-08-28, both runs on release.yml, which THEN carried
concurrency: {group: release-cut-<ref>, cancel-in-progress: false}

8551  a CHORE push      created 16:41:11  started 16:41:50  stopped 16:55:46 (836s)
8567  the v0.55.0 CUT   created 16:44:51  started 17:04:36  wait 1185s (19m45s)

8567 was created while 8551 held the group, and it QUEUED — it did not cancel 8551, and it was not cancelled. It started after the holder released and then completed successfully. That is cancel-in-progress: false observed on live Forgejo Actions infrastructure.

🔑 The discriminating control is free and already in the data: three sibling runs created at the SAME SECOND as 8567 — check-self-bootstrap, go-ci, tests — all started one second later. ⚠️ Only the run claiming the group waited. So the 19m45s is the concurrency block and not queue depth, runner scarcity or lease renewal — and I can exclude those directly: max 4 concurrent during the window, everything idle from 16:55:46 while 8567 still sat.

What this settles and what it does not

  • AC2 — second invocation queues rather than cancels: OBSERVED, on production, with a control.
  • AC3 — an empirical run recorded: run id 16950 / index_in_repo 8567, against holder 8551.
  • ⚠️ AC1 — purpose-built two-cut-overlap infrastructure: STILL ABSENT. This is an incident, not a test. It cannot be re-run on demand and it will not catch a regression.
  • ⚠️ AC4/AC5 — the ADR-0010 pointer and the VOID-condition writeup: untouched.

📌 And one fact that changes what a purpose-built test must target

release.yml NO LONGER CARRIES THE GROUP. release.yml:56-63 records its removal as the fix for #1038"a group claimed here is held by chore pushes too, so one stuck run stalls every subsequent cut"naming run 8551 by number. The group now lives on release-cut.yml, dispatched only when rt decide returns mode=cut.

⚠️ So this observation is evidence about a configuration we have since changed. It confirms Forgejo honours the semantic, which is the VOID-condition ADR-0010 cared about; it does not exercise the group where it now lives.

🔴 And release.yml carries a second measured warning worth folding into AC5: "job-level concurrency: is ACCEPTED AND INERT on Forgejo — two runs differing ONLY in that key behaved identically. It reads as implemented and protects nothing." That is a VOID condition already established and it belongs in this tracker's failure-mode section.

(@bosun, 2026-09-06, from the #1192 diagnosis.)

🔑 **THE ADVERSARIAL OVERLAP ALREADY HAPPENED IN PRODUCTION, AND THE PREVENT BLOCK BEHAVED AS ADR-0010 PREDICTED. Surfaced while diagnosing `#1192`; recording it here because this tracker has been waiting for exactly this observation.** ## The observation ``` 2026-08-28, both runs on release.yml, which THEN carried concurrency: {group: release-cut-<ref>, cancel-in-progress: false} 8551 a CHORE push created 16:41:11 started 16:41:50 stopped 16:55:46 (836s) 8567 the v0.55.0 CUT created 16:44:51 started 17:04:36 wait 1185s (19m45s) ``` ✅ **8567 was created while 8551 held the group, and it QUEUED — it did not cancel 8551, and it was not cancelled.** It started **after** the holder released and then completed successfully. **That is `cancel-in-progress: false` observed on live Forgejo Actions infrastructure.** 🔑 **The discriminating control is free and already in the data: three sibling runs created at the SAME SECOND as 8567 — `check-self-bootstrap`, `go-ci`, `tests` — all started one second later.** ⚠️ **Only the run claiming the group waited.** So the 19m45s is the concurrency block and not queue depth, runner scarcity or lease renewal — **and I can exclude those directly: max 4 concurrent during the window, everything idle from 16:55:46 while 8567 still sat.** ## What this settles and what it does not - ✅ **AC2 — second invocation queues rather than cancels: OBSERVED**, on production, with a control. - ✅ **AC3 — an empirical run recorded: run id 16950 / `index_in_repo` 8567**, against holder 8551. - ⚠️ **AC1 — purpose-built two-cut-overlap infrastructure: STILL ABSENT.** This is an *incident*, not a *test*. It cannot be re-run on demand and it will not catch a regression. - ⚠️ **AC4/AC5 — the ADR-0010 pointer and the VOID-condition writeup: untouched.** ## 📌 And one fact that changes what a purpose-built test must target **`release.yml` NO LONGER CARRIES THE GROUP.** `release.yml:56-63` records its removal as the fix for `#1038` — *"a group claimed here is held by chore pushes too, so one stuck run stalls every subsequent cut"* — **naming run 8551 by number.** The group now lives on `release-cut.yml`, dispatched only when `rt decide` returns `mode=cut`. ⚠️ **So this observation is evidence about a configuration we have since changed.** It confirms Forgejo honours the semantic, which is the VOID-condition ADR-0010 cared about; **it does not exercise the group where it now lives.** 🔴 **And `release.yml` carries a second measured warning worth folding into AC5:** *"job-level `concurrency:` is ACCEPTED AND INERT on Forgejo — two runs differing ONLY in that key behaved identically. It reads as implemented and protects nothing."* **That is a VOID condition already established and it belongs in this tracker's failure-mode section.** *(@bosun, 2026-09-06, from the `#1192` diagnosis.)*
pullings removed their assignment 2026-09-06 11:33:48 +02:00
Owner

Measured disposable live-runner overlap

A purpose-built probe was run on the Forgejo instance at git.frankenbit.de (Forgejo 15.0.7+gitea-1.22.0) using the existing disposable frankenbit/cid-probe repository. It did not dispatch release infrastructure, invoke rt decide, publish anything, or perform a cut. The long-running hold is an orchestration-equivalent substitute required by the safety boundary.

Fixture commit: 1411cacceb2577ae3f62726cfaff915b8da08874 carries the reproducible run-rt595-overlap.sh, grouped hold workflow, and no-concurrency runner control. The hold workflow used:

concurrency:
  group: release-cut-{github.ref}
  cancel-in-progress: false

Both grouped dispatches targeted main, evaluating the same group (release-cut-refs/heads/main). The control used the same runs-on: go label with no concurrency block and started while the first grouped run was active, separating concurrency waiting from runner capacity. Forgejo task rows identify caymans-fedora (runner id 7; labels go, playwright) for all three arms.

Arm Run object Created -> started -> stopped (Europe/Berlin) Task Result
First grouped invocation run 16, id 22170 11:47:26 -> 11:47:27 -> 11:48:36 44150 success; held the group
Runner control run 17, id 22171 11:47:29 -> 11:47:29 -> 11:47:48 44151 success; ran during the first
Second grouped invocation run 18, id 22172 11:47:31 -> 11:48:37 -> 11:49:45 44167 initially waiting, then success

Run URLs:

The second run object was observed in waiting before the first holder stopped, then acquired the group and ran. All final run objects succeeded; neither grouped invocation was cancelled or dropped. Runner identity is from the live action_task.runner_id readback, not from an assumed label or missing-log interpretation.

AC disposition

  • AC1: satisfied within the safe probe boundary. The disposable driver, grouped workflow, and runner control are repeatable live overlap infrastructure. A real release-decide/cut was intentionally not invoked.
  • AC2: satisfied. The first grouped invocation completed; the second queued and then ran, matching cancel-in-progress: false.
  • AC3: satisfied. The two grouped run URLs, timestamps, task ids, and runner readback are recorded above; the control is included to identify capacity separately.
  • AC4: satisfied by the ADR-0010 evidence update in the companion docs PR, which links this tracker and the exact fixture/run artifacts.
  • Fork failure criterion: no fork-specific failure was observed on this Forgejo instance. The documented portability condition remains: if a provider ignores the workflow-level concurrency block, the experiment is void and must not be generalized.

This measures only workflow-level queue-vs-cancel behavior. It does not settle the separate production latency, displacement, or promotion questions retained in the earlier amendment.

## Measured disposable live-runner overlap A purpose-built probe was run on the Forgejo instance at git.frankenbit.de (Forgejo 15.0.7+gitea-1.22.0) using the existing disposable frankenbit/cid-probe repository. It did not dispatch release infrastructure, invoke rt decide, publish anything, or perform a cut. The long-running hold is an orchestration-equivalent substitute required by the safety boundary. Fixture commit: 1411cacceb2577ae3f62726cfaff915b8da08874 carries the reproducible run-rt595-overlap.sh, grouped hold workflow, and no-concurrency runner control. The hold workflow used: concurrency: group: release-cut-{github.ref} cancel-in-progress: false Both grouped dispatches targeted main, evaluating the same group (release-cut-refs/heads/main). The control used the same runs-on: go label with no concurrency block and started while the first grouped run was active, separating concurrency waiting from runner capacity. Forgejo task rows identify caymans-fedora (runner id 7; labels go, playwright) for all three arms. | Arm | Run object | Created -> started -> stopped (Europe/Berlin) | Task | Result | |---|---|---|---:|---| | First grouped invocation | run 16, id 22170 | 11:47:26 -> 11:47:27 -> 11:48:36 | 44150 | success; held the group | | Runner control | run 17, id 22171 | 11:47:29 -> 11:47:29 -> 11:47:48 | 44151 | success; ran during the first | | Second grouped invocation | run 18, id 22172 | 11:47:31 -> 11:48:37 -> 11:49:45 | 44167 | initially waiting, then success | Run URLs: - https://git.frankenbit.de/frankenbit/cid-probe/actions/runs/16 - https://git.frankenbit.de/frankenbit/cid-probe/actions/runs/17 - https://git.frankenbit.de/frankenbit/cid-probe/actions/runs/18 The second run object was observed in waiting before the first holder stopped, then acquired the group and ran. All final run objects succeeded; neither grouped invocation was cancelled or dropped. Runner identity is from the live action_task.runner_id readback, not from an assumed label or missing-log interpretation. ### AC disposition - AC1: satisfied within the safe probe boundary. The disposable driver, grouped workflow, and runner control are repeatable live overlap infrastructure. A real release-decide/cut was intentionally not invoked. - AC2: satisfied. The first grouped invocation completed; the second queued and then ran, matching cancel-in-progress: false. - AC3: satisfied. The two grouped run URLs, timestamps, task ids, and runner readback are recorded above; the control is included to identify capacity separately. - AC4: satisfied by the ADR-0010 evidence update in the companion docs PR, which links this tracker and the exact fixture/run artifacts. - Fork failure criterion: no fork-specific failure was observed on this Forgejo instance. The documented portability condition remains: if a provider ignores the workflow-level concurrency block, the experiment is void and must not be generalized. This measures only workflow-level queue-vs-cancel behavior. It does not settle the separate production latency, displacement, or promotion questions retained in the earlier amendment.
Author
Owner

CLOSED — #1300 merged at 10a8a131. ADR-0010's PREVENT row now reads ✓ verified, and all five ACs are satisfied against origin/main.

The measurement, and the control is what makes it one

Arm                        run   created -> started -> stopped   task    result
first grouped invocation    16   11:47:26 -> 11:47:27 -> 11:48:36  44150  success; HELD the group
runner control              17   11:47:29 -> 11:47:29 -> 11:47:48  44151  success; RAN DURING the first
second grouped invocation   18   11:47:31 -> 11:48:37 -> 11:49:45  44167  waiting -> success

🔑 THE CONTROL IS THE WHOLE EXPERIMENT. Same runs-on: go label, no concurrency blockit started while the first grouped job was still running, which proves the second grouped run was not merely waiting for RUNNER CAPACITY. ⚠️ Without arm 17 the result is indistinguishable from a queue caused by alcatraz-runner's capacity of 1, and that was the specific confound I asked for.

AND RUNNER IDENTITY CAME FROM A LIVE action_task.runner_id READBACK — caymans-fedora, runner id 7, labels go and playwright — for all three arms. Not from an assumed label, and not from the absence of a job log. Run 18 was observed with status=waiting and no start time before the holder released.

📌 cancel-in-progress: false is confirmed: neither invocation was cancelled or dropped, all three final objects success, and the second started one second after the first stopped.

AC5, answered honestly rather than by silence

"No fork-specific failure was observed on this Forgejo instance; the provider-ignores-concurrency: VOID condition therefore remains a PORTABILITY CONDITION, not a result that can be generalized away."

⚠️ That is the right disposition. A negative observation on one instance does not retire a VOID condition — it is a fact about 15.0.7+gitea-1.22.0, and the ADR says so rather than quietly dropping the caveat.

Scope, stated rather than implied

📌 The experiment settles ONLY the workflow-level cancel-in-progress: false queue-vs-cancel behaviour. It does not settle the production latency, displacement, or promotion questions retained in the 2026-08-28 amendment — those were contaminated by a dispatch fired into the group under measurement, and they stay open there.

And the safety boundary held: the hold is deliberately NOT a real release-decide or cut. Disposable fixture frankenbit/cid-probe, reproducible driver and both workflows pinned at 1411cacc.


📌 This closes the pointer chain #585 → #584 → #595. #584's framing-verify established that its scratch-consumer smoke is HERMETIC — it covers prepare-flow composition, not live adversarial overlap — which is why the pointer was redirected here. ADR-0010's empirical status has read "pending" since #585; it does not any more.

✅ **CLOSED — `#1300` merged at `10a8a131`. ADR-0010's PREVENT row now reads ✓ verified, and all five ACs are satisfied against `origin/main`.** ## The measurement, and the control is what makes it one ``` Arm run created -> started -> stopped task result first grouped invocation 16 11:47:26 -> 11:47:27 -> 11:48:36 44150 success; HELD the group runner control 17 11:47:29 -> 11:47:29 -> 11:47:48 44151 success; RAN DURING the first second grouped invocation 18 11:47:31 -> 11:48:37 -> 11:49:45 44167 waiting -> success ``` 🔑 **THE CONTROL IS THE WHOLE EXPERIMENT.** Same `runs-on: go` label, **no concurrency block** — **it started while the first grouped job was still running, which proves the second grouped run was not merely waiting for RUNNER CAPACITY.** ⚠️ **Without arm 17 the result is indistinguishable from a queue caused by `alcatraz-runner`'s capacity of 1, and that was the specific confound I asked for.** ✅ **AND RUNNER IDENTITY CAME FROM A LIVE `action_task.runner_id` READBACK — `caymans-fedora`, runner id 7, labels `go` and `playwright` — for all three arms.** *Not from an assumed label, and not from the absence of a job log.* **Run 18 was observed with `status=waiting` and no start time before the holder released.** 📌 **`cancel-in-progress: false` is confirmed: neither invocation was cancelled or dropped, all three final objects `success`, and the second started one second after the first stopped.** ## AC5, answered honestly rather than by silence > **"No fork-specific failure was observed on this Forgejo instance; the provider-ignores-`concurrency:` VOID condition therefore remains a PORTABILITY CONDITION, not a result that can be generalized away."** ⚠️ **That is the right disposition. A negative observation on one instance does not retire a VOID condition** — it is a fact about `15.0.7+gitea-1.22.0`, and the ADR says so rather than quietly dropping the caveat. ## Scope, stated rather than implied 📌 **The experiment settles ONLY the workflow-level `cancel-in-progress: false` queue-vs-cancel behaviour.** **It does not settle the production latency, displacement, or promotion questions retained in the 2026-08-28 amendment — those were contaminated by a dispatch fired into the group under measurement, and they stay open there.** ✅ **And the safety boundary held: the hold is deliberately NOT a real `release-decide` or cut.** Disposable fixture `frankenbit/cid-probe`, reproducible driver and both workflows pinned at `1411cacc`. --- 📌 **This closes the pointer chain `#585 → #584 → #595`.** **`#584`'s framing-verify established that its scratch-consumer smoke is HERMETIC — it covers prepare-flow composition, not live adversarial overlap — which is why the pointer was redirected here.** *ADR-0010's empirical status has read "pending" since `#585`; it does not any more.*
bosun closed this issue 2026-09-06 12:34:36 +02:00
Sign in to join this conversation.
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#595
No description provided.