CortexDB 0.10.0 — multilingual memory, erasure that reaches every copy, and derived-data repair
Never published; superseded by 0.10.1. 0.10.0 was prepared but never tagged or published. Everything on this page ships in 0.10.1, together with 0.10.1's own changes (budget excerpts, the context receipt and the answer-context merge). Read
RELEASE_NOTES_0.10.1.md, which covers every change since v0.9.13; this page is kept for reference only.
This release captures and recalls memories in the language they were written in, makes
/v1/forget and /v1/erasures reach the uploaded blob and the bytes left in the storage files,
repairs derived facts and beliefs that earlier enrichment-on builds got wrong, and restores the
/v1/answer context quality that whole-event contexts had cost.
The measured launch set is English plus Latin-script Hinglish. Every other script is captured, tagged and recalled, but recall across scripts (a Latin query over a Devanagari memory) is not yet at the quality bar; that is the next release's work.
Before you upgrade
Read this section even if you only use English. Several contracts change.
- Gate new request fields on
capabilities[].GET /v1/admin/versionnow returnscapabilities: [...]. Every new request field below belongs to one capability, and a server that does not list it refuses the field with422 INVALID_BODY: v0.9.13 refuses each one as an unknown field, and a later server that does not list the capability names it indetails.unsupported_fieldsanddetails.required_capabilities. Check the list, never the version string. v0.9.13 has nocapabilitieskey, which means none of them. - Text blobs that are not valid text are refused. A text blob with no
charset=whose bytes are not UTF-8 (or UTF-16 with a byte-order mark) now fails the write with422 BLOB_TEXT_ENCODING_UNKNOWN(details.reason,details.charset) instead of being stored as U+FFFD replacement text. In/v1/experience/bulkone such blob fails the whole batch. Declare the charset on upload (Content-Type: text/plain; charset=windows-1252) and it decodes. /v1/forgetreports what it actually removed. It returns a top-levelpurgeobject (the shape/v1/erasuresreports assummary.purge), and every count is a real transition made by this request: a repeat forget returns zeros, a disabled graph adds no phantom document, and a scope-wide forget of 300 events reports 300, not 600.deleted.eventson a scope-wideredact_eventsforget counts each event this request blanked, once.- Forget warnings name the real reason. The "use
cascade=redact_events" warning now fires only for the whole-scopederived_onlyshape; amemory_idsrequest whoselayersleave outeventsgets "layers did not name "events" ..." instead. No selector request is told to useredact_eventsany more (dropping the selector would have turned a one-id forget into a scope wipe). - New retriable errors around destructive work.
409 DESTRUCTIVE_OPERATION_IN_PROGRESS(Retry-After: 5,details.scopes) on/v1/experienceand/v1/experience/bulk: a capture that would auto-register a new scope inside a running forget's or erasure's footprint waits up toCORTEX_DESTRUCTIVE_LEASE_WAIT_SECS(default 120) and then gets this error. It used to get a permanent-looking403 POLICY_DENIED.409 SCOPE_STATE_CHANGEDon/v1/scopesregister, member edit and delete inside that footprint.503 SERVER_SHUTTING_DOWN(Retry-After: 5) on new/v1/forgetand/v1/erasuresrequests during a graceful stop.409 REDERIVE_IN_PROGRESSon forgets and erasures under a scope root while a derived-data repair job runs there (below).
- A forget has one cutoff. Everything the server stamped before the forget's pin is forgotten and everything after it is kept, across the event log, the tombstone, the index and the artifact closure. A capture sent at the same instant as a forget-all can land on either side; send captures you need to keep after the forget returns.
?wait=capturedon an inline or transcript blob write behaves likewait=indexed, because the write now keeps its blob lease until the event's link to the blob is published./v1/recallcontext_blockbytes change for two kinds of events, English included: an assistant turn's echo of the user turn (In reply to user: ...) is dropped when that user turn is rendered in the same context, and an event too large for the remaining budget is skipped instead of ending the context. The switches are under "Answer context" below.- Packs that v0.9.13 returned empty may now answer, with a warning. When the query names an
entity no hydrated event matches, the grounding gate still empties an all-English pack over an
all-English scope. It now keeps the pack, with a
warnings[]entry and adiagnostics.filters_appliednote, when the query and the memories are in different scripts, the request carries a non-English hint, or the scope holds at least two non-English events. - Enrichment-on deployments get new derived output (facts, beliefs, extraction prompts). See "Facts and beliefs" below, including the extra enrichment spend.
- Give the server 120 s to stop. A graceful stop now waits for running forgets and erasures
(
CORTEX_SHUTDOWN_DESTRUCTIVE_WAIT_SECS, default 60), persists, then drains the erasure residual pass (CORTEX_ERASURE_RESIDUAL_SHUTDOWN_SECS, default 20). Docker's default 10 s cuts that short: usedocker stop -t 120,stop_grace_period: 120s(both compose files in this repository now set it) orTimeoutStopSec=120(the unit inPRODUCTION_CONFIG.md). - Code plane access. A repository bound under a scope that no roster governs is no longer served to per-actor tokens (404, left out of listings) until that scope is registered, and a per-actor token can no longer register a repository without a governed scope.
- First start. The code plane re-indexes every registered repository once (extractor
generation 4). On incremental-layer deployments (
CORTEX_V1_LAYERS_INCREMENTAL) a sweep removes the derived beliefs that no current topic owns (below).
Multilingual capture and recall
Nothing is translated. You send the original text plus hints, and the server keeps both.
- Capture (
capture_language_v1): optionallanguage(BCP-47),scripts[](ISO 15924) andcontext.timezone(IANA). Every event gets one language tag, from your hint, a non-Englishsource.language, alang:<tag>label, script detection or a Hinglish heuristic, and non-English events returnlanguage,scriptsandlanguage_sourceon every read and recall item. Events that are English by default carry no tag. The tags are outside write identity, so a retried write still replays. - Recall (
recall_language_v1): optionallanguageandscripts[]. They select the query stoplist (ahi/hi-Latnhint keeps the English list;fil,id,vi,und-*and non-Latin hints strip nothing) and make a grounding miss advisory, as above. Advisory notes are in the pack'swarnings[]. - Query variants (
query_variants_v1): up to 4query_variants[]({text, language?, script?}, at most 4,096 bytes each), for example an English phrasing of a Hinglish question. They are embedded in the same call as the question and merged into the result round-robin; reranking still scores against the question. Measured cost: about +100 ms p50 with two variants on text-embedding-3-small, one provider round trip. - Time (
temporal_lenient_v1,refers_to_v1):temporal.natural_mode: "lenient"turns an unparseabletemporal.naturalinto atemporal_natural_ignoredwarning instead of a 422 (strictstays the default);temporal.timezoneputs calendar phrases on that zone's calendar; captures may carrycontext.refers_to[], the times the memory talks about, and recall'stemporal.refers_duringmoves the memories that refer to that window to the front without adding or removing any. Non-English events with a capture zone get a local-time bracket after the UTC one in their context prefix; English bytes are unchanged. - Per-channel diagnostics (
recall_diagnostics_v1):diagnostics=fulladds a side table of each item's rank in every retrieval channel and every variant. - Where a long memory matched (
matched_fragments_v1): a recall item of a multi-chunk text event listsmatched_fragments[](chunk_id,text_source,byte_range,locator,language,matched_via), best first, at most 8.byte_rangere-slices the event's storedcontent.text; order is the signal, and later entries can be any chunk a channel returned. Events written before 0.10.0 have no fragments.GET /v1/admin/events/{id}/chunks(trusted operator) shows how one event was chunked.CORTEX_CHUNK_FRAGMENTS=0stops writing them. - Operator counters:
cortexdb_enrichment_non_en_script_events_enriched_totaland..._skipped_total;CORTEX_ENRICHMENT_SKIP_NON_EN=1(opt-in) skips enrichment for text in a non-Latin script.
Erasure
- Blobs (
erasure_blob_cascade_v1). An event made from an uploaded text blob, or a transcript sent in place of one, now records the link (source_blob_ids, returned on/v1/events), and erasing the event deletes the blob unless another live event still uses it.purge.blobsandpurge.retained_shared_blobsreport it. Events written before 0.10.0 have no link: runPOST /v1/admin/index-audit/source-blob-links(dry run by default) once per tenant to backfill it from thesource_blob:labels, then readGET /v1/admin/index-audit/orphan-blobs.DELETE /v1/blobs/{id}is unchanged. - Bytes left in the files. After an erasure a throttled background pass flushes the stores,
compacts the erased keys out of the RocksDB files (content, event log, session fragments) and
merges the full-text segments that still hold a deleted document.
GET /v1/erasures/{id}reportslog_residual_cleared_at, orlog_residual_pendingwith a reason. On a store whose full-text segments are larger than the pass's budget,fulltext_segment_over_budgetmay never clear; it never clears falsely.CORTEX_ERASURE_RESIDUAL=0turns the pass off (a startup warning says so), andCORTEX_ERASURE_PERIODIC_COMPACTION_SECSsets a periodic backstop. - An erased fact stays erased after a crash. An erased fact id is fenced in a synced record before it leaves memory, and the next start drops it from the fact snapshot, so a hard kill before the next snapshot save can no longer bring the fact back.
- More derived rows are erased with their capture: the extraction summary row, the facts'
question-key and gloss rows, and, with
CORTEX_ENTITY_GRAPH=1, the capture's graph and resolver rows.docs/ERASURE_COVERAGE.mdlists every store that holds text and what erasure reaches. - Faster forgets. Redactions are written in batches with one sync per 256 rows, a forget makes one full-text commit, and discovery no longer decodes rows of other scopes. Measured forget-all of a 20-event scope under concurrent captures: median 167 ms, against 298 ms on v0.9.13 and 268 ms on v0.9.12; on a 100,000-event store 369 ms against 2,471 ms on v0.9.12. Captures into registered scopes no longer wait behind a forget.
- Erasure cost follows what is erased, not the store (DB-4). A selective
/v1/erasures, a/v1/forgetby id (a repeat included) and a scope erasure no longer read the whole store: the raw-WAL discovery, the blob survival proof, thepreceded_byscrub and the idempotency cleanup read an erasure index kept with the WAL, a vector delete no longer rescans its whole HNSW shard, and/v1/erasuresdeletes its v1 rows in synced batches of 256. Measured on a 1M-event store: a forget by id 10-13 s before, 0.04-0.37 s now; a one-id/v1/erasures12.7 s before, 0.05-0.12 s now; a blob-backed one 28 s before, 0.06-0.14 s now; 1,000 ids 25 s before, 0.64 s now; a 500-event scope erasure 33 s before, 0.56 s now. On a 5M-event store the same forget by id took 64-67 s before and 0.09-0.27 s now, a blob-backed erasure 138-140 s before and 0.07-0.11 s now (docs/ERASURE_COVERAGE.md, "What an erasure reads", has the table and what still grows with the store). The first start backfills the index in the background (below). - Previews and plans.
/v1/forget/previewnow counts entity-id and attribute selectors (about_entity,about_subject,predicate,ent_ids). The erasure preview'sestimated_duration_msis 250 ms plus 80 ms per event, a planning figure.
Facts and beliefs (enrichment-on deployments)
Nothing here runs unless enrichment is on (CORTEX_ENRICHMENT_DELAY_SECONDS set).
- Extraction reads the original text. Facts keep their surfaces in the source script, with
English snake_case predicates, a byte-exact
quote, apolarity, adate_basisand, for non-English facts, a derived English gloss stored as a key-only row that recall uses to find the fact and never renders or cites. Dates are anchored to the capture'sobserved_at. These fact fields are additive and optional; thefact_surface_v1capability that will promise them is not listed yet, so do not depend on them. - Negation is kept. A denial of another value never retires a fact, on any relation ("Meera
doesn't eat non-veg" no longer supersedes "Meera eats fish"); only a cessation of the same claim
does. A restated value supersedes nothing. A claim whose polarity is in doubt gets one short
re-check call. A negation is stored once: a fact marked
negatedwhose own words already deny is re-checked, and the check's verdict is stored. That covers an object such as "wants | no delivery on Sunday" (an opener followed by a number or a name, "No. 45, 2nd Cross" or "No Cost EMI", is not a denial and is left alone, and a fact that ceased is never re-checked), an antonym such asdislikes, including a denial written as a cessation ("does not want SMS updates any more" is stored aswants | SMS updates, negated and ceased, not as "does not dislike"), and an object that states an exclusion ("all purchases except gift card purchases"). A predicate that carries its own negation keeps it when the extractor calls the fact affirmative (not_arrived | truewas stored as "arrived" for "my order has not arrived yet"). - One subject per supersession. Neither the write-time resolver nor the background
consolidator retires a fact with one about another subject: two triples need the same subject
(letter case and a leading article aside), and two sentence facts that lead with different
names ("The service centre in Chennai is open ..." / "... in Hyderabad ...") are two subjects,
however alike the rest. This changes updates stated only as sentences: a sentence whose subject
is not a name ("User lives in Boston", "I", "The order ships from Pune") and whose value is a
proper noun no longer supersedes the earlier sentence ("User lives in Denver", "... ships from
Delhi"): both stay live. An update extracted as a triple (
lives_in | Denver) still supersedes the earlier value. - Stable beliefs. Facts and beliefs share one topic key, belief ids are deterministic, beliefs
carry
claim_keybesideclaim, and a multi-valued relation gets one belief per value. Synonyms are stored as their canonical predicate (intends_toand the future forms becomeplans_to, the diet variantsdiet,flight_atflight_time). On incremental-layer deployments the first start, and every start after a rollback, sweeps the builder beliefs that no current topic owns; elsewhere a fullPOST /v1/beliefs/buildof a scope rebuilds its beliefs under the new keys. Beliefs you declared withPOST /v1/beliefsare never swept./v1/conflictsitems carryconfidence, and a later stated update of an observed value is a correction, not atemporal_overlapconflict. - Spend. The extraction prompt is about 23% larger per English knowledge call (3,564 against
2,916 prompt tokens measured), plus one extraction-model call per doubted polarity
(
CORTEX_POLARITY_RECHECK=0records the doubt without the call;CORTEX_POLARITY_QUOTE_TRIGGER_EN=0turns off only the English quote trigger). Price the model inCORTEX_MODEL_PRICES.CORTEX_ENRICHMENT_SCOPE_QUIET_MS(opt-in) batches a busy scope's captures into fewer calls at the cost of capture-to-fact latency.
Repairing derived data written by earlier builds
Enrichment-on stores written by v0.9.13 and earlier can hold facts with a lost negation, facts of different people merged, names in two scripts merged, colliding entity ids, and beliefs counting facts of the opposite polarity. New trusted-operator routes repair them from the event log, without wipes and without re-ingesting anything:
POST /v1/admin/derived/rederive(dry run by default) takes ascripts/derived_corruption_audit.pyreport (tool version 2 also lists facts whose negation is stored twice or dropped, and compares subject ids ignoring letter case and a leading article), fact ids, event ids or exact supersession edges;GET /v1/admin/derived/rederive/{job_id}reports the job. A job is resumable and continuable (continue_from,continued_by,409 JOB_ALREADY_CONTINUED). A colliding legacy entity id (one lossy id such asent_______for several non-Latin names) reaches the facts of every subject it names; the plan lists them inambiguous_entity_idswith a warning.POST /v1/admin/derived/resolver/unmergesplits names the entity resolver merged.
Follow docs/DERIVED_REDERIVE_RUNBOOK.md step by step: audit, quiesce ingestion, cold backup,
dry runs, apply, re-audit. It also says what the audit cannot see (a denial an older extractor
stored as an affirmative) and how to check for it by hand. An apply is one-way for a rollback
to v0.9.13: after it, going back means restoring the cold backup.
Answer context
/v1/answer contexts are built from whole events. This release fixes the packing that change had
broken: derived fact rows behind a timestamp were ranked as structure and cut first, assistant
replies carrying an echo of the user were counted as user evidence, and one oversized event ended
the context. Dialogue with named speakers ("Caroline: ...") ranks level with fact rows, and in a
congested multi-topic answer each topic's focused dialogue and fact rows are packed before the
rest. Each fix has a switch, and the documented set in
PRODUCTION_CONFIG.md ("Answer context") restores the previous behaviour:
CORTEX_EVIDENCE_ROLE_CLASSIFIER, CORTEX_CONTEXT_STRIP_ECHO,
CORTEX_CONTEXT_RENDER_SKIP_OVERSIZED, CORTEX_FACET_PACK_POLICY,
CORTEX_CONTEXT_ASSISTANT_EXCERPTS (off by default) and CORTEX_ABSTAIN_SELF_RESCUE (the default
is the previous pass; guarded is opt-in).
/v1/answer and /v1/compose report diagnostics.degraded_sources as stable codes only
(coordinator_recall, wal_supplement, derived_<lane>, artifact_search,
index_divergence, derived_backfill, facet_plan_unavailable, facet_recall_failed,
ms_executor_fallback, else unknown); error text stays in the server log. /v1/answer also
detects "no such information" abstentions, no longer treats general nouns ("issue", "problem") as
unsupported premises, and reports premise_unsupported_terms and extractive_retry_outcome.
LongMemEval-S (full 500 questions, blind server harness, enrichment off): this release was checked for non-inferiority against our previous baseline run under the same configuration, and passed. That is a regression check, not a published score. The run predates the answer-stage changes in the paragraph above, which came in with the final merge.
Also in this release
- Code plane. A repository whose scope no roster governs is never served to a per-actor
token (404, omitted from listings); registering one provisions an unregistered team scope with
the caller as Owner and refuses a root (
403 SCOPE_REGISTRATION_REQUIRED). Credential files are never read, plain directories index only registered-grammar files, and/v1/code/contextserves only indexed, hash-verified files.CORTEX_CODE_REPO_ROOTSlimits where repositories may be registered; system trees and the data dir are always refused. Scans are bounded. New bindings are named after the repository, and shared bindings are refused (409). Call graphs are more accurate for C and C++, and SCIP imports derive compiler-tier call edges. - Tokens.
POST /v1/auth/tokensacceptscapabilitiesas an alias ofcapsand validatesscopesentries. - Sample LiteLLM gateway.
deploy/litellm/config.yamlno longer caches embeddings. LiteLLM 1.93.0 answers a partly cached embedding batch with duplicate indices, which the server rejects, so one cache hit failed a whole/v1/experience/bulkbatch. Removeembeddingandaembeddingfromsupported_call_typesin any copy of that config you run. - Admin console. Confirm dialogs act on OK;
events_storedcounts each memory once, so the figure halves on unified-log stores; settings show unwired lanes as not configured; the update check reads GitHub Releases (with an off switch); the changelog lists every bundled release; derivation status reports off when it is off; the boot config dump marks values that came from an admin-UI override; not-configured 503s no longer count inerrors_total(they have their ownnot_configured_total); a console restart exits with code 75. - Startup warnings (advisory, never a refusal):
ENGLISH_ONLY_PLANE_ENABLED,DOCUMENT_PROVIDER_FORGET_RESIDUE,DOCUMENT_PROVIDER_NATIVE_DEAD_LETTERS,ENTITY_GRAPH_ENRICHMENT_FALSE_MERGE,ENRICHMENT_RESOLVER_ERASURE_RESIDUE,ERASURE_RESIDUAL_DISABLED,ERASURE_RESIDUAL_LEGACY_WAL. The defaults log none of them.
New environment variables
All are in docs/generated/env-vars.md (§4.2, §9.1, §11.4, §12) and the operator-facing ones in
PRODUCTION_CONFIG.md: CORTEX_DESTRUCTIVE_LEASE_WAIT_SECS,
CORTEX_SHUTDOWN_DESTRUCTIVE_WAIT_SECS, the CORTEX_ERASURE_RESIDUAL* family and
CORTEX_ERASURE_PERIODIC_COMPACTION_SECS, CORTEX_CHUNK_FRAGMENTS, CORTEX_POLARITY_RECHECK,
CORTEX_POLARITY_QUOTE_TRIGGER_EN, CORTEX_ENRICHMENT_SCOPE_QUIET_MS and ..._MAX_MS,
CORTEX_ENRICHMENT_SKIP_NON_EN, the answer-context switches above (plus
CORTEX_CONTEXT_ASSISTANT_EXCERPT_MAX_BYTES),
CORTEX_QUERY_VARIANTS_FUSION and CORTEX_QUERY_VARIANTS_DEPTH (evaluation),
CORTEX_QUERY_EMBED_CACHE_CAP, CORTEX_EMBEDDING_CACHE_DISABLE (measurement only),
CORTEX_CODE_REPO_ROOTS, and the erasure index switches CORTEX_ERASURE_INDEX,
CORTEX_ERASURE_INDEX_BACKFILL, CORTEX_ERASURE_INDEX_BACKFILL_ROWS and
CORTEX_ERASURE_INDEX_BACKFILL_PAUSE_MS. Never set a CORTEX_EVAL_* variable on a production
server.
Known issues
Found in the pilot-upgrade drills on enrichment-on stores and the reviews after them. They affect enrichment-on deployments only; with enrichment off (the default) there are no facts or beliefs to get wrong.
-
A belief can list a support of another value and the opposite polarity. For a predicate the built-in ontology does not know (
habit,habit_frequency,no_longer_worksbesideworks_in), the belief builder groups facts of different values into one belief, so "prefers email updates" can list "does not want SMS updates any more" among its supports. Neither fact is wrong; the grouping is.scripts/derived_corruption_audit.pyreports these asbelief_polarity_mismatch(4 to 6 in a 740-event test store). -
A cessation can retire a fact that is still true. A background consolidation
staleverdict has no same-claim check across a cessation, so "prefers email updates" can be retired by "does not want SMS updates any more" (1 or 2 per test run; the audit's informationalnegation_introduced_by_supersession). -
Sentence-only updates of a subject that is not a name no longer supersede. Two sentence facts supersede only when they lead with the same name, and
user,Iandtheare not names, so a sentence whose value is a proper noun leads with the value: "User lives in Boston" and a later "User lives in Denver" both stay live, where v0.9.13 retired the older one. An update extracted as a triple (lives_in | Denver) still supersedes; see "One subject per supersession" above. -
Entity-extraction spend is not priced.
GET /v1/admin/usagerecords the entity-extraction lane with tokens but no model, so it is always underunpriced_callsandCORTEX_MODEL_PRICEScannot price it (about a third of the drill's enrichment spend). Read the provider's bill for the total. -
Erased names stay in the enrichment resolver. With enrichment on and the entity graph off (the default), an erased capture's entity names remain in the resolver's files. The startup warning
ENRICHMENT_RESOLVER_ERASURE_RESIDUEsays so;docs/ERASURE_COVERAGE.mdhas the detail. -
Blob backfill is per billing tenant.
source-blob-linksandorphan-blobsare confined to the caller's billing tenant, and the static operator key isdefault: the pre-0.10.0 blobs of captures written with minted tokens of another tenant cannot be linked or reviewed by the operator in this release.
Upgrade
- Read
PRODUCTION_CONFIG.md: the enrichment spend switches, "Erasure raw residual", "Answer context" and the 120 s stop grace. - Take a cold backup (
scripts/cold_backup.py), then upgrade. Expect the one-time code-plane re-index and, on incremental-layer stores, the belief sweep on the first start. The swept beliefs are rebuilt by the auto-layer ticks, at most 2,000 topics per scope per tick with one embedding call per belief, so on a large store the reconcile outlasts the first tick: it is finished whenGET /v1/admin/layers/statsreportsbelief_reconcile.done: true(dirty_topicscounts what is still queued).doneneedsCORTEX_V1_LAYERS_AUTOon (without it nothing is seeded or swept: rebuild withPOST /v1/beliefs/build); it never turns true while aCORTEX_V1_LAYERS_AUTO_EXCLUDE_SCOPESprefix covers a scope with facts (the sweep empties that scope's beliefs and no tick rebuilds them: build it by hand); and a build that fails drops its topics, so compareby_scope.<scope>.belief_errors_totalbefore and after. If you keep the old container for a rollback, set its restart policy tono(docker update --restart=no): withalways, a Docker daemon restart starts it again beside the new one, on the same port and data dir. The first start also backfills the erasure index in the background: no downtime and no extra start time on a small store (a 1M-event store's 2M WAL rows took 6.6-7.1 s and a 5M-event store's 10M took 53 s, both overlapped with the start), and untilerasure_index.wal.readyistrueonGET /v1/admin/metricserasures read the whole store as before. The backfill resumes where it stopped after a restart or a crash. Every start also rebuilds the erasure retry candidates from the forgotten markers and the idempotency reverse index from the live records, both in the background. - For each tenant that uploaded text blobs, run the
source-blob-linksbackfill, then revieworphan-blobs(a blob inlined before 0.10.0 and not backfilled is listed even when its event is live). - Enrichment-on deployments: run the audit and, where it or the runbook's manual check finds
damage, the repair in
docs/DERIVED_REDERIVE_RUNBOOK.md. - Clients: gate the new fields on
capabilities[].
Rollback
A v0.9.13 binary opens and serves a data dir 0.10.0 has used: no column family was added (the erasure index lives in an existing family, under a key range earlier binaries never read). Three things end or limit that:
- After a derived-data repair apply, roll back only by restoring the cold backup taken before
it. v0.9.13 cannot read the repair's log rows and would bring the reverted supersessions back
the first time it rebuilds its fact store. Restore into a new directory and start v0.9.13 on
that one (a new container, or a service whose data argument names it); keep the upgraded
directory, and make sure nothing starts either build on it again: set the 0.10.0 container's
restart policy to
nobefore stopping it, since a Docker daemon restart starts a stoppedalwayscontainer again. - During a rollback window v0.9.13 erases the way it always did: it deletes no blobs, leaves
the raw residual, ignores the erased-fact fence (a crash can bring back a fact 0.10.0 erased),
records no fence for its own erasures (a crash can bring back a fact it erased or forgot
itself), builds beliefs without claim keys, and reads the new event fields (
language,scripts,context.timezone,context.refers_to,source_blob_ids) as absent; a row it rewrites in place (a redaction) may lose them. Clients gating oncapabilities[]stop sending the new fields on their own. - Entity-id lookups on v0.9.13 come back only after a belief rebuild. 0.10.0 rebuilds beliefs
under its own entity ids, so after rolling back,
GET /v1/beliefs?about=ent_...with a v0.9.13 entity id (such asent_Jos_) returns nothing on v0.9.13 until you runPOST /v1/beliefs/buildfor each scope on v0.9.13. That rebuild uses v0.9.13's ids again, including the lossy ids it gives names in non-Latin scripts. Do not rely onent_ids cached across a rollback.
After upgrading again:
-
Delete the orphan blobs of events erased during the window (
GET /v1/admin/index-audit/orphan-blobs, thenDELETE /v1/blobs/{id}; check each first, the report also lists blobs of live events written before 0.10.0), and setCORTEX_ERASURE_PERIODIC_COMPACTION_SECSfor a while to reach the raw residual. -
Facts: a fact 0.10.0 erased before the rollback is dropped again at boot. A fact that v0.9.13 erased or forgot during the window and that a v0.9.13 crash brought back is not: it is still served. Keep, for every forget and erasure you send during a rollback window, its body, the time you sent it and its response; the v1 audit log (
CORTEX_V1_AUDIT_PATH) records each request's endpoint, actor, time, status and a peppered body hash, not the body, so it tells you how many there were and when, not what they named. After upgrading again:- Re-issue as it was only a request that names memory ids (
selector.memory_idsofevt_orfact_ids), without itsfrom_preview_idandidempotency_key: it names exactly what it named before. An erasure re-issued this way matches facts by the events they came from even though the events are gone, and reports them insummary.deleted_facts. - Never re-issue any other request as it was: a scope-wide forget, an
about_subject,about_entityorpredicateforget, anent_id, a whole-scope erasure. None has a time bound, so re-issued later it also deletes what was written after the original, in the window and since the upgrade; a scope-wide redaction or erasure also moves the scope's forget tombstone, which hides every earlier event of the subtree for good. Remove instead exactly what came back: the facts in the request's scope that its selector matches, that were created before it was sent and that no later capture supports, forgotten byfact_id; for an erasure, also re-issue it by the ids of their source events that no longer exist, which names no live event. Take the time of the request from the server, never from the client: the response'saudit_idorerasure_idis a UUIDv7 that carries it. Leave facts recorded within a couple of minutes of it to a person's review rather than a script, and review the list item by item when the response was not kept.docs/ERASURE_COVERAGE.md("Rollback window") has the procedure. - Never turn a forget into an erasure: a forget that named only
layers: ["facts"]left its event live on purpose, and an erasure of that id deletes the event.
What cannot be recovered this way: a request whose body or time was not kept; a fact built after the request from one that came back (a consolidation merge), which the time bound keeps; an
about_entitythat names an entity-graph id. Better still, hold the requests that do not name memory ids until the upgraded build serves again: then there is nothing to re-issue. - Re-issue as it was only a request that names memory ids (
-
Beliefs: on incremental-layer deployments the belief sweep runs by itself; elsewhere rebuild them with
POST /v1/beliefs/buildfor each scope. -
The erasure index catches up the rows the earlier binary appended during the window, in the background, before erasures read it again.
docs/ERASURE_COVERAGE.md ("Rollback window") has the details.
Clients
Client versions prepared with this release and released with 0.10.1: cortexdbai 0.12.0 on PyPI
and 0.10.0 on npm (version(), capabilities(), the capture and recall language, time and variant
parameters, delete_blob / deleteBlob, event_chunks / eventChunks, recall diagnostics types
and matched-fragment helpers); cortexdb-mcp 0.8.0 (matched document spans in memory_search
output, clearer code-plane tool output); cortexdb-cli 0.6.0 (cortexdb auth tokens --scope is
now where your auth.mint is checked, and the new --confine CAP:PREFIX carries the token's
scopes); cortexdb-connectors 0.2.22 (GitHub: full PR, issue, comment and push bodies, REST-list
backfill with GITHUB_BACKFILL_DAYS, object-identity record keys; read its CHANGELOG before
upgrading, because GitHub records are re-keyed).
Notes
Single-node remains the only supported topology.
Erasure and forget were measured up to 5,000,000 events; 25,000,000 was not measured. What still
grows with the store is listed in docs/ERASURE_COVERAGE.md ("What an erasure reads"): a
scope-wide erasure's pass over the content store, the bitemporal closure when bitemporal records
are erased, and, on the legacy dual WAL (CORTEX_UNIFIED_WAL=0), every full scan this release
removed from the unified WAL.
POST /v1/experience?wait=settled is still accepted but cannot succeed and returns
503 SETTLEMENT_UNAVAILABLE. Use wait=indexed.