The 2023 client sends rooms/{id}/clone through the same RecNet rooms
helper as "save room" and "restore subroom save", which reads the
{success, value, error} envelope. The clone handler answered with the
bare room details, so the client found no success flag and rejected the
reply with an empty error, even though the room had been created.
Clone now returns the envelope with the full room details in value, and
refusals (unknown source, cloning not allowed) are a 200 with
success=false and a message instead of a bodyless 404/403.
Cloning was a shallow copy: the new room's scenes kept pointing at the SOURCE
room's blob until someone saved over them. So every clone of a room shared one
object, a clone read as "already has content" while owning none of it, and the
room a player copied stayed load-bearing for their copy indefinitely. Cloning
RecCenter produced rooms whose data blob was literally room_100_v1.dat, which
is visible in the join log.
Each sub-room's bytes are now copied into the clone's own namespace, keyed by
the CLONE's room id so BlobRouter files them under that room's prefix. Sub-room
0 takes the canonical default name, which matters because the details builder
falls back to exactly that name when a room has no recorded blob — writing the
copy there means both the recorded and the fallback path resolve to real
content. Each copy also gets a RoomDataBlobs row so it is tracked like any
other save.
Two cases deliberately do not clear a reference:
- With no object store configured there is nothing to copy, and a blob name is
only a reference; clearing it would throw away the player's edits, which is
strictly worse than the shared reference. That path keeps the old shallow
behaviour, which is also what the existing user-room clone test asserts.
- A source object that is missing or unreadable leaves the clone empty rather
than failing the request. The room and its sub-rooms are already committed by
that point, so a hard failure would strand a half-made room, and an unsaved
source clones to an empty room anyway.
Everywhere else a reference is either replaced by a real copy or cleared — it
is never left pointing into another room.
Starting a chat with a player POSTs thread/withmembers (which creates the
thread) and then immediately GETs thread/{id}. At that instant the thread has
no messages, and GetThreadById returned NotFound purely on message count — so
the client got a 404 for the thread it had just been handed and the
conversation could never be opened.
Confirmed from a server log: POST /thread/withmembers -> 200 at 06:38:20.862,
GET /thread/15 -> 404 at 06:38:21.277.
The handler now 404s only when nothing records the thread — no ChatThreads row
and no ChatThreadMembers rows — and otherwise returns it with an empty message
list.
Also adds RoomCloneShapeTests. The 2023 copy-room flow reads the clone reply
into FGCPNAACHIK, the same type get-by-id returns
(NLDBPDCNNCF.txt:4676 signature, :4802 route literal), so the two responses
must stay shape-identical: if they drift, the server still answers 200 and the
client throws while reading, surfacing much later from the finalizer as a bare
"Failed to copy room" with nothing in the server log. The test compares both
top-level and nested sub-room keys, and seeds a real scene row so the
comparison exercises the populated branch rather than the synthesised-default
fallback that a room with no scenes would take.