What's broken
Idle-agent GC (2 min, hardcoded) kills agent runtimes that still have in-flight background work
Summary
Since 0.2.0, the daemon garbage-collects idle agent runtimes after a hardcoded
2-minute TTL. The collectability test only considers paseo's notion of activity
(foreground turn, paseo runs, pending permissions) and has no visibility into work the
underlying agent CLI is running on its own — Claude Code background Bash commands,
Monitor watches, background subagents.
The result: a live agent process is killed while it still has pending work, the work is
lost, and nothing surfaces the loss. From the agent's perspective the absence of
further events is indistinguishable from "still running", so it waits indefinitely on
something that no longer exists.
The TTL and sweep interval are module constants with no config key and no environment
override, so this cannot be tuned or disabled.
To be clear about what is not broken: session continuity is fine. The runtime is
respawned with --resume=<same session id>, the transcript file is appended (not
duplicated), and no conversation state is lost. The problems are (a) silently killed
background work and (b) the prompt-cache cost of each respawn.
Environment
|
|
@getpaseo/cli |
0.2.2 |
@getpaseo/server |
0.2.2 |
| Previously working on |
0.1.110 (no 0.2.0 betas) |
| Agent provider |
Claude Code 2.1.220, claude-opus-5[1m] |
@anthropic-ai/claude-agent-sdk |
^0.3.220 |
| Node |
v24.16.0 |
| OS |
Ubuntu 26.04 LTS, kernel 7.0.0-27-generic |
Root cause
dist/server/server/bootstrap.js:149-150
const IDLE_AGENT_RUNTIME_TTL_MS = 2 * 60 * 1000; // 2 minutes
const IDLE_AGENT_RUNTIME_SWEEP_INTERVAL_MS = 15 * 1000; // sweep every 15s
bootstrap.js:850-879 schedules an unconditional sweep (setInterval, no feature flag)
which calls agentManager.collectIdleAgents({ cutoff, protectedAgentIds }) with
cutoff = Date.now() - IDLE_AGENT_RUNTIME_TTL_MS. The constant is consumed inline at the
use site, so there is no indirection a config value could hook into.
dist/server/server/agent/agent-manager.js:845 — the collectability gate:
isIdleAgentCollectable(agent, options) {
return (agent.lifecycle === "idle" &&
agent.updatedAt.getTime() <= options.cutoff.getTime() &&
!agent.internal &&
!options.protectedAgentIds.has(agent.id) &&
agent.activeForegroundTurnId === null &&
!this.runs.hasRun(agent.id) &&
!agent.pendingReplacement &&
agent.pendingPermissions.size === 0 &&
agent.inFlightPermissionResponses.size === 0);
}
Every clause is about paseo-side state. An agent that has finished its foreground turn but
left a background process running inside the agent CLI satisfies all of them and is
collected. collectIdleAgents then calls closeAgent → closeAgentRuntime →
agent.session.close(), terminating the child process.
The only exemptions are agent.internal agents and active schedule targets
(scheduleService.listActiveAgentTargetIds()).
Neither constant nor any equivalent idle-reaping logic exists in 0.1.110 (verified by
searching that version for collectIdle|idleAgent|reapIdle|closeIdle|evictIdle|idleTtl|IDLE_.*TTL|staleAgent
— the sole hit was an unrelated comment about HTTP closeIdleConnections()).
0.2.1 is not implicated: relative to 0.2.0 it changed only
agent/providers/claude/model-manifest.js and the web UI bundle. The @getpaseo/cli
package is byte-identical across 0.2.0/0.2.1/0.2.2.
Evidence
Daemon log (~/.paseo/daemon.log), one session collected 4 times in ~25 minutes:
07-26 06:25:10 provider=claude session=cc8a43f8-… "Collected idle agent runtime"
07-26 06:39:23 provider=claude session=cc8a43f8-…
07-26 06:46:53 provider=claude session=cc8a43f8-…
07-26 06:50:53 provider=claude session=cc8a43f8-…
23 collections total in a single log file.
The 06:25:10 entry lands exactly 2 minutes after that agent's last activity (06:22:57), at
which point it was watching a CI run via Monitor. The watch never reported again and the
agent sat waiting on a dead process until the user intervened.
Kill-and-respawn confirmed at the OS level — two different PIDs serving one session id:
PID 736048 started 06:39:58 claude … --resume=cc8a43f8-… → gone
PID 748012 started 06:59:10 claude … --resume=cc8a43f8-… → current
Secondary impact: prompt-cache cost
Each respawn re-establishes the prompt prefix. Measured from the session transcript's
usage fields, one restart moved ~73k tokens from cache-read to cache-write:
06:22:57 cache_creation= 1,119 cache_read= 91,105
06:39:58 ← runtime respawned
06:40:12 cache_creation= 72,858 cache_read= 19,897
Cache writes bill at 2× base input on a 1-hour TTL versus 0.1× for reads, so that single
restart cost roughly 138k additional input-token equivalents (~$0.69 at Claude Opus 5
input pricing) against a session total of about $3.40 — near 20% overhead from one
collection. With a 2-minute TTL and normal think-time between messages, this repeats
throughout a working session.
cache_read dropping to 19,897 rather than 0 indicates the prefix diverges partway in
rather than being wholly invalidated. Cache TTL expiry is ruled out independently: the gap
was 17 minutes against a 1-hour cache TTL.
Suggested fixes
Roughly in order of value:
- Make the collectability test aware of agent-CLI background work. The provider layer
knows whether background tasks/monitors are outstanding; surface that as a
hasBackgroundWork() signal and add it to isIdleAgentCollectable. This is the actual
fix — the rest are mitigations.
- Make the TTL configurable, with an opt-out. A
daemon.idleAgentRuntimeTtlMs config
key (0/null to disable) would let users on long-running agentic workloads turn this
off. 2 minutes is aggressive for agents that routinely wait on CI, builds, or deploys.
- Tell the agent on resume. If a runtime is collected with background work
outstanding, inject a notice on the next turn — something explicit enough that the agent
re-checks external state instead of waiting on a dead watch. The current failure is
silent, which is what makes it costly.
- Consider the cache cost in the TTL default. Collection trades a process for a
prompt-cache rebuild on the next message. At a 2-minute TTL that trade is frequently
unfavourable; a longer default (or an idle-cost heuristic) would help.
Notes
- I could not determine the issue tracker from package metadata —
@getpaseo/cli has no
repository, homepage, or bugs field. Happy to move this wherever appropriate.
- Line numbers are from the published
@getpaseo/server@0.2.2 build artifacts.
Steps to reproduce
Reproduction
- Start a Claude Code agent in a paseo workspace.
- Have it launch background work that outlives the turn — e.g. a
Monitor, or a
Bash call with run_in_background: true that polls an external service.
- Let the agent finish its foreground turn and go idle.
- Wait ~2 minutes.
Observed: the daemon logs Collected idle agent runtime, the OS process is killed, and
the background work dies. The agent is not notified in any actionable way; on the next
message it resumes with no knowledge that its background task was terminated. In the UI the
context indicator disappears.
Expected: an agent with in-flight background work is not collected — or, if collection
is unavoidable, the agent is told unambiguously on resume that its background tasks were
terminated.
Where did this happen
Daemon
Paseo version
0.2.2
OS version
ubuntu 2604
Agent provider
Claude Code
Provider configuration
No response
Logs
Screenshots or video
No response
What's broken
Idle-agent GC (2 min, hardcoded) kills agent runtimes that still have in-flight background work
Summary
Since
0.2.0, the daemon garbage-collects idle agent runtimes after a hardcoded2-minute TTL. The collectability test only considers paseo's notion of activity
(foreground turn, paseo runs, pending permissions) and has no visibility into work the
underlying agent CLI is running on its own — Claude Code background Bash commands,
Monitorwatches, background subagents.The result: a live agent process is killed while it still has pending work, the work is
lost, and nothing surfaces the loss. From the agent's perspective the absence of
further events is indistinguishable from "still running", so it waits indefinitely on
something that no longer exists.
The TTL and sweep interval are module constants with no config key and no environment
override, so this cannot be tuned or disabled.
To be clear about what is not broken: session continuity is fine. The runtime is
respawned with
--resume=<same session id>, the transcript file is appended (notduplicated), and no conversation state is lost. The problems are (a) silently killed
background work and (b) the prompt-cache cost of each respawn.
Environment
@getpaseo/cli@getpaseo/serverclaude-opus-5[1m]@anthropic-ai/claude-agent-sdkRoot cause
dist/server/server/bootstrap.js:149-150bootstrap.js:850-879schedules an unconditional sweep (setInterval, no feature flag)which calls
agentManager.collectIdleAgents({ cutoff, protectedAgentIds })withcutoff = Date.now() - IDLE_AGENT_RUNTIME_TTL_MS. The constant is consumed inline at theuse site, so there is no indirection a config value could hook into.
dist/server/server/agent/agent-manager.js:845— the collectability gate:Every clause is about paseo-side state. An agent that has finished its foreground turn but
left a background process running inside the agent CLI satisfies all of them and is
collected.
collectIdleAgentsthen callscloseAgent→closeAgentRuntime→agent.session.close(), terminating the child process.The only exemptions are
agent.internalagents and active schedule targets(
scheduleService.listActiveAgentTargetIds()).Neither constant nor any equivalent idle-reaping logic exists in
0.1.110(verified bysearching that version for
collectIdle|idleAgent|reapIdle|closeIdle|evictIdle|idleTtl|IDLE_.*TTL|staleAgent— the sole hit was an unrelated comment about HTTP
closeIdleConnections()).0.2.1is not implicated: relative to0.2.0it changed onlyagent/providers/claude/model-manifest.jsand the web UI bundle. The@getpaseo/clipackage is byte-identical across 0.2.0/0.2.1/0.2.2.
Evidence
Daemon log (
~/.paseo/daemon.log), one session collected 4 times in ~25 minutes:23 collections total in a single log file.
The 06:25:10 entry lands exactly 2 minutes after that agent's last activity (06:22:57), at
which point it was watching a CI run via
Monitor. The watch never reported again and theagent sat waiting on a dead process until the user intervened.
Kill-and-respawn confirmed at the OS level — two different PIDs serving one session id:
Secondary impact: prompt-cache cost
Each respawn re-establishes the prompt prefix. Measured from the session transcript's
usagefields, one restart moved ~73k tokens from cache-read to cache-write:Cache writes bill at 2× base input on a 1-hour TTL versus 0.1× for reads, so that single
restart cost roughly 138k additional input-token equivalents (~$0.69 at Claude Opus 5
input pricing) against a session total of about $3.40 — near 20% overhead from one
collection. With a 2-minute TTL and normal think-time between messages, this repeats
throughout a working session.
cache_readdropping to 19,897 rather than 0 indicates the prefix diverges partway inrather than being wholly invalidated. Cache TTL expiry is ruled out independently: the gap
was 17 minutes against a 1-hour cache TTL.
Suggested fixes
Roughly in order of value:
knows whether background tasks/monitors are outstanding; surface that as a
hasBackgroundWork()signal and add it toisIdleAgentCollectable. This is the actualfix — the rest are mitigations.
daemon.idleAgentRuntimeTtlMsconfigkey (
0/nullto disable) would let users on long-running agentic workloads turn thisoff. 2 minutes is aggressive for agents that routinely wait on CI, builds, or deploys.
outstanding, inject a notice on the next turn — something explicit enough that the agent
re-checks external state instead of waiting on a dead watch. The current failure is
silent, which is what makes it costly.
prompt-cache rebuild on the next message. At a 2-minute TTL that trade is frequently
unfavourable; a longer default (or an idle-cost heuristic) would help.
Notes
@getpaseo/clihas norepository,homepage, orbugsfield. Happy to move this wherever appropriate.@getpaseo/server@0.2.2build artifacts.Steps to reproduce
Reproduction
Monitor, or aBashcall withrun_in_background: truethat polls an external service.Observed: the daemon logs
Collected idle agent runtime, the OS process is killed, andthe background work dies. The agent is not notified in any actionable way; on the next
message it resumes with no knowledge that its background task was terminated. In the UI the
context indicator disappears.
Expected: an agent with in-flight background work is not collected — or, if collection
is unavoidable, the agent is told unambiguously on resume that its background tasks were
terminated.
Where did this happen
Daemon
Paseo version
0.2.2
OS version
ubuntu 2604
Agent provider
Claude Code
Provider configuration
No response
Logs
Screenshots or video
No response