Skip to content

fix(cloud): settle swept app-chat holds markup-inclusively — sweep refunded the creator markup to the org (#11592 residual) - #11686

Merged
lalalune merged 2 commits into
developfrom
fix/11592-app-chat-sweep-markup
Jul 2, 2026
Merged

fix(cloud): settle swept app-chat holds markup-inclusively — sweep refunded the creator markup to the org (#11592 residual)#11686
lalalune merged 2 commits into
developfrom
fix/11592-app-chat-sweep-markup

Conversation

@NubsCarson

Copy link
Copy Markdown
Member

Summary

Refs #11592 (auto-closed by #11493's merge — the core "stranded app-chat holds are never swept" gap IS fixed there and verified below). This PR fixes the residual money bug in the merged sweep's app-chat settle math plus real-PGlite proof that a stranded hold is swept exactly once (no double-refund).

Verified already fixed on develop (no duplication here)

The residual bug (fixed here): unit mismatch → platform eats the creator markup

The sweep settles a stale hold to metadata.estimated_cost. For generic reservations that's org-charge units — correct. For app-chat holds it's the unbuffered BASE cost (no markup), while the row amount is the org charge: buffered base plus creator markup (computeInferenceCharge via appCreditsService.deductCredits) — and the creator's earnings are recorded at deduct time.

Concretely ($1.00 base estimate, 1.5× buffer, 20% markup → $1.80 hold, $0.30 creator earnings):

org refund org nets creator keeps platform
pre-fix sweep $0.80 $1.00 (no markup at all) $0.30 eats the full markup
this PR $0.60 $1.20 = estimate × (1+markup) $0.30 eats only the buffer-delta share ($0.10)
normal settle at estimate $0.60 $1.20 $0.20 $0

Fix: for app_chat_reservation rows, settle the org to amount × (estimated_cost / reserved_amount) — exactly what a normal settle at the estimated cost charges. Markup-0 (non-monetized) holds collapse to the previous math byte-for-byte; holds without a usable base pair settle exact-cost; result clamped to the held amount so corrupt metadata can never become a surprise overage charge. The pre-existing test masked the bug by modeling reserved_amount == row amount (markup = 0).

Red / green (real PGlite, no mocks)

Pre-fix (develop credits.ts + the new test):

Expected: 8.8
Received: 9        // org over-refunded $0.80, pocketing the markup
(fail) sweep settles monetized app-chat holds markup-inclusively — exactly one refund…

Post-fix: bun test --isolate --conditions eliza-source packages/cloud/shared/src/lib/services/__tests__/credits-reconcile.test.tsRan 30 tests, 0 fail.

New tests assert the exactly-once contract: single recon:<txid>:refund row (0.600000), settled_at claimed, re-sweep scans 0, late full-refund settle after the sweep is a no-op (refund count stays 1) — plus missing-base-pair holds settle exact-cost instead of guessing.

Biome: 2 files, no fixes. Typecheck: no errors for touched files. Evidence: .github/issue-evidence/11592-app-chat-sweep-markup.md.

Remaining scope (explicit, needs [cloud-money])

The sweep still doesn't reverse the creator-earnings share of the buffer delta ((reservedBase − estimatedBase) × markup, $0.10 above): that requires appCreditsService earnings machinery, which creditsService can't import (dependency direction — app-credits already imports credits). Platform-absorbed, bounded, rare-path (mid-settle-throw strands only). If wanted, the cron route (which can import both services) could route app-chat rows through appCreditsService.reconcileCredits instead — a design call for the money lane, not snuck in here.

Money path → requesting [cloud-money] / @lalalune review. Tracked in #8434.

— [cloud-security]

The stranded-reservation sweep (#11493) settles a stale hold to
metadata.estimated_cost. For app_chat_reservation_v1 holds that number is
the UNBUFFERED BASE cost while the row amount is the org charge (buffered
base + creator markup, computeInferenceCharge) — a unit mismatch: sweeping
a stranded monetized app-chat hold refunded the org the creator markup too,
while the creator's earnings recorded at deduct time stay put, so the
platform ate the full markup on every swept monetized hold.

Scale the org charge by estimated/reserved instead, so the org nets exactly
what a normal settle at the estimated cost charges (base x (1 + markup)).
Non-monetized holds are unchanged (markup 0 collapses to the old math).
Holds without a usable base pair settle exact-cost; result is clamped to
the held amount so corrupt metadata can never become an overage charge.

Real-PGlite tests: monetized-shape hold swept exactly once (refund 0.60 on
a 1.80 hold with 1.00 base estimate + 20% markup; re-sweep scans 0; late
settle no-op) + missing-base-pair exact-cost noop.

Refs #11592

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2704e61d-7e9d-4727-ad79-57bf8874cd5f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/11592-app-chat-sweep-markup

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.

@NubsCarson

Copy link
Copy Markdown
Member Author

Money-path — needs [cloud-money]/@lalalune review. This fixes a real residual over-refund in the #11493 app-chat sweeper (which correctly closed #11592's core gap): swept monetized holds settled to the unbuffered base estimated_cost while the row amount is the org charge (buffered base + creator markup), so a stranded monetized hold over-refunded the org by the markup. Proven: RED pre-fix (org +$0.80 vs expected +$0.60), GREEN post-fix ($0.60 exactly once — single recon::refund row, re-sweep scans 0, late settle no-op); credits-reconcile 30/0, the 4 #11493 suites 68/0. Authored via Fable-5 ultracode. — [cloud-frontdoor]

@NubsCarson

Copy link
Copy Markdown
Member Author

[cloud-security] — coordination (both NubsCarson lane, avoid two competing PRs on credits.ts): #11696 is a superset of this PR. This PR correctly fixes the markup over-refund (creator markup refunded to the org). #11696 fixes that same markup issue AND two more from the pre-merge #11493 review that this one doesn't cover:

  1. the cashable double-refund — the sweep's 20-min grace is shorter than the provider retry-ladder in-flight window (~26min–2h), and the app-chat settle lane's reconcile-refund:<holdId> key is disjoint from the sweep's recon:<holdId>:refund, so a still-in-flight hold gets swept then settled → refunded twice;
  2. a creator-earnings double-apply that persists even after the org-credit leg dedupes (different request contexts).
    fix(cloud): app-chat holds sweep through the settle lane — stop the #11493 double-refund + markup over-refund (#11683) #11696 routes app-chat sweep rows through appCreditsService.reconcileCredits (shared dedup key + correct markup math, subsuming this PR's fix) + derives the grace from the retry ladder, with real-PGlite tests + a negative control. Tracking issue cloud/money: #11493 sweep double-refunds in-flight app-chat holds (disjoint dedup key + grace < retry window) + markup over-refund #11683. Suggest consolidating onto fix(cloud): app-chat holds sweep through the settle lane — stop the #11493 double-refund + markup over-refund (#11683) #11696 and closing this (or, maintainer's call, cherry-picking fix(cloud): app-chat holds sweep through the settle lane — stop the #11493 double-refund + markup over-refund (#11683) #11696's defect-1/3 changes here) so we don't conflict on credits.ts. Your markup fix + evidence file are correct — just subsumed. cc @lalalune [cloud-security]

@lalalune
lalalune merged commit cdd87af into develop Jul 2, 2026
39 of 45 checks passed
@lalalune
lalalune deleted the fix/11592-app-chat-sweep-markup branch July 2, 2026 23:57
NubsCarson added a commit that referenced this pull request Jul 3, 2026
…nblock ALL deploys (#11847)

Every develop deploy since ~03:46 UTC failed at the Deploy API Worker step:
  No matching export in "src/stubs/elizaos-core.ts" for import
  "runWithTrajectoryPurpose"  (shared/src/email-classification/email-classifier.ts:20)

email-classifier (@elizaos/shared, pulled into the Worker bundle transitively)
imports runWithTrajectoryPurpose from @elizaos/core, which the Worker aliases
to this stub — but the stub only had runWithTrajectoryContext, not the newer
runWithTrajectoryPurpose (added to core in trajectory-context.ts). So the
esbuild bundle failed and NO deploy could ship — blocking the develop→main
promote that carries the merged money fixes (#11810 withdraw, #11817
book-influencer, #11686 sweep) to prod.

Fix: add the Worker-safe stub mirroring runWithTrajectoryContext — no trajectory
context manager exists in the Worker bundle (it lives on the agent sidecar and
this path is never invoked on a Worker route), so just run the fn.

Verified: `wrangler deploy --env production --dry-run` now builds clean
(15.8 MB bundle, no missing-export error).
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

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.

2 participants