@memtensor/memos-local-openclaw-plugin 1.0.6-beta.1 → 1.0.6-beta.11

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.
Files changed (46) hide show
  1. package/dist/client/connector.d.ts.map +1 -1
  2. package/dist/client/connector.js +10 -4
  3. package/dist/client/connector.js.map +1 -1
  4. package/dist/hub/server.d.ts +2 -0
  5. package/dist/hub/server.d.ts.map +1 -1
  6. package/dist/hub/server.js +108 -54
  7. package/dist/hub/server.js.map +1 -1
  8. package/dist/ingest/providers/index.d.ts +4 -0
  9. package/dist/ingest/providers/index.d.ts.map +1 -1
  10. package/dist/ingest/providers/index.js +20 -4
  11. package/dist/ingest/providers/index.js.map +1 -1
  12. package/dist/ingest/providers/openai.d.ts +0 -3
  13. package/dist/ingest/providers/openai.d.ts.map +1 -1
  14. package/dist/ingest/providers/openai.js +9 -8
  15. package/dist/ingest/providers/openai.js.map +1 -1
  16. package/dist/recall/engine.d.ts.map +1 -1
  17. package/dist/recall/engine.js +35 -43
  18. package/dist/recall/engine.js.map +1 -1
  19. package/dist/storage/sqlite.d.ts +13 -0
  20. package/dist/storage/sqlite.d.ts.map +1 -1
  21. package/dist/storage/sqlite.js +43 -1
  22. package/dist/storage/sqlite.js.map +1 -1
  23. package/dist/update-check.d.ts.map +1 -1
  24. package/dist/update-check.js +2 -7
  25. package/dist/update-check.js.map +1 -1
  26. package/dist/viewer/html.d.ts.map +1 -1
  27. package/dist/viewer/html.js +115 -27
  28. package/dist/viewer/html.js.map +1 -1
  29. package/dist/viewer/server.d.ts +1 -0
  30. package/dist/viewer/server.d.ts.map +1 -1
  31. package/dist/viewer/server.js +191 -96
  32. package/dist/viewer/server.js.map +1 -1
  33. package/index.ts +208 -253
  34. package/openclaw.plugin.json +1 -1
  35. package/package.json +2 -1
  36. package/scripts/native-binding.cjs +32 -0
  37. package/scripts/postinstall.cjs +13 -16
  38. package/src/client/connector.ts +10 -4
  39. package/src/hub/server.ts +95 -51
  40. package/src/ingest/providers/index.ts +26 -18
  41. package/src/ingest/providers/openai.ts +5 -4
  42. package/src/recall/engine.ts +34 -41
  43. package/src/storage/sqlite.ts +43 -1
  44. package/src/update-check.ts +2 -7
  45. package/src/viewer/html.ts +115 -27
  46. package/src/viewer/server.ts +187 -64
@@ -90,23 +90,6 @@ export function applyMigrationItemToState(state: MigrationStateSnapshot, d: any)
90
90
  state.success = computeMigrationSuccess(state);
91
91
  }
92
92
 
