recent に絞り込みを足す

期間 (相対・絶対・範囲)・検索・差出人で絞れるようにする。ルーム横断や notice 種別は対象外

📅 作成: 2026-09-09 / 更新: 2026-09-09

ai-chat-lite-reviewer ではなく、public での実際の取りこぼし報告 (i260909-01、初回の wait が参加前の発言を拾わない件)をきっかけに、recent 自体の絞り込みを広げる計画である。今回のスコープは期間 (相対・絶対・上限つきの範囲)・キーワード検索・差出人の 4 種だけ。notice 種別の新設とルーム横断は対象外 (別に決める)。

目次

  1. 何を作るか
  2. 決めたこと
  3. オプション
  4. 内部の絞り込み
  5. 出力の形
  6. 作るものと確かめること

何を作るか

いまの recent は件数 (-n)でしか絞れない。「いつ以降」「何を含む発言」「誰が言ったか」は、返ってきた一覧を目で追うしかない。

足すのは 4 種の絞り込みで、すべて併用できる(AND 条件)。

種類内容
期間 (相対)--since-day <N> / --since-hour <N>。「N 日前」「N 時間前」以降
期間 (絶対)--since <日時>。「この日時以降」
期間 (上限・絶対)--before <日時>。「この日時より前」。--since と組み合わせれば範囲検索になる
検索--find <文字列>。本文にその文字列を含むものだけ
差出人--from <id>。その ID からの発言だけ

対象外にしたもの: notice 種別の新設 (周知だけを拾う経路)と、ルーム横断の検索。どちらも別の課題・計画で扱う。msg_kind の CHECK にはすでに notice が入っているが (版 2)、書く経路も読む経路も無いままである。

決めたこと

チャットで確認した 4 点。

#論点決めたこと
1--since の書式3 段の書式を受け付ける(yyyy/mm/dd ・ mm/dd ・ HH:mm、時刻は任意で追加)。年・日付を省いた分は、いまより未来になるなら 1 つ遡って解決する(詳細は 3 章)
23 オプションの関係--since-day / --since-hour / --since は排他。2 つ以上指定したらエラー (--wait-hour 系と同じ流儀)。--before はこの排他に含めない(--since 系と組み合わせて範囲にするため)
3-n との併用期間・検索・差出人のいずれかを指定したときは、既定の件数上限を 500 (MAX_HISTORY_LIMIT)にする。-n を明示すればそちらが勝つ。何も指定しなければ今までどおり 20 (recent の既定)
4--from の照合コロンで囲んでも囲まなくても受け付ける(--from :id: ・ --from id のどちらも可)。完全一致で、ID_PATTERN の文字集合だけ確かめる。前方一致は使わないので、--to のような囲み必須にする理由が無い

追加: --before (範囲検索)

チャットで出た案。--since と組にすれば「8/1 から 8/5 まで」のような範囲を表せる。書式・未来への丸めとも --since とまったく同じ規則を使う(下記)。

丸めを非対称にすると、組み合わせたときに破綻する。「--since 11/1 --before 11/30」を、丸めるのが --since だけだとすると、11/1 は今年が未来なので去年に丸まり、11/30 は丸めないので今年のまま——去年 11 月から今年 11 月末までという、意図しない 1 年がかりの範囲になる。「--since 22:00 --before 23:00」も同様に、時刻だけ日をまたいで食い違う。

「同じ規則で丸める」だけでは足りない。実装時の実測で判明した。--since と --before にそれぞれ独立に同じ規則 (「いま」と比べて未来なら遡る)を当てても、--since 9/9 --before 9/10(今日と明日)が壊れる。9/9 は今日なので未来ではなく今年のまま、9/10 を独立に見れば明日で未来なので去年に遡る——今日と去年という組み合わせになる。

正しくは、--before が --since の解決値を anchor (引き継ぎ元)として使うこと。年・日付を省いた分は、「いま」と比べるのではなく anchor の年・日付をそのまま使う (遡る判定自体をしない)。--since 単独・--before 単独のときは anchor が無いので、これまでどおり「いま」と比べて遡る。

API のパラメータ名が既存の before と衝突する。/api/history?before=<msg_seq> はすでにあり、数値の msg_seq によるページ送り(画面の遡り読み込み)に使っている。新しい絞り込みは before_ts(日時文字列)という別名にし、既存の before には触れない。CLI のオプション名 (--before)は recent にこれまで無かったので衝突しない。

オプション

options.mjs の OPTIONS に足す 6 つ。すべて cmd: 'recent'(このコマンドだけで使える)。既存の形をそのまま増やすだけなので、export-options.mjs の JSON の形 (schema)は変わらない。

longargdesc
since<日時>この日時以降 (3 段書式。下記)
since-day<N>N 日前以降
since-hour<N>N 時間前以降
before<日時>この日時より前 (--since と同じ 3 段書式・同じ丸め)
find<文字列>本文にこの文字列を含むものだけ。| 区切りで OR 検索
from<id>この ID からの発言だけ (コロンは有っても無くてもよい)

--since / --before の書式 (3 段)

どちらも同じパーサを共有する。上から順に試し、最初に形が合ったものを使う。

段形例年・日付を省いた分
① 年月日 (+任意で時刻)yyyy/m/d[<区切り>H:m[:s]]2026/8/1 / 2026/8/1-14:30 / 2026/8/1_14:30省いていない。そのまま使う
② 月日 (+任意で時刻)m/d[<区切り>H:m[:s]]8/1 / 9/10-22:0 / 9/10_22:0年は今年。今年の値がいまより未来なら 1 年遡る。--since・--before とも同じ
③ 時刻のみH:m[:s]6:0 / 22:0日付は今日。今日のその時刻がいまより未来なら 1 日遡る。--since・--before とも同じ

月・日・時・分・秒は 1〜2 桁のどちらでも受ける (8/1 でも 08/01 でもよい)。時刻を省くと 00:00:00、秒を省くと :00。年は 4 桁固定。

日付と時刻の区切りは、半角スペース・ハイフン・下線の 3 つを受け付ける(2026/8/1 14:30 / 2026/8/1-14:30 / 2026/8/1_14:30 はどれも同じ)。ハイフンか下線を使えば、コマンドラインでダブルクォートを省ける。スペースを使う形だけ、共通ルールの「CLI の引数はダブルクォートで囲む」に従って囲みが要る。

形を確かめたあと、月 1-12・日 1-31・時 0-23・分秒 0-59 の範囲だけ見る (うるう年等の厳密なカレンダー検査はしない)。合わなければ 3 段の書式を示してエラー (exit 2)。

丸めの基準は「いま」または anchor のどちらか 1 つ。--since を単独で解決するときは anchor が無いので、②③どちらの段でも組み立てた日時を「いま」と比べ、後ろなら 1 つ遡る (②は年を 1 引く、③は日を 1 引く)。--before を解決するときは、--since がすでに解決した値を anchor として渡す。anchor があれば、その年・日付をそのまま使い、「いま」とは比べない (遡る判定自体をしない)。--before が単独 (--since を渡さない)なら anchor も無いので、こちらも「いま」と比べる。

解決するコードは 1 つ (resolveDateTimeArg(raw, anchorTs))にし、--since/--before で処理を分岐させない。anchor の有無だけで両方をまかなう。

--from の照合

コロンで囲んでいれば剥がし、囲んでいなければそのまま使う。どちらの形でも ID_PATTERN の文字集合を確かめてから、完全一致で絞り込む。--to/waiters の囲み必須は「前方一致で探す式に埋め込むため」の制約だが、--from は SQL の = ? による完全一致だけなので、その制約が要らない。

内部の絞り込み

索引を 1 本足す (版 6)

いまの messages は (room_id, msg_seq) の複合索引しか持たない。日時で絞る条件 (sent_at >= ?)には効かないので、(room_id, sent_at) の複合索引を足す。sent_at は yyyy/mm/dd HH:MM:SS.mmm 固定 23 文字 (版 5 で / 区切りに統一済み)なので、文字列比較がそのまま時系列比較になる。

-- src/scripts/20_migrate/ver_000006/001-messages-sent-at-index.sql
CREATE INDEX IF NOT EXISTS messages_ix_room_id_sent_at ON messages(room_id, sent_at);

版フォルダを置くだけでよい。migrate.mjs はフォルダを読んで版を決めるため、どこかの定数を上げる作業は無い。次に本番サーバーが起動し直したときに当たる(既存行にインデックスを張るだけなので、いまの件数ならほぼ一瞬で終わる)。

絞り込みの SQL

store.mjs に getFiltered(roomId, { sinceTs, beforeTs, find, from, limit }) を足す。getLatest と同じ並び (ORDER BY msg_seq DESC LIMIT ? のあと .reverse())で、条件は渡された分だけ足す。

SELECT * FROM messages
WHERE archived_seq IS NULL AND room_id = ?
  [AND sent_at >= ?]        -- --since / --since-day / --since-hour
  [AND sent_at <  ?]        -- --before
  [AND (msg_body LIKE ? ESCAPE '\' [OR msg_body LIKE ? ESCAPE '\' …])]
  [AND from_connector_id = ?]
ORDER BY msg_seq DESC
LIMIT ?

since 側は「以降」なので >=(境界を含む)、before 側は「より前」なので <(境界を含まない)にする。境界の時刻ちょうどの発言が二重に扱われることがない。

--find の値は % _ \ をエスケープしてから LIKE に渡す。素の値だと %・_ がワイルドカードとして働き、たとえば --find "50%" が「50 のあと何でも」に化ける。エスケープすれば常にそのままの文字列を含むかだけを見る。

--find の値は | で区切ると OR 検索になる(--find "rule|ルール")。getFiltered 内で | 分割 → 前後の空白を trim → 空項を除外し、残った項ごとに (エスケープしたうえで)LIKE を OR で結ぶ。1 語だけのときは今までどおり。CLI 側の変更は無い(--find の値をそのまま find パラメータへ渡すだけなので、分割はサーバー側だけで完結する)。

大文字小文字は SQLite の既定 (ASCII だけ区別しない)。日本語には大小の区別が無いのでこの点は影響しない。実測で確認済み(--find "RULE" で本文中の rule がヒットする)。

API

GET /api/history に since_ts・before_ts・find・from_connector_id を追加する (すべて省略可)。既存の before(msg_seq によるページ送り)とは別名にし、触らない。渡さなければ今までどおりなので、画面 (chat.js の遡り読み込み)は無改修で動く。

出力の形

見出し行に何を絞ったかを出す。件数だけでは、絞り込んだ結果なのか全件なのか区別できないため。

(絞り込みなし、今までどおり)
public の直近 20 件:

(期間だけ、--since 2026/8/1)
public の 2026/08/01 00:00 以降 3 件:

(範囲、--since 8/1 --before 8/6)
public の 2026/08/01 00:00 以降 2026/08/06 00:00 より前 5 件:

(検索だけ)
public の「ルール」を含む 2 件:

(組み合わせ)
public の 2026/08/01 00:00 以降・「ルール」を含む・lightspeedc.github.io からの 1 件:

正確な文言は実装時に固定し、node 版・C# 版で一字一句揃える。突き合わせは tests/cli-cs.test.mjs が担う (このプロジェクトの決めごと。「CLI を直したら 2 本とも直す」)。

該当 0 件のときは 該当する発言がありません のように、絞り込みが効いたことが分かる文言にする (既存の「まだ何もありません」とは区別する)。

作るものと確かめること

ファイル変えること
src/client/options.mjsOPTIONS に 6 つ追加 (cmd: 'recent')
src/scripts/20_migrate/ver_000006/001-messages-sent-at-index.sql新規。(room_id, sent_at) 索引
src/server/store.mjsgetFiltered() を追加
src/server/server.mjshandleHistory が since_ts・before_ts・find・from_connector_id を読み、getFiltered に渡す
src/client/chat.mjscmdRecent に --since*・--before・--find・--from の解決と見出しの組み立てを足す。3 段書式の共有パーサを 1 つ作る
src/cli-cs/Commands.cs ・ Args.cs同じロジックを C# 版にも。2 本の出力が一致することをテストで見る
tests/索引の有無、絞り込みの単体テスト (store)、CLI 突き合わせ (cli-cs)、エラー系 (排他・書式)
USAGE ・ 設計書新しいオプションと使用例を追記

手で確かめること

テスト用サーバー (tools/40_test/start-test-server.cmd)で行う。本番へは繋がない。

  1. --since-day 1 で、1 日以内の発言だけが返ること
  2. --since "2026/8/1" と --since-day/--since-hour を同時に渡すとエラー (exit 2)になること。--before とは同時に渡せること
  3. 未来へ丸める分: 今日より先の月日・今日まだ来ていない時刻が、--since・--before のどちらでも同じ規則で 1 年前・1 日前に丸まること。--since 11/1 --before 11/30 が去年の 11 月 1 日〜30 日になる(今年の 11/1 と 11/30 がどちらも未来なら、両方が去年に丸まる)こと
  4. --since 8/1 --before 8/6 で、その範囲だけが返ること (境界の扱い: since は含む、before は含まない)
  5. --since 9/10-22:0 と --since 9/10_22:0 と --since "9/10 22:0" が、すべて同じ結果になること (区切りがスペース・ハイフン・下線のどれでも)
  6. --find "ルール" で本文にその文字列を含むものだけ返ること。%・_ を含む文字列でも文字どおりに扱われること
  7. --find "rule|ルール" で OR 検索になること。前後の空白・空項 ("rule| ")を無視すること。%・_ は OR の中でもワイルドカードとして働かないこと
  8. --find "RULE" で本文中の rule がヒットすること (大文字小文字を区別しない)
  9. --from :id: と --from id(コロン無し)の両方で、その ID からの発言だけ返ること
  10. 複数を組み合わせても正しく絞れること
  11. node 版と C# 版の出力が一字一句同じであること

本番サーバーの再起動

索引を足す版 6 のマイグレーションがあるため、実装を反映するには本番サーバーの再起動が要る(aichat restart)。再起動しない限り、新しいオプションは 400 (未知の引数)またはサーバー側の絞り込みが効かないまま返る。