refactor(effect): raise declared failures with Effect.fail, not throw inside Effect.gen (E8) - #247
refactor(effect): raise declared failures with Effect.fail, not throw inside Effect.gen (E8)#247nikomatt69 wants to merge 4 commits into
Conversation
… 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
Bugbot couldn't run - usage limit reachedBugbot 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
Bugbot couldn't run - usage limit reachedBugbot 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) |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
CI:
|
| 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 typecheckwindows-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
Bugbot couldn't run - usage limit reachedBugbot 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) |
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:Type of change
What does this PR do?
Two sites raised a declared domain error with
throwinsideEffect.gen, which makes it a defect rather than a typed failure. Both now useEffect.fail.1.
session/prompt.ts— the remainder E5 named.assertNotBusywas declaredEffect.Effect<void>and threwSession.BusyError. It reachedSessionRevert's callers typed purely by accident of the Promise bridge:runPromiseWithLayerrejects with the squashed cause, andSession.asSessionErrorpasses aBusyErrorstraight through. The vocabulary already existed —Session.BusyErroris aSchema.TaggedErrorClasswith a declared 409 body — only the signature and the raise didn't use it. NowEffect.Effect<void, Session.BusyError>.2.
server/httpapi/pty.ts— found by sweeping for the same shape. Grepping forthrowdoesn't answer this question; what matters is athrowwhose nearest enclosing function is anEffect.genbody. Brace-matching eachEffect.gen(function* () {found 57 occurrences insrc, and all but three arecodemode/interpreter/, where throwing is the interpreter's own unwind mechanism andcodemode/interpreter/errors.tsmatchesInterpreterRuntimeErrorat the boundary — a design, not a leak.The three that weren't:
session/prompt.ts, andhttpapi/pty.tstwice.handlers.get/handlers.updatethrew the declaredPty.NotFoundErrorinsideEffect.gen, soGET /pty/:idreturned 404 only becausecatchNotFoundpairedEffect.catchDefect(asNotFound)with its typed arm — exactly the compensation E5 deleted fromhttpapi/session.ts. Its siblingcatchCreateErrorcarried the same pair, and there the defect arm was dead code:Pty.Service.createbuildsCreateErrorin thecatchof theEffect.tryaroundspawnPty, so it is always typed, and nothing insrc/pty/index.tsthrows 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.mdadmits 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:handlers.get maps a missing session on the typed channel(new)SessionPrompt.assertNotBusy fails … on the typed channel(new)GET /pty/:id … returns 404 with the declared error body(pre-existing)That third row is the point — with the defect arm gone, a re-introduced
throwanswers 500, not 404. The busy-revert test correctly stays green under mutation: it reaches its assertion throughrunPromiseWithLayer, 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 typecheckclean;bun test test/session/ test/server/780 pass / 1 fail across 90 files; touched test files 23 pass / 0 fail;check:routes --strictok 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 --noEmithits V8's ~4 GB heap ceiling (FATAL ERROR: Reached heap limit, exit 134). Red identically onlive-mainat480b8724and on everylive-mainpush 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-hashes—continue-on-errorby design; its workflow run concludedsuccess. Fires only because the merge pulled inrelease: v1.321.0(touchespackage.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.Screenshots / recordings
Not a UI change.
Checklist
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
throwinsideEffect.gen(which turns them into defects).Session prompt (
assertNotBusy). The interface is nowEffect.Effect<void, Session.BusyError>and a busy session is signaled withEffect.failinstead ofthrow, so Effect callers see the 409-shaped error without relying on the Promise bridge orcatchDefect.PTY HttpApi.
handlers.get/handlers.updatefail missing sessions withEffect.fail(Pty.NotFoundError).catchNotFoundandcatchCreateErroronly useEffect.catchon the declared error types—thecatchDefectarms are removed, so a regression tothrowsurfaces as 500 instead of being silently mapped to 404/400. Wire behavior is unchanged when failures stay typed.Tests and docs add
Cause.hasDies === falsechecks on the pty handlers andassertNotBusy, andspecs/ROADMAP.mdmarks 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.