Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3147,6 +3147,10 @@ function PersonalGoalHome({
if (turnId) {
activeTurnIds.current.set(run.goalId, turnId);
const controller = new AbortController();
const observationKey = `${run.sessionId}:${turnId}`;
// History recovery must adopt this correction's observer when
// the owner returns to Chat, just as it adopts composer Turns.
turnObservers.current.set(observationKey, controller);
streamControllers.current.set(run.goalId, controller);
let streamedText = "";
const messageId = appendManagerAssistantMessage(run.goalId, {
Expand Down Expand Up @@ -3181,14 +3185,18 @@ function PersonalGoalHome({
text: interrupted ? [streamedText.trim(), "已中断。你可以在当前会话继续发送消息。"].filter(Boolean).join("\n\n") : error instanceof Error ? error.message : "纠偏回合失败。",
});
} finally {
activeTurnIds.current.delete(run.goalId);
streamControllers.current.delete(run.goalId);
recordRuntimeBinding(run.goalId, {
agentId: run.agentId,
resumable: true,
sessionId: run.sessionId,
status: "ready",
});
if (turnObservers.current.get(observationKey) === controller) turnObservers.current.delete(observationKey);
const ownsStream = streamControllers.current.get(run.goalId) === controller;
if (ownsStream) streamControllers.current.delete(run.goalId);
if (ownsStream && activeTurnIds.current.get(run.goalId) === turnId) {
activeTurnIds.current.delete(run.goalId);
recordRuntimeBinding(run.goalId, {
agentId: run.agentId,
resumable: true,
sessionId: run.sessionId,
status: "ready",
});
}
}
}
},
Expand Down
28 changes: 26 additions & 2 deletions examples/personal-workspace-browser/typed-actions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2107,19 +2107,40 @@ export const typedActionsScenario = {
await currentRuntimeRun.click();
await page.getByRole("tab", { name: "详情与操作" }).click();
const interruptionQuestion = "保持运行,等我检查中断控制。";
let correctionStreamRequests = 0;
// This assertion needs a running correction, independent of how long
// drawer navigation takes. Hold only its exact Session/Turn stream;
// completion races remain covered by conversation-activity.
await page.route("**/api/chat/sessions/*/turns/*/events", async (route) => {
const path = new URL(route.request().url()).pathname.split("/");
const correction = api.turnRequests.find((turn) => turn.message === interruptionQuestion
&& turn.sessionId === path[4] && turn.turnId === path[6]);
if (correction) await interruptedCorrectionStream.promise;
if (correction) {
// Terminal proposal readback uses this endpoint too; only a live
// Turn subscription can compete for its running row and controls.
if (page.__loopxRuntime.sessions.get(correction.sessionId)?.active_turn_id === correction.turnId) {
correctionStreamRequests += 1;
}
await interruptedCorrectionStream.promise;
}
return route.fallback();
});
const runningCorrection = page.getByLabel("输入纠偏信息");
await runningCorrection.fill(interruptionQuestion);
await page.getByRole("button", { name: "发送纠偏" }).click();
await Promise.all([
page.waitForRequest(request => {
const path = new URL(request.url()).pathname.split("/");
return path[7] === "events" && api.turnRequests.some(turn => turn.message === interruptionQuestion
&& turn.sessionId === path[4] && turn.turnId === path[6]);
}, { timeout: 8_000 }),
page.getByRole("button", { name: "发送纠偏" }).click(),
]);
await page.getByRole("button", { name: /关闭详情/ }).click();
await goalNavigation.getByRole("button", { name: /^(Overview|概览)$/ }).click();
await goalNavigation.getByRole("button", { name: /^(Chat|对话)$/ }).click();
if (!await currentRuntimeRun.isVisible()) await page.locator(".personal-activity-summary > summary").click();
await currentRuntimeRun.click();
await page.getByRole("tab", { name: "详情与操作" }).click();
await page.getByText("更多运行操作").click();
const interruptButton = page.getByRole("button", { name: "中断本次运行" });
try {
Expand All @@ -2146,6 +2167,9 @@ export const typedActionsScenario = {
}
await page.getByRole("button", { name: /关闭详情/ }).click();
await page.locator('[data-goal-panel="chat"]').getByText("已中断。你可以在当前会话继续发送消息。", { exact: true }).waitFor({ state: "visible", timeout: 10_000 });
assert.equal(correctionStreamRequests, 1, "Returning to a running correction reuses its original observer");
assert.equal(await page.locator('[data-goal-panel="chat"]').getByRole("button", { name: "中断本轮", exact: true }).count(),
0, "An interrupted correction leaves no pending duplicate in the conversation");

if (!await currentRuntimeRun.isVisible()) await page.locator(".personal-activity-summary > summary").click();
await currentRuntimeRun.click();
Expand Down
Loading