Skip to content

fix(web): stop sending prompts into a busy turn on the web UI#1522

Merged
wbxl2000 merged 10 commits into
mainfrom
fix/web-first-prompt-busy
Jul 9, 2026
Merged

fix(web): stop sending prompts into a busy turn on the web UI#1522
wbxl2000 merged 10 commits into
mainfrom
fix/web-first-prompt-busy

Conversation

@wbxl2000

@wbxl2000 wbxl2000 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — the problem is explained below.

Problem

On the web UI, users occasionally hit Cannot launch a new turn while another turn (ID 0) is active (turn.agent_busy). Two web-side paths could deliver a prompt to the core while its activeTurn was still set:

  1. Double-submit of the first message. Sending the first message from the empty-session composer awaits draft-session creation (addWorkspacecreateSessionselectSession) before the session id exists, so the per-session in-flight guard could not cover that window. A double-click / repeated Enter fired two concurrent first POSTs into the same new session; the daemon accepted both and the core rejected the second.

  2. Goal drives look idle between continuation turns. The core intentionally keeps activeTurn set across goal-driven continuation turns, but the web event projector mapped every turn.ended to idle. onSessionIdle then cleared the spinner and drained the local queue into the still-busy core — matching the reported repro ("the reply finished, the moon spinner was about to clear, I sent a message, and got the error").

Additionally: sending the first message appeared to "hang" with no feedback while the session was being created; and activating a skill as the first action of a new session failed with Model not set because the resolved model was never persisted to the session profile before the skill turn started.

What changed

All changes are in apps/kimi-web:

  • First-prompt reentry lock (useWorkspaceState.ts): startSessionAndSendPrompt now takes a per-workspace lock covering draft-session creation through prompt submission, making the duplicate-submit window unhittable.
  • Goal-aware status projection (agentEventProjector.ts): the projector tracks goal.updated and projects turn.ended as running (instead of idle) while a goal is still active, so the queue only drains when the core is truly idle. Added unit tests for the active/completed/cancelled cases.
  • "Starting conversation…" loading state (ConversationPane.vue, Composer.vue, i18n): the empty-session landing shows an immediate starting state (spinner title, disabled textarea, spinner send button) while the first prompt is in flight, so users no longer double-click out of "did it register?" uncertainty.
  • Persist model before skill activation (useWorkspaceState.ts): startSessionAndActivateSkill now includes the resolved model in the session profile patch, fixing the first skill turn failing with Model not set.

The daemon-side PromptService check-then-set race remains as hardening for non-web clients and can be addressed separately; web no longer triggers it.

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. (Bug fixes and small UX state changes only; no new docs needed.)

wbxl2000 added 2 commits July 9, 2026 21:42
…ooking idle

- Guard startSessionAndSendPrompt with a per-workspace reentry lock so a
  double-click / repeated Enter during draft-session creation cannot fire
  two concurrent first prompts into the same new session.
- Track goal.active in the agent event projector so turn.ended between
  goal-driven continuation turns keeps the session 'running' instead of
  projecting a false 'idle' that drains the local queue into a still-busy
  core (turn.agent_busy).
- Show a 'starting conversation…' loading state on the empty-session
  landing while the first prompt is being created and submitted.
- Persist the resolved model in startSessionAndActivateSkill so the first
  skill turn on a fresh session does not fail with 'Model not set'.
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 37d33cf

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

@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: 80e11842c8

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

:models="models"
:starred-ids="starredIds"
:skills="skills"
:starting="starting"

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 Pass the starting guard through the dock composer

This only disables the centered empty Composer. During startSessionAndSendPrompt, createDraftSession calls selectSession, which sets activeSessionId while it is still awaiting the snapshot, before the original prompt is submitted; at that point this pane switches to ChatDock, whose Composer is not given starting. On a slow snapshot, a user can type/submit from the dock, sendPrompt sees no inFlightPromptSessions yet, and then the original submitPromptInternal runs too, recreating the concurrent first-turn POST this change is meant to prevent.

Useful? React with 👍 / 👎.

