@@ -19,6 +19,7 @@ import { getCurrentBranch } from "@posthog/git/queries";
1919import {
2020 type Adapter ,
2121 buildPrOutput ,
22+ getErrorMessage ,
2223 mergePrUrls ,
2324 readPrUrls ,
2425} from "@posthog/shared" ;
@@ -40,6 +41,7 @@ import { hasCodexThreadState } from "../adapters/codex-app-server/thread-state";
4041import {
4142 type AgentErrorClassification ,
4243 classifyAgentError ,
44+ isPromptTooLongError ,
4345} from "../adapters/error-classification" ;
4446import {
4547 SIGNED_COMMIT_QUALIFIED_TOOL_NAME ,
@@ -249,6 +251,8 @@ interface ActiveSession {
249251 /** Whether a desktop client has ever connected via SSE during this session */
250252 hasDesktopConnected : boolean ;
251253 pendingHandoffGitState ?: HandoffLocalGitState ;
254+ /** Meta the session was created with, reused when a retry needs a fresh session */
255+ sessionMeta : Record < string , unknown > ;
252256}
253257
254258interface InstalledSkillBundle {
@@ -328,6 +332,7 @@ export class AgentServer {
328332 private lastReportedBranch : string | null = null ;
329333 private resumeState : ResumeState | null = null ;
330334 private nativeResume : { sessionId : string ; warm : boolean } | null = null ;
335+ private oversizedResumeRetried = false ;
331336 // Prewarmed runs boot before the user's first message exists, so the boot-time
332337 // --autoPublish flag can't carry the user's choice; it is resolved from run
333338 // state when the first message arrives (see resolveWarmAutoPublishUpgrade).
@@ -1409,6 +1414,7 @@ export class AgentServer {
14091414 permissionMode : initialPermissionMode ,
14101415 hasDesktopConnected : sseController !== null ,
14111416 pendingHandoffGitState : undefined ,
1417+ sessionMeta,
14121418 } ;
14131419
14141420 this . logger = new Logger ( {
@@ -1745,7 +1751,6 @@ export class AgentServer {
17451751 gitCheckpointBranch : resumeState . latestGitCheckpoint ?. branch ?? null ,
17461752 } ) ;
17471753
1748- this . resumeState = null ;
17491754 return {
17501755 prompt : resumePromptBlocks ,
17511756 ...( resumePromptMeta ? { meta : resumePromptMeta } : { } ) ,
@@ -1786,21 +1791,82 @@ export class AgentServer {
17861791 hasPendingUserMessage : ! ! pendingUserPrompt ?. prompt . length ,
17871792 } ) ;
17881793
1789- this . resumeState = null ;
1790- this . nativeResume = null ;
17911794 return {
17921795 prompt,
17931796 ...( pendingUserPrompt ?. meta ? { meta : pendingUserPrompt . meta } : { } ) ,
17941797 } ;
17951798 } ,
1799+ { retryOnOversizedPrompt : true } ,
17961800 ) ;
17971801 }
17981802
1803+ /**
1804+ * A native resume replays the prior transcript verbatim; when that
1805+ * transcript no longer fits the context window, every request (including
1806+ * auto-compaction) is rejected, so the only way forward is a fresh session
1807+ * seeded with the summarized history the non-native resume path uses.
1808+ */
1809+ private async retryOversizedResumeOnFreshSession (
1810+ payload : JwtPayload ,
1811+ taskRun : TaskRun | null ,
1812+ ) : Promise < boolean > {
1813+ if ( this . oversizedResumeRetried || ! this . session ) {
1814+ return false ;
1815+ }
1816+ this . oversizedResumeRetried = true ;
1817+
1818+ const resumeRunId = this . getResumeRunId ( taskRun ) ;
1819+ if ( ! resumeRunId ) return false ;
1820+ if ( ! this . resumeState ) {
1821+ try {
1822+ await this . loadResumeState (
1823+ payload . task_id ,
1824+ resumeRunId ,
1825+ payload . run_id ,
1826+ ) ;
1827+ } catch ( error ) {
1828+ this . logger . warn ( "Failed to reload resume state for retry" , {
1829+ error : getErrorMessage ( error ) ,
1830+ } ) ;
1831+ return false ;
1832+ }
1833+ }
1834+ if ( ! this . resumeState ?. conversation . length ) return false ;
1835+
1836+ this . logger . warn (
1837+ "Resume prompt exceeded the context window; retrying on a fresh session with summarized history" ,
1838+ { taskId : payload . task_id , runId : payload . run_id } ,
1839+ ) ;
1840+
1841+ try {
1842+ const response = await this . session . clientConnection . newSession ( {
1843+ cwd : this . config . repositoryPath ?? "/tmp/workspace" ,
1844+ mcpServers : this . config . mcpServers ?? [ ] ,
1845+ _meta : this . session . sessionMeta ,
1846+ } ) ;
1847+ this . session . acpSessionId = response . sessionId ;
1848+ } catch ( error ) {
1849+ this . logger . warn ( "Failed to start fresh session for oversized resume" , {
1850+ error : getErrorMessage ( error ) ,
1851+ } ) ;
1852+ return false ;
1853+ }
1854+
1855+ try {
1856+ await this . sendResumeMessage ( payload , taskRun ) ;
1857+ return true ;
1858+ } finally {
1859+ this . resumeState = null ;
1860+ this . nativeResume = null ;
1861+ }
1862+ }
1863+
17991864 private async runResumeTurn (
18001865 payload : JwtPayload ,
18011866 taskRun : TaskRun | null ,
18021867 logLabel : string ,
18031868 buildPrompt : ( ) => Promise < BuiltPrompt > ,
1869+ opts : { retryOnOversizedPrompt ?: boolean } = { } ,
18041870 ) : Promise < void > {
18051871 if ( ! this . session ) return ;
18061872
@@ -1819,6 +1885,10 @@ export class AgentServer {
18191885 stopReason : result . stopReason ,
18201886 } ) ;
18211887
1888+ // Kept until the turn succeeds so a prompt-too-long retry can reuse it.
1889+ this . resumeState = null ;
1890+ this . nativeResume = null ;
1891+
18221892 await this . clearPendingInitialPromptState ( payload , taskRun ) ;
18231893
18241894 if ( result . stopReason === "end_turn" ) {
@@ -1836,6 +1906,13 @@ export class AgentServer {
18361906 if ( this . session ) {
18371907 await this . session . logWriter . flushAll ( ) ;
18381908 }
1909+ if (
1910+ opts . retryOnOversizedPrompt &&
1911+ isPromptTooLongError ( error ) &&
1912+ ( await this . retryOversizedResumeOnFreshSession ( payload , taskRun ) )
1913+ ) {
1914+ return ;
1915+ }
18391916 await this . handleTurnFailure ( payload , "resume" , error ) ;
18401917 }
18411918 }
0 commit comments