Skip to content

journal: record failed action attempts, not only successes - #33

Open
Yaraslaut wants to merge 2 commits into
masterfrom
feature/23-journal-records-failures
Open

journal: record failed action attempts, not only successes#33
Yaraslaut wants to merge 2 commits into
masterfrom
feature/23-journal-records-failures

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Summary

  • Both real Model::execute() call sites (ActionDispatcher::registerAction's runner for remote/Qt topologies, and Bridge::executeVia's localOp for LocalBackend) only appended a journal LogEntry after a successful execution. A thrown action (validation failure, lost connection, rejected write) left the audit trail silent.
  • Wraps both call sites in try/catch (const std::exception&): success records outcome=Succeeded with the result (unchanged), failure records outcome=Failed with error=what() and empty result, then rethrows unchanged — no behavior change for callers, only the journal gains entries it previously lacked.
  • Adds LogEntry::outcome (new Outcome enum: Succeeded/Failed, defaulting to Succeeded so existing on-disk entries decode unchanged) and LogEntry::error. Outcome gets a glz::meta so it serialises as the readable string, not a bare 0/1.
  • Follow-on correctness fix found while implementing: since a Failed entry can now appear in the same log stream replay()/undoLast() read, and a failed attempt never mutated model state, journal::replay() now skips Outcome::Failed entries before dispatching — otherwise reconstruction would re-run the rejected action and very likely throw the same exception again, aborting reconstruction. Covered by a dedicated regression test.
  • Design docs (docs/spec/journal/journal.md) updated: field table, API reference, and the replay()/Invariants sections that previously (correctly, at the time) stated "only successful actions are recorded."

Test plan

  • tests/test_action_log.cpp: new tests for both execution sites recording outcome=Failed/error on a thrown action and still rethrowing; existing success-path tests extended to assert outcome=Succeeded; new replay() test proving a Failed entry is skipped (not re-dispatched, no throw, correct reconstructed state).
  • Full suite: ./build/tests/morph_tests — all 814 test cases / 8304 assertions pass.

Closes #23

🤖 Generated with Claude Code

Both real Model::execute() call sites -- ActionDispatcher::registerAction's
runner (registry.hpp, server/remote topologies) and Bridge::executeVia's
localOp (bridge.hpp, LocalBackend) -- appended a journal LogEntry only after
a successful execution. An action that threw (a validation failure, a lost
connection, a rejected write) left no entry at all, so an audit trail could
not answer "who tried X and was refused" -- it just looked quiet.

Wrap both call sites in try/catch: on success, record outcome=Succeeded with
the JSON result (unchanged from before); on a caught std::exception, record
outcome=Failed with the exception's what() and no result, then rethrow
unchanged -- callers see no behavior change, only the journal gains entries
it previously lacked.

Add LogEntry::outcome (a new Outcome enum: Succeeded/Failed, defaulting to
Succeeded so pre-existing on-disk entries decode unchanged) and
LogEntry::error (empty unless Failed). Outcome gets its own glz::meta so it
serialises as the readable string "Succeeded"/"Failed" rather than a bare
0/1, keeping the NDJSON audit trail human-readable.

Since a Failed entry can now appear in the same log stream replay()/
undoLast() read, and a failed attempt never mutated model state,
journal::replay() now skips Outcome::Failed entries before dispatching --
otherwise reconstruction would re-run the rejected action and very likely
hit the same exception again, aborting the reconstruction.

Closes #23

Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Windows cl-debug/cl-release failed under /W4 /WX: the catch parameter
`exc` in ActionDispatcher::registerAction's runner (registry.hpp) and
Bridge::executeVia's localOp (bridge.hpp) is only referenced inside an
`if constexpr (... == Loggable::Yes)` branch. MSVC's C4101 fires when
that branch is compiled out (e.g. lab::ListSamples, a Loggable::No
action), because the discarded branch's use of exc.what() is never
instantiated. GCC/Clang don't warn on this. Mark both parameters
[[maybe_unused]].

Also documents the try/catch outcome-recording behavior in
docs/spec/core/bridge.md and registry.md (the header <-> spec sync CI
gate flagged include/morph/core/*.hpp changing without a matching
docs/spec/core/** update).

Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Journal records only successful actions; failures leave no entry

1 participant