[codex] Improve transient recovery and Codex model refresh (#4383)

## 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>
This commit is contained in:
Dotta 2026-04-24 09:40:40 -05:00 committed by GitHub
parent 4fdbbeced3
commit 8f1cd0474f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1455 additions and 48 deletions

View file

@ -70,14 +70,15 @@ async function fetchOpenAiModels(apiKey: string): Promise<AdapterModel[]> {
}
}
export async function listCodexModels(): Promise<AdapterModel[]> {
async function loadCodexModels(options?: { forceRefresh?: boolean }): Promise<AdapterModel[]> {
const forceRefresh = options?.forceRefresh === true;
const apiKey = resolveOpenAiApiKey();
const fallback = dedupeModels(codexFallbackModels);
if (!apiKey) return fallback;
const now = Date.now();
const keyFingerprint = fingerprint(apiKey);
if (cached && cached.keyFingerprint === keyFingerprint && cached.expiresAt > now) {
if (!forceRefresh && cached && cached.keyFingerprint === keyFingerprint && cached.expiresAt > now) {
return cached.models;
}
@ -99,6 +100,14 @@ export async function listCodexModels(): Promise<AdapterModel[]> {
return fallback;
}
export async function listCodexModels(): Promise<AdapterModel[]> {
return loadCodexModels();
}
export async function refreshCodexModels(): Promise<AdapterModel[]> {
return loadCodexModels({ forceRefresh: true });
}
export function resetCodexModelsCacheForTests() {
cached = null;
}

View file

@ -1,6 +1,7 @@
export {
getServerAdapter,
listAdapterModels,
refreshAdapterModels,
listServerAdapters,
findServerAdapter,
findActiveServerAdapter,

View file

@ -55,7 +55,7 @@ import {
agentConfigurationDoc as openclawGatewayAgentConfigurationDoc,
models as openclawGatewayModels,
} from "@paperclipai/adapter-openclaw-gateway";
import { listCodexModels } from "./codex-models.js";
import { listCodexModels, refreshCodexModels } from "./codex-models.js";
import { listCursorModels } from "./cursor-models.js";
import {
execute as piExecute,
@ -145,6 +145,7 @@ const codexLocalAdapter: ServerAdapterModule = {
sessionManagement: getAdapterSessionManagement("codex_local") ?? undefined,
models: codexModels,
listModels: listCodexModels,
refreshModels: refreshCodexModels,
supportsLocalAgentJwt: true,
supportsInstructionsBundle: true,
instructionsPathKey: "instructionsFilePath",
@ -459,6 +460,20 @@ export async function listAdapterModels(type: string): Promise<{ id: string; lab
return adapter.models ?? [];
}
export async function refreshAdapterModels(type: string): Promise<{ id: string; label: string }[]> {
const adapter = findActiveServerAdapter(type);
if (!adapter) return [];
if (adapter.refreshModels) {
const refreshed = await adapter.refreshModels();
if (refreshed.length > 0) return refreshed;
}
if (adapter.listModels) {
const discovered = await adapter.listModels();
if (discovered.length > 0) return discovered;
}
return adapter.models ?? [];
}
export function listServerAdapters(): ServerAdapterModule[] {
return Array.from(adaptersByType.values());
}