Wires T93's `search_all_memories` service into a small read-only HTML
surface so users can find a memory across every chat in the database.
* `chat/web/search.py` (new): GET `/search?q=...` runs the FTS service
with k=50, hydrates each row with bot name + scene timestamp, and
renders `search.html`. Empty `q` short-circuits to no results so the
top-bar form can submit even with an empty input.
* `chat/templates/search.html` (new): empty-state placeholder, results
list with chat-level "Open chat" links (`/chats/{chat_id}` — memories
don't carry an event_id today, so no per-turn anchor).
* `chat/templates/layout.html`: append a small `<form>` to the rail
nav, additive only.
* `chat/app.py`: register `search_router` (additive import + include).
* `tests/test_search_ux.py`: 3 tests — multi-chat results, empty-query
placeholder, chat link.
Audit of chat/state/manual_edit.py target_kind dispatch found two §6.4
fields without drawer affordances despite being already-projected text
columns: chat_state.narrative_anchor and chat_state.weather. Both land
via new manual_edit branches (target_kind chat_narrative_anchor and
chat_weather) plus paired drawer routes and Scene-section text inputs.
The container properties_json blob is intentionally deferred — bounded
JSON edits aren't wired through manual_edit and the drawer never
surfaces multiple containers at once, so v1 leaves it out.
The Phase 1 single-bot ``record_turn_memory`` lingered next to the
unified ``record_turn_memory_for_present`` introduced in T84. Only test
fixtures still called the legacy entry point.
- Remove ``record_turn_memory`` from ``chat/services/memory_write.py``.
- Update the two test_memory_write.py callers to use
``record_turn_memory_for_present(..., guest_bot_id=None)``, which
produces the same ``[you=1, host=1, guest=0]`` witness mask.
The unified API returns ``dict[bot_id, (event_id, memory_id)]``; tests
extract the host entry. No production callers were affected.
The warning said "lifecycle transitions from superseded turn ARE NOT
being rolled back". When regenerating an OLDER turn, the listed
transitions can include intervening-turn ones that legitimately stand
on their own — they weren't authored by the superseded turn itself.
Reword to "lifecycle transitions at-or-after turn <id>" so operators
reading logs aren't misled into thinking every listed event id was
emitted by the target turn. Cosmetic change to a single log message.
Test: extends test_regenerate_with_prior_lifecycle_logs_warning to
assert the new phrasing is present and the old phrasing is gone.
The previous implementation pulled the last N rows in SQL across all
chats and dropped foreign-chat rows in Python. With LIMIT N this could
return far fewer than N relevant rows when other chats had recent
activity. Push the chat_id filter into SQL via json_extract so LIMIT N
always returns N rows scoped to the requested chat.
Test: seeds two chats with 60 turns each interleaved; queries chat_a
with limit=50; asserts exactly 50 chat_a rows returned (was 0 prior to
the fix because chat_b's rows dominated the global tail).
T85.1 — JSON-build audit (chat/state, chat/services, chat/eventlog):
no findings. Every JSON column write in those modules already uses
``json.dumps`` (chat/state/events.py, world.py, edges.py, group_node.py,
meanwhile.py, manual_edit.py, entities.py, chat/services/snapshot.py,
chat/eventlog/log.py); chat/state/meanwhile.py:48-49 even carries an
explicit comment about the ``json.dumps`` choice for safety against
quote/backslash injection. No production changes.
T85.2 — meanwhile cancel route-level coverage:
* ``test_meanwhile_turn_cancellation_via_route`` — pins the
end-to-end shape produced when /turns/cancel fires mid-meanwhile-
beat: assistant_turn lands with truncated=True (and the right
meanwhile_scene_id + speaker_id), no memory_written events fire, no
post-turn edge_update events fire, and _in_flight_tasks is empty
post-flight. Drives the cancel by hijacking client.stream to raise
CancelledError on first iteration — same pattern proven by
test_cancelled_turn_still_closes_scene_when_user_prose_signals_close
in tests/test_turn_flow.py. The synchronous TestClient can't issue
a second POST mid-stream from the same thread, and driving via
task.cancel() trips GeneratorExit-on-dependency that prevents the
conn from committing the partial; the inline-raise mirrors what
cancel_turn produces (CancelledError delivered on next await) and
is the standard idiom in this codebase. Combined with the existing
test_meanwhile_turn_registered_in_in_flight_tasks (registration
pin), the full Stop-button lifecycle for meanwhile beats is now
covered.
* ``test_meanwhile_cancel_route_no_op_after_turn_completes`` — runs
a meanwhile turn to completion, then POSTs /turns/cancel; asserts
204 no-op, no error, registry stays empty. Pins the cancel
endpoint's robustness against the racy "Stop just after stream
finished" sequence.
Suite: 334 -> 336 passing.
Extends record_turn_memory_for_present with a you_present: bool = True
kwarg so a single entry-point covers both you-scenes (witness_you=1)
and meanwhile scenes (witness_you=0). Validates that meanwhile callers
provide a guest_bot_id.
record_meanwhile_memory becomes a thin backward-compat wrapper that
delegates with you_present=False, preserving the call site in
chat/web/meanwhile.py without churn.
Cosmetic-only renumbering of the event-lifecycle detection block in
``regenerate_assistant_turn`` from ``# 10.`` to ``# 9a.`` — mirrors the
``# 8a.`` shape in ``chat.web.turns.post_turn``. The block was already
in the correct structural position (immediately after the interjection
branch); only the numbering and comment reflected an earlier draft
where it read as a final step rather than the post-interjection /
pre-(absent)-scene-close slot.
No behavioural change. All 9 regenerate tests + 18 turn_flow tests
pass without modification.