feat(billing): record Cloud Agent container usage - #4732
Conversation
Use ctx.container.running as the authoritative signal for adopting or reusing a usage generation. Persisted Sandbox health can remain stale after the physical process exits, which could otherwise create phantom intervals or carry one interval across two container instances. Mark heartbeat measurement active only after Container scheduling succeeds. This keeps scheduling failures recoverable instead of leaving a running generation permanently marked as metered without heartbeat segments.
Treat exhausted meter retries as telemetry delivery failures: retain durable generation state, arm heartbeat recovery, and warn without rejecting sandbox acquisition or physical container start. This preserves the record-only shadow contract while keeping local attribution validation strict. Bound deferred stopped-state observation to the existing 15-minute stale grace. If authoritative onStop never arrives, close the interval at the first observed stopped timestamp so alarms do not wake forever and the grace period is not billed.
Run acquisition and Sandbox lifecycle telemetry outside the real container control path. Start, stop, idle expiry, and wrapper lookup now complete without waiting for meter RPCs or shadow storage, while Durable Object waitUntil tasks retain best-effort recording and failure logs. Bound missed-stop retries, preserve the first observed stop cutoff, use trusted fallback routing metadata, validate origins at the Sandbox boundary, and remove the organization-handler import cycle. Shadow failures can no longer stop, reset, delay, or prevent reaping a customer container.
Record the exact Cloudflare Sandbox ID as the usage instanceId so reconciliation can pass recorder values directly to containersUsageAdaptiveGroups and match the Containers dashboard. Keep the opaque Durable Object ID as diagnostic metadata, retain trusted origin for isolated workloads, and remove allocation and repository-provider metadata that are not needed for billing reconciliation.
Shared container billing now carries the Cloudflare sandbox ID directly and intentionally omits allocation metadata. Keep the containment test aligned with that recorder contract while preserving its ordering assertions for billing dispatch, containment activation, and workspace probing.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryThe latest commit ( Files Reviewed (4 files)
Previous Review Summaries (3 snapshots, latest commit 87f30b7)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 87f30b7)Status: No Issues Found | Recommendation: Merge Executive SummaryThe new commit ( Files Reviewed (7 files)
Previous review (commit 0cd4ce2)Status: No Issues Found | Recommendation: Merge Executive SummaryAll three previously flagged WARNING issues in Files Reviewed (4 files)
Previous review (commit fdf4075)Status: 3 Issues Found | Recommendation: Address before merge Executive SummaryThe highest-risk issue is a missing Overview
Issue Details (click to expand)WARNING
Files Reviewed (23 files)
Reviewed by claude-sonnet-5 · Input: 40 · Output: 10.2K · Cached: 1.1M Review guidance: REVIEW.md from base branch |
Stop failed prior-generation recovery from falling through into replacement generation creation, consume activity-expiry intent at the physical stop boundary, and reject isolated-prefixed legacy IDs for shared sandbox classes.
Namespace recorder services by concrete Sandbox class while retaining raw Cloudflare instance IDs, and preserve authoritative isolated routing targets for code-review and DIND sessions. Capture physical stop cutoffs, deliver pending heartbeats before final stop remainders, use container transition timestamps for missed stops, bound authoritative stop retries, and start already-running replacements after prior generations close.
Summary
cloud-agent-nextSandbox class to the record-onlyContainerUsageMeterusing explicit immutable SKUs and trusted user, organization, bot, and origin attribution. Usage generations follow physical container processes, not logical Cloud Agent or Sandbox sessions.org-*,usr-*,bot-*,ubt-*,ses-*,dind-*, orcrv-*) as the recorderinstanceId, allowing reconciliation to querycontainersUsageAdaptiveGroupsdirectly with the value shown in the Cloudflare Containers dashboard.waitUntiltasks retain best-effort recording and structured failure logs.onStop, bill only through the first observed stop, and abandon local retries after one hour while PostgreSQL stale reconciliation remains the database safety net.Verification
cold-hotlifecycle. Two completed logical turns reportedsameContainers=trueand produced exactly oneses-*usage interval. The interval emitted five-minute segments (299s, then 300s segments), closed on physical idle shutdown withactivity_expired, and recorded 1,227 confirmed seconds through the final observed stop. The general harness still reported its unrelated warm-pathnoPrepare=falseassertion.kill-mid-flight hanglifecycle. The harness killed the active SandboxSmall container and proxy while the fake LLM request was pending, surfacedcloud.message.failed, and the recorder immediately closed the single interval withclose_reason=exit,exit_code=137, and a 10-second final segment. No stale-open interval or duplicate generation remained.instance_idmatched the exact Cloudflareses-*instance, the Small SKU was selected, user subject/actor and session were retained, and metadata contained trusted origin, container class, and Durable Object ID.Visual Changes
N/A
Reviewer Notes
container-usage.ts: repeated logical sessions and duplicate SDKonStartcallbacks reuse one generation, while a replacement physical process receives a newstartEpochMsunder the same CloudflareinstanceId.waitUntil; it must never stop, reset, delay, or prevent reaping a customer container.serviceis namespaced by concrete Sandbox class (for example,cloud-agent-next-sandbox-dind) so separate Cloudflare applications/namespaces cannot collide while sharing the same raw instance name.instanceIdis the exact Cloudflare dashboard/GraphQL lookup key.durable_object_idis retained only for namespace-specific diagnostics; user, organization, and bot attribution remain typed fields rather than metadata.