{"version":3,"file":"agent-session-retry.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAGvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAU5D,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC7B,gBAAgB,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAClF,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,gBAAgB,IAAI,YAAY,EAAE,CAAC;IACnC,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IACjD,sFAAsF;IACtF,aAAa,IAAI,IAAI,CAAC;IACtB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACrC;AAED,qBAAa,mBAAmB;IAMnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,gBAAgB,CAA0C;IAClE,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,QAAQ,CAAuC;IAEvD,YAA6B,IAAI,EAAE,aAAa,EAAI;IAEpD,gDAAgD;IAChD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,+CAA+C;IAC/C,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAiBnD;IAED;;;;;;OAMG;IACH,wBAAwB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAkBhD;IAED,OAAO,CAAC,4BAA4B;IAUpC;;;;OAIG;IACH,6BAA6B,IAAI,IAAI,CASpC;IAED,wCAAwC;IACxC,OAAO,IAAI,IAAI,CAMd;IAED;;;OAGG;IACG,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwEtE;IAED;;OAEG;IACH,KAAK,IAAI,IAAI,CAIZ;IAED;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAOlC;CACD","sourcesContent":["/**\n * Auto-retry controller for AgentSession.\n *\n * Owns the retry lifecycle for transient assistant errors (overloaded, rate\n * limit, server/network/transport failures): deciding whether an error is\n * retryable, arming the retry promise synchronously on agent_end, backing off\n * exponentially, and re-driving the agent via continue(). Context-overflow\n * errors are intentionally excluded here — those are handled by compaction.\n */\n\nimport type { AgentEvent, AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport { isContextOverflow, isLongRetryDelayError } from \"@kolisachint/hoocode-ai\";\nimport { sleep } from \"../utils/sleep.js\";\nimport type { AgentSessionEvent } from \"./agent-session.js\";\n\n/**\n * Retryable error signatures (overloaded, rate limit, server/network errors,\n * transport closes). Compiled once at module load instead of on every assistant\n * response. Context-overflow errors are handled separately by compaction.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;\n\n/** Narrow dependencies the retry controller needs from AgentSession. */\nexport interface AutoRetryDeps {\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number };\n\tgetModel(): Model<any> | undefined;\n\tgetAgentMessages(): AgentMessage[];\n\tsetAgentMessages(messages: AgentMessage[]): void;\n\t/** Fire-and-forget continue() on the agent (errors surface on the next agent_end). */\n\tcontinueAgent(): void;\n\twaitForAgentIdle(): Promise<void>;\n\temit(event: AgentSessionEvent): void;\n}\n\nexport class AutoRetryController {\n\tprivate _abortController: AbortController | undefined = undefined;\n\tprivate _attempt = 0;\n\tprivate _promise: Promise<void> | undefined = undefined;\n\tprivate _resolve: (() => void) | undefined = undefined;\n\n\tconstructor(private readonly deps: AutoRetryDeps) {}\n\n\t/** Current retry attempt (0 if not retrying) */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** Whether a retry is currently in progress */\n\tget isRetrying(): boolean {\n\t\treturn this._promise !== undefined;\n\t}\n\n\t/**\n\t * Check if an error is retryable (overloaded, rate limit, server errors).\n\t * Context overflow errors are NOT retryable (handled by compaction instead).\n\t */\n\tisRetryableError(message: AssistantMessage): boolean {\n\t\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\n\t\t// Context overflow is handled by compaction, not retry\n\t\tconst contextWindow = this.deps.getModel()?.contextWindow ?? 0;\n\t\tif (isContextOverflow(message, contextWindow)) return false;\n\n\t\t// An exhausted quota answers 429 the same way a burst rate limit does,\n\t\t// and the pattern below cannot tell them apart — both say \"429\". The\n\t\t// difference is in how long the provider asked us to wait: seconds mean\n\t\t// try again, weeks mean this session is not getting through no matter\n\t\t// how patiently it backs off. Retrying the second kind spends the whole\n\t\t// budget in a few seconds and reports failure as if the network were at\n\t\t// fault.\n\t\tif (isLongRetryDelayError(message.errorMessage)) return false;\n\n\t\treturn RETRYABLE_ERROR_PATTERN.test(message.errorMessage);\n\t}\n\n\t/**\n\t * Create the retry promise synchronously when an agent_end carries a\n\t * retryable error. Agent.emit() runs handlers synchronously and prompt()\n\t * calls waitForRetry() as soon as agent.prompt() resolves; arming the promise\n\t * here (rather than inside async event processing) ensures waitForRetry()\n\t * never misses an in-flight retry.\n\t */\n\tcreatePromiseForAgentEnd(event: AgentEvent): void {\n\t\tif (event.type !== \"agent_end\" || this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst lastAssistant = this._findLastAssistantInMessages(event.messages);\n\t\tif (!lastAssistant || !this.isRetryableError(lastAssistant)) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis._promise = new Promise((resolve) => {\n\t\t\tthis._resolve = resolve;\n\t\t});\n\t}\n\n\tprivate _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined {\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = messages[i];\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\treturn message as AssistantMessage;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Reset the attempt counter after a successful assistant response.\n\t * Callers invoke this only for non-error responses; it emits a success event\n\t * when a retry was in progress.\n\t */\n\tonSuccessfulAssistantResponse(): void {\n\t\tif (this._attempt > 0) {\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: true,\n\t\t\t\tattempt: this._attempt,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t}\n\t}\n\n\t/** Resolve the pending retry promise */\n\tresolve(): void {\n\t\tif (this._resolve) {\n\t\t\tthis._resolve();\n\t\t\tthis._resolve = undefined;\n\t\t\tthis._promise = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Handle retryable errors with exponential backoff.\n\t * @returns true if retry was initiated, false if max retries exceeded or disabled\n\t */\n\tasync handleRetryableError(message: AssistantMessage): Promise<boolean> {\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\n\t\t// Retry promise is created synchronously in createPromiseForAgentEnd for agent_end.\n\t\t// Keep a defensive fallback here in case a future refactor bypasses that path.\n\t\tif (!this._promise) {\n\t\t\tthis._promise = new Promise((resolve) => {\n\t\t\t\tthis._resolve = resolve;\n\t\t\t});\n\t\t}\n\n\t\tthis._attempt++;\n\n\t\tif (this._attempt > settings.maxRetries) {\n\t\t\t// Max retries exceeded, emit final failure and reset\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt: this._attempt - 1,\n\t\t\t\tfinalError: message.errorMessage,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t\tthis.resolve(); // Resolve so waitForRetry() completes\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = settings.baseDelayMs * 2 ** (this._attempt - 1);\n\n\t\tthis.deps.emit({\n\t\t\ttype: \"auto_retry_start\",\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: settings.maxRetries,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove error message from agent state (keep in session for history)\n\t\tconst messages = this.deps.getAgentMessages();\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t}\n\n\t\t// Wait with exponential backoff (abortable)\n\t\tthis._abortController = new AbortController();\n\t\ttry {\n\t\t\tawait sleep(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted during sleep - emit end event so UI can clean up\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis._abortController = undefined;\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt,\n\t\t\t\tfinalError: \"Retry cancelled\",\n\t\t\t});\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\t\tthis._abortController = undefined;\n\n\t\t// Retry via continue() - use setTimeout to break out of event handler chain\n\t\tsetTimeout(() => {\n\t\t\tthis.deps.continueAgent();\n\t\t}, 0);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Cancel in-progress retry.\n\t */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t\t// Note: _attempt is reset in the catch block of handleRetryableError\n\t\tthis.resolve();\n\t}\n\n\t/**\n\t * Wait for any in-progress retry to complete.\n\t * Returns immediately if no retry is in progress.\n\t */\n\tasync waitForRetry(): Promise<void> {\n\t\tif (!this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._promise;\n\t\tawait this.deps.waitForAgentIdle();\n\t}\n}\n"]}