@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.d.cts
CHANGED
|
@@ -1582,6 +1582,7 @@ declare class StreamerServer {
|
|
|
1582
1582
|
private allScanners;
|
|
1583
1583
|
private scannerReady;
|
|
1584
1584
|
private scannerStale;
|
|
1585
|
+
private staleFiles;
|
|
1585
1586
|
private conversationReconcileInFlight;
|
|
1586
1587
|
private refreshInFlight;
|
|
1587
1588
|
private binding;
|
|
@@ -1821,7 +1822,8 @@ declare class StreamerServer {
|
|
|
1821
1822
|
*/
|
|
1822
1823
|
private reconcileConversationsCacheFromDisk;
|
|
1823
1824
|
private startBackgroundConversationReconcile;
|
|
1824
|
-
private
|
|
1825
|
+
private conversationReconcileMode;
|
|
1826
|
+
private reconcileStaleFilesFromDisk;
|
|
1825
1827
|
private handleListConversations;
|
|
1826
1828
|
private handleConversationsCount;
|
|
1827
1829
|
private refreshCountInBackground;
|
|
@@ -1831,6 +1833,8 @@ declare class StreamerServer {
|
|
|
1831
1833
|
private buildStatCache;
|
|
1832
1834
|
private codexScanOpts;
|
|
1833
1835
|
private newScanner;
|
|
1836
|
+
private takeStaleFiles;
|
|
1837
|
+
private refreshStaleFiles;
|
|
1834
1838
|
private getScanner;
|
|
1835
1839
|
private getFreshScanner;
|
|
1836
1840
|
private rescanForRefresh;
|
package/dist/index.d.ts
CHANGED
|
@@ -1582,6 +1582,7 @@ declare class StreamerServer {
|
|
|
1582
1582
|
private allScanners;
|
|
1583
1583
|
private scannerReady;
|
|
1584
1584
|
private scannerStale;
|
|
1585
|
+
private staleFiles;
|
|
1585
1586
|
private conversationReconcileInFlight;
|
|
1586
1587
|
private refreshInFlight;
|
|
1587
1588
|
private binding;
|
|
@@ -1821,7 +1822,8 @@ declare class StreamerServer {
|
|
|
1821
1822
|
*/
|
|
1822
1823
|
private reconcileConversationsCacheFromDisk;
|
|
1823
1824
|
private startBackgroundConversationReconcile;
|
|
1824
|
-
private
|
|
1825
|
+
private conversationReconcileMode;
|
|
1826
|
+
private reconcileStaleFilesFromDisk;
|
|
1825
1827
|
private handleListConversations;
|
|
1826
1828
|
private handleConversationsCount;
|
|
1827
1829
|
private refreshCountInBackground;
|
|
@@ -1831,6 +1833,8 @@ declare class StreamerServer {
|
|
|
1831
1833
|
private buildStatCache;
|
|
1832
1834
|
private codexScanOpts;
|
|
1833
1835
|
private newScanner;
|
|
1836
|
+
private takeStaleFiles;
|
|
1837
|
+
private refreshStaleFiles;
|
|
1834
1838
|
private getScanner;
|
|
1835
1839
|
private getFreshScanner;
|
|
1836
1840
|
private rescanForRefresh;
|
package/dist/index.js
CHANGED
|
@@ -8830,6 +8830,20 @@ var StreamerServer = class {
|
|
|
8830
8830
|
// Set by onConversationChanged while a scan is in-flight; getScanner() does
|
|
8831
8831
|
// a single rescan after the current one completes instead of restarting it.
|
|
8832
8832
|
scannerStale = false;
|
|
8833
|
+
// WHICH files scannerStale is about. A directory event names exactly one
|
|
8834
|
+
// JSONL, and the only correct response is refreshFile() on that one file —
|
|
8835
|
+
// but scannerStale alone carries no identity, so honoring it used to mean a
|
|
8836
|
+
// full-tree rescan. On the non-persistent scanner this server actually runs
|
|
8837
|
+
// (buildStatCache => persistent:false, see listen()), scan() opens by
|
|
8838
|
+
// clearing metadataCache AND conversationLRU — so one live session appending
|
|
8839
|
+
// to its own transcript threw away every OTHER conversation's parsed
|
|
8840
|
+
// snapshot, and the next full fetch of an unrelated conversation re-parsed
|
|
8841
|
+
// it from disk (745-2877ms on a 5MB/1112-message history) while the
|
|
8842
|
+
// per-file paginated path stayed at ~20ms throughout. Populated alongside
|
|
8843
|
+
// scannerStale and drained with it by takeStaleFiles(); an armed flag with
|
|
8844
|
+
// an EMPTY set means "stale, source unknown" and still falls back to the
|
|
8845
|
+
// full rescan.
|
|
8846
|
+
staleFiles = /* @__PURE__ */ new Set();
|
|
8833
8847
|
// Single-flight guard for the background disk reconcile: a burst of list
|
|
8834
8848
|
// polls during active session writes shares one rescan instead of queueing
|
|
8835
8849
|
// a full rescan per request.
|
|
@@ -8998,7 +9012,10 @@ var StreamerServer = class {
|
|
|
8998
9012
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
8999
9013
|
this.markScannerStaleDebounced = debounce(() => {
|
|
9000
9014
|
if (this.scannerReady) this.scannerStale = true;
|
|
9001
|
-
else
|
|
9015
|
+
else {
|
|
9016
|
+
this.scanner = null;
|
|
9017
|
+
this.staleFiles.clear();
|
|
9018
|
+
}
|
|
9002
9019
|
}, this.directoryDebounceMs);
|
|
9003
9020
|
this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
|
|
9004
9021
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
@@ -9091,6 +9108,7 @@ var StreamerServer = class {
|
|
|
9091
9108
|
if (!tailed) this.maybeAttachExternalTail(filePath);
|
|
9092
9109
|
this.sweepIdleExternalTails();
|
|
9093
9110
|
this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });
|
|
9111
|
+
this.staleFiles.add(filePath);
|
|
9094
9112
|
this.markScannerStaleDebounced();
|
|
9095
9113
|
this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {
|
|
9096
9114
|
filePath,
|
|
@@ -10337,21 +10355,46 @@ var StreamerServer = class {
|
|
|
10337
10355
|
// so a burst of list polls during active session writes shares one rescan
|
|
10338
10356
|
// rather than queueing a full rescan each; tracked so close() awaits the
|
|
10339
10357
|
// in-flight cache write before shutting the DB.
|
|
10340
|
-
startBackgroundConversationReconcile() {
|
|
10358
|
+
startBackgroundConversationReconcile(mode = "full") {
|
|
10341
10359
|
if (this.conversationReconcileInFlight) return;
|
|
10342
|
-
const
|
|
10360
|
+
const paths = mode === "files" ? this.takeStaleFiles() : [];
|
|
10361
|
+
const task = (paths.length > 0 ? this.reconcileStaleFilesFromDisk(paths) : this.reconcileConversationsCacheFromDisk()).finally(() => {
|
|
10343
10362
|
this.conversationReconcileInFlight = null;
|
|
10344
10363
|
});
|
|
10345
10364
|
this.conversationReconcileInFlight = task;
|
|
10346
10365
|
this.trackCacheWrite(task);
|
|
10347
10366
|
}
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10367
|
+
// "files": a directory event named specific JSONLs, so refresh only those.
|
|
10368
|
+
// "full": disk drifted in ways a per-file refresh can't see (a project dir
|
|
10369
|
+
// appeared, rows vanished), so walk the tree. Order matters — the staleness
|
|
10370
|
+
// check short-circuits first so the HDD freshness probe stays off the hot
|
|
10371
|
+
// poll path, exactly as it did when this returned a boolean.
|
|
10372
|
+
conversationReconcileMode() {
|
|
10373
|
+
if (!this.cache) return null;
|
|
10374
|
+
if (this.scannerStale) return "files";
|
|
10375
|
+
if (!this.conversationsRepo || !this.cacheMetadataRepo) return null;
|
|
10352
10376
|
return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
|
|
10353
10377
|
projectsDirs: this.projectsDirsForFreshnessCheck()
|
|
10354
|
-
});
|
|
10378
|
+
}) ? "full" : null;
|
|
10379
|
+
}
|
|
10380
|
+
// The per-file half of reconcileConversationsCacheFromDisk: re-index just the
|
|
10381
|
+
// changed JSONLs and upsert their rows. No reconcileDeletions here — that
|
|
10382
|
+
// needs the whole live-path set, and deletions already have their own path
|
|
10383
|
+
// (onFileDeleted -> invalidateByFilePath). New projects still arrive via the
|
|
10384
|
+
// HDD-freshness "full" mode.
|
|
10385
|
+
async reconcileStaleFilesFromDisk(paths) {
|
|
10386
|
+
if (!this.cache) return;
|
|
10387
|
+
const scanner = await this.getScanner(true);
|
|
10388
|
+
const metas = await this.refreshStaleFiles(scanner, paths);
|
|
10389
|
+
if (metas.length === 0) return;
|
|
10390
|
+
try {
|
|
10391
|
+
this.cache.upsertFromScannerMeta(metas);
|
|
10392
|
+
} catch (err) {
|
|
10393
|
+
this.log.warn(
|
|
10394
|
+
`stale-file reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
10395
|
+
{ event: "conversations.reconcile_failed" }
|
|
10396
|
+
);
|
|
10397
|
+
}
|
|
10355
10398
|
}
|
|
10356
10399
|
async handleListConversations(url, res) {
|
|
10357
10400
|
if (this.rejectIfWarmingUp(res)) return;
|
|
@@ -10361,10 +10404,11 @@ var StreamerServer = class {
|
|
|
10361
10404
|
const project = url.searchParams.get("project") ?? void 0;
|
|
10362
10405
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
10363
10406
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
10364
|
-
|
|
10407
|
+
const reconcileMode = this.conversationReconcileMode();
|
|
10408
|
+
if (this.cache && (bustCache || reconcileMode)) {
|
|
10365
10409
|
const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
|
|
10366
10410
|
if (canServeStale) {
|
|
10367
|
-
this.startBackgroundConversationReconcile();
|
|
10411
|
+
this.startBackgroundConversationReconcile(reconcileMode ?? "full");
|
|
10368
10412
|
} else {
|
|
10369
10413
|
const shouldEmitProgress = createScanProgressThrottle();
|
|
10370
10414
|
await this.withWarmup(
|
|
@@ -10564,21 +10608,54 @@ var StreamerServer = class {
|
|
|
10564
10608
|
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
10565
10609
|
);
|
|
10566
10610
|
}
|
|
10611
|
+
// Drain the stale set and disarm the flag together. The caller owns the
|
|
10612
|
+
// returned paths: clearing before the refresh means events that land DURING
|
|
10613
|
+
// it re-arm the flag and get their own pass instead of being swallowed.
|
|
10614
|
+
takeStaleFiles() {
|
|
10615
|
+
const paths = [...this.staleFiles];
|
|
10616
|
+
this.staleFiles.clear();
|
|
10617
|
+
this.scannerStale = false;
|
|
10618
|
+
return paths;
|
|
10619
|
+
}
|
|
10620
|
+
// Reconcile exactly the JSONLs a directory event named. Failures are logged
|
|
10621
|
+
// and swallowed per file: one unreadable transcript must not abort the
|
|
10622
|
+
// others, and the file simply stays on its previous snapshot until the next
|
|
10623
|
+
// event — the same outcome the full rescan gave on a parse failure.
|
|
10624
|
+
async refreshStaleFiles(scanner, paths) {
|
|
10625
|
+
const metas = await Promise.all(
|
|
10626
|
+
paths.map(
|
|
10627
|
+
(filePath) => scanner.refreshFile(filePath).catch((err) => {
|
|
10628
|
+
this.log.warn("scanner.refreshFile: failed", {
|
|
10629
|
+
event: "scanner.refresh_failed",
|
|
10630
|
+
filePath,
|
|
10631
|
+
trigger: "directory-event",
|
|
10632
|
+
err
|
|
10633
|
+
});
|
|
10634
|
+
return null;
|
|
10635
|
+
})
|
|
10636
|
+
)
|
|
10637
|
+
);
|
|
10638
|
+
return metas.filter((m) => m !== null);
|
|
10639
|
+
}
|
|
10567
10640
|
async getScanner(skipStaleRescan = false) {
|
|
10568
10641
|
if (this.scannerReady) {
|
|
10569
10642
|
await this.scannerReady;
|
|
10570
10643
|
if (this.scanner) {
|
|
10571
10644
|
if (skipStaleRescan) return this.scanner;
|
|
10572
10645
|
if (this.scannerStale) {
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10646
|
+
const paths = this.takeStaleFiles();
|
|
10647
|
+
if (paths.length === 0) {
|
|
10648
|
+
this.scanner = null;
|
|
10649
|
+
this.scannerReady = null;
|
|
10650
|
+
return this.getScanner();
|
|
10651
|
+
}
|
|
10652
|
+
await this.refreshStaleFiles(this.scanner, paths);
|
|
10653
|
+
return this.scanner ?? this.getScanner();
|
|
10577
10654
|
}
|
|
10578
10655
|
return this.scanner;
|
|
10579
10656
|
}
|
|
10580
10657
|
}
|
|
10581
|
-
this.
|
|
10658
|
+
this.takeStaleFiles();
|
|
10582
10659
|
const statCache = this.buildStatCache(this.scanner);
|
|
10583
10660
|
this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
|
|
10584
10661
|
this.allScanners.add(this.scanner);
|
|
@@ -10612,7 +10689,7 @@ var StreamerServer = class {
|
|
|
10612
10689
|
// getScanner() anti-infinite-loop guard is preserved.
|
|
10613
10690
|
async rescanForRefresh(onProgress) {
|
|
10614
10691
|
if (this.scannerReady) await this.scannerReady;
|
|
10615
|
-
this.
|
|
10692
|
+
this.takeStaleFiles();
|
|
10616
10693
|
if (!this.scanner) {
|
|
10617
10694
|
this.scanner = new ConversationScanner();
|
|
10618
10695
|
this.allScanners.add(this.scanner);
|