[WRONG BRANCH] release: promote the verified 2.60.0 tree to main - #5249
Conversation
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…t inventories (#5075) The regex seeds place a conventionally named file, so a regression test can sit in the tree, run in CI, and still be absent from the authoritative table. That is how the regression tests for #5050, #5051 and #5055 landed without ever entering scripts/test-layout/layout.json or tests/fixtures/test-layout-expected.json (#5059). The two inventories are two copies of one table and the membership oracle already compares them, so both sides get the same three entries. A new test names the three files so they cannot fall out again silently, and checks that each one actually sits in the directory its registration claims. No repository-wide explicit-registration policy is introduced here; the seeds keep carrying brand-new files as designed.
…tself on every boot (#5077) * fix(providers): stop the Antigravity rename migration re-announcing itself The rename migration and the OAuth preset reconciliation disagreed about nine retired Antigravity Flash ids, and startServer runs both, in that order. ANTIGRAVITY_MODEL_CONTEXT_WINDOWS derives a window for every compatibility alias, so the google-antigravity preset carries a modelContextWindows key for each retired id. The migration renamed those keys and persisted, applyOAuthPresetCatalog copied the preset's record back over them in the same boot, and the next start found them again: nine [model-rename-migration] lines on every ocx start, for a user who never selected any of those nine models. Skip a field the registry's own seed still publishes keyed by the retired id. Those entries are deliberate (a request naming gemini-3.6-flash routes to 3.7 and needs a window under the id it asked for), and reconciliation owns that record anyway, so the migration has nothing to repair there. Every field the user owns (models, selectedModels, retainModels, defaultModel, disabledModels) is still renamed and still reported once. Closes #5066 * fix(providers): only skip registry residue, not a value still to carry The first cut skipped any field the registry seed published keyed by the retired id, which also skipped a row that saved only the retired key. That row needs the value carried onto the supported id, and dropping it blanks the record instead of moving it (test 4/4 caught it at gemini-37-flash-migration.test.ts:211). Require both conditions now: the registry seed still publishes the retired id in the field, AND the saved field already carries the supported id, so the rename would only delete a key reconciliation puts straight back. The reporter's post-reconcile row satisfies both for all nine ids; a row with just the retired key does not, and is still migrated and still reported.
…nts (#5008) (#5079) * diag(google): project the structure of an Antigravity wire request (#5008) Adds a pure, content-free projection of the compiled Google wire body so a reporter can describe a failing request without pasting a conversation into a public issue. * test(google): pin the wire-shape projection and record the upstream contract (#5008) * diag(google): name the signature facts the projection can actually prove (#5008) Adversarial review found the two signature booleans claimed more than the call site knows: replayedCallIds conflates a client-supplied signature with a durable-store hit and misses the Antigravity session cache entirely, and a bound replay scope is not a complete one. They are now counts and a presence flag with the names they earn, and a call turn after an unrecognized role no longer reports a model turn. * test(google): stop a fixture thread id from shadowing the anchor class name (#5008) The leak assertion planted the parent thread id 'parent-a', which is a substring of the anchor class literal 'parent-and-own' the same line asserts is present. The diagnostic never carried the id; the marker collided with the class name. Both shards reported only this one failure. * diag(google): make the wire-shape projection unable to reach the request path (#5079 review) Four review defects. The projection was evaluated in argument position, so a throw inside it would have rejected buildRequest; it now goes through a lazy provider diagnostic that gates first and evaluates the builder inside its own try/catch. The item ceilings alone let a worst case serialize past the debug buffer's per-line cap, which cut the JSON while the retained prefix still claimed truncated:false; a serialized budget at half that cap now trims turn detail and the flag stays honest. The send-count assertion counted fetches around buildRequest, which never fetches, so it is replaced by a real fetchResponse exercise under a controlled executor, a send budget and onPhysicalSend. The timing claim is corrected rather than engineered away: with debug on the walk is synchronous on the dispatch path and linear in history length, and the docs say so. Also shares the session-anchor decision, the bypass sentinel and the two upstream error predicates instead of keeping second copies. * test(google): let the tighter of the two ceilings win in the long-session assertion (#5079 review) The serialized budget trims the long-session case below the turn ceiling, so pinning the retained detail to the turn ceiling alone was wrong once both bounds existed. The assertion now fixes what actually matters: exact totals, a head-anchored remainder, and a summary that fits a debug line. The dispatch comparison also stops claiming more than it checks and now compares the whole outbound body with only the per-build request id normalized out.
* test(ci): split phase timing for the four #4997 budget overruns The four cases in #4997 exceed their own budgets only when the suite runs unsharded, and pass in the sharded lanes running the same files. Three control dispatches produced three disjoint failing sets, so a per-test duration cannot say whether the time went to cold fixture setup, to a wait the assertion needs, to slow contended execution, or to a teardown still reaping a child. This records those phases from inside the fixtures. tests/helpers/phase-timing.ts emits monotonic performance.now() boundaries plus exponential progress ticks, so a log truncated by a budget kill still distinguishes slow-but-advancing execution from a stall. The heartbeat case moved to a sibling file because web-search.test.ts sits exactly at its recorded cap. * test(ci): make the #4997 phase record say something a reader can act on Review of the first commit found the progress signal unwired in two of the three instrumented files, so every tick would have reported no movement and the hang-versus-slow distinction the record exists for would have been unavailable. The web-search case now probes its send count and the provider spine its observed-capture count; the image cases already probed the encode counter, but the per-build reset sat inside the measured segment and zeroing a probe mid-phase reports movement that did not happen. The spine also claimed to separate the server bind and did not. A phase now stops reporting after two minutes rather than after forty ticks, which is nine.
…windows (#5082) Three of the four cases spend most of their budget on work the assertion is not about. The image file builds the same 1000x1000 noise PNG seven times, once inside each of the two windows that overran; it is now built once per size and shared, which no case can observe because nothing writes to the result. The provider-option spine asked the host service manager who owns its sandboxed homes, which on macOS is up to four synchronous launchctl children at two seconds apiece, and it now uses the seam that exists for exactly this. Its WebSocket and migration child were reachable only from the success path and are now released on the failure path too.
* fix(tests): judge cold-spawn warm-up registration structurally The coverage guard recorded a file as warmed when its text contained the substring helpers/cold-spawn-warmup, so a comment, a string literal, or an import left behind after the beforeAll call was deleted all satisfied it while the measured child paid the cold module-graph load again (#5060). tests/helpers/warmup-registration.ts replaces the substring with a structural judge over the file tokens: it follows the import binding from the exact helper module through aliases, finds the bun:test beforeAll registration and the scope of its inline callback, requires the call on that callback direct path, and requires the promise to reach the hook by await or return. A file it cannot read is reported unreadable rather than answered. Parsing uses the scanner from typescript/unstable/ast, the same entry point tests/responses/responses-fetch-helpers-boundary.test.ts already uses, because TypeScript 7.0.2 is the native port and publishes no in-process parser. * fix(tests): close the scopes a warm-up registration can hide in Four gaps the first pass left, each of which accepts a file that pays the cold load anyway: a namespace import warms without binding a name the judge follows, so an unwarmed disposition now requires no helper import at all rather than no binding; a hook registered in an uncalled helper or behind a false condition registers nothing, so a registration must sit at the top level or in a describe callback, unconditionally; a callback parameter of the same name shadows the import with no declaration keyword to find; and a warm-up that is one operand of the returned expression leaves the hook settling on something else. * fix(tests): end a guarded statement without needing its semicolon A semicolonless guarded registration left the conditional flag set for the next line, so the unconditional hook after it was refused too. Statement level now also ends at a line break after a token a statement can end on, with the token that begins a guarded statement exempted so if (false) on its own line still guards what follows it. * fix(tests): keep a guarded statement that the next line continues A line break only ends a statement when the next line cannot continue the previous expression. A line opening with a paren continues it, so drawing the boundary on the line break alone read the second half of one conditional consequent as unconditional and accepted it. * fix(tests): judge four warm-up shapes exactly and refuse the rest The token walker reconstructed scoping, statement and expression grammar by hand, and the gaps were false passes: an expression-bodied arrow created no scope, so a hook registered inside one read as top-level work, and a shadowed beforeAll took a correctly awaited callback while nothing ran. It now recognises four shapes - an awaited or returned statement in a beforeAll callback, a beforeAll expression body, and a module-level await - and every other occurrence of a warm-up binding is one refusal naming its line and its scope chain. Provenance is required for beforeAll and describe as well as for the warm-up name. TypeScript 7.0.2 publishes no in-process parser (typescript/unstable/ast exports the node FACTORY, and parsed trees come from the tsgo client), so the alternative to an exact accept-set is more hand-written grammar, where each wrong guess is a false pass. Refusing a legitimate file no longer blocks it: a disposition records the construct in unmodeled, and the judge must still see the warm-up and still refuse it for that reason, so deleting the call or dropping the await fails again. * fix(tests): require a suite call this judge can see running braceLayer read a block as a describe body from the callee name alone, so describe.skip and a describe behind a false condition both produced a suite scope and a hook inside one was recorded as a registration while nothing ran. A suite now counts only for a plain statement call of the bare imported name, and the refusal says which of the two it was. Adds fixtures for both, and one for warmColdSpawn, which no fixture pinned. * fix(tests): pin the member-call suite without tripping the focused-test gate The fixture named describe.skip in a string, which PR hygiene reads as a focused test in the diff. describe.each takes the same path through the judge - a callee that is not a bare identifier - so it pins the same property without the literal.
…5078) * test(ci): give the Windows nested live-lock case its own lock owner The nested live-lock regression registered only when OCX_TEST_NO_QUEUE was not 1, and the hosted Windows batch leg sets exactly that, so the case was skipped on the only platform it applies to. A controller child now owns the lock instead of borrowing the lane's: it runs with the opt-out removed for itself alone, resolves the user-scoped path through the ordinary safe path, acquires it for its own run id, and spawns the nested Bun children that must inherit it. The outer environment, the real home, and any pre-existing owner are left untouched. Closes #4991 * test(ci): bound the nested live-lock controller and harden its receipt Adversarial review of the first commit found three real weaknesses. The controller could spend more than the caller's 45s hard kill across four child spawns, so a failure path could terminate it inside a spawn with the lock still held and its teardown never reached. It is now handed an absolute deadline 10s short of that kill and bounds every child by what is left of it, minus a cleanup reserve. A join failure inside registerMember can carry a member filename, and that path runs before the controller learns its own token, so a redactor keyed on that token was blind exactly where a leak was possible. Diagnostics now strip every UUID-shaped substring, and receiptRedacted scans for one instead of being vacuously true on the green path. Two other receipts were weak: the acquire-timeout probe accepted the message without waiting, and release checked only that our own owner file was gone. They now require the elapsed floor and the planted foreign owner intact after release. childrenReaped requires the full spawn count so a skipped scenario cannot pass. lockOwned is renamed lockHeld because the controller joins an existing owner when a wrapped run already published one. * test(ci): keep the nested case's child deadline where the guard can see it Moving the per-child timeout into the controller removed this file's only spawn-options INTERNAL_DEADLINE_MS, so the cold-spawn warm-up guard stopped matching it and its disposition became an orphan. That failed test 3/4 on Linux and windows 7/9. The deadline that bounds four cold Bun starts belongs to the case that owns them, not to the helper, so the test now declares the child spawn options and hands them over; the controller only narrows them to what its own deadline still allows. The guard's inventory and its scan agree again, and the helper no longer re-derives a budget constant.
) * fix(codex): keep routed rows from inheriting experimental context Routed catalog rows are cloned from a native template, so they inherited supports_experimental_context from the account's native models. Codex reads that flag as "this model accepts experimental context history" and drives its context-management cadence from it, which on a routed third-party provider turns into a compact-after-every-step loop: a routed DeepSeek row compacted 1,600+ times in a single thread. Strip the field in normalizeRoutedCatalogEntry, next to the existing strips for supports_websockets and supports_reasoning_summaries. Validation: - bun run typecheck: clean - bun test tests/ci-workflows/file-size-ratchet.test.ts tests/e2e-style/phase100-native-parity.test.ts tests/codex-integration/codex-catalog.test.ts: 349 pass, 0 fail - bun run privacy:scan: pass - bun run structure:check: pass * docs(routing): note the native-only flags stripped from routed rows AGENTS.md asks src/ behavior changes to update docs-site/. Record the delivery flags a routed row never inherits, including supports_experimental_context and what inheriting it looked like. Validation: cd docs-site && bun install --frozen-lockfile && bun run build (465 pages built).
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
* fix(gui): keep file clients unknown on a failed-cold client read clientsSettled counted a failed-cold read as an answer, so the overview dropped the file-client rows as a server-side omission and showed the 'No installed clients were detected' panel next to the load error. Settle on the payload itself: undefined covers pre-response and cold failures while failed-with-stale keeps its real rows. * docs(pr): add the failed-cold integrations screenshot --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
…stop hosts (#5086) Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
#5089) * fix(gui): validate Claude Desktop status payloads before trusting them The status poll accepted any JSON and cached it as DesktopStatus, so an error-shaped or malformed OK payload reached the render path where status.health dereferences crashed the page. Guard the wire response and the cached entry with isDesktopStatus: a non-conforming fetch now records a cold failure and shows the existing failure presentation, and a non-conforming cache entry is treated as absent. * docs(pr): add the malformed-status Desktop screenshot --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
…ed (#5095) (#5100) A routed Command Code Muse turn reached Codex App carrying {"type":"function_call","name":"default.view_image"} and the same item for default.apply_patch. Codex has no handler for either name, answered "unsupported call", and stored both. Every later request that replayed that history was then refused before it could run: Invalid 'input[877].name': string does not match pattern '^[a-zA-Z0-9_-]+$'. A side chat opened from an 11h43m parent task hit it through automatic compaction, and no upgrade helps once the item is in stored history. The cause is two resolvers disagreeing, not a missing mechanism. undeclaredNameInItem resolves an emitted name through normalizeDeclaredToolName, which maps a default.-prefixed code-mode helper onto the declared exec (#4412) as well as a default.-prefixed bare tool (#4176), so default.view_image was ADMITTED as exec. The emit-side rewrite, normalizeDefaultNamespaceInItem, implemented only the bare-tool half, so it forwarded the name unchanged. The guard's own conclusion never reached the client. Fall back to the resolver in the rewrite, so a name good enough to admit is the name the client receives. Gated on isSchemaValidResponsesToolName rejecting the emitted name, so a name the upstream accepts cannot be reshaped by this branch no matter what the resolver would say about it. The routed-custom-tool and namespace restores run earlier in the same rewrite chain and keep owning the shapes they convert; this is the boundary check for names no restore claimed. A dotted name that resolves to nothing declared is left alone here and stays refused by the #1700 guard, which is the pre-existing behaviour and the right one: an unresolvable invalid name must end the turn visibly rather than enter stored history. A dotted name the caller itself declared is a real identity and passes through byte-identical. No local suite was run. Refs #5095
) Stopping the emission is only half the report. Codex stores what it received, so a conversation that already contains a default.-prefixed function_call name is refused on EVERY later request that replays it: Invalid 'input[877].name': string does not match pattern '^[a-zA-Z0-9_-]+$'. The task cannot be compacted or continued, and upgrading does not reach it. That is the part of the report that actually hurts: a side chat opened from an 11h43m parent hit it on a plain OpenAI model, from history a routed provider had damaged. repairLegacyDottedToolCallNames repairs the replayed item on the way out, in buildRequest beside backfillWebSearchQueries and again in compact.ts, which forwards the caller's body directly. It runs before the canonical-destination split, because the destination that refused the request was plain OpenAI. What resolves is bounded on purpose: - a dotted spelling the caller's own catalog declares is a real tool identity and is never stripped; rewriting it would change which tool the history says was called; - a suffix claimed by two declared identities is ambiguous and is left alone; - a suffix naming exactly one declared tool resolves to it; - otherwise, only the code-mode helper spellings in CODE_MODE_HELPER_WIRE_NAMES resolve, because a code-mode catalog never declares them -- they exist solely as nested tools.* helpers inside exec, so a recorded call under one of those names can only be a provider echoing the helper. There is deliberately no "strip everything before the first dot" rule. A legitimate tool name may contain a dot in another provider's vocabulary, and a replayed item names a call that already happened, which is the worst possible place to guess. Anything unresolved is left exactly as it is; the upstream still refuses it, which is the pre-existing outcome rather than a new one. Only replayed input items are eligible. The caller's tool catalog is never rewritten, call_id is untouched so a repaired call still pairs with its stored output, and a body with nothing to repair is returned by reference. No local suite was run. Refs #5095
…ansports (#5125) * fix(transport): return null bodies for 204 and 205 on raw outbound transports Both raw outbound transports build a Response from a socket, so each applied the Fetch null-body rule on its own and the two disagreed. The SOCKS helper excluded 204 but not 205; the pinned direct helper attached a streaming body to every 2xx. A 204 or 205 therefore either raised a TypeError inside the response handler or waited for a peer that was entitled to keep the connection alive. The status set now lives in one place and both owners read it. Representation headers survive untouched: a 205 that advertises a coding it never sent is neither decoded nor refused, because there are no coded bytes to act on. * docs(transport): correct the null-body note on 101 and 103 The comment claimed an upgrade is refused before the shared predicate is consulted. It is not: the SOCKS helper stops its interim-response loop on 101 and carries that status into response construction. Describe 101 and 103 as statuses outside the final Response set these helpers support, which is what is actually true, without changing any runtime behavior. * test(transport): prove the SOCKS 205 case releases its tunnel The case was named for a release it did not check. It asserted status and body and then force-closed every socket in teardown, so the name claimed more than the assertions did. It now waits, under a bounded deadline rather than a fixed sleep, for the keep-alive peer's connection to be observably destroyed, which is the release the transport actually owes.
* fix(gui): do not offer retry for the Grok coupon reset POST A transport rejection after the redemption POST is dispatched has an unknown outcome: the request may still reach the server and spend the coupon. Classifying it as a retryable network error let the operator re-submit an irreversible redemption with the same operation id. Return the aborted outcome for any post-dispatch rejection so the UI stops posting and forces a re-read of account state. * docs(pr): add the unknown-outcome coupon dialog screenshot --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
The server-configuration table in English and all seven locale copies claimed generated apiKeys are accepted by management and data-plane auth. Runtime rejects data-plane credentials as management tokens (src/server/management-auth.ts). Correct each row to describe data-plane admission only, link the locale management reference, and record the row contract in the docs structure owner. Closes #5120
…5131) tests/providers/cursor/cursor-stream-health.test.ts asserted that meaningful frames keep resetting both stream-health deadlines by running a synthetic HTTP/2 server for three times the silence budget and requiring that no deadline expire. That is an assertion about how busy the machine is. In the unsharded macOS control lane it fails with "Cursor stream stalled: no inbound frames for 5s before turnEnded" while the watchdog is behaving correctly: under contention the fixture's own frames do not arrive inside the window. The budget is already CI-scaled, and scaling is what makes it worse. With the silence budget floored at 5s the case needs 15 consecutive seconds during which no 5s gap in frame decoding occurs, so raising the floor lengthens the exposure rather than reducing it. Add a T04-only clock seam (now, setTimeout, clearTimeout), matching StageLeaseClock in src/cli/account-api.ts. Production omits it and keeps the globals; the 30s and 90s budgets, the first-frame timer, the turn-ended grace and the outbound heartbeat are untouched. The test now advances virtual time by hand between frames whose arrival it awaits, so runner contention can delay a frame without any deadline passing, and it additionally asserts that exactly one timer is armed after each frame and none survives turnEnded. No production timeout changed, no budget widened, no retry added and no platform skipped.
…ses (#5127) * fix(transport): settle SOCKS5 uploads on abort and early final responses The upload loop awaited the caller's body reader before looking at the socket at all. Aborting destroyed the socket, which does nothing to a read owned by the caller's own stream, so a stalled body kept the fetch pending - including when the peer had already answered and the response was sitting in the receive buffer. One reader now consumes the socket for the whole exchange and starts before the upload finishes. Each body read and drain wait races the caller's abort and that pending answer, so an abort rejects with its own reason, an early final response ends the upload without writing a terminating chunk into a finished conversation, and a socket failure during a stalled read surfaces as that failure. The request body is cancelled without being awaited, because a caller's cancel algorithm is free to never settle; releasing the reader is what frees the pending read. * fix(transport): type each upload race by its own outcomes One union covering both races let the compiler accept reading a chunk value off a drain result, which is the mistake the type exists to prevent. The body race and the drain race now have separate result types, and the shared answered case is its own type. The abort regression also drove its abort from the body stream having produced a chunk. A caller's stream can be pulled before the tunnel is established, so that signal did not prove the fetch was parked mid-upload. The fake upstream now reports the first body byte it actually receives, and the abort follows that. * fix(transport): preserve a non-Error abort reason on the upload race AbortSignal.reason is whatever the caller passed, and the socket reader already hands a string or an object back unchanged. The upload race wrapped anything that was not an Error, so the same abort surfaced differently depending on which race won it. It now passes the reason through, with a regression that aborts with a string and asserts identity. The abort regression also now waits on the upstream observing the request head and its first body byte before aborting. Waiting on the caller's stream having produced a chunk did not prove the fetch was parked mid-upload, because a stream can be pulled before the tunnel is established. * fix(transport): give an aborted caller back its own abort reason The helper preserved a non-Error reason, but that was not the contract the caller saw. Several waiters inside this transport can win the race that settles an abort, and they disagree about a reason they consider absent: the socket reader substitutes an Error for null, others coerce anything that is not an Error. The same abort(null) therefore surfaced as an Error or as null depending on scheduling, and the comment claiming otherwise was wrong. An aborted request now rejects with the exact signal reason, decided in one place. undefined is left alone so a hand-built signal without a reason still reaches the underlying cause. The regression covers a string, null and a plain object, each asserted by identity.
…oute (#5126) * fix(transport): decode content-coded responses on the pinned direct route The SOCKS tunnel undoes gzip and deflate; the pinned direct helper returned the coded bytes unchanged. Provider outbound chooses between the two, so the same gzip JSON was readable on one route and a SyntaxError on the other depending on operator egress configuration. The pinned helper now asks for identity unless the caller chose an accept-encoding, decodes gzip and deflate, drops the coding and the coded length once the bytes no longer match them, and refuses any other coding under a named error instead of handing over bytes no caller can parse. Only the coding the response actually carries decides this, so a preference list naming an alternative this code cannot undo is not a refusal. maxBytes now binds the decoded body as well as the bytes that arrived, because a ceiling applied only to the coded side admits a body far larger than the caller agreed to hold. The classifier is shared with the tunnel, which keeps its own error type and message. * fix(transport): keep a pinned transport failure out of the decode taxonomy Review of the content-coding change found the decoded body rewriting every non-PinnedHttpError from the pipeline into content_decode_failed. A mid-body reset and a socket error reach a decoder as "the stream failed", so that told the caller the peer had sent unreadable bytes when the connection had actually died. The source stream's failure is now recorded as it passes and surfaced unchanged; only what the decompressor itself rejected is renamed. The two new error codes also had to be classified everywhere the union is read. Antigravity quota probing sent every code except the byte cap to "timeout", so an undecodable response was diagnosed as a timing failure; both new codes now join the byte cap under "response_unusable". The Lab pinned sender switched over the four codes that existed when it was written and let anything else fall through to a raw rethrow. Both consumers now use a total map, which makes a future code a compile error rather than a silent misdiagnosis. An unsupported coding deliberately maps to nothing in the Lab taxonomy: it is a deterministic contract mismatch, and borrowing a timeout or transient code would tell Lab to wait or retry for a failure that is neither. Adds regressions for a reset during a valid coded body, cancellation through the decoder, the socket-side byte ceiling alongside the decoded one, and observed connection release before teardown. * fix(transport): classify an unreadable pinned response in the Lab taxonomy Leaving the unsupported-coding case out of the Lab map was not the same as leaving it unclassified. The executor turns an unrecognized error into harness_failure / execution_error, so a response the peer coded in a format this transport cannot undo was attributed to the runner rather than to the upstream. Both unreadable answers now carry a new unreadable_response transport code, which Lab classifies as a protocol failure: deterministic, upstream-owned, and neither a timeout, a budget, nor a harness fault. The map stays total, so a future pinned error code cannot inherit the old misattribution by default. The quota and Lab mappings are now asserted directly. Two test oracles were also weaker than their names. The byte-ceiling case sat above both ceilings, so deleting the socket-side counter could still pass through the decoded one; it now uses a payload whose coded form is larger than its decoded form, with both sizes asserted, so only the socket-side counter can refuse it. The cancellation case sent a complete small body, so ordinary end-of-stream could release the peer; it now promises more than it sends, stays open, and is cancelled after the pipeline has produced output. * fix(transport): stop tearing down a decoded body that already ended Hosted CI showed the success-path assertion failing: the case demanded the peer connection be destroyed after a complete decoded body, and it was not. The oracle was wrong, and so was the code it was pinning. A response that ended has nothing left to release, and destroying it takes a connection the agent is entitled to reuse. The identity path only closes, so the decode path now does the same. Teardown stays where a response actually ends early: a decode failure, an exceeded ceiling, and a caller that cancels. Those three now assert an observed close rather than only the error they raise, and the success case asserts the connection is left intact. * test(transport): assert end of stream, not a pooling decision The success case asserted the connection was left intact, which pins a mechanism this transport does not own: Node documents a completed connection as destroyed or pooled depending on the agent's keepAlive setting, and the runtime's actual choice is not something a transport test should encode in either direction. What a completed decoded body owes is that it ends - the decoder flushes, the stream closes and the caller reads the whole value - so that is what the case now asserts. The cleanup this path does owe is still asserted where a response ends early, in the decode-failure, ceiling and cancellation cases. * test(transport): hold the failing responses open so the close is attributable The decode-failure and ceiling cases sent their whole declared length, so the response could complete before the failure surfaced and the connection could then be pooled rather than closed - the same agent behavior the success case above deliberately refuses to pin. Both fixtures now promise more than they send and stay open, so the close each case asserts is the one this path owes for a response that ended early. The inventory paragraph is narrowed to match: teardown belongs to responses that end early, while a completed one is left to the agent. * test(lab): classify the error the sender actually rejected with The unreadable-response case asserted the rejection shape and then classified a separately constructed TransportError. That second assertion would pass even if the sender raised something else entirely, which is the opposite of what it is there to prove. Both cases now capture the rejected value, assert it is a TransportError carrying the expected code, and classify that same instance.
…storation (#5129) * fix(responses): share one progressive freeform decoder with routed restoration Routed custom-tool restoration decoded the string inside the input wrapper and emitted it immediately, while completion strips a complete outer Markdown fence for exec and apply_patch. A delta consumer could therefore receive fence bytes that do not belong to the authoritative final input, and no SSE event can take a published prefix back. The decoder the bridge already grew for this now lives in src/responses/progressive-freeform-input.ts and both paths call it. Bridge behavior is unchanged: the same function with the same arguments at the same call site, with its separate patch-envelope hold left where it was. Routed restoration keeps its stricter holds for an unrecognized JSON object and for a code-mode patch envelope, and keeps owning its retained bytes - the shared decoder is pure and leases no translator budget. Duplicate input keys and a wrapper that only becomes invalid after a valid prefix was emitted stay bounded exceptions rather than an invariant this cannot keep: completion remains authoritative, and the new sibling test documents both. * fix(responses): hold a routed apply_patch envelope the way the bridge does The shared decoder made ordinary raw input progressive on the routed path, which is the intended behavior and is what the bridge already does. It also exposed a case the older routed decoder never reached: a raw decorated `*** Begin Patch ***` envelope now streamed, while completion rewrites those markers through normalizeApplyPatchDelimiters. The decorated bytes would be published and then replaced, which is exactly the rewind this path forbids. The routed hold now covers both reasons completion rewrites an envelope, as the bridge's does: an exec body that compiles into an apply_patch helper call, and an apply_patch body whose delimiters are normalized. Namespaced tools are unchanged, since neither reason applies to a foreign grammar. The regression drives a raw decorated envelope across every chunk split and asserts no preview alongside agreeing normalized done, item and terminal input. * fix(responses): stop previewing a wrapper with a literal control character A JSON string cannot contain a raw character below U+0020, so a wrapper carrying one never parses and completion keeps the whole wrapper as the input. The decoder was appending those characters to the preview, which publishes a decoded value the authoritative item does not contain - the same disagreement a fenced body causes, reached through a different invalid spelling. It now stops instead.
…#5130) * fix(responses): refuse a continuation whose task scope does not match A task-scope mismatch correctly refused to replay another task's state, then stripped previous_response_id and continued with whatever input the request carried. When that input was only a delta, the proxy silently turned a continuation into a fresh conversation and the model answered without the history the client believed it had. Missing or corrupt state already asks the client to replay in full; a mismatch now does the same. The refusal is the existing generic previous_response_not_found response, byte for byte, emitted before any upstream send. It is returned even when the input looks complete, because this process cannot prove that it is; a caller that really has the whole conversation retries without previous_response_id and succeeds. The response reveals neither the stored scope nor whether foreign state exists, and the internal reason stays internal. Same-scope continuation is unchanged, and two absent scopes remain the legacy unscoped cohort that still replays together. This is a deliberate change to the historical fresh-start behavior, recorded in the transport contract and in the client-facing guides. * fix(responses): answer a scope mismatch exactly as an unusable id is answered The refusal was correct but distinguishable. A continuation this process has never seen records no replay failure and reaches the route-dependent refusals, while a mismatch answered earlier with a different message. That difference is itself the disclosure the issue asks to avoid: a caller could tell "state exists and belongs to another task" from "no state at all". A mismatch now falls through to those same route-dependent refusals, so both answers are byte-identical on any given route, and a destination that genuinely owns the continuation chain keeps behaving as it does today for an unknown id rather than being rejected for lacking local state. The regressions compare the two envelopes on the same route and assert neither reaches the model. * test(responses): cover the WebSocket path for a task-scope mismatch The existing WebSocket coverage exercises expired and missing replay state. A scope mismatch is the third way local replay becomes unusable, and the client has to see the same frame or Codex terminates the task instead of reconnecting with its full input. The case stores state under one task, reconnects as another, asserts the generic refusal frame with no upstream request, and then replays complete input without the id and succeeds. * fix(responses): refuse a resolved foreign continuation before native forwarding Letting a scope mismatch fall through to the route-dependent handling was wrong. An id this process cannot resolve is the destination's business, and a destination that owns the continuation chain is entitled to receive it. An id this process CAN resolve, to another task's retained state, is not the same thing: forwarding it continues that task's conversation for a different caller. The refusal now happens before that exception, on both the ordinary and combo paths, and uses the one generic local-unavailable envelope. The paired regression asserts a resolved foreign id is refused with no upstream request while an unknown id on the identical native route is still forwarded. * fix(responses): answer every local replay failure with one envelope A resolved foreign continuation is still refused unconditionally, before any route-dependent handling, so another task's conversation is never forwarded. What changed is the two remaining local-unavailable answers: the canonical forward and routed-continuation refusals carried their own message text, so which local failure had occurred was readable from the response even though all three mean the same thing to a client. They now use the message the early failures already use. Genuine upstream-owned continuation is untouched: an id with no local state at all still reaches a destination that owns the chain. The added case compares missing and mismatched state on one route and asserts identical status, type, code and message. * test(responses): produce all three local replay failures for the equality check The equality case compared a mismatch against expired state and inferred the corrupt answer from the source string. It now produces each condition for real on one route - a spill deleted, a spill overwritten with bytes that do not parse, and an entry retained under a different task - and asserts byte-identical responses with no upstream request. * test(responses): follow the unified local replay failure message Unifying the local-unavailable answers changed the canonical-forward text this assertion matched on. It now matches the single message every local replay failure uses. * test(responses): state the local replay envelope once A second assertion matched the retired canonical-forward text through /continuation state.*expired/i, which is why unifying the message turned shard four red as well. The expected envelope now lives in one constant that every local-replay assertion in this file uses, so a future change to that contract fails one place rather than several written from memory. Status, error code, upstream-send counts and retention assertions are all unchanged; nothing was relaxed to a truthy check.
* docs(structure): bind the Lab isolation invariant Add INV-LAB-01 covering both existing guarantees -- protected core entrypoints cannot reach optional Lab runtime imports, and gated Lab activation stays synchronous until startServer returns -- bind it to the existing core/Lab boundary guard, add the reverse test-side identifier, and document the contract in the Compatibility Lab structure owner. No runtime mechanism changes. Closes #5121 * docs(structure): scope INV-LAB-01 to the guard's actual coverage Review resolution: the guard walks load-time edges (static, side-effect, re-export) transitively but deliberately does not follow deferred dynamic import() specifiers; it separately forbids a protected file from naming Lab in a direct dynamic import. Restate the invariant and the Lab contract section to distinguish load-time isolation, the direct dynamic-import ban, and the sanctioned lazy /api/lab routing pattern, without broadening the runtime guard. Also record src/server/index.ts in the Compatibility Lab documents list (the activation contract lives there) and regenerate structure/INDEX.md with the canonical generator.
…5073) (#5128) The reset fixture in tests/server/server-auth.test.ts leaked its stream controller out of start() and errored it from the test body. Whether that rejection had a consumer depended on where Bun's server-side response sink happened to be: between its reads there is no pending read request to reject, so on a loaded runner the fixture's own error escaped as an unhandled error and failed the whole file. It fired on four unrelated heads (#4989, #5024, dev at ecd3ada, and #5085). Raise it from inside pull() on a stream whose high-water mark is zero instead. shouldCallPull is then true only while a read request is outstanding, so pull() runs if and only if a consumer is waiting for the next chunk, and throwing there rejects that read request. The reset now has a consumer no matter when the test calls it. What the code under test sees is unchanged: one SSE chunk, then a mid-stream body error. Closes #5073
* docs(proxy): align SOCKS5 HTTP/SSE routing documentation The proxy-format, adapter, provider-configuration, and provider-guide pages claimed HTTP/SSE never uses ALL_PROXY. Configured outbound fetch actually selects the built-in SOCKS5 tunnel for an explicit SOCKS5 proxy or a SOCKS5 ALL_PROXY when NO_PROXY does not exempt the target (src/lib/proxy-env.ts). Correct the four English pages and the seven translated provider-configuration copies that carry the claim, link the server configuration reference, and record the alignment contract in the docs structure owner. Coordinated with open draft #3901, which owns provider-specific proxy fields untouched here. Closes #5119 * docs(proxy): correct NO_PROXY and fake-IP gate semantics in SOCKS5 docs Review resolution on the SOCKS5 documentation alignment: - A server SOCKS5 proxy configured with config.proxy is written to ALL_PROXY at startup (src/config/proxy-env.ts), so both configured and inherited SOCKS5 routes honor NO_PROXY; only the per-request RequestInit.proxy override skips it. Stop conflating the two. - The fake-IP accommodation gate counts a SOCKS5 ALL_PROXY because effectiveProxyFor selects it; only a non-SOCKS ALL_PROXY does not count. Corrected in all four English pages and all seven locale copies. DNS/pinning and private-address guard caveats preserved.
* feat(structure): add versioned contract authority to the manifest Add an optional, versioned contracts registry to structure/manifest.json so each cross-cutting contract has one authoritative document anchor while the many-to-many source review map is preserved unchanged. The checker validates declared topology (unique ids, existing owner documents/anchors, dependent links) without claiming semantic correctness, and the generated index publishes the registry. Migrate the duplicated paginated-history writer paragraph in seven dependent documents to one authority link, and register the existing request-copy and stream-buffer accounting contracts. structure/AGENTS.md now distinguishes mandatory review fan-out from the smaller set of documents whose content actually changes. Closes #5116 * fix(structure): validate the manifest before rewriting the index The --fix path parsed and cast the manifest, then rendered, before any schema validation: a malformed contracts section crashed the renderer with a TypeError instead of failing with an actionable diagnostic, and could write a broken index first. Route generation through loadManifest and add regression coverage proving the named schema failure and an unchanged INDEX.md for malformed input. * test(structure): drive the real --fix CLI in the malformed-manifest regression The seam-level tests prove writeGeneratedIndex, but the original defect lived in the CLI tail. Spawn the copied script in the synthetic scaffold: malformed contracts exit nonzero with the named schema diagnostic and no TypeError, INDEX.md unchanged; a valid manifest with a stale index exits zero and regenerates it. Authored for hosted execution; not run locally per campaign restriction.
) * feat(devin): admit every inference send through the shared budget PR #5041 shipped a pre-output replay of a stated rate-limit reset and recorded what it left undone: those inner sends never reached the request-wide send budget, the provider fetch wrapper, or physical-attempt accounting. A turn could therefore make three real inference requests while the shared cap, the pacing slot and the request log each saw one. All three now go through the existing shared physical-send primitive, so a permit is reserved once per actual send, confirmed at the wire boundary, and refunded when admission succeeded but no request followed. Nothing increments a counter directly and no second counter exists. The initial send is not double-charged: the adapter reserves it as ordinal 1, and the request observer already ignores ordinal 1 because the caller records the entry send itself. A replay the budget refuses makes no inference request, records why recovery was withheld, and surfaces the provider's original 429 rather than a local budget error, so the server-stated reset and any outer cooldown behavior survive. Reservation still happens after the stated wait, not before it, so a one-hour wait does not hold a spend booking open for an hour. Catalog and JWT calls are not inference sends and keep the global fetch. * test(responses): admit the Devin row so the counted send actually happens The case asserted one GetChatMessage call and got none, in thirteen milliseconds with no adapter output: the turn was refused before the adapter ran. The devin registry entry declares authKind "oauth", and an omitted authMode inherits it, so the fixture's row demanded an OAuth credential while supplying an apiKey. It now states authMode "key", which is what the working sibling fixture in this file does and what the supplied credential actually is. Both numbers are now asserted together and carry the response status and body in the failure message, so a turn that never reaches the adapter says so instead of presenting as an empty URL list. * test(responses): route the Devin count case through a stored credential The authMode guess was wrong: the case still recorded no send. Devin is an OAuth-kind provider, and the key its adapter uses is injected onto the row from the stored credential, so a config carrying only apiKey never routes and the turn ends before the adapter. The case now seeds the credential the same way the working Devin fixture does, which is the path production takes, and the catalog is keyed to that same value. The assertion is unchanged: one GetChatMessage call and one recorded send, reported together with the response status and body so the next failure is self-describing. * fix(devin): count the first inference send where it is admitted Review found the accounting still describing an intention rather than a send. runTurnAttempt logs the attempt's first send before handing control to the adapter, which is right for a transport whose sends the caller performs. Devin now admits its own sends through the shared budget, so that first send can be refused - and once earlier combo or empty-recovery sends have spent the allowance, the log claimed a request the wire never made. An adapter that reports every physical send now says so, and for those the caller stops pre-logging and the observer counts ordinal 1 at the executor boundary that actually dispatched it. Every other adapter and call site is unchanged, including the ordinal-1 skip they rely on. An attempt-level recovery kind still labels that first send when the adapter supplies none of its own. * test(devin): pin that a refused first send is neither made nor counted The accounting fix needs the case that exposed it: an allowance already spent before this turn starts, which is what an earlier combo fan-out or empty-response recovery leaves behind. Nothing reaches GetChatMessage, nothing is observed as a physical send, and the budget records nothing - where the previous ordering would have logged a send the wire never made. The admitted case beside it still asserts exactly one call, one observation at ordinal 1, and one charged send, so the fix cannot be satisfied by counting less. * test(responses): pin the refused Devin send at the request boundary The direct-adapter case cannot see the defect it was written for. The phantom send was logged by executeResponsesRunTurn before the adapter ran, so only a case that goes through handleResponses with a RequestLogContext can prove the attempt records nothing. This one puts Devin last in a failover combo behind a chat target that spends the allowance first, which is the shape that leaves nothing for Devin's initial send. It asserts no GetChatMessage request and a Devin attempt sendCount of zero, and that the members which did send still account for themselves - so the fix removes a phantom rather than suppressing real counts. The status and body travel in the failure message. The direct-adapter case stays for what it does cover, the executor side, and no longer carries the claim that it pins the outer behavior. * test(responses): force the refusal instead of arranging it through a combo Review traced the arithmetic: a two-target combo has five total sends against a base of four, the first target settles a counted booking leaving three, the transition books the fourth, and Devin's initial send was still admitted at five. The case therefore never reached the denied path, and an optional attempt lookup let an absent attempt satisfy a zero count. The budget is now handed to handleResponses already spent, built by the real factory rather than inferred from combo behaviour. The assertions are the ones that prove a refusal: exactly one attempt, that attempt is Devin's and its sendCount is zero, no GetChatMessage request, zero total sends, and the response carries request_send_budget_exhausted. Status and body travel in the failure message. This row fails against the eager pre-log it was written for: that path recorded the attempt's send before the adapter ran, so sendCount would read one.
* feat(providers): migrate static policy consumers onto the resolver Route derive, router merge, catalog hints, gather admission, and adapter selection through the frozen ResolvedModelPolicy captured at route construction. Every RouteResult carries staticPolicy; virtual-model rewrites recapture it atomically with the wire model; credential, quota, health, and cooldown changes continue to mutate only route.provider (late binding preserved). Observed limits stay call-local through clampObservedModelLimits. No persistence serializer or snapshot format changes. Closes #5114 * fix(providers): repair four hosted failure classes in the migration Hosted CI (run 35446350736) fold: the narrow retry route shape now picks staticPolicy; baseUrl normalization no longer throws before the existing override validation; virtual-model policy recapture skips only partial callers without a prior capture, leaving the reasoning merge untouched; catalog hints restore the configuredInputModalities oracle so exact declarations override, legacy inference falls back, and clearing the row returns to inference. No existing assertion changed. Part of #5114 * fix(providers): complete the policy consolidation in the consumers Review fold: catalog hints consume the resolved policy's modality field again — the resolver now reproduces the exact-capability, colon-family, and case-fold legacy lookups, so consolidation no longer changes behavior; the legacy helper stays exported for compatibility only. The virtual-model rewrite resolves identity from the final route.modelId, with log context reporting-only, and accepts its own mutation marker only when the wire target matches the current route model. Part of #5114 * fix(providers): recapture inbound wire policy and honor captured Fast authority Review RCA fold: request preparation now recaptures staticPolicy with the actual inboundWire after every initial, fallback, and recovery route, so translated Chat/Anthropic replays resolve their own adapters while Responses keeps its captured policy; catalog Fast support derives solely from the captured fastPolicyForModel authority so an explicit supportsServiceTier:false cannot be masked by enriched metadata; and gather capture leaves an explicitly present (even empty) modelDefaultReasoningEfforts authoritative, with registry fill only when the field is absent. Part of #5114 * docs(structure): repair the cascade merge artifact in the catalog contract The stacked cascade left the parent's "consumer migration is a separate layer" sentence and a dangling duplicate fragment; this child IS the migration. One coherent paragraph retains the call-local fill, no-widen, max-input bound, and frozen gather-flight statements. Part of #5114 * test(responses): point the passthrough oracle at the captured policy The migration intentionally resolves terminal-repair policy from the captured static policy; update the source oracle's anchor to the exact new capture while preserving the real platform gate and pure native relay invariant assertions. Part of #5114 * test(providers): cover gather-captured limits and Anthropic family precedence Review coverage fold: bounded table cases prove configured low/high context/output limits survive captureProviderGather into the emitted catalog rows, and the claude-sonnet-4 family context window outranks both the provider-wide value and the dated model row. No runtime change. Part of #5114 * test(providers): cover configured limits below and above the registry Review fold: the capture table now has both rows with the positive cap merge fields (modelContextWindows + modelMaxInputTokens) — configured 200000/150000 retained below, and above-registry 1100000/950000 resolving to the registry 1050000/922000 — asserting both captured maps and the gathered catalog row. The Anthropic family precedence case is unchanged. Part of #5114 * test(codex): add fixed-phase startup diagnostics to the inject integration Diagnostic-only patch (reviewed, authored in campaign coordination): fixed-phase progress markers at file import, beforeEach, the unreadable-preimage fixture, and the synchronous spawn return, with bounded scalars and no env/paths/stdout/stderr content. All assertions and deadlines are unchanged. Purpose: identify the macOS stall phase; no root cause is claimed. Part of #5114 * docs(structure): drop the duplicated exact-only sentence Review fold: the cascade union left the capability exact-only sentence twice; one copy removed. No content change. Part of #5114
…nd (#5223) Zen gateway stalls-then-drops inference sends; the ambiguous pre-header reset became a synthetic non-replayable 429 that surfaced immediately (and logged as rate_limit_exceeded, hiding the real cause). The Go destination carries subscription inference-only traffic, so the passthrough initial send now opts into bounded reset replays there; recovery legs keep the fail-closed refusal. The log classifier also names the proxy refusal by its message instead of calling it provider throttling.
…rdicts (#5222) * fix(codex): keep transient pool refresh failures transient and surface stored auth causes * fix(codex): persist terminal pool reauth verdicts and let stored causes outrank bare marks A memory-only reauth mark carries no cause of its own, so poolAccountDto no longer names refresh_failed on its behalf: projectCodexAccountHealth then falls back to the persisted verdict, letting a stored http_status:401/403 surface as unauthorized/forbidden while the mark is still present. Terminal refresh failures found during quota probes (revoked/expired grants) are now persisted through markCodexAccountValidationFailed with terminal: true, the same verdict the token guardian writes - the in-memory mark dies with the process, and only the stored verdict keeps a cached listing from calling the dead grant healthy after a restart. Applied in both recoverPoolQuotaFrom401 and fetchFreshPoolAccountQuota. Tests: the deferred-validation matrix now expects the stored http status cause whether or not the in-memory mark survives; the pool-reauth-cause helper gains persisted-verdict and restart-simulation assertions for the dead-grant path. --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
…ated API (#5184) * fix(tray): read restart safety through the CLI instead of the admin-gated API * fix(tray): throttle startup-health probe attempts after launch failure or invalid result * fix(tray): terminate an active startup-health probe when the tray shuts down * fix(tray): clean up startup-health probes outside the online branch, on pipe failure, and on shutdown
…ut (#5225) * test(transports): cover HEAD preservation, interim answers and the response timeout Three coverage gaps the acceptance audit found in the transports work. No production change: each case exercises a branch that is already there and was not asserted. HEAD (#5109). A HEAD answer carries the headers the GET would have carried, including the length of a body it will never send. The SOCKS transport now has a case proving it answers with a null body, keeps the advertised length in the headers where a caller is entitled to it, and releases a tunnel whose peer asked to stay alive. Interim answers (#5110). A peer may answer 100 and 103 before the request body is finished, and those are not the answer. The new case has the peer send both mid-upload and asserts the caller receives the final 200, with upload continuity witnessed by the peer receiving the rest of the body including its terminating chunk. The response timeout (#5110). The branch that matters runs while the upload is parked on a chunk the caller will never produce. Waiting out the real budget would make this a three-minute test, so the timer is observed as the transport arms it and its own callback is invoked once the peer has witnessed the stall. The budget asserted is the transport's, mirrored in the test rather than exported from the module. Settlement, reader release and socket teardown are each witnessed. local checks: NOT RUN * test(transports): make the interim-answer case prove its own ordering The case enqueued the whole request body up front, so the upload could finish before the peer ever sent 100 and 103 and the claim that the rest of the body went out after them was not causal. The peer now answers the request head before any body byte, and the caller's stream withholds its last chunk and the end of the body until that has happened. Upload continuity is measured against the moment the interim answers were sent rather than against the whole exchange, so the terminator arriving afterwards says what it appears to say. The transport-side receipt is the final answer itself: reading 200 with its body is only possible for something that consumed both interim heads and went on reading, where a transport that stopped at the first would have handed the caller an empty 100. The exchange is also required to end on its own, before the fixture tears any peer down. No production change, and every existing assertion is kept. local checks: NOT RUN * test(transports): witness the interim answers arriving on this side of the tunnel The previous version released the rest of the body once the peer had written 100 and 103, which says the bytes were sent, not that the transport had them. The release now waits until both complete interim heads have arrived on the client side of the tunnel and the reader has had a native scheduling turn to consume them, and it records that the fetch is still pending and the body uncancelled at that moment, which is what says neither head was taken for the answer. The socket is observed through the timer the transport already arms, with a passive data listener; the reader is in flowing mode, so it observes the same chunks and consumes none. No production seam and no sleep. Upload continuity is still measured from the moment the interim answers were sent, the final answer and its body are still the receipt that both were read past, and the exchange must still end on its own before the fixture tears any peer down. local checks: NOT RUN * test(transports): make the interim-answer fixture fail safely The held tail lived in a stream pull that awaited a receipt which, if it never arrived, would park the fetch until the runner's own deadline. The finally block restoring the global Socket prototype would not have run by then, and the data observer was anonymous, so nothing could remove it either. The tail is now held by the test body with bounded waits on the receipt, the outcome and the response, the observer is named and removed, and the fetch is owned by an AbortController the fixture aborts on the way out. The prototype is restored first, before anything else in teardown. Every causal assertion is kept and two more are added: at the moment the remainder is released, the peer has not yet seen it or the terminator, which makes the ordering the assertions rely on visible rather than assumed. The timeout constant and the response and closure checks are unchanged. local checks: NOT RUN
* fix(codex): recognize boolean native context opt-in Accept Codex FeatureToml's explicit boolean form as well as the existing experimental_mode table in the shared injection/runtime predicate. Keep absent, false and malformed values off and leave admission, account ownership and upstream forwarding unchanged. Add opt-in, URL-isolation and runtime-revocation regressions plus English/Korean guidance for an authenticated native trial profile. The guide separates source-level feasibility from live-account support. * docs(guides): list native context guide in the sidebar Register guides/codex-native-context in the Guides sidebar group so the English and Korean pages are reachable from the left navigation. --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
…ints (#5163) * fix(vision): let an explicit custom row outrank the provider vision hints A model added as a manual custom row with inputModalities ["text", "image"] still had every attachment replaced by the omission marker. The catalog half was already right: src/codex/catalog/routed-gather.ts copies the custom row's modalities onto the advertised row, which is why the dashboard showed "text, image". The request path consulted only providers[].noVisionModels and modelInputModalities, concluded text-only, and stripped the image before dispatch. One config, two answers. The capability predicates in src/vision/eligibility.ts and src/vision/plan.ts now read the custom row for the exact provider/model identity through customRowInputModalities. Precedence is modelCapabilities (the dedicated per-model capability axis, including the ocx provider edit --text-only write), then the custom row, then noVisionModels, then modelInputModalities, then registry/vendor metadata. A custom row that declares no modalities stays silent instead of becoming a text-only claim, and the provider-only fallback used by legacy unit callers is unchanged. The new test file pins the request-path seam and the shared predicate, which is what the sidecar picker and the web-search verbalizer read. Three of its cases fail without the fix. * fix(vision): apply the capability precedence in both predicates CodeRabbit found the order violated in two places. \`isVisionSidecarConsumerWithCache\` read the custom row before \`modelCapabilities\`, and the native arm of \`modelAcceptsImageInputWithCache\` ran the sidecar-consumer check before the custom row, so a \`noVisionModels\` listing the catalog had already overridden still forced text-only on the request path. Both predicates now resolve sources highest-first: \`modelCapabilities\`, the explicit custom row, \`noVisionModels\`, \`modelInputModalities\`, then registry/vendor metadata. Two tests pin the order at the shared predicate, and both fail without this change. * fix(vision): answer "can this model see" the same way in both predicates Maintainer review on #5163 found the custom-row branch applying a narrower test than the `modelCapabilities` branch above it. `requiresVisionPreprocessing` asked for `includes("text") && !includes("image")` while `modelAcceptsImageInput` answers "no image" for the same declaration, so a row declaring only `audio` or `video` was image-incapable to one and an image target to the other. Both now use the same rule: image is absent from the list. `usableRoutedVisionModel` also ANDed a provider-only `isModelTextOnly` on top of `modelAcceptsImageInput`. That second predicate never reads a custom row, so a describer the operator had declared image-capable was refused for the provider hint that same row overrides; the check could only subtract and is now gone. `structure/config.md` said the catalog overlay reads the field through `customRowInputModalities`; it copies `customModels[].inputModalities` directly. Only the request-path predicates go through the helper, and the note now says so. Three tests come with it: the audio-only row, the routed describer a custom row unlocks, and the existing ones stay. The first two fail with the source fix stashed. * docs-site: state the image-absent rule for custom model rows The maintainer review noted the public page still described only the text-only case, while structure/config.md already records that a row declaring only audio or video is image-incapable in both predicates. The public page now carries the same sentence, so the two documents agree.
…5164) * fix(registry): reclassify the Zen Go DeepSeek route as native vision The OpenCode Go deepseek-v4.1-flash route was declared text-only and listed in noVisionModels, so every attachment was replaced by the vision-sidecar omission marker before dispatch. That classification came from jawcode metadata and went stale: probed 2026-09-19 against the gateway with this proxy's own headers, the route accepts an image_url part and the model reads it correctly. Correcting the registry only fixes new installs. enrichProviderFromRegistry fills noVisionModels all-or-nothing and fills modelInputModalities per-key beneath the saved value, so a config saved while the stale seed was current keeps both halves of the claim forever. The new startup projection rewrites exactly those two saved values, guarded by an exact match on the stale declaration, in the shared repair pass. The sibling deepseek-v4-flash still answers HTTP 400 "Model only supports text input" on the same gateway and keeps its classification. The Zen tiers could not be probed (HTTP 402), and an unverified tier is not evidence, so they are unchanged. * test(providers): rename the migration test out of the service seed The layout seed regex for the `service` domain matches `stale-`, so the file name placed the new test in the wrong domain under the seed-only oracle that guards brand-new files. The name now follows its sibling context-window-seed-repair. * docs(structure): scope the Zen statement to the unmeasurable tiers CodeRabbit found both notes still saying "Zen routes are unchanged and unprobed" directly beside the recorded OpenCode Go probe. Only the Zen tiers (\`opencode-zen\`, \`opencode-free\`) were unmeasurable (HTTP 402); naming them removes the contradiction and keeps the note from claiming more than was verified. * fix(providers): finish a half-migrated vision row, not just the full stale pair Maintainer review on #5164 found the repair firing only when `modelInputModalities` was byte-for-byte the stale `["text"]`. A config whose modalities had already been corrected but whose id was still in `noVisionModels` was left alone, and the sidecar predicate reads that list BEFORE the modality list — so the row kept stripping images with every saved value looking repaired. The projection now handles both states a running process can be in: the full stale pair (both values rewritten) and the half-migrated row (the name is dropped, the modalities are already right). The paired modality declaration stays the guard in both, which is why a listed name with no declaration beside it is still left alone: that row is either a half-finished repair or a deliberate operator entry, and the projection does not guess which. Flagged in the reply as an open question rather than decided here. Nothing writes `modelCapabilities`. It outranks every source this file touches, so it is where a deliberate text-only override survives a restart; the module docstring and `structure/providers-and-adapters.md` now say so. Tests: the half-repaired row, the listed-name-without-declaration row, and a guard that the dedicated axis is never rewritten. The old expectation that a corrected modality value protects the list entry is replaced by the behaviour the review asked for. * fix(providers): resolve the stale vision repair through the registry transport providerMatchesRegistryTransport is the rule enrichProviderFromRegistry applies before it writes registry metadata, so the projection now answers row identity the same way and inherits the entry's preserveCustomDestination opt-in. Adapter equality stays as an additional tightening. The opencode-go entry has no preserveCustomDestination, so for it the helper resolves to the name check enrichment already relies on; the change follows the shared rule instead of a hand-rolled one. Two tests pin both directions: baseten, which does opt in, is skipped when a same-named row points at another host and repaired when it points at the registry destination. * test(providers): pin the sidecar routing the modelCapabilities axis forces The maintainer review noted that the modelCapabilities test only asserted the axis itself was left untouched. It now also asserts the repaired row (modalities rewritten to text+image, name dropped from noVisionModels) still routes through the vision sidecar, because requiresVisionPreprocessing reads that axis first.
* fix(codex): hand off affinity in refresh flight A shared refresh flight deliberately outlives the request that opened it: an aborted owner stops waiting while the detached work still commits G+1. Affinity handoff only ran on the caller path gated by selfRefreshed, so a detached commit stranded thread bindings at the old generation until the next selection released them — losing the prompt-cache affinity instead of advancing it. Register generation-dependent completion with the flight itself so it runs exactly once per committed result, for every waiter including none. * fix(codex): hand off propagated aliases and contain handoff failures A dormant same-grant alias commits the rotated credential at its own generation, so handing off only the owner left the alias's thread-affinity entries stranded at the old generation where the exact-generation liveness check fails them. Dispatch the handoff for each propagated alias at (alias.generation - 1, alias.generation). A throwing handoff listener also rejected the shared refreshPromise after the credential was already persisted: surviving waiters saw a refresh failure that never happened, and plan reconciliation was skipped. Each listener now runs independently inside the flight settlement with failures contained and logged without account ids or token material. --------- Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
* feat(integrations): own value-free mutation planning in a pure seam An operator confirming apply, overwrite, disable or undo agrees to consequences nobody has shown them, and the only way to show those consequences safely is to compute them somewhere that cannot write. src/integrations/mutation-plan.ts is that place. It owns the closed operation, change-kind and foreign-edit vocabularies, the bounded plan shape, managed schema path canonicalization, deterministic ordering with deduplication and a cap, and a versioned fingerprint over every input a plan's authority rests on. It holds no IO, takes no lock, and does not import the writer, so the direction is state/ownership/merge into the planner and the planner into the writer. Two decisions are load-bearing. A path segment that is not representable in the managed grammar invalidates its whole path rather than being skipped, because a skipped segment names a different place than the one being changed, and an ownership record on disk accepts arbitrary strings and is not a validation authority. And the fingerprint covers the model snapshot and, for restore, a digest of the snapshot's actual bytes rather than only its operation id: the contribution is derived from the models, and the bytes are what would land in the user's file. RefusalReason moves here so a plan can report a refusal without depending on the module that performs writes. The writer re-exports it, so every existing caller is unaffected and no second vocabulary exists. No behavior changes: nothing calls the planner yet. local checks: NOT RUN * refactor(integrations): share one observation between preview and mutation A preview that reads the file separately from the mutation it authorizes can disagree with it, and then the operator confirms one thing while another happens. The read, parse, contribution build, record selection and classification now live once in the planner, and the writer consumes that result instead of performing its own. The move is behaviour-preserving for mutation, including refusal ordering, which is load-bearing. What changes is that the two effects a preview must not have are named rather than implied: pending-prune maintenance and Cline transaction recovery both write, so they are explicit options with no default. Mutation passes both; a preview will pass neither. Leaving them defaulted was the more dangerous option, because a preview would inherit a write while still being called a read. Refusals cross the boundary in the planner's own vocabulary, so the planner keeps no dependency on the writer's result type; the writer translates them back into WriteRefused, preserving the Cline residual marker. IntegrationWriteInput moves down with the observation that consumes it and is re-exported, so the management routes, the Aside adapters and the existing tests are unaffected. local checks: NOT RUN * test(integrations): pin the plan projection and fingerprint contract Covers the three decisions the plan seam exists to guarantee. A path segment that is not representable in the managed grammar invalidates its whole path rather than being dropped, because an ownership record accepts arbitrary strings and a partially dropped path names a different place than the one being changed. A selected member collapses to a wildcard, so which entry was chosen is not published. The fingerprint moves for every input a confirmation rests on, including the model roster, since the contribution is derived from it, and it distinguishes an absent file from an empty one because restoring over each is a different operation. For restore it moves when the snapshot bytes change while the operation id and snapshot kind stay the same: binding only the id would leave the bytes that actually land in the user's file outside the confirmation. Reported changes are deduplicated, ordered and capped, and the result is frozen so a caller that sorted it in place could not edit shared plan state. Registered in both test-layout inventories. local checks: NOT RUN * fix(integrations): bind install state and admission to the plan fingerprint Review found that a confirmation could survive two changes it should not. The fingerprint bound the detect directory's path but not what was actually there, so an uninstall left every other component identical and the token still matched. It also bound nothing about admission eligibility, so a config change that makes the integration an illegal target was invisible to a plan that had already been shown. Both are now inputs: the observed kind of the detect directory, which is what the not_installed refusal is derived from, and whether admission policy blocks the client, which is the non_loopback refusal's input. A plan is a claim about a decision, so every input that decision is derived from has to be inside it. local checks: NOT RUN * fix(integrations): publish only declared managed paths, as templates Review found the path rule was an allowlist wearing a grammar's clothes. Any alphanumeric segment passed, and Kimi writes one fragment per model at models.<alias>, so a user's model identifier would have been published verbatim. The same rule accepted any plain path sitting in an ownership record, and a record on disk is not a validation authority. Each client now declares where its managed fragments live. A path is published only when it matches one of that client's templates exactly: static segments must match literally, a dynamic position accepts any observed segment, and the string that leaves the planner is the template rather than the observed path. Publishing a value is therefore structurally impossible rather than merely unlikely, and a path nobody declared fails closed instead of being described. The declarations are checked against the client union, so adding a client without saying where it writes is a type error. Regressions cover the Kimi alias, a foreign static segment, another client's shape, extra depth, and that every shipped client has a declaration. local checks: NOT RUN * test(integrations): prove real builder paths canonicalize, not just that templates exist The template table is a second copy of what the exporters do, and the earlier case only asserted each client had a non-empty list. That would stay green while a client's actual fragment path drifted out of its declaration, which is the one failure the table exists to prevent. Each shipped client's contribution is now built from a bounded fixture context and every emitted fragment path is canonicalized through that client's own templates. Any segment the templates do not name must not appear in the published path, so Kimi's per-model alias is checked against real builder output rather than a hand-written example. local checks: NOT RUN * feat(integrations): assemble the value-free plan an operator confirms Turns the shared observation into the plan itself: the managed places the operation would touch, the history it would write, the foreign-edit status, and whether it can proceed at all. Refusals are derived in the writer's own order, which is not cosmetic. An uninstalled client is reported as not installed rather than as whatever its leftover file classifies as, and an unreadable file is reported before either, because that is the sequence the writer refuses in. A plan naming a different reason than the mutation would name is worse than no plan. Overwrite stays the one operation allowed through a conflict the operator has been shown. Drift is passed in rather than recomputed, so the plan and the mutation read it from the same comparison, and it is bound into the fingerprint with the rest of the restore inputs. A path that does not canonicalize is omitted rather than guessed at. For a shipped client that cannot happen and the parity case proves it; what it covers is a record written by another version, where declining to describe a path is the honest answer while the fixed ownership entry still tells the operator that ownership changes. A refused plan reports no places and still carries its reason, because knowing undo is blocked by an expired backup is the answer an operator needs. Regressions cover the change list, replacement versus addition, refusal ordering, overwrite through conflict, expired and drifted restore, and that neither a configured value nor a filesystem path reaches the response. local checks: NOT RUN * feat(integrations): add the read-only preview entrypoint previewIntegration runs the shared observation with both write-capable effects off, so it prunes nothing, recovers nothing, takes no lock and enters no mutation flight. Everything it reads is a read: the target file, the ownership records, and for restore the journal row and its snapshot. When observation itself refuses, the plan carries a fixed unbound token rather than a fingerprint. Such a plan never read the state it would have bound, so there is nothing to bind, and sharing one value is safe precisely because canApply is false and only a plan that could apply may be bound to a mutation. A restore whose operation id is unknown or belongs to another client is refused without saying which, since naming it would report on journal contents the caller did not select. The filesystem regression proving preview leaves the config, store, snapshots and journal byte-identical is the next commit; it needs a temp home whose path resolution is verified rather than assumed. local checks: NOT RUN * test(integrations): prove a preview leaves the filesystem untouched The feature's whole promise is that looking changes nothing, and that is a claim about the filesystem rather than about the code's intentions. This drives the real preview against a temp home and a temp store, then compares the client file's bytes and every file under the store before and after. The path resolution is asserted rather than assumed. Every client resolves its config through the home argument today, and a client that stopped doing so must fail this test loudly instead of quietly reading somebody's real configuration. Also checks that the plan describes places and not contents: neither the home path nor the proxy address appears anywhere in it. local checks: NOT RUN * fix(integrations): decide each operation the way that operation decides Review found the plan applied one global refusal sequence to all four operations, and the writer does no such thing. Apply checks installation and admission before anything the classifier says, reports a conflict ahead of unsafe, and treats an already-current file as a success that writes nothing. Disable never asks about installation or admission at all: removing what we wrote from a file that still exists is meaningful whether or not the client is installed now, and it emits nothing admission policy could object to; an absent block is a no-op rather than a refusal. Restore stays on its own journal, snapshot and drift questions. A plan is only useful if it reaches the same verdict, for the same reason, as the mutation it describes. Two consequences follow. An operation that would write nothing now says so and names no places, because reporting a snapshot and a journal row for a no-op describes consequences that never happen. And whether a managed place is occupied is read from the document rather than from our own record, since deciding from the record called an overwrite of somebody else's key an addition, which is the one situation overwrite exists for. The filesystem proof now snapshots the entire temp home rather than the target file alone, so a writer lock or a marker sibling appearing next to the file would fail it. It seeds pending maintenance and asserts it is still pending afterwards, and it leaves an interrupted client transaction in place to show that a preview repairs nothing. local checks: NOT RUN * feat(management): let a read-only caller bring its own model roster A preview has to show what a writer would write without performing the gather, because the gather reaches providers and, through the initial-selection finalizer, can persist configuration. That is not something a read may do. Only the gather is the problem. Everything after it is the projection: the disabled computation, native and account-bound rows, custom rows and the public list. Swapping the gather while duplicating the projection would be worse than leaving it alone, because the two copies would drift and a preview would then disagree with the commit that follows it for a reason that has nothing to do with the roster changing. So the roster becomes a parameter and the projection stays exactly where it is, shared by construction. Supplying it is the only way to skip the gather, and skipping the gather is the only thing supplying it does. local checks: NOT RUN * feat(management): serve integration mutation plans on read-only routes Two POSTs that write nothing: one plans an apply, overwrite or disable for a named client, the other plans an undo for a journalled operation. Both build the same input a mutation builds and differ in one deliberate way, the roster comes from the read-only path that skips the initial-selection finalizer, so a preview and the commit that follows cannot disagree except when the state truly moved. A restore preview answers an unknown operation as not found rather than reading the row back. It runs before any confirmation, which makes it the cheapest place in the system to probe journal contents, and it declines to be one. They are POSTs because the operation descriptor is a bounded body, and they are registered as non-mutating with an interactive-preview exemption: there is no standalone thing for a CLI to do with a plan, since it is only meaningful to the caller about to commit it and a scripted caller drives the mutation directly. local checks: NOT RUN * test(management): cover the preview routes against the real temp home Plans an apply through the route and asserts the client file is byte-identical afterwards, that the response names places rather than locations, and that a malformed operation or an unknown client is refused before any planning runs. An undo for an operation that is not there answers not found rather than reading the journal row back to the caller. local checks: NOT RUN * fix(integrations): plan an undo the way the writer performs one Restore was being previewed through the general observation, and the writer's undo path reads a different specification. It compares the resolved config path against the one the journal row was recorded for, reads the snapshot, and reads the target's bytes. It never parses and never classifies. Three consequences, all wrong in the direction that matters. A file that was readable but unparseable classified unsafe, so the preview refused an undo the writer would perform, denying the operator their backup at the moment the file is in the state that most needs one. Only the row's client was checked, not the path it was recorded against, so a row from a previous home previewed as applicable when path equality is the single thing that undo exists to enforce. And the coordinated restore's refusal for writer-lock clients with no client home was never applied. observeRestore now mirrors that order exactly and derives state from bytes alone: a missing target is absent, one that no longer matches the recorded result is a conflict, anything else is current. Admission is not consulted, because restore emits nothing an admission policy could object to and the writer does not consult it either. The places an undo touches come from the provenance the row carries rather than from whatever a record says today. local checks: NOT RUN * fix(management): never gather a model roster to serve a preview Skipping the initial-selection finalizer was not enough, and the reviewer was right to push back. Discovery itself refreshes credentials and writes the provider model cache, so a preview that gathered was still a write wearing a read's name. That the models list already performs a gather describes what a GET happens to do, not what a preview is permitted to do. previewExportModels now reads already-captured per-provider cache entries and never fetches. When no provider has one there is no honest snapshot to plan against, so the route answers a bounded refusal instead of triggering discovery to manufacture one; the operator opens the models view or performs the mutation directly, both of which are allowed to gather. The route regression asserts that refusal and that the client file is untouched, which is the property that mattered in the first place. local checks: NOT RUN * feat(management): bind a confirmed plan to the mutation it authorizes Toggle and restore now accept an operation and a plan fingerprint together, and re-plan before the writer runs. A fingerprint that no longer matches returns 409 with a freshly computed plan, so the operator decides again against what is true now instead of confirming something that has moved. Both fields or neither. A half-bound request is refused rather than quietly treated as unbound, because dropping one half would answer 200 to a caller who believed their confirmation was being checked, and a confirmation naming a different operation than the request performs is not a confirmation of that request. The fingerprint is an optimistic token and never authorization: management authentication and every ownership rule still decide whether the mutation may happen at all. Existing unbound callers are unaffected. Validation currently runs immediately before the writer rather than inside its lock, which closes the window an operator can observe but not the one between this check and the lock; moving it under coordinatedWrite is the next step and the writer's own compare-before-commit guard remains in place meanwhile. local checks: NOT RUN * fix(integrations): check a confirmation where the lock already holds Validating the fingerprint just before calling the writer left a window between the check and lock acquisition. Narrow, but the wrong kind of narrow: the whole point of binding a confirmation is that it describes the state the mutation is about to change, and a check outside the lock describes state from a moment earlier. coordinatedWrite now takes a revalidation hook and runs it after the input is frozen and the lock is held, before any side effect. Clients without a writer lock, and the case where an absent client home means no lock is taken, run the same hook immediately before the operation, which is as close as those paths allow. The route keeps the fresh plan from that hook so a stale confirmation still answers 409 with something the operator can decide against, rather than a bare refusal that tells them only that they were too late. local checks: NOT RUN * fix(integrations): check an Aside confirmation before its preference write Aside is the one place where checking under the writer lock is provably too late, and the code already said so. mutateAsideProfiles persists the user's preference before any writer runs, under a comment noting that the await precedes model loading, writer preflight, snapshots and all client writes. restoreAsideProfile persists and then imports the operation, which captures a snapshot and appends a journal row, before the coordinated restore. A confirmation validated under that lock would therefore fire after it had already rewritten the preference and the history it was meant to protect. Both now take a revalidation hook and run it once profile and path selection is frozen, which is everything the check needs, and before anything is written. Bulk mutation is unaffected because one fingerprint cannot honestly bind several independently changing files. local checks: NOT RUN * test(management): a bound change cannot commit on a plan that fails to validate The case worth pinning is not which refusal comes back but that none of them lets the write through. A refactor that dropped the binding on the way to the writer, or moved the check somewhere it no longer runs, would turn this into a 200 and a modified file. Asserts the refusal and that the client file is byte-identical afterwards, accepting either reason the plan can fail to validate, since both mean the same thing to the operator: the change they confirmed is not the change that would happen now. local checks: NOT RUN * fix(integrations): answer an expired backup and a vanished file the way the writer does Two exact divergences from the writer's undo path. Expiry is now decided before the target is read. Reading first meant an expired backup over an unreadable file reported the file as the problem, when the answer the operator needs is that the backup is gone. And drift decides the state before absence does. A row that recorded a file and now finds none has drifted; calling that absent described a missing file as an ordinary undo while the writer refuses it pending confirmation. Absence is only honest when the row recorded absence too, and both cases are pinned. local checks: NOT RUN * fix(management): preview reads a finished roster, not raw provider caches The per-provider cache was the wrong source and the re-review was right to block it. Static and forward providers return without ever populating one, so a static-only install would have refused previews permanently; a mixed config would have silently dropped the cacheless providers; and the cache sits before the retention, metadata, augmentation, combo and filtering that decide what a client is actually given. So preview reads the completed projection instead. An ordinary export load retains its final result, keyed by a digest over the provider graph's shape, the blocklist and custom models, and a preview returns that only when the key still matches. Changing any of them retires the snapshot rather than letting a plan describe a roster the user no longer has. Credentials are not part of the key and are not read here. A cold process has no snapshot and the route answers a bounded refusal until the ordinary models path populates one, which is a recoverable state rather than a read that gathers. No second gather exists anywhere on this path. local checks: NOT RUN * fix(integrations): actually check a bound undo, and bind it to one roster Two defects review found in the binding, both of which made it weaker than it appeared. restoreIntegrationCoordinated has its own coordinated path and never called the revalidation hook, so a bound restore carried a fingerprint that nothing ever compared. It now runs the hook in both branches, with the lock held where there is one, before the snapshot, the write and the journal row. And a bound toggle built its writer input through the refreshing loader before the guard ran, while the guard re-planned against the passive snapshot. The mutation and its own confirmation check would then disagree about the roster by construction, which is precisely the disagreement the binding exists to detect. A bound request now uses the passive snapshot for both; an unbound one keeps its existing path unchanged. local checks: NOT RUN * fix(management): never silently drop a preview binding on the Aside routes The pre-persistence hooks added for the Aside paths were unreachable over HTTP, because these routes neither parsed nor forwarded the binding fields. A caller sending them got a mutation that proceeded unchecked while believing their confirmation was being verified, which is worse than either honouring or refusing it. Both routes now parse the fields strictly: together or not at all, the operation must agree with the change being requested, and an exact profile is required since one fingerprint cannot honestly describe several independently changing files. A well-formed binding is then refused outright, because the profile-scoped plan owner these routes would need does not exist yet. That refusal is deliberate and temporary. It removes the silent-drop failure now; honouring the binding needs the Aside preview owner, which is the next unit. Unbound callers, which is every caller today, are unaffected. local checks: NOT RUN * test(management): prove a confirmation is compared, not merely required The existing case accepted either refusal, so it could pass without a fingerprint ever being compared. It proved that a bound request without a valid plan does not write, which is worth having, but it could not distinguish a working binding from one that refuses everything. This drives the real sequence: populate the passive roster snapshot, preview an apply, commit with that exact operation and fingerprint, then replay the same confirmation. The commit succeeds once and the replay comes back stale with the file byte-identical to what the first commit produced. The replay is the case that matters. Without a genuine comparison it would apply a second time, because nothing else in the request distinguishes it from the first. local checks: NOT RUN * fix(management): carry the binding into every path that can mutate Review found two ways a bound request still reached a writer without its confirmation being examined. The generic restore route parsed the binding and then called the Aside restore handler with only the operation id and drift flag, so a bound Aside restore slipped past the rejection that route had just gained and executed unbound. The binding now travels with the request. And a bound restore built its writer input through the refreshing loader before the guard, while the guard re-planned from the passive snapshot, so the mutation and its own check disagreed about the roster by construction. Bound restores now use the passive input for both; unbound restores keep the old path. Also drops a provider field from the roster snapshot key that does not exist on the config type, which is what broke the typecheck gate. local checks: NOT RUN The Aside profile binding is still refused rather than honoured, which is an interim state and not the finished requirement. * fix(management): forward the binding through the nested Aside restore too The nested profile restore adapter rebuilt the request body from the two fields it cared about, which dropped any binding before the handler that would have examined it. A bound restore on that path therefore reached the writer with its confirmation unexamined, the same defect just fixed on the generic route and in the same shape: a body reconstructed field by field silently loses anything the reconstruction does not know about. Both Aside entry points now forward the fields as sent. local checks: NOT RUN Bound Aside profile changes are still refused rather than honoured. That is a fail-closed interim, not the delivered feature. * fix(management): validate the roster the mutation will actually write The guard rebuilt its own input from the current snapshot instead of planning the one the coordinator had frozen. An ordinary export load can replace that snapshot at any moment, and nothing serialises it against the writer lock, so the check could validate a fingerprint computed from one roster while the mutation went on to write from another. That is the exact failure binding exists to prevent, reintroduced inside the mechanism meant to prevent it. Revalidation now plans the coordinator's frozen input. Whether the snapshot it came from is still current is a separate question, answered by comparing an opaque generation identity captured with the input, so a replacement is detected without ever swapping the roster the mutation is about to use. On mismatch the response carries a plan built from the new snapshot, and the old one is never committed. The generation identity also gives the roster snapshot a real version rather than a value inferred from its contents. local checks: NOT RUN * feat(integrations): plan one Aside profile's change without performing it The Aside binding could not be honoured because nothing produced a plan an Aside route could compare a fingerprint against. This is that owner. A profile is the unit, because a fingerprint can only honestly describe one independently changing file. The scope it builds is the same scope the mutation uses - that profile's store, IO and resolved path pair - so the plan describes what would actually happen rather than an approximation. The roster comes from whatever the caller injected, so a preview inherits the caller's no-gather guarantee instead of reaching for a second source of models. Nothing calls it yet. Wiring it into the profile routes and the revalidation hooks, and deleting the interim refusal, is the next commit; the refusal stays until the guard that replaces it exists. local checks: NOT RUN * feat(management): honour a confirmed plan on Aside profile changes Replaces the interim refusal with the real guard. A bound profile toggle or restore now re-plans that exact profile scope and compares the fingerprint before anything is written, and a mismatch returns 409 with the fresh plan. Placement is the whole point here. The hooks this passes through sit ahead of the preference write and the journal import, both of which happen before any writer lock is taken, so a check under that lock would fire after the thing it was meant to prevent. The guard and the mutation share one roster by construction: a bound request replaces the injected models with the passive snapshot for both, so they cannot disagree about what would be written for any reason except the state genuinely moving. Without a cached roster the request is refused rather than planned against a gathered one. A binding still requires an exact profile, since one fingerprint cannot honestly describe several independently changing files, and bulk profile mutation stays unbound. local checks: NOT RUN * fix(management): take the roster and its identity in one read The roster was captured by an awaited call and its snapshot identity read afterwards, so a concurrent export load could publish a new snapshot in between. The guard would then hold the identity of one snapshot while the mutation held the roster of another, and the check would pass by validating the wrong pair. previewExportSnapshot returns both from a single synchronous read of one reference, and every bound path now carries that pair: the input is built from its roster and the guard compares its identity. Nothing reads the identity independently afterwards. local checks: NOT RUN * fix(management): keep the roster snapshot independent of its producer Freezing the array left the model objects shared with the caller that produced them, so anything editing one in place would have silently rewritten the roster a later preview plans against, and moved the fingerprint with it. The snapshot is now a deep clone. Also corrects the recovery claim in the comment. I had named /api/models, which calls listManagementModelRows and therefore populates nothing. The path that does populate it is the Integrations collection read, which is the page an operator must open before they can confirm anything, so a cold process recovers through the ordinary flow rather than a special step. local checks: NOT RUN * test(management): a refused confirmation writes nothing anywhere Asserting the target file's bytes was too narrow. A snapshot, an ownership record or a journal row written on the way to a refusal is still a write, and those live in the store rather than in the client's config, so the old assertion would have passed straight through them. The refusal case now compares the whole store listing before and after. local checks: NOT RUN * fix(management): clone the roster on the way out too, and check writes by content Two halves of the same mistake, both found by review. The retained snapshot was cloned on the way in but handed out by reference, so a reader holding those objects could edit the roster every later preview plans against without going near this module. Reads now clone as well, with the same atomic identity, and the regression mutates both a model and a nested array through a read before asserting a fresh read is unaffected. And the zero-write assertion compared file names. A journal append, an overwritten ownership record and a replaced snapshot all leave the listing identical, so it would have passed straight through every one of them. It now compares content. That test also stopped accepting either refusal. With a roster present the only way to reach 409 is a fingerprint comparison, so it asserts exactly that and the case can no longer pass without the binding being compared at all. local checks: NOT RUN * test(management): pin the interleaving the identity check exists for Publishes a new roster from the lock-acquisition seam, which puts the replacement exactly where it is hardest to notice: after the request captured its input and before the guard runs under the lock. A guard that re-read the roster at that moment would validate the new one and write the old. That is the failure the captured-pair ordering exists to make impossible, and until now nothing proved it stayed impossible. The commit comes back stale and the client file is untouched. local checks: NOT RUN * test(management): carry a bound undo all the way through Every binding case for restore so far was a refusal, which proves the guard can say no but not that a correct confirmation still reaches the writer. A guard that refused everything would have passed all of them. Applies, previews the resulting journal row, commits the undo with that exact operation and fingerprint, and asserts the file actually changed back. local checks: NOT RUN * feat(management): route the Aside profile preview The profile plan owner existed but nothing served it, so the dashboard had no way to obtain a plan for a profile it was about to change. A bound profile request could only ever have been refused, because no caller could get the fingerprint it needed. POST on a profile's preview path plans that one profile, using the passive roster like every other preview and refusing when none is cached. An unscoped preview is rejected: a plan describes one profile, so there is nothing for an unscoped one to describe. Also extends the interleaving case to witness the whole store's content rather than the target file alone, since a snapshot or journal row written before the guard refused would be invisible to a bytes check on one file. local checks: NOT RUN * docs(structure): record who owns a mutation plan and why it is placed where it is The integration contract gains a read-only plans section covering the single shared observation, the strict dependency direction, the two effects a preview declines by name, the declared path grammar, and the fingerprint's inputs and placement. Aside is written down as the exception that proves the placement: it persists preferences and imports journal rows before any writer lock, so its check runs earlier rather than under one. The management API contract gains the three preview endpoints, what they refuse and why, and the paired binding rules on the mutation routes. Both say plainly that the fingerprint is an optimistic token and never authorization, because that is the sentence a future change is most likely to forget. local checks: NOT RUN * fix(management): retire the roster snapshot when discovery moves underneath it The config key could not see the case that matters most: the same configuration resolving to different models because a discovery completed. A plan built before that and confirmed after it would have described a roster the provider no longer has. The snapshot now carries the model cache's own generation for each configured provider, and a preview is only served while that still matches. Reading it needed a passive observer. captureModelCacheGeneration seeds an entry for a provider it has not seen, which is right for a discovery about to run and wrong for a caller that must change nothing, and isModelCacheGenerationCurrent inherits that. observeModelCacheGeneration reads the same stamp and treats absence as zero, so observing a provider cannot alter what a later capture returns. This is the gathered half of the authority. The configuration half is still a content digest over a hand-maintained field list, because neither candidate generation owner advances on the config writes that change a roster; that remains open rather than papered over. local checks: NOT RUN * test(management): assert what a successful undo actually does The bound-restore case read the config file back after the undo, which cannot work: Hermes has no file before the apply, so the snapshot apply takes is "none" and a successful undo deletes the file rather than rewriting it. The assertion would have thrown on a passing implementation. It now asserts the file is gone and that the journal carries both the restore row and the apply it undid, which is where the evidence actually lives. local checks: NOT RUN * fix(models): retire a derived roster on publication, not on authority change The stamp I bound was the cache generation, and that advances when an authority clear revokes an in-flight discovery's right to publish. It does not move when a discovery succeeds and publishes different rows, which is the case a derived roster most needs to notice. My regression incremented the generation by hand and so never exercised the real trigger. A separate revision now counts accepted publications and removals. It is deliberately not the generation: moving that on a successful publication would cancel cache writes that are still legitimate, which is the opposite of what it is for. Reading stays passive, with an unseen provider at zero rather than a seeded entry. The regression drives a real setCached with changed rows. The configuration half of this authority is still a content digest over a hand-maintained field list, as noted before, and is still open. local checks: NOT RUN * fix(integrations): plan the undo row the mutation will actually use An Aside profile restore resolves a journal row through findAsideOperation, which selects a specific row from a specific store, and Aside can legitimately hold more than one valid copy. The preview re-resolved from a reconstructed scope, so a plan and the mutation it authorized could each pick a different valid row. The fingerprint then compared two plans that were never about the same operation. The route resolves once and passes the exact row and its source store into both the preview and the guard, and neither resolves again. The general restore observation accepts a pre-selected row for the same reason. This is the same shape as the roster substitution fixed earlier: the check was correct in form and attached to a different object than the mutation used. local checks: NOT RUN * fix: unblock the typecheck and finish carrying one selected row and one content revision Four corrections. observeRestore took a parameter named resolved into a function that already declares a local of that name, which is the compile error that took the whole gate red. The parameter is now selectedOperation. The Aside restore mutation still resolved its own copy of the journal row after the preview and guard had been given an exact one, so the three could disagree on a duplicate store. It now accepts the selected row and uses it. A wholesale cache clear empties the map, so no per-provider counter can record it and every derived roster would have read as unchanged. The observed revision now carries a global term that a clear advances, which also lets the per-provider entries be dropped in the same step without an ABA when a provider returns. Dropping those entries on a clear is also what stops the map growing with providers that no longer exist. local checks: NOT RUN Not claimed: this does not resolve gather concurrency in general, only the publication and clear events a derived roster must notice. * fix(models): prune revision entries, and advance the global term before dropping one Reconciliation tracked every per-provider map except the new revisions, so a provider the configuration no longer has kept its entry for the life of the process unless something cleared the whole cache, which may never happen. Dropping the entry alone would have been an ABA: the provider returns, its entry comes back at zero, and a roster built before it left matches again. The global content term is advanced before any deletion, so a returning provider cannot present the counter a stale roster was built against. Regressions cover both events that no per-provider counter can express: a wholesale clear, and a provider removed and then brought back. local checks: NOT RUN * test(management): prove a bound profile change is planned, committed once, and refused when stale Three cases the binding had none of. Profile zero is planned successfully, because zero is a real profile id and the kind of thing an absent-check turns into a bug. A previewed profile change commits with its binding and then reports itself stale on replay, with the profile's file unchanged and a sibling profile untouched. And a stale confirmation is refused with the preference never saved. That last assertion is the one that matters for placement: Aside writes its preference before any writer runs, so a check that fired under the writer lock would have saved it already and the test would fail. local checks: NOT RUN * fix(models): drop a pruned provider's revision after the cache deletion that bumps it The prune deleted the revision entry and then called the cache deletion, whose own bump recreated the entry it had just removed. The map therefore still grew with providers the configuration no longer has. Deletion now happens last. The global epoch is already advanced above it, so removing the entry is still not an ABA when the provider returns. local checks: NOT RUN * test(models): make the prune case actually remove a cached provider The reconcile regression supplied its roster, which skips the gather, so nothing tracked the provider and the prune had nothing to remove. It asserted the snapshot was retired by a code path that never ran. The provider is now cached before the removal and cached again on return, so the case exercises a real prune and a real ABA opportunity rather than an empty one. local checks: NOT RUN * test(management): witness the whole store when a profile confirmation is refused The stale profile case checked the target file and the saved preference. Neither sees an ownership record, a snapshot or a journal row, and an Aside restore imports history before any writer runs, so the two writes most likely to slip through were exactly the ones not being watched. It now compares the whole Aside tree and the whole store by content before and after the refusal. local checks: NOT RUN * fix(management): identify a roster by the configuration that was admitted, not a field list The snapshot key was a digest over fields I chose, which is only as complete as whoever last thought about it: a field it forgets is a roster change nothing notices, and the reviewer was right that it already missed export-affecting configuration. It now uses the admission snapshot, which hashes the configuration file in the same read it parses, so no window exists between hashing and reading and no list needs maintaining. Unreadable configuration yields no key, and that fails closed in both directions: nothing is retained and nothing is served. A preview with no provable idea of which configuration a roster belongs to is worse than no preview. One residual is named in the code rather than papered over: the digest describes the file on disk while the roster was built from an in-memory configuration a caller supplied, and proving those are the same needs the admitted configuration to carry its own revision. That is separate work and is not claimed here. local checks: NOT RUN * fix(management): a missing config file is defaults, not an unprovable configuration Binding the roster to the admission digest introduced a regression I caught reading my own change back: the snapshot reports no digest both when the file cannot be parsed and when it simply is not there. Collapsing those together would have refused every preview on a machine with no configuration file, which is the ordinary state of a fresh install and of CI. Absence is now its own stable identity. A file that exists and cannot be parsed is genuinely unprovable and still fails closed, which is the case the guard was for. local checks: NOT RUN * test(management): pin both halves of the configuration identity An absent configuration file is defaults and must serve a roster, or the feature looks dead on a fresh install and in CI. A file that exists and cannot be parsed says nothing about which configuration a roster belongs to and must fail closed. Those are different states and the code now treats them so. The cases run against their own configuration directory rather than whatever the machine happens to have, which is also what makes the second one expressible at all. local checks: NOT RUN * docs(management-api): document previewing and confirming an integration change Covers the three preview routes, the plan's shape, and what a caller must do with each refusal: a stale confirmation carries a fresh plan and needs another decision rather than a retry, and an unavailable roster is the state of a freshly started proxy that reading the integrations collection resolves. States plainly that a fingerprint is an optimistic check and never authorization, and that a plan returns no configuration value, file location or selected member identity. The seven translated copies still need the same contract; they are unwritten and this English page does not contradict them, it is ahead of them. local checks: NOT RUN * docs(ko): document previewing and confirming an integration change Korean copy of the preview and binding contract, written to the page's own register rather than transliterated from the English. Six locales still need it. They are abridged rather than full mirrors of the English reference, so each needs a section sized to the page it lands on. local checks: NOT RUN * docs(ja, fr): document previewing and confirming an integration change Japanese and French copies, each written to its own page rather than transliterated, and each sized to a reference that is abridged relative to the English one. Four locales remain: ru, tr, zh-cn and zh-tw. local checks: NOT RUN * docs(ru, tr, zh-cn, zh-tw): document previewing and confirming an integration change Completes the localized reference coverage. All seven translations now carry the three preview routes, the plan's shape, the two refusals and what a caller should do with each, and the rule that a fingerprint is an optimistic check rather than authorization. Each is written to its own page rather than transliterated, and sized to a reference that is abridged relative to the English one. local checks: NOT RUN * docs: correct what an unavailable roster actually means I wrote that the refusal means a roster has not been established yet and that reading the integrations collection establishes one. Both were too strong. The refusal means no usable roster is currently retained, which covers a cold process and equally a roster retired because the configuration or the provider cache moved. And the collection read only establishes one when discovery succeeds and the configuration can be identified, since publication is declined without a configuration identity. It is the usual remedy, not a guarantee. Corrected in the English and all seven localized references and in the integration contract. local checks: NOT RUN * test(management): a duplicate copy of an operation cannot redirect a bound undo Aside can legitimately hold the same operation in more than one profile store, and that is the situation the row-resolution fix exists for: a preview and the mutation could each resolve a valid row and resolve a different one, leaving the confirmation bound to an operation other than the one that runs. Copies a journal row into a sibling profile so the request has two valid rows available, then previews and commits a bound undo for the profile that was asked for. The undo acts on that profile and the sibling is untouched. local checks: NOT RUN * test(management): make the duplicate copy one resolution actually has to choose The previous version copied the row into a sibling profile, which nothing ever scans: operation rows come from the requested profile's store and the root store only. The duplicate was unreachable, so the case passed whether or not the selected row was carried through, which is the one thing it existed to prove. It now puts a stored copy in the root store and expires the profile's own, so resolution prefers the alternate and the request genuinely depends on which copy was chosen. The undo restores the original bytes from that copy, the journal records both it and the apply it undid, and the sibling profile's store is unchanged by content. local checks: NOT RUN * fix(models): carry each provider's revision out with the rows it vouches for Sampling the cache revision after a gather returns is wrong under concurrent flights. Flight A can publish rows and retain them while flight B publishes before A's await resolves, so the roster recorded A's rows under B's revision and every later check agreed it was current. Every provider result is built through one helper, and that helper now stamps the provider's content revision as it constructs the result. That is the only point synchronous with choosing the rows; anywhere later is another chance for the two to come from different moments. The flight carries the map out, the gather exposes it through the same out-parameter pattern its other flight-local authority already uses, and the export projection retains those revisions instead of observing globals. A provider the gather did not report falls back to observation so the stamp stays total. That covers a provider configured after the rows were chosen, and it retires the snapshot on the next read rather than implying the roster covered it. A supplied roster still observes, because no gather happened and there is nothing tighter available. local checks: NOT RUN * test(integrations): make the captured-row handoff observable, not merely plausible The HTTP duplicate case proved resolution finds the alternate copy, but it would still pass if the selected row were dropped on the way to the writer, because a fresh lookup would find the same row again. It tested discovery, not the handoff. A lower-level case now removes the row from the journal a fresh lookup reads and restores with the row already captured. Only the captured row can carry that through; a resolver that looked it up again would find nothing and refuse. The route case keeps its job as route proof and its assertions are now exact: the restored bytes, a restore row and the original operation found by parsing the profile's own journal rather than a substring of two files concatenated, the chosen copy unrewritten, and the sibling profile unchanged in both its document and its store. local checks: NOT RUN * test(integrations): choose the captured copy when only lookup diverges The previous version of this case deleted the operation the fresh lookup would find, which is not reachable: deleting the newest row throws before restore, and deleting an older one prunes the snapshot the unchanged-source guard checks. It proved nothing it claimed to prove. The divergence is now confined to enumeration. The root store's listOperations hides the copied row after capture while findOperation and readSnapshot stay real, so re-resolution lands on the profile store's expired copy and refuses before any write, and only the captured row carries the restore through. The refusal is taken through the public entry point and witnessed with no saves, an unchanged profile store tree, unchanged applied bytes and an unchanged source. The routes case now asserts the success envelope and pins the single restore row to the opId that envelope returned, and witnesses the root snapshot bytes rather than only the root journal. local checks: NOT RUN * test(models): interleave two publications inside one export load The production fix carries each provider's content revision out with the rows it vouches for, and nothing exercised the interleaving it exists for. This runs a real one. The load gathers two providers, alpha answers immediately and beta is held open, so the load is still inside its own await once alpha has chosen, published and stamped its rows. A competing publication for alpha lands in that window. The load still returns the rows it fetched, which is correct, but the retained preview is refused because the revision that vouches for those rows is no longer the current one. Reverting the fix makes the retained stamp the competing publication's, so the preview would match on the way out and serve those rows as current: the second case fails. The first case is the control, so a preview refused for an unrelated reason cannot pass for the race being closed. local checks: NOT RUN * fix(management): identify a roster by the configuration in hand, not only the file The snapshot key named the configuration file and nothing else, so it could not tell which in-memory configuration a roster was actually built from. Two callers holding different configuration objects that correspond to the same bytes shared one key, and a configuration edited in place while a load was awaiting was invisible to it. That was recorded as a known residual rather than fixed. The identity now carries both terms: the admission snapshot's file digest and a canonical structural digest of the configuration object itself. Structural for the same reason the file digest exists, because a list of the fields that seemed to matter is only as complete as whoever last thought about it, and it already missed export-affecting configuration once. The load takes that identity before the gather and requires it to be unchanged after. Reading it only at retention would attribute an edit that landed during the await to rows chosen under the previous configuration, which is the same substitution the per-provider revision prevents one layer down. A configuration that moves during a load leaves no snapshot rather than one recorded under an identity its rows never had. Null is unprovable in either term and every caller fails closed on it. Nothing derived from the configuration is logged or serialized; the digest travels and the configuration does not. The existing case asserting that a blocklist or custom-model edit retires the snapshot was left behind by the move to a file-only key and could not have been passing; it is covered again by construction, and a new case pins the in-memory divergence directly, including a field no hand-written list ever named. local checks: NOT RUN * fix(management): gather and project one admitted configuration, then prove it A roster was identified by the configuration file and, since the previous commit, by the configuration object as well. Both terms were read around the gather rather than carried through it, so one pass could still gather under the state it found and project under whatever the resident object had become, and a file that was readable but did not load cleanly was accepted on the strength of its digest alone, before anything looked at what the parse produced. An export load now detaches the configuration it is about to use and runs the whole authoritative pass against that copy: the gather, the projection and the visibility filter all see one state. The detachment is a real copy rather than a serialization, because serializing drops functions silently and invokes getters and toJSON, which would let the object decide what the check sees. An accessor, a cycle, and any value JSON could not have produced are refused rather than read. A provider's fetch executor is the one field that is not data; it is carried by reference so the transport still works and swapping it invalidates the binding. Before anything is retained the admission is revalidated against three things that move independently: the file, the resident object, and the detached copy a consumer was handed. Only then does the roster become preview authority. The binding deliberately does not require the resident configuration to equal the file. The proxy routes by what it is holding, so that is what a preview and the mutation it authorizes must describe, and a file edited under a process that has not adopted it is supported rather than broken: live reconciliation retains live changes and the active listener binding on purpose and can persist a binding the resident object does not have. Demanding equality would make preview permanently unavailable on those configurations while proving nothing about the rows. Pending initial selection is resolved against the live configuration before the admission is taken, so it still adopts and persists where it always did rather than committing into a private copy. The identity a caller carries between a preview and its confirmation is now process-local and opaque. It used to be the configuration file's digest with a counter appended, which handed a dashboard a fingerprint of the operator's configuration dressed as a token. local checks: NOT RUN * fix(integrations): freeze the configuration a coordinated write was checked against A coordinated write freezes every mutable resolution seam before its first await, and the configuration was the one it still held by reference. The sequence is plan, await the writer lock, revalidate, then serialize the document from that input, so a management route editing the live configuration in that window left the plan vouching for one configuration while the document described another. The exposure is real rather than theoretical: the client document carries the proxy base URL and its auth shape, both read from this object after the check. The freeze now takes a plain-data copy, so the plan, the revalidation and the document all read one configuration. The copy is the same detacher the export admission uses, which refuses an accessor rather than invoking it and carries a provider transport executor by reference; a configuration it cannot copy is left as the caller's object, which is no worse than the reference that was there before. The regression drives a real coordinated apply and edits the live configuration inside the revalidation, after it has agreed the write may proceed. Reverting the freeze writes the edited host into the client document. local checks: NOT RUN * fix(integrations): one input from the check to the document, or a refusal Three places still read the caller's objects twice, with an await in between, and the second read is not the one any check approved. The coordinated write froze every resolution seam except the two that carry the document's content. It now copies the configuration and the model roster as plain data before the first await, so a management route editing the live configuration, or a caller editing the model objects it passed, cannot land between the plan and the bytes. Aside is the wider window and the more certain one: it checks the confirmation, awaits the preference write, and only then builds each profile's write input. That preference write edits the live configuration itself, so the document was always serialized from a configuration the check had not seen. The context now captures the configuration when it is created, before the check, and every profile write and policy read in the action uses that copy. The preference write remains a real effect on the live configuration. An input that cannot be copied is refused before the plan, the lock and the document. Returning the caller's object was a copy in name only, and the caller went on to describe it as the input it had checked. The copier moved to src/lib/plain-data.ts and reads through property descriptors everywhere, including array elements. The previous walk iterated arrays normally, so an accessor defined on an index would have been invoked by the check that claimed not to invoke accessors. Three regressions drive real coordinated writes and edit the configuration, the roster and the Aside preference in the window each hazard lives in, plus a refusal case that asserts the accessor is never read. local checks: NOT RUN * test(integrations): pin that a committed Aside change leaves the roster available The commit reloads the roster while holding the configuration its own preference write just edited. If that load cannot admit what it is holding it clears the retained roster, and the next confirmation is answered with no roster cached rather than with a comparison. The route-level case reports exactly that, so this pins the behaviour at the layer that owns it. local checks: NOT RUN * test(integrations): say what a committed Aside change does to the roster Committing an Aside change writes the operator's preference into the configuration, so the configuration a roster was admitted under is no longer the one in hand and the roster is retired. A replayed confirmation is then refused for the earlier of the two reasons: there is nothing to replan against until an ordinary load runs. The route case expected a stale-plan comparison instead. It could only reach one because its fixture stubs persistence, so the configuration file never moved; with a real save the file moves and the roster has always been retired the same way. The expectation now matches what an operator gets, and the case keeps every assertion that matters: the replay writes nothing and the sibling profile is untouched. The owner-level case pins the same sequence at the layer that causes it, including the recovery: an ordinary load brings the roster back. local checks: NOT RUN * fix(integrations): check the Aside input that will be written, prepared before any effect Two bindings were still open on the Aside path, and both let a check answer about something other than what the write would use. The guard rebuilt its own view from the live configuration. The context copies the configuration when it is created, so a configuration edited to something else and back again while the action was in flight produced a check that planned the configuration the operator confirmed, agreed with the confirmation, and let the write proceed from the copy the action was actually holding. Both entry points now prepare the write input before the check and hand it to the check, which plans that input directly instead of reconstructing one. The roster was resolved lazily at each profile write, which happens after the preference write. A caller still holding those model objects could edit them in that window and the document carried the edit. It is now resolved once and copied before the check, so every profile in one action writes the same roster and the check reads exactly it. The check still runs before the preference write and before any import effect, which is the ordering that makes it useful at all. Preparing an input performs no write of its own, and a profile that cannot be prepared refuses rather than reaching an unchecked write. Two regressions: a confirmed plan taken under one configuration, an action begun under another, and the live configuration restored before the check, where the guard must plan the prepared input and refuse; and a roster edited during the preference write, where the document carries the roster that was checked. local checks: NOT RUN * fix(integrations): bring the Aside input forward only for a checked change Preparing every Aside change's write input before the preference write was too broad in two ways that CI caught at edbc4dce15. It resolved the roster for changes nobody checks, and an unconfirmed change whose preference write fails must not have done model work by then. That is a contract with its own regression, and this broke it. It also moved the model load ahead of the preference write for ordinary toggles, so the roster that load retained was admitted under the configuration as it stood before the toggle and was retired a moment later by the toggle itself. A preview taken after an ordinary change then had nothing to plan against, which showed up as an unrelated preview route answering 409. Preparation now happens only when a confirmation has to be checked, which is the case that needs it: the check must read the same input the write will use. An unchecked change resolves its roster where it always did, after the preference write, and the write still uses the prepared input when one exists. The synthesized refusal for a profile that cannot be prepared now carries a profile id, which the outcome type requires. local checks: NOT RUN * fix(config): treat a non-executor provider fetch field as data, not as a refusal The executor exception was keyed on the field name alone, so a provider whose configuration happens to carry a `fetch` value that is not a function refused the whole admission and took previews away for a field nobody calls. The exception now applies when the value is an executor; anything else is copied and compar…
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
… Mode (#5228) Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
#5226) Completes review findings left open when #5208 merged: normalizeAnthropicImages now receives the request abort signal at the anthropic adapter and claude-messages passthrough call sites (abortSignal moves onto NormalizeOptions so both entry points accept it), and the decode gate re-checks aborted after admission so a request cancelled while queued never starts a native decode. Adds regression coverage for the post-admission abort and for rejecting an image whose dimensions header cannot be sniffed. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
…5238) The web-search and image-bridge loops build each attempt from an iteration-local shallow copy (iterParsed) of the outer parsed request, but rotateSidecarProviderOn429 rebound only the outer parsed. On a Kiro OAuth rotation the retried request kept the failed account's _kiroAuthContext and continuation/replay scope, pairing the new account's bearer with the previous account's routing metadata. The on429 hook now receives the exact iterParsed the retry is built from. The shared hook hands it to applyFailoverSnapshot, which already syncs _kiroAuthContext for a supplied retry request, and rebinds the route reasoning-replay/continuation scope on it — the same pattern the continuation loop uses for nextParsed. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
* feat(dashboard): preview-bound confirmations for file integrations Every file-integration mutation dialog now fetches the server-owned plan, renders bounded value-free change kinds and managed paths, and submits the exact operation plus plan fingerprint. A stale plan replaces the display, announces the change, and requires an explicit re-confirmation; nothing auto-retries. Unavailable previews recover through the ordinary page load. Bulk disable plans each client independently and never reuses a fingerprint. All strings are localized in every catalog; accessibility behavior is preserved. Native integrations and the all-profiles switch stay on their existing owners. Part of #5118 * fix(dashboard): bind drift intent to the plan and fix review gates Review fold: restore state now binds confirmDrift alongside the active plan — a stale applicable plan retains the failed request's drift flag and a drift refusal strengthens the binding, with a regression proving exactly one explicit re-confirmation and no auto-retry. The consequence dialog drops its synchronous prop-to-state effect for a fingerprint-keyed override. Plan duplicate detection uses a Map<kind, Set<path>> instead of a NUL-delimited string that tripped the i18n lint. Part of #5118 * fix(dashboard): cancellable previews, refusal guidance, accessible pending states Review fold: preview reads are abortable and dismissable (Escape, backdrop, Close) with generation fences so late results cannot reopen a dismissed dialog, while pending mutations keep their dismiss policy. Known refusal reasons map to localized reason/action guidance in all ten catalogs with a generic fallback and no raw server text; mutations announce a localized pending status with aria-busy; and the privacy assertion now feeds a hostile malformed preview through the real page, asserting generic failure with no DOM leakage. Part of #5118 * fix(dashboard): load the restore preview outside the effect body react-doctor's no-set-state-after-await-in-effect fired on the restore dialog's preview effect. Preview loading now lives in a memoized async callback that the effect only invokes and cleans up; all post-await state updates happen inside the callback. Abort, generation fencing, drift binding, and stale reconfirmation are unchanged. Part of #5118 * fix(dashboard): defer the initial preview load per repo convention Review fold: the previous fix only moved the lint-visible site; the deferred load now follows the repository's own convention — the effect schedules window.setTimeout(() => void loadPreview(), 0), and cleanup clears the timer first, then invalidates the generation and aborts. Abort-on-dismiss, late-result fencing, drift binding, and stale reconfirmation are unchanged. Part of #5118 * fix(dashboard): reconciliation-safe previews and deterministic tests Review fold: loadPreview now depends only on the restore identity with latest callbacks in refs, so a parent reconciliation no longer restarts the preview or clears a mutation failure; preview-blocking and mutation failures are distinct states, and the retry-button contract holds. The cancel test waits for the observable preview signal before canceling, and the Aside test waits for an enabled Restore and asserts one exact bound POST. No auto retry anywhere. Part of #5118 * fix(dashboard): update latest-callback refs in the commit phase react-compiler's no-ref-current-in-render fired on the restore dialog's latest-callback refs. They now update in a commit-phase effect, so reconciliation never mutates refs during render and preview loading stays identity-scoped. Abort, fencing, deferred loading, drift binding, and stale reconfirmation are unchanged. Part of #5118 * test(dashboard): synchronize restore clicks on observable readiness Final gates fold: the remaining restore interaction cases now wait for an enabled Restore before clicking and assert exactly one bound POST with the expected opId, confirmDrift, operation, and fingerprint. All timing, payload, and profile assertions preserved. Part of #5118 * docs(pr): preview-confirmation screenshots for the integration plan dialogs Captured from the exact-head hosted build (dashboard-preview artifact bf1bf42, gui tree ce7f101 == PR head 580f087's gui/ tree) served statically with fixture API responses; real browser at 1280x720 and 480x800. Fixture-rendered GUI proof, not a live backend. Part of #5118 * docs(pr): Korean narrow-viewport and keyboard captures + capture receipt Supplement from the same provenance-checked artifact (gui tree ce7f101, unchanged at this head): Korean locale at 390 CSS px (restore-drift, stale-reconfirm) and a keyboard focus ring capture with Escape dismissal verified. The receipt records fixture routes, request sequences, viewports/DPR, provenance, and SHA-256 for every image. Part of #5118 * docs(pr): add the reproduction command to the capture receipt The receipt now records the fixture-server start command and the Aside repl driver path so the captures are reproducible from the artifact. Part of #5118 * docs(guides): explain the preview-confirm flow in the integrations guide The original issue's step 5: a concise user-facing section on preview, review of bounded change paths/kinds, confirm, stale-plan re-confirmation (never auto-retry), reload recovery for unavailable previews, and the single-profile Aside scope. English plus the three existing translations (fr, tr, zh-tw); no new locale files. Part of #5118 * fix(dashboard): describe no-op plans as document-scoped Final acceptance fold: a willChange:false plan means the managed client document needs no changes — not that nothing happens; a profile-scoped no-op still saves the sync preference on confirm, and the dialog now says so in all ten catalogs. The integrations guide section matches in all four of its languages. Binding and mutation semantics unchanged. Part of #5118 * fix(dashboard): promise rollback only when the plan includes one Final render-accuracy fold: the consequence dialog rendered its backup/rollback copy unconditionally, which is false for a no-op plan. The slot now renders only when the active plan (including a stale override) contains snapshot or journal changes; unbound legacy dialogs keep their generic copy. Assertions cover both directions. Part of #5118 * docs(pr): publish final integration preview evidence * fix(dashboard): preserve aggregate Aside mutations
…nCode gateways (#5240) * fix(registry): add modelResponsesTerminalRepair for muse-spark on opencode-go * fix(registry): add modelResponsesTerminalRepair for muse-spark on opencode-zen * test(registry): add regression tests for muse-spark terminal repair
* docs: close contract implementation campaign and record release handoff * docs: retain active release follow-up in its own plan unit * docs: distinguish historical checkpoints from final closure
Co-authored-by: ingwannu <ingwannu@teamwicked.me>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Important Review skippedToo many files! This PR contains 598 files, which is 298 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Repository: lidge-jun/opencodex/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: ⛔ Files ignored due to path filters (17)
📒 Files selected for processing (598)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Deterministic PR hygiene checks passed. |
⏳ DRAFT
What to do
Its title has been prefixed with |
Summary
devtree tomainfor the 2.60.0 release. Head015c67c46aaf16d4319543c8941c6dbec9887ae6carries every change integrated since 2.59.0 (134c92a01b), including the sixteen contract-campaign issue fixes ([Bug]: Return null bodies for 204 and 205 in raw outbound transports #5109–[Bug]: Request full replay when a task-scoped continuation cannot be used #5124) and the contributor fixes that landed alongside them.devwas moved to 2.61.0 in chore(release): open dev at 2.61.0 before releasing 2.60.0 #5247 before this promotion, sorelease.yml's version-line assertion holds and no open pull request inherits a version-line failure from the new tag.devwas a merge-union defect: fix(responses): reject Fernet-shaped agent plaintext #5239 tightened Fernet structural validation while an older case intests/codex-integration/multi-agent-compat.test.tsstill minted its surviving blob from a non-canonical string. test(responses): mint the surviving blob as a valid Fernet token #5246 mints it with thefernetFixture()helper, restoring the "real blobs survive" contract without touching the classifier.Verification
devhead, with no additional commits on this branch.dev. Local suites, typecheck, builds and installs were not run in this lane by explicit maintainer instruction; hosted CI on this promotion is the execution evidence.release.ymlwithexpected-shapinned to the merge commit.Checklist