Skip to content

walkie agent --cli claude posts raw stream-JSON instead of the reply text #13

Description

@rossmeyerza

Summary

walkie agent <channel> --cli claude posts raw Claude-CLI JSON to the channel instead of the assistant's reply text. Other agents (and humans) receive a blob like:

claude-bot: [{"type":"system","subtype":"init","cwd":"...","session_id":"...","tools":[...]}, ...]

The --cli codex path is unaffected — only the Claude adapter is broken.

Environment

  • walkie: 1.5.0 (installed from github:vikasprogrammer/walkie)
  • Node: v22.x
  • claude CLI: current release (Claude Code)

Repro

  1. Terminal A: walkie agent demo:secret --cli claude --name claude-bot
  2. Terminal B: walkie agent demo:secret --cli codex --name codex-bot
  3. Seed: walkie send demo:secret "introduce yourselves"

codex-bot replies in natural language. claude-bot emits the raw JSON event stream instead of text, and re-fires on subsequent messages.

Root cause

In runClaude() (bin/walkie.js), walkie calls:

const args = ['-p', prompt, '--output-format', 'json']

and then expects an object with a top-level .result, parsing line by line:

const out = { text: stdout, sessionId: null }   // defaults to RAW stdout
const lines = stdout.split('\n').filter(l => l.trim())
for (let i = lines.length - 1; i >= 0; i--) {
  try {
    const obj = JSON.parse(lines[i])
    if (obj.session_id) out.sessionId = obj.session_id
    if (obj.result !== undefined) { out.text = obj.result; break }
  } catch {}
}

But the current Claude CLI's -p --output-format json returns a single-line JSON array of events — not an object:

$ claude -p "say PONG" --output-format json | node -e '...'
typeof: array len 4
has .result: false
elem types: system,rate_limit_event,assistant,result

So:

  • there is only one line (the whole array on one line),
  • JSON.parse(line) yields an Array, on which .result / .session_id are undefined,
  • the loop never overrides out.text, so it falls back to the raw stdout dump.

The reply text actually lives in the array element with type === "result" (its .result field), and session_id is on the system/init and result events.

Suggested fix

Parse the whole stdout, handle the array-of-events shape, and keep fallbacks for the legacy single-object and NDJSON/stream-json shapes:

const stdout = (result.stdout || '').trim()
const out = { text: stdout, sessionId: null }

const applyEvent = (obj) => {
  if (!obj || typeof obj !== 'object') return
  if (obj.session_id) out.sessionId = obj.session_id
  if (obj.type === 'result' && typeof obj.result === 'string') out.text = obj.result
  else if (obj.result !== undefined) out.text = obj.result
}

let whole
try { whole = JSON.parse(stdout) } catch {}
if (Array.isArray(whole)) whole.forEach(applyEvent)
else if (whole && typeof whole === 'object') applyEvent(whole)
else for (const line of stdout.split('\n')) {
  try { applyEvent(JSON.parse(line)) } catch {}
}
return out

I have a patch + PR ready to submit for this.

Secondary observations (not addressed by the above patch)

Correction (edited): an earlier version of this section claimed there was no loop guard and that agents react to system join events. That was wrong — see the corrected note below. (Details in the comment thread.)

  • Loop guard is present but soft: the agent loop already ignores its own + system messages and does @mention filtering, and caps consecutive exchanges with the same sender (MAX_CONSECUTIVE = 10) before pausing. So two agents do not loop indefinitely. The remaining concern is just that 10 back-and-forth turns is still ~10 model calls per side before the pause kicks in, and the cap is per-consecutive-same-sender — so it's worth noting as a cost/tuning consideration, not a missing feature.
  • Duplicate delivery: during testing, a single send was occasionally received twice by a peer. May warrant message-id dedup.

Happy to open follow-up issues for these if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions