Skip to content

fix(kimi-web): stop the prompt queue from ghost-sending stale attachments - #1833

Merged
wbxl2000 merged 6 commits into
mainfrom
fix/web-prompt-queue-stale-attachments
Jul 17, 2026
Merged

fix(kimi-web): stop the prompt queue from ghost-sending stale attachments#1833
wbxl2000 merged 6 commits into
mainfrom
fix/web-prompt-queue-stale-attachments

Conversation

@wbxl2000

@wbxl2000 wbxl2000 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — this comes from a user feedback report (local deployment on macOS); the problem is described below.

Problem

In the web UI, messages sent while a turn is running are held in a local browser queue (queuedBySession) and flushed when the turn ends. When a flush failed — typically right after the user aborted a turn, while the daemon was still busy — the entry went back to the queue head with nothing left to drive another attempt, so it stayed stuck together with its recorded file ids.

Every later session open then ran the queue drain unconditionally (handleSessionSnapshotfinishPromptLocal), silently re-submitting the stuck entries. Users saw exactly that: the third file they sent arrived together with the first two, and simply reopening a session submitted a pile of previously uploaded files out of nowhere — the agent then re-read every old file and burned tokens on them.

What changed

  • Gate the queue drain on a locally witnessed prompt/turn (finishPromptLocal): merely re-opening a session can no longer submit queued prompts. This is the direct fix for the ghost-attachment resend.
  • A failed flush goes back to the head and is re-driven FIFO by real events — the next witnessed turn end, or the next send while the session is idle (which enqueues behind the stuck entries and flushes the head first). After 3 consecutive failures the entry is dropped, so one permanently rejected prompt cannot block the queue; the budget is tracked per entry, so removing or reordering the head resets it for the next one.
  • The flush callback no longer resurrects a queue whose session was forgotten while the submit was pending.
  • steerPrompt restores the merged queue entries when its submit fails instead of silently losing them.

Note: an earlier iteration of this PR also persisted the queue across page refreshes with cross-tab merge/ownership machinery. Review rightly flagged that as a distributed-systems problem beyond this fix's scope, so it was removed again — durable queued prompts will be designed together with the server-side prompt queue instead.

Tests: regression cases cover the reopen-no-drain gate, witnessed-turn drain, FIFO flush, failure re-queue + per-entry drop budget, forgotten-session guard, and steer restore (running/idle paths). Full kimi-web suite (632 tests), vue-tsc, and oxlint are green.

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.

…ents

Sending while a turn was running queues prompts locally. A failed flush
left entries stuck, and every later session open silently re-submitted
them with their old file attachments. Gate the drain on locally
witnessed turns, re-drive stuck entries FIFO from real events with a
failure budget, restore merged entries on steer failure, persist the
queue per session so a refresh loses nothing, and converge queues
across tabs via storage events.
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9c56b1d

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 17, 2026

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

commit: 9c56b1d

@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: 42ca9151e6

