@threadbase-sh/streamer 1.45.0 → 1.46.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 +84 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +84 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +84 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -5345,7 +5345,10 @@ var createMiscRoutes = (deps) => {
|
|
|
5345
5345
|
// Same contract: this server serves GET /api/config/feature-flags. Lives
|
|
5346
5346
|
// here rather than behind /api/config (admin-only) so a read-only client
|
|
5347
5347
|
// still learns the server supports flags even if it can't read values.
|
|
5348
|
-
featureFlags: true
|
|
5348
|
+
featureFlags: true,
|
|
5349
|
+
// Same contract: this server serves GET /api/projects/summary, which the
|
|
5350
|
+
// Hub's grouped views need before they can draw a tree.
|
|
5351
|
+
projectSummary: true
|
|
5349
5352
|
});
|
|
5350
5353
|
});
|
|
5351
5354
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -5492,6 +5495,11 @@ var createProjectRoutes = (deps) => {
|
|
|
5492
5495
|
deps.handleGetPopularProjects(url, c.env.outgoing);
|
|
5493
5496
|
return alreadyHandled4();
|
|
5494
5497
|
});
|
|
5498
|
+
app.get("/summary", (c) => {
|
|
5499
|
+
const url = new URL(c.req.url);
|
|
5500
|
+
deps.handleGetProjectSummaries(url, c.env.outgoing);
|
|
5501
|
+
return alreadyHandled4();
|
|
5502
|
+
});
|
|
5495
5503
|
return app;
|
|
5496
5504
|
};
|
|
5497
5505
|
|
|
@@ -6357,6 +6365,21 @@ var ConversationCache = class _ConversationCache {
|
|
|
6357
6365
|
ORDER BY cnt DESC
|
|
6358
6366
|
LIMIT ?`
|
|
6359
6367
|
),
|
|
6368
|
+
// Same table, same rows and same NULL filter /api/conversations lists
|
|
6369
|
+
// from, so a group's count/last-activity can never disagree with the
|
|
6370
|
+
// page it opens. Bare project_name is the one from the MAX(last_activity)
|
|
6371
|
+
// row (SQLite's documented min/max-aggregate bare-column rule).
|
|
6372
|
+
projectSummaries: db.prepare(
|
|
6373
|
+
`SELECT project_path, project_name, COUNT(*) as cnt, MAX(last_activity) as latest
|
|
6374
|
+
FROM conversation_meta
|
|
6375
|
+
WHERE project_path IS NOT NULL
|
|
6376
|
+
GROUP BY project_path
|
|
6377
|
+
ORDER BY latest DESC, project_path ASC
|
|
6378
|
+
LIMIT ? OFFSET ?`
|
|
6379
|
+
),
|
|
6380
|
+
projectSummaryCount: db.prepare(
|
|
6381
|
+
"SELECT COUNT(DISTINCT project_path) as n FROM conversation_meta WHERE project_path IS NOT NULL"
|
|
6382
|
+
),
|
|
6360
6383
|
getFileState: db.prepare("SELECT * FROM conversation_file_state WHERE path = ?"),
|
|
6361
6384
|
upsertFileState: db.prepare(
|
|
6362
6385
|
`INSERT INTO conversation_file_state
|
|
@@ -6732,6 +6755,23 @@ var ConversationCache = class _ConversationCache {
|
|
|
6732
6755
|
sessionCount: r.cnt
|
|
6733
6756
|
}));
|
|
6734
6757
|
}
|
|
6758
|
+
/** Every project with at least one cached conversation, most recently active
|
|
6759
|
+
* first. Paths are the raw `project_path` values, which is what
|
|
6760
|
+
* /api/conversations?project= matches on exactly — so a summary row is
|
|
6761
|
+
* always joinable against the page it describes. */
|
|
6762
|
+
listProjectSummaries(opts) {
|
|
6763
|
+
const total = this.stmts.projectSummaryCount.get().n;
|
|
6764
|
+
const rows = opts.limit === 0 ? [] : this.stmts.projectSummaries.all(opts.limit, opts.offset);
|
|
6765
|
+
return {
|
|
6766
|
+
total,
|
|
6767
|
+
projects: rows.map((r) => ({
|
|
6768
|
+
path: r.project_path,
|
|
6769
|
+
name: r.project_name ?? r.project_path.split(/[/\\]/).pop() ?? r.project_path,
|
|
6770
|
+
conversationCount: r.cnt,
|
|
6771
|
+
lastActivity: new Date(r.latest ?? 0).toISOString()
|
|
6772
|
+
}))
|
|
6773
|
+
};
|
|
6774
|
+
}
|
|
6735
6775
|
ensureFileIndex() {
|
|
6736
6776
|
if (this.fileIndexLoaded) return;
|
|
6737
6777
|
const rows = this.stmts.allFilePaths.all();
|
|
@@ -8384,6 +8424,27 @@ var ConversationWatcher = class {
|
|
|
8384
8424
|
* Watch a directory of conversation JSONL files. Fires
|
|
8385
8425
|
* onConversationChanged for any add/change/unlink event so the caller
|
|
8386
8426
|
* can mark the cache dirty without scanning everything immediately.
|
|
8427
|
+
*
|
|
8428
|
+
* **This costs one OS watch handle per file under `directory`, not one per
|
|
8429
|
+
* directory.** chokidar recurses the tree and registers a separate fs.watch
|
|
8430
|
+
* per entry, because a directory watch alone does not report writes to files
|
|
8431
|
+
* inside it — and per-file `change` events are exactly what the caller needs
|
|
8432
|
+
* (they drive poke()'s tail self-heal and the external-tail attach). So the
|
|
8433
|
+
* handle count tracks the size of the conversation corpus on disk, not the
|
|
8434
|
+
* number of live sessions, and it does not shrink until transcripts are
|
|
8435
|
+
* deleted. `ignoreInitial` suppresses the startup *events*, not the walk.
|
|
8436
|
+
*
|
|
8437
|
+
* Measured 2026-08-09 on the live macOS instance: 2131 open .jsonl fds
|
|
8438
|
+
* against 2133 files under the watched roots — 1:1, ~88% of all fds on the
|
|
8439
|
+
* process, at 2.0% of that box's 122 880 per-process ceiling. Comfortable
|
|
8440
|
+
* there. **Linux is the tight one**: these are inotify watches billed to the
|
|
8441
|
+
* per-user `max_user_watches`, which can be 8192 and is shared with every
|
|
8442
|
+
* other watcher the user runs. Exhaustion surfaces as ENOSPC on the `error`
|
|
8443
|
+
* event — which is why server.ts wires onError rather than leaving it unset.
|
|
8444
|
+
*
|
|
8445
|
+
* Before trading handles for a bound here, note the regression it invites: a
|
|
8446
|
+
* conversation excluded from the walk (by age or by an LRU cap) is one whose
|
|
8447
|
+
* external appends produce no event at all, and that failure is silent.
|
|
8387
8448
|
*/
|
|
8388
8449
|
watchDirectory(directory) {
|
|
8389
8450
|
if (this.directories.has(directory)) return;
|
|
@@ -10680,6 +10741,13 @@ var StreamerServer = class {
|
|
|
10680
10741
|
event: "cache.invalidate_on_unlink"
|
|
10681
10742
|
});
|
|
10682
10743
|
this.cacheMonitor?.recordUnlink(filePath);
|
|
10744
|
+
},
|
|
10745
|
+
onError: (filePath, err) => {
|
|
10746
|
+
const enospc = err.code === "ENOSPC";
|
|
10747
|
+
this.log.error(
|
|
10748
|
+
enospc ? `Watcher hit the OS watch-handle limit on ${filePath} \u2014 raise fs.inotify.max_user_watches (Linux) or the process fd limit; conversation discovery and live tails are degraded until then` : `Watcher error on ${filePath}: ${err.message}`,
|
|
10749
|
+
{ filePath, err, event: enospc ? "watcher.limit_exhausted" : "watcher.error" }
|
|
10750
|
+
);
|
|
10683
10751
|
}
|
|
10684
10752
|
});
|
|
10685
10753
|
this.ptyManager = new LiveSessionManager({
|
|
@@ -10873,6 +10941,7 @@ var StreamerServer = class {
|
|
|
10873
10941
|
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
10874
10942
|
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
10875
10943
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
10944
|
+
handleGetProjectSummaries: (url, res) => this.handleGetProjectSummaries(url, res),
|
|
10876
10945
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
10877
10946
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
10878
10947
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
@@ -12438,6 +12507,20 @@ var StreamerServer = class {
|
|
|
12438
12507
|
const projects = this.cache.getPopularProjects(limit);
|
|
12439
12508
|
json(res, 200, { projects, total: projects.length });
|
|
12440
12509
|
}
|
|
12510
|
+
handleGetProjectSummaries(url, res) {
|
|
12511
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
12512
|
+
const limit = intParam(url, "limit", 200);
|
|
12513
|
+
const offset = intParam(url, "offset", 0);
|
|
12514
|
+
if (!this.cache) {
|
|
12515
|
+
json(res, 503, {
|
|
12516
|
+
error: "Conversation cache unavailable",
|
|
12517
|
+
code: "CACHE_UNAVAILABLE"
|
|
12518
|
+
});
|
|
12519
|
+
return;
|
|
12520
|
+
}
|
|
12521
|
+
const { projects, total } = this.cache.listProjectSummaries({ limit, offset });
|
|
12522
|
+
json(res, 200, { projects, total, offset, hasMore: offset + projects.length < total });
|
|
12523
|
+
}
|
|
12441
12524
|
buildStatCache(previousScanner) {
|
|
12442
12525
|
if (!this.cache) return void 0;
|
|
12443
12526
|
if (!previousScanner) {
|