Skip to content

feat(explorer): add a Recent read sort — last deliberate agent read first - #718

Merged
mthines merged 4 commits into
mainfrom
feat/explorer-recent-read-sort
Oct 21, 2026
Merged

mthines merged 4 commits into
mainfrom
feat/explorer-recent-read-sort

Conversation

@dash0-dev

@dash0-dev dash0-dev Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Why

The Lore Explorer could sort by Newest or Most chosen, but not by what agents are reading now: a lifetime count cannot tell a lesson fetched forty times last spring from one fetched this morning.

What changed

  • The Sort control gains Recent read (?sort=last_opened_at): the last deliberate agent read first — a targeted memory.read / CLI read, never a bulk list/search page or a human opening the dashboard (00099's column).
  • Migration 00115 lets lorekit_memory_list order by it desc NULLS LAST (same signature); never-read lessons trail as a tail, so total is unchanged.
  • A third cursor codec, last-opened-cursor.ts (edge-mirrored, drift-checked), carries a nullable timestamp so paging continues inside that tail; both list routes accept the new value.
  • Docs, plus regenerated llms.txt and BYOD bootstrap.

How to verify

  • §115 in supabase/tests/migrations.test.sql pins the order, the total and a 4-page walk across a tie and the read → never-read boundary (Integration smoke runs it).
  • Pick Recent read in the Sort control on /lore; the URL gains sort=%22last_opened_at%22 and survives a reload.

Notes for reviewers

Under this sort a NULL p_cursor_ts with a set p_cursor_id means "inside the never-read tail", so the handler passes the id whenever a cursor decodes. The list route has no handler-level test, so that wiring is covered by review and §115's RPC-level walk only.

🧪 UI verification spec — run against the preview deployment

Target: preview
Format: intent

Spec 1: AC-1 — A reader sorts the Explorer by Recent read and the order survives a reload

Changed: the Sort control in the Lore Explorer toolbar (new "Recent read" option)
Start: /lore
Preconditions:

  • The signed-in account has at least one lesson, so the Explorer list renders.
  • The viewport is desktop width (the mobile Sort control is an icon-sized square with the same native select).
    Steps:
  1. Open the Lore Explorer.
  2. Open the Sort memories control next to the date picker and note its options.
  3. [must-follow] Choose Recent read in the Sort memories control.
  4. [must-follow] Reload the page.
  5. Choose Newest in the Sort memories control, restoring the default.
    Expected:
  • The Sort memories control offers exactly three options, in order: "Newest", "Most chosen", "Recent read".
  • POST /functions/v1/memories/list returns 200
  • After step 3 the page URL carries sort=%22last_opened_at%22.
  • After the reload in step 4 the Sort memories control still shows "Recent read".
  • After step 5 the page URL carries no sort parameter.
    Hints:
  • {role: "combobox", name: "Sort memories"} — packages/web/src/components/lore/SortSelect.tsx:88
  • Option labels — packages/web/src/lib/explorer-sort.ts (EXPLORER_SORT_LABELS)
  • The URL value is JSON-encoded (encodeURIComponent(JSON.stringify(value))) — packages/cli/src/shared/deeplink-pure.mjs:111
    Out of scope:
  • Which lesson appears first: it depends on the account's agent reads; the order itself is pinned by §115 in supabase/tests/migrations.test.sql.

mthines and others added 4 commits October 21, 2026 16:49
…d lessons last

00115 replaces the list RPC's body (same signature) so p_sort => 'last_opened_at'
orders by the last deliberate agent read, desc NULLS LAST with id as the
tie-break. The keyset reuses p_cursor_ts/p_cursor_id: a NULL timestamp with a
set id means the previous page ended inside the never-opened tail.
migrations.test.sql gains section 115.

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
GET /memories and POST /memories/list take the new order. It pages through
last-opened-cursor.ts ({ id, last_opened_at }, the timestamp nullable so a
page can end inside the never-opened tail), mirrored into the edge tree and
drift-checked like count-cursor.ts. Each cursor decoder rejects the other
formats, so flipping sort mid-pagination still restarts at page one.

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
The Sort control offers Newest / Most chosen / Recent read. Recent read is
?sort=last_opened_at: the lessons an agent deliberately read most recently
first, never-read lessons last. The option list, its labels and the URL
handling stay in lib/explorer-sort.ts; the control and the list query are
generic over it.

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
Grooming and self-improving-workflows pages, the llms.txt template (and the
regenerated llms.txt), the memories function's CLAUDE.md, the key-files index
and the regenerated BYOD bootstrap.

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

The dashboard preview for this PR — redeployed on each push that changes the web app.

Project Deployment Actions Updated
lorekit Ready Ready Preview Oct 21, 2026 5:11pm UTC

Preview always points at this PR's latest commit; Deployment is the immutable build of 3bd5f86. · Comment /web-preview to force a redeploy. · Workflow logs

@mthines
mthines marked this pull request as ready for review October 21, 2026 17:34
@mthines
mthines merged commit e19ebbf into main Oct 21, 2026
19 checks passed
@dash0-dev

dash0-dev Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Darkplane Automerge did not approve this pull request. Score 56.62 of 100, threshold 75. Blast radius 52, Evidence quality 88, Change footprint 30.

Darkplane Automerge did not approve this pull request. It scored 56.62 of 100, and 75 was required.

  • 🟡 Blast radius scored 52.
    The change is additive in intent (a new last_opened_at sort value, null-defaulted cursor, no new signature). But supabase/migrations/00115_explorer_recent_read_sort.sql uses create or replace on the shared lorekit_memory_list function and rebuilds the keyset and ORDER BY for the three existing sorts too. That function backs GET /memories, and the same SQL is duplicated in supabase/byod/bootstrap.sql. The change also touches schemas, the web explorer, the CLI and llms.txt, and a revert needs a forward migration restoring the 00114 function.
  • 🔴 Change footprint scored 30.
    The change rewrites the load-bearing lorekit_memory_list SQL function in supabase/migrations/00115_explorer_recent_read_sort.sql. It adds a hand-written nullable keyset (last_opened_at desc nulls last) inside dynamic SQL. The same function is repeated in supabase/byod/bootstrap.sql, and the cursor logic is duplicated in two new last-opened-cursor.ts files plus handlers/list.ts. It follows an existing sort-addition pattern (opened_count in 00113) and has SQL and unit tests, with the rest being docs and small web edits. Even so, the nullable-cursor pagination is new logic on a core read path.
The other criterion
  • 🟢 Evidence quality scored 88.
    The PR adds specs for the new cursor codec (last-opened-cursor.spec.ts), the sort (explorer-sort.spec.ts) and the schema. It also adds a 181-line §115 SQL test in migrations.test.sql covering order, total and a 4-page walk across a tie. CI is green, including Integration smoke, Playwright e2e, typecheck and Storybook, and mthines approved the evaluated commit. The one stated gap is that the list handler wiring has no handler-level test.

An AI evaluator produced these scores from the diff, the repository history, and the pull request text. The pull request text is author-controlled, so treat the scores as signals rather than proof. Reviewed for commit 3bd5f86.

Was this decision right? Give feedback

@lorekitbot lorekitbot Bot mentioned this pull request Oct 21, 2026
mthines added a commit that referenced this pull request Oct 22, 2026
* fix(web): write useUrlState changes with window.history so a reload keeps them

useUrlState's setter wrote the URL with router.replace, which commits the
address bar only once the route's server render returns. On PR #718's
preview that render took 4.3 s, and a reload inside that window brought
the Explorer back to Newest (aw-verify F-1).

The setter now writes with window.history.replaceState (pushState in push
mode, deduped like router.push). Next.js syncs both into useSearchParams
without a server render, so the URL holds the choice as soon as setState
returns. The write builds from the live location, so two keys set in one
tick both land. The clean-up effects keep using the router.

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>

* test(web): a sort choice is in the address bar the moment it is made

Interaction story on the Explorer: choosing Recent read must put
sort=%22last_opened_at%22 in window.location in the same step, and
Newest must remove it. Fails against the router.replace setter
(sort reads null), passes with the window.history write.

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>

* fix(web): one Back step for a handler that sets two push-mode keys

FilterBar's Clear all sets filters and then retention, both push-mode,
in one click. Writing each with pushState left two Back steps, the first
landing on a state the reader never saw. Within one task only the first
URL-changing push-mode write pushes; later ones replace that entry, as
the router used to collapse same-tick navigations.

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>

* chore(web): simplify pass — one-line docblocks, WHY comments at the lines they explain

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>

* docs(web): URL-state comments no longer describe router.replace as the write path

url-state-optimistic's header and the two MemorySidebarProvider guards
explained a router round trip and a same-tick clobber that the
window.history write from the live URL no longer has. The guards stay;
their reasons now read in the past tense.

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>

* fix(web): defer useUrlState's history write by a microtask

Next.js installs its window.history patch in an App Router effect. On a
hard load a child's mount effect in the same commit runs first, so a
setter called there (OrganizationManager clearing a stale ?org=) would
hit the raw History API and Next's router would never see the write.
React runs a commit's mount effects in one synchronous flush, so one
microtask later the patch is in; still before any paint or reload.

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>

* docs(web): the setter's URL write is a microtask later, not synchronous

Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: mads.thines <[email protected]>
Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
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