Skip to content

feat(history): split the history into topics, resumable from the list #115

Description

@smileygames

目的

履歴をトピック単位に分ける。左のリストはトピックの一覧であり、選べばそのトピックが部屋に戻り、続きを話せる。Claude Desktop の会話一覧と同じ形。

トピックは会話の記録ではなくセッションの器とする。トピックを開き直すと、そこに居た CLI セッションが自分の文脈を持って戻る。

観測

2026-08-27、#48 / #113 で入った履歴パネルを実機で確認したところ、Master の意図と形が違っていた。

入ったのは過去の発言を一本の流れで並べる列である。Master が求めていたのは「トピックで分ける」形であり、一覧から選んで開くものだった。

原因は #48 の未決処理にある。Master が判断したのは「左サイドバーに履歴として表示」「起動時は新規ルーム」の二点で、その粒度 — 一本の流れか、トピックの一覧か — は AI 側が既定で埋めた。埋めた方が外れていた。

前提

いずれも 2026-08-27 に実測して確認済み。

ログは動いているが、トピックの境界を持たない

%APPDATA%\org.liplus-project.pullcept\logs\main.jsonl   2 行 / 686 bytes

一行の欄は message_id / speaker / content / ts の四つ(to は全体宛のため不在)。トピックの境界に当たる欄が無い。

src-tauri/src/room_log.rsroom_log コマンドは引数を取らず、ファイル全体を返す。src/main.tsrenderHistory はそれを一覧に流し込む。どちらもトピックの概念を持たない。

既存の「繋ぎ直し」は死んだセッションの復帰ではない

src/main.ts:477RESUMED_NOTICE は「画面が再読み込みされました。セッションは走ったままで、この端末はそこへ繋ぎ直したものです」と述べる。adoptSeatssrc/main.ts:1680)が行うのは生きている PTY への再接続である。アプリ終了後に文脈ごと戻す機構は現在存在しない。

claude CLI は必要な二つを持つ

-r, --resume [value]      Resume a conversation by session ID
--session-id <uuid>       Use a specific session ID (must be a valid UUID)

--session-id があるため起動時にこちら側で UUID を決めて渡せる。CLI の出力からセッション ID を拾う必要がない。ただしこれは claude の仕様であり、起動コマンドはアカウントごとに設定可能(session.rsparse_launch_options / preview_launch_args)。他の CLI が同じ選択肢を持つ保証は無い。

サイドカーは既に全セッションへ配線済みで、道具を一つしか出していない

crates/mcp-config がアカウントごとに .mcp.json へ room サイドカーを登録する(SERVER_PREFIX = "pullcept-room"、キーはアカウント id 由来)。登録は起動のたびに行われ、session.rs:494register_sidecar がその入口。

サイドカーが公開する道具は say_to_room 一つだけsidecar/src/index.ts:232)。道具を足す先は既にあり、新しい配線は要らない。

部屋は既に pull の形を持っている

say_to_roomlast_seen 引数は「あなたが実際に見た最新の post の meta.message_id」を参加者が申告するもので、それより後に届いた発言があれば投稿は弾かれ、代わりにその発言が返る(room.rsAdmission::Unseen / Missed)。

部屋が「誰が何を聞いたか」を保持するのではなく、参加者が自分の位置を申告して不足分を受け取る形である。#31 / #39 が守っているのはこの向きであり、読み出しの追加はこの向きに沿う限り既存の決定と衝突しない。

決定

Master 判断(2026-08-27)

  1. 区切りは手動。 「新規」を押して切る。起動とは独立しており、一回の起動で複数トピックも、起動をまたいで一トピックの継続もありうる。
  2. 過去トピックは再開できる。 一覧から選ぶとそのトピックが部屋に戻り、続きを話せる。
  3. トピックはセッションの器。 再開時、そこに居た CLI セッションを復帰させる。参加者の文脈は CLI 自身が持って戻る。
  4. 復帰の手段は二段構え。 ネイティブ resume を持つ CLI はそれを使い、持たない CLI にはサイドカー経由の読み出しを与える。履歴をテキストで流し込む力業は採らない。
  5. 起動時は新規トピックを開く。 最後のトピックの続きを自動で開かない。部屋は毎回空から始まり、過去は一覧から選んで開くものとする。feat(room): the room keeps no history #48 の「起動時は新規ルーム」が既定として残る形になる。

決定4の内訳

B. 再開コマンドをアカウント設定に持たせる。
起動コマンド の隣に 再開コマンド を置く(例: claude --resume {session_id})。トピックが {account_id: session_uuid} を保持し、再開時に差し込む。新規トピックでの起動時は --session-id {uuid} 相当でこちらが UUID を決める。未設定なら resume 無しとして C に落ちる。

C. サイドカーに読み出しの道具を足す。
say_to_room の隣に、このトピックの過去発言を返す道具を置く。参加者は必要になったときに自分で引く。起動時に押し込まない。

