AIが途中で止まっても、続きを再開できるようにした

連載「ChatGPTと育てる、自分専用AI基盤」第7回。Runを中心にCheckpoint、Pause、Resume、保存された状態を循環させ、AIやChatが途中で止まっても仕事を再開できる仕組みを描いたシンプルな水彩画調サムネイル。 AI

※「ChatGPTと育てる、自分専用AI基盤」第7回です。

前回は、Knowledge MCP、Development MCP、Publishing MCPなどで「できること」を増やしても、次に何をするかを決めるのは毎回人間のままだったため、その上にLoop EngineというOrchestratorを作った話を書きました。

第6回:MCPをいくつ作っても、AIは自律化しなかった。そこでLoop Engineを作った

Loop Engineを作ると、Goalを受け取り、Planし、Capabilityを使い、Evidenceを見て、Evaluateするところまで一つの仕事として扱えるようになります。

これで「ループ」は作れました。

でも、実際の仕事は一回のChatや一回の処理で終わるとは限りません。

テストに時間がかかる。

GitHubのCIを待つ。

人間の承認を待つ。

外部サービスの結果を待つ。

途中でブラウザを閉じる。

サービスを再起動する。

翌日、別のChatから続きをやる。

こうなると、次の問題が出ました。

「途中で止まった仕事を、どこから再開すればいいのか?」

Chatの会話履歴だけを頼りにすると、毎回「どこまでやったっけ?」から始まります。

そこで、ChatではなくRunを仕事の正本にすることにしました。

Chatを仕事のidentityにしない

この考え方は、第1回のKnowledge MCPで「Chatを正式記録の本体にしない」と決めた時とかなり似ています。

Knowledgeでは、Chatの外にVaultを作りました。

Loop Engineでは、仕事の現在状態をChatの外に置きます。

Chat
  → その時の操作画面

Run
  → 仕事そのもののidentity

SQLite
  → Runの現在状態・Event・Checkpointの正本

Chatが閉じてもRunは残る。

別のChatに移っても同じRun IDを読める。

サービスが再起動しても、SQLiteに状態が残っていれば続きを判断できます。

この時点で、ChatGPTとの会話と「仕事の寿命」を分け始めました。

Runに何を持たせたのか

Runには、単なる「処理中」というフラグだけではなく、仕事を続けるために必要な情報を持たせます。

たとえば、

  • Goal
  • Success Criteria
  • Constraints
  • Budget
  • 現在のstatus
  • iteration / action / failureの使用量
  • どのCapabilityを使ったか
  • どんなEvidenceを得たか
  • 何を待っているか
  • 次にどの境界へ進むか

です。

状態機械としては、概念的に、

READY
  ↓
PLANNING
  ↓
WAITING_APPROVAL / WAITING_RESULT / EXECUTING
  ↓
OBSERVING
  ↓
EVALUATING
  ↓
RECORDING
  ↓
READY

を回し、

SUCCEEDED
PAUSED
FAILED
CANCELLED
BUDGET_EXHAUSTED

のような終了状態を持たせました。

重要なのは、「止まっている」と「終わった」を同じにしないことです。

人間の承認待ちなら、仕事は失敗していません。

外部結果待ちでも同じです。

いまどの境界で待っているのかが残っていれば、後から続きを判断できます。

EventとCheckpointを分けた

Runの現在状態だけでは、「なぜそうなったのか」が分かりません。

そこでEventを残します。

たとえば、

  • Runを作った
  • planを提出した
  • actionを承認した
  • Sandboxへ処理を渡した
  • external resultが返った
  • evaluationを行った
  • pauseした
  • resumeした
  • cancelした

という出来事です。

Eventは、後から追える履歴です。

一方、Checkpointは、「いまどこから再開すればよいか」を見るための境界です。

感覚としては、

Event
  → ここまで何が起きたか

Checkpoint
  → いま何を待っていて、次に何をすべきか

です。

この二つを分けると、新しいChatで全部の会話を読み直さなくても、現在地点へ戻りやすくなります。

pause / resume / cancelを明示的な操作にした

長い仕事には、「いったん止めたい」ことがあります。

ただし、止めるたびにRunを捨てて新しく作ると、履歴が分断されます。

そこでLoop Engineには、

  • pause
  • resume
  • cancel

を明示的に持たせました。

pauseは「まだ続ける可能性がある」。

cancelは「このRunはここで終わり」。

resumeは「保存された境界から続きを進める」。

つまり、止まること自体を異常扱いしない設計です。

後には、古いnonterminal Runを自動で消したり一括cancelしたりせず、read-onlyで状態を確認してから、必要なRunだけ明示的にcancelする仕組みまで追加しました。

Runを消すのではなく、履歴を残したままライフサイクルを閉じる形です。

本当にサービスを再起動して、同じRunを続けられるか試した

設計上「SQLiteに状態が残ります」だけでは不安なので、2026年8月9日に実機でrehearsalしました。

まず、HOSTDRIVENのRunを@@INLINE0@@状態で作ります。

Run IDは、

run_f0ecc6c5644b4118ad8f027d4b6137fd

でした。

その状態で、Loop Engine CoreとChatGPT向けHost Gatewayを実際に再起動しました。

普通のWebセッションなら、ここで話が切れても不思議ではありません。

再起動後、同じRun IDへcontinue_loopを実行します。

すると、restart前のplanning boundaryが復元されました。

そのままSandboxで、

17 × 19 = 323

を実行し、stdoutをEvidenceとして確認し、最終的にRunはsucceededになりました。

計算そのものはどうでもいい話です。

重要だったのは、

Runを作る
↓
サービスを再起動する
↓
同じRun IDを読む
↓
途中状態が復元される
↓
続きを実行する
↓
成功まで進む

が実環境で通ったことでした。

サービスの寿命より、仕事の寿命を長くできた。

これはかなり大きな変化でした。

記憶の保存先が止まっても、仕事そのものは失わない

もう一つ面白かったのがMemory Workerの停止試験です。

Loop Engineでは、RunのlessonをVaultへ直接書かず、まずSQLiteのMemory Outboxへ入れます。

そこで、意図的にMemory Workerを止めた状態でRunを最後まで進めました。

結果は、

  • Run自体:succeeded
  • Memory Outbox:pending=1
  • operations health:critical

でした。

つまり、Vaultへまだ届けられていないので運用上は問題があります。

でも、Runの結果とlessonそのものはSQLiteに残っています。

Memory Workerを再起動すると、pendingは0になり、Vaultへのdeliveryも完了しました。

これは、

仕事が成功すること
≠
長期記憶への配送が成功すること

を分けた例です。

どこか一つの外部サービスが一時停止しただけで、Run全体の成果を失わないようにしました。

Publishing MCPで「WordPressは成功したがInstagramだけ失敗」のような部分失敗を分けたのと似ています。

同じRunを二人で同時に進めないためにleaseを入れた

Runを長く生かせるようにすると、新しい危険も出ます。

手動のChatGPTとbackground workerが、同じRunを同時に処理したらどうなるか。

両方が「自分が次の担当だ」と判断し、同じactionを二重実行するかもしれません。

そこでRun Leaseを入れました。

ある処理主体がRunを実行している間は、そのRunにleaseを持たせる。

別の処理主体は、active leaseがあるRunを勝手に進めない。

Run
  ↓
active leaseあり
  → 他のexecutorは待つ

leaseなし
  → 条件を満たすexecutorが取得して進める

ここでも目的は、AIを止めることではありません。

長い仕事を安全に引き継げるようにすることです。

このleaseの考え方は、後に複数Chat・複数workspaceで並列開発する時にも別の形で重要になります。

ただし、workspaceやcomponent scopeのleaseは第9回の主題です。

そして、時間制限の設計を間違えた

durableなRunを作っても、実運用するとまた別の失敗が出ました。

Loop Engineには暴走防止のため、max_wall_secondsという時間budgetがありました。

最初の実装では、Runを作った時刻から現在までの経過時間をそのまま数えていました。

Run開始
↓
CIを待つ
↓
人間の承認を待つ
↓
Chatを閉じる
↓
翌日再開する

この全部がwall timeに入ります。

つまり、AIが何も実行していない時間まで「実行時間」として消費していました。

実際、Ordinary JapanのPublication Index Worker開発では、あるDevelopment Runが人間の承認待ちをしていただけなのにmax_wall_secondsで停止しました。

