mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(desktop): a status read lost to the network is asked again, so a pane nobody is looking at stops reading as running
This commit is contained in:
1 parent
022b82d2d5
commit
7b726d70fa
5 files changed
+132
-1
No files matched your search
@@ -95,6 +95,14 @@ try {
|
||||
const replies = (text.match(new RegExp(`word${WORDS - 1}`, "g")) ?? []).length;
|
||||
check("the open pane shows every reply it was sent while the network flapped", replies >= TURNS + 1, `${replies} of ${TURNS + 1} replies`);
|
||||
check("the open pane returns to idle", done);
|
||||
|
||||
// The sidebar counts what is running from the page's own state of every pane,
|
||||
// open or not; the Studio, the shared stream's metrics and the tree already agree
|
||||
// that nothing runs, so a count that stays up is the page not recovering.
|
||||
const live = () => page.evaluate(() => document.querySelector('.studio-session-segments button[data-value="live"] b')?.textContent?.trim() ?? "0");
|
||||
const quiet = await until("the sidebar's running count to reach zero", async () => (await live()) === "0", 45_000).then(() => true, () => false);
|
||||
const stuck = quiet ? "" : await page.evaluate(() => [...document.querySelectorAll(".sessrow[data-run]")].map((r) => `${r.textContent.trim().slice(0, 18)}:${r.getAttribute("data-run")}`).join(" | "));
|
||||
check("the sidebar's running count settles to zero once every turn has finished", quiet, quiet ? "" : `reads ${await live()}; rows ${stuck}`);
|
||||
if (process.env.RELAY_SHOT) {
|
||||
await page.mouse.move(700, 700);
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
@@ -61,6 +61,7 @@ import { SidePanel } from "./SidePanel";
|
||||
import { cancelJob } from "./jobcancel";
|
||||
import { ConnectNote } from "./ConnectNote";
|
||||
import { useStatusPoll } from "./useStatusPoll";
|
||||
import { useStatusRefresh } from "./useStatusRefresh";
|
||||
import { useCheckpointRefresh } from "./useCheckpointRefresh";
|
||||
import { LiveWork, useLiveWork } from "../state/foldpref";
|
||||
import { loadChangedFilesPref, useShowsChangedFiles } from "../state/changedfiles";
|
||||
@@ -180,7 +181,7 @@ function PaneView({ port, hub, rt, title, active, visible, sideHost, side, onFoc
|
||||
}, []);
|
||||
|
||||
const readJob = useCallback((id: string) => port.jobOutput(id), [port]);
|
||||
const refreshStatus = useCallback(() => port.status().then((st) => { applyStatus(st); statusRead(); }).catch(() => {}), [port, applyStatus, statusRead]);
|
||||
const refreshStatus = useStatusRefresh(port, applyStatus, statusRead);
|
||||
|
||||
const [wallet, refreshWallet] = useWallet(port, accountOf(status?.modelRef));
|
||||
const hideAmounts = useHidesAmounts();
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { act, cleanup, render, waitFor } from "@testing-library/react";
|
||||
import "./testkit";
|
||||
import { MockPort } from "../port/mock";
|
||||
import type { WireEvent } from "../port/wire";
|
||||
import { Pane, type PaneReport } from "./Pane";
|
||||
|
||||
afterEach(() => { cleanup(); vi.useRealTimers(); });
|
||||
|
||||
// A pane the person is not looking at. Its state of running comes from two
|
||||
// reads of the kernel — the frames of its stream, and the status snapshot a turn
|
||||
// boundary asks for — and when the network drops under the second one the pane
|
||||
// has no other way to hear that the turn ended.
|
||||
async function mount() {
|
||||
const port = new MockPort();
|
||||
const snapshot = await port.status();
|
||||
const kernel = { running: false, failing: 0, reads: 0 };
|
||||
vi.spyOn(port, "status").mockImplementation(async () => {
|
||||
kernel.reads++;
|
||||
if (kernel.failing > 0) {
|
||||
kernel.failing--;
|
||||
throw new TypeError("network down");
|
||||
}
|
||||
return { ...snapshot, running: kernel.running };
|
||||
});
|
||||
let deliver: ((event: WireEvent) => void) | undefined;
|
||||
vi.spyOn(port, "subscribe").mockImplementation((onEvent) => {
|
||||
deliver = onEvent;
|
||||
return () => { deliver = undefined; return true; };
|
||||
});
|
||||
const onReport = vi.fn<(id: string, report: PaneReport) => void>();
|
||||
const view = render(
|
||||
<Pane port={port} rt={{ id: "r1", base: "/rt/r1", root: "/workspace", name: "workspace", sessionPath: "/sessions/a.jsonl" }} title="a" active={false}
|
||||
visible={false} sideHost={null} side={false} onFocus={() => {}} onReport={onReport} onSessionChanged={() => {}} pulse={0} findPulse={0} onSettings={() => {}}
|
||||
needsProject={false} onOpenProject={() => {}} onKeepHere={() => {}} theme="light" dockW={320} dockMax={640} onDockW={() => {}} />,
|
||||
);
|
||||
const send = (kind: WireEvent["kind"]) => act(() => deliver?.({ kind }));
|
||||
const lastLive = () => onReport.mock.calls.at(-1)?.[1].live;
|
||||
return { kernel, send, onReport, lastLive, view };
|
||||
}
|
||||
|
||||
it("hears that a turn ended even when the read that confirms it fails", async () => {
|
||||
const pane = await mount();
|
||||
pane.kernel.running = true;
|
||||
await pane.send("turn_started");
|
||||
await waitFor(() => expect(pane.lastLive()).toBe(true));
|
||||
pane.kernel.running = false;
|
||||
pane.kernel.failing = 1;
|
||||
await pane.send("turn_done");
|
||||
await waitFor(() => expect(pane.lastLive()).toBe(false), { timeout: 4000 });
|
||||
});
|
||||
|
||||
it("keeps asking while the network stays down, and not in a tight loop", async () => {
|
||||
const pane = await mount();
|
||||
pane.kernel.running = true;
|
||||
await pane.send("turn_started");
|
||||
await waitFor(() => expect(pane.lastLive()).toBe(true));
|
||||
pane.kernel.running = false;
|
||||
pane.kernel.failing = 3;
|
||||
const before = pane.kernel.reads;
|
||||
await pane.send("turn_done");
|
||||
await waitFor(() => expect(pane.lastLive()).toBe(false), { timeout: 9000 });
|
||||
const asked = pane.kernel.reads - before;
|
||||
expect(asked).toBeGreaterThanOrEqual(4);
|
||||
expect(asked).toBeLessThan(8);
|
||||
}, 15000);
|
||||
|
||||
it("stops asking when the pane goes away", async () => {
|
||||
const pane = await mount();
|
||||
pane.kernel.running = true;
|
||||
await pane.send("turn_started");
|
||||
await waitFor(() => expect(pane.lastLive()).toBe(true));
|
||||
pane.kernel.failing = 100;
|
||||
await pane.send("turn_done");
|
||||
pane.view.unmount();
|
||||
const at = pane.kernel.reads;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2500));
|
||||
expect(pane.kernel.reads).toBe(at);
|
||||
}, 10000);
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import type { AgentPort, SessionStatus } from "../port/port";
|
||||
|
||||
const FIRST_MS = 1000;
|
||||
const CAP_MS = 15_000;
|
||||
|
||||
/** One way to ask the kernel for a pane's status that does not give up. A turn
|
||||
* boundary is the moment the snapshot is asked for, and the snapshot is what
|
||||
* closes or vouches for a run, so a read lost to the network leaves the pane
|
||||
* believing what it last heard — for a pane nobody is looking at, nothing else
|
||||
* asks again. A failed read is therefore asked again with growing gaps until it
|
||||
* answers, and stops when the pane goes away. */
|
||||
export function useStatusRefresh(port: AgentPort, adopt: (status: SessionStatus) => void, settled: () => void) {
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const tries = useRef(0);
|
||||
const alive = useRef(true);
|
||||
useEffect(() => {
|
||||
alive.current = true;
|
||||
return () => {
|
||||
alive.current = false;
|
||||
clearTimeout(timer.current);
|
||||
tries.current = 0;
|
||||
};
|
||||
}, [port]);
|
||||
const refresh = useCallback((): Promise<void> => port.status().then(
|
||||
(status) => {
|
||||
clearTimeout(timer.current);
|
||||
tries.current = 0;
|
||||
adopt(status);
|
||||
settled();
|
||||
},
|
||||
() => {
|
||||
if (!alive.current) return;
|
||||
clearTimeout(timer.current);
|
||||
const gap = Math.min(CAP_MS, FIRST_MS * 2 ** tries.current++);
|
||||
timer.current = setTimeout(() => void refresh(), gap);
|
||||
},
|
||||
), [port, adopt, settled]);
|
||||
return refresh;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
- 修复:断网恢复后,侧栏“进行中”计数不再卡在已经结束的会话上(没打开的窗格也会在网络恢复后重新读到状态)。关联 #13604。
|
||||
- Fixed: after a network flap the sidebar's running count no longer sticks on conversations that finished; a pane you have not opened reads its status again once the network is back. Refs #13604.
|
||||
Reference in new issue
Block a user