力業(起動時に履歴をテキストで PTY へ流し込む)を採らない理由:

  • トピックが伸びるほど毎回の起動が高くつく。参加者が要るかどうかに関わらず全部渡すため。
  • CLI は流し込まれた文をユーザ入力として読む。「自分が言ったこと」と「言ったと聞かされたこと」を区別できない。
  • 端末ペインにその塊が見える。あのペインはセッションのものである、というのが RESUMED_NOTICE の設計思想(fix(session): a running session becomes unmanageable after the webview reloads #84)と噛み合わない。

C は力業を push でやる代わりに pull にしたものであり、C があれば力業は要らない。

決定5から導かれること

  • 新規トピックは遅延生成とする(AI 判断)。 起動のたびに索引へ空のトピックを書くと、何も話さずに閉じた回が一覧に残り続ける。起動時の新規トピックは「まだ索引に無い現在のトピック」として保持し、最初の投稿が着いた時点で索引とファイルを作る。空のトピックが一覧に並ぶ形は取らない。

決定3・4から導かれること

AI 判断(Master はこれを一行で覆せる)

  1. ファイルの形 — 一トピック一ファイル(logs/{room}/{topic}.jsonl)+ トピック索引。索引がトピック名・作成時刻・{account_id: session_uuid} を持つ。一ファイル+topic_id 欄の案は、全読みの問題が残るうえセッション識別子の置き場が無いため採らない。perf(history): the panel reads and draws the whole log at launch #114 はこの形で解消する。
  2. 複数参加者の再開 — 復帰できる席は復帰させ、できない席は新規起動として着席する。一部が失敗してもトピックは開く。C があるため、復帰できなかった席も自分で引ける。トピックが開かない形は取らない。
  3. 起動時に開くもの — Master 判断5へ移動(2026-08-27 に差し替え)。
  4. 既存 main.jsonl — ひとつのトピックとしてまとめて移行する。現時点で 2 行のため安い。捨てない。
  5. トピック名 — 最初の発言の冒頭から自動生成し、後から手で変更できる。日時のみの一覧は可読性が低い。

実装時の確定(2026-08-27、実装 AI)

決定を変えるものではなく、決定が形を指定していなかった箇所を実装が埋めたものである。外れていれば一行で覆せる。

  • 決定4B の「--session-id {uuid} 相当」は欄を増やさずに満たした。 同じ {session_id} 置換子を起動オプションへ書く形にしている(claude では --session-id {session_id})。どのフラグが id を運ぶかは CLI ごとの問いであり、再開コマンド が答えているのと同じ問いであるため、アプリはフラグを持たず、書かれた場所へ差し込む。書かなければ id を配らず、そのアカウントは C に落ちる。欄を二つにする案(「新規セッションコマンド」+「再開コマンド」)は、B の literal が一欄であることと、置換子が既にその問いに答えていることから採らなかった。
  • resume か新規かの判定は、アカウントではなくトピックで行う。 「このトピックがこのアカウントのセッションを持つか」と「アカウントが再開コマンドを持つか」の両方が揃ったときだけ resume であり、どちらかが欠ければ新規起動である。記録が在るのに再開コマンドが無い場合は、新規起動側で id を配り直す(記録は上書きされる)——resume できない以上、その id は二度と使われず、トピックが持つべきなのは実際にそこに居るセッションだからである。
  • トピックへ入ると床を空にし、席の since を入口へ戻す。 前のトピックの発言は入るトピックの床ではない。残すと、開き直したトピックで最初に発言した参加者が、前のトピックの保持窓ごと「見落とし」として返される——実際にはその全部を読んでいる参加者に対して、である。席そのものは残す(切り替えは退室ではない)。
  • 索引の実在はディレクトリ側に置いた。 索引に無い .jsonl は読み出し時に拾って項目を建てる。遅延生成(決定5から導かれること)の安全性はこれに乗っている——発言がファイルへ届いて索引へ届く前に落ちても、ファイルだけから建て直せる。索引へ書くのは意図のある行為だけ(最初の発言 / セッション id の記録 / 名前の変更)であり、「アプリが開かれた」は含まない。
  • 読み出しの道具はページを持つ。 既定 50 件、上限 200 件、beforemessage_id を渡すとその手前が返る。トピックに上限が無い以上、一回の tool 結果へ全部を載せる形は採らない。

制約

  • feat(room): the room keeps no history #48 の制約「起動時の部屋は新規とする。過去ログを #room の本文へ復元しない」は、この issue で巻き戻るfeat(room): the room keeps no history #48 のその一行は上書きされたものとして扱う。
  • 保存する形は投稿そのもののまま(feat(room): one post frame, so a participant is a participant #39)。参加者クラスを値域に取る欄を作らない。
  • サイドカーの読み出しは pull のみ。部屋から参加者へ push する経路を作らない。
  • ローカルに留める。GitHub へ流さない。
  • 追記の失敗で発言が失われない、あるいは失敗が観測できること(feat(room): the room keeps no history #48 から継続)。
  • crates/mcp-config は tauri を持たない方針を維持する(テスト可能性のため。session.rs:5 の注記)。サイドカー側の追加もこの方針に従う。

対象ファイル

ファイル 変更
src-tauri/src/room_log.rs トピック単位の追記・読み出し。索引の読み書き。room_log コマンドはトピック指定を取る形へ。
src-tauri/src/room.rs 現在のトピックを部屋の状態に持つ。追記先の切り替え。
src-tauri/src/session.rs 再開コマンドの適用、セッション UUID の払い出しとトピックへの記録。
src-tauri/src/config.rs アカウント設定に 再開コマンド を追加。
src-tauri/src/lib.rs 追加コマンドの登録。
sidecar/src/index.ts 読み出しの道具を TOOLS に追加。ハンドラの分岐。
index.html / src/styles.css 左パネルをトピック一覧へ。新規ボタン、選択状態。
src/main.ts 一覧の描画、トピック切り替え、部屋への読み戻し、名前の編集。
docs/0-requirements.md / README.md 同 PR で更新。

依存: 索引とファイル形式(決定5)が先。サイドカーの道具(C)とアカウント設定(B)は独立に進む。UI は索引が固まってから。

関連

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancement新機能・改善要望ready本文が実装開始できる形まで収束している状態。ただし更新は継続可能

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions