fix(agent): add explicit provider retry lifecycle - #2020
Conversation
📝 WalkthroughWalkthroughDeepChat now models logical rounds separately from provider request sequences and physical attempts. Coordinator-owned retries use bounded backoff, structured failure metadata, abort propagation, per-attempt Tape provenance, and deterministic trace replay selection, with updated runtime contracts, migrations, documentation, and tests. ChangesProvider retry lifecycle and attempt provenance
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/provider/providers/acpProvider.ts (1)
373-431: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAborting mid-catch can skip ACP session cleanup, leaking the session/permissions.
signal?.throwIfAborted()at Line 426 runs beforequeue.push/queue.done(). Ifsessionwas already assigned bysessionController.open(...)and a later synchronous step (e.g.messageFormatter.format) throws while the signal happens to be aborted, this throw propagates out ofcoreStreamimmediately — bypassing the secondtry { while… } finally { … }block entirely. Thatfinallyis the only place that cancels the ACP connection, callsclearMappedSession, and resolves pending permissions, so none of that cleanup runs.🔧 Proposed fix: guarantee cleanup regardless of where the throw originates
- try { - const acpEnabled = await this.agentSettings.getAcpEnabled() - ... - } catch (error) { - signal?.throwIfAborted() - const message = - error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error' - queue.push(createStreamEvent.error(`ACP: ${message}`, extractProviderFailureMetadata(error))) - queue.done() - } - - try { - while (true) { - const event = await queue.next() - if (event === null) break - yield event - } - signal?.throwIfAborted() - } finally { - if (session) { - ... - } - } + try { + const acpEnabled = await this.agentSettings.getAcpEnabled() + ... + } catch (error) { + const message = + error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error' + queue.push(createStreamEvent.error(`ACP: ${message}`, extractProviderFailureMetadata(error))) + queue.done() + signal?.throwIfAborted() + } + + try { + while (true) { + const event = await queue.next() + if (event === null) break + yield event + } + signal?.throwIfAborted() + } finally { + if (session) { + ... + } + }Moving
signal?.throwIfAborted()to after the queue is drained/marked done ensures cleanup is always reachable, and abort is still surfaced via the second block's ownthrowIfAborted()check.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/provider/providers/acpProvider.ts` around lines 373 - 431, In the catch block around session setup and prompt formatting, move signal?.throwIfAborted() until after queue.push(createStreamEvent.error(...)) and queue.done(). Preserve the existing error-message and metadata handling, ensuring aborted errors do not escape before the downstream cleanup finally block can cancel the ACP session, clear the mapped session, and resolve pending permissions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/main/provider/providers/acpProvider.ts`:
- Around line 373-431: In the catch block around session setup and prompt
formatting, move signal?.throwIfAborted() until after
queue.push(createStreamEvent.error(...)) and queue.done(). Preserve the existing
error-message and metadata handling, ensuring aborted errors do not escape
before the downstream cleanup finally block can cancel the ACP session, clear
the mapped session, and resolve pending permissions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4a824170-52e4-432b-8d60-0caa3e8c2786
📒 Files selected for processing (57)
docs/architecture/agent-system.mddocs/architecture/cache-aware-context-runtime/spec.mddocs/architecture/tape-system.mddocs/issues/provider-retry-lifecycle/spec.mdsrc/main/agent/deepchat/loop/contextCoordinator.tssrc/main/agent/deepchat/loop/deepChatLoopEngine.tssrc/main/agent/deepchat/loop/loopRun.tssrc/main/agent/deepchat/loop/providerRetryPolicy.tssrc/main/agent/deepchat/runtime/deepChatLoopRunner.tssrc/main/agent/deepchat/runtime/process.tssrc/main/agent/deepchat/runtime/types.tssrc/main/provider/aiSdk/runtime.tssrc/main/provider/aiSdk/streamAdapter.tssrc/main/provider/baseProvider.tssrc/main/provider/index.tssrc/main/provider/providerFailure.tssrc/main/provider/providers/acpProvider.tssrc/main/provider/providers/aiSdkProvider.tssrc/main/provider/providers/githubCopilotProvider.tssrc/main/provider/providers/ollamaProvider.tssrc/main/provider/providers/voiceAIProvider.tssrc/main/session/data/tables/deepchatMessageTraces.tssrc/main/session/data/transcript.tssrc/main/session/query.tssrc/main/tape/application/providerAttemptService.tssrc/main/tape/application/viewReplayService.tssrc/main/tape/domain/providerAttempt.tssrc/main/tape/ports/application.tssrc/shared/types/agent-interface.d.tssrc/shared/types/core/llm-events.tssrc/shared/types/provider-attempt.tssrc/shared/types/provider.tssrc/shared/types/tape-replay.tstest/main/agent/deepchat/loop/contextCoordinator.test.tstest/main/agent/deepchat/loop/deepChatLoopEngine.test.tstest/main/agent/deepchat/loop/loopRun.test.tstest/main/agent/deepchat/loop/providerRetryPolicy.test.tstest/main/agent/deepchat/runtime/deepChatRuntimeCoordinator.test.tstest/main/agent/deepchat/runtime/process.test.tstest/main/provider/acpProvider.test.tstest/main/provider/aiSdkRuntime.test.tstest/main/provider/aiSdkStreamAdapter.test.tstest/main/provider/coreEvents.test.tstest/main/provider/githubCopilotProvider.test.tstest/main/provider/ollamaProviderCancellation.test.tstest/main/provider/openAICompatibleProvider.test.tstest/main/provider/providerFailure.test.tstest/main/provider/providerRuntime.test.tstest/main/provider/voiceAIProvider.test.tstest/main/session/data/tables/deepchatMessageTraces.test.tstest/main/session/data/tapeRecall.test.tstest/main/session/data/tapeTestHarness.tstest/main/session/data/tapeViewReplay.test.tstest/main/session/data/transcript.test.tstest/main/session/query.test.tstest/main/session/runtimeIntegration.test.tstest/main/session/session.integration.test.ts
Summary
Make DeepChat the explicit owner of provider retries and add durable attempt provenance across the agent loop, Tape, and message traces.
Transient failures are retried only before semantic output is committed. Context recovery remains a separate payload-changing flow and does not consume the logical-round or transient-retry budget.
Changes
logicalRound: one model response and tool-settlement cyclerequestSeq: one immutable provider payload and ViewManifestphysicalAttempt: one actual send of that requestRetry-AfterhandlingAbortSignalthrough AI SDK, Ollama, GitHub Copilot, Voice, and ACP streams.provider/attempt_completedTape records using schema v2 while retaining v1 compatibility.Compatibility
providerRoundsandmaxProviderRoundsnames are unchanged.Summary by CodeRabbit
New Features
Bug Fixes
Documentation