Skip to content

fix(bot): unlink dead Last.fm sessions on error 9 and notify once - #1946

Merged
LucasSantana-Dev merged 14 commits into
mainfrom
fix/lastfm-dead-session-handling
Aug 12, 2026
Merged

LucasSantana-Dev merged 14 commits into
mainfrom
fix/lastfm-dead-session-handling

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented Aug 3, 2026 •

Copy link
Copy Markdown
Owner

Why

Prod: 2,501 Last.fm "403 Invalid session key (error 9)" lines in 60d (~42/day), surfaced during the Lavalink re-evaluation. Root cause: the player scrobble path () only warn-logged the failure — the dead row stayed, so every track produced 2 failing API calls forever. (The external scrobbler already unlinked correctly.)

What

New shared () used by both scrobble paths, per the critic-reviewed ADR ():

  • DB-row dead key → unlink + one info log + one DM ("relink with /lastfm link"). DM guarded by an in-memory set — returns true on P2025, so it can't serve as the idempotency signal (double-DM race between updateNowPlaying + scrobble).
  • Env-fallback dead key (no DB row) → distinct config warning once per process; never unlink, never DM (avoids DM-spamming a user who never linked — the critic's critical finding).
  • Detection switched from substring to — with unlink attached, a WAF HTML 403 would otherwise nuke valid links.
  • Behavior change: player path now passes (matching the external scrobbler). Unlinked requesters' plays no longer scrobble to the env owner's account — that misattribution was latent and activated by unlinking.

Tests

  • New : 5 unit tests (env-key path, DM-once, DM-failure swallow, null client, no-id no-op)
  • updated to the new contract (3 tests rewritten)
  • 18/18 touched-suite tests + 99/99 lastfm-suite tests pass; monorepo type-check clean

Summary by cubic

Unlinks invalid Last.fm sessions (error 9) and sends one relink DM, stopping repeated 403s and preventing mis-attributed scrobbles. Uses a shared dead-session handler across the player and external scrobbler.

  • Bug Fixes

    • Shared handleDeadLastFmSession: atomic unlinkIfKeyMatches(discordId, sessionKey) with tri-state 'removed' | 'stale' | 'error'; compare failed key to stored key to protect fresh relinks; log + DM only on 'removed'.
    • Reliable detection via isLastFmInvalidSessionError; externalScrobbler now routes invalid-session errors through the shared handler.
    • DM once per session key via bounded LRU; reserve before send and release on failure; delivery is best-effort, at-most-once per key.
    • Env fallback: warn once about LASTFM_SESSION_KEY; never unlink or DM; DB lookup failures warn and skip. Player path fallback is requester-scoped (allowEnvFallback: requesterId === undefined) to prevent env-account misattribution.
  • Dependencies

    • Bumped undici to 7.29.0.
    • Audit fixes for fast-uri and ip-address.

Written for commit 391db89. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of invalid Last.fm sessions during scrobbling and now-playing updates.
    • Automatically removes expired linked sessions and sends at most one relinking message.
    • Prevents repeated notifications and safely handles messaging failures.
    • Protects newer relinked sessions from being removed by stale errors.
    • Avoids fallback account use for identified requesters and logs configuration issues without removing fallback settings.
  • Documentation
    • Added an architectural decision record documenting invalid-session behavior and trade-offs.

The player scrobble path only warn-logged invalid-session failures, so a
dead lastfm_links row produced 2 failing API calls per track forever
(~2.5k error lines/60d in prod for one dead key).

A shared deadSessionHandler now serves both scrobble paths:
- DB-row dead key: unlink + one info log + one guarded DM (in-memory set;
  unlink's P2025-true is not an idempotency signal)
- env-fallback dead key (no row): distinct config warning, never unlink,
  never DM
- detection via isLastFmInvalidSessionError instead of substring 403
- player path now passes allowEnvFallback: false, matching the external
  scrobbler, so unlinked requesters no longer scrobble to the env account

ADR: decisions/2026-08-03-lastfm-dead-session-handling.md
@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR centralizes invalid Last.fm session handling. It conditionally unlinks matching database sessions, sends deduplicated relinking DMs, protects environment fallback keys, restricts player fallback, and routes player and external scrobbling errors through the shared handler.

Changes

Last.fm dead-session handling

Layer / File(s) Summary
Conditional link cleanup
packages/shared/src/services/LastFmLinkService/index.ts, packages/bot/src/lastfm/deadSessionHandler.ts, packages/bot/src/lastfm/deadSessionHandler.spec.ts, packages/bot/src/lastfm/index.ts
Adds conditional unlinking, stale-key protection, environment-key warnings, deduplicated relinking DMs, guard reset support, and outcome coverage.
Player path integration
packages/bot/src/handlers/player/trackNowPlaying.ts, packages/bot/src/handlers/player/trackNowPlaying.spec.ts
Restricts environment fallback to requester-less playback and routes invalid-session errors to the shared handler.
External scrobbler integration
packages/bot/src/handlers/externalScrobbler.ts, packages/bot/src/handlers/externalScrobbler.spec.ts
Routes external scrobbling and now-playing invalid-session errors through the shared handler. Non-invalid-session errors remain logged.
Decision record and runtime support
decisions/2026-08-03-lastfm-dead-session-handling.md, package.json
Documents the handling rules and updates the pinned undici override.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PlayerOrExternalScrobbler
  participant handleDeadLastFmSession
  participant LastFmLinkService
  participant DiscordClient
  PlayerOrExternalScrobbler->>handleDeadLastFmSession: report invalid Last.fm session
  handleDeadLastFmSession->>LastFmLinkService: unlinkIfKeyMatches with Discord ID and failed session key
  LastFmLinkService-->>handleDeadLastFmSession: matching deletion result
  handleDeadLastFmSession->>DiscordClient: send at most one relinking DM
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: handling Last.fm error 9 by unlinking dead sessions and sending one notification.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/lastfm-dead-session-handling

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.

❤️ Share

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

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Failed to generate code suggestions for PR

@graphify-labs graphify-labs 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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This PR extracts Last.fm dead-session handling (invalid session key / error 9) into a new shared deadSessionHandler module and routes both the externalScrobbler and trackNowPlaying (main player) scrobble paths through it. Per the accompanying ADR, the intended behavior is to unlink dead lastfm_links rows, send a one-time DM prompting the user to relink (guarded by an in-memory set), skip unlink/DM when the failing key is the env key (logging a config warning instead), and disable env-fallback in the player path. The surface area covers the new ADR document, the new handler and its tests, and refactors to externalScrobbler.ts (removing its inline unlink logic in favor of the shared handler) and trackNowPlaying.ts, along with updates to the affected spec files and their mocks.

No blocking issues surfaced. 5 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 89 functions depend on the 70 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 89 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 84 function(s) in the blast radius were not formally verified this run

· 2 more finding(s) on lines outside this diff (see the check run).

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/bot/src/lastfm/deadSessionHandler.spec.ts (1)

49-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the DM-dedup test with a concurrent invocation.

This test calls handleDeadLastFmSession twice sequentially with await. The scenario this guard protects against, per the docstring in deadSessionHandler.ts, is updateNowPlaying and scrobble racing for one track — that is, two in-flight calls before either resolves. Add a Promise.all variant to directly exercise that interleaving.

♻️ Proposed additional test
     it('unlinks, logs, and DMs once when the dead key is a DB row', async () => {
         getByDiscordIdMock.mockResolvedValue({ discordId: 'user-1' })
         const { client, sendMock } = makeClient()

         await handleDeadLastFmSession('user-1', client, 'scrobble')
         await handleDeadLastFmSession('user-1', client, 'updateNowPlaying')

         expect(unlinkMock).toHaveBeenCalledTimes(2)
         expect(infoLogMock).toHaveBeenCalledWith(
             expect.objectContaining({
                 message: 'Removed invalid Last.fm session',
                 data: expect.objectContaining({ discordId: 'user-1' }),
             }),
         )
         expect(sendMock).toHaveBeenCalledTimes(1)
     })
+
+    it('DMs once when scrobble and updateNowPlaying race concurrently', async () => {
+        getByDiscordIdMock.mockResolvedValue({ discordId: 'user-1' })
+        const { client, sendMock } = makeClient()
+
+        await Promise.all([
+            handleDeadLastFmSession('user-1', client, 'scrobble'),
+            handleDeadLastFmSession('user-1', client, 'updateNowPlaying'),
+        ])
+
+        expect(sendMock).toHaveBeenCalledTimes(1)
+    })
🤖 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 `@packages/bot/src/lastfm/deadSessionHandler.spec.ts` around lines 49 - 64,
Update the test “unlinks, logs, and DMs once when the dead key is a DB row” to
invoke both handleDeadLastFmSession calls concurrently via Promise.all instead
of awaiting them sequentially, while preserving the existing assertions for
unlinking, logging, and a single DM.
🤖 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 `@packages/bot/src/handlers/player/trackNowPlaying.ts`:
- Around line 400-405: Update the session-key lookup in the track-now-playing
handler to preserve the environment Last.fm fallback when getLastFmRequesterId
returns undefined for autoplay or radio tracks. Allow getSessionKeyForUser to
use its default env-fallback behavior, while retaining the existing candidate
filtering that skips requesters without IDs.

---

Nitpick comments:
In `@packages/bot/src/lastfm/deadSessionHandler.spec.ts`:
- Around line 49-64: Update the test “unlinks, logs, and DMs once when the dead
key is a DB row” to invoke both handleDeadLastFmSession calls concurrently via
Promise.all instead of awaiting them sequentially, while preserving the existing
assertions for unlinking, logging, and a single DM.
🪄 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: b216344c-9e4c-4119-8da0-d9c9e745719a

📥 Commits

Reviewing files that changed from the base of the PR and between 03a0444 and 328472b.

📒 Files selected for processing (8)
  • decisions/2026-08-03-lastfm-dead-session-handling.md
  • packages/bot/src/handlers/externalScrobbler.spec.ts
  • packages/bot/src/handlers/externalScrobbler.ts
  • packages/bot/src/handlers/player/trackNowPlaying.spec.ts
  • packages/bot/src/handlers/player/trackNowPlaying.ts
  • packages/bot/src/lastfm/deadSessionHandler.spec.ts
  • packages/bot/src/lastfm/deadSessionHandler.ts
  • packages/bot/src/lastfm/index.ts

Comment thread packages/bot/src/handlers/player/trackNowPlaying.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 8 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread decisions/2026-08-03-lastfm-dead-session-handling.md Outdated
Comment thread packages/bot/src/lastfm/deadSessionHandler.ts Outdated
Comment thread packages/bot/src/lastfm/deadSessionHandler.ts Outdated
Comment thread packages/bot/src/lastfm/deadSessionHandler.ts
Comment thread packages/bot/src/handlers/player/trackNowPlaying.ts
Comment thread packages/bot/src/handlers/externalScrobbler.ts Outdated
- compare failed key with the stored row before unlinking — a stale
  error 9 must never delete a freshly relinked session
- only log/DM on removed === true; unlink failure keeps the guard
- DM guard keyed by session key, not user — a relinked-then-expired
  session still notifies
- lookup failure bails with a warning instead of reading as no-link
- player path env fallback is now requester-scoped: requester-less
  autoplay tracks keep env scrobbling; identified-but-unlinked
  requesters no longer misattribute to the env account
@github-actions

github-actions Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Warnings
⚠️

This PR appears to remove a feature or route (detected in commit message). Please fill in the Feature-removal sweep checklist in the PR template to ensure no orphan code (models, tests, types, imports) is left behind. See decisions/ for context.

Generated by 🚫 dangerJS against 391db89

@graphify-labs graphify-labs 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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This PR introduces a shared "dead Last.fm session" handling path for scrobbling. It adds a new deadSessionHandler module in packages/bot/src/lastfm/ and an accompanying ADR documenting the decision to unlink dead sessions and send a single DM on error 9, with requester-scoped env-key fallback rules. It refactors externalScrobbler.ts and trackNowPlaying.ts to route invalid-session errors through the new handleDeadLastFmSession function instead of calling lastFmLinkService.unlink directly. The existing and new specs are updated/added to mock and assert against the shared handler rather than the raw unlink service. Surface area spans the two scrobbler handlers, the new dead-session handler and its tests, related test mocks, and a decisions/ADR markdown file.

Worth a look

  • Non-atomic check-then-act on per-session DM guard Set allows double-DM under concurrency — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 94 functions depend on the 75 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 94 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 89 function(s) in the blast radius were not formally verified this run

· 2 more finding(s) on lines outside this diff (see the check run).

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts Outdated
Comment thread packages/bot/src/lastfm/deadSessionHandler.ts Outdated
Comment thread packages/bot/src/lastfm/deadSessionHandler.spec.ts
- unlinkIfKeyMatches(discordId, sessionKey) via deleteMany: closes the
  check-then-act window — a relink landing mid-cleanup changes the key,
  so the delete no-ops and the fresh link survives
- dm guard moved to a bounded LRU (max 500) so long-lived processes do
  not retain every expired session key forever
- spec: lookup-failure test now asserts exactly one warning

@graphify-labs graphify-labs 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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This pull request introduces a shared deadSessionHandler module in the bot package to centralize handling of dead/invalid Last.fm sessions (error 9), and routes both the externalScrobbler and trackNowPlaying scrobble paths through it. It also adjusts env-fallback behavior to be requester-scoped in the player path and adds an ADR documenting the rationale and alternatives considered. The changes touch the two scrobble handlers, their specs, the shared lastFmLinkService, and add new dead-session handler code plus tests. The spec updates replace direct unlink mocking with assertions against the new handleDeadLastFmSession entry point.

Worth a look

  • Non-atomic check-then-act on notifiedSessionKeys Set allows double-DM under concurrency — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 102 functions depend on the 83 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 102 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 97 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
decisions/2026-08-03-lastfm-dead-session-handling.md (1)

32-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

ADR says Set<string>, but the shipped guard is a bounded LRUCache.

Line 32 describes the DM guard as an in-memory Set<string>. The implementation in deadSessionHandler.ts uses new LRUCache<string, true>({ max: 500 }), not a plain Set. Update the wording so the ADR matches the actual bounded-memory behavior, since this document is the reference for future readers evaluating memory growth of the guard.

Proposed wording fix
-6. **DM guarded per session key** (in-memory `Set<string>`), not per user and not by the unlink result: updateNowPlaying/scrobble races can't double-DM, and a relinked-then-expired session (new key) still notifies.
+6. **DM guarded per session key** (bounded in-memory LRU cache, max 500 entries), not per user and not by the unlink result: updateNowPlaying/scrobble races can't double-DM, and a relinked-then-expired session (new key) still notifies.
🤖 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 `@decisions/2026-08-03-lastfm-dead-session-handling.md` at line 32, Update the
“DM guarded per session key” statement in the ADR to describe the shipped
bounded LRUCache<string, true> guard with its max-500 capacity instead of an
in-memory Set<string>. Preserve the existing per-session-key, race-prevention,
and relinked-session behavior details.
🤖 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 `@packages/bot/src/lastfm/deadSessionHandler.ts`:
- Around line 81-91: Update the failure branch in deadSessionHandler around
unlinkIfKeyMatches so a false result is logged at a non-error level, avoiding
false-positive or duplicate error reporting because database failures are
already logged internally. Preserve the early return and update the existing
deadSessionHandler test that currently expects an error log.

---

Nitpick comments:
In `@decisions/2026-08-03-lastfm-dead-session-handling.md`:
- Line 32: Update the “DM guarded per session key” statement in the ADR to
describe the shipped bounded LRUCache<string, true> guard with its max-500
capacity instead of an in-memory Set<string>. Preserve the existing
per-session-key, race-prevention, and relinked-session behavior details.
🪄 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: 8fcd595a-2aca-489f-a0be-a99f49b68fe8

📥 Commits

Reviewing files that changed from the base of the PR and between 328472b and 52e3c81.

📒 Files selected for processing (7)
  • decisions/2026-08-03-lastfm-dead-session-handling.md
  • packages/bot/src/handlers/externalScrobbler.spec.ts
  • packages/bot/src/handlers/externalScrobbler.ts
  • packages/bot/src/handlers/player/trackNowPlaying.ts
  • packages/bot/src/lastfm/deadSessionHandler.spec.ts
  • packages/bot/src/lastfm/deadSessionHandler.ts
  • packages/shared/src/services/LastFmLinkService/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/bot/src/handlers/externalScrobbler.ts
  • packages/bot/src/handlers/externalScrobbler.spec.ts
  • packages/bot/src/handlers/player/trackNowPlaying.ts

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts Outdated
Comment thread packages/bot/src/lastfm/deadSessionHandler.ts
Comment thread packages/shared/src/services/LastFmLinkService/index.ts
New production advisories since the audit gate last ran green:
- undici (5 GHSAs, fixed in 7.29.0) — override was pinned at 7.28.0
- fast-uri GHSA-7p8r-x3mc-p8w7 — via npm audit fix
- ip-address (3 GHSAs, SSRF/trust-boundary) — via npm audit fix
@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Aug 3, 2026

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 2 files (changes from recent commits).

Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.

Re-trigger cubic

@graphify-labs graphify-labs 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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This PR introduces a Last.fm dead-session handling feature for the bot. It adds a new Architecture Decision Record (decisions/2026-08-03-lastfm-dead-session-handling.md) documenting the rationale, and appears to add a shared dead-session handler module (packages/bot/src/lastfm/deadSessionHandler.ts) plus associated tests, with changes touching the trackNowPlaying player path and externalScrobbler handlers to route through it. The pull request also updates numerous package-lock.json dependency entries (reclassifying several from devOptional to dev, bumping versions like fast-uri and ip-address), and adjusts various package.json scripts, overrides, and devDependencies. The surface area spans documentation, bot Last.fm/scrobbler logic, and dependency/tooling configuration.

Worth a look

  • In-memory DM guard Set has non-atomic check-then-add allowing double-DM race — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 271 functions depend on the 252 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 271 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 266 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts
- unlinkIfKeyMatches returns 'removed' | 'stale' | 'error': a racing
  cleanup is an expected no-op, not a failed removal; the service logs
  real database failures itself
- dm dedup recorded only after a successful send, so a transient dm
  failure no longer consumes the one notification for that session key
- service-level tests for matching deletion, stale no-op, db rejection

@graphify-labs graphify-labs 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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This PR introduces a new architecture decision record (decisions/2026-08-03-lastfm-dead-session-handling.md) documenting the plan to add a shared "dead session handler" for Last.fm error-9 (invalid session key) cases across both scrobble paths, along with related decisions about unlink behavior, DM notifications, and env-fallback scoping. The changed symbols suggest accompanying implementation and test changes in the bot's externalScrobbler and trackNowPlaying handlers plus a new deadSessionHandler module and its specs. The diff also includes routine package-lock.json dependency metadata updates (e.g. dev/devOptional flag changes and minor version bumps) and package.json metadata edits.

Worth a look

  • In-memory DM guard Set may not be concurrency-safe under async races — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 277 functions depend on the 258 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 277 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 272 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts Outdated
Comment thread packages/bot/src/lastfm/deadSessionHandler.spec.ts
concurrent handlers could double-send while the reservation was only
recorded after the async fetch/send completed; a transient failure could
also suppress the notification forever. reserve before the await, delete
the reservation in the catch. spec now models the real removed-then-stale
race.

@graphify-labs graphify-labs 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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This PR adds a new architecture decision record (decisions/2026-08-03-lastfm-dead-session-handling.md) documenting a plan to handle Last.fm "dead session" errors by unlinking dead session keys and sending a one-time DM to affected users, with env-fallback and double-DM guards. Based on the changed symbols, it appears to introduce a shared deadSessionHandler module in packages/bot, wire it into the externalScrobbler and trackNowPlaying player paths, and add related helpers/tests (e.g., isLastFmInvalidSessionError, LastFmLinkService usage, and various spec mocks). The package-lock.json diff also reshuffles dependency metadata (several devOptional → dev flags and a few version bumps like fast-uri and ip-address). The reviewable surface spans the bot's scrobbling/Last.fm session logic, shared services, associated test files, and dependency lockfile changes.

No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 277 functions depend on the 258 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 277 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 272 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts
cubic-dev-ai flagged that a transient DM-send failure after a
successful unlink appears to permanently suppress the relink
notification. Verified: the guard-release on failure is a defensive
no-op under the current call graph, not a retry path, since the
lastfm_links row is already deleted by that point and no later call
for that session key can re-enter this function. No behavior change;
corrects the ADR and code comment to state what's actually achievable
instead of implying an active retry that can't fire.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/shared/src/services/LastFmLinkService/index.ts (1)

125-152: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make lookup failures distinguishable from an absent link.

getByDiscordId catches database failures and returns null at Line 25-31. The supplied dead-session handler therefore cannot enter its lookup-error catch. If envFallbackUsed is true, a database outage enters the !row branch and can emit an invalid-environment-key warning.

Propagate the lookup failure, or add a distinct lookup result for this handler. Preserve null only for an actual missing row.

🤖 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 `@packages/shared/src/services/LastFmLinkService/index.ts` around lines 125 -
152, Update getByDiscordId so database lookup failures propagate to the
dead-session handler instead of being converted to null; reserve null
exclusively for an absent link. Ensure the handler’s existing lookup-error catch
runs on database outages, preventing the !row/envFallbackUsed warning path from
treating failures as missing rows.
🤖 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.

Outside diff comments:
In `@packages/shared/src/services/LastFmLinkService/index.ts`:
- Around line 125-152: Update getByDiscordId so database lookup failures
propagate to the dead-session handler instead of being converted to null;
reserve null exclusively for an absent link. Ensure the handler’s existing
lookup-error catch runs on database outages, preventing the !row/envFallbackUsed
warning path from treating failures as missing rows.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2260c2b2-aaba-4e7a-a341-84be069fd1aa

📥 Commits

Reviewing files that changed from the base of the PR and between 52e3c81 and 636a7a6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • decisions/2026-08-03-lastfm-dead-session-handling.md
  • package.json
  • packages/bot/src/lastfm/deadSessionHandler.spec.ts
  • packages/bot/src/lastfm/deadSessionHandler.ts
  • packages/shared/src/services/LastFmLinkService/index.spec.ts
  • packages/shared/src/services/LastFmLinkService/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/bot/src/lastfm/deadSessionHandler.spec.ts
  • decisions/2026-08-03-lastfm-dead-session-handling.md
  • packages/bot/src/lastfm/deadSessionHandler.ts

cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes Aug 5, 2026

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 2 files (changes from recent commits).

Auto-approved: Focused bug fix: unlinks dead Last.fm sessions on error 9 with atomic conditional delete, DM once per key, and env-fallback scrobble scoping. All behavior changes are visible in the diff and confined to the described incorrect behavior.

Re-trigger cubic

@graphify-labs graphify-labs 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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This pull request adds a new architecture decision record (decisions/2026-08-03-lastfm-dead-session-handling.md) documenting an approach to handling dead Last.fm sessions, and appears to introduce a shared dead-session handler used by the bot's scrobble paths (external scrobbler and player now-playing). The symbol list also references related handler logic, session-key lookup/unlink behavior, DM notification guarding, and associated test mocks/specs, along with various package-lock.json dependency metadata changes (e.g. dev/devOptional reclassifications and version bumps). Reviewers should focus on the new/changed handler code, its call sites in the scrobble flows, and the accompanying tests.

No blocking issues surfaced.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 280 functions depend on the 258 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 280 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 274 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts
…on-handling

# Conflicts:
#	package-lock.json
#	package.json
#	packages/bot/src/lastfm/index.ts
@LucasSantana-Dev
LucasSantana-Dev enabled auto-merge (squash) August 11, 2026 23:19
@github-actions github-actions Bot removed the dependencies Pull requests that update a dependency file label Aug 11, 2026

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.


Graphify review — findings

This PR introduces centralized handling for dead Last.fm sessions (invalid session key / error 9). It adds a new shared deadSessionHandler in the bot's lastfm module and routes both the externalScrobbler and trackNowPlaying scrobble paths through it, replacing the previous inline unlink logic. It also adds an ADR documenting the decision, updates the lastFmLinkService (including new unlinkIfKeyMatches/getSessionKey surface area), and rewrites the associated spec files to mock and assert against the new handler. The reviewable surface spans the bot handlers (externalScrobbler, trackNowPlaying and its state helpers), the new deadSessionHandler and its tests, lastfm/index exports, and the shared lastFmLinkService implementation plus specs.

Worth a look

  • In-memory per-session DM guard Set is unguarded shared state under concurrent scrobble/nowPlaying paths — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • lastFmLinkService now exposes unlinkIfKeyMatches / getSessionKey with allowEnvFallback default true — packages/shared/src/services/lastfmLinkService/index.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 111 functions depend on the 89 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 111 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 105 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.


Graphify review — findings

This pull request adds a shared Last.fm "dead session" handler (packages/bot/src/lastfm/deadSessionHandler.ts) and routes both scrobble paths—externalScrobbler.ts and the player path in trackNowPlaying.ts—through it instead of handling invalid-session errors inline. It also introduces a getByDiscordId/session-key lookup on lastFmLinkService, adjusts the env-fallback behavior in the player path to be requester-scoped, and updates the associated unit tests to assert calls into the new handler rather than direct unlink calls. An accompanying ADR (decisions/2026-08-03-...) documents the rationale, alternatives, and revisit conditions. The surface area spans the two handler files and their spec files, the new dead-session handler and its spec, the shared lastFmLinkService and its spec, and the new decision record.

Worth a look

  • Non-atomic check-then-act on per-session DM guard Set allows double-DM under concurrency — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 111 functions depend on the 89 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 111 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 105 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.


Graphify review — findings

This PR introduces a shared "dead Last.fm session" handler and routes both scrobble paths (externalScrobbler and the player's trackNowPlaying) through it, replacing the previous inline unlink logic in externalScrobbler. It also adds an ADR documenting the decision (unlink dead sessions on error 9, send a best-effort DM, guard notifications per session key, and scope env-key fallback to requester-less tracks) and a new unlinkIfKeyMatches/getByDiscordId surface on lastFmLinkService. The changes span the two handlers, the new deadSessionHandler, the shared link service, and their accompanying spec files. The surface area is: bot handlers (external scrobbler, track-now-playing), a new bot lastfm/deadSessionHandler module, the shared lastFmLinkService, associated test mocks/specs, and one new decision document.

Worth a look

  • Non-atomic check-then-act in per-session DM guard allows double-DM under concurrent scrobble paths — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 111 functions depend on the 89 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 111 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 105 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.


Graphify review — findings

This PR introduces a shared Last.fm "dead session" handler (packages/bot/src/lastfm/deadSessionHandler.ts) and routes both scrobble paths—externalScrobbler and the main player path (trackNowPlaying)—through it when an invalid-session (error 9) is detected, replacing the previous direct lastFmLinkService.unlink call in externalScrobbler. It also adds a unlinkIfKeyMatches method to the shared lastFmLinkService and adjusts env-fallback behavior in the player path to be requester-scoped. An accompanying ADR document (decisions/2026-08-03-lastfm-dead-session-handling.md) records the rationale, and the associated spec files are updated to mock/assert against the new handler instead of the old unlink flow. Surface area: bot handlers (externalScrobbler, trackNowPlaying), a new bot lastfm module, shared lastFmLinkService, plus a new decision doc and multiple test files.

Worth a look

  • Non-atomic check-then-act on per-session DM guard Set allows double-DM under concurrent scrobble+nowPlaying — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 111 functions depend on the 89 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 111 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 105 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.


Graphify review — findings

This PR adds a Last.fm "dead session" handling path. It introduces a shared deadSessionHandler module in the bot package that both the external scrobbler and the trackNowPlaying player path route to when a Last.fm invalid-session (error 9) is detected, replacing the previous inline unlink logic in externalScrobbler. It also touches the shared lastFmLinkService (adding key-match-aware unlink helpers), the player now-playing handler, related test mocks/specs, and includes a new ADR documenting the decision. The surface area covers the bot's two scrobbling paths, the shared link service, their spec files, and a decision record; changes center on where and how invalid-session errors trigger unlinking and DM notification.

Worth a look

  • envFallbackUsed hardcoded false in player path where env fallback is requester-scoped — packages/bot/src/handlers/player/trackNowPlaying.ts · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • In-memory DM guard Set is non-atomic check-then-act, allowing double-DM under concurrency — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 111 functions depend on the 89 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 111 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 105 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.


Graphify review — findings

This pull request adds a shared deadSessionHandler module for Last.fm dead-session ("error 9" / invalid session key) handling and routes both the externalScrobbler and trackNowPlaying scrobble paths through it, replacing the previous direct lastFmLinkService.unlink call in externalScrobbler. The handler introduces logic around checking existing links, distinguishing env-fallback keys, guarding DM notifications per session key, and scoping env fallback to requester-less tracks. It also adds a new unlinkIfKeyMatches method area in the shared lastFmLinkService, updates the associated specs, and documents the reasoning in a new ADR (decisions/2026-08-03-lastfm-dead-session-handling.md). Surface area touched: bot scrobbler/player handlers, the shared Last.fm link service, related test files, and decision documentation.

Worth a look

  • envFallbackUsed hardcoded false in player path breaks env-key detection — packages/bot/src/handlers/player/trackNowPlaying.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Non-atomic check-then-act on notifiedSessionKeys Set allows double-DM under concurrent scrobble/nowPlaying — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 111 functions depend on the 89 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 111 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 105 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts
@sonarqubecloud

Copy link
Copy Markdown

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.


Graphify review — findings

This pull request adds a shared deadSessionHandler module in the bot package that centralizes handling of expired/invalid Last.fm session keys ("error 9"), and routes both the external scrobbler path and the trackNowPlaying player path through it instead of handling unlinking inline. It introduces a new unlinkIfKeyMatches method on the shared lastFmLinkService and changes the player path's env-fallback behavior to be requester-scoped. An ADR documenting the decision and its considered alternatives is also added, and the affected test suites are updated to mock and assert against the new handler. The touched surface spans packages/bot/src/lastfm/deadSessionHandler.ts, packages/bot/src/handlers/externalScrobbler.ts, packages/bot/src/handlers/player/trackNowPlaying.ts, packages/shared/src/services/lastFmLinkService, their spec files, and a new decisions document.

Worth a look

  • Non-atomic check-then-act on notifiedSessionKeys guard allows double-DM under concurrent scrobble/nowplaying — packages/bot/src/lastfm/deadSessionHandler.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 111 functions depend on the 89 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: scrobbleCurrentTrackIfLastFm() — 3 callers, 8 callees
  • new: handleDeadLastFmSession() — 6 callers, 3 callees
  • worse: updateLastFmNowPlaying() — 2 callers, 7 callees

Verification — 111 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 105 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

Comment thread packages/bot/src/lastfm/deadSessionHandler.ts
@LucasSantana-Dev
LucasSantana-Dev merged commit d22b8bd into main Aug 12, 2026
47 checks passed
@LucasSantana-Dev
LucasSantana-Dev deleted the fix/lastfm-dead-session-handling branch August 12, 2026 01:15
LucasSantana-Dev added a commit that referenced this pull request Aug 12, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>2.39.3</summary>

##
[2.39.3](v2.39.2...v2.39.3)
(2026-08-12)


### Bug Fixes

* **bot:** expose degraded-extractor signal and honest play error
([#1999](#1999))
([8348bd6](8348bd6))
* **bot:** unlink dead Last.fm sessions on error 9 and notify once
([#1946](#1946))
([d22b8bd](d22b8bd))
* **ci:** add a second retry for docker-build
([#2007](#2007))
([9347c55](9347c55))
* **ci:** extract node_modules before per-workspace build steps run
([#2006](#2006))
([ae577d1](ae577d1))
* **ci:** free disk space before Docker builds to prevent BuildKit GC
race ([#2003](#2003))
([5bcd388](5bcd388))
* **ci:** isolate docker cache scope by branch
([#2012](#2012))
([5d25ec8](5d25ec8))
* **ci:** pin node:24-alpine base image by digest
([#2004](#2004))
([f154bfe](f154bfe))
* **ci:** skip gha cache-from on docker-build retries
([#2008](#2008))
([89dbb90](89dbb90))
* **ci:** split deps-production per production target
([#2005](#2005))
([43aacce](43aacce))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
This was referenced Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant