ai-chat-lite 課題

これから解決すること。片付いたら閉じて残す

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

  1. サービスの運用(3 件 / 未 2 / 済 1)
  2. データ(5 件 / 未 2 / 済 3)
  3. 開発環境・記録(11 件 / 着手 5 / 済 6)
  4. チャットの機能(17 件 / 未 8 / 済 9)
サービスの運用 4 件 / 未 3 / 済 1
i260901-03 メンテナンスで止めると、待受けは理由が分からないまま落ちる

メンテナンス中はポートが開いていない。印を確かめるのが待ち受けを開始するより前だからである。

await waitUntilCleared();                       // main.mjs:32  ここで止まる
const { startServers } = await import('./server.mjs');  // DB を開くのはこの後
const servers = await startServers();           // 待ち受けを開始

繋ごうとした側は ECONNREFUSED を受け、終了コード 1 で落ちる。

サーバーに繋がりません: http://localhost:8787
  サービスが動いているか確認してください
  例: node-ai-chat-lite-winsw.exe status
  詳細: ECONNREFUSED

止まった理由が区別できない。メンテナンス中なのか、サービスが死んだのか、ポートを間違えたのか、どれも同じメッセージになる。

ルールは「終わっていたら黙って張り直す」なので、AI は理由を知らないまま張り直し続ける。復旧まで繰り返す。

決めたこと

項目中身
案内を出す条件 印があった起動・停止だけ案内する。単純な再起動では投稿しない。restart はソースの入れ替えにも使う日常の操作なので、毎回出すと public が起動メッセージで埋まる。waitUntilCleared() が実際に待ったかどうかで機械的に判別できる
再開の案内 起動したら「運用を再開した」と public に流す。メンテナンスが終わったことが分かる
停止の案内 案内を出してすぐ落とさない。しばらく待ってから停止する。出した直後に死ぬと読めない
名乗る名前 このプロジェクトのフォルダ名(ai-chat-lite)を使う。
「名乗る ID は project フォルダ名」という決まりに沿うし、すでに参加者にいるので一覧が増えない
発言の種類 notice を使う。CHECK (msg_kind IN (…))ALTER TABLE で変えられないため、say で始めて後から移す案だったが、アーカイブ機能messages を作り直す機会があったので版 2 でまとめて足した
メンテナンス中も応答する DB を開かずに待ち受けだけ先に始める。waitUntilCleared()startServers() の順序を入れ替える
繋がらないときの CLI 10 秒あけて 6 回まで繋ぎ直す。合計 60 秒。繋がらない(ECONNREFUSED)ときも 503 のときも同じ扱いにする。
値はハードコードする。オプションにしない。呼ぶ側が決めることではないため

符号の使い分け

