Skip to content

fix(agent-core-v2): normalize interrupted turns on restore and drop retired-generation events - #3224

Open
sailist wants to merge 1 commit into
MoonshotAI:mainfrom
sailist:bug-077-08-24-restore-dangling-turn-crash
Open

fix(agent-core-v2): normalize interrupted turns on restore and drop retired-generation events#3224
sailist wants to merge 1 commit into
MoonshotAI:mainfrom
sailist:bug-077-08-24-restore-dangling-turn-crash

Conversation

@sailist

@sailist sailist commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

Follow-up to #3206.

Problem

#3206 stopped the crash loop and made agent scope teardown awaitable, but two gaps remain when a session is restored with a dangling turn (journal ends mid-turn, e.g. the last record is turn.step.started after a process kill):

  1. The turn stays open forever — the restored session carries a turn that never terminates, and a fresh turn starts on top of the unclosed one.
  2. Agent-domain events published late by a retired agent generation (an async disposer firing after deactivation, or a stale generation after a same-id re-creation) still hit the strict "active lifecycle context" check on the event bus and escape as unhandled rejections.

What changed

  • Generation-aware event bus (agent-core-v2): EventBusService tracks the latest and retired generations per agentId. Current-generation events publish normally; events from a retired generation (including post-deactivation stragglers) are dropped safely; foreign/unknown/future generations still throw, preserving the strict guard on abnormal paths.
  • interrupted terminal state: TurnEndReason / TurnEndedPayload gain 'interrupted', with interruptReason added to the wire serialization.
  • Restore normalization: EventDispatcherService.restore() detects unclosed turns from TurnPrompt/TurnEnded facts and appends turn.ended(interrupted, aborted) before the agent becomes available — without re-executing the old turn. Session outcome maps interrupted onto the existing session-failed contract.
  • Wire mirrors: klient, kap-server events-zod, and the transcript contract accept the new interrupted reason. The v1 legacy status edge maps it to failed (v1 protocol unchanged); transcript live projection and cold rebuild both fold it to the failed turn state; the ACP edge maps it to end_turn (ACP has no interrupted stop reason).
  • Tests: event-bus generation semantics, resume with a journal truncated mid-turn, turn ops / loop snapshots, agent lifecycle teardown, legacy-status mapping, transcript fold (live + cold).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: the issue must have a maintainer's /approve).
  • 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.

@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 35f6a33

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

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

commit: 35f6a33

@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: 21b15925a9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}
this.executeEvent(event, true);
if (event instanceof TurnPrompt) {
const turnId = nextTurnId++;

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 Honor cancelled turn-ID gaps when closing restored turns

When a queued turn is cancelled, TurnCancel causes the turn fold to skip that reserved ID, so a later TurnPrompt may correspond to a higher ID than the number of prompts seen. For example, after turn 0 completes, queued turn 1 is cancelled, and turn 2 starts before a crash, this local counter assigns the restored prompt to turn 1; restore then appends turn.ended for the cancelled turn and leaves the actual turn 2 open. Derive the prompt's ID from the replayed turn state, which already incorporates TurnCancel, rather than incrementing a separate counter.

Useful? React with 👍 / 👎.

@sailist
sailist force-pushed the bug-077-08-24-restore-dangling-turn-crash branch 2 times, most recently from ee45ad3 to 97f603a Compare August 25, 2026 04:21
@sailist

sailist commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 97f603a514

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

reason: 'interrupted',
interruptReason: 'aborted',
});
this.executeEvent(event, true);

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 Publish restored turn endings to existing transcript bindings

When an agent ID is recreated within an already-live session, bindSessionTranscript subscribes the new generation during onDidCreate before restore() runs, but this silent: true call suppresses the observable turn.ended event. The live transcript is backfilled only on its initial attachment, so it retains the retired generation's running turn even though the journal and activity state now mark it interrupted; subsequent transcript reads can therefore show the old turn as running alongside new turns. Emit/project this synthetic ending to already-bound consumers, or explicitly refresh that agent's transcript after restoration.

Useful? React with 👍 / 👎.

@sailist
sailist force-pushed the bug-077-08-24-restore-dangling-turn-crash branch from 97f603a to e0652ce Compare August 25, 2026 04:35
@sailist

sailist commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}) satisfies z.ZodType<McpOAuthAuthorizationUrlUpdateData>;

export const turnEndReasonSchema = z.enum(['completed', 'cancelled', 'failed', 'blocked']) satisfies z.ZodType<TurnEndReason>;
export const turnEndReasonSchema = z.enum(['completed', 'cancelled', 'failed', 'blocked', 'interrupted']) satisfies z.ZodType<TurnEndReason>;

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 Mirror interrupted in the shared v1 protocol

When a process dies mid-turn, restore now emits reason: 'interrupted' as a raw turn.ended event over /api/v1/ws, but packages/protocol/src/events.ts still restricts both TurnEndReason and turnEndReasonSchema to the four old values. Consequently, clients validating session events with the exported sessionEventMessageSchema reject exactly this recovery event instead of applying it. Update the shared protocol type/schema as well, or translate the reason before broadcasting on the v1 surface.

Useful? React with 👍 / 👎.

time?: number;
turnId: number;
reason: 'completed' | 'cancelled' | 'failed' | 'blocked';
reason: 'completed' | 'cancelled' | 'failed' | 'blocked' | 'interrupted';

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 Settle the prompt alongside an interrupted turn

When an agent ID is recreated while its session transcript store remains live, the store retains the retired generation's running prompt entity and the new projector receives this synthetic interrupted ending. The fresh code now publishes the ending, but this branch only upserts the failed turn and idle activity; neither restore nor the projector emits a terminal prompt.upsert, so consumers show an idle agent with a prompt still running indefinitely. Settle the prompt associated with the interrupted turn during restore or projection.

Useful? React with 👍 / 👎.

@sailist
sailist force-pushed the bug-077-08-24-restore-dangling-turn-crash branch from e0652ce to c1d4138 Compare August 25, 2026 04:56
@sailist

sailist commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +789 to +790
reason: 'interrupted',
interruptReason: 'aborted',

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 Add a changeset for restored-turn recovery

This restore fix changes user-visible CLI behavior by closing turns that previously remained stuck after a restart, but the commit contains no .changeset/ entry. Add a patch changeset for @moonshot-ai/kimi-code; otherwise this user-relevant recovery fix will be omitted from the generated release changelog.

AGENTS.md reference: AGENTS.md:L85-L86

Useful? React with 👍 / 👎.

Comment on lines +789 to +790
reason: 'interrupted',
interruptReason: 'aborted',

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 user-cancelled endings during restore

When the process exits after persisting an active turn.cancel with reason: 'user_cancelled' but before the normal turn.ended is written, the turn remains in openTurns and this unconditional ending closes it as interrupted/aborted. Downstream session and transcript projections map that reason to failed, so a deliberate cancellation is reported as a failure after restart even though the journal retained the cancellation reason; track active cancellation records and synthesize cancelled/user_cancelled for this case.

Useful? React with 👍 / 👎.

@sailist
sailist force-pushed the bug-077-08-24-restore-dangling-turn-crash branch from c1d4138 to ba5d0c1 Compare August 25, 2026 05:13
@sailist

sailist commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}
this.executeEvent(event, true);
if (event instanceof TurnPrompt) {
openTurns.add(turnClock.nextTurnId);

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 Retire superseded legacy turns before normalization

For legacy journals without durable turn.ended records—the existing multi-turn resume fixtures model exactly this shape—every TurnPrompt is added here and remains in openTurns even after a later turn starts. Restoring a completed two-turn session therefore appends synthetic interrupted endings for both turns, persists false failures into the journal, and changes the session's last-turn outcome; because turn execution is serial, later turn evidence must retire earlier inferred-open turns so only the genuinely trailing turn is normalized.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L22-L22

Useful? React with 👍 / 👎.

this.markerOp('interruption', { turnId: event.turnId, reason: event.interruptReason }),
);
}
if (event.reason === 'interrupted') {

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 Settle prompts for restored cancelled turns

When the journal ends after an active turn.cancel was persisted but before the normal turn.ended and prompt.aborted, restore now synthesizes a cancelled ending rather than an interrupted one. In an already-live session transcript, this condition excludes that recovery event, leaving the retired generation's running or queued prompt entities nonterminal even though the turn and agent activity are now terminal; the recovery-cancelled path must also emit appropriate terminal prompt operations.

Useful? React with 👍 / 👎.

@sailist
sailist force-pushed the bug-077-08-24-restore-dangling-turn-crash branch from ba5d0c1 to 8654a10 Compare August 25, 2026 05:26
@sailist

sailist commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 8654a1093d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +445 to +446
for (const [promptId, prev] of this.prompts) {
if (prev.status !== 'running' && prev.status !== 'queued') continue;

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 Leave queued prompts pending when cancelling the active turn

When an active prompt is cancelled while another prompt remains queued, AgentPromptService.settle() starts that queued prompt next, but this loop marks both the running prompt and every queued prompt as aborted. Starting the queued prompt emits no event that restores its transcript status to running, so clients display a prompt that is actively executing as aborted until another terminal event arrives. Only the active prompt should be settled for an ordinary cancelled turn; mass-settling queued prompts should be limited to recovery paths where the queue was actually lost.

Useful? React with 👍 / 👎.

@sailist
sailist force-pushed the bug-077-08-24-restore-dangling-turn-crash branch from 8654a10 to 2c911eb Compare August 25, 2026 05:42
@sailist

sailist commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 2c911ebae8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +51 to +53
const latest = this.latestGeneration.get(agent.agentId) ?? 0;
const retired = this.retiredGeneration.get(agent.agentId) ?? 0;
if (agent.generation < latest || agent.generation <= retired) return;

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 Reject unseen generation-zero contexts

When an out-of-band host creates a context with makeAgentScopeContext and omits generation, its generation defaults to 0; if that context was never activated, both missing map entries also become 0, so this condition treats the unknown context as retired and silently drops every agent event. Previously this abnormal lifecycle path threw, and the stated strict guard should still reject unknown contexts; distinguish absent generation records from a recorded retired generation instead of using 0 as both the sentinel and a valid generation.

Useful? React with 👍 / 👎.

@sailist
sailist force-pushed the bug-077-08-24-restore-dangling-turn-crash branch from 2c911eb to dfad585 Compare August 25, 2026 05:48
@sailist
sailist force-pushed the bug-077-08-24-restore-dangling-turn-crash branch from dfad585 to 35f6a33 Compare August 25, 2026 05:55
@sailist

sailist commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 35f6a33d9a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

agentState.contributeState(interruptionReminderKey);
this._register(
eventBus.subscribe(TurnEnded, (event) => {
if (dispatcher.restorePhase === 'restoring') return;

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 reminders for restored user cancellations

If the process crashes after persisting an active turn.cancel with reason: 'user_cancelled' but before the normal turn.ended handler appends its interruption reminder, restore() now synthesizes the cancelled ending while restorePhase is still restoring, so this guard discards the only notification that would add the reminder. The next user prompt can therefore send the model partial output from the cancelled turn without warning that it is incomplete; suppress replayed historical endings without suppressing the newly synthesized recovery ending, or append its reminder after restoration.

Useful? React with 👍 / 👎.

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