Order: rate-limit backstop first (cheap counter), then the balance gate, then usage tracking. Guards wrap outside the tracker so a blocked execution is neither run nor tracked. Covers both planes (HTTP executor plane and MCP session DO) since both build engines through this layer.
Unified · Split
| @@ -28,19 +28,40 @@ import { · 27 unmodified lines | ||
| 28 | 28 | import { AutumnService } from "../extensions/billing/service"; |
| 29 | 29 | import type { DbService } from "../db/db"; |
| 30 | 30 | import { CloudExecutionSeamsLayer } from "../engine/execution-stack"; |
| 31 | +import { makeExecutionLimitGate } from "./execution-gate"; | |
| 32 | +import { makeCloudExecutionRateLimiter } from "./execution-rate-limit"; | |
| 31 | 33 | import { withExecutionUsageTracking } from "./execution-usage"; |
| 32 | 34 | |
| 33 | −// Usage-metering decorator bound to the billing service. `trackExecution` is | |
| 34 | −// fire-and-forget (`Effect.runFork`) so the billing call can't stall a | |
| 35 | −// user-facing execution. | |
| 35 | +// Usage-metering decorator bound to the billing service, plus the two | |
| 36 | +// pre-execution guards this layer owns, ordered cheapest first: | |
| 37 | +// 1. rate-limit backstop (counter DO, independent of billing) | |
| 38 | +// 2. execution balance gate (Autumn check, cached 60s, fails open) | |
| 39 | +// 3. usage tracking — fire-and-forget (`Effect.runFork`) so the billing | |
| 40 | +// call can't stall a user-facing execution. | |
| 41 | +// The guards wrap OUTSIDE the tracker so a blocked execution is neither run | |
| 42 | +// nor tracked. One gate/limiter instance per layer build: the balance cache is | |
| 43 | +// shared by every engine this decorator produces (in the MCP session DO that's | |
| 44 | +// the session's engines; on the HTTP plane the boot layer builds it once). | |
| 36 | 45 | export const CloudMeteringEngineDecorator: Layer.Layer<EngineDecorator, never, AutumnService> = |
| 37 | 46 | Layer.effect(EngineDecorator)( |
| 38 | − Effect.map(AutumnService.asEffect(), (autumn): EngineDecorator["Service"] => ({ | |
| 39 | − decorate: (engine, identity: EngineStackIdentity) => | |
| 40 | − withExecutionUsageTracking(identity.organizationId, engine, (organizationId) => | |
| 41 | − Effect.runFork(autumn.trackExecution(organizationId)), | |
| 42 | − ), | |
| 43 | − })), | |
| 47 | + Effect.map(AutumnService.asEffect(), (autumn): EngineDecorator["Service"] => { | |
| 48 | + const balanceGate = makeExecutionLimitGate((organizationId) => | |
| 49 | + autumn.checkExecutionBalance(organizationId), | |
| 50 | + ); | |
| 51 | + const rateLimiter = makeCloudExecutionRateLimiter(); | |
| 52 | + return { | |
| 53 | + decorate: (engine, identity: EngineStackIdentity) => | |
| 54 | + rateLimiter.decorate( | |
| 55 | + identity.organizationId, | |
| 56 | + balanceGate.decorate( | |
| 57 | + identity.organizationId, | |
| 58 | + withExecutionUsageTracking(identity.organizationId, engine, (organizationId) => | |
| 59 | + Effect.runFork(autumn.trackExecution(organizationId)), | |
| 60 | + ), | |
| 61 | + ), | |
| 62 | + ), | |
| 63 | + }; | |
| 64 | + }), | |
| 44 | 65 | ); |
| 45 | 66 | |
| 46 | 67 | /** |