fix: detect busy sessions by JSONL content, not just mtime

The previous 5-second mtime threshold caused false idle triggers during
tool calls and subtasks. Now reads the last 8KB of the JSONL to check
if the final assistant message contains tool_use — if so, Claude is
still working regardless of how long ago the file was modified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-24 21:39:11 +00:00
parent 2286aa5cc3
commit 710cb05275

View File

@@ -1,9 +1,10 @@
import { readdirSync, statSync } from "node:fs" import { readdirSync, statSync, openSync, readSync, closeSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import type { Project, SessionInfo } from "../lib/types" import type { Project, SessionInfo } from "../lib/types"
const PROJECTS_DIR = `${Bun.env.HOME}/.claude/projects` const PROJECTS_DIR = `${Bun.env.HOME}/.claude/projects`
const BUSY_THRESHOLD_MS = 5000 const BUSY_THRESHOLD_MS = 5000
const TAIL_BYTES = 8192 // read last 8KB of JSONL to find final assistant entry
export interface ActiveSession { export interface ActiveSession {
pid: string pid: string
@@ -40,6 +41,40 @@ function findActiveJsonl(projectKey: string): { path: string; mtime: number } |
} }
} }
// Check if the last assistant message in a JSONL has pending tool_use (= still working)
function lastAssistantHasToolUse(filePath: string): boolean {
try {
const st = statSync(filePath)
const size = st.size
if (size === 0) return false
const readSize = Math.min(TAIL_BYTES, size)
const buf = Buffer.alloc(readSize)
const fd = openSync(filePath, "r")
try {
readSync(fd, buf, 0, readSize, size - readSize)
} finally {
closeSync(fd)
}
const tail = buf.toString("utf-8")
const lines = tail.split("\n")
// Walk backwards to find last assistant entry
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim()
if (!line) continue
// Quick pre-check before JSON.parse
if (!line.includes('"assistant"')) continue
try {
const d = JSON.parse(line)
if (d.type !== "assistant") continue
const content = d.message?.content
if (!Array.isArray(content)) continue
return content.some((c: any) => c.type === "tool_use")
} catch {}
}
} catch {}
return false
}
function escapeAppleScript(s: string): string { function escapeAppleScript(s: string): string {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
} }
@@ -176,7 +211,10 @@ export async function detectActiveSessions(): Promise<Map<string, number>> {
const key = cwdToProjectKey(cwd) const key = cwdToProjectKey(cwd)
const jsonl = findActiveJsonl(key) const jsonl = findActiveJsonl(key)
const now = Date.now() const now = Date.now()
const busy = jsonl ? (now - jsonl.mtime) < BUSY_THRESHOLD_MS : false // Busy if: actively writing (<5s ago) OR last assistant message has tool_use (waiting for tool)
const recentlyWritten = jsonl ? (now - jsonl.mtime) < BUSY_THRESHOLD_MS : false
const pendingTool = jsonl ? lastAssistantHasToolUse(jsonl.path) : false
const busy = recentlyWritten || pendingTool
return { pid, cwd, tty, sessionFile: jsonl?.path ?? null, busy, lastActivityMs: jsonl?.mtime ?? 0 } return { pid, cwd, tty, sessionFile: jsonl?.path ?? null, busy, lastActivityMs: jsonl?.mtime ?? 0 }
} }