fix(gateway): ignore stale abort markers for fresh chat events (#91013)

Summary:
- The branch stamps Gateway chat run registrations and abort markers with ordering metadata, uses freshness checks for chat projection suppression, and updates abort/restart/maintenance tests and related types.
- PR surface: Source +79, Tests +103. Total +182 across 13 files.
- Reproducibility: yes. source-level: on current main, seed abortedRuns for a client run id, register a same-k ...  end; the presence-only checks suppress both projections. I did not execute tests in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: ci: re-trigger checks against current main
- PR branch already contained follow-up commit before automerge: Merge upstream/main into stale-abort marker fix
- PR branch already contained follow-up commit before automerge: Merge remote-tracking branch 'upstream/main' into nex/91013-conflict-…

Validation:
- ClawSweeper review passed for head 6f13d6f7c2.
- Required merge gates passed before the squash merge.

Prepared head SHA: 6f13d6f7c2
Review: https://github.com/openclaw/openclaw/pull/91013#issuecomment-4640475472

Co-authored-by: nxmxbbd <32288+nxmxbbd@users.noreply.github.com>
This commit is contained in:
David
2026-06-19 06:30:47 +00:00
committed by GitHub
parent be7d86ed80
commit cd98f195a7
13 changed files with 221 additions and 39 deletions
+3 -2
View File
@@ -12,6 +12,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { emitAgentEvent, getAgentEventLifecycleGeneration } from "../infra/agent-events.js";
import { jsonUtf8Bytes } from "../infra/json-utf8-bytes.js";
import { projectLiveAssistantBufferedText } from "./live-chat-projector.js";
import { createChatAbortMarker, type ChatAbortMarker } from "./server-chat-state.js";
const DEFAULT_CHAT_RUN_ABORT_GRACE_MS = 60_000;
@@ -337,7 +338,7 @@ export function boundInFlightRunSnapshotForChatHistory(params: {
export type ChatAbortOps = {
chatAbortControllers: Map<string, ChatAbortControllerEntry>;
chatRunBuffers: Map<string, string>;
chatAbortedRuns: Map<string, number>;
chatAbortedRuns: Map<string, ChatAbortMarker>;
clearChatRunState: (runId: string) => void;
removeChatRun: (
sessionId: string,
@@ -469,7 +470,7 @@ export function abortChatRunById(
const bufferedText = ops.chatRunBuffers.get(runId);
const partialText = bufferedText && bufferedText.trim() ? bufferedText : undefined;
ops.chatAbortedRuns.set(runId, Date.now());
ops.chatAbortedRuns.set(runId, createChatAbortMarker());
if (stopReason) {
active.abortStopReason = stopReason;
}
+3 -2
View File
@@ -11,6 +11,7 @@ import {
} from "../plugins/runtime/gateway-request-scope.js";
import { NodeRegistry } from "./node-registry.js";
import type { ChannelRuntimeSnapshot } from "./server-channel-runtime.types.js";
import { createChatRunEntry, type ChatRunEntry } from "./server-chat-state.js";
import type { GatewayRequestContext } from "./server-methods/types.js";
// Embedded/local agent calls need enough GatewayRequestContext to reuse server
@@ -52,7 +53,7 @@ export function createLocalGatewayRequestContext(
): GatewayRequestContext {
const logGateway = createSubsystemLogger("gateway/local");
const sessionEvents = new Set<string>();
const chatRuns = new Map<string, { sessionKey: string; agentId?: string; clientRunId: string }>();
const chatRuns = new Map<string, ChatRunEntry>();
const chatRunBuffers: GatewayRequestContext["chatRunBuffers"] = new Map();
const chatDeltaSentAt: GatewayRequestContext["chatDeltaSentAt"] = new Map();
const chatDeltaLastBroadcastLen: GatewayRequestContext["chatDeltaLastBroadcastLen"] = new Map();
@@ -105,7 +106,7 @@ export function createLocalGatewayRequestContext(
bufferedAgentEvents,
clearChatRunState,
addChatRun: (sessionId, entry) => {
chatRuns.set(sessionId, entry);
chatRuns.set(sessionId, createChatRunEntry(entry));
},
removeChatRun: (sessionId, clientRunId, sessionKey) => {
const entry = chatRuns.get(sessionId);
+66 -7
View File
@@ -10,13 +10,71 @@ export type ChatRunTiming = {
receivedAtMs: number;
};
export type ChatRunEntry = {
export type ChatRunRegistration = {
sessionKey: string;
agentId?: string;
clientRunId: string;
chatSendTiming?: ChatRunTiming;
};
export type ChatRunEntry = ChatRunRegistration & {
registeredAtMs: number;
registeredSequence: number;
};
export type ChatAbortMarker = number | { abortedAtMs: number; sequence: number };
let chatRunOrderingSequence = 0;
function nextChatRunOrderingSequence(): number {
chatRunOrderingSequence += 1;
return chatRunOrderingSequence;
}
/** Stamp a chat run registration with the process-local ordering metadata used for abort freshness checks. */
export function createChatRunEntry(entry: ChatRunRegistration): ChatRunEntry {
return {
...entry,
registeredAtMs: Date.now(),
registeredSequence: nextChatRunOrderingSequence(),
};
}
/** Create an abort marker ordered against chat run registrations, using a shared monotonic sequence. */
export function createChatAbortMarker(now = Date.now()): ChatAbortMarker {
return { abortedAtMs: now, sequence: nextChatRunOrderingSequence() };
}
/** Return the wall-clock timestamp used by maintenance TTL pruning for both legacy and structured markers. */
export function chatAbortMarkerTimestampMs(marker: ChatAbortMarker): number {
return typeof marker === "number" ? marker : marker.abortedAtMs;
}
/**
* Return whether an abort marker should suppress events for the given chat run registration.
* Structured markers compare the monotonic sequence first so same-millisecond aborts stay ordered;
* legacy numeric markers fall back to timestamp comparison, and a missing entry preserves old suppress-on-presence behavior.
*/
export function isChatAbortMarkerCurrent(
marker: ChatAbortMarker | undefined,
entry?: Pick<ChatRunEntry, "registeredAtMs" | "registeredSequence">,
): boolean {
if (marker === undefined) {
return false;
}
if (!entry) {
return true;
}
if (typeof marker !== "number" && typeof entry.registeredSequence === "number") {
return marker.sequence >= entry.registeredSequence;
}
if (typeof entry.registeredAtMs !== "number") {
return true;
}
const abortedAtMs = typeof marker === "number" ? marker : marker.abortedAtMs;
return abortedAtMs >= entry.registeredAtMs;
}
export type BufferedAgentEvent = {
sessionKey?: string;
agentId?: string;
@@ -24,7 +82,7 @@ export type BufferedAgentEvent = {
};
export type ChatRunRegistry = {
add: (sessionId: string, entry: ChatRunEntry) => void;
add: (sessionId: string, entry: ChatRunRegistration) => void;
peek: (sessionId: string) => ChatRunEntry | undefined;
shift: (sessionId: string) => ChatRunEntry | undefined;
remove: (sessionId: string, clientRunId: string, sessionKey?: string) => ChatRunEntry | undefined;
@@ -35,12 +93,13 @@ export type ChatRunRegistry = {
export function createChatRunRegistry(): ChatRunRegistry {
const chatRunSessions = new Map<string, ChatRunEntry[]>();
const add = (sessionId: string, entry: ChatRunEntry) => {
const add = (sessionId: string, entry: ChatRunRegistration) => {
const registeredEntry = createChatRunEntry(entry);
const queue = chatRunSessions.get(sessionId);
if (queue) {
queue.push(entry);
queue.push(registeredEntry);
} else {
chatRunSessions.set(sessionId, [entry]);
chatRunSessions.set(sessionId, [registeredEntry]);
}
};
@@ -96,7 +155,7 @@ export type ChatRunState = {
deltaLastBroadcastText: Map<string, string>;
agentDeltaSentAt: Map<string, number>;
bufferedAgentEvents: Map<string, BufferedAgentEvent>;
abortedRuns: Map<string, number>;
abortedRuns: Map<string, ChatAbortMarker>;
clearRun: (runId: string) => void;
clear: () => void;
};
@@ -112,7 +171,7 @@ export function createChatRunState(): ChatRunState {
const deltaLastBroadcastText = new Map<string, string>();
const agentDeltaSentAt = new Map<string, number>();
const bufferedAgentEvents = new Map<string, BufferedAgentEvent>();
const abortedRuns = new Map<string, number>();
const abortedRuns = new Map<string, ChatAbortMarker>();
const clearRun = (runId: string) => {
rawBuffers.delete(runId);
+79 -3
View File
@@ -44,6 +44,7 @@ import {
createAgentEventHandler,
createChatRunState,
createSessionEventSubscriberRegistry,
createChatAbortMarker,
createSessionMessageSubscriberRegistry,
createToolEventRecipientRegistry,
type AgentEventHandlerOptions,
@@ -2762,7 +2763,7 @@ describe("agent event handler", () => {
sessionKey: "session-aborted",
clientRunId: "client-aborted",
});
chatRunState.abortedRuns.set("client-aborted", 1_000);
chatRunState.abortedRuns.set("client-aborted", createChatAbortMarker());
handler({
runId: "run-aborted",
@@ -2777,6 +2778,81 @@ describe("agent event handler", () => {
expect(chatBroadcastCalls(broadcast)).toHaveLength(0);
});
it.each([
{
name: "older timestamp",
marker: () => 1_000,
},
{
name: "same-millisecond older sequence",
marker: () => ({ abortedAtMs: 2_000, sequence: -1 }),
},
])(
"ignores stale aborted markers from older same-key runs for fresh chat lifecycle events ($name)",
({ marker }) => {
const { broadcast, nodeSendToSession, chatRunState, handler } = createHarness({ now: 2_000 });
chatRunState.abortedRuns.set("client-stale-abort", marker());
chatRunState.registry.add("run-stale-abort", {
sessionKey: "session-stale-abort",
clientRunId: "client-stale-abort",
});
handler({
runId: "run-stale-abort",
seq: 1,
stream: "assistant",
ts: 2_100,
data: { text: "Fresh output", delta: "Fresh output" },
});
handler({
runId: "run-stale-abort",
seq: 2,
stream: "lifecycle",
ts: 2_200,
data: { phase: "end" },
});
const chatCalls = chatBroadcastCalls(broadcast);
expect(chatCalls).toHaveLength(2);
const deltaPayload = chatCalls[0][1];
const finalPayload = chatCalls[1][1];
expect(deltaPayload.state).toBe("delta");
expect(finalPayload.state).toBe("final");
expect(sessionChatCalls(nodeSendToSession)).toHaveLength(2);
expect(chatRunState.abortedRuns.has("client-stale-abort")).toBe(true);
expect(chatRunState.registry.peek("run-stale-abort")).toBeUndefined();
},
);
it("honors same-millisecond abort markers from the current same-key run", () => {
const { broadcast, nodeSendToSession, chatRunState, handler } = createHarness({ now: 3_000 });
chatRunState.registry.add("run-current-abort", {
sessionKey: "session-current-abort",
clientRunId: "client-current-abort",
});
chatRunState.abortedRuns.set("client-current-abort", createChatAbortMarker());
handler({
runId: "run-current-abort",
seq: 1,
stream: "assistant",
ts: 3_100,
data: { text: "Suppressed output", delta: "Suppressed output" },
});
handler({
runId: "run-current-abort",
seq: 2,
stream: "lifecycle",
ts: 3_200,
data: { phase: "end", aborted: true, stopReason: "rpc" },
});
expect(chatBroadcastCalls(broadcast)).toHaveLength(0);
expect(sessionChatCalls(nodeSendToSession)).toHaveLength(0);
expect(chatRunState.abortedRuns.has("client-current-abort")).toBe(true);
expect(chatRunState.registry.peek("run-current-abort")).toBeUndefined();
});
it("keeps live session setting metadata at the top level for lifecycle updates", async () => {
vi.mocked(loadGatewaySessionRow).mockReturnValue({
key: "session-finished",
@@ -3166,7 +3242,7 @@ describe("agent event handler", () => {
data: { phase: "error", error: "provider failed" },
});
expect(chatRunState.registry.peek("run-fallback-retry")).toEqual({
expect(chatRunState.registry.peek("run-fallback-retry")).toMatchObject({
sessionKey: "session-fallback",
clientRunId: "run-fallback-client",
});
@@ -3189,7 +3265,7 @@ describe("agent event handler", () => {
vi.advanceTimersByTime(100);
expect(chatRunState.registry.peek("run-fallback-retry")).toEqual({
expect(chatRunState.registry.peek("run-fallback-retry")).toMatchObject({
sessionKey: "session-fallback",
clientRunId: "run-fallback-client",
});
+10 -2
View File
@@ -18,6 +18,7 @@ import {
resolveMergedAssistantText,
shouldSuppressAssistantEventForLiveChat,
} from "./live-chat-projector.js";
import { isChatAbortMarkerCurrent } from "./server-chat-state.js";
import type {
BufferedAgentEvent,
ChatRunEntry,
@@ -37,6 +38,7 @@ import { loadSessionEntry } from "./session-utils.js";
import { formatForLog } from "./ws-log.js";
export {
createChatAbortMarker,
createChatRunRegistry,
createChatRunState,
createSessionEventSubscriberRegistry,
@@ -44,8 +46,10 @@ export {
createToolEventRecipientRegistry,
} from "./server-chat-state.js";
export type {
ChatAbortMarker,
ChatRunEntry,
ChatRunRegistry,
ChatRunRegistration,
ChatRunState,
SessionEventSubscriberRegistry,
SessionMessageSubscriberRegistry,
@@ -562,7 +566,8 @@ export function createAgentEventHandler({
const clientRunId = chatLink?.clientRunId ?? evt.runId;
const eventRunId = chatLink?.clientRunId ?? evt.runId;
const isAborted =
chatRunState.abortedRuns.has(clientRunId) || chatRunState.abortedRuns.has(evt.runId);
isChatAbortMarkerCurrent(chatRunState.abortedRuns.get(clientRunId), chatLink) ||
isChatAbortMarkerCurrent(chatRunState.abortedRuns.get(evt.runId), chatLink);
const deliverySessionKey = sessionKey
? resolveSessionDeliveryKey(sessionKey, sessionAgentId)
: undefined;
@@ -1148,7 +1153,9 @@ export function createAgentEventHandler({
const eventRunId = chatLink?.clientRunId ?? evt.runId;
const eventForClients = chatLink ? { ...evt, runId: eventRunId } : evt;
const isAborted =
chatRunState.abortedRuns.has(clientRunId) || chatRunState.abortedRuns.has(evt.runId);
isChatAbortMarkerCurrent(chatRunState.abortedRuns.get(clientRunId), chatLink) ||
isChatAbortMarkerCurrent(chatRunState.abortedRuns.get(evt.runId), chatLink);
const restartRecoveryState = restartRecoverySessionKey
? resolveRestartRecoveryLifecycleState(restartRecoverySessionKey, restartRecoveryAgentId, evt)
: undefined;
@@ -1173,6 +1180,7 @@ export function createAgentEventHandler({
if (lifecyclePhase !== null && lifecyclePhase !== "error") {
clearPendingTerminalLifecycleError(evt.runId);
}
// Include sessionKey so Control UI can filter tool streams per session.
const spawnedBy = sessionKey ? resolveSpawnedBy(sessionKey) : null;
const agentPayload = sessionKey
+30 -3
View File
@@ -70,7 +70,7 @@ vi.mock("../logging/subsystem.js", () => ({
}));
const { createGatewayCloseHandler } = await import("./server-close.js");
const { createChatRunState } = await import("./server-chat-state.js");
const { createChatRunState, isChatAbortMarkerCurrent } = await import("./server-chat-state.js");
const {
finishGatewayRestartTrace,
recordGatewayRestartTraceSpan,
@@ -548,7 +548,12 @@ describe("createGatewayCloseHandler", () => {
nodeSendToSession,
chatRunState,
chatAbortControllers,
removeChatRun: vi.fn(() => ({ sessionKey: "session-1", clientRunId: "run-1" })),
removeChatRun: vi.fn(() => ({
sessionKey: "session-1",
clientRunId: "run-1",
registeredAtMs: 1_000,
registeredSequence: 1,
})),
}),
);
@@ -746,16 +751,24 @@ describe("createGatewayCloseHandler", () => {
},
],
]);
const chatRunState = createTestChatRunState();
const abortedRunsSet = vi.spyOn(chatRunState.abortedRuns, "set");
const markMainSessionsAbortedForRestart = vi.fn<MarkMainSessionsAbortedForRestart>(async () => {
events.push("marker");
});
const removeChatRun = vi.fn(() => {
events.push("abort");
return { sessionKey: "agent:main:main", clientRunId: "run-1" };
return {
sessionKey: "agent:main:main",
clientRunId: "run-1",
registeredAtMs: 1_000,
registeredSequence: 1,
};
});
const close = createGatewayCloseHandler(
createGatewayCloseTestDeps({
chatAbortControllers,
chatRunState,
markMainSessionsAbortedForRestart,
removeChatRun,
resolveActiveSessionIdForKey: (sessionKey) => {
@@ -807,6 +820,20 @@ describe("createGatewayCloseHandler", () => {
expect(agentController.signal.aborted).toBe(true);
expect(completedController.signal.aborted).toBe(true);
expect(hiddenController.signal.aborted).toBe(true);
const completedMarker = abortedRunsSet.mock.calls.find(
([runId]) => runId === "completed-run",
)?.[1];
expect(completedMarker).toEqual({
abortedAtMs: expect.any(Number),
sequence: expect.any(Number),
});
chatRunState.registry.add("completed-run", {
sessionKey: "agent:main:fresh",
clientRunId: "completed-run",
});
expect(
isChatAbortMarkerCurrent(completedMarker, chatRunState.registry.peek("completed-run")),
).toBe(false);
});
it("keeps post-terminal caller work in restart drain and recovery", async () => {
+6 -2
View File
@@ -22,7 +22,11 @@ import {
measureGatewayRestartTrace,
recordGatewayRestartTrace,
} from "./restart-trace.js";
import type { ChatRunEntry, ChatRunState } from "./server-chat-state.js";
import {
createChatAbortMarker,
type ChatRunEntry,
type ChatRunState,
} from "./server-chat-state.js";
import type { GatewayPostReadySidecarHandle } from "./server-startup-post-attach.js";
const shutdownLog = createSubsystemLogger("gateway/shutdown");
@@ -406,7 +410,7 @@ function abortActiveRunsForRestart(params: RestartRunAbortParams): number {
entry.abortStopReason = "restart";
entry.controller.abort(createAgentRunRestartAbortError());
params.chatAbortControllers.delete(runId);
params.chatRunState.abortedRuns.set(runId, Date.now());
params.chatRunState.abortedRuns.set(runId, createChatAbortMarker());
params.chatRunState.clearRun(runId);
const removed = params.removeChatRun(runId, runId, entry.sessionKey);
params.agentRunSeq.delete(runId);
+3 -2
View File
@@ -10,6 +10,7 @@ import {
type RestartRecoveryCandidate,
} from "./chat-abort.js";
import { pruneStaleControlPlaneBuckets } from "./control-plane-rate-limit.js";
import { chatAbortMarkerTimestampMs } from "./server-chat-state.js";
import type { ChatRunState } from "./server-chat-state.js";
import type { ChatRunEntry } from "./server-chat.js";
import {
@@ -227,8 +228,8 @@ export function startGatewayMaintenanceTimers(params: {
}
const ABORTED_RUN_TTL_MS = 60 * 60_000;
for (const [runId, abortedAt] of params.chatRunState.abortedRuns) {
if (now - abortedAt <= ABORTED_RUN_TTL_MS) {
for (const [runId, abortMarker] of params.chatRunState.abortedRuns) {
if (now - chatAbortMarkerTimestampMs(abortMarker) <= ABORTED_RUN_TTL_MS) {
continue;
}
params.chatRunState.abortedRuns.delete(runId);
@@ -3,6 +3,7 @@
*/
import { vi } from "vitest";
import type { Mock } from "vitest";
import type { ChatAbortMarker } from "../server-chat-state.js";
import type { GatewayRequestHandler, RespondFn } from "./types.js";
export function createActiveRun(
@@ -37,7 +38,7 @@ type ChatAbortTestContext = Record<string, unknown> & {
dedupe: Map<string, unknown>;
agentDeltaSentAt: Map<string, number>;
bufferedAgentEvents: Map<string, unknown>;
chatAbortedRuns: Map<string, number>;
chatAbortedRuns: Map<string, ChatAbortMarker>;
clearChatRunState: (runId: string) => void;
removeChatRun: (
...args: unknown[]
@@ -62,7 +63,7 @@ export function createChatAbortContext(
dedupe: new Map(),
agentDeltaSentAt: new Map(),
bufferedAgentEvents: new Map(),
chatAbortedRuns: new Map<string, number>(),
chatAbortedRuns: new Map<string, ChatAbortMarker>(),
removeChatRun: vi
.fn()
.mockImplementation((run: string) => ({ sessionKey: "main", clientRunId: run })),
+4 -3
View File
@@ -150,7 +150,7 @@ import {
createManagedOutgoingImageBlocks,
} from "../managed-image-attachments.js";
import { ADMIN_SCOPE } from "../method-scopes.js";
import type { ChatRunTiming } from "../server-chat-state.js";
import { chatAbortMarkerTimestampMs, type ChatRunTiming } from "../server-chat-state.js";
import { getMaxChatHistoryMessagesBytes, MAX_PAYLOAD_BYTES } from "../server-constants.js";
import { resolveSessionHistoryTailReadOptions } from "../session-history-state.js";
import { readSessionTranscriptIndex } from "../session-transcript-index.fs.js";
@@ -3326,8 +3326,9 @@ export const chatHandlers: GatewayRequestHandlers = {
return;
}
const abortedAt = context.chatAbortedRuns.get(clientRunId);
if (abortedAt !== undefined) {
const abortMarker = context.chatAbortedRuns.get(clientRunId);
if (abortMarker !== undefined) {
const abortedAt = chatAbortMarkerTimestampMs(abortMarker);
const payload = buildAbortedChatSendPayload({
runId: clientRunId,
endedAt: abortedAt,
+9 -7
View File
@@ -20,7 +20,12 @@ import type { NodeRegistry } from "../node-registry.js";
import type { PluginNodeCapabilitySurface } from "../plugin-node-capability.js";
import type { GatewayBroadcastFn, GatewayBroadcastToConnIdsFn } from "../server-broadcast-types.js";
import type { ChannelRuntimeSnapshot } from "../server-channel-runtime.types.js";
import type { BufferedAgentEvent } from "../server-chat-state.js";
import type {
BufferedAgentEvent,
ChatAbortMarker,
ChatRunEntry,
ChatRunRegistration,
} from "../server-chat-state.js";
import type { DedupeEntry } from "../server-shared.js";
import type { GatewayEventLoopHealth } from "../server/event-loop-health.js";
@@ -96,7 +101,7 @@ export type GatewayRequestContext = {
nodeRegistry: NodeRegistry;
agentRunSeq: Map<string, number>;
chatAbortControllers: Map<string, ChatAbortControllerEntry>;
chatAbortedRuns: Map<string, number>;
chatAbortedRuns: Map<string, ChatAbortMarker>;
chatRunBuffers: Map<string, string>;
chatDeltaSentAt: Map<string, number>;
chatDeltaLastBroadcastLen: Map<string, number>;
@@ -104,15 +109,12 @@ export type GatewayRequestContext = {
agentDeltaSentAt: Map<string, number>;
bufferedAgentEvents: Map<string, BufferedAgentEvent>;
clearChatRunState: (runId: string) => void;
addChatRun: (
sessionId: string,
entry: { sessionKey: string; agentId?: string; clientRunId: string },
) => void;
addChatRun: (sessionId: string, entry: ChatRunRegistration) => void;
removeChatRun: (
sessionId: string,
clientRunId: string,
sessionKey?: string,
) => { sessionKey: string; agentId?: string; clientRunId: string } | undefined;
) => ChatRunEntry | undefined;
subscribeSessionEvents: (connId: string) => void;
unsubscribeSessionEvents: (connId: string) => void;
subscribeSessionMessageEvents: (connId: string, sessionKey: string) => void;
+3 -3
View File
@@ -4,7 +4,7 @@ import type { ModelCatalogEntry } from "../agents/model-catalog.js";
import type { CliDeps } from "../cli/deps.types.js";
import type { HealthSummary } from "../commands/health.js";
import type { ChatAbortControllerEntry } from "./chat-abort.js";
import type { ChatRunEntry } from "./server-chat.js";
import type { ChatAbortMarker, ChatRunEntry, ChatRunRegistration } from "./server-chat.js";
import type { DedupeEntry } from "./server-shared.js";
/** Runtime context available to node event handlers. */
@@ -15,14 +15,14 @@ export type NodeEventContext = {
nodeSubscribe: (nodeId: string, sessionKey: string) => void;
nodeUnsubscribe: (nodeId: string, sessionKey: string) => void;
broadcastVoiceWakeChanged: (triggers: string[]) => void;
addChatRun: (sessionId: string, entry: ChatRunEntry) => void;
addChatRun: (sessionId: string, entry: ChatRunRegistration) => void;
removeChatRun: (
sessionId: string,
clientRunId: string,
sessionKey?: string,
) => ChatRunEntry | undefined;
chatAbortControllers: Map<string, ChatAbortControllerEntry>;
chatAbortedRuns: Map<string, number>;
chatAbortedRuns: Map<string, ChatAbortMarker>;
chatRunBuffers: Map<string, string>;
chatDeltaSentAt: Map<string, number>;
dedupe: Map<string, DedupeEntry>;
+2 -1
View File
@@ -25,6 +25,7 @@ import type { GatewayBroadcastFn, GatewayBroadcastToConnIdsFn } from "./server-b
import { createGatewayBroadcaster } from "./server-broadcast.js";
import {
type ChatRunEntry,
type ChatRunRegistration,
createChatRunState,
createToolEventRecipientRegistry,
} from "./server-chat-state.js";
@@ -116,7 +117,7 @@ export async function createGatewayRuntimeState(params: {
chatRunBuffers: Map<string, string>;
chatDeltaSentAt: Map<string, number>;
chatDeltaLastBroadcastLen: Map<string, number>;
addChatRun: (sessionId: string, entry: ChatRunEntry) => void;
addChatRun: (sessionId: string, entry: ChatRunRegistration) => void;
removeChatRun: (
sessionId: string,
clientRunId: string,