Skip to content

fix(debate): keep saved pins through early changes and free deleted pins - #77

Merged
Programcode merged 7 commits into
mainfrom
fix/debate-pin-state
Sep 30, 2026
Merged

Programcode merged 7 commits into
mainfrom
fix/debate-pin-state

Conversation

@Programcode

@Programcode Programcode commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #75. An internal review after the merge found two bugs in the pinned-results store.

A pin made before the first pin load returned threw that load away. This regression came from the race fix in #75.

  • What went wrong: when a student opened Review and pinned something before the saved pins arrived, the store dropped the loaded rows. The saved pins then showed as unpinned and stopped being sent. Pinning one of them again failed on the primary key, and it couldn't be unpinned until a reload.
  • The fix: a load now replays the local changes it may have missed, meaning ones made after it started and ones still saving, on top of the saved rows.

A pin on a deleted result counted toward the 3-pin limit forever.

  • What went wrong: Review only lists results that exist, so the orphaned pin could never be unpinned.
  • The fix: the limit now counts only pins on results that still exist in the debate, the chat open in Ask included.

Also in this PR:

  • Each debate's pin writes run one at a time, so a quick pin and unpin reach the database in order.
  • Pin buttons stay disabled until saved pins have loaded, because the limit can't be checked before then.
  • CodeRabbit pointed out that two quick clicks could both pass the limit using one rendered count. c8d5d39 fixes this: the plan now passes the debate's existing result keys, and the store counts its current pins against them.

Codex review follow-ups

Two Codex reviewers (the CLI and the plugin, both on gpt-6.1-sol at high effort) went over this PR in several rounds and found five more problems. Each fix below started with a test that failed.

  • Sign-out. A pin save queued before sign-out could run under the next account, and an old failure could undo the next account's pin. A late pin load could also bring back the old account's pins. Resetting local data now starts a new generation, so work from before the reset is dropped (afb7288).
  • A failed save undid a later click. After pin, unpin and pin with the first save failing, the screen showed unpinned while the database ended pinned. A failure now rolls back only if no later change to the same result is queued behind it. An insert that hits the primary key counts as pinned (dc74e3a).
  • Pins enabled before results loaded. On a fresh sign-in, pins on results that hadn't loaded yet were left out of the limit, so a fourth pin could get through. Pin buttons now wait for saved results and sparring history (253c436).
  • A failed history query did the same. A failed query left its list empty, so real pins were left out of the limit. The session store now reports serverHydration (pending, partial or complete), and the sparring store reports historyComplete. Pins on unlisted results are left out of the limit only when every list loaded. The same commit fixes a quick pin and unpin that both failed: the result is now left unpinned, as the database has it (ad43294).
  • A load erased a pin that finished saving mid-load. A load now also replays changes that were still saving when it started (bf24d44).

Pins on sparring sessions older than the 50 kept locally stay out of the limit on purpose. They can't be shown, sent or unpinned, so counting them would bring back the stuck slot.

Tests

src/test/debate/debate-context.test.tsx has 26 tests, 13 of them new. Every new test failed before its fix.

Verification

All local, at bf24d44 on main f98624f. Hosted GitHub Actions can't start because of the account's billing block, so these runs stand in for CI.

  • ./.superset/db-gate.sh exited 0:
    • 345 pgTAP tests passed.
    • Full-stack and collaboration Playwright: 55 passed and 1 skipped, including debate-context.spec.ts on desktop and mobile.
    • The stack was stopped afterwards.
  • Lint, typecheck, build and docs:check passed.
  • Unit tests: 1,113 passed, with 90.3% statement coverage.
  • Collaboration tests: 30 passed. Edge tests: 270 passed.
  • Desktop and mobile Chromium E2E: 226 passed and 6 skipped, with nothing flaky.
  • Both Codex reviews of bf24d44 found nothing blocking.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Pin limits now count only results available in the debate, including chat results, and exclude pins for unavailable results.
    • Pin controls remain disabled until saved results and sparring history finish loading. Existing pins are retained if either list fails to load.
    • Pin and unpin changes made during loading or saving are preserved more reliably, including quick reversals and failed saves.
    • Rapid pin changes are saved in order to help retain the final pin state.

A pin made before the first pin load returned threw that load away, so
saved pins looked unpinned and could not be unpinned. Loads now replay
local changes on top of the saved rows. Pin writes for a debate run in
order, the limit counts only pins whose result still exists, and pin
buttons wait for saved pins to load.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
debate-navigator Ready Ready Preview Sep 30, 2026 2:42pm UTC

Copilot AI lite review requested due to automatic review settings September 28, 2026 19:19

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: cfe5007a-4b88-4abe-8722-7a102b3c55c3

📥 Commits

Reviewing files that changed from the base of the PR and between c8d5d39 and bf24d44.

📒 Files selected for processing (6)
  • src/components/features/debate/DebateContextControl.tsx
  • src/lib/debate/context.ts
  • src/lib/stores/debate-context-store.ts
  • src/lib/stores/session-store.ts
  • src/lib/stores/sparring-store.ts
  • src/test/debate/debate-context.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/debate/context.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The debate context plan now reports keys for valid results, including the open Ask chat. Pin limits can exclude pins for missing results. Pin controls wait for server, sparring-history, and pin readiness. The store applies pin changes optimistically, serializes writes, and reconciles concurrent loads and failures.

Changes

Debate output pin handling