Comment on lines +994 to +996
: s.goalActive
? 'running'
: '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 Emit idle when a goal blocks after turn end

In goal mode, driveGoal can emit turn.ended while the goal is still active and only then call markBlocked (for example a UserPromptSubmit hook block at packages/agent-core/src/agent/turn/index.ts:449-451, or the post-turn budget check at :464-466). This branch projects that completed turn.ended as running; the later goal.updated only clears goalActive/goal UI and never emits an idle status, so onSessionIdle is not called and sendingBySession, inFlightPromptSessions, and queued prompts remain stuck for that session.

Useful? React with 👍 / 👎.

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

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

commit: 37d33cf

- Pass the starting guard through the dock composer: draft-session
  creation selects the new session before submit, which swaps the empty
  composer for the dock; disabling both composers closes the last path
  to a concurrent first POST. Also take the workspace lock in
  startSessionAndActivateSkill / startSessionAndOpenSideChat.
- Emit the owed idle when a goal settles (blocked/paused/completed) in
  the inter-turn gap after a turn.ended was projected as 'running', so
  sending state, in-flight flags and queued prompts flush instead of
  the session staying 'running' forever.
@wbxl2000

wbxl2000 commented Jul 9, 2026

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: a22d1faad3

ℹ️ 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 +1254 to +1261
if (wasActive && !s.goalActive && s.idleOwed) {
s.idleOwed = false;
out.push({
type: 'sessionStatusChanged',
sessionId,
status: 'idle',
previousStatus: 'running',
});

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 idle debt when a new goal turn starts

When a goal has already crossed at least one active turn boundary, turn.ended leaves s.idleOwed = true. Because that flag is not cleared on the next turn.started, a later goal.updated emitted by UpdateGoal('complete'|'blocked') during that next turn satisfies this branch and projects idle before the following turn.ended; onSessionIdle can then drain queued prompts while driveGoal still has activeTurn set, reproducing turn.agent_busy for multi-turn goals. Clear the debt when a new turn starts or otherwise gate this branch on actually being between turns.

Useful? React with 👍 / 👎.

The idle debt from a 'running' projection survived turn.started, so an
UpdateGoal('complete'|'blocked') landing mid-turn in the NEXT goal turn
synthesized an early idle. onSessionIdle could then drain queued prompts
into a core that was mid-turn again, re-opening the turn.agent_busy race
for multi-turn goals. Clear the debt on turn.started: from that point the
turn's own turn.ended carries the idle with goalActive already false.
@wbxl2000

wbxl2000 commented Jul 9, 2026

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: 3979f42471

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

// turn.ended knows whether the core is merely between continuation
// turns (goal still driving) or truly idle.
const wasActive = s.goalActive;
s.goalActive = goal?.status === 'active';

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 Preserve goal-active state across snapshots

When a running goal session is rebuilt from a snapshot (reopen/select, resync, or stale-WS recovery), seedInFlight() resets the projector state and then subscribes at snapshot.asOfSeq, so the earlier goal.updated(active) frame is not replayed. Because this live goal.updated path is the only place that restores goalActive, the next turn.ended after a snapshot is projected as idle even though the core is still between goal continuation turns, allowing onSessionIdle to drain queued prompts back into turn.agent_busy. Seed this flag from the snapshot/current goal state, or otherwise replay/include the active goal state before live turn events resume.

Useful? React with 👍 / 👎.

Comment on lines +1691 to +1693
const isStartingFirstPrompt = computed<boolean>(() =>
workspaceState.isStartingFirstPrompt(rawState.activeWorkspaceId),
);

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 the first-prompt lock after workspace id changes

When createDraftSession() starts from a derived workspace, it adds the lock under the original workspace id, then addWorkspace()/selectWorkspace(session.workspaceId ...) can replace rawState.activeWorkspaceId with the registered id before submitPromptInternal() starts. This computed then returns false while the first prompt is still in the create/select window, so the composer can re-enable and a second send uses the new workspace/session id, bypassing startingFirstPromptWorkspaces and reopening the duplicate first-submit race. Lock the registered id as well or compute this state from the in-flight lock independent of the current raw workspace id.

Useful? React with 👍 / 👎.

isStartingFirstPrompt now reads from the lock set directly (size > 0)
instead of the current activeWorkspaceId. createDraftSession can swap
activeWorkspaceId to a registered id mid-flight; a workspace-keyed read
would then return false while the first prompt is still in the create/
select/submit window, re-enabling the composer and reopening the
duplicate first-submit race.
@wbxl2000

wbxl2000 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

@codex please re-review the latest state. The earlier review threads should be addressed:

  • Dock composer now receives the starting guard, and startSessionAndActivateSkill / startSessionAndOpenSideChat take the same workspace lock.
  • Goal-settle idle debt is paid on goal.updated when the goal leaves active, and cleared on turn.started so a mid-turn UpdateGoal cannot synthesize an early idle.
  • isStartingFirstPrompt is now workspace-id-agnostic (reads from the lock set directly) so an activeWorkspaceId swap mid-flight cannot re-enable the composer.

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

ℹ️ 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 +1012 to +1014
: s.goalActive
? 'running'
: '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 Seed goal state for snapshot-seeded turns

When the projector is reset from a session snapshot while an active goal turn is already in flight (seedSnapshotseedInFlight calls reset, and the snapshot payload only carries in_flight_turn, not the current goal), s.goalActive remains false because the earlier goal.updated(active) is before the subscription cursor. The first live turn.ended for that goal then falls through to idle here, so onSessionIdle can drain queued prompts into the still-running goal driver and reintroduce turn.agent_busy after a reload/reopen mid-goal; seed this state from the snapshot or current goal before using it to mark the session idle.

Useful? React with 👍 / 👎.

The goalActive / idleOwed shadow state machine grew through multiple
review rounds and still leaves edge cases (snapshot-seeded turns, mid-
turn goal updates). Roll it back to the simple 'turn.ended projects idle'
behavior. Goal-driven sessions can once again race a queued prompt into a
busy core; this is accepted as a known limitation to be resolved properly
in a follow-up that has the core emit an authoritative idle signal.
@wbxl2000

wbxl2000 commented Jul 9, 2026

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: acf74ccbca

ℹ️ 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 thread .changeset/web-first-prompt-fixes.md Outdated
"@moonshot-ai/kimi-code": patch
---

web: Fix an occasional "another turn is active" error when sending the first message of a conversation or while a goal is driving follow-up turns.

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 the missing goal-aware busy-state guard

This release note still is not true for the goal-continuation case: I checked apps/kimi-web/src/api/daemon/agentEventProjector.ts and turn.ended still projects every nonfailed turn to idle, and apps/kimi-web/src/composables/useKimiWebClient.ts still drains the queued prompt immediately from onSessionIdle. When a goal keeps the core activeTurn alive across follow-up turns, a prompt queued or typed after that turn.ended can still be POSTed into the busy turn and hit turn.agent_busy; please include the goal-aware status/queue guard before shipping this fix.

Useful? React with 👍 / 👎.

@wbxl2000

wbxl2000 commented Jul 9, 2026

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: f366a41ac5

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

: rawState.defaultModel) ?? undefined;
await persistSessionProfile(
{
model,

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 Fix the failing profile-patch test expectation

When running the existing useWorkspaceState — startSessionAndActivateSkill test, the mocked upsertSessionFront does not add sess_new and defaultModel is null, so this newly added field is passed as { model: undefined, ... } to persistSessionProfile; the assertion at apps/kimi-web/test/workspace-state.test.ts:640 still expects an object without model, which will fail the web test suite. Update the test expectation to include the resolved/undefined model field or adjust the setup to reflect the new behavior.

Useful? React with 👍 / 👎.

@wbxl2000

wbxl2000 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 37d33cf696

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

@wbxl2000 wbxl2000 merged commit ec8dc34 into main Jul 9, 2026
10 checks passed
@wbxl2000 wbxl2000 deleted the fix/web-first-prompt-busy branch July 9, 2026 15:37
@github-actions github-actions Bot mentioned this pull request Jul 9, 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