fraim-hub 2.0.318 → 2.0.319

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.
@@ -652,12 +652,12 @@ class AiHubConversationStore {
652
652
  if (fs_1.default.existsSync(path_1.default.join(dir, 'pending-head.json')) || fs_1.default.existsSync(path_1.default.join(dir, 'header-repair.json')))
653
653
  (0, conversation_store_lock_1.withBucketLock)(this.lockPath(dir), () => this.repairDerived(dir, key));
654
654
  const idx = this.loadIndex(dir, key);
655
- if (idx.headers.length && !idx.counts) {
655
+ // A legacy index (headers with no precomputed counts) or a rebuilding one both queue
656
+ // the same background upgrade, but neither blocks the read: counts are computed
657
+ // on the fly below from the headers already on hand, same as pre-#1682 behavior.
658
+ if ((idx.headers.length && !idx.counts) || idx.rebuilding) {
656
659
  (0, history_migration_1.queueHistoryMigration)({ stateFile: this.stateFilePath, source: this.indexPath(dir), mode: 'index', bucketKey: key });
657
- throw new history_policy_1.HistoryStoreError('history_migrating');
658
660
  }
659
- if (idx.rebuilding)
660
- (0, history_migration_1.queueHistoryMigration)({ stateFile: this.stateFilePath, source: this.indexPath(dir), mode: 'index', bucketKey: key });
661
661
  const counts = idx.counts ?? (0, conversation_ui_state_1.emptyConversationCounts)();
662
662
  if (!idx.counts)
663
663
  for (const header of idx.headers)
@@ -1130,19 +1130,21 @@ class AiHubConversationStore {
1130
1130
  return cached.index;
1131
1131
  }
1132
1132
  this.hotIndexCache.delete(bucketDir);
1133
- if (stat.size > history_policy_1.HISTORY_POLICY.indexBytes) {
1134
- if (this.options.skipLegacyMigration)
1135
- return null;
1133
+ // An oversized or over-long legacy index queues its own background upgrade to
1134
+ // bounded headers, but a read must never block on that migration: it still parses
1135
+ // and returns the index directly below, just without the hot in-memory cache
1136
+ // (cacheIndex already refuses to cache anything this large, so nothing leaks).
1137
+ const oversizedFile = stat.size > history_policy_1.HISTORY_POLICY.indexBytes;
1138
+ if (oversizedFile && this.options.skipLegacyMigration)
1139
+ return null;
1140
+ if (oversizedFile)
1136
1141
  (0, history_migration_1.queueHistoryMigration)({ stateFile: this.stateFilePath, source: file, mode: 'index' });
1137
- throw new history_policy_1.HistoryStoreError('history_migrating');
1138
- }
1139
1142
  const parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
1140
1143
  if (Array.isArray(parsed.headers)) {
1141
1144
  if (parsed.headers.length > history_policy_1.HISTORY_POLICY.hotHeaders) {
1142
1145
  if (this.options.skipLegacyMigration)
1143
1146
  return null;
1144
1147
  (0, history_migration_1.queueHistoryMigration)({ stateFile: this.stateFilePath, source: file, mode: 'index' });
1145
- throw new history_policy_1.HistoryStoreError('history_migrating');
1146
1148
  }
1147
1149
  return { ...parsed, activeId: parsed.activeId ?? null, headers: parsed.headers };
1148
1150
  }
@@ -32,8 +32,11 @@ function queueHistoryMigration(request) {
32
32
  if (running)
33
33
  return;
34
34
  const priorFailure = failures.get(request.source);
35
- if (priorFailure)
36
- throw new history_policy_1.HistoryStoreError(priorFailure);
35
+ if (priorFailure) {
36
+ if (Date.now() - priorFailure.at < history_policy_1.HISTORY_POLICY.migrationRetryAfterMs)
37
+ throw new history_policy_1.HistoryStoreError(priorFailure.code);
38
+ failures.delete(request.source);
39
+ }
37
40
  const worker = new worker_threads_1.Worker(`
38
41
  const { workerData, parentPort } = require('worker_threads');
39
42
  if (workerData.module.endsWith('.ts')) require('tsx/cjs');
@@ -50,12 +53,12 @@ function queueHistoryMigration(request) {
50
53
  }
51
54
  if (!message.ok && message.code !== 'history_migrating' && message.code !== 'history_upgrade_required') {
52
55
  workerCounters.failures++;
53
- failures.set(request.source, message.code);
56
+ failures.set(request.source, { code: message.code, at: Date.now() });
54
57
  if (failures.size > 16)
55
58
  failures.delete(failures.keys().next().value);
56
59
  }
57
60
  });
58
- worker.on('error', () => { workerCounters.failures++; failures.set(request.source, 'history_migration_failed'); });
61
+ worker.on('error', () => { workerCounters.failures++; failures.set(request.source, { code: 'history_migration_failed', at: Date.now() }); });
59
62
  worker.on('exit', () => { running = undefined; resolve(); });
60
63
  });
61
64
  running = { source: request.source, completion };
@@ -16,6 +16,14 @@ exports.HISTORY_POLICY = Object.freeze({
16
16
  maxPageSize: 100,
17
17
  responseBytes: 2 * 1024 * 1024,
18
18
  controlDocumentBytes: 16 * 1024 * 1024,
19
+ // A migration source that just failed (e.g. a transient Windows file lock) is retried
20
+ // after this window instead of being blacklisted for the life of the process.
21
+ migrationRetryAfterMs: 30 * 1000,
22
+ // A writer-compatibility registration this old is treated as stale regardless of
23
+ // whether its recorded pid currently matches a live process: Windows recycles pids
24
+ // quickly, and a crashed writer that never removed its own file can otherwise match
25
+ // an unrelated later process and block migration store-wide, forever.
26
+ staleRuntimeRegistrationMs: 24 * 60 * 60 * 1000,
19
27
  });
20
28
  class HistoryStoreError extends Error {
21
29
  constructor(code, statusCode = 503) {
@@ -41,7 +41,12 @@ function assertHistoryMigrationAllowed(stateFile, ownerPid) {
41
41
  }
42
42
  if (!Number.isSafeInteger(runtime.pid) || runtime.pid <= 0)
43
43
  throw new history_policy_1.HistoryStoreError('history_upgrade_required');
44
- if (runtime.pid === process.pid || !(0, process_liveness_1.isPidAlive)(runtime.pid))
44
+ // A pid match alone is not proof of liveness for an old registration: Windows
45
+ // recycles pids quickly, so a crashed writer's leftover file can otherwise match
46
+ // an unrelated process started long afterward.
47
+ const startedAtMs = typeof runtime.startedAt === 'string' ? Date.parse(runtime.startedAt) : NaN;
48
+ const stale = Number.isFinite(startedAtMs) && Date.now() - startedAtMs > history_policy_1.HISTORY_POLICY.staleRuntimeRegistrationMs;
49
+ if (runtime.pid === process.pid || stale || !(0, process_liveness_1.isPidAlive)(runtime.pid))
45
50
  continue;
46
51
  if (runtime.historyFormatVersion !== 3)
47
52
  throw new history_policy_1.HistoryStoreError('history_upgrade_required');
@@ -138,6 +138,16 @@ function runtimeFromEnv(env = process.env) {
138
138
  ? { executable: env.FRAIM_PACKAGED_CLI_EXECUTABLE, script: env.FRAIM_PACKAGED_CLI_SCRIPT }
139
139
  : null;
140
140
  }
141
+ // Regular Node.js (and ELECTRON_RUN_AS_NODE) cannot see inside Electron's
142
+ // .asar archives. For paths like …/app.asar/node_modules/…, verify the
143
+ // archive file itself rather than the internal entry.
144
+ function existsOrAsarExists(filePath) {
145
+ const normalized = filePath.replace(/\\/g, '/');
146
+ const asarSep = normalized.indexOf('.asar/');
147
+ if (asarSep === -1)
148
+ return fs_1.default.existsSync(filePath);
149
+ return fs_1.default.existsSync(filePath.slice(0, asarSep + '.asar'.length));
150
+ }
141
151
  function getPackagedFraimRuntime(env = process.env) {
142
152
  const envRuntime = runtimeFromEnv(env);
143
153
  if (envRuntime)
@@ -146,7 +156,7 @@ function getPackagedFraimRuntime(env = process.env) {
146
156
  const raw = JSON.parse(fs_1.default.readFileSync(getPackagedFraimRuntimePath(), 'utf8'));
147
157
  if (!raw.executable || !raw.script)
148
158
  return null;
149
- if (!fs_1.default.existsSync(raw.executable) || !fs_1.default.existsSync(raw.script))
159
+ if (!fs_1.default.existsSync(raw.executable) || !existsOrAsarExists(raw.script))
150
160
  return null;
151
161
  return { executable: raw.executable, script: raw.script };
152
162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.318",
3
+ "version": "2.0.319",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "author": "Sid Mathur <sid.mathur@gmail.com>",
6
6
  "homepage": "https://github.com/mathursrus/FRAIM#readme",
@@ -212,7 +212,7 @@
212
212
  "electron-updater": "^6.8.9",
213
213
  "express": "^5.2.1",
214
214
  "extract-zip": "^2.0.1",
215
- "fraim": "2.0.318",
215
+ "fraim": "2.0.319",
216
216
  "mongodb": "^7.0.0",
217
217
  "node-cron": "4.2.1",
218
218
  "node-edge-tts": "^1.2.10",