Skip to content

fix(server): synthesize session status only from the main agent's turn - #1708

Merged
wbxl2000 merged 3 commits into
mainfrom
fix/subagent-turn-status-notifications
Jul 14, 2026
Merged

fix(server): synthesize session status only from the main agent's turn#1708
wbxl2000 merged 3 commits into
mainfrom
fix/subagent-turn-status-notifications

Conversation

@wbxl2000

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — reported by a user via feedback; the problem is explained below.

Problem

On kimi-web (and any other client of the v1 WS stream), a sub-agent completing its task pushed a "turn finished" browser notification, played the completion sound, and lit the unread dot — while the main turn was still running.

Root cause: in the v2 engine a sub-agent is an agent within the same session and runs its own turn, publishing turn.started/turn.ended on its own event bus. SessionEventBroadcaster.onAgentEvent synthesized event.session.status_changed from every agent's turn boundaries without checking the agent id. Since event.session.* events fan out globally, a foreground sub-agent finishing mid-turn broadcast a bogus status_changed(idle); clients treat that as the authoritative turn-end signal (onSessionIdle in kimi-web: notification, sound, unread marker, queued-message drain). The real main-agent turn end was then swallowed by status dedup and never notified.

What changed

  • Gate the turn.started/turn.endedevent.session.status_changed synthesis on MAIN_AGENT_ID, matching the existing main-only rule in InFlightTurnTracker ("Only main-agent activity is tracked"). Sub-agent turn frames themselves are still broadcast unchanged, so the swarm/task view keeps working.
  • Add a regression test: a sub-agent completing mid main-turn emits no status transition, and the main agent's turn end still emits exactly one idle. Update two existing tests that encoded the old behavior.
  • Changeset: @moonshot-ai/kimi-code patch (kap-server source is bundled into the CLI).

Server-side fix at the single fan-out point, so every client (kimi-web, TUI, SDK) is corrected without consumer changes.

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.

A sub-agent runs its own turn on the shared session channel, and the
broadcaster synthesized event.session.status_changed from every agent's
turn.started/turn.ended. A foreground sub-agent finishing mid-turn thus
emitted a bogus idle transition that clients read as 'turn finished'
(notifications, sounds, unread dots, queued-message drain), while the
real main-agent turn end was swallowed by dedup.

Gate the status synthesis on MAIN_AGENT_ID, matching the existing
main-only rule in InFlightTurnTracker.
@changeset-bot

changeset-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f9ff00

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/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 14, 2026

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

commit: 3f9ff00

@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: 55099ddd89

ℹ️ 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".

.then(() => this.dispatch(state, wireEvent, volatile))
.catch(() => {});
if (event.type === 'turn.ended') {
if (agentId === MAIN_AGENT_ID && event.type === 'turn.ended') {

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 idle transition after subagent-only runs

If a client first subscribes or resyncs while only a background/sub-agent turn is active, ensureState still seeds lastStatus from ISessionActivity.status(), which reports running for any active agent. With this new main-agent guard, that sub-agent's later turn.ended is journaled but no status_changed(idle) is enqueued, so v1 clients that received a running snapshot can stay busy and keep subsequent prompts queued until some later main-agent status transition. Please make the initial/snapshot status use the same main-agent-only rule, or still clear a subagent-only running state without firing the premature main-turn completion path.

Useful? React with 👍 / 👎.

…agent

Defensive follow-up to the main-only status synthesis: ensureState seeds
lastStatus from ISessionActivity, which counts any agent's turn, so a
session first activated while only a sub-agent was active would stay
running for subscribers until the next main turn. A sub-agent's own
lease and loop state are already released when its turn.ended is
published, so a session-wide idle activity read cannot be a
mid-main-turn foreground sub-agent — emit idle in that case only.
Dedup keeps it a no-op everywhere else.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

Addressed the P2 review on sessionEventBroadcaster.ts (stale running seed after sub-agent-only runs) in 7918b45.

The mechanism in the review checks out (ISessionActivity.status() counts any agent's turn, and ensureState seeds lastStatus from it), though the precondition is effectively unreachable in production:

  • In-process, the broadcaster activates a session at event.session.created / first session.meta.updated at the latest and keeps it for the process lifetime. A background sub-agent is always launched from a main turn, so the session was already activated and lastStatus went idle at the main turn's end — a later sub-agent turn.ended was deduped even before this PR.
  • Across restarts it cannot occur either: persisted detached tasks are reconciled as lost (markLoadedTasksLost), never auto-resumed, so a fresh process never has a sub-agent-only running session.

The follow-up commit takes the review's second suggestion: a sub-agent's turn.ended now emits idle when session-wide activity is already idle. This is safe because the sub-agent's own lease and loop state are released before turn.ended is published (loopService releases the active turn and ends the lease in the finally block before publishing), so an idle read cannot coincide with an in-flight main turn — the premature-notification path stays closed, and dedup makes the emit a no-op in every normally-tracked case. A new test covers the stale-seed path.

On the first suggestion (main-agent-only snapshot/REST status): REST intentionally reports running while detached tasks live, corrected on the wire at main-turn boundaries — that pre-existing divergence is orthogonal to this PR and would be a product-level change.

@wbxl2000

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: 7918b45fea

ℹ️ 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".

const terminalStatus =
reason === 'cancelled' || reason === 'failed' || reason === 'blocked' ? 'aborted' : 'idle';
this.enqueueStatusChanged(state, pendingStatus(state.activity?.status()) ?? terminalStatus);
} else if (event.type === 'turn.ended' && state.activity?.status() === 'idle') {

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 aborted status for failed sub-agent endings

When a session is first activated while only a sub-agent is running (the stale running seed case this branch handles), a sub-agent that ends with reason: 'cancelled' or reason: 'failed' now always emits status_changed(idle). That regresses the previous terminal-status mapping for those turn endings: kimi-web treats idle as a successful completion and will mark unread / notify / play the completion sound, while it suppresses those side effects for aborted (apps/kimi-web/src/composables/useKimiWebClient.ts:2528-2556, apps/kimi-web/src/composables/client/useNotification.ts:19-25). Use the turn-end reason here as well, or avoid emitting an idle completion status for non-main failed/cancelled turns.

Useful? React with 👍 / 👎.

This reverts 7918b45. The stale-seed scenario it guarded is
effectively unreachable in production: sessions are activated at
creation / first prompt and stay active for the process lifetime, and
persisted detached tasks are reconciled as lost on restart, never
resumed. The guard also raised a new behavioral question of its own
(failed/cancelled sub-agent endings would emit a success-style idle).
Keep the PR to the minimal main-only status synthesis; the theoretical
corner self-corrects on the next REST status pull or main turn.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

Follow-up on both P2 reviews: commit 3f9ff00 reverts the defensive idle emit (7918b45), so the PR is back to the minimal main-only status synthesis.

Reasoning:

  • The stale-seed precondition (first review) is effectively unreachable in production: sessions are activated at event.session.created / first session.meta.updated and stay active for the process lifetime, so lastStatus already tracks the main turn to idle before any sub-agent can exist; and persisted detached tasks are reconciled as lost on restart, never resumed, so a fresh process never has a sub-agent-only running session.
  • The second review correctly noted the guard emitted a success-style idle for failed/cancelled sub-agent endings in that corner. Mirroring the terminal-status mapping there would fix that corner but reintroduce a different pre-existing wart in the reachable case: a failed/cancelled background sub-agent would flip an idle session to aborted (dedup cannot stop it, since lastStatus is normally idle). Trading a reachable wart for an unreachable one is not worth it.
  • In the theoretical corner, a subscriber now simply stays running until the next main turn or REST status pull (the session list status is REST-derived) — no incorrect notification is fired. Pre-PR, that corner only "self-healed" via the same bogus status event that caused the reported notification bug, so no correct behavior is lost.

Net PR change: main-agent-only turn.started/turn.endedevent.session.status_changed synthesis (matching the existing main-only rule in InFlightTurnTracker), plus regression tests and changeset.

@wbxl2000
wbxl2000 merged commit ddfdfb0 into main Jul 14, 2026
14 checks passed
@wbxl2000
wbxl2000 deleted the fix/subagent-turn-status-notifications branch July 14, 2026 10:35
@github-actions github-actions Bot mentioned this pull request Jul 14, 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