diff --git a/.ci/observe-workspace.patch b/.ci/observe-workspace.patch new file mode 100644 index 0000000..4634728 --- /dev/null +++ b/.ci/observe-workspace.patch @@ -0,0 +1,57 @@ +--- a/pnpm-lock.yaml ++++ b/pnpm-lock.yaml +@@ -58,6 +58,25 @@ + vite: + specifier: ^6.0.7 + version: 6.4.1(@types/node@25.9.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) ++ ++ apps/observe: ++ dependencies: ++ '@neutron-build/cli': ++ specifier: workspace:* ++ version: link:../../packages/neutron-cli ++ '@neutron-build/core': ++ specifier: workspace:* ++ version: link:../../packages/neutron ++ preact: ++ specifier: 10.25.4 ++ version: 10.25.4 ++ uplot: ++ specifier: 1.6.31 ++ version: 1.6.31 ++ devDependencies: ++ typescript: ++ specifier: 5.7.2 ++ version: 5.7.2 + + apps/playground: + dependencies: +@@ -4860,6 +4879,9 @@ + peerDependencies: + browserslist: '>= 4.21.0' + ++ uplot@1.6.31: ++ resolution: {integrity: sha512-sQZqSwVCbJGnFB4IQjQYopzj5CoTZJ4Br1fG/xdONimqgHmsacvCjNesdGDypNKFbrhLGIeshYhy89FxPF+H+w==} ++ + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + version: 1.3.3 +@@ -9722,6 +9744,8 @@ + escalade: 3.2.0 + picocolors: 1.1.1 + ++ uplot@1.6.31: {} ++ + use-callback-ref@1.3.3(@types/react@19.2.14)(react-compat-shim@file:compat-matrix/shims/react): + dependencies: + react: react-compat-shim@file:compat-matrix/shims/react +--- a/pnpm-workspace.yaml ++++ b/pnpm-workspace.yaml +@@ -6,7 +6,6 @@ + # workspace graph became machine-dependent: a snapshot written locally listed + # observe-dashboard and CI could never reproduce it. The workspace definition + # has to describe the repository, not whatever happens to be on disk. +- - "!apps/observe" + - "examples/*" + # Ecosystem compatibility harness (A-007). Never published. + - "compat-matrix" diff --git a/.ci/prepare-ui-workspace.mjs b/.ci/prepare-ui-workspace.mjs new file mode 100644 index 0000000..515410e --- /dev/null +++ b/.ci/prepare-ui-workspace.mjs @@ -0,0 +1,22 @@ +// Assemble the external Observe app in a disposable, pinned Neutron checkout. +// The patch adds only this importer and its missing dependency to the lockfile. +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { execFileSync } from 'node:child_process'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +if (!process.argv[2]) throw new Error('usage: node .ci/prepare-ui-workspace.mjs '); +const workspace = resolve(process.argv[2], 'typescript'); +const app = resolve(workspace, 'apps/observe'); +if (existsSync(app)) throw new Error(`Refusing to overwrite an existing app: ${app}`); +execFileSync('patch', ['--dry-run', '-p1', '-i', resolve(root, '.ci/observe-workspace.patch')], { cwd: workspace, stdio: 'inherit' }); +execFileSync('patch', ['-p1', '-i', resolve(root, '.ci/observe-workspace.patch')], { cwd: workspace, stdio: 'inherit' }); +mkdirSync(app, { recursive: true }); +const pkg = JSON.parse(readFileSync(resolve(root, 'ui/package.json'), 'utf8')); +for (const [oldName, newName] of [['neutron', '@neutron-build/core'], ['neutron-cli', '@neutron-build/cli']]) { + pkg.dependencies[newName] = pkg.dependencies[oldName]; + delete pkg.dependencies[oldName]; +} +writeFileSync(resolve(app, 'package.json'), JSON.stringify(pkg, null, 2) + '\n'); +writeFileSync(resolve(app, 'neutron.config.ts'), readFileSync(resolve(root, 'ui/neutron.config.ts'), 'utf8').replace('"neutron"', '"@neutron-build/core"')); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61c0b1c..0e6c953 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,12 +151,17 @@ jobs: corepack enable corepack prepare "pnpm@$version" --activate pnpm --version + - name: Add Observe to the isolated pinned workspace + run: node .ci/prepare-ui-workspace.mjs ../Neutron - name: Provision TS workspace run: pnpm -C ../Neutron/typescript install --frozen-lockfile + - name: Build framework and CLI from the pinned source + run: pnpm -C ../Neutron/typescript --filter '@neutron-build/cli...' run build - name: ui-sync (build + embed) - run: sh scripts/ui-sync.sh + run: bash scripts/ui-sync.sh env: OBSERVE_BIN: /tmp/observe-ui-sync + NEUTRON_ROOT: ${{ github.workspace }}/../Neutron - name: Embedded dist must be fresh run: git diff --exit-code -- cmd/observe/ui/dist @@ -170,10 +175,9 @@ jobs: # 027) was fixed upstream on 2026-09-18 (Neutron 6286531a) and # repo-built engines pass the full migration ladder again — the # lease-capability job below builds from the pin and relies on it. This - # job keeps the published v0.1.8 fixture for compatibility coverage; - # its known flake (intermittent committed-upsert loss on accumulated - # data, also fixed in the tree) is why the capability job exists - # separately rather than replacing this one. + # Supported-release coverage now uses v1.1.1 on both architectures. + # v0.1.8 no longer satisfies the full suite (leases, parameterized paging, + # and committed upserts); its failing run is recorded in AUDIT_OPEN.md. # X01: the vendor/ tree is what actually compiles, but go.mod's replace # points at a LOCAL Neutron checkout — nothing proved vendor/ matches the # pinned submodule until this check. Drift here means the shipped build @@ -244,7 +248,7 @@ jobs: run: | OBSERVE_NUCLEUS_URL='postgres://nucleus@127.0.0.1:55432/observe?sslmode=disable' \ OBSERVE_ADMIN_PASSWORD=ci-migration-boot \ - OBSERVE_JWT_SECRET=ci-migration-secret \ + OBSERVE_JWT_SECRET=ci-only-migration-secret-at-least-32-characters \ /tmp/observe & OBS_PID=$! for i in $(seq 1 90); do @@ -277,7 +281,7 @@ jobs: -e NUCLEUS_ALLOW_NO_AUTH=1 \ -e NUCLEUS_ALLOW_INSECURE_CLUSTER=1 \ -e NUCLEUS_ALLOW_INSECURE_REPLICATION=1 \ - ghcr.io/neutron-build/nucleus:v0.1.8 \ + ghcr.io/neutron-build/nucleus:v1.1.1 \ start --host 0.0.0.0 --port 5432 --cluster-port 5433 --data /data --max-memory 512 for i in $(seq 1 60); do if (exec 3<>/dev/tcp/127.0.0.1/55432) 2>/dev/null; then exec 3>&-; exit 0; fi @@ -292,7 +296,7 @@ jobs: run: | OBSERVE_NUCLEUS_URL='postgres://nucleus@127.0.0.1:55432/observe?sslmode=disable' \ OBSERVE_ADMIN_PASSWORD=ci-migration-boot \ - OBSERVE_JWT_SECRET=ci-migration-secret \ + OBSERVE_JWT_SECRET=ci-only-migration-secret-at-least-32-characters \ /tmp/observe & OBS_PID=$! for i in $(seq 1 90); do diff --git a/AUDIT_OPEN.md b/AUDIT_OPEN.md index f67f77f..78fc56f 100644 --- a/AUDIT_OPEN.md +++ b/AUDIT_OPEN.md @@ -14,6 +14,17 @@ report lives outside the repo) — remediation record below. Round 4: audit passes 1-5) are closed history; their one surviving item is folded into F16 below. +## L10 (2026-09-27) — Fixed in source: LLM traces omitted from retention + +LLM traces, including prompt/completion payloads, were never expired by the +scheduled cleanup. Added a validated OBSERVE_LLM_RETENTION_DAYS setting (30-day +default) and included the table in the existing bounded cleanup path. Historical +model-price catalog entries remain independent of trace retention. Regression +coverage checks expiry and preservation against a real Nucleus fixture. +This fixes unbounded logical trace retention; it does not claim SQL DELETE +shrinks files or establish this table as the cause of a past host incident. +Deployment and sustained storage measurements remain separate acceptance gates. + ## L9 (2026-09-24) - P1 - Open: live llm_traces unreadable after migration 054; ALTER-ADD migrations Found deploying `d4bcbe6` (the zombie-reaping image fix) to infra-home, the @@ -2361,3 +2372,24 @@ Open items this slice deliberately leaves (programme O11 tails): - Package publication prep is owner-controlled per the O11 spec: npm tarball dry-runs (browser, sentry-shim), PyPI sdist/wheel build, Go module tagging — none attempted from the lane. + +## 2026-09-27 — reproducible UI and supported-engine CI + +CI run 36336753676 proved the complete suite on Nucleus v1.1.1 arm64 +and the source-pinned snapshot-lease suite. Its v0.1.8 amd64 job failed +lease expectations, parameterized paging, and committed-upsert assertions. +That obsolete release is not a passing compatibility target. The amd64 +gate now uses v1.1.1, matching the existing arm64 gate; no tests are skipped +or weakened. The failed historical run remains the compatibility receipt. + +The pinned Neutron repository deliberately excludes the external Observe app. +CI now assembles it in its isolated checkout using a checked-in lockfile patch, +then installs frozen dependencies and builds the framework/CLI before ui-sync. +No developer-local canonical app or uncommitted shared Neutron changes are used. +Embedded-asset freshness remains a required check. + +A clean rebuild matched every application asset but exposed upstream static +adapter wall-clock timestamps. ui-sync normalizes those metadata timestamps +and their compressed policy/size counts before embedding; all application +assets remain byte-checked. Upstream report: private UPSTREAM_BUGS.md entry +2026-09-27. Two local builds and normalization idempotence verified. diff --git a/README.md b/README.md index 60e4da4..08fdd37 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,7 @@ observeErrors.addBreadcrumb({ type: "user", category: "click", message: "Button" | `OBSERVE_REQUIRE_WAL` | (unset) | Set to `true` (or `1`) to refuse to start when WAL-backed ingestion durability is unavailable, instead of degrading to memory-only. | | `OBSERVE_RAW_RETENTION_DAYS` | `30` | Raw event retention. Also the window over which visitor counts are exact from raw events; past it they are counted from the `sessions` table (90 days), and past both the dashboard says which window the figure covers. | | `OBSERVE_HOURLY_RETENTION_DAYS` | `365` | Hourly rollup retention. | +| `OBSERVE_LLM_RETENTION_DAYS` | `30` | LLM trace retention, including stored prompts/completions. Must be at least 1. Cleanup runs daily; historical model-price catalog entries are retained. Logical expiry does not guarantee database files immediately shrink. | | `OBSERVE_LOG_ROUTES` | `0` | Set to `1` to print route table at boot. | | `OBSERVE_SMTP_HOST` | | SMTP server for email reports. | | `OBSERVE_SMTP_PORT` | `587` | SMTP port. | diff --git a/cmd/observe/main.go b/cmd/observe/main.go index 74cda57..ab90b0e 100644 --- a/cmd/observe/main.go +++ b/cmd/observe/main.go @@ -372,7 +372,7 @@ func main() { // same numbers rather than a constant of its own. The ledger policies // (O01 slice 5) ride the same job: dedupe ledgers expire with the data // they dedupe, processed outbox intents prune, dead letters never do. - retentionPolicies := append(jobs.DefaultPolicies(cfg.RawRetentionDays, cfg.HourlyRetentionDays), + retentionPolicies := append(jobs.DefaultPolicies(cfg.RawRetentionDays, cfg.HourlyRetentionDays, cfg.LLMRetentionDays), jobs.DefaultLedgerPolicies(cfg.ErrorInboxRetentionDays, cfg.ReplayBatchesRetentionDays, cfg.DerivedOutboxRetentionDays)...) // Stats service diff --git a/cmd/observe/ui/dist/.neutron-adapter-static.json b/cmd/observe/ui/dist/.neutron-adapter-static.json index fd134a5..c8dfaf7 100644 --- a/cmd/observe/ui/dist/.neutron-adapter-static.json +++ b/cmd/observe/ui/dist/.neutron-adapter-static.json @@ -8,8 +8,7 @@ "compression": { "enabled": true, "files": 78, - "gzipBytesSaved": 642496, - "brotliBytesSaved": 703986 - }, - "generatedAt": "2026-09-24T01:37:32.305Z" + "gzipBytesSaved": 642486, + "brotliBytesSaved": 703972 + } } \ No newline at end of file diff --git a/cmd/observe/ui/dist/.neutron-static-policy.json b/cmd/observe/ui/dist/.neutron-static-policy.json index 7740da2..fcd6c72 100644 --- a/cmd/observe/ui/dist/.neutron-static-policy.json +++ b/cmd/observe/ui/dist/.neutron-static-policy.json @@ -1,5 +1,4 @@ { - "generatedAt": "2026-09-24T01:37:31.384Z", "headers": { "/*": { "X-Content-Type-Options": "nosniff", diff --git a/cmd/observe/ui/dist/.neutron-static-policy.json.br b/cmd/observe/ui/dist/.neutron-static-policy.json.br index 3056635..4584864 100644 --- a/cmd/observe/ui/dist/.neutron-static-policy.json.br +++ b/cmd/observe/ui/dist/.neutron-static-policy.json.br @@ -1,2 +1 @@ -€ v,¶f -ÎÝÞ¬ØVªÌE†ED* oø»$ˆH_+€°èXo›�£ÿÂ�¦Ùl&‚’¥¬d-Ðm>]Q¨Œ¹è‘jˆ×.z´tz Z�ÕB”lۉWLµ£ Dç;­{#�]Vj€™A�‡¿–Nçûi»,”5Hd2;Ò,o»)�²xÞH�÷Çm;>@6ƒ]�>hSÈ;8ÔÄ#r9dSüñ~ì?è×ÙS$MÑïä²=Ÿ´­"ï‹EŽXÂmV5fâ \ No newline at end of file +S v,æö<‘JÏÇÎÔtž±šF•‰c�éK�õ¶ 9ú/ühŠ P™"J–²œè6!:<º‚0Y›Ð#Õ¤]Ýem’®Ø Ì©"Iõ5™æ}j«JòÕ7·+£Ä\U“çÅ"³îÇÖæü1„ ’Ó¢ÈîtàPf#Ôf²ÿ:|�éCZ—¾mº¶wͶvO&´Èç"fC•3…­Lêlâ \ No newline at end of file diff --git a/cmd/observe/ui/dist/.neutron-static-policy.json.gz b/cmd/observe/ui/dist/.neutron-static-policy.json.gz index 99ceb45..4348959 100644 Binary files a/cmd/observe/ui/dist/.neutron-static-policy.json.gz and b/cmd/observe/ui/dist/.neutron-static-policy.json.gz differ diff --git a/cmd/observe/ui/dist/.vite/manifest.json.gz b/cmd/observe/ui/dist/.vite/manifest.json.gz index d62cd43..e070bb1 100644 Binary files a/cmd/observe/ui/dist/.vite/manifest.json.gz and b/cmd/observe/ui/dist/.vite/manifest.json.gz differ diff --git a/cmd/observe/ui/dist/assets/CodeBlock-eXEjlwde.js.gz b/cmd/observe/ui/dist/assets/CodeBlock-eXEjlwde.js.gz index 892b00e..a9ea453 100644 Binary files a/cmd/observe/ui/dist/assets/CodeBlock-eXEjlwde.js.gz and b/cmd/observe/ui/dist/assets/CodeBlock-eXEjlwde.js.gz differ diff --git a/cmd/observe/ui/dist/assets/ConfirmDialog-Dcm8ziSO.js.gz b/cmd/observe/ui/dist/assets/ConfirmDialog-Dcm8ziSO.js.gz index 63c1b63..5bacec3 100644 Binary files a/cmd/observe/ui/dist/assets/ConfirmDialog-Dcm8ziSO.js.gz and b/cmd/observe/ui/dist/assets/ConfirmDialog-Dcm8ziSO.js.gz differ diff --git a/cmd/observe/ui/dist/assets/EmptyState-Cq8HfmOy.js.gz b/cmd/observe/ui/dist/assets/EmptyState-Cq8HfmOy.js.gz index e1ea617..807df28 100644 Binary files a/cmd/observe/ui/dist/assets/EmptyState-Cq8HfmOy.js.gz and b/cmd/observe/ui/dist/assets/EmptyState-Cq8HfmOy.js.gz differ diff --git a/cmd/observe/ui/dist/assets/ExportButton-D16qcWq9.js.gz b/cmd/observe/ui/dist/assets/ExportButton-D16qcWq9.js.gz index 166d6c1..87766e9 100644 Binary files a/cmd/observe/ui/dist/assets/ExportButton-D16qcWq9.js.gz and b/cmd/observe/ui/dist/assets/ExportButton-D16qcWq9.js.gz differ diff --git a/cmd/observe/ui/dist/assets/LoadError-Cedf2Zdq.js.gz b/cmd/observe/ui/dist/assets/LoadError-Cedf2Zdq.js.gz index ddcab7d..1ae14f1 100644 Binary files a/cmd/observe/ui/dist/assets/LoadError-Cedf2Zdq.js.gz and b/cmd/observe/ui/dist/assets/LoadError-Cedf2Zdq.js.gz differ diff --git a/cmd/observe/ui/dist/assets/Modal-BXflaNUd.js.gz b/cmd/observe/ui/dist/assets/Modal-BXflaNUd.js.gz index 1a2f2ed..5c1f973 100644 Binary files a/cmd/observe/ui/dist/assets/Modal-BXflaNUd.js.gz and b/cmd/observe/ui/dist/assets/Modal-BXflaNUd.js.gz differ diff --git a/cmd/observe/ui/dist/assets/Pagination-C2Bjp44k.js.gz b/cmd/observe/ui/dist/assets/Pagination-C2Bjp44k.js.gz index d69607e..266a4b1 100644 Binary files a/cmd/observe/ui/dist/assets/Pagination-C2Bjp44k.js.gz and b/cmd/observe/ui/dist/assets/Pagination-C2Bjp44k.js.gz differ diff --git a/cmd/observe/ui/dist/assets/SearchInput-DG57hjfw.js.gz b/cmd/observe/ui/dist/assets/SearchInput-DG57hjfw.js.gz index 01ebeac..396502a 100644 Binary files a/cmd/observe/ui/dist/assets/SearchInput-DG57hjfw.js.gz and b/cmd/observe/ui/dist/assets/SearchInput-DG57hjfw.js.gz differ diff --git a/cmd/observe/ui/dist/assets/StatusBadge-D4HFP28I.js.gz b/cmd/observe/ui/dist/assets/StatusBadge-D4HFP28I.js.gz index a40a89c..23eec82 100644 Binary files a/cmd/observe/ui/dist/assets/StatusBadge-D4HFP28I.js.gz and b/cmd/observe/ui/dist/assets/StatusBadge-D4HFP28I.js.gz differ diff --git a/cmd/observe/ui/dist/assets/Tabs-HjFjaSVm.js.gz b/cmd/observe/ui/dist/assets/Tabs-HjFjaSVm.js.gz index 1ec4529..742f43d 100644 Binary files a/cmd/observe/ui/dist/assets/Tabs-HjFjaSVm.js.gz and b/cmd/observe/ui/dist/assets/Tabs-HjFjaSVm.js.gz differ diff --git a/cmd/observe/ui/dist/assets/_layout-D3Xd7fXB.js.gz b/cmd/observe/ui/dist/assets/_layout-D3Xd7fXB.js.gz index a044ab6..0b7e924 100644 Binary files a/cmd/observe/ui/dist/assets/_layout-D3Xd7fXB.js.gz and b/cmd/observe/ui/dist/assets/_layout-D3Xd7fXB.js.gz differ diff --git a/cmd/observe/ui/dist/assets/_layout-HPc_Hkdl.css.gz b/cmd/observe/ui/dist/assets/_layout-HPc_Hkdl.css.gz index c443eae..8a4472c 100644 Binary files a/cmd/observe/ui/dist/assets/_layout-HPc_Hkdl.css.gz and b/cmd/observe/ui/dist/assets/_layout-HPc_Hkdl.css.gz differ diff --git a/cmd/observe/ui/dist/assets/alerts-X8FEeBv4.js.gz b/cmd/observe/ui/dist/assets/alerts-X8FEeBv4.js.gz index b3fb992..a1f417e 100644 Binary files a/cmd/observe/ui/dist/assets/alerts-X8FEeBv4.js.gz and b/cmd/observe/ui/dist/assets/alerts-X8FEeBv4.js.gz differ diff --git a/cmd/observe/ui/dist/assets/analytics-CURgGHu0.js.gz b/cmd/observe/ui/dist/assets/analytics-CURgGHu0.js.gz index a02f825..6d89b6a 100644 Binary files a/cmd/observe/ui/dist/assets/analytics-CURgGHu0.js.gz and b/cmd/observe/ui/dist/assets/analytics-CURgGHu0.js.gz differ diff --git a/cmd/observe/ui/dist/assets/audit-CgOj4m5w.js.gz b/cmd/observe/ui/dist/assets/audit-CgOj4m5w.js.gz index dd2d8b3..56eafa0 100644 Binary files a/cmd/observe/ui/dist/assets/audit-CgOj4m5w.js.gz and b/cmd/observe/ui/dist/assets/audit-CgOj4m5w.js.gz differ diff --git a/cmd/observe/ui/dist/assets/auth-CFVI_Mbd.js.gz b/cmd/observe/ui/dist/assets/auth-CFVI_Mbd.js.gz index 4908b10..59633b4 100644 Binary files a/cmd/observe/ui/dist/assets/auth-CFVI_Mbd.js.gz and b/cmd/observe/ui/dist/assets/auth-CFVI_Mbd.js.gz differ diff --git a/cmd/observe/ui/dist/assets/boards-BaaRYrYm.css.gz b/cmd/observe/ui/dist/assets/boards-BaaRYrYm.css.gz index c03fc6f..4e7466b 100644 Binary files a/cmd/observe/ui/dist/assets/boards-BaaRYrYm.css.gz and b/cmd/observe/ui/dist/assets/boards-BaaRYrYm.css.gz differ diff --git a/cmd/observe/ui/dist/assets/boards-BtLKgPKd.js.gz b/cmd/observe/ui/dist/assets/boards-BtLKgPKd.js.gz index d585e36..a96161d 100644 Binary files a/cmd/observe/ui/dist/assets/boards-BtLKgPKd.js.gz and b/cmd/observe/ui/dist/assets/boards-BtLKgPKd.js.gz differ diff --git a/cmd/observe/ui/dist/assets/campaigns-Z-BMMaUY.js.gz b/cmd/observe/ui/dist/assets/campaigns-Z-BMMaUY.js.gz index d562cde..f244adf 100644 Binary files a/cmd/observe/ui/dist/assets/campaigns-Z-BMMaUY.js.gz and b/cmd/observe/ui/dist/assets/campaigns-Z-BMMaUY.js.gz differ diff --git a/cmd/observe/ui/dist/assets/clipboard-BRysZGwJ.js.gz b/cmd/observe/ui/dist/assets/clipboard-BRysZGwJ.js.gz index cc313af..3c24ec8 100644 Binary files a/cmd/observe/ui/dist/assets/clipboard-BRysZGwJ.js.gz and b/cmd/observe/ui/dist/assets/clipboard-BRysZGwJ.js.gz differ diff --git a/cmd/observe/ui/dist/assets/cohorts-5PmHEjPf.js.gz b/cmd/observe/ui/dist/assets/cohorts-5PmHEjPf.js.gz index 25fb8e9..0f7484d 100644 Binary files a/cmd/observe/ui/dist/assets/cohorts-5PmHEjPf.js.gz and b/cmd/observe/ui/dist/assets/cohorts-5PmHEjPf.js.gz differ diff --git a/cmd/observe/ui/dist/assets/dashboard-Bu3tsvbK.css.gz b/cmd/observe/ui/dist/assets/dashboard-Bu3tsvbK.css.gz index b0f25b5..7368f5b 100644 Binary files a/cmd/observe/ui/dist/assets/dashboard-Bu3tsvbK.css.gz and b/cmd/observe/ui/dist/assets/dashboard-Bu3tsvbK.css.gz differ diff --git a/cmd/observe/ui/dist/assets/dashboards-CMWV5B4P.js.gz b/cmd/observe/ui/dist/assets/dashboards-CMWV5B4P.js.gz index 1f8a1b7..50908a5 100644 Binary files a/cmd/observe/ui/dist/assets/dashboards-CMWV5B4P.js.gz and b/cmd/observe/ui/dist/assets/dashboards-CMWV5B4P.js.gz differ diff --git a/cmd/observe/ui/dist/assets/dashboards-TvcfB7ZH.css.gz b/cmd/observe/ui/dist/assets/dashboards-TvcfB7ZH.css.gz index 41a9e85..f19dbc4 100644 Binary files a/cmd/observe/ui/dist/assets/dashboards-TvcfB7ZH.css.gz and b/cmd/observe/ui/dist/assets/dashboards-TvcfB7ZH.css.gz differ diff --git a/cmd/observe/ui/dist/assets/docs-DtkLZ6TH.js.gz b/cmd/observe/ui/dist/assets/docs-DtkLZ6TH.js.gz index 35710a7..fccfd9f 100644 Binary files a/cmd/observe/ui/dist/assets/docs-DtkLZ6TH.js.gz and b/cmd/observe/ui/dist/assets/docs-DtkLZ6TH.js.gz differ diff --git a/cmd/observe/ui/dist/assets/docs-jX5Zsbxb.css.gz b/cmd/observe/ui/dist/assets/docs-jX5Zsbxb.css.gz index 8c60b4c..e1f76f6 100644 Binary files a/cmd/observe/ui/dist/assets/docs-jX5Zsbxb.css.gz and b/cmd/observe/ui/dist/assets/docs-jX5Zsbxb.css.gz differ diff --git a/cmd/observe/ui/dist/assets/errors-CabbRYx0.js.gz b/cmd/observe/ui/dist/assets/errors-CabbRYx0.js.gz index 97cb7e1..1bf71e5 100644 Binary files a/cmd/observe/ui/dist/assets/errors-CabbRYx0.js.gz and b/cmd/observe/ui/dist/assets/errors-CabbRYx0.js.gz differ diff --git a/cmd/observe/ui/dist/assets/errors-Cec7liwy.js.gz b/cmd/observe/ui/dist/assets/errors-Cec7liwy.js.gz index f290b70..fa36330 100644 Binary files a/cmd/observe/ui/dist/assets/errors-Cec7liwy.js.gz and b/cmd/observe/ui/dist/assets/errors-Cec7liwy.js.gz differ diff --git a/cmd/observe/ui/dist/assets/errors-R5Ijq7C9.css.gz b/cmd/observe/ui/dist/assets/errors-R5Ijq7C9.css.gz index 9bee18e..4aa4f8c 100644 Binary files a/cmd/observe/ui/dist/assets/errors-R5Ijq7C9.css.gz and b/cmd/observe/ui/dist/assets/errors-R5Ijq7C9.css.gz differ diff --git a/cmd/observe/ui/dist/assets/events-B4JfCITX.js.gz b/cmd/observe/ui/dist/assets/events-B4JfCITX.js.gz index 7450841..4e6133c 100644 Binary files a/cmd/observe/ui/dist/assets/events-B4JfCITX.js.gz and b/cmd/observe/ui/dist/assets/events-B4JfCITX.js.gz differ diff --git a/cmd/observe/ui/dist/assets/experiments-CBaekaSK.js.gz b/cmd/observe/ui/dist/assets/experiments-CBaekaSK.js.gz index be3c98f..be9618e 100644 Binary files a/cmd/observe/ui/dist/assets/experiments-CBaekaSK.js.gz and b/cmd/observe/ui/dist/assets/experiments-CBaekaSK.js.gz differ diff --git a/cmd/observe/ui/dist/assets/explorer-Dcjw3AWf.js.gz b/cmd/observe/ui/dist/assets/explorer-Dcjw3AWf.js.gz index d68483b..d76b722 100644 Binary files a/cmd/observe/ui/dist/assets/explorer-Dcjw3AWf.js.gz and b/cmd/observe/ui/dist/assets/explorer-Dcjw3AWf.js.gz differ diff --git a/cmd/observe/ui/dist/assets/explorer-ifCJQ3BP.css.gz b/cmd/observe/ui/dist/assets/explorer-ifCJQ3BP.css.gz index 4e86a72..3e61d5b 100644 Binary files a/cmd/observe/ui/dist/assets/explorer-ifCJQ3BP.css.gz and b/cmd/observe/ui/dist/assets/explorer-ifCJQ3BP.css.gz differ diff --git a/cmd/observe/ui/dist/assets/flags-B78yBvO7.js.gz b/cmd/observe/ui/dist/assets/flags-B78yBvO7.js.gz index 18d9e3f..df2e345 100644 Binary files a/cmd/observe/ui/dist/assets/flags-B78yBvO7.js.gz and b/cmd/observe/ui/dist/assets/flags-B78yBvO7.js.gz differ diff --git a/cmd/observe/ui/dist/assets/flags-CE0lBMCA.js.gz b/cmd/observe/ui/dist/assets/flags-CE0lBMCA.js.gz index 13e3288..f19df62 100644 Binary files a/cmd/observe/ui/dist/assets/flags-CE0lBMCA.js.gz and b/cmd/observe/ui/dist/assets/flags-CE0lBMCA.js.gz differ diff --git a/cmd/observe/ui/dist/assets/flags-ClqmhoJ5.css.gz b/cmd/observe/ui/dist/assets/flags-ClqmhoJ5.css.gz index 6237ea4..56da5e4 100644 Binary files a/cmd/observe/ui/dist/assets/flags-ClqmhoJ5.css.gz and b/cmd/observe/ui/dist/assets/flags-ClqmhoJ5.css.gz differ diff --git a/cmd/observe/ui/dist/assets/helpers-VROGXNpX.js.gz b/cmd/observe/ui/dist/assets/helpers-VROGXNpX.js.gz index bd61e6d..92a8a8e 100644 Binary files a/cmd/observe/ui/dist/assets/helpers-VROGXNpX.js.gz and b/cmd/observe/ui/dist/assets/helpers-VROGXNpX.js.gz differ diff --git a/cmd/observe/ui/dist/assets/hooks-ijpLJSlR.js.gz b/cmd/observe/ui/dist/assets/hooks-ijpLJSlR.js.gz index 7df1ced..b40ba3f 100644 Binary files a/cmd/observe/ui/dist/assets/hooks-ijpLJSlR.js.gz and b/cmd/observe/ui/dist/assets/hooks-ijpLJSlR.js.gz differ diff --git a/cmd/observe/ui/dist/assets/hydrate-24_12g4P.js.gz b/cmd/observe/ui/dist/assets/hydrate-24_12g4P.js.gz index 39bf69f..ac909fe 100644 Binary files a/cmd/observe/ui/dist/assets/hydrate-24_12g4P.js.gz and b/cmd/observe/ui/dist/assets/hydrate-24_12g4P.js.gz differ diff --git a/cmd/observe/ui/dist/assets/incidents-Bwvs4ZlH.js.gz b/cmd/observe/ui/dist/assets/incidents-Bwvs4ZlH.js.gz index 796cf1d..0a77a47 100644 Binary files a/cmd/observe/ui/dist/assets/incidents-Bwvs4ZlH.js.gz and b/cmd/observe/ui/dist/assets/incidents-Bwvs4ZlH.js.gz differ diff --git a/cmd/observe/ui/dist/assets/index-CQlltJUM.js.gz b/cmd/observe/ui/dist/assets/index-CQlltJUM.js.gz index e7aa82b..a5b206d 100644 Binary files a/cmd/observe/ui/dist/assets/index-CQlltJUM.js.gz and b/cmd/observe/ui/dist/assets/index-CQlltJUM.js.gz differ diff --git a/cmd/observe/ui/dist/assets/index-DLhXU6fD.js.gz b/cmd/observe/ui/dist/assets/index-DLhXU6fD.js.gz index 8c9497b..571080f 100644 Binary files a/cmd/observe/ui/dist/assets/index-DLhXU6fD.js.gz and b/cmd/observe/ui/dist/assets/index-DLhXU6fD.js.gz differ diff --git a/cmd/observe/ui/dist/assets/insights-BKICK4sl.js.gz b/cmd/observe/ui/dist/assets/insights-BKICK4sl.js.gz index 9c51577..034acf5 100644 Binary files a/cmd/observe/ui/dist/assets/insights-BKICK4sl.js.gz and b/cmd/observe/ui/dist/assets/insights-BKICK4sl.js.gz differ diff --git a/cmd/observe/ui/dist/assets/insights-Cic7Ai_u.css.gz b/cmd/observe/ui/dist/assets/insights-Cic7Ai_u.css.gz index fd0360f..ad11042 100644 Binary files a/cmd/observe/ui/dist/assets/insights-Cic7Ai_u.css.gz and b/cmd/observe/ui/dist/assets/insights-Cic7Ai_u.css.gz differ diff --git a/cmd/observe/ui/dist/assets/integrations-il7Uwk8Z.js.gz b/cmd/observe/ui/dist/assets/integrations-il7Uwk8Z.js.gz index 372929f..cd206b9 100644 Binary files a/cmd/observe/ui/dist/assets/integrations-il7Uwk8Z.js.gz and b/cmd/observe/ui/dist/assets/integrations-il7Uwk8Z.js.gz differ diff --git a/cmd/observe/ui/dist/assets/jsxRuntime-BVeGbjfr.js.gz b/cmd/observe/ui/dist/assets/jsxRuntime-BVeGbjfr.js.gz index 0f11fc6..4147bd5 100644 Binary files a/cmd/observe/ui/dist/assets/jsxRuntime-BVeGbjfr.js.gz and b/cmd/observe/ui/dist/assets/jsxRuntime-BVeGbjfr.js.gz differ diff --git a/cmd/observe/ui/dist/assets/llm-CKAOJT3f.js.gz b/cmd/observe/ui/dist/assets/llm-CKAOJT3f.js.gz index daf0f5e..ceb7711 100644 Binary files a/cmd/observe/ui/dist/assets/llm-CKAOJT3f.js.gz and b/cmd/observe/ui/dist/assets/llm-CKAOJT3f.js.gz differ diff --git a/cmd/observe/ui/dist/assets/login-CQUvaGga.js.gz b/cmd/observe/ui/dist/assets/login-CQUvaGga.js.gz index 49ce337..637ec38 100644 Binary files a/cmd/observe/ui/dist/assets/login-CQUvaGga.js.gz and b/cmd/observe/ui/dist/assets/login-CQUvaGga.js.gz differ diff --git a/cmd/observe/ui/dist/assets/logs-BCixbtxU.css.gz b/cmd/observe/ui/dist/assets/logs-BCixbtxU.css.gz index e6288c6..000840d 100644 Binary files a/cmd/observe/ui/dist/assets/logs-BCixbtxU.css.gz and b/cmd/observe/ui/dist/assets/logs-BCixbtxU.css.gz differ diff --git a/cmd/observe/ui/dist/assets/logs-CcuXv9IZ.js.gz b/cmd/observe/ui/dist/assets/logs-CcuXv9IZ.js.gz index 16a80b3..52c9f1f 100644 Binary files a/cmd/observe/ui/dist/assets/logs-CcuXv9IZ.js.gz and b/cmd/observe/ui/dist/assets/logs-CcuXv9IZ.js.gz differ diff --git a/cmd/observe/ui/dist/assets/meta-CvRzEPy0.js.gz b/cmd/observe/ui/dist/assets/meta-CvRzEPy0.js.gz index b4a90f3..97e29c4 100644 Binary files a/cmd/observe/ui/dist/assets/meta-CvRzEPy0.js.gz and b/cmd/observe/ui/dist/assets/meta-CvRzEPy0.js.gz differ diff --git a/cmd/observe/ui/dist/assets/meta-Dhn6Xpef.css.gz b/cmd/observe/ui/dist/assets/meta-Dhn6Xpef.css.gz index f430a0f..7dfcc65 100644 Binary files a/cmd/observe/ui/dist/assets/meta-Dhn6Xpef.css.gz and b/cmd/observe/ui/dist/assets/meta-Dhn6Xpef.css.gz differ diff --git a/cmd/observe/ui/dist/assets/metrics-BhMUdMNZ.js.gz b/cmd/observe/ui/dist/assets/metrics-BhMUdMNZ.js.gz index 9cb0c4f..bf35634 100644 Binary files a/cmd/observe/ui/dist/assets/metrics-BhMUdMNZ.js.gz and b/cmd/observe/ui/dist/assets/metrics-BhMUdMNZ.js.gz differ diff --git a/cmd/observe/ui/dist/assets/metrics-C1dSKLn-.js.gz b/cmd/observe/ui/dist/assets/metrics-C1dSKLn-.js.gz index 583f3e1..0b1dd57 100644 Binary files a/cmd/observe/ui/dist/assets/metrics-C1dSKLn-.js.gz and b/cmd/observe/ui/dist/assets/metrics-C1dSKLn-.js.gz differ diff --git a/cmd/observe/ui/dist/assets/monitoring-BQgKlspP.js.gz b/cmd/observe/ui/dist/assets/monitoring-BQgKlspP.js.gz index 3d5a322..d095c89 100644 Binary files a/cmd/observe/ui/dist/assets/monitoring-BQgKlspP.js.gz and b/cmd/observe/ui/dist/assets/monitoring-BQgKlspP.js.gz differ diff --git a/cmd/observe/ui/dist/assets/monitoring-BRTRhWU8.css.gz b/cmd/observe/ui/dist/assets/monitoring-BRTRhWU8.css.gz index 6917697..b5aeb8e 100644 Binary files a/cmd/observe/ui/dist/assets/monitoring-BRTRhWU8.css.gz and b/cmd/observe/ui/dist/assets/monitoring-BRTRhWU8.css.gz differ diff --git a/cmd/observe/ui/dist/assets/onboard-C8Yvwrnx.js.gz b/cmd/observe/ui/dist/assets/onboard-C8Yvwrnx.js.gz index 89b0c37..c5aabe4 100644 Binary files a/cmd/observe/ui/dist/assets/onboard-C8Yvwrnx.js.gz and b/cmd/observe/ui/dist/assets/onboard-C8Yvwrnx.js.gz differ diff --git a/cmd/observe/ui/dist/assets/onboard-DZ_JFrT5.css.gz b/cmd/observe/ui/dist/assets/onboard-DZ_JFrT5.css.gz index f104068..82ebfad 100644 Binary files a/cmd/observe/ui/dist/assets/onboard-DZ_JFrT5.css.gz and b/cmd/observe/ui/dist/assets/onboard-DZ_JFrT5.css.gz differ diff --git a/cmd/observe/ui/dist/assets/persons-B_VByFqa.js.gz b/cmd/observe/ui/dist/assets/persons-B_VByFqa.js.gz index 033c724..b1ad1ad 100644 Binary files a/cmd/observe/ui/dist/assets/persons-B_VByFqa.js.gz and b/cmd/observe/ui/dist/assets/persons-B_VByFqa.js.gz differ diff --git a/cmd/observe/ui/dist/assets/persons-DvXNJJBL.js.gz b/cmd/observe/ui/dist/assets/persons-DvXNJJBL.js.gz index f304b3d..73016f8 100644 Binary files a/cmd/observe/ui/dist/assets/persons-DvXNJJBL.js.gz and b/cmd/observe/ui/dist/assets/persons-DvXNJJBL.js.gz differ diff --git a/cmd/observe/ui/dist/assets/releases-BXFoJ3wM.js.gz b/cmd/observe/ui/dist/assets/releases-BXFoJ3wM.js.gz index 04926e0..5a618e0 100644 Binary files a/cmd/observe/ui/dist/assets/releases-BXFoJ3wM.js.gz and b/cmd/observe/ui/dist/assets/releases-BXFoJ3wM.js.gz differ diff --git a/cmd/observe/ui/dist/assets/reports-pOpVNGOC.js.gz b/cmd/observe/ui/dist/assets/reports-pOpVNGOC.js.gz index 39a7e75..89ad626 100644 Binary files a/cmd/observe/ui/dist/assets/reports-pOpVNGOC.js.gz and b/cmd/observe/ui/dist/assets/reports-pOpVNGOC.js.gz differ diff --git a/cmd/observe/ui/dist/assets/sessions-1nnFrpY_.js.gz b/cmd/observe/ui/dist/assets/sessions-1nnFrpY_.js.gz index 0ef9454..2e867e4 100644 Binary files a/cmd/observe/ui/dist/assets/sessions-1nnFrpY_.js.gz and b/cmd/observe/ui/dist/assets/sessions-1nnFrpY_.js.gz differ diff --git a/cmd/observe/ui/dist/assets/sessions-D_rwxSji.css.gz b/cmd/observe/ui/dist/assets/sessions-D_rwxSji.css.gz index 2353c2d..e3f2b4f 100644 Binary files a/cmd/observe/ui/dist/assets/sessions-D_rwxSji.css.gz and b/cmd/observe/ui/dist/assets/sessions-D_rwxSji.css.gz differ diff --git a/cmd/observe/ui/dist/assets/settings-BjDXM5Tx.js.gz b/cmd/observe/ui/dist/assets/settings-BjDXM5Tx.js.gz index 39233e5..d4c3188 100644 Binary files a/cmd/observe/ui/dist/assets/settings-BjDXM5Tx.js.gz and b/cmd/observe/ui/dist/assets/settings-BjDXM5Tx.js.gz differ diff --git a/cmd/observe/ui/dist/assets/settings-D8_XUN_q.css.gz b/cmd/observe/ui/dist/assets/settings-D8_XUN_q.css.gz index 7cbe0c8..eae7d09 100644 Binary files a/cmd/observe/ui/dist/assets/settings-D8_XUN_q.css.gz and b/cmd/observe/ui/dist/assets/settings-D8_XUN_q.css.gz differ diff --git a/cmd/observe/ui/dist/assets/settings-hOmVp-ct.js.gz b/cmd/observe/ui/dist/assets/settings-hOmVp-ct.js.gz index 02faa54..a0d5a6c 100644 Binary files a/cmd/observe/ui/dist/assets/settings-hOmVp-ct.js.gz and b/cmd/observe/ui/dist/assets/settings-hOmVp-ct.js.gz differ diff --git a/cmd/observe/ui/dist/assets/setup-XwZxYwSR.js.gz b/cmd/observe/ui/dist/assets/setup-XwZxYwSR.js.gz index 49dfc68..319be37 100644 Binary files a/cmd/observe/ui/dist/assets/setup-XwZxYwSR.js.gz and b/cmd/observe/ui/dist/assets/setup-XwZxYwSR.js.gz differ diff --git a/cmd/observe/ui/dist/assets/surveys-3ecQbmSx.js.gz b/cmd/observe/ui/dist/assets/surveys-3ecQbmSx.js.gz index 8ea176d..66bd915 100644 Binary files a/cmd/observe/ui/dist/assets/surveys-3ecQbmSx.js.gz and b/cmd/observe/ui/dist/assets/surveys-3ecQbmSx.js.gz differ diff --git a/cmd/observe/ui/dist/assets/traces-A5dZarWK.js.gz b/cmd/observe/ui/dist/assets/traces-A5dZarWK.js.gz index 9b330cb..9e4a39f 100644 Binary files a/cmd/observe/ui/dist/assets/traces-A5dZarWK.js.gz and b/cmd/observe/ui/dist/assets/traces-A5dZarWK.js.gz differ diff --git a/cmd/observe/ui/dist/assets/traces-_fezYNMq.css.gz b/cmd/observe/ui/dist/assets/traces-_fezYNMq.css.gz index 3a4edab..a0b0ca5 100644 Binary files a/cmd/observe/ui/dist/assets/traces-_fezYNMq.css.gz and b/cmd/observe/ui/dist/assets/traces-_fezYNMq.css.gz differ diff --git a/cmd/observe/ui/dist/assets/useFilters-BPIsTX3T.js.gz b/cmd/observe/ui/dist/assets/useFilters-BPIsTX3T.js.gz index 80c9d75..ecd19f1 100644 Binary files a/cmd/observe/ui/dist/assets/useFilters-BPIsTX3T.js.gz and b/cmd/observe/ui/dist/assets/useFilters-BPIsTX3T.js.gz differ diff --git a/cmd/observe/ui/dist/favicon.svg.gz b/cmd/observe/ui/dist/favicon.svg.gz index a613758..9066aec 100644 Binary files a/cmd/observe/ui/dist/favicon.svg.gz and b/cmd/observe/ui/dist/favicon.svg.gz differ diff --git a/cmd/observe/ui/dist/index.html.gz b/cmd/observe/ui/dist/index.html.gz index f93f231..15ef0c6 100644 Binary files a/cmd/observe/ui/dist/index.html.gz and b/cmd/observe/ui/dist/index.html.gz differ diff --git a/cmd/observe/ui/dist/rrweb/replayer.js.gz b/cmd/observe/ui/dist/rrweb/replayer.js.gz index 77bd9cd..15f0916 100644 Binary files a/cmd/observe/ui/dist/rrweb/replayer.js.gz and b/cmd/observe/ui/dist/rrweb/replayer.js.gz differ diff --git a/cmd/observe/ui/dist/rrweb/sanitize.js.gz b/cmd/observe/ui/dist/rrweb/sanitize.js.gz index 81bd57f..62a0a6a 100644 Binary files a/cmd/observe/ui/dist/rrweb/sanitize.js.gz and b/cmd/observe/ui/dist/rrweb/sanitize.js.gz differ diff --git a/internal/config/config.go b/internal/config/config.go index 35fc60a..af11cbe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -33,6 +33,7 @@ type Config struct { // Retention RawRetentionDays int HourlyRetentionDays int + LLMRetentionDays int // Ledger retention (O01 ADR 5.8, decided 2026-09-23). Expiry means a // retried record is processed as new — no silent infinite exactly-once // promise beyond the window. @@ -95,6 +96,7 @@ func Load() Config { set("OBSERVE_FLUSH_SIZE", 500, &c.FlushSize) set("OBSERVE_RAW_RETENTION_DAYS", 30, &c.RawRetentionDays) set("OBSERVE_HOURLY_RETENTION_DAYS", 365, &c.HourlyRetentionDays) + set("OBSERVE_LLM_RETENTION_DAYS", 30, &c.LLMRetentionDays) // O01 slice 5 ledger retention (decided defaults 2026-09-23): the // error_inbox and replay_batches ledgers must not outlive the data they // dedupe (error_events 180d, replay_sessions 14d); derived_outbox @@ -168,6 +170,9 @@ func (c Config) Validate() error { if c.HourlyRetentionDays < 1 { return fmt.Errorf("OBSERVE_HOURLY_RETENTION_DAYS must be >= 1, got %d", c.HourlyRetentionDays) } + if c.LLMRetentionDays < 1 { + return fmt.Errorf("OBSERVE_LLM_RETENTION_DAYS must be >= 1, got %d", c.LLMRetentionDays) + } if c.ErrorInboxRetentionDays < 1 { return fmt.Errorf("OBSERVE_ERROR_INBOX_RETENTION_DAYS must be >= 1, got %d", c.ErrorInboxRetentionDays) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0167f80..51b23a6 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -24,6 +24,7 @@ func TestValidateRejectsBrokenNumericConfig(t *testing.T) { {"negative interval", func(c *Config) { c.FlushInterval = -time.Second }, "OBSERVE_FLUSH_INTERVAL_MS"}, {"huge interval", func(c *Config) { c.FlushInterval = 10 * time.Minute }, "OBSERVE_FLUSH_INTERVAL_MS"}, {"zero rate limit", func(c *Config) { c.RateLimit = 0 }, "OBSERVE_RATE_LIMIT"}, + {"zero LLM retention", func(c *Config) { c.LLMRetentionDays = 0 }, "OBSERVE_LLM_RETENTION_DAYS"}, {"zero retention", func(c *Config) { c.RawRetentionDays = 0 }, "OBSERVE_RAW_RETENTION_DAYS"}, {"malformed integer", func(c *Config) { c.parseErr = fmt.Errorf("OBSERVE_BUFFER_SIZE must be an integer, got %q", "big") @@ -32,7 +33,7 @@ func TestValidateRejectsBrokenNumericConfig(t *testing.T) { for _, tc := range cases { c := Config{ BufferSize: 1000, FlushSize: 100, FlushInterval: time.Second, - RateLimit: 100, RawRetentionDays: 30, HourlyRetentionDays: 365, + RateLimit: 100, RawRetentionDays: 30, HourlyRetentionDays: 365, LLMRetentionDays: 30, ErrorInboxRetentionDays: 14, ReplayBatchesRetentionDays: 14, DerivedOutboxRetentionDays: 7, } tc.mut(&c) @@ -46,10 +47,23 @@ func TestValidateRejectsBrokenNumericConfig(t *testing.T) { func TestValidateAcceptsDefaults(t *testing.T) { c := Config{ BufferSize: 100_000, FlushSize: 500, FlushInterval: 2 * time.Second, - RateLimit: 1000, RawRetentionDays: 30, HourlyRetentionDays: 365, + RateLimit: 1000, RawRetentionDays: 30, HourlyRetentionDays: 365, LLMRetentionDays: 30, ErrorInboxRetentionDays: 14, ReplayBatchesRetentionDays: 14, DerivedOutboxRetentionDays: 7, } if err := c.Validate(); err != nil { t.Fatalf("default-shaped config must validate: %v", err) } } + +func TestLoadLLMRetentionWindow(t *testing.T) { + t.Setenv("OBSERVE_LLM_RETENTION_DAYS", "7") + c := Load() + if c.LLMRetentionDays != 7 { + t.Fatalf("LLM retention=%d, want 7", c.LLMRetentionDays) + } + t.Setenv("OBSERVE_LLM_RETENTION_DAYS", "invalid") + c = Load() + if err := c.Validate(); err == nil || !strings.Contains(err.Error(), "OBSERVE_LLM_RETENTION_DAYS") { + t.Fatalf("invalid LLM duration must fail validation, got %v", err) + } +} diff --git a/internal/ingest/buffer_test.go b/internal/ingest/buffer_test.go index e738a81..09d9477 100644 --- a/internal/ingest/buffer_test.go +++ b/internal/ingest/buffer_test.go @@ -195,14 +195,15 @@ func TestBuffer_FlushFailureSurvivesRetryAndRestart(t *testing.T) { // wakeup is one bounded attempt, so Stop completes even with a dead DB. func TestBuffer_FlushFailureDoesNotLivelock(t *testing.T) { buf := NewBuffer(nil, 8, 2, time.Hour, slog.New(slog.NewTextHandler(io.Discard, nil))) - buf.Start() - defer buf.Stop() - + // Seed before starting: after its first flush panics, the worker correctly + // refuses admission. Racing those later pushes tests scheduling, not Stop. for i := 0; i < 4; i++ { if err := buf.Push(ev(fmt.Sprintf("e%d", i))); err != nil { t.Fatalf("push %d refused: %v", i, err) } } + buf.Start() + defer buf.Stop() // nil db -> insertBatch panics inside Flush, recovered by the worker's // panic guard, leaving the events requeued... in fact the panic aborts // the whole worker goroutine (recover logs and exits the goroutine), diff --git a/internal/jobs/retention.go b/internal/jobs/retention.go index a00e10b..0c040e0 100644 --- a/internal/jobs/retention.go +++ b/internal/jobs/retention.go @@ -47,10 +47,9 @@ type RetentionService struct { policies []RetentionPolicy } -// DefaultPolicies returns the out-of-box retention policies. Called with the two -// legacy env-configured durations for raw events + hourly rollups so existing -// deployments keep their behavior. -func DefaultPolicies(rawDays, hourlyDays int) []RetentionPolicy { +// DefaultPolicies returns the out-of-box retention policies, using configured +// durations for raw events, hourly rollups and LLM traces. +func DefaultPolicies(rawDays, hourlyDays, llmDays int) []RetentionPolicy { return []RetentionPolicy{ {Table: "events", Column: "timestamp", Days: rawDays}, {Table: "events_recent", Column: "timestamp", Days: 7}, @@ -58,6 +57,7 @@ func DefaultPolicies(rawDays, hourlyDays int) []RetentionPolicy { {Table: "sessions", Column: "last_ts", Days: 90}, {Table: "error_events", Column: "timestamp", Days: 180}, {Table: "logs", Column: "timestamp", Days: 30}, + {Table: "llm_traces", Column: "timestamp", Days: llmDays}, {Table: "spans", Column: "start_time", Days: 14}, {Table: "service_stats", Column: "ts_bucket", Days: 30}, {Table: "replay_sessions", Column: "start_time", Days: 14}, @@ -111,7 +111,7 @@ func PolicyDays(policies []RetentionPolicy, table string) int { // NewRetentionService keeps the old two-arg constructor for backwards compat. func NewRetentionService(db *nucleus.Client, logger *slog.Logger, rawDays, hourlyDays int) *RetentionService { - return NewRetentionServiceWithPolicies(db, logger, DefaultPolicies(rawDays, hourlyDays)) + return NewRetentionServiceWithPolicies(db, logger, DefaultPolicies(rawDays, hourlyDays, 30)) } // NewRetentionServiceWithPolicies allows callers to supply a fully custom policy set. diff --git a/internal/jobs/retention_test.go b/internal/jobs/retention_test.go index 6f3465c..e03a8ca 100644 --- a/internal/jobs/retention_test.go +++ b/internal/jobs/retention_test.go @@ -155,3 +155,57 @@ func TestRetentionPrunesProcessedOutboxIntentsNotDeadLetters(t *testing.T) { t.Errorf("dead-lettered intent was auto-pruned (got %d, want 1) — dead letters are the operator's queue", c) } } + +// Trace expiry must use the configured window without erasing the price catalog +// needed to explain historical cost estimates. +func TestLLMRetentionExpiresOldTracesPreservesRecentAndCatalog(t *testing.T) { + ctx, db, done := connect(t) + defer done() + site := "llm_ret_" + strconv.FormatInt(time.Now().UnixNano(), 36) + now := time.Now().UnixMilli() + const day = int64(24 * 60 * 60 * 1000) + for _, row := range []struct { + id string + ts int64 + }{{"old", now - 8*day}, {"recent", now - 6*day}} { + if _, err := db.SQL().Exec(ctx, `INSERT INTO llm_traces (trace_id, site_id, timestamp, prompt, completion) VALUES ($1,$2,$3,$4,$5)`, site+row.id, site, row.ts, "retained input", "retained output"); err != nil { + t.Fatal(err) + } + } + if _, err := db.SQL().Exec(ctx, `INSERT INTO llm_model_prices (model_prefix, created_at, valid_from) VALUES ($1,$2,$3)`, site, now-40*day, now-40*day); err != nil { + t.Fatal(err) + } + policies := DefaultPolicies(30, 365, 7) + var selected []RetentionPolicy + for _, p := range policies { + if p.Table == "llm_traces" { + selected = append(selected, p) + } + } + if len(selected) != 1 { + t.Fatalf("expected exactly one LLM policy, got %d", len(selected)) + } + svc := NewRetentionServiceWithPolicies(db, slog.New(slog.NewTextHandler(io.Discard, nil)), selected) + if err := svc.RunCleanup(ctx); err != nil { + t.Fatal(err) + } + for _, row := range []struct { + id string + want int64 + }{{"old", 0}, {"recent", 1}} { + got, err := nucleus.Query[countRow](ctx, db.SQL(), `SELECT COUNT(*) AS n FROM llm_traces WHERE trace_id=$1`, site+row.id) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].N != row.want { + t.Errorf("%s trace count=%v, want %d", row.id, got, row.want) + } + } + got, err := nucleus.Query[countRow](ctx, db.SQL(), `SELECT COUNT(*) AS n FROM llm_model_prices WHERE model_prefix=$1`, site) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].N != 1 { + t.Fatalf("price catalog was removed: %v", got) + } +} diff --git a/scripts/normalize-ui-metadata.mjs b/scripts/normalize-ui-metadata.mjs new file mode 100644 index 0000000..5a5fcb9 --- /dev/null +++ b/scripts/normalize-ui-metadata.mjs @@ -0,0 +1,44 @@ +// Embedding does not need build wall-clock timestamps. Neutron currently emits +// them unconditionally (reported in Teploy/_internal/UPSTREAM_BUGS.md). +// Preserve policy content and accurate compression counts while removing time. +import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { gzipSync, brotliCompressSync, constants } from 'node:zlib'; + +if (!process.argv[2]) throw new Error('usage: node scripts/normalize-ui-metadata.mjs '); +const dist = resolve(process.argv[2]); +const policyPath = resolve(dist, '.neutron-static-policy.json'); +const metaPath = resolve(dist, '.neutron-adapter-static.json'); +const original = readFileSync(policyPath); +const policy = JSON.parse(original); +const meta = JSON.parse(readFileSync(metaPath)); +delete policy.generatedAt; +delete meta.generatedAt; +const normalized = Buffer.from(JSON.stringify(policy, null, 2)); +for (const [suffix, field, compress] of [ + ['gz', 'gzipBytesSaved', b => gzipSync(b, { level: 9 })], + ['br', 'brotliBytesSaved', b => brotliCompressSync(b, { params: { [constants.BROTLI_PARAM_QUALITY]: 11 } })], +]) { + const previous = readFileSync(`${policyPath}.${suffix}`); + const compressed = compress(normalized); + meta.compression[field] += (normalized.length - compressed.length) - (original.length - previous.length); + writeFileSync(`${policyPath}.${suffix}`, compressed); +} +writeFileSync(policyPath, normalized); +writeFileSync(metaPath, JSON.stringify(meta, null, 2)); + +// gzip's OS header differs on macOS/Linux despite identical deflate payloads. +// RFC 1952 value 255 means unknown OS; this byte is outside the payload CRC. +function normalizeGzipHeaders(dir) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = resolve(dir, entry.name); + if (entry.isDirectory()) normalizeGzipHeaders(path); + else if (entry.name.endsWith('.gz')) { + const bytes = readFileSync(path); + if (bytes[0] !== 0x1f || bytes[1] !== 0x8b || bytes[3] !== 0) throw new Error(`Unexpected gzip header: ${path}`); + bytes[9] = 255; + writeFileSync(path, bytes); + } + } +} +normalizeGzipHeaders(dist); diff --git a/scripts/ui-sync.sh b/scripts/ui-sync.sh index dc0f814..b7831cc 100755 --- a/scripts/ui-sync.sh +++ b/scripts/ui-sync.sh @@ -8,7 +8,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OBSERVE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" OBSERVE_UI_SRC="$OBSERVE_ROOT/ui/src" -CANONICAL_APP="$OBSERVE_ROOT/../../Neutron/typescript/apps/observe" +NEUTRON_ROOT="${NEUTRON_ROOT:-$OBSERVE_ROOT/../../Neutron}" +CANONICAL_APP="$NEUTRON_ROOT/typescript/apps/observe" CANONICAL_SRC="$CANONICAL_APP/src" EMBED_DIST="$OBSERVE_ROOT/cmd/observe/ui/dist" BIN_OUT="${OBSERVE_BIN:-/tmp/obs-launch/observe}" @@ -58,6 +59,8 @@ find "$CANONICAL_SRC" -type f \( -name '*.ts' -o -name '*.tsx' \) -print0 \ log "pnpm build (canonical)" pnpm -C "$CANONICAL_APP" build +node "$SCRIPT_DIR/normalize-ui-metadata.mjs" "$CANONICAL_APP/dist" + log "replacing embedded dist" rm -rf "$EMBED_DIST" mkdir -p "$EMBED_DIST"