Skip to content

Commit aad59ac

Browse files
committed
feat(tui): preserve main session continuity
1 parent 2649c03 commit aad59ac

10 files changed

Lines changed: 730 additions & 20 deletions

File tree

src/auto-reply/reply/session.test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,157 @@ describe("initSessionState reset policy", () => {
10761076
});
10771077
});
10781078

1079+
describe("initSessionState orphan short replies", () => {
1080+
async function writeTranscript(params: {
1081+
transcriptPath: string;
1082+
sessionId: string;
1083+
userText: string;
1084+
assistantText: string;
1085+
}) {
1086+
await fs.mkdir(path.dirname(params.transcriptPath), { recursive: true });
1087+
const header = {
1088+
type: "session",
1089+
version: 3,
1090+
id: params.sessionId,
1091+
timestamp: new Date().toISOString(),
1092+
cwd: process.cwd(),
1093+
};
1094+
const userMessage = {
1095+
type: "message",
1096+
id: "m1",
1097+
parentId: null,
1098+
timestamp: new Date().toISOString(),
1099+
message: { role: "user", content: params.userText },
1100+
};
1101+
const assistantMessage = {
1102+
type: "message",
1103+
id: "m2",
1104+
parentId: "m1",
1105+
timestamp: new Date().toISOString(),
1106+
message: { role: "assistant", content: params.assistantText },
1107+
};
1108+
await fs.writeFile(
1109+
params.transcriptPath,
1110+
`${JSON.stringify(header)}\n${JSON.stringify(userMessage)}\n${JSON.stringify(assistantMessage)}\n`,
1111+
"utf-8",
1112+
);
1113+
}
1114+
1115+
it("reattaches short confirmation replies to the latest awaiting TUI session", async () => {
1116+
vi.useFakeTimers();
1117+
vi.setSystemTime(new Date("2026-03-17T09:00:00.000Z"));
1118+
1119+
const root = await makeCaseDir("openclaw-orphan-short-reply-");
1120+
const storePath = path.join(root, "sessions.json");
1121+
const sessionsDir = path.join(root, "sessions");
1122+
const resumedSessionId = "tui-session-resume";
1123+
const resumedTranscript = path.join(sessionsDir, `${resumedSessionId}.jsonl`);
1124+
const resumedSessionKey = "agent:main:tui:main:tui-pane-1";
1125+
1126+
await writeTranscript({
1127+
transcriptPath: resumedTranscript,
1128+
sessionId: resumedSessionId,
1129+
userText: "Should we ship this now?",
1130+
assistantText: "Do you want me to continue with the current plan? Reply yes to continue.",
1131+
});
1132+
1133+
await writeSessionStoreFast(storePath, {
1134+
[resumedSessionKey]: {
1135+
sessionId: resumedSessionId,
1136+
sessionFile: resumedTranscript,
1137+
updatedAt: Date.now() - 8 * 60 * 60 * 1000,
1138+
origin: {
1139+
provider: "webchat",
1140+
to: "session:tui:tui-pane-1",
1141+
},
1142+
deliveryContext: {
1143+
channel: "webchat",
1144+
to: "session:tui:tui-pane-1",
1145+
},
1146+
lastChannel: "webchat",
1147+
lastTo: "session:tui:tui-pane-1",
1148+
},
1149+
});
1150+
1151+
const cfg = {
1152+
session: {
1153+
store: storePath,
1154+
},
1155+
} as OpenClawConfig;
1156+
1157+
const result = await initSessionState({
1158+
ctx: {
1159+
Body: "yes",
1160+
RawBody: "yes",
1161+
BodyForCommands: "yes",
1162+
SessionKey: "agent:main:fresh-confirmation",
1163+
Surface: "webchat",
1164+
Provider: "webchat",
1165+
OriginatingChannel: "webchat",
1166+
OriginatingTo: "session:tui:tui-pane-1",
1167+
SenderId: "gateway-client",
1168+
},
1169+
cfg,
1170+
commandAuthorized: true,
1171+
});
1172+
1173+
expect(result.sessionKey).toBe(resumedSessionKey);
1174+
expect(result.sessionEntry.sessionId).toBe(resumedSessionId);
1175+
expect(result.isNewSession).toBe(false);
1176+
vi.useRealTimers();
1177+
});
1178+
1179+
it("does not hijack short replies when the candidate belongs to a different TUI route", async () => {
1180+
const root = await makeCaseDir("openclaw-orphan-short-reply-other-route-");
1181+
const storePath = path.join(root, "sessions.json");
1182+
const sessionsDir = path.join(root, "sessions");
1183+
const resumedSessionId = "tui-session-other";
1184+
const resumedTranscript = path.join(sessionsDir, `${resumedSessionId}.jsonl`);
1185+
1186+
await writeTranscript({
1187+
transcriptPath: resumedTranscript,
1188+
sessionId: resumedSessionId,
1189+
userText: "Should we ship this now?",
1190+
assistantText: "Do you want me to continue with the current plan? Reply yes to continue.",
1191+
});
1192+
1193+
await writeSessionStoreFast(storePath, {
1194+
"agent:main:tui:main:tui-pane-2": {
1195+
sessionId: resumedSessionId,
1196+
sessionFile: resumedTranscript,
1197+
updatedAt: Date.now(),
1198+
lastChannel: "webchat",
1199+
lastTo: "session:tui:tui-pane-2",
1200+
},
1201+
});
1202+
1203+
const cfg = {
1204+
session: {
1205+
store: storePath,
1206+
},
1207+
} as OpenClawConfig;
1208+
1209+
const result = await initSessionState({
1210+
ctx: {
1211+
Body: "yes",
1212+
RawBody: "yes",
1213+
BodyForCommands: "yes",
1214+
SessionKey: "agent:main:fresh-confirmation",
1215+
Surface: "webchat",
1216+
Provider: "webchat",
1217+
OriginatingChannel: "webchat",
1218+
OriginatingTo: "session:tui:tui-pane-1",
1219+
SenderId: "gateway-client",
1220+
},
1221+
cfg,
1222+
commandAuthorized: true,
1223+
});
1224+
1225+
expect(result.sessionKey).toBe("agent:main:fresh-confirmation");
1226+
expect(result.isNewSession).toBe(true);
1227+
});
1228+
});
1229+
10791230
describe("initSessionState channel reset overrides", () => {
10801231
it("uses channel-specific reset policy when configured", async () => {
10811232
const root = await makeCaseDir("openclaw-channel-idle-");

0 commit comments

Comments
 (0)