ステータス: 合意済み(2026-09-26、Issue #24、工程4: 詳細設計)。論点A〜Gは、運用者がCODEXの検討(#6701、#6703)を踏まえて決めた。CODEXの草案レビュー(#6709)を反映した後に合意した(#6710)
更新するときは、設計文書の更新ルールに従うこと。
本書は、ランタイム管理 設計(合意済み)の§3.1・§3.1.1・§7.8・§8・§9のうちBrainが担う部分を、実装できる粒度に具体化する。範囲は実装計画書§5.1の#24である。詳細設計: UIプロセスの観測と状態取得(以下「observation」)の契約を前提とし、それに加える差分を定める。ダッシュボード(#17)は§3の書き込み契約と§6の状態取得を使い、UI(#23)は§5の管理プロトコル版2に従う。
| 本書で扱う | 扱わない(担当) |
|---|---|
| 管理トークンと、運用者向け管理API(登録・解除・復帰警告の確認・UIへの終了の要求)の契約 | ダッシュボードの画面、ブラウザ・ダッシュボード間の認可、Brainの起動・停止(#17) |
| 登録情報ファイル・世代・警告ファイル、起動時の自動復帰 | 登録情報のバックアップを別の場所へ保管する作業そのもの(運用者。手順はrunbook) |
| 管理プロトコル版2(heartbeatの応答で終了を求める) | 版2を受けたUIの動作の実装(音声UIは#23) |
状態取得のschema_version 2 |
新規導入UIを一覧で確認してから登録する手順(#17のrunbook) |
| 検証用テストUIの版2対応 |
2026-09-26時点のコード(#16の実装、d36ea81)の状態である。
| 項目 | 現状 | 課題 |
|---|---|---|
| 登録一覧 | RuntimeObserverはregistered_keys(監視対象キーの集合を返す関数)を受け取る。本番は常に空(observation §6.8) |
永続化した登録一覧を渡す |
| 認証 | 会話用トークン・閲覧トークンの2種類。状態取得は閲覧トークンだけ | 管理トークンを加え、管理APIを認可する。状態取得は管理トークンでも読めるようにする |
| 最終観測の要約 | 登録の解除で消す処理が無い(observation §6.7) | 解除のときに消す |
| 管理プロトコル | 版1だけ(SUPPORTED_PROTOCOL_VERSIONS = (1,)) |
版2を加える |
| 状態取得 | schema_version 1 |
復帰警告・終了の要求・登録日時を加え、2に上げる |
| 状態ファイル | プロセスロックだけ($XDG_STATE_HOME/keina-brain/brain.lock、BRAIN_LOCK_FILEで変更可) |
登録情報の置き場所を決める |
| メソッド・パス | 用途 | 成功 |
|---|---|---|
POST /runtime/targets |
常駐監視対象の登録(§3.4) | 201 |
DELETE /runtime/targets/{ui_kind}/{host_id}/{target_id} |
常駐監視対象の解除(§3.5) | 200 |
POST /runtime/recovery-warnings/{warning_id}/ack |
復帰警告を確認済みにする(§3.6) | 200 |
POST /runtime/processes/{process_instance_id}/exit-request |
UIへの終了の要求(§3.7) | 200 |
204は使わない)。ダッシュボードが結果を表示に使えるようにするためである| 種類 | エンドポイント | 受け付けるトークン |
|---|---|---|
| 会話・管理プロトコル | observation §3.2のとおり | 会話用トークンBRAIN_AUTH_TOKEN(変えない) |
| 状態取得 | GET /runtime/status |
閲覧トークンBRAIN_VIEW_TOKEN、または管理トークンBRAIN_ADMIN_TOKEN(設計書§9.1: 管理トークンは閲覧もできる) |
| 管理API | §3.1の4本 | 管理トークンBRAIN_ADMIN_TOKEN |
BRAIN_ADMIN_TOKENが未設定(空)のときは、管理APIを常に401にする。Brainは起動し、起動時のログで知らせる(observation §3.2の閲覧トークンと同じ扱い)BRAIN_ADMIN_TOKENを設定する場合は、BRAIN_AUTH_TOKENとも、(設定されていれば)BRAIN_VIEW_TOKENとも異なる値にする。違反は設定の誤り(終了コード2)。同値の検査は、比べる両方が設定されている場合に行う401と403: トークンが無い、または会話用・閲覧・管理のどれにも一致しないときは、今と同じ401・{"detail": "invalid or missing token"}を返す(observation §3.2との互換のため、この応答だけは文字列の形を保つ)。BRAIN_ADMIN_TOKENが設定されているときに限り、有効な閲覧トークンで管理APIを呼ぶと、403・{"detail": {"code": "forbidden", "message": "…"}}を返す。BRAIN_ADMIN_TOKENが未設定のときは、閲覧トークンを含むすべての要求を401にする(fail-closed。管理APIが使えない状態であり、権限の不足ではないため)。権限が足りないことを利用者が理由として確かめられるようにするためである(UC-D06、FR-18)。会話用トークンで管理APIを呼んだときは、閲覧トークンより権限の遠い別の用途のトークンであるため、401とする判定の順序: 管理APIは、次の順に判定し、最初に当てはまったものを返す。
401・403)503 brain_stopping)422 invalid_request)404・409)500 persist_failed)認証より先にBrainの状態や入力の誤りを返さない(権限の無い呼び出しに、Brainの状態を漏らさないため)。本文のJSONは処理関数の中で読み、FastAPIの本文の自動検証(認証より先に422を返しうる)を使わない。
エラーの形式: 401を除き、{"detail": {"code": "<機械判定用>", "message": "<人向けの説明(日本語)>"}}とする。ダッシュボードはcodeで判定し、messageは表示に使う。messageの文言は契約に含めない(変えてもよい)。
code |
HTTP | 意味 | 返すAPI |
|---|---|---|---|
forbidden |
403 | 閲覧トークンでは操作できない | 管理API |
brain_stopping |
503 | Brainが停止処理中で、受け付けない(設計書§7.8。停止中に登録情報を書かないため、他の管理APIにも広げる) | 管理API |
invalid_request |
422 | 本文・パスの形式の誤り(JSONでない、項目の欠落、監視対象キーやIDの形式違い) | 管理API |
already_registered |
409 | 同じ監視対象キーが登録済み | 登録 |
not_observed |
409 | その監視対象キーのプロセスが、今の観測一覧に無い(§3.4) | 登録 |
not_registered |
404 | 解除しようとした監視対象キーが登録されていない | 解除 |
unknown_recovery_warning |
404 | その識別子の未確認の警告が無い(存在しない、または確認済み) | 復帰警告の確認 |
unknown_process |
404 | そのprocess_instance_idの記録が無い(観測していない、または記録が破棄された) |
終了の要求 |
persist_failed |
500 | 登録情報・警告ファイルを保存できず、変更しなかった(§4.5) | 登録・解除・復帰警告の確認 |
503は、管理APIに限る。heartbeat・終了通知・停止への応答は、停止処理中も受け付ける(observation §3.1。停止シーケンスに必要なため)。状態取得も受け付ける形式の検証: 監視対象キーの3要素とprocess_instance_idは、heartbeatと同じ形式(observation §4.1)で検証する。warning_idはUUID文字列とする。本文の知らない項目は無視する(observation §4.1と同じ)。
POST /runtime/targets
{ "host_id": "3f5c…(64文字の16進数)", "ui_kind": "voice", "target_id": "default" }
409 not_observed。記録の破棄と要求が行き違った場合(画面に表示されていたが、要求の時点で破棄されていた)も409 not_observedになる。運用者は、そのUIを起動し直し、一覧に現れてから登録し直す(設計書§3.1)409 already_registered(観測済みかどうかより先に判定する)registered_atは、Brainが受け付けた時刻とする201{ "target": { "host_id": "3f5c…", "ui_kind": "voice", "target_id": "default", "registered_at": "2026-09-26T10:00:00.000+09:00" } }
DELETE /runtime/targets/{ui_kind}/{host_id}/{target_id}
404 not_registered200 {"removed": {"host_id": "…", "ui_kind": "…", "target_id": "…", "registered_at": "…"}}POST /runtime/recovery-warnings/{warning_id}/ack
404 unknown_recovery_warning(既に確認済みの識別子も同じ)registry.recovery_pending。§4.5)は、先に復帰の残りを再試行する。失敗したら500 persist_failedとし、警告を消さない(復帰を終えないまま警告だけを消さないため)500 persist_failedとし、メモリ上の警告も消さない200 {"acknowledged": "<warning_id>"}POST /runtime/processes/{process_instance_id}/exit-request(本文は不要。送られても無視する)
process_instance_idの記録(「稼働中」または「期限切れ」)が無ければ404 unknown_process。観測していないプロセス、終了通知や保持時間の経過で記録が破棄されたプロセスが当たる200{ "accepted": true, "created": true, "requested_at": "2026-09-26T10:05:00.000+09:00", "deliverable": true }
| フィールド | 意味 |
|---|---|
accepted |
受け付けた(200では常にtrue) |
created |
今回の要求で新しく要求を記録したらtrue。既に要求中だった(再送)ならfalse |
requested_at |
要求の時刻。再送では最初の要求の時刻を返す(更新しない)。「いつから応じていないか」が分かり、再送で待ち時間が戻らないようにするため |
deliverable |
記録しているプロトコル版に対して、Brainが次のheartbeatの応答に要求を載せられるならtrue(§5)。次のheartbeatが届くことや、UIが終了することまでは保証しない |
deliverable: 記録の版がexit_requestedを持つ版(版2)ならtrue。版1・非対応版はfalse。「期限切れ」の版2のプロセスもtrue(heartbeatが戻れば伝わる)exit_requested・exit_requested_atで分かる(設計書§7.8、UC-R08)deliverableはその時点の版で判断する)。Brainが再起動すると失われる(永続化しない。設計書§7.8)runtime_guard)の下で順に処理する。終了通知が先なら要求は404 unknown_process、要求が先なら200の後に終了通知で記録が消える503 brain_stopping(§3.3)。停止処理中に入る前に受け付けた要求は保持し、停止処理中のheartbeatの応答にも載せる(§5.2)設計書§3.1.1の具体化である。
BRAIN_REGISTRY_DIR(既定値$XDG_STATE_HOME/keina-brain/registry。XDG_STATE_HOMEが無ければ~/.local/state)に置く。
registry/
registrations.json 本体
registrations.generations/ 世代
registrations-20260926T010000.123456Z.json
…
recovery-warnings.json 警告ファイル
registrations.corrupt-20260926T010500.000000Z.json 退避した壊れた本体
recovery-warnings.corrupt-20260926T010500.000000Z.json 退避した壊れた警告ファイル
BRAIN_REGISTRY_DIRは絶対パスとし、/mnt/の下(Windows側のドライブ)を禁じる(BRAIN_LOCK_FILEと同じ。brain-lifecycle詳細設計§4.2)。違反は設定の誤り(終了コード2)0700、ファイル0600とする(登録情報に秘密は含まないが、運用者以外が書き換えられないようにするため)-1、-2…を付ける本体と世代は同じ形式である(人が読めるJSON、UTF-8、インデント2)。
{
"schema_version": 1,
"saved_at": "2026-09-26T10:00:00.123+09:00",
"targets": [
{ "host_id": "3f5c…", "ui_kind": "voice", "target_id": "default", "registered_at": "2026-09-26T10:00:00.000+09:00" }
]
}
targetsの並び順は、状態取得(observation §8.1)と同じ監視対象キーの順とするschema_versionが1である(知らない版は壊れているものとして扱う。新しい版のBrainから古い版へ戻した場合も、ここに当たり、警告で分かる)saved_atが時差付きのISO 8601である(復帰警告のrestored_generation_atと、世代の整理(§4.8)に使うため)targetsが配列で、各要素の監視対象キーがheartbeatと同じ形式(observation §4.1)、registered_atが時差付きのISO 8601である本体・世代・警告ファイルは、どれも次の手順で書く。
fsyncするos.replaceで目的の名前に置き換えるfsyncするいずれかが失敗したら、一時ファイルを消し(消せなくても続ける)、失敗として扱う。書き込み中にBrainが止まっても、置き換えの前後どちらかの内容が残る(設計書§3.1.1)。
登録・解除・復帰警告の確認は、登録用のロック(registry_guard)を持って1つずつ行う。
登録・解除:
409・404500 persist_failed500 persist_failedとし、メモリ上の登録一覧も変えない(設計書§3.1.1)registry_guardを持ったまま観測用のロック(runtime_guard)を取ってよい(手順1の観測済みの確認、手順5の要約の削除)。逆に、runtime_guardを持ったままregistry_guardを取らない。観測の側は、登録一覧を、差し替えるだけの読み取り専用の集合(frozenset)として読み、ファイルの入出力を待たない(observation §6.8のregistered_keys)復帰警告の確認: 登録・解除の手順2と同じく、復帰が済んでいなければ先に再試行し、失敗したら500 persist_failedとする。次に、警告ファイルを、その1件を除いた内容で書き(§4.3)、書けたらメモリ上の警告から除く。書けなければ500 persist_failedで、何も変えない。
Brainのlifespanの開始処理で、観測用スレッドの起動より前に行う。どの場合もBrainは起動を続け、会話サービスを通常どおり提供する(readinessに影響しない。設計書§3.1.1)。
警告ファイルを先に読む(§4.6)。次に本体を読み、次の表で扱う。
| 本体 | 世代 | 扱い | 警告 |
|---|---|---|---|
| 読み込める | — | その内容で起動する。読み込める世代が1件も無い、または最新の読み込める世代と内容(targets)が違えば、世代を作る(§4.7の再試行)。復帰が済んでいない警告(下記)があれば、済んだものとする |
なし |
| 無い | 無い | 初回導入として、登録0件で起動する。本体はまだ作らない(最初の登録で作る) | なし |
| 無い | 読み込めるものがある | 最新の読み込める世代の内容で本体を書き直し、起動する | registry_missing |
| 壊れている | 読み込めるものがある | 本体を退避(コピー)し、最新の読み込める世代の内容で本体を書き直し、起動する | registry_corrupted |
| 無い/壊れている | あるが、どれも読み込めない、または無い(壊れている場合) | (壊れていれば退避し、)登録0件の本体を作り、起動する | 本体が無ければregistry_missing、壊れていればregistry_corrupted。いずれも「戻せる世代なし」 |
registered_atは世代に保存されていた値をそのまま使う復帰の手順: 警告が要る場合は、設計書§3.1.1の順(退避→警告の保存→本体の書き直し)に、次の4段で行う。警告には、復帰が済んだかどうか(recovery_completed)を持たせる。
registrations.corrupt-<日時>.jsonへコピーする。本体は消さず、壊れたまま残す。途中で失敗して再起動しても、本体が「無い」に変わらず、同じ「破損」として復帰をやり直せるようにするためであるrecovery_completed: false)を警告ファイルに保存するrecovery_completed: trueにして、警告ファイルに保存するrecovery_completed: falseの)未確認の警告があり、今回の本体の状態と同じ事故であれば、新しい警告を作らず、その警告(識別子・発生日時・種別を変えない)で手順を続ける。同じ事故とは、種別が同じで、破損なら壊れた本体の内容のハッシュ(source_sha256。§4.6)も同じであることをいう。退避済み(警告にevacuated_fileがあり、そのファイルがある)なら、1は行わない。restored_generation_atは、今回戻した世代の値に改める復帰が済んでいない状態(registry.recovery_pending。§6): 次のいずれかに当てはまる間をいう。
recovery_completed: falseの警告がメモリ上にあるこの間は、登録・解除・復帰警告の確認の前に、上の手順と警告ファイルの修復の残りを再試行する(§4.4)。再試行が失敗したら、その操作を500 persist_failedで拒否する。復帰が済んでいない警告を、復帰を終えないまま確認済みにして消す経路を作らないためである。次回の起動時にも、上の規則で再開する。
{
"schema_version": 1,
"warnings": [
{
"id": "0b6c2f9e-…(UUID v4)",
"occurred_at": "2026-09-26T10:05:00.000+09:00",
"kind": "registry_corrupted",
"restored_generation_at": "2026-09-25T18:00:00.123+09:00",
"evacuated_file": "registrations.corrupt-20260926T010500.000000Z.json",
"recovery_completed": true,
"source_sha256": "9f86d0…(64文字の16進数)"
}
]
}
kind |
意味(設計書§3.1.1) | restored_generation_at |
evacuated_file |
|---|---|---|---|
registry_corrupted |
破損(本体を読めなかった) | 戻した世代のsaved_at。戻せる世代が無ければnull |
退避したファイルの名前。退避がまだ済んでいなければnull |
registry_missing |
消失(本体が無く、世代はあった、または本体が無く、世代もどれも読めなかった) | 同上 | null(退避するものが無い) |
warnings_unreadable |
警告ファイル自体を読み込めなかった(未確認の警告が失われた可能性がある) | null |
退避した警告ファイルの名前。退避がまだ済んでいなければnull |
recovery_completed: 復帰の手順(§4.5)を終えたか。warnings_unreadableは、警告ファイルの修復を終えたらtruesource_sha256: registry_corruptedのとき、壊れた本体の内容のSHA-256(同じ事故かどうかの判定用。§4.5)。他の種別はnull。状態取得には出さないschema_versionが1・各要素の形式(occurred_at・restored_generation_atは時差付きのISO 8601)・識別子の重複なし、を満たすことrecovery-warnings.corrupt-<日時>.jsonへコピーし、(2) warnings_unreadableの警告1件を含む新しい警告ファイルを書く(設計書§3.1.1)。本体の復帰で警告が増えれば、同じ書き込みに含める。(1)が失敗したら(2)を行わない(壊れた警告ファイルを上書きしない)。(1)・(2)のどちらかが失敗したら、警告はメモリ上に持って状態取得に出し、「警告ファイルの修復が済んでいない」とする(§4.5のrecovery_pending)。この間は警告ファイルに書けないため、本体の復帰は§4.5の2で止まる。再起動しても警告ファイルは壊れたままなので、修復を最初からやり直す(保存されていない警告は重ならない)restored_generation_atがnullなら「戻せる世代なし」。evacuated_fileがnullのときは、registry_missingなら「該当なし」、registry_corrupted・warnings_unreadableなら「退避未完了」(recovery_completed: falseの間だけ起こる)。ファイル名だけを返し、ディレクトリの絶対パスは返さない(置き場所は運用者が設定で知っている)registrations.generations/registrations-<日時>.jsonとして書く(§4.3)registry.backupをstaleにして運用者に分かるようにする(§6)。この間は、最新の世代が本体より古いregistry.backupをokに戻すsaved_at(読めなければファイル名の日時)が保持期間BRAIN_REGISTRY_GENERATION_RETENTION_DAYS(暫定値30日)より古い世代を削除する。ただし、新しい方からBRAIN_REGISTRY_GENERATION_KEEP_MIN(暫定値10)個は、期間によらず残す(設計書§3.1.1)observation §4.6の規則により、エンベロープ以外を変えるので版を上げる。版2の差分は、本節だけである。
| 要求・応答 | 版1からの変更 |
|---|---|
| heartbeat(要求) | なし(protocol_version: 2を申告するだけ) |
| heartbeatの応答 | exit_requested(真偽値)を加える。版2のheartbeatへの応答には常に含める |
| 終了通知・停止への応答 | なし(protocol_version: 2を申告するだけ) |
版2のheartbeatの応答:
{
"brain_state": "running",
"protocol_compatible": true,
"applied": { "heartbeat_interval_sec": 15, "expiry_sec": 45, "unresponsive_retention_sec": 600, "stop_ack_timeout_sec": 1 },
"exit_requested": false
}
[1, 2]になる(brain.supported_protocol_versions)exit_requestedは、そのプロセスに終了の要求(§3.7)が記録されていればtrueexit_requested: trueを受けたら、正常に終了する。終了の前に利用者への案内等を行うかは、UIごとに決める(設計書§7.8)brain_state: "stopping"とexit_requested: trueを同時に受けた場合(停止処理中に入る前に受け付けた要求が残っていた場合だけ起こる)は、終了を優先する。停止への応答(observation §4.5)は送らなくてよく、終了通知を送って終わる。Brainは、停止処理中に終了通知を受けたプロセスを待つ相手から外す(observation §7.1の3)ため、停止を遅らせないexit_requested: trueを受けた後、終了までの間にheartbeatを送ってもよい(応答はtrueのまま)protocol_version: 2を付ける(observation §4.6: 要求ごとに申告された版で解釈する)| UIの版 | Brainの版 | 結果 |
|---|---|---|
| 1 | 対応[1, 2] |
今と同じ。終了の要求はdeliverable: falseで、伝わらない |
| 2 | 対応[1, 2] |
終了の要求が伝わる |
| 2 | 対応[1](#24より前のBrain) |
非対応版として扱われる(observation §4.2)。登録済みなら表示上の状態は「非対応版」。終了の要求の仕組みは無い |
schema_version 2)observation §8.1の形式に、次を加える。既存の項目の名前・意味は変えない。
{
"schema_version": 2,
"brain": { "…": "…", "supported_protocol_versions": [1, 2] },
"targets": [
{
"host_id": "…", "ui_kind": "voice", "target_id": "default",
"registered_at": "2026-09-26T10:00:00.000+09:00",
"status": "running", "duplicate": false,
"matching_processes": [ { "…": "…", "exit_requested": false, "exit_requested_at": null, "exit_request_deliverable": true } ],
"last_observation": null
}
],
"unregistered_processes": [ { "…": "…", "exit_requested": true, "exit_requested_at": "2026-09-26T10:05:00.000+09:00", "exit_request_deliverable": false } ],
"recovery_warnings": [
{
"id": "0b6c2f9e-…",
"occurred_at": "2026-09-26T10:05:00.000+09:00",
"kind": "registry_corrupted",
"restored_generation_at": "2026-09-25T18:00:00.123+09:00",
"evacuated_file": "registrations.corrupt-20260926T010500.000000Z.json",
"recovery_completed": true
}
],
"registry": {
"backup": "ok",
"backup_stale_since": null,
"recovery_pending": false
},
"conversations": { "…": "…" }
}
| 項目 | 内容 |
|---|---|
brain.supported_protocol_versions |
[1, 2] |
targets[].registered_at |
登録日時(§3.4)。世代から戻しても保存値のまま。解除して登録し直せば新しい時刻 |
プロセスのexit_requested |
終了を求めているか(設計書§8.1は有無と時刻の両方を求める) |
プロセスのexit_requested_at |
最初に求めた時刻(§3.7)。求めていなければnull |
プロセスのexit_request_deliverable |
そのプロセスの記録の版に、終了の要求を載せられるか(§3.7のdeliverableと同じ判断)。求めていなくても示す。#17が「版2なら伝わる」という知識を持たずに、要求の前に伝わるかどうかを表示できるようにするため。protocol_compatible(版1もtrue)では表せない |
recovery_warnings |
未確認の復帰警告(§4.6の要素からsource_sha256を除いたもの)。無ければ空の配列。occurred_atの古い順、同じならidの昇順に並べる。recovery_completed: falseの警告は、復帰がまだ済んでいないことを示す |
registry.backup |
ok(最新の世代が本体と同じ)/stale(世代の作成に失敗し、最新の世代が本体より古い。§4.7) |
registry.backup_stale_since |
今動いているBrainがstaleを検出した時刻。okならnull。永続化しないため、Brainの起動時にも世代を作れなかった場合は、最初に失敗した時刻ではなく、その起動で検出した時刻になる |
registry.recovery_pending |
復帰が済んでいない(§4.5)。trueの間、登録・解除・復帰警告の確認は、残りの手順の再試行に失敗するとpersist_failedになる |
registryの2つの状態は、復帰警告(確認済みにするまで残る出来事)とは別に、今の状態として示す。条件が解消すれば自動でok・falseに戻り、確認の操作は要らないschema_version 1を前提にした呼び出し元は、現時点で存在しない(ダッシュボードは#17で作る)ため、1との並行提供はしないregistered_keysに、メモリ上の登録一覧(frozenset)を返す関数を渡す(observation §6.8)exit_requested_at。実時刻)を加える。heartbeatでの記録の更新では消さない。記録の破棄で一緒に消えるexit_requested・exit_requested_at・exit_request_deliverableを加える(§6)runtime_guardの下で、評価(observation §6.2)の後に判定する.envまたは環境変数で指定する(observation §13に加える)。
| 名前 | 既定値 | 用途 |
|---|---|---|
BRAIN_ADMIN_TOKEN |
(空) | 管理トークン(§3.2)。空なら管理APIは常に401 |
BRAIN_REGISTRY_DIR |
$XDG_STATE_HOME/keina-brain/registry |
登録情報の置き場所(§4.1) |
BRAIN_REGISTRY_GENERATION_RETENTION_DAYS |
30 |
世代の保持期間(§4.8) |
BRAIN_REGISTRY_GENERATION_KEEP_MIN |
10 |
期間によらず残す世代数(§4.8) |
次を満たさなければ、設定の誤り(終了コード2)とする。
BRAIN_ADMIN_TOKENを設定する場合は、BRAIN_AUTH_TOKENと異なり、BRAIN_VIEW_TOKENが設定されていればそれとも異なるBRAIN_REGISTRY_DIRは絶対パスで、/mnt/の下でないBRAIN_REGISTRY_GENERATION_RETENTION_DAYSは0より大きい有限の数、BRAIN_REGISTRY_GENERATION_KEEP_MINは1以上の整数observation §12のtools/runtime_test_ui.pyに、次を加える。
--protocol-versionの既定値を2にする(版1を模すときは1を指定する)exit_requested: trueを受けたら、その旨を表示し、終了通知を送って終わる(--no-exit-noticeなら送らずに終わる)。終了通知が失敗しても(接続できない、エラーの応答、時間切れ)、表示してから終わる(§5.2)--ignore-exit-request: 版2でexit_requested: trueを受けても終了しない(応じないUIを模す)observation §10の形式(runtime_event event=<出来事>、key=value、エスケープの規則)で出す。
出来事(event) |
出すとき | 追加の項目 | レベル |
|---|---|---|---|
target_registered |
登録した | 監視対象キー、registered_at |
INFO |
target_unregistered |
解除した | 監視対象キー、registered_at |
INFO |
registry_write_failed |
本体・警告ファイルを書けず、変更を拒否した(起動時の復帰を含む) | file(ファイル名)、operation、error |
ERROR |
registry_recovered |
起動時に自動復帰した | kind、restored_generation_at、evacuated_file、targets(件数)、completed(手順をすべて終えたか) |
WARNING |
registry_generation_unreadable |
読み込めない世代を飛ばした | file |
WARNING |
registry_generation_failed |
世代を作れなかった(§4.7) | error |
WARNING |
registry_generation_recovered |
作成に失敗していた世代を作れた | stale_since |
INFO |
registry_prune_failed |
古い世代を削除できなかった | file、error |
WARNING |
recovery_warning_acknowledged |
復帰警告を確認済みにした | warning_id、kind |
INFO |
exit_requested |
終了の要求を受け付けた | process_instance_id等の共通の項目、created、deliverable |
INFO |
exit_request_delivered |
終了の要求を、そのプロセスへのheartbeatの応答に初めて載せた | 共通の項目、requested_at |
INFO |
403・409等)は、ログに出さない(運用者の操作の結果であり、応答で分かるため)。persist_failedだけは、原因の調査のためにregistry_write_failedで出すerrorには、例外の種類と内容を出す。トークンは含まれないファイルの入出力は一時ディレクトリで行い、書き込みの失敗は書き込み関数の差し替えで起こす。時刻は時計を差し替えて確かめる。
| 対象 | 確かめること |
|---|---|
| 認証 | 管理APIが、トークンなし・不一致・会話用トークンで401(文字列のdetail)、閲覧トークンで403 forbidden、管理トークンで通る。BRAIN_ADMIN_TOKENが空なら、閲覧トークンを含めて常に401(403にならない)。状態取得が閲覧・管理の両方で読める。管理トークンで会話・heartbeatが401。全経路でトークンなしが401(observation §14.1の網羅テストに管理APIを加える) |
| 判定の順序 | 閲覧トークン+停止処理中は403、トークンなし+停止処理中は401。停止処理中+形式違いは503。形式違い+未登録は422 |
| 停止処理中 | 管理API4本が503 brain_stopping。heartbeat・終了通知・停止への応答・状態取得は受け付ける |
| 登録 | 観測中(稼働中・期限切れ)のキーを登録できる(201、registered_at)。観測していないキーは409 not_observed、登録済みは409 already_registered(観測していなくてもalready_registered)。形式違いは422。登録すると状態取得のtargetsに現れ、プロセスがunregistered_processesから移る |
| 解除 | 未登録は404 not_registered。解除で最終観測の要約が消え、プロセスと終了の要求は残り、未登録のUIとして現れる |
| 永続化 | 登録・解除のたびに本体と世代が書かれる。本体の書き込みが失敗すると500 persist_failedで、メモリ・状態取得・本体が変わらない。世代の作成だけが失敗すると変更は成功し、registry.backupがstale、次の登録で世代が作られokに戻る。起動時に最新の世代が本体と違えば世代を作る。有効な本体があり世代が0件で起動すると世代を作る(最初の登録で世代の作成だけが失敗した場合)。ファイル・ディレクトリの権限 |
| 自動復帰 | §4.5の表の各行(本体あり、初回導入、本体なし+世代あり、破損+世代あり、破損+読める世代なし、本体なし+読める世代なし)。読めない世代を飛ばしてより古い世代へ戻る。schema_versionが知らない値・重複キー・形式違い・saved_atの欠落や形式違いを破損とする。退避はコピーで、本体が残る。戻したregistered_atが保たれる。退避・警告の保存・本体の書き直し・完了の記録の各段の失敗で、後の段を行わず、recovery_pendingがtrueになり、次の登録で再試行される。各段で失敗した後にBrainを再起動すると、元の種別(破損/消失)のまま復帰を再開し、警告が重ならず、識別子・発生日時・種別が変わらない(完了の記録だけの失敗は、再起動で復帰済みになる)。壊れた本体の内容が変わっていれば、別の警告になる |
| 警告ファイル | 再起動しても警告が残る。確認済みにすると消え、再起動しても戻らない。未知・確認済みの識別子は404 unknown_recovery_warning。保存の失敗で500、警告は残る。警告ファイルの破損でwarnings_unreadable1件の新しいファイルになり、壊れたファイルが退避(コピー)される。警告ファイルの退避・書き込みの失敗でrecovery_pendingがtrueになり、警告がメモリ上から状態取得に出続け、再起動で修復をやり直す。recovery_pendingの間の確認は、再試行に失敗すると500で警告が残る。世代から戻しても、警告が増減しない |
| 世代の整理 | 保持期間より古い世代が消え、新しい10個は期間によらず残る。削除の失敗で変更が失敗しない |
| 終了の要求 | 記録のあるプロセスに200(created: true)。再送はcreated: falseでrequested_atが同じ。未知・破棄済みは404 unknown_process。版1・非対応版はdeliverable: false、版2は期限切れでもtrue。heartbeatで記録を更新しても要求が残る。終了通知で記録とともに消える |
| 版2 | 版2のheartbeatの応答にexit_requestedが常にあり、要求の後にtrueになる。版1・非対応版の応答の形が版1のまま(キーの集合が一致する)。停止処理中に入る前の要求は、停止処理中の応答にも載る(stoppingと同時)。停止処理中に終了通知を受けたプロセスを待たない。supported_protocol_versionsが[1, 2]。テストUIが、終了通知に失敗しても(Brainに接続できない、エラーの応答)終了する |
| 状態取得 | schema_version 2の項目(registered_at、exit_requested・exit_requested_at・exit_request_deliverable、recovery_warningsとその並び順(古い順)、source_sha256が出ないこと、registry)。トークンの値・パスの絶対パスが含まれない |
| 設定 | §8の条件を満たさない値で設定の誤りになる |
| ロック | 登録・解除の最中に状態取得・heartbeatが、ファイルの入出力を待たない(runtime_guardの下でregistry_guardを取らない) |
WSL2のBrainを#24の版に更新し、BRAIN_ADMIN_TOKENを設定して行う。管理APIはcurl -H "Authorization: Bearer $BRAIN_ADMIN_TOKEN"で呼ぶ(画面での確認は#17の後)。トークンの値は画面に出さない(.envから読む)。
| ユースケース | 手順 | 期待結果 |
|---|---|---|
| UC-D06 | 音声UIが動いている状態で、状態取得のunregistered_processesから音声UIの監視対象キーを取り、登録する。同じキーでもう一度登録する。閲覧トークンで登録・解除を呼ぶ。音声UIのtarget_idを変えて起動し直し、旧キーを解除し、新キーを登録する |
音声UIがtargetsに移りrunning。2回目は409 already_registered。閲覧トークンは403 forbidden。変更後は新キーだけが登録され、旧キーの要約は残らない |
| UC-D02(Brain側) | 登録済みの音声UIを止める | targetsから消えず、not_running(終了通知)になる(画面での確認は#17と共同) |
| UC-R07 | 未登録のテストUIを起動し、止める | targetsに影響せず、unregistered_processesから消える |
| UC-D07(シナリオ1) | 登録がある状態で、Brainを止め、本体を壊して(echo broken > registrations.json)起動する。次に本体を消して起動する |
どちらも登録一覧が直前の状態に戻り、音声UIと会話は何もせずに使え続ける。recovery_warningsに破損・消失の警告(戻した世代の日時、退避したファイル名)が出る。Brainを再起動しても警告が残り、確認済みにすると消え、再起動しても戻らない |
| UC-R08(Brain側) | テストUIを版2・版1・--ignore-exit-requestで起動し、それぞれに終了を求める。同じ要求を再送する。終了したプロセスにもう一度求める |
版2は終了し、一覧から消える。版1はdeliverable: falseで、稼働中のままexit_requested: trueと時刻が見える。--ignore-exit-requestはdeliverable: trueのまま稼働中で、要求が見える。再送はcreated: falseで時刻が同じ。終了したプロセスは404 unknown_process |
| UC-R08(停止処理中) | Brainの停止処理中に終了を求める | 503 brain_stopping |
| UC-R05(登録済み) | 登録済みの音声UIを動かしたまま、Brainを再起動する | 起動直後はunknown、次のheartbeatでrunning(observation §14.2で#24の後に回した確認) |
音声UIでのUC-R08は、#23の版2対応の後に行う。
runtime-management-runbook.md)に加えること.envに置き、値を画面に出さない作り方BRAIN_REGISTRY_DIRの中身(本体と世代)を別の場所へコピーする手順と頻度の目安(登録を変えた後)registrations.jsonに置き、Brainを起動する(正式な停止・起動の手段はUI。systemdは土台であることを、既存のrunbookの書き方に合わせる)registry.backupがstale、recovery_pendingがtrueのときの確認: journalのregistry_generation_failed・registry_write_failedで原因(容量不足・権限等)を確かめて取り除くconfig.py: §8の設定項目と検証server.py: 管理トークンの認可(403の判定を含む)、管理API4本、状態取得のschema_version 2、lifespanでの登録情報の読み込み(観測用スレッドの起動より前)registry.py): 本体・世代・警告ファイルの読み書き、自動復帰、世代の作成と整理、registry_guardruntime.py: 版2、終了の要求の記録、最終観測の要約の削除、観測済み・記録の有無の確認tools/runtime_test_ui.py: 版2対応(§9)| 文書 | 反映すること | 時期 |
|---|---|---|
runtime-management-design.md |
§3.1.1「世代(履歴)」の「最新の世代は常に本体と同じ内容になる」を、世代の作成に失敗した場合の縮退(本体を正として続け、状態取得で警告し、次の書き込みで再試行する。失敗している間は最新の世代が本体より古い)を許す表現に改める。§12.2の、本書で決めた項目(拒否の理由の表現、ファイルの置き場所・形式・暫定値、復帰警告の確認の形式、終了の要求の形式と版2の差分)を解決済みにする。§14に経緯を加える | 本書の合意時 |
runtime-management-detail-observation.md |
§3.1・§3.2の状態取得の認証を「閲覧または管理トークン」に改める。§4.6の対応する版の一覧と版ごとの差分の置き場所として、本書§5を参照する。§8.1のrecovery_warningsの注記を、本書§6への参照に改める。§17に経緯を加える |
本書の合意時 |
runtime-management-runbook.md |
§12 | 実装時 |
| AiChat(#23) | 版2の契約(本書§5)。AiChatのファイルは変更しない。セッションブリッジでWindows側のセッションに伝える | 本書の合意後 |
なし
運用者の方針(§17)に無く、本書の草案で決めた点である。CODEXのレビュー(#6709)で9件とも賛成を得た。3・5は、レビューの指摘1・3を反映して補った。
403でなく401とする(§3.2)already_registeredを先に返す(§3.4)registry.recovery_pendingで示し、次の登録・解除・復帰警告の確認と次回の起動で再開する。壊れた本体はコピーで退避して残し、同じ事故は1件の警告で扱う(§4.5)registry.backup(条件が解消すれば自動で戻る状態)で示す(§4.7、§6)BRAIN_REGISTRY_DIRとして設定で変えられるようにし、既定をロックファイルと同じkeina-brainの下のregistryとする(§4.1)schema_version 1を並行して提供しない(§6)| 日付 | 決めたこと | 理由 | 却下・置き換えた案 | 根拠 |
|---|---|---|---|---|
| 2026-09-26 | 管理トークンBRAIN_ADMIN_TOKEN(未設定なら管理APIは401、他のトークンと同じ値は設定の誤り)、管理API4本のパス、状態取得のschema_version 2、解除時の最終観測の要約の削除を、Claude案のとおりとする |
CODEXも賛成した(運用者の判断) | — | #6700(A・B・F・G)、#6701 |
| 2026-09-26 | 閲覧トークンでの管理APIは403、停止処理中は503とする。503は#24の管理APIに限り、heartbeat・終了通知・停止への応答は停止処理中も受け付ける。判定は401/403→503→入力・状態の検証の順とする。not_observedは409とする。未知・確認済みの復帰警告の識別子をエラーの契約に加える |
停止シーケンスに要る管理プロトコルを止めない。権限の無い呼び出しにBrainの状態を漏らさない。not_observedは形式の誤りでなく、今の状態との競合である(CODEXの指摘、運用者の判断) |
「停止処理中は書き込みをすべて503」(範囲が曖昧)。not_observedを422とする案 |
#6701(C) |
| 2026-09-26 | 本体をアトミックに保存できた時点で登録変更を成功とし、その後に世代を作る。世代だけが失敗しても本体を戻さず、状態取得で運用者に分かる警告を出し、次の書き込みで世代の作成を再試行する。設計書§3.1.1の「最新の世代は常に本体と同じ」を、この縮退を許す表現に改める | 世代は本体の写しであり、本体が正である。ログだけでは運用者が気づけない。二重の障害で直近の変更を失う危険は、復帰警告と観測一覧からの再登録で補えるため、個人・小規模の運用では単純さを優先する(運用者の判断) | pending世代とrevision_idによる小さなトランザクション(CODEX #6701。この規模には過剰とCODEX自身も#6703で取り下げた)。世代の失敗をログだけにする案(Claude案) |
#6701(D)、#6703 |
| 2026-09-26 | 版1・非対応版のプロセスへの終了の要求も受け付け、deliverable: falseを返す。再送では最初の要求時刻を保つ。版1のheartbeatの応答は変えず、版2だけにexit_requestedを加える。終了の要求APIはaccepted・created・requested_at・deliverableを返す。状態取得にはexit_requestedとexit_requested_atの両方を含める |
設計書§7.8は、要求に応じないUIが「求めた後も稼働中」と分かることを求める。再送で時刻を更新すると、いつから応じていないかが分からなくなる(運用者の判断) | 版1のプロセスへの要求を拒否する案 | #6700(E)、#6701(E・F) |
| 2026-09-26 | CODEXの草案レビューを反映した。(1) 復帰の途中で失敗・再起動しても元の種別と警告を保つ: 壊れた本体・警告ファイルはコピーで退避して残し、警告にrecovery_completedとsource_sha256を持たせ、同じ事故は1件の警告で再開する。警告ファイルの修復の失敗もrecovery_pendingとし、復帰警告の確認も先に復帰を終えられなければpersist_failedとする。evacuated_file: nullの表示を「該当なし」と「退避未完了」に分ける(§4.5、§4.6、§3.6)。(2) 管理トークンが未設定なら、閲覧トークンも401とする(§3.2)。(3) 読み込める世代が0件の場合も起動時に世代を作り、saved_atを検証する(§4.2、§4.5、§4.7)。(4) 版2のUIは、終了通知の成否にかかわらず終了する(§5.2、§9)。任意の提案3件(状態取得のexit_request_deliverable、backup_stale_sinceの意味、警告の並び順)も採った(§6) |
指摘1: 退避を移動で行うと、途中で再起動したときに「破損」が「消失」に変わり、元の理由と退避ファイルの対応が失われ、警告が重なる。指摘2: 未設定時の結果が二通りに読めた。指摘3: 最初の登録で世代の作成だけが失敗した場合が、再試行の条件から漏れていた。指摘4: 終了通知の失敗で、運用者が終了を求めたUIが終われなくなる。任意の提案は、#17が版の知識を重複して持たずに済み、解釈が1つに定まる | 草案の各記述(本行で置き換えた)。退避をos.replace(移動)で行う案。pendingの世代・revision_idによる復帰の再開(重いため採らない。CODEXも不要とした) |
#6709 |
| 2026-09-26 | 本書に運用者が合意した。合意に合わせ、設計書§3.1.1の世代の記述を縮退を許す表現に改め、observation詳細設計に本書への参照を加えた(§14) | CODEXのレビューの合意の条件4点と任意の提案3件を反映した | — | #6710 |