fix(tasks): persist gateway agent terminal state
This commit is contained in:
parent
baddb2f13d
commit
d5f0e9f437
26
dist/index.js
vendored
26
dist/index.js
vendored
@ -1,7 +1,7 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import { collectAndSnapshotXWorkmateArtifacts, exportXWorkmateArtifacts, prepareXWorkmateArtifacts, readXWorkmateArtifact, formatArtifactManifestMarkdown, } from "./src/exportArtifacts.js";
|
||||
import { getXWorkmateTaskSnapshot, recordXWorkmateSessionMapping, registerXWorkmateSessionExtension, } from "./src/taskState.js";
|
||||
import { getXWorkmateTaskSnapshot, recordXWorkmateSessionMapping, recordXWorkmateTaskRunStarted, recordXWorkmateTaskRunTerminal, registerXWorkmateSessionExtension, } from "./src/taskState.js";
|
||||
function scopedGatewayParams(params) {
|
||||
const sessionScope = getPluginRuntimeGatewayRequestScope()?.sessionScope;
|
||||
const runScope = resolveRunScope({ sessionScope });
|
||||
@ -65,6 +65,25 @@ function register(api) {
|
||||
api.logger?.warn?.(`xworkmate session_start preparation failed: ${String(error)}`);
|
||||
}
|
||||
}, { name: "openclaw-multi-session-plugins.session-start" });
|
||||
api.on("agent_end", async (event, ctx) => {
|
||||
try {
|
||||
const openclawSessionKey = stringParam(ctx?.sessionKey ?? event?.sessionKey);
|
||||
const runId = stringParam(event?.runId ?? ctx?.runId);
|
||||
if (!openclawSessionKey || !runId) {
|
||||
return;
|
||||
}
|
||||
await recordXWorkmateTaskRunTerminal({
|
||||
api,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
success: event?.success === true,
|
||||
error: event?.error,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
api.logger?.warn?.(`xworkmate agent_end state capture failed: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
api.registerGatewayMethod("xworkmate.session.prepare", async (opts) => {
|
||||
try {
|
||||
const params = scopedGatewayParams(opts.params);
|
||||
@ -82,6 +101,11 @@ function register(api) {
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
await recordXWorkmateTaskRunStarted({
|
||||
api,
|
||||
openclawSessionKey: mapping.openclawSessionKey,
|
||||
runId: stringParam(params.runId),
|
||||
});
|
||||
opts.respond(true, {
|
||||
...payload,
|
||||
mapping,
|
||||
|
||||
23
dist/src/taskState.d.ts
vendored
23
dist/src/taskState.d.ts
vendored
@ -1,5 +1,6 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
export declare const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
||||
export declare const XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE = "xworkmate.taskRuns";
|
||||
export type XWorkmateTaskMetadataV1 = {
|
||||
schemaVersion: 1;
|
||||
appThreadKey: string;
|
||||
@ -27,6 +28,16 @@ export type XWorkmateTaskLookupError = {
|
||||
mapping?: XWorkmateSessionMappingV1;
|
||||
expectedArtifactDirs?: string[];
|
||||
};
|
||||
export type XWorkmateRecordedTaskRunV1 = {
|
||||
schemaVersion: 1;
|
||||
runId: string;
|
||||
status: "running" | "completed" | "failed";
|
||||
success: boolean;
|
||||
startedAt: string;
|
||||
updatedAt: string;
|
||||
completedAt?: string;
|
||||
error?: string;
|
||||
};
|
||||
export declare function registerXWorkmateSessionExtension(api: OpenClawPluginApi): void;
|
||||
export declare function recordXWorkmateSessionMapping(input: {
|
||||
api: OpenClawPluginApi;
|
||||
@ -34,6 +45,18 @@ export declare function recordXWorkmateSessionMapping(input: {
|
||||
artifactScope?: string;
|
||||
source?: XWorkmateSessionMappingSource;
|
||||
}): Promise<XWorkmateSessionMappingV1>;
|
||||
export declare function recordXWorkmateTaskRunStarted(input: {
|
||||
api: OpenClawPluginApi;
|
||||
openclawSessionKey: string;
|
||||
runId: string;
|
||||
}): Promise<XWorkmateRecordedTaskRunV1>;
|
||||
export declare function recordXWorkmateTaskRunTerminal(input: {
|
||||
api: OpenClawPluginApi;
|
||||
openclawSessionKey: string;
|
||||
runId: string;
|
||||
success: boolean;
|
||||
error?: unknown;
|
||||
}): Promise<XWorkmateRecordedTaskRunV1>;
|
||||
export declare function getXWorkmateTaskSnapshot(input: {
|
||||
api: OpenClawPluginApi;
|
||||
params: Record<string, unknown>;
|
||||
|
||||
157
dist/src/taskState.js
vendored
157
dist/src/taskState.js
vendored
@ -2,6 +2,8 @@ import { exportXWorkmateArtifacts } from "./exportArtifacts.js";
|
||||
import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js";
|
||||
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||
export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
||||
export const XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE = "xworkmate.taskRuns";
|
||||
const MAX_RECORDED_TASK_RUNS = 32;
|
||||
export function registerXWorkmateSessionExtension(api) {
|
||||
const registerExtension = api.session?.state?.registerSessionExtension ?? api.registerSessionExtension;
|
||||
if (typeof registerExtension !== "function") {
|
||||
@ -29,6 +31,29 @@ export async function recordXWorkmateSessionMapping(input) {
|
||||
source: input.source ?? "bridge_prepare",
|
||||
});
|
||||
}
|
||||
export async function recordXWorkmateTaskRunStarted(input) {
|
||||
const now = new Date().toISOString();
|
||||
return upsertXWorkmateTaskRun(input.api, {
|
||||
openclawSessionKey: requiredString(input.openclawSessionKey, "openclawSessionKey required"),
|
||||
runId: requiredString(input.runId, "runId required"),
|
||||
status: "running",
|
||||
success: false,
|
||||
startedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
export async function recordXWorkmateTaskRunTerminal(input) {
|
||||
const now = new Date().toISOString();
|
||||
return upsertXWorkmateTaskRun(input.api, {
|
||||
openclawSessionKey: requiredString(input.openclawSessionKey, "openclawSessionKey required"),
|
||||
runId: requiredString(input.runId, "runId required"),
|
||||
status: input.success ? "completed" : "failed",
|
||||
success: input.success,
|
||||
updatedAt: now,
|
||||
completedAt: now,
|
||||
error: sanitizeTaskRunError(input.error),
|
||||
});
|
||||
}
|
||||
function normalizeXWorkmateTaskMetadataV1(input) {
|
||||
const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input;
|
||||
const schemaVersion = Number(envelope.schemaVersion ?? 1);
|
||||
@ -139,9 +164,50 @@ export async function getXWorkmateTaskSnapshot(input) {
|
||||
});
|
||||
const includeArtifacts = params.includeArtifacts !== false;
|
||||
if (!task) {
|
||||
const recordedRun = runId
|
||||
? readXWorkmateTaskRun(input.api, openclawSessionKey, runId)
|
||||
: undefined;
|
||||
const exported = includeArtifacts && runId
|
||||
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping)
|
||||
: undefined;
|
||||
if (recordedRun) {
|
||||
return {
|
||||
success: recordedRun.status === "running" ? true : recordedRun.success,
|
||||
status: recordedRun.status,
|
||||
taskStatus: recordedRun.status,
|
||||
terminal: recordedRun.status !== "running",
|
||||
terminalSource: "agent_end",
|
||||
mode: "gateway-chat",
|
||||
mapping,
|
||||
appThreadKey: mapping?.appThreadKey ?? appThreadKey,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
taskId: taskId || runId,
|
||||
task: {
|
||||
taskId: taskId || runId,
|
||||
runId,
|
||||
status: recordedRun.status,
|
||||
success: recordedRun.success,
|
||||
source: "xworkmate_run_state",
|
||||
startedAt: recordedRun.startedAt,
|
||||
updatedAt: recordedRun.updatedAt,
|
||||
completedAt: recordedRun.completedAt,
|
||||
error: recordedRun.error,
|
||||
},
|
||||
error: recordedRun.error,
|
||||
message: recordedRun.error,
|
||||
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [],
|
||||
artifactScope: exported?.artifactScope,
|
||||
remoteWorkingDirectory: exported?.remoteWorkingDirectory,
|
||||
remoteWorkspaceRefKind: exported?.remoteWorkspaceRefKind,
|
||||
scopeKind: exported?.scopeKind,
|
||||
artifacts: exported?.artifacts ?? [],
|
||||
constraintSatisfied: exported?.constraintSatisfied,
|
||||
missingRequiredExtensions: exported?.missingRequiredExtensions,
|
||||
warnings: exported?.warnings ?? [],
|
||||
artifactCount: exported?.artifacts.length ?? 0,
|
||||
};
|
||||
}
|
||||
if (exported?.artifacts.length) {
|
||||
return {
|
||||
success: false,
|
||||
@ -205,6 +271,97 @@ export async function getXWorkmateTaskSnapshot(input) {
|
||||
artifactCount: exported?.artifacts.length ?? 0,
|
||||
};
|
||||
}
|
||||
async function upsertXWorkmateTaskRun(api, input) {
|
||||
const patchSessionEntry = resolvePatchSessionEntry(api);
|
||||
if (!patchSessionEntry) {
|
||||
throw new Error("OpenClaw runtime session patch API is unavailable");
|
||||
}
|
||||
let recorded;
|
||||
await patchSessionEntry({
|
||||
sessionKey: input.openclawSessionKey,
|
||||
fallbackEntry: {
|
||||
sessionId: input.openclawSessionKey,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
preserveActivity: true,
|
||||
update: (entry) => {
|
||||
const runs = readTaskRunsFromEntry(entry);
|
||||
const existing = runs[input.runId];
|
||||
recorded = compactObject({
|
||||
schemaVersion: 1,
|
||||
runId: input.runId,
|
||||
status: input.status,
|
||||
success: input.success,
|
||||
startedAt: existing?.startedAt ?? input.startedAt ?? input.updatedAt,
|
||||
updatedAt: input.updatedAt,
|
||||
completedAt: input.completedAt,
|
||||
error: input.error,
|
||||
});
|
||||
runs[input.runId] = recorded;
|
||||
const boundedRuns = Object.fromEntries(Object.entries(runs)
|
||||
.sort((left, right) => right[1].updatedAt.localeCompare(left[1].updatedAt))
|
||||
.slice(0, MAX_RECORDED_TASK_RUNS));
|
||||
return {
|
||||
pluginExtensions: {
|
||||
...(entry.pluginExtensions ?? {}),
|
||||
[XWORKMATE_PLUGIN_ID]: {
|
||||
...(entry.pluginExtensions?.[XWORKMATE_PLUGIN_ID] ?? {}),
|
||||
[XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE]: {
|
||||
schemaVersion: 1,
|
||||
runs: boundedRuns,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
if (!recorded) {
|
||||
throw new Error("failed to write xworkmate task run state");
|
||||
}
|
||||
return recorded;
|
||||
}
|
||||
function readXWorkmateTaskRun(api, openclawSessionKey, runId) {
|
||||
const entry = resolveGetSessionEntry(api)?.({ sessionKey: openclawSessionKey });
|
||||
return readTaskRunsFromEntry(entry)[runId];
|
||||
}
|
||||
function readTaskRunsFromEntry(entry) {
|
||||
const pluginState = asRecord(entry?.pluginExtensions?.[XWORKMATE_PLUGIN_ID]);
|
||||
const store = asRecord(pluginState?.[XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE]);
|
||||
if (store?.schemaVersion !== 1) {
|
||||
return {};
|
||||
}
|
||||
const runs = asRecord(store.runs) ?? {};
|
||||
const result = {};
|
||||
for (const [key, rawValue] of Object.entries(runs)) {
|
||||
const raw = asRecord(rawValue);
|
||||
const runId = optionalString(raw?.runId) || key;
|
||||
const status = optionalString(raw?.status);
|
||||
if (!runId || (status !== "running" && status !== "completed" && status !== "failed")) {
|
||||
continue;
|
||||
}
|
||||
result[runId] = compactObject({
|
||||
schemaVersion: 1,
|
||||
runId,
|
||||
status,
|
||||
success: raw?.success === true,
|
||||
startedAt: optionalString(raw?.startedAt) || new Date(0).toISOString(),
|
||||
updatedAt: optionalString(raw?.updatedAt) || new Date(0).toISOString(),
|
||||
completedAt: optionalString(raw?.completedAt),
|
||||
error: optionalString(raw?.error),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function sanitizeTaskRunError(value) {
|
||||
const raw = optionalString(value);
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
return raw
|
||||
.replace(/\b(sk|nvapi)-[A-Za-z0-9._-]+\b/gi, "$1-<redacted>")
|
||||
.replace(/(api[_ -]?key\s*[:=]\s*)[^\s,;]+/gi, "$1<redacted>")
|
||||
.slice(0, 2048);
|
||||
}
|
||||
async function exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping) {
|
||||
return exportXWorkmateArtifacts({
|
||||
params: {
|
||||
|
||||
@ -42,6 +42,7 @@ describe("plugin registration", () => {
|
||||
tools.push({ tool, options });
|
||||
},
|
||||
registerHook: () => undefined,
|
||||
on: () => undefined,
|
||||
} as unknown as OpenClawPluginApi;
|
||||
|
||||
plugin.register(api);
|
||||
@ -73,6 +74,7 @@ describe("plugin registration", () => {
|
||||
},
|
||||
registerTool: () => undefined,
|
||||
registerHook: () => undefined,
|
||||
on: () => undefined,
|
||||
runtime: {
|
||||
agent: {
|
||||
session: {
|
||||
@ -139,7 +141,7 @@ describe("plugin registration", () => {
|
||||
it("registers xworkmate task state against the native session extension and task runtime seams", async () => {
|
||||
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-task-state-"));
|
||||
const methods = new Map<string, GatewayMethodHandler>();
|
||||
const hooks = new Map<string, (event: unknown) => Promise<void>>();
|
||||
const hooks = new Map<string, (event: unknown, ctx?: unknown) => Promise<void>>();
|
||||
const sessionExtensions: Array<Record<string, unknown>> = [];
|
||||
const sessionExtensionPatches: Array<Record<string, unknown>> = [];
|
||||
const detachedRuntimes: Array<Record<string, unknown>> = [];
|
||||
@ -197,7 +199,10 @@ describe("plugin registration", () => {
|
||||
methods.set(method, handler);
|
||||
},
|
||||
registerTool: () => undefined,
|
||||
registerHook: (event: string, handler: (payload: unknown) => Promise<void>) => {
|
||||
registerHook: (event: string, handler: (payload: unknown, ctx?: unknown) => Promise<void>) => {
|
||||
hooks.set(event, handler);
|
||||
},
|
||||
on: (event: string, handler: (payload: unknown, ctx?: unknown) => Promise<void>) => {
|
||||
hooks.set(event, handler);
|
||||
},
|
||||
|
||||
@ -257,6 +262,15 @@ describe("plugin registration", () => {
|
||||
});
|
||||
expect(snapshot.payload?.task).toMatchObject({ taskId: "native-task", status: "running" });
|
||||
expect(snapshot.payload?.artifacts).toMatchObject([{ relativePath: "reports/final.md" }]);
|
||||
|
||||
await hooks.get("agent_end")?.(
|
||||
{ runId: "turn-1", success: false, error: "401 authentication failed" },
|
||||
{ sessionKey: "draft:1780636411666238-3", runId: "turn-1" },
|
||||
);
|
||||
expect(sessionExtensionPatches.at(-1)).toMatchObject({
|
||||
sessionKey: "draft:1780636411666238-3",
|
||||
preserveActivity: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not invent default session or run ids for the optional agent tool", async () => {
|
||||
@ -266,6 +280,7 @@ describe("plugin registration", () => {
|
||||
pluginConfig: { workspaceDir: path.join(os.tmpdir(), "openclaw-multi-session-tool-test") },
|
||||
registerGatewayMethod: () => undefined,
|
||||
registerHook: () => undefined,
|
||||
on: () => undefined,
|
||||
registerTool: (tool: unknown, options: unknown) => {
|
||||
tools.push({ tool, options });
|
||||
},
|
||||
@ -295,6 +310,7 @@ describe("plugin registration", () => {
|
||||
pluginConfig: {},
|
||||
registerGatewayMethod: () => undefined,
|
||||
registerHook: () => undefined,
|
||||
on: () => undefined,
|
||||
registerTool: (tool: unknown, options: { names?: string[] }) => {
|
||||
tools.push({ tool, options });
|
||||
},
|
||||
@ -325,6 +341,7 @@ describe("plugin registration", () => {
|
||||
pluginConfig: {},
|
||||
registerGatewayMethod: () => undefined,
|
||||
registerHook: () => undefined,
|
||||
on: () => undefined,
|
||||
registerTool: (tool: unknown, options: unknown) => {
|
||||
tools.push({ tool, options });
|
||||
},
|
||||
|
||||
29
index.ts
29
index.ts
@ -15,6 +15,8 @@ import {
|
||||
import {
|
||||
getXWorkmateTaskSnapshot,
|
||||
recordXWorkmateSessionMapping,
|
||||
recordXWorkmateTaskRunStarted,
|
||||
recordXWorkmateTaskRunTerminal,
|
||||
registerXWorkmateSessionExtension,
|
||||
} from "./src/taskState.js";
|
||||
|
||||
@ -123,6 +125,28 @@ function register(api: OpenClawPluginApi) {
|
||||
{ name: "openclaw-multi-session-plugins.session-start" },
|
||||
);
|
||||
|
||||
api.on(
|
||||
"agent_end",
|
||||
async (event: any, ctx: any) => {
|
||||
try {
|
||||
const openclawSessionKey = stringParam(ctx?.sessionKey ?? event?.sessionKey);
|
||||
const runId = stringParam(event?.runId ?? ctx?.runId);
|
||||
if (!openclawSessionKey || !runId) {
|
||||
return;
|
||||
}
|
||||
await recordXWorkmateTaskRunTerminal({
|
||||
api,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
success: event?.success === true,
|
||||
error: event?.error,
|
||||
});
|
||||
} catch (error) {
|
||||
api.logger?.warn?.(`xworkmate agent_end state capture failed: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod("xworkmate.session.prepare", async (opts: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const params = scopedGatewayParams(opts.params);
|
||||
@ -140,6 +164,11 @@ function register(api: OpenClawPluginApi) {
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
await recordXWorkmateTaskRunStarted({
|
||||
api,
|
||||
openclawSessionKey: mapping.openclawSessionKey,
|
||||
runId: stringParam(params.runId),
|
||||
});
|
||||
opts.respond(
|
||||
true,
|
||||
{
|
||||
|
||||
@ -6,6 +6,8 @@ import {
|
||||
XWORKMATE_SESSION_EXTENSION_NAMESPACE,
|
||||
getXWorkmateTaskSnapshot,
|
||||
recordXWorkmateSessionMapping,
|
||||
recordXWorkmateTaskRunStarted,
|
||||
recordXWorkmateTaskRunTerminal,
|
||||
} from "./taskState.js";
|
||||
|
||||
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||
@ -252,6 +254,90 @@ describe("xworkmate task state mapping", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a durable failed agent terminal state when the native task record is absent", async () => {
|
||||
const workspaceDir = await createWorkspaceFixture();
|
||||
const { api } = createApiFixture({}, { workspaceDir });
|
||||
await recordXWorkmateSessionMapping({
|
||||
api,
|
||||
params: {
|
||||
appThreadKey: "draft:failed-run",
|
||||
openclawSessionKey: "agent:main:draft:failed-run",
|
||||
runId: "turn-failed",
|
||||
},
|
||||
});
|
||||
await recordXWorkmateTaskRunStarted({
|
||||
api,
|
||||
openclawSessionKey: "agent:main:draft:failed-run",
|
||||
runId: "turn-failed",
|
||||
});
|
||||
await recordXWorkmateTaskRunTerminal({
|
||||
api,
|
||||
openclawSessionKey: "agent:main:draft:failed-run",
|
||||
runId: "turn-failed",
|
||||
success: false,
|
||||
error: "401 Authentication Fails, api_key=sk-secret-value",
|
||||
});
|
||||
|
||||
await expect(
|
||||
getXWorkmateTaskSnapshot({
|
||||
api,
|
||||
params: {
|
||||
appThreadKey: "draft:failed-run",
|
||||
runId: "turn-failed",
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
success: false,
|
||||
status: "failed",
|
||||
taskStatus: "failed",
|
||||
terminal: true,
|
||||
terminalSource: "agent_end",
|
||||
task: {
|
||||
runId: "turn-failed",
|
||||
status: "failed",
|
||||
source: "xworkmate_run_state",
|
||||
},
|
||||
error: "401 Authentication Fails, api_key=<redacted>",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a recorded running state while the agent turn is still active", async () => {
|
||||
const { api } = createApiFixture();
|
||||
await recordXWorkmateSessionMapping({
|
||||
api,
|
||||
params: {
|
||||
appThreadKey: "draft:running-run",
|
||||
openclawSessionKey: "agent:main:draft:running-run",
|
||||
runId: "turn-running",
|
||||
},
|
||||
});
|
||||
await recordXWorkmateTaskRunStarted({
|
||||
api,
|
||||
openclawSessionKey: "agent:main:draft:running-run",
|
||||
runId: "turn-running",
|
||||
});
|
||||
|
||||
await expect(
|
||||
getXWorkmateTaskSnapshot({
|
||||
api,
|
||||
params: {
|
||||
appThreadKey: "draft:running-run",
|
||||
runId: "turn-running",
|
||||
includeArtifacts: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
success: true,
|
||||
status: "running",
|
||||
terminal: false,
|
||||
task: {
|
||||
runId: "turn-running",
|
||||
status: "running",
|
||||
source: "xworkmate_run_state",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not accept legacy sessionKey as a task lookup alias", async () => {
|
||||
const { api } = createApiFixture({
|
||||
"draft:legacy:run-1": {
|
||||
|
||||
196
src/taskState.ts
196
src/taskState.ts
@ -4,6 +4,8 @@ import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js";
|
||||
|
||||
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||
export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
||||
export const XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE = "xworkmate.taskRuns";
|
||||
const MAX_RECORDED_TASK_RUNS = 32;
|
||||
|
||||
export type XWorkmateTaskMetadataV1 = {
|
||||
schemaVersion: 1;
|
||||
@ -44,6 +46,17 @@ export type XWorkmateTaskLookupError = {
|
||||
expectedArtifactDirs?: string[];
|
||||
};
|
||||
|
||||
export type XWorkmateRecordedTaskRunV1 = {
|
||||
schemaVersion: 1;
|
||||
runId: string;
|
||||
status: "running" | "completed" | "failed";
|
||||
success: boolean;
|
||||
startedAt: string;
|
||||
updatedAt: string;
|
||||
completedAt?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type SessionEntry = Record<string, unknown> & {
|
||||
pluginExtensions?: Record<string, Record<string, unknown>>;
|
||||
};
|
||||
@ -102,6 +115,41 @@ export async function recordXWorkmateSessionMapping(input: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function recordXWorkmateTaskRunStarted(input: {
|
||||
api: OpenClawPluginApi;
|
||||
openclawSessionKey: string;
|
||||
runId: string;
|
||||
}): Promise<XWorkmateRecordedTaskRunV1> {
|
||||
const now = new Date().toISOString();
|
||||
return upsertXWorkmateTaskRun(input.api, {
|
||||
openclawSessionKey: requiredString(input.openclawSessionKey, "openclawSessionKey required"),
|
||||
runId: requiredString(input.runId, "runId required"),
|
||||
status: "running",
|
||||
success: false,
|
||||
startedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
export async function recordXWorkmateTaskRunTerminal(input: {
|
||||
api: OpenClawPluginApi;
|
||||
openclawSessionKey: string;
|
||||
runId: string;
|
||||
success: boolean;
|
||||
error?: unknown;
|
||||
}): Promise<XWorkmateRecordedTaskRunV1> {
|
||||
const now = new Date().toISOString();
|
||||
return upsertXWorkmateTaskRun(input.api, {
|
||||
openclawSessionKey: requiredString(input.openclawSessionKey, "openclawSessionKey required"),
|
||||
runId: requiredString(input.runId, "runId required"),
|
||||
status: input.success ? "completed" : "failed",
|
||||
success: input.success,
|
||||
updatedAt: now,
|
||||
completedAt: now,
|
||||
error: sanitizeTaskRunError(input.error),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeXWorkmateTaskMetadataV1(input: Record<string, unknown>): XWorkmateTaskMetadataV1 {
|
||||
const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input;
|
||||
const schemaVersion = Number(envelope.schemaVersion ?? 1);
|
||||
@ -233,9 +281,50 @@ export async function getXWorkmateTaskSnapshot(input: {
|
||||
});
|
||||
const includeArtifacts = params.includeArtifacts !== false;
|
||||
if (!task) {
|
||||
const recordedRun = runId
|
||||
? readXWorkmateTaskRun(input.api, openclawSessionKey, runId)
|
||||
: undefined;
|
||||
const exported = includeArtifacts && runId
|
||||
? await exportArtifactsForTaskLookup(input, params, openclawSessionKey, runId, mapping)
|
||||
: undefined;
|
||||
if (recordedRun) {
|
||||
return {
|
||||
success: recordedRun.status === "running" ? true : recordedRun.success,
|
||||
status: recordedRun.status,
|
||||
taskStatus: recordedRun.status,
|
||||
terminal: recordedRun.status !== "running",
|
||||
terminalSource: "agent_end",
|
||||
mode: "gateway-chat",
|
||||
mapping,
|
||||
appThreadKey: mapping?.appThreadKey ?? appThreadKey,
|
||||
openclawSessionKey,
|
||||
runId,
|
||||
taskId: taskId || runId,
|
||||
task: {
|
||||
taskId: taskId || runId,
|
||||
runId,
|
||||
status: recordedRun.status,
|
||||
success: recordedRun.success,
|
||||
source: "xworkmate_run_state",
|
||||
startedAt: recordedRun.startedAt,
|
||||
updatedAt: recordedRun.updatedAt,
|
||||
completedAt: recordedRun.completedAt,
|
||||
error: recordedRun.error,
|
||||
},
|
||||
error: recordedRun.error,
|
||||
message: recordedRun.error,
|
||||
expectedArtifactDirs: mapping?.expectedArtifactDirs ?? [],
|
||||
artifactScope: exported?.artifactScope,
|
||||
remoteWorkingDirectory: exported?.remoteWorkingDirectory,
|
||||
remoteWorkspaceRefKind: exported?.remoteWorkspaceRefKind,
|
||||
scopeKind: exported?.scopeKind,
|
||||
artifacts: exported?.artifacts ?? [],
|
||||
constraintSatisfied: exported?.constraintSatisfied,
|
||||
missingRequiredExtensions: exported?.missingRequiredExtensions,
|
||||
warnings: exported?.warnings ?? [],
|
||||
artifactCount: exported?.artifacts.length ?? 0,
|
||||
};
|
||||
}
|
||||
if (exported?.artifacts.length) {
|
||||
return {
|
||||
success: false,
|
||||
@ -308,6 +397,113 @@ export async function getXWorkmateTaskSnapshot(input: {
|
||||
};
|
||||
}
|
||||
|
||||
async function upsertXWorkmateTaskRun(
|
||||
api: OpenClawPluginApi,
|
||||
input: Omit<XWorkmateRecordedTaskRunV1, "schemaVersion" | "startedAt"> & {
|
||||
openclawSessionKey: string;
|
||||
startedAt?: string;
|
||||
},
|
||||
): Promise<XWorkmateRecordedTaskRunV1> {
|
||||
const patchSessionEntry = resolvePatchSessionEntry(api);
|
||||
if (!patchSessionEntry) {
|
||||
throw new Error("OpenClaw runtime session patch API is unavailable");
|
||||
}
|
||||
let recorded: XWorkmateRecordedTaskRunV1 | undefined;
|
||||
await patchSessionEntry({
|
||||
sessionKey: input.openclawSessionKey,
|
||||
fallbackEntry: {
|
||||
sessionId: input.openclawSessionKey,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
preserveActivity: true,
|
||||
update: (entry) => {
|
||||
const runs = readTaskRunsFromEntry(entry);
|
||||
const existing = runs[input.runId];
|
||||
recorded = compactObject({
|
||||
schemaVersion: 1 as const,
|
||||
runId: input.runId,
|
||||
status: input.status,
|
||||
success: input.success,
|
||||
startedAt: existing?.startedAt ?? input.startedAt ?? input.updatedAt,
|
||||
updatedAt: input.updatedAt,
|
||||
completedAt: input.completedAt,
|
||||
error: input.error,
|
||||
}) as XWorkmateRecordedTaskRunV1;
|
||||
runs[input.runId] = recorded;
|
||||
const boundedRuns = Object.fromEntries(
|
||||
Object.entries(runs)
|
||||
.sort((left, right) => right[1].updatedAt.localeCompare(left[1].updatedAt))
|
||||
.slice(0, MAX_RECORDED_TASK_RUNS),
|
||||
);
|
||||
return {
|
||||
pluginExtensions: {
|
||||
...(entry.pluginExtensions ?? {}),
|
||||
[XWORKMATE_PLUGIN_ID]: {
|
||||
...(entry.pluginExtensions?.[XWORKMATE_PLUGIN_ID] ?? {}),
|
||||
[XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE]: {
|
||||
schemaVersion: 1,
|
||||
runs: boundedRuns,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
if (!recorded) {
|
||||
throw new Error("failed to write xworkmate task run state");
|
||||
}
|
||||
return recorded;
|
||||
}
|
||||
|
||||
function readXWorkmateTaskRun(
|
||||
api: OpenClawPluginApi,
|
||||
openclawSessionKey: string,
|
||||
runId: string,
|
||||
): XWorkmateRecordedTaskRunV1 | undefined {
|
||||
const entry = resolveGetSessionEntry(api)?.({ sessionKey: openclawSessionKey });
|
||||
return readTaskRunsFromEntry(entry)[runId];
|
||||
}
|
||||
|
||||
function readTaskRunsFromEntry(entry: SessionEntry | undefined | null): Record<string, XWorkmateRecordedTaskRunV1> {
|
||||
const pluginState = asRecord(entry?.pluginExtensions?.[XWORKMATE_PLUGIN_ID]);
|
||||
const store = asRecord(pluginState?.[XWORKMATE_TASK_RUNS_EXTENSION_NAMESPACE]);
|
||||
if (store?.schemaVersion !== 1) {
|
||||
return {};
|
||||
}
|
||||
const runs = asRecord(store.runs) ?? {};
|
||||
const result: Record<string, XWorkmateRecordedTaskRunV1> = {};
|
||||
for (const [key, rawValue] of Object.entries(runs)) {
|
||||
const raw = asRecord(rawValue);
|
||||
const runId = optionalString(raw?.runId) || key;
|
||||
const status = optionalString(raw?.status);
|
||||
if (!runId || (status !== "running" && status !== "completed" && status !== "failed")) {
|
||||
continue;
|
||||
}
|
||||
result[runId] = compactObject({
|
||||
schemaVersion: 1 as const,
|
||||
runId,
|
||||
status,
|
||||
success: raw?.success === true,
|
||||
startedAt: optionalString(raw?.startedAt) || new Date(0).toISOString(),
|
||||
updatedAt: optionalString(raw?.updatedAt) || new Date(0).toISOString(),
|
||||
completedAt: optionalString(raw?.completedAt),
|
||||
error: optionalString(raw?.error),
|
||||
}) as XWorkmateRecordedTaskRunV1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function sanitizeTaskRunError(value: unknown): string | undefined {
|
||||
const raw = optionalString(value);
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
return raw
|
||||
.replace(/\b(sk|nvapi)-[A-Za-z0-9._-]+\b/gi, "$1-<redacted>")
|
||||
.replace(/(api[_ -]?key\s*[:=]\s*)[^\s,;]+/gi, "$1<redacted>")
|
||||
.slice(0, 2048);
|
||||
}
|
||||
|
||||
async function exportArtifactsForTaskLookup(
|
||||
input: { api: OpenClawPluginApi; params: Record<string, unknown> },
|
||||
params: Record<string, unknown>,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user