Apus applies provider-specific fixes to transcripts before a run (building model context). These are in-memory adjustments used to satisfy strict provider requirements. Runtime transcript state stays in SQLite; provider-specific assistant-prefill stripping happens only while constructing outbound payloads.

Scope includes:

If you need transcript storage details, see Session management deep dive.


Failed attempts and recovery

Text-only assistant errors are buffered until the logical run settles. Recovery discards their partial text because the recovered reply supersedes it. Terminal failure persists the last attempt's partial text and error.

Tool calls, displayable non-text content, and attachment facts are persisted immediately, before dependent tool results or the recovered reply. These fact rows omit the error and use a replayable stop reason so provider replay retains the calls. For mixed text/fact messages, partial text and the error remain buffered separately; terminal settlement does not duplicate facts or usage. This uses existing assistant-row shapes and requires no database migration.

Global rule: runtime context is not user transcript

Runtime/system context can be added to the model prompt for a turn, but it is not end-user-authored content. Apus keeps a separate transcript-facing prompt body for Gateway replies, queued followups, ACP, CLI, and embedded Apus runs. Stored visible user turns use that transcript body instead of the runtime-enriched prompt.

For a runtime-only turn with no user text, the embedded runner marks its synthetic continuation as hidden internal-system input. It remains in model history so cold replay preserves the original request prefix, but it is not human-authored text or visible chat history. The next real user input persists normally even if prompt submission fails before the runtime-only turn starts. Existing stored markers are not rewritten.

Compaction and saved CLI session notes exclude the reserved apus.runtime-context custom message type, including older entries without carrier metadata and entries that opt out of provider replay. Provider carrier metadata controls replay authority; it does not make private context eligible for summaries or saved notes. Existing transcripts receive this filtering when read, without rewriting stored history.

For legacy sessions that already persisted runtime wrappers, Gateway history surfaces apply a display projection before returning messages to WebChat, TUI, REST, or SSE clients.


Where this runs

The embedded runner selects and applies transcript policy:

Legacy JSONL validation and import belong to apus doctor --fix; the embedded runner does not repair or reopen file-backed runtime transcripts.


Global rule: image sanitization

Image payloads are always sanitized to prevent provider-side rejection due to size limits (downscale/recompress oversized base64 images). When an image backend is available, replay also checks that each retained image can be decoded. A corrupt image becomes an omitted image payload note; valid images, surrounding text, and tool errors remain intact. A successful reply does not repair the original stored image bytes, so failed checks are never remembered and a corrupt image stays omitted on every replay. Successful outcomes (verified as-is, or the downscaled replacement) are cached in process by content digest and limits, bounded to 16 MiB, so later turns reuse them instead of decoding the same bytes again. This also helps control image-driven token pressure for vision-capable models: lower max dimensions reduce token usage, higher dimensions preserve detail.

Implementation:


Global rule: malformed tool calls

Assistant tool-call blocks missing both input and arguments are dropped before model context is built. This prevents provider rejections from partially persisted tool calls (for example, after a rate limit failure).

Completed call/result pairs remain history when their tool is disabled, removed, or unavailable in the current catalog. Their names still require valid syntax; malformed calls, ambiguous pairing, and synthetic missing-result repairs do not grant this exception. Replaying a completed pair does not advertise or authorize the tool for a new call.

Implementation:


Global rule: tool result pairing

The live runner rewrites a provider ID that repeats an earlier call, keeping IDs unique across responses within an embedded run attempt, including after compaction. Persisted older transcripts still use occurrence-based pairing.

Tool results are paired to tool-call occurrences within each assistant turn before provider-specific call IDs are rewritten. Provider-generated IDs may repeat on later turns, so a result adjacent to a repeated call stays with that occurrence. A displaced result is moved only when exactly one unresolved occurrence can own it; ambiguous extras are dropped and missing occurrences receive synthetic error results.

Synthetic missing results tell the model that the outcome is unknown: retry only read-only or idempotent operations, and verify current state before repeating an operation that may have had side effects. Responses-family transports retain their aborted placeholder. Neither placeholder proves that the tool did not run.

Implementation: sanitizeToolUseResultPairing in src/agent/agents/session-transcript-repair.ts

When switching models, provider replay moves delayed asynchronous tool results next to their originating call before removing the source model's async metadata. Call and result IDs are trimmed before matching, so surrounding whitespace does not turn a real result into a synthetic missing-result error. This projection runs in packages/ai/src/transcript-transform.ts and leaves stored history intact.


Global rule: incomplete or silent reasoning-only turns

Assistant turns are omitted from the in-memory replay copy when they contain only thinking or redacted-thinking content after either of these events:

The silent-reply cleanup prevents hidden reasoning from merging into a later assistant tool-use turn when strict providers rebuild the conversation.

Empty length turns remain unchanged, as do length turns with visible text, tool calls, or unknown content blocks. Silent-reply turns with tool calls or unknown content blocks also remain unchanged. Stored transcripts are not rewritten.

Implementation: normalizeAssistantReplayContent in src/agent/agents/embedded-agent-runner/replay-history.ts


Global rule: inter-session input provenance

When an agent sends a prompt into another session via sessions_send (including a delayed reply delivered to the requester), Apus persists the created user turn with message.provenance.kind = "inter_session".

Apus also prepends a same-turn [Inter-session message] ... isUser=false marker before the routed prompt text so the active model call can distinguish foreign session output from external end-user instructions. This marker includes the source session, channel, and tool when available. The transcript still uses role: "user" for provider compatibility, but the visible text and provenance metadata both mark the turn as inter-session data.

During context rebuild, Apus applies the same marker to older persisted inter-session user turns that only have provenance metadata.


Provider matrix (current behavior)

OpenAI / OpenAI Codex

OpenAI-compatible Chat Completions

Google (Generative AI / Gemini CLI / Antigravity)

Anthropic / Minimax (Anthropic-compatible)

Amazon Bedrock (Converse API)

Mistral (including model-id based detection)

OpenRouter Gemini

OpenRouter Anthropic

Everything else


Historical behavior (pre-2026.1.22)

Before the 2026.1.22 release, Apus applied multiple layers of transcript hygiene:

This complexity caused cross-provider regressions (notably openai-responses call_id|fc_id pairing). The 2026.1.22 cleanup removed the extension, centralized logic in the runner, and made OpenAI no-touch beyond image sanitization.

Related