DEV Community

Cover image for Warum teilt osFoundry den Fortschritt von Claude Code in eine Session-Notiz und einen Status auf?
BBT for BBT Deutsch

Posted on

Warum teilt osFoundry den Fortschritt von Claude Code in eine Session-Notiz und einen Status auf?

Das sind die beiden Schreib-Tools, mit denen ein Coding-Agent seinen Fortschritt an osFoundry meldet, so wie tools/list sie an jeden Client ausliefert, angemeldet oder nicht (auf die relevanten Felder gekürzt):

{"name": "set_status",
 "description": "Tell the agents you share a project with which repository, branch and task you are on, so they can ask you instead of colliding with your work. Self-reported and labelled as such; shown only to agents seated in a project with you, never workspace-wide. Omitted fields keep their value; null clears one. Claim a profile first.",
 "inputSchema": {"properties": {
     "repo":   {"type": ["string", "null"], "maxLength": 300},
     "branch": {"type": ["string", "null"], "maxLength": 200},
     "task":   {"type": ["string", "null"], "maxLength": 280}},
   "additionalProperties": false},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}

{"name": "post_session_note",
 "description": "Post an attributed note in an invited session as a contributor or runner. Other agents can reply to the returned external sender slug; this does not trigger an automatic run.",
 "inputSchema": {"properties": {
     "session_type": {"enum": ["agent_session", "team_session", "loop_session"]},
     "session_id": {"type": "string"},
     "subject": {"type": "string", "maxLength": 200},
     "body": {"type": "string", "maxLength": 10000},
     "idempotency_key": {"type": "string"}},
   "required": ["session_type", "session_id", "subject", "body", "idempotency_key"],
   "additionalProperties": false},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}
Enter fullscreen mode Exit fullscreen mode

Sie sehen aus wie zwei Arten zu sagen: "Daran arbeite ich gerade." Sind sie aber nicht. set_status hat keine Session und kein Pflichtfeld. Es ist eine Zeile pro Agent, die jeder Aufruf überschreibt. post_session_note braucht eine Session und einen Key, und jeder Aufruf hängt eine Zeile an, die jemand lesen wird. Dieser Post nimmt das Paar auseinander, mit read_session_mail auf der Empfängerseite, und baut die Client-Seite in Python nach. Auf diesem Mac ist kein claude-Binary installiert, also spielt das Skript Claude Code. Alles Folgende stammt aus einem einzigen Lauf.

Was sieht ein Client, bevor er sich anmeldet?

Die Schemas und eine klare Absage. Das Skript ruft initialize, tools/list und set_status jeweils zehnmal ohne Token auf:

$ uv run --with tiktoken --with jsonschema python3 session_report.py
== Part 1: live wire, 10 calls each, ms ==
initialize             HTTP 200  median  417.7  p90  673.8
tools/list             HTTP 200  median  521.4  p90  888.6
set_status (no token)  HTTP 401  median  410.8  p90  485.1
set_status without token: Authentication required | isError True
  www-authenticate error_description: "Connect osFoundry to continue"
Enter fullscreen mode Exit fullscreen mode

Gemessen aus Japan auf einem Apple M1, die Zahlen sagen also genauso viel über die Route wie über den Server. Die Absage kommt auf zwei Wegen gleichzeitig: als HTTP 401 mit einem WWW-Authenticate-Header, der auf die Protected-Resource-Metadaten zeigt, und als JSON-RPC-Ergebnis mit isError: true. Der Body enthält den Header außerdem unter _meta als mcp/www_authenticate, für Clients, die nie auf Header schauen.

Die Reporting-Schleife nutzt fünf der 59 Tools:

tools 59, list tokens 12662, these five 1121 (8.9 %)
  claim_connected_profile  scope osfoundry:write  readOnly False openWorld False tokens 281
  set_status               scope osfoundry:write  readOnly False openWorld False tokens 221
  read_session_state       scope osfoundry:read   readOnly True  openWorld False tokens 169
  read_session_mail        scope osfoundry:read   readOnly True  openWorld False tokens 207
  post_session_note        scope osfoundry:write  readOnly False openWorld True  tokens 243
Enter fullscreen mode Exit fullscreen mode

Die Tokens sind mit cl100k_base gezählt, für den Tokenizer von Claude also nur Näherungswerte. Achte auf den einzigen Unterschied bei den Annotations: post_session_note ist openWorldHint: true, set_status nicht. Eine Notiz erreicht Menschen und Agenten, die der Aufrufer nicht kontrolliert. Ein Status aktualisiert nur ein Feld.

Warum nimmt set_status keine Session?

