mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:51:37 +00:00
fix(slack): keep thread session fork/history context after first turn (#23843)
* Slack thread sessions: keep forking and history context after first turn * Update CHANGELOG.md
This commit is contained in:
@@ -169,6 +169,20 @@ describe("runPreparedReply media-only handling", () => {
|
||||
expect(call?.followupRun.prompt).toContain("[User sent media without caption]");
|
||||
});
|
||||
|
||||
it("keeps thread history context on follow-up turns", async () => {
|
||||
const result = await runPreparedReply(
|
||||
baseParams({
|
||||
isNewSession: false,
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({ text: "ok" });
|
||||
|
||||
const call = vi.mocked(runReplyAgent).mock.calls[0]?.[0];
|
||||
expect(call).toBeTruthy();
|
||||
expect(call?.followupRun.prompt).toContain("[Thread history - for context]");
|
||||
expect(call?.followupRun.prompt).toContain("Earlier message in this thread");
|
||||
});
|
||||
|
||||
it("returns the empty-body reply when there is no text and no media", async () => {
|
||||
const result = await runPreparedReply(
|
||||
baseParams({
|
||||
|
||||
@@ -260,12 +260,11 @@ export async function runPreparedReply(
|
||||
prefixedBodyBase = appendUntrustedContext(prefixedBodyBase, sessionCtx.UntrustedContext);
|
||||
const threadStarterBody = ctx.ThreadStarterBody?.trim();
|
||||
const threadHistoryBody = ctx.ThreadHistoryBody?.trim();
|
||||
const threadContextNote =
|
||||
isNewSession && threadHistoryBody
|
||||
? `[Thread history - for context]\n${threadHistoryBody}`
|
||||
: isNewSession && threadStarterBody
|
||||
? `[Thread starter - for context]\n${threadStarterBody}`
|
||||
: undefined;
|
||||
const threadContextNote = threadHistoryBody
|
||||
? `[Thread history - for context]\n${threadHistoryBody}`
|
||||
: threadStarterBody
|
||||
? `[Thread starter - for context]\n${threadStarterBody}`
|
||||
: undefined;
|
||||
const skillResult = await ensureSkillSnapshot({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
|
||||
@@ -126,6 +126,81 @@ describe("initSessionState thread forking", () => {
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
it("forks from parent when thread session key already exists but was not forked yet", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const root = await makeCaseDir("openclaw-thread-session-existing-");
|
||||
const sessionsDir = path.join(root, "sessions");
|
||||
await fs.mkdir(sessionsDir);
|
||||
|
||||
const parentSessionId = "parent-session";
|
||||
const parentSessionFile = path.join(sessionsDir, "parent.jsonl");
|
||||
const header = {
|
||||
type: "session",
|
||||
version: 3,
|
||||
id: parentSessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: process.cwd(),
|
||||
};
|
||||
const message = {
|
||||
type: "message",
|
||||
id: "m1",
|
||||
parentId: null,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: { role: "user", content: "Parent prompt" },
|
||||
};
|
||||
await fs.writeFile(
|
||||
parentSessionFile,
|
||||
`${JSON.stringify(header)}\n${JSON.stringify(message)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const storePath = path.join(root, "sessions.json");
|
||||
const parentSessionKey = "agent:main:slack:channel:c1";
|
||||
const threadSessionKey = "agent:main:slack:channel:c1:thread:123";
|
||||
await saveSessionStore(storePath, {
|
||||
[parentSessionKey]: {
|
||||
sessionId: parentSessionId,
|
||||
sessionFile: parentSessionFile,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
[threadSessionKey]: {
|
||||
sessionId: "preseed-thread-session",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const first = await initSessionState({
|
||||
ctx: {
|
||||
Body: "Thread reply",
|
||||
SessionKey: threadSessionKey,
|
||||
ParentSessionKey: parentSessionKey,
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(first.sessionEntry.sessionId).not.toBe("preseed-thread-session");
|
||||
expect(first.sessionEntry.forkedFromParent).toBe(true);
|
||||
|
||||
const second = await initSessionState({
|
||||
ctx: {
|
||||
Body: "Thread reply 2",
|
||||
SessionKey: threadSessionKey,
|
||||
ParentSessionKey: parentSessionKey,
|
||||
},
|
||||
cfg,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(second.sessionEntry.sessionId).toBe(first.sessionEntry.sessionId);
|
||||
expect(second.sessionEntry.forkedFromParent).toBe(true);
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
it("records topic-specific session files when MessageThreadId is present", async () => {
|
||||
const root = await makeCaseDir("openclaw-topic-session-");
|
||||
const storePath = path.join(root, "sessions.json");
|
||||
|
||||
@@ -336,11 +336,12 @@ export async function initSessionState(params: {
|
||||
sessionEntry.displayName = threadLabel;
|
||||
}
|
||||
const parentSessionKey = ctx.ParentSessionKey?.trim();
|
||||
const alreadyForked = sessionEntry.forkedFromParent === true;
|
||||
if (
|
||||
isNewSession &&
|
||||
parentSessionKey &&
|
||||
parentSessionKey !== sessionKey &&
|
||||
sessionStore[parentSessionKey]
|
||||
sessionStore[parentSessionKey] &&
|
||||
!alreadyForked
|
||||
) {
|
||||
log.warn(
|
||||
`forking from parent session: parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
|
||||
@@ -355,6 +356,7 @@ export async function initSessionState(params: {
|
||||
sessionId = forked.sessionId;
|
||||
sessionEntry.sessionId = forked.sessionId;
|
||||
sessionEntry.sessionFile = forked.sessionFile;
|
||||
sessionEntry.forkedFromParent = true;
|
||||
log.warn(`forked session created: file=${forked.sessionFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user