Verlorene Updates erkennen statt warten: optimistische Pruefung beim Ersetzen, Konflikte im Audit und im Lagebild (#720)
FileLedger merkt je Agent die gelesene Fassung einer Datei in gemeinsamen Bereichen. write, copy und Downloads (FTP, RocketChat) ersetzen eine Datei nur, wenn sie seit dem Lesen unveraendert ist; eine fremde Datei nicht ungelesen. append bleibt ungeprueft, macht aeltere Lesestaende aber ungueltig. Abgelehnte Schreibvorgaenge: eigener Audit-Status Conflict, Tabelle FileConflicts, Hotspots (>= 3 in 7 Tagen) in der Teamuebersicht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8a7212c240
commit
79a3a7854f
@@ -67,7 +67,8 @@ public sealed record TeamSnapshot(
|
||||
IReadOnlyList<TeamMember> Members,
|
||||
IReadOnlyList<AgentLink> RecentLinks,
|
||||
DateTime AtUtc,
|
||||
IReadOnlyList<TeamUnit>? Units = null);
|
||||
IReadOnlyList<TeamUnit>? Units = null,
|
||||
IReadOnlyList<Core.Files.FileConflictHotspot>? ConflictHotspots = null);
|
||||
|
||||
/// <summary>
|
||||
/// #711: The team overview — who is working, who reached whom, where something is stuck.
|
||||
@@ -105,6 +106,12 @@ public sealed class TeamOverview(
|
||||
/// <summary>Without history, a run counts as stuck after this long.</summary>
|
||||
public static readonly TimeSpan StuckWithoutHistory = TimeSpan.FromMinutes(15);
|
||||
|
||||
/// <summary>#720: How far back conflicts count for a hotspot…</summary>
|
||||
public static readonly TimeSpan HotspotWindow = TimeSpan.FromDays(7);
|
||||
|
||||
/// <summary>…and how many make one. One conflict is an accident; three in a week are a pattern.</summary>
|
||||
public const int HotspotMinimum = 3;
|
||||
|
||||
private readonly TimeProvider _clock = clock ?? TimeProvider.System;
|
||||
|
||||
public async Task<TeamSnapshot> SnapshotAsync(CancellationToken ct)
|
||||
@@ -151,7 +158,30 @@ public sealed class TeamOverview(
|
||||
org.UnitOf(agent.AgentId)?.Id, org.LedBy(agent.AgentId) is not null));
|
||||
}
|
||||
|
||||
return new TeamSnapshot(members, links, now, RollUp(org, members));
|
||||
// #720: files where agents keep getting in each other's way — for a human to decide
|
||||
// whether they should be split, appended to, or become a table.
|
||||
var hotspots = engine?.Files is { } files
|
||||
? (await files.HotspotsAsync(now - HotspotWindow, HotspotMinimum, 10, ct))
|
||||
.Select(h => h with { Path = Readable(h.Path, configured) }).ToList()
|
||||
: [];
|
||||
|
||||
return new TeamSnapshot(members, links, now, RollUp(org, members), hotspots);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #720: A file path as a human finds it — relative to the instance's <c>Agents</c> folder,
|
||||
/// where the shared workspaces live. A path elsewhere stays as it is.
|
||||
/// </summary>
|
||||
public static string Readable(string path, IReadOnlyList<AgentConfig> agents)
|
||||
{
|
||||
var shared = agents.Select(a => a.SharedWorkspacePath).FirstOrDefault(p => !string.IsNullOrWhiteSpace(p));
|
||||
if (shared is null
|
||||
|| Path.GetDirectoryName(Path.TrimEndingDirectorySeparator(Path.GetFullPath(shared))) is not { } agentsDir)
|
||||
return path;
|
||||
var relative = Path.GetRelativePath(agentsDir, path);
|
||||
return relative.StartsWith("..", StringComparison.Ordinal) || Path.IsPathRooted(relative)
|
||||
? path
|
||||
: relative.Replace(Path.DirectorySeparatorChar, '/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user