refactor(core): decouple run settling from terminal status transition

Resolve #running before the terminal statuschange so the settle signal can
never be lost to re-entrant listeners. Hooks keep middleware semantics:
a throwing hook fails the run; integrations that don't want this should
suppress errors in their own hooks. Also make suppress() async-aware so
rejected promises (e.g. showMask) are actually caught.
This commit is contained in:
Simon
2026-06-11 17:21:11 +08:00
parent 8f9a637bdb
commit 4690aefec5
2 changed files with 9 additions and 14 deletions

View File

@@ -194,7 +194,8 @@ export class PageAgentCore extends EventTarget {
} }
/** /**
* Stop the current task and wait until the run has fully settled. * Stop the current task and wait until the run has fully settled (including lifecycle hooks).
* @note never await .stop() in a lifecycle hook.
*/ */
async stop(): Promise<void> { async stop(): Promise<void> {
if (this.#status !== 'running') return if (this.#status !== 'running') return
@@ -223,14 +224,8 @@ export class PageAgentCore extends EventTarget {
this.#setStatus('running') this.#setStatus('running')
this.#emitHistoryChange() this.#emitHistoryChange()
let settleRun!: (status: AgentStatus) => void let resolveRunning!: () => void
this.#running = new Promise<void>( this.#running = new Promise<void>((r) => (resolveRunning = r))
(resolve) =>
(settleRun = (status) => {
this.#setStatus(status)
resolve()
})
)
// Disable ask_user tool if onAskUser is not set // Disable ask_user tool if onAskUser is not set
if (!this.onAskUser) this.tools.delete('ask_user') if (!this.onAskUser) this.tools.delete('ask_user')
@@ -257,8 +252,7 @@ export class PageAgentCore extends EventTarget {
try { try {
console.group(`step: ${step}`) console.group(`step: ${step}`)
// Abort may have fired between steps (e.g. during stepDelay). // inside the try: abort between steps must settle as `stopped`
// Check inside the try so it is classified as `stopped`, and skip the observe phase.
this.#abortController.signal.throwIfAborted() this.#abortController.signal.throwIfAborted()
// observe // observe
@@ -371,7 +365,8 @@ export class PageAgentCore extends EventTarget {
suppress(() => this.pageController.cleanUpHighlights()) suppress(() => this.pageController.cleanUpHighlights())
suppress(() => this.pageController.hideMask()) suppress(() => this.pageController.hideMask())
this.#abortController.abort() this.#abortController.abort()
settleRun(finalStatus) resolveRunning()
this.#setStatus(finalStatus)
} }
} }

View File

@@ -133,9 +133,9 @@ export function assert(condition: unknown, message?: string, silent?: boolean):
/** /**
* Suppress errors from a function. * Suppress errors from a function.
*/ */
export function suppress<T>(fn: () => T): T | undefined { export async function suppress<T>(fn: () => T | Promise<T>): Promise<Awaited<T> | undefined> {
try { try {
return fn() return await fn()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
return undefined return undefined