Skip to content

fix(agent-core): drop vacuous assistant messages that permanently wedge sessions#1968

Merged
RealKai42 merged 7 commits into
mainfrom
kaiyi/ashgabat
Jul 21, 2026
Merged

fix(agent-core): drop vacuous assistant messages that permanently wedge sessions#1968
RealKai42 merged 7 commits into
mainfrom
kaiyi/ashgabat

Conversation

@RealKai42

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — the problem is explained below (diagnosed from a user-exported debug session).

Problem

After a provider safety filter blocks a response (finish_reason: content_filter), the session can get permanently stuck. The filtered stream carries an empty reasoning_content field, which the provider adapter records as an empty thinking part; the loop then seals an assistant message whose only content is that empty thinking part into the conversation history. The projection's empty-message guard only counted parts, so the message survived; on the wire it serialized as {role:'assistant', reasoning_content:''} — no content, no tool calls — and the provider rejected every subsequent request with 400 the message at position N with role 'assistant' must not be empty. Every later turn (resume, steer, cron) re-sent the same poisoned history and failed at the same position. The structural-error recovery did not recognize this 400 phrasing, so the strict resend never fired either.

What changed

  • The request projection now drops a whole message when it carries no tool calls and every recorded part serializes to nothing (empty/whitespace text, or an empty unsigned thinking block). Content-bearing messages keep every part verbatim — including empty thinking blocks, which preserved-thinking providers require round-tripped, and signature-bearing thinking blocks, which reasoning providers require back. This applies to both the legacy agent-core projector and the mirrored agent-core-v2 projector, so already-poisoned sessions heal on their next request.
  • The drop surfaces as a new vacuous_message_dropped projection anomaly, counted in the repair warning log and the context_projection_repaired telemetry event, so this class of history corruption stays observable.
  • The structural-request-error patterns now recognize the provider's "must not be empty" rejection (kosong and the v2 llmProtocol layer), so the strict resend self-heals any residual path that still produces such a message.

Tests: new projection cases in both engines (empty-think-only dropped; empty think kept on content-bearing messages; signed empty think kept; whitespace-only think dropped; tool-calling assistant kept), pattern-match cases for the new 400 phrasing in both error classifiers, and an updated telemetry payload assertion. Verified end-to-end against the real incident wire log: replaying it through the projection no longer emits the poisoned message.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

…ge sessions

A provider-filtered response can seal an assistant message holding only an
empty thinking part into the recorded history. The projection's empty-message
guard only counted parts, so it survived, and was serialized as an assistant
message with no content and no tool calls — which the provider rejects with
"the message at position N with role 'assistant' must not be empty" on
every resend, permanently wedging the session.

The projection now drops any message whose parts all serialize to nothing
(empty/whitespace text, or empty unsigned thinking). Content-bearing
messages keep every part verbatim, including empty thinking blocks that
preserved-thinking providers require back. Drops surface through a new
vacuous_message_dropped projection anomaly with log/telemetry counters, and
the structural-error recovery now recognizes this provider rejection so the
strict resend self-heals any residual path. Mirrored in agent-core-v2.
@changeset-bot

changeset-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d7a59ad

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

This PR includes changesets to release 4 packages
Name Type
@moonshot-ai/agent-core Patch
@moonshot-ai/agent-core-v2 Patch
@moonshot-ai/kosong Patch
@moonshot-ai/kimi-code 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 20, 2026

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

commit: d7a59ad

@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: 1c0bce6d17

ℹ️ 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 thread packages/agent-core-v2/src/agent/contextProjector/contextProjectorService.ts Outdated
The v2 comment convention keeps comments solely in the top-of-file block.
Move the vacuous-message drop rationale there and drop the inline/JSDoc
comments added beside statements in the projector service, the llmProtocol
error patterns, and the projector tests. No behavior change.
The loop-event fold already dropped a settled assistant message when it was
structurally empty (no content, no tool calls), but a step that recorded
only an empty thinking part — e.g. a provider-filtered response carrying an
empty reasoning field — survived into history, relying on the projection to
keep it off the wire. Widen the settle condition to drop the step whenever
nothing sendable was recorded (no tool calls; every content part vacuous),
using a predicate now shared with the context projector. This heals stored
v2 histories at restore time instead of only at request time.

@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: 23229bf4b7

ℹ️ 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 thread packages/agent-core/src/agent/context/projector.ts
…cuous content

The strict resend's dedupe pass could re-create the exact empty-assistant
shape the projection now guards against: when every tool call a message
carried was removed as a later duplicate, a remainder holding only an empty
thinking part was kept (its content was non-empty), serialized as an
assistant message with no content and no tool calls, and rejected again
with "the message at position N with role 'assistant' must not be empty"
— the same failure the strict resend was recovering from.

The keep condition now requires sendable content, so such a message is
dropped wholesale with a vacuous_message_dropped anomaly alongside the
duplicate-tool-call one. Mirrored in agent-core-v2.

@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: f36d46a3c9

ℹ️ 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 thread packages/agent-core-v2/src/agent/contextMemory/loopEventFold.ts
…educer

The live loop-event fold drops a settled assistant step when nothing
sendable was recorded (no tool calls; every content part vacuous), but the
cold transcript reducer still pushed an assistant on every step.begin and
never dropped it. Cold readers (snapshot / messages) therefore kept
showing output-free phantom assistants — including one per failed retry
attempt, which predates the vacuous-step case — and foldedLength overcounted
the live folded history, which can misplace the unflushed-tail splice in
the messages view while a turn is in flight.

The reducer now settles steps the same way the fold does: at a step's end
or at the next begin, an assistant with no tool calls and only vacuous
content is removed and foldedLength is decremented. Steps carrying any
sendable output — real text, real thinking, signed thinking, or tool calls
— are kept verbatim.

@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: 4302327a8c

ℹ️ 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 thread packages/agent-core-v2/src/agent/contextMemory/vacuousContent.ts Outdated

@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: b28a6f9845

ℹ️ 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 +220 to 221
if (open.toolCalls.length === 0 && open.content.every(isVacuousContentPart)) {
return [...closed.slice(0, index), ...closed.slice(index + 1)];

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 legacy compaction tails when dropping vacuous steps

When replaying an older v2 wire log whose context.apply_compaction record lacks keptUserMessageCount, buildContextCompactionShape keeps the old tail with history.slice(compactedCount). If a vacuous assistant before that compaction is now removed here, the replayed history is shorter than the persisted compactedCount, so the slice skips the first real retained tail message; for example [user, empty-think step, user] with compactedCount: 2 replays to just the summary instead of summary plus the tail user. Please compensate the legacy count for dropped steps or avoid shrinking the replay state before legacy compaction applies.

Useful? React with 👍 / 👎.

@RealKai42
RealKai42 merged commit 71bcfba into main Jul 21, 2026
14 checks passed
@RealKai42
RealKai42 deleted the kaiyi/ashgabat branch July 21, 2026 03:04
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