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 | import { AutumnService } from "../extensions/billing/service"; | 28 | import { AutumnService } from "../extensions/billing/service"; |
| 29 | import type { DbService } from "../db/db"; | 29 | import type { DbService } from "../db/db"; |
| 30 | import { CloudExecutionSeamsLayer } from "../engine/execution-stack"; | 30 | import { CloudExecutionSeamsLayer } from "../engine/execution-stack"; |
| 31 | import { makeExecutionLimitGate } from "./execution-gate"; | ||
| 32 | import { makeCloudExecutionRateLimiter } from "./execution-rate-limit"; | ||
| 31 | import { withExecutionUsageTracking } from "./execution-usage"; | 33 | import { withExecutionUsageTracking } from "./execution-usage"; |
| 32 | 34 | ||
| 33 | // Usage-metering decorator bound to the billing service. `trackExecution` is | 35 | // Usage-metering decorator bound to the billing service, plus the two |
| 34 | // fire-and-forget (`Effect.runFork`) so the billing call can't stall a | 36 | // pre-execution guards this layer owns, ordered cheapest first: |
| 35 | // user-facing execution. | 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 | export const CloudMeteringEngineDecorator: Layer.Layer<EngineDecorator, never, AutumnService> = | 45 | export const CloudMeteringEngineDecorator: Layer.Layer<EngineDecorator, never, AutumnService> = |
| 37 | Layer.effect(EngineDecorator)( | 46 | Layer.effect(EngineDecorator)( |
| 38 | Effect.map(AutumnService.asEffect(), (autumn): EngineDecorator["Service"] => ({ | 47 | Effect.map(AutumnService.asEffect(), (autumn): EngineDecorator["Service"] => { |
| 39 | decorate: (engine, identity: EngineStackIdentity) => | 48 | const balanceGate = makeExecutionLimitGate((organizationId) => |
| 40 | withExecutionUsageTracking(identity.organizationId, engine, (organizationId) => | 49 | autumn.checkExecutionBalance(organizationId), |
| 41 | Effect.runFork(autumn.trackExecution(organizationId)), | 50 | ); |
| 42 | ), | 51 | const rateLimiter = makeCloudExecutionRateLimiter(); |
| 43 | })), | 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 | /** |