ステータス: 合意済み(2026-09-26、Issue #15、工程4: 詳細設計)。CODEXレビュー(#6623)を反映した後に合意した
更新するときは、設計文書の更新ルールに従うこと。
本書は、ランタイム管理 設計(合意済み)§4のうちBrainが担う部分(UC-B02 二重起動の拒否、UC-B03 正常終了)と、§7.7の停止シーケンスのうちBrain側の土台を、実装できる粒度に具体化する。範囲は実装計画書§5.1の#15である。
| 本書で扱う | 扱わない(担当) |
|---|---|
正式な起動・停止の方法(systemd --userのunit、起動口) |
ログイン後の自動起動、lingerの設定、WindowsからのWSLの起動(#21) |
| 単一起動(プロセスロック) | Brainの起動時刻・commit、状態取得API(#16) |
| 停止処理中の状態と、新しい会話要求の拒否 | 停止シーケンスの通知・応答(Brain側は#16、UI側は#23) |
| UC-B03のうちBrain側の土台 | UC-B03のうち停止の通知と応答(#16・#23。UC-B03全体は#23の完了で満たす) |
| 処理中の要求の完了待ちと打ち切り | ダッシュボードからの起動・停止(#25) |
| 保持資源の解放(lifespan、セッション掃除スレッド、セッション、ツール) | 自動再起動・自動復旧(#18) |
| 起動失敗・ロック取得失敗の診断 | |
| 単体テスト、実機確認、runbookのBrainの起動・停止手順 |
2026-09-25時点のコード(src/keina_assistant/server.py)の状態である。調査の詳細は#issuecomment-6614にある。
| 項目 | 現状 | 課題 |
|---|---|---|
| 起動 | uvicorn keina_assistant.server:app --host 127.0.0.1 --port 8811 を手動でnohup起動する |
正式な起動・停止の主体がない |
| import時の副作用 | モジュール末尾のapp = create_app()で、import時にセッション掃除スレッドが起動する |
テストでimportしても起動する。停止できない |
| セッション掃除スレッド | daemon=Trueの無限ループ(5分ごと) |
停止の通知を受けて終わる構造になっていない |
| 終了処理 | FastAPIのlifespanがない | セッションのOllamaChat、ツールのhttpx.Clientを閉じていない |
| 二重起動 | 専用の仕組みがない | 同じポートなら偶然失敗するが理由が分かりにくい。別のポートなら2個目が起動できる |
| 設定の検証 | Settings.validate()は起動時に呼ばれていない |
BRAIN_AUTH_TOKENが未設定でも起動し、全要求が401になる |
| 1つの要求の長さ | Ollama呼び出し1回のタイムアウトは120秒。ツール呼び出し(最大3回)と再試行(1回)がある | 完了を待つだけでは、停止に数分かかることがある |
systemd --userのunitで起動・停止するBrainの正式な起動・停止は、systemd --userのサービスkeina-brain.serviceで行う(#issuecomment-6615の判断A1)。
systemctl --user start keina-brainsystemctl --user stop keina-brain(systemdがSIGTERMを送り、§6の停止シーケンスが始まる)systemctl --user status keina-brainjournalctl --user -u keina-brainWSL2ではsystemdが有効になっており(/etc/wsl.confの[boot] systemd=true)、ユーザーのサービスマネージャも動いていることを確認した(#issuecomment-6614)。
/admin/shutdownのようなHTTPでの停止は設けない(Issue #15の方針)。Brainが不調でも、実行環境の管理手段から止められるようにするためである。ダッシュボードからの起動・停止は#25で検討する。その場合も、ダッシュボードはこのunitを操作する。
新しい起動口python -m keina_assistant(src/keina_assistant/__main__.py)を設ける。unitもrunbookの手動起動も、これを使う。
起動口は次の順で処理する。
Settings.load())、検証する(Settings.validate())。読み込み・型変換・検証のどこで誤りが見つかっても、設定の誤りとして終了する(§8、§9)create_app())。この時点ではスレッドを起動しない(§7.1)uvicorn keina_assistant.server:appでの起動はできなくする(モジュール末尾のapp = create_app()を削除する)。ロックを通らない起動経路を残さないためである。テストは既にcreate_app()を直接呼んでおり、影響しない。
リポジトリにdeploy/systemd/keina-brain.serviceとして置き、systemctl --user linkで登録する(手順はrunbook)。
[Unit]
Description=Keina Brain (AiChatWls)
[Service]
Type=exec
WorkingDirectory=%h/develop/AiChatWls
ExecStart=%h/develop/AiChatWls/.venv/bin/python -m keina_assistant
Environment=PYTHONUNBUFFERED=1
KillSignal=SIGTERM
TimeoutStopSec=60
Restart=no
[Install]
WantedBy=default.target
Type=exec: 実行ファイルが見つからない等、起動そのものの失敗をsystemctl startの時点で失敗として返すWorkingDirectory: .envを読むために、リポジトリのディレクトリで動かす(Settings.load()は作業ディレクトリの.envを読む)ExecStart: Pythonの環境はプロジェクトの決まりどおりuvで作る(uv sync --frozen。runbook)。unitからは、その環境のpythonを直接起動する。uv runを挟むと、systemdが終了シグナルを送る相手とロックを持つプロセスが別になるためであるTimeoutStopSec=60: 停止にかかる時間の上限(§9の式)より長くする。これを過ぎるとsystemdがSIGKILLで強制終了する。正常終了の手段ではなく、停止シーケンス自体が止まった場合の最後の安全装置であるRestart=no: 異常終了時に自動で再起動しない。自動復旧は設計書§13で対象外としている(#18)[Install]: systemctl --user enable(自動起動)は#21で行う。#15ではstart・stopだけを使う待ち受けアドレスとポートを設定に加える(BRAIN_HOST、既定127.0.0.1。BRAIN_PORT、既定8811)。既定値は現在の運用(ui-brain-protocol.md§3)と同じである。
起動口で、ロックファイルに排他ロック(fcntl.flock(LOCK_EX | LOCK_NB))をかける。取得できなければ、2個目のBrainとして理由を示して終了する(#issuecomment-6615の判断B1)。
kill -9)やクラッシュでもOSがロックを外す。ロックファイル自体は残るが、次の起動はロックを取得できる。実機で確認した(#issuecomment-6615)。設計書§4.2の「既存プロセスが(正常・異常を問わず)終了していれば、次の起動は拒否されず成立する」を満たす却下した案: ポートの使用中で判定する(別のポートで2個目が動く)。systemdだけに任せる(手動起動に効かない)。PIDファイルの有無で判定する(異常終了後にファイルが残り、起動できなくなる)。
$XDG_STATE_HOME/keina-brain/brain.lock(XDG_STATE_HOMEが未設定なら~/.local/state/keina-brain/brain.lock)BRAIN_LOCK_FILEで変更できる(テストで一時ディレクトリを使うため)/mnt/c等のWindows側のドライブではflockが当てにならないため、そこを指定しない(runbookに書く)ロックの単位は「実行ホスト上の同じユーザー」である。このPCでBrainを動かすユーザーは1人なので、実行ホストごとに1つと同じ意味になる。
os.open(path, O_RDWR | O_CREAT, 0o600))。wモードやO_TRUNCで開くと、2個目がロックの取得に失敗する前に、1個目が書いた診断情報を消してしまうためであるseek(0))、切り詰め(truncate())、自分のPIDと起動時刻を書いてflushする。これは診断の表示用であり、起動できるかどうかの判定には使わない2個目は、標準エラー出力(systemd経由ならjournal)に次を出し、終了コード3で終わる(§8)。
Brainは既に起動しているため、起動を中止しました。
ロックファイル: /home/akira/.local/state/keina-brain/brain.lock
起動中のBrain(参考): PID 12345、起動時刻 2026-09-25T13:00:00+09:00
停止するには: systemctl --user stop keina-brain
「起動中のBrain」はロックファイルの中身を読んで表示する。読めなければ「不明」とする。
Brainのプロセスは、次の状態を順に進む。戻ることはない。
| 状態 | 意味 | 会話要求 | /health/live |
/health/ready |
|---|---|---|---|---|
| 起動中 | lifespanの開始処理中 | (受け付ける前) | — | — |
| 稼働中 | 通常の動作 | 受け付ける | 200 | 依存先による(現行どおり) |
| 停止処理中 | 終了シグナルを受けた後、§6の停止シーケンスの途中 | 断る(§6.2) | 200 | 503(停止処理中) |
| 停止 | lifespanの終了処理を終えた | — | — | — |
app.state)。状態取得API(#16)はこれを表示するsessions_guardを持った状態で行う(§6.2の受付ゲートと同じロック)/health/liveは「プロセスが応答できるか」を示すので、停止処理中も200を返す。/health/readyは「会話に使えるか」を示すので、停止処理中は503を返す終了シグナル(SIGTERM、または前景実行でのCtrl+CによるSIGINT)を受けると、次の順で進む。
SIGTERM
│
├─ 1. 停止処理中に入る ──── 以後の新しい会話要求を断る(6.2)
│
├─ 2. 並行して待つ(どちらも上限あり)
│ ├─ a. UIの応答待ち(6.3。#15では待つ相手がいないので、すぐ終わる)
│ └─ b. 処理中の会話要求の完了待ち(6.4)
│
├─ 3. 上限を過ぎて残った要求を打ち切る(6.5)
│
├─ 4. 待ち受けを閉じる(uvicornの終了処理)
│
├─ 5. 保持資源を解放する(lifespanの終了処理。§7)
│
└─ 6. 終了コード0で終わる(ロックはOSが外す)
TimeoutStopSec(§3.3)を過ぎたら、systemdがSIGKILLで強制終了する。停止シーケンスが何かの理由で止まっても、停止できなくなることはない会話プロトコルのエンドポイント(/ask、/clear_history、/session/{session_id}/keepalive)は、停止処理中に届いた要求を処理せず、次を返す。
受付ゲート: 「停止処理中かどうかの確認」と「処理中件数の増加」は、sessions_guardを持った状態で一体として行う(_acquire_session()と、/clear_historyがセッションをピン留めする箇所)。停止処理中への切り替えも同じsessions_guardの下で行う(§5)。これにより、要求は「切り替えの前に受け付けられ、§6.4の完了待ちの対象になる」か「切り替えの後に来て、503で断られる」かのどちらかになり、どちらにも入らない要求は生じない。sessions_guardを持ったままsession.lockを取る箇所は作らないので、ロックの順序によるデッドロックは起きない(現行の_acquire_session()の方針と同じ)。
503 Service UnavailableRetry-After: 5{"detail": "Brainは停止処理中です", "reason": "brain_stopping"}理由:
503とRetry-Afterは、一時的な失敗であることを表す。会話プロトコルは4xxを再試行しないエラーと定めており(ui-brain-protocol.md§9)、503はそれに当たらないので、UIは再試行の対象にできる。実際に再試行するか、何回・どの間隔で行うかは、UI固有の判断である(同§9)。Brainの再起動後に会話を続けられることをUIに求める場合は、UI側の契約とテストで定める(#23)ui-brain-protocol.mdの§4・§9に、この応答を追記した(§11.3)。
管理プロトコルのエンドポイント(heartbeat・終了通知・停止シーケンスへの応答。#16で追加)は、停止処理中も受け付ける。UIからの応答と終了通知を受け取るためである。
設計書§7.7のとおり、BrainはheartbeatでUIに停止処理中を伝え、UIごとの応答待ち時間まで応答を待つ。
#15では、停止シーケンスの中に「UIの応答待ち」の段階を設けるだけにする。heartbeatがまだないため、待つ相手は常に0件で、この段階はすぐ終わる。通知と応答の形式、応答待ち時間を数え始める時点は#16の詳細設計で決める(§12)。
#16の詳細設計(runtime-management-detail-observation.md§7)で、通知と応答の形式を決めた。応答待ちは、UIに通知が届いた時点(次のheartbeatに停止処理中を返した時点)から数え、全体の上限は30秒(BRAIN_SHUTDOWN_UI_WAIT_SEC)とする。§9の式では max(30, 30) + 20 = 50秒となり、TimeoutStopSec=60は変えない。
/ask・/clear_history。セッションのロック待ちを含む)が0件になるまで待つsessions_guardの下で読み書きする)。セッションごとのactive_requestsの合計にしないのは、終了処理でセッションを辞書から取り出した後(§7.2)も、打ち切った要求の件数を数える必要があるためである。受付ゲートにより、停止処理中に入った後にこの件数が増えることはないBRAIN_SHUTDOWN_DRAIN_SEC(暫定値30秒。運用者が同意した)。音声UIで利用者を待たせられる長さを基準にした値であり、運用で調整する上限を過ぎても処理中の要求が残っている場合は、それらの完了を待たずにプロセスを終える。
os._exit(0))httpx.Clientを閉じるだけでは打ち切れない: 使用中のクライアントを別のスレッドから閉じても、通信中の要求は終わらないことを実機で確認した(close()はすぐ戻るが、応答を待つ要求は10秒待っても終わらなかった。#6623への対応)。スレッドプールの作業スレッドはdaemonではないため、何もしなければPythonの終了処理がそのスレッドを待ち、Ollamaのタイムアウト(120秒)まで終われないOllamaChat.ask()は最後まで成功したときだけ会話履歴に1組を追加し、冪等性キャッシュ(last_request_id等)も成功時だけ更新する。打ち切った要求の履歴は、どのみち再起動で消える。#24で登録情報をファイルに書くようになるが、その書き込みは破損から戻せる設計になっている(設計書§3.1.1)TimeoutStopSecによる強制終了(SIGKILL)は、この打ち切りの手段ではない。停止シーケンス自体が止まった場合の最後の安全装置である却下した案: Ollamaとの通信をストリーミングにし、区切りごとに打ち切れるようにする(会話処理llm.pyの作り直しが大きい)。使用中のhttpx.Clientを閉じて打ち切る(上記のとおり打ち切れない)。
uvicorn(uv.lockで0.53.0)は、終了シグナルを受けると直ちに待ち受けを閉じる。これでは6.2の「理由を付けて断る」ができない。そこで、uvicorn.Serverを継承し、終了シグナルの扱い(handle_exit)を差し替える。
uvicorn標準のhandle_exitは、受けたシグナルを_captured_signalsに積み、終了処理の後でcapture_signals()が元のハンドラへ送り直す。標準の処理をそのまま呼ぶと、停止シーケンスを終えた後にSIGTERMで終わることになり、終了コード0にならない。そこで、差し替えたhandle_exitは標準の処理を呼ばず、_captured_signalsにも積まない。
should_exit)をすぐには立てず、停止シーケンス(6.1の1〜3)を非同期タスクとして始めるos._exit(130)で終える。uvicornのforce_exitは接続を待つのをやめるだけで、処理中の要求のスレッドが残っているとPythonの終了処理がそれを待ってしまうため(実装時にテストで確認した)。2回目のSIGTERMは、ログに残すだけで何もしないshould_exitを立て、uvicornの通常の終了処理(待ち受けを閉じる、lifespanの終了処理)に進むtimeout_graceful_shutdownは5秒にする。処理中の要求は6.4〜6.5で既に扱いを決めているためFastAPIのlifespanを設ける。
os._exit(0)で終えるスレッドの起動をcreate_app()からlifespanへ移すことで、import時やアプリを作っただけの時点ではスレッドが動かない。テストでは、TestClientをwith文で使ったときだけlifespanが動く。
sessions_guardの下では、辞書からすべてのセッションを取り出すだけにするchat.close()は、sessions_guardを離してから呼ぶ(#issuecomment-6615の判断D1)セッション掃除スレッドの_sweep_once()(ガードの中でchat.close()を呼んでいる)は変更しない(判断D1)。close()は接続を閉じるだけで通信の完了を待たず、ガードを持つ時間はごく短いため、実害がない(#issuecomment-6614)。
LocationTool・WeatherTool・WebSearchToolは既にclose()を持つ。create_app()が作ったツールを一覧として持ち、lifespanの終了処理でそれぞれのclose()を呼ぶ。ツールの一覧(ToolRegistry)は呼び出し用の関数だけを持ち、ツールのオブジェクトを持たないため、ここには閉じる手段を加えない。
threading.Eventで停止を伝える。ループはwhile not stop_event.wait(SWEEP_INTERVAL_SEC)とし、停止の通知を受けたら次の周期を待たずに終わるjoin、上限5秒)daemon=Trueはそのまま残す。万一終了を待ちきれなくても、プロセスの終了を妨げないためである起動に失敗したときは、理由を標準エラー出力(systemd経由ならjournal)に出し、原因ごとに異なる終了コードで終わる。systemctl --user status keina-brainで、終了コードと直近のログを確認できる(設計書§4.3の「実行環境の正式な管理手段」)。
| 終了コード | 原因 | 表示する内容 |
|---|---|---|
| 0 | 正常終了(停止シーケンスを終えた。処理中の要求を打ち切った場合を含む) | 打ち切った場合は、その件数 |
| 1 | 上記以外の失敗(ポートを開けない等、uvicornやPythonの例外) | 例外の内容。ポートを開けないときは、アドレス・ポートと「別のプログラムが使用中の可能性」 |
| 2 | 設定の誤り(読み込み・型変換・Settings.validate()のいずれか。§9) |
誤りの一覧(例: BRAIN_AUTH_TOKEN を設定してください、BRAIN_PORT は 1〜65535 の整数にしてください(指定値: abc)) |
| 3 | 二重起動(ロックを取得できない) | §4.4 |
| 4 | ロックファイルを作れない・開けない(権限等) | ロックファイルのパスとOSのエラー |
| 130 | 停止シーケンスの途中で2回目のSIGINTを受けた(前景実行での強制終了。§6.6) | 「後始末を待たずに終了します」 |
設定の検証を起動時に行うようにすることで、BRAIN_AUTH_TOKENが未設定のまま起動し、全要求が401になる状態(§2)は起きなくなる。
.envまたは環境変数で指定する。既存の読み込み方(Settings.load())に加える。
| 名前 | 既定値 | 用途 |
|---|---|---|
BRAIN_HOST |
127.0.0.1 |
待ち受けアドレス(§3.4) |
BRAIN_PORT |
8811 |
待ち受けポート(§3.4) |
BRAIN_LOCK_FILE |
$XDG_STATE_HOME/keina-brain/brain.lock |
ロックファイル(§4.2) |
BRAIN_SHUTDOWN_DRAIN_SEC |
30(暫定) |
処理中の会話要求の完了待ちの上限(§6.4) |
各項目の値の条件は次のとおりとし、満たさなければ設定の誤り(終了コード2)とする。
| 名前 | 条件 |
|---|---|
BRAIN_HOST |
空でない |
BRAIN_PORT |
1〜65535の整数 |
BRAIN_LOCK_FILE |
絶対パス。/mnt/の下(Windows側のドライブ)でない(§4.2) |
BRAIN_SHUTDOWN_DRAIN_SEC |
0以上の有限の数 |
既存の数値の項目(MAX_HISTORY_TURNS、TOOL_HTTP_TIMEOUT_SEC、LOCATION_LATITUDE等)も、数値に変換できなければ設定の誤りとする。現行のSettings.load()は変換の例外をそのまま投げるため、load()で変換の誤りを集め、validate()の誤りと一緒に表示する形に改める。
停止にかかる時間の上限は、次の式で見積もる。UIの応答待ちと処理中の要求の完了待ちは並行して行うので、長い方を取る(§6.1)。
停止にかかる時間の上限 = max(UIの応答待ちの上限, BRAIN_SHUTDOWN_DRAIN_SEC)
+ timeout_graceful_shutdown(5秒)
+ 掃除スレッドのjoinの上限(5秒)
+ 資源の解放と余裕(10秒)
#15の時点では、UIの応答待ちの上限は0秒なので、30 + 5 + 5 + 10 = 50秒となり、TimeoutStopSec=60に収まる。BRAIN_SHUTDOWN_DRAIN_SECを変えたとき、および#16でUIの応答待ちの上限を決めたときは、この式でTimeoutStopSecを見直す(runbookに書く。#16の範囲にも含める)。
| 対象 | 確認すること |
|---|---|
| ロック | 1個目が取得でき、2個目が拒否される。1個目のプロセスをkill -9で終わらせると、次が取得できる(子プロセスで確認)。ロックファイルの中身にPIDと起動時刻が書かれる。2個目が取得に失敗しても、1個目が書いた中身が消えない |
| 起動口 | 設定の誤り(検証の誤り、数値に変換できない値、§9の条件を満たさない値)で終了コード2、二重起動で3、ロックファイルを作れないときに4で終わる。いずれもアプリを作らず、ポートを開かない |
| 停止処理中 | 会話要求が503・reason: brain_stopping・Retry-Afterで断られる。認証のない要求は401のまま。/health/liveは200、/health/readyは503 |
| 受付ゲート | 停止処理中への切り替えと要求の受付を同時に起こしても、各要求が「完了待ちの対象」か「503」のどちらかになる(切り替えの直前・直後に要求を割り込ませる競合テスト) |
| シグナル | 1回目のSIGTERM・SIGINTで停止シーケンスが始まり、should_exitがすぐには立たない。2回目のSIGINTで強制終了、2回目のSIGTERMは無視される。停止シーケンスの後にシグナルが送り直されず、終了コード0で終わる |
| 完了待ち | 処理中の要求が上限内に終われば、その応答が返り、打ち切りが起きない |
| 打ち切り(判断基準) | 応答を返さない偽のOllamaへ要求を送った状態で停止すると、BRAIN_SHUTDOWN_DRAIN_SECを過ぎた後、後始末(掃除スレッドの停止、セッションとツールのclose())を終えてから、終了コード0でプロセスが終わる(子プロセスで確認)。打ち切った件数がログに出る |
| lifespan | 終了処理で、掃除スレッドが止まり、全セッションとツールのclose()が呼ばれる。chat.close()はsessions_guardの外で呼ばれる |
| import | keina_assistant.serverをimportしても、スレッドが起動しない |
| ユースケース | 手順 | 期待結果 |
|---|---|---|
| UC-B02 | systemctl --user start keina-brainで起動した後、端末でuv run python -m keina_assistantを実行する |
2個目が§4.4の表示とともに終了コード3で終わる。1個目は動き続ける |
| UC-B02(異常終了後) | 起動中のBrainをkill -9で終わらせ、systemctl --user start keina-brainを実行する |
起動できる |
| UC-B03(Brain側の土台) | 音声UIまたはcurlで時間のかかる/askを送っている間にsystemctl --user stop keina-brainを実行する |
停止処理中に送った会話要求が503で断られる。処理中の要求は、上限内なら応答が返る。上限を過ぎれば打ち切られる。どちらの場合も、SIGKILLではなく終了コード0で終わる(systemctl --user statusでstatus=0/SUCCESS)。journalに停止シーケンスの各段階が残る |
UC-B03のうち、観測中のUIへの停止の通知と応答(設計書§7.7)は、#16・#23の実機確認で行う。UC-B03全体は#23の完了で満たす(実装計画書§4)。
runtime-management-design.md)判断C(停止シーケンス)により、合意済みの設計を次のとおり改訂する(本書と同時に行う。経緯は設計書§14)。
runtime-management-implementation-plan.md)判断C.3(分担)により、§5.1の範囲を改める(本書と同時に行う)。
TimeoutStopSecを見直すことを含める(指摘5)ui-brain-protocol.md)停止処理中の503(§6.2)を、§4(エンドポイント一覧)と§9に追記した(2026-09-26。運用者が同意した)。
runtime-management-runbook.md)新規に作成し、Brainの節に次を書く。#21はこれを使って、自動起動・更新・回復の通しの手順をまとめる。
uv sync --frozen)systemctl --user link、daemon-reload)uv run python -m keina_assistant、Ctrl+Cで停止)BRAIN_SHUTDOWN_DRAIN_SECを変えたときのTimeoutStopSecの見直し(§9の式)なし。本書の合意時に残っていた「UIの応答待ち時間を数え始める時点」は、#16の詳細設計で「UIに通知が届いた時点から数える」と決めた(§6.3、§13)。
| 日付 | 決めたこと | 理由 | 却下・置き換えた案 | 根拠 |
|---|---|---|---|---|
| 2026-09-25 | Brainの正式な起動・停止をsystemd --userのunitで行う |
WSL2でsystemdが有効であり、Brainが不調でも実行環境から止められる。停止時のシグナル・待ち時間の上限・強制終了をsystemdに任せられる | nohup起動とスクリプトによる停止(停止がBrain側の作り込みに頼り、不調時に止めにくい)。/admin/shutdown(Issue #15の方針で不採用) |
#6614、#6615 |
| 2026-09-25 | 二重起動をflockで拒否する | 異常終了してもOSがロックを外すため、古い情報で起動できなくなることがない(実機で確認)。手動起動や別ポートでの起動にも効く | ポートの使用中での判定。systemdだけに任せる案。PIDファイル | #6615 |
| 2026-09-25 | 停止時は各UIとの停止シーケンスを持つ(管理プロトコルの拡張)。停止を知ったUIの対応はUI層ごとに決め、BrainはUIごとの応答待ち時間(デフォルト1秒)まで待つ。#15は停止処理中の状態と新しい会話要求の拒否までを実装する | 運用者の判断 | 処理中の要求の完了を上限30秒まで待ち、UIには何も伝えない案(C1)。完了まで数分待つ案(C2) | #6615 |
| 2026-09-25 | 終了時のセッションの解放は、ガードの中で取り出し、ガードの外で閉じる。セッション掃除スレッドは変更しない | close()は接続を閉じるだけで実害はほぼないが、終了時の処理は正しい形で作る。掃除スレッドは会話プロトコルの範囲であり、変えなくても実害がない |
掃除スレッドとui-brain-protocol.mdの疑似コードも直す案(D2) |
#6614、#6615 |
| 2026-09-25 | 起動口python -m keina_assistantを設け、uvicorn keina_assistant.server:appでの起動をできなくする。起動時に設定を検証する |
ロックを通らない起動経路を残さない。起動失敗を原因ごとに分かる形にする | モジュール末尾のappを残す案(ロックを迂回できる) |
#6624(2026-09-26に運用者が合意) |
| 2026-09-25 | 停止処理中の会話要求に503・reason: brain_stopping・Retry-Afterを返す |
理由を示して断る(判断C.3)。5xxは会話プロトコルで再試行の対象であり、Brainの再起動後に会話を続けられる |
待ち受けを閉じて接続を拒否する案(uvicornの既定の動作。理由が伝わらない) | #6624(2026-09-26に運用者が合意) |
| 2026-09-26 | 上限を過ぎた要求は、後始末を終えた後、完了を待たずにos._exit(0)でプロセスを終えて打ち切る |
使用中のhttpx.Clientを閉じても通信中の要求は終わらないことを実機で確認した。会話の状態はすべてメモリにあり、途中で終えても壊れるものがない。運用者の判断 |
使用中のhttpx.Clientを閉じて打ち切る(草案の記述。本行で置き換えた)。ストリーミングにして区切りごとに打ち切る(作り直しが大きい)。systemdのSIGKILLに任せる(後始末が行われず、正常終了にならない) |
#6623(指摘2) |
| 2026-09-26 | CODEXレビューの残り7件を反映した。受付ゲート(停止処理中の確認と処理中件数の増加をsessions_guardの下で一体に行う)、uvicornのシグナルを送り直さない差し替え、停止にかかる時間の式(並行部分は長い方)、503の再試行の表現、設定の読み込み・型変換の誤りを終了コード2に含めることと値の条件、ロックファイルを切り詰めずに開く手順、UC-B03の分担 |
いずれも、実装時の取り違えや、#15の完了をUC-B03全体の完了と誤って判断することにつながるため | 草案の各記述(本行で置き換えた) | #6623(指摘1、3〜8) |
| 2026-09-26 | unitからはuv syncで作った環境のpythonを直接起動する |
Pythonの環境管理はuvを使うのがプロジェクトの決まり。uv runを挟むと、終了シグナルを受けるプロセスとロックを持つプロセスが別になる |
unitのExecStartでuv runを使う案 |
運用者の指示(2026-09-26) |
| 2026-09-26 | 本書に運用者が合意した。あわせて、ui-brain-protocol.mdへの503の追記と、BRAIN_SHUTDOWN_DRAIN_SECの暫定値30秒に同意を得た |
CODEXレビュー(#6623)の反映(#6624)を経て、確認事項3点がすべて了承された | — | #6624(運用者の同意、2026-09-26) |
| 2026-09-26 | 実装に合わせて細部を改めた。処理中件数は、セッションごとのactive_requestsの合計ではなく、アプリ全体のカウンタで数える。ツールはcreate_app()が一覧として持ち、閉じる。2回目のSIGINTではos._exit(130)で直ちに終える |
終了処理でセッションを取り出した後も、打ち切った件数を数える必要があった。ToolRegistryはツールのオブジェクトを持たない。uvicornのforce_exitでは、処理中の要求のスレッドがPythonの終了を止めることをテストで確認した |
active_requestsの合計で数える案。ToolRegistryに閉じる手段を加える案。2回目のSIGINTでforce_exitを立てる案(いずれも本書の合意時の記述。本行で置き換えた) |
実装時の確認(#15の実装の記録に残す) |
| 2026-09-26 | 処理中の会話要求の完了待ちで、待ち始めの件数と、すべて完了したことをログに出す(§6.4) | 実機確認で、要求の完了を待った場合も「処理中の会話要求はありません」とだけ出て、journalから待ったことが読み取れなかった。運用者が修正を了承した | — | #6630(気づいた点2) |
| 2026-09-26 | UIの応答待ちは、UIに通知が届いた時点から数える。全体の上限は30秒とし、TimeoutStopSec=60は変えない(§6.3、§12) |
#16の詳細設計で決めた(運用者の判断)。停止処理中に入った時点から数えると、heartbeatの間隔より短いため、ほとんどのUIに停止が伝わらない | 停止処理中に入った時点から数える案。§12の未決事項(本行で解決した) | #16 #6644、#16 #6646 |