perf(gateway): skip duplicate turn session touch

This commit is contained in:
Peter Steinberger
2026-05-27 05:27:57 +01:00
parent 1c8a11265b
commit 8fa4fad3a7
4 changed files with 110 additions and 32 deletions

View File

@@ -741,6 +741,17 @@ async function runBasicAgentCommand() {
});
}
function setupSessionTouchStore(): void {
const sessionEntry: SessionEntry = {
sessionId: "session-1",
updatedAt: 1,
skillsSnapshot: { prompt: "", skills: [], version: 0 },
};
state.sessionEntryMock = sessionEntry;
state.sessionStoreMock = { "agent:main:main": sessionEntry };
state.storePathMock = "/tmp/openclaw-sessions.json";
}
function expectFallbackOverrideCalls(first: boolean, second: boolean) {
expect(state.resolveEffectiveModelFallbacksMock).toHaveBeenCalledTimes(2);
expectRecordFields(mockCallArg(state.resolveEffectiveModelFallbacksMock, 0), {
@@ -881,6 +892,56 @@ describe("agentCommand LiveSessionModelSwitchError retry", () => {
);
});
it("keeps the initial session touch for local runs", async () => {
setupSingleAttemptFallback();
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
setupSessionTouchStore();
await runBasicAgentCommand();
const touchWrites = state.persistSessionEntryMock.mock.calls.filter((call) => {
const entry = (call[0] as { entry?: Record<string, unknown> } | undefined)?.entry;
return entry?.lastInteractionAt !== undefined;
});
expect(touchWrites).toHaveLength(1);
expect(state.updateSessionStoreAfterAgentRunMock).toHaveBeenCalledTimes(1);
});
it("skips the initial session touch after gateway ingress already persisted activity", async () => {
setupSingleAttemptFallback();
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
setupSessionTouchStore();
await agentCommand({
message: "hello",
to: "+1234567890",
skipInitialSessionTouch: true,
});
expect(state.persistSessionEntryMock).not.toHaveBeenCalled();
expect(state.updateSessionStoreAfterAgentRunMock).toHaveBeenCalledTimes(1);
});
it("persists explicit overrides even when ingress skips the initial touch", async () => {
setupSingleAttemptFallback();
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
setupSessionTouchStore();
await agentCommand({
message: "hello",
to: "+1234567890",
thinking: "medium",
skipInitialSessionTouch: true,
});
const touchWrite = mockCallArg(state.persistSessionEntryMock) as {
entry?: Record<string, unknown>;
};
expect(touchWrite.entry?.thinkingLevel).toBe("medium");
expect(touchWrite.entry?.lastInteractionAt).toBeDefined();
expect(state.updateSessionStoreAfterAgentRunMock).toHaveBeenCalledTimes(1);
});
it("clears stale flag-only pending final delivery when there is no final payload", async () => {
setupSingleAttemptFallback();
state.runAgentAttemptMock.mockResolvedValue(makeEmptyResult("openai", "gpt-5.4"));

View File

@@ -865,7 +865,15 @@ async function agentCommandInternal(
}
// Persist explicit /command overrides to the session store when we have a key.
if (sessionStore && sessionKey && !suppressVisibleSessionEffects) {
const hasInitialSessionOverrides = Boolean(thinkOverride || verboseOverride);
const shouldPersistInitialSessionTouch =
opts.skipInitialSessionTouch !== true || hasInitialSessionOverrides;
if (
sessionStore &&
sessionKey &&
!suppressVisibleSessionEffects &&
shouldPersistInitialSessionTouch
) {
const now = Date.now();
const entry = sessionStore[sessionKey] ??
sessionEntry ?? { sessionId, updatedAt: now, sessionStartedAt: now };

View File

@@ -114,6 +114,8 @@ export type AgentCommandOpts = {
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
/** Internal runs can omit the channel message tool entirely. */
disableMessageTool?: boolean;
/** Gateway ingress that already persisted visible activity can skip the duplicate pre-run touch. */
skipInitialSessionTouch?: boolean;
/** Per-call stream param overrides (best-effort). */
streamParams?: AgentStreamParams;
/** Explicit workspace directory override (for subagents to inherit parent workspace). */

View File

@@ -1230,6 +1230,7 @@ export const agentHandlers: GatewayRequestHandlers = {
let isNewSession = false;
let skipTimestampInjection = false;
let shouldPrependStartupContext = false;
let skipAgentInitialSessionTouch = false;
const resetCommandMatch = message.match(RESET_COMMAND_RE);
if (resetCommandMatch && requestedSessionKey) {
@@ -1548,42 +1549,47 @@ export const agentHandlers: GatewayRequestHandlers = {
if (storePath && !suppressVisibleSessionEffects) {
const requestedStoreKey = requestedSessionKey;
let deniedBySendPolicy = false;
const persisted = await updateSessionStore(storePath, (store) => {
const { primaryKey } = migrateAndPruneGatewaySessionStoreKey({
cfg,
key: requestedStoreKey,
store,
});
const freshEntry = store[primaryKey];
patchBuild = buildSessionPatch(freshEntry);
const effectivePatch =
recoveredSessionStartedAt !== undefined &&
freshEntry?.sessionStartedAt === undefined &&
freshEntry?.sessionId === entry?.sessionId
? { ...patchBuild.patch, sessionStartedAt: recoveredSessionStartedAt }
: patchBuild.patch;
const merged = mergeSessionEntry(freshEntry, effectivePatch);
const sendPolicy =
request.deliver === true
? resolveSendPolicy({
cfg,
entry: merged,
sessionKey: canonicalKey,
channel: merged?.channel,
chatType: merged?.chatType,
})
: "allow";
if (sendPolicy === "deny") {
deniedBySendPolicy = true;
const persisted = await updateSessionStore(
storePath,
(store) => {
const { primaryKey } = migrateAndPruneGatewaySessionStoreKey({
cfg,
key: requestedStoreKey,
store,
});
const freshEntry = store[primaryKey];
patchBuild = buildSessionPatch(freshEntry);
const effectivePatch =
recoveredSessionStartedAt !== undefined &&
freshEntry?.sessionStartedAt === undefined &&
freshEntry?.sessionId === entry?.sessionId
? { ...patchBuild.patch, sessionStartedAt: recoveredSessionStartedAt }
: patchBuild.patch;
const merged = mergeSessionEntry(freshEntry, effectivePatch);
const sendPolicy =
request.deliver === true
? resolveSendPolicy({
cfg,
entry: merged,
sessionKey: canonicalKey,
channel: merged?.channel,
chatType: merged?.chatType,
})
: "allow";
if (sendPolicy === "deny") {
deniedBySendPolicy = true;
return merged;
}
store[primaryKey] = merged;
return merged;
}
store[primaryKey] = merged;
return merged;
});
},
{ takeCacheOwnership: true },
);
if (persisted) {
sessionEntry = persisted;
resolvedSessionId = sessionEntry.sessionId;
}
skipAgentInitialSessionTouch = touchInteraction;
if (deniedBySendPolicy) {
respond(
false,
@@ -2056,6 +2062,7 @@ export const agentHandlers: GatewayRequestHandlers = {
internalEvents: request.internalEvents,
inputProvenance,
sessionEffects,
skipInitialSessionTouch: skipAgentInitialSessionTouch,
preserveUserFacingSessionModelState,
sourceReplyDeliveryMode: request.sourceReplyDeliveryMode,
disableMessageTool: request.disableMessageTool,