AI-1580 Group Usage Tracking - #219
Conversation
|
I ran review through Fable. Let me know if any don't apply, otherwise I can get it to implement these: PR #219 Review — AI-1580 Group Usage Tracking
OverviewThe PR introduces a usage-metering and spending-cap system: six new Prisma tables ( VerdictRequest changes. The architecture is sound — Decimal money columns, a single shared write path with atomic summary increments, consistent UTC period keys, clean service layering, and solid controller auth. But the PR has four defects that break it outright at deploy/runtime (§1.1–1.4), and — more fundamentally for a billing feature — no idempotency story anywhere: Temporal activity retries, nightly-job retries, and schedule catch-ups all double-charge, and the cap check provides no actual concurrency protection despite comments and tests claiming it does. The blob ledger cannot represent a key written more than once, which occurs in real flows today. All Critical and most Major findings below were verified directly against PR-head source (quoted lines checked), not just pattern-matched. 1. Critical — broken at build, migrate, or first use1.1 Cap check queries a non-existent table — caps cause a full outage for the group
The raw SQL reads Failure scenario: an admin sets any cap on a group → every OCR submission, training start, and classifier training for that group (all three call 1.2 Migration enum drifts from schema — training billing inserts fail on migrated DBs
Migration: Failure scenario: on any database migrated via 1.3 Both production Docker images fail to build —
|
|
Where things are at so far. 1. Critical — broken at build, migrate, or first use1.1 Cap check queries a non-existent table — caps cause a full outage for the groupSeems like a real concern. I've updated the query, although I couldn't seem to produce a 500 error. It appeared to run fine, so maybe prisma is handling something here. 1.2 Migration enum drifts from schema — training billing inserts fail on migrated DBsUpdated migration. I'm thinking it didn't get updated due to changing the 1.3 Both production Docker images fail to build —
|
# Conflicts: # CLAUDE.md # apps/temporal/package.json
Document the AI-1580 group usage-metering and spending-cap feature to fit the repo wiki pattern: - docs-md/architecture/USAGE_METERING_AND_BILLING.md: canonical how-it-works / how-to-use reference (data model, rate versions, soft cap check, storage charging, env config, API surface, known limitations). - docs-md/wiki/billing.md: short routing/map topic page with frontmatter and canonical sources; registered in index.md and sources.md. - open-questions.md: record the soft-cap-vs-"atomic"-REQUIREMENTS contradiction and the .env.sample blob-flag name drift. - log.md: ingest entry. Validated with `node scripts/validate-wiki.js` (passes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes reviewThanks for the round of fixes — most of the build/deploy blockers look genuinely resolved. Verified against current head: Confirmed fixed: 1.1 (table name), 1.3 (Dockerfile COPY), 1.4 (retention guard handles Residual items worth another lookCritical-ish
Design / correctness
Still open and acknowledged: 2.3 (benchmarks not cap-checked), 2.4 (page-assumption undercount), 2.5 (missed-day / catch-up charging), 2.7 (cancelled terminal event), 2.8 (reused workflow IDs). 2.5 and 2.8 in particular are worth verifying against actual Temporal behavior rather than assuming. Documentation (done — please review)There was no how-to-use documentation for this feature, and the branch predated the repo-wiki setup. I've merged
Passes |
|
Temporal tests not expected to pass until changes in this PR are merged: #239 Changes since last feedback comment:Critical1.7 — .env.sample wrong variable name: Fixed CHARGE_FOR_BLOB_TRANSACTION → CHARGE_FOR_TEMPORAL_BLOB_TRANSACTION_SEPARATELY=false with updated comment. Also removed the "Known drift" note from the canonical architecture doc. 1.7 — blob writes gating documentation: The canonical doc already covered this correctly in the Known Gaps section. The spending-cap section was also updated to be clearer. 2.11 — empty PATCH silently clears cap: Changed SetBillingCapDto.monthly_cap_dollars from @IsOptional() to @isdefined() + @ValidateIf(o => o.monthly_cap_dollars !== null). A PATCH {} request now returns HTTP 400; { monthly_cap_dollars: null } still works to remove the cap. Design / correctness2.1 — soften "atomic" cap claim: Updated REQUIREMENTS.md §4.2 and §10 — "atomic" replaced with "best-effort soft cap" language that accurately reflects the serializable transaction + row lock approach. 2.13 — group admin can set own cap: Updated the Swagger @apioperation on the endpoint from "platform admin only" to "group admin or platform admin" with an explanatory description. Updated the canonical doc's spending-cap section to explicitly state this is a group self-imposed guardrail by design. 1.5 — idempotency limitation: Already documented in the canonical doc's Known Gaps section (no change needed). 2.9 — unbounded getGroupActivityHistory scan: Applied a default 24-month lookback when no startDate is supplied, matching the retention policy. Explicit startDate params are still honored as-is. 2.2 — missing await: Added await before this.documentService.updateDocument(document.id, ...) in the cap-check catch block in ocr.service.ts. 2.14 — $queryRaw mock can't catch table-name bugs: Updated Scenario 4 to also inspect the template parts array and assert the SQL contains "usage_period_summaries" and FOR UPDATE. Old Issues Addressed2.3 — benchmarks not cap-checked (documentation clarified): Updated the Known Limitations entry to explain why: benchmark runs bypass requestOcr and start Temporal directly, so the pre-flight cap check is never in the call path. 2.4 — page-assumption undercount (already documented, no change needed). 2.5 — missed-day / catch-up charging (verified + fixed): Confirmed via Temporal SDK source that the default catchupWindow is 60 seconds — any billing worker outage longer than that would silently skip a day's storage charges. Fixed by adding policies: { catchupWindow: "25 hours" } to the nightly storage charge schedule. Also confirmed that Temporal's deterministic time means catch-up runs correctly charge for the originally scheduled day (not "today"), so they won't double-charge or charge the wrong day. 2.7 — cancelled terminal event (verified + fixed): 2.8 — reused workflow IDs (confirmed real bug + fixed): Verified that reprocessDocument genuinely reuses graph-${documentId} as the Temporal workflowId, causing activity billing events from the old failed run to contaminate the new run's aggregation in recordWorkflowLifecycle. Fixed by switching the billing key from workflowId to runId throughout: the activity interceptor now uses info.workflowExecution.runId, graph-workflow.ts uses workflowInfo().runId, and startGraphWorkflow returns handle.firstExecutionRunId. The two callsites that need the Temporal workflowId for handle operations (sendHumanApproval, deleteWorkflowExecution) now compute graph-${documentId} directly instead of reading workflow_execution_id. |
Summary
AI-1580
Changes
billingpackage that holds shared types and a function that builds arguments.Testing
Considerations
Checklist
By submitting this pull request, I acknowledge that I have attempted to meet the following: