superlocalmemory 4.0.3 → 4.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +19 -13
- package/ide/configs/codex-mcp.toml +2 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -0
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +3 -2
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +5 -4
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +7 -6
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +3 -2
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -4
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/brain/__init__.py +5 -0
- package/src/superlocalmemory/brain/truth.py +348 -0
- package/src/superlocalmemory/cli/commands.py +82 -25
- package/src/superlocalmemory/cli/main.py +12 -0
- package/src/superlocalmemory/core/context_cache.py +58 -1
- package/src/superlocalmemory/core/mutations.py +155 -25
- package/src/superlocalmemory/core/recall_pipeline.py +6 -10
- package/src/superlocalmemory/core/remember_runtime.py +271 -2
- package/src/superlocalmemory/core/store_pipeline.py +100 -38
- package/src/superlocalmemory/encoding/consolidator.py +17 -47
- package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
- package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +185 -0
- package/src/superlocalmemory/learning/database.py +2 -1
- package/src/superlocalmemory/mcp/profiles.py +25 -7
- package/src/superlocalmemory/mcp/server.py +7 -2
- package/src/superlocalmemory/mcp/tools_brain.py +138 -9
- package/src/superlocalmemory/mcp/tools_core.py +88 -3
- package/src/superlocalmemory/retrieval/engine.py +7 -10
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
- package/src/superlocalmemory/server/routes/brain.py +21 -1
- package/src/superlocalmemory/server/routes/memories.py +129 -3
- package/src/superlocalmemory/storage/_migration_internals.py +8 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/agent_experience.py +26 -4
- package/src/superlocalmemory/storage/correction_cases.py +670 -0
- package/src/superlocalmemory/storage/database.py +194 -24
- package/src/superlocalmemory/storage/external_evidence.py +359 -0
- package/src/superlocalmemory/storage/migration_runner.py +12 -0
- package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +189 -0
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/write_coordinator.py +4 -0
- package/src/superlocalmemory/ui/js/brain.js +43 -7
- package/src/superlocalmemory/ui/js/od-brain.js +44 -19
|
@@ -52,6 +52,7 @@ from typing import Any
|
|
|
52
52
|
|
|
53
53
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
54
54
|
from superlocalmemory import __version__
|
|
55
|
+
from superlocalmemory.brain import BrainTruthService
|
|
55
56
|
|
|
56
57
|
from superlocalmemory.core.security_primitives import (
|
|
57
58
|
redact_secrets,
|
|
@@ -327,7 +328,12 @@ def _compute_agent_experience(profile_id: str) -> dict:
|
|
|
327
328
|
Receipt rows record observation and verification evidence only; this read
|
|
328
329
|
model never alters retrieval, ranking, or model routing.
|
|
329
330
|
"""
|
|
330
|
-
|
|
331
|
+
from superlocalmemory.storage.external_evidence import get_profile_external_evidence_summary
|
|
332
|
+
path = _learning_db_path()
|
|
333
|
+
return {
|
|
334
|
+
**get_profile_receipt_summary(path, profile_id),
|
|
335
|
+
"external_graph_evidence": get_profile_external_evidence_summary(path, profile_id),
|
|
336
|
+
}
|
|
331
337
|
|
|
332
338
|
|
|
333
339
|
def _resolve_phase(signals: int, model_active: bool,
|
|
@@ -1125,11 +1131,16 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
|
|
|
1125
1131
|
# "default" — the Brain must reflect whichever profile is active.
|
|
1126
1132
|
profile_id = _authorized_profile(request, profile_id)
|
|
1127
1133
|
lrn_db = LearningDatabase(_learning_db_path())
|
|
1134
|
+
truth_service = BrainTruthService(
|
|
1135
|
+
memory_db_path=_memory_dir() / "memory.db",
|
|
1136
|
+
learning_db_path=_learning_db_path(),
|
|
1137
|
+
)
|
|
1128
1138
|
|
|
1129
1139
|
(
|
|
1130
1140
|
preferences, learning, usage, bandit_snap, cache,
|
|
1131
1141
|
cross_platform, outcomes_preview, evolution, active_clients,
|
|
1132
1142
|
feedback_loop, source_quality, graph_summary, agent_experience,
|
|
1143
|
+
brain_truth,
|
|
1133
1144
|
) = await asyncio.gather(
|
|
1134
1145
|
asyncio.to_thread(_compute_preferences, profile_id),
|
|
1135
1146
|
asyncio.to_thread(_compute_learning_status, profile_id, lrn_db),
|
|
@@ -1147,6 +1158,7 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
|
|
|
1147
1158
|
asyncio.to_thread(_compute_source_quality, profile_id),
|
|
1148
1159
|
asyncio.to_thread(_compute_graph_summary, profile_id),
|
|
1149
1160
|
asyncio.to_thread(_compute_agent_experience, profile_id),
|
|
1161
|
+
asyncio.to_thread(truth_service.snapshot, profile_id),
|
|
1150
1162
|
return_exceptions=True,
|
|
1151
1163
|
)
|
|
1152
1164
|
|
|
@@ -1208,6 +1220,14 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
|
|
|
1208
1220
|
"source_quality": source_quality,
|
|
1209
1221
|
"graph": graph_summary,
|
|
1210
1222
|
"agent_experience": agent_experience,
|
|
1223
|
+
# Additive v4.0.5 contract. Keep the preceding legacy keys for
|
|
1224
|
+
# existing dashboard/API clients; new clients should render this
|
|
1225
|
+
# one shared, unavailable-aware snapshot.
|
|
1226
|
+
"brain_truth": _ok(brain_truth, {
|
|
1227
|
+
"availability": "unavailable",
|
|
1228
|
+
"source": "BrainTruth service unavailable",
|
|
1229
|
+
"control_plane": "observation_only",
|
|
1230
|
+
}),
|
|
1211
1231
|
"source": "local durable stores + ephemeral session registry",
|
|
1212
1232
|
},
|
|
1213
1233
|
"evolution_preview": _ok(evolution, {
|
|
@@ -1182,9 +1182,9 @@ async def merge_memory(request: Request, fact_id: str):
|
|
|
1182
1182
|
raise _canonical_mutation_error(exc, "Merge error")
|
|
1183
1183
|
|
|
1184
1184
|
|
|
1185
|
-
@router.patch("/api/memories/{fact_id}")
|
|
1185
|
+
@router.patch("/api/memories/{fact_id}", status_code=202)
|
|
1186
1186
|
async def edit_memory(request: Request, fact_id: str):
|
|
1187
|
-
"""
|
|
1187
|
+
"""Propose an immutable, review-required correction for one memory."""
|
|
1188
1188
|
try:
|
|
1189
1189
|
body = await request.json()
|
|
1190
1190
|
new_content = (body.get("content") or "").strip()
|
|
@@ -1210,13 +1210,139 @@ async def edit_memory(request: Request, fact_id: str):
|
|
|
1210
1210
|
)
|
|
1211
1211
|
if not result.get("ok"):
|
|
1212
1212
|
raise HTTPException(status_code=404, detail="Memory not found")
|
|
1213
|
-
|
|
1213
|
+
if result.get("unchanged"):
|
|
1214
|
+
return {"success": True, "fact_id": fact_id, "content": new_content, "unchanged": True}
|
|
1215
|
+
correction = result["correction_case"]
|
|
1216
|
+
return {
|
|
1217
|
+
"success": True,
|
|
1218
|
+
"fact_id": fact_id,
|
|
1219
|
+
"predecessor_fact_id": result["predecessor_fact_id"],
|
|
1220
|
+
"successor_fact_id": result["successor_fact_id"],
|
|
1221
|
+
"correction_case": correction,
|
|
1222
|
+
"review_required": True,
|
|
1223
|
+
"status": "proposed",
|
|
1224
|
+
}
|
|
1214
1225
|
except HTTPException:
|
|
1215
1226
|
raise
|
|
1216
1227
|
except Exception as exc:
|
|
1217
1228
|
raise _canonical_mutation_error(exc, "Edit error")
|
|
1218
1229
|
|
|
1219
1230
|
|
|
1231
|
+
@router.post("/api/corrections/{case_id}/{action}")
|
|
1232
|
+
async def review_correction(request: Request, case_id: str, action: str):
|
|
1233
|
+
"""Apply, reject, or roll back an active-profile correction case.
|
|
1234
|
+
|
|
1235
|
+
The caller authenticates through the daemon boundary. It cannot select a
|
|
1236
|
+
profile, fact scope, or trust tier; the canonical writer rechecks all of
|
|
1237
|
+
those fields in its one SQLite transaction.
|
|
1238
|
+
"""
|
|
1239
|
+
try:
|
|
1240
|
+
body = await request.json()
|
|
1241
|
+
if action not in {"apply", "reject", "rollback"}:
|
|
1242
|
+
raise HTTPException(422, detail="action must be apply, reject, or rollback")
|
|
1243
|
+
expected_version = body.get("expected_version") if isinstance(body, dict) else None
|
|
1244
|
+
if not isinstance(expected_version, int) or isinstance(expected_version, bool):
|
|
1245
|
+
raise HTTPException(422, detail="expected_version must be a non-negative integer")
|
|
1246
|
+
if expected_version < 0:
|
|
1247
|
+
raise HTTPException(422, detail="expected_version must be a non-negative integer")
|
|
1248
|
+
event_valid_until = body.get("event_valid_until") if isinstance(body, dict) else None
|
|
1249
|
+
if event_valid_until is not None and not isinstance(event_valid_until, str):
|
|
1250
|
+
raise HTTPException(422, detail="event_valid_until must be an RFC3339 timestamp")
|
|
1251
|
+
if event_valid_until is not None and action != "apply":
|
|
1252
|
+
raise HTTPException(422, detail="event_valid_until is permitted only for apply")
|
|
1253
|
+
engine, active_profile, hook_context = _authorize_memory_mutation(
|
|
1254
|
+
request, "update", case_id, run_pre_hook=False
|
|
1255
|
+
)
|
|
1256
|
+
result = _canonical_mutation_runtime(request).transition_correction(
|
|
1257
|
+
active_profile,
|
|
1258
|
+
case_id,
|
|
1259
|
+
action=action,
|
|
1260
|
+
expected_version=expected_version,
|
|
1261
|
+
actor_id=hook_context["agent_id"],
|
|
1262
|
+
event_valid_until=event_valid_until,
|
|
1263
|
+
idempotency_key=_mutation_idempotency_key(request),
|
|
1264
|
+
)
|
|
1265
|
+
if not result.get("ok"):
|
|
1266
|
+
raise HTTPException(404, detail="Correction case not found")
|
|
1267
|
+
if action in {"apply", "rollback"}:
|
|
1268
|
+
from superlocalmemory.core.mutations import purge_profile_context_cache
|
|
1269
|
+
|
|
1270
|
+
purge_profile_context_cache(engine, active_profile)
|
|
1271
|
+
engine._hooks.run_post("update", hook_context)
|
|
1272
|
+
return {"success": True, "correction_case": result}
|
|
1273
|
+
except HTTPException:
|
|
1274
|
+
raise
|
|
1275
|
+
except Exception as exc:
|
|
1276
|
+
raise _canonical_mutation_error(exc, "Correction review error")
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _correction_case_response(case) -> dict[str, object]:
|
|
1280
|
+
"""Return review metadata only; correction ledgers never contain fact text."""
|
|
1281
|
+
return {
|
|
1282
|
+
"case_id": case.case_id,
|
|
1283
|
+
"profile_id": case.profile_id,
|
|
1284
|
+
"scope": case.scope,
|
|
1285
|
+
"predecessor_fact_id": case.predecessor_fact_id,
|
|
1286
|
+
"successor_fact_id": case.successor_fact_id,
|
|
1287
|
+
"reason_code": case.reason_code,
|
|
1288
|
+
"status": case.status,
|
|
1289
|
+
"version": case.version,
|
|
1290
|
+
"created_at": case.created_at,
|
|
1291
|
+
"updated_at": case.updated_at,
|
|
1292
|
+
"reviewed_at": case.reviewed_at,
|
|
1293
|
+
"applied_at": case.applied_at,
|
|
1294
|
+
"system_effective_at": case.system_effective_at,
|
|
1295
|
+
"event_valid_from": case.event_valid_from,
|
|
1296
|
+
"event_valid_until": case.event_valid_until,
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _correction_store_for(engine, active_profile: str):
|
|
1301
|
+
from superlocalmemory.storage.correction_cases import CorrectionCaseStore
|
|
1302
|
+
|
|
1303
|
+
return CorrectionCaseStore(
|
|
1304
|
+
engine._db.db_path,
|
|
1305
|
+
is_profile_active=lambda candidate: candidate == active_profile,
|
|
1306
|
+
# Read operations never invoke this callback; writes use the daemon's
|
|
1307
|
+
# canonical runtime, which derives the authenticated actor separately.
|
|
1308
|
+
is_actor_trusted=lambda _actor: False,
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
@router.get("/api/corrections")
|
|
1313
|
+
async def list_corrections(request: Request, limit: int = 100):
|
|
1314
|
+
"""List bounded review metadata for the active owning profile."""
|
|
1315
|
+
try:
|
|
1316
|
+
engine, active_profile, _context = _authorize_memory_mutation(
|
|
1317
|
+
request, "update", "correction-list", run_pre_hook=False
|
|
1318
|
+
)
|
|
1319
|
+
cases = _correction_store_for(engine, active_profile).list_cases(active_profile, limit=limit)
|
|
1320
|
+
return {"success": True, "corrections": [_correction_case_response(case) for case in cases]}
|
|
1321
|
+
except HTTPException:
|
|
1322
|
+
raise
|
|
1323
|
+
except Exception as exc:
|
|
1324
|
+
raise _canonical_mutation_error(exc, "Correction list error")
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
@router.get("/api/corrections/{case_id}")
|
|
1328
|
+
async def get_correction(request: Request, case_id: str):
|
|
1329
|
+
"""Get one active-profile correction case without exposing raw memory text."""
|
|
1330
|
+
try:
|
|
1331
|
+
engine, active_profile, _context = _authorize_memory_mutation(
|
|
1332
|
+
request, "update", case_id, run_pre_hook=False
|
|
1333
|
+
)
|
|
1334
|
+
case = _correction_store_for(engine, active_profile).get_case(case_id)
|
|
1335
|
+
return {"success": True, "correction": _correction_case_response(case)}
|
|
1336
|
+
except HTTPException:
|
|
1337
|
+
raise
|
|
1338
|
+
except Exception as exc:
|
|
1339
|
+
from superlocalmemory.storage.correction_cases import CorrectionNotFoundError
|
|
1340
|
+
|
|
1341
|
+
if isinstance(exc, CorrectionNotFoundError):
|
|
1342
|
+
raise HTTPException(404, detail="Correction case not found") from exc
|
|
1343
|
+
raise _canonical_mutation_error(exc, "Correction lookup error")
|
|
1344
|
+
|
|
1345
|
+
|
|
1220
1346
|
_VALID_SCOPES = ("personal", "shared", "global")
|
|
1221
1347
|
|
|
1222
1348
|
|
|
@@ -147,6 +147,12 @@ from superlocalmemory.storage.migrations import (
|
|
|
147
147
|
from superlocalmemory.storage.migrations import (
|
|
148
148
|
M040_agent_experience_receipts as _M040,
|
|
149
149
|
)
|
|
150
|
+
from superlocalmemory.storage.migrations import (
|
|
151
|
+
M041_external_evidence_receipts as _M041,
|
|
152
|
+
)
|
|
153
|
+
from superlocalmemory.storage.migrations import (
|
|
154
|
+
M042_correction_case_ledger as _M042,
|
|
155
|
+
)
|
|
150
156
|
|
|
151
157
|
# Emit under the runner's logger name so operational log filters that key on
|
|
152
158
|
# "superlocalmemory.storage.migration_runner" keep matching after this split.
|
|
@@ -195,6 +201,8 @@ _MODULES = {
|
|
|
195
201
|
_M038.NAME: _M038,
|
|
196
202
|
_M039.NAME: _M039,
|
|
197
203
|
_M040.NAME: _M040,
|
|
204
|
+
_M041.NAME: _M041,
|
|
205
|
+
_M042.NAME: _M042,
|
|
198
206
|
}
|
|
199
207
|
|
|
200
208
|
# Exact historical DDL fingerprints whose resulting schema is intentionally
|
|
@@ -21,9 +21,9 @@ import sqlite3
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
23
|
#: Highest schema_version this runner can write. Matches the trailing serial
|
|
24
|
-
#: of the latest migration (
|
|
24
|
+
#: of the latest migration (M042). Increment when adding new migrations or
|
|
25
25
|
#: table-level breaking changes.
|
|
26
|
-
SUPPORTED_SCHEMA_VERSION: int =
|
|
26
|
+
SUPPORTED_SCHEMA_VERSION: int = 42
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class SchemaVersionError(RuntimeError):
|
|
@@ -245,17 +245,29 @@ class AgentExperienceStore:
|
|
|
245
245
|
turn_count = conn.execute(
|
|
246
246
|
"DELETE FROM cognitive_turn_receipts WHERE profile_id=?", (profile_id,)
|
|
247
247
|
).rowcount
|
|
248
|
+
external_count = 0
|
|
249
|
+
has_external = conn.execute(
|
|
250
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' "
|
|
251
|
+
"AND name='external_evidence_receipts'"
|
|
252
|
+
).fetchone() is not None
|
|
253
|
+
if has_external:
|
|
254
|
+
external_count = conn.execute(
|
|
255
|
+
"DELETE FROM external_evidence_receipts WHERE profile_id=?", (profile_id,)
|
|
256
|
+
).rowcount
|
|
257
|
+
receipt_tables = ["agent_experiences", "cognitive_turn_receipts"]
|
|
258
|
+
if has_external:
|
|
259
|
+
receipt_tables.append("external_evidence_receipts")
|
|
248
260
|
residue = sum(
|
|
249
261
|
int(
|
|
250
262
|
conn.execute(
|
|
251
263
|
f"SELECT COUNT(*) FROM {table} WHERE profile_id=?", (profile_id,)
|
|
252
264
|
).fetchone()[0]
|
|
253
265
|
)
|
|
254
|
-
for table in
|
|
266
|
+
for table in receipt_tables
|
|
255
267
|
)
|
|
256
268
|
if residue:
|
|
257
269
|
raise RuntimeError("learning receipt erasure left profile residue")
|
|
258
|
-
return experience_count + turn_count
|
|
270
|
+
return experience_count + turn_count + external_count
|
|
259
271
|
|
|
260
272
|
return self._write(erase)
|
|
261
273
|
|
|
@@ -422,7 +434,7 @@ def purge_profile_receipts(
|
|
|
422
434
|
for row in conn.execute(
|
|
423
435
|
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
424
436
|
"AND name IN ('agent_experiences', 'cognitive_turn_receipts', "
|
|
425
|
-
"'agent_receipt_profile_closures')"
|
|
437
|
+
"'agent_receipt_profile_closures', 'external_evidence_receipts')"
|
|
426
438
|
)
|
|
427
439
|
}
|
|
428
440
|
if not tables:
|
|
@@ -431,6 +443,16 @@ def purge_profile_receipts(
|
|
|
431
443
|
"agent_experiences", "cognitive_turn_receipts", "agent_receipt_profile_closures"
|
|
432
444
|
}
|
|
433
445
|
if tables != expected:
|
|
446
|
+
if tables == expected | {"external_evidence_receipts"}:
|
|
447
|
+
from superlocalmemory.storage.migrations import M041_external_evidence_receipts as m041
|
|
448
|
+
|
|
449
|
+
with sqlite3.connect(path) as conn:
|
|
450
|
+
# Erasure needs a valid table, not its optional performance indexes.
|
|
451
|
+
# A damaged index must never strand profile-scoped evidence.
|
|
452
|
+
if m041._table_is_valid(conn):
|
|
453
|
+
return AgentExperienceStore(
|
|
454
|
+
path, is_profile_active=lambda _: True
|
|
455
|
+
).erase_profile(profile_id, close_profile=close_profile)
|
|
434
456
|
raise sqlite3.OperationalError("incomplete Agent Experience receipt schema")
|
|
435
457
|
return AgentExperienceStore(
|
|
436
458
|
path, is_profile_active=lambda _: True
|
|
@@ -459,7 +481,7 @@ def get_profile_receipt_summary(
|
|
|
459
481
|
if not path.exists():
|
|
460
482
|
return unavailable
|
|
461
483
|
try:
|
|
462
|
-
conn = sqlite3.connect(f"
|
|
484
|
+
conn = sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True, timeout=0.5)
|
|
463
485
|
try:
|
|
464
486
|
experience = conn.execute(
|
|
465
487
|
"SELECT COUNT(*) FROM agent_experiences WHERE profile_id=?", (profile_id,)
|