summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-09 16:03:40 -0400
committerGitHub <[email protected]>2026-04-09 16:03:40 -0400
commit10441efad1895cbaa77be3ba5277026b489894a2 (patch)
tree1aed9e7eef0705fb40ec8f0b91905940ed87688b
parent3199383eef4cc2ac4ca086f9485b071061dcff70 (diff)
downloadopencode-10441efad1895cbaa77be3ba5277026b489894a2.tar.gz
opencode-10441efad1895cbaa77be3ba5277026b489894a2.zip
refactor(effect): extract session run state service (#21744)
-rw-r--r--packages/opencode/src/project/vcs.ts62
-rw-r--r--packages/opencode/src/server/routes/session.ts3
-rw-r--r--packages/opencode/src/session/prompt.ts65
-rw-r--r--packages/opencode/src/session/revert.ts10
-rw-r--r--packages/opencode/src/session/run-state.ts114
-rw-r--r--packages/opencode/src/session/status.ts2
-rw-r--r--packages/opencode/test/server/session-actions.test.ts3
-rw-r--r--packages/opencode/test/session/prompt-effect.test.ts37
-rw-r--r--packages/opencode/test/session/snapshot-tool-race.test.ts3
9 files changed, 186 insertions, 113 deletions
diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts
index ec6e415c8..d31dff6a9 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<State>(
- 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/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts
index b57ed9d47..fb0b6b69c 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 088a367ca..7f0a014ab 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
@@ -66,7 +66,6 @@ export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" })
export interface Interface {
- readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect<void, Session.BusyError>
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts>
readonly loop: (input: z.infer<typeof LoopInput>) => Effect.Effect<MessageV2.WithParts>
@@ -99,55 +98,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<string, Runner<MessageV2.WithParts>>()
- 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<string, Runner<MessageV2.WithParts>>, sessionID: SessionID) => {
- const existing = runners.get(sessionID)
- if (existing) return existing
- const runner = Runner.make<MessageV2.WithParts>(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<void, Session.BusyError> = 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) {
@@ -1574,16 +1529,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const loop: (input: z.infer<typeof LoopInput>) => Effect.Effect<MessageV2.WithParts> = Effect.fn(
"SessionPrompt.loop",
)(function* (input: z.infer<typeof LoopInput>) {
- 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<MessageV2.WithParts> = 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))
},
)
@@ -1704,7 +1655,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
})
return Service.of({
- assertNotBusy,
cancel,
prompt,
loop,
@@ -1718,6 +1668,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),
@@ -1741,10 +1692,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/revert.ts b/packages/opencode/src/session/revert.ts
index 9df3f36eb..a2d517f77 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 000000000..3c2022bd0
--- /dev/null
+++ b/packages/opencode/src/session/run-state.ts
@@ -0,0 +1,114 @@
+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"
+import { SessionID } from "./schema"
+import { SessionStatus } from "./status"
+
+export namespace SessionRunState {
+ export interface Interface {
+ readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect<void>
+ readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
+ readonly ensureRunning: (
+ sessionID: SessionID,
+ onInterrupt: Effect.Effect<MessageV2.WithParts>,
+ work: Effect.Effect<MessageV2.WithParts>,
+ ) => Effect.Effect<MessageV2.WithParts>
+ readonly startShell: (
+ sessionID: SessionID,
+ onInterrupt: Effect.Effect<MessageV2.WithParts>,
+ work: Effect.Effect<MessageV2.WithParts>,
+ ) => Effect.Effect<MessageV2.WithParts>
+ }
+
+ export class Service extends ServiceMap.Service<Service, Interface>()("@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<SessionID, Runner<MessageV2.WithParts>>()
+ 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<MessageV2.WithParts>,
+ ) {
+ const data = yield* InstanceState.get(state)
+ const existing = data.runners.get(sessionID)
+ if (existing) return existing
+ const next = Runner.make<MessageV2.WithParts>(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<MessageV2.WithParts>,
+ work: Effect.Effect<MessageV2.WithParts>,
+ ) {
+ return yield* (yield* runner(sessionID, onInterrupt)).ensureRunning(work)
+ })
+
+ const startShell = Effect.fn("SessionRunState.startShell")(function* (
+ sessionID: SessionID,
+ onInterrupt: Effect.Effect<MessageV2.WithParts>,
+ work: Effect.Effect<MessageV2.WithParts>,
+ ) {
+ return yield* (yield* runner(sessionID, onInterrupt)).startShell(work)
+ })
+
+ 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 34a79eed1..16fccaf3e 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 004c2900a..4ab485965 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 e4c46337c..81288f0ca 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),
@@ -300,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
@@ -800,7 +804,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 })], {
@@ -809,7 +813,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 },
),
@@ -913,6 +917,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
@@ -922,7 +927,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)
@@ -940,11 +945,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 },
@@ -985,7 +990,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",
@@ -1000,7 +1005,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 },
),
@@ -1010,7 +1015,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",
@@ -1024,7 +1029,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 },
),
@@ -1034,7 +1039,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({
@@ -1050,7 +1055,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 },
),
@@ -1060,7 +1065,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",
@@ -1073,7 +1078,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 },
),
@@ -1198,7 +1203,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" })
@@ -1209,7 +1214,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)
diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts
index c192a446b..9cc4d750c 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),