Persist worker tasks and elevate worker results

Co-authored-by: Luke The Dev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-04-19 13:59:58 +00:00
co-authored by Luke The Dev
parent 6f7acbf81a
commit 7aa272efb6
3 changed files with 93 additions and 9 deletions
+81 -2
View File
@@ -4,11 +4,10 @@ const os = require("node:os");
const path = require("node:path");
const { randomUUID } = require("node:crypto");
const THREE = require("three");
const DEFAULT_PORT = Number.parseInt(process.env.CLAW3D_STUDIO_PROVIDER_PORT || "3333", 10);
const DEFAULT_HOST = process.env.CLAW3D_STUDIO_PROVIDER_HOST || "127.0.0.1";
const TASK_TIMEOUT_MS = 1_200;
const TASK_METADATA_FILENAME = "task.json";
const respondJson = (res, statusCode, body) => {
res.writeHead(statusCode, {
@@ -58,6 +57,73 @@ const resolveWorkerDir = () => {
return dir;
};
const writeTaskMetadata = (taskDir, task) => {
const metadata = {
id: task.id,
adapterId: task.adapterId,
status: task.status,
progress: task.progress,
createdAt: task.createdAt,
startedAt: task.startedAt,
finishedAt: task.finishedAt,
modelPath: task.modelPath ? path.basename(task.modelPath) : null,
thumbnailPath: task.thumbnailPath ? path.basename(task.thumbnailPath) : null,
errorMessage: task.errorMessage,
sourceImagePath: task.sourceImagePath ? path.basename(task.sourceImagePath) : null,
palette: task.palette,
size: task.size,
};
fs.writeFileSync(
path.join(taskDir, TASK_METADATA_FILENAME),
JSON.stringify(metadata, null, 2),
"utf8",
);
};
const loadTaskMetadata = (rootDir, taskId) => {
const taskDir = path.join(rootDir, taskId);
const metadataPath = path.join(taskDir, TASK_METADATA_FILENAME);
if (!fs.existsSync(metadataPath)) return null;
const raw = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
const revivePath = (fileName) => (typeof fileName === "string" && fileName ? path.join(taskDir, fileName) : null);
const task = {
id: typeof raw.id === "string" ? raw.id : taskId,
adapterId: typeof raw.adapterId === "string" && raw.adapterId ? raw.adapterId : "heightfield-relief",
status:
raw.status === "PENDING" ||
raw.status === "IN_PROGRESS" ||
raw.status === "SUCCEEDED" ||
raw.status === "FAILED" ||
raw.status === "CANCELED"
? raw.status
: "FAILED",
progress: Number.isFinite(raw.progress) ? raw.progress : 0,
createdAt: Number.isFinite(raw.createdAt) ? raw.createdAt : Date.now(),
startedAt: Number.isFinite(raw.startedAt) ? raw.startedAt : 0,
finishedAt: Number.isFinite(raw.finishedAt) ? raw.finishedAt : 0,
modelPath: revivePath(raw.modelPath),
thumbnailPath: revivePath(raw.thumbnailPath),
errorMessage: typeof raw.errorMessage === "string" ? raw.errorMessage : "",
sourceImagePath: revivePath(raw.sourceImagePath),
palette: Array.isArray(raw.palette) ? raw.palette : [],
size:
raw.size && typeof raw.size === "object"
? {
width: Number.isFinite(raw.size.width) ? raw.size.width : 1024,
height: Number.isFinite(raw.size.height) ? raw.size.height : 1024,
}
: { width: 1024, height: 1024 },
};
if (task.status === "PENDING" || task.status === "IN_PROGRESS") {
task.status = "FAILED";
task.progress = 100;
task.finishedAt = Date.now();
task.errorMessage = "Worker restarted before task completion.";
writeTaskMetadata(taskDir, task);
}
return task;
};
const hex = (value) => value.toString(16).padStart(2, "0");
const rgbToHex = (red, green, blue) => `#${hex(red)}${hex(green)}${hex(blue)}`;
@@ -291,11 +357,13 @@ const createTaskStore = () => {
size: { width: 1024, height: 1024 },
};
tasks.set(taskId, task);
writeTaskMetadata(taskDir, task);
setTimeout(async () => {
task.status = "IN_PROGRESS";
task.progress = 18;
task.startedAt = Date.now();
writeTaskMetadata(taskDir, task);
try {
const result = await adapter.generate({
buffer: params.buffer,
@@ -312,11 +380,13 @@ const createTaskStore = () => {
task.progress = 100;
task.status = "SUCCEEDED";
task.finishedAt = Date.now();
writeTaskMetadata(taskDir, task);
} catch (error) {
task.status = "FAILED";
task.progress = 100;
task.finishedAt = Date.now();
task.errorMessage = error instanceof Error ? error.message : String(error);
writeTaskMetadata(taskDir, task);
}
}, TASK_TIMEOUT_MS);
@@ -324,6 +394,14 @@ const createTaskStore = () => {
};
return {
initialize() {
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const task = loadTaskMetadata(rootDir, entry.name);
if (!task) continue;
tasks.set(task.id, task);
}
},
createTask,
getTask(taskId, baseUrl) {
const task = tasks.get(taskId);
@@ -344,6 +422,7 @@ const createStudioAiWorkerServer = (params = {}) => {
const host = params.host || DEFAULT_HOST;
const port = Number.isFinite(params.port) ? params.port : DEFAULT_PORT;
const taskStore = createTaskStore();
taskStore.initialize();
const server = http.createServer(async (req, res) => {
if (!req.url) {
-2
View File
@@ -15,9 +15,7 @@ import {
buildStudioAiProviderAvailability,
createSelfHostedImageTo3dTask,
getSelfHostedImageTo3dTask,
getDefaultSelfHostedProviderUrl,
isRealStudioAiEnabled,
resolveStudioAiProvider,
} from "@/lib/studio-world/provider";
import {
createStudioProject,
@@ -721,7 +721,7 @@ export function StudioWorldScreen() {
Project library
</div>
<div className="mt-1 text-sm text-muted-foreground">
Generated drafts persist locally in Studio.
Generated drafts and worker-backed results persist locally in Studio.
</div>
</div>
<div className="rounded-full border border-border/60 px-2 py-1 font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
@@ -781,9 +781,16 @@ export function StudioWorldScreen() {
</div>
{project.externalModel ? (
<div className="mt-3 rounded-xl border border-border/60 bg-surface-1/35 p-2 text-xs text-muted-foreground">
AI task {project.externalModel.status} {project.externalModel.progress}%.
{project.externalModel.glbUrl ? " GLB ready from self-hosted AI." : ""}
{project.externalModel.errorMessage ? ` ${project.externalModel.errorMessage}` : ""}
<div>
AI task {project.externalModel.status} {project.externalModel.progress}%.
{project.externalModel.glbUrl ? " GLB ready from self-hosted AI." : ""}
{project.externalModel.errorMessage ? ` ${project.externalModel.errorMessage}` : ""}
</div>
{project.externalModel.glbUrl ? (
<div className="mt-2 font-medium text-foreground">
Worker result is ready and should be treated as the primary artifact.
</div>
) : null}
</div>
) : null}
{project.externalModel?.thumbnailUrl ? (
@@ -838,7 +845,7 @@ export function StudioWorldScreen() {
{project.externalModel?.glbUrl ? (
<button
type="button"
className="ui-btn-secondary px-3 py-1.5 text-xs"
className="ui-btn-primary px-3 py-1.5 text-xs"
onClick={() => void handleExportProviderGlb(project)}
disabled={busy}
>