保存と復旧
複数のエージェント、CLI、Webサーバー、テキストエディターが、同時に同じファイルに触れることがあります。このページでは、Agent Boardがファイルの整合性を保つ仕組みと、障害時の動作を説明します。失敗を調べているときはトラブルシューティングも読んでください。
ロック
| ロック | 範囲 | 用途 |
|---|---|---|
.internal/write.lock | 1プロジェクト | CLIとWebのすべての書き込み。task.json・epic.json・project.json・TASK.md・EPIC.md。 |
shared/.internal/catalog.lock | 全プロジェクト | 名前の一意性を確かめたうえでの、プロジェクトの作成・名前変更・削除。 |
lifecycle.lock | データ領域 | サーバーが一つだけ動くよう、serveが起動から終了まで保持する(サーバーと画面を参照)。 |
- どれも標準のファイルロックです。書き込みはプロジェクトのロックを最大5秒待ち、超えると何も変えずに
database_busy(終了コード1)で失敗します。エージェントはそのまま再実行してかまいません。 - ロックの中で最新のファイルを読み直し、必要な所だけを変え、同じフォルダに書いた一時ファイルをrenameして各ファイルを置き換えます。
- タスクのロックは、最新のJSONとMarkdownを読んでから、組の確定と必要な復旧が終わるまで保持します。公開のタスクの読み込み・スナップショット・versionの計算も同じロックの中で二つのファイルを読むため、保存途中の不整合な状態は公開されません。状態の更新中の内部の読み込みは、ロックを取り直しません。
- サーバーの索引が関わるときは、索引→プロジェクトの順にロックを取ります。状態の更新は、プロジェクトのロックを解放してから索引の更新と通知に進みます。
- 直接編集はロックを取りません。
TASK.mdやEPIC.mdを保存するエディターはCLIと調整されず、後から保存した内容で上書きされます。ボードのファイルには更新番号による競合検出はありません。競合を検出するのはプロジェクトのドキュメントだけです。
タスクの保存
タスクの状態(task.json)と履歴(TASK.md)が食い違ってはいけないので、.internal/transactions/の永続ジャーナルを使って一組で確定します。
- プロジェクトのロックの中で最新の組を読み、更新後の内容を組み立てます。
- 更新前後の内容・相対パス・ハッシュ・
preparedをジャーナルに書いて同期します。 - 一時ファイルから両方のファイルを置き換え、親フォルダを同期します。
- ジャーナルの
committedを同期します。ここが確定点です。 - ジャーナルを消し、索引を更新し、通知します。応答と
changedの通知は確定の後にだけ行います。
復旧は確定点から決まります。
committedより前の失敗は、ロックを保持したまま更新前の状態に戻します。- 異常終了の後は、次の読み込みか書き込みが、進む前にプロジェクトのロックの中で復旧します。
preparedは旧い組に戻し、committedは更新後の状態を保ちます。 - 確定後のジャーナルの掃除に失敗しても、操作を未確定にはしません。
- 直接編集によってジャーナルが想定するハッシュと一致しなくなった場合、ジャーナルで上書きせず
file_errorを報告します。復旧できなければジャーナルを残し、その組は公開も変更もしません。 - ジャーナルは
version・cursorの計算に含めません。
状態変更・回答・下書きの切り替え・エピックの変更・アーカイブ・復元・自動Done・history addなど、タスクを更新する操作はすべてこの手順で保存します。どれも最新の本文・受入条件・未知のfrontmatterのキーを保ちます。history addはロックの中でAIの制限(Done・下書き・アーカイブ)を確かめ、不正な履歴は何も変えずに拒否します。
原子的なのはTASK.mdとtask.jsonの組だけです。画像・EPIC.md・epic.jsonは別に保存するので、画像を保存した後にタスクの保存が失敗した場合、応答で保存できた範囲を示し、画面は入力を保持します。Webでのタスクの保存は、frontmatterと本文からTASK.md全体を組み立てて検証し、一度だけ書きます。検証に失敗すれば何も書きません。未知のキーは保ち、キーの順序はYAMLの書き出しに従い、コメントは保ちません。
作成と削除
- タスクの作成では、ロックの中で
nextTaskIdを予約し、task.jsonとTASK.md(生成した作成の項目を含む)をtasks/.new-<random>/に書き、フォルダをtasks/<task-id>/にrenameします。エピックも同じです。失敗しても作りかけのタスクは見えず、予約した番号は使いません。 initは、プロジェクトのtasks/とepics/を一時的な名前に変えてから削除し、ID・名前・nextTaskIdを保ちます。initとproject rmは、アーカイブ済みのタスク・画像を含めて指定したプロジェクトのデータを消し、ほかには触れません。- 一括アーカイブはタスクごとに確定し、タスクをまたいだロールバックはありません。
簡易依頼からタスクを作成する
簡易依頼フォームは、依頼本文・任意のエピック・画像をまとめてPOST /api/projects/{projectId}/tasks/multipartへ送ります。FileStoreは一つのプロジェクトロックの中で、プロジェクト・エピック・本文・すべての画像を検証します。受け付けた送信は毎回新しいタスクになります。通信エラー後に保存結果が不明なまま再送すると、同じ内容のタスクがもう一件作成される場合があります。
タスクのメタデータと本文を非公開のステージング用フォルダに書き、続けて画像を保存します。同一ファイルシステム上でタスクフォルダを一度renameして、完成したタスクを公開します。このrenameまで、タスク一覧や待機セレクターに現れません。成功応答と変更通知は公開後に行います。途中で失敗した場合はステージング用フォルダを削除しますが、先に予約したタスク番号が未使用のまま残ることがあります。この経路が作るタスクはAI担当・Todoで、入力した本文は## 元の依頼に保存します。通常の手動作成と編集は従来どおり利用できます。
プロジェクトのドキュメント
Agent Boardは、shared/projects/<project-id>/docs/のファイルを書きません。書くのは、フォルダを作るときに置くREADME.mdだけです。ドキュメントのダイアログはファイルを読むだけで、編集は利用者のエディタで行います(エディターで開くで起動します。Web APIを参照)。そのため、ドキュメントには専用のロック、ジャーナル、revisionの確認、復旧の仕組みはありません。ファイルを書くエディタがその責任を持ちます。
ドキュメントがボードのデータに影響することもありません。監視はdocs/の変更をSSEのdocsイベントとして知らせるだけで、ボードの読み込み直し、プロジェクトのversionの変更、changedの送信はしません(変更の通知を参照)。
ファイルに不備があるとき
Agent Boardは、不正なファイルを自動修正しません。問題のある箇所を報告し、読み取れる内容は表示し続けます。不正なデータをさらに更新する書き込みは拒否します。
| 不備 | 影響 |
|---|---|
| 読めないファイル | 相対パス付きのfile_unreadable。何も書かない。 |
不正なtask.json | そのタスクをtasksから外してproblemsに載せる。そのタスクへの書き込みはfile_invalidで失敗する。 |
1以外のformatVersion | format_unsupportedとして載せて外す。ファイルを変換・削除・書き換えしない。 |
不正なproject.json | そのプロジェクトの読み込み全体がfile_invalidで失敗する。 |
TASK.mdのYAMLを解析できない | 閉じ線の後の文(またはファイル全体)を本文として表示し、acceptanceを空、acceptanceError: trueにする。どの主体の更新も拒否する。 |
acceptanceだけが不正 | 本文と履歴は使える。未達成の条件がないと確かめられないため、AIのDoneへの移動を拒否する。人の移動はできる。 |
historyだけが不正 | 本文と有効な受入条件は読める。履歴は空として返すが、その値で保存しない。厳密な読み込み・更新・回答・自動Doneは拒否する。 |
| 本文の書式の不備 | 本文をそのまま表示し、不備をproblemsに載せる。 |
プロジェクトに不備がある間、serveの索引は直前の正常な表示を保ち、不備をエラーとして返します。ファイルを手で直せば、次の読み込みで通常どおり表示されます。
初回案内の状態
案内は共有ディレクトリ全体で 1 つのファイル shared/.internal/onboarding.json を使います。ファイルには、状態(unseen、offered、active、closing、completed、interrupted、skipped)、リースを持つタブと期限、実行中の run とその段階・サンプルカードの ID、適用済みの直近のコマンド ID を保存します。
どの操作も、案内のロックを取り、次にプロジェクトのロックを取ります。状態を変えるコマンドは、新しいリビジョンとともにファイルをアトミックに保存します。要求は、読んだリビジョンとコマンド ID を名乗ります。同じコマンドを再送すると、手順を繰り返さず現在の状態を返します。古いリビジョンを名乗る要求は拒否されます。
タブは 30 秒のリースを持ち、10 秒ごとの heartbeat で延長します。heartbeat はリースだけを延ばし、リビジョンは変えません。サーバーが起動したときと、回収の要求が届いたときに、リースが切れた run は終了します。カードはアーカイブされ、案内は interrupted になります。ボードも、最初の読み取りで、リースが切れているか持ち主のいない active の案内を見つけたときに回収の要求を送ります。タブを閉じたり再読み込みしたりすると、この状態が残ります。まだ有効なリースは別のタブのものなので、そのままにします。
障害時の振る舞いの早見表
| 状況 | 何が起きるか |
|---|---|
serveが動いていない、serve.jsonが古い、通知が失われた | CLIは成功し、保存を保つ。変更はOSのイベントか、サーバーの起動時に取り込まれる。 |
serveが異常終了してserve.jsonが残った | CLIの通知が接続に失敗して無視される。次のserveがファイルを上書きする。 |
| 監視を登録できない | serveがエラーで終了する。 |
| 稼働中に監視がエラーを報告した | すべてのプロジェクトを読み直し、エラーをログに残す。 |
| OSがイベントを取りこぼして再走査を求めた | すべてのプロジェクトを読み直す。 |
| エディターが一時ファイルを使わずにその場で上書きした | 書きかけの本文が一時的にproblemsに現れ、書き終えたときのイベントで読み直される。 |
| プロジェクトのロックが5秒を超えて使われている | database_busy。何も変わらないので再実行する。 |
| タスクがRunningのままエージェントが異常終了した | タスクはRunningのまま。自動では未稼働に戻らない。 |
| 共有領域がネットワークドライブにある | 対象外。OSのイベントが届かないことがある。 |
ブラウザ側の失敗(応答の喪失、再試行、切断)はサーバーと画面で扱います。

