Skip to content

Add per-model scoped killswitch threshold#109

Merged
ualtinok merged 4 commits into
cortexkit:mainfrom
iceteaSA:feat/scoped-killswitch
Jul 5, 2026
Merged

Add per-model scoped killswitch threshold#109
ualtinok merged 4 commits into
cortexkit:mainfrom
iceteaSA:feat/scoped-killswitch

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What

Adds a third, model-scoped dimension to the per-account killswitch: a scoped threshold that hard-limits traffic to a weekly scoped quota carve-out (the weekly_scoped "Fable only" window) without killing the account for other models.

When the killswitch is enabled and a request's model matches a scoped window at/below the scoped threshold, the request reroutes to a scoped-eligible fallback and hard-blocks with a 429 if none qualify — while Sonnet/Opus traffic on the same account continues normally. Additive to the existing five_hour/seven_day checks; generic over any scoped window (not Fable-hardcoded).

Changes

  • killswitchPassesPolicy(quota, storage, accountId?, modelId?) — new optional modelId. When present and a scoped window matches it, the window's remainingPercent is checked against the per-account scoped threshold. When absent, behavior is unchanged. The scoped check runs before the unknown-5h/7d fail-open return, so an exhausted scoped window blocks independently of missing 5h/7d windows.
  • Comparison is <= (inclusive) for scoped — deliberately distinct from the strict < on 5h/7d — so the default threshold 0 fires at true exhaustion (matching quotaSnapshotModelScopeIsExhausted's <= 0).
  • ConfigKillswitchThresholds gains an optional scoped key (default 0); normalizeKillswitchThresholds resolves it with the same finite-guard/default pattern. Backward-compatible: existing two-value configs read scoped as the default.
  • Command/claude-killswitch set main:80,10,0 accepts an optional third number (fh, sd, scoped). Two-number form parses identically to before. Status table gains a Scoped column.
  • EnforcementmodelId is threaded into the three routing-path call sites (main killswitch gate + both fallback filters), leaving the sidebar/status callers model-free so account-level killed state is unchanged.
  • Block message — a scoped-driven terminal 429 names the model ("<Model> weekly limit reached, …") and killswitchRetryAfterSeconds honors the scoped window's weekly resetsAt. A 5h/7d-driven block keeps the account-level message (classified by the same <= scoped threshold check that drives the block, so message and reason never disagree).

Opencode-only, consistent with the existing killswitch (Pi has no killswitch enforcement).

Verification

  • bun run typecheck — clean (all packages)
  • bun run lint — clean
  • bun test: core 34/0, opencode 770/0, pi 47/0
  • New tests cover: model-scoped isolation (same quota, non-matching model → account stays live), the <= boundary at the 0 default and a raised threshold, modelId-absent regression lock, the unknown-window × fail-open × scoped-exhausted interaction, block-message attribution (5h/7d block with a healthy scoped window → generic message; exhausted scoped window → model-named), and config round-trip for the two- and three-value forms.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds a per-model scoped killswitch threshold to enforce weekly model carve-outs (e.g., Fable) without killing the account for other models. Routing, CLI, retry hints, and 429 messages are scope-aware with correct attribution and weekly reset timing; tests lock the priority rules.

  • New Features

    • Add scoped threshold (default 0); inclusive <=; evaluated when a request includes modelId; additive to 5h/7d.
    • Thread modelId through the main gate and fallback filters; exhausted scoped windows reroute to scoped-eligible fallbacks or 429.
    • Update retry hints to accept a scopedModelId and use only the matched weekly reset; extend /claude-killswitch set to accept a third number (fh, sd, scoped); status table shows a Scoped column; thresholds normalized; block messages name the model for scoped-driven blocks.
  • Bug Fixes

    • Run the scoped check before the unknown 5h/7d fail-open decision (MUST-1).
    • Correct 429 attribution: only label a block as model-scoped when the matched scoped window is at/below the scoped threshold and 5h/7d did not already kill the account (MUST-2, FINDING 2).
    • Fix retry-after for scoped blocks: when scopedModelId is provided, consider only the matched scoped window’s resetsAt so the hint reflects the weekly reset (FINDING 1).
    • Strengthen the MUST-2 test to actually trigger account-level blocks by setting 5h/7d remaining below the strict < thresholds (ensures the priority guard is exercised).

Written for commit 660e47d. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR adds a scoped third dimension to the per-account killswitch, enabling model-specific weekly quota enforcement (e.g., Fable carve-outs) without blocking the account for other models. The change threads modelId through the main killswitch gate and both fallback filters, and introduces scope-aware block-message attribution and retry-after hints.

  • killswitchPassesPolicy gains an optional modelId; when present and a scoped window matches at/below the scoped threshold, the request is blocked independently of 5h/7d — using <= (inclusive) so the default 0 fires at true exhaustion.
  • resolveScopedDrivenBlock distinguishes a scoped-driven block from a 5h/7d-driven one by re-running policy without modelId; if 5h/7d already killed the account, the block is classified as account-level regardless of the scoped window's state (addressing the previous FINDING 2).
  • killswitchRetryAfterSeconds now takes a scopedModelId and, when provided, collects only the matched scoped window's weekly resetsAt — ignoring the 5h reset that would otherwise produce a misleadingly short retry hint (addressing the previous FINDING 1).

Confidence Score: 5/5

Safe to merge — the two previously flagged issues (retry-after dominated by 5h/7d resets; misclassification when both 5h/7d and scoped are simultaneously exhausted) are both resolved in this revision, with regression-lock tests co-located alongside each fix.

The scoped check is correctly ordered before the unknown-window fail-open branch, the resolveScopedDrivenBlock priority logic (5h/7d wins over scoped when both are exhausted) matches the stated invariant and is directly verified, and the retry-after split (scoped-only vs 5h/7d) prevents the retry-storm scenario. The change is backward-compatible: existing two-value configs read scoped as 0 on normalization, and callers that omit modelId see identical behavior to before.

No files require special attention.

Important Files Changed

Filename Overview
packages/core/src/accounts.ts Adds scoped to KillswitchThresholds, DEFAULT_KILLSWITCH_THRESHOLDS, and normalizeKillswitchThresholds; threads optional modelId into killswitchPassesPolicy with correct ordering (before the unknown-window fail-open branch); splits killswitchRetryAfterSeconds into scoped-only vs 5h/7d paths — all sound.
packages/core/src/killswitch.ts Extends the command parser regex to accept an optional third number, updates the set-entry type, and adds a Scoped column to the status table via normalizeKillswitchThresholds — backward-compatible and clean.
packages/opencode/src/index.ts Introduces resolveScopedDrivenBlock and formatKillswitchBlockMessage; threads modelId into the main gate, getRoutableFallbackAccounts, and the second fallback filter; correctly conditions retry-after and block message on isScopedDriven — logic is consistent with the policy functions.
packages/core/src/tests/killswitch.test.ts New test file with 34 cases covering defaults, normalization, per-model isolation, boundary conditions (<=), MUST-1 fail-open ordering, FINDING 1 retry-after branching, and config round-trips — comprehensive.
packages/opencode/src/tests/killswitch.test.ts Adds formatKillswitchBlockMessage and resolveScopedDrivenBlock tests covering MUST-2 (5h/7d priority), FINDING 2 regression lock, and message attribution; thoroughly exercises the correct block-attribution logic.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Request with modelId] --> B{killswitch enabled?}
    B -- No --> Z[Pass through to main]
    B -- Yes --> C[killswitchPassesPolicy
mainQuota, storage, undefined, modelId]
    C --> D{5h/7d window
below threshold?}
    D -- Yes return false --> E[Account-level kill]
    D -- No check scoped --> F{modelId provided
and scoped window
matches model?}
    F -- No scoped window --> G{sawUnknownWindow?}
    F -- Window at/below
scoped threshold --> E
    F -- Window above
scoped threshold --> G
    G -- failClosed=true --> E
    G -- failClosed=false --> Z
    E --> H[getRoutableFallbackAccounts
filtered by modelId scoped check]
    H --> I{survivingFallbacks > 0?}
    I -- Yes --> J[Route to fallback]
    I -- No --> K[resolveScopedDrivenBlock
check 5h/7d WITHOUT modelId]
    K --> L{5h/7d passes
without modelId?}
    L -- No account-level --> M[Generic 429 message
retryAfter from 5h/7d resets]
    L -- Yes + scoped window
at/below threshold --> N[Scoped-driven 429
model-named message
retryAfter from weekly resetsAt]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Request with modelId] --> B{killswitch enabled?}
    B -- No --> Z[Pass through to main]
    B -- Yes --> C[killswitchPassesPolicy
mainQuota, storage, undefined, modelId]
    C --> D{5h/7d window
below threshold?}
    D -- Yes return false --> E[Account-level kill]
    D -- No check scoped --> F{modelId provided
and scoped window
matches model?}
    F -- No scoped window --> G{sawUnknownWindow?}
    F -- Window at/below
scoped threshold --> E
    F -- Window above
scoped threshold --> G
    G -- failClosed=true --> E
    G -- failClosed=false --> Z
    E --> H[getRoutableFallbackAccounts
filtered by modelId scoped check]
    H --> I{survivingFallbacks > 0?}
    I -- Yes --> J[Route to fallback]
    I -- No --> K[resolveScopedDrivenBlock
check 5h/7d WITHOUT modelId]
    K --> L{5h/7d passes
without modelId?}
    L -- No account-level --> M[Generic 429 message
retryAfter from 5h/7d resets]
    L -- Yes + scoped window
