Skip to content

bug(router): stats.filePath is parsed as a file path but always writes stats.jsonl #251

Description

@MicroMilo

Summary

router.stats.filePath is declared as an override for the stats file path, but the parser drops it, and the collector would not honor the full filename even if it received the field.

Why it matters

A user or test can reasonably set router.stats.filePath: /tmp/custom-name.jsonl and expect stats to be written to that exact file. Today the field does not survive parsing. Separately, TokenStatsCollector treats filePath as a directory hint by taking dirname(filePath) and then always writing stats.jsonl. This can make stats data appear missing, cause tests to read the wrong file, or make multiple custom configs collide on the same default filename.

Evidence

  • src/router/config/schema.ts:58 documents filePath as an override for the default ~/.pilotdeck/router/stats.json path.
  • src/router/config/schema.ts:59 declares filePath?: string.
  • src/router/config/parseRouterConfig.ts:491 enters parseStats.
  • src/router/config/parseRouterConfig.ts:508 parses enabled.
  • src/router/config/parseRouterConfig.ts:510 parses modelPricing.
  • src/router/config/parseRouterConfig.ts:532 parses baselineModel.
  • src/router/config/parseRouterConfig.ts:538 returns { enabled, modelPricing, baselineModel }, dropping filePath.
  • src/router/stats/TokenStatsCollector.ts:71 checks config?.filePath.
  • src/router/stats/TokenStatsCollector.ts:72 uses path.dirname(config.filePath).
  • src/router/stats/TokenStatsCollector.ts:76 always sets jsonlPath to path.join(routerDir, "stats.jsonl").

Validation

Validation level: dynamic parser reproduction plus source-control-flow confirmation.

Minimal reproduction:

  1. Parse router config containing:
    • router.stats.enabled: true
    • router.stats.filePath: /tmp/custom-name.jsonl
  2. Inspect parsed router stats.
  3. Inspect collector path construction for the same field.

Key output:

  • Parser output is { enabled: true }.
  • filePath is absent from parsed stats.
  • Collector code would convert /tmp/custom-name.jsonl to directory /tmp, then write /tmp/stats.jsonl.

Boundary: the parser loss was reproduced directly. The collector behavior is confirmed by source control flow. This does not validate concurrent stats writes or migration behavior.

Expected behavior

The behavior should match the field contract. Either:

  • router.stats.filePath means a full output file path, in which case the parser should preserve it and the collector should write exactly that path; or
  • the supported override is only a directory, in which case the schema/config field should be renamed or documented as a directory setting instead of filePath.

Existing coverage checked

No matching fix was found.

Checked adjacent work:

Search terms checked included router.stats.filePath, TokenStatsCollector filePath, and stats.jsonl custom.

Suggested fix

Pick one contract and make both parser and collector follow it.

Recommended contract:

  • Treat router.stats.filePath as a full file path.
  • Preserve filePath in parseStats.
  • In TokenStatsCollector, create path.dirname(filePath) but set jsonlPath to filePath itself.
  • Keep the current default path when filePath is absent.

If the intended contract is directory-only, rename the setting or update schema/docs so users are not promised a file path override.

Suggested tests

  • Parsing router.stats.filePath preserves the exact string in RouterStatsConfig.
  • TokenStatsCollector({ enabled: true, filePath: "/tmp/custom-name.jsonl" }) writes to /tmp/custom-name.jsonl, not /tmp/stats.jsonl.
  • Default behavior without filePath still writes the existing default stats file.
  • Migration logic still works when a custom full file path is configured.

Submitted with Codex.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions