12 tests using testEffect + TestLLM pattern (no HTTP server):
- loop exits on stop finish, calls LLM for new messages, continues on tool-calls
- loop sets status busy→idle
- cancel interrupts cleanly, returns last assistant, records MessageAbortedError
- cancel with queued callers resolves all cleanly
- concurrent callers get same result via Deferred queue
- concurrent callers receive same error on failure
- assertNotBusy throws BusyError when running, succeeds when idle
- shell rejects with BusyError when loop running
Yield effectified services instead of going through async facades. Eliminates
Effect→Promise→Effect double-bounce for processor.create, processor.process,
Session.get/touch/setPermission/updateMessage, and Agent.get/list. Await cancel
in session route. Remove redundant InstanceState.get in shellE ensuring block.
Yield the effectified services in the layer and call their methods
directly, eliminating the double Effect→Promise→Effect bounce through
async facades. Layer.unwrap(Effect.sync(...)) breaks the circular
import. Also improves the assertNotBusy test with a proper gate/spy
so it deterministically catches the busy state.
Migrate SessionPrompt to the Effect service pattern (Interface, Service,
Layer, InstanceState, makeRuntime + async facades).
Key design decisions:
- Fiber-based cancellation replaces manual AbortController management.
Effect.promise((signal) => ...) derives AbortSignals automatically;
cancel() interrupts fibers and signals propagate to the AI SDK,
shell processes, and tool execution.
- Deferred queue replaces Promise callback queue. Concurrent loop()
callers get a Deferred that resolves when the running fiber finishes.
On cancel or error, queued callers now receive proper errors instead
of hanging forever.
- Separate loops/shells maps in InstanceState replace the single shared
state object, with shell-to-loop handoff preserved: if callers queue
a loop while a shell is running, shellE cleanup starts the loop.
- Heavy helper functions (createUserMessage, handleSubtask, shellImpl,
resolveCommand, insertReminders, ensureTitle) stay as plain async
functions called via Effect.promise, keeping the migration incremental.
- resolveTools and createStructuredOutputTool are unchanged (deeply tied
to AI SDK tool callbacks).