at/below threshold --> N[Scoped-driven 429
model-named message
retryAfter from weekly resetsAt]
Loading

Comments Outside Diff (1)

  1. packages/opencode/src/index.ts, line 886-911 (link)

    P1 resolveScopedDrivenBlock misclassifies when 5h/7d and scoped are both exhausted

    resolveScopedDrivenBlock checks only whether the scoped window is at/below threshold — it does not verify that 5h/7d windows are themselves healthy. When 5h/7d exhaustion (e.g. remainingPercent: 4 vs a threshold of 5) caused killswitchPassesPolicy to return false, a simultaneously exhausted scoped window still produces { isScopedDriven: true }. The caller then emits "<Model> weekly limit reached, no routable accounts", misleading operators into thinking Sonnet requests on the same account would succeed — when in reality the account is blocked for all models due to the 5h/7d breach.

    The fix is to confirm that no 5h/7d window is independently below its threshold before declaring the block scoped-driven: if any finite 5h/7d window is < thresholds[key], return { isScopedDriven: false } immediately so the generic account-level message is used instead.

Reviews (3): Last reviewed commit: "test(killswitch): make MUST-2 5h/7d prem..." | Re-trigger Greptile

cortexkit-dev added 2 commits July 5, 2026 11:12
Extend the per-account killswitch with a third, model-scoped dimension
that hard-limits traffic to a weekly scoped quota carve-out (Anthropic's
'weekly_scoped' limits — the Fable-only window today, any future
carve-out tomorrow) WITHOUT killing the account for other models.

Behavior, evaluated per-request against the request's modelId:
  * 5h/7d healthy, Fable window remainingPercent <= scoped threshold
    + request IS a Fable-model → reroute to scoped-eligible fallback,
    hard-block 429 if none. Account STAYS LIVE for Sonnet/Opus.
  * 5h/7d below threshold → whole account killed (unchanged).
  * Scoped check is additive to the 5h/7d check.

Core (packages/core/src/accounts.ts)
  * KillswitchThresholds gains optional 'scoped' key
  * DEFAULT_KILLSWITCH_THRESHOLDS.scoped = 0
  * normalizeKillswitchThresholds exports and resolves 'scoped' with
    the same finite-number guard / default fallback as 5h/7d
  * getKillswitchThresholdsForAccount exports and widens return type
  * killswitchPassesPolicy gains optional trailing modelId; scoped
    check uses inclusive <= so the default 0 fires at exhaustion;
    a missing scoped window is not 'unknown quota' (most models have
    no carve-out) — only a present window at/below threshold blocks
  * killswitchRetryAfterSeconds gains optional scopedModelId; the
    matched scoped window's resetsAt is also considered so the retry
    hint reflects the weekly reset for a scoped-driven block

Command (packages/core/src/killswitch.ts)
  * KillswitchCommandAction 'set' entries gain optional scoped?: number
  * parseKillswitchCommandAction regex: optional 3rd capture is scoped
  * executeKillswitchCommand writes scoped into the KillswitchThresholds
    when present
  * buildStatusTable gains a Scoped column (<= X%)
  * USAGE_TEXT documents the optional third number

