import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime"; import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing"; import { listSessionCatalogEntries, sessionCatalogAdoptedSessionKey, sessionCatalogAdoptedSourceKey, type SessionCatalogEntrySnapshot, } from "openclaw/plugin-sdk/session-catalog"; import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { CodexThread } from "./app-server/protocol.js"; import { importCodexThreadHistoryToTranscript } from "./app-server/transcript-mirror.js"; import { CatalogParamsError } from "./session-catalog-parsing.js"; import type { CodexSessionCatalogSession } from "./session-catalog-types.js"; const CODEX_NODE_SESSION_KEY_PREFIX = "harness:codex:node-session:"; type CatalogSessionEntry = ReturnType< PluginRuntime["agent"]["session"]["listSessionEntries"] >[number]["entry"]; type CodexNodeSessionMarker = { sourceHostId: string; sourceThreadId: string; nodeId: string; initializing?: true; }; export type AdoptedSessionEntry = { key: string; sessionId: string; agentId: string; initializing?: true; /** Bound canonical thread for local supervision adoptions; absent for node entries. */ boundThreadId?: string; }; export type CodexNodeHistory = { thread: CodexThread; throughTurnId: string | null; }; export type CodexSessionDisposition = "existing" | "forked"; export const continueOperations = new Map< string, Promise<{ sessionKey: string; disposition: CodexSessionDisposition }> >(); const sessionActionTails = new Map>(); export async function runSessionActionExclusive( threadId: string, run: () => Promise, ): Promise { const previous = sessionActionTails.get(threadId) ?? Promise.resolve(); const operation = previous.then(run); const tail = operation.then( () => undefined, () => undefined, ); sessionActionTails.set(threadId, tail); try { return await operation; } finally { if (sessionActionTails.get(threadId) === tail) { sessionActionTails.delete(threadId); } } } // Session creation persists this plugin-owned suffix under an agent-qualified key. // Restart discovery must compare the parsed suffix, not the returned canonical key. export function adoptionSessionKeyRest(sessionKey: string): string { const trimmed = sessionKey.trim(); return parseAgentSessionKey(trimmed)?.rest ?? trimmed; } function nodeAdoptionSessionKey(hostId: string, threadId: string): string { const source = JSON.stringify([hostId, threadId]); return sessionCatalogAdoptedSessionKey(CODEX_NODE_SESSION_KEY_PREFIX, source); } function readNodeSessionMarker(entry: CatalogSessionEntry): CodexNodeSessionMarker | undefined { const codex = isRecord(entry.pluginExtensions?.codex) ? entry.pluginExtensions.codex : undefined; const marker = codex && isRecord(codex.sessionCatalog) ? codex.sessionCatalog : undefined; if ( !marker || typeof marker.sourceHostId !== "string" || !marker.sourceHostId.startsWith("node:") || typeof marker.sourceThreadId !== "string" || !marker.sourceThreadId.trim() || typeof marker.nodeId !== "string" || !marker.nodeId.trim() ) { return undefined; } return { sourceHostId: marker.sourceHostId, sourceThreadId: marker.sourceThreadId, nodeId: marker.nodeId, ...(marker.initializing === true ? { initializing: true } : {}), }; } export function listNodeAdoptedSessionEntries(params: { agentId?: string; config?: OpenClawConfig; runtime: PluginRuntime; includeInitializing?: boolean; sessionEntries?: SessionCatalogEntrySnapshot; }): Map { const adopted = new Map(); for (const { agentId, entry, sessionKey } of listSessionCatalogEntries({ ...(params.agentId ? { agentId: params.agentId } : {}), config: params.config ?? {}, runtime: params.runtime, sessionEntries: params.sessionEntries, })) { const marker = readNodeSessionMarker(entry); const sessionId = entry.sessionId?.trim(); if ( !marker || (marker.initializing === true && params.includeInitializing !== true) || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true || !sessionId || adoptionSessionKeyRest(sessionKey) !== nodeAdoptionSessionKey(marker.sourceHostId, marker.sourceThreadId) || marker.sourceHostId !== `node:${marker.nodeId}` ) { continue; } const sourceKey = sessionCatalogAdoptedSourceKey(marker.sourceHostId, marker.sourceThreadId); if (adopted.has(sourceKey)) { throw new Error( `multiple OpenClaw sessions adopt Codex thread ${marker.sourceThreadId} on ${marker.sourceHostId}`, ); } adopted.set(sourceKey, { key: sessionKey, sessionId, agentId, ...(marker.initializing === true ? { initializing: true } : {}), }); } return adopted; } export function findNodeAdoptedSessionEntry(params: { agentId?: string; config: OpenClawConfig; runtime: PluginRuntime; hostId: string; threadId: string; includeInitializing?: boolean; }): AdoptedSessionEntry | undefined { return listNodeAdoptedSessionEntries(params).get( sessionCatalogAdoptedSourceKey(params.hostId, params.threadId), ); } export function nodeSessionMarker(params: { hostId: string; threadId: string; nodeId: string; initializing?: true; }): CodexNodeSessionMarker { return { sourceHostId: params.hostId, sourceThreadId: params.threadId, nodeId: params.nodeId, ...(params.initializing === true ? { initializing: true } : {}), }; } export async function finalizeNodeAdoptedSession(params: { api: OpenClawPluginApi; adopted: AdoptedSessionEntry; marker: CodexNodeSessionMarker; }): Promise { const changedError = () => new CatalogParamsError("Codex OpenClaw session changed before it could be bound. Retry."); let finalized: CatalogSessionEntry | null; try { finalized = await params.api.runtime.agent.session.patchSessionEntry({ sessionKey: params.adopted.key, readConsistency: "latest", preserveActivity: true, update: (entry) => { const current = readNodeSessionMarker(entry); if ( entry.sessionId?.trim() !== params.adopted.sessionId || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true || !current || current.sourceHostId !== params.marker.sourceHostId || current.sourceThreadId !== params.marker.sourceThreadId || current.nodeId !== params.marker.nodeId ) { throw changedError(); } if (current.initializing !== true) { return { archivedAt: undefined }; } const codex = isRecord(entry.pluginExtensions?.codex) ? entry.pluginExtensions.codex : {}; return { archivedAt: undefined, pluginExtensions: { ...entry.pluginExtensions, codex: { ...codex, sessionCatalog: params.marker }, }, }; }, }); } catch (error) { const currentEntry = params.api.runtime.agent.session.getSessionEntry({ sessionKey: params.adopted.key, readConsistency: "latest", }); const current = currentEntry ? readNodeSessionMarker(currentEntry) : undefined; if ( currentEntry?.sessionId?.trim() === params.adopted.sessionId && current?.initializing !== true && current?.sourceHostId === params.marker.sourceHostId && current.sourceThreadId === params.marker.sourceThreadId && current.nodeId === params.marker.nodeId ) { return; } throw error; } if (!finalized) { throw changedError(); } } export async function createOrReuseNodeAdoptedSession(params: { agentId: string; api: OpenClawPluginApi; config: OpenClawConfig; hostId: string; nodeId: string; record: CodexSessionCatalogSession; history: CodexNodeHistory; }): Promise { const existing = findNodeAdoptedSessionEntry({ agentId: params.agentId, config: params.config, runtime: params.api.runtime, hostId: params.hostId, threadId: params.record.threadId, includeInitializing: true, }); if (existing) { return existing; } const marker = nodeSessionMarker({ hostId: params.hostId, threadId: params.record.threadId, nodeId: params.nodeId, }); const initializingMarker = { ...marker, initializing: true as const }; try { const created = await params.api.runtime.agent.session.createSessionEntry({ cfg: params.config, key: nodeAdoptionSessionKey(params.hostId, params.record.threadId), agentId: params.agentId, recoverMatchingInitialEntry: true, ...(params.record.name?.trim() ? { label: params.record.name.trim() } : {}), ...(params.record.cwd?.trim() ? { spawnedCwd: params.record.cwd.trim() } : {}), initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: { codex: { sessionCatalog: initializingMarker, }, }, }, afterCreate: async (entry) => { const storePath = resolveStorePath(params.config.session?.store, { agentId: entry.agentId, }); await importCodexThreadHistoryToTranscript({ thread: params.history.thread, throughTurnId: params.history.throughTurnId, storePath, sessionId: entry.sessionId, sessionKey: entry.key, agentId: entry.agentId, ...(params.record.cwd?.trim() ? { cwd: params.record.cwd.trim() } : {}), modelProvider: params.record.modelProvider, config: params.config, }); return { pluginExtensions: { codex: { sessionCatalog: initializingMarker } } }; }, }); return { key: created.key, sessionId: created.sessionId, agentId: created.agentId, initializing: true, }; } catch (error) { const raced = findNodeAdoptedSessionEntry({ agentId: params.agentId, config: params.config, runtime: params.api.runtime, hostId: params.hostId, threadId: params.record.threadId, includeInitializing: true, }); if (raced) { return raced; } throw error; } }