Record all agent work here when working for Saketh. Include status, file paths, verification, and blockers or handoff notes.
Lane: catalogue, search and the language layer. Branch: codex/saketh-catalogue. Feature doc: docs/backend/catalogue.md.
- Prepare and verify the shared Haussmann room locally — required for each collaborator.
- Use HAUSSMANN APARTMENT as the shared Gaussian room test download. SuperSplat login may be required. Credit Stéphane Agullo (sa3d), CC BY 4.0; retain the downloaded license. This is an authored test scene with assumed scale, not a measured room.
After downloading, keep the ZIP intact and run these commands from the repository root after saving current work. The && chain stops at the first failure:
git switch main &&
git pull --ff-only &&
./setup.sh &&
python3 scripts/gaussian_room/prepare_haussmann.py "$HOME/Downloads/HAUSSMANN APARTMENT.zip" &&
python3 scripts/gaussian_room/review_surface.py &&
BACKEND_PORT=8222 FRONTEND_PORT=5222 ./run-local.shOpen http://localhost:5222/, or http://localhost:<FRONTEND_PORT>/ if using another frontend port. Haussmann is already the default; no ?room parameter is needed.
Adjust the ZIP path if needed. Run preparation in a normal terminal for GPU access; the previously reported run took about 2½ minutes. Preparation is a one-time step and refuses to overwrite existing output. If already prepared, skip preparation and run the surface review and stack commands. Generated room assets are ignored by Git, so each collaborator must prepare them locally. Keep the attribution/license. This remains an experimental test room with assumed scale and known downward/ceiling/window artifacts. Stop the stack with Ctrl-C when finished.
Tracking update (2026-09-20): standardized this MUST DO checklist across TODO_SIN.md, TODO_SAKETH.md, TODO_WILLIAM.md, and TODO_ADELLE.md; synchronized owner-specific Kanban tasks. Checked the default / route in frontend/src/App.tsx. Local preparation and room verification remain pending for each collaborator; this documentation update does not claim they ran.
- Add speech and typed user input (Kanban
3dc03f7b, owner Saketh, not done). Typed input and the server path are verified. Waiting on one thing: Saketh presses the microphone on the demo laptop. Real speech through a real microphone in a browser is unverified; headless Chrome's fake microphone delivered silence. The card stays open until then. - Next after the Deepgram PR merges: read through Sin's updated engine (contract surface,
validatePlacementproperty test, the four item C bindings), reconcile his how-to against the source, then the room bundle.
-
Add Blender MCP-style scene and object context to the agent — requested by William, 2026-09-19; Kanban
6ae44373-c5ee-44b4-867c-99e96937aef8, open. Handoff oncodex/visa-sandbox: reference, response contracts and implementation steps. This is a requested follow-up, not a claim Saketh has started or completed it.- Match Blender MCP's scene-summary → detailed-object → action → image/result loop. Each placed object needs a stable instance ID plus product ID, precise location/orientation, dimensions/world bounds, units/axes/pivot, known asset/material metadata, hierarchy/visibility where supported, provenance and current scene revision.
- Inspect the pinned official Blender Lab and community sources linked in the handoff to see exactly what JSON/text and PNG content reach the model. Account for all component families in the capability map; explicitly mark unsupported Blender/provider operations.
- Reuse current scene/placement/capture services; coordinate shared read schemas and UI metadata with Sin. Register agent tools with trusted session context, preserve revision/idempotency and placement checks, and pass real screenshot pixels through the agent adapter.
- Build summary/detail tools first, then wire edits and capture feedback. Verify duplicate-product instances, more than ten objects, rotation/units, rejected and stale edits, missing metadata and actual model-visible image content. Record successful/rejected traces before marking done.
- Preserve existing catalogue/compiler code and the Visa HTML tester. Shared cart checkout and guest-room account ownership remain separate handoff tasks; the database-cache test compatibility issue is resolved in PR #21.
-
Normalize DummyJSON and WANDS reference products for Elasticsearch — Saketh. Pin source revisions; read DummyJSON
products.json(MIT) and WANDSproduct.csv(MIT; tab-delimited despite.csv). Give each record a namespaced ID and bulk-index it into a new, explicitly mappedreference_productsindex with source, title, original/normalized category, description, tags/features, optional integer-cent price and stock, and provenance for each commercial value. Keep unknown fields absent. Do not pass source dimensions to the scene until units and accuracy are verified; neither source has live reservable stock. Preserve the existing strictListingcontract andlistingsindex for scene-ready demo items. -
Import CC0 GLB asset metadata into a separate Elasticsearch index — Saketh. Pin and import the three.ws object manifest (field/API documentation, Poly Haven CC0) into an explicitly mapped
model_assetsindex: stable source ID, label, categories/tags, format, license/source URL, original and browser-tested GLB URL, thumbnail reference, file size, and geometry/placement validation status. Index metadata, not GLB bytes. Kenney Furniture Kit and Quaternius House Interior Pack may use the same adapter after checking each file and license. Keep a reachable, stable served copy for selected demo models; verify browser loading, scale, orientation, pivot, and bounds. -
Curate product-to-GLB links for placeable demo listings — Saketh owns the catalogue links; coordinate physical model preparation with Adelle and scene integration with Sin. Record
reference_product_id,asset_id,match_type(generic_visual_proxyor verified exact match), and review status; never infer SKU identity from similar names. Only a small reviewed subset with positivedims_mm, explicit price provenance, and workingmodel_urlbecomesListingrecords. Publish those records to bothbackend/catalogue/data/listings.json(or its shared feed successor) and the Elasticsearchlistingsindex from the same normalized source:backend/api/catalogue_products.pyvalidates scene products againstload_catalogue(), so Elasticsearch-only rows cannot be placed. Expose validated reference/asset lookup to the agent separately from the currentGET /api/search; scene placement and checkout stay gated by verified data and explicit sandbox approval. Keep product photos out of the public UI until image rights are verified.
For each import, verify expected/actual counts, rejected rows, stable IDs on a second run, search results, and one selected GLB through the real browser scene before marking these tasks done. No source data has been imported yet.
-
2026-09-20: Update Kanban wording for OpenAI API and Codex — updated
.github/extensions/project-manager/{data/board.json,server.mjs,bin.mjs,extension.mjs,README.md}on main. The existing agent card now says Connect room, catalogue, and user input to the OpenAI API agent; Agent Harness and demo descriptions mention OpenAI API/Agents SDK and Codex. Removed old vendor wording from launcher/docs; retained the legacy adapter package import for compatibility. IDs, ownership, order and completion preserved. Verification:node --test .github/extensions/project-manager/tests/board.test.mjs→tests 6 / pass 6 / fail 0; launcher smoke →HTTP 200, both requested names visible, no old vendor wording in rendered board. Syntax and whitespace checks passed. Stopped the launcher;lsofconfirmed no listener on port 50717. No commit or push. -
Openings: the Cg Arch room's window is measured and "by the window" works (2026-09-20). Branch
codex/saketh-openings. Verified against the model rather than a screenshot: nodeRoom Glass Windowsx 8.065 to 10.865 m, y 0.075 to 2.125 m, z 1.49 to 1.51 m, no node transform, identity manifest transform, so north wall,startCm19.5,widthCm280, sill 7.5. Newshared/rooms/cg-arch-interior/openings.json,frontend/src/region/roomOpenings.ts,scripts/room_openings.py,backend/api/test_room_openings.py;Opening.sillCm?;useRegiontakes openings; the PlayCanvas catalogue layer passes them. Not inmanifest.json/spatial.json: the room importer compares those byte for byte and would refuse the room for everyone. Hero sentence whole: HERRÅKRA 60 / 75 / 60 / 75 (hand-derived, solver agrees); 3 ft 240 / 255; 5 ft none, "needs 371 cm of width". Seen in the editor: chips correct, small lit patch in front of the glazing, no set-aside clause. -
Lane change (2026-09-20, from Saketh): a second agent now owns the asset work:
shared/models/**,backend/catalogue/data/listings.json,scripts/fit_glb.py,backend/api/test_furniture_assets.py, and the re-ingest after any listings change. Dropped from this lane and never started: the ABO spike and the Quaternius fallback. Already finished before the change and left as they are for the new owner: PR #45 (bed and table refit) and one re-ingest frommain3a96dd6. I do not push to #45 again. -
Refit under a 10% stretch cap (PR #45).
fit_to_boxinapi/glb.py; bed at the cap lands 0.67 cm off on two axes (recorded), table exact at 6.83%, EKTORP and HEKTAR lamp stay unbound with reasons. The cap is asserted from each GLB's own root scale. -
Index agreement check (PR #44).
python -m catalogue.index_check: count plus SHA-256 over every document. Found the live index 2 listings short and 2model_urls stale; re-ingested; both sides12043 listings, sha256 6d510c1fd1412fa3. -
mainwent red a second time (#42), fixed in #43. And the 206 to 170 question: nothing was lost, I had drifted from the all-apps command totest api catalogue, which skipsaccounts(15),checkout(9) andvisa(12). Both commands give the same numbers on4b3a258and on currentmain. Rule now inAGENTS.md: no app labels, and quote the line you read. -
Placements survive a storage hiccup in the PlayCanvas editor (2026-09-20). Branch
codex/saketh-quiet-retry. Newfrontend/src/catalogue/quietSave.ts(+ test, 4 cases) andfrontend/src/scene/playcanvas/pendingGhost.ts;SplatCatalogueLayer.tsxshows the item at once and drives Sin'ssession.retry()on the 0.5 s to 8 s backoff;useRoomSession.tsuntouched;SplatEditor.tsxmount line gained two props. Found before running: the overlay effect was keyed onready, which drops while offline and would have disposed the ghost exactly then; now latched. Verified in headless Chrome with the first three saves forced to 503: box stayed on the floor, saved at revision 1 about 4 s later with no user action. Lag documented indocs/backend/contracts/scene-api.md; follow-up for Sin inTODO_SIN.md. Frontend 117 + 5, build OK. -
Main went red after #34 and was fixed in #37 (2026-09-20). Seven backend failures: tests pinned to 41 listings (now 43), two off-palette colours, two generated models 14 and 18 cm off their listings. Listings kept, models unlinked with an
unboundreason in their metadata, asset test accepts an unbound asset only if no listing links to it. -
Port merged (#36), walls merged (#33), hero sentence is four feet (#38). Cg Arch room imported locally via
scripts/import-room.sh(writes only the gitignoredassets/room.glb). In that room the overlay drew white: bakedscene.exposuremultiplied the emissive; fixed inregionOverlay.ts. HERRÅKRA placed there at (990, 440), revision 2. HERRÅKRA at 4 ft: 220 / 265 / 220 / 265. -
Walls come from the free floor, not the shell (2026-09-20). Branch
codex/saketh-walls-from-free-area, off freshmainafter merging PR #31 (mainwith Sin's PlayCanvas engine: 206 backend, 110 + 5 frontend, build OK).- Engine read-through first, no code changed: contract surface additive except
validatePlacement, whose new rules (free-area coverage, fixed obstacles, calibration) apply only to rooms withscan. Fresh-seed two-directional check, 2,511,192 grid points over four rooms including two non-rectangular: 0 lit-but-refused, 0 dark-but-accepted. Item C's four bindings pass on?legacy. The default route is now Sin'sSplatEditor, which has none of item C; Saketh decided to port it (issue #32 asks Sin which files he is in). Doc-versus-code differences reported to Saketh. Issue #30 to William retracted and closed. - Saketh's local-coordinate offset was dropped at his instruction: Sin's oracle already knows the free areas, so the room is passed unchanged. The real gap was walls:
wallBounds()infrontend/src/region/boundary.tsnow takes them from the free areas' bounding box;openingZoneand the nothing-fits arithmetic follow it. - Numbers, Cg Arch living room (334 × 580 cm): "5 feet from any wall" → POÄNG 0 positions,
needs 373 cm of width, this room has 334 cm(HERRÅKRA 371). 4 feet → 255. 3 feet → 1,071. The hero sentence was NOT changed; that is Saketh's call. - Verification: frontend tests and build.
- Engine read-through first, no code changed: contract surface additive except
-
Deepgram voice input, built (2026-09-20); card still open, see In progress. Branch
codex/saketh-deepgram, off freshmainafter merging PR #28 and PR #29 (suite run on freshmainafter each: 179 backend, 73 frontend, build OK). Both PRs first got review fixes: room links open only when the layout is really parked and the parked copy follows later edits (#28); catalogue doc counts and the asset regression pinned to its fixture (#29).- The project calls a Deepgram API: pre-recorded
v1/listen,nova-3, from the server.backend/catalogue/transcribe.py(transcribe(),TRANSCRIBE_BACKEND=deepgram|browser, stdlib HTTP so no new dependency),GET/POST /api/transcribeinviews.py/urls.py, a raw-audio parser,TranscribeThrottleat 20/min.CompileThrottleand it now shareFailOpenThrottle. Frontendfrontend/src/catalogue/transcribe.tsand a press-to-talk button in the shelf; Web Speech is the fallback. Key set in the ignored.env; names only in.env.example. - Verification: backend 187 tests offline with both API keys blanked (8 new, recorded Deepgram response); live test behind
TRANSCRIBE_LIVE_TEST=1passed. Through the app's proxy the fixture recording came back as "A reading chair under $400." in 540 ms and compiled toarmchair,under $400. 415 and 413 refusals confirmed. The key appears in no log or response. In headless Chrome the button recorded withMediaRecorder, uploaded and got a 200, but Chrome's fake microphone delivered silence, so real speech through a real microphone in the browser is not yet verified by me and needs one human try. - Saketh checked the challenge wording: it requires calling a Deepgram API; pre-recorded qualifies.
- Review follow-ups in the same PR: a Deepgram 503 now falls back for that press only, using a shadow browser recogniser that ran alongside the recording, and never latches;
?dev=1shows which backend actually answered;search,compile_programandtranscribe_audioare explicitly anonymous (a signed-in shopper's POSTs were being refused for a missing CSRF token), which also closed a hole where signing in bypassed the throttle on the paid endpoints, now per client address for everyone; noted inTODO_WILLIAM.md. Kanban: I had missed that the board rule reachedAGENTS.md; from here I update my own cards only, minimally. - Also at this boundary: item C re-verified on merged
main(overlay anddiscard, raycast, GLB load, footprint). No new renderer or GLB dataset from William is onmain, any remote branch or any PR; issue #30 asks him to push it. Saketh's later instruction replaces the window question there: openings are to be extracted from the room model when it lands.
- The project calls a Deepgram API: pre-recorded
-
First real 3D asset: HERRÅKRA armchair fixed, listed and wired (2026-09-20). Branch
codex/saketh-herrakra-chair, off freshmain.- Adele had not pushed a fix or replied, so per Saketh the transform was baked here. Her commit was cherry-picked with her authorship; the
TODO_ADELLE.mdconflict was resolved keeping both sides. backend/api/glb.py(stdlib GLB reader, world-space bounds,fit_to_height),scripts/fit_glb.py. Applied to hermodel.glb: uniform scale 0.3845, lift 0.3654 m, as a root node. Lossless: the binary chunk is byte-identical. Box 182.4 × 189.9 × 175.3 cm → 70.1 × 73.0 × 67.4 cm, feet at y = 0.- Catalogue:
ikea-405.355.47HERRÅKRA armchair, 710 × 660 × 730 mm, $149,model_urlset, colour from the existing palette. Data, not a schema change. Hero count 40 → 41; eight count assertions updated. Elasticsearch re-ingested: 12,041, mapping unchanged. - The cross-lane bbox assertion is real now:
backend/api/test_furniture_assets.pychecks every asset folder against its listing (2 cm or 3%, whichever is larger), floor contact, centring, size and triangle budget, and proves it would catch a unit-cube export. Units still cross in exactly two places. - Browser: "an armchair" lists HERRÅKRA; hover lights the floor; placed at (485, 140); Objects panel "3D model loaded"; top view shows the 71 × 66 cm footprint outline wrapping the model, seat facing +Z.
docs/demo/run-of-show.mdadded and indexed (the demo hovers HERRÅKRA now, and ends in the Studio). Asset rules and triangle target added to the collaborator handoff; note to Adele.- Verification: backend 179 tests, frontend 65, build OK. Processes stopped; 0 listeners on 8211/5211/9334.
- Adele had not pushed a fix or replied, so per Saketh the transform was baked here. Her commit was cherry-picked with her authorship; the
-
A failing save can no longer trap the user in a room (2026-09-20). Branch
codex/saketh-room-switch-never-stuck, off freshmainafter merging PR #27 (suite re-run on thatmain: 175 / 65 / build OK). Sin was messaged directly (commit comment one93075a) about the assertion of his that PR #26 changed.- The trap Saketh spotted: PR #25 made room links wait for a saved scene; PR #26 made a failing save retry forever. With a wedged backend the Studio room would have been unreachable on stage.
- Fix, keeping both halves:
canLeaveRoom()reopens the links after two failed saves or when offline/in conflict; from the first failed save the layout is parked per room inlocalStorage(frontend/src/scene/pendingEdits.ts), restored on return if the server still has that revision, then saved normally; cleared on success. - Found by the dead-backend browser test: a failed background poll still set the old hard
offlineblock, which stopped saves, so an item placed while the server was down was neither retried nor parked, and "The server is busy." leaked onto the status line. After the initial load nothing transient blocks anything now. - Browser verification (Django and Vite started separately on 8211/5211, Django killed mid-session): status went to "Reconnecting. Your room is safe in this browser."; a chair placed with the backend dead showed "Saving your room…", the Studio link was inert after one failure and live after two, the layout was parked (
default=2); switching to Studio worked; with the backend restarted and the living room reopened, the parked layout was restored and saved (server revision 2, both chairs). - Answer to Saketh's question on PR #26's numbers: 3,297 and 1,733 were attempts. Successful saves went from 1,000 to 1,733 in the same 12 s, so
IMMEDIATEbought throughput as well as correctness. - Verification: frontend 70 tests (6 new), build OK, backend unchanged at 175. Processes stopped; 0 listeners on 8211/5211/9334.
-
AGENTS.md: trust the remote, not your terminal (2026-09-20). Branchcodex/saketh-merge-discipline, off freshmainafter merging PR #26 (mainre-verified: 175 backend tests, 65 frontend tests, build OK; merged branch deleted locally and remotely; remote head compared by hash). One block holding the push-verification rules and the after-merge sequence, with the reason each exists. Posted toTODO_SIN.md,TODO_WILLIAM.mdandTODO_ADELLE.md. Documentation only. -
Storage may delay a save, never refuse a placement (2026-09-20). Branch
codex/saketh-storage-never-blocks, off freshmainafter merging PR #25 (main: 174 backend tests with William's accounts/Visa/checkout work in, 61 frontend tests, build OK). PR #25 first got a review fix: room switching is inert until the scene is saved.- Read the other logs. William: PR #19 and #21 merged; his open items are account/checkout routes in the UI, guest-room and account scene ownership (will meet the
<session>:<room>layout key), and the catalogue/cart contract; he added four tasks to this file's Next (Blender-MCP agent context; DummyJSON/WANDS import; CC0 GLB metadata index; product-to-GLB links), all deferred by Saketh. Adele: first asset oncodex/adelle-herrakra-chair-asset. No file collisions with this work. - Adele's chair inspected and she has the verdict (comment on commit
eb7b3ec, note inTODO_ADELLE.md): axes, size and triangle count pass; scale and pivot fail (1.82 × 1.90 × 1.75 m centred on the origin). Exact fix given: scale 0.3845, lift 0.3654 m; offered to apply it losslessly on the root node. HERRÅKRA is not in the catalogue yet. Measuring script is in.scratch/; it becomes a real assertion when the chair is wired. - SQLite:
backend/config/settings.pynow setstransaction_mode: IMMEDIATE,journal_mode=WAL,timeout: 20. Root cause of the 503s was deferred transactions failing instantly on lock upgrade, which ignores the busy timeout.scripts/hammer_storage.py(scene saves + compile throttle writes + sign-ins, concurrently): before 2,297 of 3,297 saves were 503; after 0 of 1,733, no 5xx anywhere. - Browser:
frontend/src/scene/useSceneSync.tsretries a transient save failure quietly with backoff (0.5 s → 8 s), never shows a status code, never takes the item back; refusals and 409 unchanged. One assertion in Sin'ssync.test.tsupdated, three tests added. - Claims now have tests: the compile throttle failing open is asserted to answer 200 and log exactly once, both with a faked throttle and with the real cache backend raising; the overlay is asserted to write uniforms through the live material and to set every texture parameter explicitly. Shader declares
precision mediump float; the pulse is computed in JavaScript so nothing in the shader grows with time. - Verification: backend 175 tests, frontend 65 tests, build OK; overlay re-checked in headless Chrome after the shader change (0 → 212,788 lit pixels on hover). Processes stopped; 0 listeners on 8211/5211/9334; scratch under
.scratch/.
- Read the other logs. William: PR #19 and #21 merged; his open items are account/checkout routes in the UI, guest-room and account scene ownership (will meet the
-
Item C — catalogue in the room: hover lights the floor, click places (2026-09-19). Branch
codex/saketh-floor-overlay.- Before it: PR #22 (compile tests independent of the project cache) merged, then found that my piped
git pushhad been rejected because William's agent had pushed a docs commit onto that branch, so #22 merged without the fail-open throttle its description claimed. Landed it as PR #24, corrected the record on #22, and verifiedmain. Runtime path verified on a fresh clone ofcodex/visa-sandboxplus the fix:setup.shthere creates the cache table; two real compiles wrote a throttle row to the database cache; with the table dropped compile still answered 200. - Built:
frontend/src/region/FloorOverlay.tsx(only renderer-specific file; mask in, pose out),useRegion.ts,frontend/src/catalogue/{api.ts,CatalogueShelf.tsx,shelf.css}, room presets (shared/scene-fixtures.jsonrooms,frontend/src/scene/fixtures.ts, per-room layouts inbackend/api/scene_service.py/scene_views.py), testsfrontend/src/region/rooms.test.tsandbackend/api/test_rooms.py. - Found and fixed in the browser: (1) three.js clones shader uniforms, so the drawn region never changed after first mount although the count did, exactly the "rotation is cosmetic" failure Saketh predicted; (2) the chosen turn reset when the pointer moved from the shelf to the floor; (3) a stale hover survived a new search; (4) dev counts overlapped the results list.
- Browser verification (headless Chrome, software WebGL, private stack on 8211/5211): "an armchair" → chip
armchair, 1,005 matches. Hover POÄNG → floor lights. Per-rotation counts 8881 / 8961 / 8881 / 8961, matching Saketh's hand calculation; R changes the count and visibly reshapes the lit rectangle (520 × 553 px → 533 × 527 px in top view). Click outside the lit region: nothing, item stays in hand. Click inside: placed, saved (revision 1), POÄNG joins the server's product list. Snap-to-grid off (aria-checked=false): placed at a proven sample, turn kept (180°). No solver/editor disagreement logged. One transient 503 from SQLite contention in scene storage; the app's retry saved it. Eye-level view checked; dot pattern now fades with screen-space sample size. Studio room: one click, server agrees (260 × 200, two seeded items), EKTORP → "won't fit — there is no free floor big enough for its 218 × 88 cm footprint." - Copy: the panel says it fits or why it won't; raw sample counts only with
?dev=1. - Docs:
docs/frontend/region-solver.md(app flow, renderer contract, rooms),docs/backend/contracts/scene-api.md(room presets), notes appended toTODO_SIN.md,TODO_ADELLE.md(overlay colour is her call),TODO_WILLIAM.md.AGENTS.mdgains "Clean up after yourself";.gitignoregains.scratch/. - Verification: backend 138 tests, frontend 61 tests, build OK. Processes stopped and ports checked (0 listeners); scratch files under
.scratch/; the stray screenshot beside the repo was removed. - Not started, by decision: William's Blender-MCP agent-context request and his three catalogue-import tasks stay in Next.
- Before it: PR #22 (compile tests independent of the project cache) merged, then found that my piped
-
Combined product and GLB import handoff (2026-09-19). Added three planning tasks under Next and matching Saketh catalogue cards on the Kanban board. Sources, proposed Elasticsearch indices, field/provenance requirements, scene-ready listing gate, and verification criteria are recorded above. No data was imported and no application code or runtime was changed. Verification: task titles and IDs match the board;
git diff --checkpassed. -
Read the other lanes' logs; fixed the compile tests for a database cache (2026-09-19). Branch
codex/saketh-compile-tests-cache, off freshmainafter merging PR #18 (main: 132 backend tests, 57 frontend tests, build OK). PR #18 first got a review fix: every carried product field is checked and the server's copy is what gets stored.- William's work is on
codex/visa-sandbox, open PR #19 (Visa IDX sandbox, accounts with MFA, checkout review), not onmain. It touches none offrontend/src/App.tsx,frontend/src/scene,frontend/src/region,backend/api,backend/catalogueorshared/; he deliberately left the App and Vite proxy alone. No collision with the floor-overlay work. - He logged two things for this lane. (1) A requested follow-up in this file's Next on his branch: Blender MCP-style scene and object context for the agent, with
docs/backend/contracts/blender-mcp-agent-handoff.md. Not started; not part of the overlay. (2) HisDatabaseCachemakes the fiveCompileEndpointTestsfail, because they areSimpleTestCaseand the compile throttle counts in the cache. Reproduced exactly (5 failures under a database cache) and fixed by pinning an in-memory cache on that test class only; runtime keeps the project's cache. Verified 132 tests pass under both the project settings and a database-backed cache. - Still open with William, not blocking: the shared catalogue/cart contract (his checkout uses a two-product fixture adapter).
- William's work is on
-
Catalogue items placeable in a scene (2026-09-19). Branch
codex/saketh-catalogue-placement, off freshmainafter merging PRs #16 and #17 (main: 123 backend tests, 54 frontend tests, build OK).- Sin gave blanket permission to work in his lane; this PR touches his files and says so.
- Frontend: optional
productonInstance;frontend/src/scene/products.ts(validInlineProduct,productOf,productsWith);placement.ts,commands.ts,useSceneSync.ts,App.tsxresolve through it;instanceFromListinginfrontend/src/region/boundary.ts. - Backend:
backend/api/catalogue_products.py(catalogue listing → scene product, the backend's only mm→cm crossing) andresolve_productinbackend/api/scene_service.py. Non-fixture ids must be catalogue listings, must carry their product, and the carried dimensions must match the catalogue within 0.05 cm. Snapshots list placed catalogue products. - Docs: new section in
docs/backend/contracts/scene-api.md;docs/frontend/region-solver.mdandTODO_SIN.mdnotes updated. - Verification: backend 131 tests (8 new), frontend 57 tests (3 new), build OK; all pre-existing tests unchanged and passing. End to end by real id
ikea-193.025.39: backend test adds, reads back, moves, collides with and removes a POÄNG; frontend test takes the real feed entry through reducer, placement check, solver and snapshot round trip. Live on a private port:/api/search?q=poäng→POST /api/scene/commands/→ read back; the same id with width shrunk to 30 cm is refused with "The carried product does not match the catalogue." - Noticed: the dev stack running in Saketh's own terminal is stale (its Django knows only health, search and compile; autoreload missed the branch switches). Left alone; it needs a restart.
-
Shared convention: sync before work (2026-09-19). Branch
codex/saketh-sync-convention. Added a "Sync before starting work" section toAGENTS.md(pull,./setup.sh, tests and build on freshmain, branch offmain, re-readAGENTS.mdandINDEX.md; stop if cleanmainis red; retarget a stacked PR before deleting its base). Scoped per Saketh: pull at task boundaries (task start, after a merge), never mid-task, so Sin's no-mid-task-fetch rule and the sync both stand. Appended a note about the change toTODO_SIN.mdandTODO_WILLIAM.md.CLAUDE.mdis a symlink, so it follows. Documentation only; links checked. -
Solver follow-up: fixture room, "nothing fits" reasons, near semantics (2026-09-19). Branch
codex/saketh-nothing-fits, off freshmainafter merging PR #15 (main: 123 backend tests, 51 frontend tests, build OK;AGENTS.mdunchanged).- Dev page and debug script now solve in Sin's 600 × 500 cm fixture room (
DEV_SCENEinfrontend/src/region/devScene.ts); the small mock room stays asMOCK_SCENEfor tests. frontend/src/region/explain.ts:whyNothingFitson the solution, derived from the binding constraint (height, footprint vs room, no free floor, wall-clearance span shortfall, or the one clause whose removal makes room). Shown on the dev page.- Found: "by the window, 5 feet from any wall" contradicted itself in every room, because the window is on a wall the item must stay 152 cm from and
nearreached only 75 cm. An unstatednearnow reachesNEAR_DEFAULT_CMbeyond the clearance its target's wall demands; a stated distance stays literal. The hero sentence lights 705 centres in the fixture room and correctly reportsneeds 373 cm of depth, this room has 360 cmin the mock room. - Verification:
npm test— 54 tests,npm run buildOK; rendered SVG inspected.
- Dev page and debug script now solve in Sin's 600 × 500 cm fixture room (
-
Region solver (2026-09-19). Branch
codex/saketh-region-solver. Doc: docs/frontend/region-solver.md, linked fromINDEX.md. Handoff notes appended toTODO_SIN.md.frontend/src/region/:types.ts,boundary.ts(only unit crossing, listing adapter, wall mapping),grid.ts,occupancy.ts,invariants.ts(calls Sin'svalidatePlacementper grid point),geometry.ts,clauses.ts(six clause rules, every/some split, tunables,ALLOW_STACKING = false),solve.ts,svg.ts,debugScenes.ts,devScene.ts,rng.ts, tests.frontend/scripts/region-debug.mjs.- Decisions by Saketh: edge-to-edge distances;
againstfixes yaw with 5 cm tolerance;neardefault 75 cm; door swing always kept free; 90 cm sill is a commented assumption;on(item)dropped behind a flag; dropped clauses shown on the dev page. - Dev page solves the compiled sentence against a stub of the mock room and shows the floor, legal counts per rotation and dropped clauses in red.
- Shared files touched, additively:
frontend/src/scene/all-tests.ts(one import),frontend/package.json(one script),INDEX.md,TODO_SIN.md(appended section). - Verification:
npm test— 51 tests (24 new),npm run buildOK, backend 123 tests OK. Property tests, seeded, no new dependency: mask equalsvalidatePlacementat every grid point over 40 random scenes × 4 rotations; 2,000 snapped drag poses each land on a sample; no lit point overlaps blocked or unknown occupancy; over 60 random scenes with random clauses every lit point (>100,000) is accepted by the editor and clauses only ever narrow. Dev page driven in headless Chrome: droppedon(sofa-1)shown with its reason; "nothing fits" shown when 5 ft from every wall is impossible in a 4.2 × 3.6 m room. Debug SVGs inspected by eye.
-
Merge and sync before the solver (2026-09-19). Merged PR #13 (wall exceptions) to
main;any_walldual meaning approved by Saketh. Syncedmainafter Sin's Three.js editor merge:./setup.shOK, 123 backend tests OK (1 skipped),npm run buildOK, 27 frontend tests OK. Re-readAGENTS.md(one new rule: every doc underdocs/needs a workingINDEX.mdlink before handing back) andINDEX.md. Readdocs/frontend/3d-engine-plan.md, the splatting pivot doc,docs/backend/contracts/scene-api.md, andfrontend/src/scene/{types,units,placement,useFurnitureDrag}.ts. Findings reported to Saketh and decided: sample the mask at grid points so it coincides with drag snap; fixedCELL_CM = 5, never derived fromSCENE_UNIT_CM; emit and accept Sin'sfloor-grid-v1; unknown is never lit; walls west=x0, east=x=width, north=z0, south=z=depth (Sin to confirm); no new test dependency; tests joinall-tests.ts. Integration blocker raised by Saketh with Sin: the scene accepts only the 3 fixture products, so a catalogueListingcannot be placed yet. -
compile(): wall exceptions (2026-09-19). Branch
codex/saketh-wall-exceptions, off freshmain(97 tests green before branching). No schema change.- Bug: "3 feet from all walls except left wall, 2 feet there" compiled to
distance_min(any_wall, 914)ANDdistance_min(w-w, 610);any_wallcoversw-w, so the exception was silently erased. - Prompt (
backend/catalogue/dsl/prompt.py):any_wallonly for uniform rules; one clause per wall when there is an exception or differing values; left/right mapped to west/east; separate rule for one-wall kinds. Strengthenedfits_w_maxafter the longer prompt made the bookcase case drop it. - Code (
backend/catalogue/dsl/compile.py):repair_walls()innormalise()makes it deterministic — expandsany_wallbeside a looser per-wall clause, collapses identical per-wall clauses toany_wall, turns several walls onnear/against/onintoany_wall; place clauses ordered by room wall order. - Found while checking
clearandnear:clearhad the same bug and the same fix works.near/againstare different: enumerating walls ANDs them into an impossible placement, and an exception there cannot be expressed in the DSL, so it is dropped. - Ninth fixture case added; all nine re-recorded. Verification:
manage.py test api catalogue— 103 tests OK, 1 skipped, API key blanked. Live: 11 wall phrasings × 8 rounds all correct after the final change; nine-case live fixture passed 4 of 4.
- Bug: "3 feet from all walls except left wall, 2 feet there" compiled to
-
Phase D follow-up: fallback, timeout, retry, review fixes (2026-09-19). Same branch and PR (#11);
mainmerged in after #9 landed, one conflict in this file resolved by keeping both entries.- Latency DoD revised by Saketh to p50 under 1,500 ms (compile is a submit action). Measured after these changes: p50 1,182 ms, p90 1,667 ms, max 2,392 ms over 24 live calls, 24 of 24 exact.
gpt-4.1-ministays. - Fallback fixed:
backend/catalogue/text.pygainsSTOP_WORDS,content_words()andkeep_matching();fallback_program()searches content words and keeps a word only if results remain (text_search_has_resultsinviews.py, against the memory catalogue so it works with the model and Elasticsearch both down). The hero sentence now degrades to "reading chair" (235 results) instead of the whole sentence (0 results). Costs 0–45 ms; title tokens are cached intitle_words(). - Timeout 3 s → 6 s; the single retry now also covers timeouts; auth and quota errors are not retried; an abandoned call is cancelled.
- Review fixes:
POST /api/compilethrottled to 30/min per client (CompileThrottle), search untouched;GET /api/searchaccepts repeatedmaterial/colourparams so "oak and steel" keeps both clauses, and the dev page sends them. - Verification:
manage.py test api catalogue— 97 tests OK, 1 skipped, API key blanked;npm run buildpasses; live fixture test passed 3 of 3 runs; forced-failure run (nonexistent model) returned useful results for every sentence tried.
- Latency DoD revised by Saketh to p50 under 1,500 ms (compile is a submit action). Measured after these changes: p50 1,182 ms, p90 1,667 ms, max 2,392 ms over 24 live calls, 24 of 24 exact.
-
Phase D — language (2026-09-19). Branch
codex/saketh-compile, offmain(not stacked).- Built:
backend/catalogue/dsl/compile.py(compile_text: one structured-output call through the OpenAI Agents SDK withProgramas output type, one turn, no tools; at most one retry on an invalid Program; never raises; falls back to{find:[{k:"text",q}],place:[]}),prompt.py,render.py+units.py+colours.py(Program → chips, pure),POST /api/compileinviews.py/urls.py(AllowAny, 300-character cap), fixturedsl/fixtures/compile_cases.json(8 sentences, expected Programs, chips, recorded model outputs), tests intest_compile.py, dev page text box wired to the endpoint. Wall labels added to the mock room for chips. - Model:
OPENAI_MODEL, defaultgpt-4.1-mini. Started at the bottom:gpt-4.1-nanoscored 4/8, then 6–7/8 after prompt work, and kept inventing placement clauses or expandingany_wallinto four walls;gpt-4o-mini6–7/8 (drops materials);gpt-5.4-nano6/8;gpt-4.1-mini8/8 on every run. Latency is the same across tiers. - Shared file touched:
.env.examplegains an optionalOPENAI_MODELline. - Verification:
manage.py test api catalogue— 79 tests OK, 1 skipped (the live test), run withOPENAI_API_KEYblanked to prove no network.npm run buildpasses. Live throughPOST /api/compile: 40 compiles, 40 of 40 exact-match Programs, median 1,056 ms, p90 1,336 ms, min 709, max 2,620. Dev page driven in headless Chrome end to end. - Latency DoD not met: median 1,056 ms against a 400 ms budget. A one-word reply from the same API takes about 560 ms from this network (time to first token 360–490 ms on every model tried, including priority tier), so no model choice reaches 400 ms here.
- About 1 call in 20–50 hangs past the 3 s timeout and degrades to a text search. Every live failure observed was a timeout; none was a wrong Program. Timeouts are not retried, as specified.
- Built:
-
facets.fits_room_of(2026-09-19). Branchcodex/saketh-fits-room-of. PR #8 (Phase E) merged tomainfirst, including a review fix: ingest now removes stale seed documents when the seed count shrinks.- Contract change, approved by Saketh, additive and optional, in its own commit touching only
backend/catalogue/types.pyandfrontend/src/lib/types.ts:fits_room_ofis the count matching every clause except the width gap. Absent withoutfits_w_mm. - Implementation:
facets.py,memory.py,to_es_query.py(width clause moves topost_filter;fits_room_ofis amatch_allfilter aggregation; category and price facets nest underfits_room). Dev page shows "N of M fit". - Verification:
manage.py test api catalogue— 64 tests OK;npm run buildpasses. Live:category=armchair&fits_w_mm=900gives 523 of 1,005 in one request from Elasticsearch; 270-query sweep across both backends, 268 identical, 2 free-text queries with identical totals and facets in relevance order.
- Contract change, approved by Saketh, additive and optional, in its own commit touching only
-
Phase E — scale and aggregations (2026-09-19), done before Phase D because D is blocked on the OpenAI key. Branch
codex/saketh-scale.- PRs #2, #3, #5 merged to
mainin order (merge commits). Deleting #2's branch closed #3 instead of retargeting it; recovered by restoring the branch, reopening, retargeting tomain, then deleting again. For #5 the retarget was done before the delete.mainafter all three: 45 tests OK,npm run buildpasses. - Built:
backend/catalogue/seed.py(12,000 deterministic seed listings, 30-colour hero palette only),facets.py(memory counting + matching Elasticsearch aggregations:termscategory,rangeprice bands,filterfits_room),load_catalogue()infeed.py, aggregations into_es_query.py/es.py, facets on the memory backend, ingest of the full catalogue with a single refresh, facets on the dev page. Tests intest_scale.py. - Fixed:
offset + limitbeyond 10,000 made Elasticsearch reject the request and silently fall back to memory. Both backends now return 400 past that window. - Verification:
manage.py test api catalogue— 57 tests OK;npm run buildpasses. Live: ingest indexed 12,040, 0 failed, in 2.8 s; count 12,040 (12,000 seed + 40 ikea); mapping still exactly the agreed one, no dynamic fields; 30 distinct colours./api/search?fits_w_mm=600 / 900 / 1200 givesfits_room3,383 / 5,142 / 6,924 withX-Search-Backend: elastic; absent without the param. Fresh API processes after ingest. 266-query sweep across both live backends comparing items and facets: 265 identical, no fallbacks;q=lamp steelhas the same total (388) and facets but a different first page, the documented relevance-order difference. Latency on localhost: memory 5–15 ms, Elasticsearch 40–50 ms from this laptop.
- PRs #2, #3, #5 merged to
-
Phase C — Elastic (2026-09-19). Branch
codex/saketh-elastic, stacked on Phase B. PR #5.- Built:
backend/catalogue/to_es_query.py(pure),es.py(client + backend),ingest.py(bulk, refuses a missing index),text.py(shared tokenizer), backend switch with memory fallback andX-Search-Backendheader inviews.py, tests intest_elastic.py. - Backend alignment: memory now orders by id (the Elasticsearch tiebreak) and matches text by whole title word, exact category or material substring, the same three ways as the Elasticsearch query. Colour threshold tightened from 90 to 60 after a test showed dark green matching near-black.
- Verification:
manage.py test api catalogue— 45 tests OK, no network, all 7 find clauses covered; colour agreement proven offline over 246 probe colours. Live on Elasticsearch 9.5.4: ingest indexed 40, 0 failed, count 40, no dynamic fields added to the mapping.curl '/api/search?category=armchair&fits_w_mm=900'undermemoryandelasticis byte-identical (2,648 bytes), headersmemoryandelastic, no fallback logged. Sweep of 261 queries across both live backends: 260 identical, 0 with different results, 1 order-only (q=lamp steel, relevance order — deliberate, documented).
- Built:
-
Phase B — search stubs (2026-09-19). Branch
codex/saketh-search-stubs, stacked on the Phase A branch.GET /api/searchon the in-memory backend:backend/catalogue/{views,urls,params,memory,colour,feed,thumbs}.py, feed inbackend/catalogue/data/listings.json(40 items, all 12 categories), tests inbackend/catalogue/test_search.py.backend/catalogue/mock/room.py— dev stub room (wallsw-n/w-e/w-s/w-w, doord1, windoww1, instancessofa-1,lamp-1) androom_refs()for compile().- Dev page
frontend/dev-search.html+frontend/src/dev/search.tsx— standalone Vite entry, soApp.tsxis untouched and it is not in the production build. - Shared file touched: one
include("catalogue.urls")line inbackend/api/urls.py(agreed with Saketh). - Auth decision: search is
AllowAny, and compile will be too. Reason: catalogue browsing must work logged-out like any storefront, and the UI lane is blocked without it. Scoped to these two views only; the project default staysIsAuthenticated. Cart and checkout auth are William's call — tell him when he is next around. - Env: catalogue keys now live in the repo's single
.env;.env.localremoved. Key name isOPENAI_API_KEY(empty until Phase D). - Verification:
manage.py checkclean;manage.py test api catalogue— 21 tests OK;npm run buildpasses;curl '/api/search?category=armchair&fits_w_mm=900'returns 4 armchairs with integer mm dims; dev page rendered in headless Chrome: 24 rows on load, 4 after applying the same filters, all thumbnails decoded.
-
Phase A — scaffold and shared types (2026-09-19).
- Adapted the lane to the existing Django + Vite stack instead of Next.js: API in Django, validating schemas in Pydantic, TypeScript mirrors for the frontend.
- Added
backend/catalogue/types.py,backend/catalogue/dsl/schema.py,backend/catalogue/tests.py,frontend/src/lib/types.ts,frontend/src/lib/dsl/schema.ts,docs/backend/catalogue.md. - Dependencies added to
backend/pyproject.toml/uv.lock:elasticsearch,pydantic,openai-agents. No frontend dependencies added. - Appended catalogue keys (empty values) to the shared
.env.example; added the doc toINDEX.md. - Verification:
manage.py checkclean;manage.py test api catalogue— 8 tests OK;npm run buildpasses;./run-local.shserves the frontend (200) and/api/health/directly and through the Vite proxy.
-
PR #2 review fixes (2026-09-19).
- Contract wire format: added
to_wire()inbackend/catalogue/types.py(exclude_none), sofacets,fits_room, optionalRefids,near.mmandqtyare omitted instead of serialized asnull, matching the TypeScript types.Listing.model_urlstays present-and-nullable via a serializer. TypeScript unchanged. Tests:WireFormatTestsinbackend/catalogue/tests.py. - Removed the stale
.env.localguidance from.env.example; only.envis loaded. - Verification:
manage.py test api catalogue— 12 tests OK on the Phase A branch.
- Contract wire format: added
-
Cross-lane, now built (
backend/api/test_furniture_assets.py), kept here for the reasoning: units stay millimetres in the catalogue and centimetres in the 3D pipeline, converted asmm / 10at Sin's boundary. Wanted later: a check that a listing's GLB bounding box agrees withdims_mm / 10, failing loudly instead of rendering at the wrong scale. Needs Sin's loader and realmodel_urlassets. -
Phase E note: seeded listings must draw colours from a bounded palette (under 2,000 distinct), or the colour filter's palette aggregation truncates and the backends drift.
-
Shared files touched, additive only:
.env.example,INDEX.md,backend/pyproject.toml,backend/uv.lock. Teammates need to rerun./setup.shafter pulling. -
Tell William: search and compile are
AllowAny(see Phase B). His lanes are unaffected. -
OPENAI_API_KEYis empty in.env; needed before Phase D. -
The Elastic onboarding skill is installed locally only (
.agents/,.claude/,skills-lock.jsonare excluded via.git/info/exclude, not committed).
Edited through the board's own API (its write lock), two changes, both cards owned by Saketh:
- Done: "Search the catalogue using spatial constraints" (
41b8ced7).fits_room/fits_room_ofaspost_filterwith facets, identical in the memory and Elasticsearch backends; merged since the search phases and re-verified tonight when the index was re-ingested (12043 listings,AGREE). - Removed, cancelled by Saketh: "Normalize DummyJSON and WANDS reference products for Elasticsearch" (
5f377efe). His words on 2026-09-20: "Kenney, WANDS and DummyJSON stay deferred permanently." The board has no cancelled state, and an open card for work nobody will do misleads the other three. The card held only a title, owner andphase: mvp; it is recoverable from Git history.
Left open on purpose:
- "Implement spatial constraints and valid placement regions" (
5823c711): the solver, the lit floor in both editors and the wall fix are merged and verified; it closes when the window PR (#46) merges, because "by the window" is part of what that card means. - "Add speech and typed user input" (
3dc03f7b): typed input and Deepgram are merged and verified with a recorded clip; it closes after Saketh's own real-microphone test, which a headless browser cannot do (its fake microphone delivers silence). - "Import CC0 GLB asset metadata…" (
46dd9a0d) and "Curate product-to-GLB links…" (1baafdb6): asset work, which moved to the second agent's lane tonight. Not touched. - "Add Blender MCP-style scene and object context to the agent" (
6ae44373): deferred by Saketh earlier; not touched.
Lane since 2026-09-20 00:35: shared/models/**, backend/catalogue/data/listings.json, scripts/fit_glb.py and new scripts, backend/api/test_furniture_assets.py, their docs, and the Elasticsearch re-ingest after every listings change. Kanban card: "Curate product-to-GLB links for placeable demo listings" (1baafdb6), still open; no board edit made. Works in its own worktree (.scratch/asset-lane, never holding main, so the first checkout can always git checkout main); ports 8311/5311 if it ever starts a server.
- Task 1, bed refit: already done in PR #45 before the hand-off; verified, not redone (2026-09-20, 00:50).
- On
b97ac44:uv run python manage.py test→Ran 210 tests in 10.164s/OK (skipped=2);npm test→tests 118 / pass 118 / fail 0andtests 5 / pass 5 / fail 0;npm run build→✓ built in 3.65s. - Measured with
api.glb.measure, not read from metadata: bed 162.67 × 212.00 × 99.33 cm against 1620 × 2120 × 1000 mm, root scale 1.0410 / 1.1451 / 1.1160, anisotropy 10.00% against the 0.1 cap,model_urlset onikea-805.589.66. The +0.67 / −0.67 cm residual is recorded inmetadata.jsonand asserted. Better than the 9.9% / 1.9 cm fit in the brief because the miss is shared by two axes. LISABO table exact at 6.83%. EKTORP (165.01 cm wide against 179) and the HEKTAR lamp stay unbound. - Trial merge, local and thrown away, of
main5e56eef+ #45 + #49:Ran 221 tests in 8.694s/OK (skipped=4); frontend 122 + 5 passing; build OK. - After #45 merges the index is stale by two
model_urls untiluv run python -m catalogue.ingestruns frommain; this lane does that and thenuv run python -m catalogue.index_check.
- On
- Index agreement, read-only, before anything else (00:40):
python -m catalogue.index_check(then still PR #44, run from a scratch export) →files: 12043 listings, sha256 6d510c1fd1412fa3/index: 12043 listings, sha256 6d510c1fd1412fa3 ('listings')/AGREE. The 12,041 in the brief was already out of date. - Literal count pins (PR #49). Pins that break on any new listing:
backend/catalogue/test_search.pylen(listings) == 43and(total, len(items)) == (43, 10). Unpinned under Saketh's carve-out, two lines, nothing else in that file: the first is now a floor (>= 43), the secondlen(load_listings()). Probe, not committed, with a throwaway 44th listing: old pins →FAILED (failures=2, skipped=4), exactly those two tests; unpinned →Ran 217 tests in 6.917s/OK (skipped=4), frontend unchanged. Armchair pins left alone on purpose (test_search.py(6, 5),(5, 6),5;test_elastic.pytotal == 5): this lane adds no armchair without raising it first. - Found, for task 2 (Amazon Berkeley Objects):
SourceisLiteral["ikea", "stub", "seed"]withextra="forbid", so an ABO listing needssource: "stub"or a contract change intypes.py+types.ts; ABO carries no prices, soprice_centswould be a placeholder and must say so; hero colours must equal the 30-colour palette exactly (test_scale.py);match_typeis written nowhere yet; the per-axis fit lives in PR #45, so #45 lands first. Open PR #35 (Adelle, bookshelf + coffee table) adds two listings and would have tripped the same two pins. - Merged #49 then #45, on Saketh's instruction to merge inside this lane and report afterwards (00:58 to 01:00). After #49, fresh
main27ba503:uv run python manage.py test→Ran 217 tests in 6.886s/OK (skipped=4);npm test→tests 122 / pass 122 / fail 0,tests 15 / pass 15 / fail 0;npm run build→✓ built in 2.69s. After #45, freshmain0042078:Ran 221 tests in 7.477s/OK (skipped=4); 122 + 15;✓ built in 2.82s. Thenpython -m catalogue.index_check→DISAGREE in 2 place(s)(the bed's and the table'smodel_url, count equal at 12043, hashes different),python -m catalogue.ingest→indexed 12043 into 'listings', 0 failed, 0 stale seed documents removed, check again →files: 12043 listings, sha256 9aac6c07eae0d3a0/index: … 9aac6c07eae0d3a0/AGREE. Merged branches deleted. - Took over Adelle's #35 as #54 and merged it (01:06). Her branch merged in with her commits intact;
INDEX.mdandTODO_ADELLE.mdconflicts kept both sides. As pushed it failed two tests on currentmain(FAILED (failures=2, skipped=4): the coffee table 24.5 cm narrow, and two listing colours off the 30-colour palette). Coffee table refitted withscripts/fit_glb.py <model> 180 59 40: exact needs 15.8%, so it stops at the 10% cap, 178.31 × 59.00 × 41.69 cm, 1.69 cm residual recorded and asserted. Bookshelf bound as pushed (generic model, syntheticstublisting). Colours#6b4a34→#7a5235,#1f2733→#30475e. CI on the PR: backend pass 40 s, frontend pass 43 s. Freshmaincac9880:Ran 221 tests in 13.035s/OK (skipped=4); 122 + 15;✓ built in 4.64s. Ingest →indexed 12045 into 'listings', 0 failed; check →12045 listings, sha256 d2449beb1b56b722both sides,AGREE. #35 shows as merged. - Task 2 spike, one Amazon Berkeley Objects item: draft PR #58, NOT merged, price decision pending (01:05 to 01:20, about 13 minutes of work against a 25-minute box). Licence read from the dataset's own README, LICENSE and website: CC BY 4.0, not the CC BY-NC we expected. ABO has no price field, and the results card prints
price_cents(frontend/src/catalogue/CatalogueShelf.tsx:159), so the spike listing carries0as a placeholder andpriceProvenance: "placeholder"in itsmetadata.json."abo"added to theSourceenum and its TypeScript mirror in a commit by itself (Saketh's carve-out).scripts/abo_import.pyimports one product by ASIN: unit-parsed dimensions (20,502 of the dimension fields on modelled products are inches), the model's box decides width against depth, model and record must agree inside the asset tolerance, a size in the title must agree, cameras/lights/animations stripped, gltf-transform with the repo's flags, floor fit, re-check. First itemabo-B072M1WJ8S(Rivet Emerly sofa): 51.9 MB → 2.72 MB, 13,822 triangles, 212.19 × 88.94 × 86.40 cm against 2121 × 889 × 864 mm. New assertions inbackend/api/test_furniture_assets.py. On the branch:Ran 229 tests in 11.970s/OK (skipped=4); 122 + 15;✓ built in 4.31s. Probe, not committed: the sofa's inches entered as centimetres →FAILED (failures=2). Dry run of eight more products, nothing written: 8 of 8 would import, mean 6.5 s each including the download. Supply on amazon.com inside the triangle budget: 21 distinct sofas, 94 chairs, 149 tables, 33 lamps, 363 rugs, 17 shelves, 19 vases. Lamps, rugs and vases carry nocolor_code, so their palette colour needs another source before a batch. matchTypeon every binding (#60, merged 01:33) and the armchair count unpins (#61, merged 01:36); CI waited for on both. #60:verified/reconstructed_from_product_photo(every Meshy asset, Saketh approved the third value) /generic_visual_proxy(demo bookshelf, fixture sofa; refused behind a real SKU). Probes: HERRÅKRA without it →FAILED (failures=1, skipped=4); HERRÅKRA as a generic proxy →FAILED (failures=1, skipped=4). Freshmaincf3ca7c:uv run python manage.py test→Ran 222 tests in 7.238s/OK (skipped=4);npm test122 + 15;npm run build→✓ built in 2.60s. #61: five one-line unpins intest_search.pyandtest_elastic.py; the fifth (candidates=6, a keyword argument) was missed by grep and found by the throwaway-armchair probe (old pins →FAILED (failures=4, skipped=4); unpinned with two probe armchairs →Ran 221 tests/OK (skipped=4)). Freshmain1052e2b:Ran 222 tests in 6.735s/OK (skipped=4); 122 + 15;✓ built in 2.41s.- The ABO batch: 51 products in draft PR #58, held (01:30 to 01:55). Saketh's decisions applied: price 0 means unknown;
"abo"source; title size is a gate (ABO scaled its models to the record, so only the title can catch a wrong record); zero rugs; 512 px textures (measured: about 36% of the 1024 px file, sofa 2.72 → 0.98 MB); colour from the texture median where the record has nocolor_code. Newscripts/abo_select.pyandscripts/abo_batch.py. Result: 11 armchairs, 10 tables, 9 sofas, 8 lamps, 5 bookcases, 4 dining chairs, 4 vases; 36.4 MB of GLBs, largest 1.57 MB. Ten are tabletop objects (six table or desk lamps, four vases):ALLOW_STACKINGhas something to stack now; not touched. Batch: 50 quotas met in about four minutes, eight refusals replaced by spares; two removed by hand (a "Pack of 2" chair; a vase with "green" in its title, becausetest_model_failure_is_a_200_with_a_text_searchassumes no listing matches that word). On the branch merged withmaine29f020:uv run python manage.py test→Ran 241 tests in 14.391s/OK (skipped=4);npm test→tests 135 / pass 135 / fail 0,tests 15 / pass 15 / fail 0;npm run build→✓ built in 4.43s. Probes: invented price beside a placeholder provenance →89900 != 0; emptytitleConfirms→[] is not true. - Why #58 is held, and what unblocks it (for the catalogue lane). Today a
price_centsof 0 satisfiesprice_max(probe: returned byprice_max=40000), lands in the0-10000price band, and prints as$0. Saketh's rule: unknown cannot satisfy a price constraint. Needed:price_cents > 0in both price clauses (memory.py:27-28,to_es_query.py:41-42), unknown prices left out of the price bands (facets.py, both the memory counter and therangeaggregation), and the card saying "price unavailable" (CatalogueShelf.tsx:159, alsodev/search.tsx:207). Consequence to say out loud: the hero sentence says "under $400", so it will return only priced (IKEA) listings; ABO breadth shows on every other query. Checked: the results list fetches no GLBs (cards use the SVGthumb_url; hover runs the solver; a model loads only once an item is in the scene), and catalogue prices do not reach the Visa checkout. - #58 merged (02:44), then the office batch #76 (02:48); the asset lane is done. The catalogue lane's #70 made an unknown price behave as unknown. Its tests, and #65's, assumed things the 51 ABO listings changed, so under Saketh's widened carve-out (fix tests this lane's own data invalidates, minimally, by name) #58 also touches:
test_scale.pytwo lines + thehas_priceimport (price bands add up to the hits that HAVE a price:12045 != 12096,3383 != 3402),test_unknown_price.pyone line (nine real unpriced sofas beside the probe:5 != 14),test_results_filter.pyfive lines in one test (58 listings with a model no longer fit a page of 24; the page is nowlen(with_models) + 24so the "rest" assertions stay real). The patch was first proven on both trees and posted on the PR before anyone was asked. On #58:uv run python manage.py test→Ran 262 tests in 17.130s/OK (skipped=5); the same three files on plainmain→Ran 253 tests in 17.937s/OK (skipped=5); CI backend 50 s, frontend 54 s. Freshmaina753b83:Ran 263 tests in 18.667s/OK (skipped=5);npm test142 + 16;npm run build→✓ built in 5.26s. Index:DISAGREE in 51 place(s)→indexed 12096 into 'listings', 0 failed→12096 listings, sha256 364d92ab76a51a08both sides,AGREE. - Office batch #76: 28 products (10 desks, 4 file cabinets, 2 sideboards, 5 dining tables kept labelled as dining tables, 3 desk lamps, 1 bookcase, 3 office chairs), 18.6 MB, 28 of 28 in about two and a half minutes. The three office chairs are every one that passes the gates; each needed the new simplify ladder (
--simplify-error0.0003 / 0.001 / 0.003 / 0.01, recorded assource.simplifyError, asserted; probe with 0.05 →FAILED (failures=1, skipped=4)): 22,707, 24,144 and 9,784 triangles. Why office chairs are scarce is indocs/frontend/3d-object/abo-asset-notes.md: adjustable chairs are modelled at one height while the record states another, and most titles state no size. CI backend 53 s, frontend 46 s. Freshmaina3eb13d:Ran 263 tests in 20.728s/OK (skipped=5); 142 + 16;✓ built in 6.16s. Index:indexed 12124 into 'listings', 0 failed, 0 stale seed documents removed→12124 listings, sha256 39b0f5f840ad04f1both sides,AGREE. - Where the lane stands. 124 hero listings, 86 with a model: 79 Amazon Berkeley Objects (
verified, CC BY 4.0, price unknown), 6 IKEA reconstructions, 1 generic demo bookshelf. Ten tabletop objects for the stacking rule. Attribution text for the Devpost is in the ABO notes. Kanban "Curate product-to-GLB links for placeable demo listings" (1baafdb6) marked done; "Import CC0 GLB asset metadata…" (46dd9a0d) not started and not needed for the demo. Still temporary:AVOID_IN_TITLE = greeninscripts/abo_select.py, untiltest_model_failure_is_a_200_with_a_text_searchstops assuming no listing matches that word. No rugs were imported. 363 distinct rug models agree with their records, but rug titles state sizes in feet (6'7" x 9'), whichtitle_sizesdoes not parse, so every rug would fail the title gate today: teach it feet first, and wait for #68 (flat items do not block). After any future listings change: merge, suite on freshmain,python -m catalogue.ingest,python -m catalogue.index_check.
- Merged #44, #46, #47, #48, each followed by the whole suite on fresh
main. Last read,main3478b27:uv run python manage.py test→Ran 217 tests in 6.951s / OK (skipped=3);npm run build→✓ built in 2.74s;npm test→tests 122 pass 122 fail 0,tests 15 pass 15 fail 0. - CI (#50), merged. Proved from a bare worktree with an empty environment first (
Ran 217 tests in 8.459s / OK (skipped=4)), then its first run on its own PR, quoted from the job logs: backendRan 217 tests in 21.285s / OK (skipped=4)on Python 3.12.3, frontend# tests 122 # pass 122 # fail 0,# tests 15 # pass 15 # fail 0,✓ built in 8.27son Node v22.23.2; 40 s and 49 s wall clock. Writing it found thatnpm testnever ran the two auth test files (9 tests); it does now, andfrontend/scripts/every-test-runs.test.tsfails for any test file unreachable fromnpm test(checked with an orphan file: exit 1, names it). - Docs: three known limits in
docs/frontend/region-solver.md(footprint is not the bounding box; diagonal walls; left and right), the three-foot fallback on the real-GPU checklist, the stale "settle the pointer" sentence removed from the run of show now that #47 is in. - Next: the room model (distance from the mask, portals, components), behind the safety gate Saketh set: Cg Arch 4 ft stays 220 / 265 / 220 / 265, the full hero sentence 60 / 75 / 60 / 75, 5 ft still says "needs 371 cm of width, this room has 334 cm", and the two-directional property test at full scale stays at zero both ways.
- For the asset lane, the rug answer: yes, every placed item is a blocking box, in TWO places that must agree: the editor's oracle (
frontend/src/scene/placement.ts, theoverlaps(target, footprint(other…))loop) and the server's save validation (backend/api/scene_service.py, "Overlaps …"). Neither looks at height or category, so today a rug makes its floor unusable. A rule "an item no taller than 3 cm does not block" is cheap (one condition in each loop, plus lifting the lit-floor overlay above 3 cm so it is not hidden under the rug), but it must land in both checks together or the editor lights floor the server refuses. Not built yet; do not link rug models until it is. Waiting for Saketh's go, because it touches the two most safety-critical functions. - For the asset lane, the hero sentence: measured against the ABO draft (#58), a colour word returns 1 to 5 of its armchairs ("light-coloured" → 1; the batch is grey, brown and black), a style word far more ("modern" → 8, "accent" → 8, "mid-century" → 5). Today's hero is "a yellow reading chair by the window, four feet from any wall", which returns HERRÅKRA with no price clause.
- Merged tonight: #57 (mirrored overlay), #63 (TOTP test race), #52, #56, #59. Re-walked the whole demo path on
mainafter the mirror fix, every claim checked by a click at a point that separates the true region from its mirror: all beats hold. Open: #65 (models-first mode), and the hero-colour PR that carries this entry.
- For the asset lane: rugs are open. #68 is merged: an item no taller than 3 cm neither blocks nor is blocked, in the editor's check and the server's. From Saketh: rugs are low priority, three to five for texture, not fifty.
- For the asset lane: the price rule is merged (#70).
price_cents: 0means unknown: it matches no price filter, is in no price band, and reads "price unavailable". After you ingest unpriced listings, runELASTIC_LIVE_TEST=1 uv run python manage.py test catalogue.test_unknown_pricefrombackend/: it compares memory with the live index on six price queries. - Heads-up from #69 (William's room shopping): in shopping mode (
/room/<id>) a card is disabled unless its listing has a/models/furniture/model AND a price above zero, so unpriced ABO items cannot be picked there. The demo route (/?room=cg-arch-interior) is not in shopping mode and is unaffected. - Search now defaults to models-first (boost): everything is returned, listings with a model on top. Merged tonight after the last entry: #70, #65, #67, #68.
Lane: backend/shopping, backend/checkout, frontend/src/shopping, frontend/src/checkout, CatalogueShelf.tsx, SplatEditor.tsx. Same worktree (.scratch/asset-lane), ports 8311/5311, and a headless Chrome of its own on 9411 for looking at the app. Every PR below: CI green before merge, then uv run python manage.py test, npm test and npm run build on fresh main.
- Read first (03:34 and 03:50). At 03:34 nothing called "Cart preview", no placement animation and nothing by Sin was on
main, any branch or any PR; the cart/checkout, category rail, walk/jump and skyscraper floors were William's. Sin merged at 03:47 (cb21109). HisCartPreview(/?cartPreview) is a hands-off choreography over the live room with two FIXED sample thumbnails and "no purchase submitted": it reads no cart, price or backend, so it is not a checkout. What it adds that matters:SplatEditor'sobservationmode renders the real room as an inert backdrop. Placement animation is the materialize reveal plus halo helpers inscene/playcanvas/. Baseline oncb21109:Ran 274 tests in 19.332s/OK (skipped=5);tests 165 / pass 165,tests 18 / pass 18;✓ built in 5.50s. - (a) #90, merged 04:04: a piece with a model and no known price can go in the cart. Demo-blocking: a tower furnished from the ABO office batch gave an empty cart (7 of 124 hero listings buyable, $1,623.99; all 79 ABO refused twice: no price, and a lowercase-only model-folder pattern while ABO folders are
abo-<ASIN>). Now two facts: can go in the cart = deployed model with approved metadata (cart_product, capitals accepted); priced =price_cents > 0. The false label "Preview model unavailable" is gone. Found only by eye: the placement-confirm panel printed "$0.00 USD · sandbox" for an ABO chair; it goes throughpriceLabelnow. Probe: old pattern →FAILED (failures=1, errors=1, skipped=5), "This furniture has no deployed model." Freshmain8da8020:Ran 277 tests in 17.576s/OK (skipped=5); 166 + 18;✓ built in 4.68s. - (b) #92, merged 04:12: the partial bill. "N items · M priced · $X" in the cart and the checkout; unpriced lines list with
nullamounts and read "price unavailable";amount, and so Visa'spurchaseAmount, is the priced sum (asserted against the mocked Visa call); the MFA consent sentence names the priced items; a cart with nothing priced is refused in plain words. Freshmain00b6368:Ran 278 tests in 17.896s/OK (skipped=5); 167 + 20;✓ built in 4.79s. - Routing #93, merged 04:21 (found by Saketh rehearsing).
/is the gallery again;cg-arch-interioris the gallery's third room; the plain editor route shows Rooms and Cart. The gallery lists every public room and marks the ones whose assets are not on the machine ("Not on this machine") instead of silently dropping them. Freshmain8f81e32:Ran 278 tests in 12.727s/OK (skipped=4); 167 + 21;✓ built in 4.33s. (4 skips, not CI's 5, only because the cg-arch model was in the worktree for the eye check.) - (c) #94, merged 04:25: a vendor on every line.
ikea→ IKEA,abo→ Amazon, synthetic → the sandbox vendor. FRIDAY stays the merchant of record and Visa'smerchantName(asserted). Freshmain9cc3ca0:Ran 278 tests in 17.253s/OK (skipped=4); 167 + 22;✓ built in 4.75s. - By eye, headless Chrome against each branch: ABO armchair picked from the search, floor lit, placed, confirmed; it materialized bottom-up with the gold threads and the two halo helpers, then settled; category rail in place; W walked, Space jumped and landed.
/cart: "Amazon · Quantity 1 · price unavailable", "IKEA · Quantity 1 · $149.00 each", "2 items · 1 priced · $149". Not seen by eye: the checkout review page, which needs a signed-in account with MFA; its data is asserted by backend tests and its lines are type-checked. - Not done. (d) retiring the unused fixture checkout path: cut by Saketh. (e) the room behind the real checkout: not started; Sin's
observationmode makes it a small change, but it is the one item that touches the editor on the pages where people sign in. The "Cart preview" link still opens Sin's choreography with its fixed thumbnails. - A slip, recorded: one shell chain ran past a failed
git diff --check, so a commit failed for want of its message file and the push after it still ran, pushing the branch with no new commit. Harmless (it equalledmain), caught from the output, redone as a strict&&chain. The subshell pattern(cmd; echo exit=$?)hides a failure from&&; do not use it before a push.
- Look while holding (#91, merged 04:07). With a piece in hand a press that travels 4 px is a look and a press that does not is a click that places; walking, jumping and pointer-lock look stay suspended. Two causes, one per flow: the rail's
pendingProductIdwas folded into navigation'ssuspended(), and the search panel's layer swallowedpointerdown/upon the canvas so the engine never saw the press. Files:frontend/src/scene/playcanvas/navigation.ts(holding()predicate, looking gated apart from moving),interaction.ts,runtime.ts(externalHold, set byfrontend/src/catalogue/SplatCatalogueLayer.tsx; on the runtime so thatSplatEditor.tsx, another lane's file, is untouched), and a test ininteraction.test.tsthat fails when the gate is reverted (tests 153 / pass 152 / fail 1). CI on the head:Ran 274 tests in 29.235s / OK (skipped=5);# tests 166 # pass 166 # fail 0,# tests 18 # pass 18 # fail 0;✓ built in 7.92s. - By hand at eye level on that head (reads of the saved scene): HERRÅKRA picked up, 524 px drag (about 90°), still in hand, nothing placed; back past the start, patch at screen (741–840, 595–640); click beside the green, a 6 px press and an out-and-back press each placed nothing; a click with 2 px of tremor placed it at (955, 380), inside the hero patch x 945–965, z 340–410, and the halo helpers carried it in. Rail: grey sofa in hand, drag turned the view, W moved nothing, tremor click placed it at (860, 380). Nothing in hand: W 26.2% of room pixels, mid-jump 40.7%, landed 0.1%.
- Whole demo path re-walked on
main9cc3ca0(67742a3adds only a log entry to this file). Suite there:uv run python manage.py test→Ran 278 tests in 16.973s / OK (skipped=4);npm run build→✓ built in 4.56s;npm test→tests 167 pass 167 fail 0,tests 22 pass 22 fail 0. Every beat held: "1,017 matches, showing 12"; hover lights 114,942 px and leaving clears it; R changes the region (112,254 px); Esc drops the piece; unlit floor places nothing, lit floor placed at (915, 410); Undo, Redo, Undo; five feet says "needs 371 cm of width, this room has 334 cm" with nothing lit; the hero sentence gives its four chips and "228 matches · 1 ready in 3D, shown first"; from above the patch is at screen (1007–1027, 335–397), where the true region maps and not where a front-to-back mirror would put it (y 499–558); a click at the mirror spot (1017, 528) and one beside the patch (1045, 366) placed nothing, a click on it (1017, 366) placed at (955, 375); reload keeps the URL and the chair, the screenshots before and after are identical (0 of 434,600 pixels); "392 matches · 1 ready in 3D, shown first". - Found, not changed (other lanes' files): (1) with a search-panel piece in hand, W A S D or F still asks for the pointer (
SplatEditor.tsxkey handler knowspendingProductId, not the panel's hold); Esc recovers but drops the piece and leaves walk mode. (2) Adding a catalogue item from the rail is refused with400 A catalogue item must carry its productbecause the rail's add sends no inline product. (3) In headless Chrome a plain press with nothing in hand drops its look gesture when the pointer-lock request is refused; identical oncb21109, before #91. All three are in the run of show now. - A slip, recorded:
./setup.shrunsnpm ci, which deletesfrontend/node_modulesbefore reinstalling. Run in the shared checkout after a pull, it took the modules out from under whoever else was using them. From 04:00 this agent installs and runs only in its own worktree (.scratch/wt-saketh, ownnode_modules, venv and database), and the shared checkout getsgit pullonly. - Dropped on Saketh's instruction: the vendor registry build (the checkout lane's #94 carries the vendor on every line; the WIP branch was never pushed) and the tower measurement (William's #86 loads 32 floors; his slab heights agree with the measurement, 7.18 m to 145.2 m with unequal spacing, and his 4 × 8 m zone lies inside the slab at the lowest, widest and highest floors).
Cart choreography and the Elasticsearch flip (the former asset-lane agent, 2026-09-20 04:50–05:25): work log, append-only
Same worktree (.scratch/asset-lane, own node_modules, venv, database), ports 8311/5311 and a headless Chrome on 9411. Nothing was run in repo/; 5173, 8000 and 9200 were never used. No Kanban card was added: no existing card covers this, and it was finished inside the hour.
- #97, merged 05:18: the cart preview is folded into the cart. The
?cartPreviewroute,CartPreview.tsx,cart-preview.cssandpreviewOrderDispatch.tsare gone; an old/?cartPreviewlink lands on the gallery. On/cart, once the account is ready, the shopper's own pieces gather under Review checkout and settle, from the real cart only: real names, thumbnails, quantities and prices,price unavailablewhere the price is unknown, no sample data. Empty cart: no stage. New:frontend/src/checkout/CartChoreography.tsx,cart-choreography.css,cartLines.ts;frontend/src/shopping/CartReady.tsxis the only place that mounts it. Doc:docs/frontend/shopping-cart-animation.md(Sin's original note kept word for word under History). - The hard line held by construction. Anonymous
/cartis the sign-in step (the form is on that page), so the room backdrop could not be kept off those steps cleanly. It is dropped; the item choreography runs on a plain background. No WebGL, no room, no film, and no "on its way" (nothing has been ordered on/cart).SplatEditor.tsxwas not touched: itsobservationmode is now unused, andfrontend/public/animations/order-dispatch.{mp4,jpg}are now unreferenced. Both left for their owners. - A bug every suite missed, found by eye. The first version kept an "already played" ref set when the gathering started. StrictMode runs mount effects twice in development, which is how the demo runs, so the stage reached its finished state 2 ms after mounting (
"marks":{"choreo":413,"together":415}): the pieces never gathered. The component now has no such ref. No DOM test renderer exists in this repo, so the suite can only keep the ref from coming back. - By eye (one ABO armchair with no known price and the IKEA HERRÅKRA, put in the cart through the editor): signed-out
/cartshows both real lines and2 items · 1 priced · $149with"canvases":0,"videos":0,"choreo":0,"runningAnimations":0; ready step"marks":{"summaryLines":344,"choreo":373,"buttonLive":395,"together":2096}, button box ends at y 414 and the stage starts at y 442,"pointerEvents":"none"; a click on Review checkout 144 ms into the gathering went straight to the review step ("choreo":0,"canvases":0,"videos":0,"allAnimations":0); authenticator step the same four zeros; reduced motion"cls":"cart-choreo is-together is-still"at the first look with"allAnimations":0; ready step with an empty cart"choreo":0. - Suites. Branch:
uv run python manage.py test→Ran 278 tests in 18.436s/OK (skipped=5);npm test→tests 167/pass 167/fail 0, thentests 25/pass 25/fail 0(22 before, +3);npm run build→✓ built in 4.69s. CI on #97: backend pass, frontend pass. Freshmain67f44bbafter the merge:Ran 278 tests in 16.746s/OK (skipped=5);tests 167/pass 167/fail 0andtests 25/pass 25/fail 0;✓ built in 4.66s. - Search is on Elasticsearch.
repo/.envline 11 now readsSEARCH_BACKEND=elastic(the file is ignored and was not committed; no key was printed). The backend inrepo/has to be restarted to pick it up. Index and files agree, so nothing was reingested:python -m catalogue.index_check→files: 12124 listings, sha256 39b0f5f840ad04f1/index: 12124 listings, sha256 39b0f5f840ad04f1 ('listings')/AGREE. Through the interface on the private stack, headerX-Search-Backend: elasticon all three: "an armchair" →1,017 matches, showing 12; the hero sentence →228 matches · 1 ready in 3D, shown first, HERRÅKRA first; "armchairs under $400" →392 matches · 1 ready in 3D, shown first, HERRÅKRA first. These are the run of show's numbers, so the doc was not changed. Replayed at 05:21 against the live index:X-Search-Backend=elastic total=1017,total=228,total=392. Warm requests took 23–65 ms; the first request of a fresh process took 1,400 ms against a 3 s timeout (REQUEST_TIMEOUT_S), so run one search after restarting, before the demo. - Hazard, not fixed (another lane's tests): the backend suite is not hermetic to
SEARCH_BACKEND. With the demo.env, the README command fails two tests and queries the live cluster:FAIL: test_public_and_shaped_like_search_responseandFAIL: test_elastic_returns_the_same_shape_as_memory,AssertionError: Tuples differ: (532, 1017) != (13, 17),FAILED (failures=2).mainis not red; CI has no.env. Inrepo/run it asSEARCH_BACKEND= OPENAI_API_KEY= COMPILE_LIVE_TEST=0 uv run python manage.py test. A proven two-line pin ("SEARCH_BACKEND": "memory"in the twosetUps) is saved at.scratch/search-backend-test-pin.patch: with it and the demo.env,Ran 278 tests in 19.928s/OK (skipped=5). Not applied. - Cleanup. Stack and headless Chrome stopped;
lsofon 8311, 5311 and 9411 printed nothing;lsof -nP -iTCP -sTCP:LISTEN | grep -E "5173|8000|9200"printed nothing; the throwaway Chrome profile (and the throwaway test account's session in it) is deleted; the worktree's.envis back to its blank version, with no copy of any key.
Five editor fixes on Saketh's instruction, one commit each, in one PR. All work done in this agent's own worktree (.scratch/wt-saketh: own node_modules, venv, database; ports 8211/5211, headless Chrome 9334; SEARCH_BACKEND=memory for the stack so nothing of mine touched 9200). Nothing was run in the shared checkout but git checkout main && git pull.
- 1. Cart preview link removed from the editor header (
frontend/src/SplatEditor.tsx, the one anchor).App.tsx, the/?cartPreviewroute andcheckout/untouched. Header reads Rooms · Cart (n) · title · Saved · Reset view; checked by eye on both routes and the tower. - 2. A catalogue piece added from the rail carries its product. The add is built in
SplatEditor.place(the rail only callsonChoose), so that is where it is fixed, through a new pure helperinstanceToAddinfrontend/src/scene/products.ts: a catalogue piece is one a standing instance carries inline, and the add carries it again with only the fields the server stores. The server check is unchanged. Tests:frontend/src/scene/products.test.ts(reverted:tests 171 / pass 168 / fail 3) and a server-side test inbackend/api/test_catalogue_placement.pythat the listed product is accepted and the bare add still refused. By hand: HERRÅKRA placed from the search panel at (955, 375), then Chairs → HERRÅKRA from the rail, clicked (940, 670): the request carriedproductwith the server's eight keys,POST /api/scene/commands/ … 200, second chair at (905, 450). - 3. W A S D and F do nothing with a search-panel piece in hand. The key handler and
startWalkgated onpendingProductId(the rail's hold) only.frontend/src/scene/walkKeys.tsnow decides:pieceInHand(pendingProductId, runtime)is true for either hand,walkKeyActionreturns toggle / capture / null with the handler's old conditions term for term.startWalkasks too, which covers the Capture pointer button. Esc untouched. TestwalkKeys.test.ts(reverted:tests 175 / pass 173 / fail 2). By hand, countingrequestPointerLockcalls: piece in hand, W A S D F → 0 calls, cursorgrab, still in hand, Esc drops it; nothing in hand, W → 1, F → 2, as before. - 4. The search panel is open on load in both modes (
useState(true), wasuseState(!shopping)). Close search keeps it closed; the rail's Search purchasable catalogue reopens it. TestsearchPanel.test.ts(reverted:tests 177 / pass 176 / fail 1). By eye on/room/cg-arch-interiorwith no clicks: sentence box and microphone on screen, hovering the first result lit 114,942 px; Close search → gone and still gone 5 s later; reopened from the rail. - 5. The floor map stays reachable with the panel open.
FloorMaptakescompact(= the panel is open): floor number, up/down and the attribution line, under the view buttons (top:162px), clear of the panel at its tallest. The panel does not move; the map is never hidden; the full map returns when the panel closes. The attribution line is kept because for the CC BY tower it is the on-screen credit. By eye on/room/london-skyscraper-test: "Floor 1 / 32", both controls topmost under the pointer; Floor up with the panel open →?floor=C02, "Floor 2 / 32"; reopened the panel, hover lit the tower floor (201,243 px), Floor down →?floor=C01. Changing floor still closes the panel (William'schangeFloor, untouched). - Also in the PR, test-only: two search tests took their "memory" baseline from the ambient
.env. WithSEARCH_BACKEND=elasticthere and the cluster reachable they read the live index,(532, 1017)instead of(13, 17), and the suite wentFAILED (failures=2)on this machine; CI has no.envand never saw it. BothsetUps now pinSEARCH_BACKEND=memory. - Demo path, beats 1 to 8, re-walked on the branch on
/?room=cg-arch-interior: every read identical to the 04:40 entry above (1,017 / 114,942 px / 112,254 px after R / Esc / unlit nothing, lit (915, 410) / Undo / 371 cm refusal / four chips, 228 matches / patch (1054–1159, 625–675) / look while holding / from above (1007–1027, 335–397), mirror spot and beside place nothing, on the patch (955, 375) / reload identical, 0 of 434,600 px / 392 matches). On/room/cg-arch-interiorthe same hero beat ends in Placement preview (X 955, Z 375) and Confirm placement →POST /api/cart/confirm-placement/ … 200, Cart (1).