fix(session): prevent isWorking flag from getting stuck true

- Add _awaitingIdleConfirmation flag to prevent status bar redraws from
  resetting the 2-second idle detection timeout
- Strip ANSI/OSC escape sequences before checking working patterns to
  avoid false positives from window titles like '3 File Reading Task'
- Reset _awaitingIdleConfirmation on PTY exit for proper cleanup

chore: bump version to 0.1412

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-28 05:34:25 +01:00
co-authored by Claude Opus 4.5
parent 1ff89c8251
commit 077e296750
3 changed files with 60 additions and 23 deletions
+27 -2
View File
@@ -16,7 +16,7 @@ When user says "COM":
1. Increment version in BOTH `package.json` AND `CLAUDE.md` 1. Increment version in BOTH `package.json` AND `CLAUDE.md`
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` 2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
**Version**: 0.1411 (must match `package.json`) **Version**: 0.1412 (must match `package.json`)
## Project Overview ## Project Overview
@@ -69,11 +69,23 @@ journalctl --user -u claudeman-web -f
| `src/session-manager.ts` | Session lifecycle, cleanup | | `src/session-manager.ts` | Session lifecycle, cleanup |
| `src/respawn-controller.ts` | State machine for autonomous cycling | | `src/respawn-controller.ts` | State machine for autonomous cycling |
| `src/ralph-tracker.ts` | Detects `<promise>PHRASE</promise>`, todos | | `src/ralph-tracker.ts` | Detects `<promise>PHRASE</promise>`, todos |
| `src/subagent-watcher.ts` | Monitors Claude Code's Task tool (background agents) |
| `src/run-summary.ts` | Timeline events for "what happened while away" |
| `src/ai-idle-checker.ts` | AI-powered idle detection with `ai-checker-base.ts` |
| `src/plan-orchestrator.ts` | Multi-agent plan generation | | `src/plan-orchestrator.ts` | Multi-agent plan generation |
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` | | `src/web/server.ts` | Fastify REST API + SSE at `/api/events` |
| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows | | `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows |
| `src/types.ts` | All TypeScript interfaces | | `src/types.ts` | All TypeScript interfaces |
### Utility Files (`src/utils/`)
| File | Purpose |
|------|---------|
| `lru-map.ts` | LRU eviction Map for bounded caches |
| `stale-expiration-map.ts` | TTL-based Map with lazy expiration |
| `cleanup-manager.ts` | Centralized resource disposal |
| `buffer-accumulator.ts` | Chunk accumulator with size limits |
### Data Flow ### Data Flow
1. Session spawns `claude --dangerously-skip-permissions` via node-pty 1. Session spawns `claude --dangerously-skip-permissions` via node-pty
@@ -140,14 +152,27 @@ The app must stay fast with 20 sessions and 50 agent windows:
- Debounced state persistence (500ms) - Debounced state persistence (500ms)
- SSE batching (16ms) - SSE batching (16ms)
## Buffer Limits ## Resource Limits
Limits are centralized in `src/config/buffer-limits.ts` and `src/config/map-limits.ts`.
**Buffer limits** (per session):
| Buffer | Max | Trim To | | Buffer | Max | Trim To |
|--------|-----|---------| |--------|-----|---------|
| Terminal | 2MB | 1.5MB | | Terminal | 2MB | 1.5MB |
| Text output | 1MB | 768KB | | Text output | 1MB | 768KB |
| Messages | 1000 | 800 | | Messages | 1000 | 800 |
**Map limits** (global):
| Resource | Max |
|----------|-----|
| Tracked agents | 500 |
| Concurrent sessions | 50 |
| SSE clients total | 100 |
| File watchers | 500 |
Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-based cleanup.
## Where to Find More Information ## Where to Find More Information
| Topic | Location | | Topic | Location |
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claudeman", "name": "claudeman",
"version": "0.1411", "version": "0.1412",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
+32 -20
View File
@@ -315,6 +315,7 @@ export class Session extends EventEmitter {
private _isWorking: boolean = false; private _isWorking: boolean = false;
private _lastPromptTime: number = 0; private _lastPromptTime: number = 0;
private activityTimeout: NodeJS.Timeout | null = null; private activityTimeout: NodeJS.Timeout | null = null;
private _awaitingIdleConfirmation: boolean = false; // Prevents timeout reset during idle detection
private _taskTracker: TaskTracker; private _taskTracker: TaskTracker;
// Token tracking for auto-clear // Token tracking for auto-clear
@@ -922,34 +923,44 @@ export class Session extends EventEmitter {
// Detect if Claude is working or at prompt // Detect if Claude is working or at prompt
// The prompt line contains "❯" when waiting for input // The prompt line contains "❯" when waiting for input
if (data.includes('❯') || data.includes('\u276f')) { if (data.includes('❯') || data.includes('\u276f')) {
// Reset activity timeout - if no activity for 2 seconds after prompt, Claude is idle // Only start a new timeout if we're not already awaiting idle confirmation
if (this.activityTimeout) clearTimeout(this.activityTimeout); // This prevents status bar redraws (which include ❯) from resetting the timer
this.activityTimeout = setTimeout(() => { if (!this._awaitingIdleConfirmation) {
// Emit idle if either: if (this.activityTimeout) clearTimeout(this.activityTimeout);
// 1. Claude was working and is now at prompt (normal case) this._awaitingIdleConfirmation = true;
// 2. Session just started and is ready (status is 'busy' but _isWorking is false) this.activityTimeout = setTimeout(() => {
const wasWorking = this._isWorking; this._awaitingIdleConfirmation = false;
const isInitialReady = this._status === 'busy' && !this._isWorking; // Emit idle if either:
if (wasWorking || isInitialReady) { // 1. Claude was working and is now at prompt (normal case)
this._isWorking = false; // 2. Session just started and is ready (status is 'busy' but _isWorking is false)
this._status = 'idle'; const wasWorking = this._isWorking;
this._lastPromptTime = Date.now(); const isInitialReady = this._status === 'busy' && !this._isWorking;
this.emit('idle'); if (wasWorking || isInitialReady) {
} this._isWorking = false;
}, IDLE_DETECTION_DELAY_MS); this._status = 'idle';
this._lastPromptTime = Date.now();
this.emit('idle');
}
}, IDLE_DETECTION_DELAY_MS);
}
} }
// Detect when Claude starts working (thinking, writing, etc) // Detect when Claude starts working (thinking, writing, etc)
if (data.includes('Thinking') || data.includes('Writing') || data.includes('Reading') || // Strip ANSI/OSC sequences to avoid false positives from window titles like "3 File Reading Task"
data.includes('Running') || data.includes('⠋') || data.includes('⠙') || const cleanDataForWorkingCheck = data.replace(ANSI_ESCAPE_PATTERN, '');
data.includes('⠹') || data.includes('⠸') || data.includes('⠼') || if (cleanDataForWorkingCheck.includes('Thinking') || cleanDataForWorkingCheck.includes('Writing') ||
data.includes('⠴') || data.includes('⠦') || data.includes('⠧')) { cleanDataForWorkingCheck.includes('Reading') || cleanDataForWorkingCheck.includes('Running') ||
cleanDataForWorkingCheck.includes('⠋') || cleanDataForWorkingCheck.includes('⠙') ||
cleanDataForWorkingCheck.includes('⠹') || cleanDataForWorkingCheck.includes('⠸') ||
cleanDataForWorkingCheck.includes('⠼') || cleanDataForWorkingCheck.includes('⠴') ||
cleanDataForWorkingCheck.includes('⠦') || cleanDataForWorkingCheck.includes('⠧')) {
if (!this._isWorking) { if (!this._isWorking) {
this._isWorking = true; this._isWorking = true;
this._status = 'busy'; this._status = 'busy';
this.emit('working'); this.emit('working');
} }
// Reset timeout since Claude is active // Reset timeout and idle confirmation flag since Claude is active
this._awaitingIdleConfirmation = false;
if (this.activityTimeout) clearTimeout(this.activityTimeout); if (this.activityTimeout) clearTimeout(this.activityTimeout);
} }
}); });
@@ -959,6 +970,7 @@ export class Session extends EventEmitter {
this.ptyProcess = null; this.ptyProcess = null;
this._pid = null; this._pid = null;
this._status = 'idle'; this._status = 'idle';
this._awaitingIdleConfirmation = false;
// Clear all timers to prevent memory leaks // Clear all timers to prevent memory leaks
if (this.activityTimeout) { if (this.activityTimeout) {
clearTimeout(this.activityTimeout); clearTimeout(this.activityTimeout);