/** * DkgChannelPlugin — Spike A: DKG UI ↔ OpenClaw channel bridge. * * Makes the DKG Node UI a first-class OpenClaw channel. Messages sent * through the Agent Hub chat go through this channel into the OpenClaw * gateway's session system, meaning they share the same transcript and * context as messages from Telegram, WhatsApp, or any other channel * (when `dmScope: "main"`). * * Transport: The DKG daemon exposes `/api/openclaw-channel/send` for * the frontend. The daemon forwards the message to this plugin via * a standalone HTTP server on a dedicated port (bridge mode). * * Message routing uses OpenClaw's plugin-sdk `dispatchInboundReplyWithBase` * helper — the same pathway used by built-in channels (Telegram, Discord, * etc.). This ensures the message enters the agent's session system with * full context continuity. */ import type { ChannelOutboundReply, DkgOpenClawConfig, OpenClawPluginApi } from './types.js'; import type { DkgDaemonClient, OpenClawAttachmentRef } from './dkg-client.js'; import type { ChatTurnWriter } from './ChatTurnWriter.js'; export declare const CHANNEL_NAME = "dkg-ui"; interface ChatContextEntry { key: string; label: string; value: string; } interface InboundChatOptions { attachmentRefs?: OpenClawAttachmentRef[]; contextEntries?: ChatContextEntry[]; /** * UI-selected project context graph ID for this turn. The node UI stamps * this onto the outbound `/api/openclaw-channel/send` payload; the * adapter uses it to scope slot-backed memory recall and per-project * memory imports to the user's current project. Optional — turns that * arrive without it run in the documented degraded mode * (single-graph agent-context only, or `needs_clarification` on write). */ uiContextGraphId?: string; persistUserMessage?: string; } /** * Format a one-line diagnostic describing the parsed envelope for an * inbound chat turn. Used by `handleInboundHttp` and * `handleInboundStreamHttp` to give operators runtime ground truth on * whether the UI-selected project (`uiContextGraphId`) and the * `contextEntries` the renderer sees are actually arriving at the * adapter bridge. The log line is info-level because it is the only * observable signal for the envelope-stamping chain between the UI * dropdown and the agent body renderer; without it, operators have to * guess whether a "can't see UI state" symptom is a UI React-state bug, * a daemon-proxy dropout, or an agent-interpretation issue. * * Log-injection hardening: `normalizeChatContextEntry` only trims * whitespace at parse time — it does NOT strip control characters. * Full control-char sanitization (`sanitizeChatContextEntries`) happens * later in `processInbound`/`processInboundStream`, AFTER this * diagnostic log has already fired. So this formatter runs its own * sanitization pass (`sanitizeDiagnosticField`) on every field it * echoes — correlation id, `uiContextGraphId`, entry keys, entry * values — to defeat a crafted envelope like * `value: "foo\n[dkg-channel] FAKE LOG LINE: bar"` from injecting a * forged log line. Bridge auth limits the reach of this attack to * authorized callers anyway, but log integrity should not be * load-bearing on authorization. */ export declare function formatInboundTurnDiagnostic(correlationId: string, uiContextGraphId: string | undefined, contextEntries: ChatContextEntry[] | undefined): string; export declare class DkgChannelPlugin { private readonly config; private client; private api; /** OpenClaw runtime — provides channel routing, session, and reply subsystems. */ private runtime; /** Full OpenClawConfig — needed for agent dispatch. */ private cfg; /** Plugin-sdk helpers — lazily loaded at first dispatch. */ private sdk; /** True after the first loadSdk() attempt — prevents re-trying and re-logging every turn. */ private sdkLoaded; private server; private serverStart; private readonly pendingRequests; private readonly pendingTurnPersistence; private readonly pendingMarkerPersistence; /** * Per-dispatch AsyncLocalStorage holding the UI-selected project * context graph for the currently-running turn. Populated by * `runWithDispatchContext` at the start of each dispatch and read by * `getSessionProjectContextGraphId` from inside the dispatch's async * call tree. Automatically scoped to the dispatch — no explicit * clear needed, concurrent turns on the same `sessionKey` cannot * collide. */ private readonly dispatchContext; private readonly port; private useGatewayRoute; private channelRegistered; private gatewayRoutesRegistered; private inFlight; private readonly maxInFlight; private stopping; private readonly stopWaiters; private stopDrainDeadlineAt; private serverStop; private serverStopShouldUpdateGatewayStatus; private gatewayLifecycleStop; private gatewayLifecycleOwner; private gatewayLifecyclePendingOwner; private gatewayLifecycleStatusContext; private gatewayLifecycleStatusOwner; private readonly gatewayLifecycleOwnersByAccount; private readonly gatewayLifecyclePendingOwnersByAccount; private readonly gatewayLifecycleOwnersByContext; private readonly gatewayLifecycleOwnersBySignal; private chatTurnWriter; /** * Pre-dispatch memory-slot re-assert callback. Set by `DkgNodePlugin` * to `memoryPlugin.reAssertCapability.bind(memoryPlugin)`. Called * once per `processInbound` / `processInboundStream` so the slot * stays owned by this adapter even when another plugin's startup * code overwrote `memoryPluginState.capability` after our * registration ran. Mode-independent — fires for every UI dispatch * regardless of `full` vs `setup-runtime`. */ private preDispatchReAssert; constructor(config: NonNullable, client: DkgDaemonClient); setClient(client: DkgDaemonClient): void; /** Wire the memory-slot re-assert callback. Called by `DkgNodePlugin`. */ setPreDispatchReAssert(cb: (() => void) | null): void; setChatTurnWriter(writer: ChatTurnWriter | null): void; /** * Read the UI-selected project context graph for the currently-running * dispatch. Used by `DkgMemorySessionResolver` inside `DkgNodePlugin` * to scope slot-backed memory recall to the user's current project. * * Implementation: reads from AsyncLocalStorage, so the value is only * visible to code running inside the dispatch's async call tree. The * `sessionKey` argument is used as a sanity check — if the dispatch * stamped a different sessionKey than the caller is asking about, we * return `undefined` rather than a mismatched CG. Tool calls made * during the dispatch all share the same sessionKey, so the check * costs nothing in practice. * * Returns `undefined` when: * - the caller is not inside an active dispatch (no ALS store), * - the dispatch carried no `uiContextGraphId` (non-UI turn, or user * deselected the project), * - the caller's `sessionKey` does not match the dispatch's * `sessionKey` (defensive: indicates a misuse where the resolver * is being called from outside the owning dispatch's call tree). */ getSessionProjectContextGraphId(sessionKey: string | undefined): string | undefined; /** * Run `fn` inside an AsyncLocalStorage-scoped dispatch context so that * any `getSessionProjectContextGraphId` call issued from inside `fn` * (directly or via async descendants) observes the per-turn UI context * graph. Scope is automatically cleared when `fn` resolves, rejects, * or throws — no manual cleanup required. * * Concurrent dispatches on the same `sessionKey` each get their own * isolated store; one cannot clobber another. */ private runWithDispatchContext; register(api: OpenClawPluginApi): void; start(): Promise; stop(options?: { updateGatewayStatus?: boolean; }): Promise; private reportGatewayLifecycleStopped; private getGatewayAccountId; private getGatewayLifecycleStatus; private setGatewayLifecycleStatus; private reportUnsupportedGatewayAccount; private ensureSupportedGatewayAccount; private ignoreUnsupportedGatewayStop; private cancelPendingGatewayLifecycle; private stopAbortedGatewayLifecycle; private stopCurrentGatewayLifecycle; private runGatewayLifecycle; private deletePendingTurnPersistence; private reservePendingTurnPersistence; private clearPendingTurnPersistence; private deletePendingMarkerPersistence; private clearPendingMarkerPersistence; private flushPendingPersistenceBeforeDrop; private waitForPendingTurnPersistenceBeforeDrop; private flushPendingMarkerPersistenceBeforeDrop; private waitForExternalMarkerWrite; private notifyStopIdle; private waitForStopDrain; private canContinuePersistenceAttempt; private loadSdk; private buildRegisteredChannelPlugin; private waitForGatewayLifecycleStop; private resolveRegisteredAccount; private buildStreamingReplyOptions; /** * Process an inbound message from the DKG UI. * Routes through the OpenClaw session system and returns the agent reply. */ processInbound(text: string, correlationId: string, identity: string, opts?: InboundChatOptions): Promise; /** * Dispatch an inbound message using OpenClaw's plugin-sdk dispatch system. * This is the same pathway used by Telegram, Discord, and other built-in channels. */ private dispatchViaPluginSdk; private dispatchWithSdk; private dispatchWithRuntime; /** * Stream variant of processInbound. Yields events as the agent produces them. * The caller is responsible for writing SSE frames and calling persistTurn after. */ processInboundStream(text: string, correlationId: string, identity: string, opts?: InboundChatOptions): AsyncGenerator<{ type: 'text_delta'; delta: string; } | { type: 'final'; text: string; correlationId: string; }>; private handleOutboundReply; private dispatchRuntimeReply; private buildSdkCore; private recordRuntimeInboundSession; private handleUnexpectedHttpError; private handleUnexpectedGatewayError; /** * Persist a chat turn into the `'chat-turns'` Working Memory assertion of * the `'agent-context'` context graph via the daemon's * `/api/openclaw-channel/persist-turn` route. Fire-and-forget — errors * are logged but don't affect the reply. */ private persistTurn; private markExternalTurnPersistedAfterStore; private writeExternalTurnMarker; private scheduleExternalTurnMarkerRetry; private queueTurnPersistence; private buildFailedAssistantReply; private handleHttpRequest; private handleInboundHttp; /** SSE streaming handler — yields events as the agent produces them. */ private handleInboundStreamHttp; /** Handler for api.registerHttpRoute() — same logic, different req/res shape. */ private handleGatewayRoute; private authorizeBridgeRequest; get bridgePort(): number; get isListening(): boolean; get isUsingGatewayRoute(): boolean; } export {}; //# sourceMappingURL=DkgChannelPlugin.d.ts.map