Skip to content
This text was generated by an LLM (how cgwalters uses LLMs). cgwalters has not reviewed it yet.

The ACP session

agentic-job run drives its agent over the Agent Client Protocol: JSON-RPC between a client and an agent, the same for every agent that implements it. The session (src/session/) is the client. That gives one integration, a transcript with the same structure whatever the agent, permission requests as explicit messages, and a session a person could join (interactive.md). Where its code sits and what it is handed is in layout.md.

CI drives the scripted fake agent. Real-agent deployment evidence is reported in the README; configuration unit tests alone do not show that an adapter works.

What it does

It starts the agent through the wrapper run gives it and speaks ACP over the agent's standard input and output; nothing listens on the network. Without --runner-home the wrapper is run0 and the agent is the sandbox user, while the session runs as the runner's user, so the agent cannot alter the transcript or the limits. The reusable workflow, by default (agent-user: runner), and review.yml pass --runner-home: the agent is the runner's user too, so it can write the session's files, the transcript and limits included, read run's environment and stop run; the record is then evidence of what the agent let through, not a guarantee (requirements.md). The agent's environment holds none of the job's ACTIONS_* variables and none of its provider settings.

It sends the task as one prompt and advertises no filesystem or terminal capability, so the agent uses its own tools inside the sandbox. Every message in both directions is recorded as it passes. That limits no tool: Claude Code still offers its own Read, Edit and Write, in the working directory the session names, which just claude-acp-test checks against the pinned adapter and a loopback stub. The run's prompt tells every agent to change files with its edit tools, and shell_edits in summary.json flags a change made without one call of them.

It answers permission requests from a policy: allow, except a git push and an edit, delete or move outside the sandbox user's home. An allow is the agent's "allow once", never "allow always". This is for tidiness and for the record, since an agent only asks when it chooses to: the sandbox is the boundary.

When the session ends it kills every process of the sandbox user, or, with --runner-home, only the agent's process group: what the agent detached from it runs until the job ends. What run leaves under --out is in layout.md.

A run that allows outputs gets one reminder if it ends without a hand-back, then fails if none is left. A fix can leave a change for the pull request's branch, a replacement series, or a noop or missing_data request with a reason. An unfinished rebase or unmerged index prevents completion even if a request exists; the reminder names that state. These checks run under the session's existing limits and do not replace output validation.

Agents

Reading a session in the summary

The session is included in the run summary by default. Set render-session = false in [agent] in the repository-owned sandbox configuration to opt out; there is no dispatcher input. The trusted runner renders its redacted ACP copy, not files written by the sandbox user. User and assistant text is prose; reasoning and tools are collapsed separately. Tools show arguments, measured duration, reported status and exit code when available, and at most 8192 bytes of output with an omitted-byte count. Transcript punctuation is encoded to prevent HTML or Markdown injection, including closing a generated details block. Joined text is redacted again so secrets split across streamed chunks stay hidden.

session.md in the agent-run artifact contains the complete rendered session (tool outputs are still bounded). The summary reserves space for accounting and stays below GitHub's 1 MiB limit: assistant prose is retained first, then head and tail blocks. If assistant prose alone exceeds the limit, each message is abbreviated; the artifact keeps it.

A subagent's messages and tool calls render inside the call that started it, where the agent says which that is (Claude Code does; opencode and vibe report only the call's output). Each step shows the context's tokens as the agent reports them. Where it reports the session's running cost in USD, each reading shows what the turn added, and the three costliest turns are bold; session/prompt responses show their turn's tokens. These are the agent's figures, not the inference proxy's.

To replay a long real run in the renderer regression test, download its *-agent-transcript artifact and extract acp.jsonl, then run SESSION_TRANSCRIPT=/absolute/path/acp.jsonl cargo test --locked --lib real_long_session_summary_and_artifact -- --ignored --nocapture. The test checks the full artifact and summary, then forces a smaller preview budget to check prose retention. The ordinary suite separately covers overflow of the 1 MiB limit with 150 tool calls. Real transcripts stay external, like the corpus inputs, rather than committing whole task conversations.

Claude currently runs through claude-agent-acp, not native stream-json. This renderer uses that recorded protocol. Native stream-json ingestion, subagent-turn nesting (not exposed as separate turns by the current adapter), and agent-specific opencode/vibe fidelity remain follow-ups. No sandbox transcript collection or additional permissions are introduced.

Agent adapters

To the session, an agent is an entry of session/agents.toml, built into the binary: its command, how it is told the model, and whether it takes notices. This is not a caller-extensible registry for run: run also has a closed Kind enum and agent-specific configuration and launcher code. ACP makes the session transport common, not provider setup. claude is Claude Code through claude-agent-acp, opencode is opencode acp, and fake is fake-agent demo, which plays a recorded session and needs no model.

vibe is Mistral Vibe 2.26.1, using its legacy Python ACP runtime. The launcher uses its library entrypoint with only the user configuration source and isolated Python imports: .vibe/config.toml, project profiles and checkout modules cannot redirect inference. Both built-in provider names are replaced with the broker's /mistral/v1 chat-completions route; the caller's model is selected under its own name, which Vibe reports over ACP and tells the model it is, and also replaces the built-in model, which Vibe's default and fallback paths select. A model named devstral-2, which Vibe renames to the built-in's, is refused. Mid-turn notices are not supported. That broker model uses text-only tools rather than inheriting Medium 3.5's capabilities. Its context window is its entry of context-windows in [agent] in the repository-owned configuration, for the main agent and the child alike, and Vibe compacts at three quarters of it. config check refuses a window over 1,048,576, which keeps a request under the broker's 16 MiB body limit. Without an entry a model gets a conservative 32k, compacted at 24k, at which a GLM research task compacted 18 times and lost its findings (#821). Z.ai documents a 1M window for GLM-5.3; check what the broker's provider serves before claiming it:

[agent.context-windows]
"zai-glm-5-3" = 1048576

Each model's reply is capped by its max_output_tokens, at most 65,536. A model without one gets 32,000, or a quarter of its declared window if that is less: 8,192 for the job's own model. Uncapped, one response ran past the broker's body limit. The legacy runtime ignores the model's max_output_tokens, so the session also sends the active model's cap as Vibe's undeclared max_tokens session config option, which that runtime sends with every model request. That runtime does not fail a reply cut off at the cap; how a model recovers from one has not been tested. Vibe 2.26.1 does not retry a model stream that ends without a finish reason, and retries a dropped connection only before the first chunk. Only its terminal interface retries those, and no setting enables that for ACP. Over ACP the failure is a generic internal error, which the session cannot tell from others to resend the turn. Such a run fails: a known limitation.

A configuration repository (agent-config-repo, agent-config-path) may instead declare the models, in its config.toml. Only its [[models]] (at most 32) and active_model, the alias of one of them, are taken; each model's provider must be mistral or llamacpp, the two names the job writes as the broker, and a model may set only the fields of the pinned Vibe's model definition, none of them an address or a key, under an alias that is not Vibe's own (mistral-medium-3.5, devstral-2), which Vibe would merge with its built-in or rename. The file's providers and every other setting are ignored: they could send inference and the run token elsewhere, or turn on telemetry and connectors. The file holds no key. Its models' window, compaction, thinking and output settings apply as written; only max-requests bounds what they spend. A model the caller names still wins over active_model: it selects the declared model of that alias, else of that name, or, if none is declared, is added as above. Dispatch and review always pass AGENT_MODEL, so there the file only declares models. Vibe keeps its built-in model beside the file's, on the same broker provider, where it may serve as the image fallback of a text-only active model. The run token lives only in the private generated TOML, as an Authorization header, with no provider key or browser login. Telemetry, experiments, connectors and update checks are disabled. Instructions are pointed out in the task rather than automatically loaded from the project.

The credentialless hosted adapter test uses generated configuration and a loopback model to verify ACP model selection, the output cap, a permission-requested file write, cancellation followed by hand-back, and hostile project config and import canaries. Run it with AGENTIC_JOB_TEST_VIBE_PYTHON=/path/to/venv/bin/python3 cargo test --locked --test vibe -- --nocapture after installing mistral-vibe==2.26.1 there. This is adapter evidence, not a privileged sandbox or live broker test.

run does not start the real agents by those commands. It starts agentic-job launch-agent NAME, a hidden command that runs as the sandbox user, reads the one file holding the run token, sets the agent's environment and becomes the agent. So the binary must be where the sandbox user can run it, such as /usr/local/bin; run checks that before it clones.

Adding an ACP agent to run

Paths here are relative to crates/agentic-job/src/. Adding a TOML entry alone does not make a new [agent] name work. Keep provider setup in reviewed code: an agent's configuration precedence and alternate login paths cannot be secured by assuming it honors a base-URL variable.

First establish the adapter's contract. It must speak ACP on stdio and use its own filesystem and terminal tools (the client advertises neither). Identify the inference API it speaks, how it takes a base URL and run token, and how it selects a model. Check which user, project, environment, profile and login settings can override those choices. Determine whether a mid-turn session/prompt steers the current turn or ends it, how it ends a turn whose model request the provider refused, and test cancellation followed by the hand-back prompt against the actual adapter.

The implementation recipe is:

  1. In session/agents.toml, add the ACP command and non-secret defaults. Use model-env if the adapter selects its model with an environment variable; otherwise the session uses ACP's session/set_config_option with the model option, and fails the run unless the agent's answer reports that exact value as current: so opencode's provider/model/variant, reported without the variant, is refused. Neither a model from the environment nor a later change of model is checked. Leave notices false unless a mid-turn prompt is known to reach the next step without ending the running turn. Set provider-error to what the adapter's last message starts with if it answers a turn whose model request was refused as done. Registry env values go on the wrapped command line: never put a token there, or in command.
  2. In run/agent.rs, extend Kind, its name/parser list and its matches. Choose the proxy API route, declare the one token-bearing home file, implement configuration generation and validation, and decide which instruction files need to be pointed out in the task. If configuration repositories are supported, explicitly select and validate their files; do not copy a directory of plugins or tools. Pin or disable settings that could redirect inference, enable alternate providers or load project configuration. Tokens belong only in the private home file (0600, directory 0700), never root-owned managed settings, argv or logs. If managed settings are needed, provide them through managed_settings for sandbox setup; run cannot write them after hardening.
  3. In run/launch.rs, add the agent's launch behavior: read the run's configuration, set only the required environment, and execute the registry command. Extend KEPT only for non-secret model settings that must survive filtering. In session/process.rs, extend NOT_INHERITED for the new agent's provider, credential and configuration variables, so neither the wrapper nor agent inherits ambient settings. Account for configuration-location variables too, while preserving the login session variables needed by rootless containers. The launcher has no privilege or credential the agent lacks; the sandbox and egress policy remain the security boundary.

The current integrations illustrate why the last two steps are code. Claude uses ANTHROPIC_BASE_URL, a private environment file and managed settings that override project provider switches. With the proxy run API, it sends a placeholder credential plus the run token in x-run-token; with token-file registration, the token is the credential itself. Its model is ANTHROPIC_MODEL, and mid-turn notices are disabled. opencode instead gets a single provider's options.baseURL and options.apiKey rewritten in its global JSON configuration; its launcher disables project configuration and model fetching. Its model is an ACP session option and mid-turn notices are enabled. See inference.md for the proxy modes and routes; an ACP adapter still needs a compatible inference API, not just a compatible session protocol.

Extend the table tests beside run/agent.rs, run/launch.rs, session/agents.rs and session/process.rs. Cover registry/Kind agreement, model delivery, missing inference, exactly one private token file, token-free managed settings and argv, hostile inherited variables, and project/configuration overrides. Check run/probe.rs's token-location probes for the new file. Review session/digest.rs's subagent tool and cost interpretation so task and spending limits remain meaningful. Exercise permission requests, notices, cancellation, limits and cleanup through the scripted session tests, then verify the real adapter in the sandbox with the proxy: generated-config tests alone are not deployment evidence. Install the agent and adapter where the sandbox user can execute them, and document the tested versions and results before claiming support.

Limits

[limits] has timeout-minutes, max-requests (model requests, as the inference proxy counts them), max-tasks (subagent tasks) and budget (hundredths of a dollar, of the cost the agent reports). Three tables are refused before the agent starts:

  • one without a timeout;
  • one with neither max-requests nor budget, unless it says uncapped = true (and one that says it together with either);
  • one with max-requests where nothing counts requests: no proxy, register = "token-file", or a proxy that registered the run without a count. The cap is refused, not dropped.

At 60% and 80% of the more used of the time and the requests, an agent whose entry says notices = true is told so in a prompt of its own. Claude Code is not: its adapter would end the running turn for one. At 95% of the timeout, within ten requests of the cap, or once the agent starts more tasks than allowed, the turn is cancelled and one more prompt asks the agent to hand back what it has. At the limit the session is cancelled. Spending over budget stops it at once, with no hand-back (then). The cost comes after each model reply, so the reply that crosses the budget is often the last of a turn the agent has already ended. If the agent answers the cancel with end_turn, the run ends well, exit state 0: its harness.json message says how far over it went, the run action warns and sets its over-budget output, and the status comment says so. A turn answered with cancelled is a stop as before. A turn that ends just as the timeout cancels it is treated the same way. The budget cannot refuse a reply in advance, because no one knows its cost before it comes; max-requests is the cap that acts before a request is made.

max-repeats (off unless set, 2 to 100) is for an agent stuck making one tool call over and over: GLM agents have repeated one gh api command hundreds of times, until the provider's rate limit ended the run with nothing handed back. Once that many finished calls in a row had the same tool, input and result, the agent is told so in a notice. The step under way may still make the call once more; a second call after the notice has the agent hand back as at a limit. Only a changed result starts the count again: a poll that keeps answering "in progress" counts, so leave room for one. The harness sees calls only as the agent reports them, after they ran, so it cannot refuse one. An agent without notices is not told, and hands back at the same call.

A task's turn that ends on an error is not taken for a finished task. That is a turn the agent answers with an error, as opencode and Vibe answer one whose model request failed and as Claude Code's adapter answers one it gave up on (Internal error: API Error: Request rejected (429) ..., with errorKind in the error's data), or one it answers as done after saying it gave up on a refused request, as Claude Code says API Error: ... after its own retries (the entry's provider-error names the words). After a pause of a minute (no longer than the hand-back window), the agent is asked to hand back as near a limit. If it does, the run is stopped at a limit, exit state 3, with the error as the reason; if it doesn't, the run fails with the error. A limit the run reaches meanwhile stops it as any other time, and a bridge client's prompts meanwhile are refused.

The agent's word is not needed to keep its change, though: handing back needs the model too, and two runs lost 45 minutes of work to a provider that stayed overloaded (#1860). A run that failed on a model provider's failure of a known class (any but other, an agent that died of one included) and whose working tree has a change is handed back as if stopped at a limit: exit state 3, harness.json and summary.json with result budget and a message starting "the model provider stopped the run" that names the class but not the error's words, the default partial pull request with that as stopped_early, and its checkpoint under ready_when_complete. Like a run whose agent handed back, it is filed as budget, a partial result to continue rather than a run to repeat, with the class in provider_error. The session's own harness.json under work/ keeps the failure. A run that changed nothing, whose change was not handed back, or that failed for anything else, still fails with exit state 1.

A model provider's failure that a wait may clear gets a few retries before that, for every agent. The session sorts the error's words, or the agent's, into a class (session/provider.rs): rate_limited (HTTP 429, "rate limit"), overloaded (5xx, Anthropic's 529, "overloaded"), unreachable (refused, reset, closed, EOF, a timeout), quota (a usage or spending limit), unauthorized (401 or 403) or other. The words are those of real runs' transcripts, in tests/data/provider/errors.json; a status code counts only next to a word that makes it one. Vibe's RateLimitError, code -31001, is rate_limited by the entry's rate-limit-code whatever its words (JSON-RPC's reserved codes are refused there). A turn that ended on one of the first three is sent again on the same session, labelled rate limited or provider error, behind a note, after a wait of 10, 20 and 40 seconds plus up to a quarter of jitter, or as long as the provider said to (retry-after), each no longer than the hand-back window: three times, and then the turn is taken as one that ended on an error. An overloaded provider is out of capacity for a while rather than refusing the run, and is waited out for about four minutes: six times, the waits growing to a minute each. A provider that asks for more than two minutes is not waited for at all. Runs had ended on a 429 that a minute's wait would have cleared (#1339). A quota or unauthorized failure is never waited on: nothing within the run clears it, so the agent is asked to hand back at once, without the pause. The waits count against the run's time; a limit reached in one stops the run, and near one the agent is asked to hand back instead of waiting on. provider_error in harness.json and summary.json names the class of the failure a run that did not end well ended on. An agent that died mid-turn, after letting a failure out on its standard error or in its words in that turn, died of it: that, not the adapter's crash, is the class; the words of an earlier turn do not count, and the agent's own log uses words like "timed out" or "connection closed" for other things, so only a provider's phrasing of them does. resends and resend_wait_s count the times the task was sent again and the seconds spent waiting, a wait a limit interrupted included. summary.json files the failure of a run that did not hand back under inference_ and the class, and the run action and the status comment carry that word on (workflow.md).

An overload that outlasts its waits can be worked around rather than handed back: fallback-model in [agent] (for example opus for a run of fable; Claude Code and the fake agent only, whose models are names the provider resolves) has the session switch the agent to that model through its model session config option (ACP's session/set_config_option, which claude-agent-acp serves), so that it keeps its context, and send the task again at once, the waits starting over. It switches once; an agent that refuses the switch, reports no model or still the one it ran is asked to hand back as without one. fallback_model in harness.json and summary.json names the model the agent then said it ran, and model in summary.json names it too, so that pacing charges the pool the run went on with.

Nor is a task's turn that stalls left to the timeout. Vibe has no read timeout on its model requests, and one waited six minutes on idle connections before going on (#983). An agent entry with stall-timeout (seconds; Vibe's is 120, no other has one) has a turn in which nothing comes from the agent for that long, with no tool call running, cancelled and the task sent again once on the same session, labelled stalled, behind a note that the last turn stalled. A second stall, or a cancel the agent does not answer, fails the run with a message that starts the agent stalled, and summary.json files it as a stall failure rather than agent_exit. stalls and stall_retries in harness.json and summary.json count both. Only the task's turn is watched; the hand-back, reminder and client turns are not.

Nor is a task's turn that ends well without the output the run requires. A run whose policy allows any output requires one: a request in out/safe-outputs.jsonl, a replacement, or, where the policy carries a change, a change of the checkout. Agents have answered end_turn without theirs, after a compaction of their context, and the run ended well with nothing handed back (#607). The session then checks, as the sandbox user, and asks once more in a prompt of its own, labelled reminder, under the same limits. A requests file the run would not take (over its limit, or not a regular file) is reminded of the same way, naming the file and the limit, since the hand-back would otherwise be dropped whole without the agent hearing of it. If the agent still left none, the run fails, exit state 1. reminded in harness.json and summary.json says a reminder was sent. The check only decides the reminder; the hand-back is still collected and checked as before. A run with a bridge client is not reminded: the client sees what the agent left.

