powered by
etapx

0%

(
July 28, 2026
)

How Influxx Normalizes Codex, Grok, and Kimi Into One Live Status Feed

Inside the decoder layer that turns Codex, Grok, and Kimi transcript formats into one live agent status system alongside Claude in Influxx.
How Influxx Normalizes Codex, Grok, and Kimi Into One Live Status Feed
How Influxx Normalizes Codex, Grok, and Kimi Into One Live Status Feed
Inside the decoder layer that turns Codex, Grok, and Kimi transcript formats into one live agent status system alongside Claude in Influxx.

Influxx supports more than 30 AI coding agent CLIs, and almost none of them log a session to disk the same way. Codex writes two different JSONL record kinds per turn, Grok writes a chat_history.jsonl where some tool calls never get a follow-up result row at all, and Kimi writes an event-sourced wire.jsonl where a single reply can arrive as several streamed fragments before it's "done." Influxx 1.2 hardens the decoder layer that reads all of these formats — plus Claude's own transcript shape, which the other three quietly normalize toward — into one internal message model, so the live status a developer watches in Native Chat looks and behaves the same no matter which CLI is actually doing the work underneath.

One Chat UI, Four Very Different Transcript Formats

Each supported provider gets its own decoder file: transcript-line-decoders-codex.ts, transcript-line-decoders-grok.ts, transcript-line-decoders-kimi.ts, and the Claude-compatible path that Agent Teams and GLM share. Every decoder takes one raw JSONL line and a fallback id, and either returns a normalized NativeChatMessage or returns null when the line isn't something the UI needs to show. None of that plumbing is visible to the rest of the app — Native Chat's rendering layer just consumes a stream of messages with a role, a timestamp, and a list of content blocks.

The tell for how this normalization actually works is a single shared function, claudeContentBlocks, imported by all three non-Claude decoders. Claude's own content-block shape — text, tool-call, tool-result, image-ref — is the reference schema every other provider's raw JSON gets mapped into. Codex, Grok, and Kimi don't get their own parallel block vocabulary; they get translated into Claude's.

Codex: Two Record Kinds and a Turn-Abort Marker

Codex rollouts carry two record types: response_item and event_msg. A response_item can be a message (mapped straight to content blocks), a reasoning item (its text pulled from either a direct text field or a summary array joined into one block), or one of four tool-call shapes — function_call, local_shell_call, custom_tool_call, and tool_search_call — all folded into the same tool-call block. custom_tool_call is how Codex represents every file edit via apply_patch; tool_search_call is the MCP/tool-catalog probe Codex runs to see what tools are available. Both were previously dropped and never rendered at all.

On the event_msg side, Codex emits a distinct turn-aborted marker that Influxx maps to a system message carrying its own interrupted-status text, plus plain user_message / agent_message events for straightforward text turns. Codex's tool-call id resolution prefers the wire field call_id and falls back to id — that pairing detail is what lets a tool call and its later result render as one row instead of two disconnected bubbles.

Grok's Backend Tool Calls: a Row Written After the Fact

Grok's chat_history.jsonl distinguishes backend_tool_call from tool_call, and that distinction is the most provider-specific thing in the entire decoder layer. A tool_call behaves the way you'd expect: a call row, then eventually a separate tool_result row. A backend_tool_call is different — it runs on Grok's own infrastructure, and the row describing it is written to the transcript only after the call has already finished. No matching tool_result row ever follows. Without special handling, that call would sit on a permanent "still working" spinner in the UI, because nothing in the transcript ever tells Influxx it settled.

"You can't treat every provider's tool-call framing as the same shape with different field names. Grok's backend tool calls genuinely don't have a result event — the call event is written after the work is already done, so if you wait for a result row the way Codex or Kimi's decoder does, you wait forever. We had to build a settled-status check specifically for that one record type."

— Dara Okonkwo, Staff Engineer, Native Chat Platform at ETAPX

The decoder handles this by checking whether the backend call's status field is one of a small set of in-flight values — in_progress, running, pending, queued. If the status is anything else, including absent, the call is treated as settled and a synthetic tool-result block is appended in the same message, built from whatever source URLs the call already carried. That's a deliberate asymmetry: an absent status means settled for a backend call, but it would not mean settled for a regular tool_call waiting on its own result row.

Kimi's Event-Sourced Wire Log

Kimi's wire.jsonl isn't one row per message — it's event-sourced. A user turn arrives as a complete context.append_message record, filtered so only rows with origin.kind === 'user' count as real turns (Kimi also injects synthetic user rows for system reminders that must not appear as if the developer typed them). Assistant output arrives wrapped inside context.append_loop_event, and a single reply can show up as multiple streamed content.part chunks that render as separate bubbles until a later fold.

Tool activity inside that same event envelope uses tool.call / tool_call and tool.result / tool_result / tool.output / tool_output — four accepted spellings for essentially two concepts, reflecting how Kimi's own event schema has shifted across versions. One detail that only shows up once you've actually debugged a real transcript: the failure flag on a Kimi tool result lives under event.result.isError, not on the event's root object. A decoder that only checked the root would silently treat every Kimi tool failure as a success.

Deciding Whether a Tool Call Actually Failed

None of the three providers give Influxx a clean, universally reliable "this failed" flag, so each decoder carries its own failure heuristic tuned to how that CLI actually writes error text in practice:

  • Codex parses the output body for an exit-code phrase ("Process exited with code 1", "Exit code: 1", "exit: 1") or a line that starts with "Error: " — because roughly 95% of real Codex tool outputs are plain strings, so a record-level success or is_error flag alone almost never fires for shell results.
  • Grok checks anchored patterns like a line starting with "Tool \`x\` failed:", "Error: ", a non-zero "exit: N", or "Status: failed" appearing on its own field line — anchored specifically so ordinary success text that happens to mention the word "failed" mid-sentence doesn't get flagged.
  • Kimi checks explicit error flags on the event root and, separately, under nested result and output objects, plus a status field matched against a fail/error pattern, plus the mere presence of a non-null nested error value.

"I run three different CLIs across different projects depending on the client, and I used to have to remember which one's failures I could trust in the log versus which ones I had to actually scroll and read myself. Now a failed tool call looks the same red state no matter which agent produced it, and that consistency alone saved me from missing a Grok fetch failure I would have skimmed right past."

— Renata Faulks, freelance backend contractor at Faulks Systems Consulting, Influxx user

Pairing a Tool Call With Its Result When Nothing Is in Order

A tool-call row and its result don't always arrive next to each other, and for some providers they don't even arrive in order. Grok's own decoder comment notes that Grok returns results out of order in most multi-tool turns — measured at roughly 30% of pairs mismatched on real transcripts when a simple first-in-first-out fallback is used instead of matching by id. Kimi has the same problem for a different reason: it emits genuinely parallel tool calls, so its toolCallId resolver checks roughly a dozen possible field names across the event, its nested tool object, its result object, and its output object before giving up. Codex's version of the same problem is simpler because Codex doesn't parallelize the same way — it just needs to prefer call_id over id to match the wire format's own naming.

The common thread across all three is that id-based pairing, not arrival order, is what keeps a tool call and its result attached to the same row in the UI. Every decoder was written or hardened specifically because order-based pairing produced visibly wrong results on real sessions, not as a theoretical concern.

Normalizing Toward One Shape, Not the Union of All Shapes

The easy version of this problem is to add a field to the internal message model every time a new provider needs one, until the shared type becomes a superset of every CLI's quirks. Influxx's decoder layer does the opposite: each provider file absorbs its own provider's oddities internally — Grok's synthetic backend results, Kimi's nested error flags, Codex's dual record kinds — so that what comes out the other side is always the same small set of block types the rest of the app already knows how to render.

"Supporting thirty-plus CLIs isn't a UI problem, it's a normalization problem. If we let every provider's transcript quirks leak into the shared chat model, we'd be rewriting the rendering layer every time a new agent shipped a schema change. Isolating that mess into one small decoder file per provider is what lets us add the next CLI without touching anything a user actually looks at."

— Marcus Whitfield, VP of Engineering at ETAPX

Frequently Asked Questions

Does Influxx show a different chat interface depending on which CLI I'm running?

No. Every supported provider's transcript is decoded into the same internal message and content-block model, so Native Chat renders identically regardless of which CLI produced the session.

Why would a Grok tool call show a result immediately with no "in progress" state?

Grok's backend-executed tool calls are written to the transcript only after they've already completed, so Influxx synthesizes the result in the same message rather than waiting for a row that will never arrive.

Can a tool call and its result ever get mismatched in the UI?

The decoders pair calls to results by id specifically because order-based pairing produced measurably wrong matches on real Grok and Kimi transcripts. Pairing can still fail if a provider's raw event omits every id field the decoder checks, which is rare in practice.

Does Influxx modify or filter what an agent CLI actually did?

No. The decoders only reshape how existing transcript data is represented for display — reasoning, tool calls, and results all come from the CLI's own on-disk log, not from Influxx altering agent behavior.

Why do some tool calls get hidden entirely from the chat view?

Certain rows are intentionally filtered because they aren't real conversation turns — for example, Grok's bootstrap context rows and Kimi's synthetic injected system-reminder rows, which would otherwise appear as if the user or agent wrote them.

Is Claude's transcript format treated as just another provider, or something more central?

Claude's content-block shape is the reference schema — the Codex, Grok, and Kimi decoders all reuse the same block-parsing function built around Claude's format rather than maintaining separate parallel structures.

What happens if a provider changes its transcript format in a future CLI update?

Because each provider's parsing logic is isolated to its own decoder file, a format change only requires updating that one file — it doesn't require changes to the shared message model or the rendering layer.

The part of this system a user never sees is the part that took the most engineering effort: making four incompatible logging formats disappear into one honest, consistent status.