@threadbase-sh/streamer 1.52.4 → 1.52.6
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 +786 -472
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +260 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -6
- package/dist/index.d.ts +66 -6
- package/dist/index.js +257 -64
- package/dist/index.js.map +1 -1
- package/dist/migrations/011_create_devices.sql +15 -0
- package/dist/runtime-migrations/003_create_devices.sql +52 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -607,7 +607,8 @@ function loadOrCreateApiKey() {
|
|
|
607
607
|
const key = generateApiKey();
|
|
608
608
|
(0, import_fs.mkdirSync)(configDir(), { recursive: true });
|
|
609
609
|
(0, import_fs.writeFileSync)(configFile(), `api_key: ${key}
|
|
610
|
-
`, "utf-8");
|
|
610
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
611
|
+
(0, import_fs.chmodSync)(configFile(), 384);
|
|
611
612
|
return key;
|
|
612
613
|
}
|
|
613
614
|
function loadBrowseRoot() {
|
|
@@ -3916,8 +3917,8 @@ var import_events = require("events");
|
|
|
3916
3917
|
var import_fs27 = require("fs");
|
|
3917
3918
|
var import_promises7 = require("fs/promises");
|
|
3918
3919
|
var import_http = require("http");
|
|
3919
|
-
var
|
|
3920
|
-
var
|
|
3920
|
+
var import_os13 = require("os");
|
|
3921
|
+
var import_path23 = require("path");
|
|
3921
3922
|
|
|
3922
3923
|
// src/api/app.ts
|
|
3923
3924
|
var import_hono18 = require("hono");
|
|
@@ -4053,6 +4054,8 @@ var DevicesRepository = class {
|
|
|
4053
4054
|
listStmt;
|
|
4054
4055
|
revokeStmt;
|
|
4055
4056
|
touchStmt;
|
|
4057
|
+
deleteStmt;
|
|
4058
|
+
deleteRevokedStmt;
|
|
4056
4059
|
constructor(db) {
|
|
4057
4060
|
this.insertStmt = db.prepare(`
|
|
4058
4061
|
INSERT INTO devices (
|
|
@@ -4066,6 +4069,8 @@ var DevicesRepository = class {
|
|
|
4066
4069
|
this.listStmt = db.prepare("SELECT * FROM devices ORDER BY created_at DESC");
|
|
4067
4070
|
this.revokeStmt = db.prepare("UPDATE devices SET revoked_at = ? WHERE device_id = ?");
|
|
4068
4071
|
this.touchStmt = db.prepare("UPDATE devices SET last_seen_at = ? WHERE device_id = ?");
|
|
4072
|
+
this.deleteStmt = db.prepare("DELETE FROM devices WHERE device_id = ?");
|
|
4073
|
+
this.deleteRevokedStmt = db.prepare("DELETE FROM devices WHERE revoked_at IS NOT NULL");
|
|
4069
4074
|
}
|
|
4070
4075
|
/**
|
|
4071
4076
|
* Record a newly paired device and mint its token.
|
|
@@ -4112,6 +4117,33 @@ var DevicesRepository = class {
|
|
|
4112
4117
|
revoke(deviceId, now = Date.now()) {
|
|
4113
4118
|
return this.revokeStmt.run(now, deviceId).changes > 0;
|
|
4114
4119
|
}
|
|
4120
|
+
/**
|
|
4121
|
+
* Erase one device's record outright.
|
|
4122
|
+
*
|
|
4123
|
+
* Deliberately separate from `revoke`, which is a soft delete that keeps the
|
|
4124
|
+
* row so `list()` can show what happened. That audit trail is the right
|
|
4125
|
+
* default — but it meant a `devices` row, including the user-supplied `name`
|
|
4126
|
+
* ("Ronen's iPhone"), had no removal path at all once the registry moved to
|
|
4127
|
+
* runtime.db, which no command deletes. This is that path.
|
|
4128
|
+
*
|
|
4129
|
+
* Erasure is NOT revocation: deleting a row frees its `token_hash`, so a
|
|
4130
|
+
* device whose token is still on a phone somewhere stops being *known* rather
|
|
4131
|
+
* than being *refused*. Revoke first, delete second, is the safe order, and
|
|
4132
|
+
* `deleteRevoked()` exists so that is the easy thing to do.
|
|
4133
|
+
*/
|
|
4134
|
+
delete(deviceId) {
|
|
4135
|
+
return this.deleteStmt.run(deviceId).changes > 0;
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Erase every already-revoked device. The bulk companion to `delete`, and the
|
|
4139
|
+
* one that is safe by construction: a revoked device is already refused, so
|
|
4140
|
+
* removing its row cannot restore access to anything.
|
|
4141
|
+
*
|
|
4142
|
+
* Returns the number of rows removed.
|
|
4143
|
+
*/
|
|
4144
|
+
deleteRevoked() {
|
|
4145
|
+
return this.deleteRevokedStmt.run().changes;
|
|
4146
|
+
}
|
|
4115
4147
|
touch(deviceId, now = Date.now()) {
|
|
4116
4148
|
this.touchStmt.run(now, deviceId);
|
|
4117
4149
|
}
|
|
@@ -4638,6 +4670,34 @@ var createDeviceRoutes = (deps) => {
|
|
|
4638
4670
|
repo.revoke(id);
|
|
4639
4671
|
return c.json({ ok: true, alreadyRevoked: false });
|
|
4640
4672
|
});
|
|
4673
|
+
app.delete("/:id", (c) => {
|
|
4674
|
+
const repo = deps.devicesRepo();
|
|
4675
|
+
if (!repo) {
|
|
4676
|
+
return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
4677
|
+
}
|
|
4678
|
+
const id = c.req.param("id");
|
|
4679
|
+
const existing = repo.get(id);
|
|
4680
|
+
if (!existing) return c.json({ ok: true, alreadyDeleted: true });
|
|
4681
|
+
const force = c.req.query("force") === "1" || c.req.query("force") === "true";
|
|
4682
|
+
if (existing.revoked_at == null && !force) {
|
|
4683
|
+
return c.json(
|
|
4684
|
+
{
|
|
4685
|
+
error: "Revoke the device before deleting it, or pass ?force=1",
|
|
4686
|
+
code: "DEVICE_ACTIVE"
|
|
4687
|
+
},
|
|
4688
|
+
409
|
|
4689
|
+
);
|
|
4690
|
+
}
|
|
4691
|
+
repo.delete(id);
|
|
4692
|
+
return c.json({ ok: true, alreadyDeleted: false });
|
|
4693
|
+
});
|
|
4694
|
+
app.delete("/", (c) => {
|
|
4695
|
+
const repo = deps.devicesRepo();
|
|
4696
|
+
if (!repo) {
|
|
4697
|
+
return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
4698
|
+
}
|
|
4699
|
+
return c.json({ ok: true, deleted: repo.deleteRevoked() });
|
|
4700
|
+
});
|
|
4641
4701
|
return app;
|
|
4642
4702
|
};
|
|
4643
4703
|
|
|
@@ -5554,6 +5614,13 @@ var createMiscRoutes = (deps) => {
|
|
|
5554
5614
|
// Same contract: this server serves GET /api/projects/summary, which the
|
|
5555
5615
|
// Hub's grouped views need before they can draw a tree.
|
|
5556
5616
|
projectSummary: true,
|
|
5617
|
+
// The paired-device registry lives in runtime.db, so it survives
|
|
5618
|
+
// `tb-streamer cache clear` and the integrity monitor's reset-and-rescan.
|
|
5619
|
+
// A client may only prefer its scoped device token over the shared API
|
|
5620
|
+
// key when this is true: on an older server the registry is inside
|
|
5621
|
+
// cache.db, where a documented troubleshooting step deletes it and every
|
|
5622
|
+
// device token with it. Absent means "old server, assume not durable".
|
|
5623
|
+
devicesDurable: true,
|
|
5557
5624
|
// Delivery capability, not endpoint support: whether this server can
|
|
5558
5625
|
// actually send a push, so mobile can hide an affordance instead of
|
|
5559
5626
|
// registering tokens nothing will ever send to. Absent on older servers,
|
|
@@ -5964,7 +6031,13 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
5964
6031
|
const app = new import_hono17.Hono();
|
|
5965
6032
|
app.get(
|
|
5966
6033
|
"/ws",
|
|
5967
|
-
|
|
6034
|
+
// The principal is read here, at the upgrade, and captured for the life of
|
|
6035
|
+
// the socket. authMiddleware sets it because /ws is classified
|
|
6036
|
+
// `history:read`, but it only ever reaches the HTTP request — without
|
|
6037
|
+
// capturing it the socket has no principal at all, so every frame after
|
|
6038
|
+
// the upgrade is unauthorized-by-omission.
|
|
6039
|
+
upgradeWebSocket((c) => {
|
|
6040
|
+
const principal = c.get("principal") ?? null;
|
|
5968
6041
|
let openWs = null;
|
|
5969
6042
|
return {
|
|
5970
6043
|
onOpen(_evt, ws) {
|
|
@@ -5974,7 +6047,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
5974
6047
|
deps.handleWsOpen(raw);
|
|
5975
6048
|
},
|
|
5976
6049
|
onMessage(evt, _ws) {
|
|
5977
|
-
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
6050
|
+
if (openWs) deps.handleWsMessage(openWs, evt.data, principal);
|
|
5978
6051
|
},
|
|
5979
6052
|
onClose(_evt, _ws) {
|
|
5980
6053
|
if (openWs) deps.handleWsClose(openWs);
|
|
@@ -10696,6 +10769,11 @@ var SessionsRepository = class {
|
|
|
10696
10769
|
|
|
10697
10770
|
// src/db/runtime-store.ts
|
|
10698
10771
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
10772
|
+
var import_os7 = require("os");
|
|
10773
|
+
var import_path16 = require("path");
|
|
10774
|
+
function resolveRuntimeDbPath(override) {
|
|
10775
|
+
return override ?? process.env.THREADBASE_RUNTIME_DB ?? (0, import_path16.join)(process.env.THREADBASE_CONFIG_DIR ?? (0, import_path16.join)((0, import_os7.homedir)(), ".threadbase"), "runtime.db");
|
|
10776
|
+
}
|
|
10699
10777
|
var RuntimeStore = class _RuntimeStore {
|
|
10700
10778
|
constructor(db) {
|
|
10701
10779
|
this.db = db;
|
|
@@ -10713,23 +10791,64 @@ var RuntimeStore = class _RuntimeStore {
|
|
|
10713
10791
|
/**
|
|
10714
10792
|
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
10715
10793
|
*
|
|
10716
|
-
* Non-destructive by design: the source table is left in place so an older
|
|
10717
|
-
* streamer rolled back onto the same machine still finds its registry. Runs
|
|
10718
|
-
* only when this file's table is empty, so a second boot is a no-op rather
|
|
10719
|
-
* than a re-copy that would resurrect rows deleted since.
|
|
10720
|
-
*
|
|
10721
10794
|
* Returns the number of rows copied.
|
|
10722
10795
|
*/
|
|
10723
10796
|
importLegacyManagedSessions(source) {
|
|
10724
|
-
|
|
10797
|
+
return this.importLegacyTable(source, "managed_sessions");
|
|
10798
|
+
}
|
|
10799
|
+
/**
|
|
10800
|
+
* One-time move of `devices` rows out of `cache.db`, where the registry used
|
|
10801
|
+
* to live (migration `011_create_devices.sql`).
|
|
10802
|
+
*
|
|
10803
|
+
* Losing this table invalidates every device token ever issued, and cache.db
|
|
10804
|
+
* is the file `tb-streamer cache clear` deletes and the integrity monitor
|
|
10805
|
+
* rebuilds — see `runtime-migrations/003_create_devices.sql`.
|
|
10806
|
+
*
|
|
10807
|
+
* Unlike `managed_sessions`, this one MOVES rather than copies: the source
|
|
10808
|
+
* rows are deleted once the copy is verified. A `devices` row carries a
|
|
10809
|
+
* user-supplied label ("Ronen's iPhone"), and leaving a second copy of that
|
|
10810
|
+
* on disk indefinitely — in the one file the user is told to delete when
|
|
10811
|
+
* something goes wrong — is more retained personal data than the rollback
|
|
10812
|
+
* path is worth. Recovering from a rollback is re-scanning a pairing QR.
|
|
10813
|
+
*
|
|
10814
|
+
* The delete is conditional on the copy being complete: `INSERT OR IGNORE`
|
|
10815
|
+
* can silently skip a row, so the destination count must match what was read
|
|
10816
|
+
* before anything is removed. A mismatch keeps the source and reports
|
|
10817
|
+
* `purged: false` rather than throwing — the import itself still succeeded,
|
|
10818
|
+
* and keeping data is the safe direction to fail in.
|
|
10819
|
+
*/
|
|
10820
|
+
importLegacyDevices(source) {
|
|
10821
|
+
const copied = this.importLegacyTable(source, "devices");
|
|
10822
|
+
if (copied === 0) return { copied: 0, purged: false };
|
|
10823
|
+
const landed = this.db.prepare("SELECT COUNT(*) AS n FROM devices").get().n;
|
|
10824
|
+
if (landed !== copied) return { copied, purged: false };
|
|
10825
|
+
source.prepare("DELETE FROM devices").run();
|
|
10826
|
+
return { copied, purged: true };
|
|
10827
|
+
}
|
|
10828
|
+
/**
|
|
10829
|
+
* Copy a whole table out of a pre-split `cache.db` into this file.
|
|
10830
|
+
*
|
|
10831
|
+
* The copy itself is non-destructive — the source is left in place, so an
|
|
10832
|
+
* older streamer rolled back onto the same machine still finds its data.
|
|
10833
|
+
* `importLegacyDevices` deletes the source afterwards for its own reasons;
|
|
10834
|
+
* `managed_sessions` does not. Runs only when this file's table is empty, so
|
|
10835
|
+
* a second boot is a no-op rather than a re-copy that would resurrect rows
|
|
10836
|
+
* deleted since.
|
|
10837
|
+
*
|
|
10838
|
+
* The table name is interpolated into SQL, so it is typed as a closed union
|
|
10839
|
+
* rather than `string` — the set of tables that can ever be lifted is known
|
|
10840
|
+
* at compile time, and that is what keeps a caller from making this a hole.
|
|
10841
|
+
*/
|
|
10842
|
+
importLegacyTable(source, table) {
|
|
10843
|
+
const existing = this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
10725
10844
|
if (existing.n > 0) return 0;
|
|
10726
|
-
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name =
|
|
10845
|
+
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
10727
10846
|
if (!hasTable) return 0;
|
|
10728
|
-
const rows = source.prepare(
|
|
10847
|
+
const rows = source.prepare(`SELECT * FROM ${table}`).all();
|
|
10729
10848
|
if (rows.length === 0) return 0;
|
|
10730
10849
|
const columns = Object.keys(rows[0]);
|
|
10731
10850
|
const insert = this.db.prepare(
|
|
10732
|
-
`INSERT OR IGNORE INTO
|
|
10851
|
+
`INSERT OR IGNORE INTO ${table} (${columns.join(", ")})
|
|
10733
10852
|
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
10734
10853
|
);
|
|
10735
10854
|
this.db.transaction((batch) => {
|
|
@@ -11051,8 +11170,8 @@ function spawnDetachedHost(socketPath, entryPoint) {
|
|
|
11051
11170
|
// src/scanner-manager.ts
|
|
11052
11171
|
var import_scanner4 = require("@threadbase-sh/scanner");
|
|
11053
11172
|
var import_fs17 = require("fs");
|
|
11054
|
-
var
|
|
11055
|
-
var
|
|
11173
|
+
var import_os9 = require("os");
|
|
11174
|
+
var import_path18 = require("path");
|
|
11056
11175
|
|
|
11057
11176
|
// src/services/cache/cacheMetadata.ts
|
|
11058
11177
|
function getCacheMetadata(repo, key) {
|
|
@@ -11152,9 +11271,9 @@ function refreshConversationCache(deps) {
|
|
|
11152
11271
|
|
|
11153
11272
|
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
11154
11273
|
var import_fs16 = require("fs");
|
|
11155
|
-
var
|
|
11156
|
-
var
|
|
11157
|
-
var DEFAULT_PROJECTS_DIR = (0,
|
|
11274
|
+
var import_os8 = require("os");
|
|
11275
|
+
var import_path17 = require("path");
|
|
11276
|
+
var DEFAULT_PROJECTS_DIR = (0, import_path17.join)((0, import_os8.homedir)(), ".claude", "projects");
|
|
11158
11277
|
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
11159
11278
|
let maxMs;
|
|
11160
11279
|
try {
|
|
@@ -11166,7 +11285,7 @@ function maxProjectsTreeMtimeMs(projectsDir) {
|
|
|
11166
11285
|
for (const ent of (0, import_fs16.readdirSync)(projectsDir, { withFileTypes: true })) {
|
|
11167
11286
|
if (!ent.isDirectory()) continue;
|
|
11168
11287
|
try {
|
|
11169
|
-
const childMs = (0, import_fs16.statSync)((0,
|
|
11288
|
+
const childMs = (0, import_fs16.statSync)((0, import_path17.join)(projectsDir, ent.name)).mtimeMs;
|
|
11170
11289
|
if (childMs > maxMs) maxMs = childMs;
|
|
11171
11290
|
} catch {
|
|
11172
11291
|
}
|
|
@@ -11338,9 +11457,9 @@ var ScannerManager = class {
|
|
|
11338
11457
|
projectsDirs() {
|
|
11339
11458
|
const profiles = this.deps.scanProfiles;
|
|
11340
11459
|
if (profiles && profiles.length > 0) {
|
|
11341
|
-
return profiles.filter((p) => p.enabled).map((p) => (0,
|
|
11460
|
+
return profiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
|
|
11342
11461
|
}
|
|
11343
|
-
return [(0,
|
|
11462
|
+
return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
|
|
11344
11463
|
}
|
|
11345
11464
|
// ─── staleness ────────────────────────────────────────────────────
|
|
11346
11465
|
// Drain the stale set and disarm the flag together. The caller owns the
|
|
@@ -11621,8 +11740,8 @@ var import_fs19 = require("fs");
|
|
|
11621
11740
|
|
|
11622
11741
|
// src/handlers/handleListProjects.ts
|
|
11623
11742
|
var import_fs18 = require("fs");
|
|
11624
|
-
var
|
|
11625
|
-
var
|
|
11743
|
+
var import_os10 = require("os");
|
|
11744
|
+
var import_path19 = require("path");
|
|
11626
11745
|
var HEAD_BYTES = 64 * 1024;
|
|
11627
11746
|
var MAX_FILES_PROBED = 3;
|
|
11628
11747
|
function readRecordedCwd(dir) {
|
|
@@ -11635,7 +11754,7 @@ function readRecordedCwd(dir) {
|
|
|
11635
11754
|
for (const file of files.slice(0, MAX_FILES_PROBED)) {
|
|
11636
11755
|
let fd;
|
|
11637
11756
|
try {
|
|
11638
|
-
fd = (0, import_fs18.openSync)((0,
|
|
11757
|
+
fd = (0, import_fs18.openSync)((0, import_path19.join)(dir, file), "r");
|
|
11639
11758
|
const buf = Buffer.alloc(HEAD_BYTES);
|
|
11640
11759
|
const bytes = (0, import_fs18.readSync)(fd, buf, 0, HEAD_BYTES, 0);
|
|
11641
11760
|
for (const line of buf.subarray(0, bytes).toString("utf8").split("\n")) {
|
|
@@ -11659,11 +11778,11 @@ function decodeProjectPath(dirName) {
|
|
|
11659
11778
|
function handleListProjects(url, res) {
|
|
11660
11779
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
11661
11780
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
11662
|
-
const projectsDir = (0,
|
|
11781
|
+
const projectsDir = (0, import_path19.join)((0, import_os10.homedir)(), ".claude", "projects");
|
|
11663
11782
|
let entries;
|
|
11664
11783
|
try {
|
|
11665
11784
|
entries = (0, import_fs18.readdirSync)(projectsDir).map((dirName) => {
|
|
11666
|
-
const fullPath = (0,
|
|
11785
|
+
const fullPath = (0, import_path19.join)(projectsDir, dirName);
|
|
11667
11786
|
let mtime = 0;
|
|
11668
11787
|
try {
|
|
11669
11788
|
mtime = (0, import_fs18.statSync)(fullPath).mtimeMs;
|
|
@@ -11678,7 +11797,7 @@ function handleListProjects(url, res) {
|
|
|
11678
11797
|
}
|
|
11679
11798
|
const total = entries.length;
|
|
11680
11799
|
const page = entries.slice(offset, offset + limit).map(({ dirName }) => {
|
|
11681
|
-
const path = readRecordedCwd((0,
|
|
11800
|
+
const path = readRecordedCwd((0, import_path19.join)(projectsDir, dirName)) ?? decodeProjectPath(String(dirName));
|
|
11682
11801
|
const name = path.split(/[\\/]/).filter(Boolean).pop() ?? dirName;
|
|
11683
11802
|
return { name, path, dirName };
|
|
11684
11803
|
});
|
|
@@ -11687,6 +11806,9 @@ function handleListProjects(url, res) {
|
|
|
11687
11806
|
}
|
|
11688
11807
|
|
|
11689
11808
|
// src/server-wiring.ts
|
|
11809
|
+
function wsAllows(principal, required) {
|
|
11810
|
+
return principal === null || hasCapability(principal, required);
|
|
11811
|
+
}
|
|
11690
11812
|
function createConversationWatcherEvents(deps) {
|
|
11691
11813
|
return {
|
|
11692
11814
|
onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
|
|
@@ -11968,7 +12090,15 @@ function createApiDeps(deps) {
|
|
|
11968
12090
|
const alertMsg = deps.cacheMonitor()?.wsMessage();
|
|
11969
12091
|
if (alertMsg) deps.wsHub.unicast(ws, alertMsg);
|
|
11970
12092
|
},
|
|
11971
|
-
handleWsMessage: async (ws, raw) => {
|
|
12093
|
+
handleWsMessage: async (ws, raw, principal) => {
|
|
12094
|
+
const deny = (type, required) => {
|
|
12095
|
+
deps.log().warn(`[ws.capability_denied] ${type} requires ${required}`, {
|
|
12096
|
+
event: "ws.capability_denied",
|
|
12097
|
+
type,
|
|
12098
|
+
required,
|
|
12099
|
+
...principal?.deviceId ? { deviceId: principal.deviceId } : {}
|
|
12100
|
+
});
|
|
12101
|
+
};
|
|
11972
12102
|
try {
|
|
11973
12103
|
const msg = JSON.parse(String(raw));
|
|
11974
12104
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
@@ -11978,6 +12108,10 @@ function createApiDeps(deps) {
|
|
|
11978
12108
|
deps.wsToClientId.set(ws, msg.clientId);
|
|
11979
12109
|
}
|
|
11980
12110
|
if (msg.type === "subscribe_session" && typeof msg.sessionId === "string") {
|
|
12111
|
+
if (!wsAllows(principal, "history:read")) {
|
|
12112
|
+
deny(msg.type, "history:read");
|
|
12113
|
+
return;
|
|
12114
|
+
}
|
|
11981
12115
|
deps.addSessionSubscriber(msg.sessionId, ws);
|
|
11982
12116
|
if (deps.ptyManager.hasSession(msg.sessionId)) {
|
|
11983
12117
|
const lines = await deps.ptyManager.getOutputLines(msg.sessionId, 200);
|
|
@@ -12026,6 +12160,10 @@ function createApiDeps(deps) {
|
|
|
12026
12160
|
}
|
|
12027
12161
|
}
|
|
12028
12162
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
12163
|
+
if (!wsAllows(principal, "session:control")) {
|
|
12164
|
+
deny(msg.type, "session:control");
|
|
12165
|
+
return;
|
|
12166
|
+
}
|
|
12029
12167
|
deps.startGraceTimer(msg.sessionId, deps.ptyGracePeriodMs);
|
|
12030
12168
|
}
|
|
12031
12169
|
} catch {
|
|
@@ -12053,11 +12191,11 @@ var import_fs22 = require("fs");
|
|
|
12053
12191
|
|
|
12054
12192
|
// src/services/cache-integrity/alertStore.ts
|
|
12055
12193
|
var import_fs20 = require("fs");
|
|
12056
|
-
var
|
|
12057
|
-
var
|
|
12194
|
+
var import_os11 = require("os");
|
|
12195
|
+
var import_path20 = require("path");
|
|
12058
12196
|
function alertStatePath() {
|
|
12059
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0,
|
|
12060
|
-
return (0,
|
|
12197
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path20.join)((0, import_os11.homedir)(), ".threadbase");
|
|
12198
|
+
return (0, import_path20.join)(dir, "cache-alert.json");
|
|
12061
12199
|
}
|
|
12062
12200
|
function loadAlertState() {
|
|
12063
12201
|
try {
|
|
@@ -12069,14 +12207,14 @@ function loadAlertState() {
|
|
|
12069
12207
|
}
|
|
12070
12208
|
function saveAlertState(state) {
|
|
12071
12209
|
const path = alertStatePath();
|
|
12072
|
-
(0, import_fs20.mkdirSync)((0,
|
|
12210
|
+
(0, import_fs20.mkdirSync)((0, import_path20.dirname)(path), { recursive: true });
|
|
12073
12211
|
(0, import_fs20.writeFileSync)(path, `${JSON.stringify(state, null, 2)}
|
|
12074
12212
|
`);
|
|
12075
12213
|
}
|
|
12076
12214
|
|
|
12077
12215
|
// src/services/cache-integrity/backup.ts
|
|
12078
12216
|
var import_fs21 = require("fs");
|
|
12079
|
-
var
|
|
12217
|
+
var import_path21 = require("path");
|
|
12080
12218
|
var DEFAULT_RETAIN = 3;
|
|
12081
12219
|
function retainCount() {
|
|
12082
12220
|
const parsed = Number.parseInt(process.env.THREADBASE_CACHE_BACKUP_RETAIN ?? "", 10);
|
|
@@ -12087,13 +12225,13 @@ function timestamp(d) {
|
|
|
12087
12225
|
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
12088
12226
|
}
|
|
12089
12227
|
async function backupCacheDb(db, cacheDir) {
|
|
12090
|
-
const backupsDir = (0,
|
|
12228
|
+
const backupsDir = (0, import_path21.join)(cacheDir, "backups");
|
|
12091
12229
|
(0, import_fs21.mkdirSync)(backupsDir, { recursive: true });
|
|
12092
|
-
const destPath = (0,
|
|
12230
|
+
const destPath = (0, import_path21.join)(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
|
|
12093
12231
|
await db.backup(destPath);
|
|
12094
12232
|
const retain = retainCount();
|
|
12095
12233
|
const backups = (0, import_fs21.readdirSync)(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
|
|
12096
|
-
const full = (0,
|
|
12234
|
+
const full = (0, import_path21.join)(backupsDir, f);
|
|
12097
12235
|
return { full, mtime: (0, import_fs21.statSync)(full).mtimeMs };
|
|
12098
12236
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
12099
12237
|
for (const stale of backups.slice(retain)) {
|
|
@@ -13282,6 +13420,7 @@ function autoResumeSkipReason(row, opts) {
|
|
|
13282
13420
|
if (opts.now - row.status_updated_at > AUTO_RESUME_WINDOW_MS) return "too_old";
|
|
13283
13421
|
if (!opts.projectExists(row.project_path)) return "project_missing";
|
|
13284
13422
|
if (resumeIdForRow(row) == null) return "resume_identity_missing";
|
|
13423
|
+
if (!opts.historyExists(row)) return "history_missing";
|
|
13285
13424
|
return null;
|
|
13286
13425
|
}
|
|
13287
13426
|
function planAutoResume(rows, opts) {
|
|
@@ -13543,7 +13682,34 @@ var SessionRegistryBoot = class {
|
|
|
13543
13682
|
/** Resume only the recent sessions the user explicitly allowed us to start at boot. */
|
|
13544
13683
|
async autoResumePreviousSessions(rows) {
|
|
13545
13684
|
if (!this.autoResumeOnBoot) return;
|
|
13546
|
-
const
|
|
13685
|
+
const now = Date.now();
|
|
13686
|
+
const baseOptions = { now, projectExists: import_fs25.existsSync, historyExists: () => true };
|
|
13687
|
+
const historyExists = /* @__PURE__ */ new Map();
|
|
13688
|
+
const preflights = /* @__PURE__ */ new Set();
|
|
13689
|
+
for (const row of rows) {
|
|
13690
|
+
if (autoResumeSkipReason(row, baseOptions) != null) {
|
|
13691
|
+
historyExists.set(row.session_id, false);
|
|
13692
|
+
continue;
|
|
13693
|
+
}
|
|
13694
|
+
while (preflights.size >= AUTO_RESUME_CONCURRENCY) {
|
|
13695
|
+
await Promise.race(preflights);
|
|
13696
|
+
}
|
|
13697
|
+
const preflight = (async () => {
|
|
13698
|
+
try {
|
|
13699
|
+
const target = await this.deps.resolveConversationTarget(row.session_id);
|
|
13700
|
+
historyExists.set(row.session_id, target.ok || target.reason !== "history_file_missing");
|
|
13701
|
+
} catch {
|
|
13702
|
+
historyExists.set(row.session_id, true);
|
|
13703
|
+
}
|
|
13704
|
+
})();
|
|
13705
|
+
preflights.add(preflight);
|
|
13706
|
+
void preflight.then(() => preflights.delete(preflight));
|
|
13707
|
+
}
|
|
13708
|
+
await Promise.all(preflights);
|
|
13709
|
+
const plan = planAutoResume(rows, {
|
|
13710
|
+
...baseOptions,
|
|
13711
|
+
historyExists: (row) => historyExists.get(row.session_id) ?? false
|
|
13712
|
+
});
|
|
13547
13713
|
const skippedBy = {};
|
|
13548
13714
|
for (const { row, reason } of plan.skipped) {
|
|
13549
13715
|
skippedBy[reason] = (skippedBy[reason] ?? 0) + 1;
|
|
@@ -14016,8 +14182,8 @@ function discoveredToResponse(d, conversationId) {
|
|
|
14016
14182
|
|
|
14017
14183
|
// src/session-watchers.ts
|
|
14018
14184
|
var import_fs26 = require("fs");
|
|
14019
|
-
var
|
|
14020
|
-
var
|
|
14185
|
+
var import_os12 = require("os");
|
|
14186
|
+
var import_path22 = require("path");
|
|
14021
14187
|
var SessionWatchers = class {
|
|
14022
14188
|
constructor(deps) {
|
|
14023
14189
|
this.deps = deps;
|
|
@@ -14098,9 +14264,9 @@ var SessionWatchers = class {
|
|
|
14098
14264
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
14099
14265
|
watchForJsonl(sessionId, projectPath) {
|
|
14100
14266
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
14101
|
-
const projectsDir = (0,
|
|
14267
|
+
const projectsDir = (0, import_path22.join)((0, import_os12.homedir)(), ".claude", "projects", encoded);
|
|
14102
14268
|
const expectedFile = `${sessionId}.jsonl`;
|
|
14103
|
-
const filePath = (0,
|
|
14269
|
+
const filePath = (0, import_path22.join)(projectsDir, expectedFile);
|
|
14104
14270
|
const deadline = Date.now() + 12e4;
|
|
14105
14271
|
let watcher = null;
|
|
14106
14272
|
const cleanup = () => {
|
|
@@ -14122,10 +14288,10 @@ var SessionWatchers = class {
|
|
|
14122
14288
|
if (!resolvedFilePath && (0, import_fs26.existsSync)(projectsDir)) {
|
|
14123
14289
|
try {
|
|
14124
14290
|
const now = Date.now();
|
|
14125
|
-
const match = (0, import_fs26.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs26.statSync)((0,
|
|
14126
|
-
({ f }) => (0,
|
|
14291
|
+
const match = (0, import_fs26.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs26.statSync)((0, import_path22.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
14292
|
+
({ f }) => (0, import_path22.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path22.join)(projectsDir, f)) === sessionId
|
|
14127
14293
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
14128
|
-
if (match) resolvedFilePath = (0,
|
|
14294
|
+
if (match) resolvedFilePath = (0, import_path22.join)(projectsDir, match.f);
|
|
14129
14295
|
} catch {
|
|
14130
14296
|
}
|
|
14131
14297
|
}
|
|
@@ -14169,7 +14335,7 @@ var SessionWatchers = class {
|
|
|
14169
14335
|
watchForCodexRollout(sessionId, projectPath) {
|
|
14170
14336
|
const deadline = Date.now() + 12e4;
|
|
14171
14337
|
const now = /* @__PURE__ */ new Date();
|
|
14172
|
-
const dateDir = (0,
|
|
14338
|
+
const dateDir = (0, import_path22.join)(
|
|
14173
14339
|
String(now.getFullYear()),
|
|
14174
14340
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
14175
14341
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -14210,7 +14376,7 @@ var SessionWatchers = class {
|
|
|
14210
14376
|
this.deps.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
14211
14377
|
);
|
|
14212
14378
|
for (const root of this.deps.codexRoots) {
|
|
14213
|
-
const sessionsDir = (0,
|
|
14379
|
+
const sessionsDir = (0, import_path22.join)(root, dateDir);
|
|
14214
14380
|
if (!(0, import_fs26.existsSync)(sessionsDir)) continue;
|
|
14215
14381
|
let candidateFiles;
|
|
14216
14382
|
try {
|
|
@@ -14219,9 +14385,9 @@ var SessionWatchers = class {
|
|
|
14219
14385
|
continue;
|
|
14220
14386
|
}
|
|
14221
14387
|
const nowMs = Date.now();
|
|
14222
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs26.statSync)((0,
|
|
14388
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs26.statSync)((0, import_path22.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
14223
14389
|
for (const { f } of recentCandidates) {
|
|
14224
|
-
const candidatePath = (0,
|
|
14390
|
+
const candidatePath = (0, import_path22.join)(sessionsDir, f);
|
|
14225
14391
|
const match = matchesProjectPath(candidatePath);
|
|
14226
14392
|
if (!match) continue;
|
|
14227
14393
|
if (boundElsewhere.has(match.id)) continue;
|
|
@@ -14625,7 +14791,7 @@ var StreamerServer = class {
|
|
|
14625
14791
|
this.skipStartupWarmup = config.skipStartupWarmup ?? false;
|
|
14626
14792
|
this.autoResumeOnBoot = config.autoResumeOnBoot ?? false;
|
|
14627
14793
|
this.scanProfiles = config.scanProfiles;
|
|
14628
|
-
this.codexRoots = config.codexRoots ?? [(0,
|
|
14794
|
+
this.codexRoots = config.codexRoots ?? [(0, import_path23.join)((0, import_os13.homedir)(), ".codex", "sessions")];
|
|
14629
14795
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
14630
14796
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
14631
14797
|
const flagResolution = resolveFeatureFlags({
|
|
@@ -14642,8 +14808,8 @@ var StreamerServer = class {
|
|
|
14642
14808
|
this.claudeFlagsPersistable = config.claudeFlags === void 0;
|
|
14643
14809
|
this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
|
|
14644
14810
|
this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
14645
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0,
|
|
14646
|
-
this.runtimeDbPath = config.runtimeDbPath
|
|
14811
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path23.join)((0, import_os13.homedir)(), ".threadbase", "cache");
|
|
14812
|
+
this.runtimeDbPath = resolveRuntimeDbPath(config.runtimeDbPath);
|
|
14647
14813
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
14648
14814
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
14649
14815
|
this.scannerManager = new ScannerManager({
|
|
@@ -14675,7 +14841,8 @@ var StreamerServer = class {
|
|
|
14675
14841
|
selfPtyEndedAt: this.selfPtyEndedAt,
|
|
14676
14842
|
resumeSession: (opts) => this.sessionHandlers.resumeSession(opts),
|
|
14677
14843
|
watchConversationFile: (sessionId, historyId) => this.sessionWatchers.watchConversationFile(sessionId, historyId),
|
|
14678
|
-
broadcastSessionList: () => this.wsHub.broadcast(this.sessionListPayload())
|
|
14844
|
+
broadcastSessionList: () => this.wsHub.broadcast(this.sessionListPayload()),
|
|
14845
|
+
resolveConversationTarget: (sessionId) => this.resolveConversationTarget(sessionId)
|
|
14679
14846
|
});
|
|
14680
14847
|
this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
|
|
14681
14848
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
@@ -14818,7 +14985,7 @@ var StreamerServer = class {
|
|
|
14818
14985
|
temporalClient,
|
|
14819
14986
|
taskQueue: agentConfig.temporal.taskQueue
|
|
14820
14987
|
});
|
|
14821
|
-
const conversationsBaseDir = agentConfig.conversationsDir || (0,
|
|
14988
|
+
const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path23.join)((0, import_path23.dirname)(this.cacheDir), "conversations");
|
|
14822
14989
|
conversationWriter = createConversationWriter({
|
|
14823
14990
|
baseDir: conversationsBaseDir
|
|
14824
14991
|
});
|
|
@@ -15070,14 +15237,14 @@ var StreamerServer = class {
|
|
|
15070
15237
|
}
|
|
15071
15238
|
this.apnsClient = new ApnsClient(creds);
|
|
15072
15239
|
const sender = new LiveActivitySender(this.apnsClient, pushRepo);
|
|
15073
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
15074
|
-
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0,
|
|
15240
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os13.hostname)();
|
|
15241
|
+
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os13.hostname)());
|
|
15075
15242
|
this.liveActivityRenewal = new LiveActivityRenewalScheduler({
|
|
15076
15243
|
repo: pushRepo,
|
|
15077
15244
|
sender,
|
|
15078
15245
|
sessionStore: this.sessionStore,
|
|
15079
15246
|
serverId,
|
|
15080
|
-
serverLabel: (0,
|
|
15247
|
+
serverLabel: (0, import_os13.hostname)()
|
|
15081
15248
|
});
|
|
15082
15249
|
this.liveActivityRenewal.start();
|
|
15083
15250
|
this.log.info("Live Activity push enabled", {
|
|
@@ -15097,7 +15264,7 @@ var StreamerServer = class {
|
|
|
15097
15264
|
*/
|
|
15098
15265
|
initWaitingInputPush(pushRepo) {
|
|
15099
15266
|
const sender = new ExpoPushSender(pushRepo, process.env.THREADBASE_EXPO_ACCESS_TOKEN);
|
|
15100
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
15267
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os13.hostname)();
|
|
15101
15268
|
this.waitingInputNotifier = new WaitingInputNotifier(
|
|
15102
15269
|
sender,
|
|
15103
15270
|
serverId,
|
|
@@ -15234,7 +15401,7 @@ var StreamerServer = class {
|
|
|
15234
15401
|
let sessions = null;
|
|
15235
15402
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
15236
15403
|
const transport = await connectOrSpawnHost({
|
|
15237
|
-
instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
15404
|
+
instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0, import_os13.hostname)()
|
|
15238
15405
|
});
|
|
15239
15406
|
try {
|
|
15240
15407
|
sessions = await this.ptyManager.useRemoteRunner(transport);
|
|
@@ -15291,6 +15458,7 @@ var StreamerServer = class {
|
|
|
15291
15458
|
try {
|
|
15292
15459
|
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
15293
15460
|
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
15461
|
+
this.devicesRepo = new DevicesRepository(this.runtimeStore.getDatabase());
|
|
15294
15462
|
} catch (err) {
|
|
15295
15463
|
const message = err instanceof Error ? err.message : String(err);
|
|
15296
15464
|
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
@@ -15310,7 +15478,7 @@ var StreamerServer = class {
|
|
|
15310
15478
|
}
|
|
15311
15479
|
try {
|
|
15312
15480
|
this.cache = ConversationCache.open(
|
|
15313
|
-
(0,
|
|
15481
|
+
(0, import_path23.join)(this.cacheDir, "cache.db"),
|
|
15314
15482
|
this.tailSize,
|
|
15315
15483
|
void 0,
|
|
15316
15484
|
{
|
|
@@ -15340,18 +15508,39 @@ var StreamerServer = class {
|
|
|
15340
15508
|
if (copied > 0) {
|
|
15341
15509
|
this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
|
|
15342
15510
|
copied,
|
|
15511
|
+
table: "managed_sessions",
|
|
15343
15512
|
event: "runtime.legacy_import"
|
|
15344
15513
|
});
|
|
15345
15514
|
}
|
|
15346
15515
|
} catch (err) {
|
|
15347
15516
|
this.log.warn("[registry] legacy managed_sessions copy failed", {
|
|
15348
15517
|
event: "runtime.legacy_import_failed",
|
|
15518
|
+
table: "managed_sessions",
|
|
15519
|
+
err
|
|
15520
|
+
});
|
|
15521
|
+
}
|
|
15522
|
+
try {
|
|
15523
|
+
const result = this.runtimeStore?.importLegacyDevices(db);
|
|
15524
|
+
if (result && result.copied > 0) {
|
|
15525
|
+
this.log.info(
|
|
15526
|
+
`Moved ${result.copied} device row(s) from cache.db to runtime.db` + (result.purged ? "; removed the cache-side copy" : "; KEPT the cache-side copy (row count did not match after copy)"),
|
|
15527
|
+
{
|
|
15528
|
+
copied: result.copied,
|
|
15529
|
+
purged: result.purged,
|
|
15530
|
+
table: "devices",
|
|
15531
|
+
event: "runtime.legacy_import"
|
|
15532
|
+
}
|
|
15533
|
+
);
|
|
15534
|
+
}
|
|
15535
|
+
} catch (err) {
|
|
15536
|
+
this.log.warn("[registry] legacy devices move failed", {
|
|
15537
|
+
event: "runtime.legacy_import_failed",
|
|
15538
|
+
table: "devices",
|
|
15349
15539
|
err
|
|
15350
15540
|
});
|
|
15351
15541
|
}
|
|
15352
15542
|
this.cacheMetadataRepo = new CacheMetadataRepository(db);
|
|
15353
15543
|
this.pushRepo = new PushRepository(db);
|
|
15354
|
-
this.devicesRepo = new DevicesRepository(db);
|
|
15355
15544
|
this.initLiveActivityPush(this.pushRepo);
|
|
15356
15545
|
this.initWaitingInputPush(this.pushRepo);
|
|
15357
15546
|
this.cacheMonitor = new CacheIntegrityMonitor(
|
|
@@ -15677,7 +15866,7 @@ var StreamerServer = class {
|
|
|
15677
15866
|
nonce: sealed.nonce,
|
|
15678
15867
|
ephemeralPublicKey: sealed.ephemeralPublicKey,
|
|
15679
15868
|
publicUrl: this.publicUrl,
|
|
15680
|
-
machineName: (0,
|
|
15869
|
+
machineName: (0, import_os13.hostname)(),
|
|
15681
15870
|
...device && {
|
|
15682
15871
|
deviceId: device.deviceId,
|
|
15683
15872
|
deviceToken: device.deviceToken,
|
|
@@ -15884,13 +16073,17 @@ var StreamerServer = class {
|
|
|
15884
16073
|
conv = await this.conversationHandlers.findConversationByUuid(boundId);
|
|
15885
16074
|
}
|
|
15886
16075
|
}
|
|
16076
|
+
const cachedConvMeta = this.cache?.getMetaById(historyId);
|
|
16077
|
+
const cachedPath = cachedConvMeta?.filePath ? toNativeFilePath(cachedConvMeta.filePath) : null;
|
|
16078
|
+
const cachedCodexPath = (registryProvider === CODEX_CLI_PROVIDER || cachedConvMeta?.provider === CODEX_CLI_PROVIDER) && cachedPath != null && (0, import_fs27.existsSync)(cachedPath) ? cachedPath : null;
|
|
15887
16079
|
const jsonlCwd = jsonlPath ? await this.conversationHandlers.readCwdFromJsonl(jsonlPath) : null;
|
|
15888
|
-
const projectPath = jsonlCwd ?? conv?.projectPath;
|
|
16080
|
+
const projectPath = jsonlCwd ?? conv?.projectPath ?? (cachedCodexPath ? cachedConvMeta?.projectPath : null);
|
|
15889
16081
|
if (!projectPath) {
|
|
15890
|
-
if (!conv && !jsonlPath)
|
|
16082
|
+
if (!conv && !jsonlPath && !cachedCodexPath) {
|
|
16083
|
+
return { ok: false, reason: "history_file_missing" };
|
|
16084
|
+
}
|
|
15891
16085
|
return { ok: false, reason: "no_project_path" };
|
|
15892
16086
|
}
|
|
15893
|
-
const cachedConvMeta = this.cache?.getMetaById(historyId);
|
|
15894
16087
|
const provider = coerceProviderForRunner(
|
|
15895
16088
|
conv?.provider ?? cachedConvMeta?.provider ?? registryProvider
|
|
15896
16089
|
);
|
|
@@ -15904,7 +16097,7 @@ var StreamerServer = class {
|
|
|
15904
16097
|
// conversation. Kept separate from `jsonlPath` deliberately: feeding it to
|
|
15905
16098
|
// conversationBusy() would newly arm the mtime heuristic for Codex, which
|
|
15906
16099
|
// is exactly the over-broad signal the report ruled out.
|
|
15907
|
-
historyPath: jsonlPath ?? conv?.filePath ?? null,
|
|
16100
|
+
historyPath: jsonlPath ?? conv?.filePath ?? cachedCodexPath ?? null,
|
|
15908
16101
|
conv,
|
|
15909
16102
|
projectPath,
|
|
15910
16103
|
provider
|