Bugtracker-Durchgang: 13 Bugs behoben, Build ohne Warnungen

Alle Behebungen auf der Linux-Test-VM gegengeprueft (Ubuntu, .NET 10.0.111):
Build 0 Warnungen / 0 Fehler, 637 Tests gruen, 1 uebersprungen (WindowsFact).

Sicherheit und Zielplattform
  #373 PathBoundary.Canonicalize fiel offen aus. File.ResolveLinkTarget wirft
       fuer einen fehlenden Abschnitt FileNotFoundException; der catch(IOException)
       verliess daraufhin die GANZE Aufloesung mit dem unaufgeloesten Pfad - und
       der besteht jede Praefixpruefung. Der Symlink-Schutz war damit bei jedem
       Schreibvorgang wirkungslos. Jetzt ueber FileSystemInfo.LinkTarget, und
       TryCanonicalize meldet "nicht bestimmbar", statt fail-open durchzureichen.
  #374 WorkspacePath weist den Backslash rundheraus ab, wie den Doppelpunkt.
       Eine Schreibweise statt einer Sonderregel je Windows-Form.
  #375 Log-Verzeichnis: leer heisst "Anwendungseinstellung"; ein relativer Wert
       zaehlt gegen das Instanzverzeichnis, nicht gegen das Arbeitsverzeichnis
       des Prozesses. Ein systemd-Dienst landet nicht mehr auf /Logs.

Agentenkommunikation
  #206 Delegationskette gegen A->B->A (Test A13). Zyklusfrei per Bauart, dazu
       MaxDelegationDepth gegen die Staffel, die niemand beauftragt hat.
  #461 Zwei unabhaengige Ketten ueber Kreuz - beim Beheben von #206 gefunden und
       reproduziert. Ein delegierter Lauf wartet nur noch begrenzt auf ein fremdes
       Gate; ein Chat vom Menschen reiht sich weiter unbegrenzt ein.
  #234 Instanz-Id wird durchgereicht statt aus einem nur beilaeufig gesetzten Feld
       gelesen.

Betrieb und Oberflaeche
  #376 Log-Ansicht chronologisch statt gruppiert je Moduldatei, mit Stacktraces
       an ihrer Meldung - auch ueber Taktgrenzen hinweg.
  #377 Info-Tabelle: feste Spaltenbreiten, damit ein langer Aenderungstext den
       anderen Spalten nicht den Platz nimmt.
  #378 Sicherung: Mindestabstand zwischen Ueberschrift und Schaltflaechenleiste.
  #379 Hellmodus: drei von vier gedaempften Textpinseln lagen unter 4,5:1. Die
       helle Palette ist als Ganzes verschoben, damit die Staffelung sichtbar
       bleibt statt auf einen Wert zusammenzufallen.

Hygiene
  #233 stock_add: der Indexstand wird innerhalb der Sperre festgehalten.
  #235 Zeitumstellung: eine nicht existierende Ortszeit wird auf den ersten
       Zeitpunkt nach der Luecke verschoben statt uebersprungen (R4, R5).
  #236 45 Nullable-Warnungen und eine CA1416 - nicht unterdrueckt, sondern die
       Stellen so umgebaut, dass die Zusage nachvollziehbar wird. Mail meldet
       jetzt, WELCHER Zugangsschluessel fehlt, statt an einer
       KeyNotFoundException zu scheitern.
  #203 Kompaktiert wird vor dem API-Aufruf statt danach; die gemessenen
       Prompt-Groessen kalibrieren die Schaetzung je Modell.

Neu: tests/ClawdDotNet.App.Tests - fuer #375 und #376 gab es keinen Ort fuer
Tests. Dazu Regressionstests fuer jede Behebung; jeder wurde gegen den alten
Stand geprueft, damit belegt ist, dass er den Fehler faengt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-03 19:04:48 +02:00
co-authored by Claude Opus 5
parent 740649789e
commit cffd6f4b77
44 changed files with 1942 additions and 228 deletions
@@ -16,5 +16,12 @@ public sealed record AgentToolContext(
string? SharedWorkspacePath = null,
IAgentMessageRouter? MessageRouter = null,
IMemoryRepository? Memory = null,
ITaskRepository? Tasks = null
ITaskRepository? Tasks = null,
/// <summary>
/// Wer an diesem Aufruf schon beteiligt ist. Werkzeuge, die einen anderen Agenten
/// beauftragen, reichen die Kette weiter — sonst kann die Engine einen Zyklus
/// nicht von einer neuen Delegation unterscheiden. Siehe <see cref="DelegationChain"/>.
/// </summary>
DelegationChain? Delegation = null
);
@@ -0,0 +1,56 @@
namespace ClawdDotNet.Core.Tools;
/// <summary>
/// Wer an diesem Aufruf schon beteiligt ist — die Kette vom ersten Agenten bis zum
/// gerade laufenden.
///
/// <para><b>Warum es die Kette gibt (B8).</b> <c>send_message</c> ist ein synchroner
/// Aufruf: A hält sein Gate, während es auf B wartet. Ruft B nun A, wartet B auf ein
/// Gate, das A hält und erst freigibt, wenn B geantwortet hat. Das löste nur der
/// Timeout des äußeren Laufs auf — bis dahin standen beide Agenten.</para>
///
/// <para><b>Die Zusagen</b>, auf denen alles Weitere aufsetzt:</para>
/// <list type="number">
/// <item>Ein Agent kommt in einer Kette höchstens einmal vor. Damit wartet niemand
/// auf ein Gate, das er in derselben Kette schon hält — der Verklemmung ist die
/// Grundlage entzogen, nicht nur die Wahrscheinlichkeit genommen.</item>
/// <item>Eine Kette ist höchstens <c>MaxDelegationDepth</c> Schritte lang. Zyklen
/// sind damit ausgeschlossen, eine lange Staffel A→B→C→D→… aber nicht; die
/// kostet Zeit und Tokens, ohne dass jemand sie beauftragt hätte.</item>
/// <item>Die Kette gehört zum <b>Aufruf</b>, nicht zum Agenten. Zwei unabhängige
/// Läufe desselben Agenten behindern einander nicht.</item>
/// </list>
///
/// <para>Unveränderlich: <see cref="Append"/> gibt eine neue Kette zurück. Ein Aufruf
/// kann die Kette seines Aufrufers deshalb nicht verändern, und die Kette lässt sich
/// gefahrlos über Fadengrenzen reichen.</para>
/// </summary>
public sealed class DelegationChain
{
private readonly string[] _agents;
private DelegationChain(string[] agents) => _agents = agents;
/// <summary>Der Anfang: ein Lauf, den niemand delegiert hat.</summary>
public static DelegationChain Root(string agentId) => new([agentId]);
/// <summary>Die Agenten in der Reihenfolge, in der sie beauftragt wurden.</summary>
public IReadOnlyList<string> Agents => _agents;
/// <summary>
/// Die Zahl der Weitergaben — nicht die der Agenten. Ein Lauf ohne Delegation hat
/// die Tiefe 0, A→B die Tiefe 1.
/// </summary>
public int Depth => _agents.Length - 1;
/// <summary>Ist dieser Agent in der Kette schon vertreten?</summary>
public bool Contains(string agentId)
=> Array.IndexOf(_agents, agentId) >= 0;
/// <summary>Die Kette mit einem weiteren Agenten am Ende.</summary>
public DelegationChain Append(string agentId)
=> new([.. _agents, agentId]);
/// <summary>Für Meldungen und Protokoll: <c>A -> B -> C</c>.</summary>
public override string ToString() => string.Join(" -> ", _agents);
}
@@ -10,15 +10,32 @@ public interface IAgentMessageRouter
{
IReadOnlyList<AgentInfo> ListAgents(string callerAgentId);
/// <param name="instanceId">
/// Die Instanz des aufrufenden Laufs — aus <see cref="AgentToolContext.InstanceId"/>
/// durchgereicht. Leer heißt „nimm die Instanz, mit der die Engine eingerichtet
/// wurde". Vorher galt immer Letzteres, auch wenn der Lauf mit einer anderen
/// Instanz-Id angestoßen worden war (B13).
/// </param>
/// <param name="chain">
/// Die Delegationskette des aufrufenden Laufs — aus
/// <see cref="AgentToolContext.Delegation"/> durchgereicht. <c>null</c> heißt
/// „von aussen angestossen", die Kette beginnt dann beim Absender. Ohne sie kann
/// die Engine A→B→A nicht von einer neuen Delegation unterscheiden (B8).
/// </param>
Task<AgentMessageResult> SendMessageAsync(
string fromAgentId,
string toAgentId,
string message,
string? instanceId,
DelegationChain? chain,
CancellationToken ct);
/// <inheritdoc cref="SendMessageAsync"/>
Task<AgentSpawnResult> SpawnAgentAsync(
string fromAgentId,
string targetAgentId,
string taskMessage,
string? instanceId,
DelegationChain? chain,
CancellationToken ct);
}