fix: drop the deployed schema from a form attached after the turn stopped

This commit is contained in:
AlexRV12
2026-09-03 16:30:09 +02:00
parent 48e9383dea
commit c7c473be0f
2 changed files with 29 additions and 0 deletions
@@ -1809,6 +1809,11 @@ export class AIChatManager {
// would park the turn on a form the settled card no longer renders, leaving nothing
// able to resolve it. The controller is per-turn, so a later turn still opens.
if (this.abortController?.signal.aborted) {
// Settle the form the tool attached after the stop. Its card is about to stop
// loading without ever having rendered, and settledToolDisplay only reaches a
// loading one — so this is the last point the deployed schema, with the
// script's own password and file defaults, can be dropped.
this.#patchRunForm(toolId, { canceled: true })
return Promise.resolve(undefined)
}
return new Promise((resolve) => {
@@ -301,6 +301,30 @@ describe('AIChatManager run form', () => {
expect(manager.isRunFormPending('call_late')).toBe(false)
})
// The stop lands while the tool is still reading the deployed schema, so the form is
// attached after the card was settled. Nothing settles it a second time — the card
// stops loading without the form ever rendering — so the schema would otherwise stay
// in the transcript with the script's own password default inside it.
it('drops the schema from a form attached after the turn was stopped', async () => {
const manager = new AIChatManager()
manager.abortController = new AbortController()
manager.displayMessages = [
{ role: 'tool', tool_call_id: 'call_x', content: 'Executing...', isLoading: true }
]
manager.cancel()
const runForm = {
path: 'f/a/b',
schema: { properties: { tok: { password: true, default: 'hunter2' } } },
args: {}
}
manager.applyToolStatus('call_x', { content: 'Waiting for you...', runForm, isLoading: true })
await expect(manager.requestRunArgs('call_x', runForm)).resolves.toBeUndefined()
expect(manager.displayMessages[0].runForm?.schema).toBeUndefined()
expect(JSON.stringify(manager.displayMessages[0])).not.toContain('hunter2')
})
// Stop ends the turn, not the job: the deployed script is already running with all
// its side effects, so the transcript must not record it as cancelled.
it('does not mark a started run cancelled when the turn is stopped', () => {