Skip to content

refactor(effect): raise declared failures with Effect.fail, not throw inside Effect.gen (E8) - #247

Open
nikomatt69 wants to merge 4 commits into
live-mainfrom
claude/roadmap-integration-effect-ts-8b7vrn
Open

refactor(effect): raise declared failures with Effect.fail, not throw inside Effect.gen (E8)#247
nikomatt69 wants to merge 4 commits into
live-mainfrom
claude/roadmap-integration-effect-ts-8b7vrn

Conversation

@nikomatt69

@nikomatt69 nikomatt69 commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Issue for this PR

Closes #

Not an issue — this comes from specs/ROADMAP.md. E5's own section records a remainder it deliberately did not claim:

Caveat for the next reader. SessionPrompt.assertNotBusy is still declared Effect.Effect<void> and raises by throw inside Effect.gen … Narrowing that signature to Session.BusyError with an explicit Effect.fail is a separate cleanup, not a reopening of E5.

Note on the id. This was drafted as E6 while the near plan was empty. live-main then refilled the queue in parallel and claimed E6 for the Effect pin bump, so this is now E8. The numbering gap is that collision, not a missing item. E8 is independent of E6 — it uses only APIs beta.83 already ships, so it neither blocks nor waits on the pin.

Type of change

  • Refactor / code improvement
  • Documentation
  • Bug fix
  • New feature

What does this PR do?

Two sites raised a declared domain error with throw inside Effect.gen, which makes it a defect rather than a typed failure. Both now use Effect.fail.

1. session/prompt.ts — the remainder E5 named. assertNotBusy was declared Effect.Effect<void> and threw Session.BusyError. It reached SessionRevert's callers typed purely by accident of the Promise bridge: runPromiseWithLayer rejects with the squashed cause, and Session.asSessionError passes a BusyError straight through. The vocabulary already existed — Session.BusyError is a Schema.TaggedErrorClass with a declared 409 body — only the signature and the raise didn't use it. Now Effect.Effect<void, Session.BusyError>.

2. server/httpapi/pty.ts — found by sweeping for the same shape. Grepping for throw doesn't answer this question; what matters is a throw whose nearest enclosing function is an Effect.gen body. Brace-matching each Effect.gen(function* () { found 57 occurrences in src, and all but three are codemode/interpreter/, where throwing is the interpreter's own unwind mechanism and codemode/interpreter/errors.ts matches InterpreterRuntimeError at the boundary — a design, not a leak.

The three that weren't: session/prompt.ts, and httpapi/pty.ts twice.

handlers.get / handlers.update threw the declared Pty.NotFoundError inside Effect.gen, so GET /pty/:id returned 404 only because catchNotFound paired Effect.catchDefect(asNotFound) with its typed arm — exactly the compensation E5 deleted from httpapi/session.ts. Its sibling catchCreateError carried the same pair, and there the defect arm was dead code: Pty.Service.create builds CreateError in the catch of the Effect.try around spawnPty, so it is always typed, and nothing in src/pty/index.ts throws at all.

Both combinators now take the declared error type instead of a free E, so the compiler keeps the mapping total rather than a second runtime arm. The raise and the arm-removal have to land together: removing the defect arm alone would turn every missing-pty lookup into a 500.

No HTTP wire change.

specs/ROADMAP.md admits this as E8 with its own evidence/implementation/gate rather than reopening E5's already-met gate, and marks E5's caveat closed.

How did you verify your code works?

Mutation, which is what makes the claim behavioural rather than stylistic. Restoring the two throws turns exactly three tests red:

Test Under mutation
handlers.get maps a missing session on the typed channel (new) fail
SessionPrompt.assertNotBusy fails … on the typed channel (new) fail
GET /pty/:id … returns 404 with the declared error body (pre-existing) fail

That third row is the point — with the defect arm gone, a re-introduced throw answers 500, not 404. The busy-revert test correctly stays green under mutation: it reaches its assertion through runPromiseWithLayer, which squashes a die and a fail alike, which is the very reason E5 called the old signature typed only by accident.

Locally (re-run after merging live-main): bun run typecheck clean; bun test test/session/ test/server/ 780 pass / 1 fail across 90 files; touched test files 23 pass / 0 fail; check:routes --strict ok at 338 contracts / 315 handlers / 23 raw; oxlint and prettier clean.

CI on this head is green except for two pre-existing failures, both investigated and neither caused by this change:

  • smoke (windows-latest, cmd | pwsh)tsc --noEmit hits V8's ~4 GB heap ceiling (FATAL ERROR: Reached heap limit, exit 134). Red identically on live-main at 480b8724 and on every live-main push in the listed history, plus every other open PR. Details and a proposed one-line workflow patch are in this comment; deliberately not folded into this PR.
  • update-node-modules-hashescontinue-on-error by design; its workflow run concluded success. Fires only because the merge pulled in release: v1.321.0 (touches package.json). Its error, Workspace not found "github" in the nix build, is pre-existing.

