Repository navigation
fix(billing): answer an in-flight retry at once, and tell Chat what provenance withheld - #9094
Conversation
…rovenance withheld A cost callback retried while an earlier attempt of the same request was still committing (a commit can wait on a synchronous standby while holding the request's ledger lock) waited out lock_timeout on a pooled connection and failed with a 500. The ledger lock is now taken without waiting: a held lock answers 503 BILLING_REQUEST_IN_FLIGHT (Retry-After) at once, and the caller resends later; the monotonic top-up applies the higher cumulative once. Provenance refusals are policy, not faults: each gate admission now reports its refusals as one warning with counts and a bounded sample instead of one error per item, and the table envelope's unknown-row line is a warning. Chat's CLI result now notes how many items of each kind a gate withheld, on stderr, so the model can tell a withheld item from an absent one. Callers outside Chat never classify content and see no change. Also corrects two comments: the worker's callback timeout is 15 s, and no transaction setting bounds a commit waiting on a standby.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
[High impact] The PR appears safe to merge; no blocking issues remain. SummaryThe PR returns a retryable
Reviews (4) · Last reviewed commit: "fix(provenance): give every refusal line..." · Reviewed by Greptile |
…e route context The usage-log unit mock answered every statement with undefined, which the try-lock cannot read; it now answers whether the lock was taken, with a test that a held lock refuses before any read or write. The new route test passes the route context like the typed handler expects, and the refusal reason type stays module-private.
…be the rows that read as unknown A gate refuses either the item (unknown provenance) or only the requested view (an excerpt or raw bytes redaction cannot cover), so the note no longer says another read cannot return it. Untracked legacy rows read as exact-empty; only tracked rows with a stale, missing or malformed sidecar read as unknown.
|
@greptileai review |
|
@cubic-dev-ai review |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
…ox files by hash, and tell Chat why each item was withheld Refusal reports are grouped by surface, cause and owning record (document, table or chat), each line with top-level surface, cause and refusedCount and the refused ids (up to 200, flagged when truncated), so per-cause queries and id-to-cause pairing survive aggregation. A sandbox file is attributed by a chat-scoped hash of its path rather than the path, which carries the user's file name. A streamed sandbox image declares its view opaque, so its refusal reads as opaque content rather than a missing registry; the use case now requires every caller to declare the view. Gate verdicts carry the refusal reason, and Chat's CLI note says per reason what a read can do next: unknown provenance will not return on a retry, recorded secrets can be read as text for a redacted copy, and provenance that could not be verified may be readable later. Chunk write refusals name their document as the parent.
The usage ledger's slow-transaction warning fired for every in-flight refusal (stage lock, not succeeded), although nothing waited and the route already logs the refusal once at INFO. It now skips refusals. The span keeps the existing internal_error outcome, since the closed outcome set is generated from the legacy contracts; the response code and the INFO line tell it apart.
…roduced Drops the sandbox attribution id, the streamed-image view and the owning-record ids from this change; they belong to the provenance owner's work and fix behavior that predates it. Refusal lines stay one per surface and cause with top-level surface, cause and refusedCount and every refused id as each line names it today, and the note keeps its per-reason wording.
|
@greptileai review |
|
@cubic-dev-ai review |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
… tells an in-flight 503 apart A single refusal now carries refusedCount 1 like a grouped one, so summing refusedCount by cause covers both. The route comment now says the span is the same as the retryable settlement 503's and that the body code and the INFO line distinguish them.
|
@greptileai review |
|
@cubic-dev-ai review |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
Summary
lock_timeouton the per-request ledger lock. While an earlier attempt of the same request was still committing (a commit can wait on a synchronous standby while holding that lock), a retry held a pooled connection for 3s and then failed with a 500 plus an ERROR log.503withcode: BILLING_REQUEST_IN_FLIGHTandRetry-After: 5immediately, logged at INFO.lock_timeoutstill bounds the row locks taken under the ledger lock.surface,causeandrefusedCount, plus the refused ids as each line names them today (up to 200, flaggedresourceIdsTruncatedbeyond that), so every id stays paired with its cause. A single refusal keeps itsresourceIdfield.countWithheldContentwraps each Chat CLI request. When items were withheld, the CLI's stderr says, per kind and reason, what the model can do next, and names no item:internal_erroroutcome, because the closed outcome set is generated from the legacy contracts.BillingInFlightCodeis synced from the worker contracts.Companion: simstudioai/mothership#705
Release order: either order is safe.
Type of Change
Testing
Each of these fails without this PR and passes with it:
usage-log.integration.ts:route.test.ts: the in-flight error maps to the 503 with its code andRetry-After. Before this PR it returned a 500.registry-gate.test.tsandsession-gate.test.ts: verdicts carry the refusal reason, and 50 refusals produce one batched report.durable-secret-provenance-telemetry.test.ts: 163 refusals produce one line per cause, each keeping every id. A 250-item group is capped at 200 ids and flagged as truncated.content-delivery.test.ts:countWithheldContentcounts refusals by kind and reason across nested requests, and counts nothing for non-Chat callers.telemetry.test.ts: the note wording is checked for each refusal reason.usage-log.test.ts: an in-flight refusal emits no slow-transaction WARN.file-provenance.test.ts: the CLI result carries the reason-specific note.Checklist