fix(entitlement): preserve grant usage across balance snapshots - #4846
fix(entitlement): preserve grant usage across balance snapshots#4846GAlexIHU wants to merge 8 commits into
Conversation
📝 WalkthroughWalkthroughThe PR adds cumulative usage snapshots to credit balances, persists them alongside legacy usage data, updates credit history and rollover accounting, and changes entitlement balance calculations to use the new snapshot fields. ChangesCredit snapshot contract and persistence
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 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 |
e365ec2 to
59058ea
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
openmeter/credit/engine/history.go (1)
104-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the rollover-ordering comparator into a named helper.
The
sort.SliceStablecomparator at lines 123-129 encodes a real domain rule: rollover segments must sort before usage segments at the same timestamp. Move this into a named helper, for examplesegmentsLessForSortorrolloverPrecedesUsageAtSameTime. This keeps the domain rule visible outside the closure and makes it independently testable.The rest of
NewGrantBurnDownHistory(copy, per-segment ordering check, anchor validation, contiguity validation) is correct and matches the test coverage inhistory_test.go.Based on learnings, AGENTS.md states: "Do not hide type switching, validation, persistence mapping, or meaningful domain translation inside local closures. Use a named helper; reserve inline callbacks for obvious, tiny logic."
♻️ Proposed extraction
- sort.SliceStable(s, func(i, j int) bool { - if s[i].ClosedPeriod.From.Equal(s[j].ClosedPeriod.From) { - return s[i].TerminationReasons.Rollover && !s[j].TerminationReasons.Rollover - } - - return s[i].ClosedPeriod.From.Before(s[j].ClosedPeriod.From) - }) + sort.SliceStable(s, func(i, j int) bool { + return segmentLess(s[i], s[j]) + }) +} + +// segmentLess orders segments chronologically. Rollover transitions precede +// regular segments at the same timestamp so the reset transition is applied +// before new-period usage. +func segmentLess(a, b GrantBurnDownHistorySegment) bool { + if a.ClosedPeriod.From.Equal(b.ClosedPeriod.From) { + return a.TerminationReasons.Rollover && !b.TerminationReasons.Rollover + } + + return a.ClosedPeriod.From.Before(b.ClosedPeriod.From)🤖 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 `@openmeter/credit/engine/history.go` around lines 104 - 165, Extract the domain-specific comparator from sort.SliceStable in NewGrantBurnDownHistory into a named helper such as segmentsLessForSort, preserving chronological ordering and ensuring rollover segments precede non-rollover segments at equal timestamps. Pass the helper to sort.SliceStable so the ordering rule is independently testable.Source: Coding guidelines
openmeter/credit/balance/balance_test.go (1)
93-114: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtend
TestSnapshotCloneCopiesUsageSnapshotto coverBalancesandUnitConfigindependence.This test only checks that mutating the clone's
UsageSnapshotdoes not affect the original.Snapshot.Clone()also clonesBalancesandUnitConfigindependently. Add assertions for those two fields so a future regression in either clone path is caught here.♻️ Suggested extension
func TestSnapshotCloneCopiesUsageSnapshot(t *testing.T) { snapshot := balance.Snapshot{ + Balances: balance.Map{"grant-1": 100}, UsageSnapshot: &balance.UsageSnapshot{ Usage: 5, TotalGrantUsage: 10, }, } cloned := snapshot.Clone() + cloned.Balances["grant-1"] = 999 + assert.Equal(t, 100.0, snapshot.Balances["grant-1"]) require.NotNil(t, cloned.UsageSnapshot) ...As per path instructions, "Make sure the tests are comprehensive and cover the changes," and
Clone()is new behavior introduced by this PR without full field coverage.🤖 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 `@openmeter/credit/balance/balance_test.go` around lines 93 - 114, Extend TestSnapshotCloneCopiesUsageSnapshot to initialize representative Balances and UnitConfig values, then assert the cloned fields match the original values and are independent. Mutate the clone’s Balances and UnitConfig after cloning and verify the corresponding fields on the original snapshot remain unchanged, alongside the existing UsageSnapshot assertions.Source: Path instructions
🤖 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 `@openmeter/credit/engine/reset.go`:
- Around line 51-86: Clone rolledOver before passing it to burnDownGrants in the
reset flow so in-place mutations cannot alter the original rollover balances.
Keep balances initialized from rolledOver for the reset snapshot, but pass a
cloned map as burnDownGrants’ balance input; preserve BalanceAtStart as the
pre-burn rolled-over state.
In `@openmeter/credit/engine/run.go`:
- Around line 251-277: Preserve the conversion configuration when constructing
the snapshot returned by runBetweenResets: copy
params.StartingSnapshot.UnitConfig into the new balance.Snapshot alongside the
existing fields. Also update reset() to copy UnitConfig into its returned reset
snapshot so subsequent engine segments retain the same configuration.
In `@openmeter/entitlement/README.md`:
- Around line 27-28: Update the sentence in the entitlement documentation so the
singular subject “Entitlement” uses the possessive pronoun “its” instead of
“their,” leaving the rest of the wording unchanged.
---
Nitpick comments:
In `@openmeter/credit/balance/balance_test.go`:
- Around line 93-114: Extend TestSnapshotCloneCopiesUsageSnapshot to initialize
representative Balances and UnitConfig values, then assert the cloned fields
match the original values and are independent. Mutate the clone’s Balances and
UnitConfig after cloning and verify the corresponding fields on the original
snapshot remain unchanged, alongside the existing UsageSnapshot assertions.
In `@openmeter/credit/engine/history.go`:
- Around line 104-165: Extract the domain-specific comparator from
sort.SliceStable in NewGrantBurnDownHistory into a named helper such as
segmentsLessForSort, preserving chronological ordering and ensuring rollover
segments precede non-rollover segments at equal timestamps. Pass the helper to
sort.SliceStable so the ordering rule is independently testable.
🪄 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: d267b431-eec1-4a79-a976-00385739403a
⛔ Files ignored due to path filters (9)
openmeter/ent/db/balancesnapshot.gois excluded by!**/ent/db/**openmeter/ent/db/balancesnapshot/balancesnapshot.gois excluded by!**/ent/db/**openmeter/ent/db/balancesnapshot/where.gois excluded by!**/ent/db/**openmeter/ent/db/balancesnapshot_create.gois excluded by!**/ent/db/**openmeter/ent/db/balancesnapshot_update.gois excluded by!**/ent/db/**openmeter/ent/db/migrate/schema.gois excluded by!**/ent/db/**openmeter/ent/db/mutation.gois excluded by!**/ent/db/**openmeter/ent/db/runtime.gois excluded by!**/ent/db/**tools/migrate/migrations/atlas.sumis excluded by!**/*.sum,!**/*.sum
📒 Files selected for processing (34)
AGENTS.mdopenmeter/credit/README.mdopenmeter/credit/adapter/balance_snapshot.goopenmeter/credit/balance/balance.goopenmeter/credit/balance/balance_test.goopenmeter/credit/balance/repository.goopenmeter/credit/balance/service.goopenmeter/credit/balance/service_test.goopenmeter/credit/engine/engine.goopenmeter/credit/engine/engine_test.goopenmeter/credit/engine/history.goopenmeter/credit/engine/history_test.goopenmeter/credit/engine/reset.goopenmeter/credit/engine/reset_test.goopenmeter/credit/engine/run.goopenmeter/credit/engine/run_test.goopenmeter/credit/engine/runresult_test.goopenmeter/credit/engine/snapshot_test.goopenmeter/credit/helper.goopenmeter/ent/schema/balance_snapshot.goopenmeter/entitlement/README.mdopenmeter/entitlement/metered/balance.goopenmeter/entitlement/metered/balance_test.goopenmeter/entitlement/metered/balance_total_available_test.goopenmeter/entitlement/metered/balance_unitconfig_test.goopenmeter/entitlement/metered/lateevents_test.goopenmeter/entitlement/metered/reset_test.goopenmeter/entitlement/metered/utils_test.goopenmeter/registry/builder/entitlement.goopenmeter/subscription/README.mdtest/billing/subscription_suite.gotest/entitlement/regression/framework_test.gotools/migrate/migrations/20260803135655_add_balance_snapshot_usage_snapshot.down.sqltools/migrate/migrations/20260803135655_add_balance_snapshot_usage_snapshot.up.sql
💤 Files with no reviewable changes (1)
- openmeter/credit/balance/service_test.go
| balances := rolledOver | ||
| overage := startingOverage | ||
| var grantUsages GrantUsages | ||
| if startingOverage != 0 { | ||
| balances, grantUsages, overage = e.burnDownGrants(rolledOver, prioritizedGrants, startingOverage) | ||
| } | ||
|
|
||
| return balance.Snapshot{ | ||
| // The reset snapshot is the point-in-time balance after grant balance | ||
| // rollover and preserved overage burn. | ||
| resetSnapshot := balance.Snapshot{ | ||
| At: at, | ||
| Balances: rolledOver, | ||
| Overage: startingOverage, | ||
| }, nil | ||
| Balances: balances, | ||
| Overage: overage, | ||
| Usage: balance.SnapshottedUsage{ | ||
| Since: at, | ||
| Usage: 0, | ||
| }, | ||
| UsageSnapshot: &balance.UsageSnapshot{ | ||
| Usage: 0, | ||
| TotalGrantUsage: grantUsages.Sum().InexactFloat64(), | ||
| }, | ||
| } | ||
|
|
||
| // The rollover segment captures the instantaneous transition from rolled-over | ||
| // balances to the reset snapshot. | ||
| rolloverSegment := GrantBurnDownHistorySegment{ | ||
| ClosedPeriod: timeutil.ClosedPeriod{From: at, To: at}, | ||
| BalanceAtStart: rolledOver, | ||
| TerminationReasons: SegmentTerminationReason{ | ||
| Rollover: true, | ||
| }, | ||
| TotalUsage: 0, | ||
| OverageAtStart: startingOverage, | ||
| Overage: overage, | ||
| GrantUsages: grantUsages, | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- burnDownGrants definition ---"
rg -n -B2 -A40 'func \(e \*engine\) burnDownGrants' --type=go
echo "--- fallback broader search in case receiver differs ---"
rg -n -B2 -A40 'func.*burnDownGrants\(' --type=goRepository: openmeterio/openmeter
Length of output: 193
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- git status/stat ---"
git diff --stat
echo
echo "--- locate reset.go and related credit engine files ---"
git ls-files | rg '(^|/)reset\.go$|credit|engine' | head -200
echo "--- exact filename matches ---"
fd -a -i 'reset\.go$' . || true
fd -a -i 'engine' . || trueRepository: openmeterio/openmeter
Length of output: 12741
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- reset.go size and contents ---"
wc -l openmeter/credit/engine/reset.go
sed -n '1,180p' openmeter/credit/engine/reset.go
echo "--- burnDownGrants exact search ---"
rg -n -B3 -A120 'burnDownGrants' --type=go || true
echo "--- reset_test relevant assertions ---"
sed -n '1,260p' openmeter/credit/engine/reset_test.goRepository: openmeterio/openmeter
Length of output: 2959
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- all Reset/ResetBehavior references ---"
rg -n -B3 -A50 'ResetBehavior|PreserveOverage|func \(.*engine\)|PrioritizeGrants|GrantBurnDownHistorySegment|BalanceAtStart|TotalUsage' --type=go | head -500 || true
echo "--- files in openmeter/credit/engine ---"
for f in openmeter/credit/engine/*.go; do echo "### $f"; sed -n '1,260p' "$f"; done
echo "--- all occurrences of BurnDown/Burn down / Overages ---"
rg -n -i 'burn(down)?|preserve.?overage|overage|roll.?over' openmeter/credit openmeter/credit/engine openmeter/credit/grant openmeter/credit/balance --type=goRepository: openmeterio/openmeter
Length of output: 202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
root = Path('openmeter/credit/engine')
for f in sorted(root.glob('*.go')):
text = f.read_text(errors='replace').splitlines()
hits = []
for i, line in enumerate(text, 1):
if any(s in line for s in ['burnDownGrants', 'PrioritizeGrants', 'BalanceAtStart', 'GrantBurnDownHistorySegment', 'ResetBehavior', 'PreserveOverage', 'Map.Clone', 'Snapshot.Clone']):
hits.append((i, line.rstrip()))
if hits:
print(f'--- {f} ---')
for i, line in hits[:30]:
print(f'{i}: {line}')
PY
echo "--- read-only structural probe: function/member definitions and relevant mutations in credit engine ---"
python3 - <<'PY'
from pathlib import Path
import re
files = [Path('openmeter/credit/engine/reset.go'), Path('openmeter/credit/engine/grant.go'), Path('openmeter/credit/engine/burnphase.go'), Path('openmeter/credit/engine/history.go')]
for f in files:
if not f.exists():
print(f'missing {f}')
continue
text = f.read_text()
print(f'### {f} ({len(text)} bytes) ###')
for name in ['burnDownGrants', 'PrioritizeGrants', 'GrantBurnDownHistorySegment', 'reset']:
m = re.search(r'(?:func|type)\b[^;]+' + re.escape(name), text)
print(f'{name}: found={bool(m)}')
print('assignment lines mentioning rolledOver/balances/startOverage/overage:')
for i, line in enumerate(text.splitlines(), 1):
if any(s in line for s in ['rolledOver', 'balances', 'startingOverage', 'overage', 'burnDownGrants']):
print(f'{i}: {line.rstrip()}')
PY
echo "--- exact all-occurrence search ---"
find . -type f \( -name '*.go' \) -print0 | xargs -0 rg -n 'burnDownGrants|PrioritizeGrants|GrantBurnDownHistorySegment|BalanceAtStart|PreserveOverage|Rotate|RolledOver|rolledOver|burn.*overage|overage.*burn'Repository: openmeterio/openmeter
Length of output: 48106
Clone rolledOver before calling burnDownGrants.
balances := rolledOver makes both aliases point at the same map. The existing burnDownGrants in engine lifecycle code mutates its balance map in place, so when startingOverage != 0 the BalanceAtStart assignment later would use the post-burn balances instead of the balances after rollover. Use rolledOver.Clone() for the burn-down input only.
🤖 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 `@openmeter/credit/engine/reset.go` around lines 51 - 86, Clone rolledOver
before passing it to burnDownGrants in the reset flow so in-place mutations
cannot alter the original rollover balances. Keep balances initialized from
rolledOver for the reset snapshot, but pass a cloned map as burnDownGrants’
balance input; preserve BalanceAtStart as the pre-burn rolled-over state.
| history, err := NewGrantBurnDownHistory(segments, params.StartingSnapshot) | ||
| if err != nil { | ||
| return RunResult{}, fmt.Errorf("failed to create grant burn down history: %w", err) | ||
| } | ||
|
|
||
| totalUsage := history.TotalUsageInHistory() | ||
| usage := balance.SnapshottedUsage{ | ||
| Since: params.StartingSnapshot.Usage.Since, | ||
| Usage: params.StartingSnapshot.Usage.Usage + totalUsage, | ||
| } | ||
| usageSnapshot := balance.UsageSnapshot{ | ||
| Usage: params.StartingSnapshot.UsageSnapshot.Usage + totalUsage, | ||
| TotalGrantUsage: alpacadecimal.NewFromFloat(params.StartingSnapshot.UsageSnapshot.TotalGrantUsage). | ||
| Add(history.TotalGrantUsage()). | ||
| InexactFloat64(), | ||
| } | ||
|
|
||
| return RunResult{ | ||
| Snapshot: balance.Snapshot{ | ||
| Balances: balancesAtPhaseStart, | ||
| Overage: overage, | ||
| At: period.To, | ||
| Usage: balance.SnapshottedUsage{ | ||
| Since: params.StartingSnapshot.Usage.Since, | ||
| Usage: params.StartingSnapshot.Usage.Usage + history.TotalUsageInHistory(), | ||
| }, | ||
| Usage: usage, | ||
| UsageSnapshot: &usageSnapshot, | ||
| Balances: balancesAtPhaseStart, | ||
| Overage: overage, | ||
| At: period.To, | ||
| }, | ||
| History: history, | ||
| }, nil |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check whether snapshot construction sites propagate UnitConfig consistently.
rg -n 'balance\.Snapshot\{' openmeter/credit/engine/*.go -A 8
rg -n 'UnitConfig' openmeter/credit/engine/*.go openmeter/credit/balance/*.goRepository: openmeterio/openmeter
Length of output: 37532
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== engine run.go relevant sections =="
sed -n '1,90p' openmeter/credit/engine/run.go
sed -n '230,285p' openmeter/credit/engine/run.go
echo "== reset.go relevant sections =="
sed -n '1,95p' openmeter/credit/engine/reset.go
echo "== balance snapshot definition =="
sed -n '115,145p' openmeter/credit/balance/balance.go
echo "== git diff stat/name-only if available =="
git diff --stat
git diff --openmeter/credit/engine/run.go --openmeter/credit/engine/reset.go --openmeter/credit/balance/balance.go --openmeter | sed -n '1,200p' || true
echo "== structural scan balance.Snapshot assignment sites in openmeter/credit =="
python3 - <<'PY'
from pathlib import Path
import re
for p in sorted(Path('openmeter/credit').rglob('*.go')):
text = p.read_text(errors='ignore')
if 'balance.Snapshot{' not in text:
continue
lines=text.splitlines()
for i,l in enumerate(lines,1):
if 'balance.Snapshot{' in l:
snippet='\n'.join(lines[i-1:min(i+20,len(lines))])
has_unit='UnitConfig' in snippet
print(f"{p}:{i}: UnitConfig={has_unit}")
print(snippet[:800])
print("----")
PYRepository: openmeterio/openmeter
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== snapshot validation / read usage =="
rg -n 'func validateStartingSnapshot|StartingSnapshot|UnitConfig' openmeter/credit/openmeter credit openmeter/credit -g '*.go' | sed -n '1,220p'
echo "== balance snapshot read validation =="
rg -n 'func .*Snapshot|Snapshot|UnitConfig' openmeter/credit/adapter openmeter/credit openmeter -g '*.go' | rg -n 'Snapshot|UnitConfig' | sed -n '1,240p'Repository: openmeterio/openmeter
Length of output: 12212
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== snapshot validation / read usage =="
rg -n 'func validateStartingSnapshot|StartingSnapshot|UnitConfig' openmeter/credit -g '*.go' | sed -n '1,220p'
echo "== balance snapshot read validation =="
rg -n 'Snapshot|UnitConfig' openmeter/credit -g '*.go' | sed -n '1,240p'Repository: openmeterio/openmeter
Length of output: 35919
Preserve UnitConfig on both returned snapshot sites.
Run() replaces snapshot with runRes.Snapshot, and runBetweenResets() returns a new balance.Snapshot without copying UnitConfig, so later engine runs lose the conversion regime. Add params.StartingSnapshot.UnitConfig here, and carry it through reset() as well where the reset snapshot is returned for the next segment.
🤖 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 `@openmeter/credit/engine/run.go` around lines 251 - 277, Preserve the
conversion configuration when constructing the snapshot returned by
runBetweenResets: copy params.StartingSnapshot.UnitConfig into the new
balance.Snapshot alongside the existing fields. Also update reset() to copy
UnitConfig into its returned reset snapshot so subsequent engine segments retain
the same configuration.
Source: Path instructions
| that materializes and supersedes subscription-managed entitlements. | ||
| Entitlement owns their persisted lifecycle and value resolution. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the pronoun-agreement slip.
"Entitlement" is singular, so "their" should be "its" in this sentence.
✏️ Proposed fix
-Entitlement owns their persisted lifecycle and value resolution.
+Entitlement owns its persisted lifecycle and value resolution.📝 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.
| that materializes and supersedes subscription-managed entitlements. | |
| Entitlement owns their persisted lifecycle and value resolution. | |
| that materializes and supersedes subscription-managed entitlements. | |
| Entitlement owns its persisted lifecycle and value resolution. |
🤖 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 `@openmeter/entitlement/README.md` around lines 27 - 28, Update the sentence in
the entitlement documentation so the singular subject “Entitlement” uses the
possessive pronoun “its” instead of “their,” leaving the rest of the wording
unchanged.
Source: Path instructions
What changed
Why
totalAvailableGrantAmount was reconstructed from grant usage after the latest persisted snapshot. Grant usage from earlier in the same usage period was therefore lost, allowing API responses where usage exceeded total available grants while balance remained positive and overage stayed zero.
A persisted snapshot must be a complete, independent checkpoint. Its result must not depend on earlier snapshots still being present.
Impact
Entitlement balance reads now preserve the full usage-period grant history when resuming from snapshots. Existing snapshots without the new usage state are ignored and recalculated from measurement start until new checkpoints are written. The legacy field remains written so old and new binaries can overlap during deployment.
Checks
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Greptile Summary
The PR makes balance snapshots independent resumable checkpoints by persisting cumulative usage-period state and ignoring legacy incomplete snapshots.
Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains within the eligible follow-up review scope.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR Read[Balance read] --> Select{Complete compatible snapshot?} Select -->|Yes| Resume[Resume from persisted checkpoint] Select -->|No| Start[Start from measurement start] Resume --> Engine[Run credit engine] Start --> Engine Engine --> Reset{Reset encountered?} Reset -->|Yes| Rollover[Apply rollover and preserved overage] Rollover --> Continue[Continue new usage period] Reset -->|No| Continue Continue --> Result[Balance and cumulative usage state] Result --> Persist[Persist complete snapshot and legacy usage shape]Reviews (2): Last reviewed commit: "docs(entitlement): document balance sema..." | Re-trigger Greptile
Context used: