paperclipai/paperclip · warning · Error
Run log tail tick failed (exit ${result.exitCode ?? "null"}$
Error message
Run log tail tick failed (exit ${result.exitCode ?? "null"}${result.timedOut ? ", timed out" : "}). What it means
Thrown by the run-log tail poller's tick() when the remote tail command exited non-zero or timed out. Each tick runs a sh script that tails the agent's stdout/stderr log files in base64 chunks; a non-zero exit (or timeout) means the remote shell, the bridge exec channel, or the log files are unavailable. The surrounding loop tolerates a few consecutive failures (maxConsecutiveFailures) before giving up.
Source
Thrown at packages/adapter-utils/src/sandbox-run-log-stream.ts:177
async function emitBytes(state: TailStreamState, bytes: Buffer): Promise<void> {
if (bytes.length === 0) return;
state.offset += bytes.length;
const text = state.decoder.write(bytes);
if (text.length > 0 && sink) {
await sink(state.stream, text);
}
}
async function tick(): Promise<void> {
const result = await options.runner.execute({
command: shellCommand,
args: shellCommandArgs(buildTickScript()),
cwd: options.remoteCwd,
env: { [SANDBOX_EXEC_CHANNEL_ENV]: SANDBOX_EXEC_CHANNEL_BRIDGE },
timeoutMs: tickTimeoutMs,
});
if (result.timedOut || (result.exitCode ?? 1) !== 0) {
throw new Error(
`Run log tail tick failed (exit ${result.exitCode ?? "null"}${result.timedOut ? ", timed out" : ""}).`,
);
}
const sections = parseTickOutput(result.stdout);
if (!sections) {
throw new Error("Run log tail tick returned unparseable output.");
}
await emitBytes(streams[0], sections.stdout);
await emitBytes(streams[1], sections.stderr);
}
async function loop(): Promise<void> {
let consecutiveFailures = 0;
while (!stopped) {
await sleep(pollIntervalMs);
if (stopped) break;
try {
await tick();View on GitHub (pinned to 67001ec6eb)
Solutions
- Raise tickTimeoutMs and confirm it exceeds worst-case tail+base64 latency for the largest expected chunk (maxChunkBytes).
- Check the bridge process (pid from bridge directories) is alive and the exec channel returns quickly.
- Verify the log files referenced by stream states exist and are readable by the remote user.
- Tune maxConsecutiveFailures/pollIntervalMs so transient exec hiccups do not abort the stream.
Defensive patterns
Strategy: retry
Validate before calling
if (tickTimeoutMs < maxChunkBytes /* approx base64 cost */) {
throw new Error('tickTimeoutMs too small for maxChunkBytes; raise it');
} Try / catch
// The loop already tolerates consecutiveFailures; surface only if it gives up.
// In your own code wrapping streamRunLog:
try {
await streamRunLog(opts);
} catch (err) {
logger.warn('run log stream terminated', { message: (err as Error).message });
} Prevention
- Size tickTimeoutMs above worst-case tail+base64 latency.
- Keep the bridge exec channel responsive; monitor its pid.
- Ensure log files are readable by the remote user throughout the run.
When it happens
Trigger: streamRunLog called while the remote log files are missing/unreadable, the bridge exec channel is saturated or down, tickTimeoutMs is too short for the tail+base64 command, or the sandbox host is unreachable. Each tick that fails increments consecutiveFailures.
Common situations: tickTimeoutMs set lower than the time to base64 a large log chunk; remote filesystem where the log file lives was unmounted; bridge process crashed mid-run; SSH/exec channel latency spikes; permission change on the log dir.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- stop sandbox callback bridge timed out${detail ? `: ${detail
- Timed out checking command "${command}" on sandbox target.
- Timed out while installing the adapter runtime command via:
- Sandbox callback bridge wrote invalid readiness JSON: ${erro
- Run log tail tick returned unparseable output.
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/dda16533670f07d6.
Report an issue: GitHub.