You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #583. Depends on #584, #585, #593 and #599. Complements #590, which covers only Twitch channel points and its claim-only alarm coordination.
Problem
Claiming that is not channel points has no owner in the #583 plan:
runDropClaims(platform): the manual-watch drop-claim job for both Twitch and Kick (TWITCH_DROP_CLAIMS_ALARM_NAME, KICK_DROP_CLAIMS_ALARM_NAME), scheduled by reconcileManualWatchClaimAlarms and cancelled by clearManualWatchClaimAlarmsBestEffort, with dropClaimOperations and waitingClaimRewardIds.
runClaimHandoff / claimHandoffs: the post-claim handoff loop to the successor reward.
claimRewardNow: the popup's manual claim message.
In-tick claiming: runSchedulerTick calls claimReadyRewards → adapter.isClaimReady / adapter.claimReward while the platform lock is held. refactor(core): split scheduler decisions from side effects #599 moves this out of the lock into an interim executor handler.
Without an owner, #590 and #588 each have to reach into this shared code, and #591 cannot become a thin facade.
Scope
Take over the reward-claim effect from refactor(core): split scheduler decisions from side effects #599's interim executor handler: ClaimService registers the handler for that effect type and the interim one is deleted. Claims run outside every lock, and successful claims are recorded through the existing preserveClaimedRewards merge.
There is no local claim journal: the provider's inventory is the source of truth. Recording a claim intent before sending would not help, because after a crash the engine still could not tell whether the request reached the provider.
Within one process, the in-flight set guarantees at most one claim request per reward across overlapping ticks, manual claims, jobs and the handoff loop.
Across a restart, the process may have stopped after the provider accepted a claim and before state.json or storage.local was saved. On restart the reward is still unclaimed locally, and nothing is published for it yet. The next discovery re-reads provider inventory before any claim runs. A reward the provider reports as claimed is no longer claim-ready, so no request is sent, and it is recorded as claimed without publishing activity.
If the reward is sent again anyway (inventory still stale), what happens depends on the platform:
Twitch (verified): Twitch answers DROP_INSTANCE_ALREADY_CLAIMED, which runClaim already treats as success. The claim publishes its activity and notification once, from the new process. A claim accepted just before a crash therefore publishes either once or not at all, never twice from the same process.
Kick (unverified): Kick's response to a duplicate claim has not been observed. Keep today's handling unchanged. Since fix(kick): keep definitive client errors off the page-tab fallback #606 (v1.14.0), a definitive 4xx from drops/claim is rethrown from the background transport without a page-tab retry. claimReward then classifies it as link_required only when campaign.accountLinked === false, and otherwise rethrows it into the claim failure path. Don't assume a duplicate means success or failure. Record the observed response in docs/architecture.md once seen. Any change to how it is classified is a separate behavior-change PR, not part of this refactor.
Document this model in docs/architecture.md.
Acceptance criteria
A claim that actually succeeded is recorded, and its activity and notification are published exactly once within the process, even if the decision that triggered it went stale. A claim cancelled before it was sent records and publishes nothing.
Within one process, each reward gets at most one claim request across overlapping ticks, manual claims, jobs and the handoff loop.
Restart follows the failure model above, with tests for a crash between provider acceptance and the save. The next claim decision runs only after inventory is re-read, and a reward the provider reports as claimed sends no request. On Twitch, a DROP_INSTANCE_ALREADY_CLAIMED response counts as success and does not back off the platform. On Kick, duplicate-response handling is identical to today's post-fix(kick): keep definitive client errors off the page-tab fallback #606 path (characterized in test(core): capture background controller ownership and concurrency contracts #584, not redefined here), and no claim failure opens a page tab.
Part of #583. Depends on #584, #585, #593 and #599. Complements #590, which covers only Twitch channel points and its claim-only alarm coordination.
Problem
Claiming that is not channel points has no owner in the #583 plan:
runDropClaims(platform): the manual-watch drop-claim job for both Twitch and Kick (TWITCH_DROP_CLAIMS_ALARM_NAME,KICK_DROP_CLAIMS_ALARM_NAME), scheduled byreconcileManualWatchClaimAlarmsand cancelled byclearManualWatchClaimAlarmsBestEffort, withdropClaimOperationsandwaitingClaimRewardIds.runClaimHandoff/claimHandoffs: the post-claim handoff loop to the successor reward.claimRewardNow: the popup's manual claim message.runSchedulerTickcallsclaimReadyRewards→adapter.isClaimReady/adapter.claimRewardwhile the platform lock is held. refactor(core): split scheduler decisions from side effects #599 moves this out of the lock into an interim executor handler.reconcileManualWatchClaimAlarms, although refactor(kick): isolate challenge and page-context lifecycle policy #588 owns its policy.Without an owner, #590 and #588 each have to reach into this shared code, and #591 cannot become a thin facade.
Scope
ClaimServiceregisters the handler for that effect type and the interim one is deleted. Claims run outside every lock, and successful claims are recorded through the existingpreserveClaimedRewardsmerge.ClaimServicethat owns drop-claim jobs (registered through the refactor(core): define host ports and explicit runtime capabilities for the extension and CLI #593 scheduler port), in-flight operations, waiting reward IDs, the post-claim handoff, and manual claim requests. It uses the shared reward rules (claimReadyRewards,reconcileCampaignAfterClaims) and the refactor(core): extract state commits and event publication #585 transaction.rankCampaigns(feat(popup): rebuild the popup as a sidebar workspace with one campaign ranking #571): a claim can hand off to a higher-ranked campaign, not only the next reward in the same one. Keep that as is.Claim failure model
There is no local claim journal: the provider's inventory is the source of truth. Recording a claim intent before sending would not help, because after a crash the engine still could not tell whether the request reached the provider.
state.jsonorstorage.localwas saved. On restart the reward is still unclaimed locally, and nothing is published for it yet. The next discovery re-reads provider inventory before any claim runs. A reward the provider reports as claimed is no longer claim-ready, so no request is sent, and it is recorded as claimed without publishing activity.DROP_INSTANCE_ALREADY_CLAIMED, whichrunClaimalready treats as success. The claim publishes its activity and notification once, from the new process. A claim accepted just before a crash therefore publishes either once or not at all, never twice from the same process.drops/claimis rethrown from the background transport without a page-tab retry.claimRewardthen classifies it aslink_requiredonly whencampaign.accountLinked === false, and otherwise rethrows it into the claim failure path. Don't assume a duplicate means success or failure. Record the observed response indocs/architecture.mdonce seen. Any change to how it is classified is a separatebehavior-changePR, not part of this refactor.Document this model in
docs/architecture.md.Acceptance criteria
DROP_INSTANCE_ALREADY_CLAIMEDresponse counts as success and does not back off the platform. On Kick, duplicate-response handling is identical to today's post-fix(kick): keep definitive client errors off the page-tab fallback #606 path (characterized in test(core): capture background controller ownership and concurrency contracts #584, not redefined here), and no claim failure opens a page tab.ClaimService's. The interim refactor(core): split scheduler decisions from side effects #599 handler and this issue's allowlist entries are gone.claimRewardNow) keeps its current eligibility and messaging.pnpm test,pnpm typecheckand the CLI tests pass.Out of scope
Changing claim payloads, reward eligibility, ranking or claim ordering. A persistent claim journal. Reclassifying Kick's duplicate-claim response.
Rebased on v1.14.0 as released: Kick's current claim path includes #606, and handoff follows #571's ranking. Generated by Claude Code