Skip to content

feat: the client declares its author in the X-Author header and notes read by default in the author's own scope - #379

Merged
yw0nam merged 13 commits into
mainfrom
feat/author-header-server
Oct 19, 2026
Merged

yw0nam merged 13 commits into
mainfrom
feat/author-header-server

Conversation

@yw0nam

@yw0nam yw0nam commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

Several agents share one memory store, each with its own persona. Until now the model filled the author on every write and every client searched every note with no author shown, so one persona received another's notes as its own memory (live repro on 2026-10-19 in the perlica profile).

The author now comes from one request header, X-Author, set once in each client's configuration. The server requires it on every keyed route, attributes every write to it, and returns by default only the header author's notes plus notes saved with shared: true. The author filter of the read tools and routes is three-way: omitted, "all", or a slug. Note ids hash author and content. Consolidation runs per persona: groups are built among one author's notes, a merged note keeps the members' author, and the action rows carry the header author. The consolidator key and author are gone. The Hermes provider and the Claude Code plugin send the header from their author setting and show an author on a prefetch line only when it is another agent's. ADR-0015 records the decision.

Design: issue #378 (settled with the owner). Spec reviewed three times by gpt-6.1-sol; implementation reviewed on two axes by Opus, with every finding fixed.

Related issues

Closes #378. Supersedes #282 (closed).

Runtime evidence (required for ingest / retrieval / serving behavior)

Branch deploy verified on the operator's host: two REST clients with different X-Author values; a private note of one is absent from the other's default /search, a shared: true note appears in both, a request without the header or with all gets 400, a body author gets 400, and the groups route echoes the header author. Full table and raw output in the evidence comment.

Verification

  • uv run pytest passes: 1741 unit tests; the integration suite against a throwaway Postgres and the live embedder passes except tests/serve/test_turns_integration.py::test_concurrent_appends_to_one_session_get_distinct_seqs, which fails intermittently in full runs (Lock is bound to a different event loop, the module-level pool lock in core/db.py reused across asyncio.run loops) and passes alone every time; pre-existing and unrelated
  • uv run ruff format --check . and uv run ruff check . pass
  • docker compose build mcp passes (the branch deploy rebuilt api and mcp)
  • Runtime evidence attached above

Checklist (core principles)

  • Selective storage — notes stay distilled; verbatim text lives only in doc_rows and the turns lane
  • Source-specific logic stays out of the source-agnostic core (ingest pipeline, retrieval, serving)
  • No hardcoded endpoints/models — configuration lives in .env
  • New/changed behavior ships a test in this PR (tests/serve/test_author_scope_integration.py, tests/serve/test_note_scope.py, consolidation, Hermes and Claude Code hook tests)

Operator steps after merge (outside the PR)

  • Add X-Author next to X-API-Key in the mcp_servers headers of the natsume2 and perlica Hermes profiles; the provider's author setting is now required.
  • Update the Claude Code plugin (memory-base@memory_layer, now 0.2.0) so its hooks and .mcp.json send the header; remove the hand-patched hook copy.
  • Put MEMORY_AUTHOR=<slug> in the consolidation env file; retire the consolidator key; register a run for claude-code notes.

yw0nam added 13 commits October 19, 2026 18:21
The Hermes client takes its author and sends the X-Author header beside
X-API-Key; its prefetch renders a hit's author only when it differs from
the reader's own. The Claude Code context block takes the reader's
author for the same rule, the hooks' requests carry the header, and
.mcp.json forwards it from the plugin option.
The Hermes client takes its author and sends X-Author beside X-API-Key
on every request; a prefetch hit line names its author only when another
agent wrote it. The provider passes the configured author to the client.
The Claude Code plugin sends X-Author on every hook request and on the
MCP server connection, and its context block marks foreign authors the
same way.
… notes

ADR-0015 records the decision and ADR-0014's Status links the superseded
sentence. The consolidation procedure consolidates per persona: the env
file carries MEMORY_AUTHOR, groups take the author parameter, and the
write bodies carry no author. The configuration doc adds MEMORY_AUTHOR
beside MEMORY_API_KEY. CONTEXT.md defines Author and Shared. The REST
and tool references, the data flow, both integration READMEs, and the
connect-agent skill describe the header, the shared flag, the scoped
reads, and the foreign-author rendering.
An author-less profile would send an empty X-Author header and be
refused on every call; is_available now requires the author beside the
URL and the key.
Consolidation groups default to exactly the header author's notes and
params echo the resolved author; a slug search drops author-less
documents and cards; a mixed-author verdict is rejected per verdict;
duplicates take the three-way author; archive discovery replaces the
default scope with a slug or all; the author-less-note sentence moves
to ADR-0015.
@yw0nam

yw0nam commented Oct 19, 2026

Copy link
Copy Markdown
Owner Author

Runtime evidence — branch deploy on the operator's host (2026-10-19, HEAD 6fb5c1f)

Deploy: docker compose up -d --build api mcp from origin/feat/author-header-server; /health → {"status":"ok"}; api log shows 0 errors during the run. Two REST clients, one throwaway key (label probe-378, revoked afterwards), authors probe-a and probe-b, all in namespace default.

Check Result
POST /search without X-Author 400 missing or malformed X-Author header: an author slug is required
X-Author: all 400, same message
probe-a saves a private note with no body author 200, stored: true
probe-b saves a note with shared: true 200, stored: true
probe-b saves a private note 200, stored: true
a body author field 400 author comes from the X-Author header
probe-a default /search 2 hits: its own note and probe-b's shared note (shared: true); probe-b's private note absent
probe-a /search with author: "all" 3 hits, including probe-b's private note
probe-a GET /notes default the same 2 notes, each row carrying author and shared
probe-b GET /admin/consolidate/groups without author params.author = "probe-b"
cleanup POST /admin/notes/delete → {"deleted": 3}; GET /notes?author=all for the probe tag → []; probe key revoked

Raw output:

## 1 no X-Author -> 400
{"error":"missing or malformed X-Author header: an author slug is required"} 400
## 1b X-Author: all -> 400
{"error":"missing or malformed X-Author header: an author slug is required"} 400
## 2 probe-a saves a private note, no body author
200
{"id": "note:default:6986e2b59564179d", "kind": "work", "stored": true, "superseded": null, "similar": []}
## 3 probe-b saves a shared note
200
{"id": "note:default:c395af45fa4d381e", "kind": "work", "stored": true, "superseded": null, "similar": []}
## 4 probe-b saves a private note
200
{"id": "note:default:062bfe955a01a4c3", "kind": "work", "stored": true, "superseded": null, "similar": []}
## 5 body author is refused
400
{"error": "author comes from the X-Author header"}
## 6 probe-a default search: own + b's shared, never b's private
 - probe-a None Probe probe378-1792409173: author a keeps a private fact about the Sak
 - probe-b True Probe probe378-1792409173: author b shares that the office Sakura espr
## 7 probe-a search author=all: all three
 - probe-a None Probe probe378-1792409173: author a keeps a private fact about the Sak
 - probe-b None Probe probe378-1792409173: author b privately notes the Sakura espress
 - probe-b True Probe probe378-1792409173: author b shares that the office Sakura espr
## 8 probe-a GET /notes default
 - probe-b True note:default:c395af45fa4d381e
 - probe-a None note:default:6986e2b59564179d
## 9 probe-b groups default -> params.author
 params: probe-b
## 10 ids for cleanup
note:default:062bfe955a01a4c3 note:default:c395af45fa4d381e note:default:6986e2b59564179d

Deviation from the issue's last acceptance criterion: the two clients were REST calls with different X-Author headers on this host, not two Hermes profiles on ibricks43. The server-side behaviour is the same; the Hermes side (header from the author setting, foreign-author rendering, unavailable without an author) is covered by tests/integrations/test_hermes_provider.py and test_hermes_client.py. Configuring the two profiles is an operator step after merge.

@yw0nam
yw0nam marked this pull request as ready for review October 19, 2026 11:27
@yw0nam
yw0nam merged commit 5e46814 into main Oct 19, 2026
4 checks passed
@yw0nam
yw0nam deleted the feat/author-header-server branch October 19, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: the client declares its author once in the X-Author header; notes read by default in the author's own scope

1 participant