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-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
|
## Why
|
||||||
|
|
||||||
XWorkmate talks to OpenClaw through `xworkmate-bridge` using the app-facing
|
XWorkmate talks to OpenClaw through `xworkmate-bridge` using the app-facing
|
||||||
`/acp` and `/acp/rpc` contract with OpenClaw routing metadata. The bridge sends
|
`/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.
|
`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.
|
changing the UI or adding provider-specific routes.
|
||||||
|
|
||||||
This plugin is not a scheduler or bridge client. OpenClaw core owns sub-agents,
|
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
|
multi-agent routing, queues, cron, task registry state, and cross-session
|
||||||
execution. This package only adapts those existing OpenClaw task/session
|
execution. This package only adapts existing OpenClaw task and session
|
||||||
identities into isolated artifact directories, session key mapping, and signed
|
identities into isolated artifact directories, durable session key mappings,
|
||||||
artifact reads.
|
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:
|
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) {
|
if (explicit) {
|
||||||
return expandUserPath(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) {
|
function safeRelativePath(root, target) {
|
||||||
const relative = path.relative(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,
|
runId,
|
||||||
taskId,
|
taskId,
|
||||||
});
|
});
|
||||||
|
const includeArtifacts = params.includeArtifacts !== false;
|
||||||
if (!task) {
|
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";
|
const code = runId || taskId ? "no_native_task_record" : "task_not_found";
|
||||||
return lookupError(code, `No native OpenClaw task record found for ${openclawSessionKey}`, mapping);
|
return lookupError(code, `No native OpenClaw task record found for ${openclawSessionKey}`, mapping);
|
||||||
}
|
}
|
||||||
const taskStatus = optionalString(task.status) || "running";
|
const taskStatus = optionalString(task.status) || "running";
|
||||||
const includeArtifacts = params.includeArtifacts !== false;
|
|
||||||
const exported = includeArtifacts
|
const exported = includeArtifacts
|
||||||
? await exportXWorkmateArtifacts({
|
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId || optionalString(task.runId) || optionalString(task.taskId), mapping)
|
||||||
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;
|
: undefined;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -198,6 +221,19 @@ export async function getXWorkmateTaskSnapshot(input) {
|
|||||||
artifactCount: exported?.artifacts.length ?? 0,
|
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) {
|
function resolveNativeTask(api, input) {
|
||||||
try {
|
try {
|
||||||
const bound = api.runtime?.tasks?.runs?.bindSession?.({ sessionKey: input.openclawSessionKey });
|
const bound = api.runtime?.tasks?.runs?.bindSession?.({ sessionKey: input.openclawSessionKey });
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "openclaw-multi-session-plugins",
|
"name": "openclaw-multi-session-plugins",
|
||||||
"version": "2026.6.1",
|
"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",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@ -935,7 +935,7 @@ function resolveWorkspaceDir(input: {
|
|||||||
if (explicit) {
|
if (explicit) {
|
||||||
return expandUserPath(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 {
|
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 { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
XWORKMATE_PLUGIN_ID,
|
XWORKMATE_PLUGIN_ID,
|
||||||
@ -8,11 +11,11 @@ import {
|
|||||||
readXWorkmateSessionMapping,
|
readXWorkmateSessionMapping,
|
||||||
} from "./taskState.js";
|
} 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 sessions = new Map<string, any>();
|
||||||
const api = {
|
const api = {
|
||||||
config: {},
|
config: {},
|
||||||
pluginConfig: {},
|
pluginConfig,
|
||||||
logger: { warn: () => {} },
|
logger: { warn: () => {} },
|
||||||
runtime: {
|
runtime: {
|
||||||
agent: {
|
agent: {
|
||||||
@ -58,6 +61,10 @@ function createApiFixture(tasks: Record<string, unknown> = {}) {
|
|||||||
return { api: api as any, sessions };
|
return { api: api as any, sessions };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createWorkspaceFixture() {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), "xworkmate-task-state-"));
|
||||||
|
}
|
||||||
|
|
||||||
describe("xworkmate task state mapping", () => {
|
describe("xworkmate task state mapping", () => {
|
||||||
it("requires typed appThreadKey metadata", () => {
|
it("requires typed appThreadKey metadata", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
@ -159,8 +166,56 @@ describe("xworkmate task state mapping", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns no_native_task_record instead of inferring success from artifacts", async () => {
|
it("resolves completed snapshot from task artifacts when native task record is unavailable", async () => {
|
||||||
const { api } = createApiFixture();
|
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({
|
await recordXWorkmateSessionMapping({
|
||||||
api,
|
api,
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@ -253,25 +253,54 @@ export async function getXWorkmateTaskSnapshot(input: {
|
|||||||
runId,
|
runId,
|
||||||
taskId,
|
taskId,
|
||||||
});
|
});
|
||||||
|
const includeArtifacts = params.includeArtifacts !== false;
|
||||||
if (!task) {
|
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";
|
const code: XWorkmateTaskLookupErrorCode = runId || taskId ? "no_native_task_record" : "task_not_found";
|
||||||
return lookupError(code, `No native OpenClaw task record found for ${openclawSessionKey}`, mapping);
|
return lookupError(code, `No native OpenClaw task record found for ${openclawSessionKey}`, mapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskStatus = optionalString((task as any).status) || "running";
|
const taskStatus = optionalString((task as any).status) || "running";
|
||||||
const includeArtifacts = params.includeArtifacts !== false;
|
|
||||||
const exported = includeArtifacts
|
const exported = includeArtifacts
|
||||||
? await exportXWorkmateArtifacts({
|
? await exportArtifactsForTaskLookup(
|
||||||
params: {
|
input,
|
||||||
...params,
|
params,
|
||||||
openclawSessionKey,
|
openclawSessionKey,
|
||||||
runId: runId || optionalString((task as any).runId) || optionalString((task as any).taskId),
|
runId || optionalString((task as any).runId) || optionalString((task as any).taskId),
|
||||||
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? normalizeExpectedArtifactDirs(params.expectedArtifactDirs),
|
mapping,
|
||||||
includeContent: params.includeContent ?? false,
|
)
|
||||||
},
|
|
||||||
config: input.api.config,
|
|
||||||
pluginConfig: input.api.pluginConfig,
|
|
||||||
})
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
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(
|
function resolveNativeTask(
|
||||||
api: OpenClawPluginApi,
|
api: OpenClawPluginApi,
|
||||||
input: { openclawSessionKey: string; runId?: string; taskId?: string },
|
input: { openclawSessionKey: string; runId?: string; taskId?: string },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user