Skip to content

[feature request] Auto-resume an agent that stopped on a subscription usage limit #256

Description

@kiwimato

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:

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions