Repository navigation
Fix analytics history and log token accounting - #2904
diegosouzapw merged 4 commits into
Conversation
…on window - Add rollupUsageHistoryBeforeDate() to aggregate usage_history rows into daily_usage_summary before they are deleted by cleanupUsageHistory() - Update cleanupUsageHistory() to run the rollup before DELETE, ensuring historical token data is preserved across retention boundaries - Update /api/usage/analytics to UNION usage_history (recent) with daily_usage_summary (older) so the dashboard shows full history even after raw rows are purged by the aggregation retention policy - Raise rawDataRetentionDays Zod cap from 90 to 3650 days Previously, when aggregation.rawDataRetentionDays was set (default: 30, users reported 7), the cleanup job deleted usage_history rows but never wrote them to daily_usage_summary. The analytics route only queried usage_history, so any data older than rawDataRetentionDays disappeared from /dashboard/analytics regardless of the retention.usageHistory setting.
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to roll up historical usage data from usage_history into daily_usage_summary before cleanup, and updates the analytics route to query a unified source merging raw and aggregated data. It also refines token accounting to prefer prompt_tokens over input_tokens to prevent double-counting. Feedback on these changes highlights critical SQL injection vulnerabilities and double-counting risks in the analytics route due to direct string interpolation and lack of boundary checks. Additionally, the reviewer noted that deleting sidebar settings from the schema will cause validation failures, proceeding with cleanup after a failed rollup risks data loss, and using DATE(timestamp) in the rollup query bypasses database indexes.
| const rawConditions: string[] = []; | ||
| if (sinceIso) rawConditions.push("timestamp >= @since"); | ||
| if (untilIso) rawConditions.push("timestamp <= @until"); | ||
| if (apiKeyWhere) rawConditions.push(apiKeyWhere); | ||
| const rawWhere = rawConditions.length > 0 ? `WHERE ${rawConditions.join(" AND ")}` : ""; | ||
|
|
||
| // Aggregated rows only span dates within the requested window (no api_key filter). | ||
| const aggConditions: string[] = []; | ||
| if (sinceIso) { | ||
| // Use date comparison on the summary's date column (YYYY-MM-DD). | ||
| const sinceDate = sinceIso.split("T")[0]; | ||
| aggConditions.push(`date >= '${sinceDate}'`); | ||
| } | ||
| if (untilIso) { | ||
| const untilDate = untilIso.split("T")[0]; | ||
| aggConditions.push(`date <= '${untilDate}'`); | ||
| } | ||
| const aggWhere = aggConditions.length > 0 ? `WHERE ${aggConditions.join(" AND ")}` : ""; |
There was a problem hiding this comment.
SQL Injection & Data Leakage / Double Counting Vulnerability
- SQL Injection: Direct string interpolation of
sinceIsoanduntilIsosplits into the SQL query (date >= '${sinceDate}') creates a SQL injection vulnerability because these values originate from user-controlled query parameters (startDateandendDate). - Double Counting & Data Leakage: Without a boundary check, the query will select overlapping rows from both
usage_historyanddaily_usage_summaryfor the recent period. Furthermore, sincedaily_usage_summarydoes not store API key or connection details, any query filtered by API key will leak other keys' aggregated usage for the overlapping period.
Fix: Use parameter binding for the date parameters and restrict the queries using the rawCutoff boundary to ensure non-overlapping datasets.
const rawConditions: string[] = [];
if (needsAggregated) {
rawConditions.push("timestamp >= @rawCutoff");
params.rawCutoff = rawCutoffIso;
} else if (sinceIso) {
rawConditions.push("timestamp >= @since");
}
if (untilIso) rawConditions.push("timestamp <= @until");
if (apiKeyWhere) rawConditions.push(apiKeyWhere);
const rawWhere = rawConditions.length > 0 ? `WHERE ${rawConditions.join(" AND ")}` : "";
// Aggregated rows only span dates within the requested window and strictly before the raw cutoff
// to prevent double-counting and data leakage (since api_key/connection filters don't apply to daily_usage_summary).
const aggConditions: string[] = [];
if (sinceIso) {
aggConditions.push("date >= @sinceDate");
params.sinceDate = sinceIso.split("T")[0];
}
if (untilIso) {
aggConditions.push("date <= @untilDate");
params.untilDate = untilIso.split("T")[0];
}
aggConditions.push("date < @rawCutoffDate");
params.rawCutoffDate = rawCutoffIso.split("T")[0];
const aggWhere = aggConditions.length > 0 ? `WHERE ${aggConditions.join(" AND ")}` : "";References
- Never write raw SQL in routes — always go through src/lib/db/ domain modules. (link)
| const presetRawConds: string[] = []; | ||
| if (presetSinceIso) { | ||
| presetConditions.push("timestamp >= @presetSince"); | ||
| presetRawConds.push("timestamp >= @presetSince"); | ||
| presetParams.presetSince = presetSinceIso; | ||
| } | ||
| if (apiKeyWhere) { | ||
| presetConditions.push(apiKeyWhere); | ||
| presetRawConds.push(apiKeyWhere); | ||
| Object.assign(presetParams, params); | ||
| } | ||
| const presetRawWhere = | ||
| presetRawConds.length > 0 ? `WHERE ${presetRawConds.join(" AND ")}` : ""; | ||
|
|
||
| const presetWhere = | ||
| presetConditions.length > 0 ? `WHERE ${presetConditions.join(" AND ")}` : ""; | ||
| const presetAggConds: string[] = []; | ||
| if (presetSinceIso) { | ||
| presetAggConds.push(`date >= '${presetSinceIso.split("T")[0]}'`); | ||
| } | ||
| const presetAggWhere = | ||
| presetAggConds.length > 0 ? `WHERE ${presetAggConds.join(" AND ")}` : ""; |
There was a problem hiding this comment.
SQL Injection & Double Counting in Preset Query
This block suffers from the same SQL injection and double-counting vulnerabilities as the main query. Direct string interpolation is used for presetSinceIso splits, and there is no boundary check to prevent overlapping data between usage_history and daily_usage_summary.
Fix: Apply parameter binding and restrict the datasets using the rawCutoff boundary.
const presetRawConds: string[] = [];
if (presetNeedsAggregated) {
presetRawConds.push("timestamp >= @presetRawCutoff");
presetParams.presetRawCutoff = rawCutoffIso;
} else if (presetSinceIso) {
presetRawConds.push("timestamp >= @presetSince");
presetParams.presetSince = presetSinceIso;
}
if (apiKeyWhere) {
presetRawConds.push(apiKeyWhere);
Object.assign(presetParams, params);
}
const presetRawWhere =
presetRawConds.length > 0 ? `WHERE ${presetRawConds.join(" AND ")}` : "";
const presetAggConds: string[] = [];
if (presetSinceIso) {
presetAggConds.push("date >= @presetSinceDate");
presetParams.presetSinceDate = presetSinceIso.split("T")[0];
}
presetAggConds.push("date < @presetRawCutoffDate");
presetParams.presetRawCutoffDate = rawCutoffIso.split("T")[0];
const presetAggWhere =
presetAggConds.length > 0 ? `WHERE ${presetAggConds.join(" AND ")}` : "";References
- Never write raw SQL in routes — always go through src/lib/db/ domain modules. (link)
| import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode"; | ||
| import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize"; | ||
| import { HIDEABLE_SIDEBAR_ITEM_IDS, SIDEBAR_SECTIONS } from "@/shared/constants/sidebarVisibility"; | ||
| import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; |
There was a problem hiding this comment.
Accidental Deletion of Sidebar Settings Schema Import
The import of SIDEBAR_SECTIONS was deleted from settingsSchemas.ts. This will cause a compilation/runtime error once the deleted sidebar fields are restored to the schema.
Fix: Restore the import of SIDEBAR_SECTIONS.
| import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; | |
| import { HIDEABLE_SIDEBAR_ITEM_IDS, SIDEBAR_SECTIONS } from "@/shared/constants/sidebarVisibility"; |
| hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(), | ||
| sidebarSectionOrder: z | ||
| .array(z.enum(SIDEBAR_SECTIONS.map((s) => s.id) as [string, ...string[]])) | ||
| .optional(), | ||
| sidebarItemOrder: z.record(z.string(), z.array(z.string().max(100))).optional(), | ||
| sidebarActivePreset: z.enum(["all", "minimal", "developer", "admin"]).nullable().optional(), | ||
| comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(), |
There was a problem hiding this comment.
Restore Sidebar Settings Schema Fields
The sidebar settings fields (sidebarSectionOrder, sidebarItemOrder, and sidebarActivePreset) were deleted from updateSettingsSchema. Since no corresponding UI changes are made in this PR, any attempts by the UI to save or update these settings will now fail Zod validation with a 400 Bad Request error.
Fix: Restore the deleted fields to the schema.
hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(),
sidebarSectionOrder: z
.array(z.enum(SIDEBAR_SECTIONS.map((s) => s.id) as [string, ...string[]]))
.optional(),
sidebarItemOrder: z.record(z.string(), z.array(z.string().max(100))).optional(),
sidebarActivePreset: z.enum(["all", "minimal", "developer", "admin"]).nullable().optional(),
comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(),| try { | ||
| // Roll up rows that are about to be deleted into daily_usage_summary so that | ||
| // the analytics route can still surface historical data via the UNION query. | ||
| await rollupUsageHistoryBeforeDate(cutoffDateStr); | ||
| } catch (err: unknown) { | ||
| // Non-fatal: log but continue with deletion so cleanup still runs. | ||
| console.error("[Cleanup] Error rolling up usage_history before deletion:", err); | ||
| } |
There was a problem hiding this comment.
Data Loss Risk on Rollup Failure
If rollupUsageHistoryBeforeDate fails (e.g., due to database locks or disk issues), the cleanup process currently logs the error but still proceeds to execute DELETE FROM usage_history. This will result in permanent loss of raw usage data without any aggregated backup.
Furthermore, the catch block here is dead code because rollupUsageHistoryBeforeDate internally catches its own errors and returns an AggregationResult object with errors > 0 instead of throwing.
Fix: Check the returned rollupResult.errors and abort the deletion if any errors occurred.
const rollupResult = await rollupUsageHistoryBeforeDate(cutoffDateStr);
if (rollupResult.errors > 0) {
console.error("[Cleanup] Aborting usage_history deletion because rollup failed.");
result.errors += rollupResult.errors;
return result;
}| COALESCE(SUM(tokens_output), 0) as total_output_tokens, | ||
| 0.0 as total_cost | ||
| FROM usage_history | ||
| WHERE DATE(timestamp) < ? |
There was a problem hiding this comment.
Performance Bottleneck: Index Bypassed in WHERE Clause
Using DATE(timestamp) < ? in the WHERE clause prevents SQLite from using any index on the timestamp column, forcing a full table scan of usage_history. Since usage_history can grow extremely large, this will cause severe performance degradation during cleanup.
Since beforeDate is a standard YYYY-MM-DD string, a direct string comparison timestamp < ? is perfectly correct and allows SQLite to perform an efficient index range scan.
Fix: Compare timestamp directly.
| WHERE DATE(timestamp) < ? | |
| WHERE timestamp < ? |
efcd062
into
diegosouzapw:release/v3.8.6
|
Thank you @unitythemaker for your contribution! This has been successfully merged into the release branch (with date parameters in the analytics route query parameterized to secure against SQL injection) and will be included in the next release. |
Integrated into release/v3.8.6
…l_logs only The 3.8.6 variant of #2904 added SELECTs of combo_name/requested_model against usage_history, but those columns only exist in call_logs (no migration adds them to usage_history). This returned HTTP 500 on /api/usage/analytics. Restore the working query shape from the 3.8.7 variant. Fixes 18 failing usage-analytics-route tests.
…owup-v387 Fix analytics follow-up regressions from #2904
* feat(plugins): WordPress-style plugin system backend
* fix(plugins): address code review feedback
- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): replace vm with child_process, add auth to all routes
Addresses all remaining code review feedback:
1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
for proper process-level isolation. Complies with Rule 3 (no eval).
Each plugin runs in a separate Node.js process with IPC communication.
2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
API route files (list, install, scan, details, activate, deactivate, config).
3. **Env filtering**: Only safe env vars passed to plugin processes unless
"env" permission is granted.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): security + ESM fixes for loader and manager
loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)
manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)
hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)
* feat(discovery): add discovery tool stub service
Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)
* chore(plugins): slop cleanup — pino logger, remove redundant sorts
- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import
* test(plugins): add scanner, loader, manager unit tests
- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing
* fix(settings): add missing home page pin keys to updateSettingsSchema
* feat(plugins): add i18n keys to all 42 locales
* fix(settings): add missing security keys to updateSettingsSchema and add tests
* fix(usage): analytics route reads combo_name/requested_model from call_logs only
The 3.8.6 variant of #2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.
* fix(types,test): resolve noImplicitAny in progressiveAging + align semaphore test to #2903 gate pruning
- progressiveAging: type compression results so messages[0].content is
indexable (was TS7053 against {}); restores typecheck:noimplicit:core gate.
- services-branch-hardening: #2903 (perf-ram) prunes idle rate-limit gates
on zero; assert no-running/empty-queue without assuming the entry persists.
* fix(analytics): address merged review regressions
* fix(executor): normalize max effort for openai shape providers
* Make zero-latency combo optimizations opt-in
* Address zero-latency combo review feedback
* chore(release): sync v3.8.7 touchpoints + credit contributors
- llm.txt → 3.8.7 (Current version + Key Features header)
- CHANGELOG: add Dmitry Kuznetsov & Nikolay Alafuzov to 3.8.6 Hall of Contributors
- version already 3.8.7 across package.json/open-sse/electron/openapi (from #2909)
* fix(cleanup): restore usage history cutoff boundary
* docs(changelog): rank 3.8.6 contributors in a commits table with their PRs
* fix(dashboard): theme ReactFlow Controls +/- buttons for dark mode
* fix(settings): add missing home page pin keys to updateSettingsSchema
* fix(settings): add missing security keys to updateSettingsSchema and add tests
* fix(executor): normalize max effort for openai shape providers
* Make zero-latency combo optimizations opt-in
* Address zero-latency combo review feedback
* fix(analytics): address merged review regressions
* fix(cleanup): restore usage history cutoff boundary
* feat(plugins): WordPress-style plugin system backend
* fix(plugins): address code review feedback
- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): replace vm with child_process, add auth to all routes
Addresses all remaining code review feedback:
1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
for proper process-level isolation. Complies with Rule 3 (no eval).
Each plugin runs in a separate Node.js process with IPC communication.
2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
API route files (list, install, scan, details, activate, deactivate, config).
3. **Env filtering**: Only safe env vars passed to plugin processes unless
"env" permission is granted.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): security + ESM fixes for loader and manager
loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)
manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)
hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)
* feat(discovery): add discovery tool stub service
Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)
* chore(plugins): slop cleanup — pino logger, remove redundant sorts
- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import
* test(plugins): add scanner, loader, manager unit tests
- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing
* feat(plugins): add i18n keys to all 42 locales
* chore(plugins): remove duplicate migration 059_create_plugins.sql
* chore(plugins): remove duplicate migration 059_create_plugins.sql (post-merge)
* fix(sse): guard non-string error.code in proxyFetch + harden model parsing (#2463) (#2923)
Integrated into release/v3.8.7
* fix(docker): add runner-web stage with Playwright Chromium (#2832) (#2846)
Integrated into release/v3.8.7
* docs(changelog): document NVIDIA NIM and error code type-crash fix (#2463)
* test: ignore NVIDIA_BASE_URL and NVIDIA_MODEL in env contract check
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Apostol Apostolov <theapoapostolov@gmail.com>
Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com>
Co-authored-by: R.D. <rogerproself@gmail.com>
Integrated into release/v3.8.6
Integrated into release/v3.8.6
Hotfixes da release/v3.8.7 (perf RAM diegosouzapw#2903, self-service diegosouzapw#2908, analytics diegosouzapw#2904, bump 3.8.7, docs) na main consolidada com 3.8.6.
* feat(plugins): WordPress-style plugin system backend
* fix(plugins): address code review feedback
- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): replace vm with child_process, add auth to all routes
Addresses all remaining code review feedback:
1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
for proper process-level isolation. Complies with Rule 3 (no eval).
Each plugin runs in a separate Node.js process with IPC communication.
2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
API route files (list, install, scan, details, activate, deactivate, config).
3. **Env filtering**: Only safe env vars passed to plugin processes unless
"env" permission is granted.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): security + ESM fixes for loader and manager
loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)
manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)
hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)
* feat(discovery): add discovery tool stub service
Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)
* chore(plugins): slop cleanup — pino logger, remove redundant sorts
- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import
* test(plugins): add scanner, loader, manager unit tests
- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing
* fix(settings): add missing home page pin keys to updateSettingsSchema
* feat(plugins): add i18n keys to all 42 locales
* fix(settings): add missing security keys to updateSettingsSchema and add tests
* fix(usage): analytics route reads combo_name/requested_model from call_logs only
The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.
* fix(types,test): resolve noImplicitAny in progressiveAging + align semaphore test to diegosouzapw#2903 gate pruning
- progressiveAging: type compression results so messages[0].content is
indexable (was TS7053 against {}); restores typecheck:noimplicit:core gate.
- services-branch-hardening: diegosouzapw#2903 (perf-ram) prunes idle rate-limit gates
on zero; assert no-running/empty-queue without assuming the entry persists.
* fix(analytics): address merged review regressions
* fix(executor): normalize max effort for openai shape providers
* Make zero-latency combo optimizations opt-in
* Address zero-latency combo review feedback
* chore(release): sync v3.8.7 touchpoints + credit contributors
- llm.txt → 3.8.7 (Current version + Key Features header)
- CHANGELOG: add Dmitry Kuznetsov & Nikolay Alafuzov to 3.8.6 Hall of Contributors
- version already 3.8.7 across package.json/open-sse/electron/openapi (from diegosouzapw#2909)
* fix(cleanup): restore usage history cutoff boundary
* docs(changelog): rank 3.8.6 contributors in a commits table with their PRs
* fix(dashboard): theme ReactFlow Controls +/- buttons for dark mode
* fix(settings): add missing home page pin keys to updateSettingsSchema
* fix(settings): add missing security keys to updateSettingsSchema and add tests
* fix(executor): normalize max effort for openai shape providers
* Make zero-latency combo optimizations opt-in
* Address zero-latency combo review feedback
* fix(analytics): address merged review regressions
* fix(cleanup): restore usage history cutoff boundary
* feat(plugins): WordPress-style plugin system backend
* fix(plugins): address code review feedback
- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): replace vm with child_process, add auth to all routes
Addresses all remaining code review feedback:
1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
for proper process-level isolation. Complies with Rule 3 (no eval).
Each plugin runs in a separate Node.js process with IPC communication.
2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
API route files (list, install, scan, details, activate, deactivate, config).
3. **Env filtering**: Only safe env vars passed to plugin processes unless
"env" permission is granted.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): security + ESM fixes for loader and manager
loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)
manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)
hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)
* feat(discovery): add discovery tool stub service
Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)
* chore(plugins): slop cleanup — pino logger, remove redundant sorts
- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import
* test(plugins): add scanner, loader, manager unit tests
- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing
* feat(plugins): add i18n keys to all 42 locales
* chore(plugins): remove duplicate migration 059_create_plugins.sql
* chore(plugins): remove duplicate migration 059_create_plugins.sql (post-merge)
* fix(sse): guard non-string error.code in proxyFetch + harden model parsing (diegosouzapw#2463) (diegosouzapw#2923)
Integrated into release/v3.8.7
* fix(docker): add runner-web stage with Playwright Chromium (diegosouzapw#2832) (diegosouzapw#2846)
Integrated into release/v3.8.7
* docs(changelog): document NVIDIA NIM and error code type-crash fix (diegosouzapw#2463)
* test: ignore NVIDIA_BASE_URL and NVIDIA_MODEL in env contract check
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Apostol Apostolov <theapoapostolov@gmail.com>
Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com>
Co-authored-by: R.D. <rogerproself@gmail.com>
…l_logs only The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model against usage_history, but those columns only exist in call_logs (no migration adds them to usage_history). This returned HTTP 500 on /api/usage/analytics. Restore the working query shape from the 3.8.7 variant. Fixes 18 failing usage-analytics-route tests.
Integrated into release/v3.8.6
Integrated into release/v3.8.6
Hotfixes da release/v3.8.7 (perf RAM diegosouzapw#2903, self-service diegosouzapw#2908, analytics diegosouzapw#2904, bump 3.8.7, docs) na main consolidada com 3.8.6.
* feat(plugins): WordPress-style plugin system backend
* fix(plugins): address code review feedback
- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): replace vm with child_process, add auth to all routes
Addresses all remaining code review feedback:
1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
for proper process-level isolation. Complies with Rule 3 (no eval).
Each plugin runs in a separate Node.js process with IPC communication.
2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
API route files (list, install, scan, details, activate, deactivate, config).
3. **Env filtering**: Only safe env vars passed to plugin processes unless
"env" permission is granted.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): security + ESM fixes for loader and manager
loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)
manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)
hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)
* feat(discovery): add discovery tool stub service
Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)
* chore(plugins): slop cleanup — pino logger, remove redundant sorts
- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import
* test(plugins): add scanner, loader, manager unit tests
- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing
* fix(settings): add missing home page pin keys to updateSettingsSchema
* feat(plugins): add i18n keys to all 42 locales
* fix(settings): add missing security keys to updateSettingsSchema and add tests
* fix(usage): analytics route reads combo_name/requested_model from call_logs only
The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.
* fix(types,test): resolve noImplicitAny in progressiveAging + align semaphore test to diegosouzapw#2903 gate pruning
- progressiveAging: type compression results so messages[0].content is
indexable (was TS7053 against {}); restores typecheck:noimplicit:core gate.
- services-branch-hardening: diegosouzapw#2903 (perf-ram) prunes idle rate-limit gates
on zero; assert no-running/empty-queue without assuming the entry persists.
* fix(analytics): address merged review regressions
* fix(executor): normalize max effort for openai shape providers
* Make zero-latency combo optimizations opt-in
* Address zero-latency combo review feedback
* chore(release): sync v3.8.7 touchpoints + credit contributors
- llm.txt → 3.8.7 (Current version + Key Features header)
- CHANGELOG: add Dmitry Kuznetsov & Nikolay Alafuzov to 3.8.6 Hall of Contributors
- version already 3.8.7 across package.json/open-sse/electron/openapi (from diegosouzapw#2909)
* fix(cleanup): restore usage history cutoff boundary
* docs(changelog): rank 3.8.6 contributors in a commits table with their PRs
* fix(dashboard): theme ReactFlow Controls +/- buttons for dark mode
* fix(settings): add missing home page pin keys to updateSettingsSchema
* fix(settings): add missing security keys to updateSettingsSchema and add tests
* fix(executor): normalize max effort for openai shape providers
* Make zero-latency combo optimizations opt-in
* Address zero-latency combo review feedback
* fix(analytics): address merged review regressions
* fix(cleanup): restore usage history cutoff boundary
* feat(plugins): WordPress-style plugin system backend
* fix(plugins): address code review feedback
- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): replace vm with child_process, add auth to all routes
Addresses all remaining code review feedback:
1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
for proper process-level isolation. Complies with Rule 3 (no eval).
Each plugin runs in a separate Node.js process with IPC communication.
2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
API route files (list, install, scan, details, activate, deactivate, config).
3. **Env filtering**: Only safe env vars passed to plugin processes unless
"env" permission is granted.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): security + ESM fixes for loader and manager
loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)
manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)
hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)
* feat(discovery): add discovery tool stub service
Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)
* chore(plugins): slop cleanup — pino logger, remove redundant sorts
- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import
* test(plugins): add scanner, loader, manager unit tests
- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing
* feat(plugins): add i18n keys to all 42 locales
* chore(plugins): remove duplicate migration 059_create_plugins.sql
* chore(plugins): remove duplicate migration 059_create_plugins.sql (post-merge)
* fix(sse): guard non-string error.code in proxyFetch + harden model parsing (diegosouzapw#2463) (diegosouzapw#2923)
Integrated into release/v3.8.7
* fix(docker): add runner-web stage with Playwright Chromium (diegosouzapw#2832) (diegosouzapw#2846)
Integrated into release/v3.8.7
* docs(changelog): document NVIDIA NIM and error code type-crash fix (diegosouzapw#2463)
* test: ignore NVIDIA_BASE_URL and NVIDIA_MODEL in env contract check
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Apostol Apostolov <theapoapostolov@gmail.com>
Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com>
Co-authored-by: R.D. <rogerproself@gmail.com>
…l_logs only The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model against usage_history, but those columns only exist in call_logs (no migration adds them to usage_history). This returned HTTP 500 on /api/usage/analytics. Restore the working query shape from the 3.8.7 variant. Fixes 18 failing usage-analytics-route tests.
…-gemini-followup-v387 Fix analytics follow-up regressions from diegosouzapw#2904
Integrated into release/v3.8.6
Integrated into release/v3.8.6
Hotfixes da release/v3.8.7 (perf RAM diegosouzapw#2903, self-service diegosouzapw#2908, analytics diegosouzapw#2904, bump 3.8.7, docs) na main consolidada com 3.8.6.
* feat(plugins): WordPress-style plugin system backend
* fix(plugins): address code review feedback
- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): replace vm with child_process, add auth to all routes
Addresses all remaining code review feedback:
1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
for proper process-level isolation. Complies with Rule 3 (no eval).
Each plugin runs in a separate Node.js process with IPC communication.
2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
API route files (list, install, scan, details, activate, deactivate, config).
3. **Env filtering**: Only safe env vars passed to plugin processes unless
"env" permission is granted.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): security + ESM fixes for loader and manager
loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)
manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)
hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)
* feat(discovery): add discovery tool stub service
Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)
* chore(plugins): slop cleanup — pino logger, remove redundant sorts
- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import
* test(plugins): add scanner, loader, manager unit tests
- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing
* fix(settings): add missing home page pin keys to updateSettingsSchema
* feat(plugins): add i18n keys to all 42 locales
* fix(settings): add missing security keys to updateSettingsSchema and add tests
* fix(usage): analytics route reads combo_name/requested_model from call_logs only
The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model
against usage_history, but those columns only exist in call_logs (no
migration adds them to usage_history). This returned HTTP 500 on
/api/usage/analytics. Restore the working query shape from the 3.8.7
variant. Fixes 18 failing usage-analytics-route tests.
* fix(types,test): resolve noImplicitAny in progressiveAging + align semaphore test to diegosouzapw#2903 gate pruning
- progressiveAging: type compression results so messages[0].content is
indexable (was TS7053 against {}); restores typecheck:noimplicit:core gate.
- services-branch-hardening: diegosouzapw#2903 (perf-ram) prunes idle rate-limit gates
on zero; assert no-running/empty-queue without assuming the entry persists.
* fix(analytics): address merged review regressions
* fix(executor): normalize max effort for openai shape providers
* Make zero-latency combo optimizations opt-in
* Address zero-latency combo review feedback
* chore(release): sync v3.8.7 touchpoints + credit contributors
- llm.txt → 3.8.7 (Current version + Key Features header)
- CHANGELOG: add Dmitry Kuznetsov & Nikolay Alafuzov to 3.8.6 Hall of Contributors
- version already 3.8.7 across package.json/open-sse/electron/openapi (from diegosouzapw#2909)
* fix(cleanup): restore usage history cutoff boundary
* docs(changelog): rank 3.8.6 contributors in a commits table with their PRs
* fix(dashboard): theme ReactFlow Controls +/- buttons for dark mode
* fix(settings): add missing home page pin keys to updateSettingsSchema
* fix(settings): add missing security keys to updateSettingsSchema and add tests
* fix(executor): normalize max effort for openai shape providers
* Make zero-latency combo optimizations opt-in
* Address zero-latency combo review feedback
* fix(analytics): address merged review regressions
* fix(cleanup): restore usage history cutoff boundary
* feat(plugins): WordPress-style plugin system backend
* fix(plugins): address code review feedback
- Path traversal guard: validate entryPoint stays within plugin dir
- install() now handles direct plugin directories (not just parent dirs)
- Non-null assertion replaced with explicit null check
- require efficiency: allowedModules map moved outside function
- Source wrapper: add newlines to prevent trailing comment issues
- Config validation: validate values against configSchema on save
- Dynamic import comment: clarify Node.js caching behavior
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): replace vm with child_process, add auth to all routes
Addresses all remaining code review feedback:
1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork()
for proper process-level isolation. Complies with Rule 3 (no eval).
Each plugin runs in a separate Node.js process with IPC communication.
2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin
API route files (list, install, scan, details, activate, deactivate, config).
3. **Env filtering**: Only safe env vars passed to plugin processes unless
"env" permission is granted.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(plugins): security + ESM fixes for loader and manager
loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)
manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)
hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)
* feat(discovery): add discovery tool stub service
Phase 1 scaffold for automated provider discovery:
- DiscoveryConfig, DiscoveryResult types
- probeEndpoint() for URL availability checking
- scanProvider() stub (Phase 2 will implement real scanning)
- getDiscoveryResults() stub
- Default config: disabled (opt-in)
* chore(plugins): slop cleanup — pino logger, remove redundant sorts
- index.ts: replace console.log/error with pino structured logging
- hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration)
- manager.ts: add readFile import
* test(plugins): add scanner, loader, manager unit tests
- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple)
- loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces)
- manager: 6 tests (singleton, lifecycle methods, error on unknown)
- Total: 20 tests, all passing
* feat(plugins): add i18n keys to all 42 locales
* chore(plugins): remove duplicate migration 059_create_plugins.sql
* chore(plugins): remove duplicate migration 059_create_plugins.sql (post-merge)
* fix(sse): guard non-string error.code in proxyFetch + harden model parsing (diegosouzapw#2463) (diegosouzapw#2923)
Integrated into release/v3.8.7
* fix(docker): add runner-web stage with Playwright Chromium (diegosouzapw#2832) (diegosouzapw#2846)
Integrated into release/v3.8.7
* docs(changelog): document NVIDIA NIM and error code type-crash fix (diegosouzapw#2463)
* test: ignore NVIDIA_BASE_URL and NVIDIA_MODEL in env contract check
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: Apostol Apostolov <theapoapostolov@gmail.com>
Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com>
Co-authored-by: R.D. <rogerproself@gmail.com>
…l_logs only The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model against usage_history, but those columns only exist in call_logs (no migration adds them to usage_history). This returned HTTP 500 on /api/usage/analytics. Restore the working query shape from the 3.8.7 variant. Fixes 18 failing usage-analytics-route tests.
Summary
Two narrow fixes on top of
release/v3.8.6:src/app/api/usage/analytics/route.ts,src/lib/usage/aggregateHistory.ts,src/lib/db/cleanup.ts,src/shared/validation/settingsSchemas.ts)usage_history, so once the configured raw-retention window (default 30 days) elapses andcleanupdeletes those rows, the dashboard showed empty/short history even thoughdaily_usage_summaryalready held the aggregated data created byaggregateHistory.UNIONsusage_history(recent, raw) withdaily_usage_summary(older, aggregated) in a single source query, then groups by day / provider / model /serviceTier.daily_usage_summaryrows that pre-date service-tier tracking are reported as'standard'.src/lib/usage/tokenAccounting.ts)getLoggedInputTokenspreviously hit theinput_tokensbranch first and addedcache_read_input_tokens + cache_creation_input_tokenson top. For Claude streaming responses translated to OpenAI shape, the translator emits bothprompt_tokens(already containing the full input + cache totals) and keepsinput_tokensas a compatibility alias. The old order double-counted the cache portion — e.g. a 600k-token request was logged as ~1.2MTIon the Logs page.tokens.input→prompt_tokens→ (input_tokens+ cache fields). The third branch is unchanged and still handles raw Anthropic /anthropic-compatible-ccstreaming whereinput_tokensis only the non-cached portion.prompt_tokens=600000+input_tokens=600000+cache_read_input_tokens=600000→600000(not1_200_000).Related Issues
getLoggedInputTokenstrustingprompt_tokensas the full input total)Validation
npm run lint— 0 errors, 2985 pre-existing warnings repo-wide; zero findings on files touched by this PRnpm run test:unit— targeted:node --import tsx --import ./open-sse/utils/setupPolyfill.ts --test tests/unit/token-accounting-input-fix.test.ts→ 9 pass / 0 failnpm run test:coverage>= 60%for statements, lines, functions, and branchesTests Added Or Updated
tests/unit/token-accounting-input-fix.test.ts— rewritten to import the realgetLoggedInputTokensfromsrc/lib/usage/tokenAccounting.ts(previously asserted against a local re-implementation that masked the bug) and extended with a Claude-streaming regression: mixedprompt_tokens+input_tokens+cache_read_input_tokensmust not double-count.No other test files changed. The analytics history fix is exercised through the existing
aggregateHistory/ cleanup paths; a dedicated test for the union behavior is a sensible follow-up if reviewers prefer.Coverage Notes
src/lib/usage/tokenAccounting.ts: the new branch ordering is covered by the regression test plus the existing cases for thetokens.input/input_tokenspaths. No coverage regression expected in this file.src/app/api/usage/analytics/route.ts,src/lib/usage/aggregateHistory.ts,src/lib/db/cleanup.ts,src/shared/validation/settingsSchemas.ts: changes are additive (new union arm, new retention wiring, schema fields). Existing analytics integration tests still apply; a follow-up unit covering the boundary day where raw rows have just been cleaned butdaily_usage_summarystill has them would close the remaining gap.Reviewer Notes
service_tiercolumns. Rows coming fromdaily_usage_summarythat pre-date service-tier tracking are surfaced as'standard'. If you’d rather expose them asNULL/'unknown', that’s a one-line change in the union projection.aggregateHistoryhas already populateddaily_usage_summaryfor any day older thanrawDataRetentionDays. If aggregation is disabled or lagging, those days will still appear empty — same behavior as before for that subset; this PR doesn’t change the aggregation cadence.prompt_tokensandinput_tokensas compatibility aliases are left alone, since downstream OpenAI-format consumers rely on that dual shape.open-sse/executors/base.ts) appears to forwardcontext-1m-2025-08-07beta headers without the model gating thatclaudeIdentity.tsperforms for native Claude OAuth. We observed a[429] Usage credits are required for long context requests.from a Sonnet request via the CC bridge that is consistent with that behavior. Not touched here.