Skip to content

test(sandbox): prove #455 end to end against a live Daytona sandbox - #459

Open
khaliqgant wants to merge 8 commits into
mainfrom
proof/455-sandbox-e2e
Open

test(sandbox): prove #455 end to end against a live Daytona sandbox#459
khaliqgant wants to merge 8 commits into
mainfrom
proof/455-sandbox-e2e

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Sep 2, 2026

Copy link
Copy Markdown
Member

Verification lane for #457. I did not write that PR and this does not change it — this is the missing evidence that it works on the real path.

Verdict: CONFIRMED. Based on fix/455-state-json-single-writer so the diff here is only the proof.

Why this exists

#457's evidence is sandboxInitialSyncGuard, a Go transcription of the sandbox readiness guard. The transcription is faithful — but no sandbox had ever provisioned with this build, so the claim was untested where it actually fails. This closes that gap.

The guard here is imported from @agent-relay/sandbox at origin/main cfdf801 (v0.1.14, the version cloud/packages/core pins), not reimplemented, and invoked through buildRelayfileMountInitialSyncBackgroundShell — the detached launcher, because Daytona's exec proxy read-times-out around 120s and cannot host a real initial sync in the foreground.

Both exit codes

Sandbox 8a7d6049-ca63-447b-abdf-f750f121c994, remote root /github/repos/AgentWorkforce/relay, 5,685 files. Both arms run the identical generated command against the identical subtree and differ only in which relayfile-mount is first on PATH.

arm A — snapshot-baked arm B — built from 4e3c1105
guard exit 75 0
files mirrored 2,000 5,685
bootstrap non-null, filesSynced: 2000 key absent
lastSuccessfulReconcileAt 2026-09-02T12:18:24.303486294Z 2026-09-02T12:47:48.742036968Z
status bootstrapping ready
state.json sha256 3d59ae71dff2…37dc1d43 39329c7c14f5…14dad359

Raw .relay/state.json bytes for both arms are committed under docs/evidence/daytona-455-initial-sync-readiness-20260902/raw/, with the sha256 taken inside the sandbox.

All four claimed steps in one run (arm A):

bootstrap file budget reached (2000 files this cycle, max 2000); yielding at entry 170
mount full-tree traversal summary ... files_seen=2000 traversal_complete=false traversal_failed=false
mount sync cycle completed                                  <-- --once reports SUCCESS
relayfile initial sync paused before complete readiness      <-- guard exits 75

And the fix (arm B): 2,000 → 4,000 → 5,685 across three resume cycles, then initial sync: bootstrap complete.

The control is tight. Both arms' first cycles are numerically identical — list_calls=6 entries_seen=3986 files_seen=2000 directories_seen=1986 bytes_seen=26734874, both yielding at entry 170 with 887 directories pending. Same fixture, same budget, same stopping point; they diverge only in what happens next.

The harness fails closed

scripts/test-sandbox-initial-sync-readiness-e2e.mjs exits 2 (UNKNOWN), not 0, when it cannot provision, when an arm times out, or when arm A passes — a must-fail control that does not fail means the fixture never crossed the 2,000-file budget and neither arm proves anything.

Three things found on the way that are not in the claim

  1. Publishing fix(mount): make --once reach a complete reconcile, and stop the two state.json writers clobbering each other #457 will not reach any sandbox. The mount binary is baked into the Daytona snapshot, not resolved at provision time — snapshot names encode it, and the newest snapshot built 2026-09-02 still carries v0.10.50. A snapshot rebuild and a fleet pin bump are required on top of the merge.

  2. The snapshot's label disagrees with its contents. Named v0.10.50, but @relayfile/mount-linux-x64 inside is 0.10.51, and three different relayfile-mount binaries with three different sha256s are present. Which one runs depends on PATH.

  3. tokenIngress: 'env' in @agent-relay/sandbox is broken. It renders RELAYFILE_MOUNT_TOKEN=, but cmd/relayfile-mount/main.go reads only RELAYFILE_TOKEN / RELAYFILE_MOUNT_CREDS_FILE. No build — including this PR's head — reads it. Observed directly: token is required (--token, RELAYFILE_TOKEN, or --creds-file), exit 1, before the guard ran. These runs used 'creds-file', which is what fleet uses.

One caveat on the fix

The fixed build removes the bootstrap key rather than setting it to null. The guard tests state.bootstrap != null, and loose != treats undefined as null, so it passes. A strict !== null would have failed it. Guard and writer agree today only by virtue of a loose comparison — worth pinning deliberately.

Not merging, and nothing was pushed to fix/455-state-json-single-writer. Khaliq holds the merge gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R22c8M6BAYQykoDjVEmMtv

Review in cubic

…ete reconcile

`relayfile-mount --once` returned as soon as one reconcile finished. A single
reconcile mirrors at most defaultBootstrapMaxFilesPerCycle (2000) files, then
persists a resume cursor and yields with traversal_complete=false while
markSyncSuccess still stamps lastSuccessfulReconcileAt. So on any workspace
larger than that budget `--once` exited 0 with a non-null `bootstrap` block
still in .relay/state.json.

AgentWorkforce/sandbox reads exactly that field as the initial-sync readiness
barrier and exits 75 (TEMPFAIL, "relayfile initial sync paused before complete
readiness"). That is the mechanism behind #455's 100% JIT-provision failure
rate: it is structural, not the race hypothesised from #412 -- the sandbox runs
this binary, which has a single .relay/state.json writer.

--once now resumes the persisted traversal checkpoint until the bootstrap
completes. The loop is bounded by root-context cancellation, a terminal error,
and a no-progress guard keyed on every resumable coordinate the public
bootstrap block exposes (filesSynced alone can be flat while the directory
queue advances). Cancellation returns nil so the exit code keeps its historical
meaning and the downstream guard still reports a resumable TEMPFAIL, and a
cycle that *failed* is never retried -- that keeps one transient cloud error
from escalating into a bootstrap stall.

Recreate-then-verify: TestInitialSyncOnceSatisfiesSandboxReadinessGuard drives
the same path with a 5-file/cycle budget and applies the sandbox's own guard
verbatim. It fails with "state.bootstrap != null" on the pre-fix code and
passes here.

Refs #455

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDWHJzWsSYwv9fCtzhZrj

Session-Id: 8817afd3-0a74-42ab-8841-5e358ded0641
…state

`<localDir>/.relay/state.json` has two writers in one process emitting disjoint
schemas to the same path, roughly once per second: mountsync's savePublicState
and the CLI's writeMirrorStateFile. The mirror writer serialized its own struct
over the whole file, so whichever writer ran last decided which half of the
schema existed -- consumers saw a document with no `providers` (mountsync won)
or no `files`/`counters`/`states`/`circuit` (the CLI won), and every guard keyed
on the missing half silently failed open.

The mirror writer now merges: it clears the keys it owns -- so a cleared field
(a drained stallReason, a resolved lastError) is not resurrected -- then
overlays its snapshot, leaving mountsync's fields intact. mirrorStateOwnedKeys
is pinned to the syncStateFile JSON surface by a reflection test so it cannot
drift. The merged write is compact, matching the mountsync writer, because the
document now carries the per-file map and this write fires on every
local-change batch.

Two consequences of the same file that this also closes:

- writeMirrorStateFile stamped lastReconcileAt = now unconditionally, making
  the document fresh by construction, so no consumer could ever observe a stale
  mount. It now only stamps when the mount reported no reconcile time.
- The CLI daemon's mountsync.NewSyncer call omitted Interval, so the syncer's
  interval was the zero value and its public state advertised intervalMs: 0 --
  which makes every consumer's staleness check early-return "fresh" forever.

Recreate-then-verify: TestMirrorStateWriteKeepsMountsyncFields runs a CLI mount
cycle and asserts both writers' fields survive in one document. On the pre-fix
code it reports localRoot, syncMode, states, files, counters and staleAfter all
clobbered.

Not addressed here, both on other surfaces: the SDK's isMountStateReady
fail-open on an absent `providers` array, and the failedWritebacks lost update
across the two writers.

Refs #412

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDWHJzWsSYwv9fCtzhZrj

Session-Id: 8817afd3-0a74-42ab-8841-5e358ded0641
…s rules

#412 named this file as a documented public contract that three guides tell
agents to read, but it did not say what the two in-process writers guarantee,
and #455 turned on a readiness rule the contract never stated.

Record both: a write from either writer must not remove the other's keys, an
absent key means "not reported" rather than "the other writer won", and a
non-null `bootstrap` block means the mount is not ready however fresh
`lastSuccessfulReconcileAt` looks.

Refs #412, #455

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDWHJzWsSYwv9fCtzhZrj

Session-Id: 8817afd3-0a74-42ab-8841-5e358ded0641
Codex flagged on PR #457 that the previous commit fixed only one direction:
the CLI mirror writer stopped deleting mountsync's keys, but mountsync still
serialized publicState over the whole document and deleted the CLI's.

The path it named is real. In the CLI mount daemon's timer loop, a healthy
websocket plus watcher takes the non-reconcile branch, which calls
RefreshRealtimeStateWithContext -> saveStateWithoutLocalScan ->
savePublicState with no following writeSnapshot. Every such refresh removed
`providers`, `daemon` and `guards` from the published document until some
later snapshot happened to run.

Both writers now go through internal/mountstate, declaring the keys they own:

- Merge/MergeFunc replace exactly those keys and preserve every other key,
  including ones the build does not know about.
- One process-wide lock replaces the arrangement #412 documented, where one
  writer's mutex appeared to synchronize against a writer that could not take
  it.
- The document holds json.RawMessage values, so merging a public state that
  carries a multi-megabyte per-file map does not deep-decode it on a path that
  runs on every reconcile and every local-change batch.

That lock also closes #412's lost update. Both writers now read
`failedWritebacks` inside the same lock hold as the write that carries it
forward, and incrementFailedWritebacksInState became mountstate.Increment.
TestIncrementSurvivesConcurrentMerges lands all 200 increments against a
concurrently republishing writer under -race; before, increments were lost in
5 of 6 runs.

Removes the now-dead failedWritebacksStateMu, readPersistedFailedWritebacksUnlocked,
uint64FromJSONValue and readPublicFailedWritebacks.

Recreate-then-verify: TestSavePublicStateKeepsCLIMirrorFields reports
`providers`, `daemon` and `guards` clobbered when only the savePublicState
merge is reverted.

Refs #412

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDWHJzWsSYwv9fCtzhZrj

Session-Id: 8817afd3-0a74-42ab-8841-5e358ded0641
…cellation

cubic flagged on PR #457 that TestInitialSyncOnceStopsWhenRootContextEnds
could pass by finishing normally: nothing stopped the bootstrap completing
inside the window, and the test never asserted that cancellation had fired,
so it could not catch a regression in the bound it was named for.

That is right. Measured on this machine the run did cancel -- 200 files at 2
per cycle takes longer than 2s because each cycle re-lists the tree and
rescans locally -- but nothing guaranteed it, and a faster machine would have
turned the test green for the wrong reason.

The workspace is now sized so it cannot finish (400 files, 2 per cycle, each
read delayed), and the test asserts both that the context was cancelled and
that the bootstrap is still in progress afterwards -- so a run that completes
normally fails instead of passing silently.

Timing should not be what pins a branch, so each bound also gets a
deterministic unit test against finishInitialBootstrap directly:

- an already-cancelled context returns without running a cycle;
- a failed cycle is not retried, keeping --once's single-attempt behavior so
  one transient cloud error cannot escalate into a bootstrap stall;
- a checkpoint that stops advancing stops after onceBootstrapStableCycleLimit
  cycles rather than spinning to the ceiling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDWHJzWsSYwv9fCtzhZrj

Session-Id: 8817afd3-0a74-42ab-8841-5e358ded0641
…read

cubic flagged on PR #457 that readDocumentLocked treated every read failure as
an empty document. A merge writes back everything it read, so a transient EIO
or a permissions change would have deleted the other writer's keys — the exact
clobber this package exists to prevent, reached by the code meant to prevent
it.

Read failures other than "not found" now propagate, and the write does not
happen. Three cases, each pinned by a test:

- missing file: not an error, the first write has to start somewhere;
- unreadable file: error, nothing is written, the previous document survives
  intact;
- unparseable file: still overwritten on purpose. Writes go through an atomic
  rename so a torn document should be unreachable, and refusing to rewrite a
  corrupt one would strand the mount with it forever.

`Read` keeps returning an empty document on any failure: a reader has nothing
to destroy, and its callers have no error path.

Also closes the coverage gap cubic found in the resume-loop tests.
TestFinishInitialBootstrapDoesNotRetryAFailedCycle returned an error from the
first `lastCycleErr` call, which is the pre-loop gate — so the in-loop
failed-resume-cycle branch was never executed. Split into two tests: the
pre-loop gate, and a new one where the first cycle succeeds and a resume cycle
then fails, asserting exactly one cycle ran.

Refs #412

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDWHJzWsSYwv9fCtzhZrj

Session-Id: 8817afd3-0a74-42ab-8841-5e358ded0641
PR #457's evidence was a Go transcription of the sandbox readiness guard.
The transcription is faithful, but no sandbox had ever provisioned with
this build, so the claim was untested on the real path.

This adds a two-arm proof that runs against a real Daytona sandbox and
imports the readiness guard from @agent-relay/sandbox 0.1.14 — the version
cloud pins — rather than reimplementing it. Both arms run the identical
generated command against the identical remote subtree and differ only in
which relayfile-mount is first on PATH.

Result on /github/repos/AgentWorkforce/relay (5,685 files):

  arm A, snapshot-baked binary: guard exit 75, bootstrap non-null at
    filesSynced 2000, lastSuccessfulReconcileAt stamped, status
    "bootstrapping"
  arm B, built from 4e3c110:   guard exit 0, bootstrap key absent,
    status "ready", 5,685 files across three resume cycles

Both arms' first cycles are numerically identical (entries_seen=3986
files_seen=2000 bytes_seen=26734874, both yielding at entry 170 with 887
directories pending); they diverge only in what happens after. The claim
is CONFIRMED.

The harness treats a must-fail control that does not fail as UNKNOWN
rather than green: if arm A passes, the fixture never crossed the
2000-file budget and neither arm proves anything. A timeout is UNKNOWN,
never a pass.

Raw .relay/state.json bytes from both arms are included, with the sha256
taken inside the sandbox.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R22c8M6BAYQykoDjVEmMtv

Session-Id: 6123871c-ba3b-49ec-8855-2064a56321d9
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T12:57:05.545719Z c52d5ae PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: da30ec0f-fe1c-4518-847f-a72e4ad5b43d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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: c52d5ae8ff

ℹ️ 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 scripts/test-sandbox-initial-sync-readiness-e2e.mjs Outdated
Comment thread scripts/test-sandbox-initial-sync-readiness-e2e.mjs Outdated
Comment thread scripts/test-sandbox-initial-sync-readiness-e2e.mjs Outdated
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-09-02T15-23-09-529Z-HEAD-provider
Mode: provider
Git SHA: 11f638e

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 9 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread scripts/test-sandbox-initial-sync-readiness-e2e.mjs Outdated
Comment thread scripts/test-sandbox-initial-sync-readiness-e2e.mjs Outdated
Comment thread scripts/test-sandbox-initial-sync-readiness-e2e.mjs
Comment thread scripts/test-sandbox-initial-sync-readiness-e2e.mjs Outdated
Comment thread scripts/test-sandbox-initial-sync-readiness-e2e.mjs Outdated
Comment thread docs/evidence/daytona-455-initial-sync-readiness-20260902/raw/SHA256SUMS Outdated
Comment thread docs/evidence/daytona-455-initial-sync-readiness-20260902/METHODOLOGY.md Outdated
Comment thread scripts/test-sandbox-initial-sync-readiness-e2e.mjs Outdated
Review found twelve issues on the harness itself. Because this harness
gates future merges, a weak check here becomes a false green for
everyone later, so all of them are addressed.

Correctness of the verdict:

* arm A exiting 75 is no longer accepted as a valid must-fail control on
  its own. The guard returns 75 for ANY incomplete-or-unreadable state,
  including an auth failure before a single file syncs — exactly what
  happened during the original run (403 missing required scope: fs:read,
  filesSynced 0, exit 75). The control now additionally requires a
  non-null bootstrap block at filesSynced >= 2000 and a log reporting
  both "bootstrap file budget reached" and traversal_complete=false.
* infrastructure failure is UNKNOWN, never REFUTED. validateEnv() and
  module loading previously threw outside the try, so a HarnessUnknown
  escaped as an unhandled rejection and node exited 1 — the code that
  means "the fix does not work". Everything is inside the try now.
* credential failure in either arm is UNKNOWN, and each arm mints its
  own token, so a one-hour credential cannot expire during the second
  sequential 45-minute arm and score an interrupted run as REFUTED.
* the scored state file is derived from the package's own
  resolveRelayfileMountExactLayout, the same computation the guard uses,
  so the harness cannot drift from the guard.

Resolvability:

* the import specifier @agent-relay/sandbox/relayfile/mount-script.js
  did not exist; the package exposes these through /core. Verified:
  ERR_MODULE_NOT_FOUND before the fix.
* createRequire().resolve() cannot be used either — it resolves under
  the CJS "require" condition and the package exports only import/types
  (ERR_PACKAGE_PATH_NOT_EXPORTED). Resolution now goes through a
  throwaway ESM shim written into RELAYFILE_PROOF_MODULE_BASE, which
  uses Node's real resolver with the right conditions and no hardcoded
  package internals.
* all credentials are validated before mkdtemp and the go build, so a
  missing variable costs nothing and leaves no litter in os.tmpdir().

Evidence:

* SHA256SUMS is now verifiable with `sha256sum -c` against the committed
  .gz files, with the uncompressed hashes and byte counts in comments.
* METHODOLOGY no longer claims the two arms' generated shell strings are
  byte-identical. They are not — localDir, stateDir, credsFilePath and
  runId necessarily differ per arm. What makes it a control is that
  every input governing the mechanism is the same.

Adds a red-check for the verdict logic itself, built from the real
recorded data. Against the previous logic all five adversarial cases
scored wrong — three of them reporting PROOF HELD off a false control.

The verdict is unchanged: arm A exit 75 at 2000 files, arm B exit 0 at
5685 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R22c8M6BAYQykoDjVEmMtv

Session-Id: 6123871c-ba3b-49ec-8855-2064a56321d9

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/test-sandbox-initial-sync-readiness-e2e.verdict.test.mjs">

<violation number="1" location="scripts/test-sandbox-initial-sync-readiness-e2e.verdict.test.mjs:58">
P2: The red-check never guards a false green where arm B 'passes' without mirroring anything. `verdict()` returns PROOF_HELD whenever `B.guardExit === 0 && B.bootstrapNull` — it never requires `B.mirroredFiles > 0`. Because the control arm A has already proven the subtree holds >= 2000 files, an arm B that exits 0 with zero (or a handful of) mirrored files is evidence the candidate silently failed to mount, yet the harness would report PROOF HELD and gate the merge. Add a red-check case (and a `B.mirroredFiles > 0` requirement in `verdict`) so a candidate that produced no mirror cannot be read as convergence.</violation>
</file>

<file name="scripts/test-sandbox-initial-sync-readiness-e2e.mjs">

<violation number="1" location="scripts/test-sandbox-initial-sync-readiness-e2e.mjs:370">
P1: When arm B encounters a non-auth network or server error, the harness reports REFUTED instead of UNKNOWN. Require a successful candidate traversal with no infrastructure/error signal before classifying non-convergence as a refutation.</violation>

<violation number="2" location="scripts/test-sandbox-initial-sync-readiness-e2e.mjs:393">
P2: When `RELAYFILE_PROOF_MODULE_BASE` contains a different sandbox package, this harness silently tests its guard as if it were the pinned production guard. Assert the expected package version or resolve dependencies from the pinned lockfile before running the proof.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if (B.stateMissing) {
return [EXIT_UNKNOWN, `UNKNOWN: arm B left no state file at ${B.stateFile}`];
}
if (B.guardExit !== 0 || !B.bootstrapNull) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When arm B encounters a non-auth network or server error, the harness reports REFUTED instead of UNKNOWN. Require a successful candidate traversal with no infrastructure/error signal before classifying non-convergence as a refutation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/test-sandbox-initial-sync-readiness-e2e.mjs, line 370:

<comment>When arm B encounters a non-auth network or server error, the harness reports REFUTED instead of UNKNOWN. Require a successful candidate traversal with no infrastructure/error signal before classifying non-convergence as a refutation.</comment>

<file context>
@@ -187,38 +309,116 @@ function summarize(r) {
+  if (B.stateMissing) {
+    return [EXIT_UNKNOWN, `UNKNOWN: arm B left no state file at ${B.stateFile}`];
+  }
+  if (B.guardExit !== 0 || !B.bootstrapNull) {
+    return [EXIT_PROOF_REFUTED,
+      `REFUTED: arm B exited ${B.guardExit} with bootstrap${B.bootstrapNull ? " null" : " still non-null"} `
</file context>

const A = (o = {}) => ({ ...goodControl, ...o });
const B = (o = {}) => ({ ...goodCandidate, ...o });

test("must-fire: the real observed run is a pass", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The red-check never guards a false green where arm B 'passes' without mirroring anything. verdict() returns PROOF_HELD whenever B.guardExit === 0 && B.bootstrapNull — it never requires B.mirroredFiles > 0. Because the control arm A has already proven the subtree holds >= 2000 files, an arm B that exits 0 with zero (or a handful of) mirrored files is evidence the candidate silently failed to mount, yet the harness would report PROOF HELD and gate the merge. Add a red-check case (and a B.mirroredFiles > 0 requirement in verdict) so a candidate that produced no mirror cannot be read as convergence.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/test-sandbox-initial-sync-readiness-e2e.verdict.test.mjs, line 58:

<comment>The red-check never guards a false green where arm B 'passes' without mirroring anything. `verdict()` returns PROOF_HELD whenever `B.guardExit === 0 && B.bootstrapNull` — it never requires `B.mirroredFiles > 0`. Because the control arm A has already proven the subtree holds >= 2000 files, an arm B that exits 0 with zero (or a handful of) mirrored files is evidence the candidate silently failed to mount, yet the harness would report PROOF HELD and gate the merge. Add a red-check case (and a `B.mirroredFiles > 0` requirement in `verdict`) so a candidate that produced no mirror cannot be read as convergence.</comment>

<file context>
@@ -0,0 +1,134 @@
+const A = (o = {}) => ({ ...goodControl, ...o });
+const B = (o = {}) => ({ ...goodCandidate, ...o });
+
+test("must-fire: the real observed run is a pass", () => {
+  const [code, msg] = verdict(A(), B());
+  assert.equal(code, EXIT_PROOF_HELD);
</file context>

const loader = await externalLoader();
disposeResolver = loader.dispose;
return Promise.all([
loader.load("@agent-relay/sandbox/core"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When RELAYFILE_PROOF_MODULE_BASE contains a different sandbox package, this harness silently tests its guard as if it were the pinned production guard. Assert the expected package version or resolve dependencies from the pinned lockfile before running the proof.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/test-sandbox-initial-sync-readiness-e2e.mjs, line 393:

<comment>When `RELAYFILE_PROOF_MODULE_BASE` contains a different sandbox package, this harness silently tests its guard as if it were the pinned production guard. Assert the expected package version or resolve dependencies from the pinned lockfile before running the proof.</comment>

<file context>
@@ -187,38 +309,116 @@ function summarize(r) {
+      const loader = await externalLoader();
+      disposeResolver = loader.dispose;
+      return Promise.all([
+        loader.load("@agent-relay/sandbox/core"),
+        loader.load("@daytonaio/sdk"),
+        loader.load("@cloud/core/relayfile/client.js"),
</file context>

Base automatically changed from fix/455-state-json-single-writer to main September 7, 2026 14:23
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