mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 09:32:12 +00:00
perf(gateway): skip duplicate turn session touch
This commit is contained in:
@@ -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"));
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user