diff --git a/docs/architecture-invariants.md b/docs/architecture-invariants.md index d4484540..c6282ade 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -81,7 +81,7 @@ OMP's registry entry declares `privilegedParams: []` and a launch spec that only Further detail: ⚠️ **Auto-reconnect revives ONLY when the durable remote tmux session is verifiably still alive** (`remoteTmuxSessionAlive()`, a `has-session` probe over ssh, #355): a clean agent exit (Ctrl-C, Ctrl-D, `exit`) tears that session down, and `isPaneDead()` cannot tell it from a transport drop, so the watcher used to relaunch a FRESH agent after every clean exit (claude only looked fine because its `|| --resume` fallback masked it). An unreachable host answers `undefined`, which also means do not revive. ⚠️ `has-session` prints NOTHING on success, so the probe is classified by EXIT STATUS (`classifyRemoteAliveExit`: 0 alive, ssh's 255 or a timeout unknown, anything else gone); reading stdout classified every live session as gone and silently disabled transport-drop reconnects. The answer is cached per session and forgotten whenever the pane is seen alive again, or a stale `true` from one transport drop would revive the next clean exit. -File reads in a remote case are the second ssh surface (see Remote SSH cases below). The reason writes, office previews and thumbnails are deliberately unsupported over ssh is that no remote file is ever copied onto the server's disk. ssh children are bounded because terminal output in a remote session is written on the remote host, so a prompt-injected agent there can print hundreds of `codeman://attach` links. The third surface is a remote session's full-history load, which reads the scrollback from the remote tmux (`src/remote-pane-history.ts`) through the same limiter, with a bounded slot wait (`maxWaitMs`) since the local capture is its fallback. +⚠️ The image watcher never watches a remote session's `workingDir` (a path on the remote host, which it would watch on the LOCAL disk): every call site that starts a watch skips `session.remote` (`setupSessionListeners()`, the app-wide toggle in `PUT /api/settings`, and `POST /api/sessions/:id/image-watcher`, which refuses `enabled: true` with `INVALID_INPUT`), so a new call site must too. File reads in a remote case are the second ssh surface (see Remote SSH cases below). The reason writes, office previews and thumbnails are deliberately unsupported over ssh is that no remote file is ever copied onto the server's disk. ssh children are bounded because terminal output in a remote session is written on the remote host, so a prompt-injected agent there can print hundreds of `codeman://attach` links. The third surface is a remote session's full-history load, which reads the scrollback from the remote tmux (`src/remote-pane-history.ts`) through the same limiter, with a bounded slot wait (`maxWaitMs`) since the local capture is its fallback. ### Remote SSH cases diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js index 45c3af07..1e5961d4 100644 --- a/src/web/public/session-ui.js +++ b/src/web/public/session-ui.js @@ -3152,9 +3152,17 @@ Object.assign(CodemanApp.prototype, { async toggleSessionImageWatcher() { if (!this.editingSessionId) return; - const enabled = document.getElementById('modalImageWatcherEnabled').checked; + const toggle = document.getElementById('modalImageWatcherEnabled'); + const enabled = toggle.checked; try { - await this._apiPost(`/api/sessions/${this.editingSessionId}/image-watcher`, { enabled }); + const res = await this._apiPost(`/api/sessions/${this.editingSessionId}/image-watcher`, { enabled }); + // `_apiPost` hands back the Response without checking it, so a refused request (a 400) must not + // read as a save: put the switch back and say so. + if (!res?.ok) { + toggle.checked = !enabled; + this.showToast('Failed to toggle image watcher', 'error'); + return; + } // Update local session state const session = this.sessions.get(this.editingSessionId); if (session) {