Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<!-- Version can be overridden from the command line: -p:Version=0.3.1
AssemblyVersion and FileVersion are derived automatically by the SDK
(prerelease suffixes like -beta001 are stripped for assembly versions). -->
<Version>0.13.0</Version>
<Version>0.13.1</Version>
</PropertyGroup>

<!-- NuGet package metadata (shared across all packable projects) -->
Expand Down
16 changes: 11 additions & 5 deletions design/client-rendering-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,14 +356,20 @@ These are deliberately out of scope for v1:
shared-PVC **path references** (`{ mime, path, fileName? }`), reusing the MCP-attachment
scheme — bytes never ride the bus. Producing side: an `attach_image` LLM tool
(`AttachmentReplyTools`) validates a model-named file under the shared attachments dir and
stages it in a session-keyed `ReplyAttachmentBuffer`; `UserMessageHandler` drains the buffer
onto the final reply (the **user-message path** only in v1). Rendering side: Blazor
stages it in a **per-turn-keyed** `ReplyAttachmentBuffer` (`session → turn`, issue #482);
`UserMessageHandler` drains it via the shared `DrainForFinalReply` seam onto the final reply
(the **user-message path** only in v1). The buffer is hardened for the deferred multi-producer
paths below: per-turn keying means concurrent producers for one primary session each drain only
their own stage rather than scooping every staged attachment; staged turns are TTL-swept
(default 30 min) and cleared on turn cancel (`UserMessageHandler` finally blocks) and on
`ClearContext` (session-level `Clear`). Rendering side: Blazor
co-mounts the shared PVC read-only and serves bytes from a minimal `/attachments` endpoint,
rendering images as native `<img>` **outside** the markdown sanitizer (which strips `<img>`
and `data:` URLs); the CLI prints a placeholder line per attachment.
Still deferred: scheduled-task / subagent / A2A producing paths (the buffer generalizes
trivially), SVG→PNG rasterization, chat-platform proxies (none exist yet), and attachments in
conversation-history replay (replies are ephemeral; history stores text).
Still deferred: wiring attachments into the scheduled-task / subagent / A2A producing paths
(the buffer is now keyed and swept to make that safe), SVG→PNG rasterization, chat-platform
proxies (none exist yet), and attachments in conversation-history replay (replies are
ephemeral; history stores text).
- **Platform-native UI emission** — `DiscordEmbed` / `SlackBlockKit` /
`TeamsAdaptiveCard` bits are reserved in the enum but no tooling produces
them. Adding them is opportunistic upgrade work in each proxy (e.g., a
Expand Down
7 changes: 5 additions & 2 deletions src/RockBot.Agent/AttachmentReplyTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,27 @@ namespace RockBot.Agent;
/// tool — so the agent never handles bytes itself (the "Nothing trusts the LLM" rule). The
/// tool validates containment and existence, infers the MIME type from the extension when not
/// given, and stages an <see cref="AgentAttachment"/> in the <see cref="ReplyAttachmentBuffer"/>
/// keyed to this session. The reply-publishing path drains the buffer onto the final reply.
/// keyed to this (session, turn). The reply-publishing path drains the buffer onto the final reply.
/// </summary>
public sealed class AttachmentReplyTools
{
private readonly IAttachmentStorage _storage;
private readonly ReplyAttachmentBuffer _buffer;
private readonly string _sessionId;
private readonly string _turnId;
private readonly ILogger _logger;

public AttachmentReplyTools(
IAttachmentStorage storage,
ReplyAttachmentBuffer buffer,
string sessionId,
string turnId,
ILogger logger)
{
_storage = storage;
_buffer = buffer;
_sessionId = sessionId;
_turnId = turnId;
_logger = logger;

Tools = [AIFunctionFactory.Create(AttachImage)];
Expand Down Expand Up @@ -74,7 +77,7 @@ public string AttachImage(
var leaf = Path.GetFileName(path);
var displayName = string.IsNullOrWhiteSpace(fileName) ? leaf : fileName.Trim();

_buffer.Add(_sessionId, new AgentAttachment
_buffer.Add(_sessionId, _turnId, new AgentAttachment
{
Mime = resolvedMime,
Path = leaf,
Expand Down
5 changes: 5 additions & 0 deletions src/RockBot.Agent/ClearContextHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal sealed class ClearContextHandler(
IConversationMemory conversationMemory,
ISessionTracker sessionTracker,
SessionClientCapabilityStore clientCapabilityStore,
ReplyAttachmentBuffer attachmentBuffer,
SessionOriginStore originStore,
IMessagePublisher publisher,
AgentIdentity agent,
Expand All @@ -33,6 +34,10 @@ public async Task HandleAsync(ClearContextRequest message, MessageHandlerContext
// them from scratch — important if the user returns on a different client.
clientCapabilityStore.Clear(message.SessionId);

// Drop any attachments staged but not yet drained for this session, so a cleared context
// can't replay an old turn's images onto a future reply.
attachmentBuffer.Clear(message.SessionId);

// Drop the cached origin so the next turn re-anchors fresh background work.
originStore.Clear(message.SessionId);

Expand Down
Loading
Loading