fix(chat): pending dialog survives mid-window transcript writes and refetch churn #90

Merged
dominik.polakovics merged 1 commit from lab/20260710-0155 into main 2026-07-10 03:04:36 +02:00

Closes #89

Two independent bugs behind the operator report ("question dialog disappears but is still in Claude Code" / "what I type as the answer just disappears"):

Server: mtime staleness guard hid genuinely pending dialogs

claudecode.PendingDialog suppressed the dialog spool whenever the transcript's mtime was newer than the spool's — premised on compat §5's "the transcript is byte-frozen while a dialog is pending". Live evidence (grill transcript d4be520a, 2026-07-10): a message queued while the picker was up appended queue-operation + attachment entries mid-window, after which the server served pending_dialog: null for a still-open picker (card gone in the chat, composer unlocked over a focused picker).

The rotation staleness issue #34 actually cared about is now keyed to the spool payload's own session identity (session_id, transcript_path-stem fallback) compared to the current transcript's filename stem: exact for /clear//rewind (suppresses regardless of which file is newer on disk), immune to mid-window writes. A payload with no session identity degrades to the old mtime backstop. compat §5/§9 record the corrected ground truth.

Client: dialog answers wiped by refetch churn

Every messages response is a fresh JSON parse, so the same pending dialog arrived as a new object each SSE tick. DialogPanel's and MultiQuestionForm's reset effects used Solid's on(...) without equality gating — any refetch (sibling-run run.changed, resync, the needs-input marker) wiped in-progress picks and half-typed Other text; the identity churn also recreated the option rows' <input> (focus loss). Now pendingDialogField keeps its object identity while the same tool_id stays pending, and both resets key on an equality-gated createMemo.

Tests

All red on the old code, green with the fix:

  • TestPendingDialog_survivesTranscriptWritesDuringPending — same-session transcript write during the pending window keeps the dialog served
  • TestPendingDialog_staleAfterTranscriptRotation — rotated-out session suppressed regardless of mtime order (hardened both directions)
  • TestPendingDialog_legacyPayloadFallsBackToMtime — identity-less payload keeps the old backstop
  • RunChat: picks, typed Other text, and the input element itself survive a same-dialog refetch (flat multi-select + multi-question form); a real identity change still resets

Gates: go test ./..., go vet, tsc --noEmit, eslint, prettier, 560 web tests — all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GTGfgrhqMrfNotfZ6dypsS

Closes #89 Two independent bugs behind the operator report ("question dialog disappears but is still in Claude Code" / "what I type as the answer just disappears"): ## Server: mtime staleness guard hid genuinely pending dialogs `claudecode.PendingDialog` suppressed the dialog spool whenever the transcript's mtime was newer than the spool's — premised on compat §5's "the transcript is byte-frozen while a dialog is pending". Live evidence (grill transcript `d4be520a`, 2026-07-10): a message queued while the picker was up appended `queue-operation` + `attachment` entries mid-window, after which the server served `pending_dialog: null` for a still-open picker (card gone in the chat, composer unlocked over a focused picker). The rotation staleness issue #34 actually cared about is now keyed to the spool payload's own session identity (`session_id`, `transcript_path`-stem fallback) compared to the current transcript's filename stem: exact for /clear//rewind (suppresses regardless of which file is newer on disk), immune to mid-window writes. A payload with no session identity degrades to the old mtime backstop. compat §5/§9 record the corrected ground truth. ## Client: dialog answers wiped by refetch churn Every messages response is a fresh JSON parse, so the same pending dialog arrived as a new object each SSE tick. `DialogPanel`'s and `MultiQuestionForm`'s reset effects used Solid's `on(...)` without equality gating — any refetch (sibling-run `run.changed`, resync, the needs-input marker) wiped in-progress picks and half-typed Other text; the identity churn also recreated the option rows' `<input>` (focus loss). Now `pendingDialogField` keeps its object identity while the same `tool_id` stays pending, and both resets key on an equality-gated `createMemo`. ## Tests All red on the old code, green with the fix: - `TestPendingDialog_survivesTranscriptWritesDuringPending` — same-session transcript write during the pending window keeps the dialog served - `TestPendingDialog_staleAfterTranscriptRotation` — rotated-out session suppressed regardless of mtime order (hardened both directions) - `TestPendingDialog_legacyPayloadFallsBackToMtime` — identity-less payload keeps the old backstop - RunChat: picks, typed Other text, and the input element itself survive a same-dialog refetch (flat multi-select + multi-question form); a real identity change still resets Gates: `go test ./...`, `go vet`, `tsc --noEmit`, eslint, prettier, 560 web tests — all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01GTGfgrhqMrfNotfZ6dypsS
fix(chat): pending dialog survives mid-window transcript writes and refetch churn (#89)
All checks were successful
ci / native (pull_request) Successful in 4m50s
f0080df140
Two root causes behind "dialog disappears but is still in Claude Code" /
"what I type as the answer disappears":

Server — claudecode.PendingDialog judged the dialog spool stale whenever the
transcript mtime passed the spool mtime, premised on compat §5's "byte-frozen
while pending". Live 2026-07-10 (grill transcript d4be520a): a message queued
while the picker was up appends queue-operation/attachment entries mid-window,
so the guard hid a genuinely pending dialog until it resolved (and unlocked
the composer over a focused picker). Staleness is now keyed to the spool
payload's session_id (transcript_path stem fallback) vs the current
transcript's stem — exact for the issue #34 rotation case, immune to
mid-window writes; a payload with no session identity keeps the mtime
backstop. compat §5/§9 record the corrected ground truth.

Client — every refetch delivers the same pending dialog as a new object;
DialogPanel's and MultiQuestionForm's reset effects (Solid `on` without
equality gating) wiped in-progress picks and Other text on every SSE tick,
and the identity churn recreated the option rows' input element (focus loss).
pendingDialogField now keeps its object identity while the same tool_id stays
pending, and both resets key on an equality-gated createMemo.

Regression tests both ways (red on the old code): a same-session transcript
write during the pending window keeps the dialog served; rotation still
suppresses regardless of mtime order; picks/typed text/input element survive
a same-dialog refetch; a real identity change still resets.

Closes #89

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTGfgrhqMrfNotfZ6dypsS
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!90
No description provided.