fix: count the day in local time, not UTC (eod-gather + session-learner) - #34
Open
juanparisma wants to merge 2 commits into
Open
fix: count the day in local time, not UTC (eod-gather + session-learner)#34juanparisma wants to merge 2 commits into
juanparisma wants to merge 2 commits into
Conversation
`today` came from toISOString() and was matched with startsWith() against UTC observation timestamps. The reported day is therefore the UTC one, not the operator's. West of Greenwich the two disagree every evening: in UTC-5 the UTC bucket for a date runs from 19:00 the previous local day to 19:00 that local day. That is exactly when closes run. On one machine the safety net fires at 19:00, 21:00 and 23:30 local, so all three land on the wrong side of UTC midnight. The same day was reported as 27 observations by a gather at 21:43 and as ~522 by a close written at 21:08; the truth was 382. Changing only the date computation to local makes it worse: the prefix would then match the whole UTC bucket and always over-count (521 in the case above). The defect is comparing a local date against a UTC instant, so the fix is an explicit local [midnight, next midnight) window compared with Date.parse(). dayEnd comes from the calendar rather than +24h so it stays correct across a DST transition, where a local day is 23 or 25 hours long. The early-exit branch printed `date -u` too, and is now local for consistency. Tests: fixtures were "<UTC date>T10:00:00Z", which fall outside the local day every evening west of Greenwich; they now anchor to local noon. The new helper first passed its day offset through argv, where node parsed a leading "-1" as one of its own options and left YESTERDAY empty -- Test 5 kept passing because an empty timestamp is excluded exactly like a stale one. It goes through the environment instead.
Same defect as the gather. `today` came from toISOString(), so from 19:00 local onward in UTC-5 every date written here belonged to tomorrow. It surfaces as "Última sesión: <tomorrow>" in each project's context.md, which the gather injects into the agent's context at session start. The harm is not only cosmetic. The proposals file keys on session_date, so evening work started a new day instead of continuing the current one -- and the file is rewritten wholesale on each run, so that day's earlier proposals were dropped sooner than intended. Changing only that line would introduce a subtler bug: priorPropDates built its day set from `p.proposed_at.slice(0, 10)`, a UTC date. Mixing UTC-derived and local days in one Set makes a single local day count twice, and that Set feeds the "3+ distinct days" threshold that promotes a repetition proposal. Two proposals from the same local evening (say 10:00 and 22:00 in UTC-5) would have counted as two days. Both sides now go through localDayOf(), which converts the instant instead of slicing it. proposed_at stays UTC on purpose: it is an instant, not a day.
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.
The bug
Both
_eod-gather.shand_session-learner.shderive "today" withnew Date().toISOString().slice(0, 10), which is the UTC date. West ofGreenwich the UTC date and the operator's date disagree for part of every day:
in UTC-5 the UTC bucket for a given date runs from 19:00 the previous local
day to 19:00 that local day.
That happens to be exactly when closes run. On my machine the
/eodsafety netfires at 19:00, 21:00 and 23:30 local — all three land on the wrong side of
UTC midnight.
On 2026-07-30 the same day was reported two different wrong ways:
date: 2026-07-31, 27 observationsThe low number looks like a quiet day. The high one is worse: it silently
absorbs the previous evening, and nobody audits a total that looks plausible.
_session-learner.shshows it asÚltima sesión: <tomorrow>in every project'scontext.md— which the gather then injects into the agent's context at sessionstart.
Why not just make the date local
That was the first thing I tried, and it makes the gather worse: the
startsWithprefix then matches the whole UTC bucket and always over-counts(521 instead of 382 in the case above). The defect is comparing a local date
against a UTC instant, not how the date is computed.
So the gather now builds an explicit local
[midnight, next midnight)windowand compares instants with
Date.parse().dayEndcomes from the calendarrather than
+24h, so it stays correct across a DST transition where a localday is 23 or 25 hours long.
The learner had its own version of the same trap:
priorPropDatesbuilt its dayset from
p.proposed_at.slice(0, 10), a UTC date. Changing onlytodayto localwould mix UTC-derived and local days in one
Set— and thatSetfeeds the"3+ distinct days" threshold that promotes a repetition proposal. Two
proposals from the same local evening (10:00 and 22:00 in UTC-5) would have
counted as two days and promoted early. Both sides now go through
localDayOf(), which converts the instant instead of slicing it.proposed_atstays UTC on purpose: it is an instant, not a day.Also fixed: the test shared the bug
tests/test-eod-gather.shbuilt fixtures as"<UTC date>T10:00:00Z", which falloutside the local day every evening west of Greenwich — the suite would have gone
red on a correct implementation, depending on the hour it ran. Fixtures now
anchor to local noon, far from either midnight in any timezone.
One note on that helper, because it is a nice illustration of the failure mode
this PR is about: it first passed its day offset through
argv, where nodeparsed a leading
-1as one of its own options and printed nothing.YESTERDAYcame out empty — and Test 5 still passed, because an emptytimestamp is excluded exactly like a stale one. The offset goes through the
environment now.
Verification
tests/test-eod-gather.sh: 8 passed, 0 failed.github/workflows/tests.ymlgreen locally on a cleanclone at
a038b50hand-measured
[05:00Z, 05:00Z)window exactly;context.mdnow reads thelocal date; the instincts index is untouched
Scope
Three files, +81/−20. No workflow or packaging changes.