Skip to content

v5.20.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 08:18
· 20 commits to main since this release
622ac48

A global memory layer, and the layered read that makes it reachable. Knowl has had four
namespaces since long before this release -- session, project, organization, global -- with
precedence, round-robin interleaving and per-row embedding identity all implemented. None of it
could be read: the layered reader ran only when vector search was off, and vector search is the
default, so a configured global or organization namespace was written to and never queried.

The reader now spans namespaces under vector search. Each one is searched with its own
embedding identity, resolved from its own config root -- the project's for session and
project, the Knowl home for the standalone stores -- because searchKnowledgeEmbeddings filters
on that fingerprint and scoring a 768-dimension query against 384-dimension rows is meaningless.
A namespace whose profile cannot be served is skipped and named, never silently dropped.

What that unlocks:

  • ~/.knowl/global.db, a machine-wide store for what is true of you rather than of a
    repository: preferences, machine quirks, conventions that hold everywhere. A file beside the
    machine home rather than a project at it, because knowl init at ~ would put a store on top
    of models/, cache/, repos.json and credentials.json.
  • knowl link global [--off], per project and reversible. Project answers still outrank
    global ones, so linking never changes what a repository says about itself.
  • knowl store --namespace global, where every --path must be absolute: a relative path
    names nothing in a store that spans repositories. Those paths are provenance for a reader and
    are not indexed -- impact detection, drift and evidence staleness stay project-only, and the
    write says so rather than looking wired up.
  • Sessions with no project at all -- knowl outside a repository, or a Hermes Desktop window
    with no folder open -- resolve to global alone, which is the difference between having memory
    there and having none. Only when there is genuinely no project: a repository whose config is
    malformed is an error, never quietly answered from someone's personal defaults.
  • knowl init --global runs anywhere, setting up the machine-wide store and any hosts
    named beside it without writing anything into the current directory — which is how a
    machine-wide host such as Hermes is wired in one command. Plain knowl init still
    initializes the directory you run it in.

Hermes Agent is driven by a plugin now, and it reaches Hermes Desktop. 5.19.0 wired Hermes up through the hooks.<event> shell commands its config.yaml accepts. Those are terminal-only: the serve backend Hermes Desktop launches takes a fast path that never calls register_from_config, so not one of them is registered there (upstream hermes-agent#69825), and hermes hooks doctor reports them healthy regardless because it reads the config file rather than the live registry. Python plugins load from agent/agent_init.py, which every path builds an agent through. So knowl init hermes now installs integrations/hermes/knowl/ into the Hermes plugins directory, enables it in config.yaml beside the MCP entry, and removes the shell hooks the previous version wrote — registering both would send every event twice. The plugin sends exactly what a shell hook would have sent, and adds three things a subprocess cannot: the project resolves from Hermes' per-session working directory instead of the backend's, the memory rules ride in the system prompt, and a file write gets a same-turn impact card appended to its result. Restart Hermes after knowl init hermes to load it.

The plugin also registers knowl_query and knowl_store as Hermes tools of its own, and that is not duplication of the MCP server — it is the only correct channel on Desktop. knowl serve resolves the project from its own process directory, and Hermes Desktop runs one server for every project from a directory that is not any of them, so its mcp__knowl__* tools report No Knowl project found while the store is healthy. Pinning mcp_servers.knowl.cwd would fix one repository and silently answer from it in all the others, so init does not set it. The plugin's two tools run in the session's own directory instead, which is right however many projects are open; everything they do not cover is a knowl <command> away in the agent's terminal.

The Hermes bootstrap card was being thrown away, on every host path. The profile registered on_session_start, so the engine bound the session and spent the bootstrap card on an event whose return value Hermes discards — and the first real turn then arrived on a session the engine had already seen, with nothing to say. Measured: a fresh session whose first event is pre_llm_call gets a 3,030-character card; the same session preceded by on_session_start gets an empty answer on both. That event is no longer registered, so the first turn binds the session and carries the card.

Knowl can be Hermes' memory provider, not only its plugin. Hermes has a first-class slot for
a memory backend -- Settings > Memory & Context > Memory Provider, beside Mem0, Honcho and the
rest -- and it scans $HERMES_HOME/plugins/ for candidates, which is exactly where
knowl init hermes already installs. Knowl now appears in that dropdown with no extra step and no
second install path; selecting it is optional and additive.

The two surfaces are complementary rather than alternatives, because neither reaches what the
other does. A MemoryProvider gets no tool-level event at all, so the write gate and the
same-turn impact card can only be hooks. The hooks have no compaction event and can only append
to the user message, so three things can only be the provider:

  • Recall in the system prompt, where instructions belong, instead of appended to the message.
  • Hermes' recall indicator, which reports what was injected without depending on the model to
    mention it.
  • A checkpoint before compaction. Hermes fires no hook before it compresses a conversation,
    so without this a session's knowledge is summarised away before capture ever sees it. The
    engine normalizes it to the same checkpoint event PreCompact maps to elsewhere.

One directory serves both, which needs two things to stay true. Hermes imports it twice -- the
plugin manager for the hooks, plugins/memory for the provider -- and the collector it passes
the second time forwards register_hook to a real plugin context, so the module name is what
decides which half registers; registering both would fire every event twice. And plugin.yaml
must keep its explicit kind: standalone: without it Hermes sniffs MemoryProvider out of the
source, reclassifies the plugin as exclusive, and skips it entirely -- silently taking every
hook with it. Both are covered by tests.

When Knowl is the selected provider the pre_llm_call hook stops injecting its card, so recall is
never delivered twice. It still fires, because that is what binds the session and carries capture.

The Hermes plugin's tests now run in CI. They are Python, so npm test never reached them and
neither did anything else -- 31 tests sat in the repo running nowhere, including the ones
asserting the hook payload shapes Desktop depends on. They run on the ubuntu lint job's
preinstalled python via npm run test:plugin, stdlib unittest only, no pip install.

In the same job, audit:prod now retries when the registry is unreachable. npm audit exits 1
both when it finds vulnerabilities and when it cannot reach the audit endpoint at all, and on
release day npm returned 503 Service Unavailable on four runs while flapping -- one succeeded
in between -- burning seven minutes each time on npm's own internal retries. Only the second case
is retried: a real finding still fails on the first attempt, and an outage that never clears still
fails the build. The gate is not weakened, it just stops going red when the service is flaky.

Antigravity recorded nothing, four independent times over, and knowl fleet had never listed
one of its sessions.
The payload is protojson: every key camelCase, the session
conversationId, the root workspacePaths, the tool one toolCall: {name, args} object. The
stdin allowlist carried none of those names, so every event reached the normalizer empty and threw
IncompleteHostHookPayloadError -- which the hook entry swallows in silence. No row, no log, no
symptom, identical to a host nobody had configured. Three of the five registered events were also
written in a shape Antigravity parses and ignores: only PreToolUse and PostToolUse take the
{matcher, hooks} wrapper, and PreInvocation -- the one event that starts a session there -- is
a bare handler list. The tool names were wrong too: the real writes are replace_file_content,
multi_replace_file_content and write_to_file, the read is view_file, and the shell is
run_command. All of it is now read off the installed bundle and five real transcripts rather
than quoted from documentation, and docs/hosts.md says so. Payload remapping is one new profile
member, normalizePayload, applied once before anything reads a field.

Codex lost two thirds of its shell commands, found while verifying the above. isShellEvent
delegated to the shared helper, which knows bash and shell -- but across this machine's codex
sessions the tool is called shell_command 14,329 times against shell 2,059.

knowl cloud push can drain a queue again. Two independent faults could each leave staged
knowledge unsendable indefinitely.

--yes passed the strict snapshot check, which refuses when the queue merely GREW since the
snapshot was taken. That check exists to protect what a human read at the prompt, and --yes
shows no prompt — so with auto-staging on, any agent writing beside the push (including the
session running it) restaged continuously and every push --yes died with "the queue changed
while you were deciding". Strictness now follows the prompt: an addition refuses only when the
list was actually shown. An atom that CHANGED still refuses either way.

A staged id whose atom has since been deleted is now named. The ledger keeps such a row on
purpose so the push can report it, but nothing ever did: status counted the row, push skipped
it and reported "Published 0 new", and the queue never reached zero with no id to act on. The
push now lists the ids and points at knowl cloud unstage <id>, and sends the rest of the queue
as normal rather than refusing over it.