@contextq/edge
v0.1.0
Published
ContextQ Edge -- offline-first agent-memory replica (SQLite + sqlite-vec + FTS5) that boots from a Memory Interchange Format (MIF) export and syncs with a parent ContextQ instance via hybrid-logical-clock CRDT reconciliation
Readme
@contextq/edge
T379: an offline-first ContextQ replica. Postgres -> SQLite, pgvector ->
sqlite-vec, Elasticsearch -> FTS5. Boots from a Memory Interchange Format
(MIF v1.0) export and serves hybrid search fully offline; optionally syncs
back to a parent ContextQ instance via the existing MIF export/import HTTP
endpoints, reconciling concurrent edits with a hybrid-logical-clock CRDT.
Standalone package (own package.json/tsconfig.json, same convention as
../mcp/): no import path back into the main server's src/. The MIF wire
format is deliberately designed to be reimplemented by a third party
(../docs/interchange/mif-spec.md), and this package is that third party --
a handful of files under src/mif/ are therefore vendored ports of the
main package's src/utils/tar.ts / mif-canonical.ts /
interchange-keys.service.ts, not shared imports.
Status
- Phase (a) -- read-only offline replica: shipped.
contextq-edge boot <bundle.tar.gz>verifies the bundle's Ed25519 signature + every blob's SHA-256, materializes it into local SQLite, and serves hybrid (FTS5 + optional sqlite-vec ANN) search with zero network calls. - Phase (b) -- offline writes + sync: working prototype. Local writes are
HLC-stamped and appended to an append-only commit log
(
local_write_log);ctx_syncpush/pull reconciles against a parent over the existingPOST /api/interchange/export/.../importendpoints (no server change required). Proven against a real running instance -- seescripts/live-sync-proof.ts. - Phase (c) -- CRDT conflict resolution: working prototype. LWW-register
per scalar field (
src/crdt.ts'slwwMerge), OR-Set for tags (src/crdt.ts'sOrSet). Full design, what's genuinely wired vs. prototype-only, and three interop gotchas discovered while proving the live round-trip:docs/edge-crdt-sync-design.md.
Install
cd edge
npm install
npx tsc --noEmit # typecheck
npm test # 14 unit tests, no network/DB requiredCLI
# Phase (a): boot offline from a MIF export, optionally serve search over HTTP
contextq-edge boot ./mif-export-acme.tar.gz --db ./edge.sqlite
contextq-edge boot ./mif-export-acme.tar.gz --db ./edge.sqlite --serve --port 4000
# Search the local replica (works even with the network off)
contextq-edge search "deployment runbook" --db ./edge.sqlite --workspace acme --limit 5
# Phase (b): author a context entirely offline
contextq-edge create --db ./edge.sqlite --workspace acme --type reference \
--name "Offline note" --content "written on a plane"
# Phase (b): reconcile with a parent instance
contextq-edge sync pull --db ./edge.sqlite --parent-url http://localhost:38200 \
--api-key sk_live_... --workspace acme
contextq-edge sync push --db ./edge.sqlite --parent-url http://localhost:38200 \
--api-key sk_live_...HTTP server (--serve)
POST /search, POST /contexts, PATCH /contexts/:ref,
POST /contexts/:ref/tags, DELETE /contexts/:ref/tags/:tag,
POST /sync/pull, POST /sync/push, GET /health. No auth layer -- this is
a local sidecar for a process on the same machine, the same trust boundary
as talking to the SQLite file directly.
Live sync proof
scripts/live-sync-proof.ts is an ops script (like the main package's
scripts/backfill.ts), not part of npm test -- it needs a real running
ContextQ instance:
PARENT_URL=http://localhost:38200 \
PARENT_API_KEY=sk_live_... \
PARENT_WORKSPACE=acme \
npx tsx scripts/live-sync-proof.tsIt pulls a real export, creates a context entirely offline, pushes it, independently re-exports from the parent to confirm the create landed, edits that same context offline, pushes again, and independently re-exports again to confirm the edit landed in place (not as a duplicate row) -- every step is verified by re-fetching from the parent, not by trusting an HTTP 200.
Known limitations
See docs/edge-crdt-sync-design.md section 3 (scope) and section 5
(discovered interop gotchas) for the full, honest list -- in short: tag/link
sync against the parent is snapshot-granularity (not field-level) because
the parent's MIF export doesn't carry edge's op log; links and chunks are
pull-only; and editing a pre-existing native parent context in place
requires that context to already carry a persisted external_id (one edge
itself created, or one sourced from a connector/prior MIF import) -- MIF
v1.0's idempotency key is not retroactively assignable to an arbitrary
native row from the edge side.