Where the bounds set ready_when_complete for the run's change, the brief asks the agent to write its outcome early, with a checkpoint object (complete, done, left, next) marked incomplete, keep it current, and set complete to true last. Restating the hand-back's path and what was left is what brought back a hand-back lost to a compaction, where a bare reminder did not (#813). So the reminder quotes the checkpoint's left and next, and after each compaction the session sees during the task's turn (a Vibe-style compaction tool call that completes, or the context reported in a usage_update falling below half, as for a second agent), an agent that takes notices is sent a compaction notice: the start of its task, the paths it hands back in, and what its checkpoint says is left. An open checkpoint is never taken for done: see ready or draft. Without ready_when_complete, the brief asks for no checkpoint and no compaction notice is sent.

Updates of the run's item

Direction added to a run's issue used to mean cancelling the run and dispatching it again, losing its work (#925). With the repository-owned configuration's

[session]
watch-item = true
watch-seconds = 60 # the default; 10 to 3600

run polls the issue or pull request the run is for (--watch-item, which the reusable workflow sets from the event or the issue input) with the agent's own read-only token, and sends the agent each comment made on it (not a pull request's review comments), and each edit of the body, while it works. What came between the event and the session's start is in the brief or not seen. Off by default; without the token or an item it watches nothing, and says so. The polls are conditional (If-None-Match), so an unchanged item costs no rate limit, and a poll GitHub refuses for a limit waits as long as GitHub asks.