Enforcement (packages/opencode/src/index.ts)
  * Thread requestModelId into the THREE routing-path call sites:
      - getRoutableFallbackAccounts fallback filter
      - tryFallbackAccounts reactive-path fallback filter
      - main killswitch gate (accountId stays undefined)
  * Display/status callers (sidebar) keep account-level state — no
    modelId threading there
  * Terminal 429 block: scoped-aware message via new
    formatKillswitchBlockMessage helper. The helper is unit-tested.

Tests
  * New packages/core/src/tests/killswitch.test.ts (24 tests):
      - default scoped = 0; normalizeKillswitchThresholds resolution
      - matching model + scoped window at/below threshold -> false
      - NON-matching model + scoped-exhausted window -> true (key proof)
      - modelId absent: byte-identical to pre-change (regression lock)
      - 5h/7d below threshold blocks regardless of scoped
      - default 0 inclusive boundary (passes at 1, blocks at 0)
      - raised threshold (20) blocks at <= 20
      - non-finite scoped remainingPercent -> not blocked
      - generic: Mythos carve-out behaves identically (no Fable string)
      - killswitch disabled short-circuits before scoped check
      - parseKillswitchCommandAction: two-number form byte-identical,
        three-number form, mixed entries
      - executeKillswitchCommand: per-account scoped round-trip
      - buildStatusTable renders Scoped column
      - killswitchRetryAfterSeconds: scoped resetsAt considered;
        omitting is backward-compatible
  * packages/opencode/src/tests/killswitch.test.ts (4 new):
      - formatKillswitchBlockMessage: scoped-driven names the model,
        account-level uses generic phrasing, generic modelName works,
        retry hint formatting (m/s)

Gates
  * bun run typecheck -> 0
  * bun run lint -> '1 info' (pre-existing biome-schema drift, unchanged)
  * core bun test -> 30 pass / 0 fail
  * opencode bun test -> 763 pass / 0 fail
  * pi bun test -> 47 pass / 0 fail
MUST-1: killswitchPassesPolicy bypassed the scoped check when 5h/7d
was missing/non-finite under fail-OPEN (failClosedOnUnknownQuota=false).
The function short-circuited on the unknown-5h/7d decision before
reaching the scoped check, so an exhausted scoped window was
incorrectly treated as a pass.

Fix: reorder the function so the scoped check runs BEFORE the
unknown-5h/7d fail-closed decision. The scoped check is an
independent block reason; the unknown-5h/7d decision only changes
the fall-through for accounts that did not already block on scoped.
The inclusive <= comparison and the missing-scoped-window-doesn't-
block semantics are preserved exactly.

Adds 4 tests in packages/core/src/tests/killswitch.test.ts:
  * scoped check runs even when 5h/7d is unknown and fail-OPEN
  * scoped check runs when one 5h/7d window is non-finite + fail-OPEN
  * healthy scoped window with unknown 5h/7d + fail-OPEN still passes
  * absent scoped window with unknown 5h/7d + fail-OPEN still passes

MUST-2: the 429 block message misattributed a 5h/7d-driven block as
scoped-driven whenever the request's model happened to match a scoped
window, even if that window was healthy. A Fable request with a
healthy Fable window + 5h/7d breach would produce
'Claude Fable 5 weekly limit reached' even though the actual cause
was a whole-account 5h/7d kill.

Fix: extract a new resolveScopedDrivenBlock helper in
packages/opencode/src/index.ts. It classifies a block as scoped-driven
ONLY when the request's model matches a scoped window AND that window
is at or below the per-account scoped threshold (via the existing
getKillswitchThresholdsForAccount, now imported into opencode). The
terminal block uses the helper's result to:
  * pass the modelId to killswitchRetryAfterSeconds (so the weekly
    reset hint is used only for genuine scoped-driven blocks)
  * pass modelName to formatKillswitchBlockMessage (so the message
    names the model only for genuine scoped-driven blocks)

Adds 7 tests in packages/opencode/src/tests/killswitch.test.ts:
  * 5h/7d-driven block + HEALTHY Fable window -> NOT scoped-driven
  * EXHAUSTED Fable window at/below scoped threshold -> IS scoped-driven
  * raised scoped threshold (20) marks a 20% Fable window as scoped-driven
  * same 20% threshold + 21% remaining Fable window -> NOT scoped-driven
  * Sonnet request: no matching scoped window -> NOT scoped-driven
  * no requestModelId -> NOT scoped-driven
  * non-finite remainingPercent on a matched window -> NOT scoped-driven

