Skip to content

Commit 111584e

Browse files
fix(providers): match standalone 'terminated' in transport retry regex (#267)
## Summary - The transport-level retry regex in `isTransportLevelError` required `fetch failed` before `terminated`, but pi-agent-core only captures `error.message` (not `error.cause`), so the `errorMessage` can be just `"terminated"` without the prefix - This caused the retry logic to be skipped, surfacing `CodesignError: terminated` directly to the user instead of retrying with conversation replay - Added `terminated` as a standalone match in the regex, and added tests at both the provider and agent layers ## Test plan - [x] `pnpm --filter @open-codesign/providers test` — all 37 tests pass (new: `marks standalone terminated as retryable`) - [x] `pnpm --filter @open-codesign/core test -- -t "transport-level retry"` — all 7 tests pass (new: `retries a standalone terminated error`) - [ ] Manual: trigger a generation with a proxy that drops SSE connections → should retry instead of showing "terminated" error 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com> Co-authored-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c0d7e9c commit 111584e

3 files changed

Lines changed: 36 additions & 1 deletion

File tree

‎packages/core/src/agent.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,32 @@ describe('generateViaAgent() — transport-level retry', () => {
12381238
expect(onRetry.mock.calls[0]?.[0].reason).toMatch(/transport retry/);
12391239
});
12401240

1241+
it('retries a standalone terminated error (without fetch failed prefix)', async () => {
1242+
scriptedAgent = {
1243+
assistantText: RESPONSE_WITH_ARTIFACT,
1244+
stopReason: 'error',
1245+
errorMessage: 'terminated',
1246+
overrideScriptForCallIndex: 1,
1247+
overrideScript: {
1248+
assistantText: RESPONSE_WITH_ARTIFACT,
1249+
stopReason: 'stop',
1250+
},
1251+
};
1252+
const onRetry = vi.fn();
1253+
const result = await generateViaAgent(
1254+
{
1255+
prompt: 'design a meditation app',
1256+
history: [],
1257+
model: MODEL,
1258+
apiKey: 'sk-test',
1259+
},
1260+
{ onRetry, fs: makeStubFs({ 'index.html': SAMPLE_HTML }) },
1261+
);
1262+
expect(result.artifacts).toHaveLength(1);
1263+
expect(agentCalls.length).toBe(2);
1264+
expect(onRetry).toHaveBeenCalledTimes(1);
1265+
});
1266+
12411267
it('does not retry non-transport errors like 400', async () => {
12421268
scriptedAgent = {
12431269
assistantText: '',

‎packages/providers/src/retry.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ describe('classifyError', () => {
7575
expect(d.retry).toBe(true);
7676
expect(d.reason).toMatch(/transport-level error/);
7777
});
78+
it('marks standalone terminated as retryable', () => {
79+
const d = classifyError(new Error('terminated'));
80+
expect(d.retry).toBe(true);
81+
expect(d.reason).toMatch(/transport-level error/);
82+
});
83+
it('does not treat unterminated as a terminated transport error', () => {
84+
const d = classifyError(new Error('unterminated JSON response'));
85+
expect(d.retry).toBe(false);
86+
});
7887
it('marks premature close as retryable', () => {
7988
const d = classifyError(new Error('premature close'));
8089
expect(d.retry).toBe(true);

‎packages/providers/src/retry.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function classifyByStatus(status: number, err: unknown, wire?: WireApi): RetryDe
8585
}
8686

8787
const TRANSPORT_ERROR_RE =
88-
/(?:fetch\s+failed.*terminated|premature\s+close|stream\s+(?:ended|closed)|ECONNRESET)\b/i;
88+
/(?:fetch\s+failed.*\bterminated\b|\bterminated\b|premature\s+close|stream\s+(?:ended|closed)|ECONNRESET)\b/i;
8989

9090
export function isTransportLevelError(errorMessage: string | undefined): boolean {
9191
if (!errorMessage) return false;

0 commit comments

Comments
 (0)