fix(runtime): deliver fire-and-forget notify() when the script throws - #133
Merged
ralyodio merged 1 commit intoJul 31, 2026
Merged
Conversation
runScript drained un-awaited async verbs only after fn(scope) returned, so a
script that threw skipped the drain entirely. The CLI catches that throw and
calls process.exit(1), which kills the in-flight POST — dropping exactly the
notification the operator most wants:
notify("build failed"); nope();
printed the "notify() -> build failed" line but never delivered it.
Drain in a finally block instead. Promise.allSettled never rejects, so the
script's own error still propagates unchanged.
Merged
ralyodio
added a commit
that referenced
this pull request
Jul 31, 2026
Cuts a release off main so the fixes merged after v0.13.1 actually reach installs. v0.13.1 shipped before #135, so re-running the installer still handed you a moshcode that reported turso as missing. Included since v0.13.1: - #135 fix(tools): find turso in ~/.turso instead of reporting it missing - #134 fix(tui): stop styling the agent-view notice as an error - #133 fix(runtime): deliver fire-and-forget notify() when the script throws Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
runScriptcollects un-awaited async verbs inpendingand drains them afterfn(scope)returns:If the script throws, that drain never runs.
bin/moshcode.mjscatches the throw and callsprocess.exit(1), which kills the in-flightPOST /api/approvals— so the notification is silently dropped.The dropped one is the failure ping, which is the one you most want:
This violates the file's own stated contract at
src/runtime.mjs:58:That is precisely what happens; the guard just doesn't cover the throwing path.
Reproduced end to end on unmodified
main(8e183ad)No mocking —
notify()is pointed at a real local HTTP server via the documentedMOSHCODE_API/MOSHCODE_API_KEYenv overrides, which records every request it actually receives.The
🔔 notify() → build failedline still prints, so the run looks like it notified. Deterministic, 5/5 runs,exit=1 delivered=0. The control (same script withoutnope()) delivers 1/1.After the fix, the same command is
exit=1 delivered=1, the error still prints, and the approval link now prints too.The fix
Drain in a
finallyblock.Promise.allSettlednever rejects, so this cannot mask or replace the script's own error.12 insertions / 3 deletions, 6 of the additions comment.
Tests
New
test/runtime-drain-on-throw.test.mjs, 10 tests. The fake verb resolves on a macrotask (setTimeout), not a microtask — an un-drained promise chained purely off microtasks can still land by accident, which would let these tests pass for the wrong reason. A timer cannot.4 are the bug (script throws; a verb throws; all three of several queued notifies deliver, not just the first; notifies queued inside a
while (alive)loop).6 pass both ways and deliberately assert the opposite direction, so the fix cannot buy delivery by swallowing errors: the script's error survives the drain unchanged; a rejecting fire-and-forget verb does not mask it; success-path delivery unchanged;
{ iterations, stopped }unchanged; a throwing script with nothing queued still rejects; anawaitednotify()still returns its value.Fail-before via
git checkout -- src/runtime.mjs: 4 fail / 6 pass unpatched, 10/10 patched.Full suite 464 → 474, 0 fail (baseline measured by holding the new file out, not assumed).
Scoped out
src/tui.mjs:356has the same shape but recovers instead of exiting (catch→ prints, keeps the REPL alive), so the event loop stays open and the POST does land. It benefits from this fix but was not broken.