Document: loomworks-list-loading-step-0-report-v0_1
Version: v0.1
Status: complete — read-only inspection
Date: 2026-06-27
By: Claude Code (CC) on DUNIN7-M4
Answers: loomworks-list-loading-step-0-inspection-brief-v0_1
Paired with: loomworks-list-loading-scoping-note-v0_1 (not present in the record; CC worked from the canonical queue + engine source directly)
Queue item: Companion build queue Item 1 — list-loading (cross-cutting pagination)
> Read-only inspection. No code changed, no commits, no migrations. This report tells the CR-drafting chat the real engine delta before the CR is written.
Canonical source: loomworks-record/current-status/loomworks-companion-build-queue-v0_3.md (dated 2026-06-27). This is the highest version of the build-queue lineage and is the single canonical queue — the sibling loomworks-companion-build-queue-and-list-loading-v0_1.md carries an explicit banner: "SUPERSEDED by loomworks-companion-build-queue-v0_3.md." So v0_3 governs; the "and-list-loading-v0_1" doc is preserved only for discovery-trajectory and must not be worked from.
Exact scope text of Item 1 (queue table row, v0_3 line 35):
> List-loading pattern (cross-cutting PAGINATION) — State: Scope next — "NOT 'loading states/skeletons' — that's item 4. The unbounded-growth pagination concern across EVERY surface. Pattern pre-decided (below). Engine extends its existing has_more/cursor pattern to room + engagement listings."
The settled pattern (v0_3 §"Item 1", lines 70–83), six rules:
total_count always visible — no silent truncation ("showing 50 of 2,000").has_more.(created_at, id) — the Axis 11 R2 lesson (a created_at-only strict-< cursor can skip a turn sharing the boundary timestamp). Engine-side.Surfaces Item 1 names (v0_3 §"Every surface it touches", lines 56–68) — this is more specific than the scoping note's reconstruction:
total_count surfaced" — a second precedent.Did Item 1 name surfaces/endpoints the scoping note missed? Yes — Item 1 explicitly enumerates the Held exemption, the personal-memory backstop precedent, and the three distinct home lenses. It does not name route paths (the queue stays at the surface level) — supplying those is precisely the job of 0c below.
Is "loading states / skeletons" a separate sub-item? Confirmed yes. v0_3 makes this a first-class correction: it is Item 4 ("Loading STATES (home + rooms)"), explicitly not the list-loading concern. The v0.3 change log calls out that an earlier CC draft wrongly narrowed "list-loading" to skeletons/retry; v0.3 reverses that. The scoping note's treatment of skeletons as out of scope is correct.
| Item | Scoping-note assumption | Canonical reality | Delta |
|---|---|---|---|
| Seed | v0.12 | v0.12 confirmed — loomworks-record/candidate-seeds/loomworks/loomworks-candidate-seed-v0_12.md, header Version. 0.12. Only one current seed present (earlier in archive/). | None. |
| Manifest | v0.56 (stale mirror) | Highest present = v0.68 (current-status-manifest-v0_68.md). | 12 versions stale. |
List-loading / pagination decisions in the canonical manifest the v0.56 mirror would have missed: The governing record is Manifest Entry 109 — "Axis 11: CenterPane conversation pagination" (manifest v0.48, present in v0_68). Key facts the scoping note could not have had at full fidelity:
before cursor + has_more already existed on /operator/conversation-history; the client type already carried has_more.has_more (not infinite scroll) is the §6 Q2 precedent rule 3 cites.created_at-only strict-< cursor can skip a boundary-tie turn; "the complete fix is an engine-side composite (created_at, id) cursor… recorded as a follow-on." This follow-on has not been built (see 0d). It is the one engine-side tail of Axis 11 and it is the same composite-cursor work rule 5 calls for.No other pagination decision is recorded in v0.57–v0.68 that bears on this scope.
Every read endpoint in loomworks-engine that returns a list of accumulating items, grouped by pagination posture. All routes verified against router source (src/loomworks/api/routers/ and src/loomworks/orchestration/routers/). All rows are pure reads (see 0e).
total_count + has_more-equivalent)
| Route | Returns | Unbounded? | Paginated today? | Cursor shape | total_count? | Sort |
|---|---|---|---|---|---|---|
| GET /me/dashboard/needs_you | held assertions + pending shapes + review reports (home "Needs you") | Yes | Yes — limit (1–100, def 50) + opaque cursor | Composite {ts,id} (base64 JSON) | Yes + next_cursor | surfaced_at DESC, item_id DESC |
| GET /me/dashboard/recent | produced renders/artifacts (home "Recent") | Yes | Yes — limit + cursor | Composite {ts,id} | Yes + next_cursor | completed_at DESC, artifact_id DESC |
| GET /me/dashboard/active | in-flight jobs | Yes | Yes — limit + cursor | Composite {ts,id} | Yes + next_cursor | started_at DESC, item_id DESC |
| GET /operator/inbox | operator-vocab wrap of needs_you | Yes | Yes — inherits needs_you (limit + cursor) | Composite {ts,id} | Yes + next_cursor | surfaced_at DESC, item_id DESC |
These four already implement rules 1–5 exactly, composite cursor included. No engine work needed.
created_at-only cursor; no total_count)
| Route | Returns | Unbounded? | Paginated today? | Cursor shape | total_count? | Sort |
|---|---|---|---|---|---|---|
| GET /operator/conversation-history | conversation turns | Yes | Yes — limit (1–100, def 50) + before (datetime) | created_at-only strict-< | No (returns has_more only) | created_at DESC, id DESC |
Returns ConversationHistoryResponse(turns, has_more). The ORDER BY is composite for stable ordering, but the cursor boundary uses created_at alone — the Axis 11 R2 gap. See 0d.
limit/offset + total_count, but no cursor and no has_more
| Route | Returns | Unbounded? | Paginated today? | Cursor shape | total_count? | Sort |
|---|---|---|---|---|---|---|
| GET /engagements/{id}/assertions | assertions (Memory — Settled/In record) | Yes | Partial — limit (def 50, ≤200) + offset | Offset (not cursor) | Yes | COALESCE(payload→created_at, last_updated_at) DESC, object_id ASC |
| GET /engagements/{id}/renders | renders (Rendering room) | Yes (Goosey at 13) | Partial — limit (def 50, ≤500) + offset | Offset | Yes | created_at ASC, object_id ASC |
total_count present (client can compute "more exists"), but the mechanism is offset, not the composite cursor rule 5 requires, and there is no has_more flag.
limit/offset only (no total_count, no has_more)
| Route | Returns | Unbounded? | Paginated today? | Cursor shape | total_count? | Sort |
|---|---|---|---|---|---|---|
| GET /engagements/{id}/manifestations | manifestations (Manifestation room) | Yes | Partial — limit (def 50, ≤500) + offset | Offset | No | derived_at DESC NULLS LAST |
| GET /engagements/{id}/shapings | shapings (Shaping room) | Yes | Partial — limit (def 50, ≤500) + offset | Offset | No | created_at DESC |
| Route | Returns | Unbounded? | Paginated today? | Cursor shape | total_count? | Sort |
|---|---|---|---|---|---|---|
| GET /engagements | engagement summaries (home "Your engagements") | Yes (grows with memberships) | No — no limit/offset/cursor | — | No | e.id |
| GET /engagements/{id}/assertion-groups | assertion groups (Memory grouping) | Yes | No — wholesale | — | No | snapshot dict order (unspecified) |
| GET /engagements/{id}/considerations | considerations (Memory-adjacent) | Yes | No — filters only (triggering_reason, current_stage, terminal_state, since) | — | No | last_updated_at DESC |
| GET /engagements/{id}/shape-events | shape events (Shaping production runs) | Yes | No — filters only (declared_shape_type_id, state, version range, self_consumer) | — | No | engagement_version_at_production ASC, object_id ASC |
| GET /engagements/{id}/compositions | render compositions (Rendering pipeline) | Yes | No — state filter only | — | No | created_at DESC |
| Route | Returns | Why wholesale OK |
|---|---|---|
| GET /engagements/{id}/assertions/{aid}/history | one assertion's versions | bounded by revision count |
| GET /engagements/{id}/assertions/{aid}/relationships | one assertion's relationships | structurally small |
| GET /engagements/{id}/contributions/supported-types | registered extraction types | registry-bounded |
| GET /engagements/{id}/considerations/dismissal-patterns | aggregated report | one row per trigger |
| GET /engagements/{id}/declared-shape-types | shape-type config | config-bounded, small |
| GET /engagements/{id}/declared-render-types | render-type config | config-bounded, small |
| GET /operator/dashboard | preview aggregator | hardcoded _PREVIEW_LIMIT = 10 + surfaces *_count; intentionally a bounded preview |
Home-consumption note (frontend, flagged for the CR chat): The home's "Needs you" and "Recent" lenses are backed by distinct endpoints — /me/dashboard/needs_you and /me/dashboard/recent (Group A, fully conformant) — not by filtered views over /engagements. But /operator/dashboard (Group F) is a preview aggregator that calls those sections with limit=10, cursor=None and returns only counts. If the built home currently consumes /operator/dashboard, then making those two lenses page is a frontend re-wire (switch to the paginated section endpoints + add the load-more control) — not engine work. Which endpoint the home consumes today is a frontend (loomworks OL repo) question outside this engine inspection; the CR chat should confirm it before sizing the home lenses.
GET /operator/conversation-history cursor is still created_at-only. It has NOT been upgraded to composite (created_at, id).
Verified in src/loomworks/orchestration/routers/conversation_history.py:
before: Optional[datetime] = Query(default=None).WHERE created_at < before (strict <, timestamp only).created_at DESC, id DESC (composite for ordering stability, but id is not part of the cursor boundary).ConversationHistoryResponse(turns, has_more) — has_more present, no total_count.Consequence for the CR: the composite-cursor work in rule 5 includes fixing the reference endpoint itself, not only the new ones. This is the Axis 11 R2 follow-on (manifest Entry 109), still open. It is the single endpoint where the manifest already pre-acknowledges the engine debt.
Every unbounded listing endpoint that currently returns wholesale is a pure read. Confirmed per handler:
| Endpoint | Writes? | Derives? | Touches provenance/FORAY? | Classification |
|---|---|---|---|---|
| GET /engagements | No | No | No | pure read (SELECT engagements ⨝ memberships) |
| GET /…/assertion-groups | No | No | No | pure read (snapshot over memory_events) |
| GET /…/considerations | No | No | No | pure read (SELECT current_memory_objects) |
| GET /…/shape-events | No | No | No | pure read (SELECT shape_events_view) |
| GET /…/compositions | No | No | No | pure read (SELECT render_compositions_view) |
The partially-paginated reads (assertions, renders, manifestations, shapings) and the conformant dashboard reads are likewise pure SELECT-and-resolve handlers — no writes, no derivation, no provenance thread contact on the GET path. No listing endpoint does more than read.
Flag: none. Every endpoint in scope carries the same Memory-read-boundary classification Axis 11 carried — a pure-read, no-Memory-contact, consumer/reader-surface capability. The eventual CR inherits Axis 11's classification cleanly.
The scoping concern was: how many listing endpoints already paginate vs. need engine pagination added. The answer is not binary — the engine sits at three tiers:
needs_you, recent, active, inbox — already implement the composite-cursor + total_count pattern end-to-end. Whether the home uses them with a load-more control is a frontend question, not an engine delta.GET /engagements (home "Your engagements"), assertion-groups, considerations, shape-events, compositions — all wholesale, all unbounded, all pure reads. These are the core "extend the pattern" work: add limit + composite (created_at, id) cursor + has_more + total_count.assertions and renders (offset + total_count, need composite cursor + has_more); manifestations and shapings (offset only, also need total_count + has_more); and conversation-history itself — the Axis 11 R2 follow-on — needs the created_at-only cursor upgraded to composite (and arguably total_count added to match rule 2).
So the real engine delta is ~10 endpoints touched (5 new-pagination + 5 conform-upgrade), governed by one decision: standardize on composite (created_at, id) cursor + has_more + total_count, including retrofitting the reference endpoint the prior phase deliberately left as a follow-on. This is materially larger than Axis 11's frontend-only footprint — it is a genuine engine CR, not a frontend skin — but every endpoint is a pure read, so it carries Axis 11's Memory-read-boundary classification without a four-room production change. The held/small sets (Memory Held, the Group F bounded reads) stay wholesale per rule 6 and are explicitly out of the delta.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks list-loading — Step-0 inspection report — v0.1 — 2026-06-27 Read-only inspection. Halt after report — CR not drafted (a fresh Claude.ai chat drafts it with this report in hand).