Summary
When using specstory run claude with multiple concurrent sessions, the specstory parent process consumes 20–50% CPU per instance even when the wrapped claude child process is completely idle (waiting for user input at the prompt).
This appears to be a busy-wait / active polling loop in specstory's I/O monitoring of the child process, rather than using blocking I/O primitives (select/poll/epoll/kqueue).
Environment
- specstory version: 1.12.0
- OS: Darwin arm64 (macOS)
- claude version: 2.1.83 (Claude Code)
- invocation:
specstory run claude -c /path/to/claude --settings /path/to/settings.json --silent --no-version-check --local-time-zone
Reproduction
- Open multiple terminal sessions (20+) each running
specstory run claude
- Leave all sessions idle at the Claude prompt (waiting for user input)
- Observe CPU usage with
ps aux | grep specstory
Observed Behavior
With 21 concurrent sessions all idle (claude children waiting for input):
| # |
specstory CPU% |
specstory state |
claude child CPU% |
claude child state |
| 1 |
46.0 |
R+ |
0.0 |
S+ |
| 2 |
42.4 |
R+ |
0.0 |
S+ |
| 3 |
31.2 |
R+ |
0.0 |
S+ |
| 4 |
22.8 |
R+ |
0.0 |
S+ |
| 5 |
35.1 |
R+ |
0.0 |
S+ |
| 6 |
37.9 |
R+ |
0.0 |
S+ |
| 7 |
30.2 |
R+ |
0.0 |
S+ |
| 8 |
34.1 |
R+ |
0.0 |
S+ |
| 9 |
34.8 |
R+ |
0.0 |
S+ |
| 10 |
38.2 |
R+ |
0.0 |
S+ |
| 11 |
51.9 |
R+ |
0.0 |
S+ |
| 12 |
23.4 |
R+ |
0.0 |
S+ |
| 13 |
20.1 |
R+ |
0.0 |
S+ |
| 14 |
37.8 |
R+ |
0.0 |
S+ |
| 15 |
25.7 |
R+ |
0.0 |
S+ |
| 16 |
41.9 |
R+ |
0.0 |
S+ |
| 17 |
30.1 |
R+ |
0.0 |
S+ |
| 18 |
40.4 |
R+ |
0.0 |
S+ |
| 19 |
46.0 |
R+ |
0.0 |
S+ |
| 20 |
42.4 |
R+ |
0.0 |
S+ |
| 21 |
35.1 |
R+ |
0.0 |
S+ |
Totals:
- specstory total CPU: ~746%
- claude children total CPU: ~2.9%
- All specstory processes:
R+ (running, foreground) — actively consuming CPU
- All claude children:
S+ (sleeping, foreground) — idle, waiting for input
Expected Behavior
When the child process is idle (waiting at its input prompt), specstory should block on I/O (using select, poll, epoll, or kqueue) consuming ~0% CPU, exactly like the child itself does.
Impact
- Machine unusable with 20+ concurrent sessions (fans spinning, battery drain)
- ~750% wasted CPU from idle monitoring
- No workaround available — the specstory CLI has no
--poll-interval or --no-poll flags
- Disabling specstory entirely (
NO_SPECSTORY=1) is the only current workaround, but loses transcript history
Suggested Fix
Replace the busy-wait polling loop with a proper blocking I/O wait (kqueue on macOS / epoll on Linux) when monitoring the child process's stdout/stderr. The specstory process should only wake up when there is actual data to read from the child.
Summary
When using
specstory run claudewith multiple concurrent sessions, thespecstoryparent process consumes 20–50% CPU per instance even when the wrappedclaudechild process is completely idle (waiting for user input at the prompt).This appears to be a busy-wait / active polling loop in specstory's I/O monitoring of the child process, rather than using blocking I/O primitives (select/poll/epoll/kqueue).
Environment
specstory run claude -c /path/to/claude --settings /path/to/settings.json --silent --no-version-check --local-time-zoneReproduction
specstory run claudeps aux | grep specstoryObserved Behavior
With 21 concurrent sessions all idle (claude children waiting for input):
Totals:
R+(running, foreground) — actively consuming CPUS+(sleeping, foreground) — idle, waiting for inputExpected Behavior
When the child process is idle (waiting at its input prompt),
specstoryshould block on I/O (usingselect,poll,epoll, orkqueue) consuming ~0% CPU, exactly like the child itself does.Impact
--poll-intervalor--no-pollflagsNO_SPECSTORY=1) is the only current workaround, but loses transcript historySuggested Fix
Replace the busy-wait polling loop with a proper blocking I/O wait (kqueue on macOS / epoll on Linux) when monitoring the child process's stdout/stderr. The specstory process should only wake up when there is actual data to read from the child.