Skip to content

CPLAT-12461: stop dropped commands stranding scratchpad collection - #171

Open
gavin-jeong wants to merge 1 commit into
masterfrom
CPLAT-12461-scratchpad-dropped-cmds
Open

gavin-jeong wants to merge 1 commit into
masterfrom
CPLAT-12461-scratchpad-dropped-cmds

Conversation

@gavin-jeong

Copy link
Copy Markdown
Collaborator

JIRA: https://sendbird.atlassian.net/browse/CPLAT-12461

Follow-up to #170, reported as "scratchpad doesn't show in the day pane" — with a screenshot of the pane sitting on the Plans tab.

The bug

dayScratchpadCmds marks a session in-flight before returning its command, and six call sites discarded that return value — the View path, which structurally cannot dispatch a tea.Cmd, plus the tab-switch and query handlers, which returned nil.

A discarded command left the session marked as "already walking" against work that never ran. It was then neither collected nor eligible for re-dispatch: its scratchpad was missing permanently. Switching tabs was enough to trigger it, which is exactly what the screenshot shows.

refreshOutputsPreviewLayout already documents this precise failure for the outputs digest. The new code did not follow that pattern — my miss in #170.

Fix, in three layers

Any one alone leaves a sharp edge:

  1. Propagate. setDayOutputTabKind, cycleDayOutputTab, selectDayOutputTab, applyDayOutputQuery and clearDayOutputSearch were void and now return tea.Cmd; every caller passes it on.
  2. A non-dispatching form for render paths. refreshDayPreviewLayout / refreshSessionPreviewLayout re-render without arming anything — the same shape as refreshOutputsPreviewLayout.
  3. A TTL on the in-flight mark. A dropped command now costs a retry after 10s instead of a permanent hole. Marking work in flight before it reaches the runtime cannot be made safe by discipline alone.

A second bug, found while verifying the first

The fan-out cap was applied per call, not over the total in flight. Every completion re-renders the pane, so each message started a fresh batch of 8 — 250 sessions reached 218 walks running at once. The budget now covers the running set, with stale marks expired first.

Measured after the fix: 250 sessions collect completely, in-flight never exceeds 8, cursor movement is 12µs against a 16ms frame budget.

Test plan

  • go build ./... && go vet ./... && go test ./... green
  • Both bugs reproduced first, through the real Update loop, before any change
  • TestDayScratchpadStaysBoundedAndCompletes — dispatches repeatedly without completing anything, which is how the overrun actually accumulates. My first version of this test checked a single call and passed with the bug present; it now fails on it (verified by reverting).
  • TestDayCursorMoveCostWithScratchpad — the existing TestDayCursorMoveCost builds sessions with no scratchpad, so it never exercised this path at all.
  • Verified against the real on-disk scratchpad from the report (mitm-proxy, 16 rows) end to end.

One existing test's assertion is now wrong

TestDayPaneScratchpadCollectionIsBounded asserted that a later render dispatches the next batch. That was my own wording in #170 and it is incorrect: re-rendering is not evidence that anything finished, and dispatching there is the overrun. It now asserts the real contract — nothing new while the batch is running, progress once one completes.

Security checklist

  • No SecurityGroup rule changes
  • No 0.0.0.0/0 inbound
  • No public subnet resources
  • No IAM user changes
  • No secrets in code, commits, or logs

dayScratchpadCmds marks a session in flight before returning its command,
and six call sites discarded that return value — the View path, which
structurally cannot dispatch a tea.Cmd, plus the tab-switch and query
handlers, which returned nil.

A discarded command left the session marked as "already walking" against
work that never ran: neither collected nor eligible for re-dispatch, so
its scratchpad was missing permanently. Switching tabs was enough to
trigger it, which is what the report showed.

refreshOutputsPreviewLayout already documents this exact failure for the
outputs digest. The new code did not follow it.

Fixed in three layers, because any one alone leaves a sharp edge:

  - propagate: setDayOutputTabKind, cycleDayOutputTab, selectDayOutputTab,
    applyDayOutputQuery and clearDayOutputSearch were void and now return
    tea.Cmd, and every caller passes it on
  - a non-dispatching form for render paths: refreshDayPreviewLayout and
    refreshSessionPreviewLayout re-render without arming anything, the
    same shape as refreshOutputsPreviewLayout
  - a TTL on the in-flight mark, so a dropped command costs a retry after
    10s rather than a permanent hole. Marking work in flight before it
    reaches the runtime cannot be made safe by discipline alone

Verifying that turned up a second bug: the fan-out cap was applied per
call rather than over the total in flight. Every completion re-renders
the pane, so each message started a fresh batch of 8 and 250 sessions
reached 218 walks running at once. The budget now covers the running
set, with stale marks expired first. Measured after: 250 sessions
collect completely, in flight never exceeds 8, cursor movement is 12µs
against a 16ms frame budget.

TestDayScratchpadStaysBoundedAndCompletes dispatches repeatedly WITHOUT
completing anything, which is how the overrun actually accumulates — a
single-call check passes with the bug present, which the first version
of this test did.

TestDayCursorMoveCost builds sessions with no scratchpad, so it never
exercised this path; TestDayCursorMoveCostWithScratchpad covers it.

TestDayPaneScratchpadCollectionIsBounded asserted that a later render
dispatches the next batch. That is now wrong: re-rendering is not
evidence that anything finished. It asserts the real contract instead —
nothing new while the batch runs, progress once one completes.
@upwind-code-us

upwind-code-us Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Upwind Upwind Code Scan - ✅ Passed

0 newly introduced vulnerabilities · 0 resolved · 1 total in this PR vs master

Total breakdown: 🔶 1 High

View full analysis in Upwind Console

Scan completed in 8s

Scan history (1 scan)
Commit Scanned at New Resolved Net
db1e2b6 < 2026-09-29 10:00 UTC 0 0 0

Last scanned: db1e2b6 · 2026-09-29 10:00 UTC

@upwind-code-us

upwind-code-us Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Upwind Upwind IaC Scan - ✅ Passed

0 newly introduced misconfigurations · 0 resolved · 0 total in this PR vs master

View full analysis in Upwind Console →

Scan completed in 5s

Scan history (1 scan)
Commit Scanned at New Resolved Net
db1e2b6 < 2026-09-29 10:00 UTC 0 0 0

Last scanned: db1e2b6 · 2026-09-29 10:00 UTC

@Kairo-Kim Kairo-Kim added the auto-review/approved Auto-approved by the Slack auto-reviewer bot label Sep 29, 2026

@jinsekim jinsekim 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.

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-review/approved Auto-approved by the Slack auto-reviewer bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants