fix(*): report an invalid memory identity as the config error it is - #350
fix(*): report an invalid memory identity as the config error it is#350gloryfromca wants to merge 1 commit into
Conversation
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
What I checked
The diff, the four start() call sites, the ServiceState machine, backward compatibility, the project rules in AGENTS.md / CONTEXT*.md, and whether the tests were weakened.
The change holds up
The premise checks out. _validate_identity is called from exactly one place (start()), and all four callers -- agent_commands.py:314, gateway_commands.py:398, tui_commands.py:746, import_commands.py:153 -- do fold a raise into logger.exception. Nothing else in the tree references _PATH_SAFE_ID_RE or _validate_identity, so no caller loses a signal by the raise becoming a state.
The state placement is right on both axes. _TERMINAL_STATES: identity comes from the ServiceLocator at construction and is never re-read, so no probe could ever promote out of it -- which is also why "start a new session" is the correct remedy in the message. _NEVER_HAD_MEMORY: with an invalid id no write would land regardless of the server's health, so counting the turn as a dropped write and blaming the service would send the user to debug something that never broke.
One thing the commit message understates: _state is initialised to READY when an adapter is injected (backend.py:379), not just to UNKNOWN. On that path the old raise left the state at READY, so store() would go on to write with the rejected identity. Setting BAD_IDENTITY closes that too.
_kick_probe returns before touching _last_probe_at for terminal states, so recall() on a bad identity does no work per turn.
The escape() is not theoretical -- I confirmed rich swallows the bracket class without it, and the test asserts the rendered form survives.
Repo rules
Commit header 65 chars, lowercase, ASCII, (*) scope for a two-package change; Co-authored-by trailer present. Both test files are existing ones extended rather than new files, per 5.4. Comments are English and carry a why. No new domain term needs a CONTEXT.md entry -- ServiceState members are not documented there. No assets touched.
Tests
Not weakened. test_illegal_identity_rejected_on_start became test_illegal_identity_is_reported_as_a_config_error, and the replacement asserts strictly more: the key name reaches stderr, the character class survives rich's markup parser, and the state lands on BAD_IDENTITY. The new outage test pins _dropped_writes == 0 and the absence of the "unavailable" line at stop().
Ran on 88984eb21613:
uv run pytest tests/test_everos_backend.py tests/test_cli_import_commands.py-- 149 passed, 0 skipped.uv run pytest tests/-- 6464 passed, 43 skipped, 1 failed.
The failure is tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare. It is not from this PR: that file is untouched, the failure reproduces from the file alone (pytest tests/test_cli_theme.py -- intra-file order pollution, it passes when run as a single node id), and it fails identically on origin/main in a clean worktree. Flagging it as a pre-existing bug someone should pick up separately, not as something this branch needs to fix. The 43 skips are the optional channel SDKs plus parametrize-level skips, all pre-existing.
One nit, non-blocking
On the raven import run path the user now gets the remedy twice with two different endings: start() prints to stderr "...Fix memory.userId / memory.agentId in your config.json, then start a new session", and _require_memory_service_ready then prints to stdout "...Fix memory.userId / memory.agentId in your config.json, then: raven import run". The second is the correct advice for a one-shot command and the first is not, but both are on screen. Options, in the order I would rank them: leave it (the wording is close enough that the reader takes the last one); or have the import gate print only the first line it adds -- "Memory identity is invalid; nothing would be imported" plus "Retry: raven import run" -- and let start()'s message carry the key names, since it always runs first on that path. Your call, and either is fine to merge as-is.
|
One describe-level correction before merge, since the PR body becomes the squash R1 (describe): "Every caller folds the raise into logger.exception" holds for The fix is unaffected -- it replaces a traceback with an actionable line on those |
|
R1 is real, and I owe you a correction of my own: my review said "all four callers -- Verified inventory on
So your quote from #271 is the accurate framing: three surfaces showed it as a traceback, and the TUI is the one that was silent. I suspect "every caller" came from #271's own list, which enumerates four Suggested replacement for the two sentences in Summary:
That also keeps the "one-shot CLI run writes no log file" clause honest -- it is true of the agent REPL, but R2, same describe-only class, since you are editing the body anyway. The Risk section says " On scope: editing the PR description is sufficient for what lands on Agreed that the fix itself is unaffected: on all four surfaces it replaces a traceback or silence with an actionable line, and the import path keeps the same exit code (1) while trading the traceback for two printed lines. No blockers on the code from me. |
An id EverOS cannot accept becomes a directory segment on its write path, so start() rejects it. The rejection carried the one message the user can act on, naming the key and the accepted pattern, and no surface delivered it as one. Three of the four call sites fold the raise into logger.exception; under the TUI loguru is file-only, so there an invalid identity turned memory off with nothing on screen. The fourth, in import_commands._build_and_run, does not wrap the call at all, so the ValueError escaped as a bare traceback. Where it was visible it was a traceback, which is not a thing a user reads as "edit this key". The state machine then said the opposite of the truth. start() died before assigning a state, so _state stayed UNKNOWN, which is not in _NEVER_HAD_MEMORY; every store() after it counted a dropped write, and the session ended by telling the user the memory service was unavailable. The service was fine. That sent them to debug a server that never broke. Catch the ValueError in start(), give it BAD_IDENTITY, and file that state with the other two the user must fix themselves. Terminal, so a stray probe cannot promote it to READY; never-had-memory, so no dropped-write count. The message goes to stderr, matching what the unconfigured-LLM and missing-binary paths beside it already do. The message is escaped on the way out. It quotes the accepted-character class, and rich reads "[a-zA-Z0-9_.@+-]" as a markup tag and eats it, leaving the user the pattern "^+$" to match their id against. raven import kept the outage wording on its own gate and pointed at the server log, which holds nothing about a config error. It now names the config keys instead. Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
88984eb to
1fc4c5d
Compare
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking, and only on prose: the R1 correction went into the commit body, which the squash throws away, and not into the PR description, which is the copy that lands on main. The code is byte-identical to what I signed off and still has my sign-off.
What moved since 88984eb21613
Two things, no code: a rebase onto d35e398c (#349 landed), and the commit body reworded to take R1. I diffed the branch's own content at both heads -- git diff 5edcda9c 88984eb21613 against git diff d35e398c 1fc4c5d601d4 -- and they are identical, so the earlier review of the implementation stands unchanged.
Blocker: the fix is in the discarded half
1fc4c5d6's body now reads correctly ("Three of the four call sites fold the raise into logger.exception ... The fourth, in import_commands._build_and_run, does not wrap the call at all"). The PR description still opens with the sentence @0xKT asked to have rewritten:
The rejection carried the one message a user can act on -- it names the key and the accepted pattern -- and nobody ever saw it. Every caller folds the raise into
logger.exception, and a one-shot CLI run writes no log file for it to land in.
That is the copy that survives. From the repo API:
allow_squash_merge: true
allow_merge_commit: false
allow_rebase_merge: false
squash_merge_commit_message: PR_BODY
squash_merge_commit_title: PR_TITLE
Squash is the only method enabled, so there is no path on which 1fc4c5d6's body reaches main -- AGENTS.md 3.3 says the same thing, and the API confirms it is configured, not just documented. Merge as it stands and main permanently records "Every caller folds the raise into logger.exception" while the accurate paragraph is dropped, which is the exact inversion of what @0xKT asked for: "in the PR body (it lands on main) and ideally the commit body too." The "ideally" half is done; the required half is not.
The failure is not recoverable after the fact -- correcting it post-merge means rewriting main. That is why I am calling it blocking rather than a nit, on a change whose code I would otherwise approve as-is.
Fix is a description edit, no code and no force-push: replace that paragraph in ## Summary with the one already written in 1fc4c5d6's body. If you would rather keep the two in sync permanently, the cheap habit is to write the paragraph once and paste it both places at push time; nothing enforces it, and this is the failure mode it has.
While you are in there: R2 from my last comment is still live at the same address. ## Risk still says "_may_spawn iterates the enum, so it covers the new member without a change", and _may_spawn is return self._state in _SPAWNABLE_STATES over the one-member allowlist frozenset({ServiceState.UNKNOWN}). Right conclusion, wrong mechanism, and it lands on main with the same merge. Details in the earlier comment rather than repeated here.
Rebase verification (AGENTS.md 3.5 step 4)
Ran on 1fc4c5d601d4, after the rebase:
uv run pytest tests/test_everos_backend.py tests/test_cli_import_commands.py-- 149 passed, 0 skipped.uv run pytest tests/-- 6478 passed, 43 skipped, 1 failed. The +14 over the 6464 I saw at the old head are #349's tests arriving with the new base, not anything here.
The one failure is tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare again. Still pre-existing and still unrelated: raven/cli/_theme.py and its test are untouched both by this branch and by 5edcda9c..d35e398c, and it reproduces from the file alone (pytest tests/test_cli_theme.py -- 1 failed / 44 passed) while passing as a single node id, so it is intra-file order pollution, not a base or rebase effect. The 43 skips are the optional channel SDKs plus parametrize-level skips, unchanged.
Covered
The delta between the two heads, the rebase (new base, re-run tests, no new failures), the PR description and commit body against AGENTS.md 3.1.1 and 3.3 (both ASCII-clean, header 67 chars, Fixes #271 present, Co-authored-by present), and whether the reword changed any code (it did not). The implementation, callers, state machine, back-compat and test-weakening checks are from the earlier review and unchanged.
|
Both corrections are in, in the PR body and in the commit body. R1Confirmed independently before editing. On the pre-change code the four I used gloryfromca's replacement paragraph with one change: Dropping "a one-shot CLI run writes no log file for it to land in" also removes the inaccuracy about R2Also confirmed: ScopeBoth bodies were rewritten rather than the PR description alone, so The non-blocking nit from the review: leaving itOn Option b -- have the gate print only its own first line plus The clean version is |
gloryfromca
left a comment
There was a problem hiding this comment.
Blocker lifted. No blockers; this can merge as far as I am concerned.
Both edits verified against the live PR description, not taken on report:
- R1 --
## Summarynow reads "no surface delivered it as one. Three of the four call sites fold the raise intologger.exception; under the TUI loguru is file-only ... The fourth, inimport_commands._build_and_run, is not wrapped at all, so theValueErrorescaped as a bare traceback." That is the accurate inventory, and the "one-shot CLI run writes no log file" clause is gone with it. Since squash takesPR_BODY, this is now the copy that lands onmain. - R2 --
## Risknow reads "_may_spawnallowlistsUNKNOWNvia_SPAWNABLE_STATES, so a new member is non-spawnable without a change." Matchesbackend.py:return self._state in _SPAWNABLE_STATESoverfrozenset({ServiceState.UNKNOWN}).
Description is still ASCII-clean (grep -nP "[^\x00-\x7F]" on the fetched body, 0 matches), Fixes #271 intact, and the head sha is unchanged at 1fc4c5d601d4, so the commit body is the one I already checked and the code is byte-identical to what I reviewed.
I also re-ran the three numbers the Verification section claims, since this whole thread has been about claims matching reality:
uv run pytest tests/test_everos_backend.py tests/test_cli_import_commands.py tests/test_everos_plugin_discovery.py -q 162 passed
uv run ruff check <the four touched files> All checks passed
uv run ruff format --check <the four touched files> 4 files already formatted
All three match, and the 1 failed, 6478 passed, 43 skipped full-suite line matches what I measured independently on this head last round -- the failure being the pre-existing test_cli_theme.py order-dependence in an untouched file.
Nothing outstanding from me on this revision.
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
Summary
An identity EverOS cannot accept becomes a directory segment on its write path, so
EverosBackend.start()rejects it. The rejection carried the one message a user can act on -- it names the key and the accepted pattern -- and no surface delivered it as one. Three of the four call sites fold the raise intologger.exception; under the TUI loguru is file-only, so there an invalid identity turned memory off with nothing on screen. The fourth, inimport_commands._build_and_run, is not wrapped at all, so theValueErrorescaped as a bare traceback. Where it was visible it was a traceback, which is not a thing a user reads as "edit this key".The state machine then told the user the opposite of the truth.
start()died before assigning a state, so_statestayedUNKNOWN, which is not in_NEVER_HAD_MEMORY; everystore()after it counted a dropped write, and the session ended with:The service was fine. That sends the user to debug a server that never broke.
The change. Catch the
ValueErrorinstart(), give it aBAD_IDENTITYstate, and file that state with the other two the user must fix themselves:READY;Two things came out of running it rather than reading it:
[a-zA-Z0-9_.@+-]as a markup tag and eats it, leaving the user the pattern^+$to match their id against. The test now pins the full class.raven importhad the same defect on its own gate. It kept the outage wording and pointed at the server log, which holds nothing about a config error. It now names the config keys instead.Before / after, same config (
memory.userId: "bad/id"), same command:Type
Verification
Re-run after rebasing onto
d35e398c, the currentmain.The one failure is
tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare. It is pre-existing on this base: checked outd35e398cin a clean worktree and ranuv run pytest tests/test_cli_theme.pythere, same failure, 1 failed / 44 passed. It passes in isolation and fails when the file runs as a whole, so it is order-dependent, and it is unrelated to anything here. That file is untouched by this change.Manual check, sandbox
RAVEN_HOME,memory.userId: "bad/id",raven agent -m "hi": prints the block quoted above, and the session no longer ends with the dropped-write line.Risk
Terminal-only wording plus one new enum member. No config, schema, or protocol change; an identity that was accepted before is accepted now, on the same code path. The only behaviour change for a valid setup is none.
For an invalid identity the turn behaves as it did -- memory off, turn completes -- and only what the user is told changes. Rollback is reverting the single commit.
ServiceState.BAD_IDENTITYis new. Every consumer was checked:raven/cli/import_commands.py(handled here) andtests/test_everos_plugin_discovery.py(readsREADYonly)._may_spawnallowlistsUNKNOWNvia_SPAWNABLE_STATES, so a new member is non-spawnable without a change.Related Issues
Fixes #271