Skip to content

feat(tui): scope the activity feed to the current day by default - #237

Merged
eaitbrahim merged 1 commit into
mainfrom
feat/tui-activity-today-scope
Aug 11, 2026
Merged

feat(tui): scope the activity feed to the current day by default#237
eaitbrahim merged 1 commit into
mainfrom
feat/tui-activity-today-scope

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Follow-up to #235, which merged before this landed. #235 added the v activity feed; this scopes it.

The instruction

"make sure the activity displayed in the TUI is for the current day, don't display historical or old activities"

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.date in the same local clock _stamp renders 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 fixed now - 86400 would 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 use now is 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 over feed_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 keeps feed_from_lines the unscoped function every existing caller already relied on. now_ts is injected (defaulting to time.time() at exactly one seam), so no test in this change depends on the day it runs.

run_live passes the same now_fn the rest of the dashboard clocks itself with, so the day boundary can never disagree with the timestamps on screen.

The toggle: t

t cycles today → 7 days → all, shown in the header. Verified free — q Q h ? i r a f s p d v are the dashboard's keys, k/j/Enter/Space the in-overlay ones, and nothing bound t.

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 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.log exactly as the overlay would have shown it at 07:00 this morning — the log filtered to the records that existed at that moment:

keel tui -- activity
scope: today (2026-08-11)  ·  0 cycles  ·  76 older hidden  ·  press t to widen

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.

That one line is all the history this panel shows: it is scoped to today.
Press t to widen the scope: today -> 7 days -> all history in the window.

source: /Users/…/keel/logs/keel.log  (315 records in window)
scope today (2026-08-11): 76 older cycle(s) in the window are hidden -- press t to widen

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:

keel has not run yet today.
Its usual start time today (09:00 local) passed 5h 00m ago.
If no row appears here shortly, check that the agent's schedule is still running.

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:

case what it says
today has none, earlier days do keel has not run yet today + the last run + the next due (above)
today has none and neither has any earlier day says so, and points at logging.verbose: false being the default that records only errors
widened to all and still nothing the pre-existing No cycles in the window … wording, unchanged
the log could not be read every describe_status path from #235 is untouched — missing / empty / oversized / unparseable / unreadable, each with its existing test

A 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 today filter 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. So ActivityFeed.scope_fully_covered records 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:

COVERAGE UNPROVEN: the bounded read begins inside today, so earlier cycles from today may
exist in the log but outside what was read -- this is NOT evidence that today was quiet

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; md5 8bb83b43ffa592aafdb37b84923b76ff before and after every run.

Default (today), as it renders right now:

scope: today (2026-08-11)  ·  1 cycle  ·  76 older hidden  ·  press t to widen

   when                 mode    sig  blk  ent  exi  err  what happened
 ▸ 2026-08-11 09:00:09  paper     0    0    0    0    0  5 products / turtle_breakout  gate rejected: choppy_regime (PAXG-USD)

source: /Users/…/keel/logs/keel.log  (329 records in window)
scope today (2026-08-11): 76 older cycle(s) in the window are hidden -- press t to widen

Widened with t to all — the same feed, unscoped (77 rows, head shown):

scope: all history in the window  ·  77 cycles  ·  press t to widen

   when                 mode    sig  blk  ent  exi  err  what happened
 ▸ 2026-08-11 09:00:09  paper     0    0    0    0    0  5 products / turtle_breakout  gate rejected: choppy_regime (PAXG-USD)
 ▸ 2026-08-10 09:00:01  paper     0    0    0    0    0  5 products / turtle_breakout  quiet -- looked, nothing to do
 ▸ 2026-08-09 09:00:04  paper     0    0    0    0    0  5 products / turtle_breakout  quiet -- looked, nothing to do
 ▸ 2026-08-08 09:00:06  paper     1    1    0    0    0  5 products / turtle_breakout  rail veto: per_asset_concentration_cap | rail veto: monthly_subscription_allowance | not placed: paper: vetoed by rails
 ▸ 2026-08-07 09:00:05  paper     0    0    0    0    0  5 products / turtle_breakout  quiet -- looked, nothing to do
 ▸ 2026-08-06 09:00:03  paper     0    0    0    0    0  5 products / turtle_breakout  quiet -- looked, nothing to do
 ▸ 2026-08-05 09:00:01  paper     0    0    0    0    0  5 products / turtle_breakout  quiet -- looked, nothing to do
 ▸ 2026-08-05 08:12:58  --        0    0    0    0    2  uncorrelated events (no cycle_id)  error: cb_client.accounts_fetch_failed | error: executor.quote_fetch_failed
 … 69 more rows, back to 2026-07-20

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:

  • today with several cycles, with exactly one, and with none while earlier days have some (the pre-09:00 case — asserts the empty state names the last run and the next due time)
  • all three scopes over one fixture, with exact expected counts (7d includes day 6 and excludes day 9; all reaches the 40-day-old one)
  • a cycle at exactly local midnight landing on the day that begins then — and being out of scope again seen from the following day
  • an entirely empty log still reporting empty with its logging.verbose advice, not "keel has not run yet today"
  • the empty state being non-blank for every scope
  • coverage proven / unproven, and the footer refusing to imply a quiet day when the window cannot prove it
  • apply_scope failing the test if it reads the wall clock instead of the injected now_ts
  • a nonsensical clock (inf, nan, ±1e30) degrading to unbounded, never to empty
  • the live loop: opens at today, t widens, and reopening after widening is back to today

Every existing test still passes. Two fixtures moved: the run_live log is now anchored on NOW_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 for scope="all" explicitly.

Gates

$ uv run ruff check keel tests packages scripts
All checks passed!

$ uv run pytest -q
2664 passed, 1 skipped in 38.15s
SKIPPED [1] packages/keel-broker-api/keel_broker_api/conformance/suite.py:254:
           adapter serves no granularity the suite could exercise

$ uv run mypy
Success: no issues found in 221 source files

Baseline on main (re-measured at 8829781, 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

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 eaitbrahim added the enhancement New feature or request label Aug 11, 2026
@eaitbrahim
eaitbrahim merged commit ec8aa29 into main Aug 11, 2026
@eaitbrahim
eaitbrahim deleted the feat/tui-activity-today-scope branch August 11, 2026 16:15
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant