Skip to content

feat: add WorkBuddy (WorkBuddy AI) token and session support - #706

Open
MisonL wants to merge 41 commits into
nocoo:mainfrom
MisonL:feat/workbuddy-support
Open

MisonL wants to merge 41 commits into
nocoo:mainfrom
MisonL:feat/workbuddy-support

Conversation

@MisonL

@MisonL MisonL commented Oct 12, 2026 •

Copy link
Copy Markdown

Summary

Implements the WorkBuddy portion of #696 (QwenWork, DoubaoWork and the rest of
that list remain open). Adds WorkBuddy as a supported
AI coding tool, tracking both the domestic (~/.workbuddy) and international
(~/.workbuddy-ai) roots.

  • Token + session sync via a byte-offset JSONL driver with continuity
    anchors, mirroring the existing per-source driver architecture.
  • Dedup across files and syncs by providerData.messageId, so a session
    copied between the two roots is not counted twice.
  • Dashboard surface: palette (chart-16), source labels, per-source pricing
    fallback, ?source= allowlists and the landing/harness agent lists.
  • Registered in SOURCES; RETIRED_SOURCES unchanged. Total supported
    identities go from 15 to 16.
  • packages/worker and packages/worker-read need no change: they consume
    core's validator and pass source through to bound SQL parameters.

Parsing notes (both verified against a live install)

  • WorkBuddy writes both a cache-read decoy and an inclusive message counter;
    the normalizer picks the live value and splits miss+write /
    completion-reasoning.
  • ai-title, session-meta and file-history-snapshot rows are metadata, not
    messages; they are excluded from message counts.
  • Rows with unrepresentable timestamps are skipped per-line rather than losing
    the whole file.
  • A persistently-empty file no longer oscillates between a normal cursor and a
    full rescan every other sync.

Alignment with upstream

Rebased onto current main (now at 3.3.2, Next 16.4.0), which retired six collectors and added
antigravity. Commit 6944f641 reconciles the stale expectations, restores the
Grok session-progress line and updates the palette/label/pricing maps. The
later commits (fc8aad50, a9ff81b4, cc9bd6d5) tighten the review
findings from that rebase: fc8aad50 makes the source-filter parity assertion
real and restores retired-source coverage, a9ff81b4 drops stale
"alphabetical" claims, and cc9bd6d5 caps the workbuddy string fields (an
over-long model or sessionId would 400 every upload) and derives the L3
harness source count from core, and b00b42a9 gates timestamps on the server's
4-digit-year ISO window (a representable-but-far-future row would format as
+160413-…Z, also 400ing the batch). A later review round added three
more server-limit gates: e93b867e normalizes accounting record models
through usageLabel (a model that passes the token endpoint but not the
accounting endpoint's stricter label rule would leave accounting details
pending forever), and 9b1cbfce gates oversized providerData.messageId
values (which would bloat the cursor's dedup ring) and over-cap session
message counts (which would 400 and wedge every later session upload).

Testing

  • L1 unit + coverage: 268 files / 4420 tests green (one intermittent
    environment flake in scripts/__tests__/e2e-utils.test.ts aside — that file
    is untouched by this branch and passes on upstream main).
  • L2 CLI e2e: 13 tests green.
  • L3 API e2e: 102 tests green (re-verified at this HEAD on the local-bindings stack).
  • Per-commit tsc passes for every commit (the last one is docs-only).
  • source-filter-parity drives every VALID_SOURCES entry through all four
    ?source= routes and asserts 200 (mutation-checked: hardcoding a route's
    source list turns it red).

Note: L2/L3 jobs are skipped on fork PRs by .github/workflows/ci.yml
(if: ... head.repo.full_name == github.repository), so they run on the
merge-to-main push rather than here. Pre-push runs the full L2+L3+gitleaks stack locally on an internal-disk
clone; the network-attached working copy cannot meet the L3 dev-server
readiness budget.

🤖 Generated with Claude Code

MisonL and others added 21 commits October 14, 2026 10:54
Add "workbuddy" to the Source union and SOURCES, wire it through both
exhaustive sourceKey() switches, SyncResult/SessionSyncResult buckets,
DiscoverOpts, resolveDefaultPaths, cli isSource/SOURCE_LABELS/summaries,
status classifySource, enrich sourceOptions, the accounting-queue source
allowlist, and usesJsonlOffsetResume. Update the source-count test
expectations (9 -> 10 active sources, 18 -> 19 path properties).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parse ~/.workbuddy/projects/<slug>/<sessionId>.jsonl: one delta per
usage-bearing record, deduped by providerData.messageId, with inclusive
prompt/completion counters split into pew's disjoint TokenDelta buckets.

Two traps handled, both verified against real installs: the cache-read
count lives in prompt_tokens_details.cached_tokens / prompt_cache_hit_tokens
(cache_read_input_tokens and top-level cached_tokens are stale zeros), and
prompt_tokens / completion_tokens already include the cache read and the
reasoning tokens, so both are subtracted before bucketing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Discover ~/.workbuddy/projects/<slug>/<sessionId>.jsonl depth-2 only, so
subagent transcripts and tool outputs nested deeper are excluded by design.
Register a byte-offset token driver (per-parse messageId dedup, no cross-sync
id ring — WorkBuddy never rewrites a request's usage) and a full-scan session
driver using the mtime+size dual check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chart palette was capped at 15 colors, so add a 16th (leaf green,
chart-16) in light and dark themes. Also register workbuddy in the source
labels, the per-source pricing fallback defaults, the four ?source= query
allowlists, and the landing/harness agent lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add docs/52-workbuddy-support.md (data model, both usage traps, dedup key,
golden verification numbers, accepted limitations, change list) and register
it in the docs index. Update README/README.en/AGENTS supported-tool lists and
the PRIVACY.md read-only path disclosure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The JSON.parse catch branch was the only uncovered line in
parsers/workbuddy.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The workbuddy ?source= case ingests a usage row, so the by-device
breakdown test (which asserts exact device, timeline and detail-row
counts) must account for it the same way grok and zcode already do.

Verified with the full local e2e suites: L2 API e2e 100/100, L3 browser
e2e 91/91.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both "all 15 sources" cases listed every source except the one added in
this branch, leaving the validators untested for the new slug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…table

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tests

Complete the per-source enumeration tests for the workbuddy wiring, which
compared green only by omission before:

- status.test.ts: SourceDirs fixture was missing workbuddyDir; add it plus a
  classification case for cursors under ~/.workbuddy.
- session-sync-helpers.test.ts: sourceKey's mapped list predates workbuddy —
  and zcode/omp were missing too; add all three.
- continuity-anchor.test.ts: usesJsonlOffsetResume covers every offset-JSONL
  source explicitly; add workbuddy.

Each assertion verified by mutation: deleting the production wiring branch for
each source makes exactly that case fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
collectWorkbuddySessions excluded only rows without a sessionId, so on live
installs the ai-title row — which does carry a sessionId — was counted,
inflating totalMessages by one per session. Verify against a live install: the
two sample sessions reported 64 and 34, now correct at 63 and 33.

Exclude ai-title and file-history-snapshot by type instead. Time bounds are
unchanged (the title row never set them). The unit test now pins the aux row
with a sessionId present, and the parser header and docs/52 are corrected —
they previously claimed the title row had no session id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The international build installs under ~/.workbuddy-ai (product name
`workbuddy-ai`, endpoint workbuddy.ai, locale en-US) while the domestic build
uses ~/.workbuddy (endpoint copilot.tencent.com, zh-CN). Both keep the same
on-disk layout — projects/<slug>/<sessionId>.jsonl, identical workbuddy.db
schema — so the only gap was discovery: the driver read a single root.

Replace the singular workbuddyDir with workbuddyDirs: string[], defaulting to
both roots and following the vscodeCopilotDirs precedent. Discovery merges the
roots (deduped, globally sorted, missing roots tolerated); the token and session
drivers, status classification and paths all take the list.

Verified on a live machine: with a synthetic international session present the
dual-root scan reports 30 deltas / 3 files (29 domestic + 1 international) and
3 session snapshots; a domestic-only scan reports 29. Mutation-checked:
dropping to roots[0], dropping the -ai default, or classifying only the first
root each fail a test. The status fixture uses non-prefix-overlapping roots so
it cannot pass by accident.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WorkBuddy 5.7.6 (both the domestic ~/.workbuddy and international
~/.workbuddy-ai builds, same build 306add2a) writes a `session-meta` row that
carries a sessionId, so the session collector counted it — one extra
totalMessages per session, two per file on observed sessions. Exclude it by
type alongside ai-title and file-history-snapshot.

Verified on live 5.7.6 sessions: the two new files now report 32 and 53
totalMessages, matching an independent jq recount; a 5.5.x file without
session-meta is unaffected. The token parser needs no change — session-meta
carries no usage and is not matched by the '"usage"' fast filter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d shape gaps

Three defensive gaps found by review, all reproduced before fixing:

- Out-of-range timestamps crashed the whole file. A finite-but-unrepresentable
  epoch (e.g. 1e20) passed the Number.isFinite guard, then
  new Date(ts).toISOString() threw RangeError inside the parse loop, discarding
  every delta in the file and never advancing the cursor — so the file's usage
  was lost on every sync. Guard with isRepresentableEpochMs (|ms| <= 8.64e15)
  in both the token and session parsers; coerceEpochMs gets the same bound.
- The '"usage"' fast filter skipped rawUsage-only rows. "rawUsage" does not
  contain the substring "usage", so a line whose only usage shape was the
  documented primary rawUsage was dropped. Match '"rawUsage"' too.
- An empty rawUsage {} shadowed a populated message.usage, because ?? stopped
  at the empty object and normalization produced all-zero. Treat an empty
  object as absent so the fallback chain continues.

Verified on live data: the 68-request total is unchanged (prompt 4,267,488 =
cache 3,814,535 + uncached 452,953; completion 122,069 = output 53,211 +
reasoning 68,858). docs/52 corrected where it described the accounting group
shape, the discovery entry point and the data-model table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each added assertion is mutation-checked: deleting the production code under
test makes exactly the new case fail.

- workbuddy-parser: zero-token stub must not occupy a messageId slot the real
  row needs; camelCase providerData.usage fallback (array details carry the
  cache and reasoning split); prompt_cache_write_tokens alias; rawUsage-only
  rows and an empty rawUsage that must fall through.
- workbuddy-session: file-history-snapshot exclusion reachable with a
  sessionId; ISO string timestamp as a bound; out-of-range timestamp ignored.
- workbuddy-session-driver: multi-root discover, symmetric with the token driver.
- accounting-queue: a workbuddy-sourced record passes checkedRecord's
  hand-written source whitelist.
- usage/sessions route tests: it.each over every accepted source, so dropping a
  whitelist entry from VALID_SOURCES fails the unit suite, not only L2.
- pricing: every entry in SOURCES has a source-default fallback price.
- cli: isSource accepts every SOURCES entry (exported for the test).
- api-e2e: stale comment and "three devices" title corrected to four.

docs/52 documents the accepted no-cross-run-dedup limitation; CHANGELOG gets
the WorkBuddy entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mocked SyncResult/SessionSyncResult literals omitted workbuddy (and omp),
which tsc cannot see through the vi.mock factory. Add them so the fixture
matches the real result shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Count each providerData.messageId once per sync across both WorkBuddy
roots (domestic + international), and persist a bounded per-file ring on
the cursor, so a session copied between ~/.workbuddy and ~/.workbuddy-ai
— or a row re-emitted past the cursor — is no longer counted twice.

- parser: accept an external seenMessageIds set, return emittedMessageIds
- driver: WorkbuddyCursor ring + preload/needsReplay/resumeState/buildCursor,
  mirroring the Claude driver; keyed on seenMessageIds so preload() never
  pulls a sibling source's seenIds into the workbuddy set
- shared state: snapshot/restore the workbuddy set across discarded parses
- docs + changelog: replace the "no cross-file/cross-run dedup" limitations
  with the ring design; note the 5.7.6 startsNewUserRequest user-prompt row

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase onto upstream/main retired six sources and added antigravity,
which left a batch of stale expectations and a few real regressions.
Fold them into one commit:

- add the isWorkbuddyEmptyEpochCursor guard to prevent a persistently-
  empty workbuddy file from oscillating between legacy and modern cursors
- restore the Grok line in the session-sync progress output
- restore the docs index rows for 47-50 and renumber the workbuddy doc
- drop zcode/omp from the sourceKey expectations and cover the retired
  copilot roots, so status no longer replays them
- add the antigravity key to DEFAULT_SOURCE_DEFAULTS and widen the
  palette, session and usage test expectations to 16 sources
- cover the workbuddy discovery/parser/session paths the review flagged
  as false-green, and assert route acceptance for every VALID_SOURCES
  entry

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d coverage

Follow-up on the review of the previous commit. Three fixes:

- source-filter-parity asserted `not.toBe(400)` for every source, but the
  mocked DB reads returned undefined, so each route threw and answered 500 —
  which also satisfies `not.toBe(400)`. The gate was never actually exercised.
  Give the mock typed empty results and assert 200, so a route that drops the
  allowlist now fails the test. Mutation-checked: hardcoding the usage route's
  source list turns it red.
- The parity test comment (and the CHANGELOG/doc) claimed each route used to
  carry its own 15-source literal. It never did — all four gated on core's
  VALID_SOURCES before this branch. Reword to describe what the test guards.
- leaderboard.test's source-filter matrix had been narrowed from 14 literals
  (including the six retired sources) to [...SOURCES] (10 active). Iterate
  VALID_SOURCES instead to keep the retired-source coverage.

Also two robustness fixes on the new workbuddy paths:
- sync/session-sync activePaths now include opts.workbuddyDirs, so a workbuddy
  cursor is never mistaken for a retired one and dropped.
- status.classifySource iterates workbuddyDirs with a `?? []` guard, matching
  the optional-chained reads elsewhere.

And tighten AGENT_COLOR_MAP to Record<Source,...> so a new source can't be
added without a color.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…itle

The AGENT_COLOR_MAP entries are ordered by chart color number, not by source
slug (opencode precedes openclaw), so the title overclaimed. It has read that
way since before this branch; reword to describe what the case actually checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ling newline

The previous commit corrected the palette test title but left three siblings
that describe the same source-keyed maps as alphabetical when they are ordered
by chart color (opencode precedes openclaw). All three predate this branch;
reword them the same way rather than reorder the maps, which would churn many
assertions for no behavioral gain.

Also add the trailing newline the parity test was missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MisonL and others added 11 commits October 14, 2026 10:54
Two review findings, both fixed with a failing test first:

- The workbuddy parsers emitted `model` and the constructed
  `workbuddy:<sessionId>` session key with no length bound. The server
  rejects either over MAX_STRING_LENGTH with a 400, and the upload engine
  does not retry or skip a 4xx, so a single over-long row (a malformed
  sessionId, or a hostile edit to a synced project file) would wedge every
  later upload for the account. Drop the offending row at parse time, as
  antigravity already does, and keep the rest of the file.

- `leaderboard.spec.ts` asserted `toHaveCount(15)` for the harness source
  dropdown and titled the landing test "nine active collectors". Both were
  correct before this branch (9 active + 6 retired = 15) but adding
  workbuddy makes them 16 and ten, so the L3 gate would have gone red on
  merge to main. Derive the count from `VALID_SOURCES` and reword the title
  instead of re-hardcoding the next number.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ence

The fault-injection review found two doc-accuracy gaps in doc 52:

- The coverage table said "partial-line round-trip", but the tested case is a
  trailing partial line after complete content; a file whose FIRST line is
  unterminated falls to the shared continuity-anchor path (same as claude),
  so scope the claim to what the test pins.
- Section 4 did not say which copy wins when the two roots carry the same
  messageId with different values. Discovery sorts paths lexicographically
  and `.workbuddy-ai` (hyphen) sorts before `.workbuddy` (slash), so the
  international copy wins; write that down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…table

Fourth-round review follow-ups, both documentation-only:

- sync.ts's sourceKey default claimed a compile-time exhaustiveness check,
  but retired sources legitimately flow through it (they are Source members
  without drivers), so `source` is not `never` there and no `const _:
  never` assertion can compile — verified by attempting it. Reword the
  comment to state the actual invariant; the session-side switch keeps its
  real assertion.
- docs/52 §6 was missing the length-cap parser tests and the L3
  leaderboard spec change added by 15f664d.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cold-path review found that isRepresentableEpochMs allows ±275,760
years while the server's ISO regex only accepts a 4-digit year. A row
with, say, ts=5e15 passes the representable guard and formats as
`+160413-09-10T…Z`, which validateIngestRecord rejects with a 400 — and
the upload engine does not retry or skip a 4xx, so one such row wedges
every later upload for the account. The token parser now drops the row
via isValidISODate on its own output, and the session parser bounds
min/max with the same rule, so snapshots stay server-compatible by
construction. Mutation-checked: the new test fails without the guard.

Shared-baseline note: opencode and other coerceEpochMs consumers carry
the same wider window upstream; filed separately rather than swept here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2f82099 fix added an isValidISODate gate in both workbuddy parsers;
the data-model section still described the timestamp rule as only
"representable epoch", and the coverage table did not name the new test
cases. Align both with the shipped behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 4-digit-year gate landed in two places but not symmetrically:

- The session parser's ISO-string path ran Date.parse without the gate,
  and Date.parse accepts extended years ("+275760-…Z"). The resulting
  ms formatted back out of toISOString with a 6-digit year, which the
  server's ingest regex rejects — wedging the upload batch. Gate both
  timestamp paths once on the parsed ms.
- The token parser claimed the row's providerData.messageId before the
  ISO check, so a row dropped for an out-of-window timestamp still
  occupied the dedup ring — permanently suppressing a later corrected
  rewrite of the same request. Move the check before the claim, matching
  the zero-usage-stub rule.

Both behaviors pinned in workbuddy-timestamp-gate.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ddy coverage

Three behaviors had no test that would fail if the implementation changed:

- The three usage shapes can disagree: pin that rawUsage wins wholesale
  (no field-wise merging) when all three are present.
- The session key sits exactly at MAX_STRING_LENGTH: the boundary is kept
  (the existing overflow test only covered the drop side).
- The session parser's bounds reject a representable-but-far-future epoch,
  matching the token parser's server-window gate.

Also pin the dual-root same-id precedence end to end: a session copied
between the international and domestic roots counts once, with the
international copy's value, because discovery sorts paths and the
international root's file is parsed first into the shared dedup ring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dropdown gate derived its expected count from core, but only named the
retired sources explicitly — a duplicate label colliding with WorkBuddy's
would still pass on count alone. Name it in the visibility loop too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atch

Two review follow-ups:

- Doc 52 claimed the international copy wins whenever the two roots carry
  the same id with different values. That holds only when both copies are
  new in the same sync: if the domestic copy was counted in an earlier
  sync, its persisted ring suppresses the international copy instead.
  Scope the claim, and record the prune-loses-the-ring limitation next to
  the other accepted ones.
- classifySource matched the workbuddy roots with a bare startsWith, and
  `.workbuddy` is a string prefix of `.workbuddy-ai` — correct today only
  because both roots share a label. Require the path separator so a
  future sibling root cannot be misclassified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ISO-gate regression tests only covered the bad and corrected rows in
one parse. Add the two-sync shape — bad row dropped and offset advanced,
corrected row appended, incremental read resumes from the previous
endOffset — and record the timestamp-gate file in doc 52's coverage list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server rejects any token field over MAX_RECORD_TOKENS and any session
duration over MAX_SESSION_DURATION_SECONDS with a 400, and the upload
engine does not retry a 4xx — one over-cap record would wedge every later
upload for the account. Antigravity already gates both at its parser; the
workbuddy parser queueed such records instead:

- Two individually-valid timestamps on a clock-skewed machine (year 1 and
  year 9999) compose a duration of ~315 billion seconds. Drop the
  snapshot when its duration crosses the server's cap.
- Aliased inclusive counters can compose buckets over the token cap even
  when each raw counter stays under it. Check the composed buckets before
  emitting, after the ISO gate and before the id is claimed, so a dropped
  row also never occupies the dedup ring.

Both behaviors pinned in workbuddy-record-caps.test.ts, including the
exact-at-cap boundaries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MisonL
MisonL force-pushed the feat/workbuddy-support branch from e8c6d8b to 2ca503c Compare October 14, 2026 02:55
MisonL and others added 8 commits October 14, 2026 11:20
Community parsers (fuyi, Jimmy6188) also read cache-write from the nested
details objects — cached_creation_tokens / cache_write_tokens — and a build
that only fills the nested fields would land its write tokens in the miss
bucket. The top-level aliases stay first so the verified shapes win.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The changelog stated cross-root dedup without the 2000-id window bound
that doc 52 documents, and doc 52's coverage list missed the
workbuddy-record-caps tests added with the server-limit gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dual-root edges

status.ts required a literal "/" after the root, which never matches the
backslash paths a Windows cursor store produces — workbuddy cursors fell
into "unknown". Accept either separator after the root prefix.

Doc 52's dedup-key section now names the two real-world triggers of the
documented window bound: renaming a project slug directory re-sums ids
outside the ring once, and a root that temporarily disappears is
re-summed from scratch when it returns (the accounting merge keeps queue
values stable there).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

Records two design notes from the final logic review: the decoy read
aliases are last-resort and protected by firstPositive chain order, and
the miss fallback (prompt - read) assumes cache-write tokens are already
inside the miss total.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two coordinated changes are needed to retire workbuddy (RETIRED_SOURCES
plus a regex branch that only matches once workbuddyDirs leaves
paths.ts), and future cursor migrations must not fill seenMessageIds on
an empty-epoch cursor — the field's absence is load-bearing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The accounting ingest endpoint validates model with the strict label
rule (ASCII, at most 128 chars) while the token endpoint accepts any
non-empty string up to 1024. A delta whose model passes the token
upload but not the accounting one would make the details upload 400
forever (no retry on 4xx, dirty keys never clear), leaving accounting
details permanently pending. planAccountingUpdates now normalizes every
record's model through usageLabel — a plain ASCII model is unchanged —
and looks the token basis up by the raw model, since the token bucket
keeps its own key. Covered by accounting-model-label.test.ts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A corrupted or hostile file carrying huge providerData.messageId values
grows the cursor's dedup ring (2000 entries) to hundreds of megabytes,
taxing every later sync's cursor read/write; an over-long id is now
treated like a missing one, before the row can claim the id slot. A
session snapshot whose message counts exceed the server's
MAX_RECORD_MESSAGES would 400 and wedge every later session upload, so
the collector drops it — same wedge defense the duration and sessionKey
gates already had. The snapshot-builder part of the collector is
extracted into buildWorkbuddySessionSnapshots so the 1e8 count gate is
testable without writing 1e8 lines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
§6 records the message-count, messageId and accounting-label gate
tests; §2 records the nested cache-write aliases and widens the rawTotal
diagnostic wording to cover the invalid-counts branch; CHANGELOG gains
entries for the accounting model normalization and the new parser gates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MisonL
MisonL force-pushed the feat/workbuddy-support branch from 572615e to 1c34284 Compare October 14, 2026 06:24
…path

Two review-round findings worth recording: the 2000-id ring's measured
cursor cost (78-134KB per file, hundreds of MB at the 10k-file extreme)
and the fact that downgrading to 3.3.2 makes pew sync throw
"Invalid accounting ledger" on a WorkBuddy record until the queue file
is moved aside — retired sources never hit this because both versions'
whitelists contain their slugs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant