feat(core): record title and compaction usage in session totals (#37441)

This commit is contained in:
Aiden Cline
2026-07-17 00:10:30 -05:00
committed by GitHub
parent 91238441a6
commit 73567f3570
14 changed files with 234 additions and 74 deletions
@@ -803,6 +803,16 @@ export type SessionRevertCommitted = {
data: { sessionID: string; to: string }
}
export type SessionUsageRecorded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.usage.recorded"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; source: "title" | "compaction"; cost: MoneyUSD; tokens: TokenUsageInfo }
}
export type ModelsDevRefreshed = {
id: string
created: number
@@ -2216,6 +2226,7 @@ export type SessionEventDurable =
| SessionRevertStaged
| SessionRevertCleared
| SessionRevertCommitted
| SessionUsageRecorded
export type SessionMessagesResponse = {
data: Array<SessionMessageInfo>
+5 -6
View File
@@ -210,12 +210,11 @@ export interface Interface {
*/
readonly pending: (sessionID: SessionSchema.ID) => Effect.Effect<SessionPending.Info[], NotFoundError>
/**
* Durable, ordered session log read. Replays public durable
* session events after the exclusive `after` cursor, emits a `Synced`
* marker at the captured replay watermark, then continues live when `follow`
* is set.
* The marker's seq may exceed the last emitted event because non-public
* durable events share the aggregate's sequence space.
* Durable, ordered session log read. Replays durable session events after
* the exclusive `after` cursor, emits a `Synced` marker at the captured
* replay watermark, then continues live when `follow` is set.
* The marker's seq may exceed the last emitted event because other durable
* events share the aggregate's sequence space.
*/
readonly log: (input: {
sessionID: SessionSchema.ID
+33 -8
View File
@@ -14,6 +14,8 @@ import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema"
import { toSessionError } from "./to-session-error"
import { Token } from "../util/token"
import type { ModelV2 } from "../model"
import { SessionUsage } from "./usage"
const DEFAULT_BUFFER = 20_000
const DEFAULT_KEEP_TOKENS = 8_000
@@ -70,6 +72,7 @@ export type AutoInput = {
readonly session: SessionSchema.Info
readonly messages: readonly SessionMessage.Info[]
readonly model: Model
readonly cost: ModelV2.Info["cost"]
}
export type ManualInput = {
@@ -81,6 +84,7 @@ export type ManualInput = {
type Plan = {
readonly session: SessionSchema.Info
readonly model: Model
readonly cost: ModelV2.Info["cost"]
readonly reason: SessionMessage.Compaction["reason"]
readonly prompt: string
readonly recent: string
@@ -240,6 +244,16 @@ const make = (dependencies: Dependencies) => {
const chunks: string[] = []
let failure: SessionError.Error | undefined
let usage: SessionUsage.Recorded | undefined
const recordUsage = Effect.suspend(() =>
usage
? dependencies.events.publish(SessionEvent.UsageRecorded, {
sessionID: plan.session.id,
source: "compaction",
...usage,
})
: Effect.void,
)
yield* dependencies.llm
.stream(
LLM.request({
@@ -263,6 +277,10 @@ const make = (dependencies: Dependencies) => {
text: event.text,
})
}
if (LLMEvent.is.stepFinish(event)) {
const step = SessionUsage.record(event.usage, plan.cost)
usage = usage ? SessionUsage.add(usage, step) : step
}
return Effect.void
}),
Effect.catchTag("LLM.Error", (error) =>
@@ -271,16 +289,21 @@ const make = (dependencies: Dependencies) => {
}),
),
Effect.onInterrupt(() =>
plan.reason === "auto"
? failed({
sessionID: plan.session.id,
reason: plan.reason,
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
inputID: plan.inputID,
}).pipe(Effect.asVoid)
: Effect.void,
recordUsage.pipe(
Effect.andThen(
plan.reason === "auto"
? failed({
sessionID: plan.session.id,
reason: plan.reason,
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
inputID: plan.inputID,
}).pipe(Effect.asVoid)
: Effect.void,
),
),
),
)
yield* recordUsage
const summary = chunks.join("")
if (failure || !summary.trim()) {
const error = failure ?? { type: "compaction.failed" as const, message: "Compaction produced no summary" }
@@ -305,6 +328,7 @@ const make = (dependencies: Dependencies) => {
return yield* execute({
session: input.session,
model: input.model,
cost: input.cost,
reason: "auto",
...content,
})
@@ -353,6 +377,7 @@ const make = (dependencies: Dependencies) => {
return yield* execute({
session: input.session,
model: resolved.model,
cost: resolved.cost,
reason: "manual",
inputID: input.inputID,
...content,
@@ -142,6 +142,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return Effect.gen(function* () {
yield* SessionEvent.All.match(event, {
"session.usage.updated": () => Effect.void,
"session.usage.recorded": () => Effect.void,
"session.agent.selected": (event) => {
return adapter.appendMessage(
SessionMessage.AgentSelected.make({
+2 -1
View File
@@ -609,6 +609,7 @@ const layer = Layer.effectDiscard(
.run()
.pipe(Effect.orDie),
)
yield* events.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data))
yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event))
yield* events.project(SessionEvent.InputPromoted, (event) =>
Effect.gen(function* () {
@@ -781,7 +782,7 @@ const layer = Layer.effectDiscard(
yield* InstructionState.reset(db, event.data.sessionID)
}),
)
yield* events.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed]).pipe(
yield* events.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed, SessionEvent.UsageRecorded]).pipe(
Stream.runForEach((event) => {
if (
event.type === SessionEvent.Step.Failed.type &&
+5 -29
View File
@@ -2,11 +2,9 @@ export * as SessionRunnerLLM from "./llm"
import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent } from "@opencode-ai/ai"
import { SessionError } from "@opencode-ai/schema/session-error"
import { Money } from "@opencode-ai/schema/money"
import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
import { Database } from "../../database/database"
import { EventV2 } from "../../event"
import { ModelV2 } from "../../model"
import { PermissionV2 } from "../../permission"
import { QuestionTool } from "../../tool/question"
import { ToolOutputStore } from "../../tool-output-store"
@@ -28,30 +26,7 @@ import { llmClient } from "../../effect/app-node-platform"
import { StepFailedError } from "../error"
import { toSessionError } from "../to-session-error"
import { SessionRunnerRetry } from "./retry"
type StepTokens = {
readonly input: number
readonly output: number
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
// TODO(#35765): Use Copilot's reported billed amount once billing has a dedicated typed runtime contract.
export function calculateCost(costs: ModelV2.Info["cost"], tokens: StepTokens) {
const context = tokens.input + tokens.cache.read + tokens.cache.write
const tier = costs
.filter((cost) => cost.tier?.type === "context" && context > cost.tier.size)
.toSorted((a, b) => (b.tier?.size ?? 0) - (a.tier?.size ?? 0))[0]
const cost = tier ?? costs.find((cost) => cost.tier === undefined)
if (!cost) return Money.USD.zero
return Money.USD.make(
(tokens.input * cost.input +
(tokens.output + tokens.reasoning) * cost.output +
tokens.cache.read * cost.cache.read +
tokens.cache.write * cost.cache.write) /
1_000_000,
)
}
import { SessionUsage } from "../usage"
const layer = Layer.effect(
Service,
@@ -127,7 +102,7 @@ const layer = Layer.effect(
const agent = loaded.agent
const resolved = loaded.model
const model = resolved.model
const compactionInput = { session, messages: loaded.messages, model }
const compactionInput = { session, messages: loaded.messages, model, cost: resolved.cost }
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
const compacted = yield* compaction.compact(compactionInput)
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
@@ -216,7 +191,7 @@ const layer = Layer.effect(
)
const stepUsage = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) => ({
cost: calculateCost(resolved.cost, settlement.tokens),
cost: SessionUsage.calculateCost(resolved.cost, settlement.tokens),
tokens: settlement.tokens,
})
@@ -258,7 +233,8 @@ const layer = Layer.effect(
recoverOverflow &&
!publisher.hasRetryEvidence() &&
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
(yield* restore(recoverOverflow({ session, messages: loaded.messages, model }))).status === "completed"
(yield* restore(recoverOverflow({ session, messages: loaded.messages, model, cost: resolved.cost })))
.status === "completed"
)
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
@@ -1,4 +1,4 @@
import { ToolOutput, type LLMEvent, type ProviderMetadata, type ToolResultValue, type Usage } from "@opencode-ai/ai"
import { ToolOutput, type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai"
import { Effect } from "effect"
import { EventV2 } from "../../event"
import { ModelV2 } from "../../model"
@@ -9,6 +9,7 @@ import { SessionError } from "@opencode-ai/schema/session-error"
import { Money } from "@opencode-ai/schema/money"
import { AgentV2 } from "../../agent"
import { Snapshot } from "../../snapshot"
import { SessionUsage } from "../usage"
type Input = {
readonly sessionID: SessionSchema.ID
@@ -19,20 +20,6 @@ type Input = {
readonly assistantMessageID?: SessionMessage.ID
}
const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0)
const tokens = (usage: Usage | undefined) => {
const reasoning = safe(usage?.reasoningTokens)
const read = safe(usage?.cacheReadInputTokens)
const write = safe(usage?.cacheWriteInputTokens)
return {
input: safe(usage?.nonCachedInputTokens),
output: safe(usage?.visibleOutputTokens),
reasoning,
cache: { read, write },
}
}
const record = (value: unknown): Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : { value }
@@ -77,7 +64,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
let stepSettlement:
| {
readonly finish: Extract<LLMEvent, { type: "step-finish" }>["reason"]
readonly tokens: ReturnType<typeof tokens>
readonly tokens: ReturnType<typeof SessionUsage.tokens>
}
| undefined
@@ -251,7 +238,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
const publishStepFailure = Effect.fnUntraced(function* (usage?: {
readonly cost: Money.USD
readonly tokens: ReturnType<typeof tokens>
readonly tokens: ReturnType<typeof SessionUsage.tokens>
}) {
if (stepFailed || stepFailure === undefined) return
const assistantMessageID = yield* startAssistant()
@@ -429,7 +416,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
case "step-finish":
yield* flush()
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
stepSettlement = { finish: event.reason, tokens: tokens(event.usage) }
stepSettlement = { finish: event.reason, tokens: SessionUsage.tokens(event.usage) }
if (event.reason === "content-filter") {
providerFailed = true
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" }, true)
+18 -1
View File
@@ -1,7 +1,7 @@
export * as SessionTitle from "./title"
import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest } from "@opencode-ai/ai"
import { Context, DateTime, Effect, Layer, Stream } from "effect"
import { Context, Effect, Layer, Stream } from "effect"
import { AgentV2 } from "../agent"
import { Database } from "../database/database"
import { EventV2 } from "../event"
@@ -12,6 +12,7 @@ import { SessionHistory } from "./history"
import { SessionModelHeaders } from "./model-headers"
import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema"
import { SessionUsage } from "./usage"
const MAX_LENGTH = 100
@@ -51,6 +52,16 @@ const make = (dependencies: Dependencies) => {
if (!resolved) return
const chunks: string[] = []
let failed = false
let usage: SessionUsage.Recorded | undefined
const recordUsage = Effect.suspend(() =>
usage
? dependencies.events.publish(SessionEvent.UsageRecorded, {
sessionID: session.id,
source: "title",
...usage,
})
: Effect.void,
)
const streamed = yield* dependencies.llm
.stream(
LLM.request({
@@ -65,11 +76,17 @@ const make = (dependencies: Dependencies) => {
Stream.runForEach((event) => {
if (LLMEvent.is.providerError(event)) failed = true
if (LLMEvent.is.textDelta(event)) chunks.push(event.text)
if (LLMEvent.is.stepFinish(event)) {
const step = SessionUsage.record(event.usage, resolved.cost)
usage = usage ? SessionUsage.add(usage, step) : step
}
return Effect.void
}),
Effect.as(true),
Effect.catchTag("LLM.Error", () => Effect.succeed(false)),
Effect.onInterrupt(() => recordUsage.pipe(Effect.asVoid)),
)
yield* recordUsage
if (!streamed || failed) return
const title = chunks
.join("")
+55
View File
@@ -0,0 +1,55 @@
export * as SessionUsage from "./usage"
import type { Usage } from "@opencode-ai/ai"
import { Money } from "@opencode-ai/schema/money"
import type { TokenUsage } from "@opencode-ai/schema/token-usage"
import type { ModelV2 } from "../model"
const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0)
export const tokens = (usage: Usage | undefined): TokenUsage.Info => ({
input: safe(usage?.nonCachedInputTokens),
output: safe(usage?.visibleOutputTokens),
reasoning: safe(usage?.reasoningTokens),
cache: {
read: safe(usage?.cacheReadInputTokens),
write: safe(usage?.cacheWriteInputTokens),
},
})
// TODO(#35765): Use Copilot's reported billed amount once billing has a dedicated typed runtime contract.
export function calculateCost(costs: ModelV2.Info["cost"], usage: TokenUsage.Info) {
const context = usage.input + usage.cache.read + usage.cache.write
const tier = costs
.filter((cost) => cost.tier?.type === "context" && context > cost.tier.size)
.toSorted((a, b) => (b.tier?.size ?? 0) - (a.tier?.size ?? 0))[0]
const cost = tier ?? costs.find((cost) => cost.tier === undefined)
if (!cost) return Money.USD.zero
return Money.USD.make(
(usage.input * cost.input +
(usage.output + usage.reasoning) * cost.output +
usage.cache.read * cost.cache.read +
usage.cache.write * cost.cache.write) /
1_000_000,
)
}
export type Recorded = { readonly tokens: TokenUsage.Info; readonly cost: Money.USD }
export const record = (usage: Usage | undefined, costs: ModelV2.Info["cost"]): Recorded => {
const normalized = tokens(usage)
return { tokens: normalized, cost: calculateCost(costs, normalized) }
}
export const add = (a: Recorded, b: Recorded): Recorded => ({
cost: Money.USD.make(a.cost + b.cost),
tokens: {
input: a.tokens.input + b.tokens.input,
output: a.tokens.output + b.tokens.output,
reasoning: a.tokens.reasoning + b.tokens.reasoning,
cache: {
read: a.tokens.cache.read + b.tokens.cache.read,
write: a.tokens.cache.write + b.tokens.cache.write,
},
},
})
+35 -2
View File
@@ -21,6 +21,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Money } from "@opencode-ai/schema/money"
import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
import { asc, eq } from "drizzle-orm"
import { testEffect } from "./lib/effect"
@@ -31,17 +32,44 @@ const model = Model.make({
provider: "test",
route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }),
})
const cost = [
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(2),
cache: {
read: Money.USDPerMillionTokens.make(0.1),
write: Money.USDPerMillionTokens.make(0.5),
},
},
]
const client = Layer.mock(LLMClient.Service)({
prepare: () => Effect.die("unused"),
stream: (request: LLMRequest) => {
requests.push(request)
return Stream.make(LLMEvent.textDelta({ id: "summary", text: "manual summary" }))
return Stream.make(
LLMEvent.textDelta({ id: "summary", text: "manual summary" }),
LLMEvent.stepFinish({
index: 0,
reason: "stop",
usage: {
inputTokens: 15,
outputTokens: 6,
nonCachedInputTokens: 10,
cacheReadInputTokens: 3,
cacheWriteInputTokens: 2,
reasoningTokens: 2,
},
}),
LLMEvent.finish({
reason: "stop",
}),
)
},
generate: () => Effect.die("unused"),
})
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
const models = Layer.mock(SessionRunnerModel.Service)({
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model)),
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model, undefined, cost)),
})
const it = testEffect(
AppNodeBuilder.build(
@@ -169,6 +197,10 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
expect(yield* store.context(sessionID)).toMatchObject([
{ type: "compaction", reason: "manual", summary: "manual summary", recent: "" },
])
expect(yield* store.get(sessionID)).toMatchObject({
cost: 0.0000233,
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 2 } },
})
expect(
yield* db
.select({ type: EventTable.type })
@@ -179,6 +211,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
.pipe(Effect.orDie),
).toEqual([
{ type: EventV2.versionedType(SessionEvent.Compaction.Started.type, 1) },
{ type: EventV2.versionedType(SessionEvent.UsageRecorded.type, 1) },
{ type: EventV2.versionedType(SessionEvent.Compaction.Ended.type, 1) },
])
}),
+3 -2
View File
@@ -42,6 +42,7 @@ import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator
import { SessionRunner } from "@opencode-ai/core/session/runner"
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { SessionUsage } from "@opencode-ai/core/session/usage"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
@@ -165,7 +166,7 @@ const recoveryModel = Model.make({
test("calculates step cost using the matching context tier", () => {
expect(
SessionRunnerLLM.calculateCost(
SessionUsage.calculateCost(
[
{
input: Money.USDPerMillionTokens.make(1),
@@ -192,7 +193,7 @@ test("calculates step cost using the matching context tier", () => {
test("does not apply an ineligible tier without base pricing", () => {
expect(
SessionRunnerLLM.calculateCost(
SessionUsage.calculateCost(
[
{
tier: { type: "context", size: 100 },
+33 -3
View File
@@ -20,7 +20,8 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { DateTime, Effect, Layer, Stream } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Layer, Stream } from "effect"
import { testEffect } from "./lib/effect"
let requests: LLMRequest[] = []
@@ -29,16 +30,43 @@ const model = Model.make({
provider: "test",
route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }),
})
const cost = [
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(2),
cache: {
read: Money.USDPerMillionTokens.make(0.1),
write: Money.USDPerMillionTokens.make(0.5),
},
},
]
const client = Layer.mock(LLMClient.Service)({
prepare: () => Effect.die("unused"),
stream: (request: LLMRequest) => {
requests.push(request)
return Stream.make(LLMEvent.textDelta({ id: "title", text: "Generated Title\n" }))
return Stream.make(
LLMEvent.textDelta({ id: "title", text: "Generated Title\n" }),
LLMEvent.stepFinish({
index: 0,
reason: "stop",
usage: {
inputTokens: 15,
outputTokens: 6,
nonCachedInputTokens: 10,
cacheReadInputTokens: 3,
cacheWriteInputTokens: 2,
reasoningTokens: 2,
},
}),
LLMEvent.finish({
reason: "stop",
}),
)
},
generate: () => Effect.die("unused"),
})
const models = Layer.mock(SessionRunnerModel.Service)({
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model)),
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model, undefined, cost)),
})
const it = testEffect(
AppNodeBuilder.build(
@@ -130,6 +158,8 @@ it.effect("generates a title from the sole user message and renames the session"
expect(JSON.stringify(requests[0]?.messages)).toContain("Help me debug the failing build")
const renamed = yield* store.get(sessionID)
expect(renamed?.title).toBe("Generated Title")
expect(renamed?.tokens).toEqual({ input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 2 } })
expect(renamed?.cost).toBeCloseTo(0.0000233)
}),
)
+17 -1
View File
@@ -86,6 +86,18 @@ export const Renamed = Event.durable({
})
export type Renamed = typeof Renamed.Type
export const UsageRecorded = Event.durable({
type: "session.usage.recorded",
...options,
schema: {
...Base,
source: Schema.Literals(["title", "compaction"]),
cost: Money.USD,
tokens: TokenUsage.Info,
},
})
export type UsageRecorded = typeof UsageRecorded.Type
export const UsageUpdated = Event.ephemeral({
type: "session.usage.updated",
schema: {
@@ -569,8 +581,10 @@ export const Definitions = Event.inventory(
RevertEvent.Committed,
)
// UsageRecorded is durable but internal: excluded from Definitions so it never reaches the public manifest.
export const DurableDefinitions = Event.inventory(
...Definitions.filter((definition) => definition.durability === "durable"),
UsageRecorded,
)
export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" })
@@ -578,6 +592,8 @@ export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" })
.annotate({ identifier: "Session.Event.Durable" })
export type DurableEvent = typeof Durable.Type
export const All = Schema.Union(Definitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
export const All = Schema.Union(Event.inventory(...Definitions, UsageRecorded), { mode: "oneOf" }).pipe(
Schema.toTaggedUnion("type"),
)
export type Event = typeof All.Type
export type Type = Event["type"]
+11 -3
View File
@@ -104,6 +104,7 @@ describe("public event manifest", () => {
"session.model.selected.1",
"session.moved.1",
"session.renamed.1",
"session.usage.recorded.1",
"session.forked.2",
"session.input.promoted.1",
"session.input.admitted.1",
@@ -139,9 +140,16 @@ describe("public event manifest", () => {
"session.revert.committed.1",
].toSorted(),
)
expect(SessionEvent.DurableDefinitions).toEqual(
SessionEvent.Definitions.filter((definition) => definition.durability === "durable"),
)
expect(SessionEvent.DurableDefinitions).toEqual([
...SessionEvent.Definitions.filter((definition) => definition.durability === "durable"),
SessionEvent.UsageRecorded,
])
expect(SessionEvent.UsageRecorded.durability).toBe("durable")
expect(EventManifest.Durable.get("session.usage.recorded.1")).toBe(SessionEvent.UsageRecorded)
expect(SessionEvent.Definitions).not.toContain(SessionEvent.UsageRecorded)
expect(EventManifest.Definitions).not.toContain(SessionEvent.UsageRecorded)
expect(EventManifest.ServerDefinitions).not.toContain(SessionEvent.UsageRecorded)
expect(EventManifest.Latest.has("session.usage.recorded")).toBe(false)
expect(SessionEvent.UsageUpdated.durability).toBe("ephemeral")
expect(SessionEvent.Compaction.Delta.durability).toBe("ephemeral")
expect(EventManifest.Durable.has("session.compaction.delta.1")).toBe(false)