Memory v2拡張ガイド:連想走査・重要度重み付け・頻度ベース忘却 - Memory v2 Enhancement Guide: Associative Traversal, Salience Weighting, and Access-Based Forgetting
OpenClawのMemory v2をエンティティ共起トラバーサル、重要度重み付け保持、アクセスベース減衰で拡張し、長期実行エージェントデプロイメントでの検索精度を向上させるアーキテクチャガイド。
🔍 症状
現在のMemory v2検索の制限事項
長時間(日単位から週単位)で実行されるエージェントは、既存の検索メカニズムを使用すると文脈的整合性が低下します。以下の症状が本番環境に現れます:
症状1:表層的な字句検索
時間をおいて概念的に関連する情報をクエリすると、エージェントは表面的な一致のみを取得します:
$ openclaw memory recall "app performance improvements"
---
RETRIEVED FACTS (3):
- W(s=0.3) @config: Updated heartbeat interval from 5m to 30m.
- W(s=0.3) @config: Increased worker pool size to 4.
- W(s=0.3) @api: Added rate limiting middleware.
EXPECTED: Connection to Week 2 debugging session about slow database queries
ACTUAL: Generic config changes onlyエージェントは暗黙的なチェーンを横断できません: “performance” → “slow endpoint” → “database query” → “Sarah’s expertise.”
症状2:異なるメモリの均等な重み付け
保存されたすべてのファクトは、重要性に関係なくコンテキストバジェットを等しく競合します:
$ openclaw memory recall "any recent updates"
---
RETRIEVED (k=10, context budget: 4KB):
1. W(s=0.3) @config: Updated heartbeat interval from 5m to 30m.
2. W(s=0.3) @config: Increased worker pool size to 4.
3. W(s=0.3) @config: Set log level to INFO.
4. W(s=0.3) @config: Disabled telemetry opt-in.
5. B(s=0.3) @Sarah @project: Sarah announced she's leaving next month.
6. B(s=0.3) @user @identity: User prefers morning standups.
...
CRITICAL GAP: No salience differentiation. Sarah's departure competes equally with log level changes.症状3:減衰のない無制限なインデックスの成長
30日以上連続稼働後:
$ sqlite3 ~/.openclaw/memory.db "SELECT COUNT(*) FROM facts;"
487
$ sqlite3 ~/.openclaw/memory.db "SELECT COUNT(*) FROM facts WHERE last_accessed > datetime('now', '-7 days');"
12過去1週間にアクセスされたファクトはわずか2.5%ですが、すべての487個が検索スコアリングで競合しています。リフレクトジョブは優先順位付けのシグナルなしに増え続けるセットを処理する必要があります。
症状4:ハブノードの汚染(CLS-Mベンチマークからの引用)
多くのファクトに登場するエンティティは、検索激活を吸収します:
$ sqlite3 ~/.openclaw/memory.db "SELECT entity, COUNT(*) as cnt FROM fact_entities GROUP BY entity ORDER BY cnt DESC LIMIT 5;"
entity|cnt
@Peter|203
@heartbeat|57
@api|89
@config|112
@system|78@Peter(203のファクト)を介した直接のエンティティ横断は、特定の関連のある接続のシグナルを希釈します。
🧠 原因
現在のMemory v2設計におけるアーキテクチャ上のギャップ
現在の検索システムには、長時間稼働のデプロイメントで精度を維持するために不可欠な3つの重要なメカニズムが欠けています:
ギャップ1:シングルホップのエンティティ検索
既存のエンティティ認識検索モデルは、クエリアンティティで直接タグ付けされたファクトを返しますが、共存するエンティティを再帰的に横断しません:
-- Current query (single-hop)
SELECT f.content, f.salience
FROM facts f
JOIN fact_entities fe ON f.id = fe.fact_id
JOIN entities e ON fe.entity_id = e.id
WHERE e.name = 'performance';
-- Returns only: facts explicitly tagged @performance
-- Misses: facts about @database that co-occur with @performance across the corpusこれはエンティティルックアップ(「Xについて教えてください」)には建築上正しいですが、エージェントが暗黙的な接続を発見する探索的クエリには不十分です。
ギャップ2:保持時の重要度トラッキングの欠如
Lettaコントロールループの基本的な洞察は、経験を持つエージェントが何を保持するかを決定するということです。ただし、保持コールに重要度パラメータがない場合、この決定は二値的(保持/破棄)で、段階的ではありません:
-- Current (binary)
openclaw memory retain "Sarah is leaving the company next month"
-- Missing salience metadata that would distinguish:
-- A config file tweak (s=0.2)
-- A critical team change (s=0.95)重要度なしでは、リフレクトジョブはシグナルとノイズを区別できません—それは実際の重要性のpoorプロキシである更新頻度またはアクセス頻度で重要性をプロキシする必要があります。
ギャップ3:アクセスベースの減衰メカニズムがない
現在の設計では、すべての履歴ファクトがエンゲージメントパターンに関係なく同等に検索可能として扱われます:
-- No temporal or access-based scoring
SELECT content FROM facts
ORDER BY created_at DESC -- Only recency, not relevance
LIMIT 10;これは3つの連鎖する問題を生み出します:
- 精度の劣化:インデックスが増加するにつれて、関連ファクツと無関係なファクトの比率が減少します
- リフレクトジョブの非効率性:リフレクションプロセッサは優先順位付けなしで越来越大さなコアパスを評価する必要があります
- ハブノイズの増幅:高次数のエンティティ(100+のファクトに登場)は、減衰なしで横断を支配します
CLS-Mプロトタイプからの根本原因分析
CLS-Mプロトタイプ(132ノード、802エッジ)は、これらのギャップを経験的に検証しました:
- リコールは許容可能(65%)だったが精度は低かった(35%)—つまり、取得コンテンツの65%はノイズだった
- ハブノードが精度を破壊:
heartbeatノードは57のエッジを持ち、特定のノードに向かうべきだった激活を吸収した - 時間ベースの減衰が失敗:3ヶ月前の-factで毎週アクセスされるものは、依然として注目されるべき;年齢 aloneは関連性のシグナルではない
修正は個別のナレッジグラフを構築することではなく、既存のSQLiteインデックスを以下で拡張することです:
- IDF重み付けによるエンティティ共存トラッキング
- 保持操作における第一級パラメータとしての重要度
- 純粋な年齢ベースの減衰ではなく、取得時にリセットされるアクセスベースの減衰
🛠️ 解決手順
フェーズ1:SQLiteインデックス用のスキーマ拡張
既存のスキーマに重要度とアクセストラッキングのカラムを追加します:
-- Migration: add_salience_and_access_tracking.sql
-- 1. Add salience column (0.0 to 1.0, default 0.5)
ALTER TABLE facts ADD COLUMN salience REAL DEFAULT 0.5;
-- 2. Add access tracking columns
ALTER TABLE facts ADD COLUMN last_accessed_at DATETIME DEFAULT NULL;
ALTER TABLE facts ADD COLUMN access_count INTEGER DEFAULT 0;
-- 3. Create index for access-based queries
CREATE INDEX idx_facts_last_accessed ON facts(last_accessed_at);
CREATE INDEX idx_facts_salience ON facts(salience);
-- 4. Precompute entity frequencies for IDF weighting
CREATE TABLE entity_stats AS
SELECT
e.id,
e.name,
COUNT(fe.fact_id) as fact_count,
1.0 / LOG(COUNT(fe.fact_id) + 1) as idf_weight
FROM entities e
LEFT JOIN fact_entities fe ON e.id = fe.entity_id
GROUP BY e.id;
CREATE INDEX idx_entity_stats_fact_count ON entity_stats(fact_count);フェーズ2:エンティティ共存テーブル
既存のfactインデックスから共存マトリックスを構築します:
-- Migration: build_entity_cooccurrence.sql
-- 1. Create co-occurrence table
CREATE TABLE entity_cooccurrence (
entity_id_1 INTEGER NOT NULL,
entity_id_2 INTEGER NOT NULL,
cooccur_count INTEGER DEFAULT 1,
cooccur_weight REAL DEFAULT 0.0,
PRIMARY KEY (entity_id_1, entity_id_2),
FOREIGN KEY (entity_id_1) REFERENCES entities(id),
FOREIGN KEY (entity_id_2) REFERENCES entities(id)
);
-- 2. Populate from existing fact_entities (facts with 2+ entities)
INSERT INTO entity_cooccurrence (entity_id_1, entity_id_2, cooccur_count)
SELECT
fe1.entity_id,
fe2.entity_id,
COUNT(DISTINCT fe1.fact_id)
FROM fact_entities fe1
JOIN fact_entities fe2 ON fe1.fact_id = fe2.fact_id
WHERE fe1.entity_id < fe2.entity_id -- Avoid duplicates
GROUP BY fe1.entity_id, fe2.entity_id;
-- 3. Compute weighted co-occurrence using IDF
UPDATE entity_cooccurrence SET cooccur_weight = (
SELECT
CAST(cooccur_count AS REAL) *
(SELECT idf_weight FROM entity_stats WHERE idf_weight = entity_id_1) *
(SELECT idf_weight FROM entity_stats WHERE entity_stats.id = entity_id_2)
WHERE entity_cooccurrence.entity_id_1 = entity_id_1
AND entity_cooccurrence.entity_id_2 = entity_id_2
);
-- 4. Create index for fast co-occurrence lookups
CREATE INDEX idx_cooccur_lookup ON entity_cooccurrence(entity_id_1, cooccur_weight DESC);フェーズ3:CLIコマンドの更新
保持コマンドを重要度パラメータで拡張します:
# Before
openclaw memory retain "Sarah is leaving the company next month"
# After (with salience)
openclaw memory retain "Sarah is leaving the company next month" \
--type B \
--entity Sarah \
--entity project \
--salience 0.95検索コマンドを重要度フィルタと連想横断で拡張します:
# Before
openclaw memory recall "performance improvements"
# After (with enhanced options)
openclaw memory recall "performance improvements" \
--k 10 \
--min-salience 0.3 \
--associative-depth 2 \
--activation-decay 0.5フェーズ4:連想横断アルゴリズム
深度制限と活性化減衰を伴う横断を実装します:
def associative_traverse(seed_entities: list[str], depth: int = 2, decay: float = 0.5) -> dict:
"""
Traverse entity co-occurrence graph with depth limiting and activation decay.
Returns:
dict: {entity_name: accumulated_activation_score}
"""
activation = {}
visited = set()
# Initialize seed entities with full activation
for entity_name in seed_entities:
activation[entity_name] = 1.0
visited.add(entity_name)
current_entities = seed_entities
current_activation = 1.0
for hop in range(depth):
next_entities = []
next_activation = current_activation * decay
for entity_name in current_entities:
# Query co-occurring entities with IDF weighting
cooccurring = query("""
SELECT e.name, c.cooccur_weight, es.idf_weight
FROM entity_cooccurrence c
JOIN entities e ON c.entity_id_2 = e.id
JOIN entity_stats es ON e.id = es.id
WHERE c.entity_id_1 = (
SELECT id FROM entities WHERE name = ?
)
AND e.name NOT IN ({}),
ORDER BY c.cooccur_weight * es.idf_weight DESC
LIMIT 10
""", entity_name)
for coentity_name, cooccur_weight, idf_weight in cooccurring:
if coentity_name not in visited:
contribution = next_activation * cooccur_weight * idf_weight
activation[coentity_name] = activation.get(coentity_name, 0) + contribution
next_entities.append(coentity_name)
visited.add(coentity_name)
current_entities = next_entities
current_activation = next_activation
return activationフェーズ5:アクセスベース減衰の実装
取得スコアに累乗法則減衰を実装します:
def compute_retrieval_score(fact: dict, query_entities: list[str],
now: datetime = None) -> float:
"""
Compute composite retrieval score including salience and access-based decay.
Components:
- Base match score (lexical/semantic/associative)
- Salience weight (from retain call)
- Access decay (power-law, reset on retrieval)
"""
if now is None:
now = datetime.utcnow()
base_score = compute_base_match_score(fact, query_entities)
salience_score = fact.get('salience', 0.5)
# Access-based decay (power-law, halves every 7 days)
last_accessed = fact.get('last_accessed_at')
if last_accessed:
days_since_access = (now - last_accessed).days
access_decay = 0.5 ** (days_since_access / 7.0)
else:
access_decay = 0.25 # Never-accessed facts start quieter
# Boost for frequent access (logarithmic to prevent hub dominance)
access_count = fact.get('access_count', 0)
access_boost = 1.0 + (0.1 * math.log1p(access_count))
composite_score = (
base_score * 0.4 +
salience_score * 0.35 +
access_decay * access_boost * 0.25
)
return composite_score
def on_fact_retrieved(fact_id: int) -> None:
"""Update access tracking when a fact is retrieved."""
execute("""
UPDATE facts
SET last_accessed_at = ?,
access_count = access_count + 1
WHERE id = ?
""", (datetime.utcnow(), fact_id))フェーズ6:リフレクトループの統合
リフレクトジョブを更新して、最近アクセスされたファクトを優先します:
# In reflect job processor
def reflect_on_memories(agent_id: str, core_memory_max_tokens: int = 2048) -> None:
# Query recently-accessed facts weighted by salience
recent_facts = query("""
SELECT f.*,
COALESCE(f.salience, 0.5) *
(1.0 + 0.1 * LOG1P(COALESCE(f.access_count, 0))) as priority_score
FROM facts f
WHERE f.agent_id = ?
AND (
f.last_accessed_at > datetime('now', '-30 days')
OR f.salience > 0.8
)
ORDER BY priority_score DESC, f.last_accessed_at DESC
LIMIT 100
""", agent_id)
# Existing reflect logic operates on priority-filtered set
consolidated = consolidate_memories(recent_facts)
update_core_memory(consolidated, max_tokens=core_memory_max_tokens)🧪 検証
検証テストスイート
各強化を検証するには、以下のコマンドを実行します:
テスト1:スキーマ移行
$ sqlite3 ~/.openclaw/memory.db ".schema facts"
--- Expected output ---
CREATE TABLE facts (
...
salience REAL DEFAULT 0.5,
last_accessed_at DATETIME,
access_count INTEGER DEFAULT 0
);
$ sqlite3 ~/.openclaw/memory.db "SELECT COUNT(*) FROM entity_cooccurrence;"
--- Expected output ---
> 0 (before population) or > 100 (after population with populated index)テスト2:重要度対応の保持と検索
# Retain with salience
$ openclaw memory retain "Sarah is leaving the company next month" \
--type B \
--entity Sarah \
--entity project \
--salience 0.95
--- Expected output ---
✓ Retained: B(s=0.95) @Sarah @project: Sarah is leaving...
# Verify in database
$ sqlite3 ~/.openclaw/memory.db \
"SELECT content, salience FROM facts WHERE content LIKE '%Sarah%';"
--- Expected output ---
Sarah is leaving the company next month|0.95テスト3:アクセストラッキング
# Query a fact (simulated)
$ openclaw memory recall "heartbeat configuration"
# Verify access tracking updated
$ sqlite3 ~/.openclaw/memory.db \
"SELECT content, last_accessed_at, access_count FROM facts ORDER BY access_count DESC LIMIT 3;"
--- Expected output ---
Updated heartbeat interval from 5m to 30m.|2025-01-15 10:30:00|5
Increased worker pool size to 4.|2025-01-15 09:15:00|3
Rate limiting middleware added.|2025-01-14 14:22:00|1テスト4:連想横断クエリ
# Query with associative depth
$ openclaw memory recall "app performance" \
--associative-depth 2 \
--min-salience 0.3
--- Expected output ---
RETRIEVED (associative, depth=2):
Direct matches:
- W(s=0.2) @config: Updated heartbeat interval from 5m to 30m.
2-hop connections:
- B(s=0.95) @Sarah @project: Sarah is leaving... (via @database → @slow-endpoint)
- W(s=0.3) @api: Rate limiting middleware added. (via @slow-endpoint)
# Verify traversal path in debug mode
$ openclaw memory recall "app performance" --associative-depth 2 --debug
--- Expected output ---
Traversal: performance → {database, slow-endpoint, api}
→ database → {Sarah, PostgreSQL, indexing}
→ Final activation: {Sarah: 0.42, indexing: 0.31, ...}テスト5:複合スコアリング検証
$ python3 -c "
from openclaw.memory.scoring import compute_retrieval_score
import datetime
test_fact = {
'content': 'Sarah is leaving next month',
'salience': 0.95,
'last_accessed_at': datetime.datetime.now() - datetime.timedelta(days=2),
'access_count': 5
}
score = compute_retrieval_score(test_fact, query_entities=['personnel'])
print(f'Composite score: {score:.3f}')
print(f' - Salience contribution: {0.95 * 0.35:.3f}')
print(f' - Access decay (2 days): {0.5 ** (2/7) * 1.15 * 0.25:.3f}')
"
--- Expected output ---
Composite score: 0.573
- Salience contribution: 0.333
- Access decay (2 days): 0.240テスト6:リフレクトジョブの優先順位付け
# Run reflect with debug output
$ openclaw memory reflect --agent-id test-agent --debug
--- Expected output ---
Processing 47 facts (filtered from 487 total by priority)
Top priority facts:
1. B(s=0.95) @Sarah @project: Sarah is leaving... (priority: 1.23)
2. B(s=0.9) @user @identity: User prefers morning standups... (priority: 1.19)
3. W(s=0.8) @Peter @deadline: Q1 deadline is March 15... (priority: 1.08)
Core memory updated: 1,847 tokens (was 2,103)⚠️ よくある落とし穴
実装のトラップと環境固有の考慮事項
落とし穴1:IDF重み付けなしでのハブノード支配
**症状:**連想横断がクエリに関係なくほぼ同一の結果を返す—高次数のエンティティ(Peter、config、system)がすべてのパスを支配する。
**原因:**逆エンティティ頻度重み付けなしの生の共存カウント。
**修正:**すべての共存クエリに entity_stats.idf_weight = 1 / log(entity_fact_count) 公式が適用されていることを確認します:
-- Wrong (hub dominance)
SELECT e.name FROM entities e
JOIN fact_entities fe ON e.id = fe.entity_id
WHERE fe.fact_id IN (
SELECT fact_id FROM fact_entities WHERE entity_id = ?
)
ORDER BY COUNT(*) DESC
-- Correct (IDF-weighted)
SELECT e.name FROM entities e
JOIN entity_stats es ON e.id = es.id
JOIN fact_entities fe ON e.id = fe.entity_id
WHERE fe.fact_id IN (
SELECT fact_id FROM fact_entities WHERE entity_id = ?
)
ORDER BY es.idf_weight * COUNT(*) DESC落とし穴2:時間ベース減衰とアクセスベース減衰の混同
**症状:**古いが頻繁にアクセスされるファクトは低いスコアを受け取る;新鮮なだがアクセスされたことのないファクトは高いスコアを受け取る。
**原因:**boostを持つアクセスベース減衰ではなく、純粋な last_accessed_at 年齢の使用。
**ルール:**アクセスベース減衰(取得時にリセット)は時間ベース減衰より優れています。毎週アクセスされる3ヶ月前のファクトは、アクセスされたことのない1日前のファクトより優先されるべきです:
# Wrong: Pure age decay
score = salience * (0.5 ** (age_in_days / 30))
# Correct: Access-based decay with boost
access_decay = 0.5 ** (days_since_last_access / 7) # Halves every 7 days
access_boost = 1.0 + (0.1 * log1p(access_count)) # Logarithmic, prevents hub dominance
score = salience * access_decay * access_boost落とし穴3:連想深度が深すぎる
**症状:**取得レイテンシが500msを超える;出力に無作為なファクトが含まれる。
**原因:**活性化カットオフなしの深度 > 3が横断を洪水させる。
**修正:**深度制限AND最小活性化しきい値の両方を実装します:
MAX_DEPTH = 3
MIN_ACTIVATION = 0.05
INITIAL_ACTIVATION = 1.0
DECAY_PER_HOP = 0.5
# Traversal stops when:
# - Depth limit reached, OR
# - No entities exceed MIN_ACTIVATION threshold落とし穴4:保持時の重要度推定の失敗
**症状:**すべてのファクトが類似した重要度スコア(0.4-0.6)を受け取る;差別化が失われる。
**原因:**LLM推定が過度に保守的;デフォルトで中間の値を使用する。
**修正:**明示的なアンカーを持つプロンプトベースの重要度推定を実装します:
SYSTEM_PROMPT = """
Estimate salience (0.0-1.0) for this memory:
- 0.9-1.0: Identity-defining, relationship-changing, career-affecting
- 0.7-0.9: Important project decisions, team changes, deadlines
- 0.4-0.7: Routine work, configurations, bug fixes
- 0.1-0.4: Minor preferences, temp states, easily reconstructed
Memory: {fact_content}
Respond ONLY with a number between 0.0 and 1.0.
"""常に --salience CLIフラグまたは直接ファイル編集 통해人のオーバーライドを許可します。
落とし穴5:Docker/コンテナ環境の権限
**症状:**Dockerで実行ときに sqlite3: unable to open database file 。
**原因:**SQLiteデータベースが正しくない権限またはパスでボリュームにマウントされている。
**修正:**ボリュームマウントがディレクトリ構造を保持していることを確認します:
# Wrong
docker run -v /host/memory:/container/memory image
# Correct (bind mount the parent directory)
docker run -v /host/.openclaw:/root/.openclaw image
# Verify permissions
docker exec container ls -la /root/.openclaw/memory.db
# Should show: -rw-r--r-- 1 root root ...落とし穴6:Raspberry Pi 5のリソース制約
**症状:**連想横断がARMデバイスでメモリ圧力を引き起こす。
**原因:**活性化トラッキング用のPython辞書 + 再帰的クエリが利用可能なRAMを超える。
**修正:**横断スコープを制限し、カーソルベースの反復を使用します:
# Limit activation dict size
MAX_ACTIVATION_ENTITIES = 50
# Use generator for memory efficiency
def associative_traverse_stream(seed, depth, decay):
frontier = {seed: 1.0}
visited = {seed}
for _ in range(depth):
next_frontier = {}
for entity, activation in frontier.items():
if activation < MIN_ACTIVATION:
continue
for coentity in fetch_cooccurring(entity, limit=5):
if coentity not in visited:
next_frontier[coentity] = next_frontier.get(coentity, 0) + \
activation * decay
visited.add(coentity)
frontier = next_frontier
yield from frontier.items()🔗 関連するエラー
文脈的に接続された問題と歴史的参照
関連する設計ドキュメント
- Workspace Memory v2 Research Doc — このガイドが拡張するベースラインアーキテクチャ。主要セクション:「Entity-Aware Retrieval」、「Incremental Indexing」、「Reflect Loop」
- Hindsight × Letta Integration — 信頼度付きオピニオンを持つ型付きファクトは、重要度重み付けの基盤を提供します
- CLS-M Prototype Analysis — ナイーブな-spreading activationで精度課題を実証する経験的検証(132ノード、802エッジ、F1=44%)
メモリシステムでの一般的なエラーコード
| エラーコード | 説明 | 関連項目 |
|---|---|---|
E2BIG | _assembledコンテキストがトークンバジェットを超える;リフレクトジョブが圧縮できない | 重要度重み付け、アクセス減衰 |
ENOENTITY | エンティティルックアップが空を返すがセマンティック検索が結果を見つける | エンティティ抽出のギャップ、FTSフォールバック |
EDUPFACTS | 統合なしのほぼ重複するファクトが蓄積 | リフレクトループの制限 |
EHUBNODES | 高頻度エンティティ(Peter、system、config)による検索支配 | IDF重み付けの欠如 |
ECOLDSTART | 連想横断に十分なfact密度がない新しいデプロイ | エンティティ共存密度のしきい値 |
EDECAYTOOFAST | 時間ベースの減衰が有用な古いメモリをprematurely消去 | アクセスベース vs. 時間ベースの減衰 |
CLS-Mからの歴史的文脈
CLS-Mプロトタイプはこれらの推奨事項 inform た Failure modes を特定しました:
- 45クエリベンチマークでF1=44% — 精度(35%)がボトルネックであり、リコール(65%)ではなかった
- ハブノイズのkill:
heartbeatノード(57エッジ)はすべてのクエリで総激活の15%を吸収した - Delegation failure:サブエージェントのメモリ抽出が一貫して失敗した;体験するエージェントが保持を所有する必要がある
- spread too thin:800+エッジ全体の激活が有用なしきい値以下に希釈された
これらの発見は段階的アプローチを検証します:FTS5から開始し、埋め込みを追加し、十分なインデックス密度に達した後にのみエンティティ共存を追加します。
OpenClawバージョン互換性
| バージョン | 必要な機能 | 移行パス |
|---|---|---|
| v0.11.x | 基本的なfactストレージ、FTS5 | フェーズ1-2の移行を適用 |
| v0.12.0 | エンティティ抽出、重要度フィールド | フェーズ1-6を段階的に適用 |
| v0.13.0 (計画中) | 連想横断、アクセストラッキング | 完全な実装 |