* fix(memory): filter honcho search by category instead of stamping it on unfiltered results
HonchoMemoryManager.search sent an identical unfiltered query whether or
not a category was requested, then stamped the requested category on
every hit, so memory_search(category=...) returned unfiltered results
attested to be that category (e.g. a work fact labeled "preference")
— the opposite of the contract, where category filters before the
top_k slice and only facts with that exact category are returned.
Send the category as a real server-side metadata filter (Honcho applies
"metadata" filters in its ranked query, before the limit) and label
each result from the record's own metadata, falling back to "memory"
as before. HonchoClient.search gains a keyword-only filters argument
that is only put on the wire when set, so unscoped requests are
byte-identical to before.
* docs(memory): note honcho category filtering requires stored metadata
* fix(memory): reject a Honcho base_url that can never resolve
HonchoConfig only did str(...).rstrip('/') on backend_config.base_url, so a
scheme-less or non-http value was accepted at Gateway startup and then made
every memory read return empty and every write drop silently, with only a
per-operation WARNING. httpx turns 'localhost:8000' into the bogus scheme
'localhost:'. The same check already exists in the mem0 and OpenViking
backends, and honcho/README.md plus HonchoMemoryManager.from_config both
promise a bad URL fails fast at startup.
* fix(memory): cover the scheme-case and host-less variants of the guard
- drive the api_key-over-plain-HTTP check off urlsplit(...).scheme: urlsplit
lowercases the scheme while startswith("http://") does not, so
"HTTP://internal:8000" reached httpx over plain HTTP with the key set and
allow_insecure_http still False
- read hostname instead of netloc in the base_url guard, so an authority
without a host ("http://", "http://:8000") is rejected at config time like
the scheme-less shape this PR already removes
- test direct HonchoConfig(base_url=...) construction, the path the guard's
placement in __post_init__ exists to protect
* fix(memory): report non-mapping Honcho backend_config values as ValueError
failure_policy, workspace_overrides and user_peer_overrides were read with a
falsy-only `or {}` fallback, so a truthy non-mapping (a bare string, a YAML
list) reached .get/.items and escaped as a bare AttributeError from inside
backend construction. Route all three through one _mapping helper that keeps
falsy values meaning "unset" and names the offending key as a ValueError, the
posture the mem0 and OpenViking backends already take.
* fix(memory): name the Honcho numeric knobs that cannot be cast
timeout_seconds / connect_timeout_seconds / message_char_limit /
max_injection_chars still reached float() / int() with a YAML null or a
mapping, so the operator got a TypeError naming neither the key nor the
config file. Narrow all four through one _number helper that keeps falsy
values meaning "unset" the way the sibling api_key / storage_path scalars
already do, and reports a value that cannot be cast as a ValueError on the
key. Numeric strings keep parsing, since that is what float/int accept.
* fix(memory): report non-numeric backend_config knobs by name in mem0 and OpenViking
mem0 casts top_k, score_threshold, max_injection_chars and timeout_seconds, and
OpenViking casts timeout_seconds, max_seen_message_ids, retrieval.top_k and
retrieval.max_injection_chars, straight through int()/float(). A knob written
without a value in YAML therefore escapes as "TypeError: int() argument must be
a string..." from inside backend construction, naming neither which knob nor
which file is wrong, and a non-numeric value escapes as the equally anonymous
"could not convert string to float".
Both now resolve numeric knobs through a helper that keeps a value-less key at
its default, the way the same dicts already treat failure_policy and
allow_insecure_http, and turns an uncastable value into a ValueError that names
the knob. OpenViking's score_threshold keeps None as a meaningful value rather
than a default to fall back to.
* feat(memory): honcho backend config parsing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(memory): honcho v3 http client
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(memory): honcho memory manager (workspace-per-user, fail-closed identity, async offload)
- HonchoMemoryManager implements the MemoryManager contract (add/get_context/
search/get_memory/shutdown_flush + aadd/aget_context/asearch offloaded via
asyncio.to_thread), signatures verified against manager.py's tier-1/tier-2/
async abstracts.
- Workspace resolution: workspace_overrides[user_id] else
workspace_prefix + sanitize_id(user_id); missing/empty user_id fails closed
(no-op write, empty read) rather than falling back to a shared workspace.
User peer: user_peer_overrides[user_id] else sanitize_id(user_id).
- get_context self-truncates to max_injection_chars and raises
MemoryManagerError only under failure_policy.read=fail_closed; default is
log-and-return "".
- Restore backends/honcho/__init__.py to the noop direct-import convention
(MANAGER_CLASS = HonchoMemoryManager) now that honcho_manager.py exists,
replacing Task 10's temporary lazy __getattr__ scaffold.
- Fix Task 10 deferred docstring minor: sanitize_id docstring now states the
grammar allows up to 100 chars while this helper caps at 64.
- 19 new tests appended to test_honcho_memory_backend.py (write/read/async/
lifecycle/factory-discovery); 27/27 pass. Verified end-to-end that
manager.py's drop-in backend scanner resolves "honcho" with no core edits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(memory): collision-resistant identity derivation, exception containment, passive-writes flag
Task review findings (2 Critical + 1 Important), all fixed in the same worktree:
- CRITICAL (cross-user bleed): sanitize_id is lossy -- "user.name@example.com"
and "user-name@example.com" both sanitized to the same string, merging two
users' memory into one workspace/peer. Add _stable_id() (sanitize_id output
+ 8-hex-char SHA-256 suffix of the raw id) and use it on the default
(non-override) path in _workspace/_user_peer; workspace_overrides /
user_peer_overrides still match on the raw key, unchanged. The hash suffix
also guarantees a non-empty result for a raw id that sanitizes to "" (e.g.
"!!!"), so _user_peer can no longer return "". Documented in the manager's
isolation docstring.
- CRITICAL (exception containment): client.py's _post() called response.json()
outside the try block, so a 200 with a non-JSON body raised a bare
JSONDecodeError that would escape add() with no upstream handler. Wrap the
parse and raise HonchoRequestError (mirrors Mem0Client._request). Broadened
the manager's four boundary excepts from `except HonchoRequestError` to
`except Exception` (mirrors openviking_manager.py's broad-guard precedent),
with `except MemoryManagerError: raise` first so a contract error is never
swallowed or double-wrapped.
- IMPORTANT: added requires_passive_writes_in_tool_mode: ClassVar[bool] = True
-- Honcho's only write path is passive add() (no fact CRUD hooks), so tool
mode must keep MemoryMiddleware writes flowing to the deriver. Mirrors
mem0_manager.py's identical flag/rationale.
Minors addressed: get_memory(user_id=None) empty-shape-with-no-calls test;
empty-string user_id tests for add()/get_context(); dedicated collision test
proving two colliding raw ids resolve to different workspaces/peers.
10 new tests (37/37 total pass); RED verified by stashing only the
implementation files (tests import the not-yet-existing _stable_id, so the
whole module fails to collect) before restoring the fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(memory): blocking-io anchor for honcho backend; docs + config example
- Adds test_honcho_memory_backend.py in tests/blocking_io/ with fake-client blocking IO
- Mirrors openviking anchor structure and conftest conventions
- Updates backends/README.md with honcho row and config keys section
- Updates config.example.yaml with honcho commented block
- Updates backend/AGENTS.md with honcho memory backend bullet
- Documents workspace resolution (prefix + collision-resistant sanitized id)
- Documents tool mode passive write retention via MemoryMiddleware
- Documents async entrypoint offloading via asyncio.to_thread
- Documents fail_closed vs fail_open recall failure policy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(memory): wire close() to shutdown hook; correct honcho README defaults and tool-mode note
- HonchoMemoryManager.close() releases the HTTP client, mirroring
mem0_manager.py's pattern and the base MemoryManager.close() shutdown hook.
- README: fix workspace_prefix (deerflow-u-), message_char_limit (8000),
max_injection_chars (6000), and base_url (default http://localhost:8000,
not required) against backends/honcho/config.py; add missing
timeout_seconds/connect_timeout_seconds rows; replace the "middleware
mode only" claim with wording matching reality (tool mode supported,
search implemented, passive writes retained via MemoryMiddleware).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(memory): honor failure_policy.read on all honcho recall paths; review nits
Addresses PR #4730 review feedback:
- search() and get_memory() now route through a _read_or_fallback policy
gate (mem0's pattern), so failure_policy.read: fail_closed raises
MemoryManagerError on every recall path as documented; get_context()
uses the same helper, preventing future drift.
- Session ids use the collision-resistant _stable_id derivation; bare
sanitize_id would merge threads like "t.1"/"t-1" into one session.
- HonchoClient accepts a transport kwarg (Mem0Client precedent) so tests
inject httpx.MockTransport through the constructor.
- Config: empty/null workspace/peer override values fail fast at parse
time instead of silently falling through to the default derivation.
- _UTC_NOW_FIELDS 1-tuple replaced by a plain _UTC_NOW_FORMAT constant.
- README: user_peer_overrides row described the wrong target (it
overrides the user's own peer, not assistant_peer); document the
non-empty constraint on override values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(memory): qualify honcho isolation claim for shared workspace_overrides
The module docstring claimed users cannot see each other's memory by
construction, unconditionally. That holds for the default
one-workspace-per-user derivation, but a workspace_overrides entry mapping
several users to one workspace shares that workspace's search index:
search() uses Honcho's workspace-scoped /search (no peer filter), while
get_context()/get_memory() stay peer-scoped via working_representation.
State the asymmetry in the docstring, the README Workspace Resolution
section, and the workspace_overrides table row.
Docs-only; no behavior change (review follow-up on #4730).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>