diff --git a/src/session-monitor.js b/src/session-monitor.js index 34abe0c..a815cb6 100644 --- a/src/session-monitor.js +++ b/src/session-monitor.js @@ -116,6 +116,39 @@ class SessionMonitor extends EventEmitter { this._poll(); } + /** + * Find a session key by its transcript file path. + * Used when a lock file fires for a session the watcher doesn't have mapped yet. + * @param {string} jsonlPath - Absolute path to the .jsonl file + * @returns {string|null} + */ + findSessionByFile(jsonlPath) { + for (const [sessionKey, entry] of this._knownSessions) { + if (entry.sessionFile === jsonlPath) return sessionKey; + } + // Also check completed sessions + // Re-read sessions.json to find it + for (const agentId of this._getAgentIds()) { + const sessions = this._readSessionsJson(agentId); + for (const [sessionKey, entry] of Object.entries(sessions)) { + if (entry.sessionFile === jsonlPath) return sessionKey; + } + } + return null; + } + + /** + * Get all agent IDs under transcriptDir. + * @private + */ + _getAgentIds() { + try { + return fs.readdirSync(this.transcriptDir).filter(f => { + try { return fs.statSync(path.join(this.transcriptDir, f)).isDirectory(); } catch(_) { return false; } + }); + } catch(_) { return []; } + } + /** * Get all agent directories under transcriptDir. * @private diff --git a/src/status-watcher.js b/src/status-watcher.js index 94c8da2..f40f3bc 100644 --- a/src/status-watcher.js +++ b/src/status-watcher.js @@ -220,6 +220,28 @@ class StatusWatcher extends EventEmitter { * @private */ _onFileChange(fullPath) { + // Lock file appeared/changed: gateway started processing a new turn. + // This fires BEFORE any JSONL content is written — earliest possible signal. + // Emit 'session-lock' so watcher-manager can trigger immediate reactivation + // without waiting for the first JSONL write. + if (fullPath.endsWith('.jsonl.lock')) { + // Derive the JSONL path and find the session key + const jsonlPath = fullPath.slice(0, -5); // strip '.lock' + const sessionKey = this.fileToSession.get(jsonlPath) || + this.fileToSession.get('\x00ghost:' + jsonlPath.replace('\x00ghost:', '')); + if (sessionKey) { + const originalKey = sessionKey.startsWith('\x00ghost:') ? sessionKey.slice(7) : sessionKey; + if (this.logger) { + this.logger.info({ sessionKey: originalKey }, 'Lock file detected — session started, triggering early reactivation'); + } + this.emit('session-lock', originalKey); + } else { + // Unknown session — emit with path so watcher-manager can look it up + this.emit('session-lock-path', jsonlPath); + } + return; + } + // Only process .jsonl files if (!fullPath.endsWith('.jsonl')) return; diff --git a/src/watcher-manager.js b/src/watcher-manager.js index 46a9c4a..a90f245 100644 --- a/src/watcher-manager.js +++ b/src/watcher-manager.js @@ -375,6 +375,27 @@ async function startDaemon() { monitor.pollNow(); }); + // ---- Lock file reactivation (earliest possible trigger) ---- + // The gateway writes a .jsonl.lock file the instant it starts processing a turn — + // before any JSONL content is written. This is the infrastructure-level signal + // that a session became active from user input, not from the first reply line. + watcher.on('session-lock', (sessionKey) => { + logger.info({ sessionKey }, 'Lock file: session activated by user message — early reactivation'); + monitor.clearCompleted(sessionKey); + monitor.pollNow(); + }); + + // Lock file for a session the watcher doesn't know about yet — look it up by path + watcher.on('session-lock-path', (jsonlPath) => { + // Find the session key by matching sessionFile in sessions.json + const sessionKey = monitor.findSessionByFile(jsonlPath); + if (sessionKey) { + logger.info({ sessionKey }, 'Lock file (path lookup): session activated — early reactivation'); + monitor.clearCompleted(sessionKey); + monitor.pollNow(); + } + }); + // ---- Session Update (from watcher) ---- watcher.on('session-update', (sessionKey, state) => { const box = activeBoxes.get(sessionKey);