Skip to content

fix: writeAtomicFile only unlinks the temporary after the write succeeded, and surfaces a non-ENOENT cleanup failure - #15

Merged
EauDoon merged 3 commits into
mainfrom
fix/write-atomic-file-cleanup-warning
Sep 4, 2026
Merged

fix: writeAtomicFile only unlinks the temporary after the write succeeded, and surfaces a non-ENOENT cleanup failure#15
EauDoon merged 3 commits into
mainfrom
fix/write-atomic-file-cleanup-warning

Conversation

@EauDoon

@EauDoon EauDoon commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Bug review of EauDoon/agent-action-stack found:

  • LOW: bin/aas.mjs writeAtomicFile had a try/catch that called unlink(temporary) in the catch arm of the write-or-rename try. The unlink ran even when the writeFile itself failed, in which case the temporary was never created and unlink would throw ENOENT that the catch silently swallowed. The two real failure modes (write failed, rename failed) both got the same misleading "cleanup noise ignored" path, and a real cleanup failure (e.g. EBUSY) was indistinguishable from a no-op ENOENT.

Fix: track whether the write succeeded with a written flag. Only attempt the unlink in a finally block when the temporary actually exists. Distinguish ENOENT (legitimate "nothing to clean up") from any other unlink error; the latter is a real operator-visible problem (the temporary is stuck on disk) so emit a process warning named "WriteAtomicFileCleanup" instead of swallowing it. The original write-or-rename error is still the one that propagates.

Verified: node --test test/stack.test.mjs runs 40 tests, all pass.


Devin Review

…and runProve

Bug review of EauDoon/agent-action-stack found:
- MEDIUM: runAct (bin/aas.mjs) short-circuits to childProcessError when result.status !== 0, before parseStageJson is reached. Any JSON crctl wrote to stdout is dropped. The throw also bypasses failedStderr, but the bigger loss is the stdout payload: persistRunBundle only writes a stage artifact when current.raw !== undefined, and stageErrorFields never sets raw, so a `--fault duplicate` failure that surfaces as JSON on stdout is not persisted, does not appear in report.stages.act, and printHuman shows only act_code / act_reason / act_stderr. runDecide and runProve both parse the JSON first and return {ok: false, raw, status, stderr} for nonzero exits; runAct was the asymmetric outlier.

Fix: parse the JSON first, then if status is nonzero return the same {ok: false, raw, status, ...failedStderr(result)} shape that runDecide and runProve use. Throw childProcessError only when the spawn itself failed (result.error) or when the child exited nonzero with no parseable JSON.

Verified: node --test test/stack.test.mjs runs 40 tests, all pass.
Bug review of EauDoon/agent-action-stack found:
- LOW: bin/aas-gui.mjs requestBoundaryFailure returns the same "origin" code whether the caller is missing the Origin header entirely or sent a wrong-origin header. The handler at line 100 returns 403 Forbidden for both, with the body { error: "Forbidden" }. A programmatic local client that forgets to send Origin (curl, a CI step, a non-browser agent) sees a generic 403 and has no way to tell that the only fix is to add the header. A wrong-origin POST is genuinely forbidden; a missing-Origin POST is a misconfiguration that should be 400.

Fix: add a "missing-origin" branch in requestBoundaryFailure and a 400 response with a clear "Origin header required for POST" message in the handler. Wrong-origin POSTs still get 403. Update test/gui.test.mjs to assert the 400 and the new error message.

Verified: node --test test/gui.test.mjs runs 2 tests, all pass. node --test test/stack.test.mjs still runs 40 tests, all pass.
…eded, and surfaces a non-ENOENT cleanup failure

Bug review of EauDoon/agent-action-stack found:
- LOW: bin/aas.mjs writeAtomicFile had a try/catch that called unlink(temporary) in the catch arm of the write-or-rename try. The unlink ran even when the writeFile itself failed, in which case the temporary was never created and unlink would throw ENOENT that the catch silently swallowed. The two real failure modes (write failed, rename failed) both got the same misleading "cleanup noise ignored" path, and a real cleanup failure (e.g. EBUSY) was indistinguishable from a no-op ENOENT.

Fix: track whether the write succeeded with a `written` flag. Only attempt the unlink in a finally block when the temporary actually exists. Distinguish ENOENT (legitimate "nothing to clean up") from any other unlink error; the latter is a real operator-visible problem (the temporary is stuck on disk) so emit a process warning named "WriteAtomicFileCleanup" instead of swallowing it. The original write-or-rename error is still the one that propagates.

Verified: node --test test/stack.test.mjs runs 40 tests, all pass.
@EauDoon
EauDoon merged commit 9c334f4 into main Sep 4, 2026
@EauDoon
EauDoon deleted the fix/write-atomic-file-cleanup-warning branch September 4, 2026 10:52

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Comment thread bin/aas.mjs
Comment on lines +525 to +530
if (result.status !== 0) {
// A nonzero exit with parseable JSON is an unsuccessful act (the CLI
// surfaces structured errors as JSON on stdout), not a child-process
// error. Mirror runProve so persistRunBundle and printHuman see the
// structured failure and the GUI can render the stage artifact.
return { ok: false, raw: payload, status: result.status, ...failedStderr(result) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Failed actions become successful runs

When runAct returns a nonzero result, runDemo ignores ok and records the action as passed. The run can exit successfully after the action failed.

Prompt for agents
Update bin/aas.mjs so runDemo handles the new runAct failure result instead of treating every resolved result as a passed action. A result with ok false must mark the act stage failed, preserve its raw payload and stderr in the bundle and report, set a nonzero run exit code, and avoid presenting the action as passed. Decide explicitly whether proof still runs for this failure shape, while ensuring a successful proof cannot turn the overall run back into success. Add coverage for a nonzero, parseable act result through runDemo, including report status, manifest status, artifact persistence, and exitCode.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread bin/aas.mjs
Comment on lines 617 to +618
writeFile(temporary, data, { encoding: "utf8", flag: "wx" });
written = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Partial writes leave temporary files

When writeFile creates a partial temporary file before throwing, written remains false and skips cleanup. Repeated write failures leave orphaned files.

Prompt for agents
Revise writeAtomicFile in bin/aas.mjs so cleanup also handles writes that create or partially write the temporary file before throwing. The current written flag only proves that writeFile returned, not whether the temporary path exists. Preserve the original write or rename error, ignore ENOENT during cleanup, and emit the cleanup warning for other unlink failures. Avoid deleting an unrelated file if exclusive creation itself failed. Add a test whose writeFile implementation creates the temporary path and then throws, and assert that cleanup removes it.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant