Skip to content

[METRICS-M3] Prove prompt request session and PR usage attribution - #4

Draft
nullStack65 wants to merge 7 commits into
mainfrom
feat/usage-attribution-proof-20260923
Draft

nullStack65 wants to merge 7 commits into
mainfrom
feat/usage-attribution-proof-20260923

Conversation

@nullStack65

@nullStack65 nullStack65 commented Sep 23, 2026

Copy link
Copy Markdown
Owner

What this is

METRICS-M3R repair slice, continued through METRICS-1:M3D. T3's public usage summaries
aggregate by (day, hour, provider, model), and the underlying UsageRecord rows differ
per provider, so there is no universal provider-request unit. This PR proves the cheapest
path from the measurements that already exist to four reporting levels — prompt,
provider request, native session, and pull request — without inventing a storage
backend, endpoint, or identifier in PostHog, and repairs the reproduced attribution
failures plus the persisted-binding proof.

Draft; not for merge. No endpoint, UI, PostHog, storage schema, collector, gateway,
routing, or installation change. Upstream pingdotgg/t3code is untouched.

  • Base (fork main): bcc1a58b19a9d610a4f08fed191a364767bc65b3
  • Head (round 3): daa55eb88966665faa8c1770c35fc3c53a68f6fa
  • Head (round 4, current): af70495442678a06e007d13bba477f123170c0f5

Findings repaired

  1. Orphan usage no longer disappears. Records with no native session id are kept in
    an explicit orphan bucket, coverage seeds from declared sources and records
    before any session exists, and the reconciliation identity holds against the
    deduplicated input, not a pre-filtered session list:
    sum(PR.attributed) + shared + unallocated + orphan === measured.
  2. Content equality is no longer event identity. dedupeKey is the scan/delivery
    identity with an explicit dedupeKeyScope: global keys are provider-namespaced and
    collapse copies at another path; source-local keys (the Codex occurrence seam) are
    qualified by the canonical native session, so equal local keys in two sessions stay
    distinct. A global key reused under a second session is incompatible ownership
    surfaced as a conflict. Cost and provenance are part of the observation, so a
    repriced record conflicts instead of silently deduping. Unkeyed records are counted
    and surfaced as uncertain, not merged. A snapshot record replaces instead of
    adding.
  3. v3 and predecessor-v4 caches are read, not discarded, and extant ones are enriched
    once.
    A warm-cache hit now requires both the current identity/measurement format
    and the current numeric-quality metadata, so an unchanged extant v3 transcript — or
    an unchanged v4 transcript whose rows predate the completeness fields — is cold
    re-parsed once to fill in ids and quality without duplicate totals. Deleted-transcript
    history survives, a read failure keeps the retained fallback rows, and a deleted file
    is never re-parsed. A legacy nonzero row keeps its identityAvailable: false marker
    through to the projection, so an erased native id is unavailable, never missing
    and never recovered from token magnitude. A predecessor row's missing completeness
    decodes as partial, never complete.
  4. "Measured" reflects measurements, and a zero subtotal is not a reason to discard
    one.
    Parsers classify token fields by actual validity and provider completeness,
    not property presence: an invalid value (null, a string, a negative number) is
    invalid; a valid known subset (Claude input_tokens without output_tokens) is
    partial; an explicit valid zero is measured; a legacy erased row is
    unavailable. Codex and Grok now retain an eligible event whose total is zero
    (complete zero, known-zero subset, or all-invalid payload), so its quality reaches
    the projection instead of vanishing at a parser gate; only a container with no
    recognised token field is no-usage. A nonzero total never implies a complete
    measurement. Identity validity, measurement completeness, level support, source
    coverage, and allocation certainty are separate axes.
  5. Linking proof finished. usageAttributionSources.ts is a read-only extraction
    seam from actually written provider_session_runtime.resume_cursor_json /
    runtime_payload_json.importedTranscripts and projection_thread_pull_requests into
    the allowlisted binding/link snapshot, with diagnostics for absent, malformed,
    overwritten, and ambiguous history. It never returns a runtime payload.

What changed

  • apps/server/src/usage/usageAttribution.ts — projection rewrite: orphan bucket,
    provider-namespaced + occurrence-aware identity, conflict/snapshot handling, split
    identity/measurement/level/source/allocation axes, per-model contributions with cost
    provenance, association basis.
  • apps/server/src/usage/usageAttributionSources.ts (new) — read-only extraction from
    persisted runtime and PR-link rows.
  • apps/server/src/usage/usageTranscripts.tsUsageMeasurement (+ invalid),
    UsageMeasurementCompleteness, DedupeKeyScope, the shared occurrence seam, field
    validity/completeness classification, Claude usage: {} detection, and (round 4)
    zero-total retention for Codex/Grok plus emitted-tick cost accounting.
  • apps/server/src/usage/usageScanCache.ts — v3 read + legacy marker, measurement /
    completeness / key-scope serialization, legacy identity-erased marker, and (round 4)
    qualityMetadata: "declared" | "predecessor" with missing completeness decoding as
    partial.
  • apps/server/src/usage/UsageService.ts — reuse the seam, warm-cache acceptance
    requires the current identity and quality format, no incremental resume of a legacy
    or predecessor entry, count usage: {} and invalid containers as malformedRecords.
  • Tests: usageAttribution.test.ts, usageAttributionSources.test.ts,
    usageScanCache.test.ts, usageTranscripts.test.ts, UsageService.test.ts.
  • docs/internals/usage-attribution.md.

Source capability (source-grounded; live qualification is false for all rows)

Provider Session Prompt Request Native unit
Claude Code supported unsupported supported one assistant message = one API response (message.id + requestId)
Codex supported unsupported unsupported token_count delta, one per model turn
Grok Build supported supported (prompt_id) unsupported turn_completed, one per prompt per model

UsageProviderKind is claude | codex | grok. OpenCode, Antigravity, and Cursor have a
native cursor id but no transcript T3 scans, so they have no usage source here; they are
exposed only as ExtractedNativeSession with usageProvider: null. No OpenCode parser
is added and the public union is not widened. Turn-only data remains turn-only.

Correctness rules encoded

  • Association ≠ attribution: a session linked to several PRs feeds one global
    non-additive shared pool, never cloned onto each link. stack links are
    association-only; stack-dismissed tombstones are ignored.
  • Associations are read at generatedAtMs (association.basis: "links-at-read-time").
    linkedAt does not gate allocation, so pre-link implementation work is included;
    a changed link rewrites the recomputed view, and historical allocation as of a past
    instant is unavailable without temporal evidence.
  • Missing is never zero; an explicit zero is never missing; a zero subtotal is never a
    reason to drop an observation.
  • Native-id availability and numeric-quality freshness are separate axes; missing
    quality metadata is never silently promoted to complete.
  • Cost is API-equivalent list value, not subscription spend; per-model costSource is
    preserved or reported unknown.

Validation (local receipts; CI is separate and reported in the RESULT)

vp test run apps/server/src/usage/   # 181 passed (9 files)  [WSL Ubuntu 24.04, Node 26.8.2, project-local vp/vitest]
vp run --filter t3 typecheck         # exit 0 (no error diagnostics)
vp lint apps/server/src/usage/       # 0 warnings, 0 errors
vp fmt --check apps/server/src/usage/ docs/internals/usage-attribution.md   # clean

Round 3 (METRICS-1:M3C)

The three material residuals from the independent review are repaired: numeric
validity/completeness, explicit identity scope with cost-only conflicts, and warm legacy
cache enrichment (with a service-level regression proving enrichment happens once,
totals stay stable, and request-ID availability is truthful). The M1C interchange fixture
is generated by the real extractor from persisted-row shapes only and posted as a #4
comment. Corrected head daa55eb88966665faa8c1770c35fc3c53a68f6fa; #6 must eventually
rebase onto it.

Round 4 (METRICS-1:M3D)

Two bounded repairs with before/after evidence:

  1. Zero-total filters no longer erase quality. Codex returned null and Grok
    dropped aggregate/per-model rows when the normalized total was zero, before numeric
    classification, so an all-invalid payload never reached UsageService's
    malformedRecords and a measured zero never reached session quality. Now an eligible
    event is emitted whenever its usage container has a recognised token field, with its
    real measurement and completeness; only a no-field container is no-usage. Grok
    per-model cost accounting reduces the aggregate for every emitted ticked row, so an
    emitted zero-token row cannot double count, while a row with no token field does not
    silently swallow its ticks.
  2. Cache freshness is no longer inferred from native-id availability. The
    predecessor head e4f36af5e wrote 15-field v4 rows with no completeness metadata,
    and the current decoder defaulted a non-legacy observed row to complete, promoting
    an unknown measurement and letting identity: "declared" bypass the corrected
    parser on an unchanged file. Supported-format policy is now explicit: missing
    completeness decodes as partial (never complete), the entry is
    qualityMetadata: "predecessor", and warm acceptance + incremental resume both
    require the current quality format. An extant file is cold re-parsed once and then
    warms; deleted/unreadable history is retained conservatively; v3 identity enrichment
    is unchanged. No database or session-history migration.

Before/after receipt on the pinned predecessor fixture (generated by the actual
predecessor writer at e4f36af5e, verbatim 15-field rows): the pre-fix decoder at
daa55eb88 reads completeness=["complete","complete","complete"]; the round-4 decoder
reads ["partial","partial","partial"]. Service-seam regressions cover one-time
refresh, warm/cold parity, restart, deleted source, and failed-read fallback.

The M1D bridge contract is unaffected: usageAttributionSources.ts and the posted
extractor snapshot did not change, so the M1C interchange fixture remains byte-stable.

Not in scope / needs Kameron

No endpoint, no persistence choice, no PostHog change, no live collection. Durable
per-thread session history (an additive identity record instead of the single current
resume cursor) and pre-execution experiment metadata are the follow-on that would widen
coverage; specified but not adopted. Provider-instance identity is not recoverable from
a transcript scan. No fixture-tested coverage here is claimed as live-qualified.

Model/harness: opencode-go/deepseek-v4.1-flash via opencode.

Public usage summaries aggregate by day/provider/model and the underlying
UsageRecord rows differ per provider, so there is no single provider-request
unit today. Add a pure projection that re-projects measured usage onto prompt,
provider-request, native-session, and pull-request levels using only explicit
existing bindings (provider_session_runtime resume cursors and imported
transcripts) and existing thread/PR links.

Granularity is asserted per source, never inferred: Claude exposes per-request
identity, Grok per prompt, Codex only per turn. The projection reports
unsupported levels as null instead of dividing a turn. A session linked to
several PRs feeds a non-additive shared pool rather than being cloned onto
each link, and a known session with no usage is missing (null), never zero.

Adds the native request/message/prompt ids to UsageRecord and the v4 scan
cache so they stay separate from dedupeKey.

Model/harness: opencode-go/deepseek-v4.1-flash via opencode.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL labels Sep 23, 2026
A known session with no measured usage reported 
equestCount: 0 while its
quality was missing, which reads as a zero-request success. Return null
unless the level's quality is measured or partial, so absence stays absent.

Model/harness: opencode-go/deepseek-v4.1-flash via opencode.
@github-actions github-actions Bot added size:XXL and removed size:XL labels Sep 23, 2026
Adds the METRICS-M1 invariant sum(attributed PR totals) + shared +
unallocated = distinct measured total, and records that the legacy
projection_thread_sessions.provider_session_id columns are not written by
the live projector.

Model/harness: opencode-go/deepseek-v4.1-flash via opencode.
@nullStack65

Copy link
Copy Markdown
Owner Author

METRICS-M3 RESULT — prompt / request / session / PR usage attribution proof

Status: complete (source-only proof + focused offline tests + this draft PR). Draft; not for merge. No endpoint, storage, collector, gateway, PostHog, or routing change.

What this proves vs. what still needs approval

Proven here (source + fixtures): the native-session → T3-thread → PR join is expressible from persisted state alone; the four levels can be projected with honest per-source granularity; association and allocation can be kept separate; missing is representable as null, never zero.

Not proven / needs approval: live qualification of any source; durable per-thread session history (the current resume cursor is single-valued); provider-instance identity at record level; an OpenCode adapter; any storage/transport choice; any cost/invoice basis.

Concrete diff

File Change
apps/server/src/usage/usageAttribution.ts new pure projection: buildUsageAttribution, renderUsageAttributionText, ATTRIBUTION_SOURCE_CAPABILITIES
apps/server/src/usage/usageAttribution.test.ts new: 18 tests over the acceptance cases
apps/server/src/usage/usageTranscripts.ts additive providerRequestId / providerMessageId / promptId on UsageRecord (kept distinct from dedupeKey)
apps/server/src/usage/usageScanCache.ts v4 cache so those ids persist; v3 entries cold-re-parse
docs/internals/usage-attribution.md capability matrix, identity join, quality semantics, limitations

The projection reads only allowlisted metadata: provider, native session id, model, timestamp, token totals, priced cost, native ids, source fingerprint; plus explicit bindings and existing PR links. It never reads prompts, responses, tool output, titles, or paths.

Source-capability matrix (source-grounded; liveQualified: false for every row)

Provider Session Prompt Request Native unit
Claude Code supported unsupported supported one assistant message = one API response (message.id + requestId)
Codex supported unsupported unsupported token_count delta, one per model turn
Grok Build supported supported (prompt_id) unsupported turn_completed, one per prompt per model
  • Codex turn-level usage is never divided into requests: requestCount is null, quality unsupported.
  • Claude has no prompt id, so a prompt spanning several requests stays at request level.
  • OpenCode / Antigravity / Cursor expose a native cursor id but no transcript T3 scans → no usage source (nativeSource: "none" when supplied). Complementary to M1's OpenCode-store slice; this projection is record-agnostic and can consume OpenCode records once an adapter emits them.
  • CLIProxyAPI in this repo is a quota/limit source only (auth-files, credits, api-call); it does no token accounting, so it is not an accounting authority and must not be for one.
  • Correction for M1 §2: projection_thread_sessions.provider_session_id / provider_thread_id exist in migration 005 but the live upsert in ProjectionThreadSessions.ts:19-53 never writes them — they are dead columns. The durable mapping is provider_session_runtime.resume_cursor_json ({threadId} Codex, {resume} Claude, {sessionId} ACP/OpenCode) plus runtime_payload_json.importedTranscripts (providerSessionId + file identity). Also, an upstream PostHog identifier filter does not exist in code; the exclusion is by convention and omission (product-analytics.md), worth knowing before relying on it.

Example output (synthetic fixture; real module output)

Human-readable:

Usage attribution

Sessions: 5
  claude:5a128faa-8253-489e-b935-6c08e8e670c0 [measured/attributed] 285 tokens $0.0600 prompts=unsupported requests=2 threads=t1
  claude:99999999-8888-7777-6666-555555555555 [missing/missing] missing prompts=unsupported requests=unsupported threads=t5
  claude:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee [measured/unallocated] 125 tokens $0.0100 prompts=unsupported requests=1 threads=t4
  codex:019fbbc1-b12c-7360-a685-28c181f0025f [measured/shared] 170 tokens $0.0300 prompts=unsupported requests=unsupported threads=t3
  grok:019fec1a-12f7-72f2-9b1f-7778a00aea3c [measured/attributed] 235 tokens $0.0070 prompts=1 requests=unsupported threads=t2

Pull requests:
  github.com/acme/repo#12 attributed=520 tokens shared=0 tokens sessions=claude:5a128faa-8253-489e-b935-6c08e8e670c0,grok:019fec1a-12f7-72f2-9b1f-7778a00aea3c sources=agent,manual
  github.com/acme/repo#13 attributed=0 tokens shared=170 tokens sessions=codex:019fbbc1-b12c-7360-a685-28c181f0025f sources=manual
  github.com/acme/repo#14 attributed=0 tokens shared=170 tokens sessions=codex:019fbbc1-b12c-7360-a685-28c181f0025f sources=manual

Shared (not additive): 170 tokens
Unallocated: 125 tokens

Coverage:
  claude session=supported prompt=unsupported request=supported measured=2 missing=1 unbound=0 ambiguous=0
  codex session=supported prompt=unsupported request=unsupported measured=1 missing=0 unbound=0 ambiguous=0
  grok session=supported prompt=supported request=unsupported measured=1 missing=0 unbound=0 ambiguous=0

Machine-readable (verbatim module output; verbose tokens bodies kept for exactness):

{
  "contractVersion": 1,
  "generatedAtMs": 1786100000000,
  "sessions": [
    {
      "provider": "claude",
      "sessionId": "5a128faa-8253-489e-b935-6c08e8e670c0",
      "models": [
        "claude-fable-5"
      ],
      "totals": {
        "tokens": {
          "uncachedInputTokens": 200,
          "cachedInputTokens": 20,
          "cacheCreationTokens": 0,
          "outputTokens": 65,
          "reasoningTokens": 0
        },
        "totalTokens": 285,
        "costUsd": 0.06,
        "records": 2
      },
      "quality": "measured",
      "promptQuality": "unsupported",
      "requestQuality": "measured",
      "promptCount": null,
      "requestCount": 2,
      "boundThreadIds": [
        "t1"
      ],
      "providerInstanceIds": [
        "claude-default"
      ],
      "bindingOrigins": [
        "runtimeCursor"
      ],
      "allocation": "attributed",
      "pullRequestKeys": [
        "github.com/acme/repo#12"
      ],
      "stackOnlyPullRequestKeys": []
    },
    {
      "provider": "claude",
      "sessionId": "99999999-8888-7777-6666-555555555555",
      "models": [],
      "totals": null,
      "quality": "missing",
      "promptQuality": "unsupported",
      "requestQuality": "missing",
      "promptCount": null,
      "requestCount": null,
      "boundThreadIds": [
        "t5"
      ],
      "providerInstanceIds": [
        "claude-default"
      ],
      "bindingOrigins": [
        "runtimeCursor"
      ],
      "allocation": "missing",
      "pullRequestKeys": [],
      "stackOnlyPullRequestKeys": []
    },
    {
      "provider": "claude",
      "sessionId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
      "models": [
        "claude-fable-5"
      ],
      "totals": {
        "tokens": {
          "uncachedInputTokens": 100,
          "cachedInputTokens": 10,
          "cacheCreationTokens": 0,
          "outputTokens": 15,
          "reasoningTokens": 0
        },
        "totalTokens": 125,
        "costUsd": 0.01,
        "records": 1
      },
      "quality": "measured",
      "promptQuality": "unsupported",
      "requestQuality": "measured",
      "promptCount": null,
      "requestCount": 1,
      "boundThreadIds": [
        "t4"
      ],
      "providerInstanceIds": [
        "claude-default"
      ],
      "bindingOrigins": [
        "runtimeCursor"
      ],
      "allocation": "unallocated",
      "pullRequestKeys": [],
      "stackOnlyPullRequestKeys": []
    },
    {
      "provider": "codex",
      "sessionId": "019fbbc1-b12c-7360-a685-28c181f0025f",
      "models": [
        "gpt-5.6-sol"
      ],
      "totals": {
        "tokens": {
          "uncachedInputTokens": 100,
          "cachedInputTokens": 10,
          "cacheCreationTokens": 0,
          "outputTokens": 60,
          "reasoningTokens": 0
        },
        "totalTokens": 170,
        "costUsd": 0.03,
        "records": 1
      },
      "quality": "measured",
      "promptQuality": "unsupported",
      "requestQuality": "unsupported",
      "promptCount": null,
      "requestCount": null,
      "boundThreadIds": [
        "t3"
      ],
      "providerInstanceIds": [
        "codex-default"
      ],
      "bindingOrigins": [
        "runtimeCursor"
      ],
      "allocation": "shared",
      "pullRequestKeys": [
        "github.com/acme/repo#13",
        "github.com/acme/repo#14"
      ],
      "stackOnlyPullRequestKeys": []
    },
    {
      "provider": "grok",
      "sessionId": "019fec1a-12f7-72f2-9b1f-7778a00aea3c",
      "models": [
        "grok-4.5",
        "grok-fast"
      ],
      "totals": {
        "tokens": {
          "uncachedInputTokens": 200,
          "cachedInputTokens": 20,
          "cacheCreationTokens": 0,
          "outputTokens": 15,
          "reasoningTokens": 0
        },
        "totalTokens": 235,
        "costUsd": 0.007,
        "records": 2
      },
      "quality": "measured",
      "promptQuality": "measured",
      "requestQuality": "unsupported",
      "promptCount": 1,
      "requestCount": null,
      "boundThreadIds": [
        "t2"
      ],
      "providerInstanceIds": [],
      "bindingOrigins": [
        "runtimeCursor"
      ],
      "allocation": "attributed",
      "pullRequestKeys": [
        "github.com/acme/repo#12"
      ],
      "stackOnlyPullRequestKeys": []
    }
  ],
  "prompts": [
    {
      "provider": "grok",
      "sessionId": "019fec1a-12f7-72f2-9b1f-7778a00aea3c",
      "promptId": "p1",
      "totals": {
        "tokens": {
          "uncachedInputTokens": 200,
          "cachedInputTokens": 20,
          "cacheCreationTokens": 0,
          "outputTokens": 15,
          "reasoningTokens": 0
        },
        "totalTokens": 235,
        "costUsd": 0.007,
        "records": 2
      },
      "models": [
        "grok-4.5",
        "grok-fast"
      ],
      "boundThreadIds": [
        "t2"
      ],
      "allocation": "attributed"
    }
  ],
  "requests": [
    {
      "provider": "claude",
      "sessionId": "5a128faa-8253-489e-b935-6c08e8e670c0",
      "providerRequestId": "r1",
      "providerMessageId": "m1",
      "totals": {
        "tokens": {
          "uncachedInputTokens": 100,
          "cachedInputTokens": 10,
          "cacheCreationTokens": 0,
          "outputTokens": 40,
          "reasoningTokens": 0
        },
        "totalTokens": 150,
        "costUsd": 0.04,
        "records": 1
      },
      "model": "claude-fable-5",
      "boundThreadIds": [
        "t1"
      ],
      "allocation": "attributed"
    },
    {
      "provider": "claude",
      "sessionId": "5a128faa-8253-489e-b935-6c08e8e670c0",
      "providerRequestId": "r2",
      "providerMessageId": "m2",
      "totals": {
        "tokens": {
          "uncachedInputTokens": 100,
          "cachedInputTokens": 10,
          "cacheCreationTokens": 0,
          "outputTokens": 25,
          "reasoningTokens": 0
        },
        "totalTokens": 135,
        "costUsd": 0.02,
        "records": 1
      },
      "model": "claude-fable-5",
      "boundThreadIds": [
        "t1"
      ],
      "allocation": "attributed"
    },
    {
      "provider": "claude",
      "sessionId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
      "providerRequestId": "r9",
      "providerMessageId": "m9",
      "totals": {
        "tokens": {
          "uncachedInputTokens": 100,
          "cachedInputTokens": 10,
          "cacheCreationTokens": 0,
          "outputTokens": 15,
          "reasoningTokens": 0
        },
        "totalTokens": 125,
        "costUsd": 0.01,
        "records": 1
      },
      "model": "claude-fable-5",
      "boundThreadIds": [
        "t4"
      ],
      "allocation": "unallocated"
    }
  ],
  "pullRequests": [
    {
      "key": "github.com/acme/repo#12",
      "host": "github.com",
      "repository": "acme/repo",
      "number": 12,
      "threadIds": [
        "t1",
        "t2"
      ],
      "linkSources": [
        "agent",
        "manual"
      ],
      "attributed": {
        "tokens": {
          "uncachedInputTokens": 400,
          "cachedInputTokens": 40,
          "cacheCreationTokens": 0,
          "outputTokens": 80,
          "reasoningTokens": 0
        },
        "totalTokens": 520,
        "costUsd": 0.067,
        "records": 4
      },
      "shared": {
        "tokens": {
          "uncachedInputTokens": 0,
          "cachedInputTokens": 0,
          "cacheCreationTokens": 0,
          "outputTokens": 0,
          "reasoningTokens": 0
        },
        "totalTokens": 0,
        "costUsd": 0,
        "records": 0
      },
      "stackAssociationSessions": [],
      "contributingSessions": [
        "claude:5a128faa-8253-489e-b935-6c08e8e670c0",
        "grok:019fec1a-12f7-72f2-9b1f-7778a00aea3c"
      ]
    },
    {
      "key": "github.com/acme/repo#13",
      "host": "github.com",
      "repository": "acme/repo",
      "number": 13,
      "threadIds": [
        "t3"
      ],
      "linkSources": [
        "manual"
      ],
      "attributed": {
        "tokens": {
          "uncachedInputTokens": 0,
          "cachedInputTokens": 0,
          "cacheCreationTokens": 0,
          "outputTokens": 0,
          "reasoningTokens": 0
        },
        "totalTokens": 0,
        "costUsd": 0,
        "records": 0
      },
      "shared": {
        "tokens": {
          "uncachedInputTokens": 100,
          "cachedInputTokens": 10,
          "cacheCreationTokens": 0,
          "outputTokens": 60,
          "reasoningTokens": 0
        },
        "totalTokens": 170,
        "costUsd": 0.03,
        "records": 1
      },
      "stackAssociationSessions": [],
      "contributingSessions": [
        "codex:019fbbc1-b12c-7360-a685-28c181f0025f"
      ]
    },
    {
      "key": "github.com/acme/repo#14",
      "host": "github.com",
      "repository": "acme/repo",
      "number": 14,
      "threadIds": [
        "t3"
      ],
      "linkSources": [
        "manual"
      ],
      "attributed": {
        "tokens": {
          "uncachedInputTokens": 0,
          "cachedInputTokens": 0,
          "cacheCreationTokens": 0,
          "outputTokens": 0,
          "reasoningTokens": 0
        },
        "totalTokens": 0,
        "costUsd": 0,
        "records": 0
      },
      "shared": {
        "tokens": {
          "uncachedInputTokens": 100,
          "cachedInputTokens": 10,
          "cacheCreationTokens": 0,
          "outputTokens": 60,
          "reasoningTokens": 0
        },
        "totalTokens": 170,
        "costUsd": 0.03,
        "records": 1
      },
      "stackAssociationSessions": [],
      "contributingSessions": [
        "codex:019fbbc1-b12c-7360-a685-28c181f0025f"
      ]
    }
  ],
  "shared": {
    "tokens": {
      "uncachedInputTokens": 100,
      "cachedInputTokens": 10,
      "cacheCreationTokens": 0,
      "outputTokens": 60,
      "reasoningTokens": 0
    },
    "totalTokens": 170,
    "costUsd": 0.03,
    "records": 1
  },
  "unallocated": {
    "tokens": {
      "uncachedInputTokens": 100,
      "cachedInputTokens": 10,
      "cacheCreationTokens": 0,
      "outputTokens": 15,
      "reasoningTokens": 0
    },
    "totalTokens": 125,
    "costUsd": 0.01,
    "records": 1
  },
  "coverage": [
    {
      "provider": "claude",
      "nativeSource": "transcript",
      "liveQualified": false,
      "session": "supported",
      "prompt": "unsupported",
      "request": "supported",
      "measuredSessions": 2,
      "missingSessions": 1,
      "invalidSessions": 0,
      "unboundSessions": 0,
      "ambiguousSessions": 0,
      "recordsWithoutSessionId": 0
    },
    {
      "provider": "codex",
      "nativeSource": "transcript",
      "liveQualified": false,
      "session": "supported",
      "prompt": "unsupported",
      "request": "unsupported",
      "measuredSessions": 1,
      "missingSessions": 0,
      "invalidSessions": 0,
      "unboundSessions": 0,
      "ambiguousSessions": 0,
      "recordsWithoutSessionId": 0
    },
    {
      "provider": "grok",
      "nativeSource": "transcript",
      "liveQualified": false,
      "session": "supported",
      "prompt": "supported",
      "request": "unsupported",
      "measuredSessions": 1,
      "missingSessions": 0,
      "invalidSessions": 0,
      "unboundSessions": 0,
      "ambiguousSessions": 0,
      "recordsWithoutSessionId": 0
    }
  ],
  "limitations": [
    "A native session maps to a T3 thread only through the current resume cursor or imported-transcript metadata; a session switch, fork, or restart that overwrote the cursor leaves earlier usage unbound.",
    "Provider-instance identity is not recoverable from a transcript scan, so two instances of one provider cannot be told apart at the record level.",
    "Request and prompt counts are reported only where the native source writes those ids; a turn-level source reports `unsupported`, never an inferred count.",
    "Some known sessions have no measured usage. They are reported as `missing` with a null total; this is not a zero-cost success.",
    "Only the newest native session id per thread is durable. Additive retention must land before historical re-attribution is possible."
  ]
}

