mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
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:
@@ -16,7 +16,7 @@ When user says "COM":
|
||||
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`
|
||||
|
||||
**Version**: 0.1411 (must match `package.json`)
|
||||
**Version**: 0.1412 (must match `package.json`)
|
||||
|
||||
## Project Overview
|
||||
|
||||
@@ -69,11 +69,23 @@ journalctl --user -u claudeman-web -f
|
||||
| `src/session-manager.ts` | Session lifecycle, cleanup |
|
||||
| `src/respawn-controller.ts` | State machine for autonomous cycling |
|
||||
| `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/web/server.ts` | Fastify REST API + SSE at `/api/events` |
|
||||
| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows |
|
||||
| `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
|
||||
|
||||
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)
|
||||
- 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 |
|
||||
|--------|-----|---------|
|
||||
| Terminal | 2MB | 1.5MB |
|
||||
| Text output | 1MB | 768KB |
|
||||
| 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
|
||||
|
||||
| Topic | Location |
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
+32
-20
@@ -315,6 +315,7 @@ export class Session extends EventEmitter {
|
||||
private _isWorking: boolean = false;
|
||||
private _lastPromptTime: number = 0;
|
||||
private activityTimeout: NodeJS.Timeout | null = null;
|
||||
private _awaitingIdleConfirmation: boolean = false; // Prevents timeout reset during idle detection
|
||||
private _taskTracker: TaskTracker;
|
||||
|
||||
// Token tracking for auto-clear
|
||||
@@ -922,34 +923,44 @@ export class Session extends EventEmitter {
|
||||
// Detect if Claude is working or at prompt
|
||||
// The prompt line contains "❯" when waiting for input
|
||||
if (data.includes('❯') || data.includes('\u276f')) {
|
||||
// Reset activity timeout - if no activity for 2 seconds after prompt, Claude is idle
|
||||
if (this.activityTimeout) clearTimeout(this.activityTimeout);
|
||||
this.activityTimeout = setTimeout(() => {
|
||||
// Emit idle if either:
|
||||
// 1. Claude was working and is now at prompt (normal case)
|
||||
// 2. Session just started and is ready (status is 'busy' but _isWorking is false)
|
||||
const wasWorking = this._isWorking;
|
||||
const isInitialReady = this._status === 'busy' && !this._isWorking;
|
||||
if (wasWorking || isInitialReady) {
|
||||
this._isWorking = false;
|
||||
this._status = 'idle';
|
||||
this._lastPromptTime = Date.now();
|
||||
this.emit('idle');
|
||||
}
|
||||
}, IDLE_DETECTION_DELAY_MS);
|
||||
// Only start a new timeout if we're not already awaiting idle confirmation
|
||||
// This prevents status bar redraws (which include ❯) from resetting the timer
|
||||
if (!this._awaitingIdleConfirmation) {
|
||||
if (this.activityTimeout) clearTimeout(this.activityTimeout);
|
||||
this._awaitingIdleConfirmation = true;
|
||||
this.activityTimeout = setTimeout(() => {
|
||||
this._awaitingIdleConfirmation = false;
|
||||
// Emit idle if either:
|
||||
// 1. Claude was working and is now at prompt (normal case)
|
||||
// 2. Session just started and is ready (status is 'busy' but _isWorking is false)
|
||||
const wasWorking = this._isWorking;
|
||||
const isInitialReady = this._status === 'busy' && !this._isWorking;
|
||||
if (wasWorking || isInitialReady) {
|
||||
this._isWorking = false;
|
||||
this._status = 'idle';
|
||||
this._lastPromptTime = Date.now();
|
||||
this.emit('idle');
|
||||
}
|
||||
}, IDLE_DETECTION_DELAY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect when Claude starts working (thinking, writing, etc)
|
||||
if (data.includes('Thinking') || data.includes('Writing') || data.includes('Reading') ||
|
||||
data.includes('Running') || data.includes('⠋') || data.includes('⠙') ||
|
||||
data.includes('⠹') || data.includes('⠸') || data.includes('⠼') ||
|
||||
data.includes('⠴') || data.includes('⠦') || data.includes('⠧')) {
|
||||
// Strip ANSI/OSC sequences to avoid false positives from window titles like "3 File Reading Task"
|
||||
const cleanDataForWorkingCheck = data.replace(ANSI_ESCAPE_PATTERN, '');
|
||||
if (cleanDataForWorkingCheck.includes('Thinking') || cleanDataForWorkingCheck.includes('Writing') ||
|
||||
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) {
|
||||
this._isWorking = true;
|
||||
this._status = 'busy';
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -959,6 +970,7 @@ export class Session extends EventEmitter {
|
||||
this.ptyProcess = null;
|
||||
this._pid = null;
|
||||
this._status = 'idle';
|
||||
this._awaitingIdleConfirmation = false;
|
||||
// Clear all timers to prevent memory leaks
|
||||
if (this.activityTimeout) {
|
||||
clearTimeout(this.activityTimeout);
|
||||
|
||||
Reference in New Issue
Block a user