Skip to content

ReceiptStore.query: support DESC ordering and remove silent default-limit cap #300

@ojongerius

Description

@ojongerius

Problem

ReceiptStore.query orders results ORDER BY timestamp ASC and applies a hard-coded DEFAULT_QUERY_LIMIT = 10000 when no limit is passed. Two consequences for any caller that wants the newest receipts (a common audit/UX pattern — "what just happened?"):

  1. Silent truncation past 10k. With >10k matching receipts, the SDK returns the 10k oldest and the newest are unreachable through the public query API. A caller that wants newest-first by sorting in memory still loses them — they were never returned.
  2. No way to ask for newest-first. Callers must fetch ASC, then reverse/sort in JS (or equivalent in other SDKs), which is wasteful when the underlying SQLite has an index on timestamp that could serve a DESC scan natively.

This affects every language SDK in this monorepo (TypeScript, Go, Python) since the protocol-level query semantics should be consistent across them.

Context

Surfaced while fixing agent-receipts/openclaw#118 — the openclaw plugin's ar_query_receipts tool was returning oldest-first because it inherited the SDK's ordering. The plugin-side PR (agent-receipts/openclaw#119) sorts in JS as a workaround, but that workaround silently fails past the 10k cap. Fixing this in the SDK is the proper resolution.

Proposed change

Add an order option to the query interface:

interface ReceiptQuery {
  // ...existing filters
  order?: "asc" | "desc"; // default "asc" for backwards compatibility
}

When order: "desc", emit ORDER BY timestamp DESC (with sequence DESC as a same-millisecond tiebreaker, matching how callers naturally want to read the audit trail). The existing idx_receipts_timestamp index serves both directions.

On the default cap

Separate question worth deciding: should DEFAULT_QUERY_LIMIT exist at all, or should an unset limit mean "all matching rows"? The cap protects against runaway memory use, but it does so silently — a caller passing precise filters genuinely expects to get every match. Options:

  • Keep the cap, surface it. Return a sentinel/flag when truncation occurred, so callers can detect it.
  • Drop the cap. Trust callers to pass limit when they need bounded result sets; let SQLite stream rows.
  • Raise the cap and document. Cheap stopgap.

Whichever direction this goes, it should be the same across all SDKs.

Equivalence across SDKs

This change should be applied consistently to:

  • TypeScript (packages/sdk-ts or wherever ReceiptStore lives in the monorepo)
  • Go SDK
  • Python SDK

The protocol/spec docs in this repo should also document the query ordering contract so external implementations behave the same.

Acceptance

  • query({ order: "desc" }) returns receipts newest-first across all three SDKs
  • Decision made on default-limit behavior and applied uniformly
  • Spec/docs updated to define query ordering semantics
  • Migration note for downstream consumers (e.g. openclaw plugin) so they can drop in-language workarounds

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions