This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

loop guard is now a nudge

+56 -6
+11
src/runner/loop-completion.ts
··· 612 612 promptTooLargeAttempts++ 613 613 if (recovered) continue 614 614 } 615 + if (shouldFallback(fallbackErr)) { 616 + const retryAfter = retryDelayMs(fallbackErr) 617 + console.warn( 618 + `[fallback] transient failure (${errorSummary(fallbackErr)}); retrying after ${Math.ceil(retryAfter / 1000)}s`, 619 + ) 620 + console.log( 621 + `[runner] backing off ${Math.ceil(retryAfter / 1000)}s (until ${formatRetryAt(retryAfter)}) before retrying fallback...`, 622 + ) 623 + await sleep(retryAfter) 624 + continue 625 + } 615 626 logApiError(fallbackErr, `model=${FALLBACK_MODEL} api=${FALLBACK_BASE}`) 616 627 throw fallbackErr 617 628 }
+2 -1
src/runner/loop.ts
··· 41 41 } 42 42 43 43 async function stopLoopForGuard(state: LoopState, hooks: LoopHooks, reason: string): Promise<RunLoopExit> { 44 - const guardMessage = `[system] safety stop: ${reason}. pausing until a new external event wakes niri again.` 44 + const guardMessage = 45 + `[system] hey, you've been going for a while (${reason}). are you stuck and need to tell who you're talking to there's a problem? or is nothing happening and maybe it's time for a rest? remember to tell your important people you are resting if you are going to.` 45 46 console.warn(`[runner] ${reason}`) 46 47 state.conversation.push({ 47 48 role: "user",
+17 -1
src/runner/util.test.ts
··· 1 1 import assert from "node:assert/strict" 2 2 import test from "node:test" 3 - import { sanitizeMessages } from "./util.js" 3 + import { isTransientTransportError, sanitizeMessages, shouldFallback } from "./util.js" 4 4 5 5 test("sanitizeMessages backfills empty reasoning_content for assistant history", () => { 6 6 const messages = sanitizeMessages([ ··· 21 21 assert.equal(assistant.role, "assistant") 22 22 assert.equal(assistant.reasoning_content, "") 23 23 }) 24 + 25 + test("terminated fetch errors are treated as retryable transport failures", () => { 26 + const err = new TypeError("terminated") 27 + 28 + assert.equal(isTransientTransportError(err), true) 29 + assert.equal(shouldFallback(err), true) 30 + }) 31 + 32 + test("nested undici errors are treated as retryable transport failures", () => { 33 + const cause = new Error("other side closed") 34 + const nested = new TypeError("fetch failed") as TypeError & { cause?: unknown } 35 + nested.cause = cause 36 + 37 + assert.equal(isTransientTransportError(nested), true) 38 + assert.equal(shouldFallback(nested), true) 39 + })
+26 -4
src/runner/util.ts
··· 632 632 if (!err.status || err.status === 429 || err.status >= 500) return true 633 633 return false 634 634 } 635 - // Node fetch errors (ECONNREFUSED, ENOTFOUND, ETIMEDOUT…) 636 - if (err instanceof Error) { 637 - return /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|fetch failed/i.test(err.message) 635 + return isTransientTransportError(err) 636 + } 637 + 638 + function errorCauseChainText(err: unknown): string { 639 + const parts: string[] = [] 640 + let current: unknown = err 641 + 642 + for (let depth = 0; depth < 4 && current instanceof Error; depth++) { 643 + parts.push(current.name, current.message) 644 + const withMetadata = current as Error & { code?: unknown; cause?: unknown } 645 + if (typeof withMetadata.code === "string") parts.push(withMetadata.code) 646 + current = withMetadata.cause 638 647 } 639 - return false 648 + 649 + return parts.join("\n") 650 + } 651 + 652 + /** 653 + * Detects retryable network/stream failures thrown below the OpenAI SDK. 654 + */ 655 + export function isTransientTransportError(err: unknown): boolean { 656 + if (!(err instanceof Error)) return false 657 + 658 + const text = errorCauseChainText(err) 659 + return /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|EPIPE|UND_ERR|fetch failed|terminated|socket hang up|other side closed|aborted/i.test( 660 + text, 661 + ) 640 662 } 641 663 642 664 const PROMPT_TOO_LARGE_PHRASES = [