From 6dd1733bbfb79319cde73c8fdd4bfdc626cc31c8 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 31 Aug 2026 20:12:47 -0400 Subject: [PATCH] fix(core): preserve continuation across chained moves Carry unfinished model work across consecutive Location handoffs without resetting the logical step allowance. Keep idle moves and queued prompt admission unchanged. Cover steered and queued second moves, preserved tool history, and durable event ordering. --- packages/core/src/session/runner/llm.ts | 2 +- packages/core/test/session-runner.test.ts | 67 ++++++++++++++--------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index b35308626c9..c9622e5ee45 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -96,7 +96,7 @@ const layer = Layer.effect( step = 1 } if (pending?.type === "move") - return DrainResult.Moved({ continuation: !entering && continuing ? { step } : undefined }) + return DrainResult.Moved({ continuation: continuing ? { step } : undefined }) if (pending?.type === "compaction") { const session = yield* store.get(sessionID) if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`)) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index e50ec46818b..c5a2bf30ad1 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -1454,32 +1454,49 @@ describe("SessionRunnerLLM", () => { ).toEqual([Bus.versionedType(SessionEvent.Moved.type, 1), Bus.versionedType(SessionEvent.InboxDelivered.type, 1)]) }) - scenario("preserves a tool continuation across a steered move", function* (s) { - yield* s.admit("Echo before moving") - yield* s.llm.push(TestLLM.tool("call-move", "echo", { text: "moving" }), TestLLM.text("Done", "text-after-move")) - const tools = yield* s.blockTools() - const run = yield* s.resume.pipe(Effect.forkChild) - yield* tools.started - yield* s.sessionInbox.admit({ - id: SessionMessage.ID.create(), - sessionID, - item: { - type: "move", - payload: { - location: Location.Ref.make({ directory: AbsolutePath.make("/project") }), - projectID: Project.ID.global, - }, - delivery: "steer", - }, + for (const delivery of ["steer", "queue"] as const) { + scenario(`preserves a tool continuation and step allowance across chained moves (${delivery})`, function* (s) { + const agents = yield* Agent.Service + yield* agents.transform((editor) => + editor.update(Agent.ID.make("build"), (agent) => { + agent.steps = 2 + }), + ) + yield* s.admit("Echo before moving") + yield* s.llm.push(TestLLM.tool("call-move", "echo", { text: "moving" }), TestLLM.text("Done", "text-after-move")) + const tools = yield* s.blockTools() + const run = yield* s.resume.pipe(Effect.forkChild) + yield* tools.started + yield* Effect.forEach(["steer", delivery] as const, (delivery) => + s.sessionInbox.admit({ + id: SessionMessage.ID.create(), + sessionID, + item: { + type: "move", + payload: { + location: Location.Ref.make({ directory: AbsolutePath.make("/project") }), + projectID: Project.ID.global, + }, + delivery, + }, + }), + ) + + yield* tools.release + yield* Fiber.join(run) + + expect(s.requests).toHaveLength(2) + expect(messageRoles(s.requests[1])?.slice(0, 3)).toEqual(["user", "assistant", "tool"]) + expect(s.requests[0]?.toolChoice).toBeUndefined() + expect(s.requests[1]?.toolChoice).toMatchObject({ type: "none" }) + expect( + (yield* recordedEventTypes(sessionID)).filter( + (type) => type === "session.step.started.1" || type === "session.moved.1", + ), + ).toEqual(["session.step.started.1", "session.moved.1", "session.moved.1", "session.step.started.1"]) + expect(yield* s.inbox).toEqual([]) }) - - yield* tools.release - yield* Fiber.join(run) - - expect(s.requests).toHaveLength(2) - expect(s.requests.map(messageRoles).at(1)?.slice(0, 3)).toEqual(["user", "assistant", "tool"]) - expect(yield* s.inbox).toEqual([]) - }) + } scenario("keeps queued input parked across a mid-turn move", function* (s) { yield* s.admit("Echo before moving")