feat: lock file deletion = instant done signal
When the gateway deletes .jsonl.lock it means the final reply was sent.
Use this as an immediate 'turn complete' trigger instead of waiting for
cache-ttl (3s grace) or idle timeout (60s).
- status-watcher.js: _onFileChange checks if .lock file exists on event.
If deleted -> emits 'session-lock-released'. Added triggerIdle() which
cancels all timers and emits session-idle immediately.
- watcher-manager.js: wires session-lock-released/session-lock-released-path
to watcher.triggerIdle() for instant completion.
Combined with lock-created trigger (previous commit), the full lifecycle is:
User sends message
-> .jsonl.lock created -> status box appears immediately
-> JSONL writes -> status box updates in real time
-> Gateway sends reply -> .jsonl.lock deleted -> status box marks done instantly
This commit is contained in:
@@ -225,19 +225,31 @@ class StatusWatcher extends EventEmitter {
|
|||||||
// Emit 'session-lock' so watcher-manager can trigger immediate reactivation
|
// Emit 'session-lock' so watcher-manager can trigger immediate reactivation
|
||||||
// without waiting for the first JSONL write.
|
// without waiting for the first JSONL write.
|
||||||
if (fullPath.endsWith('.jsonl.lock')) {
|
if (fullPath.endsWith('.jsonl.lock')) {
|
||||||
// Derive the JSONL path and find the session key
|
|
||||||
const jsonlPath = fullPath.slice(0, -5); // strip '.lock'
|
const jsonlPath = fullPath.slice(0, -5); // strip '.lock'
|
||||||
const sessionKey = this.fileToSession.get(jsonlPath) ||
|
const lockExists = (() => { try { fs.statSync(fullPath); return true; } catch(_) { return false; } })();
|
||||||
this.fileToSession.get('\x00ghost:' + jsonlPath.replace('\x00ghost:', ''));
|
|
||||||
if (sessionKey) {
|
// Resolve session key from known sessions or ghost watch
|
||||||
const originalKey = sessionKey.startsWith('\x00ghost:') ? sessionKey.slice(7) : sessionKey;
|
let sessionKey = this.fileToSession.get(jsonlPath);
|
||||||
if (this.logger) {
|
if (sessionKey && sessionKey.startsWith('\x00ghost:')) sessionKey = sessionKey.slice(7);
|
||||||
this.logger.info({ sessionKey: originalKey }, 'Lock file detected — session started, triggering early reactivation');
|
|
||||||
|
if (lockExists) {
|
||||||
|
// Lock file CREATED — gateway started processing user message.
|
||||||
|
// Earliest possible signal: fires before any JSONL write.
|
||||||
|
if (sessionKey) {
|
||||||
|
if (this.logger) this.logger.info({ sessionKey }, 'Lock file created — session active, triggering early reactivation');
|
||||||
|
this.emit('session-lock', sessionKey);
|
||||||
|
} else {
|
||||||
|
this.emit('session-lock-path', jsonlPath);
|
||||||
}
|
}
|
||||||
this.emit('session-lock', originalKey);
|
|
||||||
} else {
|
} else {
|
||||||
// Unknown session — emit with path so watcher-manager can look it up
|
// Lock file DELETED — gateway finished the turn and sent final reply.
|
||||||
this.emit('session-lock-path', jsonlPath);
|
// Immediate idle signal: no need to wait for cache-ttl or 60s timeout.
|
||||||
|
if (sessionKey) {
|
||||||
|
if (this.logger) this.logger.info({ sessionKey }, 'Lock file deleted — turn complete, marking session done immediately');
|
||||||
|
this.emit('session-lock-released', sessionKey);
|
||||||
|
} else {
|
||||||
|
this.emit('session-lock-released-path', jsonlPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -490,6 +502,22 @@ class StatusWatcher extends EventEmitter {
|
|||||||
* Schedule an idle check for a session.
|
* Schedule an idle check for a session.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Immediately mark a session as done without waiting for idle timeout.
|
||||||
|
* Called when the lock file is deleted — the gateway has sent the final reply.
|
||||||
|
* @param {string} sessionKey
|
||||||
|
*/
|
||||||
|
triggerIdle(sessionKey) {
|
||||||
|
const state = this.sessions.get(sessionKey);
|
||||||
|
if (!state) return;
|
||||||
|
// Cancel any pending timers
|
||||||
|
if (state.idleTimer) { clearTimeout(state.idleTimer); state.idleTimer = null; }
|
||||||
|
if (state._filePollTimer) { clearInterval(state._filePollTimer); state._filePollTimer = null; }
|
||||||
|
if (this.logger) this.logger.info({ sessionKey }, 'triggerIdle: lock released — marking session done immediately');
|
||||||
|
state.status = 'done';
|
||||||
|
this.emit('session-idle', sessionKey, this._sanitizeState(state));
|
||||||
|
}
|
||||||
|
|
||||||
_scheduleIdleCheck(sessionKey, state) {
|
_scheduleIdleCheck(sessionKey, state) {
|
||||||
if (state.idleTimer) {
|
if (state.idleTimer) {
|
||||||
clearTimeout(state.idleTimer);
|
clearTimeout(state.idleTimer);
|
||||||
|
|||||||
@@ -387,7 +387,6 @@ async function startDaemon() {
|
|||||||
|
|
||||||
// Lock file for a session the watcher doesn't know about yet — look it up by path
|
// Lock file for a session the watcher doesn't know about yet — look it up by path
|
||||||
watcher.on('session-lock-path', (jsonlPath) => {
|
watcher.on('session-lock-path', (jsonlPath) => {
|
||||||
// Find the session key by matching sessionFile in sessions.json
|
|
||||||
const sessionKey = monitor.findSessionByFile(jsonlPath);
|
const sessionKey = monitor.findSessionByFile(jsonlPath);
|
||||||
if (sessionKey) {
|
if (sessionKey) {
|
||||||
logger.info({ sessionKey }, 'Lock file (path lookup): session activated — early reactivation');
|
logger.info({ sessionKey }, 'Lock file (path lookup): session activated — early reactivation');
|
||||||
@@ -396,6 +395,22 @@ async function startDaemon() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- Lock file released: turn complete, mark done immediately ----
|
||||||
|
// Gateway deletes .jsonl.lock when the final reply is sent to the user.
|
||||||
|
// This is the most reliable "done" signal — no waiting for cache-ttl or 60s idle.
|
||||||
|
watcher.on('session-lock-released', (sessionKey) => {
|
||||||
|
logger.info({ sessionKey }, 'Lock file released — turn complete, triggering immediate idle');
|
||||||
|
watcher.triggerIdle(sessionKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('session-lock-released-path', (jsonlPath) => {
|
||||||
|
const sessionKey = monitor.findSessionByFile(jsonlPath);
|
||||||
|
if (sessionKey) {
|
||||||
|
logger.info({ sessionKey }, 'Lock file released (path lookup) — triggering immediate idle');
|
||||||
|
watcher.triggerIdle(sessionKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ---- 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