Repository navigation
fix(pulse): cut the usage day in the principal's timezone - #2294
Open
jacobo-ortiz wants to merge 1 commit into
Open
jacobo-ortiz wants to merge 1 commit into
jacobo-ortiz wants to merge 1 commit into
Conversation
Three places cut the day in UTC: UsageAggregator buckets usage-daily.jsonl rows by the first ten characters of a session's first timestamp (the UTC date for Z-stamped rows), modules/usage.ts computes today and the 7/30-day cuts from toISOString(), and Performance/module.ts keys its daily costs and failure trend by timestamp.slice(0, 10). For a principal west of UTC every request made in the evening lands on the next calendar day, so the Usage tab's "today" is empty until UTC midnight and the two tabs disagree about what happened on a given date. Rolling N x 24 h windows (the cost and summary cutoffs) are windows, not day keys, and stay as they were. LifeosConfig.ts, which already owns [principal].timezone, now exports two helpers. principalTimeZone() returns the configured IANA zone when the config loads and Intl knows the zone, else "UTC", with the same try/catch shape as paiUserDir() so Pulse paths and the launchd job never throw on a missing config. dayKey(timestamp, tz) returns the "YYYY-MM-DD" of an instant in that zone through a memoised Intl.DateTimeFormat, or null when the timestamp does not parse. The aggregator's day bucket, daysAgo() in usage.ts (today, week, month and the 30-day models window all derive from it; isoWeek is calendar arithmetic on the key and needs no change) and the three day keys in Performance/module.ts use them. With "UTC" every Z-stamped input yields the same day as before, byte for byte. Migration: the aggregator rebuilds usage-daily.jsonl from session-costs.jsonl and the live transcripts on every run and never reads or appends to the previous file, so rows written with the UTC cut keep their date only until the next run (nightly at 03:30, RunAtLoad, or a manual `bun LIFEOS/TOOLS/UsageAggregator.ts`), which re-cuts every row that still has a source. Known residue: tool-activity and tool-failures rows are stamped by the hooks with the principal's local offset, so on an install without a valid LIFEOS_CONFIG.toml (UTC fallback) the Performance trend keys for those rows move from the writer's local day to the UTC day; with a configured zone they land where they did. The aggregator still assigns a whole session to its first timestamp while the Performance tab uses the last, and the Performance session table keeps a UTC date in the UI; both are outside this change. Test: LIFEOS/PULSE/test/day-key.test.ts (bun:test only, synthetic fixtures in a temp dir) drives the helpers, the aggregator as a subprocess and both modules in-process; a `test` script is added to PULSE/package.json. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Three places cut the day in UTC, and two of them disagree with each other:
TOOLS/UsageAggregator.tsusage-daily.jsonlrows by the first ten characters of the session's first timestamp (the UTC date for Z-stamped rows)PULSE/modules/usage.ts(daysAgo,buildSummary)todayand the 7/30-day cuts fromtoISOString()and filters daily rows withd.date === todayPULSE/Performance/module.ts(the three.slice(0, 10)day keys)For a principal away from UTC the day boundary is wrong by their offset: every evening request lands on the next calendar day, the Usage tab's "today" stays empty until UTC midnight, and the two tabs put the same session on different days.
Measured on a UTC-5 host over one month (2026-10-02, deduplicated transcripts priced per request): 16.3% of the month's cost has its requests between 00:00 and 05:00 UTC (19:00 to 24:00 local), so it is shown on the wrong local day.
LifeosConfigalready requiresprincipal.timezone; nothing in Pulse read it for day math.Fix
TOOLS/LifeosConfig.tsexports two helpers.principalTimeZone()returns the configured IANA zone when the config loads andIntlknows the zone, else"UTC", with the same try/catch shape aspaiUserDir(), so Pulse paths and the launchd job never throw on a missing config.dayKey(timestamp, tz)returns theYYYY-MM-DDof an instant in that zone through a memoisedIntl.DateTimeFormat(assembled fromformatToParts, so no locale separators leak in), ornullwhen the timestamp does not parse.daysAgo()inusage.ts(today, week, month and the 30-day models window derive from it;isoWeekis calendar arithmetic on the key and needs no change) and the three day keys inPerformance/module.tsuse them.daysAgosubtracts calendar days from the local key instead of multiples of 24 h, so a DST change cannot move the cut.cutoff = now - days * 86400000) are unchanged: they are windows, not day keys."UTC", or with no config, every Z-stamped input yields the same day as before, byte for byte.Migration and residue
usage-daily.jsonlfromsession-costs.jsonland the live transcripts on every run and never reads or appends to the previous file, so rows written with the UTC cut keep their date only until the next run (nightly at 03:30,RunAtLoad, or a manualbun LIFEOS/TOOLS/UsageAggregator.ts).tool-activityandtool-failuresrows are stamped by the hooks with the writer's local offset; on an install with no validLIFEOS_CONFIG.toml(UTC fallback) the Performance trend keys for those rows move from the writer's local day to the UTC day. With a configured zone they land where they did.healthsync/store.tshas its owndayKey(epochMs, zone); the two could be unified later.Test
PULSE/test/day-key.test.ts(bun:test only, synthetic fixtures in a temp dir, zone pinned throughLIFEOS_CONFIG_PATH, clock pinned withsetSystemTime): the helpers; the aggregator as a subprocess; both modules in-process. A request at 03:00Z withAmerica/Bogotalands on the previous local day in all three sites (red before the fix: on the UTC day); without a config the UTC day is kept (green before and after).bun testfromLIFEOS/PULSE: 8 pass; atestscript is added toPULSE/package.json. First*.test.tsin the tree; happy to move it wherever you prefer tests to live.Related: #2284 (item 5 is this defect; it suggested a separate local-day series, but
usage-daily.jsonlis rebuilt from scratch on every run and has one reader, so this re-cuts the existing series instead) and #1783 (same class of defect, in healthsync).