From d27ae6262a5becb2d3f58379ca7ab4d64c435253 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:59:01 +0000 Subject: [PATCH 1/6] feat(server): recover limited Codex threads with opted-in reset credits Unattended threads currently wait for the natural allowance reset even when their account has banked reset credits. Add an off-by-default per-instance opt-in and redeem only under fresh account checks and shared manual/automatic locks. Keep one durable attempt per account so an eligible thread can adopt a lost response with the same key, failed pre-send claims can be released, and expired windows can recover. Persist provider-confirmed outcomes separately from fresh allowance proof, preserve the armed recovery request, and give later reports and natural continuations distinct receipt identities. Expose the preference and recovery outcome on web and mobile, restricted to one selected server. Include migration 077 and regressions for replay, Stop races, account locking, and both early and natural continuation. --- .../SettingsServerControlsRouteScreen.tsx | 1 + .../settings/ThreadSettingsSections.tsx | 47 +- .../threads/UsageLimitRecoveryCard.tsx | 31 +- .../src/orchestration-v2/Orchestrator.ts | 150 +++++- .../orchestration-v2/ProjectionStore.test.ts | 338 ++++++++++++ .../src/orchestration-v2/ProjectionStore.ts | 132 ++++- .../ProviderTurnControlService.test.ts | 4 + .../UsageLimitRecoveryWorker.test.ts | 477 ++++++++++++++++- .../UsageLimitRecoveryWorker.ts | 485 +++++++++++++++++- .../src/orchestration-v2/runtimeLayer.test.ts | 187 ++++++- .../src/orchestration-v2/runtimeLayer.ts | 2 +- apps/server/src/persistence/Migrations.ts | 2 + .../Migrations/077_CodexAutoResetAttempts.ts | 18 + apps/server/src/provider/CodexProvider.ts | 14 +- .../src/provider/Drivers/CodexDriver.ts | 281 ++++++++-- .../provider/Drivers/CodexManagedProvider.ts | 5 +- .../provider/ProviderInstanceRegistry.test.ts | 130 +++++ .../src/provider/ProviderRegistry.test.ts | 20 + apps/server/src/provider/ProviderRegistry.ts | 44 ++ .../src/provider/codexUsageLimits.test.ts | 87 ++++ apps/server/src/provider/codexUsageLimits.ts | 48 +- .../providerMaintenanceRunner.test.ts | 2 + .../provider/resetCreditCoordinator.test.ts | 188 +++++++ .../src/provider/resetCreditCoordinator.ts | 111 +++- .../testFixtures/codexCollabMockPeer.mjs | 50 +- .../testUtils/providerRegistryMock.ts | 2 + .../chat/UsageLimitRecoveryBanner.tsx | 37 +- .../components/settings/SettingsPanels.tsx | 42 +- docs/user/usage.md | 12 + .../contracts/src/orchestrationV2.test.ts | 13 + packages/contracts/src/orchestrationV2.ts | 33 ++ packages/contracts/src/settings.ts | 6 + packages/provider-core/src/server/driver.ts | 33 ++ 33 files changed, 2948 insertions(+), 84 deletions(-) create mode 100644 apps/server/src/persistence/Migrations/077_CodexAutoResetAttempts.ts diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx index 91b0b1b044e58..0dcf009c2e761 100644 --- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx @@ -136,6 +136,7 @@ const AUTO_SETTLE_SETTING_KEYS: ReadonlySet = new Set([ "sidebarAutoSettleAfterDays", "autoResumeLimitedThreads", "snoozeLimitedThreads", + "autoRedeemCodexResetCredits", ]); function planServerSettingsWrites( diff --git a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx index 2e77d4d3937e7..26670b9635ece 100644 --- a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx +++ b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx @@ -1,5 +1,10 @@ import { Button } from "../../components/ui/Button"; -import { AuthSettingsWriteScope, type ServerSettingsPatch } from "@t3tools/contracts"; +import { + AuthSettingsWriteScope, + ProviderDriverKind, + ProviderInstanceId, + type ServerSettingsPatch, +} from "@t3tools/contracts"; import { useEnvironmentsWithScope } from "../../state/session"; import { AutoSettleDaysField } from "./components/AutoSettleDaysField"; import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScrollView"; @@ -76,10 +81,24 @@ export function AutoSettleSettingsRows(props: { return null; } + const codexInstances = Object.entries(referenceSettings.providerInstances).filter( + ([, instance]) => { + if (instance.driver !== ProviderDriverKind.make("codex")) return false; + const config = instance.config; + return !( + typeof config === "object" && + config !== null && + "setupMode" in config && + config.setupMode === "managed" + ); + }, + ); + const writeToAll = ( patch: Partial & { autoResumeLimitedThreads?: boolean; snoozeLimitedThreads?: boolean; + autoRedeemCodexResetCredits?: ServerSettingsPatch["autoRedeemCodexResetCredits"]; }, ) => props.write(patch); @@ -118,6 +137,32 @@ export function AutoSettleSettingsRows(props: { disabled={disabled} onValueChange={(value) => writeToAll({ snoozeLimitedThreads: value })} /> + {displayTargets.length > 1 && codexInstances.length > 0 ? ( + + Select one server to change per-account reset settings. + + ) : displayTargets.length === 1 ? ( + codexInstances.map(([instanceId, instance]) => ( + + writeToAll({ + autoRedeemCodexResetCredits: { + [ProviderInstanceId.make(instanceId)]: value, + }, + }) + } + /> + )) + ) : null} ) : null} diff --git a/apps/mobile/src/features/threads/UsageLimitRecoveryCard.tsx b/apps/mobile/src/features/threads/UsageLimitRecoveryCard.tsx index 791dc2ab5d847..a9de2921b2f4c 100644 --- a/apps/mobile/src/features/threads/UsageLimitRecoveryCard.tsx +++ b/apps/mobile/src/features/threads/UsageLimitRecoveryCard.tsx @@ -2,7 +2,7 @@ import { Button } from "../../components/ui/Button"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { AsyncResult } from "effect/reactivity"; import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; -import type { EnvironmentId } from "@t3tools/contracts"; +import type { EnvironmentId, OrchestrationV2CodexResetRedemption } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import { useState } from "react"; import { View } from "react-native"; @@ -28,6 +28,10 @@ export function UsageLimitRecoveryCard({ const resetPassed = useDeadlinePassed(resetAt === null ? null : Date.parse(resetAt)); const runId = thread.latestRun?.runId; const recovery = thread.limitRecovery; + const redemption = + recovery?.runId === runId && recovery?.resetAt === resetAt + ? recovery?.codexResetRedemption + : undefined; const scheduled = recovery?.runId === runId && recovery?.resetAt === resetAt && recovery?.autoResume; if ( @@ -75,6 +79,11 @@ export function UsageLimitRecoveryCard({ ? `Usage limit resets ${DateTime.toDateUtc(DateTime.makeUnsafe(resetAt)).toLocaleString()}.` : "The provider did not report a reset time. Retry manually when your limit is available."} + {redemption ? ( + + {codexResetRedemptionMessage(redemption)} + + ) : null} {canSchedule ? (