Simplify OpenClaw multi-session plugin adapter

This commit is contained in:
Haitao Pan 2026-06-06 07:56:32 +08:00
parent 8c4dcb45bc
commit ba7b447af6
10 changed files with 486 additions and 473 deletions

67
dist/index.js vendored
View File

@ -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 } : {}),

View File

@ -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<X
export declare function collectAndSnapshotXWorkmateArtifacts(input: ExportInput): Promise<XWorkmateArtifactSnapshot>;
export declare function exportXWorkmateArtifacts(input: ExportInput): Promise<XWorkmateArtifactExport>;
export declare function readXWorkmateArtifact(input: ReadInput): Promise<XWorkmateArtifactExport>;
export declare function normalizeExpectedArtifactDirs(value: unknown): string[];
export declare function formatArtifactManifestMarkdown(input: {
remoteWorkingDirectory: string;
artifactScope?: string;

View File

@ -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",

View File

@ -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<string, XWorkmateTaskRecord>;
sessionMappingsByAppKey: Map<string, XWorkmateSessionMapping>;
sessionMappingsByOpenClawKey: Map<string, XWorkmateSessionMapping>;
};
export declare function createXWorkmateTaskStore(): XWorkmateTaskStore;
export declare function registerXWorkmateSessionExtension(api: OpenClawPluginApi): void;
export declare function recordXWorkmateSessionMapping(input: {
api: OpenClawPluginApi;
taskStore: XWorkmateTaskStore;
params: Record<string, unknown>;
artifactScope?: string;
}): Promise<void>;
export declare function registerXWorkmateDetachedTaskRuntime(api: OpenClawPluginApi, taskStore: XWorkmateTaskStore): void;
source?: XWorkmateSessionMappingSource;
}): Promise<XWorkmateSessionMappingV1>;
export declare function normalizeXWorkmateTaskMetadataV1(input: Record<string, unknown>): XWorkmateTaskMetadataV1;
export declare function normalizeExpectedArtifactDirs(value: unknown): string[];
export declare function upsertXWorkmateSessionMapping(api: OpenClawPluginApi, input: {
metadata: XWorkmateTaskMetadataV1;
openclawSessionKey: string;
source: XWorkmateSessionMappingSource;
}): Promise<XWorkmateSessionMappingV1>;
export declare function readXWorkmateSessionMapping(api: OpenClawPluginApi, lookup: {
appThreadKey?: string;
openclawSessionKey?: string;
}): Promise<XWorkmateSessionMappingV1 | undefined>;
export declare function getXWorkmateTaskSnapshot(input: {
api: OpenClawPluginApi;
taskStore: XWorkmateTaskStore;
params: Record<string, unknown>;
}): Promise<Record<string, unknown>>;
export declare function createOrUpdateXWorkmateTaskRecord(input: XWorkmateTaskStore, options: {
params: Record<string, unknown>;
status: XWorkmateTaskRecord["status"];
progressSummary?: string;
}): XWorkmateTaskRecord;
export {};

508
dist/src/taskState.js vendored
View File

@ -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 === "<nil>" ? "" : 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, "_");
}

View File

@ -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<string, unknown>) => 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: "/",
});

View File

@ -14,7 +14,6 @@ import {
import {
getXWorkmateTaskSnapshot,
recordXWorkmateSessionMapping,
registerXWorkmateDetachedTaskRuntime,
registerXWorkmateSessionExtension,
} from "./src/taskState.js";
@ -53,7 +52,7 @@ function scopedGatewayParams(params: Record<string, unknown>): Record<string, un
}
return {
...params,
sessionKey: runScope.sessionKey,
openclawSessionKey: runScope.sessionKey,
runId: runScope.runId,
...(runScope.workspaceDir ? { workspaceDir: runScope.workspaceDir } : {}),
...(runScope.artifactScope ? { artifactScope: runScope.artifactScope } : {}),
@ -94,16 +93,14 @@ const plugin = {
export default plugin;
function register(api: OpenClawPluginApi) {
const taskStore = {};
registerXWorkmateSessionExtension(api);
registerXWorkmateDetachedTaskRuntime(api, taskStore);
api.registerHook(
"session_start",
async (event: any) => {
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 } : {}),

View File

@ -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",

View File

@ -117,7 +117,7 @@ export async function prepareXWorkmateArtifacts(input: ExportInput): Promise<XWo
const params = input.params ?? {};
const pluginConfig = input.pluginConfig ?? {};
const runId = requiredString(params.runId, "runId required");
const sessionKey = requiredString(params.openclawSessionKey ?? params.sessionKey, "openclawSessionKey required");
const sessionKey = requiredString(params.openclawSessionKey, "openclawSessionKey required");
const expectedArtifactDirs = normalizeExpectedArtifactDirs(params.expectedArtifactDirs);
const expectedArtifactScope = artifactScopeFor(sessionKey, runId);
const requestedArtifactScope = optionalArtifactScope(params.artifactScope);
@ -154,7 +154,7 @@ export async function collectAndSnapshotXWorkmateArtifacts(input: ExportInput):
const params = input.params ?? {};
const pluginConfig = input.pluginConfig ?? {};
const runId = requiredString(params.runId, "runId required");
const sessionKey = requiredString(params.openclawSessionKey ?? params.sessionKey, "openclawSessionKey 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);
@ -224,7 +224,7 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
const params = input.params ?? {};
const pluginConfig = input.pluginConfig ?? {};
const runId = requiredString(params.runId, "runId required");
const sessionKey = requiredString(params.openclawSessionKey ?? params.sessionKey, "openclawSessionKey required");
const sessionKey = requiredString(params.openclawSessionKey, "openclawSessionKey required");
const maxFiles = positiveInteger(params.maxFiles, pluginConfig.maxFiles, DEFAULT_MAX_FILES);
const maxInlineBytes = nonNegativeInteger(
@ -261,7 +261,9 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
try {
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({
@ -368,7 +370,7 @@ export async function readXWorkmateArtifact(input: ReadInput): Promise<XWorkmate
const params = input.params ?? {};
const pluginConfig = input.pluginConfig ?? {};
const runId = requiredString(params.runId, "runId required");
const sessionKey = requiredString(params.openclawSessionKey ?? params.sessionKey, "openclawSessionKey required");
const sessionKey = requiredString(params.openclawSessionKey, "openclawSessionKey required");
const expectedArtifactScope = artifactScopeFor(sessionKey, runId);
const expectedSessionScope = taskSessionScopeFor(sessionKey);
const requestedArtifactRef = optionalString(params.artifactRef);

View File

@ -26,7 +26,6 @@ export type XWorkmateSessionMappingV1 = {
createdAt: string;
updatedAt: string;
source: XWorkmateSessionMappingSource;
legacyDerived?: boolean;
};
export type XWorkmateTaskLookupErrorCode =
@ -44,8 +43,6 @@ export type XWorkmateTaskLookupError = {
expectedArtifactDirs?: string[];
};
export type XWorkmateTaskStore = Record<string, never>;
type SessionEntry = Record<string, unknown> & {
pluginExtensions?: Record<string, Record<string, unknown>>;
};
@ -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<string, unknown>;
artifactScope?: string;
source?: XWorkmateSessionMappingSource;
@ -160,7 +148,6 @@ export async function upsertXWorkmateSessionMapping(
metadata: XWorkmateTaskMetadataV1;
openclawSessionKey: string;
source: XWorkmateSessionMappingSource;
legacyDerived?: boolean;
},
): Promise<XWorkmateSessionMappingV1> {
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<string, unknown>;
}): Promise<Record<string, unknown>> {
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 } : {}),
};
}