Skip to content

fix(kosong): retry a dropped provider stream (terminated) - #1274

Merged
RealKai42 merged 1 commit into
mainfrom
kaiyi/fix-session-recovery-terminated
Jul 1, 2026
Merged

fix(kosong): retry a dropped provider stream (terminated)#1274
RealKai42 merged 1 commit into
mainfrom
kaiyi/fix-session-recovery-terminated

Conversation

@RealKai42

@RealKai42 RealKai42 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Problem

A raw undici TypeError: terminated — raised when an SSE/HTTP response body is dropped mid-flight, common on long streaming responses — fell through convertAnthropicError to a generic base ChatProviderError. isRetryableGenerateError treats that base class as terminal, so the dropped stream was never retried and failed the turn outright. The OpenAI path already recognized this signature and retried; the Anthropic path did not.

Fix

Route raw, non-SDK errors in convertAnthropicError through a shared classifyBaseApiError heuristic (promoted from the OpenAI provider into errors.ts so both providers classify transport errors the same way). A dropped connection / terminated becomes a retryable APIConnectionError, a timeout becomes APITimeoutError, and anything else stays a non-retryable base ChatProviderError — so a transient stream drop is retried instead of failing the turn.

Tests

convertAnthropicError(new TypeError('terminated')) is asserted to be a retryable APIConnectionError, both as a unit case and through streamed SSE iteration (mirroring the existing OpenAI-path tests). Full kosong suite green (1126 tests), typecheck clean.

Note: this PR was narrowed to just the retry classification. The broader session-recovery work explored earlier (daemon busy-state reconciliation, TUI busy-flag reset) was dropped as its edge cases (compaction-deferred prompts, shell busy state, queue ordering) warrant a separate, more careful change.

@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e8698c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kosong Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 1, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@5e8698c
npx https://pkg.pr.new/@moonshot-ai/kimi-code@5e8698c

commit: 5e8698c

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b57c798f54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +315 to +318
const closingPrefix = `${sessionId}${String.fromCharCode(0)}`;
for (const activeKey of [...this._active.keys()]) {
if (activeKey.startsWith(closingPrefix)) this._active.delete(activeKey);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear active prompts on the real close path

These deletes only run from ISessionService.onDidClose, but the normal SDK/TUI close path calls Session.close()rpc.closeSession, and CoreImpl.closeSession removes the core session without firing that service event; SessionService.onDidClose is only fired by archive. In the close/reopen-after-dropped-stream case this patch is meant to fix, _active therefore survives and the next submit can still queue behind the stale prompt, so the cleanup needs to be triggered from the actual closeSession path as well.

Useful? React with 👍 / 👎.

Comment on lines +860 to +866
if (
this.host.state.appState.streamingPhase !== 'idle' &&
!this.host.streamingUI.hasActiveTurn()
) {
this.host.setAppState({ streamingPhase: 'idle' });
this.host.resetLivePane();
const next = this.host.shiftQueuedMessage();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore stale turn errors before draining queued input

Agent turn failures emit turn.ended and then a separate error event (packages/agent-core/src/agent/turn/index.ts:580-599). If a failed turn has queued input, finalizeTurn shifts one message and schedules it with setTimeout(0); when that new prompt is in waiting but has not received turn.started yet, a delayed error from the previous turn satisfies this condition, resets the new request to idle, and shifts another queued message. Users with multiple queued messages after a failed turn can therefore have prompts dequeued/submitted out of order; this recovery should be gated to errors for the current request rather than any no-active-turn error.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 11022106d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +863 to +864
this.host.setAppState({ streamingPhase: 'idle' });
this.host.resetLivePane();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drain queued input after pre-turn failures

When the first prompt fails before any turn.started, the user can already have typed another message while streamingPhase was waiting. This branch is the only recovery path because no turn.ended will arrive, but it sets the UI back to idle without shifting/sending state.queuedMessages, so the queued message stays stuck until some later action, and a later manual prompt can run ahead of it. Please drain the next queued item on the current pre-turn-error path, with a stale-error guard as needed.

Useful? React with 👍 / 👎.

{ sid, promptId: existing.promptId, agentId: existing.agentId },
'[prompt-service] releasing stale active prompt: core reports no turn in flight',
);
this._active.delete(key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve daemon queue order when releasing stale prompts

If prompt B was queued while prompt A was genuinely active, then A dies without turn.ended and prompt C is submitted, this new stale-active branch deletes A and starts C immediately while B remains in _queued. Since _startNextQueued only runs after C ends, the daemon executes C before the older queued B. When clearing a stale active slot, promote the existing queue first or enqueue the new state behind it so submitted prompts keep FIFO order.

Useful? React with 👍 / 👎.

@RealKai42

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 11022106d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// flight, the slot is stale, so release it and start this prompt instead
// of queueing behind a phantom forever. This is what keeps "send a
// message" and "resume" from being permanently blocked.
if (await this._coreHasActiveTurn(sid)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep compaction-buffered prompts active

When the first prompt is only buffered by manual/SDK compaction, agent-core has no active turn yet (TurnFlow.launch buffers while fullCompaction.isCompacting, and hasActiveTurn is false until a real active turn exists). A second submit during that window now treats the existing prompt as stale, deletes _active, and starts the new prompt instead of queueing it, so the first buffered turn's later turn.started/turn.ended can be attributed to the wrong prompt and the original prompt never gets a prompt lifecycle event.

Useful? React with 👍 / 👎.

Comment on lines +863 to +864
this.host.setAppState({ streamingPhase: 'idle' });
this.host.resetLivePane();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drain queued input after pre-turn errors

If the user submits another message while the current request is still in waiting and the provider then emits an error before any turn.started, this branch correctly notes that no turn.ended will arrive but only marks the UI idle. The queued message remains in queuedMessages because queue draining only happens from finalizeTurn/shell completion, so it can sit there until a later unrelated turn completes and then be sent out of order.

Useful? React with 👍 / 👎.

…ropic path

A raw undici `terminated` error — an SSE/HTTP response body cut mid-flight,
common on long streaming responses — fell through convertAnthropicError to a
generic base ChatProviderError, which isRetryableGenerateError treats as fatal,
so it was never retried. Route raw non-SDK errors through the shared
classifyBaseApiError heuristic (already used by the OpenAI path) so a dropped
stream is classified as a retryable APIConnectionError and retried instead of
failing the turn.
@RealKai42
RealKai42 force-pushed the kaiyi/fix-session-recovery-terminated branch from 1102210 to 5e8698c Compare July 1, 2026 14:32
@RealKai42 RealKai42 changed the title fix: recover session after a mid-stream terminated error fix(kosong): retry a dropped provider stream (terminated) Jul 1, 2026
@RealKai42
RealKai42 merged commit 074bb9b into main Jul 1, 2026
10 checks passed
@RealKai42
RealKai42 deleted the kaiyi/fix-session-recovery-terminated branch July 1, 2026 14:49
@github-actions github-actions Bot mentioned this pull request Jul 1, 2026
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.

1 participant