Skip to content

Commit 66056a6

Browse files
fix: serialize the register route's reload, and tighten the Base tests
- The register route runs its check-and-reload one call at a time, so concurrent calls cannot both dispose the same live resources. Inside that section it skips only when this process has already reloaded for the credential now on disk, which makes the before/after read unnecessary. - Tests: a concurrent-register case; an ACP case where the model changes between sessions; the picker's HTTP fallback asserts method and body; the disclosure notice flushes its KV write before cleanup; the retry case's name says it covers the re-fire only; the slow-registration test releases its gate on failure; the error tests assert their type and the budget message; a duplicate logout test is removed. - Docs: drop the stale "consented" registration wording. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_016x3nbZU5Vb6HnSTqE7vELm
1 parent f29253f commit 66056a6

10 files changed

Lines changed: 120 additions & 67 deletions

File tree

‎docs/docs/configure/providers.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ altimate
111111
The URL must use HTTPS. Credentials,
112112
query strings, and fragments in the URL are rejected. `ALTIMATE_FREE_GATEWAY_URL` is retained as a
113113
legacy fallback, but `ALTIMATE_BASE_GATEWAY_URL` takes precedence. If the configured gateway host
114-
changes, credentials issued by the previous host are not loaded and the consented registration
114+
changes, credentials issued by the previous host are not loaded and the registration
115115
flow must run again.
116116

117117
Altimate Base waits up to **5 minutes** for the gateway to send response headers, because the

‎packages/opencode/src/server/server.ts‎

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export namespace Server {
7676
// because a cache built before a background registration finished cannot be told apart from one
7777
// built after it, in any directory or either registry.
7878
let appliedBaseCredential: string | undefined
79+
// Serializes the register route's check-and-reload, so concurrent calls reload at most once.
80+
let baseReloadQueue: Promise<unknown> = Promise.resolve()
7981
// altimate_change end
8082

8183
export const Default = lazy(() => createApp({}))
@@ -781,15 +783,6 @@ export namespace Server {
781783
)
782784
}
783785

784-
// altimate_change — Codex review finding: `FreeTier.register()` returns success whether
785-
// it minted/rotated a credential OR just found an existing valid one (the idempotent
786-
// fast path — see its "already registered" branch in free/client.ts). Everything below
787-
// is instance-wide teardown that only matters when the credential on disk actually
788-
// changed; comparing it before/after `gate.register()` (rather than changing
789-
// `register()`'s own return shape, which every other caller — the picker, and a dozen
790-
// existing tests — depends on as a plain `Credentials`) reports that without touching
791-
// that contract.
792-
const before = await FreeTier.credentials().catch(() => undefined)
793786
const gate = FreeTierConsent.createRegistrationGate({
794787
register: () => FreeTier.register({ origin: "server" }),
795788
onUnexpectedError: (error) => log.error("Altimate Base registration failed", { error }),
@@ -815,45 +808,47 @@ export namespace Server {
815808
// A failure in either leaves the credential written but provider lists possibly stale, so
816809
// it is reported rather than swallowed: the client needs to know its picker may be wrong.
817810
//
818-
// Skipped entirely when nothing changed: an idempotent register (already valid
819-
// credentials for this gateway) has nothing for a provider loader to re-read, so tearing
820-
// down every session, LSP, PTY, MCP connection and file watcher in the process would be
821-
// pure disruption for zero benefit.
811+
// Skipped when this process has already reloaded for the credential now on disk: an
812+
// idempotent register (the "already registered" fast path) then has nothing for a
813+
// provider loader to re-read, and tearing down every session, LSP, PTY, MCP connection
814+
// and file watcher again would be pure disruption. An unchanged file alone is not enough:
815+
// a startup registration that finished in the background leaves caches built before it
816+
// without Base, in any directory and in either registry.
817+
//
818+
// The check and the reload run one at a time, so two concurrent calls cannot both pass
819+
// the check and dispose the same live resources twice; the second sees the first's mark.
822820
if (outcome.ok) {
823-
const after = await FreeTier.credentials().catch(() => undefined)
824-
// Identity covers everything that decides whether a loader sees Base: an expired or
825-
// rejected credential loads as absent, and a logout elsewhere rotates the nonce, so the
826-
// same key and URL reissued after either still has to reload.
827-
const identity = (value: typeof after) =>
828-
value
829-
? [value.baseURL, value.apiKey, value.expiresAt ?? "", value.rejected ? "rejected" : "", value.logoutNonce ?? ""].join("\n")
821+
const reload = baseReloadQueue.then(async () => {
822+
const current = await FreeTier.credentials().catch(() => undefined)
823+
// Identity covers everything that decides whether a loader sees Base: an expired or
824+
// rejected credential loads as absent, and a logout elsewhere rotates the nonce, so the
825+
// same key and URL reissued after either still has to reload.
826+
const fingerprint = current
827+
? [current.baseURL, current.apiKey, current.expiresAt ?? "", current.rejected ? "rejected" : "", current.logoutNonce ?? ""].join("\n")
830828
: undefined
831-
const fingerprint = identity(after)
832-
const changed = identity(before) !== fingerprint
833-
// An unchanged file is not enough to skip: a startup registration that finished in the
834-
// background leaves caches built before it without Base, in any directory and in either
835-
// registry. Skip only when this process has already reloaded for this exact credential.
836-
if (!changed && fingerprint !== undefined && fingerprint === appliedBaseCredential) {
837-
return c.json(outcome)
838-
}
839-
const disposed = await Promise.all([
840-
Instance.disposeAll().then(
841-
() => true,
842-
(error) => {
843-
log.error("Altimate Base registered but legacy instance disposal failed", { error })
844-
return false
845-
},
846-
),
847-
AppRuntime.runPromise(InstanceStore.Service.use((store) => store.disposeAll())).then(
848-
() => true,
849-
(error) => {
850-
log.error("Altimate Base registered but InstanceStore disposal failed", { error })
851-
return false
852-
},
853-
),
854-
]).then((results) => results.every(Boolean))
855-
if (disposed) appliedBaseCredential = fingerprint
856-
return c.json(disposed ? outcome : { ...outcome, staleProviders: true as const })
829+
if (fingerprint !== undefined && fingerprint === appliedBaseCredential) return true
830+
const disposed = await Promise.all([
831+
Instance.disposeAll().then(
832+
() => true,
833+
(error) => {
834+
log.error("Altimate Base registered but legacy instance disposal failed", { error })
835+
return false
836+
},
837+
),
838+
AppRuntime.runPromise(InstanceStore.Service.use((store) => store.disposeAll())).then(
839+
() => true,
840+
(error) => {
841+
log.error("Altimate Base registered but InstanceStore disposal failed", { error })
842+
return false
843+
},
844+
),
845+
]).then((results) => results.every(Boolean))
846+
if (disposed) appliedBaseCredential = fingerprint
847+
return disposed
848+
})
849+
baseReloadQueue = reload.catch(() => undefined)
850+
const reloaded = await reload
851+
return c.json(reloaded ? outcome : { ...outcome, staleProviders: true as const })
857852
}
858853
return c.json(outcome)
859854
},

‎packages/opencode/test/acp/service-session.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,34 @@ describe("ACP service sessions", () => {
402402
})
403403
})
404404

