Docs

Log

Delivery

Where each Log event goes, the moment it is written: a folder on your machine (the default) or a server, through a local retry queue. Delivery never slows or fails a run, and never touches the forge.

Live
Folder or server
Retry queue
Fail-open

About delivery

Every event is delivered as soon as it is written. There is no batch at the end of a round and nothing to flush.

The logs setting in vinaya.config.json names exactly one destination, a folder or a server. Vinaya resolves it once per process and then delivers every event there.

Delivery is fail-open: writing an event never throws. A failed write or send warns at most once per process and leaves the events queued. The run carries on at full speed.

How it works

Vinaya picks one of three destinations:

DestinationWhat happens
folderThe event is appended to <folder>/<owner>-<repo>/<issue-or-none>.ndjson.
serverThe event is appended to a local retry queue, which is then sent to the server.
noneNothing is recorded. Only happens in CI.

It chooses by checking these rules in order. The first that applies wins.

  1. 1

    A confined dispatch's spool

    If VINAYA_LOG_SPOOL_DIR is set and not empty, events go to a folder at that spool. This beats every other rule.

  2. 2

    The trusted value for unattended runs

    For an unattended caller (an agent, or a CI job), a logs value in the working tree counts only if the default branch declares the identical value. With no local logs, the default branch's value applies. An attended caller (you, at your terminal) uses the working tree's value directly. See Redaction and trust.

  3. 3

    CI

    On a CI host (GITHUB_ACTIONS is set), events go to a server if one is configured and its credential is present. Otherwise nothing is recorded. CI never writes to a folder.

  4. 4

    A server

    A logs.url sends events to that server, with header values filled in from environment variables.

  5. 5

    A folder

    A logs.folder inside the repository is ignored. Otherwise events go to the configured folder, or to the default folder.

Folder delivery

A folder is the default.

  • With no logs configured, the folder is <runtimeDir>/logs/.
  • Each file is <folder>/<owner>-<repo>/<issue-or-none>.ndjson, where <issue-or-none> is the event's Issue number, or none.
  • Writing the line is the delivery. You can read the file while the run is going.
  • logs.folder must be an absolute path outside the repository. A folder inside the repository is refused for every caller.
  • A folder file rotates at 8 MiB, the same way as the retry queue below.

Server delivery

With a server, each event is first appended to a local retry queue, and the queue is then sent to the server.

  • Events are sent as application/x-ndjson in a POST body.
  • Within one process, sends happen one after another, never in parallel.
  • The server stores each event by its event_id, so a chunk sent twice creates no duplicates.

The local retry queue

ItemValue
Path~/.vinaya/outbox/<owner>-<repo>/<issue-or-none>.ndjson, or ~/.vinaya/outbox/unresolved/… when the remote cannot be resolved
Why not under runtimeDirThe queue is plumbing for this machine, not a file of a task run
Each writeFolder created with mode 0o700; a symlink or non-regular file is refused; the file is opened for append with mode 0o600, never following a symlink; one write per line, ending in a newline
RotationAt 8 MiB the file moves to a single slot, <name>.1.ndjson, overwriting the previous slot
OverflowWhen a rotation overwrites events, a warning names the events lost (once per process)

How the queue is sent

  1. 1

    Oldest first

    The queue is sent one file at a time, oldest first: a leftover <name>.draining.ndjson, then <name>.1.ndjson, then the live file.

  2. 2

    Set aside before reading

    Each file is renamed to <name>.draining.ndjson before a byte is read. New events keep landing in a fresh live file that the send never touches.

  3. 3

    Sent in chunks

    Chunks of at most 5 MiB are cut from the head of the file. Each POST times out after 30 seconds. Once the server answers with a 2xx, the bytes it confirmed are removed before the next chunk is built.

  4. 4

    Stopped on the first failure

    A refused or failed chunk ends the send. The draining file keeps exactly what was not confirmed, and the next send retries the same head chunk. A non-2xx answer (for example 401) never marks a line as bad: the queue stays whole.

Before it is sent, each line is checked and redacted again. A line that is invalid, of an unknown version, or larger than one POST moves to <name>.rejected.ndjson with its reason, classification and identity, and the send carries on. This warns once per process. If the rejected file cannot be written, the send stops.

Only one sender at a time

  • A .flush-lock file next to the queue is created exclusively and held for the whole send, across processes. Another process that finds it taken returns at once and catches up on its next event.
  • The holder renews the lock before every chunk. A lock not renewed for two minutes (four times the 30-second POST timeout) is taken over. A holder that finds its lock taken over stops.

Sandboxed agents

A sandboxed Developer or Reviewer cannot write to <runtimeDir>/logs/. So the driver sets up a spool inside the dispatch's scratch area and passes it as VINAYA_LOG_SPOOL_DIR.

After the turn, before the scratch area is removed, the driver copies only this dispatch's own <owner>-<repo>/<issue>.ndjson into the real folder, line for line. Any other file in the spool is ignored.

Delivery in CI

A CI job delivers to a server or records nothing.

CaseResult
No logs.url approved by the default branchnone: "no server destination is configured for CI delivery"
logs.url set, but a ${VAR} in logs.headers is empty or missing (a fork pull request, a missing secret)none: "this job holds no delivery credential"
Both presentserver, as above

Vinaya warns once per process with the reason. The generated vinaya-checks.yml workflow sets VINAYA_UNATTENDED: '1' and VINAYA_LOG_TOKEN: ${{ secrets.VINAYA_LOG_TOKEN }}. An event written at process exit on a CI host records nothing.

When a run falls back to the folder

If an unattended run declared a logs.url that the default-branch rule refused, its events go to the folder, and the folder records why:

ReasonMeaningvinaya doctor shows
anchor-unreadableThe default branch's config could not be read: offline, not signed in, or slower than 8 seconds. Events wait in the folder for vinaya log send.info
anchor-mismatchThe default branch was read and declares a different url.warn

The fallback is recorded once per process: one line on stderr, a line in the task's driver.log when the event names a task, and ~/.vinaya/fallback/<owner>-<repo>.json. vinaya doctor shows the last one as [logs-fallback]. The plain default folder carries no reason.

Time limits

LimitValueCovers
Context lookup3 secondsThe branch, repository and doctrine lookups for each event; the lookup is killed at the limit
Default-branch read8 secondsReading the default branch's logs value, once per process per repository

Events at process exit

Writing an event is fire-and-forget. If a process calls exit before the write settles, the event is lost. So Vinaya waits for every pending write and send before an abrupt exit. The loop driver does this in its signal handlers, before it re-executes itself, and when it finishes.

For a process that exits straight after writing, Vinaya also has a synchronous write that appends the line before returning. If the destination is not resolved yet, that line goes to a fallback folder (never a server, and nothing in CI). It does not send anything; a later send or vinaya log send delivers it.

Never the forge

Events are never written to the forge: no Issue or pull-request comment, no CI artifact.

Configuration

Key or variableEffect
logs.folderFolder destination (absolute, outside the repository)
logs.url, logs.headersServer destination and its ingest headers (${VAR} filled from the environment)
runtimeDirParent of the default folder
VINAYA_LOG_SPOOL_DIRSpool for a sandboxed dispatch
VINAYA_UNATTENDEDMarks a caller as unattended (a CI job)
GITHUB_ACTIONSMarks the host as CI

Commands

CommandWhat it does
vinaya log sendMoves folder files into the retry queue and sends it once
vinaya doctorShows the destination ([logs]), where the credential comes from ([log-credential]) and the last fallback ([logs-fallback])

Troubleshooting

What's next