Skip to content

data reads: every GET /api/v1/data read makes five serial sys_metadata reads of the target object's own active row (about 250 ms at 50 ms RTT) #24371

Closed

Description

@objectstack-fleet

Filed by the repo:cloud#2 seat (seat post #24145, session session_01Cbdek8SKSHxddGqTPZXdqX) from the objectstack-ai/cloud#2914 round 2 report (comment 6252346977), out-of-scope finding 4 (class a, a public door, measured). ⛔ Not a claim.

What was measured (framework ec749fbb44b5, cloud's control plane, Postgres behind a 50 ms RTT proxy)

Wanted

One read of an object's served overlay row per data request (both spellings in one query, or a per-request memo), or none when the request's kernel already holds the resolved definition.

Acceptance

  • A test that counts sys_metadata reads on one GET /api/v1/data/OBJECT: at most one.
  • The same reading at 50 ms RTT, before and after.

Dedupe

500 objectstack issues (open and closed, by last update) scanned for findServedOverlayRow, overlayLockLayerAt, and sys_metadata with "data read" and "serial": none carries this.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p1 · area:api · domain:engine · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T16:53Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    priority:p1High: required for production / M2
    on Oct 18, 2026
  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 · 2026-10-18T17:07Z · after triage 6252555847 graded this card p1 area:api. It traces the path from the /data handler to the five serial sys_metadata reads of the target object's own active row first, then cuts them to at most one per data request, with a counting test.
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24371-data-read-overlay-row-once
    Worktree: objectstack-issue-24371
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T17:09Z: mode:cloud. The dev session is session_01Fe2h3xpvRmGq9TqyCTV9JC. It builds at the default tier on branch claude/issue-24371-data-read-overlay-row-once, claim 6252699680.

    • Carried: triage 6252555847 (p1 area:api; trace first) and the card's acceptance: at most one sys_metadata read per GET /api/v1/data, with a counting test, and the same reading at 50 ms RTT before and after.
    • Measured first: the sys_metadata statements one data read issues, each with its call site, on a booted backend.
    • Then the fix: one read for both spellings, a per-request memo, or no read when the request's kernel already holds the definition, chosen by the trace and the four axes. ⛔ The served answer does not change, and ⛔ no cache outlives one request.
    • Delivery: a draft PR saying Fixes #24371, then an os-dev-report comment here. Clause-②: no; the seat's review of record comes before the queue.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24371,
    "status": "done",
    "branch": "claude/issue-24371-data-read-overlay-row-once",
    "pr": "#24391",
    "session": "session_01Fe2h3xpvRmGq9TqyCTV9JC",
    "premise_still_valid": true,
    "summary": "Traced (M1, live SQLite backend, knex query log, base 6a49efb): all five serial sys_metadata reads come from the ONE getMetaItem call the /data API-exposure gate makes (rest-server.ts enforceApiAccess). They are packagelessAddress (object, no limit), findServedOverlayRow once per spelling (limit 1), and overlayLockLayerAt once per spelling (no limit). H1 was confirmed with one correction: the fifth read is the package-less binding, not a second caller. findData made no sys_metadata read. Fix (H2, protocol.ts only at runtime): readMetaItem reads the item's active rows ONCE for both spellings (readItemActiveRows, type IN (canonical, other)). The three lookups select from that read by the same where they used to send (rowMatchesOverlayWhere). The version token reuses the selected package binding. Nothing is cached past the call. Measured (M2, Postgres 16 behind a 25 ms each-way TCP delay proxy, 15 timed reads per leg): median GET /api/v1/data/todo_task went from 551 ms with 5 sys_metadata statements to 340 ms with 1, and the bodies are identical once ids and timestamps are masked. Assignee untouched; claim 6252699680 names this branch.",
    "tests": "Final head f958423, merged origin/main 5da2e88. Exit 0 throughout. Full package suites: metadata-protocol 20655 passed; rest 6051; objectql 8301; runtime 6608; plugin-approvals 1108; plugin-security 3736; service-automation 2533; service-package 125; client 671; cloud-connection 614; cli unit project 4609. Typecheck exit 0 with 0 TS errors for metadata-protocol, rest (incl. check:test-typecheck, 0 debt), objectql and runtime. Before the double sweep: metadata-protocol 5803 failed in 27 files, rest 51 in 8, objectql 60 in 11, runtime 6 in 1, all equality-only where doubles that cannot match type $in. New pins: rest-data-read-one-overlay-read.test.ts (7 passed: exactly 1 read for list and get of a code object; 1 read plus 1 version head read for a stored object; stored enable whitelist under either spelling still 405; an edited row is seen by the next request). protocol.item-active-rows-one-read.test.ts (6 passed: differential, every lookup from the one read equals the store's own answer over 76 object and 584 email_template row arrangements x every address). Ablation, one leg: readItemActiveRows replaced through scripts/ablation-replace.mjs (anchor 1 to 0, marker 0 to 1); metadata-protocol rebuilt; ablation-dist-preflight: marker present in 2 built files. Count rows red, 5 failed, 39 passed (code object 5 reads, stored object 4); the 405 rows failed on the count after their status assertions passed. Restore: git hash-object equals the HEAD blob; rebuild; preflight --absent passed with a clean tree; 44 passed. An earlier attempt was void: its comment-only marker never reached dist, as preflight reported, and the run was redone with a runtime marker.",
    "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, on f958423 derived 79 commands. All 79 were run with the exit code captured before any pipe, and all exited 0. --ran (record lines carry ' :: exit N'): 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero. On the first pass, check:dts-closure and check:dual-build-cjs-loads were red only because this tree was built with OS_SKIP_DTS; after a full pnpm build (76/76) they exit 0. check:engine-double-contract asked for the two new doubles: fixed (assertEngineFindOnePredicate added; --write added 2 ledger rows), then exit 0. CI on PR head f958423 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; every other check-run in_progress, including the five required ones (Lint & Repo Gates jobs, TypeScript Type Check jobs, Governed Surface Queue Guard; Test Core and Build Core not yet listed). Not run locally: test:repo and the repo-wide lint (CI-owned), and the cli integration tier (no spawn entry touched).",
    "mcp_calls": "5: issue_read get (#24371), issue_read get_comments (#24371), pull_request_read get (#24391), pull_request_read get_check_runs (#24391), issue_read get_comments (#24371, report read-back). No MCP write tool.",
    "api_writes": "git push to claude/issue-24371-data-read-overlay-row-once: the empty-branch probe plus 9 incremental WIP and fix pushes, one per commit (os-dev WIP rule), all to this branch only and never forced. Relay: 2 fleet-write strokes. (1) pr_create, run 39160747476: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/24391/assignees [os-project-manager]; read-back identical, 9838 bytes. (2) comment: POST /repos//issues/24371/comments (this report).",
    "line_budget": "none set. Diff vs origin/main: +839/-91 over 48 files. Runtime: packages/metadata-protocol/src/protocol.ts +106/-14. Changeset: .changeset/24371-data-read-one-overlay-read.md +11/-0. New pins: packages/rest/src/rest-data-read-one-overlay-read.test.ts +184/-0; packages/metadata-protocol/src/protocol.item-active-rows-one-read.test.ts +143/-0. Flipped pins: packages/objectql/src/protocol-meta.test.ts +76/-30. Ledger: scripts/engine-double-contract.pinned.json +10/-0. Double sweep, each +7/-1 unless named: metadata-protocol 22 files (save-union-issues +21/-8, packageless-save-address and save-door-lock-family +10/-2, runtime-authoring-gate and save-flow-canonicalization +8/-1); rest 8 files; objectql 10 more (default-record-page-save-door, environment-overlay-email-locale-restart, environment-overlay-five-types and registry-field-type-refused-at-door +5/-0; protocol-publish-rollback +8/-1); runtime meta-save-destructive-remedy +7/-1.",
    "files_changed": [
    "packages/metadata-protocol/src/protocol.ts",
    ".changeset/24371-data-read-one-overlay-read.md",
    "packages/rest/src/rest-data-read-one-overlay-read.test.ts",
    "packages/metadata-protocol/src/protocol.item-active-rows-one-read.test.ts",
    "scripts/engine-double-contract.pinned.json",
    "packages/objectql/src/protocol-meta.test.ts",
    "22 metadata-protocol test doubles (packages/metadata-protocol/src/.test.ts, listed in the PR file list)",
    "8 rest test doubles (packages/rest/src/meta-
    .test.ts, rest-server-meta-.test.ts, rest-exposure-gate-one-object-read.test.ts)",
    "10 further objectql test doubles (packages/objectql/src/
    .test.ts)",
    "packages/runtime/src/domains/meta-save-destructive-remedy.test.ts"
    ],
    "deviations": [
    "Files outside the claim's File surface, all test-only except the ledger: 9 in packages/rest/src (the new count pin plus 8 doubles), 11 in packages/objectql/src, 1 in packages/runtime/src/domains, plus scripts/engine-double-contract.pinned.json. The claim allowed this (the trace's read fix stays in protocol.ts; the sweep is forced by the $in read). The seat owes the lane declaration for rest, objectql and runtime before the queue.",
    "Acceptance is met for objects with no stored row, which covers the card's measured case: 5 to 1. An object with a stored environment row makes 2: the one read plus SysMetadataRepository.headAt behind getMetaItem's version. headAt is the repository's one write-address resolution, and its own docblock forbids re-deriving it, so it is left as is. See open_questions.",
    "M2 used a private Postgres cluster in /tmp/os24371-pg rather than the scratchpad: the harness keeps the scratchpad parent root-only, and postgres refuses to run as root. The cluster was stopped and the directory deleted; the proxy, both dev servers and the worktree were torn down.",
    "pushes: 10 git pushes to the own branch, one per commit, where the prompt's budget counts git push as one write. Never forced, never another branch.",
    "PR footer: the session-URL footer form from AGENTS.md was used; the harness's alternate PR attribution line was not added (AGENTS.md wins)."
    ],
    "open_questions": [
    {
    "question": "Should a data read of an object WITH a stored environment row also reach one sys_metadata read, i.e. should getMetaItem's version head read (SysMetadataRepository.headAt) be served from the rows the read already holds?",
    "options": [
    "A: leave it (2 reads for stored-row objects). headAt stays the one write-address resolution its contract names.",
    "B: thread the pre-read active rows into SysMetadataRepository.resolveWriteHead for the active state, so the head comes from the same read. This touches sys-metadata-repository.ts and its put/lock path, and needs a differential pin against put's own resolution.",
    "C: let the exposure gate ask a narrower read that serves no version. That adds a protocol face, a public-surface move."
    ],
    "recommendation": "A. Business need (measured): the card's measured reads are code-defined objects, now 1. Stored env objects pay one extra round trip, not four. Long-term soundness: B forks the single write-address resolution the repository's docblock forbids forking, and C widens the protocol surface. AI-safety: A keeps one resolution for the token a save accepts. Startup scope: no named consumer pulls for B or C. If a stored-object console measure pulls for it, B is a separate card."
    },
    {
    "question": "Settled by the four axes, recorded: the shape of the single read.",
    "options": [
    "A: one query, type IN (canonical, other), with the test doubles taught $in.",
    "B: two parallel equality reads (Promise.all), no double sweep.",
    "C: a per-request memo, or reading the registry instead."
    ],
    "recommendation": "A (taken). It is the only shape that is one statement and still uses the (type, name, ...) overlay index. B is 2 statements and serializes to 2 round trips on one connection, failing the card's count. C either outlives the call (the ruling forbids that) or changes the served answer (stored overlays are not in the registry). The sweep keeps every double strict: it refuses every other operator it refused before."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) - noted, not filed. Several metadata-protocol and objectql sys_metadata doubles answered find without filtering by name (protocol.save-flow-canonicalization, protocol.runtime-authoring-gate, objectql protocol-publish-rollback). Before this PR only the lock read reached find there; it now filters by name. A looser-than-driver double, test-only; no public door gives a wrong answer, so no card class applies. Dedupe words: double find name filter, where matcher loose, sys_metadata double."
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24391 comment 6254722707 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T20:25Z.

    • Review: the seat's review of record at TIER_DEFAULT, 6254708368, PASS, at head f95842334f.
    • Queue: PR perf(metadata-protocol): one GET /api/v1/data read makes one sys_metadata read of its object, not five #24391 is marked ready and set to auto-merge through the queue. It says Fixes #24371, so this card closes on merge.
    • What lands: one GET /api/v1/data/OBJECT reads its object's active sys_metadata rows once, for both spellings, instead of five serial reads. Measured at 50 ms RTT: a median of 551 ms → 340 ms, with identical bodies. An object with a stored environment row makes 2 reads (the version head), disclosed in the PR's acceptance notes.
    • For objectstack-ai/cloud#2914: the change rides the next next cut (chore: version packages (next) #24168).

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T21:00Z

    Probe Reading
    Review The seat's review of record at TIER_DEFAULT, 6254708368, PASS, at head f95842334f. ACCEPT 6254722707. Not governed.
    Queue It entered the merge queue at 2026-10-18T20:26Z and left it merged at 2026-10-18T20:58Z. No gh-readonly-queue/main/pr-24391-* branch remains.
    origin/main 182757145e is on origin/main. It carries PR #24391's 48 files, +839 / −91.
    Card Closed completed through Fixes #24371; pm:dispatched is cleared.

    What main now holds: one GET /api/v1/data/OBJECT reads its object's active sys_metadata rows once, for both spellings. The package-less binding, the served overlay row and the overlay lock layer are selected from that one read. It was five serial reads before. Measured at 50 ms RTT: a median of 551 ms with 5 statements, then 340 ms with 1, with identical bodies. An object with a stored environment row makes 2 reads (the version head through SysMetadataRepository.headAt), as the PR's acceptance notes say. If a stored-object measure ever pulls for it, threading that read through is its own card.

    For objectstack-ai/cloud#2914: the cut rides the next next cut (#24168). #24360 (this lane's engine copy strings, which waited on protocol.ts) is free to claim. The dev session session_01Fe2h3xpvRmGq9TqyCTV9JC is archived.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsdomain:enginefindingpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions