← ResourcesBLOG

Your vault has one answer to every question, and it is always something.

A vault answers every question with something. Why nearest-neighbour search over your notes cannot say "I don't know", why contradictions never resolve, and what a memory engine has to do instead.

The absence this makes loud: a question nobody has written down.

Why an Obsidian vault behind an embedding index fails as an agent’s memory — not because of Obsidian, but because a document store cannot represent the thing it does not contain.

The pattern is now standard enough to have a shape. You keep a vault — Obsidian, or a docs/ folder, it makes no difference — and you point an agent at it. Notes go in. An embedding index goes over the top. The agent asks a question, the index returns the nearest few notes, and the agent reads them as though they were what it knows.

It is a good way to think and a reasonable way to start. It fails as a memory engine for a reason that has nothing to do with Obsidian, and everything to do with what a document store is able to say when it is asked a question. It has exactly one answer, and the answer is always something.

A vault cannot say “I don’t know”

Nearest-neighbour search returns the top k by similarity. That is its whole contract. Ask it about a subsystem nobody has ever written a note about and it does not return nothing — it returns the k least-unrelated notes it has, with scores that look exactly like the scores from a query it could answer well.

So two very different states arrive at the agent in identical clothing: we have recorded something about this and we have recorded nothing, here is the nearest thing anyway. The agent has no way to tell them apart, and neither do you, because the retrieval layer does not distinguish them either.

A vector search over a vault answers a question it has notes for and a question it has no notes for in the same shape: three results with plausible scores. The empty case is not reported as empty. a vault “how do we handle retries?” retry-policy.md · 0.81 backoff-notes.md · 0.74 infra-braindump.md · 0.71 a real answer the same vault “how do we handle tenancy?” retry-policy.md · 0.69 infra-braindump.md · 0.66 onboarding.md · 0.64 nobody has ever written this down
Same shape, same confidence, opposite epistemic status. Hatched = an absence being rendered as a result.

This is the failure that matters, because it is the one that compounds. An agent that receives three weakly-related notes does not conclude “there is no policy here”. It reads them, finds nothing contradicting its plan, and proceeds — and the silence gets recorded as agreement.

Everything in the vault has the same standing

A note you typed at 2am while annoyed and a finding that came out of a two-day incident are, to the index, the same object: text with an embedding. There is no field for how do we know this, no record of who agreed, and no way for retrieval to prefer the one that was paid for.

Worse, contradictions do not resolve. Write “we retry three times” in March and “retries are handled by the gateway now” in July, and both match the same query forever. A vault has a notion of similar. It has no notion of superseded, because nothing ever went back and marked the March note as wrong — you moved on, which is what people do.

It gets worse as it gets bigger

The pitch for a second brain is that it compounds. For retrieval, the opposite happens: every note added is another candidate to be confused with, and the nearest neighbours get nearer to each other rather than to the truth. The asset that was supposed to appreciate is the one thing in your stack that degrades on write.

And the important lessons were never written down

Ask what you actually want an agent to remember and it is rarely the tidy stuff. It is: this refactor looked safe and was not. That test passes for the wrong reason. The last three people who touched this file broke it the same way.

Those are learned by being wrong, and being wrong is precisely when nobody stops to write a note. The vault gets the lessons you had spare attention to record, which is a biased sample of the ones worth having — and it is silent about the bias, because a document store has no way to represent a thing it does not contain.

Zero and unknown are not the same number

Every one of these collapses into a single design error, and it is worth stating on its own because it survives long after you have stopped using Obsidian for this: a memory that cannot represent its own absences will report them as results.

What happenedWhat a vault returnsWhat it should say
Nobody wrote about thisThree loosely-related notesNot recorded
Two notes disagreeBoth, ranked by similarityOne superseded the other
A lesson nobody vettedSame as a vetted oneUnvetted — weigh accordingly
Never measured0NULL

That last row is the one people argue with, so: a rule that fired zero times and a rule whose firing was never observed are different facts, and rendering both as 0 eventually retires something for being unused when nobody ever checked. Zero is a result. Unknown is not one.

What a memory engine has to do instead

None of this argues for a database over a folder. It argues for three properties a folder cannot have, and they are worth naming independently of anyone’s product.

Derive it from what happened, not from what somebody remembered to write. The record of a session already exists — the transcript. Reading episodes rather than notes removes the attention bias entirely, and it is the only source that contains the moments nobody would have written up.

Make refusal cheaper than acceptance. A candidate lesson should meet its vetoes before it meets its evidence, and a veto should not be outvoted by enthusiasm. In practice most of the work is refusing to learn from noise: in one real corpus, 19 of 42 surviving messages were harness-injected — system reminders, hook output, compaction summaries — text that looks exactly like something a person said.

Count sources, not occurrences. Recurrence is the strongest signal that a lesson is real, and the easiest to fake by accident: one confused run repeating itself three times looks identical to three independent confirmations, unless you are counting distinct origins.

