Skip to content

fix: don't point the Ralph @fix_plan.md watcher at a remote session's directory - #722

Merged
Ark0N merged 1 commit into
Ark0N:masterfrom
timkjr:fix/ralph-watcher-remote
Oct 28, 2026
Merged

Ark0N merged 1 commit into
Ark0N:masterfrom
timkjr:fix/ralph-watcher-remote

Conversation

@timkjr

@timkjr timkjr commented Oct 27, 2026

Copy link
Copy Markdown

What

setupSessionListeners() hands every non-external session's workingDir to the Ralph tracker (session.ralphTracker.setWorkingDir). For a remote-SSH session that directory is a path on the remote host, but RalphFixPlanWatcher reads and fs.watch()es it on the local disk. The Ralph tracker auto-enables on pattern detection, so this runs for remote sessions whether or not the user ever turned Ralph on.

Observed on a server with two remote sessions on another host, once per remote session at every server start:

[RalphFixPlanWatcher] Could not watch @fix_plan.md: Error: ENOENT: no such file or directory, watch '/home/<user>/homelab-infrastructure'

Two side effects beyond the log noise:

  • Where a directory with the same path happens to exist locally, the watcher watches (and imports todos from) the wrong tree.
  • isFileAuthoritative is _fixPlanPath !== null, so setting a path that can never be read made the fix-plan file "authoritative" for every remote session, which switched off the tracker's output-based todo completion (detectAllTasksComplete / detectTaskCompletion return early).

Fix

One condition next to the existing external-CLI skip in src/web/server.ts:

if (!isExternalCliMode(session.mode) && !session.remote) {
  session.ralphTracker.setWorkingDir(session.workingDir);
}

Every path that wires a session (create, /interactive, /interactive-respawn, /shell, reboot restore) goes through setupSessionListeners(), and restore rehydrates session.remote before it runs, so the one guard covers them all. Docker-case sessions are unchanged: their workingDir is the host-side workspace, which is local.

Tests

test/ralph-fix-plan-remote.test.ts drives the real WebServer.setupSessionListeners() (same pattern as discard-partially-built-session.test.ts):

  • a remote session: the tracker is never given the directory, nothing logs Could not watch @fix_plan.md, and isFileAuthoritative stays false (fails on master: the remote path is passed to setWorkingDir);
  • a local session: still gets its directory.

npm run typecheck, lint, format:check and the full npm test pass locally.

Not covered here (same root cause, left for separate PRs if wanted)

These also treat a remote session's workingDir as a local path:

  • POST /api/sessions/:id/fix-plan/write and GET .../fix-plan/read (ralph-routes.ts) read and write @fix_plan.md on the local disk;
  • imageWatcher.watchSession(session.id, session.workingDir) a few lines below the fix (quiet, because chokidar waits on a missing path, but it would report local files if the same path exists locally).

🤖 Generated with Claude Code

… directory

A remote-SSH session's workingDir is a path on the remote host, but
setupSessionListeners() handed it to the Ralph tracker, whose
RalphFixPlanWatcher reads and fs.watch()es it on the LOCAL disk. Where the
path does not exist locally (the usual case), every server start logged

  [RalphFixPlanWatcher] Could not watch @fix_plan.md: Error: ENOENT ...

once per remote session; where a same-named local directory did exist, it
watched the wrong tree. Setting the path also made the tracker treat that
unreadable file as authoritative (isFileAuthoritative is "a path is set"),
which switched off its output-based todo completion for remote sessions.

Skip the tracker's working dir for remote sessions, next to the existing
external-CLI skip.
@Ark0N

Ark0N commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Thank you @timkjr, and welcome! This PR stops the Ralph tracker from pointing its @fix_plan.md watcher at a remote SSH session's working directory, which is a path on the remote host that was being read and watched on the local disk.

I checked every path that wires a session (create, /interactive, /interactive-respawn, respawn, Ralph start, reboot restore, scheduled runs and boot recovery): they all go through setupSessionListeners(), and Session.remote is set in the constructor, so the one guard covers them all. Leaving Docker sessions alone is right, since their workspace is bind-mounted at the same path. I also ran your test against master's server.ts: it fails there and passes with the fix, so it is a real regression test.

Nothing needs to change on your side. The only touches at merge time are a one-line docs note that the @fix_plan.md auto-load does not run for remote sessions, and rewrapping the header comment line at test/ralph-fix-plan-remote.test.ts:12.

The two follow-ups you listed (the fix-plan/write and fix-plan/read routes in ralph-routes.ts, and imageWatcher.watchSession in setupSessionListeners()) have the same root cause, and a separate PR for either one is welcome. Thanks again for the careful write-up.

@Ark0N
Ark0N merged commit 0a56c14 into Ark0N:master Oct 28, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 28, 2026
Note in docs/remote-sessions.md that the @fix_plan.md auto-load does not
run for remote sessions, and rewrap the test's header comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 28, 2026
Ark0N pushed a commit that referenced this pull request Oct 28, 2026
…722)

#722 stopped handing a remote session's workingDir to the Ralph tracker,
which also left isFileAuthoritative false for remote sessions only. That
flag is what keeps the output-based completion guessers off, and it is
true for every local session with a working directory, so a remote loop
could end, and mark every todo done, on a prose line such as "All tasks
completed" while a local one cannot.

setRemoteWorkingDir() records the path without reading or watching the
local disk, so the fix plan stays authoritative as for a local session;
the file still reaches the tracker only through fix-plan/read, over ssh.
The test pins both, and fails on the previous head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 28, 2026
…eview, #722)

setRemoteWorkingDir() records the path for authority only, and today
nothing else reads it for a remote session (loadFixPlanFromDisk's only
callers are setWorkingDir and the local watcher). The watcher now
remembers the remote mode and refuses both the local read and the local
watch for it, so a future caller cannot import a same-named local twin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Merged in 1.47.0 (https://github.com/Ark0N/Codeman/releases/tag/codeman%401.47.0). Thank you for tracking this down from a real log line. At merge I added the note to docs/remote-sessions.md and rewrapped the test header (74fc3ea). The release review changed one thing on top (1ad9db7, ca0f891): a remote session now RECORDS its path through a new setRemoteWorkingDir() instead of getting none, so isFileAuthoritative stays true for it. That flag is true for every local session with a working directory, so the output-based guessers are off there; leaving it false for remote sessions alone would have let a prose line like "All tasks completed" mark every todo done and end a remote loop when it cannot end a local one. Nothing is read or watched on the local disk for a remote session (the watcher refuses both in remote mode), and your test now pins the authority, the guessers staying off, and a same-named local @fix_plan.md never being imported. Whether those guessers should run at all is a fair question for a separate change, for local and remote sessions together.

@timkjr
timkjr deleted the fix/ralph-watcher-remote branch October 28, 2026 15:25
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.

2 participants