Owner-diagnosed: the turn gate checked only model coordinates, so a
pinned slot whose provider key was deleted passed the gate and died
mid-stream masked to 'An error occurred.' The key is now gated where
the model is — by what would RUN (env-first keyValue truth, so an
env-shadowed key passes).
- core/settings.ts: the provider_key_missing envelope family (code,
provider, slots, human line naming the provider from AUTH_PROVIDER_KEYS
labels) + ProviderKeyMissingError + isProviderKeyRefusal matcher.
- chat-handler turn gate: refuses 503 before beginTurn when the resolved
conversation's provider key is gone (slots: [strong]).
- Job runner: same check via an injected dep (agent layer cannot read
the key store) — a keyless engine refuses the spawn through the
existing failure-shape convention, no phantom run.
- Extraction pipeline: refuseIfStagesUnready refuses BEFORE any batch
(first offender: extractor before auditor); the trigger skips info-
level, /api/graph/import surfaces the 503 envelope per file.
- Tests: 8 new pins (turn gate both ways, runner refusal, pipeline
first-offender + pass-through, envelope factory, matcher); the three
turn-driving suites seed OPENROUTER_API_KEY like any real setup.
- Doc fix: stale EXTRACTION.chunkTokens comment in the manual-extract
route (left over from eed4b4e).
Known residual (pre-existing, flagged for master): the manual extract
route logs BOTH config refusals as console.error (fire-and-forget path);
the coverage ledger keeps everything uncovered either way.
- thinking-default.ts: defaultOrderLevel (the ordering's first supported,
the client twin of the server's defaultThinkingLevel) + populateLevel
(an explicit level always rides; the WRITE clamps server-side, bd68486,
and the cross-surface signal re-reads the clamped truth; only absence
populates from the ordering) + THINKING_LABEL shared.
- Setup dialog (item 5): per-slot thinking selectors pre-populated with
the default-order level per picked model, editable; disabled until the
model pick; prefillLevelFromPin keeps a stored level as-is and populates
legacy pins from the ordering.
- Composer + agent section (item 6): fresh model picks always write a
level: the stored one rides, absence populates; the unset state can
never re-enter. Item 4: records with a model but no stored level display
the RESOLVED level (ordering's first supported) instead of a dash, in
the composer pill and the settings slot triggers/menus.
- Item 7 verified live end-to-end: an explicit level rides into the PUT,
the server clamps, the signal re-reads, the pill shows the clamped truth.
- existing sessions: a send that lands the 503 models_not_configured envelope
renders as the SETUP FLOW, never the error banner — the record refetch drops
the refused orphan (the gate refuses before the write-ahead persist), the
draft goes home to the composer, and Save auto-replays it (owner ruling);
parseModelsRefusal reads the envelope from the transport's verbatim error
text (exported pure, tested)
- persistent 'Set up your models' CTA above the chat composer while
slotsConfigured is false (server-read seed via app/chat/[id]/page.tsx) —
legacy per-chat overrides may still send with the pill present; the server
gate stays the truth
- SetupDialog prefill + 'needed' marks (owner ruling): pinned tiers open with
their pick (matched in the federated catalog — lapsed keys don't prefill),
unpinned tiers carry the mark; Save still requires all three;
prefillFromSlots exported pure, tested
- refused jobs read as INFO: dispatch card shows a quiet 'needs model setup'
line + 'Set up models' deep-link (ChatActionsContext.openSetup); dock blobs'
activity reads 'needs model setup' instead of 'failed'; real failures keep
the red tone; isModelsRefusal added to the envelope's home module
- agent-section ModelsBody.slots type honest to the nullable contract