Cron: preserve session scope for main-target reminders

This commit is contained in:
Vignesh Natarajan
2026-02-28 14:53:10 -08:00
parent 61989091a4
commit 2050fd7539
4 changed files with 12 additions and 10 deletions

View File

@@ -82,6 +82,7 @@ Docs: https://docs.openclaw.ai
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz. - Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.
- TUI/Session model status: clear stale runtime model identity when model overrides change so `/model` updates are reflected immediately in `sessions.patch` responses and `sessions.list` status surfaces. (#28619) Thanks @lejean2000. - TUI/Session model status: clear stale runtime model identity when model overrides change so `/model` updates are reflected immediately in `sessions.patch` responses and `sessions.list` status surfaces. (#28619) Thanks @lejean2000.
- Memory/Hybrid recall: when strict hybrid scoring yields no hits, preserve keyword-backed matches using a text-weight floor so freshly indexed lexical canaries no longer disappear behind `minScore` filtering. (#29112) Thanks @ceo-nada. - Memory/Hybrid recall: when strict hybrid scoring yields no hits, preserve keyword-backed matches using a text-weight floor so freshly indexed lexical canaries no longer disappear behind `minScore` filtering. (#29112) Thanks @ceo-nada.
- Cron/Reminder session routing: preserve `job.sessionKey` for `sessionTarget="main"` runs so queued reminders wake and deliver in the originating scoped session/channel instead of being forced to the agent main session.
- Agents/Sessions list transcript paths: resolve `sessions_list` `transcriptPath` via agent-aware session path options and ignore combined-store sentinel paths (`(multiple)`) so listed transcript paths always point to the state directory. (#28379) Thanks @fafuzuoluo. - Agents/Sessions list transcript paths: resolve `sessions_list` `transcriptPath` via agent-aware session path options and ignore combined-store sentinel paths (`(multiple)`) so listed transcript paths always point to the state directory. (#28379) Thanks @fafuzuoluo.
- Podman/Quadlet setup: fix `sed` escaping and UID mismatch in Podman Quadlet setup. (#26414) Thanks @KnHack and @vincentkoc. - Podman/Quadlet setup: fix `sed` escaping and UID mismatch in Podman Quadlet setup. (#26414) Thanks @KnHack and @vincentkoc.
- Browser/Navigate: resolve the correct `targetId` in navigate responses after renderer swaps. (#25326) Thanks @stone-jin and @vincentkoc. - Browser/Navigate: resolve the correct `targetId` in navigate responses after renderer swaps. (#25326) Thanks @stone-jin and @vincentkoc.

View File

@@ -509,7 +509,7 @@ describe("CronService", () => {
await store.cleanup(); await store.cleanup();
}); });
it("passes agentId and resolves main session for wakeMode now main jobs", async () => { it("passes agentId and preserves scoped session for wakeMode now main jobs", async () => {
const runHeartbeatOnce = vi.fn(async () => ({ status: "ran" as const, durationMs: 1 })); const runHeartbeatOnce = vi.fn(async () => ({ status: "ran" as const, durationMs: 1 }));
const { store, cron, enqueueSystemEvent, requestHeartbeatNow } = const { store, cron, enqueueSystemEvent, requestHeartbeatNow } =
@@ -534,13 +534,13 @@ describe("CronService", () => {
expect.objectContaining({ expect.objectContaining({
reason: `cron:${job.id}`, reason: `cron:${job.id}`,
agentId: "ops", agentId: "ops",
sessionKey: undefined, sessionKey,
}), }),
); );
expect(requestHeartbeatNow).not.toHaveBeenCalled(); expect(requestHeartbeatNow).not.toHaveBeenCalled();
expect(enqueueSystemEvent).toHaveBeenCalledWith( expect(enqueueSystemEvent).toHaveBeenCalledWith(
"hello", "hello",
expect.objectContaining({ agentId: "ops", sessionKey: undefined }), expect.objectContaining({ agentId: "ops", sessionKey }),
); );
cron.stop(); cron.stop();
@@ -578,7 +578,7 @@ describe("CronService", () => {
expect(requestHeartbeatNow).toHaveBeenCalledWith( expect(requestHeartbeatNow).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
reason: `cron:${job.id}`, reason: `cron:${job.id}`,
sessionKey: undefined, sessionKey,
}), }),
); );
expect(job.state.lastStatus).toBe("ok"); expect(job.state.lastStatus).toBe("ok");

View File

@@ -640,9 +640,10 @@ export async function executeJobCore(
: 'main job requires payload.kind="systemEvent"', : 'main job requires payload.kind="systemEvent"',
}; };
} }
// main-target cron jobs should always resolve via the agent's main session. // Preserve the job session namespace for main-target reminders so heartbeat
// Avoid forwarding persisted channel session keys from legacy records. // routing can deliver follow-through in the originating channel/thread.
const targetMainSessionKey = undefined; // Downstream gateway wiring canonicalizes/guards this key per agent.
const targetMainSessionKey = job.sessionKey;
state.deps.enqueueSystemEvent(text, { state.deps.enqueueSystemEvent(text, {
agentId: job.agentId, agentId: job.agentId,
sessionKey: targetMainSessionKey, sessionKey: targetMainSessionKey,

View File

@@ -40,7 +40,7 @@ describe("buildGatewayCronService", () => {
fetchWithSsrFGuardMock.mockClear(); fetchWithSsrFGuardMock.mockClear();
}); });
it("routes main-target jobs to the main session for enqueue + wake", async () => { it("routes main-target jobs to the scoped session for enqueue + wake", async () => {
const tmpDir = path.join(os.tmpdir(), `server-cron-${Date.now()}`); const tmpDir = path.join(os.tmpdir(), `server-cron-${Date.now()}`);
const cfg = { const cfg = {
session: { session: {
@@ -73,12 +73,12 @@ describe("buildGatewayCronService", () => {
expect(enqueueSystemEventMock).toHaveBeenCalledWith( expect(enqueueSystemEventMock).toHaveBeenCalledWith(
"hello", "hello",
expect.objectContaining({ expect.objectContaining({
sessionKey: "agent:main:main", sessionKey: "agent:main:discord:channel:ops",
}), }),
); );
expect(requestHeartbeatNowMock).toHaveBeenCalledWith( expect(requestHeartbeatNowMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
sessionKey: undefined, sessionKey: "agent:main:discord:channel:ops",
}), }),
); );
} finally { } finally {