fix(hub+cli): defer mergeSessions on cursor ACP reopen until session/load succeeds (closes #939) (#948)

* fix(hub+cli): defer mergeSessions on cursor ACP reopen until session/load succeeds

Emit session-ready from the CLI after ACP load/newSession completes; hub
resumeSession and cursor dedup wait for that signal before merging rows so a
failed session/load no longer deletes the archived session the operator can retry.

Refs #917. Closes #939.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): gate session-ready wait on cursor ACP protocol only

Legacy stream-json Cursor resumes use cursorLegacyRemoteLauncher, which does
not emit session-ready; limiting the defer-merge and dedup gates to ACP avoids
60s resume_failed timeouts on those sessions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): block ACP dedup until session-ready, including on session-end

Inactive ACP spawns that never emitted session-ready could still trigger
deduplicateByAgentSessionId on session-end and delete the original row.
Require session-ready for all ACP dedup paths and skip end-of-session dedup
when load never succeeded.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): restore session-end dedup for non-ACP cursor duplicates

Only skip the session-end dedup retry for Cursor ACP rows that never emitted
session-ready. Codex/Claude/legacy Cursor duplicates still merge when the live
row ends.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
HeavyGee
2026-06-18 10:10:10 +08:00
committed by GitHub
co-authored by Cursor
parent 0bf17b818e
commit 26d3c2eb34
11 changed files with 455 additions and 22 deletions
+31 -1
View File
@@ -105,7 +105,7 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
mcpServers: mcpServerList
});
} catch (error) {
logger.warn('[cursor-acp] session/load failed', error);
logger.warn('[cursor-acp] session/load failed', formatAcpLoadError(error));
throw new Error(
'Failed to resume Cursor ACP session. Legacy stream-json sessions cannot be loaded via ACP.'
);
@@ -125,6 +125,8 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
session.onSessionFoundWithProtocol(acpSessionId, 'acp');
}
session.client.emitSessionReady();
syncCursorModelsFromAcp(backend, acpSessionId);
const initialMetadata = backend.getSessionModelsMetadata(acpSessionId);
@@ -436,6 +438,34 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
}
}
function formatAcpLoadError(error: unknown): Record<string, unknown> {
if (error instanceof Error) {
const record: Record<string, unknown> = {
name: error.name,
message: error.message
};
const code = (error as Error & { code?: unknown }).code;
if (code !== undefined) {
record.code = code;
}
const data = (error as Error & { data?: unknown }).data;
if (data !== undefined) {
record.data = data;
}
const cause = error.cause;
if (cause !== undefined) {
record.cause = cause instanceof Error
? { name: cause.name, message: cause.message }
: cause;
}
return record;
}
if (typeof error === 'object' && error !== null) {
return { ...(error as Record<string, unknown>) };
}
return { message: String(error) };
}
function isSpawnDefaultModel(modelId: string): boolean {
const normalized = modelId.trim().toLowerCase();
return normalized === 'auto' || normalized === 'default' || normalized === 'default[]';