feat(core): deterministic session log replay with synced watermark (#35040)

This commit is contained in:
Kit Langton 2026-07-02 21:38:20 -04:00 committed by GitHub
commit 57e9e9771d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 205 additions and 87 deletions

View file

@ -1575,7 +1575,7 @@ export type SessionLogOutput =
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string }
}
)
| { readonly type: "log.caught_up"; readonly aggregateID: string; readonly seq?: number }
| { readonly type: "log.synced"; readonly aggregateID: string; readonly seq?: number }
export type SessionInterruptInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }

View file

@ -3,7 +3,7 @@ import { DateTime, Effect, Stream } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { AbsolutePath, Agent, Event, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect"
const caughtUp = { type: "log.caught_up" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) }
const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) }
test("session.get returns the decoded Effect projection", async () => {
const httpClient = HttpClient.make((request) =>
@ -70,7 +70,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(caughtUp)}\n\n`, {
new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(synced)}\n\n`, {
headers: { "content-type": "text/event-stream" },
}),
),
@ -149,11 +149,11 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
expect(result.context).toEqual([])
expect(logQueries[0]).toEqual({ after: "0" })
const logged = Array.from(result.log)
expect(logged.map((item) => item.type)).toEqual(["session.next.model.switched", "log.caught_up"])
expect(logged.map((item) => item.type)).toEqual(["session.next.model.switched", "log.synced"])
expect(logged[0]?.type === "session.next.model.switched" && DateTime.toEpochMillis(logged[0].data.timestamp)).toBe(
1_717_171_717_000,
)
expect(logged.at(-1)).toEqual(caughtUp)
expect(logged.at(-1)).toEqual(synced)
expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
})

View file

@ -232,7 +232,7 @@ test("session methods use the public HTTP contract", async () => {
})
}
if (url.includes("/log")) {
return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(caughtUp)}\n\n`, {
return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(synced)}\n\n`, {
headers: { "content-type": "text/event-stream" },
})
}
@ -273,7 +273,7 @@ test("session methods use the public HTTP contract", async () => {
expect(created.id).toBe("ses_test")
expect(admitted.id).toBe("msg_test")
expect(context).toEqual([])
expect(log).toEqual([modelSwitchedEvent, caughtUp])
expect(log).toEqual([modelSwitchedEvent, synced])
expect(message).toEqual(modelSwitchedMessage)
expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
["GET", "http://localhost:3000/api/session?limit=10&order=desc&parentID=null"],
@ -368,7 +368,7 @@ const modelSwitchedMessage = {
model: { id: "claude", providerID: "anthropic" },
}
const caughtUp = { type: "log.caught_up", aggregateID: "ses_test", seq: 1 }
const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 }
const modelSwitchedEvent = {
id: "evt_model",