Publish AquaSynth live session state

This commit is contained in:
Meta Vens 2026-06-15 03:33:03 +01:00
commit a87e5aa4e0
9 changed files with 93 additions and 11 deletions

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -437,6 +437,7 @@ public static class AquaSynthCultNetDocuments
.Register(CultNetDocumentBinding.ForDocument<AquaSynthInstrumentBlockCommand>())
.Register(CultNetDocumentBinding.ForDocument<AquaSynthInstrumentCloseCommand>())
.Register(CultNetDocumentBinding.ForDocument<AquaSynthCompiledInstrumentSession>())
.Register(CultNetDocumentBinding.ForDocument<AquaSynthLiveInstrumentSessionState>())
.Register(CultNetDocumentBinding.ForDocument<AquaSynthPatchCompileReceipt>())
.Register(CultNetDocumentBinding.ForDocument<AquaSynthRenderSampleReceipt>())
.Register(CultNetDocumentBinding.ForDocument<AquaSynthAutomationStreamReceipt>())

View file

@ -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<string, float> 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<string, float>(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<string, float> Controls { get; }
public string OpenedAtUtc { get; }
public ulong NextSequenceValue => sequence;
public void ApplyControls(IReadOnlyDictionary<string, float> controls)
{
foreach (var pair in controls)

View file

@ -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"}

View file

@ -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

View file

@ -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.

View file

@ -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<AquaSynthLiveInstrumentSessionState> 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<AquaSynthLiveInstrumentSessionState>());
}
}