Skip to content

Guides Ambient Recall

May-Kirihara edited this page Jul 2, 2026 · 13 revisions

Ambient Recall — 受動的な文脈注入

Ambient Recall は、エージェントフレームワークの「プロンプト送信時フック」を使って、ユーザーが送ったプロンプトを GaOTTT で自動 recall し、検索結果をそのターンの文脈に添える機構。LLM が明示的に recall を呼ばなくても、長期記憶から関連する前提知識が受動的に注入される。

「明示的に使わなくても、自動で記憶が効く」— recall を 道具 から 環境 に変える。

三つの read 側使い分け (それぞれ異なる主体・モード):

  1. Task-driven retrieval (Use as Memory) — agent が明示的に recall(query) で叩く
  2. Passive lens ← 本ガイド — hook injection で何もしなくても文脈が浮かぶ
  3. Narrative engine (Use as Narrative Engine) — explore × N + recall × N 並列で重力場を能動的に辿り、創発的な narrative を組み立てる (好奇心駆動の navigation、external LLM が同等体験を独立確証 → evaluation-2026-05-27)

ambient_recall は (2) 専用 — (1)(3) のタスク駆動・好奇心駆動の場合は本機構を bypass して上記 Guide へ。

対をなす write-side 機能 → Save Candidates Hook (2026-05-27: v1 Claude Code + v2 opencode 完了)。ambient_recall が「過去の記憶を read として注入」する読み側 lens なのに対し、save_candidates は「直前ターンから save 候補を抽出して次プロンプトに注入」する書き側 lens。Claude Code では Stop hook で auto_remember → state file → 次 UserPromptSubmit で inject という 2-script bridge、opencode では chat.message の 1 plugin (scripts/hooks/opencode-save-candidates.ts) で前ターン lookback + 当ターン message への inject を同期実行 (state file 不要)。設計原理は同じ「観察層は自動、physics 層 (remember 呼び出し = mass の入口) は能動的判断のまま」。両者で read と write の対称な observation lens が揃う。

対応フロントエンド

同じ GaOTTT エンジン・同じ重力場を、複数のエージェントフロントエンドが共有する:

フロントエンド フックポイント リポジトリ内スクリプト(SoT)
Claude Code UserPromptSubmit hook scripts/hooks/ambient_recall.py
opencode chat.message プラグイン scripts/hooks/opencode-ambient-recall.ts
Codex CLI UserPromptSubmit hook (--codex) scripts/hooks/ambient_recall.py (同一スクリプト)

Codex CLI は Claude Code とほぼ同じ hook イベント体系(Codex hooks)を持つので、opencode のように別スクリプトを書かず 同じ Python フックを --codex フラグ付きで再利用する。唯一の差は出力形式: Claude Code は hook の raw stdout をそのまま注入するが、Codex は JSON エンベロープ ({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": ...}}) を読む。--codex(or GAOTTT_HOOK_OUTPUT=codex)がこのエンベロープに切替えるだけで、relevance gate・スロット組み立て・GAOTTT_AMBIENT_* env・fail-safe は全フロントエンド共通。transcript reader も Codex の rollout JSONL(event_msg/user_message)を解釈するので、Claude Code が transcript_path を scan するのと同じ downstream 変数(history / recently_surfaced)に集約され、multi-turn・novelty も parity が成立する(Claude-only の hook_success attachment 走査は Codex transcript では no-op に degrade)。source 分岐ゼロ: フロントエンド判定は出力アダプタ層に閉じ込められ、physics(relevance/gravity)には一切触れない。

opencode プラグインは独自に MCP を叩かない — Claude Code 版の Python フック (scripts/hooks/ambient_recall.py) をそのまま子プロセスとして呼ぶ。relevance gate・スロット組み立て・GAOTTT_AMBIENT_* 環境変数・fail-safe はすべて 1 箇所(Python フック)に集約され、ambient_recall プロトコルが変われば両フロントエンドが同時に追従する。opencode プラグインは「メッセージのテキストを取り出し Python フックに渡し、ブロックが返ってきたら追記する」薄い shim だが、transcript file を持たない opencode のために 1 つだけ追加責務を持つ: client.session.messages() (OpenCode SDK) で過去の user message を取得 → 過去 N turn の text を history (Refinement Stage 4 multi-turn 連結用) として、過去 N turn の <!-- ambient-ids ... --> manifest から id 出現回数を recently_surfaced (Lateral Association Stage 1 novelty decay 用) として、Python フック stdin の payload に直接乗せる。Claude Code は transcript_path を渡してフック側で scan、opencode は同じデータを SDK 経由で組み立てて直接渡す — どちらの経路も Python フック側で同じ downstream 変数に集約され、機構として完全に parity が成立する。

何をするか

ユーザーがプロンプト送信
   ↓
プロンプト送信時フック発火(Claude Code: UserPromptSubmit / opencode: chat.message)
   ↓
プロンプトを query にして MCP ツール ambient_recall を呼ぶ
   ↓
サーバ側で 1 回の passive recall → 構造化スロットを組み立て + relevance gate
   ↓
gate を通れば <gaottt-ambient-recall> 構造化ブロックを文脈に添付(通らなければ無言)
   ↓
LLM はユーザー発話 + 関連記憶 の両方を見て応答

注入ブロックは明示的に <gaottt-ambient-recall>...</gaottt-ambient-recall> で囲まれ、「これは検索された参考知識であってユーザーの発話ではない」と LLM に伝える。

構造化スロット — 注入はフラットな top-k ではなく、複数スロットの構造ブロック(Ambient Recall Enrichment、MCP ツール ambient_recall):

  • ▼ 直接ヒット — final_score 上位の記憶。
  • ▼ 重力レンズ — embedding 的には query から遠いのに、重力場の displacement が query 近傍まで引き寄せた記憶。場が学習した類推で、素の検索には出せない枠。Lateral Association Stage 3 で top-1 → top-K に拡張 (config.ambient_lensing_max_k 既定 2):複数の lateral 連想 ("X といえば Y で、Y といえば Z") が同 turn で同時発火し、人間が会話で自然に紡ぐ associative chain を機構として担保する。各 pick は独立に min_score/min_gap を clear する必要があり、quota 緩和はない (= 「無理に第 2 を出す」のではなく「第 2 も genuinely な bent association ならば surface する」)。Stage 5 で各 pick に resonance ([0, 1)、cooccurrence-derived) も付く: gap は「曲げの強さ」だけを表すが、resonance は 「場が過去にこの memo を今日の direct hits と何度一緒に引いたか」 — 「曲げが強い + 場が学習した path」を agent が両面で weigh できるようになる (= 突飛な lensing を discount する材料)。
  • ▼ ささやき — Observation Apparatus Refinement Stage 2 (2026-05-26 追加)。mode="dormant" で取り出される「使われていない・古い memo」(mass ≤ dormant_mass_threshold、age ≥ dormant_age_threshold_seconds、source ∈ dormant_source_classes) のうち、query に対して BM25 で強一致 したもの 1 件を whisper として混ぜる。これは「〇〇といえば〜だったよな」感覚を機構化する slot — 場が手放しかけていたが、語が触れて 1 度だけ surface する。一致しなければ slot は空 (silence beats off-topic noise)。force computation / mass update は一切触らない (counter-importance sampling)。ambient_dormant_relevance_floor (既定 0.5) 以上の BM25 score が必要、ambient_dormant_slot_count (既定 1) で件数上限、ambient_dormant_slot_enabled=False で完全 off。manifest にも dormant= キーで載るので hook の rotation list が次 turn の重複を防ぐ。
  • ▼ ⚠ 矛盾 — surface 記憶の contradicts エッジのペア。
  • ▼ いま誰として — active な declared value/intention(grounding)。スロットを埋めるのは「mass 上位を query 関連度で再ランクした top1」: 候補プール (mass 上位 N、既定 N=10) を score = (mass ** w) × cos(query, persona_vec) で再ランクし、cos < ambient_persona_min_relevance なら slot を空にする (Plans — Ambient Recall Refinement Stage 1)。w = ambient_persona_mass_weight (既定 0.3、2026-07-02 昇格) は heavy persona dominance の対処 knob — production で 1 つの persona の mass が突出している場合、0.5 (緩やか) / 0.0 (cos のみ) とさらに下げて cos 軸の影響を強める、逆に 1.0 で Stage 1 (mass 完全寄与) に戻す (Operations — Troubleshooting 「query 横断で同じ persona に固定される」節)。

各エントリは provenance メタ(source · certainty · age)付き。スロットの組み立て・relevance gate・整形はすべて サーバ側の ambient_recall サービスが行い、フックは「ブロックが返ってきたら emit、センチネルなら無言」だけの薄いラッパ。

「〇〇といえば〜だったよな」軸 — session-aware novelty decay

毎ターン同じ memo が surface すると、3 turn 目には LLM がそれを白色雑音として読み飛ばし始める。Lateral Association Stage 1 (Plans — Ambient Recall Lateral Association) は 直近 N turn で何回 surface したか を caller (フック) が tracking して、recently-surfaced な memo の ranking を decay ** count で抑える。会話の自然な「あ、そういえば〜」の 新鮮さ 軸を機構で担保する。

component 役割
<!-- ambient-ids ... --> manifest サーバの formatter が ambient block 末尾に毎回付ける id list (HTML コメントなので markdown render では不可視、フックからは parse 容易)
フックの _recently_surfaced() transcript の直近 GAOTTT_AMBIENT_NOVELTY_TURNS 回 (既定 5) の <gaottt-ambient-recall> ブロック (UserPromptSubmit の hook_success attachment) から manifest を抽出して {node_id: count} を組み立てる
ambient_recall(recently_surfaced=...) 各 slot の ranking を score *= ambient_novelty_decay ** count で減衰 (decay=0.7 既定で 1 回再 surface → 70%、2 回 → 49%)。direct slot は再 sort、lensing は decay された gap で argmax (露出 gap は raw 値)、persona は (mass^w) × cos × novelty で再 winner 決定

ロールバック: フック側 GAOTTT_AMBIENT_NOVELTY_TURNS=0 で manifest scan を止める (= recently_surfaced 送付なし)、または server config ambient_novelty_decay=1.0 で式を no-op に。両方 default 値で本番有効、片方を逃すだけで他方は legacy 等価。

Passive 原則との関係: novelty decay は slot pick の ranking だけ を曲げる。node の mass・displacement・co-occurrence は一切触らない (ambient_recall は passive=True のまま)。「経験は言葉にすることで質量を持つ」原則は無変更で、session 内 surface 履歴は per-call の transient な ranking 補正としてだけ存在する。

S/N knob — 会話 source damping + dump-shape gate (Round 2 E1/E2)

Observation Apparatus Round 2 (Plans Stage E) で導入された 2 つの slot 選別 lens。2026-06-12 dogfooding で direct hits 6 件中 5 件が chat-history raw chunk (state-dict ダンプ・無関係コード断片・依頼定型句一致) だった観察への対処。2026-06-12 に両方とも default ON へ昇格 (本番実機評価 id=fa1d03a8 で意図どおりの分離を確認、旧 ambient が会話ログ raw chunk に支配されて実用性が低かった経緯)。

knob default 効果
ambient_conversational_source_factor (E1) 0.5 = ON (1.0=OFF) ambient_conversational_sources (既定 openai / claude-web / claude-code) の item の slot ranking score を factor 倍。novelty decay と同じ combined multiplier に合成、direct + lensing のみ (persona slot 不触)
ambient_dump_symbol_ratio (E2) 0.45 = ON (>=1.0=OFF) content 先頭 400 字の dump score (max(記号比率, 長 ASCII 識別子トークン比率)) が閾値超の候補を slot から除外、次点繰り上げ (自然文 <0.05 / readable code ≤0.40 / state-dict 羅列 >0.97)

どちらも 観測・注入層のみ — recall 本体 / force / mass には不適用 (Phase M 単一規則不変。precedent: exclude_tags、novelty decay)。rollout は Stage 7 anti-hub と同じ 2 段階を踏んだ: code merge (OFF) → env/config opt-in で dogfooding → 実機評価で効果確認 → code default 昇格 (本ターン)。legacy 挙動に戻すには両 knob を 1.0 に。

なぜ passive recall なのか — 観察者効果

GaOTTT の recall は副作用を持つ TTT ステップである。1 回 recall するたびに retrieved nodes は query 方向に displacement が nudge され (Phase I query attraction)、mass が accrete し、co-occurrence edge が引かれる。recall は重力勾配を供給する backward pass。

ところが ambient recall は毎ターン・全プロンプトで発火する。「ls して」「typo 直して」のようなノイズクエリまでもが毎回 recall を走らせることになる。通常の recall をそのまま使うと、ノイズクエリが毎ターン重力場を揺らし、displacement / mass / edge をでたらめな方向に学習させてしまう — GaOTTT が acceptance test を別プロセスに隔離してまで守ってきた「観察行為が観察対象を変える (P7-Z)」を、エージェント本体が毎ターン破ることになる。

そこで recall(passive=True) を使う。passive recall は検索・wave 伝播・scoring はそのまま走らせ結果も同一だが、末尾の simulation update を丸ごとスキップする:

通常 recall passive recall
mass 更新あり mass 更新なし
query attraction displacement あり displacement なし
co-occurrence edge あり edge なし
last_access 更新あり last_access 更新なし
prefetch cache に書く cache を読むが書かない

passive recall は 「摂動なしの観察」 — 場を読むが動かさない。ambient な大量クエリが無制御の TTT シグナルになることがない。詳細は MCP Reference — recall の Passive recall 節。

接続先 — なぜ 7878 の MCP backend か

フックは稼働中の GaOTTT エンジンに接続する必要がある。proxy mode(推奨構成)では稼働中のエンジンはポート 7878 の MCP backend ただ 1 プロセス。Python フックは mcp クライアント SDK で http://127.0.0.1:7878/mcp に streamable-http 接続し、ambient_recall ツールを呼ぶ。

第 2 のエンジンプロセス(独立 REST サーバ等)を立てない理由は、RURI 二重ロード・stale cache・write-behind 上書き罠(Operations — Server Setup 参照)を避けるため。フックは単一の共有エンジンに相乗りする。Claude Code・opencode・複数エージェントが同時に走っても、相乗り先は常にこの 1 プロセス — だから全員が同じ記憶・同じ重力場を見る。

セットアップ — Claude Code

.claude/ は gitignore 対象なので、フックスクリプト(scripts/hooks/ambient_recall.py)はリポジトリで共有されるが、フック登録は各自の settings.json に書く。

  • global: ~/.claude/settings.json — 全レポで Claude Code を起動したときに hook が effective (GaOTTT 以外のレポでも自動で記憶が引かれる)
  • per-project: <project>/.claude/settings.json — そのプロジェクトでだけ on

どちらにせよ path は絶対パスで書く こと。$CLAUDE_PROJECT_DIR を使うと Claude Code が「いま開いている project の dir」に展開するので、GaOTTT 以外のレポで起動した瞬間に hook script が見つからず operation blocked by hook で agent 動作が止まる (2026-05-27 production 発覚)。

/Path/to/GaOTTT を実際の GaOTTT clone の絶対パスに置き換え:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"/Path/to/GaOTTT/.venv/bin/python\" \"/Path/to/GaOTTT/scripts/hooks/ambient_recall.py\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

GaOTTT working tree が main (or scripts/hooks/ambient_recall.py を含む branch) に check out されていることも確認 — 古い branch のままだと disk から script が消えていて、上記と同じく hook block が起きる。

セットアップ — opencode

opencode は起動時にプラグインディレクトリの *.ts を自動ロードする。スコープは 2 つ:

  • グローバル ~/.config/opencode/plugin/ — 全 opencode セッションに効く
  • プロジェクト単位 <project>/.opencode/plugin/ — そのプロジェクトでのみ

リポジトリ内の SoT は scripts/hooks/opencode-ambient-recall.ts。これをプラグインディレクトリにコピー(または symlink)する + GAOTTT_REPO の export は必須:

# 1. plugin install
mkdir -p ~/.config/opencode/plugin
cp scripts/hooks/opencode-ambient-recall.ts \
   ~/.config/opencode/plugin/gaottt-ambient-recall.ts

# 2. shell rc に GAOTTT_REPO を追加 (必須)
echo 'export GAOTTT_REPO=/Path/to/GaOTTT' >> ~/.bashrc   # 自分の clone path に置き換え
source ~/.bashrc

⚠️ GAOTTT_REPO を設定しないと plugin は silent fail する。TS plugin の内部 fallback は process.env.GAOTTT_REPO ?? "/mnt/holyland/Project/GaOTTT" — 本リポジトリ作者の machine の path が default で hard-coded されている。他人の machine では env を set しないと wrong path で Python interpreter を探し、error も出さず block を inject しない(hook が「fail-safe」設計なので一切静かに止まる)。GAOTTT_AMBIENT_PYTHON / GAOTTT_AMBIENT_SCRIPT を個別に指定することも可能だが、GAOTTT_REPO 1 本で済むのが推奨。

プラグインは opencode の chat.message フック(新しいユーザーメッセージ受信時に発火)で動く。メッセージのテキストパートを連結して prompt にし、Python フック scripts/hooks/ambient_recall.py を Bun.spawn で子プロセス起動して {"prompt": ...} を stdin に渡す。返ってきた <gaottt-ambient-recall> ブロックを、メッセージ末尾のテキストパートに追記する(Claude Code フックがユーザー発話の後ろに文脈を添えるのと同じ振る舞い)。

opencode サブエージェントにも効く — secondopinion-MCP 経由で起動される opencode サブエージェントを含め、chat.message を持つすべての opencode セッションに注入される。グローバルインストールなら、どのディレクトリで起動した opencode エージェントも同じ長期記憶を共有する(「opencode エージェントにも同じ記憶を」という目的どおり)。ただし subprocess の env に GAOTTT_REPO が継承されている必要があるので、shell rc に書くのが確実。

セットアップ — Codex CLI

Codex は ~/.codex/hooks.json(グローバル)または <repo>/.codex/hooks.json(プロジェクト単位)で hook を登録する。リポジトリに雛形 .codex/hooks.json を同梱済み — UserPromptSubmit に ambient(ambient_recall.py --codex)+ save-candidates inject(save_candidates_inject.py --codex)、Stop に save-candidates 抽出(save_candidates.py)を配線してある。パスは README 規約の $HOME/GaOTTT(ホーム直下に clone)で書いてあるので 書き換え不要。

mkdir -p ~/.codex
cp "$HOME/GaOTTT/.codex/hooks.json" ~/.codex/hooks.json
# ホーム以外に clone した場合だけ $HOME/GaOTTT を実パスに置換:
#   sed -i 's#\$HOME/GaOTTT#/your/path/to/GaOTTT#g' ~/.codex/hooks.json

保存後、Codex CLI 内で /hooks を実行して各定義を review → trust(Codex は未信頼の command hook を実行しない)。[features] で hooks を無効化していなければ default で有効。MCP backend(proxy mode port 7878)は gaottt を MCP server 登録済みなら自動起動するので追加作業は不要 — backend が落ちている/遅いときは fail-safe で無出力(発話は決してブロックしない)。

  • パスの可搬性: Codex は command を shlex で分解するだけで $HOME を展開しない(Rust 実装は shellexpand 等を持たず shlex tokenizer のみ)。そこで各 hook を sh -c '…' 経由で起動し、シェルに $HOME/GaOTTT を展開させている。これで雛形が machine 非依存になり、~/.codex/hooks.json をそのままコピーで配れる。Codex の hooks.json は strict JSON(コメント不可・unknown field を弾く可能性があるので _comment も付けない)— 説明はこのドキュメント側に置く。Windows は sh -c が無いので絶対パス形式に置換。
  • 出力アダプタ: --codex フラグ(~/.codex/hooks.json が渡す)か GAOTTT_HOOK_OUTPUT=codex env で、raw stdout → JSON エンベロープに切替。それ以外は全フロントエンド共通の Python フック。
  • save-candidates bridge: Claude Code と同型の 2-script bridge(Stop で state file 書き込み → 次 UserPromptSubmit で読み出し + inject)。session_id で per-session に紐付くので Codex でもそのまま機能する。
  • 無効化: GAOTTT_AMBIENT_RECALL=0 / GAOTTT_SAVE_CANDIDATES_ENABLED=0、または ~/.codex/hooks.json から該当エントリを削除。

ステップ別の導入手順は Tutorial 03 — クライアント接続 §E を参照。

backend の再起動が必要なとき

passive 引数・ambient_recall ツールは新規追加。proxy mode の HTTP backend は git push だけでは更新されない(CLAUDE.md / Operations — Server Setup の「backend kill on code deploy」)。ambient recall を有効化する前に、古い backend を kill して新コードを乗せる:

ps -ef | grep "gaottt.server.mcp_server.*streamable-http" | grep -v grep
kill <pid>   # 次の MCP 接続で新コードの backend が auto-respawn

古い backend のままだと ambient_recall / passive=true が未知として弾かれ、フックは fail-safe で無出力になる(壊れはしないが効かない)。

設定(環境変数)

Claude Code は .claude/settings.json の command か shell 環境で、opencode は opencode を起動するシェル環境で、Codex は ~/.codex/hooks.json の command か shell 環境で渡せる。

環境変数 既定 適用 説明
GAOTTT_AMBIENT_RECALL 1 両方 0/false/off でフック全体を無効化
GAOTTT_HOOK_OUTPUT (未設定) Python フック 出力形式。未設定 / text で raw stdout ブロック(Claude Code / opencode)、codex で JSON エンベロープ(Codex CLI)。Codex の ~/.codex/hooks.json は --codex フラグで同じ切替を行う
GAOTTT_AMBIENT_URL http://127.0.0.1:7878/mcp Python フック MCP backend の URL
GAOTTT_AMBIENT_DIRECT_K 2 Python フック 直接ヒットスロットの件数(ambient_recall の direct_k)
GAOTTT_AMBIENT_MIN_SCORE (未設定) Python フック フォールバック virtual_score gate のしきい値上書き。主たる gate は BM25(後述、config.ambient_bm25_min_score)で、これは BM25 不在時のみ効く
GAOTTT_AMBIENT_TIMEOUT 6.0 両方 recall のハードタイムアウト(秒)。steady-state ~0.5s だが backend 再起動直後の数分は virtual FAISS 等の warmup で ~3-4s。opencode プラグインは子プロセスにこれ + 3 秒の余裕を与える
GAOTTT_AMBIENT_MIN_CHARS 12 両方 この文字数未満のプロンプトはスキップ
GAOTTT_HOOK_ANTI_RESTACK 1 Python フック 0/false/no で再注入 marker ガードを無効化 (default on)。プロンプトに既注入 marker が含まれている場合、バックエンド呼び出しをスキップし二重注入を防止する (opencode plugin と parity)
GAOTTT_AMBIENT_EXCLUDE_TAGS smoke-test,test Python フック substring マッチで direct / lensing / persona 候補から除外する tag リスト(CSV)。MCP/REST smoke 用 memory を corpus に残しつつ ambient 注入だけ silent にする(Plans — Ambient Recall Refinement Stage 2)。空文字列で除外無効化
GAOTTT_AMBIENT_EXPOSE_BREAKDOWN (未設定) Python フック 1/true/on で各 slot 行末に [raw=.. virt=.. bm25 mass=..] を追記し、なぜその memory が surface したかを caller に露出する(Plans — Ambient Recall Refinement Stage 3)。default off — 本番では token budget 優先、debug session / measurement (Stage 5) で opt-in
GAOTTT_AMBIENT_HISTORY_TURNS 2 両方 当該プロンプトの前に concat する 直前 N 件のユーザープロンプト(Plans — Ambient Recall Refinement Stage 4)。Claude Code は transcript_path を tolerant に解析、opencode は client.session.messages() (SDK) を呼んで取得 — どちらも Python フック stdin の history payload に乗せる。0 で legacy(当該プロンプトのみ)。短い接続的 prompt("次のステップに進みましょう" 等)で context が落ちる失敗を補正。取得失敗時は silent に legacy 動作にフォールバック
GAOTTT_AMBIENT_NOVELTY_TURNS 5 両方 直近 N 個の ambient block の <!-- ambient-ids ... --> manifest を parse し、{node_id: count} を recently_surfaced として server に forward する(Lateral Association Stage 1)。Claude Code は transcript の hook_success attachment から、opencode は過去 user message text に append された ambient block から抽出。server 側で各 slot の ranking が ambient_novelty_decay ** count で減衰し、直近で出た memo が rotation する。0 で disable (= 旧挙動・無 decay)
GAOTTT_AMBIENT_SHOW_COMPOSED_QUERY (未設定) Python フック debug-only。1/true/on で ambient block 末尾 (closing tag 直前) に <!-- ambient: composed query = "..." --> を 1 行追加する(Lateral Association Stage 4)。GAOTTT_AMBIENT_HISTORY_TURNS で multi-turn 連結された 実際の query が agent から見えるので、「ambient 結果が変なのは query が変なのか recall が変なのか」を即座に分離できる。composed query が現プロンプトと同一 (連結が起きてない) のときは line 自体が省略される (debug 価値ゼロ)。token +50-200 字、本番では off
GAOTTT_REPO /mnt/holyland/Project/GaOTTT opencode プラグイン GaOTTT リポジトリのルート(PYTHON / SCRIPT の既定値の基点)
GAOTTT_AMBIENT_PYTHON $GAOTTT_REPO/.venv/bin/python opencode プラグイン Python フックを実行するインタプリタ
GAOTTT_AMBIENT_SCRIPT $GAOTTT_REPO/scripts/hooks/ambient_recall.py opencode プラグイン 呼び出す Python フックスクリプト
GAOTTT_AMBIENT_DEBUG (未設定) opencode プラグイン ファイルパスを設定すると各ステップの診断ログを追記。プラグインは設計上 fail-safe で無言なので、挙動の確認はこれで行う

relevance gate — 文脈汚染の防止

関連性に関わらず毎プロンプトに記憶を注入すると、無関係な記憶でコンテキストが薄まり逆に応答品質が落ちる。そこで relevance gate: gate を通らなければ ambient_recall は空応答を返し、MCP は (関連する記憶なし) センチネルを返す(フックはこれを見て無言)。gate はサーバ側の ambient_recall サービスが適用する。

gate の信号源は 語単位(Sudachi)BM25 の「強一致」gate。corpus 全体に対する専用の word-level BM25 index でプロンプトをスコアし、top BM25 が config.ambient_bm25_min_score(既定 32.0)未満なら注入しない。gate はサーバ側で recall の前に走るので、通らないプロンプトは recall コスト自体をスキップする。

なぜ「強一致 gate」なのか — 4 ラウンドの校正

2026-05-21 の本番コーパス(~32k memories)校正で、gate 信号を 4 段階で検証した:

  1. virtual_score(dense cosine) — ✗ off-topic も on-topic も ~0.6 に集まり温度ノイズ(±0.1)に埋没。max でも margin でも分離不能。
  2. char-3gram BM25 raw — ✗ クエリ長依存。「映画を3つ教えてください」のような長い off-topic が共通形態素を積み増し、簡潔な on-topic を上回る。
  3. char-3gram BM25 normalized — ✗ 短い 1-token クエリで破綻。
  4. 語単位(Sudachi)BM25 — ✓ 「卵焼き」が単一の語トークンになり、共通 3-gram の積み増しが消える。off-topic を ≤29 に抑え込み、強い on-topic は ≥34 — 32.0 はその谷。

重要な発見: この 32k コーパスはめいさんの生活と仕事まるごとで、真の "off-topic" は存在しない(「卵焼き」も雑談メモに語として実在する)。だから gate が分けられるのは「on/off-topic」ではなく「強一致 vs 弱一致」。閾値 32 の gate は「その話題を実質的に議論したことがある」プロンプトでだけ発火する高精度・低再現の動作になる(弱い on-topic も巻き込んで弾く)。

ambient_bm25_min_score はコーパス規模・クエリ長依存。記憶が増えたら再校正する。ambient_gate_tokenizer="sudachi" には bm25-sudachi extra が要る — 未導入なら gate index は構築されず virtual_score gate(ambient_min_score、弱い分離)に自動フォールバック。ambient_gate_use_bm25=False でも同じくフォールバック。

しきい値の校正

本番 DB の content から BM25Index(tokenizer="sudachi") を再構築し、代表プロンプト(強い on-topic / 弱い on-topic / off-topic)を search して top スコアの谷を観察し config.ambient_bm25_min_score に置く(Operations — Tuning の ambient_* 節、H5 env override は GAOTTT_AMBIENT_BM25_MIN_SCORE)。

fail-safe 設計

フックは構造上 fail-safe — ユーザーのプロンプトを絶対にブロックしない。backend が落ちている / 遅い / プロトコルエラー、stdin が壊れている、いずれの場合も無出力で exit 0。GAOTTT_AMBIENT_TIMEOUT 秒を超えたら諦める。GaOTTT が動いていなくてもエージェントの利用は一切妨げない。opencode プラグインも同じ — chat.message フック全体が try/catch で囲まれ、いかなる例外もメッセージをブロックも摂動もしない。挙動を確認したいときは GAOTTT_AMBIENT_DEBUG でログを出す。

Python hook は anti-restack guard (GAOTTT_HOOK_ANTI_RESTACK、default on) を持ち、prompt に既注入 ambient block の marker 文字列が含まれている場合は backend を呼ばず即座に return 0 する — フロントエンドが既注入プロンプトを再投入した際の二重スタックを構造的に防止する (opencode plugin と parity、backend 非依存)。

既知の性質 — ambient-only な記憶は decay する

passive recall は last_access を更新しない(read-only の一貫性)。したがって ambient フックでしか surface されない記憶は、使われていても decay し続ける。これは意図的: passive = 摂動なしの観察であり、「アクセスした」という recency 記録すら場に書かない。

実害は小さい — decay した記憶は消えるわけではなく、relevance gate が virtual_score(decay 非依存)を見るので ambient 注入は古い記憶でも効き続ける。記憶を「温かく」保ちたいなら、エージェントが明示的に recall するか remember し直せばよい(通常の能動的利用は last_access を更新する)。ambient recall は能動 recall を置き換えるのではなく補う。

無効化

  • 一時的に: GAOTTT_AMBIENT_RECALL=0 を環境に置く(両フロントエンド)
  • Claude Code 恒久的に: .claude/settings.json の UserPromptSubmit エントリを削除
  • opencode 恒久的に: ~/.config/opencode/plugin/gaottt-ambient-recall.ts を削除

関連

GaOTTT Wiki

(formerly GER-RAG)

🏠 Home

🚀 Getting Started

🌱 Tutorial / はじめての方へ

📚 Guides

🛠 MCP Reference

🌐 REST

🏗 Architecture

⚙ Operations

🗺 Plans

🔬 Research

💭 Reflections

Clone this wiki locally