Skip to Content
EntwicklerhandbuchDaemonSession-Lifecycle & Identität

Session-Lifecycle & Identität

Übersicht

Eine Daemon-Session ist eine logische Konversation, die an eine ACP-sessionId gebunden ist. Die Bridge verwaltet pro Session einen SessionEntry (siehe 03-acp-bridge.md), der die ACP-Child-Verbindung mit dem HTTP-seitigen Bookkeeping koppelt: Prompt-FIFO, Model-Change-FIFO, Event Bus, ausstehende Permissions, angehängte Clients, Heartbeats, Restore-State, Terminal-Frame-Tombstones.

Ein Daemon-Client wird durch X-Qwen-Client-Id identifiziert – ein opaker, vom Daemon validierter String, den der HTTP-Caller seinen Requests hinzufügt. Die Bridge verfolgt, welche Clients mit welchen Sessions verbunden sind, und verwendet die Originator-Client-ID, um die designated Permission-Policy, Audit-Trails und Event-Attribution zu steuern.

Dieses Dokument erläutert jeden Session-Lifecycle-Übergang (create / attach / load / resume / close / die / evict) und jede Identitätsoberfläche, die der Daemon bereitstellt.

Zuständigkeiten

  • Sessions minten, anhängen, wiederherstellen und bereinigen.
  • X-Qwen-Client-Id validieren und fehlerhafte IDs ablehnen.
  • Mehrere angehängte Clients pro Session verfolgen (clientIds: Map<string, count>, attachCount).
  • originatorClientId auf ausgehende Events stempeln.
  • Heartbeats ausführen, damit Dashboards wissen, welche Clients noch verbunden sind.
  • Session-Metadaten (displayName) bereitstellen, die Operatoren über PATCH /session/:id/metadata setzen.
  • Ausgabe von Terminal-Frames steuern (session_died, session_closed, client_evicted, stream_error).

Architektur

BereichQuelleHinweise
SessionEntrypackages/acp-bridge/src/bridge.tsPro-Session-Struct; siehe 03-acp-bridge.md für die vollständige Feldauflistung.
BridgeSession (public)packages/acp-bridge/src/bridgeTypes.ts{ sessionId, workspaceCwd, attached, clientId?, createdAt? }, das an HTTP-Handler zurückgegeben wird.
BridgeSessionStatepackages/acp-bridge/src/bridgeTypes.tsLoadSessionResponse | ResumeSessionResponse, das im Entry als restoreState zwischengespeichert wird.
DaemonSession (SDK)packages/sdk-typescript/src/daemon/types.ts{ sessionId, workspaceCwd, attached, clientId?, createdAt? }.
Client-id validationpackages/acp-bridge/src/bridge.ts (around spawnOrAttach)Pattern [A-Za-z0-9._:-]{1,128}; InvalidClientIdError bei fehlerhafter Formatierung.
Session disconnect-reaperpackages/cli/src/serve/server.tsVerfolgt Spawn-Owner-Disconnects mit attachCount + spawnOwnerWantedKill.

State machine

Attach vs spawn

Unter sessionScope: 'single' (Standard) wird der defaultEntry der Bridge von jedem verbindenden Client geteilt. Ein POST /session, das eintrifft, während defaultEntry bereits existiert, gibt attached: true zurück, ohne ein neues ACP-Child zu spawnen. Die Bridge erhöht synchron attachCount und registriert die X-Qwen-Client-Id des Callers in clientIds.

Unter sessionScope: 'thread' kann jeder Thread eine eigene Session minten. Der Caller beachtet dabei weiterhin maxSessions.

Identität

X-Qwen-Client-Id ist optional, wird aber dringend empfohlen. Der Daemon generiert keine im Namen des Callers – Clients wählen ihre eigene und verwenden sie über Requests hinweg wieder, damit der Daemon Votes zuordnen, Events auditieren und Reconnects erkennen kann.

Jeder unabhängige Controller sollte eine eigene, stabile ID verwenden. Die WebUI generiert standardmäßig IDs mit dem Präfix webui_. Ein Host und eine eingebettete WebShell sollten sich eine ID nur teilen, wenn sie absichtlich als ein logischer Controller agieren; sobald sie geteilt wird, können Daemon-Logs nicht unterscheiden, welcher der beiden einen Request ausgelöst hat.

Validierungsregeln:

  • Zeichensatz: [A-Za-z0-9._:-].
  • Länge: 1–128.
  • Außerhalb dieses Sets: InvalidClientIdError (400).

Der Daemon stempelt originatorClientId auf ausgehende SSE-Events, wenn:

  1. der Request, der das Event ausgelöst hat, X-Qwen-Client-Id enthielt, UND
  2. die ID derzeit im clientIds-Set der Session registriert ist, UND
  3. die Session eine activePromptOriginatorClientId gesetzt hat (Inline-sessionUpdate und permission_request erben den Originator vom aktiven Prompt).

Anonyme Caller (ohne X-Qwen-Client-Id) funktionieren einwandfrei mit der first-responder-Policy; designated lehnt ihre Votes mit permission_forbidden{ reason: 'designated_mismatch' } ab; consensus lehnt mit demselben forbidden-Grund ab, da der Voter nicht im votersAtIssue-Snapshot zum Ausgabezeitpunkt ist; local-only ist die einzige Policy, die anonyme Loopback-Voter akzeptiert.

Workflow

Create or attach

Load / resume

POST /session/:id/load – stellt eine persistierte Session wieder her und gibt das aktuelle begrenzte Replay-Snapshot-Fenster zurück (session/load-Benachrichtigungen oder Response-Mode-Replay werden vor der Response-Ausgabe gespeist). POST /session/:id/resume – stellt wieder her, ohne abzuspielen (connection.unstable_resumeSession, verfügbar unter der stabilen session_resume-Daemon-Capability; unstable_session_resume bleibt ein deprecated Alias).

Beide:

  1. Verwenden ein session-spezifisches pendingRestoreIds-Set auf dem Channel, sodass gleichzeitige Restore-Aufrufe zusammengeführt werden (RestoreInProgressError).
  2. Zwischenspeichern von restoreState im Entry, sodass ein später hinzukommender Attacher dieselbe Payload erhält wie der ursprüngliche Restorer.

Heartbeat

POST /session/:id/heartbeat aktualisiert sessionLastSeenAt unabhängig von der clientId. Wenn der Request eine registrierte X-Qwen-Client-Id enthält, wird zusätzlich clientLastSeenAt.set(clientId, Date.now()) aktualisiert. Eine client-spezifische Eviction ist in v1 nicht implementiert; Revocation ist für F-Series Wave 5 geplant. Heute bieten Heartbeats Observability für Dashboards und für die kommende Revocation-Policy in PR 24.

Metadata

PATCH /session/:id/metadata akzeptiert {displayName?}. Validierung:

  • Maximale Länge: MAX_DISPLAY_NAME_LENGTH = 256.
  • Darf keine Steuerzeichen enthalten (hasControlCharacter lehnt Codepoints ≤ 0x1f oder == 0x7f ab).
  • InvalidSessionMetadataError (400) bei Verstößen.

Ein erfolgreiches Update verteilt session_metadata_updated an jeden Subscriber.

Termination

Terminal-FrameAuslöser
session_closedDELETE /session/:id (client_close) oder programmatisches Schließen.
session_diedchannel.exited wird aus irgendeinem Grund ausgelöst (Crash, Child-Kill). Enthält exitCode? + signalCode?, wenn der OS-Exit-Pfad verwendet wurde.
client_evictedQueue-Überlauf pro Subscriber auf dem EventBus (siehe 10-event-bus.md). KEINE Session-Level-Terminierung – nur dieser Subscriber wird geschlossen.
stream_errorSubscriberLimitExceededError oder anderer Route-Level-Stream-Fehler.

Ausstehende Permissions werden auf jedem Terminierungspfad über mediator.forgetSession(sessionId) als {kind:'cancelled', reason:'session_closed'} aufgelöst.

Disconnect-reaper guard

Wenn die HTTP-Response des Spawn-owning Clients nicht geschrieben werden kann (TCP-Reset mitten im Handshake), ruft die Route killSession({ requireZeroAttaches: true }) auf. Wenn bereits ein anderer Client angehängt ist (attachCount > 0), greift der Guard nicht und die Session bleibt bestehen. Das Setzen von spawnOwnerWantedKill = true merkt sich die Absicht, sodass ein späteres detachClient(), das attachCount wieder auf 0 bringt, das aufgeschobene Aufräumen (reap) abschließt. Ohne dies würde ein schnell trennender Spawn-Owner bei jedem zweiten Reconnect eine gesunde Session abbauen.

Zustand & Lebenszyklus

Für den Lifecycle kritische SessionEntry-Felder:

FeldTypBedeutung
clientIdsMap<string, number>Registrierte Client-IDs → Referenzzähler der Registrierung.
attachCountnumberWie oft spawnOrAttach für diesen Entry attached: true zurückgegeben hat.
activePromptOriginatorClientIdstring?Originator für den aktuell laufenden Prompt.
restoreStateBridgeSessionState?Zwischengespeicherte Load/Resume-Response, sodass spätere Attacher konsistente Payloads sehen.
spawnOwnerWantedKillbooleanTombstone für aufgeschobenes Aufräumen (siehe Disconnect-Reaper oben).
sessionLastSeenAtnumber?Letzter Heartbeat über alle Clients hinweg (Epoch-ms).
clientLastSeenAtMap<string, number>Heartbeat pro Client.
pendingPermissionIdsSet<string>Derzeit ausstehende ACP-Request-IDs – werden bei Cancel/Close verwendet, um sie als cancelled aufzulösen.

Abhängigkeiten

  • ACP-Layer: connection.newSession, connection.unstable_resumeSession, connection.loadSession.
  • 03-acp-bridge.md für die umgebende Bridge-Architektur.
  • 04-permission-mediation.md dafür, wie Originator + Identität Policy-Entscheidungen steuern.
  • 10-event-bus.md für die Zustellung von Terminal-Frames.

Zusätzliche Session-Endpoints

Diese Endpoints erweitern die Basis-Lifecycle-Oberfläche:

Non-blocking Prompt (non_blocking_prompt capability tag)

POST /session/:id/prompt gibt jetzt HTTP 202 mit { promptId, lastEventId } zurück, anstatt zu blockieren, bis der Prompt abgeschlossen ist. Das eigentliche Ergebnis trifft auf SSE als turn_complete / turn_error ein, und das promptId-Feld korreliert diese Events mit der 202-Response. DaemonSessionClient.prompt() verwendet automatisch den nicht-blockierenden Pfad, wenn es ein aktives Event-Abonnement hat, und gleicht das Ergebnis transparent aus dem SSE-Stream ab.

Session Recap (session_recap capability tag)

POST /session/:id/recap fragt das schnelle Modell nach einer einzeiligen Zusammenfassung im Stil von „Wo habe ich aufgehört“. Es gibt { sessionId, recap: string | null } zurück; null bedeutet, dass die Historie zu kurz war oder das Modell vorübergehend fehlgeschlagen ist. Dieser Endpoint ist Best-Effort.

Session BTW / Side Question (session_btw Capability-Tag)

POST /session/:id/btw stellt eine einmalige Frage im Session-Kontext, ohne den Hauptkonversationsfluss zu unterbrechen. Es verwendet runForkedAgent auf dem Cache-Pfad für einen Single-Turn-LLM-Aufruf ohne Tool-Nutzung und gibt { sessionId, answer: string | null } zurück. Die Implementierung erzwingt BTW_MAX_INPUT_LENGTH, Cross-Session-Leakage-Guards und Timeout-Handling.

Shell Command Execution

POST /session/:id/shell führt einen Shell-Befehl direkt auf dem Daemon-Host aus, ohne Routing über das LLM. Es streamt die Ausgabe über den Session-SSE-Bus via user_shell_command / user_shell_result Events und injiziert den Befehl sowie das Ergebnis in den LLM-Konversationsverlauf. Die Antwort ist { exitCode, output, aborted }. Für eine Live-Secondary-Workspace-Session löst die singuläre REST-Route den Session-Owner auf und führt auf der Bridge dieser Runtime aus, sodass der Befehl im Workspace-Cwd des Owners startet. Die Route stellt keine Pfad-Sandbox bereit. Workspace-qualifizierte ACP-Clients können weiterhin _qwen/session/shell auf der Owner-Workspace-Verbindung verwenden.

Session Rewind

GET /session/:id/rewind/snapshots und POST /session/:id/rewind lösen die besitzende Live-Workspace-Runtime auf. Persistierte Sessions müssen vor dem Rewind geladen oder fortgesetzt werden. Rewind kürzt den Konversationsverlauf und stellt optional Dateien wieder her, die von edit und write_file getrackt werden; er macht Shell-Befehle, Git, Skripte oder manuelle Änderungen nicht rückgängig. Die Dateiwiederherstellung ist Best-Effort, sodass eine Antwort rewound: false und filesFailed[] melden kann, nachdem der Konversationsverlauf bereits verschoben wurde. SDK-Rewind-Aufrufe verwenden immer Owner-aware-REST, auch wenn der Client ansonsten ACP-Transport nutzt, weil die Mutation eine strikte REST-Authentifizierung beibehalten muss.

Session Detach

POST /session/:id/detach trennt einen Client explizit von einer Session, indem attachCount dekrementiert wird; es schließt die Session nicht von selbst. Wenn kein weiterer Attach oder Subscriber verbleibt, wird die Session bereinigt. Der Endpoint gibt 204 zurück.

Batch Session Delete

POST /sessions/delete akzeptiert { sessionIds: string[] } (bis zu 100 IDs), schließt Bridge-Sessions und löscht aktive oder archivierte Transcript-Dateien. Wenn für dieselbe ID sowohl aktive als auch archivierte JSONL-Dateien existieren, entfernt ein Hard Delete beide, damit Betreiber den Konflikt bereinigen können. Es bereinigt aktive und archivierte Worktree-Sidecars, lässt aber File-History-Snapshots, Subagent-Transcripts und Runtime-Sidecars intakt. Es verwendet Promise.allSettled für die Resilienz und gibt { removed, notFound, errors } zurück.

Session Archive

POST /sessions/archive verschiebt inaktive Session-JSONL-Dateien von chats/ nach chats/archive/. Wenn die Ziel-Session live ist, durchläuft der Daemon zunächst ein sessionspezifisches Archive-Gate und führt ein striktes Schließen durch, das erfordert, dass das ACP-Child den ChatRecordingService flush; der Archivierungsvorgang belässt die JSONL an ihrem Platz, wenn das Schließen oder Flush fehlschlägt.

POST /sessions/unarchive verschiebt archivierte JSONL-Dateien zurück nach chats/. Dies ist nur eine Storage-State-Transition; Clients müssen danach session/load oder session/resume aufrufen. Archivierte Sessions geben 409 session_archived für Load/Resume zurück, und Mutationen, die mit einem Archivierungsübergang konkurrieren, geben 409 session_archiving zurück.

Context Usage (session_context_usage Capability-Tag)

GET /session/:id/context-usage gibt die strukturierte Context-Window-Nutzung zurück. ?detail=true schließt eine feiner granulierte Nutzung ein, gruppiert nach Tool, Memory und Skill.

Session Stats (session_stats Capability-Tag)

GET /session/:id/stats gibt Nutzungsstatistiken zurück: Modell-Metriken (Input/Output-Tokens, Cache-Reads/Writes, Gesamtkosten), Anzahl der Tool-Calls und Latenzen pro Tool, Anzahl der File-Edits und Anzahl der Skill-Invocations pro Skill für die Live-Session. Der skills-Block spiegelt nur Skill-Body-Loads und Skill-Slash-Commands innerhalb dieser Session wider; er ist kein sessionübergreifendes Aktivitätsaggregat.

Session Tasks (session_tasks Capability-Tag)

GET /session/:id/tasks gibt einen Background-Task-Snapshot für Agent-Tasks, Shell-Tasks, Monitor-Tasks und deren Lifecycle-States zurück. Agent-Einträge, die von einem anderen Sub-Agenten erzeugt wurden, tragen optionale Lineage-Felder (parentAgentId, parentName, depth), sodass Clients verschachtelte Sub-Agenten als Baum rendern können; siehe das Payload-Beispiel in qwen-serve-protocol.md.

Die session_monitor_tool_correlation-Capability garantiert zusätzlich, dass Monitor-Einträge toolUseId tragen, sodass Clients einen Transkript-Tool-Call mit seinen Task-Details korrelieren können.

Session LSP Status (session_lsp Capability-Tag)

GET /session/:id/lsp gibt den bereinigten, sessionspezifischen LSP-Status für Daemon-Clients zurück: Enablement-Status, aggregierte Server-Anzahlen, Unavailable/Initialization-Status sowie pro-Server name, status, languages, transport, command und error. Deaktivierter oder nicht verfügbarer LSP wird als HTTP-200-Statusdaten dargestellt, nicht als Transport-Fehler.

Compacted Replay

POST /session/:id/load gibt nun eine BridgeRestoredSession zurück, die compactedReplay?: BridgeEvent[], liveJournal?: BridgeEvent[] und lastEventId?: number enthalten kann. Diese Felder sind das begrenzte In-Memory-Replay-Fenster des Daemons für eine Live-Session, keine vollständige Transkript-API. Der Standard-Fenster-Cap beträgt 4 MiB pro Live-Session (--compacted-replay-max-bytes), und der Boot weist ungültige Caps zurück; die harte Obergrenze liegt bei 256 MiB. compactedReplay wird von der TurnBoundaryCompactionEngine erzeugt: An Turn-Grenzen faltet sie aufeinanderfolgende Text-/Thought-Blöcke, komprimiert Tool-Call-Sequenzen auf ihren Endzustand, verwirft transiente Signale und erzeugt O(turns) Replay-Logs anstelle von O(tokens) Logs (typischerweise eine 25-30-fache Reduzierung). Wenn ältere Replay-Einträge aus diesem Byte-Fenster entfernt wurden, ist compactedReplay[0] ein synthetischer ID-loser history_truncated-Marker mit {reason: 'replay_window_exceeded', truncatedEvents, retainedEvents, maxBytes, truncatedTurns?, fullTranscriptAvailable: boolean}. fullTranscriptAvailable ist ein Capability-Flag: true bedeutet, dass der Client das vollständige persistierte Transkript mit GET /session/:id/transcript seitenweise abrufen kann, während false bedeutet, dass nur das begrenzte Replay verfügbar ist. Clients sollten es als Status darstellen und das behaltene Replay normal anwenden; es darf keinen Resync-Loop auslösen.

ACP Child Preheat

bridge.preheat() wärmt den ACP-Child-Prozess vor der ersten Session auf, sodass die erste echte Session die Cold-Start-Latenz vermeidet. Es wird gepaart mit channelIdleTimeoutMs, das den ACP-Child nach dem Schließen der letzten Session am Leben hält, und dem Skip-Relaunch-Verhalten, das ein bereits idles Child wiederverwendet, wenn eine neue Session eintrifft.

Konfiguration

  • BridgeOptions.maxSessions (Standard 32) — Obergrenze.
  • BridgeOptions.sessionScope (Standard 'single'; optional 'thread').
  • BridgeOptions.initializeTimeoutMs (Standard 10s) — ACP-initialize-Handshake.
  • BridgeOptions.sessionRestoreTimeoutMs (Standard 60s) — ACP-loadSession / unstable_resumeSession-Deadline. Standardmäßig 60s; eine explizit konfigurierte Initialize-Timeout kann sie erhöhen, aber niemals senken.
  • BridgeOptions.channelIdleTimeoutMs (Standard 0; ACP-Child sofort bereinigen).
  • Capability-Tags: session_create, session_id_override, session_scope_override, session_load, session_resume, unstable_session_resume (deprecated alias), session_list, session_info, session_close, session_metadata, session_set_model, client_identity, client_heartbeat, session_recap, session_generation, session_btw, session_context_usage, session_tasks, session_monitor_tool_correlation, session_stats, session_lsp, session_status, non_blocking_prompt.

Zustandslose Generierung (session_generation Capability-Tag)

POST /session/:id/generate akzeptiert { "prompt": string } und gibt einen request-scoped SSE-Stream mit started, optionalem thinking, delta, done oder error Events zurück. Der Request liest keinen Konversationsverlauf, zeichnet keinen Turn auf und stellt keine Tools bereit. Das ACP-Child verwendet ein gültiges konfiguriertes schnelles Modell, falls verfügbar, und andernfalls das Hauptmodell der Session.

Hinweise & bekannte Grenzen

  • connection.unstable_resumeSession kann auf der ACP-Ebene noch instabil sein, aber der Daemon bietet den festgeschriebenen v1-Route-Contract mit session_resume an. unstable_session_resume wird nur als deprecated Kompatibilitäts-Alias beibehalten.
  • v1 hat keine client-spezifische Eviction; nur session-spezifische und subscriber-spezifische Terminierung. Die Revocation-Policy ist F-Series Wave 5 / PR 24.
  • client_evicted ist subscriber-spezifisch, nicht session-spezifisch. Ein Client, dessen SSE-Subscriber evicted wurde, kann sich erneut verbinden.
  • Anonyme Clients (ohne X-Qwen-Client-Id) können unter designated- oder consensus-Policies nicht voten.

Referenzen

  • packages/acp-bridge/src/bridge.ts (SessionEntry-Definition)
  • packages/acp-bridge/src/bridgeTypes.ts (HttpAcpBridge, BridgeSession, BridgeSessionState)
  • packages/sdk-typescript/src/daemon/types.ts (DaemonSession)
  • packages/sdk-typescript/src/daemon/DaemonSessionClient.ts
  • Wire-Referenz: ../qwen-serve-protocol.md (Route-Katalog).
Last updated on