mirror of
https://github.com/paperclipai/paperclip
synced 2026-04-25 17:25:15 +02:00
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Adapter execution and retry classification decide whether agent work pauses, retries, or recovers automatically > - Transient provider failures need to be classified precisely so Paperclip does not convert retryable upstream conditions into false hard failures > - At the same time, operators need an up-to-date model list for Codex-backed agents and prompts should nudge agents toward targeted verification instead of repo-wide sweeps > - This pull request tightens transient recovery classification for Claude and Codex, updates the agent prompt guidance, and adds Codex model refresh support end-to-end > - The benefit is better automatic retry behavior plus fresher operator-facing model configuration ## What Changed - added Codex usage-limit retry-window parsing and Claude extra-usage transient classification - normalized the heartbeat transient-recovery contract across adapter executions and heartbeat scheduling - documented that deferred comment wakes only reopen completed issues for human/comment-reopen interactions, while system follow-ups leave closed work closed - updated adapter-utils prompt guidance to prefer targeted verification - added Codex model refresh support in the server route, registry, shared types, and agent config form - added adapter/server tests covering the new parsing, retry scheduling, and model-refresh behavior ## Verification - `pnpm exec vitest run --project @paperclipai/adapter-utils packages/adapter-utils/src/server-utils.test.ts` - `pnpm exec vitest run --project @paperclipai/adapter-claude-local packages/adapters/claude-local/src/server/parse.test.ts` - `pnpm exec vitest run --project @paperclipai/adapter-codex-local packages/adapters/codex-local/src/server/parse.test.ts` - `pnpm exec vitest run --project @paperclipai/server server/src/__tests__/adapter-model-refresh-routes.test.ts server/src/__tests__/adapter-models.test.ts server/src/__tests__/claude-local-execute.test.ts server/src/__tests__/codex-local-execute.test.ts server/src/__tests__/heartbeat-process-recovery.test.ts server/src/__tests__/heartbeat-retry-scheduling.test.ts` ## Risks - Moderate behavior risk: retry classification affects whether runs auto-recover or block, so mistakes here could either suppress needed retries or over-retry real failures - Low workflow risk: deferred comment wake reopening is intentionally scoped to human/comment-reopen interactions so system follow-ups do not revive completed issues unexpectedly > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex GPT-5-based coding agent with tool use and code execution in the Codex CLI environment ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [ ] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
extractClaudeRetryNotBefore,
|
|
isClaudeTransientUpstreamError,
|
|
} from "./parse.js";
|
|
|
|
describe("isClaudeTransientUpstreamError", () => {
|
|
it("classifies the 'out of extra usage' subscription window failure as transient", () => {
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
errorMessage: "You're out of extra usage · resets 4pm (America/Chicago)",
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
parsed: {
|
|
is_error: true,
|
|
result: "You're out of extra usage. Resets at 4pm (America/Chicago).",
|
|
},
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("classifies Anthropic API rate_limit_error and overloaded_error as transient", () => {
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
parsed: {
|
|
is_error: true,
|
|
errors: [{ type: "rate_limit_error", message: "Rate limit reached for requests." }],
|
|
},
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
parsed: {
|
|
is_error: true,
|
|
errors: [{ type: "overloaded_error", message: "Overloaded" }],
|
|
},
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
stderr: "HTTP 429: Too Many Requests",
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
stderr: "Bedrock ThrottlingException: slow down",
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("classifies the subscription 5-hour / weekly limit wording", () => {
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
errorMessage: "Claude usage limit reached — weekly limit reached. Try again in 2 days.",
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
errorMessage: "5-hour limit reached.",
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not classify login/auth failures as transient", () => {
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
stderr: "Please log in. Run `claude login` first.",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("does not classify max-turns or unknown-session as transient", () => {
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
parsed: { subtype: "error_max_turns", result: "Maximum turns reached." },
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
parsed: {
|
|
result: "No conversation found with session id abc-123",
|
|
errors: [{ message: "No conversation found with session id abc-123" }],
|
|
},
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("does not classify deterministic validation errors as transient", () => {
|
|
expect(
|
|
isClaudeTransientUpstreamError({
|
|
errorMessage: "Invalid request_error: Unknown parameter 'foo'.",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("extractClaudeRetryNotBefore", () => {
|
|
it("parses the 'resets 4pm' hint in its explicit timezone", () => {
|
|
const now = new Date("2026-04-22T15:15:00.000Z");
|
|
const extracted = extractClaudeRetryNotBefore(
|
|
{ errorMessage: "You're out of extra usage · resets 4pm (America/Chicago)" },
|
|
now,
|
|
);
|
|
expect(extracted?.toISOString()).toBe("2026-04-22T21:00:00.000Z");
|
|
});
|
|
|
|
it("rolls forward past midnight when the reset time has already passed today", () => {
|
|
const now = new Date("2026-04-22T23:30:00.000Z");
|
|
const extracted = extractClaudeRetryNotBefore(
|
|
{ errorMessage: "Usage limit reached. Resets at 3:15 AM (UTC)." },
|
|
now,
|
|
);
|
|
expect(extracted?.toISOString()).toBe("2026-04-23T03:15:00.000Z");
|
|
});
|
|
|
|
it("returns null when no reset hint is present", () => {
|
|
expect(
|
|
extractClaudeRetryNotBefore({ errorMessage: "Overloaded. Try again later." }, new Date()),
|
|
).toBeNull();
|
|
});
|
|
});
|