bake: name the five ways the extension can fail to answer, and move the landing verdict into a pure, tab-provable classifier

A no-answer stays a failure; it now says whether the extension never ran the landing, answered a stale generation, spoke a wire this build cannot read, cleared the request, or refused it.
This commit is contained in:
2026-08-02 06:38:04 -04:00
parent 6e937b9c61
commit 962ab64ef0
8 changed files with 373 additions and 48 deletions
+18 -27
View File
@@ -6,7 +6,6 @@
#include "shell/capture/bake_land.h"
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <filesystem>
#include <fstream>
@@ -53,13 +52,6 @@ using wire::BakeOutcome;
using wire::BakeRequest;
using wire::BakeStatus;
// The whole chain is a call and a return inside ONE editor tick, so a request older than
// this has no reader left: it is a crash leftover, and it is CLEARED rather than landed.
// Without the guard a stranded request would be banked on the next unrelated instance's
// click, and the outcome written back to a key nobody will collect would persist into the
// .rpp forever.
constexpr std::int64_t kMaxRequestAgeSeconds = 30;
BakeOutcome refuse(BakeStatus status, std::string message, std::int64_t generation) {
BakeOutcome out;
out.status = status;
@@ -268,15 +260,12 @@ struct Answer {
} // namespace
void RunResampleBake(ReaSamplerSession& session) {
// Three projects have to agree before anything may land: the tab the request was found
// in, the tab whose book/ledger poll() last loaded, and the tab saveToActiveProject
// will persist into. Land on a disagreement and one tab's bake is written into
// another's bank. Whether REAPER makes Main_OnCommandEx's `proj` current for the
// action's duration is unverified in the DAW; this holds either way, and a request it
// cannot land is told why rather than silently ignored.
// Whether REAPER makes Main_OnCommandEx's `proj` current for the action's duration is
// unverified in the DAW; the per-key verdict holds either way, and a request it cannot
// land is told why rather than silently ignored.
const void* loaded = session.loadedProject();
const void* active = EnumProjects(-1, nullptr, 0);
const bool sessionUsable = loaded != nullptr && loaded == active;
const wire::BakeScanContext scanContext{loaded != nullptr, loaded == active};
const std::int64_t nowSec = static_cast<std::int64_t>(std::time(nullptr));
std::vector<Answer> answers;
@@ -285,21 +274,23 @@ void RunResampleBake(ReaSamplerSession& session) {
for (const OpenProject& open : openProjects()) {
for (const std::string& key : pendingBakeKeys(open.proj)) {
const std::optional<std::string> raw = readKey(open.proj, key);
if (!raw) continue;
const std::optional<BakeRequest> request = wire::decodeBakeRequest(*raw);
// Not a request: an outcome this instance has not yet collected, or a value from
// a build we do not read. Leave it — the writing instance owns clearing its key.
if (!request) continue;
const std::optional<BakeRequest> request =
raw ? wire::decodeBakeRequest(*raw) : std::nullopt;
const wire::BakeScanKey scanKey{
request.has_value(), request ? request->generation : 0,
static_cast<const void*>(open.proj) == loaded};
const wire::BakeScanVerdict verdict =
wire::classifyBakeScan(scanContext, scanKey, nowSec);
// Stale (either direction, so a clock moved backwards is caught too): clear the
// key, never answer it. The instance that could read an answer is gone.
if (std::llabs(nowSec - request->generation) > kMaxRequestAgeSeconds) {
// Leave it — the writing instance owns clearing its own key.
if (verdict == wire::BakeScanVerdict::Ignore) continue;
if (verdict == wire::BakeScanVerdict::ClearStale) {
answers.push_back(Answer{open.proj, key, std::string{}, std::string{}});
continue;
}
BakeOutcome outcome;
if (!sessionUsable || static_cast<const void*>(open.proj) != loaded) {
if (verdict == wire::BakeScanVerdict::RefuseWrongProject) {
outcome = refuse(BakeStatus::WrongProject,
"this bake's project tab is not the one the extension has "
"loaded -- focus that tab and try again",
@@ -310,9 +301,9 @@ void RunResampleBake(ReaSamplerSession& session) {
}
// A WrongProject request left sitting in a tab this call did not come from
// prints again on every OTHER tab's bake, since the scan revisits every open
// project each time. `active` is this call's own proxy for "the invoking tab"
// (see the three-projects comment above) — only that tab's own refusal is fresh
// feedback to a user who just clicked bake; every other one is a rescan repeat.
// project each time. `active` is this call's own proxy for "the invoking tab" —
// only that tab's own refusal is fresh feedback to a user who just clicked
// bake; every other one is a rescan repeat.
const bool ownRequest = static_cast<const void*>(open.proj) == active;
std::string console;
if (outcome.status != BakeStatus::Ok &&