Layer / File(s) Summary
Track hydration and gate pin controls
src/lib/stores/session-store.ts, src/lib/stores/sparring-store.ts, src/components/features/debate/DebateContextControl.tsx, src/test/debate/debate-context.test.tsx
The session store tracks server hydration as pending, partial, or complete. The sparring store tracks successful saved-history loading. The review controls use these states to gate pin actions. Tests cover readiness and incomplete result lists.
Count available results for pin-limit checks
src/lib/debate/context.ts, src/components/features/debate/DebateContextControl.tsx, src/lib/stores/debate-context-store.ts, src/test/debate/debate-context.test.tsx
The plan includes keys for valid debate results, including the open Ask chat. The control passes those keys to the store when results are complete. The store uses the keys to count pins against the limit.
Reconcile pin loads and serialize pin updates
src/lib/stores/debate-context-store.ts, src/test/debate/debate-context.test.tsx
The store overlays tracked changes on loaded pins, serializes writes per debate, and invalidates pending work on reset. Tests cover overlapping changes, load races, ordered writes, failures, duplicate saves, and account isolation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DebateContextControl
  participant SessionStore
  participant SparringStore
  participant DebateContextStore
  participant PinDatabase
  SessionStore-->>DebateContextControl: server hydration status
  SparringStore-->>DebateContextControl: saved-history completion
  DebateContextControl->>DebateContextStore: loadPins
  DebateContextStore->>PinDatabase: fetch saved pins
  PinDatabase-->>DebateContextStore: saved pins
  DebateContextControl->>DebateContextStore: togglePin with resultKeys
  DebateContextStore->>DebateContextStore: apply optimistic change
  DebateContextStore->>PinDatabase: serialize pin write
Loading

Architecture Summary

Architecture risk: 🔵 Low · up to bf24d

The change affects 1 system.

Changed systems: src

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — src (service) was modified; 6 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in src/components/features/debate/DebateContextControl.tsx: Imports the session and sparring stores to read hydration state.
  • observed — Modified behavior in src/components/features/debate/DebateContextControl.tsx: Adds server-hydration, sparring-hydration, and sparring-history-completion state reads for determining pin and result readiness.
  • observed — Modified behavior in src/components/features/debate/DebateContextControl.tsx: Passes pin readiness and result-completion state into DebateContextReview. The component’s internal signature gains required pinsReady and resultsComplete parameters.
  • observed — Modified behavior in src/components/features/debate/DebateContextControl.tsx: onPin now passes plan.resultKeys to togglePin only when results are complete; previously it called togglePin without that argument.

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

…ed count

Two quick pin clicks shared one rendered count, so both could pass the
three-pin limit. The plan now passes the debate's existing result keys,
and the store counts its current pins against them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

The retained stale-count concern is fixed in c8d5d39:

  • The plan now passes the debate's existing result keys, not a rendered count.
  • togglePin counts the store's current pins against those keys, so a second quick click sees the first click's change.
  • A new test makes two clicks from one render with two pins already set, and expects the second to be rejected.

Enforcing the three-pin limit across devices is left out on purpose. The limit is a preference. Every request uses at most three pins (selectDefaultOutputs slices them), and the server caps saved results at six per request. Extra rows can't change ownership, because RLS and the composite foreign key still govern that.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Programcode

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Programcode and others added 5 commits September 30, 2026 13:21
A pin save queued before sign-out ran after it, reading the next account's
user ID, and a failed old save could undo the next account's pin. The next
account's first pin change also waited behind the old request, and a pin load
that returned after sign-out filled in the old account's pins.

Resetting local data now starts a new generation. Old writes that have not
started are skipped, old results leave state alone, the write queue starts
fresh, and late loads are ignored.

Also explains why pins on results that are not loaded locally do not count
toward the limit: they cannot be shown, sent or unpinned.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pin, unpin and pin again while the first save was pending: if that save
failed, its rollback removed the latest pin from the screen, while the queued
delete and insert still succeeded. The database ended pinned and the app did
not.

A failed save now undoes its change only when no later change to the same
result is queued behind it. An insert that hits the primary key means the
result is already pinned, so it counts as saved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pin limit leaves out pins on results that are not listed, so a deleted
result cannot hold a slot forever. On a fresh sign-in, results load in
parallel with pins. Until they arrive, pins on results still loading were left
out too, and a fourth pin could get through.

Pin buttons now also wait for saved results and sparring history to load. The
session store gets a serverHydrated flag for this. It is not persisted and is
cleared on sign-out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A saved-result or sparring history query that fails still finishes
hydration, leaving that list empty. The pin limit then left out real pins
on the missing results, so a fourth pin could get through.

The session store now reports serverHydration as pending, partial or
complete, and the sparring store reports historyComplete. Pins on unlisted
results are left out of the limit only when every list loaded.

A quick pin and unpin that both failed also left the result pinned,
although neither save reached the database. A failed change that a later
change replaced now hands its prior state to that change, so a failure
rolls back to the last saved state.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A pin load reads the table and answers later. If a pin was still saving
when the load read the table, but finished before the load answered, the
load's older rows replaced it and the pin vanished from the screen.

A load now also replays every change that was still saving when it
started.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Programcode
Programcode merged commit fe6100a into main Sep 30, 2026
5 of 8 checks passed

This branch was successfully deployed

1 active deployment
Preview — bf24d446 Deployed Sep 30, 2026 by vercel[bot]
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.

2 participants