validate, typecheck, test (linux), nix-eval, dependency-review, CodeQL and all standards/compliance checks pass.

Correction to an earlier revision of this description. It claimed the generated SDK types were a pre-existing red gate because regenerating reordered 29 Event* declarations locally. That was wrong. validate runs precisely that check — generate:httpapi-clients then git diff --exit-code -- packages/sdk/js/src/httpapi/generated — and it passes here; the committed file is correct. The reordering is an artifact of an interrupted bun install in my sandbox (a different set of event modules registered, and declaration order follows registration). No item needs opening. The roadmap entry is corrected in 37529ed.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Note

Low Risk
Refactor with no intentional HTTP wire change; regression risk is mis-raising errors (500 instead of 404/409), which new tests are meant to catch.

Overview
E8 finishes the pattern from E5: domain errors that already have declared HTTP bodies must ride Effect’s typed failure channel, not throw inside Effect.gen (which turns them into defects).

Session prompt (assertNotBusy). The interface is now Effect.Effect<void, Session.BusyError> and a busy session is signaled with Effect.fail instead of throw, so Effect callers see the 409-shaped error without relying on the Promise bridge or catchDefect.

PTY HttpApi. handlers.get / handlers.update fail missing sessions with Effect.fail(Pty.NotFoundError). catchNotFound and catchCreateError only use Effect.catch on the declared error types—the catchDefect arms are removed, so a regression to throw surfaces as 500 instead of being silently mapped to 404/400. Wire behavior is unchanged when failures stay typed.

Tests and docs add Cause.hasDies === false checks on the pty handlers and assertNotBusy, and specs/ROADMAP.md marks E8 done and closes E5’s caveat.

Reviewed by Cursor Bugbot for commit 37529ed. Bugbot is set up for automated code reviews on this repo. Configure here.

claude added 2 commits August 26, 2026 18:22
… inside Effect.gen (E6)

E5 closed the session HTTP boundary but recorded one remainder it did not
claim: `SessionPrompt.assertNotBusy` was declared `Effect.Effect<void>` and
raised `Session.BusyError` with `throw` inside `Effect.gen`, so it reached
`SessionRevert`'s callers typed only by accident of the Promise bridge.

A sweep for that shape found one other live site. `httpapi/pty.ts` threw the
declared `Pty.NotFoundError` inside `Effect.gen` in `handlers.get` and
`handlers.update`, so `GET /pty/:id` answered 404 rather than 500 only
because `catchNotFound` carried an `Effect.catchDefect` arm beside its typed
one — the same compensation E5 removed from `httpapi/session.ts`.
`catchCreateError` carried the same pair, and there the defect arm was dead:
`Pty.Service.create` builds `CreateError` in the `catch` of the `Effect.try`
around `spawnPty`, so it is always typed.

Both now fail on the typed channel. `catchNotFound` / `catchCreateError`
narrow their input to the declared error type and drop the defect arm, so a
re-introduced `throw` is a visible 500 instead of being silently absorbed.

No HTTP wire change. The existing route tests still pin the 404 body and the
busy revert as `Session.BusyError`; the added service-level assertions read
`Cause.hasDies === false`, which is what separates a fail from a die and
therefore what goes red if a `throw` comes back.

specs/ROADMAP.md admits this as E6 with its own acceptance gate rather than
reopening E5's met one, and marks E5's caveat closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C76KUCD7BVi9bB9brPDsjP
…e-existing red gate

Adds the dated landing log for E6 with the verified counts, and applies
prettier to the two combinators whose signatures changed.

The mutation run is the part worth keeping: restoring the two `throw`s turns
exactly three tests red, and one of them is the *pre-existing* pty 404 route
test. That is what makes the claim behavioural rather than stylistic — with
the defect arm removed, a re-introduced `throw` answers 500, not 404, so the
raise and the arm removal have to land together.

Also records, without fixing, that `generate:httpapi-clients` reorders the
Event declarations in the generated SDK types by 29 lines. It reproduces with
the E6 source reverted, so it is pre-existing; C1 makes generated drift
blocking, so it wants its own item rather than being buried in this diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C76KUCD7BVi9bB9brPDsjP
@github-actions github-actions Bot added size/L PR touches 288 lines area/docs area/server and removed size/L PR touches 288 lines labels Aug 26, 2026
@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_2ad28e71-22dc-4b66-8b73-0f273f04ea29)

live-main refilled the near plan while this branch was open, and the refill
claims the id `E6` for the Effect pin bump (beta.83 -> release candidate).
This item was drafted as E6 when the queue was empty, so it is renumbered to
E8 across the spec, both source files and both test files. The numbering gap
is that collision, not a missing item.

