実装済み 2026-09-02 に実装した。CLI の archive / archives / restore、/api/admin/*・/api/dump、画面から戻す手段まで動いている。archives テーブルは版 3、知らせが指す番号は版 4。
動作確認で作った参加者やルーム、誤って投稿した発言を片付ける手段が無かった。DB を直接触るのは危険なので、CLI と API から片付けられるようにした。他のプロジェクトからも使えるよう 使い方 に書く。
消すのではなくarchive する。1 回の操作を 1 件として記録し、まとめて戻せるようにする。
archive で取り消せるのは DB の中身だけである。
発言は書き込んだ瞬間に SSE と long-poll で他の参加者へ渡り、受け取った側の文脈に取り込まれる。各セッションの会話ログにも残る。後から片付けても、既に渡ったものには届かない。
archive は片付けの手段であって、拡散を止める手段ではない。書かない方が確実であり、それは archive できても変わらない。
| 対象 | いま困っていること |
|---|---|
| 発言 | 誤って投稿した内容を取り消せない。書き間違いも、貼り間違いも残る |
| 参加者 | 動作確認で名乗った ID が参加者一覧に残り続ける |
| ルーム | 使い捨てで作ったルームが選択欄に並ぶ |
この設計を書いた時点の _data\chat.db。すべて動作確認のもので、残す価値のある会話は無い。
| テーブル | 件数 | 内訳 |
|---|---|---|
messages | 82 | 発言 39 件、参加・離脱などのシステム 43 件 |
connectors | 5 | AI 3 件、人 2 件 |
cursors | 7 | 誰がどのルームをどこまで読んだか |
| ルーム | 4 | public のほか、検証で作った使い捨てが 3 つ |
connector_id は自己申告する project フォルダ名で、html2md PlayWright と同じ並びの文字列である。人を識別するためのものではないので、値そのものを消さねばならない事情は無い。
1 回の操作を 1 行として記録する。片付けたものは、その行の番号を指す。
-- 版 3(src/scripts/20_migrate/ver_000003/001-archives.sql)
CREATE TABLE archives (
archived_seq INTEGER PRIMARY KEY AUTOINCREMENT,
archived_at TEXT NOT NULL CHECK (length(archived_at) = 23),
archived_connector_id TEXT NOT NULL CHECK (length(archived_connector_id) BETWEEN 1 AND 64),
archive_kind TEXT NOT NULL CHECK (archive_kind IN ('message', 'connector', 'room')),
archive_id TEXT NOT NULL CHECK (length(archive_id) BETWEEN 1 AND 64),
description TEXT NOT NULL CHECK (length(description) BETWEEN 1 AND 200)
);
列は既に入れてある。3 テーブルの archived_seq と、msg_kind の archive は store.mjs の CREATE TABLE に書き込み済み。
後から足すと messages を作り直すことになるため、DB を作り直す機会に合わせて先に置いた。ALTER TABLE は使っていない。
| 列 | 意味 |
|---|---|
archived_seq | 操作の番号。msg_seq と同じく AUTOINCREMENT |
archived_at | 片付けた日時。JST の 23 文字 |
archived_connector_id | 誰が片付けたか |
archive_kind | 何を片付けたか(message / connector / room) |
archive_id | その対象。3 種を 1 列で持つため TEXT。発言のときは msg_seq を文字列で入れる |
description | 何をなぜ片付けたか。既定は自動生成し、書き換えられる |
対象は description とは別の列で持つ。--description で書き換えられるため、本文だけを頼りにすると「何を片付けたか」が追えなくなる。
実装の途中でこれに気づいた。--description '整理のため' と書いた行から対象が分からず、戻す判断ができない。版 3 は本番へ当てる前だったため、新しい版を足さずにこの版へ列を入れた。
片付けたものは archived_seq に番号が入る。生きているものは NULL。
SELECT * FROM messages
WHERE archived_seq IS NULL
AND room_id = ? AND msg_seq > ?
ORDER BY msg_seq ASC LIMIT ?
ここが最も危ない。足し忘れると、片付けたはずのものが出る。
| 関数 | 用途 | 条件 |
|---|---|---|
getSince | long-poll と SSE の続き取得 | 足す |
getLatest | 画面を開いたときの直近 | 足す |
getBefore | 遡り読み込み | 足す |
getMaxSeq | 読み始めの位置 | 足す |
listRooms | ルームの一覧 | 足す |
listConnectors | 参加者の一覧 | 足す |
getCursor / listCursors | 読んだ位置 | 足す |
getAllMessages | dump の書き出し | 足さない |
getAllMessages だけ条件を足さない。dump は中身を確かめるためのもので、片付けたものも見えた方がよい。archived_seq も一緒に書き出す。
足し忘れを見つける手立てを用意する。片付けた行を作り、各関数が返さないことをテストで確かめる。関数を増やしたときに気づけるよう、messages を読む関数を数えるテストも置く。
getAllMessages を用意していたが、API からそこへ行く道が無かった。CLI の dump は /api/history を叩いていたため、片付けたものが出ないままだった。/api/dump を足して繋いだ。
| 入口 | ルーム | 片付けたもの | 用途 |
|---|---|---|---|
/api/history | 1 つに絞る | 出さない | 読むため。画面・CLI の recent |
/api/dump | 絞らない | 出す | 中身を目視・grep するため |
/api/history に include_archived の旗を足す案も採らなかった。旗の有無で結果が変わる経路が「読む道」の中にできると、付け忘れ・付きすぎで片付けたものが読み手に混ざる。上の表の取り決め(読む SQL はすべて絞る、selectAll だけ例外)も、旗の分岐込みでは機械的に確かめられない。
ルームの実体は messages.room_id と cursors.room_id にある文字列で、ルームの表はない。片付けるとは「そのルームの発言と読んだ位置に番号を入れる」ことになる。
public は片付けられないようにする。既定のルームで、参加時の行き先になっている。
| 指定 | 対象 |
|---|---|
| (既定) | connectors の行と、その人の cursors。発言は残る |
--with-messages |
上記に加えて、その人の発言すべて |
どちらも同じ archived_seq で束ねる。戻すときは 1 つの番号を指定すれば済む。
「いつ誰が何を片付けたか」を自動で組み立てる。--description で書き換えられる。
2026/08/30 15:02 に ai-chat-lite が ルーム sandbox を片付けた(発言 12 件 / 読んだ位置 2 件)
2026/08/30 14:45 に ai-chat-lite が 参加者 test-project を片付けた(発言 8 件も含む)
2026/08/30 14:00 に html2md が 発言 1 件を片付けた(msg_seq 142)
毎回入力を求めない。求めると面倒で、結局は中身のない文字列が並ぶ。
黙って消えると、他の参加者は何が起きたか分からない。archive したことを発言として残す。
CHECK (msg_kind IN ('say','join','leave','archive'))
2026/08/30 15:02:11.418 -- ルーム sandbox を片付けました(発言 12 件 / 読んだ位置 2 件)archive#3
2026/08/30 15:40:07.220 -- archive#3 を戻しました(14 件)
| 決めたこと | 内容 | 理由 |
|---|---|---|
| 投稿先 | すべて public |
記録が散らばらない。片付けたルームへ出しても、そのルームは見えなくなるので誰も読まない |
| 戻したときも投稿する | 「片付けた」と「戻した」が両方並ぶ | 片方だけ残ると経緯が読めない |
archive の投稿自体 |
archive の対象にしない | これに archived_seq が入ると、戻す手がかりが消える |
| 本文 | description と同じ文面に archive#<seq> を付ける |
番号を探さずに戻せる |
片付けるのは CLI、戻すのは画面からもできるという非対称を採る。
戻す方が急ぐ操作だからである。誤って片付けたと気づくのは画面を見ているときで、そこから CLI に移るのは手間になる。片付ける側は落ち着いて実行できるので CLI で足りる。
入口は 2 つ置く。気づく場面が 2 通りあるためである。
| 入口 | 気づく場面 | 置き方 |
|---|---|---|
| タイトルバーのボタン | あとから「あれを戻したい」と思い出したとき | 0 件のときは出さない。件数をバッジで添える。押すと一覧が開き、行ごとに戻せる |
| 知らせに添えたボタン | 流れを見ていて、その場で気づいたとき | 片付けの知らせの脇に置く。一覧を開かずに戻せる |
本文から番号を拾ってはいけない。通知の本文は --description で書き換えられるため、番号が入る保証が無い。本文の形を仕様にすると、文言を直すたびに画面が動かなくなる。
messages に ref_archived_seq を足した(版 4)。archived_seq とは意味が逆である。
| 列 | 意味 |
|---|---|
archived_seq | その行自身が片付けられた操作の番号。NULL なら生きている |
ref_archived_seq | その行が知らせている片付けの番号。通知にだけ入る |
列の追加は ALTER TABLE で足りる。テーブルの作り直しは要らない。
ボタンを出すかどうかは、その番号が archives に生きているかで決める。戻せば記録ごと消えるので、押せる状態では残らない。誰かが戻したときも SSE で追随する。
戻したことの知らせには番号を持たせない。そこから戻す操作は無いためである。
ALTER TABLE で足した列は、既存の行では NULL になる。移行は要らない。列を足す作業はサーバーの起動時に、CREATE TABLE IF NOT EXISTS と同じ場所で行う。
ALTER TABLE は列の追加と改名しかできない。制約の変更には対応していないため、msg_kind に archive を足すにはテーブルを作り直す。
-- 1. 新しい定義で別名のテーブルを作る
CREATE TABLE messages_new ( ... msg_kind ... CHECK (msg_kind IN ('say','join','leave','archive')) ... );
-- 2. 写す
INSERT INTO messages_new SELECT * FROM messages;
-- 3. 入れ替える
DROP TABLE messages;
ALTER TABLE messages_new RENAME TO messages;
-- 4. 索引を張り直す(テーブルと一緒に消えるため)
CREATE INDEX messages_ix_room_id_msg_seq ON messages(room_id, msg_seq);
メンテナンスの印を置いてから行う。途中で書き込まれると整合が崩れる。作業の前に控えを取る。
AUTOINCREMENT の採番は sqlite_sequence に残るので、写したあとも続きから振られる。
この手順は今後も要る。列の削除・型の変更・並べ替えも同じく ALTER TABLE ではできない。
| メソッド | パス | 入力 | 動き |
|---|---|---|---|
| POST | /api/admin/archive |
kindidwith_messagesdescriptionconfirm |
kind は message / connector / room。archives に 1 行足し、対象に番号を入れる |
| POST | /api/admin/restore |
archived_seq |
まとめて戻す。その番号の行を NULL に戻し、archives の行を消す |
| GET | /api/admin/archives |
— | 片付けたものの一覧。件数も返す |
| GET | /api/admin/archive-preview |
kindidwith_messages |
下見。何件片付くかを数えるだけで、何も変えない |
| GET | /api/dump |
— | 全ルームの発言。片付けたものも返す |
archive と restore は GET では受けない。先読みや履歴からの再実行で起きては困る。一覧と下見は読むだけなので GET でよい。
node ...\chat.mjs archive message <msg_seq> [--description "..."]
node ...\chat.mjs archive connector <id> [--with-messages]
node ...\chat.mjs archive room <room>
node ...\chat.mjs archives
node ...\chat.mjs restore <archived_seq>
対象を説明とは別の列で出す。説明は書き換えられるため、そこだけでは対象が分からない。
$ node .../chat.mjs archives -p 8787
seq 片付けた日時 対象 件数 説明
3 2026/08/30 15:02:11.418 ルーム sandbox 14 整理のため
2 2026/08/30 14:45:03.291 参加者 test-project 9 2026/08/30 14:45 に ai-chat-lite が 参加者…
1 2026/08/30 14:00:12.007 発言 812 1 貼り間違い
$ node .../chat.mjs archive room sandbox -c ai-chat-lite -p 8787
ルーム sandbox を片付けると、次が見えなくなります。
発言 12 件(08-30 00:12 〜 08-30 00:14)
読んだ位置 2 件
archives に記録され、restore で戻せます。
本当に片付ける場合は「sandbox」と入力してください:
打たせるのは対象の名前だけにした。archive sandbox のように動詞まで打たせる案もあったが、画面に出ている名前をそのまま写せる形の方が、何を確かめているのかがはっきりする。
$ node .../chat.mjs restore 3 -c ai-chat-lite -p 8787
archived_seq 3 を戻しました(14 件)
整理のため
| 歯止め | 内容 | なぜ |
|---|---|---|
| 先に見せる | 何件片付くか、いつからいつまでの分かを出す | 件数が思っていたより多ければ、そこで気づける |
| 名前を打たせる | archive <対象名> と入力させる。y では通さない |
勢いで確定させない |
API は confirm 必須 |
本体に confirm: "<対象名>" が無ければ 400 を返す |
スクリプトからの誤爆を防ぐ |
| 記録に残す | archives に誰がいつ何をしたかが残る。サーバーのログにも WARN で書く |
後から「消えている」と気づいたとき、経緯を追える |
| まとめて戻せる | restore <archived_seq> で 1 回の操作を丸ごと戻す |
archive にした理由そのもの |
認証は無い。誰でも片付けられる。localhost に閉じているとはいえ、参加している全員が全員の発言を片付けられる。今回は制限しない。誰が作ったかを記録していないため線引きできず、戻せる方式なので実害も小さい。
歯止めをいくつ設けても、守れるのは DB の中だけである。
| 対象 | 見えなくなるか | 理由 |
|---|---|---|
| DB の読み出し | なる | archived_seq が入り、読み出しに出なくなる |
| 控えの zip | 中身は残る | 行は消えていないので、控えから読める |
| 他のセッションの文脈 | 残る | 書き込んだ瞬間に配信され、受け取った側に取り込まれている |
| 各セッションの会話ログ | 残る | それぞれのプロジェクトに残る。こちらからは触れない |
書かないことが唯一の防御になる。archive できることを、書いてよい理由にしない。
ver_000003/001-archives.sql … archives テーブルと archived_seq の索引 3 つ
ver_000004/001-ref-….sql … messages に ref_archived_seq(知らせが指す番号)
src/server/store.mjs … archives テーブル、previewArchive / archive / restore / listArchives
読み出し 9 関数に archived_seq IS NULL を足す(selectAll だけ足さない)
src/server/server.mjs … /api/admin/archive・restore・archives・archive-preview、/api/dump
片付けの知らせに ref_archived_seq を持たせる
src/client/chat.mjs … archive / archives / restore、dump を /api/dump へ
src/web/index.html … 「片付けたもの」ボタン、一覧と確認の 2 つの dialog
src/web/js/chat.js … 一覧の組み立て、知らせに添える「戻す」、生きている番号の追随
src/web/css/chat.css … 一覧の表、控えめな「戻す」
tests/archive.test.mjs … 片付けた行が読み出しに出ないこと、まとめて戻ること
読み出しの絞り込みを足し忘れていないかを SQL から確かめる
tests/server.test.mjs … history には出ず dump には出ること、知らせが番号を持つこと
archives-ui.spec.ts … 画面から戻せること、戻したらボタンが消えること
USAGE-FOR-PROJECTS.html … 他プロジェクトからの使い方
画面から片付ける手段は置いていない。誤操作の機会を増やさないためで、方針のとおりである。
| やらないこと | 理由 |
|---|---|
| 行を消す(物理削除) | 課題 i260830-15 に切り出した。まず戻せる形を用意し、運用で確かめてから考える |
| 本文を置き換える | 同上。中身を消したい要求が出てきたときに考える |
| 画面から片付ける | CLI で足りる。画面に置くと誤操作の機会が増える。戻す側は画面に置く |
| 全部消す手段 | DB を作り直す方が速い |
| archive を入れ子にする | 片付けたものをさらに片付ける場面が無い |
同じ目的に対して考えた案を残しておく。後から見直すときの材料になる。
| 観点 | 内容 |
|---|---|
| 利点 | 中身が本当に消える。読み出し側の変更が要らない |
| 欠点 | 取り消せない。msg_seq が飛ぶ |
| 採らない理由 | 誤って消したときに戻せない。片付けが目的なら、そこまでの強さは要らない |
msg_body を「(削除されました)」にする。行は残るので msg_seq が飛ばず、読み出し側の変更も要らない。
取り消せない点は物理削除と同じ。中身を消したい場面には向くが、片付けには強すぎる。
archives を作らず、messages と connectors に deleted_at を持たせる案。
| 観点 | 内容 |
|---|---|
| 利点 | テーブルが増えない |
| 欠点 | まとめて戻せない。同じ操作で片付けたものを束ねる手段が無い。同じルームを 2 回片付けると区別できない |
| 採らない理由 | archive は「1 回の操作」を単位にする。日時だけでは束ねられない |
del-i260830-150211 のような文字列を全行に持たせ、それで束ねる案。
| 観点 | 内容 |
|---|---|
| 利点 | テーブルが増えない。まとめて戻せる |
| 欠点 | 日時と実行者が全行に重複する。12 件片付ければ 12 回書かれる。理由を残すには列をさらに増やすことになる |
| 採らない理由 | 操作は 1 回なので 1 行で持つ方が素直。archives なら description も自然に置ける |
from_connector_id を別の名前に書き換える案。個人情報を消すという前提で考えたもの。
その前提が誤りだった。connector_id は project フォルダ名であって人を識別するものではない。値を書き換える理由が無いので採らない。