93
- /** Epoch ms for Chunk; OpenClaw SQLite may store Unix seconds or ms. */
94
- function normalizeTimestamp(value: unknown): number {
95
- if (value == null) return Date.now();
96
- if (typeof value === "string") {
97
- const parsed = Date.parse(value.trim());
98
- if (Number.isFinite(parsed)) return parsed;
99
- const n = Number(value);
100
- if (Number.isFinite(n)) return normalizeTimestamp(n);
101
- return Date.now();
102
- }
103
- if (typeof value === "number" && Number.isFinite(value)) {
104
- if (value > 0 && value < 5_000_000_000) return Math.round(value * 1000);
105
- return Math.round(value);
106
- }
107
- return Date.now();
108
- }
109
-
110
93
  export interface ViewerServerOptions {
111
94
  store: SqliteStore;
112
95
  embedder: Embedder;
@@ -1932,18 +1915,25 @@ export class ViewerServer {
1932
1915
 
1933
1916
  private handleRetryJoin(req: http.IncomingMessage, res: http.ServerResponse): void {
1934
1917
  this.readBody(req, async (_body) => {
1935
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1918
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable", errorCode: "sharing_unavailable" });
1936
1919
  const sharing = this.ctx.config.sharing;
1937
1920
  if (!sharing?.enabled || sharing.role !== "client") {
1938
- return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
1921
+ return this.jsonResponse(res, { ok: false, error: "not_in_client_mode", errorCode: "not_in_client_mode" });
1939
1922
  }
1940
1923
  const hubAddress = sharing.client?.hubAddress ?? "";
1941
1924
  const teamToken = sharing.client?.teamToken ?? "";
1942
1925
  if (!hubAddress || !teamToken) {
1943
- return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
1926
+ return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token", errorCode: "missing_config" });
1944
1927
  }
1928
+ const hubUrl = normalizeHubUrl(hubAddress);
1929
+
1930
+ try {
1931
+ await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
1932
+ } catch {
1933
+ return this.jsonResponse(res, { ok: false, error: "hub_unreachable", errorCode: "hub_unreachable" });
1934
+ }
1935
+
1945
1936
  try {
1946
- const hubUrl = normalizeHubUrl(hubAddress);
1947
1937
  const os = await import("os");
1948
1938
  const nickname = sharing.client?.nickname;
1949
1939
  const username = nickname || os.userInfo().username || "user";
@@ -1971,9 +1961,19 @@ export class ViewerServer {
1971
1961
  lastKnownStatus: result.status || "",
1972
1962
  hubInstanceId,
1973
1963
  });
1964
+ if (result.status === "blocked") {
1965
+ return this.jsonResponse(res, { ok: false, error: "blocked", errorCode: "blocked" });
1966
+ }
1974
1967
  this.jsonResponse(res, { ok: true, status: result.status || "pending" });
1975
1968
  } catch (err) {
1976
- this.jsonResponse(res, { ok: false, error: String(err) });
1969
+ const errStr = String(err);
1970
+ if (errStr.includes("(409)") || errStr.includes("username_taken")) {
1971
+ return this.jsonResponse(res, { ok: false, error: "username_taken", errorCode: "username_taken" });
1972
+ }
1973
+ if (errStr.includes("(403)") || errStr.includes("invalid_team_token")) {
1974
+ return this.jsonResponse(res, { ok: false, error: "invalid_team_token", errorCode: "invalid_team_token" });
1975
+ }
1976
+ this.jsonResponse(res, { ok: false, error: errStr, errorCode: "unknown" });
1977
1977
  }
1978
1978
  });
1979
1979
  }
@@ -2886,20 +2886,25 @@ export class ViewerServer {
2886
2886
  const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
2887
2887
  const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
2888
2888
  let joinStatus: string | undefined;
2889
+ let joinError: string | undefined;
2889
2890
  if (nowClient && !previouslyClient) {
2890
2891
  try {
2891
2892
  joinStatus = await this.autoJoinOnSave(finalSharing);
2892
2893
  } catch (e) {
2893
- this.log.warn(`Auto-join on save failed: ${e}`);
2894
+ const msg = String(e instanceof Error ? e.message : e);
2895
+ this.log.warn(`Auto-join on save failed: ${msg}`);
2896
+ if (msg === "hub_unreachable" || msg === "username_taken" || msg === "invalid_team_token") {
2897
+ joinError = msg;
2898
+ }
2894
2899
  }
2895
2900
  }
2896
2901
 
2897
- this.jsonResponse(res, { ok: true, joinStatus, restart: true });
2902
+ if (joinError) {
2903
+ this.jsonResponse(res, { ok: true, joinError, restart: false });
2904
+ return;
2905
+ }
2898
2906
 
2899
- setTimeout(() => {
2900
- this.log.info("config-save: triggering gateway restart via SIGUSR1...");
2901
- try { process.kill(process.pid, "SIGUSR1"); } catch (sig) { this.log.warn(`SIGUSR1 failed: ${sig}`); }
2902
- }, 500);
2907
+ this.jsonResponseAndRestart(res, { ok: true, joinStatus, restart: true }, "config-save");
2903
2908
  } catch (e) {
2904
2909
  this.log.warn(`handleSaveConfig error: ${e}`);
2905
2910
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -2914,16 +2919,37 @@ export class ViewerServer {
2914
2919
  const teamToken = String(clientCfg?.teamToken || "");
2915
2920
  if (!hubAddress || !teamToken) return undefined;
2916
2921
  const hubUrl = normalizeHubUrl(hubAddress);
2922
+
2923
+ try {
2924
+ await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
2925
+ } catch {
2926
+ throw new Error("hub_unreachable");
2927
+ }
2928
+
2917
2929
  const os = await import("os");
2918
2930
  const nickname = String(clientCfg?.nickname || "");
2919
2931
  const username = nickname || os.userInfo().username || "user";
2920
2932
  const hostname = os.hostname() || "unknown";
2921
2933
  const persisted = this.store.getClientHubConnection();
2922
2934
  const existingIdentityKey = persisted?.identityKey || "";
2923
- const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
2924
- method: "POST",
2925
- body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
2926
- }) as any;
2935
+
2936
+ let result: any;
2937
+ try {
2938
+ result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
2939
+ method: "POST",
2940
+ body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
2941
+ });
2942
+ } catch (err) {
2943
+ const errStr = String(err);
2944
+ if (errStr.includes("(409)") || errStr.includes("username_taken")) {
2945
+ throw new Error("username_taken");
2946
+ }
2947
+ if (errStr.includes("(403)") || errStr.includes("invalid_team_token")) {
2948
+ throw new Error("invalid_team_token");
2949
+ }
2950
+ throw err;
2951
+ }
2952
+
2927
2953
  const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
