fix(research): reuse a recent identical capture instead of re-measuring (chat#1912 row 4) - #800
Conversation
…ng (chat#1912 row 4) One uninterrupted pass through /setup measured the same albums twice: the seeding valuation via POST /api/valuation, then about three minutes later the first-task pre-run, whose prompt posts to /api/research/measurement-jobs with the same album ids. Both scrape every album and bill for it, and the second leaves a catalog-less snapshot behind. createSnapshot now hands back an identical recent capture (same album set, platforms and schedule, inside 15 minutes) rather than starting a second scrape. Play counts do not move meaningfully in minutes, so the earlier capture is the correct answer. Reuse is reported with estimated_cost_usd 0 and reused: true; the month's snapshots were already loaded for the cost cap, so the check costs no extra query. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 46 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
📝 WalkthroughWalkthroughChangesPlaycount snapshot reuse
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant createSnapshot
participant playcount_snapshots
participant CaptureWorkflow
Caller->>createSnapshot: submit snapshot request
createSnapshot->>playcount_snapshots: load lookback snapshots
createSnapshot->>createSnapshot: calculate spend and find reusable snapshot
alt reusable snapshot found
createSnapshot-->>Caller: return reused payload
else no reusable snapshot
createSnapshot->>playcount_snapshots: insert queued snapshot
createSnapshot->>CaptureWorkflow: start capture workflow
CaptureWorkflow-->>Caller: return new snapshot result
end
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/research/playcounts/createSnapshot.ts`:
- Around line 54-60: Update the monthSnapshots query in createSnapshot to load
records from the earlier of monthStart and the reuse-window cutoff, allowing
reuse across UTC month boundaries. Keep cap-enforcement totals restricted to
rows created in the current month, and preserve the existing
findReusableSnapshot flow.
- Around line 54-73: Make the reusable-snapshot lookup and subsequent snapshot
creation in createSnapshot atomic for each request scope, preventing concurrent
calls from both creating snapshots and starting workflows. Use the existing
database transaction, locking, or idempotency mechanism to serialize the
findReusableSnapshot check with insertion; preserve the current reuse response
for an existing snapshot and ensure only one workflow starts for concurrent
duplicates.
- Around line 50-73: Decompose the playcount snapshot flow into focused helpers:
in lib/research/playcounts/createSnapshot.ts lines 50-73, extract the reuse
response and/or cap, creation, and persistence workflow so createSnapshot
remains under 50 lines; in lib/research/playcounts/findReusableSnapshot.ts lines
22-52, extract the reusable-candidate predicate while keeping
findReusableSnapshot focused on selecting a matching snapshot. Preserve the
existing reuse and cap behavior at both sites.
In `@lib/research/playcounts/findReusableSnapshot.ts`:
- Line 42: Update the created_at validation in findReusableSnapshot to parse the
timestamp once and require it to be finite and within the inclusive range cutoff
<= createdAt <= now. Reject missing, malformed, and future dates while
preserving acceptance of valid timestamps at either boundary.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9c7efbc1-1d33-45cf-8581-7c37b7581929
⛔ Files ignored due to path filters (1)
lib/research/playcounts/__tests__/findReusableSnapshot.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (2)
lib/research/playcounts/createSnapshot.tslib/research/playcounts/findReusableSnapshot.ts
|
|
||
| // Hand back an identical capture taken moments ago rather than scraping the | ||
| // same albums twice. The month's snapshots are already loaded for the cap | ||
| // check, so this costs no extra query. | ||
| const reusable = findReusableSnapshot({ | ||
| snapshots: monthSnapshots, | ||
| albumIds, | ||
| platforms: params.body.platforms, | ||
| schedule: params.body.schedule, | ||
| windowMinutes: REUSE_WINDOW_MINUTES, | ||
| now: new Date(), | ||
| }); | ||
| if (reusable) { | ||
| return { | ||
| data: { | ||
| status: "success", | ||
| snapshot_id: reusable.id, | ||
| state: reusable.state, | ||
| album_count: reusable.album_count, | ||
| estimated_cost_usd: 0, | ||
| reused: true, | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Decompose the changed domain functions to keep them small.
The new reuse branch leaves createSnapshot at roughly 74 lines, while findReusableSnapshot is 31 lines. Extract focused helpers for the reuse response, cap calculation, or persistence/workflow path.
lib/research/playcounts/createSnapshot.ts#L50-L73: split the reuse/cap/create branches socreateSnapshotstays under 50 lines.lib/research/playcounts/findReusableSnapshot.ts#L22-L52: extract the reusable-candidate predicate and keep the exported selector focused.
As per coding guidelines, functions longer than 20 lines must be flagged and kept small. As per path instructions, lib/**/*.ts domain functions must remain under 50 lines.
📍 Affects 2 files
lib/research/playcounts/createSnapshot.ts#L50-L73(this comment)lib/research/playcounts/findReusableSnapshot.ts#L22-L52
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/research/playcounts/createSnapshot.ts` around lines 50 - 73, Decompose
the playcount snapshot flow into focused helpers: in
lib/research/playcounts/createSnapshot.ts lines 50-73, extract the reuse
response and/or cap, creation, and persistence workflow so createSnapshot
remains under 50 lines; in lib/research/playcounts/findReusableSnapshot.ts lines
22-52, extract the reusable-candidate predicate while keeping
findReusableSnapshot focused on selecting a matching snapshot. Preserve the
existing reuse and cap behavior at both sites.
Sources: Coding guidelines, Path instructions
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/research/playcounts/createSnapshot.ts`:
- Around line 54-60: Update the monthSnapshots query in createSnapshot to load
records from the earlier of monthStart and the reuse-window cutoff, allowing
reuse across UTC month boundaries. Keep cap-enforcement totals restricted to
rows created in the current month, and preserve the existing
findReusableSnapshot flow.
- Around line 54-73: Make the reusable-snapshot lookup and subsequent snapshot
creation in createSnapshot atomic for each request scope, preventing concurrent
calls from both creating snapshots and starting workflows. Use the existing
database transaction, locking, or idempotency mechanism to serialize the
findReusableSnapshot check with insertion; preserve the current reuse response
for an existing snapshot and ensure only one workflow starts for concurrent
duplicates.
- Around line 50-73: Decompose the playcount snapshot flow into focused helpers:
in lib/research/playcounts/createSnapshot.ts lines 50-73, extract the reuse
response and/or cap, creation, and persistence workflow so createSnapshot
remains under 50 lines; in lib/research/playcounts/findReusableSnapshot.ts lines
22-52, extract the reusable-candidate predicate while keeping
findReusableSnapshot focused on selecting a matching snapshot. Preserve the
existing reuse and cap behavior at both sites.
In `@lib/research/playcounts/findReusableSnapshot.ts`:
- Line 42: Update the created_at validation in findReusableSnapshot to parse the
timestamp once and require it to be finite and within the inclusive range cutoff
<= createdAt <= now. Reject missing, malformed, and future dates while
preserving acceptance of valid timestamps at either boundary.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9c7efbc1-1d33-45cf-8581-7c37b7581929
⛔ Files ignored due to path filters (1)
lib/research/playcounts/__tests__/findReusableSnapshot.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (2)
lib/research/playcounts/createSnapshot.tslib/research/playcounts/findReusableSnapshot.ts
🛑 Comments failed to post (1)
lib/research/playcounts/findReusableSnapshot.ts (1)
42-42: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject future or invalid
created_atvalues.The current lower-bound-only check lets future timestamps through, and
NaN < cutoffis false for malformed dates. Require a finite timestamp withincutoff <= createdAt <= now.Proposed fix
- if (!row.created_at || new Date(row.created_at).getTime() < cutoff) return false; + const createdAt = row.created_at ? new Date(row.created_at).getTime() : NaN; + if (!Number.isFinite(createdAt) || createdAt < cutoff || createdAt > now.getTime()) return false;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const createdAt = row.created_at ? new Date(row.created_at).getTime() : NaN; if (!Number.isFinite(createdAt) || createdAt < cutoff || createdAt > now.getTime()) return false;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/research/playcounts/findReusableSnapshot.ts` at line 42, Update the created_at validation in findReusableSnapshot to parse the timestamp once and require it to be finite and within the inclusive range cutoff <= createdAt <= now. Reject missing, malformed, and future dates while preserving acceptance of valid timestamps at either boundary.
There was a problem hiding this comment.
2 issues found across 3 files
Confidence score: 3/5
- In
lib/research/playcounts/createSnapshot.ts, the non-atomic read/check/insert flow means concurrent identical requests can both create and scrape snapshots, causing duplicate work and potentially inconsistent snapshot selection for callers — switch to a DB-backed lock or an atomic find-or-create/unique-claim path so only one request wins. - In
lib/research/playcounts/createSnapshot.ts,album_countcan benullon the reuse branch but numeric on the normal branch, which can break consumers expecting a stable number type or force defensive client handling — compute a numeric fallback (for example fromreusable.album_ids?.lengthoralbumIds.length) before returning the payload.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/research/playcounts/createSnapshot.ts">
<violation number="1" location="lib/research/playcounts/createSnapshot.ts:54">
P2: Simultaneous identical requests can still create and scrape two snapshots: the read/check/insert sequence is non-atomic. Use a database-backed lock or atomic find-or-create/unique-claim flow so one request returns the row created by the other.</violation>
<violation number="2" location="lib/research/playcounts/createSnapshot.ts:68">
P2: The response payload's `album_count` field can be `null` on the reused path while it is always a `number` on the normal path. Consider falling back to `reusable.album_ids?.length ?? albumIds.length` instead of passing the nullable column through directly, so the caller always receives a consistent type regardless of the reuse decision.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as API Client
participant CS as createSnapshot
participant Find as findReusableSnapshot (pure)
participant DB as Supabase (playcount_snapshots)
participant WF as playcountSnapshotWorkflow
Note over Client,WF: Snapshot creation with duplicate detection
Client->>CS: POST /api/research/measurement-jobs (body with albumIds, platforms, schedule)
CS->>DB: selectPlaycountSnapshots() for month
DB-->>CS: monthSnapshots (already loaded for cap check)
Note over CS,Find: No extra query – month data reused
CS->>Find: findReusableSnapshot({ snapshots, albumIds, platforms, schedule, windowMinutes=15, now })
Note over Find: Pure function: checks same albums (order-insensitive), same platforms/schedule, created within 15 min, state in (queued/running/done), newest win
alt Reusable snapshot found
Find-->>CS: snapshot row (state, id, album_count, etc.)
CS-->>Client: { status:"success", snapshot_id, state, album_count, estimated_cost_usd:0, reused:true }
else No reusable snapshot
Find-->>CS: null
CS->>CS: Check monthly cap (existing logic)
CS->>DB: insertPlaycountSnapshot(...)
DB-->>CS: new snapshot row
CS->>WF: playcountSnapshotWorkflow(new snapshot)
WF-->>CS: (async)
CS-->>Client: { status:"success", snapshot_id, ...estimated_cost_usd, reused:false }
end
Note over Client,Find: Duplicate scenario (chat#1912 row 4)
Client->>CS: First call (albums A, B, C) – no prior snapshot
CS->>Find: Finds nothing
CS->>DB: insert + workflow starts
Client->>CS: Second call (same albums, 3 min later)
CS->>Find: Finds first snapshot (e.g. state: "running")
CS-->>Client: reuse=true, cost=0
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // Hand back an identical capture taken moments ago rather than scraping the | ||
| // same albums twice. The month's snapshots are already loaded for the cap | ||
| // check, so this costs no extra query. | ||
| const reusable = findReusableSnapshot({ |
There was a problem hiding this comment.
P2: Simultaneous identical requests can still create and scrape two snapshots: the read/check/insert sequence is non-atomic. Use a database-backed lock or atomic find-or-create/unique-claim flow so one request returns the row created by the other.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/research/playcounts/createSnapshot.ts, line 54:
<comment>Simultaneous identical requests can still create and scrape two snapshots: the read/check/insert sequence is non-atomic. Use a database-backed lock or atomic find-or-create/unique-claim flow so one request returns the row created by the other.</comment>
<file context>
@@ -41,6 +47,30 @@ export async function createSnapshot(params: {
+ // Hand back an identical capture taken moments ago rather than scraping the
+ // same albums twice. The month's snapshots are already loaded for the cap
+ // check, so this costs no extra query.
+ const reusable = findReusableSnapshot({
+ snapshots: monthSnapshots,
+ albumIds,
</file context>
There was a problem hiding this comment.
Correct, and deliberately not fixed here. Recording the reasoning rather than silently leaving it.
The read/check/insert sequence is not atomic, so two simultaneous identical requests can both miss and both scrape. What this PR targets is the sequential duplicate that is actually happening in production: the seeding valuation and the /setup/tasks first-task pre-run measure the same albums about three minutes apart (chat#1912 row 4 has the timings). A pre-check fully covers that, and now covers anything inside 60 minutes.
Closing the concurrent window properly needs a database-level guard, and the obvious shapes do not fit cheaply:
- a unique index cannot express "same
album_idsarray, same platforms, within a rolling time window" - a Postgres advisory lock keyed on the scope hash would work, but it is a different change with its own failure modes (lock lifetime across the workflow start, behaviour under connection pooling)
So the residual is: two requests landing in the same instant still both scrape. That is strictly better than today, where requests three minutes apart both scrape, and it is not the observed failure. Flagging it on the tracking issue as a follow-up rather than expanding this PR is the honest trade. If you would rather it land here, say so and I will do the advisory-lock version with its own tests.
| status: "success", | ||
| snapshot_id: reusable.id, | ||
| state: reusable.state, | ||
| album_count: reusable.album_count, |
There was a problem hiding this comment.
P2: The response payload's album_count field can be null on the reused path while it is always a number on the normal path. Consider falling back to reusable.album_ids?.length ?? albumIds.length instead of passing the nullable column through directly, so the caller always receives a consistent type regardless of the reuse decision.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/research/playcounts/createSnapshot.ts, line 68:
<comment>The response payload's `album_count` field can be `null` on the reused path while it is always a `number` on the normal path. Consider falling back to `reusable.album_ids?.length ?? albumIds.length` instead of passing the nullable column through directly, so the caller always receives a consistent type regardless of the reuse decision.</comment>
<file context>
@@ -41,6 +47,30 @@ export async function createSnapshot(params: {
+ status: "success",
+ snapshot_id: reusable.id,
+ state: reusable.state,
+ album_count: reusable.album_count,
+ estimated_cost_usd: 0,
+ reused: true,
</file context>
| album_count: reusable.album_count, | |
| album_count: reusable.album_count ?? reusable.album_ids?.length ?? albumIds.length, |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.
Re-trigger cubic
Review (@sweetmantech): the reuse window is 60 minutes, not 15. Nothing about a catalog's play counts changes inside an hour, so re-valuing it sooner is pure scraper spend. Review (coderabbit / cubic, both flagged): the candidate list was the monthly-cap query, whose lower bound is the start of the UTC month. In the first hour of a month the previous month's captures were invisible and an identical request re-scraped them. Widening the window to 60 minutes widened that hole, so the lookup now starts at whichever of monthStart and the reuse cutoff reaches further back, while the cap still counts only current-month rows via getMonthlySpendUsd. Review (coderabbit): createSnapshot was growing. Extracted buildReusedSnapshotResult and getMonthlySpendUsd. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Confidence score: 5/5
- In
lib/research/playcounts/createSnapshot.ts, the monthly-cap test fixture is missingcreated_at, so the new current-month filter excludes the mock row and leaves cap logic effectively unverified; this risks masking regressions in monthly limiting behavior—add a current-month timestamp to the fixture and a focused helper test for the filter path.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/research/playcounts/createSnapshot.ts">
<violation number="1" location="lib/research/playcounts/createSnapshot.ts:59">
P3: The existing monthly-cap test now fails because its mock row has no `created_at`, which this new calculation filters out. Include a current-month timestamp in that fixture (and add a direct helper test) so the cap behavior remains covered.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| }); | ||
| const spentUsd = monthSnapshots.reduce((sum, row) => sum + (row.estimated_cost_usd ?? 0), 0); | ||
|
|
||
| const spentUsd = getMonthlySpendUsd(snapshots, monthStart); |
There was a problem hiding this comment.
P3: The existing monthly-cap test now fails because its mock row has no created_at, which this new calculation filters out. Include a current-month timestamp in that fixture (and add a direct helper test) so the cap behavior remains covered.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/research/playcounts/createSnapshot.ts, line 59:
<comment>The existing monthly-cap test now fails because its mock row has no `created_at`, which this new calculation filters out. Include a current-month timestamp in that fixture (and add a direct helper test) so the cap behavior remains covered.</comment>
<file context>
@@ -38,39 +41,35 @@ export async function createSnapshot(params: {
});
- const spentUsd = monthSnapshots.reduce((sum, row) => sum + (row.estimated_cost_usd ?? 0), 0);
+
+ const spentUsd = getMonthlySpendUsd(snapshots, monthStart);
const estimatedCostUsd = Number((albumIds.length * COST_PER_ALBUM_USD).toFixed(4));
</file context>
…dpoint Found during preview verification: createSnapshot returns reused: true, but createMeasurementJob rebuilt the payload without it, so a handed-back capture was indistinguishable from a fresh one except by its zero cost. A caller polling for a new capture needs to know it got an existing one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preview verification — 2026-07-30Two preview builds, each resolved by sha:
API keys were minted on each preview via Results
Row 5 is the one that matters: three requests for the same albums produced exactly one A gap the preview caught that the unit tests could not
Caught on the Review findings
Cost noteThese runs created real captures. Total scraper spend across both preview accounts: $0.009 (3 albums at $0.003). Full suite 4291 passed / 791 files, |
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.
Re-trigger cubic
Closes row 4 of chat#1912.
Why
One uninterrupted pass through
/setupon a fresh account writes twoplaycount_snapshotsrows for the same 24 albums. Measured on prod 2026-07-30, account10c76dc5:006ca598…POST /api/valuationseeding, from the first artist add/setup/tasksfirst-task pre-runBoth did real work — 24
song_measurementsrows landed at 14:30:54 and again at 14:33:43 — so the scraper was billed twice for an identical capture three minutes apart.The second one comes from
buildFirstTaskPrompt, whose step 2 instructs the agent toPOST /api/research/measurement-jobswith the artist's album ids. That path creates a snapshot but never materializes a catalog, which is why the second row hascatalog = null.This also corrects the record on chat#1912. Ben Hanchett's 2026-07-27 pair (
19:50:51with a catalog,19:54:20withcatalog = null, 209s apart) has exactly this signature. It was read as proof that he re-ran the valuation manually. He may well have, but those two rows would have appeared anyway.What changed
createSnapshotnow looks for an identical recent capture before starting a new one, via the new purefindReusableSnapshot:queued,runningordone(neverfailed)On a hit it returns that snapshot with
estimated_cost_usd: 0andreused: trueinstead of inserting a row and starting the workflow. A still-in-flight capture is deliberately reusable: the caller polls for measurements either way, and starting a second scrape is the waste.The month's snapshots are already loaded for the monthly cost cap, so this costs no extra query.
Why here and not in the prompt
The prompt could be told to skip its capture during onboarding, but the duplicate is a property of the endpoint, not of one caller: any two callers asking for the same albums within minutes should not both scrape. Fixing it in
createSnapshotalso covers the retry case (an agent that re-posts after a timeout) which a prompt change would not.Tests
lib/research/playcounts/__tests__/findReusableSnapshot.test.ts— 10 cases, TDD red→green (RED: module not found). Covers reuse, album-order insensitivity, differing album sets, the window boundary, failed captures, in-flight captures, platform and schedule mismatches, newest-wins, and the empty case.Domain suites
lib/research lib/catalog lib/valuation: 417 passed / 93 files.tsc --noEmitclean.Verification
Preview verification against the issue's Works when script (fresh signup, one clean
/setuppass, wait 5 min, then the snapshot query expecting exactly one row with a non-nullcatalog) will be posted as a PR comment once the preview builds.🤖 Generated with Claude Code
Summary by cubic
Reuses a recent identical playcount snapshot (60-minute window) instead of starting a new one to prevent double scraping and charges. Fixes the duplicate captures during a single
/setuppass (chat#1912 row 4), including requests that cross a month boundary, and surfaces reuse to callers.Bug Fixes
createSnapshot, return a reusable snapshot when the same album set (order-insensitive), platforms, and schedule are requested within 60 minutes and the state isqueued/running/done; respond withestimated_cost_usd: 0andreused: true.getMonthlySpendUsd.createMeasurementJob, includereused: truein the response when handing back an existing capture so clients can distinguish reuse from a fresh run.Refactors
buildReusedSnapshotResultandgetMonthlySpendUsdfor clarity.Written for commit 2618d2f. Summary will update on new commits.
Summary by CodeRabbit