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;