fix: recover Codex resume ID from stored messages (#1180)

* refactor(hub): generalize recovered ID helpers

* fix(hub): recover Codex resume ID from messages

* fix(web): allow Codex message recovery
This commit is contained in:
Junmo Kim
2026-07-27 12:59:57 +08:00
committed by GitHub
parent d115f8d960
commit bb5275a333
4 changed files with 402 additions and 17 deletions
+80 -11
View File
@@ -842,7 +842,9 @@ export class SyncEngine {
}
const flavor = this.resolveFlavor(session)
if (flavor === 'codex') return metadata.codexSessionId ?? null
if (flavor === 'codex') {
return metadata.codexSessionId ?? this.recoverCodexSessionIdFromMessages(session.id, namespace)
}
if (flavor === 'gemini') return metadata.geminiSessionId ?? null
if (flavor === 'opencode') return metadata.opencodeSessionId ?? null
if (flavor === 'grok') return metadata.grokSessionId ?? null
@@ -1495,19 +1497,69 @@ export class SyncEngine {
const found = this.extractClaudeSessionId(messages[i].content)
if (!found) continue
this.persistRecoveredClaudeSessionId(sessionId, namespace, found)
return found
return this.persistRecoveredAgentSessionId(sessionId, namespace, 'claudeSessionId', found)
}
return null
}
private recoverCodexSessionIdFromMessages(sessionId: string, namespace: string): string | null {
const messages = this.messageService.getMessages(sessionId, 200)
for (let i = messages.length - 1; i >= 0; i -= 1) {
const content = messages[i].content
if (this.isCodexContextResetMessage(content)) return null
const found = this.extractCodexParentThreadId(content)
if (!found) continue
return this.persistRecoveredAgentSessionId(sessionId, namespace, 'codexSessionId', found)
}
return null
}
private isCodexContextResetMessage(value: unknown): boolean {
const message = asRecord(value)
const content = asRecord(message?.content)
const event = asRecord(content?.data)
return message?.role === 'agent'
&& content?.type === 'event'
&& event?.type === 'message'
&& event.message === 'Context was reset'
}
private extractCodexParentThreadId(value: unknown): string | null {
const message = asRecord(value)
const content = asRecord(message?.content)
const event = asRecord(content?.data)
if (message?.role !== 'agent' || content?.type !== 'codex' || !event) {
return null
}
const scope = asRecord(event.scope)
const roles = [event.scope_role, event.scopeRole, scope?.role]
.filter((role) => role !== undefined)
if (roles.length === 0 || roles.some((role) => role !== 'parent')) {
return null
}
const threadIds: string[] = []
for (const threadId of [event.thread_id, event.threadId, scope?.thread_id, scope?.threadId]) {
if (threadId === undefined) continue
const normalized = this.normalizeAgentSessionId(threadId)
if (!normalized) return null
threadIds.push(normalized)
}
const uniqueThreadIds = [...new Set(threadIds)]
return uniqueThreadIds.length === 1 ? uniqueThreadIds[0] : null
}
private extractClaudeSessionId(value: unknown): string | null {
if (!value || typeof value !== 'object') {
return null
}
const obj = value as Record<string, unknown>
const direct = this.normalizeClaudeSessionId(obj.session_id) ?? this.normalizeClaudeSessionId(obj.sessionId)
const direct = this.normalizeAgentSessionId(obj.session_id) ?? this.normalizeAgentSessionId(obj.sessionId)
if (direct) {
return direct
}
@@ -1527,7 +1579,7 @@ export class SyncEngine {
return null
}
private normalizeClaudeSessionId(value: unknown): string | null {
private normalizeAgentSessionId(value: unknown): string | null {
if (typeof value !== 'string') {
return null
}
@@ -1537,28 +1589,45 @@ export class SyncEngine {
: null
}
private persistRecoveredClaudeSessionId(sessionId: string, namespace: string, claudeSessionId: string): void {
private persistRecoveredAgentSessionId(
sessionId: string,
namespace: string,
field: 'claudeSessionId' | 'codexSessionId',
agentSessionId: string
): string {
for (let attempt = 0; attempt < 2; attempt += 1) {
const latest = this.sessionCache.getSessionByNamespace(sessionId, namespace)
?? this.sessionCache.refreshSession(sessionId)
if (!latest?.metadata) return
if (latest.metadata.claudeSessionId === claudeSessionId) return
if (!latest?.metadata) return agentSessionId
const existingAgentSessionId = latest.metadata[field]
if (typeof existingAgentSessionId === 'string') {
return existingAgentSessionId
}
const result = this.store.sessions.updateSessionMetadata(
sessionId,
{ ...latest.metadata, claudeSessionId },
{ ...latest.metadata, [field]: agentSessionId },
latest.metadataVersion,
namespace,
{ touchUpdatedAt: false }
)
if (result.result === 'success') {
this.sessionCache.refreshSession(sessionId)
return
return agentSessionId
}
if (result.result !== 'version-mismatch') {
return
return agentSessionId
}
this.sessionCache.refreshSession(sessionId)
const refreshed = this.sessionCache.getSessionByNamespace(sessionId, namespace)
const authoritativeAgentSessionId = refreshed?.metadata?.[field]
if (typeof authoritativeAgentSessionId === 'string') {
return authoritativeAgentSessionId
}
}
return agentSessionId
}
private hasSameAgentSessionIds(