harness: tag episodic entries with active profile name - #5
Merged
codejunkie99 merged 1 commit intoApr 18, 2026
Merged
Conversation
The portable brain is designed to be mounted by any harness, and many
harnesses (Hermes Agent with `--profile`, others in the list that
support multi-identity) run several isolated agents against a single
brain. Without a profile field on each episodic entry, the dream cycle
clusters across all of them as one stream: a lesson graduated from a
quant-focused agent's trial-and-error can end up rendered as
"agent learned X" alongside a leisure-focused agent's unrelated
discovery. Per-agent retrospectives are impossible.
This adds a `profile` field to the episodic `source` block:
"source": {
"skill": "<what skill fired>",
"profile": "<harness profile name>", # new
"run_id": "<run identity>",
"commit_sha": "<agent git hash>"
}
Resolution order:
1. `AGENT_PROFILE` env var (canonical; any harness can export this).
2. `HERMES_HOME` basename under `/profiles/<name>` (Hermes-specific
fallback so existing Hermes installs Just Work without touching
adapter code).
3. "default" when neither applies.
No breaking changes: consumers that ignore `source["profile"]` see the
same behaviour as before. Cluster.py and promote.py can now partition
by profile when that sharpens the review-queue signal; existing code
that reads `source` as an opaque blob keeps working.
Tested four cases:
- default (no env): "default"
- AGENT_PROFILE set: value used verbatim
- HERMES_HOME under /profiles/fin: "fin"
- HERMES_HOME at base: "default"
mimed95
added a commit
to mimed95/hermes-agent
that referenced
this pull request
Apr 18, 2026
Adds a memory provider that wires Hermes's MemoryProvider hooks to an
agentic-stack (codejunkie99/agentic-stack) portable brain. Intended for
users who want to:
- Accumulate turns into a structured, tiered memory (working /
episodic / semantic / personal) that survives across sessions and
is reviewable by the operator.
- Share the same brain across multiple harnesses (Claude Code,
Cursor, Windsurf, Hermes, etc.); the brain lives outside Hermes so
memory portability is a first-class property.
- Get PREFERENCES.md and graduated LESSONS.md injected into every
system prompt without SOUL-level read-discipline.
- Have routing-partner specialists (Hermes multi-profile) log their
turns automatically, profile-tagged via `HERMES_HOME`.
Hooks implemented against the `MemoryProvider` ABC:
- `sync_turn` auto-logs each completed turn (importance
inferred by a regex heuristic, configurable
threshold).
- `on_delegation` logs parent-side subagent handoff outcomes.
- `on_session_end` writes a heuristic session rollup at higher
importance (LLM-backed rollup is a clean
follow-up).
- `on_pre_compress` tells the compressor to preserve memory-
curation content.
- `system_prompt_block` injects PREFERENCES.md, LESSONS.md, and the
review queue (when populated) per session.
Also surfaces a CAMOFOX_URL /health warning
when the optional stealth browser is down.
- `prefetch` tokenized, stopword-filtered ripgrep search
over the semantic tier; keyword-match not
vector (portable, no extra deps).
- 5 `brain_*` tools for in-session graduation / rejection / search /
review-queue / explicit log.
Design notes:
- No new pip dependencies beyond Hermes's base; the brain's own Python
harness is imported lazily at runtime from `brain_path`, so the
plugin is inert (but safe to install) when no brain is mounted.
- `brain_path` tilde expansion uses `pwd.getpwuid(os.getuid()).pw_dir`
rather than `os.path.expanduser`, because Hermes's terminal backend
overrides `$HOME` for subprocess isolation. A `~/.agent` config would
otherwise resolve to `<profile>/home/.agent` when a specialist profile
was shelled-out to from another. Absolute paths in config sidestep
the issue entirely.
- `is_available()` returns True only when `<brain>/harness` and
`<brain>/memory` both exist, so a misconfigured path leaves the
builtin memory provider untouched.
- Respects `agent_context`: writes are disabled for "cron" and "flush"
contexts to avoid corrupting user representations with non-
interactive traffic. System prompt injection still fires (cron jobs
benefit from PREFERENCES/LESSONS context).
- Ripgrep optional; pure-Python fallback runs when `rg` is not on PATH.
Tests cover: is_available true/false, system_prompt_block behaviour in
primary/cron contexts, and brain_* tool schema registration. Live
behaviour of sync_turn is better verified against a real brain than in
unit tests because of the dynamic harness import; it is exercised by
smoke tests in the plugin's README.
Relates-to: codejunkie99/agentic-stack#5 (profile-tagged provenance in
the brain, required for the `profile` field the plugin relies on).
codejunkie99
pushed a commit
to hovhannest/agentic-stack
that referenced
this pull request
Apr 23, 2026
Cross-model review (Claude + Codex adversarial) flagged 4 issues that would either lose user data on Windows or silently degrade episodic memory quality. 1. install.ps1:157-159 — DATA LOSS on Windows re-install. `Remove-Item -LiteralPath $skillsDst -Recurse -Force` on PowerShell 5.1 (default Windows shell) traverses INTO a symlink target and deletes its contents before removing the link. A second `install.ps1 pi` run would wipe .agent/skills/. Detect ReparsePoint via Get-Item.Attributes BEFORE Remove-Item; use .NET Directory.Delete($path, false) on links so only the link is removed, never the target. 2. adapters/pi/memory-hook.ts — no subprocess timeout, hangs Pi. `await runHook(payload)` is awaited inside Pi's tool_result handler; a stuck Python child blocks Pi's event loop forever. Add 3s default timeout (overridable via $AGENT_HOOK_TIMEOUT_MS), kill the child on timeout, surface `timeout` as a separate result kind. 3. adapters/pi/memory-hook.ts — stderr was dropped, failures undiagnosable. Switch stdio to capture stderr (bounded to 4KB to avoid memory blowup on a wedged hook) and surface the first line in the failure notification. 4. .agent/harness/hooks/pi_post_tool.py — Pi sends tool_input with camelCase keys (filePath, oldString, newString), but the shared cc.* helpers (action_label, reflection, importance) expect Claude Code's snake_case keys. Without normalization, every Edit/Write logged by Pi degraded to "edit: ?" / "Edited ?" with empty detail. Add a Pi→canonical input key map applied in _normalize_input(). 5. .agent/harness/hooks/pi_post_tool.py — fail-open on malformed payload was logging bogus "Unknown success" entries (Codex's High codejunkie99#1). If Pi ever changes the event shape or sends invalid JSON, episodic memory got polluted with noise instead of a real signal. Add a _emit_malformed() path that records an explicit `hook:malformed_payload` failure entry with a 200-char excerpt of the offending payload — visible in AGENT_LEARNINGS.jsonl as a real error, not noise. Smoke-tested: - empty payload → `hook:malformed_payload | empty payload` - malformed JSON → `hook:malformed_payload | json decode error: ...` - Pi camelCase Edit (filePath/oldString/newString) → produces correct `edit: /tmp/x.txt` action label and `Edited /tmp/x.txt: replaced 'a' with 'b'` reflection (was: `edit: ?` / `Edited ?`) - well-formed bash success → unchanged behavior Codex's concurrent-write concern (Codex codejunkie99#3) and the rsync-style sync for .pi/skills (Codex codejunkie99#5) are NOT addressed here — they apply to pre-existing infrastructure (post_execution.py write semantics, pi's existing symlink path) and are scoped as separate follow-ups.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The portable brain is designed to mount on multiple harnesses (the README explicitly lists Claude Code, Cursor, Windsurf, OpenCode, OpenClaw, Hermes, Pi, and standalone Python). Several of those harnesses support running multiple isolated agents against a single brain - notably Hermes Agent, where
hermes --profile <name>spawns a dedicated agent per identity. A single user might runhermes -p fin chat -q ...(personal finance specialist),hermes -p quant chat -q ...(math/theory specialist), andhermes -p bliss chat -q ...(travel concierge) in the same day, all writing into onememory/episodic/AGENT_LEARNINGS.jsonl.Today the episodic
sourceblock carriesskill,run_id, andcommit_sha, but nothing to tell the dream cycle which agent the entry came from. That's a real gap:auto_dream.py's clustering happily mixes a portfolio-sizing observation from afinagent with a restaurant pattern from ablissagent, and the resulting LESSONS.md graduation loses per-specialist granularity. Per-agent retrospectives (e.g. "show me everythingquanthas learned this month") can't be answered.What
Adds a
profilefield to the episodicsourceblock produced bybuild_source(skill)in.agent/harness/hooks/_provenance.py:Resolution order, cached per-process:
AGENT_PROFILEenv var - canonical. Any harness can export this; documented intent so new adapters have a single place to hook in.HERMES_HOMEenv var - if it points under<...>/profiles/<name>, use that as the profile. Handles existing Hermes installs without requiring their adapter to change."default"- safe fallback when neither applies (includes cron-invokedauto_dream.pyruns and single-agent harnesses).Not a breaking change
source["profile"]see the same behaviour as before (the field is additive).cluster_and_extractandwrite_candidatesstill operate on the episodic stream as-is; profile-aware clustering is an optional follow-up (opt-in insideauto_dream.py, not forced here).Tested
Four scenarios, all pass:
profile"default"AGENT_PROFILE=coder"coder"HERMES_HOME=/home/user/.hermes/profiles/fin"fin"HERMES_HOME=/home/user/.hermes"default"Motivation from actual use
Running this patch on my Hermes install, I now see per-profile clustering surface real signal:
finentries cluster around position-sizing regrets,scribeentries cluster around signal-source quality judgments,carmackentries cluster around refactor patterns. Pre-patch, these three streams collided in one dream-cycle bucket and the promoted lessons read like kitchen-sink generalities.Follow-ups (out of scope for this PR)
auto_dream.pycould grow a--per-profileflag that partitions the episodic stream before clustering. Low-risk; cleanly layerable.AGENT_PROFILEenv var deserves a mention in AGENTS.md under "Memory" once this lands.