context-engineering-core 0.12.0__py3-none-any.whl

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.
Files changed (84) hide show
  1. context_engine/__init__.py +15 -0
  2. context_engine/__main__.py +3 -0
  3. context_engine/_sqlite.py +19 -0
  4. context_engine/_windows_security.py +125 -0
  5. context_engine/_windows_storage.py +110 -0
  6. context_engine/adapters.py +148 -0
  7. context_engine/answers.py +90 -0
  8. context_engine/budget.py +93 -0
  9. context_engine/cli.py +222 -0
  10. context_engine/config.py +193 -0
  11. context_engine/context.py +116 -0
  12. context_engine/demo.py +98 -0
  13. context_engine/errors.py +91 -0
  14. context_engine/evaluation/__init__.py +1 -0
  15. context_engine/evaluation/commands.py +90 -0
  16. context_engine/evaluation/corpus.py +247 -0
  17. context_engine/evaluation/data/freeze.json +26 -0
  18. context_engine/evaluation/data/ground_truth.json +287 -0
  19. context_engine/evaluation/data/pin_retention.json +8 -0
  20. context_engine/evaluation/data/protocol.json +122 -0
  21. context_engine/evaluation/data/scenario.json +1226 -0
  22. context_engine/evaluation/data/summary_fixture.json +7 -0
  23. context_engine/evaluation/execution.py +538 -0
  24. context_engine/evaluation/fixtures.py +32 -0
  25. context_engine/evaluation/grading.py +62 -0
  26. context_engine/evaluation/harness.py +165 -0
  27. context_engine/evaluation/heldout/__init__.py +260 -0
  28. context_engine/evaluation/heldout/__main__.py +40 -0
  29. context_engine/evaluation/heldout/data/ground_truth.json +18 -0
  30. context_engine/evaluation/heldout/data/manifest.json +9 -0
  31. context_engine/evaluation/heldout/data/protocol.json +14 -0
  32. context_engine/evaluation/heldout/data/scenarios.json +18 -0
  33. context_engine/evaluation/heldout/data/scoring_manifest.json +4 -0
  34. context_engine/evaluation/heldout/data/scoring_protocol.json +12 -0
  35. context_engine/evaluation/heldout/scoring.py +176 -0
  36. context_engine/evaluation/protocol.py +264 -0
  37. context_engine/evaluation/reporting.py +294 -0
  38. context_engine/evaluation/results.py +331 -0
  39. context_engine/evaluation/run_commands.py +146 -0
  40. context_engine/evaluation/strategies.py +261 -0
  41. context_engine/inspection.py +266 -0
  42. context_engine/layers/__init__.py +18 -0
  43. context_engine/layers/cap.py +99 -0
  44. context_engine/layers/common.py +162 -0
  45. context_engine/layers/pin.py +68 -0
  46. context_engine/layers/retrieve.py +233 -0
  47. context_engine/layers/summarize.py +202 -0
  48. context_engine/layers/window.py +29 -0
  49. context_engine/mcp_server.py +267 -0
  50. context_engine/memory/__init__.py +5 -0
  51. context_engine/memory/codec.py +91 -0
  52. context_engine/memory/demo.py +90 -0
  53. context_engine/memory/store.py +770 -0
  54. context_engine/models.py +384 -0
  55. context_engine/pipeline.py +383 -0
  56. context_engine/providers/__init__.py +1 -0
  57. context_engine/providers/client.py +301 -0
  58. context_engine/providers/commands.py +138 -0
  59. context_engine/providers/contracts.py +324 -0
  60. context_engine/providers/credentials.py +66 -0
  61. context_engine/providers/memory.py +112 -0
  62. context_engine/providers/store.py +408 -0
  63. context_engine/providers/transport.py +195 -0
  64. context_engine/providers/windows_credentials.py +23 -0
  65. context_engine/py.typed +0 -0
  66. context_engine/service/__init__.py +1 -0
  67. context_engine/service/__main__.py +68 -0
  68. context_engine/service/app.py +404 -0
  69. context_engine/service/auth.py +132 -0
  70. context_engine/service/client.py +52 -0
  71. context_engine/service/control.py +134 -0
  72. context_engine/service/demo.py +151 -0
  73. context_engine/service/integrations.py +28 -0
  74. context_engine/service/schemas.py +59 -0
  75. context_engine/service/worker.py +59 -0
  76. context_engine/service/workers.py +156 -0
  77. context_engine/summaries.py +196 -0
  78. context_engine/tokens.py +171 -0
  79. context_engine/work.py +46 -0
  80. context_engineering_core-0.12.0.dist-info/METADATA +203 -0
  81. context_engineering_core-0.12.0.dist-info/RECORD +84 -0
  82. context_engineering_core-0.12.0.dist-info/WHEEL +4 -0
  83. context_engineering_core-0.12.0.dist-info/entry_points.txt +3 -0
  84. context_engineering_core-0.12.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,770 @@
1
+ """Scoped transactional memory, resumable chunk indexes and deletion-aware local recovery."""
2
+
3
+ import os
4
+ import sqlite3
5
+ import stat
6
+ import time
7
+ import uuid
8
+ from contextlib import closing, contextmanager
9
+ from dataclasses import dataclass
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+
13
+ from .._sqlite import connect
14
+ from ..config import RetrievalConfig
15
+ from ..errors import ContractError, MemoryConflict, MemoryIntegrityError, StorageError
16
+ from ..layers.common import history_digest
17
+ from ..layers.retrieve import ChunkIndex, chunk_turns
18
+ from ..layers.summarize import FrozenSummaryPolicy, PrefixSummaryPolicy, SummarySnapshot
19
+ from ..models import ChatRequest, KeyedPins, Pin, Scope, Turn, aware_time, integer_value, text_value
20
+ from ..pipeline import assemble_context
21
+ from ..work import raise_if_cancelled, validate_cancellation
22
+ from .codec import decode, digest, encode, plain, unpack
23
+
24
+ MAX_TURNS, MAX_BYTES, MAX_CHUNKS = 10000, 8_000_000, 100000
25
+
26
+
27
+ def private_file(path, *, create=False, exclusive=False):
28
+ if os.name == "nt":
29
+ from .._windows_storage import private_file as windows_private_file
30
+
31
+ return windows_private_file(path, create=create, exclusive=exclusive)
32
+ try:
33
+ path = Path(path).absolute()
34
+ if create:
35
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
36
+ flags = os.O_RDWR | getattr(os, "O_NOFOLLOW", 0)
37
+ if create:
38
+ flags |= os.O_CREAT
39
+ if exclusive:
40
+ flags |= os.O_EXCL
41
+ descriptor = os.open(path, flags, 0o600)
42
+ try:
43
+ info = os.fstat(descriptor)
44
+ if not stat.S_ISREG(info.st_mode) or (os.name == "posix" and info.st_mode & 0o077):
45
+ raise StorageError("Memory files must be private regular files")
46
+ finally:
47
+ os.close(descriptor)
48
+ return path
49
+ except (OSError, ValueError, TypeError):
50
+ raise StorageError("Cannot open private memory file") from None
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class MemorySnapshot:
55
+ scope: Scope
56
+ revision: int
57
+ snapshot_id: str
58
+ history: tuple[Turn, ...]
59
+ pins: KeyedPins
60
+ summary: SummarySnapshot | None
61
+ at: datetime
62
+
63
+ def to_dict(self, *, include_content=False):
64
+ if type(include_content) is not bool:
65
+ raise MemoryIntegrityError("Explicit content export must be boolean")
66
+ result = {
67
+ "scope": plain(self.scope),
68
+ "revision": self.revision,
69
+ "snapshot_id": self.snapshot_id,
70
+ "turns": len(self.history),
71
+ "pins": len(self.pins.pins),
72
+ "summary_available": self.summary is not None,
73
+ "content_included": include_content,
74
+ }
75
+ if include_content:
76
+ result.update(
77
+ history=plain(self.history),
78
+ pinned_facts=plain(self.pins),
79
+ summary=plain(self.summary),
80
+ )
81
+ return result
82
+
83
+
84
+ class MemoryStore:
85
+ def __init__(self, path, *, deletion_path=None, clock=None):
86
+ self.clock = clock or (lambda: datetime.now(UTC))
87
+ path = Path(path).absolute()
88
+ deletion_path = (
89
+ Path(deletion_path).absolute()
90
+ if deletion_path
91
+ else path.with_suffix(".deletions.sqlite")
92
+ )
93
+ if path == deletion_path:
94
+ raise StorageError("Memory and deletion authority require distinct files")
95
+ creating = not path.exists()
96
+ if creating and deletion_path.exists():
97
+ raise StorageError("Existing deletion authority requires explicit restore")
98
+ self.path = private_file(path, create=creating, exclusive=creating)
99
+ self.deletion_path = private_file(deletion_path, create=creating, exclusive=creating)
100
+ with self._transaction() as db:
101
+ version = db.execute("PRAGMA main.user_version").fetchone()[0]
102
+ dv = db.execute("PRAGMA deletions.user_version").fetchone()[0]
103
+ if creating:
104
+ lineage = uuid.uuid4().hex
105
+ db.execute(
106
+ "CREATE TABLE meta(lineage TEXT, epoch TEXT, applied INTEGER, last_clock REAL)"
107
+ )
108
+ db.execute("INSERT INTO meta VALUES(?,?,0,0)", (lineage, uuid.uuid4().hex))
109
+ db.execute("CREATE TABLE deletions.authority(lineage TEXT)")
110
+ db.execute("INSERT INTO deletions.authority VALUES(?)", (lineage,))
111
+ db.execute(
112
+ "CREATE TABLE deletions.events(seq INTEGER PRIMARY KEY AUTOINCREMENT, "
113
+ "scope TEXT, kind TEXT, key TEXT, UNIQUE(scope,kind,key))"
114
+ )
115
+ for sql in (
116
+ "CREATE TABLE scopes(scope TEXT PRIMARY KEY, revision INTEGER, "
117
+ "deleted INTEGER DEFAULT 0)",
118
+ "CREATE TABLE turns(scope TEXT, key TEXT, position INTEGER, revision INTEGER, "
119
+ "payload TEXT, digest TEXT, expires REAL, PRIMARY KEY(scope,key))",
120
+ "CREATE TABLE pins(scope TEXT, key TEXT, revision INTEGER, payload TEXT, "
121
+ "digest TEXT, expires REAL, PRIMARY KEY(scope,key))",
122
+ "CREATE TABLE summaries(scope TEXT PRIMARY KEY, payload TEXT, "
123
+ "digest TEXT, expires REAL)",
124
+ "CREATE TABLE operations(scope TEXT, key TEXT, digest TEXT, "
125
+ "result INTEGER, PRIMARY KEY(scope,key))",
126
+ "CREATE TABLE chunks(scope TEXT, profile TEXT, turn_id TEXT, key TEXT, "
127
+ "payload TEXT, PRIMARY KEY(scope,profile,key))",
128
+ "CREATE TABLE coverage(scope TEXT, profile TEXT, turn_id TEXT, "
129
+ "source_hash TEXT, chunks_hash TEXT, PRIMARY KEY(scope,profile,turn_id))",
130
+ "CREATE TABLE replay(scope TEXT, key TEXT, snapshot TEXT, payload TEXT, "
131
+ "digest TEXT, expires REAL, PRIMARY KEY(scope,key))",
132
+ ):
133
+ db.execute(sql)
134
+ db.execute("PRAGMA main.user_version=1")
135
+ db.execute("PRAGMA deletions.user_version=1")
136
+ elif (version, dv) != (1, 1):
137
+ raise StorageError("Unsupported memory/deletion schema; migration required")
138
+ self._maintain(db)
139
+
140
+ @contextmanager
141
+ def _transaction(self):
142
+ db = None
143
+ try:
144
+ private_file(self.path)
145
+ private_file(self.deletion_path)
146
+ db = connect(self.path, timeout=0.5, isolation_level=None)
147
+ db.row_factory = sqlite3.Row
148
+ db.execute("ATTACH DATABASE ? AS deletions", (str(self.deletion_path),))
149
+ for schema in ("main", "deletions"):
150
+ mode = db.execute(f"PRAGMA {schema}.journal_mode").fetchone()[0]
151
+ if mode != "delete":
152
+ raise StorageError(
153
+ "Atomic memory/deletion transactions require DELETE journal mode"
154
+ )
155
+ db.execute(f"PRAGMA {schema}.synchronous=FULL")
156
+ db.execute(f"PRAGMA {schema}.secure_delete=ON")
157
+ db.execute("BEGIN IMMEDIATE")
158
+ yield db
159
+ db.execute("COMMIT")
160
+ except sqlite3.Error:
161
+ if db is not None and db.in_transaction:
162
+ db.rollback()
163
+ raise StorageError("Memory database unavailable or corrupt") from None
164
+ except BaseException:
165
+ if db is not None and db.in_transaction:
166
+ db.rollback()
167
+ raise
168
+ finally:
169
+ if db is not None:
170
+ db.close()
171
+
172
+ @staticmethod
173
+ def _scope(scope):
174
+ if not isinstance(scope, Scope):
175
+ raise MemoryIntegrityError("Every memory operation requires an explicit Scope")
176
+ return encode(scope)
177
+
178
+ def _maintain(self, db):
179
+ now = self.clock()
180
+ aware_time("clock", now)
181
+ stamp = now.timestamp()
182
+ meta = db.execute("SELECT * FROM meta").fetchall()
183
+ authority = db.execute("SELECT * FROM deletions.authority").fetchall()
184
+ if len(meta) != 1 or len(authority) != 1 or meta[0]["lineage"] != authority[0]["lineage"]:
185
+ raise MemoryIntegrityError("Deletion authority does not match this memory store")
186
+ meta = meta[0]
187
+ if stamp < meta["last_clock"]:
188
+ raise MemoryConflict("Memory clock moved backwards")
189
+ maximum, count = db.execute(
190
+ "SELECT coalesce(max(seq),0),count(*) FROM deletions.events"
191
+ ).fetchone()
192
+ if maximum != count or maximum < meta["applied"]:
193
+ raise MemoryIntegrityError("Deletion authority is incomplete or rolled back")
194
+ for event in db.execute(
195
+ "SELECT * FROM deletions.events WHERE seq>? ORDER BY seq", (meta["applied"],)
196
+ ).fetchall():
197
+ self._erase(db, event["scope"], event["kind"], event["key"])
198
+ db.execute(
199
+ "UPDATE meta SET applied=(SELECT coalesce(max(seq),0) FROM deletions.events),"
200
+ "last_clock=?",
201
+ (stamp,),
202
+ )
203
+ for table, kind in (("turns", "turn"), ("pins", "pin")):
204
+ expired = db.execute(
205
+ f"SELECT scope,key FROM {table} WHERE expires IS NOT NULL AND expires<=?", (stamp,)
206
+ ).fetchall()
207
+ for row in expired:
208
+ self._delete(db, row["scope"], kind, row["key"])
209
+ for row in db.execute(
210
+ "SELECT scope FROM summaries WHERE expires IS NOT NULL AND expires<=?", (stamp,)
211
+ ).fetchall():
212
+ self._changed(db, row["scope"])
213
+ db.execute("DELETE FROM replay WHERE expires<=?", (stamp,))
214
+ return now
215
+
216
+ def _active(self, db, scope, expected=None):
217
+ row = db.execute("SELECT * FROM scopes WHERE scope=?", (scope,)).fetchone()
218
+ if row is None:
219
+ raise MemoryConflict("Memory scope does not exist")
220
+ if row["deleted"]:
221
+ raise MemoryConflict("Memory scope is permanently deleted")
222
+ if expected is not None:
223
+ integer_value("expected_revision", expected)
224
+ if row["revision"] != expected:
225
+ raise MemoryConflict("Memory scope revision changed")
226
+ return row["revision"]
227
+
228
+ def _changed(self, db, scope, turn_id=None):
229
+ db.execute("UPDATE scopes SET revision=revision+1 WHERE scope=?", (scope,))
230
+ summary = db.execute("SELECT * FROM summaries WHERE scope=?", (scope,)).fetchone()
231
+ # A summary survives only appends of turns it does not cover; reads re-verify lineage.
232
+ if (
233
+ summary is not None
234
+ and turn_id is not None
235
+ and turn_id not in self._read("summary", summary).covered_turn_ids
236
+ ):
237
+ summary = None
238
+ if summary is not None:
239
+ db.execute("DELETE FROM summaries WHERE scope=?", (scope,))
240
+ db.execute("DELETE FROM replay WHERE scope=?", (scope,))
241
+ if turn_id is not None:
242
+ db.execute("DELETE FROM chunks WHERE scope=? AND turn_id=?", (scope, turn_id))
243
+ db.execute("DELETE FROM coverage WHERE scope=? AND turn_id=?", (scope, turn_id))
244
+ for row in db.execute(
245
+ "SELECT key,payload FROM pins WHERE scope=?", (scope,)
246
+ ).fetchall():
247
+ pin = unpack("pin", row["payload"])
248
+ if pin.source and pin.source.turn_id == turn_id:
249
+ # Tombstone derived pins as well, so an older backup cannot revive them.
250
+ self._delete(db, scope, "pin", row["key"])
251
+
252
+ def _erase(self, db, scope, kind, key):
253
+ if kind not in ("scope", "turn", "pin"):
254
+ raise MemoryIntegrityError("Unknown deletion record")
255
+ if kind == "scope":
256
+ db.execute("INSERT OR IGNORE INTO scopes VALUES(?,0,1)", (scope,))
257
+ for table in (
258
+ "turns",
259
+ "pins",
260
+ "summaries",
261
+ "chunks",
262
+ "coverage",
263
+ "replay",
264
+ "operations",
265
+ ):
266
+ db.execute(f"DELETE FROM {table} WHERE scope=?", (scope,))
267
+ db.execute("UPDATE scopes SET deleted=1,revision=revision+1 WHERE scope=?", (scope,))
268
+ else:
269
+ table = "turns" if kind == "turn" else "pins"
270
+ db.execute(f"DELETE FROM {table} WHERE scope=? AND key=?", (scope, key))
271
+ self._changed(db, scope, key if kind == "turn" else None)
272
+
273
+ def _delete(self, db, scope, kind, key):
274
+ db.execute(
275
+ "INSERT INTO deletions.events(scope,kind,key) SELECT ?,?,? WHERE NOT EXISTS"
276
+ "(SELECT 1 FROM deletions.events WHERE scope=? AND kind=? AND key=?)",
277
+ (scope, kind, key, scope, kind, key),
278
+ )
279
+ self._erase(db, scope, kind, key)
280
+ db.execute("UPDATE meta SET applied=(SELECT coalesce(max(seq),0) FROM deletions.events)")
281
+
282
+ @staticmethod
283
+ def _not_deleted(db, scope, kind, key):
284
+ if db.execute(
285
+ "SELECT 1 FROM deletions.events WHERE scope=? AND kind=? AND key=?", (scope, kind, key)
286
+ ).fetchone():
287
+ raise MemoryConflict("Deleted identifiers cannot be reused")
288
+
289
+ def create_scope(self, scope):
290
+ scope = self._scope(scope)
291
+ with self._transaction() as db:
292
+ self._maintain(db)
293
+ self._not_deleted(db, scope, "scope", "")
294
+ db.execute("INSERT OR IGNORE INTO scopes VALUES(?,0,0)", (scope,))
295
+ return self._active(db, scope)
296
+
297
+ def put_turn(self, turn, *, operation_id, expected_revision, expires_at=None):
298
+ if not isinstance(turn, Turn):
299
+ raise MemoryIntegrityError("Ingestion requires a validated Turn")
300
+ ChatRequest(turn.messages) # Complete tool-call pairs must be ingested atomically.
301
+ text_value("operation_id", operation_id)
302
+ integer_value("expected_revision", expected_revision)
303
+ if expires_at is not None:
304
+ aware_time("expires_at", expires_at)
305
+ payload = encode(turn)
306
+ if len(payload.encode()) > 1_000_000:
307
+ raise MemoryIntegrityError("Turn exceeds persistent payload limit")
308
+ scope = self._scope(turn.scope)
309
+ operation_hash = digest([plain(turn), expires_at])
310
+ with self._transaction() as db:
311
+ now = self._maintain(db)
312
+ self._active(db, scope)
313
+ old_op = db.execute(
314
+ "SELECT * FROM operations WHERE scope=? AND key=?", (scope, operation_id)
315
+ ).fetchone()
316
+ if old_op:
317
+ if old_op["digest"] != operation_hash:
318
+ raise MemoryConflict("Idempotency key reused with different input")
319
+ return old_op["result"]
320
+ self._active(db, scope, expected_revision)
321
+ self._not_deleted(db, scope, "turn", turn.turn_id)
322
+ if expires_at is not None and expires_at <= now:
323
+ raise MemoryIntegrityError("Cannot ingest already expired content")
324
+ old = db.execute(
325
+ "SELECT * FROM turns WHERE scope=? AND key=?", (scope, turn.turn_id)
326
+ ).fetchone()
327
+ if turn.revision != (old["revision"] + 1 if old else 1):
328
+ raise MemoryConflict("Turn revision must advance exactly once")
329
+ count, size = db.execute(
330
+ "SELECT count(*),coalesce(sum(length(CAST(payload AS BLOB))),0) "
331
+ "FROM turns WHERE scope=?",
332
+ (scope,),
333
+ ).fetchone()
334
+ if (
335
+ count + (old is None) > MAX_TURNS
336
+ or size + len(payload.encode()) - (len(old["payload"].encode()) if old else 0)
337
+ > MAX_BYTES
338
+ ):
339
+ raise MemoryIntegrityError("Memory scope exceeds retention capacity")
340
+ position = (
341
+ old["position"]
342
+ if old
343
+ else db.execute(
344
+ "SELECT coalesce(max(position),0)+1 FROM turns WHERE scope=?", (scope,)
345
+ ).fetchone()[0]
346
+ )
347
+ db.execute(
348
+ "INSERT INTO turns VALUES(?,?,?,?,?,?,?) ON CONFLICT(scope,key) DO UPDATE SET "
349
+ "revision=excluded.revision,payload=excluded.payload,digest=excluded.digest,"
350
+ "expires=excluded.expires",
351
+ (
352
+ scope,
353
+ turn.turn_id,
354
+ position,
355
+ turn.revision,
356
+ payload,
357
+ digest(turn),
358
+ expires_at.timestamp() if expires_at else None,
359
+ ),
360
+ )
361
+ self._changed(db, scope, turn.turn_id)
362
+ revision = self._active(db, scope)
363
+ db.execute(
364
+ "INSERT INTO operations VALUES(?,?,?,?)",
365
+ (scope, operation_id, operation_hash, revision),
366
+ )
367
+ return revision
368
+
369
+ def put_pin(self, pin, *, expected_revision):
370
+ integer_value("expected_revision", expected_revision)
371
+ if not isinstance(pin, Pin) or len(encode(pin).encode()) > 65536:
372
+ raise MemoryIntegrityError("Invalid or oversized persistent pin")
373
+ scope = self._scope(pin.scope)
374
+ with self._transaction() as db:
375
+ now = self._maintain(db)
376
+ self._active(db, scope, expected_revision)
377
+ self._not_deleted(db, scope, "pin", pin.key)
378
+ if pin.expires_at and pin.expires_at <= now:
379
+ raise MemoryIntegrityError("Cannot persist expired pin")
380
+ old = db.execute(
381
+ "SELECT revision FROM pins WHERE scope=? AND key=?", (scope, pin.key)
382
+ ).fetchone()
383
+ if pin.revision != (old[0] + 1 if old else 1):
384
+ raise MemoryConflict("Pin revision conflict")
385
+ if (
386
+ old is None
387
+ and db.execute("SELECT count(*) FROM pins WHERE scope=?", (scope,)).fetchone()[0]
388
+ >= 256
389
+ ):
390
+ raise MemoryIntegrityError("Pin limit exceeded")
391
+ if pin.source:
392
+ source_row = db.execute(
393
+ "SELECT * FROM turns WHERE scope=? AND key=?", (scope, pin.source.turn_id)
394
+ ).fetchone()
395
+ if source_row is None:
396
+ raise MemoryConflict("Pin source no longer exists")
397
+ pin.source.extract(self._read("turn", source_row))
398
+ db.execute(
399
+ "INSERT INTO pins VALUES(?,?,?,?,?,?) ON CONFLICT(scope,key) DO UPDATE SET "
400
+ "revision=excluded.revision,payload=excluded.payload,digest=excluded.digest,"
401
+ "expires=excluded.expires",
402
+ (
403
+ scope,
404
+ pin.key,
405
+ pin.revision,
406
+ encode(pin),
407
+ digest(pin),
408
+ pin.expires_at.timestamp() if pin.expires_at else None,
409
+ ),
410
+ )
411
+ self._changed(db, scope)
412
+ return self._active(db, scope)
413
+
414
+ @staticmethod
415
+ def _read(kind, row):
416
+ value = unpack(kind, row["payload"])
417
+ if digest(value) != row["digest"]:
418
+ raise MemoryIntegrityError("Memory payload digest mismatch")
419
+ if "scope" in row.keys() and encode(value.scope) != row["scope"]:
420
+ raise MemoryIntegrityError("Persisted memory scope mismatch")
421
+ if kind in ("turn", "pin") and (
422
+ (value.turn_id if kind == "turn" else value.key) != row["key"]
423
+ or value.revision != row["revision"]
424
+ ):
425
+ raise MemoryIntegrityError("Persisted memory identity mismatch")
426
+ return value
427
+
428
+ def _snapshot(self, db, scope, now):
429
+ encoded = self._scope(scope)
430
+ revision = self._active(db, encoded)
431
+ turns = tuple(
432
+ self._read("turn", r)
433
+ for r in db.execute("SELECT * FROM turns WHERE scope=? ORDER BY position", (encoded,))
434
+ )
435
+ pins = KeyedPins(
436
+ scope,
437
+ tuple(
438
+ p
439
+ for r in db.execute("SELECT * FROM pins WHERE scope=? ORDER BY key", (encoded,))
440
+ if (p := self._read("pin", r)).is_active(now)
441
+ ),
442
+ )
443
+ row = db.execute("SELECT * FROM summaries WHERE scope=?", (encoded,)).fetchone()
444
+ summary = self._read("summary", row) if row else None
445
+ if summary and not summary.matches(
446
+ tuple(t for t in turns if t.turn_id in summary.covered_turn_ids), scope, now
447
+ ):
448
+ raise MemoryIntegrityError("Persisted summary lineage mismatch")
449
+ epoch = db.execute("SELECT epoch FROM meta").fetchone()[0]
450
+ identifier = digest(
451
+ [epoch, encoded, revision, [t.content_hash for t in turns], pins, summary]
452
+ )
453
+ return MemorySnapshot(scope, revision, identifier, turns, pins, summary, now)
454
+
455
+ def snapshot(self, scope):
456
+ with self._transaction() as db:
457
+ now = self._maintain(db)
458
+ return self._snapshot(db, scope, now)
459
+
460
+ def _current(self, db, snapshot, now):
461
+ if not isinstance(snapshot, MemorySnapshot):
462
+ raise MemoryIntegrityError("Expected a memory snapshot")
463
+ current = self._snapshot(db, snapshot.scope, now)
464
+ if current != snapshot and current.snapshot_id != snapshot.snapshot_id:
465
+ raise MemoryConflict("Memory snapshot is stale")
466
+ return current
467
+
468
+ def assert_current(self, snapshot):
469
+ with self._transaction() as db:
470
+ return self._current(db, snapshot, self._maintain(db))
471
+
472
+ def delete(self, scope, *, kind, key="", expected_revision):
473
+ integer_value("expected_revision", expected_revision)
474
+ if kind not in ("scope", "turn", "pin") or (kind == "scope" and key):
475
+ raise MemoryIntegrityError("Invalid deletion target")
476
+ if kind != "scope":
477
+ text_value("key", key)
478
+ encoded = self._scope(scope)
479
+ with self._transaction() as db:
480
+ self._maintain(db)
481
+ self._active(db, encoded, expected_revision)
482
+ self._delete(db, encoded, kind, key)
483
+
484
+ def put_summary(self, snapshot, summary):
485
+ if not isinstance(summary, SummarySnapshot) or len(encode(summary).encode()) > 256000:
486
+ raise MemoryIntegrityError("Invalid or oversized summary")
487
+ with self._transaction() as db:
488
+ current = self._current(db, snapshot, self._maintain(db))
489
+ history = tuple(t for t in current.history if t.turn_id in summary.covered_turn_ids)
490
+ if not summary.matches(history, current.scope, current.at):
491
+ raise MemoryConflict("Summary lineage or expiry does not match source")
492
+ encoded = self._scope(current.scope)
493
+ self._changed(db, encoded)
494
+ db.execute(
495
+ "INSERT INTO summaries VALUES(?,?,?,?)",
496
+ (
497
+ encoded,
498
+ encode(summary),
499
+ digest(summary),
500
+ summary.expires_at.timestamp() if summary.expires_at else None,
501
+ ),
502
+ )
503
+
504
+ def index_batch(self, snapshot, config=None, *, batch_size=32, cancellation=None):
505
+ config = RetrievalConfig() if config is None else config
506
+ integer_value("batch_size", batch_size, minimum=1)
507
+ validate_cancellation(cancellation)
508
+ raise_if_cancelled(cancellation)
509
+ if batch_size > 256 or not isinstance(config, RetrievalConfig):
510
+ raise MemoryIntegrityError("Invalid bounded indexing configuration")
511
+ profile = digest(config)
512
+ with self._transaction() as db:
513
+ current = self._current(db, snapshot, self._maintain(db))
514
+ scope = self._scope(current.scope)
515
+ # Keep one active profile per scope, rather than accumulating unbounded caches.
516
+ for table in ("coverage", "chunks"):
517
+ db.execute(f"DELETE FROM {table} WHERE scope=? AND profile<>?", (scope, profile))
518
+ completed = {
519
+ r["turn_id"]: r["source_hash"]
520
+ for r in db.execute(
521
+ "SELECT * FROM coverage WHERE scope=? AND profile=?", (scope, profile)
522
+ )
523
+ }
524
+ pending = [t for t in current.history if completed.get(t.turn_id) != t.content_hash]
525
+ changed = pending[:batch_size]
526
+ for turn in changed:
527
+ raise_if_cancelled(cancellation)
528
+ maximum = sum(
529
+ (len(m.content) + config.chunk_characters - config.overlap_characters - 1)
530
+ // (config.chunk_characters - config.overlap_characters)
531
+ for m in turn.messages
532
+ )
533
+ count = db.execute(
534
+ "SELECT count(*) FROM chunks WHERE scope=?", (scope,)
535
+ ).fetchone()[0]
536
+ if count + maximum > MAX_CHUNKS:
537
+ raise MemoryIntegrityError("Persistent chunk index exceeds bound")
538
+ # Preserve the exact 0.8.0 call form when no token is supplied.
539
+ chunks = (
540
+ chunk_turns((turn,), config)
541
+ if cancellation is None
542
+ else chunk_turns((turn,), config, cancellation=cancellation)
543
+ )
544
+ db.execute("DELETE FROM chunks WHERE scope=? AND turn_id=?", (scope, turn.turn_id))
545
+ db.executemany(
546
+ "INSERT INTO chunks VALUES(?,?,?,?,?)",
547
+ [(scope, profile, turn.turn_id, c.chunk_id, encode(c)) for c in chunks],
548
+ )
549
+ db.execute(
550
+ "INSERT INTO coverage VALUES(?,?,?,?,?) ON CONFLICT(scope,profile,turn_id) "
551
+ "DO UPDATE SET source_hash=excluded.source_hash,"
552
+ "chunks_hash=excluded.chunks_hash",
553
+ (
554
+ scope,
555
+ profile,
556
+ turn.turn_id,
557
+ turn.content_hash,
558
+ digest(sorted((c.chunk_id, digest(c)) for c in chunks)),
559
+ ),
560
+ )
561
+ raise_if_cancelled(cancellation)
562
+ return {
563
+ "status": "READY" if len(pending) <= batch_size else "BUILDING",
564
+ "processed": len(changed),
565
+ "remaining": max(0, len(pending) - batch_size),
566
+ "profile": profile,
567
+ }
568
+
569
+ def chunk_index(self, snapshot, config=None):
570
+ config = RetrievalConfig() if config is None else config
571
+ if not isinstance(config, RetrievalConfig):
572
+ raise MemoryIntegrityError("Invalid indexing configuration")
573
+ profile = digest(config)
574
+ with self._transaction() as db:
575
+ current = self._current(db, snapshot, self._maintain(db))
576
+ scope = self._scope(current.scope)
577
+ chunks = []
578
+ for turn in current.history:
579
+ coverage = db.execute(
580
+ "SELECT * FROM coverage WHERE scope=? AND profile=? AND turn_id=?",
581
+ (scope, profile, turn.turn_id),
582
+ ).fetchone()
583
+ if coverage is None or coverage["source_hash"] != turn.content_hash:
584
+ raise MemoryConflict("Chunk index is not ready for this snapshot")
585
+ values = tuple(
586
+ unpack("chunk", r[0])
587
+ for r in db.execute(
588
+ "SELECT payload FROM chunks WHERE scope=? AND profile=? "
589
+ "AND turn_id=? ORDER BY key",
590
+ (scope, profile, turn.turn_id),
591
+ )
592
+ )
593
+ if (
594
+ digest(sorted((c.chunk_id, digest(c)) for c in values))
595
+ != coverage["chunks_hash"]
596
+ ):
597
+ raise MemoryIntegrityError("Persistent chunk index is incomplete or corrupt")
598
+ for chunk in values:
599
+ chunk.validate_source(turn)
600
+ chunks.extend(values)
601
+ return ChunkIndex(history_digest(current.history), digest(config), tuple(chunks))
602
+
603
+ def cache_put(self, snapshot, key, payload, *, ttl_seconds=3600):
604
+ text_value("cache_key", key)
605
+ integer_value("ttl_seconds", ttl_seconds, minimum=1)
606
+ if (
607
+ ttl_seconds > 86400
608
+ or len(encode(payload).encode()) > 1_000_000
609
+ or not isinstance(payload, dict)
610
+ ):
611
+ raise MemoryIntegrityError("Invalid bounded replay payload")
612
+ with self._transaction() as db:
613
+ current = self._current(db, snapshot, self._maintain(db))
614
+ scope = self._scope(current.scope)
615
+ if (
616
+ db.execute("SELECT count(*) FROM replay WHERE scope=?", (scope,)).fetchone()[0]
617
+ >= 100
618
+ ):
619
+ raise MemoryIntegrityError("Replay cache entry limit reached")
620
+ db.execute(
621
+ "INSERT INTO replay VALUES(?,?,?,?,?,?) ON CONFLICT(scope,key) DO UPDATE SET "
622
+ "snapshot=excluded.snapshot,payload=excluded.payload,digest=excluded.digest,"
623
+ "expires=excluded.expires",
624
+ (
625
+ scope,
626
+ key,
627
+ current.snapshot_id,
628
+ encode(payload),
629
+ digest(payload),
630
+ current.at.timestamp() + ttl_seconds,
631
+ ),
632
+ )
633
+
634
+ def cache_get(self, snapshot, key):
635
+ text_value("cache_key", key)
636
+ with self._transaction() as db:
637
+ current = self._current(db, snapshot, self._maintain(db))
638
+ row = db.execute(
639
+ "SELECT * FROM replay WHERE scope=? AND key=? AND snapshot=?",
640
+ (self._scope(current.scope), key, current.snapshot_id),
641
+ ).fetchone()
642
+ if row is None:
643
+ return None
644
+ value = decode(row["payload"])
645
+ if digest(value) != row["digest"]:
646
+ raise MemoryIntegrityError("Replay payload is corrupt")
647
+ return value
648
+
649
+ def assemble(
650
+ self,
651
+ scope,
652
+ question,
653
+ budget,
654
+ *,
655
+ system,
656
+ retrieval_config=None,
657
+ cancellation=None,
658
+ summary_prefix=False,
659
+ **options,
660
+ ):
661
+ """Assemble the current snapshot. `summary_prefix=True` opts into rolling reuse."""
662
+ if type(summary_prefix) is not bool:
663
+ raise ContractError("summary_prefix must be an explicit boolean")
664
+ retrieval_config = RetrievalConfig() if retrieval_config is None else retrieval_config
665
+ validate_cancellation(cancellation)
666
+ raise_if_cancelled(cancellation)
667
+ snapshot = self.snapshot(scope)
668
+ while (
669
+ self.index_batch(snapshot, retrieval_config, cancellation=cancellation)["status"]
670
+ != "READY"
671
+ ):
672
+ raise_if_cancelled(cancellation)
673
+ result = assemble_context(
674
+ snapshot.history,
675
+ question,
676
+ snapshot.pins,
677
+ budget,
678
+ system=system,
679
+ at=snapshot.at,
680
+ retrieval_config=retrieval_config,
681
+ chunk_index=self.chunk_index(snapshot, retrieval_config),
682
+ summary_policy=(
683
+ None
684
+ if snapshot.summary is None
685
+ else (PrefixSummaryPolicy if summary_prefix else FrozenSummaryPolicy)(
686
+ snapshot.summary
687
+ )
688
+ ),
689
+ cancellation=cancellation,
690
+ **options,
691
+ )
692
+ self.assert_current(snapshot)
693
+ raise_if_cancelled(cancellation)
694
+ return snapshot, result
695
+
696
+ def deletion_watermark(self):
697
+ with self._transaction() as db:
698
+ self._maintain(db)
699
+ row = db.execute("SELECT lineage,applied FROM meta").fetchone()
700
+ return {"lineage": row["lineage"], "minimum_deletion_seq": row["applied"]}
701
+
702
+ def prune(self):
703
+ """Commit pending expiry/deletion cleanup even when no context is requested."""
704
+ return self.deletion_watermark()
705
+
706
+ @staticmethod
707
+ def _copy(source, destination):
708
+ source = private_file(source)
709
+ destination = private_file(destination, create=True, exclusive=True)
710
+ start = time.monotonic()
711
+
712
+ def progress(*_):
713
+ if time.monotonic() - start > 10:
714
+ raise StorageError("Backup deadline exceeded; partial destination is not usable")
715
+
716
+ try:
717
+ with closing(connect(Path(source).absolute().as_uri() + "?mode=ro", uri=True)) as src:
718
+ with closing(connect(destination)) as dst:
719
+ src.backup(dst, pages=128, progress=progress, sleep=0.01)
720
+ except sqlite3.Error:
721
+ raise StorageError("Cannot copy validated memory database") from None
722
+ return destination
723
+
724
+ def backup(self, destination):
725
+ self.deletion_watermark()
726
+ return self._copy(self.path, destination)
727
+
728
+ @classmethod
729
+ def restore(cls, backup, destination, *, deletion_path, minimum_deletion_seq, clock=None):
730
+ integer_value("minimum_deletion_seq", minimum_deletion_seq)
731
+ private_file(backup)
732
+ private_file(deletion_path)
733
+ try:
734
+ with closing(
735
+ connect(Path(backup).absolute().as_uri() + "?mode=ro", uri=True)
736
+ ) as source:
737
+ if source.execute("PRAGMA user_version").fetchone()[0] != 1:
738
+ raise MemoryIntegrityError("Unsupported backup schema")
739
+ records = source.execute("SELECT lineage,applied FROM meta").fetchall()
740
+ if len(records) != 1:
741
+ raise MemoryIntegrityError("Invalid backup metadata")
742
+ lineage, applied = records[0]
743
+ with closing(
744
+ connect(Path(deletion_path).absolute().as_uri() + "?mode=ro", uri=True)
745
+ ) as authority:
746
+ known = authority.execute("SELECT lineage FROM authority").fetchall()
747
+ maximum, count = authority.execute(
748
+ "SELECT coalesce(max(seq),0),count(*) FROM events"
749
+ ).fetchone()
750
+ if (
751
+ known != [(lineage,)]
752
+ or maximum != count
753
+ or maximum < max(applied, minimum_deletion_seq)
754
+ ):
755
+ raise MemoryIntegrityError("Deletion authority is missing, mismatched or stale")
756
+ except (sqlite3.Error, ValueError, TypeError):
757
+ raise MemoryIntegrityError("Cannot validate recovery authority") from None
758
+ destination = cls._copy(backup, destination)
759
+ # Constructor refuses absent/wrong/degraded deletion authority before any public read.
760
+ restored = cls(destination, deletion_path=deletion_path, clock=clock)
761
+ if restored.deletion_watermark()["minimum_deletion_seq"] < minimum_deletion_seq:
762
+ raise MemoryIntegrityError(
763
+ "Deletion authority is older than the required recovery watermark"
764
+ )
765
+ with restored._transaction() as db:
766
+ db.execute("UPDATE meta SET epoch=?", (uuid.uuid4().hex,))
767
+ for table in ("chunks", "coverage", "summaries", "replay"):
768
+ db.execute(f"DELETE FROM {table}")
769
+ db.execute("UPDATE scopes SET revision=revision+1")
770
+ return restored