Merge commit '8a5231ec26' into session-event-stream

This commit is contained in:
Kit Langton 2026-06-25 23:10:04 -04:00
commit 96fbcd7747
18 changed files with 186 additions and 68 deletions

View file

@ -5,6 +5,8 @@ import { SessionRunner } from "./runner/index"
import { SessionSchema } from "./schema"
export interface Interface {
/** Snapshots active execution owned by this process. */
readonly active: Effect.Effect<ReadonlySet<SessionSchema.ID>>
/** Starts execution while idle or joins the active execution. */
readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, SessionRunner.RunError>
/** Registers newly recorded work. Repeated wakeups may coalesce. */
@ -19,5 +21,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
/** Low-level compatibility layer for callers that only need durable Session recording. */
export const noopLayer = Layer.succeed(
Service,
Service.of({ resume: () => Effect.void, wake: () => Effect.void, interrupt: () => Effect.void }),
Service.of({
active: Effect.succeed(new Set()),
resume: () => Effect.void,
wake: () => Effect.void,
interrupt: () => Effect.void,
}),
)

View file

@ -28,6 +28,7 @@ export const layer = Layer.effect(
})
return SessionExecution.Service.of({
active: coordinator.active,
interrupt: coordinator.interrupt,
resume: coordinator.run,
wake: coordinator.wake,

View file

@ -4,6 +4,8 @@ import { Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
/** Serializes execution for each key while allowing different keys to run concurrently. */
export interface Coordinator<Key, E> {
/** Snapshots keys with an execution owned by this coordinator. */
readonly active: Effect.Effect<ReadonlySet<Key>>
/** Starts execution while idle or joins the active execution. */
readonly run: (key: Key) => Effect.Effect<void, E>
/** Registers one coalesced follow-up after newly recorded work. */
@ -98,5 +100,5 @@ export const make = <Key, E>(options: {
return Fiber.interrupt(entry.owner)
})
return { run, wake, interrupt }
return { active: Effect.sync(() => new Set(active.keys())), run, wake, interrupt }
})

View file

@ -25,6 +25,7 @@ const wakeCalls: SessionV2.ID[] = []
const execution = Layer.succeed(
SessionExecution.Service,
SessionExecution.Service.of({
active: Effect.succeed(new Set()),
resume: (sessionID) =>
Effect.sync(() => {
executionCalls.push(sessionID)

View file

@ -66,6 +66,39 @@ describe("SessionRunCoordinator", () => {
),
)
it.effect("snapshots only active executions", () =>
Effect.scoped(
Effect.gen(function* () {
const firstStarted = yield* Deferred.make<void>()
const secondStarted = yield* Deferred.make<void>()
const firstGate = yield* Deferred.make<void>()
const secondGate = yield* Deferred.make<void>()
const coordinator = yield* SessionRunCoordinator.make({
drain: (key: string) =>
Deferred.succeed(key === "first" ? firstStarted : secondStarted, undefined).pipe(
Effect.andThen(Deferred.await(key === "first" ? firstGate : secondGate)),
),
})
expect(Array.from(yield* coordinator.active)).toEqual([])
const first = yield* coordinator.run("first").pipe(Effect.forkChild)
yield* Deferred.await(firstStarted)
expect(Array.from(yield* coordinator.active)).toEqual(["first"])
const second = yield* coordinator.run("second").pipe(Effect.forkChild)
yield* Deferred.await(secondStarted)
expect(Array.from(yield* coordinator.active)).toEqual(["first", "second"])
yield* Deferred.succeed(firstGate, undefined)
yield* Fiber.join(first)
expect(Array.from(yield* coordinator.active)).toEqual(["second"])
yield* Deferred.succeed(secondGate, undefined)
yield* Fiber.join(second)
expect(Array.from(yield* coordinator.active)).toEqual([])
}),
),
)
it.effect("coalesces wakes received during active execution", () =>
Effect.scoped(
Effect.gen(function* () {

View file

@ -95,6 +95,7 @@ const execution = Layer.effect(
drain: (sessionID, force) => sessionRunner.run({ sessionID, force }),
})
return SessionExecution.Service.of({
active: coordinator.active,
resume: coordinator.run,
wake: coordinator.wake,
interrupt: coordinator.interrupt,

View file

@ -254,6 +254,7 @@ const execution = Layer.effect(
drain: (sessionID, force) => sessionRunner.run({ sessionID, force }),
})
return SessionExecution.Service.of({
active: coordinator.active,
resume: coordinator.run,
wake: coordinator.wake,
interrupt: coordinator.interrupt,