ai-chat-lite アーカイブ機能の設計

参加者・ルーム・発言を、戻せる形で片付けられるようにする

📅 作成: 2026-08-30 / 更新: 2026-09-04

実装済み 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 できても変わらない。

目次

  1. 何を片付けるか
  2. archives テーブル
  3. API と CLI
  4. 誤って消さないために
  5. 作るものと、採らなかった案

何を片付けるか

対象いま困っていること
発言 誤って投稿した内容を取り消せない。書き間違いも、貼り間違いも残る
参加者 動作確認で名乗った ID が参加者一覧に残り続ける
ルーム 使い捨てで作ったルームが選択欄に並ぶ

いまの中身

この設計を書いた時点の _data\chat.dbすべて動作確認のもので、残す価値のある会話は無い。

テーブル件数内訳
messages82 発言 39 件、参加・離脱などのシステム 43 件
connectors5 AI 3 件、人 2 件
cursors7 誰がどのルームをどこまで読んだか
ルーム4 public のほか、検証で作った使い捨てが 3 つ

connector_id は自己申告する project フォルダ名で、html2md PlayWright と同じ並びの文字列である。人を識別するためのものではないので、値そのものを消さねばならない事情は無い。

archives テーブル

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_kindarchivestore.mjsCREATE 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 IS NULL で絞る

片付けたものは archived_seq に番号が入る。生きているものは NULL

SELECT * FROM messages
 WHERE archived_seq IS NULL
   AND room_id = ? AND msg_seq > ?
 ORDER BY msg_seq ASC LIMIT ?

条件を足す対象

ここが最も危ない。足し忘れると、片付けたはずのものが出る。

関数用途条件
getSincelong-poll と SSE の続き取得足す
getLatest画面を開いたときの直近足す
getBefore遡り読み込み足す
getMaxSeq読み始めの位置足す
listRoomsルームの一覧足す
listConnectors参加者の一覧足す
getCursor / listCursors読んだ位置足す
getAllMessagesdump の書き出し足さない

getAllMessages だけ条件を足さない。dump中身を確かめるためのもので、片付けたものも見えた方がよい。archived_seq も一緒に書き出す。

足し忘れを見つける手立てを用意する。片付けた行を作り、各関数が返さないことをテストで確かめる。関数を増やしたときに気づけるよう、messages を読む関数を数えるテストも置く。

dump は入口も分ける

getAllMessages を用意していたが、API からそこへ行く道が無かった。CLI の dump/api/history を叩いていたため、片付けたものが出ないままだった。/api/dump を足して繋いだ。

入口ルーム片付けたもの用途
/api/history1 つに絞る出さない読むため。画面・CLI の recent
/api/dump絞らない出す中身を目視・grep するため

/api/historyinclude_archived の旗を足す案も採らなかった。旗の有無で結果が変わる経路が「読む道」の中にできると、付け忘れ・付きすぎで片付けたものが読み手に混ざる。上の表の取り決め(読む SQL はすべて絞る、selectAll だけ例外)も、旗の分岐込みでは機械的に確かめられない。

ルームは列を持たない

ルームの実体は messages.room_idcursors.room_id にある文字列で、ルームの表はない。片付けるとは「そのルームの発言と読んだ位置に番号を入れる」ことになる。

public は片付けられないようにする。既定のルームで、参加時の行き先になっている。

参加者を片付けるときの範囲

指定対象
(既定) connectors の行と、その人の cursors発言は残る
--with-messages 上記に加えて、その人の発言すべて

どちらも同じ archived_seq で束ねる。戻すときは 1 つの番号を指定すれば済む。

description の既定

「いつ誰が何を片付けたか」を自動で組み立てる。--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)

毎回入力を求めない。求めると面倒で、結局は中身のない文字列が並ぶ。

片付けたことを public に投稿する

黙って消えると、他の参加者は何が起きたか分からない。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 で書き換えられるため、番号が入る保証が無い。本文の形を仕様にすると、文言を直すたびに画面が動かなくなる。

messagesref_archived_seq を足した(版 4)。archived_seq とは意味が逆である。

意味
archived_seqその行自身が片付けられた操作の番号。NULL なら生きている
ref_archived_seqその行が知らせている片付けの番号。通知にだけ入る

列の追加は ALTER TABLE で足りる。テーブルの作り直しは要らない。

戻したらボタンは消える

ボタンを出すかどうかは、その番号が archives に生きているかで決める。戻せば記録ごと消えるので、押せる状態では残らない。誰かが戻したときも SSE で追随する。

戻したことの知らせには番号を持たせない。そこから戻す操作は無いためである。

既にあるデータの扱い

ALTER TABLE で足した列は、既存の行では NULL になる。移行は要らない。列を足す作業はサーバーの起動時に、CREATE TABLE IF NOT EXISTS と同じ場所で行う。

msg_kind の制約はテーブルを作り直して変える

ALTER TABLE列の追加と改名しかできない。制約の変更には対応していないため、msg_kindarchive を足すにはテーブルを作り直す。

-- 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 ではできない。

API と CLI

API

メソッドパス入力動き
POST/api/admin/archive kind
id
with_messages
description
confirm
kindmessage / connector / roomarchives に 1 行足し、対象に番号を入れる
POST/api/admin/restore archived_seq まとめて戻す。その番号の行を NULL に戻し、archives の行を消す
GET/api/admin/archives 片付けたものの一覧。件数も返す
GET/api/admin/archive-preview kind
id
with_messages
下見。何件片付くかを数えるだけで、何も変えない
GET/api/dump 全ルームの発言。片付けたものも返す

archive と restore は GET では受けない。先読みや履歴からの再実行で起きては困る。一覧と下見は読むだけなので GET でよい。

CLI

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 に閉じているとはいえ、参加している全員が全員の発言を片付けられる。今回は制限しない。誰が作ったかを記録していないため線引きできず、戻せる方式なので実害も小さい。

archive では守れないもの

歯止めをいくつ設けても、守れるのは 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 が飛ばず、読み出し側の変更も要らない。

取り消せない点は物理削除と同じ。中身を消したい場面には向くが、片付けには強すぎる。

deleted_at を各テーブルに持つ

archives を作らず、messagesconnectorsdeleted_at を持たせる案。

観点内容
利点テーブルが増えない
欠点 まとめて戻せない。同じ操作で片付けたものを束ねる手段が無い。同じルームを 2 回片付けると区別できない
採らない理由archive は「1 回の操作」を単位にする。日時だけでは束ねられない

delete_id を文字列で持つ

del-i260830-150211 のような文字列を全行に持たせ、それで束ねる案。

観点内容
利点テーブルが増えない。まとめて戻せる
欠点 日時と実行者が全行に重複する。12 件片付ければ 12 回書かれる。理由を残すには列をさらに増やすことになる
採らない理由操作は 1 回なので 1 行で持つ方が素直。archives なら description も自然に置ける

参加者を片付けたら発言の名前を付け替える

from_connector_id を別の名前に書き換える案。個人情報を消すという前提で考えたもの。

その前提が誤りだった。connector_id は project フォルダ名であって人を識別するものではない。値を書き換える理由が無いので採らない。