From ba7b447af67ccbff18a25741820a5fb96ae4b09f Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 6 Jun 2026 07:56:32 +0800 Subject: [PATCH] Simplify OpenClaw multi-session plugin adapter --- dist/index.js | 67 +++-- dist/src/exportArtifacts.d.ts | 9 + dist/src/exportArtifacts.js | 52 +++- dist/src/taskState.d.ts | 83 +++--- dist/src/taskState.js | 508 ++++++++++++++++------------------ index.test.ts | 26 +- index.ts | 13 +- src/exportArtifacts.test.ts | 173 ++++++------ src/exportArtifacts.ts | 12 +- src/taskState.ts | 16 -- 10 files changed, 486 insertions(+), 473 deletions(-) diff --git a/dist/index.js b/dist/index.js index dd543f6..7dcfb71 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,6 +1,6 @@ import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime"; import { collectAndSnapshotXWorkmateArtifacts, exportXWorkmateArtifacts, prepareXWorkmateArtifacts, readXWorkmateArtifact, formatArtifactManifestMarkdown, } from "./src/exportArtifacts.js"; -import { createOrUpdateXWorkmateTaskRecord, createXWorkmateTaskStore, getXWorkmateTaskSnapshot, recordXWorkmateSessionMapping, registerXWorkmateDetachedTaskRuntime, registerXWorkmateSessionExtension, } from "./src/taskState.js"; +import { getXWorkmateTaskSnapshot, recordXWorkmateSessionMapping, registerXWorkmateSessionExtension, } from "./src/taskState.js"; function scopedGatewayParams(params) { const sessionScope = getPluginRuntimeGatewayRequestScope()?.sessionScope; const runScope = resolveRunScope({ sessionScope }); @@ -9,7 +9,7 @@ function scopedGatewayParams(params) { } return { ...params, - sessionKey: runScope.sessionKey, + openclawSessionKey: runScope.sessionKey, runId: runScope.runId, ...(runScope.workspaceDir ? { workspaceDir: runScope.workspaceDir } : {}), ...(runScope.artifactScope ? { artifactScope: runScope.artifactScope } : {}), @@ -29,6 +29,9 @@ function resolveRunScope(ctx) { ...(scope?.relativeTaskDirectory ? { artifactScope: scope.relativeTaskDirectory } : {}), }; } +function stringParam(value) { + return typeof value === "string" ? value.trim() : ""; +} const plugin = { id: "openclaw-multi-session-plugins", name: "openclaw-multi-session-plugins", @@ -37,57 +40,67 @@ const plugin = { }; export default plugin; function register(api) { - const taskStore = createXWorkmateTaskStore(); registerXWorkmateSessionExtension(api); - registerXWorkmateDetachedTaskRuntime(api, taskStore); - api.registerHook("session.start", async (event) => { + api.registerHook("session_start", async (event) => { try { const params = scopedGatewayParams(event?.context ?? event); - if (params.sessionKey && params.runId) { - createOrUpdateXWorkmateTaskRecord(taskStore, { - params, - status: "running", - progressSummary: "OpenClaw task is running", - }); + const openclawSessionKey = stringParam(params.openclawSessionKey); + if (openclawSessionKey && params.runId) { + const hookParams = { ...params, openclawSessionKey }; const prepared = await prepareXWorkmateArtifacts({ - params, + params: hookParams, config: api.config, pluginConfig: api.pluginConfig, }); await recordXWorkmateSessionMapping({ api, - taskStore, - params, + params: hookParams, artifactScope: prepared.artifactScope, + source: "session_start", }); } } catch (error) { - api.logger?.warn?.(`xworkmate session.start preparation failed: ${String(error)}`); + api.logger?.warn?.(`xworkmate session_start preparation failed: ${String(error)}`); } }, { name: "openclaw-multi-session-plugins.session-start" }); - api.registerGatewayMethod("xworkmate.tasks.get", async (opts) => { + api.registerGatewayMethod("xworkmate.session.prepare", async (opts) => { try { - const payload = await getXWorkmateTaskSnapshot({ + const params = scopedGatewayParams(opts.params); + const mapping = await recordXWorkmateSessionMapping({ api, - taskStore, - params: scopedGatewayParams(opts.params), + params, + source: "bridge_prepare", }); - opts.respond(true, payload, undefined); + const payload = await prepareXWorkmateArtifacts({ + params: { + ...params, + openclawSessionKey: mapping.openclawSessionKey, + expectedArtifactDirs: mapping.expectedArtifactDirs, + }, + config: api.config, + pluginConfig: api.pluginConfig, + }); + opts.respond(true, { + ...payload, + mapping, + appThreadKey: mapping.appThreadKey, + openclawSessionKey: mapping.openclawSessionKey, + expectedArtifactDirs: mapping.expectedArtifactDirs, + }, undefined); } catch (error) { opts.respond(false, undefined, { - code: "INVALID_REQUEST", + code: String(error).includes("conflict") ? "CONFLICT" : "INVALID_REQUEST", message: error instanceof Error ? error.message : String(error), }); } }); - api.registerGatewayMethod("xworkmate.artifacts.prepare", async (opts) => { + api.registerGatewayMethod("xworkmate.tasks.get", async (opts) => { try { - const payload = await prepareXWorkmateArtifacts({ + const payload = await getXWorkmateTaskSnapshot({ + api, params: scopedGatewayParams(opts.params), - config: api.config, - pluginConfig: api.pluginConfig, }); opts.respond(true, payload, undefined); } @@ -220,10 +233,10 @@ function createXWorkmateArtifactsTool(api, ctx) { throw new Error("runId required"); } const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir; - const { sessionKey: _ignoredSessionKey, runId: _ignoredRunId, workspaceDir: _ignoredWorkspaceDir, ...operationParams } = params; + const { sessionKey: _ignoredSessionKey, openclawSessionKey: _ignoredOpenclawSessionKey, runId: _ignoredRunId, workspaceDir: _ignoredWorkspaceDir, ...operationParams } = params; const baseParams = { ...operationParams, - sessionKey, + openclawSessionKey: sessionKey, runId, ...(workspaceDir ? { workspaceDir } : {}), ...(runScope?.artifactScope ? { artifactScope: runScope.artifactScope } : {}), diff --git a/dist/src/exportArtifacts.d.ts b/dist/src/exportArtifacts.d.ts index 28aec98..7992c2c 100644 --- a/dist/src/exportArtifacts.d.ts +++ b/dist/src/exportArtifacts.d.ts @@ -20,6 +20,8 @@ export type XWorkmateArtifactExport = { scopeKind: XWorkmateArtifactScopeKind; artifacts: XWorkmateArtifact[]; warnings: string[]; + expectedArtifactDirs: string[]; + expectedArtifactDirStatus: XWorkmateExpectedArtifactDirStatus[]; }; export type XWorkmateArtifactPrepare = { runId: string; @@ -31,6 +33,12 @@ export type XWorkmateArtifactPrepare = { artifactDirectory: string; relativeArtifactDirectory: string; warnings: string[]; + expectedArtifactDirs: string[]; + expectedArtifactDirStatus: XWorkmateExpectedArtifactDirStatus[]; +}; +export type XWorkmateExpectedArtifactDirStatus = { + relativePath: string; + exists: boolean; }; export type XWorkmateArtifactSnapshot = { runId: string; @@ -58,6 +66,7 @@ export declare function prepareXWorkmateArtifacts(input: ExportInput): Promise; export declare function exportXWorkmateArtifacts(input: ExportInput): Promise; export declare function readXWorkmateArtifact(input: ReadInput): Promise; +export declare function normalizeExpectedArtifactDirs(value: unknown): string[]; export declare function formatArtifactManifestMarkdown(input: { remoteWorkingDirectory: string; artifactScope?: string; diff --git a/dist/src/exportArtifacts.js b/dist/src/exportArtifacts.js index c854394..7dcbaa7 100644 --- a/dist/src/exportArtifacts.js +++ b/dist/src/exportArtifacts.js @@ -21,7 +21,8 @@ export async function prepareXWorkmateArtifacts(input) { const params = input.params ?? {}; const pluginConfig = input.pluginConfig ?? {}; const runId = requiredString(params.runId, "runId required"); - const sessionKey = requiredString(params.sessionKey, "sessionKey required"); + const sessionKey = requiredString(params.openclawSessionKey, "openclawSessionKey required"); + const expectedArtifactDirs = normalizeExpectedArtifactDirs(params.expectedArtifactDirs); const expectedArtifactScope = artifactScopeFor(sessionKey, runId); const requestedArtifactScope = optionalArtifactScope(params.artifactScope); if (requestedArtifactScope && requestedArtifactScope !== expectedArtifactScope) { @@ -37,6 +38,7 @@ export async function prepareXWorkmateArtifacts(input) { const artifactScope = expectedArtifactScope; const scopeRoot = resolveScopeRoot(workspaceRoot, artifactScope); await fs.mkdir(scopeRoot, { recursive: true }); + const expectedArtifactDirStatus = await expectedArtifactDirStatuses(workspaceRoot, expectedArtifactDirs); return { runId, sessionKey, @@ -47,13 +49,15 @@ export async function prepareXWorkmateArtifacts(input) { artifactDirectory: scopeRoot, relativeArtifactDirectory: artifactScope, warnings: [], + expectedArtifactDirs, + expectedArtifactDirStatus, }; } export async function collectAndSnapshotXWorkmateArtifacts(input) { const params = input.params ?? {}; const pluginConfig = input.pluginConfig ?? {}; const runId = requiredString(params.runId, "runId required"); - const sessionKey = requiredString(params.sessionKey, "sessionKey required"); + const sessionKey = requiredString(params.openclawSessionKey, "openclawSessionKey required"); const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0); const maxFiles = positiveInteger(params.maxFiles, pluginConfig.snapshotMaxFiles, DEFAULT_MAX_FILES); const expectedArtifactScope = artifactScopeFor(sessionKey, runId); @@ -120,7 +124,7 @@ export async function exportXWorkmateArtifacts(input) { const params = input.params ?? {}; const pluginConfig = input.pluginConfig ?? {}; const runId = requiredString(params.runId, "runId required"); - const sessionKey = requiredString(params.sessionKey, "sessionKey required"); + const sessionKey = requiredString(params.openclawSessionKey, "openclawSessionKey required"); const maxFiles = positiveInteger(params.maxFiles, pluginConfig.maxFiles, DEFAULT_MAX_FILES); const maxInlineBytes = nonNegativeInteger(params.maxInlineBytes, pluginConfig.maxInlineBytes, DEFAULT_MAX_INLINE_BYTES); const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0); @@ -133,6 +137,7 @@ export async function exportXWorkmateArtifacts(input) { }); const workspaceRoot = await fs.realpath(workspaceDir); const warnings = []; + const expectedDirs = normalizeExpectedArtifactDirs(params.expectedArtifactDirs); const expectedArtifactScope = artifactScopeFor(sessionKey, runId); const requestedArtifactScope = optionalArtifactScope(params.artifactScope); if (requestedArtifactScope && requestedArtifactScope !== expectedArtifactScope) { @@ -152,7 +157,9 @@ export async function exportXWorkmateArtifacts(input) { const scopeStat = await fs.stat(scopeRoot); effectiveSince = Math.min(sinceUnixMs, scopeStat.birthtimeMs || scopeStat.mtimeMs); } - catch { } + catch (error) { + warnings.push(`Unable to read artifact scope timestamp: ${String(error)}`); + } } const scopedCandidates = (await directoryExists(scopeRoot)) ? await collectCandidates({ @@ -165,9 +172,6 @@ export async function exportXWorkmateArtifacts(input) { }) : []; const candidates = scopedCandidates; - const expectedDirs = Array.isArray(params.expectedArtifactDirs) - ? params.expectedArtifactDirs.map((d) => String(d).trim()).filter(Boolean) - : []; if (candidates.length === 0 && expectedDirs.length > 0) { for (const dir of expectedDirs) { const dirPath = path.join(workspaceRoot, safeInputRelativePath(dir, "expectedArtifactDir")); @@ -245,6 +249,8 @@ export async function exportXWorkmateArtifacts(input) { scopeKind, artifacts, warnings, + expectedArtifactDirs: expectedDirs, + expectedArtifactDirStatus: await expectedArtifactDirStatuses(workspaceRoot, expectedDirs), }; return result; } @@ -252,7 +258,7 @@ export async function readXWorkmateArtifact(input) { const params = input.params ?? {}; const pluginConfig = input.pluginConfig ?? {}; const runId = requiredString(params.runId, "runId required"); - const sessionKey = requiredString(params.sessionKey, "sessionKey required"); + const sessionKey = requiredString(params.openclawSessionKey, "openclawSessionKey required"); const expectedArtifactScope = artifactScopeFor(sessionKey, runId); const expectedSessionScope = taskSessionScopeFor(sessionKey); const requestedArtifactRef = optionalString(params.artifactRef); @@ -347,9 +353,39 @@ export async function readXWorkmateArtifact(input) { scopeKind, artifacts: [artifact], warnings, + expectedArtifactDirs: [], + expectedArtifactDirStatus: [], }; return result; } +export function normalizeExpectedArtifactDirs(value) { + if (!Array.isArray(value)) { + return []; + } + const seen = new Set(); + const result = []; + for (const entry of value) { + const normalized = safeInputRelativePath(entry, "expectedArtifactDir"); + const withSlash = normalized.endsWith("/") ? normalized : `${normalized}/`; + if (seen.has(withSlash)) { + continue; + } + seen.add(withSlash); + result.push(withSlash); + } + return result; +} +async function expectedArtifactDirStatuses(workspaceRoot, expectedArtifactDirs) { + const statuses = []; + for (const relativePath of expectedArtifactDirs) { + const dirPath = path.join(workspaceRoot, safeInputRelativePath(relativePath, "expectedArtifactDir")); + statuses.push({ + relativePath, + exists: await directoryExists(dirPath), + }); + } + return statuses; +} export function formatArtifactManifestMarkdown(input) { const lines = [ "## XWorkmate artifacts", diff --git a/dist/src/taskState.d.ts b/dist/src/taskState.d.ts index ec19535..3ddcd14 100644 --- a/dist/src/taskState.d.ts +++ b/dist/src/taskState.d.ts @@ -1,57 +1,52 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -type XWorkmateTaskRecord = { - taskId: string; - runtime: "acp"; - taskKind: "xworkmate-openclaw"; - requesterSessionKey: string; - ownerKey: string; - scopeKind: "session"; - runId: string; - label: string; - task: string; - status: "queued" | "running" | "succeeded" | "failed" | "timed_out" | "cancelled" | "lost"; - deliveryStatus: "pending" | "delivered" | "session_queued" | "failed" | "parent_missing" | "not_applicable"; - notifyPolicy: "done_only" | "state_changes" | "silent"; - createdAt: number; - startedAt?: number; - endedAt?: number; - lastEventAt?: number; - error?: string; - progressSummary?: string; - terminalSummary?: string; - terminalOutcome?: "succeeded" | "blocked"; +export declare const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; +export declare const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping"; +export type XWorkmateTaskMetadataV1 = { + schemaVersion: 1; + appThreadKey: string; + openclawSessionKey?: string; + expectedArtifactDirs: string[]; + requestId?: string; + externalTaskId?: string; + createdAt: string; }; -type XWorkmateSessionMapping = { - appSessionKey: string; - openClawSessionKey: string; - appThreadId?: string; - sessionId?: string; - runId: string; - artifactScope?: 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[]; }; -export type XWorkmateTaskStore = { - records: Map; - sessionMappingsByAppKey: Map; - sessionMappingsByOpenClawKey: Map; -}; -export declare function createXWorkmateTaskStore(): XWorkmateTaskStore; export declare function registerXWorkmateSessionExtension(api: OpenClawPluginApi): void; export declare function recordXWorkmateSessionMapping(input: { api: OpenClawPluginApi; - taskStore: XWorkmateTaskStore; params: Record; artifactScope?: string; -}): Promise; -export declare function registerXWorkmateDetachedTaskRuntime(api: OpenClawPluginApi, taskStore: XWorkmateTaskStore): void; + source?: XWorkmateSessionMappingSource; +}): Promise; +export declare function normalizeXWorkmateTaskMetadataV1(input: Record): XWorkmateTaskMetadataV1; +export declare function normalizeExpectedArtifactDirs(value: unknown): string[]; +export declare function upsertXWorkmateSessionMapping(api: OpenClawPluginApi, input: { + metadata: XWorkmateTaskMetadataV1; + openclawSessionKey: string; + source: XWorkmateSessionMappingSource; +}): Promise; +export declare function readXWorkmateSessionMapping(api: OpenClawPluginApi, lookup: { + appThreadKey?: string; + openclawSessionKey?: string; +}): Promise; export declare function getXWorkmateTaskSnapshot(input: { api: OpenClawPluginApi; - taskStore: XWorkmateTaskStore; params: Record; }): Promise>; -export declare function createOrUpdateXWorkmateTaskRecord(input: XWorkmateTaskStore, options: { - params: Record; - status: XWorkmateTaskRecord["status"]; - progressSummary?: string; -}): XWorkmateTaskRecord; -export {}; diff --git a/dist/src/taskState.js b/dist/src/taskState.js index e762878..904e7f4 100644 --- a/dist/src/taskState.js +++ b/dist/src/taskState.js @@ -1,13 +1,6 @@ import { exportXWorkmateArtifacts } from "./exportArtifacts.js"; -const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate"; -const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; -export function createXWorkmateTaskStore() { - return { - records: new Map(), - sessionMappingsByAppKey: new Map(), - sessionMappingsByOpenClawKey: new Map(), - }; -} +export const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins"; +export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping"; export function registerXWorkmateSessionExtension(api) { const registerExtension = api.session?.state?.registerSessionExtension ?? api.registerSessionExtension; if (typeof registerExtension !== "function") { @@ -15,251 +8,269 @@ export function registerXWorkmateSessionExtension(api) { } registerExtension({ namespace: XWORKMATE_SESSION_EXTENSION_NAMESPACE, - description: "XWorkmate OpenClaw/App session key mapping for artifact and task recovery.", + description: "Durable XWorkmate app/OpenClaw session key mapping.", sessionEntrySlotKey: "xworkmate", project: (ctx) => { - const state = asRecord(ctx.state) ?? {}; - const appSessionKey = optionalString(state.appSessionKey) || - optionalString(state.appThreadId) || - optionalString(state.threadId) || - appSessionKeyFromOpenClawSessionKey(ctx.sessionKey); - const openClawSessionKey = optionalString(state.openClawSessionKey) || ctx.sessionKey; - return { - ...state, - appSessionKey, - openClawSessionKey, - sessionId: optionalString(state.sessionId) || optionalString(ctx.sessionId), - }; + const state = asRecord(ctx.state); + return state ?? {}; }, }); } export async function recordXWorkmateSessionMapping(input) { - const appSessionKey = requiredString(input.params.sessionKey || input.params.appSessionKey, "sessionKey required"); - const runId = requiredString(input.params.runId, "runId required"); - const openClawSessionKey = optionalString(input.params.openClawSessionKey) || - optionalString(input.params.openClawSessionId) || - agentMainSessionKeyFor(appSessionKey); - const expectedArtifactDirs = stringList(input.params.expectedArtifactDirs); - const mapping = compactObject({ - appSessionKey, - openClawSessionKey, - appThreadId: optionalString(input.params.threadId) || appSessionKey, - sessionId: optionalString(input.params.sessionId), - runId, - artifactScope: input.artifactScope || optionalString(input.params.artifactScope), - expectedArtifactDirs: expectedArtifactDirs.length > 0 ? expectedArtifactDirs : undefined, - }); - input.taskStore.sessionMappingsByAppKey.set(appSessionKey, mapping); - input.taskStore.sessionMappingsByOpenClawKey.set(openClawSessionKey, mapping); - const patchSessionExtension = resolvePatchSessionExtension(input.api); - if (!patchSessionExtension) { - // Legacy fallback owner: this plugin. Scope: tests and OpenClaw hosts that do not expose - // session extension patching yet. Exit: remove this map once 2026.6.1+ hosts expose the patch - // method on the public plugin API in all supported deployments. - return; - } - await patchSessionExtension({ - key: openClawSessionKey, - sessionKey: openClawSessionKey, - pluginId: XWORKMATE_PLUGIN_ID, - namespace: XWORKMATE_SESSION_EXTENSION_NAMESPACE, - value: mapping, + 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", }); } -export function registerXWorkmateDetachedTaskRuntime(api, taskStore) { - const registerRuntime = api.registerDetachedTaskRuntime; - if (typeof registerRuntime !== "function") { - return; +export function normalizeXWorkmateTaskMetadataV1(input) { + 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"); } - registerRuntime({ - createQueuedTaskRun: (params) => createOrUpdateXWorkmateTaskRecord(taskStore, { params, status: "queued" }), - createRunningTaskRun: (params) => createOrUpdateXWorkmateTaskRecord(taskStore, { params, status: "running" }), - startTaskRunByRunId: (params) => updateXWorkmateTaskRecordsByRunId(taskStore, params, { status: "running", startedAt: Date.now() }), - recordTaskRunProgressByRunId: (params) => updateXWorkmateTaskRecordsByRunId(taskStore, params, { - lastEventAt: Date.now(), - progressSummary: optionalString(params.progressSummary) || optionalString(params.eventSummary), - }), - finalizeTaskRunByRunId: (params) => updateXWorkmateTaskRecordsByRunId(taskStore, params, terminalPatch(params)), - completeTaskRunByRunId: (params) => updateXWorkmateTaskRecordsByRunId(taskStore, params, { - status: "succeeded", - endedAt: numberOrNow(params.endedAt), - lastEventAt: numberOrNow(params.lastEventAt), - terminalSummary: optionalString(params.terminalSummary) || optionalString(params.progressSummary), - terminalOutcome: "succeeded", - }), - failTaskRunByRunId: (params) => updateXWorkmateTaskRecordsByRunId(taskStore, params, { - status: taskStatusFrom(params.status, "failed"), - endedAt: numberOrNow(params.endedAt), - lastEventAt: numberOrNow(params.lastEventAt), - error: optionalString(params.error), - terminalSummary: optionalString(params.terminalSummary) || optionalString(params.progressSummary), - }), - setDetachedTaskDeliveryStatusByRunId: (params) => updateXWorkmateTaskRecordsByRunId(taskStore, params, { - deliveryStatus: deliveryStatusFrom(params.deliveryStatus, "delivered"), - error: optionalString(params.error), - }), - cancelDetachedTaskRunById: async (params) => { - const taskId = optionalString(params.taskId); - const record = taskId ? findXWorkmateTaskByTaskId(taskStore, taskId) : undefined; - if (!record) { - return { found: false, cancelled: false }; + const appThreadKey = requiredString(envelope.appThreadKey, "appThreadKey required"); + const createdAt = optionalString(envelope.createdAt) || new Date().toISOString(); + return compactObject({ + schemaVersion: 1, + appThreadKey, + openclawSessionKey: optionalString(envelope.openclawSessionKey), + expectedArtifactDirs: normalizeExpectedArtifactDirs(envelope.expectedArtifactDirs), + requestId: optionalString(envelope.requestId), + externalTaskId: optionalString(envelope.externalTaskId ?? envelope.taskId), + createdAt, + }); +} +export function normalizeExpectedArtifactDirs(value) { + if (!Array.isArray(value)) { + return []; + } + const seen = new Set(); + const result = []; + for (const entry of value) { + const text = optionalString(entry).replaceAll("\\", "/").replace(/^\.\/+/u, ""); + if (!text || seen.has(text)) { + continue; + } + if (text.startsWith("/") || /^[A-Za-z]:\//u.test(text) || text.split("/").includes("..")) { + throw new Error("expectedArtifactDirs must be relative paths without traversal"); + } + const normalized = text.endsWith("/") ? text : `${text}/`; + if (!seen.has(normalized)) { + seen.add(normalized); + result.push(normalized); + } + } + return result; +} +export async function upsertXWorkmateSessionMapping(api, input) { + const patchSessionEntry = resolvePatchSessionEntry(api); + if (!patchSessionEntry) { + throw new Error("OpenClaw runtime session patch API is unavailable"); + } + const now = new Date().toISOString(); + let mapping; + await patchSessionEntry({ + sessionKey: input.openclawSessionKey, + 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, + }; } - record.status = "cancelled"; - record.endedAt = Date.now(); - record.lastEventAt = record.endedAt; - return { found: true, cancelled: true, reason: optionalString(params.reason), task: record }; + else { + mapping = compactObject({ + schemaVersion: 1, + appThreadKey: input.metadata.appThreadKey, + openclawSessionKey: input.openclawSessionKey, + expectedArtifactDirs: input.metadata.expectedArtifactDirs, + createdAt: input.metadata.createdAt || now, + updatedAt: now, + source: input.source, + }); + } + return { + pluginExtensions: writeMappingToPluginExtensions(entry.pluginExtensions, mapping), + }; }, }); + if (!mapping) { + throw new Error("failed to write xworkmate session mapping"); + } + return mapping; +} +export async function readXWorkmateSessionMapping(api, lookup) { + 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) { - const sessionKey = requiredString(input.params.sessionKey, "sessionKey required"); - const runId = requiredString(input.params.runId, "runId required"); - const mapping = resolveSessionMapping(input.taskStore, input.params, sessionKey); - const openClawSessionKey = mapping?.openClawSessionKey || optionalString(input.params.openClawSessionKey) || agentMainSessionKeyFor(sessionKey); - const appSessionKey = mapping?.appSessionKey || sessionKey; - const nativeTask = resolveNativeTask(input.api, openClawSessionKey, runId) || resolveNativeTask(input.api, sessionKey, runId); - const storedTask = findXWorkmateTask(input.taskStore, sessionKey, runId); - const exported = await exportXWorkmateArtifacts({ - params: input.params, - config: input.api.config, - pluginConfig: input.api.pluginConfig, + const params = input.params ?? {}; + const appThreadKey = optionalString(params.appThreadKey); + const explicitOpenclawSessionKey = optionalString(params.openclawSessionKey); + const mapping = await readXWorkmateSessionMapping(input.api, { + appThreadKey, + openclawSessionKey: explicitOpenclawSessionKey, }); - const task = nativeTask || storedTask; - const taskStatus = normalizeTaskStatus(optionalString(task.status), exported.artifacts.length > 0); - if (storedTask && taskStatus === "succeeded" && storedTask.status !== "succeeded") { - storedTask.status = "succeeded"; - storedTask.endedAt = Date.now(); - storedTask.lastEventAt = storedTask.endedAt; - storedTask.terminalOutcome = "succeeded"; + 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, + }); + if (!task) { + const code = 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.status) || "running"; + const includeArtifacts = params.includeArtifacts !== false; + const exported = includeArtifacts + ? await exportXWorkmateArtifacts({ + params: { + ...params, + openclawSessionKey, + runId: runId || optionalString(task.runId) || optionalString(task.taskId), + expectedArtifactDirs: mapping?.expectedArtifactDirs ?? normalizeExpectedArtifactDirs(params.expectedArtifactDirs), + includeContent: params.includeContent ?? false, + }, + config: input.api.config, + pluginConfig: input.api.pluginConfig, + }) + : undefined; return { success: true, status: appStatusFromTaskStatus(taskStatus), taskStatus, mode: "gateway-chat", - sessionKey, - openClawSessionKey, - appSessionKey, - runId, + mapping, + appThreadKey: mapping?.appThreadKey ?? appThreadKey, + openclawSessionKey, + runId: runId || optionalString(task.runId), + taskId: taskId || optionalString(task.taskId), task, - artifactScope: exported.artifactScope, - remoteWorkingDirectory: exported.remoteWorkingDirectory, - remoteWorkspaceRefKind: exported.remoteWorkspaceRefKind, - scopeKind: exported.scopeKind, - artifacts: exported.artifacts, - warnings: exported.warnings, - artifactCount: exported.artifacts.length, + expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [], + artifactScope: exported?.artifactScope, + remoteWorkingDirectory: exported?.remoteWorkingDirectory, + remoteWorkspaceRefKind: exported?.remoteWorkspaceRefKind, + scopeKind: exported?.scopeKind, + artifacts: exported?.artifacts ?? [], + warnings: exported?.warnings ?? [], + artifactCount: exported?.artifacts.length ?? 0, }; } -export function createOrUpdateXWorkmateTaskRecord(input, options) { - const sessionKey = requiredString(options.params.sessionKey || options.params.requesterSessionKey, "sessionKey required"); - const runId = requiredString(options.params.runId, "runId required"); - const key = taskRecordKey(sessionKey, runId); - const now = Date.now(); - const existing = input.records.get(key); - if (existing) { - existing.status = options.status; - existing.lastEventAt = now; - if (options.status === "running" && !existing.startedAt) { - existing.startedAt = now; - } - if (options.progressSummary) { - existing.progressSummary = options.progressSummary; - } - return existing; - } - const record = { - taskId: `xworkmate:${safeTaskIdSegment(sessionKey)}:${safeTaskIdSegment(runId)}`, - runtime: "acp", - taskKind: "xworkmate-openclaw", - requesterSessionKey: optionalString(options.params.openClawSessionKey) || agentMainSessionKeyFor(sessionKey), - ownerKey: sessionKey, - scopeKind: "session", - runId, - label: optionalString(options.params.label) || "XWorkmate OpenClaw task", - task: optionalString(options.params.taskPrompt) || optionalString(options.params.task) || "XWorkmate OpenClaw task", - status: options.status, - deliveryStatus: "pending", - notifyPolicy: "state_changes", - createdAt: now, - startedAt: options.status === "running" ? now : undefined, - lastEventAt: now, - progressSummary: options.progressSummary, - }; - input.records.set(key, record); - return record; -} -function updateXWorkmateTaskRecordsByRunId(input, params, patch) { - const runId = optionalString(params.runId); - const sessionKey = optionalString(params.sessionKey || params.requesterSessionKey); - const records = [...input.records.values()].filter((record) => { - if (runId && record.runId !== runId) { - return false; - } - if (sessionKey && record.ownerKey !== sessionKey && record.requesterSessionKey !== sessionKey) { - return false; - } - return true; - }); - for (const record of records) { - Object.assign(record, compactObject(patch)); - } - return records; -} -function resolveNativeTask(api, sessionKey, runId) { +function resolveNativeTask(api, input) { try { - const bound = api.runtime?.tasks?.runs?.bindSession?.({ sessionKey }); - const resolved = bound?.resolve?.(runId) || bound?.get?.(runId); + const bound = api.runtime?.tasks?.runs?.bindSession?.({ sessionKey: input.openclawSessionKey }); + 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 task native registry lookup failed: sessionKey=${sessionKey} runId=${runId} error=${String(error)}`); + api.logger?.warn?.(`xworkmate native task lookup failed: sessionKey=${input.openclawSessionKey} error=${String(error)}`); return undefined; } } -function resolveSessionMapping(input, params, sessionKey) { - const explicitOpenClawKey = optionalString(params.openClawSessionKey); - if (explicitOpenClawKey) { - const byOpenClaw = input.sessionMappingsByOpenClawKey.get(explicitOpenClawKey); - if (byOpenClaw) { - return byOpenClaw; - } - } - return input.sessionMappingsByAppKey.get(sessionKey) || input.sessionMappingsByOpenClawKey.get(sessionKey); -} -function findXWorkmateTask(input, sessionKey, runId) { - return input.records.get(taskRecordKey(sessionKey, runId)); -} -function findXWorkmateTaskByTaskId(input, taskId) { - return [...input.records.values()].find((record) => record.taskId === taskId); -} -function taskRecordKey(sessionKey, runId) { - return `${sessionKey}\u0000${runId}`; -} -function appSessionKeyFromOpenClawSessionKey(sessionKey) { - return sessionKey.startsWith("agent:main:") ? sessionKey.slice("agent:main:".length) : sessionKey; -} -function agentMainSessionKeyFor(sessionKey) { - return sessionKey.startsWith("agent:") ? sessionKey : `agent:main:${sessionKey}`; -} -function terminalPatch(params) { - const status = taskStatusFrom(params.status, "succeeded"); +function lookupError(code, message, mapping) { return { - status, - endedAt: numberOrNow(params.endedAt), - lastEventAt: numberOrNow(params.lastEventAt), - error: optionalString(params.error), - progressSummary: optionalString(params.progressSummary), - terminalSummary: optionalString(params.terminalSummary), - terminalOutcome: status === "succeeded" ? "succeeded" : "blocked", + ok: false, + code, + message, + ...(mapping ? { mapping, expectedArtifactDirs: mapping.expectedArtifactDirs } : {}), }; } -function normalizeTaskStatus(status, hasArtifacts) { - const normalized = taskStatusFrom(status, hasArtifacts ? "succeeded" : "running"); - if (normalized === "running" && hasArtifacts) { - return "succeeded"; +function readMappingFromEntry(entry) { + const pluginState = asRecord(entry?.pluginExtensions?.[XWORKMATE_PLUGIN_ID]); + const raw = asRecord(pluginState?.[XWORKMATE_SESSION_EXTENSION_NAMESPACE]); + if (!raw || raw.schemaVersion !== 1) { + return undefined; } - return normalized; + 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, mapping) { + if (!mapping) { + return current; + } + return { + ...(current ?? {}), + [XWORKMATE_PLUGIN_ID]: { + ...(current?.[XWORKMATE_PLUGIN_ID] ?? {}), + [XWORKMATE_SESSION_EXTENSION_NAMESPACE]: mapping, + }, + }; +} +function assertMappingCompatible(existing, appThreadKey, openclawSessionKey) { + if (existing.appThreadKey !== appThreadKey || existing.openclawSessionKey !== openclawSessionKey) { + throw new Error("conflict: xworkmate session mapping already points to a different session"); + } +} +function resolvePatchSessionEntry(api) { + const runtimeSession = (api.runtime?.agent?.session ?? {}); + const candidate = runtimeSession.patchSessionEntry; + return typeof candidate === "function" ? candidate : undefined; +} +function resolveGetSessionEntry(api) { + const runtimeSession = (api.runtime?.agent?.session ?? {}); + const candidate = runtimeSession.getSessionEntry; + return typeof candidate === "function" ? candidate : undefined; +} +function resolveListSessionEntries(api) { + const runtimeSession = (api.runtime?.agent?.session ?? {}); + const candidate = runtimeSession.listSessionEntries; + return typeof candidate === "function" + ? candidate + : undefined; } function appStatusFromTaskStatus(status) { if (status === "succeeded") { @@ -270,38 +281,12 @@ function appStatusFromTaskStatus(status) { } return "running"; } -function taskStatusFrom(value, fallback) { - const status = optionalString(value); - if (status === "queued" || - status === "running" || - status === "succeeded" || - status === "failed" || - status === "timed_out" || - status === "cancelled" || - status === "lost") { - return status; +function parseMappingSource(value) { + const source = optionalString(value); + if (source === "session_start" || source === "bridge_prepare") { + return source; } - return fallback; -} -function deliveryStatusFrom(value, fallback) { - const status = optionalString(value); - if (status === "pending" || - status === "delivered" || - status === "session_queued" || - status === "failed" || - status === "parent_missing" || - status === "not_applicable") { - return status; - } - return fallback; -} -function resolvePatchSessionExtension(api) { - const stateApi = (api.session?.state ?? {}); - const apiRecord = api; - const candidate = stateApi.patchSessionExtension || apiRecord.patchSessionExtension; - return typeof candidate === "function" - ? candidate - : undefined; + return "bridge_prepare"; } function requiredString(value, message) { const text = optionalString(value); @@ -317,26 +302,6 @@ function optionalString(value) { const text = String(value).trim(); return text === "" ? "" : text; } -function stringList(value) { - if (!Array.isArray(value)) { - return []; - } - const seen = new Set(); - const result = []; - for (const entry of value) { - const text = optionalString(entry); - if (!text || seen.has(text)) { - continue; - } - seen.add(text); - result.push(text); - } - return result; -} -function numberOrNow(value) { - const parsed = Number(value); - return Number.isFinite(parsed) && parsed > 0 ? parsed : Date.now(); -} function asRecord(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return undefined; @@ -346,6 +311,3 @@ function asRecord(value) { function compactObject(value) { return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== undefined && entry[1] !== "")); } -function safeTaskIdSegment(value) { - return value.replace(/[^A-Za-z0-9._:-]+/g, "_"); -} diff --git a/index.test.ts b/index.test.ts index a70f07d..612c0ea 100644 --- a/index.test.ts +++ b/index.test.ts @@ -92,12 +92,12 @@ describe("plugin registration", () => { openclawSessionKey: "thread-main", runId: "turn-1", }); - console.log(prepared); expect(prepared.ok).toBe(true); + expect(prepared.ok).toBe(true); expect(prepared.payload?.artifactScope).toBe("tasks/thread-main/turn-1"); const artifactDirectory = String(prepared.payload?.artifactDirectory); const emptyExport = await callGatewayMethod(methods, "xworkmate.artifacts.export", { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.payload?.artifactScope, }); @@ -109,7 +109,7 @@ describe("plugin registration", () => { await fs.promises.writeFile(path.join(artifactDirectory, "reports", "final.md"), "final"); const listed = await callGatewayMethod(methods, "xworkmate.artifacts.list", { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.payload?.artifactScope, }); @@ -119,7 +119,7 @@ describe("plugin registration", () => { expect(listedArtifacts[0]).not.toHaveProperty("content"); const read = await callGatewayMethod(methods, "xworkmate.artifacts.read", { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.payload?.artifactScope, relativePath: "reports/final.md", @@ -128,7 +128,7 @@ describe("plugin registration", () => { expect(read.payload?.artifacts).toMatchObject([{ relativePath: "reports/final.md", encoding: "base64" }]); const unprepared = await callGatewayMethod(methods, "xworkmate.artifacts.export", { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-unprepared", }); expect(unprepared.ok).toBe(true); @@ -211,15 +211,21 @@ describe("plugin registration", () => { sessionEntrySlotKey: "xworkmate", }); const projected = (sessionExtensions[0]?.project as (ctx: Record) => unknown)({ - sessionKey: "draft:1780636411666238-3", + openclawSessionKey: "draft:1780636411666238-3", state: {}, }); expect(projected).toMatchObject({}); expect(detachedRuntimes).toHaveLength(0); + await hooks.get("session_start")?.({ + appThreadKey: "draft:legacy-session-key-only", + sessionKey: "draft:legacy-session-key-only", + runId: "turn-legacy", + }); + expect(sessionExtensionPatches).toHaveLength(0); + await hooks.get("session_start")?.({ appThreadKey: "draft:1780636411666238-3", - sessionKey: "draft-1780636411666238-3", openclawSessionKey: "draft:1780636411666238-3", threadId: "draft-1780636411666238-3", runId: "turn-1", @@ -302,11 +308,11 @@ describe("plugin registration", () => { it("uses host context scope for the optional agent tool", async () => { const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-tool-")); const current = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); const other = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-2" }, + params: { openclawSessionKey: "thread-main", runId: "turn-2" }, pluginConfig: { workspaceDir: root }, }); await fs.promises.writeFile(path.join(current.artifactDirectory, "current.txt"), "current"); @@ -340,7 +346,7 @@ describe("plugin registration", () => { }); const result = await tool.execute("call-1", { action: "list", - sessionKey: "thread-other", + openclawSessionKey: "thread-other", runId: "turn-2", workspaceDir: "/", }); diff --git a/index.ts b/index.ts index 24979b3..ec5ab8b 100644 --- a/index.ts +++ b/index.ts @@ -14,7 +14,6 @@ import { import { getXWorkmateTaskSnapshot, recordXWorkmateSessionMapping, - registerXWorkmateDetachedTaskRuntime, registerXWorkmateSessionExtension, } from "./src/taskState.js"; @@ -53,7 +52,7 @@ function scopedGatewayParams(params: Record): Record { try { const params = scopedGatewayParams(event?.context ?? event); - const openclawSessionKey = stringParam(params.openclawSessionKey) || stringParam(params.sessionKey); + const openclawSessionKey = stringParam(params.openclawSessionKey); if (openclawSessionKey && params.runId) { const hookParams = { ...params, openclawSessionKey }; const prepared = await prepareXWorkmateArtifacts({ @@ -113,7 +110,6 @@ function register(api: OpenClawPluginApi) { }); await recordXWorkmateSessionMapping({ api, - taskStore, params: hookParams, artifactScope: prepared.artifactScope, source: "session_start", @@ -131,7 +127,6 @@ function register(api: OpenClawPluginApi) { const params = scopedGatewayParams(opts.params); const mapping = await recordXWorkmateSessionMapping({ api, - taskStore, params, source: "bridge_prepare", }); @@ -167,7 +162,6 @@ function register(api: OpenClawPluginApi) { try { const payload = await getXWorkmateTaskSnapshot({ api, - taskStore, params: scopedGatewayParams(opts.params), }); opts.respond(true, payload, undefined); @@ -303,13 +297,14 @@ function createXWorkmateArtifactsTool( const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir; const { sessionKey: _ignoredSessionKey, + openclawSessionKey: _ignoredOpenclawSessionKey, runId: _ignoredRunId, workspaceDir: _ignoredWorkspaceDir, ...operationParams } = params; const baseParams = { ...operationParams, - sessionKey, + openclawSessionKey: sessionKey, runId, ...(workspaceDir ? { workspaceDir } : {}), ...(runScope?.artifactScope ? { artifactScope: runScope.artifactScope } : {}), diff --git a/src/exportArtifacts.test.ts b/src/exportArtifacts.test.ts index 25d1ed3..5ecbf1a 100644 --- a/src/exportArtifacts.test.ts +++ b/src/exportArtifacts.test.ts @@ -15,11 +15,11 @@ describe("exportXWorkmateArtifacts", () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const first = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); const second = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-2" }, + params: { openclawSessionKey: "thread-main", runId: "turn-2" }, pluginConfig: { workspaceDir: root }, }); @@ -31,11 +31,22 @@ describe("exportXWorkmateArtifacts", () => { expect(first.scopeKind).toBe("task"); }); + it("rejects legacy sessionKey artifact params", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); + + await expect( + prepareXWorkmateArtifacts({ + params: { sessionKey: "thread-main", runId: "turn-1" }, + pluginConfig: { workspaceDir: root }, + }), + ).rejects.toThrow("openclawSessionKey required"); + }); + it("normalizes task scope segments like the OpenClaw session scope runtime", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "agent::main:main", runId: "run alpha" }, + params: { openclawSessionKey: "agent::main:main", runId: "run alpha" }, pluginConfig: { workspaceDir: root }, }); @@ -45,7 +56,7 @@ describe("exportXWorkmateArtifacts", () => { it("exports changed files with metadata and base64 content", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "run-1" }, + params: { openclawSessionKey: "thread-main", runId: "run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.mkdir(path.join(prepared.artifactDirectory, "reports"), { recursive: true }); @@ -55,7 +66,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", sinceUnixMs: stat.mtimeMs - 1, }, @@ -84,7 +95,7 @@ describe("exportXWorkmateArtifacts", () => { const prepared = await prepareXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-expected", expectedArtifactDirs: ["artifacts/", "assets/images"], }, @@ -92,7 +103,7 @@ describe("exportXWorkmateArtifacts", () => { }); const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-expected", artifactScope: prepared.artifactScope, expectedArtifactDirs: ["artifacts/", "assets/images"], @@ -114,7 +125,7 @@ describe("exportXWorkmateArtifacts", () => { await expect( prepareXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-unsafe", expectedArtifactDirs: ["../outside"], }, @@ -130,7 +141,7 @@ describe("exportXWorkmateArtifacts", () => { const mediaRoot = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-media-")); const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-global-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "run-1" }, + params: { openclawSessionKey: "thread-main", runId: "run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.mkdir(path.join(mediaRoot, "browser"), { recursive: true }); @@ -144,7 +155,7 @@ describe("exportXWorkmateArtifacts", () => { const snapshot = await collectAndSnapshotXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", artifactScope: prepared.artifactScope, sinceUnixMs: snapshotSinceUnixMs, @@ -165,7 +176,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", artifactScope: prepared.artifactScope, includeContent: false, @@ -182,7 +193,7 @@ describe("exportXWorkmateArtifacts", () => { it("skips excluded directories and symlinks", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "run-1" }, + params: { openclawSessionKey: "thread-main", runId: "run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.mkdir(path.join(prepared.artifactDirectory, ".git"), { recursive: true }); @@ -194,7 +205,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", }, pluginConfig: { workspaceDir: root }, @@ -207,7 +218,7 @@ describe("exportXWorkmateArtifacts", () => { it("applies artifact-ignore.md inside the current task scope", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "run-1" }, + params: { openclawSessionKey: "thread-main", runId: "run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.mkdir(path.join(prepared.artifactDirectory, "tmp"), { recursive: true }); @@ -234,7 +245,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", }, pluginConfig: { workspaceDir: root }, @@ -246,11 +257,11 @@ describe("exportXWorkmateArtifacts", () => { it("exports only files inside a task artifact scope", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const first = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); const second = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-2" }, + params: { openclawSessionKey: "thread-main", runId: "turn-2" }, pluginConfig: { workspaceDir: root }, }); await fs.mkdir(path.join(first.artifactDirectory, "reports"), { recursive: true }); @@ -260,7 +271,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: first.artifactScope, }, @@ -279,7 +290,7 @@ describe("exportXWorkmateArtifacts", () => { it("exports nested dist and build deliverables inside the task scope", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await fs.mkdir(path.join(prepared.artifactDirectory, "dist"), { recursive: true }); @@ -289,7 +300,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", maxInlineBytes: 0, }, @@ -305,11 +316,11 @@ describe("exportXWorkmateArtifacts", () => { it("uses the current task scope when artifactScope is omitted", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const current = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); const other = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-2" }, + params: { openclawSessionKey: "thread-main", runId: "turn-2" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(root, "global.txt"), "global"); @@ -318,7 +329,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", }, pluginConfig: { workspaceDir: root }, @@ -332,14 +343,14 @@ describe("exportXWorkmateArtifacts", () => { it("does not scan the workspace root without a current-run timestamp", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(root, "global.txt"), "global"); const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", }, pluginConfig: { workspaceDir: root }, @@ -356,7 +367,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "draft-article", + openclawSessionKey: "draft-article", runId: "openclaw-run-1", sinceUnixMs, maxInlineBytes: 0, @@ -372,7 +383,7 @@ describe("exportXWorkmateArtifacts", () => { it("exports explicitly expected artifact dirs when the task scope is empty", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "draft-article", runId: "openclaw-run-1" }, + params: { openclawSessionKey: "draft-article", runId: "openclaw-run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.mkdir(path.join(root, "assets", "images"), { recursive: true }); @@ -383,7 +394,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "draft-article", + openclawSessionKey: "draft-article", runId: "openclaw-run-1", artifactScope: prepared.artifactScope, expectedArtifactDirs: ["assets/images", "reports"], @@ -403,7 +414,7 @@ describe("exportXWorkmateArtifacts", () => { it("keeps scoped artifacts authoritative over expected artifact dirs", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "draft-article", runId: "openclaw-run-1" }, + params: { openclawSessionKey: "draft-article", runId: "openclaw-run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.mkdir(path.join(prepared.artifactDirectory, "reports"), { recursive: true }); @@ -413,7 +424,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "draft-article", + openclawSessionKey: "draft-article", runId: "openclaw-run-1", artifactScope: prepared.artifactScope, expectedArtifactDirs: ["reports"], @@ -428,7 +439,7 @@ describe("exportXWorkmateArtifacts", () => { it("does not adopt old workspace root files into a later task scope", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(root, "old-root.md"), "old"); @@ -436,7 +447,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", sinceUnixMs: stat.mtimeMs + 10_000, }, @@ -450,18 +461,18 @@ describe("exportXWorkmateArtifacts", () => { it("rejects scoped exports that do not match the requested session/run", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const first = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-2" }, + params: { openclawSessionKey: "thread-main", runId: "turn-2" }, pluginConfig: { workspaceDir: root }, }); await expect( exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-2", artifactScope: first.artifactScope, }, @@ -473,11 +484,11 @@ describe("exportXWorkmateArtifacts", () => { it("does not adopt old workspace files when the scoped directory is empty", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); const otherTask = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-2" }, + params: { openclawSessionKey: "thread-main", runId: "turn-2" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(root, "existing.pdf"), "pdf"); @@ -488,7 +499,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.artifactScope, sinceUnixMs: stat.mtimeMs + 10_000, @@ -505,11 +516,11 @@ describe("exportXWorkmateArtifacts", () => { it("does not borrow previous session task files when current task scope is empty", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const previousTask = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-previous" }, + params: { openclawSessionKey: "thread-main", runId: "turn-previous" }, pluginConfig: { workspaceDir: root }, }); await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-follow-up" }, + params: { openclawSessionKey: "thread-main", runId: "turn-follow-up" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(previousTask.artifactDirectory, "k8s-networking.pdf"), "pdf"); @@ -517,7 +528,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-follow-up", sinceUnixMs: Date.now() + 10_000, }, @@ -534,7 +545,7 @@ describe("exportXWorkmateArtifacts", () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); await prepareXWorkmateArtifacts({ params: { - sessionKey: "draft:1779524982823421-3", + openclawSessionKey: "draft:1779524982823421-3", runId: "turn-1779685283403237342", }, pluginConfig: { workspaceDir: root }, @@ -561,7 +572,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "draft:1779524982823421-3", + openclawSessionKey: "draft:1779524982823421-3", runId: "turn-1779685283403237342", sinceUnixMs: Date.now() + 10_000, }, @@ -594,7 +605,7 @@ describe("exportXWorkmateArtifacts", () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); await prepareXWorkmateArtifacts({ params: { - sessionKey: "draft:1779524982823421-3", + openclawSessionKey: "draft:1779524982823421-3", runId: "turn-1779685283403237342", }, pluginConfig: { workspaceDir: root }, @@ -605,7 +616,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "draft:1779524982823421-3", + openclawSessionKey: "draft:1779524982823421-3", runId: "turn-1779685283403237342", }, pluginConfig: { workspaceDir: root }, @@ -630,15 +641,15 @@ describe("exportXWorkmateArtifacts", () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await Promise.all([ prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-a", runId: "turn-1" }, + params: { openclawSessionKey: "thread-a", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }), prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-b", runId: "turn-1" }, + params: { openclawSessionKey: "thread-b", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }), prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-a", runId: "turn-2" }, + params: { openclawSessionKey: "thread-a", runId: "turn-2" }, pluginConfig: { workspaceDir: root }, }), ]); @@ -648,15 +659,15 @@ describe("exportXWorkmateArtifacts", () => { const results = await Promise.all([ exportXWorkmateArtifacts({ - params: { sessionKey: "thread-a", runId: "turn-1" }, + params: { openclawSessionKey: "thread-a", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }), exportXWorkmateArtifacts({ - params: { sessionKey: "thread-b", runId: "turn-1" }, + params: { openclawSessionKey: "thread-b", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }), exportXWorkmateArtifacts({ - params: { sessionKey: "thread-a", runId: "turn-2" }, + params: { openclawSessionKey: "thread-a", runId: "turn-2" }, pluginConfig: { workspaceDir: root }, }), ]); @@ -672,14 +683,14 @@ describe("exportXWorkmateArtifacts", () => { it("leaves oversized artifacts out of inline content", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "run-1" }, + params: { openclawSessionKey: "thread-main", runId: "run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(prepared.artifactDirectory, "large.pdf"), Buffer.from("large-content")); const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", maxInlineBytes: 2, }, @@ -695,14 +706,14 @@ describe("exportXWorkmateArtifacts", () => { it("can list artifacts without inline content", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "run-1" }, + params: { openclawSessionKey: "thread-main", runId: "run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(prepared.artifactDirectory, "small.txt"), "small"); const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", maxInlineBytes: 0, }, @@ -718,7 +729,7 @@ describe("exportXWorkmateArtifacts", () => { it("limits exported files", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "run-1" }, + params: { openclawSessionKey: "thread-main", runId: "run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(prepared.artifactDirectory, "a.txt"), "a"); @@ -726,7 +737,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", maxFiles: 1, }, @@ -742,14 +753,14 @@ describe("exportXWorkmateArtifacts", () => { const agentRoot = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-agent-")); await fs.writeFile(path.join(mainRoot, "main.txt"), "main"); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "agent:research:thread-1", runId: "run-1" }, + params: { openclawSessionKey: "agent:research:thread-1", runId: "run-1" }, pluginConfig: { workspaceDir: agentRoot }, }); await fs.writeFile(path.join(prepared.artifactDirectory, "agent.txt"), "agent"); const result = await exportXWorkmateArtifacts({ params: { - sessionKey: "agent:research:thread-1", + openclawSessionKey: "agent:research:thread-1", runId: "run-1", }, config: { @@ -771,7 +782,7 @@ describe("exportXWorkmateArtifacts", () => { await expect( readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", relativePath: "reports/final.txt", }, @@ -783,7 +794,7 @@ describe("exportXWorkmateArtifacts", () => { it("reads one artifact inside a task artifact scope", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await fs.mkdir(path.join(prepared.artifactDirectory, "reports"), { recursive: true }); @@ -791,7 +802,7 @@ describe("exportXWorkmateArtifacts", () => { const result = await readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.artifactScope, relativePath: "reports/final.txt", @@ -814,7 +825,7 @@ describe("exportXWorkmateArtifacts", () => { it("rejects direct reads from another run artifact scope", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const first = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(first.artifactDirectory, "first.txt"), "first"); @@ -822,7 +833,7 @@ describe("exportXWorkmateArtifacts", () => { await expect( readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-2", artifactScope: first.artifactScope, relativePath: "first.txt", @@ -835,13 +846,13 @@ describe("exportXWorkmateArtifacts", () => { it("rejects signed task artifact refs from another session", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(prepared.artifactDirectory, "first.txt"), "first"); const exported = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.artifactScope, }, @@ -851,7 +862,7 @@ describe("exportXWorkmateArtifacts", () => { await expect( readXWorkmateArtifact({ params: { - sessionKey: "thread-other", + openclawSessionKey: "thread-other", runId: "turn-1", artifactRef: exported.artifacts[0]?.artifactRef, }, @@ -863,14 +874,14 @@ describe("exportXWorkmateArtifacts", () => { it("rejects signed task artifact refs from another run", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(prepared.artifactDirectory, "existing.txt"), "existing"); const exported = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.artifactScope, }, @@ -880,7 +891,7 @@ describe("exportXWorkmateArtifacts", () => { await expect( readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-2", artifactRef: exported.artifacts[0]?.artifactRef, }, @@ -892,13 +903,13 @@ describe("exportXWorkmateArtifacts", () => { it("rejects tampered artifact refs", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "run-1" }, + params: { openclawSessionKey: "thread-main", runId: "run-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(prepared.artifactDirectory, "existing.txt"), "existing"); const exported = await exportXWorkmateArtifacts({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", }, pluginConfig: { workspaceDir: root }, @@ -909,7 +920,7 @@ describe("exportXWorkmateArtifacts", () => { await expect( readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", artifactRef: tampered, }, @@ -938,7 +949,7 @@ describe("exportXWorkmateArtifacts", () => { await expect( readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", artifactRef: legacyRef, }, @@ -950,14 +961,14 @@ describe("exportXWorkmateArtifacts", () => { it("reads artifact metadata without inline content when the file exceeds the limit", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await fs.writeFile(path.join(prepared.artifactDirectory, "large.bin"), Buffer.from("large-content")); const result = await readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.artifactScope, relativePath: "large.bin", @@ -981,14 +992,14 @@ describe("exportXWorkmateArtifacts", () => { it("rejects relative path traversal when reading artifacts", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-")); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await expect( readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.artifactScope, relativePath: "../outside.txt", @@ -1004,7 +1015,7 @@ describe("exportXWorkmateArtifacts", () => { await expect( readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "run-1", artifactScope: "../outside", relativePath: "secret.txt", @@ -1020,7 +1031,7 @@ describe("exportXWorkmateArtifacts", () => { const outsideFile = path.join(outsideRoot, "secret.txt"); await fs.writeFile(outsideFile, "secret"); const prepared = await prepareXWorkmateArtifacts({ - params: { sessionKey: "thread-main", runId: "turn-1" }, + params: { openclawSessionKey: "thread-main", runId: "turn-1" }, pluginConfig: { workspaceDir: root }, }); await fs.symlink(outsideFile, path.join(prepared.artifactDirectory, "linked-secret.txt")); @@ -1028,7 +1039,7 @@ describe("exportXWorkmateArtifacts", () => { await expect( readXWorkmateArtifact({ params: { - sessionKey: "thread-main", + openclawSessionKey: "thread-main", runId: "turn-1", artifactScope: prepared.artifactScope, relativePath: "linked-secret.txt", diff --git a/src/exportArtifacts.ts b/src/exportArtifacts.ts index e0b522c..0ab0c4b 100644 --- a/src/exportArtifacts.ts +++ b/src/exportArtifacts.ts @@ -117,7 +117,7 @@ export async function prepareXWorkmateArtifacts(input: ExportInput): Promise; - type SessionEntry = Record & { pluginExtensions?: Record>; }; @@ -65,10 +62,6 @@ type BoundTaskRunsRuntime = { resolve?: (token: string) => unknown; }; -export function createXWorkmateTaskStore(): XWorkmateTaskStore { - return {}; -} - export function registerXWorkmateSessionExtension(api: OpenClawPluginApi) { const registerExtension = api.session?.state?.registerSessionExtension ?? (api as any).registerSessionExtension; @@ -86,13 +79,8 @@ export function registerXWorkmateSessionExtension(api: OpenClawPluginApi) { }); } -export function registerXWorkmateDetachedTaskRuntime(_api: OpenClawPluginApi, _taskStore: XWorkmateTaskStore) { - // OpenClaw native task-registry is the only task status source for this plugin. -} - export async function recordXWorkmateSessionMapping(input: { api: OpenClawPluginApi; - taskStore?: XWorkmateTaskStore; params: Record; artifactScope?: string; source?: XWorkmateSessionMappingSource; @@ -160,7 +148,6 @@ export async function upsertXWorkmateSessionMapping( metadata: XWorkmateTaskMetadataV1; openclawSessionKey: string; source: XWorkmateSessionMappingSource; - legacyDerived?: boolean; }, ): Promise { const patchSessionEntry = resolvePatchSessionEntry(api); @@ -192,7 +179,6 @@ export async function upsertXWorkmateSessionMapping( createdAt: input.metadata.createdAt || now, updatedAt: now, source: input.source, - legacyDerived: input.legacyDerived === true ? true : undefined, }) as XWorkmateSessionMappingV1; } return { @@ -238,7 +224,6 @@ export async function readXWorkmateSessionMapping( export async function getXWorkmateTaskSnapshot(input: { api: OpenClawPluginApi; - taskStore?: XWorkmateTaskStore; params: Record; }): Promise> { const params = input.params ?? {}; @@ -360,7 +345,6 @@ function readMappingFromEntry(entry: SessionEntry | undefined | null): XWorkmate createdAt: optionalString(raw.createdAt) || new Date(0).toISOString(), updatedAt: optionalString(raw.updatedAt) || optionalString(raw.createdAt) || new Date(0).toISOString(), source: parseMappingSource(raw.source), - ...(raw.legacyDerived === true ? { legacyDerived: true } : {}), }; }