| D-M4-23-0 |
2026-10-24 |
Built on main after M4-20 and M3-14 landed; the changelog entry and these rulings are fragments. Differences to the brief's names, following the merged code: the search controller is SearchQueryController (provider searchQueryControllerProvider({channelId, threadId}), the in-thread search of M4-24 is this family with channelId and threadId), because SearchController is a class of Flutter's Material library; its logic lies in SearchSession and the ports in search/search_port.dart; the providers of the views lie in search/, switcher/, mentions/ and running/providers.dart; M4-14 indexes messages and thread titles on apply, so sync/search_index_feed.dart indexes only transcripts; the cross-cutting views open in the history area (thread/history_area.dart), which already shows the route of the shell; the app depends on built_collection directly, because the generated search takes BuiltList filters. |
D-W-100, D-W-114 (merged code wins over the brief's names). |
| D-M4-23-1 |
2026-10-24 |
Merge rule (Design note 1): the local and the server query start together; local hits show with the note "Messages stored only on this device" while the server is pending or the app is offline; the first server page replaces them. empty, too_long and other failures replace the hits by the reason; a connection failure keeps the local hits. While the connection is not live no server request is sent; when it becomes live again the server query of an offline search runs again. A newer submit makes every answer of an older submit stale (a generation counter). |
14 6; brief Design note 1. |
| D-M4-23-2 |
2026-10-24 |
Notation (Design note 2): from: (handle, optional @) and in: (channel name, optional #) resolve case-insensitively against the local principals and visible channels, an unknown value stays text, the last from: wins and several in: add up; has:file sets has_file; before: and after: take a date YYYY-MM-DD as the start of the day in UTC with the half-open interval of 14 4.4, an invalid date stays text; text in quotes stays text. For the same parameter the notation wins over the filter column; the in-thread search replaces every place filter. Filters outside their types (14 4.4) are not sent and are disabled in the column. The type is a single choice, because the API takes one type; transcripts belong to files (14 2). The time filters "last 24 hours, 7 days, 30 days" become after = now minus the window by the app clock. |
14 4.1, 4.4; brief Design note 2. |
| D-M4-23-3 |
2026-10-24 |
Local search: words with - and or are dropped instead of becoming required words; the local hits are checked against the local database for type, space, channel, thread, author, time, attachment and thread state, and a hit whose object is no longer stored is dropped; the local page size is ui.quick_switch_max_results (the only client key that bounds a hit list; no literal limit). A server file hit opens the channel of its scope_channel_id, because the API names no message or thread for a file; a local transcript hit opens its thread. |
Fixed requirement 9; 14 6; API SearchHit. |
| D-M4-23-4 |
2026-10-24 |
Recent targets (Design note 3): the local-only table recent_targets (kind, target_id, visited_at, primary key (kind, target_id)), schema version 3, never synchronized; visit keeps the newest ui.quick_switch_max_results rows; the shell records every /s/, /c/ and /t/ route once while it is shown; removeTargets deletes the rows of removed spaces, channels and threads (04 8.3), and the wipe deletes the database. |
01 D6, 01 11 has no key; 15 6.1. |
| D-M4-23-5 |
2026-10-24 |
Quick switcher: rows are spaces, text and forum channels, threads (main threads are represented by their channel), people (users except the signed-in person) and agents, and the commands of the instance, the current space and the current channel; DM channels are reached through their participant's row, which opens the stored DM whose other participants are exactly that person or agent, and otherwise only closes (the client never creates a DM). Matching is a case-insensitive word prefix per query word (14 4.9). Ranking: matching recent targets newest first, then rows whose whole name starts with the query, then the other matches, each group ordered spaces, channels, threads by activity, people, agents, commands; at most ui.quick_switch_max_results. An empty input shows the recent targets. Server titles of searchThreadTitles (only while connected, one request per input) merge by thread ID, the stored thread wins, an unstored one opens /t/<id>. Filters All, Channels (with spaces), Threads, People, Agents, Commands cycle with Tab and Shift+Tab. A command row closes the switcher without inserting anything, because commands run from the composer (M4-21). showQuickSwitcher(context) is the action M4-24 binds to Cmd+K and Ctrl+K. |
01 D6; brief Design note 3. |
| D-M4-23-6 |
2026-10-24 |
Running now: the view loads every page of listActiveRuns into the local runs table by the version rule when it opens and then follows the table (run.state_changed); a failed load shows a note and the local rows. Stop shows only where the server reported the channel level trigger and the person started the run or holds the capability manage (the server's ChannelView, as D-M4-20-4 does for the thread actions; R-AZ-07); it goes through the outbox as cancelRun and asks for no confirmation. The context share shows only while the run store holds the run's document (the run is focused elsewhere): the view never focuses runs itself. Durations count locally from started_at with the app clock once a second. The footer sums the costs of the shown organization runs per currency, always as an estimate (an active run is never reconciled), and notes "Personal runs: information only" when a personal run or a run of an unknown mode is shown; the cost mode is billing_mode_used, else billing_mode_expected. The sidebar counter is the count of active runs in the local table. |
01 D10, 6.7; brief Design note 4; 15 1 principle 1. |
| D-M4-23-7 |
2026-10-24 |
Mentions list listMyMentions page by page (server order, newest first) with the path and author from the local database and jump to /t/<thread>?m=<message>; the message text shows as plain text. |
01 3.5; brief requirement 16. |
| D-M4-23-8 |
2026-10-24 |
Transcripts in the offline index: a transcript is pending when its file has transcript_status = done, a stored non-deleted message attaches it and no transcript row exists; SearchIndexFeed fetches it once with getTranscript and indexes it with the location of the earliest attaching message; a failed fetch is retried on the next change of the watched tables (no timer). Transcript rows of files that are gone, no longer done or no longer attached to a stored non-deleted message are deleted, so a deleted voice message is not found offline. |
14 6; 04 2.2 item 5. |
| D-M4-23-9 |
2026-10-24 |
The fake server serves search, searchThreadTitles, listMyMentions and listActiveRuns (rest/search_handlers.dart) with the simplifications listed in the sync engine README; its limits lie in world.searchSettings. The integration tests start the run of Running now with world.createRun instead of a scripted agent script, so the stop does not race the script's steps. |
Brief Scope (handler registry of M4-17) and Tests. |
| D-M4-23-10 |
2026-10-24 |
Red team finding of M4-23 (a filtered offline search found too little, because every filter ran after the SQL limit): SearchDao.search takes the kind, space, channel and thread filters and an offset and applies them in SQL before the limit; LocalSearch reads further pages until the filters that need the stored objects (author, time, attachment, thread state) kept the limit or the index has no more rows. local_search_test.dart covers the class with more better-ranked rows than the limit. |
14 6: a filter must not hide stored hits. |