Three identical shards from one session count as one distinct source and are refused. Three shards from three sessions count as three and pass. one session, said three times “the cache is the problem” “the cache is the problem” “the cache is the problem” distinct_sources = 1 not evidence — refused three sessions, said once each session A · “the cache is the problem” session B · “the cache is the problem” session C · “the cache is the problem” distinct_sources = 3 corroboration — published
The counter counts origins. Repetition inside one run is one voice repeating itself, which is the thing recurrence was supposed to rule out.

The part nobody wants to hear

A memory that refuses is smaller than a vault, and it feels worse to use at first, because most of what you would have written down does not survive contact with its own vetoes. That is the trade. A hundred notes you cannot rank are not more useful than eleven you can act on — they are a search problem wearing a knowledge base’s clothes.

Keep the vault. It is a good place to think, and thinking is not the thing that broke. Just stop asking it a question it has no way to decline to answer.

Where this comes from

These are the mechanics of the learning loop in Graphban, an agent-native work tracker whose whole premise is that an absence should never read as a clean result. The loop reads harness transcripts rather than authored notes, evaluates vetoes before evidence, counts distinct sources, and renders NULL where it has not measured. The longer write-up is memory and the learning loop; the neighbouring argument, about why a folder cannot arbitrate between two agents, is Markdown always says yes.

What's new

latest 2026-11-10
run

The playbook pack, applied with gban update

A deployment publishes the playbook pack at GET /api/playbooks. gban update applies that pack to the checkout. The fleet supervisor still holds no authority of its own.

fleet

Harness capacity is a routing mode beside the grades

Cheap or frontier still comes from measured sign-offs, and a cell under five samples stays unmeasured. The Harness Capacity tab is the other mode: entitlements, a subscription hierarchy you set, and a forecast of spend. A routing verdict the router never recorded is null, with a note. It is not zero.

run

First run is a checklist, not an empty form

After the stack starts, first run is a checklist, a setup-status card that stays until the first job is proven, and a one-time tour. Finish later with no project and the shell keeps the setup chip. It does not drop you into an empty create form.

run

Specs is a library, and a canvas finding stays open

The Specs list is the PRD, its preamble, and the design-set documents, plus the folder they sync to. search_docs searches baselined PRD bodies; drafts and design rows stay out until the call opts in. A design set compiles a prompt, stores the canvas, and can file a finding with a canvas check. That finding stays open until the bound canvas for the latest compiled prompt passes it.

honesty

An unloaded rule pack is not off

Build → Rules draws each rule at a strength: enforced, checked, external, advisory, unmeasured, or not used. library_loaded: false means no pack is loaded. It does not mean every rule is off, and it does not mean nothing was adopted. Turning a rule on or off is a person's REST action. get_rule_toggles has no MCP write.

run

The tracker board has a Blocked column

The tracker defaults to a board. The columns are Backlog, Next, In progress, Review, Blocked, and Done. Blocked sits beside Review, so a card that cannot move is its own column.

graph

Dead code and test results join the Stack overlay

Two new code-health categories on every component. Dead code reads vulture, knip and ts-prune. Tests reads pytest, vitest, jest or any JUnit report, with passed, failed and skipped counts, and each failing test becomes a finding you can Defrag into a card. A run that executed zero tests is recorded as a finding, not a pass, and a suite that hasn't run at the current commit shows as stale or never run.

graph

The Stack view: an architecture map nobody has to draw

Build → Stack maps the running system from the topology the API reports: your deployables grouped by host, the infrastructure you run, and the external services you depend on, in three nested rings. A code-health overlay records scan findings per component and colours each box by its worst severity. A box that was never scanned reads as never scanned, not as clean.

graph

The code graph groups by area

Nodes now cluster by area and draw their kind as their shape, so the map reads at a glance before you open a single label.

fleet

Gemini and DeepSeek join the fleet

Two new named adapters, agy for Google's Antigravity CLI and dsh for DeepSeek, bring the fleet to seven harnesses alongside Claude Code, Cursor, Grok, Qwen Code and local models through gbagent. Each is version-checked before a child starts. Codex still connects over MCP but has no fleet adapter.

honesty

Hand-merged work can be closed without faking a sign-off

Work merged outside the fleet used to sit open forever, or get a sign-off it never had. A project owner can now close it as merged, and the record says that is what happened.

honesty

A check that did not run is not a pass

Sign-off now refuses evidence that denies an acceptance clause, counts only the reviewer's own tests, and treats a skipped check as missing rather than green. A refusal names the word it matched and the rule behind it.

memory

The decider sorts the memory queue

A project can opt in to a fast System One model that grades each memory candidate: grounded, ready, and whether it contradicts a published lesson. Thresholds are calibrated against human decisions, the uncertain middle stays with a person, and a decider that cannot answer says so on the page.

fleet

Review-only seats, and branches published at review

A review-only seat can review but never build, so a reviewer cannot quietly take a slice. When a card reaches review the supervisor pushes its branch and opens a draft PR, one card per PR. With --merge it enables auto-merge after sign-off; without it, a person merges.

run

