mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
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:
@@ -32,6 +32,12 @@ type OpencodeRemoteLauncherOptions = {
|
||||
// call (and summary lookup) settles, so a cancelled request's result
|
||||
// doesn't surface for an action the user no longer expects a reply from.
|
||||
isLocalIdCancelled?: (localId: string) => boolean;
|
||||
// Called only after /clear reaches its FIFO position *and* this
|
||||
// launcher has disconnected its OpenCode backend. The caller then performs
|
||||
// the source lifecycle cleanup before requesting the fresh process.
|
||||
onClearRequested?: () => Promise<void>;
|
||||
onClearCleanupComplete?: () => Promise<void>;
|
||||
onClearCleanupFailed?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type AbortStatusDecision = {
|
||||
@@ -77,6 +83,10 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
private baseUrl: string | null = null;
|
||||
private permissionHandler: OpencodePermissionHandler | null = null;
|
||||
private happyServer: { stop: () => void } | null = null;
|
||||
// Becomes true when the FIFO loop reaches /clear. Its callback is deferred
|
||||
// until cleanup() completes so a failed OpenCode disconnect cannot create a
|
||||
// replacement while the source backend may still be live.
|
||||
private clearRequested = false;
|
||||
private abortController = new AbortController();
|
||||
// Set by the dequeue loop as soon as a batch is identified as a
|
||||
// `operation:'compact'` one — deliberately *before* that batch's inline
|
||||
@@ -303,6 +313,18 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
break;
|
||||
}
|
||||
|
||||
// /clear is deliberately a queue operation rather than a direct
|
||||
// slash side effect: every prompt and /compact ahead of it has
|
||||
// completed before this point. In particular, do not route this
|
||||
// through handleAbort(true): that method exists to interrupt an
|
||||
// in-flight compact, while clear can only run after one finishes.
|
||||
if (batch.mode.operation === 'clear') {
|
||||
await this.options.onClearRequested?.();
|
||||
this.clearRequested = true;
|
||||
await this.requestExit('exit', async () => {})
|
||||
break;
|
||||
}
|
||||
|
||||
// Created here — before the model/effort switch below — rather
|
||||
// than inside runCompactOperation(), so it already exists for
|
||||
// handleAbort() to act on during that switch. backend.setModel()/
|
||||
@@ -601,21 +623,45 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
|
||||
protected async cleanup(): Promise<void> {
|
||||
this.clearAbortHandlers(this.session.client.rpcHandlerManager);
|
||||
|
||||
const failures: unknown[] = [];
|
||||
if (this.permissionHandler) {
|
||||
await this.permissionHandler.cancelAll('Session ended');
|
||||
this.permissionHandler = null;
|
||||
try {
|
||||
await this.permissionHandler.cancelAll('Session ended');
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
} finally {
|
||||
this.permissionHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.backend) {
|
||||
await this.backend.disconnect();
|
||||
this.backend = null;
|
||||
try {
|
||||
await this.backend.disconnect();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
} finally {
|
||||
this.backend = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.happyServer) {
|
||||
this.happyServer.stop();
|
||||
this.happyServer = null;
|
||||
try {
|
||||
this.happyServer.stop();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
} finally {
|
||||
this.happyServer = null;
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
if (this.clearRequested) await this.options.onClearCleanupFailed?.();
|
||||
throw failures.length === 1 ? failures[0] : new AggregateError(failures, 'OpenCode cleanup failed');
|
||||
}
|
||||
if (this.clearRequested) await this.options.onClearCleanupComplete?.();
|
||||
|
||||
// Signal the runner only after the native backend is gone. If an
|
||||
// awaited teardown above fails, RemoteLauncherBase propagates that
|
||||
// failure and this callback never runs; runOpencode then archives the
|
||||
// source as an error rather than spawning a potentially concurrent
|
||||
// replacement.
|
||||
}
|
||||
|
||||
private rollbackReasoningEffort(batch: { mode: OpencodeMode }, effort: string | null): void {
|
||||
|
||||
Reference in New Issue
Block a user