Repository navigation
Description
domain:devx#1 · sunhaolin · session_017xRBcUWM7N73xRVd64cyAE
QA-source: #28545 · api-backend.query-contract-matrix · observed beside the clauses
A list read that skips rows without naming a page size reports a wrong total: the number of matching rows minus the offset, instead of the number of matching rows. Once a limit is given, the same query reports the true count. Found by an off-clause probe and reproduced twice, on two sessions, two objects, and both the GET and POST doors.
Reproduction (stock showcase at eeec88d1, as admin; showcase_account has 14 seeded rows)
GET /api/v1/data/showcase_account?$count=true→ 14 records,total: 14.GET /api/v1/data/showcase_account?$skip=3&$count=true→ 11 records,total: 11.GET /api/v1/data/showcase_account?$skip=3&$top=100&$count=true→ 11 records,total: 14.POST /api/v1/data/showcase_account/query{"offset":3,"count":true}→total: 11.{"offset":999,"count":true}→total: 0.{"offset":14,"limit":5,"count":true}→total: 14.showcase_task(10 rows) behaves the same:?$skip=3&$count=true→total: 7, while?$skip=3&$top=100&$count=true→total: 10.
Expected: total is the number of rows matching the filter, whatever the offset, as it already is when a limit is present (and as #22690 / #22588 settled for $top=0).
Actual: without a limit, total equals the number of rows returned after the offset.
Mechanism
packages/metadata-protocol/src/protocol.ts, findData, around lines 14016–14019. When no limit is given (pageLimit === undefined), the code keeps let total = records.length on the reasoning that "without a limit the full result set is returned". That reasoning ignores pageOffset: it is computed on the line before and used only inside the pageLimit !== undefined branch. So an offset with no limit short-counts by exactly the offset.
Impact is low for the Console, which always sends $top. Every API client that pages with $skip alone sees a wrong total. Not a duplicate: #22588 ($top=0) and #22790 (search estimate) are closed and different.
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 1, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 可对外的 API | none | P②
Triage: p3 ·
bug·domain:server·area:api· 2026-11-01T05:57Z⛔ Not a claim.
totalshort-counts by the offset. The Console always sends$top, so only API clients see it. p3.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Claim: PM loop round 1 · card thread · 2026-11-01T06:52Z
Session:
session_01FEkQ5kE5BrPUPNMMyv6oCH(the seat); the card thread issession_01FEkQ5pNK9fm2KtGj8ijVqCAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-7ter86(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#3Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」, and 2026-11-01T06:39Z 「server 队列还是很多任务,再开一个席位吧」. Triage
6554165818grades p3. Expected result per the card:totalis the filtered count whatever the offset.File surface:
packages/metadata-protocol/src/protocol.ts(findDatapagination block), any other list door that computestotalthe same way, tests, and a changeset.Container & model:
S/M,mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: highClause-②: no — a declared field (
total) made truthful; no accept set changes. The dev confirms againstclause2-line.mjs.Metadata-protocol: none (expected; the dev measures and declares)
Responsibility: the code is
findDatain metadata-protocol · the platform path is the count arm the limit case already runs · reached by API clients sending an offset without a limit (the Console always sends$top)Thread-read: 6554165818
Premise: holds (read at
origin/main310cb96d7: the no-limit arm keepslet total = records.lengthwhilepageOffsetis computed and unused there); the dev re-measures firstSerial constraints cleared: in-flight #28410 (seat 1) is in
packages/metadata-protocol; the edit here is a few lines insidefindData, mergeorigin/mainonce if aprotocol.tsPR lands first. Held split #25467 is not in development.Resume-from: none — fresh from
origin/mainState: →
pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28547.md(project folder).objectstack-fleet commentedon Nov 1, 2026
domain:server#3 · os-project-manager · session_01FEkQ5pNK9fm2KtGj8ijVqC
os-dev-report
{
"issue": 28547,
"status": "done",
"branch": "claude/project-thread-7ter86",
"pr": "#28611",
"session": "session_01FEkQ5pNK9fm2KtGj8ijVqC",
"premise_still_valid": true,
"summary": "findData's paged arm now opens on pageLimit !== undefined || pageOffset > 0, so an offset with no limit runs the same count a limited read runs (same where/search/searchFields) and total is the filtered count whatever the offset; an offset past the end also reports the count (adding the offset back was rejected: it answers 999 for offset 999 over 14 rows). The $count=false arm keeps #10757 (no count, total omitted) and its hasMore now reads pageLimit !== undefined && records.length === pageLimit, so it is false with no limit. No offset and no limit, offset 0, and any limited read are unchanged. Premise held at 310cb96 (no-limit arm kept total = records.length); merged origin/main once at cd065d3 because #28583 touched protocol.ts (auto-merge, no conflict). Card assignee untouched; PR opened with no assignees and no labels (the order names none and a changeset ships, so no skip-changeset).",
"tests": "New packages/metadata-protocol/src/protocol.offset-no-limit-total.test.ts (11 cases, 14-row engine double honouring offset/limit). Red first: base protocol.ts blob 81ff63a0 written over the file -> 'Tests 8 failed | 3 passed (11)' (expected 11 to be 14 for GET $skip=3 and POST offset 3; expected +0 to be 14 for offset 999; expected 3 to be 4 filtered; count not called for $search; $count=false still reported total); restored via git checkout HEAD, blob 5dac1154 == HEAD, git diff HEAD empty. Ablation on the committed fix (ablation-replace.mjs, anchor 'if (pageLimit !== undefined || pageOffset > 0) {' -> 'if (pageLimit !== undefined) {'): 'mutation landed: anchor 1 -> 0, blob 5dac11542031 -> 161f280e49ec', 'Tests 8 failed | 3 passed (11)', 'restored: blob == HEAD (5dac11542031) and git diff HEAD is empty'; tests import src/ so no dist leg. Controls green in both: no offset/no limit (no count), offset 0 (no count), offset 3 limit 5 (one count, total 14, hasMore true). Package suite on merged head 0cd2449 (build + typecheck + test under the lock): VERDICT command-exit 0, 'Test Files 291 passed | 4 skipped (295)', 'Tests 21908 passed | 26 skipped (21934)'; typecheck includes src/**/* so it covers the new test. eslint packages/metadata-protocol --no-inline-config exit 0.",
"gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 0cd2449 -> 61 commands, each run with exit captured before any pipe (gate loop, stdin /dev/null, timeout 900): all 61 exit 0. --ran: '61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). check:type-check-debt: '0 raw tsc error(s) total, none above its recorded number'. First pass at d5bcd30 (pre-merge) also 61/61 accounted, with 3 prerequisite exits (teardown self-test shallow clone, dual-build-cjs-loads, lean-entry-closure) cleared after fetching the control commit and building objectql. pnpm lint scoped: eslint packages/metadata-protocol --no-inline-config exit 0. CI families: PR #28611 check-runs in_progress at report time (head 0cd2449); the seat reads their conclusions. test:repo not run locally (diff touches no repo-project test; CI Test Core runs it).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"line_budget": "222 changed lines (+219 / -3) in 3 files, under the 3000 human-merge threshold",
"files_changed": [
".changeset/28547-offset-no-limit-total.md",
"packages/metadata-protocol/src/protocol.offset-no-limit-total.test.ts",
"packages/metadata-protocol/src/protocol.ts"
],
"deviations": "none",
"mcp_calls": "0 — no GitHub MCP calls",
"api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, run 41286721273, read-back identical, assignees none); this os-dev-report comment (POST /repos//issues/28547/comments). Plus 1 git push (not a REST write). Write budget spent: 3 of 3.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed — packages/mcp/src/stdio-data-bridge.ts query and the no-protocol fallback in runtime/src/action-execution.ts callData report total = records.length whatever the offset or limit (page size as total even with a limit); read from source only, no public-door measurement, so not class (a); recorded in the PR Acceptance notes. Dedupe words: stdio bridge total records.length, query_records total page size, callData fallback total"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
ACCEPT → PR #28611 comment 6555440846 · head
0cd24491· 2026-11-01T08:19Z · landing sent (ready + auto-merge; no contract review owed: Clause-② no, no spec path, not a narrowing). Out-of-scope readings (stdio MCPquery, runtime fallback total) kept as Acceptance notes on the PR.objectstack-fleet commentedon Nov 1, 2026
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Landing record · #28547 · PR #28611
domain:serverseat 3 ·session_01FEkQ5kE5BrPUPNMMyv6oCH· 2026-11-01T08:50Zgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28611-*'git merge-base --is-ancestor 40e7bfc8 origin/maingit grep -c "pageOffset > 0" origin/main -- packages/metadata-protocol/src/protocol.ts40e7bfc8;Fixes #28547closed the card. A list read with an offset and no limit now reportstotalas the filtered row count, not the count minus the offset;$count=falsestill omits it.queryand the runtime fallback path still reportrecords.lengthas total; cite this line if a card of that shape appears.6554650640closed by the merge. Resume-from:mainat40e7bfc8or later.