fix: bake-answer messages stop asserting causes the classifier can't see

Rewords Cleared/Unanswered/ForeignRequest to name only observed facts and every live hypothesis instead of picking a winner; adds a guarded answeredOutcome accessor against a future unhandled BakeAnswerKind; folds an unreachable BakeScanKey/Context test state; fixes generation-stamp timing.
This commit is contained in:
2026-08-02 06:57:08 -04:00
parent 962ab64ef0
commit 7f3b00a646
6 changed files with 139 additions and 35 deletions
+13 -5
View File
@@ -127,6 +127,9 @@ std::optional<BakeOutcome> decodeBakeOutcome(const std::string& wire) {
BakeAnswer classifyBakeAnswer(const std::optional<std::string>& raw,
const BakeRequest& sent) {
BakeAnswer answer;
// `raw` folds three distinct bridge outcomes into one — absent, explicitly
// cleared, and an unreadable/oversized read — because the bridge cannot label
// which occurred. A caller's message for `Cleared` must not claim more than that.
if (!raw || raw->empty()) {
answer.kind = BakeAnswerKind::Cleared;
return answer;
@@ -150,6 +153,11 @@ BakeAnswer classifyBakeAnswer(const std::optional<std::string>& raw,
return answer;
}
const BakeOutcome* answeredOutcome(const BakeAnswer& answer) {
if (answer.kind != BakeAnswerKind::Answered || !answer.outcome) return nullptr;
return &*answer.outcome;
}
BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanKey& key,
std::int64_t nowSec) {
// Not a request: an outcome the writing instance has not collected yet, or a value
@@ -161,11 +169,11 @@ BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanK
if (age > kMaxRequestAgeSeconds || age < -kMaxRequestAgeSeconds)
return BakeScanVerdict::ClearStale;
// Three tabs have to agree before anything may land: the tab the request was found in,
// the tab the session's book/ledger last loaded, and the tab a persist will write into.
// Landing on a disagreement writes one tab's bake into another's bank.
const bool landable = session.sessionHasLoadedProject &&
session.loadedProjectIsActive && key.inLoadedProject;
// Two facts have to agree before anything may land: the loaded project is the one a
// persist would write into (loadedProjectIsActive), and this key's tab IS that loaded
// project (matchesLoadedProject — false by construction whenever no project is
// loaded, which is what folds the former three-way check into two).
const bool landable = session.loadedProjectIsActive && key.matchesLoadedProject;
return landable ? BakeScanVerdict::Land : BakeScanVerdict::RefuseWrongProject;
}
+26 -9
View File
@@ -92,7 +92,8 @@ enum class BakeAnswerKind {
ForeignOutcome, // a decodable outcome, but for another generation
Unanswered, // this request, unchanged: nothing on the extension side read it
ForeignRequest, // a request that is not ours — another instance shares this key
Cleared, // the key holds nothing: cleared without an answer
Cleared, // the key holds nothing — absent, explicitly cleared, or a read
// failure at the bridge; these are not distinguishable from here
Undecodable, // neither record — a build whose wire this one does not read
};
@@ -105,9 +106,20 @@ struct BakeAnswer {
BakeAnswer classifyBakeAnswer(const std::optional<std::string>& raw,
const BakeRequest& sent);
// The outcome iff `answer.kind == Answered`; nullptr in every other state. The ONE place
// the Answered-implies-outcome-is-set contract is enforced, so a caller can fail closed
// on a state this switch does not (yet) name instead of dereferencing `answer.outcome` on
// the strength of switch exhaustiveness alone — exhaustiveness a future BakeAnswerKind
// enumerator (BakeAnswerKind is documented append-only, like its BakeStatus neighbor)
// would silently break with no compiler diagnostic (no -Wswitch/-Werror configured).
const BakeOutcome* answeredOutcome(const BakeAnswer& answer);
// The whole chain is a call and a return inside ONE editor tick, so a request older than
// this has no reader left. Landing one would bank it for nobody and leave an outcome
// nobody collects in the .rpp forever.
// nobody collects in the .rpp forever. The bound is only meaningful because both ends
// read the same wall clock (std::time) AND the instrument stamps `generation`
// immediately before publishing the request — after staging the WAV, so that write
// never eats into the budget.
inline constexpr std::int64_t kMaxRequestAgeSeconds = 30;
// The extension's per-key verdict on one scanned `rsbake_*` key.
@@ -118,19 +130,24 @@ enum class BakeScanVerdict {
Ignore, // not a request (an uncollected outcome, or a wire we do not read)
};
// The session's side of the verdict which tab the extension's book belongs to, and
// whether that tab is the one REAPER will persist into.
// The session's side of the verdict: whether the loaded project is the one REAPER will
// persist into. Whether a project is loaded at ALL is folded into
// BakeScanKey::matchesLoadedProject below rather than carried as a second flag here — a
// key can only ever match a project that is loaded, so a standalone "session has a
// project" flag on this side could disagree with the key's and describe a state the
// shell can never actually produce.
struct BakeScanContext {
bool sessionHasLoadedProject = false; // the session has polled a project
bool loadedProjectIsActive = false; // that project is REAPER's active tab
bool loadedProjectIsActive = false; // the loaded project is REAPER's active tab
};
// The scanned key's side. Both flags are per-TAB, which is what makes a request found in
// a background tab decidable without any REAPER type crossing into this module.
// The scanned key's side per-TAB, which is what makes a request found in a background
// tab decidable without any REAPER type crossing into this module.
struct BakeScanKey {
bool decoded = false; // the value decoded as a BakeRequest
std::int64_t generation = 0;
bool inLoadedProject = false;
bool matchesLoadedProject = false; // this key's tab IS the session's loaded project
// — false whenever the session has no loaded
// project, by construction (see BakeScanContext)
};
BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanKey& key,