abhigyanpatwari/GitNexus · critical · SandboxError
transcript artifact changed while opening: {path}
Error message
transcript artifact changed while opening: {path} What it means
Raised by `_bound_transcript_artifact` during the open-fstat recheck: after `os.open(... O_NOFOLLOW)`, the descriptor's fstat must still be a regular file on the same device/inode captured by the earlier lstat. A mismatch means the file was swapped between lstat and open (TOCTOU).
Source
Thrown at eval/workflow_bench/evolve.py:404
relative, expected_digest, expected_size = _transcript_artifact_metadata(metadata)
path = _results_artifact_path(root, relative, transcript=True)
try:
before = path.lstat()
except OSError as exc:
raise SandboxError(f"transcript artifact is unavailable: {path}: {exc}") from exc
if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
raise SandboxError(f"transcript artifact must be a regular non-symlink file: {path}")
if stat.S_IMODE(before.st_mode) & 0o077:
raise SandboxError(f"transcript artifact must be owner-only: {path}")
if before.st_size != expected_size:
raise SandboxError(f"transcript artifact size does not match its results row: {path}")
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
try:
opened = os.fstat(descriptor)
if not stat.S_ISREG(opened.st_mode) or opened.st_dev != before.st_dev or opened.st_ino != before.st_ino:
raise SandboxError(f"transcript artifact changed while opening: {path}")
digest = hashlib.sha256()
content = bytearray()
while chunk := os.read(descriptor, 64 * 1024):
digest.update(chunk)
content.extend(chunk)
if len(content) > MAX_EVIDENCE_FILE_BYTES:
del content[: len(content) - MAX_EVIDENCE_FILE_BYTES]
after = os.fstat(descriptor)
if (opened.st_size, opened.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
raise SandboxError(f"transcript artifact changed while reading: {path}")
finally:
os.close(descriptor)
if digest.hexdigest() != expected_digest:
raise SandboxError(f"transcript artifact digest does not match its results row: {path}")
return bytes(content).decode(errors="replace")
def proposer_evidence_entries(View on GitHub (pinned to d540b00184)
Solutions
- Ensure nothing else writes to the results dir while evolve is reading it (stop concurrent benchmark/restore jobs).
- Re-stage the results dir into a location no other process touches, then retry.
- If reproducible, audit for a process that is rewriting transcript files and treat as an integrity incident.
Defensive patterns
Strategy: try-catch
Try / catch
from workflow_bench.proposer_sandbox import SandboxError
try:
proposer_evidence_entries(results_dir=rd, evidence=ev, learnings=lr, gate_summary=gs)
except SandboxError as exc:
if 'changed while opening' in str(exc):
# TOCTOU: another process touched the results dir mid-read
raise RuntimeError('results dir is being modified concurrently; isolate it') from exc
raise Prevention
- Run evolve against an idle, private copy of the results dir.
- Never point two tools at the same results dir simultaneously.
When it happens
Trigger: Between the initial `path.lstat()` and the subsequent `os.open` + `os.fstat`, the path was replaced (different dev/ino) or changed type. This is a race/tamper signal, not a normal operational condition.
Common situations: Another process rewriting the transcript file concurrently with the proposer run, or an adversarial artifact that hot-swaps the path. Extremely rare in normal single-user use.
Related errors
- transcript artifact changed while reading: {path}
- {label} changed while opening: {path}
- transcript artifact must be a regular non-symlink file: {pat
- transcript artifact must be owner-only: {path}
- Compound Engineering plugin file changed during validation:
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/9002b01396ef5754.
Report an issue: GitHub.