The conflict is `specs/ROADMAP.md` and is resolved in favour of live-main
everywhere the two disagree: upstream's refilled id table and its "E6, then
E7; R2 and H9 follow" ordering statement are the live queue and are taken
verbatim. This change appends only that E8 landed and is independent of the
pin — it uses APIs beta.83 already ships, so it neither blocks nor waits on
E6.

Worth noting for whoever picks up the queue: R2 ("retire the instance ALS
across its remaining boundary reads") is the R1 remainder this branch
deliberately left alone on the grounds that the roadmap deferred it. That
reasoning no longer holds — it now has an id and a gate.

Verified on the merge result: `bun run typecheck` clean on packages/nikcli;
the three touched test files 23 pass / 0 fail; prettier and oxlint clean on
the four changed sources plus the reload.ts wrap live-main brought in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C76KUCD7BVi9bB9brPDsjP
@github-actions github-actions Bot added the size/L PR touches 288 lines label Aug 26, 2026
@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_56fb030d-1af6-4773-a12a-7a88a4896dde)

@nikomatt69 nikomatt69 changed the title refactor(effect): raise declared failures with Effect.fail, not throw inside Effect.gen (E6) refactor(effect): raise declared failures with Effect.fail, not throw inside Effect.gen (E8) Aug 26, 2026
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copy link
Copy Markdown
Owner Author

CI: smoke (windows-latest, *) is failing, and it is not this PR's

What is failing. The typecheck step of windows-compat (bun run typechecktsc --noEmit) dies with:

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
error: script "typecheck" exited with code 134

Why it is not this PR's. It is red on the base branch, identically:

commit last GC before abort
live-main 480b8724 (the roadmap-refill commit, pushed before this branch existed) 4047.4 (4140.5) -> 4037.6 (4146.0) MB
this PR eb83b258 4045.8 (4140.5) -> 4033.4 (4146.5) MB

Same job, same step, same exit code, same ~4 GB ceiling. windows-compat has in fact failed on every live-main push in the listed history (2026-08-25 → 2026-08-26, including 6e411a48, the R1 closing commit), and on every other open PR branch — #246 and #223 included.

It is also a whole-repo tsc --noEmit, so its peak heap is a property of the project's total type graph, not of a diff. This PR adds about ten lines of type annotations across two files.

Root cause, for whoever picks it up. Windows resolves typescript@5.9.3 — the JS compiler on Node, which caps out at V8's default old-space of ~4 GB. Linux uses the native @typescript/typescript-linux-x64 TS7 binary, which has no V8 heap cap; the same typecheck peaks around 6.8 GB there and passes. So the project's type graph already needs more than 4 GB, and only the Windows runner is subject to the limit. The typecheck job on Linux is green on this PR.

No fix exists to port. No open PR addresses it (#246 hits the same wall). Rather than widen this PR with an unrelated CI change, the proposed patch is one line in .github/workflows/windows-compat.yml:

      - name: typecheck
        shell: pwsh
        env:
          NODE_OPTIONS: --max-old-space-size=8192
        run: bun run typecheck

windows-latest runners have 16 GB, so 8 GB of old-space is safe. The alternative — making Windows resolve the same native TS7 compiler Linux uses — is the better long-term fix but is a dependency change, not a CI tweak, and deserves its own item.

No re-run spent. A re-run is for confirming a suspected flake; this is a deterministic memory ceiling that reproduces at the same byte figure on two different commits, and base-branch redness is already established directly. Re-running would burn a Windows runner to learn nothing.

The Linux typecheck, test, nix-eval, dependency-review, CodeQL and all standards/compliance checks are green on this head. (update-node-modules-hashes also reports failure but is continue-on-error by design and its workflow run concluded success; it fires only because the merge pulled in release: v1.321.0, which touches package.json. Its error — Workspace not found "github" in the nix build — is likewise pre-existing.)


Generated by Claude Code

…ndbox artifact

The E8 landing log recorded the generated-client `Event*` reordering as a
pre-existing red gate that needed its own item. That was wrong, and the
correction matters because it would otherwise send someone to open an item
for a check that is green.

`validate` runs exactly that check — `generate:httpapi-clients` followed by
`git diff --exit-code -- packages/sdk/js/src/httpapi/generated` — and it
passes in CI on this commit. The committed file is correct.

The reordering was real in the development sandbox and reproduced with the
E8 source reverted, so it was never this change; the likely cause is an
interrupted `bun install` there leaving a module graph in which a different
set of event modules registered, and declaration order follows registration.
Recorded as that rather than deleted, so the next person who regenerates
locally and sees an uncaused diff knows to check `validate` first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C76KUCD7BVi9bB9brPDsjP
@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_283b178b-840c-438c-bf49-42efdabbf485)

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants