refactor(runtime): own communication and social state

Construct chat history, negotiated channels, friends, squelch, and reply targets in one presentation-independent Runtime owner. Make live routing, retained UI, devtools, and current-runtime projections borrow the exact instances, preserve reconnect reset semantics, and publish failure-isolated reentrant-safe chat commits.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 07:50:16 +02:00
parent e321410770
commit c9d25ade50
19 changed files with 684 additions and 117 deletions

View file

@ -0,0 +1,84 @@
namespace AcDream.Core.Chat;
/// <summary>
/// Presentation-independent reply/retell targets derived from the committed
/// chat transcript.
/// </summary>
/// <remarks>
/// Incoming tells carry a non-zero sender GUID and update the reply target.
/// Local outgoing tell echoes carry a zero sender GUID and place the target
/// name in <see cref="ChatEntry.Sender"/>. This is the existing retail
/// <c>@reply</c>/<c>@retell</c> behavior formerly owned by <c>ChatVM</c>.
/// </remarks>
public sealed class ChatCommandTargetState : IDisposable
{
private readonly ChatLog _chat;
private readonly object _gate = new();
private string? _lastIncomingTellSender;
private string? _lastOutgoingTellTarget;
private bool _disposed;
public ChatCommandTargetState(ChatLog chat)
{
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
_chat.EntryAppended += OnEntryAppended;
}
public string? LastIncomingTellSender
{
get
{
lock (_gate)
return _lastIncomingTellSender;
}
}
public string? LastOutgoingTellTarget
{
get
{
lock (_gate)
return _lastOutgoingTellTarget;
}
}
/// <summary>
/// Forget character-scoped command targets without clearing visible
/// transcript history.
/// </summary>
public void ResetSession()
{
lock (_gate)
{
_lastIncomingTellSender = null;
_lastOutgoingTellTarget = null;
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed)
return;
_disposed = true;
_chat.EntryAppended -= OnEntryAppended;
}
}
private void OnEntryAppended(ChatEntry entry)
{
if (entry.Kind != ChatKind.Tell || string.IsNullOrEmpty(entry.Sender))
return;
lock (_gate)
{
if (_disposed)
return;
if (entry.SenderGuid != 0u)
_lastIncomingTellSender = entry.Sender;
else
_lastOutgoingTellTarget = entry.Sender;
}
}
}