Skip to content

feat(dashboard): continuous call-log export to pluggable destinations (BigQuery first) - #11945

Merged
diegosouzapw merged 91 commits into
diegosouzapw:release/v3.8.51from
dpozimski:feat/log-export-destinations
Aug 30, 2026
Merged

diegosouzapw merged 91 commits into
diegosouzapw:release/v3.8.51from
dpozimski:feat/log-export-destinations

Conversation

@dpozimski

@dpozimski dpozimski commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Logs tab keeps request history in SQLite, bounded by rotation and retention, so anything older than the window is gone. This adds a scheduled export that ships those same records to an external store before that happens.

A destination is a plugin. It declares a Zod config schema, a field descriptor the dashboard renders its form from, which config keys are secret, and a client with test() / prepare() / send(). Adding Datadog or Grafana Loki later means one file under src/lib/logExport/destinations/ and one line in the registry. The runner, the REST layer and the UI never learn what a destination is.

Google BigQuery is the first one. It talks the REST API directly with a self-signed RS256 assertion rather than pulling @google-cloud/bigquery and google-auth-library into the tree, which is about 100 lines of node:crypto and fetch and keeps the dependency count where it was.

Some details worth knowing:

  • The cursor is call_logs.rowid, not timestamp. saveCallLog accepts a caller-supplied timestamp, so a slow request can be written after a faster one that started later, and a timestamp cursor would step over it. A cursor above MAX(rowid) means the table was purged and rowids restarted, so the runner rewinds instead of going permanently blind.
  • The cursor moves only after send() resolves. A failed batch is retried on the next run. The guarantee is at-least-once plus destination-side dedup, not exactly-once, and every row carries the call-log id as its insertId so a resent batch collapses on BigQuery's side.
  • A partial failure comes back as HTTP 200 with a non-empty insertErrors[]. That throws. Treating it as success is how you advance the cursor past rows BigQuery never took.
  • The batch size is a cursor unit, not an HTTP one. send() chunks on row count and serialised bytes, closing at 500 rows or 9 MB, whichever comes first. Row count alone is not enough once payloads ship: 500 rows carrying prompts can be tens of megabytes against insertAll's 10 MB cap.
  • Service-account keys are encrypted with the existing field encryption and never returned by any endpoint. Because encrypt() is a silent passthrough without STORAGE_ENCRYPTION_KEY, storing a destination that carries a credential is refused with a 400 when the key is absent, following the guard the Telegram webhook already uses.

Payloads are a separate, opt-in step. By default the export carries the summary fields the Logs list shows. A destination can set includeBodies to additionally ship what the Logs detail pane shows: the request and response bodies plus the pipeline's route decision, client raw request, normalised OpenAI request, provider-dialect request, provider response and client response. This is prompt content, so it is off by default and chosen per destination. Both the export and the dashboard read through getCallLogById, so the two cannot drift, and payloads arrive already PII-sanitised and secret-redacted with noLog calls carrying none.

The auto-created BigQuery table is day-partitioned on timestamp and clustered on api_key_name, provider, model, status, ordered by how call logs are actually filtered. Partition retention is configurable. Both apply at creation, so an existing table keeps its layout.

New env var: OMNIROUTE_LOG_EXPORT_CRON, default 0 * * * *. Docs in docs/frameworks/LOG-EXPORT.md.

A build failure this PR introduced, and the panic that hid it

Every page under docs/ is compiled by fumadocs-mdx, whose schema requires a title. The new
docs/frameworks/LOG-EXPORT.md shipped without frontmatter, so npm run build failed:

Error: [MDX] invalid frontmatter in /app/docs/frameworks/LOG-EXPORT.md:
- title: Invalid input: expected string, received undefined

Turbopack never prints that message. It panics inside ImportTracer::get_traces while building
the import trace for the issue it is about to report, so the build dies with
internal error: entered unreachable code: there must be a path to a root
(turbopack-core/src/module_graph/mod.rs:746) and the cause is swallowed. Building with the
documented escape hatch --build-arg OMNIROUTE_USE_TURBOPACK=0 surfaces it immediately.

Frontmatter added; the production Docker image now builds and runs. The Turbopack panic itself is
independent of this change and still reproduces on the base tip with the frontmatter fixed in
place, so it is an upstream bug worth its own issue, not something this PR can address.

Related Issues

Validation

  • Change type: DB + UI + other (background job, REST)
  • Focused tests and category gates from the golden path
  • npm run lint
  • Reconciled with the current active release base (release/v3.8.51)
  • Production-code changes include new automated tests in this PR
node --import tsx/esm --test tests/unit/log-export-*.test.ts      62 pass, 0 fail
npx eslint <changed files>                                        clean
npx tsc --noEmit -p tsconfig.json                                 clean for changed files
npm run check:route-validation:t06                                PASS
npm run check:openapi-routes                                      OK (276 paths)
npm run check:openapi-coverage                                    PASS 39.7% (up from 38.8%)
npm run check:openapi-security-tiers                              PASS
npm run check:env-doc-sync                                        PASS
npm run check:docs-sync / docs-counts / doc-links                 PASS
npm run i18n:check-ui-coverage                                    PASS (42 locales)
npm run check:file-size / cycles / error-helper                   OK

Full CI on the current head is green: all four unit shards, Fast Quality Gates, Docs Gates, ESLint, Vitest, Merge integrity and both semgrep jobs.

Two gate failures from the first push are fixed here. check:db-rules requires every src/lib/db/ module to be re-exported from localDb.ts, which logExportDestinations was not; and the locale parity suites require every en.json key to exist in all 42 locales, so sidebar.logExport and sidebar.logExportSubtitle are now translated rather than English-only.

Live validation: the production image, a real provider, a real BigQuery table

Built the production image from the repo Dockerfile (--target runner-web), ran it, pointed it at
a real Cursor API connection and a real BigQuery dataset, and drove the whole path through the
shipped HTTP surface. 21 checks, all green, repeatable on a fresh table name each run:

1.  dashboard login
2.  POST /api/providers  cursor-api connection  -> 201, upstream test valid in 425ms
3.  POST /api/keys                              -> 201
4.  POST /api/log-export/destinations           -> 201
    response body contains no private key; the stored config column is enc:v1:
5.  POST /destinations/{id}/test                -> ok, authenticated as the service account
6.  GET  /api/log-export/status                 -> cron "0 * * * *", UTC
7.  3 live chat completions through /v1/chat/completions on cua/auto
    answers came back "alpha" / "bravo" / "charlie"
8.  GET /api/usage/call-logs                    -> 16 rows
9.  POST /destinations/{id}/run                 -> exported=16 batches=1
10. BigQuery holds 16 rows, 0 duplicate ids, every dashboard id present
11. rerun with no new calls                     -> exported=0, BigQuery still 16
12. one more call, rerun                        -> exported=1, BigQuery 17, still 0 duplicates
13. auto-created table has all 47 columns, day-partitioned and clustered
14. field-for-field match against the Logs tab: 17 rows compared, 0 mismatches

A sample exported row, showing the null and boolean handling that matters:

{"id":"1787997867123-e1ba1e","model":"auto","requested_model":"cursor-api/auto",
 "provider":"cursor-api","status":200,"tokens_in":56,"tokens_out":25,
 "tokens_cache_read":null,"model_pinned":false,"api_key_name":"log-export-e2e",
 "error_summary":null,"duration_ms":2743}

Separately, to prove the schedule fires rather than merely being registered, a second container ran
with OMNIROUTE_LOG_EXPORT_CRON="* * * * *". One live call, then nothing was triggered by hand:
the job ran on its own, job_runs recorded status=success, and the rows landed in BigQuery.

Two defects from earlier runs against the real API are fixed here: a table created moments before is
not yet visible to the streaming endpoint and answers 404 for a few seconds, so that 404 is retried
but only when the run itself created the table; and the retry's sleep used an unref'd timer, which
let the process exit out from under an in-flight retry.

Tests Added Or Updated

  • tests/unit/log-export-runner.test.ts (new) cursor advance, batching, retry after failure, max_rows_per_run, purge rewind, concurrent-run guard, migration 170 upgrade path, and for payloads: nothing ships with includeBodies off (asserted against the wire, not the record), both client and provider views ship when on, oversized payloads truncate and flag, a missing artifact exports its summary instead of failing the batch
  • tests/unit/log-export-bigquery.test.ts (new) field mapping vs table schema, insertId dedup, insertErrors on HTTP 200, chunking at 500, transient retry, terminal 403, fresh-table 404, token cache, clustering and partition retention on the created table, byte-aware chunking under multi-megabyte payloads, and a single oversized row still shipping rather than wedging the cursor
  • tests/unit/log-export-routes.test.ts (new) CRUD, validation, 404s, secret redaction, placeholder round trip, encryption-key guard
  • tests/unit/log-export-secrets.test.ts (new) encrypt, decrypt, redact, merge
  • tests/unit/log-export-extensibility.test.ts (new) a fake destination drives the whole runner with no BigQuery code loaded, plus a grep assertion that no destination-specific vocabulary leaked into the 17 generic files
  • tests/unit/sidebar-visibility.test.ts updated for the new sidebar entry

Coverage Notes

Everything under src/lib/logExport/, src/lib/db/logExportDestinations.ts, src/lib/usage/callLogExportSource.ts, src/lib/jobs/logExportJob.ts and src/app/api/log-export/ is covered by the five new suites, including the failure paths. The dashboard components under src/app/(dashboard)/dashboard/log-export/ have no unit tests and follow the existing dashboard pages in that respect.

