@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
@@ -108,26 +108,6 @@ function applyMigrationItemToState(state, d) {
108
108
  state.lastItem = d;
109
109
  state.success = computeMigrationSuccess(state);
110
110
  }
111
- /** Epoch ms for Chunk; OpenClaw SQLite may store Unix seconds or ms. */
112
- function normalizeTimestamp(value) {
113
- if (value == null)
114
- return Date.now();
115
- if (typeof value === "string") {
116
- const parsed = Date.parse(value.trim());
117
- if (Number.isFinite(parsed))
118
- return parsed;
119
- const n = Number(value);
120
- if (Number.isFinite(n))
121
- return normalizeTimestamp(n);
122
- return Date.now();
123
- }
124
- if (typeof value === "number" && Number.isFinite(value)) {
125
- if (value > 0 && value < 5_000_000_000)
126
- return Math.round(value * 1000);
127
- return Math.round(value);
128
- }
129
- return Date.now();
130
- }
131
111
  class ViewerServer {
132
112
  server = null;
133
113
  store;
@@ -2090,18 +2070,24 @@ class ViewerServer {
2090
2070
  handleRetryJoin(req, res) {
2091
2071
  this.readBody(req, async (_body) => {
2092
2072
  if (!this.ctx)
2093
- return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
2073
+ return this.jsonResponse(res, { ok: false, error: "sharing_unavailable", errorCode: "sharing_unavailable" });
2094
2074
  const sharing = this.ctx.config.sharing;
2095
2075
  if (!sharing?.enabled || sharing.role !== "client") {
2096
- return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
2076
+ return this.jsonResponse(res, { ok: false, error: "not_in_client_mode", errorCode: "not_in_client_mode" });
2097
2077
  }
2098
2078
  const hubAddress = sharing.client?.hubAddress ?? "";
2099
2079
  const teamToken = sharing.client?.teamToken ?? "";
2100
2080
  if (!hubAddress || !teamToken) {
2101
- return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
2081
+ return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token", errorCode: "missing_config" });
2082
+ }
2083
+ const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
2084
+ try {
2085
+ await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
2086
+ }
2087
+ catch {
2088
+ return this.jsonResponse(res, { ok: false, error: "hub_unreachable", errorCode: "hub_unreachable" });
2102
2089
  }
2103
2090
  try {
2104
- const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
2105
2091
  const os = await Promise.resolve().then(() => __importStar(require("os")));
2106
2092
  const nickname = sharing.client?.nickname;
2107
2093
  const username = nickname || os.userInfo().username || "user";
@@ -2130,10 +2116,20 @@ class ViewerServer {
2130
2116
  lastKnownStatus: result.status || "",
2131
2117
  hubInstanceId,
2132
2118
  });
2119
+ if (result.status === "blocked") {
2120
+ return this.jsonResponse(res, { ok: false, error: "blocked", errorCode: "blocked" });
2121
+ }
2133
2122
  this.jsonResponse(res, { ok: true, status: result.status || "pending" });
2134
2123
  }
2135
2124
  catch (err) {
2136
- this.jsonResponse(res, { ok: false, error: String(err) });
2125
+ const errStr = String(err);
2126
+ if (errStr.includes("(409)") || errStr.includes("username_taken")) {
2127
+ return this.jsonResponse(res, { ok: false, error: "username_taken", errorCode: "username_taken" });
2128
+ }
2129
+ if (errStr.includes("(403)") || errStr.includes("invalid_team_token")) {
2130
+ return this.jsonResponse(res, { ok: false, error: "invalid_team_token", errorCode: "invalid_team_token" });
2131
+ }
2132
+ this.jsonResponse(res, { ok: false, error: errStr, errorCode: "unknown" });
2137
2133
  }
2138
2134
  });
2139
2135
  }
@@ -3118,24 +3114,24 @@ class ViewerServer {
3118
3114
  const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
3119
3115
  const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
3120
3116
  let joinStatus;
3117
+ let joinError;
3121
3118
  if (nowClient && !previouslyClient) {
3122
3119
  try {
3123
3120
  joinStatus = await this.autoJoinOnSave(finalSharing);
3124
3121
  }
3125
3122
  catch (e) {
3126
- this.log.warn(`Auto-join on save failed: ${e}`);
3123
+ const msg = String(e instanceof Error ? e.message : e);
3124
+ this.log.warn(`Auto-join on save failed: ${msg}`);
3125
+ if (msg === "hub_unreachable" || msg === "username_taken" || msg === "invalid_team_token") {
3126
+ joinError = msg;
3127
+ }
3127
3128
  }
3128
3129
  }
3129
- this.jsonResponse(res, { ok: true, joinStatus, restart: true });
3130
- setTimeout(() => {
3131
- this.log.info("config-save: triggering gateway restart via SIGUSR1...");
3132
- try {
3133
- process.kill(process.pid, "SIGUSR1");
3134
- }
3135
- catch (sig) {
3136
- this.log.warn(`SIGUSR1 failed: ${sig}`);
3137
- }
3138
- }, 500);
3130
+ if (joinError) {
3131
+ this.jsonResponse(res, { ok: true, joinError, restart: false });
3132
+ return;
3133
+ }
3134
+ this.jsonResponseAndRestart(res, { ok: true, joinStatus, restart: true }, "config-save");
3139
3135
  }
3140
3136
  catch (e) {
3141
3137
  this.log.warn(`handleSaveConfig error: ${e}`);
@@ -3151,16 +3147,35 @@ class ViewerServer {
3151
3147
  if (!hubAddress || !teamToken)
3152
3148
  return undefined;
3153
3149
  const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
3150
+ try {
3151
+ await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/info", { method: "GET" });
3152
+ }
3153
+ catch {
3154
+ throw new Error("hub_unreachable");
3155
+ }
3154
3156
  const os = await Promise.resolve().then(() => __importStar(require("os")));
3155
3157
  const nickname = String(clientCfg?.nickname || "");
3156
3158
  const username = nickname || os.userInfo().username || "user";
3157
3159
  const hostname = os.hostname() || "unknown";
3158
3160
  const persisted = this.store.getClientHubConnection();
3159
3161
  const existingIdentityKey = persisted?.identityKey || "";
3160
- const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
3161
- method: "POST",
3162
- body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
3163
- });
3162
+ let result;
3163
+ try {
3164
+ result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
3165
+ method: "POST",
3166
+ body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
3167
+ });
3168
+ }
3169
+ catch (err) {
3170
+ const errStr = String(err);
3171
+ if (errStr.includes("(409)") || errStr.includes("username_taken")) {
3172
+ throw new Error("username_taken");
3173
+ }
3174
+ if (errStr.includes("(403)") || errStr.includes("invalid_team_token")) {
3175
+ throw new Error("invalid_team_token");
3176
+ }
3177
+ throw err;
3178
+ }
3164
3179
  const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
