ステータス: 合意済み(2026-09-26、Issue #16、工程4: 詳細設計)。CODEXレビュー(#6645)と再レビューを反映した後に合意した(#6646)
改訂: 2026-09-26 #24の詳細設計の合意に合わせ、§3.1・§4.6・§8.1に、管理プロトコル版2と状態取得schema_version 2(runtime-management-detail-registration.md)への参照を加えた(#24 #6710)
更新するときは、設計文書の更新ルールに従うこと。
本書は、ランタイム管理 設計(合意済み)の§3.2・§6・§7・§8のうちBrainが担う部分を、実装できる粒度に具体化する。範囲は実装計画書§5.1の#16である。UI側(#23、AiChatリポジトリ)は、本書の§4(管理プロトコル)と§11(host_idの算出規則)に従って実装する。
ui-brain-protocol.mdへの参照の追記、ログの形式)| 本書で扱う | 扱わない(担当) |
|---|---|
| 管理heartbeat・終了通知・停止への応答の受信と、その形式(UIとの契約) | UI側の送信(音声UIは#23)、UIのローカル排他(#19) |
| プロセスの記録、状態の6軸の導出、表示上の状態 | 常駐監視対象の登録・解除、永続化、復帰警告(#24)。本書は登録一覧を受け取る口だけを定める(§6.8) |
| 状態取得API(閲覧トークンで認可) | 管理トークンと書き込みAPI(#24)、ダッシュボード画面(#17) |
| Brain自身の起動時刻・commit・人向けversion | 自動起動(#21) |
| 停止シーケンスの通知と応答(Brain側) | 停止を知ったUIの対応(#23) |
| 観測の出来事のログ | ロギング全体の方針(#12) |
host_idの算出規則(全コンポーネント共通) |
macOS・コンテナでのhost_idの取得(実装計画書§6) |
| 検証用のテストUI(スクリプト) | |
| 認証をエンドポイントの種類ごとに付け替えること |
2026-09-26時点のコード(src/keina_assistant/server.py)の状態である。
| 項目 | 現状 | 課題 |
|---|---|---|
| 認証 | アプリ全体の依存(FastAPI(dependencies=[Depends(_verify_token)]))として、全エンドポイントを会話用トークンで判定する |
状態取得は閲覧トークンで認可する(設計書§8.4)。エンドポイントの種類ごとに認証を分ける必要がある |
/docs・/openapi.json |
FastAPIの既定で有効。今はアプリ全体の認証がかかっている | 認証をエンドポイントごとにすると、認証なしで公開されてしまう |
/health/ready |
呼ぶたびにhttpx.Clientを作り、閉じていない |
状態取得でも同じ判定を使う(設計書§8.1)ため、共通の関数に切り出す |
| UIプロセスの観測 | 仕組みがない | 本書で作る |
| 停止シーケンスのUIの応答待ち | _wait_for_ui_acks()はすぐ戻る(brain-lifecycle詳細設計§6.3) |
本書§7で実装する |
| ログ | logging.basicConfigで標準エラー出力へ出し、systemd経由ではjournalに入る(#15)。#12の方針は決まっていない |
観測の出来事の形式を本書で決める(§10) |
| メソッド・パス | 用途 | 認証 | 停止処理中 |
|---|---|---|---|
POST /runtime/heartbeat |
管理heartbeat(§4.1) | 会話用トークン | 受け付ける。応答で停止処理中を伝える |
POST /runtime/exit |
終了通知(§4.4) | 会話用トークン | 受け付ける |
POST /runtime/stop-ack |
停止への応答(§4.5) | 会話用トークン | 受け付ける |
GET /runtime/status |
状態取得(§8) | 閲覧トークン(#24以降は管理トークンも。registration詳細設計§3.2) | 受け付ける。brain.stateがstoppingになる |
protocol_versionで申告する(設計書§7.3のエンベロープ)。状態取得の応答にもschema_versionを持たせる(§8.1)/ask等)が停止処理中に503を返すのとは異なるアプリ全体の依存をやめ、エンドポイントの種類ごとに認証の依存を付ける(FastAPIのAPIRouterのdependencies)。
| 種類 | エンドポイント | 受け付けるトークン |
|---|---|---|
| 会話 | /ask、/session/{session_id}/keepalive、/clear_history、/health/live、/health/ready |
会話用トークンBRAIN_AUTH_TOKEN(今と同じ) |
| 管理プロトコル(UIが送る) | /runtime/heartbeat、/runtime/exit、/runtime/stop-ack |
会話用トークン(設計書§7.6) |
| 状態取得(人が見る) | /runtime/status |
閲覧トークンBRAIN_VIEW_TOKEN。#24で管理トークンを加える(設計書§9.1: 管理トークンは閲覧もできる) |
401にする(今の_verify_tokenと同じ)。BRAIN_VIEW_TOKENが未設定でもBrainは起動し、状態取得だけが使えない。起動時のログで知らせる(運用者の判断、#6644の2)BRAIN_VIEW_TOKENがBRAIN_AUTH_TOKENと同じ値なら、設定の誤り(終了コード2)とする401・{"detail": "invalid or missing token"}を返す。どのトークンが要るかは応答に書かない/docs・/redoc・/openapi.jsonは無効にする(FastAPI(docs_url=None, redoc_url=None, openapi_url=None))。認証なしでエンドポイントの一覧を公開しないためである401になることを単体テストで確かめる(§14.1)UIとBrainの間で交わすJSONの形式である。#23(音声UI)とテストUI(§12)は、この契約に従う。
POST /runtime/heartbeat
{
"protocol_version": 1,
"host_id": "3f5c…(64文字の16進数)",
"ui_kind": "voice",
"target_id": "default",
"process_instance_id": "8b1f3c2e-5a7d-4e0b-9c61-2f4d8a9e7b10",
"heartbeat_interval_sec": 15,
"expiry_sec": 45,
"instance_policy": "host:1",
"started_at": "2026-09-26T09:00:00+09:00",
"commit": "700665af0c3e…",
"unresponsive_retention_sec": 600,
"stop_ack_timeout_sec": 1,
"host_label": "DESKTOP-ABC",
"app_version": "1.4.0"
}
前半の8項目がエンベロープ(設計書§7.3。どの版でも名前・形式・意味を変えない)、後半が版1の項目である。
| フィールド | 必須 | 形式 | 意味 |
|---|---|---|---|
protocol_version |
必須 | 1以上の整数 | プロトコル版(§4.6) |
host_id |
必須 | 64文字の16進数(小文字) | 実行ホスト(§11) |
ui_kind |
必須 | 1〜64文字の[a-z0-9._-] |
UIの種類 |
target_id |
必須 | 1〜64文字の[A-Za-z0-9._-] |
同じホスト・同じ種類の中の枠。単一ならdefault |
process_instance_id |
必須 | UUID文字列(UIはUUID v4で作る) | このプロセスの識別子(起動のたびに変わる) |
heartbeat_interval_sec |
必須 | 0より大きい数 | heartbeat間隔の申告値(§5で丸める) |
expiry_sec |
必須 | 0より大きい数 | 期限切れ時間の申告値(§5で丸める) |
instance_policy |
必須 | multi、host:N、limit:N(Nは1〜1000の整数。空白を入れない) |
起動数ポリシー(設計書§5.1) |
started_at |
版1で必須 | ISO 8601の日時。時差(+09:00等)を含む |
プロセスの起動時刻 |
commit |
任意 | 7〜64文字の16進数(小文字) | 実行中のコードのcommit。取得できなければ省略する(設計書§6.6) |
unresponsive_retention_sec |
任意 | 0より大きい数 | 応答なし保持時間の申告値(§5で丸める) |
stop_ack_timeout_sec |
任意 | 0以上の数 | 停止への応答待ち時間の申告値(§5で丸める。§7) |
host_label |
任意 | 1〜64文字 | 表示用のホスト名。判定には使わない |
app_version |
任意 | 1〜64文字 | 人向けversion |
nullは省略と同じに扱う| 場合 | 扱い |
|---|---|
| エンベロープのいずれかが無い、または形式が違う | 422。観測しない(監視対象への対応付け・途絶判定ができないため) |
Brainが対応する版(§4.6)で、版1の項目の形式が違う、またはstarted_atが無い |
422。観測しない |
| Brainが対応しない版(非対応版) | エンベロープだけを検証し、受け付ける(200)。エンベロープ以外の項目は、読めた範囲で表示に使い、読めなければ「不明」(null)とする。応答なし保持時間・停止への応答待ち時間は申告値を使わず、Brainのデフォルト値にする(設計書§7.3) |
process_instance_idのheartbeatで監視対象キー等が前回と変わっていた場合は、最新の申告で置き換える(自己申告を信頼する。設計書§7.4)process_instance_idのheartbeatが届けば、「稼働中」に戻す(通信の一時的な途絶からの回復)。破棄された後に届けば、新しく観測を始める200
{
"brain_state": "running",
"protocol_compatible": true,
"applied": {
"heartbeat_interval_sec": 15,
"expiry_sec": 45,
"unresponsive_retention_sec": 600,
"stop_ack_timeout_sec": 1
}
}
| フィールド | 意味 |
|---|---|
brain_state |
running(稼働中)またはstopping(停止処理中。§7) |
protocol_compatible |
申告した版にBrainが対応しているか |
applied |
Brainが丸めた後の値(§5)。UIは、次のheartbeatをapplied.heartbeat_interval_secの間隔で送る(申告値が範囲外だった場合に、期限切れを避けるため) |
POST /runtime/exit
{ "protocol_version": 1, "process_instance_id": "8b1f3c2e-5a7d-4e0b-9c61-2f4d8a9e7b10" }
protocol_version・process_instance_idはいずれも必須で、形式はheartbeatと同じ(§4.1)。欠落・形式違いは422process_instance_idだけを使って処理する(process_instance_idはエンベロープの項目で、どの版でも意味が変わらないため)200 {"status": "ok"}。観測していないprocess_instance_idでも200を返す(何もしない)。終了処理中のUIに、エラーの扱いを求めないためであるPOST /runtime/stop-ack
{ "protocol_version": 1, "process_instance_id": "8b1f3c2e-5a7d-4e0b-9c61-2f4d8a9e7b10" }
protocol_version・process_instance_idの扱いは終了通知(§4.4)と同じbrain_state: "stopping"を受け取り、停止への対応(利用者への案内等。UIごとに決める)を終えたら送る200 {"status": "ok", "accepted": true}。停止処理中でない、または応答を待っていないプロセスからの応答は、accepted: falseで返す(何もしない)protocol_versionを持つ。Brainは、要求ごとに申告された版で解釈する[1])。状態取得のbrain.supported_protocol_versionsで公開する[1, 2]になる設計書§6.3・§7.7で詳細設計へ送った暫定値である。いずれもBrainの設定で変更できる(§13)。
| 値 | 下限 | 上限 | デフォルト |
|---|---|---|---|
| heartbeat間隔 | 5秒 | 60秒 | なし(エンベロープで必須) |
| 期限切れ時間 | 15秒 | 120秒 | なし(エンベロープで必須) |
| 許容幅 | — | — | 10秒 |
| 応答なし保持時間 | 60秒 | 3600秒 | 600秒 |
| 停止への応答待ち時間 | 0秒 | 10秒 | 1秒(設計書§7.7) |
丸めの手順(設計書§6.3):
値の根拠:
Brainは、UIプロセスごとに次の記録をメモリに持つ(process_instance_idをキーにする。設計書§3.2)。
null)Brain再起動で記録は失われ、次のheartbeatから作り直す(設計書§3.2、§6.4)。
時間の経過による遷移(稼働中→期限切れ→破棄)を、次のときに評価する。評価は1つの関数で行い、観測用のロック(runtime_guard。会話のsessions_guardとは別)を持って行う。
runtime-observer)が1秒ごとに評価する。遷移のログ(§10)を、実際の時刻から1秒以内に出すためである。スレッドはセッション掃除スレッドと同じく、lifespanで起動・停止する(threading.Eventで停止を伝える。daemon=True)。終了処理では、両方のスレッドに停止を先に伝え、共通の期限(5秒)までそれぞれの終了を待つ。2つのスレッドの終了待ちの合計は5秒以内であり、停止にかかる時間の式(brain-lifecycle詳細設計§9の「掃除スレッドのjoinの上限(5秒)」)は変わらないruntime_guardを持ったままsessions_guardを取らない。会話の集計(§8.1のconversations)は、runtime_guardを離してから読む。
設計書§6.1のとおり。本書で決める点だけを書く。
| 軸 | 本書で決める点 |
|---|---|
| プロセス観測状態 | 「終了通知済み」の記録は直ちに破棄するため、状態取得に現れるのは「稼働中」(running)と「期限切れ」(expired)だけである。期限は「最後に確認した時刻+丸めた期限切れ時間」、破棄は「期限+応答なし保持時間」 |
| サービス利用可能性 | /health/readyと同じ判定(§9.2)。停止処理中はnot_ready |
| 監視対象充足状態 | §6.4、§6.6 |
| 登録対応状態 | 監視対象キーの3要素が、登録一覧(§6.8)の1件と完全に一致すれば登録一致 |
| 起動数判定 | §6.5 |
| プロトコル互換性 | 申告した版が、Brainの対応する版の一覧(§4.6)にあれば対応 |
登録済みの監視対象ごとに、一致するプロセスの記録から、次の順に最初に当てはまるものを表示上の状態とする(設計書§6.2)。
| 順 | 条件 | 表示上の状態(status) |
|---|---|---|
| 1 | 猶予期間中(§6.6)で、Brainの起動後に一致するプロセスを一度も観測していない | unknown(不明) |
| 2 | 一致する「稼働中」のプロセスがあり、うち1つ以上が対応版 | running(稼働中) |
| 3 | 一致する「稼働中」のプロセスがあり、すべて非対応版 | unsupported_version(非対応版) |
| 4 | 一致する「期限切れ」の記録が残っている | unresponsive(応答なし) |
| 5 | 上記以外(観測されていない、終了通知で破棄された、保持時間を過ぎて破棄された) | not_running(未稼働) |
duplicate(真偽値)で示す(設計書§6.2)。一致する「稼働中」のプロセスのいずれかが§6.5で重複と判定されればtrueprocess_instance_idが監視対象キーを変えた場合も、変更前と変更後の両方が集合に残る。記録の破棄や監視対象キーの変更で、観測済みの対象が未観測(unknown)へ戻ることはない「稼働中」のプロセスだけを数える(設計書§6.1)。次のいずれかに当てはまるプロセスを、重複(duplicate: true)とする。
host_id + ui_kind + target_idの「稼働中」のプロセスが2つ以上ある(ポリシーによらない)host:Nについて、同じhost_id + ui_kindの「稼働中」のプロセス数がNを超えるlimit:Nについて、同じui_kindの「稼働中」のプロセス数がNを超えるmultiは上限を持たないので、1だけが適用されるbrain.grace_period_untilに終わる時刻を示す登録済みの監視対象に一致するプロセスの記録を破棄するとき、その監視対象ごとに要約を残す(設計書§3.2)。
process_instance_id、終わり方(exit: 終了通知/expired: 期限切れ)登録済みの監視対象の一覧は#24で実装する。#16では、状態の導出が登録一覧を外から受け取る形にする。
host_id, ui_kind, target_id)の集合を返す関数。create_app()の引数で差し替えられるtargetsは空になり、観測したUIはすべてunregistered_processesに並ぶbrain-lifecycle詳細設計§6.1の段階2aを実装する。応答待ちは、UIに通知が届いた時点から数える(運用者の判断、#6644の1)。
brain_state: "stopping"を返す。待つ相手のheartbeatに初めてstoppingを返した時点を、そのプロセスへの通知の時刻とするBRAIN_SHUTDOWN_UI_WAIT_SEC(暫定値30秒)が過ぎたら、残りを待たずに終える処理中の会話要求の完了待ち(brain-lifecycle詳細設計§6.4)とは、今と同じく並行して進める(asyncio.gather)。
503で断られるか、接続できなくなることで停止を知る(設計書§7.7の「heartbeatを送らないUI」と同じ扱い)。停止を確実に知る必要があるUIは、heartbeat間隔を15秒以下にする(音声UIの推奨値。§5)。全体の上限をheartbeat間隔の上限に合わせて延ばすことはしない(停止に1分以上かかるようになり、TimeoutStopSecも延ばす必要があるため)TimeoutStopSec=60は変えなくてよい。BRAIN_SHUTDOWN_UI_WAIT_SECまたはBRAIN_SHUTDOWN_DRAIN_SECを変えたときは、式で見直す(runbookに書く)待ち始めに待つ相手の件数と全体の上限を、終わりに結果の内訳(応答あり、応答待ち時間切れ、通知が届かなかった、終了通知、全体の上限で打ち切り)を出す。プロセスごとの出来事は§10の形式で出す。
GET /runtime/status → 200
{
"schema_version": 1,
"generated_at": "2026-09-26T09:30:00.123+09:00",
"brain": {
"state": "running",
"live": true,
"readiness": "not_ready",
"readiness_checks": [
{ "name": "ollama", "ok": false, "detail": "Ollamaへの疎通に失敗しました: ..." },
{ "name": "searxng", "ok": true, "detail": null }
],
"started_at": "2026-09-26T09:00:00.000+09:00",
"commit": "700665af0c3e…",
"version": "0.1.0",
"grace_period_until": "2026-09-26T09:02:00.000+09:00",
"supported_protocol_versions": [1]
},
"targets": [
{
"host_id": "3f5c…",
"ui_kind": "voice",
"target_id": "default",
"status": "running",
"duplicate": false,
"matching_processes": [ { "…": "下記のプロセスの形式" } ],
"last_observation": {
"last_seen": "2026-09-26T08:55:00.000+09:00",
"process_instance_id": "…",
"ended_by": "expired"
}
}
],
"unregistered_processes": [
{
"process_instance_id": "8b1f3c2e-…",
"host_id": "3f5c…",
"ui_kind": "test-ui",
"target_id": "default",
"host_label": "DESKTOP-ABC",
"status": "running",
"observation": "running",
"duplicate": false,
"protocol_version": 1,
"protocol_compatible": true,
"instance_policy": "multi",
"started_at": "2026-09-26T09:10:00+09:00",
"commit": "700665af0c3e…",
"app_version": null,
"first_seen": "2026-09-26T09:10:00.500+09:00",
"last_seen": "2026-09-26T09:29:55.000+09:00",
"expires_at": "2026-09-26T09:30:40.000+09:00",
"discard_at": null,
"heartbeat_interval_sec": 15,
"expiry_sec": 45,
"unresponsive_retention_sec": 600
}
],
"conversations": {
"session_count": 2,
"in_flight_requests": 0
}
}
| 項目 | 内容 |
|---|---|
schema_version |
この応答の形式の版。項目の追加・変更のたびに上げる(管理プロトコルの版とは別) |
generated_at |
応答を作った時刻。画面が経過時間を計算するのに使う |
brain.state |
running/stopping(brain-lifecycle詳細設計§5) |
brain.live |
常にtrue(応答できていること自体が生存を示す。/health/liveと同じ) |
brain.readiness |
ready/not_ready/unknown(§9.2) |
brain.readiness_checks |
依存先ごとの結果。detailは失敗の理由(URLと例外の内容。トークンを含まない) |
brain.commit・brain.version |
取得できなければnull(§9.1) |
targets |
登録済みの監視対象(§6.4)。#24までは空 |
targets[].matching_processes |
一致するプロセス(unregistered_processesの要素と同じ形式) |
targets[].last_observation |
最終観測の要約(§6.7)。無ければnull |
unregistered_processes |
登録に一致しないプロセス。statusはrunning(稼働中)またはunresponsive(期限切れ。応答なし) |
プロセスのobservation |
running/expired(§6.3) |
プロセスのdiscard_at |
期限切れのとき、記録を破棄する予定の時刻。稼働中はnull |
conversations |
保持しているセッション数と、処理中の会話要求の件数(設計書§3.3) |
schema_version 1には含めない。#24でschema_versionを2に上げ、recovery_warnings・終了の要求・登録日時等を加える(registration詳細設計§6)started_atはそのまま返すtargetsは監視対象キー(ui_kind・host_id・target_id)の順、プロセスはui_kind・host_id・target_id・started_at・process_instance_idの順とする。started_atがnull(非対応版で読めなかった場合)のプロセスは、同じキーの中で最後に並べる。画面の表示が呼び出しのたびに入れ替わらないようにするためである設計書§8.2のとおり。会話内容、個々のsession_id、トークン(設定済みかどうかも)を含めない。単体テストで、応答に各トークンの値とsession_idが現れないことを確かめる。
git -C <プロジェクトのディレクトリ> rev-parse HEADを1回だけ実行して得る(上限5秒)。失敗すればnull(設計書§6.6)。未commitの変更の有無は扱わない(ユースケース文書§9-5: commit済みの状態から起動する前提)importlib.metadata.version("keina-brain"))。得られなければnullevent=brain_started)/health/readyの判定を共通の関数に切り出し、/health/readyと状態取得の両方から呼ぶ(設計書§8.1)。
/api/tags、ツールが有効ならSearXNGの/healthz。上限5秒)。httpx.Clientはwithで使い、閉じるnot_readyとするhttpx.HTTPError以外の例外が起きたときはunknownとする(設計書§6の軸の値「不明」)/health/readyの応答(200/500/停止処理中の503)は今のまま変えない今のログの仕組み(loggingで標準エラー出力へ。systemd経由ではjournal)に、key=value形式の1行で出す(運用者の判断、#6644の3)。#12でロギングの方針が決まったら、それに合わせて改める。
2026-09-26 09:10:00,500 INFO keina_assistant.runtime: runtime_event event=observed ui_kind=test-ui target_id=default host_id=3f5c… process_instance_id=8b1f3c2e-… protocol_version=1 compatible=true instance_policy=multi commit=700665a started_at=2026-09-26T09:10:00+09:00 interval=15 expiry=45
keina_assistant.runtime、本文はruntime_event event=<出来事>で始める。journalctl --user -u keina-brain | grep runtime_eventで追えるui_kind、target_id、host_id(64文字のまま)、process_instance_id"、=、制御文字のいずれかを含む場合(host_label等)は、"で囲む。中の"と\は\でエスケープし、改行は\n、復帰は\r、タブは\t、その他の制御文字は\xHHに置き換える。UIの申告値から複数行のログや偽の項目を作れないようにするためである出来事(event) |
出すとき | 追加の項目 | レベル |
|---|---|---|---|
brain_started |
lifespanの開始処理 | commit、version、grace_period_until |
INFO |
observed |
新しいプロセスの観測を始めた | protocol_version、compatible、instance_policy、commit、started_at、host_label、丸めたinterval・expiry |
INFO |
expired |
期限切れになった | last_seen、expires_at |
WARNING |
resumed |
期限切れの後、破棄される前に再びheartbeatが届いた | last_seen(前回) |
INFO |
exit_notified |
終了通知を受けた | last_seen |
INFO |
discarded |
記録を破棄した | reason(exit/expired)、last_seen、registered(登録一致ならtrue) |
INFO |
stop_notified |
停止処理中をheartbeatの応答で伝えた(待つ相手のみ) | ack_timeout |
INFO |
stop_acked |
停止への応答を受けた | elapsed(通知からの秒数) |
INFO |
stop_ack_timeout |
通知の後、応答待ち時間を過ぎた | ack_timeout |
WARNING |
stop_not_delivered |
通知する前に期限切れになった、または全体の上限で打ち切った | reason(expired/deadline) |
WARNING |
observed・expired・exit_notified・discardedで満たすhost_idの算出規則(全コンポーネント共通)設計書§5.2の「AiChatWlsの全コンポーネントで共通の方式・名前空間」を次のとおり定める。#23(音声UI)とテストUIは、この規則で算出する。Brainはhost_idを算出しない(Brainは監視対象キーを持たない。設計書§3.1)。
/etc/machine-idの内容HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptographyの値MachineGuidSHA-256(手順2の値 + ":aichatwls-host-id:v1")をUTF-8で計算し、小文字の16進数64文字にする。設計書§5.2の「取得値+名前空間文字列」の順に従うimport hashlib
HOST_ID_NAMESPACE = ":aichatwls-host-id:v1"
def host_id_from_raw(raw: str) -> str:
value = raw.strip().lower()
if not value:
raise ValueError("host_idの元になる値が空です")
return hashlib.sha256((value + HOST_ID_NAMESPACE).encode("utf-8")).hexdigest()
固定のテストベクトル(#23の実装とテストUIの単体テストで、同じ値になることを確かめる):
| 生の値(Pythonの文字列表記) | 手順2の後 | host_id |
|---|---|---|
" 0123456789ABCDEF0123456789abcdef\n" |
0123456789abcdef0123456789abcdef |
d558fd731c8a58e1b5803d954df5effaeea646084a135b14ed2c5731b08f9c31 |
host_idが変わり、登録情報の移行が必要になる(設計書§5.1)。変えないこと--print-host-idで、そのホストのhost_idを確かめられる(§12)。#23の実装が同じ値になることの確認に使う実機検証(§14.2)のためのスクリプトをtools/runtime_test_ui.pyに置く。
urllib、uuid、hashlib、winreg等)。Windows側でも、パッケージを入れずに動かすためである。Python 3.11以上uv run python tools/runtime_test_ui.py …。Windowsでは、Windows側のPythonで\\wsl$\…\tools\runtime_test_ui.pyを直接実行する(ファイルのコピーは要らない)BRAIN_AUTH_TOKEN、または--env-fileで指定した.envから読む。コマンドラインの引数では受け取らない(プロセスの一覧に出るため)| 引数 | 既定値 | 用途 |
|---|---|---|
--brain-url |
http://127.0.0.1:8811 |
接続先 |
--ui-kind |
test-ui |
UIの種類 |
--target-id |
default |
枠 |
--policy |
multi |
起動数ポリシー(UC-M01はmulti、UC-M03はhost:1) |
--interval・--expiry |
5・15 |
heartbeat間隔・期限切れ時間の申告値 |
--retention |
(申告しない) | 応答なし保持時間の申告値(UC-R07で短くする) |
--stop-ack-timeout |
(申告しない) | 停止への応答待ち時間の申告値 |
--commit |
スクリプトがあるリポジトリのHEAD。取得できなければ省略 |
旧commitのプロセスを模す(UC-D03)。noneで省略 |
--protocol-version |
1 |
非対応版を模す(単体テストの補助。実機検証の対象外) |
--host-label |
ホスト名 | 表示用のホスト名 |
--no-exit-notice |
— | 終了時に終了通知を送らない |
--no-stop-ack |
— | 停止の通知を受けても応答しない |
--print-host-id |
— | host_idを表示して終わる |
process_instance_idを作り、host_idと一緒に表示する。heartbeatをapplied.heartbeat_interval_secの間隔で送り、応答を1行ずつ表示する。brain_state: "stopping"を受けたら表示し、--no-stop-ackでなければ停止への応答を送る。Ctrl+Cで終了通知を送って終わる。Brainに接続できないときは表示して、間隔ごとに送り直すkill -9、Windowsではtaskkill /Fで行う/ask)を行わない。UC-R06(heartbeatを送らないクライアント)はcurlで/askを呼んで確かめる.envまたは環境変数で指定する(brain-lifecycle詳細設計§9に加える)。
| 名前 | 既定値 | 用途 |
|---|---|---|
BRAIN_VIEW_TOKEN |
(空) | 閲覧トークン(§3.2)。空なら状態取得は常に401 |
BRAIN_HEARTBEAT_INTERVAL_MIN_SEC・_MAX_SEC |
5・60 |
heartbeat間隔の下限・上限(§5) |
BRAIN_EXPIRY_MIN_SEC・_MAX_SEC |
15・120 |
期限切れ時間の下限・上限。上限は猶予期間の長さにもなる(§6.6) |
BRAIN_EXPIRY_MARGIN_SEC |
10 |
許容幅 |
BRAIN_UNRESPONSIVE_RETENTION_DEFAULT_SEC・_MIN_SEC・_MAX_SEC |
600・60・3600 |
応答なし保持時間のデフォルト・下限・上限 |
BRAIN_STOP_ACK_TIMEOUT_DEFAULT_SEC・_MAX_SEC |
1・10 |
停止への応答待ち時間のデフォルト・上限(下限は0) |
BRAIN_SHUTDOWN_UI_WAIT_SEC |
30 |
停止シーケンスでUIの応答を待つ全体の上限(§7.1) |
次を満たさなければ、設定の誤り(終了コード2)とする。
_MIN_SEC・_MAX_SEC・_DEFAULT_SECは0より大きい。ただし、停止への応答待ち時間のデフォルト・上限、BRAIN_EXPIRY_MARGIN_SEC、BRAIN_SHUTDOWN_UI_WAIT_SECは0以上BRAIN_VIEW_TOKENを設定する場合は、BRAIN_AUTH_TOKENと異なる値である時刻に依存する判定は、時計を差し替えて確かめる(観測の記録と導出は、時計を引数で受け取る)。
| 対象 | 確かめること |
|---|---|
| 認証 | 全経路でトークンなしが401。会話用トークンで状態取得が401、閲覧トークンで会話・heartbeatが401。BRAIN_VIEW_TOKENが空なら状態取得は常に401。/docs・/openapi.jsonが無い |
| heartbeatの検証 | エンベロープの欠落・形式違いで422。版1の項目の形式違い・started_atの欠落で422。非対応版はエンベロープだけで受け付け、他の項目は読めた範囲、保持時間・応答待ち時間はデフォルト。知らない項目は無視する |
| 終了通知・停止への応答の検証 | protocol_version・process_instance_idの欠落・形式違いで422。非対応版でもprocess_instance_idで処理する |
| 丸め | 範囲外の申告が範囲に丸められる。期限切れ時間が「間隔+許容幅」まで引き上げられる(上限は超えない)。応答のappliedに反映される |
| プロセス観測状態 | 期限を過ぎるとexpired、さらに保持時間を過ぎると破棄。期限切れの後、破棄前のheartbeatでrunningに戻る。終了通知で直ちに破棄。未知のprocess_instance_idの終了通知は200で何もしない |
| 表示上の状態 | §6.4の5つの状態と、その順序(稼働中と非対応版の混在は稼働中、期限切れの記録が残る間は応答なし等) |
| 起動数判定 | 同じキーの2プロセス、host:1でtarget_id違いの2プロセス(重複)、host:1でhost_id違い(重複でない)、limit:N、multi、ポリシーの混在(厳しい方)、期限切れのプロセスを数えないこと |
| 猶予期間 | 猶予期間中に未観測の登録済み対象はunknown、heartbeatを受ければrunning、猶予期間の後はnot_running。猶予期間中に一度観測した後で終了通知を受けた対象はnot_running。同じprocess_instance_idが監視対象キーを変えても、変更前の対象がunknownへ戻らない |
| 最終観測の要約 | 破棄のときに登録済み対象にだけ残る。新しい方で置き換わる。終わり方(exit/expired) |
| 停止シーケンス | 停止処理中はheartbeatの応答がstopping。応答を受けたら待ち終わる。応答待ち時間を通知の時刻から数える。通知前に期限切れなら待たない。非対応版・停止処理中に観測したプロセスは待たない。全体の上限で打ち切る(heartbeat間隔が全体の上限より長いUIは、通知が届かないままstop_not_delivered・reason=deadlineになる)。処理中の要求の完了待ちと並行に進む。停止処理中も管理プロトコルを受け付ける |
| 状態取得 | §8.1の形式(targets・unregistered_processes・conversations・brain)。停止処理中はstate: stopping・readiness: not_ready。会話内容・session_id・トークンの値が含まれない。並び順が安定している |
| 利用可能性 | Ollama・SearXNGの成否でready/not_ready、想定外の例外でunknown。/health/readyの応答は変わらない |
| Brain自身の情報 | commitが取れないときにnull |
| ログ | 各出来事が§10の形式で1回だけ出る。heartbeatのたびには出ない。改行・タブ・"・=を含むhost_labelが1行のまま、エスケープされて出る |
| 観測用スレッド | lifespanで起動・停止する。create_app()だけでは起動しない |
| 設定 | §13の条件を満たさない値で設定の誤りになる |
host_id |
§11のテストベクトルの値になる。空の値でエラーになる(テストUIの関数で確かめる) |
| 観測用スレッドの終了 | 掃除スレッドと合わせて、終了待ちの合計が5秒以内に収まる |
いずれも、WSL2のBrainをsystemctl --user start keina-brainで起動し、状態取得はcurl -H "Authorization: Bearer $BRAIN_VIEW_TOKEN" http://127.0.0.1:8811/runtime/statusで確かめる(Windowsからの確認は、WindowsのPowerShellで同じ要求を送る)。
| ユースケース | 手順 | 期待結果 |
|---|---|---|
| UC-B04 | Ollamaを止めて状態取得を呼び、戻して再び呼ぶ | 止めている間はlive: true・readiness: not_readyでOllamaの失敗が分かる。戻すとready |
| UC-R06 | テストUIを1つ動かしたまま、curlで/askを呼ぶ |
conversations.session_countが増え、プロセスの一覧は増えない |
| UC-R07 | テストUIを起動し、Ctrl+Cで終える。次に起動してkill -9で終える(--retention 60) |
起動で一覧に現れ、Ctrl+Cで直ちに消える。kill -9では期限切れ(unresponsive)になり、保持時間の後に消える。journalにobserved・exit_notified・expired・discardedが残る |
| UC-M01 | テストUIを--policy multiでWSL2に3つ起動する |
3つが別々のプロセスとして並び、いずれもduplicate: false |
| UC-M02(検出側) | テストUIを--policy host:1で、--target-idを変えてWSL2に2つ起動する |
2つともduplicate: true |
| UC-M03 | テストUIを--policy host:1で、WindowsとWSL2に1つずつ起動する |
host_idが異なり、いずれもduplicate: false。host_labelでホストが分かる |
| UC-D01(API)・UC-D04 | 会話を数回行った後に状態取得を呼ぶ | 1回の呼び出しでBrainとUIの状態が分かる。会話内容・session_id・トークンが含まれず、セッション数と処理中件数だけが分かる |
| UC-D03 | テストUIを--commit <古いcommit>で起動したまま、同じ--ui-kind・--target-idでもう1つ起動する。次に、Brainを新しいcommitにして再起動する |
2つがprocess_instance_id・started_at・commitで区別できる(同じキーなのでduplicate: true)。Brainのstarted_at・commitが変わる |
| UC-R05(テストUI) | テストUIを動かしたまま、Brainを再起動する | Brainの起動直後に一覧が空になり、次のheartbeatで再び現れる(登録済みのunknown→runningは#24の後、音声UIでは#23の後に確かめる) |
| UC-B03(Brain側の通知と応答) | テストUIを2つ動かし、1つは--no-stop-ackにして、systemctl --user stop keina-brainを実行する |
テストUIがstoppingを受け取る。応答した方はstop_acked、しない方はstop_ack_timeoutがjournalに出る。終了コード0で終わる |
UC-R02〜R05の音声UIでの確認と、UC-B03の音声UIでの確認は、#23の実装後に行う。
| 文書 | 反映すること | 時期 |
|---|---|---|
ui-brain-protocol.md |
§1またはエンドポイント一覧の近くに、「UIプロセスの稼働状態の通知(管理heartbeat・終了通知・停止への応答)と状態取得は、本書ではなくruntime-management-design.md§7・§8と本書で定義する」旨の参照を加え、冒頭の「改訂」行に残す(運用者が同意した。#6644の4) |
本書の合意時 |
runtime-management-detail-brain-lifecycle.md |
§6.3(UIの応答待ち)に本書§7への参照を加え、§12の未決事項(応答待ちを数え始める時点)を解決済みにし、§13に経緯を加える | 本書の合意時 |
runtime-management-implementation-plan.md |
§5.1の#16の詳細設計の成果物の名前を、本書(runtime-management-detail-observation.md)に改める。#24のregistration.mdと紛らわしいため |
本書の合意時 |
runtime-management-runbook.md |
閲覧トークンの用意と配布(当面は手動。設計書§9.4)、状態取得の確かめ方、テストUIの使い方、BRAIN_SHUTDOWN_UI_WAIT_SECを変えたときのTimeoutStopSecの見直し |
実装時 |
なし
| 日付 | 決めたこと | 理由 | 却下・置き換えた案 | 根拠 |
|---|---|---|---|---|
| 2026-09-26 | 停止シーケンスのUIの応答待ちは、UIに通知が届いた時点(次のheartbeatにstoppingを返した時点)から数える。全体の上限は30秒とし、処理中の要求の完了待ちと並行に進める |
UIが停止を知るのは次のheartbeatの時点であり、停止処理中に入った時点から数えると、heartbeatの間隔より短いため、ほとんどのUIに停止が伝わらない。上限30秒は処理中の要求の完了待ちと同じで、並行に進めるためTimeoutStopSec=60を変えずに済む(運用者の判断) |
停止処理中に入った時点から数える案 | #6644(1) |
| 2026-09-26 | BRAIN_VIEW_TOKENが未設定でもBrainは起動し、状態取得だけを常に401にする。会話用トークンと同じ値は設定の誤りとする |
状態取得は会話サービスの依存先ではない。未設定を起動失敗にすると、今の.envに項目を足すまでBrainが起動しなくなる(運用者の判断) |
未設定を設定の誤り(終了コード2)にする案 | #6644(2) |
| 2026-09-26 | 観測の出来事のログは、今のログの仕組みにkey=value形式の1行で出す。#12で方針が決まったら合わせる | #12の方針が決まっておらず、合わせる先がない。journalctlで人が読め、grepで追える(運用者の判断) | JSONの1行(人が読みにくい) | #6644(3) |
| 2026-09-26 | ui-brain-protocol.mdに、管理heartbeat等は別文書で定義する旨の参照を追記する(§15) |
設計書§10の反映方針。確定済みの文書の改訂に、運用者が同意した | 実装が終わるまで保留する案 | #6644(4) |
| 2026-09-26 | CODEXレビューを受け、次を反映した。終了通知・停止への応答にもprotocol_versionを持たせる(§4.4〜§4.6)。host_idのハッシュを設計書§5.2どおり「取得値+名前空間」の順にし、テストベクトルを載せる(§11)。Brain起動後に観測した監視対象キーを別の集合で持つ(§6.4)。停止の通知がbest effortであることと、その条件を書く(§7.2)。設定の検証、ログのエスケープ、並び順、スレッドの終了待ちを具体化する。recovery_warningsをschema_version 1から外し、#24で加える(§8.1) |
版を持たない要求は、版を上げたときに解釈を判定できない。草案のハッシュの順序は合意済みの設計と逆だった。監視対象キーの変更で観測済みの対象が「不明」へ戻り得た。要素の形式が未定義の項目を版1に含めると、#24で互換性が崩れる | 草案の各記述(本行で置き換えた)。終了通知・停止への応答の形式を全版で不変とする案(版を足す方が、将来の変更に対応できる)。設計書を「名前空間+取得値」に改訂する案(改訂する理由がない)。停止の全体の上限をheartbeat間隔の上限に合わせて延ばす案(停止に1分以上かかる) | #6645 |
| 2026-09-26 | 猶予期間中に一度観測した対象は、以後は通常の規則に従う(§6.4)。設計書§6.4の「充足へ確定する」の具体化として扱い、設計書は改訂しない | 設計書は、猶予期間中にheartbeatを受信した対象を「確定」させており、確定した対象を再び「不明」に戻す規定はない。原因が確定した終了を「不明」と表示するのは不正確である | 設計書§6.4を改訂する案(CODEXの提案。規定の範囲内の具体化であり、改訂は要らないと判断した) | #6645 |
| 2026-09-26 | §7.2の目安を、通知そのものが届く基準(20秒)と、通知後の応答待ちまで終えられる基準(19秒)に分けて書いた。本書に運用者が合意した | CODEXの再レビューで、「約19秒」に2つの基準が混ざっていると指摘された。再レビューで合意の条件がすべて解消されたと判定された | #6645の「約19秒を超えると通知が届かないことがある」(本行で置き換えた) | #6646 |
| 2026-09-26 | §3.1・§4.6・§8.1に、#24の詳細設計(管理プロトコル版2、状態取得schema_version 2、状態取得の管理トークンでの認可)への参照を加えた。本書の契約(版1、schema_version 1)は変えない |
版ごとの差分を後継の文書に書くという§4.6の規則に従った | — | #24 #6710 |