Skip to content

Latest commit

 

History

History
315 lines (245 loc) · 84.9 KB

TODO_SAKETH.md

File metadata and controls

315 lines (245 loc) · 84.9 KB

Saketh — tasks and agent work log

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.

MUST DO — Shared room test download

  • 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.sh

Open 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.

In progress

  • 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, validatePlacement property test, the four item C bindings), reconcile his how-to against the source, then the room bundle.

Next

  • 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 on codex/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 WANDS product.csv (MIT; tab-delimited despite .csv). Give each record a namespaced ID and bulk-index it into a new, explicitly mapped reference_products index 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 strict Listing contract and listings index 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_assets index: 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_proxy or verified exact match), and review status; never infer SKU identity from similar names. Only a small reviewed subset with positive dims_mm, explicit price provenance, and working model_url becomes Listing records. Publish those records to both backend/catalogue/data/listings.json (or its shared feed successor) and the Elasticsearch listings index from the same normalized source: backend/api/catalogue_products.py validates scene products against load_catalogue(), so Elasticsearch-only rows cannot be placed. Expose validated reference/asset lookup to the agent separately from the current GET /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.

Done

  • 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; lsof confirmed 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: node Room Glass Windows x 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, startCm 19.5, widthCm 280, sill 7.5. New shared/rooms/cg-arch-interior/openings.json, frontend/src/region/roomOpenings.ts, scripts/room_openings.py, backend/api/test_room_openings.py; Opening.sillCm?; useRegion takes openings; the PlayCanvas catalogue layer passes them. Not in manifest.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 from main 3a96dd6. I do not push to #45 again.

  • Refit under a 10% stretch cap (PR #45). fit_to_box in api/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 2 model_urls stale; re-ingested; both sides 12043 listings, sha256 6d510c1fd1412fa3.

  • main went 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 to test api catalogue, which skips accounts (15), checkout (9) and visa (12). Both commands give the same numbers on 4b3a258 and on current main. Rule now in AGENTS.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. New frontend/src/catalogue/quietSave.ts (+ test, 4 cases) and frontend/src/scene/playcanvas/pendingGhost.ts; SplatCatalogueLayer.tsx shows the item at once and drives Sin's session.retry() on the 0.5 s to 8 s backoff; useRoomSession.ts untouched; SplatEditor.tsx mount line gained two props. Found before running: the overlay effect was keyed on ready, 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 in docs/backend/contracts/scene-api.md; follow-up for Sin in TODO_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 unbound reason 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 gitignored assets/room.glb). In that room the overlay drew white: baked scene.exposure multiplied the emissive; fixed in regionOverlay.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 fresh main after merging PR #31 (main with 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 with scan. 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's SplatEditor, 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() in frontend/src/region/boundary.ts now takes them from the free areas' bounding box; openingZone and 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.
  • Deepgram voice input, built (2026-09-20); card still open, see In progress. Branch codex/saketh-deepgram, off fresh main after merging PR #28 and PR #29 (suite run on fresh main after 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/transcribe in views.py/urls.py, a raw-audio parser, TranscribeThrottle at 20/min. CompileThrottle and it now share FailOpenThrottle. Frontend frontend/src/catalogue/transcribe.ts and 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=1 passed. Through the app's proxy the fixture recording came back as "A reading chair under $400." in 540 ms and compiled to armchair, under $400. 415 and 413 refusals confirmed. The key appears in no log or response. In headless Chrome the button recorded with MediaRecorder, 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=1 shows which backend actually answered; search, compile_program and transcribe_audio are 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 in TODO_WILLIAM.md. Kanban: I had missed that the board rule reached AGENTS.md; from here I update my own cards only, minimally.
    • Also at this boundary: item C re-verified on merged main (overlay and discard, raycast, GLB load, footprint). No new renderer or GLB dataset from William is on main, 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.
  • First real 3D asset: HERRÅKRA armchair fixed, listed and wired (2026-09-20). Branch codex/saketh-herrakra-chair, off fresh main.

    • 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.md conflict was resolved keeping both sides.
    • backend/api/glb.py (stdlib GLB reader, world-space bounds, fit_to_height), scripts/fit_glb.py. Applied to her model.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.47 HERRÅKRA armchair, 710 × 660 × 730 mm, $149, model_url set, 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.py checks 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.md added 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.
  • A failing save can no longer trap the user in a room (2026-09-20). Branch codex/saketh-room-switch-never-stuck, off fresh main after merging PR #27 (suite re-run on that main: 175 / 65 / build OK). Sin was messaged directly (commit comment on e93075a) 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 in localStorage (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 offline block, 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 IMMEDIATE bought 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). Branch codex/saketh-merge-discipline, off fresh main after merging PR #26 (main re-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 to TODO_SIN.md, TODO_WILLIAM.md and TODO_ADELLE.md. Documentation only.

  • Storage may delay a save, never refuse a placement (2026-09-20). Branch codex/saketh-storage-never-blocks, off fresh main after 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 on codex/adelle-herrakra-chair-asset. No file collisions with this work.
    • Adele's chair inspected and she has the verdict (comment on commit eb7b3ec, note in TODO_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.py now sets transaction_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.ts retries 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's sync.test.ts updated, 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/.
  • 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 push had 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 verified main. Runtime path verified on a fresh clone of codex/visa-sandbox plus the fix: setup.sh there 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.json rooms, frontend/src/scene/fixtures.ts, per-room layouts in backend/api/scene_service.py / scene_views.py), tests frontend/src/region/rooms.test.ts and backend/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 to TODO_SIN.md, TODO_ADELLE.md (overlay colour is her call), TODO_WILLIAM.md. AGENTS.md gains "Clean up after yourself"; .gitignore gains .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.
  • 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 --check passed.

  • Read the other lanes' logs; fixed the compile tests for a database cache (2026-09-19). Branch codex/saketh-compile-tests-cache, off fresh main after 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 on main. It touches none of frontend/src/App.tsx, frontend/src/scene, frontend/src/region, backend/api, backend/catalogue or shared/; 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) His DatabaseCache makes the five CompileEndpointTests fail, because they are SimpleTestCase and 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).
  • Catalogue items placeable in a scene (2026-09-19). Branch codex/saketh-catalogue-placement, off fresh main after 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 product on Instance; frontend/src/scene/products.ts (validInlineProduct, productOf, productsWith); placement.ts, commands.ts, useSceneSync.ts, App.tsx resolve through it; instanceFromListing in frontend/src/region/boundary.ts.
    • Backend: backend/api/catalogue_products.py (catalogue listing → scene product, the backend's only mm→cm crossing) and resolve_product in backend/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.md and TODO_SIN.md notes 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 to AGENTS.md (pull, ./setup.sh, tests and build on fresh main, branch off main, re-read AGENTS.md and INDEX.md; stop if clean main is 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 to TODO_SIN.md and TODO_WILLIAM.md. CLAUDE.md is 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 fresh main after merging PR #15 (main: 123 backend tests, 51 frontend tests, build OK; AGENTS.md unchanged).

    • Dev page and debug script now solve in Sin's 600 × 500 cm fixture room (DEV_SCENE in frontend/src/region/devScene.ts); the small mock room stays as MOCK_SCENE for tests.
    • frontend/src/region/explain.ts: whyNothingFits on 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 near reached only 75 cm. An unstated near now reaches NEAR_DEFAULT_CM beyond the clearance its target's wall demands; a stated distance stays literal. The hero sentence lights 705 centres in the fixture room and correctly reports needs 373 cm of depth, this room has 360 cm in the mock room.
    • Verification: npm test — 54 tests, npm run build OK; rendered SVG inspected.
  • Region solver (2026-09-19). Branch codex/saketh-region-solver. Doc: docs/frontend/region-solver.md, linked from INDEX.md. Handoff notes appended to TODO_SIN.md.

    • frontend/src/region/: types.ts, boundary.ts (only unit crossing, listing adapter, wall mapping), grid.ts, occupancy.ts, invariants.ts (calls Sin's validatePlacement per 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; against fixes yaw with 5 cm tolerance; near default 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 build OK, backend 123 tests OK. Property tests, seeded, no new dependency: mask equals validatePlacement at 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: dropped on(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_wall dual meaning approved by Saketh. Synced main after Sin's Three.js editor merge: ./setup.sh OK, 123 backend tests OK (1 skipped), npm run build OK, 27 frontend tests OK. Re-read AGENTS.md (one new rule: every doc under docs/ needs a working INDEX.md link before handing back) and INDEX.md. Read docs/frontend/3d-engine-plan.md, the splatting pivot doc, docs/backend/contracts/scene-api.md, and frontend/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; fixed CELL_CM = 5, never derived from SCENE_UNIT_CM; emit and accept Sin's floor-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 join all-tests.ts. Integration blocker raised by Saketh with Sin: the scene accepts only the 3 fixture products, so a catalogue Listing cannot be placed yet.

  • compile(): wall exceptions (2026-09-19). Branch codex/saketh-wall-exceptions, off fresh main (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) AND distance_min(w-w, 610); any_wall covers w-w, so the exception was silently erased.
    • Prompt (backend/catalogue/dsl/prompt.py): any_wall only 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. Strengthened fits_w_max after the longer prompt made the bookcase case drop it.
    • Code (backend/catalogue/dsl/compile.py): repair_walls() in normalise() makes it deterministic — expands any_wall beside a looser per-wall clause, collapses identical per-wall clauses to any_wall, turns several walls on near/against/on into any_wall; place clauses ordered by room wall order.
    • Found while checking clear and near: clear had the same bug and the same fix works. near/against are 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.
  • Phase D follow-up: fallback, timeout, retry, review fixes (2026-09-19). Same branch and PR (#11); main merged 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-mini stays.
    • Fallback fixed: backend/catalogue/text.py gains STOP_WORDS, content_words() and keep_matching(); fallback_program() searches content words and keeps a word only if results remain (text_search_has_results in views.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 in title_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/compile throttled to 30/min per client (CompileThrottle), search untouched; GET /api/search accepts repeated material / colour params 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 build passes; live fixture test passed 3 of 3 runs; forced-failure run (nonexistent model) returned useful results for every sentence tried.
  • Phase D — language (2026-09-19). Branch codex/saketh-compile, off main (not stacked).

    • Built: backend/catalogue/dsl/compile.py (compile_text: one structured-output call through the OpenAI Agents SDK with Program as 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/compile in views.py / urls.py (AllowAny, 300-character cap), fixture dsl/fixtures/compile_cases.json (8 sentences, expected Programs, chips, recorded model outputs), tests in test_compile.py, dev page text box wired to the endpoint. Wall labels added to the mock room for chips.
    • Model: OPENAI_MODEL, default gpt-4.1-mini. Started at the bottom: gpt-4.1-nano scored 4/8, then 6–7/8 after prompt work, and kept inventing placement clauses or expanding any_wall into four walls; gpt-4o-mini 6–7/8 (drops materials); gpt-5.4-nano 6/8; gpt-4.1-mini 8/8 on every run. Latency is the same across tiers.
    • Shared file touched: .env.example gains an optional OPENAI_MODEL line.
    • Verification: manage.py test api catalogue — 79 tests OK, 1 skipped (the live test), run with OPENAI_API_KEY blanked to prove no network. npm run build passes. Live through POST /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.
  • facets.fits_room_of (2026-09-19). Branch codex/saketh-fits-room-of. PR #8 (Phase E) merged to main first, 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.py and frontend/src/lib/types.ts: fits_room_of is the count matching every clause except the width gap. Absent without fits_w_mm.
    • Implementation: facets.py, memory.py, to_es_query.py (width clause moves to post_filter; fits_room_of is a match_all filter aggregation; category and price facets nest under fits_room). Dev page shows "N of M fit".
    • Verification: manage.py test api catalogue — 64 tests OK; npm run build passes. Live: category=armchair&fits_w_mm=900 gives 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.
  • 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 main in order (merge commits). Deleting #2's branch closed #3 instead of retargeting it; recovered by restoring the branch, reopening, retargeting to main, then deleting again. For #5 the retarget was done before the delete. main after all three: 45 tests OK, npm run build passes.
    • Built: backend/catalogue/seed.py (12,000 deterministic seed listings, 30-colour hero palette only), facets.py (memory counting + matching Elasticsearch aggregations: terms category, range price bands, filter fits_room), load_catalogue() in feed.py, aggregations in to_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 in test_scale.py.
    • Fixed: offset + limit beyond 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 build passes. 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 gives fits_room 3,383 / 5,142 / 6,924 with X-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 steel has 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.
  • 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 and X-Search-Backend header in views.py, tests in test_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' under memory and elastic is byte-identical (2,648 bytes), headers memory and elastic, 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).
  • Phase B — search stubs (2026-09-19). Branch codex/saketh-search-stubs, stacked on the Phase A branch.

    • GET /api/search on the in-memory backend: backend/catalogue/{views,urls,params,memory,colour,feed,thumbs}.py, feed in backend/catalogue/data/listings.json (40 items, all 12 categories), tests in backend/catalogue/test_search.py.
    • backend/catalogue/mock/room.py — dev stub room (walls w-n/w-e/w-s/w-w, door d1, window w1, instances sofa-1, lamp-1) and room_refs() for compile().
    • Dev page frontend/dev-search.html + frontend/src/dev/search.tsx — standalone Vite entry, so App.tsx is untouched and it is not in the production build.
    • Shared file touched: one include("catalogue.urls") line in backend/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 stays IsAuthenticated. 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.local removed. Key name is OPENAI_API_KEY (empty until Phase D).
    • Verification: manage.py check clean; manage.py test api catalogue — 21 tests OK; npm run build passes; 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 to INDEX.md.
    • Verification: manage.py check clean; manage.py test api catalogue — 8 tests OK; npm run build passes; ./run-local.sh serves 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() in backend/catalogue/types.py (exclude_none), so facets, fits_room, optional Ref ids, near.mm and qty are omitted instead of serialized as null, matching the TypeScript types. Listing.model_url stays present-and-nullable via a serializer. TypeScript unchanged. Tests: WireFormatTests in backend/catalogue/tests.py.
    • Removed the stale .env.local guidance from .env.example; only .env is loaded.
    • Verification: manage.py test api catalogue — 12 tests OK on the Phase A branch.

Blockers / handoff

  • 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 as mm / 10 at Sin's boundary. Wanted later: a check that a listing's GLB bounding box agrees with dims_mm / 10, failing loudly instead of rendering at the wrong scale. Needs Sin's loader and real model_url assets.

  • 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.sh after pulling.

  • Tell William: search and compile are AllowAny (see Phase B). His lanes are unaffected.

  • OPENAI_API_KEY is empty in .env; needed before Phase D.

  • The Elastic onboarding skill is installed locally only (.agents/, .claude/, skills-lock.json are excluded via .git/info/exclude, not committed).

Kanban: own-lane edits and what is deliberately still open (2026-09-20, 00:50)

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_of as post_filter with 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 and phase: 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.

Asset lane (second agent): work log, append-only

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 0 and tests 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_url set on ikea-805.589.66. The +0.67 / −0.67 cm residual is recorded in metadata.json and 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 main 5e56eef + #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 until uv run python -m catalogue.ingest runs from main; this lane does that and then uv run python -m catalogue.index_check.
  • 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.py len(listings) == 43 and (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 second len(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.py total == 5): this lane adds no armchair without raising it first.
  • Found, for task 2 (Amazon Berkeley Objects): Source is Literal["ikea", "stub", "seed"] with extra="forbid", so an ABO listing needs source: "stub" or a contract change in types.py + types.ts; ABO carries no prices, so price_cents would be a placeholder and must say so; hero colours must equal the 30-colour palette exactly (test_scale.py); match_type is 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 main 27ba503: 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, fresh main 0042078: Ran 221 tests in 7.477s / OK (skipped=4); 122 + 15; ✓ built in 2.82s. Then python -m catalogue.index_check → DISAGREE in 2 place(s) (the bed's and the table's model_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.md and TODO_ADELLE.md conflicts kept both sides. As pushed it failed two tests on current main (FAILED (failures=2, skipped=4): the coffee table 24.5 cm narrow, and two listing colours off the 30-colour palette). Coffee table refitted with scripts/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, synthetic stub listing). Colours #6b4a34 → #7a5235, #1f2733 → #30475e. CI on the PR: backend pass 40 s, frontend pass 43 s. Fresh main cac9880: 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 d2449beb1b56b722 both 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 carries 0 as a placeholder and priceProvenance: "placeholder" in its metadata.json. "abo" added to the Source enum and its TypeScript mirror in a commit by itself (Saketh's carve-out). scripts/abo_import.py imports 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 item abo-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 in backend/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 no color_code, so their palette colour needs another source before a batch.
  • matchType on 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). Fresh main cf3ca7c: uv run python manage.py test → Ran 222 tests in 7.238s / OK (skipped=4); npm test 122 + 15; npm run build → ✓ built in 2.60s. #61: five one-line unpins in test_search.py and test_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)). Fresh main 1052e2b: 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 no color_code. New scripts/abo_select.py and scripts/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_STACKING has 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, because test_model_failure_is_a_200_with_a_text_search assumes no listing matches that word). On the branch merged with main e29f020: 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; empty titleConfirms → [] is not true.
  • Why #58 is held, and what unblocks it (for the catalogue lane). Today a price_cents of 0 satisfies price_max (probe: returned by price_max=40000), lands in the 0-10000 price band, and prints as $0. Saketh's rule: unknown cannot satisfy a price constraint. Needed: price_cents > 0 in 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 the range aggregation), and the card saying "price unavailable" (CatalogueShelf.tsx:159, also dev/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 SVG thumb_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.py two lines + the has_price import (price bands add up to the hits that HAVE a price: 12045 != 12096, 3383 != 3402), test_unknown_price.py one line (nine real unpriced sofas beside the probe: 5 != 14), test_results_filter.py five lines in one test (58 listings with a model no longer fit a page of 24; the page is now len(with_models) + 24 so 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 plain main → Ran 253 tests in 17.937s / OK (skipped=5); CI backend 50 s, frontend 54 s. Fresh main a753b83: Ran 263 tests in 18.667s / OK (skipped=5); npm test 142 + 16; npm run build → ✓ built in 5.26s. Index: DISAGREE in 51 place(s) → indexed 12096 into 'listings', 0 failed → 12096 listings, sha256 364d92ab76a51a08 both 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-error 0.0003 / 0.001 / 0.003 / 0.01, recorded as source.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 in docs/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. Fresh main a3eb13d: 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 39b0f5f840ad04f1 both 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 = green in scripts/abo_select.py, until test_model_failure_is_a_200_with_a_text_search stops 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'), which title_sizes does 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 fresh main, python -m catalogue.ingest, python -m catalogue.index_check.

Log, appended (2026-09-20, 00:55)

  • Merged #44, #46, #47, #48, each followed by the whole suite on fresh main. Last read, main 3478b27: 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: backend Ran 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.27s on Node v22.23.2; 40 s and 49 s wall clock. Writing it found that npm test never ran the two auth test files (9 tests); it does now, and frontend/scripts/every-test-runs.test.ts fails for any test file unreachable from npm 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.

Log, appended by the first agent (2026-09-20, 02:35)

  • 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, the overlaps(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 main after 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.

Log, appended by the first agent (2026-09-20, 03:05)

  • 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: 0 means unknown: it matches no price filter, is in no price band, and reads "price unavailable". After you ingest unpriced listings, run ELASTIC_LIVE_TEST=1 uv run python manage.py test catalogue.test_unknown_price from backend/: 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.

Checkout lane (the former asset-lane agent, reassigned 2026-09-20 03:30): work log, append-only

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). His CartPreview (/?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's observation mode renders the real room as an inert backdrop. Placement animation is the materialize reveal plus halo helpers in scene/playcanvas/. Baseline on cb21109: 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 through priceLabel now. Probe: old pattern → FAILED (failures=1, errors=1, skipped=5), "This furniture has no deployed model." Fresh main 8da8020: 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 null amounts and read "price unavailable"; amount, and so Visa's purchaseAmount, 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. Fresh main 00b6368: 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-interior is 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. Fresh main 8f81e32: 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's merchantName (asserted). Fresh main 9cc3ca0: 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 observation mode 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 equalled main), 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.

Log, appended by the first agent (2026-09-20, 04:40)

  • 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 pendingProductId was folded into navigation's suspended(), and the search panel's layer swallowed pointerdown/up on 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 by frontend/src/catalogue/SplatCatalogueLayer.tsx; on the runtime so that SplatEditor.tsx, another lane's file, is untouched), and a test in interaction.test.ts that 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 main 9cc3ca0 (67742a3 adds 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.tsx key handler knows pendingProductId, 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 with 400 A catalogue item must carry its product because 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 on cb21109, before #91. All three are in the run of show now.
  • A slip, recorded: ./setup.sh runs npm ci, which deletes frontend/node_modules before 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, own node_modules, venv and database), and the shared checkout gets git pull only.
  • 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 ?cartPreview route, CartPreview.tsx, cart-preview.css and previewOrderDispatch.ts are gone; an old /?cartPreview link 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 unavailable where 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.tsx is 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 /cart is 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.tsx was not touched: its observation mode is now unused, and frontend/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 /cart shows both real lines and 2 items · 1 priced · $149 with "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, then tests 25 / pass 25 / fail 0 (22 before, +3); npm run build → ✓ built in 4.69s. CI on #97: backend pass, frontend pass. Fresh main 67f44bb after the merge: Ran 278 tests in 16.746s / OK (skipped=5); tests 167 / pass 167 / fail 0 and tests 25 / pass 25 / fail 0; ✓ built in 4.66s.
  • Search is on Elasticsearch. repo/.env line 11 now reads SEARCH_BACKEND=elastic (the file is ignored and was not committed; no key was printed). The backend in repo/ 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, header X-Search-Backend: elastic on 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_response and FAIL: test_elastic_returns_the_same_shape_as_memory, AssertionError: Tuples differ: (532, 1017) != (13, 17), FAILED (failures=2). main is not red; CI has no .env. In repo/ run it as SEARCH_BACKEND= OPENAI_API_KEY= COMPILE_LIVE_TEST=0 uv run python manage.py test. A proven two-line pin ("SEARCH_BACKEND": "memory" in the two setUps) 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; lsof on 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 .env is back to its blank version, with no copy of any key.

Log, appended by the first agent (2026-09-20, 05:25)

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 /?cartPreview route and checkout/ 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 calls onChoose), so that is where it is fixed, through a new pure helper instanceToAdd in frontend/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 in backend/api/test_catalogue_placement.py that 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 carried product with 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 startWalk gated on pendingProductId (the rail's hold) only. frontend/src/scene/walkKeys.ts now decides: pieceInHand(pendingProductId, runtime) is true for either hand, walkKeyAction returns toggle / capture / null with the handler's old conditions term for term. startWalk asks too, which covers the Capture pointer button. Esc untouched. Test walkKeys.test.ts (reverted: tests 175 / pass 173 / fail 2). By hand, counting requestPointerLock calls: piece in hand, W A S D F → 0 calls, cursor grab, 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), was useState(!shopping)). Close search keeps it closed; the rail's Search purchasable catalogue reopens it. Test searchPanel.test.ts (reverted: tests 177 / pass 176 / fail 1). By eye on /room/cg-arch-interior with 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. FloorMap takes compact (= 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's changeFloor, untouched).
  • Also in the PR, test-only: two search tests took their "memory" baseline from the ambient .env. With SEARCH_BACKEND=elastic there and the cluster reachable they read the live index, (532, 1017) instead of (13, 17), and the suite went FAILED (failures=2) on this machine; CI has no .env and never saw it. Both setUps now pin SEARCH_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-interior the same hero beat ends in Placement preview (X 955, Z 375) and Confirm placement → POST /api/cart/confirm-placement/ … 200, Cart (1).