-
Notifications
You must be signed in to change notification settings - Fork 91
fix(coding-agent): recover oversized session resumes #1263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1424e67
b5d6c84
0b3be0a
83a032d
71e65e0
4befc1a
6dc052a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1154,6 +1154,8 @@ export class AgentSession { | |
| private _currentServiceTier: ServiceTier | undefined = undefined; | ||
| private _sessionFastMode = false; | ||
| private readonly _shownHighReasoningWarningKeys = new Set<string>(); | ||
| /** Buffers public events until transactional model admission commits. */ | ||
| private _deferredSessionEvents?: AgentSessionEvent[]; | ||
| // Widened with the upstream BuildSystemPromptOptions user-override fields so | ||
| // extensions (prompt-preset) can see CLI/SDK custom prompts via | ||
| // before_agent_start/model_select systemPromptOptions and ctx.getSystemPromptOptions(). | ||
|
|
@@ -1613,6 +1615,10 @@ export class AgentSession { | |
| } | ||
|
|
||
| private _emit(event: AgentSessionEvent): void { | ||
| if (this._deferredSessionEvents) { | ||
| this._deferredSessionEvents.push(event); | ||
| return; | ||
| } | ||
| this._logSessionEvent(event); | ||
| for (const l of this._eventListeners) { | ||
| l(event); | ||
|
|
@@ -4493,6 +4499,8 @@ export class AgentSession { | |
| nextModel: Model<any>, | ||
| previousModel: Model<any> | undefined, | ||
| source: ModelSelectSource, | ||
| publish = true, | ||
| provisional = false, | ||
| ): Promise<SystemPromptChangeEvent | undefined> { | ||
| this.syncPromptCacheSafeWaitEnv(); | ||
| if (!this._modelSelectionChangesContext(previousModel, nextModel)) return undefined; | ||
|
|
@@ -4501,6 +4509,7 @@ export class AgentSession { | |
| model: nextModel, | ||
| previousModel, | ||
| source, | ||
| ...(provisional ? { provisional: true } : {}), | ||
| systemPrompt: this.agent.state.systemPrompt, | ||
| systemPromptOptions: this._baseSystemPromptOptions, | ||
| }); | ||
|
|
@@ -4526,9 +4535,13 @@ export class AgentSession { | |
| if (result.systemPromptName) { | ||
| event.systemPromptName = result.systemPromptName; | ||
| } | ||
| if (publish) await this._publishSystemPromptChange(event); | ||
| return event; | ||
| } | ||
|
|
||
| private async _publishSystemPromptChange(event: SystemPromptChangeEvent): Promise<void> { | ||
| await this._extensionRunner.emit(event); | ||
| this._emit(event); | ||
| return event; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -4583,6 +4596,39 @@ export class AgentSession { | |
| return this._setModel(model, false); | ||
| } | ||
|
|
||
| /** | ||
| * Recover an existing session whose restored model cannot carry its live | ||
| * context. Startup owns candidate selection and full-budget admission; this | ||
| * seam records the recovered model without changing global defaults or | ||
| * treating the restore as a manual fallback reset. | ||
| * @internal | ||
| */ | ||
| async setStartupRecoveryModel( | ||
| model: Model<Api>, | ||
| liveContextTokens: number, | ||
| ): Promise<SystemPromptChangeEvent | undefined> { | ||
| this.assertModelUsable(model, liveContextTokens); | ||
| const previousModel = this.model; | ||
| const systemPromptChange = await this._switchActiveModel(model, { | ||
| persistDefault: false, | ||
| appendSessionEntry: false, | ||
| emitModelSelect: true, | ||
|
Comment on lines
+4612
to
+4615
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the recovery candidate supports different thinking levels and a Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 0b3be0a: startup recovery now uses the target model’s normal thinking resolution but applies it ephemerally ( |
||
| modelSelectSource: "restore", | ||
| invalidateCompaction: true, | ||
| persistThinkingLevel: false, | ||
| liveContextTokens, | ||
| transactionalModelSelect: true, | ||
| }); | ||
| this.sessionManager.appendModelChange( | ||
| model.provider, | ||
| model.id, | ||
| undefined, | ||
| previousModel?.provider, | ||
| previousModel?.id, | ||
| ); | ||
| return systemPromptChange; | ||
| } | ||
|
|
||
| private async _setModel( | ||
| model: Model<Api>, | ||
| updateGlobalDefaults: boolean, | ||
|
|
@@ -4640,9 +4686,78 @@ export class AgentSession { | |
| modelSelectSource: ModelSelectSource; | ||
| invalidateCompaction: boolean; | ||
| ephemeralThinkingLevel?: ThinkingLevel; | ||
| persistThinkingLevel?: boolean; | ||
| liveContextTokens?: number; | ||
| transactionalModelSelect?: boolean; | ||
| }, | ||
| ): Promise<SystemPromptChangeEvent | undefined> { | ||
| const previousModel = this.model; | ||
| const snapshot = opts.transactionalModelSelect | ||
| ? { | ||
| model: this.agent.state.model, | ||
| systemPrompt: this.agent.state.systemPrompt, | ||
| tools: this.agent.state.tools, | ||
| thinkingLevel: this.agent.state.thinkingLevel, | ||
| thinkingSelection: this.agent.state.thinkingSelection, | ||
| abortServerSideFallback: this.agent.abortServerSideFallback, | ||
| currentServiceTier: this._currentServiceTier, | ||
| sessionFastMode: this._sessionFastMode, | ||
| baseSystemPrompt: this._baseSystemPrompt, | ||
| baseSystemPromptOptions: { ...this._baseSystemPromptOptions }, | ||
| systemPromptOverride: this._systemPromptOverride, | ||
| requestedActiveToolNames: this._requestedActiveToolNames | ||
| ? [...this._requestedActiveToolNames] | ||
| : undefined, | ||
| withheldEvalOnlyToolNames: new Set(this._withheldEvalOnlyToolNames), | ||
| publishedEvalOnlyHintNames: new Set(this._publishedEvalOnlyHintNames), | ||
| removedToolHints: { ...this.agent.removedToolHints }, | ||
| toolRegistry: new Map(this._toolRegistry), | ||
| toolDefinitions: new Map(this._toolDefinitions), | ||
| toolPromptSnippets: new Map(this._toolPromptSnippets), | ||
| toolPromptGuidelines: new Map(this._toolPromptGuidelines), | ||
| shownHighReasoningWarningKeys: new Set(this._shownHighReasoningWarningKeys), | ||
| } | ||
| : undefined; | ||
| const ownsDeferredEvents = opts.transactionalModelSelect && this._deferredSessionEvents === undefined; | ||
| if (ownsDeferredEvents) this._deferredSessionEvents = []; | ||
| const restoreSnapshot = () => { | ||
| if (!snapshot) return; | ||
| this.agent.state.model = snapshot.model; | ||
| this.agent.state.systemPrompt = snapshot.systemPrompt; | ||
| this.agent.state.tools = snapshot.tools; | ||
| this.agent.state.thinkingLevel = snapshot.thinkingLevel; | ||
| this.agent.state.thinkingSelection = snapshot.thinkingSelection; | ||
| this.agent.abortServerSideFallback = snapshot.abortServerSideFallback; | ||
| this._currentServiceTier = snapshot.currentServiceTier; | ||
| this._sessionFastMode = snapshot.sessionFastMode; | ||
| this._baseSystemPrompt = snapshot.baseSystemPrompt; | ||
| this._baseSystemPromptOptions = snapshot.baseSystemPromptOptions; | ||
| this._systemPromptOverride = snapshot.systemPromptOverride; | ||
| this._requestedActiveToolNames = snapshot.requestedActiveToolNames; | ||
| this._withheldEvalOnlyToolNames.clear(); | ||
| for (const toolName of snapshot.withheldEvalOnlyToolNames) { | ||
| this._withheldEvalOnlyToolNames.add(toolName); | ||
| } | ||
| this._publishedEvalOnlyHintNames.clear(); | ||
| for (const toolName of snapshot.publishedEvalOnlyHintNames) { | ||
| this._publishedEvalOnlyHintNames.add(toolName); | ||
| } | ||
| this.agent.removedToolHints = snapshot.removedToolHints; | ||
| this._toolRegistry = snapshot.toolRegistry; | ||
| this._toolDefinitions = snapshot.toolDefinitions; | ||
| this._toolPromptSnippets = snapshot.toolPromptSnippets; | ||
| this._toolPromptGuidelines = snapshot.toolPromptGuidelines; | ||
| this._shownHighReasoningWarningKeys.clear(); | ||
| for (const key of snapshot.shownHighReasoningWarningKeys) { | ||
| this._shownHighReasoningWarningKeys.add(key); | ||
| } | ||
| }; | ||
| const flushDeferredEvents = () => { | ||
| if (!ownsDeferredEvents) return; | ||
| const deferredEvents = this._deferredSessionEvents ?? []; | ||
| this._deferredSessionEvents = undefined; | ||
| for (const event of deferredEvents) this._emit(event); | ||
| }; | ||
| if ( | ||
| opts.invalidateCompaction && | ||
| (this._modelSelectionChangesContext(previousModel, model) || | ||
|
|
@@ -4652,7 +4767,7 @@ export class AgentSession { | |
| this._invalidateCompactionForModelSelection(); | ||
| } | ||
| const thinking = this._getThinkingForModelSwitch(model, opts.ephemeralThinkingLevel); | ||
| const liveContextTokens = this._getDownswitchLiveContextTokens(model); | ||
| const liveContextTokens = opts.liveContextTokens ?? this._getDownswitchLiveContextTokens(model); | ||
| this.agent.state.model = model; | ||
| this.agent.abortServerSideFallback = | ||
| this.settingsManager.getAbortServerSideFallback() && this._retryFallback.hasConfiguredChain(); | ||
|
|
@@ -4676,6 +4791,8 @@ export class AgentSession { | |
|
|
||
| if (opts.ephemeralThinkingLevel !== undefined) { | ||
| this._applyEphemeralThinkingLevel(thinking.level); | ||
| } else if (opts.persistThinkingLevel === false) { | ||
| this._applyEphemeralThinkingLevel(thinking.level); | ||
| } else { | ||
| this._setThinkingLevel(thinking.level, false, thinking.selection); | ||
| } | ||
|
|
@@ -4694,13 +4811,38 @@ export class AgentSession { | |
| if (!opts.emitModelSelect) return undefined; | ||
| const previousSystemPrompt = this.agent.state.systemPrompt; | ||
| try { | ||
| const systemPromptChange = await this._emitModelSelect(model, previousModel, opts.modelSelectSource); | ||
| const systemPromptChange = await this._emitModelSelect( | ||
| model, | ||
| previousModel, | ||
| opts.modelSelectSource, | ||
| !opts.transactionalModelSelect, | ||
| opts.transactionalModelSelect, | ||
| ); | ||
| this.assertModelUsable(model, liveContextTokens); | ||
| const committedSystemPromptChange = opts.transactionalModelSelect | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When a recovery hook returns the same prompt on both passes, the provisional pass consumes the prompt transition and the committed pass emits no Prompt for AI agents |
||
| ? await this._emitModelSelect(model, previousModel, opts.modelSelectSource, false) | ||
| : systemPromptChange; | ||
| this.assertModelUsable(model, liveContextTokens); | ||
| return systemPromptChange; | ||
| flushDeferredEvents(); | ||
| if (committedSystemPromptChange && opts.transactionalModelSelect) { | ||
| await this._publishSystemPromptChange(committedSystemPromptChange); | ||
| } | ||
| return committedSystemPromptChange; | ||
| } catch (error) { | ||
| if (previousModel) this.agent.state.model = previousModel; | ||
| else delete (this.agent.state as { model?: Model<Api> }).model; | ||
| this.agent.state.systemPrompt = previousSystemPrompt; | ||
| if (snapshot) { | ||
| restoreSnapshot(); | ||
| try { | ||
| if (previousModel) | ||
| await this._emitModelSelect(previousModel, model, opts.modelSelectSource, false, true); | ||
| } finally { | ||
| restoreSnapshot(); | ||
| } | ||
| } else { | ||
| if (previousModel) this.agent.state.model = previousModel; | ||
| else delete (this.agent.state as { model?: Model<Api> }).model; | ||
| this.agent.state.systemPrompt = previousSystemPrompt; | ||
| } | ||
| if (ownsDeferredEvents) this._deferredSessionEvents = undefined; | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,40 @@ | ||
| # changes | ||
|
|
||
| ## 2026-09-01 - Recover oversized saved sessions onto a usable model | ||
|
|
||
| ### What changed | ||
|
|
||
| - `sdk.ts` now distinguishes an implicit saved-model restore from an explicit | ||
| startup model selection when the assembled runtime fails the live-context | ||
| usability projection. | ||
| - Implicit restores deterministically select the authenticated candidate with | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The doc claims recovery 'select[s] the authenticated candidate with the greatest remaining context budget' and that a session with 'no capable authenticated recovery model' keeps the typed ModelUsabilityBudgetError. In the code, findStartupRecoveryModel() selects purely by remaining budget from configured (not authenticated) models, and setStartupRecoveryModel() throws a plain Prompt for AI agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in b5d6c84 and documented in the trackers: candidates are budget-ranked, then provider auth is checked in that order; unavailable providers are skipped and deduplicated before the next capable candidate is selected. |
||
| the greatest remaining context budget, persist that change only in the | ||
| session history, and return a visible fallback notice. | ||
| - Recovery validates candidate credentials in budget order, skips unavailable | ||
| providers, and defers both model and model-specific thinking persistence | ||
| until extension admission succeeds. | ||
| - If a model-select hook makes the highest-capacity candidate unusable, startup | ||
| rolls back its runtime state before continuing through the remaining | ||
| budget-ranked candidates; Claude SDK continuity ignores provisional candidate | ||
| probes and provider authentication is cached per recovery. | ||
| - Explicit startup models remain fail-closed, and sessions with no capable | ||
| authenticated recovery model keep the typed `ModelUsabilityBudgetError`. | ||
|
|
||
| ### Why | ||
|
|
||
| - The restored-transcript admission guard correctly prevented undersized model | ||
| switches, but it also made long existing sessions impossible to reopen from | ||
| the normal session picker even when a larger configured model was available. | ||
|
|
||
| ### Why an extension could not handle it | ||
|
|
||
| - Model restoration and the first usability assertion happen before extensions | ||
| receive a live session surface. | ||
|
|
||
| ### Expected merge conflict zones | ||
|
|
||
| - MEDIUM: `sdk.ts` startup model selection and final usability assertion. | ||
|
|
||
| ## 2026-08-31 - Session activity contract for host occupancy decisions | ||
|
|
||
| ### What changed | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1125,6 +1125,11 @@ export interface ModelSelectEvent { | |
| model: Model<any>; | ||
| previousModel: Model<any> | undefined; | ||
| source: ModelSelectSource; | ||
| /** | ||
| * A startup-recovery candidate probe. Handlers may adjust in-memory prompt or | ||
| * tool state, but must defer external side effects until the committed event. | ||
| */ | ||
| provisional?: boolean; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This public API addition to Prompt for AI agents |
||
| /** The active system prompt before model_select handlers run. */ | ||
| systemPrompt: string; | ||
| /** Structured options used to build the base system prompt. */ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.