dent8 v0.7.0
Install: cargo install dent8-cli --locked · pip install dent8 · npm install dent8 · prebuilt archives below. Docs: https://xyzzylabs.github.io/dent8/
Added
- OS-keychain-backed identity keys (roadmap: identity productization; macOS in this
release):keychain:<account>is accepted wherever a signing-key path is —
DENT8_IDENTITY_KEY, keygen--out,grant-issue --issuer-key/--public-key,
trust-add— naming a generic-password item under keychain servicedent8instead of a
0600file.identity agent-keygen --out keychain:<account>generates straight into the
keychain (no file ever exists, the public key is printed and derivable from the private
item, and an existing item is refused, same as files). This narrows the threat model's
top residual: the key is encrypted at rest, locks with the session, and never lands in a
dotfile a backup or homedir sync would sweep. The secret passes to/usr/bin/security
over stdin, never argv. Windows Credential Manager / Linux secret-service are the
documented follow-up, with a clear error meanwhile. - MCP
resources/subscribe(roadmap: fact-change push): subscribe to a
dent8://{kind}/{key}/{predicate}fact stream and the server pushes
notifications/resources/updatedwhen it gains events — immediately after a write
through the same connection, and within a ~2s poll tick for writes from any other
process sharing the store (another agent, the CLI, a daemon peer), so long-running
agents stop re-pollingexplain. Subscribing to a not-yet-asserted stream is allowed and
notifies on its first write. Works on both transports: the stdio server (notifier thread;
responses and pushes interleave under one stdout lock) and the local daemon (per-connection
notifier + single writer task).dent8 mcp proxynow pumps frames bidirectionally
(previously strict request/response), so daemon clients receive pushes through it too.
initializeadvertisesresources.subscribe: trueonly where a notifier is actually
wired. dent8 whatif— policy-counterfactual replay (the rank-2 novelty direction in
research/novelty.md, now surfaced): re-fold the same immutable log
under a swapped epistemic trust policy —--distrust <source>(repeatable),
--authority-floor <level>,--confidence-floor <millis>— and see what would be believed,
with a per-fact structural diff (appeared / disappeared / lifecycle / value / supersession /
evidence changes) against the real fold. Read-only, deterministic, zero model invocations;
freshness is deliberately not a policy knob. Also an MCP tool (whatif, same arguments,
available to read-only daemon connections), with the diff mirrored instructuredContentand
an advertisedoutputSchema. At least one policy knob is required — the identity policy is
the plain fold (explain).scripts/load-test.sh— the concurrency harness (roadmap: load testing and tuning):
N parallel writers against one shared store, two phases — distinct facts (throughput +
event-id uniqueness) and a deliberate same-fact supersession herd (every write eventually
admitted, exactly one believed value,verifygreen over the full log). Defaults to a
temporary SQLite store; pointDENT8_STORE_URLat a throwaway Postgres (withPSQL
overridable for dockerized databases) to run the Postgres leg. The harness found every
concurrency fix below.- Cross-process write lease for
sqlite://andpostgres://stores
(SqliteEventStore::acquire_write_lease,PostgresEventStore::acquire_write_lease):
each write attempt now holds a backend lease across the whole decide+commit cycle — a
BEGIN IMMEDIATEon a<db>-leasesidecar database for SQLite, a session advisory lock
on a dedicated connection for Postgres. Optimistic retry alone is safe but livelocks
under sustained same-fact contention — the decide step re-reads a growing log, so a slow
writer's snapshot is perpetually stale by commit time; the lease turns the herd into a
fair queue. A crashed holder releases automatically (SQLite file locks and Postgres
sessions die with the process), waits are bounded, and a timeout is a retryable conflict
(SQLSTATE55P03on Postgres). Before: 16 writers × 50 contended supersessions exhausted
the retry budget; after: all 800 admitted on both backends, none exhausted. The SQLite
sidecar holds no data and may be deleted when no writer is running; in-memory stores skip
the lease (single-process by construction). See the new write-concurrency section in
docs/storage.md.
Fixed
SQLITE_BUSYat connect/migrate is a retryable conflict now: concurrent
first-connects race the schema DDL for the write lock; that BUSY was classified
Unavailable(fatal) and crashed parallel writers on a fresh store.connect_backend
also flattened errors to text, destroying the retryable class — it now returns the typed
StoreError, and the write path routesConflictinto the retry loop.- A stale-snapshot commit is retried, not reported as terminal: when a concurrent
writer lands between an op's decide snapshot and its durable append, the backend's
re-arbitration correctly rejects the commit (e.g.cannot mutate terminal fact state Superseded) — but that rejection surfaced as a non-retryable failure and crashed the
writer. It is now classified as a write conflict: the op re-decides from a fresh snapshot
and the write lands, or is genuinely rejected against current state. - Write-conflict retry widened: 32 attempts (was 16), exponential backoff capped at
256 ms (was 128 ms), still decorrelated per-process jitter.
Full changelog: https://github.com/xyzzylabs/dent8/blob/main/CHANGELOG.md