Skip to Content
DesignSession RecapEntwurf der Sitzungszusammenfassung

Entwurf der Sitzungszusammenfassung

Eine kurze (1–2 Sätze) Zusammenfassung zum Thema „Wo war ich stehen geblieben?“, die dem Benutzer angezeigt wird, wenn er zu einer inaktiven Sitzung zurückkehrt – entweder auf Abruf (/recap) oder nachdem das Terminal für 5+ Minuten unscharf war.

Übersicht

Wenn ein Benutzer Tage später eine alte Sitzung /resumet, ist das Zurückscrollen durch Seiten von Verlauf, um sich zu erinnern, was er gerade gemacht hat und was als Nächstes kommt, ein echter Reibungspunkt. Das einfache erneute Laden von Nachrichten löst dieses UX-Problem nicht.

Ziel ist es, proaktiv eine kurze Zusammenfassung von 1–2 Sätzen einzublenden, wenn der Benutzer zurückkehrt:

  • Übergeordnete Aufgabe (was wird getan) → nächster Schritt (was als Nächstes zu tun ist).
  • Visuell deutlich von echten Assistentenantworten abgesetzt, damit sie nie mit neuer Modellausgabe verwechselt wird.
  • Best-Effort: Fehler müssen stillschweigend erfolgen und dürfen den Hauptablauf niemals unterbrechen.

Auslöser

AuslöserBedingungenImplementierung
ManuellBenutzer führt /recap ausrecapCommand.ts ruft denselben zugrunde liegenden Dienst auf
AutomatischTerminal unscharf (DECSET 1004 Fokusprotokoll) für ≥ 5 Min. + Fokus kehrt zurück + Stream ist IdleuseAwaySummary.ts – 5-Min.-Unscharf-Timer + useFocus-Ereignis-Listener
Daemon HTTPRemote-Client ruft POST /session/:id/recap aufserver.ts-Route → bridge.generateSessionRecap (Ext-Method-Roundtrip) → acpAgent.ts ruft generateSessionRecap(session.getConfig(), signal) auf

Alle drei Pfade münden in dieselbe Funktion generateSessionRecap() in core/services/sessionRecap.ts, um identisches Verhalten zu gewährleisten. Der automatische Auslöser wird durch general.showSessionRecap gesteuert (Standard: aus – explizites Opt-in, damit LLM-Aufrufe im Hintergrund niemals stillschweigend der Rechnung des Benutzers hinzugefügt werden); der manuelle Befehl und die Daemon-HTTP-Route ignorieren diese Einstellung (der Aufrufer stellt eine explizite Anfrage).

Daemon-Zugriffspfad

Die Daemon-Route ist nicht streng abgeschirmt (entspricht der Haltung von /session/:id/prompt – Recap kostet Tokens, verändert aber keinen Zustand). Das Fähigkeiten-Tag session_recap bewirbt die Route unter /capabilities.features. SDK-Helfer: DaemonClient.recapSession(sessionId, opts) und DaemonSessionClient.recap(opts). Siehe docs/developers/qwen-serve-protocol.md § POST /session/:id/recap für den Wire-Vertrag und das Fehler-Envelope.

Ein Abbruch ist in v1 nicht vorhanden. Die Route lauscht nicht auf HTTP-Client-Trennung, es wird kein AbortSignal in bridge.generateSessionRecap eingefädelt, und der ACP-Child-Handler übergibt ein nie abgebrochenes AbortController().signal an den Kern-Helfer (noch keine abteilungsübergreifende Abbruch-Installation). Die einzigen Obergrenzen sind der 60-Sekunden-SESSION_RECAP_TIMEOUT_MS-Backstop der Bridge und das Transport-geschlossene-Rennen gegen den ACP-Kanal-Tod. Ein isoliertes Verdrahten eines HTTP-seitigen AbortController wäre kosmetisch – der LLM-Aufruf auf der Child-Seite würde trotzdem bis zum Ende laufen, sodass ein Ende-zu-Ende-Abbruch ohne das abteilungsübergreifende Abbruch-Stück nicht realisierbar ist. Dies ist für v1 akzeptabel, da Recap kurz ist (einzelner Versuch, Nebenabfrage, maxOutputTokens: 300, typisch ~1–5 s). Eine zukünftige anforderungs-ID-basierte Cancel-Ext-Method kann den vollständigen Ende-zu-Ende-Abbruch implementieren, falls/wenn die Bandbreitenkosten dies rechtfertigen.

Architektur

┌────────────────────────────────────────────────────────────────────────┐ │ AppContainer.tsx │ │ isFocused = useFocus() │ │ isIdle = streamingState === Idle │ │ │ │ │ ├─→ useAwaySummary({enabled, config, isFocused, isIdle, │ │ │ │ addItem}) │ │ │ └─→ 5 min blur timer + idle/dedupe gates │ │ │ │ │ │ │ ↓ │ │ └─→ recapCommand (slash) ─→ generateSessionRecap(config, signal) │ │ │ │ │ ↓ │ │ ┌─────────────────────────┐ │ │ │ packages/core/services/ │ │ │ │ sessionRecap.ts │ │ │ └─────────────────────────┘ │ │ │ │ │ ↓ │ │ GeminiClient.generateContent │ │ (fastModel + tools:[]) │ │ │ │ addItem({type: 'away_recap', text}) ─→ HistoryItemDisplay │ │ └─ AwayRecapMessage rendered inline like any other history │ │ item (※ + bold "recap: " + italic content, all dim); │ │ scrolls naturally with the conversation. Mirrors Claude │ │ Code's away_summary system message. │ └────────────────────────────────────────────────────────────────────────┘

Dateien

DateiVerantwortung
packages/core/src/services/sessionRecap.tsEinmaliger LLM-Aufruf + Verlaufsfilter + Tag-Extraktion
packages/cli/src/ui/hooks/useAwaySummary.tsReact-Hook zum automatischen Auslösen
packages/cli/src/ui/commands/recapCommand.tsManueller Einstiegspunkt über /recap
packages/cli/src/ui/components/messages/StatusMessages.tsxRenderer für AwayRecapMessage ( + fett recap: + kursiver Inhalt, alles gedimmt)
packages/cli/src/ui/types.tsTyp HistoryItemAwayRecap
packages/cli/src/ui/components/HistoryItemDisplay.tsxLeitet away_recap-Verlaufselemente an den Renderer weiter
packages/cli/src/config/settingsSchema.tsgeneral.showSessionRecap + general.sessionRecapAwayThresholdMinutes-Einstellungen

Prompt-Design

System-Prompt

generationConfig.systemInstruction ersetzt das System-Prompt des Hauptagenten für diesen einzelnen Aufruf, sodass das Modell sich nur als Zusammenfassungsgenerator und nicht als Code-Assistent verhält.

Beachte, dass GeminiClient.generateContent() intern den Prompt durch getCustomSystemPrompt() laufen lässt, der den Benutzerspeicher (QWEN.md / verwalteter Auto-Speicher) als Suffix anhängt. Der finale System-Prompt ist daher Zusammenfassungs-Prompt + Benutzerspeicher — nützlicher Projektkontext für die Zusammenfassung, kein Leck.

Die folgenden Punkte entsprechen 1:1 RECAP_SYSTEM_PROMPT:

  • Unter 40 Wörtern, 1–2 einfache Sätze (kein Markdown / Listen / Überschriften). Für Chinesisch: Budget beträgt etwa 80 Zeichen insgesamt.
  • Erster Satz: die übergeordnete Aufgabe. Danach: der konkrete nächste Schritt.
  • Explizit verboten: Aufzählen, was getan wurde; Wiedergeben von Tool-Aufrufen; Statusberichte.
  • Die vorherrschende Sprache der Unterhaltung verwenden (Englisch oder Chinesisch).
  • Ausgabe in <recap>...</recap> einschließen; nichts außerhalb der Tags.

Strukturierte Ausgabe + Extraktion

Das Modell wird angewiesen, seine Antwort in <recap>...</recap> zu verpacken:

<recap>Refactoring von loopDetectionService.ts zur Behebung von OOM bei langen Sitzungen. Nächster Schritt: Option B implementieren.</recap>

Warum: Einige Modelle (GLM-Familie, Reasoning-Modelle) schreiben einen “Denk”-Absatz vor der endgültigen Antwort. Die Rohausgabe auszugeben würde dieses Denken in die UI durchsickern lassen.

extractRecap() hat drei Fallback-Ebenen:

  1. Beide Tags vorhanden: Nimm den Inhalt zwischen <recap>...</recap> (bevorzugt).
  2. Nur das öffnende Tag (z. B. wenn maxOutputTokens das schließende Tag abgeschnitten hat): Nimm alles nach dem öffnenden Tag.
  3. Tag vollständig fehlend: Leeren String zurückgeben → Service gibt null zurück → UI rendert nichts.

Die dritte Ebene ist “lieber auslassen, als das Falsche anzeigen” – das Vorwort mit dem Denken des Modells anzuzeigen ist schlimmer, als gar keine Zusammenfassung zu zeigen.

Aufrufparameter

ParameterWertGrund
modelgetFastModel() ?? getModel()Zusammenfassung braucht kein Frontier-Modell
tools[]Einmalige Abfrage, keine Tool-Nutzung
maxOutputTokens300Spielraum für 1–2 kurze Sätze + Tags
temperature0.3Meist deterministisch, mit etwas natürlicher Variation
systemInstructionDas reine Zusammenfassungs-Prompt (siehe oben)Ersetzt die Rollendefinition des Hauptagenten

Verlaufsfilterung

geminiClient.getChat().getHistory() gibt ein Content[] zurück, das Folgendes enthält:

  • user- / model-Textnachrichten
  • model- functionCall-Teile
  • user- functionResponse-Teile (die vollständige Dateiinhalte enthalten können)
  • model-Denkteile (part.thought / part.thoughtSignature, das verborgene Reasoning des Modells)

filterToDialog() behält nur user- / model-Teile, die nicht leeren Text haben und keine Gedanken sind. Zwei Gründe:

  • Tool-Aufrufe / Antworten: Ein einzelner functionResponse kann 10K+ Tokens umfassen. 30 solcher Nachrichten würden das Zusammenfassungs-LLM in irrelevanten Details ertränken, sowohl Tokens verschwenden als auch die Zusammenfassung in Richtung Implementierungsrauschen wie “hat X-Tool aufgerufen, um Y-Datei zu lesen” verzerren.
  • Denkteile: Enthalten das interne Reasoning des Modells. Sie einzubeziehen birgt die Gefahr, dass die verborgene Gedankenkette als Dialog behandelt und im Zusammenfassungstext angezeigt wird.

Nachdem leere Nachrichten entfernt wurden, schneidet takeRecentDialog die letzten 30 Nachrichten ab und weigert sich, das Segment auf einer hängenden Modell-/Tool-Antwort beginnen zu lassen.

Nebenläufigkeit und Randfälle

Zustandsmaschine des automatischen Auslöse-Hooks

useAwaySummary verwaltet drei Refs:

RefBedeutung
blurredAtRefStartzeitpunkt der Unschärfe (wird erst bei Rückkehr des Fokus gelöscht)
recapPendingRefGibt an, ob ein LLM-Aufruf läuft
inFlightRefAktueller in Bearbeitung befindlicher AbortController

useEffect-Dependencies: [enabled, config, isFocused, isIdle, addItem, thresholdMs].

EreignisAktion
!enabled || !configLaufenden Aufruf abbrechen + inFlightRef leeren + blurredAtRef leeren
!isFocused und blurredAtRef === nullSetze blurredAtRef = Date.now()
isFocused und blurredAtRef === nullFrüh beenden (kein Unschärfe-Zyklus zu verarbeiten – erster Render oder direkt nach einem kurzen Unschärfe-Reset)
isFocused und Unschärfe-Dauer < 5 MinutenblurredAtRef leeren, auf nächsten Unschärfe-Zyklus warten
isFocused und Unschärfe ≥ 5 Minuten und recapPendingRefBeenden (Deduplizierung)
isFocused und Unschärfe ≥ 5 Minuten und !isIdleBehalte blurredAtRef und warte, bis der Durchlauf beendet ist (isIdle ist in den Dependencies, daher feuert der Effekt erneut, wenn das Streaming abgeschlossen ist)
isFocused und Unschärfe ≥ 5 Minuten und shouldFireRecap gibt false zurückblurredAtRef leeren und beenden – die Konversation hat sich seit der letzten Zusammenfassung nicht genug bewegt (≥ 2 Benutzer-Durchläufe erforderlich, spiegelt Claude Code)
isFocused und alle Bedingungen erfülltblurredAtRef leeren, recapPendingRef = true setzen, AbortController erstellen, LLM-Anfrage senden

Der .then-Callback prüft erneut isIdleRef.current: Wenn der Benutzer während der LLM-Ausführung eine neue Runde gestartet hat, wird die spät eintreffende Zusammenfassung verworfen, um eine Einfügung mitten in der Runde zu vermeiden.

Der .finally-Block löscht recapPendingRef und löscht inFlightRef nur dann, wenn inFlightRef.current === controller (damit kein neuerer Controller überschrieben wird).

Ein zweiter useEffect bricht den laufenden Controller beim Unmount ab.

/recap-Sperre

CommandContext.ui.isIdleRef gibt den aktuellen Stream-Zustand preis (spiegelt das bestehende btwAbortControllerRef-Muster wider). Im interaktiven Modus verweigert recapCommand die Ausführung, wenn !isIdleRef.current oder pendingItem !== null gilt. Allein pendingItem reicht nicht, da eine normale Modellantwort mit streamingState === Responding und einem null-pendingItem läuft.

Konfiguration und Modellauswahl

Benutzersichtbare Einstellungen

EinstellungStandardHinweise
general.showSessionRecapfalseNur automatische Auslösung. Manuelles /recap ignoriert dies.
general.sessionRecapAwayThresholdMinutes5Minuten Unschärfe, bevor bei Fokus-Rückkehr die automatische Zusammenfassung ausgelöst wird. Entspricht dem Standard von Claude Code.
fastModelnicht gesetztEmpfohlen (z.B. qwen3-coder-flash) für schnelle und günstige Zusammenfassungen.

Modell-Fallback

config.getFastModel() ?? config.getModel():

  • Der Benutzer hat ein fastModel gesetzt und es ist für den aktuellen Authentifizierungstyp gültig → fastModel verwenden.
  • Andernfalls → Fallback auf das Hauptsitzungsmodell (funktioniert, ist aber teurer und langsamer).

Beobachtbarkeit

createDebugLogger('SESSION_RECAP') gibt aus:

  • abgefangene Ausnahmen aus dem Recap-Pfad (debugLogger.warn).

Alle Fehler sind für den Benutzer völlig transparent – die Zusammenfassung ist eine Hilfsfunktion und wirft niemals Fehler in die UI. Entwickler können nach dem [SESSION_RECAP]-Tag in der Debug-Logdatei suchen: standardmäßig geschrieben nach ~/.qwen/debug/<sessionId>.txt (latest.txt verweist per Symlink auf die aktuelle Sitzung); deaktivieren über QWEN_DEBUG_LOG_FILE=0.

Außerhalb des Rahmens

PunktWarum nicht
Fortschrittsanzeige für /recap (Spinner / pendingItem)3-5 Sekunden Wartezeit sind akzeptabel; erhöht die Komplexität.
Automatisierte TestsDer Service ist klein (~150 Zeilen), zuerst manuell end-to-end getestet; Unit-Tests können in einem separaten PR eingebracht werden.
Lokalisierte PromptsDer System-Prompt ist für das Modell; Englisch ist die zuverlässigste Grundlage. Das Modell wählt die Ausgabesprache aus der Konversation.
QWEN_CODE_ENABLE_AWAY_SUMMARY-UmgebungsvariableClaude Code verwendet sie, um die Funktion bei deaktivierter Telemetrie aktiv zu halten; Qwen Codes aktuelles Telemetriemodell benötigt dies nicht.
Automatische Zusammenfassung nach /resume-AbschlussEin naheliegendes Folgefeature, benötigt aber einen Hook-Punkt in useResumeCommand; nicht im Rahmen dieses PRs.
Last updated on