From a87e5aa4e0d8220d3031eba145411adbdebae7df Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 15 Jun 2026 03:33:03 +0100 Subject: [PATCH] Publish AquaSynth live session state --- docs/daemonic-swarm-prior-work.md | 6 +- docs/gamecult-daemonic-swarm.md | 8 +-- docs/verse-service-contract.md | 7 ++- src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs | 1 + src/AquaSynth.Faust/AquaSynthDaemonService.cs | 61 ++++++++++++++++++- state/evidence.jsonl | 1 + state/map.yaml | 2 +- state/spine.yaml | 2 +- .../AquaSynthDaemonServiceTests.cs | 16 +++++ 9 files changed, 93 insertions(+), 11 deletions(-) diff --git a/docs/daemonic-swarm-prior-work.md b/docs/daemonic-swarm-prior-work.md index 64c91ed..3078b56 100644 --- a/docs/daemonic-swarm-prior-work.md +++ b/docs/daemonic-swarm-prior-work.md @@ -16,8 +16,10 @@ architecture. It is a reference ledger for `instrument.open`, `instrument.control`, `instrument.block`, and `instrument.close` execution. It writes compile/session/render/stream/live/ operator witnesses, keeps native streaming patch instances for live sessions, - releases live sessions without deleting compiled patch witnesses, and emits - CultMesh-compatible stream descriptors and packet receipts. + rewrites retained `aquasynth.live_instrument_session.v1` state on lifecycle + transitions, releases live sessions without deleting compiled patch + witnesses, and emits CultMesh-compatible stream descriptors and packet + receipts. - `src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs`: local CultNet database host and command watcher. Typed command documents enter the database; typed receipts and provider state come back out. diff --git a/docs/gamecult-daemonic-swarm.md b/docs/gamecult-daemonic-swarm.md index 618235e..dee8f94 100644 --- a/docs/gamecult-daemonic-swarm.md +++ b/docs/gamecult-daemonic-swarm.md @@ -116,8 +116,8 @@ client can send `instrument.sample`, `instrument.stream`, `instrument.open`, state through the local CultNet database. AquaSynth compiles or opens the instrument, keeps the native streaming patch instance, applies controls, releases live sessions, writes sample/block artifacts and receipts, and -publishes the result. The client consumes the capability; it does not become -the synth. +publishes retained live-session state. The client consumes the capability; it +does not become the synth. That lets one AquaSynth daemon sit on this machine or another machine in the Verse and serve multiple apps. The same engine can support Fensalir, Eve @@ -296,8 +296,8 @@ should eventually move through a low-latency local transport such as shared-memory rings, platform audio graph nodes, memory-mapped packet buffers, or another fixed-block lane that does not drag filesystem writes or chatty command protocols into the audio callback. The command surface still owns -intent, patch lifetime, controls, and receipts. The realtime lane just moves -sound fast. +intent, patch lifetime, controls, retained session state, and receipts. The +realtime lane just moves sound fast. That distinction matters. Transport is not authority. A ring buffer should not decide what instrument exists. A renderer should not decide which command diff --git a/docs/verse-service-contract.md b/docs/verse-service-contract.md index 00b363c..b04bd76 100644 --- a/docs/verse-service-contract.md +++ b/docs/verse-service-contract.md @@ -43,6 +43,9 @@ binary lifetime, live instrument handles, control sessions, and sampled outputs. control catalog, architecture target, and lifecycle receipts. - `.aquasynth/sessions/{sessionId}.cc`: live instrument handle, lease, control values, transport state, and teardown receipt. +- `.aquasynth/live-sessions/{sessionId}.cc`: retained live session state for + the service-owned streaming patch instance: open/closed status, current + control values, block size, next sequence, and last command/receipt ids. - `.aquasynth/renders/{renderId}.cc`: offline/live sample outputs, feature measurements, and scoring receipts. - `.aquasynth/speech/{utteranceId}.cc`: Weksa handoff consumption, packed @@ -116,7 +119,9 @@ invent a separate truth for compiled products or control state. `instrument.control`, `instrument.block`, and `instrument.close` envelopes. These live commands keep and release service-owned native streaming patch instances; closing a live session does not delete the reusable compiled patch - witness. + witness. Each lifecycle transition also rewrites + `.aquasynth/live-sessions/{sessionId}.cc` so clients and operators can inspect + the current session truth without replaying receipts. - `cultnet-host`: starts a local CultNet host/database for AquaSynth command and receipt documents, publishes `aquasynth.cultnet_provider.v1`, and watches typed command records until the process is stopped. diff --git a/src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs b/src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs index a4bf12a..5fff7ce 100644 --- a/src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs +++ b/src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs @@ -437,6 +437,7 @@ public static class AquaSynthCultNetDocuments .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) + .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) .Register(CultNetDocumentBinding.ForDocument()) diff --git a/src/AquaSynth.Faust/AquaSynthDaemonService.cs b/src/AquaSynth.Faust/AquaSynthDaemonService.cs index 80a4730..6cda512 100644 --- a/src/AquaSynth.Faust/AquaSynthDaemonService.cs +++ b/src/AquaSynth.Faust/AquaSynthDaemonService.cs @@ -21,6 +21,7 @@ public static class AquaSynthDaemonSchemas public const string InstrumentControlCommand = "aquasynth.command.instrument_control.v1"; public const string InstrumentBlockCommand = "aquasynth.command.instrument_block.v1"; public const string InstrumentCloseCommand = "aquasynth.command.instrument_close.v1"; + public const string LiveInstrumentSession = "aquasynth.live_instrument_session.v1"; public const string InstrumentOpenReceipt = "aquasynth.instrument_open_receipt.v1"; public const string InstrumentControlReceipt = "aquasynth.instrument_control_receipt.v1"; public const string InstrumentBlockReceipt = "aquasynth.instrument_block_receipt.v1"; @@ -129,6 +130,25 @@ public sealed record AquaSynthCompiledInstrumentSession( [property: Key(11)] string[] ControlPaths, [property: Key(12)] string[] ProbePaths); +[CultDocument("aquasynth.live_instrument_session", AquaSynthDaemonSchemas.LiveInstrumentSession)] +[MessagePackObject] +public sealed record AquaSynthLiveInstrumentSessionState( + [property: Key(0), CultName] string SessionId, + [property: Key(1), CultIndex("patch")] string PatchId, + [property: Key(2)] string Status, + [property: Key(3)] string OpenedAtUtc, + [property: Key(4)] string UpdatedAtUtc, + [property: Key(5)] string ClosedAtUtc, + [property: Key(6)] int SampleRate, + [property: Key(7)] int InputCount, + [property: Key(8)] int OutputCount, + [property: Key(9)] int BlockSize, + [property: Key(10)] float Gain, + [property: Key(11)] ulong NextSequence, + [property: Key(12)] Dictionary Controls, + [property: Key(13)] string LastCommandId, + [property: Key(14)] string LastReceiptId); + [CultDocument("aquasynth.patch_compile_receipt", AquaSynthDaemonSchemas.PatchCompileReceipt)] [MessagePackObject] public sealed record AquaSynthPatchCompileReceipt( @@ -430,7 +450,7 @@ public sealed class AquaSynthDaemonService : IDisposable oldLive.Dispose(); } - liveSessions[compile.SessionId] = new LiveInstrumentSession( + var liveSession = new LiveInstrumentSession( compile.SessionId, command.PatchId, command.BlockSize, @@ -438,6 +458,7 @@ public sealed class AquaSynthDaemonService : IDisposable patch, patch.CreateStreamingPatch(), controls); + liveSessions[compile.SessionId] = liveSession; var receipt = new AquaSynthInstrumentOpenReceipt( ReceiptId(command.CommandId, "open"), @@ -453,6 +474,7 @@ public sealed class AquaSynthDaemonService : IDisposable [.. patch.ControlPaths], [.. patch.ProbePaths]); await WriteReceiptAsync("live", receipt.ReceiptId, receipt).ConfigureAwait(false); + await WriteLiveSessionStateAsync(liveSession, "open", command.CommandId, receipt.ReceiptId).ConfigureAwait(false); await WriteOperatorStateAsync(command.CommandId, receipt.Status).ConfigureAwait(false); return receipt; } @@ -488,6 +510,7 @@ public sealed class AquaSynthDaemonService : IDisposable Now(), controls.Count); await WriteReceiptAsync("live", receipt.ReceiptId, receipt).ConfigureAwait(false); + await WriteLiveSessionStateAsync(session, "open", command.CommandId, receipt.ReceiptId).ConfigureAwait(false); await WriteOperatorStateAsync(command.CommandId, receipt.Status).ConfigureAwait(false); return receipt; } @@ -538,13 +561,14 @@ public sealed class AquaSynthDaemonService : IDisposable var floatPath = Path.Combine(blockDir, $"{SafeFileName(blockId)}.f32"); await WriteFloat32Async(floatPath, mono).ConfigureAwait(false); + var sequence = session.NextSequence(); var receipt = new AquaSynthInstrumentBlockReceipt( blockId, command.CommandId, command.SessionId, "succeeded", Now(), - session.NextSequence(), + sequence, session.Patch.Manifest.SampleRate, mono.Length, Peak(mono), @@ -552,6 +576,7 @@ public sealed class AquaSynthDaemonService : IDisposable ToArtifactUri(floatPath), await Sha256Async(floatPath).ConfigureAwait(false)); await WriteReceiptAsync("live", receipt.BlockId, receipt).ConfigureAwait(false); + await WriteLiveSessionStateAsync(session, "open", command.CommandId, receipt.BlockId).ConfigureAwait(false); await WriteOperatorStateAsync(command.CommandId, receipt.Status).ConfigureAwait(false); return receipt; } @@ -585,6 +610,7 @@ public sealed class AquaSynthDaemonService : IDisposable "succeeded", Now(), true); + await WriteLiveSessionStateAsync(session, "closed", command.CommandId, receipt.ReceiptId, receipt.ClosedAtUtc).ConfigureAwait(false); await WriteReceiptAsync("live", receipt.ReceiptId, receipt).ConfigureAwait(false); await WriteOperatorStateAsync(command.CommandId, receipt.Status).ConfigureAwait(false); return receipt; @@ -843,6 +869,32 @@ public sealed class AquaSynthDaemonService : IDisposable await WriteReceiptAsync("operator", "operator-state", state, global: true).ConfigureAwait(false); } + private Task WriteLiveSessionStateAsync( + LiveInstrumentSession session, + string status, + string commandId, + string receiptId, + string closedAtUtc = "") + { + var state = new AquaSynthLiveInstrumentSessionState( + session.SessionId, + session.PatchId, + status, + session.OpenedAtUtc, + Now(), + closedAtUtc, + session.Patch.Manifest.SampleRate, + session.Stream.InputCount, + session.Stream.OutputCount, + session.BlockSize, + session.Gain, + session.NextSequenceValue, + new Dictionary(session.Controls, StringComparer.OrdinalIgnoreCase), + commandId, + receiptId); + return WriteReceiptAsync("live-sessions", session.SessionId, state); + } + private bool TryCompileScriptForDuration( AquaSynthCompileIdentity identity, float durationSeconds, @@ -1109,6 +1161,7 @@ public sealed class AquaSynthDaemonService : IDisposable Patch = patch; Stream = stream; Controls = controls; + OpenedAtUtc = Now(); } public string SessionId { get; } @@ -1125,6 +1178,10 @@ public sealed class AquaSynthDaemonService : IDisposable public Dictionary Controls { get; } + public string OpenedAtUtc { get; } + + public ulong NextSequenceValue => sequence; + public void ApplyControls(IReadOnlyDictionary controls) { foreach (var pair in controls) diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 3fc24bf..a907504 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -284,4 +284,5 @@ {"timestamp":"2026-06-15T02:22:00+01:00","topic":"cinematic-daemonic-swarm-pipeline","claim":"The daemonic swarm documentation now includes the creative cinematic pipeline: Ghostlight coordinates scene state and next beats, Weksa lowers writing/agent state into utterance and delivery intent, AquaSynth or external providers produce voice, Vili wraps Kimodo for motion generation, and Brokkr mirrors/synchronizes Unity and Blender through CultMesh without stealing editor truth.","evidence":"docs/gamecult-daemonic-swarm.md; docs/daemonic-swarm-prior-work.md; Brokkr README/docs/provider-contract.md; Vili README/docs/verse-service-contract.md; Ghostlight prompts/sandboxed-coordinator-turn.md; E:/Projects/weksa/docs/verse-service-contract.md"} {"timestamp":"2026-06-15T03:04:00+01:00","topic":"aquasynth-daemon-live-session-control","claim":"AquaSynth live daemon control now belongs to AquaSynthDaemonService: instrument.open compiles once and keeps a native streaming patch instance, instrument.control updates session controls, and instrument.block processes fixed blocks through the same live session. AquaSynthCultNetDaemon only observes typed command documents and publishes typed receipts; it is not a second synth/session owner.","evidence":"src/AquaSynth.Faust/AquaSynthDaemonService.cs; src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs; tools/AquaSynthDaemon/Program.cs; tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs; focused AquaSynthDaemonServiceTests passed 5/5"} {"timestamp":"2026-06-15T03:31:00+01:00","topic":"aquasynth-daemon-live-session-lifecycle","claim":"Live AquaSynth daemon sessions now have an explicit release path. instrument.close is owned by AquaSynthDaemonService, removes and disposes the native streaming patch instance, writes a close receipt, leaves the compiled patch witness reusable, and makes subsequent instrument.block calls for that live session fail with session_not_found.","evidence":"src/AquaSynth.Faust/AquaSynthDaemonService.cs; src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs; tools/AquaSynthDaemon/Program.cs; tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs"} +{"timestamp":"2026-06-15T03:55:00+01:00","topic":"aquasynth-daemon-live-session-state","claim":"Live AquaSynth daemon sessions now publish retained aquasynth.live_instrument_session.v1 state under .aquasynth/live-sessions/{sessionId}.cc. AquaSynthDaemonService rewrites it on open/control/block/close with status, current controls, block size, next sequence, and last command/receipt ids, so clients can inspect current session truth without replaying event receipts.","evidence":"src/AquaSynth.Faust/AquaSynthDaemonService.cs; src/AquaSynth.Faust/AquaSynthCultNetDaemon.cs; tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs; docs/verse-service-contract.md; focused AquaSynthDaemonServiceTests passed 5/5"} {"timestamp":"2026-06-15T02:35:00+01:00","topic":"greater-daemon-composition-rule","claim":"The daemonic swarm docs now state the greater-daemon composition rule: smaller daemon capabilities compose upward through typed command/receipt surfaces, while larger workflow daemons own orchestration and aggregate receipts without absorbing lower-daemon truth, provenance, rejections, or failures.","evidence":"docs/gamecult-daemonic-swarm.md; docs/daemonic-swarm-prior-work.md"} diff --git a/state/map.yaml b/state/map.yaml index 557d155..e0e9223 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -124,7 +124,7 @@ boundaries: - Faust source generation in AquaSynth.Core - target-code generation, compile-product contracts, dynamic patch compilation policy, cache identity, generated artifact manifests, and Faust toolchain selection in AquaSynth.Faust - live patch compilation, native Faust factory lifetime, and offline sample-buffer render proof in AquaSynth.Faust - - AquaSynth daemon command ownership for patch.compile, instrument.sample, bounded instrument.stream automation, and live instrument.open/instrument.control/instrument.block/instrument.close sessions: compile/session/render/stream/live/operator typed receipts, requested-duration sample renders, persistent native streaming patch instances for live controls, explicit live-session release without deleting compiled patch witnesses, CultMesh-compatible audio/control stream descriptors, per-block .f32 packet artifacts, JSON-lines local transport through tools/AquaSynthDaemon, and a local CultNet host/database that watches typed command documents and publishes typed receipt/provider documents without becoming a second render/session authority + - AquaSynth daemon command ownership for patch.compile, instrument.sample, bounded instrument.stream automation, and live instrument.open/instrument.control/instrument.block/instrument.close sessions: compile/session/render/stream/live/live-session-state/operator typed receipts and witnesses, requested-duration sample renders, persistent native streaming patch instances for live controls, retained aquasynth.live_instrument_session.v1 state for open/closed status/current controls/next sequence, explicit live-session release without deleting compiled patch witnesses, CultMesh-compatible audio/control stream descriptors, per-block .f32 packet artifacts, JSON-lines local transport through tools/AquaSynthDaemon, and a local CultNet host/database that watches typed command documents and publishes typed receipt/provider documents without becoming a second render/session authority - IPA-to-phonetic-feature planning, articulatory gesture planning, and human/alien vocal-tract roadmap design - phonetic intent, articulatory plan, morphology capability, and constraint-report contracts for vocal tract synthesis - packed-buffer native backprop/Adam as the shared AquaSynth gradient-descent primitive diff --git a/state/spine.yaml b/state/spine.yaml index 60a995e..1799608 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -57,7 +57,7 @@ current_mechanism: live_invariants: - AquaSynth owns its patch graph; external synths are references, not runtime dependencies. - - AquaSynthDaemonService owns compile/render/stream/live-session execution for patch.compile, instrument.sample, bounded instrument.stream automation, instrument.open, instrument.control, instrument.block, and instrument.close. It delegates DSP to AquaSynth.Faust, writes compile/session/render/stream/live/operator .cc witnesses under the configured .aquasynth store, writes .f32/.wav sample artifacts, keeps native streaming patch instances for live sessions, releases live sessions without deleting reusable compiled patch witnesses, and emits CultMesh-compatible audio/control stream descriptors plus per-block packet receipts. AquaSynthCultNetDaemon owns the local CultNet command-observation boundary: typed command documents enter the CultNet database, the daemon serializes execution through AquaSynthDaemonService, and typed receipt/provider documents come back out. JSON-lines modes are only operator/xeno edges; they must not become second audio/session authorities. + - AquaSynthDaemonService owns compile/render/stream/live-session execution for patch.compile, instrument.sample, bounded instrument.stream automation, instrument.open, instrument.control, instrument.block, and instrument.close. It delegates DSP to AquaSynth.Faust, writes compile/session/render/stream/live/live-session-state/operator .cc witnesses under the configured .aquasynth store, writes .f32/.wav sample artifacts, keeps native streaming patch instances for live sessions, rewrites retained aquasynth.live_instrument_session.v1 state for open/closed status, current controls, next sequence, and last command/receipt ids, releases live sessions without deleting reusable compiled patch witnesses, and emits CultMesh-compatible audio/control stream descriptors plus per-block packet receipts. AquaSynthCultNetDaemon owns the local CultNet command-observation boundary: typed command documents enter the CultNet database, the daemon serializes execution through AquaSynthDaemonService, and typed receipt/provider documents come back out. JSON-lines modes are only operator/xeno edges; they must not become second audio/session authorities. - Patch scripts lower into explicit graph records before Faust emission. - Song-snippet parity is scene-audio pressure, not redistributed fixture data. The worker may derive local tempo/register/scale hints from private clips, but agents must express live rhythm and pitch motion through owned AquaSynth controls until native sequencing sugar has an explicit DSL/lowering owner. - Operator graphs own FM topology: operators, modulation edges, carriers, and feedback. Do not hide this authority inside Voice fields. diff --git a/tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs b/tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs index 90a8e30..7ccb69e 100644 --- a/tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs +++ b/tests/AquaSynth.Dsl.Tests/AquaSynthDaemonServiceTests.cs @@ -1,5 +1,6 @@ using AquaSynth.Faust; using GameCult.Caching; +using GameCult.Caching.MessagePack; namespace AquaSynth.Dsl.Tests; @@ -154,6 +155,13 @@ public sealed class AquaSynthDaemonServiceTests Assert.True(File.Exists(UriToPath(loud.Float32Uri))); Assert.Equal(128 * sizeof(float), new FileInfo(UriToPath(loud.Float32Uri)).Length); Assert.True(File.Exists(Path.Combine(storeRoot, "live", $"{close.ReceiptId}.cc"))); + var liveState = await ReadLiveSessionStateAsync(storeRoot, open.SessionId); + Assert.Equal(open.SessionId, liveState.SessionId); + Assert.Equal("closed", liveState.Status); + Assert.Equal((ulong)2, liveState.NextSequence); + Assert.Equal("live-close", liveState.LastCommandId); + Assert.Equal(close.ReceiptId, liveState.LastReceiptId); + Assert.Equal(.3f, liveState.Controls["macro/gain"]); } [Fact] @@ -198,4 +206,12 @@ public sealed class AquaSynthDaemonServiceTests } private static string UriToPath(string uri) => new Uri(uri).LocalPath; + + private static async Task ReadLiveSessionStateAsync(string storeRoot, string sessionId) + { + using var cache = await CultCacheMessagePack.OpenAsync( + Path.Combine(storeRoot, "live-sessions", $"{sessionId}.cc"), + new CultCacheOpenOptions { PullOnOpen = true }).ConfigureAwait(false); + return Assert.Single(cache.GetAll()); + } }