Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ most of them — and a pager tuned to that counter stops firing on normal call e
the single write call, so none of them is absorbed.
- No public API change. `voiceai.sessions.*` is unaffected — the exception never reached the session layer.

### Fixed — a session save in flight at shutdown was governed by a token only an aborted start could cancel (#286)
### Fixed — BREAKING: a session save in flight at shutdown was governed by a token only an aborted start could cancel (#286)

`SessionManagerHostedService` stored the token the host hands `StartAsync` and gave it to
`CallSessionManager`, which runs **every** session save under it — not only the ones at shutdown. That
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Release history — every version, with what changed and why — is in [`CHANGEL

API coverage (cumulative): 148/152 AMI actions (97%), 94/98 ARI endpoints (96%), 46/46 ARI event types (100%), 27/27 ARI models (100%), 269 AMI events covering Asterisk 18-23. Asterisk 22.5+ outbound WebSocket and Asterisk 22.8/23.2+ `chan_websocket` JSON control protocol both supported. Compatible with **Asterisk 18, 20, 22 LTS, and 23 Standard** — see [`docs/guides/asterisk-version-matrix.md`](docs/guides/asterisk-version-matrix.md) for lifecycle and break-change risk areas.

Architecture decisions: **58 ADRs** in [`docs/decisions/`](docs/decisions/) covering AOT-first design, source-generator-over-reflection policy, three-tier test strategy, push-bus design, cadence commitment, resilience/cluster primitive split between MIT and Pro, the rebrand to Verbara, and the cross-repo ADR reference convention.
Architecture decisions: **59 ADRs** in [`docs/decisions/`](docs/decisions/) covering AOT-first design, source-generator-over-reflection policy, three-tier test strategy, push-bus design, cadence commitment, resilience/cluster primitive split between MIT and Pro, the rebrand to Verbara, and the cross-repo ADR reference convention.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/claim-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ is the worked example: its vendor wire captures are EVIDENCE, its counts of *our
| ~~67~~ | 94.26% English accuracy, in upstream's voice | — | — | **DELETED** — lived in the release bullets, cut by the 2026-09-20 ruling on `README.md` release history. The figure survives at `:465` (row below) with its citation and hash pin; this was the duplicate. |
| ~~67~~ | ~12 ms CPU inference | — | `TurnDetectionBenchmark` | **DELETED** — measured at 26.18–37.30 ms; deferred, see *Deferrals*. Its line was also removed with the release bullets on 2026-09-20. |
| 65 | 148/152 AMI (97%), 94/98 ARI (96%), 46/46, 27/27, 269 events | ENFORCING | — | GAP — the event figure was corrected 2026-09-24 (was 278) |
| 67 | **58 ADRs** | ENFORCING | `StatusBlockCoherenceTests` — counts `docs/decisions/*.md`, excluding the catalog `README.md` | **OK** |
| 67 | **59 ADRs** | ENFORCING | `StatusBlockCoherenceTests` — counts `docs/decisions/*.md`, excluding the catalog `README.md` | **OK** |
| 91 | measurement provenance (Ryzen 9 9900X, .NET 10.0.5, BDN v0.14.0, 2026-04-18) | COHERENCE | `PerformanceTableCoherenceTests` — the header provenance test | PARTIAL — matched against the whole file rather than this line, and blind to the AMI row's .NET 10.0.6 MediumRunJob exception (`performance-record.json:12`); the two session-store rows measured apart from it state their own at :110 |
| 95 | AMI parse+dispatch 1.62M events/sec (617.6 ns) | ENFORCING + COHERENCE | `perf-regression.yml` `*AmiProtocolReader*` | GAP — observational only (`\|\| true`, no baseline) |
| 96 | ARI deserialize Channel 3.54M ops/sec (283 ns) | ENFORCING + COHERENCE | `*AriJson*` | GAP — observational only |
Expand Down
139 changes: 139 additions & 0 deletions docs/decisions/0061-only-a-changed-breaking-forces-a-minor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# ADR-0061: Only a `Changed — BREAKING` entry forces a minor

- **Status:** Accepted
- **Date:** 2026-09-24
- **Deciders:** Harol A. Reina H.
- **Related:** ADR-0028 (cadence commitment — **this ADR amends its post-v2.0 commitment 3**),
ADR-0050 (the first behavioural break released as a minor, and the origin of the written rule),
ADR-0052 F4 (restates it), ADR-0055 (the version is cut at release time, so a tier ruling binds the
release and not the diff), ADR-0060 (the most recent `Changed — BREAKING`, and the migration guide
its tier obliged), ADR-0048 D7 (reads as a tier rule and is not one — see *Context*), ADR-0027 (the
stewardship pledge, whose constraint on removing public MIT types this ADR does not touch).
Harvested from the change `ari-failed-connect-and-silent-catches`, task 9.5.

## Context

Three records in this repository say a behavioural break takes a minor, and the shipped history
contradicts them twice.

What the records say:

- **ADR-0028** (2026-04-20), post-v2.0 commitment 3: *"Patch cadence: mensual o por needs,
**no-breaking por definición**."* In force since v2.0.0 shipped on 2026-05-06.
- **ADR-0050** (2026-08-17): *"It ships in a minor with an explicit callout, never in a patch."*
- **ADR-0052 F4** (2026-08-19): *"it is a behavioural break and gets a `BREAKING` CHANGELOG entry and
a minor bump, matching how ADR-0050 itself shipped."*

What shipped. Every `BREAKING` heading in `CHANGELOG.md`, counted 2026-09-24:

| Release | Tier | `Fixed — BREAKING` | `Changed — BREAKING` |
|---|---|---|---|
| 2.5.0 (2026-08-24) | minor | 10 | 3 |
| 2.5.2 (2026-09-13) | **patch** | 2 | 0 |
| 2.5.3 (2026-09-13) | **patch** | 2 | 0 |
| `[Unreleased]` | — | 2 | 2 |

Both patches landed after all three records. And 2.5.2's is not a distant cousin of the class those
ADRs were written about: `Fixed — BREAKING: AmiConnection.ConnectAsync completed as Connected when
cancelled` is the AMI sibling of the ARI connect-state fix that ADR-0056 later shipped.

**The pattern nobody wrote down is narrower than any of the three records.** A
`Changed — BREAKING` has shipped three times and all three are in 2.5.0, a minor; it has never
shipped in a patch. A `Fixed — BREAKING` has shipped fourteen times, in both tiers. And 2.5.0's ten
did not cause its minor — the three `Changed — BREAKING` in the same release already forced it, so no
release in this repository's history has ever taken a minor *for* a `Fixed — BREAKING`.

Four `BREAKING` markers appear outside a heading (`CHANGELOG.md:532`, `:1415`, `:1488`, `:1848`).
Each sits under a `Fixed — BREAKING` heading and elaborates it. No break has reached a release
without a heading declaring it, so the headings are a complete index of this history.

**One record reads like a third tier rule and is not.** ADR-0048 D7 says *"a route fix that changes
the meaning of a public property is an API decision, not a patch"*. Its next sentence settles the
sense: *"never applied inline because the route is obviously wrong"* — "patch" there is an ad-hoc code
change, not a release tier. The case it names resolves the ambiguity anyway: `SpeechmaticsOptions.BaseUri`
shipped as one of 2.5.0's three `Changed — BREAKING` entries, in a minor, which is what D1 below
requires. A rule nobody had written was followed a fourth time.

**Why the split is not arbitrary.** The two labels make different promises about what the consumer
was relying on. `Fixed` says the documented behaviour was not being delivered and now is: the
consumer who depended on the old behaviour depended on something the SDK never promised. `Changed`
says the documented behaviour itself moved: the consumer depended on exactly what was promised, and
the promise was withdrawn. Those are different debts, and the history priced them differently
without ever saying so.

## Decision

**D1 — Only a `Changed — BREAKING` entry forces a minor.** A release whose breaking entries are all
`Fixed — BREAKING` MAY ship as a patch.

**D2 — This amends ADR-0028's post-v2.0 commitment 3.** *"No-breaking por definición"* now reads: a
patch carries no `Changed — BREAKING`. It may carry a `Fixed — BREAKING`, and when it does, the entry
keeps its label and its explicit callout — the tier is not what the label is for. The rest of
ADR-0028 stands untouched: the 8–12 minors/year cap, the annual major on the .NET cycle, the LTS
line, and the migration-guide obligation on a minor that carries a breaking change. ADR-0027's
constraint on removing public MIT types is untouched as well: this ADR prices behavioural breaks, and
says nothing about removing public surface, which is a separate promise under the stewardship pledge.

**D3 — The label is the decision, and it is made when the entry is written**, by the author, against
what the SDK documented before the change. An entry that cannot say which documented behaviour it
restores is a `Changed`.

**D4 — The tier binds the release, not the diff.** `Directory.Build.props` is cut at release time
(ADR-0055), so a ruling recorded against a pull request constrains the release that carries it and
changes nothing in that pull request.

**D5 — 2.5.2 and 2.5.3 are applications of this rule, not errors.** Recorded here so a reader
comparing those two releases against ADR-0028, ADR-0050 or ADR-0052 does not have to reconstruct
whether the repository broke its own commitment. It did not; the commitment was written more
broadly than the practice it described.

## Consequences

- **Positive, and the point: a defect fix can reach consumers in a patch.** Fourteen of the
seventeen breaking entries this repository has released are `Fixed` — defects in provider
integrations, cancellation accounting and audio handling, none of which withdrew a promise the SDK
had kept. Pricing each at a minor would consume the 8–12 minors/year ADR-0028 caps on bug fixes,
and would make the class of fix ADR-0050 exists to encourage the most expensive kind to ship.

- **Negative, and the headline: a label now carries the release tier, and nothing verifies it.** No
test in this repository parses `CHANGELOG.md`; `publish.yml` reads only `## [<version>]` headings
and never reads `[Unreleased]`. An author who wants a patch can write `Fixed` on an entry that
moves a documented behaviour, and no gate will notice. The mitigation is D3 plus the callout text
itself, which a reviewer can check against the diff — deliberately a review property rather than a
gate, because a parser over prose headings would fail in the direction of blocking a correct
release.

- **The pending release is unaffected.** `[Unreleased]` holds two `Changed — BREAKING` (#302's
AudioSocket frame format and #291's `AriOutboundListener` accept behaviour), so 2.6.0 is a minor
under D1 — the same answer already ruled on that change, now reached by a stated rule instead of a
case-by-case reading.

- **ADR-0028's migration-guide obligation is now the binding constraint on that cut, and it is not
yet met.** One guide exists, `docs/guides/audiosocket-wire-format-migration.md`, written for #302.
Recorded here as owed rather than resolved, because this ADR settles the tier and the tier is what
triggers the obligation.

- **This gives no procedure for choosing `Fixed` over `Changed` on a given entry.** D3 states the
test — which documented behaviour does this restore — but the judgement stays with the author, and
it now costs a release tier instead of nothing. That is a real increase in the stakes of a label
that used to be presentational.

## Alternatives considered

1. **Reaffirm ADR-0028 as written, and record 2.5.2 and 2.5.3 as two violations.** Rejected.
Fourteen of the seventeen breaking entries this repository has released are `Fixed` — the count
excludes `[Unreleased]`. Under this reading each one costs a minor, the cap ADR-0028 sets is
consumed by bug fixes, and the incentive runs against shipping exactly the fixes ADR-0050 was
written to encourage. It also leaves two shipped releases standing as errors that no consumer was
harmed by.

2. **Declare that every breaking change takes a minor, in one place, and delete the three
scattered statements.** Same effect as 1, with tidier records. Rejected for the same reason.

3. **Decide the tier per release by measured blast radius.** Rejected: no written procedure, nothing
to check a past release against, and the measurement would be made by the same author whose label
it replaces.

4. **Leave it unwritten and keep ruling case by case.** Rejected: the contradiction now sits in three
accepted records and two shipped releases, and the next reader who finds it has to redo the
reading this ADR is the result of.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,4 @@ The decision, stated in one or two sentences.
- [ADR-0058](0058-a-stopping-token-never-gates-a-handoff.md) — A token that stops a server never gates a hand-off carrying ownership of a resource the server has already acquired: the hand-off is unconditional, `Task.Run`'s own token is an explicit `CancellationToken.None` (`CA2016` is an error here, so the bare call does not compile), and the stopping token travels *into* the handler, which is the single owner that closes the connection — quietly, because such a connection missed no deadline. ADR-0053's one-owner rule extended to the connection that has not become a session yet; the two accept loops in this repo now hand over with a line identical character for character, and `IDISP016` already refuses the rejected alternative at build time. (Accepted, 2026-09-21)
- [ADR-0059](0059-a-lifetime-token-is-cancelled-by-the-phase-it-names.md) — A token a component keeps past a phase comes from a source that outlives that phase, and is cancelled by the phase it names: `SessionManagerHostedService` owns the source behind the token every `CallSessionManager` save runs under, instead of storing the host's `StartAsync` token — whose documented meaning is that the start was aborted, and which is provably inert after a completed start because the host releases it there, so #261's shutdown filter could never fire in the phase it was written for. The stop phase cancels the source through a registration rather than on entry, the detach precedes that registration (load-bearing, and unbound until a test proved red against the mutation bound it), and `Dispose` cancels before releasing behind an idempotency gate. ADR-0054's one-owner rule read across a lifetime boundary. (Accepted, 2026-09-21)
- [ADR-0060](0060-the-wire-format-is-asterisks-and-no-parser-is-held-to-its-own-encoder.md) — The AudioSocket frame is Asterisk's, not ours: **one byte of type, two of big-endian length**, with the values `res_audiosocket.h` defines (`Hangup 0x00`, `Uuid 0x01`, `Dtmf 0x03`, `Audio 0x10`, `Error 0xFF`); both of this repo's codecs read a four-byte header and took `0x01` for audio, so **neither server had ever completed a handshake**, and both suites stayed green for six months because every test built its input with the codec under test. **No parser may be held to its own encoder**: the format is pinned by bytes captured from a real Asterisk, in **one** fixture shared by every parser (a per-package copy is the same loop reassembled), the fixture grows only from a capture — which is why it has no hangup frame — and a real Asterisk dials both servers in the functional lane. Supersedes ADR-0017. Enum values change on shipped public types: breaking, minor not patch. (Accepted, 2026-09-23)
- [ADR-0061](0061-only-a-changed-breaking-forces-a-minor.md) — The release tier is set by **which kind** of breaking entry a release carries, not by the fact that something broke: a `Changed — BREAKING` forces a minor, a release whose breaking entries are all `Fixed — BREAKING` may be a patch. The labels promise different things — `Fixed` says the documented behaviour was not being delivered and now is, so the consumer relied on something never promised; `Changed` withdraws a promise that was kept. Measured across every `BREAKING` heading in `CHANGELOG.md`: all three `Changed — BREAKING` are in 2.5.0, a minor, and none has ever shipped in a patch, while fourteen `Fixed — BREAKING` shipped in both tiers — and 2.5.0's ten did not cause its minor, the three `Changed` did. **Amends ADR-0028's post-v2.0 commitment 3** (*a patch is no-breaking por definición*), which turns 2.5.2 and 2.5.3 from contradictions into applications; the rest of ADR-0028 stands, including the migration guide a minor owes. The cost is stated rather than mitigated: a label now carries the tier and nothing parses `CHANGELOG.md` to check it. (Accepted, 2026-09-24)
Loading