Reconciliation holds in the fixture: allocated 520 + shared 170 + unallocated 125 = distinct measured 815, and the duplicate Codex scan is counted once.

Validation (run locally, node 24.21.0)

vp test run apps/server/src/usage/usageAttribution.test.ts apps/server/src/usage/usageTranscripts.test.ts apps/server/src/usage/usageScanCache.test.ts apps/server/src/usage/usageAggregation.test.ts
# 4 files, 69 passed
vp run --filter t3 typecheck        # no errors
vp lint <changed files>             # clean
git diff --check                    # clean

Note: the checkout had no node on PATH; ran against ~/AppData/Local/t3-build/node-v24.21.0-win-x64, satisfying engines: node ^24.

CI

  • PR Vouch, PR Size, Label PR: passed (on an earlier head; the label/CI runs re-triggered per push).
  • CI run for head db1a3db83: https://github.com/nullStack65/t3code/actions/runs/35846007902queued, runner not yet assigned on the fork (no CI result available yet).
  • CodeRabbit: skipped (draft).
  • Local focused tests are green; repo-wide checks were not run (per AGENTS.md).

Coverage and limitations

  • Bound native session → thread only through the current resume cursor or imported-transcript metadata. A switch/fork/restart that overwrote the cursor leaves earlier usage unallocated — demonstrated by test.
  • Provider-instance identity is not recoverable from a transcript scan; two instances of one provider are indistinguishable at record level.
  • missing sessions carry null totals/counts and never enter a pool; invalid Claude ids are flagged; partial marks levels where some records lack the id.
  • Duplicate scans de-dupe by dedupeKey with a content-signature fallback for keyless Codex turns; duplicate source fingerprints are surfaced as a limitation.
  • No percentiles, p50/p95/p99, workload strata, or cost-basis separation yet — that is the M1 §4/§5 report layer, not this projection.
  • Enum naming differs from M1's exact|partial|unavailable|ambiguous: here it is measured|partial|missing|invalid|unsupported. Rename is mechanical if M1's nomenclature is adopted.

Schema/privacy decisions requiring Kameron

  1. Durable session-history schema (highest value): an additive record of (threadId, provider, providerInstanceId, nativeSessionId, firstSeenAt, lastSeenAt, origin) written alongside the resume-cursor upsert, so re-attribution survives switches/forks. Not adopted; the current single-cursor row is the binding.
  2. Provider-instance tagging at scan time (tag each scanned file with the instance that produced it) to disambiguate multi-instance providers.
  3. Enum/nomenclature alignment with M1 §2 (mechanical).
  4. Storage/transport for the report remains M1's D2/D3 decision; this PR deliberately chooses none.
  5. OpenCode adapter ownership (M1 §9 M1b) — this projection can consume it but does not build it.

No privacy expansion: no raw content, no new PostHog field, no conversation identifier added anywhere.

Recommendation

SMALL EXTENSION WORTH LANDING — as a scoped, source-only slice. The projection, the id fields, and the capability/quality contract are small, pure, and tested; they convert "measure usage" from a day/model bucket into a level-honest join without touching storage or telemetry.

If the owner prefers zero schema/cache movement now, the cheaper fallback is DEFER the request/prompt id extension: keep the projection but run it over session + PR levels only (Claude/Codex/Grok all remain unsupported at request/prompt), which leaves usageTranscripts.ts and usageScanCache.ts untouched. That trades the request-level proof for a smaller diff.

Next step

M1/M3 owner review of (1) the enum alignment and (2) the additive durable-session-history schema. On approval, the follow-up is the M1 §9 M1b-style offline exporter (OpenCode store + T3 projection DB) reusing this projection; until then the live sources stay unqualified and unactivated.

Model/harness: opencode-go/deepseek-v4.1-flash via opencode.

@nullStack65

Copy link
Copy Markdown
Owner Author

START METRICS-M3R — bounded T3 attribution repair.

Copy link
Copy Markdown
Owner Author

ROUTE6-1 linkage: this attribution proof is a dependency of nullStack65/closura-agent-config#238.

