@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/cli.cjs
CHANGED
|
@@ -142383,7 +142383,10 @@ var createMiscRoutes = (deps) => {
|
|
|
142383
142383
|
// Same contract: this server serves GET /api/config/feature-flags. Lives
|
|
142384
142384
|
// here rather than behind /api/config (admin-only) so a read-only client
|
|
142385
142385
|
// still learns the server supports flags even if it can't read values.
|
|
142386
|
-
featureFlags: true
|
|
142386
|
+
featureFlags: true,
|
|
142387
|
+
// Same contract: this server serves GET /api/projects/summary, which the
|
|
142388
|
+
// Hub's grouped views need before they can draw a tree.
|
|
142389
|
+
projectSummary: true
|
|
142387
142390
|
});
|
|
142388
142391
|
});
|
|
142389
142392
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -142658,6 +142661,11 @@ var createProjectRoutes = (deps) => {
|
|
|
142658
142661
|
deps.handleGetPopularProjects(url2, c.env.outgoing);
|
|
142659
142662
|
return alreadyHandled4();
|
|
142660
142663
|
});
|
|
142664
|
+
app.get("/summary", (c) => {
|
|
142665
|
+
const url2 = new URL(c.req.url);
|
|
142666
|
+
deps.handleGetProjectSummaries(url2, c.env.outgoing);
|
|
142667
|
+
return alreadyHandled4();
|
|
142668
|
+
});
|
|
142661
142669
|
return app;
|
|
142662
142670
|
};
|
|
142663
142671
|
|
|
@@ -143533,6 +143541,21 @@ var ConversationCache = class _ConversationCache {
|
|
|
143533
143541
|
ORDER BY cnt DESC
|
|
143534
143542
|
LIMIT ?`
|
|
143535
143543
|
),
|
|
143544
|
+
// Same table, same rows and same NULL filter /api/conversations lists
|
|
143545
|
+
// from, so a group's count/last-activity can never disagree with the
|
|
143546
|
+
// page it opens. Bare project_name is the one from the MAX(last_activity)
|
|
143547
|
+
// row (SQLite's documented min/max-aggregate bare-column rule).
|
|
143548
|
+
projectSummaries: db.prepare(
|
|
143549
|
+
`SELECT project_path, project_name, COUNT(*) as cnt, MAX(last_activity) as latest
|
|
143550
|
+
FROM conversation_meta
|
|
143551
|
+
WHERE project_path IS NOT NULL
|
|
143552
|
+
GROUP BY project_path
|
|
143553
|
+
ORDER BY latest DESC, project_path ASC
|
|
143554
|
+
LIMIT ? OFFSET ?`
|
|
143555
|
+
),
|
|
143556
|
+
projectSummaryCount: db.prepare(
|
|
143557
|
+
"SELECT COUNT(DISTINCT project_path) as n FROM conversation_meta WHERE project_path IS NOT NULL"
|
|
143558
|
+
),
|
|
143536
143559
|
getFileState: db.prepare("SELECT * FROM conversation_file_state WHERE path = ?"),
|
|
143537
143560
|
upsertFileState: db.prepare(
|
|
143538
143561
|
`INSERT INTO conversation_file_state
|
|
@@ -143908,6 +143931,23 @@ var ConversationCache = class _ConversationCache {
|
|
|
143908
143931
|
sessionCount: r.cnt
|
|
143909
143932
|
}));
|
|
143910
143933
|
}
|
|
143934
|
+
/** Every project with at least one cached conversation, most recently active
|
|
143935
|
+
* first. Paths are the raw `project_path` values, which is what
|
|
143936
|
+
* /api/conversations?project= matches on exactly — so a summary row is
|
|
143937
|
+
* always joinable against the page it describes. */
|
|
143938
|
+
listProjectSummaries(opts) {
|
|
143939
|
+
const total = this.stmts.projectSummaryCount.get().n;
|
|
143940
|
+
const rows = opts.limit === 0 ? [] : this.stmts.projectSummaries.all(opts.limit, opts.offset);
|
|
143941
|
+
return {
|
|
143942
|
+
total,
|
|
143943
|
+
projects: rows.map((r) => ({
|
|
143944
|
+
path: r.project_path,
|
|
143945
|
+
name: r.project_name ?? r.project_path.split(/[/\\]/).pop() ?? r.project_path,
|
|
143946
|
+
conversationCount: r.cnt,
|
|
143947
|
+
lastActivity: new Date(r.latest ?? 0).toISOString()
|
|
143948
|
+
}))
|
|
143949
|
+
};
|
|
143950
|
+
}
|
|
143911
143951
|
ensureFileIndex() {
|
|
143912
143952
|
if (this.fileIndexLoaded) return;
|
|
143913
143953
|
const rows = this.stmts.allFilePaths.all();
|
|
@@ -145879,6 +145919,27 @@ var ConversationWatcher = class {
|
|
|
145879
145919
|
* Watch a directory of conversation JSONL files. Fires
|
|
145880
145920
|
* onConversationChanged for any add/change/unlink event so the caller
|
|
145881
145921
|
* can mark the cache dirty without scanning everything immediately.
|
|
145922
|
+
*
|
|
145923
|
+
* **This costs one OS watch handle per file under `directory`, not one per
|
|
145924
|
+
* directory.** chokidar recurses the tree and registers a separate fs.watch
|
|
145925
|
+
* per entry, because a directory watch alone does not report writes to files
|
|
145926
|
+
* inside it — and per-file `change` events are exactly what the caller needs
|
|
145927
|
+
* (they drive poke()'s tail self-heal and the external-tail attach). So the
|
|
145928
|
+
* handle count tracks the size of the conversation corpus on disk, not the
|
|
145929
|
+
* number of live sessions, and it does not shrink until transcripts are
|
|
145930
|
+
* deleted. `ignoreInitial` suppresses the startup *events*, not the walk.
|
|
145931
|
+
*
|
|
145932
|
+
* Measured 2026-08-09 on the live macOS instance: 2131 open .jsonl fds
|
|
145933
|
+
* against 2133 files under the watched roots — 1:1, ~88% of all fds on the
|
|
145934
|
+
* process, at 2.0% of that box's 122 880 per-process ceiling. Comfortable
|
|
145935
|
+
* there. **Linux is the tight one**: these are inotify watches billed to the
|
|
145936
|
+
* per-user `max_user_watches`, which can be 8192 and is shared with every
|
|
145937
|
+
* other watcher the user runs. Exhaustion surfaces as ENOSPC on the `error`
|
|
145938
|
+
* event — which is why server.ts wires onError rather than leaving it unset.
|
|
145939
|
+
*
|
|
145940
|
+
* Before trading handles for a bound here, note the regression it invites: a
|
|
145941
|
+
* conversation excluded from the walk (by age or by an LRU cap) is one whose
|
|
145942
|
+
* external appends produce no event at all, and that failure is silent.
|
|
145882
145943
|
*/
|
|
145883
145944
|
watchDirectory(directory) {
|
|
145884
145945
|
if (this.directories.has(directory)) return;
|
|
@@ -148440,6 +148501,13 @@ var StreamerServer = class {
|
|
|
148440
148501
|
event: "cache.invalidate_on_unlink"
|
|
148441
148502
|
});
|
|
148442
148503
|
this.cacheMonitor?.recordUnlink(filePath);
|
|
148504
|
+
},
|
|
148505
|
+
onError: (filePath, err) => {
|
|
148506
|
+
const enospc = err.code === "ENOSPC";
|
|
148507
|
+
this.log.error(
|
|
148508
|
+
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}`,
|
|
148509
|
+
{ filePath, err, event: enospc ? "watcher.limit_exhausted" : "watcher.error" }
|
|
148510
|
+
);
|
|
148443
148511
|
}
|
|
148444
148512
|
});
|
|
148445
148513
|
this.ptyManager = new LiveSessionManager({
|
|
@@ -148633,6 +148701,7 @@ var StreamerServer = class {
|
|
|
148633
148701
|
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
148634
148702
|
handleListProjects: (url2, res) => handleListProjects(url2, res),
|
|
148635
148703
|
handleGetPopularProjects: (url2, res) => this.handleGetPopularProjects(url2, res),
|
|
148704
|
+
handleGetProjectSummaries: (url2, res) => this.handleGetProjectSummaries(url2, res),
|
|
148636
148705
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
148637
148706
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
148638
148707
|
handleBrowse: (url2, res) => this.handleBrowse(url2, res),
|
|
@@ -150198,6 +150267,20 @@ var StreamerServer = class {
|
|
|
150198
150267
|
const projects = this.cache.getPopularProjects(limit);
|
|
150199
150268
|
json2(res, 200, { projects, total: projects.length });
|
|
150200
150269
|
}
|
|
150270
|
+
handleGetProjectSummaries(url2, res) {
|
|
150271
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
150272
|
+
const limit = intParam(url2, "limit", 200);
|
|
150273
|
+
const offset = intParam(url2, "offset", 0);
|
|
150274
|
+
if (!this.cache) {
|
|
150275
|
+
json2(res, 503, {
|
|
150276
|
+
error: "Conversation cache unavailable",
|
|
150277
|
+
code: "CACHE_UNAVAILABLE"
|
|
150278
|
+
});
|
|
150279
|
+
return;
|
|
150280
|
+
}
|
|
150281
|
+
const { projects, total } = this.cache.listProjectSummaries({ limit, offset });
|
|
150282
|
+
json2(res, 200, { projects, total, offset, hasMore: offset + projects.length < total });
|
|
150283
|
+
}
|
|
150201
150284
|
buildStatCache(previousScanner) {
|
|
150202
150285
|
if (!this.cache) return void 0;
|
|
150203
150286
|
if (!previousScanner) {
|