1. Executive Summary
Hillock is a local, single-user memory prototype: 1,827 lines of Python across eight files, licensed AGPL-3.0 with a contributor licence agreement its README says exists to preserve the option of commercial dual-licensing later. It stores facts as triples in SQLite, tracks Hebbian co-activation weights between entities, and holds a 10,000-dimensional hypervector space for matching.
Two things make it interesting to this atlas, and neither is the hyperdimensional computing.
The refusal is control flow rather than instruction.
In main.py, the local model is only invoked after
the symbolic layer has matched at least one stored fact above a
similarity threshold, and it is handed those facts to render. When
nothing matches, the function returns a fixed string — "I do not
have verified information about that" — and no model is called at
all. Almost every system in this corpus that promises not to hallucinate
does it by asking a model to say "I don't know"; this one makes the
question unaskable. That is a structurally different guarantee, and it
is twelve lines of if.
It publishes a seven-row version table including the rows where its own numbers fell. Extraction precision 15.5%, extraction recall 50.0%, retrieval accuracy 45.0%, gate accuracy 43.3% — on the README, above the install instructions, beside every previous version's figures and a paragraph warning that the benchmark is one 32-sentence text and "not enough to claim statistical robustness yet". Publishing the regressions is the creditable part and almost nothing in this corpus does it: gate accuracy peaked at 60.0% two versions earlier and retrieval accuracy at 55.0% one version earlier, and both rows are still there. The prose beneath the table is where the candour stops — it names only the two figures that rose, and the 🎉 marking "Retrieval Accuracy 45.0%" sits on a number ten points below the row above it. The harness that computes them is committed, and so is the whole benchmark: twenty answerable questions with expected subject-predicate-object and ten hard negatives each annotated with the reason it is unanswerable. What is not committed is any run output.
The fading-context reservoir leaves its zero state because
the token is added to it directly. step computes
state = decay*state + token_hv + roll(state)*token_hv, and
the middle term is what makes the recurrence go anywhere: without it
every term is proportional to the state, and a zero-initialised vector
maps to zero forever. Reproducing both forms in separate code, the old
rule holds a norm of 0.0 across five steps and the current
one climbs 8.0 → 34.4. The added term also does more than
revive the state: get_context_fingerprint scores the state
against unbound codebook entity vectors, so a purely
permutation-bound state would have had no component to align with
them.
Against that, two findings that reading the code produces and the README does not.
The gate's operating point is a function of how long the
question is, and the threshold has been raised past the
benchmark. Facts are bundled from exactly three components — a
deliberate normalization the README explains — while the query is
bundled from all its tokens, and both are compared against a fixed
HDC_THRESHOLD of 0.72, commented
"Recalibrated gating threshold to eliminate hallucination
leaks". Reimplementing the encoder and the bundling in separate
code, all four of the benchmark's own answerable sample questions score
between 0.367 and 0.450 against the exact
triple they ask about, and none of them clears the gate. The README's
architecture diagram prints
Passed Threshold >= 0.42.
And the published gate accuracy does not measure
gating.
gate_acc = (correct_blocks + correct_answers) / len(questions)
pools blocking on the ten negatives with answering on the twenty
positives. Read against the other published number, the pooled figure
implies the gate blocked four of ten hard negatives.
2. Mental Model
A memory here is a triple —
Marie_Curie → born_in → Poland — and nothing else is
durable except the strength of association between two entities.
A fact becomes a memory when a local model extracts it, either from
an ingested document or from a sentence the user typed that parses as a
declaration. There is no admission gate, no confidence, and no review:
update_relation is called and the triple is in the store. A
fact stops being a memory when a later assertion overwrites it — which
happens only for five named functional predicates, born_in,
died_in, capital_of,
place_of_birth and place_of_death — or when
/reset drops all three tables. Every other relation
accumulates. There is no per-fact deletion, no status, no timestamp, and
no record that anything was ever removed.
So the epistemic vocabulary is thin by construction: everything stored is treated as true, and the entire trust decision has been moved from the store to the read path. That is a coherent position for a prototype and it puts unusual weight on the gate.
The second durable structure is associative rather than
propositional. HebbianPlasticityEngine.update_associations
takes the set of entities active on a turn and, for each pair, moves
their weight toward 1.0:
new_w = current_w + self.eta * (1.0 - current_w) # eta = 0.15
then multiplies every pair not co-active this turn by
1 - decay (0.01). Reinforcement is asymptotic and cannot
exceed one; decay is per-turn rather than per-unit-time, so an
association fades by conversational distance rather than by the calendar
— which is consistent, since no row in this schema carries a time at
all. These weights survive the session in SQLite. They are used to prime
the renderer with related concepts, never to select which facts answer a
question.
The third structure does not survive anything. The codebook is an
in-process dictionary, allocated lazily on first sight of a token, and
the reservoir state is a NumPy array in memory. Both are rebuilt from
scratch on every launch, and rebuilding them is free:
get_or_allocate_hypervector routes every string through
resolve_predicate_hypervector, which sums the character 3-,
4- and 5-grams of the padded string, each hashed with MD5 into a seeded
±1 draw. A given entity therefore resolves to the same
hypervector in every run, and two strings sharing character n-grams
resolve to correlated ones. Measuring the encoder in separate code, ten
unrelated entity names average a pairwise cosine of +0.003,
while born against born_in is
+0.32 and discover against
discovered is +0.55. That morphological signal
does real work — it is how a query token reaches a fact predicate
without a synonym table. The vector layer holds no memory in the sense
this atlas uses the word: nothing is stored, and every vector is a pure
function of its string.
Diagram source
flowchart TD
Q["user turn"] --> L["link_entities:<br/>string match to entity ids"]
L --> F["SQL: every triple where the entity<br/>is subject or object"]
F --> M["bundle query tokens · bundle each fact<br/>as exactly 3 components · cosine"]
M --> G{"cosine ≥ 0.72?"}
G -->|no| R["return 'I do not have verified<br/>information about that' —<br/>the model is never called"]
G -->|yes| H["Hebbian: strengthen every<br/>co-active pair, decay the rest"]
H --> P["render prompt = matched facts<br/>+ primed associations"]
P --> O["Ollama renders the answer"]
S["reservoir state"] -. "decay·state + token + roll(state)·token" .-> S
S -.->|"top-1 codebook match"| PR["pronoun resolution<br/>when no entity linked"]3. Architecture
A console program. python main.py opens a REPL against a
local Ollama endpoint; there is no server, no API and no importable
library boundary.
main.py(20 KB) — the loop, entity linking, the HDC matcher, the gate, extraction prompts, and the three verbosity modes.database.py— three-table SQLite schema and all SQL.plasticity.py— the Hebbian engine, also over the same SQLite file.reservoir.py(236 lines) — the subword encoder, a sign-random-projection SimHash for GloVe vectors, and the fading-context reservoir.ingestor.py— threaded chunking of.txtand.pdfinto blocks of five sentences with two overlapping.talon_engine.py(20 KB) — the CUDA relation extractor, which the README presents as stages one to three of its architecture and lists in its file overview.evaluate_hillock_PROTO_ish.py(16 KB) — the benchmark.config.py— every tunable constant in one place, which at this size is the right call.
Deployment and ergonomics
The dependency list is numpy, psutil,
pypdf — three lines, unpinned — and the README's setup does
not agree with it: it instructs a CUDA PyTorch install from an external
index before pip install -r requirements.txt, and lists
"NVIDIA GPU with CUDA support (8GB VRAM recommended)" as a
prerequisite. torch is what that command installs;
transformers, glirel, spacy,
fastcoref and sentence-transformers — the five
packages talon_engine.py imports — are in neither the
command nor the file. So the documented install produces a machine with
CUDA PyTorch on it and the extraction pipeline the architecture diagram
is drawn around inert.
A first run downloads 822 MB from a third-party host without
asking. IntegratedHillock.__init__ calls
load_lightweight_glove(), which checks for
glove.6B.50d.txt, does not find it — the file is not in the
repository and nothing in the README mentions it — and calls
urllib.request.urlretrieve("https://nlp.stanford.edu/data/glove.6B.zip", "glove.6B.zip")
before extracting the 50-dimensional table. That is the whole of the
GLOVE_PATH story: a bare filename in the working directory,
a silent fetch on the first launch, and a ~10MB RAM
footprint claimed in the docstring for the trimmed dictionary rather
than the archive it arrives in.
The store is one SQLite file of three small tables, so it is
inspectable and repairable with any SQL client, and /reset
re-seeds ten entities and seven relations so a fresh database is never
empty. Nothing runs in the background; every write, decay step and query
happens inline on the turn.
talon_engine.py is where the install cost
changes, and it carries a hazard worth stating plainly. It
loads jackboyla/glirel-large-v0 through HuggingFace, and to
do so it disables a safety check:
transformers.utils.import_utils.check_torch_load_is_safe = lambda: None
transformers.modeling_utils.check_torch_load_is_safe = lambda: None
That check is what refuses to deserialize a
torch.load-format checkpoint, which is a pickle and
therefore an arbitrary-code-execution surface; the comment above it
calls it a "security block". Two further patches sit beside it and
neither disables a guard — one supplies missing tied-weight attributes
on an older fastcoref model class, the other defaults two
keyword arguments on GLiREL._from_pretrained for Hub
compatibility. The bypass is one patch applied to two module paths.
One thing bounds the risk: transformers and
glirel are absent from requirements.txt, so a
reader following the documented install gets
TalonEngine = None and an inert path — the import is
wrapped in try/except ImportError and the initializer in a
second try, so it fails closed twice — and the engine is
hardcoded to cuda:0. What that bound does not cover is
intent. The README's architecture diagram opens with the TALON engine,
its prerequisites list a CUDA GPU, and its setup instructs a PyTorch
install; a reader who does what the README says is aiming for the
configuration in which the guard is off. Nothing in the README tells
them so.
The v0.4 schema work is all on that path.
DEFAULT_PREDICATE_TAXONOMY (about fifty Wikidata
relations), ORIGIN_PREDICATES with its person-to-location
directionality rule, SYMMETRIC_PREDICATES,
get_canonical_triple_key — which maps
(A, collaborated_with, B) and
(B, collaborated_with, A) onto one key —
is_inverted_asymmetric_pair and
clean_entity_text are defined in
talon_engine.py and referenced in no other file. The
canonical key is the exact machinery a store would need to correct a
symmetric relation, and database.py never calls it.
4. Essential Implementation Paths
The gate —
main.py, select_answering_facts
The query is bundled into one hypervector by summing the vector of
every surviving token. Each candidate fact is bundled from
exactly three components: resolved subject, resolved
object, and the predicate's hypervector from
resolve_predicate_hypervector. Cosine is taken between the
two bundles and compared against HDC_THRESHOLD = 0.72.
The three-component rule is deliberate and the README explains it: holding every fact to the same number of components keeps a short fact from scoring higher than a long one for structural rather than semantic reasons. That reasoning is right, and it is more care than most threshold-based retrieval in this corpus receives.
The unhandled half is that the query side has no such rule.
Tokens are deduplicated by resolved identity and anything of two
characters or fewer is dropped unless it is a known entity, but what
survives is an unbounded bundle compared against a bundle of three.
Because the encoder is near-orthogonal on unrelated strings — measured
at +0.003 mean pairwise cosine — the bundle of n components
has a norm proportional to sqrt(n), so the score falls as
the question gets longer while the threshold stays fixed. Reimplementing
the encoder and the bundling in separate code, with the subject and
object shared between query and fact:
| Surviving query components | Cosine | Against the 0.72 in config.py |
Against the 0.42 the README prints |
|---|---|---|---|
| 2 | 0.822 | pass | pass |
| 3 | 0.663 | block | pass |
| 4 | 0.495 | block | pass |
| 5 | 0.385 | block | block |
| 8 | 0.217 | block | block |
Same fact, same overlap, opposite outcomes — and the window in which
a two-component match survives is two tokens wide. Sharing all three
components buys four: 1.000, 0.868,
0.691, 0.553 at n = 3, 4, 5, 6.
Run the benchmark's own questions through it and none of them clears the gate. Each scored against the exact triple it asks about:
| Benchmark question | Cosine | config.py 0.72 |
README 0.42 |
|---|---|---|---|
| "Where was Marie Curie born?" | 0.423 | block | pass |
| "Where was Alan Turing born?" | 0.429 | block | pass |
| "What did Alan Turing crack?" | 0.450 | block | pass |
| "Who did Turing work with?" | 0.367 | block | block |
The two threshold columns are both the project's own:
0.72 is what config.py sets and
0.42 is what the README's architecture diagram advertises,
so the documentation describes a gate that admits three of these four
questions and the code ships one that admits none. For a system whose
central claim is refusing to answer when it should, the threshold is the
most important number in the repository, and raising it "to
eliminate hallucination leaks" is the move that trades answers for
refusals without changing what the gate measures. The README's own table
records the trade in the two columns that depend on it: retrieval
accuracy fell from 55.0% to 45.0% and gate accuracy from 56.7% to 43.3%
while precision rose. Normalising the query side the way the fact side
is normalised — a fixed component budget, or dividing by the component
count — is the change the fixed threshold is waiting for, and it is the
change that would let the threshold be raised without paying for it in
answerable questions.
These figures are the subword-only regime, which is what runs when the GloVe fetch fails and what applies to any token outside GloVe's trimmed 50,000-word vocabulary. With the table loaded, a predicate or entity found in it is bundled with a SimHash projection as well, which moves the absolute numbers; the norm argument that produces the length dependence does not depend on which regime is active.
The refusal — main.py
Three return statements produce
DETERMINISTIC_GATED_FALLBACK, and what matters is where
they sit. The Ollama call is inside the branch that has already matched
facts; every path that fails to match returns the fixed string first.
The model is never given a question without evidence, so it has no
opportunity to answer one from parametric knowledge.
That is the strongest idea here and it generalizes past the prototype. A system prompt saying "only answer from the provided context" is a request; a control-flow structure in which the un-evidenced case never reaches the model is a property. The cost is equally structural: everything the model could legitimately have contributed — paraphrase, arithmetic, combining two facts — is also unreachable, and the system's ceiling is whatever its triple extractor managed to store.
Correction —
database.py, update_relation
SINGLE_VALUED_PREDICATES = {"born_in", "died_in", "capital_of", "place_of_birth", "place_of_death"}
...
if predicate in SINGLE_VALUED_PREDICATES:
cursor.execute("DELETE FROM relations WHERE source_id = ? AND predicate = ?", (src_key, predicate))
cursor.execute("INSERT OR REPLACE INTO relations VALUES (?, ?, ?)", (src_key, predicate, tgt_key))
Correction is destructive and unrecorded where it happens at all: the prior object is gone, with no supersession pointer, no tombstone, no event and no timestamp. Re-ingesting the same document re-derives the same fact, and nothing consults what a user previously replaced.
The allowlist is the whole correction policy, and it reaches
one of the predicates this system produces.
predicate_map in main.py normalises everything
the extractor emits into four canonical forms — born_in,
collaborated_with, discovered,
cracked — of which only born_in appears in the
set. capital_of exists only in the seed data;
died_in, place_of_birth and
place_of_death appear nowhere but this set, the
fifty-relation taxonomy in talon_engine.py, and that file's
ORIGIN_PREDICATES. Anything the model extracts under an
un-normalised predicate — the README's own example of extraction noise
is [Grace_Hopper] -[became_a_pioneer]-> […] — is
append-only by construction. talon_engine.py does define
get_canonical_triple_key, which folds a symmetric relation
and its inverse onto one key and is precisely what would let
collaborated_with be corrected; no code outside that file
calls it.
So a user who corrects "Marie Curie discovered Radioactivity" leaves
both triples in the store, and both are candidates at the next
retrieval. The arrangement protects multi-valued relations from data
loss and makes single-valued correctness depend on a hand-maintained set
of five strings meeting a predicate vocabulary that a language model
invents at ingest time. A functional column on the
predicate, or a supersession row instead of a DELETE, would
not have that coupling.
Writes —
main.py, conversational learning
A user sentence that is not a question goes to a two-pass extraction
prompt, and if it parses, update_relation is called
immediately: "I have recorded a new factual declaration." There
is no confirmation, no provenance, and no distinction in the store
between a fact extracted from an ingested PDF and one the user asserted
in passing. Everything the gate later protects rests on this being
right.
Ingestion — ingestor.py
Documents are split into blocks of five sentences with two
overlapping, dispatched to worker threads (MAX_WORKERS = 1
by default, with a comment noting the GTX 1070 it was tuned on), and
each block is sent to the model for triple extraction. The overlap is a
sensible cheap defence against a fact straddling a chunk boundary; the
deduplication that would otherwise imply is handled by the triple
primary key.
5. Memory Data Model
Three tables. entities(id, name, type),
relations(source_id, predicate, target_id) with the triple
as primary key and cascading foreign keys, and
hebbian_weights(entity_a, entity_b, weight) with the pair
as primary key.
What is absent is easier to list than what is present: no timestamp, no provenance, no confidence, no status, no source document, no scope key, no version, no soft-delete column. A stored fact carries exactly its three strings.
Two smaller observations. PRAGMA foreign_keys = ON is
set on the connections in _initialize_db and
update_relation, but SQLite applies that pragma per
connection, and plasticity.py opens its own connections
without it — so the cascade the schema declares does not apply on the
writer that touches entity pairs most often. It happens not to matter,
because nothing deletes an entity row outside the full reset. And
hebbian_weights rows are decayed but never pruned, so the
table grows monotonically with the number of entity pairs ever
co-active.
6. Retrieval Mechanics
Three stages, all on the turn. link_entities matches
query tokens against entity ids by string; a single SQL statement
fetches every triple where a linked entity is the subject or
the object, which is a one-hop neighbourhood rather than a traversal;
the HDC matcher scores and thresholds them.
query_relation carries a small fallback worth naming: if
the exact predicate misses, it stems both sides — stripping
ed, ing, s and collapsing
separators — and accepts a substring match in either direction. It is
crude and it is honest about being crude, and for a triple store whose
predicates come out of a language model with no controlled vocabulary,
some such tolerance is unavoidable.
The failure mode is the one the benchmark measures. Extraction
produces predicates like became_a_pioneer where the query
expects developed, and no amount of matcher tolerance
recovers a relation the extractor never formed. Retrieval quality here
is bounded above by extraction quality, and extraction recall is the
lowest of the four published numbers.
7. Write Mechanics
Every write is synchronous and inline. The user waits for extraction on a conversational assertion; ingestion of a document blocks the console. There is no queue, no background pass and no re-indexing, which for a store of a few thousand triples is a reasonable trade and one the README implicitly makes by targeting a single local user.
Deletion is /reset and nothing else. Conflict handling
does not exist as a concept: a contradicting assertion is an overwrite,
and two facts that disagree can only coexist if their predicates
differ.
Operational cost
- The write path is synchronous and dominated by a local model call per block or per assertion.
- The lag before a memory is retrievable is zero — the fact is in SQLite before the turn returns.
- No background pass rewrites the store, so nothing can silently undo a correction. In a corpus where most deletion claims expire at the next scheduled job, "there are no scheduled jobs" is a real property rather than a missing feature.
- The read path injects only matched triples plus primed associations, so the prompt grows with the number of matching facts and nothing bounds it explicitly.
8. Agent Integration
There is none, and the report should be plain about it. Hillock is a
console application with a hardcoded Ollama URL; there is no MCP server,
no HTTP API, no plugin contract and no packaging beyond
requirements.txt. /ingest, /mode
and /reset are the whole surface. Adapting it for another
agent means importing main.py's class and calling
execute_chat_turn, which is possible — the return tuple
carries the mode string — but nothing in the repository is arranged for
that.
9. Reliability, Safety, and Trust
Strengths:
- The un-evidenced case never reaches the model, which is a property rather than an instruction.
- The benchmark's hard negatives are committed with their reasoning, ten of them, each annotated with why the text does not support an answer.
- Facts are normalized to a fixed component count before comparison, so fact length does not distort ranking.
- Every hyperparameter is in one file, named, with the calibrated threshold marked as calibrated.
- The optional GPU path fails closed twice — on import and on initialization.
- No background job can undo a correction, because there are none.
- The published numbers are unflattering and prominent, with a written account of why they are low.
Gaps:
- The reservoir has no test. The recurrence that revived it is one term in one line, and nothing asserts the state changes when a token is fed to it.
- The gate's threshold is length-sensitive, so admission depends on phrasing in a way nothing in the repository states.
- The published gate accuracy pools blocking with answering, and the pooled number is dominated by the twenty answerable questions.
- Correction reaches one predicate the system actually produces, and is destructive and unrecorded where it does.
- Nothing stored carries provenance, time or trust, so a user assertion and a PDF extraction are indistinguishable afterwards.
- No scope of any kind, which is consistent with one local user and worth stating anyway.
talon_engine.pydisables a checkpoint-deserialization guard on the path the README's architecture diagram and prerequisites point at.- A stated capability rests on a component that does not run, which is the general form of the first gap and the one a reader should check for themselves before adopting anything here.
10. Tests, Evals, and Benchmarks
There is no test suite. No tests/
directory, no test file, no assertion outside the benchmark. For 1,827
lines that is defensible; it also means the inert reservoir had nothing
to fail, and that a threshold can be moved three times across four
releases with nothing asserting what it should admit.
I ran nothing from this repository. The screen found
no auto-executing surfaces and no dependency inside the cooldown, but
the benchmark requires a local Ollama with a pulled model and its
numbers are model-dependent by construction — and a first launch would
fetch 822 MB from nlp.stanford.edu. The reservoir, encoder
and gate findings above are all reproductions: the subword encoder and
the bundling arithmetic were reimplemented from
reservoir.py and main.py in a separate file
and run against a throwaway virtualenv holding nothing but NumPy.
evaluate_hillock_PROTO_ish.py is better than its
filename. It writes its own fixtures, wipes the database first so a run
cannot inherit state, ingests, queries, and scores four metrics. Twenty
questions carry an expected subject, predicate and object; ten are
negatives, each with an inline comment giving the reason: Newton is
1600s, Tesla is 1800s; Curie discovered radioactivity, Einstein
didn't; Enigma is target object, testing link-routing
direction. Those ten are committed cases asserting that particular
material must not be returned on a read path, which is the negative retrieval
assertion this atlas counts, and annotating each with its rationale
is rarer than the assertion itself.
Two things are wrong with the scoring, and they are worth separating from the low scores themselves.
gate_acc is not a gate metric. The
formula is
(correct_blocks + correct_answers) / len(questions) —
thirty questions, of which twenty are answerable — so a system that
answered every answerable question and blocked nothing would score 66.7%
on a metric the script labels "Hallucination defense rate". A
system that blocked everything would score 33.3%, which is lower than
the 43.3% reported and higher than four of the seven rows in the
README's version table. Pooling a positive obligation with a negative
invariant into one scalar is exactly the failure the AOEP-v0 protocol separates its
scorecard to avoid, and this is the clearest live instance of it in the
corpus.
The two published numbers together imply the gate's actual
performance. With
retrieval_acc = correct_answers / 20 = 45.0%,
correct_answers is 9; with
gate_acc = (correct_blocks + 9) / 30 = 43.3%,
correct_blocks is 4. So the hard-negative block rate behind
the headline "Gate Accuracy 43.3%" is four of ten, and
six of the ten baited queries produced what the script itself names a
HALLUCINATION_LEAK — after a threshold recalibration whose
stated purpose was to eliminate them. The reported figure is not wrong
arithmetic; it is a label that does not describe its formula, and the
label is what makes the recalibration look like it worked.
No run output is committed — no JSON, no log, no results file — so all twenty-eight numbers in the README's version table rest on the author's report of local runs. The harness and the full fixture set being committed puts this well ahead of the published-numbers-without-artifacts antipattern this atlas names elsewhere, and one committed output file would close the gap entirely.
No paper, arXiv reference or citation file exists in this repository.
11. For Your Own Build
Steal
- Make the refusal control flow. If the retrieval step returns nothing, return a fixed string and do not call the model. A prompt asking a model to decline is a request; a branch that never reaches the model is a guarantee, and it costs less code than the prompt does.
- Normalize what you compare before you threshold it. Holding every candidate to the same component count so length cannot inflate similarity is the right instinct — and then apply it to both sides, or the fixed threshold you calibrated moves under you.
- Commit the negatives with their reasons. Ten unanswerable questions, each with a comment saying why the source text does not support an answer, are worth more than a hundred recall cases, and the comments are what let a later reader check the test rather than trust it.
- Publish the number that embarrasses you, with the diagnosis beside it — and keep the row after it stops being your best. A seven-row version table that still shows the release where gate accuracy peaked at 60.0%, two releases before the current one, is worth more than any single figure in it. The discipline to finish is writing the prose under the table about the columns that fell, not only the ones that rose.
- Put every constant in one file and mark the ones you calibrated. At this size it turns "what would I tune" into a five-minute question.
- Decay by turn when you have no clock. If nothing in the schema carries a timestamp, decaying associations per interaction is coherent; adding a half-life in days would not be.
Avoid
- A recurrence with no additive term.
state = decay*state + f(state, token)is zero-preserving, and a zero-initialised state stays zero forever however long you run it. The assertion that catches it is one line — feed a token, check the norm moved — and it belongs beside any state you carry across turns. - A hand-maintained list of which predicates are functional. If the vocabulary is invented by a language model at ingest time and the list is five strings in a source file, the two will not meet. Put the property on the predicate, or supersede instead of deleting.
- A threshold calibrated against one input shape. If the score depends on a property of the query the threshold does not know about — length, token count, language — the gate is measuring phrasing as much as relevance.
- Raising the threshold as the fix for a leak. It is the one knob that always appears to work, because it moves both error rates in the direction the metric you are watching happens to reward. Without an assertion pinning what the gate must still admit, the version that stops the leaks is also the version that stops the answers, and only a metric that separates the two obligations will show it.
- A single scalar over positive and negative obligations. Blocking what must be blocked and answering what must be answered are different jobs with opposite degenerate solutions; a pooled score hides which one you failed.
- Keying a correction on
(subject, predicate)for every relation. It silently makes them all functional, and the first multi-valued predicate you meet loses data with no error — but narrowing it to an allowlist trades that for relations nobody can correct at all. Both failures are quiet. - Disabling a deserialization guard to load a model, especially in a module the README does not list. If the optional path needs it, say so where the person installing it will read it.
Fit
This is a prototype and says so, so the question is not whether to deploy it but what to take from it. The gate is the answer: it is the cleanest demonstration in this atlas that "the system will not answer without evidence" can be a structural property, and it fits in a file you can read in one sitting. Take that.
Do not take the storage layer. Three strings per fact with no time, no source and no status is below the floor for anything that has to be corrected later, and the destructive update makes the first multi-valued predicate a data-loss event. Anyone who wanted to build on this would be adding those columns before adding anything else — which is a compliment to how little else is in the way.
The hyperdimensional layer is the part to be most careful about, and
not because the idea is wrong. Gradient-free symbolic vector matching
over a codebook is a real technique with a real literature, and the
matcher here is a working instance of it — the subword encoder in
particular earns its place, giving born a
+0.32 cosine against born_in for the cost of
an MD5 per n-gram. What a reader should check is which half of any given
release runs: the architecture diagram is drawn around an extraction
engine whose five imports are not in requirements.txt, the
schema and directionality work is defined in that same unreachable file,
and the semantic half of the encoder depends on a table the first launch
downloads without asking.
12. Open Questions
- Does the revived reservoir resolve pronouns correctly in practice, or does the bundle term dominate the permutation-bound term enough that word order stops mattering?
- Is the five-predicate allowlist meant to grow with the extractor's vocabulary, and what maintains it?
HDC_THRESHOLDhas been0.42,0.78,0.68and0.72across four releases. What is it calibrated against, given that at0.72none of the benchmark's own answerable questions clears it?- What is the intended behaviour for a genuinely multi-valued
predicate — is
collaborated_withmeant to hold one target, or is theDELETEan oversight? - The README presents
talon_engine.pyas the architecture and instructs a CUDA PyTorch install, whilerequirements.txtnames none of the five packages that file imports. Is the omission deliberate, and does the author know the patch disables atorch.loadguard on the path the README points at? - Is the GloVe fetch — 822 MB from
nlp.stanford.eduon first launch, unmentioned in the setup instructions — intended, or a development convenience that shipped? - Do the version table's rows come from one run each, and would committing those runs' outputs be acceptable given they depend on a local model?
Appendix: File Index
- Gate, matcher, entity linking, console loop:
main.py(select_answering_facts,execute_chat_turn,link_entities,resolve_entity_identity). - Schema and all SQL, including the destructive correction:
database.py(update_relation,query_relation,get_all_facts_for_entities). - Hebbian reinforcement and decay:
plasticity.py(update_associations,get_associated_priming_context). - VSA context math:
reservoir.py(step,get_context_fingerprint). - Document chunking and the optional GPU extractor:
ingestor.py,talon_engine.py. - Benchmark, fixtures and metrics:
evaluate_hillock_PROTO_ish.py(generate_test_assets,run_evaluation). - Hyperparameters:
config.py.
History
2026-08-13 — 97678045…
— twenty commits on, v0.2.3 to v0.4.1, with every one of the eight
source files changed. Screened again before reading: 0 auto-run
surfaces, 0 build-time exec, nothing inside the cooldown, one unpinned
manifest — identical to the previous two screens. Nothing from the
repository was executed; the encoder and bundling arithmetic were
reimplemented in a separate file and run in a throwaway virtualenv
holding only NumPy.
Three published claims went stale, all in the same
direction. HDC_THRESHOLD moved
0.42 → 0.78 → 0.68 → 0.72, so the cosine table in section 4
was recomputed. The codebook is no longer random:
get_or_allocate_hypervector routes every string through
resolve_predicate_hypervector, so a hypervector is now a
deterministic function of its characters rather than a fresh random draw
per launch — which retires the hand-written predicate synonym table and
gives born a +0.32 cosine against
born_in. And the four README scores became a seven-row
version table whose current row reads 15.5% / 50.0% / 45.0% / 43.3%, so
the implied hard-negative block rate moved from three of ten to four of
ten.
One claim was imprecise when written rather than
overtaken. The report described a third monkey-patch as
overriding GLiREL._from_pretrained "similarly" to
the check_torch_load_is_safe bypass. At that pin and at
this one it defaults two keyword arguments for Hub compatibility, and
the patch beside it supplies missing tied-weight attributes to an older
fastcoref class. There is one security bypass, applied to
two module paths.
What the recalibration bought and what it cost. The
comment on the new threshold reads "Recalibrated gating threshold to
eliminate hallucination leaks". Reproducing the arithmetic, all
four of the benchmark's own answerable sample questions now score
between 0.367 and 0.450 against the exact
triple they ask about — every one below 0.72, where three
of the four cleared 0.42. The README's own table records
the trade: retrieval accuracy 55.0% → 45.0% and gate
accuracy 56.7% → 43.3% while extraction precision rose
11.5% → 15.5%. Six of ten hard negatives still leak.
The optional path became the documented one and stayed
uninstallable. The README's architecture diagram now opens with
the TALON engine, the prerequisites list a CUDA GPU, and the setup
instructs a PyTorch install from an external index — while
requirements.txt remains numpy,
psutil, pypdf and names none of the five
packages talon_engine.py imports. All of the v0.4 schema
work — the fifty-relation taxonomy, SYMMETRIC_PREDICATES,
get_canonical_triple_key, the directionality guard, the
span sanitizer — is defined in that file and called from no other, so
the canonical key that would let a symmetric relation be corrected sits
beside a database.py that never asks for it.
SINGLE_VALUED_PREDICATES and predicate_map are
both unchanged, so correction still reaches born_in
alone.
One new user-facing behaviour.
IntegratedHillock.__init__ calls
load_lightweight_glove(), which on a machine without
glove.6B.50d.txt — absent from the repository, unmentioned
in the README — fetches 822 MB from nlp.stanford.edu and
extracts it. The README's architecture diagram also still prints
Passed Threshold >= 0.42, and its self-deprecating
opening is gone.
2026-08-12 — a0499a55…
— re-read one day past the first pin, at v0.2.3, two commits later.
Screened again before reading: 0 auto-run surfaces, 0 build-time exec,
nothing inside the cooldown, one unpinned manifest; nothing was
installed and nothing from the repository was executed.
348f0834…
changes three things in twenty-five added lines.
reservoir.step gains + token_hv, so the
recurrence has an additive term and a zero-initialised state leaves zero
— reproducing both forms in separate code, the previous rule holds a
norm of 0.0 across five steps where the current one climbs
to 34.4. update_relation narrows its
DELETE to a set of five named functional predicates, so a
second collaborated_with no longer removes the first. And
select_answering_facts deduplicates query tokens by
resolved identity and drops tokens of two characters or fewer.
What the second fix trades. Removing the data loss
for multi-valued relations leaves correction reaching only
born_in: predicate_map normalises the
extractor's output into born_in,
collaborated_with, discovered and
cracked, and only the first is in the allowlist, while
died_in, place_of_birth and
place_of_death appear nowhere else in the tree but the
optional GLiREL label list. A corrected discovered fact now
leaves both triples in the store. The report's previous statement — that
the delete keyed on (subject, predicate) made every
relation functional — was accurate at that pin and describes a defect
the project has replaced with a narrower one.
What the third fix does not change. The report's
claim that the gate's operating point moves with question length holds
at this commit: the fact side is still exactly three components, the
query side is still unbounded, and HDC_THRESHOLD is still
0.42. The two-character filter removes nothing from three
of the benchmark's own four sample phrasings and one token from the
fourth, so the crossover between passing and blocking still sits between
six and eight surviving components.
The README, its four published metrics and config.py are
untouched by the commit, so the numbers on the front page describe the
extraction, matching and gating behaviour of the previous version.
2026-08-11 — 62f75e0c…
— first reading, on the master default branch, at the
fifty-sixth commit of a repository created 11 June 2026. Screened before
reading: 0 auto-run surfaces, 0 build-time exec surfaces, 0 dependency
surfaces inside the cooldown, 1 unpinned manifest (numpy,
psutil, pypdf, none pinned); nothing was
installed and nothing from the repository was executed. The reservoir
and gate-geometry findings were checked by reproducing the arithmetic in
separate code rather than by importing the modules.