Reviewer Notes

  • Migration 170_log_export_destinations.sql adds one table and one index. Nothing else is touched, and the upgrade path is covered by a test that drops the table and its ledger row and reopens the database. This takes the repo to 167 migrations, so the three docs that hardcode the count and the 42 llm.txt mirrors are updated.
  • The job is registered in initCloudSync, so OMNIROUTE_DISABLE_BACKGROUND_SERVICES and the automated-test guard already switch it off. A fresh install has no destinations and exports nothing.
  • Nothing runs until an operator configures a destination and enables it.
  • Payloads are opt-in per destination (includeBodies, off by default). When on, the export additionally carries the request and response bodies plus the pipeline's client and provider views, read through getCallLogById so it cannot drift from the Logs detail pane. They arrive already PII-sanitised and secret-redacted, and a call made with a noLog API key stores none, so there is nothing to export. maxBodyBytes truncates rather than drops and flags the row. Streamed chunk deltas are not exported.
  • The registry has two test-only exports (__registerLogExportDestinationTypeForTest / __reset...), matching the pattern already used by __resetJobRegistry. They exist so the extensibility test can prove the pipeline works with no BigQuery code loaded. Happy to drop them if you would rather keep the registry closed.
  • POST /destinations/{id}/run returns skipped: true rather than a 409 when a run for that destination is already in flight. Easy to change if you prefer the status code.

Ships the call logs behind the Logs tab to an external analytics store on an
hourly schedule. A destination is a plugin: it declares a Zod config schema, a
UI field descriptor, which keys are secret, and a client that can test, prepare
and send. Adding Datadog or Grafana Loki is one file plus one registry line, so
the runner, the REST layer and the dashboard form stay destination-agnostic.

Google BigQuery is the first destination. It talks the REST API directly with a
self-signed RS256 assertion rather than pulling in a Google SDK, chunks a batch
into insertAll calls of at most 500 rows, retries transient statuses with
backoff, and keys every row by the call-log id so a retried chunk is
de-duplicated instead of double-written. A partial failure arrives as HTTP 200
with insertErrors, which is treated as a failure so the cursor cannot advance
past rows BigQuery never accepted.

The cursor is call_logs.rowid, not timestamp: saveCallLog accepts a
caller-supplied timestamp, so a slow request can be written after a faster one
that started later and a timestamp cursor would skip it. A cursor above
MAX(rowid) means the table was purged and rowids restarted, so the runner
rewinds rather than going permanently blind.

Service-account keys are encrypted at rest and never returned by any endpoint.
Because encrypt() is a silent passthrough without STORAGE_ENCRYPTION_KEY,
storing a destination that carries a credential is refused when the key is
absent instead of writing it to SQLite in plaintext.
Found while exporting into a real BigQuery dataset end to end.

A table created moments earlier is not yet visible to the streaming endpoint,
which answers 404 for a few seconds. That 404 is now retried, but only when the
run itself created the table, so a genuinely missing table still fails fast.

The retry's sleep used an unref'd timer, which let the process exit out from
under an in-flight retry when nothing else held the event loop open.

Also adds the extensibility proof the plugin contract was missing: a fake
destination registered through the registry drives the runner, the secret
handling and the presenter with no BigQuery code loaded, and a companion test
asserts no destination-specific vocabulary has leaked into the generic layers.
That test caught two real leaks: describeLogExportDestinationTypes() read the
static array instead of the live registry, so a registered destination was
invisible to the dashboard, and the config form carried a BigQuery-specific
placeholder.

Migration 170 takes the repo to 167 migrations; the three docs that hardcode
that count and the 42 llm.txt mirrors are updated to match.
dpozimski and others added 4 commits August 29, 2026 04:04
Bring inherited Fast Quality Gates / ESLint / unit-test / docs-sync fixes from diegosouzapw#11940/diegosouzapw#11955/diegosouzapw#11975.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
… db module

The db-rules gate requires every src/lib/db module to be re-exported from
src/lib/localDb.ts, and the locale parity tests require every en.json key to
exist in all 42 locales.

- localDb.ts re-exports ./db/logExportDestinations
- sidebar.logExport / sidebar.logExportSubtitle translated into all 42 locales
…uires

Every page under docs/ is compiled by fumadocs-mdx, whose schema requires a
title. Without it the production build fails with "invalid frontmatter ...
title: expected string, received undefined".

Turbopack hides this: it panics inside ImportTracer::get_traces while
formatting the import trace for the error, so the build dies with "internal
error: entered unreachable code" and never prints the cause. Building with
OMNIROUTE_USE_TURBOPACK=0 shows the real message.
@dpozimski
dpozimski marked this pull request as draft August 29, 2026 10:07
@dpozimski

Copy link
Copy Markdown
Contributor Author

All gates are green except Build (advisory), which fails exit 143 (SIGTERM at 13 minutes) with no compile output.

That job is fork-PR-only and continue-on-error: true, and quality.yml already documents the condition in its own comments: build.yml went workflow_dispatch-only on 2026-08-29 because "the hosted runner cannot build this tree in any profile, 8/8 recent fork PRs included", and 6/6 sampled failures were shutdown signals with "zero OOM, zero build errors". So it is the runner, not this branch.

I did not want to leave the production build unverified on that basis, so I built and ran it locally instead:

docker build --target runner-web --build-arg OMNIROUTE_USE_TURBOPACK=0 -t omni-local .
docker run -d --env-file env -p 20128:20128 omni-local

The image builds, boots, and serves. Against it: a real cursor-api connection, three live completions on cua/auto, 16 call-log rows exported to a real BigQuery table with no duplicates, a rerun exporting nothing, an incremental run picking up exactly one, and a 37-column day-partitioned table matching the Logs tab field for field. A second container with OMNIROUTE_LOG_EXPORT_CRON="* * * * *" confirmed the schedule fires on its own.

