Changelog
All notable changes to gonzalo are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. While the project is pre-1.0, the minor version is bumped for new features and the patch version for fixes.
Unreleased
Added
gonzalo purgeremoves one tombstone, discarding its restore window. It refuses a live record and an absent key (exit 1), takes no prompt, and prints the key and revision. (ADR 0030, #333)- Deleting a manifest is now recoverable.
Recordgains an additivedeleted_kindfield (tombstones only; absent on every existing record). AGraphManifestorVectorManifesttombstone now retains the deleted body instead of discarding it, andgonzalo gcmarks through that body, so a deleted index's slices or shards survive untilcollectremoves the tombstone.gonzalo_core::undelete, thegonzalo undeletesubcommand and the facade re-export restore a manifest from its tombstone, preservingmeta.created. Aputthat creates over a manifest tombstone is refused (see below). An older binary'sgcdoes not readdeleted_kindand frees the shards the tombstone pins, so upgrade every binary that runsgcagainst a store first. (ADR 0029, #327) BlobStore::has_blob. A defaulted method (get_blob(..).is_some()), overridden by the filesystem store withtry_exists, by S3 withHeadObject, and by the remote store with the presence route below. A failure that is not "no such key" is an error, notfalse. (#325, #329)HEAD /v1/blobs/{hash}and theHasBlobgRPC RPC answer whether a blob exists without reading it, andServerStore::has_blobnow uses them instead of the downloading default. A writer confirming its referenced blobs survived a commit previously pulled every one of them over the daemon — 600 KB a shard for a vector index, on every interactive upsert. Additive on both surfaces. Two notes for existing callers:HEAD /v1/blobs/{hash}already answered200/404, because axum servesHEADfrom theGEThandler and discards the body; it now reportscontent-length: 0rather than the blob's size, since a size needs the read the route exists to avoid.GETthe blob if you need its length. Over gRPC a daemon with noHasBlobRPC earnsDAEMON_PREDATES_BLOB_PRESENCE("upgrade gonzalod") rather than a silent fall back to downloading, which would keep paying the cost with nothing to show why; over HTTP no such message is needed, because axum servesHEADfrom a route'sGEThandler and an older daemon therefore answers anyway. (ADR 0028, #329)- A sweep policy for blob GC.
SweepPolicy,DEFAULT_MIN_AGE(one hour),sweep_blobs_withandgc_blobs_withlet a caller set the minimum blob age explicitly;gc_blobsandsweep_blobskeep their signatures and use the default.GcReportgainsdeferred, the unreferenced blobs held back for being too young, andfreednow comes back in hash order. (#325) gonzalo gc --min-age <duration>(default1h, same spellings ascollect --older-than: one positive whole number and one unit ofd,h,mors). Thegcandindex --gcsummaries printdeferred. (#325)- Vectors now survive the process that built them. A new
RecordVectorIndexpersists aVectorIndexbehind sharded, content-addressed blobs named by a newRecordKind::VectorManifestrecord (MergeClass::Opaque, since caller-supplied vectors can never be re-derived the way a code-graph manifest can).open(store, key, space, dim)declares an embedding space and dimension and errors naming both values on a mismatch, at open and again on every OCC conflict, so two handles racing to create the same missing index can't silently mix models. Shard assignment (shard_of) and the default shard count (DEFAULT_SHARDS, 256) now takestd::num::NonZeroU16, so a zero shard count is unrepresentable rather than a runtime panic waiting to happen.gonzalo gcmarks a vector manifest's shard blobs as live, the same way it already marks a graph manifest's slices, so running GC against a store holding a durable vector index is safe when no writer is mid-commit. A sweep that lands between a write's shard upload and its manifest commit used to delete the new shard and leave the index unopenable; see the grace-period fix under Fixed for how that is now narrowed, and what remains. A durable index's writes commit under optimistic concurrency, retrying up to 5 times on a conflict. Each commit is checked against the manifest revision the writer's in-memory state came from, so two writers on the same shard cannot silently overwrite each other; and a conflict reloads every shard that changed, not only the ones the write touched, so a writer that lost a race cannot later overwrite the winner's vectors from stale memory. Thegonzalofacade'svectormodule (behind thevectorfeature) re-exportsRecordVectorIndex,DEFAULT_SHARDS,shard_ofandVectorManifest, so a facade consumer can reach the durable index without depending ongonzalo-vector/gonzalo-coredirectly. See ADR 0027. (#323) KnowledgeStore::openrebuilds chunk counts from a durable index.KnowledgeStore.chunk_countsonly ever lived in memory, so a re-ingest of a shrunk record after a restart used to leave its high-ordinal chunks behind forever once the index itself became durable.open(store, index, embedder)scans the index once via the newVectorIndex::keys(below) to rebuild counts at construction;KnowledgeStore::newis unchanged and still starts counts empty, which is correct for a fresh in-memory index. (#323)- Records carry their times.
Meta.createdandMeta.updatedwere always0; the store now stamps both inside the same critical section that decides the write, exactly as it stamps a tombstone'sdeleted_at.createdsurvives every edit, a delete counts as the record's last update (itsupdatedequals the tombstone'sdeleted_at), and a recreation over a tombstone starts a newcreated, because that is a new record at an old key. A replication write keeps the source's times, so syncing a record is not an edit. A client's values are overwritten: times a caller can set are times a caller can lie about. The conformance suite asserts all of it, so every substrate agrees. (#293) - Builders for the common record paths.
Record::createmakes a record that doesn't exist yet, andrecord.update(body, meta)makes the next version of one you read: it sets the revision to follow andparentto the revision it was built from, which is the pairing OCC depends on and the easiest thing to get wrong by hand — a mismatch doesn't fail at build time, it surfaces later as a spurious conflict.Meta::new(author, origin_system)covers the provenance. Fields stay public, so nothing existing breaks. (#305) - Blob garbage collection, and tombstones pin their blobs.
gonzalo gcnow marks against every record in the store instead of a caller-supplied list of manifests: a record whose body is a blob, the blob a tombstone pins, and every code-graph slice a manifest names. A tombstone carries the deleted record's blob hash in the newRecord.deleted_blob, so a delete never destroys content a peer can still sync back, and re-putting the same content doesn't re-upload it. Reclaiming a deleted record's bytes is three deliberate steps:delete,collectpast the horizon, thengc.gc_blobstakes the store and does its own listing;sweep_blobsandlive_blob_hashesare the halves, for a caller that already knows its live set. See ADR 0024. (#292) - Records over MCP, and an MCP server that can talk to a daemon.
gonzalo-mcpexposed only the code graph, so an agent could ask what calls a function but could not read the records the store exists to hold — the gap both the Zep and Letta parity matrices meant by "no MCP surface", while their rows wrongly claimed there was no MCP server at all. It now also exposesrecord_getandrecord_list, withrecord_putandrecord_deletebehind--allow-writes(orGONZALO_MCP_ALLOW_WRITES=1) and absent from the advertised tool list without it, so an agent never sees a tool it cannot call.GONZALO_DAEMON(or--daemon <url>, withGONZALO_TOKEN) points the server at a runninggonzalodinstead of a local directory, so it can share a store rather than answer from one machine's copy. An absent key is an error naming it, never an empty result, for the same reason an unknown view is. Vector and knowledge recall over MCP are tracked separately (#317). (#197) - Published schema artifacts, so non-Rust clients are generated rather than
maintained. Client SDKs are 🔴 in all three competitor matrices, each
suggesting the same fix: generate one from the daemon's schema. But
gonzalo-protowas an internal workspace crate and the HTTP/JSON surface had no description at all, so the daemon's non-Rust promise (ADR 0007) was unfulfilled at the packaging layer. Every release now attachesgonzalo-vX.Y.Z.protoandgonzalo-openapi-vX.Y.Z.jsonto its GitHub Release, and the OpenAPI document lives in-tree atdocs/api/openapi.json. They cannot drift:SERVED_OPERATIONSis checked against the router's own source and against the document, in both directions, so an operation added without describing it fails CI and so does one described but not served. The guide gains a Generating a client chapter with a worked Python example. No client code in any other language enters this repo (ADR 0020). (#198)
Changed
-
BREAKING:
RecordVectorIndex::openrefuses a deleted index. Opening an index whose manifest is tombstoned used to succeed, hand back a handle onto an empty in-memory index, and fail only at the first commit — so an operator who deleted an index and restarted sawopensucceed, queries return nothing, and the real explanation arrive on the next upsert.opennow reads raw and errors withCoreError::Invalid, naming the key and the two ways out (gonzalo undelete,gonzalo purge). The trigger is the same conditionplan_putuses — the stored tombstone'sdeleted_kindnaming a manifest kind — soopenrefuses exactly when the first commit would have, and a tombstone with no restore window (written before #327) still opens fresh rather than stranding the key. There is deliberately no flag to skip the refusal: a caller starting over purges first, which keeps discarding a restore window one explicit operator action. One side benefit: a refused reopen now stages no shard blob, where the commit-time refusal left one orphan forgc. (ADR 0029, ADR 0030, #340) -
BREAKING: the raw replication write requires an admin token.
PUT /v1/raw/records/{ns}/{col}/{id}and thePutRawRPC authorizedwriteon the namespace; they now require admin, matchingPOST /v1/purge/...andPurge. A replication peer configured with a non-admin token begins getting403/PermissionDeniedand must be re-credentialed — ADR 0015 already described an admin token as the replication credential, so a correctly configured peer is unaffected. The route writes verbatim and so reaches past theplan_putguard added above, which let a principal with plainWritediscard a deleted manifest's restore window by aiming a fresh record at the tombstone's revision — predictable, becausetombstone_hash()is a constant. Authorship now carries across stores unconditionally, since the non-admin restamping that hardened the old path is no longer reachable. (ADR 0031, #342) -
BREAKING: the daemon refuses to purge a live record.
POST /v1/purge/...and thePurgeRPC now answer400/InvalidArgumentwhen the named revision matches a live record, asgonzalo purgehas since #333; purging a live record leaves no tombstone, so a peer that has not synced since would resurrect it (ADR 0021). An admin purge of a live record therefore starts failing where it previously succeeded; usegonzalo deleteand then purge the tombstone. Tombstone collection is unaffected: the refusal is narrow enough that a purge naming a revision that no longer matches — the key was recreated while collection held the old tombstone revision — still answers409 Conflictand removes nothing, whichcollectrecords and carries on from. (ADR 0031, #341) -
A
putthat creates over a manifest tombstone is now refused. A consumer create (expected: None) at a key holding aGraphManifestorVectorManifesttombstone used to succeed, overwriting the tombstone and discarding the retained body and the shard pin. It now fails withCoreError::Invalid(HTTP 400 / gRPCInvalidArgument), naminggonzalo undeleteandgonzalo purge. This affectsRecordVectorIndexcommits andgonzalo indexover a deleted manifest, sogonzalo resetfollowed bygonzalo indexnow needs a purge or undelete first. Replication writes andundeleteare unchanged.RecordVectorIndex::openstill succeeds and fails at its first commit (gonzalo#340), and the daemon's admin purge route does not check for a live record (gonzalo#341). (ADR 0030, #333) -
BREAKING:
gonzalo_core::unreferenced_slicesandgonzalo_core::live_slice_hashesare removed. After #325 rewrote the sweep, neither had a caller outside its own unit tests —live_blob_hashesinlines the manifest-entry logic rather than callinglive_slice_hashes, andsweep_blobs_withdoes its own retain loop. Both were also footguns:unreferenced_slicesis age-unaware, so mappinglist_blobs()down to hashes and feeding it rebuilt the unsafe delete list #325 removed, andlive_slice_hashesused alone as a mark set keeps only manifest slices and sweeps every record body. Usegc_blobs_with/sweep_blobs_withto sweep andlive_blob_hashesto mark. Thegonzalofacade never re-exported either, so only a directgonzalo-coredependant is affected. (#331) -
BREAKING: the indexing knobs move into
gonzalo_cli::IndexOptions.index_with_gc_filteredandindex_with_gc_filtered_workerare replaced byindex_with_options(root, src, repo, view, &IndexOptions), which carries{gc_after, min_age, filter, worker}. The eight-argument worker variant needed#[allow(clippy::too_many_arguments)]and respelled every argument at each fan-out layer, so a new knob cost six edit sites; it is now a field.index_with_gcis unchanged.IndexOptions::default()deliberately setsmin_agetoDEFAULT_MIN_AGErather than the derivedDuration::ZERO— a zero default would let a caller who setsgc_afterreinstate the age-unaware sweep ADR 0028 removed. (#331) -
BREAKING:
IngestError::Conflictis a boxed tuple variant. It wasConflict { key, expected, current }with the fields inlined, which made the error path of everyResultingonzalo-ticket's ingest module 136 bytes wide — clippy 1.99'sresult_large_err. It is nowConflict(Box<gonzalo_core::Conflict>), the same shapePutResult::Conflictalready returns, so the ingest path stops unboxing and re-inlining and callers get the whole conflicting record rather than its revision alone. Code that matches on the struct variant's fields must match the boxed value instead. (#336) -
The pinned Rust toolchain is 1.99.0.
rust-toolchain.tomlmoved from 1.95.0, four releases behind. Getting the gate green on it needed five fixes across four crates, each hidden behind the previous because clippy stops at the first failing crate:async-trait0.1.89 → 0.1.92 (upstream stopped emitting the#[must_use]that made clippy'sdouble_must_usefire on 14 desugaredasync fns ingonzalo-core— preferred to an#[allow], which would also have masked the lint for hand-written code, and it pulls insyn 3.0.6);as_chunks::<2>()forchunks_exact(2)ingonzalo-domain's hex parser; theIngestErrorboxing above; aresult_large_errallow scoped togonzalo-proto's generatedv1module, where tonic'sResult<_, Status>signatures are build-script output and not editable; andslice::fillfor a manual loop ingonzalo-soak's oracle tests. (#336) -
BREAKING:
BlobStore::list_blobsreturnsVec<BlobEntry>. Each entry carries the blob's hash andmodified_unix_ms, which the sweep needs to tell a freshly uploaded blob from an abandoned one. Any out-of-treeBlobStoreimplementor must change its signature. (#325) -
BREAKING: the daemon's blob listing returns objects. HTTP
GET /v1/blobsreturns[{"hash": …, "modified_unix_ms": …}]instead of["…"], and gRPCListBlobsResponsereserves field 1 and addsrepeated BlobEntry entries = 2, so an old gRPC client sees an empty list and a GC through it deletes nothing.docs/api/openapi.jsongains aBlobEntryschema; note that the #198 drift check compares paths and methods, not response schemas, so this change is not guarded by it. See ADR 0028. (#325) -
BREAKING:
VectorIndexgains a requiredkeysmethod. There was no way to enumerate an arbitrary index's contents, which is whatKnowledgeStore::openneeds to rebuild derived state — its per-record chunk counts — after a restart (gonzalo gcnever callskeys; it marks a vector manifest's shard blobs directly).keyshas no default implementation and must be added by any out-of-treeVectorIndeximplementor.upsert_manywas also added, but as a default that loops overupsert, so it is not itself breaking —RecordVectorIndexoverrides it to batch a bulk load into one manifest commit instead of one per vector, and the newimpl VectorIndex for Arc<T>forwards both methods (includingupsert_many, explicitly, so a sharedArchandle doesn't silently fall back to the looping default). See ADR 0027. (#323) -
gonzalo-vectornow requires a tokio runtime.RecordVectorIndex::openreads shard blobs concurrently viatokio::task::JoinSet::spawn, which panics outside a tokio runtime.tokio(featurert) moved from a dev-dependency to a real one. Every gonzalo binary already runs on tokio, so nothing breaks today, but a future non-tokio consumer ofgonzalo-vectorwould need one to callopen. (#323) -
BREAKING: the facade's surface is grouped into modules.
gonzalore-exported 74 names flat, many of them ordinary nouns —Actor,State,Provider,Container,Link,Topic,Session,Priority,Person— which collided with a consumer's own types and said nothing about which layer they came from. The flat surface also could not express two layers using one word:gonzalo-ticketandgonzalo-graphboth define aPage, and only one of them could ever be re-exported. The root now holds the record and store core and nothing else; every domain type lives under the module naming its domain, mirroring the structuregonzalo-domainalready had. There is no deprecation window — the flat names are gone in this release — so the mapping is below. See ADR 0026. (#319)was now gonzalo::MemoryTier,Topicgonzalo::memory::{MemoryTier, Topic}gonzalo::Session,Turngonzalo::session::{Session, Turn}gonzalo::Checkpointgonzalo::checkpoint::Checkpointgonzalo::RecordCodecgonzalo::codec::RecordCodecgonzalo::Ticket,TicketBody,TicketEvent,State,StateCategory,Actor,ActorRole,Priority,PriorityLevel,Resolution,Provider,Container,Link,LinkKind,LinkTarget,BodyFormatgonzalo::ticket::*gonzalo::TicketSource,Capabilities,Cursor,FieldMapping,InMemorySource,Page,SourceError,StateMapping,StateSignal,record_key,scoped_uidgonzalo::ticket::*gonzalo::GitHubSource,JiraSource,LinearSource,GitLabSource,AsanaSourcegonzalo::ticket::*gonzalo::Person,RoleGrant,IdentityBinding,ChannelConfig,LinkToken,AuditEntry,AuditResult,Authenticator,BindingOrigin,Consumption,EmptyFollowSet,FleetActor,FleetKeyError,FleetRole,Follows,GrantScope,LinkSecret,LinkSecretError,NotifyPreset,RedeemError,VerifiedEmailgonzalo::fleet::*(already available there)gonzalo::CodeGraph,GraphStore,InMemoryGraphStore,Located,Reference,Symbol,SymbolKind,assemble,build_rustgonzalo::graph::*gonzalo::Embedder,Match,MemoryVectorIndex,VectorIndexgonzalo::vector::*gonzalo::Hit,KnowledgeStore,knowledge_textgonzalo::knowledge::*Unchanged at the root:
Record,RecordKey,RecordKind,Revision,Body,Meta,Identity,ContentHash,KeyPrefix,Store,BlobStore,AncestryStore,PutResult,DeleteResult,Conflict,CoreError,Result,MergeClass,MergeOutcome,sync,sync_with_ancestry,merge,collect,reset,reset_as,gc_blobs,now_ms, the reports, andFsStore/GitStore/S3Store/ServerStore. -
S3 listings stop paying a read per record. Hiding tombstones from
listcost aGetObjectper key, because a tombstone lives at the deleted record's own object key and only its body says so;resetandcollectinherited it. A delete now also writes a small marker beside the record, so oneListObjectsV2pass classifies every key and only marked keys are read — one read per tombstone, whichcollectbounds, instead of one per record, which nothing did. Over RustFS with 500 live records and 200 tombstones, a listing went from 700 reads (216 ms) to 200 (110 ms), and to none at all (40 ms) once the tombstones were collected. The record object does not move, so every compare-and-swap is still a single conditional write. A bucket written by 0.7.0 upgrades itself: until a collection is flagged,listreads every key exactly as before and backfills the missing markers as it goes, so there is no admin step and no window where a deleted record could reappear. See ADR 0025. (#294)
Fixed
-
An S3
HeadObjectfault was described as "unhandled error". A failed blob presence check surfacedHeadObjectError's ownDisplay, which renders the unmodelled variant with no code and no status — so the most common misconfigurations, a 403 from a bucket policy or wrong credentials, were indistinguishable from each other and from anything else. The error now leads with the service's error code and the HTTP status, read off the response before the SDK discards it, and names the blob. (#330) -
KnowledgeStore::ingestcommitted once per chunk on a durable index.ingestloopedupsertper chunk. OnMemoryVectorIndexthat is cheap; on theRecordVectorIndexadded in #323 every call is a full OCC commit — re-serialise the shard, upload it as a blob, write a new manifest revision — so ingesting one three-chunk record advanced the manifest three times and left two superseded shard blobs forgc. At the default 256 shards and ~100k vectors a shard is ~600 KB, so that was ~1.8 MB of blob writes for one record. It is now oneupsert_manyper record. One behaviour change falls out: every chunk is embedded before anything is written, so a failing embedding now writes nothing, where before the earlier chunks were already committed. Orphaned chunks (from a record that shrank) are still removed one at a time; batching them needs a newVectorIndexmethod, which cannot be called fromgonzalo-knowledgeuntil the workspace version is bumped, becausecargo publish --dry-runverifies each packaged crate against the publishedgonzalo-vector. (#326) -
A sweep could delete a blob between a writer's upload and the manifest commit naming it. Both manifest writers upload blobs before committing, so a
gonzalo gc(orgonzalo index --gcunder--watch, which needs no operator) landing in between deleted the blob; for a vector shard that left the index unopenable and unrecoverable. A sweep now keeps unreferenced blobs younger than the minimum age, and each writer re-checks the blobs it newly referenced after its manifest commits and re-uploads any that are gone. This narrows the race rather than closing it: a writer that crashes between its commit and that re-check, or a live writer whose old blob a sweep had already decided to delete, can still lose it, and the minimum age is also the clock-skew tolerance: a GC host running ahead of the store defeats it. See ADR 0028. (#325) -
gonzalo gcno longer deletes live records' content. The mark set was built from graph manifests alone — the only blob references that existed when it was written — so on any store holding a record whose body is a blob, a sweep deleted that record's bytes while the record still pointed at them. The dangling reference surfaced later, as a failed fetch, with nothing to tie it back to the sweep. GC now marks from the records themselves, and an undecodable manifest is an error rather than an empty reference set. (#292) -
S3 hardening, before any backend other than RustFS is qualified. Four fixes from the #203 review, none of which affected the pinned RustFS: an ambiguous commit (the write applies, the response times out, the retry sees
412) no longer comes back as aConflictfor the caller's own write; aGetObjectthat returns no ETag is now an error instead of an emptyIf-Match, which a permissive server could read as "no condition" and turn every compare-and-swap into a blind overwrite; a409retries after a jittered, bounded delay rather than immediately, so a burst cannot spend all eight attempts inside one in-flight window; and consumerlistreads keys in bounded batches instead of one at a time. The soak's bucket is now created with the maintained AWS CLI image — MinIO stopped publishingmc, which had already broken CI once. (#286) -
Git
pullhonours ancestor ordering, likesync. For a record changed on both sides, pull went straight to comparing kinds and bodies, so a remote that had simply advanced past the local revision — the normal state after a peer received the record throughsyncand wrote again — was reported as a divergence. A remote delete came back as a delete-versus-edit conflict that kept the deleted record alive, and an advance of anOpaquebody, a checkpoint say, conflicted too, whilesyncfast-forwarded both. Pull now checks ancestry first and takes the side that is ahead, naming those keys in the newPullReport.advanced. Nothing was ever lost — the old behaviour failed safe and kept the local side — but pull and sync disagreed about the same pair of stores. (#289) -
The git store writes record files atomically. It wrote with
std::fs::write, which truncates the file before writing it, whilegetandlistread the working tree without the repo lock. A read that landed mid-write saw an empty or half-written record and failed with a serialization error, and a crash between the truncate and the write left a zero-length record. Since 0.7.0 a delete writes a tombstone file too, so deletes had the same exposure. A write now goes to a temp file that is synced, renamed over the record and followed by a directory fsync, as the fs store already did, so a reader sees the old record or the new one and never a torn file. (#283)
Docs
- The container guide covers scoped auth.
docs/container.mdlistedGONZALO_AUTH_FILEbut never said what goes in it. It now documents the mode precedence, the principals file with an example, the_blobsnamespace for blob access, the 401/403 responses on both transports, and how writes are stamped with the principal's name. It also listsGONZALO_ANCESTOR_CAP. (#281) - The
gonzalocrate docs have runnable examples. The docs.rs page for the crate consumers depend on was a bare list of re-exports; the only walkthrough lived in a private test that never reached the published docs. It now opens with four doctests: store and read a record, update it conditionally and handle the conflict, delete it and watch the delete survive a sync, and map a typed view throughRecordCodec. They run in CI, so they cannot drift from the API. (#307)
0.7.0 - 2026-09-15
Deletion replicates. A delete used to be local: sync met a peer that still held
the record and copied it straight back, so a deleted record returned on the next
run and a namespace-wide reset was unusable on anything replicated. This release
makes a delete a write of its own — a tombstone that travels like any record —
and builds namespace reset and horizon-based collect on top of it. It also
adds fleet access-control records, the identity, role, channel and audit state
the Ariel chat bridge keeps in gonzalo.
Upgrade every gonzalo binary together. Deletion now replicates, and it does
so with a record kind older binaries can't read. A 0.6 gonzalo, gonzalod, or
any program built on gonzalo-core 0.6 that reads a store holding a tombstone
fails on that key with a serialization error: loud, and no data is lost. The
silent failure is worse: a 0.6 binary that runs sync against 0.7 data can't see
tombstones and copies deleted records back, and nothing on the new side can tell
that apart from a genuine recreation. Upgrade every binary that reads a store
directly, and every binary that runs sync, at the same time. A 0.7 client
against a 0.6 gonzalod keeps working for normal reads and writes, and fails
replication reads with an explicit upgrade gonzalod error rather than falling
back. See the guide's "Deletion, reset & collection" page and ADR 0021. (#203)
The new fleet access-control record kinds need the same all-at-once upgrade: a binary built before them fails to decode those records. Upgrade every gonzalod, CLI and embedded consumer before any writer uses them. (#278)
Added
-
Replicated deletion. A delete writes a tombstone (
RecordKind::Tombstone) at the record's own path, andsyncand gitpullpropagate it, so a delete on one store sticks everywhere instead of coming back on the next sync. Tombstones carrydeleted_at(ms since the Unix epoch), and records carry a boundedancestorslist. Both are optional fields, so live records written by 0.7 still read on 0.6. See ADR 0021. (#203) -
gonzalo delete --namespace <N> --collection <C> --id <I> [--expected <REVISION_JSON>] [--root <DIR>] [--ancestor-cap <K>]: delete one record by writing a tombstone attributed togonzalo-cli.--expectedis therevisionobject fromgonzalo get's JSON output (e.g.{"counter":3,"hash":"…"}).- On success, including an absent or already-deleted record, prints
deleted: N/C/Iand exits0. - On a stale
--expected, printsconflict: N/C/Iandcurrent: {…}and exits3. - Malformed
--expectedexits2.
(#203)
-
gonzalo reset --namespace <N> [--collection <C>] [--root <DIR>] [--ancestor-cap <K>]: tombstone every live record in a namespace or collection. It's not atomic but it's safe to re-run. PrintsX deleted, M conflicts, plus oneconflict: <ns/col/id>line per conflict on stderr. When there are conflicts, it also prints a finalre-run \gonzalo reset` to delete the M conflicted record(s)line on stderr. Exits0with no conflicts and3with any (re-run to finish). Omitting--namespaceis a usage error (exit2`). (#203) -
gonzalo collect --older-than <DURATION> [--namespace <N> [--collection <C>]] [--root <DIR>] [--ancestor-cap <K>]: physically purge tombstones older than a horizon you choose.<DURATION>is a positive integer plus exactly one unit ofd,h,mors(e.g.30d), with no default. Zero, a missing, compound or unknown unit, or overflow exits2, as does--collectionwithout--namespace.- It prints
horizon:,purged:,unstamped:andconflicts:lines, and exits0even with conflicts. - Purging a tombstone before every peer has synced lets the deleted record come back from that peer.
- Tombstones with no deletion time are never collected, and clock skew can only delay collection.
- The guide has a section on choosing a horizon.
(#203)
-
Common to
delete,resetandcollect:--root <DIR>(default., expands a leading~) and--ancestor-cap <K>(default 32).0exits1withancestor cap must be at least 1.- Exit codes:
0success,1error,2usage error,3conflict (deleteandreset). Each command's--helplists its codes. - The CLI operates on a local fs store only.
(#203)
-
Replication surface on
Store:get_rawandlist_rawshow tombstones;put_raw(record, expected)stores a record with its revision verbatim and never re-stamps the revision (through the daemon, a non-admin caller still becomes the author, while admin and open mode keep the replicated author);purge(key, expected)is the only physical removal left. (#203) -
Store::delete_as(key, expected, author)records who deleted a record as the tombstone's author. The daemon stampsPrincipal::delete_author(claimed): a non-admin is always stamped as itself; an admin keeps a named deleter, or is stamped as itself when there is none; a daemon without auth keeps a named deleter, or stamps none when there is none. The HTTPDELETEbody and the gRPCDeleteRequestgain an optionalauthor/author_jsonclaim, whichServerStore::delete_assends. CLIdeleteandresetboth stampgonzalo-cli. Libraryreset, and the provideddeleteit uses by default, keep each record's own last author instead;reset_asnames a deleter, the same waydelete_asdoes. (#203) -
Daemon routes:
GET /v1/raw/records/{ns}/{col}/{id}andGET /v1/raw/keys(readon the namespace;readon*when unscoped);PUT /v1/raw/records/{ns}/{col}/{id}(writeon the namespace);POST /v1/purge/{ns}/{col}/{id}(admin);- the matching
GetRaw,ListRaw,PutRawandPurgeRPCs.
(#203)
-
Ancestor cap:
--ancestor-cap <n>ongonzalo delete,reset,collectandsync, and theGONZALO_ANCESTOR_CAPenvironment variable ongonzalod, set how many prior revisions a record remembers. The default is 32 and 0 is rejected. Peers with different caps interoperate. (#203) -
gonzalo_core::{reset, reset_as, collect, ResetReport, CollectReport}: library reset (keeps each record's own author;reset_asnames a deleter) and explicit tombstone collection. Both stop at the first store error, and re-running either afterward is safe. All five are also re-exported from thegonzalofacade crate. (#203) -
SyncReport.fast_forwarded_to_a/fast_forwarded_to_b: the keys overwritten on each side because it was behind the other's ancestor chain. (#203) -
Fleet access-control records. Six record kinds for people and their access to the fleet:
Person,IdentityBinding,RoleGrant,ChannelConfig,LinkTokenandAuditEntry. Typed views live ingonzalo-domain'sfleetmodule and are re-exported by thegonzalofacade. Records sit in thefleetnamespace, with audit entries infleet-audit. A link token stores only a hash of its secret and is marked consumed when redeemed; audit entries are write-once. gonzalo stores these records but doesn't evaluate the roles in them. A channel config is keyed by provider, tenant and channel, and carries what the channelfollows(the whole fleet, or a non-empty set of workspaces), anotifypreset ofall,terminalorfailures, and a roleceiling. Grants scope to the fleet or to one workspace. See ADR 0022 and ADR 0023. (#278) -
SyncReportsays when sync did not converge. A newunconvergedfield lists the keys whose write was still losing a race when sync gave up after its pass limit, andSyncReport::converged()reports whether any pass landed cleanly. Before this, a run that never won a race returned an emptyconflictslist, which read as "in sync" while the two stores still disagreed.gonzalo syncprintsunconvergedand warns on stderr. (#290)
Changed
-
Breaking:
CoreErrorgainsInvalid. A call the store will never accept — a consumerputof a tombstone record, which must go throughdelete_as— now returnsCoreError::Invalidinstead ofBackend. The daemon answers400(gRPCInvalidArgument) rather than an opaque500, andServerStorerestores it asInvalid, so a store behind a daemon fails the way a local one does. Code matching exhaustively onCoreErrormust add the variant. (#299) -
gonzalo syncnames the keys it could not settle. It printed counts only, so an operator learned a conflict existed but not where; it now prints aconflict: <key>orunconverged: <key>line per key on stderr, asdelete,resetandcollectalready do, andSyncSummary'sconflictsandunconvergedfields carry the keys rather than counts.sync --helpgained the exit-code section every other command has, andmigratenow names the directory it could not read. (#299) -
The
gonzalofacade re-exportsBlobStore,AncestryStore,sync_with_ancestryandnow_ms. WithoutBlobStore,Body::Blobcould not be used through the facade at all, though its own docs point at the trait; without the ancestry pair, a consumer wanting ADR 0016's three-way merge had to depend ongonzalo-coredirectly. (#299) -
gonzalo syncnow exits 3 on conflicts or non-convergence, instead of always exiting 0. A conflict to resolve and a run that gave up before converging are both recoverable outcomes a script should notice, which is whatdelete,resetandcollectalready signal with exit 3. A sync that converges with no conflicts still exits 0. (#290) -
Breaking:
gonzalo-core'sStoretrait gains required methods (get_raw,list_raw,put_raw,purge,delete_as),deletebecomes a provided method overdelete_as, andRecordKindgainsTombstone, so this release is 0.7.0 across the workspace. The replication methods have no default implementation on purpose:get_raw = getwould compile, pass every test that doesn't delete, and resurrect records in production. Third-partyStoreimplementations get a compile error naming exactly what to add, and must implementdelete_asinstead ofdelete. (#203) -
Sync copies one-sided records with
put_raw. A delete landing on the destination between sync's read and its copy now conflicts and is resolved on the next pass. Before, the copy became a recreation and brought the deleted record back. (#203) -
s3 retries lost conditional writes. A write that loses an
If-Matchrace —PreconditionFailed(412),ConditionalRequestConflict(409), orNoSuchKey(the object vanished to a concurrent purge) — re-reads, re-plans and retries for up to 8 attempts before returning a backend error, so s3 reaches the same outcomes as the lock-based fs and git stores. (#203) -
Store::deletewrites a tombstone instead of removing the record.getandlisthide tombstones, so applications see no difference. Deleting an already-deleted or absent key is still an idempotentDeleted. ADR 0018's local-only decision is superseded by ADR 0021, and its conflict semantics are unchanged. (#203) -
Writing to a deleted key re-stamps the revision. A
putwith no expected revision over a tombstone stores the record with a revision one past the delete, so the recreation wins over the delete when synced.putstill returns the stored revision; a caller that ignores it and reuses its own will get a conflict on its next conditional write. Aputwith any expected revision over a deleted key returnsNotFound. (#203) -
The daemon returns 412 for a write rejected as not found. A consumer
PUT /v1/records/{ns}/{col}/{id}(or rawPUT) whose expected revision the store rejects withNotFound, such as a conditional write over a deleted key, now returns HTTP 412 (gRPCFailedPrecondition) instead of an opaque 500 (Internal).ServerStoremaps it back toCoreError::NotFound. 404 is deliberately not used, because on the raw routes it signals a daemon too old to serve them. (#203) -
Raw writes through the daemon keep ADR 0015's authorship rule. A
put_rawfrom a non-admin principal hasmeta.authorrestamped to that principal, like a consumerput. An admin principal, or a daemon without auth, keeps the replicated record's author, so an admin token is the replication credential. The revision is never re-stamped in either case. (#203) -
Sync fast-forwards when one side is simply behind. Using each record's ancestors, sync now overwrites the stale side instead of running a merge, and reports the overwritten keys in
SyncReport.fast_forwarded_to_a/fast_forwarded_to_binstead ofmerged. Before, anOpaquekind such asCheckpointreported a conflict even though nothing had diverged. A delete racing an edit of the same revision is reported as aSyncConflictwith neither side written. Records written before 0.7 have no ancestors and take the old merge path unchanged.gonzalo syncnow prints afast_forwarded: Nline, and its output labels are re-aligned to the width of that line. (#203) -
git
pullhandles tombstones on paths changed on both sides: concurrent tombstones converge on the newer one, and delete-versus-edit is reported as aPullConflictthat keeps local. (#203) -
The HA soak and s3 conformance require RustFS ≥ 1.0.0-rc.6, whose conditional
DeleteObjectis atomic; earlier releases can let a purge remove a concurrent recreation.docker-compose.rustfs.ymlpins it. (#203) -
The HA soak adds replicated-deletion invariants. It races deletes against edits and recreations across
gonzalodreplicas under replica-kill chaos, and checks that acknowledged deletes survive replica kills, that no two edits commit on the same base revision and no edit commits on the same base as a delete that actually wrote a tombstone (StaleBaseCommitted), that every run observes at least one delete conflict seeded deterministically so the check doesn't depend on scheduling, that every replica agrees on each lifecycle key's deletion state, and that consumer reads match raw reads. It now also triggers on changes togonzalo-coreandgonzalo-store-server, and its timeout is 30 minutes. The tombstone conformance cases run on fs, git, s3 and the daemon client. (#203)
Docs
- ADR 0021, superseding ADR 0018's local-only deletion decision; the "Deletion,
reset & collection" guide page, with horizon guidance; the storage, daemon
and CLI guide pages updated for the replication surface and authorship
rules; the tombstone replication spec aligned with what shipped; the Mem0
parity matrix
Deleteandresetrows, both citing ADR 0021. (#203)
0.6.0 - 2026-09-10
Resolution that uses what the syntax already says, and prebuilt binaries to run
it with. The code graph stops identifying every symbol by its bare name: a call
site's qualifier, a definition's owning type, a receiver's written type, and a
file's own imports are all recorded and consulted, so Foo::get and Bar::get
stop being one node. Each rule only ever narrows — where it cannot settle a
question it declines and says so, and the counts of what it declined travel with
the answer.
Much of this was found by pointing gonzalo at code it was not designed against.
Seven of the sixteen tickets here came from indexing a C++ engine, a Next.js
app, a Node service, three Java and Kotlin course repositories and two Python
projects — .h headers parsed as C, #include ignored entirely, Java and Kotlin
imports unrecorded, React's wrapper idiom yielding zero symbols per file. Two of
them corrected tickets written from inside this repository.
Fixed
-
The guide's binary install example no longer pins a tag. It pointed at
v0.5.0, which carries no release assets — prebuilt binaries start with this release — so the documented install 404'd from the moment it was published, and pinning any concrete tag would re-create that on every release. It now reads the latest tag, with a note that archives begin afterv0.5.0. (#242) -
A skipped file now says which file. When an isolated parse worker dies on a file, the CLI reported
skipping a file — parse worker error: ...without naming it. On a 2000-file tree that left no way to find the offender but bisecting by directory, which is exactly what landing #266 cost: four rounds of re-indexing subtrees to locate one 13843-line generated header that aborts tree-sitter-cpp.That matters more than the inconvenience. A skipped file is silently absent from the view —
searchfinds none of its symbols,callersmisses every call it makes — so the one line saying it happened has to say where.The message names the path, and
IndexSummarycarriesskipped_pathsso a caller can list them rather than only count them. The summary listing is bounded at five with a count of what it left out, matching how #259 bounds its extension list. No extraction change. (#270) -
.hheaders are parsed as C++, not C..his the conventional header extension for C++ as much as for C, and the C grammar mis-parses C++ rather than failing:enum class Color { Red };recordedColoras a function and invented a symbol literally namedclass.Found by indexing a real C++ project (2017 files):
before after symbols extracted 82331 88385 symbols from .hfiles22951 29005 symbols named class372 0 keyword-shaped names in total 470 26 Those 372 were spread across 133 files and outranked every genuine symbol in the ambiguity ranking. The 6054 new symbols are the real classes, namespaces and scoped enums those headers declare — which for a C++ project is where its types and interfaces actually live.
Safe because the two grammars agree on plain C, which is now pinned by a test comparing symbol and reference extraction under both rather than asserted. The residual 26 keyword-shaped names come from
inline namespaceinside a#definebody, a preprocessor limitation gonzalo already documents.One caveat worth stating: a 13843-line generated header in that project parses under the C grammar and aborts tree-sitter-cpp. Crash isolation handled it exactly as designed — the worker died, the pool respawned, one file of 2017 was skipped and indexing continued.
.hfiles also now report ascpprather thancinoverview's language breakdown.EXTRACTION_VERSIONis 10. (#266) -
Indexing a tree gonzalo cannot parse now says so. A file whose extension names no known language was dropped uncounted — not in
files, not inskipped, not inignored— so a repository gonzalo could not read reported every number at zero with no explanation. Querying that view afterwards returned empty answers that looked like real ones, the same failure shape as #210 and #228.Found by indexing a Vanderbilt machine-learning course repository whose entire content is one Jupyter notebook. Before, it printed four zeros and nothing else. Now:
files: 0 ignored: 0 files, 2 dirs not descended unindexed: 4 files, no grammar for .csv, .ipynb, .md, .txt note: nothing was indexed — gonzalo parses none of the files in this treeThe count is distinct from
skipped(a parse worker crashed on it) andignored(excluded on purpose), and it names the extensions responsible so the message is actionable. A file that was going to be excluded anyway stays as invisible as it already was, soignoredis unchanged and a README inside a vendored tree does not start appearing. The extra note fires only when a run indexed nothing at all, which is the case most easily misread. No extraction change, so no re-walk. (#259) -
A JavaScript or TypeScript function wrapped in a higher-order call is now a named function.
export const Controls = React.memo(() => { ... })is the dominant React idiom, and gonzalo extracted zero symbols from files written that way: the binding's value is a call rather than an arrow, so the component had no name in the graph and every call in its body had no caller. Object properties bound to a function ({ fetchAll: () => {} }) were missed too, because the name lives inkeyrather thanname.Measured by re-indexing a real Next.js app:
before after symbols extracted 482 638 production references with a caller 1799 2247 production references orphaned 589 141 Test files are deliberately unchanged (4581 to 4564). Their calls sit inside anonymous Jest callbacks —
it('...', () => { ... })— which genuinely have no name, and inventing one would be worse than reporting none. The residual 141 are top-level statements likeexport default React.memo(ColorPicker), which correctly have no enclosing function.The wrapper must be called on a bare identifier (
memo,forwardRef,styled) or a capitalized namespace (React.memo). That is what keepsitems.map(x => f(x))out: an iterator method returns an array, and treating its binding as a function would attribute everything inside the lambda to that binding instead of to the function actually containing it — trading a missing answer for a wrong one. Both exclusions are pinned by tests.EXTRACTION_VERSIONis 8. (#257)
Changed
-
Calls in module-level declarations are attributed, and say so. A great deal of modern TypeScript lives in module-level builder objects — a Zod schema, a tRPC router — where a call sits inside a named binding but not inside a function. Those calls reached neither
callersnorimpact.They are now attributed to the nearest named binding. Since that widens what a reference's
frommeans, the scope travels with it rather than the meaning changing silently:Reference::from_scopedistinguishesFunctionfromModule, and the MCPcallerstool keeps its exact previous meaning — functions that call this — with the module-level ones in a newmodule_callers.On a real Next.js/tRPC codebase (815 files):
before after production references with a caller 20754 27524 production references orphaned 7795 1025 production orphan rate 27% 3.6% Strictly additive, and measured as such: function-scope attributions came to 20754 — exactly the number that had a caller before, not one changed. The 6770 module-scope attributions are pure addition. A call inside a real function keeps today's answer, including the iterator case #257 guards.
The nearest binding wins, so
z.string()under aPORT:key reportsPORTrather than the enclosing schema. Deliberately over-inclusive in the same wayRefKind::Valueis, which is exactly why the scope is a separate field rather than folded in. Test files are unchanged: their calls sit in anonymous Jest callbacks, which genuinely have no name. JS/TS only; Python's module-level tables and Rust'sstaticinitializers are the same shape and each needs its own evidence.EXTRACTION_VERSIONis 13. (#268) -
CommonJS
require()is recorded as an import. JavaScript and TypeScript extraction handled the ESimportstatement only, so a Node codebase written in the older module system got no import-aware resolution at all. All three binding forms are covered:const fs = require('fs'), destructuring, and a rename, which records under its local name as the ESasalias already does. A computedrequire(name)records nothing, since there is no literal path to key on.On the one CommonJS-heavy repository available, imports went from 4 to 93 across 22 files, and relative paths resolve as expected —
require('../util/aws')recordsutil/aws.Worth stating plainly: that repository has no name collisions among its imported symbols, so this resolved zero ambiguous references there. The extraction is correct and the payoff is prospective — a CommonJS codebase with internal collisions gains what #252 gives every other language. It is not demonstrated on the corpus at hand.
EXTRACTION_VERSIONis 12. (#269) -
C and C++
#includedirectives are now recorded and used to resolve. #252 and #260 gave import-aware resolution to six languages; C and C++ had none, even though an include is a stronger signal than any of them. A package import names a package that a file path is assumed to mirror; an include names the file.On a real C++ project (2017 files), 9860 includes recorded across 1614 files. Of the 17884 ambiguous references in files that include something, 4905 have an include pointing at a candidate and 4749 now resolve — the largest single resolution gain of any change in this cycle.
An include names a path rather than a symbol, so it is recorded with no name and matched by path.
Import::brings_whole_filesays so explicitly rather than callers testing for an empty string. Dropping the extension is what makes it work: a prototype in a header is not a symbol, the definition in the translation unit is, and both sit at the same path stem, so includingrender/pipeline.hresolves torender/pipeline.c.Narrowing only, as established in #248: a system header matches nothing in the view and leaves the reference ambiguous, and an include fitting several candidates declines unless one is unambiguously nearer.
EXTRACTION_VERSIONis 11. (#267) -
Imports now anchor to the file that wrote them. #252 matched an import's module as a free-floating path suffix, which fails exactly where a codebase holds two copies of the same tree — a vendored reference beside your own work, or one package tree per assignment. Every copy matched, so the reference was correctly but uselessly declined.
Two lexical rules fix it, neither needing a project root. A Python relative import carries its dot count now, so
from ..DataTypes import Actionresolves by walking up from the referencing file's own package — arithmetic on a path gonzalo already has. And when an absolute import matches several candidates, the one sharing the longest path prefix with the referencing file wins, since a file means the copy in its own tree.Measured on four repositories, counting ambiguous references whose file imports the name:
repo candidates resolved before after CS-5260 (Python) 85 0 65 cs-395-spring-2024 29 14 19 cs-5253-fall-2023 701 0 185 cs-5254-summer-2023 679 0 49 Confirmed through the server rather than only in tests:
impacton a symbol defined identically in two parallel trees now reaches 18 callers, all 18 in the importing file's own tree and none in the other.Both rules stay narrowing, as #248 established. A tie is left ambiguous rather than broken by a coin flip, candidates sharing no prefix at all are not ranked, and a relative import claiming more levels than the path has falls through to the older rules.
EXTRACTION_VERSIONis 9, and the store gains an import depth column. (#261) -
Java and Kotlin imports are now recorded. #252 shipped import-aware resolution for Rust, Python, JavaScript and TypeScript and left out the two most import-dense languages gonzalo supports — every type from another package must be imported by name, so the signal is denser there than anywhere else. Found by indexing three Vanderbilt course repositories, which between them contained thousands of import lines and recorded zero.
repo files imports before after cs-395-spring-2024 401 0 2657 cs-5253-fall-2023 728 0 6620 cs-5254-summer-2023 752 0 5642 A Java static import records the member it brings into scope rather than the class holding it, and a Kotlin
asalias records under its local name. A wildcard records nothing, as with Rust's glob. Kotlin needed one wrinkle: its grammar drops the*, soimport a.b.*andimport a.bparse identically and the source text is the only way to tell them apart.Extraction only — resolution already consumed imports generically.
EXTRACTION_VERSIONis 7, so each view re-walks once on upgrade. (#260) -
Imports narrow an otherwise ambiguous reference. Resolution was file-scoped and consulted nothing outside the calling file, so a name defined in several places with none of them local was simply dropped — even when the file's own imports named exactly one of them.
Each file's imports are now recorded and consulted at the point the ladder would otherwise give up.
use crate::model::makeprefers themakeundermodel.rs. This is deliberately lexical, not a resolved module path: resolving an import to a file needs the crate root, which means reading the manifest and modelling the workspace, and extraction would then depend on files other than the one being parsed (ADR 0012). An import says which module a name came from, and a module's name almost always appears in its own file's path, which is enough. Three layout conventions are folded in: an entry file (mod.rs,lib.rs,main.rs,__init__.py,index.ts) names its container rather than itself, asrcdirectory is build layout rather than a module, and-and_compare alike so a crate writtengonzalo_climatchesgonzalo-clion disk.It only ever narrows, following #248: when the import fits several candidates the reference stays ambiguous, which is the honest answer, and nothing that resolves today stops resolving.
Measured by indexing three real repositories — gonzalo itself, caliban, and a Next.js app:
repo imports recorded ambiguous refs whose file imports the name now resolved gonzalo 1408 27 11 caliban 5803 68 45 web-viz-next 782 21 21 A bare
@in a JS/TS import is a tsconfig path alias rooted at the project (@/lib/utils), not a directory, and left in place it added a segment that could never match — the Next.js app resolved 0 of 21 until it was dropped, and 21 of 21 after. A scoped package (@testing-library/react) leads with@scope, which is a real name and survives.Rust, Python, JavaScript and TypeScript. A glob import contributes nothing, since it introduces names the file never spells out.
EXTRACTION_VERSIONis 6, so each view re-walks once on upgrade, and the store gains animportstable. (#252) -
Method calls are attributed when the body says what the receiver is. A call through a receiver of unknown type is deliberately not attributed, because guessing was measurably wrong (#223) — but a large share of receivers have a type written down a few lines up. Three purely syntactic shapes are now read: a binding initialized from a constructor-style call, a binding initialized from a struct literal, and a parameter with a written type, including behind a reference.
selftakes the enclosingimpl.On gonzalo's own source 1043 of 6732 method references now carry a type, 437 of which name a type the view defines;
impactoninsertwent from 60 symbols reached to 219, and its unattributed method edges fell from 368 to 274.insertis the seed that #248 could not move at all, because it is reached almost entirely throughstore.insert(..).This is syntax, not inference, and the limits are deliberate: a receiver from a plain call, a generic parameter, or a trait object still declines and is still counted, because choosing an impl is the guess #223 removed. Bindings are scoped to the function body they appear in. Rust only for now; every other language records no receiver type and behaves exactly as before.
EXTRACTION_VERSIONis 5, so each view re-walks once on upgrade. (#251) -
callersandcalleesnow report how ambiguous the queried name is. Both are raw name matches, so a name defined in several places returns the merged answer for all of them — and a bare list said nothing about that, which is the point where a heuristic result is most easily read as a precise one. Each now answers with adefinition_countalongside the list, plusdefined_inwhile there are few enough paths to be a lead rather than noise. A count of 0 means the name is defined nowhere in the view, which is what makes an empty list readable rather than ambiguous between "nothing calls this" and "you asked the wrong question".This changes the shape of those two tool results: the list moved under a
callers/calleeskey, so they return an object rather than a bare array.impactalready reported the edges it declined; this brings the same honesty to the tools that do no resolution at all. (#249)
Fixed
-
unreferencedno longer reports live callbacks as dead code. A function passed as a value rather than called —and_then(helper),register(handler)— is a path expression, not a call expression, so it recorded no reference at all and the function looked unused. That was the documented main false positive of the one tool whose suggested action is deletion. On gonzalo's own source it was flagging 98 symbols that are genuinely used, among them the test fixtures handed tobuild_rustand constants passed to functions.Such a reference is now recorded with its own kind and deliberately kept out of the call graph:
callers,calleesandtopstay call-only, because passing a function is not calling it.impactcounts them in a newvalue_edgesrather than walking them — a callback is a real dependency, so dropping it silently would under-report, but extraction is per-file and cannot tell an identifier naming a function from one naming a local, so traversing them would put false edges back into the walk #207 and #223 cleaned up. Non-zerovalue_edgesmeans go look.The over-inclusion is deliberate and one-directional: a local variable handed to a call is recorded too, which for dead-code detection errs towards not calling something dead.
EXTRACTION_VERSIONis 4, so each view re-walks once on upgrade. (#250)
Added
-
Qualified symbol identity in the code graph. A call site's path qualifier and a definition's owning type are now both recorded, so
Beta::get()is attributed toBeta'sgetinstead of merging with every othergetin the view. Two pieces of disambiguating information were sitting in the syntax tree and being discarded: a qualified callee was reduced to its trailing identifier, and the walk tracked only the enclosing function, so a method in animplblock was stored as a bare name with nothing recording what it hangs off. The qualifier only ever narrows — when it names something the view does not own, such as a file module or a dependency, it is ignored and the previous rules apply, so no edge that resolved before stops resolving now. Recorded where a grammar makes a path unambiguously a path (Rust, C++, PHP); Go'spkg.Func()and Java'sFoo.bar()cannot be told from a receiver call at this level and stay unqualified rather than record a variable as a type.EXTRACTION_VERSIONis 3, so each view re-walks once on upgrade. (#248) -
Prebuilt macOS Apple Silicon binaries on every tagged release. A
v*tag now also buildsgonzalo-vX.Y.Z-aarch64-apple-darwin.tar.gz(plus a.sha256) and attaches it to the GitHub Release. The archive holdsgonzalo,gonzalo-mcpandgonzalo-parse-workertogether, which is the point: installed separately from crates.io those three can land at different versions, and a CLI newer than its parse worker indexes with the old parser and then marks the view current (#212, #228).aarch64-apple-darwinis the only target built; the container image covers Linux. Seedocs/releasing.md. (#229)
Changed
-
ParseModeandWorkerSourceare#[non_exhaustive]. They ship for the first time this release, and the set of ways a worker can be located is already known to be growing, so sealing them now avoids a breaking change later. (#241) -
Recorded the Rust-native deliverables principle as ADR 0020: gonzalo ships Rust crates, the container image and its own binaries — not client SDKs in other languages — and non-Rust consumers integrate over the daemon plus its published schema. The decision was real but written down nowhere, so the competitor parity evaluation kept ranking Python/TS SDKs as the single most convergent gap in the whole evaluation. All five SDK rows are now marked
by designwith the citation. (#196) -
docs/releasing.mdno longer claims version bumps are exempt from rate limiting. crates.io enforces a second limit on updates to existing crates, and a 24-crate workspace trips it on every release —v0.5.0stopped after 23 of 24. The expected 429-and-resume flow, and a check that every crate is live before creating the Release, are now documented. (#227) -
Documented that the gRPC decode ceiling is per-server, not per-method: raising
GONZALO_MAX_BLOB_SIZEalso raises the decode limit for record RPCs, which HTTP does not do. Bounded and authenticated, so documented rather than closed. (#194)
Fixed
-
The packaged
gonzalobinary is portable. It linked Homebrew's libgit2 at/opt/homebrew/opt/libgit2/…, an absolute path in the Mach-O load command, so on any Mac without Homebrew it died in dyld before executing an instruction — not even--versionran.gonzalo-mcpandgonzalo-parse-workerwere always clean, so the archive shipped one broken binary out of three, and it was the one that creates the view the MCP server reads. libgit2 is now vendored and statically linked, matching what the Linux container already did by accident of having nolibgit2-devinstalled.scripts/package-macos.shnow refuses to package a binary that links anything outside/usr/liband/System/Library, because which way this went depended on whether the build machine happened to have a system libgit2. (#246) -
Ticket connectors keep the provider's error message instead of discarding it.
SourceError::Backendis documented as carrying the provider's message, but all five connectors reached it through reqwest'serror_for_status(), which drops the response body — so an expired token, a missing scope, a malformed JQL query or a rate-limit hint all surfaced as a generic "HTTP status client error". Twenty call sites across Jira, GitHub, GitLab, Asana and Linear now report the status and the provider's own reason, bounded in length. (#240) -
gonzalo syncexpands a leading~in its two store roots. Those are positional arguments and were the one place #211's expansion did not reach, sogonzalo sync '~/a' '~/b'from a non-shell context silently operated on two directories that did not exist and reported success. (#238) -
The parse-worker version note no longer claims correct data is wrong, and no longer repeats. A worker released before
--extraction-versioncannot report one while still producing current extraction, so that case now reads as an unconfirmed note rather than a warning to go fix your data; a worker that reports a different version still warns plainly. It is also emitted once per process rather than once per index, which under--watchmeant once per file save. (#239) -
Remote reads report failures the way remote writes do.
ServerStore's read paths used reqwest'serror_for_status(), which discards the response body, so a403on a read surfaced as a generic HTTP status line while the same refusal on a write reported the daemon's reason. All four HTTP read paths now carry status and body;404still meansOk(None)onget/get_blob. (#195) -
A leading
~in a store root is expanded to$HOMEinstead of being taken literally.GONZALO_ROOT=~/.gonzaloin an MCP client config reaches the process verbatim — there is no shell in that path — so it created a directory literally named~in whatever the working directory happened to be, and every query then answered from the wrong, empty store. The form users naturally write, and the one that works when tried in a shell, was exactly the form that silently misbehaved where it mattered. Expansion coversGONZALO_ROOTand every--rootargument, which has the same exposure from a systemd unit or container spec, andstatusnow reports the expanded path so it is verifiable. Only a leading~is touched:~otheruser/…is left alone, and a~anywhere but the front is an ordinary directory name. (#211) -
gonzalo indexsays which parse mode it is using instead of silently degrading. Crash isolation depends on findinggonzalo-parse-worker, and when it could not, indexing quietly parsed in-process — no log line, no warning, no way to tell. The two modes produce byte-identical graphs, so the loss was invisible until a tree-sitter grammar aborted and took down the whole run instead of skipping one file. Aparse:line now names the worker and how it was found; the fallback prints a warning listing every location searched; and--require-parse-workerturns a missing worker into a hard error for CI and container builds. Lookup also coversPATH, so symlink and split-install layouts — where a sibling check cannot succeed but the worker is plainly available — find it. (#212) -
A stale
gonzalo-parse-workerno longer produces pre-upgrade extraction that the view records as current.EXTRACTION_VERSIONguarded the CLI, but the worker is a separately installed binary and is what actually parses — so a 0.5.0 CLI driving a 0.4.0 worker emitted old-format slices and then stamped the view with the new version, leaving it wrong and marked up to date, which no later re-index would repair. The worker now answers--extraction-version, the indexer asks before parsing, and a view is credited to the version the parse path actually produced. A mismatch warns, rebuilds in full, and self-heals once the worker is upgraded. A worker too old to answer is recorded as unknown rather than assumed to agree. (#228)
0.5.0 - 2026-08-22
Code-graph correctness. impact and reference resolution stop asserting edges
they cannot justify — a closure that returned half the repo for one seed, and a
name-matched resolver that credited std methods to same-named project functions.
Alongside that, the MCP server gains aggregate queries that answer questions about
a whole view rather than about a symbol name the caller already has.
Upgrading: the first index after this release does a one-time full re-walk
of every view (see EXTRACTION_VERSION below) — expected, not a fault. Reinstall
the binary as well as re-indexing: the resolution fixes live in the query path, so
a stale gonzalo-mcp keeps returning the old answers over freshly indexed data.
Fixed
-
A method call no longer claims a same-named free function (#223).
Resolution::UniqueGlobalattributed a reference to the sole definition of that name in the view — including when the name was really a std or dependency method. In gonzalo,.chain(ours_obj.keys())ingonzalo-coreresolved tofn chain(), a test fixture ingonzalo-graph, a crategonzalo-coredoes not depend on.References now record the shape of the call site (
RefKind::{Free, Method}), and a cross-file method call resolves to the newResolution::ReceiverUnknownrather than guessing. Measured over gonzalo's own source: of the 388 cross-file method calls that used to resolveUniqueGlobal, 294 pointed at a different crate —push,filter,send,bytes,nextand friends. Same-file method calls still resolveLocal, and free and path calls (foo(),a::b::foo()) are unaffected.Effect on
impact(#207), same 122-file source both runs:seed before after build_rust185 126 assemble60 23 resolve_references_to6 6 The 24 provably-false
gonzalo-corenodes in thebuild_rustclosure are now 0. Dropped edges are reported asreceiver_unknown_edges, counted separately fromambiguous_edgesbecause the cause differs: not "too many candidates" but "cannot claim any candidate".RefKindis omitted from the serialized slice when free, so a file of plain calls keeps its existing content hash. -
impactno longer merges unrelated code through shared identifiers (#207). The closure walked the name-matched caller graph, so one hop into a name with several definitions absorbed every subgraph sharing that identifier. The walk now keys nodes on(name, defining path)and consults the resolver for every edge: anAmbiguousreference is counted and dropped rather than traversed.On the gonzalo view, seeded at
build_rust: 356 → 178 reached names, with 10 ambiguous edges reported rather than followed. Seeds that were already sharp are unchanged (resolve_references_to: 5 → 5).The result is now a report rather than a name list — every node carries the path defining it,
ambiguous_edgessays how many edges could not be attributed (so a non-zero count means the true set may be larger), andtruncatedreports a walk stopped by the new optionalmax_depth. The daemon's HTTP and gRPC transports keep their existing name-list shape and so get the precision fix without the report fields.Of the remaining 178, 17 are still provably false and trace to a single
UniqueGlobalover-attribution — std'sIterator::chainresolving to a same-named test fixture. That is a distinct defect, filed as #223. -
An incremental re-index now prunes paths a laxer run admitted (#209 follow-up). The filter added in #218 only applied to newly walked or changed files, so an existing view kept its vendored bundles forever: a bundle never changes, so it never appears in the git diff and was never reconsidered — and once a base commit is recorded there is no full walk to clean it up. Upgrading therefore fixed new views only, which is the case least in need of fixing.
The carried-forward set is now re-checked against the current rules, including
.gitignore— necessary becausedocs/guide/book/is build output excluded by ignore rules rather than by any directory-name rule, so a path-only prune left it behind.Re-indexing the existing
caliban-ai/calibanview: 17 162 symbols → 8 549, with vendored symbols going 8 618 → 0 and the largest file becomingcaliban/src/tui/events.rs(142) instead of a copy ofmermaid.min.js. -
An unknown
repo/view_idis now an error, not an empty result (#210). Every graph query returned[]withisError: falsewhen the selector named no indexed view, so a one-character typo inview_idwas indistinguishable from a symbol that genuinely is not there — an agent read it as "nothing calls this" and reported a wrong answer as fact.Service::viewnow fails withNotFound, and the MCP layer turns that into a tool error naming the unresolved selector and listing the views that do exist, so a caller can correct itself in one round trip. A real miss inside a real view still returns[], so the two cases are finally distinguishable.diffgets the same check on bothview_aandview_b. -
Calls inside Rust macro arguments are now recorded as references (#216). Macro arguments parse as a
token_treeof raw tokens rather than expressions, soassert_eq!(f(), 1)contained nocall_expressionand the call tofwas never seen. Because assertions are where much of a codebase is exercised, this silently removed a large share of the call graph:callers,callees,impactandtop by=fan_inall undercounted, andunreferencedreported live functions as dead.Re-indexing gonzalo itself, with an identical file set (1 852 symbols both runs), references go from 10 247 to 12 196 — +1 949 edges, +19.0%.
Language::from_extension, the symbol that exposed the bug, goes from 0 recorded references to 28.Detection is token-level: an identifier whose immediate next sibling is a parenthesised token tree. A nested macro has a
!between the two and is excluded. It is deliberately over- rather than under-inclusive — a tuple-struct pattern likeSome(_)reads as a call — which matches a graph that already records constructors and enum variants as calls.The other 17 grammars were audited for the same opaque-node hole. Only C/C++ has one: a
#definebody is a single opaquepreproc_argtoken with no child nodes to read. It is left in place and pinned by a test so the gap is discoverable rather than silent. -
The indexer no longer walks vendored bundles or gitignored build output (#209).
is_indexableskipped onlytarget,.git, and dotted components, so half of a real repo's graph was not that repo's code. Membership now lives in one place (IndexFilter), shared by the full walk and the git-incremental driver so they cannot disagree: dependency/output directories (node_modules,vendor,dist,build,site-packages,third_party) and generated files (*.min.js,*.min.css,*.bundle.js,*-lock.json) are dropped on both paths, and the full walk additionally honours.gitignore.Re-indexing
caliban-ai/calibandrops it from 16 986 symbols to 8 501 (-50.0%) with zero symbols frombook/**or any*.min.js; the largest file in the view is nowcaliban/src/tui/events.rs(142 symbols) rather than a 4 231-symbol copy ofmermaid.min.js. This also removes a reproducibility hole — indexing gitignored output made the graph depend on whether anyone had run a build.gonzalo indexnow reports what it excluded (ignored: N files, M dirs not descended), and--include <path>re-admits a vendored path that a built-in rule would drop.--includedeliberately cannot override.gitignore, so no flag can make a view irreproducible.
Added
-
EXTRACTION_VERSION, and a full walk when it changes (#223). The incremental driver carries unchanged slices forward untouched, so a parser improvement never reached files that did not change — an existing view stayed permanently half-upgraded.gonzalo indexnow records the extraction format alongside the view and rebuilds in full when it differs, which is what lets #216's and #223's parsing changes actually reach an established view. -
A guide chapter for the MCP server (#208) —
docs/guide/src/mcp.md, covering install → index → register → verify → keep fresh, a tool reference grouped by the question each tool answers, and the capability boundaries. It leads with the thing nothing in the repo stated: the server only reads,gonzalo indexwrites, so an unindexed setup answers every query forever with no indication why. It also records the traps found while wiring the server up for real —GONZALO_ROOTnot expanding~(#211),~/.cargo/binmissing from the non-interactive shells an MCP client spawns, and needing to reconnect the server to pick up a newly installed binary — plus a troubleshooting table keyed by symptom. -
viewsdiscovery tool and a view count instatus(#210).viewslists every indexed(repo, view_id)with its file count and the commit it was indexed at, which makes the server self-describing rather than dependent on out-of-band documentation; comparingbase_commitagainst the checkout's HEAD also surfaces a stale view, the quieter form of the same problem.statusnow reports how many views are indexed, so the natural health-check call actually detects a server pointed at an empty or wrong store. Therepo/view_idschema descriptions now say they must match an indexed view and point atviews. -
Aggregate code-graph queries (#214) — three MCP tools that answer questions about a view rather than about a symbol name the caller already has:
overview(file/symbol/reference counts, a breakdown by kind and language, and the largest files),top(rank byfan_in,fan_out, ordefinitions— adefinitionsscore above 1 marks an ambiguous name), andlist(enumerate symbols filtered bypath_prefix,kind, andname_contains). Backed by new defaultGraphStoremethods, so every store implementation inherits them. Results are bounded and reporttotal+truncatedrather than silently cutting. -
unreferenceddead-code candidates (#214) — a fourth aggregate tool listing symbols with no inbound reference, filtered by the samepath_prefix/kind/name_containsand bounded the same way.exclude_tests(default on) drops members of amod tests/mod testblock by line range and anything under atests/directory; on gonzalo itself that is the difference between 515 hits and 40. Deliberately errs toward silence — a reference from anywhere counts, including from tests and from the symbol itself. Its false positives are documented in the tool description, the rustdoc, and a pinned test: calls inside macro arguments are not recorded at all (assert_eq!(f(), 1)registers nothing), and a function passed as a value is a path expression rather than a call, so both look uncalled.
0.4.0 - 2026-08-01
The remote-parity & backend-qualification release. Deletion and blobs — the two
gaps that kept the daemon substrate behind fs/s3 — close, so a daemon-backed
consumer now gets the full Store + BlobStore surface. Alongside them, an HA
soak harness doubles as a conditional-write qualifier for S3 backends, and its
first finding disqualifies Garage outright.
Added
Store::delete(#183) — OCC-aware record deletion across every substrate and the daemon, propagated bySync. Local-only semantics; see ADR 0018. (#185)- Blobs over the daemon (#184) — the content-addressed
BlobStoreis exposed ongonzalo-server(HTTPGET|PUT|DELETE /v1/blobs/{hash},GET /v1/blobs, plus gRPC), andServerStoreimplementsBlobStoreover both transports, so a daemon-backed consumer gets the fullStore+BlobStoresurface. Blobs previously worked only onfs/s3. Adds theGONZALO_MAX_BLOB_SIZEdaemon knob (default 64 MiB). (#192)
Changed
- S3 backends are now qualified, and Garage is not among them (#52) — atomic
If-Matchis a hard requirement for any S3-compatible backend. Garage does not provide it: gonzalo's conditional-write conformance case expects exactly 1 of 8 concurrent racers to commit, and Garage let 8/8 through on v1.0.1 and 3–8 through non-deterministically on v2.1.0 — the signature of a check-then-set, not an atomic CAS. Deployments running gonzalo over Garage can silently lose concurrent writes. RustFS (Apache-2.0) is the qualified backend; MinIO passes the qualifier but is rejected on project sustainability. See ADR 0019. (#186, #205)
Testing: an HA soak harness for stateless gonzalod replicas over an
S3-compatible store — backend-agnostic, doubling as the conditional-write
qualifier above (#52, #186); cross-crate integration tests extracted into
gonzalo-integration-tests (#190, #191).
Project: a crates.io publishing pipeline for the workspace, triggered on v*
tags (#187, #189).
Docs: competitor capability inventories and parity-gap matrices for mem0, Zep,
and Letta under docs/evaluation/ (#193); ADR 0019 recording the S3 backend
qualification (#205).
0.3.0 - 2026-07-11
The hardening & language-breadth release. A broad correctness and robustness sweep — a 20-finding QA pass turned into fixes across the core merge/OCC model, every storage substrate, the daemon, the ticket connectors, and the knowledge/vector layer — lands alongside eight new code-graph grammars that take language coverage from nine to seventeen.
Added
- Language breadth for the code graph — grammars for Ruby, PHP, Bash
(#87), Kotlin (#87), Swift (#87), Lua (#87), Scala (#87), and
Elixir (#87). Elixir is homoiconic (
def/defp/defmacro/defmoduleparse as ordinarycallnodes), so it uses a value-basedwalk()dispatch on the call target's text rather than a node-kind mapping. Coverage is now 17 languages. (#126, #127, #128, #129, #130, #181)
Changed
- Ticket
RecordKeys are board-scoped — a board-scoped source folds its connection/board discriminator into the key, so the same issue imported from two boards no longer collides onto one thrashing record. Stored keys for board sources change shape and re-import on the next sync. (#159)
Fixed
- core — record-key encoding is now a reversible, injective percent-style
codec, so distinct keys can never collide onto one physical path (silent
cross-key overwrite / OCC bypass); append-only merge preserves blank and
legitimately-repeated committed lines instead of stripping/de-duping them, and
the
Derived/gc semantics are corrected. (#131, #133) - storage substrates — the git substrate locks the
putcritical section (no lost updates under concurrent writers) and detects non-fast-forward push rejection; filesystem writes fsync the temp file and parent directory for crash durability; S3 list-pagination terminates when the continuation token is absent; graph-sqlite view-db paths use the injective encoder. (#132, #134, #144, #145) - daemon — authorization runs before request deserialization, internal backend errors are returned opaquely (no path/bucket/SQLite leakage), the PUT record route validates its URL path against the body key, and the remote client surfaces daemon 403/413 responses instead of masking them as a decode error. (#146, #147)
- code graph — JS/TS arrow-function and function-expression bindings are
extracted, PHP method and static calls are recorded, and Swift/Kotlin
struct/enum/interfacedeclarations get their correctSymbolKind. (#136) - knowledge / vector / domain — a corrupt knowledge-bearing body surfaces an
ingest error (rather than silently not indexing) and removed records are
de-indexed; non-finite vectors score
0.0and rank deterministically; the domain codec rejects aBody::Blobinstead of misparsing its content hash. (#139, #149, #154) - cli —
get/ticket getexit non-zero (message on stderr) when a record is absent,indexadvances the persistent SQLite graph only after the manifest commits, and--gcis honored under--watch. (#152) - ticket connectors — Jira routes a
Canceledmove to a won't-do status rather than Done; a closed GitLab issue is terminal and non-terminal moves no longer report a false success; Linear fails a mutation that returnssuccess: false; the GitHub REST connector followsLinkpagination so all issues import, not just the first 100. (#138, #140, #141, #142)
Testing: de-flaked gonzalo-parse's hung-worker timeout test, whose 300 ms
budget false-timed-out the healthy recovery parse under heavy build load. (#178)
0.2.0 - 2026-07-06
The code-graph release. Gonzalo grows a full code-graph capability —
tree-sitter parsing across nine languages, content-addressed slices with
two-level keying, a persistent SQLite GraphStore, and structural queries over
both the daemon and an MCP server — alongside real semantic search (a local
Candle embedder feeding an approximate ANN index over per-kind-chunked
knowledge), namespace-scoped daemon auth, and correct content-aware 3-way merges
on both store-sync and git pull. Distributed as the gonzalod container image.
Added
- Code-graph capability (EPIC A–K): tree-sitter parsing into path-agnostic,
content-addressed slices with two-level keying (content+grammar hash for
storage, per-view manifest for resolution); a persistent
SqliteGraphStore; assembly-time name resolution; and structural queries — definitions, references, callers, callees, transitive impact, and cross-view diff — served over the daemon and a dedicated code-graph MCP server. Parsing is crash-isolated behind a worker-subprocessParserPool. (ADR 0012; #48, #50, #54, #56, #61, #64, #66, #70, #71, #74, #77, #88, #89, #90, #10, #30) - Language breadth for the code graph — a
Languagedispatch with grammars for Rust, Python, JavaScript/TypeScript/TSX, Go, Java, C#, C, and C++. (#79, #81, #83, #84, #85, #86) - CLI indexing (
gonzalo index): index a source tree into a code-graph view, git-diff-driven incremental re-sync, a--watchfile-watcher for live re-index, and opt-in mark-sweep GC of unreferenced slices. (#74, #93, #94, #100, #104) - Knowledge store (
gonzalo-knowledge): a "what do we know about X" surface composingStore+VectorIndex+Embedder, a vector⋈graph join (semantically similar and structurally near), and per-kind chunking so long records retrieve at turn/section granularity. (ADR 0011; #30, #29) - Real semantic vector search:
gonzalo-embed— a local CPU sentence embedder (Candle + all-MiniLM-L6-v2) — andHnswVectorIndex, an approximate ANN backend, behind the existingEmbedder/VectorIndextraits. (ADR 0013, ADR 0014; #97, #9) - Daemon substrate selection & health: env-driven
fs|s3store selection with a native S3BlobStore, and unauthenticated/healthz+/readyzprobes for k8s. (#62, #63) - Namespace-scoped daemon auth: a token→principal model with per-namespace read/write scoping enforced on both transports, plus unforgeable author stamping. (ADR 0015; #11)
gonzalodcontainer image — the release artifact, published on av*tag. (#51, #65)
Changed
- Store sync — true 3-way merge:
AncestryStoreretains each version's body by revision hash sosynccan merge divergent structured records against their real common ancestor instead of an empty base. (ADR 0016; #2) - Git pull — content-aware non-fast-forward merge: a diverged pull now
reconciles per-record through gonzalo's class-aware
merge()into a two-parent merge commit, surfacing unresolved records instead of erroring. (ADR 0017; #7) - S3 native conditional writes:
If-Match/If-None-Matchclose the optimistic-concurrency TOCTOU window in the S3 substrate. (#5)
Fixed
- CI: serialize GitHub Pages deploys with a concurrency group. (#107)
Internal
- ADRs 0010–0017 added (ticket capability layer, two-level code-graph keying, local embedder, ANN backend, namespace auth, stored-ancestry 3-way merge, content-aware non-FF pull).
- Docs: an mdBook guide publishing the ADR log, changelog, and a synthesized Guiding Principles & Invariants page; README status badges. (#103, #38)
0.1.0 - 2026-07-03
The initial development line — a generic, versioned, conflict-aware persistence layer for caliban, built milestone by milestone (M1–M6).
Added
- Record/Store core (M1):
gonzalo-core— one uniformRecordmodel and a genericStoretrait, with revisions, optimistic-concurrencyparenttracking,PutResult::Conflict, per-RecordKindmerge, and a feature-gated substrate conformance suite. No I/O in the core. (ADR 0002, ADR 0005, ADR 0006) - Filesystem substrate + domain + facade (M1):
gonzalo-store-fs(mirrors caliban's on-disk layout, the zero-dependency default),gonzalo-domain(typedMemoryTier/Topic/Session/Checkpointviews), and thegonzalofacade. (ADR 0004, ADR 0008, ADR 0009) - Git & S3 substrates + Sync (M2):
gonzalo-store-git(commit-per-write, fast-forward pull/push) andgonzalo-store-s3(S3-compatible object store), plus theSyncengine reusing the core conflict/merge machinery. (ADR 0004, ADR 0005) - Daemon + remote substrate (M3):
gonzalo-proto(one canonical schema),gonzalo-server(gonzalod) serving the store over both gRPC (tonic) and HTTP/JSON (axum) on one core service layer with optional bearer auth, andgonzalo-store-serveras the client substrate. (ADR 0007) - Vector layer (M4):
gonzalo-vector—Embedder+VectorIndextraits with a caller-delegating default embedder and an exact in-memory cosine index. (ADR 0008) - Code-graph layer (M5):
gonzalo-graph— a tree-sitter Rust symbol/ref index (build_rust) behind aGraphStoretrait. (ADR 0008) - Admin CLI (M6):
gonzalo-cli(gonzalo) —list,get,status,migrate,sync.
Internal
- Project: established
docs/adr/(MADR-lite) with the initial retrospective ADRs 0001–0009; added CI (fmt/clippy/build/test), a line-coverage gate, the Kanban label taxonomy, and board/triage automation.