終章:IMEからロボットへ

四本脚のロボットに、最後にコマンドを送る関数があります。名前はexecuteといいます。関数の中身は数行だけです。受け取ったトルク値を、そのまま送り出す(publish)だけです。
この関数が呼ばれる直前には、もう一つの処理があります。ウォッチドッグ(処理が一定間隔で反応しているかを見張り、途絶えていれば異常とみなす仕組み)が、直前のtick(制御プログラムが一定周期で繰り返す処理の一区切り)が生きているかどうかだけを確認します。観測がいつ取得されたものかは、ここでは確認されません。生きているかどうかと、新しいかどうかは、別の問いです。ここでは前者しか問われていません。
awaseのEffect dispatchの直前にも、よく似た形の一瞬がありました。キューに積まれたコマンドを送り出す直前、そこにあるのはOS側の受け口が生きているかという確認だけで、そのコマンドがどの文脈で生成されたかまでは、通常は問われません。
ロボットの場合、この一瞬の重みはIMEより重くなります。IMEが取り違えるのは一文字ですが、ロボットが取り違えるのは、地面を蹴る力の向きや大きさです。古い観測に基づく指示がそのまま実行されれば、ロボットは足場のない方向へ体重をかけてしまうかもしれません。取り消せないという性質は同じでも、取り消せなかったときの結果は、文字よりも重くなります。
前章の最後で、この「実行直前の空白」は「後に調べたロボットランタイムにも存在していた」と書きました。ここからは、その実物を見ていきます。相手は、著者が別に設計しているロボット(Unitree Go2)向けアプリケーションランタイム、Go2 Runtimeです。この問題が、本当にIME固有ではないのかを確かめます。
すでに観測の型はあった
Go2 Runtimeには、観測の鮮度を表す型が、すでに存在していました。
pub enum Observed<T> {
Fresh(T),
Stale(T),
Unknown,
Broken(BrokenReason),
}
Observed<T>は、値をそのまま保持するのではなく、その値が新鮮か、陳腐化しているか、そもそも取得できていないかを型で区別します。BrokenReasonにも、数値として不正(Nan)・観測が届かなかった(Lost)・複数の観測が食い違う(Contradiction)・想定した形式と一致しない(SchemaMismatch)という具体的な破損の種類が並び、「分からない」を一つの雑多な例外として握りつぶさない設計でした。awaseのshadow state・belief(第5章)で、センチネル値を専用の型へ置き換えた道筋と、発想の芯は同じでした。
信念の側にも、確信度と裏付けの種別を保持するBeliefView<T>という型がありました。確信度はUnknownからPeakまでの五段階、裏付けは直接観測か複数センサーの一致か推論かといった種別で表されます。鮮度の情報は、この型には含まれません。鮮度はObserved<T>の側だけが持つ、という一元化がすでになされていました。
BeliefView<T>という名前は、当初BeliefSnapshot<S,B>という形で設計され、後に現在の形へ整理されています。awaseでも、BeliefStoreという名前を何度か付け替えながら、観測から信念を導く仕組みを整理してきました(第6章)。二つのプロジェクトは、互いを知らないまま、同じ改名の道筋をたどっていました。設計が一度で定まらず、何度も呼び名を変えながら収束していくという経験は、どちらのプロジェクトにも共通していました。
複数の裏付けを重ね合わせて一つの確信度に落とし込む、という考え方も共通していました。awaseは、複数の観測源を信頼度で重み付けして一つの信念へ融合する場面がありました。Go2 Runtimeのevidenceという区分も、直接観測か複数センサーの一致かを見分ける点で、根は同じ発想でした。片方が先に思いついた技法ではなく、同じ種類の不確実性を相手にすれば、同じ形の解決策に行き着く、という例がここにもう一つ増えました。
設計者自身、危険性を知っていた
Go2 Runtimeの設計文書には、鮮度を無視することの危険性を指摘した一文が残っています。
「stale but not missing が最も陰湿」
これはField<T>という型――信念を経由しない生のセンサー値にも、鮮度と品質を必須で添付する型――の根拠として書かれたコメントです。文脈は、オドメトリ(自己位置の推定値)についてでした。オドメトリは速度ガバナ(算出した速度指令を安全な範囲に収まっているか最終確認する制御ロジック)が読み、将来の経路計画も読む値です。値が届かないことより、古い値がまだ届いているように見えることの方が危険だと、設計者自身がこの一文で認識していました。
値が消えれば、それを使う側は「ない」ことに気づけます。しかし値が古いまま届き続ければ、使う側には新しい値と見分けがつきません。速度ガバナは、数百ミリ秒前の位置を、いまの位置として使い続けてしまいます。これは、awaseで固定待機時間が状態表現の代わりにならなかった事情(第4章)と、根が同じ問題です。
つまり、この危険性は見落とされていたわけではありません。設計として言語化され、Observed<T>やField<T>という形で、観測を受け取る側の危険は、すでに一部が塞がれていました。この章で見ていく空白は、無知の産物ではなく、対処の届いていない範囲の問題です。
危険性を言語化していたことと、その危険性をすべての場所で塞いでいたことは、別の事柄です。この区別は、awaseの開発でも繰り返し確認したことでした。分からない状態をセンチネル値として握りつぶさないと決めたあとも(第5章)、その決定が及んでいない場所で、同じ種類の不具合が形を変えて再発しました(第6章)。設計原則を一度立てたことは、その原則があらゆる境界に適用されたことを意味しません。
それでも、実行直前の空白は塞がれていない
Executorは、検証済みのコマンドを実行するための境界です。
pub trait Executor {
fn execute(&self, cmd: &Go2Cmd<Checked>, ctx: &TickCtx) -> Result<(), ExecutorError>;
}
Go2Cmd<Checked>という型は、apply_safetyという検査を通過した証拠を運びます。しかし、この型が証明しているのは「検査を通過した」という過去の事実だけです。「いま実行してよい」という現在の事実は、証明していません。型は、検査の瞬間の写真であり、実行の瞬間の写真ではありません。
このADR(ADR-R009、「Executor Seam」という名で管理されている設計文書)は、以前は実装されていない仕様書の段階にとどまっていました。その後、2026年6月11日に仕様が固まり、同月26日には実装も完了しています。実際の呼び出し箇所を確認すると、executeの直前にあるガードは、ウォッチドッグによる生存確認だけでした。世代(epoch)を照合するような、鮮度の再検証は行われていません。実装が仕様書の段階から先へ進んでも、この空白は自然には埋まりませんでした。設計として意図的に埋めない限り、実装の進行だけでは解消しない種類の空白だということが、ここで裏付けられました。
起こりうる失敗の形を、一つだけ挙げておきます。安全性検査を通過した時点では、コマンドは正しい前提のもとで組み立てられていました。しかしキューで順番を待つ間に、ロボットの姿勢や周囲の状況は変化しているかもしれません。executeは、その変化を知る手段を持たないまま、検査時点の前提でコマンドを送り出します。これは、awaseでフォーカスが変わったあとに古い文脈のコマンドが送信されていた事情(第12章)と、同じ形をしています。
awaseとGo2 Runtimeを並べると、同じ形の空白が見えてきます。
| awase | Go2 Runtime |
|---|---|
| IME状態の観測 | センサー・WorldStateの観測 |
| FocusEpoch | TickId(tick単調カウンタ。epoch照合には未使用) |
| Effectのdispatch | ロボットコマンドのexecute |
| 取消不能なOS操作 | 取消困難な物理的な動作 |
| dispatch直前の空白 | execute直前の空白 |
対応関係を図にすると、FocusEpochだけが「構造は同じだが用途が違う」という、少し 異なる形の一致になっていることが分かります。

