#3732·nanoclaw

Transcript rotation never runs for tasks that keep their container alive

Author: TO-maschenbornCreated Sep 7, 2026Updated Sep 7, 2026

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

  1. Give an agent group a recurring task at */15 * * * * (anything below the idle ceiling works).
  2. Let it run for a week.
  3. docker ps — the container's uptime is measured in days; it never restarts.
  4. ls -la <data>/v2-sessions/<ag>/.claude-shared/projects/-workspace-agent/ — the active .jsonl is 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:

typescript
// 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.