Files
VolcanicAgenticCore/extensions/Volcanic.AgenticCore.Extensions.A2A/PendingFile.cs
2026-10-08 12:36:29 +00:00

70 lines
2.8 KiB
C#

using System.Collections.Concurrent;
using System.Text.Json;
namespace Volcanic.AgenticCore.Extensions.A2A;
public static class WatchSources
{
public const string Push = "push";
public const string Subscribe = "subscribe";
public const string Poll = "poll";
}
/// <param name="Target">The host's delivery target (Telegram: the chat id).</param>
/// <param name="PushToken">Our secret for this watch's push webhook; null when not using push.</param>
/// <param name="Undelivered">A finished result that couldn't be delivered yet (sent on the target's next session start).</param>
public sealed record WatchRecord(
string WatchId, string Target, string Agent, string TaskId, string ContextId, string? PushToken,
DateTimeOffset StartedAt, string Source, string? Undelivered = null);
/// <summary><c>&lt;workspace&gt;/.volcanic/a2a/pending.json</c>: the watches of a workspace, so they survive restarts.</summary>
public static class PendingFile
{
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web) { WriteIndented = true };
private static readonly ConcurrentDictionary<string, object> Locks = new(StringComparer.Ordinal);
public static string PathFor(string workspace) => Path.Combine(workspace, ".volcanic", "a2a", "pending.json");
public static IReadOnlyList<WatchRecord> Read(string workspace)
{
lock (LockFor(workspace)) return ReadUnlocked(workspace);
}
public static void Upsert(string workspace, WatchRecord record) => Change(workspace, list =>
{
list.RemoveAll(r => r.WatchId == record.WatchId);
list.Add(record);
});
public static void Remove(string workspace, string watchId) => Change(workspace, list => list.RemoveAll(r => r.WatchId == watchId));
private static void Change(string workspace, Action<List<WatchRecord>> change)
{
lock (LockFor(workspace))
{
var list = ReadUnlocked(workspace).ToList();
change(list);
var path = PathFor(workspace);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
var temp = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
File.WriteAllText(temp, JsonSerializer.Serialize(list, Json));
File.Move(temp, path, overwrite: true);
}
}
private static List<WatchRecord> ReadUnlocked(string workspace)
{
var path = PathFor(workspace);
try
{
return File.Exists(path) ? JsonSerializer.Deserialize<List<WatchRecord>>(File.ReadAllText(path), Json) ?? [] : [];
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
{
return [];
}
}
private static object LockFor(string workspace) => Locks.GetOrAdd(Path.GetFullPath(workspace), _ => new object());
}