From ce8244be53ab899796cc1540b0e54643c89ec1e1 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 9 Apr 2026 15:47:09 -0400 Subject: [PATCH 1/2] refactor(effect): extract session run state service --- packages/opencode/src/project/vcs.ts | 62 +++++----- packages/opencode/src/session/prompt.ts | 61 ++-------- packages/opencode/src/session/revert.ts | 10 +- packages/opencode/src/session/run-state.ts | 106 ++++++++++++++++++ .../test/session/prompt-effect.test.ts | 3 + .../test/session/snapshot-tool-race.test.ts | 3 + 6 files changed, 156 insertions(+), 89 deletions(-) create mode 100644 packages/opencode/src/session/run-state.ts diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index ec6e415c8207..d31dff6a9797 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -161,39 +161,37 @@ export namespace Vcs { const bus = yield* Bus.Service const state = yield* InstanceState.make( - Effect.fn("Vcs.state")((ctx) => - Effect.gen(function* () { - if (ctx.project.vcs !== "git") { - return { current: undefined, root: undefined } - } - - const get = Effect.fnUntraced(function* () { - return yield* git.branch(ctx.directory) - }) - const [current, root] = yield* Effect.all([git.branch(ctx.directory), git.defaultBranch(ctx.directory)], { - concurrency: 2, - }) - const value = { current, root } - log.info("initialized", { branch: value.current, default_branch: value.root?.name }) - - yield* bus.subscribe(FileWatcher.Event.Updated).pipe( - Stream.filter((evt) => evt.properties.file.endsWith("HEAD")), - Stream.runForEach((_evt) => - Effect.gen(function* () { - const next = yield* get() - if (next !== value.current) { - log.info("branch changed", { from: value.current, to: next }) - value.current = next - yield* bus.publish(Event.BranchUpdated, { branch: next }) - } - }), - ), - Effect.forkScoped, - ) + Effect.fn("Vcs.state")(function* (ctx) { + if (ctx.project.vcs !== "git") { + return { current: undefined, root: undefined } + } - return value - }), - ), + const get = Effect.fnUntraced(function* () { + return yield* git.branch(ctx.directory) + }) + const [current, root] = yield* Effect.all([git.branch(ctx.directory), git.defaultBranch(ctx.directory)], { + concurrency: 2, + }) + const value = { current, root } + log.info("initialized", { branch: value.current, default_branch: value.root?.name }) + + yield* bus.subscribe(FileWatcher.Event.Updated).pipe( + Stream.filter((evt) => evt.properties.file.endsWith("HEAD")), + Stream.runForEach((_evt) => + Effect.gen(function* () { + const next = yield* get() + if (next !== value.current) { + log.info("branch changed", { from: value.current, to: next }) + value.current = next + yield* bus.publish(Event.BranchUpdated, { branch: next }) + } + }), + ), + Effect.forkScoped, + ) + + return value + }), ) return Service.of({ diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 19f0850ff4c2..76319b202c4a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -20,7 +20,6 @@ import PROMPT_PLAN from "../session/prompt/plan.txt" import BUILD_SWITCH from "../session/prompt/build-switch.txt" import MAX_STEPS from "../session/prompt/max-steps.txt" import { ToolRegistry } from "../tool/registry" -import { Runner } from "@/effect/runner" import { MCP } from "../mcp" import { LSP } from "../lsp" import { FileTime } from "../file/time" @@ -48,6 +47,7 @@ import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" import { TaskTool } from "@/tool/task" +import { SessionRunState } from "./run-state" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -99,55 +99,11 @@ export namespace SessionPrompt { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const scope = yield* Scope.Scope const instruction = yield* Instruction.Service - - const state = yield* InstanceState.make( - Effect.fn("SessionPrompt.state")(function* () { - const runners = new Map>() - yield* Effect.addFinalizer( - Effect.fnUntraced(function* () { - yield* Effect.forEach(runners.values(), (r) => r.cancel, { concurrency: "unbounded", discard: true }) - runners.clear() - }), - ) - return { runners } - }), - ) - - const getRunner = (runners: Map>, sessionID: SessionID) => { - const existing = runners.get(sessionID) - if (existing) return existing - const runner = Runner.make(scope, { - onIdle: Effect.gen(function* () { - runners.delete(sessionID) - yield* status.set(sessionID, { type: "idle" }) - }), - onBusy: status.set(sessionID, { type: "busy" }), - onInterrupt: lastAssistant(sessionID), - busy: () => { - throw new Session.BusyError(sessionID) - }, - }) - runners.set(sessionID, runner) - return runner - } - - const assertNotBusy: (sessionID: SessionID) => Effect.Effect = Effect.fn( - "SessionPrompt.assertNotBusy", - )(function* (sessionID: SessionID) { - const s = yield* InstanceState.get(state) - const runner = s.runners.get(sessionID) - if (runner?.busy) throw new Session.BusyError(sessionID) - }) + const state = yield* SessionRunState.Service const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) { log.info("cancel", { sessionID }) - const s = yield* InstanceState.get(state) - const runner = s.runners.get(sessionID) - if (!runner || !runner.busy) { - yield* status.set(sessionID, { type: "idle" }) - return - } - yield* runner.cancel + yield* state.cancel(sessionID) }) const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) { @@ -1568,16 +1524,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the const loop: (input: z.infer) => Effect.Effect = Effect.fn( "SessionPrompt.loop", )(function* (input: z.infer) { - const s = yield* InstanceState.get(state) - const runner = getRunner(s.runners, input.sessionID) - return yield* runner.ensureRunning(runLoop(input.sessionID)) + return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID)) }) const shell: (input: ShellInput) => Effect.Effect = Effect.fn("SessionPrompt.shell")( function* (input: ShellInput) { - const s = yield* InstanceState.get(state) - const runner = getRunner(s.runners, input.sessionID) - return yield* runner.startShell(shellImpl(input)) + return yield* state.startShell(input.sessionID, lastAssistant(input.sessionID), shellImpl(input)) }, ) @@ -1698,7 +1650,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the }) return Service.of({ - assertNotBusy, + assertNotBusy: state.assertNotBusy, cancel, prompt, loop, @@ -1712,6 +1664,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const defaultLayer = Layer.unwrap( Effect.sync(() => layer.pipe( + Layer.provide(SessionRunState.layer), Layer.provide(SessionStatus.layer), Layer.provide(SessionCompaction.defaultLayer), Layer.provide(SessionProcessor.defaultLayer), diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 9df3f36eb8c7..a2d517f77c0a 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -9,8 +9,9 @@ import { Log } from "../util/log" import { Session } from "." import { MessageV2 } from "./message-v2" import { SessionID, MessageID, PartID } from "./schema" -import { SessionPrompt } from "./prompt" +import { SessionRunState } from "./run-state" import { SessionSummary } from "./summary" +import { SessionStatus } from "./status" export namespace SessionRevert { const log = Log.create({ service: "session.revert" }) @@ -38,9 +39,10 @@ export namespace SessionRevert { const storage = yield* Storage.Service const bus = yield* Bus.Service const summary = yield* SessionSummary.Service + const state = yield* SessionRunState.Service const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) { - yield* Effect.promise(() => SessionPrompt.assertNotBusy(input.sessionID)) + yield* state.assertNotBusy(input.sessionID) const all = yield* sessions.messages({ sessionID: input.sessionID }) let lastUser: MessageV2.User | undefined const session = yield* sessions.get(input.sessionID) @@ -93,7 +95,7 @@ export namespace SessionRevert { const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) { log.info("unreverting", input) - yield* Effect.promise(() => SessionPrompt.assertNotBusy(input.sessionID)) + yield* state.assertNotBusy(input.sessionID) const session = yield* sessions.get(input.sessionID) if (!session.revert) return session if (session.revert.snapshot) yield* snap.restore(session.revert!.snapshot!) @@ -151,6 +153,8 @@ export namespace SessionRevert { export const defaultLayer = Layer.unwrap( Effect.sync(() => layer.pipe( + Layer.provide(SessionRunState.layer), + Layer.provide(SessionStatus.layer), Layer.provide(Session.defaultLayer), Layer.provide(Snapshot.defaultLayer), Layer.provide(Storage.defaultLayer), diff --git a/packages/opencode/src/session/run-state.ts b/packages/opencode/src/session/run-state.ts new file mode 100644 index 000000000000..051632c474f8 --- /dev/null +++ b/packages/opencode/src/session/run-state.ts @@ -0,0 +1,106 @@ +import { InstanceState } from "@/effect/instance-state" +import { Runner } from "@/effect/runner" +import { Effect, Layer, Scope, ServiceMap } from "effect" +import { Session } from "." +import { MessageV2 } from "./message-v2" +import { SessionID } from "./schema" +import { SessionStatus } from "./status" + +export namespace SessionRunState { + export interface Interface { + readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect + readonly cancel: (sessionID: SessionID) => Effect.Effect + readonly ensureRunning: ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + work: Effect.Effect, + ) => Effect.Effect + readonly startShell: ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + work: Effect.Effect, + ) => Effect.Effect + } + + export class Service extends ServiceMap.Service()("@opencode/SessionRunState") {} + + export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const status = yield* SessionStatus.Service + + const state = yield* InstanceState.make( + Effect.fn("SessionRunState.state")(function* () { + const scope = yield* Scope.Scope + const runners = new Map>() + yield* Effect.addFinalizer( + Effect.fnUntraced(function* () { + yield* Effect.forEach(runners.values(), (runner) => runner.cancel, { + concurrency: "unbounded", + discard: true, + }) + runners.clear() + }), + ) + return { runners, scope } + }), + ) + + const runner = Effect.fn("SessionRunState.runner")(function* ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + ) { + const data = yield* InstanceState.get(state) + const existing = data.runners.get(sessionID) + if (existing) return existing + const next = Runner.make(data.scope, { + onIdle: Effect.gen(function* () { + data.runners.delete(sessionID) + yield* status.set(sessionID, { type: "idle" }) + }), + onBusy: status.set(sessionID, { type: "busy" }), + onInterrupt, + busy: () => { + throw new Session.BusyError(sessionID) + }, + }) + data.runners.set(sessionID, next) + return next + }) + + const assertNotBusy = Effect.fn("SessionRunState.assertNotBusy")(function* (sessionID: SessionID) { + const data = yield* InstanceState.get(state) + const existing = data.runners.get(sessionID) + if (existing?.busy) throw new Session.BusyError(sessionID) + }) + + const cancel = Effect.fn("SessionRunState.cancel")(function* (sessionID: SessionID) { + const data = yield* InstanceState.get(state) + const existing = data.runners.get(sessionID) + if (!existing || !existing.busy) { + yield* status.set(sessionID, { type: "idle" }) + return + } + yield* existing.cancel + }) + + const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + work: Effect.Effect, + ) { + return yield* (yield* runner(sessionID, onInterrupt)).ensureRunning(work) + }) + + const startShell = Effect.fn("SessionRunState.startShell")(function* ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + work: Effect.Effect, + ) { + return yield* (yield* runner(sessionID, onInterrupt)).startShell(work) + }) + + return Service.of({ assertNotBusy, cancel, ensureRunning, startShell }) + }), + ) +} diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 38d7ed9f5aca..5f3d5c88e28f 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -25,6 +25,7 @@ import { SessionCompaction } from "../../src/session/compaction" import { Instruction } from "../../src/session/instruction" import { SessionProcessor } from "../../src/session/processor" import { SessionPrompt } from "../../src/session/prompt" +import { SessionRunState } from "../../src/session/run-state" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { Shell } from "../../src/shell/shell" @@ -143,6 +144,7 @@ const filetime = Layer.succeed( ) const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer)) +const run = SessionRunState.layer.pipe(Layer.provide(status)) const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer) function makeHttp() { const deps = Layer.mergeAll( @@ -174,6 +176,7 @@ function makeHttp() { return Layer.mergeAll( TestLLMServer.layer, SessionPrompt.layer.pipe( + Layer.provideMerge(run), Layer.provideMerge(compact), Layer.provideMerge(proc), Layer.provideMerge(registry), diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index c192a446bd49..9cc4d750c2c1 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -43,6 +43,7 @@ import { Todo } from "../../src/session/todo" import { SessionCompaction } from "../../src/session/compaction" import { Instruction } from "../../src/session/instruction" import { SessionProcessor } from "../../src/session/processor" +import { SessionRunState } from "../../src/session/run-state" import { SessionStatus } from "../../src/session/status" import { Shell } from "../../src/shell/shell" import { Snapshot } from "../../src/snapshot" @@ -107,6 +108,7 @@ const filetime = Layer.succeed( ) const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer)) +const run = SessionRunState.layer.pipe(Layer.provide(status)) const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer) function makeHttp() { @@ -139,6 +141,7 @@ function makeHttp() { return Layer.mergeAll( TestLLMServer.layer, SessionPrompt.layer.pipe( + Layer.provideMerge(run), Layer.provideMerge(compact), Layer.provideMerge(proc), Layer.provideMerge(registry), From 217a8e05cace003fdcfd95f41f82a8b0694a2837 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 9 Apr 2026 15:55:01 -0400 Subject: [PATCH 2/2] refactor(effect): move busy checks to session run state --- .../opencode/src/server/routes/session.ts | 3 +- packages/opencode/src/session/prompt.ts | 6 ---- packages/opencode/src/session/run-state.ts | 8 +++++ packages/opencode/src/session/status.ts | 2 +- .../test/server/session-actions.test.ts | 3 +- .../test/session/prompt-effect.test.ts | 34 ++++++++++--------- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index b57ed9d47c0b..fb0b6b69c109 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -6,6 +6,7 @@ import z from "zod" import { Session } from "../../session" import { MessageV2 } from "../../session/message-v2" import { SessionPrompt } from "../../session/prompt" +import { SessionRunState } from "@/session/run-state" import { SessionCompaction } from "../../session/compaction" import { SessionRevert } from "../../session/revert" import { SessionStatus } from "@/session/status" @@ -698,7 +699,7 @@ export const SessionRoutes = lazy(() => ), async (c) => { const params = c.req.valid("param") - await SessionPrompt.assertNotBusy(params.sessionID) + await SessionRunState.assertNotBusy(params.sessionID) await Session.removeMessage({ sessionID: params.sessionID, messageID: params.messageID, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 76319b202c4a..c297b59c08eb 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -66,7 +66,6 @@ export namespace SessionPrompt { const log = Log.create({ service: "session.prompt" }) export interface Interface { - readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect readonly cancel: (sessionID: SessionID) => Effect.Effect readonly prompt: (input: PromptInput) => Effect.Effect readonly loop: (input: z.infer) => Effect.Effect @@ -1650,7 +1649,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the }) return Service.of({ - assertNotBusy: state.assertNotBusy, cancel, prompt, loop, @@ -1688,10 +1686,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the ) const { runPromise } = makeRuntime(Service, defaultLayer) - export async function assertNotBusy(sessionID: SessionID) { - return runPromise((svc) => svc.assertNotBusy(SessionID.zod.parse(sessionID))) - } - export const PromptInput = z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional(), diff --git a/packages/opencode/src/session/run-state.ts b/packages/opencode/src/session/run-state.ts index 051632c474f8..3c2022bd00bf 100644 --- a/packages/opencode/src/session/run-state.ts +++ b/packages/opencode/src/session/run-state.ts @@ -1,5 +1,6 @@ import { InstanceState } from "@/effect/instance-state" import { Runner } from "@/effect/runner" +import { makeRuntime } from "@/effect/run-service" import { Effect, Layer, Scope, ServiceMap } from "effect" import { Session } from "." import { MessageV2 } from "./message-v2" @@ -103,4 +104,11 @@ export namespace SessionRunState { return Service.of({ assertNotBusy, cancel, ensureRunning, startShell }) }), ) + + export const defaultLayer = layer.pipe(Layer.provide(SessionStatus.defaultLayer)) + const { runPromise } = makeRuntime(Service, defaultLayer) + + export async function assertNotBusy(sessionID: SessionID) { + return runPromise((svc) => svc.assertNotBusy(sessionID)) + } } diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index 34a79eed112c..16fccaf3e831 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -85,7 +85,7 @@ export namespace SessionStatus { }), ) - const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) + export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) const { runPromise } = makeRuntime(Service, defaultLayer) export async function get(sessionID: SessionID) { diff --git a/packages/opencode/test/server/session-actions.test.ts b/packages/opencode/test/server/session-actions.test.ts index 004c2900a208..4ab485965ea2 100644 --- a/packages/opencode/test/server/session-actions.test.ts +++ b/packages/opencode/test/server/session-actions.test.ts @@ -5,6 +5,7 @@ import { Session } from "../../src/session" import { ModelID, ProviderID } from "../../src/provider/schema" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { SessionPrompt } from "../../src/session/prompt" +import { SessionRunState } from "../../src/session/run-state" import { Log } from "../../src/util/log" import { tmpdir } from "../fixture/fixture" @@ -64,7 +65,7 @@ describe("session action routes", () => { fn: async () => { const session = await Session.create({}) const msg = await user(session.id, "hello") - const busy = spyOn(SessionPrompt, "assertNotBusy").mockRejectedValue(new Session.BusyError(session.id)) + const busy = spyOn(SessionRunState, "assertNotBusy").mockRejectedValue(new Session.BusyError(session.id)) const remove = spyOn(Session, "removeMessage").mockResolvedValue(msg.id) const app = Server.Default().app diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 5f3d5c88e28f..45461d5ced4d 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -303,9 +303,10 @@ const addSubtask = (sessionID: SessionID, messageID: MessageID, model = ref) => const boot = Effect.fn("test.boot")(function* (input?: { title?: string }) { const prompt = yield* SessionPrompt.Service + const run = yield* SessionRunState.Service const sessions = yield* Session.Service const chat = yield* sessions.create(input ?? { title: "Pinned" }) - return { prompt, sessions, chat } + return { prompt, run, sessions, chat } }) // Loop semantics @@ -716,7 +717,7 @@ it.live("concurrent loop callers get same result", () => provideTmpdirInstance( (dir) => Effect.gen(function* () { - const { prompt, chat } = yield* boot() + const { prompt, run, chat } = yield* boot() yield* seed(chat.id, { finish: "stop" }) const [a, b] = yield* Effect.all([prompt.loop({ sessionID: chat.id }), prompt.loop({ sessionID: chat.id })], { @@ -725,7 +726,7 @@ it.live("concurrent loop callers get same result", () => expect(a.info.id).toBe(b.info.id) expect(a.info.role).toBe("assistant") - yield* prompt.assertNotBusy(chat.id) + yield* run.assertNotBusy(chat.id) }), { git: true }, ), @@ -829,6 +830,7 @@ it.live( provideTmpdirServer( Effect.fnUntraced(function* ({ llm }) { const prompt = yield* SessionPrompt.Service + const run = yield* SessionRunState.Service const sessions = yield* Session.Service yield* llm.hang @@ -838,7 +840,7 @@ it.live( const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) yield* llm.wait(1) - const exit = yield* prompt.assertNotBusy(chat.id).pipe(Effect.exit) + const exit = yield* run.assertNotBusy(chat.id).pipe(Effect.exit) expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit)) { expect(Cause.squash(exit.cause)).toBeInstanceOf(Session.BusyError) @@ -856,11 +858,11 @@ it.live("assertNotBusy succeeds when idle", () => provideTmpdirInstance( (dir) => Effect.gen(function* () { - const prompt = yield* SessionPrompt.Service + const run = yield* SessionRunState.Service const sessions = yield* Session.Service const chat = yield* sessions.create({}) - const exit = yield* prompt.assertNotBusy(chat.id).pipe(Effect.exit) + const exit = yield* run.assertNotBusy(chat.id).pipe(Effect.exit) expect(Exit.isSuccess(exit)).toBe(true) }), { git: true }, @@ -901,7 +903,7 @@ unix("shell captures stdout and stderr in completed tool output", () => provideTmpdirInstance( (dir) => Effect.gen(function* () { - const { prompt, chat } = yield* boot() + const { prompt, run, chat } = yield* boot() const result = yield* prompt.shell({ sessionID: chat.id, agent: "build", @@ -916,7 +918,7 @@ unix("shell captures stdout and stderr in completed tool output", () => expect(tool.state.output).toContain("err") expect(tool.state.metadata.output).toContain("out") expect(tool.state.metadata.output).toContain("err") - yield* prompt.assertNotBusy(chat.id) + yield* run.assertNotBusy(chat.id) }), { git: true, config: cfg }, ), @@ -926,7 +928,7 @@ unix("shell completes a fast command on the preferred shell", () => provideTmpdirInstance( (dir) => Effect.gen(function* () { - const { prompt, chat } = yield* boot() + const { prompt, run, chat } = yield* boot() const result = yield* prompt.shell({ sessionID: chat.id, agent: "build", @@ -940,7 +942,7 @@ unix("shell completes a fast command on the preferred shell", () => expect(tool.state.input.command).toBe("pwd") expect(tool.state.output).toContain(dir) expect(tool.state.metadata.output).toContain(dir) - yield* prompt.assertNotBusy(chat.id) + yield* run.assertNotBusy(chat.id) }), { git: true, config: cfg }, ), @@ -950,7 +952,7 @@ unix("shell lists files from the project directory", () => provideTmpdirInstance( (dir) => Effect.gen(function* () { - const { prompt, chat } = yield* boot() + const { prompt, run, chat } = yield* boot() yield* Effect.promise(() => Bun.write(path.join(dir, "README.md"), "# e2e\n")) const result = yield* prompt.shell({ @@ -966,7 +968,7 @@ unix("shell lists files from the project directory", () => expect(tool.state.input.command).toBe("command ls") expect(tool.state.output).toContain("README.md") expect(tool.state.metadata.output).toContain("README.md") - yield* prompt.assertNotBusy(chat.id) + yield* run.assertNotBusy(chat.id) }), { git: true, config: cfg }, ), @@ -976,7 +978,7 @@ unix("shell captures stderr from a failing command", () => provideTmpdirInstance( (dir) => Effect.gen(function* () { - const { prompt, chat } = yield* boot() + const { prompt, run, chat } = yield* boot() const result = yield* prompt.shell({ sessionID: chat.id, agent: "build", @@ -989,7 +991,7 @@ unix("shell captures stderr from a failing command", () => expect(tool.state.output).toContain("not found") expect(tool.state.metadata.output).toContain("not found") - yield* prompt.assertNotBusy(chat.id) + yield* run.assertNotBusy(chat.id) }), { git: true, config: cfg }, ), @@ -1114,7 +1116,7 @@ unix( provideTmpdirInstance( (dir) => Effect.gen(function* () { - const { prompt, chat } = yield* boot() + const { prompt, run, chat } = yield* boot() const sh = yield* prompt .shell({ sessionID: chat.id, agent: "build", command: "sleep 30" }) @@ -1125,7 +1127,7 @@ unix( const status = yield* SessionStatus.Service expect((yield* status.get(chat.id)).type).toBe("idle") - const busy = yield* prompt.assertNotBusy(chat.id).pipe(Effect.exit) + const busy = yield* run.assertNotBusy(chat.id).pipe(Effect.exit) expect(Exit.isSuccess(busy)).toBe(true) const exit = yield* Fiber.await(sh)