You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Data API: with $count=true and $skip (or offset) but no $top/limit, total is the match count minus the offset #28547
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.
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.
Read fresh, and reproduced on two objects and both doors. With an offset and no limit, total short-counts by the offset. The Console always sends $top, so only API clients see it. p3.
Claim: PM loop round 1 · card thread · 2026-11-01T06:52Z
Session: session_01FEkQ5kE5BrPUPNMMyv6oCH (the seat); the card thread is session_01FEkQ5pNK9fm2KtGj8ijVqC
Account: 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:server
Seat: domain:server#3
Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」, and 2026-11-01T06:39Z 「server 队列还是很多任务,再开一个席位吧」. Triage 6554165818 grades p3. Expected result per the card: total is the filtered count whatever the offset.
File surface: packages/metadata-protocol/src/protocol.ts (findData pagination block), any other list door that computes total the same way, tests, and a changeset.
Container & model: S/M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
Clause-②: no — a declared field (total) made truthful; no accept set changes. The dev confirms against clause2-line.mjs.
Metadata-protocol: none (expected; the dev measures and declares)
Responsibility: the code is findData in 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 keeps let total = records.length while pageOffset is computed and unused there); the dev re-measures first
Serial constraints cleared: in-flight #28410 (seat 1) is in packages/metadata-protocol; the edit here is a few lines inside findData, merge origin/main once if a protocol.ts PR lands first. Held split #25467 is not in development.
Resume-from: none — fresh from origin/main
State: → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28547.md (project folder).
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"
]
}
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 MCP query, runtime fallback total) kept as Acceptance notes on the PR.
Landed: merged as 40e7bfc8; Fixes #28547 closed the card. A list read with an offset and no limit now reports total as the filtered row count, not the count minus the offset; $count=false still omits it.
Acceptance notes (not filed): stdio MCP query and the runtime fallback path still report records.length as total; cite this line if a card of that shape appears.
Claim 6554650640 closed by the merge. Resume-from: main at 40e7bfc8 or later.
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.