Skip to content

httpapi-exercise effect mode: session.prompt_async/session.command hang, session.prompt gets wrong LLM text #205

Closed

Description

@alltomatos

Summary

Three `bun run test:httpapi` (effect mode) scenarios added in #203 have never been reliably green:

  • `session.prompt` (POST /session/{sessionID}/message) — intermittently fails with `assistant message should use fake LLM text`
  • `session.prompt_async` (POST /session/{sessionID}/prompt_async) — `scenario timed out after 30s`
  • `session.command` (POST /session/{sessionID}/command) — `scenario timed out after 30s`

Root cause

This is a test-harness bug, not an app regression.

`test/server/httpapi-exercise/runner.ts`'s `withContext` builds the shared `AppLayer` via `Layer.buildWithMemoMap(modules.AppLayer, modules.memoMap, scope)`, where `modules.memoMap` is a single `Layer.MemoMap` created once for the whole exerciser process (intentionally, for speed — the app is built once and reused across all 245+ scenarios).

However, the `scope` passed alongside it was obtained fresh per scenario (`yield* Scope.Scope` inside each scenario's own `Effect.scoped` in `runScenario`). Because the memo map only builds the layer once and caches its output, the very first scenario to trigger construction "wins": every service inside `AppLayer` that does `const scope = yield* Scope.Scope` at layer-build time to fork long-lived background work (e.g. `SessionPrompt`'s title-generation and auto-summarize forks, and the `session.ts` httpapi handler's `promptAsync` fire-and-forget fork) permanently captures that first scenario's scope.

The instant that first scenario's own `Effect.scoped` finishes and closes its scope, every later `Effect.forkIn(scope, ...)` anywhere in the app — across every subsequent scenario in the same process — forks into an already-closed scope. In Effect, that means the forked fiber is immediately interrupted and never actually runs:

  • `session.command` and `session.prompt_async` route straight through `SessionPrompt.prompt`'s `runLoop`, which is otherwise synchronous/awaited — so the effect itself doesn't rely on a background fork to make progress. Confirmed via instrumentation that the request in these two scenarios simply never reaches the fake LLM server at all when run after another scenario has already forced the layer to build (zero HTTP hits recorded).
  • `session.prompt` intermittently reads the wrong fake-LLM text because a different scenario's leaked background title/summarize fiber can still consume a fake-LLM queue slot when it does happen to run before the scope is closed.

Reproduced reliably:
```
bun run script/httpapi-exercise.ts --mode effect --include session.prompt --fail-on-missing
```
fails on `session.prompt_async` every time (session.prompt + session.prompt_async run in the same batch, in that order), while running `session.prompt_async` alone always passes.

Fix

Give the whole exerciser run one top-level `Scope` (obtained once in `main` in `test/server/httpapi-exercise/index.ts`) and thread it through `runScenario` → `runActive` → `withContext` for the `Layer.buildWithMemoMap` call, instead of a fresh per-scenario scope. This matches how the app is actually scoped in production (built once against the server's own lifetime scope).

Verification

  • `session.prompt`, `session.prompt_async`, `session.command` now pass reliably (3x runs, both isolated and as part of the full effect-mode suite).
  • Full `--mode effect --fail-on-missing --fail-on-skip`: 242 pass / 3 fail — the 3 remaining failures (`pty.create`, `worktree.reset`, `v2.pty.create`) are pre-existing and unrelated (untouched by this fix).
  • `--mode coverage --fail-on-missing --fail-on-skip`: 245/245 pass (no regression).
  • `--mode auth --fail-on-missing --fail-on-skip`: 245/245 pass (no regression).
  • `bun run turbo typecheck --filter=opencode`: passes.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions