Skip to content

fix(tools): add timestamp/chain filters and newest-first ordering to ar_query_receipts#119

Merged
ojongerius merged 3 commits intomainfrom
fix/issue-118-query-filters-and-ordering
May 2, 2026
Merged

fix(tools): add timestamp/chain filters and newest-first ordering to ar_query_receipts#119
ojongerius merged 3 commits intomainfrom
fix/issue-118-query-filters-and-ordering

Conversation

@ojongerius
Copy link
Copy Markdown
Contributor

@ojongerius ojongerius commented May 1, 2026

Summary

Fixes #118. Two bugs in ar_query_receipts:

  • timestamp_after silently dropped: the parameter wasn't in the TypeBox schema and wasn't passed to the SDK query. timestamp_before and chain_id had the same gap. All three are now wired through.
  • Stale receipts returned by default: the SDK's ReceiptStore.query returns results ORDER BY timestamp ASC, so with a LIMIT 20 the tool returned the 20 oldest receipts, not the 20 newest. Results are now sorted newest-first in JS after fetching, then sliced to limit. Sequence number is the tiebreaker for receipts within the same millisecond.

What changed

  • src/tools.ts: three new parameters (chain_id, timestamp_after, timestamp_before) added to the schema and execute signature; sort+slice replaces the SDK-side limit; tool description updated with polling guidance; light ISO 8601 validation (consistent with how invalid risk_level/status are already handled).
    • timestamp_after is now exclusive (>): the SDK's after filter is >=, so polling with the last-seen timestamp would re-return that receipt. The JS post-filter drops any receipt whose timestamp exactly equals timestamp_after. Parameter description updated to say "exclusive".
    • chain_id added to result shape: each receipt in results now includes chain_id so callers can identify which chain it belongs to and use it in follow-up queries.
    • limit clamped to non-negative integers: fractional values (e.g. 5.7) and negative values (e.g. -1) fall back to the default of 20, consistent with how invalid risk_level/status are already silently ignored.
    • Default chain_id to current session's chain: when chain_id is omitted, results are scoped to the current session's chain (resolved via factory context). New boolean all_chains: true opts out and queries across all chains.
  • src/tools.test.ts: new and updated tests — 8 existing tests migrated to factory-with-context pattern; 10 new tests covering exclusive timestamp_after, chain_id in result shape, limit clamping (negative and fractional), same-millisecond sequence tiebreaker, session-chain defaulting, all_chains: true, and explicit chain_id override.
  • src/integration.test.ts: cross-session queries updated to pass all_chains: true (the tool is resolved at registration time with a mock context that doesn't match the session under test).

Out of scope (intentional follow-ups)

  • >10k-receipt blind spot — the SDK's DEFAULT_QUERY_LIMIT plus ASC ordering means stores with more than 10,000 matching receipts will silently lose the newest ones, even with this PR's JS-side sort. The proper fix is at the SDK level (DESC ordering and/or removing the silent cap) and applies equally to all language SDKs (TS, Go, Python). Tracked in ReceiptStore.query: support DESC ordering and remove silent default-limit cap ar#300.
  • Streaming / long-poll "follow" modear_query_receipts is request/response; a true tail -f pattern would require a different mechanism. Out of scope per this issue.
  • Cross-chain splinter detection (ADR-10) — the chain_id filter enables per-chain queries but doesn't enforce or detect cross-chain splits. ADR-10 tracks that separately.

Test plan

  • pnpm test — 171 tests, 7 files, all pass
  • pnpm typecheck — clean
  • Verify timestamp_after is exclusive: polling with last-seen timestamp no longer returns duplicate
  • Verify limit: 5 returns the 5 newest, not the 5 oldest
  • Verify chain_id in result shape
  • Verify default scopes to current session; all_chains: true returns all

…ar_query_receipts

Fixes #118.

Two bugs fixed:

1. `timestamp_after` (and `timestamp_before`, `chain_id`) were silently
   dropped because they weren't in the TypeBox schema or passed to the SDK
   query. All three are now wired through.

2. Default ordering returned the oldest receipts first (SDK uses ASC).
   Results are now sorted newest-first in JS after fetching (no SDK limit),
   then sliced to `limit`. Sequence number is the tiebreaker for receipts
   within the same millisecond.

The tool description documents the polling pattern: pass `timestamp_after`
set to the last-seen receipt's timestamp to fetch only new receipts since
then.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the ar_query_receipts agent tool so it can filter by chain and timestamp bounds, and so it returns newest receipts first instead of the SDK’s default oldest-first ordering. It fits into the plugin’s audit-trail surface by making receipt queries more useful for session inspection and polling workflows.

Changes:

  • Added chain_id, timestamp_after, and timestamp_before parameters to ar_query_receipts, with light timestamp validation.
  • Changed receipt query behavior to sort newest-first in tool code before applying limit.
  • Expanded unit tests for filtering/ordering and updated integration expectations to match descending results.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
src/tools.ts Extends ar_query_receipts parameters and changes result ordering/limit behavior.
src/tools.test.ts Adds focused tests for new filters, ordering, and invalid timestamp handling.
src/integration.test.ts Updates end-to-end assertions to match newest-first receipt ordering.

Comment thread src/tools.ts Outdated
Comment on lines +103 to +105
chainId: params.chain_id,
after,
before,
Comment thread src/tools.ts Outdated
Comment on lines +108 to +119
const limit = params.limit ?? 20;
const results = all
.sort((a, b) => {
const ta = a.credentialSubject.action.timestamp;
const tb = b.credentialSubject.action.timestamp;
if (tb < ta) return -1;
if (tb > ta) return 1;
// Tiebreak by sequence descending so calls within the same millisecond
// are still returned newest-first within their chain.
return b.credentialSubject.chain.sequence - a.credentialSubject.chain.sequence;
})
.slice(0, limit);
Comment thread src/tools.ts
Comment on lines +54 to +55
chain_id: Type.Optional(
Type.String({ description: "Restrict results to a single receipt chain." }),
Comment thread src/tools.ts
Comment on lines +97 to +119
// Fetch all matching receipts without a limit so we can sort newest-first
// in JS before slicing. The SDK only supports ASC ordering today.
const all = deps.store.query({
actionType: params.action_type,
riskLevel,
status,
limit: params.limit ?? 20,
chainId: params.chain_id,
after,
before,
});

const limit = params.limit ?? 20;
const results = all
.sort((a, b) => {
const ta = a.credentialSubject.action.timestamp;
const tb = b.credentialSubject.action.timestamp;
if (tb < ta) return -1;
if (tb > ta) return 1;
// Tiebreak by sequence descending so calls within the same millisecond
// are still returned newest-first within their chain.
return b.credentialSubject.chain.sequence - a.credentialSubject.chain.sequence;
})
.slice(0, limit);
Comment thread src/tools.ts
Comment on lines +115 to +117
// Tiebreak by sequence descending so calls within the same millisecond
// are still returned newest-first within their chain.
return b.credentialSubject.chain.sequence - a.credentialSubject.chain.sequence;
Comment thread src/tools.ts
Comment on lines +42 to +43
"To poll for new actions since your last check, pass `timestamp_after` set to the timestamp of " +
"the most recent receipt you've already seen.",
- Make timestamp_after exclusive (>): SDK is >=, narrow in JS post-filter
  before sort; update parameter description accordingly
- Add chain_id to result shape so callers can identify which chain each
  receipt belongs to
- Clamp limit to non-negative integers; fractional/negative values fall
  back to default 20 (consistent with how invalid risk_level/status are
  silently ignored)
- Test same-millisecond sequence tiebreaker (seq DESC wins)
- Default chain_id to current session's chain when omitted; add
  all_chains boolean opt-out to query across chains
- Update integration.test.ts queries that span sessions to pass
  all_chains: true; update tools.test.ts to construct tools via factory
  with matching session context
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment thread src/tools.ts
Comment on lines +114 to +115
const limit =
typeof params.limit === "number" && Number.isInteger(params.limit) && params.limit >= 0
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Misread of the diff: this commit (5dfb9e7) removed those casts. Current code at this line is the typeof-narrow form you're suggesting (typeof params.limit === "number" && Number.isInteger(params.limit) && params.limit >= 0 ? params.limit : 20) — no assertions left. Marking not applicable.

Comment thread src/tools.ts
Comment on lines +131 to +133
// drop any receipt whose timestamp exactly equals params.timestamp_after.
const filtered = after
? all.filter((r) => r.credentialSubject.action.timestamp !== after)
Comment thread src/tools.ts
Comment on lines +136 to +147
const results = filtered
.sort((a, b) => {
const ta = a.credentialSubject.action.timestamp;
const tb = b.credentialSubject.action.timestamp;
if (tb < ta) return -1;
if (tb > ta) return 1;
// Tiebreak by sequence descending so calls within the same millisecond
// are still returned newest-first within their chain.
return b.credentialSubject.chain.sequence - a.credentialSubject.chain.sequence;
})
.slice(0, limit);

Comment thread src/tools.ts
Comment on lines +118 to +126

// Fetch all matching receipts without a limit so we can sort newest-first
// in JS before slicing. The SDK only supports ASC ordering today.
const all = deps.store.query({
actionType: params.action_type,
riskLevel,
status,
limit: params.limit ?? 20,
chainId,
after,
@ojongerius ojongerius merged commit fb8ea74 into main May 2, 2026
5 checks passed
@ojongerius ojongerius deleted the fix/issue-118-query-filters-and-ordering branch May 2, 2026 22:08
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.

Audit trail queries ignore timestamp_after and return stale data

2 participants