Skip to content

ADR-141: Storing a reset_after limit is gated on reader versions

Status: Accepted Date: 2026-09-27 Issue: #638 Related: ADR-139, ADR-137, ADR-009, #640, #644

Context

A reader predating ADR-139 cannot read a reset_after limit. An old client sees a zero rate with no reset, which ADR-137 rejects: under on_unavailable=block every acquire on that level fails, and under allow every acquire is admitted with no limiting at all on that level. An old aggregator reads the quota as a dripping limit, so its proactive-sharding clone mints a fresh share per new shard — the #587 over-admission, where a v0.15 aggregator funds the clone by ADR-145's move. An old provisioner stores a reset_after manifest limit as a dripping one.

The version record already carries lambda_version and client_min_version, but neither could be trusted. lambda_version was stamped by writers that had not necessarily updated both Lambdas (a deploy on an existing stack pushes code but never adds or removes a function), and client_min_version was reset to 0.0.0 by every Lambda update and ignored by clients, which fell through an incompatible check with no flag set. There is one aggregator per stack, so its version is the one reader a writer can verify; old clients cannot be made to fail closed from a newer build. The options, the owner's 2026-09-26 decision, and the mechanics are in #638 and CLAUDE.md "Version gate (#638)".

Decision

Every writer of a reset_after limit — config setters, the provisioner, and an acquire() limits override — must refuse it unless lambda_version is at least 0.15.0 or equals the writer's own build, and a config write it admits must raise client_min_version to 0.15.0 (capped at a development writer's own build), which no writer may lower and below which clients from v0.15.0 on must refuse to run. A writer may stamp lambda_version with its build only when it created the stack in that call, or when the aggregator and the provisioner each either had code pushed in that run or do not exist.

Consequences

Positive: - A pre-v0.15 aggregator or provisioner can no longer be handed a limit it would misread. - One predicate decides the stamp for CLI deploy, CLI upgrade, infrastructure provisioning, open()'s record initialization and its Lambda auto-update, so the rule cannot drift between them. - A missing record or unknown Lambda version fails closed and names its remedy. - client_min_version becomes a working field, so the next incompatible feature is protected by the same ratchet without new machinery. - Writes of every other limit pay nothing.

Negative: - Nothing makes a v0.14 client fail closed; it does not enforce the session limit, and every client must still be upgraded before one is relied on (see ADR-142). - The gate holds only at write time: a v0.14 CLI can later put the old Lambdas back, and a v0.14 upgrade also resets the minimum. Never run a v0.14 CLI against such a stack. - The minimum, and the override gate's cached Lambda version, are checked when a repository opens, so a long-lived process is not refused until it restarts. - A refused CloudFormation update whose previous properties also carried reset_after leaves the stack in UPDATE_ROLLBACK_FAILED, needing a manual rollback continuation. - deploy performs no minimum check of its own, and --no-aggregator stacks get no exemption in the gate; zae-limiter upgrade and open(auto_update=True) push only to the Lambdas a stack has, so they are the remedy on every stack shape (#644).

Alternatives Considered

Gate writers on client_min_version alone

Rejected because: v0.14 ignores that field, so it stops neither v0.14 clients nor the v0.14 aggregator.

Bump the schema major so old clients raise IncompatibleSchemaError

Rejected because: it breaks every v0.x client on the table and needs a migration.

A stored flag old clients already reject

Rejected because: it fails exactly as today, and so still fails open under allow.

Trust the stack's EnableAggregator parameter to earn the stamp

Rejected because: the function does the reading, and the template creates it only when a role is also available.

Hide the configuration from pre-v0.15 readers

Rejected as a replacement because: it hides the limit from old clients but cannot keep an old aggregator from cloning a bucket that carries one; adopted alongside this gate in ADR-142.