fix(sessions): avoid guarded route-only entries · openclaw/openclaw@465b621
steipete
·
2026-04-28
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -15,7 +15,7 @@ Docs: https://docs.openclaw.ai
|
15 | 15 | |
16 | 16 | ### Fixes |
17 | 17 | |
18 | | -- 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. |
| 18 | +- 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. |
19 | 19 | - 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. |
20 | 20 | - 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. |
21 | 21 | - 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. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -59,6 +59,13 @@ OpenClaw infers a pinned owner from `allowFrom` when all of these are true:
|
59 | 59 | In that mismatch case, OpenClaw still records inbound session metadata, but it |
60 | 60 | skips updating the main session `lastRoute`. |
61 | 61 | |
| 62 | +## Guarded inbound recording |
| 63 | + |
| 64 | +Channel plugins can mark an inbound session record as `createIfMissing: false` |
| 65 | +when a guarded path must not create a new OpenClaw session. In that mode, |
| 66 | +OpenClaw may update metadata and `lastRoute` for an existing session, but it |
| 67 | +does not create a route-only session entry just because a message was observed. |
| 68 | + |
62 | 69 | ## Routing rules (how an agent is chosen) |
63 | 70 | |
64 | 71 | Routing picks **one agent** for each inbound message: |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -133,7 +133,7 @@ describe("recordInboundSession", () => {
|
133 | 133 | }); |
134 | 134 | }); |
135 | 135 | |
136 | | -it("skips last-route updates when session creation is disabled", async () => { |
| 136 | +it("forwards session creation policy to last-route updates", async () => { |
137 | 137 | await recordInboundSession({ |
138 | 138 | storePath: "/tmp/openclaw-session-store.json", |
139 | 139 | sessionKey: "agent:main:demo-channel:1234:thread:42", |
@@ -152,6 +152,11 @@ describe("recordInboundSession", () => {
|
152 | 152 | createIfMissing: false, |
153 | 153 | }), |
154 | 154 | ); |
155 | | -expect(updateLastRouteMock).not.toHaveBeenCalled(); |
| 155 | +expect(updateLastRouteMock).toHaveBeenCalledWith( |
| 156 | +expect.objectContaining({ |
| 157 | +sessionKey: "agent:main:main", |
| 158 | +createIfMissing: false, |
| 159 | +}), |
| 160 | +); |
156 | 161 | }); |
157 | 162 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -51,7 +51,7 @@ export async function recordInboundSession(params: {
|
51 | 51 | .catch(params.onRecordError); |
52 | 52 | |
53 | 53 | const update = params.updateLastRoute; |
54 | | -if (!update || createIfMissing === false) { |
| 54 | +if (!update) { |
55 | 55 | return; |
56 | 56 | } |
57 | 57 | if (shouldSkipPinnedMainDmRouteUpdate(update.mainDmOwnerPin)) { |
@@ -70,5 +70,6 @@ export async function recordInboundSession(params: {
|
70 | 70 | // Avoid leaking inbound origin metadata into a different target session. |
71 | 71 | ctx: targetSessionKey === canonicalSessionKey ? ctx : undefined, |
72 | 72 | groupResolution, |
| 73 | + createIfMissing, |
73 | 74 | }); |
74 | 75 | } |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -356,6 +356,52 @@ describe("sessions", () => {
|
356 | 356 | expect(store[sessionKey]?.origin?.chatType).toBe("group"); |
357 | 357 | }); |
358 | 358 | |
| 359 | +it("updateLastRoute skips missing sessions when creation is disabled", async () => { |
| 360 | +const sessionKey = "agent:main:demo-chat:group:room-123"; |
| 361 | +const { storePath } = await createSessionStoreFixture({ |
| 362 | +prefix: "updateLastRoute-no-create", |
| 363 | +entries: {}, |
| 364 | +}); |
| 365 | + |
| 366 | +const result = await updateLastRoute({ |
| 367 | + storePath, |
| 368 | + sessionKey, |
| 369 | +deliveryContext: { |
| 370 | +channel: "demo-chat", |
| 371 | +to: "room-123", |
| 372 | +}, |
| 373 | +createIfMissing: false, |
| 374 | +}); |
| 375 | + |
| 376 | +const store = loadSessionStore(storePath); |
| 377 | +expect(result).toBeNull(); |
| 378 | +expect(store[sessionKey]).toBeUndefined(); |
| 379 | +}); |
| 380 | + |
| 381 | +it("updateLastRoute updates existing sessions when creation is disabled", async () => { |
| 382 | +const sessionKey = "agent:main:demo-chat:group:room-123"; |
| 383 | +const { storePath } = await createSessionStoreFixture({ |
| 384 | +prefix: "updateLastRoute-existing-no-create", |
| 385 | +entries: { |
| 386 | +[sessionKey]: buildMainSessionEntry(), |
| 387 | +}, |
| 388 | +}); |
| 389 | + |
| 390 | +await updateLastRoute({ |
| 391 | + storePath, |
| 392 | + sessionKey, |
| 393 | +deliveryContext: { |
| 394 | +channel: "demo-chat", |
| 395 | +to: "room-123", |
| 396 | +}, |
| 397 | +createIfMissing: false, |
| 398 | +}); |
| 399 | + |
| 400 | +const store = loadSessionStore(storePath); |
| 401 | +expect(store[sessionKey]?.lastChannel).toBe("demo-chat"); |
| 402 | +expect(store[sessionKey]?.lastTo).toBe("room-123"); |
| 403 | +}); |
| 404 | + |
359 | 405 | it("updateLastRoute does not bump updatedAt on existing sessions (#49515)", async () => { |
360 | 406 | const mainSessionKey = "agent:main:main"; |
361 | 407 | const frozenUpdatedAt = 1000; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -70,4 +70,5 @@ export type UpdateLastRoute = (params: {
|
70 | 70 | deliveryContext?: DeliveryContext; |
71 | 71 | ctx?: MsgContext; |
72 | 72 | groupResolution?: GroupKeyResolution | null; |
73 | | -}) => Promise<SessionEntry>; |
| 73 | +createIfMissing?: boolean; |
| 74 | +}) => Promise<SessionEntry | null>; |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -757,12 +757,17 @@ export async function updateLastRoute(params: {
|
757 | 757 | deliveryContext?: DeliveryContext; |
758 | 758 | ctx?: MsgContext; |
759 | 759 | groupResolution?: import("./types.js").GroupKeyResolution | null; |
760 | | -}) { |
| 760 | +createIfMissing?: boolean; |
| 761 | +}): Promise<SessionEntry | null> { |
761 | 762 | const { storePath, sessionKey, channel, to, accountId, threadId, ctx } = params; |
| 763 | +const createIfMissing = params.createIfMissing ?? true; |
762 | 764 | return await withSessionStoreLock(storePath, async () => { |
763 | 765 | const store = loadSessionStore(storePath); |
764 | 766 | const resolved = resolveSessionStoreEntry({ store, sessionKey }); |
765 | 767 | const existing = resolved.existing; |
| 768 | +if (!existing && !createIfMissing) { |
| 769 | +return null; |
| 770 | +} |
766 | 771 | const explicitContext = normalizeDeliveryContext(params.deliveryContext); |
767 | 772 | const inlineContext = normalizeDeliveryContext({ |
768 | 773 | channel, |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。