Building with OMNIROUTE_USE_TURBOPACK=0 is also what exposed the frontmatter bug fixed in this branch. Turbopack panics inside ImportTracer::get_traces while formatting the import trace for an issue, so the underlying message never reaches the log. Worth knowing when a build here dies with internal error: entered unreachable code and nothing else.

diegosouzapw and others added 11 commits August 29, 2026 08:10
…t createTask flow (diegosouzapw#11296) (diegosouzapw#11985)

flux/kontext is catalogued with isMarket: true, so handleKieImageGeneration
routed it through KIE's unified Market createTask endpoint with
model: "flux/kontext". KIE does not expose Flux Kontext through the Market
catalog at all -- it lives under a dedicated API tree
(POST /api/v1/flux/kontext/generate, poll GET /api/v1/flux/kontext/record-info,
models flux-kontext-pro/flux-kontext-max) -- so the Market endpoint rejected it
with "model name not supported", matching the reporter's exact error text.

Special-case flux/kontext ahead of the isMarket branch so it hits the
dedicated endpoint/payload shape instead of being treated as a Market entry.
z-image/4.0-*/4.5-* remains intentionally untouched (still blocked on
reporter/live confirmation per the existing in-code comment).

Co-authored-by: Markus Hartung <mail@hartmark.se>
…outs (diegosouzapw#11500) (diegosouzapw#11989)

* fix(db): rate-limit Arena ELO fetch-failure warnings on repeated timeouts (diegosouzapw#11500)

* test(quality): split the diegosouzapw#11500 fetch-failure-dedup tests into their own file

tests/unit/arena-elo-sync.test.ts crossed the 1000-line new-test-file cap
(file-size gate, PR mode). The two new tests don't need the file's DB
fixture (fetchArenaLeaderboards() never touches the DB), so they move to a
self-contained sibling file instead of growing the frozen suite.

---------

Co-authored-by: Markus Hartung <mail@hartmark.se>
…te path (diegosouzapw#11707) (diegosouzapw#11984)

enforceCodexResponsesLiteParallelToolCalls() forces parallel_tool_calls:false
at the top of CodexExecutor.execute(), but transformRequest() early-returns
the body before its RESPONSES_API_ALLOWLIST field filter only when
_nativeCodexPassthrough is set. Any request that reaches the codex
executor via the translated (non-native-passthrough) path never gets that
flag, so the allowlist filter silently deleted parallel_tool_calls right
before the fetch body was sent, reproducing the reported upstream
rejection ('X-OpenAI-Internal-Codex-Responses-Lite requires
parallel_tool_calls to be false') for every model.

Add parallel_tool_calls to RESPONSES_API_ALLOWLIST so the value survives
the translated path too. Update the sibling diegosouzapw#2608 allowlist test that
previously asserted parallel_tool_calls gets stripped like other Chat
Completions-only fields -- it is a legitimate Responses API field that
must now survive.

Co-authored-by: Markus Hartung <mail@hartmark.se>
…publish's opencode-plugin skip check (diegosouzapw#11787) (diegosouzapw#11990)

* fix(cli): drop the never-produced dist/index.cjs requirement from prepublish's opencode-plugin skip check (diegosouzapw#11787)

* test(build): resolve tsup/npm portably in the diegosouzapw#11787 regression test instead of a hardcoded .bin path

The old test assumed @omniroute/opencode-plugin/node_modules/.bin/tsup
already existed. A fresh checkout (CI's npm ci never installs this
standalone package's own deps) has no such node_modules at all, so the
test failed with MODULE_NOT_FOUND in CI while passing locally on a devbox
that had installed it before. Mirror scripts/build/prepublish.ts's own
install-then-resolveLocalBinEntry approach.

---------

Co-authored-by: Markus Hartung <mail@hartmark.se>
…ked rows (diegosouzapw#9133) (diegosouzapw#11994)

* fix(sse): stop the auto-combo candidates inspector from dropping blocked rows (diegosouzapw#9133)

prepareVirtualAutoComboInputs applied filterResilienceBlockedCandidates
before the diegosouzapw#7819 read-only candidate inspector ever saw the pool, so a
model-locked or cooled-down candidate silently disappeared from
/auto-combo/*/candidates instead of showing up as reachable:false with a
reason (modelLocked/connectionCooldown/breakerState were dead fields by
construction). Add an opt-in `skip` parameter so the inspector builds its
own unfiltered pool; routing (createVirtualAutoCombo/createBuiltinAutoCombo
called without a prepared override) is unchanged. Also aligns
isModelLocked's model argument to the bare model id, matching every lock
writer and the routing-side filter, instead of the "provider/model" string.

Regression test: tests/unit/auto-combo-candidates-locked-model-visible.test.ts
(red before the fix — locked account's row silently missing; green after).

* chore(quality): register the diegosouzapw#9133 regression test in stryker tap.testFiles

tests/unit/auto-combo-candidates-locked-model-visible.test.ts covers
open-sse/services/accountFallback.ts (via isModelLocked) but wasn't listed,
so its mutant kills wouldn't count toward mutation coverage.

---------

Co-authored-by: Markus Hartung <mail@hartmark.se>
…SBOM on dispatch (twin of diegosouzapw#11982 + diegosouzapw#12020) (diegosouzapw#12022)

* fix(release): resync the electron lockfile, build a dispatch from a repaired ref, keep curated notes, attach the SBOM on dispatch (release/v3.8.51 twin of diegosouzapw#11982 + diegosouzapw#12020)

Same four changes as diegosouzapw#11982 and diegosouzapw#12020 on main, applied to this branch's own copies:

- electron/package-lock.json regenerated (271 -> 284 entries): the optional
  electron-builder-squirrel-windows subtree was missing and `npm ci` refused the lock
  (EUSAGE) on the Linux and macOS legs; a clean `npm ci --ignore-scripts` on the
  result exits 0.
- electron-release.yml: `build_ref` dispatch input (default: the version tag) and
  `generate_release_notes` only on the tag push (a re-attach dispatch appended
  GitHub's auto notes to the curated body on v3.8.50).
- npm-publish.yml: the SBOM attaches to the GitHub Release on workflow_dispatch
  publishes too, whenever a release for the tag exists.

actionlint and prettier clean; electron-release-desktop-channel-8949,
electron-release-efficiency, electron-release-latest-yml.repro, check-workflows
and npm-publish-artifact-provenance suites pass.

* fix(release): validate build_ref in the validate job before any checkout uses it

CodeQL (actions/cache-poisoning/poisonable-step, high) on release/v3.8.51 — the
default branch: a raw dispatch input checked out next to setup-node's npm cache is a
cache-poisoning vector. The input now goes through the validate job's regex
allowlist (main or release/vX.Y.Z, empty = the version tag) and every build job
checks out needs.validate.outputs.build_ref, never the input itself.

* fix(release): drop the build_ref input — a dispatch builds the ref it is dispatched on

CodeQL (actions/cache-poisoning/poisonable-step) tracks the input through the
validate job's output regardless of the regex allowlist: an input-controlled
checkout next to setup-node's npm cache on the default branch is a cache-poisoning
vector. The ref is not an input any more; the checkouts use github.ref, so
`gh workflow run electron-release.yml --ref v3.8.50 -f version=v3.8.50` rebuilds
the tag and `--ref main` builds the repaired line. The tag-push path is unchanged.
…iegosouzapw#12021)

* fix(ci): stop hosted docker-publish OOM and unpaint Build (advisory)

docker-publish was firing 8 concurrent hosted builds on every merge
storm; each died ResourceExhausted in npm run build (diegosouzapw#11976). One
publish per ref, webpack instead of Turbopack so native RSS stays
inside the V8 heap we can cap. Build (advisory) is skipped: continue-on-error
still reports FAILURE and was painting every fork PR red.

Closes diegosouzapw#11976

* fix(ci): run docker-publish amd64 on omni-build and share the heavy lane

The .113 box is 31 GB / 32 cores — enough for one next-build. Hosted
ubuntu-24.04 is ~7 GB and ResourceExhausted every publish (diegosouzapw#11976).
amd64 now targets [self-hosted, omni-build] (Turbopack) when
USE_VPS_RUNNER is on, joins the existing heavy-build-main group so it
queues beside ci.yml Build instead of becoming a third heavy, and
falls back to hosted + webpack if the VPS is off. arm64 stays on
ubuntu-24.04-arm with webpack (no ARM box).

* test(ci): align the advisory-build contract with the hosted-OOM skip

if: ${{ false }} tripped zizmor obfuscation (194→195). Bare if: false
skips the job without a new finding. The diegosouzapw#7307 test now pins the skip
and keeps the job body as the restore recipe.
…out for querying

Two additions to the log export.

Payloads. The export carried only the summary fields the Logs list shows. A
destination can now opt into `includeBodies`, which additionally ships what the
Logs detail pane shows: the request and response bodies plus the pipeline's
route decision, client raw request, normalised OpenAI request, provider-dialect
request, provider response and client response.

This is prompt content, so it is off by default and set per destination. What
ships is what the dashboard shows, because both read through `getCallLogById`:
payloads are already PII-sanitised and secret-redacted when written, and a call
made with a `noLog` API key stores none, so there is nothing to export. Rows
whose artifact is missing export their summary rather than failing the batch.
`maxBodyBytes` caps each field and truncates rather than dropping, flagging the
row with `bodies_truncated`.

insertAll chunking now closes on bytes as well as row count. 500 rows of
summaries is small; 500 rows carrying prompts can be tens of megabytes against
a 10 MB request cap.

Table layout. The auto-created table was day-partitioned but unclustered.
It now clusters on api_key_name, provider, model, status, ordered by how call
logs are actually filtered, and accepts an optional partition retention.
@dpozimski

Copy link
Copy Markdown
Contributor Author

Live run: dashboard and BigQuery evidence

Everything below came from the production image built from this branch's Dockerfile, running
against a real Cursor connection and a real BigQuery dataset. The config path was driven through
the browser, not the API.

The destinations page

Hourly schedule, the call-log high-water mark, and per-destination state with Test / Run now /
Disable / Edit / Delete.

Log export destinations

ui-configured-prompts in that list was created by filling this form in the browser, then tested
and run from the same page: Test returned Authenticated as …iam.gserviceaccount.com. Table call_logs_from_ui will be created on the first export., and Run now reported
exported 9 rows · 0 pending · cursor 9.

The config form

Scrolled to the payload controls. The toggle is unchecked by default; ticking it reveals the
guidance and the per-field byte cap.

Add export destination form

Reading the destination back through the API after creating it in the UI:

{"includeBodies": true, "maxBodyBytes": 262144, "enabled": true,
 "table": "…omniroute_live_e2e.call_logs_from_ui", "retentionDays": 30,
 "credential": "__stored__"}

The exported data

omniroute-call-logs-sample.csv
is 9 real rows pulled back out of BigQuery with SELECT *, all 47 columns. One row, abridged:

requested_model : cursor-api/auto
status          : 200      tokens_in 62   tokens_out 20    duration_ms 2750
request_body    : {"model":"cursor-api/auto","messages":[{"role":"user",
                   "content":"Reply with exactly this word: capybara-1788010347897"}],"max_tokens":30}
response_body   : {"choices":[{"message":{"content":"capybara-1788010347897"}}], …}
pipeline_client_request   : 1153 bytes
pipeline_provider_request :  796 bytes  {"url":"https://agentn.global.api5.cursor.sh/agent.…
pipeline_provider_response:  562 bytes
pipeline_client_response  :  439 bytes
bodies_truncated: false

Opening that same call id in the Logs detail pane returns the identical prompt and the same four
pipeline stages, which is the parity this feature is meant to hold.

Payload presence by API key in that table:

(none)          rows= 4   without payload=4     internal calls, no payload captured
bodies-nolog    rows= 2   without payload=2     noLog key: summary exports, payloads do not
bodies-normal   rows= 2   without payload=0
debug-key       rows= 1   without payload=0

The noLog row is the one worth checking: it still exports its summary, and carries no prompt.

Table layout, read back from BigQuery

timePartitioning : {"type":"DAY","field":"timestamp","expirationMs":"7776000000"}
clustering       : ["api_key_name","provider","model","status"]
schema           : 47 columns

One dependency to be aware of

The four pipeline_* columns only populate when pipeline capture is on
(call_log_pipeline_enabled, off by default). My first live run failed exactly those four
assertions and passed everything else: the export was correct, the data simply had not been
captured. Both states are visible in the CSV, since the two oldest rows predate enabling it.
request_body and response_body do not depend on that setting.

Safety

The CSV was scanned for service-account keys, bearer tokens, JWTs, provider keys and unredacted
auth headers before publishing. authorization reads [REDACTED] in the captured headers, which
is the existing payload protection applied at write time rather than anything the exporter does.

Assets live on a separate evidence/log-export-11945 branch of the fork and are not part of this
diff.

@dpozimski
dpozimski marked this pull request as ready for review August 29, 2026 13:45
dpozimski and others added 4 commits August 29, 2026 15:48
…t a driver line the primary path never prints (twin of diegosouzapw#12032) (diegosouzapw#12047)

* fix(release): the packaged-app smoke verifies the database opened, not a driver line the primary path never prints (release/v3.8.51 twin of diegosouzapw#12032)

Same change as diegosouzapw#12032 on main: the packaged app opens SQLite during the smoke but
its primary open path prints no "[DB] Driver: …" line (only the recovery path and
the sql.js fallback do), so the diegosouzapw#7592 assertion failed every Linux release leg. The
guard rejects the sql.js fallback line, accepts a native driver line, and otherwise
accepts demonstrable database activity; after readiness the smoke requests
/api/monitoring/health and waits for that activity outside the readiness loop.
electron-smoke-script suite 10/10.

* fix(release): reapply the smoke rework on top of release/v3.8.51's own copy of the script

The previous commit copied main's file wholesale and dropped this branch's
ensureSmokeEnvDirs(currentPlatform) fix and its tests; this reapplies only the
DB-open evidence change as a patch. electron-smoke-script suite green.
…souzapw#12048)

* docs(ops): the .113 heavy-build ceiling is one runner, not two

Two concurrent next-builds (15.4 GB + 17.2 GB RSS) OOM-killed one on 2026-08-29 17:26 UTC;
systemd booked the kill on the other runner's unit and its job died with the same
"shutdown signal" text a hosted-runner OOM shows. omni-build now lives on
omniroute-113-5 only; 113-6 keeps omni-release. The janitor ceiling counts every
listener on the box (4 OmniRoute + OmniHeuris + OmniMind = 6). The second heavy slot
returns when the Proxmox VM gets more RAM; the exact command is in the doc.

* docs(ops): apply the single-heavy-slot text (previous commit only carried formatting)
…12050)

Turbopack had 31 GB on omniroute-113-6 and still panicked
(TurbopackInternalError: there must be a path to a root, run
33253576569). The same tree's arm64 webpack build on hosted ARM
succeeded. Dockerfile already documents webpack as the Docker
escape hatch. Keep amd64 on the one omni-build slot (diegosouzapw#12048).
@dpozimski

Copy link
Copy Markdown
Contributor Author

@diegosouzapw hi, pls code review. in summary i propose to have exporters to some external datasets (1st implementation - bigquery)

diegosouzapw#11600) (diegosouzapw#11996)

Fixes the blocking Lint job's own ci.yml cache: PR diegosouzapw#11963 removed the stale restore-keys fallback from quality.yml but left ci.yml's two "Restore ESLint file cache" steps carrying the same prefix-match fallback that lets a cache from a different lint config report stale per-file verdicts. Byte-level parity with diegosouzapw#11963's already-merged fix.

Deliberately half of diegosouzapw#11600 — the other half (run-eslint-json.mjs) is covered by PR diegosouzapw#11983 from a parallel session, so the two don't collide on the same file.
… list (diegosouzapw#11949) (diegosouzapw#11995)

Local no-API-key providers (ollama-local, lm-studio, vllm, etc.) were invisible in the Qdrant embedding-model dropdown because configuredProviders required a real apiKey or OAuth. Extended the filter to also include providerAllowsOptionalApiKey(connection.provider) — the same canonical helper already used for the identical check elsewhere. TDD: 21/21 integration tests pass (was 20/21 before the fix).
…gosouzapw#11861) (diegosouzapw#11993)

Fixes a copy-paste label typo (Hermes-4-405B mislabeled "7B") in both the registry and the free-model catalog data, spotted in the diegosouzapw#11861 comment thread. TDD: 3/3 tests, generic parameter-size consistency check + exact regression guard.
diegosouzapw and others added 27 commits August 30, 2026 05:25
… to the curated body (diegosouzapw#12096)

Phase 3 creates the GitHub Release with the curated notes right after the tag push,
so softprops always finds an existing body; generate_release_notes must be false
on every event, not only on workflow_dispatch (v3.8.48 shipped with the auto block
appended; the body sits ~3 KB under the 125,000-char cap). Same hunk as main (diegosouzapw#12086);
the diegosouzapw#12085 squash did not carry it.

Refs diegosouzapw#12084
…egosouzapw#11937)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Trivial, correct log-level fix — both conditions are already handled gracefully by callers. Thanks.
…iegosouzapw#11936)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Verified the console fallback → null fix removes the duplicate plain-text line while the structured pino log is unaffected. Thanks.
diegosouzapw#11923)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green. Retargeted from main to release/v3.8.51. One-line baseUrl fix with a live endpoint probe documenting the exact 404→401 transition — solid verification. Thanks.
…unks (diegosouzapw#11921)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green. Retargeted from main to release/v3.8.51. Confirmed ollamaTransform.ts was the only streaming transform not using a persistent { stream: true } decoder — matches the pattern already established in responsesTransformer.ts. TDD repro included. Thanks for finding an unreported bug.
…gosouzapw#11910)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Reviewed the security fencing closely — the status query fences on lease_owner_hash + api_key_id + generation + state=ACTIVE + not-expired, gated behind the existing lease:exclusive scope check. configuredConnectionName() correctly excludes email-derived fallback labels from the response. Test coverage explicitly verifies foreign key / different owner / stale generation all fail closed with 409, and no metadata leaks for released/expired/invalidated/missing leases. Thanks for the careful privacy-safe design.
…directory (diegosouzapw#11827) (diegosouzapw#11906)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Clean, well-scoped env-override with correct blank-value handling and a startup log naming the resolution source. Thanks.
…in manifest (diegosouzapw#11903)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Discovery-only as claimed — nothing reads the new tag yet, dashboard quota widget stays gated by USAGE_SUPPORTED_PROVIDERS. Thanks.
…lently dropped (diegosouzapw#11863)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Fixes a genuinely confusing failure mode — matches the documented TROUBLESHOOTING.md symptom exactly. Thanks for the preflight check and clear diagnostics.
…diegosouzapw#11857)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Live-reproduced root cause (byte-for-byte reproduction/removal of the malformed schema) is solid evidence. Thanks for tracing this to the builtin skill schemas rather than stopping at "provider outage".
…gosouzapw#11825) (diegosouzapw#11934)

Resynced onto release/v3.8.51 (originally targeted main; retargeted since the default branch is release/v3.8.51). One real conflict in open-sse/handlers/chatCore.ts, but it was entirely unrelated to this PR's actual purpose: the antigravity-aware lockExactModel branching and deferAntigravityQuotaStateToCaller state exist on main but haven't been synced to release/v3.8.51 yet (confirmed by diffing your branch against its own main merge-base — the only change there was a Prettier reformat, not new logic). Discarded that unrelated drift and kept the release tip's current quota-lock shape; the onStreamComplete plugin wiring itself is untouched and intact. typecheck:core clean, 13/13 plugin delivery tests pass. Thanks for the thorough three-layer root-cause writeup.
…iegosouzapw#11927)

Routine patch bump of a GitHub-owned action (codeql-action 4.37.7 → 4.37.8).
…uzapw#11926)

Routine patch bump of a GitHub-owned action (codeql-action 4.37.7 → 4.37.8).
…egosouzapw#11925)

Routine patch bump of a GitHub-owned action (codeql-action 4.37.7 → 4.37.8).
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Correção pequena e correta — `passthroughModels: true` para o Vercel AI Gateway. Validado no worktree combinado do lote (typecheck limpo, gates estáticos verdes). Obrigado!
…gosouzapw#11766) (diegosouzapw#11794)

Fix correto — CLI agora sonda IPv4 e IPv6 no probe de prontidão do servidor, com teste de regressão próprio (`tests/unit/cli-waitForServer.test.mjs`). Validado no worktree combinado (typecheck limpo, teste focado verde). Obrigado!
…diegosouzapw#11784)

Regeneração legítima do `package-lock.json` do workspace `packages/browser-pool`. Sem alteração de código, validado no worktree combinado. Obrigado!
…gistration (diegosouzapw#11830)

Adiciona GLM-5.3-Flash ao catálogo com pricing/specs e teste próprio. Validado no worktree combinado (typecheck limpo, teste focado verde). Obrigado!
…nd GitHub wiki (diegosouzapw#11834)

Resolução de links relativos entre Fumadocs e a wiki do GitHub, com dois arquivos de teste novos e bem focados (`docs-link-resolver.test.ts`, `sync-wiki.test.ts`). Validado no worktree combinado. Obrigado!
… chart tooltips (diegosouzapw#11960)

Fix de contraste no tooltip do gráfico de custos (fundo opaco + cor de texto legível). Mudança isolada de CSS/classe, validada no worktree combinado. Obrigado!
…form/pluginType as numeric enums) (diegosouzapw#11969)

Envia metadata completo do loadCodeAssist (ideType/platform/pluginType como enums numéricos) para o Antigravity, com teste de regressão atualizado. Validado no worktree combinado. Obrigado!
…et (diegosouzapw#11959)

Dá aos alvos de extended-thinking o orçamento de prontidão de reasoning, com cobertura de teste ampliada em `stream-readiness-policy.test.ts`. Validado no worktree combinado. Obrigado!
…diegosouzapw#11854)

Corrige divergência de scoring no relatório de saúde do auto-combo, com testes atualizados em `combo-resolve-auto-strategy-split.test.ts` e `combo-scoring-inspector.test.ts`. Validado no worktree combinado. Obrigado!
Adiciona 5dive como configure target, com teste de regressão próprio (`tests/unit/cli/setup-5dive.test.ts`) e strings i18n em 12 locales. Validado no worktree combinado (typecheck limpo, 26 testes focados). 

Nota: um dos subtestes desse arquivo ("falls back to the local server when no context") depende de não haver contexto CLI ativo em `~/.omniroute/` — nesta máquina de desenvolvimento compartilhada existe um contexto real configurado, então o teste lê a config real em vez do fallback via `PORT`. Confirmado que é vazamento de ambiente do devbox (não do CI): reproduzido isoladamente, rastreado até `resolveActiveContext()` lendo `~/.omniroute/*.json` antes de cair no fallback de `PORT`. Não bloqueia o merge, mas fica registrado — o teste merece ficar hermético (mockar/isolar o data dir) numa limpeza futura.
…11970)

Mostra a % de cache nos logs de requisição, com teste próprio (`request-logger-cache-percentage.test.ts`). Validado no worktree combinado do lote.

Pequeno ajuste feito por cima: `formatCachePercentage` movida de `RequestLoggerV2.tsx` para `src/shared/utils/formatting.ts`. `RequestLoggerV2.tsx` importa `RequestLoggerDetail`, que desde o diegosouzapw#11703 (mergeado nesta mesma sessão) importa CSS bruto de `react18-json-view` — algo que o Node native test runner não consegue carregar. O teste original importava a função direto do componente e quebrava por causa dessa cadeia de import, não por bug na PR. Movida a função (pura, sem dependências) para o utils compartilhado; ajustado o import do componente e do teste. Typecheck limpo, teste passando (6/6).
@diegosouzapw
diegosouzapw merged commit 385e90f into diegosouzapw:release/v3.8.51 Aug 30, 2026
0 of 3 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
… (BigQuery first) (diegosouzapw#11945)

Feature grande e bem construída: exportação contínua de call logs para destinos plugáveis (BigQuery primeiro). Revisei especificamente o tratamento de segredos (`src/lib/logExport/secrets.ts`) e a migração — encryption gate real (`requiresEncryptionKey` recusa gravação em texto plano quando `STORAGE_ENCRYPTION_KEY` não está setada), redação antes de qualquer resposta de API, e a migração cria a tabela com `enabled=0`/`include_bodies=0` por padrão (opt-in, sem exportar nada até o operador configurar). 62/62 testes focados verdes, typecheck limpo.

Resolvido o conflito com o barrel `src/lib/localDb.ts` (removido nesta mesma sessão, diegosouzapw#11795 fase 5 — todo consumidor já migrado para `src/lib/db/*`); a PR só adicionava um re-export nele, que não é mais necessário. Obrigado pela contribuição!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.