feat(tui): scope the activity feed to the current day by default - #237
Merged
Conversation
The `v` activity feed (#235) showed every cycle the bounded read window held -- 77 of them on the real deployment, going back three weeks. The instruction is that the feed shows TODAY: "what has keel been doing" means today unless asked otherwise, and a fortnight of scrollback is not an answer to it. The scoping lives in the pure layer (`apply_scope`), not in the curses code, and takes its "now" as a parameter so the day boundary is injectable and every test of it is deterministic on whatever day it runs. "Today" is the local CALENDAR day -- derived through `datetime.date` in the same local clock `_stamp` renders timestamps in, so it survives DST (and errs early rather than late in the zones where midnight itself does not exist), and is not a rolling 24 hours, which would put yesterday's 09:00 cycle on screen every morning and drop it every afternoon. `t` inside the overlay cycles today -> 7 days -> all, and the scope resets to today on every open and every close. A widened view answers one question once; it never becomes tomorrow's default. The delicate part is that the deployment runs ONCE A DAY at 09:00, so "today" holds at most one row and none at all before 09:00 -- and a blank panel would be strictly worse than the state-only dashboard this whole feature exists to fix, since a blank panel and a dead agent look identical. So an empty day is never blank. It says, in this order: that keel has not run YET today; the timestamp of the last cycle and how long ago that was; and when the next one is due, inferred from that cycle's own time of day (or that its usual time has passed, which is the case worth acting on). Naming one timestamp is a status line, not a feed -- no historical row is rendered. The read bounds are unchanged (1 MiB / 5000 lines / 200 cycles / 400 events), and their interaction with a day filter is reported rather than glossed: `scope_fully_covered` records whether the window PROVED it reached back past midnight, and both the footer and the empty state say so when it did not, instead of letting an unread morning read as a quiet one. Real deployment log, unchanged on disk, at 11:53 today: scope: today (2026-08-11) · 1 cycle · 76 older hidden · press t to widen ▸ 2026-08-11 09:00:09 paper 0 0 0 0 0 gate rejected: choppy_regime (PAXG-USD) and the same log as it stood at 07:00 this morning, before that cycle ran: keel has not run yet today. Last cycle: 2026-08-10 09:00:01 -- yesterday, 21h 59m ago. Next cycle due today around 09:00 local -- in 2h 00m. Gates: ruff clean, mypy clean, 2664 passed / 1 skipped (from 2607 on main -- 57 new tests, same single expected skip). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 11, 2026
…the CTS scoring fix (#241) A minor bump, not a patch, for three reasons that each require operator action or change behaviour the deployment is currently relying on. SCHEMA. `SCHEMA_VERSION` goes 9 -> 10 (#223). Both deployed databases are at 9 and must be migrated before this build can use them. BEHAVIOUR REQUIRING OPERATOR ACTION. #223 adds a second attested claim -- what CONTRACT a venue listing is, not only what the underlying asset is. It fails closed with no backfill, deliberately, so after this lands `keel assets screen` REJECTS every product with `instrument_wrapper: UNATTESTED` until `keel assets attest-instrument` is run once per product. Live trading is unaffected: rail 1 gates buys on `config.allowlist`, not on the screen. LIVE SCORING CHANGED. #227 fixed `is_round_number`, which returned True for every 2dp-quoted price and so handed BTC/ETH/PAXG a free CTS point on every bar. Scores on those three assets are genuinely lower under this build than under 0.5.7. Also ships: the Robinhood crypto adapter behind the broker port (#216/#218/#222/#229, not wired to the live path), the TUI activity feed (#235/#237), the CTS factor collinearity study (#224), `Preview.synthetic` at the confirm gate (#221), rail 9 seeing a bracket's own stop (#212), and CI gating merges on the `test` check (#234/#238). Co-authored-by: Claude Opus 5 (1M context) <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.
Follow-up to #235, which merged before this landed. #235 added the
vactivity feed; this scopes it.The instruction
The feed as merged showed every cycle in the bounded read window — 77 rows on the real deployment, going back three weeks. It now shows today.
What "today" means
The local calendar day, midnight to now, derived through
datetime.datein the same local clock_stamprenders the timestamps in. Not a rolling 24 hours: on a deployment that runs at 09:00, a rolling window would put yesterday's cycle on screen every morning and drop it every afternoon, so the same day's feed would change shape depending on when it was opened.Going through
date→ naive midnight →.timestamp()uses the offset in force on that day, so a 23- or 25-hour DST day still starts where the civil day starts (a fixednow - 86400would drift an hour twice a year). In the few zones whose transition happens at midnight, so 00:00 does not exist, the conversion resolves an hour early — deliberately the safe direction, since a boundary that errs early can only ever include a cycle that belongs to today, never exclude one.The boundary is inclusive, so a cycle at exactly local midnight belongs to the day beginning then. There is deliberately no upper bound at
now: in live usenowis the current instant, so the only record an upper bound could exclude is one stamped in the future by a writer whose clock is ahead — and in this module of all modules, showing the anomalous row beats quietly shrinking the panel.Where it lives
keel/commands/activity.py, the pure layer —apply_scope(feed, scope, now_ts=...), a separate pass overfeed_from_lines's result rather than a parameter threaded through it. That keeps "parse the window" and "decide which days to show" independently testable, and keepsfeed_from_linesthe unscoped function every existing caller already relied on.now_tsis injected (defaulting totime.time()at exactly one seam), so no test in this change depends on the day it runs.run_livepasses the samenow_fnthe rest of the dashboard clocks itself with, so the day boundary can never disagree with the timestamps on screen.The toggle:
ttcyclestoday → 7 days → all, shown in the header. Verified free —q Q h ? i r a f s p d vare the dashboard's keys,k/j/Enter/Space the in-overlay ones, and nothing boundt.The scope resets to
todayon every open and every close. A widened view answers one question once; it never becomes tomorrow's default.The empty state — the important part
The deployment runs once a day, at 09:00. "Today" therefore holds at most one row, and none at all before 09:00. A blank panel there would be strictly worse than the state-only dashboard this feature exists to fix, because a blank panel and a dead agent look identical.
So an empty day is never blank. Rendered from the real
~/keel/logs/keel.logexactly as the overlay would have shown it at 07:00 this morning — the log filtered to the records that existed at that moment:Three lines answer "is keel alive" before any question of what it did. The next-run estimate is inferred from the last cycle's own time of day, taken from the log rather than from a schedule setting this module would otherwise have to be taught about and could then disagree with. When that time has already passed, it reads differently, because it is different news:
Naming one timestamp is not "displaying historical activity": it is a status line, not a feed. No historical row is rendered, nothing scrolls, and nothing about what happened that day is shown beyond when it began.
Three other empty cases are distinguished, in plain words:
logging.verbose: falsebeing the default that records only errorsalland still nothingNo cycles in the window …wording, unchangeddescribe_statuspath from #235 is untouched — missing / empty / oversized / unparseable / unreadable, each with its existing testA cycle that did run today and was quiet is still a row. The run of quiet cycles is the answer to "is it alive", and dropping them would reproduce the impression the feed exists to fix.
The read bounds, and the interaction they create
Unchanged: 1 MiB tail / 5000 lines / 200 cycles / 400 events per cycle.
But a
todayfilter over a bounded window can in principle miss part of today — a busy log could push this morning past the cap. A feed that filtered such a window and came back empty would be asserting something it cannot know. SoActivityFeed.scope_fully_coveredrecords whether the window proved it reached back past midnight (it did if it saw any cycle older than the boundary, or if it read the file whole), and when it did not, both the footer and the empty state say so:Nothing is silently hidden either: the header and footer both report how many older cycles the scope withheld.
Real data, before and after
Read-only against
~/keel/logs/keel.log; md58bb83b43ffa592aafdb37b84923b76ffbefore and after every run.Default (
today), as it renders right now:Widened with
ttoall— the same feed, unscoped (77 rows, head shown):77 rows collapse to 1, and the one that remains is the one that is about today.
Tests
57 new, covering exactly the cases that could go wrong:
7dincludes day 6 and excludes day 9;allreaches the 40-day-old one)emptywith itslogging.verboseadvice, not "keel has not run yet today"apply_scopefailing the test if it reads the wall clock instead of the injectednow_tsinf,nan, ±1e30) degrading to unbounded, never to emptytwidens, and reopening after widening is back to todayEvery existing test still passes. Two fixtures moved: the
run_livelog is now anchored onNOW_TS's own local day (it is a today-scoped overlay now, and a 2026-08-08 fixture would correctly render the empty state instead of the rows those tests are about), and the one end-to-end test whose two cycles are deliberately a day apart asks forscope="all"explicitly.Gates
Baseline on
main(re-measured at8829781, which carries both #234 and #235) is 2607 passed / 1 skipped; this adds 57 tests and the same single expected skip.🤖 Generated with Claude Code