Skip to content

feat(store): persistent SQLite event log + JSON-file artifact store#232

Merged
dgenio merged 5 commits into
mainfrom
claude/triage-issues-7Pd1o
May 17, 2026
Merged

feat(store): persistent SQLite event log + JSON-file artifact store#232
dgenio merged 5 commits into
mainfrom
claude/triage-issues-7Pd1o

Conversation

@dgenio
Copy link
Copy Markdown
Owner

@dgenio dgenio commented May 16, 2026

Lands the store-only slice of the persistent-backends group (#42, #174,
#223). Two new protocol-conformant backends + an EventLog lifecycle
contract refinement; zero behavior change for existing in-memory users.

Why

The context engine's event log and artifact store are in-memory only —
session data vanishes on process exit. Agents that persist across
restarts or need auditable history require durable backends. This PR
delivers the first two: a SQLite event log and a filesystem artifact
store, both conforming to the existing protocol interfaces.

What changed

SqliteEventLog + shared _sqlite_base.py (#174, #223)

  • store/_sqlite_base.py: connection helper (WAL, foreign_keys=ON,
    parent-dir auto-create) + apply_migrations() driven by a
    _contextweaver_schema_version table. Designed for reuse by the
    remaining SQLite stores in epic [epic] SQLite persistent stores #174.
  • store/sqlite_event_log.py: implements every EventLog protocol
    method against a single-process SQLite file. Append-only writes via
    append() (duplicate ids → DuplicateItemError); insertion order
    preserved by an auto-increment ordinal column; indexes on kind
    and parent_id. Round-trips every ContextItem field including JSON
    metadata and nested ArtifactRef.
  • [sqlite] extras placeholder in pyproject.toml (stdlib sqlite3
    only; placeholder for a future aiosqlite async variant).

JsonFileArtifactStore (#42)

  • store/json_file_artifacts.py: filesystem-backed ArtifactStore.
    Layout: {base_dir}/{handle}.data + {base_dir}/{handle}.json.
    Auto-creates base_dir; rejects handles containing path separators,
    .., ., or null bytes. Re-instantiating against an existing
    directory recovers refs from metadata files.
  • store/artifacts.py: extracts drilldown selector dispatch (head /
    lines / json_keys / rows) to a shared _apply_selector()
    helper so both backends produce identical output.

EventLog lifecycle

  • store/protocols.py: EventLog protocol now requires close(),
    __enter__, __exit__.
  • store/event_log.py: InMemoryEventLog.close() is a no-op; existing
    callers unaffected. with SqliteEventLog(path) as log: is the
    recommended idiom.

Review-driven fixes

  • SqliteEventLog.query(): since applied to the full insertion-ordered
    log before kinds filter, matching InMemoryEventLog semantics.
  • JsonFileArtifactStore: path-traversal validation moved into
    _meta_path / _data_path so every public method (not just put)
    rejects unsafe handles.
  • StoreClosedError (new exception in exceptions.py): replaces bare
    RuntimeError for use-after-close, keeping the ContextWeaverError
    hierarchy consistent per AGENTS.md.
  • JsonFileArtifactStore.list_refs(): catches TypeError from
    ArtifactRef.from_dict when a .json file is valid JSON but not a
    mapping.
  • Top-level __init__.py: re-exports SqliteEventLog,
    JsonFileArtifactStore, and StoreClosedError.
  • _sqlite_base.py: rollback in apply_migrations uses
    contextlib.suppress for safety if the connection is lost.

Non-goals (out of scope)

Test plan

make ci  →  all 6 targets passed
pytest   →  1150 passed, 1 skipped
mypy     →  0 issues / 77 files
ruff     →  clean (format + check)

65 new tests: 32 SqliteEventLog, 29 JsonFileArtifactStore, 2 in-memory
lifecycle, 2 parity tests (SQLite vs in-memory query semantics).

Risks

Low. Both backends are opt-in (imported explicitly), single-process, and
have no effect on the default in-memory code path. The EventLog
protocol addition (close / context manager) is backwards-compatible
since the in-memory implementation's close() is a no-op.

Closes #42
Closes #223
Refs #174

Lands the store-only slice of the persistent-backends group (#42, #174,
#223). Two new protocol-conformant backends + an EventLog lifecycle
contract refinement; zero behavior change for existing in-memory users.

#223 — SqliteEventLog + shared _sqlite_base.py
  - store/_sqlite_base.py: connection helper (WAL, foreign_keys=ON, parent
    dir auto-create) + apply_migrations() driven by a
    _contextweaver_schema_version table. The other 3 SQLite stores in
    epic #174 reuse this.
  - store/sqlite_event_log.py: implements every EventLog protocol method
    against a single-process SQLite file. Append-only writes via append()
    (duplicate ids raise DuplicateItemError); insertion order preserved
    by an auto-increment ordinal column; indexes on kind and parent_id.
    Round-trips every ContextItem field (metadata + artifact_ref via
    JSON columns).
  - [sqlite] extras placeholder in pyproject.toml (stdlib sqlite3 only;
    placeholder exists so a future aiosqlite variant can opt in without
    changing the install surface).

#42 — JsonFileArtifactStore
  - store/json_file_artifacts.py: filesystem-backed ArtifactStore.
    Layout {base_dir}/{handle}.data + {base_dir}/{handle}.json. Auto-
    creates base_dir; rejects handles containing path separators / ..
    / null bytes. Re-instantiating recovers refs from existing metadata
    files.
  - store/artifacts.py: extracts the drilldown selector dispatch
    (head / lines / json_keys / rows) to a module-private
    _apply_selector() helper so both backends share one implementation.
    Behaviour preserved bit-for-bit; existing tests untouched.

EventLog lifecycle (Mode B addition for clean SQLite cleanup)
  - store/protocols.py: EventLog protocol now requires close(),
    __enter__, __exit__.
  - store/event_log.py: InMemoryEventLog.close() is a no-op; existing
    callers continue to work without changes. `with SqliteEventLog(path)
    as log:` is the recommended idiom for the new backend.

Tests: 63 new (32 SqliteEventLog, 29 JsonFileArtifactStore, 2 in-memory
lifecycle). New modules at 87–100% coverage. Full suite 1011 passed,
8 skipped.

Verification
  ruff format --check src/ tests/ examples/ scripts/   - clean
  ruff check src/ tests/ examples/ scripts/            - clean
  mypy src/                                            - 0 issues / 67 files
  pytest --cov=contextweaver -q                        - 1011 passed, 8 skipped
  make example, make demo, make scorecard-check        - clean
  make llms-check                                      - up to date

Closes #42
Closes #223
Refs #174

https://claude.ai/code/session_01ADTmGUqM66tqnGMqrefy4e
Copilot AI review requested due to automatic review settings May 16, 2026 16:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds persistent store backends for the store layer while preserving the existing in-memory implementations.

Changes:

  • Adds SqliteEventLog with shared SQLite connection/migration scaffolding.
  • Adds JsonFileArtifactStore and shares artifact drilldown selector logic.
  • Extends EventLog lifecycle support with close() and context manager methods, plus tests and docs/changelog updates.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/contextweaver/store/_sqlite_base.py Adds reusable SQLite connection and migration helpers.
src/contextweaver/store/sqlite_event_log.py Adds SQLite-backed EventLog implementation.
src/contextweaver/store/json_file_artifacts.py Adds filesystem-backed artifact persistence.
src/contextweaver/store/artifacts.py Extracts shared drilldown selector helper.
src/contextweaver/store/event_log.py Adds no-op lifecycle methods to in-memory event log.
src/contextweaver/store/protocols.py Extends EventLog protocol with lifecycle methods.
src/contextweaver/store/__init__.py Re-exports new store backends.
tests/test_store_sqlite_event_log.py Adds SQLite event log coverage.
tests/test_store_json_file_artifacts.py Adds JSON-file artifact store coverage.
tests/test_store_event_log.py Adds in-memory event log lifecycle tests.
pyproject.toml Adds placeholder [sqlite] optional extra.
CHANGELOG.md Documents new persistent stores and lifecycle contract.
AGENTS.md Updates agent-facing module map for new store backends.

Comment thread src/contextweaver/store/sqlite_event_log.py Outdated
Comment thread src/contextweaver/store/json_file_artifacts.py
Comment thread src/contextweaver/store/sqlite_event_log.py Outdated
Comment thread src/contextweaver/store/json_file_artifacts.py Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 16, 2026

Benchmark delta (vs main)

Soft regression feedback only — this comment never blocks the PR.
Latency budget: ⚠️ when head > base × 1.3. Accuracy budget: ⚠️ when head < base - 1pp.

Routing summary (single backend × catalog sizes)

size recall@k (head Δ vs base) MRR (head Δ vs base) p99 (ms)
50 ✅ 0.5649 (+0.0000) ✅ 0.4978 (+0.0000) ✅ 0.453 (base 0.463)
83 ✅ 0.3825 (+0.0000) ✅ 0.3242 (+0.0000) ✅ 0.624 (base 0.876)
1000 ✅ 0.1475 (+0.0000) ✅ 0.1456 (+0.0000) ✅ 30.611 (base 31.897)

Per-backend × per-size matrix

backend size recall@k (Δ) MRR (Δ) p99 (ms)
bm25 100 ✅ 0.3825 (+0.0000) ✅ 0.3399 (+0.0000) ✅ 5.797 (base 5.642)
bm25 500 ✅ 0.2250 (+0.0000) ✅ 0.2165 (+0.0000) ✅ 28.868 (base 27.538)
bm25 1000 ✅ 0.1575 (+0.0000) ✅ 0.1525 (+0.0000) ✅ 78.840 (base 78.368)
fuzzy 100 ✅ 0.0000 (+0.0000) ✅ 0.0000 (+0.0000) ✅ 0.000 (base 0.000)
fuzzy 500 ✅ 0.0000 (+0.0000) ✅ 0.0000 (+0.0000) ✅ 0.000 (base 0.000)
fuzzy 1000 ✅ 0.0000 (+0.0000) ✅ 0.0000 (+0.0000) ✅ 0.000 (base 0.000)
tfidf 100 ✅ 0.3825 (+0.0000) ✅ 0.3220 (+0.0000) ✅ 0.911 (base 0.872)
tfidf 500 ✅ 0.2325 (+0.0000) ✅ 0.2314 (+0.0000) ✅ 8.230 (base 8.660)
tfidf 1000 ✅ 0.1475 (+0.0000) ✅ 0.1456 (+0.0000) ✅ 31.190 (base 30.071)

Context pipeline (per scenario)

scenario tokens dropped dedup
large_catalog 1514 (base 1514, Δ+0) 0 (base 0, Δ+0) 0 (base 0, Δ+0)
long_conversation 2548 (base 2548, Δ+0) 0 (base 0, Δ+0) 0 (base 0, Δ+0)
short_conversation 496 (base 496, Δ+0) 0 (base 0, Δ+0) 0 (base 0, Δ+0)
stress_conversation 6651 (base 6651, Δ+0) 7 (base 7, Δ+0) 4 (base 4, Δ+0)

Numbers come from make benchmark / make benchmark-matrix.
Latency is hardware-dependent — treat the markers as a rough guide.
See benchmarks/scorecard.md for the full picture.

claude and others added 4 commits May 16, 2026 22:07
Addresses all 4 Copilot review comments on the persistent-store PR.

1. **SqliteEventLog.query filter order mismatch with InMemoryEventLog**
   (Copilot #1). The SQL path applied `kinds` before `since`, while the
   in-memory path applies `since` to the full insertion-ordered log
   *before* filtering by kind. On mixed-kind logs this gave different
   results for the same `(kinds, since, limit)` triple. Switched to
   pull-all + slice-by-since + filter-by-kind + slice-by-limit, matching
   the in-memory semantics byte-for-byte. The existing
   test_query_combined_filters expected the buggy ordering and was
   updated; a new test_query_kinds_since_matches_in_memory_semantics
   pins parity across four representative (kinds, since, limit) cases
   against an InMemoryEventLog mirror.

2. **JsonFileArtifactStore path traversal**
   (Copilot #2). Handle validation was only enforced inside `put()`,
   leaving `get`, `ref`, `exists`, `delete`, `metadata`, and `drilldown`
   vulnerable to `..`, path separators, and absolute paths (read /
   delete outside `base_dir`). Centralised the validation inside
   `_meta_path` and `_data_path`, so every public method that resolves
   a handle is now safe. Parametrised regression test
   `test_read_path_methods_reject_unsafe_handles` covers all five read /
   mutate methods × six unsafe-handle shapes; a separate
   `test_drilldown_rejects_unsafe_handle` covers the drilldown path.

3. **Bare RuntimeError on store-closed**
   (Copilot #3). AGENTS.md requires public-facing errors to come from
   `contextweaver.exceptions`. Added `StoreClosedError(ContextWeaverError)`
   to the exceptions module and replaced the `RuntimeError` raised by
   `_require_conn` with it. The lifecycle docstring on `close()` now
   references the new exception. `test_use_after_close_raises` updated
   to assert both the specific exception and that it remains catchable
   as `ContextWeaverError`.

4. **list_refs crashes on wrong-shape JSON**
   (Copilot #4). `ArtifactRef.from_dict()` raises `TypeError` (not
   `ValueError`) when given a non-mapping JSON value such as `[]`,
   `null`, or a bare string. `list_refs` only caught
   `json.JSONDecodeError | KeyError | ValueError`, so a wrong-shape
   file crashed the whole listing. Added `TypeError` to the except
   clause and documented the failure-mode taxonomy on the method.
   `test_list_refs_skips_non_mapping_json` covers all three shapes.

Verification:
- ruff format/lint clean
- mypy strict — 0 issues / 67 source files
- pytest -q — 1047 passed, 5 skipped (+36 new tests over baseline)
- make example + make demo clean
- make scorecard-check + make llms-check clean
…7Pd1o

# Conflicts:
#	CHANGELOG.md
#	pyproject.toml
Rich outputs UTF-8 box-drawing characters that fail to decode with
the default cp1252 codec on Windows.  Adding encoding='utf-8' to
subprocess.run matches the PYTHONIOENCODING=utf-8 already set in
the subprocess environment.
- Re-export StoreClosedError, SqliteEventLog, JsonFileArtifactStore from
  contextweaver/__init__.py and __all__
- Use contextlib.suppress for rollback safety in _sqlite_base.py
- Clarify delete() docstring in JsonFileArtifactStore
@dgenio dgenio merged commit d559ecc into main May 17, 2026
4 checks passed
@dgenio dgenio deleted the claude/triage-issues-7Pd1o branch May 17, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[store] SqliteEventLog + shared _sqlite_base.py (first slice of #174) [store] Add JsonFileArtifactStore for filesystem-based artifact persistence

3 participants