Every update is passed on, whoever wrote it, and none claims authority: it is quoted with its author and link as new input on the item, which the agent weighs as any other text it reads. Anyone who can comment on the item could already put text before an agent that reads it with gh. A run takes at most ten updates, each cut to 8000 characters.

Updates go into a running turn only: by claude-agent-acp's _session/steering for an entry with steering = true, which asks the adapter not to start a turn when none is running; otherwise as a notice is, for an entry with notices = true. An agent that takes neither (Vibe) is sent none, and the condensed log says so. Each update sent is a line of the condensed log, and its link is in updates of summary.json and in summary.md.

A second agent

A run can let its agent start a second agent, the child, and drive it over ACP turn by turn: to watch, for one, what a model does after its context is compacted. Only Vibe can be the child. It is off unless the repository-owned configuration names one:

[child-agent]
name = "vibe"
model = "zai-glm-5-3"

sandbox setup installs the same pinned Vibe 2.26.1 runtime whether Vibe is the main agent, the child, or both. Other child names are refused by config check and setup: only Vibe has a supported child installer.

The child uses [inference] url plus /mistral/v1, not the main agent's openai-url or anthropic-url. The broker's run-token contract admits an active run token on every injected route, including Mistral; registration is not scoped to the main agent's provider. No child pool list or second registration is needed by this protocol (this repository has no pool configuration). The broker deployment must enable its Mistral route and supply its Mistral credential. This confirms the documented protocol, not a live deployment test. The repository-owned configuration must explicitly set [inference] url, register (and audience for github-oidc), allow that address in egress.direct when required, and set [limits] timeout-minutes and max-requests.

