挙動契約 (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 にすると rsf と
rrf は 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 を使ってください。
裏を返せば、依存してよい保証でもあります: 変更のない内容の再投入は構造上 無害なので、コーパスを素朴に丸ごと再投入しても安全です。この意味論の上で 文書索引を回す方法は コーパス索引パターン を参照してください。
ハンドラまで到達した store は result を持ちます: stored (行を書いた)、
skipped (重複ヒット、または永続化が一時停止中 — 異常ではない)、rejected
(拒否、reason 付き)。その上位に 1 層あり、そこは汎用の形で答えます: ACL を
設定している場合、クライアントに許可されていない呼び出しは
{ok: false, error: "permission_denied", tool, client_id} を返し result を
持ちません。まず ok is false で分岐し、その後に result を見てください。
6. autocut は類似度スケールのシグナルでしか発火しない¶
autocut (スコア差が最大の箇所で切り落とす仕組み) は、スコアの差が関連性の 切れ目を表しているという前提に立ちます。これが成り立つのは類似度スケールの シグナルだけです:
- 発火する: confidence スコアリング下、または全行がシグナルを持つ均質な 生コサインのリスト。
- 意図的に不活性:
rsfとrrfの順序付け下。ランク融合のスコアは構造上 双曲的に減衰し、その差は関連性の切れ目ではなく 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_healthはstatusという単一の判定を返します。従来の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 を伴う
応答は失敗として扱う」と述べているのはそのためです。