@threadbase-sh/streamer 1.34.0 → 1.36.0
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 +94 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +94 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.js +94 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -5513,12 +5513,13 @@ function fingerprintOf(ids) {
|
|
|
5513
5513
|
return `sha256:${(0, import_crypto8.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
|
|
5514
5514
|
}
|
|
5515
5515
|
var CacheIntegrityMonitor = class {
|
|
5516
|
-
constructor(cache, wsHub, log3, cacheDir, rescan) {
|
|
5516
|
+
constructor(cache, wsHub, log3, cacheDir, rescan, runDuringReset) {
|
|
5517
5517
|
this.cache = cache;
|
|
5518
5518
|
this.wsHub = wsHub;
|
|
5519
5519
|
this.log = log3;
|
|
5520
5520
|
this.cacheDir = cacheDir;
|
|
5521
5521
|
this.rescan = rescan;
|
|
5522
|
+
this.runDuringReset = runDuringReset;
|
|
5522
5523
|
const state = loadAlertState();
|
|
5523
5524
|
this._pending = state.pending ?? null;
|
|
5524
5525
|
this.ignoredIds = new Set(state.ignoredIds ?? []);
|
|
@@ -5528,6 +5529,7 @@ var CacheIntegrityMonitor = class {
|
|
|
5528
5529
|
log;
|
|
5529
5530
|
cacheDir;
|
|
5530
5531
|
rescan;
|
|
5532
|
+
runDuringReset;
|
|
5531
5533
|
_pending;
|
|
5532
5534
|
ignoredIds;
|
|
5533
5535
|
deferredUnlinks = [];
|
|
@@ -5731,18 +5733,20 @@ var CacheIntegrityMonitor = class {
|
|
|
5731
5733
|
}
|
|
5732
5734
|
case "reset_rescan": {
|
|
5733
5735
|
const backupPath = await this.ensureBackup(pending);
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5736
|
+
const reset = async () => {
|
|
5737
|
+
this.cache.clearAll();
|
|
5738
|
+
if (this.rescan) {
|
|
5737
5739
|
const metas = await this.rescan();
|
|
5738
5740
|
this.cache.upsertFromScannerMeta(metas);
|
|
5739
|
-
} catch (err) {
|
|
5740
|
-
this.log.error("cache-integrity reset rescan failed", {
|
|
5741
|
-
event: "cache_integrity.reset_rescan_failed",
|
|
5742
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5743
|
-
});
|
|
5744
5741
|
}
|
|
5745
|
-
}
|
|
5742
|
+
};
|
|
5743
|
+
const resetPromise = this.runDuringReset ? this.runDuringReset(reset) : reset();
|
|
5744
|
+
void resetPromise.catch((err) => {
|
|
5745
|
+
this.log.error("cache-integrity reset rescan failed", {
|
|
5746
|
+
event: "cache_integrity.reset_rescan_failed",
|
|
5747
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5748
|
+
});
|
|
5749
|
+
});
|
|
5746
5750
|
this.clearPending();
|
|
5747
5751
|
this.broadcastResolved(fingerprint, action);
|
|
5748
5752
|
return { ok: true, action, backupPath };
|
|
@@ -6733,7 +6737,8 @@ var StreamerServer = class {
|
|
|
6733
6737
|
// listener-level 'error' handler demotes EADDRINUSE to debug during this
|
|
6734
6738
|
// window so the self-healing kickstart-relaunch race doesn't spam warn.
|
|
6735
6739
|
binding = false;
|
|
6736
|
-
|
|
6740
|
+
activeWarmups = /* @__PURE__ */ new Map([[0, "startup"]]);
|
|
6741
|
+
nextWarmupId = 1;
|
|
6737
6742
|
// Every fire-and-forget task that runs a scan and then writes to this.cache
|
|
6738
6743
|
// in an async continuation (startup warm-up, background count refresh, …).
|
|
6739
6744
|
// close() awaits all of them before closing this.cache, so a scan's post-scan
|
|
@@ -7083,7 +7088,7 @@ var StreamerServer = class {
|
|
|
7083
7088
|
this.wsHub.addClient(ws);
|
|
7084
7089
|
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
7085
7090
|
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
7086
|
-
if (this.
|
|
7091
|
+
if (!this.currentWarmupState()) {
|
|
7087
7092
|
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
7088
7093
|
}
|
|
7089
7094
|
const alertMsg = this.cacheMonitor?.wsMessage();
|
|
@@ -7292,6 +7297,39 @@ var StreamerServer = class {
|
|
|
7292
7297
|
const addr = this.httpServer.address();
|
|
7293
7298
|
return typeof addr === "object" && addr ? addr.port : 0;
|
|
7294
7299
|
}
|
|
7300
|
+
currentWarmupState() {
|
|
7301
|
+
let current = null;
|
|
7302
|
+
for (const state of this.activeWarmups.values()) current = state;
|
|
7303
|
+
return current;
|
|
7304
|
+
}
|
|
7305
|
+
beginWarmup(state) {
|
|
7306
|
+
const id = this.nextWarmupId++;
|
|
7307
|
+
this.activeWarmups.set(id, state);
|
|
7308
|
+
return id;
|
|
7309
|
+
}
|
|
7310
|
+
finishWarmup(id) {
|
|
7311
|
+
if (!this.activeWarmups.delete(id) || this.activeWarmups.size > 0) return;
|
|
7312
|
+
this.wsHub.broadcast({ type: "cache_ready" });
|
|
7313
|
+
}
|
|
7314
|
+
async withWarmup(state, operation) {
|
|
7315
|
+
const id = this.beginWarmup(state);
|
|
7316
|
+
try {
|
|
7317
|
+
return await operation();
|
|
7318
|
+
} finally {
|
|
7319
|
+
this.finishWarmup(id);
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
rejectIfWarmingUp(res) {
|
|
7323
|
+
const warmupState = this.currentWarmupState();
|
|
7324
|
+
if (!warmupState) return false;
|
|
7325
|
+
const body = {
|
|
7326
|
+
error: "Server is warming up",
|
|
7327
|
+
code: "SERVER_WARMING_UP",
|
|
7328
|
+
warmupState
|
|
7329
|
+
};
|
|
7330
|
+
json(res, 503, body);
|
|
7331
|
+
return true;
|
|
7332
|
+
}
|
|
7295
7333
|
async listen(port, opts) {
|
|
7296
7334
|
const dbConfig = this.disableDb ? null : getDbConfig();
|
|
7297
7335
|
if (dbConfig) {
|
|
@@ -7349,6 +7387,11 @@ var StreamerServer = class {
|
|
|
7349
7387
|
async () => {
|
|
7350
7388
|
const scanner = await this.rescanForRefresh();
|
|
7351
7389
|
return [...scanner.getMetadataCache().values()];
|
|
7390
|
+
},
|
|
7391
|
+
(operation) => {
|
|
7392
|
+
const reset = this.withWarmup("cache_reset", operation);
|
|
7393
|
+
this.trackCacheWrite(reset);
|
|
7394
|
+
return reset;
|
|
7352
7395
|
}
|
|
7353
7396
|
);
|
|
7354
7397
|
for (const dir of this.projectsDirs()) {
|
|
@@ -7444,8 +7487,7 @@ var StreamerServer = class {
|
|
|
7444
7487
|
event: "cache.warmup_failed"
|
|
7445
7488
|
});
|
|
7446
7489
|
}).finally(() => {
|
|
7447
|
-
this.
|
|
7448
|
-
this.wsHub.broadcast({ type: "cache_ready" });
|
|
7490
|
+
this.finishWarmup(0);
|
|
7449
7491
|
resolveWarm();
|
|
7450
7492
|
});
|
|
7451
7493
|
}
|
|
@@ -7677,6 +7719,7 @@ var StreamerServer = class {
|
|
|
7677
7719
|
return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
|
|
7678
7720
|
}
|
|
7679
7721
|
async handleListConversations(url, res) {
|
|
7722
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
7680
7723
|
const limit = intParam(url, "limit", 50);
|
|
7681
7724
|
const offset = intParam(url, "offset", 0);
|
|
7682
7725
|
const sort = url.searchParams.get("sort") ?? "recent";
|
|
@@ -7684,7 +7727,7 @@ var StreamerServer = class {
|
|
|
7684
7727
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
7685
7728
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
7686
7729
|
if (bustCache && this.cache) {
|
|
7687
|
-
const scanner2 = await this.rescanForRefresh();
|
|
7730
|
+
const scanner2 = await this.withWarmup("conversation_refresh", () => this.rescanForRefresh());
|
|
7688
7731
|
const metas2 = [...scanner2.getMetadataCache().values()];
|
|
7689
7732
|
try {
|
|
7690
7733
|
this.cache.upsertFromScannerMeta(metas2);
|
|
@@ -7765,6 +7808,7 @@ var StreamerServer = class {
|
|
|
7765
7808
|
json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });
|
|
7766
7809
|
}
|
|
7767
7810
|
async handleConversationsCount(url, res) {
|
|
7811
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
7768
7812
|
const project = url.searchParams.get("project") ?? void 0;
|
|
7769
7813
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
7770
7814
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
@@ -7792,7 +7836,7 @@ var StreamerServer = class {
|
|
|
7792
7836
|
// path — refresh=1 returns the cached total synchronously and this catches up.
|
|
7793
7837
|
refreshCountInBackground() {
|
|
7794
7838
|
this.trackCacheWrite(
|
|
7795
|
-
(async () => {
|
|
7839
|
+
this.withWarmup("conversation_refresh", async () => {
|
|
7796
7840
|
try {
|
|
7797
7841
|
const scanner = await this.getFreshScanner();
|
|
7798
7842
|
if (this.cache) {
|
|
@@ -7804,13 +7848,15 @@ var StreamerServer = class {
|
|
|
7804
7848
|
{ event: "count.refresh_failed" }
|
|
7805
7849
|
);
|
|
7806
7850
|
}
|
|
7807
|
-
})
|
|
7851
|
+
})
|
|
7808
7852
|
);
|
|
7809
7853
|
}
|
|
7810
7854
|
handleSessionsCount(res) {
|
|
7855
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
7811
7856
|
json(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });
|
|
7812
7857
|
}
|
|
7813
7858
|
handleGetRecentSessions(url, res) {
|
|
7859
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
7814
7860
|
const limit = intParam(url, "limit", 20);
|
|
7815
7861
|
if (!this.cache) {
|
|
7816
7862
|
json(res, 200, { sessions: [], total: 0 });
|
|
@@ -8258,6 +8304,7 @@ var StreamerServer = class {
|
|
|
8258
8304
|
return isScannedSnapshotStale(conv.timestamp, mtimeMs);
|
|
8259
8305
|
}
|
|
8260
8306
|
async handleGetConversation(id, url, res, ifNoneMatch) {
|
|
8307
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
8261
8308
|
const conversation = await this.findConversationByUuid(id);
|
|
8262
8309
|
if (!conversation && this.cache) {
|
|
8263
8310
|
const isFirstLoad = !url.searchParams.has("before_index");
|
|
@@ -8280,6 +8327,7 @@ var StreamerServer = class {
|
|
|
8280
8327
|
id,
|
|
8281
8328
|
profile_id: cachedMeta?.account ?? void 0,
|
|
8282
8329
|
project_name: cachedMeta?.projectName ?? void 0,
|
|
8330
|
+
session_name: cachedMeta?.title ?? void 0,
|
|
8283
8331
|
project_path: cachedMeta?.projectPath ?? void 0,
|
|
8284
8332
|
file_path: cachedMeta?.filePath ?? void 0,
|
|
8285
8333
|
last_updated_at: cachedMeta?.lastActivity ?? void 0,
|
|
@@ -8453,6 +8501,7 @@ var StreamerServer = class {
|
|
|
8453
8501
|
id,
|
|
8454
8502
|
profile_id: conv.account,
|
|
8455
8503
|
project_name: conv.projectName,
|
|
8504
|
+
session_name: conv.sessionName || void 0,
|
|
8456
8505
|
project_path: conv.projectPath,
|
|
8457
8506
|
file_path: conv.filePath,
|
|
8458
8507
|
last_updated_at: metaLastUpdatedAt,
|
|
@@ -8585,6 +8634,7 @@ var StreamerServer = class {
|
|
|
8585
8634
|
});
|
|
8586
8635
|
}
|
|
8587
8636
|
async handleListSessions(url, res) {
|
|
8637
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
8588
8638
|
const now = Date.now();
|
|
8589
8639
|
if (!this.discoveryCache || now - this.discoveryCache.fetchedAt >= DISCOVERY_TTL_MS) {
|
|
8590
8640
|
try {
|
|
@@ -8617,6 +8667,7 @@ var StreamerServer = class {
|
|
|
8617
8667
|
}
|
|
8618
8668
|
}
|
|
8619
8669
|
handleGetSession(sessionId, res) {
|
|
8670
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
8620
8671
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
8621
8672
|
if (session) {
|
|
8622
8673
|
if (!(0, import_fs17.existsSync)(session.projectPath)) {
|
|
@@ -9068,12 +9119,21 @@ var StreamerServer = class {
|
|
|
9068
9119
|
json(res, 404, { error: "Discovered session not found" });
|
|
9069
9120
|
return;
|
|
9070
9121
|
}
|
|
9071
|
-
const {
|
|
9122
|
+
const { branch } = discSession;
|
|
9123
|
+
let { projectPath, projectName } = discSession;
|
|
9072
9124
|
const convId = discSession.id;
|
|
9073
9125
|
if (discSession.pid == null) {
|
|
9074
9126
|
json(res, 400, { error: "Session has no known PID" });
|
|
9075
9127
|
return;
|
|
9076
9128
|
}
|
|
9129
|
+
if (!projectPath) {
|
|
9130
|
+
const jsonlPath = this.findJsonlPath(convId);
|
|
9131
|
+
const jsonlCwd = jsonlPath ? await this.readCwdFromJsonl(jsonlPath) : null;
|
|
9132
|
+
if (jsonlCwd) {
|
|
9133
|
+
projectPath = jsonlCwd;
|
|
9134
|
+
projectName = projectName || (0, import_path17.basename)(jsonlCwd);
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9077
9137
|
if (!projectPath) {
|
|
9078
9138
|
this.log.warn("adopt: refusing, working directory unknown", {
|
|
9079
9139
|
event: "adopt.no_project_path",
|
|
@@ -9086,6 +9146,22 @@ var StreamerServer = class {
|
|
|
9086
9146
|
});
|
|
9087
9147
|
return;
|
|
9088
9148
|
}
|
|
9149
|
+
const availability = classifyResumability(projectPath);
|
|
9150
|
+
if (!availability.resumable) {
|
|
9151
|
+
this.log.warn("adopt: refusing, project directory no longer exists", {
|
|
9152
|
+
event: "adopt.project_path_missing",
|
|
9153
|
+
sessionId,
|
|
9154
|
+
pid: discSession.pid,
|
|
9155
|
+
projectPath,
|
|
9156
|
+
reason: availability.unavailable_reason
|
|
9157
|
+
});
|
|
9158
|
+
json(res, 400, {
|
|
9159
|
+
error: "Cannot take over this session: its project directory no longer exists",
|
|
9160
|
+
code: "ADOPT_PROJECT_PATH_MISSING",
|
|
9161
|
+
reason: availability.unavailable_reason
|
|
9162
|
+
});
|
|
9163
|
+
return;
|
|
9164
|
+
}
|
|
9089
9165
|
this.ptyManager.killPid(discSession.pid);
|
|
9090
9166
|
const exited = await waitForProcessExit(discSession.pid, ADOPT_KILL_TIMEOUT_MS);
|
|
9091
9167
|
if (!exited) {
|