Skip to content

fix: report session-workspace failures faithfully and prime inline files atomically - #44

Open
danny-avila wants to merge 2 commits into
mainfrom
fix/session-workspace-error-fidelity
Open

fix: report session-workspace failures faithfully and prime inline files atomically#44
danny-avila wants to merge 2 commits into
mainfrom
fix/session-workspace-error-fidelity

Conversation

@danny-avila

@danny-avila danny-avila commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Three fixes to how session mode reports and protects workspace state. All three surfaced while reviewing the subtree import of this code into a downstream monorepo — the code is unchanged from main there, so these are pre-existing here rather than import artifacts.

Inline priming destroyed before it wrote. writeFile unlinked the destination and then wrote the replacement, so a write that failed partway (ENOSPC/EIO) left the previous turn's bytes gone and the workspace dirty — forcing a recycle/restore that loses warm state when no checkpoint exists yet. By-reference priming already avoids exactly this and says so in a comment: it keeps a regular file in place and lets streamToDisk's rename replace it atomically. Inline priming now does the same, while still clearing a squatting symlink or directory so a prior turn cannot redirect the write.

Checkpoint/restore collapsed two different bind failures into one generic 409. A missing header (a caller error) and a rejected bind (this runner is pinned to a different session, so it must be recycled) both answered Missing runtime session header with no error code, leaving the control plane unable to see the conflict. The conflict case now returns the same session_workspace_dirty signal /execute already uses for this condition, so an older service fronting a newer runner still recycles the VM. A malformed or duplicated header now returns 400 instead of propagating SessionWorkspaceBindingError out of the route.

A validation failure after priming reported the workspace as dirty. Any error thrown once priming completed took the dirty branch, so a ValidationError — a deterministic rejection of the request, after which nothing ran — answered session_workspace_dirty instead of 400. That cost a needless restore and hid the reason the caller needed to fix its request.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

api suite: 367 passed, 0 failed (28 files), and tsc --noEmit reports the same 3 pre-existing errors as main, none in the touched files.

Each fix has a test that fails without it — verified by stashing the change and re-running:

  • src/inline-prime-atomicity.test.ts (new) — replacement without unlinking, symlink/directory squatting still cleared, and the regression itself: an injected ENOSPC at the write leaves the previous turn's bytes intact. Worth noting for future readers: making the workspace directory read-only does not reproduce the bug, because that blocks the unlink too — the write has to fail while the destination is still removable.
  • src/api/v2-checkpoint-binding.test.ts (new) — missing header stays a plain 409 with no recycle signal; a rejected bind returns session_workspace_dirty; malformed and duplicated headers return 400.
  • src/api/v2-session-binding.test.ts — adds the post-prime ValidationError case.
cd api && bun test

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective
  • Local unit tests pass with my changes

…les atomically

Three fixes to how session mode reports and protects workspace state, all
found reviewing the subtree import of this code into a downstream monorepo.

Inline priming destroyed before it wrote. writeFile unlinked the
destination and then wrote the replacement, so a write that failed partway
(ENOSPC/EIO) left the previous turn's bytes gone and the workspace dirty —
forcing a recycle/restore that loses warm state when no checkpoint exists
yet. By-reference priming already avoids exactly this: it keeps a regular
file in place and lets a rename replace it atomically. Inline priming now
does the same, and still clears a squatting symlink or directory so a prior
turn cannot redirect the write.

Checkpoint/restore collapsed two different bind failures into one generic
409. A missing header (a caller error) and a REJECTED bind (this runner is
pinned to a different session, so it must be recycled) both answered
"Missing runtime session header" with no error code, leaving the control
plane unable to see the conflict. The conflict case now returns the same
session_workspace_dirty signal /execute already uses for this condition, so
an older service fronting a newer runner still recycles the VM. A malformed
or duplicated header now returns 400 instead of propagating
SessionWorkspaceBindingError out of the route.

A validation failure after priming reported the workspace as dirty. Any
error thrown once priming completed took the dirty branch, so a
ValidationError — a deterministic rejection of the request, after which
nothing ran — answered session_workspace_dirty instead of 400. That cost a
needless restore and hid the reason the caller needed to fix its request.

Each fix is covered by a test that fails without it.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e130c736e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/src/api/v2.ts Outdated
Addresses review feedback on the previous commit. Rejecting a request with
no runnable source AFTER priming is too late, and answering it with a clean
400 was worse than the behavior it replaced.

`getJob`'s gate accepted any utf8 file, including the `.dirkeep` sentinel,
while `Job.execute` required a utf8 file that is NOT `.dirkeep`. A request
carrying only `.dirkeep` plus binary inputs therefore passed the gate,
reached `prime()` — replacing files in the session workspace and recording
priming metadata — and only then failed. Session cleanup deliberately
preserves the workspace, so the rejected request's writes stayed visible to
the next execution.

The previous commit made that case return 400 by moving the ValidationError
branch above the dirty branch, which traded a false-dirty for a false-clean:
the workspace really had been written to. Both checks now call a single
`hasRunnableSource` predicate, and the request gate runs it before any Job is
built, so such a request is rejected without touching the workspace. The
catch block is restored to its original order: once priming has completed,
any later failure — validation included — reports the workspace as dirty,
which is the honest answer.

Tests: a request with nothing runnable is rejected with prime() never called,
and a genuine post-prime failure still reports dirty. Both fail without this
change.
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