fix(sessions): avoid guarded route-only entries

This commit is contained in:
Peter Steinberger
2026-04-27 21:10:44 +01:00
parent d62cb3c681
commit 465b621cf1
7 changed files with 71 additions and 6 deletions

View File

@@ -15,7 +15,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Channels/sessions: skip last-route writes when inbound session recording explicitly disables creation, so plugin-owned guarded inbound paths cannot create route-only phantom sessions. Carries forward #73009. Thanks @jzakirov.
- Channels/sessions: prevent guarded inbound session recording from creating route-only phantom sessions while still allowing last-route updates for sessions that already exist. Carries forward #73009. Thanks @jzakirov.
- Plugins/runtime deps: stage bundled plugin dependencies imported by mirrored root dist chunks, so packaged memory and status commands do not miss `chokidar` or similar root-chunk dependencies after update. Fixes #72882 and #72970; carries forward #72992. Thanks @shrimpy8, @colin-chang, and @Schnup03.
- Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg.
- CLI/message: resolve targeted `openclaw message` channels to their owning plugin before loading the registry, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans without assuming channel ids match plugin ids. Fixes #73006. Thanks @jasonftl.

View File

@@ -59,6 +59,13 @@ OpenClaw infers a pinned owner from `allowFrom` when all of these are true:
In that mismatch case, OpenClaw still records inbound session metadata, but it
skips updating the main session `lastRoute`.
## Guarded inbound recording
Channel plugins can mark an inbound session record as `createIfMissing: false`
when a guarded path must not create a new OpenClaw session. In that mode,
OpenClaw may update metadata and `lastRoute` for an existing session, but it
does not create a route-only session entry just because a message was observed.
## Routing rules (how an agent is chosen)
Routing picks **one agent** for each inbound message:

View File

@@ -133,7 +133,7 @@ describe("recordInboundSession", () => {
});
});
it("skips last-route updates when session creation is disabled", async () => {
it("forwards session creation policy to last-route updates", async () => {
await recordInboundSession({
storePath: "/tmp/openclaw-session-store.json",
sessionKey: "agent:main:demo-channel:1234:thread:42",
@@ -152,6 +152,11 @@ describe("recordInboundSession", () => {
createIfMissing: false,
}),
);
expect(updateLastRouteMock).not.toHaveBeenCalled();
expect(updateLastRouteMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: "agent:main:main",
createIfMissing: false,
}),
);
});
});

View File

@@ -51,7 +51,7 @@ export async function recordInboundSession(params: {
.catch(params.onRecordError);
const update = params.updateLastRoute;
if (!update || createIfMissing === false) {
if (!update) {
return;
}
if (shouldSkipPinnedMainDmRouteUpdate(update.mainDmOwnerPin)) {
@@ -70,5 +70,6 @@ export async function recordInboundSession(params: {
// Avoid leaking inbound origin metadata into a different target session.
ctx: targetSessionKey === canonicalSessionKey ? ctx : undefined,
groupResolution,
createIfMissing,
});
}

View File

@@ -356,6 +356,52 @@ describe("sessions", () => {
expect(store[sessionKey]?.origin?.chatType).toBe("group");
});
it("updateLastRoute skips missing sessions when creation is disabled", async () => {
const sessionKey = "agent:main:demo-chat:group:room-123";
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute-no-create",
entries: {},
});
const result = await updateLastRoute({
storePath,
sessionKey,
deliveryContext: {
channel: "demo-chat",
to: "room-123",
},
createIfMissing: false,
});
const store = loadSessionStore(storePath);
expect(result).toBeNull();
expect(store[sessionKey]).toBeUndefined();
});
it("updateLastRoute updates existing sessions when creation is disabled", async () => {
const sessionKey = "agent:main:demo-chat:group:room-123";
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute-existing-no-create",
entries: {
[sessionKey]: buildMainSessionEntry(),
},
});
await updateLastRoute({
storePath,
sessionKey,
deliveryContext: {
channel: "demo-chat",
to: "room-123",
},
createIfMissing: false,
});
const store = loadSessionStore(storePath);
expect(store[sessionKey]?.lastChannel).toBe("demo-chat");
expect(store[sessionKey]?.lastTo).toBe("room-123");
});
it("updateLastRoute does not bump updatedAt on existing sessions (#49515)", async () => {
const mainSessionKey = "agent:main:main";
const frozenUpdatedAt = 1000;

View File

@@ -70,4 +70,5 @@ export type UpdateLastRoute = (params: {
deliveryContext?: DeliveryContext;
ctx?: MsgContext;
groupResolution?: GroupKeyResolution | null;
}) => Promise<SessionEntry>;
createIfMissing?: boolean;
}) => Promise<SessionEntry | null>;

View File

@@ -757,12 +757,17 @@ export async function updateLastRoute(params: {
deliveryContext?: DeliveryContext;
ctx?: MsgContext;
groupResolution?: import("./types.js").GroupKeyResolution | null;
}) {
createIfMissing?: boolean;
}): Promise<SessionEntry | null> {
const { storePath, sessionKey, channel, to, accountId, threadId, ctx } = params;
const createIfMissing = params.createIfMissing ?? true;
return await withSessionStoreLock(storePath, async () => {
const store = loadSessionStore(storePath);
const resolved = resolveSessionStoreEntry({ store, sessionKey });
const existing = resolved.existing;
if (!existing && !createIfMissing) {
return null;
}
const explicitContext = normalizeDeliveryContext(params.deliveryContext);
const inlineContext = normalizeDeliveryContext({
channel,