3165
3180
  let hubInstanceId = persisted?.hubInstanceId || "";
3166
3181
  try {
@@ -3206,16 +3221,7 @@ class ViewerServer {
3206
3221
  this.log.info("handleLeaveTeam: config updated, sharing disabled");
3207
3222
  }
3208
3223
  }
3209
- this.jsonResponse(res, { ok: true, restart: true });
3210
- setTimeout(() => {
3211
- this.log.info("handleLeaveTeam: triggering gateway restart via SIGUSR1...");
3212
- try {
3213
- process.kill(process.pid, "SIGUSR1");
3214
- }
3215
- catch (sig) {
3216
- this.log.warn(`SIGUSR1 failed: ${sig}`);
3217
- }
3218
- }, 500);
3224
+ this.jsonResponseAndRestart(res, { ok: true, restart: true }, "handleLeaveTeam");
3219
3225
  }
3220
3226
  catch (e) {
3221
3227
  this.log.warn(`handleLeaveTeam error: ${e}`);
@@ -3389,17 +3395,45 @@ class ViewerServer {
3389
3395
  }
3390
3396
  }
3391
3397
  catch { }
3392
- const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
3398
+ const baseUrl = hubUrl.replace(/\/+$/, "");
3399
+ const infoUrl = baseUrl + "/api/v1/hub/info";
3393
3400
  const ctrl = new AbortController();
