Skip to content

Latest commit

 

History

History
636 lines (545 loc) · 35.1 KB

File metadata and controls

636 lines (545 loc) · 35.1 KB

Request log

omni-dev keeps a local, append-only log of every invocation and the HTTP requests it issues, and ships an omni-dev log subcommand to search and pretty-print it. It is the single, durable, queryable record of what was run and what it talked to over the network — the thing RUST_LOG tracing (ephemeral, stderr-only) is not.

What gets recorded

One JSON object per line (NDJSON). A kind field discriminates the two record types, so the log is a complete invocation history, not just an HTTP history:

  • kind: "invocation" — one per process run (and one per MCP tool call): the resolved subcommand path, full argv (with secret-bearing flag values redacted), exit code, duration, any top-level error, and a whitelisted OMNI_DEV_* env snapshot.

  • kind: "http" — one per outbound request (recorded inside each client's retry loop, so retries and transport failures are captured too): service, method, URL (secret-bearing query/fragment parameter values redacted — see Redaction posture), status, elapsed, and any error.

  • kind: "gh" — one per gh CLI subprocess invocation. Every GitHub call funnels through gh (the token never enters our process), so these are subprocess records, not http ones. Each carries the semantic subcommand (api graphql, pr list, repo view, …) in command, the scrubbed argv in command_line, the source, the exit code, and the duration. This is what omni-dev log count --kind gh aggregates — see Counting GitHub API calls.

  • kind: "worktree" — one per omni-dev git worktree <verb> invocation (thin logged wrappers over git worktree add/remove/list/move/prune/repair, #1392). Tagged service: "worktree", with command set to ["git", "worktree", "<verb>"] and the recovery-relevant metadata in the free-form context map — for remove: the worktree's path, branch, commit (HEAD before removal), had_uncommitted, and used_force; for prune: a JSON pruned list of {path, branch, commit}. See Recovering a removed worktree.

  • kind: "drivemutation" — one per omni-dev drive rename/drive move/drive create/drive upload/drive edit/drive sheets * attempt, written from inside the mutation itself so it covers every current and future caller (CLI today, MCP later), not just the CLI. Includes a refused move (Blocked, ADR-0070) or create/upload/edit (Blocked by the folder write-permission gate, ADR-0071, issue #1574) — no mutating API call happens for those, but the refusal is itself the security-relevant event. Tagged service: "drive", with command set to ["drive", "<operation>"] and the outcome detail in the free-form context map: file_id, file_name, status, and — only when present — added_principals/removed_principals (comma-separated, move-only) and crosses_drive_boundary (move-only), plus resolved_folder_id/decided_by_folder_id/decided_by_depth (the folder the write-permission gate evaluated against, and which configured rule, if any, decided the verdict — absent for the ungated rename/move), and decided_by_file_id when a file rule decided it (issue #1612). decided_by_file_id is mutually exclusive with decided_by_folder_id, deliberately: a file id never appears in the folder field, so an existing --query decided_by_folder_id:<id> cannot start matching a different kind of id. A file rule matches the target itself, so it walks no chain — decided_by_depth and resolved_folder_id are both absent alongside it. See docs/drive.md.

    Cell writes through the Sheets API (issue #1589, ADR-0073) use this same kind, with operation of sheets-write/sheets-append/sheets-clear/sheets-create — so command reads ["drive", "sheets-write"] even though the CLI spells it drive sheets write. They add five more omit-if-absent context keys: range (the A1 range as composed and sent), and updated_range, updated_rows, updated_columns, updated_cells (what the API reported actually changing). updated_range can differ from range — the server resolves an open-ended range against the sheet's real extent — which is what makes omni-dev log --query kind:drivemutation able to answer "how much did that write touch", not just "a write happened".

    Structural edits (issue #1613, ADR-0075) use the same kind again, with operation of sheets-add-sheet/sheets-rename-sheet/sheets-insert-rows/ sheets-insert-columnsone record per verb, which is also one per spreadsheets.batchUpdate request, since each verb sends a batch of exactly one. They add four more omit-if-absent context keys: sheet_id (the stable numeric id the API addresses, which survives a later rename and so is the only durable answer to "which tab was this"), sheet_title (its title at the time), sheet_new_title (the title a rename-sheet moved it to — set by that verb alone, since sheet_title necessarily holds the title the sheet had before, and without it a record could say which tab was renamed but not to what), and dimension_range (e.g. "ROWS 5:7", 1-based inclusive like the CLI's --at). dimension_range is the structural analogue of range, for effects A1 notation cannot express, and is omitted for a span that spans nothing; a structural verb sets no range and no cell counts.

    Destructive edits (issue #1623, ADR-0077) use the same kind again, with operation of sheets-delete-sheet/sheets-delete-rows/ sheets-delete-columns/sheets-delete-range — same one-record-per-verb-and-per-request shape as the structural edits above, and the same four structural context keys (sheet_id, sheet_title, dimension_range for the two dimension-delete verbs). They add one more omit-if-absent context key: grid_range (e.g. "rows 2-4, columns 2-3", 1-based inclusive like the CLI's --start-row/--end-row/ --start-column/--end-column) — the deleteRange analogue of dimension_range, which only ever spans one axis and so cannot record a rectangle. delete-sheet sets neither dimension_range nor grid_range. Every --dry-run, additive or destructive, is unlogged — record_attempt only ever runs for a real mutation attempt — and a destructive one stays structural-only besides: no cell content is ever read or logged for any structural verb, delete included.

    Formatting, data validation and protected ranges (issue #1643, ADR-0078) use the same kind again, still one record per verb: update-borders can set up to four Border sides in a single request, which is the first case where one request carries more than one independently-toggleable effect, but it is still one request, so the one-record-per-verb-per-request equivalence ADR-0075 §7 established holds. New operation values: sheets-format-cells, sheets-update-borders, sheets-merge-cells, sheets-unmerge-cells, sheets-auto-resize-dimension, sheets-update-dimension-properties, sheets-duplicate-sheet, sheets-reorder-sheet, sheets-hide-sheet, sheets-show-sheet, sheets-set-data-validation, sheets-clear-data-validation, sheets-protect-range, sheets-update-protection, sheets-unprotect-range. Six more omit-if-absent context keys: fields_changed (a human-readable summary of which CellFormat/border/dimension-property fields a verb set — the formatting analogue of dimension_range, needed because a single request here can carry more than one named effect), discarded_cells (merge-cells only — the non-top-left, non-blank cells a merge discarded, as "A1: value" strings, so the one formatting request that destroys data has a record saying exactly what was lost), validation_type (the condition type a set-data-validation applied, or "cleared"), protected_range_id (the stable numeric id of a protected range a protection verb acted on — server-assigned for protect-range, otherwise the one resolved against), and protection_editors_added/ protection_editors_removed (comma-separated).

    Text writes through the Docs API (issue #1615, ADR-0076) use this same kind, with operation of docs-replace/docs-append, and add three more omit-if-absent context keys: occurrences_changed (what the server reported changing, which can differ from the client-side --dry-run estimate), inserted_chars, and required_revision_id (the revision lease presented, recorded so a stale-revision refusal is as auditable as a success — an opaque, short-lived id, not a secret).

    The searched, replacement and appended text are never recorded. They are user prose, and often the most sensitive thing in the invocation — sharper than the Sheets case, where an A1 range is metadata rather than content. Only counts and the opaque revision id go in.

  • kind: "audit" — written to the separate audit.jsonl sink, never to this file — see Audit log. Every other kind above stays exactly as described regardless of the audit log's existence: a leased Drive write (issue #1664, ADR-0080) still produces its normal drivemutation record here, correlated to its audit record by the shared invocation_id.

Every HTTP, gh, worktree, and drivemutation record shares an invocation_id with the invocation that issued it, so you can pull a run and all of its requests with a single --id.

Coverage caveat: omni-dev's own in-process HTTP clients and its Rust gh calls (daemon and one-shot CLI) are recorded. The one gap left is the VS Code companion extension's gh pr list, which runs in a separate Node process and does not write to this log.

Location

Resolved in this order:

  1. OMNI_DEV_LOG_FILE if set.
  2. Otherwise dirs::state_dir() joined with omni-dev/log.jsonl — on Linux, ~/.local/state/omni-dev/log.jsonl.
  3. On platforms without a state dir (macOS), it falls back to the data dir, matching the daemon's convention: ~/Library/Application Support/omni-dev/log.jsonl.

The directory is created 0700 and the file 0600, the same posture as other omni-dev runtime state. The log lives entirely on your machine.

Environment variables

Variable Effect
OMNI_DEV_LOG_FILE Override the log path.
OMNI_DEV_LOG_DISABLE=1 Disable logging entirely.
OMNI_DEV_LOG_BODIES=1 Opt in to recording request/response bodies (off by default; payloads are large and may contain customer content).
OMNI_DEV_LOG_HEADERS=1 Opt in to recording (redacted) request/response headers.
OMNI_DEV_LOG_MAX_SIZE Enable automatic size-capped rotation on write, e.g. 10mb (unix only; see Bounding growth).
OMNI_DEV_LOG_KEEP_FILES Number of rotated files to keep when rotation is enabled (default 3).
OMNI_DEV_AUDIT_LOG_FILE Override the audit log path — see Audit log; none of the other variables above affect it.

Logging is best effort: a write failure is swallowed (logged only at tracing::debug) and can never change the command's exit code. The audit log is the deliberate exception — see Audit log.

omni-dev log

omni-dev log [OPTIONS]          # search (default)
omni-dev log prune [OPTIONS]    # trim the log — see Bounding growth

With no subcommand, omni-dev log searches and prints the log using the filters below. The prune subcommand trims it; see Bounding growth.

Filters

Flag Matches
--since <DUR_OR_TS> Lower time bound: a relative window (30m, 2h, 1d, 1w, 45s), a date (2026-07-01), or an RFC3339 timestamp.
--until <DUR_OR_TS> Upper time bound: same forms as --since (a relative value means that long ago). Pair with --since for a bounded window.
--method <METHOD> HTTP method (case-insensitive).
--status <STATUS> Exact (200), class (5xx), or comma list (4xx,5xx).
--service <NAME> jira, confluence, datadog, browser-bridge, snowflake, transcript, anthropic, bedrock, openai, ollama, claude-cli.
--command <PATH> Resolved command-path prefix on whole segments, e.g. "jira read".
--url <SUBSTR> Substring of the request URL.
--grep <REGEX> Regular expression against the raw JSON line.
--fuzzy <TOKEN> Substring of the raw line; repeatable, AND-ed.
--query <EXPR> Query expression (see below); repeatable, AND-ed.
--id <ID> This record id or invocation_id — pulls a run and its requests.

Output

Flag Effect
-o, --output <oneline|json|full> oneline (default), json (NDJSON, byte-identical to the file — composes with jq), or full (pretty block).
-n, --limit <N> Show at most the N most recent matching records.
-f, --follow Tail the log, printing new matching records as they arrive.
--audit Read audit.jsonl instead of log.jsonl — see Audit log. Every filter, the --query mini-language, and all three output formats apply unchanged; only the file being read differs.

The --query mini-language

  • Structured terms: field:valuekind, source, service, method, status (supports 5xx), command, url, id, invocation_id (alias inv), mcp_tool (alias tool), via_daemon, error, plus the fields that were previously reachable only through --grep: exit_code (alias exit), duration_ms (aliases duration, dur), elapsed_ms (alias elapsed), hostname (alias host), system_user (alias user), cwd, and auth_principal (alias principal). Field matching is shared with the flags, so --status 5xx and status:5xx behave identically.
  • status is kind-aware: for every kind but drivemutation it matches the HTTP status_code (class or comparator syntax, below). A drivemutation record has no status_code — its domain status (blocked, written, stale-revision, ...) lives in the context map instead, so status: there matches context["status"] by exact case-insensitive equality, e.g. kind:drivemutation status:blocked.
  • Numeric comparisons: numeric fields (exit_code, duration_ms, elapsed_ms, and status) accept a leading comparator — >, >=, <, <=, or bare =/N for equality — so slow requests and failed runs are expressible directly (elapsed:>1000, exit_code:>0). status still also accepts its class syntax (5xx). Comparator/class syntax applies only to the HTTP status_code path, not the drivemutation case above.
  • Text fields (url, hostname, system_user, cwd, auth_principal) match a case-insensitive substring.
  • Context fields: any other field name falls back to the record's free-form context map (case-insensitive substring), so worktree recovery fields query directly: branch:issue-1392, path:demo-wt.
  • Bare tokens are fuzzy substring matches against the raw JSON line.
  • Operators: AND (also implicit between adjacent terms), OR, NOT (or a leading -), and parentheses. Use "quotes" for a value containing spaces.
omni-dev log --query 'kind:http AND (status:5xx OR method:POST)'
omni-dev log --query 'service:jira -status:2xx'        # jira requests that did not 2xx
omni-dev log --query 'elapsed:>1000'                   # requests slower than 1s
omni-dev log --query 'kind:invocation exit_code:>0'    # failed runs

Examples

# The last 20 things you ran.
omni-dev log -n 20

# Server errors in the last two hours.
omni-dev log --since 2h --status 5xx

# Slow datadog requests (native — no jq needed).
omni-dev log --service datadog --query 'elapsed:>1000'

# A bounded historical window.
omni-dev log --since 2026-07-01 --until 2026-07-02

# A run and every request it made.
omni-dev log --id 0001718000000-0a1b2c3d4e5f6071

# Compose with jq for anything not directly expressible.
omni-dev log -o json --service datadog | jq 'select(.status_code == 429)'

# Follow live.
omni-dev log -f --service browser-bridge

Recovering a removed worktree

omni-dev git worktree remove records the worktree's branch, HEAD commit, and dirtiness before the removal, so an accidental remove is recoverable minutes later from the log alone:

# What worktree did I just remove?
omni-dev log --command 'git worktree remove' --since 1d -o full

# Or by any recovery field directly.
omni-dev log --service worktree --since 30m
omni-dev log --query 'branch:issue-1392'

The full record's context carries path, branch, commit, had_uncommitted, and used_force — enough to re-create the branch (git branch <branch> <commit>) and re-attach a worktree (omni-dev git worktree add <path> <branch>). prune records carry the same triple per pruned entry in a JSON pruned list.

Counting records

omni-dev log count aggregates the log by record kind and source over an optional window — a quick tally without paging through matches:

omni-dev log count [--since <DUR_OR_TS>] [--until <DUR_OR_TS>] [--source <SOURCE>] [--kind <KIND>] [--json]
Flag Effect
--since <DUR_OR_TS> Lower time bound — same forms as omni-dev log --since (1h, 2026-07-01, RFC3339).
--until <DUR_OR_TS> Upper time bound (a relative value means that long ago).
--source <SOURCE> Restrict to cli, daemon, or mcp.
--kind <KIND> Restrict to invocation, http, gh, worktree, or drivemutation. gh unlocks the GitHub breakdown (below).
--json Emit JSON (string map keys, composes with jq) instead of the table.

With no --kind it reports the total and the split by kind and source:

# What has this machine done in the last day?
omni-dev log count --since 1d

Counting GitHub API calls

Because every GitHub call is recorded as a kind: "gh" line (see What gets recorded), --kind gh turns the log into a ground-truth count of the GitHub API calls omni-dev itself makes — the local counterpart to gh api rate_limit, which is GitHub's server-side view of everything the token spent. It renders a richer breakdown by categoryapi (gh api …), subcommand (gh pr list, gh repo view, …), and local (gh --version, excluded from the API total) — as well as by subcommand and by source:

# All GitHub API calls in the last hour.
omni-dev log count --kind gh --since 1h

# Just what the daemon's pollers spent today, as JSON.
omni-dev log count --kind gh --since 1d --source daemon --json

# The raw records (composes with jq / anything log can express).
omni-dev log --query 'kind:gh'

The counts share the request log's storage, so they honor the same window and are subject to prune/rotation. The daemon also logs a summary of these counters to its own log (daemon.log / the journal) ~5 seconds after startup, every 10 minutes, and once on shutdown — a periodic, restart-delimited footprint without anyone running a command; daemon status surfaces the current count too.

Not yet counted: the VS Code companion extension's gh pr list runs in a separate process and does not write to the log (a planned follow-up).

Bounding growth

The log is default-on for every invocation and every outbound request, so on an active machine it grows steadily. Two opt-in bounds keep it in check; both are off by default, so nothing changes unless you ask for it.

omni-dev log prune

Trims the log in place, by age and/or by size:

omni-dev log prune [--older-than <DUR>] [--max-size <SIZE>] [--dry-run]
Flag Effect
--older-than <DUR> Remove records older than a relative window (7d, 24h, 2w, 45m).
--max-size <SIZE> After age pruning, drop the oldest records until the file is at most <SIZE> (10mb, 512kb, or a bare byte count).
--dry-run Report what would be removed without modifying the file.

At least one of --older-than / --max-size is required. Sizes are binary (kb/mb/gb = 1024-based). Records with a missing or unparseable timestamp are kept (age pruning only removes records it can positively date as old), and --max-size always keeps at least the single most recent record.

# Keep the last 30 days; preview first.
omni-dev log prune --older-than 30d --dry-run
omni-dev log prune --older-than 30d

# Cap the file at 20 MB, dropping the oldest records to fit.
omni-dev log prune --max-size 20mb

Pruning rewrites the file atomically (a same-directory temp file is renamed over the original, preserving the 0600 mode), so a concurrent reader never sees a half-written file. It is not locked against concurrent writers, though: a record appended during the rewrite may be lost, and — because prune is itself a logged invocation — pruning the active log appends one new record of its own. For exact accounting, prune with OMNI_DEV_LOG_DISABLE=1 or when the log is idle.

Automatic size-capped rotation

Set OMNI_DEV_LOG_MAX_SIZE (e.g. 10mb) to rotate on write: before an append that would push the file past the cap, log.jsonl is renamed to log.jsonl.1 (shifting any existing log.jsonl.1.2, and so on) and a fresh log.jsonl is started. OMNI_DEV_LOG_KEEP_FILES (default 3) bounds how many rotated files are retained; the oldest beyond that is deleted. Total on-disk use is therefore roughly (OMNI_DEV_LOG_KEEP_FILES + 1) × OMNI_DEV_LOG_MAX_SIZE.

Rotation is unix-only and best effort: when it is enabled, writers serialize on a stable log.jsonl.lock file (created 0600) for the check-rotate-append sequence, and a rotation failure falls back to appending without rotating rather than dropping the record. The env vars are read per write, so they must be present in the environment of whatever writes the log (your shell for CLI runs, or the daemon's environment for daemon-served requests). A set-but-invalid OMNI_DEV_LOG_MAX_SIZE is ignored (logged at tracing::debug) and leaves rotation off. The omni-dev log reader already tolerates truncation and rotation, so -f/--follow keeps working across a rotation (it restarts from the top of the fresh file).

Audit log

audit.jsonl is a separate file, a sibling of log.jsonl under the same runtime directory, reusing the same LogRecord schema and the same omni-dev log reader — pass --audit to read it instead of log.jsonl; see Filters/Output above, all of which apply unchanged. It exists to serve the opposite guarantee from everything above: where log.jsonl is best-effort, prunable, and can be disabled outright, audit.jsonl is fail-closed — an operation whose audit record cannot be written does not happen — and exempt from every growth bound this page just described:

log.jsonl audit.jsonl
On a write failure Swallowed; the command's exit code is unaffected Propagated; the caller aborts the operation it was about to audit
OMNI_DEV_LOG_DISABLE=1 Suppresses all writes No effect
omni-dev log prune Trims by age/size Refuses --audit outright (log prune does not support --audit), and refuses any resolved log path that names the audit file — e.g. OMNI_DEV_LOG_FILE pointed at audit.jsonl directly, via a .. segment, or via a symlink
OMNI_DEV_LOG_MAX_SIZE rotation Applies Never applies, regardless of the setting or how OMNI_DEV_LOG_FILE is spelled
Path override OMNI_DEV_LOG_FILE OMNI_DEV_AUDIT_LOG_FILE
Location when unset <state dir>/omni-dev/log.jsonl <state dir>/omni-dev/audit.jsonl

--audit is a flag on the bare search form (omni-dev log --audit ...) and, separately, on prune (omni-dev log prune --audit, always refused, above); placed before a subcommand name instead — omni-dev log --audit prune ... — it is refused rather than silently ignored, since a subcommand never consults it. record_audit also refuses to write if OMNI_DEV_LOG_FILE and OMNI_DEV_AUDIT_LOG_FILE are configured to resolve to the same file — compared by file identity (the same-file crate — inode on unix, a file handle on Windows — falling back to a normalized path when one side doesn't exist yet), not merely by identical spelling, so a .. segment, a relative-vs-absolute spelling, or a symlink can't slip past the check: the two sinks are siblings by default but not otherwise mutually exclusive by construction, so misconfiguration is caught at write time instead of silently blending the fail-closed sink into the best-effort one. That guard only stops writes into log.jsonl; a colliding OMNI_DEV_LOG_FILE can still cause a best-effort record to land in audit.jsonl (it just can no longer be pruned or rotated away, per the table above) — the next record_audit call still fails loudly, surfacing the misconfiguration.

Every record carries kind: "audit" and, in context, an integration key ("drive" today, so a later integration's audit trail is additive rather than a rename), a file_id, and a verdict — alongside the usual invocation/HTTP fields. RecordKind::Audit is the schema landed by issue #1664 (ADR-0080 §11); what populates it is the Drive leased-write lifecycle, landing across that same issue's phases.

drive lease acquire writes one record per attempt, command: ["drive", "lease-acquire"], regardless of outcome — verdict is acquired, acquired-headless-waiver (ADR-0080 §8/§13, issue #1677: proceeded under the headless opt-out, no human ever prompted), already-leased, refused-native-document, denied, unavailable, or failed, matching AcquireResult's own kebab-case status — plus, on already-leased/failed, the -backup-orphaned suffix (issue #1690): this attempt took a real backup before discovering it isn't referenced by any ledger row, and reclaiming that backup itself then failed, so it is now truly orphaned (drive lease prune cannot see it either). An acquired record additionally carries lease_id (the token), version_after/modified_time_after (the Drive state actually recorded into the ledger — re-read after the backup, not the pre-authentication snapshot, for the same TOCTOU reason the ledger itself does), backup_location (a local path for a byte backup, or the backup copy's own file id for a native-document backup), backup_sha256/backup_size (byte backups only), and auth_policy (device-owner or biometrics-only). An already-leased/failed record also carries backup_location whenever this attempt took a backup, whether or not reclaiming it succeeded — the -backup-orphaned suffix is what distinguishes the two. Unlike a leased write's own record (below), this one is best-effort rather than write-ahead/fail-closed: acquiring mutates no Drive content — by the time the record is written the consent, the backup and the ledger row have already durably happened, so a logging failure is warned and does not turn a successful acquisition into a reported failure.

A leased write produces records in both files, correlated by invocation_id: its ordinary drivemutation record here in log.jsonl (ADR-0070/0071/0073/0075/0076/0077/0078), and, in audit.jsonl, one or two audit records from crate::drive::lease::check — shared by every leased-write engine (drive edit, the five Sheets engines and drive docs write). Their command is ["drive", <operation>] with the verb's operation name — ["drive", "edit"], ["drive", "sheets-delete-sheet"], ["drive", "docs-replace"] — byte-for-byte the command of the same write's drivemutation record, so --query 'command:sheets-delete-sheet' matches both files and the audit record names the verb that ran, not merely which engine ran it:

  • A refusal that never reaches the mutating call writes one best-effort record — verdict: refused-no-lease, refused-lease-expired, refused-lease-wrong-file or refused-lease-stale, mirroring the engine's own reported result, or failed with the error when the ledger lock could not be taken. There is no mutation to pair a write-ahead record around.
  • A presented lease that checks out writes a write-ahead pending intent record carrying lease_id, version_before and modified_time_before, fsynced before the mutating API call this lease authorises, and fail-closed: if this record cannot be written the write itself is refused rather than proceeding unaudited (the one place in the whole ADR this contract applies, per §11). After the mutating call returns, a best-effort outcome record follows with the same lease_id: verdict: allowed with version_after/modified_time_after (from files.update's own response for drive edit, from the post-write files.get the ledger refresh already pays for on Sheets/Docs), or verdict: failed with the API error — including a Docs 412 on writeControl.requiredRevisionId, which the CLI reports as stale-revision: the lease was not stale, the mutation simply did not happen, and the error says why.

An intent record with no matching outcome is itself the signal that something was interrupted mid-write — the whole point of writing the former durably before the latter can even be attempted.

drive lease restore <TOKEN> writes its own top-level, best-effort record, command: ["drive", "lease-restore"], in addition to the generic write-ahead/outcome pair its internal fresh-lease acquisition and restore write already produce (both keyed on the fresh token). This one names both tokens (ADR-0080 §10/§11): lease_id is the fresh token once minted (or the existing one, for already-leased), and restored_from_lease_id is always the backup lease's own token — the <TOKEN> argument — so --query 'restored_from_lease_id:<token>' finds every restore attempt made from one backup regardless of outcome. verdict matches drive lease restore's own reported status: restored, restored-sheet, restored-headless-waiver, restored-sheet-headless-waiver, sheet-already-restored, no-such-backup-token, no-typed-restore-path, backup-too-large-for-simple-upload, refused-no-visible-parents, blocked, already-leased, refused-native-document, denied, unavailable, failed, or fresh-lease-but-write-failed.

Redaction posture

No secret material is ever written, under any code path:

  • Auth headers/tokens are redacted centrally before writing; only a non-secret auth_principal identity is ever kept. Redaction matches both a fixed list of known header names and any name containing auth, token, key, secret, cookie, password, session, signature, or credential (case-insensitive).
  • URL query and fragment parameters whose keys look secret-bearing have their values replaced with REDACTED before writing: keys suffixed token, secret, password, passwd, signature, or api_key/apikey; the exact keys sig, sas, jwt, and auth; and the X-Amz-* / X-Goog-* signed-URL families. Host, path, and parameter keys are preserved, so --url substring filtering keeps working. This matters mostly for the browser bridge, which logs arbitrary operator-supplied target URLs (presigned URLs, ?access_token=…).
  • Request/response bodies are opt-in via OMNI_DEV_LOG_BODIES=1.
  • The OMNI_DEV_* env snapshot redacts any name containing TOKEN, SECRET, KEY, PASSWORD, or PASSWD.
  • Argv in the invocation record is scrubbed before writing, in both --flag value and --flag=value forms: --header values naming a sensitive header are redacted keeping the name (Authorization: REDACTED), inline --body values are redacted (@file references are kept), and any flag whose name has a token/secret/password/passwd/key segment has its value redacted (flags ending in -file/-path carry paths and are exempt). Every argv element is then run through the same URL query/fragment redaction as --url above, so a secret carried in a URL argument (e.g. --url /path?access_token=… or a presigned target) is redacted even though --url is not a secret-bearing flag name; benign argv passes through byte-identical.

What redaction does not cover: prompt bodies

The guarantees above keep secret material out of the log; they do not make prompt content secret. AI prompts carry whatever you asked about — repo diffs, commit messages, JIRA/Confluence data — and that content surfaces on three paths:

  • OMNI_DEV_LOG_BODIES=1 records AI request/response bodies, which are the prompts.
  • RUST_LOG=debug stderr tracing emits the full system/user prompt and full response from every AI backend. Tracing is ephemeral and stderr-only, but if you redirect it to a file or paste it into a bug report, prompt bodies go with it.
  • On a claude-cli subprocess failure, the returned error embeds the child's stdout/stderr verbatim, which can carry prompt-derived content into whatever captures the error (terminal, CI logs, this log's error field).

No API keys or tokens appear on any of these paths — this is a data-sensitivity note, not a credential leak.

Daemon-served requests

Requests executed inside the daemon (the browser bridge and the Snowflake session pool) set via_daemon: true. Although the daemon is a separate process from the CLI that asked it to act, such requests are still stamped with the originating CLI invocation's invocation_id: the thin client threads its id across to the daemon — on the control socket for Snowflake (an origin_invocation_id envelope field) and on the X-Omni-Bridge-Origin header for the bridge — and the daemon scopes it around the request it serves. So omni-dev log --id <cli-invocation> surfaces the via_daemon requests that run triggered, correlating both halves of one logical operation (#1198).

The threaded id is non-secret (it is not a token), and the record's source stays daemon, so a daemon-served request remains distinguishable by via_daemon / source even while its invocation_id points at the caller. The same mechanism correlates a standalone bridge serve (there source stays cli and via_daemon is unset).

Schema and compatibility

Records are read and written through a single forward-compatible struct: every field is #[serde(default)] and every optional field is skip_serializing_if. A newer reader never chokes on an older line, and an older reader never chokes on a newer one — the same forward-rolling contract the daemon wire types use. The record id is time-sortable, so sorting lines by id ≈ sorting by time.