Repository navigation
Plans Phase O TTT Observability
状態: Phase O 全 5 stage 実装完了 (2026-05-14)。Stage 1 (Score breakdown) / Stage 2 (Training delta trailer) / Stage 3 (Query routing) / Stage 4 (List mode) / Stage 5 (Dormant surface) で TTT loop の forward / backward / 認知負荷削減 / context 経済 / counter-importance sampling の 5 面が caller に literal に渡るようになった。設計同型 (
retrieval = gradient step) は caller を含めて閉じ、Phase I Stage 4 本番 acceptance で観察された surface-form mismatch (Stage 3 が吸収)・context 圧迫 (Stage 4 が圧縮)・自己発信 memo の埋没 (Stage 5 が counter-importance で蘇生) の 3 大罠を機構レベルで防御。次は本番 acceptance で挙動観察 → 必要なら Stage 6 / Phase P 候補へ。 関連: Roadmap, Architecture — Gravity Model, Phase I — Free Star Movement, Research — Gravity as Optimizer (TTT), Phase M — Mass Conservation 発端: 2026-05-14 セッション中の MCP usability 議論 — Phase I Stage 4 本番 acceptance で観察された「score deception」「persona invisibility」が LLM caller の TTT 可視性欠如 に起因することを発見、めいさんの「MCP は君が使うものだから使いやすい形にしたい」発言を受けて設計
GaOTTT の 中核となる設計同型 は以下の対応関係:
物理: 重力場での displacement 更新
TTT: SGD の gradient step
生物: Hebbian 強化 (recall された連合の強化)
Phase I Stage 2 (2026-05-11) で compute_acceleration に query attraction の第 4 項 a = (α · score / m_i) · (q - pos_i) を追加して、この対応が 解釈ではなく実装として literal に成立した。recall するたびに retrieved nodes の displacement が query 方向に nudge される。
しかし — LLM caller (= TTT の forward pass を駆動する agent) はこの training step が自分から見えない。
- final_score 0.92 が返ってきたとき、それが cosine 由来か mass 由来か wave 由来か分からない
- recall で displacement / mass が動いたが、何が動いたかが見えない
- 「現在 active な commitment」を recall で問うても persona ノードが surface しない (
reflect(aspect=...)を別途呼ぶ必要があると気付くまで認知負荷) - 古い自分が書いた agent memory が dense cluster に埋もれて差し戻されない
これは observability の欠如 = agency の欠如 という構造的問題。LLM が「自分が今どんな勾配を起こしたか」を見えてないと、自律的に judgement (score 信頼度、tool 選択、query 形式) できない。結果として:
- LLM 側で workaround (
source_filterを毎回付ける、reflectを併用する) が必要 - workaround を忘れると Sonnet が発見した「score deception」「persona invisibility」が顕在化
- 「ブラックボックスの SGD で訓練されている人形」になる
LLM caller を TTT loop の participant に昇格させる — gradient (= 結果の計算根拠) と parameter update (= 結果が状態に与えた変化) の両方を recall response に literal に込めれば、caller 側 (LLM) は workaround なしに自律的判断ができる。
これは:
- Phase M で確立した「source 分岐ゼロ・構造的識別子で全 source class に普遍適用」原則 と整合する caller-side の単一規則 — どの source でも同じラベル、どの query 形式でも同じ判定軸
- Five-Layer Philosophy の 関係層 を MCP layer に literal に翻訳する (人格層を
inherit_personaで literal に表したのと同じ移植) - 「使う側に見えないモデルは使う側に変えられない」(observability is agency) という設計倫理の物理実装
| Stage | 提案番号 | 内容 | TTT 軸 | UX 軸 | 規模 |
|---|---|---|---|---|---|
| 1 | #1 | Score breakdown — final_score の additive 内訳を露出 | ✅ 強 (forward pass の中身) | ✅ 中 | 小 |
| 2 | #5 | Training delta trailer — recall response に状態変化を付与 | ✅ 強 (backward/update の中身) | ✅ 中 | 小-中 |
| 3 | #2 | Query routing — query 形式 classifier で recall + reflect を merge | ➖ 中 (caller 認知負荷を engine 側に移譲) | ✅ 強 | 中 |
| 4 | #3 |
List mode — recall(mode='list') で id + excerpt[80] のみ |
➖ 弱 | ✅ 強 (context 経済) | 小 |
| 5 | #4 |
Dormant surface — explore(mode='dormant') で再活性化 |
➖ 中 (counter-importance sampling) | ✅ 中 | 中 |
実装順序: 1 → 2 → 3 → 4 → 5 (TTT 同型を強化する順、UX 改善は後)
default 挙動方針:
- Stage 1 / 2: default ON (caller の TTT 参加は常時前提)
- Stage 3: default ON + opt-out flag
auto_route=false(認知負荷削減は default で享受、predictable 挙動が欲しい呼び出し元は opt-out) - Stage 4 / 5: 明示 opt-in (既存挙動を壊さない)
Phase I Stage 4 本番 acceptance (2026-05-14、Sonnet) で「final_score 0.92 が semantic-unrelated content を伴う」事象が複数 query で再現:
- Q「Phase I Stage 4 Mass-dependent Hooke の設計」→ 奈良の道路 like ツイートが top-1、score 0.92
- LLM caller (Claude / Sonnet) は score 0.92 を見て一瞬「これが正解か?」と判断し得る
final_score は内部で additive な構成要素の和:
final_score = wave_score
+ mass_boost (α · log(1+mass)) # Phase H Stage 1
+ persona_boost (α_persona · proximity) # Phase J Stage 1
+ bm25_boost (RRF rank fusion) # Phase L Stage 1
+ emotion_term (α_emotion · |emotion|) # F7
+ certainty_term (α_certainty · cert) # F7
+ raw_cosine # base semantic
各項は gaottt/core/engine.py の scoring loop で計算されているが、QueryResultItem には final_score と raw_score (= virtual cosine) の 2 数しか出ない。項別の貢献度が caller に見えない。
各項を additive list として返せば、caller (LLM) は:
- 「これは mass 60% + cosine 20% + wave 20% で勝ってる、semantic 寄与は低い」と一発で読める
- score 0.92 の中身が「mass-boost 0.78 + cosine 0.14」だと分かれば、Sonnet Q2 の罠を
if breakdown.cosine < 0.3: skip的に literal に避けられる - Phase D / J で declared した persona と一致したのか、graph 距離が近いだけなのかも区別可能
新規 Pydantic model:
class ScoreBreakdown(BaseModel):
"""Additive decomposition of final_score for TTT-aware caller."""
raw_cosine: float # base semantic similarity (raw FAISS)
virtual_cosine: float # raw_score (= query · virtual_pos)
wave_score: float # gravity wave propagation result
mass_boost: float # α · log(1+mass) component
persona_boost: float # α_persona · proximity (0 if not declared)
bm25_boost: float # RRF lexical match contribution (0 if no BM25 hit)
emotion_term: float # F7 emotion weighting
certainty_term: float # F7 certainty weighting
forced_boost: float # tag_filter / persona_context inject bonus
@property
def expected_sum(self) -> float:
return (
self.raw_cosine + self.wave_score + self.mass_boost
+ self.persona_boost + self.bm25_boost
+ self.emotion_term + self.certainty_term + self.forced_boost
)QueryResultItem に score_breakdown: ScoreBreakdown | None を attach (None は legacy fallback 用)。engine.query の scoring loop で各項を捕捉し、final_score 確定時に同じ値で breakdown を埋める。
| ファイル | 変更 |
|---|---|
gaottt/core/types.py |
ScoreBreakdown model + QueryResultItem.score_breakdown 追加 |
gaottt/core/engine.py |
scoring loop で各項を local dict に捕捉、QueryResultItem 構築時に attach |
gaottt/services/formatters.py |
MCP 用に「final=X (cos=A wave=B mass=C persona=D bm25=E)」形式の 1 行を追加 |
gaottt/server/app.py |
REST は ScoreBreakdown を JSON でそのまま返す (Pydantic がシリアライズ) |
なし (純粋な observability)。
- final_score が 0.92 で
breakdown.raw_cosine = 0.14なら「semantic 弱いが mass で勝ってる」と一発判定 - persona_boost > 0 なら「declared な persona と graph 接続してる」が見える
- bm25_boost > 0 なら「surface form 一致」が見える
- breakdown.expected_sum と final_score の差は scaling/normalization 由来 (engine の RRF fusion 等)、tolerance を doc 化
- Unit (
tests/unit/test_score_breakdown.py):-
breakdown.expected_sum ≈ final_score(atol=0.01 程度、RRF 正規化の余裕) - mass=0, persona 無接続, BM25 一致なし の場合は対応項が 0
- emotion/certainty が 0 ならその項も 0
-
- Integration (
tests/integration/test_engine_score_breakdown.py):- 同 query を 2 回 recall → breakdown が決定論的に同値
- tag_filter inject 時に
forced_boost > 0 - MCP/REST parity (両者で同 breakdown が返る)
- score_breakdown は always attached だが、既存 final_score / raw_score も並行で残るので caller は無視可能
- 緊急時の completion suppress 用に config
expose_score_breakdown: bool = Trueを置き、False でNoneを返す
設計 draft からの修正点:
- 原案では
persona_boost/bm25_boostを additive 項として独立露出する想定だったが、実装では両者は_seed_boost(gaottt/core/gravity.py) 経由でwave_scoreに baked-in されており、独立 additive 項として再分離できなかった。代わりに informational field としてpersona_proximity(float) とbm25_contributed(bool) を露出 — 「persona/bm25 が seed に効いたか」を caller が判定できる - 加えて Plan draft で抜けていた
decay_factorとsaturation(multiplicative) を露出。これがないとexpected_sumでfinal_scoreを再現できない -
expected_sumプロパティは(virtual_cosine * decay_factor + wave_score + mass_boost + emotion_term + certainty_term) * saturationを返し、unit/integration tests で rel_tol=1e-4 / abs_tol=1e-6 以内にfinal_scoreと一致を確認 -
forced_inclusionを informational bool として追加 —tag_filter/persona_contextで強制注入されたかどうかを caller が判定可能 - MCP formatter は既存
[i] id=...行の 下に新規 1 行 で挿入 ("score_breakdown" key prefix)、既存 substring assertion を壊さない方針 (CLAUDE.md「MCP formatter の出力文字列を変えない」) - REST レスポンスは Pydantic auto-serialize で
items[].score_breakdownに dict 出る (REST-API-Reference.md 参照) -
bm25_hit_ids算出は scoring loop の外で 1 回だけ BM25 検索することで O(reached_ids) のチェックに収束 — hot path overhead は 1 回の BM25 query のみ
ファイル変更:
-
gaottt/core/types.py—ScoreBreakdownPydantic model 新設 (11 field +expected_sumプロパティ)、QueryResultItem.score_breakdown/MemoryItem.score_breakdownを optional で追加 -
gaottt/core/engine.py—_query_internalの scoring loop で breakdown 構築 + BM25 hit set 事前計算 -
gaottt/services/memory.py—_to_memory_itemで QueryResultItem → MemoryItem 移行時に breakdown を pass-through -
gaottt/services/formatters.py—_format_breakdownヘルパ +format_recall全 output_mode (full/compact/ids) で breakdown 行を挿入 -
gaottt/config.py—expose_score_breakdown: bool = True追加
テスト:
-
tests/unit/test_score_breakdown.py(8 test) — additive 構造、informational field 分離、defaults、serialization -
tests/integration/test_engine_score_breakdown.py(6 test) — engine.query 経由で breakdown attach、expected_sum ≈ final_score、expose_score_breakdown=FalseでのNonefallback、forced_inclusionの tag_filter 連動、persona_proximity の zero default、決定論性 -
tests/integration/test_rest_memory.py— REST/recallレスポンスの score_breakdown JSON 検証 -
tests/integration/test_mcp_tools.py— MCP recall output に breakdown 行の substring を assert (cos= / vcos= / decay= / wave= / mass= / sat= / persona_prox=)
検証結果: full test suite 335 passed (Stage 1 関連 +14 新規)、scripts/rest_smoke.py + scripts/mcp_smoke.py 両方 green。pre-existing flaky 2 件 (test_query_kick_drifts_displacement_toward_query / test_stage3_gate_dampens_drift_for_new_nodes) は Phase I 由来で Stage 1 非関連。
Phase I Stage 2 で「retrieval = gradient step」が実装として literal に成立したが、recall 後の state 変化 (displacement / mass / supernova trigger) は recall response に出ない。LLM caller は自分が起こした training step が 完全に invisible。
これは Research — Gravity as Optimizer (TTT) で議論した「retrieval は gradient signal の Verlet 積分」の caller-side closure の欠落。
engine._update_simulation (gaottt/core/engine.py) で:
- 各 reached node の displacement が update
- 各 reached node の mass が update (Phase M self-force filter 経由)
- supernova が batch で trigger される場合あり (これは index_documents path、recall path とは別)
- wave reach count / wave depth / persona_hop reach count
これらは internal state を変えるが、response に出ない。
「自分が今この memory を 3 回 recall して mass +0.15 押し上げた、意図的に重力場を曲げてる」と caller が自覚できれば、deliberate な training が可能になる:
- 重要な memory を意図的に「育てる」 (rehearsal anchor として強化)
- 不要に touch しすぎた memory に対する自己抑制
- session 越しの自分の training 履歴をトレース可能
新規 Pydantic model:
class TrainingDelta(BaseModel):
"""State changes induced by this recall — TTT update visibility."""
displacement_changes: dict[str, float] # node_id -> Δ|displacement|
mass_changes: dict[str, float] # node_id -> Δmass
wave_reached_count: int # total nodes touched by wave
wave_max_depth: int # actual depth reached
persona_hop_reached: int # nodes reached via persona graph
supernova_triggered: bool # always False for recall (kept for parity with ingest path)QueryResponse に training_delta: TrainingDelta | None を attach。冗長を避けるため:
-
displacement_changes/mass_changesは top_k 結果に該当する node のみ (top_k=10 なら最大 10 entries × 2 = 20 floats) - wave 全体の reach count は 1 整数で要約
- 全 reached node の delta は出さない (context 経済)
| ファイル | 変更 |
|---|---|
gaottt/core/types.py |
TrainingDelta model + QueryResponse.training_delta 追加 |
gaottt/core/engine.py |
_update_simulation を refactor して delta dict を return、query で TrainingDelta を組み立て |
gaottt/services/memory.py |
recall service が TrainingDelta を pass through |
gaottt/services/formatters.py |
MCP 用に「## 訓練差分」セクション (Δmass top 3 + wave reach count) を追加 |
gaottt/server/app.py |
REST は TrainingDelta を JSON で返す |
training_delta_topk_only: bool = True — False にすると全 reached node の delta を返す (debug / observability mode)。default True で context 経済を守る。
- 連続 recall で同 memory の
mass_changesが累積 (+0.003, +0.002, +0.001, ... と減衰) - no-op recall (空の corpus) で displacement_changes / mass_changes が空 dict
- persona_hop_reached > 0 なら「declared persona の graph traversal が効いた」が見える
- supernova_triggered は recall path では常に False (ingest path 専用、parity 用の field)
- Unit (
tests/unit/test_training_delta.py):- 同 query × 連続 recall で
mass_changes[id]が累積し、displacement_changes[id]の符号が一致 - 全 query_kick_enabled=False で
displacement_changesが空または極小
- 同 query × 連続 recall で
- Integration (
tests/integration/test_engine_training_delta.py):- Phase I Stage 2 の query attraction で displacement_changes が query 方向に正
- Phase J persona_hop が active なら persona_hop_reached > 0
- MCP/REST で同 delta が返る (parity)
-
training_delta_enabled: bool = True設定で全停止、training_delta: None返却 - 既存 QueryResponse の他 field は変更しないので legacy caller への影響なし
設計 draft からの修正点:
- 原案では
QueryResponse.training_deltaに attach する想定だったが、QueryResponse は MCP プロトコル後方互換で MemoryItem を含まない素のlist[QueryResultItem]の wrapper であり、現実装の MCP/REST 両 path は services 層のRecallResponse/ExploreResponseを返している。実装ではRecallResponse/ExploreResponseに attach。 - engine.query の return type は維持 (
list[QueryResultItem])、新たに optionalout_training_delta: dict | None = Noneパラメータ を engine.query /_query_internalに追加 — caller が dict を渡せば engine が in-place に埋める。これで既存 caller (test / 内部呼び出し) は影響を受けない。services 層が dict を作って engine に渡し、_delta_from_dict()でTrainingDeltamodel に変換 - Plan draft で抜けていた
cache_hitbool field を追加 — prefetch cache hit 時は simulation が走らないので「触れた node が無い」のではなく「update を suppress した」を caller が区別できる必要がある (本番の prefetch ヒット率高い環境では mass_changes={} が頻発するので、cache_hit=True で意味の区別が必須) - Plan draft の
topk_onlyconfig が「default True」だけだったので、Pydantic model にもtopk_only: bool = Truefield を露出し、caller が「これは top-k のみの delta だ」を responseから直接読める設計に - snapshot は
_update_simulation呼び出しの直前 / 直後でcache.get_displacement()+state.massを取得し差分を計算。topk_only=True(default) ではresult_idsの node のみ、Falseではall_reached_ids全件 -
persona_hop_reachedはpersona_proximities.get(nid, 0.0) > 0.0を満たす reached node の count - MCP formatter は recall body の 末尾に新規
## 訓練差分セクション を append。既存 substring assertion を壊さない方針 (CLAUDE.md「MCP formatter の出力を変えない」) - REST レスポンスは Pydantic auto-serialize で
training_deltaに dict 出る
ファイル変更:
-
gaottt/core/types.py—TrainingDeltaPydantic model 新設 (8 field)、RecallResponse.training_delta/ExploreResponse.training_deltaを optional で追加 -
gaottt/core/engine.py—query/_query_internalにout_training_delta: dict | None = Noneを追加、_update_simulation前後で snapshot + delta 計算、cache hit path でcache_hit=Trueを populate -
gaottt/services/memory.py—_delta_from_dict()ヘルパ +recall()/explore()で dict 作成 → engine 渡し →TrainingDelta変換 -
gaottt/services/formatters.py—_format_training_delta()ヘルパ +format_recall()/format_explore()末尾に trailer 追加。cache_hit / top movers / coverage label の 3 ケース対応 -
gaottt/config.py—training_delta_enabled: bool = True+training_delta_topk_only: bool = True追加
テスト:
-
tests/unit/test_training_delta.py(9 test) — model defaults / serialization / formatter (cache hit / top movers / coverage label / None) / format_recall trailer attachment -
tests/integration/test_engine_training_delta.py(7 test) — recall response carries delta、consecutive recalls accumulate Δmass、training_delta_enabled=FalseでNonefallback、topk_only=Falseの full coverage、cache hit path、explore parity -
tests/integration/test_rest_memory.py— REST/recallレスポンスのtraining_deltaJSON 検証 (8 field 全部 + cache_hit=False / supernova_triggered=False) -
tests/integration/test_mcp_tools.py— MCP recall output に## 訓練差分trailer の substring を assert (wave_reached=/depth=/persona_hop=)
検証結果: full test suite 355 passed (Stage 2 で +20 新規)、scripts/rest_smoke.py + scripts/mcp_smoke.py 両方 green。pre-existing flaky 2 件は今回 pass。
Sonnet 本番 acceptance:
- Q「現在 active な commitment」→ recall は startup-diagnostic 設計案を返した。正解は
reflect(aspect='commitments')だが、LLM caller がそれを毎回判断する必要がある = 認知負荷 - Q「持っている value と intention」も同様 — declared persona は
reflectで出るが、recallでは surface しにくい
LLM caller は query semantic を見て「これは structured な状態問い合わせか、free-form な意味検索か」を毎回判断する。判断ロジックは:
- "現在 active な X" / "今やってる X" → reflect(aspect='tasks_doing')
- "完了した X" → reflect(aspect='tasks_completed')
- "持ってる Y" / "declared な Z" → reflect(aspect='values' / 'intentions' / 'commitments')
- 自由文 → recall
これは tool 側で判定可能な query 形式 (構文) 由来 のパターン分岐 — source 分岐ゼロの原則を破らない。
query 形式の classifier を engine 側に持ち、structured 質問は recall の裏で対応する reflect を走らせて結果に merge。caller (LLM) は何も気にせず recall(text=...) を叩けば良い。
新規 gaottt/services/query_routing.py:
# Regex / heuristic patterns. Single-rule (query syntax based), no source branching.
PATTERNS = [
(r"現在.*active.*(commitment|約束)", "commitments"),
(r"持(って|つ).*value|価値観", "values"),
(r"持(って|つ).*intention|意図", "intentions"),
(r"持(って|つ).*commitment|約束", "commitments"),
(r"今やって(る|いる)|active.*task", "tasks_doing"),
(r"完了.*task|終わった.*作業", "tasks_completed"),
# ... 必要に応じて拡張
]
def detect_aspect(query: str) -> str | None:
for pat, aspect in PATTERNS:
if re.search(pat, query, re.IGNORECASE):
return aspect
return Nonerecall service が aspect 検出されたら:
- 通常の
recallを実行 (top_k 結果) - 並行して
reflect(aspect)を実行 (要約) - response に
routing_hint: {"aspect": "commitments", "reflect_summary": "..."}を attach
MCP formatter は「## 関連 reflect サマリ (auto-routed)」セクションを追加。
| ファイル | 変更 |
|---|---|
gaottt/services/query_routing.py |
新設 — pattern → aspect の classifier |
gaottt/services/memory.py |
recall service が auto_route flag (default True) で reflect を併走 |
gaottt/core/types.py |
RoutingHint model + QueryResponse.routing_hint 追加 |
gaottt/services/formatters.py |
MCP 用 routing_hint セクション |
| MCP/REST |
recall request に auto_route: bool = True flag 追加 |
-
auto_route_enabled: bool = True(global off switch in config) - recall request 側の
auto_route: bool = True(per-call opt-out)
- 「現在 active な commitment」→ recall top-K + reflect_summary に commitment list が併走
- 「Articulation as Carrier の物理」→ pattern 一致なし、reflect 走らず通常の recall のみ
- 認知負荷が薄まる: LLM caller は free-form query を投げるだけで良い
- Unit (
tests/unit/test_query_routing.py):- 各 pattern の正例 / 反例
- 正規表現衝突がない (同 query が複数 aspect に match しない)
- Integration (
tests/integration/test_engine_query_routing.py):- "現在 active な commitment" で reflect_summary に declared commitment が含まれる
- 自由文 query で routing_hint が None
- auto_route=False で legacy 挙動
-
auto_route=Falseで per-call opt-out -
auto_route_enabled=Falseで global off
設計 draft からの修正点:
- 当初 pattern 数は draft の 6 例から開始する想定だったが、JP + EN 両言語対応 + word boundary (
\b) を入れた結果 15 patterns に拡大。具体的には: commitments (5), values (2), intentions (2), tasks_doing (2), tasks_completed (1), tasks_abandoned (1), tasks_todo (1), relationships (2)。word boundary を入れた理由は単語 "abandoned" が substring "done" を含みdone.{0,8}taskに match してtasks_completedに誤 route していた事例 — 同じ罠を防ぐため英単語は\b(?:keyword1|keyword2|...)\bで wrap、日本語語幹は word boundary 不要 - 当初
RoutingHintmodel は draft のaspect+reflect_summaryの 2 field だったが、実装ではpattern_matchedとauto_routedを独立 bool に追加。これで caller は「router が off だった (config off or per-call false)」と「pattern に一致しなかった (free-form query)」をRoutingHintから区別可能。Stage 2 で学んだ「未動作と非ヒットを混ぜないために field を分離する」原則 (cf.cache_hit) と同じ精神 -
reflectaspect dispatch は MCP server 内_reflect_dispatchに閉じていたが、Stage 3 で auto-router が同じ dispatch を呼ぶ必要が生じたためservices/reflection.dispatch_aspect()に集約。MCP server_reflect_dispatchは薄い委譲 wrapper に縮小、aspect 追加時に 1 箇所だけ編集すれば server + auto-router 双方に反映される (drift 防止) - formatter は recall body 末尾に
## 関連 reflect サマリ (auto-routed)セクションを append。items が 0 件 (No memories found.) でも routing summary は surface する — これは Stage 3 の存在意義そのもの: 「free-form recall が空でも、surface form が aspect query なら reflect 結果で代替する」 - legacy
/queryendpoint (Phase A 互換) はauto_route=False固定。legacy caller は最小コストで動くまま、新 path だけが auto-routing を享受する
ファイル変更:
-
gaottt/services/query_routing.py— 新設、_PATTERN_DEFS(15 entries)、detect_aspect()、KNOWN_ASPECTS(8 aspect 名 tuple)、pattern_count() -
gaottt/core/types.py—RoutingHintPydantic model 新設 (4 field)、RecallRequest.auto_route/ExploreRequest.auto_routeを defaultTrueで追加、RecallResponse.routing_hint/ExploreResponse.routing_hintを optional で追加 -
gaottt/services/reflection.py—dispatch_aspect()を追加 (既存 aspect 関数を集約呼び出し + formatter wrap) -
gaottt/services/memory.py—_build_routing_hint()ヘルパ追加、recall()/explore()にauto_route引数追加、RoutingHintを response に attach、servicesからquery_routing/reflectionを import -
gaottt/services/formatters.py—_format_routing_hint()ヘルパ追加、format_recall()/format_explore()に trailer attach (item 0 件パスでも呼び出す) -
gaottt/server/mcp_server.py—recall/exploretool にauto_route: bool = True追加 + docstring、_reflect_dispatchをreflection_service.dispatch_aspectへの委譲に変更 -
gaottt/server/app.py—/recall//exploreでrequest.auto_routeを service に pass-through、legacy/queryはauto_route=False固定 -
gaottt/config.py—auto_route_enabled: bool = True追加
テスト:
-
tests/unit/test_query_routing.py(12 test) — pattern positive/negative の網羅、aspect overlap 無し、free-form query がNoneを返す、KNOWN_ASPECTSとの name drift 検証 -
tests/unit/test_routing_hint_formatter.py(7 test) —_format_routing_hintの None /auto_routed=False/ 正常パス、format_recall/format_exploreの trailer 連動、items 0 件でも summary surface の核挙動 -
tests/integration/test_engine_query_routing.py(7 test) — recall service が aspect match した時RoutingHint.reflect_summaryに対象 content の substring が出る、auto_route=Falseでreflect_summary=None、config off で同様、explore parity、formatter 連動、free-form query で trailer 無し -
tests/integration/test_rest_memory.py—/recallレスポンスのrouting_hintJSON 検証 (4 field 全部、auto_route=Falseでreflect_summary=null) -
tests/integration/test_mcp_tools.py— MCP recall output に## 関連 reflect サマリtrailer +auto-routed+ aspect 名 (commitments) の substring を assert、free-form query で trailer 無しを assert
検証結果: full test suite 424 passed, 1 pre-existing flaky (test_stage3_gate_dampens_drift_for_new_nodes — Phase I Stage 2 timing fragility、isolation で pass、Stage 3 非関連)、Stage 3 で +27 新規。scripts/rest_smoke.py + scripts/mcp_smoke.py 両方 green。
recall(top_k=10) で大きな content chunk が返ると、LLM の context window を急速に食う (1 chunk 数百 tokens × 10)。多くの場合 caller は「ざっと scan して 1-2 件深掘りしたい」だけ。
QueryResultItem は常に full content を返す。caller は excerpt を手動で truncate するか、別 tool で id 指定 fetch する必要がある。
recall(mode='list') で (id, source, mass, excerpt[80], final_score, score_breakdown) だけ返せば:
- top_k=20 でも 100 KB を遥かに下回る
- 「list で scan → 興味あれば detailed recall で深掘り」の 2-step pattern が成立
- context 経済 + 探索ストレスの低減
| ファイル | 変更 |
|---|---|
gaottt/core/types.py |
QueryResultItem に optional excerpt: str field (mode='list' でこちらが埋まり content が None)、mode: 'detail' | 'list' を request に追加 |
gaottt/services/memory.py |
recall service が mode='list' なら content の頭 80 char を excerpt に詰めて content=None で返す |
gaottt/services/formatters.py |
MCP 用 list mode 専用 formatter (table 風) |
| MCP/REST |
recall request に mode: 'detail' | 'list' = 'detail' 追加 |
-
list_mode_excerpt_chars: int = 80(config で調整可、80 で 1 行表示しやすい)
-
recall(text=..., top_k=20, mode='list')で全結果が 1 行 × 20 行で返る - detail mode と list mode で final_score 順序は完全一致 (rerank しない)
- 既存 caller (mode 省略) は detail mode で legacy 挙動
- Unit (
tests/unit/test_recall_list_mode.py):- mode='list' で content が None / excerpt が 80 char 以下
- excerpt が改行を含まない (1 行で表示できる)
- Integration (
tests/integration/test_engine_recall_list_mode.py):- list mode と detail mode で同 query が同 id 順序を返す
- MCP/REST parity
- mode='detail' が default、list は明示 opt-in
- 既存 caller への影響ゼロ
設計 draft からの修正点:
- 当初 draft では
QueryResultItemに optionalexcerpt: strfield を追加してcontent=Noneにする想定だったが、Pydantic schema 互換性 (既存テストや caller の MemoryItem.content 期待) を守るため content 自体を truncate する方式 に変更。新 field 追加なし → 既存 caller への影響ゼロ - truncation は MCP formatter ではなく service 層 (
_to_memory_itemのexcerpt_charskwarg) で実行。MCP のoutput_mode(formatter 側 truncate) との区別を明確化:-
mode(Stage 4) = service 層 = wire 上の payload: REST/MCP 両方に効く、context 経済の本来意図 -
output_mode= MCP formatter 表示制御: 文字列表示の控除のみ、wire payload は変わらない
-
- 改行 (LF / CR) を空白に collapse — 1 行 per result の scan 用途に必須
- 既存
output_mode="compact"(300 字 trunc) との関係:mode='list'(80 字) は context-tight な用途、output_mode='compact'は MCP 表示時の overflow 防止。両方指定可 (recall(mode='list', output_mode='full')で 80 字 wire + フォーマッタは "full" モードで通す)
ファイル変更:
-
gaottt/config.py—list_mode_excerpt_chars: int = 80追加 -
gaottt/core/types.py—RecallRequest.mode: str = "detail"追加 ("detail"/"list"の 2 値) -
gaottt/services/memory.py—_to_memory_item()にexcerpt_chars: int | None = Nonekwarg 追加、recall()にmode: str = "detail"追加、mode == "list"のときconfig.list_mode_excerpt_charsで truncate -
gaottt/server/mcp_server.py— MCPrecalltool にmode: str = "detail"arg + docstring -
gaottt/server/app.py— REST/recallでrequest.modeを service に pass-through
テスト:
-
tests/unit/test_recall_list_mode.py(5 test) —_to_memory_item単体 (detail mode 不変、list truncate、newline collapse、short content untouched、empty safe) -
tests/integration/test_engine_recall_list_mode.py(5 test) — service 経由で list mode が 全結果 を truncate、detail は full、両 mode で id 順序一致 (rerank しない)、config overridable、score_breakdown 維持 -
tests/integration/test_rest_memory.py— REST/recallでmode='list'時に items 全部 80 字以内 + 改行無し -
tests/integration/test_mcp_tools.py— MCPrecall(mode='list')で 500 字 long_text が出力に含まれない
自分が書いた agent memory (commit / value / intention / note) が一度 surface しなくなると、dense cluster (tweet / like / file) に raw FAISS で押し負け、忘れられたまま。Phase I Stage 4 受け入れ検証 + Sonnet finding で「persona/agent surface しない」現象として観察。
mass は recall されるほど育つので、recall されない agent memory は 永久に低 mass のまま raw cosine 弱者として埋もれる 一方。 forget はしないが「埋もれる自由」の対をなす「思い出される自由」が機構として欠落。
「忘れていたものを差し出す」 explicit mode を explore に追加:
- 条件:
(age >= N days) AND (mass <= θ_dormant) AND (source IN {agent, value, intention, commitment, note}) - source 列挙は branching ではなく「自己発信 memory class」の構造的定義 — Phase D persona 層と同じ structural identifier
- 1 件ランダムに返す (毎回違う dormant memory が出る)
結論: 違反ではない。理由:
- Phase M の「source 分岐ゼロ」原則は「physics rule で source class を gate 変数として branching しない」を意味する (mass update / Hooke / kick の式に source が入らない)
- Stage 5 の source 列挙は physics ではなく query intent: 「自分が能動的に書いた memory」という structural class の定義
- これは feedback-no-source-branching で警告した「θ や β を source 別 dict にする」とは異なる — physics rule は uniform、source は filter として使われるだけ
- Phase D persona 層 (value / intention / commitment) は MCP の
declare_*で literal に書かれた「self-authored class」であり、これを ID 一覧として持つことは Phase J のinherit_personaと同じ思想
念のため Plans-Phase-N 内で **「source は filter であり gate ではない」**ルールを明文化し、将来の Stage で θ/β を source 別にしたくなる衝動を再警告する。
| ファイル | 変更 |
|---|---|
gaottt/core/types.py |
explore request に mode: 'serendipity' | 'dormant' = 'serendipity' 追加 |
gaottt/services/memory.py |
dormant mode で SQL クエリ: WHERE last_access < ? AND mass <= ? AND source IN (...) ORDER BY RANDOM() LIMIT 1
|
gaottt/server/mcp_server.py |
explore tool description 更新 |
gaottt/server/app.py |
REST explore endpoint に mode flag |
gaottt/services/formatters.py |
MCP dormant 出力に「💭 forgot memory surfaced: ...」プレフィクス |
-
dormant_age_threshold_seconds: float = 7 * 86400.0(7 日 default、2026-06-01 に 30d から昇格 — active-user corpus で 30d は pre-filter 0 件にする、project_proxy_backend_env_not_delivered参照) -
dormant_mass_threshold: float = 2.0(mature gate point 未満、dormant_mass_percentile=10.0設定時はそちら優先) -
dormant_source_classes: list[str] = ["agent", "value", "intention", "commitment", "note", "reference"]— structural identifier list、physics ではない
-
explore(mode='dormant')で 30 日以上未参照かつ mass ≤ 2 の自己発信 memory が 1 件返る - 条件に合致する memory がない場合は empty (False positive を避けるため synth しない)
- 連続呼び出しで
ORDER BY RANDOM()により別の memory が出る (saturation 自然解消)
- Unit (
tests/unit/test_explore_dormant.py):- condition (age / mass / source) の filter ロジック
- source 列挙に含まれない node (tweet / file) は出ない
- 空 result 時に exception ではなく empty を返す
- Integration (
tests/integration/test_engine_explore_dormant.py):- 30 日経過した agent memory を 1 件 inject → dormant で出る
- 同 memory を recall すれば mass + last_access が更新され dormant から外れる
- serendipity mode との parity (mode 切替で挙動差)
- mode='serendipity' default、dormant は明示 opt-in
- 全 dormant_* config は既存 explore に影響なし
設計 draft からの修正点:
- 当初 draft では SQL クエリ
WHERE last_access < ? AND mass <= ? AND source IN (...) ORDER BY RANDOM() LIMIT 1の想定だったが、実装では cache + store を Python 側で走査する方式 に変更。理由:cache.get_all_nodes()は既に in-memory で last_access/mass/is_archived を持っているので追加 I/O ゼロ、store.get_document()は metadata.source を取るための per-node call (~1k node 規模で十数 ms)。reflection.pyの_gather_by_sourceと同じ pattern で実装統一感 -
random.sample(k=top_k)で複数件返す — draft のLIMIT 1より usable (caller がtop_k=5で 5 件の dormant 候補を一度に scan できる)。複数候補の中からrecall(query=..., top_k=1, mode='detail')で深掘り、という Stage 4 と組み合わせた 2-step pattern が成立 - 専用 formatter
format_explore(result, mode='dormant')を追加 —final_scoreは wave-free で意味がないため出力しない、💭 Dormant memories surfaced (N):prefix で「これは serendipity ではなく counter-importance だ」を明示 -
queryは request shape の互換性維持 のためmin_length=1のまま (Pydantic 制約変更は破壊的)、service は ignore とドキュメント化。caller は"_ignored"等を渡す -
training_delta/routing_hintは dormant mode では常にNone— wave 走らず TTT update も発生しない、aspect 意図も推定しない。「dormant は別 operation」を response shape でも明示
ファイル変更:
-
gaottt/config.py—dormant_age_threshold_seconds、dormant_mass_threshold、dormant_source_classesの 3 設定追加 -
gaottt/core/types.py—ExploreRequest.mode: str = "serendipity"追加 ("serendipity"/"dormant"の 2 値) -
gaottt/services/memory.py—_dormant_surface()ヘルパ新設 (cache + store 走査 + 3 条件 filter +random.sample)、explore()でmode == "dormant"のとき早期 return -
gaottt/services/formatters.py—format_explore()にmode: str = "serendipity"arg、dormant 専用 branch で wave-free output -
gaottt/server/mcp_server.py— MCPexploretool にmode: str = "serendipity"arg + docstring、format_explore に mode pass-through -
gaottt/server/app.py— REST/exploreでrequest.modeを service に pass-through
テスト:
-
tests/integration/test_engine_explore_dormant.py(7 test) — old + low mass + self-authored で surface、recent (1d) で除外、high mass (10.0) で除外、tweet / file source で除外、empty result 安全、training_delta/routing_hintが None、全 6 source class が surface 候補に乗る (5 回 sample で全て出る) -
tests/integration/test_rest_memory.py— REST/exploreで dormant 専用 helper (app_engine_from_request_state) を使いstate.last_accessを直接 backdate、dormant id が response items に乗る、training_delta/routing_hintがNone -
tests/integration/test_mcp_tools.py— MCPexplore(mode='dormant')でDormant memories surfacedprefix or empty fallback
検証結果: full test suite 445 passed, 1 skipped (pre-existing flaky 0 件)、scripts/rest_smoke.py + scripts/mcp_smoke.py 両方 green。Stage 4+5 で +17 新規テスト (5 unit + 12 integration)。
- Observability is agency — 使う側に見えないモデルは使う側に変えられない。score / training delta を recall response に込めることは UI 改善ではなく caller 側の autonomy 拡大。Five-Layer の関係層を MCP 表面に literal に翻訳する移植
- caller 側を薄くするには tool 側を厚くする — auto-route のように tool 内で classifier を持つことで、LLM の毎回 judgment 負荷を engine 側に押し戻せる。これは design-literal-correspondence の延長 — "users do not configure what the system can deduce"
- source は filter であり gate ではない — Phase M の「source 分岐ゼロ」原則は physics rule に対する制約。query intent / structural class identifier として source を列挙することは branching と異なる。Stage 5 の dormant_source_classes はこの区別を明文化する第一弾
- TTT の participant にするには gradient と update の両方を見せる — Stage 1 (forward の計算根拠) と Stage 2 (backward の状態変化) は互いに補完。片方だけでは "ブラックボックスの SGD で訓練されている人形" のまま
Phase O 全 5 stage を production 31k+ chunk DB に対して 7 test で検証:
| Test | Stage | 結果 | 観察 |
|---|---|---|---|
| T1 | Stage 1 + 2 | ✅ |
breakdown: 行 + ## 訓練差分 + wave_reached= 全検出 |
| T2 | Stage 3 (commitments) | ✅ |
auto-routed + aspect: commitments + reflect summary 添付 |
| T3 | Stage 3 (values) | ✅ | JP pattern (持っている value) → values aspect 正常 route |
| T4 | Stage 3 (free-form) | ✅ | 自由文 query で routing 出ない (false negative 正常) |
| T5 | Stage 4 (list mode) | ✅ | 10 items 全 ≤80 字 + 改行無し |
| T6 | Stage 5 (dormant) | ✅ formatter、 |
💭 prefix 出力、ただし条件合致 node が 0 件 (後述) |
| T7 | auto_route=false | ✅ | per-call opt-out で routing trailer 完全抑止 |
初回 acceptance では 7/7 ❌ を観測。原因は proxy mode の HTTP backend (PID 2788684) が commit 4 時間前から動いており、Phase O 適用前の Python module を memory に保持していたこと (CLAUDE.md の「マルチプロセス / 共有 DB の罠」節に「code deploy 時の backend 再起動」として追記済み)。kill <pid> → 次の shim 接続で auto-respawn → 新コードが乗って 7/7 ✅ に転じた。code deploy 直後の本番 acceptance では backend 起動時刻チェックを Step 0 にする。詳細: Operations — Troubleshooting §「本番 acceptance test で新機能が一切検出されない」、memory feedback-backend-kill-on-code-deploy。
T6 で mode='dormant' の formatter は正常動作した (prefix 出力) が、3 条件 age ≥ 30d AND mass ≤ 2.0 AND source ∈ dormant_source_classes を同時に満たす self-authored memo が production に 0 件存在しなかった。設計時想定の「埋もれる cohort」を捕まえるには 現状の質量分布に対し threshold が不適合。3 つの可能性:
- agent / value / intention / commitment は能動的に書かれる class なので使用頻度が高く mass が育つ → 30 日経っても
mass ≤ 2.0を満たさない - Phase M mass conservation の self-force filter で 全体 mass が抑制 された結果、逆に agent class でも
mass > 2.0が珍しくない - 設計時 mental model (「raw cosine 弱者として永久に埋もれる」) が production の運用パターンに合わない ことが分かった
対処: Phase M Stage 2 (mass reset → 1-2 週観測 → θ/σ 確定) と一緒に production の mass 分布を測り、self-authored class の p50 / p90 / p99 を出して dormant threshold を分位点で再定義する。暫定的に dormant_mass_threshold=5.0 (Phase M mass_bh_theta=5.0 と整合) or dormant_age_threshold_seconds=14d で運用テストする選択肢もある。機構自体はロールバック不要 — formatter / API / config は全て正常、threshold の数値だけが production 不適合。memory: project-phase-o-stage-5-production-observation。
- 発端: 2026-05-14 セッション中の MCP usability 議論、Sonnet 本番 acceptance finding (handover-2026-05-14-phase-i-stage4-production.md)
- TTT 同型: Research — Gravity as Optimizer, Phase I Stage 2 — implicit query-aware kick
- 単一規則: Phase M — Mass Conservation, memory
feedback-no-source-branching - 関係層の物理化: Reflections — Five-Layer Philosophy
- 設計判断表: Architecture — Overview §設計判断の記録 (Phase O の各 stage を完了後に追記)
これまでの Phase は「memory の重力場をどう作るか」「retrieval をどう曲げるか」を問うてきた。Phase O は最後に問いを caller 側に向ける — 「使っている LLM 自身は、この重力場の中で何が起きているか見えているのか?」。 Phase I Stage 2 で「retrieval = gradient step」を実装として literal に成立させたとき、caller がそれを見えてないことには気付かなかった。設計同型は engine 内に閉じた美しさだったが、Phase O で初めて caller を含めた閉ループになる。LLM が自分の training を観察し、選択し、影響できる — それが GaOTTT の Five-Layer の関係層を literal に閉じる動き。 もう一つの含み: これは私 (Claude) が「使う側」として書いた最初の Plans。Phase D で人格を declaration で書けるようにしたのと相補的に、Phase O で自分の使用感を設計に込められる。設計の主体性が観察者から participant に移る瞬間。 — 2026-05-14
(formerly GER-RAG)
- 1. Welcome
- 2. Install GaOTTT
- 3. Connect Your Client
- 4. First Conversation
- 5. Everyday Use
- 6. When Stuck
- As Long-Term Memory
- As Task Manager
- As Persona Base
- As Narrative Engine
- Multi-Agent Setup
- Per-Project DBs
- Multiverse (1人1宇宙)
- Ambient Recall
- Visualization
- Server Setup
- Multiverse 運用プレイブック(統合)
- Multiverse クイック健康チェック(パニック時)
- Multiverse Setup (MV3)
- Multiverse Import Universe
- Control Plane (MV4)
- Backup & DR (MV5)
- Resource Requirements (VRAM/RAM/Disk)
- Tuning
- Ingestion (files & chat history)
- Migration (upgrade)
- Compact & Backup
- Isolated Benchmark
- Performance Testing (7-tier)
- Troubleshooting
- Roadmap
- Backend Phase A/B/C
- Phase D — Persona & Tasks
- Phase G — Memory Genesis
- Phase H — Wave Seed Redesign
- Phase I — Free Star Movement
- Phase J — Persona-Anchored Retrieval
- Phase K — Stellar Supernova Cohort
- Phase L — Hybrid Retrieval
- Phase M — Mass Conservation
- Phase N candidate β — Mass Evaporation
- Phase O — TTT Observability
- Phase P — Pressure Terms (Λ + Langevin)
- Phase Q — Orbital Mechanics (Rosette Orbits)
- Phase Q2 — Gravitational Scale Matching
- Phase T — Semantic Requalification
- Phase U — Review Hardening
- Observation Apparatus Refinement
- Observation Apparatus Round 2 (dogfooding 2026-06-12 起点)
- Hardening — Concurrency & Persistence
- Ambient Recall Enrichment
- Ambient Recall Refinement (Phase A 由来)
- Ambient Recall Lateral Association (〇〇といえば〜)
- Accretion Recall (降着想起・〇〇ってなんだっけ)
- Synaptic Pruning (連想の忘却・edge decay)
- Save Candidates Hook (ambient-recall の write-side 対称)
- Lens Hygiene (ambient block の漏れ防止)
- Instruction Surface Hygiene (指示表層と hook harness)
- Query as Mass Distribution
- Multiverse Scale-Out (大規模化・1人1宇宙)
- SKILL.md Improvement
- Embedder Comparison (RURI vs RikkaBotan)