TickIdは単調に増加するという構造こそFocusEpochと同じですが、コマンドの世代と照合する用途には使われていません。カウンタという道具はすでにそこにありながら、観測を受け入れてよいかを判定する関所(admission gate)としては使われていない、という状態です。
なお、Go2 RuntimeにはControlLeaseという、複数の操作者の間で操作権を切り替える仕組みも別に存在します。これはフェンシング(古い操作者からの指示を無効化し、いま操作してよいのは誰か一人だけに確定させる排他制御の方式)と呼ばれる考え方にもとづきます。Gateway側の早期フィルタと、Runtime tick側の最終判定という二段構えで、いま誰が操作してよいかを決める設計です。これは、一人の操作者のtick内で観測が陳腐化する、ここで扱っている問題とは別物です。フェンシングが守るのは「誰の指示か」であり、ここで足りていないのは「いつの観測に基づく指示か」を守る仕組みです。
FocusEpochの構造を持ち込めるとしたら
awaseがAcceptedObservationで解いた問題は、「admitを通らない限り、その値を構築できない」という制約を型に持たせることでした。同じ発想を、Go2Cmd<Checked>とTickCtxの間に持ち込めないか、という問いが自然に浮かびます。
TickCtxはすでにtick単調カウンタを持っています。apply_safetyを通過した時点のtick番号をGo2Cmd<Checked>に刻み、executeを呼ぶ直前で現在のtick番号と照合する。差が許容範囲を超えていれば実行せず、破棄するか再検証へ回す。これだけで、「検査を通過した」証明と「いま実行してよい」証明の間にあった空白は、構造として塞がれます。
ここで示したのは概念上の設計であり、Go2 Runtime側にまだ実装されているものではありません。awaseで実際に機能した型の作り方が、別のコードベースにもそのまま持ち込める形をしている、という見通しにとどめておきます。
どこまで一般化できるか
ここまでの観察から確実に言えるのは、一つのことだけです。IMEという領域で見つかった構造上の空白と同じ形のものが、ロボットランタイムという別の領域でも、独立に見つかったという事実です。
あらゆるPhysical AIランタイムに共通する一般原則だとまでは、まだ言えません。確認できたのは一つの実装だけであり、二つの独立した事例が一致したという段階にとどまります。「型は過去の証明であって、現在の証明ではない」という言い方は、この一致から導ける教訓の候補ですが、まだ本書の外にある多くの系で確かめられたわけではありません。
同じ形の空白は、IMEとロボット以外にも現れうる場所があります。GUIを自動操作するツールが、画面の状態を読み取ってからクリックを送るまでの間。分散システムのクライアントが、古いリーダー情報をもとにリクエストを送ってしまう間。これらは類推であり、確認済みの事実ではありません。この本で示せるのは、二つの独立した実装で同じ空白が見つかったという範囲までです。それ以上の一般化は、今後の観察に委ねます。
幸い、この空白は机上の議論にとどまりません。Go2 RuntimeのSimIoという模擬入力の仕組みは、関節の位置・速度・トルクといった状態を注入するinject_joint_stateのような関数を備えています。現時点では、注入した観測をそのまま返すだけの、単純な作りです。遅延・順序の入れ替え・重複したコマンドを模擬する専用の仕組みは、まだありません。しかし、admissionを通過した直後に古い観測を差し込み、executeが呼ばれるまでの間にそれがどう扱われるかを観察する実験であれば、いまのSimIoを少し拡張するだけで組めます。証明ではなく再現から始める、という本書の姿勢は、ここでも変わりません。この検証は、今後の課題として残っています。
最初の一文字に戻って
本書は、最初の一文字がローマ字のまま残った、という異常から始まりました。原因は、変換ロジックの誤りではありませんでした。IMEがいまどういう状態にあるかを、こちらが正しく把握できていなかったことでした。
その後、千六百を超えるコミットを重ねた道のりは、この一つの事実を、さまざまな場面で繰り返し学び直す過程でした。観測は、常に少し過去のものです。実行は、常にいまこの瞬間に対して行われます。この二つを同じ値として扱ったとき、awaseは何度も壊れました。そして、この二つを型として分けたときにだけ、壊れ方が減りました。
Go2 Runtimeという、著者と面識のない設計者が別につくった環境でも、観測と実行の間には同じ形の空白が残っていました。二つのプロジェクトは、示し合わせたわけではありません。それでも、同じ場所に、同じ形の問題を見つけました。IMEの一文字も、ロボットの一歩も、実行してしまえば取り消せません。取り消せない一歩の直前だけは、観測の鮮度を疑ってかからなければなりません。
awaseを書いていた当初、この問題を日本語入力エンジンに固有の癖だと考えていました。IMEという相手が、特に不誠実だからだと思っていました。しかし、Go2 Runtimeという別の相手を見たあとでは、そうは言えなくなりました。不誠実なのはIMEではなく、外の世界そのものでした。観測は届いた時点ですでに過去であり、それを疑わずに実行へ渡す設計だけが、繰り返し壊れていました。
外の世界を相手にするソフトウェアでは、観測したことと、いま実行してよいことを分けなければなりません。この一文は、IMEのために書いたものでした。しかし、この一文が指す境界は、IMEの外にもありました。
本書で追ってきたのは、一つのソフトウェアの千六百を超えるコミットそのものではなく、一つの区別が繰り返し必要になる場所の記録でした。その場所は、日本語入力エンジンの中にもあり、四本脚のロボットの中にもありました。次にどこで同じ形の空白に出会うとしても、確かめ方はもう分かっています。取消不能な境界の手前で、観測がいつのものかを、もう一度だけ問い直すことです。
この区別を保つことの難しさは、本書を書く過程そのものでも一度顔を出しました。やまぶきの内部構造を調べていた際、調査に当たったAIが一度、確かめてもいない実装を断定調で語ったことがあります。指摘を受けて実際にバイナリを解析し直すまで、その一文は根拠のない推測のままでした。観測していないことを断定してしまう失敗は、コードの中だけでなく、コードについて語る言葉の中にも、同じ形で現れます。
設計原則
観測の鮮度は、取得した瞬間に一度だけ確認して終わりにしてはいけません。取消不能な作用へ進む、最後の境界で再検証しなければなりません。
この原則は、観測から作用までの間に非同期の遅延が生じるすべての系に適用できます。単一のプロセス内で完結し、観測から作用までが同期的に進む処理には、通常あてはまりません。
実装の形は、鮮度を運ぶ専用の値(世代・epoch・タイムスタンプ)を型として持たせ、作用を実行する直前の境界でその値を照合することです。検査を通過した証拠と、いま実行してよい証拠は、別の型として扱います。片方だけを型で保証し、もう片方を暗黙の前提のまま残すと、空白はその境界に残ります。
この原則は、観測の陳腐化による誤作用は防ぎますが、他の要因による失敗までは保証しません。実行そのものが失敗する場合や、検証の基準自体が誤っている場合には、別の対策が必要です。再検証の仕組みを一度作ったあとも、それが実際にすべての実行経路を通っているかどうかは、別途確かめなければなりません。