The T3 lane in pingdotgg#238 should build on this PR rather than recreate attribution. The narrow follow-on is durable session/history plus pre-execution experiment metadata sufficient to preserve manager/agent/model/PR attribution across resumes, model switches, and child sessions. T3 must not become the canonical model router. Please post future durable RESULT/PR links back to agent-config#238.

A v3 scan cache is read instead of discarded, so measured records from deleted transcripts survive; v3 rows decode with native ids and presence explicitly unavailable and extant files are cold re-parsed to enrich them. Parsers record whether tokens were actually observed, and the occurrence-aware dedupe key moves to a shared seam reused by the scan.
Preserve orphan usage in an explicit bucket and reconcile attributed + shared + unallocated + orphan against the deduplicated input. Replace content-equality dedupe with a provider-namespaced declared key plus occurrence-aware scan identity, keeping unkeyed records uncertain and surfacing conflicts and snapshot replacement. Split identity, measurement, level support, and allocation; seed coverage from declared sources; retain per-model and cost provenance. Add a read-only extraction seam from provider_session_runtime and projection_thread_pull_requests.
@nullStack65

Copy link
Copy Markdown
Owner Author

CHECKPOINT METRICS-M3R — repair pushed.

  • Head: e4f36af5ef279246bcb0f8463adeee8a09b7bde1 (2 commits on feat/usage-attribution-proof-20260923, fast-forward from reviewed db1a3db83).
  • Touch set: apps/server/src/usage/{usageAttribution,usageAttributionSources,usageTranscripts,usageScanCache,UsageService}.ts + their tests; docs/internals/usage-attribution.md.
  • Local receipts: vp test run apps/server/src/usage/ → 137 passed (9 files); vp run --filter t3 typecheck exit 0; vp lint apps/server/src/usage/ exit 0; vp fmt --check clean.
  • CI: main jobs queued (run 35920455981). Will report exact state in the RESULT; queued is not passed.

@nullStack65

Copy link
Copy Markdown
Owner Author

RESULT METRICS-M3R — bounded T3 attribution repair. Status: complete (source + focused tests + docs; draft PR). CI cannot be observed green on the fork (see Checks).

Touch set

  • apps/server/src/usage/usageAttribution.ts, usageAttributionSources.ts (new), usageTranscripts.ts, usageScanCache.ts, UsageService.ts
  • apps/server/src/usage/{usageAttribution,usageAttributionSources,usageScanCache,usageTranscripts}.test.ts
  • docs/internals/usage-attribution.md

No endpoint/UI, PostHog, storage/history schema, collector, gateway/routing, install, merge, or activation change. Upstream pingdotgg/t3code untouched.

Per-finding dispositions

  1. Orphan usage (fixed). No-session records are kept in an explicit orphan bucket. Coverage seeds from declared sources and records before any session exists, so a missing/failed source still produces a row. Reconciliation now holds against the deduplicated input: sum(PR.attributed) + shared + unallocated + orphan === measured; the test computes measured independently from the raw input records (including the orphan, excluding a known duplicate), not from pre-filtered sessions. No synthetic session is invented.
  2. Content ≠ event identity (fixed). dedupeKey is the scan/delivery identity, namespaced by provider. Keyless records use the occurrence seam (usageEventOccurrenceBaseKey + per-delivery occurrence) now shared with UsageService. Repeated scans and copied/moved history collapse; two equal keyless occurrences are kept; unkeyed records are counted and marked recordIdentity: "uncertain" rather than merged. Same identity + different content is a surfaced conflict (kept-first); scope: "snapshot" replaces instead of adding. Native observation ids stay distinct from delivery identity.
  3. v3 cache history (fixed). v3 documents are read, not rejected. Deleted-transcript history survives; v3 rows decode with native ids and presence explicitly unavailable (nonzero row = known measurement; all-zero row = unknown, never a measured zero). The entry is flagged so it is never resumed incrementally; an extant file is cold re-parsed to enrich ids/presence, replacing the entry so totals do not double count. Caches are not cleared.
  4. "Measured" (fixed). Parsers record observed | empty | unavailable. Claude usage: {}empty; an explicit zero → observed; a legacy erased row → unavailable. Identity validity, measurement completeness, level support, source coverage, and allocation certainty are separate axes; declared failed/partial sources are reported in coverage.sourceStatus and a limitation. Historical zeros are unavailable, not missing. UsageService now counts usage: {} in malformedRecords.
  5. Linking proof (finished). usageAttributionSources.ts extracts from actually written resume_cursor_json ({resume} Claude, {threadId} Codex, {sessionId} Grok/OpenCode/Antigravity) and runtime_payload_json.importedTranscripts (thread must be import:<instance>:<session>), plus projection_thread_pull_requests, into the allowlisted binding/link snapshot. Diagnostics expose absent, malformed, overwritten, and ambiguous history. It never returns a runtime payload (test asserts a payload marker/path does not leak). Output is labelled as current associations at cutoffMs with basis: "links-at-read-time"; linkedAtGovernsAllocation: false and the doc now states that a changed link rewrites the recomputed view (the old "cannot rewrite" claim is corrected). Pre-link implementation work is included.

Reconciliation evidence

  • usageAttribution.test.ts "reconciles attributed + shared + unallocated + orphan to the deduplicated input": independent expected total from raw records; measured equals it and allocated + shared + unallocated + orphan equals it.
  • Per-model contributions preserved at session and PR level with costUsd/costSource; unpriced/unknown provenance kept separate (API-equivalent value, not subscription spend).

Checks

Local (worktree, off reviewed head):

vp test run apps/server/src/usage/                      # 137 passed (9 files)
vp run --filter t3 typecheck                            # exit 0
vp lint apps/server/src/usage/                          # exit 0
vp fmt --check apps/server/src/usage/ docs/internals/usage-attribution.md   # clean

CI (run 35920455981, head e4f36af5e): queued, not passed. The CI workflow's Test, Test Server 1/2/3, Rust, Release Smoke, Mobile Native Changes and Check jobs require blacksmith-* self-hosted runners, which the fork does not have, so they remain queued. Small jobs that run on GitHub-hosted runners passed (PR Size, PR Vouch, Thread Transfer Report). No green CI is claimed.

Capability / coverage limits

  • UsageProviderKind is claude | codex | grok; liveQualified is false for every source. Fixture-tested coverage here is not live-qualified.
  • OpenCode/Antigravity/Cursor have a native cursor id but no scanned transcript; no OpenCode parser is added and the public union is not widened. Turn-only data remains turn-only.
  • Only the newest native session id per thread is durable (runtimeCursor); earlier ids survive only if present in importedTranscripts.
  • Provider-instance identity is not recoverable from a transcript scan.
  • Historical allocation as of a past instant is unavailable without temporal evidence.

Metadata interface M1R can consume (read-only, no payload)

extractAttributionBindings(rows) -> {
  bindings: AttributionThreadBinding[],              // usage providers only
  nativeSessions: ExtractedNativeSession[],           // includes usageProvider: null
  diagnostics: { runtimeRows, runtimeCursorBindings, importedTranscriptBindings,
                 absentIdentityRows, malformedResumeCursors, malformedRuntimePayloads,
                 skippedImportedTranscripts, unsupportedProviderBindings,
                 overwrittenThreads, ambiguousSessionIds }
}
ExtractedNativeSession = { threadId, providerName, adapterKey, providerInstanceId,
                           nativeSessionId, origin: "runtimeCursor"|"importedTranscript",
                           usageProvider: UsageProviderKind | null }
extractAttributionLinks(rows) -> { links, diagnostics: { rows, links, dismissed, malformed } }
extractAttributionSnapshot({ cutoffMs, runtimeRows, linkRows })

Ready for independent review

The proof is source-grounded and fixture-tested only. Requested review focus: the identity model (declared vs occurrence vs unkeyed), the reconciliation identity, and the v3 read/enrich path.

@nullStack65

Copy link
Copy Markdown
Owner Author

ROUTE6-1:T3 follow-on — durable model-canary attribution, stacked on this PR's head e4f36af5e without modifying it.

This PR's branch and reviewed head are unchanged. Do not merge.

Classify usage fields by validity and provider completeness instead of
property presence: an invalid value is invalid, a valid known subset is
partial, and a nonzero total never implies a complete measurement.

Make dedupe-key scope explicit. A source-local key is qualified by its
native session; a global key reused under another session is incompatible
ownership surfaced as a conflict. Cost and provenance are part of the
observation, so a repriced record conflicts rather than silently deduping.