2928
2954
  let hubInstanceId = persisted?.hubInstanceId || "";
2929
2955
  try {
@@ -2971,12 +2997,7 @@ export class ViewerServer {
2971
2997
  }
2972
2998
  }
2973
2999
 
2974
- this.jsonResponse(res, { ok: true, restart: true });
2975
-
2976
- setTimeout(() => {
2977
- this.log.info("handleLeaveTeam: triggering gateway restart via SIGUSR1...");
2978
- try { process.kill(process.pid, "SIGUSR1"); } catch (sig) { this.log.warn(`SIGUSR1 failed: ${sig}`); }
2979
- }, 500);
3000
+ this.jsonResponseAndRestart(res, { ok: true, restart: true }, "handleLeaveTeam");
2980
3001
  } catch (e) {
2981
3002
  this.log.warn(`handleLeaveTeam error: ${e}`);
2982
3003
  this.jsonResponse(res, { ok: false, error: String(e) });
@@ -3142,14 +3163,43 @@ export class ViewerServer {
3142
3163
  }
3143
3164
  }
3144
3165
  } catch {}
3145
- const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
3166
+ const baseUrl = hubUrl.replace(/\/+$/, "");
3167
+ const infoUrl = baseUrl + "/api/v1/hub/info";
3146
3168
  const ctrl = new AbortController();
3147
3169
  const timeout = setTimeout(() => ctrl.abort(), 8000);
3148
3170
  try {
3149
- const r = await fetch(url, { signal: ctrl.signal });
3171
+ const r = await fetch(infoUrl, { signal: ctrl.signal });
3150
3172
  clearTimeout(timeout);
3151
3173
  if (!r.ok) { this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` }); return; }
3152
3174
  const info = await r.json() as Record<string, unknown>;
3175
+
3176
+ const { teamToken, nickname } = JSON.parse(body);
3177
+ if (teamToken) {
3178
+ const username = (typeof nickname === "string" && nickname.trim()) || os.userInfo().username || "user";
3179
+ const persisted = this.store.getClientHubConnection();
3180
+ const identityKey = persisted?.identityKey || "";
3181
+ try {
3182
+ const joinR = await fetch(baseUrl + "/api/v1/hub/join", {
3183
+ method: "POST",
3184
+ headers: { "content-type": "application/json" },
3185
+ body: JSON.stringify({ teamToken, username, identityKey, deviceName: os.hostname(), dryRun: true }),
3186
+ });
3187
+ const joinData = await joinR.json() as Record<string, unknown>;
3188
+ if (!joinR.ok && joinData.error === "username_taken") {
3189
+ this.jsonResponse(res, { ok: false, error: "username_taken", teamName: info.teamName || "" });
3190
+ return;
3191
+ }
3192
+ if (!joinR.ok && joinData.error === "invalid_team_token") {
3193
+ this.jsonResponse(res, { ok: false, error: "invalid_team_token", teamName: info.teamName || "" });
3194
+ return;
3195
+ }
3196
+ if (joinR.ok && joinData.status === "blocked") {
3197
+ this.jsonResponse(res, { ok: false, error: "blocked", teamName: info.teamName || "" });
3198
+ return;
3199
+ }
3200
+ } catch { /* join check is best-effort; connection itself is OK */ }
3201
+ }
3202
+
3153
3203
  this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
3154
3204
  } catch (e: unknown) {
3155
3205
  clearTimeout(timeout);
@@ -3234,26 +3284,35 @@ export class ViewerServer {
3234
3284
  }
3235
3285
 
3236
3286
  private async handleUpdateCheck(res: http.ServerResponse): Promise<void> {
3287
+ const sendNoStore = (data: unknown, statusCode = 200) => {
3288
+ res.writeHead(statusCode, {
3289
+ "Content-Type": "application/json; charset=utf-8",
3290
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
3291
+ "Pragma": "no-cache",
3292
+ "Expires": "0",
3293
+ });
3294
+ res.end(JSON.stringify(data));
3295
+ };
3237
3296
  try {
3238
3297
  const pkgPath = this.findPluginPackageJson();
3239
3298
  if (!pkgPath) {
3240
- this.jsonResponse(res, { updateAvailable: false, error: "package.json not found" });
3299
+ sendNoStore({ updateAvailable: false, error: "package.json not found" });
3241
3300
  return;
3242
3301
  }
3243
3302
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
3244
3303
  const current = pkg.version as string;
3245
3304
  const name = pkg.name as string;
3246
3305
  if (!current || !name) {
3247
- this.jsonResponse(res, { updateAvailable: false, current });
3306
+ sendNoStore({ updateAvailable: false, current });
3248
3307
  return;
3249
3308
  }
3250
3309
  const { computeUpdateCheck } = await import("../update-check");
3251
3310
  const result = await computeUpdateCheck(name, current, fetch, 6_000);
3252
3311
  if (!result) {
3253
- this.jsonResponse(res, { updateAvailable: false, current, packageName: name });
3312
+ sendNoStore({ updateAvailable: false, current, packageName: name });
3254
3313
  return;
3255
3314
  }
3256
- this.jsonResponse(res, {
3315
+ sendNoStore({
3257
3316
  updateAvailable: result.updateAvailable,
3258
3317
  current: result.current,
3259
3318
  latest: result.latest,
@@ -3264,7 +3323,7 @@ export class ViewerServer {
3264
3323
  });
3265
3324
  } catch (e) {
3266
3325
  this.log.warn(`handleUpdateCheck error: ${e}`);
3267
- this.jsonResponse(res, { updateAvailable: false, error: String(e) });
3326
+ sendNoStore({ updateAvailable: false, error: String(e) });
3268
3327
  }
3269
3328
  }
3270
3329
 
@@ -3273,13 +3332,14 @@ export class ViewerServer {
3273
3332
  req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
3274
3333
  req.on("end", () => {
3275
3334
  try {
3276
- const { packageSpec: rawSpec } = JSON.parse(body);
3335
+ const { packageSpec: rawSpec, targetVersion: rawTargetVersion } = JSON.parse(body);
3277
3336
  if (!rawSpec || typeof rawSpec !== "string") {
3278
3337
  res.writeHead(400, { "Content-Type": "application/json" });
3279
3338
  res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
3280
3339
  return;
3281
3340
  }
3282
3341
  const packageSpec = rawSpec.trim().replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/i, "");
3342
+ const targetVersion = typeof rawTargetVersion === "string" ? rawTargetVersion.trim() : "";
3283
3343
  const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
3284
3344
  this.log.info(`update-install: received packageSpec="${packageSpec}" (len=${packageSpec.length})`);
3285
3345
  if (!allowed.test(packageSpec)) {
@@ -3296,16 +3356,42 @@ export class ViewerServer {
3296
3356
  const shortName = pluginName?.replace(/^@[\w-]+\//, "") ?? "memos-local-openclaw-plugin";
3297
3357
  const extDir = path.join(os.homedir(), ".openclaw", "extensions", shortName);
3298
3358
  const tmpDir = path.join(os.tmpdir(), `openclaw-update-${Date.now()}`);
3359
+ const backupDir = path.join(path.dirname(extDir), `${shortName}.backup-${Date.now()}`);
3360
+ let backupReady = false;
3361
+
3362
+ const cleanupTmpDir = () => {
3363
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
3364
+ };
3365
+ const rollbackInstall = () => {
3366
+ try { fs.rmSync(extDir, { recursive: true, force: true }); } catch {}
3367
+ if (!backupReady) return;
3368
+ try {
3369
+ fs.renameSync(backupDir, extDir);
3370
+ backupReady = false;
3371
+ this.log.info(`update-install: restored previous version from ${backupDir}`);
3372
+ } catch (restoreErr: any) {
3373
+ this.log.warn(`update-install: failed to restore previous version: ${restoreErr?.message ?? restoreErr}`);
3374
+ }
3375
+ };
3376
+ const discardBackup = () => {
3377
+ if (!backupReady) return;
3378
+ try {
3379
+ fs.rmSync(backupDir, { recursive: true, force: true });
3380
+ backupReady = false;
3381
+ } catch (cleanupErr: any) {
3382
+ this.log.warn(`update-install: failed to remove backup dir ${backupDir}: ${cleanupErr?.message ?? cleanupErr}`);
3383
+ }
3384
+ };
3299
3385
 
3300
3386
  // Download via npm pack, extract, and replace extension dir.
3301
3387
  // Does NOT touch openclaw.json → no config watcher SIGUSR1.
3302
3388
  this.log.info(`update-install: downloading ${packageSpec} via npm pack...`);
3303
3389
  fs.mkdirSync(tmpDir, { recursive: true });
3304
- exec(`npm pack ${packageSpec} --pack-destination ${tmpDir}`, { timeout: 60_000 }, (packErr, packOut) => {
3390
+ exec(`npm pack ${packageSpec} --pack-destination ${tmpDir} --prefer-online`, { timeout: 60_000 }, (packErr, packOut) => {
3305
3391
  if (packErr) {
3306
3392
  this.log.warn(`update-install: npm pack failed: ${packErr.message}`);
3307
3393
  this.jsonResponse(res, { ok: false, error: `Download failed: ${packErr.message}` });
3308
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
3394
+ cleanupTmpDir();
3309
3395
  return;
3310
3396
  }
3311
3397
  const tgzFile = packOut.trim().split("\n").pop()!;
@@ -3318,7 +3404,7 @@ export class ViewerServer {
3318
3404
  if (tarErr) {
3319
3405
  this.log.warn(`update-install: tar extract failed: ${tarErr.message}`);
3320
3406
  this.jsonResponse(res, { ok: false, error: `Extract failed: ${tarErr.message}` });
3321
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
3407
+ cleanupTmpDir();
3322
3408
  return;
3323
3409
  }
3324
3410
 
@@ -3326,23 +3412,36 @@ export class ViewerServer {
3326
3412
  const srcDir = path.join(extractDir, "package");
3327
3413
  if (!fs.existsSync(srcDir)) {
3328
3414
  this.jsonResponse(res, { ok: false, error: "Extracted package has no 'package' dir" });
3329
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
3415
+ cleanupTmpDir();
3330
3416
  return;
3331
3417
  }
3332
3418
 
3333
3419
  // Replace extension directory
3334
3420
  this.log.info(`update-install: replacing ${extDir}...`);
3335
- try { fs.rmSync(extDir, { recursive: true, force: true }); } catch {}
3336
- fs.mkdirSync(path.dirname(extDir), { recursive: true });
3337
- fs.renameSync(srcDir, extDir);
3421
+ try {
3422
+ fs.mkdirSync(path.dirname(extDir), { recursive: true });
3423
+ try { fs.rmSync(backupDir, { recursive: true, force: true }); } catch {}
3424
+ if (fs.existsSync(extDir)) {
3425
+ fs.renameSync(extDir, backupDir);
3426
+ backupReady = true;
3427
+ }
3428
+ fs.renameSync(srcDir, extDir);
3429
+ } catch (replaceErr: any) {
3430
+ this.log.warn(`update-install: replace failed: ${replaceErr?.message ?? replaceErr}`);
3431
+ cleanupTmpDir();
3432
+ rollbackInstall();
3433
+ this.jsonResponse(res, { ok: false, error: `Replace failed: ${replaceErr?.message ?? replaceErr}` });
3434
+ return;
3435
+ }
3338
3436
 
3339
3437
  // Install dependencies
3340
3438
  this.log.info(`update-install: installing dependencies...`);
3341
3439
  const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
3342
3440
  execFile(npmCmd, ["install", "--omit=dev", "--ignore-scripts"], { cwd: extDir, timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
3343
3441
  if (npmErr) {
3344
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
3345
3442
  this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
3443
+ cleanupTmpDir();
3444
+ rollbackInstall();
3346
3445
  this.jsonResponse(res, { ok: false, error: `Dependency install failed: ${npmStderr || npmErr.message}` });
3347
3446
  return;
3348
3447
  }
@@ -3356,28 +3455,36 @@ export class ViewerServer {
3356
3455
 
3357
3456
  this.log.info(`update-install: running postinstall...`);
3358
3457
  execFile(process.execPath, ["scripts/postinstall.cjs"], { cwd: extDir, timeout: 180_000 }, (postErr, postOut, postStderr) => {
3359
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
3458
+ cleanupTmpDir();
3360
3459
 
3361
3460
  if (postErr) {
3362
3461
  this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
3363
3462
  const postStderrStr = String(postStderr || "").trim();
3364
3463
  if (postStderrStr) this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
3464
+ rollbackInstall();
3465
+ this.jsonResponse(res, { ok: false, error: `Postinstall failed: ${postStderrStr || postErr.message}` });
3466
+ return;
3365
3467
  }
3366
3468
 
3367
- // Read new version
3368
3469
  let newVersion = "unknown";
3369
3470
  try {
3370
3471
  const newPkg = JSON.parse(fs.readFileSync(path.join(extDir, "package.json"), "utf-8"));
3371
3472
  newVersion = newPkg.version ?? newVersion;
3372
3473
  } catch {}
3373
3474
 
3374
- this.log.info(`update-install: success! Updated to ${newVersion}`);
3375
- this.jsonResponse(res, { ok: true, version: newVersion });
3475
+ if (targetVersion && newVersion !== targetVersion) {
3476
+ this.log.warn(`update-install: version mismatch! expected=${targetVersion}, got=${newVersion} — rolling back`);
3477
+ rollbackInstall();
3478
+ this.jsonResponse(res, {
3479
+ ok: false,
3480
+ error: `Version mismatch: expected ${targetVersion} but downloaded ${newVersion}. npm cache may be stale — please try again.`,
3481
+ });
3482
+ return;
3483
+ }
3376
3484
 
3377
- setTimeout(() => {
3378
- this.log.info(`update-install: triggering gateway restart via SIGUSR1...`);
3379
- try { process.kill(process.pid, "SIGUSR1"); } catch (sig) { this.log.warn(`SIGUSR1 failed: ${sig}`); }
3380
- }, 500);
3485
+ discardBackup();
3486
+ this.log.info(`update-install: success! Updated to ${newVersion}`);
3487
+ this.jsonResponseAndRestart(res, { ok: true, version: newVersion }, "update-install");
3381
3488
  });
3382
3489
  });
3383
3490
  });
@@ -3962,8 +4069,8 @@ export class ViewerServer {
3962
4069
  mergeCount: 0,
3963
4070
  lastHitAt: null,
3964
4071
  mergeHistory: "[]",
3965
- createdAt: normalizeTimestamp(row.updated_at),
3966
- updatedAt: normalizeTimestamp(row.updated_at),
4072
+ createdAt: Number(row.updated_at) < 1e12 ? Number(row.updated_at) * 1000 : Number(row.updated_at),
4073
+ updatedAt: Number(row.updated_at) < 1e12 ? Number(row.updated_at) * 1000 : Number(row.updated_at),
3967
4074
  };
3968
4075
 
3969
4076
  this.store.insertChunk(chunk);
@@ -4510,6 +4617,22 @@ export class ViewerServer {
4510
4617
  req.on("end", () => cb(body));
4511
4618
  }
4512
4619
 
4620
+ private jsonResponseAndRestart(
4621
+ res: http.ServerResponse,
4622
+ data: unknown,
4623
+ source: string,
4624
+ delayMs = 1500,
4625
+ statusCode = 200,
4626
+ ): void {
4627
+ res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
4628
+ res.end(JSON.stringify(data), () => {
4629
+ setTimeout(() => {
4630
+ this.log.info(`${source}: triggering gateway restart via SIGUSR1...`);
4631
+ try { process.kill(process.pid, "SIGUSR1"); } catch (sig) { this.log.warn(`SIGUSR1 failed: ${sig}`); }
4632
+ }, delayMs);
4633
+ });
4634
+ }
4635
+
4513
4636
  private jsonResponse(res: http.ServerResponse, data: unknown, statusCode = 200): void {
4514
4637
  res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
4515
4638
  res.end(JSON.stringify(data));