From f81413ab2179feafef325eb939585bc45c74fff6 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:59:33 -0700 Subject: [PATCH] perf(rate-limits): throttle statusline usage posts to one curl per pane per 15s (#9829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(rate-limits): throttle statusline usage posts to one curl per pane per 15s The managed Claude statusline script posted on every statusLine tick carrying rate_limits — ~3 curl spawns/sec per streaming pane, multiplied across concurrent panes. The service drops same-value posts inside its 30s dedupe window anyway, so most spawns bought nothing. Gate the post on a per-pane stamp file: POSIX compares date +%s against the stamp; Windows uses an all-builtin seconds-of-day parse of %TIME% (octal-safe, no extra process). Both fail open — unparseable time, garbage stamp, or midnight wrap posts rather than darkening the live usage feed — and the stamp only advances when a post actually fires, so skipped ticks never defer the next one. Measured: 30 rapid rate-limit ticks spawn 1 curl (was 30). * perf(rate-limits): keep throttled statusline ticks process-free Use Claude's monotonic session duration for the POSIX throttle so skipped ticks do not replace curl churn with date churn, while retaining a fail-open date fallback. Key temp files by the stable leaf UUID so path-like or long host tab IDs cannot disable the throttle. * fix(rate-limits): preserve throttle isolation across upgrades * fix(rate-limits): reject leading-zero stamp values before arithmetic All-digits validation still admitted values like 008, which are invalid octal inside $(( )) — and dash treats that expansion error as fatal, so the script died before rewriting the stamp and the pane's live usage feed stayed dark until the file was deleted. Allow-list canonical decimals (same pattern as the duration parse) on both the stamp and the computed clock so malformed values fail open to posting. Verified under dash: the old digits-only check aborts at the arithmetic (Illegal number: 008); the allow-list survives and posts. * perf(rate-limits): eliminate POSIX statusline cat churn * test(rate-limits): exercise overlapping statusline ticks The stamp check/write is deliberately lock-free (a lock could wedge the feed closed; fail-open is the contract), so a truly concurrent burst may post more than once, bounded by overlap width — an exact at-most-one assertion would be flaky by design. Assert the invariants that do hold: every overlapping run exits 0, the raced stamp lands valid, and it throttles the following ticks. --- src/main/claude/statusline-script.test.ts | 269 +++++++++++++++++++- src/main/claude/statusline-script.ts | 90 ++++++- src/shared/claude-statusline-rate-limits.ts | 5 + 3 files changed, 357 insertions(+), 7 deletions(-) diff --git a/src/main/claude/statusline-script.test.ts b/src/main/claude/statusline-script.test.ts index 36261adfc48..1f5f3f6d51b 100644 --- a/src/main/claude/statusline-script.test.ts +++ b/src/main/claude/statusline-script.test.ts @@ -1,4 +1,9 @@ +import { spawn } from 'node:child_process' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' +import { CLAUDE_STATUSLINE_MIN_POST_INTERVAL_SECONDS } from '../../shared/claude-statusline-rate-limits' import { getManagedStatusLineScript } from './statusline-script' const ORIGINAL_PLATFORM = process.platform @@ -53,10 +58,12 @@ describe('getManagedStatusLineScript (win32 local)', () => { it('posts the buffered payload file and deletes it afterwards', () => { stubPlatform('win32') const script = getManagedStatusLineScript('local') - // Why: the temp file is per-pane (pane key with ":" mapped to "_") because %RANDOM% - // collides across cmd instances spawned in the same second. + // Why: the stable leaf UUID stays filename-safe even when a host-supplied tab id does not, + // while the delimiter replacement keeps a surviving legacy numeric key valid on Windows. + expect(script).toContain('set "ORCA_STATUSLINE_PANE_ID=%ORCA_PANE_KEY:~-36%"') + expect(script).toContain('set "ORCA_STATUSLINE_PANE_ID=%ORCA_STATUSLINE_PANE_ID::=_%"') expect(script).toContain( - 'set "ORCA_STATUSLINE_PAYLOAD_FILE=%TEMP%\\orca-claude-statusline-%ORCA_PANE_KEY::=_%.tmp"' + 'set "ORCA_STATUSLINE_PAYLOAD_FILE=%TEMP%\\orca-claude-statusline-%ORCA_STATUSLINE_PANE_ID%.tmp"' ) expect(script).toContain('--data-urlencode "payload@%ORCA_STATUSLINE_PAYLOAD_FILE%"') expect(script).not.toContain('payload@-') @@ -89,4 +96,260 @@ describe('getManagedStatusLineScript (win32 local)', () => { expect(paneGuardIndex).toBeLessThan(captureIndex) expect(script).toContain(':orca_agent_hook_drain_stdin') }) + + it('throttles with an all-builtin seconds-of-day stamp that fails open to posting', () => { + stubPlatform('win32') + const script = getManagedStatusLineScript('local') + const captureIndex = script.indexOf('more.com') + const stampIndex = script.indexOf( + 'set "ORCA_STATUSLINE_STAMP_FILE=%TEMP%\\orca-claude-statusline-last-%ORCA_STATUSLINE_PANE_ID%.tmp"' + ) + const throttleIndex = script.indexOf( + `if %ORCA_STATUSLINE_ELAPSED% GEQ 0 if %ORCA_STATUSLINE_ELAPSED% LSS ${CLAUDE_STATUSLINE_MIN_POST_INTERVAL_SECONDS} goto :orca_statusline_cleanup` + ) + const findstrIndex = script.indexOf('findstr.exe') + const stampWriteIndex = script.indexOf( + 'if defined ORCA_STATUSLINE_NOW (>"%ORCA_STATUSLINE_STAMP_FILE%" echo %ORCA_STATUSLINE_NOW%)' + ) + const tokenGuardIndex = script.indexOf('if "%ORCA_AGENT_HOOK_TOKEN%"=="" goto') + const curlIndex = script.indexOf('curl.exe') + // Why: the check precedes findstr so throttled ticks skip that spawn too, but the stamp + // only advances after every post guard passes — skipped ticks must not defer the next post. + expect(stampIndex).toBeGreaterThan(captureIndex) + expect(throttleIndex).toBeGreaterThan(stampIndex) + expect(throttleIndex).toBeLessThan(findstrIndex) + expect(stampWriteIndex).toBeGreaterThan(tokenGuardIndex) + expect(stampWriteIndex).toBeLessThan(curlIndex) + // Fail-open shape: undefined elapsed (unparseable time/stamp) proceeds to the probe. + expect(script).toContain('if not defined ORCA_STATUSLINE_ELAPSED goto :orca_statusline_probe') + expect(script).toContain( + 'for /f "delims=0123456789" %%d in ("%ORCA_STATUSLINE_LAST%") do set "ORCA_STATUSLINE_LAST="' + ) + expect(script).toContain( + 'if defined ORCA_STATUSLINE_NOW if defined ORCA_STATUSLINE_LAST set /a "ORCA_STATUSLINE_ELAPSED=ORCA_STATUSLINE_NOW-ORCA_STATUSLINE_LAST" 2>nul' + ) + // cmd parses leading-zero numbers as octal; 1%%x %% 100 defuses 08/09. + expect(script).toContain('(1%%a %% 100)*3600+(1%%b %% 100)*60+(1%%c %% 100)') + expect(script).toContain('set "ORCA_STATUSLINE_TIME=%TIME: =0%"') + }) +}) + +describe('statusline curl throttle (posix)', () => { + it('checks the per-pane stamp after the env guards and before curl', () => { + stubPlatform('darwin') + const script = getManagedStatusLineScript('local') + const envGuardIndex = script.indexOf('-z "$ORCA_AGENT_HOOK_PORT"') + const durationIndex = script.indexOf('"total_duration_ms"') + const stampIndex = script.indexOf('orca-claude-statusline-last-${orca_statusline_pane_id}') + const intervalIndex = script.indexOf(`-lt ${CLAUDE_STATUSLINE_MIN_POST_INTERVAL_SECONDS}`) + const curlIndex = script.indexOf('curl -sS') + expect(envGuardIndex).toBeLessThan(stampIndex) + expect(stampIndex).toBeLessThan(durationIndex) + expect(durationIndex).toBeLessThan(intervalIndex) + expect(intervalIndex).toBeLessThan(curlIndex) + // Fail-open shape: non-numeric date output or stamp content must never suppress the post. + // Why: the allow-list (not a mere digits check) matters — leading-zero values like 008 are + // invalid octal inside $(( )) and abort the whole script under dash, wedging the stamp. + expect(script).toContain( + 'case "$orca_statusline_now" in 0|[1-9]|[1-9][0-9]*) ;; *) orca_statusline_now=' + ) + expect(script).toContain( + 'case "$orca_statusline_last" in 0|[1-9]|[1-9][0-9]*) ;; *) orca_statusline_last=' + ) + }) +}) + +describe.skipIf(process.platform === 'win32')('statusline curl throttle (posix behavioral)', () => { + const LEAF_ID = '00000000-0000-4000-8000-000000000000' + const PANE_KEY = 'tab-1:00000000-0000-4000-8000-000000000000' + const dirs: string[] = [] + + afterEach(() => { + for (const dir of dirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }) + } + }) + + function rateLimitPayload(durationMs: number): string { + return JSON.stringify({ + cost: { total_duration_ms: durationMs }, + rate_limits: { five_hour: { used_percentage: 12 } } + }) + } + + function makeHarness(): { + scriptPath: string + dir: string + curlLog: string + payloadLog: string + dateLog: string + catLog: string + } { + const dir = mkdtempSync(join(tmpdir(), 'orca-statusline-throttle-')) + dirs.push(dir) + const curlLog = join(dir, 'curl.log') + const payloadLog = join(dir, 'payload.log') + const dateLog = join(dir, 'date.log') + const catLog = join(dir, 'cat.log') + const scriptPath = join(dir, 'statusline.sh') + writeFileSync(scriptPath, getManagedStatusLineScript('posix')) + const binDir = join(dir, 'stub-bin') + mkdirSync(binDir, { recursive: true }) + writeFileSync( + join(binDir, 'curl'), + `#!/bin/sh\n/bin/cat > "${payloadLog}"\nprintf 'x\\n' >> "${curlLog}"\nexit 0\n`, + { mode: 0o755 } + ) + writeFileSync( + join(binDir, 'date'), + `#!/bin/sh\nprintf 'x\\n' >> "${dateLog}"\nprintf '2000000000\\n'\n`, + { mode: 0o755 } + ) + writeFileSync(join(binDir, 'cat'), `#!/bin/sh\nprintf 'x\\n' >> "${catLog}"\n/bin/cat "$@"\n`, { + mode: 0o755 + }) + return { scriptPath, dir, curlLog, payloadLog, dateLog, catLog } + } + + function runScript( + scriptPath: string, + dir: string, + payload: string, + paneKey = PANE_KEY + ): Promise { + return new Promise((resolve, reject) => { + const child = spawn('sh', [scriptPath], { + env: { + PATH: `${join(dir, 'stub-bin')}:${process.env.PATH ?? ''}`, + TMPDIR: dir, + ORCA_AGENT_HOOK_PORT: '65535', + ORCA_AGENT_HOOK_TOKEN: 'test-token', + ORCA_PANE_KEY: paneKey + }, + stdio: ['pipe', 'ignore', 'pipe'] + }) + let stderr = '' + child.stderr.on('data', (chunk: Buffer) => { + stderr += chunk.toString() + }) + child.on('error', reject) + child.on('close', (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`statusline script exited ${code}: ${stderr}`)) + } + }) + child.stdin.write(payload) + child.stdin.end() + }) + } + + function lineCount(logPath: string): number { + try { + return readFileSync(logPath, 'utf8').split('\n').filter(Boolean).length + } catch { + return 0 + } + } + + function stampPathFor(dir: string, leafId = LEAF_ID): string { + return join(dir, `orca-claude-statusline-last-${leafId}`) + } + + it('spawns one curl and no capture or clock subprocesses across 30 rapid ticks', async () => { + const { scriptPath, dir, curlLog, dateLog, catLog } = makeHarness() + for (let index = 0; index < 30; index += 1) { + await runScript(scriptPath, dir, rateLimitPayload(1_000 + index * 100)) + } + expect(lineCount(curlLog)).toBe(1) + expect(lineCount(dateLog)).toBe(0) + expect(lineCount(catLog)).toBe(0) + expect(readFileSync(stampPathFor(dir), 'utf8')).toMatch(/^[0-9]+$/) + }) + + it('posts again once the interval has elapsed', async () => { + const { scriptPath, dir, curlLog } = makeHarness() + await runScript(scriptPath, dir, rateLimitPayload(1_000)) + await runScript(scriptPath, dir, rateLimitPayload(16_000)) + expect(lineCount(curlLog)).toBe(2) + }) + + it('stays bounded and keeps a valid stamp under overlapping same-pane ticks', async () => { + const { scriptPath, dir, curlLog } = makeHarness() + // Why: the stamp check/write is deliberately lock-free — a lock could wedge the feed closed, + // and fail-open is the contract — so a truly concurrent burst may post more than once, + // bounded by the overlap width. The deterministic invariants are: every overlapping run + // exits 0, the raced stamp still lands valid, and it throttles the very next ticks. + await Promise.all( + Array.from({ length: 10 }, () => runScript(scriptPath, dir, rateLimitPayload(1_000))) + ) + const burstPosts = lineCount(curlLog) + expect(burstPosts).toBeGreaterThanOrEqual(1) + expect(readFileSync(stampPathFor(dir), 'utf8')).toBe('1') + for (let index = 0; index < 5; index += 1) { + await runScript(scriptPath, dir, rateLimitPayload(2_000 + index * 100)) + } + expect(lineCount(curlLog)).toBe(burstPosts) + }) + + it('preserves multiline payloads while capturing stdin with shell builtins', async () => { + const { scriptPath, dir, payloadLog } = makeHarness() + const payload = JSON.stringify(JSON.parse(rateLimitPayload(1_000)), null, 2) + await runScript(scriptPath, dir, payload) + expect(readFileSync(payloadLog, 'utf8')).toBe(payload) + }) + + it('fails open on a garbage stamp even early in a session', async () => { + const { scriptPath, dir, curlLog } = makeHarness() + writeFileSync(stampPathFor(dir), 'not-a-number') + await runScript(scriptPath, dir, rateLimitPayload(1_000)) + expect(lineCount(curlLog)).toBe(1) + expect(readFileSync(stampPathFor(dir), 'utf8')).toMatch(/^[0-9]+$/) + }) + + it('fails open and repairs a leading-zero stamp instead of dying in arithmetic', async () => { + const { scriptPath, dir, curlLog } = makeHarness() + // Why: 008 is all-digits but invalid octal inside $(( )) — under dash the old digits-only + // check made the script abort before rewriting the stamp, wedging this pane's feed dark. + writeFileSync(stampPathFor(dir), '008') + await runScript(scriptPath, dir, rateLimitPayload(1_000)) + expect(lineCount(curlLog)).toBe(1) + expect(readFileSync(stampPathFor(dir), 'utf8')).toBe('1') + }) + + it('uses the clock fallback when the payload omits session duration', async () => { + const { scriptPath, dir, curlLog, dateLog } = makeHarness() + const payload = '{"rate_limits":{"five_hour":{"used_percentage":12}}}' + await runScript(scriptPath, dir, payload) + await runScript(scriptPath, dir, payload) + expect(lineCount(curlLog)).toBe(1) + expect(lineCount(dateLog)).toBe(2) + }) + + it('uses only the stable leaf id for temp files', async () => { + const { scriptPath, dir, curlLog } = makeHarness() + const paneKey = `${'path/segment/'.repeat(30)}tab:${LEAF_ID}` + await runScript(scriptPath, dir, rateLimitPayload(1_000), paneKey) + await runScript(scriptPath, dir, rateLimitPayload(2_000), paneKey) + expect(lineCount(curlLog)).toBe(1) + expect(readFileSync(stampPathFor(dir), 'utf8')).toBe('1') + }) + + it('keeps legacy numeric pane ids isolated by tab', async () => { + const { scriptPath, dir, curlLog } = makeHarness() + await runScript(scriptPath, dir, rateLimitPayload(1_000), 'legacy-tab-a:1') + await runScript(scriptPath, dir, rateLimitPayload(1_000), 'legacy-tab-b:1') + expect(lineCount(curlLog)).toBe(2) + expect(readFileSync(stampPathFor(dir, 'legacy-tab-a_1'), 'utf8')).toBe('1') + expect(readFileSync(stampPathFor(dir, 'legacy-tab-b_1'), 'utf8')).toBe('1') + }) + + it('never touches curl or the stamp for payloads without rate_limits', async () => { + const { scriptPath, dir, curlLog, dateLog } = makeHarness() + await runScript(scriptPath, dir, '{"model":{"id":"claude-fable-5"}}') + expect(lineCount(curlLog)).toBe(0) + expect(lineCount(dateLog)).toBe(0) + expect(() => readFileSync(stampPathFor(dir), 'utf8')).toThrow() + }) }) diff --git a/src/main/claude/statusline-script.ts b/src/main/claude/statusline-script.ts index 36c20f5f7a1..65830af6407 100644 --- a/src/main/claude/statusline-script.ts +++ b/src/main/claude/statusline-script.ts @@ -1,12 +1,15 @@ import { - buildPosixHookPayloadCapture, buildWindowsHookStdinDrainEpilogue, WINDOWS_HOOK_STDIN_DRAIN_LABEL, WINDOWS_HOOK_STDIN_READER } from '../agent-hooks/hook-stdin-contract' -import { CLAUDE_STATUSLINE_PATHNAME } from '../../shared/claude-statusline-rate-limits' +import { + CLAUDE_STATUSLINE_MIN_POST_INTERVAL_SECONDS, + CLAUDE_STATUSLINE_PATHNAME +} from '../../shared/claude-statusline-rate-limits' const STATUSLINE_CLEANUP_LABEL = 'orca_statusline_cleanup' +const STATUSLINE_PROBE_LABEL = 'orca_statusline_probe' // Why: Claude Code pipes `rate_limits` to the statusLine command on every turn; forwarding // it gives Orca live usage without spending the OAuth usage endpoint's tight budget. @@ -18,10 +21,26 @@ export function getManagedStatusLineScript(target: 'local' | 'posix' = 'local'): 'setlocal', // Why: pane key is static PTY env (the endpoint file never sets it), so it can gate before stdin is consumed. `if "%ORCA_PANE_KEY%"=="" goto :${WINDOWS_HOOK_STDIN_DRAIN_LABEL}`, + // Why: current keys end in a UUID; replacing the legacy delimiter also keeps surviving numeric-pane keys filename-safe. + 'set "ORCA_STATUSLINE_PANE_ID=%ORCA_PANE_KEY:~-36%"', + 'set "ORCA_STATUSLINE_PANE_ID=%ORCA_STATUSLINE_PANE_ID::=_%"', // Why: cmd has no builtin stdin capture, so buffer the payload in a per-pane temp file // (%RANDOM% collides across same-second cmd spawns) to guard before any curl spawn. - 'set "ORCA_STATUSLINE_PAYLOAD_FILE=%TEMP%\\orca-claude-statusline-%ORCA_PANE_KEY::=_%.tmp"', + 'set "ORCA_STATUSLINE_PAYLOAD_FILE=%TEMP%\\orca-claude-statusline-%ORCA_STATUSLINE_PANE_ID%.tmp"', `${WINDOWS_HOOK_STDIN_READER} >"%ORCA_STATUSLINE_PAYLOAD_FILE%" 2>nul`, + // Why: an all-builtin seconds-of-day throttle avoids spawning findstr+curl on every streaming tick. + 'set "ORCA_STATUSLINE_STAMP_FILE=%TEMP%\\orca-claude-statusline-last-%ORCA_STATUSLINE_PANE_ID%.tmp"', + 'set "ORCA_STATUSLINE_NOW="', + 'set "ORCA_STATUSLINE_TIME=%TIME: =0%"', + 'for /f "tokens=1-3 delims=:.," %%a in ("%ORCA_STATUSLINE_TIME%") do set /a "ORCA_STATUSLINE_NOW=(1%%a %% 100)*3600+(1%%b %% 100)*60+(1%%c %% 100)" 2>nul', + 'set "ORCA_STATUSLINE_LAST="', + 'set "ORCA_STATUSLINE_ELAPSED="', + 'if exist "%ORCA_STATUSLINE_STAMP_FILE%" set /p ORCA_STATUSLINE_LAST=<"%ORCA_STATUSLINE_STAMP_FILE%"', + 'if defined ORCA_STATUSLINE_LAST for /f "delims=0123456789" %%d in ("%ORCA_STATUSLINE_LAST%") do set "ORCA_STATUSLINE_LAST="', + 'if defined ORCA_STATUSLINE_NOW if defined ORCA_STATUSLINE_LAST set /a "ORCA_STATUSLINE_ELAPSED=ORCA_STATUSLINE_NOW-ORCA_STATUSLINE_LAST" 2>nul', + `if not defined ORCA_STATUSLINE_ELAPSED goto :${STATUSLINE_PROBE_LABEL}`, + `if %ORCA_STATUSLINE_ELAPSED% GEQ 0 if %ORCA_STATUSLINE_ELAPSED% LSS ${CLAUDE_STATUSLINE_MIN_POST_INTERVAL_SECONDS} goto :${STATUSLINE_CLEANUP_LABEL}`, + `:${STATUSLINE_PROBE_LABEL}`, // Why: rate_limits appears only for Claude.ai-subscriber sessions after the first API response; the // statusline ticks ~3x/sec during streaming, so skip the endpoint call and curl spawn otherwise. // Why: \" is the MSVC argv escape — findstr sees the quoted JSON key, so a cwd containing rate_limits can't false-match (POSIX guard parity). @@ -31,6 +50,8 @@ export function getManagedStatusLineScript(target: 'local' | 'posix' = 'local'): 'if defined ORCA_AGENT_HOOK_ENDPOINT if exist "%ORCA_AGENT_HOOK_ENDPOINT%" call "%ORCA_AGENT_HOOK_ENDPOINT%" 2>nul', `if "%ORCA_AGENT_HOOK_PORT%"=="" goto :${STATUSLINE_CLEANUP_LABEL}`, `if "%ORCA_AGENT_HOOK_TOKEN%"=="" goto :${STATUSLINE_CLEANUP_LABEL}`, + // Why: stamp only when a post is certain, so skipped ticks (no rate_limits, missing port/token) never push the next allowed post out. + 'if defined ORCA_STATUSLINE_NOW (>"%ORCA_STATUSLINE_STAMP_FILE%" echo %ORCA_STATUSLINE_NOW%)', // Why: pre-build the field from an always-defined variable so an unset CLAUDE_CONFIG_DIR posts // empty (matching POSIX and the null attribution snapshot), never a literal %VAR% token. 'set "ORCA_STATUSLINE_CONFIG_DIR_FIELD=configDir="', @@ -58,7 +79,15 @@ export function getManagedStatusLineScript(target: 'local' | 'posix' = 'local'): return [ '#!/bin/sh', - ...buildPosixHookPayloadCapture(), + // Why: this runs on every statusline tick; builtin capture avoids replacing curl churn with cat churn. + 'payload=', + 'while IFS= read -r orca_statusline_line || [ -n "$orca_statusline_line" ]; do', + ' payload="${payload}${orca_statusline_line}\n"', + 'done', + 'payload=${payload%?}', + 'if [ -z "$payload" ]; then', + ' exit 0', + 'fi', // Why: rate_limits appears only for Claude.ai-subscriber sessions after the first API response; skip the post (and its curl spawn) otherwise. 'case "$payload" in', ' *\'"rate_limits"\'*) ;;', @@ -70,6 +99,59 @@ export function getManagedStatusLineScript(target: 'local' | 'posix' = 'local'): 'if [ -z "$ORCA_AGENT_HOOK_PORT" ] || [ -z "$ORCA_AGENT_HOOK_TOKEN" ] || [ -z "$ORCA_PANE_KEY" ]; then', ' exit 0', 'fi', + // Why: the stable leaf UUID avoids path-unsafe and overlong user-supplied tab ids. + 'orca_statusline_pane_id=${ORCA_PANE_KEY##*:}', + // Why: pre-migration numeric leaf ids were tab-local, so include a safe tab id to avoid cross-pane throttle collisions after upgrade. + 'case "$orca_statusline_pane_id" in', + " ''|*[!0-9]*) ;;", + ' *)', + ' orca_statusline_tab_id=${ORCA_PANE_KEY%:*}', + ' case "$orca_statusline_tab_id" in', + " ''|*[!A-Za-z0-9._-]*) ;;", + ' *) orca_statusline_pane_id="${orca_statusline_tab_id}_${orca_statusline_pane_id}" ;;', + ' esac', + ' ;;', + 'esac', + 'orca_statusline_stamp="${TMPDIR:-/tmp}/orca-claude-statusline-last-${orca_statusline_pane_id}"', + // Why: the payload clock keeps throttled ticks free of subprocesses; date is only a schema-drift fallback. + 'orca_statusline_now=', + 'case "$payload" in', + ' *\'"total_duration_ms"\'*)', + ' orca_statusline_duration=${payload#*\'"total_duration_ms"\'}', + ' orca_statusline_duration=${orca_statusline_duration#*:}', + ' orca_statusline_duration=${orca_statusline_duration#"${orca_statusline_duration%%[![:space:]]*}"}', + ' orca_statusline_duration=${orca_statusline_duration%%[!0-9]*}', + ' case "$orca_statusline_duration" in', + ' 0|[1-9]|[1-9][0-9]*)', + ' if [ "${#orca_statusline_duration}" -le 15 ]; then', + ' orca_statusline_now=$((orca_statusline_duration / 1000))', + ' fi', + ' ;;', + ' esac', + ' ;;', + 'esac', + 'if [ -z "$orca_statusline_now" ]; then', + ' orca_statusline_now=$(date +%s 2>/dev/null) || orca_statusline_now=', + 'fi', + // Why: leading zeros read as octal inside $(( )), and a bad constant (008) is FATAL in dash — + // the script would die before rewriting the stamp, wedging the pane dark. Allow-list canonical + // decimals so any malformed value fails open to posting instead. + 'case "$orca_statusline_now" in 0|[1-9]|[1-9][0-9]*) ;; *) orca_statusline_now= ;; esac', + 'if [ -n "$orca_statusline_now" ] && [ -f "$orca_statusline_stamp" ]; then', + ' orca_statusline_last=', + ' IFS= read -r orca_statusline_last <"$orca_statusline_stamp" 2>/dev/null || :', + ' case "$orca_statusline_last" in 0|[1-9]|[1-9][0-9]*) ;; *) orca_statusline_last= ;; esac', + ' if [ "${#orca_statusline_last}" -gt 15 ]; then orca_statusline_last=; fi', + ' if [ -n "$orca_statusline_last" ]; then', + ' orca_statusline_elapsed=$((orca_statusline_now - orca_statusline_last))', + ` if [ "$orca_statusline_elapsed" -ge 0 ] && [ "$orca_statusline_elapsed" -lt ${CLAUDE_STATUSLINE_MIN_POST_INTERVAL_SECONDS} ]; then`, + ' exit 0', + ' fi', + ' fi', + 'fi', + 'if [ -n "$orca_statusline_now" ]; then', + ' printf \'%s\' "$orca_statusline_now" >"$orca_statusline_stamp" 2>/dev/null || :', + 'fi', `printf '%s' "$payload" | curl -sS -X POST "http://127.0.0.1:\${ORCA_AGENT_HOOK_PORT}${CLAUDE_STATUSLINE_PATHNAME}" \\`, ' --connect-timeout 0.5 --max-time 1.5 \\', ' -H "Content-Type: application/x-www-form-urlencoded" \\', diff --git a/src/shared/claude-statusline-rate-limits.ts b/src/shared/claude-statusline-rate-limits.ts index c2ac5df727b..4484809d531 100644 --- a/src/shared/claude-statusline-rate-limits.ts +++ b/src/shared/claude-statusline-rate-limits.ts @@ -4,6 +4,11 @@ export const CLAUDE_STATUSLINE_PATHNAME = '/statusline/claude' +// Why: the statusline ticks ~3x/sec while streaming and the service drops same-value posts +// inside LIVE_CLAUDE_INGEST_DEDUPE_MS (30s) anyway; a per-pane client floor below that bound +// keeps the usage bar live while capping curl spawns at one per pane per interval. +export const CLAUDE_STATUSLINE_MIN_POST_INTERVAL_SECONDS = 15 + export type ClaudeStatusLineWindow = { used_percentage?: number /** OAuth-usage-shaped sibling field (0-100); accepted so a CLI schema drift degrades instead of going dark. */