コンテンツにスキップ

挙動契約 (Behavior Contracts)

対象: CPersona 2.5.x。 このページの記述は現行リリースラインのソースと 照合済みです。ここに書かれた挙動は契約です: 呼び出し側は依存してよく、 変更する場合は pre-release ladder とリリースノートを通ります (リリースライフサイクル 参照) — 黙って 変わることはありません。

翻訳について: 正本は英語版です。記述が食い違う場合は英語版が優先です (右上の言語切替から参照できます)。

このページは、ツール名だけからは取り違えやすい挙動を集めたものです。 いくつかは実運用のオペレーターが外側から CPersona を計測して発見したもので、 挙動が意外に見えるものにはその場で理由を併記しています。


1. recall の返却順: 末尾が最良

recall は内部で候補を「良い順」に並べ、limit で切り、そのスライスを 反転します。応答はスコアの昇順で並んでおり、最後の要素が最強のマッチ です。

これは意図的な設計です。LLM は文脈の末尾に最も強く注意を向ける ("lost in the middle") ため、最強の記憶を注入点の手前側に置いています。

帰結:

  • 評価するとき: recall の応答に対して hit@k を測るなら、末尾から 数えてください。先頭から数えると結果が反転します。
  • recall_with_context は契約が異なります: 渡した会話履歴と想起した記憶を マージし、スコア順ではなく時系列順で返します。

2. confidence スコアリングは融合モードを上書きする

CPERSONA_CONFIDENCE_ENABLED (既定 false) は、メタデータを足すだけの スイッチではありません。on にすると:

  • 結果集合が confidence スコアで並べ直され
  • 品質ゲートが融合スコアではなく confidence を見るようになります。

融合モード (CPERSONA_RECALL_MODE=rrf|rsf|cascade) はどの候補が結果集合に 入るかは引き続き決めますが、返ってくる順序はもう決めません。 1,545 文書のコーパスに 394 クエリで実測: confidence を on にすると rsfrrf は 394 クエリ全てで同一の行を同一の順序で返し、off では一致率が 10% 未満でした。

ランキングとゲートのシグナル優先順位は confidence > rsf > cosine > rrf です。スコアの付いた行では match_reason.signal が、その行で実際にどの分岐が 効いたかを報告します。スコアの付かない行はこのキー自体を持ちません — 注入される プロフィール行と、cascade recall が埋める FTS / keyword 行です。match_reason は「全行にあるフィールド」ではなく「あるかないか」として扱ってください。

なお confidence はマッチ強度ではありません: コサイン類似度・時間減衰・ resolved 状態・想起回数をブレンドした別の量です。完全一致の行が言い換えの行 よりこの尺度で低く出ることは、正当に起こり得ます。

3. エピソード境界ペナルティ

エピソードが存在するとき、最新のエピソード境界より古い記憶には減衰係数が 掛かります:

factor = max(exp(-RATE × hours_before_boundary), FLOOR)
つまみ 環境変数 既定値
有効化 CPERSONA_EPISODE_PENALTY_ENABLED true
レート CPERSONA_EPISODE_DECAY_RATE 0.01
下限 CPERSONA_EPISODE_DECAY_FLOOR 0.5
  • 境界は最新エピソードの created_at で、クエリと同じ分離軸 (agent / project / channel) にスコープされます — 無関係なバケットの エピソードがあなたの境界を動かすことはありません。
  • 境界以降の記憶 (現在のセッション) は影響を受けません (係数 1.0)。
  • 既定値では係数は約 69 時間 (ln 2 / 0.01) で下限に達します。つまり 約 3 日より古いものは一律に半減します。この機構は現在のセッションへの 緩い優先であって、細かい新しさランキングではありません — 直近数日の 内部での順序付けはこの分解能の外です。(緩やかなカーブが欲しければ RATE=0.002 でランプを約 2 週間に伸ばせます。)

一括インポートの落とし穴: 境界は単に「最も新しいエピソード行」です。 過去の会話を archive_episode で後から流し込むと、インポート時刻が境界に なり、既存の記憶が全てペナルティ領域に落ちます。エピソードを後から流し込まない か、インポート中は CPERSONA_EPISODE_PENALTY_ENABLED=false にしてください。

4. ベクトル走査ウィンドウ (CPERSONA_MAX_MEMORIES)

CPERSONA_MAX_MEMORIES (既定 10000) は保存上限ではありませんベクトル検索の走査ウィンドウです: ベクトル検索は直近 N 行だけを対象に します (記憶とエピソードはそれぞれ別にウィンドウ適用されます)。ウィンドウより 古い行はベクトル検索からは見えませんが、ウィンドウ制限のない FTS 経路と キーワード経路からは到達できます。

大きなコーパスに対するサポートされた答えは環境変数を上げることです — この定数は回避策を設計すべき限界ではなく、つまみとして存在しています。 コスト目安: 768 次元 float32 の埋め込みは 1 行あたり約 3 KB なので、 10,000 行のウィンドウでは最悪ケースで 1 回の recall あたり約 60 MB を読みます (記憶 + エピソード)。アーカイブや間引きの定期処理は不要です: 長期的なモデルは 物理削除をしない — 古い行はウィンドウと減衰で沈むです。

5. 重複排除の意味論: upsert ではなく skip

store は 2 通りの重複排除を行いますが、両者のスコープは異なります:

  • msg_id 重複排除 — 既存の msg_id を持つ storeスキップされ ます (result: "skipped"、既存行の id をエコー)。この判定は agent と project には及びますが channel には及びません: 同じ msg_id を別の channel に 書いても、最初の channel の行に対してスキップされます。
  • 内容重複排除 — 同一の内容文字列も同様にスキップされ、agent / project / channel にスコープされます。ユニーク索引が裏打ちしますが、その範囲は厳密な バケット (agent_id, project_id, channel, content) に限られる一方、先に走る 判定はグローバルプールも見ます。したがって 異なる project バケットへ並行に 書き込む 2 者は、どちらも着地しえます。

決定的な帰結: upsert は存在しません同じ msg_id変更された内容を 再保存しても、保存済みの行は更新されません — スキップされます。保存済みの 記憶を変更するには update_memory (自動で再埋め込み) か、delete_memory + store を使ってください。

裏を返せば、依存してよい保証でもあります: 変更のない内容の再投入は構造上 無害なので、コーパスを素朴に丸ごと再投入しても安全です。この意味論の上で 文書索引を回す方法は コーパス索引パターン を参照してください。

ハンドラまで到達した storeresult を持ちます: stored (行を書いた)、 skipped (重複ヒット、または永続化が一時停止中 — 異常ではない)、rejected (拒否、reason 付き)。その上位に 1 層あり、そこは汎用の形で答えます: ACL を 設定している場合、クライアントに許可されていない呼び出しは {ok: false, error: "permission_denied", tool, client_id} を返し result を 持ちません。まず ok is false で分岐し、その後に result を見てください。

6. autocut は類似度スケールのシグナルでしか発火しない

autocut (スコア差が最大の箇所で切り落とす仕組み) は、スコアの差が関連性の 切れ目を表しているという前提に立ちます。これが成り立つのは類似度スケールの シグナルだけです:

  • 発火する: confidence スコアリング下、または全行がシグナルを持つ均質な 生コサインのリスト。
  • 意図的に不活性: rsfrrf の順序付け下。ランク融合のスコアは構造上 双曲的に減衰し、その差は関連性の切れ目ではなく retriever の重なりを表します。 融合順の結果では、混入の抑制は融合側の品質ゲートが担います。

したがって既定構成 (confidence off、rrf または rsf) では、 CPERSONA_AUTOCUT_MIN_RESULTS をいじっても recall 件数は変わりません。 融合モードでゲートを動かすつまみは set_recall_precision です — recall のチューニング を参照してください。

7. プロフィール行はスコアを持たない

update_profile の行は注入行として recall 応答に付加されます — スコアリングには 参加しません。行は最大 1 件です: profiles(agent_id, user_id) で一意であり、 どの書き込み経路も user_id'' に束縛するため、2 回目の update_profile は 蓄積されず 1 件目を置き換えます。

  • confidence off (既定) では、プロフィール行はスコアを持たず最後尾に並び、 スコア付きの結果だけで limit が埋まっていると切り落とされます。 実コーパスに rsf / limit=10 で実測: 生き残ったプロフィール行は 0 件 でした。
  • confidence on では、プロフィール行は高い confidence スコアを受け取り、 安定して上位に現れます。

confidence を有効にして運用しているのでない限り、プロフィールを「常に必ず 注入されるチャネル」として扱わないでください。常に必ず存在してほしい事実に 対する正しい機構は、確率的な recall ではなく決定的注入 (あなたの CLAUDE.md やシステムプロンプト) です — recall に頼らないという選択 を参照してください。

8. gate_fallback の応答は低信頼

recall の応答が gate_fallback: true を伴う場合 (伴わないときは欠落)、 全ての候補が品質ゲートを下回ったため、空の結果の代わりにゲート未満の 字句マッチを返したという意味です。これらの行は低信頼として扱ってください — 識別子やハッシュの検索で、完全一致がクエリ文と意味的に遠い場合に典型的です。

この救済経路は confidence スコアリング有効時にしか存在しません。 返される行に 印を付けるのは CPERSONA_CONFIDENCE_ENABLED が有効なときだけ走る backfill なので、 既定の構成では gate_fallback は決して現れません: 全候補がゲートを下回った recall は 単に空を返します。

9. lock_memory は保護する、押し上げはしない

lock_memory は行を削除と編集から保護します。ランキングには影響しません — ロックした記憶でも recall に負けることはあります。要件が「決して失われて はならない」ならロックしてください。要件が「常に文脈に載っていてほしい」 なら、決定的注入を使ってください (プロフィールに関する注意は §7 を参照)。

10. 応答の形: 成功と失敗の見分け方

v2.5.2 以降、規則は統一されています: ok is false で分岐し、error を 伴う応答は ok の有無にかかわらず失敗として扱ってください。

同リリースで 3 点が変わりました。いずれも、それまでの形では失敗が成功として 読めてしまったためです:

  • store は結果を result で報告しますstored / skipped / rejected — 常に true の ok ではなくなりました。以前は拒否された書き込みが 成功と同じ形に見えていました (§5 参照)。
  • check_healthstatus という単一の判定を返します。従来の healthy ブール値はありません。
  • ハンドラが返すツールレベルの失敗は、すべて ok: false を伴います。 以前は多くが error だけを返し、分岐できる ok がありませんでした。説明は 従来どおり error で運ばれます — ただし store だけは reason に入ります。

次の 2 つの形は、この規則の外側にあり、以前からそうでした:

  • 最外層の MCP ディスパッチは、未知のツール名や、ハンドラから漏れた例外に 対して、ok を持たない素の error で応答します。この層は他の Cloto サーバーと 共有するライブラリから vendoring されているため、揃えるにはローカルの修正では なく上流の変更が要ります。
  • 成功した読み取り (get_contents / list_memories / list_episodes / get_profile) も ok を持たないペイロードを返します。

どちらも上の規則で覆われます。規則を「ok を確認する」ではなく「error を伴う 応答は失敗として扱う」と述べているのはそのためです。