そのRunは、Development actionが0件、failureも0件でした。

何も失敗していないのに、ただ待っていただけでbudget exhaustedになったわけです。

これは明らかにおかしい。

そこで、

Runの年齢と、実際にAIやサーバーが働いている時間を分ける

方向へ設計を変更しました。

後の仕様では、

  • active execution time
  • Run age / stale TTL

を分け、HOST-drivenなRunがCI、承認、外部結果、Chat再接続を待っている時間を通常の実行budgetとして数えない方針にしました。

Chatが閉じることと、Runが止まることも分けた

この時間制限事故から、さらに一段はっきりしたことがあります。

Chatが消えたからといって、仕事が失敗したわけではない。

ChatはOrchestratorの一つです。

Runが、

  • WAITING_CI
  • WAITING_APPROVAL
  • WAITINGEXTERNALRESULT
  • READYTORESUME

のような安全な境界にいるなら、Chatが閉じてもRunはそのままでよい。

そこで後のContinuity設計では、Chatの不在を必要ならCHAT_ORCHESTRATOR_DETACHEDとして観測しつつ、健康なRunをfailedやcancelledへ変えない方針にしました。

別のChatは会話履歴から「たぶんここだろう」と推測するのではなく、Runのcontrol planeを読み、exactな現在状態から続きを始めます。

これは、長期開発をかなり楽にしました。

「再開できる」は、同じ処理をもう一回やることではない

resumeという言葉から、単に失敗した処理を再実行するイメージを持つかもしれません。

でも、私が欲しかったのはそれではありません。

途中で、

  • PRがすでにmergeされていた
  • CIが終わっていた
  • requirementが変わっていた
  • candidateがrollbackされていた
  • 外部サービスの状態が変わっていた

可能性があります。

なので再開時には、保存されたCheckpointだけでなく、必要に応じてlive stateを読み直します。

過去の状態を復元することと、古い前提をそのまま信じることは違う。

この考え方は第4回のexpected HEADやstale-state rejectionと同じです。

Runは継続性を与えますが、現実が変わっていたらfail closedで止まる。

継続性と安全性を両立させるには、この二つが必要でした。

振り返ると、作ったのは「長時間動くAI」ではなく「長時間続く仕事」だった

当初は、「ChatGPTが途中で止まらず長く動いてほしい」と考えがちでした。

でも実際に作ったものは少し違います。

AIそのものを何時間も一つのセッションで動かし続ける必要はありません。

必要だったのは、

  • 仕事にRun IDがある
  • 現在状態が外に残る
  • Eventで履歴が分かる
  • Checkpointで再開地点が分かる
  • pause / resumeできる
  • 二重処理をleaseで防ぐ
  • サービスが再起動しても続けられる
  • 外部待ちを実行時間として誤カウントしない
  • 新しいChatがlive stateから再接続できる

ことでした。

つまり、

「AIを止めない」のではなく、「AIが止まっても仕事を失わない」

ようにしたわけです。

この違いは、その後かなり効いてきます。

長い開発を途中で別Chatへ引き継ぐ。

GitHub CIを待つ間は人間もChatGPTも離れる。

複数のWorkerへ仕事を渡す。

VPS上のjobだけを継続する。

こうした運用は、すべて「ChatよりRunの寿命が長い」という前提の上に乗っています。

それでも、この時点では「自分で次を選ぶ」ところまでは別だった

Runがdurableになったことで、仕事を途中から続けられるようになりました。

でも、これはまだ「自律するAI」と同じではありません。

Runの状態を残すことと、次に何をすべきかをAI自身が判断することは別問題です。

HOST_DRIVENなら、ChatGPTが次のplanやevaluationを返して進めます。

つまり、

途中で止まっても再開できる仕事

は作れた。

では次に、

人間が毎回次の一手を与えなくても、Run自身が次の行動を選べるか。

ここが次の段階になります。

次回:作業自動化と「自律するAI」は何が違うのか

第8回では、durableなRunの上で、Goal、Success Criteria、Planner、Evaluator、Capability、Evidence、Memoryをどうつなぎ、単なる自動処理から「次の行動を選ぶLoop」へ進めたのかを扱います。

連載ナビ

関連記事

コメント

タイトルとURLをコピーしました