Skip to content

feat(rpc): TSF の変更系送信を Change / Restore へ移行し、BG 系の応答型を確定する(#56 PR (b)) - #68

Merged
fukuyori merged 3 commits into
fukuyori:mainfrom
nick20002005:feat/rpc-change-migration-56b
Oct 9, 2026
Merged

fukuyori merged 3 commits into
fukuyori:mainfrom
nick20002005:feat/rpc-change-migration-56b

Conversation

@nick20002005

@nick20002005 nick20002005 commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

ホスト再起動後も、composition ごとに保持した読みと未確定ローマ字から編集を復元できるようにします。最初の変更は Restore.then で送り、返された世代を後続の Change に使います。復元は有効なフォーカス先へのキー入力に限り、タイマーは所有権を取り戻しません。復元した Space は Preedit に戻し、次の Space で変換します。

BG の状態と開始・取得結果を型で区別し、旧要求は宣言順とフィールドを保った _Reserved… にしてホストから Error を返します。読み取りにも所有者・世代の照合を加え、取得失敗で表示を空にしたり Idle と扱ったりしません。

受入条件と対応

  • A: crates/rakukan-engine/src/ffi.rs の ENGINE_ABI_VERSION と crates/rakukan-engine-abi/src/lib.rs の EXPECTED_ENGINE_ABI_VERSION を同時に 10 → 11。cpu / vulkan / cuda の 3 variant すべてが 11 を返す。every_backend_exports_abi11_and_exact_pending_romaji がテストで確かめるのは、そのとき実行しているビルドの variant の版と未確定ローマ字だけです(手元で実行したのは cpu と Vulkan feature です)。3 variant に共通という点は、版を返す export に feature の条件が無いことをコードで読んでの判断です。CUDA Toolkit がないため、CUDA ビルドでの実行は未確認です。
  • B: 読み取り要求の入口での bg_reclaim() は、engine_gen / owner の照合に通った後だけ。回収するのは bg_owner が無い Done だけで、自分の Done は読み取りでは回収しない。冪等(2 回目は状態不変)で、応答・再送に影響しない。この 3 点をテストで確かめる。checked_read_slot と rejected_reads_do_not_reclaim / reads_preserve_own_done_and_reclaim_orphan_once_without_touching_replay で確認します。
  • C: BgStart 内の自分の Done の回収は try_lock() ではなく blocking lock(空振りを起こさない)。poison は読み取り失敗と同じ扱い。WorkerBusy に自分の Done を含めない。start_bg / reclaim_done_from と、開始判定・ロック競合・poison のテストで確認します。
  • D: BgSlot の doc に、same_reading は pending があれば pending のキーで判定/順位は Queued > Running > Done > Empty/Queued は旧要求の変換が実行中で次の要求が待っている場合を含む、を書く。conv_cache::BgSlot の doc と classify_slot のテストに反映しています。
  • E: 同じ要求番号の BgTakeCandidates 再送では保持した BgTakeOutcome を返し、取り出しを再実行しない。テストあり。apply_recorded_change と bg_take_replay_uses_retained_outcome_without_taking_twice で、空の Taken も含めて確認します。このテストは、apply_change の BgTakeCandidates が呼ぶのと同じ take_bg を通します。NotYours(所有者なし・別の所有者で、変換器に触れない)と NotReady は、bg_take_touches_the_converter_only_for_its_owner で確認します。
  • 通信失敗で確定時の追いつきができなかった場合は「未収束」として学習を見送る(LearnAction::Skip を使う。判定は新設)。見送った事実をログに残す。catch_up_live_preview と新設 learn_live_action で処理し、Skip 判定をテストしています。アプリへの確定は現在のプレビューで続けます。
  • 読み取りの失敗(poison)はホストが既存の Response::Error で返し、TSF は ReadError として扱う。Rejected は照合に落ちた意味に限定する。poison_is_error_not_idle_or_rejection と TSF の ReadError 分類テストで確認します。

Space の WorkerBusy は独立した分岐で辞書候補を表示し、MODEL_WAIT の上限を適用します。Waiting とライブ WorkerBusy の上限は 10 秒です。ライブの時計はタイマーを再開しても維持し、別の BG 状態を観測した場合に解除します。時計のテストを追加しています。

検証

  • cargo fmt --all -- --check:成功です。
  • release Clippy(-D warnings):TSF / RPC / engine-abi が成功です(10/9 の修正後に再実行)。engine / engine-host は 10/3 の結果で、10/9 の修正では再実行していません(engine は変更なし、engine-host は RPC の変更だけを受けます)。
  • release テスト:TSF 206 件、RPC 69 件が成功です(10/9 の修正後に再実行)。engine-abi 16 件、engine 278 件(既存 7 件は ignored)は 10/3 の結果で、10/9 の修正では再実行していません(engine-abi の変更はコメント 1 行です)。
  • Vulkan feature の ABI テストは成功です。CUDA は手元に Toolkit が無く、ビルドしての確認はしていません(ABI の版を返す export に feature の条件はありません)。
  • 実機では確かめていません。 インストールと、アプリでのホスト再起動・入力・表示の確認は未実施です。

C の補足(10/3 のご依頼の 3 点)

  1. 要求の入口の回収との関係。 入口の回収(bg_owner のない Done の回収)は try_lock のままです。空振りした Done は、10/2 の案どおり WorkerBusy に含めます(reclaim_unowned / checked_read_slot、テストは queued_running_and_missed_orphan_done_are_worker_busy)。blocking にしたのは、BgStart の中の、自分の Done の回収だけです。入口は読み取り要求でも通るので、待ちうる箇所を増やさないほうを選びました。
  2. 既存の呼び出し元への影響。 既存の関数は変えていません。conv_cache::reclaim_done_blocking() を新しく足し(ABI は engine_bg_reclaim_blocking)、呼ぶのはホストの start_bg が自分の Done を見たときだけです。try_reclaim_done() と、それを使う bg_start()(lib.rs)・start_load_model(ffi.rs)・bg_reclaim() → reclaim_nonblocking() の経路は、(a) のままです。Restore と BgReclaim の回収の挙動は変わりません。
  3. ロックの順序。 順序は「エンジンのロック → キャッシュのロック」の一方向です。キャッシュのロックを持ったまま別のロックを取る箇所はありません。ワーカーはエンジンのロックを取らず、キャッシュのロックを持つのは、pending を取り出して Running を書く区間と、Done を書く区間の 2 つだけです(変換そのものはロックの外です)。stall watchdog は stall_probe → キャッシュのロック(run_state / confirm_stalled)→ 解放 → health の順で、エンジンのロックは取りません。循環はありません。

待ちが長引いた場合。 blocking の待ちに上限は置いていません。理由は次の 2 点です。

  • キャッシュのロックを持つ区間は、どれも状態の読み書きだけで、変換も I/O もしません。ワーカーが Done を書く区間も、値の移動と notify_all だけです。
  • 変換が止まった場合、ワーカーはロックを持っていないので、BgStart の待ちは延びません。その場合は stall watchdog が同じロックで状態を読み、既存の手順でホストを終了します。

キー処理の中でこのロックを blocking で取る経路は、(a) 以前から take_ready / status / wait_done_timeout にあり、今回の追加で性質は変わりません。

テスト。 競合時に取り損ねず待つこと(reclaim_waits_for_contention_instead_of_missing_the_lock)と、poison を失敗として返すこと(poisoned_slot_and_blocking_reclaim_return_failure)はエンジン側にあります。ホスト側の poison → Response::Error は、読み取り(BgStatus)にテストがあります(poison_is_error_not_idle_or_rejection)。BgStart 側は bg_start_poison_is_error_and_does_not_start を足しました(状態を読む段階と、blocking の回収の段階の両方)。

合意に無い判断(ご確認ください)

  1. RPC protocol は v6 のままです。 (a) のときと同じく、v6 は未リリースなので版は上げていません。(a) だけの main のホストと (b) の TSF は wire 形式が合わないので、開発ビルドを入れ替えるときは engine / host / tsf をそろえてください。区別が必要なら 7 に上げます。
  2. ABI 11 に BgSlot 以外の追加があります。 未確定ローマ字の取得(復元元に必要)、poison を区別する取り出し・peek、blocking の回収(C)です。
  3. composition 依存の読み取りは、末尾に足した Request::Read { expect, request } に集約しました。 旧の読み取り variant(PreeditDisplay など)と ConvertSync / BgWaitMs も _Reserved… にしています。
  4. キー入力の入口で、所有権が失われていないかを読み取り 1 回で確かめています(失われていれば、その場で Restore します)。この読み取りは、入口の診断ログ用の bg_status と 1 回にまとめました(10/9)。ふだんの打鍵の往復数は、(a) までと同じです。
  5. フォーカスの確認: 復元してよいかの判定に、キー入力時の GetFocus と文脈の一致、DM 台帳での生存を使っています。フォーカス通知が遅れて届く間も最初の打鍵を扱えるよう、一致した DM をその場で記録します。

A〜E の内容は、10/3 の回答から変えていません。

10/5 のレビューへの対応(10/9)

範囲は、10/8 の訂正のとおりです。BgView::Unreadable、Restored.then の「適用しなかった」、poison 下で入力を続けるための変更は入れていません。

conv_cache の poison は Response::Error のまま

変換キャッシュの mutex が poison していると、apply_change の冒頭の回収と bg_slot()、dispatch_restore の bg_slot()、読み取りの checked_read_slot が失敗し、BG と無関係な変更・復元・読み取りも Response::Error になります。直していません。理由は次のとおりです。

  • 標準のタスク(cargo make build-engine / build-tsf)でビルドした配布物は、[profile.release] が panic = "abort" なので、panic で巻き戻しが起きず、mutex は poison しません。
  • poison が起きうるのは、巻き戻しのあるビルド(cargo test、-Profile debug)だけです。その場合は Error を返し、入力の継続は保証しません。
  • poison のとき conv_cache::status() は "idle" を返すので、変更要求で呼ばれる apply_health_action は、ホストの終了を要求しません。Restore は冒頭で早期 return するので、apply_health_action に届きません。

同じ内容を apply_change の doc コメントに書いています。既存の poison のテスト(poison_is_error_not_idle_or_rejection / bg_start_poison_is_error_and_does_not_start と、エンジン側のテスト)は、そのまま残しています。配布ビルドで panic を起こしての確認はしていません。

入口の読み取りを 1 回に

recover_on_input が、所有権の確認に使った BgStatus の値を返し、入口の診断ログはその値を使います。フォーカスの無い文脈(recover_on_input を呼ばない)と、復元した直後は、これまでどおり診断ログの側で 1 回読みます。WorkerBusy の時計の解除は、入口の読み取りでも bg_status() と同じ条件で行います。

変更の失敗を止める範囲

push_raw / force_preedit は Result を返し、呼び出し元(27 か所)は、失敗したらそのキーの処理を中断します。commit / reset_* / learn* / bg_reclaim は、後ろに読み取りが続かない終端の操作なので、警告ログのままです。

細かい点

  • 最初の変更が失敗したときの保存: 新しい composition の最初の要求が、何も送らないまま失敗した場合(Commit などの再送できない要求)は、手元の状態を保存しません。送った後の失敗(通信の失敗、Restore の拒否)は、再送用の要求を残すために、これまでどおり保存します。first_change_that_sends_nothing_is_not_saved で確認します。
  • コメント: abi/lib.rs(pending_romaji の上)、client.rs(ReadError の上)、protocol.rs(Response::Restored)、candidate_window.rs(機械置換で崩れた 3 か所)を、実装に合わせて直しました。
  • composition が無いときの bg_reclaim(): 所有権を持つ composition が無ければ、送らずに戻ります。warn は出ません。
  • WorkerBusy の待機表示: 10/1 の合意どおり、既存のモデル待ちの表示のままです。

テスト

  • E のテストは、take_bg(apply_change の BgTakeCandidates の本体)を通す形に直しました。取り出しの回数は、偽の変換器の側で数えます。
  • codec.rs に appended_variants_keep_their_slots を足し、Request::Read(Change の 2 つ後ろ、56)と、Response::Bg / Response::OptionalString(Changed の 1 つ後ろ・2 つ後ろ、17 / 18)の番号を固定しました。

既知の制限

  • Restore は input_log を復元しません。 EditState が持つのは、読みと未確定ローマ字だけです。ホストの再起動をまたいだ composition では、打鍵ログを使う操作(F9 / F10 のラテン変換など)が、再起動前の打鍵を対象にできない可能性があります(コードを読んでの判断で、実機では確かめていません)。(b) の範囲外としています。
  • poison: 上のとおり、巻き戻しのあるビルドでは、poison の後の入力の継続を保証しません。

Refs #56

🤖 Generated with Claude Code

@nick20002005

Copy link
Copy Markdown
Contributor Author

PR #67 の 10/3 のコメント(C について本文に書く 3 点と、レビューで見る 3 点)を確認しました。この PR はそのコメントの前に出していたので、本文に「C の補足」の節を足しました。

  • 入口の回収は try_lock のままで、空振りした Done は WorkerBusy に含めます。
  • 既存の回収の関数は変えず、BgStart 用に blocking の関数を 1 つ足しています。Restore と BgReclaim の経路は変わりません。
  • ロックは「エンジン → キャッシュ」の一方向で、待ちに上限は置いていません。理由は本文に書きました。

レビューで見る 3 点のうち、BgStart の poison → Response::Error は、ホスト側のテストが抜けていました。bg_start_poison_is_error_and_does_not_start を追加しました(start_bg がエンジンに求める操作を trait に切り出しただけで、挙動は変えていません)。

@fukuyori

fukuyori commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

訂正(10/5): 初版の poison の節に、2 か所の誤りがありました。(1) apply_health_action は変更要求では呼ばれますが、poison は idle と判定され、ホストの終了につながりません(Restore は早期 return で届きません)。(2) InputChar の文字が復元元から落ちるのは、poison が解消した後に Restore が成功した場合です。該当箇所は、本文で直しています。他の指摘は変わりません。

PR #68 を確認しました。A〜E と、10/3 に求めた C の補足 3 点は、本文のとおり実装されていることをコードで確かめました。その上で、マージ前に直してほしい点が 1 つ、確認したい点が 2 つ、細かい点が数件あります。コードを読んでの照合で、実機では確かめていません(実機の確認は、(b) の修正が済んでから別に行います)。

確認できたこと

  • A: ENGINE_ABI_VERSION(ffi.rs)と EXPECTED_ENGINE_ABI_VERSION(engine-abi/src/lib.rs)は、どちらも 11 です。追加した 5 つの export は、名前とシグネチャが両側で一致しています。
  • 旧 variant の _Reserved… 化: 宣言位置とフィールドが保たれています。新しい Request::Read、Response::Bg、Response::OptionalString は末尾の追加です。
  • 照合の順序: session_tsf → gen → owner → seq です。ロックの順序は、エンジン → records、エンジン → キャッシュで一貫しています。
  • B・C: 入口の回収は、bg_owner がない Done だけです。照合に落ちた読み取りでは回収しません。blocking の回収は、start_bg が自分の Done を見たときだけです。
  • TSF: 最初の変更は Restore{then} で送り、Restored の世代を後続の Change.expect に使います。WorkerBusy は独立した分岐で、上限は 10 秒です。

修正してほしい点

1. conv_cache の poison で、変更系も Restore も通らなくなる

apply_change は、種類を問わず冒頭で reclaim_unowned(...)? と eng.bg_slot()? を呼びます(server.rs:1040-1041)。dispatch_restore も、eng.bg_slot() の失敗で Response::Error を返し、復元の処理に入りません(server.rs:939)。そのため、キャッシュの mutex が poison したままだと、そのホストでは、BG に関係ない Commit / ResetAll / PushChar / Learn も、Restore も、Response::Error になります。

  • 変更系の Error は記録され、同じ番号の再送にも返ります。Restore は早期 return なので、応答は保存されません(server.rs:939-941、保存箇所は 958)。
  • 復元で抜けることもできないので、そのホストは、ホストを再起動するまで composition の変更・復元ができません。
  • (a) までは、poison を idle 扱いにして、入力は動き続けていました。(b) で、動作が変わる点です。
  • poison は、キャッシュの mutex を持っているスレッドが panic したときに起きます(conv_cache.rs)。発生頻度が低いかどうかは、コードだけでは分かりません(※推測です。頻度が低い場合でも、起きたときの影響は上の範囲です)。

直すなら、BG と無関係な要求では、BG の状態の読み取りの失敗で、処理全体を失敗させない形を提案します。ただし、次の点を決めてからの評価になります。

  • InputChar は応答に BgView を含み、処理の後にも BG の状態を読みます(server.rs:1058-1062)。冒頭の読み取りを省くだけでは、この経路の失敗は解消しません。
  • reclaim_unowned は、Done を回収する副作用を持ちます。無関係な要求で回収を省いた場合の扱いも、決めてください。

直さない場合は、(b) で poison を致命として扱う理由(ホストを止めて再起動させたほうが安全、など)と、それがどの経路で実現されるか(poison のとき conv_cache::status() が "idle" を返すので、変更要求で呼ばれる apply_health_action がホストの終了を要求しない点、Restore は冒頭で早期 return して apply_health_action に届かない点を含む)を、コメントと PR 本文に残してください。

関連として、InputChar は push_char を適用してから、応答に載せる BG の状態を読みます(server.rs:1043-1062)。ここで poison により失敗すると、文字はホストに入ったまま Error が返り、TSF は state.edit を更新しません(rpc_composition.rs:174-199)。poison が解消した後(たとえば、ホストの再起動後)に Restore が成功すると、その 1 文字が復元元から落ちます。poison が続く間は、次の入力で recover_on_input が先に bg_status を読み、失敗して返るので、Restore は送られません(rpc_composition.rs:243-256)。現在の TSF の呼び出しは input_char(…, None) で、start_bg は呼ばれません(on_input.rs:145-148 など)。したがって、原因は start_bg ではなく、応答用の bg_slot() の読み取りです。

確認したい点

2. 打鍵ごとの RPC の追加

本文の「合意に無い判断」の 4 番(キー入力の入口の読み取り 1 回)について、体感への影響は、実機で測らないと分かりません。次の 2 つのどちらを意図しているかを書いてください。

  • 診断ログ用の bg_status と 1 回にまとめる(本文にある案)。
  • 今のまま、別の往復として残す。

私の判断は、実機の結果を見てから行います。

3. 変更の失敗が握りつぶされる箇所

DynEngine::unit() は、変更の失敗を警告ログだけで続行します(rpc_composition.rs:438)。push_raw / force_preedit / bg_reclaim / commit / reset_* / learn* が通る経路です。例えば、push_raw が通信失敗や Unresolved で失敗したあと、続く preedit_display() は古い値で成功しうるので、打鍵が黙って落ちます。入口の recover_on_input が先に走るので、頻度は低いと見ています(※推測です)。「取得失敗を空値に戻さない」という方針が、変更系にも及ぶかどうかを教えてください。

細かい点

  • 最初の変更が失敗したときの状態。 change_local は、スナップショットが無いとき、最初の要求が再送できないもの(Commit など)だと、apply_via が失敗します。それでも save(state) が走り(rpc_composition.rs:289)、世代なしの RemoteComposition が残ります。次の復元までの間、preedit_display() などが NoComposition で失敗します。経路は稀ですが、失敗したときは保存しない形が安全です。
  • Restore は input_log を復元しません。 EditState は、読みと未確定ローマ字だけです。再起動をまたいだ composition で、F9/F10 のラテン変換が効かない可能性があります(※推測です)。(b) の範囲外なら、そう書いてください。
  • 残っているコメント。 abi/lib.rs の「BG 変換を起動する。true = 起動した」、client.rs の「Change / Restore を送れなかった…」(ReadError の上)、protocol.rs:583 の「編集状態の詳細は (b) で応答へ追加する」、candidate_window.rs の機械置換で崩れたコメントが、実装と合っていません。
  • WorkerBusy の待機表示。 MODEL_LOADING_STATUS と MODEL_WAIT_SLOW_STATUS を流用しているので、利用者には「モデル読込中」と見えます。意図したものなら、そのままで構いません。
  • composition が無いときの bg_reclaim()。 change_local が失敗し、unit() が不要な warn を出します。

テストの検証力

  • bg_take_replay_uses_retained_outcome_without_taking_twice は、apply_recorded_change に渡すクロージャが Taken を直接返します。記録と再送の仕組みの確認にはなりますが、apply_change の NotYours / Taken の分岐は通っていません。E の受入条件の達成は、この範囲での確認、と読みます。
  • every_backend_exports_abi11_and_exact_pending_romaji(ffi.rs)は、現在のビルドの版と pending_romaji だけを確認します。cpu / vulkan / cuda の 3 variant が 11 を返すことは、このテストでは検証できません。本文の「3 variant 共通」は、コードを読んだ上での判断、と書き分けてください。
  • codec.rs の判別子のピン留め(ConvertSync + 2)は、新しい末尾の Request::Read、Response::Bg、Response::OptionalString を固定していません。末尾追加の順序を固定するテストを足してください。

判断

1 は、次のどちらかを、マージの前提にします。停止動作を直す、または、致命として扱う理由とその経路をコメントと PR 本文に残す(その場合は、私が妥当かどうかを判断します)。2・3 と細かい点は、回答または修正の後に再確認します。実機での復旧動作の確認は、(b) の修正が済んでから、別に行います。

@fukuyori

fukuyori commented Oct 5, 2026

Copy link
Copy Markdown
Owner

先ほどのレビューコメント(https://github.com/fukuyori/rakukan/pull/68#issuecomment-5987071786)の poison の節に、2 か所の誤りがあり、コメントを直しました。修正してほしい点 1 の趣旨と、他の指摘は変わりません。

  • apply_health_action は、変更要求では呼ばれます(server.rs:866-868)。ただし、poison のとき conv_cache::status() は "idle" を返すので、ホストの終了につながりません。Restore は冒頭で早期 return するため、届きません。
  • InputChar の文字が復元元から落ちるのは、poison が解消した後(たとえば、ホストの再起動後)に Restore が成功した場合です。poison が続く間は、次の入力の recover_on_input で失敗して返るので、Restore は送られません。

すでに初版を読んでいた場合は、上のコメントの訂正後の内容を確認してください。

@nick20002005

Copy link
Copy Markdown
Contributor Author

レビューをありがとうございます。訂正後の内容で、f2fc9bb のコードと照合しました。1 の停止動作、3 の握りつぶし、細かい点、テストの 3 件とも、指摘のとおりです。実装の前に、方針を示します。

1. conv_cache の poison — 直します

(a) までと同じく、poison でも入力が動き続ける形に戻します。致命扱いにはしません。

  • BG と無関係な変更(PushChar / PushRaw / Backspace / FlushPendingN / ForcePreedit / Commit / Reset* / Learn*): 冒頭の reclaim_unowned と bg_slot() の失敗では、処理を止めません。
  • 回収を省いた場合の扱い: 回収は「所有者なしの Done だけ・冪等」なので、警告ログを出して飛ばし、次の要求の入口でやり直します。try_lock が空振りしたときと同じ扱いです。
  • InputChar の応答: 文字を適用した後の読み取りが失敗したら、BgView に「読めなかった」を表す値を 1 つ足して返します(BgView::Unreadable)。Idle には倒しません。TSF は、その打鍵でライブ変換を始めないだけで、state.edit は更新します。これで、文字がホストにだけ入って復元元から落ちる経路が無くなります。
  • Restore: bg_slot() の失敗による早期 return をやめ、復元は続けます。then が BG と無関係なら、そのまま通ります。
  • BG を使う要求(BgStart / BgTakeCandidates / BgReclaim と BG の読み取り)は、今のまま Response::Error です。

BgView に値を足すのが合わなければ、InputChar の bg を Result 相当の型にする形でも構いません。指定してください。

poison を検知してホストを自己再起動させる処理は、#43 の範囲と考え、この PR には入れません。

2. 打鍵ごとの RPC — 1 回にまとめます

入口の所有権の確認と、診断ログ用の bg_status を、1 回の読み取りにまとめます。ふだんの打鍵の往復数は、(a) までと同じになります。

3. 変更の失敗の握りつぶし — 表示に続く経路には及ぼします

  • push_raw と force_preedit は Result を返す形にし、失敗したら、そのキーの処理を中断します(続く preedit_display() を古い値で成功させません)。
  • commit / reset_* / learn* / bg_reclaim は、後ろに読み取りが続かない終端の操作なので、警告ログのままにします。失敗した場合は、次の入力の recover_on_input で復元されます。

範囲を全部に広げるほうがよければ、そうします。

細かい点

  • 最初の変更が失敗したときの保存: 失敗したときは save しない形に直します。
  • input_log: Restore の対象外です。(b) の範囲外として、本文に既知の制限として書きます。
  • 残っているコメント 4 か所: 実装に合わせて直します。
  • WorkerBusy の待機表示: 意図したものです(10/1 に合意した、既存のモデル待ちの形に寄せる扱い)。そのままにします。
  • composition が無いときの bg_reclaim(): 送らずに戻り、warn を出さない形にします。

テスト

  • E のテストは、apply_change の NotYours / Taken の分岐を実際に通す形に直します。
  • 本文の「3 variant 共通」は、「テストで確かめたのは現在のビルドの variant だけ、残りはコードを読んでの判断」と書き分けます。
  • codec.rs に、末尾の Request::Read / Response::Bg / Response::OptionalString の順序を固定するテストを足します。

1 の形(BgView の値の追加)と 3 の範囲に異論が無ければ、この内容で実装します。

@fukuyori

fukuyori commented Oct 6, 2026

Copy link
Copy Markdown
Owner

方針を確認しました。f2fc9bb のコードと照合しました。1(poison でも入力を続ける)、2(打鍵ごとの RPC を 1 回にまとめる)、3 の範囲(push_raw / force_preedit のみ Result 化)、細かい点、テストは、方針どおりで了承します。BgView::Unreadable を足す形にも異論はありません。WorkerBusy の待機表示は、10/1 の合意どおりで構いません。

実装の前に、方針に足してほしい点が 4 つあります。1 つ目は、方針の目的(入力が動き続けること)に関わります。

1. 入口の所有権確認の読み取りも、poison で失敗させない

recover_on_input は、bg_status の読み取りが所有権の喪失(GenMismatch / OwnerMismatch)以外で失敗すると、Err を返して処理を終えます(rpc_composition.rs:246-253)。dispatch.rs:53 はその Err を ? で伝播するので、フォーカス中の通常の入力アクションは処理に進めません(CommitRaw は、事前回復を迂回するので対象外です)。

方針では、BG の読み取りを Response::Error のままにするので、poison が続くと、この入口で、通常の入力アクションが毎回止まります。(a) までのように、入力が動き続けるという目的に反します。

次の 2 点を、方針に足してください。

  • 2 で 1 回にまとめる読み取りは、poison のとき Error ではなく、所有権の結果と BgView::Unreadable を返すこと。
  • recover_on_input は現在、Ok(_) をすべて「所有権あり」と扱います(rpc_composition.rs:248)。Unreadable を、回復の対象にするのか、状態未確認として打鍵を続けるのかを決め、その分岐を実装してください。私は、所有権の結果が得られているなら、後者(打鍵を続ける)が方針に合うと考えます。

2. Restore の then の扱い

Restore の bg_slot() の失敗による早期 return をやめるだけでは、then の適用に失敗することがあります。Restore は、reset_preedit → force_preedit → bg_reclaim → bg_owner = None の後に、then を apply_change に渡します(server.rs:942 付近)。apply_change は、冒頭で reclaim_unowned と bg_slot() を呼ぶので(server.rs:1034)、poison が続くと、then の InputChar は、編集の前に失敗します。

そのとき、Restore はすでに編集状態と所有者を変えているのに、Response::Error が、その要求番号の応答として保存されます(server.rs:953-959)。冒頭の bg_slot() の失敗による早期 return(server.rs:939-941)は、復元の前なので、応答は保存されません。この 2 つは、区別して扱ってください。再送しても同じ Error が返り、クライアントは、新しい世代と then の編集状態を受け取れません。

方針の 1 では、BG と無関係な変更の冒頭の失敗を止めない形なので、この問題も解消するように読めます。次の 2 点を明記してください。

  • then が BG と無関係な変更(bg_start_n_cands: None の InputChar を含む)のとき、poison 下でも、Restored に then の応答が載ること。
  • then が BG を使う要求(BgStart / BgTakeCandidates / BgReclaim、bg_start_n_cands: Some の InputChar)のとき、Restore の応答と要求記録をどう扱うか(Restore だけ成功させて then を結果不明として返すのか、全体を拒否するのか)。

3. InputChar に bg_start_n_cands が付くとき

InputChar は、文字を適用した後に start_bg を呼びます(server.rs:1043-1062)。ここで失敗した場合に、Unreadable で返すのか、Error で返すのかを決めてください。最後の BG 状態の読み取りだけでなく、文字の適用後に失敗しうる start_bg も、対象に含めてください。変更要求の Error は、要求番号とともに記録されるので、同じ番号の再送でも編集状態は得られません(server.rs:895-899)。編集は適用済みなので、Error にすると、文字がホストにだけ入ったまま、TSF の state.edit が更新されない経路が残ります。Unreadable で返し、state.edit を更新する形を提案します。

4. BgView::Unreadable の追加の影響

  • postcard の互換: 末尾への追加で、既存の番号は変わりません。このリポジトリで確認できる範囲では、v6 は未配布と読めます(ローカルの 0.11.9 タグは v5、docs/September_Late_Plan.md に配布前の記述)。両側を同時に更新する前提で構いません。本文の注意書きにも書いてください。
  • TSF 側の分岐: 方針の「その打鍵でライブ変換を始めない」は、現在の呼び出しでは成立しません。on_input.rs:320-340 は、bg_status の値にかかわらず start_live_bg_if_ready を呼びます。Unreadable のとき開始を抑止する分岐が、別に要ります。
  • 網羅 match: on_convert.rs(BgView の判定)など、すべての利用箇所を更新してください。特に on_convert.rs:2236-2245 の _ 分岐は、新しい値を BG の再開始側に流します。Unreadable を、Idle / Failed / Done と同じに扱わない規則(状態不明として、何もせず、bg_start もフォールバックもしない)を、本文に書いてください。

poison の検知と自己再起動

「#43 の範囲」とのことですが、#43 は、本文とクローズ時のコメントを読んだ範囲では、GPU の推論失敗からの復帰が主題で、poison の記述は確認できませんでした。また、#43 の検知は bg_status() の状態の変化を数えますが、poison のとき conv_cache::status() は "idle" を返すので(conv_cache.rs)、そのままでは poison を数えません。この PR に入れない方針には賛成です。poison が続くと、変換が静かに使えなくなる点は、既知の制限として本文に書き、別の Issue にするかどうかは、私が判断します。

判断

上の 1〜4 を、実装の前に、このスレッドか PR 本文に示してください。示された内容を確認してから、実装の着手を判断します。今回も、コードを読んでの照合で、実機では確かめていません。

@nick20002005

Copy link
Copy Markdown
Contributor Author

確認をありがとうございます。4 点とも、f2fc9bb のコードで指摘のとおりであることを確かめました。方針に次を足します。

1. 入口の所有権確認の読み取り

  • 1 回にまとめる読み取りは、poison のとき Error にせず、所有権の照合の結果と BgView::Unreadable を返します。照合に落ちた場合は、これまでどおり Rejected(GenMismatch / OwnerMismatch) です。
  • recover_on_input は、Unreadable を「所有権あり・BG の状態は未確認」として扱い、回復には入らず、打鍵を続けます。所有権の結果は得られているので、復元をやり直す理由が無いためです。Ok(_) をまとめて受けている箇所は、値ごとの分岐に書き直します。
  • 所有権の喪失以外の Err(通信の失敗など)は、今のまま Err で返します。

2. Restore の then

  • then が BG と無関係な変更のとき(bg_start_n_cands: None の InputChar を含む): apply_change の冒頭の reclaim_unowned と bg_slot() の失敗では止めないので、poison 下でも Restored に then の応答が載ります。InputChar の bg は Unreadable です。
  • then が BG を使う要求のとき:
    • bg_start_n_cands: Some の InputChar は、下の 3 のとおり、文字を適用して bg: Unreadable で返します。Restored に then の応答が載ります。
    • BgStart / BgTakeCandidates / BgReclaim は、Restore だけ成功させます。全体は拒否しません。復元は BG と無関係で、拒否すると poison の間は復元できず、入力が止まるためです。この 3 つは編集状態を変えないので、適用しなくても復元元は崩れません。
    • 応答は Restored で、then には「適用しなかった」ことが分かる値を載せます(then が無かった場合の None とは区別します)。要求記録にはこの Restored を保存するので、再送でも新しい世代が返ります。TSF は、これを BG の読み取りの失敗と同じに扱い、BG の要求は再送しません。
  • 冒頭の bg_slot() の失敗による早期 return(復元の前・応答を保存しない)は無くなります。復元の後に Error を保存する経路は、poison では通らなくなります。
  • なお、TSF が then に載せるのは、現在の実装では編集の変更だけです(rpc_composition.rs の replayable)。上の BG だけの要求の扱いは、ホスト側の規則として決め、テストで確かめます。

then に載せる値の形(Option の中身を「適用した結果/適用しなかった」の 2 値にする形を考えています)に指定があれば、合わせます。

3. InputChar に bg_start_n_cands が付くとき

提案のとおりにします。文字の適用後は、start_bg の失敗も、最後の BG 状態の読み取りの失敗も、bg: Unreadable で返し、Error にはしません。応答には適用後の edit が載るので、TSF の state.edit は更新されます。文字がホストにだけ入る経路は残りません。

4. BgView::Unreadable の追加の影響

  • 互換: 末尾に足します。既存の番号は変わりません。v6 は未配布なので、両側を同時に更新する前提であることを、本文の注意書きに書きます。
  • ライブ変換の開始の抑止: on_input.rs は、input_char の応答が Unreadable のとき、start_live_bg_if_ready を呼ばない分岐を足します。表示(update_composition)は、そのまま行います。
  • 利用箇所の規則: BgView を見る箇所は、すべて _ を使わない網羅 match に直します。Unreadable は、Idle / Failed / Done と同じに扱いません。状態不明として、何もせず、bg_start もフォールバックもしません。on_convert.rs の catch_up_live_preview は、WorkerBusy と同じく、preview をそのまま返して学習を見送ります。この規則を本文に書きます。

poison が続く場合

この PR には入れません。「poison が続くと、入力は続くが、変換が静かに使えなくなる。ホストを再起動するまで戻らない」を、既知の制限として本文に書きます。#43 の範囲と書いたのは誤りでした。別の Issue にするかどうかは、お任せします。

テスト

前回のテストに、次を足します。

  • poison 下で、BG と無関係な Change が通ること。
  • poison 下で、1 回にまとめた読み取りが、所有権の結果と Unreadable を返すこと。
  • poison 下の Restore: then が BG と無関係なら Restored に応答が載ること、then が BG だけの要求なら Restored と「適用しなかった」が返り、同じ番号の再送でも同じ応答が返ること。
  • bg_start_n_cands: Some の InputChar が、poison 下で、適用後の edit と bg: Unreadable を返すこと。
  • TSF 側: Unreadable で recover_on_input が回復に入らないこと、ライブ変換を開始しないこと。

実機では確かめていません。この内容でよければ、実装します。

@fukuyori

fukuyori commented Oct 8, 2026

Copy link
Copy Markdown
Owner

10/7 の方針を確認しました。f2fc9bb のコードと照合しました。1〜4 と、poison が続く場合を既知の制限として本文に書く扱い、追加のテストは、方針どおりで了承します。

実装の前に、足してほしい点が 4 つあります。1 つ目は、方針の目的(poison でも入力を続ける)に関わり、10/5 のレビューと 10/6 の追加の要求のどちらでも、私が漏らしていた点です。

1. BG と無関係な読み取りも、poison で失敗する

ホストの dispatch_read は、要求の種類を問わず、照合の後に checked_read_slot を呼びます(server.rs:1218)。checked_read_slot は BG の状態を読み、poison なら Response::Error を返します(server.rs:1187)。そのため、BG と無関係な PreeditDisplay / PreeditIsEmpty / HiraganaText / RomajiLogStr なども、poison の間はすべて Error になります。

TSF は、これらの読み取りを ? で伝播する箇所があります(例: edit_ops.rs:50, 111, 176, 322, 364, 515, 526)。したがって、10/6・10/7 の方針(BG と無関係な変更は通す、InputChar は Unreadable で返す)を実装しても、poison の間は、Backspace などのキー処理がこの読み取りで止まります。方針の目的に届きません。

次を方針に足してください。

  • BG と無関係な読み取りは、checked_read_slot の状態の読み取りと、所有者のない Done の回収の失敗では、全体を失敗させない。回収を飛ばした場合は、変更系と同じく警告ログを出し、次の要求の入口でやり直す。世代・所有者の照合は、これまでどおり先に行う。
  • BG の読み取り(BgStatus / BgWaitMs / BgPeekTopCandidate)の扱いは、下の 2 の回答に合わせる。
  • テストに、poison 下で PreeditDisplay などの読み取りが通ること、照合に落ちた場合は従来どおり Rejected になることを足す。

2. Unreadable を返す範囲

10/6 の方針では「BG の読み取りは今のまま Response::Error」、10/7 では「1 回にまとめた読み取りは、poison のとき Unreadable を返す」となっています。入口の読み取り(recover_on_input、rpc_composition.rs:247)と、診断ログ用の読み取り(dispatch.rs:73)は、どちらも ReadRequest::BgStatus です。TSF の他の箇所も、同じ BgStatus と BgWaitMs を使っています。

BgStatus / BgWaitMs 全体が poison で Unreadable を返す形にすると、on_convert.rs・candidate_window.rs・dispatch.rs にある 35 か所の bg_status() / bg_wait_ms() の呼び出しのすべてに、Unreadable が届きます。その多くは match ではなく == / != の比較なので、方針の「_ を使わない網羅 match に直す」では対象から漏れます。10/7 の規則(状態不明として、何もせず、bg_start もフォールバックもしない)に反する分岐に入る箇所があります。

  • on_convert.rs:466: bg_status()? != Done が真になり、bg_start を呼ぶ。
  • on_convert.rs:2236-2245: _ の分岐に入り、bg_start を呼ぶ。
  • candidate_window.rs:1508: != Done で早期 return し、結果を取り出さないままポーリングを続ける。

一方、on_convert.rs:1037 のように全 variant を列挙した match は、新しい値の追加をコンパイルで検出できます。

次のどちらにするかを決めてください。私は A を推奨します。

  • A: Unreadable を返すのは、InputChar の応答と、入口の 1 回の読み取りだけにする。入口の読み取りは、BgStatus とは別の要求にするか、別の手段で区別する。それ以外の BgStatus / BgWaitMs は、10/6 の方針どおり poison で Response::Error のままにする(poison_is_error_not_idle_or_rejection もそのまま)。
  • B: BgStatus / BgWaitMs 全体で Unreadable を返す。この場合は、比較の箇所も含めた 35 か所の一覧と、それぞれの Unreadable の扱いを PR 本文に書いてください。

A の場合の、10/6 の 4 の扱い: 10/6 の 4 で、BgView のすべての利用箇所の更新を求めたのは、Unreadable がそれらの箇所に届く前提でした。A では、InputChar の応答と入口の読み取り以外には Unreadable が届かないので、Unreadable の扱いを決める対象は、この 2 つの経路に絞って構いません(on_convert.rs:2236-2245 の catch_up_live_preview は、poison では従来どおり Error を受けます)。型として網羅 match が必要な箇所では、10/7 の規則(状態不明として、何もせず、bg_start もフォールバックもしない)に従ってください。

どちらの場合も、ライブ変換の開始の抑止は、10/6 に示した on_input.rs:320-340 だけでは足りません。input_char の BG の値を捨ててから start_live_bg_if_ready を呼ぶ箇所が、ほかにもあります(on_input.rs:148 → 174、224 → 229、291 → 292)。start_live_bg_if_ready のすべての呼び出し元(on_input.rs の 7 か所、on_convert.rs:1782, 1884)について、Unreadable のときの扱いを決めてください。

3. Unreadable で WorkerBusy の時計を解除しない

DynEngine::bg_status() と bg_wait_ms() は、値が WorkerBusy 以外なら reset_worker_busy_clock() を呼びます(rpc_composition.rs:338, 355)。本文の「別の BG 状態を観測した場合に解除」に照らすと、Unreadable は状態を観測していないので、解除の対象から外してください。A を選んだ場合も、入口の読み取りが Unreadable を返すので、同じ扱いが要ります。テストに、Unreadable で時計が維持されることを足してください。

4. Restored.then の形

「適用した結果/適用しなかった」の 2 値にする形に異論はありません。次の点を合わせてください。

  • 既存の Response::Restored のフィールドの型が変わります。互換は、10/6 の 4 の BgView::Unreadable と同じ扱い(両側を同時に更新する前提)で構いません。10/5 に依頼した末尾の判別子の固定(Request::Read / Response::Bg / Response::OptionalString)に、BgView::Unreadable と、then の新しい型の判別子も加えてください。
  • TSF の restore_via / apply_via(rpc_composition.rs:151, 171, 185)は、今は then が Some なら編集状態を更新し、None なら missing Restore.then で失敗します。「適用しなかった」を受けたときは、世代の更新はそのまま行い、編集状態は更新せず、BG の要求は再送しない失敗として扱ってください(10/7 の方針どおり)。現在の TSF は then に BG の要求を載せませんが(replayable)、受けたときの分岐は書いておいてください。
  • protocol.rs:582-583 の doc は、10/5 に指摘した「(b) で応答へ追加する」に加えて、「then が無ければ None」の部分も、新しい形に合わせて直してください。

細かい点

  • recover_on_input の未解決の要求: recover_on_input は、所有権が失われていなくても、未解決の要求があれば Restore に進みます(rpc_composition.rs:257-264)。10/7 の「Unreadable なら回復に入らず打鍵を続ける」は、未解決の要求が無い場合の話と読みます。未解決の要求がある場合は従来どおり Restore に進む(poison 下でも、2 の Restore の変更で通る)ことを、方針とテストに明記してください。
  • BgReclaim の分類: 10/6 に了承した、「BG を使う要求」として poison で Error にする扱いについて、理由(冒頭の読み取りを除くと適用中に失敗する経路が無く、TSF 側は警告ログで続行する)をコメントに 1 行残してください。
  • 既知の制限の書き方: 「ホストを再起動するまで戻らない」は、コードで確かめた範囲に寄せてください。確かめられるのは、poison のとき conv_cache::status() が "idle" を返すので、現在の health の経路では poison を検知しない、という点までです。

判断

上の 1 への対応と、2 の A / B のどちらにするかを、このスレッドで示してください。3・4 と細かい点は、その内容で実装に進んでください。1 と 2 の回答を確認してから、実装の着手を了承します。今回も、コードを読んでの照合で、実機では確かめていません。

@fukuyori

fukuyori commented Oct 8, 2026

Copy link
Copy Markdown
Owner

訂正(10/8): 10/5 のレビューから、先ほどのコメントまで、私は conv_cache の poison が配布ビルドでも起きる前提で書いていました。この前提が誤っていました。poison に関する要求を取り下げ、範囲を縮めます。

根拠

  • Cargo.toml:64 の [profile.release] は panic = "abort" です。crate ごとの上書きはありません。
  • 配布物のビルドに使う標準のタスク cargo make build-engine / cargo make build-tsf は、どちらも -Profile release を渡します(Makefile.toml:32, 37)。スクリプトはその場合 --release を付けます(scripts/build-engine.ps1:136、scripts/build-tsf.ps1:38)。
  • panic = "abort" では panic で巻き戻しが起きないので、mutex は poison しません。engine DLL の中で panic すると、engine-host のプロセスが終了します。その後は、次の RPC や入力で TSF が再接続し、Restore に進む経路があります。
  • 現行の engine DLL の実装では、bg_slot() / bg_reclaim_blocking() が失敗する原因は poison です(conv_cache.rs の lock() の失敗を、ffi.rs の engine_bg_slot / engine_bg_reclaim_blocking が失敗の値に変換し、engine-abi が bail!("conversion cache poisoned") にする)。

したがって、標準のタスクの既定でビルドした場合、poison が起きうるのは、巻き戻しのあるビルド(cargo test、-Profile debug のビルド)だけです。テストのうち、実際に mutex を poison させているのは conv_cache のテストで、巻き戻しを使っています。ホスト側の poison_is_error_not_idle_or_rejection / bg_start_poison_is_error_and_does_not_start は、テスト用の reader で poison の失敗を模擬しています。

ただし、スクリプトは -Profile debug も受け付け、CARGO_PROFILE_RELEASE_PANIC などの環境変数による上書きも防いでいません。配布物が実際にどの設定でビルドされたかは、リポジトリからは確かめられません。私の手元の環境には、release の panic を上書きする設定はありませんでした。ここまでは、コードと設定を読んでの判断で、配布ビルドで panic を起こしての確認はしていません。

取り下げる要求

  • 今日のコメントの 1〜4 と、細かい点のうち「recover_on_input の未解決の要求」「BgReclaim の分類」「既知の制限の書き方」。
  • 10/6 の私のコメントの 1〜4(入口の読み取りの Unreadable、Restore の then、bg_start_n_cands 付きの InputChar、BgView::Unreadable の追加の影響)。
  • これに伴い、10/6・10/7 の方針のうち、poison でも入力を続けるための部分(BG と無関係な変更で冒頭の失敗を止めない、BgView::Unreadable、Restored.then の「適用しなかった」、poison 下のテスト)は、実装しなくて構いません。

取り下げの代わりにお願いすること

10/5 のコメントで示した 2 つの選択肢のうち、「直さない」側にします。その場合に理由を残すようお願いしていたので、上の取り下げの代わりに、次の小さな追加だけお願いします。

  • poison のときは、f2fc9bb のとおり Response::Error で構いません。
  • 理由(標準のタスクでビルドした配布物は panic = "abort" なので poison は起きず、巻き戻しのあるビルドだけで起きる。その場合は Error を返し、入力の継続は保証しない)を、apply_change / dispatch_restore / checked_read_slot のいずれか 1 か所のコメントと、PR 本文に書いてください。
  • 既存の poison のテストは、そのまま残してください。

引き続きお願いする点

10/6 に了承した次の点は、変わりません。

  • 2: 入口の所有権の確認と、診断ログ用の bg_status を、1 回の読み取りにまとめる。
  • 3: push_raw / force_preedit を Result を返す形にし、失敗したらそのキーの処理を中断する。commit / reset_* / learn* / bg_reclaim は、後ろに読み取りが続かない終端の操作なので、警告ログのままとする。
  • 細かい点: 最初の変更が失敗したときは save しない/input_log を既知の制限として本文に書く/残っているコメント 4 か所を直す/composition が無いときの bg_reclaim() は送らずに戻る。
  • テスト: E のテストで apply_change の NotYours / Taken の分岐を通す/「3 variant 共通」の書き分け/codec.rs で末尾の Request::Read / Response::Bg / Response::OptionalString の順序を固定する。

確認したい点

nick さんの手元で、次のいずれかに当てはまる場合は教えてください。その場合は、poison の扱いを改めて決めます。

  • 標準のタスク以外のコマンド(-Profile debug や、直接の cargo build など)でビルドしたものを、常用している。
  • CARGO_PROFILE_RELEASE_PANIC・RUSTFLAGS・Cargo 設定ファイルなどで、release の panic を上書きしている。

10/6・10/7 に検討してもらった内容の多くを、取り下げる形になります。上の確認したい点に該当しなければ、上の範囲で実装を進めてください。

@nick20002005

Copy link
Copy Markdown
Contributor Author

訂正と範囲の整理をありがとうございます。根拠の 2 点(Cargo.toml の [profile.release] が panic = "abort"、標準のタスクが -Profile release を渡す)は、main のコードで確かめました。

確認したい点への回答

どちらにも該当しません。

  • 常用しているものは、scripts\build-engine.ps1 -Profile release / scripts\build-tsf.ps1 -Profile release(または同じ release プロファイルの cargo build --release)でビルドしています。-Profile debug のものは常用していません。
  • CARGO_PROFILE_RELEASE_PANIC・RUSTFLAGS は設定していません。ユーザー側の Cargo 設定ファイルは無く、リポジトリの .cargo/config.toml は空です。フォーク側の Cargo.toml も panic = "abort" のままです。

配布ビルドで panic を起こしての確認は、こちらでもしていません。

実装する範囲

示していただいた範囲で進めます。

  • poison は f2fc9bb のとおり Response::Error のままにします。理由(標準のタスクでビルドした配布物は panic = "abort" なので poison は起きず、巻き戻しのあるビルドだけで起きる。その場合は Error を返し、入力の継続は保証しない)を、apply_change のコメントと PR 本文に書きます。既存の poison のテストは残します。
  • BgView::Unreadable、Restored.then の「適用しなかった」、poison 下のテストは入れません。
  • 10/6 に了承をいただいた点(入口の所有権の確認と診断ログ用の bg_status を 1 回の読み取りにまとめる/push_raw・force_preedit の Result 化/細かい点 4 つ/テスト 3 つ)は、そのまま実装します。

main が 2b340c0 まで進んでいるので、rebase してから push します。できたら、このスレッドでお知らせします。

nick20002005 and others added 3 commits October 9, 2026 12:37
Part (b) of fukuyori#56. The TSF now sends every composition change through
`Change`, starts each composition with `Restore { .., then }`, and keeps
the reading and pending romaji returned with each outcome as the restore
source. Composition reads go through `Read { expect, request }` and
return `Result`; failures are no longer turned into empty values.

BG conversion answers are typed (`BgView`, `BgStartOutcome`,
`BgTakeOutcome`) and are derived from `(bg_owner, BgSlot)`. `BgSlot` is
a new engine DLL read, so the engine ABI goes 10 -> 11 in both places.
The old change/read variants become `_Reserved…` and the host answers
`Error`. v6 is unreleased, so no further protocol version bump.

Refs fukuyori#56

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extract the engine operations start_bg needs into a BgStarter trait so the
fake used by the read-side tests can drive it. Behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Fold the per-key ownership check and the diagnostic bg_status into one
  read: recover_on_input returns the BgView it observed.
- push_raw / force_preedit return Result and callers stop handling the
  key on failure instead of reading a stale preedit.
- Do not save a brand-new composition whose first change failed without
  sending anything; skip bg_reclaim when no owned composition exists.
- Keep conv_cache poison as Response::Error and document why
  (release builds use panic = "abort", so the mutex cannot be poisoned).
- Route the BgTake replay test through take_bg, pin the appended
  Request/Response discriminants, and fix four stale comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nick20002005
nick20002005 force-pushed the feat/rpc-change-migration-56b branch from f2fc9bb to 6e5d52a Compare October 9, 2026 03:43
@nick20002005

Copy link
Copy Markdown
Contributor Author

10/8 の範囲で実装し、push しました(main 2b340c0 に rebase、先端は 6e5d52a)。PR 本文に「10/5 のレビューへの対応(10/9)」と「既知の制限」を足しています。

  • poison: f2fc9bb のとおり Response::Error のままです。理由は apply_change の doc コメントと本文に書きました。既存の poison のテストは残しています。
  • 入口の読み取り: recover_on_input が所有権の確認で得た BgStatus の値を返し、診断ログはその値を使います。ふだんの打鍵の往復数は、(a) までと同じです。
  • push_raw / force_preedit: Result を返し、呼び出し元(27 か所)は失敗したらそのキーの処理を中断します。
  • 細かい点: 何も送らずに失敗した最初の変更は保存しない/コメントの修正/所有権を持つ composition が無いときの bg_reclaim() は送らない/input_log は既知の制限として本文に記載。
  • テスト: E のテストは apply_change の BgTakeCandidates と同じ take_bg を通す形にし、NotYours / NotReady のテストを足しました。codec.rs で Request::Read / Response::Bg / Response::OptionalString の番号を固定しました。A の「3 variant 共通」は、本文で書き分けました。

fmt、TSF / RPC / engine-abi の Clippy、TSF 206 件・RPC 69 件のテストは、手元で通っています。engine と engine-abi のテストは、今回は再実行していません。実機では確かめていません。

@fukuyori

fukuyori commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

10/8 に残した依頼について、6e5d52a のコードで実装と本文への追記を確かめました。rebase 後の 2 コミット(7b04112 / eed54ad)は、git range-diff で 1dceaac / f2fc9bb と同じであることを確かめています。CI(Build & Test / Format Check)は 6e5d52a で成功しています(run)。

確認できたこと

  • poison: Response::Error のままで、理由が apply_change の doc コメントと本文にあります。既存の poison のテストは残っています。
  • 入口の読み取り: recover_on_input が所有権の確認で得た値を InputEntry で返し、診断ログはその値を使います。読み直すのは、フォーカスの無い文脈と復元した直後だけです。WorkerBusy の時計の解除の条件は、bg_status() と同じです。
  • push_raw / force_preedit: Result を返し、呼び出し元 27 か所(edit_ops.rs 12、on_convert.rs 9、on_input.rs 6)で ? を付けています。commit / reset_* / learn* / bg_reclaim は、unit() を通る警告ログのままです。
  • 細かい点: 何も送らずに失敗した最初の変更は保存しない(keep_after_change)/所有権を持つ composition が無いときの bg_reclaim() は送らない/4 ファイル(engine-abi/src/lib.rs、client.rs、protocol.rs、candidate_window.rs)の古いコメントの修正/input_log の既知の制限。
  • テスト: take_bg を切り出し、E のテストが apply_change と同じ関数を通るようになりました。NotYours / NotReady のテストも足されています。codec.rs で Request::Read(56)、Response::Bg(17)、Response::OptionalString(18)の番号が固定されています。
  • 本文: 「3 variant 共通」の検証範囲の書き分けと、既知の制限の追記。

マージします

この PR は、このままマージします。

照合の中で、次の点が見つかりました。1〜3 は、10/6 に合意した「push_raw / force_preedit が失敗したら、そのキーの処理を中断する」で中断する範囲が広がったことに関わる点で、合意の時点で私が考えていなかった経路です。4・5 は、テストと本文の説明の補足です。いずれもマージの後にこちらで対応し、追加の作業はお願いしません。記録として、要点を残します。

  1. 部分確定の途中での中断: 確定する部分を commit → reset_preedit してから、残りの読みを push_raw で送り、最後に commit_then_start_composition でアプリ側を確定する経路が、4 か所あります(on_convert.rs の RangeSelect と Selecting の部分確定、edit_ops.rs の候補選択の部分確定、on_input.rs の候補選択中の文字入力)。途中の push_raw で中断すると、アプリ側の確定処理まで到達せず、セッションは Idle になります。元の復元元は reset_preedit で消えているので、再構築した接頭辞と再送用の要求だけでは、確定する部分と残りの読みの全体を保てません。以前も、後続の読み取りが失敗すれば同じ問題がありました。今回は、push_raw が失敗しても後続の読み取りは成功し、以前ならアプリ側の確定まで進めた場合にも、その前で中断するようになっています。
  2. 中断したときにキーをアプリへ渡す: handle_action の Err は、OnKeyDown で FALSE(キーを食べない)になります。一方で、通信失敗の要求は再送用に残ります。アプリがそのキーを処理し、その後に復元で再送されると、二重入力になりうると推測されます(実機では確かめていません)。input_char などの経路では、この PR の修正前(eed54ad)からある形で、今回 push_raw / force_preedit の経路にも広がりました。
  3. 保留キーの再生: drain_pending_keys() は保留キーを全件取り出すので、再生の途中で push_raw が失敗すると、まだ再生していないキーが捨てられます(input_char の経路には、この PR の修正前からある形です)。
  4. E のテストの数え方: FakeBg::take_checked は取り出しに成功したときだけ takes を数えるので、takes のカウンタだけでは、空振りの余分な呼び出しを検出できません。現在の実装は、再送のときに適用用のクロージャ(その中の take_bg)を呼ばずに保持した応答を返すので、二重に取り出しているという指摘ではありません。
  5. 本文の言い回し: 「commit / reset_* は後ろに読み取りが続かない終端の操作」は、上の 1 の経路では成り立ちません。10/6 の方針と 10/8 の私の訂正でも同じ説明を使っていたので、こちらの記録で補足します。

実機での確認(ホストの再起動をまたいだ入力の復元など)は、RPC protocol v6 をリリースする前の統合試験で行います。今回も、コードを読んでの照合で、実機では確かめていません。

長い期間、対応をありがとうございました。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants