mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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 head6f13d6f7c2. - Required merge gates passed before the squash merge. Prepared head SHA:6f13d6f7c2Review: https://github.com/openclaw/openclaw/pull/91013#issuecomment-4640475472 Co-authored-by: nxmxbbd <32288+nxmxbbd@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user