Skip to content

feat(insights): Phase 1 β€” post-game LLM summaries (M0a + M1)#262

Open
mwickett wants to merge 8 commits into
feature/llm-insightsfrom
feature/llm-insights-p1-summary
Open

feat(insights): Phase 1 β€” post-game LLM summaries (M0a + M1)#262
mwickett wants to merge 8 commits into
feature/llm-insightsfrom
feature/llm-insights-p1-summary

Conversation

@mwickett

Copy link
Copy Markdown
Owner

LLM Insights β€” Phase 1: post-game summaries (M0a + M1)

First slice of the LLM Insights feature (design spec: docs/superpowers/specs/2026-06-13-llm-insights-design.md). After a Dutch Blitz game finishes, Claude writes a short, warm, neutral recap grounded in the real scores. Ships dark behind the existing llm-features flag.

This is the integration base; later phases (chat, metric-plan DSL, dashboard tiles, RLS substrate) land as their own PRs into feature/llm-insights.

What's in it

M0a β€” Claude foundation

  • @anthropic-ai/sdk + a singleton client; generateSummaryText (Opus 4.8, adaptive thinking, low effort, no sampling params; throws on refusal/empty; sums all billed tokens).
  • GameSummary model + migration (hand-authored, verified identical to Prisma's canonical DDL). The row doubles as the durable job unit (status machine + retryCount + sourceStatsHash).

M1 β€” Post-game summary

  • Deterministic recap pipeline (pure, fully tested): buildGameRecap β†’ recapHash β†’ pseudonymize β†’ summaryPrompt β†’ grounding. Built entirely in playerKey space: real names and internal IDs never reach the model; names are re-substituted server-side after generation.
  • Durable generation: a synchronous in-request enqueue writes the pending row (primary DB), then the LLM runs via after(); a /api/insights/sweep cron route (CRON_SECRET-guarded) retries anything stuck. Terminal writes are hash-conditional so a stale in-flight run can't clobber newer work.
  • Triggered from both finish paths β€” direct finish and edits to already-finished games (syncGameCompletionAfterScoreWrite) β€” hash-gated so unchanged scores are a no-op.
  • Renders on the game page (flag-gated) with ready / pending / insufficient states.

Review

Two Codex review rounds (plan + implementation), all findings addressed:

  • Plan: durability (write pending synchronously, not inside after()), the finished-game-edit trigger, playerKey-keyed pseudonymization, migrate dev β†’ hand-authored migration, satisfies over as, refusal/empty handling, sourceUpdatedAt from source data.
  • Implementation: race-safe hash-conditional terminal writes (blocker), order-independent recap hash, retry-budget reset on new hash, no fake "pending" UI.

Verification

  • npm test β†’ 28 suites / 172 tests pass Β· npm run typecheck clean Β· npm run lint clean.
  • No changes to existing gameplay schema or scoring logic; existing mutation tests still pass (summary stubbed).

πŸ”΄ Needs a human before this does anything in prod

  • Set ANTHROPIC_API_KEY in the deployment env (+ local .env for manual E2E). Documented in .env.example.
  • The migration auto-applies on deploy via prisma migrate deploy (build script). For local testing, apply it against a dev DB.
  • Enable the llm-features PostHog flag for the intended users β€” both generation and rendering are gated on it.
  • Set CRON_SECRET and add a Vercel cron hitting GET /api/insights/sweep (e.g. every 10 min) for retry durability.
  • Manual E2E: finish a game (flag on), confirm the recap renders with the real winner and grounded numbers, no second-person.

Known limitations (by design, for MVP)

  • Games finished while the flag was off are never back-filled (summaries are forward-looking from flag enablement). A backfill job can be added later.
  • Pseudonymization is correct for duplicate guest names (keyed by playerKey); the broader chat-side scope/RLS substrate is a later phase.
  • In an exact double-tie (two players with the same cumulative total and the same final-round blitz pile), the recap hash can vary by DB score order, because breakTie (core scoring) keeps the first DB-order candidate. Impact is only a rare redundant regeneration β€” within any render the recap agrees with the scoreboard. The underlying core-scoring non-determinism is tracked as a separate follow-up (out of scope here per repo convention: don't change scoring logic in an insights PR).

mwickett added 7 commits June 13, 2026 15:03
Single non-streaming Messages call on claude-opus-4-8 (adaptive thinking,
low effort, no sampling params). Throws on refusal/empty output; sums all
billed token fields. Document ANTHROPIC_API_KEY + CRON_SECRET in .env.example.
Durable per-game recap row (status machine + retryCount + sourceStatsHash)
with unique (game_id, prompt_version). Hand-authored migration verified
identical to Prisma's canonical DDL via migrate diff --from-empty.
buildGameRecap builds deterministic facts in playerKey space (winner at
rank 1 even on tiebreaks); recapHash stable-hashes them; pseudonymizeRecap
projects to an LLM-facing payload with no real names or internal ids;
grounding flags ungrounded numbers. 14 tests.
Split enqueue (sync, primary, writes durable pending row) from run (async
LLM call), scheduled via after() and flag-gated on llm-features. Triggered
from updateGameAsFinished AND syncGameCompletionAfterScoreWrite so edits to
finished games regenerate (hash-gated). Sweep for retries. 9 tests.
getGameSummary reads the primary (avoids replica lag on a just-finished
game). GameSummaryCard renders ready/pending/insufficient states; the page
shows it flag-gated when finished. /api/insights/sweep retries stuck rows,
guarded by CRON_SECRET. 4 component tests.
- Race-safe terminal writes: runGameSummary persists ready/failed/insufficient
  via a hash-conditional updateMany, so a stale in-flight run can't overwrite
  newer work (Codex BLOCKER).
- Deterministic recap hash: build calcGameStats input in sorted playerKey order
  so player/score DB ordering can't change biggestRound/worstRound.
- Reset retryCount when the source hash changes, so post-edit work is sweepable.
- Pseudonymizer never falls back to a raw playerKey.
- Render the recap card only when a row exists (no fake 'pending' for games
  finished while the flag was off). 2 new tests (172 total).
@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
blitzer Ready Ready Preview, Comment Jun 13, 2026 10:21pm

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

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// (audienceUserId is null in MVP β€” neutral recap). The row is also the durable
// job unit: status + retryCount drive enqueue/run/sweep. sourceStatsHash is
// stored (not in the unique key) so score edits after finish are detected.
model GameSummary {

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 Regenerate the Prisma client for GameSummary

This repo checks in the custom Prisma client under src/generated/prisma, but this commit adds GameSummary without updating those generated files (rg gameSummary src/generated/prisma returns nothing). As a result, a checkout of this commit has no prisma.gameSummary delegate and npm run typecheck -- --pretty false fails with TS2339 at every new summary query, so the app cannot typecheck until the client is regenerated/committed or generation is added before typecheck/build.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment thread src/server/ai/summary.ts
// Flag-gated entry point used by the game-finish paths. Writes the durable
// pending row synchronously, then runs the LLM after the response is sent.
export async function scheduleGameSummary(gameId: string): Promise<void> {
if (!(await isLlmFeaturesEnabled())) 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 Enqueue summaries without relying on the editor's flag

Because this flag check is evaluated for the user who happened to finish or edit the game, a game-level summary is never enqueued when that editor lacks llm-features. The game page separately checks the viewing user's flag and renders only the stored getGameSummary row, so during a per-user rollout a flagged teammate can open the finished game and see no recap, or continue seeing a stale recap after an unflagged member edits finished scores; enqueueing needs to be based on a game/org/global condition or refresh existing summaries regardless of the editor's flag.

Useful? React with πŸ‘Β / πŸ‘Ž.

…scheduling

- failed run-writes are now conditional on status != ready, so a concurrent
  or late failure can't knock a succeeded summary back to failed.
- enqueue no longer re-schedules when the row already matches the hash,
  preventing duplicate runs/cost on repeated same-source triggers (the sweep
  still retries non-ready rows).
- scheduleGameSummary is best-effort on the finish/edit paths (try/catch) so a
  summary failure can never fail the game finish or score edit. 3 new tests (174).
@mwickett

Copy link
Copy Markdown
Owner Author

Codex review trail (Phase 1)

Five Codex passes, all findings addressed:

  1. Design spec β€” 24 findings β†’ folded into the spec (durability, RLS/pooling, metric-plan DSL, etc.).
  2. Phase 1 plan (verified vs codebase) β€” 14 findings, 3 blockers (finished-game edits, after() not a durable queue, per-user-flag pending UI) β†’ plan revised.
  3. Implementation (full diff) β€” 1 blocker (stale in-flight run overwriting newer work) + 5 β†’ fixed.
  4. Fix verification β€” confirmed; flagged a rare exact-double-tie determinism edge in core breakTie (out of scope here; tracked as a separate follow-up).
  5. Comprehensive final-state review β€” ran typecheck + the full suite itself; no blockers; 2 should-fixes β†’ fixed in 27126c6:
    • failed run-writes are now conditional on status != ready (a concurrent/late failure can't knock a succeeded summary back).
    • enqueue no longer re-schedules same-hash work (no duplicate runs/cost); the sweep still retries non-ready rows.
    • summary scheduling is best-effort (try/catch) so it can never fail the game finish / score edit.

Status: npm test 28 suites / 174 tests Β· typecheck clean Β· lint clean. No live E2E yet (gated on ANTHROPIC_API_KEY + the llm-features flag β€” see the human checklist in the PR description).

The two final fixes (27126c6) implement Codex's exact recommendations and are covered by 3 new tests; I did not run a 6th pass on them to avoid review-loop diminishing returns.

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