Skip to content

Skip output symlinks and publish only changed Artifacts - #47

Merged
SaladDay merged 6 commits into
mainfrom
codex/artifacts-publication
Sep 23, 2026
Merged

SaladDay merged 6 commits into
mainfrom
codex/artifacts-publication

Conversation

@SaladDay

@SaladDay SaladDay commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Artifact capture failed a whole Turn when the outputs directory contained a symlink, and it republished every output file on every later Turn. The official service skips symlinks and publishes only new or changed paths. This batch aligns capture and the Artifact list with the first owned official Artifact observations. The pinned baseline is unchanged (SDK 3.13.0 / d7c41ef / agents=v1).

Behavior

  • Symlinks (A1): symlinks anywhere inside the outputs tree are skipped, whether they point to a file, to a directory, nowhere, or outside the workspace. They are never followed, opened or resolved, and every regular file is still captured, so the Turn completes. The rules for a symlinked outputs root, hard links, special files, device crossings and concurrent changes are unchanged, since there is no official evidence for them.

  • Republication (A2): a later Turn publishes a path only if:

    • it has no remaining published Artifact in the Session, or
    • its bytes (sha256) differ from the newest remaining one. "Newest" follows the producing Turn's order.

    Unchanged files keep their existing Artifact IDs. The decision runs in the Turn completion transaction, under the same Session lock as Artifact deletion, so a deletion that commits during capture is honored. There is no migration: sha256 is already stored.

  • List envelope (A3): {object: "list", data, first_id, last_id, has_more}, with null IDs on an empty page, like the Session, Turn and Item lists.

  • Malformed environment_id filter (A4): returns 200 with an empty page. Tenant and Session checks still run first.

Documented local decision: comparison is against the newest remaining Artifact. If the newest one is deleted while an older Artifact with the same bytes remains, nothing is republished. Recording deletions would need a schema change, and the official behavior for this edge is unknown.

Evidence

Campaign scan 2 hosted-environment findings HE-51/52/53/56: 3 owned official hosted Sessions, all deleted. Recorded in contracts/agents-api/official-semantics-alignment.md and operation-evidence.md rows 16–19.

Validation

  • Live acceptance through Core, the daemon, native Codex and the real Kimi K3 model, on the Core-managed Docker openai_hosted Runtime. The run was built from the exact source, including the Rust export helper, whose hash was checked inside the running Runtime containers.

    Check main b710064 this head
    Turn 1 with outputs plus four symlink kinds every Turn failed (artifact_capture_failed) completed, regular files only
    Turn 2 after deleting a.txt, writing only c.txt nothing published exactly c.txt and a.txt published
    Turn 3 changing sub/b.txt nothing published only sub/b.txt, new bytes (the old Artifact keeps the old bytes)
    Envelope and malformed filter (pinned SDK and raw HTTP) old envelope, 400 new envelope, 200 empty page
    Tenant B 404 everywhere 404 everywhere

    3 Turns per phase. Cleanup and secret scans passed.

  • Rust export tests: every link kind, plus an inotify proof (with a positive control) that no link target is opened; rustfmt and clippy clean.

  • Real-PostgreSQL store tests: new, unchanged, changed, changed-back and deleted-then-unchanged paths; producing-Turn ordering; a truly concurrent deletion; no stored-byte leaks.

  • HTTP tests: envelope and filter, with tenant isolation.

  • Server gate on this head: make -o check-web check plus Web typecheck, core-doctor, 301 client tests, 602 Web tests and the build all pass. The Playwright browser cases were not run on the server (no Google Chrome; skip approved by the user).

  • Generated files: make sqlc-generate and make openapi are byte-identical.

  • Independent blind review by a fresh Claude Code subagent (the user-approved replacement for GPT-6 Astra): no blockers. Its follow-ups (two regression tests, doc qualification, the documented A2 edge) are in the last two commits, which touch only tests and docs.

Deferred

  • A symlinked outputs root still fails the Turn.
  • The unknown after cursor status (HE-57).
  • Subagent list envelopes, which follow the same outlier pattern but have no official evidence.
  • The Environment Files wire alignment, which is a separate batch.

No full protocol compatibility is claimed.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

A symlink anywhere below outputs/ is now skipped by its lstat type instead of
aborting the export and failing the Turn, matching the official service (HE-51).
Links are never followed, opened or resolved. Hard links, FIFOs, sockets,
devices, device crossings, a linked outputs root and concurrent changes keep
rejecting the export. The new test proves through inotify that no link target
inside or outside the workspace is opened or read.
A completed Turn no longer republishes every output (HE-52). In the Turn's
terminal transaction, which holds the Session lock that also orders Artifact
deletion, staged paths whose sha256 equals the newest remaining published
Artifact for that path in the Session are dropped and their private objects
unlinked. New paths, changed bytes and paths without a remaining Artifact are
published; unchanged paths keep their existing immutable Artifact IDs. The first
Turn is unchanged and no migration is needed.

Real-PostgreSQL tests cover new, unchanged, changed, changed-back,
deleted-then-unchanged and deleted-during-capture paths, Session scoping and
private object accounting. The user-managed Runtime acceptance no longer
expects unchanged outputs under later Turns.
The Artifact list now returns object, data, first_id, last_id and has_more,
with null first and last IDs on an empty page, like the Session, Turn and Item
lists (HE-53). Paging and cursors are unchanged. A malformed environment_id
resolves to the never-assigned maximum UUID and returns an empty page instead
of 400, matching another Environment's ID (HE-56); tenant and Session lookup
still run first, and cursor and limit errors are unchanged.

Handler and real-PostgreSQL HTTP tests cover the envelope, other, foreign and
malformed filters, and foreign or missing Sessions. The shared pinned-SDK and
raw HTTP verifier asserts the envelope and filter and now runs against
PostgreSQL across three Turns without a model.
Add the September 23 Artifact section with rows A1-A4, the decision to settle
republication in the Turn's terminal transaction, the newest-version order,
the no-tombstone edge and deferred cases. Register the hosted-environment
evidence as Y, update operation rows 16-19, the README ledger and the capture
rules in CONTRIBUTING.
One test publishes Turn 1 with a runtime-reported completion an hour ahead,
so a later Turn's Artifact has an earlier publication time, and asserts that
the newest version follows Turn order in both directions. The other holds the
Session lock while deleting the newest Artifact, confirms Turn completion waits
on that lock, then commits and asserts the path is republished with no leaked
private object. Ordering by publication time, or deciding at capture time,
fails these tests.
Record comparison against the newest remaining Artifact as an accepted known
difference from the batch plan's wording, with the bravo/bravo-v2 example, and
list the two new regression tests. The README ledger again marks exact upstream
defaults as unverified, matching OpenAPI and the handler description.
@SaladDay
SaladDay merged commit e4b124c into main Sep 23, 2026
4 checks passed
@SaladDay
SaladDay deleted the codex/artifacts-publication branch October 7, 2026 06:37
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