fix(core,web): retry transport and quota LLM errors, disable dead sessions on auth failure (#82)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-07-27 22:29:16 +08:00
1 parent c369e089a7
commit 3d3495528c
54 files changed
+2278 -161

No files matched your search

@@ -58,6 +58,7 @@ function approvalFakeSession(sessionId: string, toolName = "exec_command"): Runt
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
async *run(_input: OmniMessage[], opts: { approve: ApproveFn; signal: AbortSignal }) {
const tc = toolCall({ name: toolName, arguments: "{}", toolCallId: "tc-1" });
yield tc;
@@ -165,6 +166,7 @@ describe("session-manager", () => {
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
async *run(_input: OmniMessage[], opts: { thinkingLevel?: string }) {
seen.push(opts.thinkingLevel);
yield assistantText("ok");
@@ -192,6 +194,7 @@ describe("session-manager", () => {
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
async *run(): AsyncGenerator<OmniMessage> {
yield requestBegin();
yield toolCall({ name: "exec_command", arguments: "{}", toolCallId: "tc-1" });
@@ -335,6 +338,7 @@ describe("session-manager", () => {
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
// eslint-disable-next-line require-yield
async *run(input: OmniMessage[]): AsyncGenerator<OmniMessage> {
runInputs.push(input.map((m) => (m.payload as { text?: string }).text ?? ""));
@@ -417,6 +421,7 @@ describe("session-manager", () => {
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
async *run() {
// The parent-level run_subagent call (no origin): its prompt becomes the sub-session title.
yield toolCall({
@@ -493,6 +498,7 @@ describe("session-manager", () => {
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
async *run(_input, opts) {
const tc1 = toolCall({ name: "t1", arguments: "{}", toolCallId: "tc-1" });
yield tc1;
@@ -699,6 +705,41 @@ describe("session-manager", () => {
expect(loads).toBe(2);
});
it("invalidateProjectRuntimes: every cached entry in the Project is rebuilt; other Projects unaffected", async () => {
// Two Agents with cached runtimes in p1, one in p2 — a models/credential update to p1
// must reach BOTH of p1's Agents (collected from the active table) and leave p2 alone.
const rowA2: SessionRow = { ...ROW, sessionId: "session-2", agentId: "a2" };
const rowP2: SessionRow = { ...ROW, sessionId: "session-3", projectId: "p2" };
sessions.insert(rowA2);
sessions.insert(rowP2);
let loads = 0;
const loader: SessionLoader = {
load: async (row) => {
loads++;
return approvalFakeSession(row.sessionId);
},
};
const manager = makeManager(loader);
for (const sid of ["session-1", "session-2", "session-3"]) {
sessions.updateApprovalMode(sid, "allow-all");
}
manager.adopt(ROW, approvalFakeSession("session-1"));
manager.adopt(rowA2, approvalFakeSession("session-2"));
manager.adopt(rowP2, approvalFakeSession("session-3"));
manager.invalidateProjectRuntimes("p1");
// Both p1 entries are stale and rebuild through the loader on their next Task.
await manager.startTask("session-1", [userText("a")]);
await waitFor(() => manager.statusOf("session-1") === "idle");
await manager.startTask("session-2", [userText("b")]);
await waitFor(() => manager.statusOf("session-2") === "idle");
expect(loads).toBe(2);
// The p2 entry keeps its cached runtime.
await manager.startTask("session-3", [userText("c")]);
await waitFor(() => manager.statusOf("session-3") === "idle");
expect(loads).toBe(2);
});
it("sweepIdle: entries that are running / have pending approvals are not evicted", async () => {
const manager = makeManager(loaderOf(approvalFakeSession("session-1")));
await manager.startTask("session-1", [userText("go")]);
@@ -731,6 +772,7 @@ describe("session-manager", () => {
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
async *run() {
yield thinkingMessage("thinking");
yield assistantText("answer A");
@@ -785,6 +827,7 @@ describe("session-manager", () => {
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
async *run() {
yield assistantText("x".repeat(600));
// Sub-session text doesn't count toward the main-session body threshold
@@ -836,6 +879,7 @@ describe("session-manager", () => {
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
async *run() {
yield toolCall({
name: "run_subagent",