Skip to content

fix: surface missing-history-acked messages as Not delivered with explicit resend - #74

Merged
lodystage[bot] merged 4 commits into
mainfrom
fix/issue-47-missing-history-redeliver
Aug 27, 2026
Merged

fix: surface missing-history-acked messages as Not delivered with explicit resend#74
lodystage[bot] merged 4 commits into
mainfrom
fix/issue-47-missing-history-redeliver

Conversation

@lodystage

@lodystage lodystage Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes defect 2 (and the one-line part of defect 3) of #47. The missing-history negative acknowledgement (SessionMeta.lastMissingHistoryUserMsgId) was a one-way door: once the bounded history-sync wait expired, the exact user turn was excluded from every dispatch path forever, even when its history entry arrived late and sat visibly pending in the conversation.

Final semantics (option A): the marker is a permanent one-shot negative ack for the exact turn — no path ever re-dispatches the old turn. Recovery is an explicit, user-driven resend-as-new-message.

Behavior

  • Row: a visible, non-terminal user entry whose id exactly matches the marker renders a terminal "Not delivered / 未送达" label (red icon + text; icon-only on mobile) instead of an endless sending state. Display-only derivation (packages/components/src/lib/undelivered-user-turn.ts) — no CLI repair write, no schema change.
  • Recovery: the label is the entry point. Clicking it opens a confirmation dialog that resends the turn's exact content (canonical inputConfig.inputBlocks, attachments included) as a brand-new message through the ordinary composer send path with a fresh turn id. The ordinary producer write clears the marker, hiding both the dialog trigger and the label.
  • Duplicate-dispatch guards: the resend supersedes the abandoned entry to canceled, and the daemon's RPC stash now drops turns whose history entry is already terminal — otherwise the stale pending payload could execute again once the marker is gone. peekStashedRpcTurn also honors the marker itself, so a duplicate RPC offer can never resurrect the turn while the marker stands.
  • Observability (defect 3): the history-sync wait-start log and the missing-history recovery log now include the pending turn id.

Test plan

  • apps/cli: marker-matched turn is not dispatchable from history or the RPC stash; a stashed turn superseded to canceled is dropped (never duplicate-dispatched); existing marker-suppression suites stay green (50 tests).
  • packages/components: derivation predicate (exact id, terminal states, legacy read); resend block extraction (canonical blocks with attachments, history-items and prompt fallbacks); dialog flow (label opens dialog, confirm resends exact content once, cancel is a no-op, label non-interactive without a handler); ordinary send path mints a fresh turn id for identical content (36 tests).
  • Storybook: Sessions/UserMessageNotDelivered (row label + click-to-open dialog, light/dark, en/zh).
  • pnpm check: typecheck, type-aware lint (0 errors), i18n and boundary checks pass. The only failing tests are 7 pre-existing failures in local-terminal-panel and create-workspace-runtime-meta-recovery, verified to fail identically on clean main.

Model: kimi-code/k3

zxch3n and others added 4 commits August 27, 2026 03:47
e8d8748 introduced an unused rest parameter in
session-file-content-view.test.tsx and a valueless return in
KeyboardShortcutsSetting.stories.tsx, leaving 'pnpm lint' (and therefore
'pnpm check') red at HEAD. Remove the dead parameter and return undefined
explicitly so the repository-wide check can pass again.

Model: kimi-code/k3
The missing-history negative acknowledgement (SessionMeta
lastMissingHistoryUserMsgId) was a one-way door: once the bounded
history-sync wait expired, the exact turn was excluded from every dispatch
path forever, even when its history entry arrived late and sat visibly
pending in the conversation (issue #47, defect 2).

Adopt the option C semantics: the marker stays a one-shot negative ack for
the exact turn and nothing ever re-dispatches it automatically. The
renderer now derives a terminal "Not delivered" state for the matching
non-terminal entry (display-only derivation from meta + entry state — no
CLI repair write, no schema change) and offers an explicit "Deliver now"
action. That action is a dispatch-producer write: it re-aims
latestUserMsgId at the exact entry and clears the marker only when it
names that entry, landing before the RPC fast path fires, so ordinary turn
selection dispatches the entry exactly once. The RPC stash honors the same
exact-id suppression, so a duplicate offer alone cannot resurrect the turn
while the marker stands.

Also log the pending turn id when the history-sync wait starts and when
the missing-history recovery is recorded (issue #47, defect 3).

Model: kimi-code/k3
…viving

Rework the recovery UX for the missing-history negative acknowledgement
(issue #47, defect 2) to option A semantics: lastMissingHistoryUserMsgId
is a permanent one-way door for the exact turn — no path re-dispatches it.

- The user row keeps the derived "Not delivered" label (now shared via
  src/lib/undelivered-user-turn.ts) and loses its row-level action.
- Recovery moves to a conversation-bottom resend bar above the composer:
  it resends the undelivered turn's exact content (canonical inputConfig
  blocks, attachments included) as a brand-new message through the
  ordinary composer send path with a fresh turn id. The ordinary producer
  write clears the marker, hiding both the bar and the label.
- The abandoned entry is superseded to 'canceled' on resend, and the RPC
  stash now drops turns whose history entry is already terminal —
  otherwise the stale pending payload could duplicate-dispatch once the
  marker is gone.
- Removed the previous old-turn redelivery write path
  (redeliverSessionUserTurnWithRuntime / hook method) and its tests.

The missing-history wait/recovery logs keep the pending turn id (defect 3).

Model: kimi-code/k3
…dialog

Simplify the missing-history recovery UX (issue #47, defect 2): the
conversation-bottom resend bar is gone. The "Not delivered" label on the
user row is now itself the recovery entry point — clicking it opens a
confirmation dialog that resends the turn's exact content as a NEW
message through the ordinary composer send path (fresh turn id), then
supersedes the abandoned entry to 'canceled' so the stale pending copy
can never duplicate-dispatch once the ordinary send clears the marker.

Model: kimi-code/k3
@lodystage
lodystage Bot merged commit baf7044 into main Aug 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants