diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 4282d6771a..ea18ed3909 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -637,7 +637,7 @@ const layer = Layer.effect( compact: Effect.fn("V2Session.compact")(function* (input) { yield* result.get(input.sessionID) const inputID = input.id ?? SessionMessage.ID.create() - const admission = yield* SessionInput.admitCompaction(db, events, { + const admitted = yield* SessionInput.admitCompaction(db, events, { id: inputID, sessionID: input.sessionID, }).pipe( @@ -647,9 +647,8 @@ const layer = Layer.effect( : Effect.die(defect), ), ) - if (admission.newlyAdmitted) yield* execution.interrupt(input.sessionID) yield* execution.wake(input.sessionID) - return admission.entry + return admitted }), wait: Effect.fn("V2Session.wait")(function* (sessionID) { yield* result.get(sessionID) diff --git a/packages/core/src/session/input.ts b/packages/core/src/session/input.ts index bfc0095a95..c08439d0c1 100644 --- a/packages/core/src/session/input.ts +++ b/packages/core/src/session/input.ts @@ -142,12 +142,11 @@ export const admitCompaction = Effect.fn("SessionInput.admitCompaction")(functio Effect.gen(function* () { const exact = yield* find(db, input.id) if (exact) { - if (exact.type === "compaction" && exact.sessionID === input.sessionID) - return { entry: exact, newlyAdmitted: false } + if (exact.type === "compaction" && exact.sessionID === input.sessionID) return exact return yield* Effect.die(new LifecycleConflict({ id: input.id })) } const pending = yield* pendingCompaction(db, input.sessionID) - if (pending) return { entry: pending, newlyAdmitted: false } + if (pending) return pending return yield* events .publish(SessionEvent.Compaction.Admitted, { inputID: input.id, @@ -159,17 +158,13 @@ export const admitCompaction = Effect.fn("SessionInput.admitCompaction")(functio return Effect.die(new Error("Compaction admission event is missing aggregate sequence")) return pendingCompaction(db, input.sessionID).pipe( Effect.flatMap((stored) => - stored - ? Effect.succeed({ entry: stored, newlyAdmitted: stored.id === input.id }) - : Effect.die(new LifecycleConflict({ id: input.id })), + stored ? Effect.succeed(stored) : Effect.die(new LifecycleConflict({ id: input.id })), ), ) }), Effect.catchDefect((defect) => pendingCompaction(db, input.sessionID).pipe( - Effect.flatMap((stored) => - stored ? Effect.succeed({ entry: stored, newlyAdmitted: false }) : Effect.die(defect), - ), + Effect.flatMap((stored) => (stored ? Effect.succeed(stored) : Effect.die(defect))), ), ), ) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 23cbfa049d..3bf12caaeb 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -624,13 +624,10 @@ const layer = Layer.effect( } needsContinuation = result.needsContinuation step = result.step + 1 - if (needsContinuation) { - promotion = (yield* SessionInput.pendingCompaction(db, input.sessionID)) ? undefined : "steer" - continue - } + // Manual compaction is a barrier: handle it before continuing or promoting steers. yield* runPendingCompaction(input.sessionID) promotion = "steer" - needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, "steer") + if (!needsContinuation) needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, "steer") } yield* runPendingCompaction(input.sessionID) const hasSteer = yield* SessionInput.hasPending(db, input.sessionID, "steer") diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 487577a9f4..24d314182f 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -1346,16 +1346,22 @@ describe("SessionRunnerLLM", () => { }), ) - it.effect("interrupts active work to run one durable compaction barrier before later prompts", () => + it.effect("runs compaction at the next step boundary before continuation, steer, and queue", () => Effect.gen(function* () { yield* setup requests.length = 0 + executions.length = 0 currentModel = recoveryModel const session = yield* SessionV2.Service streamGate = yield* Deferred.make() streamStarted = yield* Deferred.make() responses = [ - fragmentFixture("text", "text-active", ["Active complete"]).completeEvents, + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-before-compaction", name: "echo", input: { text: "before compaction" } }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], [LLMEvent.textDelta({ id: "summary", text: "durable summary" })], fragmentFixture("text", "text-steer", ["Steer complete"]).completeEvents, fragmentFixture("text", "text-queue", ["Queue complete"]).completeEvents, @@ -1365,8 +1371,12 @@ describe("SessionRunnerLLM", () => { yield* Deferred.await(streamStarted) const first = yield* session.compact({ sessionID }) - const second = yield* session.compact({ sessionID }) - expect(second.id).toBe(first.id) + const [exactRetry, coalesced] = yield* Effect.all( + [session.compact({ id: first.id, sessionID }), session.compact({ sessionID })], + { concurrency: "unbounded" }, + ) + expect(exactRetry.id).toBe(first.id) + expect(coalesced.id).toBe(first.id) expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toMatchObject({ id: first.id, }) @@ -1389,10 +1399,11 @@ describe("SessionRunnerLLM", () => { expect(yield* SessionInput.hasPending((yield* Database.Service).db, sessionID, "steer")).toBe(false) yield* Deferred.succeed(streamGate, undefined) - expect(yield* Fiber.await(active)).toMatchObject({ _tag: "Failure" }) - yield* session.wait(sessionID) + yield* Fiber.join(active) expect(requests).toHaveLength(4) + expect(executions).toEqual(["before compaction"]) + expect(userTexts(requests[0])).toContain("Active work") expect(userTexts(requests[1])[0]).toContain("Create a new anchored summary") expect(userTexts(requests[2])).toContain("Steer after compaction") expect(userTexts(requests[3])).toContain("Queue after compaction") @@ -1430,8 +1441,7 @@ describe("SessionRunnerLLM", () => { resume: false, }) yield* Deferred.succeed(streamGate, undefined) - expect(yield* Fiber.await(active)).toMatchObject({ _tag: "Failure" }) - yield* session.wait(sessionID) + yield* Fiber.join(active) expect(requests).toHaveLength(3) expect(userTexts(requests[2])).toContain("Continue after failure")