This commit is contained in:
Aiden
2026-05-11 19:09:01 +10:00
parent 718e4dcadd
commit fdcc38c6ae
7 changed files with 64 additions and 10 deletions

View File

@@ -241,6 +241,7 @@ void ControlServices::PollLoop(RuntimeCoordinator& runtimeCoordinator)
completedCommit.generation = entry.second.generation;
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
mCompletedOscCommits.push_back(std::move(completedCommit));
PublishOscOverlaySettled(entry.second);
}
else if (!result.errorMessage.empty())
{
@@ -321,3 +322,22 @@ void ControlServices::PublishOscCommitRequested(const PendingOscCommit& commit)
OutputDebugStringA("OscCommitRequested event publish threw.\n");
}
}
void ControlServices::PublishOscOverlaySettled(const PendingOscCommit& commit)
{
try
{
OscOverlayEvent event;
event.routeKey = commit.routeKey;
event.layerKey = commit.layerKey;
event.parameterKey = commit.parameterKey;
event.generation = commit.generation;
event.settled = true;
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
OutputDebugStringA("OscOverlaySettled event publish failed.\n");
}
catch (...)
{
OutputDebugStringA("OscOverlaySettled event publish threw.\n");
}
}

View File

@@ -76,6 +76,7 @@ private:
void PublishRuntimeStateBroadcastRequested(const std::string& reason);
void PublishOscValueReceived(const PendingOscUpdate& update, const std::string& routeKey);
void PublishOscCommitRequested(const PendingOscCommit& commit);
void PublishOscOverlaySettled(const PendingOscCommit& commit);
std::unique_ptr<ControlServer> mControlServer;
std::unique_ptr<OscServer> mOscServer;

View File

@@ -178,9 +178,14 @@ RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameterByControlKey(co
RuntimeCoordinatorResult RuntimeCoordinator::CommitOscParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue)
{
std::lock_guard<std::mutex> lock(mMutex);
constexpr RuntimeCoordinatorOscCommitPersistence kDefaultOscCommitPersistence =
RuntimeCoordinatorOscCommitPersistence::SessionOnly;
constexpr bool kPersistSettledOscCommits =
kDefaultOscCommitPersistence == RuntimeCoordinatorOscCommitPersistence::Persistent;
std::string error;
ResolvedParameterMutation mutation;
if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, true, mutation, error))
if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, kPersistSettledOscCommits, mutation, error))
{
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
PublishCoordinatorResult("CommitOscParameterByControlKey", result);

View File

@@ -32,6 +32,12 @@ enum class RuntimeCoordinatorTransientOscInvalidation
All
};
enum class RuntimeCoordinatorOscCommitPersistence
{
SessionOnly,
Persistent
};
struct RuntimeCoordinatorResult
{
bool accepted = false;

View File

@@ -7,8 +7,8 @@ Phase 1 named the subsystems. Phase 2 added the typed event substrate. Phase 3 m
## Status
- Phase 5 design package: proposed.
- Phase 5 implementation: Step 3 complete.
- Current alignment: Phase 3 introduced the first pure composition boundary and transient OSC overlay owner. Phase 5 now has a small `RuntimeStateLayerModel` inventory that names the current state categories, `RenderStateComposer` consumes a `LayeredRenderStateInput` whose fields make base persisted, committed live, and transient automation inputs explicit, and `RuntimeLiveState` owns transient-overlay invalidation against current layer/parameter compatibility. Committed runtime values are still physically stored through `RuntimeStore`/`LayerStackStore`.
- Phase 5 implementation: Step 4 complete.
- Current alignment: Phase 3 introduced the first pure composition boundary and transient OSC overlay owner. Phase 5 now has a small `RuntimeStateLayerModel` inventory that names the current state categories, `RenderStateComposer` consumes a `LayeredRenderStateInput` whose fields make base persisted, committed live, and transient automation inputs explicit, `RuntimeLiveState` owns transient-overlay invalidation against current layer/parameter compatibility, and settled OSC commits have an explicit session-only persistence policy. Committed runtime values are still physically stored through `RuntimeStore`/`LayerStackStore`.
Current live-state footholds:
@@ -20,6 +20,7 @@ Current live-state footholds:
- `RuntimeServiceLiveBridge` drains OSC ingress/completion queues and applies them to render live state during frame preparation.
- `RuntimeStateLayerModel` names the Phase 5 state categories and classifies current fields as base persisted, committed live, transient automation, render-local, or health/config state.
- `RuntimeCoordinator` can request layer-scoped transient OSC invalidation, while `RuntimeLiveState` prunes overlays that no longer map to the current render-facing layer/parameter definitions.
- `RuntimeCoordinator::CommitOscParameterByControlKey(...)` commits settled OSC values into session state without requesting persistence by default.
## Why Phase 5 Exists
@@ -287,13 +288,22 @@ Make the transient-to-committed path explicit.
Initial target:
- document and test whether settled OSC commits persist
- ensure stale generation completions are ignored
- ensure one settled route does not clear unrelated overlay state
- publish or preserve useful events for accepted overlay commits
- [x] document and test whether settled OSC commits persist
- [x] ensure stale generation completions are ignored
- [x] ensure one settled route does not clear unrelated overlay state
- [x] publish or preserve useful events for accepted overlay commits
Current Phase 3 behavior is a good base; Phase 5 should make the policy easier to reason about from the code.
Current policy:
- settled OSC commits are `RuntimeCoordinatorOscCommitPersistence::SessionOnly` by default
- accepted settled OSC commits update the committed session value through `RuntimeStore::SetStoredParameterValue(..., persistState = false, ...)`
- accepted settled OSC commits publish runtime mutation/state-change observations, but no `RuntimePersistenceRequested` event
- accepted service-side commit completions publish `OscOverlaySettled`
- stale generation completions are ignored by `RuntimeLiveState::ApplyOscCommitCompletions(...)`
- unrelated routes remain untouched when a different route settles or completes
### Step 5. Separate Committed-Live Concept From Durable Storage
Decide whether to physically split committed-live state now or introduce a read/model boundary first.
@@ -377,7 +387,7 @@ Phase 5 can be considered complete once the project can say:
- [x] final render-value precedence is explicit and covered by tests
- [x] `RenderStateComposer` or its replacement consumes a layered input contract
- [x] reset/reload/preset behavior for transient overlays is centralized or clearly delegated
- [ ] OSC overlay settle/commit behavior is explicit, including persistence policy
- [x] OSC overlay settle/commit behavior is explicit, including persistence policy
- [ ] `RuntimeStore` remains durable-state focused and does not absorb transient automation policy
- [ ] render-local temporal/feedback state remains separate from live parameter layering
- [ ] subsystem docs and the architecture review reflect the final ownership model

View File

@@ -272,6 +272,8 @@ For OSC specifically, the coordinator should eventually decide:
- whether it should later commit into committed live state
- what reset/reload actions invalidate it
Phase 5 sets the default settled OSC policy to session-only. `CommitOscParameterByControlKey(...)` updates committed session state through the store with persistence disabled, publishes ordinary mutation/state-change observations, and does not request a persistence write unless a future explicit policy opts into durable OSC commits.
### Health and timing state
The coordinator may emit events like:

View File

@@ -311,8 +311,18 @@ void TestRuntimeCoordinatorPersistenceEvents()
Expect(countEvents(overlayEvents, RuntimeEventType::OscOverlayApplied) == 1, "transient OSC overlay is observable");
Expect(countEvents(overlayEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "transient OSC overlay does not request persistence");
expectAcceptedPersistence(coordinator.CommitOscParameterByControlKey("alpha", "gain", JsonValue(0.2)), "CommitOscParameterByControlKey",
"accepted OSC commit is persistent");
RuntimeCoordinatorResult oscCommitResult = coordinator.CommitOscParameterByControlKey("alpha", "gain", JsonValue(0.2));
std::vector<RuntimeEvent> oscCommitEvents = dispatchAndClear();
Expect(oscCommitResult.accepted, "accepted OSC commit updates committed session state");
Expect(!oscCommitResult.persistenceRequested, "settled OSC commit does not request persistence by default");
Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimeMutationAccepted) == 1, "settled OSC commit publishes accepted fact");
Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimeStateChanged) == 1, "settled OSC commit publishes state change");
Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "settled OSC commit publishes no persistence request");
RuntimeStore::StoredParameterSnapshot oscCommitSnapshot;
Expect(store.TryGetStoredParameterByControlKey("alpha", "gain", oscCommitSnapshot, error), "settled OSC commit can be read back");
Expect(!oscCommitSnapshot.currentValue.numberValues.empty() &&
oscCommitSnapshot.currentValue.numberValues[0] == 0.2,
"settled OSC commit updates the committed session value");
}
std::filesystem::remove_all(root);