From 5c4a665fb96cbb61533fb06e49b2a1ac604eb5e9 Mon Sep 17 00:00:00 2001 From: Xen Date: Tue, 10 Mar 2026 08:38:24 +0000 Subject: [PATCH] fix: ghost watch false-positive reactivation on trailing writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the lock file is deleted (turn complete) and triggerIdle fires, the transcript file continues receiving writes (the agent's own reply being appended). The ghost watch was firing session-reactivate on these trailing writes, causing an immediate complete→reactivate→complete loop within the same turn. Fix: only emit session-reactivate from ghost watch if the lock file currently exists. A JSONL write without a lock file is a trailing write from the completed turn, not a new user message. --- src/status-watcher.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/status-watcher.js b/src/status-watcher.js index e8c3a20..4a8ee6b 100644 --- a/src/status-watcher.js +++ b/src/status-watcher.js @@ -341,9 +341,21 @@ class StatusWatcher extends EventEmitter { // Ghost watch: file changed for a completed session — signal immediate re-detection if (sessionKey.startsWith('\x00ghost:')) { const originalKey = sessionKey.slice(7); + // Only trigger reactivation if the lock file currently exists. + // A JSONL write without a lock file is a trailing write from the just-completed + // turn — not a new user message. Emitting reactivation here causes a false + // complete→reactivate→complete loop within the same turn. + const lockFile = fullPath + '.lock'; + const lockExists = (() => { try { fs.statSync(lockFile); return true; } catch (_) { return false; } })(); + if (!lockExists) { + if (this.logger) { + this.logger.debug({ sessionKey: originalKey }, 'fs.watch: ghost file change but no lock file — skipping reactivation (trailing write)'); + } + return; + } // Do NOT delete ghost entry here — let caller clean up after pollNow confirms the session if (this.logger) { - this.logger.info({ sessionKey: originalKey }, 'fs.watch: file change on completed session — triggering reactivation'); + this.logger.info({ sessionKey: originalKey }, 'fs.watch: file change on completed session (lock file present) — triggering reactivation'); } this.emit('session-reactivate', originalKey, fullPath); return;