diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c716810..acd0e05a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. - 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. +- 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. - 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. diff --git a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts index cc3d6cd46..5856a007b 100644 --- a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts +++ b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts @@ -509,7 +509,7 @@ describe("CronService", () => { 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 { store, cron, enqueueSystemEvent, requestHeartbeatNow } = @@ -534,13 +534,13 @@ describe("CronService", () => { expect.objectContaining({ reason: `cron:${job.id}`, agentId: "ops", - sessionKey: undefined, + sessionKey, }), ); expect(requestHeartbeatNow).not.toHaveBeenCalled(); expect(enqueueSystemEvent).toHaveBeenCalledWith( "hello", - expect.objectContaining({ agentId: "ops", sessionKey: undefined }), + expect.objectContaining({ agentId: "ops", sessionKey }), ); cron.stop(); @@ -578,7 +578,7 @@ describe("CronService", () => { expect(requestHeartbeatNow).toHaveBeenCalledWith( expect.objectContaining({ reason: `cron:${job.id}`, - sessionKey: undefined, + sessionKey, }), ); expect(job.state.lastStatus).toBe("ok"); diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index 7a0a8d181..5d12e96ee 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -640,9 +640,10 @@ export async function executeJobCore( : 'main job requires payload.kind="systemEvent"', }; } - // main-target cron jobs should always resolve via the agent's main session. - // Avoid forwarding persisted channel session keys from legacy records. - const targetMainSessionKey = undefined; + // Preserve the job session namespace for main-target reminders so heartbeat + // routing can deliver follow-through in the originating channel/thread. + // Downstream gateway wiring canonicalizes/guards this key per agent. + const targetMainSessionKey = job.sessionKey; state.deps.enqueueSystemEvent(text, { agentId: job.agentId, sessionKey: targetMainSessionKey, diff --git a/src/gateway/server-cron.test.ts b/src/gateway/server-cron.test.ts index cd322bc6e..945840a71 100644 --- a/src/gateway/server-cron.test.ts +++ b/src/gateway/server-cron.test.ts @@ -40,7 +40,7 @@ describe("buildGatewayCronService", () => { 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 cfg = { session: { @@ -73,12 +73,12 @@ describe("buildGatewayCronService", () => { expect(enqueueSystemEventMock).toHaveBeenCalledWith( "hello", expect.objectContaining({ - sessionKey: "agent:main:main", + sessionKey: "agent:main:discord:channel:ops", }), ); expect(requestHeartbeatNowMock).toHaveBeenCalledWith( expect.objectContaining({ - sessionKey: undefined, + sessionKey: "agent:main:discord:channel:ops", }), ); } finally {