Skip to content

Commit 8c9e518

Browse files
committed
fix(acp): narrow persistent-session resume matcher to SessionResumeRequiredError
1 parent 9984a0d commit 8c9e518

2 files changed

Lines changed: 91 additions & 1 deletion

File tree

src/acp/control-plane/manager.runtime-resume-state.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ function isRecoverableMissingManagerPersistentSessionError(message: string): boo
2121
const normalized = message.trim();
2222
return (
2323
/persistent acp session .* could not be resumed/i.test(normalized) &&
24-
/(resource not found|no matching session|internal error)/i.test(normalized)
24+
(
25+
/(resource not found|no matching session)/i.test(normalized) ||
26+
/(session resume required|SessionResumeRequiredError)/i.test(normalized)
27+
)
2528
);
2629
}
2730

src/acp/control-plane/manager.turn-results.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,93 @@ describe("AcpSessionManager turn results", () => {
10591059
expect(states).not.toContain("error");
10601060
});
10611061

1062+
it("does not retry after a persistent resume wrapper with generic Internal error without SessionResumeRequiredError", async () => {
1063+
const runtimeState = createRuntime();
1064+
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
1065+
id: "acpx",
1066+
runtime: runtimeState.runtime,
1067+
});
1068+
const sessionKey =
1069+
"agent:kiro:acp:binding:discord:default:no-retry-generic-internal";
1070+
let currentMeta: SessionAcpMeta = {
1071+
...readySessionMeta({
1072+
agent: "kiro",
1073+
}),
1074+
runtimeSessionName: sessionKey,
1075+
identity: {
1076+
state: "resolved",
1077+
source: "status",
1078+
acpxSessionId: "acpx-sid-stale",
1079+
lastUpdatedAt: Date.now(),
1080+
},
1081+
};
1082+
hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => {
1083+
const key = (paramsUnknown as { sessionKey?: string }).sessionKey ?? sessionKey;
1084+
return {
1085+
sessionKey: key,
1086+
storeSessionKey: key,
1087+
acp: currentMeta,
1088+
};
1089+
});
1090+
hoisted.upsertAcpSessionMetaMock.mockImplementation(async (paramsUnknown: unknown) => {
1091+
const params = paramsUnknown as {
1092+
mutate: (
1093+
current: SessionAcpMeta | undefined,
1094+
entry: { acp?: SessionAcpMeta } | undefined,
1095+
) => SessionAcpMeta | null | undefined;
1096+
};
1097+
const next = params.mutate(currentMeta, { acp: currentMeta });
1098+
if (next) {
1099+
currentMeta = next;
1100+
}
1101+
return {
1102+
sessionId: "session-kiro-1",
1103+
updatedAt: Date.now(),
1104+
acp: currentMeta,
1105+
};
1106+
});
1107+
runtimeState.ensureSession.mockImplementation(async (inputUnknown: unknown) => {
1108+
const input = inputUnknown as {
1109+
sessionKey: string;
1110+
mode: "persistent" | "oneshot";
1111+
resumeSessionId?: string;
1112+
};
1113+
return {
1114+
sessionKey: input.sessionKey,
1115+
backend: "acpx",
1116+
runtimeSessionName: `${input.sessionKey}:${input.mode}:runtime`,
1117+
backendSessionId: input.resumeSessionId ? "acpx-sid-stale" : "acpx-sid-fresh",
1118+
};
1119+
});
1120+
runtimeState.getStatus.mockResolvedValue({
1121+
summary: "status=alive",
1122+
backendSessionId: "acpx-sid-fresh",
1123+
details: { status: "alive" },
1124+
});
1125+
runtimeState.runTurn.mockImplementationOnce(async function* () {
1126+
yield {
1127+
type: "error" as const,
1128+
code: "NO_SESSION",
1129+
message:
1130+
"Persistent ACP session acpx-sid-stale could not be resumed: Internal error",
1131+
};
1132+
});
1133+
1134+
const manager = new AcpSessionManager();
1135+
const turnPromise = manager.runTurn({
1136+
cfg: baseCfg,
1137+
sessionKey,
1138+
text: "do kiro work",
1139+
mode: "prompt",
1140+
requestId: "run-kiro-generic-internal",
1141+
});
1142+
await expect(turnPromise).rejects.toMatchObject({
1143+
code: "ACP_TURN_FAILED",
1144+
});
1145+
1146+
expect(runtimeState.prepareFreshSession).not.toHaveBeenCalled();
1147+
});
1148+
10621149
it("retries once with a fresh persistent session after a kiro-style SessionResumeRequiredError with Internal error", async () => {
10631150
const runtimeState = createRuntime();
10641151
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({

0 commit comments

Comments
 (0)