feat(*): add hermes as a cold-start import source - #264
Conversation
Store side read user_id/agent_id from the plugin config slice while the recall side read them from config.memory, so a user editing only one of the two silently split store and recall onto different owner_ids and every written memory became unrecallable. Identity is host policy, so it now travels through ServiceLocator as required fields. Also corrects the PluginContext docstring: config_schema is parsed but never validated and its defaults are never applied. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…g_schema Identity now comes from ServiceLocator. A leftover user_id/agent_id in the plugin config slice is ignored, and warned about only when it differs from the host value. start() rejects identities EverOS cannot accept, because they become directory segments on its write path and would otherwise fail late with a 422 buried in the server log. The manifest config_schema block is removed: it was never validated, its agent_id default disagreed with the code and contained a colon the write path rejects, and it declared only one of the three keys the factory actually reads. The surviving comment now states that rather than promising validation that is no longer planned. The default base URL is consolidated into _server.py, which owns the server lifecycle and is imported by backend.py rather than the reverse. It had a third copy as ensure_everos_server's default parameter, which onboard relies on by calling it with no argument, so a port change would previously have made onboard probe one port while the backend used another. config/update.py keeps its own copy on purpose: config must not import from a plugin. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
_validate_identity reported memory.user_id, which is the Python attribute rather than the userId key the user actually has in config.json, so the message pointed at a string they could not find. _warn_stale_identity_keys in the same file already used the camelCase key; the two now agree. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
include_profile defaults to false server-side, so the branch that flattens data.profiles into Memory rows was dead and every extracted user profile was unreachable. Profiles are a direct fetch: unranked, not counted against top_k, at most one row, so the flag is always on for user-track recall. Agent owners ignore it, so it is not sent for agent_id. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
_flatten_profile was written before anything exercised it. Once profiles
actually started coming back, its f"{k}: {v}" emitted 11414 characters of
Python repr into every turn's system prompt, because the two substantial
fields are lists of dicts.
Rendering now uses an allowlist: category or trait as the label, description
as the body, one bullet per item. Everything else in an item is dropped,
including evidence and basis, which narrate how a trait was inferred rather
than stating a fact about the user. Keys ending in _ms are machine fields and
are skipped. Scalars keep the old key: value form.
Measured against a live server the same payload renders as 5172 characters of
prose.
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The rendering rule states a precedence but nothing asserted it, so an item carrying both fields could have silently started using trait. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Second platform for cold-start import. Home resolution mirrors Hermes' own: HERMES_HOME when set and non-blank, else the platform default. A missing home means Hermes is not installed, which is not an error. Only the resolved home is scanned. Hermes also supports named profiles under <root>/profiles/<name>, each with its own memories/ and skills/, but importing every profile would need a cross-profile collision policy no current use case calls for. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Entries split on the exact newline-delimited section-sign sequence so an entry whose own text contains that character is not torn in half, a trap Hermes' own parser calls out. USER.md holds what the assistant learned about the user, so its entries are user turns. MEMORY.md holds the assistant's own notes written in its first person -- one real entry reads "the user corrected me about the model name" -- so its entries are assistant turns. That file therefore needs a leading user preamble, which is load-bearing rather than decorative: EverOS' user-track extraction skips a memcell containing no user sender outright, so a run of pure assistant turns would extract nothing. Preamble language samples the whole file rather than is_cjk's 200-char default, because these files are a short list of atomic facts where the first entry's language does not represent the rest. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Six host consumers read user_memory/profile/user.md directly -- the Curator, the Personalizer and four Sentinel producers -- and the Sentinel daily plan takes that file as its entire input, so content that only reaches EverOS is invisible to all of them. One entry becomes one H2 section because injection keeps the top two whole sections by lexical overlap plus Notes as a catchall, so a single large section would be all-or-nothing and would occupy one of only two slots. The model picks only the heading; entry text is written verbatim, and every failure path -- a raised exception, a finish_reason of error, or no provider at all -- lands the entry under Notes, which injection always includes. A misclassification therefore costs tokens, never visibility. Idempotency is keyed on the entry text rather than the heading, and an existing section is appended to rather than replaced. Keying it on the heading lost content: the seeded template already defines Preferences, Work Context, Topics of Interest and Basic Information, four of the seven headings the prompt offers, so on a fresh install -- the cold-start case this feature exists for -- an entry classified into any of them was skipped and landed nowhere. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The idempotency guard read "stripped and stripped in current", where the first clause only existed to stop an empty string matching everything. A blank entry therefore fell through to the classifier and created a section with an empty body. Blank entries carry nothing to import, so they are dropped outright. Also corrects the docstring: entry text is written unchanged apart from surrounding whitespace, and the return value is one heading per entry written rather than one per distinct section. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
build_scanners now returns the Hermes scanner alongside Claude Code, and the run path mirrors Hermes USER.md into the native user_memory/profile/user.md after the EverOS pass. The mirror is additive, so a failure in it is reported without reversing an import that already succeeded -- the comment claiming that property previously sat above code that did not have it. loguru is redirected to a file during run, so the warning also goes to the console where the user will see it. Provider construction is fault-tolerant: make_lazy_provider runs a credential check that raises when no LLM is configured, and cold-start import has to work in that case, so the mirror falls back to provider=None and lands everything under the Notes catchall. The delimiter rule now lives in one place: split_memory_entries is public on the scanner module and used by both the scanner and the CLI, instead of the CLI reaching across for a private constant. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
A real run flooded the terminal with LiteLLM DEBUG records and raw response JSON, defeating the point of redirecting loguru to a file so the run shows only the progress bar. Cause is ordering, not configuration. redirect_loguru_to_file already strips TTY StreamHandlers, but litellm was imported lazily on the first heading call, well after that, and reattached its own stderr handler. The records reached both the file sink via propagation and the terminal via that handler. The provider is now built eagerly so litellm is imported inside _make_hermes_provider, and the handlers are stripped again immediately after. A one-shot command gains nothing from deferring the import. Also drops a noqa for BLE001, which this project's ruff select never enables. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Skills cannot travel the memory pipeline: a skill is a directory of SKILL.md plus references, scripts, templates and assets, while an everos agent-skill row carries only text, so store() would drop every attachment. package_hash reproduces Hermes' own _dir_hash byte-for-byte, including the try/except wrapping the whole loop rather than each file. Faithfulness is the requirement, not robustness: Hermes compares its manifest md5 against a digest computed this exact way, so any deviation would classify every skill as user-modified. Cross-checked equal on all 82 skill directories of a real install and all 70 md5 values in skills/.bundled_manifest; the golden-digest test pins the format, since Hermes is not a dependency a test could import. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The suite could not tell abort-on-first-failure apart from skip-the-bad-file: the only blocked file was alphabetically last, so both algorithms produced the same digest. Nor was sorted() load-bearing -- this filesystem's raw rglob order happened to match sorted order for the old fixtures. Both are exactly the 'more robust' refactors that would silently break parity with Hermes and misclassify every skill as user-modified. The golden fixture now holds a top-level file that sorts after a nested one, so rglob order and sorted order differ, and a second golden pins the digest of a package with one unreadable attachment. Both constants were cross-checked against Hermes' own _dir_hash, and both mutations now fail. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The bundled manifest plus a whole-package MD5, then the hub lock, then the usage file. Hashing only SKILL.md would call a skill whose attachments the user edited pristine. A missing manifest proves nothing, so everything reads unknown and is therefore imported; if the hash ever diverges from upstream the failure direction is redundancy, never data loss. Manifest keys are tried frontmatter-name first and directory name second, the order Hermes writes them in. Two of the 82 skills on a real install carry a frontmatter name that differs from their directory name, so a single-key lookup would misread them, and the reverse order would compare one skill against a same-named directory belonging to another. Discovery mirrors Hermes' own exclusion set rather than skipping .hub alone. Without .archive we would resurrect precisely the skills the curator retired, and without the support-dir rule we would import the documentation copies of old skills that archive workflows preserve under a live skill's references/. Measured on a real install: 82 skills, 70 pristine and skipped, 12 imported. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Hermes' usage record field is created_by: agent, which reads like provenance but upstream consumes as a curator-management opt-in: hermes curator adopt stamps the same marker on a skill the user wrote by hand, and upstream's own _is_curator_managed_record docstring says to prefer is_curator_managed at call sites rather than propagate the misleading name. Keeping AGENT_CREATED would have told a user that skills they authored were written by an agent as soon as the import summary surfaced the label. It also missed the older agent_created flag that upstream still accepts. Also pins the one shape where the support-dir index guard changes the answer. Both guards were previously mutation-proof: no test failed when either was removed. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Only bundled_pristine is skipped: importing too much leaves unused entries in a pool that injects at most two skills per turn by relevance and that the user can delete wholesale, while importing too little is silent. The browsing category level is dropped and a single hermes/ level interposed, because the registry reads the first level below a layer root as the source label. Two situations that look alike are kept apart. A name claimed earlier in the same run is a flatten collision to disambiguate; a target that already exists on disk is the user's, and is never overwritten nor duplicated under another name. Depth is not fixed either -- a skill can sit at Hermes' skills root with no category to borrow -- so that case falls back to a numeric suffix rather than an invented category word in a directory the user browses. The summary keeps factory content in its own field: a run that installs 12 of 82 must not read as though it dropped 70. Verified on a real install: 12 of 82 skills landed, 224 KB, and the registry resolves all 12 under source hermes. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
copytree fails partway through, and by then SKILL.md is usually already in place. Two things followed from leaving that directory behind: the skill pool served an entry whose attachments were missing, and every later run read the leftover as already present and skipped it, so the one console warning from the first run was the only notice the user would ever get. That is the silent under-import the module's own docstring names as the failure to avoid. Removing the partial restores the retry, since a failed entry is not submitted, and restores the invariant that a directory in the pool is a whole skill. The failure branch had no test at all: deleting the entire try/except left the suite green. It now fails both when the cleanup is dropped and when the whole guard is removed. Also drops a test named for the removed root-prefix behaviour whose assertion was already true before the fix it claimed to guard. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
hermes prints only the first 100 rows of its export dry run and appends a count of the rest, so parsing that listing alone would have reported success while dropping every session past the hundredth. Above the cap the started_at axis is partitioned into windows and probed recursively until each window is small enough to be listed in full. Three constraints drive the shape and none are guessable from the output: --dry-run needs an output path and at least one filter; --after and --before filter started_at and silently truncate to the minute, so bounds must be minute-aligned or adjacent windows overlap and import the same session twice; and only ended sessions are ever candidates, which is why hermes sessions list can show more than the export dry run does. The partition floor is the epoch rather than a recent date because a session older than the floor lands in no window at all, and the empty halves left of real data cost one probe each. The window results are reconciled against the count the root probe reported, since a partition that misses a range returns a perfectly well-formed short list that nothing else would notice. Verified against the real CLI: 2 sessions, one probe, no windowing. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Images become a marker rather than travelling: ImportMessage.content is a str and the everos adapter reduces list content to its text parts anyway, so a multi-megabyte inlined image would be carried the whole way only to be dropped, after exceeding the importer's 30,000-character batch limit by orders of magnitude and being posted as a body that size. Hermes stores a text part two different ways. An input_text part keeps its text under content rather than text, so reading only the text field discards real user prose and, worse, counts it as an image. Both shapes are handled, only the known image types are counted, and a part of an unrecognised shape gives up its text instead of being labelled an image. Neither real export sampled here has list content at all -- the image-heavy session on that install had not ended, and only ended sessions are exportable -- so that path rests on the shapes hermes documents rather than on observed data. active and compacted need no filtering: hermes' exporter already excludes inactive rows, so rewound turns never reach us. Also pins three properties on the session enumerator that no test held: the listing cap boundary at exactly 100, the ceiling's buffer past the current minute, and the role filter itself, which real session_meta rows never exercised because their empty content dropped them first. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…roperties The reconciliation docstring claimed ended_at never goes back, so a shortfall had to mean a coverage bug rather than a race. That is wrong: reopen_session clears ended_at when a session is resumed, and it is called from six places, so a session genuinely leaves the candidate set mid-scan. The race does run both ways, as the original wording said. The error came from reading a field name instead of checking what writes it. The image docstring likewise kept a size claim nothing in this tree can support; it now cites only the batch limit, which is a constant here. Five properties were asserted nowhere, each verified by mutation: the ceiling buffer past the current minute (reachable only with a frozen clock, which is why the previous test passed with the buffer removed), tool_call_id staying absent on non-tool roles, tool_calls arriving as a tuple rather than the list hermes hands over, sender_id's split, and a zero timestamp being dropped as EverOS requires. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Sessions are enumerated through list_exportable_sessions, which pages around hermes' 100-row listing cap, and fetched one at a time by --session-id. Export goes to stdout rather than a temp file: a real session is one JSON object on one line with nothing human-readable mixed in, so there is no temp directory to create, no partial file to clean up, and one seam serves both the dry run and the export, since the output path is just another argument. scan_all no longer lets one platform take the others down with it. The hermes scanner has to shell out to enumerate conversations, and it raises when the binary is off PATH -- which, through a bare gather, meant a user who had once installed hermes could import nothing at all, not even their Claude Code history. Failures are now isolated per scanner and handed to an optional callback, because the logger is silenced during scan and file-only during run, so without it a failed platform would look exactly like a platform with no data. filter_by_tier needs no change: conversations carry SourceKind.CONVERSATION and the default tier already keeps memory files only. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
scan() probes for conversations, which shells out, so a test that forgot to supply a fake would spawn the developer's own hermes and pass or fail depending on what sessions it happened to hold. Safety was a convention -- remember the stub -- rather than a property. An autouse fixture now replaces the default runner with one that names the mistake, and the single test that wants the real runner takes the reference captured before the guard is installed. Verified by adding a stub-less test: it fails with the diagnostic instead of invoking the binary. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The _run helper used asyncio.get_event_loop().run_until_complete, which only finds a loop when some earlier test in the session created one. The file therefore passed in a full run and failed 22 of 22 on its own, and from Python 3.12 on get_event_loop no longer creates a loop at all, so it raises outright -- on a 3.14 interpreter these were 22 hard failures. asyncio.run owns the loop it uses, which makes each call self-contained. Unrelated to the hermes import work around it; separable if this branch should stay single-topic. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Both files hard-failed on a checkout without the extras installed, which made the result depend on the developer's environment rather than on the code. test_sandbox_debug_server claimed in its own docstring to run without boxlite, and it nearly does: every reference is a patch of boxlite.Boxlite, so the real package is never called into. But mock.patch has to import its target and the server probes availability with a bare import, so 22 tests failed instead of exercising anything. A scoped stand-in carrying the two names raven actually reaches, Boxlite and Options, makes the docstring true; skipping instead would have cost 22 tests on the platform where they are cheapest to run. The two tests that assert the not-installed path patch builtins.__import__ themselves. test_channels_errors needs the real slack_sdk in exactly one of its six tests, because the classifier isinstance-checks SlackApiError, so that case now uses importorskip the way test_channels_slack already does. A module-level skip would have taken the five unrelated cases with it. Verified both ways with a meta-path blocker standing in for a fresh worktree: extras absent goes from 47 failed to 4769 passed and 4 skipped, extras present stays at 4804 passed, and each file passes on its own. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
A text part carrying its prose under "text" was thrown away. run_agent reads that key for input_text exactly as it does for text, and the canonical flattener tries "content" only second, so the mapping this branch shipped had it backwards and a message whose only part was such a text part was dropped whole. Both keys are now accepted. The non-text set also missed image, audio and input_audio, so an Anthropic-native image message flattened to nothing and disappeared rather than leaving a marker; it now mirrors upstream's set exactly, and the marker says media rather than image because audio reaches it too. Session rows are read structurally instead of by guessing the id's shape. Cron ids are cron_<job>_<stamp> and ACP ids are bare uuids, so the old pattern silently discarded every session either subsystem ever created. The header count is now reconciled on the short path as well, which is the path nearly every install takes and where an unrecognised row was previously invisible, and more rows than the header counted is an error rather than an invented session. A missing hermes binary no longer costs the memory files, which are read off disk and never needed the CLI. The reason still reaches the user: a scanner can report a deliberate partial result and scan_all forwards it down the same path a total failure takes, since the logger is silenced during scan and file-only during run. Onboarding was the one scan_all caller left without that callback, so this branch had turned its loud failure into No importable data found. Also pins the identity plumbing: swapping user_id and agent_id at the only production wiring point left all 4804 tests green, which for a refactor whose whole purpose is preventing that mismatch is the assertion that mattered most. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
resolve_hermes_home read the platform-default root while hermes itself points HERMES_HOME at the sticky active profile when one is set. Since HERMES_HOME is not in raven's environment, a user on a named profile would have had their default profile imported instead of the data they actually use -- upstream warns loudly about exactly that read rather than treating it as an equal fallback. The Windows fallback also now matches upstream's AppData location. The user.md mirror held the profile write lock across every classification call, which is blocking and untimed, so a long run stalled every other writer of user.md. The consolidator documents the opposite pattern for the same reason: classification never depends on file content, so headings are picked first and only the read-check-write of each section is serialized. Idempotency also moves off a whole-file substring test, which dropped a short entry that happened to appear inside unrelated text, and a genuine skip is now counted and returned rather than logged to a file nobody reads. Skill provenance reads .usage.json by the frontmatter name it is keyed on, and DiscoveredSkill now carries the name the pool identifies a skill by. A registry-name collision cannot be renamed away, because that name rides inside the copied SKILL.md: the second skill would sit on disk and never be what get(name) returns. It is skipped and counted instead, and the directory-rename path is left to the case it can actually fix -- two skills whose declared names differ, which is the shape two of the 82 skills on a real install have. Deletes ImportMessage.sender_id and DiscoveredSkill.size, both written by every producer and read by none; the everos adapter synthesises its own sender from role plus the host identity. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…ct stale docs The everos profile recalls as a single Memory whose score falls back to 1.0, which places it above every similarity-scored episode, and everos grows it monotonically with no server-side limit. Turning include_profile on therefore handed one unranked blob an unbounded and growing share of every turn's prompt. It is now capped client-side at roughly the combined budget of a full episode batch, truncated on a line boundary with a visible marker. The flag's comment said it costs nothing; that is true of the server and false of the prompt, and now says which. An install whose only importable data is skills was told there was nothing to import. Skills are directories rather than message sources, so they never appear as ScanResults, and both the empty-scan and empty-tier paths returned before the skill phase could run. The scan test that covered the empty case had to stub discover() as well -- it was reading the developer's own Hermes install, so its result depended on the machine. MemoryConfig's docstring and the plugin architecture doc still instructed users to keep memory.userId in sync with a per-plugin copy, which is the duplication this branch removed to stop a silent split between writes and reads. Both now describe ctx.services as the single path. The config docstring also still claimed a plugin's slice is validated against its manifest config_schema, which was never true and is why that block was deleted. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…ults Both everos scripts import _RealEverosAdapter and read _mode, neither of which survived the move to an HTTP-only backend, so they raise on import. That matters because the plugin architecture doc prescribed the roundtrip script as the EverOS-upgrade smoke step and the scripts README claimed it verified the identity wiring end-to-end; a maintainer following either would have hit a TypeError and had no smoke test at all. The USER.md mirror printed nothing on success while the skill phase printed its own line, so a mirror that worked looked like one that had not run. It now says what landed and what was already there. A conversation carries no cost hermes will state up front -- the listing gives an id and a source -- so the table showed 0 files at 0 B, which reads as an empty conversation rather than an unmeasured one. Unmeasured now renders as a dash. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The everos user profile recalls as prose, so a hit is not always one line, and the renderer emitted it as a single dash followed by unindented continuations -- which read as body text that escaped the list rather than as part of that entry. Also corrects the note beside the new profile ceiling. It described the score falling back to 1.0 as though it were a second exposure; it sorts the profile above every episode, but the profile is a direct fetch that never counts against top_k and the caller renders every hit, so nothing is displaced by it being first. Length was the whole problem, and length is what is capped. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The placeholders rendered in the same colour as the platforms that actually have data, so a menu of two real choices read as a menu of six. They were selectable too: picking one printed "not yet supported" and redrew the step, a round trip that told the user what the label already said. They now pass `disabled`, which greys the row through RAVEN_STYLE's `disabled` class -- defined since the theme landed and never once used -- and makes the arrow keys skip it, so the branch handling a pick of one is gone. `disabled=True` rather than a reason string: questionary appends the reason in its own hardcoded " (...)", and the label already carries the full-width pair the rest of the Chinese copy uses. The "- " prefix it puts on a disabled row is hardcoded in both of its branches and stays. Sorted so the pickable platforms come first. In enum order the two kinds interleave, which left Hermes sandwiched between two placeholders. The scripted-questionary fake in the tests kept dropping `**kwargs`, so `disabled=True` could be deleted with the suite still green; it records the flag now, and the ordering, the flag and the full-width label each have a test that a mutation confirmed fails without them. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Recording `disabled` in the previous commit fixed one instance of a general defect rather than the defect. The fake still dropped every other keyword a `Choice` is given, and dropped all of `select`'s -- so no test could observe `style` or `qmark`, and any prompt in the wizard could lose `style=RAVEN_STYLE`, fall back to questionary's own colours and marker mid-flow, and leave the suite green. `test_cli_import_commands` already records them; this file did not. Both now keep everything they are handed. That makes the assertion possible, so it is written: every prompt the import step raises carries the shared style and marker. Mutation-checked in both directions -- dropping the chrome from a prompt fails it, and so does the fake going back to discarding keywords. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The wizard's two early returns -- no scan result at all, and none surviving the tier filter -- printed "nothing to import" and stopped there. Skills are directories rather than message sources, so they never arrive as ScanResults and neither return had anything of theirs to act on: an install whose only importable data is skills was told there was nothing to import. `raven import run` already covered both paths, and the wizard is the one every new user takes. Both entry points now share a single skill counter as well. The wizard kept its own copy and called it inside the platform loop, so returning to that prompt re-walked the whole Hermes skill tree; the shared counter runs once, outside the loop. Also corrects two docstrings claiming `_memory_enabled` gates on llm and embedding. It has gated on llm alone since embedding became optional, and a maintainer reading either docstring would conclude the opposite. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`start()` called the capability probe inline. The probe is synchronous httpx with a 5s timeout and the config read behind it blocks too, so a wedged server stalled the loop every session begins on for the whole timeout. The sibling `_probe_health` in the same package already goes through `asyncio.to_thread`. The bool-only parse of `/health` also existed twice -- here, and in the adapter that picks its search lane from the same map -- along with two definitions of the timeout. Parsed two ways, `raven doctor` and recall could disagree about the same server, so both now call `parse_capabilities`. Records why `_flatten_profile` drops `*_ms` keys while here: they are the store's bookkeeping about when it learned something, not a fact about the user. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`partial_failure` is mutable instance state that `scan()` set but never reset, and `scan_all(scanners=...)` accepts instances a caller may scan more than once. The first run's failure then travelled with the second run's results, reporting a complete scan as one that came up short. Drops two `except HermesExportError: raise` clauses on the way past. The clause below each catches OSError, which cannot match a RuntimeError subclass, so neither branch changed any behaviour. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Cold-start import writes into the same `user.md` the consolidator owns, and reached its parser as `_parse_user_md_sections`, aliasing the underscore away at the import site. Renaming it drops that fiction: parsing the file a second way is how the two sides would come to disagree about what a section is. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
everos 1.2.1 migrates the LanceDB schema and prunes older manifest versions on first start, and reverting the pin undoes neither. That was recorded only in a pull-request description, which is known to whoever read it. The upgrade SOP now says to take a copy of ~/.everos/ beforehand, and to put a one-way migration in the release notes. Corrects two stale versions in the same section: the heading pinned 1.0.0, and the bump example installed 1.2.0 -- the one release that must be skipped, for a path-traversal regression fixed in 1.2.1. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
|
Adversarial review. I read the full diff and pulled every suspicion back onto the Line numbers are on The engineering is unusually careful -- Confirmed defects1. Skills are installed before the
|
| sessions | span | hermes subprocess invocations |
|---|---|---|
| 150 | 180d | 18 |
| 500 | 180d | 28 |
| 2000 | 365d | 78 |
Each is a cold Python CLI start, and _collect_window is sequential
(await left then await right). raven import scan and raven import run each
pay a full enumeration. asyncio.gather on the two halves is a cheap improvement if
hermes' SQLite reads tolerate the concurrency.
9. One /health, three readers
_server._probe_health, _health.probe_capabilities (sync httpx), and
_HttpEverosAdapter._probe_capabilities (async, on the client that already exists).
start() has ensure_everos_server prove reachability, then probes again
off-thread, and the adapter probes a third time on first recall.
Neither of the latter two sends the Authorization header the adapter uses
elsewhere -- if /health is ever auth-gated, both read as "no capabilities".
10. Menu padding implemented twice with different width functions
onboard_commands._tier_choice_label uses the CJK-aware _cell_len;
import_commands._pick_tier / _platform_choice_label reimplement the same padding
rule with bare len. Both sides are ASCII today so nothing is visibly wrong, but it
is one layout rule with two implementations.
11. `partial_failure` is an implicit protocol
scanners/__init__.py reads it via getattr(scanner, "partial_failure", None).
Putting it on the Scanner protocol with a None default makes it type-checkable
and makes "I can fail partially" an explicit part of the scanner contract.
What is good here
_uncoveredreconciling window coverage by arithmetic. A partition that misses a
range returns a perfectly well-formed short list; counting is the only thing that
can catch it, and that judgement is correct.parse_dry_run_listingtreating an unrecognised header as an error rather than a
zero. A silent 0 would read as success.- The
rmtreeafter a failedcopytree, restoring both the retry and the invariant
that a directory in the pool is a complete skill. _flatten_profileusing an allowlist rather than a denylist forevidence/
basis.- The multi-line recall bullet fix in
render.py, and dropping
ImportMessage.sender_id-- grepping the whole repo confirms it was dead.
On the size
57 files / +6597 carries at least five separable changes: the Hermes importer, the
everos 1.1.3 -> 1.2.1 upgrade, the wizard redesign, two incidental pre-existing bug
fixes, and the identity single-source refactor. Nothing in AGENTS.md forbids that,
but the identity refactor (two non-defaulted fields on ServiceLocator, 20+ test
construction sites touched) and the everos upgrade each deserve their own review
lens -- and the latter carries the one-way index migration this PR itself flags. If
anything can still be split out, the everos upgrade plus the identity refactor is
the seam I would cut on.
|
Second review pass, complementary to the earlier comment rather than a repeat of it. Rebase note first: the branch now conflicts with main. #266 landed after this PR's base Scope note: I re-verified every upstream claim this branch makes against the Hermes Before mergeR1 -- org-shared skills are imported past a gate Hermes itself applies
Here patch: # raven/importer/skills/hermes.py
_ORG_MIRROR_DIR = "_org"
_ORG_ACTIVE_MARKER = ".active_org"
def _read_active_org(root: Path) -> str | None:
"""Mirrors Hermes' ``read_active_org_id`` (agent/skill_utils.py).
No marker means no org mirror resolves at all; that is the gate, not a
default. A blank marker counts as absent.
"""
try:
return (root / _ORG_MIRROR_DIR / _ORG_ACTIVE_MARKER).read_text(encoding="utf-8").strip() or None
except OSError:
return None
# in discover(), before the loop:
active_org = _read_active_org(root)
for skill_md in sorted(root.rglob("SKILL.md")):
if not _is_discoverable(skill_md, root, active_org):
continue
# in _is_discoverable(skill_md, root, active_org), first check:
if parts and parts[0] == _ORG_MIRROR_DIR:
# Token-gated upstream: only the marked org's mirror resolves, so a
# stale mirror is content Hermes itself no longer loads.
if active_org is None or len(parts) < 2 or parts[1] != active_org:
return FalseDo NOT just add verify: R2 -- the agent-track e2e test can no longer fail
This PR moved identity to the host ( Consequence: assistant/tool rows are stamped from services ( patch: -def _backend(tmp_path: Path, *, agent_id: str) -> EverosBackend:
+def _backend(tmp_path: Path, *, user_id: str, agent_id: str) -> EverosBackend:
be = EverosBackend(
PluginContext(
- config={"agent_id": agent_id},
- services=ServiceLocator(workspace=tmp_path, user_id="default", agent_id="default"),
+ config={},
+ services=ServiceLocator(workspace=tmp_path, user_id=user_id, agent_id=agent_id),
)
)
- be = _backend(tmp_path, agent_id=ids.agent_id)
+ be = _backend(tmp_path, user_id=ids.user_id, agent_id=ids.agent_id)Three call sites in this file (L74, L106, L146).
verify: run with a real everos and confirm the skill assertions execute instead of R3 -- plugin manifest version not bumped, against this PR's own SOP
Description -- must land before the squash, since the PR body becomes the commit bodyR4 -- two factual errors, plus two metadata items
Follow-upR5 -- a configured-but-unbuilt multimodal role is reported by nobody
patch: # raven/plugin/memory/everos/_health.py
-DEGRADING_SECTIONS = ("embedding", "rerank")
+DEGRADING_SECTIONS = ("embedding", "rerank", "multimodal")
# raven/cli/doctor_commands.py, _degradation_note()
return {
"embedding": " (recall matches keywords, not meaning)",
"rerank": " (agent-track recall uses the LLM lane instead of a cross-encoder)",
+ "multimodal": " (images / PDFs / audio stay out of memory)",
}.get(section, "")The warning text needs adjusting with it: R6 -- "All platforms" plus "Run in background" starts an import that cannot run
Not a regression -- main is equally broken here (there patch (smallest correct option -- fall through to the foreground, which already works): else:
platform_flag = selected_platform if selected_platform != "all" else None
+ if platform_flag is None and len(by_platform) > 1:
+ # The child would have to ask which platform to use, and a
+ # detached process has no terminal to ask on.
+ console.print(_t(
+ " [yellow]Background mode cannot run an all-platforms import; "
+ "running in the foreground instead.[/yellow]",
+ " [yellow]<zh>[/yellow]",
+ ))
+ exec_mode = "foreground"
+ else:
cmd = [raven_bin, "import", "run", "--tier", selected_tier.value, "--yes"]
...The alternative is a real all-platforms flag (plus making verify: a test asserting the spawned argv needs no TTY -- the Popen branch has no coverage R7 -- residual on the window bounds (this closes G7)G7 asked whether hermes parses the naive bounds as UTC. It does not: One narrower residual survives: patch: -_PARTITION_FLOOR = datetime(1970, 1, 1)
+# 1970-01-02, not 1970-01-01: the bound is formatted as naive local time and
+# hermes parses it the same way, so an epoch-day floor is a pre-epoch local
+# timestamp east of UTC, which raises OSError on Windows.
+_PARTITION_FLOOR = datetime(1970, 1, 2)Omitting NitR8 -- log path hardcoded right next to the helper introduced to prevent that
- console.print("[dim]Check the server log: ~/.raven/logs/everos-server.log[/dim]")
+ from raven.plugin.memory.everos._server import server_log_path
+
+ console.print(f"[dim]Check the server log: {server_log_path()}[/dim]")Corrections to the earlier commentG1's fix belongs at the call sites, and wants the count. Do not prompt inside -async def _install_skills_without_a_scan(platform_filter: Platform | None) -> bool:
+async def _install_skills_without_a_scan(platform_filter: Platform | None, *, assume_yes: bool) -> bool:
if platform_filter not in (None, Platform.HERMES):
return False
+ count = await _importable_skill_count(platform_filter)
+ if not count:
+ return False
+ console.print(f"\nAbout to import {count} Hermes skills.")
+ if not assume_yes and not typer.confirm("Proceed?", default=True):
+ return True # handled: the user declined, nothing left to report
summary = await install_skills(HermesSkillSource(), load_config().workspace_path, _default_state())
- if summary.total == 0:
- return FalseCallers: G4's fix, both variants incomplete. Reporting after the await leaves the first call - for index, (entry, _) in enumerate(kept, start=1):
+ for index, (entry, _) in enumerate(kept):
if on_progress is not None:
on_progress(index, len(kept))
headings.append(await _pick_heading(entry, provider=provider, model=model))
+ if on_progress is not None and kept:
+ on_progress(len(kept), len(kept))G5's fix has a trap. Seeding - if skill.registry_name in claimed_registry_names:
- ...skip...
target = _target_for(skill, dest_root, claimed)
if target is None:
skipped += 1
continue
+ if skill.registry_name in claimed_registry_names | _pool_registry_names(dest_root, exclude=target):
+ ...skip...where G9 is real but hygiene only. everos ships no built-in auth ( G10 and G11 can be dropped. The The One blind spot to record rather than fix
I would not change code for this: telling "not started yet" from "crashes at boot" needs Suggested order
|
…t_import Conflict in tests/test_cli_onboard_commands.py: both sides appended tests at the end of the file, so both are kept -- #266's Ctrl+C contract test and its AST guard, plus this branch's role-cost and import-step tests. The AST guard resolves `_prompt_local_api_base` call sites only, and the selects this branch adds are in the chain #266 left alone, so it passes unchanged.
Four paths of one command that no test reached. The two early returns that install a skills-only import sit above the run's own `Proceed?` gate, so a machine whose only importable data is skills had directories copied into its skill pool before being asked anything -- and copying is the part nothing undoes. Both now name the count and ask. The wizard's `Start?` gate turned out to sit below its own two call sites, so those ask as well rather than inheriting a confirmation that comes later. Every covering test passed `--yes`, which is exactly what hid the missing gate. `run_import` returns normally once it sees the cancel file, so nothing downstream noticed: `raven import stop` halted the conversation loop and then started an LLM call per USER.md entry plus a full skill-tree copy. Both post-phases are now guarded on `summary.cancelled`. "All platforms" plus "run in background" could not work. `import run` has no all-platforms flag, so with more than one platform holding data the child reached its platform picker -- on DEVNULL, in a session with no controlling terminal, after the wizard had already reported the import as started. It now falls back to the foreground and says why. Also replaces a hardcoded `~/.raven/logs/everos-server.log` with `server_log_path()`, the helper added to stop that name from drifting. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`raven doctor` and the wizard both called `probe_capabilities()` with its default base URL while the backend reads `plugins.config[...].base_url`. Someone who moved everos off port 18791 was therefore told it was not running -- by the two surfaces added in this branch specifically so that a degraded server would be visible. `configured_base_url` reads the same two keys `_resolve_plugin_config_slice` does, without building a registry neither caller has another reason to build. The multimodal role was in the section-to-capability map and in neither consumer's iteration, so its mapping entry was dead and the role could fail to build with nothing saying so. It is config surface the wizard writes, so it joins DEGRADING_SECTIONS with a note of its own cost -- images, PDFs and audio staying out of memory, which also makes "so recall runs degraded" the wrong summary for the group; it now reads "so memory runs degraded". Records why nothing reaches doctor's exit-2 branch on everos 1.2.1: its `/health` reports llm as a hardcoded True, because a server that answered at all has one. The section list is the contract, and everos' own comment says that literal may become a real probe, so the branch stays. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`_EXCLUDED_DIRS` mirrors Hermes' EXCLUDED_SKILL_DIRS, but Hermes gates its org mirror separately and by token rather than by name: with no `.active_org` marker it prunes `_org` entirely, and with one it descends only into the org the marker names (`agent/skill_utils.py`, "leave an org and its skills stop resolving, without any manual cleanup"). Discovery walked the mirror regardless, so every org on disk was imported -- including orgs whose marker is gone, whose skills Hermes itself will not load, and for which there is no delete path once imported. Adding `_org` to the excluded set would have been the opposite error: it drops the active org's skills, which Hermes does load, and under-importing is the costlier direction here. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`claimed_registry_names` started empty every run and was filled only on a successful install, so a skill whose frontmatter name collided with one a previous run installed -- or with the user's own skill in the pool -- passed the guard, landed under a non-colliding directory name, and became precisely the skill `get(name)` never returns. That is the loss the guard exists to report. Ordered after `_target_for` so "already on disk" stays its answer to give: a re-run of the same skill is a no-op, not a dropped duplicate. That ordering also makes excluding the resolved target from the lookup unnecessary -- `_target_for` returns a path only when it does not exist yet, so the target is never among the pool's names. A first version excluded it anyway; mutation testing showed the exclusion could not change any outcome. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The progress line reported the call it was about to make, so with three entries it read 3/3 while the third LLM call was still in flight -- on a measured 9.4s run, about three seconds sitting at 100%, which is the symptom the phase reporter was added to remove. Reporting after the await instead leaves the first call with no progress at all, and reporting index-1 never reaches N/N; each report is now a count of what is done, with one close after the loop. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The window bounds are formatted as naive local time and hermes parses them the same way (`hermes_cli/session_filters.py`, "naive = local time", then `dt.timestamp()`). East of UTC, a floor of 1970-01-01 00:00 local is therefore a pre-epoch instant, and a naive `timestamp()` goes through mktime, which raises OSError for those on Windows. One day of headroom covers every real offset, and no hermes session predates it. Reachable only on the paged path -- Windows, plus more than the 100 sessions the dry-run listing caps at, plus a positive offset -- and it fails loudly there rather than silently dropping sessions, which is why this is a floor change and not a rework of the bounds. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Identity moved to the host in this branch, so `config={"agent_id": ...}` is
ignored and additionally trips the branch's own stale-key warning. The
agent-track test therefore stamped its assistant and tool rows from the services
default while `recall` queried the tag: `hits` came back empty, every per-hit
assertion ran over an empty list, and the `xfail` below absorbed the miss. The
test claimed to prove user_id/agent_id routing and could not go red.
The skill-evolution test carries the same dead key. There the store goes through
`everos.service.memorize` directly, so its recall already matched and the argument
was merely inert -- but nothing reads it, and the warning is noise.
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The upgrade SOP this branch edits asks for it: the everos pin moved 1.1.3 -> 1.2.1, the API prefix moved v1 -> v2, identity moved to the host, and `config_schema` is gone, while the manifest still said 1.0.0. Pinned in four places that have to move together, plus the two tests asserting them. Two corrections to the same package while here. The health probe on the adapter now sends the headers every other call on that client sends: everos ships no auth today, but a deployment behind a proxy that adds it would read an unauthenticated probe as a server with no capabilities. And `feedback`'s docstring named everos 1.0.0 as the version with no endpoint to consume the signals -- still true on 1.2.1, whose routes are get / health / knowledge / memorize / metrics / ome / search, so the statement is now dated to the version actually pinned. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
|
Both reviews are addressed. Thank you both -- between them they found nine Fixed
Every behavioural change had its fix reverted and the covering test confirmed red. G10 and G11 dropped as advised. The Not fixed (2), deliberatelyG6 -- G8 -- session-enumeration cost. Measured rather than estimated, and it comes out
Probe counts land within one of the review's own table, which is a useful Three places I ended up disagreeing1. G5's 2. My own first fix for the wizard's two call sites was wrong. I passed 3. G7 is closed with a second, independent check. Alongside the source reading On splitting: agreed in principle, and the everos upgrade plus the identity refactor |
|
The two items left deliberately unfixed are now tracked, so nothing here needs to grow the
Bare references, not Nothing else outstanding on my side. All eight items I raised are fixed at 723ac26, and I Spec side, re-verified on the new head rather than carried over: the eight new commits are One thing for whoever merges: the everos index migration is one-way, so per this PR's own |
Summary
Adds Hermes as a cold-start import source, alongside the existing Claude Code
scanner, across three kinds of data.
Memory files.
MEMORY.mdandUSER.mdare discovered through the sameprofile resolution Hermes itself uses (
HERMES_HOME, thenactive_profileredirecting to
<root>/profiles/<name>).USER.mdentries are additionallymirrored into the native
user_memory/profile/user.md: six host consumers(Curator, Personalizer, four Sentinel producers) read that file directly and
never see EverOS-only content. An LLM picks each entry's H2 heading; every
failure path lands the entry under
## Notes, which injection always includes,so a misclassification costs tokens rather than visibility.
Conversations. Sessions are enumerated by shelling out to
hermes sessions export --dry-run, whose listing is capped at 100 rows. Thescanner walks time windows until every session is covered rather than silently
importing the first 100. Only ended sessions are candidates, and
reopen_session()can clearended_at, so the window reconciliation documentsthe resulting race in both directions instead of assuming monotonicity.
Skills. Provenance is classified from on-disk evidence -- the
.bundled_manifestname-to-md5 map,.hub/lock.json, and.usage.json-- witha
package_hashbyte-identical to Hermes' own_dir_hash, so a skill stillmatching its factory content is left alone and only user-authored or
curator-managed skills are copied into the local skill pool. Skills never travel
as ScanResults, so the menus, the confirm line and the final summary all state
the skill count explicitly; without it a run that installs 12 of 82 reads as
having dropped 70.
Failure isolation:
scan_allnow reports a failing scanner through anon_errorcallback and keeps the other platforms' results, and a Hermes runwhose conversations cannot be enumerated still returns its memory files.
Two pre-existing defects found while working here are fixed in the same branch:
a multi-line recall hit escaped its bullet in the context segment renderer, and
a recalled EverOS profile was rendered as a Python repr and left uncapped.
Also in this branch, from hands-on testing of the finished flow:
raven import runcould never reach its interactive path. The selectorscalled questionary's synchronous
ask()from insideasyncio.run(), andprompt_toolkit drives
Application.run()throughasyncio.run(), whichraises inside a running loop. No test covered it: every case passed
--platformand--tier, bypassing the selectors.Printing them from
_build_and_runsent them to this module's Console whileonboard's live progress belongs to its themed Console, and Rich's Live can
only hold back writes on its own console, so they landed mid-bar and left the
bar duplicated.
import runselectors use the shared prompt style and glyphs instead ofquestionary's defaults; onboard's foreground import gains the phase reporter
it never had, so the multi-second USER.md mirror is no longer silent behind a
bar reading 100%.
The EverOS backend moves with it. Agent-track recall turned out to be dead:
agent-track HYBRID fuses
agent_caseandagent_skillthrough a rerankcross-encoder, and with no rerank provider the server refuses the request
outright, which
recallcatches and turns into an empty list against afile-only logger. A live 1.1.3 probe returns
RuntimeError: owner_type='agent' with method='hybrid' requires a rerank provider. The adapter now readscapabilities.rerankfrom/healthand asks for the LLM lane only when thecross-encoder is genuinely absent -- always sending it would make every
correctly configured user pay an LLM call per recall, never sending it leaves
the track dead for anyone who skipped the optional rerank role in onboard.
That capability is only readable from everos 1.2.1, so the pin moves 1.1.3 ->
1.2.1, skipping 1.2.0 (built from a branch missing the 1.1.4 fixes, including a
knowledge-upload path traversal). Memory endpoints move to
/api/v2, canonicalsince 1.2.0;
/api/v1still resolves to the same handlers but is documented asa legacy alias. Only everos endpoints move -- the Skill Hub and OpenRouter
clients have their own v1 surfaces.
That upgrade also broke an assumption Raven was making. "Server running" and
"server can recall" used to be the same statement; 1.2.1 boots with
[llm]aloneinstead of aborting, so a misconfigured provider answers 200 and quietly serves
less. Raven decided a role was configured by reading model + api_key out of
everos.toml, which cannot see that. Capabilities are now read from/healthatall three points where it matters: the memory step (right after the server it
already starts),
raven doctor(which had no EverOS check at all), andbackend.start()-- the only one of the three on the path every session takes,which is where an expired key actually surfaces. It follows the precedent already
set there for native Windows and prints to stderr rather than a log nobody reads,
but does not drop to a no-op: writes still land, and
everos cascade backfillgives those rows their vectors once the provider is fixed.
The same reading makes the embedding role genuinely optional. It was not before:
recall sends no
method, so it took the server default of HYBRID, which everosrefuses without an embedding provider -- skipping the role produced a memory that
stored everything and returned nothing. The adapter now asks for KEYWORD when no
embedding capability is reported, searching the same rows lexically instead of
semantically, and that also rules out the rerank fallback because KEYWORD's agent
path never reaches the cross-encoder. So the wizard stops requiring embedding and
states what each role buys before asking for it -- llm alone gets keyword recall,
embedding adds meaning, rerank sharpens ordering -- and skipping embedding is
reported in colour rather than dim, because skipping rerank costs ordering while
skipping embedding costs semantic recall entirely.
Fault and degradation are now distinguished rather than lumped together: only a
missing llm sets doctor's exit code, and
raven doctornames what eachunconfigured optional role costs, per role, since "optional" alone does not help
anyone decide. The wizard also stops asking whether to enable memory at all --
everos is the only backend, so the question framed a choice that does not exist,
and both of its answers were described wrongly (declining was said to use "native
Markdown memory", which does not exist:
raven/plugin/memory/holds only everos,and
ContextAssembler.owns_compactionis a hardcoded True that stops theconsolidator from ever updating user.md). Backing out is still possible and now
states the real consequence in colour. The memory LLM field is pre-filled with
the user's own main model rather than a recommended id, because a recommendation
is only reachable if their key carries it.
Two rounds of review on this branch found defects worth naming, since each was
invisible for the same reason -- no test covered the path:
Proceed?gate. The two earlyreturns that install a skills-only import sit above it, so a machine whose only
importable data is skills had directories copied before being asked anything.
Both paths now name the count and ask; the wizard's
Start?gate is furtherdown still, so its two call sites ask as well. Every covering test passed
--yes, which is what hid it.run_importreturns normallyonce it sees the cancel file, so
raven import stophalted the conversationloop and then started an LLM call per USER.md entry plus a full skill copy.
token-gates
_org/: with no.active_orgmarker it prunes the whole mirror,and with one it descends only into the named org, so a stale mirror is content
Hermes no longer loads. Discovery now mirrors that. Excluding
_orgoutrightwould have been the opposite error -- it drops the active org's skills.
doctorand the wizard probed the default address. Both tookDEFAULT_EVEROS_BASE_URLwhile the backend reads the configuredbase_url, soa user who moved everos off port 18791 was told it was not running -- by the
checks added to make a degraded server visible.
mapping had an entry both consumers skipped.
counted the call about to start; on a measured 9.4s run that left ~3s sitting at
100%, the exact symptom the phase reporter was added to remove.
earlier import or the user's own skill already claimed produced the invisible
skill that
get(name)never returns.import runhas noall-platforms flag, so the child reached its platform picker with DEVNULL on
both streams and no terminal, after the wizard reported the import as started.
It now runs in the foreground and says so.
The e2e tests this branch made dead are fixed too: identity moved to the host, so
config={"agent_id": ...}is ignored, and the agent-track test wrote under theservices default while querying the tag --
hitswas empty, its per-hitassertions vacuous, and an
xfailabsorbed it. The plugin manifest moves to1.1.0, which the upgrade SOP in this same diff asks for.
Not in scope: Codex, KimiCode and OpenClaw remain placeholders; cold-start
import stays an EverOS-only path with no native fallback.
Type
Verification
Beyond the suite, the importer was run against a real Hermes install at
--tier full: memory files, conversations and skills all landed, EverOSextracted agent cases from the imported conversations (
quality_score=1.0inthe server log), and a second run over the same data reported
Submitted: 0, Skipped: 4, confirming the checkpointing is idempotent.The everos upgrade was verified against a live server holding real imported
data: on 1.1.3 the agent track returns 500 and
/api/v2returns 404; on 1.2.1with this change the agent track returns an
agent_caseand the user track isunchanged at three episodes. Re-verified after killing the server and cold
starting it, with the resolved binary printed to confirm which build answered.
raven doctorand the wizard both report llm and embedding available withrerank absent against that server.
The suite was also run with HOME pointed at an empty directory, which is how the
memory-probe and import-step tests were caught reading the developer's own
configuration instead of their own fixtures. That pass also found the test suite
waiting out
ensure_everos_server's 30s readiness timeout on every machinewithout a running server -- always, on CI -- which cost one file 34 of its 35
seconds and is now stubbed.
The interactive flow was verified under a pty, comparing the two console
wirings directly, and each new assertion was mutation-tested: every behavioural
change on this branch had its fix reverted and the covering test confirmed red,
and only that test. Where a fix had a plausible wrong shape, that shape was
mutated too -- excluding
_orgwholesale rather than token-gating it, reportingprogress after the await instead of before, seeding the registry-name check from
the pool without ordering it after the target resolution. Two of those mutations
survived and were the more useful result: one showed a guard clause could not
fire (
_target_foronly ever returns a path that does not exist, so excluding itfrom the pool lookup was dead code, now removed), the other showed the wizard's
own confirmation sits below the returns it was supposed to cover.
User-facing docs are unchanged: this adds a platform to an existing documented
command rather than a new surface. The developer docs move with the code --
docs/memory-plugin-architecture.md(the everos pin, and the upgrade SOP nowstating that a one-way migration belongs in the release notes) and
scripts/README.md(two deleted scripts).Risk
The import writes to EverOS and to
user_memory/profile/user.md, and copiesskill directories into the local skill pool. EverOS has no delete endpoint, so
an unwanted import is not undone by reverting this branch; the skill copies and
the profile edits are plain files and can be removed by hand. Import state
lives in
~/.raven/import_state.jsonand can be deleted to start over.Behaviour changes for existing users: the onboard wizard's import step and
raven import runboth look different (shared prompt style, skill counts,one summary block).
raven import run's interactive path changes from"crashes immediately" to working. Rolling back restores the crash.
Three more, from the review round: a skills-only import now asks before copying
(one extra prompt, skippable with
--yes), an all-platforms import chosen with"run in background" runs in the foreground instead and says so, and skills under
a stale
_org/mirror are no longer imported -- Hermes does not load themeither, but anyone who had them imported by an earlier build keeps them, since
nothing here deletes.
Two consequences of the everos upgrade that operators need to know:
on first start and prunes older manifest versions, so once a machine has run
this, its memory index can no longer be read by an older everos. Reverting
the branch does not undo it.
[llm]alone. A misconfigured embedding provider used to fail loudly; it now degrades
to keyword search, and rows written in that state carry no vector until
everos cascade backfillis run. everos itself only logs one warning line,which is why the wizard,
raven doctorandbackend.start()all surface it --the last of those runs every session, so an expired key is reported rather
than discovered.
matching instead of failing, and the wizard no longer stops them from getting
there. That is the point of the change, but it means a user can now end up with
a working memory that is measurably worse at recall than the previous
all-or-nothing setup would have allowed.
Related Issues
N/A