@threadbase-sh/streamer 1.39.0 → 1.39.1
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/dist/cli.cjs +93 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +93 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +93 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -8867,6 +8867,20 @@ var StreamerServer = class {
|
|
|
8867
8867
|
// Set by onConversationChanged while a scan is in-flight; getScanner() does
|
|
8868
8868
|
// a single rescan after the current one completes instead of restarting it.
|
|
8869
8869
|
scannerStale = false;
|
|
8870
|
+
// WHICH files scannerStale is about. A directory event names exactly one
|
|
8871
|
+
// JSONL, and the only correct response is refreshFile() on that one file —
|
|
8872
|
+
// but scannerStale alone carries no identity, so honoring it used to mean a
|
|
8873
|
+
// full-tree rescan. On the non-persistent scanner this server actually runs
|
|
8874
|
+
// (buildStatCache => persistent:false, see listen()), scan() opens by
|
|
8875
|
+
// clearing metadataCache AND conversationLRU — so one live session appending
|
|
8876
|
+
// to its own transcript threw away every OTHER conversation's parsed
|
|
8877
|
+
// snapshot, and the next full fetch of an unrelated conversation re-parsed
|
|
8878
|
+
// it from disk (745-2877ms on a 5MB/1112-message history) while the
|
|
8879
|
+
// per-file paginated path stayed at ~20ms throughout. Populated alongside
|
|
8880
|
+
// scannerStale and drained with it by takeStaleFiles(); an armed flag with
|
|
8881
|
+
// an EMPTY set means "stale, source unknown" and still falls back to the
|
|
8882
|
+
// full rescan.
|
|
8883
|
+
staleFiles = /* @__PURE__ */ new Set();
|
|
8870
8884
|
// Single-flight guard for the background disk reconcile: a burst of list
|
|
8871
8885
|
// polls during active session writes shares one rescan instead of queueing
|
|
8872
8886
|
// a full rescan per request.
|
|
@@ -9035,7 +9049,10 @@ var StreamerServer = class {
|
|
|
9035
9049
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
9036
9050
|
this.markScannerStaleDebounced = debounce(() => {
|
|
9037
9051
|
if (this.scannerReady) this.scannerStale = true;
|
|
9038
|
-
else
|
|
9052
|
+
else {
|
|
9053
|
+
this.scanner = null;
|
|
9054
|
+
this.staleFiles.clear();
|
|
9055
|
+
}
|
|
9039
9056
|
}, this.directoryDebounceMs);
|
|
9040
9057
|
this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
|
|
9041
9058
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
@@ -9128,6 +9145,7 @@ var StreamerServer = class {
|
|
|
9128
9145
|
if (!tailed) this.maybeAttachExternalTail(filePath);
|
|
9129
9146
|
this.sweepIdleExternalTails();
|
|
9130
9147
|
this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });
|
|
9148
|
+
this.staleFiles.add(filePath);
|
|
9131
9149
|
this.markScannerStaleDebounced();
|
|
9132
9150
|
this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {
|
|
9133
9151
|
filePath,
|
|
@@ -10374,21 +10392,46 @@ var StreamerServer = class {
|
|
|
10374
10392
|
// so a burst of list polls during active session writes shares one rescan
|
|
10375
10393
|
// rather than queueing a full rescan each; tracked so close() awaits the
|
|
10376
10394
|
// in-flight cache write before shutting the DB.
|
|
10377
|
-
startBackgroundConversationReconcile() {
|
|
10395
|
+
startBackgroundConversationReconcile(mode = "full") {
|
|
10378
10396
|
if (this.conversationReconcileInFlight) return;
|
|
10379
|
-
const
|
|
10397
|
+
const paths = mode === "files" ? this.takeStaleFiles() : [];
|
|
10398
|
+
const task = (paths.length > 0 ? this.reconcileStaleFilesFromDisk(paths) : this.reconcileConversationsCacheFromDisk()).finally(() => {
|
|
10380
10399
|
this.conversationReconcileInFlight = null;
|
|
10381
10400
|
});
|
|
10382
10401
|
this.conversationReconcileInFlight = task;
|
|
10383
10402
|
this.trackCacheWrite(task);
|
|
10384
10403
|
}
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10404
|
+
// "files": a directory event named specific JSONLs, so refresh only those.
|
|
10405
|
+
// "full": disk drifted in ways a per-file refresh can't see (a project dir
|
|
10406
|
+
// appeared, rows vanished), so walk the tree. Order matters — the staleness
|
|
10407
|
+
// check short-circuits first so the HDD freshness probe stays off the hot
|
|
10408
|
+
// poll path, exactly as it did when this returned a boolean.
|
|
10409
|
+
conversationReconcileMode() {
|
|
10410
|
+
if (!this.cache) return null;
|
|
10411
|
+
if (this.scannerStale) return "files";
|
|
10412
|
+
if (!this.conversationsRepo || !this.cacheMetadataRepo) return null;
|
|
10389
10413
|
return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
|
|
10390
10414
|
projectsDirs: this.projectsDirsForFreshnessCheck()
|
|
10391
|
-
});
|
|
10415
|
+
}) ? "full" : null;
|
|
10416
|
+
}
|
|
10417
|
+
// The per-file half of reconcileConversationsCacheFromDisk: re-index just the
|
|
10418
|
+
// changed JSONLs and upsert their rows. No reconcileDeletions here — that
|
|
10419
|
+
// needs the whole live-path set, and deletions already have their own path
|
|
10420
|
+
// (onFileDeleted -> invalidateByFilePath). New projects still arrive via the
|
|
10421
|
+
// HDD-freshness "full" mode.
|
|
10422
|
+
async reconcileStaleFilesFromDisk(paths) {
|
|
10423
|
+
if (!this.cache) return;
|
|
10424
|
+
const scanner = await this.getScanner(true);
|
|
10425
|
+
const metas = await this.refreshStaleFiles(scanner, paths);
|
|
10426
|
+
if (metas.length === 0) return;
|
|
10427
|
+
try {
|
|
10428
|
+
this.cache.upsertFromScannerMeta(metas);
|
|
10429
|
+
} catch (err) {
|
|
10430
|
+
this.log.warn(
|
|
10431
|
+
`stale-file reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
10432
|
+
{ event: "conversations.reconcile_failed" }
|
|
10433
|
+
);
|
|
10434
|
+
}
|
|
10392
10435
|
}
|
|
10393
10436
|
async handleListConversations(url, res) {
|
|
10394
10437
|
if (this.rejectIfWarmingUp(res)) return;
|
|
@@ -10398,10 +10441,11 @@ var StreamerServer = class {
|
|
|
10398
10441
|
const project = url.searchParams.get("project") ?? void 0;
|
|
10399
10442
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
10400
10443
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
10401
|
-
|
|
10444
|
+
const reconcileMode = this.conversationReconcileMode();
|
|
10445
|
+
if (this.cache && (bustCache || reconcileMode)) {
|
|
10402
10446
|
const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
|
|
10403
10447
|
if (canServeStale) {
|
|
10404
|
-
this.startBackgroundConversationReconcile();
|
|
10448
|
+
this.startBackgroundConversationReconcile(reconcileMode ?? "full");
|
|
10405
10449
|
} else {
|
|
10406
10450
|
const shouldEmitProgress = createScanProgressThrottle();
|
|
10407
10451
|
await this.withWarmup(
|
|
@@ -10601,21 +10645,54 @@ var StreamerServer = class {
|
|
|
10601
10645
|
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
10602
10646
|
);
|
|
10603
10647
|
}
|
|
10648
|
+
// Drain the stale set and disarm the flag together. The caller owns the
|
|
10649
|
+
// returned paths: clearing before the refresh means events that land DURING
|
|
10650
|
+
// it re-arm the flag and get their own pass instead of being swallowed.
|
|
10651
|
+
takeStaleFiles() {
|
|
10652
|
+
const paths = [...this.staleFiles];
|
|
10653
|
+
this.staleFiles.clear();
|
|
10654
|
+
this.scannerStale = false;
|
|
10655
|
+
return paths;
|
|
10656
|
+
}
|
|
10657
|
+
// Reconcile exactly the JSONLs a directory event named. Failures are logged
|
|
10658
|
+
// and swallowed per file: one unreadable transcript must not abort the
|
|
10659
|
+
// others, and the file simply stays on its previous snapshot until the next
|
|
10660
|
+
// event — the same outcome the full rescan gave on a parse failure.
|
|
10661
|
+
async refreshStaleFiles(scanner, paths) {
|
|
10662
|
+
const metas = await Promise.all(
|
|
10663
|
+
paths.map(
|
|
10664
|
+
(filePath) => scanner.refreshFile(filePath).catch((err) => {
|
|
10665
|
+
this.log.warn("scanner.refreshFile: failed", {
|
|
10666
|
+
event: "scanner.refresh_failed",
|
|
10667
|
+
filePath,
|
|
10668
|
+
trigger: "directory-event",
|
|
10669
|
+
err
|
|
10670
|
+
});
|
|
10671
|
+
return null;
|
|
10672
|
+
})
|
|
10673
|
+
)
|
|
10674
|
+
);
|
|
10675
|
+
return metas.filter((m) => m !== null);
|
|
10676
|
+
}
|
|
10604
10677
|
async getScanner(skipStaleRescan = false) {
|
|
10605
10678
|
if (this.scannerReady) {
|
|
10606
10679
|
await this.scannerReady;
|
|
10607
10680
|
if (this.scanner) {
|
|
10608
10681
|
if (skipStaleRescan) return this.scanner;
|
|
10609
10682
|
if (this.scannerStale) {
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10683
|
+
const paths = this.takeStaleFiles();
|
|
10684
|
+
if (paths.length === 0) {
|
|
10685
|
+
this.scanner = null;
|
|
10686
|
+
this.scannerReady = null;
|
|
10687
|
+
return this.getScanner();
|
|
10688
|
+
}
|
|
10689
|
+
await this.refreshStaleFiles(this.scanner, paths);
|
|
10690
|
+
return this.scanner ?? this.getScanner();
|
|
10614
10691
|
}
|
|
10615
10692
|
return this.scanner;
|
|
10616
10693
|
}
|
|
10617
10694
|
}
|
|
10618
|
-
this.
|
|
10695
|
+
this.takeStaleFiles();
|
|
10619
10696
|
const statCache = this.buildStatCache(this.scanner);
|
|
10620
10697
|
this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
|
|
10621
10698
|
this.allScanners.add(this.scanner);
|
|
@@ -10649,7 +10726,7 @@ var StreamerServer = class {
|
|
|
10649
10726
|
// getScanner() anti-infinite-loop guard is preserved.
|
|
10650
10727
|
async rescanForRefresh(onProgress) {
|
|
10651
10728
|
if (this.scannerReady) await this.scannerReady;
|
|
10652
|
-
this.
|
|
10729
|
+
this.takeStaleFiles();
|
|
10653
10730
|
if (!this.scanner) {
|
|
10654
10731
|
this.scanner = new import_scanner3.ConversationScanner();
|
|
10655
10732
|
this.allScanners.add(this.scanner);
|