docs: Dings patch identity authority map #40
1 changed files with 340 additions and 0 deletions
340
docs/dings-patch-identity-authority-map.md
Normal file
340
docs/dings-patch-identity-authority-map.md
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
# Dings patch identity: authority map
|
||||
|
||||
Serves follow-up `aqua-body:follow_up:aquasynth-dings-authority-map`. Claims are from
|
||||
source at `c0fe4ee` (origin/main). Nothing was built or run; the one place behaviour is
|
||||
inferred rather than read is marked. Paths are repo-relative; `Host` =
|
||||
`tools/AquaSynthDingsHost`, `Mcp` = `tools/AquaSynthDingsMcp`.
|
||||
|
||||
Companion to `docs/research/dings-playback-and-ogg-export.md`, which describes the
|
||||
playback path and an export sketch. This note asks a narrower question: in the Dings
|
||||
path, what decides which sound "this patch" is, and how many places decide it.
|
||||
|
||||
## What I counted
|
||||
|
||||
The follow-up says four owners. The source supports four places that *decide* a
|
||||
voice's identity, plus one place that names the same thing a second time without
|
||||
deciding, and one generated mirror:
|
||||
|
||||
| # | Owner | Decides | Section |
|
||||
|---|---|---|---|
|
||||
| 1 | `DingCatalog` | the instrument id and the script that defines its sound | A |
|
||||
| 2 | Host and daemon derivation | the daemon `patchId`, `faustName`, `revision`, session id and store file names | B |
|
||||
| 3 | MCP `AgentInstrumentRegistry` | which instrument an agent session gets | C |
|
||||
| 4 | `tools/dings-voices` | which voice a sender (agent localpart) gets, in a separate synth | D |
|
||||
| - | Earcon Workshop `IDENTITIES` | reuses catalog instrument ids with its own parameters | E |
|
||||
| - | `tools/aqua-feel/lowered-dings` | checked-in copy of #1, guarded by a test | E |
|
||||
|
||||
The follow-up does not list its four, so this grouping is my reading. Someone who
|
||||
counted differently (for example splitting the daemon's session id from the store file
|
||||
name, or counting Earcon Workshop as a fifth) would get a different number from the same
|
||||
code. The grouping above follows who writes the key.
|
||||
|
||||
## A. `DingCatalog` instrument id and script
|
||||
|
||||
- Where: `src/AquaSynth.Dings/DingCatalog.cs:22-28` (four instruments), built by the
|
||||
private `Instrument(...)` at `:48-50`, which writes the `Script` string (an AquaSynth patch script, `param path=...; voice wave=...`, that `FaustEmitter.EmitScript` lowers to Faust; not Faust source) and
|
||||
fixes `RootFrequency = 261.6256f` for all four.
|
||||
- Keys on: the instrument `Id` string (`warm-bell`, `glass-chime`, `soft-triangle`,
|
||||
`muted-pluck`), case-insensitive dictionary (`:28`). Events are a separate keyed
|
||||
dictionary (`:9-20`, nine ids) and are not voice identity.
|
||||
- Lifetime: compiled in. Changing an entry means a new build of every consumer.
|
||||
- Writer: whoever edits the file. `ValidateCuration` (`:31-46`) is the only gate.
|
||||
- Readers: `Host/DingsPlaybackHost.cs:66-67`, `Mcp/DingService.cs:9-10`,
|
||||
`Mcp/AgentInstrumentRegistry.cs:11`, `Mcp/DingTools.cs:28,31,49`,
|
||||
`tools/AquaLower/Program.cs:17`, and the tests `DingCatalogTests`.
|
||||
- Content hash: none. The id is a label; the script is the content. Nothing here says
|
||||
"this script, at this version".
|
||||
|
||||
## B. Host and daemon derivation of the patch
|
||||
|
||||
The Host does not pass the catalog id through. It builds the daemon's identity fields
|
||||
itself, and the daemon builds more keys from them.
|
||||
|
||||
- `Host/AquaDaemonClient.cs:20` per note: `patchId = "dings." + instrument.Id`,
|
||||
`faustName = "dings_" + instrument.Id.Replace('-', '_')`, `revision = 1` (a literal),
|
||||
`durationSeconds = 1.5f`, `controls["/ding/frequency"]`.
|
||||
- `src/AquaSynth.Faust/NativeFaustRuntime.cs:596-599`: the compile key is SHA-256 of
|
||||
`"{revision}\n{faustName}\n{script}"`. `patchId` is not in it. So two ids with the same
|
||||
name and script share a compile key; one id with an edited script gets a new key even
|
||||
though `revision` stays 1.
|
||||
- `src/AquaSynth.Faust/AquaSynthDaemonService.cs:1136`: session id =
|
||||
`StoreFileName.From(patchId) + "-" + compileKey[..12]`. A recompile with the same
|
||||
session id disposes the old session (`:366-376`).
|
||||
- `src/AquaSynth.Core/StoreFileName.cs:26-54`: the mapping from any id to a file name
|
||||
(ASCII letters, digits, `-`, `_`, `.` pass unchanged unless the id has an uppercase letter, a device stem, a trailing dot, is over 100 characters or already ends in a hash shape; anything else gets a 16-hex SHA-256 suffix, `StoreFileName.cs:37,59-67`).
|
||||
`dings.warm-bell` is a fixed point (pinned by `StoreFileNameTests`).
|
||||
- Keyed by patch: the session document. `WriteReceiptAsync("sessions", sessionId, session)`
|
||||
(`:407`) runs on every compile, and `SampleAsync` compiles on every note
|
||||
(`:622-635`), so the store holds `sessions/<StoreFileName(sessionId)>.cc` for each
|
||||
distinct `(patchId, compile key)`, rewritten on each note. This is the one store
|
||||
artifact addressable by instrument. `NativeFaustRuntime.cs:516,628-636` would also write
|
||||
`<StoreFileName(patchId)>.dsp`, but only when `options.DspSourceDirectory` is set; the
|
||||
daemon builds its native options from sample rate and render limits only
|
||||
(`AquaSynthDaemonService.cs:330-334`) and nothing else in `src` or `tools` sets it, so
|
||||
for the Dings host it is off. (Read from source, not observed.)
|
||||
- Not keyed by patch: render output and the render and compile receipts. Samples are
|
||||
`<store>/samples/<StoreFileName(commandId + "-render")>.wav` (`:664-667`), where
|
||||
`commandId` is a fresh `ding-<guid>` per note (`AquaDaemonClient.cs:19`). Receipts go
|
||||
through `WriteReceiptAsync` (`:922-937`) under command-derived ids, with the patch id
|
||||
only in the body. The store therefore keeps one WAV per note played, none addressable
|
||||
by instrument. Whether the store ever gets cleaned I did not find.
|
||||
- Faust name, three forms: the Host sends `dings_<id with - as _>`; the daemon applies
|
||||
`SafeFaustName` (`NativeFaustRuntime.cs:602-607`, called at
|
||||
`AquaSynthDaemonService.cs:911` and `NativeFaustRuntime.cs:513`) so the manifest and
|
||||
emitted source carry the rewritten name, while the compile key (`:596-599`) hashes the
|
||||
raw `identity.FaustName`. Two raw names that sanitise alike compile alike but key
|
||||
differently.
|
||||
- The `dings_<id>` rule is written three times: `AquaDaemonClient.cs:20` and
|
||||
`tools/AquaLower/Program.cs:19`, and
|
||||
`DingCatalogTests.cs:45` (`LoweredDingsFedToTheFeelMeterMatchTheCatalog` builds the
|
||||
name with the same expression). Nothing ties the three together except the test.
|
||||
- Writer: the Host (fields) and the daemon (session id, file names). Readers: the
|
||||
daemon's compile/render path and receipts; no Dings code reads the patch id back.
|
||||
- `Host/AquaDaemonClient.cs:23-28` returns the WAV path from the receipt. The Host
|
||||
never asks the daemon which patch rendered it.
|
||||
|
||||
## C. MCP `AgentInstrumentRegistry`
|
||||
|
||||
- Where: `Mcp/AgentInstrumentRegistry.cs:10` (dictionary), `:13-17` `GetOrAssign`,
|
||||
`:19-29` `Claim`, `:31-36` `AssignDeterministically`.
|
||||
- Keys on: `agentSessionId`, which is `server.SessionId ?? "stdio:" + ProcessId`
|
||||
(`Mcp/DingTools.cs:45`; that `SessionId` is null under stdio is inferred from the `??` fallback, not read from the SDK). That is the MCP transport session, not the agent. In HTTP
|
||||
mode (`Mcp/Program.cs:34-54`, `Stateless = false`) it lives as long as that MCP
|
||||
session; in stdio mode it is the bridge process id.
|
||||
- Value: an instrument id string. Unclaimed sessions get
|
||||
`SHA256(sessionId)[0..4] % sortedInstrumentIds.Length`. `Claim` overwrites silently,
|
||||
and there is no uniqueness check, so two sessions can hold the same instrument.
|
||||
- Lifetime: in-process, a `ConcurrentDictionary`. Lost on bridge restart. A restarted
|
||||
or reconnected session gets a different session id, hence generally a different
|
||||
hashed instrument unless it claims again. (Inferred from the keying; I did not
|
||||
observe a reconnect.)
|
||||
- Writer: the agent, through `claim_instrument`, or the hash. Reader: `play_ding`
|
||||
(`DingTools.cs:16`) and `get_agent_instrument` (`:19`).
|
||||
- Also decides: `DingService.PlayAsync` (`Mcp/DingService.cs:9-11`) re-validates event,
|
||||
instrument and gain, and the Host validates the same three again
|
||||
(`DingsPlaybackHost.cs:66-68`). Two gates on one decision; they agree today because
|
||||
both read `DingCatalog`.
|
||||
|
||||
## D. `tools/dings-voices` sender voices
|
||||
|
||||
- Where: `tools/dings-voices/src/voices.mjs:8` (`SENDERS`, eight localparts), `:51-73`
|
||||
(`VOICES`, one `hit` function each), `:13-18` (`RHYTHM`, four motifs).
|
||||
- Keys on: sender localpart x motif (`mention`, `dm`, `message`, `invite`), emitted as
|
||||
`senders/<sender>/<motif>.ogg` and `manifest.json`
|
||||
(`finish.py:62`, shape `{neutral, senders:{localpart:{motif:path}}}`).
|
||||
- It does not use `DingCatalog`, `AquaSynthDaemon` or Faust. It is a JS synthesizer that
|
||||
imports only the sample rate from `earcon-workshop/web/earcon.js` (`voices.mjs:5`).
|
||||
Its vocabulary (4 motifs, 8 senders) shares no ids with Aqua's (9 events, 4
|
||||
instruments).
|
||||
- One link to the catalog family: the neutral fallback is rendered through the Earcon
|
||||
Workshop `warm-bell` identity (`build.mjs:10-14`), with an event mapping of
|
||||
`mention=warning, dm=waiting, message=message, invite=done` (`:12`) that the file
|
||||
itself calls provisional.
|
||||
- Lifetime: a build artifact. The files are not committed (README, "Not here"); the
|
||||
consumer, Ratatoskr's `dings.ts`, is outside this repo and I did not read it.
|
||||
- Writer: a person editing `voices.mjs`; `NIBU_PROPOSALS` (`:81-92`) shows the selection
|
||||
of the default voice is a one-line assignment (`VOICES.nibu = ...`, `:92`).
|
||||
- This is the only place a *sender* is identity. Everything else in the Dings path
|
||||
keys on an MCP session.
|
||||
|
||||
## E. Places that echo the identity without deciding it
|
||||
|
||||
- Earcon Workshop `IDENTITIES`, `tools/earcon-workshop/web/earcon.js:22-27`. Same four
|
||||
ids; its comment (`:21`) says values "start from the Dings catalog instruments and
|
||||
were not tuned by ear". The values have drifted from the catalog:
|
||||
|
||||
| id | catalog root / decay (`DingCatalog.cs:24-27`) | workshop `rootHz` / `decay` |
|
||||
|---|---|---|
|
||||
| warm-bell | 261.6256 / 1.15 | 261.6256 / 0.55 |
|
||||
| glass-chime | 261.6256 / 0.85 | 329.6276 / 0.45 |
|
||||
| muted-pluck | 261.6256 / 0.34 | 293.6648 / 0.25 |
|
||||
|
||||
Decay is read with different units in the two voice models, so only the root column
|
||||
is a clean disagreement. A "warm-bell" in the Ratatoskr neutral fallback is therefore
|
||||
not the "warm-bell" an MCP agent hears.
|
||||
- `tools/aqua-feel/lowered-dings/` (checked in): `.dsp` per instrument and `dings.json`,
|
||||
produced by `AquaLower --dings`. `DingCatalogTests` fails when it falls out of date
|
||||
(the test comment at the top of `LoweredDingsFedToTheFeelMeterMatchTheCatalog` names
|
||||
the regeneration command). A mirror guarded by a test; not an independent owner.
|
||||
`tools/aqua-feel/shelf.mjs:64` and `catalog.js:22-24` read it, and
|
||||
`shelf.mjs:64` defines `warm-bell` as the shelf default (`DEF_INS`, used at
|
||||
`:71,:75,:76`), a fifth voice default beyond the registry's hash.
|
||||
|
||||
## Where owners can disagree
|
||||
|
||||
1. **Name versus content.** A is a name plus a script. B keys sessions on the content
|
||||
(compile key) and labels receipts with the name. Editing a script in place keeps
|
||||
`patchId = dings.warm-bell` and `revision = 1`, so receipts from before and after the
|
||||
edit carry the same patch id and different session ids. Nothing in a receipt says
|
||||
which catalog version produced it.
|
||||
2. **Session versus agent.** C assigns by MCP session; the stated intent in
|
||||
`Mcp/README.md:3-4` is "which agent spoke". After a reconnect the same agent can
|
||||
change timbre. D, meanwhile, keys on the agent address localpart. No code maps a
|
||||
session id to a localpart, so the two cannot be joined today.
|
||||
3. **Four timbres versus eight senders.** C picks among 4 catalog instruments by hash;
|
||||
D has 8 hand-made voices. An agent can have one voice in `play_ding` and a different
|
||||
one in its Ratatoskr notification, and nothing records either choice.
|
||||
4. **Same id, different sound.** `warm-bell` is a catalog instrument (A), a workshop
|
||||
identity (E) and the neutral fallback voice (D). Three definitions, parameter sets
|
||||
that have drifted (table above).
|
||||
5. **Derivation written three times.** `dings_<id>` in `AquaDaemonClient.cs:20`,
|
||||
`AquaLower/Program.cs:19`, and the test; and the `dings.<id>` patch id appears in
|
||||
`StoreFileNameTests` as a literal. A rename rule changed in one place breaks
|
||||
store-file naming in another without a compile error.
|
||||
6. **Double validation.** `DingService.cs:9-11` and `DingsPlaybackHost.cs:66-68` each
|
||||
decide whether an id is valid. Safe while both read one dictionary.
|
||||
|
||||
## The Host pipe protocol, as read from source
|
||||
|
||||
This is the "private pipe": the only channel into the audio host. MCP is a client of it;
|
||||
it is not itself the pipe.
|
||||
|
||||
**Listener.** `AquaSynthDingsHost.exe`. `Host/Program.cs:3-4` takes the named mutex
|
||||
`Local\GameCult.AquaSynthDings.Playback.v1` and exits if another instance holds it, then
|
||||
runs `DingsPlaybackHost.RunAsync` (`:7`). The accept loop is
|
||||
`DingsPlaybackHost.cs:12-20`: one `NamedPipeServerStream` per accepted client, name
|
||||
`GameCult.AquaSynthDings.Playback.v1` (`src/AquaSynth.Dings/DingsProtocol.cs:8`),
|
||||
`PipeDirection.InOut`, byte mode, `MaxAllowedServerInstances`, `Asynchronous |
|
||||
CurrentUserOnly` (`:16`). Each connection is handled fire-and-forget (`:18`). The host
|
||||
uses NAudio WASAPI (`Host/DingAudioPlayer.cs`) and is Windows-only in practice.
|
||||
|
||||
**Clients.** `Mcp/DingsHostClient.cs`, used by the MCP bridge (both stdio and
|
||||
`--http`) and by the bridge's direct flags `--play`, `--get-volume`, `--set-volume`,
|
||||
`--mute` (`Mcp/Program.cs:9-32`). Nothing else in the repo opens the pipe.
|
||||
|
||||
**Framing** (identical in both directions):
|
||||
`[int32 length][MessagePack body]`. The length is `BitConverter.GetBytes(int)`, so
|
||||
native byte order (little-endian on every platform this builds for, but the code does not
|
||||
fix it). Valid length is 1 to 1,048,576 bytes; outside that the reader throws
|
||||
(`DingsPlaybackHost.cs:86-87`, `DingsHostClient.cs:42`). Body is
|
||||
`MessagePackSerializer` of the keyed records below. One request, one response, then the
|
||||
server disposes the pipe (`DingsPlaybackHost.cs:24-36`). No streaming, no pipelining, no
|
||||
request id.
|
||||
|
||||
**Request** `DingsCommand` (`DingsProtocol.cs:13-19`), keyed:
|
||||
|
||||
| key | field | note |
|
||||
|---|---|---|
|
||||
| 0 | `Version` | must equal `DingsProtocol.Version` = 1, else status `incompatible` (`DingsPlaybackHost.cs:41`) |
|
||||
| 1 | `Kind` | `Ping, Play, StopAll, GetVolume, SetVolume, SetMuted` (`:11`) |
|
||||
| 2 | `EventId` | catalog event id, `Play` only |
|
||||
| 3 | `InstrumentId` | catalog instrument id, `Play` only |
|
||||
| 4 | `Value` | `Play`: gain 0..1; `SetVolume`: volume 0..1; `SetMuted`: `>= .5` means muted |
|
||||
|
||||
**Response** `DingsResponse` (`DingsProtocol.cs:21-31`): `Version`, `Succeeded`,
|
||||
`Status`, `Message`, `PlaybackId`, `Volume`, `Muted`, `HostProcessId`, `SessionName`
|
||||
(default `"AquaSynth Dings"`). Every response carries current volume and mute, built at
|
||||
`DingsPlaybackHost.cs:79-80`.
|
||||
|
||||
**Per kind** (`DingsPlaybackHost.cs:39-62`): `Ping` and `GetVolume` are the same branch
|
||||
(status `ready`). `SetVolume` rejects non-finite or out-of-range values
|
||||
(`rejected`) and otherwise sets `audio.Volume` (`volume-set`). `SetMuted` -> `muted` /
|
||||
`unmuted`. `StopAll` -> `stopped`. `Play` (`:64-77`) validates event, instrument and
|
||||
gain, renders each note through the daemon, queues each WAV with its delay, and answers
|
||||
`playing` with a fresh `ding-<guid>` `PlaybackId`. The answer comes after rendering and
|
||||
queueing, not after sound. `PlaybackId` is not stored and nothing takes it as input.
|
||||
Any exception becomes `Succeeded = false, Status = "failed", Message = ex.Message`
|
||||
(`:32-35`), provided the pipe is still connected.
|
||||
|
||||
**Client behaviour** (`Mcp/DingsHostClient.cs:15-48`): the client validates the
|
||||
response `Version` (`:46`). Connect timeout 350 ms; on `TimeoutException` or
|
||||
`IOException` it launches the host (`StartHost`, `:50-56`: newest
|
||||
`AquaSynthDingsHost.exe` under `tools/AquaSynthDingsHost/bin`, hidden window, working
|
||||
directory the repo root found by walking up to `AquaSynth.sln`) and retries up to 20
|
||||
times at 500 ms connect with 100 ms gaps, all inside a 15 s overall timeout. A
|
||||
`Succeeded = false` response is turned into `InvalidOperationException(response.Message)`
|
||||
by `DingService.EnsureSuccess` (`DingService.cs:22-26`).
|
||||
|
||||
**MCP side, for completeness.** Tools (`Mcp/DingTools.cs`): `play_ding`,
|
||||
`get_agent_instrument`, `claim_instrument`, `list_ding_events`, `list_instruments`,
|
||||
`stop_dings`, `get_volume`, `set_volume`, `mute_dings`. Transports: stdio by default
|
||||
(`Program.cs:56-65`); `--http [url]` serves Streamable HTTP at `/mcp`, default
|
||||
`http://127.0.0.1:17878`, singleton by mutex `Local\GameCult.AquaSynthDings.McpHttp.v1`
|
||||
(`Program.cs:34-54`). Only `play_ding`, `get_agent_instrument`, `claim_instrument` touch
|
||||
agent identity; the volume and stop tools address the whole host and are
|
||||
machine-global.
|
||||
|
||||
**Not authenticated beyond the OS.** `CurrentUserOnly` limits it to the same Windows
|
||||
user. Any process of that user can send `Play`. There is no per-caller identity on the
|
||||
pipe: `DingsCommand` carries an instrument, not a caller, so the Host cannot know who
|
||||
asked.
|
||||
|
||||
## PROPOSED: one owner (a recommendation for the stewarding decision, not decided)
|
||||
|
||||
**Proposal.** `DingCatalog` (`src/AquaSynth.Dings`) becomes the only place that decides
|
||||
what a voice is, and also the only place that derives the daemon keys from it. Its
|
||||
instrument gains a content revision (a hash of `Script` plus the parameters that matter),
|
||||
and one function in that assembly produces `PatchId`, `FaustName` and the revision for
|
||||
the daemon (`AquaLower` and the test need only the faust-name part of it, not
|
||||
`patchId` or revision). Everything else keys on the catalog id and takes the rest from the catalog.
|
||||
|
||||
**What the others become.**
|
||||
- B (Host/daemon): the Host stops composing `patchId`/`faustName`/`revision`; it calls
|
||||
the catalog function. `AquaLower/Program.cs:19` and `DingCatalogTests.cs:45` call only the faust-name slice of it. The daemon
|
||||
keeps its session and file naming; those are daemon-internal and derive from what it
|
||||
is given.
|
||||
- C (registry): stays in the bridge as a cache of choices, but its assignment function
|
||||
moves beside the catalog (so a future export can call it) and its key becomes an
|
||||
agent identity rather than an MCP session id. This depends on the open question
|
||||
below.
|
||||
- D (`dings-voices`): stays a separate synthesizer for sender notifications, since its
|
||||
voices are JS functions with no Faust form. Its link to the owner is by reference:
|
||||
the neutral fallback names a catalog instrument id instead of a second `warm-bell`.
|
||||
Whether sender voices should ever become catalog instruments is a product fork, not
|
||||
part of this change.
|
||||
- E: Earcon Workshop `IDENTITIES` either read their shared values from a generated
|
||||
file (as `lowered-dings` already is) or drop the shared ids; the drifted values in
|
||||
the table above are resolved by whoever owns the workshop's tuning.
|
||||
|
||||
**Why this owner.** A already has the most readers (the list in section A), is the only
|
||||
definition of the script, and is the only one the host cannot run without. C, D and E
|
||||
are all consumers or approximations of it by name.
|
||||
|
||||
**Migration order** (each step independent and reversible):
|
||||
1. Add the catalog function; switch `AquaDaemonClient.cs:20` and `AquaLower` onto it;
|
||||
delete the test's private copy (see the oracle risk below). Pure refactor while
|
||||
`revision` stays the literal 1: the compile key, and so the session id, stay equal
|
||||
for unchanged scripts, which is checkable by comparing receipts before and after.
|
||||
2. Decide the agent-identity question (below); change the registry key. Wire-visible:
|
||||
existing sessions lose their hashed assignment.
|
||||
3. Content revision: DEFERRED, not scheduled here. Replacing the literal `revision = 1`
|
||||
with a catalog hash changes every compile key and session id (and the
|
||||
`sessions/<...>.cc` file names), so it breaks step 1's equality check by design. It
|
||||
needs its own decision on what feeds the hash and whether old session files are
|
||||
left or removed.
|
||||
4. Remove the duplicate gates in `DingService.cs:9-11` or have the Host's gate be the
|
||||
single one (the bridge then only forwards). Keep whichever side the tests
|
||||
(`DingsBridgeTests`) need for fail-fast, but one of them must be the owner.
|
||||
5. Point the neutral fallback and Earcon Workshop at the catalog.
|
||||
|
||||
**Risks.**
|
||||
- Step 2 changes what agents hear on first use after deploy; nobody has listened to a
|
||||
before/after.
|
||||
- The deferred content revision changes session ids and session file names, visible in
|
||||
receipts but not read by anything I found.
|
||||
- Oracle loss in step 1: deleting the test's private `"dings_" + id` copy makes
|
||||
`LoweredDingsFedToTheFeelMeterMatchTheCatalog` compare the shared function against
|
||||
itself. The test should keep an independent expected value (a literal per instrument
|
||||
or the checked-in `.dsp` header) so a wrong rule still fails.
|
||||
- Collapsing 8 senders onto 4 instruments would lose distinctness; the proposal does not
|
||||
do that, and I am recommending against it without having listened to either set.
|
||||
- Moving derivation to the catalog assembly makes `AquaSynth.Dings` the home of
|
||||
daemon-protocol knowledge; if the daemon is expected to run without that assembly,
|
||||
this is the wrong direction. I did not check whether anything else wants that.
|
||||
|
||||
**Confidence.** Moderate on the owner choice; it follows reader counts and the write
|
||||
path, not any prior art on identity registries. Lower on step 2, because it depends on
|
||||
what the MCP bridge can learn about its caller.
|
||||
|
||||
## Could not verify
|
||||
|
||||
- That "four owners" in the follow-up means these four. The count here is mine.
|
||||
- Whether an MCP client can tell the bridge which agent it is. The only identity read in
|
||||
source is `server.SessionId` (`DingTools.cs:45`); I did not inspect the MCP SDK for
|
||||
client-info fields, or any harness config.
|
||||
- What Ratatoskr's `dings.ts` actually reads and whether it shares any naming with
|
||||
`DingCatalog`; it is not in this repo.
|
||||
- Behaviour at runtime: reconnect session ids, store growth under `.aquasynth/dings-host`,
|
||||
and whether the Host's `Ping`/`GetVolume` have different callers in practice. The
|
||||
pipe protocol above is from reading, not from a captured exchange. Nothing was built,
|
||||
run, or listened to for this note.
|
||||
Loading…
Reference in a new issue