@@ -234,6 +234,7 @@ func (s *Session) runLoop(ctx context.Context, sys promptSpec, messages *[]wire.
234234 Effort : eff , MaxTokens : maxTok ,
235235 Difficulty : s .turnDifficulty , // the judge's tier verdict → the ladder's difficulty input
236236 BillingLane : s .turnBillingLane (), // "" normally; "credits" after an explicit BYOK-failure consent
237+ LaneBypass : s .turn .laneBypass , // "" normally; "gateway" after a consented lane-exhaustion fallback
237238 // Escalation signals only the CLI can see (self-heal, room friction,
238239 // high-risk surfaces) — inputs to the CLI's own semantic ladder.
239240 RoutingHint : s .turnRoutingHint (),
@@ -307,6 +308,19 @@ func (s *Session) runLoop(ctx context.Context, sys promptSpec, messages *[]wire.
307308 s .printf ("\n ■ %s\n " , metaStyle .Render ("Your API key was rejected — fix or remove it with /apikeys" ))
308309 return iterations , false , nil
309310 }
311+ // A subscription/own-key lane hit its usage window. Same doctrine as
312+ // BYOK: never SILENTLY move spend — the only path onto credits is an
313+ // explicit per-vendor choice, sticky for this session. Headless
314+ // sessions fail with the reason (and the reset time when known).
315+ var laneExh * provider.ErrLaneExhausted
316+ if errors .As (err , & laneExh ) {
317+ if s .consentLaneFallback (ctx , laneExh ) {
318+ s .printf ("\n %s\n " , metaStyle .Render ("⊙ " + laneExh .Error ()+ " — continuing on memcode credits (session choice; /status to review)" ))
319+ continue
320+ }
321+ s .printf ("\n ■ %s\n " , metaStyle .Render (laneExh .Error ()+ " — turn stopped. Pick another family with /model, or wait for the window." ))
322+ return iterations , false , nil
323+ }
310324 // Token rejected (401): the SESSION is signed out (expired or revoked
311325 // key). Disconnect the provider so the front-end's signed-out gate
312326 // takes over (no more doomed dispatches), and say what fixes it.
@@ -1316,6 +1330,43 @@ func estimateTokens(chars int) int {
13161330 return chars / 4
13171331}
13181332
1333+ // consentLaneFallback asks (once per vendor per session) how to continue when
1334+ // a lane's subscription/key hits its usage window. True = serve this and
1335+ // future exhausted turns for that vendor on memcode credits via the gateway.
1336+ func (s * Session ) consentLaneFallback (ctx context.Context , exh * provider.ErrLaneExhausted ) bool {
1337+ if ! exh .CanFallback {
1338+ return false // no gateway base — nothing to fall back to
1339+ }
1340+ if s .laneFallback == nil {
1341+ s .laneFallback = map [string ]string {}
1342+ }
1343+ if choice , ok := s .laneFallback [exh .Lane .Vendor ]; ok {
1344+ if choice == "gateway" {
1345+ s .turn .laneBypass = "gateway"
1346+ return true
1347+ }
1348+ return false
1349+ }
1350+ if s .ask == nil || s .purpose != llm .MainLoop {
1351+ return false // headless / sub-agent: fail with the reason
1352+ }
1353+ resp := s .ask (ctx , AskRequest {
1354+ Question : exh .Error () + ". How should this session continue when it's exhausted?" ,
1355+ Options : []AskOption {
1356+ {Label : "Stop the turn" , Description : "Wait for the window, or /model another family" },
1357+ {Label : "Continue on memcode credits" , Description : "This and future exhausted turns — billed to your credit balance" },
1358+ },
1359+ })
1360+ ans := strings .ToLower (strings .TrimSpace (resp .Answer ))
1361+ if strings .HasPrefix (ans , "continue" ) {
1362+ s .laneFallback [exh .Lane .Vendor ] = "gateway"
1363+ s .turn .laneBypass = "gateway"
1364+ return true
1365+ }
1366+ s .laneFallback [exh .Lane .Vendor ] = "stop"
1367+ return false
1368+ }
1369+
13191370// turnBillingLane returns the billing-lane extension for this turn's model
13201371// calls: "credits" only after the user explicitly consented to serve this
13211372// turn on memcode credits (a BYOK key failure); "" otherwise (byok-preferred,
0 commit comments