@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.d.cts
CHANGED
|
@@ -824,6 +824,22 @@ declare class ConversationCache {
|
|
|
824
824
|
name: string;
|
|
825
825
|
sessionCount: number;
|
|
826
826
|
}>;
|
|
827
|
+
/** Every project with at least one cached conversation, most recently active
|
|
828
|
+
* first. Paths are the raw `project_path` values, which is what
|
|
829
|
+
* /api/conversations?project= matches on exactly — so a summary row is
|
|
830
|
+
* always joinable against the page it describes. */
|
|
831
|
+
listProjectSummaries(opts: {
|
|
832
|
+
limit: number;
|
|
833
|
+
offset: number;
|
|
834
|
+
}): {
|
|
835
|
+
projects: Array<{
|
|
836
|
+
path: string;
|
|
837
|
+
name: string;
|
|
838
|
+
conversationCount: number;
|
|
839
|
+
lastActivity: string;
|
|
840
|
+
}>;
|
|
841
|
+
total: number;
|
|
842
|
+
};
|
|
827
843
|
private ensureFileIndex;
|
|
828
844
|
updateFromLine(filePath: string, rawLine: string): void;
|
|
829
845
|
/**
|
|
@@ -1689,6 +1705,7 @@ type ApiDeps = {
|
|
|
1689
1705
|
handleSearchTarget: (id: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
1690
1706
|
handleListProjects: (url: URL, res: ServerResponse) => void;
|
|
1691
1707
|
handleGetPopularProjects: (url: URL, res: ServerResponse) => void;
|
|
1708
|
+
handleGetProjectSummaries: (url: URL, res: ServerResponse) => void;
|
|
1692
1709
|
handlePairStart: (res: ServerResponse) => void;
|
|
1693
1710
|
handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
1694
1711
|
handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
|
|
@@ -2119,6 +2136,7 @@ declare class StreamerServer {
|
|
|
2119
2136
|
private handleSessionsCount;
|
|
2120
2137
|
private handleGetRecentSessions;
|
|
2121
2138
|
private handleGetPopularProjects;
|
|
2139
|
+
private handleGetProjectSummaries;
|
|
2122
2140
|
private buildStatCache;
|
|
2123
2141
|
private codexScanOpts;
|
|
2124
2142
|
private newScanner;
|
|
@@ -2322,6 +2340,27 @@ declare class ConversationWatcher {
|
|
|
2322
2340
|
* Watch a directory of conversation JSONL files. Fires
|
|
2323
2341
|
* onConversationChanged for any add/change/unlink event so the caller
|
|
2324
2342
|
* can mark the cache dirty without scanning everything immediately.
|
|
2343
|
+
*
|
|
2344
|
+
* **This costs one OS watch handle per file under `directory`, not one per
|
|
2345
|
+
* directory.** chokidar recurses the tree and registers a separate fs.watch
|
|
2346
|
+
* per entry, because a directory watch alone does not report writes to files
|
|
2347
|
+
* inside it — and per-file `change` events are exactly what the caller needs
|
|
2348
|
+
* (they drive poke()'s tail self-heal and the external-tail attach). So the
|
|
2349
|
+
* handle count tracks the size of the conversation corpus on disk, not the
|
|
2350
|
+
* number of live sessions, and it does not shrink until transcripts are
|
|
2351
|
+
* deleted. `ignoreInitial` suppresses the startup *events*, not the walk.
|
|
2352
|
+
*
|
|
2353
|
+
* Measured 2026-08-09 on the live macOS instance: 2131 open .jsonl fds
|
|
2354
|
+
* against 2133 files under the watched roots — 1:1, ~88% of all fds on the
|
|
2355
|
+
* process, at 2.0% of that box's 122 880 per-process ceiling. Comfortable
|
|
2356
|
+
* there. **Linux is the tight one**: these are inotify watches billed to the
|
|
2357
|
+
* per-user `max_user_watches`, which can be 8192 and is shared with every
|
|
2358
|
+
* other watcher the user runs. Exhaustion surfaces as ENOSPC on the `error`
|
|
2359
|
+
* event — which is why server.ts wires onError rather than leaving it unset.
|
|
2360
|
+
*
|
|
2361
|
+
* Before trading handles for a bound here, note the regression it invites: a
|
|
2362
|
+
* conversation excluded from the walk (by age or by an LRU cap) is one whose
|
|
2363
|
+
* external appends produce no event at all, and that failure is silent.
|
|
2325
2364
|
*/
|
|
2326
2365
|
watchDirectory(directory: string): void;
|
|
2327
2366
|
unwatchDirectory(directory: string): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -824,6 +824,22 @@ declare class ConversationCache {
|
|
|
824
824
|
name: string;
|
|
825
825
|
sessionCount: number;
|
|
826
826
|
}>;
|
|
827
|
+
/** Every project with at least one cached conversation, most recently active
|
|
828
|
+
* first. Paths are the raw `project_path` values, which is what
|
|
829
|
+
* /api/conversations?project= matches on exactly — so a summary row is
|
|
830
|
+
* always joinable against the page it describes. */
|
|
831
|
+
listProjectSummaries(opts: {
|
|
832
|
+
limit: number;
|
|
833
|
+
offset: number;
|
|
834
|
+
}): {
|
|
835
|
+
projects: Array<{
|
|
836
|
+
path: string;
|
|
837
|
+
name: string;
|
|
838
|
+
conversationCount: number;
|
|
839
|
+
lastActivity: string;
|
|
840
|
+
}>;
|
|
841
|
+
total: number;
|
|
842
|
+
};
|
|
827
843
|
private ensureFileIndex;
|
|
828
844
|
updateFromLine(filePath: string, rawLine: string): void;
|
|
829
845
|
/**
|
|
@@ -1689,6 +1705,7 @@ type ApiDeps = {
|
|
|
1689
1705
|
handleSearchTarget: (id: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
1690
1706
|
handleListProjects: (url: URL, res: ServerResponse) => void;
|
|
1691
1707
|
handleGetPopularProjects: (url: URL, res: ServerResponse) => void;
|
|
1708
|
+
handleGetProjectSummaries: (url: URL, res: ServerResponse) => void;
|
|
1692
1709
|
handlePairStart: (res: ServerResponse) => void;
|
|
1693
1710
|
handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
1694
1711
|
handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
|
|
@@ -2119,6 +2136,7 @@ declare class StreamerServer {
|
|
|
2119
2136
|
private handleSessionsCount;
|
|
2120
2137
|
private handleGetRecentSessions;
|
|
2121
2138
|
private handleGetPopularProjects;
|
|
2139
|
+
private handleGetProjectSummaries;
|
|
2122
2140
|
private buildStatCache;
|
|
2123
2141
|
private codexScanOpts;
|
|
2124
2142
|
private newScanner;
|
|
@@ -2322,6 +2340,27 @@ declare class ConversationWatcher {
|
|
|
2322
2340
|
* Watch a directory of conversation JSONL files. Fires
|
|
2323
2341
|
* onConversationChanged for any add/change/unlink event so the caller
|
|
2324
2342
|
* can mark the cache dirty without scanning everything immediately.
|
|
2343
|
+
*
|
|
2344
|
+
* **This costs one OS watch handle per file under `directory`, not one per
|
|
2345
|
+
* directory.** chokidar recurses the tree and registers a separate fs.watch
|
|
2346
|
+
* per entry, because a directory watch alone does not report writes to files
|
|
2347
|
+
* inside it — and per-file `change` events are exactly what the caller needs
|
|
2348
|
+
* (they drive poke()'s tail self-heal and the external-tail attach). So the
|
|
2349
|
+
* handle count tracks the size of the conversation corpus on disk, not the
|
|
2350
|
+
* number of live sessions, and it does not shrink until transcripts are
|
|
2351
|
+
* deleted. `ignoreInitial` suppresses the startup *events*, not the walk.
|
|
2352
|
+
*
|
|
2353
|
+
* Measured 2026-08-09 on the live macOS instance: 2131 open .jsonl fds
|
|
2354
|
+
* against 2133 files under the watched roots — 1:1, ~88% of all fds on the
|
|
2355
|
+
* process, at 2.0% of that box's 122 880 per-process ceiling. Comfortable
|
|
2356
|
+
* there. **Linux is the tight one**: these are inotify watches billed to the
|
|
2357
|
+
* per-user `max_user_watches`, which can be 8192 and is shared with every
|
|
2358
|
+
* other watcher the user runs. Exhaustion surfaces as ENOSPC on the `error`
|
|
2359
|
+
* event — which is why server.ts wires onError rather than leaving it unset.
|
|
2360
|
+
*
|
|
2361
|
+
* Before trading handles for a bound here, note the regression it invites: a
|
|
2362
|
+
* conversation excluded from the walk (by age or by an LRU cap) is one whose
|
|
2363
|
+
* external appends produce no event at all, and that failure is silent.
|
|
2325
2364
|
*/
|
|
2326
2365
|
watchDirectory(directory: string): void;
|
|
2327
2366
|
unwatchDirectory(directory: string): void;
|
package/dist/index.js
CHANGED
|
@@ -5306,7 +5306,10 @@ var createMiscRoutes = (deps) => {
|
|
|
5306
5306
|
// Same contract: this server serves GET /api/config/feature-flags. Lives
|
|
5307
5307
|
// here rather than behind /api/config (admin-only) so a read-only client
|
|
5308
5308
|
// still learns the server supports flags even if it can't read values.
|
|
5309
|
-
featureFlags: true
|
|
5309
|
+
featureFlags: true,
|
|
5310
|
+
// Same contract: this server serves GET /api/projects/summary, which the
|
|
5311
|
+
// Hub's grouped views need before they can draw a tree.
|
|
5312
|
+
projectSummary: true
|
|
5310
5313
|
});
|
|
5311
5314
|
});
|
|
5312
5315
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -5453,6 +5456,11 @@ var createProjectRoutes = (deps) => {
|
|
|
5453
5456
|
deps.handleGetPopularProjects(url, c.env.outgoing);
|
|
5454
5457
|
return alreadyHandled4();
|
|
5455
5458
|
});
|
|
5459
|
+
app.get("/summary", (c) => {
|
|
5460
|
+
const url = new URL(c.req.url);
|
|
5461
|
+
deps.handleGetProjectSummaries(url, c.env.outgoing);
|
|
5462
|
+
return alreadyHandled4();
|
|
5463
|
+
});
|
|
5456
5464
|
return app;
|
|
5457
5465
|
};
|
|
5458
5466
|
|
|
@@ -6320,6 +6328,21 @@ var ConversationCache = class _ConversationCache {
|
|
|
6320
6328
|
ORDER BY cnt DESC
|
|
6321
6329
|
LIMIT ?`
|
|
6322
6330
|
),
|
|
6331
|
+
// Same table, same rows and same NULL filter /api/conversations lists
|
|
6332
|
+
// from, so a group's count/last-activity can never disagree with the
|
|
6333
|
+
// page it opens. Bare project_name is the one from the MAX(last_activity)
|
|
6334
|
+
// row (SQLite's documented min/max-aggregate bare-column rule).
|
|
6335
|
+
projectSummaries: db.prepare(
|
|
6336
|
+
`SELECT project_path, project_name, COUNT(*) as cnt, MAX(last_activity) as latest
|
|
6337
|
+
FROM conversation_meta
|
|
6338
|
+
WHERE project_path IS NOT NULL
|
|
6339
|
+
GROUP BY project_path
|
|
6340
|
+
ORDER BY latest DESC, project_path ASC
|
|
6341
|
+
LIMIT ? OFFSET ?`
|
|
6342
|
+
),
|
|
6343
|
+
projectSummaryCount: db.prepare(
|
|
6344
|
+
"SELECT COUNT(DISTINCT project_path) as n FROM conversation_meta WHERE project_path IS NOT NULL"
|
|
6345
|
+
),
|
|
6323
6346
|
getFileState: db.prepare("SELECT * FROM conversation_file_state WHERE path = ?"),
|
|
6324
6347
|
upsertFileState: db.prepare(
|
|
6325
6348
|
`INSERT INTO conversation_file_state
|
|
@@ -6695,6 +6718,23 @@ var ConversationCache = class _ConversationCache {
|
|
|
6695
6718
|
sessionCount: r.cnt
|
|
6696
6719
|
}));
|
|
6697
6720
|
}
|
|
6721
|
+
/** Every project with at least one cached conversation, most recently active
|
|
6722
|
+
* first. Paths are the raw `project_path` values, which is what
|
|
6723
|
+
* /api/conversations?project= matches on exactly — so a summary row is
|
|
6724
|
+
* always joinable against the page it describes. */
|
|
6725
|
+
listProjectSummaries(opts) {
|
|
6726
|
+
const total = this.stmts.projectSummaryCount.get().n;
|
|
6727
|
+
const rows = opts.limit === 0 ? [] : this.stmts.projectSummaries.all(opts.limit, opts.offset);
|
|
6728
|
+
return {
|
|
6729
|
+
total,
|
|
6730
|
+
projects: rows.map((r) => ({
|
|
6731
|
+
path: r.project_path,
|
|
6732
|
+
name: r.project_name ?? r.project_path.split(/[/\\]/).pop() ?? r.project_path,
|
|
6733
|
+
conversationCount: r.cnt,
|
|
6734
|
+
lastActivity: new Date(r.latest ?? 0).toISOString()
|
|
6735
|
+
}))
|
|
6736
|
+
};
|
|
6737
|
+
}
|
|
6698
6738
|
ensureFileIndex() {
|
|
6699
6739
|
if (this.fileIndexLoaded) return;
|
|
6700
6740
|
const rows = this.stmts.allFilePaths.all();
|
|
@@ -8347,6 +8387,27 @@ var ConversationWatcher = class {
|
|
|
8347
8387
|
* Watch a directory of conversation JSONL files. Fires
|
|
8348
8388
|
* onConversationChanged for any add/change/unlink event so the caller
|
|
8349
8389
|
* can mark the cache dirty without scanning everything immediately.
|
|
8390
|
+
*
|
|
8391
|
+
* **This costs one OS watch handle per file under `directory`, not one per
|
|
8392
|
+
* directory.** chokidar recurses the tree and registers a separate fs.watch
|
|
8393
|
+
* per entry, because a directory watch alone does not report writes to files
|
|
8394
|
+
* inside it — and per-file `change` events are exactly what the caller needs
|
|
8395
|
+
* (they drive poke()'s tail self-heal and the external-tail attach). So the
|
|
8396
|
+
* handle count tracks the size of the conversation corpus on disk, not the
|
|
8397
|
+
* number of live sessions, and it does not shrink until transcripts are
|
|
8398
|
+
* deleted. `ignoreInitial` suppresses the startup *events*, not the walk.
|
|
8399
|
+
*
|
|
8400
|
+
* Measured 2026-08-09 on the live macOS instance: 2131 open .jsonl fds
|
|
8401
|
+
* against 2133 files under the watched roots — 1:1, ~88% of all fds on the
|
|
8402
|
+
* process, at 2.0% of that box's 122 880 per-process ceiling. Comfortable
|
|
8403
|
+
* there. **Linux is the tight one**: these are inotify watches billed to the
|
|
8404
|
+
* per-user `max_user_watches`, which can be 8192 and is shared with every
|
|
8405
|
+
* other watcher the user runs. Exhaustion surfaces as ENOSPC on the `error`
|
|
8406
|
+
* event — which is why server.ts wires onError rather than leaving it unset.
|
|
8407
|
+
*
|
|
8408
|
+
* Before trading handles for a bound here, note the regression it invites: a
|
|
8409
|
+
* conversation excluded from the walk (by age or by an LRU cap) is one whose
|
|
8410
|
+
* external appends produce no event at all, and that failure is silent.
|
|
8350
8411
|
*/
|
|
8351
8412
|
watchDirectory(directory) {
|
|
8352
8413
|
if (this.directories.has(directory)) return;
|
|
@@ -10643,6 +10704,13 @@ var StreamerServer = class {
|
|
|
10643
10704
|
event: "cache.invalidate_on_unlink"
|
|
10644
10705
|
});
|
|
10645
10706
|
this.cacheMonitor?.recordUnlink(filePath);
|
|
10707
|
+
},
|
|
10708
|
+
onError: (filePath, err) => {
|
|
10709
|
+
const enospc = err.code === "ENOSPC";
|
|
10710
|
+
this.log.error(
|
|
10711
|
+
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}`,
|
|
10712
|
+
{ filePath, err, event: enospc ? "watcher.limit_exhausted" : "watcher.error" }
|
|
10713
|
+
);
|
|
10646
10714
|
}
|
|
10647
10715
|
});
|
|
10648
10716
|
this.ptyManager = new LiveSessionManager({
|
|
@@ -10836,6 +10904,7 @@ var StreamerServer = class {
|
|
|
10836
10904
|
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
10837
10905
|
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
10838
10906
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
10907
|
+
handleGetProjectSummaries: (url, res) => this.handleGetProjectSummaries(url, res),
|
|
10839
10908
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
10840
10909
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
10841
10910
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
@@ -12401,6 +12470,20 @@ var StreamerServer = class {
|
|
|
12401
12470
|
const projects = this.cache.getPopularProjects(limit);
|
|
12402
12471
|
json(res, 200, { projects, total: projects.length });
|
|
12403
12472
|
}
|
|
12473
|
+
handleGetProjectSummaries(url, res) {
|
|
12474
|
+
if (this.rejectIfWarmingUp(res)) return;
|
|
12475
|
+
const limit = intParam(url, "limit", 200);
|
|
12476
|
+
const offset = intParam(url, "offset", 0);
|
|
12477
|
+
if (!this.cache) {
|
|
12478
|
+
json(res, 503, {
|
|
12479
|
+
error: "Conversation cache unavailable",
|
|
12480
|
+
code: "CACHE_UNAVAILABLE"
|
|
12481
|
+
});
|
|
12482
|
+
return;
|
|
12483
|
+
}
|
|
12484
|
+
const { projects, total } = this.cache.listProjectSummaries({ limit, offset });
|
|
12485
|
+
json(res, 200, { projects, total, offset, hasMore: offset + projects.length < total });
|
|
12486
|
+
}
|
|
12404
12487
|
buildStatCache(previousScanner) {
|
|
12405
12488
|
if (!this.cache) return void 0;
|
|
12406
12489
|
if (!previousScanner) {
|