fix: resolve xworkmate task snapshots from artifacts
This commit is contained in:
parent
f0ffa62f52
commit
35379f2fb0
17
README.md
17
README.md
@ -1,20 +1,27 @@
|
||||
# openclaw-multi-session-plugins
|
||||
|
||||
OpenClaw plugin for logical multi-session isolation and scoped XWorkmate artifact manifests.
|
||||
OpenClaw plugin for per-session workspace isolation and scoped XWorkmate artifact handling.
|
||||
|
||||
## Why
|
||||
|
||||
XWorkmate talks to OpenClaw through `xworkmate-bridge` using the app-facing
|
||||
`/acp` and `/acp/rpc` contract with OpenClaw routing metadata. The bridge sends
|
||||
`chat.send`, waits for `agent.wait`, then asks this plugin for a session/run-scoped artifact manifest.
|
||||
The APP can then sync generated files into its local thread workspace without
|
||||
The app can then sync generated files into its local thread workspace without
|
||||
changing the UI or adding provider-specific routes.
|
||||
|
||||
This plugin is not a scheduler or bridge client. OpenClaw core owns sub-agents,
|
||||
multi-agent routing, queues, cron, task registry state, and cross-session
|
||||
execution. This package only adapts those existing OpenClaw task/session
|
||||
identities into isolated artifact directories, session key mapping, and signed
|
||||
artifact reads.
|
||||
execution. This package only adapts existing OpenClaw task and session
|
||||
identities into isolated artifact directories, durable session key mappings,
|
||||
and signed artifact reads.
|
||||
|
||||
In practice, it provides:
|
||||
|
||||
- session preparation for a specific app thread and run
|
||||
- task-scoped artifact directories under the resolved OpenClaw workspace
|
||||
- safe export and read operations for XWorkmate Bridge
|
||||
- signed artifact references that are bound to the issuing session and run
|
||||
|
||||
It registers the minimal Gateway methods needed by XWorkmate:
|
||||
|
||||
|
||||
2
dist/src/exportArtifacts.js
vendored
2
dist/src/exportArtifacts.js
vendored
@ -752,7 +752,7 @@ function resolveWorkspaceDir(input) {
|
||||
if (explicit) {
|
||||
return expandUserPath(explicit);
|
||||
}
|
||||
throw new Error("UnsupportedError: workspaceDir must be explicitly provided in params or pluginConfig");
|
||||
return expandUserPath(path.join("~", ".openclaw", "workspace"));
|
||||
}
|
||||
function safeRelativePath(root, target) {
|
||||
const relative = path.relative(root, target);
|
||||
|
||||
60
dist/src/taskState.js
vendored
60
dist/src/taskState.js
vendored
@ -158,24 +158,47 @@ export async function getXWorkmateTaskSnapshot(input) {
|
||||
runId,
|
||||
taskId,
|
||||
});
|
||||
const includeArtifacts = params.includeArtifacts !== false;
|
||||
if (!task) {
|
||||
const exported = includeArtifacts && runId
|
||||
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping)
|
||||
: undefined;
|
||||
if (exported?.artifacts.length) {
|
||||
return {
|
||||
success: true,
|
||||
status: "completed",
|
||||
taskStatus: "succeeded",
|
||||
mode: "gateway-chat",
|
||||
mapping,
|
||||
appThreadKey: mapping?.appThreadKey ?? appThreadKey,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
taskId: taskId || runId,
|
||||
task: {
|
||||
taskId: taskId || runId,
|
||||
runId,
|
||||
status: "succeeded",
|
||||
source: "artifact_fallback",
|
||||
},
|
||||
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [],
|
||||
artifactScope: exported.artifactScope,
|
||||
remoteWorkingDirectory: exported.remoteWorkingDirectory,
|
||||
remoteWorkspaceRefKind: exported.remoteWorkspaceRefKind,
|
||||
scopeKind: exported.scopeKind,
|
||||
artifacts: exported.artifacts,
|
||||
warnings: [
|
||||
...exported.warnings,
|
||||
`Native OpenClaw task record was unavailable for ${openclawSessionKey}; resolved from task artifacts.`,
|
||||
],
|
||||
artifactCount: exported.artifacts.length,
|
||||
};
|
||||
}
|
||||
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,
|
||||
})
|
||||
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId || optionalString(task.runId) || optionalString(task.taskId), mapping)
|
||||
: undefined;
|
||||
return {
|
||||
success: true,
|
||||
@ -198,6 +221,19 @@ export async function getXWorkmateTaskSnapshot(input) {
|
||||
artifactCount: exported?.artifacts.length ?? 0,
|
||||
};
|
||||
}
|
||||
async function exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping) {
|
||||
return exportXWorkmateArtifacts({
|
||||
params: {
|
||||
...params,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? normalizeExpectedArtifactDirs(params.expectedArtifactDirs),
|
||||
includeContent: params.includeContent ?? false,
|
||||
},
|
||||
config: input.api.config,
|
||||
pluginConfig: input.api.pluginConfig,
|
||||
});
|
||||
}
|
||||
function resolveNativeTask(api, input) {
|
||||
try {
|
||||
const bound = api.runtime?.tasks?.runs?.bindSession?.({ sessionKey: input.openclawSessionKey });
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "openclaw-multi-session-plugins",
|
||||
"version": "2026.6.1",
|
||||
"description": "OpenClaw multi-session plugin runtime support for scoped XWorkmate artifacts",
|
||||
"description": "OpenClaw plugin for per-session workspace isolation and scoped XWorkmate artifact handling",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@ -935,7 +935,7 @@ function resolveWorkspaceDir(input: {
|
||||
if (explicit) {
|
||||
return expandUserPath(explicit);
|
||||
}
|
||||
throw new Error("UnsupportedError: workspaceDir must be explicitly provided in params or pluginConfig");
|
||||
return expandUserPath(path.join("~", ".openclaw", "workspace"));
|
||||
}
|
||||
|
||||
function safeRelativePath(root: string, target: string): string {
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
XWORKMATE_PLUGIN_ID,
|
||||
@ -8,11 +11,11 @@ import {
|
||||
readXWorkmateSessionMapping,
|
||||
} from "./taskState.js";
|
||||
|
||||
function createApiFixture(tasks: Record<string, unknown> = {}) {
|
||||
function createApiFixture(tasks: Record<string, unknown> = {}, pluginConfig: Record<string, unknown> = {}) {
|
||||
const sessions = new Map<string, any>();
|
||||
const api = {
|
||||
config: {},
|
||||
pluginConfig: {},
|
||||
pluginConfig,
|
||||
logger: { warn: () => {} },
|
||||
runtime: {
|
||||
agent: {
|
||||
@ -58,6 +61,10 @@ function createApiFixture(tasks: Record<string, unknown> = {}) {
|
||||
return { api: api as any, sessions };
|
||||
}
|
||||
|
||||
async function createWorkspaceFixture() {
|
||||
return fs.mkdtemp(path.join(os.tmpdir(), "xworkmate-task-state-"));
|
||||
}
|
||||
|
||||
describe("xworkmate task state mapping", () => {
|
||||
it("requires typed appThreadKey metadata", () => {
|
||||
expect(() =>
|
||||
@ -159,8 +166,56 @@ describe("xworkmate task state mapping", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns no_native_task_record instead of inferring success from artifacts", async () => {
|
||||
const { api } = createApiFixture();
|
||||
it("resolves completed snapshot from task artifacts when native task record is unavailable", async () => {
|
||||
const workspaceDir = await createWorkspaceFixture();
|
||||
const appThreadKey = "draft:sample-task";
|
||||
const openclawSessionKey = "agent:main:draft:sample-task";
|
||||
const runId = "turn-sample";
|
||||
const artifactDir = path.join(workspaceDir, "tasks", "agent_main_draft_sample-task", runId);
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
await fs.writeFile(path.join(artifactDir, "report.md"), "# Report\n", "utf8");
|
||||
|
||||
const { api } = createApiFixture({}, { workspaceDir });
|
||||
await recordXWorkmateSessionMapping({
|
||||
api,
|
||||
params: {
|
||||
schemaVersion: 1,
|
||||
appThreadKey,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getXWorkmateTaskSnapshot({
|
||||
api,
|
||||
params: {
|
||||
appThreadKey,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
success: true,
|
||||
status: "completed",
|
||||
taskStatus: "succeeded",
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
task: {
|
||||
source: "artifact_fallback",
|
||||
},
|
||||
artifacts: [
|
||||
{
|
||||
relativePath: "report.md",
|
||||
contentType: "text/markdown",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns no_native_task_record when neither native task record nor task artifacts exist", async () => {
|
||||
const workspaceDir = await createWorkspaceFixture();
|
||||
const { api } = createApiFixture({}, { workspaceDir });
|
||||
await recordXWorkmateSessionMapping({
|
||||
api,
|
||||
params: {
|
||||
|
||||
@ -253,25 +253,54 @@ export async function getXWorkmateTaskSnapshot(input: {
|
||||
runId,
|
||||
taskId,
|
||||
});
|
||||
const includeArtifacts = params.includeArtifacts !== false;
|
||||
if (!task) {
|
||||
const exported = includeArtifacts && runId
|
||||
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping)
|
||||
: undefined;
|
||||
if (exported?.artifacts.length) {
|
||||
return {
|
||||
success: true,
|
||||
status: "completed",
|
||||
taskStatus: "succeeded",
|
||||
mode: "gateway-chat",
|
||||
mapping,
|
||||
appThreadKey: mapping?.appThreadKey ?? appThreadKey,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
taskId: taskId || runId,
|
||||
task: {
|
||||
taskId: taskId || runId,
|
||||
runId,
|
||||
status: "succeeded",
|
||||
source: "artifact_fallback",
|
||||
},
|
||||
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [],
|
||||
artifactScope: exported.artifactScope,
|
||||
remoteWorkingDirectory: exported.remoteWorkingDirectory,
|
||||
remoteWorkspaceRefKind: exported.remoteWorkspaceRefKind,
|
||||
scopeKind: exported.scopeKind,
|
||||
artifacts: exported.artifacts,
|
||||
warnings: [
|
||||
...exported.warnings,
|
||||
`Native OpenClaw task record was unavailable for ${openclawSessionKey}; resolved from task artifacts.`,
|
||||
],
|
||||
artifactCount: exported.artifacts.length,
|
||||
};
|
||||
}
|
||||
const code: XWorkmateTaskLookupErrorCode = runId || taskId ? "no_native_task_record" : "task_not_found";
|
||||
return lookupError(code, `No native OpenClaw task record found for ${openclawSessionKey}`, mapping);
|
||||
}
|
||||
|
||||
const taskStatus = optionalString((task as any).status) || "running";
|
||||
const includeArtifacts = params.includeArtifacts !== false;
|
||||
const exported = includeArtifacts
|
||||
? await exportXWorkmateArtifacts({
|
||||
params: {
|
||||
...params,
|
||||
openclawSessionKey,
|
||||
runId: runId || optionalString((task as any).runId) || optionalString((task as any).taskId),
|
||||
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? normalizeExpectedArtifactDirs(params.expectedArtifactDirs),
|
||||
includeContent: params.includeContent ?? false,
|
||||
},
|
||||
config: input.api.config,
|
||||
pluginConfig: input.api.pluginConfig,
|
||||
})
|
||||
? await exportArtifactsForTaskLookup(
|
||||
input,
|
||||
params,
|
||||
openclawSessionKey,
|
||||
runId || optionalString((task as any).runId) || optionalString((task as any).taskId),
|
||||
mapping,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
@ -296,6 +325,26 @@ export async function getXWorkmateTaskSnapshot(input: {
|
||||
};
|
||||
}
|
||||
|
||||
async function exportArtifactsForTaskLookup(
|
||||
input: { api: OpenClawPluginApi; params: Record<string, unknown> },
|
||||
params: Record<string, unknown>,
|
||||
openclawSessionKey: string,
|
||||
runId: string,
|
||||
mapping?: XWorkmateSessionMappingV1,
|
||||
) {
|
||||
return exportXWorkmateArtifacts({
|
||||
params: {
|
||||
...params,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? normalizeExpectedArtifactDirs(params.expectedArtifactDirs),
|
||||
includeContent: params.includeContent ?? false,
|
||||
},
|
||||
config: input.api.config,
|
||||
pluginConfig: input.api.pluginConfig,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveNativeTask(
|
||||
api: OpenClawPluginApi,
|
||||
input: { openclawSessionKey: string; runId?: string; taskId?: string },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user