🧪 D-studyLabブログ
🔗 AI連携

AIに待たせるのをやめた——3体を無人で回す仕組みと、そこで踏んだ罠2つ

2026年8月16日

AIを長時間動かしていて、いちばん無駄なのは待っている時間です。

実行役のAIが長い作業をしている間、司令塔のAIは「終わった?」「まだ?」と見に行きます。 見に行くたびにトークンを使う。何もしていないのに減る。

これをやめる仕組みを作りました。 司令塔は一切ポーリングしません。終わった側から叩き起こされます。

作ったのは常駐スクリプト1本、280行です。 ただ、そこで踏んだ罠2つのほうが、たぶん役に立ちます。先に書いておきます。

  1. Windowsの既定コンソールは cp932。 を print した瞬間に落ちる
  2. wait --for tui-idle は、TUIでない相手だと指定時間まるごと待つ

どちらも動かしてみるまで分からない種類のもので、 どちらも常駐スクリプトでは致命的でした。

誰が何をするか(3体の分担)

担当なぜそこか
計画高価だが賢いAI手順を書くのは一度きり。ここは質で選ぶ
実行安価で長時間動くAI同じことを何十回もやる。ここは単価で選ぶ
司令塔判断するAI結果と実物を突き合わせ、次を決める。判断のときだけ動く

実際の並びはこうなります。

計画役が steps/STEP01.md を置く
→ 実行役が実行 → STEP01.result.mdSTEP01.done を置く
→ ★常駐スクリプトが検知 → 司令塔のペインへ通知を投げ込む
→ 司令塔が result と実物を突き合わせる
  定型どおり → 司令塔が自分で STEP02 を書く(計画役を呼ばない=安い)
  想定と違う → 計画役に要約を渡して直させる
  判断が要る → 人に聞く(ここだけ止まる

肝は★の行です。司令塔は見張りません。 STEP01.done という空ファイルが置かれた瞬間、常駐スクリプトが司令塔の端末に プロンプトを打ち込みます。司令塔は、呼ばれるまで完全に停止しています。

「長く寝ているローカルプロセスに仕事をさせ、AIは呼ばれた時だけ動く」形自体は、 前に作ったローカルハブと同じ考え方です。新しい発想ではありません。 新しいのは、それをAIどうしの受け渡しに使ったところです。

設計で決めたこと(崩さない3つ)

ファイルを窓口にする

AIどうしを直接つながない。間にファイルを置きます。 実行役は STEP01.done を置くだけ。司令塔はそれを見るだけ。 お互いの存在を知らなくていいので、片方を別のAIに差し替えても壊れません。

1ファイル=1人が書く

STEP01.done を置くのは実行役だけ。誰も改名・編集しません。 「処理済みかどうか」は、常駐スクリプトが自分の状態ファイルで持ちます。 人が目視で追う印が要るなら STEP01.reviewed という別のファイルを置きます。

2人が同じファイルを触った瞬間、どちらが最後に書いたかで結果が変わります。 分散システムの難しい話ではなく、単に事故ります

状態はファイルにしかない

常駐スクリプトはメモリに何も溜めません。 だから落ちてもいい。再起動するとフォルダを全部見直し、 死んでいた間に置かれたマーカーも拾い直します。

このスクリプトは「たまたま起きている便利屋」であって、いなくても状態は壊れない。

引き継ぎもここで効きます。別のAIに渡すとき、渡すのはファイルの場所だけでいい。 会話の履歴を持っていく必要がありません。

罠その1 ── を print したら落ちた

Windowsの既定コンソールは cp932。UTF-8ではありません。

print("⚠ 司令塔のペインが見つかりません")
# UnicodeEncodeError: 'cp932' codec can't encode character '⚠'

も cp932 にありません。その行に到達した瞬間にスクリプトが死にます。

普通のスクリプトなら「文字化けした、直そう」で済みます。 しかし常駐スクリプトでは意味が変わります。

落ちるのは正常系ではなく、エラーを報告しようとした行です。 つまり「何かおかしいことが起きた」→「それを知らせようとして死ぬ」→ 誰も気づかないまま監視が止まる。いちばん困る壊れ方をします。

対策は冒頭に3行。

for _s in (sys.stdout, sys.stderr):
    try:
        _s.reconfigure(encoding="utf-8", errors="replace")
    except Exception:
        pass

stderr も一緒に直します。トレースバックのほうが先に出る場面があるからです。 errors="replace" を付けておくのは、リダイレクト先によっては reconfigure が効かない場合の保険。ここで落ちるくらいなら化けたほうがいい。

罠その2 ── 「相手が手を止めるまで待つ」が120秒フルで待った

司令塔の端末に文字を打ち込むので、相手が作業中だと入力が混ざります。 だから送る前に「手が空くまで待つ」ようにしました。使ったのはこれです。

terminal wait --for tui-idle --timeout-ms 120000

TUI(画面を描き替えるタイプの端末)が落ち着くまで待つ、という指示です。

相手がTUIでないと、この条件は永遠に成立しません。 素のシェルに対して投げたところ、120秒まるごと待たされました(実測)。 タイムアウトするまで、ただ止まっています。

5秒間隔で見に行く常駐スクリプトが、1回の通知で2分固まる。 その間に次の .done が置かれても気づきません。

直し方は2つあります。

  1. 待ちを短くする(既定60秒。0で待たない)
  2. タイムアウトしても送信は続行する

2つめが本題です。「待てなかった」は「送るな」ではありません。

w = orca("terminal", "wait", ...)
if not (w and w.get("ok")):
    log("(相手が手を止めるのを待てなかったのでそのまま送ります)")
# ← ここで return せず、そのまま送信へ進む
d = orca("terminal", "send", ...)

待ちは best-effort。成立すれば嬉しいが、成立しなくても仕事は進める。 これを「失敗したら中止」で書いていたら、通知が1件も届かない仕組みになっていました。

もう1つ、地味に効いた決め事

送信に失敗しても「処理済み」にする。

一見おかしい。失敗したのだから、次の周期でもう一度試すべきに見えます。 だが5秒間隔で回るループでは、それは永久リトライになります。 相手が見つからない状態が10分続けば、120回叩きに行く。

なので、こうしました。

取りこぼしは、リトライではなくキューで拾う。 自動で消えるのは仕組みの信頼を壊しますが、 別の場所に残っていれば、あとから人が拾えます。

いま言えること・言えないこと

言えること。作って、使い捨ての端末を立てて、通しで検証しました。

言えないこと。

効果を書きたい気持ちはありますが、書けるのは数字を取ってからです。 この判断自体、以前に測っていない数字を公開した失敗から来ています。 あのとき決めた「公開する数字には標本数を書く」を、ここでも守ります。

この記事の要点

仕組みの話より、後半2つのほうが明日から使えると思います。

※ この記事の内容は、記事公開日に作成・検証したものです。実運用の記録ではありません。 効果(トークン消費の増減など)は測定していないため、数値は書いていません。

© D-studyLab / ブログ一覧運営者情報プライバシーポリシー