docs: the bounded capacity queue, ADR 0042 and SDK 1.28.0 (#1033) - #1876
Conversation
67e60cd to
8472183
Compare
65d100f to
6311bdf
Compare
This is the same trap that already bit each stack once: 1.26.0 was taken by #1634 mid-flight, then 1.27.0 was taken by #1849/#1637 mid-flight. The gate compares against the PR's base, not Before merging whichever of these goes first: leave its number alone, then re-number the other to Two further re-cut stacks (#1565 reapply, #1125 OAuth self-service) are in flight and will each want a number too. Worth deciding the merge order for all four in one go rather than discovering it a gate at a time. |
|
All three of these claim
Each is individually correct and all three release gates are green, because the gate compares against the PR's base, not Merging any one of these obliges re-numbering the other two before they merge — A fourth stack (#1565, conversation reapply) is still being cut and will want a number too. Given that, it is probably worth picking the merge order for all four now and assigning |
|
All four are individually correct and all four release gates are green, because the gate compares against each PR's base, not Recommendation: take the version bump out of all four, and do one SDK release afterwards. Assigning Instead:
That removes the ordering constraint entirely, gives users one coherent release note instead of four near-simultaneous minors, and means no stack can silently publish nothing by merging second. The cost is that the SDK types for whichever features land first sit unreleased on |
8472183 to
25d6171
Compare
6311bdf to
cc70a46
Compare
BinaryBourbon
left a comment
There was a problem hiding this comment.
Part 7 of 7. ADR 0042, the docs and the SDK bump. Approving.
The ADR is the best part of the stack. It records the two departures from #1481 as departures with reasons, it names what the claim fence does and does not buy, and the Alternatives section argues against the four positions someone will actually raise — including the #1026 upgrade-lever argument that #1033 deliberately left open. "The queue delays the cap; it never raises it" is the right one-line summary and it is true, which I verified rather than took on faith: Quotas.with_sandbox_reservation/3 still runs check_fleet_ceiling → Credits.gate → check_sandbox_quota under both advisory locks on every replay, so docs/api.md's "must pass the credit gate and the inference gate again" holds.
Recording that #1768's reset fence composes with this rather than fighting it — a held slot is capacity the replay does not find — is exactly the kind of thing that is worth writing down while both are in flight and impossible to reconstruct later. Same for the 0005 addendum: the cap did not change, the wait in front of it is new, and that is the honest way to amend a decision rather than superseding it.
docs/api.md, docs/configuration.md, the architecture cron row and the pruner's window all match the code I read. The prices guide is the honest operator-facing version, and its "Other callers keep the immediate 429 or 503 answer" is the sentence that carries the whole opt-in design.
Three notes:
The marketing copy is less careful than the guide. The home and launch pages now read "Starts beyond your limit can wait in a bounded queue." A console start does not queue — nothing in StartLive sets queue: true — so for the person most likely to be reading that page, starts beyond the limit are still refused. "can wait" is hedged enough that I would not call it false, and it is a marketing page rather than a contract, but #1027 changed this exact sentence for accuracy, which makes it the one place in the repo where a vague claim has history. Something like "API starts can opt to wait in a bounded queue" costs four words and cannot be read as a promise to a console user. The persistent-mode sentence being deliberately left alone is right, and worth having said explicitly in the PR body.
The SDK collision is now three-way, not hypothetical. #1860 (#1635) and #1885 (#1125) both also claim 1.28.0. main is at 1.27.0 and npm's latest is 1.27.0, so all four edits here are correct today; whichever of the three lands second re-rolls package.json, both package-lock.json occurrences, USER_AGENT and the SDK CHANGELOG.md. The warning block in the description is the right thing to have written — just note it is a queue of three.
ADR 0042 decision 3's waiting for a free sandbox slot sentence needs to follow whatever #1872 does. As written it describes the row's state for one drain pass only. Details on that PR.
The verified/stale_after frontmatter and the regenerated decisions/index.md are in the same PR, which is what okf validate wants. "Not carried: nothing" is a claim I spot-checked against #1481's file list and did not find a counterexample to.
25d6171 to
8bb25fa
Compare
cc70a46 to
5dc8ae7
Compare
|
ADR and copy updated. Force-pushed. ADR 0042 gained three things, all from findings on the stack:
The marketing copy names the caller that can wait. You were right that "Starts beyond your limit can wait in a bounded queue" reads as a promise to whoever is looking at the page, and a console start does not queue. Both pages now say:
Which is the same thing the prices guide already said honestly, and it keeps #1027's reason for touching this sentence intact. The SDK version is still 1.28.0 and |
BinaryBourbon
left a comment
There was a problem hiding this comment.
Re-reviewed the delta since my approval (cc70a461 → 5dc8ae7c): three ADR paragraphs and one marketing sentence on two pages. Approving.
The marketing copy is now true
"Starts beyond your limit can wait in a bounded queue" was a promise the server does not keep. A start waits only if it asks (queue: true), and the one caller that waits without asking is a cron firing. The replacement says both halves — "An API start beyond your limit can ask to wait in a bounded queue, and a scheduled run waits by itself" — which is the asymmetry decision 3 argues for, in a sentence somebody reading the pricing page can act on. Someone who read the old line and did not send the flag would have concluded the queue was broken.
marketing_pricing_test.exs asserts both halves on both pages rather than just the changed prefix, so the scheduled-run clause cannot be dropped later without the test noticing.
The three ADR additions
Decision 2's new paragraph is the one I wanted, and it generalises past the instance: "Anything that narrows what a request may do has to survive the wait, or the queue is a way around it." source and sandbox_key_id are then the two worked examples rather than two special cases. The next person adding a restriction to the door now has a rule to check against.
Decision 3 now separates the two questions that were tangled: who enqueues (the audit actor, which also keeps the replay out of its own queue) and what the row says while a request is live (whichever caller asks, including the drainer's replay). Recording that a waiting firing stamps no last_run_at and writes no team.schedule.fired is right to have in the ADR — it is a deliberate absence, and an undocumented absence reads as an oversight.
Decision 7's addition is the precision note from my #1870 review, and it is sharper than what I wrote: "What the fence removes is the state in which one request reads as one conversation while two are running." An orphaned conversation stays visible in the tenant's own list, which is the property that makes the residue tolerable.
One follow-up, not for this PR: decision 3 now describes what the row reads during the wait but not after it ends. I have blocked #1872 on the row still saying waiting for a free sandbox slot once the request expires or is cancelled — indefinitely, for a one-off. Whatever that fix writes, this paragraph should gain a sentence for it.
Nit
The reflow left one line unwrapped mid-sentence:
as `POST /api/conversations` answers without `queue: true`. Queueing there would answer a person with an error while a
decisions/ is deliberately unpublished, so no prose gate will catch it. Worth a wrap on the next touch; not worth a push on its own.
Verified locally
marketing_pricing_test.exs and docs_test.exs pass at the stack tip, along with the rest of the stack's files. okf frontmatter is untouched, so no index refresh is owed.
CI has reported no verdict on this head — the 05:26 jobs are cancelled by concurrency, which gh pr checks prints as fail.
5dc8ae7 to
0d3df52
Compare
8bb25fa to
08d8ba6
Compare
BinaryBourbon
left a comment
There was a problem hiding this comment.
Re-reviewed the delta since my approval (5dc8ae7c → 0d3df52c). Decision 3 only. Approving — this closes the follow-up I left last time.
Decision 3 now carries both halves of what part 4 actually does.
The separation is stated as a rule rather than as a case list: "What the row says and whether the caller fired are separate questions." Then the two silent callers and why each is silent — the cron queued instead of firing, the drainer is replaying a firing already on the trail — and everyone else recorded with the refusal they received, "even though the row goes on reporting the wait". Keeping that last clause matters: a reader who finds last_error saying "waiting" and an audit row saying "sandbox quota: 5/5" would otherwise file it as an inconsistency rather than read it as the row describing the schedule and the trail describing the caller.
And it ends with the failure it prevents — "Deciding this from 'is anybody waiting' rather than 'did this caller wait' is how a person's action came to leave no audit trace at all." An ADR that records the wrong version of a decision alongside the right one is worth more than one that only states the conclusion; the next person to simplify that cond now has the reason not to.
The new paragraph on the wait ending is the sentence I asked for, with both strings quoted and the one_off argument for why a cron's next firing is not a fix. Decision 3 now describes the row before, during and after the wait, which is the whole of what a person reading /team can see.
The unwrapped line is reflowed too.
Nothing else moved. Marketing copy, the SDK line and the rest of the ADR are unchanged from my last pass.
Verified locally
docs_test.exs and marketing_pricing_test.exs pass at the stack tip, with the rest of the stack's files — 302 tests, 0 failures. okf frontmatter untouched, so no index refresh is owed.
0d3df52 to
f4c668f
Compare
08d8ba6 to
0e03779
Compare
0e03779 to
4f146ec
Compare
ADR 0042 records the whole decision, including the two places this re-cut differs from the original proposal: the claim is a compare-and-swap fenced on the version it observed, with stale-claim recovery folded into it rather than run as a separate pass, and the SDKs omit `/api/sandbox-queue` with the reason written down. It also records that #1768's reset fence composes with this rather than fighting it: a slot an unconfirmed reset holds is simply capacity the replay does not find. ADR 0005 gets the addendum that says its cap now has a bounded wait in front of it. `decisions/index.md` is regenerated by `scripts/decisions-index.sh`, and `okf validate decisions` passes. `docs/api.md` gains a "Wait for capacity" section, `docs/configuration.md` the two new environment variables, `docs/architecture.md` the drainer's cron row and the pruner's new window, and the prices guide the operator-facing version. The marketing claim "starts beyond your limit are refused, not queued" was true and is no longer, so it and its test change together. The sentence about persistent-mode turn capacity is deliberately untouched: that ceiling is `sandbox_at_capacity` on one machine, which this queue does not cover. All three prose gates pass on the changed pages. **SDK version**: 1.28.0. 1.27.0 was claimed and published by the #1637 stack while this one was being written, and the release gate refuses a version npm already carries. Concurrent bumps land in ascending order, so re-check `sdk/typescript/package.json` on `main` immediately before merging and re-roll the four edits (`package.json`, `package-lock.json` twice, `src/http.ts`, `CHANGELOG.md`) if `main` has moved past 1.27.0 again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S9jevFQT5MkF3rJUieeiHW
f4c668f to
12c9630
Compare
Stacked on #1874. Last of seven for #1033, and the one that names the decision.
ADR 0042 records the whole thing, including the two places this re-cut departs from #1481's proposal:
/api/sandbox-queueand the reason is written down rather than left implicit.It also records that #1768's reset fence composes with this rather than fighting it: a slot an unconfirmed reset holds is simply capacity the replay does not find, and the request waits for it the way it waits for any other. Nothing here needs to change when that lands. (#1768 is not on
mainas of this writing.)ADR 0005 gets the addendum saying its cap now has a bounded wait in front of it.
decisions/index.mdis regenerated withscripts/decisions-index.sh, andokf validate decisionspasses.Docs.
docs/api.mdgains a "Wait for capacity" section,docs/configuration.mdthe two environment variables,docs/architecture.mdthe drainer's cron row and the pruner's new window, and the prices guide the operator-facing version. The marketing claim "starts beyond your limit are refused, not queued" was true and no longer is, so it and its test change together. The sentence about persistent-mode turn capacity on the home page is deliberately untouched: that ceiling issandbox_at_capacityon one machine, which this queue does not cover. All three prose gates pass on the changed pages.Bumped to 1.28.0. 1.27.0 was claimed and published to npm by the #1637 stack while this one was being written, and the release gate correctly refused it (
@managoat/fountain-sdk@1.27.0 is already on npm). Concurrent bumps must land in ascending order, so immediately before merging this PR, checksdk/typescript/package.jsononmainagain and, if it has moved past 1.27.0, re-roll the four edits:package.json,package-lock.json(two occurrences),src/http.ts(USER_AGENT), andsdk/typescript/CHANGELOG.md.What this stack covers of #1481, and what it does not
Covered, all of it: the
sandbox_requeststable and context; the claim/drain state machine with transient, capacity and terminal classification; the event-driven drain with its Oban backstop;queue: trueonPOST /api/conversations; automatic queueing of scheduled teammate runs; the three/api/sandbox-queueendpoints; audit events on all five transitions; telemetry and the ops gauge; theRetentionPrunerwindow; ADR 0042 and the 0005 addendum;.env.example,docs/api.md,docs/configuration.md,docs/architecture.md, the prices guide, the marketing copy,CHANGELOG.md, the SDK contract, omissions and generated types.Deliberately different from #1481:
labels,execution_limitsandsandbox_api_access. feat(sandbox): add bounded capacity queue #1481 predates all three, so a queued start on that branch would have silently dropped a conversation's labels and its execution limits.docs/api.mdis prose on today'smain(the Diataxis rebuild, Docs IA redesign: the remaining steps #903) rather than the route listing and status table feat(sandbox): add bounded capacity queue #1481 edited, so the same content lands as a section instead of table rows.forbiddenresponse, per the seven-gates checklist.Not carried: nothing.
Verification
The whole stack, rebased onto
mainatea71d0ed: core +ee4,977 tests, 0 failures (seed 424242),fountain_buzz144, 0,fountain_support33, 0.mix format --check-formatted,scripts/sdk-contract/build.sh --check(167 operations, 193 schemas),npx tsc --noEmit, 104 TypeScript SDK tests,okf validate decisions, and all three prose gates.Part 7 of 7 for #1033.
🤖 Generated with Claude Code
https://claude.ai/code/session_01S9jevFQT5MkF3rJUieeiHW
Review fixes (round 2)
ADR 0042 gained three things, each from a finding on the stack:
sandbox_key_idfix: a replay carries the restrictions the door was under, not only its attributes.sourceandsandbox_key_idare named as the two fields stored for that reason.last_run_atand records no firing.The marketing copy now names the caller that can wait — "An API start beyond your limit can ask to wait in a bounded queue, and a scheduled run waits by itself" — because a console start does not queue, and #1027 touched that sentence for accuracy in the first place.
decisions/index.mdneeds no regeneration (frontmatter unchanged) andokf validate decisionspasses.Review fixes (round 3)
ADR 0042 decision 3 now covers the whole lifecycle of what the schedule row says:
timed out waiting for a free sandbox sloton expiry,the queued run was cancelledon a cancellation — because neither transition reachesrun_schedule/2and aone_offhas no next firing to self-correct.Closes #1033