Require the current identity format for a warm scan-cache hit so an
unchanged legacy transcript is cold re-parsed once to enrich it, keeping
deleted history and read-failure fallbacks. Carry the legacy
identity-erased marker through to the projection so an erased native id is
unavailable, not missing.
@nullStack65

Copy link
Copy Markdown
Owner Author

M1C BRIDGE FIXTURE — extractAttributionSnapshot interchange

Artifact for METRICS-1:M1C (the report's load_bindings rejected the previous output because the payload shapes differ). This is the exact output of the real extractor extractAttributionSnapshot at:

  • Source pin: daa55eb88966665faa8c1770c35fc3c53a68f6fa (PR [METRICS-M3] Prove prompt request session and PR usage attribution #4 head, round 3)
  • File: apps/server/src/usage/usageAttributionSources.ts
  • Pinned by test: apps/server/src/usage/usageAttributionSources.test.tsextractAttributionSnapshot > produces a stable fixture for the M1C interchange (asserts deep equality, so it cannot drift).

It is generated from persisted-row shapes only — no runtime payload is ever returned. The input rows are the allowlisted provider_session_runtime shape (threadId, providerName, providerInstanceId, adapterKey, resumeCursor, runtimePayload) and the allowlisted projection_thread_pull_requests shape (threadId, host, repository, number, url?, source, linkedAt). The fixture deliberately includes an imported Claude cursor carrying both threadId and resume (the native id is resume) and an OpenCode session with usageProvider: null.

{
  "cutoffMs": 1786100000000,
  "bindings": [
    {
      "threadId": "import:claude-original:original-session",
      "provider": "claude",
      "providerInstanceId": "claude-default",
      "nativeSessionId": "5a128faa-8253-489e-b935-6c08e8e670c0",
      "origin": "runtimeCursor"
    },
    {
      "threadId": "import:claude-original:original-session",
      "provider": "claude",
      "providerInstanceId": "claude-original",
      "nativeSessionId": "original-session",
      "origin": "importedTranscript"
    },
    {
      "threadId": "thread-codex",
      "provider": "codex",
      "providerInstanceId": "codex-default",
      "nativeSessionId": "019fbbc1-b12c-7360-a685-28c181f0025f",
      "origin": "runtimeCursor"
    }
  ],
  "nativeSessions": [
    {
      "threadId": "thread-opencode",
      "providerName": "opencode",
      "adapterKey": "opencode",
      "providerInstanceId": "opencode-default",
      "nativeSessionId": "ses_opencode_1",
      "origin": "runtimeCursor",
      "usageProvider": null
    },
    {
      "threadId": "import:claude-original:original-session",
      "providerName": "claudeAgent",
      "adapterKey": "claudeAgent",
      "providerInstanceId": "claude-default",
      "nativeSessionId": "5a128faa-8253-489e-b935-6c08e8e670c0",
      "origin": "runtimeCursor",
      "usageProvider": "claude"
    },
    {
      "threadId": "import:claude-original:original-session",
      "providerName": "claudeAgent",
      "adapterKey": "claudeAgent",
      "providerInstanceId": "claude-original",
      "nativeSessionId": "original-session",
      "origin": "importedTranscript",
      "usageProvider": "claude"
    },
    {
      "threadId": "thread-codex",
      "providerName": "codex",
      "adapterKey": "codex",
      "providerInstanceId": "codex-default",
      "nativeSessionId": "019fbbc1-b12c-7360-a685-28c181f0025f",
      "origin": "runtimeCursor",
      "usageProvider": "codex"
    }
  ],
  "links": [
    {
      "threadId": "thread-opencode",
      "host": "github.com",
      "repository": "acme/repo",
      "number": 12,
      "source": "manual",
      "linkedAt": "2026-09-01T00:00:00.000Z",
      "url": "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/acme/repo/pull/12"
    },
    {
      "threadId": "thread-codex",
      "host": "github.com",
      "repository": "acme/repo",
      "number": 12,
      "source": "agent",
      "linkedAt": "2026-09-01T00:00:00.000Z",
      "url": "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/acme/repo/pull/12"
    },
    {
      "threadId": "thread-codex",
      "host": "github.com",
      "repository": "acme/repo",
      "number": 13,
      "source": "stack",
      "linkedAt": "2026-09-01T00:00:00.000Z",
      "url": "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/acme/repo/pull/12"
    }
  ],
  "diagnostics": {
    "bindings": {
      "runtimeRows": 3,
      "runtimeCursorBindings": 3,
      "importedTranscriptBindings": 1,
      "absentIdentityRows": 0,
      "malformedResumeCursors": 0,
      "malformedRuntimePayloads": 0,
      "skippedImportedTranscripts": 0,
      "unsupportedProviderBindings": 1,
      "overwrittenThreads": 1,
      "ambiguousSessionIds": 0
    },
    "links": {
      "rows": 3,
      "links": 3,
      "dismissed": 0,
      "malformed": 0
    }
  }
}

Allowlisted field semantics (one narrow interchange; no second report/reader is added):

  • cutoffMs — associations are read as of this instant; the projection stamps basis: "links-at-read-time".
  • bindings[] — usage-provider native session → T3 thread. provider is the canonical usage provider (claude | codex | grok), never the adapter key; origin is runtimeCursor | importedTranscript. Only providers with a scanned usage source appear here.
  • nativeSessions[]every native identity, including providers with no usage source. usageProvider: null for OpenCode/Antigravity/Cursor. This is a label, never a join key. providerName/adapterKey are the raw persisted values.
  • links[] — canonical thread → PR links from projection_thread_pull_requests. source is manual | created | agent | stack | stack-dismissed; tombstones are preserved here for the projection to filter. url is optional and only used to recover a Forgejo HTTP port.
  • diagnostics.bindingsabsentIdentityRows, malformedResumeCursors, malformedRuntimePayloads, skippedImportedTranscripts, unsupportedProviderBindings, overwrittenThreads, ambiguousSessionIds. What could not be read is reported, never silently dropped.
  • diagnostics.linksrows, links, dismissed, malformed.

Retained: nativeSessions for OpenCode with usageProvider: null; canonical links; ambiguity; cutoff/provenance; stack/dismiss behavior. The previous comment claiming a cursor carries exactly one field is corrected in source: Claude writes { threadId, resume } together for an imported session, and resume (the native session id) is preferred over threadId (the T3 thread id).

@nullStack65

Copy link
Copy Markdown
Owner Author

RESULT METRICS-1:M3C — finish T3 measurement and cache correctness

Status: complete (source + focused tests + docs; draft PR). Agent label METRICS-1:M3C. Session reference: unknown (openCode/T3 harness exposes no native session URL/id to this agent; not reused from METRICS-M3/M3R).

Touch set (11 files, +1070/-72)

apps/server/src/usage/{usageTranscripts,usageScanCache,usageAttribution,usageAttributionSources,UsageService}.ts + {usageTranscripts,usageScanCache,usageAttribution,usageAttributionSources,UsageService}.test.ts; docs/internals/usage-attribution.md. No endpoint/UI, PostHog, storage/history schema, collector, gateway/routing, install, merge, or activation change. Upstream pingdotgg/t3code untouched. #6's durable migrations (054/055) were not touched.

Dispositions of the three material residuals

  1. Numeric validity and completeness — FIXED. Fields are classified by actual value and provider required/optional semantics, not property presence. usage:{} stays invalid; {input_tokens:null}, {input_tokens:"missing"}, {input_tokens:-99}, NaN/Infinity are now invalid (no valid subset), not measured zeros; {input_tokens:10} is observed + measurementCompleteness:"partial" (required output_tokens absent), not measured; a complete explicit valid zero stays complete/measured; {input_tokens:10, output_tokens:null} is partial with invalidTokenFields:1, so an invalid value is distinguishable from an absent field. The completeness/validity metadata is carried parser → scan cache → projection, and the projection never infers a complete measurement from a nonzero total. Identity availability is a separate axis from numeric quality. Codex and Grok get the same treatment.
  2. Identity scope and cost-only conflicts — FIXED. dedupeKeyScope is now explicit at the boundary: global (Claude message.id:requestId, Grok sessionId:promptId:model) is namespaced by provider and collapses copies at another path; source-local (the Codex occurrence seam) is qualified by the canonical native session, never a physical path, so equal local keys in two sessions stay distinct. A global key reused under a second native session is incompatible ownership surfaced as a conflict (both sessions flagged), not a silent duplicate with a claimed exact identity. observationContent now includes costUsd and costSource, so a repriced/provenance-changed record is a conflict rather than a silent duplicate. Snapshot replacement is preserved where the source specifies it; unkeyed records remain counted and uncertain.
  3. Warm legacy cache enrichment — FIXED. A warm-cache hit now requires cached.identity === "declared", so an unchanged extant v3 transcript no longer returns early and is cold re-parsed once to enrich ids/presence (the re-parse replaces the entry, so totals do not double count). Deleted-transcript history is preserved; a read failure keeps the retained fallback rows and does not poison the cache. The legacy erased-identity marker (identityAvailable:false) is carried through to the projection, so a nonzero legacy row reports requestQuality:"unavailable" instead of "missing", without recovering certainty from token magnitude. Added a service-level regression with an unchanged live v3 source, a deleted v3 source, and a second scan after enrichment.

Exact commands / results (local, isolated worktree)

TMPDIR=/private/var/folders/l9/z97_k2450xv4z04_6wwsw5zh0000gn/T \
  vp test run apps/server/src/usage/        # 159 passed (9 files)
vp run --filter t3 typecheck                # exit 0
vp lint apps/server/src/usage/              # exit 0
vp fmt --check apps/server/src/usage/ docs/internals/usage-attribution.md   # clean

TMPDIR is pinned to its own resolved path because this host's /var/folders is a symlink to /private/var/folders; without it two pre-existing UsageService tests fail on macOS realPath resolution only (not a code defect — with the pin they pass). Baseline before the repair was 137 tests; the added cases are parser→projection exact cases, cache round trips, identity-scope/cost-conflict cases, the imported-cursor case, and the service-level legacy-enrichment regression.

Reproduced-before-fixing: with cached.identity === "declared" removed from the warm-hit guard, the new service-level regression fails at liveEntry.li === undefined (the live v3 entry never reaches the cold re-parse); restored, it passes.

Reconciliation evidence

usageAttribution.test.ts "reconciles attributed + shared + unallocated + orphan to the deduplicated input": measured is computed independently from raw records (including the orphan, excluding the known duplicate) and sum(PR.attributed) + shared + unallocated + orphan === measured holds after explicit duplicate/conflict handling. New identity tests assert that a source-local collision keeps both records (measured.outputTokens === 30, 0 duplicates/conflicts) while a global collision surfaces 1 conflict and keeps one version; a cost-only change surfaces 1 conflict and keeps-first. The service-level test asserts totals stay 12 across enrichment, second scan, and restart, and that the persisted cache is byte-identical after the warm second scan (enrichment happened once).

M1C bridge fixture

Posted as a separate #4 comment (#4 (comment)): the exact extractAttributionSnapshot output, generated by the real extractor from persisted-row shapes only at pin daa55eb88966665faa8c1770c35fc3c53a68f6fa, pinned by usageAttributionSources.test.ts "produces a stable fixture for the M1C interchange", with documented allowlisted field semantics. It includes the imported Claude cursor carrying both threadId and resume and an OpenCode session with usageProvider: null; the stale "a cursor carries exactly one field" comment is corrected in source. Linked from pingdotgg#231 for M1C. One narrow interchange; no second report/reader was added.

CI (honest, refreshed at corrected head)

Run for daa55eb88: the CI workflow's Test, Test Server 1/2/3, Rust, Release Smoke, Mobile Native Changes, Check, and Native fingerprint diff jobs are queued on unavailable blacksmith-* self-hosted runner labels the fork does not have, so they are not passed. GitHub-hosted jobs that ran passed (Collect PR targets, Prepare PR size config); CodeRabbit was skipped (draft); Deploy web preview/EAS Preview/Sync PR size label definitions skipped. No green CI is claimed. Fork/environment CI ownership stays with the existing environment lane (pingdotgg#237); this repair does not change runner policy or start an infrastructure effort.

Remaining limits and next action

  • liveQualified is false for every source; fixture-tested coverage is not live-qualified.
  • UsageProviderKind stays claude | codex | grok; OpenCode/Antigravity/Cursor remain ExtractedNativeSession with usageProvider:null. No OpenCode parser or public-union change.
  • Only the newest native session id per thread is durable here (runtimeCursor); earlier ids survive only via importedTranscripts. Durable history is [ROUTE6-1:T3] Durable model-canary attribution across resumes and model switches #6's scope.
  • Provider-instance identity is not recoverable from a transcript scan.
  • Next action: M1C consumes the bridge fixture to build the explicit adapter on the M1 side; [ROUTE6-1:T3] Durable model-canary attribution across resumes and model switches #6 must eventually rebase onto corrected head daa55eb88966665faa8c1770c35fc3c53a68f6fa after review of this PR.

Backlinks: https://github.com/nullStack65/closura-agent-config/pull/231 · https://github.com/nullStack65/closura-agent-config/pull/238 · follow-on owner notified on #6.

Model/harness: opencode-go/deepseek-v4.1-flash via opencode. Agent label METRICS-1:M3C; session reference unknown.

@nullStack65

Copy link
Copy Markdown
Owner Author

START METRICS-1:M3D — preserve zero/invalid usage and cache quality

  • Scope: repair the two review-identified residuals on this PR only.
    • REPAIR 1: apps/server/src/usage/usageTranscripts.ts zero-total filters (Codex returns null, Grok []/skip) erase eligible zero/partial/invalid quality evidence before classification and projection.
    • REPAIR 2: cache freshness — predecessor head e4f36af5ef279246bcb0f8463adeee8a09b7bde1 wrote 15-field v4 rows without current completeness metadata; current decoder defaults non-legacy observed rows to complete, so old-v4 inputs are silently promoted to complete and identity=declared bypasses the corrected parser on unchanged files.
  • Head at start: daa55eb88966665faa8c1770c35fc3c53a68f6fa (fetched; unchanged remotely)
  • Base/main: bcc1a58b19a9d610a4f08fed191a364767bc65b3
  • Isolated worktree from fetched PR head; uniquely named local branch; existing commits preserved; no force-push, no shared reset, no duplicate PR.
  • Session reference: unknown (harness exposes no native session URL/id; fresh session, not continued from an earlier agent session).
  • Out of scope: merge/install/deploy, new durable schema, routing, paid inference, recurring collection, runner/environment policy (Failing to create PR on Windows pingdotgg/t3code#237), [ROUTE6-1:T3] Durable model-canary attribution across resumes and model switches #6 migrations.

@nullStack65

Copy link
Copy Markdown
Owner Author

METRICS-1:M1D fixture note (from closura-agent-config pingdotgg#231).

The M1D core interchange fixture is now generated by the real extractAttributionSnapshot at this PR's reviewed head daa55eb88966665faa8c1770c35fc3c53a68f6fa (nullStack65/closura-agent-config docs/telemetry/fixtures/t3-attribution.snapshot.json + .provenance.json), signature { cutoffMs, runtimeRows, linkRows }. history/routeEvents are #6-only and live in a separate optional fixture pinned to 67b4c9c4.

The report side consumes nativeSessions (including usageProvider: null OpenCode rows) and qualifies the join by harness (providerName/adapterKey) and, where established, providerInstanceId. If #4 changes, the M1D lane refreshes the pin and inspects the extractor delta; no parser/cache ownership moves from M3D.

Retain eligible zero-total observations instead of dropping them before
classification. Codex no longer returns null for a zero subtotal, and Grok no
longer skips zero-total aggregate or per-model rows: a complete measured zero, a
known-zero subset, and an all-invalid payload all reach the projection with their
quality, while a container with no recognised token field is still no-usage.
Grok per-model cost accounting now reduces the aggregate for every emitted
ticked row, so an emitted zero-token row cannot double count.

Treat missing completeness metadata as unasserted. The predecessor v4 writer
emitted 15-field rows without it, and the decoder defaulted a non-legacy observed
row to complete, silently promoting an unknown measurement. Missing completeness
now decodes as partial, and the entry is marked qualityMetadata: "predecessor"
so warm-cache acceptance and resume both require the current quality format. An
extant file is cold re-parsed once; deleted and unreadable history is retained
conservatively. Native-id availability is a separate axis from numeric freshness.

Regressions cover Codex/Grok complete zero, partial zero, invalid, and
valid-plus-invalid distinct events, the parser-to-projection path, a pinned
predecessor fixture generated by the actual predecessor writer, and the
service-seam refresh/restart/deleted/failed-read paths.
@nullStack65

Copy link
Copy Markdown
Owner Author

RESULT METRICS-1:M3D — preserve zero/invalid usage and cache quality

Status: complete (source + focused tests + docs; draft PR updated in place). Agent label METRICS-1:M3D, round 4. Session reference: unknown (harness exposes no native session URL/id; fresh session, not continued from an earlier agent session).

Touch set (8 files, +829/−72): apps/server/src/usage/{usageTranscripts,usageScanCache,UsageService}.ts + {usageTranscripts,usageScanCache,usageAttribution,UsageService}.test.ts; docs/internals/usage-attribution.md. No endpoint/UI/PostHog/storage-schema/collector/gateway/routing/install/merge change; upstream pingdotgg/t3code untouched; #6 migrations (054/055) untouched.

REPAIR 1 — zero-total filters erase quality: FIXED

usageTranscripts.ts no longer drops an event at a zero subtotal before classification.

  • Codex: classification runs before the old if (totalTokens(totals) === 0) return null;. The record is emitted whenever the usage container has a recognised token field, carrying its real measurement (was hardcoded "observed"). A no-field container (last_token_usage: {}) is still no-usage.
  • Grok aggregate: if (totalTokens(...) === 0) return [] and the per-model if (totalTokens(totals) === 0) continue are replaced by classification.measurement !== "empty" emission. Only a no-field container is skipped.
  • Grok cost: an emitted ticked row now always reduces the aggregate before pro-rating, so an emitted zero-token ticked row cannot be double-counted onto its siblings; a row with no recognised token field (no-usage) does not swallow its ticks, preserving the old aggregate allocation for the emitted rows.

Independent execution of eligible source-shaped events now:

Input Before After
Codex {input_tokens:null,output_tokens:null} dropped (null) invalid, totals 0, retained
Codex {input_tokens:0,output_tokens:0} dropped (null) observed/complete, totals 0
Codex {input_tokens:10} partial partial (unchanged)
Codex valid 10+2 then separate all-invalid 1 record; invalid never reached malformedRecords 2 distinct retained records; invalid counted
Grok aggregate {inputTokens:null,outputTokens:null} [] 1 invalid record
Grok aggregate {inputTokens:0,outputTokens:0} [] 1 observed/complete
Grok per-model invalid/zero + valid zero/invalid rows skipped all 3 retained with quality

UsageService already counts measurement === "empty" || "invalid" as malformedRecords, so the invalid events now reach that loop and the session measurementQuality agrees with retained evidence — no double counting (Codex occurrence keys and Grok global keys dedupe as before).

Direct parser + parser→projection regressions added (usageTranscripts.test.ts 11 cases; usageAttribution.test.ts 4 cases; UsageService.test.ts Codex/Grok malformed-count service case). Existing Grok tests whose premise was "zero-token rows are never emitted" were updated to the repaired behavior (the all-explicit-zero sibling is now a retained complete zero with its own zero cost; the pro-rated siblings and their 0.75/0.25 split are unchanged, total still 1).

REPAIR 2 — cache measurement freshness: FIXED

Smallest parser-format/freshness repair, no version bump, no history discarded.

Supported-format policy (explicit):

  • v3 documents are read (unchanged): retained deleted-transcript history decodes with ids/presence unavailable; extant files cold re-parse once.
  • Predecessor v4 rows (15 fields, no completeness/validity/key-scope metadata): read conservatively. Missing completeness decodes as partial, never complete. The entry is marked qualityMetadata: "predecessor".
  • Only v1/v2 are rejected.
  • Warm-cache acceptance and incremental resume now require both identity: "declared" and qualityMetadata: "declared". Either stale → one full cold re-parse that replaces the entry (no double count). Deleted/unreadable history is retained conservatively; a failed read keeps the fallback rows and does not poison the cache.
  • Native-ID availability alone does not establish numeric metadata freshness — the two are separate axes.

The v3 identity-enrichment path is unchanged and still passes its service-level regression (one-time refresh, warm second scan byte-identical, restart, deleted source).

Before/after evidence. A fixture was generated by the actual predecessor writer (parseClaudeLine + encodeScanCache at pin e4f36af5ef279246bcb0f8463adeee8a09b7bde1) — receipt: a one-off worktree test at that pin parsed three Claude lines {input_tokens:10}, {input_tokens:null}, {input_tokens:4,output_tokens:6} and encoded one entry; the printed document is pinned verbatim in usageScanCache.test.ts (PINNED_PREDECESSOR_DOCUMENT, 15-field rows). Decoding that same fixture:

# pre-fix decoder sources at daa55eb88 (node 26 type-stripping, no deps)
BEFORE identity=declared completeness=["complete","complete","complete"]
# round-4 decoder sources
AFTER  identity=declared completeness=["partial","partial","partial"]

That is the silent promotion reproduced (the row whose source was {input_tokens:null} read as complete) and closed.

Service-seam receipts (UsageService.test.ts, real transcript tree + real persisted cache file):

  • refreshes a predecessor v4 quality cache entry once and keeps deleted history: first scan cold re-parses the live file (10) and keeps the deleted predecessor entry (7) = 17; persisted row is 18 fields with completeness code 1 (partial); second scan is a warm hit with byte-identical cache (one-time refresh); restart = 17; deleted entry retained with partial.
  • keeps retained fallback rows when an extant transcript cannot be read: predecessor row matches the file exactly, file chmod 000 forces open to fail; cached 9 tokens survive and nothing is zeroed.
  • Parser→session regressions assert malformedRecords and distinctSessions agree with retained evidence.

Exact commands / environment (local, honest)

Ran in WSL Ubuntu 24.04, Node v26.8.2, project-local vite-plus 0.3.3 / vitest 4.1.11 (invoked as ./node_modules/.bin/vp, not the global shim), from the worktree root:

vp test run apps/server/src/usage/    # 181 passed (9 files)   [baseline 159; +22 new]
vp run --filter t3 typecheck          # exit 0, no error diagnostics (only pre-existing suggestions)
vp lint apps/server/src/usage/        # 0 warnings, 0 errors
vp fmt --check apps/server/src/usage/ docs/internals/usage-attribution.md   # clean
git diff --check                      # clean

The previous result pinned TMPDIR for two macOS realPath tests; this host is Linux, so no TMPDIR pin was needed and all 9 files/181 tests passed as-is. The scripts/, oxlint-plugin-t3code/, and infra/relay workspace node_modules were hardlinked from the base clone to make tsc resolve effect; that was harness setup only.

CI (exact state for af7049544, refreshed)

Queued, not passed: Test, Test Server 1/2/3, Rust, Release Smoke, Mobile Native Changes, Native fingerprint diff, Check are pending on unavailable blacksmith-* self-hosted runner labels the fork does not have. Passed (GitHub-hosted): Collect PR targets, Label PR 4, Prepare PR size config, Label PR size. CodeRabbit pass (review skipped: draft). EAS Preview, Deploy web preview, Sync PR size label definitions skipping. No green CI is claimed. Fork/environment CI ownership stays with pingdotgg#237; no runner policy, paid workaround, provider, dashboard, or broader instrumentation was added.

Coordination

Preserved limits and smallest next action

  • liveQualified is false for every source; all coverage is fixture-tested, not live-qualified.
  • UsageProviderKind stays claude | codex | grok; OpenCode/Antigravity/Cursor remain ExtractedNativeSession with usageProvider: null. No OpenCode parser, no public-union change.
  • Provider-instance identity is not recoverable from a transcript scan; only the newest native session id per thread is durable here.
  • No durable per-thread session history, no DB/session-history migration, no OpenCode usage parser (M1D owns qualified matching and the current-head bridge fixture).
  • Smallest next action: M1D consumes the (unchanged) bridge fixture; [ROUTE6-1:T3] Durable model-canary attribution across resumes and model switches #6 rebases onto af70495442678a06e007d13bba477f123170c0f5 when the history owner is ready.

Backlinks: https://github.com/nullStack65/closura-agent-config/pull/231 · #6

Model/harness: opencode-go/deepseek-v4.1-flash via opencode. Agent label METRICS-1:M3D; session reference unknown.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant