Daemon-Modus (qwen serve)
Führe Qwen Code als lokalen HTTP-Daemon aus, damit mehrere Clients (IDE-Plugins, Web-UIs, CI-Skripte, benutzerdefinierte CLIs) eine einzige Agent-Sitzung über HTTP + Server-Sent Events teilen können, anstatt dass jeder Client seinen eigenen Subprozess startet.
🚧 v0.16-alpha:
qwen servewird in v0.16-alpha erstmals auf npm als reiner Text-Chat / Coding mit lokalem Deployment veröffentlicht. Bild-/Dateianhänge im Prompt-Pfad, containerisiertes Deployment (Docker / k8s / nginx Reverse-Proxy) sowie Remote-/Multi-Daemon-Härtung folgen in einem späteren Patch, sobald ein Enterprise-Pilot fest zugesagt ist. Die vollständige Liste der zurückgestellten Features findest du unter v0.16-alpha known limits.
Status: Stage 1 (experimentell). Die Protokoll-Oberfläche ist in der §04-Routen-Tabelle aus Issue #3803 festgeschrieben. Stage 1.5 (
qwen --serveFlag — TUI hostet denselben HTTP-Server) und Stage 2 (In-Process-Refactoring +mDNS/OpenAPI/WebSocket/Prometheus-Polish) sind die unmittelbaren nächsten Schritte.Scope-Transparenz: Stage 1 ist auf Entwickler ausgelegt, die Clients gegen die Protokoll-Oberfläche prototypen, sowie auf lokale Single-User- / Small-Team-Kollaboration. Produktionsreife Multi-Client- / Langzeit- / Netzwerk-instabile Workloads (Mobile Companions, IM-Bots mit 1000+ Chats) benötigen die Garantien von Stage 1.5+, die in diesem Release noch nicht enthalten sind. Siehe Stage 1.5+ runtime guarantees für die vollständige Liste der Lücken und #3803 für die Convergence-Roadmap.
Was es dir bietet
- Integrierte Web-Shell-UI —
qwen servestellt die browserbasierte Web-Shell standardmäßig unter seiner Root-URL (http://127.0.0.1:4170/) bereit; starteqwen serve --open, um sie automatisch im Browser zu öffnen. Sie wird auf derselben Origin wie die API ausgeliefert, sodass kein zweiter Port oder Reverse-Proxy benötigt wird. Übergib--no-webfür einen reinen API-Daemon. - Bis zu ein primäres ACP-Child plus ein On-Demand-Child pro vertrauenswürdigem Secondary, viele Clients — Production versucht, die primäre Bridge vorzuwärmen und wiederholt den ersten Versuch nach einem Fehlschlag; vertrauenswürdige Secondary-Runtimes starten ihr eigenes Child bei Bedarf, während nicht vertrauenswürdige Secondaries niemals eines starten. Unter dem Standard-
sessionScope: 'single'teilen sich Clients, die auf denselben Workspace abzielen, eine ACP-Sitzung und arbeiten an derselben Konversation, denselben Datei-Diffs und denselben Permission-Prompts zusammen. - Reconnect-sicheres Streaming — SSE mit
Last-Event-IDReconnect ermöglicht es einem Client, die Verbindung zu trennen und exakt dort wieder aufzunehmen, wo er aufgehört hat (innerhalb des Replay-Fensters des Rings). - Paginierte persistierte Transkripte —
GET /session/:id/transcriptgibt das vollständige aktive persistierte Transkript als Replay-Seiten zurück, ohne einen Client anzuhängen oder das Live-SSE-Replay-Fenster zu verändern. - First-Responder-Permissions — wenn der Agent die Erlaubnis anfordert, ein Tool auszuführen, sieht jeder verbundene Client die Anfrage; der Client, der zuerst antwortet, erhält den Zuschlag.
- Ein Daemon, ein oder mehrere Workspaces — wiederhole
--workspace, um isolierte Workspace-Runtimes unter einem Listener zu registrieren. Der erste Workspace ist primär und bleibt der Standard für Anfragen, diecwdweglassen. - Experimentelle, daemon-verwaltete Channels — starte mit
qwen serve --channel <name>, oder starte ohne Channel und wähle später einen mitqwen channel set. Worker sind separate Prozesse, die vom Daemon-Lifecycle besessen werden. Ihre Auswahl kann abgefragt, ersetzt, neu geladen und gestoppt werden, ohne den Daemon neu zu starten. - Remote-Runtime-Steuerung — ändere den Approval-Modus einer Sitzung (
POST /session/:id/approval-mode), schalte ein Tool (POST /workspace/tools/:name/enable) oder einen geladenen Skill (POST /workspace/skills/:name/enable) pro Workspace um, erstelle eine leereQWEN.md(POST /workspace/init, nur mechanisch — ruft NICHT das Modell auf; für KI-Befüllung folge mitPOST /session/:id/prompt), starte einen einzelnen MCP-Server mit einer Budget-Vorabprüfung neu (POST /workspace/mcp/:server/restart) oder füge MCP-Server zur Laufzeit hinzu/entferne sie ohne Daemon-Neustart (POST /workspace/mcp/servers,DELETE /workspace/mcp/servers/:name). Alles streng limitiert — konfiguriere zuerst--token. - Session-Recap (#4175 Follow-up) — rufe eine einzeilige “Wo habe ich aufgehört”-Zusammenfassung einer aktiven Sitzung ab (
POST /session/:id/recap). Wrapper fürgenerateSessionRecapaus dem Core als Side-Query gegen das schnelle Modell; verunreinigt weder den Haupt-Chat-Verlauf noch den SSE-Stream. Non-strict Gate (gleiche Haltung wie/prompt); SDK-Helperclient.recapSession(sessionId).- Bekannte Einschränkung — Token-Kosten-Verstärkung: Die Route ist ein reiner Kosten-Endpunkt (jeder Aufruf ist eine LLM-Side-Query, kein State-Nutzen) und der Daemon hat in v1 kein Rate-Limit pro Route. Auf einem No-Token-Loopback-Default kann ein fehlerhafter oder bösartiger lokaler Client sie spammen, um Token zu verbrennen. Konfiguriere
--token(und optional--require-auth) auf gemeinsamen Dev-Hosts, bevor du den Daemon freigibst. - Sicherheit bei gleichzeitigen Recaps: Zwei gleichzeitige
/recap-Aufrufe auf derselben Sitzung führen zwei unabhängige Side-Queries aus.generateSessionRecapliest einen Snapshot des Chat-Verlaufs überGeminiClient.getChat().getHistory()und füttert ihn an einen separatenBaseLlmClient.generateText-Aufruf (viarunSideQuery); es hängt nichts an oder mutiert dasGeminiChatder Sitzung. Kann sicher von mehreren Clients ohne Koordination aufgerufen werden.
- Bekannte Einschränkung — Token-Kosten-Verstärkung: Die Route ist ein reiner Kosten-Endpunkt (jeder Aufruf ist eine LLM-Side-Query, kein State-Nutzen) und der Daemon hat in v1 kein Rate-Limit pro Route. Auf einem No-Token-Loopback-Default kann ein fehlerhafter oder bösartiger lokaler Client sie spammen, um Token zu verbrennen. Konfiguriere
Bekannte Einschränkungen in v0.16-alpha
Die erste npm-Veröffentlichung von qwen serve (v0.16-alpha) ist absichtlich eng gefasst — reiner Text-Chat / Coding für Entwickler, die den Daemon auf ihrem eigenen Rechner ausführen. Die folgende Liste macht die zurückgestellte Oberfläche explizit, damit Adopters darum herum planen können; alles hier steht auf der v0.16.x-Patch-Roadmap oder einem kurzfristigen Follow-up-Release.
Produkt-Oberfläche — nur Text:
- ✅ Text-Prompts und Text-Antworten (Chat, Coding, Tool-Calls, MCP-Integration)
- ❌ Bild-/Dateianhänge im Prompt-Pfad —
MessageEmitterrendert derzeit nur Text; multimodales Echo kommt, wenn ein Alpha-Ziel mit Bildbedarf fest zugesagt ist (#4175 chiga0 #27 P0 item) - ❌ Streaming-Uploads — gleiche Abhängigkeit wie bei Multimodal
Deployment-Oberfläche — nur lokal:
- ✅ Loopback (
127.0.0.1, Standard) — keine Auth erforderlich, geeignet für Dev-Workstations - ✅ Lokaler Start via
systemd/launchd/nohup &/tmux— siehe Local launch templates - ✅ Bring-Your-Own-Bearer-Token via
QWEN_SERVER_TOKENUmgebungsvariable (Authentication für das Setup) - ❌ Containerisiertes Deployment — Docker / Compose / Kubernetes / nginx Reverse-Proxy mit TLS-Terminierung NICHT in v0.16-alpha. Wird auf v0.16.x verschoben, sobald ein Enterprise-Pilot fest zugesagt ist (würde sonst verrotten, weil niemand es validiert).
- ❌ Multi-Daemon-Koordination auf einem Host — ein Daemon kann mehrere explizit registrierte Workspaces hosten, aber Daemons koordinieren sich nicht untereinander. Cross-Host-Föderation, Instance-Path-Token-Keying und Stale-Token-Bereinigung werden auf v0.16.x verschoben.
- ✅ Revokierbare Local-Control-Pairing-Tokens —
--local-controlprägt ein separates LAN-Pairing-Token, das im Besitz des Daemons ist. Die allgemeine Daemon-Token-Speicherung bleibt BYO-Token.
Härtung — minimal überlebensfähig für lokalen Single-User:
- ✅ Security-Gate beim Boot (lehnt Non-Loopback-Bind ohne Token ab, PR 15 / #4236 )
- ✅ Auth-Gate für Mutations-Routen, sitzungsbezogenes Permission-Routing (Wave 4 PRs)
- ✅ MCP-Guardrails + Multi-Client-Permission-Koordination (F2 / F3)
- ✅ Prompt Absolute Deadline + SSE-Writer-Idle-Timeout — Opt-in via
--prompt-deadline-msund--writer-idle-timeout-ms; wird bei Aktivierung überprompt_absolute_deadlineundwriter_idle_timeoutbekannt gegeben. - ✅ HTTP-Rate-Limiting — Opt-in via
--rate-limitund schwellenwertbasierte Limits pro Stufe; wird bei Aktivierung überrate_limitbekannt gegeben. - ⏸️ Prometheus-Metriken + Load-Test-Harness — wird auf v0.17 F4 Phase-1 Scale-Instrumentation verschoben, wenn 30-50 aktive Sitzungen ein echtes Ziel werden.
- ⏸️
--max-body-sizeCLI-Flag — Daemon erzwingt standardmäßigexpress.json({ limit: '10mb' }), was für reine Text-Prompts bequem ausreicht (Modell-Kontextfenster liegen weit unter 10 MiB an Zeichen). In v0.16.x über Flag anpassbar.
Für die tiefere Aufzählung von “was wir in Stage 1 nicht beheben” (Single-Host-Session-State-Mutationsmodell + N-parallele-Sitzungen, die sich ein ACP-Child teilen), siehe Stage 1 scope boundaries weiter unten.
Quickstart
1. Daemon starten (Loopback, keine Auth)
cd your-project/
qwen serve
# → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/path/to/your-project)
# → qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.Der Standard-Bind ist 127.0.0.1:4170. Bearer-Auth ist bei Loopback ausgeschaltet, damit die lokale Entwicklung “einfach funktioniert”. Der Daemon registriert das aktuelle Arbeitsverzeichnis als seinen primären Workspace; verwende ein absolutes --workspace /path/to/dir, um ihn zu überschreiben, und wiederhole das Flag, um zusätzliche isolierte Runtimes zu registrieren.
Web-Shell-UI öffnen. Navigiere zu http://127.0.0.1:4170/ (oder starte den Daemon mit qwen serve --open, um sie automatisch zu öffnen) für das vollständige Browser-Terminal — Chat, Diffs, Commit-History, Tool-Calls und Permission-Prompts. Die UI wird am Daemon-Root auf derselben Origin wie die API ausgeliefert. Der Rest dieses Guides verwendet rohes HTTP, damit du direkt gegen die API skripten kannst.
Für einen authentifizierten Single-User-Start ohne manuelles Erstellen eines Tokens, opt-in explizit:
qwen serve --open-with-authDieser Loopback-only-Modus generiert ein 256-Bit-Bearer-Token, wenn weder --token noch QWEN_SERVER_TOKEN eines bereitstellen, und übergibt es dann der geöffneten Web-Shell als #token= URL-Fragment. Die Shell entfernt das Fragment und hält die Credentials im sessionStorage dieses Tabs; Aktualisieren funktioniert, aber das Schließen des Tabs oder ein Neustart des Daemons verwirft die Credentials. In CI, SSH oder einer anderen Umgebung, in der Auto-Open nicht verfügbar ist, startet der Daemon und gibt die Fragment-URL zum manuellen Öffnen aus. Die ausgegebene URL enthält Secrets.
Das Flag ist standardmäßig aus, beinhaltet das Browser-Öffnen-Verhalten und erfordert die Vorhandensein der Web-Shell, gebauter Web-Shell-Assets und eines Loopback-Binds. Bare qwen serve --open bleibt token-less auf Loopback. Im Authenticated-Open-Modus lehnen normale API-Routen andere lokale Clients ohne Bearer ab; statische Web-Shell-Assets und Loopback-/health behalten ihr bestehendes vor-authentifiziertes Verhalten, außer --require-auth ist ebenfalls gesetzt. Für mehrere Clients oder eine wieder öffnbare Web-Shell verwende stattdessen ein stabiles Shared-Token:
export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
qwen serve --openDas Flag --open-with-auth ist ein CLI-eigener Komfort, keine weitere Daemon-Token-Quelle: es wählt --token, wenn diese Option definiert ist (auch wenn leer), andernfalls QWEN_SERVER_TOKEN, trimmt dann den ausgewählten Wert und generiert nur, wenn das Ergebnis leer ist. Der Daemon persistiert den generierten Wert nicht und exportiert ihn nicht als QWEN_SERVER_TOKEN; die bestehende interne Authenticated-Child-Übergabe bleibt unverändert. Die Web-Shell speichert ihre Browser-Kopie nur im sessionStorage des empfangenden Tabs; dieser Modus fügt keinen Cross-Tab- oder External-Client-Credential-Discovery-Mechanismus hinzu. Das Token ist nicht unabhängig widerrufbar oder an eine Client-Identität gebunden. Besitz gewährt dieselbe Daemon-Autorität wie jedes andere Bearer-Token. Siehe das authentifizierte Web-Shell-Launch-Design und verwandte zukünftige Arbeit in #4514 .
2. Sanity-Check
curl http://127.0.0.1:4170/health
# → {"status":"ok"}
curl http://127.0.0.1:4170/capabilities
# → {"v":1,"mode":"http-bridge","features":["health","daemon_status","capabilities","session_create",...],"workspaceCwd":"/path/to/your-project"}
curl http://127.0.0.1:4170/daemon/status
# → {"v":1,"detail":"summary","status":"ok","runtime":{...}}Das Feld workspaceCwd macht den primären Kompatibilitäts-Workspace sichtbar, sodass Clients absichtlich cwd bei POST /session weglassen können. Aktuelle Clients sollten einen vertrauenswürdigen Eintrag aus workspaces[] auswählen und das cwd dieses Eintrags senden, wenn sie explizit eine Runtime ansprechen.
Das Feld limits.maxPendingPromptsPerSession gibt das aktive Prompt-Zulassungslimit pro Sitzung an; null bedeutet, dass das Limit deaktiviert ist. limits.maxTotalSessions gibt das optionale Daemon-weite Limit für neue Sitzungen an; null bedeutet unbegrenzt.
Channels vom Daemon aus ausführen
# Start one configured channel under qwen serve
qwen serve --channel telegram
# Start several configured channels under daemon-owned workspace workers
qwen serve --channel telegram --channel feishu
# Start all configured channels
qwen serve --channel all
# Or start a token-protected daemon with no channel worker
QWEN_SERVER_TOKEN=secret qwen serve
# Enable or replace its runtime selection later
qwen channel set telegram --token secret
qwen channel set telegram feishu --token secret
qwen channel set all --token secret
# Inspect or stop daemon-managed channels
qwen channel status --daemon-url http://127.0.0.1:4170 --token secret
qwen channel stop --daemon-url http://127.0.0.1:4170 --token secretDieser Modus ist experimentell und daemon-verwaltet. Er ersetzt nicht den eigenständigen Befehl qwen channel start: ohne --daemon-url bleibt das bestehende Verhalten von qwen channel start, stop und status eigenständig. Mit qwen serve --channel reserviert der Daemon die Channel-Service-Lease vor dem Lauschen und schlägt beim Start fehl, wenn der initiale Worker nicht bereit werden kann. Wenn ein bereiter Worker später abstürzt, läuft der Daemon weiter, startet ihn unter einer begrenzten Neustart-Policy neu und meldet seinen Zustand (einschließlich channel_worker_exited-Warnungen) in GET /daemon/status. Ohne --channel lädt er keine Channel-Runtime und reserviert keine Channel-Service-Lease bis zum ersten Runtime-PUT.
Die Runtime-Steuerung wird als GET, PUT und DELETE /workspace/channel exposed; SDK-Helper sind getChannelWorkerControl(), setChannelWorkerSelection() und stopChannelWorker(). PUT/DELETE/Reload verwenden das strikte Mutations-Gate, daher muss der Daemon einen Bearer-Token konfiguriert haben. Runtime-Auswahlen sind absichtlich ephemer: PUT bearbeitet nicht die Settings oder Boot-Optionen, und ein Neustart kehrt zur Auswahl von qwen serve --channel zurück (oder deaktiviert, wenn das Flag weggelassen wurde). Benannte Auswahlen werden gekürzt und in Reihenfolge des ersten Vorkommens dedupliziert; die Reihenfolge bleibt erhalten, da der erste Channel die gemeinsame Modellauswahl beeinflussen kann.
Daemon-gestützte qwen channel set und qwen channel reload sowie status und stop mit --daemon-url können ein durch --open-with-auth generiertes Token nicht entdecken. Verwende QWEN_SERVER_TOKEN und übergebe denselben Wert mit --token an diese Clients, oder lasse den Authenticated-Open-Modus weg.
Der Daemon liest die Settings jedes Channels (Tokens, proxy, model pro Channel) beim Start seines Workers. Um Settings neu zu lesen, ohne die festgelegte Auswahl zu ändern, rufe POST /workspace/channel/reload auf (SDK client.reloadChannelWorker() oder qwen channel reload). Reload löst die Workspace-Zugehörigkeit neu auf und startet ausgewählte Worker über denselben Rollback-sicheren reconcile-Pfad neu. Die channel_control-Capability ist vorhanden, wenn die Runtime-Steuerung verdrahtet ist; channel_reload ist nur vorhanden, während der Manager aktiviert ist. Persistierte Threads werden von der Festplatte wiederhergestellt.
Das cwd jedes ausgewählten Channels muss auf einen registrierten Workspace auflösen, und Channels werden nach diesem besitzenden Workspace gruppiert: Ein Single-Workspace-Daemon betreibt einen Worker (unverändert wie zuvor); ein Multi-Workspace-Daemon (--workspace wiederholt) betreibt einen Worker pro Workspace, der einen ausgewählten Channel besitzt, jeweils gebunden an das cwd, QWEN_DAEMON_WORKSPACE und das Env-Overlay des Workspaces. Um einen Channel in einem nicht-primären Workspace zu hosten, definiere ihn in der eigenen .qwen/settings.json des Workspaces (kein cwd erforderlich) oder setze ein explizites cwd gleich dem Workspace-Pfad; ein Channel, der nur im Benutzer-/System-Scope ohne cwd definiert ist, ist über Workspaces hinweg mehrdeutig und verursacht einen Boot-Fehler. --channel all bleibt primär-only (es hostet die Channels des primären Workspaces) und kann nicht mit benannten Channels kombiniert werden.
Das Ersetzen einer Auswahl prüft Konfiguration, Zugehörigkeit und Trust, bevor etwas gestoppt wird. Es behält Workspace-Worker bei, deren geordnete Auswahl unverändert ist. Wenn ein geänderter Worker nicht starten kann, stoppt der Daemon neue Worker und stellt die alte Auswahl wieder her. Wenn der Daemon nicht bestätigen kann, dass ein altes Kind selbst nach SIGKILL beendet wurde, behält es das PID-Lease und verweigert die Erstellung eines doppelten Workers. Ein Worker gilt weiterhin als bereit, wenn mindestens ein angeforderter Adapter verbunden ist; PUT gibt dann partial: true zurück, und /daemon/status meldet channel_worker_partial_connect für die fehlenden Adapter.
Wenn ein Adapter connect() ablehnt, können aktuelle Worker-Snapshots startupFailures-Einträge mit dem Channel, phase: "connect", einem optionalen Adapter-Code und einer Credential-bereinigten Nachricht enthalten. qwen channel set, qwen channel reload und Remote-qwen channel status --daemon-url … geben diese Gründe aus. Wenn jeder Adapter bei einem dynamischen Set oder Reload fehlschlägt, erhält der Befehl 502 channel_worker_start_failed; die Antwortgründe beschreiben diesen Versuch und sein state beschreibt das Ergebnis nach dem Rollback. Der fehlgeschlagene Versuch wird von späteren Statusanfragen nicht beibehalten. Maximal 64 Gründe werden pro Worker-Start beibehalten, und Adapter-Codes sollten als diagnostisch und nicht als stabile Kategorien behandelt werden. Der initiale Start von qwen serve --channel … beendet sich weiterhin, wenn kein Adapter verbunden wird.
Der Daemon stellt auch schreibgeschützte Runtime-Snapshots für Client-UIs und Operatoren bereit:
GET /daemon/status, GET /workspace/mcp,
GET /workspace/skills, GET /workspace/providers, GET /workspace/env,
GET /workspace/preflight,
GET /workspace/:id/session-info,
GET /session/:id/status, GET /session/:id/context,
GET /session/:id/supported-commands, und
GET /session/:id/tasks, GET /session/:id/lsp, und
GET /session/:id/transcript.
GET /workspace/:id/session-info (und der plurale Zwilling
GET /workspaces/:workspace/session-info) gibt aggregierte Session-
Counts für einen Workspace zurück: persistierte active / archived /
total, plus den aktuellen In-Memory-live-Count, wenn Live-State
verfügbar ist. Registrierte nicht vertrauenswürdige Secondary-Workspaces
lassen live weg, da ihre Katalog-Lesungen die Live-Bridge nicht abfragen.
Die paginierte Liste GET /workspace/:id/sessions enthält keine Summe,
daher ist dies die dedizierte Oberfläche für “Wie viele Sitzungen
existieren?” — nützlich, wenn geplante oder wiederkehrende Aufgaben einen
großen lokalen Speicher hinterlassen.
⚠️ Disk-Scan — nicht pollen. Dieser Endpunkt durchläuft lokale Session-JSONL-Dateien unter dem Workspace-Chats-Verzeichnis. Antworten enthalten immer
expensive: trueundcost: "disk_scan". Rufe ihn selten auf (manuelles Neuladen, Operator-Tooling, gelegentliches UI- Laden) — niemals auf einem engen Timer oder bei jedem Sidebar-Render. BevorzugeGET /workspace/:id/sessionszum Durchsuchen von Seiten undGET /daemon/statusfür Live-In-Memory-Session-Counts. Eine Antwort mittruncated: truebedeutet, dass der Scan sein Sicherheitslimit erreicht hat oder nicht jede Kandidatendatei klassifizieren konnte, sodass die persistierten Counts Untergrenzen sind.
curl http://127.0.0.1:4170/workspace/$(python3 -c "import urllib.parse,os; print(urllib.parse.quote(os.getcwd(), safe=''))")/session-info
# → {"active":450,"archived":30,"total":480,"live":2,"expensive":true,"cost":"disk_scan"}GET /session/:id/status gibt die Live-Bridge-Zusammenfassung für eine einzelne Sitzung zurück:
sessionId, workspaceCwd, createdAt, optionales displayName, clientCount,
und hasActivePrompt. Es antwortet mit 200 und der Zusammenfassung, wenn der Daemon eine Live-Sitzung mit dieser ID hält, und andernfalls mit 404 (Body { "error": …, "sessionId": … }). Nutze es, um zu pollen, ob eine bekannte Sitzung noch läuft
(hasActivePrompt) oder wie viele Clients verbunden sind (clientCount), ohne die gesamte paginierte Sitzungsliste abrufen und durchsuchen zu müssen:
curl http://127.0.0.1:4170/session/$SESSION_ID/status
# → {"sessionId":"…","workspaceCwd":"…","createdAt":"…","clientCount":1,"hasActivePrompt":false}Dies ist die rohe Live-Sitzungsansicht, sodass clientCount und hasActivePrompt mit dem entsprechenden Eintrag in GET /workspace/:id/sessions übereinstimmen — aber die beiden Routen sind nicht byte-identisch. Der Listen-Endpunkt reichert jedes Element mit persistierten Session-Store-Daten an: sein createdAt ist die persistierte First-Prompt-Zeit, und es fügt updatedAt sowie ein displayName hinzu, das vom gespeicherten Titel oder ersten Prompt abgeleitet wird. /status hingegen meldet das eigene createdAt der Live-Sitzung, lässt updatedAt weg und gibt displayName nur zurück, wenn eines auf der Live-Sitzung gesetzt ist.
GET /session/:id/lsp gibt den strukturierten LSP-Status pro Sitzung zurück. Starte den Daemon mit --experimental-lsp, um LSP in erzeugten Agent-Sitzungen zu aktivieren; andernfalls gibt die Route enabled: false ohne Server zurück.
GET /daemon/status ist das konsolidierte Troubleshooting-Snapshot. Der Standard detail=summary liest nur den In-Memory-Daemon-State (Sitzungen, Permissions, SSE/ACP-Transport-Counts, Rate-Limit-Ablehnungen, Prozess-Speicher, aufgelöste Limits) und startet nicht das ACP-Child. Verwende GET /daemon/status?detail=full für Diagnose-Daten pro Sitzung, ACP-Verbindungsdetails, Auth-Device-Flow-Counts und Workspace-Status-Abschnitte, wenn du aktiv ein Problem untersuchst.
GET /workspace/mcp, GET /workspace/skills und GET /workspace/providers melden die Live-ACP-Runtime und starten das ACP-Child nicht, wenn es im Leerlauf ist; ein inaktiver Daemon gibt initialized: false mit einem leeren Snapshot zurück. Sobald eine Sitzung aktiv ist, wechseln sie zu initialized: true und zeigen den tatsächlichen State an.
Um das CLI-/skills-Panel remote zu spiegeln, rufe POST /workspace/skills/:name/enable mit { "enabled": true | false } auf, nachdem du die workspace_skill_toggle-Capability geprüft hast. Um mehrere Skills zu ändern, prüfe workspace_skill_batch_toggle und rufe POST /workspace/skills/enable mit { "skillNames": ["review", "deploy"], "enabled": false } auf; die Antwort trennt erfolgreiche results von zielbezogenen errors, persistiert gültige Ziele zusammen und aktualisiert aktive ACP-Sitzungen einmalig. Die Routen aktualisieren skills.disabled und skills.enabled des Workspaces nach Bedarf, lehnt unbekannte, versteckte, inaktive-Extension-, höherer-Scope-gesperrte und nicht vertrauenswürdige Ziele ab. Das Aktivieren eines skills.defaultDisabled-Skills schreibt ein kanonisches Opt-In in skills.enabled; ein harter skills.disabled-Eintrag, der von einem höheren Scope geerbt wurde, kann weiterhin nicht überschrieben werden. Skill-Status-Zellen legen disabledReason (hard, default oder inactive_extension) und ein optionales lockedScope offen. Eine deferred-Antwort bedeutet, dass die Einstellung gespeichert wurde, während kein ACP-Child lief; sie wird angewendet, wenn das Child startet. skills.disabled deaktiviert sowohl manuelle als auch Modell-Nutzung, im Gegensatz zu disable-model-invocation: true, das die direkte /skill-name-Invocation verfügbar hält. Für V2-Extension-Batches prüfe extension_batch_activation_v2: PUT /extensions/activation ändert globale Standards, während PUT /workspaces/:workspace/extensions/activation genaue Overrides für den ausgewählten Workspace ändert und "inherit" zum Löschen akzeptiert. Beide akzeptieren Namen in extensionNames; enabled und disabled können vor der Installation deklariert werden, während inherit für einen unbekannten Namen ein No-Op ist. Jede Anfrage gibt eine Operation zum Pollen zurück.
GET /workspace/env und GET /workspace/preflight antworten immer mit initialized: true, unabhängig vom ACP-State. env konsultiert niemals ACP (nur Daemon-Prozess-Infos); preflight antwortet mit Daemon-Level-Zellen aus process.* und gibt status: 'not_started'-Platzhalter für ACP-Level-Zellen aus, wenn das Child im Leerlauf ist.
GET /workspace/env meldet die Runtime, Plattform, Sandbox, Proxy und das Vorhandensein (niemals den Wert) von gewhitelisteten Secret-Umgebungsvariablen wie OPENAI_API_KEY des Daemon-Prozesses. Proxy-URLs werden vor dem Versand um Credentials bereinigt und auf host:port reduziert. Die Route antwortet immer direkt aus dem Daemon-Prozess und erzeugt niemals ein ACP-Child.
GET /workspace/preflight gibt eine Liste von Readiness-Checks zurück. Daemon-Level-Zellen (Node-Version, CLI-Entry, Workspace-Verzeichnis, ripgrep, git, npm) werden immer gerendert. ACP-Level-Zellen (Auth, MCP-Discovery, Skills, Providers, Tool-Registry, Egress) erfordern ein aktives ACP-Child — wenn der Daemon im Leerlauf ist, geben sie status: 'not_started'-Platzhalter aus, anstatt ACP nur zu ihrer Befüllung zu starten. Fehler werden auf eine geschlossene errorKind-Enum abgebildet (missing_binary, auth_env_error, init_timeout, restore_timeout, protocol_error, missing_file, parse_error, blocked_egress), damit Client-UIs strukturierte Abhilfen rendern können.
Der Daemon stellt auch Workspace-Datei-Helper bereit:
GET /fileliest Textdateien. Full-Snapshot-Antworten geben einen Raw-Byte-sha256:<hex>-Hash zurück; Endlich-Zeilen-Fenster aus Dateien über 256 KiB lassen ihn weg.GET /file/bytesliest begrenzte Raw-Byte-Fenster und gibt Base64-Content zurück.POST /file/writeerstellt oder ersetzt Textdateien.POST /file/editwendet genau eine Textersetzung an.
Write/Edit sind strikte Mutations-Routen: Auch bei Loopback erfordern sie einen konfigurierten Bearer-Token, andernfalls geben sie token_required zurück. Ersetzungen und Edits erfordern den neuesten expectedHash von einem Full-Snapshot-GET /file (oder einem Full-Window-GET /file/bytes). Ein partielles Großdatei-Fenster kann nicht als Optimistic-Concurrency-Token verwendet werden. create überschreibt niemals. Explizite Writes auf ignorierte Pfade sind erlaubt, werden aber auditiert. Binary-Writes, Delete/Move/Mkdir und rekursive Parent-Erstellung sind nicht Teil dieser Oberfläche.
3. Sitzung öffnen
curl -X POST http://127.0.0.1:4170/session \
-H 'Content-Type: application/json' \
-d '{}'
# → {"sessionId":"<uuid>","workspaceCwd":"…","attached":false}cwd kann weggelassen werden — die Route fällt auf den primären Workspace des Daemons zurück. Das Posten eines cwd, das nicht zu einem registrierten Workspace kanonisiert werden kann, gibt 400 workspace_mismatch zurück.
Ein zweiter Client, der für dieselbe aufgelöste Workspace-Runtime an /session postet, erhält "attached": true unter dem Standard-sessionScope: 'single' — er teilt sich nun die Agent-Sitzung dieser Runtime. Das Weglassen von cwd löst sich auf primär auf; die Auswahl eines anderen registrierten Workspaces erstellt oder hängt sich an die separate Standard-Sitzung dieser Runtime an.
4. Event-Stream abonnieren (zuerst in einem anderen Terminal)
SESSION_ID="<from step 3>"
curl -N http://127.0.0.1:4170/session/$SESSION_ID/events
# → id: 1
# event: session_update
# data: {"id":1,"v":1,"type":"session_update","data":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"…"}}}Die data:-Zeile ist das vollständige Event-Envelope — {id?, v, type, data, originatorClientId?} — als JSON-String in einer einzigen Zeile. Die ACP-Payload (der sessionUpdate-Block in diesem Beispiel) befindet sich innerhalb dieses Envelopes unter data. Die SSE-Level-id:- / event:-Zeilen sind eine Erleichterung für EventSource-Clients; dieselben Werte erscheinen auch im JSON-Envelope, sodass auch Raw-fetch-Consumer sie erhalten.
Öffne dies bevor du den Prompt sendest — der SSE-Replay-Buffer hält die letzten 8000 Events vor, sodass ein später Subscriber über Last-Event-ID aufholen kann, aber für den einfachen Fall “einen einzelnen Prompt beobachten” ist es am einfachsten, zuerst zu abonnieren und es live streamen zu lassen.
Der Stream emittiert session_update (LLM-Chunks, Tool-Calls, Usage), permission_request (Tool benötigt Approval), permission_resolved (jemand hat abgestimmt), model_switched, model_switch_failed und die Terminal-Frames session_died (Agent-Child abgestürzt — SSE schließt dann) und client_evicted (deine Queue ist übergelaufen — SSE schließt dann).
5. Prompt senden (zurück im ursprünglichen Terminal)
curl -X POST http://127.0.0.1:4170/session/$SESSION_ID/prompt \
-H 'Content-Type: application/json' \
-d '{"prompt":[{"type":"text","text":"What does src/main.ts do?"}]}'
# → {"stopReason":"end_turn"}Das curl -N aus Schritt 4 gibt die Frames aus, sobald sie eintreffen.
Optional Todo Stop Guard
Langlaufende Daemon-Clients können eine begrenzte Fortsetzung aktivieren,
wenn die aktuelle Arbeitskette erfolgreich eine Top-Level-Todo-Liste
schreibt und dann mit noch ausstehenden oder in Bearbeitung befindlichen
Elementen stoppt. Füge dies zur settings.json hinzu und starte den
Daemon neu:
{
"experimental": {
"todoStopGuard": true
}
}Der Guard fügt höchstens zwei aufeinanderfolgende Primärmodell-Aufrufe
ohne neue Benutzereingabe hinzu. Eine Benutzer-Nachricht während eines
Turns wird zuerst ausgeführt und startet eine neue Zwei-Versuch-Stufe;
Retry/Continue und verwandte Hintergrund-Ergebnisse behalten das Budget
der aktuellen Stufe. Jeder Aufruf und der endgültige Erschöpfungs-Zustand
erscheinen als abspielbare session_update-Events mit _meta.source: "todo_stop_guard"; die Metadaten enthalten den Versuch und den
unfertigen Count, aber niemals Todo-Text. Ein wartender voller Prompt
wird ebenfalls zuerst ausgeführt, und bestehende Permission-/Abbruch-
Regeln bleiben unverändert.
Während eine aktivierte Kette auf verwandte Hintergrundarbeit wartet, werden nicht verwandte Cron/Loop-Feuer und Alt-Task-Benachrichtigungen aufgeschoben. Wiederkehrende Arbeit wird begrenzt und pro Task zusammengefasst, bis die Kette nachgibt.
Die Option ist standardmäßig false, erfordert einen Neustart und wird
im Safe-Modus, Bare-Modus und Approval-plan-Modus zwangsweise
deaktiviert. Sie ist nur im Speicher: Das Laden des Todo-Zustands von
der Festplatte oder ein Neustart des Daemons aktiviert sie nicht. Ein
neuer gewöhnlicher Prompt muss erfolgreich ein eigenes Top-Level-
todo_write ausführen; Retry/Continue und Live-Client-Wiederanhang
behalten die aktuelle In-Memory-Arbeitskette. Das erfolgreiche Ändern
des Arbeitsverzeichnisses der Sitzung löscht es, sodass ein alter Todo
nicht in einem neuen Workspace fortgesetzt werden kann.
Authentication
Für alles, was über Loopback hinausgeht, musst du einen Bearer-Token übergeben:
export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
qwen serve --hostname 0.0.0.0 --port 4170
# → boot refuses without QWEN_SERVER_TOKENClients senden dann bei jeder Anfrage Authorization: Bearer $QWEN_SERVER_TOKEN. /health ist nur bei Loopback-Binds ausgenommen, damit k8s/Compose-Liveness-Probes innerhalb des Pods (wo der Daemon auf 127.0.0.1 lauscht) keine Credentials benötigen. Bei Non-Loopback-Binds (--hostname 0.0.0.0 usw.) erfordert /health wie jede andere Route den Token — andernfalls kann ein Angreifer beliebige Adressen abfragen, um die Existenz des Daemons zu bestätigen. Verwende /capabilities, um zu überprüfen, ob dein Token End-to-End korrekt ist (er erfordert immer Auth):
Gehärtetes Loopback (
--require-auth). Das Standard-Loopback-Verhalten ohne Token ist für einen Single-User-Laptop in Ordnung, aber unsicher auf gemeinsamen Dev-Hosts, CI-Runnern oder Multi-Tenant-Workstations, wo jeder lokale Benutzercurl 127.0.0.1:4170ausführen kann. Übergib--require-auth, um den Bearer-Token auf jeder Route zwingend erforderlich zu machen — einschließlich/healthund/capabilities— auch wenn er an127.0.0.1gebunden ist. Der Boot schlägt ohne Token fehl. Mit diesem Flag kann ein nicht authentifizierter Client/capabilitiesnicht lesen, um herauszufinden, dass Auth erforderlich ist; die Discovery-Oberfläche ist der 401-Response-Body selbst. Nach der Authentifizierung ist der Tagcaps.features.require_autheine Post-Auth-Bestätigung, dass das Deployment gehärtet ist (nützlich für Audit-/Compliance-UIs):qwen serve --require-auth --token "$(openssl rand -hex 32)" # → /health, /capabilities, /session, … all require Authorization: Bearer … curl http://127.0.0.1:4170/health # → 401 curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4170/capabilities | jq '.features | index("require_auth")' # → 13 (or whatever index — non-null after authenticating means the tag is present)
curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" http://your-host:4170/capabilities
# → {"v":1,"mode":"http-bridge","features":[...],"modelServices":[],"workspaceCwd":"/path/to/your-project"}
# Wrong token → 401Der Token-Vergleich ist Constant-Time (SHA-256 + crypto.timingSafeEqual); 401-Responses sind einheitlich für “fehlender Header”, “falsches Schema” und “falscher Token”, sodass ein Side-Channel sie nicht unterscheiden kann.
HTTPS / TLS (für mobilen / geräteübergreifenden Zugriff)
Standardmäßig liefert der Daemon reines HTTP aus. Das ist auf localhost in Ordnung, aber ein Handy oder Tablet, das eine LAN-IP (https://192.168.x.x:4170) ansteuert, ist über http:// kein Secure Context — daher blockieren Browser getUserMedia (Spracheingabe), WebRTC und andere APIs, die nur im Secure Context verfügbar sind. Übergib --tls-cert + --tls-key, um die Web-Shell über HTTPS auszuliefern und sie freizuschalten:
# 1. Installiere eine lokale CA und vertraue ihr (einmalig). Das mobile Gerät muss
# dieser CA ebenfalls vertrauen – mkcert gibt aus, wo sich das Root-Zertifikat befindet.
mkcert -install
# 2. Generiere ein Zertifikat für die LAN-IP deines Rechners. Füge localhost / 127.0.0.1
# ebenfalls zu den SANs hinzu: Bei `--open` schreibt der Daemon die Browser-URL auf
# 127.0.0.1 um, sodass ein Zertifikat, das nur auf die LAN-IP ausgestellt ist, mit
# ERR_CERT_COMMON_NAME_INVALID abgelehnt würde. (mkcert benennt die Ausgabe nach allen Hosts.)
mkcert 192.168.1.100 localhost 127.0.0.1
# 3. Starte den Daemon über HTTPS. Non-Loopback-Binds erfordern weiterhin ein Token,
# und der Browser-Origin muss über CORS erlaubt sein.
qwen serve \
--hostname 0.0.0.0 \
--token "$(openssl rand -hex 32)" \
--tls-cert "./192.168.1.100+2.pem" \
--tls-key "./192.168.1.100+2-key.pem" \
--allow-origin "https://192.168.1.100:4170"
# → qwen serve listening on https://0.0.0.0:4170Hinweise:
- Beide Flags oder keines – der Start schlägt fehl, wenn nur eines angegeben wird (ein Zertifikat ohne Key kann keinen HTTPS-Listener starten).
- TLS ist orthogonal zur Authentifizierung – HTTPS verschlüsselt den Transport; das Bearer-Token schützt weiterhin jede API-Route. Non-Loopback-Binds erfordern ein Token, mit oder ohne TLS.
- Gilt nur für TLS-Terminierung – keine automatische Generierung, kein ACME / Let’s Encrypt. Dies ist eine Erleichterung für LAN/Entwicklung; für internetzugängliche Deployments sollte TLS an einem Reverse Proxy terminiert werden (siehe das Threat Model unten).
CLI-Flags
| Flag | Default | Zweck |
|---|---|---|
--port <n> | 4170 | TCP-Port. 0 = vom Betriebssystem zugewiesener ephemeraler Port. |
--hostname <addr> | 127.0.0.1 | Bind-Interface. Alles über Loopback hinaus erfordert ein Token. |
--local-control | false | Teilt die Web Shell auf einer ausgewählten privaten IPv4-Schnittstelle mit einem Daemon-eigenen widerruflichen Pairing-Token, Terminal-QR-Code, exakter Browser-Origin und bestmöglicher Sleep-Inhibierung. Komponiert mit --token, --allow-origin und --port 0; steht im Konflikt mit --no-web und nicht-standard --hostname. Verwende --local-control-address, wenn mehrere LAN-Kandidaten verfügbar sind, und füge --tls-cert + --tls-key hinzu für Secure-Context-Browser-APIs wie Spracheingabe. |
--local-control-address <ip> | — | Welche LAN-IPv4-Adresse geteilt wird, wenn der Host mehr als einen Kandidaten hat. Nur erforderlich, wenn --local-control eine mehrdeutige Auswahl meldet. |
--token <str> | — | Bearer-Token. Fällt auf die Umgebungsvariable QWEN_SERVER_TOKEN zurück (mit entfernten führenden/anhängenden Leerzeichen – praktisch für $(cat token.txt)). |
--require-auth | false | Verweigert den Start ohne Bearer-Token, auch auf Loopback. Härtet den 127.0.0.1-Entwickler-Standard für gemeinsam genutzte Dev-Hosts / CI-Runner / Multi-Tenant-Workstations ab, bei denen jeder lokale Benutzer den Listener erreichen kann. Startet nur, wenn --token oder QWEN_SERVER_TOKEN gesetzt ist; schützt auch /health hinter dem Bearer-Token. |
--tls-cert <path> | — | Pfad zu einer PEM-Zertifikatsdatei. Bedient über HTTPS statt HTTP. Muss mit --tls-key gekoppelt werden (Start schlägt fehl, wenn nur eines angegeben wird). Entsperrt Secure-Context-Browser-APIs – Spracheingabe (getUserMedia), WebRTC – über eine LAN-IP, was Browser bei normalem http:// sonst blockieren. Nur TLS-Terminierung; keine automatische Generierung / ACME. Siehe HTTPS / TLS unten. |
--tls-key <path> | — | Pfad zu einer PEM-Private-Key-Datei. Muss mit --tls-cert gekoppelt werden. |
--max-sessions <n> | 32 | Obergrenze für gleichzeitige Live-Sessions. Neue POST /session-Anfragen, die ein neues Kind erzeugen würden, geben 503 (mit Retry-After: 5) zurück, wenn die Obergrenze erreicht ist; Anhängen an bestehende Sessions wird NICHT gezählt. Setze auf 0, um es zu deaktivieren. Ausgelegt für Single-User / Small-Team-Nutzung; erhöhe den Wert, wenn dein Deployment über genügend RAM/FD-Reserven verfügt (~30–50 MB pro Session). |
--max-total-sessions <n> | abgeleitet | Optionale nicht-negative Ganzzahl, Daemon-weite Obergrenze für die Erstellung neuer Sitzungen über alle registrierten Workspace-Runtimes hinweg. Gilt für neue Child-Sitzungen, Session-Wiederherstellung und über Branch/Fork erstellte Sitzungen; das Anhängen an eine bestehende Live-Sitzung verbraucht keinen Slot. Setze auf 0 für unbegrenzt. Wenn weggelassen bei mehreren Startup/wiederhergestellten Workspaces, leitet der Daemon eine feste Obergrenze aus dem pro-Workspace-Limit und der Startup-Workspace-Anzahl ab; spätere dynamische Registrierung berechnet sie nicht neu. |
--max-pending-prompts-per-session <n> | 5 | Session-spezifische Obergrenze für Prompts, die von POST /session/:id/prompt akzeptiert, aber noch nicht abgeschlossen wurden, einschließlich wartender Prompts und des aktiven Prompts. Die Bridge lehnt Überläufe synchron mit 503, Retry-After: 5 und code: "prompt_queue_full" ab, bevor eine promptId zurückgegeben wird. Setze auf 0, um es zu deaktivieren. branchSession serialisiert auf derselben FIFO, zählt aber nicht gegen diese Prompt-Obergrenze. |
--workspace <path> | process.cwd() | Absolutes Workspace-Verzeichnis, das von diesem Daemon registriert wird. Wiederhole das Flag, um mehrere Workspaces in einem Prozess zu hosten; der erste ist primär und bleibt der Standard, wenn eine Anfrage cwd weglässt. Relative Werte werden abgelehnt. Session-Anfragen, deren kanonisches cwd nicht registriert ist, geben 400 workspace_mismatch zurück. |
--memory-project-scope <mode> | workspace | Projekt-Speicher-Partitionierungsmodus. workspace (Standard) keyt Speicher nach dem exakten registrierten Workspace-Verzeichnis, sodass jeder Daemon-Workspace seinen eigenen isolierten Speicher erhält; git-root ist der Legacy-Kompatibilitätsmodus, der von Workspaces mit demselben Git-Root geteilt wird. Überschreibt QWEN_CODE_MEMORY_PROJECT_SCOPE wenn angegeben; ein leerer Umgebungsvariablen-Wert wird als nicht gesetzt behandelt, während ein unbekannter nicht-leerer Wert mit einer einmaligen Warnung ignoriert wird und das Legacy-git-root-Verhalten beibehält. Der neue Standard migriert bestehenden Git-Root-Projektspeicher nicht – verwende einen expliziten git-root-Scope, um diese Einträge während der Migration zu lesen. |
--channel <name|all> | — | Experimenteller, vom Daemon verwalteter Channel-Worker. Wiederhole das Flag, um mehrere konfigurierte Channels auszuwählen, oder übergebe all, um jeden konfigurierten Channel zu starten. all kann nicht mit benannten Channels kombiniert werden. Ausgewählte cwd-Werte für Channels müssen auf einen registrierten Workspace auflösen; ein Multi-Workspace-Daemon betreibt einen Worker pro besitzendem Workspace. Der Worker gehört qwen serve; stoppe den Daemon, um die von serve verwalteten Channels zu stoppen. |
--max-connections <n> | 256 | TCP-Verbindungsobergrenze auf Listener-Ebene (server.maxConnections). Begrenzt die Anzahl der Raw-Sockets unabhängig von der Session-Anzahl – langsame / Phantom-SSE-Clients werden beim Akzeptieren abgelehnt, sobald das Limit erreicht ist. Erhöhe diesen Wert zusammen mit --max-sessions, wenn dein Deployment viele SSE-Abonnenten pro Session erwartet. |
--memory-budget-mb <n> | 50% von Cgroup/Host | Gesamtspeicherbudget in MB für den gesamten Daemon-Prozessbaum. Wenn nicht gesetzt, abgeleitet als 50 % des Cgroup-Limits oder des Host-Speichers; in jedem Fall wird der effektive Wert auf den aufgelösten verfügbaren Speicher begrenzt, und sowohl der konfigurierte als auch der effektive Wert werden berichtet. Es ändert nicht, wie ein qwen --acp-Child dimensioniert wird; der einzige Consumer heute ist adaptives Live-Journal-Wachstum: ein Daemon-weiter Wachstumspool, abgeleitet als 5 % des effektiven Budgets (begrenzt auf 1024 MB; auf Hosts, die insufficientMemory melden, ist der Pool 0 und adaptives Wachstum ist deaktiviert), wird von jeder Workspace-Bridge geteilt — siehe --max-journal-bytes. Aufgelöste Werte erscheinen unter limits.memory in GET /daemon/status, neben registrierten und Live-Child-Counts und advisory Pro-Child-Anteilen unter runtime.memory. Ein zu kleiner Host meldet insufficientMemory statt nach oben zu clampen; da der abgeleitete Anteil 50 % beträgt, löst jeder Host unter ~2 GB dies aus. Übergib ein explizites --memory-budget-mb 1024 auf einem solchen Host, um den abgeleiteten Wert zu überschreiben (das Flag erfordert weiterhin mindestens 1024 MB verfügbaren Speicher, um die Warnung zu beseitigen). Muss eine Ganzzahl in [1024, 1048576] sein. |
--memory-pressure-mode <mode> | observe | Ob der Daemon seine eigene Speicherablesung in ein Urteil umwandelt. observe (Standard) meldet das Pressure-Level unter runtime.memory.pressure in GET /daemon/status und löst ein daemon_memory_pressure-Issue aus — ein warning, sodass der Gesamt-status ok bleibt — wenn das Level normal verlässt. off meldet weiterhin alle Werte, einschließlich des Levels, löst aber kein Issue aus, sodass der Gesamt-status unverändert bleibt; verwende es beim Kalibrieren oder wenn du auf den Top-Level-Status alarmierst. Das Level ist das schlechtere von zwei Verhältnissen: RSS gegen verfügbaren Speicher (was der Cgroup-OOM-Killer beobachtet) und V8-Heap-Nutzung gegen die Heap-Obergrenze dieses Prozesses. Es umfasst nur den Daemon-Root-Prozess; vergleiche ihn mit runtime.memory.children.rssBytes für die Children. Nichts remidiert in einem der Modi. Einer von off, observe. |
--child-heap-mode <mode> | observe | Ob der Daemon eine Pro-Child-Heap-Partition von --memory-budget-mb modelliert. observe (Standard) meldet, was es anwenden würde — limits.memory.childHeap.perChildCeilingMb und maxConcurrentChildren — und zählt Spawns, die das Limit überschritten hätten. Nichts wird angewendet: Kein Child wird aus dem Budget dimensioniert und kein Spawn abgelehnt. off modelliert nichts und sagt das auch über die Leitung: maxConcurrentChildren und perChildCeilingMb sind beide null, statt eine Partition zu tragen, die du ausgeschaltet hast. Eine Ablehnungszählung von 0 bedeutet nicht, dass die Partition sicher anwendbar wäre: Children laufen weiterhin auf der viel größeren Host-abgeleiteten Obergrenze, sodass ein Workload, der mehr Old Space als die modellierte Obergrenze benötigt, hier vollkommen gesund aussieht. Die Anwendung der Partition kommt mit der Messung, die das beantworten kann. |
--event-ring-size <n> | 8000 | Pro-Session SSE-Replay-Ring-Tiefe (#3803 §02-Ziel). Setzt den Backlog, der GET /session/:id/events mit Last-Event-ID: N zur Verfügung steht. Größer = mehr Spielraum für Reconnects auf Kosten von einigen hundert KB zusätzlichem RAM pro Session. SDK-Clients können zusätzlich über ?maxQueued=N eine größere Backlog-Obergrenze pro Abonnent für ein bestimmtes Abonnement anfordern (Bereich [16, 2048], Standard 256). Daemons senden außerdem einen nicht-terminalen slow_client_warning-SSE-Frame bei 75 % Queue-Füllung, damit Clients drainen / reconnecten können, bevor sie aus der Queue entfernt werden. Pre-flight caps.features.slow_client_warning. |
--compacted-replay-max-bytes <n> | 4194304 | Pro-Live-Session Byte-Obergrenze für die zurückgehaltenen Replay-Events im begrenzten Snapshot, der von POST /session/:id/load zurückgegeben wird. Die Obergrenze gilt für compactedReplay; das aktuelle In-Flight-liveJournal wird separat durch --max-journal-events und --max-journal-bytes begrenzt (Basis-Obergrenzen, die adaptives Wachstum erhöhen kann — siehe --max-journal-bytes). Werte müssen positive Safe-Integers sein; ungültige Werte schlagen beim Boot fehl, und die harte Obergrenze ist 256 MiB. Wenn ältere zurückgehaltene Replay-Daten verworfen werden, beginnt der Snapshot mit history_truncated. Dies begrenzt nicht das Transkript auf der Festplatte. |
--max-journal-events <n> | 10000 | Pro-Session Basis-Obergrenze für Replay-Einträge, die im In-Flight-liveJournal für den aktuellen unfertigen Turn zurückgehalten werden. Aufeinanderfolgende kompatible Text- oder Thought-Chunks teilen sich einen Eintrag, mit höchstens 256 Quell-Events pro Eintrag; andere Event-Grenzen bleiben erhalten. Bei Überschreitung versucht der Daemon zuerst adaptives Wachstum (siehe --max-journal-bytes); wenn kein Spielraum gewährt wird oder die Gewährung die Überschreitung nicht abdeckt, werden die ältesten Einträge verworfen und ein history_truncated-Marker vorangestellt. Die truncatedEvents- und retainedEvents-Zähler des Markers beschreiben Quell-Events. Muss ein positiver Safe-Integer sein. Das Fixieren dieses Flags (oder --max-journal-bytes) deaktiviert adaptives Wachstum. |
--max-journal-bytes <n> | 8388608 | Pro-Session Basis-Byte-Obergrenze für das In-Flight-liveJournal, verrechnet ab den serialisierten Quell-Events, auch wenn kompatible Chunks sich einen Replay-Eintrag teilen. Wenn ein Turn die Obergrenze überschreitet, erhöht adaptives Wachstum die Obergrenzen der Sitzung gegen das Doppelte (bis zu einer harten pro-Session-Obergrenze von 256 MiB, begrenzt durch den verbleibenden Pool-Spielraum), während das über alle Live-Sessions des Daemons gewährte Wachstum in einen gemeinsamen Wachstumspool passt, der zu 5 % des effektiven Speicherbudgets des Daemons bemessen ist – der --memory-budget-mb-Wert, wenn übergeben, begrenzt auf den aufgelösten verfügbaren Speicher, andernfalls 50 % des automatisch erkannten Speichers (siehe --memory-budget-mb) – begrenzt auf 1024 MB; auf Hosts, die insufficientMemory melden, ist der Pool 0 und adaptives Wachstum ist deaktiviert. Wachstum erfolgt on-demand und nur soweit der Pool es erlaubt; wenn es verweigert wird, der Pool erschöpft ist oder eine Gewährung die Überschreitung nicht abdeckt, werden die ältesten Einträge vollständig verworfen (mindestens ein Eintrag wird immer behalten), sodass das behaltene Ende deutlich kleiner als die Obergrenze sein kann. Das Fixieren dieses Flags (oder --max-journal-events) deaktiviert adaptives Wachstum. Muss ein positiver Safe-Integer sein. Standard 8 MiB. |
--mcp-client-budget <n> | — | Obergrenze als positive Ganzzahl für Live-MCP-Clients. Wenn mcp_workspace_pool beworben wird, werden die Obergrenze und Transports pro Workspace-Runtime geteilt; wenn der Tag fehlt, erzwingt es der Legacy-Pro-Session-Manager. Kombiniere mit --mcp-budget-mode. Wenn nicht gesetzt, keine accounting-basierte Durchsetzung (aber GET /workspace/mcp meldet weiterhin clientCount). Unterschiedlich zu claude-codes MCP_SERVER_CONNECTION_BATCH_SIZE, das die Start-Parallelität begrenzt, nicht die Gesamtzahl der Live-Clients. Pre-flight caps.features.mcp_guardrails und caps.features.mcp_workspace_pool. |
--mcp-budget-mode <m> | warn / off | Wie --mcp-client-budget durchgesetzt wird. warn (Standard, wenn Budget gesetzt): keine Ablehnung, budgets[0].status des Snapshots springt bei ≥75 % des Budgets auf warning. enforce: Verbindungen über der Obergrenze werden abgelehnt, die Pro-Server-Zelle zeigt disabledReason: 'budget', deterministisch nach der Deklarationsreihenfolge von mcpServers. off (Standard, wenn Budget nicht gesetzt): reine Beobachtbarkeit. Der Start lehnt enforce ohne Budget ab. |
--external-tool-guard-mode <m> | off | Verwaltete ACP-externe Pre-Execution-Policy. off ruft keine Provider auf und bewirbt keine Capability. required schlägt beim Start fehl, es sei denn, ein kompatibler Provider schließt den v1-Handshake ab, und schlägt dann jede unterstützte Top-Level-Tool-Ausführung geschlossen fehl, es sei denn, ihre einzelne Prepare-Anfrage wird erlaubt. |
--external-tool-guard-endpoint <url> | — | Origin-only Loopback-HTTP(S)-Provider-URL, die im required-Modus verwendet wird, z. B. http://127.0.0.1:8787. Pfade, URL-Credentials, Redirects, Non-Loopback-Hosts und Proxy-Routing werden nicht akzeptiert. |
--external-tool-guard-timeout-ms <n> | 3000 | Integer 100..30000; gilt unabhängig für den Startup-Handshake und jede Prepare-Anfrage. |
--http-bridge | true | Stage-1-Modus: Production versucht, ein primäres qwen --acp-Child für Kompatibilität vorzuwärmen und wiederholt den ersten Versuch nach einem Fehlschlag, während jeder vertrauenswürdige Secondary ein Child bei Bedarf starten kann. Sessions, die auf eine Runtime abzielen, werden über ACP newSession() auf dessen Child gemultiplext; nicht vertrauenswürdige Secondaries können keinen ACP starten. Stage 2 nativ im Prozess wird später verfügbar. |
--initialize-timeout-ms <n> | 10000 | ACP-Child-Request-Timeout, einschließlich des initialize-Handshakes (ms). Muss ein positiver Integer bis 2147483647 sein. Werte über der JS-Timer-Obergrenze (2^31-1) werden beim Boot abgelehnt, da Node sie stillschweigend auf 1 ms komprimiert. Cold-Container-Deployments, die zusätzliche Spielraum für den Child-Start benötigen, können dies erhöhen; derselbe Wert regelt newSession, Workspace-Status-Polls und andere ACP-Ext-Method-Deadlines. |
--session-restore-timeout-ms <n> | 60000 | ACP-Session-Load/Resume-Deadline in Millisekunden. Muss ein positiver Integer bis 2147483647 sein; 0 ist ungültig. Wenn weggelassen, beträgt der Standard 60 Sekunden, erhöht auf einen explizit angegebenen --initialize-timeout-ms, wenn dieser Wert größer ist; ein kürzerer Initialize-Timeout senkt niemals das Restore-Budget. SDK und WebUI addieren 10 bzw. 15 Sekunden Client-Spielraum. Ein Timeout gibt retryable 504 session_restore_timeout zurück; es bedeutet nicht, dass der Daemon selbst beendet wurde. |
--allow-origin <pat> | — | T2.4 (#4514 ). Cross-Origin-Allowlist für Browser-WebUI-Clients. Wiederholbar. Jeder Wert ist * (beliebiger Origin – Start wird verweigert, wenn kein Bearer-Token konfiguriert ist; --require-auth auf Loopback wird empfohlen, damit /health ebenfalls durch Bearer geschützt ist, da es auf Loopback standardmäßig pre-auth ist; die Web-Shell-Static-Assets bleiben in jedem Modus pre-auth, daher verwende --no-web, um sie zu entfernen) oder ein kanonischer URL-Origin (<scheme>://<host>[:<port>], kein abschließender Schrägstrich / Pfad / Userinfo / Query). Subdomain-Wildcards (https://*.example.com) werden absichtlich nicht unterstützt – liste jede Subdomain explizit auf oder verwende * mit einem konfigurierten Token (und --require-auth für vollständige Härtung). Übereinstimmende Origins erhalten CORS-Antwortheader (Access-Control-Allow-Origin, Vary: Origin, Methods, Headers, Max-Age und exponiertes Retry-After); nicht übereinstimmende Origins erhalten weiterhin einen 403 mit demselben Envelope wie die heutige Wall. Origin: null (sandboxed iframes, file:// docs) wird immer abgelehnt, auch unter *. Pre-flight über caps.features.allow_origin. Loopback-Self-Origin-Hits sind nicht betroffen. |
--web / --no-web | true | Bedient die gebaute Web-Shell-SPA am Daemon-Root (GET /, /assets/* und GET /session/<id> Dokument-Navigationen). Diese Endpunkte sind vor dem Bearer-Auth-Gate registriert – ein Browser kann kein Token an eine <script>-Subressource oder eine Adressleisten-Navigation anhängen, und die Shell enthält keine Secrets. Jede API-Route bleibt unabhängig davon Token-geschützt, und der SPA-Deep-Link-Fallback für alle anderen Pfade liegt ebenfalls hinter dem Bearer-Gate. Bei Non-Loopback-Binds weist eine einzeilige Stderr-Warnung darauf hin, dass die UI ohne Auth erreichbar ist. Verwende --no-web für einen reinen API-Daemon. Keine Auswirkung, wenn der Build die Web-Shell-Assets weglässt (der Daemon loggt einen Breadcrumb und läuft nur als API). |
--open | false | Nachdem der Listener gestartet ist, öffne die Web-Shell in deinem Standardbrowser unter der Daemon-URL (mit #token= als URL-Fragment angehängt, wenn ein Token konfiguriert ist – ein Fragment wird niemals an den Server gesendet, wodurch das Token aus den Zugriffslogs und Referer-Headern ferngehalten wird). No-op mit --no-web oder in Headless-/CI-/SSH-Umgebungen, in denen kein Browser verfügbar ist. |
--open-with-auth | false | Öffne die Web-Shell mit Bearer-Authentifizierung auf Loopback. Erfordert eine aktivierte Web-Shell und gebaute Assets. Verwendet ein konfiguriertes Token oder generiert ein 256-Bit-Bearer für die Prozesslebensdauer und liefert es im Web-Shell-Fragment aus. Browser-ungeeignete Umgebungen geben die Secret-haltige manuelle URL aus. Andere Clients benötigen stattdessen dasselbe explizit konfigurierte Shared-Token. |
Größenanpassung der Lastregler.
--max-sessionsist die pro-Workspace Obergrenze für neue Sitzungen.--max-total-sessions, wenn gesetzt, ist die Daemon-weite Obergrenze für neue Sitzungen. Drei weitere Ebenen begrenzen ebenfalls die Last – stimme sie bei der Dimensionierung für ein High-Concurrency-Deployment aufeinander ab:
- Listener-Ebene:
--max-connections/server.maxConnections=256begrenzt reine TCP-Verbindungen (Back-Pressure bei langsamen Clients).- Abonnenten pro Session: Der EventBus begrenzt SSE-Abonnenten standardmäßig auf 64 pro Session; der 65. Client erhält einen terminalen
stream_errorund wird getrennt.- Prompt-Zulassungen pro Session:
--max-pending-prompts-per-session=5begrenzt die wartenden + aktiven Prompts, die für eine Session akzeptiert werden. Bei Überlauf wird503mitRetry-After: 5zurückgegeben.- Daemon-weite neue Sitzungen:
--max-total-sessions=Nbegrenzt die Erstellung neuer Sitzungen Daemon-weit. Überlauf erhält dieselbesession_limit_exceeded-Form mitscope: "total".- Backlog pro Abonnent: Eine 256-Frames-Warteschlange pro SSE-Client; ein Client mit Überkapazität erhält ein terminales
client_evicted-Frame und wird getrennt (ein langsamer Consumer kann den Daemon nicht blockieren).Diese Limits interagieren miteinander: Jede Runtime wird durch
--max-sessionsbegrenzt, während--max-total-sessionsderen Gesamtmenge begrenzt. Die effektive Session-Obergrenze ist das Minimum aus einer endgültigen Daemon-weiten Obergrenze und der aggregierten pro-Runtime-Obergrenze (behandele letztere als unbegrenzt, wenn die pro-Workspace-Obergrenze unbegrenzt ist). Wenn keine endlich ist, gibt es keine endgültige Session-Obergrenze. Eine endgültige Obergrenze × 64 Abonnenten × 256 Frames ist der Worst-Case In-Memory-Speicherbedarf auf der EventBus-Ebene; multipliziert mit--max-pending-prompts-per-sessionbegrenzt dies die akzeptierte Prompt-Arbeit auf der Admission-Ebene. Die Standarddimensionierung geht von Single-User- / Klein-Team-Last aus; für größere Deployments schrittweise erhöhen (und RSS beobachten).
MCP-Client-Schutzmechanismen (Issue #4175 PR 14). Ein Workspace, der 30 MCP-Server in
mcpServersdeklariert, startet 30 Clients ohne Obergrenze, sofern du keine festlegst.--mcp-client-budget=Nbegrenzt die Anzahl der aktiven MCP-Clients;--mcp-budget-mode={enforce,warn,off}wählt das Verhalten. Der Standard istwarn, wenn ein Budget festgelegt ist (der Snapshot zeigt die Warnung an, aber kein Client wird abgelehnt – nützlich, um das reale Fanout zu messen, bevor die Durchsetzung aktiviert wird). Abgewiesene Server imenforce-Modus erhaltendisabledReason: 'budget'in ihrer serverbezogenen Zelle, und diebudgets[0]-Zelle zeigtstatus: 'error'+errorKind: 'budget_exhausted'. Die Slot-Reservierung erfolgt nach Servername und übersteht Reconnects / Discovery-Timeouts – ein abgelehnter Server kann einem gesunden Server keinen Slot wegnehmen.Aktueller Scope ist Capability-gesteuert. Wenn
mcp_workspace_poolvorhanden ist, teilen sich alle Sessions in einer Workspace-Runtime dessen MCP-Transport-Pool und Budget-Controller;GET /workspace/mcpemittiertscope: 'workspace'. Ein zweiter Workspace hat einen unabhängigen Pool und Budget. Wenn der Tag fehlt (einschließlichQWEN_SERVE_NO_MCP_POOL=1), verwendet der Daemon den Legacy-Pro-Session-McpClientManagerund emittiertscope: 'session'; in diesem Fallback können N Sessions jeweils das konfigurierte Limit verbrauchen.qwen serve --mcp-client-budget=10 --mcp-budget-mode=warn # later, after telemetry shows your real-world distribution: qwen serve --mcp-client-budget=10 --mcp-budget-mode=enforceDies ist nicht dasselbe wie
MCP_SERVER_CONNECTION_BATCH_SIZEin claude-code (welches die Startup-Concurrency steuert); sie sind orthogonal. Clients müssen aufmcp_workspace_poolverzweigen, nicht einen Scope allein aus der Protokollversion ableiten.Push-Events (Issue #4175 PR 14b). SDK-Clients, die
GET /session/:id/eventsabonniert haben, erhalten typisierte Frames, wenn Budget-Schwellenwerte überschritten werden —mcp_budget_warning(synthetisch, feuert einmal pro aufwärts gerichtetem 75%-Überschreitung mit Hysterese-Re-Arm bei 37,5%, beworben übermcp_guardrail_events) undmcp_child_refused_batch(zusammengefasst einmal pro Discovery-Pass imenforce-Modus; Länge-1 ausreadResource-Lazy-Spawn-Ablehnung). Der Snapshot unterGET /workspace/mcpbleibt die Source-of-Truth für den Zustand nach einem Reconnect; Events sind Änderungs-Flanken. Nützlich für Echtzeit-Dashboards ohne Polling.
Hinweise zum Memory-Projekt-Scope.
- Daemon vs. Standalone-CLI. Das Flag oder die Launch-Umgebung des Daemons setzt einen festen Scope für jede Runtime, die diesem Daemon gehört. Eine Workspace-
.envodersettings.envkann ihn nicht für einen registrierten Workspace überschreiben. Eine Standalone-qwen-TUI verwendet standardmäßig weiterhin den Git-Root-Scope; um beide Einstiegspunkte konsistent zu halten, exportiereQWEN_CODE_MEMORY_PROJECT_SCOPEin der Shell oder Service-Umgebung, die sie startet.- Verzeichnisnamen-Kollisionen. Der Speicher-Key wird von
sanitizeCwdabgeleitet, das jedes nicht-alphanumerische Zeichen durch-ersetzt. Geschwister-Verzeichnisse, die sich nur in der Punktuierung unterscheiden (z. B.feature_1undfeature-1), werden auf dasselbe Speicherverzeichnis abgebildet, auch unterworkspace-Scope. Vermeide solche Benennungen, wenn du dich auf Workspace-Isolation verlässt.- Normalisierung unterscheidet sich zwischen Flag und Umgebungsvariable. Die Umgebungsvariable wird getrimmt und kleingeschrieben (
" Workspace "funktioniert); das CLI-Flag wird von yargschoicescase-sensitiv abgeglichen (--memory-project-scope Workspacewird abgelehnt). Verwende kleingeschriebene Werte beim Kopieren zwischen den beiden.
Eingebauter Daemon-Git-Relocation-Guard
Jede verwaltete Daemon-ACP-Sitzung wendet einen eingebauten Pre-Execution-Guard für Modell-Shell-Befehle an, unabhängig von --external-tool-guard-mode und ohne Capability-Bewerbung. Der Daemon besitzt den gebundenen Workspace und das aktuelle effektive Arbeitsverzeichnis der Session; beide werden aus vertrauenswürdigem Session-State geliefert und niemals vom ACP-Child übernommen.
Der Guard prüft die Tools, die eine Shell-Befehlszeile ausführen — run_shell_command und monitor — und verweigert einen mutierenden Git-Befehl vor der Ausführung, wenn sein Repository-Pfad sich außerhalb des effektiven Arbeitsverzeichnisses der Session auflöst. Relocation wird erkannt für literale Formen von git -C <path>, git --git-dir[=]<path>, git --work-tree[=]<path>, führende GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_INDEX_FILE-Zuweisungen (auch wenn sie über export/declare/readonly erfolgen, die sie in der Umgebung jedes späteren Befehls in der Kette belassen), verzeichnisverschiebende Wrapper-Flags (env -C, sudo -D) und cd-, pushd- oder popd-Builtins früher in derselben Befehlskette. Häufige Wrapper-Präfixe (sh -c, bash -c, eval, sudo, nohup, timeout, exec, command, builtin, env, pfadqualifizierte git-Binaries und { …; } / ! …-Shell-Syntax) werden entpackt, sodass dieselbe Policy für die innere Git-Invocation gilt, und $(…)- oder Backtick-Substitutionskörper werden als eigene Befehle analysiert.
Ein Sub-Agent, der auf sein eigenes Worktree fixiert ist, wird auf dieses Worktree begrenzt statt auf das Verzeichnis der Session; ein Shell-Aufruf, dessen Ausführungsverzeichnis der Daemon nicht platzieren kann, wird verweigert.
Relative Ziele lösen sich vom effektiven Startverzeichnis des Befehls auf (arguments.directory wenn vorhanden, andernfalls das aktuelle effektive Arbeitsverzeichnis der Session) nach kanonischer Pfadauflösung, einschließlich .git-Gitfile-Redirects, Symlinks und pro-Worktree-Administrationsverzeichnisse. Ein relociertes Ziel, das vor der Ausführung nicht vollständig aufgelöst werden kann — ein dynamisches Ziel ($VAR, Backticks, ~, Globs), ein noch nicht existierender Pfad oder eine unlesbare Indirektion — wird für mutierende oder nicht klassifizierbare Subcommands verweigert. Ein relociertes Ziel, das nicht aufgelöst werden kann, wird unabhängig vom Subcommand verweigert — einschließlich der lese-only Subcommands. Relocierte Befehle, deren Subcommand zu einem kleinen verifizierten lese-only-Set gehört (rev-parse, cat-file), bleiben erlaubt, sobald das Ziel sich auflöst, es sei denn, der Befehl trägt eine Befehl-ausführende -c-Config oder er trägt ein --output-, --textconv- oder --filters-Flag: diese schreiben eine Datei oder führen die konfigurierten Treiber des Ziel-Repositories aus. Befehle ohne erkannte Relocation behalten ihr bestehendes Verhalten. Ablehnungen sind endgültig und werden dem Modell als Daemon shell guard denied a mutating Git command… für einen aufgelösten, dynamischen oder nicht auflösbaren Repository-Pfad gemeldet, und als Daemon shell guard denied a shell command… wenn der Befehl nicht geparst werden konnte, seine Nutzlast nicht aufgelöst werden konnte oder ein unbekanntes Programm einen relocierten Git-Befehl ausführen könnte.
Der Guard ist zuverlässig gegen Git-Relocation in den obigen literalen Formen — der falsch gerichtete Befehl, für den diese Kontrolle existiert — und best-effort, keine Grenze, gegen Shell-Text, der geschrieben wurde, um ihn zu umgehen: Konstruktionen, die die Relocation vor einem statischen Leser verbergen, können durchkommen, und neue werden weiterhin gefunden. Gewähre einem Daemon daraufhin kein breiteres Vertrauen. Er interpretiert keine Skriptdateien, verfolgt keine Umgebungsvariablenwerte über Befehle hinweg und analysiert keine Heredoc-Bodies (Git-förmiger Text in einem Heredoc kann verweigert werden, auch wenn die Shell ihn nie ausführt). /fork und Agent-gestützte Workspace-Memory-Remember/Dream bleiben unter dem eingebauten Guard verfügbar; sie sind nur eingeschränkt, während der externe Provider-Modus unten aktiv ist. Ein optionaler externer Tool-Guard bleibt eine zusätzliche Policy und erhält dieselbe Anfrage erst, nachdem die eingebaute Policy sie erlaubt.
Required External Tool Guard
Dieses Opt-in ist für verwaltete ACP-Deployments, die eine externe
Allow/Deny-Entscheidung an der finalen Tool-Executor-Grenze benötigen. Es
ist vollständig dunkel, es sei denn, --external-tool-guard-mode=required
ist vorhanden:
export QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN='replace-with-local-secret'
qwen serve \
--external-tool-guard-mode=required \
--external-tool-guard-endpoint=http://127.0.0.1:8787 \
--external-tool-guard-timeout-ms=3000Der Provider muss POST /v1/handshake und POST /v1/prepare
bereitstellen, Authorization: Bearer <token> erfordern, JSON
zurückgeben, die mitgelieferte Nonce oder Request-ID echoen und
Protokollversion 1 verwenden. Der Token muss nicht-leer sein,
höchstens 8192 UTF-16 Code-Einheiten enthalten und keine Steuerzeichen
enthalten. Requests sind auf 1 MiB begrenzt, Responses auf 64 KiB, und
optionale Ablehnungsgründe auf 500 UTF-16 Code-Einheiten ohne
Steuerzeichen. Eine erfolgreiche Prepare-Response ist:
{ "protocolVersion": 1, "requestId": "<echo>", "allowed": true }Eine Ablehnung verwendet allowed:false und kann einen kurzen reason
hinzufügen. Für jede unterstützte Top-Level-Tool-Ausführung, die
bestehende Permission- und PreToolUse-Gates passiert und die finale
Ausführungsgrenze erreicht, sendet Qwen Code eine Prepare-Anfrage und
wiederholt sie niemals. Eine frühere Permission/Hook-Ablehnung sendet
keine Prepare-Anfrage. Timeout, Abbruch, Transportfehler, fehlerhafte
oder nicht übereinstimmende Responses und explizite Ablehnung verhindern
die Ausführung des Executors. Jeder erzeugte ACP-Channel muss ebenfalls
bestätigen, dass er den erforderlichen Callback installiert hat; eine
fehlende oder inkompatible Bestätigung lehnt den Channel vor der
Session-Erstellung ab.
Der Provider-Request trägt sessionId, promptId, toolCallId,
kanonischen toolName und finale arguments; toolCallId ist ein
Korrelations-Label, keine Authentifizierungsidentität oder eigenständiger
Idempotenz-Key.
Finale Argumente können sensible Anwendungsdaten enthalten. Behandle sie in Provider-Logs und Audit-Speichern entsprechend.
PreToolUse-Hooks laufen vor dieser finalen Executor-Entscheidung.
Required-Guard-Modus autorisiert oder sandboxt kein Hook-Verhalten;
Deployments, die eine Grenze um jeden möglichen Side-Effekt benötigen,
müssen Hooks deaktivieren oder ihre Implementierungen separat
steuern.
Slash-Command-Aktionen laufen ebenfalls vor Modell/Tool-Scheduling und
sind keine Guard-Aufrufe. Einige Built-ins können direkt Dateien oder
Einstellungen ändern. Ein verwaltetes Deployment, das eine Grenze um
alle Effekte benötigt, muss Slash-Command-Eingabe ablehnen oder jeden
nicht genehmigten Befehl über slashCommands.disabled oder
--disabled-slash-commands deaktivieren.
Der v1-Verwaltungsbereich sind Top-Level-Tools, die von einem aktiven
Vordergrund-verwalteten Prompt aufgerufen werden. Verschachtelte oder
delegierende agent-, workflow-, create_sub_session-,
send_message-, direkte /fork- und Agent-gestützte
Workspace-Memory-Remember/Dream-Steuerungen werden abgelehnt, während
der Required-Modus aktiv ist. Eine Top-Level-Hintergrund-Shell oder
Monitor-Start ist weiterhin eine bewachte Ausführung und ihre finalen
Argumente erreichen den Provider, aber dieses Feature autorisiert den
Prozess nicht kontinuierlich oder fügt ein Prozess-Abschluss-Audit-
Protokoll hinzu; eine Policy, die Vordergrund-Abschluss erfordert,
sollte diese Formen ablehnen. Bewachte MCP-Aufrufe deaktivieren auch
den automatischen Reconnect/Replay nach einem Transportfehler. Nach
einem erfolgreichen Startup-Handshake bewirbt /capabilities
external_tool_guard; dessen Fehlen bedeutet, dass Clients keine
Durchsetzung annehmen dürfen.
Dieses Feature autorisiert keine expliziten Daemon-REST/ACP- Verwaltungsaufrufe; diese verwenden weiterhin die bestehende Authentifizierung und Route-Verträge des Daemons. Es macht ein erlaubtes Tool oder Shell-Kommando auch nicht deterministisch oder sandboxt seine Interna; verwaltete Deployments müssen die Provider-Entscheidung mit ihrer normalen Tool-Policy und Isolationsgrenze kombinieren.
Größenordnung der Last-Regler.
--max-sessionsist das pro-Workspace-Limit für neue Sessions.--max-total-sessions, wenn gesetzt, ist das Daemon-weite Limit für neue Sessions. Drei weitere Ebenen begrenzen ebenfalls die Last — stimme sie bei einem hochskalierenden Deployment gemeinsam ab:
- Listener-Ebene:
--max-connections/server.maxConnections=256begrenzt rohe TCP-Verbindungen (Slow-Client-Back-Pressure).- Pro-Session-Subscriber: der EventBus begrenzt SSE-Subscriber standardmäßig auf 64 pro Session; der 65. Client erhält ein terminales
stream_errorund wird getrennt.- Pro-Session-Prompt-Zulassung:
--max-pending-prompts-per-session=5begrenzt wartende + aktive Prompts, die für eine Session akzeptiert werden. Überlauf erhält503mitRetry-After: 5.- Daemon-weite neue Sessions:
--max-total-sessions=Nbegrenzt die Erstellung neuer Sessions Daemon-weit. Überlauf erhält dieselbesession_limit_exceeded-Form mitscope: "total".- Pro-Subscriber-Backlog: eine 256-Frame-Queue pro SSE-Client; ein überkapazitiver Client erhält ein terminales
client_evicted-Frame und wird getrennt (ein langsamer Consumer kann den Daemon nicht blockieren).Diese Obergrenzen interagieren: jede Runtime ist durch
--max-sessionsbegrenzt, während--max-total-sessionsderen Aggregat begrenzt. Die effektive Session-Obergrenze ist das niedrigere einer endlichen Daemon-weiten Obergrenze und der aggregierten pro-Runtime-Obergrenze (betrachte dieses Aggregat als unbegrenzt, wenn das pro-Workspace-Limit unbegrenzt ist). Wenn keines endlich ist, gibt es keine endliche Session-Obergrenze. Eine endliche Obergrenze × 64 Subscriber × 256 Frames ist der Worst-Case In-Flight-Speicher auf EventBus-Ebene; multipliziert mit--max-pending-prompts-per-sessionbegrenzt es die akzeptierte Prompt-Arbeit auf der Zulassungsebene. Die Standard-Größenordnung geht von Single-User- / Small-Team-Last aus; erhöhe schrittweise (und beobachte RSS) für größere Deployments.
MCP-Client-Guardrails (Issue #4175 PR 14). Ein Workspace, der 30 MCP-Server in
mcpServersdeklariert, startet 30 Clients ohne Obergrenze, es sei denn, du setzt eine.--mcp-client-budget=Nbegrenzt die Live-MCP-Client-Anzahl;--mcp-budget-mode={enforce,warn,off}wählt das Verhalten. Standard istwarn, wenn ein Budget gesetzt ist (Snapshot zeigt die Warnung, aber kein Client wird abgelehnt — nützlich zum Messen der realen Fanout vor dem Umschalten auf Enforcement). Abgewiesene Server imenforce-Modus erhaltendisabledReason: 'budget'in ihrer Pro-Server-Zelle, und diebudgets[0]-Zelle zeigtstatus: 'error'+errorKind: 'budget_exhausted'. Slot-Reservierung erfolgt nach Servername und übersteht Reconnects / Discovery-Timeouts — ein abgelehnter Server kann einem gesunden keinen Slot wegnehmen.Aktueller Scope ist Capability-getrieben. Wenn
mcp_workspace_poolvorhanden ist, teilen sich alle Sessions in einer Workspace-Runtime seinen MCP-Transport-Pool und Budget-Controller;GET /workspace/mcpemittiertscope: 'workspace'. Ein zweiter Workspace hat einen unabhängigen Pool und Budget. Wenn der Tag fehlt (einschließlichQWEN_SERVE_NO_MCP_POOL=1), verwendet der Daemon den Legacy-Pro-Session-McpClientManagerund emittiertscope: 'session'; in diesem Fallback kann jede der N Sessions das konfigurierte Limit verbrauchen.qwen serve --mcp-client-budget=10 --mcp-budget-mode=warn # später, nachdem Telemetrie die reale Verteilung zeigt: qwen serve --mcp-client-budget=10 --mcp-budget-mode=enforceDies ist nicht dasselbe wie die
MCP_SERVER_CONNECTION_BATCH_SIZEvon claude-code (die die Startup-Parallelität begrenzt); sie sind orthogonal. Clients müssen aufmcp_workspace_poolverzweigen, nicht allein von der Protokollversion auf einen Scope schließen.Push-Events (Issue #4175 PR 14b). SDK-Clients, die
GET /session/:id/eventsabonniert haben, erhalten typisierte Frames, wenn Budget-Schwellen überschritten werden —mcp_budget_warning(synthetisch, feuert einmal pro aufwärtsgerichteter 75%-Überschreitung mit Hysterese-Neubewaffnung bei 37,5 %, beworben übermcp_guardrail_events) undmcp_child_refused_batch(zusammengefasst einmal pro Discovery-Pass imenforce-Modus; Länge-1 ausreadResource-Lazy-Spawn-Ablehnung). Der Snapshot unterGET /workspace/mcpist weiterhin die Source-of-Truth für den Zustand nach einem Reconnect; Events sind Änderungs-Flanken. Nützlich beim Dashboarden in Echtzeit ohne Polling.
Standard-Bedrohungsmodell für Deployments
- Nur 127.0.0.1 – Loopback-Bind, keine Authentifizierung erforderlich.
--hostname 0.0.0.0erfordert ein Token – der Start wird ohne Token verweigert.LOOPBACK_BINDSumfasst IPv6 –::1und[::1]gelten für die Keine-Token-Regel als Loopback.- Host-Header-Allowlist – Bei Loopback-Binds prüft der Daemon, ob
Host:mitlocalhost:port/127.0.0.1:port/[::1]:port/host.docker.internal:portübereinstimmt (Groß-/Kleinschreibung ignorierend gemäß RFC 7230 §5.4), um sich gegen DNS-Rebinding zu verteidigen. Non-Loopback-Binds (--hostname 0.0.0.0) umgehen absichtlich die Host-Allowlist – der Operator hat die Angriffsfläche gewählt, daher ist das Bearer-Token-Gate die einzige Authentifizierungsebene; Reverse Proxies / SNI / Client-Cert-Pinning liegen in der Verantwortung des Operators, nicht des Daemons. Wenn du Host-basierte Isolierung bei einem Non-Loopback-Bind benötigst, terminiere TLS + prüfe den Host an einem Front-Proxy. - CORS verweigert standardmäßig jeden Browser-Origin – gibt
403JSON zurück. Übergebe--allow-origin <pattern>(wiederholbar, T2.4 #4514), um bestimmte Browser-Origin zuzulassen. Jeder Wert ist entweder das Literal*(beliebiger Origin – der Start wird verweigert, wenn kein Bearer-Token konfiguriert ist;--require-authauf Loopback wird für vollständige Härtung empfohlen, da/healthauf Loopback standardmäßig vor der Authentifizierung bleibt – beachte, dass die Web-Shell-Static-Assets (/,/assets/*,/session/:idDokument-Navigationen) in jedem Modus vor dem Bearer eingehängt werden und auch unter--require-authpre-auth bleiben, verwende also--no-web, wenn die verbleibende Browser-Oberfläche relevant ist) oder ein kanonischer URL-Origin (<scheme>://<host>[:<port>], kein abschließender Schrägstrich / Pfad / Userinfo). Übereinstimmende Origins erhalten korrekte CORS-Antwortheader (Access-Control-Allow-Origin: <echoed>,Vary: Origin, sowie Standard-Methods / -Headers / -Max-Age und offengelegtesRetry-After); nicht übereinstimmende Origins erhalten weiterhin ein 403 mit demselben Envelope wie die Standard-Barriere.caps.features.allow_originwird bedingt beworben, damit SDK- / Web-UI-Clients vor dem Absenden von Cross-Origin-Requests per Pre-Flight prüfen können, ob der Daemon diese akzeptiert. Beispiel:qwen serve --allow-origin http://localhost:3000 --allow-origin http://localhost:5173. Loopback-Self-Origin-Requests (z. B. die Web-Shell-UI) sind davon unberührt – ein separater Origin-Strip-Shim behandelt sie unabhängig von--allow-origin. Browser-Web-UIs ohne konfiguriertes--allow-originfallen weiterhin auf die gleichen Stage-1-Optionen wie zuvor zurück: Pakete sie als native Shell (Electron/Tauri), damit keinOrigin-Header gesendet wird, oder stelle dem Daemon einen Same-Origin-Reverse-Proxy voran. - Chrome-Extension-Browser-Automatisierung ist getrennt von Framing.
qwen serve --allow-origin chrome-extension://<id>lässt die Extension die Web Shell framem und sich mit dem Daemon verbinden. Console/Network/Screenshot/Click-Tools erfordern einen externen CDP-MCP-Adapter-Befehl:QWEN_CDP_MCP_COMMAND=/path/to/cdp-mcp-adapter qwen serve --allow-origin chrome-extension://<id>. Das Haupt-CLI-Paket bündelt keinen Browser-Automatisierungs-Adapter; Clients könnencaps.features.includes('browser_automation_mcp')prüfen, bevor sie diese Tools als verfügbar darstellen. - Ein gestartetes
qwen --acp-Child erhält die effektive Umgebung seiner besitzenden Runtime. Der Daemon friert eine Prozess-Env-Basis ein, wendet das Settings/Env-File-Overlay des Workspaces auf einen Runtime-lokale Snapshot an und schreibt das Overlay niemals zurück inprocess.env; gleichnamige Keys in einer anderen Runtime übergreifen nicht.QWEN_SERVER_TOKENwird vor dem Spawnen entfernt, da der Agent das Daemon-Bearer nicht benötigt. Loader-beeinflussende Variablen (NODE_OPTIONS,npm_config_node_optionsund npm’s Config-File-Weiterleitungen,NODE_PATH,OPENSSL_CONF,NODE_REPL_EXTERNAL_MODULE,npm_config_node_gyp,npm_config_init_module,LD_PRELOAD,LD_AUDIT,DYLD_INSERT_LIBRARIES,BASH_ENV,ZDOTDIR, exportierte Bash-FunktionsdefinitionenBASH_FUNC_*) werden ebenfalls niemals an Session-Subprozesse übergeben — der Daemon entfernt sie aus seiner eigenenprocess.envund aus der Frozen-Base-Env, mit der Session-hostende Children spawnen (die Base-Env behält sie nur unter demDEV=true-Harness, dessen.ts-Einträge den tsx-Loader benötigen), und.env- /settings.json-env-Quellen weisen sie zurück (siehe Settings); dies gilt für jede Session, die der Daemon hostet. Basis-Credentials wieOPENAI_API_KEY,ANTHROPIC_API_KEY,QWEN_*undDASHSCOPE_API_KEYwerden ansonsten durchgereicht, es sei denn, das Runtime-Overlay ändert sie. Dies ist beabsichtigt, keine Sandbox. Der Agent läuft unter derselben UID mit Shell-Tool-Zugriff, sodass unabhängig davon alles in~/.bashrc/~/.aws/credentials/~/.npmrcdurch Prompt-Injection erreichbar ist. Umgebungsisolation zwischen Runtimes ist keine Betriebssystem-Sicherheitsgrenze; führeqwen servenicht unter einer Identität aus, die über Credentials verfügt, die du dem Agenten nicht anvertrauen würdest. - Agent-Text-Lesungen sind child-lokal und folgen den regulären CLI-Permission-Regeln, nicht der Workspace-Dateisystem-Grenze. Direktes
read_filekann Host-Text-Pfade außerhalb jedes registrierten Workspaces erreichen: externe Pfade erfordern standardmäßig eine Bestätigung, und Allow-Regeln oder Genehmigungsmodi können sie automatisch genehmigen. Genehmigte Lesungen verwenden die konfigurierbaren CLI-Output-Limits anstelle der Returned-Output-, Full-Snapshot- und Large-Text-Scan-Caps des Workspace-Dateisystems. Dies gilt für jeden geteilten Text-Lese-Consumer, sodass die Pre-Reads von Write-, Edit-, Notebook-, Sed- und Artifact-Operationen diese Caps zusammen mit dem Lese-Audit des Workspace-Dateisystems, der Symlink-Ablehnung und den Read-seitigen TOCTOU-Schutzmechanismen verlieren — siehe das Lese-Design für die genaue Liste. Da ein Bestätigungs-Payload durch Lesen der Datei erstellt wird, wird ein Out-of-Workspace-Diff an jeden angehängten SSE-Subscriber verteilt, bevor jemand ihn genehmigt — im interaktiven CLI wird dieser Inhalt nur von der Person am Terminal gesehen. Behandle authentifizierte Daemon-Clients wie denselben Sicherheitsprinzipal. HTTP-Dateisystem-Routen bleiben Workspace-scoped und das Verhalten von Agent-Discovery-Tools bleibt unverändert. - Genehmigte finale Writes von eingebauten Text-Tools haben eine schmale Same-Host-Route.
write_file,edit,notebook_editund der simulierte Sed-Editor des Shell-Tools hängen interne Provenienz erst an, nachdem die bestehende Permission-Policy die Ausführung erlaubt. Ihr finaler ACP-Text-Write kann daher einen absoluten Pfad außerhalb des besitzenden Workspaces ohne zweite Bestätigung ansteuern; Allow-Regeln, AUTO/AUTO_EDIT und YOLO verhalten sich wie die CLI, während Ablehnung, Plan, Hook/Guard-Verweigerung und Vor-Ausführungs-Abbruch den finalen Write nicht senden. Abbruch nach Eintritt eines Tools in eine nicht-abbrechbare Dateisystem-Operation behält das bestehende Verhalten des Tools bei. Workspace-Ziele verwenden weiterhin WFS. Externe Ziele verwenden einen Daemon-Host-Writer mit demselben Trust-Snapshot, 5 MiB Encoded-Limit, Leaf-Symlink-Ablehnung, Canonical-Path-Lock, Atomic-Rename, Mode-Erhaltung,0600-New-File-Mode standardmäßig (konfigurierbar — siehe New-file mode for agent text writes), Generation-Guard und Dateisystem-Audit. HTTP-Writes, generische oder nicht markierte ACP-Writes, injizierte Bridge/Workspace-Registry/Factory-Integrationen und beliebige Shell-Redirects erhalten diese Ausnahme nicht. Siehe das External-Write-Design. - Begrenzte SSE-Warteschlangen pro Abonnent – ein langsamer Client, der seine Warteschlange überläuft, erhält ein terminales
client_evicted-Frame und wird getrennt; ein festsitzender Consumer kann den Daemon nicht blockieren. - Limit für die Prompt-Zulassung pro Session – standardmäßig 5 akzeptierte, aber noch nicht abgeschlossene Prompts pro Session. Ein fehlerhafter Client kann keine unbegrenzten Prompt-Promises oder temporäre SSE-Wartezeiten für eine Session in die Warteschlange stellen.
- Graceful Shutdown – SIGINT/SIGTERM fahren die Agent-Child-Prozesse herunter, bevor der Listener geschlossen wird (10s-Frist pro Child-Prozess).
⚠️ Bekannte Lücke in Stage 1 – Berechtigungen sind daemon-global, nicht pro Session (BUy4H).
pendingPermissionslebt im Daemon-Scope; jeder Client, der das Bearer-Token besitzt, kann über jederequestIdfür jede Session abstimmen, die er sehen kann (und SSE-permission_request-Events tragen dierequestIdin ihrem Payload). Dies ist unter dem Single-User- / Klein-Team-Trust-Modell akzeptabel, bei dem jeder authentifizierte Client dieselbe Person oder vertrauenswürdige Mitarbeiter sind. Stage 1.5 wird aufPOST /session/:id/permission/:requestId+ session-scoped Pending-Map + pro-Client-Identität umstellen (Must-have #3 aus dem Downstream-Review); führeqwen servebis dahin nicht hinter einem Bearer aus, das mit nicht vertrauenswürdigen Parteien geteilt wird.⚠️ Bekannte Lücke in Stage 1 –
POST /session/:id/prompt-Body auf 10 MB begrenzt (BUy4L). Multimodale Prompts, die Bilder / PDFs / Audio enthalten und 10 MB überschreiten, schlagen beim Body-Parsing fehl, bevor die Routenlogik ausgeführt wird (kein Streaming, kein Abbruch mitten im Upload). Workaround: Verkleinere den Inhalt clientseitig oder übergebe eine Pfadreferenz und lass den Agenten die Datei überreadTextFilelesen. Stage 1.5 wirdmultipart/form-dataoder Chunked-Encoding auf/promptakzeptieren, damit große Prompts nicht abrupt abbrechen.⚠️ Bekannte Lücke in Stage 1 – Phantom-SSE-Verbindungen hinter NAT. Der Daemon erkennt tote Clients über TCP-Back-Pressure bei Heartbeats (15s-Intervall). Ein Client, der OHNE ein TCP-RST verschwindet (z. B. eine NAT-Box, die Idle-Flows still verwirft), hält den Socket auf Kernel-Ebene “am Leben”, bis die Keepalive-Probes von Node timeouten – standardmäßig typischerweise ~2 Stunden unter Linux. Bei Deployments mit
--hostname 0.0.0.0hinter solchen NATs können sich Phantom-SSE-Verbindungen ansammeln und irgendwann die 256server.maxConnections-Obergrenze erreichen.Setze
--writer-idle-timeout-ms <n>(Issue #4514 T2.9), um die Lücke mit einer expliziten Idle-Frist auf Anwendungsebene zu schließen: wenn fürnms kein Write erfolgreich geflusht wurde, gibt der Daemon ein terminalesclient_evicted-Frame mitreason: 'writer_idle_timeout'aus und schließt den Stream. Das Flag ist standardmäßig deaktiviert, um den Legacy-Vertrag zu erhalten – Operatoren in Netzwerken, die RSTs verschlucken, sollten einen Wert deutlich über dem 15s-Heartbeat-Intervall wählen (z. B.60000–300000), damit legitime Idle-Verbindungen nicht getrennt werden, während wirklich festsitzende Writer promptly bereinigt werden. Führecaps.features.includes('writer_idle_timeout')per Pre-Flight in deinem SDK aus, um zu bestätigen, dass der Daemon dies unterstützt.
Deadlines und Writer-Idle-Timeout
Issue #4514 T2.9 liefert zwei Opt-in-Flags, die die Lücken bei langlaufenden / Remote-Deployments schließen, die der 15s-Heartbeat + AbortSignal nicht abdecken. Der shared Permission-Response-Timeout ist hier ebenfalls aufgeführt. Alle drei sind standardmäßig deaktiviert.
| Flag | Env var | Default | What it does |
|---|---|---|---|
--prompt-deadline-ms <n> | QWEN_SERVE_PROMPT_DEADLINE_MS | nicht gesetzt | Serverseitige Wallclock-Obergrenze für ein einzelnes POST /session/:id/prompt. Bei Ablauf bricht der Daemon den AbortController des Prompts ab und gibt HTTP 504 mit {code:"prompt_deadline_exceeded", errorKind:"prompt_deadline_exceeded", deadlineMs:n} zurück. Ein deadlineMs-Feld im Request-Body pro Prompt kann die effektive Frist unterhalb des Flags verkürzen, aber niemals verlängern. Capability-Tag (bedingt): prompt_absolute_deadline. |
--writer-idle-timeout-ms <n> | QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS | nicht gesetzt | Idle-Frist pro SSE-Verbindung. Wenn für n ms kein Write ERFOLGREICH geflusht wurde – weder ein echtes Event noch der 15s-Heartbeat – gibt der Daemon ein terminales client_evicted-Frame mit data.reason = 'writer_idle_timeout' (gespiegelt in data.errorKind) aus und schließt den Stream. Wähle einen Wert komfortabel über dem 15s-Heartbeat (z. B. 30000–300000), damit legitime Idle-Streams nicht getrennt werden; Werte < 15000 TRENNEN ansonsten gesunde Idle-Verbindungen, bevor der erste Heartbeat feuert (nur beabsichtigt für Tests / kurzlebige Dev-Sessions). Capability-Tag (bedingt): writer_idle_timeout. |
--permission-response-timeout-ms <n> | — | 0 | Gemeinsame Wallclock für Ordinary-Permission- und ask_user_question-Antworten im Daemon-Modus. 0 oder ein weggelassenes Flag wartet unbegrenzt; eine positive Ganzzahl erzwingt eine Frist für beide. Voter-Abbruch, Session-Abbruch und Daemon-Shutdown lösen pending Interaktionen weiterhin auf, wenn der Timer deaktiviert ist. |
Die Prompt- und Writer-Flags akzeptieren eine positive Ganzzahl in Millisekunden; 0, NaN, nicht-ganzzahlige oder negative Werte werden beim Start mit einer klaren Fehlermeldung abgelehnt. Der Permission-Response-Timeout akzeptiert 0 oder eine positive Ganzzahl. Für die beiden Umgebungsvariablen-gestützten Deadlines haben explizite ServeOptions-Felder Vorrang vor Umgebungsvariablen-Werten. SDK-Nutzer sollten den passenden Capability-Tag per Pre-Flight prüfen, bevor sie sich auf das Prompt- und Writer-Verhalten verlassen.
New-file mode for agent text writes
Agent-Text-Schreibvorgänge (write_file, edit, notebook_edit und der simulierte Sed-Editor des Shell-Tools) werden über den atomaren Writer des Daemons veröffentlicht, der den Modus einer bestehenden Datei beibehält und — für neue Dateien — standardmäßig auf 0600 (nur Owner) setzt, unabhängig von der umask des Daemon-Prozesses. Dieser fail-closed Standard ist beabsichtigt: Eine frisch vom Agenten erstellte Datei ist niemals versehentlich für Gruppe/Other lesbar, egal wie permissiv die Supervisor-umask ist.
Betreiber, deren Deployment-Konvention umask-gesteuert ist (z. B. eine systemd-Unit mit UMask=0002, Shared-Group-Repositories), können neue Dateien für die Standard-POSIX-Behandlung aktivieren mit:
| Env-Variable | Werte | Standard | Wirkung |
|---|---|---|---|
QWEN_SERVE_NEW_FILE_MODE | owner | system | owner | system erstellt NEUE Dateien mit 0o666 & ~umask, sodass vom Agenten erstellte Dateien der umask des Daemon-Prozesses folgen wie jeder andere Prozess auf der Maschine. owner behält den umask-unabhängigen 0600-Standard. Werte sind case-insensitive; das Literal 0600 wird als Alias für owner akzeptiert (keine anderen Oktalmodi werden unterstützt), und jeder andere Wert wird mit einer stderr-Warnung abgelehnt und der 0600-Standard beibehalten. |
Geltungsbereich und Grenzen:
- Gilt für NEUE Dateien, die von den Text-Write-Routen erstellt werden (Workspace-Ziele, der Same-Host-External-Host-Writer und HTTP-Text-Schreibvorgänge). Bestehende Dateien behalten immer ihren On-Disk-Modus — das Bearbeiten eines
0600-Secrets lässt es0600, eine ausführbare Datei behält+x. - Binär-Uploads (
POST /file/upload) erstellen immer mit0600, unabhängig von dieser Einstellung. - Der Daemon liest die Variable bei der Konstruktion des Workspace-Dateisystems; starte den Daemon nach einer Änderung neu.
Multi-Session- & Multi-Workspace-Deployment
Übergib --workspace mehr als einmal, um mehrere nicht überlappende Workspaces in einem qwen serve-Prozess zu registrieren. Der erste Pfad ist primär. Jeder registrierte Workspace besitzt eine isolierte Runtime-Grenze, während der Daemon-weite Listener, die Authentifizierungs-Policy und das Gesamt-Session-Limit geteilt werden. Production versucht, das primäre ACP-Child für Kompatibilität vorzuwärmen und wiederholt den ersten Versuch nach einem Fehlschlag; vertrauenswürdige Secondaries starten ihr eigenes Child bei Bedarf, und nicht vertrauenswürdige Secondaries starten keinen ACP. Anfragen können einen registrierten Workspace nach kanonischem cwd auswählen; Anfragen, die cwd weglassen, verwenden den primären Workspace. Verwende einen Daemon pro Benutzer oder Sicherheitsprinzipal; Workspace-Trust ist ein Ausführungs-Gate, keine ACL.
Ein nicht vertrauenswürdiger Secondary-Workspace ist in der Web Shell als untrusted und read-only sichtbar. Er kann erweitert werden, um den persistierten Session-Katalog zu inspizieren, aber er kann noch nicht in der Web Shell ausgewählt oder geöffnet, fortgesetzt, zur Erstellung von Sessions verwendet oder vollständig exportiert werden. Die REST-API folgt der bestehenden begrenzten Dateisystem-Lese-Policy und legt auch ihren persistierten Session-Gruppen-Katalog und, wenn workspace_persisted_transcript beworben wird, ihr aktives persistiertes Transkript über den begrenzten Workspace-qualifizierten Pager offen. Diese Lesungen enthalten keinen Live-Runtime-State und starten kein ACP-Child. Der vollständige Workspace-qualifizierte Export erfordert einen vertrauenswürdigen Workspace und die separate workspace_session_export-Capability. Vertraue dem Workspace und starte den Daemon neu, bevor du Ausführungs-, Mutations- oder Export-Features verwendest. Ein nicht vertrauenswürdiger primärer Workspace bleibt in der Web Shell deaktiviert.
Verwende separate Daemon-Prozesse, wenn du eine kleinere Fehler- oder Sicherheitsgrenze, unabhängige Bearer-Tokens, Quotas, Audit-Grenzen, Betriebssystem-Isolation oder unabhängige Ressourcen-Überwachung benötigst. Der Multi-Workspace-Modus ist dafür gedacht, dass ein Operator mehrere Repos hostet; er ist keine Multi-Tenant-Isolationsgrenze. Ein einzelner Daemon-Token autorisiert jede Route, die der Daemon exposed, einschließlich des erlaubten Lese-Katalogs für alle registrierten Workspaces.
Abonniere BEVOR du
modelServiceIdbeim Attach postest. Wenn ein ClientPOST /sessionmit einermodelServiceIdaufruft und der Workspace bereits eine Session hat, die ein anderes Modell ausführt, gibt der Daemon einen internensetSessionModel-Aufruf aus – Fehler werden NICHT als HTTP-Fehler propagiert (die Session bleibt auf ihrem aktuellen Modell betriebsbereit). Das sichtbare Fehlersignal ist einmodel_switch_failed-Event im SSE-Stream der Session. Wenn duPOST /sessionaufrufst und erst DANNGET /session/:id/eventsöffnest, verpasst du das Fehler-Event und sprichst stillschweigend weiter mit dem falschen Modell. Öffne zuerst den SSE-Stream oder übergebeLast-Event-ID: 0beim Abonnieren, um das älteste verfügbare Event des Rings erneut abzuspielen.
Um mehrere Benutzer oder Sicherheitsprinzipale zu verwalten (jeder mit einem unabhängigen Token, Quota, Audit-Log, Sandbox oder Prozess-Fehlgrenze) oder um über die Reichweite eines einzelnen Prozesses hinaus zu skalieren (Cold-Start-Budget, FD-Anzahl, RSS), starte einen Daemon pro Prinzipal hinter einem externen Orchestrator. Jeder solcher Daemon kann weiterhin mehrere Workspaces für diesen Prinzipal hosten. Der Orchestrator (Multi-Tenancy / OIDC / Quota / Audit / k8s) liegt außerhalb des Scopes des qwen-code-Projekts – siehe Issue #3803 “External Reference Architecture” für die Design-Hinweise.
Laden und Fortsetzen einer persistierten Session
Der Daemon legt ACPs session/load- und Resume-Flow über HTTP offen, plus einen separaten Lese-Transkript-Pager:
| Route | Use when |
|---|---|
POST /session/:id/load | Der Client hat keine nützliche lokale Historie gerendert (Cold-Reconnect, Picker-then-Open). Für eine Live-Sitzung gibt der Daemon das aktuelle begrenzte Replay-Snapshot-Fenster zurück und injiziert es; wenn älteres Replay verworfen wurde, beginnt der Snapshot mit history_truncated. Capability-Tag: session_load. |
POST /session/:id/resume | Der Client hat die Turns bereits auf dem Bildschirm und benötigt nur das daemon-seitige Handle zurück. Der Modellkontext wird auf Agent-Seite ohne UI-Replay wiederhergestellt – der SSE-Stream bleibt sauber. Capability-Tag: session_resume (unstable_session_resume bleibt ein deprecated Alias für ältere Clients). |
GET /session/:id/transcript | Der Client benötigt das vollständige aktive persistierte Transkript. Es gibt id-loser Replay-Frames in Cursor-Seiten zurück und ruft nicht /load auf, hängt keinen Client an, seedet den Live-EventBus, erstellt keine Live-Sitzung oder verändert das Live-Replay-Fenster. Capability-Tag: session_transcript. |
GET /workspaces/:workspace/session/:id/transcript | Der Client benötigt ein aktives persistiertes Transkript aus einem ausgewählten Workspace, ohne ACP zu starten oder Workspace-Settings zu laden. Registrierte nicht vertrauenswürdige Secondary-Workspaces können diesen lesegeschützten Pfad verwenden. Capability-Tag: workspace_persisted_transcript. |
GET /workspaces/:workspace/session/:id/export | Der Client benötigt einen vollständigen html-, md-, json- oder jsonl-Anhang aus einem ausgewählten vertrauenswürdigen Workspace. Er liest aktiven persistierten Speicher, ohne ACP zu starten oder auf primär zurückzufallen. Capability-Tag: workspace_session_export. |
GET /workspaces/:workspace/session/:id/archive/export | Der Client benötigt dieselben Anhangsformate aus archiviertem persistiertem Speicher in einem ausgewählten vertrauenswürdigen Workspace. Er archiviert nicht um, startet ACP nicht oder fällt auf eine aktive oder primäre Sitzung zurück. Capability-Tag: workspace_archived_session_export. |
Für Load und Resume macht das TypeScript-SDK statische Factories auf
DaemonSessionClient verfügbar:
import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk';
const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170' });
// Cold reconnect — daemon will replay the bounded snapshot window through SSE.
const session = await DaemonSessionClient.load(client, 'persisted-id');
// Or, if your UI already has the history, skip the replay:
// const session = await DaemonSessionClient.resume(client, 'persisted-id');
for await (const event of session.events()) {
// First the replayed `session_update` frames (load only),
// then live events.
}Führe caps.features.session_load, caps.features.session_resume oder caps.features.session_transcript vor dem Aufruf der passenden Route per Pre-Flight aus – ältere Daemons geben 404 zurück. unstable_session_resume wird weiterhin als deprecated Kompatibilitäts-Alias beworben. Gleichzeitige Same-Action-Requests für dieselbe ID werden zusammengeführt; Cross-Action-Races (ein load, der mit einem resume wetteifert) und Caller-supplied-id-Spawns, die mit einem Restore wetteifern, erhalten 409 restore_in_progress mit Retry-After: 5. Ein Restore, der limits.sessionRestoreTimeoutMs überschreitet, erhält retryable 504 session_restore_timeout mit einem Budget-abgeleiteten Retry-After (auf 5-120s begrenzt); der noch laufende Child-Request bleibt eingezäunt, bis die Bereinigung abgeschlossen ist, und gleichzeitige Retries mit derselben ID erhalten in diesem Fenster 409 restore_in_progress mit reason: awaiting_abandoned_cleanup und einem Budget-abgeleiteten Retry-After, das auf 5-120 Sekunden begrenzt ist, anstatt einer festen 5-Sekunden-Verzögerung. Wenn die Bereinigung unklar ist oder der aufgegebene Restore nach seiner Frist noch immer kein vollständiges Restore-Budget beglichen hat, erhält frische Session-Arbeit vorübergehend 503 acp_channel_unavailable mit reason: restore_cleanup_failed oder restore_settlement_overdue, während bereits aktive Sessions weiterhin nutzbar bleiben. Siehe die Protokollreferenz für das vollständige Fehler-Envelope.
Für das vollständige persistierte Replay, seite mit DaemonClient.getSessionTranscriptPage(sessionId, { cursor, limit }) oder der rohen REST-Route:
curl "http://127.0.0.1:4170/session/$SESSION_ID/transcript?limit=100"Für einen registrierten Workspace verwende client.workspaceById(workspaceId).getSessionTranscriptPage(sessionId, { cursor, limit }) oder /workspaces/:workspace/session/:id/transcript. Die Workspace-qualifizierte Methode verwendet immer natives REST, selbst wenn der SDK-Client einen ersetzbaren ACP-Transport hat. Seine Cursor sind nur für die Daemon-Lebensdauer gültig und müssen nach einem Daemon-Neustart von Seite eins neu gestartet werden.
Für einen vollständigen Anhang aus einem vertrauenswürdigen registrierten Workspace, führe workspace_session_export per Pre-Flight aus und rufe client.workspaceById(workspaceId).exportSession(sessionId, { format: 'html' }) oder die rohe Route /workspaces/:workspace/session/:id/export auf. Leite die Unterstützung nicht von session_export oder workspace_qualified_rest_core ab: ältere Daemons können beide bewerben und dennoch nur den primären Export beibehalten. Die aktuelle Web-Shell-Export-Aktion bleibt primär-only; verwende das SDK oder die REST-Route für einen anderen Workspace.
Für einen archivierten Anhang, führe workspace_archived_session_export per Pre-Flight aus und rufe client.workspaceById(workspaceId).exportArchivedSession(sessionId, { format: 'html' }) oder /workspaces/:workspace/session/:id/archive/export auf. Dieser Pfad liest archivierten Speicher direkt und gibt 409 session_not_archived für eine nur-aktive ID zurück; er archiviert die Sitzung nicht um. Die Web Shell legt denselben Export für archivierte Zeilen in vertrauenswürdigen primären und sekundären Workspaces offen, wenn die Capability vorhanden ist.
limit zählt aktive Chat-Datensätze, nicht emittierte Replay-Frames; ein Datensatz kann mehrere session_update-Events erzeugen. Die erste Antwort friert die JSONL-Snapshot-Größe ein und gibt nextCursor zurück, während hasMore wahr ist. Spätere Seiten ignorieren Anhänge nach Seite 1, geben aber 409 zurück, wenn die Datei gelöscht, gekürzt, ersetzt, archiviert wurde oder anderweitig mit dem eingefrorenen Cursor in Konflikt steht. Sehr große Snapshots geben 413 transcript_too_large vor der Indizierung zurück, damit der Daemon keine unbegrenzten Transkriptdateien auf dem Request-Pfad scannt.
Für wiederholtes Paging über die Legacy-Singularroute setze --channel-idle-timeout-ms auf einen positiven Wert. Beim Standard 0 wird das ACP-Child eines idle Workspaces – und der In-Prozess-Transkript-Index-Cache, den es hält – nach jeder Seite abgebaut, sodass jede Seite das Child neu startet und den Index durch erneutes Scannen des gesamten eingefrorenen Präfix neu aufbaut (O(snapshotSize) pro Seite). Ein positiver Timeout hält das Child über den Cursor-Walk hinweg am Leben, sodass es seinen zwischengespeicherten Transkript-Index und Replay-Config wiederverwendet. Die Workspace-qualifizierte persistierte Route startet niemals ein ACP-Child und ist von diesem Timeout nicht betroffen.
Hinweis: Das erneute Abspielen der Live-Sitzungs-Historie wird doppelt begrenzt: durch den SSE-Ring für Last-Event-ID-Reconnects und durch --compacted-replay-max-bytes für den Snapshot, der von POST /session/:id/load zurückgegeben wird. Lange Historien mit gesprächigen Turns können eine der beiden Grenzen überschreiten. Der Daemon zeigt Snapshot-Kürzung mit history_truncated an; verwende /transcript, wenn du die vollständige aktive persistierte Historie benötigst.
Durability-Modell
Sessions sind in Stage 1 über Daemon-Neustarts hinweg weiterhin ephemer, aber persistierte Sessions auf der Festplatte können neu geladen werden:
- Ein Child-Prozess-Crash veröffentlicht
session_diedund entfernt die Live-Session aus den Maps des Daemons. Die persistierte Session auf der Festplatte kann überPOST /session/:id/loadneu geladen werden, wenn ein frischer Agent-Child-Prozess gestartet werden kann. - Ein Daemon-Neustart verliert jede In-Flight-Live-Session. Die persistierten Sessions bleiben auf der Festplatte und können gegen einen neuen Daemon-Prozess geladen werden, vorbehaltlich derselben Workspace-Binding-Regeln.
- Lange Client-Disconnects (>5 Min. bei einem gesprächigen Turn) können den SSE-Replay-Ring überholen (standardmäßig 8000 Frames) –
Last-Event-ID-Reconnect löststate_resync_requiredaus. Für Mobile- / instabile Netzwerk-Clients, plane das erneute Öffnen von SSE bei langen Ausfällen oder rufePOST /session/:id/loadauf, um den aktuellen begrenzten Replay-Snapshot wiederherzustellen; gehe nicht davon aus, dass die Route das vollständige Transkript zurückgibt. - Dateioperationen (
writeTextFile) sind über Crashes hinweg atomar (Write-then-Rename); sie sind über Daemon-Neustarts hinweg nicht im Sinne eines Replays atomar – der Datei-Write ist entweder angekommen oder nicht.
Wenn deine Integration serverseitige Cross-Restart-Durability benötigt, die über das hinausgeht, was session/load abdeckt (z. B. serververwaltete Retry-Queues), benötigst du weiterhin eine State-Recovery auf Anwendungsebene. Halte keinen langlaufenden, Neustart-sensitiven Zustand innerhalb der Session des Daemons.
Stage 1.5+ Runtime-Garantien
Der Vertrag von Stage 1 ist auf Prototyping ausgelegt. Gemäß #3889 chiga0 downstream-consumer review sind die folgenden Punkte nicht in Stage 1 enthalten – produktionsreife Integrationen benötigen Stage 1.5+, bevor sie sich darauf verlassen: Blocker für den ernsthaften Einsatz in nachgelagerten Systemen:
loadSession/unstable_resumeSessionüber HTTP — ohne dies kann keine Integration einen Child-Crash oder Daemon-Neustart überstehen, und auch ein Orchestrator, der den Daemon koordiniert, kann den Zustand nicht wiederherstellen.- Persistente Client-Identität (Pair-Tokens + client-spezifische Sperrung) — Stufe 1 verwendet einen gemeinsamen Bearer; ein geleakter Token sperrt alle, und
originatorClientIdwird vom Client selbst deklariert, anstatt vom Daemon basierend auf der authentifizierten Identität gestempelt zu werden.
Zuverlässigkeits-Baseline:
Vom Client initiierter Heartbeat-Pfad— ausgeliefert über #4175 PR 9.POST /session/:id/heartbeatzeichnet zuletzt gesehene Zeitstempel auf dem Daemon auf (Capability-Tagclient_heartbeat); SDK-Helper sindDaemonClient.heartbeat()/DaemonSessionClient.heartbeat().permission_already_resolved-Event, wenn ein Vote das First-Responder-Rennen verliert — derzeit müssen UIs den Zustand aus einem404ableiten.Größerer Replay-Ring— auf 8000 erhöht. Pro Sitzung konfigurierbarer Ring noch offen — Mobile- / Chatty-Turn-Workloads benötigen möglicherweise session-spezifische Overrides.slow_client_warning-Event vorclient_evicted— sanfter Backpressure, damit sich gutmütige, langsame Clients selbst drosseln können (Render-Tiefe reduzieren, Chunks verwerfen), bevor sie beendet werden.
Ergonomie der Integration:
POST /session/:id/_metafür IM-artigen Kontext — Pro-Sitzung Key-Value, das nachfolgenden Prompts angehängt wird (Chat-ID, Absender, Thread-ID), ersetzt die Improvisation pro Kanal.- Tatsächliche Feature-Verhandlung über
/capabilities—protocol_versions: { acp: '0.14.x', daemon_envelope: 1 }, damit Clients Drift erkennen können, anstatt auf “unknown frame, ignore” zurückzufallen. - Erstklassige Durability-Dokumentation (dieser Abschnitt) — bereits weiter oben ausgeliefert.
Die vollständige Convergence-Roadmap wird in #3803 verfolgt.
Grenzen des Stage-1-Scopes — was wir in Stage 1.5 nicht beheben werden
Zwei strukturelle Entscheidungen sind explizite Non-Goals für die Stage 1 / 1.5 / 2 Mainline-Roadmap. Wenn dein Use-Case von einem davon abhängt, plane drumherum, anstatt auf uns zu warten.
Session-State ist nur lokale Mutation (gemäß LaZzyMan Review #4270256721 )
Der Stage-1.5-Plan beschreibt die TUI als In-Process-EventBus-Subscriber. In der Praxis ist die TUI-UI strikt größer als das Wire-Protokoll:
- Nur lokale UI — die ~15 Ink-Dialogkomponenten (
ModelDialog,MemoryDialog,PermissionsDialog,SessionPicker,WelcomeBackDialog,FolderTrustDialog, …) und dielocal-jsx-Slash-Commands (/ide,/auth,/init,/resume,/rename,/delete,/language,/arena, …) rendern terminalspezifisches Ink-JSX. Remote-Clients über HTTP/SSE können Ink nicht äquivalent rendern, und diese Flows emittieren kein Wire-Event. - Session-State-Mutationen ohne Wire-Events —
/approval-mode,/memory add,/mcp add-server,/agents,/tools enable/disable,/auth,/init(Schreiben vonCLAUDE.md) ändern alle das Agent-Verhalten, aber nur/modelpubliziert derzeit ein Event (model_switched).
Stage-1-Entscheidung — Option (A) aus dem Review: Diese Mutationen nicht zu Wire-Events befördern. Die beiden Deployment-Modi haben unterschiedliche Konsequenzen.
Modus 1 — Headless qwen serve (dieser PR)
Keine TUI-Shell läuft innerhalb des Daemons. Die oben aufgeführten Slash-Commands existieren nicht in diesem Modus — es gibt keine Terminal-UI, von der aus sie aufgerufen werden könnten. Der Session-State ist daher:
- Beim Booten eingefroren für
approval-mode/memory/agents/tools-Allowlist /auth— alles wird aus den Settings + Festplatte geladen, wenn derqwen --acp-Child des Daemons startet; unveränderlich für die Lebensdauer der Session. Über Settings definierte MCP-Server sind ebenfalls beim Booten eingefroren, aber zur Laufzeit hinzugefügte Server (überPOST /workspace/mcp/servers) können ohne Neustart hinzugefügt oder entfernt werden. - Über HTTP veränderbar via
POST /session/:id/model(publiziertmodel_switched),POST /workspace/mcp/servers/DELETE /workspace/mcp/servers/:name(publiziertmcp_server_added/mcp_server_removed) und Permission-Votes (POST /permission/:requestId).
Konsequenz: Remote-Clients im Headless-Modus sehen den vollständigen Session-State. Keine TUI verbirgt zusätzlichen Zustand; Drift ist nicht möglich. Wenn du approval-mode ändern möchtest, starte den Daemon mit neuen Settings neu. MCP-Server können jetzt zur Laufzeit über die Mutations-Routen hinzugefügt/entfernt werden (POST /workspace/mcp/servers, DELETE /workspace/mcp/servers/:name) — siehe Runtime MCP server management.
Modus 2 — Stage 1.5 qwen --serve co-gehostete TUI (nicht in diesem PR)
Wenn Stage 1.5 qwen --serve ausliefert (der TUI-Prozess hostet denselben HTTP-Server mit), existiert die TUI neben den Remote-Clients. Ein lokaler Operator, der /approval-mode yolo oder /mcp add-server eingibt, mutiert den Session-State, und Remote-Clients über HTTP haben kein Event, um die Änderung zu beobachten.
In diesem Modus ist die TUI ein “Super-Client” — sie beobachtet dieselbe Agent-Konversation wie die Remote-Clients UND kann den Session-State mutieren, was Remote-Clients nicht können. Die Asymmetrie ist:
- ✅ Sowohl TUI als auch Remote-Clients sehen dieselben Agent-Nachrichten, Tool-Calls, File-Diffs und Permission-Prompts.
- ❌ Nur die TUI sieht / mutiert Approval-Mode / Memory / MCP-Server-Liste / Agents / Tools-Allowlist / Auth-State.
Konsequenz in Modus 2: Wenn eine Remote-Client-UI versucht, die Session-Settings zu spiegeln, kann sie nach jedem TUI-Slash-Command driften. Remote-Clients sollten bei Attach / Reconnect den Zustand neu abrufen (verwende Last-Event-ID: 0, um das älteste Event des Rings für Dinge wie model_switched abzuspielen); sie sollten sich NICHT auf inkrementelle Events für TUI-seitige Mutationen verlassen.
Warum (A) und nicht (B) (Mutationen in die session_state_changed-Event-Familie befördern)
(B) ist die ambitioniertere Antwort, bindet Stage 1.5 aber an eine wesentlich größere Wire-Surface, die auch sauber durch das geplante In-Process-Refactoring laufen muss. Wir gehen lieber ehrlich den kleineren Scope. Die Arbeit an der Session-State-Event-Taxonomie — die Aufzählung, welche TUI-Flows by Design nur lokal sind gegenüber denen, die plausibel unter einer zukünftigen Opt-in-(B)-artigen Erweiterung zum Wire befördert werden könnten — wandert nach #3803 , nicht in den Stage-1.5-Code.
N parallele Sessions teilen sich ein qwen --acp-Child pro Workspace-Runtime
Mehrere Sessions auf demselben vertrauenswürdigen Workspace teilen sich den qwen --acp-Child-Prozess dieser Runtime über die native Multi-Session-Unterstützung des Agents (packages/cli/src/acp-integration/acpAgent.ts:194: private sessions: Map<string, Session>). Die Bridge ruft connection.newSession({cwd, mcpServers}) für jede Session auf — der Agent speichert sie in seiner Sessions-Map und demultiplext die sessionId pro Aufruf. Production kann bis zu ein primäres Child besitzen (Vorwärmung standardmäßig versucht) plus ein On-Demand-Child pro vertrauenswürdigem Secondary; nicht vertrauenswürdige Secondaries besitzen keines.
Konkreter Aufwand bei N=5 Sessions auf demselben Workspace:
| Ressource | Pro Session | Bei N=5 |
|---|---|---|
| Daemon-Node-Prozess | einer | 30–50 MB (ein Daemon) |
qwen --acp-Child | geteilt | 60–100 MB (ein Child) |
| MCP-Server-Children | Workspace-Pool wenn beworben; sonst pro Session | geteilt nach passenden Pool-Einträgen, oder bis zu 3×N im Legacy-Fallback |
FileReadCache (im-Child-Heap) | geteilt | einmal geparst |
CLAUDE.md / Hierarchie-Memory-Parse | geteilt | einmal geparst |
| OAuth-Refresh-Token-State | geteilt | ein Refresh-Pfad |
| Auto-Memory gelernte Fakten | geteilt | eine Knowledge-Base pro Child |
| Cold Start | nur der erste | <200 ms nach der ersten Session |
Jede aktive Workspace-Runtime hält eine Bridge-Grenze. Production versucht, den primären Kanal vorzuwärmen und wiederholt den ersten Versuch nach einem Fehlschlag; ein vertrauenswürdiger Secondary öffnet seinen Kanal und Child bei Bedarf, während ein nicht vertrauenswürdiger Secondary dies niemals tut. Ein Kanal bleibt am Leben, solange mindestens eine Session aktiv ist. Nach dem letzten killSession beendet die Runtime ihr Child standardmäßig sofort oder nach der konfigurierten Kanal-Idle-Gnade; ein Crash auf Kanal-Ebene reißt es ebenfalls ab, ohne eine andere Runtime auszuwählen.
MCP-Server-Children verwenden den Workspace-weiten Transport-Pool, wenn mcp_workspace_pool beworben wird: übereinstimmende (Workspace-Runtime, Servername, Config-Fingerprint)-Einträge werden über Sessions hinweg refgezählt. Wenn die Capability fehlt, spawnt der Legacy-Pro-Session-Manager sie unabhängig.
Peer-Agents (Cursor / Continue / Claude Code / OpenCode / Gemini CLI) setzen alle auf Single-Process-Multi-Session. qwen-code zieht auf Agent-Ebene gleich; die Stage-1-Bridge in diesem PR macht dieselbe Architektur über HTTP sichtbar.
Einloggen in einen Remote-Daemon (Issue #4175 PR 21)
Wenn der Daemon auf einem Remote-Pod läuft (keine gemeinsame Anzeige mit dir), kann ein Client einen OAuth-Device-Flow über HTTP auslösen. Der Daemon pollt den IdP selbst; deine Aufgabe ist es nur, eine URL auf einem beliebigen Gerät mit einem Browser zu öffnen.
Die Qwen OAuth Free Tier wurde am 15.04.2026 eingestellt. Die folgenden qwen-oauth
Beispiele dokumentieren die Form des Device-Flow-Protokolls und den Legacy-Provider-Identifier;
neue Setups sollten einen derzeit unterstützten Auth-Provider verwenden.
# 1. Starte einen Flow. Der Daemon kontaktiert den IdP und gibt einen Code + eine URL zurück.
curl -X POST http://127.0.0.1:4170/workspace/auth/device-flow \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"providerId":"qwen-oauth"}'
# → 201 {
# "deviceFlowId": "fa07c61b-…",
# "userCode": "USER-1",
# "verificationUri": "https://chat.qwen.ai/api/v1/oauth2/device",
# "verificationUriComplete": "https://chat.qwen.ai/...?user_code=USER-1",
# "expiresAt": 1700000600000,
# "intervalMs": 5000,
# "attached": false
# }
# 2. Öffne die URL auf deinem Handy / Laptop und gib den User-Code ein.
# 3. Pollen auf Abschluss (oder SSE für das auth_device_flow_authorized-Event abonnieren):
curl http://127.0.0.1:4170/workspace/auth/device-flow/fa07c61b-… \
-H "Authorization: Bearer $TOKEN"
# → Status-Übergänge: pending → authorizedDas TypeScript-SDK kapselt beide Schritte in einem einzigen Helper:
import { DaemonClient } from '@qwen-code/sdk';
const client = new DaemonClient({ baseUrl, token });
const flow = await client.auth.start({ providerId: 'qwen-oauth' });
console.log(`Open ${flow.verificationUri}\nCode: ${flow.userCode}`);
const result = await flow.awaitCompletion({ signal: abortCtrl.signal });
// result.status === 'authorized'Der Daemon öffnet niemals einen Browser in deinem Namen. Auch bei lokaler Ausführung bleibt der Daemon passiv — er gibt die URL zurück und überlässt es dem SDK / Benutzer, zu entscheiden, wo sie geöffnet wird. Das ist Absicht: Ein Daemon auf einem Headless-Pod, der xdg-open aufrufen würde, würde stillschweigend fehlschlagen und die eigentliche Auth-Surface verschleiern. Spiegle die UX von gh auth login (“Press Enter to open browser”) in deinem Client nach.
--require-auth und Entwickler-Komfort. Die Device-Flow-Routen verwenden das strikte Mutations-Gate (PR 15), was bedeutet, dass ein Loopback-Default ohne Token 401 token_required zurückgibt. Lokal ist der einfachste Weg, dies während der Entwicklung zu umgehen, qwen serve --token=dev-token; du brauchst --require-auth nur, wenn du den Loopback-Default absicherst.
Daemon-übergreifende Einschränkung. oauth_creds.json wird Daemon-übergreifend geteilt (~/.qwen/oauth_creds.json), sodass ein erfolgreicher Login in Daemon A automatisch beim nächsten Token-Refresh von Daemon B übernommen wird — aber die SDK-Clients von Daemon B erhalten nicht das auth_device_flow_authorized-Event (Events sind pro Daemon).
Client-übergreifende Übernahme. Zwei SDK-Clients auf demselben Daemon, die beide POST /workspace/auth/device-flow für denselben Provider aufrufen, erhalten das Pro-Provider-Singleton: Der erste Aufruf startet eine frische IdP-Anfrage und gibt attached: false zurück; der zweite Aufruf gibt den BESTEHENDEN In-Flight-Eintrag mit attached: true zurück. Die Übernahme wird im Audit-Trail protokolliert (unter der X-Qwen-Client-Id des zweiten Clients), emittiert aber KEIN separates Event — beide Clients beobachten schließlich dasselbe auth_device_flow_authorized, sobald der Benutzer die IdP-Seite abgeschlossen hat. Wenn deine UI zwischen “Ich habe das gestartet” und “Ich bin dem Flow eines anderen beigetreten” unterscheidet, verzweige basierend auf dem von start() zurückgegebenen attached-Feld.
Daemon-Logdatei
qwen serve hängt Diagnose-Datensätze über normale Neustarts hinweg am
stabilen aktiven Pfad an:
${QWEN_RUNTIME_DIR or ~/.qwen}/debug/daemon/daemon.logJeder Datei-Datensatz enthält eine zufällige pro-Start-runId und die
Daemon-PID. Ein erfolgreicher stabiler Eigentümer aktualisiert auch
debug/daemon/latest auf daemon.log auf Plattformen, die Symlinks
unterstützen. Unter macOS/Linux folge der Rotation mit:
tail -F ~/.qwen/debug/daemon/daemon.logAuf anderen Plattformen konfiguriere den Viewer, den Pfadnamen nach dem Ersetzen erneut zu öffnen. Ein Viewer, der nur das alte Datei-Handle behält, bleibt nach der Rotation auf dem Archiv.
Das Log erfasst Lifecycle-Nachrichten, Route-Fehler (mit route=- und
sessionId=-Kontext), ACP-Child-Stderr und — wenn QWEN_SERVE_DEBUG=1
gesetzt ist — zusätzliche Bridge-Breadcrumbs. Zeilen, die heute nach
Stderr gehen, gehen weiterhin nach Stderr; das Datei-Log ist additiv,
kein Ersatz.
Die aktive Datei rotiert, bevor sie 10 MiB überschreiten würde. Jede
Familie behält vier Archive unter archive/, und jeder Datei-Datensatz
ist auf 256 KiB begrenzt. Die In-Memory-Queue akzeptiert höchstens 4 MiB
an unsettled Datei-Payload. Queue-Druck, Rotationsfehler oder
Dateisystemfehler können daher Datei-Kopien verlieren;
GET /daemon/status?detail=full legt Logger-Gesundheit, Probleme und
Zähler für verworfene Datensätze/Bytes offen.
Nur ein Daemon darf die stabile Familie in einem Log-Namespace besitzen.
Ein gleichzeitiger Daemon schreibt nach
debug/daemon/runs/run-<runId>/daemon.log; das Startup-Banner und der
vollständige Status enthalten den autoritativen Pfad.
runs/recent-fallback ist ein Best-Effort-Locator für eine kürzliche
Fallback-Familie und kann auf eine zeigen, die noch aktiv ist. Ein
gesunder Namespace konvergiert auf ungefähr 100 MiB: etwa 50 MiB für
stabile plus eine inaktive Fallback-Familie. Aktive oder noch nicht
veraltete Fallback-Familien werden beibehalten, sodass gleichzeitige
Daemons oder Crash/Neustart-Stürme vorübergehend mehr verwenden können.
Ein Runtime-Verzeichnis ist ein Eigentums- und
Aufbewahrungs-Namespace. Verwende unterschiedliche
QWEN_RUNTIME_DIR-Werte, wenn Daemons unabhängige Historie benötigen.
Neue Daemon-Log-Verzeichnisse sind privat für den Benutzer (0700) und
neue Dateien verwenden 0600 auf POSIX. Es gibt keine altersbasierte
Ablaufzeit.
Deaktivieren
Setze QWEN_DAEMON_LOG_FILE=0 (oder false/off/no), um das Datei-Logging vollständig zu überspringen. Die Stderr-Ausgabe bleibt unberührt.
Beziehung zu Session-Debug-Logs
Session-spezifische Debug-Logs (~/.qwen/debug/<sessionId>.txt und der ~/.qwen/debug/latest-Symlink) sind unabhängig. Das Daemon-Log liegt in einem Geschwister-Unterverzeichnis daemon/; die Debug-Semantik pro Session wird durch dieses Feature nicht geändert.
Externe Rotation
Richte keine externe Logrotate-Regel auf die aktive daemon.log aus. Der
Daemon ist der einzige unterstützte Schreiber und Rotator; externes
Umbenennen, Löschen oder Kürzen invalidiert sein Größenmodell. Kopieren
oder Versenden von Datensätzen ohne Mutation der Familie ist sicher.
Ältere serve-<pid>.log- und
serve-<pid>-<workspaceHash>.log-Dateien bleiben unberührt und werden
nicht von der neuen Aufbewahrungs-Policy gezählt.
Runtime-MCP-Server-Verwaltung (Issue #4514 )
Füge MCP-Server zur Laufzeit hinzu oder entferne sie, ohne den Daemon neu zu starten. Runtime-Einträge leben in einem ephemeren Overlay, das gleichnamige, über Settings definierte Server beschattet; die zugrundeliegende settings.json / mcpServers-Config wird niemals beschrieben.
Pre-Flight: Prüfe caps.features auf mcp_server_runtime_mutation, bevor du eine der beiden Routen aufrufst. Ältere Daemons ohne diesen Tag geben 404 zurück.
POST /workspace/mcp/servers — einen Runtime-MCP-Server hinzufügen
Strikt gegatet (Bearer-Token erforderlich). Verbindet den Server sofort über den aktiven McpClientManager und entdeckt dessen Tools.
Request:
{
"name": "my-server",
"config": {
"command": "npx",
"args": ["-y", "@my-org/mcp-server"]
}
}name muss alphanumerisch sein, plus _ und - (max. 256 Zeichen). config ist dasselbe MCP-Server-Konfigurationsobjekt, das in settings.json mcpServers-Einträgen verwendet wird (transportabhängige Felder: command/args für stdio, url für SSE/HTTP). Sicherheitssensible Felder (trust, env, cwd, oauth, headers, authProviderType, includeTools, excludeTools, type) werden vom Daemon entfernt und ignoriert.
Response (200) — Erfolg:
{
"name": "my-server",
"transport": "stdio",
"replaced": false,
"shadowedSettings": false,
"toolCount": 3,
"originatorClientId": "client-1"
}replaced: true— ein Runtime-Eintrag mit demselben Namen existierte bereits und der Config-Fingerprint weicht ab; alte Verbindung abgebaut, neue aufgebaut. Wenn der Fingerprint übereinstimmt (idempotentes erneutes Hinzufügen), istreplacedfalse.shadowedSettings: true— ein über Settings definierter Server mit demselben Namen existiert; der Runtime-Eintrag beschattet ihn jetzt. Der Settings-Eintrag bleibt unberührt und taucht wieder auf, wenn der Runtime-Eintrag später entfernt wird.toolCount— Anzahl der auf dem neu verbundenen Server entdeckten Tools.
Response (200) — Soft-Refuse (Budget-Warning-Modus):
{
"name": "my-server",
"skipped": true,
"reason": "budget_warning_only"
}Wird zurückgegeben, wenn --mcp-budget-mode=warn gesetzt ist und das Hinzufügen des Servers das konfigurierte --mcp-client-budget überschreiten würde. Der Server wird NICHT verbunden. Caller sollten den Budget-Druck dem Benutzer anzeigen.
Fehler:
| Status | Code | Wann |
|---|---|---|
400 | invalid_server_name | Name leer, überschreitet 256 Zeichen oder enthält Zeichen außerhalb von [A-Za-z0-9_-] |
400 | missing_required_field | config fehlt oder ist kein Nicht-Null-Objekt |
400 | invalid_client_id | X-Qwen-Client-Id-Header vorhanden, aber nicht für diesen Workspace registriert |
400 | invalid_config | Config-Form vom MCP-Transport-Validator abgelehnt |
401 | token_required | Kein Bearer-Token konfiguriert (striktes Gate) |
409 | mcp_budget_would_exceed | --mcp-budget-mode=enforce und Budget ist voll |
502 | mcp_server_spawn_failed | Server-Prozess beendet oder Timeout beim Verbinden; Body enthält serverName, exitCode, stderr |
503 | acp_channel_unavailable | Kein aktives ACP-Child (es wurde noch keine Session erstellt) |
DELETE /workspace/mcp/servers/:name — einen Runtime-MCP-Server entfernen
Strikt gegatet. Trennt die Verbindung zum Server und entfernt ihn aus dem Runtime-Overlay. Idempotent — das Entfernen eines Namens, der nie hinzugefügt wurde, gibt eine Skip-Response zurück (keinen Fehler).
Der :name-Pfadparameter ist der URL-kodierte Servername.
Response (200) — Erfolg:
{
"name": "my-server",
"removed": true,
"wasShadowingSettings": false,
"originatorClientId": "client-1"
}wasShadowingSettings: true— der entfernte Runtime-Eintrag hatte einen über Settings definierten Server mit demselben Namen beschattet. Dieser Settings-Eintrag ist jetzt nicht mehr beschattet und wird bei der nächsten Discovery / beim nächsten Neustart verwendet.
Response (200) — idempotenter Skip:
{
"name": "ghost",
"skipped": true,
"reason": "not_present"
}Wird zurückgegeben, wenn der Name nicht im Runtime-Overlay war (er kann noch in den Settings existieren — Settings-Einträge können nicht über diese Route entfernt werden).
Fehler:
| Status | Code | Wann |
|---|---|---|
400 | invalid_server_name | Name leer, überschreitet 256 Zeichen oder enthält Zeichen außerhalb von [A-Za-z0-9_-] |
400 | invalid_client_id | X-Qwen-Client-Id-Header vorhanden, aber nicht für diesen Workspace registriert |
401 | token_required | Kein Bearer-Token konfiguriert (striktes Gate) |
503 | acp_channel_unavailable | Kein aktives ACP-Child |
Shadow-Semantik
Runtime-Einträge bilden ein ephemeres Overlay über den über Settings definierten MCP-Servern:
- Hinzufügen eines Runtime-Servers mit demselben Namen wie ein Settings-Eintrag beschattet diesen — die Runtime-Config hat Vorrang. Der ursprüngliche Settings-Eintrag wird nicht verändert.
- Entfernen eines Runtime-Servers, der einen Settings-Eintrag beschattet hatte, hebt die Beschattung auf — die über Settings definierte Config wird bei der nächsten Verbindung wieder aktiv.
- Daemon-Neustart verwirft alle Runtime-Einträge. Nur über Settings definierte Server überstehen Neustarts. Runtime-Server sind auf die Lebensdauer der Session beschränkt.
GET /workspace/mcpmeldet die zusammengeführte Ansicht — sowohl über Settings definierte als auch Runtime-Server erscheinen imservers[]-Array. Im Snapshot gibt es heute keine Unterscheidung auf Wire-Ebene zwischen den beiden Ursprüngen.
Events
Beide Routen emittieren Workspace-weite SSE-Events (alle aktiven Session-Busse empfangen sie):
| Event | Emittiert wenn | Payload-Felder |
|---|---|---|
mcp_server_added | POST erfolgreich (nicht übersprungen) | name, transport, replaced, shadowedSettings, toolCount, originatorClientId |
mcp_server_removed | DELETE erfolgreich (nicht übersprungen) | name, wasShadowingSettings, originatorClientId |
Übersprungene Antworten (budget_warning_only, not_present) lösen KEINE Events aus. |
Budgetbezogene Events aus der bestehenden mcp_guardrail_events-Surface (mcp_budget_warning, mcp_child_refused_batch) werden auch dann ausgelöst, wenn zur Laufzeit hinzugefügte Elemente die Budget-Schwelle überschreiten.
Nächste Schritte
- Einen langlaufenden Daemon einrichten? Lokale Startvorlagen (systemd / launchd / nohup / tmux) für v0.16-alpha (nur lokal).
- Einen Client entwickeln? Siehe den DaemonClient TypeScript Quickstart und die HTTP-Protokollreferenz.
- Den Quellcode lesen? Der Bridge-Code befindet sich unter
packages/cli/src/serve/; der SDK-Client unterpackages/sdk-typescript/src/daemon/. - Die Roadmap verfolgen? Stage 1.5 / Stage 2-Fortschritt wird im Issue #3803 getrackt.