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

  1. Raise tickTimeoutMs and confirm it exceeds worst-case tail+base64 latency for the largest expected chunk (maxChunkBytes).
  2. Check the bridge process (pid from bridge directories) is alive and the exec channel returns quickly.
  3. Verify the log files referenced by stream states exist and are readable by the remote user.
  4. 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

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

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/dda16533670f07d6. Report an issue: GitHub.