Skip to content

3ngram 1.4.1

Choose a tag to compare

@github-actions github-actions released this 12 Aug 08:49
e902d24

3ngram v1.4.1

Minor release: structured retrieval. Numbers no longer live only inside prose —
facts get a live write path, search learns an exhaustive chronological mode, and
bi-temporal range reads turn a fact series into one call. No new MCP tool was
added for any of it; every capability extends an existing tool's query shape,
and the tool count stays at eleven.

Structured facts capture

  • remember accepts facts[] — structured facts (subject, predicate,
    value, optional validity window) written atomically with the memory, in the
    same transaction, composite-FK'd to it. The response echoes factIds exactly
    when facts were written; existing responses are byte-identical. Both
    transports (MCP and POST /api/v1/memories) carry the same contract.

  • The convention is one measure per fact, unit in the predicate: subject
    lift.back_squat, predicate top_set.weight_kg, value 98. Values stay
    text; charting consumers cast on read.

  • Fact timestamps are ISO strings at the boundary. A Date-typed field
    cannot ride the JSON Schema that MCP publishes via tools/list, so the
    contract is ISO-only with conversion at the handler seam. Validity windows are
    compared as instants, not strings, so mixed-precision pairs
    (…00Z vs …00.001Z) behave correctly.

Fact proposals: staged extraction awaiting review

  • A new fact_proposals table stages machine-extracted facts for human
    review before they become queryable truth. One open proposal per candidate
    fact (partial unique index over md5(value) — btree-safe for long values),
    first-proposal-wins collapse semantics, append-and-supersede lifecycle, full
    RLS with the tenant-qualified composite FK making cross-tenant references
    unrepresentable.

  • review_proposals reviews both kinds. The list grows an optional
    factProposals array (edge-only responses are byte-identical); accept and
    reject take the same proposalId-only input and probe both proposal kinds.
    Accepting a fact proposal materializes the fact in the same transaction;
    a concurrent double-apply loses the race cleanly rather than double-writing.

  • The extraction job itself is deliberately not in this release; the seam it
    will use (an injectable extractor behind the same repository pattern the
    consolidator uses) ships ready.

Search: supersession visibility

  • Superseded predecessors are demoted for every revise kind. The ranking
    penalty previously keyed on supersedes edges only; updates revises closed
    a memory's validity without demoting it. Both now demote.

  • Every search hit carries superseded: boolean, on both transports and
    both projections, computed from the same predicate that drives the demotion —
    a demoted hit can never read false. Superseded rows remain retrievable
    (demote, never filter); the flag is what tells you which ones they are.

Search: chronological list mode

  • order: "chronological" turns search into an exhaustive, filter-driven
    enumeration: no query needed (filters required instead), no embedding call, no
    semantic ranking — newest-recorded first, live memories only, with as_of
    time travel lifting the live gate when asked. "Every gym session, in order"
    is now a contract the server actually offers rather than a top-k
    approximation.

  • The list cursor is small and drift-free. Ranked search's continuation
    token carries a frozen candidate pool (~4–6 KB); the chronological cursor is a
    (recorded_at, id) keyset position. It carries the timestamp at full
    microsecond precision as opaque text — a millisecond-floored cursor silently
    skips rows when a batch insert shares one transaction timestamp, and a
    regression test now pins the exact failure.

  • Cursors are bound to the mode and filters that minted them; replaying a
    cursor across modes is rejected as a typed mismatch, never silently misread.

Facts: bi-temporal range reads

  • get_facts accepts from/to — a half-open [from, to) overlap window
    over fact validity, ordered chronologically, returning every generation in the
    window (superseded ones included: that is what a history is). recordedAt
    joins the output so consumers can tell when a fact was observed versus when it
    was written down — retroactive logging stops lying about dates.

  • Range and as_of are mutually exclusive; empty and inverted ranges are
    rejected; bounds reject sub-millisecond precision and advertise the limit.

Also in this release

  • GET /api/v1/version (authenticated) reports the running server version, so
    deploy tooling can tell a finished rollout from one still in flight.
  • MCP reference documentation for resources and the tool surface expanded, and
    the design docs now record the deliberate decision to keep the twelfth tool
    slot unspent.

Review hardening

Pre-release review (v1.4.0 was versioned but never tagged; this release
supersedes it) closed five findings before anything shipped:

  • Account erasure and portability export now cover fact_proposals
    staged proposal content is redacted on erasure and included in the export
    archive like every other user-owned table.
  • Chronological search rejects a query rather than silently ignoring it —
    the query never applied to the enumeration, so a caller could receive the
    entire live set believing it was filtered.
  • Fact write timestamps reject sub-millisecond precision (the read bounds
    already did), so nothing is silently truncated on the way in.
  • The superseded flag and its ranking demotion now require closed validity
    in addition to an incoming revision edge, so an imported updates edge whose
    target is still live is neither flagged nor demoted — search now agrees with
    memory_history about what "superseded" means.

Upgrading

  • One additive migration (0031_fact_proposals) — a new table; no existing
    table is altered. The migration adds a foreign key referencing memories, so
    apply it with a lock_timeout and retry rather than queueing behind
    long-running transactions. Self-hosters on FIPS-mode Postgres builds: the new
    unique index uses md5(), which FIPS disables.
  • No breaking API changes. All new request fields are optional; all new response
    fields are additive. Shipped schemas are byte-identical for existing calls.

Immutable container reference

ghcr.io/b3dmar/3ngram@sha256:932e54b6e8cabe1c2ee3fd1dea225b57def47b64017e277bb608e8a2d4248c2e