fix(sdk): preserve session stream ordering
This commit is contained in:
parent
59c1f89d39
commit
5146f01e0a
8 changed files with 252 additions and 95 deletions
|
|
@ -450,6 +450,30 @@ describe("EventV2", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("observes replay commits that are not republished", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const aggregateID = Session.ID.create()
|
||||
const observed = yield* events.observeAggregate({ aggregateID, live: () => false })
|
||||
const update = yield* observed.updates.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||
|
||||
yield* events.replay(
|
||||
{
|
||||
id: EventV2.ID.create(),
|
||||
aggregateID,
|
||||
seq: 0,
|
||||
type: EventV2.versionedType(DurableMessage.type, 1),
|
||||
data: durableData(aggregateID, "replayed"),
|
||||
},
|
||||
{ publish: false },
|
||||
)
|
||||
|
||||
expect(Array.from(yield* Fiber.join(update)).map((event) => event.data)).toEqual([
|
||||
durableData(aggregateID, "replayed"),
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("drains causally preceding durable rows before a live payload", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
|
|
@ -488,6 +512,33 @@ describe("EventV2", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("coalesces durable notifications without repeated empty reads", () =>
|
||||
Effect.gen(function* () {
|
||||
let reads = 0
|
||||
const eventLayer = EventV2.layerWith({
|
||||
beforeAggregateRead: () =>
|
||||
Effect.sync(() => {
|
||||
reads++
|
||||
}),
|
||||
}).pipe(Layer.provide(Database.defaultLayer))
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const aggregateID = Session.ID.create()
|
||||
const count = 20
|
||||
const observed = yield* events.observeAggregate({ aggregateID, live: () => false })
|
||||
|
||||
for (let index = 0; index < count; index++) {
|
||||
yield* events.publish(DurableMessage, durableData(aggregateID, String(index)))
|
||||
}
|
||||
const updates = yield* observed.updates.pipe(Stream.take(count), Stream.runCollect)
|
||||
|
||||
expect(updates).toHaveLength(count)
|
||||
expect(reads).toBe(2)
|
||||
}).pipe(Effect.provide(Layer.mergeAll(Database.defaultLayer, eventLayer)))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("coalesces durable aggregate wakes while draining every committed event", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
|
||||
import { DateTime, Deferred, Effect, Fiber, Layer, Stream } from "effect"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
|
@ -22,11 +22,29 @@ import { testEffect } from "./lib/effect"
|
|||
const executionCalls: SessionV2.ID[] = []
|
||||
const interruptCalls: SessionV2.ID[] = []
|
||||
const wakeCalls: SessionV2.ID[] = []
|
||||
let activityObserver: ((active: boolean) => void) | undefined
|
||||
let activityState = false
|
||||
const setActivity = (active: boolean) =>
|
||||
Effect.sync(() => {
|
||||
activityState = active
|
||||
activityObserver?.(active)
|
||||
})
|
||||
const execution = Layer.succeed(
|
||||
SessionExecution.Service,
|
||||
SessionExecution.Service.of({
|
||||
active: Effect.succeed(new Set()),
|
||||
activity: () => Effect.succeed({ snapshot: Effect.succeed(false), changes: Stream.never }),
|
||||
activity: () =>
|
||||
Effect.sync(() => {
|
||||
activityState = false
|
||||
activityObserver = undefined
|
||||
return {
|
||||
attach: (observer: (active: boolean) => void) =>
|
||||
Effect.sync(() => {
|
||||
activityObserver = observer
|
||||
return activityState
|
||||
}),
|
||||
}
|
||||
}),
|
||||
resume: (sessionID) =>
|
||||
Effect.sync(() => {
|
||||
executionCalls.push(sessionID)
|
||||
|
|
@ -231,6 +249,84 @@ describe("SessionV2.prompt", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("orders activity around the durable rows owned by a run", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
const streamed = yield* session.events({ sessionID }).pipe(Stream.take(5), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
yield* setActivity(true)
|
||||
yield* events.publish(SessionEvent.Text.Started, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
textID: "text-activity",
|
||||
timestamp: yield* DateTime.now,
|
||||
})
|
||||
yield* events.publish(SessionEvent.Text.Ended, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
textID: "text-activity",
|
||||
text: "done",
|
||||
timestamp: yield* DateTime.now,
|
||||
})
|
||||
yield* setActivity(false)
|
||||
|
||||
expect(Array.from(yield* Fiber.join(streamed)).map((event) => [event.type, event.data])).toEqual([
|
||||
["session.activity", { sessionID, active: false }],
|
||||
["session.activity", { sessionID, active: true }],
|
||||
["session.next.text.started", expect.objectContaining({ sessionID })],
|
||||
["session.next.text.ended", expect.objectContaining({ sessionID })],
|
||||
["session.activity", { sessionID, active: false }],
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("terminates the Session stream when live event buffering saturates", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const initial = yield* Deferred.make<void>()
|
||||
const release = yield* Deferred.make<void>()
|
||||
let seenInitial = false
|
||||
const streamed = yield* session.events({ sessionID }).pipe(
|
||||
Stream.mapEffect((event) => {
|
||||
if (!seenInitial) {
|
||||
seenInitial = true
|
||||
return Deferred.succeed(initial, undefined).pipe(Effect.as(event))
|
||||
}
|
||||
return Deferred.await(release).pipe(Effect.as(event))
|
||||
}),
|
||||
Stream.runCollect,
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* Deferred.await(initial)
|
||||
|
||||
const timestamp = yield* DateTime.now
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
yield* Effect.forEach(
|
||||
Array.from({ length: 1_000 }, (_, index) => index),
|
||||
(index) =>
|
||||
events.publish(SessionEvent.Text.Delta, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
textID: "text-saturation",
|
||||
delta: String(index),
|
||||
timestamp,
|
||||
}),
|
||||
{ concurrency: "unbounded", discard: true },
|
||||
)
|
||||
yield* Deferred.succeed(release, undefined)
|
||||
|
||||
const result = Array.from(yield* Fiber.join(streamed))
|
||||
expect(result[0]?.type).toBe("session.activity")
|
||||
expect(result.length).toBeLessThan(1_001)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("resumes through a recorded message without appending another prompt", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber, Layer, Stream } from "effect"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
|
|
@ -111,12 +111,11 @@ describe("SessionRunCoordinator", () => {
|
|||
|
||||
const run = yield* coordinator.run("session").pipe(Effect.forkChild)
|
||||
yield* Deferred.await(started)
|
||||
expect(yield* activity.snapshot).toBeTrue()
|
||||
|
||||
const inactive = yield* activity.changes.pipe(Stream.take(1), Stream.runCollect, Effect.forkChild)
|
||||
const transitions = new Array<boolean>()
|
||||
expect(yield* activity.attach((active) => transitions.push(active))).toBeTrue()
|
||||
yield* Deferred.succeed(gate, undefined)
|
||||
yield* Fiber.join(run)
|
||||
expect(Array.from(yield* Fiber.join(inactive))).toEqual([false])
|
||||
expect(transitions).toEqual([false])
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue