mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 07:59: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`
|
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
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user