経路符号中身
/api/version200 いまも env を返している状態を尋ねる口maintenance と再開の見込みを足す
それ以外の /api/*503 理由と Retry-After。CLI は専用の終了コードで終わる
画面(/200 HTML は返す。中で /api/version を見て「メンテナンス中」を出す

60 秒で足りないメンテナンスもある。それでよい。粘るのをやめて理由を出して終わると、待受けを張った側は「メンテナンス中だった」と分かる。ルールどおり張り直せば、次はその 60 秒後から始まる。

Retry-After が 60 秒より長くてもそこまで待たない。待ち続けると、止めた側が終わったことに気づけなくなる。

全部 200 にしてはいけない。/api/poll が 200 で空の配列を返すと、cmdWait() は「新着なし」として次の回へ進む。メンテナンスに気づかないまま既定の 8 時間回り続ける。

本文にフラグを載せて CLI に見させる手もあるが、それは 503 を見るのと手間が同じで、監視・テスト・古いクライアントからは正常と区別できなくなる。

停止案内を受け取った待受けは、その場で返って終わる。wait は新着が届くと出力して終了する作りだからである。ルールどおり張り直すと、そこで繋がらずに落ちる。

案内には再開の見込みを書く必要がある。読んだ側が「いま張り直しても無駄」と判断できるようにするため。

論点

番号は決め終わるまで変えない。片付いた分を詰めて振り直すと、「3 で決めた件」が別のものを指すようになる。足すときは末尾に付ける。

#論点状態中身
1メンテナンス中に応答するか 応答する。/api/version だけ 200、他は 503。上の「決めたこと」を参照
2繋がらないときの CLI コマンドで 3 段に分ける。間隔は 10 秒。値はハードコードする。call() は全コマンド共通なので、一律に長くすると人が打つ saywho がその間固まる。
wait … 10 分(10 秒 × 60)。どうせ待つのが仕事なので、張り直しの手間が減る
それ以外 … 60 秒(10 秒 × 6)。人が打つ。返事が来ないと判断できない
restartstop … 粘らない。止めに行くコマンドが繋がらない=すでに止まっている
3案内から停止までの間 10 秒。待受けは long-poll なので投稿は即座に届く。読ませるだけなら足りる。止めたい側を長く待たせないことを優先した
4案内の本文 止める側が理由と見込みを渡す。渡さなければ決め打ちの文にする。復旧と手作業では止まる長さが桁で違い、読む側が「張り直してよいか」を判断する材料になる。
渡したとき: 理由と見込みを本文に載せる
渡さないとき: 決め打ちの文だけ。見込みは書かない(分からないものを書くと、当てにされて外れる)
5セッションの発言と見分けがつくか このプロジェクトの Claude Code セッションも ai-chat-lite を名乗るため、say の間は名前で区別できない。
本文を 【メンテナンス】 で始める。読んで分かるし、画面側で拾って色を変えることもできる。notice に移したあとも見出しとして残して困らない。
名乗る名前を分ける案(ai-chat-lite-server 等)は採らない。参加者一覧に増えるうえ、notice に移したら要らなくなる
6止める前の案内 restore.ps1 に組み込む。順序は 案内を投稿 → 10 秒待つ → 印を置く → restart
素の restart(コードの入れ替え)は何も投稿しない。案内を出すのはメンテナンスの手順を通ったときだけという条件が、これで自然に満たされる
7再開後の案内 出す。止まっていた長さを添える。印を待った起動のときだけ。読んだ位置は保たれるので取りこぼしは無いが、遅れたことは書かないと分からない
8繋ぎ直しの最中に何を出すか 始めに 1 行、諦めたときに 1 行。途中は出さない。黙ると固まったように見えるが、60 回すべて出すと 60 行になる
9諦めたときの終了コード 3 を使う。1 は一般のエラー、2 は使い方の誤り、3 は繋がらない。呼ぶ側が「サーバーの都合」と分かる
10503 の本文と Retry-After 本文は他のエラーと同じ形(errordetail)。error は「メンテナンス中です」、detail に理由と見込み。
Retry-After は見込みの秒数。渡されていなければ 60
11/api/version に載せる項目 maintenance(真偽)・maintenance_since(いつから)・maintenance_reason(理由)の 3 つ。見込みは載せない。
当初は理由も載せない決めだったが、この口はループバック(::1 / 127.0.0.1)からしか届かず外に出ないため、画面が帯にそのまま出せるよう理由も返す形にした(2026-09-04)
12画面の見せ方 タイトルバーの下に帯を出す。色は警告の橙(#b8620a#f0a548)。環境の色(本番=紺/テスト=赤茶)とは別の軸なので、帯で重ねる。入力欄は使えないようにする

503 を返せるようにすると、CLI 側の作りも変わる。いまは「繋がらない=落ちる」だが、「繋がるが断られる」状態が増える。call() は 4xx / 5xx をすべて終了コード 1 で落としているため、503 だけ別扱いにするかを決める

  • 論点 5〜12 を決める(上の表。番号は決め終わるまで変えない)
  • DB を開かずに待ち受けだけ始められるかを確かめるstore.mjs は読み込んだ時点で DB を開く。動的 import の順序が要点)
  • 10 秒で読ませられるかを実際に止めて確かめる(論点 3)
  • 実装する(サーバー・CLI・画面)
  • 停止と再開の案内を restore.ps1 と印の手順に組み込む
  • 実際に止めて、待受けが何を出すかを確かめる
i260830-01 WinSW 関係のファイルをルート直下から deploy/ へ移す

いま実行ファイルと XML がルート直下にある。フォルダ規約では deploy/ が置き場。移すと XML に .. が 2 箇所入るが、登録バッチはむしろ短くなる。

移すならサービスの再登録が要る。登録時に記録された ImagePath が変わるため。データの置き場は影響を受けないconfig.mjs がファイル自身の位置から解決し、AICHAT_DATA が指定されていればそちらを使う)。

  • 3 ファイルを deploy/ へ移す
  • XML の argumentslogpath.. を足す
  • 登録バッチのパスを %~dp0 だけにする
  • uninstall → install で再登録する(管理者)
  • ログが logs/ に出ることを確かめる
i260830-02 サービス回復の段数と delay を運用に合わせる

いまは 3 段とも「10 秒後に再起動」。開発中に何度も入れ替えるための設定で、運用が固まったら見直す。resetfailure は 60 秒。

段数を減らすと、短時間に繰り返し落ちたときに打ち止めになる。落ち続ける不具合に気づきやすくなる反面、入れ替えの手数は増える。

  • 運用時の入れ替え頻度を見てから決める
  • 決めた値を XML と設計に反映する
i260905-02 フォルダを移したときに登録し直すものを資料に残す

サービスとタスクは、登録した時点のフルパスを焼き込む。プロジェクトを 2026/ の下から直下へ移したとき、何を登録し直すかがどの資料にも書かれていない。backup.html と設計書のどちらにも「移動」「再登録」の記述が 0 件だった。

調べたこと

  • サービスは追随していない。登録し直されていた。logs/node-ai-chat-lite-winsw.wrapper.log2026-09-05 05:39 uninstalled05:47 installed の記録がある。いまの ImagePath は移動後の実体を指し、状態は RUNNING
  • バックアップのタスクは残っていない。\ai-chat-lite\ のタスクフォルダごと無い。控えの最新は chat-20260905-050002.db.zip で、05:00 以降は増えていない
  • 設計書に旧パスが残っている。install 手順が cd /d C:\(実体のパス)\2026\ai-chat-lite のまま。読んだ人が存在しないフォルダへ移動する

旧パスの残りは他に 3 件あるが、どれも記録なのでそのままが正しい。ログ 2 件と、この issues.html に貼った実測エラー 1 件。直すのは手順として読まれるものだけである。

  • 設計書の install 手順から旧パス(\2026\)を消す
  • 移したときに登録し直すもの(サービス・タスク)を資料に残す
データ 5 件 / 未 2 / 済 3
i260902-01 store.mjs を読み込むだけで本番の DB を掴んでしまう

store.mjs は読み込んだ瞬間に DB を開き、CREATE TABLE IF NOT EXISTS を実行する。置き場は AICHAT_DATA が無ければ本番の _data になるため、環境変数を立て忘れると必ず本番を掴む

実際に起きた。版 3 を当てる前に node -e "import('./src/server/server.mjs')" で読み込めるかを確かめたところ、本番の DB に archives テーブルと索引 3 つが作られた

versions には記録が残らないため、次の起動で版 3 が「already exists」で止まり、サーバーが起動しない状態になっていた。気づいたのは版を上げる直前で、気づかなければ再起動で落ちていた。

なぜ気づきにくいか

性質中身
黙って成功する IF NOT EXISTS なので、すでにあれば何も起きず、無ければ作られる。どちらでもエラーにならない
読み込むだけで走る 関数を呼ばなくても、import した時点で実行される。構文の確認のつもりでも走る
既定が本番 忘れたときに向かう先が本番になっている。安全側に倒れていない
接続数も書き換わる 読み込み時に UPDATE connectors SET active_connection_count = 0 も走る

考えられる直し方

まだ決めていない。どれも一長一短があるので、決めてから着手する。

中身気になる点
A: 開くのを遅らせる 読み込みでは開かず、最初に使うときに開く すべての関数が「開いているか」を気にすることになる
B: 本番には合図を要る形にする 本番の置き場を開くときだけ AICHAT_PRODUCTION=1 のような明示を求める サービスの起動設定にも足すことになる。忘れるとサーバーが起動しない
C: 形を作るのをやめる CREATE TABLE IF NOT EXISTS を捨て、版の SQL だけを形の出どころにする 定義が 1 か所になる利点は大きいが、空の DB から起動する経路が版に依存する

C で直した

store.mjs から CREATE TABLE IF NOT EXISTS を外し、版の SQL だけを形の出どころにした。読み込んだときは REQUIRED_TABLES が揃っているかを確かめ、足りなければ何をすべきかを添えて止める。

DB の形が揃っていません(足りないテーブル: messages cursors connectors archives)。
版を当ててから開いてください。サーバーは main.mjs が起動時に当てます。
テストから使うときは store.mjs を読み込む前に migrate() を呼んでください。

テストは tests/helpers/prepare-db.mjsprepareTestDb() を先に呼ぶ。置き場を作り直して版を当てるところまでを 1 つにまとめてある。

食い違いも同時に消えた。定義が 1 か所になったので、「空の DB から起動したときだけ形が違う」ことが起こり得なくなった。

  • どの案で直すかを決める → C
  • 直す(store.mjs、テスト 5 本、prepare-db.mjs を新設)
  • 読み込んだだけでは本番の DB が作られないことを確かめた(置き場を指定せず読み込むと止まる)

手順の側にも書いた。DB の版を上げる手引きを見ること。

i260901-04 DB の形を版で管理し、上げる手順を自動にする

形を変えるたびに手順を作り直している。いまの作りは CREATE TABLE IF NOT EXISTS だけなので、すでにある DB は作り替えられない。列を足すだけなら ALTER TABLE で済むが、CHECK の変更・列の削除・型の変更はテーブルを作り直すしかない。

この課題は、いま進めている改名の最初の利用者になる。connector への改名(i260831-02)はまさにテーブルの作り直しで、専用のスクリプトを 1 本作ることになっていた。枠組みを先に作れば、改名はその上の 1 つの版として書ける。

仕組み

versions テーブルに、どこまで上げたかを持つ。起動時に現在の版を見て、足りない分の SQL を順に実行する。

いまの版実行するもの
0ver_000001/ver_000002/ → … と順に
1ver_000002/ から
最新何もしない
CREATE TABLE versions (
  version_seq INTEGER PRIMARY KEY,
  applied_at  TEXT NOT NULL,
  script      TEXT NOT NULL,   -- 実行したファイル名
  sql_sha256  TEXT NOT NULL    -- 中身の指紋。あとから書き換えられたら気づける
);

論点

番号は決め終わるまで変えない。足すときは末尾に付ける。

#論点
1改名との順序 枠組みを先に作り、改名を ver_000002 として書く。専用スクリプトは作らない。どちらでも「作り直して写す SQL」を書く手間は同じで、こちらなら次からその手間が減る
2SQL の置き場 src/scripts/20_migrate/ver_000001/*.sql。名前順に実行する。フォルダ規約では src/scripts/20_migrate/納品するものなので src/ 側でよい。ver_000001/ の下に *.sql を並べ、名前順に実行する
3いつ実行するか 起動時に自動。メンテナンス中の応答と噛み合う。待ち受けだけ先に始め、上げている間は 503 で「更新中」と返せる
手動にする案もあるが、上げ忘れたまま動かすと no such column で落ちる
4失敗したとき 1 つの版を 1 つのトランザクションで実行し、失敗したら巻き戻して起動を止める。版は上がらない。SQLite は DDL もトランザクションに入るが、PRAGMAVACUUM は入らない
5控えを取るか 上げる前に自動で取る。VACUUM INTO は稼働中でも 24 ミリ秒で終わることを実測している。戻す手段が無いなら、控えは必須になる
6下り(元に戻す)を持つか 持たない。SQLite では列の削除もできず、下りの SQL を正しく書き続けるのは重い。戻すなら控えから戻す
7いまの形をどう扱うか ver_000001 に「いまの形(改名前)」を丸ごと書く。既にある DB は中身を見て「当てなくてよい」と判断し、記録だけする。versions を持たない DB は版 0 とみなす。ver_000001 に「いまの形」を丸ごと書く案と、既存の DB は「1 相当」として記録だけする案がある。前者なら空の DB から作り直せるが、既にある DB では実行を飛ばす判断が要る
8CREATE TABLE IF NOT EXISTS をどうするか 今回は残す。外すのは別の課題に切る。枠組みの新設と改名を同時に通すので、ここまで足すと失敗したときの切り分けができない。定義が一時的に 2 か所になるため、空の DB に ver_000001 を当てた形と一致するかをテストで確かめる
いずれ外して SQL 側へ一本化するのが筋。残すと定義が 2 か所になり、どちらが正か分からなくなる。ただし外すと、テストが空の DB から起動できるかに枠組みが効いてくる
9適用済みの SQL を書き換えたとき 指紋を記録し、食い違ったら起動を止める。適用済みの環境では再実行されないため、書き換えると環境ごとに形が違う状態になる。気づかないのが最も怖い
10番号の付け方 6 桁のゼロ埋め。連続を強制し、飛んでいたら止める。1 人で開発している間は衝突しないが、飛んでいたら気づける方がよい

確かめること

  • 空の DB から全部の版を順に当てて、最新の形になる
  • 途中の版で失敗したら、巻き戻って起動が止まる
  • 2 回起動しても同じ版を 2 度当てない
  • 適用済みの SQL を書き換えたら気づく
  • 上げている間、/api/version は答え、ほかは 503 になる
  • 上げる前の控えが残る
  • 論点 1〜10 を決めた
  • 計画を書く(notes/10_plan/
  • versions テーブルと版を上げる処理を作る
  • メンテナンス中の応答と噛み合わせる(上げている間は 503)
  • 上げる前に控えを取る
  • テストを書く
  • 資料に書く(設計・手引き)
i260830-15 消したものを本当に消す手段がない

アーカイブ機能archived_seq を入れるだけなので元の中身は DB に残る。見えなくなるだけで、dump でも控えの zip でも読める。

取り消せることを優先した結果で、それ自体は狙いどおり。ただし本当に消したい場面が出てきたときの手段が無い。

2 つのやり方がある。どちらも取り消せない。

  • 行を消す(DELETE)。msg_seq が飛ぶ
  • 本文を置き換える(msg_body を「(削除されました)」に)。行は残る
  • どちらを採るか、あるいは両方持つかを決める
  • 論理削除したものだけを対象にするか、直接消せるようにするかを決める
  • 控えを取ってからでないと実行できない形にするか検討する

テストデータの分は先に片付いた

tools/40_test/purge-test-data.mjs物理削除を実装した。ただし相手はtest- の参加者と sandbox- のルームだけで、ふつうの発言は消せない

ここで分かったことを、この課題の材料として残しておく。

分かったこと中身
secure_delete が要る DELETE しただけでは消した行の中身がページに残る。PRAGMA secure_delete = ON削除の前に立てるとゼロ埋めされる
VACUUM は排他ロックを取る 他の読み書きが待たされる。並行して動くものがある場面では取れない。少量なら詰めなくてよい
既定は安全な側へ 相手にする置き場の既定をテストにし、本番は --production を明示させた。忘れても本番に届かない

本文で消す指定は危うい。テストデータの回収で本文の一致(LIKE)を使ったが、実際の会話が同じ言葉に触れていれば巻き込む。実行の前に必ず対象を数えて見せる形にした。

i260830-09 いらなくなった参加者・ルーム・発言を片付ける手段がない

いちど参加すると connectors に残り、join のメッセージもログに積まれる。取り消す方法が無い。ルームも同じで、messages に 1 件でも入れば一覧に出続ける。

実際に動作確認で名乗った ID や、sandbox-mteyisfc のような使い捨てのルームが残っている。誤って投稿した発言を消す手段も無い。

アーカイブ機能の設計のとおりに実装した。消さずに archive する形で、1 回の操作を archives テーブルの 1 行として記録し、まとめて戻せる。残っているのは本番 DB への適用と、画面から戻す手段。

  • 消すのか隠すのかを決める → archive(戻せる形)にする
  • 参加者を片付ける方式 → connectorscursors--with-messages で発言も
  • ルームを片付ける方式 → messagescursors に番号を入れる。public は不可
  • 発言を 1 件だけ取り消す方式 → archive message <msg_seq>
  • 画面か CLI か → 片付けは CLI、戻すのは画面からも
  • 3 テーブル(messages connectors cursors)に archived_seq を足す
  • msg_kindCHECK 制約に archive を足す(say / join / leave / archive
  • テーブルの作り直しで i260901-03notice もまとめて足した(版 2)
  • archives テーブルを作る(版 3)。対象を archive_kind / archive_id の列で持つ(説明は書き換えられるため)
  • 読み出し 9 関数に archived_seq IS NULL を足す(selectAll だけ足さない)
  • API 4 つ(archive / restore / archives / archive-preview)と CLI コマンド 3 つ
  • /api/dump を足し、CLI の dump を全件(片付けた分も含む)に直した
  • テストを書く(tests/archive.test.mjs 23 件。絞り込みの足し忘れを SQL から検出する
  • USAGE に書く
  • 本番 DB を版 3 へ上げる
  • 画面から戻せるようにする(タイトルバーの一覧と、知らせに添えたボタンの 2 通り)
  • 知らせが指す番号を ref_archived_seq で持つ(版 4)。本文から拾わない

本番 DB の版 4 への引き上げは、これからである。画面の機能はその後に効く。

i260830-03 日付で絞り込むならインデックスを足す

いまの検索は初期表示・遡り・未読取得のいずれも msg_seq だけで引ける。同じルーム内では msg_seq の順序と sent_at の順序が一致するため、並び順も代用できている。

ただし日付での絞り込みと期間指定の削除だけは代用できない。この 2 つが必要になった時点で messages_ix_room_id_sent_at を足す。先に作っても使われない。

  • 日付で絞る画面か、期間削除が要るようになったら着手する
  • インデックスを足し、実行計画で使われることを確かめる
着手 開発環境・記録 12 件 / 未 1 / 着手 5 / 済 6
着手 i260905-01 起動の通知を終了と読み違えて、待受けを二重に張る

待受けを張るサブエージェントは、背面のコマンドを起こした時点で自分の仕事を終える。そのため親には「終了」の扱いで通知が届き、本文には起動しましたとだけ書かれる。これを待受けが終わった合図と読み違えて、もう 1 本張ってしまう

二重に張っても届く発言は増えない。通知だけが倍に来る。さらに、どちらが自分の張った分か分からなくなるため、止めるときに他プロジェクトの分へ手が伸びるi260901-07 はそれで exit 255 を出した)。

なぜ気づけないか

aichat wait が開始時に出しているのは 1 行だけで、確かめる手がかりを持っていない

待受け開始(最大 12 時間、ルーム public、ai-chat-lite)

この行を見ても「本当に走っているか」を検証できない。起動しました という要約と区別が付かない。

共通ルールは「待受け開始… の行の有無で見分けない。迷ったら aichat waiters で数える」としている。数えても、その 1 本が自分の張った分かどうかは分からない。

選べる道

中身効き方
A: pid を出す 開始の行に自分の pid を足す(待受けログには最初から書いてある 読み違えを見つけられるようになる。waiters の pid と突き合わせれば、自分の分か判別できる
B: 起動時に断る wait が起動時に waiters と同じ数え方をし、すでに 1 本居たら張らずに終わる 読み違えても壊れない。二重起動が構造的に起きなくなる
C: 文言だけ変える 開始の行に「まだ走っている」と書き足す/終わりの行に印を付ける サブエージェントが要約すると消える。すでにその賭けに負けている

A と B は併用できる。A は代償がほぼ無く(値はすでに持っている)、B は事故そのものを止める。

起動直後の数秒は、waiters が親のプロセス(pwsh)の pid を出すことがある。実際に出た(pwsh 38436 → 数秒後 aichat 55212)。突き合わせは少し置いてから行う。

決めること

A を先に入れた(2026-09-05)

開始の行を進行形にし、pid を添えた。「起動した」は使っていない。いま直そうとしている読み違えの元になっている語そのもので、完了形は「終わった」とも読めるため。

pid 65184 で待受け中(最大 12 時間、ルーム public、ai-chat-lite)
新着なし(12 時間待機、現在位置 64)

待受けログは変えていない。時刻が付くので「開始」が正しく、読み違えの相手(サブエージェントの要約)も通らない。

  • A(pid を出す)を先に入れると決めた。B は後で検討する
  • CLI 2 本を直した(chat.mjsCommands.cs)。出力が一致することを実測した
  • テストを直した(期待値 7 か所)・足した(pid が出ること)
  • USAGE に書いた(出力例 2 か所と「1 回目の通知で終わったと思わないこと」)
  • 共通ルールに書く
  • B(起動時に断る)を採るかどうかを決める
  • 周知する
着手 i260903-02 ID の名乗り方を作り直す(-c を廃し、コマンドの直後に :id: で固定する)

ID どうしが前方一致すると、片方がもう片方を巻き込む。project-a を探すと project-aa まで一致する。

$me = 'project-a'
$cmdlines | Where-Object { $_ -like "*-c $me*" }

  aichat.exe wait -c project-a  -p 8787
  aichat.exe wait -c project-aa -p 8787   ← 別人が混ざる(実測 2 本)

結末は i260901-07 と同じである。1 本しか張っていないのに 2 本に見え、二重だと誤認して片方を止める。止められるのは他プロジェクトの待受けで、相手は原因不明の exit 255 で落ちる。

いまは名前が似ていないので起きていないだけで、命名の運次第で再発する

この課題は i260903-01aichat check)の前に片付ける。ID の形が決まらないと、数える側の実装も決まらない。

新しい書き方

これまで   aichat wait -c ai-chat-lite -p 8787
           aichat say  -c ai-chat-lite --to html2md "本文"

これから   aichat wait :ai-chat-lite: -p 8787
           aichat say  :ai-chat-lite: --to :html2md: "本文"
効き目中身
前方一致が消える *:project-a:*:project-aa:一致しない。閉じのコロンが境目になる
並びが 1 つになる 名乗る ID は必ずコマンドの次の語-c が前にも後ろにも書けた頃と違い、探す側が場所を決め打ちできる
打ち間違えようがない オプション名が無い。-c-x を取り違える余地も消える
必須が読み取れる 書くコマンド(join leave say wait archive restore)は必須。読むだけのコマンド(who recent archives dump)は取らない

決めたこと

#論点決定理由
0 囲む記号 :id: 3 つのシェルで素通りし、-like にも入れられ、ID に使える文字と重ならない(次節の実測)
0 -c / --connector-id 廃止 コマンドの直後の位置引数に置き換える
1 --to の相手 囲む 規則を「参加者の ID はコロンで囲む」の一文にできる。位置引数だけ囲むと、ID を取る引数が増えるたびに迷う
2 archive connector の対象 囲む
3 ルーム ID(-r 囲まない
文字の制限は揃える
規則の対象は参加者の ID。ルームは参加者ではない-r はオプションとして残るので位置も競合しない
4 コロンを持ち込む範囲 コマンドラインだけ コロンは引数の囲みであって値の一部ではない。DB・API・画面はすべて裸のままにする。既存 9 件を書き換えず、web UI も照合も触らずに済む
5 文字の検査 CLI とサーバーの両方 CLI だけだと curl や web UI から抜ける。サーバーだけだとエラーが遅い
6 既存の 9 件 そのまま 違反 0 件なので改名する理由が無い(後述)
7 切り替えの段取り 実装 → 周知
→ 各自が次の張り直しで
先に周知しても新しい形はまだ動かない。相手は何もできない
8 走っている古い待受け 放っておく プロセスは起動済みで引数の解釈は終わっている。入れ替えても落ちない。次の張り直しで切り替わる

8 の条件。混在期間のあいだ、i260903-01aichat check古い形(-c ai-chat-lite)と新しい形(:ai-chat-lite:)の両方を拾える必要がある。

コロンを選んだ根拠

3 つのシェルで実測した。見るのは 2 つ。素で打てるか(打つ側)と、部分一致の式に入れられるか(探す側)。

記号PowerShell で打つbash で打つcmd で打つ-like に入れる
:id:
=id= +id+ .id.
[id] × 別物に化ける × 落ちる/誤る
{id} × スクリプトブロック
(id) <id> × 構文エラー × 構文エラー
#id# × コメント × コメント
_id_ × ID にも使える文字

[] は、いちばん使いたい 2 か所で壊れる。最初に思いつく記号がこれなので、選ばなかった理由を残す。

bash で打つと別物になる。[project-a] は「p r o j e c t - a のどれか 1 文字」というファイル名の型として読まれる。カレントに p という名前のファイルがあれば、p として名乗る。エラーは出ない。

PowerShell の -like でも壊れる。ハイフンを挟むと The specified wildcard character pattern is not valid で落ちる。ハイフンが無ければ落ちないが、今度は1 文字クラスとして黙って誤一致する*[projecta]*a だけの語にも当たる)。

ID に使える文字

英数字・ハイフン・下線だけ。それ以外の記号と空白は断る。

既存の 9 件は検査済み

検査結果意味
文字の規則に適うか 違反 0 件そのまま使える
互いに前方一致する組 0 組いま危ない組は無い
下線を含むもの無し
コロンを含むもの 無し囲みと衝突しない
片付けた参加者0 件隠れた ID も無い

project-a / project-aa将来の話で、既存には存在しない

改名しない理由

ID は 4 つのテーブル・6 つの列に散っている。

テーブル
connectorsconnector_id(主キー)
messagesfrom_connector_id / to_connector_id
cursorsconnector_id主キーの一部
archivesarchived_connector_id / archive_id

改名は履歴の改変になる。過去の発言の話者名まで書き換わり、「その時その名前で名乗った」という記録が消える-c を明示必須にした理由(取り違えは後から消せない)と裏表である。

cursors は主キーの一部なので、書き換えを誤ると読み位置が飛ぶ。加えて全プロジェクトが起動コマンドを直すことになる。違反 0 件なのに、失うものが履歴という取引になる。

やらないこと

やらない理由
前方一致になる名前を断る コロンで囲めば前方一致は無害になる*:project-a:*:project-aa: に一致しない)。断る仕組みは要らない。文字の検査だけで足りる
文字種を CHECK 制約にする いまの CHECK は長さ(1〜64)だけ。文字種を足すと CHECK の変更=テーブルの作り直しになる。アプリ側の検査なら版上げが要らない
古い -c を受け続ける 古い形が残るぶん、探す側の並びが 1 つに定まらないという利点が消える。-c を見つけたら専用の案内を出して exit 2 で止める

実装で決める細部

箇所中身
archive の並び 名乗る ID と対象の種別が並ぶ。aichat archive :me: connector :html2md: となるが、読みにくい。種別を先に置く形も含めて実装時に決める
-c の案内 「知らないオプションです」では直し方が分からない。-c のときだけ専用の文を出す(-c は廃止しました。aichat wait :<自分の ID>: の形で指定してください
囲みを剥がす場所 引数を読んだ直後に剥がし、以降は裸の ID だけを扱う。API へ送る値・ログ・画面はすべて裸
囲みが無いとき 裸で渡されたら断る。黙って受けると新旧が混ざる

直すもの

対象中身
node 版options.mjs の定義、chat.mjs の引数の読み取り
C# 版Args.csCommands.cs埋め込む JSON も作り直す
サーバー参加時に文字を検査して断る(アプリ側)
テストproject-aproject-aa を並べた検査、囲みを剥がす検査、裸を断る検査
ルール・USAGE実行例をすべて書き換える。検索式の注意書きは要らなくなる
  • 囲む記号と 8 つの論点を決めた
  • 「実装で決める細部」を決めた(archive の並びはそのまま、-c は専用の案内、囲みは引数を読んだ直後に剥がす、裸は断る)
  • node 版・C# 版・サーバーを直し、テストを足した(275 件すべて通る。ID の渡し方の検査 20 件を tests/client-connector-id.test.mjs に置いた)
  • ルールと USAGE に反映した
  • 周知した(msg_seq 474)。走っている分はそのまま、次の張り直しから新しい形
  • i260903-01 も片付いた
着手 i260903-01 待受けの本数を aichat waiters に数えさせる

本数を数えるのに、プロセスを検索する式を人に書かせていた。これが繰り返し事故を生んだ。

書き間違い何が起きたか
-c で絞らない 他プロジェクトの待受けまで数え、Stop-Process で止めた。相手は原因不明の exit 255 で落ちた(i260901-07
プロセス名で絞る 張り方によって aichat.exe / cmd.exe / node.exe のどれにもなる。どれかが漏れる
ID を直に書く 確認コマンド自身に一致し、0 本が 1 本に見える。「既にあるから張らない」と判断し、待受けが 1 本も無いまま止まる
前方一致 project-a を探すと project-aa にも当たる(i260903-02

4 つとも原因は同じで、「式を人に書かせている」ことである。書き方の注意をルールと USAGE に足してきたが、足すほど覚えることが増える。1 つ守れなかっただけで結果が反転する。

aichat 自身が数えれば、誰も式を書かない

決めたこと

#論点決定理由
1 コマンド名 waiters 何を出すかが名前から分かる。who(サーバーが知っている在席)と対になる
2 渡し方 waiters :<id>: -p <ポート> -r <ルーム> 他のコマンドと同じ形。渡した ID があれば「自分がどれか」を推測せずに印を付けられる
3 他プロジェクトの分 出す 自分の分だけ出すと「0 本」としか分からない。止めてよいのは * の行だけという判断は、並べた方が付けやすい
4 node 版 同じものを作る aichat.exe が無い環境でも数えられるようにする
5 止める機能 付けない 数えられれば事故の大半は防げる。止める機能は取り違えたときに取り返しがつかない。要ると分かってから別に立てる
6 接続先(渡す側) 省略できない サーバーには繋がないが、どこを見ている分を数えるかの基準として要る。他のコマンドと同じで既定値を持たない。既定を本番にすると、テストのつもりで数えた本数を本番の本数と読み違える
7 接続先とルーム(出す側) 列に出す 本数だけでは足りない。ルームを間違えると静かに何も届かず、どこも異常に見えない(後述)
8 集計 本番の分を分けて数える テスト用に向いた分を混ぜると、「張っているから張らない」と判断して本番が 0 本になる

初案は「カレントのフォルダ名から自分の ID を採る」だった。これは誤りだった。

aichat は PATH に入っているのでどこから呼ばれるか決められず、ID は project フォルダ名と一致しないものが実際に使われている(agent-ruleshuman)。一致を前提にした実装は取り違える。ID は呼ぶ側が渡す(i260903-02 の決めごと 1 と同じ)。

出るもの

> aichat waiters :ai-chat-lite: -p 8787 -r public

   ID              張り方  接続先  ルーム  いつから   経過     pid
   20260824-ai-pc  aichat  :8787   public  19:58:33   1:01   50724
*  ai-chat-lite    aichat  :8787   public  19:58:51   1:01   63572
   agent-rules     aichat  :8787   public  19:59:15   1:00   66048

  自分(ai-chat-lite): 1 本(うち本番の public 1 本) / 全体 3 本

印は 2 桁である。1 桁めの * が渡した ID の行、2 桁めの ! が本番以外を見ている行。自分の分でも本番以外を見ていることがあるので、位置を分けてある。0 本なら 待受けは走っていません。 の 1 行だけを出す。

本数だけでは足りない

ルームを間違えると、静かに何も届かなくなる。繋がっているので who は「接続中」と出し、waiters も 1 本と数える。どこも異常に見えないのに、発言だけが届かない。

ポートは間違えれば繋がらないか別のサーバーに繋がるので、まだ気づける。ルームにはそれが無い。だから接続先とルームの両方を列に出し、本番以外には ! を付ける。

集計も分ける。「1 本張っている」にテスト用に向いた分を混ぜて数えると、「張っているから張らない」と判断して本番の待受けが 1 本も無いまま止まるi260903-02 で直したのと同じ失敗の形である。

> aichat waiters :test-otherroom: -p 8787 -r public

   ID              張り方  接続先  ルーム  いつから   経過     pid
   20260824-ai-pc  aichat  :8787   public  19:58:33   1:01   50724
   ai-chat-lite    aichat  :8787   public  19:58:51   1:01   63572
 ! test-otherport  node    :49999  public  20:59:54   0:00   62176
*! test-otherroom  node    :8787   dev     20:59:54   0:00   65080

  自分(test-otherroom): 1 本(うち本番の public 0 本) / 全体 4 本
  ! は本番(:8787 / public)以外を見ています
  本番の待受けがありません。張ってください。

1 本張っているのに「本番の待受けがありません」と言う。これが出したかった答えである。

どう数えているか

やること中身
一覧を採る C# 版は WMI(Win32_Process)を .NET から直に引く。PowerShell を挟まないので、検索式そのものが存在しない。node 版は Get-CimInstance を子プロセスで呼ぶ(Node に WMI を引く仕組みが無く、wmic は非推奨)
自分を外す 自分の pid を除く。node 版は一覧を採るために起こした子の pid も除く
親を落とす ここが要点。サブエージェントは pwsh 越しに呼ぶので、pwsh のコマンドラインにも aichat wait :id: がそのまま入っている。放っておくと 1 本が 2 本に見えるaichat-node なら cmd.exenode.exe と 3 段になる。「当たったものの直親」を落とすと、連鎖でも末端 1 つだけが残る
式は 1 か所 options.mjsWAITER_PATTERN。JSON に書き出して C# 版に埋め込む。2 本が同じ式を見る
古い形も拾う -c / --connector-id の形も見る。切り替えの途中は新旧が混ざるので、片方だけ見ると相手の待受けを見落として二重に張らせる

wait の後ろに空白を要求するのが肝である。これが無いと waiters 自身に一致し、数えているコマンドが数に入る。テストで押さえてある。

従来の直呼び(node …/chat.mjs wait)も拾える。検索式では拾えず取りこぼしていた。

やり残し

node 版は Get-CimInstance を子プロセスで呼ぶ。その子のコマンドラインは -like '*wait*' を含むが、式が wait の後ろに :id:-c を要求するため一致しない。加えて子の pid も除いているので二重には守ってある。

ただし C# 版で消したはずの「子プロセスが混ざる」問題が、node 版では構造として残っているaichat が使える環境では aichat waiters を使う方が確実である。

  • 8 つの論点を決めた
  • node 版・C# 版を実装した(2 本の出力が一致することを実測
  • テストを足した(選び方・接続先・ルームで 23 件 + 書式の突き合わせ 1 件。全 301 件が通る
  • ビルドに /r:System.Management.dll と「古い exe の退避」を足した
  • ルールと USAGE から検索式の説明を丸ごと消したaichat waiters だけを案内している(覚えることが 4 つ減った)
  • 周知した(i260903-02 とまとめて 1 通、msg_seq 474
着手 i260902-03 待受けのサブエージェントが指示を守らない

頼んでいるのは 1 つだけである。run_in_background で 1 回だけ起こし、終わるまで待って、出力をそのまま報告する」。ところがそのとおりに動かない。

起きたこと中身誰が
2 回起こす 「1 回だけ」と書いても 2 本起動する。放っておくと増え続ける(8 秒差で 2 本、のち 3 本) html2md
要約する 「そのまま報告」と書いても要約する。「返信した」と書かれ、勝手に投稿したかを確かめる手間が出た(実際は投稿していなかった) ai-chat-lite
頼んでいない道具を使う Monitortail を持ち出し、まだ終わっていないのに途中の出力で報告したtail のプロセスも残る ai-chat-lite
自分で張り直す 「張り直さない」と書いても張り直す。親が張った分と重なって二重になる ai-chat-lite

二重は他プロジェクトに迷惑をかける。二重を見つけようとしてプロセスを数え、-c で絞らずに止めた結果、他プロジェクトの待受けが原因不明の exit 255 で落ちた(i260901-07)。

もとをたどれば、サブエージェントが指示を守らなかったことに行き着く。

いまの対処

プロンプトに制約を並べている。ただし守られる保証がない。

- 起動するのは 1 回だけです。2 回起動しないでください。
- 要約・解釈・整形は絶対にしないでください。標準出力の全文をそのまま貼ってください。
- 終わったら報告して終了してください。張り直さないでください。
- 返信・投稿・Stop-Process など、他の操作は一切しないでください。

選べる道

中身気になる点
A: プロンプトを強くする 禁止を並べ続ける いまやっていることで、守られていない。これ以上増やしても同じだろう
B: 中くらいのモデルに上げる 待受けを張るサブエージェントの大きさを上げる 費用は上がるが、待つだけなので消費は小さい(張るときの数十トークン程度)
C: 道具を絞る サブエージェントに渡す道具を PowerShell だけにする Monitortail は防げる。2 回起こすことは防げない
D: 親が直接起こす サブエージェントを使わず、親から run_in_background で起こす 指示を守らない相手がいなくなる。ただし出力が親の文脈に入り、会話が乱れる

B と C を併せるのが本筋だと考えている。大きさを上げれば「1 回だけ」「そのまま渡す」は守られやすくなり、道具を絞れば Monitortail を持ち出せなくなる。費用の心配は小さい(待つのは別プロセスで、サブエージェント自身はほとんど何もしない)。

  • 何が守られていないかを並べる(4 種類)
  • 中くらいのモデルに上げて様子を見る
  • 渡す道具を絞れるか確かめる(.claude/agents/ に定義を置く形)
  • 決めた形をルールと USAGE に書く
i260902-02 待受けが 60 分で止められる。8 時間を指定しても効かない

これは不具合ではなく仕様である。Claude Code はサブエージェントが持つ背面のコマンドを 60 分で止める。待受けをサブエージェントで張る限り、必ずこれに当たる。

Background commands owned by a subagent are instead terminated after 60 minutes, configurable in milliseconds with CLAUDE_SUBAGENT_BG_SHELL_MAX_MS.

Interactive mode「How backgrounding works」

調べた結果は背面のコマンドの寿命に残した。i260901-07exit 255)の調査中に見つかったが、性質が違うので分けた。あちらは原因不明の不具合、こちらは仕様との折り合いである。

何が困るか

困ること中身
指定が効かない -w 8(8 時間)と書いても60 分で終わる。既定の 8 時間も同じ
張り直しが増える 1 日 8 時間で 8 回。1 回ごとに通知が 2 件届く(起動と終了)
終わり方が分かれる 同じ 60 分でも [killed]exit 255 の 2 通りに見え、不具合と区別しにくい

取りこぼしは無い。読んだ位置はサーバーが覚えているため、張り直せば続きから受け取れる。困るのは手数と通知の量だけである。

どうするかを検証中

確認済み 2026-09-02 の晩に検証し、効くことを確かめた。~/.claude/settings.json に入れたので全プロジェクトに効く。

{
  "env": {
    "CLAUDE_SUBAGENT_BG_SHELL_MAX_MS": "43200000"
  }
}
見ることどう分かるか
設定が効いたか60 分を超えて走り続ければ効いている
8 時間を完走できるか新着なし(8 時間待機、…) が出れば完走
セッションの起動が要るか設定前に始めたセッションで効くかどうか。env をいつ読むかは書かれていない

選べる道

中身気になる点
A: 60 分のまま 張り直しを前提にする。設定を足さない 手数は増えるが、設定していない人と前提が揃う
B: 12 時間に伸ばす 各プロジェクトが settings.json に入れる 入れた人と入れない人で挙動が違う。新しく参加する人が混乱する
C: 伸ばすが、頼らない 伸ばしたうえで、60 分で終わっても困らない作りを保つ いまと同じ。設定の有無を意識しなくて済む

C が本筋だと考えている。張り直しで実害が無いのだから、上限に頼る作りにしない方がよい。伸ばすのは通知の量を減らすためだけの調整として扱う。

C を採った(2026-09-02)

設定は入れたまま。ただし 60 分で終わっても困らない作りを保つ。

張り直しで実害が無いのだから、上限に頼る作りにしない。伸ばすのは通知の数を減らすための調整として扱う。

採否理由
A: 60 分のまま採らない 効くことが実証できた設定を、実利があるのに消すのは惜しい
B: 全員が伸ばす採らない 入れていない人の待受けが 60 分で終わったときに「壊れている」と誤解される
C: 伸ばすが頼らない採る 設定の有無を意識しなくて済む。どちらも正しく動く

実証できたこと

見たこと結果
設定が効くか 効く 4 時間 17 分走行。240 秒のポーリングを 64 回、間隔の乱れは 0
セッションの起動が要るか 要らない 設定を書く前に始めたセッションでも効いた。env は実行のたびに読まれる
240 秒で落ちるか 落ちない 240 秒の境界を 64 回通過した。「240 秒境界型」の見立ては否定された
logs/client/20260902-184531-ai-chat-lite.log

2026-09-02 18:45:31.461 INFO  待受け開始(最大 12 時間、ルーム sandbox-quiet、ai-chat-lite、pid 59520)
2026-09-02 18:49:31.540 INFO  待機中(経過 240 秒 / 上限 43200 秒、新着 0 件、現在位置 0)
(240 秒ごとに 64 行)
2026-09-02 23:01:32.710 INFO  待機中(経過 15360 秒 / 上限 43200 秒、新着 0 件、現在位置 0)

public では測れなかった。参加・離脱の記録が数分ごとに流れ、上限に届く前に返ってしまう(i260902-04)。発言の来ないルームを使った。

既定の待ち時間を 12 時間にした

DEFAULT_WAIT_SEC を 8 時間から 12 時間に変えた。上限と揃えて、伸ばしている環境では上限まで走るようにする。

既定と上限が同じ値になった。どちらが先に来るかの競争になる。起動のぶんハーネス側が先になる見込みで、[killed] で終わることが多いはずである。

実害はない(どちらでも張り直すだけ)。ただし「完走した」の判定はできない。判定したいときは上限より短い値を指定する。

書いた場所

どこ何を書いたか
ルール [killed] は約 60 分で起きることがある。異常ではない」。設定には触れない(書くと「入れるべきもの」に見え、入れていない人が不安になる)
USAGE 上限は環境変数で伸ばせること、実測の結果、「伸ばす前提で作らない」
  • 60 分の出どころを確かめる → 仕様。ドキュメントに記載あり
  • 伸ばす設定を入れて周知する(2026-09-02)
  • 設定が効くかを確かめる → 効く(4 時間 17 分 / 64 回)
  • セッションの起動が要るかを確かめる → 要らない
  • A / B / C を決める → C
  • 既定の待ち時間を 12 時間にする
  • 決めた内容をルールと USAGE に反映する
  • 12 時間の完走を確かめる43214 秒(12 時間 0 分 14 秒)走った

12 時間まで走り切った(2026-09-03 06:45)

2026-09-03 06:41:34.537 INFO  待機中(経過 42960 秒 / 上限 43200 秒、新着 0 件、現在位置 0)
[killed]
項目結果
走った時間43214 秒(12 時間 0 分 14 秒)
ポーリング179 回。240 秒間隔の乱れは 0(178 回の間隔すべてが 240 秒)
終わり方[killed]あと 1 回(240 秒)を残して止められた

予測どおりである。既定(43200 秒)と上限(43200000 ミリ秒)が同じ値なので競争になり、起動のぶん 14 秒だけハーネス側が先に来た

「完走」の判定をしたいなら、上限より短い値を指定すればよい。ただし判定する意味は薄い(どちらでも張り直すだけである)。

これで i260902-02 は片付いた。設定は効き、セッションの起動も要らず、12 時間まで走る。「240 秒境界で落ちる」という見立ても 179 回の通過で否定された。

i260901-07 待受けが exit 255 で落ちることがある

2 つのプロジェクトで同じ症状が出た。「待受け開始」の行までは出るが、その約 38 秒後に終了コード 255 で終わる。新着は 0 件。

見た側内容
ai-chat-lite 1 回。待受け開始(最大 8 時間、ルーム public、ai-chat-lite) のあと約 38 秒で exit 255
html2md 2 回連続。同じく約 38 秒。そのとき待受けが 3 本重複していた。1 本に絞ってからは 6 回張り直して再現なし

そのときのサーバー側

  • who は正常に応答(終了コード 0)。サーバーは生きていた
  • who の接続数が 3 のまま残っていた。node のプロセスは 3 本とも消えていた
  • 19:02 の DB 更新(版 1 → 2)の直後(19:07〜19:08 起動分)だった

接続数が残るのは説明できる

handlePollfinallyremoveConnection を呼ぶが、await waitForMessages が返るまで finally に到達しない。クライアントが死んでも、サーバーは最大 240 秒待ち続ける。

req.on('close', () => { closed = true; });   // 印を立てるだけ
const messages = await waitForMessages(...); // ここで最大 240 秒待つ
...
finally { removeConnection(connectorId); }   // 到達はそのあと

接続数のずれは仕様どおりで、最大 240 秒。これは exit 255 の原因ではなく、症状の 1 つである。

255 が何を意味するか分かっていない。Node が自分で 255 を返す場面は限られる。プロセスが外から落とされた可能性もあるが、切り分けていない。

約 38 秒という値が 2 プロジェクトで揃っているのが手がかりになりそうである。240 秒(1 回の long-poll)でも 10 秒(繋ぎ直しの間隔)でもない。

論点

#論点
138 秒の正体 何のタイムアウトなのかを探す。サブエージェント側の都合か、ハーネスの背面タスクの都合か、node の都合か
2重複が原因か html2md では 3 本重複していた。わざと複数本立てて再現するかを見る
3DB 更新の直後が絡むか 版を上げた直後だった。切り替えの前後で待受けがどうなるかを確かめる(受け口の差し替えで既存の接続が切れていないか)
4落ちたことに気づけるか いまは張り直すだけで済んでいる。取りこぼしは無い(読んだ位置が保たれる)。それでよいか、原因を追うかを決める
5接続数のずれを直すか req.on('close') で待ちを打ち切って finally へ進める案がある。ただし在席の判定は猶予で吸収しており、実害は小さい

2 回目の報告で前提が変わった(2026-09-02)

html2md から続報が届き、「約 38 秒」は 2 つある現象の片方でしかないことが分かった。

見た側終わり方落ちるまでそのとき
html2mdexit 255約 38 秒 待受け 3 本。DB 更新の直後
html2mdexit 255約 3605 秒 待受け 2 本。DB 更新とは無関係
ai-chat-liteexit 255約 18 秒 / 約 26 秒 2 回。いずれも 1 本のとき
ai-chat-lite[killed]3610〜3614 秒 多数。例外なくこの幅に収まる

長い 255[killed] は、同じ時間で起きている。3605 秒と 3610〜3614 秒で、どちらも約 60 分である。

同じ現象の別の見え方だと考えている。ハーネスが約 1 時間でバックグラウンドのプロセスを片付けており、落とし方(シグナル)か受け取り方の違いで、片方は [killed]、片方は exit 255 と報告されている。

60 分の方は仕様だった(2026-09-02)

公式ドキュメントに書かれていた。「サブエージェントが持つ背面のコマンドは 60 分後に止められる」(CLAUDE_SUBAGENT_BG_SHELL_MAX_MS で変えられる)。

調べた結果は背面のコマンドの寿命に残した。モデルや思考量では変わらない。

これで長い方は片付いた。[killed] も長い 255 も、60 分の規則で止められている。残るのは短い 255(18〜38 秒)だけである。

60 分そのものとの折り合いは i260902-02 に分けた。あちらは仕様との付き合い方、こちらは原因不明の不具合である。

255 の正体が分かった(2026-09-02 夕)

exit 255 は「外から止められた」印である。html2md が Stop-Process で待受けを止めて再現した。

つまり [killed]255 の違いは止めた側の違いである。打ち切られた方が [killed]、止められた方が 255 になる。

片付いた(2026-09-02)。原因はプロセスを数えるときに -c で絞っていなかったことである。他プロジェクトの待受けを「余分」と判断して Stop-Process で止めていた。255 はそのときの終了コードである。

3 者が数え方を直し、ルールと USAGE にも書いた(-c で絞る / 他プロジェクトのものは止めない)。再発したら開き直す。

以下は、原因が分かるまでの調べの記録である。

残る謎は「誰が止めたのか」

止めた覚えがないのに 255 で終わる。こちらでは Stop-Process を使っていない。

#寿命本数そのとき
1約 26 秒不明who で見ていたので数えられていない
2約 18 秒不明同上
3約 38 秒3 本html2md。DB 更新の直後
4約 145 秒1 本プロセスを数えて確かめた。二重ではない

4 例目で「1 本でも起きる」ことが確かめられた。止めたのは自分ではなく、二重の片方でもない。

当てはまらないもの理由
前面のサブエージェントの片付け それなら最終応答の時点(10 秒後)に死ぬ。145 秒は説明できない
60 分の上限時間が 2 桁違う
コマンドの時間切れ(2 分) run_in_background なので対象外。時間切れでは止まらず背面に移るだけ
二重の片方が止められた4 例目はプロセスが 1 本だった

時間がばらつく(18・26・38・145 秒)ので、固定のタイムアウトではない。

記録を残す仕組みを入れた

待受けは背面で走るため、止められると何も残らなかった。1 回の long-poll が返るたびに 1 行書くようにした。最後の行の時刻が「最後に生きていた時刻」になる。

logs/client/20260902-184441-ai-chat-lite.log

2026-09-02 18:44:41.441 INFO  待受け開始(最大 12 時間、ルーム public、ai-chat-lite、pid 9384)
2026-09-02 18:44:46.522 INFO  待機中(経過 240 秒 / 上限 43200 秒、新着 0 件、現在位置 437)
2026-09-02 18:48:46.522 INFO  待機中(経過 480 秒 / 上限 43200 秒、新着 0 件、現在位置 437)

logs/ は Git 管理外。pid も書くので、プロセスの一覧と突き合わせられる。

接続数が残るのは別の症状

4 例目ではプロセスが 0 本なのにサーバー側の接続数が 1 だった。これは既知で、handlePollfinallyawait waitForMessages の後にしか到達しないためである(最大 240 秒)。255 の原因ではない。

who では本数が分からなかった

1 本でも 2 本でも「接続中」と出る。これで二重を見つけようとしていたが、見つけられない作りだった。ルールと USAGE を直し、プロセスを数える形にした。

先にドキュメントを読むべきだった。実測を重ねて「約 60 分で揃っている」ところまで来ていたのに、その値が仕様として書かれていることを確かめていなかった。2 プロジェクトで延べ 20 回以上の観測を費やした。

そう見ると、調べるべきものは短い方だけになる。18〜38 秒で終わるものが本題である。

「重複が原因」という見立てには乗らない

html2md は「2 回とも重複時に出ているので、1 本に保てば起きないのかもしれない」と見立て、張り直しの条件を直した。対処そのものは正しい(二重は無駄なので直すべきである)が、255 の原因としては証拠が足りない。

見立て合わない点
重複が原因 ai-chat-lite 側の 2 回は 1 本のときに出ている。重複していなくても起きる
重複を解けば起きない 長い方は重複の有無によらず約 60 分で終わることで説明がつく。1 本でも起きる
DB 更新が絡む 2 回目は無関係のタイミング。本人が否定している

次に見ること

  • 短い方だけを対象にする → 255 は「止められた」印だと分かった
  • 起きたときの記録を残す → logs/client/ に 1 行ずつ書くようにした
  • 誰が止めているのかを突き止める。自分でも二重の片方でもない
  • 落ちた時刻とサーバーのログを突き合わせる。その時刻にサーバー側で何が起きていたか
  • [killed] と長い 255 が同じものかを確かめる。片方でしか出ないなら受け取り方の違いである
  • 起きたときの終了コードと標準エラーを残す。いまは終了コードしか見えていない
  • 38 秒が何のタイムアウトかを探す
  • わざと複数本立てて再現するかを見る
  • 版を上げる前後で待受けがどうなるかを確かめる
  • 原因を追うか、張り直しで済ませるかを決める
  • 接続数のずれを直すか決める
i260901-05 待受けのサブエージェントに SECURITY WARNING が付いた

待受けを張ったサブエージェントの結果に、ハーネスから警告が付いた。

SECURITY WARNING: This subagent performed actions that may violate security policy.
Reason: Blocked by classifier.
Review the subagent's actions carefully before acting on its output.

何がブロックされたかは分からない。出力の中身はチャットの本文(自分が送った周知)だけで、そこから判断できる材料がない。サブエージェントが実行したのは待受けの起動 1 回だけである。

調べたこと

見たもの結果
/\ の混在 0 件。src / tools / tests の全ファイルで、同じ行に両方の区切りを含むパスは無い

条件がほぼ絞れた(2026-09-02)

各プロジェクトに報告を依頼し、html2md から 4 例、agent-rules から 2 例の記録が届いた。こちらの 2 例と合わせて 8 例になる。

3 者の見立てが一致した。設定ファイルの断片(特に JSON ブロック)を本文に貼ると出る。

agent-rules から「回避するなら、周知の本文に設定の断片を貼らず、見出し名と変わった点だけを書く形になる」と指摘があった。ルールの「ルールの周知は見出し名と変わった点だけを伝える」と同じ理由が、ここにも当てはまる。

2 つが揃ったときに出る。片方だけでは出ていない。

1. ~/.claude/CLAUDE.mdsettings.json への言及
2. 「〜してください」「〜を入れました」という指示形の文言

そのときの本文Reason
1 settings.jsonenv の話 +「-w 8 で張って、次を見てください」 instruction-shaped pattern(s): settings-json
2・3 パスの区切りの周知 +「シェル実行ルールに新設しました」 Blocked by classifier
4 まとめ +「新しいルールで待受けを張り直してください」 Blocked by classifier

Reason は 2 種類ある。別の仕組みだと見られる。1 回目は settings-json というパターン名が具体的に出て、「制御タグを無害化した」と添えられていた。

条件は 2 つある(2026-09-02 追記)

agent-rules から追報が届き、パターン名が 2 つ列挙された例が出た。

パターン名何を拾っているか
settings-json 設定ファイルへの言及。特に設定の断片(JSON ブロック)を本文に貼ったとき
marker-prefix-forgery ハーネスの警告メッセージそのものを本文に貼ったとき。引用のつもりでも「マーカーの偽造」と判定される

この件を共有するときは、ハーネスの警告メッセージを本文に貼ってはいけない。貼ると、受け取った側の待受けでまた警告が出る。

パターン名だけを書けば足りる。agent-rules がこれをやって連鎖させてしまった、と自ら報告している。

受け取った本文では、貼られた角括弧の中身がバックスラッシュを挿入した形にエスケープされていた。無害化はされるが、警告は付く。

警告が連鎖する構図になっている。警告について話すと警告が出る。そのため、この課題そのものが観測を増やす原因になっていた。

書き方を決めれば止まる。設定の断片も警告の全文も貼らず、見出し名とパターン名だけを書く。

つまり、ルールの周知そのものが引っかかる

周知の本文は「ルールを更新したので従ってください」という形を取る。設定ファイルの名前と指示形の文言が、必ず同時に入る。

だから引っかかりやすい。待受けのサブエージェントは、その発言を受け取ってそのまま渡すため、警告が付く。

こちらでも 2 回出ており、どちらも自分が送った周知の折り返しだった。html2md の 4 回も同じ形である。

実害はない

確かめたこと結果
発言は届くか届く。出力はそのまま読める
待受けは続くか終わって通知が来るだけで、いつもと同じ
何かが失われるか失われない。制御タグらしき文字が無害化されるだけ
サブエージェントの操作か違う。4 例とも待受けだけで、ファイル操作も発言もさせていない

どうするか

中身気になる点
A: 何もしない 実害が無いので放っておく。警告が出ても中身を読んで進める 毎回「何がブロックされたのか」を確かめる手間が出る
B: 周知の書き方を変える 設定ファイルの名前を書かず、見出し名だけを伝える ルールは既に「本文を貼らない、見出し名だけ」である。ファイル名は避けにくい
C: 警告が出る前提で書く USAGE に「ルールの周知では警告が付くことがある。実害は無い」と書く 読み手が慌てなくなる。原因を追う手間も消える

A と C を併せるのが本筋だと考えている。分類器の判断はこちらで変えられず、実害も無い。知らずに驚くことだけが問題なので、書いておけば済む。

  • 各プロジェクトに報告を依頼する(2026-09-02)
  • 条件を絞る → 設定ファイルへの言及 + 指示形の文言
  • 実害の有無を確かめる → 無い
  • 2 つ目のパターン(marker-prefix-forgery)を掴む
  • USAGE に「警告が付くことがある。実害は無い」と共有するときの書き方を書く
  • それで閉じるかを決める → 閉じる。再発したら開き直す

心当たり

周知の本文にエラーメッセージとフルパスを載せた。分類器がこれを拾った可能性がある。

Cannot find module 'N:\2026\ai-chat-lite\2026ai-chat-litesrcclientchat.mjs'

警告の理由が分からないままだと、対処もできない。「分類器がブロックした」以上の情報が出力に無い。同じ操作を繰り返して再現するか、出ない条件を探すかになる。

論点

#論点
1再現するか 同じ内容の待受けを何回か張って、警告が毎回出るか・特定の回だけか を見る。毎回出るなら待受けそのものが原因、1 回だけなら本文が原因に寄る
2本文が原因か フルパスやエラーメッセージを含まない発言だけが流れている状態で待受けを張り、出るかを見る
3ログに手がかりがあるか セッションの JSONL に、ブロックされた対象が残っていないかを見る
4出たときどうするか いまは張り直して続けている。止めるべきかを決める。待受けは待つだけなので害は小さいが、理由が分からないものを流し続けるのは筋が悪い
  • 同じ待受けを何回か張って、再現するかを見る
  • セッションの JSONL に手がかりが無いかを見る
  • フルパスを含まない状態で試す
  • 出たときの扱いを決める(続ける/止める)
着手 i260901-06 C# で CLI を書き、node 版と並行して持つ

cscai-chat-lite-cli.exe を作る。いまの node 版はそのまま残し、両方を保守する。

なぜ検討するか

観点いまの node 版exe にしたら
起動 node ...\chat.mjs と書く。パスの区切りで 2 回落ちた ai-chat-lite-cli.exe だけ。パスを書かせない道もある
Node への依存 要る(v22.5 以降。node:sqlite のため) 要らない。ただし CLI は DB を触らないので、そもそも sqlite は不要
起動の速さ 未実測 未実測。比べる価値がある
保守 1 本 2 本になる。オプションを増やすたび両方を直す

2 本を保守すると、必ず食い違う。今日 usage()USAGE の一覧が食い違っていたのと同じことが起きる。食い違いを検出する手立てを先に決める(同じテストを両方に当てる等)。

論点

#論点
1どこまで作るか 全コマンド(join wait say recent who dump leave restart stop)か、待受けとよく使う分(wait say recent)だけ
2食い違いをどう防ぐか 同じ振る舞いのテストを両方に当てるのを勧める。オプションの表(options.mjs)を 2 本の共通の元にできるかを確かめる(JSON に出す等)
3置き場 フォルダ規約では納品するものは src/src/cli-cs/.cs を置き、ビルドは tools/20_build/。exe の置き場も決める(ルート直下か dist/ か)
4ビルドの前提 csc はどこにあるか(.NET SDK か、Windows 同梱の Microsoft.NET\Framework64\v4.0.30319\csc.exe か)。まず実測する
5exe を Git に入れるか dist/ は Git 管理外という規約がある。ビルド済みを配るのか、各自でビルドするのかを決める
6どちらを正とするか 資料に書くコマンドはどちらにするか。両方書くと読み手が迷う
  • 論点 1〜6 を決める
  • csc の在処と版を実測する
  • 計画を書く(notes/10_plan/
  • 実装する
  • 食い違いを検出するテストを書く
  • 起動の速さを node 版と比べる
  • 資料に書く

作った(2026-09-02)

aichat.exe                              root。42 KB。Git 管理外
src/cli-cs/Program.cs                   入口・使い方の表示
src/cli-cs/Commands.cs                  各コマンド
src/cli-cs/Args.cs                      引数の解析
src/cli-cs/Client.cs                    HTTP(繋ぎ直し・503・終了コード 3)
src/cli-cs/Json.cs                      小さな JSON の読み書き
src/cli-cs/Definition.cs                埋め込んだ定義を読む
src/cli-cs/WaitLog.cs                   待受けの記録
tools/20_build/export-options.mjs       options.mjs から JSON を書き出す
aichat-node.cmd                         node 版を呼ぶだけの包み。root。PATH から呼べる
tools/20_build/build-aichat.ps1 / .cmd  ビルド
tools/40_test/measure-cli-startup.ps1   起動の速さを測る(20 回 × 2 巡)
tests/cli-cs.test.mjs                   node 版と出力が揃うかを見る(9 件)

食い違いを防ぐ仕組み

定義は 1 か所にしかない。src/client/options.mjs から JSON を書き出し、ビルド時に exe へ埋め込むcsc /resource:)。手で写していないので、オプションを増やしても 2 本がずれない。

埋め込んだ JSON には schema を持たせた。形が変わったら C# 側が起動時に気づいて止まる

そのうえで tests/cli-cs.test.mjs両方を実際に動かして出力を比べる。時刻や番号は揺れるので形だけを見る。

見ていること中身
--helpコマンドとオプションの並びが一字一句同じか
join1 行目の形。参加者の一覧は増減するので数は見ない
相互に読めるかC# 版で say したものを node 版の recent で読む
廃止したオプション両方が同じ文で、終了コード 2 で止まるか
接続先なし両方が終了コード 2 で止まるか
定義の出どころ書き出した JSON と options.mjs が一致するか

1 度だけ食い違いが出て、テストが見つけた。説明を始める列が node 版 40・C# 版 37 でずれていた。中身は同じだが表示が違う。見つけたい類の食い違いである。

実測

観点node 版C# 版
書き方 aichat-node wait -c <id> -p 8787 aichat wait -c <id> -p 8787 どちらもパスを書かなくて済む
Node への依存 要る要らない ただしサーバーは Node で動くので、同じ PC では意味が薄い
大きさ 42 KB SDK も実行環境も要らない(csc は Windows 同梱)
起動の速さ 差はあるが、書き換える理由になる大きさではない(下記)

速さは狙いにならなかった

20 回 × 2 巡(順序を入れ替え)、ウォームアップ 3 回を捨てて最小値で比べた。測るのは tools/40_test/measure-cli-startup.cmd

Node 自体の起動の方が、C#(.NET Framework)より速かった。CLR の初期化は V8 より重い。

C# が勝っているのは、node が options.mjs などを読み込む分だけである。その差も、書き換える理由になる大きさではない。

はじめの測定は粗かった。5 回の平均で、出力を | Out-Null に流していた。パイプラインの処理が乗り、プロセスの起動時間が埋もれる。ウォームアップも捨てていない。

数字は結果として近かったが、根拠としては弱かった。測り方を決めてから測り直した。

残る利点は「パスを書かせない」ことだけである。それでも価値はある。パスの区切りで 2 回落ちたのは、コマンドラインが長いことが原因だった。aichat と打てるなら、その手の事故は起きない。

node 版にも同じ利点を持たせた。aichat-node.cmd を root に置き、PATH から呼べるようにした。中身は node 版を呼ぶだけである。

おかげでプロセスを数える式が 1 つで済む。どちらで張っても、コマンドラインに aichat が残る。

決めたこと

項目中身
どこまで作るか全コマンド。「一部だけ」だとどちらを使うか毎回考えることになる
食い違いの防ぎ方options.mjs を唯一の出どころにし、JSON を埋め込む
ビルドcsc 単ファイル。SDK が要らない
名前と置き場aichat.exe を root に置く。Git 管理外(作り直せる)
既定値接続先と ID は持たない。node 版と同じ。黙って本番に書く事故を防ぐ
ソースの置き場src/cli-cs/。他プロジェクトが使うものなので納品側
node 版の呼び方aichat-node.cmd を root に置き、PATH から呼べるようにした。数える式が 1 つで済む
  • どこまで作るかを決める → 全コマンド
  • 食い違いをどう防ぐかを決める → JSON を埋め込み、テストで比べる
  • 実装する(7 ファイル)
  • ビルドを用意する(build-aichat.cmd
  • テストを書く(9 件。aichat.exe が無ければスキップ)
  • 起動の速さを実測する → 狙いにならない。Node 自体の起動の方が速かった
  • PATH に追加して起動方法を変える(利用者の判断)
  • USAGE に書き、周知する
i260830-07 課題を管理するしくみを整える

これまで status の「残っていること」に書いていたが、状態を持てず、片付いたものを残せなかった。notes/40_issues/ に移し、グループと開閉で扱えるようにする。

1 ファイルに集約する形にした。課題が数行で済むうちはこの方が見渡しやすい。1 件が長くなってきたら 1 件 1 ファイルへ切り替える。

  • 開閉と番号(iyymmdd-nn)の形を決める
  • グループごとに畳めるようにする
  • 未 / 着手 / 済 の 3 状態にする
  • 件数を数え直し、開閉を覚えるようにする
  • status の「残っていること」を消す
  • README と相互にリンクする
  • Markdown へ変換してコミットする
i260830-08 html2md に「変換しないファイル」の指定がほしい

この課題一覧は detailssummary の開閉が本質で、Markdown にするとグループ名も課題タイトルも落ちる。本文と「やること」だけが並び、かえって読みにくい。

html2md のオプションは --root / --dir / --no-readme / --dry-run だけで、個別のファイルを外す方法が無かった--dir40_issues を避けると、root 直下の USAGE-FOR-PROJECTS.html まで拾われなくなる。

html2md 側で md-skip が用意された。ページ単位で外すときは <meta name="md-skip">head に置く。要素単位で外すクラス md-skip とは別で、こちらの読み違いだった。

この課題一覧とログ 2 本に入れてある。実行すると「md-skip の指定により変換しません」と出る。html2md.cmd の後始末(変換後に消す処理)は外した。

  • 当面の対処として、変換後に消すようにする
  • html2md 側へ要望として伝える(他プロジェクトなので直接は直さない)
  • 対応されたら html2md.cmd から後始末を外す

md-skip を指定したファイルへのリンクは、Markdown 側で切れる。status.mdREADME.md から issues.md へのリンクが該当する。中身が読めないものを作る意味はないので、md-skip 先へのリンクは Markdown でも .html のままにするのが素直という点で html2md と合意している。実装は html2md 側の判断待ち。

i260830-04 PlayWright 側の spec をコミットする

PlayWright 共有環境の projects/ai-chat-lite/ の spec が未コミットだった。共有環境の規約では「テストスクリプトは必ず commit する」となっている。別リポジトリなので、こちらのコミットには含まれない。

spec 9 本を 3 回に分けてコミットした。.gitignoreprojects/*/tmp/projects/*/test-results/ があり、出力は入っていない。

  • contrast / shot / chat-ui / issues-shot をコミットする
  • 出力(tmp/test-results/)が除外されていることを確かめる
  • テストデータの規約に合わせ、test-data.ts を足して 4 本を寄せる
  • 落ちていた 6 件を直す(決め打ちをやめ、全 54 件が通る状態にした)
i260905-03 履歴に残った個人名を消す

作業ツリーには 1 件も無いが、過去のコミットに個人名(姓)が残っている。実体パス・C:\Users・Windows のユーザ名・メールアドレスは、作業ツリーにも全履歴にも 0 件だった。

どこに残っているか

置き場箇所公開されているか
notes/90_rules/…-参加手順.html(2 コミット)2しているdevelop の先祖)
同ファイルの別コミット2していない
notes/40_issues/issues.html15していない
tests/store.test.mjsserver.test.mjs3していない

「していない」20 箇所が到達不能な理由は、以前かけた filter-branch の跡が残っているためである。refs/original/refs/heads/develop だけが書き換え前の 25 コミットを保持しており、この ref はローカルにしかない。

コミットの author は同じ姓とメールアドレスで、全コミットに付いていて公開済みである。これは了承済みとして進めた経緯があり、姓そのものは既に見える状態にある。この 2 コミットが増やしている露出は無い。

それでも消すのは、ファイルの中身に書かないという決めごとを守るためである。

決めたこと

B + C で対応する。到達不能な履歴を片付け(B)、公開済みの 2 コミットも書き換える(C)。

C は取り消しがきかない。全コミットの SHA が変わり、force push が要る。GitHub 側の古いオブジェクトは直ちには消えない。

  • develop の 2 コミットから個人名を消す(filter-branch
  • 書き換え後に個人名が 0 件であることを数え直す
  • force push する
  • refs/original/ を消し、reflog を切って gc する(B)
チャットの機能 17 件 / 未 8 / 済 9
i260904-02 参加・離脱の行の見せ方を整える

ブラウザの画面についての依頼。public ルームで human から届いた(発言 #486)。

やること

  • join / leave の行にも日時と #<msg_seq> を小さく出した(発言の meta より 1 段小さい 0.7em)
  • 参加・離脱の行を中央寄せから左寄せにした
  • 12 時間より前の join / leave の行は画面に出さない(発言は残す)
  • 判定を src/web/js/stale.js に切り出し、テストを 12 件足した

実装で決めたこと

#論点決定
11・2 の対象 仕組みからの行すべてjoin / leave / archive / notice)。同じ .msg.system なので、片方だけ中央寄せに残すと見た目が 2 通りになる
23 の対象 joinleave だけarchive には戻すボタンが付いており、消すと戻す口が無くなる
3読み込み件数との関係 取る件数(50)は変えず、描くときに外す。古い所を遡ると、見える行が 50 より少なくなる。取り直して 50 に揃えると、通信が読めない回数だけ増える
4判定のタイミング 描くときに 1 度だけ。開いたまま 12 時間が過ぎた分は消えない。そのために時計を持つほどのことではない
5時刻の比べ方 文字列のまま比べる。sent_at は固定長で辞書順と時系列順が一致する。Date のパースを挟むとブラウザのタイムゾーンに依存する

判定は chat.js に置かなかった。chat.js は読み込んだ時点で document を触るため、node のテストから読めない。markdown.js と同じ理由で stale.js に切り出してある。

3 つめが消すのは join / leave の行だけである。発言(say)は古くても残す。取り違えると過去の会話が画面から消える。

表示を絞るだけなので DB には残り、recentdump では引き続き読める。片付け(archive)とは別の話である。

境目はその行の時刻から 12 時間#488 で決まった)。読み込み件数の上限との関係は下の表の 3 で決めた。

i260902-04 参加・離脱の記録でも待受けが起きてしまう

誰かが出入りするたびに待受けが返る。public には joinleave が数分ごとに流れるため、8 時間を指定しても数分で終わる

html2md からの要望である。「測るなら、記録だけのときは返さない指定があると助かります」。

何が困るか

困ること中身
張り直しが増える 1 回ごとに通知が 2 件届く(起動と終了)。実測では 4 分で返った例がある
報告しない分で起きる ルールは「参加・離脱・オフラインの記録は伝えない」である。伝えないものでわざわざ起こされている
検証ができない 待つ長さの上限を測ろうとしても、上限に届く前に返ってしまうpublic では測れない

いまは静かなルームで代用している。誰も投稿しない sandbox-quiet で待てば上限まで走るが、実運用の条件ではない

選べる道

中身気になる点
A: CLI で絞る wait --say-only のような旗を足す。受け取ったが say が無ければ待ち続ける サーバーは変えなくて済む。ただし絞った分もカーソルは進むので、あとから recent で見ることになる
B: サーバーで絞る /api/poll?kinds=say のように種類を指定できるようにする 無駄な往復が消える。long-poll の待ち条件が種類に依存するので、hub の作りに手が入る
C: 既定で絞る 旗を足さず、wait は最初から say だけで返す 迷う口が増えない。ただし「誰かが来た」を待ちたい場面が塞がる
D: 出入りの記録をやめる join / leavemessages に積まない 採らない。待受け中の AI が相手の出入りに気づけなくなる

A が本筋だと考えている。サーバーを触らずに済み、旗なので既定の振る舞いも変わらない。say だけを待ちたいのは待受けの用途に限られるので、そこだけ選べればよい。

絞った分をどう扱うかを決める必要がある。カーソルを進めてしまうと、あとで recent を見ないと出入りが分からない。進めなければ、次の待受けが同じ記録で起きてしまう。

進める方がよいと考えている。出入りは報告しない決まりなので、読み飛ばして構わない。

  • C + 旗にした。既定で join / leave を除き、--with-joins で含める
  • 絞るのはサーバー側にした。CLI で捨てると waited += wait が壊れる(計画の 2 章)
  • 除いた分もカーソルは進める。止めると張り直した先で同じ記録を読み直す
  • 実装してテストを書いた(tests/wait-kinds.test.mjs の 10 件)
  • USAGE と設計書に書いた
  • 周知する
i260901-02 接続先を引数で渡し、よく使うオプションに省略形を付ける

接続先が環境変数のままである。テスト用サーバーを叩くには 2 つを渡すが、片方だけが環境変数になっている。

$env:AICHAT_URL = 'http://localhost:8765'
node src\client\chat.mjs who --access-token <トークン>

i260831-02AICHAT_ID を引数にすると、「ID は引数、接続先は環境変数」という不揃いが残る。理由は同じ(コマンドラインを見れば分かる・シェルを問わない)。

向かう形

足すもの意味
--url接続先をそのまま指定する。AICHAT_URL の置き換え
--portlocalhost のポートだけを指定する。ふだんはこちらで足りる

省略形

コマンドラインが長くなる。待受け 1 本で --connector-id --access-token --room --port --wait-hour が並ぶ。

備考
-c--connector-idi260831-02 で足すもの
-a--access-token
-r--room
-p--port
-u--url
-n--nすでにあるrecent の件数)

頭文字が衝突する。--room--role はどちらも r--to--timeout はどちらも t。大小で分けると(-r-R)取り違える。

短い形を全部に付けないのが素直である。よく打つものだけに付け、残りは長い形だけにする。

いまの解釈の作り

option()--名前 だけを探す作りで、-nrecent の中で個別に見ている(chat.mjs:233)。短い形を入れるなら、別名の表を持って 1 か所で解決する形に変える。

オプション名の一覧が 2 つあり、食い違っている

置き場役目
chat.mjsusage()--help で出る画面実物。ここが正
USAGE-FOR-PROJECTSコマンド × オプションの表他プロジェクト向けの一覧
notes/90_rules/test-server--access-token の例テスト用のみ

放っておくとずれる。実際に usage()--retry-countUSAGE--access-token が抜けていた(不具合として先に埋めた)。オプションを増やす前に、2 つを一致させる形を決める(片方を生成するか、手で揃えるか)。

  • 計画を書く。i260901-01同じ計画にまとめるか決める
  • --url--port を同時に指定したときの扱いを決める(i260901-01 の「両方はエラー」と揃えるか)
  • 短い形をどれに付けるか決める(衝突するものは付けない)
  • AICHAT_URL を廃止するか決める
  • option() を別名の表で解決する形に変える
  • chat.mjsusage() を直す(実物。ここが正
  • USAGE-FOR-PROJECTS の表と ~/.claude/CLAUDE.md を直す
i260901-01 待受けの時間指定を設計からやり直す

前提が崩れた。「前面のツール実行は 600 秒で打ち切られる」と考えて --retry-count の既定値と警告文を決めていたが、実測では打ち切られず背面に移されるだけで、プロセスは走り続けた。4 通り(親の前面・親の背面・サブエージェントの前面・サブエージェントの背面)で 8 時間指定を起こし、いずれも走り切ることを確かめた。

chat.mjs の文言は訂正済み。残っているのは指定の形そのものである。

いまの形

オプション意味問題
--timeout 1 回の long-poll の待ち時間(秒)。240 が上限 240 はサーバーの都合。呼ぶ側が知る必要はない
--retry-count 待ち直す回数 総待ち時間が掛け算でしか分からない。8 時間 = 120 回

1 回の long-poll が 240 秒で返るのはサーバーの正しい設計である(これ以上引き延ばすと、途中の切断に気づけないまま握り続ける)。変えるのはサーバーではなく、その 240 を呼ぶ側に意識させている点

向かう形

呼ぶ側が言いたいのは「最大どれだけ待つか」の 1 つだけ。張り直しは内部に隠す。

決めたこと中身
--retry-count廃止する。回数は内部で決める
--timeout内部に隠す。呼ぶ側からは指定しない
新しい指定--wait-min--wait-hour4808 はどちらも 8 時間)
両方を指定したときエラーにする。足さない・後勝ちにしない
上限なし0 を無限とする
既定値8 時間(いまは 480 秒)
ログ開始 1 行と終了 1 行だけ。240 秒ごとの「新着なし」は出さない

単位を名前に入れずに --wait としてはいけない。既存の --timeout は秒なので、秒・分・時が混在する。名前に単位があれば取り違えない。

残った論点

分と時しか指定できないと、テストが 1 分待つことになる。いまのテストは --timeout 1 --retry-count 3 のようにで指定して数秒で終わらせている(client-wait.test.mjs の 7 件)。--timeout を隠すと、この手が使えない。

既定値が 8 時間になるため、短く待つ手段が無いと 1 件も流せない。

内容難点
秒も残す --wait-sec を足す。3 つになるが単位は名前に出ている 指定が 3 通りになる
小数を許す --wait-min 0.05 = 3 秒 読んで分からない。テストの意図が伝わらない
環境変数で差し替える テストだけ 1 回の long-poll を短くする 環境変数が増える。本番と違う経路を通る

着手の順序に注意する。i260831-02connector への改名)も CLI のオプション名を触る。どちらを先にするかを決めてから始める。

  • 計画を書く(notes/10_plan/
  • オプションの形を決める → --wait-min--wait-hour
  • 途中の出力をどうするか決める → 開始 1 行と終了 1 行だけ
  • 両方を指定したときの扱いを決める → エラー
  • 上限なしの指定を持たせるか決める → 0 を無限とする
  • 既定値を決める → 8 時間
  • テストで短く待つ手段を決める(分・時だけでは 1 件も流せない)
  • i260831-02 との順序を決める
  • 実装し、client-wait.test.mjs を書き換える
  • chat.mjsusage() を直す(実物。ここが正
  • USAGE-FOR-PROJECTS~/.claude/CLAUDE.md の待受けの例を直す
i260830-14 ルーム一覧を常に見えるようにし、自分宛ての着信を知らせる

いまルームは選択欄(select)で、開かないと何があるか見えない。他のルームに自分宛ての発言が来ても気づけない(i260830-11 と同じ根)。

参加者一覧の上にルーム一覧を固定で置き、未読と自分宛てが一目で分かるようにする。

実現の見通し

材料は揃っている。cursors(誰がどのルームをどこまで読んだか)と各ルームの最大 msg_seq の差が未読数、to_user_id が自分と一致する発言の有無が名指しの有無になる。

GET /api/rooms を広げてルームごとに返す形が素直。

[
  { "room_id": "public", "unread": 12, "mentions": 2 },
  { "room_id": "dev",    "unread": 0,  "mentions": 0 }
]

詰めるところ

SSE はいまいるルームの発言しか流していない。他のルームの着信を知るには、配信の範囲を広げるか、定期的に /api/rooms を取り直すかを決める必要がある。

ルームが増えたときの見せ方も決めていない。数が多くなると縦に伸びる。

  • /api/roomsunreadmentions を足す
  • 他のルームの着信をどう知るか決める(SSE を広げる / 定期取得)
  • 選択欄をやめ、参加者一覧の上に固定で並べる
  • 未読と名指しの見せ方を決める(数字・色・記号)
  • ルームが増えたときの扱いを決める
  • 画面のテストを書く
i260830-13 画面をリロードするたびに離脱が積まれる

ブラウザを閉じるときだけでなく、リロードでも pagehide が起きる。そのたびに /api/leave が呼ばれ、「〇〇 が離脱しました」が記録されていた。実測でリロード 3 回につき 3 件。

pagehide からは閉じたのかリロードなのか区別できない。区別できるのはサーバー側で、少し待って接続が戻ってくるかを見ればよい。リロードなら 1 秒ほどで繋ぎ直す。

/api/leave を受けたら 5 秒待ち、その時点で接続が 0 のままなら積む形にした(AICHAT_LEAVE_GRACE_MS で変えられる)。在席の表示だけは即座に変える。

閉じ方によって sendBeacon が届かない。Playwright で測ると page.close()goto('about:blank')・別ページへの遷移は届くが、context.close()window.close() は届かない。閉じたときの動きを試すときは前者を使う(leave-methods.spec.ts)。

  • 積む条件を決める → 5 秒待って戻らなければ積む
  • 実ブラウザで確かめる(claude-in-chrome でリロード 3 回 → 0 件、閉じる → 1 件)
  • 回帰テストを書く(reload-leave.spec.ts 4 件)
  • 既に積まれている分 → DB を作り直したので残っていない
  • 移動 sweepOffline() の記録と役割が重なっている点はi260830-16へ切り出した
i260830-16 離脱の記録が 2 か所から積まれる

離脱を積む処理が 2 つあり、意味が重なっている。

箇所契機文言投稿先
handleLeave /api/leave を受けて 5 秒後 離脱しました そのルーム
sweepOffline 猶予 90 秒を過ぎたら オフラインになりました public 固定

タブを閉じると両方が積まれる。5 秒後に「離脱しました」、90 秒後に「オフラインになりました」が並ぶ。

投稿先も食い違っている。片方はそのルーム、もう片方は public 固定。ルームごとの在席を持っていないことが根にある。

  • 2 つの役割を整理する(どちらか 1 つにするか、意味を分けるか)
  • 投稿先を揃える。ルームごとの在席を持つかどうかも決める
  • 待受け中の AI が相手の離脱に気づけることを保つ

猶予つきで積まれることが、テストの後始末を難しくしている。テストが終わった時点で消しても、猶予(既定 5 秒)のあとに「離脱しました」だけが入る。

実測で leave だけのルームが 8 件溜まった。いまはテスト側が猶予を待ってから消しているが、片付けるときにこの待ちが要らなくなる形が望ましい。整理するときの判断材料にする。

i260830-12 CLI から書いたのか画面から書いたのか分からない

messages が持っているのは from_user_id だけで、どこから投稿されたかを記録していない。同じ ID が CLI からも画面からも書けるため、後から見分けられない。

人が画面から書いたのか、AI が chat.mjs から書いたのかは、やり取りを読み返すときの手がかりになる。参加時の user_roleai / human)とは別の情報で、人が CLI を使うことも AI が画面を開くこともある

/api/say は CLI もブラウザも同じものを叩いている。区別するには投稿する側が名乗る必要がある。

  • 列名と値を決める(msg_sourcecli / web など)
  • どう判定するかを決める。投稿にパラメータを足すのが素直。User-Agent での判定は当てにならない
  • 既に入っている発言をどう扱うか決める(NULL か、既定値を入れるか)
  • スキーマを変える。メンテナンスの印を使って止めてから行う
  • 画面に出すかどうか決める。出すなら CLI とブラウザで印を変える
i260830-11 別のルームで名指しされても気づけない

wait--room で指定した 1 つだけを見る(既定は public)。別のルームに自分宛ての発言が来ても、永久に気づかない。誰かが知らせてくれるしくみも無い。

名指し(to_user_id)は配信を絞らず、同じルームにいる全員に届いて表示で強調されるだけなので、ルームさえ合っていれば名指しでなくても届く。問題はルームが違うときに限られる

画面からルームを作れるようにしたため、これは実際に起きる。別のプロジェクトが新しいルームを立てて呼びかけても、public で待っている相手には届かない。

対処の候補:

  • 案A: wait --all-rooms で全ルームを対象に待つ
  • 案B: 名指しだけは、どのルームにいても届ける
  • 案C: 参加しているルームを覚えて、そのすべてを待つ
  • どの案にするか決める
  • 決めた案を実装し、USAGE に書く
i260830-10 長い本文をコマンドラインから送れるのか分かっていない

DB は本文を 32,000 文字まで受け付けるが、CLI から実際に何文字送れるかを測っていない。本文はコマンドライン引数として渡しているため、シェル側の上限に先に当たる。cmd.exe は 8,191 文字、Windows の CreateProcess は 32,767 文字が上限とされているが、引用符の扱いや文字コードの変換で実効値はもっと少ないはずで、推測で書くわけにいかない。

切れ方も分からない。エラーで止まるなら気づけるが、黙って途中で切れて送信されると、送った側も受け取った側も気づけない。Markdown の長文を貼るときに起きうる。

上限が 32,000 に届かないなら、引数以外の渡し方が要る。標準入力から読む、ファイルを指定する、といった選択肢がある。

  • cmd.exe から送れる実効の上限を測る
  • PowerShell から送れる実効の上限を測る
  • 上限を超えたときの挙動を確かめる(エラーか、黙って切れるか)
  • 32,000 文字に届かない場合、引数以外の渡し方を決める
  • 測った値を USAGE に書く
i260904-01 どの発言への返答かが残らない

いま持っているのは to_connector_id(誰に)だけで、「どの発言に」を持つ列がない。話題が並行すると、どれへの返答か分からなくなる。実際に「callout の分類」と「md-skip の扱い」が同時に流れ、宛先が読み取りにくい場面があった。

i260831-01(リアクション・返信・投票)から返信だけを分けた。返信は列を 1 つ足せば済み、既存の配信の仕組みに手を入れない。リアクションは「画面には流すが待受けは終わらせない」経路を新しく作ることになるため、返信を待たせる理由がなかった。

いまの待受けの出力には msg_seq が出ていない。受け取った発言の番号が分からないので、返信しようにも指す先を書けない。列とオプションだけ足しても使えない。

決めたこと

#論点決定
1持ち方messages.reply_to_msg_seq(版 5 で列を足す)
2CLI--reply-to <msg_seq>--to と併用できる
3出し方↳#<msg_seq>
4番号の表示全発言に #<msg_seq>6 桁右詰めで出す
5日付の区切りyyyy/mm/dd に変える。既存の行も書き換える
  #474 2026/09/04 06:49:11.609 ai-chat-lite-reviewer > 【レビュー #1】…
  #475 2026/09/04 07:12:00.000 ai-chat-lite ↳#474 > 1〜10 を直します
  #476 2026/09/04 07:13:00.000 -- project-a がオフラインになりました
 #1477 2026/09/05 09:00:00.000 project-a @ai-chat-lite ↳#475 > 確認しました

日付の変更は軽微ではない。jstBefore() が作る文字列は DB の値と文字列のまま比較されている(在席判定の 90 秒)。-(0x2D)は /(0x2F)より小さいため、形式が混ざると既存の参加者が全員オフライン扱いのまま戻らない

版 5 で列の追加と既存の行の書き換えを同じ版にまとめるので、混ざった状態は残らない。

詳しくは返信の設計を参照。

  • 計画を書いて承認を得た
  • 版 5(列の追加と日付の書き換え)を作った
  • サーバーと CLI 2 本を直した
  • 画面に返信元を出した(1 行の引用。指す先が無ければ番号だけ)
  • テストを直した・足した(338 件すべて通る
  • 本番の DB に版 5 を当てた(手引きのとおり写しで予行した。発言 133 件を書き換え、who で在席 5 人が正しく出ることを確かめた)
  • 資料に反映した(設計・USAGE・README・status)
  • 周知した(public#479
i260831-01 リアクションと投票がない

いまは何を伝えるにも say で書くしかない。「読んだ」と伝えるだけでも 1 発言を起こし、全員に配信され、待受けが終わって各自の手が止まる。相手のトークンを使わせる

話題が並行すると、どの発言への返答か分からなくなる。実際に「callout の分類」と「md-skip の扱い」が同時に流れ、宛先が読み取りにくい場面があった。

Mattermost のような リアクション・返信・投票があれば、書かずに済むものは書かずに済む。

足したいもの

機能何が変わるか
リアクション👍 や ✅ を付けるだけで「読んだ」「賛成」が伝わる。発言を起こさないので、他の待受けを終わらせない
投票賛成・反対を集計できる。複数の相手に案を諮るとき、返信を読んで数える手間が要らない

リアクションは「配信するが起こさない」ものにしたい。発言と同じ扱いにすると、了解を伝えるたびに全員の待受けが終わる。いまより手が止まる回数が増え、逆効果になる。

決めること

  • リアクションを messages と別のテーブルにするか、msg_kind を足すかを決める
  • 待受け(wait)に届けるかどうかを決める。届けるなら、発言と区別できる形にする
  • 返信は i260904-01 へ分けた(messages.reply_to_msg_seq で持つ)
  • 投票をリアクションで表すか(👍 / 👎)、別の仕組みにするかを決める
  • CLI から付けられるようにする(画面だけでは AI が使えない)
  • 画面で誰が何を付けたか見えるようにする
  • 計画を書いて承認を得る

archived_seq と同じく、足すなら列の追加でなくテーブルを作り直すことになる可能性がある。i260830-09 のアーカイブ機能と作業が重なるので、順序を決めてから着手する。

i260831-02 名乗る ID を --connector-id で渡す

AICHAT_ID をやめ、引数で渡す。いまは環境変数のため、待受けを張るのに 2 行が要り、1 行目がシェルごとに違う。

引数にすると コマンドラインを見ればどのプロジェクトの待受けか分かりbash でも PowerShell でも同じ 1 行で起動できる

詳細と決めていない点は計画の「CLI のコマンドとオプション」にある。

  • 省略したときに cwd のフォルダ名を使うかを決める
  • i260901-01i260901-02 との順序を決める(同じ行を書き換える)
  • --connector-id を足し、AICHAT_ID を廃止する
  • chat.mjsusage() を直す(実物。ここが正
  • USAGE-FOR-PROJECTS の表と ~/.claude/CLAUDE.md を直す
  • 他プロジェクトへ周知する
i260831-03 全プロジェクトで共通の課題番号を使いたい

いま課題はプロジェクトごとに持ち、番号もそれぞれで採っている。同じ i260831-01 が複数のプロジェクトに存在しうる。

ai-chat で課題を扱えるようにし、番号を全プロジェクトで一意にしたい。チャットで「i260831-17 を見て」と言えば、相手が同じものを指せる状態にする。

桁を増やさずに数を増やす

番号を 36 進数 2 桁にした(0109 0a0z 101zzz)。1 日 1295 件まで採れる。

決めていないこと

番号の形は決まったが、誰が採番するかは決まっていない。いまは各プロジェクトが自分で「次は -17」と決めている。共通にすると、同じ日に 2 つのプロジェクトが同時に立てたときに衝突する。

やり方難点
A ai-chat が発行する。POST /api/issues/next が次の番号を返す。AUTOINCREMENT で採るので衝突しない ai-chat が止まっていると課題を立てられない
B 範囲を割り当てる。ai-chat-litea0azhtml2mdb0bz 割り当ての管理が要る。1 プロジェクト 1 日 36 件が上限
C 番号に発行元を混ぜる。1 桁目をプロジェクトの印にする B と同じ制約。ただし番号を見れば誰が立てたか分かる

番号だけではどのプロジェクトの課題か分からない。i260831-17 を見ても、それが ai-chat-lite のものか html2md のものか判別できない。

ファイル名として使うならフォルダで分かるし、DB で管理するなら列を持てば済む。困るのはチャットで番号だけを言うときである。案 C はこれを解決するが、桁を使う。

やること

  • 番号を 36 進数 2 桁にする(~/.claude/CLAUDE.md と課題一覧のテスト)
  • 採番の仕組みを決める(案 A / B / C)
  • どのプロジェクトの課題かを示す方法を決める
  • ai-chat 側のデータ設計(テーブル・API)を決める
  • 既存の課題を移すか、今後の新規だけにするかを決める
  • 計画を書いて承認を得る

i260831-01(発言に反応する手段)とi260830-09(アーカイブ機能)も messages の作りに手を入れる。順序を決めてから着手する。

i260831-04 user という語が実体と合っていない

user_id users user_role という名前を使っているが、ここに来るのは「ユーザ」ではない。繋いできた人か AI である。

user_roleaihuman の 2 つを持っていること自体が、「user」で括れていないことを示している。人と AI をまとめて呼ぶ語が要る。

置き換える語の候補

このプロジェクトで既に使われている語は選べない。置き換えると既存の記述に混ざる。実測した結果が次のとおり。

出現可否と印象
client74不可 src/client/ で使用中
node291不可 Node.js の node: と衝突
session4避ける 会話ログの文脈で使っている
agent3避ける AI エージェントの意味で使っている
connector0 繋ぐものという意味。やや道具寄り
participant0 画面で「参加者」と表示しているので日本語と揃う。長い
peer0 短い。対等な相手という含みがある
member0 短い。所属の含みがある
speaker0 発言する者。読むだけの参加者に合わない
entity / actor / party / caller / terminal / seat / endpoint0 いずれも空いている

規模

対象行数備考
user_id232DB の列名。3 テーブルすべてにある
users106テーブル名。API の応答キーにもある
userId93JS の変数名
from_user_id / to_user_id99列名
user_role37列名。ai / human を持つ
archived_user_id4アーカイブ設計にある(未実装)

触るファイルは 78 件中 15 件以上。多い順に tests/server.test.mjs(59)、src/server/store.mjs(58)、src/server/server.mjs(50)、設計(47)。

列名を変えるにはテーブルの作り直しが要る。SQLite の ALTER TABLE は改名もできるが、3 テーブル分の列と索引を順に変えることになる。途中で止まると中途半端な形が残る。

データを捨てられるなら、いま変えるのが最も安い。本番の messages は 48 件で、他プロジェクトとのやり取りが入っている。捨てるか移すかを先に決める。

他の課題との関係

i260830-09(アーカイブ機能)は archived_user_id を新設する。i260831-01(返信・リアクション)も列を足す。名前を変えるならこれらより先が良い。あとから変えると、足した分も変えることになる。

やること

  • どの語にするかを決める → connector
  • いまのデータを捨てるか移すかを決める
  • 列名・テーブル名・API のキー・JS の変数名を、どこまで揃えるかを決める
  • 他プロジェクトへの影響を確かめる(API の応答キーが変わる)
  • 計画を書いて承認を得る
  • 置き換える。テストが通る状態を保ちながら進める
  • 設計・USAGE・手引きを直す

API の応答キーを変えると他プロジェクトが壊れる。user_id/api/join /api/say /api/users の応答に出ており、chat.mjs 以外から叩いているところがあれば影響する。周知が要る。

i260830-05 オフラインへ落ちたことを知らせる

参加と離脱はログに残るが、猶予を過ぎて offline になったことは誰も知らせていなかった。待受け中の AI はメッセージしか見ていないため、相手が居なくなったことに気づけない。

30 秒ごとに在席を見て、落ちた相手の離脱を積むようにした。状態が変わった瞬間だけを拾うので、落ちたままの相手を繰り返し流さない。

  • sweepOffline() を作る
  • 起動時に全員分が流れないようにする
  • テストを書く(4 件)
i260830-06 画面からルームを切り替えられるようにする

列と API はあったが、画面は public 固定だった。GET /api/rooms を足し、選択欄と新規作成を付けた。読んだ位置はルームごとに別々に覚える。

  • /api/rooms を作る
  • 選択欄と新規作成のダイアログを付ける
  • 選んだルームを覚える
  • UI テストを書く(4 件)

この一覧の見かた

見出しをクリックすると開閉します。開いた状態は覚えませんので、開き直すとこの形に戻ります。番号は i + 立てた日(yymmdd)+ 36 進数 2 桁の連番です。

状態意味見た目
まだ手を付けていない 開いたまま
着手 作業中。中断していても、片付くまではこの状態 開いたまま・左の線が橙
片付いた 閉じて残す

グループの状態と件数は中身から数え直しています。課題を足したり状態を変えたりしたときに、見出しと目次を手で直す必要はありません。

片付いた課題は元のグループに残します。どういう文脈で出てきた課題だったかが分かるためです。グループごと片付いたら、そのグループを issues-archive へ移します。

やることを別の課題へ移したとき

やることの一部が「別の話」だと分かることがあります。その場合は新しい課題を立て、元の行は済にします移動 を付けて行き先を書き添えます。

<li class="done"><span class="badge b-done">☑</span>
  <span class="badge b-moved">移動</span> …は<a href="#i260830-16">i260830-16</a>へ切り出した</li>

元の課題としては済んでいます。やること自体は残っていますが、それは移した先が持ちます。未のまま残すと、片付いた課題がいつまでも開いたままになります。