aichatのsymlink環境でfunction callingが止まる原因
aichatのshell toolがPATHの再帰で停止した原因と修正。LLM導入で使う開発ツール連携を、実パス比較で安定させた記録です。
背景
aichat のfunction callingが、symlink経由の llm-functions で止まる問題を直しました。Rust製CLIから llm-functions/tools/*.sh の jq、rg、tokei、dust を呼ぶ構成です。LLMを開発ツールへ接続する部分の修正です。
argc build は llm-functions/bin/ にsymlinkを作ります。bin/jq → ../scripts/run-tool.sh のように共通runnerへ向け、run-tool.sh が $0 から名前を判定して tools/*.sh を実行し、$LLM_OUTPUT へ書きます。
通常のチャットは動きますが、shell toolは [DEBUG] tool start: tokei の後に止まり、[DEBUG] tool done が出ませんでした。
この環境の llm-functions はsubmoduleではなく、/Users/ksh3/Development/aichat/llm-functions → /Users/ksh3/Development/llm-functions のsymlinkです。このパスの違いが原因でした。
Rust側で確認したこと
最初は、直前に追加したRust側の機能を疑いました。
src/function.rs と src/utils/command.rs に未コミットの差分はなく、明確な回帰も見つかりませんでした。git log の「mistake refactor」も直接の原因ではありません。
cargo run -- --info で function_calling: true、functions_dir、functions.json、tools.txt を確認しました。./Argcfile.sh test-function-calling は Error: Unknown role '%functions%' で失敗しましたが、これは別のロール設定の問題です。
再現ログからの手がかり
再現ログです。
Call ctree_check {}
[DEBUG] tool start: ctree_check
[DEBUG] tool done: ctree_check exit=1
Error: Tool call exit with 1
pilot> run tokei
Call tokei {}
[DEBUG] tool start: tokei
MCP bridgeの ctree_check はエラー終了しましたが、止まりません。shellの llm-functions/tools/tokei.sh は tool start の後に返ってきませんでした。
MCPは run-mcp-tool.sh、shellは run-tool.sh を通ります。この差から後者へ調査を絞りました。
fork bomb の発見
bin/tokei '{}' </dev/null の直接実行は「error: invalid JSON data」で終了し、ハングしませんでした。
ps -Ao 'pid,ppid,command' には、同じ形のプロセスが数百ありました。
bash /Users/ksh3/Development/aichat/llm-functions/bin/jq -r def escape_shell_word:...
親子プロセスが無限再帰していました。run-tool.sh がJSON解析に jq を呼ぶと、PATH先頭の llm-functions/bin/ にある bin/jq が選ばれます。これは同じrunnerへのsymlinkなので、再び jq を呼びます。システムの /usr/bin/jq へ到達せず、fork bombでプロセス枠を使い切っていました。
再現条件です。
llm-functions/bin/が PATH に含まれている(aichat がツール実行時に自動的に追加する)run-tool.sh内の JSON パース処理がjqを呼ぶタイミングで、PATH クリーンアップがまだ行われていない- ツール名が
jqのようにシステムバイナリと衝突するものに限らず、すべてのシェルツールで発生する(run-tool.shは全ツール共通のエントリポイントであり、引数パースのjq呼び出しは全ツールの実行前に走る)
初回修正と失敗
既存の export PATH="${PATH//"$root_dir/bin:"/}" を jq より前へ移し、未処理だった run-agent.sh にも加えました。
それでも直りませんでした。root_dir/bin は実パス /Users/ksh3/Development/llm-functions/bin、PATHはsymlink経由の /Users/ksh3/Development/aichat/llm-functions/bin です。${PATH//"$root_dir/bin:"/} では一致しません。通常のsubmodule配置では、このパス差は生じません。
sanitize_path による解消
sanitize_path() を追加しました。
root_dir/binのリアルパスをcd ... && pwd -Pで解決する- PATH の各エントリについて、リテラルパスとリアルパス(
pwd -P解決後)の両方を比較する - どちらかが一致したエントリを PATH から除外する
run-tool.sh と run-agent.sh の最初の jq より前で実行し、古い文字列置換は削除しました。
次の動作を確認しました。
get_current_time '{}'— 正常動作tokei '{"path":"src"}'— ハングなしで正常動作jq '{"filter":".a","input":"{\"a\":1}"}'— ツール名がシステムバイナリと完全に衝突するケースでも正常動作pgrep -f 'escape_shell_word'— ゾンビプロセスがゼロであることを確認
Rust 側の事後クリーンアップ
調査中に追加したRust側の変更も、develop との差分で整理しました。
src/function.rsのデバッグ用eprintln!文([DEBUG] tool start/done)— デバッグ中に追加したもので不要なため削除src/utils/command.rsのrun_command_no_stdin()— stdin にStdio::null()を渡す関数で、ツールが stdin を待ってハングするのを防ぐ有効な改善。これは残した
パス除外の修正点
修正は、jq の前に bin/ をPATHから除き、pwd -P で実パスも比較することです。symlinkの表記が異なっても同じディレクトリを除外できます。
