Skip to content

Commit 9c95abd

Browse files
authored
fix: seed Slack thread context after reset (#97100)
1 parent 119dc4b commit 9c95abd

21 files changed

Lines changed: 823 additions & 96 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
760812c17f7e48d7ceafeebbbe348dad13916ccb9ecaf41b3abc9a09b1e690c1 plugin-sdk-api-baseline.json
2-
4d9b76016b2f845e101949a3d2ac92437f49783906d1c263d65f3534bb333de5 plugin-sdk-api-baseline.jsonl
1+
ad74d16da51b0d6da0b1fad75b4903c5554e02ac41c00ee5dae89fa63ceef802 plugin-sdk-api-baseline.json
2+
d1a9c33c40c039cda1f5d1a7d29ed20fb88d073b6a84899faca0a44fbc5a5092 plugin-sdk-api-baseline.jsonl

docs/plugins/sdk-testing.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import { mockNodeBuiltinModule } from "openclaw/plugin-sdk/test-node-mocks";
102102
| `registerProviderPlugins` | Capture provider registrations across multiple plugins. Import from `plugin-sdk/plugin-test-runtime` |
103103
| `requireRegisteredProvider` | Assert that a provider collection contains an id. Import from `plugin-sdk/plugin-test-runtime` |
104104
| `createRuntimeEnv` | Build a mocked CLI/plugin runtime environment. Import from `plugin-sdk/plugin-test-runtime` |
105+
| `createPluginRuntimeMock` | Build a mocked plugin runtime surface. Import from `plugin-sdk/plugin-test-runtime` |
105106
| `createPluginSetupWizardStatus` | Build setup status helpers for channel plugins. Import from `plugin-sdk/plugin-test-runtime` |
106107
| `describeOpenAIProviderRuntimeContract` | Install provider-family runtime contract checks. Import from `plugin-sdk/provider-test-contracts` |
107108
| `expectPassthroughReplayPolicy` | Assert provider replay policies pass through provider-owned tools and metadata. Import from `plugin-sdk/provider-test-contracts` |
@@ -213,11 +214,10 @@ entry to declare `kind: "memory"`.
213214

214215
### Testing runtime config access
215216

216-
Prefer the shared plugin runtime mock from `openclaw/plugin-sdk/channel-test-helpers`
217-
when testing bundled channel plugins. Its deprecated `runtime.config.loadConfig()` and
218-
`runtime.config.writeConfigFile(...)` mocks throw by default so tests catch new
219-
usage of compatibility APIs. Override those mocks only when the test is
220-
explicitly covering legacy compatibility behavior.
217+
Prefer the shared plugin runtime mock from `openclaw/plugin-sdk/plugin-test-runtime`.
218+
Its deprecated `runtime.config.loadConfig()` and `runtime.config.writeConfigFile(...)`
219+
mocks throw by default so tests catch new usage of compatibility APIs. Override
220+
those mocks only when the test is explicitly covering legacy compatibility behavior.
221221

222222
### Unit testing a channel plugin
223223

extensions/slack/src/monitor/config.runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
33
export { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
44
export {
55
readSessionUpdatedAt,
6+
resolveChannelResetConfig,
67
resolveSessionKey,
78
resolveStorePath,
89
updateLastRoute,

extensions/slack/src/monitor/context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { App } from "@slack/bolt";
33
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
44
import { formatAllowlistMatchMeta } from "openclaw/plugin-sdk/allow-from";
5+
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
56
import type {
67
OpenClawConfig,
78
SlackReactionNotificationMode,
@@ -97,6 +98,7 @@ export type SlackMonitorContext = {
9798
botToken: string;
9899
app: App;
99100
runtime: RuntimeEnv;
101+
channelRuntime?: ChannelRuntimeSurface;
100102

101103
botUserId: string;
102104
botId?: string;
@@ -184,6 +186,7 @@ export function createSlackMonitorContext(params: {
184186
botToken: string;
185187
app: App;
186188
runtime: RuntimeEnv;
189+
channelRuntime?: ChannelRuntimeSurface;
187190

188191
botUserId: string;
189192
botId?: string;
@@ -601,6 +604,7 @@ export function createSlackMonitorContext(params: {
601604
botToken: params.botToken,
602605
app: params.app,
603606
runtime: params.runtime,
607+
channelRuntime: params.channelRuntime,
604608
botUserId: params.botUserId,
605609
botId: params.botId,
606610
teamId: params.teamId,

extensions/slack/src/monitor/message-handler/prepare-thread-context.ts

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Slack plugin module implements prepare thread context behavior.
22
import { formatInboundEnvelope } from "openclaw/plugin-sdk/channel-inbound";
33
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
4-
import type { ContextVisibilityMode } from "openclaw/plugin-sdk/config-contracts";
4+
import type { ContextVisibilityMode, OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
55
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
66
import {
77
filterSupplementalContextItems,
@@ -10,7 +10,7 @@ import {
1010
import type { ResolvedSlackAccount } from "../../accounts.js";
1111
import type { SlackMessageEvent } from "../../types.js";
1212
import { resolveSlackAllowListMatch } from "../allow-list.js";
13-
import { readSessionUpdatedAt } from "../config.runtime.js";
13+
import { readSessionUpdatedAt, resolveChannelResetConfig } from "../config.runtime.js";
1414
import type { SlackMonitorContext } from "../context.js";
1515
import type { SlackMediaResult } from "../media-types.js";
1616
import { resolveSlackThreadHistory, type SlackThreadStarter } from "../thread.js";
@@ -35,13 +35,49 @@ function loadSlackMediaModule(): Promise<SlackMediaModule> {
3535
type SlackThreadContextData = {
3636
threadStarterBody: string | undefined;
3737
threadHistoryBody: string | undefined;
38-
threadSessionPreviousTimestamp: number | undefined;
38+
shouldSeedInitialThreadContext: boolean;
3939
threadLabel: string | undefined;
4040
threadStarterMedia: SlackMediaResult[] | null;
4141
};
4242

4343
const SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY = 4;
4444

45+
type SlackSessionResetFreshness = {
46+
state: "missing" | "fresh" | "stale";
47+
};
48+
49+
type SlackSessionFreshnessRuntime = {
50+
session?: {
51+
resolveEntryResetFreshness?: (params: {
52+
storePath?: string;
53+
sessionKey: string;
54+
sessionCfg?: OpenClawConfig["session"];
55+
resetType: "thread";
56+
resetOverride?: ReturnType<typeof resolveChannelResetConfig>;
57+
}) => SlackSessionResetFreshness;
58+
};
59+
};
60+
61+
function resolveSlackThreadSessionFreshness(params: {
62+
ctx: SlackMonitorContext;
63+
storePath: string;
64+
sessionKey: string;
65+
}): SlackSessionResetFreshness | undefined {
66+
// Gateway startup supplies the full channel runtime, but the public surface
67+
// intentionally keeps non-context helpers untyped for external plugins.
68+
const runtime = params.ctx.channelRuntime as SlackSessionFreshnessRuntime | undefined;
69+
return runtime?.session?.resolveEntryResetFreshness?.({
70+
storePath: params.storePath,
71+
sessionKey: params.sessionKey,
72+
sessionCfg: params.ctx.cfg.session,
73+
resetType: "thread",
74+
resetOverride: resolveChannelResetConfig({
75+
sessionCfg: params.ctx.cfg.session,
76+
channel: "slack",
77+
}),
78+
});
79+
}
80+
4581
function isSlackThreadContextSenderAllowed(params: {
4682
allowFromLower: string[];
4783
allowNameMatching: boolean;
@@ -125,19 +161,36 @@ export async function resolveSlackThreadContextData(params: {
125161
let threadHistoryBody: string | undefined;
126162
let threadLabel: string | undefined;
127163
let threadStarterMedia: SlackMediaResult[] | null = null;
128-
const threadSessionPreviousTimestamp =
164+
const threadSessionFreshness =
129165
params.isThreadReply && params.threadTs
166+
? resolveSlackThreadSessionFreshness({
167+
ctx: params.ctx,
168+
storePath: params.storePath,
169+
sessionKey: params.sessionKey,
170+
})
171+
: undefined;
172+
const threadSessionPreviousTimestamp =
173+
params.isThreadReply && params.threadTs && !threadSessionFreshness
130174
? readSessionUpdatedAt({
131175
storePath: params.storePath,
132176
sessionKey: params.sessionKey,
133177
})
134178
: undefined;
179+
const shouldSeedInitialThreadContext = Boolean(
180+
params.isThreadReply &&
181+
params.threadTs &&
182+
(threadSessionFreshness
183+
? threadSessionFreshness.state !== "fresh"
184+
: threadSessionPreviousTimestamp === undefined),
185+
);
186+
const shouldLoadInitialThreadHistory =
187+
shouldSeedInitialThreadContext || params.forceInitialHistory === true;
135188

136189
if (!params.isThreadReply || !params.threadTs) {
137190
return {
138191
threadStarterBody,
139192
threadHistoryBody,
140-
threadSessionPreviousTimestamp,
193+
shouldSeedInitialThreadContext,
141194
threadLabel,
142195
threadStarterMedia,
143196
};
@@ -195,10 +248,9 @@ export async function resolveSlackThreadContextData(params: {
195248
threadLabel = `Slack thread ${params.roomLabel}`;
196249
}
197250

198-
const isNewThreadSession = !threadSessionPreviousTimestamp;
199251
const includeBotStarterAsRootContext = shouldIncludeBotThreadStarterContext({
200252
starterIsCurrentBot,
201-
isNewThreadSession,
253+
isNewThreadSession: shouldSeedInitialThreadContext,
202254
hasStarterText: Boolean(starter?.text),
203255
});
204256

@@ -218,10 +270,7 @@ export async function resolveSlackThreadContextData(params: {
218270

219271
const threadInitialHistoryLimit = params.account.config?.thread?.initialHistoryLimit ?? 20;
220272

221-
if (
222-
threadInitialHistoryLimit > 0 &&
223-
(!threadSessionPreviousTimestamp || params.forceInitialHistory)
224-
) {
273+
if (threadInitialHistoryLimit > 0 && shouldLoadInitialThreadHistory) {
225274
const currentBotRootTs = starter?.ts ?? params.threadTs;
226275
const threadHistory = await resolveSlackThreadHistory({
227276
channelId: params.message.channel,
@@ -333,7 +382,7 @@ export async function resolveSlackThreadContextData(params: {
333382
return {
334383
threadStarterBody,
335384
threadHistoryBody,
336-
threadSessionPreviousTimestamp,
385+
shouldSeedInitialThreadContext,
337386
threadLabel,
338387
threadStarterMedia,
339388
};

extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import fs from "node:fs";
33
import path from "node:path";
44
import type { App } from "@slack/bolt";
5+
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
56
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
7+
import { createPluginRuntimeMock } from "openclaw/plugin-sdk/plugin-test-runtime";
68
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
79
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
810
import type { ResolvedSlackAccount } from "../../accounts.js";
@@ -17,13 +19,15 @@ export function createInboundSlackTestContext(params: {
1719
channelsConfig?: SlackChannelConfigEntries;
1820
threadRequireExplicitMention?: boolean;
1921
dmHistoryLimit?: number;
22+
channelRuntime?: ChannelRuntimeSurface;
2023
}) {
2124
return createSlackMonitorContext({
2225
cfg: params.cfg,
2326
accountId: "default",
2427
botToken: "token",
2528
app: { client: params.appClient ?? {} } as App,
2629
runtime: {} as RuntimeEnv,
30+
channelRuntime: params.channelRuntime ?? createPluginRuntimeMock().channel,
2731
botUserId: "B1",
2832
botId: "B1",
2933
teamId: "T1",

0 commit comments

Comments
 (0)