fix: ghost watch false-positive reactivation on trailing writes

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.
This commit is contained in:
Xen
2026-03-10 08:38:24 +00:00
parent f1d3ae9c4c
commit 5c4a665fb9

View File

@@ -341,9 +341,21 @@ class StatusWatcher extends EventEmitter {
// Ghost watch: file changed for a completed session — signal immediate re-detection // Ghost watch: file changed for a completed session — signal immediate re-detection
if (sessionKey.startsWith('\x00ghost:')) { if (sessionKey.startsWith('\x00ghost:')) {
const originalKey = sessionKey.slice(7); 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 // Do NOT delete ghost entry here — let caller clean up after pollNow confirms the session
if (this.logger) { 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); this.emit('session-reactivate', originalKey, fullPath);
return; return;