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:
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",
|
||||
|
||||
Reference in new issue
Block a user