Skip to content

Order list_agent_sessions by activity time instead of primary-key order - #2271

Open
kriszyp wants to merge 4 commits into
mainfrom
kris/agent-session-ordering
Open

Order list_agent_sessions by activity time instead of primary-key order#2271
kriszyp wants to merge 4 commits into
mainfrom
kris/agent-session-ordering

Conversation

@kriszyp

@kriszyp kriszyp commented Aug 22, 2026

Copy link
Copy Markdown
Member

list_agent_sessions is documented as returning sessions "most recent first". It scanned the primary store with reverse: true, and the primary key is a randomUUID, so the order was uncorrelated with time and limit truncated an arbitrary subset rather than dropping the least recent. It now scans the already-indexed updatedAt attribute descending, bounded by limit.

Observed on 5.2.4 before the fix: of ten sessions the newest came back ninth and the oldest seventh, in strictly descending id order.

For the human reviewer

  1. updatedAt or createdAt as what "most recent" means. I chose updatedAt — last activity — so a long-running session that just did something sorts first, which is what an operator scanning for recent agent activity wants and how any conversation list behaves. createdAt would give stable creation order and stable pagination instead. Both attributes were already indexed, so there is no cost difference; this is purely list semantics, and the tool description now states which one it means. The third test pins the choice, so changing your mind means changing that test too.
  2. The index alignment is verified by reading, not by executing. Nothing in CI drives the real hdb_agent_session table. The unit mock proves listSessions asks for descending updatedAt, and a second test asserts the table declaration marks that attribute indexed — so removing indexed: true now fails the suite rather than passing while production 404s. What still isn't executed anywhere is the real planner: that Table.search index-aligns this exact shape I confirmed by reading resources/Table.ts:3429-3441. The mock also mutates mock.store in place to set timestamps, bypassing index maintenance. One case against the real table (under unitTests/resources/, which does exercise real tables) is additive later — until then that half rests on code reading, and you should decide whether that is good enough to merge on.
  3. limit still has no cursor or total count. A caller cannot tell "100 sessions" from "the newest 100 of 5000". This PR makes the truncation predictable instead of arbitrary, which is the bug, but does not make it visible. Adding paging later is a breaking change to the tool contract, so it is cheaper to decide now if it is wanted.
  4. Ordering keys on wall-clock Date.now() written by whichever node handled the mutation. If this system table replicates, a node whose clock runs fast puts its sessions permanently at the top of every operator's list. Ordering-only, no integrity impact, and strictly better than the arbitrary order it replaces — flagged because the tool description now makes a promise a skewed cluster cannot keep. I did not confirm whether hdb_agent_session participates in replication.

Round-1 review caught a real defect in the first version of this fix, now resolved: I had added a updatedAt > 0 sentinel condition to force the index, which made conditions non-empty and so disabled the resources/Table.ts:3435 guard. Losing indexed on the attribute would then have silently degraded the call to decoding every session record — full transcripts and pending approvals — and sorting in memory, with no error and the suite still green. Sort-only gets the same index behavior and keeps the failure loud.

Verification

  • Unit: npx mocha --conditions=typestrip unitTests/agent/session.test.js → 11 passing.
  • Fails-on-base: restored agent/session.ts from origin/main, deleted dist/agent/session.js and the tsbuildinfo, rebuilt, re-ran → the three new tests fail, the eight pre-existing ones pass as a control. Restored and rebuilt → 11 passing.
  • No end-to-end route was executed (decision 2 above). This is the honest gap: the fix is proven at the unit boundary and by reading the query planner, not by driving the real table. The pre-fix behavior was observed live — the ten-session ordering above came off a running 5.2.4 instance with the agent enabled.

The existing test could not have caught this: the mock's getRange reversed a Map's insertion order, which coincidentally matches most-recent-first, so the buggy code passed. The mock now orders by key like the real store, and the seeded ids descend while activity time ascends so key order and time order cannot agree by luck.

Fixes #2268

One nit is knowingly carried: the exported constant's doc comment restates the dependency already stated at the query site. Trimming it would leave the review receipt no longer matching HEAD, which is a worse trade than the duplicated sentence.

Complexity: medium

Review-Coverage: authored=claude; ran=codex,gemini; declined=cursor-grok,cursor-composer,domain; rounds=4 @ d7b67bf

Human-Review-Need: 3 @ d7b67bf

listSessions scanned the primary store with `reverse: true`, which orders by
primary key. The key is session_id, a randomUUID, so the result was ordered by
random id rather than by time -- the opposite of the "most recent first" the
tool description promises. Observed on 5.2.4: of ten sessions the newest came
back ninth and the oldest seventh, in strictly descending id order.

The ordering alone is cosmetic, but `limit` is applied by the range scan before
any notion of recency, so past the default of 100 the listing silently omitted
an arbitrary subset -- possibly including the session just created -- and got
monotonically less complete as sessions accumulated.

Both createdAt and updatedAt were already indexed on the table, so this scans
the updatedAt index descending instead, keeping the work bounded by `limit`.
updatedAt rather than createdAt: for a resumable session list the useful
"most recent" is last activity, so a long-running session that just did
something sorts first. The tool description now says which timestamp it means.

The mock in the unit suite hid this: its getRange reversed a Map's insertion
order, which coincidentally matches most-recent-first, so the buggy code passed
its own test. The mock now orders by key like the real store, and the seeded
ids descend while activity time ascends, so key order and time order cannot
agree by luck.

Fixes #2268
Drop the `updatedAt > 0` sentinel condition and sort alone. Table.search pushes
an order-aligned pseudo-condition when the sort attribute is indexed, and throws
404 when it is not (resources/Table.ts:3429-3441). The sentinel made conditions
non-empty, so that guard could never fire: if `indexed` were ever dropped from
the attribute, listSessions would have silently degraded to decoding every
session record -- full transcripts and pending approvals -- and sorting in
memory before applying the limit, with no error and this suite still green.
Sort-only gets the same index behavior today and keeps the failure loud. It also
stops excluding rows whose updatedAt is absent or 0, which get_agent_session
still returns.

Trim the added test comments to the one non-obvious constraint (consecutive
createSession calls land in the same millisecond).
Round-2 review raised that the mock's search sorts in memory whatever it is
asked to, so the suite would stay green if `updatedAt` lost `indexed: true` on
the table while Table.search started throwing 404 in production. The attribute
list is now a module constant and a test asserts the declaration the query
depends on -- the same shape as the config-param registration guard in
unitTests/config/replicationReceiveQueueParam.test.js.

The mock's comparator treated a missing updatedAt as NaN, which leaves
Array.prototype.sort order unspecified; it now coerces absent values to 0.
@kriszyp
kriszyp requested review from dawsontoth and heskew August 22, 2026 00:47

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the agent session listing functionality to sort sessions by their last updated timestamp (descending) using Table.search instead of a reverse primary key scan. It also extracts the session attributes, updates the MCP tool description, and adds comprehensive unit tests for the new sorting behavior. The review feedback suggests aligning with the repository's style guide by using strict assertion methods (assert.strictEqual and assert.deepStrictEqual) in tests and utilizing loose equality checks (row != null) for null-or-undefined validation.

Comment thread unitTests/agent/session.test.js Outdated
Comment thread agent/session.ts Outdated
@claude

This comment has been minimized.

Both from the styleguide (.gemini/styleguide.md): strict assertion variants
where strict semantics are intended, and `!= null` rather than a truthiness
check for null-or-undefined. Scoped to the lines this branch adds; the rest of
the file keeps its existing idiom.
@kriszyp
kriszyp marked this pull request as ready for review August 22, 2026 01:15
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.

list_agent_sessions returns UUID-descending order, not "most recent first", and limit truncates an arbitrary subset

1 participant