feat(cron): support custom session IDs and auto-bind to current session (#16511)

feat(cron): support persistent session targets for cron jobs (#9765)

Add support for `sessionTarget: "current"` and `session:<id>` so cron jobs can
bind to the creating session or a persistent named session instead of only
`main` or ephemeral `isolated` sessions.

Also:
- preserve custom session targets across reloads and restarts
- update gateway validation and normalization for the new target forms
- add cron coverage for current/custom session targets and fallback behavior
- fix merged CI regressions in Discord and diffs tests
- add a changelog entry for the new cron session behavior

Co-authored-by: kkhomej33-netizen <kkhomej33-netizen@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
This commit is contained in:
kkhomej33-netizen
2026-03-14 13:48:46 +08:00
committed by GitHub
parent 61d171ab0b
commit e7d9648fba
33 changed files with 617 additions and 118 deletions

View File

@@ -23,8 +23,7 @@ describe("renderDiffDocument", () => {
expect(rendered.html).toContain("data-openclaw-diff-root");
expect(rendered.html).toContain("src/example.ts");
expect(rendered.html).toContain("/plugins/diffs/assets/viewer.js");
expect(rendered.imageHtml).not.toContain("/plugins/diffs/assets/viewer.js");
expect(rendered.imageHtml).toContain('data-openclaw-diffs-ready="true"');
expect(rendered.imageHtml).toContain("/plugins/diffs/assets/viewer.js");
expect(rendered.imageHtml).toContain("max-width: 960px;");
expect(rendered.imageHtml).toContain("--diffs-font-size: 16px;");
expect(rendered.html).toContain("min-height: 100vh;");

View File

@@ -241,14 +241,6 @@ function renderDiffCard(payload: DiffViewerPayload): string {
</section>`;
}
function renderStaticDiffCard(prerenderedHTML: string): string {
return `<section class="oc-diff-card">
<diffs-container class="oc-diff-host" data-openclaw-diff-host>
<template shadowrootmode="open">${prerenderedHTML}</template>
</diffs-container>
</section>`;
}
function buildHtmlDocument(params: {
title: string;
bodyHtml: string;
@@ -257,7 +249,7 @@ function buildHtmlDocument(params: {
runtimeMode: "viewer" | "image";
}): string {
return `<!doctype html>
<html lang="en"${params.runtimeMode === "image" ? ' data-openclaw-diffs-ready="true"' : ""}>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -349,7 +341,7 @@ function buildHtmlDocument(params: {
${params.bodyHtml}
</div>
</main>
${params.runtimeMode === "viewer" ? `<script type="module" src="${VIEWER_LOADER_PATH}"></script>` : ""}
<script type="module" src="${VIEWER_LOADER_PATH}"></script>
</body>
</html>`;
}
@@ -360,16 +352,12 @@ type RenderedSection = {
};
function buildRenderedSection(params: {
viewerPrerenderedHtml: string;
imagePrerenderedHtml: string;
payload: Omit<DiffViewerPayload, "prerenderedHTML">;
viewerPayload: DiffViewerPayload;
imagePayload: DiffViewerPayload;
}): RenderedSection {
return {
viewer: renderDiffCard({
prerenderedHTML: params.viewerPrerenderedHtml,
...params.payload,
}),
image: renderStaticDiffCard(params.imagePrerenderedHtml),
viewer: renderDiffCard(params.viewerPayload),
image: renderDiffCard(params.imagePayload),
};
}
@@ -401,21 +389,20 @@ async function renderBeforeAfterDiff(
};
const { viewerOptions, imageOptions } = buildRenderVariants(options);
const [viewerResult, imageResult] = await Promise.all([
preloadMultiFileDiff({
preloadMultiFileDiffWithFallback({
oldFile,
newFile,
options: viewerOptions,
}),
preloadMultiFileDiff({
preloadMultiFileDiffWithFallback({
oldFile,
newFile,
options: imageOptions,
}),
]);
const section = buildRenderedSection({
viewerPrerenderedHtml: viewerResult.prerenderedHTML,
imagePrerenderedHtml: imageResult.prerenderedHTML,
payload: {
viewerPayload: {
prerenderedHTML: viewerResult.prerenderedHTML,
oldFile: viewerResult.oldFile,
newFile: viewerResult.newFile,
options: viewerOptions,
@@ -424,6 +411,16 @@ async function renderBeforeAfterDiff(
newFile: viewerResult.newFile,
}),
},
imagePayload: {
prerenderedHTML: imageResult.prerenderedHTML,
oldFile: imageResult.oldFile,
newFile: imageResult.newFile,
options: imageOptions,
langs: buildPayloadLanguages({
oldFile: imageResult.oldFile,
newFile: imageResult.newFile,
}),
},
});
return {
@@ -456,24 +453,29 @@ async function renderPatchDiff(
const sections = await Promise.all(
files.map(async (fileDiff) => {
const [viewerResult, imageResult] = await Promise.all([
preloadFileDiff({
preloadFileDiffWithFallback({
fileDiff,
options: viewerOptions,
}),
preloadFileDiff({
preloadFileDiffWithFallback({
fileDiff,
options: imageOptions,
}),
]);
return buildRenderedSection({
viewerPrerenderedHtml: viewerResult.prerenderedHTML,
imagePrerenderedHtml: imageResult.prerenderedHTML,
payload: {
viewerPayload: {
prerenderedHTML: viewerResult.prerenderedHTML,
fileDiff: viewerResult.fileDiff,
options: viewerOptions,
langs: buildPayloadLanguages({ fileDiff: viewerResult.fileDiff }),
},
imagePayload: {
prerenderedHTML: imageResult.prerenderedHTML,
fileDiff: imageResult.fileDiff,
options: imageOptions,
langs: buildPayloadLanguages({ fileDiff: imageResult.fileDiff }),
},
});
}),
);
@@ -514,3 +516,49 @@ export async function renderDiffDocument(
inputKind: input.kind,
};
}
type PreloadedFileDiffResult = Awaited<ReturnType<typeof preloadFileDiff>>;
type PreloadedMultiFileDiffResult = Awaited<ReturnType<typeof preloadMultiFileDiff>>;
function shouldFallbackToClientHydration(error: unknown): boolean {
return (
error instanceof TypeError &&
error.message.includes('needs an import attribute of "type: json"')
);
}
async function preloadFileDiffWithFallback(params: {
fileDiff: FileDiffMetadata;
options: DiffViewerOptions;
}): Promise<PreloadedFileDiffResult> {
try {
return await preloadFileDiff(params);
} catch (error) {
if (!shouldFallbackToClientHydration(error)) {
throw error;
}
return {
fileDiff: params.fileDiff,
prerenderedHTML: "",
};
}
}
async function preloadMultiFileDiffWithFallback(params: {
oldFile: FileContents;
newFile: FileContents;
options: DiffViewerOptions;
}): Promise<PreloadedMultiFileDiffResult> {
try {
return await preloadMultiFileDiff(params);
} catch (error) {
if (!shouldFallbackToClientHydration(error)) {
throw error;
}
return {
oldFile: params.oldFile,
newFile: params.newFile,
prerenderedHTML: "",
};
}
}

View File

@@ -57,7 +57,7 @@ describe("diffs tool", () => {
const cleanupSpy = vi.spyOn(store, "scheduleCleanup");
const screenshotter = createPngScreenshotter({
assertHtml: (html) => {
expect(html).not.toContain("/plugins/diffs/assets/viewer.js");
expect(html).toContain("/plugins/diffs/assets/viewer.js");
},
assertImage: (image) => {
expect(image).toMatchObject({
@@ -332,13 +332,13 @@ describe("diffs tool", () => {
const html = await store.readHtml(id);
expect(html).toContain('body data-theme="light"');
expect(html).toContain("--diffs-font-size: 17px;");
expect(html).toContain('--diffs-font-family: "JetBrains Mono"');
expect(html).toContain("JetBrains Mono");
});
it("prefers explicit tool params over configured defaults", async () => {
const screenshotter = createPngScreenshotter({
assertHtml: (html) => {
expect(html).not.toContain("/plugins/diffs/assets/viewer.js");
expect(html).toContain("/plugins/diffs/assets/viewer.js");
},
assertImage: (image) => {
expect(image).toMatchObject({