run then writes the child a Vibe configuration of its own, ~/.config/agentic-job/child-vibe/config.toml, as it would write the main agent's for that model: the same proxy route and run token, so the token probes expect it in that file and the agent's, and nowhere else. Its model is a built-in definition, with the window [agent] context-windows gives it (32k without an entry); the main agent may edit that file, which gives it nothing it does not have with its own token. The task is told the path of acp-drive.

The configuration is refused without limits.max-requests. The budget reads only the cost the main agent reports, but the proxy counts every request made with the run's token, so the request cap covers both agents: a run capped by its budget alone could spend without bound through the child. The timeout covers both too, since the session kills every process of the sandbox user when it ends. run --runner-home has no sandbox user to end, and the daemon's session of its own would outlive the agent's, so it refuses [child-agent].

The main agent uses it through bash; each command prints JSON:

  • agentic-job acp-drive start --agent vibe [--model M] [--cwd DIR] starts a daemon, as the sandbox user, that starts Vibe through launch-agent --child, opens a session and holds its standard streams. It prints the session and the model Vibe reports.
  • prompt TEXT [--timeout S] sends a turn and prints its stop reason, or the error it was answered with. With --timeout it stops waiting (exit 124) and the turn goes on.
  • events [--since N] prints the events from the Nth on: the prompt, messages, thinking, tool calls and their results, permission answers, compactions, the end of each turn and the child's exit. An event never changes once printed; a message still streaming is cut where it stood and goes on in the next.
  • cancel cancels the running turn; status prints the session's state; stop kills the child and ends the daemon.

Vibe 2.26.1 reports a compaction as a tool call titled "Compacting context", then "Context compacted"; those are compaction events. A compaction it does not report is inferred, "inferred": true, when the context it reports in a usage_update falls below half the last one. The child's permission requests are answered by the main agent's policy.

The daemon talks to its commands over a socket in ~/.local/state/agentic-job/acp-drive, private to the sandbox user, and writes there every ACP message (acp.jsonl), the child's standard error and its own log. It holds nothing the main agent does not and is not a boundary: the sandbox and the proxy's count are. After the session run reads that directory and the child's Vibe logs as the sandbox user, as it reads the agent's own logs, into child-session.txt and child-logs.txt of the transcript, redacted with it; child_logs in summary.json says what was taken. As there, the last 4 MiB of each file is kept.

tests/acp_drive.rs drives the scripted agent through a prompt, a cancel, reported and inferred compactions and a crash. Driving a real Vibe this way has not been tested.

How a run ends

The session's result becomes run's exit state: 0 when the agent ended its turn, 1 when it failed, exited, could not be started or cancelled, 3 when a request, task or spending limit stopped it (or it handed back after a turn that ended on an error), and 124 for the timeout. interface.md has the whole table, with the states that are run's own. handed_back in summary.json says whether a stopped agent handed back.

The transcript also holds agent-logs.txt: the agent's own log files written during the session, which may say what its standard error and ACP do not, such as a dropped model stream. They are read as the sandbox user from one directory under its home: ~/.config/agentic-job/vibe/logs (the launch command turns Vibe's log on, as its ACP entrypoint does), ~/.local/share/opencode/log, and ~/.claude/debug, which Claude Code writes only with debugging on. Links are not followed; the newest 16 files are taken, the last 4 MiB of each and 16 MiB in all, and redacted with the rest of the transcript. agent_logs in summary.json says what was taken, and the summary says when the directory is missing.

Vibe sends its reasoning as message chunks holding Python reprs of thinking blocks; the condensed log and the rendered session read them as reasoning in plain text, as other agents' thought chunks.