pi-ai credentials carry optional members as explicit undefined (a github.com
Copilot grant holds enterpriseUrl: undefined), and the store bridge committed
the object verbatim, so the credential store's strict validator refused the
write and sign-in failed after the provider had already authorized it.
toRecord now drops explicitly-undefined members and renders undefined array
entries null, exactly as JSON.stringify would; everything else passes through
untouched so genuinely unstorable values still fail loud at the store.
Master dimensions move from a 2048 long-edge rule to a total-pixel
budget (normalizedImageMaxPixels, default 2048x2048) with an 8192
long-edge cap, so extreme aspect ratios keep short-edge resolution.
The shared quality ladder and lazy execution move to encoding.ts,
review-round doc fixes land across attachment and llm packages, and
the superseded facts in the unified-image-pipeline note now describe
the shipped routing.
Delete the 5-bit colour-count classifier and palette PNG branch that
misrouted high-frequency photographic JPEGs (issue #2885 images 23/24)
into an encoder 100x slower with 4x larger output. Both normalization
and request-image encoding now route by the decoded alpha fact alone:
opaque sources down a JPEG ladder and alpha sources down a WebP
effort-0 ladder, each at qualities 85/75/60. Byte budgets become ladder
targets: the downscale retry loop is gone and a ladder-exhausted encode
keeps its smallest output, while provider byte caps stay enforced at
the transmitting route. Request transforms move to request-image-v5.
Remove dead design-session citations, change narration, indexical
stamps, and review-adjacent justification found by the
dsh-trim-cot-leakage recall batteries in prose that landed after the
last purge. Bilingual README pairs are re-recorded.
AttachmentStore.saveImage now resolves SavedImageAttachment: the durable
reference paired with the submitted raster's intrinsic facts, so a store may
persist a canonical re-encoding while callers keep the source dimensions for
coordinate mapping. saveImages keeps returning refs; every fake store and the
cordis API catalog follow the new signature.
os.homedir() reads HOME on POSIX and USERPROFILE on Windows; stubbing
only HOME left the Windows lane expanding ~ into the real profile
directory and failing the fileExists assertions.
Review findings on #2509, all confirmed:
- Every writer of .credentials.yaml now waits out the record-mutation
lock (DOCUMENT_LOCK_WAIT_MS): refs and records share one file and one
lock, so a reference write or record delete contending with an OAuth
refresh must not fail at the 2s file-work default.
- api-key records are admitted before they are rendered: an empty key,
a non-POSIX env name, or an empty env value is refused at the write
instead of persisting a document the next boot rejects wholesale.
- llm-pi-ai no longer lets the credential-key grammar reject legal
route ids: reads answer "nothing stored" via isCredentialKeySegment
(new dsh-credentials export), deletes have nothing to remove, and only
a write refuses, as LlmError UNSTORABLE_PROVIDER_ID; flow registration
skips a future catalog id outside the grammar instead of failing the
mount.
- authorization/settled fans out with contained listener failures on
the credentials seam's terms (INVARIANT still rethrows), so a broken
watcher can never turn a finished attempt into a failure.
- notify() is fire-and-forget at the seam: a surface that cannot render
a notice loses the notice, never the attempt.
- A declined prompt is an outcome: interactions reject with the new
AuthorizationDeclinedError and the attempt settles cancelled instead
of failed.
- NOT_COMMITTED now confirms a commit observed during the attempt
(credentials/record-updated for the flow's key), so a re-auth cannot
pass a stale record off as fresh; a flow that deletes its record is
refused on the same code.
READMEs, the subsystem/event/config catalogs, and the Agent Note follow
the shipped behavior; memory.ts carries the dedup TODO.
pi-ai's auth model reaches this adapter through three translations, all
of which live here: a CredentialStore over the harness credential
records, an AuthContext over the credential plane and the host
filesystem, and one authorization flow per installed provider that
ships a login. The seams they consume name nothing from pi-ai, so a
second adapter family can arrive with a different auth model and share
them.
Every collection is now built with the store and the context rather
than with nothing, which is what makes a signed-in provider stay signed
in across the collection rebuild a configuration change causes. With a
posture that works, the configurable-provider directory no longer
withholds OAuth-only routes and `openai-codex` is offered again; the
predicate that withheld it is gone.
The credential plane stays optional. Reads answer "nothing stored"
without a credentials service because such a composition genuinely
holds no credential, while writes refuse by name — a login whose grant
evaporated would report success and then fail every request. Flow
registration is scoped to the authorization seam, so a headless or ACP
composition mounts with no sign-in and everything else unchanged.
Two fixes found while wiring this up: pre-release credential fixtures
in the llm suites still used the flat document the record work
replaced, and a flow that ignores its cancellation signal would have
held its key for the life of the process — withdrawal now settles the
attempt either way.
The upstream-type proof only ran covariantly, so it caught a profile
field wider than pi-ai's but not one narrower — the direction its own
JSDoc claimed to guard. It now asserts both directions; the reverse holds
today because every field is either derived from upstream or a boolean.
`ModelCompat` gained `BedrockCompat`, which the gate had already started
serving, and the per-field protocol lists now match what the gates
resolve: `docs/config-catalog.md` pastes that JSDoc verbatim, so a stale
list there contradicted the README in the same change. The interface
header names the Responses grouping, since a catalog reader never sees
the README passage that explains it.
Valueless keys are judged after the name, so a withheld or misspelled key
written bare is refused for being that name rather than sent back for a
value it would be refused with anyway; the remedy no longer promises an
installed catalog value that a hand-declared route does not have. The
check covers `undefined` beside `null`: schemastery keeps the key either
way, and a cordis.yml entry reaches that state through `!!js undefined`,
so it is a config boundary rather than a typed one.
Coverage follows the composed path: the rejection is asserted through a
written settings section, and a switch is carried from that section onto
the wire a provider receives.
Refs #2646
Review found two live defects in the compat surface.
A valueless key (`supportsDeveloperRole:`) survives schemastery as null,
and resolution carried it forward as a configured value. It landed on
`Model.compat` as null, which replaced the installed catalog entry's
value and left pi-ai's `??` reaching for its baseURL detection — the
catalog layer skipped entirely, and the switch written but not applied.
The vocabulary check now refuses it where it is written, matching the
`reasoningEfforts` precedent in the same file.
The gates were keyed by protocol name, but pi-ai keys compat by type:
`openai-responses`, `azure-openai-responses`, and `openai-codex-responses`
share one `OpenAIResponsesCompat`, so two shipped catalog routes were
refused the fields their own models declare. Gates now group by compat
type, `bedrock-converse-stream` gains its own, and the protocol set is
derived from `Model.compat`'s conditional so a release that gives a
further protocol a compat type fails the gate list by name.
Field types are derived from upstream rather than restated, with a proof
pinning the profile assignable to the upstream types, so a widened value
union cannot silently narrow what configuration accepts.
The `undefined` filter stays removed: `exactOptionalPropertyTypes` keeps
a typed caller from writing one, and schemastery never materializes one,
so it was validation for a value the static interface already excludes.
Refs #2646