mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(tui): preserve active conversations during session reset (#113841)
This commit is contained in:
committed by
GitHub
parent
a18c314377
commit
f2d2cc34bb
@@ -1069,6 +1069,45 @@ describe("tui command handlers", () => {
|
||||
expect(addSystem).toHaveBeenCalledWith("abort the current run before /new");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
activeChatRunId: "active-run",
|
||||
pendingSubmit: null,
|
||||
activityStatus: "running",
|
||||
},
|
||||
{
|
||||
activeChatRunId: null,
|
||||
pendingSubmit: {
|
||||
phase: "accepted" as const,
|
||||
runId: "pending-run",
|
||||
draftText: null,
|
||||
},
|
||||
activityStatus: "sending",
|
||||
},
|
||||
{
|
||||
activeChatRunId: null,
|
||||
pendingSubmit: {
|
||||
phase: "sending" as const,
|
||||
runId: "pending-run",
|
||||
draftText: "pending",
|
||||
},
|
||||
activityStatus: "sending",
|
||||
},
|
||||
{
|
||||
activeChatRunId: null,
|
||||
pendingSubmit: null,
|
||||
activityStatus: "finishing context",
|
||||
},
|
||||
])("blocks /reset while the current session lifecycle is unfinished", async (runState) => {
|
||||
const resetSession = vi.fn();
|
||||
const { handleCommand, addSystem } = createHarness({ resetSession, ...runState });
|
||||
|
||||
await handleCommand("/reset");
|
||||
|
||||
expect(resetSession).not.toHaveBeenCalled();
|
||||
expect(addSystem).toHaveBeenCalledWith("abort the current run before /reset");
|
||||
});
|
||||
|
||||
it("serializes input until /new adopts the created session", async () => {
|
||||
let resolveCreate: ((value: { ok: true; key: string }) => void) | undefined;
|
||||
const createSession = vi.fn().mockImplementation(
|
||||
|
||||
@@ -179,6 +179,17 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
const hasUnsafeSessionRollover = () =>
|
||||
hasTrackedAbortTarget() || state.activityStatus === "finishing context";
|
||||
|
||||
const rejectUnsafeSessionRollover = (command: "new" | "reset") => {
|
||||
if (!hasUnsafeSessionRollover()) {
|
||||
return false;
|
||||
}
|
||||
// Reset interrupts admitted Gateway work, so both rollover commands must
|
||||
// reject active, queued, and finishing runs before mutating the session.
|
||||
chatLog.addSystem(`abort the current run before /${command}`);
|
||||
tui.requestRender();
|
||||
return true;
|
||||
};
|
||||
|
||||
const currentSessionPatchTarget = () => ({
|
||||
key: state.currentSessionKey,
|
||||
...(state.currentSessionKey === "global" ? { agentId: state.currentAgentId } : {}),
|
||||
@@ -736,9 +747,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
break;
|
||||
}
|
||||
case "new":
|
||||
if (hasUnsafeSessionRollover()) {
|
||||
chatLog.addSystem("abort the current run before /new");
|
||||
tui.requestRender();
|
||||
if (rejectUnsafeSessionRollover("new")) {
|
||||
break;
|
||||
}
|
||||
sessionCreationInFlight = true;
|
||||
@@ -769,6 +778,9 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
}
|
||||
break;
|
||||
case "reset":
|
||||
if (rejectUnsafeSessionRollover("reset")) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
// Clear token counts immediately to avoid stale display (#1523)
|
||||
state.sessionInfo.inputTokens = null;
|
||||
|
||||
@@ -232,7 +232,11 @@ async function writeTuiPtyFixtureScript(dir: string) {
|
||||
return { runId };
|
||||
}
|
||||
const responseDelayMs =
|
||||
opts.message === "slow prompt" || opts.message === "streaming prompt" ? 500 : 20;
|
||||
opts.message === "slow prompt" ||
|
||||
opts.message === "slow reset proof" ||
|
||||
opts.message === "streaming prompt"
|
||||
? 500
|
||||
: 20;
|
||||
if (opts.message === "streaming prompt") {
|
||||
setTimeout(() => {
|
||||
this.onEvent?.({
|
||||
@@ -834,6 +838,30 @@ describe.sequential("TUI PTY harness", () => {
|
||||
TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it(
|
||||
"keeps an active session intact when /reset is submitted from the terminal",
|
||||
async () => {
|
||||
const priorResetCount = (await readFixtureLog(fixture.logPath)).filter(
|
||||
(entry) => entry.method === "resetSession",
|
||||
).length;
|
||||
|
||||
await fixture.run.write("slow reset proof\r");
|
||||
await fixture.waitForLogEntry(
|
||||
(entry) =>
|
||||
entry.method === "sendChat" && objectFieldEquals(entry, "message", "slow reset proof"),
|
||||
);
|
||||
await fixture.run.write("/reset\r", { delay: false });
|
||||
await fixture.run.waitForOutput("abort the current run before /reset");
|
||||
|
||||
const resetCalls = (await readFixtureLog(fixture.logPath)).filter(
|
||||
(entry) => entry.method === "resetSession",
|
||||
);
|
||||
expect(resetCalls).toHaveLength(priorResetCount);
|
||||
await fixture.run.waitForOutput("PTY_RESPONSE: slow reset proof");
|
||||
},
|
||||
TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it(
|
||||
"submits a follow-up prompt while a run is streaming",
|
||||
async () => {
|
||||
|
||||
@@ -67,6 +67,14 @@ const GATEWAY_SCENARIOS = {
|
||||
holdFirstResponse: true,
|
||||
followupReplyText: "FOLLOWUP_RUN_COMPLETE",
|
||||
},
|
||||
reset: {
|
||||
agentId: "tui-pty-reset",
|
||||
modelId: "tui-pty-reset",
|
||||
toolsProfile: "minimal",
|
||||
replyText: "FIRST_RUN_ACTIVE",
|
||||
holdFirstResponse: true,
|
||||
followupReplyText: "FOLLOWUP_RUN_COMPLETE",
|
||||
},
|
||||
emptyReply: {
|
||||
agentId: "tui-pty-empty-reply",
|
||||
modelId: "tui-pty-empty-reply",
|
||||
@@ -1128,6 +1136,50 @@ describe("TUI PTY real backends", () => {
|
||||
LOCAL_TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
registerGatewayTest(
|
||||
"preserves a running session when /reset is typed against the real Gateway",
|
||||
async ({ onTestFinished }) => {
|
||||
const fixture = await startGatewayModeTui("reset", onTestFinished);
|
||||
try {
|
||||
await fixture.run.waitForOutput("gateway connected", LOCAL_STARTUP_TIMEOUT_MS);
|
||||
await fixture.run.write("keep the active Gateway turn\r");
|
||||
await waitFor({
|
||||
timeoutMs: LOCAL_OUTPUT_TIMEOUT_MS,
|
||||
read: () => (fixture.mockModel.requests().length === 1 ? true : null),
|
||||
onTimeout: () =>
|
||||
new Error(`active Gateway turn did not reach the model\n${fixture.run.output()}`),
|
||||
});
|
||||
|
||||
await fixture.run.write("/reset\r", { delay: false });
|
||||
await fixture.run.waitForOutput("abort the current run before /reset", 5_000);
|
||||
expect(fixture.mockModel.requests()).toHaveLength(1);
|
||||
|
||||
fixture.mockModel.releaseFirstResponse();
|
||||
await fixture.run.waitForOutput("FIRST_RUN_ACTIVE");
|
||||
|
||||
await fixture.run.write("continue the preserved Gateway session\r");
|
||||
await waitFor({
|
||||
timeoutMs: LOCAL_OUTPUT_TIMEOUT_MS,
|
||||
read: () => (fixture.mockModel.requests().length === 2 ? true : null),
|
||||
onTimeout: () =>
|
||||
new Error(
|
||||
`preserved Gateway session did not accept its next turn\n${fixture.gateway.logs()}\n${fixture.run.output()}`,
|
||||
),
|
||||
});
|
||||
const preservedRequest = JSON.stringify(fixture.mockModel.requests()[1]?.body);
|
||||
expect(preservedRequest).toContain("keep the active Gateway turn");
|
||||
expect(preservedRequest).toContain("continue the preserved Gateway session");
|
||||
await fixture.run.waitForOutput("FOLLOWUP_RUN_COMPLETE");
|
||||
|
||||
await fixture.run.write("/exit\r", { delay: false });
|
||||
expect((await fixture.run.waitForExit()).exitCode).toBe(0);
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
},
|
||||
LOCAL_TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
registerGatewayTest(
|
||||
"forwards an active-run prompt through the real Gateway followup queue",
|
||||
async ({ onTestFinished }) => {
|
||||
|
||||
Reference in New Issue
Block a user