ima2-gen 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -214,7 +214,7 @@ environment variables > ~/.ima2/config.json > built-in defaults
214
214
  | `IMA2_OAUTH_PROXY_PORT` / `OAUTH_PORT` | `10531` | OAuth proxy port |
215
215
  | `IMA2_SERVER` | — | CLI target override |
216
216
  | `IMA2_CONFIG_DIR` | `~/.ima2` | Config and SQLite location |
217
- | `IMA2_GENERATED_DIR` | `generated/` | Generated image directory |
217
+ | `IMA2_GENERATED_DIR` | `~/.ima2/generated` | Generated image directory |
218
218
  | `IMA2_NO_OAUTH_PROXY` | — | Set `1` to disable auto-starting the OAuth proxy |
219
219
  | `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `30000` | Retention for opt-in terminal in-flight debug jobs |
220
220
  | `VITE_IMA2_NODE_MODE` | enabled | Set `0` at UI build time to hide node mode |
package/config.js CHANGED
@@ -108,8 +108,8 @@ export const config = {
108
108
  storage: {
109
109
  configDir,
110
110
  packageRoot,
111
- generatedDir: pickStr(env.IMA2_GENERATED_DIR, fileCfg.storage?.generatedDir, join(packageRoot, "generated")),
112
- trashDir: pickStr(env.IMA2_TRASH_DIR, fileCfg.storage?.trashDir, join(packageRoot, "generated", ".trash")),
111
+ generatedDir: pickStr(env.IMA2_GENERATED_DIR, fileCfg.storage?.generatedDir, join(configDir, "generated")),
112
+ trashDir: pickStr(env.IMA2_TRASH_DIR, fileCfg.storage?.trashDir, join(configDir, "generated", ".trash")),
113
113
  generatedDirName: pickStr(env.IMA2_GENERATED_DIRNAME, fileCfg.storage?.generatedDirName, "generated"),
114
114
  trashDirName: pickStr(env.IMA2_TRASH_DIRNAME, fileCfg.storage?.trashDirName, ".trash"),
115
115
  dbPath: pickStr(env.IMA2_DB_PATH, fileCfg.storage?.dbPath, join(configDir, "sessions.db")),
@@ -8,6 +8,7 @@ const TRASH = config.storage.trashDirName;
8
8
  const TRASH_TTL_MS = config.trash.ttlMs;
9
9
 
10
10
  function resolveInGenerated(rootDir, relPath) {
11
+ void rootDir;
11
12
  if (typeof relPath !== "string" || relPath.length === 0) {
12
13
  const err = new Error("filename required");
13
14
  err.status = 400;
@@ -20,7 +21,7 @@ function resolveInGenerated(rootDir, relPath) {
20
21
  err.code = "INVALID_FILENAME";
21
22
  throw err;
22
23
  }
23
- const baseDir = resolve(rootDir, DIR);
24
+ const baseDir = resolve(config.storage.generatedDir);
24
25
  const target = resolve(baseDir, relPath);
25
26
  if (target !== baseDir && !target.startsWith(baseDir + sep)) {
26
27
  const err = new Error("filename escapes generated/");
@@ -78,7 +79,7 @@ export async function trashAsset(rootDir, filename) {
78
79
  err.code = "ASSET_NOT_FOUND";
79
80
  throw err;
80
81
  }
81
- const trashDir = resolve(rootDir, DIR, TRASH);
82
+ const trashDir = resolve(config.storage.trashDir);
82
83
  await mkdir(trashDir, { recursive: true });
83
84
  // Flatten filename (subdir separators -> __) so trash is flat & easy to restore
84
85
  const flat = filename.replace(/[\\/]+/g, "__");
@@ -107,7 +108,8 @@ export async function trashAsset(rootDir, filename) {
107
108
  }
108
109
 
109
110
  export async function restoreAsset(rootDir, trashId, originalFilename) {
110
- const trashDir = resolve(rootDir, DIR, TRASH);
111
+ void rootDir;
112
+ const trashDir = resolve(config.storage.trashDir);
111
113
  const src = resolve(trashDir, trashId);
112
114
  if (!src.startsWith(trashDir + sep) && src !== trashDir) {
113
115
  const err = new Error("invalid trashId");
@@ -52,6 +52,7 @@ export async function listHistoryRows(baseDir = config.storage.generatedDir) {
52
52
  nodeId: meta?.nodeId || null,
53
53
  parentNodeId: meta?.parentNodeId || null,
54
54
  clientNodeId: meta?.clientNodeId || null,
55
+ requestId: meta?.requestId || null,
55
56
  kind: meta?.kind || null,
56
57
  refsCount: Number.isFinite(meta?.refsCount) ? meta.refsCount : 0,
57
58
  };
package/lib/nodeStore.js CHANGED
@@ -1,23 +1,23 @@
1
- import { writeFile, readFile, access } from "fs/promises";
1
+ import { writeFile, readFile, access, mkdir } from "fs/promises";
2
2
  import { join, resolve, sep } from "path";
3
3
  import { randomBytes } from "crypto";
4
4
  import { config } from "../config.js";
5
5
 
6
- const DIR = config.storage.generatedDirName;
7
-
8
6
  export function newNodeId() {
9
7
  return "n_" + randomBytes(config.ids.nodeHexBytes).toString("hex");
10
8
  }
11
9
 
12
- export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png" }) {
10
+ export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png", generatedDir = config.storage.generatedDir }) {
11
+ void rootDir;
13
12
  const filename = `${nodeId}.${ext}`;
14
- await writeFile(join(rootDir, DIR, filename), Buffer.from(b64, "base64"));
15
- await writeFile(join(rootDir, DIR, filename + ".json"), JSON.stringify(meta, null, 2));
13
+ await mkdir(generatedDir, { recursive: true });
14
+ await writeFile(join(generatedDir, filename), Buffer.from(b64, "base64"));
15
+ await writeFile(join(generatedDir, filename + ".json"), JSON.stringify(meta, null, 2));
16
16
  return { filename };
17
17
  }
18
18
 
19
- export async function loadNodeB64(rootDir, filename) {
20
- const p = resolveGeneratedPath(rootDir, filename);
19
+ export async function loadNodeB64(rootDir, filename, generatedDir = config.storage.generatedDir) {
20
+ const p = resolveGeneratedPath(rootDir, filename, generatedDir);
21
21
  try { await access(p); } catch {
22
22
  const err = new Error(`Node file not found: ${filename}`);
23
23
  err.code = "NODE_NOT_FOUND";
@@ -28,16 +28,17 @@ export async function loadNodeB64(rootDir, filename) {
28
28
  return buf.toString("base64");
29
29
  }
30
30
 
31
- export async function loadNodeMeta(rootDir, nodeId, ext = "png") {
31
+ export async function loadNodeMeta(rootDir, nodeId, ext = "png", generatedDir = config.storage.generatedDir) {
32
+ void rootDir;
32
33
  try {
33
- return JSON.parse(await readFile(join(rootDir, DIR, `${nodeId}.${ext}.json`), "utf-8"));
34
+ return JSON.parse(await readFile(join(generatedDir, `${nodeId}.${ext}.json`), "utf-8"));
34
35
  } catch {
35
36
  return null;
36
37
  }
37
38
  }
38
39
 
39
- export async function loadAssetB64(rootDir, externalSrc) {
40
- const p = resolveGeneratedPath(rootDir, externalSrc);
40
+ export async function loadAssetB64(rootDir, externalSrc, generatedDir = config.storage.generatedDir) {
41
+ const p = resolveGeneratedPath(rootDir, externalSrc, generatedDir);
41
42
  try { await access(p); } catch {
42
43
  const err = new Error(`Asset file not found: ${externalSrc}`);
43
44
  err.code = "NODE_NOT_FOUND";
@@ -48,14 +49,15 @@ export async function loadAssetB64(rootDir, externalSrc) {
48
49
  return buf.toString("base64");
49
50
  }
50
51
 
51
- function resolveGeneratedPath(rootDir, relPath) {
52
+ function resolveGeneratedPath(rootDir, relPath, generatedDir = config.storage.generatedDir) {
53
+ void rootDir;
52
54
  if (typeof relPath !== "string" || relPath.length === 0) {
53
55
  const err = new Error("Asset path is required");
54
56
  err.code = "NODE_SOURCE_INVALID";
55
57
  err.status = 400;
56
58
  throw err;
57
59
  }
58
- const baseDir = resolve(rootDir, DIR);
60
+ const baseDir = resolve(generatedDir);
59
61
  const target = resolve(baseDir, relPath);
60
62
  if (target !== baseDir && !target.startsWith(baseDir + sep)) {
61
63
  const err = new Error(`Asset path escapes generated/: ${relPath}`);
package/lib/oauthProxy.js CHANGED
@@ -46,6 +46,24 @@ function extractSseData(block) {
46
46
  return eventData;
47
47
  }
48
48
 
49
+ function extractPartialImage(data) {
50
+ if (typeof data?.type !== "string" || !data.type.includes("partial")) return null;
51
+ const item = data.item || {};
52
+ const b64 =
53
+ data.partial_image ||
54
+ data.image ||
55
+ data.result ||
56
+ item.partial_image ||
57
+ item.image ||
58
+ item.result;
59
+ if (typeof b64 !== "string" || b64.length === 0) return null;
60
+ const index =
61
+ Number.isFinite(data.index) ? data.index :
62
+ Number.isFinite(item.index) ? item.index :
63
+ null;
64
+ return { b64, index, eventType: data.type };
65
+ }
66
+
49
67
  function makeOAuthError(message, { status, code = "OAUTH_UPSTREAM_ERROR", upstreamBodyChars, eventType } = {}) {
50
68
  const err = new Error(message);
51
69
  err.code = code;
@@ -55,7 +73,7 @@ function makeOAuthError(message, { status, code = "OAUTH_UPSTREAM_ERROR", upstre
55
73
  return err;
56
74
  }
57
75
 
58
- async function readImageStream(res, { requestId = null, scope = "oauth" } = {}) {
76
+ async function readImageStream(res, { requestId = null, scope = "oauth", onPartialImage = null } = {}) {
59
77
  const reader = res.body.getReader();
60
78
  const decoder = new TextDecoder();
61
79
  let buffer = "";
@@ -81,6 +99,17 @@ async function readImageStream(res, { requestId = null, scope = "oauth" } = {})
81
99
  const data = JSON.parse(eventData);
82
100
  eventCount++;
83
101
 
102
+ const partial = extractPartialImage(data);
103
+ if (partial) {
104
+ logEvent(scope, "partial", {
105
+ requestId,
106
+ index: partial.index,
107
+ imageChars: partial.b64.length,
108
+ eventType: partial.eventType,
109
+ });
110
+ if (requestId) setJobPhase(requestId, "partial");
111
+ if (typeof onPartialImage === "function") onPartialImage(partial);
112
+ }
84
113
  if (data.type === "response.output_item.done" && data.item?.type === "image_generation_call") {
85
114
  if (data.item.result) {
86
115
  imageB64 = data.item.result;
@@ -123,11 +152,18 @@ export async function generateViaOAuth(
123
152
  requestId = null,
124
153
  mode = "auto",
125
154
  ctx = {},
155
+ options = {},
126
156
  ) {
127
157
  const oauthUrl = getOAuthUrl(ctx);
128
158
  const tools = [
129
159
  { type: "web_search" },
130
- { type: "image_generation", quality, size, moderation },
160
+ {
161
+ type: "image_generation",
162
+ quality,
163
+ size,
164
+ moderation,
165
+ ...(options.partialImages ? { partial_images: options.partialImages } : {}),
166
+ },
131
167
  ];
132
168
 
133
169
  const textPrompt = buildUserTextPrompt(prompt, mode);
@@ -187,7 +223,11 @@ export async function generateViaOAuth(
187
223
  throw new Error("No image data in response (non-stream mode)");
188
224
  }
189
225
 
190
- const { imageB64, usage, webSearchCalls, revisedPrompt, eventCount } = await readImageStream(res, { requestId, scope: "oauth" });
226
+ const { imageB64, usage, webSearchCalls, revisedPrompt, eventCount } = await readImageStream(res, {
227
+ requestId,
228
+ scope: "oauth",
229
+ onPartialImage: options.onPartialImage,
230
+ });
191
231
  logEvent("oauth", "stream_end", { requestId, events: eventCount, hasImage: !!imageB64 });
192
232
 
193
233
  if (!imageB64) {
@@ -0,0 +1,91 @@
1
+ import { mkdir, readdir, copyFile, stat } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join, resolve, sep } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ const PACKAGE_NAME = "ima2-gen";
7
+
8
+ function addStats(a, b) {
9
+ return {
10
+ copied: a.copied + b.copied,
11
+ skippedExisting: a.skippedExisting + b.skippedExisting,
12
+ };
13
+ }
14
+
15
+ async function copyMissingTree(srcDir, dstDir) {
16
+ await mkdir(dstDir, { recursive: true });
17
+ const entries = await readdir(srcDir, { withFileTypes: true });
18
+ let stats = { copied: 0, skippedExisting: 0 };
19
+ for (const entry of entries) {
20
+ const src = join(srcDir, entry.name);
21
+ const dst = join(dstDir, entry.name);
22
+ if (entry.isDirectory()) {
23
+ stats = addStats(stats, await copyMissingTree(src, dst));
24
+ continue;
25
+ }
26
+ if (!entry.isFile()) continue;
27
+ if (existsSync(dst)) {
28
+ stats.skippedExisting += 1;
29
+ continue;
30
+ }
31
+ await copyFile(src, dst);
32
+ stats.copied += 1;
33
+ }
34
+ return stats;
35
+ }
36
+
37
+ function isSameOrInside(child, parent) {
38
+ const a = resolve(child);
39
+ const b = resolve(parent);
40
+ return a === b || a.startsWith(b + sep);
41
+ }
42
+
43
+ export async function migrateGeneratedStorage(ctx, options = {}) {
44
+ const targetDir = ctx.config.storage.generatedDir;
45
+ const candidates = options.legacyDirs || getLegacyGeneratedCandidates(ctx, options.env);
46
+ const result = {
47
+ copied: 0,
48
+ skippedExisting: 0,
49
+ sourcesScanned: 0,
50
+ sourcesSkipped: 0,
51
+ };
52
+ try {
53
+ for (const legacyDir of candidates) {
54
+ if (isSameOrInside(legacyDir, targetDir) || isSameOrInside(targetDir, legacyDir)) {
55
+ result.sourcesSkipped += 1;
56
+ continue;
57
+ }
58
+ try {
59
+ const legacyStat = await stat(legacyDir);
60
+ if (!legacyStat.isDirectory()) continue;
61
+ result.sourcesScanned += 1;
62
+ const copyStats = await copyMissingTree(legacyDir, targetDir);
63
+ result.copied += copyStats.copied;
64
+ result.skippedExisting += copyStats.skippedExisting;
65
+ } catch (err) {
66
+ if (err?.code !== "ENOENT") {
67
+ console.warn("[storage] generated asset migration source skipped:", legacyDir, err.message);
68
+ }
69
+ }
70
+ }
71
+ if (result.copied > 0) console.log(`[storage] migrated ${result.copied} generated assets to ${targetDir}`);
72
+ } catch (err) {
73
+ console.warn("[storage] generated asset migration skipped:", err.message);
74
+ }
75
+ return result;
76
+ }
77
+
78
+ export function getLegacyGeneratedCandidates(ctx, env = process.env) {
79
+ const home = homedir();
80
+ const nodePrefix = dirname(dirname(process.execPath));
81
+ const npmPrefix = env.npm_config_prefix || nodePrefix;
82
+ const appData = env.APPDATA || join(home, "AppData", "Roaming");
83
+ return Array.from(new Set([
84
+ join(ctx.rootDir, "generated"),
85
+ join(npmPrefix, "lib", "node_modules", PACKAGE_NAME, "generated"),
86
+ join(npmPrefix, "node_modules", PACKAGE_NAME, "generated"),
87
+ join(appData, "npm", "node_modules", PACKAGE_NAME, "generated"),
88
+ join(home, ".npm-global", "lib", "node_modules", PACKAGE_NAME, "generated"),
89
+ join(home, ".nvm", "versions", "node", process.version, "lib", "node_modules", PACKAGE_NAME, "generated"),
90
+ ].map((p) => resolve(p))));
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "GPT Image 2 generator with OAuth & API key support",
5
5
  "type": "module",
6
6
  "bin": {
package/routes/nodes.js CHANGED
@@ -22,9 +22,40 @@ function validateModeration(ctx, moderation) {
22
22
  return { moderation };
23
23
  }
24
24
 
25
+ function wantsSse(req) {
26
+ const accept = typeof req.headers.accept === "string" ? req.headers.accept : "";
27
+ return accept.includes("text/event-stream");
28
+ }
29
+
30
+ function writeSse(res, event, data) {
31
+ res.write(`event: ${event}\n`);
32
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
33
+ }
34
+
35
+ function writeNodeError(res, status, code, message, parentNodeId) {
36
+ if (res.headersSent) {
37
+ writeSse(res, "error", {
38
+ error: { code, message },
39
+ parentNodeId,
40
+ status,
41
+ });
42
+ res.end();
43
+ return;
44
+ }
45
+ res.status(status).json({
46
+ error: { code, message },
47
+ parentNodeId,
48
+ });
49
+ }
50
+
51
+ function dataUrlFromB64(format, b64) {
52
+ return `data:image/${format === "jpeg" ? "jpeg" : format};base64,${b64}`;
53
+ }
54
+
25
55
  export function registerNodeRoutes(app, ctx) {
26
56
  app.post("/api/node/generate", async (req, res) => {
27
57
  const body = req.body || {};
58
+ const streamResponse = wantsSse(req);
28
59
  const parentNodeId = body.parentNodeId ?? null;
29
60
  const requestId = typeof body.requestId === "string" ? body.requestId : null;
30
61
  const sessionId = typeof body.sessionId === "string" ? body.sessionId : null;
@@ -125,9 +156,9 @@ export function registerNodeRoutes(app, ctx) {
125
156
  const startTime = Date.now();
126
157
  let parentB64 = null;
127
158
  if (parentNodeId) {
128
- parentB64 = await loadNodeB64(ctx.rootDir, `${parentNodeId}.png`);
159
+ parentB64 = await loadNodeB64(ctx.rootDir, `${parentNodeId}.png`, ctx.config.storage.generatedDir);
129
160
  } else if (typeof externalSrc === "string" && externalSrc.length > 0) {
130
- parentB64 = await loadAssetB64(ctx.rootDir, externalSrc);
161
+ parentB64 = await loadAssetB64(ctx.rootDir, externalSrc, ctx.config.storage.generatedDir);
131
162
  }
132
163
  logEvent("node", "request", {
133
164
  requestId,
@@ -144,6 +175,15 @@ export function registerNodeRoutes(app, ctx) {
144
175
  styleSheetApplied: !!styleSheetApplied,
145
176
  });
146
177
 
178
+ if (streamResponse) {
179
+ res.writeHead(200, {
180
+ "Content-Type": "text/event-stream; charset=utf-8",
181
+ "Cache-Control": "no-cache, no-transform",
182
+ Connection: "keep-alive",
183
+ });
184
+ writeSse(res, "phase", { requestId, phase: "streaming" });
185
+ }
186
+
147
187
  let b64, usage, webSearchCalls = 0, revisedPrompt = null;
148
188
  const MAX_RETRIES = 1;
149
189
  let lastErr;
@@ -151,7 +191,27 @@ export function registerNodeRoutes(app, ctx) {
151
191
  try {
152
192
  const r = parentB64
153
193
  ? await editViaOAuth(effectivePrompt, parentB64, quality, size, moderation, normalizedPromptMode, ctx, requestId)
154
- : await generateViaOAuth(effectivePrompt, quality, size, moderation, refCheck.refs, requestId, normalizedPromptMode, ctx);
194
+ : await generateViaOAuth(
195
+ effectivePrompt,
196
+ quality,
197
+ size,
198
+ moderation,
199
+ refCheck.refs,
200
+ requestId,
201
+ normalizedPromptMode,
202
+ ctx,
203
+ {
204
+ partialImages: streamResponse ? 2 : 0,
205
+ onPartialImage: streamResponse
206
+ ? (partial) =>
207
+ writeSse(res, "partial", {
208
+ requestId,
209
+ image: dataUrlFromB64(format, partial.b64),
210
+ index: partial.index,
211
+ })
212
+ : null,
213
+ },
214
+ );
155
215
  if (r.b64) {
156
216
  b64 = r.b64;
157
217
  usage = r.usage;
@@ -172,10 +232,13 @@ export function registerNodeRoutes(app, ctx) {
172
232
  finishStatus = "error";
173
233
  finishHttpStatus = 422;
174
234
  finishErrorCode = "SAFETY_REFUSAL";
175
- return res.status(422).json({
176
- error: { code: "SAFETY_REFUSAL", message: lastErr?.message || "Empty response after retry" },
235
+ return writeNodeError(
236
+ res,
237
+ 422,
238
+ "SAFETY_REFUSAL",
239
+ lastErr?.message || "Empty response after retry",
177
240
  parentNodeId,
178
- });
241
+ );
179
242
  }
180
243
 
181
244
  const nodeId = newNodeId();
@@ -199,6 +262,7 @@ export function registerNodeRoutes(app, ctx) {
199
262
  webSearchCalls,
200
263
  provider: "oauth",
201
264
  kind: parentB64 ? "edit" : "generate",
265
+ requestId,
202
266
  refsCount: refCheck.refs.length,
203
267
  quality,
204
268
  size,
@@ -206,7 +270,13 @@ export function registerNodeRoutes(app, ctx) {
206
270
  moderation,
207
271
  };
208
272
  await mkdir(ctx.config.storage.generatedDir, { recursive: true });
209
- const { filename } = await saveNode(ctx.rootDir, { nodeId, b64, meta, ext: format });
273
+ const { filename } = await saveNode(ctx.rootDir, {
274
+ nodeId,
275
+ b64,
276
+ meta,
277
+ ext: format,
278
+ generatedDir: ctx.config.storage.generatedDir,
279
+ });
210
280
  finishMeta = { nodeId, filename, imageChars: b64.length };
211
281
  finishHttpStatus = 200;
212
282
  logEvent("node", "saved", {
@@ -217,11 +287,11 @@ export function registerNodeRoutes(app, ctx) {
217
287
  elapsedMs: Date.now() - startTime,
218
288
  });
219
289
 
220
- res.json({
290
+ const payload = {
221
291
  nodeId,
222
292
  parentNodeId,
223
293
  requestId,
224
- image: `data:image/${format === "jpeg" ? "jpeg" : format};base64,${b64}`,
294
+ image: dataUrlFromB64(format, b64),
225
295
  filename,
226
296
  url: `/generated/${filename}`,
227
297
  elapsed,
@@ -233,17 +303,21 @@ export function registerNodeRoutes(app, ctx) {
233
303
  warnings: qualityWarnings,
234
304
  revisedPrompt,
235
305
  promptMode: normalizedPromptMode,
236
- });
306
+ };
307
+
308
+ if (streamResponse) {
309
+ writeSse(res, "done", payload);
310
+ res.end();
311
+ } else {
312
+ res.json(payload);
313
+ }
237
314
  } catch (err) {
238
315
  const code = err.code || classifyUpstreamError(err.message) || "NODE_GEN_FAILED";
239
316
  finishStatus = "error";
240
317
  finishHttpStatus = err.status || 500;
241
318
  finishErrorCode = code;
242
319
  logError("node", "error", err, { requestId, code, parentNodeId, sessionId, clientNodeId });
243
- res.status(err.status || 500).json({
244
- error: { code, message: err.message },
245
- parentNodeId,
246
- });
320
+ writeNodeError(res, err.status || 500, code, err.message, parentNodeId);
247
321
  } finally {
248
322
  finishJob(requestId, {
249
323
  status: finishStatus,
@@ -257,7 +331,7 @@ export function registerNodeRoutes(app, ctx) {
257
331
  app.get("/api/node/:nodeId", async (req, res) => {
258
332
  try {
259
333
  const { nodeId } = req.params;
260
- const meta = await loadNodeMeta(ctx.rootDir, nodeId);
334
+ const meta = await loadNodeMeta(ctx.rootDir, nodeId, "png", ctx.config.storage.generatedDir);
261
335
  if (!meta) {
262
336
  return res.status(404).json({ error: { code: "NODE_NOT_FOUND", message: "Node metadata missing" } });
263
337
  }
@@ -266,7 +266,9 @@ export function registerSessionRoutes(app, ctx) {
266
266
  const code = err.code || "DB_ERROR";
267
267
  const payload = { error: { code, message: err.message } };
268
268
  if (typeof err.currentVersion === "number") payload.currentVersion = err.currentVersion;
269
- logError("session", "graph_error", err, { sessionId: req.params.id, code });
269
+ if (code !== "GRAPH_VERSION_CONFLICT") {
270
+ logError("session", "graph_error", err, { sessionId: req.params.id, code });
271
+ }
270
272
  res.status(err.status || 500).json(payload);
271
273
  }
272
274
  });
package/server.js CHANGED
@@ -13,6 +13,7 @@ import { fileURLToPath, pathToFileURL } from "url";
13
13
  import { onShutdown } from "./bin/lib/platform.js";
14
14
  import { ensureDefaultSession } from "./lib/sessionStore.js";
15
15
  import { startOAuthProxy } from "./lib/oauthLauncher.js";
16
+ import { migrateGeneratedStorage } from "./lib/storageMigration.js";
16
17
  import { configureRoutes } from "./routes/index.js";
17
18
  import { config } from "./config.js";
18
19
 
@@ -114,6 +115,7 @@ export async function createRuntimeContext(overrides = {}) {
114
115
 
115
116
  export async function startServer(overrides = {}) {
116
117
  const ctx = await createRuntimeContext(overrides);
118
+ await migrateGeneratedStorage(ctx);
117
119
  const app = buildApp(ctx);
118
120
  const oauthChild =
119
121
  overrides.oauthChild !== undefined