All existing 5h/7d killswitch tests are unchanged in count and still
pass — the reorder only changes behavior for the (formerly buggy)
fail-OPEN + unknown-5h/7d + scoped-exhausted combination.

Gates
  * bun run typecheck -> 0
  * bun run lint -> '1 info' (pre-existing biome-schema drift)
  * core bun test -> 34 pass / 0 fail (was 30; +4 MUST-1)
  * opencode bun test -> 851 pass / 0 fail (was 844; +7 MUST-2)
  * pi bun test -> 47 pass / 0 fail (unchanged)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files

Confidence score: 3/5

  • In packages/core/src/accounts.ts, scoped-driven killswitch blocks can return an overly aggressive Retry-After by taking the minimum across unrelated 5h/7d reset windows, which may throttle users longer than necessary and cause misleading backoff behavior — compute killswitchRetryAfterSeconds from the matching scoped reset window only before merging.
  • In packages/opencode/src/index.ts, resolveScopedDrivenBlock re-implements scoped-threshold checks already handled by core killswitchPassesPolicy, which risks cross-package drift and inconsistent block decisions as logic evolves — de-duplicate by delegating to the core policy path (or add parity tests) before merging.
Architecture diagram
sequenceDiagram
    participant Client
    participant API as API Route (opencode)
    participant KSW as killswitchPassesPolicy
    participant ScopeHelper as getScopedQuotaWindowForModel
    participant Storage as AccountStorage (config)
    participant Fallback as Fallback Filter Loop
    participant Resolve as resolveScopedDrivenBlock
    participant Formatter as formatKillswitchBlockMessage
    participant CLI as /claude-killswitch set

    Note over Client,CLI: NEW: Per-model scoped killswitch threshold

    %% Happy path: request with modelId, window healthy
    Client->>API: POST /v1/messages (modelId: "claude-fable-5")
    API->>KSW: killswitchPassesPolicy(quota, storage, accountId?, modelId)
    KSW->>KSW: Check killswitch enabled, normalize thresholds<br/>including scoped (default 0)
    KSW->>KSW: Evaluate five_hour (strict <) and seven_day (strict <)
    alt ModelId provided
        KSW->>ScopeHelper: getScopedQuotaWindowForModel(quota, modelId)
        ScopeHelper-->>KSW: scopedWindow or null
        alt scopedWindow exists AND remainingPercent finite AND remainingPercent <= thresholds.scoped
            KSW-->>API: false (block)
        else scopedWindow missing or above threshold
            KSW-->>API: true (pass)
        end
    else No modelId
        KSW-->>API: true (pass, no scoped check)
    end

    %% Main killed, routing to fallbacks
    API->>API: Eager killswitch refresh (QuotaManager)
    alt Main quota fails (killswitchPassesPolicy returned false)
        API->>Fallback: Loop over fallback accounts
        loop each fallback account
            Fallback->>KSW: killswitchPassesPolicy(fallbackQuota, storage, account.id, modelId)
            KSW-->>Fallback: true/false (scoped check included)
        end
        alt No fallbacks qualify
            API->>Resolve: resolveScopedDrivenBlock(mainQuota, requestModelId, storage)
            Resolve->>ScopeHelper: getScopedQuotaWindowForModel (same as above)
            ScopeHelper-->>Resolve: matchedWindow
            alt matchedWindow.remainingPercent <= thresholds.scoped
                Resolve-->>API: { isScopedDriven: true, modelName, modelId }
            else
                Resolve-->>API: { isScopedDriven: false }
            end
            API->>Formatter: formatKillswitchBlockMessage(retryAfterSeconds, modelName?)
            Formatter-->>API: "<Model> weekly limit reached" or "Killswitch: no routable accounts"
            API->>Client: 429 { type: "rate_limit_error", message }
        else Fallback account qualifies
            API->>Client: Route to fallback (normal flow)
        end
    else Main ok
        API->>Client: Normal response
    end

    %% CLI configuration flow
    rect rgb(240, 240, 240)
        Note over CLI,Storage: CLI configuration (/threshold)
        Client->>CLI: /claude-killswitch set main:5,10,0
        CLI->>CLI: parseKillswitchCommandAction()<br/>NEW: optional third number (scoped)
        CLI->>Storage: Save killswitch config with scoped field
        Storage-->>CLI: Confirmed
        CLI->>Client: Status table with Scoped column
    end

    Note over KSW: NEW: Scoped check runs BEFORE unknown-window fail-closed return<br/>so exhausted scoped window blocks even if 5h/7d windows are missing.
    Note over API: killswitchRetryAfterSeconds also includes scoped window's resetsAt<br/>when scopedModelId is provided.
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/core/src/accounts.ts
Comment thread packages/opencode/src/index.ts
Comment thread packages/core/src/accounts.ts
FINDING 1 (P1 — retry-after too aggressive):
killswitchRetryAfterSeconds was collecting BOTH 5h/7d resetsAt AND the
matched scoped window's resetsAt when scopedModelId was provided, then
returning Math.min(...). For a weekly scoped block the 5h reset
(hours away) always beat the weekly reset (days away), so the client
was told 'Retry in 2h' against a block that won't clear for days
-> retry-storm.