3394
3401
  const timeout = setTimeout(() => ctrl.abort(), 8000);
3395
3402
  try {
3396
- const r = await fetch(url, { signal: ctrl.signal });
3403
+ const r = await fetch(infoUrl, { signal: ctrl.signal });
3397
3404
  clearTimeout(timeout);
3398
3405
  if (!r.ok) {
3399
3406
  this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` });
3400
3407
  return;
3401
3408
  }
3402
3409
  const info = await r.json();
3410
+ const { teamToken, nickname } = JSON.parse(body);
3411
+ if (teamToken) {
3412
+ const username = (typeof nickname === "string" && nickname.trim()) || node_os_1.default.userInfo().username || "user";
3413
+ const persisted = this.store.getClientHubConnection();
3414
+ const identityKey = persisted?.identityKey || "";
3415
+ try {
3416
+ const joinR = await fetch(baseUrl + "/api/v1/hub/join", {
3417
+ method: "POST",
3418
+ headers: { "content-type": "application/json" },
3419
+ body: JSON.stringify({ teamToken, username, identityKey, deviceName: node_os_1.default.hostname(), dryRun: true }),
3420
+ });
3421
+ const joinData = await joinR.json();
3422
+ if (!joinR.ok && joinData.error === "username_taken") {
3423
+ this.jsonResponse(res, { ok: false, error: "username_taken", teamName: info.teamName || "" });
3424
+ return;
3425
+ }
3426
+ if (!joinR.ok && joinData.error === "invalid_team_token") {
3427
+ this.jsonResponse(res, { ok: false, error: "invalid_team_token", teamName: info.teamName || "" });
3428
+ return;
3429
+ }
3430
+ if (joinR.ok && joinData.status === "blocked") {
3431
+ this.jsonResponse(res, { ok: false, error: "blocked", teamName: info.teamName || "" });
3432
+ return;
3433
+ }
3434
+ }
3435
+ catch { /* join check is best-effort; connection itself is OK */ }
3436
+ }
3403
3437
  this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
3404
3438
  }
3405
3439
  catch (e) {
@@ -3486,26 +3520,35 @@ class ViewerServer {
3486
3520
  return null;
3487
3521
  }
3488
3522
  async handleUpdateCheck(res) {
3523
+ const sendNoStore = (data, statusCode = 200) => {
3524
+ res.writeHead(statusCode, {
3525
+ "Content-Type": "application/json; charset=utf-8",
3526
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
3527
+ "Pragma": "no-cache",
3528
+ "Expires": "0",
3529
+ });
3530
+ res.end(JSON.stringify(data));
3531
+ };
3489
3532
  try {
3490
3533
  const pkgPath = this.findPluginPackageJson();
3491
3534
  if (!pkgPath) {
3492
- this.jsonResponse(res, { updateAvailable: false, error: "package.json not found" });
3535
+ sendNoStore({ updateAvailable: false, error: "package.json not found" });
3493
3536
  return;
3494
3537
  }
3495
3538
  const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf-8"));
3496
3539
  const current = pkg.version;
3497
3540
  const name = pkg.name;
3498
3541
  if (!current || !name) {
3499
- this.jsonResponse(res, { updateAvailable: false, current });
3542
+ sendNoStore({ updateAvailable: false, current });
3500
3543
  return;
3501
3544
  }
3502
3545
  const { computeUpdateCheck } = await Promise.resolve().then(() => __importStar(require("../update-check")));
3503
3546
  const result = await computeUpdateCheck(name, current, fetch, 6_000);
3504
3547
  if (!result) {
3505
- this.jsonResponse(res, { updateAvailable: false, current, packageName: name });
3548
+ sendNoStore({ updateAvailable: false, current, packageName: name });
3506
3549
  return;
3507
3550
  }
3508
- this.jsonResponse(res, {
3551
+ sendNoStore({
3509
3552
  updateAvailable: result.updateAvailable,
3510
3553
  current: result.current,
3511
3554
  latest: result.latest,
@@ -3517,7 +3560,7 @@ class ViewerServer {
3517
3560
  }
3518
3561
  catch (e) {
3519
3562
  this.log.warn(`handleUpdateCheck error: ${e}`);
3520
- this.jsonResponse(res, { updateAvailable: false, error: String(e) });
3563
+ sendNoStore({ updateAvailable: false, error: String(e) });
3521
3564
  }
3522
3565
  }
3523
3566
  handleUpdateInstall(req, res) {
@@ -3525,13 +3568,14 @@ class ViewerServer {
3525
3568
  req.on("data", (chunk) => { body += chunk.toString(); });
3526
3569
  req.on("end", () => {
3527
3570
  try {
3528
- const { packageSpec: rawSpec } = JSON.parse(body);
3571
+ const { packageSpec: rawSpec, targetVersion: rawTargetVersion } = JSON.parse(body);
3529
3572
  if (!rawSpec || typeof rawSpec !== "string") {
3530
3573
  res.writeHead(400, { "Content-Type": "application/json" });
3531
3574
  res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
3532
3575
  return;
3533
3576
  }
3534
3577
  const packageSpec = rawSpec.trim().replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/i, "");
3578
+ const targetVersion = typeof rawTargetVersion === "string" ? rawTargetVersion.trim() : "";
3535
3579
  const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
3536
3580
  this.log.info(`update-install: received packageSpec="${packageSpec}" (len=${packageSpec.length})`);
3537
3581
  if (!allowed.test(packageSpec)) {
@@ -3552,18 +3596,50 @@ class ViewerServer {
3552
3596
  const shortName = pluginName?.replace(/^@[\w-]+\//, "") ?? "memos-local-openclaw-plugin";
3553
3597
  const extDir = node_path_1.default.join(node_os_1.default.homedir(), ".openclaw", "extensions", shortName);
3554
3598
  const tmpDir = node_path_1.default.join(node_os_1.default.tmpdir(), `openclaw-update-${Date.now()}`);
3599
+ const backupDir = node_path_1.default.join(node_path_1.default.dirname(extDir), `${shortName}.backup-${Date.now()}`);
3600
+ let backupReady = false;
3601
+ const cleanupTmpDir = () => {
3602
+ try {
3603
+ node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3604
+ }
3605
+ catch { }
3606
+ };
3607
+ const rollbackInstall = () => {
3608
+ try {
3609
+ node_fs_1.default.rmSync(extDir, { recursive: true, force: true });
3610
+ }
3611
+ catch { }
3612
+ if (!backupReady)
3613
+ return;
3614
+ try {
3615
+ node_fs_1.default.renameSync(backupDir, extDir);
3616
+ backupReady = false;
3617
+ this.log.info(`update-install: restored previous version from ${backupDir}`);
3618
+ }
3619
+ catch (restoreErr) {
3620
+ this.log.warn(`update-install: failed to restore previous version: ${restoreErr?.message ?? restoreErr}`);
3621
+ }
3622
+ };
3623
+ const discardBackup = () => {
3624
+ if (!backupReady)
3625
+ return;
3626
+ try {
3627
+ node_fs_1.default.rmSync(backupDir, { recursive: true, force: true });
3628
+ backupReady = false;
3629
+ }
3630
+ catch (cleanupErr) {
3631
+ this.log.warn(`update-install: failed to remove backup dir ${backupDir}: ${cleanupErr?.message ?? cleanupErr}`);
3632
+ }
3633
+ };
3555
3634
  // Download via npm pack, extract, and replace extension dir.
3556
3635
  // Does NOT touch openclaw.json → no config watcher SIGUSR1.
3557
3636
  this.log.info(`update-install: downloading ${packageSpec} via npm pack...`);
3558
3637
  node_fs_1.default.mkdirSync(tmpDir, { recursive: true });
3559
- (0, node_child_process_1.exec)(`npm pack ${packageSpec} --pack-destination ${tmpDir}`, { timeout: 60_000 }, (packErr, packOut) => {
3638
+ (0, node_child_process_1.exec)(`npm pack ${packageSpec} --pack-destination ${tmpDir} --prefer-online`, { timeout: 60_000 }, (packErr, packOut) => {
3560
3639
  if (packErr) {
3561
3640
  this.log.warn(`update-install: npm pack failed: ${packErr.message}`);
3562
3641
  this.jsonResponse(res, { ok: false, error: `Download failed: ${packErr.message}` });
3563
- try {
3564
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3565
- }
3566
- catch { }
3642
+ cleanupTmpDir();
3567
3643
  return;
3568
3644
  }
3569
3645
  const tgzFile = packOut.trim().split("\n").pop();
@@ -3575,40 +3651,45 @@ class ViewerServer {
3575
3651
  if (tarErr) {
3576
3652
  this.log.warn(`update-install: tar extract failed: ${tarErr.message}`);
3577
3653
  this.jsonResponse(res, { ok: false, error: `Extract failed: ${tarErr.message}` });
3578
- try {
3579
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3580
- }
3581
- catch { }
3654
+ cleanupTmpDir();
3582
3655
  return;
3583
3656
  }
3584
3657
  // npm pack extracts to a "package" subdirectory
3585
3658
  const srcDir = node_path_1.default.join(extractDir, "package");
3586
3659
  if (!node_fs_1.default.existsSync(srcDir)) {
3587
3660
  this.jsonResponse(res, { ok: false, error: "Extracted package has no 'package' dir" });
3588
- try {
3589
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3590
- }
3591
- catch { }
3661
+ cleanupTmpDir();
3592
3662
  return;
3593
3663
  }
3594
3664
  // Replace extension directory
3595
3665
  this.log.info(`update-install: replacing ${extDir}...`);
3596
3666
  try {
3597
- node_fs_1.default.rmSync(extDir, { recursive: true, force: true });
3667
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(extDir), { recursive: true });
3668
+ try {
3669
+ node_fs_1.default.rmSync(backupDir, { recursive: true, force: true });
3670
+ }
3671
+ catch { }
3672
+ if (node_fs_1.default.existsSync(extDir)) {
3673
+ node_fs_1.default.renameSync(extDir, backupDir);
3674
+ backupReady = true;
3675
+ }
3676
+ node_fs_1.default.renameSync(srcDir, extDir);
3677
+ }
3678
+ catch (replaceErr) {
3679
+ this.log.warn(`update-install: replace failed: ${replaceErr?.message ?? replaceErr}`);
3680
+ cleanupTmpDir();
3681
+ rollbackInstall();
3682
+ this.jsonResponse(res, { ok: false, error: `Replace failed: ${replaceErr?.message ?? replaceErr}` });
3683
+ return;
3598
3684
  }
3599
- catch { }
3600
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(extDir), { recursive: true });
3601
- node_fs_1.default.renameSync(srcDir, extDir);
3602
3685
  // Install dependencies
3603
3686
  this.log.info(`update-install: installing dependencies...`);
3604
3687
  const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
3605
3688
  (0, node_child_process_1.execFile)(npmCmd, ["install", "--omit=dev", "--ignore-scripts"], { cwd: extDir, timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
3606
3689
  if (npmErr) {
3607
- try {
3608
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3609
- }
3610
- catch { }
3611
3690
  this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
3691
+ cleanupTmpDir();
3692
+ rollbackInstall();
3612
3693
  this.jsonResponse(res, { ok: false, error: `Dependency install failed: ${npmStderr || npmErr.message}` });
3613
3694
  return;
3614
3695
  }
@@ -3621,34 +3702,34 @@ class ViewerServer {
3621
3702
  }
3622
3703
  this.log.info(`update-install: running postinstall...`);
3623
3704
  (0, node_child_process_1.execFile)(process.execPath, ["scripts/postinstall.cjs"], { cwd: extDir, timeout: 180_000 }, (postErr, postOut, postStderr) => {
3624
- try {
3625
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
3626
- }
3627
- catch { }
3705
+ cleanupTmpDir();
3628
3706
  if (postErr) {
3629
3707
  this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
3630
3708
  const postStderrStr = String(postStderr || "").trim();
3631
3709
  if (postStderrStr)
3632
3710
  this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
3711
+ rollbackInstall();
3712
+ this.jsonResponse(res, { ok: false, error: `Postinstall failed: ${postStderrStr || postErr.message}` });
3713
+ return;
3633
3714
  }
3634
- // Read new version
3635
3715
  let newVersion = "unknown";
3636
3716
  try {
3637
3717
  const newPkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(extDir, "package.json"), "utf-8"));
3638
3718
  newVersion = newPkg.version ?? newVersion;
3639
3719
  }
3640
3720
  catch { }
3721
+ if (targetVersion && newVersion !== targetVersion) {
3722
+ this.log.warn(`update-install: version mismatch! expected=${targetVersion}, got=${newVersion} — rolling back`);
3723
+ rollbackInstall();
3724
+ this.jsonResponse(res, {
3725
+ ok: false,
3726
+ error: `Version mismatch: expected ${targetVersion} but downloaded ${newVersion}. npm cache may be stale — please try again.`,
3727
+ });
3728
+ return;
3729
+ }
3730
+ discardBackup();
3641
3731
  this.log.info(`update-install: success! Updated to ${newVersion}`);
3642
- this.jsonResponse(res, { ok: true, version: newVersion });
3643
- setTimeout(() => {
3644
- this.log.info(`update-install: triggering gateway restart via SIGUSR1...`);
3645
- try {
3646
- process.kill(process.pid, "SIGUSR1");
3647
- }
3648
- catch (sig) {
3649
- this.log.warn(`SIGUSR1 failed: ${sig}`);
3650
- }
3651
- }, 500);
3732
+ this.jsonResponseAndRestart(res, { ok: true, version: newVersion }, "update-install");
3652
3733
  });
3653
3734
  });
3654
3735
  });
@@ -4223,8 +4304,8 @@ class ViewerServer {
4223
4304
  mergeCount: 0,
4224
4305
  lastHitAt: null,
4225
4306
  mergeHistory: "[]",
4226
- createdAt: normalizeTimestamp(row.updated_at),
4227
- updatedAt: normalizeTimestamp(row.updated_at),
4307
+ createdAt: Number(row.updated_at) < 1e12 ? Number(row.updated_at) * 1000 : Number(row.updated_at),
4308
+ updatedAt: Number(row.updated_at) < 1e12 ? Number(row.updated_at) * 1000 : Number(row.updated_at),
4228
4309
  };
4229
4310
  this.store.insertChunk(chunk);
4230
4311
  if (embedding && dedupStatus === "active") {
@@ -4781,6 +4862,20 @@ class ViewerServer {
4781
4862
  req.on("data", (chunk) => { body += chunk.toString(); });
4782
4863
  req.on("end", () => cb(body));
4783
4864
  }
4865
+ jsonResponseAndRestart(res, data, source, delayMs = 1500, statusCode = 200) {
4866
+ res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
4867
+ res.end(JSON.stringify(data), () => {
4868
+ setTimeout(() => {
4869
+ this.log.info(`${source}: triggering gateway restart via SIGUSR1...`);
4870
+ try {
4871
+ process.kill(process.pid, "SIGUSR1");
4872
+ }
4873
+ catch (sig) {
4874
+ this.log.warn(`SIGUSR1 failed: ${sig}`);
4875
+ }
4876
+ }, delayMs);
4877
+ });
4878
+ }
4784
4879
  jsonResponse(res, data, statusCode = 200) {
4785
4880
  res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
4786
4881
  res.end(JSON.stringify(data));