Transcript rotation never runs for tasks that keep their container alive
Version: 2.1.53
Summary
maybeRotateContinuation() is only ever called from runPollLoop(), i.e. once per container start. Any scheduled task whose recurrence is shorter than the host's 30-minute idle ceiling keeps its container alive indefinitely, so the check never runs and the session transcript grows without bound — far past both the 12 MB size cap and the 14 day age cap that are meant to bound it.
Why this matters beyond disk usage
The transcript is what gets resumed, so an oversized one keeps weeks-old attachment entries in the model's context. In our case an agent kept being shown file attachments replaying reads it had performed 13 days earlier, plus date_change attachments from every midnight in between.
The agent concluded it was under a prompt-injection attack: it saw "Read tool" output for files it had not requested, containing data timestamped days in the past, and a date_change reminder that arrives without a <system-reminder> wrapper. It correctly refused to act on them — and escalated to a human seven times over twelve days. The false positives cost real engineering time and eroded trust in the agent's own reporting.
So the practical impact of the missed rotation is not "the file is big", it is the agent reasons over stale context and misclassifies platform mechanics as an attack.
Reproduction
- Give an agent group a recurring task at
*/15 * * * *(anything below the idle ceiling works). - Let it run for a week.
docker ps— the container's uptime is measured in days; it never restarts.ls -la <data>/v2-sessions/<ag>/.claude-shared/projects/-workspace-agent/— the active.jsonlis well past 12 MB and has no.rotated-*sibling from the current run.
What we measured
One agent group, */15 * * * *, started 21 Aug:
| container uptime | 12 days, uninterrupted |
| active transcript | 33 MB (cap: 12 MB) |
| age of oldest entry | 13 days (cap: 14 days) |
| sibling transcripts that did rotate | 9, all at ~12.6 MB |
file attachments carried in that session |
250 |
date_change attachments carried |
82 |
A second group in the same install reached 84 MB — seven times the cap — over two weeks.
Stopping the container once was enough: the next spawn ran the check and rotated the 33 MB transcript immediately. That confirms the logic itself is fine; only its call site is wrong.
Root cause
container/agent-runner/src/poll-loop.ts:
// Before resuming, drop a session whose on-disk transcript has grown too
// large/old to cold-resume within the host's idle ceiling. […]
if (continuation) {
const rotateReason = config.provider.maybeRotateContinuation?.(continuation, config.cwd);
…
}This sits above the poll loop, so it is evaluated exactly once. The comment describes the intent correctly ("a long-lived hub keeps trying to reload an ever-growing .jsonl") — but the guard only fires for sessions that are cold-resumed, which is precisely the case that a long-lived container never reaches.
Suggested fix
Rotation does not actually require a container restart. Re-checking between turns, when nothing is pending, is enough: archive and rename the transcript, clear continuation, and the next turn starts a fresh SDK session inside the same container. That is a few lines at the end of the loop body rather than a new lifecycle.
Doing it between turns (never mid-turn) keeps the current guarantee that an agent is not interrupted while working.
An alternative or additional safeguard would be to bound container lifetime directly, but that discards a healthy session on a timer, whereas the size/age caps already express the right policy — they just need to be evaluated more than once.
Source: nanocoai/nanoclaw