ima2-gen 1.0.7 → 1.0.8
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 +1 -1
- package/config.js +2 -2
- package/lib/assetLifecycle.js +5 -3
- package/lib/historyList.js +1 -0
- package/lib/nodeStore.js +7 -6
- package/lib/oauthProxy.js +43 -3
- package/lib/storageMigration.js +41 -0
- package/package.json +1 -1
- package/routes/nodes.js +79 -11
- package/routes/sessions.js +9 -1
- package/server.js +2 -0
- package/ui/dist/assets/{index-Jcs5Q1sj.css → index-CBrmEeD7.css} +1 -1
- package/ui/dist/assets/index-DRST1V_0.js +22 -0
- package/ui/dist/assets/index-DRST1V_0.js.map +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-D1MUxZaB.js +0 -16
- package/ui/dist/assets/index-D1MUxZaB.js.map +0 -1
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` | `
|
|
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(
|
|
112
|
-
trashDir: pickStr(env.IMA2_TRASH_DIR, fileCfg.storage?.trashDir, join(
|
|
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")),
|
package/lib/assetLifecycle.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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");
|
package/lib/historyList.js
CHANGED
|
@@ -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
|
@@ -3,16 +3,15 @@ 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
10
|
export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png" }) {
|
|
11
|
+
void rootDir;
|
|
13
12
|
const filename = `${nodeId}.${ext}`;
|
|
14
|
-
await writeFile(join(
|
|
15
|
-
await writeFile(join(
|
|
13
|
+
await writeFile(join(config.storage.generatedDir, filename), Buffer.from(b64, "base64"));
|
|
14
|
+
await writeFile(join(config.storage.generatedDir, filename + ".json"), JSON.stringify(meta, null, 2));
|
|
16
15
|
return { filename };
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -29,8 +28,9 @@ export async function loadNodeB64(rootDir, filename) {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
export async function loadNodeMeta(rootDir, nodeId, ext = "png") {
|
|
31
|
+
void rootDir;
|
|
32
32
|
try {
|
|
33
|
-
return JSON.parse(await readFile(join(
|
|
33
|
+
return JSON.parse(await readFile(join(config.storage.generatedDir, `${nodeId}.${ext}.json`), "utf-8"));
|
|
34
34
|
} catch {
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
@@ -49,13 +49,14 @@ export async function loadAssetB64(rootDir, externalSrc) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function resolveGeneratedPath(rootDir, relPath) {
|
|
52
|
+
void rootDir;
|
|
52
53
|
if (typeof relPath !== "string" || relPath.length === 0) {
|
|
53
54
|
const err = new Error("Asset path is required");
|
|
54
55
|
err.code = "NODE_SOURCE_INVALID";
|
|
55
56
|
err.status = 400;
|
|
56
57
|
throw err;
|
|
57
58
|
}
|
|
58
|
-
const baseDir = resolve(
|
|
59
|
+
const baseDir = resolve(config.storage.generatedDir);
|
|
59
60
|
const target = resolve(baseDir, relPath);
|
|
60
61
|
if (target !== baseDir && !target.startsWith(baseDir + sep)) {
|
|
61
62
|
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
|
-
{
|
|
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, {
|
|
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,41 @@
|
|
|
1
|
+
import { mkdir, readdir, copyFile, stat } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
async function copyMissingTree(srcDir, dstDir) {
|
|
6
|
+
await mkdir(dstDir, { recursive: true });
|
|
7
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
const src = join(srcDir, entry.name);
|
|
10
|
+
const dst = join(dstDir, entry.name);
|
|
11
|
+
if (entry.isDirectory()) {
|
|
12
|
+
await copyMissingTree(src, dst);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (!entry.isFile()) continue;
|
|
16
|
+
if (existsSync(dst)) continue;
|
|
17
|
+
await copyFile(src, dst);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isSameOrInside(child, parent) {
|
|
22
|
+
const a = resolve(child);
|
|
23
|
+
const b = resolve(parent);
|
|
24
|
+
return a === b || a.startsWith(b + sep);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function migrateGeneratedStorage(ctx) {
|
|
28
|
+
const legacyDir = join(ctx.rootDir, "generated");
|
|
29
|
+
const targetDir = ctx.config.storage.generatedDir;
|
|
30
|
+
if (isSameOrInside(legacyDir, targetDir) || isSameOrInside(targetDir, legacyDir)) return;
|
|
31
|
+
try {
|
|
32
|
+
const legacyStat = await stat(legacyDir);
|
|
33
|
+
if (!legacyStat.isDirectory()) return;
|
|
34
|
+
await copyMissingTree(legacyDir, targetDir);
|
|
35
|
+
console.log(`[storage] migrated generated assets to ${targetDir}`);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err?.code !== "ENOENT") {
|
|
38
|
+
console.warn("[storage] generated asset migration skipped:", err.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
CHANGED
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;
|
|
@@ -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(
|
|
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
|
|
176
|
-
|
|
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,
|
|
@@ -217,11 +281,11 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
217
281
|
elapsedMs: Date.now() - startTime,
|
|
218
282
|
});
|
|
219
283
|
|
|
220
|
-
|
|
284
|
+
const payload = {
|
|
221
285
|
nodeId,
|
|
222
286
|
parentNodeId,
|
|
223
287
|
requestId,
|
|
224
|
-
image:
|
|
288
|
+
image: dataUrlFromB64(format, b64),
|
|
225
289
|
filename,
|
|
226
290
|
url: `/generated/${filename}`,
|
|
227
291
|
elapsed,
|
|
@@ -233,17 +297,21 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
233
297
|
warnings: qualityWarnings,
|
|
234
298
|
revisedPrompt,
|
|
235
299
|
promptMode: normalizedPromptMode,
|
|
236
|
-
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (streamResponse) {
|
|
303
|
+
writeSse(res, "done", payload);
|
|
304
|
+
res.end();
|
|
305
|
+
} else {
|
|
306
|
+
res.json(payload);
|
|
307
|
+
}
|
|
237
308
|
} catch (err) {
|
|
238
309
|
const code = err.code || classifyUpstreamError(err.message) || "NODE_GEN_FAILED";
|
|
239
310
|
finishStatus = "error";
|
|
240
311
|
finishHttpStatus = err.status || 500;
|
|
241
312
|
finishErrorCode = code;
|
|
242
313
|
logError("node", "error", err, { requestId, code, parentNodeId, sessionId, clientNodeId });
|
|
243
|
-
res
|
|
244
|
-
error: { code, message: err.message },
|
|
245
|
-
parentNodeId,
|
|
246
|
-
});
|
|
314
|
+
writeNodeError(res, err.status || 500, code, err.message, parentNodeId);
|
|
247
315
|
} finally {
|
|
248
316
|
finishJob(requestId, {
|
|
249
317
|
status: finishStatus,
|
package/routes/sessions.js
CHANGED
|
@@ -266,7 +266,15 @@ 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
|
-
|
|
269
|
+
if (code === "GRAPH_VERSION_CONFLICT") {
|
|
270
|
+
logEvent("session", "graph_conflict", {
|
|
271
|
+
sessionId: req.params.id,
|
|
272
|
+
code,
|
|
273
|
+
currentVersion: err.currentVersion,
|
|
274
|
+
});
|
|
275
|
+
} else {
|
|
276
|
+
logError("session", "graph_error", err, { sessionId: req.params.id, code });
|
|
277
|
+
}
|
|
270
278
|
res.status(err.status || 500).json(payload);
|
|
271
279
|
}
|
|
272
280
|
});
|
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
|