Short message
Hey, I just want to say this is an awesome project, thank you! I used Conductor before
and I moved to this Termic since last week. I'm willing to help a bit here and there if there's time.
The use case
I run agents in termic overnight and while I'm out of the house. That is the
whole reason I picked a tool that runs four of them in parallel worktrees.
A subscription usage limit ends that, and it does it in a way a plain "the
agent finished" indicator can't tell you about, because the agent hasn't
finished. It has stopped mid-task and it is not coming back.
Recorded on my machine on 2026-08-23, a real session limit:
Running scheduled task (Aug 23 6:44pm)
⌊ You've hit your session limit · resets 7:30pm (Europe/Amsterdam)
/usage-credits to finish what you're working on.
That is 46 minutes of nothing, on a task that had 46 minutes of work left in
it. I get back at 9pm and find an agent that stopped at 6:44 and could have
restarted itself at 7:30.
There is a second shape of this. On some plans claude does not print a hint
line, it blocks on an interactive menu:
❯ 1. Stop and wait for limit to reset
2. Upgrade your plan
Now the session is not merely idle, it is holding an unanswered question, so
nothing else in that tab happens until a human presses a key. Same outcome,
worse: the tab looks busy.
Neither shape self-recovers. "Stop and wait" stops; it does not come back.
anthropics/claude-code#18980
asks the CLI for a third "continue after the limit is reset" option, and until
that ships, something outside the CLI has to do it.
Why the existing tools don't fit termic
The prior art here (claude-auto-retry
and similar) runs the agent inside tmux and drives it with tmux send-keys.
From outside the process that is the only handle available, so it also needs a
loop that scrapes a tmux pane every 5 seconds and a check that the foreground
process is even the agent before it starts typing.
Running one of those alongside termic means a second thing owning the terminal.
Termic already owns the PTY, and already has every part this needs:
| Need |
Already in termic |
| See the output |
the pty://<id> chunk listener in TerminalPane |
| Match lines against per-agent patterns |
scanOutputLines (tier 3, #68) |
| Read the current screen |
visibleTailRows |
| Type into an agent like a human |
lib/agentSend (text, then the CR a beat later) |
| Know which tab is which agent |
the tab's cli + the agent registry |
So it's a detector, a clock, and some wiring. No new dependency, no polling
loop, no process sniffing, and it works on a tab that isn't on screen.
I've built it and it works
Working branch against main (41b3c5e), off by default behind a setting.
It has fired for real, once, on the limit quoted above. It parsed resets 7:30pm, parked the tab, and re-prompted at 19:31:21 local, which is the 19:30
reset plus the 60s default margin picked up on the next tick. Confirmed from
the session transcript rather than by watching the screen.
That one real run also corrected a design assumption, which is why I mention it
rather than just claiming it works: a session limit does not always render a
menu. Mine printed a plain /usage-credits hint with nothing to answer, so
the run took the no-menu path (parse the clock, park, re-prompt). The
menu-answering half is for the #18980 shape and is unit-tested but has not yet
met a real menu.
The rule the whole thing is built around
Never select a menu option that spends money.
A false negative parks a tab until the user looks at it, which is what already
happens today. A false positive buys credits on their card. So every ambiguous
case refuses and leaves the prompt for the human: no selection marker on the
menu, no identifiably-"wait" row, a wait row that also mentions credits, a list
that isn't numbered 1..n, fewer than two rows.
Navigation is arrow keys to the matched row, not the row's digit: the marker
position is observable fact, a digit shortcut is an assumption about the CLI's
input handling.
Where it lives
src/lib/autoRetry.ts (new, ~320 lines). Pure and now-injected:
patterns, parseResetAt, findWaitOption, planLimitPark. No terminal, no
clock, no rate-limited account needed to test it.
src/lib/autoRetry.test.ts (new). 33 cases, most of them the refusals.
src/components/task/TerminalPane.tsx. Detection folded into the
existing scanOutputLines (one decode shared with tier 3, not a second
TextDecoder on the data path); a 30s wall-clock effect for the re-prompt;
a parked banner reusing TerminalExitedBanner.
src/lib/types.ts. TerminalTab.limitWait (runtime-only, never
persisted) and capabilities.signals.limit.
src/store/prefs.ts + Settings → Tasks. autoResumeOnLimit (off),
autoResumeMessage, autoResumeMarginSec.
- Settings → Agents.
limit as a fifth signal class next to
busy/idle/attention/pending, so a CLI with no built-ins is a settings edit
rather than a patch. This is the "CLI registry improvements" bullet on your
will-accept list.
No IPC changes. No Rust changes. No new dependency.
Cost when it's off
None, and I treated that as a requirement rather than a nice-to-have given
performance.md. Line scanning was already gated behind match_output because
decoding and splitting every chunk isn't free. With auto-resume off the limit
half contributes no patterns, so the gate is exactly as false as it was before
the feature existed and the data path is byte for byte unchanged.
The pref is read through a store subscription rather than captured at spawn, so
turning it on reaches terminals that are already running. That one matters:
this is the setting someone flips because they are about to walk away, and
"restart your agents for it to take effect" would miss precisely that case.
Bounds
- 5 re-prompts per PTY lifetime. A limit that hasn't really lifted re-prints
the notice the moment termic speaks, which re-arms the detector; the ceiling
stops that becoming a loop. Hitting it gives up in a toast, never silently.
- Real user input clears the park and resets the budget, and says so. This one
was learned the hard way: cancelling was silent in the first cut and my first
test run was lost to an Enter pressed to check on a parked tab, which
cancelled the resume invisibly. The person most likely to press Enter "just
to check" is the person who walked away.
- A respawn or a PTY exit clears the park.
- Agent tabs only, never a shell, a run tab or a custom terminal entry. Those
print whatever they're told to, including the string "usage limit reached"
out of a log file, and none of them has a menu to answer.
What isn't done
- No e2e spec. The parsing and every refusal branch are unit-tested, but
nothing exercises detect → answer → park → resume against a live PTY. It
wants a fixture agent that prints a notice and a two-row menu; the assertion
that matters is that the fixture received the keys for the wait row and not
the paid one. I'd write it before sending a PR, following the e2e skill,
and add it to docs/e2e-coverage.md.
- Tested on Linux, not macOS. I'm on NixOS. Nothing in the change is
platform-specific (no Rust, no window code) but I can't claim a macOS run.
- One recorded wording. The rest of the patterns come from Anthropic's docs
and #18980. That's why they're overridable per agent.
- Claude only for built-in patterns. The menu reader is agent-agnostic (it
wants a numbered list, a marker, and a row that says wait and reset), but I
didn't want to ship patterns for CLIs I can't drive into a limit.
What I'd like from you
Per CONTRIBUTING.md this is the argument, not the patch. Three questions,
and I'd rather ask than send a diff you don't want:
- Do you want this in termic at all? It's a feature that types into an
agent unattended. That's a reasonable thing for a maintainer to say no to,
and I'd rather hear it now.
- Is the safety model the one you'd pick? Specifically: refuse-on-ambiguity
with no override, versus something that would let a user opt into "just
press 1". I'm strongly for the former and built only that, but it's your
call whether the paid options should be untouchable or merely default-off.
- Off by default, or off with a nudge? It ships off. There's an argument
for surfacing it once, the first time a tab actually hits a limit, since
that's the moment the user learns the problem exists.
Happy to split it: the limit signal class and the Settings field are useful
on their own, and the parked banner is meaningless without the engine, so the
natural seam is engine+banner as one PR and the registry field as another.
I've read CLA.md and will sign.
Built with agent assistance and then driven by hand, per the AI-contributions
note in CONTRIBUTING.md: the real-limit run above was a live account, and the
menu path was exercised against a mock agent whose cursor starts on the paid
option so that a pass proves the money guard held.
Short message
Hey, I just want to say this is an awesome project, thank you! I used Conductor before
and I moved to this Termic since last week. I'm willing to help a bit here and there if there's time.
The use case
I run agents in termic overnight and while I'm out of the house. That is the
whole reason I picked a tool that runs four of them in parallel worktrees.
A subscription usage limit ends that, and it does it in a way a plain "the
agent finished" indicator can't tell you about, because the agent hasn't
finished. It has stopped mid-task and it is not coming back.
Recorded on my machine on 2026-08-23, a real session limit:
That is 46 minutes of nothing, on a task that had 46 minutes of work left in
it. I get back at 9pm and find an agent that stopped at 6:44 and could have
restarted itself at 7:30.
There is a second shape of this. On some plans claude does not print a hint
line, it blocks on an interactive menu:
Now the session is not merely idle, it is holding an unanswered question, so
nothing else in that tab happens until a human presses a key. Same outcome,
worse: the tab looks busy.
Neither shape self-recovers. "Stop and wait" stops; it does not come back.
anthropics/claude-code#18980
asks the CLI for a third "continue after the limit is reset" option, and until
that ships, something outside the CLI has to do it.
Why the existing tools don't fit termic
The prior art here (claude-auto-retry
and similar) runs the agent inside tmux and drives it with
tmux send-keys.From outside the process that is the only handle available, so it also needs a
loop that scrapes a tmux pane every 5 seconds and a check that the foreground
process is even the agent before it starts typing.
Running one of those alongside termic means a second thing owning the terminal.
Termic already owns the PTY, and already has every part this needs:
pty://<id>chunk listener inTerminalPanescanOutputLines(tier 3, #68)visibleTailRowslib/agentSend(text, then the CR a beat later)cli+ the agent registrySo it's a detector, a clock, and some wiring. No new dependency, no polling
loop, no process sniffing, and it works on a tab that isn't on screen.
I've built it and it works
Working branch against
main(41b3c5e), off by default behind a setting.It has fired for real, once, on the limit quoted above. It parsed
resets 7:30pm, parked the tab, and re-prompted at 19:31:21 local, which is the 19:30reset plus the 60s default margin picked up on the next tick. Confirmed from
the session transcript rather than by watching the screen.
That one real run also corrected a design assumption, which is why I mention it
rather than just claiming it works: a session limit does not always render a
menu. Mine printed a plain
/usage-creditshint with nothing to answer, sothe run took the no-menu path (parse the clock, park, re-prompt). The
menu-answering half is for the #18980 shape and is unit-tested but has not yet
met a real menu.
The rule the whole thing is built around
Never select a menu option that spends money.
A false negative parks a tab until the user looks at it, which is what already
happens today. A false positive buys credits on their card. So every ambiguous
case refuses and leaves the prompt for the human: no selection marker on the
menu, no identifiably-"wait" row, a wait row that also mentions credits, a list
that isn't numbered 1..n, fewer than two rows.
Navigation is arrow keys to the matched row, not the row's digit: the marker
position is observable fact, a digit shortcut is an assumption about the CLI's
input handling.
Where it lives
src/lib/autoRetry.ts(new, ~320 lines). Pure andnow-injected:patterns,
parseResetAt,findWaitOption,planLimitPark. No terminal, noclock, no rate-limited account needed to test it.
src/lib/autoRetry.test.ts(new). 33 cases, most of them the refusals.src/components/task/TerminalPane.tsx. Detection folded into theexisting
scanOutputLines(one decode shared with tier 3, not a secondTextDecoderon the data path); a 30s wall-clock effect for the re-prompt;a parked banner reusing
TerminalExitedBanner.src/lib/types.ts.TerminalTab.limitWait(runtime-only, neverpersisted) and
capabilities.signals.limit.src/store/prefs.ts+ Settings → Tasks.autoResumeOnLimit(off),autoResumeMessage,autoResumeMarginSec.limitas a fifth signal class next tobusy/idle/attention/pending, so a CLI with no built-ins is a settings edit
rather than a patch. This is the "CLI registry improvements" bullet on your
will-accept list.
No IPC changes. No Rust changes. No new dependency.
Cost when it's off
None, and I treated that as a requirement rather than a nice-to-have given
performance.md. Line scanning was already gated behindmatch_outputbecausedecoding and splitting every chunk isn't free. With auto-resume off the limit
half contributes no patterns, so the gate is exactly as false as it was before
the feature existed and the data path is byte for byte unchanged.
The pref is read through a store subscription rather than captured at spawn, so
turning it on reaches terminals that are already running. That one matters:
this is the setting someone flips because they are about to walk away, and
"restart your agents for it to take effect" would miss precisely that case.
Bounds
the notice the moment termic speaks, which re-arms the detector; the ceiling
stops that becoming a loop. Hitting it gives up in a toast, never silently.
was learned the hard way: cancelling was silent in the first cut and my first
test run was lost to an Enter pressed to check on a parked tab, which
cancelled the resume invisibly. The person most likely to press Enter "just
to check" is the person who walked away.
print whatever they're told to, including the string "usage limit reached"
out of a log file, and none of them has a menu to answer.
What isn't done
nothing exercises detect → answer → park → resume against a live PTY. It
wants a fixture agent that prints a notice and a two-row menu; the assertion
that matters is that the fixture received the keys for the wait row and not
the paid one. I'd write it before sending a PR, following the
e2eskill,and add it to
docs/e2e-coverage.md.platform-specific (no Rust, no window code) but I can't claim a macOS run.
and #18980. That's why they're overridable per agent.
wants a numbered list, a marker, and a row that says wait and reset), but I
didn't want to ship patterns for CLIs I can't drive into a limit.
What I'd like from you
Per
CONTRIBUTING.mdthis is the argument, not the patch. Three questions,and I'd rather ask than send a diff you don't want:
agent unattended. That's a reasonable thing for a maintainer to say no to,
and I'd rather hear it now.
with no override, versus something that would let a user opt into "just
press 1". I'm strongly for the former and built only that, but it's your
call whether the paid options should be untouchable or merely default-off.
for surfacing it once, the first time a tab actually hits a limit, since
that's the moment the user learns the problem exists.
Happy to split it: the
limitsignal class and the Settings field are usefulon their own, and the parked banner is meaningless without the engine, so the
natural seam is engine+banner as one PR and the registry field as another.
I've read
CLA.mdand will sign.Built with agent assistance and then driven by hand, per the AI-contributions
note in
CONTRIBUTING.md: the real-limit run above was a live account, and themenu path was exercised against a mock agent whose cursor starts on the paid
option so that a pass proves the money guard held.