Portability and maintenance

Long-lived project memory needs a transport format, recoverable snapshots, and bounded cleanup. These operations intentionally cover different subsets of local state.

JSONL export and import

knowl export ./knowl-export.jsonl
knowl import ./knowl-export.jsonl --dry-run
knowl import ./knowl-export.jsonl --on-divergence newer

The checksummed JSONL export is at format version 2. It includes complete item objects in every status, with originRepo, visibility and lifecycleHash written on import as well as read on export. It also includes assertions, evidence and links, file-backed skill files, and tombstones. It excludes knowledge commits, access telemetry, sessions, code indexes, vector embeddings, project configuration, workspace manifests, and workspace membership.

This build reads format versions 1 and 2. A version-1 file imports with ownership defaulted — originRepo null and visibility repo — which is what a file written before those fields existed means. A version it does not recognise is refused rather than imported with the unknown fields dropped.

Import only JSONL that you created or otherwise trust. The checksum detects corruption; it does not authenticate the source or make malicious skill-package paths safe. --dry-run checks the database import plan but returns before skill-package files are validated or written.

After checking the checksum, header, and item records, import supports four divergence policies:

  • newer compares an incoming item with the same ID and different contentHash, then chooses by updatedAt, by version when timestamps tie, and otherwise keeps the local tie.
  • skip keeps the local divergent item.
  • theirs selects the incoming item.
  • fail aborts the import when divergence is found.

Use --dry-run to see wouldApply counts without applying records. Only fail treats a divergence as an import-wide abort condition; the other policies select or skip individual records. When newer or theirs selects an incoming item, Knowl writes supported content and history columns with the incoming contentHash, version, and timestamps without normal reconciliation.

Content and lifecycle diverge independently. contentHash covers title, content, reasoning, source and paths; lifecycleHash covers status, freshness, supersession, originRepo and visibility. An item whose content matches but whose lifecycle does not is metadata-divergent, resolved by the same policy and applied to the lifecycle columns only, leaving contentHash untouched so the next round classifies as identical instead of trading updates. A promotion, retirement or supersession therefore propagates; before this it did not, because content-only comparison called it identical and skipped it. Promotion also advances updatedAt, since newer has nothing to order by otherwise.

After checksum, header, and item validation, non-dry-run database changes apply in one SQL transaction and roll back together on failure. Imported skill-package files are filesystem writes and are not covered by that database rollback.

Tombstones are monotonic. deletedAt only moves forward, whether written locally or received in an import, so replaying an older delete cannot rewind a newer one. Import also consults local tombstones before inserting: an item whose export predates a local delete is not reinstated, and the count is reported as blockedByTombstone rather than folded into identical. A tie favours the item, matching the delete path, so knowledge deliberately re-recorded after a delete still lands. Best-effort vector indexing after import uses only the locally available model.

Garbage collection

knowl gc previews by default:

knowl gc
knowl gc --apply

The default policy:

  • removes exact active duplicates only in fact, state, and goal;
  • archives non-hot active state older than 60 days;
  • protects an item as hot after at least three retrievals or a retrieval within 21 days;
  • compresses archived content after 30 days when it is at least 180 bytes; and
  • removes tombstones after 90 days.

Flags adjust these thresholds, and --ignore-access disregards access heat for stale-state archival. Review the preview before applying it; GC does not infer semantic equivalence across different content.

The forget log

Every destroyed item leaves one append-only row recording what was true at the instant of destruction: the policy that fired, its reason in words, and the retrieval evidence that policy decided against. That makes a collection threshold checkable after the fact — you can ask which items were taken while they were still being retrieved.

knowl forget-log
knowl forget-log --limit 100 --json
knowl forget-log --repo web            # only items owned by that workspace repo
knowl forget-log --prune-days 365

This is a separate table from knowledge_tombstones and it never leaves the machine. A tombstone rides in every portable export and merges by upsert on import, so retrieval numbers there would both publish local telemetry and let a peer's import overwrite this machine's audit trail. Tombstones are pruned after 90 days on every GC run; forget-log rows are kept until --prune-days asks for them to go, because the question they answer arrives months late.

Snapshots, audit, and doctor

knowl snapshot create
knowl snapshot restore .knowl/snapshots/<snapshot>.db --confirm
knowl audit
knowl doctor

Snapshot creation uses SQLite VACUUM INTO and writes a checksum manifest. The manifest is required on restore, not optional: restore verifies its schema version, byte size and SHA-256, copies the snapshot out of the snapshot directory, and re-verifies the copy it is about to read — then checks that copy's own SQLite integrity_check and user_version before any destructive statement runs. Restore also takes a pre-restore snapshot first, and refuses to delete the snapshot it was asked to restore from.

Restore is a partial operation, and which half is which is a decision recorded in src/store/snapshot-tables.ts rather than a side effect of the schema. Restored: knowledge items, assertions, evidence and its links, access telemetry, knowledge commits and the commit-to-item index, skill rows, and embeddings. Preserved at their current values: memory sessions and their events, host bindings, tombstones, MCP call watermarks, the code index, and the drift watermark — each describes the machine and working tree you are on now, not the knowledge. Full-text search indexes rebuild themselves as rows land. A test fails if any table in the schema is missing from that registry, so restore behaviour cannot drift by accident.

A restore runs an audit after committing the restored data. An audit failure is diagnostic and does not roll back that commit. knowl audit itself is read-only and performs a limited set of secret, JSON/status, dangling-row, and FTS checks; it is not a complete reconstruction of every history or evidence invariant.

knowl doctor checks initialization, configuration, guidance, .gitignore, integrity, schema, stale sessions, retrieval, the MCP inventory, agents and lifecycle registration, vector coverage, and workspace health. Warnings mean the project is not reported ready; they should not be treated as an all-clear.

Startup diagnostics

knowl diagnose-startup reports why knowl serve startups were slow: per-phase timings, SQLite contention, stalls and host kills.

knowl diagnose-startup
knowl diagnose-startup --since 12
knowl diagnose-startup --clear

The log lives under the Knowl home rather than in any repository, because the question it answers spans every project on the machine. Each record holds a boot id, PID, elapsed and per-phase timings, Node version, hostname, load average, free memory, and a 16-character hash of the project root — never the path itself, and never environment variables or command-line arguments. On a shared host the path would have told every local account which projects you work on and when; the hash still answers "was this the same project?" and "were several servers stalling together?".

The file is capped at 4MB, created owner-only (0600, in a 0700 directory), and removable with --clear. Set KNOWL_DISABLE_STARTUP_TRACE=1 to turn it off entirely.

Update checks

knowl status and knowl doctor ask the npm registry whether a newer Knowl exists and print a line when there is one. status caches the answer for a day; doctor always asks, because a diagnostic that reports a stale version is worse than one that takes an extra moment. The request times out after two seconds and fails silently, so being offline costs nothing.

Only those two commands check. Hooks, MCP, and knowl serve never do.

knowl config set updateCheck.enabled false   # this repository
KNOWL_NO_UPDATE_CHECK=1                      # one invocation, or exported
NO_UPDATE_NOTIFIER=1                         # the cross-tool convention, also honoured