fix(cron): reuse existing sessionId for webhook/cron sessions

When a webhook or cron job provides a stable sessionKey, the session
should maintain conversation history across invocations. Previously,
resolveCronSession always generated a new sessionId and hardcoded
isNewSession: true, preventing any conversation continuity.

Changes:
- Check if existing entry has a valid sessionId
- Evaluate freshness using configured reset policy
- Reuse sessionId and set isNewSession: false when fresh
- Add forceNew parameter to override reuse behavior
- Spread existing entry to preserve conversation context

This enables persistent, stateful conversations for webhook-driven
agent endpoints when allowRequestSessionKey is configured.

Fixes #18027
This commit is contained in:
Operative-001
2026-02-16 13:08:40 +01:00
committed by Peter Steinberger
parent 952db1a3e2
commit 57c8f62396
2 changed files with 150 additions and 5 deletions

View File

@@ -4,9 +4,14 @@ import type { OpenClawConfig } from "../../config/config.js";
vi.mock("../../config/sessions.js", () => ({
loadSessionStore: vi.fn(),
resolveStorePath: vi.fn().mockReturnValue("/tmp/test-store.json"),
evaluateSessionFreshness: vi.fn().mockReturnValue({ fresh: true }),
resolveSessionResetPolicy: vi.fn().mockReturnValue({ mode: "idle", idleMinutes: 60 }),
}));
import { loadSessionStore } from "../../config/sessions.js";
import {
loadSessionStore,
evaluateSessionFreshness,
} from "../../config/sessions.js";
import { resolveCronSession } from "./session.js";
describe("resolveCronSession", () => {
@@ -21,6 +26,7 @@ describe("resolveCronSession", () => {
model: "k2p5",
},
});
vi.mocked(evaluateSessionFreshness).mockReturnValue({ fresh: true });
const result = resolveCronSession({
cfg: {} as OpenClawConfig,
@@ -44,6 +50,7 @@ describe("resolveCronSession", () => {
model: "claude-opus-4-5",
},
});
vi.mocked(evaluateSessionFreshness).mockReturnValue({ fresh: true });
const result = resolveCronSession({
cfg: {} as OpenClawConfig,
@@ -69,5 +76,98 @@ describe("resolveCronSession", () => {
expect(result.sessionEntry.modelOverride).toBeUndefined();
expect(result.sessionEntry.providerOverride).toBeUndefined();
expect(result.sessionEntry.model).toBeUndefined();
expect(result.isNewSession).toBe(true);
});
// New tests for session reuse behavior (#18027)
describe("session reuse for webhooks/cron", () => {
it("reuses existing sessionId when session is fresh", () => {
vi.mocked(loadSessionStore).mockReturnValue({
"webhook:stable-key": {
sessionId: "existing-session-id-123",
updatedAt: Date.now() - 1000,
systemSent: true,
},
});
vi.mocked(evaluateSessionFreshness).mockReturnValue({ fresh: true });
const result = resolveCronSession({
cfg: {} as OpenClawConfig,
sessionKey: "webhook:stable-key",
agentId: "main",
nowMs: Date.now(),
});
expect(result.sessionEntry.sessionId).toBe("existing-session-id-123");
expect(result.isNewSession).toBe(false);
expect(result.systemSent).toBe(true);
});
it("creates new sessionId when session is stale", () => {
vi.mocked(loadSessionStore).mockReturnValue({
"webhook:stable-key": {
sessionId: "old-session-id",
updatedAt: Date.now() - 86400000, // 1 day ago
systemSent: true,
},
});
vi.mocked(evaluateSessionFreshness).mockReturnValue({ fresh: false });
const result = resolveCronSession({
cfg: {} as OpenClawConfig,
sessionKey: "webhook:stable-key",
agentId: "main",
nowMs: Date.now(),
});
expect(result.sessionEntry.sessionId).not.toBe("old-session-id");
expect(result.isNewSession).toBe(true);
expect(result.systemSent).toBe(false);
});
it("creates new sessionId when forceNew is true", () => {
vi.mocked(loadSessionStore).mockReturnValue({
"webhook:stable-key": {
sessionId: "existing-session-id-456",
updatedAt: Date.now() - 1000,
systemSent: true,
},
});
vi.mocked(evaluateSessionFreshness).mockReturnValue({ fresh: true });
const result = resolveCronSession({
cfg: {} as OpenClawConfig,
sessionKey: "webhook:stable-key",
agentId: "main",
nowMs: Date.now(),
forceNew: true,
});
expect(result.sessionEntry.sessionId).not.toBe("existing-session-id-456");
expect(result.isNewSession).toBe(true);
expect(result.systemSent).toBe(false);
});
it("creates new sessionId when entry exists but has no sessionId", () => {
vi.mocked(loadSessionStore).mockReturnValue({
"webhook:stable-key": {
updatedAt: Date.now() - 1000,
modelOverride: "some-model",
} as any,
});
vi.mocked(evaluateSessionFreshness).mockReturnValue({ fresh: true });
const result = resolveCronSession({
cfg: {} as OpenClawConfig,
sessionKey: "webhook:stable-key",
agentId: "main",
nowMs: Date.now(),
});
expect(result.sessionEntry.sessionId).toBeDefined();
expect(result.isNewSession).toBe(true);
// Should still preserve other fields from entry
expect(result.sessionEntry.modelOverride).toBe("some-model");
});
});
});