import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; import { exportXWorkmateArtifacts } from "./exportArtifacts.js"; import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js"; const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping"; export type XWorkmateTaskMetadataV1 = { schemaVersion: 1; appThreadKey: string; openclawSessionKey?: string; expectedArtifactDirs: string[]; requestId?: string; externalTaskId?: string; createdAt: string; }; export type XWorkmateSessionMappingSource = | "session_start" | "bridge_prepare"; export type XWorkmateSessionMappingV1 = { schemaVersion: 1; appThreadKey: string; openclawSessionKey: string; expectedArtifactDirs: string[]; createdAt: string; updatedAt: string; source: XWorkmateSessionMappingSource; }; export type XWorkmateTaskLookupErrorCode = | "mapping_not_found" | "task_not_found" | "no_native_task_record" | "conflict" | "invalid_lookup"; export type XWorkmateTaskLookupError = { ok: false; code: XWorkmateTaskLookupErrorCode; message: string; mapping?: XWorkmateSessionMappingV1; expectedArtifactDirs?: string[]; }; type SessionEntry = Record & { pluginExtensions?: Record>; }; type PatchSessionEntry = (params: { sessionKey: string; fallbackEntry?: SessionEntry; preserveActivity?: boolean; update: (entry: SessionEntry) => Partial | null; }) => Promise | SessionEntry | null; type GetSessionEntry = (params: { sessionKey: string }) => SessionEntry | undefined; type BoundTaskRunsRuntime = { get?: (taskId: string) => unknown; list?: () => unknown[]; findLatest?: () => unknown; resolve?: (token: string) => unknown; }; export function registerXWorkmateSessionExtension(api: OpenClawPluginApi) { const registerExtension = api.session?.state?.registerSessionExtension ?? (api as any).registerSessionExtension; if (typeof registerExtension !== "function") { return; } registerExtension({ namespace: XWORKMATE_SESSION_EXTENSION_NAMESPACE, description: "Durable XWorkmate app/OpenClaw session key mapping.", sessionEntrySlotKey: "xworkmate", project: (ctx: { sessionKey: string; state?: unknown }): any => { const state = asRecord(ctx.state); return state ?? {}; }, }); } export async function recordXWorkmateSessionMapping(input: { api: OpenClawPluginApi; params: Record; artifactScope?: string; source?: XWorkmateSessionMappingSource; }): Promise { const metadata = normalizeXWorkmateTaskMetadataV1(input.params); const openclawSessionKey = requiredString( input.params.openclawSessionKey ?? metadata.openclawSessionKey, "openclawSessionKey required", ); return upsertXWorkmateSessionMapping(input.api, { metadata: { ...metadata, openclawSessionKey, }, openclawSessionKey, source: input.source ?? "bridge_prepare", }); } function normalizeXWorkmateTaskMetadataV1(input: Record): XWorkmateTaskMetadataV1 { const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input; const schemaVersion = Number(envelope.schemaVersion ?? 1); if (schemaVersion !== 1) { throw new Error("schemaVersion must be 1"); } const appThreadKey = requiredString(envelope.appThreadKey, "appThreadKey required"); const createdAt = optionalString(envelope.createdAt) || new Date().toISOString(); return compactObject({ schemaVersion: 1 as const, appThreadKey, openclawSessionKey: optionalString(envelope.openclawSessionKey), expectedArtifactDirs: normalizeExpectedArtifactDirs(envelope.expectedArtifactDirs), requestId: optionalString(envelope.requestId), externalTaskId: optionalString(envelope.externalTaskId ?? envelope.taskId), createdAt, }) as XWorkmateTaskMetadataV1; } async function upsertXWorkmateSessionMapping( api: OpenClawPluginApi, input: { metadata: XWorkmateTaskMetadataV1; openclawSessionKey: string; source: XWorkmateSessionMappingSource; }, ): Promise { const patchSessionEntry = resolvePatchSessionEntry(api); if (!patchSessionEntry) { throw new Error("OpenClaw runtime session patch API is unavailable"); } const now = new Date().toISOString(); let mapping: XWorkmateSessionMappingV1 | undefined; await patchSessionEntry({ sessionKey: input.openclawSessionKey, fallbackEntry: { sessionId: input.openclawSessionKey, updatedAt: Date.now(), }, preserveActivity: true, update: (entry) => { const existing = readMappingFromEntry(entry); if (existing) { assertMappingCompatible(existing, input.metadata.appThreadKey, input.openclawSessionKey); mapping = { ...existing, expectedArtifactDirs: input.metadata.expectedArtifactDirs, updatedAt: now, source: existing.source, }; } else { mapping = compactObject({ schemaVersion: 1 as const, appThreadKey: input.metadata.appThreadKey, openclawSessionKey: input.openclawSessionKey, expectedArtifactDirs: input.metadata.expectedArtifactDirs, createdAt: input.metadata.createdAt || now, updatedAt: now, source: input.source, }) as XWorkmateSessionMappingV1; } return { pluginExtensions: writeMappingToPluginExtensions(entry.pluginExtensions, mapping), }; }, }); if (!mapping) { throw new Error("failed to write xworkmate session mapping"); } return mapping; } async function readXWorkmateSessionMapping( api: OpenClawPluginApi, lookup: { appThreadKey?: string; openclawSessionKey?: string; }, ): Promise { const getSessionEntry = resolveGetSessionEntry(api); if (!getSessionEntry) { return undefined; } const openclawSessionKey = optionalString(lookup.openclawSessionKey); if (openclawSessionKey) { return readMappingFromEntry(getSessionEntry({ sessionKey: openclawSessionKey })); } const appThreadKey = optionalString(lookup.appThreadKey); if (!appThreadKey) { return undefined; } const listSessionEntries = resolveListSessionEntries(api); for (const item of listSessionEntries?.() ?? []) { const mapping = readMappingFromEntry(item.entry); if (mapping?.appThreadKey === appThreadKey) { return mapping; } } return undefined; } export async function getXWorkmateTaskSnapshot(input: { api: OpenClawPluginApi; params: Record; }): Promise> { const params = input.params ?? {}; const appThreadKey = optionalString(params.appThreadKey); const explicitOpenclawSessionKey = optionalString(params.openclawSessionKey); const mapping = await readXWorkmateSessionMapping(input.api, { appThreadKey, openclawSessionKey: explicitOpenclawSessionKey, }); if (!mapping && appThreadKey && !explicitOpenclawSessionKey) { return lookupError("mapping_not_found", `No OpenClaw session mapping found for ${appThreadKey}`); } const openclawSessionKey = mapping?.openclawSessionKey || explicitOpenclawSessionKey; if (!openclawSessionKey) { return lookupError("invalid_lookup", "openclawSessionKey or appThreadKey required"); } const runId = optionalString(params.runId); const taskId = optionalString(params.taskId); const task = resolveNativeTask(input.api, { openclawSessionKey, runId, taskId, }); const includeArtifacts = params.includeArtifacts !== false; if (!task) { const exported = includeArtifacts && runId ? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping) : undefined; if (exported?.artifacts.length) { return { success: false, status: "unknown", taskStatus: "unknown", evidence: "artifacts_present", mode: "gateway-chat", mapping, appThreadKey: mapping?.appThreadKey ?? appThreadKey, openclawSessionKey, runId, taskId: taskId || runId, task: { taskId: taskId || runId, runId, status: "unknown", source: "artifact_fallback", }, expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [], artifactScope: exported.artifactScope, remoteWorkingDirectory: exported.remoteWorkingDirectory, remoteWorkspaceRefKind: exported.remoteWorkspaceRefKind, scopeKind: exported.scopeKind, artifacts: exported.artifacts, constraintSatisfied: exported.constraintSatisfied, missingRequiredExtensions: exported.missingRequiredExtensions, warnings: [ ...exported.warnings, `Native OpenClaw task record was unavailable for ${openclawSessionKey}; artifacts are present but task status is unknown.`, ], artifactCount: exported.artifacts.length, }; } const code: XWorkmateTaskLookupErrorCode = runId || taskId ? "no_native_task_record" : "task_not_found"; return lookupError(code, `No native OpenClaw task record found for ${openclawSessionKey}`, mapping); } const taskStatus = optionalString((task as any).status) || "running"; const exported = includeArtifacts ? await exportArtifactsForTaskLookup( input, params, openclawSessionKey, runId || optionalString((task as any).runId) || optionalString((task as any).taskId), mapping, ) : undefined; return { success: true, status: appStatusFromTaskStatus(taskStatus), taskStatus, mode: "gateway-chat", mapping, appThreadKey: mapping?.appThreadKey ?? appThreadKey, openclawSessionKey, runId: runId || optionalString((task as any).runId), taskId: taskId || optionalString((task as any).taskId), task, expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [], artifactScope: exported?.artifactScope, remoteWorkingDirectory: exported?.remoteWorkingDirectory, remoteWorkspaceRefKind: exported?.remoteWorkspaceRefKind, scopeKind: exported?.scopeKind, artifacts: exported?.artifacts ?? [], constraintSatisfied: exported?.constraintSatisfied, missingRequiredExtensions: exported?.missingRequiredExtensions, warnings: exported?.warnings ?? [], artifactCount: exported?.artifacts.length ?? 0, }; } async function exportArtifactsForTaskLookup( input: { api: OpenClawPluginApi; params: Record }, params: Record, openclawSessionKey: string, runId: string, mapping?: XWorkmateSessionMappingV1, ) { return exportXWorkmateArtifacts({ params: { ...params, openclawSessionKey, runId, expectedArtifactDirs: mapping?.expectedArtifactDirs ?? normalizeExpectedArtifactDirs(params.expectedArtifactDirs), includeContent: params.includeContent ?? false, }, config: input.api.config, pluginConfig: input.api.pluginConfig, }); } function resolveNativeTask( api: OpenClawPluginApi, input: { openclawSessionKey: string; runId?: string; taskId?: string }, ): Record | undefined { try { const bound = api.runtime?.tasks?.runs?.bindSession?.({ sessionKey: input.openclawSessionKey }) as | BoundTaskRunsRuntime | undefined; if (!bound) { return undefined; } const lookup = input.taskId || input.runId || ""; const resolved = lookup ? bound.resolve?.(lookup) || bound.get?.(lookup) : bound.findLatest?.(); return asRecord(resolved); } catch (error) { api.logger?.warn?.( `xworkmate native task lookup failed: sessionKey=${input.openclawSessionKey} error=${String(error)}`, ); return undefined; } } function lookupError( code: XWorkmateTaskLookupErrorCode, message: string, mapping?: XWorkmateSessionMappingV1, ): XWorkmateTaskLookupError { return { ok: false, code, message, ...(mapping ? { mapping, expectedArtifactDirs: mapping.expectedArtifactDirs } : {}), }; } function readMappingFromEntry(entry: SessionEntry | undefined | null): XWorkmateSessionMappingV1 | undefined { const pluginState = asRecord(entry?.pluginExtensions?.[XWORKMATE_PLUGIN_ID]); const raw = asRecord(pluginState?.[XWORKMATE_SESSION_EXTENSION_NAMESPACE]); if (!raw || raw.schemaVersion !== 1) { return undefined; } const appThreadKey = optionalString(raw.appThreadKey); const openclawSessionKey = optionalString(raw.openclawSessionKey); if (!appThreadKey || !openclawSessionKey) { return undefined; } return { schemaVersion: 1, appThreadKey, openclawSessionKey, expectedArtifactDirs: normalizeExpectedArtifactDirs(raw.expectedArtifactDirs), createdAt: optionalString(raw.createdAt) || new Date(0).toISOString(), updatedAt: optionalString(raw.updatedAt) || optionalString(raw.createdAt) || new Date(0).toISOString(), source: parseMappingSource(raw.source), }; } function writeMappingToPluginExtensions( current: SessionEntry["pluginExtensions"], mapping: XWorkmateSessionMappingV1 | undefined, ): SessionEntry["pluginExtensions"] { if (!mapping) { return current; } return { ...(current ?? {}), [XWORKMATE_PLUGIN_ID]: { ...(current?.[XWORKMATE_PLUGIN_ID] ?? {}), [XWORKMATE_SESSION_EXTENSION_NAMESPACE]: mapping, }, }; } function assertMappingCompatible( existing: XWorkmateSessionMappingV1, appThreadKey: string, openclawSessionKey: string, ) { if (existing.appThreadKey !== appThreadKey || existing.openclawSessionKey !== openclawSessionKey) { throw new Error("conflict: xworkmate session mapping already points to a different session"); } } function resolvePatchSessionEntry(api: OpenClawPluginApi): PatchSessionEntry | undefined { const runtimeSession = (api.runtime?.agent?.session ?? {}) as Record; const candidate = runtimeSession.patchSessionEntry; return typeof candidate === "function" ? (candidate as PatchSessionEntry) : undefined; } function resolveGetSessionEntry(api: OpenClawPluginApi): GetSessionEntry | undefined { const runtimeSession = (api.runtime?.agent?.session ?? {}) as Record; const candidate = runtimeSession.getSessionEntry; return typeof candidate === "function" ? (candidate as GetSessionEntry) : undefined; } function resolveListSessionEntries( api: OpenClawPluginApi, ): (() => Array<{ sessionKey: string; entry: SessionEntry }>) | undefined { const runtimeSession = (api.runtime?.agent?.session ?? {}) as Record; const candidate = runtimeSession.listSessionEntries; return typeof candidate === "function" ? (candidate as () => Array<{ sessionKey: string; entry: SessionEntry }>) : undefined; } function appStatusFromTaskStatus(status: string): string { if (status === "succeeded") { return "completed"; } if (status === "failed" || status === "timed_out" || status === "cancelled" || status === "lost") { return "failed"; } return "running"; } function parseMappingSource(value: unknown): XWorkmateSessionMappingSource { const source = optionalString(value); if (source === "session_start" || source === "bridge_prepare") { return source; } return "bridge_prepare"; } function requiredString(value: unknown, message: string): string { const text = optionalString(value); if (!text) { throw new Error(message); } return text; } function optionalString(value: unknown): string { if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") { return ""; } const text = String(value).trim(); return text === "" ? "" : text; } function asRecord(value: unknown): Record | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) { return undefined; } return value as Record; } function compactObject>(value: T): Partial { return Object.fromEntries( Object.entries(value).filter((entry) => entry[1] !== undefined && entry[1] !== ""), ) as Partial; }