ℹ️ 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 +1840 to +1841
rawState.queuedBySession = { ...rawState.queuedBySession, [sid]: rest };
void submitPromptInternal(sid, next.text, next.attachments).then((ok) => {

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 Elect one tab before flushing the shared queue

When two tabs hold the same persisted queue and receive the same turn-ended event before either storage event is handled, both execute this removal and immediately start submitPromptInternal; the Vue watcher and cross-tab storage notification run too late to prevent the second request. The daemon can therefore accept the same prompt and attachments twice, or the rejected tab can restore them for a later retry, recreating the ghost-send problem. Claim the entry atomically or establish a single flushing tab before submitting it.

Useful? React with 👍 / 👎.

Comment on lines +388 to +389
function syncQueuedPromptsFromStorage(): void {
rawState.queuedBySession = loadQueuedPrompts();

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 Merge concurrent cross-tab queue updates instead of replacing them

If two tabs enqueue different prompts close together, each persists a whole-record snapshot, and this unconditional replacement adopts only whichever snapshot is currently in localStorage; the other tab's unique queued prompt is silently discarded. Storage events do not serialize read-modify-write operations, so queue entries need stable identities and merge/conflict handling rather than last-writer-wins replacement.

Useful? React with 👍 / 👎.

Comment on lines +1846 to +1849
const failures = (queueFlushFailures.get(sid) ?? 0) + 1;
if (failures >= MAX_QUEUE_FLUSH_FAILURES) {
queueFlushFailures.delete(sid);
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 Reset the failure budget when the queue head changes

The failure count is keyed only by session, not by the entry being retried. If one head fails twice and the user then removes or reorders it through unqueue/reorderQueue, the next entry is dropped on its first failure because it inherits the count of two. Track failures per entry or clear the session count whenever user queue operations replace the head.

Useful? React with 👍 / 👎.

Whole-record adoption could silently discard a prompt another tab
enqueued concurrently. Queue entries now carry a stable id and enqueue
timestamp; adoption union-merges snapshots by id, a shared TTL'd removal
set stops flushed/discarded entries from resurrecting (and being
flushed twice), an in-flight marker covers the submit window, and
manual reorders re-stamp timestamps so they survive merges. The flush
failure budget is also tracked per entry instead of per session, so
removing or reordering the head no longer hands its strikes to the
next entry.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

Addressed both review comments in 932e228:

  • P1 (merge instead of replace): Queue entries now carry a stable id + enqueue timestamp. Cross-tab adoption union-merges snapshots by entry id instead of last-writer-wins replacement, so two tabs enqueueing concurrently no longer discard each other's prompts. Removals propagate through a shared TTL'd removal set (flushed/discarded entries can't resurrect and flush twice), an in-flight marker covers the window between pop and submit ack, and manual reorders re-stamp timestamps so they survive merges.
  • P2 (failure budget per entry): The 3-strike flush budget is now keyed by entry id (falling back to text for id-less entries), so removing or reordering the head resets the budget for the next entry instead of inheriting stale strikes.

Regression coverage for all of the above is in test/workspace-state.test.ts (union merge, no-resurrect, in-flight window, budget reset, reorder stability). Full kimi-web suite (641 tests) + typecheck + lint green.

@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: 932e228f7b

ℹ️ 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 +2050 to +2055
if (next.id !== undefined) {
const submitting = queueInFlightSubmits.get(sid) ?? new Set<string>();
submitting.add(next.id);
queueInFlightSubmits.set(sid, submitting);
}
void submitPromptInternal(sid, next.text, next.attachments).then((ok) => {

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 Claim queue entries across tabs before submitting

When two tabs have adopted the same queued entry and both observe the same main-turn end, each independently reaches this block and submits the entry because queueInFlightSubmits is only local to each tab. The server accepts concurrent submissions as distinct pending prompts (PromptService.enqueue in packages/agent-core-v2/src/agent/prompt/promptService.ts:84-108), so the tombstone written after either request resolves is too late to prevent duplicate messages, attachments, agent work, and token usage.

Useful? React with 👍 / 👎.

Comment on lines +217 to +220
for (const entry of [...(a[sid] ?? []), ...(b[sid] ?? [])]) {
if (entry.id !== undefined) {
if (seenIds.has(entry.id) || gone[entry.id] !== undefined || submitting?.has(entry.id)) {
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 Prefer newer reorder metadata during cross-tab merges

When both tabs already contain the same IDs and one tab reorders the queue, this loop always processes the local entries first and discards the storage entries carrying the newer enqueuedAt values. The first tab consequently writes its old order back, while the tab that performed the reorder likewise retains and rewrites its own order; their storage handlers can therefore ping-pong writes indefinitely instead of converging, causing continuous browser work and an unstable queue order.

Useful? React with 👍 / 👎.

Comment on lines +2077 to +2081
const current = rawState.queuedBySession[sid] ?? [];
rawState.queuedBySession = {
...rawState.queuedBySession,
[sid]: [next, ...current],
};

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 Avoid restoring a failed flush after its session is forgotten

If the user archives a session while this queued submission is pending and the request subsequently rejects—for example because of a concurrent network failure—forgetSession has already discarded the session queue, but this callback unconditionally recreates it. The persistence watch then stores the stale prompt for the archived session, allowing it to reappear after restoration or cross-tab synchronization; guard the callback against forgotten sessions or tombstone/cancel the popped in-flight entry during teardown.

Useful? React with 👍 / 👎.

… forget guard (review round 2)

Turn-end events reach every open tab and the server accepts concurrent
submissions as distinct prompts, so two tabs holding the same adopted
entry could both submit it. Entries now record their owner tab and only
the owner flushes (ownerless legacy entries flush anywhere); an idle
send adopts entries left behind by closed tabs so a stranded queue can
still drain. Cross-tab merges now keep the newer enqueuedAt copy per id
so a manual reorder converges instead of ping-ponging writes, and the
flush failure callback no longer resurrects a queue whose session was
forgotten while the submit was pending.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

Round 2 addressed in 56889eb:

  • P1 (claim before submit): Fixed by making flush rights exclusive instead of transactional. Every queued entry now records its owner tab (per-page-load id, not persisted); only the owner tab flushes, so two tabs witnessing the same turn end no longer double-submit a shared entry. Entries left behind by a closed tab are adopted on the next user-initiated idle send (never on passive sync, so live tabs can't steal from each other).
  • P2 (reorder ping-pong): Merges now keep the copy with the newer enqueuedAt per id instead of always preferring the local one. A reordering tab's re-stamped order is the deterministic winner on every tab, writes settle, and the skip-if-equal guard stops the loop — verified by a convergence test.
  • P2 (forgotten session): The session-forget path now tombstones in-flight submits as well, and the flush failure callback checks the session still exists before re-queueing — archiving mid-submit no longer resurrects the entry or re-persists it for the archived session.

4 new regression tests (owner gate, idle adopt, merge convergence, forget guard). Suite: 645 tests passing, typecheck + lint clean.

@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

const queue = rawState.queuedBySession[sid] ?? [];
const parts: string[] = [];
const mergedAttachments: PromptAttachment[] = [];
for (const q of queue) {
const trimmed = q.text.trim();
if (trimmed) parts.push(trimmed);
if (q.attachments?.length) mergedAttachments.push(...q.attachments);

P2 Badge Respect queue ownership when steering

In a multi-tab session, this queue can contain entries owned by another tab after storage synchronization, but steerPrompt captures, clears, and submits every entry without checking or claiming ownerTabId. If the owner tab also steers or processes the turn end before the resulting tombstone storage event arrives, both tabs submit the same queued text and attachments. Steering should exclude foreign entries or acquire the same coordinated ownership required by normal queue flushing.

ℹ️ 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 +1895 to +1899
[sid]: (rawState.queuedBySession[sid] ?? []).map((entry) =>
entry.ownerTabId !== undefined && entry.ownerTabId !== TAB_ID
? { ...entry, ownerTabId: TAB_ID }
: entry,
),

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 Claim foreign entries before flushing them

When two tabs share a session, tab A can already be flushing its owned head after a turn ends while tab B sees the session as idle and sends a new message. This loop unconditionally transfers A's entries to B and the following flushQueueHead submits the same head before A's request has produced a tombstone; the localStorage write offers no atomic claim or acknowledgement to prevent that race. This can duplicate the queued prompt and its attachments, so foreign entries should only be adopted through a coordinated claim or after establishing that their owner is gone.

Useful? React with 👍 / 👎.

Comment on lines +190 to +194
const cutoff = Date.now() - QUEUE_GONE_TTL_MS;
const out: Record<string, number> = {};
for (const [id, at] of Object.entries(gone)) {
if (at >= cutoff) out[id] = at;
}

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 Retain tombstones while stale tabs can still rejoin

If a tab retaining a queued entry is frozen for more than 30 minutes while another tab flushes or discards that entry, the delayed storage synchronization will prune its tombstone here before performing the union merge. The frozen tab's stale in-memory copy is then resurrected and persisted again, and its original owner can later submit it a second time. Because there is no acknowledgement proving every tab has observed a removal, expiring tombstones solely by elapsed time reintroduces the stale-attachment ghost-send this change is intended to prevent.

Useful? React with 👍 / 👎.

wbxl2000 added 2 commits July 17, 2026 18:26
…nimal fix

Cross-tab queue sync over localStorage is a distributed-systems problem
(claim/lease, conflict merge, ownership) that keeps generating review
findings far beyond this PR's scope. Remove the persistence/hydration/
adoption machinery wholesale; keep the bug fix proper: gated queue
drain, event-driven FIFO retry with a per-entry failure budget, steer
queue restore, and the forgotten-session flush guard. Durable queued
prompts will be designed together with the server-side prompt queue.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

On the steer-ownership P2 and the cross-tab findings overall: the queue no longer crosses tabs at all. After two rounds of increasingly exotic localStorage multi-tab races, we've cut the persistence/adoption machinery from this PR (9781b50) — durable queued prompts will be designed together with the server-side prompt queue instead of on top of localStorage.

What remains is the minimal bug fix itself: gated queue drain, event-driven FIFO retry with a per-entry failure budget (the earlier budget P2 stays fixed), steer queue restore, and the forgotten-session flush guard (the earlier forget P2 stays fixed). The steer-ownership scenario no longer exists because foreign-owned entries no longer exist.

Final diff is 4 files, ~250 lines net (mostly tests and comments). Suite: 632 tests passing, typecheck + lint clean. This should be mergeable now.

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

ℹ️ 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 +1630 to +1633
const restoreQueue = (): void => {
if (queue.length === 0) return;
const current = rawState.queuedBySession[sid] ?? [];
rawState.queuedBySession = { ...rawState.queuedBySession, [sid]: [...queue, ...current] };

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 Avoid restoring prompts after ambiguous submit failures

When a steer POST is accepted by the daemon but its response is lost or cannot be parsed, submitPrompt rejects even though the merged prompt is already queued server-side. Unconditionally prepending the captured entries here causes those prompts and attachments to be submitted again during a later drain, recreating the duplicate/ghost-attachment behavior this change is intended to prevent. Restore only after a definitive server rejection, or reconcile ambiguous network failures using the prompt event/request identity.

Useful? React with 👍 / 👎.

Comment on lines +1785 to +1787
if (count >= MAX_QUEUE_FLUSH_FAILURES) {
queueFlushFailures.delete(sid);
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 Continue draining after dropping the failed queue head

When the third attempt drops the head while later entries are queued, this returns without submitting the new head. Because the failed submission produced no turn, there is no subsequent turn-end event to drive another flush, so prompts behind it—including the new prompt whose send action triggered this attempt—remain stuck until the user sends yet another message. Advance the queue when dropping the exhausted entry, or otherwise schedule a driver for the remaining head.

Useful? React with 👍 / 👎.

…after drop

Two review findings: (1) restoring merged queue entries after ANY steer
failure could re-submit prompts the daemon had already accepted when the
failure was a lost response — submits now report ok/rejected/uncertain
and restores + flush re-queues happen only on definitive daemon
rejections, while ambiguous failures drop the entry (the failure toast
still tells the user); (2) dropping an exhausted queue head no longer
strands the entries behind it — the new head is submitted immediately,
carrying its own failure budget.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

Round 3 addressed in 9c56b1d, with one principle applied to both: never re-submit anything the daemon might already hold.

  • Ambiguous steer/flush failures: submitPromptInternal now reports three states — ok / rejected (structured daemon error = server holds nothing, safe to restore/re-queue) / uncertain (lost response or network error = the prompt may already be queued server-side). Steer restores merged entries only on rejected; a flush on uncertain drops the entry instead of risking a duplicate. Every failure still surfaces a toast, so a dropped message is never silent.
  • Drop strands the queue: after an exhausted head is dropped, the next queued entry is submitted immediately with its own failure budget — no more waiting for the user's next send.

New regression tests cover drop-then-advance, ambiguous-flush drop, definitive-vs-ambiguous steer restore (running and idle paths). Suite: 635 tests, typecheck + lint green.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 9c56b1d42f

ℹ️ 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 03021b6 into main Jul 17, 2026
14 checks passed
@wbxl2000
wbxl2000 deleted the fix/web-prompt-queue-stale-attachments branch July 17, 2026 10:57
@github-actions github-actions Bot mentioned this pull request Jul 17, 2026
7723qqq pushed a commit to 7723qqq/kimi-code that referenced this pull request Jul 17, 2026
…ents (MoonshotAI#1833)

* fix(kimi-web): stop the prompt queue from ghost-sending stale attachments

Sending while a turn was running queues prompts locally. A failed flush
left entries stuck, and every later session open silently re-submitted
them with their old file attachments. Gate the drain on locally
witnessed turns, re-drive stuck entries FIFO from real events with a
failure budget, restore merged entries on steer failure, persist the
queue per session so a refresh loses nothing, and converge queues
across tabs via storage events.

* fix(kimi-web): merge cross-tab queue updates by entry id (review P1/P2)

Whole-record adoption could silently discard a prompt another tab
enqueued concurrently. Queue entries now carry a stable id and enqueue
timestamp; adoption union-merges snapshots by id, a shared TTL'd removal
set stops flushed/discarded entries from resurrecting (and being
flushed twice), an in-flight marker covers the submit window, and
manual reorders re-stamp timestamps so they survive merges. The flush
failure budget is also tracked per entry instead of per session, so
removing or reordering the head no longer hands its strikes to the
next entry.

* fix(kimi-web): single-flusher queue entries, merge order convergence, forget guard (review round 2)

Turn-end events reach every open tab and the server accepts concurrent
submissions as distinct prompts, so two tabs holding the same adopted
entry could both submit it. Entries now record their owner tab and only
the owner flushes (ownerless legacy entries flush anywhere); an idle
send adopts entries left behind by closed tabs so a stranded queue can
still drain. Cross-tab merges now keep the newer enqueuedAt copy per id
so a manual reorder converges instead of ping-ponging writes, and the
flush failure callback no longer resurrects a queue whose session was
forgotten while the submit was pending.

* refactor(kimi-web): drop the cross-tab queue persistence, keep the minimal fix

Cross-tab queue sync over localStorage is a distributed-systems problem
(claim/lease, conflict merge, ownership) that keeps generating review
findings far beyond this PR's scope. Remove the persistence/hydration/
adoption machinery wholesale; keep the bug fix proper: gated queue
drain, event-driven FIFO retry with a per-entry failure budget, steer
queue restore, and the forgotten-session flush guard. Durable queued
prompts will be designed together with the server-side prompt queue.

* chore: align the changeset wording with the reduced scope

* fix(kimi-web): never duplicate on ambiguous submit failures; advance after drop

Two review findings: (1) restoring merged queue entries after ANY steer
failure could re-submit prompts the daemon had already accepted when the
failure was a lost response — submits now report ok/rejected/uncertain
and restores + flush re-queues happen only on definitive daemon
rejections, while ambiguous failures drop the entry (the failure toast
still tells the user); (2) dropping an exhausted queue head no longer
strands the entries behind it — the new head is submitted immediately,
carrying its own failure budget.
7723qqq pushed a commit to 7723qqq/kimi-code that referenced this pull request Jul 17, 2026
…ents (MoonshotAI#1833)

* fix(kimi-web): stop the prompt queue from ghost-sending stale attachments

Sending while a turn was running queues prompts locally. A failed flush
left entries stuck, and every later session open silently re-submitted
them with their old file attachments. Gate the drain on locally
witnessed turns, re-drive stuck entries FIFO from real events with a
failure budget, restore merged entries on steer failure, persist the
queue per session so a refresh loses nothing, and converge queues
across tabs via storage events.

* fix(kimi-web): merge cross-tab queue updates by entry id (review P1/P2)

Whole-record adoption could silently discard a prompt another tab
enqueued concurrently. Queue entries now carry a stable id and enqueue
timestamp; adoption union-merges snapshots by id, a shared TTL'd removal
set stops flushed/discarded entries from resurrecting (and being
flushed twice), an in-flight marker covers the submit window, and
manual reorders re-stamp timestamps so they survive merges. The flush
failure budget is also tracked per entry instead of per session, so
removing or reordering the head no longer hands its strikes to the
next entry.

* fix(kimi-web): single-flusher queue entries, merge order convergence, forget guard (review round 2)

Turn-end events reach every open tab and the server accepts concurrent
submissions as distinct prompts, so two tabs holding the same adopted
entry could both submit it. Entries now record their owner tab and only
the owner flushes (ownerless legacy entries flush anywhere); an idle
send adopts entries left behind by closed tabs so a stranded queue can
still drain. Cross-tab merges now keep the newer enqueuedAt copy per id
so a manual reorder converges instead of ping-ponging writes, and the
flush failure callback no longer resurrects a queue whose session was
forgotten while the submit was pending.

* refactor(kimi-web): drop the cross-tab queue persistence, keep the minimal fix

Cross-tab queue sync over localStorage is a distributed-systems problem
(claim/lease, conflict merge, ownership) that keeps generating review
findings far beyond this PR's scope. Remove the persistence/hydration/
adoption machinery wholesale; keep the bug fix proper: gated queue
drain, event-driven FIFO retry with a per-entry failure budget, steer
queue restore, and the forgotten-session flush guard. Durable queued
prompts will be designed together with the server-side prompt queue.

* chore: align the changeset wording with the reduced scope

* fix(kimi-web): never duplicate on ambiguous submit failures; advance after drop

Two review findings: (1) restoring merged queue entries after ANY steer
failure could re-submit prompts the daemon had already accepted when the
failure was a lost response — submits now report ok/rejected/uncertain
and restores + flush re-queues happen only on definitive daemon
rejections, while ambiguous failures drop the entry (the failure toast
still tells the user); (2) dropping an exhausted queue head no longer
strands the entries behind it — the new head is submitted immediately,
carrying its own failure budget.
ywh114 pushed a commit to ywh114/kimi-code that referenced this pull request Jul 19, 2026
…ents (MoonshotAI#1833)

* fix(kimi-web): stop the prompt queue from ghost-sending stale attachments

Sending while a turn was running queues prompts locally. A failed flush
left entries stuck, and every later session open silently re-submitted
them with their old file attachments. Gate the drain on locally
witnessed turns, re-drive stuck entries FIFO from real events with a
failure budget, restore merged entries on steer failure, persist the
queue per session so a refresh loses nothing, and converge queues
across tabs via storage events.

* fix(kimi-web): merge cross-tab queue updates by entry id (review P1/P2)

Whole-record adoption could silently discard a prompt another tab
enqueued concurrently. Queue entries now carry a stable id and enqueue
timestamp; adoption union-merges snapshots by id, a shared TTL'd removal
set stops flushed/discarded entries from resurrecting (and being
flushed twice), an in-flight marker covers the submit window, and
manual reorders re-stamp timestamps so they survive merges. The flush
failure budget is also tracked per entry instead of per session, so
removing or reordering the head no longer hands its strikes to the
next entry.

* fix(kimi-web): single-flusher queue entries, merge order convergence, forget guard (review round 2)

Turn-end events reach every open tab and the server accepts concurrent
submissions as distinct prompts, so two tabs holding the same adopted
entry could both submit it. Entries now record their owner tab and only
the owner flushes (ownerless legacy entries flush anywhere); an idle
send adopts entries left behind by closed tabs so a stranded queue can
still drain. Cross-tab merges now keep the newer enqueuedAt copy per id
so a manual reorder converges instead of ping-ponging writes, and the
flush failure callback no longer resurrects a queue whose session was
forgotten while the submit was pending.

* refactor(kimi-web): drop the cross-tab queue persistence, keep the minimal fix

Cross-tab queue sync over localStorage is a distributed-systems problem
(claim/lease, conflict merge, ownership) that keeps generating review
findings far beyond this PR's scope. Remove the persistence/hydration/
adoption machinery wholesale; keep the bug fix proper: gated queue
drain, event-driven FIFO retry with a per-entry failure budget, steer
queue restore, and the forgotten-session flush guard. Durable queued
prompts will be designed together with the server-side prompt queue.

* chore: align the changeset wording with the reduced scope

* fix(kimi-web): never duplicate on ambiguous submit failures; advance after drop

Two review findings: (1) restoring merged queue entries after ANY steer
failure could re-submit prompts the daemon had already accepted when the
failure was a lost response — submits now report ok/rejected/uncertain
and restores + flush re-queues happen only on definitive daemon
rejections, while ambiguous failures drop the entry (the failure toast
still tells the user); (2) dropping an exhausted queue head no longer
strands the entries behind it — the new head is submitted immediately,
carrying its own failure budget.
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