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:
@@ -116,6 +116,39 @@ class SessionMonitor extends EventEmitter {
|
|||||||
this._poll();
|
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.
|
* Get all agent directories under transcriptDir.
|
||||||
* @private
|
* @private
|
||||||
|
|||||||
@@ -220,6 +220,28 @@ class StatusWatcher extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_onFileChange(fullPath) {
|
_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
|
// Only process .jsonl files
|
||||||
if (!fullPath.endsWith('.jsonl')) return;
|
if (!fullPath.endsWith('.jsonl')) return;
|
||||||
|
|
||||||
|
|||||||
@@ -375,6 +375,27 @@ async function startDaemon() {
|
|||||||
monitor.pollNow();
|
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) ----
|
// ---- Session Update (from watcher) ----
|
||||||
watcher.on('session-update', (sessionKey, state) => {
|
watcher.on('session-update', (sessionKey, state) => {
|
||||||
const box = activeBoxes.get(sessionKey);
|
const box = activeBoxes.get(sessionKey);
|
||||||
|
|||||||
Reference in New Issue
Block a user