Skip to content

Commit b782ecb

Browse files
committed
refactor: harden plugin install flow and main DM route pinning
1 parent af637de commit b782ecb

22 files changed

Lines changed: 737 additions & 269 deletions

docs/channels/channel-routing.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ Examples:
4141
- `agent:main:telegram:group:-1001234567890:topic:42`
4242
- `agent:main:discord:channel:123456:thread:987654`
4343

44+
## Main DM route pinning
45+
46+
When `session.dmScope` is `main`, direct messages may share one main session.
47+
To prevent the session’s `lastRoute` from being overwritten by non-owner DMs,
48+
OpenClaw infers a pinned owner from `allowFrom` when all of these are true:
49+
50+
- `allowFrom` has exactly one non-wildcard entry.
51+
- The entry can be normalized to a concrete sender ID for that channel.
52+
- The inbound DM sender does not match that pinned owner.
53+
54+
In that mismatch case, OpenClaw still records inbound session metadata, but it
55+
skips updating the main session `lastRoute`.
56+
4457
## Routing rules (how an agent is chosen)
4558

4659
Routing picks **one agent** for each inbound message:

src/channels/session.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,32 @@ describe("recordInboundSession", () => {
103103
}),
104104
);
105105
});
106+
107+
it("skips last-route updates when main DM owner pin mismatches sender", async () => {
108+
const { recordInboundSession } = await import("./session.js");
109+
const onSkip = vi.fn();
110+
111+
await recordInboundSession({
112+
storePath: "/tmp/openclaw-session-store.json",
113+
sessionKey: "agent:main:telegram:1234:thread:42",
114+
ctx,
115+
updateLastRoute: {
116+
sessionKey: "agent:main:main",
117+
channel: "telegram",
118+
to: "telegram:1234",
119+
mainDmOwnerPin: {
120+
ownerRecipient: "1234",
121+
senderRecipient: "9999",
122+
onSkip,
123+
},
124+
},
125+
onRecordError: vi.fn(),
126+
});
127+
128+
expect(updateLastRouteMock).not.toHaveBeenCalled();
129+
expect(onSkip).toHaveBeenCalledWith({
130+
ownerRecipient: "1234",
131+
senderRecipient: "9999",
132+
});
133+
});
106134
});

src/channels/session.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,28 @@ export type InboundLastRouteUpdate = {
1616
to: string;
1717
accountId?: string;
1818
threadId?: string | number;
19+
mainDmOwnerPin?: {
20+
ownerRecipient: string;
21+
senderRecipient: string;
22+
onSkip?: (params: { ownerRecipient: string; senderRecipient: string }) => void;
23+
};
1924
};
2025

26+
function shouldSkipPinnedMainDmRouteUpdate(
27+
pin: InboundLastRouteUpdate["mainDmOwnerPin"] | undefined,
28+
): boolean {
29+
if (!pin) {
30+
return false;
31+
}
32+
const owner = pin.ownerRecipient.trim().toLowerCase();
33+
const sender = pin.senderRecipient.trim().toLowerCase();
34+
if (!owner || !sender || owner === sender) {
35+
return false;
36+
}
37+
pin.onSkip?.({ ownerRecipient: pin.ownerRecipient, senderRecipient: pin.senderRecipient });
38+
return true;
39+
}
40+
2141
export async function recordInboundSession(params: {
2242
storePath: string;
2343
sessionKey: string;
@@ -41,6 +61,9 @@ export async function recordInboundSession(params: {
4161
if (!update) {
4262
return;
4363
}
64+
if (shouldSkipPinnedMainDmRouteUpdate(update.mainDmOwnerPin)) {
65+
return;
66+
}
4467
const targetSessionKey = normalizeSessionStoreKey(update.sessionKey);
4568
await updateLastRoute({
4669
storePath,

0 commit comments

Comments
 (0)