Fix: when scopedModelId is truthy, consider ONLY the matched scoped
window's resetsAt (skip the 5h/7d collection entirely); when absent,
the 5h/7d behavior is byte-identical to before. Restructured the
per-quota loop as if/else on scopedModelId so the two branches can't
accidentally compose again.

Adds 3 tests in packages/core/src/tests/killswitch.test.ts:
  * scoped branch REPLACES 5h/7d, not adds to it — main quota has
    5h+7d reset in 2h + Fable reset in 2 days; the 2-day reset must win
  * matched scoped window in a FALLBACK is the only source — same
    2h/2days shape on the fallback quota; the fallback 5h reset must
    also be ignored
  * no matched scoped window in any quota -> 300 fallback (the no-reset
    path, even with 5h present)

FINDING 2 (P2 — attribution ignores 5h/7d priority):
resolveScopedDrivenBlock only checked the scoped window's threshold.
It did NOT check whether 5h/7d already killed the account. When BOTH
5h/7d AND the Fable window are exhausted, the real block reason is
account-level (5h/7d), yet the function reported isScopedDriven: true
-> misleading 'Fable weekly limit reached' message + weekly retry-after
when the account is actually dead for ALL models.

Fix: guard BEFORE the scoped check with a no-modelId call to
killswitchPassesPolicy, which evaluates only 5h/7d. If it returns
false, 5h/7d drove the block -> account-level, NOT scoped-driven.
This also resolves cubic's cross-package-drift concern by reusing the
core gating priority rather than re-deriving 5h/7d logic in opencode.

Adds 3 tests in packages/opencode/src/tests/killswitch.test.ts:
  * BOTH 5h/7d AND Fable exhausted -> NOT scoped-driven (the key
    priority proof; the existing MUST-2 proof test is the inverse)
  * same with Sonnet request (no matching scoped window) ->
    NOT scoped-driven (5h/7d guard short-circuits before the
    scoped check)
  * HEALTHY 5h/7d + EXHAUSTED Fable -> still IS scoped-driven
    (regression lock: the 5h/7d guard must not over-trigger and
    turn a genuine scoped-driven block into account-level)

Existing 5h/7d behavior preserved
  * killswitchRetryAfterSeconds: all 5 opencode retries (3 from
    opencode tests + 2 from core tests) still pass; the no-scoped
    path is byte-identical
  * resolveScopedDrivenBlock: all 7 MUST-2 tests still pass
    (healthy-5h/7d + exhausted-Fable remains isScopedDriven: true,
    exactly as designed)

Gates
  * bun run typecheck -> 0
  * bun run lint -> '1 info' (pre-existing biome-schema drift)
  * core bun test -> 37 pass / 0 fail (was 34; +3 FINDING 1)
  * opencode bun test -> 773 pass / 0 fail (was 770; +3 FINDING 2)
  * pi bun test -> 47 pass / 0 fail (unchanged)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files

Confidence score: 3/5

  • In packages/opencode/src/tests/killswitch.test.ts, the MUST-2 boundary case uses remainingPercent values equal to the 5h/7d thresholds even though the logic is described as strict <, so the test may assert behavior that doesn’t match production edge conditions and could hide a real killswitch regression at exactly-threshold values—update the fixture/expectation (or explicitly change the operator semantics) before merging.
