mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
Every working Claude session reported `status: "idle"` about two seconds into its turn. Measured on live workers: two sessions mid-tool-call at 13 and 17 minutes both read `idle` while their panes showed `✻ Actualizing… (13m 23s · ↓ 47.5k tokens)`. Two things had drifted apart: 1. The working indicator changed. Claude animates the glyph through `· ✢ ✳ ∗ ✻ ✽` and randomizes the gerund per turn, so neither SPINNER_PATTERN (braille, no longer drawn) nor the keyword list (Thinking/Writing/Reading/Running) matches a turn anymore. 2. A `❯` sighting is not the end of a turn. Claude redraws the composer roughly once a second all the way through one, and that redraw armed the "2s later, call it idle" timer. Matching the new status line in the STREAM does not fix it either: tmux ships partial repaints, so the complete line reached the PTY about once every 20 seconds while the `❯` arrived every second. So the decision moves off the stream: - An unbroken run of repaints marks a turn as started. Sampled once a second for 12s over six live sessions, the two working ones produced output in 12/12 windows and the four idle ones in 0/12. Pure helpers in session-activity.ts carry the thresholds. - Idle now needs the pane to go quiet AND the screen to agree. `_confirmIdle()` asks tmux what is rendered (new `capturePaneText()`, one plain `capture-pane`, floored at 1.5s per session and only ever at a transition) and re-checks every 5s while the screen still shows work. A turn can sit silent for tens of seconds inside one tool call, so silence alone proves nothing. - The same screen check vetoes keystroke echo, which is a steady stream of repaints too but is not work. CLAUDE_WORKING_LINE_PATTERN matches the `… (elapsed)` shape rather than the glyph, because the FINISHED line (`✻ Cooked for 2m 49s`) carries the same glyph and would otherwise pin a session at working forever. Claude mode only. An external CLI has no `❯`, so nothing would arm the confirmation and such a session would latch busy. respawn-patterns.hasWorkingPattern() had the same blind spot (its gerund list cannot see "Actualizing"), so it takes the pattern as an extra signal. That can only make respawn less eager, never more. Idle now lands about 3 to 5 seconds after a turn ends instead of 2 seconds into one. Verified end to end against a live worker, sampled against the CLI's own "esc to interrupt" footer as independent ground truth: busy for all 25s of a turn, idle 3s after it ended. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
94 lines
3.6 KiB
TypeScript
94 lines
3.6 KiB
TypeScript
/**
|
||
* @fileoverview Shared regex patterns for terminal and token parsing.
|
||
*
|
||
* Pre-compiled patterns avoid re-compilation overhead on each use.
|
||
* Import these patterns instead of defining them locally.
|
||
*
|
||
* @module utils/regex-patterns
|
||
*/
|
||
|
||
/**
|
||
* Comprehensive ANSI escape pattern that handles:
|
||
* - SGR (colors/styles): ESC [ params m
|
||
* - CSI sequences (cursor, scroll, etc.): ESC [ params letter
|
||
* - OSC sequences (title, etc.): ESC ] ... BEL or ESC ] ... ST
|
||
* - Single-char escapes: ESC = or ESC >
|
||
*
|
||
* Use this when you need complete ANSI stripping including OSC sequences.
|
||
* Note: Has global flag - reset lastIndex before exec() if reusing.
|
||
*/
|
||
// eslint-disable-next-line no-control-regex
|
||
export const ANSI_ESCAPE_PATTERN_FULL = /\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[=>])/g;
|
||
|
||
/**
|
||
* Simple ANSI CSI-only pattern for basic escape code stripping.
|
||
* Matches: ESC [ params letter (e.g., colors, cursor movement)
|
||
*
|
||
* Use this for faster stripping when OSC sequences aren't a concern.
|
||
* Note: Has global flag - reset lastIndex before exec() if reusing.
|
||
*/
|
||
// eslint-disable-next-line no-control-regex
|
||
export const ANSI_ESCAPE_PATTERN_SIMPLE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
||
|
||
/**
|
||
* Pattern to extract token count from Claude's status line.
|
||
* Matches: "123.4k tokens", "5234 tokens", "1.2M tokens"
|
||
*
|
||
* Capture groups:
|
||
* - Group 1: The numeric value (e.g., "123.4", "5234", "1.2")
|
||
* - Group 2: Optional suffix (k, K, m, M) or undefined
|
||
*/
|
||
export const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
|
||
|
||
/**
|
||
* Strips ANSI escape codes from text using the comprehensive pattern.
|
||
* @param text - Text containing ANSI escape codes
|
||
* @returns Text with ANSI codes removed
|
||
*/
|
||
export function stripAnsi(text: string): string {
|
||
return text.replace(ANSI_ESCAPE_PATTERN_FULL, '');
|
||
}
|
||
|
||
/**
|
||
* Regex to validate safe file paths (no shell metacharacters).
|
||
* Shared by schemas.ts and tmux-manager.ts for consistent path validation.
|
||
*/
|
||
/**
|
||
* Braille spinner characters used by Claude Code's Ink UI.
|
||
* Matches any of: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧
|
||
* Used on the hot path (every PTY chunk) — pre-compiled for performance.
|
||
*/
|
||
export const SPINNER_PATTERN = /[⠋⠙⠹⠸⠼⠴⠦⠧]/;
|
||
|
||
/**
|
||
* Claude Code's live working status line, e.g.
|
||
* `✻ Actualizing… (13m 23s · ↓ 47.5k tokens)`
|
||
* `✽ Herding… (3s · esc to interrupt)`
|
||
*
|
||
* Matched on the ELLIPSIS + elapsed timer, never on the leading glyph: the
|
||
* animation cycles through `· ✢ ✳ ∗ ✻ ✽` (two of those are ordinary punctuation)
|
||
* and the gerund is randomized per turn, while the finished line (`✻ Cooked for
|
||
* 2m 49s`) carries the same glyph with no `…` and no parenthesis. Feed this
|
||
* ANSI-STRIPPED data: tmux colours the timer separately, so the raw stream has
|
||
* escape sequences sitting between the `…` and the `(`.
|
||
*
|
||
* A sighting is proof the pane is working; its ABSENCE proves nothing, because
|
||
* tmux repaints partially and the whole line reaches the PTY only occasionally
|
||
* (see `session-activity.ts` for what carries the idle decision instead).
|
||
*/
|
||
export const CLAUDE_WORKING_LINE_PATTERN = /…\s*\((?:\d+h\s+)?(?:\d+m\s+)?\d+s\b|esc to interrupt/;
|
||
|
||
export const SAFE_PATH_PATTERN = /^[\p{L}\p{N}_/\-. ~]+$/u;
|
||
|
||
/**
|
||
* Execute a global regex pattern against data, calling the callback for each match.
|
||
* Automatically resets lastIndex before execution.
|
||
*/
|
||
export function execPattern(pattern: RegExp, data: string, callback: (match: RegExpExecArray) => void): void {
|
||
pattern.lastIndex = 0;
|
||
let match: RegExpExecArray | null;
|
||
while ((match = pattern.exec(data)) !== null) {
|
||
callback(match);
|
||
}
|
||
}
|