こんにちは。クラウド活用推進担当の榊原です。今回もよろしくお願いします。
Kiro CLI、便利ですよね。
AWS関連なら大抵のことは任せられますし、AWSに限らずとりあえずなんでも開いて相談できますし、会話内容をまとめて作業メモ作ってもらえたりしますし。本当に頼れるAIアシスタントだと思います。
……なんですが、うっかり --resume を付け忘れて新しいセッションを始めてしまったり、
アップデートや何かの拍子に「前の会話どこいった?」となったことはありませんか?私はあります。
そこで今回は、Kiro CLI の会話を自動で永続化してみました。
まずはKiro CLIのチャット機能について確認します。
➜ ~ kiro-cli --version
kiro-cli 1.22.0
Kiro CLIでは、チャット内容はディレクトリごとに保存されます。
そのため、ディレクトリを切り替えれば別のセッションとして保存されます。
--resumeで再開した場合も、そのディレクトリの最後の会話から再開することができるようになっています。
しかし1つのディレクトリつき1つのセッションしか保持できない制約があります。
--resumeの再開機能は便利ですが、再開できるのはあくまで直前のセッションのみ。
それ以前のセッションは復元することができません。
実はKiro CLIには、スラッシュコマンドとして、チャットの永続化機能が搭載されています。
それが、/saveコマンドです。
このコマンドは、/save <file_name>の形式で実行することで、任意のパスにJSON形式で、実行中のセッションのチャット内容を保存することができます。
しかしこのコマンドは、当然ですがコマンド実行時点での内容しか保存することができず、新しいチャットがあれば、都度実行しなくてはなりません。
また単純にコマンドを叩くのがいささか面倒です。
ファイルパスの指定が必要になりますが、毎回入力しなければなりません。Kiro CLIのセッション内で実行するため、Tab補完等シェルからのサポートも限定的です。
きちんと保存しておくには、マメに保存を実行する必要があり、保存用ディレクトリやファイルも管理する必要があります。
私のようなめんどくさがりさんにはなかなか厳しいところがあるというのが正直な感想です。
それでも過去の会話を再開したい。
だったらやるしかありません。自動化しましょう。
自動永続化をするにあたって、まずはKiro CLIがどのようにチャット内容を保存しているかを調査しました。
調査の結果、以下のことが分かりました。
チャットの内容はローカルのSQLiteに以下の形式で保存されていました。
ディレクトリパスを主キーとしてチャットデータが保存されているため、同ディレクトリでの重複は不可と分かります。
CREATE TABLE conversations (
key TEXT PRIMARY KEY, -- ディレクトリパス
value TEXT -- JSON形式のチャットデータ
);
チャットデータのJSONは以下のような構造を持っています。
重要な部分を抜き出すと以下の通りです。
conversation_id: セッションを識別するためのIDlatest_summary: /compactが実行された際にサマリを保持history: チャット内容を保存する構造request_id: ユーザの入力+AIの出力 を1リクエストとして保存するID{
"conversation_id": "UUID",
"checkpoint_manager": {...},
"context_manager": {...},
"context_message_length": number,
"file_line_tracker": {...},
"latest_summary": {...},
"mcp_enabled": boolean,
"model_info": {...},
"next_message": {...},
"tools": {...},
"transcript": {...},
"user_turn_metadata": {...},
"valid_history_range": {...},
"history": [
{
"user": {
"content": {
"Prompt": {
"prompt": "ユーザーメッセージ"
}
},
"additional_context": "...",
"env_context": {...},
"images": null,
"timestamp": "..."
},
"assistant": {
"Response": {
"message_id": "UUID",
"content": "アシスタント応答"
}
},
"request_metadata": {
"request_id": "UUID",
"message_id": "UUID",
"request_start_timestamp_ms": number,
"stream_end_timestamp_ms": number,
"time_to_first_chunk": {...},
"time_between_chunks": [...],
"user_prompt_length": number,
"response_size": number,
"chat_conversation_type": "...",
"tool_use_ids_and_names": [...],
"model_id": "...",
"message_meta_tags": [...]
}
}
]
}
Kiro CLIの場合、セッションの途中でコンテキストを使い切ると、それまでのチャット内容をコンパクト化してサマリとして持つことで処理を続行します。
このコンパクト化が実行された場合、SQLite内のhistoryデータがサマリとして圧縮されlatest_summaryに保存されます。その後historyは空になり、新たにデータが追加されていきます。
このため、コンテキストがオーバーフローすると、それ以前の詳細なチャットデータは破棄されてしまいます。
SQLiteへのチャット内容の保存は、1リクエストごとに行われることが分かりました。
会話終了時にまとめて保存などではなく、Kiroからの返答があるたびにDBが更新されているようです。
・/saveのJSONはSQLiteの構造を維持している
チャット内容永続化のために使用される/saveですが、出力されるJSONファイルを確認すると、SQLiteで保存しているJSONをそのまま出力していることが分かりました。
調査の結果から、以下のような方針をとることができそうです。
・SQLiteからJSONデータを抽出して外部ファイルとして保存
・conversation_idをキーにチャットごとに保存
・ファイル上書きではなく、増分バックアップ方式
・Kiro CLIの仕様に合わせて1リクエストごとに保存
これをスクリプト化すれば、Kiro CLIが読み込み可能な形式で、チャット内容を漏れなく永続化できます。
残された問題はどうやって実行するかのみとなりました。
会話の節目で自動的にスクリプトを実行できれば、保存し忘れがなくなります。
そこで登場するのがAgent Hookです。
Agentは、Kiro CLIでの会話における設定プロファイルで、AIの振る舞いや利用可能な機能を定義します。
会話のスタイルや役割設定、使用するMCPツールの設定や自動実行設定などをカスタマイズすることで、
特定の作業内容に特化したエージェントを作成することができます。
Hookは特定イベントをトリガーにコマンドを実行する仕組みです。
エージェントが有効化したタイミングやツールを実行したタイミングなど、特定のタイミングでカスタムコマンドを実行できます。
(参考: Agent Hooks)
今回の目的で候補になるイベントは2つありました。
・userPromptSubmit: ユーザーがメッセージを送信したタイミング
こちらの方式はユーザがメッセージを送信したタイミングで実行されます。
少しラグがあるとはいえ、チャットの最中は問題なく保存できる方式です。
しかし/quitなどでチャットを終了した場合Hooksのイベントが発火しないため、最後のチャットが保存されないというデメリットがあります。
・stop: アシスタントの応答が完了したタイミング
こちらはAIの出力が終了したタイミングで実行される方式になります。
SQLiteへの保存とタイミング合わせるならこのイベントを使えばいいのですが、使ってみると1つ問題がありました。
Kiro CLIの出力が上書きされてしまう点です。
[gentest] > 1-5まで1行ずつ改行しながら出力してください
> 1
2
3
4
✓ 1 of 1 hooks finished in 0.16 s
▸ Time: 4s
上記の例では、1から5までの数字を1行ずつ出力するように求めています。しかしAIの出力では、最終行に出力されるはずの5が、Hooksの出力(✓ 1 of 1 hooks finished in 0.16 s)に上書きされています。
stopイベントが起動すると、その出力によってKiroの出力の最終行が上書きされてしまうようです。
SQLiteを確認すると1から5まで問題なく生成と保存が行われていました。
なので問題は表示だけですが、ここが不完全なのは利便性を大きく損ないます。結論やまとめなど、重要情報が出力されがちな最終行が常に欠けた状態では、結局何が言いたいのか分からなくなってしまいますからね。
そのためuserPromptSubmitが候補に挙がりました。
しかし今回は表示の完全性よりもバックアップの完全性に重きを置きたいところ。
というわけでstopを採用することにしました。
通常の利用であればuserPromptSubmitの方が使い勝手が良いと思います。
なおこのstopに関する問題はすでにissueが上がっており、今後改善される可能性があります。
(参考: The "stop hook" deletes the latest line of the CLI response #4183)
というわけで実装していきます。
実装内容をおさらいすると、
・チャット内容をJSON形式で増分バックアップするスクリプトを
・Agent Hooksのstopイベントで実行する
これだけです。
今回のスクリプトでは、以下のコマンドを使用します。
・sqlite3:KiroのSQLiteから会話JSONを取得
・jq:JSONの解析・マージ
・flock:多重起動防止
処理の流れは以下の通りです。
・スクリプト実行時のカレントディレクトリ(pwd)を取得
・その文字列をSQLiteのconversations.keyとして使い、conversations.value(会話JSON)を取り出す
2. ファイル命名
・取得したJSONからconversation_idを抽出
・保存先ファイルは次の形式: ~/.kiro/backups/session_<conversation_id>.json
3. 既存ファイルがあるか確認
・session_<conversation_id>.json が存在するかで分岐
・存在しない → 初回作成(取得したJSONをそのまま保存)
・存在する → 増分更新(後述)
4. 増分バックアップ
・既存ファイルにrequest_idが存在しないデータだけを追加
・追加した件数(added)を数えてログに出力
・追加が0件なら更新しない
5. ログ保存
・実行結果を~/.kiro/backups/backup.logに追記
自動バックアップスクリプト(クリックして展開)
#!/usr/bin/env bash
# Kiro auto backup script (incremental)
# - Reads current conversation JSON from Kiro SQLite
# - Appends only new history entries based on request_id
# - Safe for large JSON (no big JSON in shell variables)
# - Atomic write (mktemp -> mv within same directory)
# - Best-effort: never breaks Kiro hook UX (exit 0 with logs)
set -euo pipefail
umask 077
BACKUP_DIR="$HOME/.kiro/backups"
DB_PATH="$HOME/.local/share/kiro-cli/data.sqlite3"
CURRENT_DIR="$(pwd)"
LOG_FILE="$BACKUP_DIR/backup.log"
mkdir -p "$BACKUP_DIR"
log() {
printf '%s: %s\n' "$(date -Is)" "$*" >>"$LOG_FILE"
}
require_cmd() {
local missing=0
local c
for c in "$@"; do
if ! command -v "$c" >/dev/null 2>&1; then
log "SKIP: missing command: $c"
missing=1
fi
done
return "$missing"
}
TEMP_FILES=()
tmpfile() {
local f
f="$(mktemp -p "$BACKUP_DIR" ".kiro_backup.$1.XXXXXX")"
TEMP_FILES+=("$f")
printf '%s' "$f"
}
cleanup() {
local f
for f in "${TEMP_FILES[@]}"; do
[[ -f "$f" ]] && rm -f "$f" || true
done
}
trap cleanup EXIT
# Dependencies (best-effort)
if ! require_cmd sqlite3 jq flock mktemp stat; then
exit 0
fi
# Concurrency guard
LOCK_FILE="$BACKUP_DIR/backup.lock"
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
log "Another backup process is running, skipping..."
exit 0
fi
# DB exists?
if [[ ! -f "$DB_PATH" ]]; then
log "DB not found: $DB_PATH"
exit 0
fi
# SQLite key is cwd string; escape single quotes for SQL literal
SQL_KEY=${CURRENT_DIR//\'/\'\'}
# Dump current conversation JSON to temp file (no big JSON in vars)
TEMP_CURRENT="$(tmpfile current)"
sqlite3 "$DB_PATH" \
"SELECT value FROM conversations WHERE key = '$SQL_KEY' LIMIT 1;" \
>"$TEMP_CURRENT" 2>/dev/null || true
# Empty?
if [[ ! -s "$TEMP_CURRENT" ]]; then
log "No conversation data found for key=$CURRENT_DIR"
exit 0
fi
# conversation_id
CONV_ID="$(jq -r '.conversation_id // empty' "$TEMP_CURRENT" 2>/dev/null || true)"
if [[ -z "$CONV_ID" ]]; then
log "Failed to extract conversation_id for key=$CURRENT_DIR"
exit 0
fi
BACKUP_FILE="$BACKUP_DIR/session_${CONV_ID}.json"
if [[ -f "$BACKUP_FILE" ]]; then
TEMP_RESULT="$(tmpfile result)"
# Merge + count in one jq pass; keep original top-level fields
if ! jq --slurpfile existing "$BACKUP_FILE" '
def normhist(h):
(h // [])
| map(if type=="array" then .[] else . end)
| flatten
| map(select(type=="object"));
def rid(e): e.request_metadata?.request_id? // empty;
. as $root
| (normhist(.history)) as $current
| (normhist($existing[0].history)) as $backed
# Build seen map from backed request_ids
| ($backed
| map(rid(.) | select(. != ""))
| reduce .[] as $r ({}; .[$r] = true)
) as $seen0
# Reduce current history, appending only unseen ids
| reduce $current[] as $e (
{history: $backed, added: 0, seen: $seen0};
(rid($e)) as $id
| if ($id | length) > 0 and (.seen[$id] // false | not) then
.history += [$e]
| .added += 1
| .seen[$id] = true
else
.
end
)
| {added: .added, noop: (.added == 0), data: ($root + {history: .history})}
' "$TEMP_CURRENT" >"$TEMP_RESULT" 2>/dev/null; then
log "ERROR: jq merge failed for session=$CONV_ID key=$CURRENT_DIR"
exit 0
fi
# Check if nothing was added
if jq -e '.noop == true' "$TEMP_RESULT" >/dev/null 2>&1; then
log "No new entries for session=$CONV_ID key=$CURRENT_DIR"
else
NEW_COUNT="$(jq -r '.added // 0' "$TEMP_RESULT" 2>/dev/null || echo 0)"
TEMP_OUT="$(tmpfile out)"
jq '.data' "$TEMP_RESULT" >"$TEMP_OUT"
mv -f "$TEMP_OUT" "$BACKUP_FILE"
log "Updated backup with $NEW_COUNT new entries for session=$CONV_ID key=$CURRENT_DIR"
fi
else
# First time: atomic copy current JSON to backup file
TEMP_OUT="$(tmpfile out)"
cp -f "$TEMP_CURRENT" "$TEMP_OUT"
mv -f "$TEMP_OUT" "$BACKUP_FILE"
ENTRY_COUNT="$(jq '.history | length' "$BACKUP_FILE" 2>/dev/null || echo 0)"
log "Created new backup file entries=$ENTRY_COUNT session=$CONV_ID key=$CURRENT_DIR"
fi
# Size check (100MB)
if [[ -f "$BACKUP_FILE" ]]; then
SIZE="$(stat -c%s "$BACKUP_FILE" 2>/dev/null || stat -f%z "$BACKUP_FILE" 2>/dev/null || echo 0)"
if [[ "$SIZE" =~ ^[0-9]+$ ]] && [[ "$SIZE" -gt 104857600 ]]; then
log "WARNING: Backup file exceeds 100MB size=${SIZE} path=$BACKUP_FILE"
fi
fi
exit 0
作成したスクリプトをAgent Hooksのstopイベントで起動するように設定します。
"hooks": {
"stop": [
{
"command": "/<dir>/kiro_auto_save.sh",
"timeout_ms": 10000
}
]
}
まずは初めての起動を確認してみます。
新しいディレクトリでKiro CLIを起動し、Hooksが設定されたAgentを切り替えて話しかけてみます。
> /agent swap
✔ Choose one of the following agents · gentest
[gentest] > はじめまして。あなたは何ができますか?
> はじめまして!私はKiroです。AWSが開発したAIアシスタントで、以下のことができます:
AWS関連
- AWS CLIコマンドの実行とリソース管理
- インフラコードの作成・最適化
- AWSサービスの設定とトラブルシューティング
- セキュリティのベストプラクティス提案
開発・システム管理
- ファイルの読み書き・ディレクトリ操作
- bashコマンドの実行
- コードの作成・修正・デバッグ
- テストケースの生成と自動化
その他
- 設定ファイルの分析
- パフォーマンス最適化の提案
- エラーの診断と解決策の提示
✓ 1 of 1 hooks finished in 0.08 s
▸ Time: 7s
無事Hooksが起動しました。
ファイルが生成されているか確認します。
➜ ~ tail -1 ~/.kiro/backups/backup.log
yyyy-mm-ddThh:mm:ss+09:00: Created new backup file entries=1 session=b4da9058-648f-454e-a748-3ef3c1615868 key=/<dir>/backup_test
セッションはb4da9058-648f-454e-a748-3ef3c1615868のようです。JSONファイルも確認してみましょう。
➜ ~ jq -r '.history[0].assistant.Response' ~/.kiro/backups/session_b4da9058-648f-454e-a748-3ef3c1615868.json | head -c 1000
{
"message_id": "512660f1-9d37-4492-b2d2-6bf8c9a76a19",
"content": "はじめまして!私はKiroです。AWSが開発したAIアシスタントで、以下のことができます:\n\n**AWS関連**\n- AWS CLIコマンドの実行とリソース管理\n- インフラコードの作成・最適化\n- AWSサービスの設定とトラブルシューティング\n- セキュリティのベストプラクティス提案\n\n**開発・システム管理**\n- ファイルの読み書き・ディレクトリ操作\n- bashコマンドの実行\n- コードの作成・修正・デバッグ\n- テストケースの生成と自動化\n\n**その他**\n- 設定ファイルの分析\n- パフォーマンス最適化の提案\n- エラーの診断と解決策の提示\n\n現在あなたは `/<dir>/backup_test` ディレクトリにいます。何かお手伝いできることがあれば、お気軽にお聞かせください!"
}
ちゃんと作成されていました。成功です。
自動追記も試してみます。
[gentest] > 好きなAWSのサービスはありますか?
> 私はAIアシスタントなので「好き」という感情はありませんが、よく活用されて実用的だと思うAWSサービスをいくつか挙げるとすれば:
Lambda - サーバーレスで運用が楽、コスト効率も良い
S3 - シンプルで信頼性が高く、様々な用途に使える
CloudFormation - インフラをコードで管理できて再現性が高い
VPC - ネットワークを柔軟に設計できる基盤サービス
これらは設計がシンプルで、他のサービスとの連携もしやすく、多くのユースケースで重宝されています。
✓ 1 of 1 hooks finished in 0.08 s
▸ Time: 8s
結果を確認してみます。
➜ ~ tail -2 ~/.kiro/backups/backup.log
yyyy-mm-ddThh:mm:ss+09:00: Created new backup file entries=1 session=b4da9058-648f-454e-a748-3ef3c1615868 key=/<dir>/backup_test
yyyy-mm-ddThh:mm:ss+09:00: Updated backup with 1 new entries for session=b4da9058-648f-454e-a748-3ef3c1615868 key=/<dir>/backup_test
➜ ~ jq -r '.history[0].assistant.Response' ~/.kiro/backups/session_b4da9058-648f-454e-a748-3ef3c1615868.json | head -c 1000
{
"message_id": "512660f1-9d37-4492-b2d2-6bf8c9a76a19",
"content": "はじめまして!私はKiroです。AWSが開発したAIアシスタントで、以下のことができます:\n\n**AWS関連**\n- AWS CLIコマンドの実行とリソース管理\n- インフラコードの作成・最適化\n- AWSサービスの設定とトラブルシューティング\n- セキュリティのベストプラクティス提案\n\n**開発・システム管理**\n- ファイルの読み書き・ディレクトリ操作\n- bashコマンドの実行\n- コードの作成・修正・デバッグ\n- テストケースの生成と自動化\n\n**その他**\n- 設定ファイルの分析\n- パフォーマンス最適化の提案\n- エラーの診断と解決策の提示\n\n現在あなたは `/<dir>/backup_test` ディレクトリにいます。何かお手伝いできることがあれば、お気軽にお聞かせください!"
}
➜ ~ jq -r '.history[1].assistant.Response' ~/.kiro/backups/session_b4da9058-648f-454e-a748-3ef3c1615868.json | head -c 1000
{
"message_id": "a065d0e0-4150-4287-a80f-4a13330756ff",
"content": "私はAIアシスタントなので「好き」という感情はありませんが、よく活用されて実用的だと思うAWSサービスをいくつか挙げるとすれば:\n\n**Lambda** - サーバーレスで運用が楽、コスト効率も良い\n**S3** - シンプルで信頼性が高く、様々な用途に使える\n**CloudFormation** - インフラをコードで管理できて再現性が高い\n**VPC** - ネットワークを柔軟に設計できる基盤サービス\n\nこれらは設計がシンプルで、他のサービスとの連携もしやすく、多くのユースケースで重宝されています。\n\nあなたが使っているAWSサービスや、興味のあるサービスはありますか?具体的な用途があれば、最適な構成を一緒に考えることもできます。"
}
一度チャットを終了し、改めてKiro CLIを起動してから動作を確認してみます。
[gentest] > はじめまして
> はじめまして!私はKiroです。AWSが開発したAIアシスタントで、テストケースの生成と管理を専門としています。
単体テスト、統合テスト、テスト自動化の支援ができます。テストのベストプラクティス、エッジケース、エラーハンドリング、テストカバレッジの向上についてもお手伝いします。
✓ 1 of 1 hooks finished in 0.09 s
▸ Time: 5s
結果を確認してみます。
➜ ~ tail -1 ~/.kiro/backups/backup.log
yyyy-mm-ddThh:mm:ss+09:00: Created new backup file entries=1 session=c1edbe69-e473-41a1-a216-2d1cb389046f key=/<dir>/backup_test
➜ ~ ls -lt ~/.kiro/backups/session_*.json | head -2
-rw------- 1 user user 64799 mmm dd hh:mm /<dir>/.kiro/backups/session_c1edbe69-e473-41a1-a216-2d1cb389046f.json
-rw------- 1 user user 112102 mmm dd hh:mm /<dir>/.kiro/backups/session_b4da9058-648f-454e-a748-3ef3c1615868.json
新しいセッションに切り替わり、JSONファイルが新たに作成されました。
/saveのJSONと同じ形式で保存しているので/loadで読み込みが可能なはずです。
1回目の会話を読み込んでみます。
[gentest] > /load /<dir>/.kiro/backups/session_b4da9058-648f-454e-a748-3ef3c1615868.json
✔ Imported conversation state from /<dir>/.kiro/backups/session_b4da9058-648f-454e-a748-3ef3c1615868.json
[gentest] > 前回の会話内容を要約して
> 前回の会話内容の要約:
1. 自己紹介の質問 - あなたが私の機能について質問
- 私がKiro(AWS開発のAIアシスタント)であることを説明
- AWS関連、開発・システム管理、その他の機能を紹介
2. 好きなAWSサービスについて - あなたが私の好みを質問
- 感情はないが実用的なサービスとして Lambda、S3、CloudFormation、VPC を挙げた
- あなたの使用状況や興味について逆質問
現在のディレクトリは /<dir>/backup_test です。
✓ 1 of 1 hooks finished in 0.08 s
▸ Time: 6s
読み込み成功です。完全に終了してしまったセッションを復元できました。
セッションIDは1回目のもの(b4da9058-648f-454e-a748-3ef3c1615868)がそのまま使われるようで、新規作成ではなく追記になりました。
➜ ~ tail -1 ~/.kiro/backups/backup.log
yyyy-mm-ddThh:mm:ss+09:00: Updated backup with 1 new entries for session=b4da9058-648f-454e-a748-3ef3c1615868 key=/<dir>/backup_test
Kiro CLIとのチャット内容を自動永続化できました!
自動実行はAgent Hooksによるイベント処理を行うことで実装できました。
今回使ったのはstopイベントだけでしたが、他にも様々なタイミングでイベントを発生させることができるため、
うまく活用すれば業務効率を大幅に向上させるカスタムエージェントが作れそうです。
今回の実行結果では、Hooksの処理時間は0.09s程度と非常に短い時間でした。
そのためチャットの流れを阻害せずに自動化できていましたが、JSONファイルが大きくなると処理時間が長くなる可能性があります。
対策としては、
・スクリプトをバックグラウンド実行
・サービス化してsystemdで実行
などが考えられます。
今回は可能な限りKiro CLIのライフサイクルの範囲内で収めるように実装しました。
そのため保存処理はリクエストのたびに実行されます。
これを、Kiro CLIが終了したときに1回だけ保存等、処理回数減らしたい場合は、
シェル関数やエイリアスでKiro CLIの起動コマンド自体をラップして、終了コードを受け取ったら処理を実行する、
といった実装になるかと思います。
/loadについて今回は/saveと同等の形式で保存したため、/loadで読み込むことでチャットを復元することができました。
しかし/loadはファイル名を指定する必要があります。/saveと同様、なかなか手間です。
また今回の保存方法ではファイル名にセッションIDを付けていますが、それだけではどんな内容のチャットだったかを知ることができません。
せっかく保存を簡単にしたので、読み込みについても要検討です。
以上になります。
ここまで読んでいただき、ありがとうございました。