Document: loomworks-list-loading-cr-2-step0-report-v0_1
Status: complete — READ-ONLY inspection
Date: 2026-06-27
By: Claude Code (CC) on DUNIN7-M4
For: the CR-2 (frontend half of list-loading) drafting chat
Repos read: loomworks-engine @ eeae9c2 (merged main, alembic 0096 — CR-1 landed) · loomworks (Operator Layer) branch feat-companion-surface @ 17263c9
> Read-only. No edits, no commits, no migrations. Confirms the live facts CR-2's draft depends on, against the merged CR-1 contract (not the CR's restatement) and the built OL frontend.
limit + cursor request params and returns has_more + total_count + next_cursor. The only per-endpoint variation is the item-array key name (table below). before survives nowhere. One minor wart: assertions still returns a vestigial limit field./me/dashboard/* (the paginated Group-A sections), not /operator/dashboard. So the engine already pages the home lenses; the frontend currently throws the cursor away. CR-2 for the home lenses is a frontend re-wire**, not from-scratch engine work.lists.page_size slots in as a bounded-numeric SettingSpec. PUT /me/settings/{setting_key} is live.eeae9c2)
Request params — identical across all ten: limit: int + cursor: str | None. Pass a response's next_cursor back as cursor. (limit ceilings differ: ≤100 for engagements/considerations/compositions/shape-events/assertion-groups/conversation-history; ≤200 assertions; ≤500 manifestations/shapings/renders — default 50 everywhere.)
Response fields — identical spelling across all ten: has_more: bool, total_count: int, next_cursor: str | null.
The one per-endpoint difference — the item-array key:
| Endpoint | Request | Response item key | has_more / total_count / next_cursor |
|---|---|---|---|
| GET /engagements | limit, cursor | engagements | ✓ ✓ ✓ |
| GET /engagements/{id}/considerations | limit, cursor | considerations | ✓ ✓ ✓ |
| GET /engagements/{id}/compositions | limit, cursor | items | ✓ ✓ ✓ |
| GET /engagements/{id}/shape-events | limit, cursor | shape_events | ✓ ✓ ✓ |
| GET /engagements/{id}/assertion-groups | limit, cursor | assertion_groups | ✓ ✓ ✓ |
| GET /engagements/{id}/manifestations | limit, cursor | manifestations | ✓ ✓ ✓ |
| GET /engagements/{id}/shapings | limit, cursor | shapings | ✓ ✓ ✓ |
| GET /engagements/{id}/assertions | limit, cursor | assertions | ✓ ✓ ✓ + vestigial limit |
| GET /engagements/{id}/renders | limit, cursor | renders | ✓ ✓ ✓ |
| GET /operator/conversation-history | project_id, limit, cursor | turns | ✓ ✓ ✓ |
Flags for CR-2:
conversation-history: before is GONE — the live param is cursor (conversation_history.py:71, Optional[str]). The frontend adapter still sends before (see D-4) — that is the breaking update site.compositions item key is items (not compositions) — the lone naming outlier among the rooms.assertions still returns a limit field in its response (a vestige of the old offset pagination; offset itself is gone). Harmless, but the frontend should ignore it and rely on has_more/next_cursor.KeysetPage mixin (api/schemas.py); assertions + renders declare the three fields inline; conversation-history declares them in orchestration/schemas.py. Field spelling is identical regardless — a single frontend pagination type fits all ten.
**The home consumes /me/dashboard/* (the paginated section endpoints), NOT /operator/dashboard.**
src/app/operator/engagement-navigation/useHomeData.ts — composes the home from three fetches (useHomeData.ts:129–130 + the engagements fetch).src/lib/api/dashboard.tsfetchInboxItems() → api(...)("/me/dashboard/needs_you") (dashboard.ts:72) — the "Needs you" lens.fetchLibraryArtifacts() → api(...)("/me/dashboard/recent") (dashboard.ts:119) — the "Recent" lens.src/lib/api/projects.ts:51 → api(...)("/engagements")./operator/dashboard anywhere in the home path.
Consequence (the decisive answer): the home's two dashboard lenses hit /me/dashboard/needs_you + /me/dashboard/recent — the Group-A endpoints that were already keyset-conformant before CR-1 (they ship total_count + next_cursor). So the engine already pages the home lenses. But the frontend adapter discards the cursor — e.g. fetchInboxItems returns wire.items.map(...) and drops next_cursor/total_count (dashboard.ts:72–74). The SubstrateNeedsYouResponse type even declares next_cursor: string | null (dashboard.ts:53–57) — it's parsed and thrown away.
→ CR-2 for the home lenses is a frontend re-wire (consume the existing next_cursor, render load-more), not from-scratch engine work. The "Your engagements" list (GET /engagements) is a CR-1 endpoint and also pages now — same re-wire shape.
(1) KNOWN_SETTINGS — src/loomworks/preferences/person_settings.py. A dict[str, SettingSpec] with three entries today:
| key | default | bounds | kind |
|---|---|---|---|
| voice.listening.blur_intensity | 0.6 | (0.0, 1.0) | numeric |
| voice.listening.silence_submit_seconds | 2.5 | (0.5, 10.0) | numeric |
| conversation.message_order | "newest_first" | — (validator) | enum |
SettingSpec shape: (key, display_name, default, validator, bounds?). A numeric setting carries bounds; an enum carries a validator and no bounds. → lists.page_size slots in as a bounded-numeric SettingSpec (e.g. bounds=(10, 200), default=50, an int validator).
(2) _LABEL_TO_KEY — src/loomworks/orchestration/tune_setting.py. A flat dict[str, str] mapping voice synonyms → setting key (~17 synonyms over the 3 keys: "blur"/"voice blur"→blur; "order"/"message order"→message_order; "wait"/"silence"/"pause"→silence). → add a synonym cluster for the new key (e.g. "page size", "list size", "how many" → lists.page_size).
(3) Frontend apply branch — src/app/operator/engagement/[engagement_address]/useConversation.ts:71–81, the applySettingChange if-ladder (currently a single branch on conversation.message_order), driven by res.setting_changes after each converse turn (useConversation.ts:216). Initial-read + UI-toggle pattern lives in InEngagementSurface.tsx:72–96 (fetchMeSetting/putMeSetting). → a UI-adjustable lists.page_size adds one branch here; a voice-only setting needs no frontend branch (it just re-fetches with the new size).
The three-place extension still describes how a setting is added — confirmed live, unchanged from the Phase-60 pattern.
tune_setting mechanics — confirmed, no drift (orchestration/tune_setting.py):
SettingSpec.bounds; value_kind is "numeric" (carries bounds) or "enum" (tune_setting.py:204–241).setting_change audit: yes — _audit_setting_change → write_setting_change_event + _foray_reserved_emit, best-effort, joined to the converse transaction (tune_setting.py:52–53, 68–97, 523).execute_tune_setting (:388) and direct PUT /me/settings/{setting_key}.
PUT route: src/loomworks/api/routers/me_settings.py:96 — @router.put("/me/settings/{setting_key}") (GET at :70), with its own audit write (:159). The direct-set path exists and is live.
The reusable load-more control attaches to live list surfaces. Of the ten CR-1 endpoints, four have an OL consumer today; the home adds two more (Group-A dashboard lenses).
| # | Surface | Component | Endpoint | Paginates today? |
|---|---|---|---|---|
| 1 | Home — "Your engagements" | engagement-navigation/useHomeData.ts → lib/api/projects.ts:51 | GET /engagements | No — wholesale |
| 2 | Home — "Needs you" lens | useHomeData.ts → lib/api/dashboard.ts:72 | GET /me/dashboard/needs_you | No — cursor parsed then discarded (engine pages) |
| 3 | Home — "Recent" lens | useHomeData.ts → lib/api/dashboard.ts:119 | GET /me/dashboard/recent | No — cursor discarded (engine pages) |
| 4 | Memory room (Held + Settled) | engagement/[…]/MemoryRoom.tsx → lib/api/memory.ts:122 | GET …/assertions?state=held|committed&limit=200 | No — limit=200 ceiling, no cursor (code comment: "pagination — a later increment") |
| 5 | Rendering room | engagement/[…]/RenderingRoom.tsx → lib/api/renders.ts:105 | GET …/renders?state=produced&limit=200 | No — limit=200 ceiling, no cursor |
| 6 | Conversation transcript (CenterPane) | engagement/[…]/ConversationPane.tsx + useConversation.ts → lib/api/converse.ts:44 | GET /operator/conversation-history | YES — the Axis 11 "load earlier" precedent; but still sends before= (converse.ts:53) → must migrate to cursor |
The six unconsumed endpoints — considerations, compositions, shape-events, assertion-groups, manifestations, shapings — have no OL surface today. The Manifestation ("compose") and Shaping ("shape") rooms render empty-state stubs (engagement/[…]/RoomView.tsx:37–47, EmptyRoom), not wired to any engine data. → CR-2 can attach load-more only to surfaces that render a list; these six wait for their rooms to be built (a separate piece). Per-surface page-size defaults attach to surfaces #1–#6, not all ten.
Existing precedent (the reusable control generalises this): CenterPane's "load earlier conversation" button (ConversationPane.tsx:179–188, gated on hasMore) is the working load-more pattern. It's also the one breaking update site: useConversation.ts tracks the oldest created_at and passes it as before (converse.ts:53) — CR-2 must switch it to consume next_cursor and pass cursor, since the engine dropped before.
limit/cursor → has_more/total_count/next_cursor; only the item-array key varies (and compositions→items, conversation-history→turns).next_cursor on /me/dashboard/needs_you + /recent + /engagements; no engine work.conversation-history is the breaking site — migrate before→cursor in converse.ts/useConversation.ts; the load-earlier control already exists and is the pattern to generalise.limit=200 wholesale fetch for cursor pagination + the load-more control.lists.page_size adds cleanly via the intact three-place settings pattern (bounded-numeric SettingSpec + _LABEL_TO_KEY synonyms + optional frontend apply-branch), with the dual voice/PUT path live.
HALT. No edits. Report dropped in Downloads; canonical copy staged to loomworks-record/inspection-briefs/ — staged, not committed, awaiting Operator authorisation.
DUNIN7 — Done In Seven LLC — Miami, Florida
Loomworks list-loading — CR-2 Step-0 inspection report — v0.1 — 2026-06-27
Engine eeae9c2 (alembic 0096) · OL feat-companion-surface 17263c9. Read-only.