feat: lock file trigger for instant session activation

The gateway writes a .jsonl.lock file the instant it starts processing a
user message — before any JSONL content is written. This is the earliest
possible infrastructure signal that a session became active.

Previously the status box only appeared after the first JSONL write (first
assistant response token), meaning turns with no tool calls showed nothing.

Changes:
- status-watcher.js: _onFileChange handles .jsonl.lock events, emits
  'session-lock' (known session) or 'session-lock-path' (unknown session)
- watcher-manager.js: wires session-lock/session-lock-path to clearCompleted
  + pollNow for immediate reactivation from lock file event
- session-monitor.js: findSessionByFile() looks up session key by transcript
  path for lock events on sessions not yet in fileToSession map;
  _getAgentIds() helper for directory enumeration

Result: status box appears the moment the gateway receives the user message,
not when the first reply token is written.
This commit is contained in:
sol
2026-03-09 18:46:52 +00:00
parent b0cb6db2a3
commit 0bdfaaa01d
3 changed files with 76 additions and 0 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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);