paperclipai/paperclip · warning · Error

Run log tail tick returned unparseable output.

Error message

Run log tail tick returned unparseable output.

What it means

Thrown by tick() after a successful (exit 0) tail command when the captured stdout does not contain the expected marker frame: a TAIL_MARKER_STDOUT line, a later TAIL_MARKER_STDERR line, and a later TAIL_MARKER_END line, in that order. It means the remote shell produced structurally unexpected output — e.g. a login banner, a different shell that does not support printf, or a partial capture cut between markers.

Source

Thrown at packages/adapter-utils/src/sandbox-run-log-stream.ts:183

      }
    }

    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();
          consecutiveFailures = 0;
        } catch {
          consecutiveFailures += 1;
          if (consecutiveFailures >= maxConsecutiveFailures) {
            degraded = true;
            break;

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Log result.stdout verbatim on parse failure to see what broke the marker frame.
  2. Ensure the remote login shell for the exec channel is POSIX (sh/bash) and non-interactive (no profile output to stdout).
  3. Increase tickTimeoutMs so the full marker frame is captured atomically.
  4. Confirm base64/tail/head exist on the remote host and the marker constants are not accidentally redefined.
Defensive patterns

Strategy: validation

Validate before calling

function validateTickFrame(stdout: string): void {
  const lines = stdout.split(/\r?\n/);
  const s = lines.indexOf(TAIL_MARKER_STDOUT);
  const e = lines.indexOf(TAIL_MARKER_STDERR);
  const end = lines.indexOf(TAIL_MARKER_END);
  if (s < 0 || e < s || end < e) {
    throw new Error(`tail frame malformed; first 200 bytes: ${stdout.slice(0, 200)}`);
  }
}

Try / catch

try { await tick(); } catch (err) { consecutiveFailures += 1; /* loop tolerates up to maxConsecutiveFailures */ }

Prevention

When it happens

Trigger: parseTickOutput returns null because indexOf markers are missing or mis-ordered: stdoutIndex<0, stderrIndex<stdoutIndex, or endIndex<stderrIndex. Happens when remote shell is not POSIX sh (printf flags differ), a banner precedes the first marker, or stdout was truncated mid-frame.

Common situations: Remote default shell is fish/csh which mishandle the printf/head/base64 pipeline; .bashrc prints to stdout; tickTimeoutMs cut the output before TAIL_MARKER_END; a wrapper script injects text; locale/encoding corrupts the marker bytes.

Related errors


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