feat(opencode): open a fresh session on clear (#1300)

* test(opencode): specify fresh-session clear

* feat(opencode): open a fresh session on clear

* fix(opencode): release clear latch on cancel

* fix(opencode): retry transient clear handoffs

* fix(opencode): confirm clear archive delivery

* fix(web): preserve superseded session access

* fix(clear): invalidate transferred schedules

* fix(runner): restore live spawn dedupe

* fix(clear): preserve latched scheduled prompts

* fix(runner): quarantine unverified children

* fix(clear): retain handoff retry ownership

* fix(runner): release recovered spawn dedupe

* fix(clear): retain archive retry ownership

* fix(clear): settle rejected immediate prompts

* fix(clear): block reopening replaced sources

* fix(clear): settle prompts when clear is cancelled

* fix(clear): make fresh-session handoff durable

* fix(clear): finalize only after native cleanup

* fix(clear): abort failed native handoffs

* fix(clear): gate recovery on cleanup proof

* fix(clear): retry metadata persistence failures

* fix(clear): preserve handoff ownership through teardown

* fix(clear): abort incomplete cleanup reservations

* fix(clear): require explicit exit before abort

* fix(clear): verify owner exit before recovery

* fix(clear): guard recovery handoff races

* fix(clear): serialize cleanup callbacks

* fix(clear): make callback retries idempotent

* fix(clear): bind callbacks to reservations

* fix(clear): recover pending spawns

* fix(clear): deduplicate held prompts

* fix(clear): validate redirect ownership

* fix(clear): replay prompts in FIFO order

* fix(clear): gate replacement delivery
This commit is contained in:
Junmo Kim
2026-08-03 18:06:39 +08:00
committed by GitHub
parent 1b8cc334ea
commit f44c9ff3e6
41 changed files with 3209 additions and 73 deletions
+16 -9
View File
@@ -1098,7 +1098,7 @@ export class ApiSessionClient extends EventEmitter {
return await this.drainLock(this.metadataLock, timeoutMs)
}
async flush(options?: { timeoutMs?: number }): Promise<void> {
async flush(options?: { timeoutMs?: number }): Promise<boolean> {
const deadlineMs = Date.now() + (options?.timeoutMs ?? 5_000)
const remainingMs = () => Math.max(0, deadlineMs - Date.now())
@@ -1106,37 +1106,44 @@ export class ApiSessionClient extends EventEmitter {
if (materializationTask) {
this.materializationDrainRequested = true
this.materializationRetryAbortController?.abort()
await this.waitForPromise(materializationTask, remainingMs())
if (!await this.waitForPromise(materializationTask, remainingMs())) {
return false
}
}
if (this.state !== 'active') {
return
return false
}
if (!this.socket.connected) {
const connected = await this.waitForConnected(remainingMs())
if (!connected) {
return
return false
}
}
await this.drainLock(this.metadataLock, remainingMs())
await this.drainLock(this.agentStateLock, remainingMs())
if (!await this.drainLock(this.metadataLock, remainingMs())) {
return false
}
if (!await this.drainLock(this.agentStateLock, remainingMs())) {
return false
}
if (remainingMs() === 0) {
return
return false
}
const pingTimeoutMs = remainingMs()
if (pingTimeoutMs === 0) {
return
return false
}
try {
await this.socket.timeout(pingTimeoutMs).emitWithAck('ping')
this.awaitingMaterializedConnection = false
return true
} catch {
// best effort
return false
}
}