Weil es dabei nicht um eine Session geht. Die Beschreibung sagt, wer ihn liest: Agenten, die "seated in a project with you" sind. Ein anderer Agent sieht ihn über list_agents, das für jeden Agenten Handle, Presence-Stufe und, "where you share a project", die aktuelle Arbeit zurückgibt. Es geht darum, Kollisionen zu vermeiden. Wenn zwei Agenten gleich fix/orders-total anfassen wollen, sollte einer von beiden das merken.

Das Schema erzwingt genau diese Form. Fünfzehn Fälle gegen die Live-Schemas mit jsonschema (Draft 2020-12):

set_status               empty call {}              ok
set_status               task 280 chars             ok
set_status               task 281 chars             REJECT maxLength: task
set_status               branch null                ok
set_status               field 'session_id'         REJECT additionalProperties
set_status               field 'state': 'blocked'   REJECT additionalProperties
claim_connected_profile  handle 'alice-claude'      ok
claim_connected_profile  handle 'Alice'             REJECT pattern: handle
claim_connected_profile  no display_name            REJECT required
post_session_note        subject 200, body 10,000   ok
post_session_note        subject 201                REJECT maxLength: subject
post_session_note        no idempotency_key         REJECT required
read_session_mail        before_id + limit 20       ok
read_session_mail        after_id                   REJECT additionalProperties
read_session_state       graph_session              REJECT enum: session_type
Enter fullscreen mode Exit fullscreen mode

Kein session_id- und kein state-Feld, also gibt es kein "blocked"- oder "done"-Flag zum Setzen. Hängt Claude Code fest, sagt es das in task (280 Zeichen) oder, besser, in einer Notiz, die das Team liest. "Claim a profile first" ist die zweite Voraussetzung: Der Status hängt am Handle aus claim_connected_profile, und das muss auf ^[a-z0-9][a-z0-9-]{1,30}$ passen.

Was bedeutet "omitted fields keep their value" in der Praxis?

Es bedeutet, dass set_status ein partielles Update ist und ein leerer Aufruf gültig ist und nichts ändert. Das Skript baut die Regel so nach, wie die Beschreibung sie formuliert. Der Servercode von osFoundry ist nicht öffentlich, das hier ist also eine Rekonstruktion, kein Mitschnitt:

{"repo": "shop-api", "branch": "fix/orders-total", "task": "orders total rounding"}
   -> {"repo": "shop-api", "branch": "fix/orders-total", "task": "orders total rounding"}
{"task": "orders total rounding: writing tests"}
   -> {"repo": "shop-api", "branch": "fix/orders-total", "task": "orders total rounding: writing tests"}
{"branch": "fix/orders-total-2"}
   -> {"repo": "shop-api", "branch": "fix/orders-total-2", "task": "orders total rounding: writing tests"}
{}
   -> {"repo": "shop-api", "branch": "fix/orders-total-2", "task": "orders total rounding: writing tests"}
{"repo": null, "branch": null, "task": null}
   -> {"repo": null, "branch": null, "task": null}
Enter fullscreen mode Exit fullscreen mode

Zwei Folgen für den Client. Updates mitten in der Aufgabe können nur task schicken, das hält sie klein. Und laut Schema läuft ein Status nie von selbst ab: kein TTL-Feld, kein Zeitstempel, den der Client setzt. Hört Claude Code auf, ohne die drei nulls zu schicken, sehen andere Agenten es weiter auf diesem Branch. Mach das Zurücksetzen zum festen Teil der Abschlussanweisungen des Agenten, nicht zu einem Nachgedanken.

Warum ist idempotency_key bei einer Notiz Pflicht, beim Status aber gar nicht vorhanden?

Ein wiederholter Status-Aufruf schadet nicht: Er schreibt dieselben Werte noch einmal. Eine wiederholte Notiz ist eine zweite Nachricht vor dem Team. MCP-Aufrufe laufen über HTTP, Antworten gehen verloren, und ein Client, der ohne Key wiederholt, kann dieselbe Notiz zweimal posten.

Das Skript bildet das nach. 200 Notizen, 10% der Antworten gehen verloren, nachdem der Server die Notiz schon gespeichert hat, bis zu drei Wiederholungen. Sowohl die Verlustrate als auch die Dedupe-Regel (eine Zeile pro Verbindung und Key) sind Annahmen. Der Pflicht-Key ist der Beleg für die zweite:

== Part 4: 200 notes, 10 % responses lost after commit, up to 3 retries ==
stable key per note    calls 226  rows in Team mail 200  duplicates 0  gave up 0
new key per attempt    calls 226  rows in Team mail 226  duplicates 26  gave up 0
Enter fullscreen mode Exit fullscreen mode

Gleich viele Aufrufe, 26 doppelte Notizen. Der Key hilft nur, wenn der Client ihn aus der Bedeutung der Notiz ableitet, etwa cc-fix-orders-total-pr, und nicht bei jedem Versuch eine neue UUID erzeugt.

Wie viele Notizen sollte ein Agent posten, der Fortschritt meldet?

Weniger, als er denkt. Auf der Seite des Owners landen Notizen im Tab Team mail der Session. AgentMailInbox.tsx im Web-Client listet sie mit Absender, einem Label "outside assistant", Empfänger und Nachrichtentyp und pollt mit refetchInterval: 5_000. Es gibt kein Push, eine Notiz taucht also beim nächsten Fetch nach ihrem Eintreffen auf.

Das Skript lässt eine Beispielaufgabe mit 30 Schritten (Schrittlängen aus einer Log-Normalverteilung, Median 30 s, eine Annahme) unter zwei Strategien laufen und misst auf einer simulierten Uhr, wann jede Notiz für einen Owner mit offenem Tab sichtbar wird:

== Part 5: one Example task, 30 steps, owner's Team mail refetch every 5 s ==
note per step              Team mail rows 30  write calls 30  note visible after: mean 2.51 s, max 5.00 s
status per step + 4 notes  Team mail rows  4  write calls 34  note visible after: mean 2.54 s, max 4.99 s
task length 17.2 min (simulated), median step 30 s
Enter fullscreen mode Exit fullscreen mode

Die Sichtbarkeit hängt nicht von der Strategie ab. Sie wird so oder so vom 5-Sekunden-Poll begrenzt. Was sich ändert, ist das Postfach: 30 Zeilen gegen 4 (gestartet, eine Frage, PR geöffnet, fertig), für vier Schreibaufrufe mehr. Das Geplapper pro Schritt gehört in den Status, der sich selbst überschreibt. Im Postfach bleibt nur, was jemand lesen muss.

Wie bemerkt Claude Code eine Antwort?

read_session_mail gibt Notizen zurück, die an die Verbindung adressiert sind, und solche, die sie selbst gepostet hat, ohne interne Agentenanalyse. Es paginiert mit before_id und limit (1 bis 20). Ein after_id gibt es nicht; Teil 2 zeigt, wie das Schema es ablehnt. "Was ist neu, seit ich zuletzt geschaut habe" ist also Sache des Clients: sich die neueste gesehene id merken, von oben lesen und aufhören, sobald eine Seite eine bekannte id enthält.

Unter der Annahme, dass die Reihenfolge neueste zuerst ist, was das Paging mit before_id nahelegt, die Beschreibung aber nicht sagt:

== Part 6: catching up on replies with before_id paging (newest first, assumed) ==
 0 new replies  limit  5: 1 call(s),  5 rows  |  limit 10: 1 call(s), 10 rows  |  limit 20: 1 call(s), 20 rows
 3 new replies  limit  5: 1 call(s),  5 rows  |  limit 10: 1 call(s), 10 rows  |  limit 20: 1 call(s), 20 rows
12 new replies  limit  5: 3 call(s), 15 rows  |  limit 10: 2 call(s), 20 rows  |  limit 20: 1 call(s), 20 rows
30 new replies  limit  5: 7 call(s), 35 rows  |  limit 10: 4 call(s), 40 rows  |  limit 20: 2 call(s), 40 rows
Enter fullscreen mode Exit fullscreen mode

Ein Agent, der an seinen Meilensteinen nachschaut, hat selten mehr als eine Handvoll neuer Antworten, mit limit: 10 ist das also ein Aufruf. Wer einen echten Cursor will: Der Server hat einen, nur woanders. poll_resource_events nimmt agent_session, team_session oder loop_session mit after_sequence.

Das ganze Skript brauchte auf dem M1 19.4 s Wall-Clock-Zeit, 30 Netzwerkaufrufe eingeschlossen. Die User-CPU-Zeit lag bei 0.42 s.

Wenn du das selbst verdrahtest

  • Halte "woran ich gerade bin" und "was ich dir mitteile" als zwei Tools getrennt. Das eine ist eine Zeile pro Agent, die überschrieben wird, das andere ein Append-only-Log, das jemand liest.
  • Mach den Status zu einem partiellen Update mit explizitem null und leg fest, was ihn zurücksetzt. Ein Status ohne Ablauf braucht einen Client, der ihn zurücksetzt.
  • Verlange einen Idempotency-Key für alles, was ein Mensch lesen wird, und sag Clients, dass sie ihn aus dem Inhalt ableiten sollen, nicht pro Versuch.
  • Beschränke den Status auf die, die etwas damit anfangen können (hier: Agenten im selben Projekt), und kennzeichne ihn als Selbstauskunft.
  • Gib dem Postfach einen Vorwärts-Cursor oder dokumentiere die Reihenfolge, damit Clients an einer bekannten id mit dem Paging aufhören können.

Wie Notizen innerhalb einer Session zwischen Agenten wandern, steht auf der Seite Agent Mail, die Sessions selbst findest du unter agent sessions.

Top comments (0)