Usage, Activity and OTLP log export

A deployment-wide Usage page with model usage, an Activity view rebuilt around lenses and facets, and log export over OTLP to the observability stack you already run.

run

The app, reorganised: Plan, Build, Observe

The redesign groups every surface into three sections: Plan (tracker, requests, PRDs, roadmap), Build (code graph, stack, MCP tools, fleet, outposts) and Observe (memory review, lessons, activity, live, harness). The same in self-host and hosted.

run

Feedback Kit: public boards, votes and a roadmap

Collect feedback through an ingest API or the embeddable widget, publish boards people can vote on, and show a public roadmap. Submissions land in triage and become cards.

fleet

Outposts and the harness mix

The Fleet view is rebuilt around a catalog of harnesses, a mix that sets each vendor's share of launches, and outposts: the macOS, Linux and Windows machines your fleets run on.

fleet

A capability matrix built from real sign-offs

Models are graded per function set from reviewed work and probes, learned across users. Ask for a tier and the resolver picks from what has been measured. A cell with under five samples says unmeasured instead of guessing.

honesty

Done cannot be minted at the door

Two ways around the completion gate existed: create an item already marked done, or release a claimed one into done. Both now refuse. Signing work off still requires an attestation — the status is not a field you get to set because the form accepted it.

honesty

Agents are told the git process, or told it is unmeasured

Nothing used to name a base branch, so agents opened PRs against main because that is what models do. The org — or the local project — now stores a sparse delivery contract: base branch, whether they may push to it, optional naming. get_context returns it. An unset field stays unmeasured, not a silent main. Graphban still does not open PRs, merge, or rebase.

fleet

until is a planner loop, not a thicker supervisor

gbfleet up is still one wave: spawn, wait, reap. until sits around it — keep going while there is work, spawn reviewers when review appears, and on restart adopt the children that are already running rather than launching a second set beside them. The supervisor still holds no authority of its own.

fleet

A wait for a human is a blocked item

When an agent needs a person, that wait is a blocked item on the same tracker rather than a side channel nobody will look at. The work stays visible, and the reason is on the record.

memory

A published lesson that nobody retrieved is a miss

Observe now has a Lessons catalog. Reach and outcomes are computed rather than assumed, and publishing something the next agent never found is recorded as a miss — not as a silent success that looks like the lesson landed.

run

Install without Docker, as a system service

Compose is still the default. A Mac or Linux box can now run Graphban as launchd or systemd, with one CLI for install, upgrade and uninstall. Postgres with pgvector is required and verified, never installed for you. There is no .app.

fleet

The caller picks the model a child runs

Every spawned child used to run whatever its vendor CLI defaulted to, with no way to say otherwise — the cost lever the whole fleet idea rests on, missing. The supervisor now passes a model through, along with the two knobs one vendor each exposes. Pass-through, not selection: the supervisor still does not choose models on your behalf, and a knob the chosen vendor does not have is refused rather than quietly ignored.

graph

The living graph answers back

Hubs, shortest path and two-hop reach reach the UI rather than staying in the API. Paths say which way each edge points, and the whole graph is one tab stop — arrows move real focus, so it is navigable without a mouse.

honesty

The reference docs cannot silently fall behind

Three documents presented themselves as complete inventories and were not: the API reference covered 87 of 160 routes, configuration 19 of 51 settings, the data model 15 of 48 tables. A ratchet now records the known gap and fails on any new one, in both directions — documenting something and leaving it listed as missing fails too.

fleet

The fleet supervisor

gbfleet runs one wave: a child per seat, spawned in its own worktree, waited on, reaped. It is a separate distribution that depends on httpx and nothing else, and it holds no authority of its own — it can only launch a process holding a seat the server issued. Delete it and every invariant still holds; the fleet just needs a human to open terminals again.

fleet

Vendor adapters, named and version-checked

claude, cursor-agent, grok and codex, each resolved by name rather than guessed at, with the supported version asserted before a child is spawned. The credential is written to the child's config file and never to argv, because argv is world-readable on a shared box.

fleet

Mint, list and retire became one capability

A planner can retire the seats it minted and only those, scoped by minted_by. Ending a wave expires seats, never credentials — no client config is touched and nothing has to reconnect.

proof

The supervisor walk, run against a real server

Sixteen steps passed, one is blocked on a decision rather than a budget, and it found nothing in the supervisor. The walk refuses to run unless pointed at a real instance and refuses any project it did not create itself — a walk that quietly passed by not running would be the worst possible version of it.

fix

A closed PRD no longer blanks the PRD page

The status map covered three states and the server sends four. One closed PRD threw during render and took the whole list and editor with it. An unrecognised status now costs one oddly-labelled chip instead of a white screen, and a test compares the two lists across the stack.

fix

Linking a request to an item works on a renamed project

Item ids are frozen at issue time while the key you see is rendered from the project's current tag. The link stored the key you sent instead of the id it resolved to, so on any renamed project it violated a foreign key and returned a 500.

Every entry is work that merged. The full history is on GitHub.