405+
// altimate_change start — the cases above all resolve to Base, so they cannot tell a fresh
406+
// re-read from a reused first selection; this one changes the answer between sessions.
407+
it("re-reads a recent pick that changes between sessions in a cached directory", async () => {
408+
const base = {
409+
...provider,
410+
id: ProviderID.make("altimate-free"),
411+
models: {
412+
[ModelID.make("altimate-base")]: {
413+
...provider.models[modelID],
414+
id: ModelID.make("altimate-base"),
415+
providerID: ProviderID.make("altimate-free"),
416+
},
417+
},
418+
} satisfies Provider.Info
419+
await withTestStateHome(async () => {
420+
const stateFile = path.join(Global.Path.state, "model.json")
421+
await fs.writeFile(stateFile, JSON.stringify({ recent: [{ providerID: "altimate-free", modelID: "altimate-base" }] }))
422+
const { service } = makeService([], { providers: [provider, base] })
423+
const first = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] }))
424+
expect(select(first, "model")?.currentValue).toBe("altimate-free/altimate-base")
425+
426+
await fs.writeFile(stateFile, JSON.stringify({ recent: [{ providerID, modelID }] }))
427+
const second = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] }))
428+
expect(select(second, "model")?.currentValue).toBe(`${providerID}/${modelID}`)
429+
})
430+
})
431+
// altimate_change end
432+
405433
it("fails before creating a session when the configured model is unavailable", async () => {
406434
const bigPickleProvider = {
407435
...provider,

‎packages/opencode/test/altimate/altimate-base-auto-register.test.ts‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,6 @@ describe("FreeTier.autoRegister", () => {
9999
expect(gateway.registerCalls).toHaveLength(0)
100100
})
101101

102-
test("a logout that lands before the registration lock is acquired is not missed", async () => {
103-
// Simulates the race the spec calls out: nothing has registered yet (no pre-existing
104-
// credential), and a logout call — which takes the SAME lock — completes before autoRegister's
105-
// own lock body runs. Because that body reads the store fresh from inside the lock (no
106-
// pre-lock "expected" value carried in), it sees the logout unconditionally.
107-
await FreeTierStore.remove()
108-
await FreeTier.logout()
109-
const result = await FreeTier.autoRegister()
110-
expect(result).toEqual({ status: "skipped", reason: "logged-out" })
111-
expect(gateway.registerCalls).toHaveLength(0)
112-
})
113-
114102
test("is skipped when no gateway URL is configured, with no network call", async () => {
115103
delete process.env.ALTIMATE_BASE_GATEWAY_URL
116104
delete process.env.ALTIMATE_FREE_GATEWAY_URL
@@ -199,6 +187,7 @@ describe("FreeTier.autoRegisterWithin", () => {
199187
const registered = await waitFor(() => FreeTier.isRegistered(), (v) => v === true)
200188
expect(registered).toBe(true)
201189
} finally {
190+
resolveRequest()
202191
slow.mockRestore()
203192
}
204193
})

‎packages/opencode/test/altimate/altimate-base-catalog.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ afterEach(() => {
5757
gateway.restore()
5858
})
5959

60-
/** Registers a real credential through the production consent path against the fake gateway. */
60+
/** Registers a real credential through the real `/register` path against the fake gateway. */
6161
async function registerCredential(): Promise<void> {
6262
gateway.registerNext({ kind: "ok" })
6363
await FreeTier.register({ origin: "picker" })

‎packages/opencode/test/provider/error.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ describe("ProviderError.parseAPICallError: Altimate Base isolation", () => {
464464
providerID: "altimate-free" as any,
465465
error: rateLimited("throttling_error", "", { "retry-after": "12" }),
466466
})
467+
expect(result.type).toBe("api_error")
467468
if (result.type === "api_error") {
468469
expect(result.responseHeaders?.["retry-after"]).toBe("12")
469470
}
@@ -526,6 +527,8 @@ describe("ProviderError.parseAPICallError: Altimate Base isolation", () => {
526527
"retry-after": "900",
527528
}),
528529
})
530+
expect(result.type).toBe("api_error")
531+
expect(result.message).toContain("Altimate Base has reached its shared daily limit")
529532
if (result.type === "api_error") {
530533
expect(result.isRetryable).toBe(false)
531534
expect(result.responseHeaders?.["retry-after"]).toBe("900")

‎packages/opencode/test/server/altimate-base-registration.test.ts‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ describe("Altimate Base registration route", () => {
136136
// idempotent path is pure disruption for zero benefit, since no provider loader has anything new
137137
// to re-read.
138138
test("disposes every instance when registration actually mints a new credential", async () => {
139-
mockCredentialsSequence(undefined, { apiKey: "sk-new", baseURL: "https://gateway.test", installSecret: "s" })
139+
mockCredentialsSequence({ apiKey: "sk-new", baseURL: "https://gateway.test", installSecret: "s" })
140140
mockRegister(async () => ({ apiKey: "sk-new", baseURL: "https://gateway.test", installSecret: "s" }))
141141
const disposeAll = spyOn(Instance, "disposeAll")
142142
try {
@@ -196,7 +196,7 @@ describe("Altimate Base registration route", () => {
196196
expect(disposeAll).toHaveBeenCalledTimes(1)
197197
credentialsSpy?.mockRestore()
198198
registerSpy?.mockRestore()
199-
mockCredentialsSequence(expired, renewed)
199+
mockCredentialsSequence(renewed)
200200
mockRegister(async () => renewed)
201201
expect(await (await post()).json()).toMatchObject({ ok: true })
202202
expect(disposeAll).toHaveBeenCalledTimes(2)
@@ -233,6 +233,38 @@ describe("Altimate Base registration route", () => {
233233
}
234234
})
235235

236+
test("two concurrent register calls reload once", async () => {
237+
const shared = { apiKey: "sk-concurrent", baseURL: "https://gateway.test", installSecret: "s" }
238+
mockCredentialsSequence(shared)
239+
mockRegister(async () => shared)
240+
let release!: () => void
241+
const held = new Promise<void>((resolve) => {
242+
release = resolve
243+
})
244+
// Hold the first disposal open so the second request arrives while it is still in flight.
245+
const disposeAll = spyOn(Instance, "disposeAll").mockImplementation(async () => {
246+
await held
247+
})
248+
const post = () =>
249+
app().request("/altimate/base/register", {
250+
method: "POST",
251+
headers: { "content-type": "application/json" },
252+
body: JSON.stringify({}),
253+
})
254+
try {
255+
const first = post()
256+
const second = post()
257+
await Bun.sleep(50)
258+
release()
259+
const bodies = await Promise.all([first, second].map(async (r) => (await r).json()))
260+
for (const body of bodies) expect(body).toMatchObject({ ok: true })
261+
expect(disposeAll).toHaveBeenCalledTimes(1)
262+
} finally {
263+
release()
264+
disposeAll.mockRestore()
265+
}
266+
})
267+
236268
test("reloads for a credential registered in the background, even if one directory already sees Base", async () => {
237269
// A startup auto-registration that finished after the server started leaves the file unchanged
238270
// across this request, while caches built earlier (another directory, or the /api registry)

‎packages/tui/test/cli/tui/dialog-model-welcome.test.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ test("outside a first run the picker records an impression but not a choice", as
288288
// first-run welcome picker — the one shown to users with no model at all — a failed Base
289289
// registration then bricked Enter, `/` and mouse-up for the rest of the dialog session. Confirms
290290
// the fix: the same row can be retried after a failure, and the register attempt actually re-fires.
291-
test("a failed Altimate Base selection on the welcome picker can be retried", async () => {
291+
// It covers the re-fire only: the mocked `/provider` never lists Base's model, so the second
292+
// attempt cannot complete a selection here (selectAltimateBase's success path is tested directly).
293+
test("a failed Altimate Base selection on the welcome picker re-fires registration when retried", async () => {
292294
const picker = await mountPicker("first_run", [...ALL_PROVIDER_IDS, "altimate-free"], {
293295
registerOutcomes: ["error", "ok"],
294296
})

‎packages/tui/test/component/altimate-base-disclosure-notice.test.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const baseProvider = { id: "altimate-free", name: "Altimate", models: { "altimat
4242

4343
async function mount(options: { preSeedShown: boolean }) {
4444
const [
45-
{ KVProvider },
45+
{ KVProvider, useKV },
4646
{ LocalProvider, useLocal },
4747
{ ArgsProvider },
4848
{ ThemeProvider },
@@ -87,10 +87,12 @@ async function mount(options: { preSeedShown: boolean }) {
8787

8888
let localAccessor: ReturnType<typeof useLocal> | undefined
8989
let toastAccessor: ReturnType<typeof useToast> | undefined
90+
let kvAccessor: ReturnType<typeof useKV> | undefined
9091
const shownMessages: string[] = []
9192
function Probe() {
9293
localAccessor = useLocal()
9394
toastAccessor = useToast()
95+
kvAccessor = useKV()
9496
useAltimateBaseDisclosureNotice()
9597
createEffect(() => {
9698
const message = toastAccessor?.currentToast?.message
@@ -139,6 +141,8 @@ async function mount(options: { preSeedShown: boolean }) {
139141
},
140142
async cleanup() {
141143
app.renderer.destroy()
144+
// Let the notice's "shown" write land before its state directory is removed.
145+
await kvAccessor?.flush()
142146
await tmp[Symbol.asyncDispose]()
143147
},
144148
}

‎packages/tui/test/component/select-altimate-base.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,16 @@ describe("selectAltimateBase", () => {
160160
const calls: string[] = []
161161
const fakes = fakeCollaborators({
162162
registerAltimateBase: undefined,
163-
fetchImpl: (async (input: RequestInfo | URL) => {
164-
calls.push(String(input))
163+
fetchImpl: (async (input: RequestInfo | URL, init?: RequestInit) => {
164+
calls.push(`${init?.method} ${String(input)} ${String(init?.body)}`)
165165
return Response.json({ ok: true })
166166
}) as typeof fetch,
167167
})
168168

169169
const result = await selectAltimateBase(fakes)
170170

171171
expect(result).toBe(true)
172-
expect(calls).toEqual(["http://test/altimate/base/register"])
172+
expect(calls).toEqual(["POST http://test/altimate/base/register {}"])
173173
expect(fakes.modelSetCalls).toHaveLength(1)
174174
expect(fakes.dialogReplaceCount).toBe(0)
175175
})

0 commit comments

Comments
 (0)