Architecture diagram
sequenceDiagram
    participant Client as API Client
    participant Router as Routing Engine
    participant KS as Killswitch Gate
    participant Quota as Quota Manager
    participant CLI as Killswitch CLI
    participant Storage as Account Storage

    Note over Client,Storage: NEW: Per-Model Scoped Killswitch Flow

    CLI->>Storage: set main:5,10,3 (fh, sd, scoped)
    Storage-->>CLI: Config updated
    CLI->>CLI: normalizeKillswitchThresholds(s)
    Note over CLI: Parsed thresholds: {five_hour: 5, seven_day: 10, scoped: 3}

    Client->>Router: Request with modelId (e.g. claude-fable-5)
    Router->>KS: killswitchPassesPolicy(quota, storage, accountId?, modelId?)
    
    alt Killswitch disabled
        KS-->>Router: true → pass through
    else Killswitch enabled
        KS->>KS: Check 5h window vs threshold (strict <)
        alt 5h remaining < threshold
            KS-->>Router: false → block (account-level)
        else 5h passes
            KS->>KS: Check 7d window vs threshold (strict <)
            alt 7d remaining < threshold
                KS-->>Router: false → block (account-level)
            else 7d passes
                Note over KS: NEW: Scoped check (runs BEFORE unknown-window fail-open)
                alt modelId provided
                    KS->>Quota: getScopedQuotaWindowForModel(quota, modelId)
                    Quota-->>KS: matching window or null
                    alt window found AND remainingPercent <= scoped threshold (inclusive)
                        KS-->>Router: false → block (model-scoped)
                    else window not found or above threshold
                        KS->>KS: Check sawUnknownWindow
                        alt failClosedOnUnknown
                            KS-->>Router: false
                        else failOpen
                            KS-->>Router: true
                        end
                    end
                else no modelId
                    KS->>KS: Check sawUnknownWindow (unchanged)
                end
            end
        end
    end

    Note over Router: NEW: Model-scoped 429 Response

    alt Block triggered
        Router->>Router: resolveScopedDrivenBlock()
        Note over Router: First: killswitchPassesPolicy WITHOUT modelId (5h/7d only)
        alt 5h/7d alone blocks
            Router->>Router: isScopedDriven = false (account-level)
        else 5h/7d passes (block is scoped-only)
            Router->>Router: check matched scoped window <= threshold
            Router->>Router: isScopedDriven = true
        end
        
        Router->>Router: killswitchRetryAfterSeconds(quota, fallbacks, now, scopedModelId?)
        alt scopedModelId present
            Router->>Router: Only consider scoped window's resetsAt
            Note over Router: Ignore 5h/7d resets (weekly window)
        else no scopedModelId
            Router->>Router: Check 5h + 7d resets (unchanged)
        end
        
        Router->>Router: formatKillswitchBlockMessage()
        alt scoped-driven
            Router->>Router: "Claude Fable 5 weekly limit reached..."
        else account-level
            Router->>Router: "Killswitch: no routable accounts..."
        end
        Router-->>Client: 429 + Retry-After header
    end

    Note over Router: NEW: Fallback Filter with modelId
    
    alt Main killed, checking fallbacks
        Router->>Router: Filter fallbacks with killswitchPassesPolicy(quota, storage, id, modelId)
        Note over Router: Accounts with exhausted Fable scoped are excluded from Fable routing
        alt Qualified fallbacks found
            Router->>Client: Route to fallback
        else No qualified fallbacks
            Router->>Client: 429 (same message as above)
        end
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/tests/killswitch.test.ts Outdated
The '5h/7d-driven block + healthy Fable' test set 5h/7d remaining to exactly
5/10 against thresholds of 5/10, but 5h/7d use strict '<', so 5<5 and 10<10
never fired — the test passed only because the Fable window was healthy, never
exercising the account-level priority guard (cubic P2). Lower remaining to 4/9
so the killswitch genuinely fires and the test traverses the guard branch. The
both-exhausted guard lock is already covered by the existing FINDING 2 test.
@ualtinok ualtinok merged commit 00f5ae1 into cortexkit:main Jul 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants