Repository navigation
Conversation
…ng it
The top-level handler bound nothing:
).catch(() => {
console.error("worker terminated");
process.exitCode = 1;
});
So every failure that is not one of the two classes `diagnosticCodeForError`
recognises produced the string "worker terminated", exit code 1, and no cause
anywhere -- not in the terminal contract, not on the volume, not in stderr.
That is how five sandboxes ended at `stateful-invariant-setup` across three Aave v4
runs. R47 attempt 1 finally recorded `worker_exit_code: 1`, captured only because of
#303, and exit 1 traces to exactly this handler: the worker exited deliberately,
knew why, and threw the reason away. Two separate memory theories were pursued and
disproven in the meantime -- a V8 string ceiling in patch capture, then a `git add`
cgroup OOM -- both of which one logged error string would have pre-empted.
Deliberately NOT persisted into the worker result contract. That was my first
attempt and the suite refused it: `writes and flushes an unexpected worker failure
without serializing its detail` pins that the contract must not carry failure text,
and the reason holds up -- `status.json` and `result.json` are collected, and there
is a public results path, so an arbitrary error string could put target source or
configuration into a published artifact. `redactSecretsInText` removes known secret
shapes, not arbitrary content, so it is not sufficient to cross that boundary.
Reverted, and the invariant stands.
Stderr is the right place for this: it is inside the sandbox, outside the collected
contract. A durable private channel would be better still -- but `worker.log` is
itself in `MODAL_COLLECT_RESULT_FILES`, so it is subject to the same publication
question and deserves its own change rather than a quiet extension here. Recorded on
#307.
packages/modal 311/311.
Refs #307
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewer 2 found my fourth false causal claim, and I verified it. Closing this PR.The comment I wrote asserts this handler "used to discard [the reason] … which is how five sandboxes ended at one node." That is wrong, and the code is three functions away in the same file:
const streams = [child.stdout, child.stderr].map(async (stream) => {
for await (const _chunk of stream) {
// Drain child output without persisting provider responses or benchmark contents.
}
});
if (exitCode !== 0) {
await appendGenericLog("operation-failed");
throw new OperationalDispositionError(failureCategory(options)); // no cause
}The Smithers workflow — where No That is my fourth refuted causal claim in this sequence — after a V8 string ceiling, a What actually needs doing, per reviewReviewer 2's suggestion is right and is a different change from this one:
Note the order matters, and reviewer 1 independently found why: Two reviewers, two different halves of the same trap. That is worth more than this PR was. Also relevant#311 now catches the ENOBUFS at source and names the offending paths, which addresses the diagnosis gap for this specific class without touching the worker's privacy boundary at all. Both reviewers said that change was worth more than this one, and I agree. Leaving #307 open with the stderr-threading design recorded. Closing this rather than merging a comment I cannot stand behind. |
* fix(modal): give the worker's terminal failure a reason to print The worker's top-level catch discarded the rejection value, so every failure outside the two classes `diagnosticCodeForError` recognises produced the literal string "worker terminated" and exit code 1. Binding the error alone would not have helped: `runChecked` drains child stderr into a no-op and throws `OperationalDispositionError` with no `cause`, so the reason was already gone two layers earlier. Retain a bounded, redacted stderr tail in `runChecked` and attach it as `cause`, then render the whole `cause` chain at the top-level catch. The chain is rendered by reading only `name`, `code` and `message` at each link — never by inspecting the error object, because an `execFileSync` ENOBUFS `SystemError` carries `output`/`stdout`/`stderr` holding up to the whole captured workspace diff. The mirror leak in `public-worker.ts` — 4000 raw stderr bytes embedded as a `cause`, hidden today only because `.stack` does not render it — now goes through the same sanitizer, which is `public-worker.ts`'s own redaction path lifted into a shared module. Refs #307. Supersedes #309. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(modal): keep the failing frames the terminal reason is meant to carry `describeWorkerTermination` reads only `name`, `code` and `message`, so the class of failure #307 is actually chasing -- an unanticipated error in the worker's own code -- prints "TypeError: Cannot read properties of undefined" and names no file and no line. The stated reason for dropping the stack does not hold: `Error.prototype.stack` is a plain string of `name: message` plus frames and never carries an error's own enumerable properties, so an `execFileSync` ENOBUFS `SystemError`'s `output`/`stdout`/`stderr` cannot reach it. That is now measured by a test rather than assumed. Add `workerTerminationStack`, which redacts and bounds those frames the same way every other diagnostic string in this module is bounded, and print it from the top-level catch. Also make a rejection with `undefined` or `null` name itself instead of collapsing to the bare prefix the issue is about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Ubuntu <ubuntu@ip-192-168-2-198.ec2.internal> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
What was there
packages/modal/src/worker.ts:619:The rejection value was never bound. Combined with
diagnosticCodeForErrorrecognising only two error classes, any other failure produced the stringworker terminated, exit code 1, and no cause anywhere.What that cost
Six sandboxes have now ended at
stateful-invariant-setupacross R45, R46 and R47. R47 attempt 1 finally recordedworker_exit_code: 1— available only because #302/#303 fixed the same class of gap one layer out — and exit 1 traces to exactly this handler. The worker exited deliberately, knew why, and discarded the reason.In the meantime I pursued and published two wrong causal theories: a V8 string ceiling in patch capture (refuted by a reviewer running the code) and a
git addcgroup OOM (refuted by/usr/bin/time -vshowing peak RSS is per-file — 620 MB across four blobs peaks at 155 MB, against a 32 GiB request). One logged error string would have pre-empted both.What this deliberately does NOT do
My first attempt also persisted a redacted, bounded
failure_messageinto the worker result contract. The suite refused it, by name:That invariant is deliberate and correct:
status.jsonandresult.jsonare inMODAL_COLLECT_RESULT_FILESand there is a public results path, so an arbitrary error string could put target source or configuration into a published artifact.redactSecretsInTextstrips known secret shapes, not arbitrary content, so it does not license crossing that boundary. Reverted; the invariant stands untouched.stderr is the right destination here — inside the sandbox, outside the collected contract.
The durable channel is a separate decision
stderr is not durable; by the time anyone looks the sandbox is gone. The obvious fix is the volume's
worker.log, except that is also inMODAL_COLLECT_RESULT_FILES, so it inherits the same publication question. #307 records three options (a private uncollected file, a strictly-filtered contract field, or extendingdiagnosticCodeForErrorto categorise more error classes) and leaves the choice to whoever owns that boundary. It should not ride quietly on a logging fix.Validation
packages/modal311/311; typecheck, eslint, prettier clean; full monorepo suite running.Greptile Summary
This PR improves Modal worker failure diagnostics without changing the persisted result contract.
Confidence Score: 5/5
The PR appears safe to merge with no actionable defects identified.
The change improves diagnostics only on the existing worker failure path, preserves exit code 1, and does not alter persisted or collected result data.
Important Files Changed
Reviews (1): Last reviewed commit: "fix(modal): print the error that ended t..." | Re-trigger Greptile
Context used: