Skip to content

fix(mountsync): refresh state-file mtime on every bootstrap touch(), not just per-page saveState() - #416

Merged
khaliqgant merged 2 commits into
mainfrom
fix/bootstrap-watchdog-progress-mtime
Aug 13, 2026
Merged

fix(mountsync): refresh state-file mtime on every bootstrap touch(), not just per-page saveState()#416
khaliqgant merged 2 commits into
mainfrom
fix/bootstrap-watchdog-progress-mtime

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Aug 13, 2026

Copy link
Copy Markdown
Member

Summary

Tonight's Daytona fleet-node relayfile-mount repair (via AgentWorkforce/cloud's POST /api/v1/fleet/nodes/sandbox/{sandboxId}/relayfile-mount, cloud#3007) got past auth/binding/get/proof and failed at the mount phase with Failed initial relayfile sync: exit 124, root-caused via live Cloudflare Worker log tail to a false-positive external watchdog cancellation, not a real stall:

2026/08/13 09:40:52 resuming bootstrap bounded-tree pull at /github/repos/AgentWorkforce__chief-app from persisted cursor (15721 directories pending, 17209 files already synced)
relayfile initial sync made no progress for 60s; canceling
2026/08/13 09:41:48 mount full-tree traversal summary ... entries_seen=873 files_seen=758 directories_seen=115 ... duration_ms=55709 traversal_complete=false traversal_failed=true

Root cause. Two watchdogs are stacked around the bootstrap reconcile:

  1. Internal (this package, bootstrapContext/bootstrapProgress.touch()): correctly sees per-file progress — touch() already fires per ListTree page and per individual file read in readBootstrapFiles.
  2. External (AgentWorkforce/sandbox's buildIdleWatchedCommand, which wraps every relayfile-mount invocation the sandbox orchestrator launches): can only observe the --state-file's on-disk mtime as its liveness signal — it has no visibility into this process's internal state.

Before this change, the state file's mtime only advanced once per fully completed page: persistTraversal() calls saveState() only after every file in that page has been read. A page against a large tracked-file set (this workspace: 17,384 files) can spend well over a minute downloading files before the file is ever touched again — even though real internal progress (each individual file read) is happening the whole time. The external watchdog sees a frozen mtime and kills the process at 60s despite the daemon being alive and working, which then forces the next attempt to detect "non-empty state without completed bootstrap" and restart the same expensive full reconcile — a sticky failure loop.

AgentWorkforce/sandbox's orchestrator.ts already has a comment acknowledging half of this ("the daemon's atomic full export does not report progress until the body fully returns") and mitigates it by matching the two watchdogs' timeout values — that doesn't fix the granularity mismatch, it just delays the same failure to a bigger/slower page.

Fix

bootstrapProgress.touch() now also does a cheap os.Chtimes() on the configured state file on every touch — no full rewrite, just the mtime — so external mtime-polling supervisors see the same liveness signal the internal watchdog already had. Best-effort: a missing file (fresh bootstrap, before the first saveState()) or an unset path (zero-value bootstrapProgress, used in hard-cap mode before this change) silently no-ops, matching prior behavior exactly in those cases.

Test plan

  • TestBootstrapProgressTouchRefreshesStateFileMtime — direct unit test on touch()'s mtime refresh (backdate a real file, call touch(), assert mtime advanced; confirm missing-file and unset-path cases don't panic).
  • TestBootstrapProgressTouchesStateFileMidPage — end-to-end reproduction of the production shape: cycle 1 seeds real persisted (non-complete) state via an interrupted first page; cycle 2 resumes with every remaining file in one slow page (serialized reads) and polls the on-disk state file, proving its mtime advances mid-page, before persistTraversal's saveState() runs for that page.
  • Both new tests verified to fail against the pre-fix touch() (temporarily no-opped the Chtimes call) and pass with the fix — confirmed not tautological.
  • go test ./internal/mountsync/... — full package green.
  • go test ./... — full repo green (all packages).
  • go vet ./... — clean.
  • make build — all three binaries (relayfile-cli, relayfile-server, relayfile-mount) build cleanly.
  • gofmt -l — clean.

Draft — not merging tonight; Khaliq reviews and merges.

🤖 Generated with Claude Code

Review in cubic

…not just per-page saveState()

An external supervisor that starts/monitors relayfile-mount (e.g.
AgentWorkforce/sandbox's shell-level idle watchdog in
buildIdleWatchedCommand) can only observe the daemon's liveness through
the --state-file's on-disk mtime — it has no visibility into this
process's internal bootstrapProgress.touch() calls.

Before this change, the state file's mtime only advanced once per FULLY
COMPLETED page: persistTraversal() calls saveState() only after every
file in that page has been read via readBootstrapFiles. A single page
against a large tracked-file set (17k+ files) can spend well over a
minute downloading files before the file is ever touched again, even
though touch() is already firing correctly per file read and per
ListTree page for the *internal* watchdog.

Observed in production: a resumed bounded-tree bootstrap reconcile
against a 17384-file workspace made real progress (873 entries / 758
files across 4 ListTree calls in ~56s) but was killed by an external
"relayfile initial sync made no progress for 60s" watchdog anyway,
because no single page had finished within that window and the state
file's mtime never moved.

Fix: bootstrapProgress.touch() now also does a cheap os.Chtimes() on
the configured state file on every touch — no full rewrite, just the
mtime — so external mtime-polling supervisors see the same liveness
signal the internal watchdog already had. Best-effort: a missing file
(fresh bootstrap, before the first saveState()) or an unset path
(zero-value bootstrapProgress) silently no-ops, matching prior
behaviour exactly in those cases.

Adds two regression tests: a direct unit test on touch()'s mtime
refresh, and an end-to-end test that reproduces the production false-
cancel shape (a slow multi-file page resumed from persisted state) and
proves the state file's mtime advances mid-page, before
persistTraversal's saveState() runs. Both fail against the pre-fix
touch() and pass with it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 250eee64-9f66-4b93-8bca-84b8323e86e1

📥 Commits

Reviewing files that changed from the base of the PR and between bb58018 and 4830294.

📒 Files selected for processing (2)
  • internal/mountsync/bootstrap_test.go
  • internal/mountsync/syncer.go

📝 Walkthrough

Walkthrough

Bootstrap progress now initializes the private state file before traversal and refreshes its modification time on each progress touch. State persistence uses separate private and public save paths. Tests cover direct, resumed, and first-page bootstrap activity.

Changes

Bootstrap liveness tracking

Layer / File(s) Summary
Private state-file initialization and persistence
internal/mountsync/syncer.go
Bootstrap creates or verifies the private state file before traversal. Private-state and public-state persistence use separate paths.
Bootstrap initialization error propagation
internal/mountsync/syncer.go
Periodic and primary full-pull flows propagate errors returned by bootstrapContext.
Progress liveness validation
internal/mountsync/bootstrap_test.go, internal/mountsync/syncer.go
Tests cover direct mtime refreshes, unavailable state files, resumed mid-page reads, and slow first-page reads. The progress tracker preserves watchdog updates while refreshing the state-file mtime.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Mergeability Score: ⚪ Minimal · up to 48302

This localized change refreshes the state-file timestamp during bootstrap progress so external watchdogs can observe ongoing work; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant BootstrapContext
  participant Syncer
  participant StateFile
  participant BootstrapProgress
  participant FileReader
  BootstrapContext->>StateFile: initialize private state file
  BootstrapContext->>BootstrapProgress: provide state-file path
  Syncer->>FileReader: read bootstrap files
  FileReader->>BootstrapProgress: touch progress
  BootstrapProgress->>StateFile: refresh modification time
  BootstrapProgress->>Syncer: update watchdog timestamp
  Syncer->>StateFile: persist private and public state
Loading

Possibly related PRs

Suggested reviewers: miyaontherelay, kjgbot, willwashburn

Poem

A rabbit checks the state-file light,
Progress stays fresh through day and night.
Slow reads leave a timely trace,
Each checkpoint keeps its place.
Bootstrap finishes without fright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: refreshing the state-file mtime on every bootstrap progress touch.
Description check ✅ Passed The description directly explains the watchdog issue, the mtime-based fix, and the regression tests for bootstrap progress.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/bootstrap-watchdog-progress-mtime

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.

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-08-13T13-30-03-780Z-HEAD-provider
Mode: provider
Git SHA: 1d4c211

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.

@khaliqgant
khaliqgant marked this pull request as ready for review August 13, 2026 10:37
@cursor

cursor Bot commented Aug 13, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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

ℹ️ 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 internal/mountsync/syncer.go Outdated

@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.

1 issue found across 2 files

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="internal/mountsync/syncer.go">

<violation number="1" location="internal/mountsync/syncer.go:3699">
P3: touch() now issues an unbounded os.Chtimes syscall on every invocation, and touch() fires once per file read and once per ListTree page in the heavy full-pull loops. On a bootstrap over many small files this turns each per-file progress signal into a full inode metadata write on the state file with no throttling, which can add measurable metadata-writeback overhead exactly in the hot per-file loop the PR is trying to keep cheap (the same reason saveState() was deliberately avoided). Throttle the mtime refresh (e.g. at most once per ~1s or per idle-window fraction) so aggregate cost stays bounded while still keeping the file's mtime well inside the external watchdog's timeout.</violation>
</file>

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

Re-trigger cubic

Comment thread internal/mountsync/bootstrap_test.go
Comment thread internal/mountsync/syncer.go
// (before the first saveState()) — that's fine, this only needs to
// help once the file exists, which is exactly the resumed-large-
// state case where the false-cancel was observed.
_ = os.Chtimes(p.stateFile, now, now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: touch() now issues an unbounded os.Chtimes syscall on every invocation, and touch() fires once per file read and once per ListTree page in the heavy full-pull loops. On a bootstrap over many small files this turns each per-file progress signal into a full inode metadata write on the state file with no throttling, which can add measurable metadata-writeback overhead exactly in the hot per-file loop the PR is trying to keep cheap (the same reason saveState() was deliberately avoided). Throttle the mtime refresh (e.g. at most once per ~1s or per idle-window fraction) so aggregate cost stays bounded while still keeping the file's mtime well inside the external watchdog's timeout.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/mountsync/syncer.go, line 3699:

<comment>touch() now issues an unbounded os.Chtimes syscall on every invocation, and touch() fires once per file read and once per ListTree page in the heavy full-pull loops. On a bootstrap over many small files this turns each per-file progress signal into a full inode metadata write on the state file with no throttling, which can add measurable metadata-writeback overhead exactly in the hot per-file loop the PR is trying to keep cheap (the same reason saveState() was deliberately avoided). Throttle the mtime refresh (e.g. at most once per ~1s or per idle-window fraction) so aggregate cost stays bounded while still keeping the file's mtime well inside the external watchdog's timeout.</comment>

<file context>
@@ -3659,16 +3659,45 @@ func (s *Syncer) HTTPClient() (*HTTPClient, bool) {
+		// (before the first saveState()) — that's fine, this only needs to
+		// help once the file exists, which is exactly the resumed-large-
+		// state case where the false-cancel was observed.
+		_ = os.Chtimes(p.stateFile, now, now)
+	}
 }
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I evaluated debouncing and am leaving this thread open for Khaliq’s call. I do not recommend adding it in this fix: each Chtimes follows a successful remote ReadFile (plus comparatively few ListTree/page signals), so even at 17k files the local metadata syscall is dominated by the network read and local materialization already paid per file. Debouncing would add concurrent timing/coordination state to bootstrapProgress and weaken the simple guarantee that every real progress signal is externally visible, while the wrapper timeout is outside this package and may vary. If production profiling shows metadata pressure, a timeout-aware rate limit can be added with an explicit contract and benchmark.

@khaliqgant
khaliqgant merged commit 081efd1 into main Aug 13, 2026
10 checks passed
@khaliqgant
khaliqgant deleted the fix/bootstrap-watchdog-progress-mtime branch August 13, 2026 20:19
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