@threadbase-sh/streamer 1.39.1 → 1.41.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/README.md +1 -1
- package/dist/cli.cjs +1055 -397
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +881 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +824 -166
- package/dist/index.js.map +1 -1
- package/dist/{migrations/010_create_managed_sessions.sql → runtime-migrations/001_create_managed_sessions.sql} +6 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -448,6 +448,12 @@ var FEATURE_FLAGS = [
|
|
|
448
448
|
description: "Send the built system prompt to fresh Codex sessions. Off by default: Codex has no --system-prompt flag, so the prompt goes in the positional [PROMPT] argument, which Codex treats as the user's opening turn rather than a system-level instruction.",
|
|
449
449
|
default: false,
|
|
450
450
|
env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
id: "sessionRehydration",
|
|
454
|
+
description: "Seed the session list at boot with sessions a previous streamer run left behind, so a restart leaves them one tap from resuming instead of silently gone. On by default, with a kill switch: it changes what GET /api/sessions contains.",
|
|
455
|
+
default: true,
|
|
456
|
+
env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
|
|
451
457
|
}
|
|
452
458
|
];
|
|
453
459
|
function findFeatureFlag(id) {
|
|
@@ -1883,6 +1889,12 @@ function detectShellPrompt(lines) {
|
|
|
1883
1889
|
return null;
|
|
1884
1890
|
}
|
|
1885
1891
|
|
|
1892
|
+
// src/utils/deriveSessionName.ts
|
|
1893
|
+
function deriveSessionName(firstMessageText) {
|
|
1894
|
+
const firstLine = firstMessageText.split("\n", 1)[0]?.trim() ?? "";
|
|
1895
|
+
return firstLine.slice(0, 80);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1886
1898
|
// src/pty-manager.ts
|
|
1887
1899
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1888
1900
|
var INPUT_HISTORY_MAX2 = 50;
|
|
@@ -2356,6 +2368,10 @@ var PTYManager = class {
|
|
|
2356
2368
|
if (session.inputHistory.length > INPUT_HISTORY_MAX2) {
|
|
2357
2369
|
session.inputHistory.shift();
|
|
2358
2370
|
}
|
|
2371
|
+
if (session.firstMessageText === void 0) {
|
|
2372
|
+
session.firstMessageText = text;
|
|
2373
|
+
session.sessionName = deriveSessionName(text);
|
|
2374
|
+
}
|
|
2359
2375
|
this.onUserMessage?.(session.id, text, ts);
|
|
2360
2376
|
}
|
|
2361
2377
|
getSession(sessionId) {
|
|
@@ -2621,7 +2637,9 @@ function toPublicSession2(s) {
|
|
|
2621
2637
|
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
2622
2638
|
...s.statusSource != null && { statusSource: s.statusSource },
|
|
2623
2639
|
...s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt },
|
|
2624
|
-
...s.filePath != null && { filePath: s.filePath }
|
|
2640
|
+
...s.filePath != null && { filePath: s.filePath },
|
|
2641
|
+
...s.sessionName != null && { sessionName: s.sessionName },
|
|
2642
|
+
...s.firstMessageText != null && { firstMessageText: s.firstMessageText }
|
|
2625
2643
|
};
|
|
2626
2644
|
}
|
|
2627
2645
|
function stripAnsi2(str) {
|
|
@@ -3017,7 +3035,7 @@ import { randomUUID as randomUUID5 } from "crypto";
|
|
|
3017
3035
|
import { EventEmitter } from "events";
|
|
3018
3036
|
import {
|
|
3019
3037
|
createReadStream,
|
|
3020
|
-
existsSync as
|
|
3038
|
+
existsSync as existsSync11,
|
|
3021
3039
|
watch as fsWatch,
|
|
3022
3040
|
readdirSync as readdirSync6,
|
|
3023
3041
|
readFileSync as readFileSync8,
|
|
@@ -3025,7 +3043,7 @@ import {
|
|
|
3025
3043
|
} from "fs";
|
|
3026
3044
|
import { realpath as realpath2 } from "fs/promises";
|
|
3027
3045
|
import { createServer } from "http";
|
|
3028
|
-
import { homedir as homedir9, hostname as
|
|
3046
|
+
import { homedir as homedir9, hostname as hostname3 } from "os";
|
|
3029
3047
|
import { basename as basename5, dirname as dirname9, join as join18 } from "path";
|
|
3030
3048
|
import { createInterface } from "readline";
|
|
3031
3049
|
|
|
@@ -3285,7 +3303,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
3285
3303
|
}
|
|
3286
3304
|
|
|
3287
3305
|
// src/api/app.ts
|
|
3288
|
-
import { Hono as
|
|
3306
|
+
import { Hono as Hono18 } from "hono";
|
|
3289
3307
|
|
|
3290
3308
|
// src/db/repositories/devices.repository.ts
|
|
3291
3309
|
import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
@@ -3605,12 +3623,222 @@ var errorMiddleware = (err, c) => {
|
|
|
3605
3623
|
return c.json({ error: message }, 500);
|
|
3606
3624
|
};
|
|
3607
3625
|
|
|
3608
|
-
// src/api/routes/
|
|
3626
|
+
// src/api/routes/backup.routes.ts
|
|
3609
3627
|
import { Hono as Hono2 } from "hono";
|
|
3628
|
+
import { hostname } from "os";
|
|
3629
|
+
|
|
3630
|
+
// src/services/backup/backup.ts
|
|
3631
|
+
var BACKUP_FORMAT_VERSION = 1;
|
|
3632
|
+
var BackupError = class extends Error {
|
|
3633
|
+
constructor(message, code) {
|
|
3634
|
+
super(message);
|
|
3635
|
+
this.code = code;
|
|
3636
|
+
}
|
|
3637
|
+
code;
|
|
3638
|
+
};
|
|
3639
|
+
function validateArchive(input) {
|
|
3640
|
+
if (!input || typeof input !== "object") {
|
|
3641
|
+
throw new BackupError("Backup is not an object", "INVALID_ARCHIVE");
|
|
3642
|
+
}
|
|
3643
|
+
const archive = input;
|
|
3644
|
+
const manifest = archive.manifest;
|
|
3645
|
+
if (!manifest || typeof manifest !== "object") {
|
|
3646
|
+
throw new BackupError("Backup is missing its manifest", "INVALID_ARCHIVE");
|
|
3647
|
+
}
|
|
3648
|
+
if (manifest.formatVersion !== BACKUP_FORMAT_VERSION) {
|
|
3649
|
+
throw new BackupError(
|
|
3650
|
+
`Unsupported backup format version ${String(manifest.formatVersion)}; this build reads version ${BACKUP_FORMAT_VERSION}`,
|
|
3651
|
+
"UNSUPPORTED_VERSION"
|
|
3652
|
+
);
|
|
3653
|
+
}
|
|
3654
|
+
if (!Array.isArray(archive.projects)) {
|
|
3655
|
+
throw new BackupError("Backup is missing its projects array", "INVALID_ARCHIVE");
|
|
3656
|
+
}
|
|
3657
|
+
for (const [i, p] of archive.projects.entries()) {
|
|
3658
|
+
if (!p || typeof p !== "object") {
|
|
3659
|
+
throw new BackupError(`Project at index ${i} is not an object`, "INVALID_ARCHIVE");
|
|
3660
|
+
}
|
|
3661
|
+
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
3662
|
+
throw new BackupError(`Project at index ${i} has no id`, "INVALID_ARCHIVE");
|
|
3663
|
+
}
|
|
3664
|
+
if (typeof p.path !== "string" || p.path.length === 0) {
|
|
3665
|
+
throw new BackupError(`Project at index ${i} has no path`, "INVALID_ARCHIVE");
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
const ids = new Set(archive.projects.map((p) => p.id));
|
|
3669
|
+
if (ids.size !== archive.projects.length) {
|
|
3670
|
+
throw new BackupError("Backup contains duplicate project ids", "INVALID_ARCHIVE");
|
|
3671
|
+
}
|
|
3672
|
+
return archive;
|
|
3673
|
+
}
|
|
3674
|
+
function remapPaths(projects, rules) {
|
|
3675
|
+
const ordered = [...rules].sort((a, b) => b.from.length - a.from.length);
|
|
3676
|
+
return projects.map((p) => {
|
|
3677
|
+
const rule = ordered.find((r) => p.path === r.from || p.path.startsWith(`${r.from}/`));
|
|
3678
|
+
if (!rule) return p;
|
|
3679
|
+
return { ...p, path: `${rule.to}${p.path.slice(rule.from.length)}` };
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
function planRestore(incoming, existing) {
|
|
3683
|
+
const byId = new Map(existing.map((e) => [e.id, e]));
|
|
3684
|
+
const byPath = new Map(existing.map((e) => [e.path, e]));
|
|
3685
|
+
const plan = { create: [], update: [], conflict: [] };
|
|
3686
|
+
for (const p of incoming) {
|
|
3687
|
+
const sameId = byId.get(p.id);
|
|
3688
|
+
if (sameId) {
|
|
3689
|
+
if (sameId.path !== p.path) plan.update.push(p);
|
|
3690
|
+
continue;
|
|
3691
|
+
}
|
|
3692
|
+
const samePath = byPath.get(p.path);
|
|
3693
|
+
if (samePath) {
|
|
3694
|
+
plan.conflict.push({ incoming: p, existingId: samePath.id });
|
|
3695
|
+
continue;
|
|
3696
|
+
}
|
|
3697
|
+
plan.create.push(p);
|
|
3698
|
+
}
|
|
3699
|
+
return plan;
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
// src/version.ts
|
|
3703
|
+
import { readFileSync as readFileSync4, realpathSync } from "fs";
|
|
3704
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
3705
|
+
var cached;
|
|
3706
|
+
function getVersion() {
|
|
3707
|
+
if (cached !== void 0) return cached;
|
|
3708
|
+
cached = resolveVersion();
|
|
3709
|
+
return cached;
|
|
3710
|
+
}
|
|
3711
|
+
function resolveVersion() {
|
|
3712
|
+
const scriptPath = process.argv[1] ?? "";
|
|
3713
|
+
const here = scriptPath ? dirname5(scriptPath) : process.cwd();
|
|
3714
|
+
let realHere = here;
|
|
3715
|
+
try {
|
|
3716
|
+
realHere = dirname5(realpathSync(scriptPath));
|
|
3717
|
+
} catch {
|
|
3718
|
+
}
|
|
3719
|
+
const searchDirs = realHere === here ? [here, join7(here, "..")] : [here, join7(here, ".."), realHere, join7(realHere, "..")];
|
|
3720
|
+
for (const dir of searchDirs) {
|
|
3721
|
+
try {
|
|
3722
|
+
const v = readFileSync4(join7(dir, "version.txt"), "utf8").trim();
|
|
3723
|
+
if (v) return v;
|
|
3724
|
+
} catch {
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
try {
|
|
3728
|
+
const pkg = JSON.parse(readFileSync4(join7(here, "..", "package.json"), "utf8"));
|
|
3729
|
+
if (pkg.version) return `${pkg.version}+source`;
|
|
3730
|
+
} catch {
|
|
3731
|
+
}
|
|
3732
|
+
return "0.0.0+unknown";
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
// src/api/routes/backup.routes.ts
|
|
3736
|
+
function readBody(c) {
|
|
3737
|
+
return new Promise((resolve2, reject) => {
|
|
3738
|
+
const chunks = [];
|
|
3739
|
+
c.env.incoming.on("data", (chunk) => chunks.push(chunk));
|
|
3740
|
+
c.env.incoming.on("end", () => {
|
|
3741
|
+
try {
|
|
3742
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
3743
|
+
resolve2(raw ? JSON.parse(raw) : {});
|
|
3744
|
+
} catch {
|
|
3745
|
+
reject(new Error("Invalid JSON body"));
|
|
3746
|
+
}
|
|
3747
|
+
});
|
|
3748
|
+
c.env.incoming.on("error", reject);
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3751
|
+
var createBackupRoutes = (deps) => {
|
|
3752
|
+
const app = new Hono2();
|
|
3753
|
+
app.get("/export", (c) => {
|
|
3754
|
+
const repo = deps.projectsRepo();
|
|
3755
|
+
if (!repo) {
|
|
3756
|
+
return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
3757
|
+
}
|
|
3758
|
+
const projects = repo.listProjects().map((p) => ({
|
|
3759
|
+
id: p.id,
|
|
3760
|
+
path: p.path,
|
|
3761
|
+
name: p.name ?? null,
|
|
3762
|
+
createdAt: p.createdAt,
|
|
3763
|
+
updatedAt: p.updatedAt
|
|
3764
|
+
}));
|
|
3765
|
+
return c.json({
|
|
3766
|
+
manifest: {
|
|
3767
|
+
formatVersion: BACKUP_FORMAT_VERSION,
|
|
3768
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3769
|
+
streamerVersion: getVersion(),
|
|
3770
|
+
sourceHost: hostname(),
|
|
3771
|
+
// No endpoint here exports the API key. The flag is recorded so an
|
|
3772
|
+
// archive is self-describing about its own sensitivity rather than
|
|
3773
|
+
// requiring a reader to infer it.
|
|
3774
|
+
includesSecrets: false,
|
|
3775
|
+
counts: { projects: projects.length }
|
|
3776
|
+
},
|
|
3777
|
+
projects
|
|
3778
|
+
});
|
|
3779
|
+
});
|
|
3780
|
+
app.post("/restore", async (c) => {
|
|
3781
|
+
const repo = deps.projectsRepo();
|
|
3782
|
+
if (!repo) {
|
|
3783
|
+
return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
3784
|
+
}
|
|
3785
|
+
let body;
|
|
3786
|
+
try {
|
|
3787
|
+
body = await readBody(c);
|
|
3788
|
+
} catch {
|
|
3789
|
+
return c.json({ error: "Invalid JSON body", code: "INVALID_BODY" }, 400);
|
|
3790
|
+
}
|
|
3791
|
+
let archive;
|
|
3792
|
+
try {
|
|
3793
|
+
archive = validateArchive(body.archive);
|
|
3794
|
+
} catch (err) {
|
|
3795
|
+
if (err instanceof BackupError) {
|
|
3796
|
+
return c.json({ error: err.message, code: err.code }, 400);
|
|
3797
|
+
}
|
|
3798
|
+
throw err;
|
|
3799
|
+
}
|
|
3800
|
+
const rules = Array.isArray(body.pathMap) ? body.pathMap.filter((r) => typeof r?.from === "string" && typeof r?.to === "string").map((r) => ({ from: r.from, to: r.to })) : [];
|
|
3801
|
+
const incoming = rules.length > 0 ? remapPaths(archive.projects, rules) : archive.projects;
|
|
3802
|
+
const existing = repo.listProjects().map((p) => ({ id: p.id, path: p.path }));
|
|
3803
|
+
const plan = planRestore(incoming, existing);
|
|
3804
|
+
const summary = {
|
|
3805
|
+
create: plan.create.length,
|
|
3806
|
+
update: plan.update.length,
|
|
3807
|
+
conflict: plan.conflict.length
|
|
3808
|
+
};
|
|
3809
|
+
if (body.apply !== true) {
|
|
3810
|
+
return c.json({ applied: false, summary, plan });
|
|
3811
|
+
}
|
|
3812
|
+
if (plan.conflict.length > 0) {
|
|
3813
|
+
return c.json(
|
|
3814
|
+
{
|
|
3815
|
+
error: "Restore has unresolved conflicts",
|
|
3816
|
+
code: "RESTORE_CONFLICT",
|
|
3817
|
+
summary,
|
|
3818
|
+
plan
|
|
3819
|
+
},
|
|
3820
|
+
409
|
|
3821
|
+
);
|
|
3822
|
+
}
|
|
3823
|
+
let applied = 0;
|
|
3824
|
+
for (const p of [...plan.create, ...plan.update]) {
|
|
3825
|
+
try {
|
|
3826
|
+
repo.upsertProjectByPath(p.path, { name: p.name });
|
|
3827
|
+
applied++;
|
|
3828
|
+
} catch {
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
return c.json({ applied: true, summary, appliedCount: applied });
|
|
3832
|
+
});
|
|
3833
|
+
return app;
|
|
3834
|
+
};
|
|
3835
|
+
|
|
3836
|
+
// src/api/routes/browse.routes.ts
|
|
3837
|
+
import { Hono as Hono3 } from "hono";
|
|
3610
3838
|
var ALREADY_HANDLED = 597;
|
|
3611
3839
|
var alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });
|
|
3612
3840
|
var createBrowseRoutes = (deps) => {
|
|
3613
|
-
const app = new
|
|
3841
|
+
const app = new Hono3();
|
|
3614
3842
|
app.get("/browse", async (c) => {
|
|
3615
3843
|
const url = new URL(c.req.url);
|
|
3616
3844
|
await deps.handleBrowse(url, c.env.outgoing);
|
|
@@ -3624,7 +3852,7 @@ var createBrowseRoutes = (deps) => {
|
|
|
3624
3852
|
};
|
|
3625
3853
|
|
|
3626
3854
|
// src/api/routes/cacheAlert.routes.ts
|
|
3627
|
-
import { Hono as
|
|
3855
|
+
import { Hono as Hono4 } from "hono";
|
|
3628
3856
|
|
|
3629
3857
|
// src/schemas/cacheAlert.schema.ts
|
|
3630
3858
|
import { z } from "zod";
|
|
@@ -3647,7 +3875,7 @@ function readRawBody2(req) {
|
|
|
3647
3875
|
});
|
|
3648
3876
|
}
|
|
3649
3877
|
var createCacheAlertRoutes = (deps) => {
|
|
3650
|
-
const app = new
|
|
3878
|
+
const app = new Hono4();
|
|
3651
3879
|
app.get("/", (c) => {
|
|
3652
3880
|
const monitor = deps.cacheMonitor();
|
|
3653
3881
|
return c.json({ pending: monitor?.pending ?? null });
|
|
@@ -3684,7 +3912,7 @@ var createCacheAlertRoutes = (deps) => {
|
|
|
3684
3912
|
};
|
|
3685
3913
|
|
|
3686
3914
|
// src/api/routes/config.routes.ts
|
|
3687
|
-
import { Hono as
|
|
3915
|
+
import { Hono as Hono5 } from "hono";
|
|
3688
3916
|
|
|
3689
3917
|
// src/schemas/claudeFlags.schema.ts
|
|
3690
3918
|
import { z as z2 } from "zod";
|
|
@@ -3705,7 +3933,7 @@ function readRawBody3(req) {
|
|
|
3705
3933
|
});
|
|
3706
3934
|
}
|
|
3707
3935
|
var createConfigRoutes = (deps) => {
|
|
3708
|
-
const app = new
|
|
3936
|
+
const app = new Hono5();
|
|
3709
3937
|
app.get("/claude-flags", (c) => c.json(deps.claudeFlagsConfig()));
|
|
3710
3938
|
app.get("/feature-flags", (c) => c.json(deps.featureFlagsConfig()));
|
|
3711
3939
|
app.put("/claude-flags", async (c) => {
|
|
@@ -3740,11 +3968,11 @@ var createConfigRoutes = (deps) => {
|
|
|
3740
3968
|
};
|
|
3741
3969
|
|
|
3742
3970
|
// src/api/routes/conversations.routes.ts
|
|
3743
|
-
import { Hono as
|
|
3971
|
+
import { Hono as Hono6 } from "hono";
|
|
3744
3972
|
var ALREADY_HANDLED2 = 597;
|
|
3745
3973
|
var alreadyHandled2 = () => new Response(null, { status: ALREADY_HANDLED2 });
|
|
3746
3974
|
var createConversationRoutes = (deps) => {
|
|
3747
|
-
const app = new
|
|
3975
|
+
const app = new Hono6();
|
|
3748
3976
|
app.get("/count", async (c) => {
|
|
3749
3977
|
const url = new URL(c.req.url);
|
|
3750
3978
|
await deps.handleConversationsCount(url, c.env.outgoing);
|
|
@@ -3771,9 +3999,9 @@ var createConversationRoutes = (deps) => {
|
|
|
3771
3999
|
};
|
|
3772
4000
|
|
|
3773
4001
|
// src/api/routes/devices.routes.ts
|
|
3774
|
-
import { Hono as
|
|
4002
|
+
import { Hono as Hono7 } from "hono";
|
|
3775
4003
|
var createDeviceRoutes = (deps) => {
|
|
3776
|
-
const app = new
|
|
4004
|
+
const app = new Hono7();
|
|
3777
4005
|
app.get("/", (c) => {
|
|
3778
4006
|
const repo = deps.devicesRepo();
|
|
3779
4007
|
if (!repo) return c.json({ devices: [], available: false });
|
|
@@ -3796,45 +4024,134 @@ var createDeviceRoutes = (deps) => {
|
|
|
3796
4024
|
return app;
|
|
3797
4025
|
};
|
|
3798
4026
|
|
|
3799
|
-
// src/api/routes/
|
|
3800
|
-
import {
|
|
4027
|
+
// src/api/routes/diagnostics.routes.ts
|
|
4028
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4029
|
+
import { Hono as Hono8 } from "hono";
|
|
3801
4030
|
|
|
3802
|
-
// src/
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
if (
|
|
3808
|
-
|
|
3809
|
-
|
|
4031
|
+
// src/services/diagnostics/diagnostics.ts
|
|
4032
|
+
var DIAGNOSTICS_CONTRACT_VERSION = 1;
|
|
4033
|
+
function redactPath(path) {
|
|
4034
|
+
if (!path) return null;
|
|
4035
|
+
const parts = path.split(/[/\\]/).filter(Boolean);
|
|
4036
|
+
if (parts.length <= 2) return parts.join("/");
|
|
4037
|
+
return `\u2026/${parts.slice(-2).join("/")}`;
|
|
4038
|
+
}
|
|
4039
|
+
function worstStatus(checks) {
|
|
4040
|
+
const rank = { ok: 0, unknown: 1, degraded: 2, failed: 3 };
|
|
4041
|
+
return checks.reduce(
|
|
4042
|
+
(worst, c) => rank[c.status] > rank[worst] ? c.status : worst,
|
|
4043
|
+
"ok"
|
|
4044
|
+
);
|
|
3810
4045
|
}
|
|
3811
|
-
function
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
}
|
|
4046
|
+
function buildReport(checks, now = /* @__PURE__ */ new Date()) {
|
|
4047
|
+
return {
|
|
4048
|
+
contractVersion: DIAGNOSTICS_CONTRACT_VERSION,
|
|
4049
|
+
generatedAt: now.toISOString(),
|
|
4050
|
+
overall: worstStatus(checks),
|
|
4051
|
+
checks
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
var SECRET_KEY_RE = /(key|token|secret|password|passwd|credential|authorization|cookie)/i;
|
|
4055
|
+
function redactValue(value) {
|
|
4056
|
+
if (Array.isArray(value)) {
|
|
4057
|
+
return value.map((v) => redactValue(v));
|
|
3818
4058
|
}
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
if (v) return v;
|
|
3824
|
-
} catch {
|
|
4059
|
+
if (value && typeof value === "object") {
|
|
4060
|
+
const out = {};
|
|
4061
|
+
for (const [k, v] of Object.entries(value)) {
|
|
4062
|
+
out[k] = SECRET_KEY_RE.test(k) ? "[redacted]" : redactValue(v);
|
|
3825
4063
|
}
|
|
4064
|
+
return out;
|
|
3826
4065
|
}
|
|
4066
|
+
return value;
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// src/api/routes/diagnostics.routes.ts
|
|
4070
|
+
function providerCheck(name, resolve2) {
|
|
3827
4071
|
try {
|
|
3828
|
-
const
|
|
3829
|
-
|
|
4072
|
+
const exe = resolve2();
|
|
4073
|
+
return {
|
|
4074
|
+
id: `provider:${name}`,
|
|
4075
|
+
status: "ok",
|
|
4076
|
+
summary: `${name} CLI is installed.`,
|
|
4077
|
+
remediation: "NONE",
|
|
4078
|
+
detail: { location: redactPath(exe) }
|
|
4079
|
+
};
|
|
3830
4080
|
} catch {
|
|
4081
|
+
return {
|
|
4082
|
+
id: `provider:${name}`,
|
|
4083
|
+
status: "failed",
|
|
4084
|
+
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
4085
|
+
remediation: "PROVIDER_NOT_INSTALLED"
|
|
4086
|
+
};
|
|
3831
4087
|
}
|
|
3832
|
-
return "0.0.0+unknown";
|
|
3833
4088
|
}
|
|
4089
|
+
var createDiagnosticsRoutes = (deps) => {
|
|
4090
|
+
const app = new Hono8();
|
|
4091
|
+
app.get("/", (c) => {
|
|
4092
|
+
const checks = [];
|
|
4093
|
+
checks.push({
|
|
4094
|
+
id: "streamer",
|
|
4095
|
+
status: "ok",
|
|
4096
|
+
summary: "Streamer is running.",
|
|
4097
|
+
remediation: "NONE",
|
|
4098
|
+
detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
|
|
4099
|
+
});
|
|
4100
|
+
checks.push(providerCheck("claude-code", resolveClaudeExe));
|
|
4101
|
+
checks.push(providerCheck("codex-cli", resolveCodexExe));
|
|
4102
|
+
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
4103
|
+
checks.push(
|
|
4104
|
+
cacheAlert ? {
|
|
4105
|
+
id: "cache",
|
|
4106
|
+
status: "degraded",
|
|
4107
|
+
summary: "Conversation cache reported an integrity alert.",
|
|
4108
|
+
remediation: "CACHE_DEGRADED"
|
|
4109
|
+
} : {
|
|
4110
|
+
id: "cache",
|
|
4111
|
+
status: "ok",
|
|
4112
|
+
summary: "Conversation cache is healthy.",
|
|
4113
|
+
remediation: "NONE"
|
|
4114
|
+
}
|
|
4115
|
+
);
|
|
4116
|
+
let ptyOk = true;
|
|
4117
|
+
try {
|
|
4118
|
+
__require.resolve("node-pty");
|
|
4119
|
+
} catch {
|
|
4120
|
+
ptyOk = false;
|
|
4121
|
+
}
|
|
4122
|
+
checks.push(
|
|
4123
|
+
ptyOk ? { id: "pty", status: "ok", summary: "PTY subsystem is available.", remediation: "NONE" } : {
|
|
4124
|
+
id: "pty",
|
|
4125
|
+
status: "failed",
|
|
4126
|
+
summary: "node-pty failed to load, so no managed session can start.",
|
|
4127
|
+
remediation: "PTY_UNAVAILABLE"
|
|
4128
|
+
}
|
|
4129
|
+
);
|
|
4130
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
4131
|
+
const claudeProjects = home ? `${home}/.claude/projects` : "";
|
|
4132
|
+
checks.push(
|
|
4133
|
+
claudeProjects && existsSync5(claudeProjects) ? {
|
|
4134
|
+
id: "filesystem",
|
|
4135
|
+
status: "ok",
|
|
4136
|
+
summary: "Provider history directory is present.",
|
|
4137
|
+
remediation: "NONE",
|
|
4138
|
+
detail: { location: redactPath(claudeProjects) }
|
|
4139
|
+
} : {
|
|
4140
|
+
id: "filesystem",
|
|
4141
|
+
status: "degraded",
|
|
4142
|
+
summary: "Provider history directory was not found; history may be unavailable.",
|
|
4143
|
+
remediation: "FS_SCOPE_MISSING"
|
|
4144
|
+
}
|
|
4145
|
+
);
|
|
4146
|
+
return c.json(redactValue(buildReport(checks)));
|
|
4147
|
+
});
|
|
4148
|
+
return app;
|
|
4149
|
+
};
|
|
3834
4150
|
|
|
3835
4151
|
// src/api/routes/health.routes.ts
|
|
4152
|
+
import { Hono as Hono9 } from "hono";
|
|
3836
4153
|
var createHealthRoutes = (deps) => {
|
|
3837
|
-
const app = new
|
|
4154
|
+
const app = new Hono9();
|
|
3838
4155
|
app.get("/", (c) => {
|
|
3839
4156
|
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
3840
4157
|
return c.json({ ok: true, version: getVersion(), ...cacheAlert ? { cacheAlert } : {} });
|
|
@@ -3843,9 +4160,9 @@ var createHealthRoutes = (deps) => {
|
|
|
3843
4160
|
};
|
|
3844
4161
|
|
|
3845
4162
|
// src/api/routes/logs.routes.ts
|
|
3846
|
-
import { closeSync, existsSync as
|
|
4163
|
+
import { closeSync, existsSync as existsSync6, fstatSync, openSync, readSync, statSync } from "fs";
|
|
3847
4164
|
import { join as join9 } from "path";
|
|
3848
|
-
import { Hono as
|
|
4165
|
+
import { Hono as Hono10 } from "hono";
|
|
3849
4166
|
|
|
3850
4167
|
// src/lifecycle/constants.ts
|
|
3851
4168
|
import { homedir as homedir4 } from "os";
|
|
@@ -3863,12 +4180,12 @@ function resolveLogPath(source) {
|
|
|
3863
4180
|
function pickDefaultSource() {
|
|
3864
4181
|
for (const source of ["stdout", "stderr", "dev"]) {
|
|
3865
4182
|
const p = resolveLogPath(source);
|
|
3866
|
-
if (
|
|
4183
|
+
if (existsSync6(p) && statSync(p).size > 0) return source;
|
|
3867
4184
|
}
|
|
3868
4185
|
return "stdout";
|
|
3869
4186
|
}
|
|
3870
4187
|
function readLogLines(filePath, sinceOffset, limit) {
|
|
3871
|
-
if (!
|
|
4188
|
+
if (!existsSync6(filePath)) {
|
|
3872
4189
|
return { lines: [], offset: 0, total: 0 };
|
|
3873
4190
|
}
|
|
3874
4191
|
const fd = openSync(filePath, "r");
|
|
@@ -3903,7 +4220,7 @@ function readLogLines(filePath, sinceOffset, limit) {
|
|
|
3903
4220
|
}
|
|
3904
4221
|
}
|
|
3905
4222
|
function createLogsRoutes() {
|
|
3906
|
-
const app = new
|
|
4223
|
+
const app = new Hono10();
|
|
3907
4224
|
app.get("/", (c) => {
|
|
3908
4225
|
try {
|
|
3909
4226
|
const sourceParam = (c.req.query("source") || "").toLowerCase();
|
|
@@ -3911,7 +4228,7 @@ function createLogsRoutes() {
|
|
|
3911
4228
|
const logPath = resolveLogPath(source);
|
|
3912
4229
|
const sinceOffset = parseInt(c.req.query("since") || "0", 10);
|
|
3913
4230
|
const limit = Math.min(parseInt(c.req.query("limit") || "100", 10) || 100, 1e3);
|
|
3914
|
-
if (!
|
|
4231
|
+
if (!existsSync6(logPath)) {
|
|
3915
4232
|
return c.json({
|
|
3916
4233
|
logs: [],
|
|
3917
4234
|
message: `No log file found for source=${source}`,
|
|
@@ -3948,7 +4265,7 @@ function createLogsRoutes() {
|
|
|
3948
4265
|
try {
|
|
3949
4266
|
const sources = ["stdout", "stderr", "dev"].map((source) => {
|
|
3950
4267
|
const logPath = resolveLogPath(source);
|
|
3951
|
-
if (!
|
|
4268
|
+
if (!existsSync6(logPath)) {
|
|
3952
4269
|
return { source, exists: false, total: 0, fileSize: 0 };
|
|
3953
4270
|
}
|
|
3954
4271
|
const stats = statSync(logPath);
|
|
@@ -3974,8 +4291,8 @@ function createLogsRoutes() {
|
|
|
3974
4291
|
// src/api/routes/misc.routes.ts
|
|
3975
4292
|
import { spawn } from "child_process";
|
|
3976
4293
|
import { createHmac, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
3977
|
-
import { Hono as
|
|
3978
|
-
import { hostname } from "os";
|
|
4294
|
+
import { Hono as Hono11 } from "hono";
|
|
4295
|
+
import { hostname as hostname2 } from "os";
|
|
3979
4296
|
|
|
3980
4297
|
// src/config/update-config.ts
|
|
3981
4298
|
import { readFileSync as readFileSync5 } from "fs";
|
|
@@ -4305,12 +4622,12 @@ function verifyWebhookSignature(body, header, secret) {
|
|
|
4305
4622
|
}
|
|
4306
4623
|
var clientLog = getLogger("client");
|
|
4307
4624
|
var createMiscRoutes = (deps) => {
|
|
4308
|
-
const app = new
|
|
4625
|
+
const app = new Hono11();
|
|
4309
4626
|
app.get("/api/info", (c) => {
|
|
4310
4627
|
const ptyIds = deps.ptyAttachedIds();
|
|
4311
4628
|
return c.json({
|
|
4312
4629
|
version: getVersion(),
|
|
4313
|
-
machineName:
|
|
4630
|
+
machineName: hostname2(),
|
|
4314
4631
|
platform: process.platform,
|
|
4315
4632
|
activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === "running").length,
|
|
4316
4633
|
publicUrl: deps.publicUrl,
|
|
@@ -4436,11 +4753,11 @@ var createMiscRoutes = (deps) => {
|
|
|
4436
4753
|
};
|
|
4437
4754
|
|
|
4438
4755
|
// src/api/routes/pair.routes.ts
|
|
4439
|
-
import { Hono as
|
|
4756
|
+
import { Hono as Hono12 } from "hono";
|
|
4440
4757
|
var ALREADY_HANDLED3 = 597;
|
|
4441
4758
|
var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
|
|
4442
4759
|
var createPairRoutes = (deps) => {
|
|
4443
|
-
const app = new
|
|
4760
|
+
const app = new Hono12();
|
|
4444
4761
|
app.post("/start", (c) => {
|
|
4445
4762
|
deps.handlePairStart(c.env.outgoing);
|
|
4446
4763
|
return alreadyHandled3();
|
|
@@ -4453,11 +4770,11 @@ var createPairRoutes = (deps) => {
|
|
|
4453
4770
|
};
|
|
4454
4771
|
|
|
4455
4772
|
// src/api/routes/projects.routes.ts
|
|
4456
|
-
import { Hono as
|
|
4773
|
+
import { Hono as Hono13 } from "hono";
|
|
4457
4774
|
var ALREADY_HANDLED4 = 597;
|
|
4458
4775
|
var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
|
|
4459
4776
|
var createProjectRoutes = (deps) => {
|
|
4460
|
-
const app = new
|
|
4777
|
+
const app = new Hono13();
|
|
4461
4778
|
app.get("/", (c) => {
|
|
4462
4779
|
const url = new URL(c.req.url);
|
|
4463
4780
|
deps.handleListProjects(url, c.env.outgoing);
|
|
@@ -4472,7 +4789,7 @@ var createProjectRoutes = (deps) => {
|
|
|
4472
4789
|
};
|
|
4473
4790
|
|
|
4474
4791
|
// src/api/routes/providers.routes.ts
|
|
4475
|
-
import { Hono as
|
|
4792
|
+
import { Hono as Hono14 } from "hono";
|
|
4476
4793
|
|
|
4477
4794
|
// src/services/providers/providerHealth.ts
|
|
4478
4795
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -4595,7 +4912,7 @@ async function providerHealth(name, resolveExe, detect = runVersion) {
|
|
|
4595
4912
|
|
|
4596
4913
|
// src/api/routes/providers.routes.ts
|
|
4597
4914
|
var createProviderRoutes = () => {
|
|
4598
|
-
const app = new
|
|
4915
|
+
const app = new Hono14();
|
|
4599
4916
|
app.get("/", async (c) => {
|
|
4600
4917
|
const providers = await Promise.all([
|
|
4601
4918
|
providerHealth(CLAUDE_CODE_PROVIDER, resolveClaudeExe),
|
|
@@ -4607,11 +4924,11 @@ var createProviderRoutes = () => {
|
|
|
4607
4924
|
};
|
|
4608
4925
|
|
|
4609
4926
|
// src/api/routes/scanner.routes.ts
|
|
4610
|
-
import { Hono as
|
|
4927
|
+
import { Hono as Hono15 } from "hono";
|
|
4611
4928
|
var ALREADY_HANDLED5 = 597;
|
|
4612
4929
|
var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
|
|
4613
4930
|
var createScannerRoutes = (deps) => {
|
|
4614
|
-
const app = new
|
|
4931
|
+
const app = new Hono15();
|
|
4615
4932
|
app.get("/api/search", async (c) => {
|
|
4616
4933
|
const url = new URL(c.req.url);
|
|
4617
4934
|
await deps.handleSearch(url, c.env.outgoing);
|
|
@@ -4621,11 +4938,11 @@ var createScannerRoutes = (deps) => {
|
|
|
4621
4938
|
};
|
|
4622
4939
|
|
|
4623
4940
|
// src/api/routes/sessions.routes.ts
|
|
4624
|
-
import { Hono as
|
|
4941
|
+
import { Hono as Hono16 } from "hono";
|
|
4625
4942
|
var ALREADY_HANDLED6 = 597;
|
|
4626
4943
|
var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
|
|
4627
4944
|
var createSessionRoutes = (deps) => {
|
|
4628
|
-
const app = new
|
|
4945
|
+
const app = new Hono16();
|
|
4629
4946
|
app.get("/count", (c) => {
|
|
4630
4947
|
deps.handleSessionsCount(c.env.outgoing);
|
|
4631
4948
|
return alreadyHandled6();
|
|
@@ -4700,9 +5017,9 @@ var createSessionRoutes = (deps) => {
|
|
|
4700
5017
|
};
|
|
4701
5018
|
|
|
4702
5019
|
// src/api/routes/ws.routes.ts
|
|
4703
|
-
import { Hono as
|
|
5020
|
+
import { Hono as Hono17 } from "hono";
|
|
4704
5021
|
var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
4705
|
-
const app = new
|
|
5022
|
+
const app = new Hono17();
|
|
4706
5023
|
app.get(
|
|
4707
5024
|
"/ws",
|
|
4708
5025
|
upgradeWebSocket(() => {
|
|
@@ -4728,7 +5045,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
4728
5045
|
|
|
4729
5046
|
// src/api/app.ts
|
|
4730
5047
|
var createHonoApp = (deps, upgradeWebSocket) => {
|
|
4731
|
-
const app = new
|
|
5048
|
+
const app = new Hono18();
|
|
4732
5049
|
const httpLog = getLogger("http");
|
|
4733
5050
|
app.use("*", async (c, next) => {
|
|
4734
5051
|
const start = Date.now();
|
|
@@ -4749,6 +5066,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
4749
5066
|
app.use("*", authMiddleware(deps));
|
|
4750
5067
|
app.onError(errorMiddleware);
|
|
4751
5068
|
app.route("/healthz", createHealthRoutes(deps));
|
|
5069
|
+
app.route("/api/diagnostics", createDiagnosticsRoutes(deps));
|
|
4752
5070
|
app.route("/", createMiscRoutes(deps));
|
|
4753
5071
|
app.route("/api/sessions", createSessionRoutes(deps));
|
|
4754
5072
|
app.route("/api/conversations", createConversationRoutes(deps));
|
|
@@ -4757,6 +5075,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
4757
5075
|
app.route("/api/projects", createProjectRoutes(deps));
|
|
4758
5076
|
app.route("/api/providers", createProviderRoutes());
|
|
4759
5077
|
app.route("/api/devices", createDeviceRoutes(deps));
|
|
5078
|
+
app.route("/api/backup", createBackupRoutes(deps));
|
|
4760
5079
|
app.route("/api/pair", createPairRoutes(deps));
|
|
4761
5080
|
app.route("/api", createBrowseRoutes(deps));
|
|
4762
5081
|
app.route("/", createScannerRoutes(deps));
|
|
@@ -4825,7 +5144,7 @@ import {
|
|
|
4825
5144
|
parseJsonlLine
|
|
4826
5145
|
} from "@threadbase-sh/scanner";
|
|
4827
5146
|
import Database from "better-sqlite3";
|
|
4828
|
-
import { closeSync as closeSync3, existsSync as
|
|
5147
|
+
import { closeSync as closeSync3, existsSync as existsSync7, mkdirSync as mkdirSync3, openSync as openSync3, readSync as readSync3, statSync as statSync3 } from "fs";
|
|
4829
5148
|
import { open as openAsync } from "fs/promises";
|
|
4830
5149
|
import { dirname as dirname7 } from "path";
|
|
4831
5150
|
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
@@ -4840,6 +5159,9 @@ function getMigrationsDir2() {
|
|
|
4840
5159
|
}
|
|
4841
5160
|
return __dirname;
|
|
4842
5161
|
}
|
|
5162
|
+
function resolveMigrationsDir(name = "migrations") {
|
|
5163
|
+
return join12(getMigrationsDir2(), name);
|
|
5164
|
+
}
|
|
4843
5165
|
var SCHEMA_MIGRATIONS_SQL = `
|
|
4844
5166
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
4845
5167
|
id TEXT PRIMARY KEY,
|
|
@@ -4848,7 +5170,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
4848
5170
|
`;
|
|
4849
5171
|
function runSqliteMigrations(db, migrationsDir) {
|
|
4850
5172
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
4851
|
-
const dir = migrationsDir ??
|
|
5173
|
+
const dir = migrationsDir ?? resolveMigrationsDir();
|
|
4852
5174
|
const files = readdirSync2(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
4853
5175
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
4854
5176
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
@@ -6057,7 +6379,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6057
6379
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
6058
6380
|
* JSONL has been deleted.
|
|
6059
6381
|
*/
|
|
6060
|
-
pruneGhostFiles(exists =
|
|
6382
|
+
pruneGhostFiles(exists = existsSync7) {
|
|
6061
6383
|
const rows = this.stmts.allFilePaths.all();
|
|
6062
6384
|
const ghosts = [];
|
|
6063
6385
|
const prune = this.db.transaction((ids) => {
|
|
@@ -6112,7 +6434,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6112
6434
|
* Returns the removed IDs.
|
|
6113
6435
|
*/
|
|
6114
6436
|
reconcileDeletions(livePaths, opts) {
|
|
6115
|
-
const exists = opts?.exists ??
|
|
6437
|
+
const exists = opts?.exists ?? existsSync7;
|
|
6116
6438
|
const rows = this.stmts.allFilePaths.all();
|
|
6117
6439
|
const removed = [];
|
|
6118
6440
|
const drop = this.db.transaction((ids) => {
|
|
@@ -6147,7 +6469,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6147
6469
|
* reports drift for the CacheIntegrityMonitor to classify. `tailed` flags
|
|
6148
6470
|
* rows that still have cached history (which pruneGhostFiles would keep).
|
|
6149
6471
|
*/
|
|
6150
|
-
listMissingFiles(exists =
|
|
6472
|
+
listMissingFiles(exists = existsSync7) {
|
|
6151
6473
|
const rows = this.stmts.allFilePathsWithTitle.all();
|
|
6152
6474
|
const missing = [];
|
|
6153
6475
|
for (const row of rows) {
|
|
@@ -6259,6 +6581,7 @@ var ManagedSessionsRepository = class {
|
|
|
6259
6581
|
updateStatusStmt;
|
|
6260
6582
|
getStmt;
|
|
6261
6583
|
listNonTerminalStmt;
|
|
6584
|
+
listRecoverableStmt;
|
|
6262
6585
|
deleteStmt;
|
|
6263
6586
|
constructor(db) {
|
|
6264
6587
|
this.upsertStmt = db.prepare(`
|
|
@@ -6311,6 +6634,13 @@ var ManagedSessionsRepository = class {
|
|
|
6311
6634
|
WHERE completed_at IS NULL
|
|
6312
6635
|
ORDER BY started_at ASC
|
|
6313
6636
|
`);
|
|
6637
|
+
this.listRecoverableStmt = db.prepare(`
|
|
6638
|
+
SELECT * FROM managed_sessions
|
|
6639
|
+
WHERE (completed_at IS NULL OR status_source = 'shutdown')
|
|
6640
|
+
AND status_updated_at >= @since
|
|
6641
|
+
ORDER BY status_updated_at DESC
|
|
6642
|
+
LIMIT @limit
|
|
6643
|
+
`);
|
|
6314
6644
|
this.deleteStmt = db.prepare("DELETE FROM managed_sessions WHERE session_id = ?");
|
|
6315
6645
|
}
|
|
6316
6646
|
/** Record a session at spawn, or refresh every field of an existing row. */
|
|
@@ -6362,6 +6692,14 @@ var ManagedSessionsRepository = class {
|
|
|
6362
6692
|
listNonTerminal() {
|
|
6363
6693
|
return this.listNonTerminalStmt.all();
|
|
6364
6694
|
}
|
|
6695
|
+
/**
|
|
6696
|
+
* Rows a restart could bring back: still open, or closed by our own shutdown,
|
|
6697
|
+
* and touched no longer ago than `sinceMs`. Newest first, capped — the caller
|
|
6698
|
+
* decides which of these actually deserve rehydrating (`shouldRehydrate`).
|
|
6699
|
+
*/
|
|
6700
|
+
listRecoverable({ sinceMs, limit }) {
|
|
6701
|
+
return this.listRecoverableStmt.all({ since: sinceMs, limit });
|
|
6702
|
+
}
|
|
6365
6703
|
delete(sessionId) {
|
|
6366
6704
|
this.deleteStmt.run(sessionId);
|
|
6367
6705
|
}
|
|
@@ -6497,6 +6835,54 @@ var SessionsRepository = class {
|
|
|
6497
6835
|
}
|
|
6498
6836
|
};
|
|
6499
6837
|
|
|
6838
|
+
// src/db/runtime-store.ts
|
|
6839
|
+
import Database2 from "better-sqlite3";
|
|
6840
|
+
var RuntimeStore = class _RuntimeStore {
|
|
6841
|
+
constructor(db) {
|
|
6842
|
+
this.db = db;
|
|
6843
|
+
}
|
|
6844
|
+
db;
|
|
6845
|
+
static open(dbPath, migrationsDir) {
|
|
6846
|
+
const db = new Database2(dbPath);
|
|
6847
|
+
db.pragma("journal_mode = WAL");
|
|
6848
|
+
runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
|
|
6849
|
+
return new _RuntimeStore(db);
|
|
6850
|
+
}
|
|
6851
|
+
getDatabase() {
|
|
6852
|
+
return this.db;
|
|
6853
|
+
}
|
|
6854
|
+
/**
|
|
6855
|
+
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
6856
|
+
*
|
|
6857
|
+
* Non-destructive by design: the source table is left in place so an older
|
|
6858
|
+
* streamer rolled back onto the same machine still finds its registry. Runs
|
|
6859
|
+
* only when this file's table is empty, so a second boot is a no-op rather
|
|
6860
|
+
* than a re-copy that would resurrect rows deleted since.
|
|
6861
|
+
*
|
|
6862
|
+
* Returns the number of rows copied.
|
|
6863
|
+
*/
|
|
6864
|
+
importLegacyManagedSessions(source) {
|
|
6865
|
+
const existing = this.db.prepare("SELECT COUNT(*) AS n FROM managed_sessions").get();
|
|
6866
|
+
if (existing.n > 0) return 0;
|
|
6867
|
+
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'managed_sessions'").get();
|
|
6868
|
+
if (!hasTable) return 0;
|
|
6869
|
+
const rows = source.prepare("SELECT * FROM managed_sessions").all();
|
|
6870
|
+
if (rows.length === 0) return 0;
|
|
6871
|
+
const columns = Object.keys(rows[0]);
|
|
6872
|
+
const insert = this.db.prepare(
|
|
6873
|
+
`INSERT OR IGNORE INTO managed_sessions (${columns.join(", ")})
|
|
6874
|
+
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
6875
|
+
);
|
|
6876
|
+
this.db.transaction((batch) => {
|
|
6877
|
+
for (const row of batch) insert.run(row);
|
|
6878
|
+
})(rows);
|
|
6879
|
+
return rows.length;
|
|
6880
|
+
}
|
|
6881
|
+
close() {
|
|
6882
|
+
this.db.close();
|
|
6883
|
+
}
|
|
6884
|
+
};
|
|
6885
|
+
|
|
6500
6886
|
// src/db/upload-records.ts
|
|
6501
6887
|
async function recordUpload(pool2, instanceId, row) {
|
|
6502
6888
|
if (!pool2) return;
|
|
@@ -6648,7 +7034,7 @@ function setCacheMetadata(repo, key, value) {
|
|
|
6648
7034
|
|
|
6649
7035
|
// src/services/cache-integrity/cacheIntegrityMonitor.ts
|
|
6650
7036
|
import { createHash as createHash3 } from "crypto";
|
|
6651
|
-
import { existsSync as
|
|
7037
|
+
import { existsSync as existsSync9 } from "fs";
|
|
6652
7038
|
|
|
6653
7039
|
// src/services/cache-integrity/alertStore.ts
|
|
6654
7040
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -6674,7 +7060,7 @@ function saveAlertState(state) {
|
|
|
6674
7060
|
}
|
|
6675
7061
|
|
|
6676
7062
|
// src/services/cache-integrity/backup.ts
|
|
6677
|
-
import { existsSync as
|
|
7063
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync4, statSync as statSync5, unlinkSync } from "fs";
|
|
6678
7064
|
import { join as join15 } from "path";
|
|
6679
7065
|
var DEFAULT_RETAIN = 3;
|
|
6680
7066
|
function retainCount() {
|
|
@@ -6696,7 +7082,7 @@ async function backupCacheDb(db, cacheDir) {
|
|
|
6696
7082
|
return { full, mtime: statSync5(full).mtimeMs };
|
|
6697
7083
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
6698
7084
|
for (const stale of backups.slice(retain)) {
|
|
6699
|
-
if (
|
|
7085
|
+
if (existsSync8(stale.full)) unlinkSync(stale.full);
|
|
6700
7086
|
}
|
|
6701
7087
|
return destPath;
|
|
6702
7088
|
}
|
|
@@ -6793,7 +7179,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6793
7179
|
* the pending record, back up on high severity, and broadcast the alert.
|
|
6794
7180
|
*/
|
|
6795
7181
|
async runDetection(detectedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
6796
|
-
const all = this.cache.listMissingFiles(
|
|
7182
|
+
const all = this.cache.listMissingFiles(existsSync9);
|
|
6797
7183
|
const missing = all.filter((m) => !this.ignoredIds.has(m.id));
|
|
6798
7184
|
if (missing.length === 0) {
|
|
6799
7185
|
if (this._pending) {
|
|
@@ -6897,7 +7283,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6897
7283
|
case "prune_all": {
|
|
6898
7284
|
await this.ensureBackup(pending);
|
|
6899
7285
|
const backupPath = pending.backupPath;
|
|
6900
|
-
const stillMissing = pending.missing.filter((m) => !
|
|
7286
|
+
const stillMissing = pending.missing.filter((m) => !existsSync9(m.filePath)).map((m) => m.id);
|
|
6901
7287
|
const pruned = this.cache.dropRowsById(stillMissing);
|
|
6902
7288
|
this.applyDeferredUnlinks();
|
|
6903
7289
|
this.clearPending();
|
|
@@ -7167,14 +7553,14 @@ function findSearchTarget(messages, query) {
|
|
|
7167
7553
|
}
|
|
7168
7554
|
|
|
7169
7555
|
// src/services/conversations/pruneAgentConversations.ts
|
|
7170
|
-
import { existsSync as
|
|
7556
|
+
import { existsSync as existsSync10 } from "fs";
|
|
7171
7557
|
function pruneAgentConversations(cache) {
|
|
7172
7558
|
const db = cache.getDatabase();
|
|
7173
7559
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
7174
7560
|
let pruned = 0;
|
|
7175
7561
|
let missing = 0;
|
|
7176
7562
|
for (const row of rows) {
|
|
7177
|
-
if (!
|
|
7563
|
+
if (!existsSync10(row.file_path)) {
|
|
7178
7564
|
missing += 1;
|
|
7179
7565
|
continue;
|
|
7180
7566
|
}
|
|
@@ -7514,7 +7900,8 @@ function contentStateForSession(args) {
|
|
|
7514
7900
|
status,
|
|
7515
7901
|
startedAt: args.startedAtOverride ?? args.session.startedAt.getTime(),
|
|
7516
7902
|
lastOutput: truncateLastOutput(args.session.lastOutput ?? ""),
|
|
7517
|
-
...args.serverLabel != null && { serverLabel: args.serverLabel }
|
|
7903
|
+
...args.serverLabel != null && { serverLabel: args.serverLabel },
|
|
7904
|
+
...args.session.sessionName != null && { sessionName: args.session.sessionName }
|
|
7518
7905
|
};
|
|
7519
7906
|
}
|
|
7520
7907
|
var LiveActivityNotifier = class {
|
|
@@ -7527,14 +7914,17 @@ var LiveActivityNotifier = class {
|
|
|
7527
7914
|
serverId;
|
|
7528
7915
|
serverLabel;
|
|
7529
7916
|
/**
|
|
7530
|
-
*
|
|
7917
|
+
* Sessions with a currently open (pushed) activity.
|
|
7531
7918
|
*
|
|
7532
|
-
*
|
|
7533
|
-
*
|
|
7534
|
-
*
|
|
7535
|
-
*
|
|
7919
|
+
* An activity opens on a `waiting_input → running` edge (the user sent a
|
|
7920
|
+
* prompt) and closes on the matching `running → waiting_input` edge (the
|
|
7921
|
+
* response, including any sub-agents, finished) — so this set is what makes
|
|
7922
|
+
* the notifier per-turn rather than per-session. A session's very first
|
|
7923
|
+
* `running` (right after spawn, before any user prompt) has no prior
|
|
7924
|
+
* `waiting_input` and therefore no edge, so it never opens an activity —
|
|
7925
|
+
* this is what keeps a fresh/idle session from pushing anything.
|
|
7536
7926
|
*/
|
|
7537
|
-
|
|
7927
|
+
openActivity = /* @__PURE__ */ new Map();
|
|
7538
7928
|
/**
|
|
7539
7929
|
* React to a session status change.
|
|
7540
7930
|
*
|
|
@@ -7542,34 +7932,22 @@ var LiveActivityNotifier = class {
|
|
|
7542
7932
|
* transition, so this returns a promise the caller may ignore and every error
|
|
7543
7933
|
* is logged rather than propagated.
|
|
7544
7934
|
*/
|
|
7545
|
-
async onStatusChange(session) {
|
|
7935
|
+
async onStatusChange(session, previousStatus) {
|
|
7546
7936
|
const status = toLiveActivityStatus(session.status);
|
|
7547
7937
|
try {
|
|
7548
7938
|
if (!status) {
|
|
7549
|
-
await this.endFor(session);
|
|
7939
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
7550
7940
|
return;
|
|
7551
7941
|
}
|
|
7552
|
-
if (
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
serverId: this.serverId,
|
|
7556
|
-
serverLabel: this.serverLabel
|
|
7557
|
-
});
|
|
7558
|
-
if (!contentState) return;
|
|
7559
|
-
const outcome = await this.sender.send({
|
|
7560
|
-
sessionId: session.id,
|
|
7561
|
-
event: "update",
|
|
7562
|
-
contentState
|
|
7563
|
-
});
|
|
7564
|
-
this.lastPushed.set(session.id, status);
|
|
7565
|
-
if (outcome.attempted > 0) {
|
|
7566
|
-
log4.info("live_activity.updated", {
|
|
7567
|
-
event: "live_activity.updated",
|
|
7568
|
-
sessionId: session.id,
|
|
7569
|
-
status,
|
|
7570
|
-
...outcome
|
|
7571
|
-
});
|
|
7942
|
+
if (status === "running" && previousStatus === "waiting_input") {
|
|
7943
|
+
await this.startTurn(session);
|
|
7944
|
+
return;
|
|
7572
7945
|
}
|
|
7946
|
+
if (status === "waiting_input" && previousStatus === "running") {
|
|
7947
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
7948
|
+
return;
|
|
7949
|
+
}
|
|
7950
|
+
await this.maybeSendName(session);
|
|
7573
7951
|
} catch (err) {
|
|
7574
7952
|
log4.error("live_activity.notify_failed", {
|
|
7575
7953
|
event: "live_activity.notify_failed",
|
|
@@ -7579,14 +7957,57 @@ var LiveActivityNotifier = class {
|
|
|
7579
7957
|
});
|
|
7580
7958
|
}
|
|
7581
7959
|
}
|
|
7960
|
+
async startTurn(session) {
|
|
7961
|
+
const contentState = contentStateForSession({
|
|
7962
|
+
session,
|
|
7963
|
+
serverId: this.serverId,
|
|
7964
|
+
serverLabel: this.serverLabel
|
|
7965
|
+
});
|
|
7966
|
+
if (!contentState) return;
|
|
7967
|
+
const outcome = await this.sender.send({
|
|
7968
|
+
sessionId: session.id,
|
|
7969
|
+
event: "update",
|
|
7970
|
+
contentState
|
|
7971
|
+
});
|
|
7972
|
+
this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
|
|
7973
|
+
if (outcome.attempted > 0) {
|
|
7974
|
+
log4.info("live_activity.updated", {
|
|
7975
|
+
event: "live_activity.updated",
|
|
7976
|
+
sessionId: session.id,
|
|
7977
|
+
status: contentState.status,
|
|
7978
|
+
...outcome
|
|
7979
|
+
});
|
|
7980
|
+
}
|
|
7981
|
+
}
|
|
7982
|
+
async maybeSendName(session) {
|
|
7983
|
+
const open2 = this.openActivity.get(session.id);
|
|
7984
|
+
if (!open2 || open2.sessionNameSent || session.sessionName == null) return;
|
|
7985
|
+
const contentState = contentStateForSession({
|
|
7986
|
+
session,
|
|
7987
|
+
serverId: this.serverId,
|
|
7988
|
+
serverLabel: this.serverLabel
|
|
7989
|
+
});
|
|
7990
|
+
if (!contentState) return;
|
|
7991
|
+
const outcome = await this.sender.send({
|
|
7992
|
+
sessionId: session.id,
|
|
7993
|
+
event: "update",
|
|
7994
|
+
contentState
|
|
7995
|
+
});
|
|
7996
|
+
open2.sessionNameSent = true;
|
|
7997
|
+
if (outcome.attempted > 0) {
|
|
7998
|
+
log4.info("live_activity.updated", {
|
|
7999
|
+
event: "live_activity.updated",
|
|
8000
|
+
sessionId: session.id,
|
|
8001
|
+
status: contentState.status,
|
|
8002
|
+
...outcome
|
|
8003
|
+
});
|
|
8004
|
+
}
|
|
8005
|
+
}
|
|
7582
8006
|
async endFor(session) {
|
|
7583
|
-
|
|
7584
|
-
|
|
8007
|
+
this.openActivity.delete(session.id);
|
|
8008
|
+
const status = toLiveActivityStatus(session.status);
|
|
7585
8009
|
const contentState = contentStateForSession({
|
|
7586
|
-
session: {
|
|
7587
|
-
...session,
|
|
7588
|
-
status: lastStatus === "waiting_input" ? "waiting_input" : "running"
|
|
7589
|
-
},
|
|
8010
|
+
session: { ...session, status: status ?? "waiting_input" },
|
|
7590
8011
|
serverId: this.serverId,
|
|
7591
8012
|
serverLabel: this.serverLabel
|
|
7592
8013
|
});
|
|
@@ -7600,9 +8021,9 @@ var LiveActivityNotifier = class {
|
|
|
7600
8021
|
});
|
|
7601
8022
|
}
|
|
7602
8023
|
}
|
|
7603
|
-
/** Drop cached state for a session, so a resume re-
|
|
8024
|
+
/** Drop cached state for a session, so a resume re-opens on its next turn. */
|
|
7604
8025
|
forget(sessionId) {
|
|
7605
|
-
this.
|
|
8026
|
+
this.openActivity.delete(sessionId);
|
|
7606
8027
|
}
|
|
7607
8028
|
};
|
|
7608
8029
|
|
|
@@ -7833,7 +8254,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
7833
8254
|
status,
|
|
7834
8255
|
startedAt,
|
|
7835
8256
|
lastOutput: session.lastOutput ?? "",
|
|
7836
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
8257
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
8258
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
7837
8259
|
};
|
|
7838
8260
|
try {
|
|
7839
8261
|
await this.deps.sender.send({
|
|
@@ -7893,7 +8315,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
7893
8315
|
// Carried through unchanged — the whole point of the renewal.
|
|
7894
8316
|
startedAt: args.startedAt,
|
|
7895
8317
|
lastOutput: truncateLastOutput(session.lastOutput ?? ""),
|
|
7896
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
8318
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
8319
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
7897
8320
|
},
|
|
7898
8321
|
now: args.now,
|
|
7899
8322
|
staleDate: args.startedAt + ACTIVITY_MAX_LIFETIME_MS
|
|
@@ -8051,6 +8474,88 @@ function resolveAnswer(pending, body) {
|
|
|
8051
8474
|
}
|
|
8052
8475
|
}
|
|
8053
8476
|
|
|
8477
|
+
// src/services/search/searchQuery.ts
|
|
8478
|
+
var DEFAULT_SEARCH_LIMIT = 50;
|
|
8479
|
+
var MAX_SEARCH_LIMIT = 200;
|
|
8480
|
+
var MAX_QUERY_LENGTH = 256;
|
|
8481
|
+
var SearchQueryError = class extends Error {
|
|
8482
|
+
constructor(message, code) {
|
|
8483
|
+
super(message);
|
|
8484
|
+
this.code = code;
|
|
8485
|
+
}
|
|
8486
|
+
code;
|
|
8487
|
+
};
|
|
8488
|
+
function intOr(raw, fallback) {
|
|
8489
|
+
if (raw === null) return fallback;
|
|
8490
|
+
const n = Number.parseInt(raw, 10);
|
|
8491
|
+
return Number.isFinite(n) ? n : fallback;
|
|
8492
|
+
}
|
|
8493
|
+
function parseSearchQuery(params) {
|
|
8494
|
+
const q = (params.get("q") ?? "").trim();
|
|
8495
|
+
if (!q) {
|
|
8496
|
+
throw new SearchQueryError("Missing query parameter: q", "invalid_query");
|
|
8497
|
+
}
|
|
8498
|
+
if (q.length > MAX_QUERY_LENGTH) {
|
|
8499
|
+
throw new SearchQueryError(`Query exceeds ${MAX_QUERY_LENGTH} characters`, "query_too_long");
|
|
8500
|
+
}
|
|
8501
|
+
const limit = Math.min(
|
|
8502
|
+
Math.max(intOr(params.get("limit"), DEFAULT_SEARCH_LIMIT), 1),
|
|
8503
|
+
MAX_SEARCH_LIMIT
|
|
8504
|
+
);
|
|
8505
|
+
const offset = Math.max(intOr(params.get("offset"), 0), 0);
|
|
8506
|
+
const filters = {};
|
|
8507
|
+
const provider = params.get("provider");
|
|
8508
|
+
if (provider !== null) {
|
|
8509
|
+
if (!isProviderName(provider)) {
|
|
8510
|
+
throw new SearchQueryError(`Unknown provider: ${provider}`, "invalid_filter");
|
|
8511
|
+
}
|
|
8512
|
+
filters.provider = provider;
|
|
8513
|
+
}
|
|
8514
|
+
const projectPath = params.get("projectPath");
|
|
8515
|
+
if (projectPath) filters.projectPath = projectPath;
|
|
8516
|
+
const branch = params.get("branch");
|
|
8517
|
+
if (branch) filters.branch = branch;
|
|
8518
|
+
for (const [key, field] of [
|
|
8519
|
+
["since", "since"],
|
|
8520
|
+
["until", "until"]
|
|
8521
|
+
]) {
|
|
8522
|
+
const raw = params.get(key);
|
|
8523
|
+
if (raw === null) continue;
|
|
8524
|
+
const ms = Date.parse(raw);
|
|
8525
|
+
if (Number.isNaN(ms)) {
|
|
8526
|
+
throw new SearchQueryError(`Invalid ${key}: expected an ISO 8601 date`, "invalid_filter");
|
|
8527
|
+
}
|
|
8528
|
+
filters[field] = ms;
|
|
8529
|
+
}
|
|
8530
|
+
if (filters.since != null && filters.until != null && filters.since > filters.until) {
|
|
8531
|
+
throw new SearchQueryError("`since` must not be after `until`", "invalid_filter");
|
|
8532
|
+
}
|
|
8533
|
+
return { q, limit, offset, filters };
|
|
8534
|
+
}
|
|
8535
|
+
function applyFilters(results, filters) {
|
|
8536
|
+
return results.filter((r) => {
|
|
8537
|
+
if (filters.provider && r.provider !== filters.provider) return false;
|
|
8538
|
+
if (filters.projectPath && r.projectPath !== filters.projectPath) return false;
|
|
8539
|
+
if (filters.branch && r.branch !== filters.branch) return false;
|
|
8540
|
+
if (filters.since != null || filters.until != null) {
|
|
8541
|
+
const ts = r.lastActivity == null ? Number.NaN : new Date(r.lastActivity).getTime();
|
|
8542
|
+
if (Number.isNaN(ts)) return false;
|
|
8543
|
+
if (filters.since != null && ts < filters.since) return false;
|
|
8544
|
+
if (filters.until != null && ts > filters.until) return false;
|
|
8545
|
+
}
|
|
8546
|
+
return true;
|
|
8547
|
+
});
|
|
8548
|
+
}
|
|
8549
|
+
function paginate(results, offset, limit) {
|
|
8550
|
+
const items = results.slice(offset, offset + limit);
|
|
8551
|
+
return {
|
|
8552
|
+
items,
|
|
8553
|
+
total: results.length,
|
|
8554
|
+
offset,
|
|
8555
|
+
hasMore: offset + items.length < results.length
|
|
8556
|
+
};
|
|
8557
|
+
}
|
|
8558
|
+
|
|
8054
8559
|
// src/services/sessions/conversationBusy.ts
|
|
8055
8560
|
import { statSync as statSync8 } from "fs";
|
|
8056
8561
|
var RESUME_BUSY_WINDOW_MS = 12e4;
|
|
@@ -8198,6 +8703,50 @@ async function reconcileSessions(rows, probe, currentInstanceId) {
|
|
|
8198
8703
|
return Promise.all(rows.map((row) => classifySession(row, probe, currentInstanceId)));
|
|
8199
8704
|
}
|
|
8200
8705
|
|
|
8706
|
+
// src/services/sessions/rehydrateSessions.ts
|
|
8707
|
+
var REHYDRATE_MAX = 25;
|
|
8708
|
+
var REHYDRATE_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8709
|
+
var AGENT_EXIT_SOURCES = /* @__PURE__ */ new Set(["exit", "process-exit"]);
|
|
8710
|
+
function shouldRehydrate(row, opts) {
|
|
8711
|
+
if (!opts.projectExists(row.project_path)) return false;
|
|
8712
|
+
if (opts.now - row.status_updated_at > REHYDRATE_WINDOW_MS) return false;
|
|
8713
|
+
if (AGENT_EXIT_SOURCES.has(row.status_source) && row.failure_reason == null) return false;
|
|
8714
|
+
return true;
|
|
8715
|
+
}
|
|
8716
|
+
function rowToStubSession(row) {
|
|
8717
|
+
return {
|
|
8718
|
+
id: row.session_id,
|
|
8719
|
+
provider: row.provider,
|
|
8720
|
+
projectPath: row.project_path,
|
|
8721
|
+
projectName: row.project_name,
|
|
8722
|
+
branch: row.branch,
|
|
8723
|
+
// No PTY exists for a stub, so this is the only truthful status.
|
|
8724
|
+
status: "idle",
|
|
8725
|
+
startedAt: new Date(row.started_at),
|
|
8726
|
+
completedAt: row.completed_at != null ? new Date(row.completed_at) : null,
|
|
8727
|
+
promptCount: row.prompt_count,
|
|
8728
|
+
lastOutput: "",
|
|
8729
|
+
rehydrated: true,
|
|
8730
|
+
...row.session_name != null && { sessionName: row.session_name },
|
|
8731
|
+
...row.project_id != null && { projectId: row.project_id },
|
|
8732
|
+
...row.bound_conversation_id != null && { boundConversationId: row.bound_conversation_id },
|
|
8733
|
+
...row.resumed_from_conversation_id != null && {
|
|
8734
|
+
resumedFromConversationId: row.resumed_from_conversation_id
|
|
8735
|
+
},
|
|
8736
|
+
...row.failure_reason != null && { failureReason: row.failure_reason },
|
|
8737
|
+
...row.last_activity_at != null && { lastActivityAt: new Date(row.last_activity_at) },
|
|
8738
|
+
// Only `shutdown` crosses over. It is the one registry source that is also a
|
|
8739
|
+
// wire StatusSource *and* that genuinely describes the `idle` above — the
|
|
8740
|
+
// streamer stopped this session. A crashed row still says `transition` over
|
|
8741
|
+
// a `running` status, and copying that here would attach observed-confidence
|
|
8742
|
+
// provenance to a status we derived at boot, so leave it unset instead.
|
|
8743
|
+
...row.status_source === "shutdown" && {
|
|
8744
|
+
statusSource: "shutdown",
|
|
8745
|
+
statusUpdatedAt: new Date(row.status_updated_at)
|
|
8746
|
+
}
|
|
8747
|
+
};
|
|
8748
|
+
}
|
|
8749
|
+
|
|
8201
8750
|
// src/types.ts
|
|
8202
8751
|
function confidenceForSource(source) {
|
|
8203
8752
|
return source === "timeout-fallback" || source === "quiet-fallback" ? "inferred" : "observed";
|
|
@@ -8345,14 +8894,15 @@ function managedToResponse(s, ptyAttached) {
|
|
|
8345
8894
|
// Lifecycle for a session this run knows about. `attached` while we hold
|
|
8346
8895
|
// its PTY; once the PTY is gone the session is terminal from this run's
|
|
8347
8896
|
// perspective — `failed` when it recorded a reason, else `completed`.
|
|
8348
|
-
//
|
|
8349
|
-
//
|
|
8350
|
-
//
|
|
8351
|
-
|
|
8352
|
-
|
|
8897
|
+
// A `rehydrated` stub is the exception: the boot rehydrator seeded it from
|
|
8898
|
+
// the durable registry, so it is a previous run's session with no process
|
|
8899
|
+
// behind it — `resumable`, and `historical` rather than `managed`
|
|
8900
|
+
// (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).
|
|
8901
|
+
lifecycle: ptyAttached ? "attached" : s.rehydrated ? "resumable" : s.failureReason != null ? "failed" : "completed",
|
|
8902
|
+
lifecycleSource: ptyAttached ? "spawn" : s.rehydrated ? "reconcile" : "exit",
|
|
8353
8903
|
// We spawned it, so `status` is the authoritative signal — no inferred
|
|
8354
8904
|
// `activity` is attached for managed sessions.
|
|
8355
|
-
ownership: "managed",
|
|
8905
|
+
ownership: s.rehydrated ? "historical" : "managed",
|
|
8356
8906
|
projectPath: s.projectPath,
|
|
8357
8907
|
projectName: s.projectName,
|
|
8358
8908
|
branch: s.branch,
|
|
@@ -8753,6 +9303,8 @@ var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
|
8753
9303
|
var GRACE_MAX_DEFERS = 4;
|
|
8754
9304
|
var IDLE_REAP_AFTER_MS = 6 * 60 * 60 * 1e3;
|
|
8755
9305
|
var IDLE_REAP_SWEEP_MS = 5 * 60 * 1e3;
|
|
9306
|
+
var SEARCH_OVERFETCH = 4;
|
|
9307
|
+
var SEARCH_MAX_SCAN = 1e3;
|
|
8756
9308
|
var RESUME_DISCOVERY_TIMEOUT_MS = 750;
|
|
8757
9309
|
var DISCOVERY_TTL_MS = 15e3;
|
|
8758
9310
|
var ADOPT_KILL_TIMEOUT_MS = 5e3;
|
|
@@ -8940,10 +9492,13 @@ var StreamerServer = class {
|
|
|
8940
9492
|
projectsRepo = null;
|
|
8941
9493
|
conversationsRepo = null;
|
|
8942
9494
|
sessionsRepo = null;
|
|
8943
|
-
// Durable session registry (C1 Phase 2). Null when
|
|
8944
|
-
//
|
|
8945
|
-
// taking the server down with it, so every write goes through `?.`.
|
|
9495
|
+
// Durable session registry (C1 Phase 2). Null when runtime.db failed to open
|
|
9496
|
+
// — persistence degrades to today's in-memory-only behaviour rather than
|
|
9497
|
+
// taking the server down with it, so every write goes through `?.`. Note the
|
|
9498
|
+
// handle is runtime.db, NOT the conversation cache: a cache failure used to
|
|
9499
|
+
// null this repo and silently disable all session persistence.
|
|
8946
9500
|
managedSessionsRepo = null;
|
|
9501
|
+
runtimeStore = null;
|
|
8947
9502
|
// Identifies this streamer run. A registry row carrying a different id is a
|
|
8948
9503
|
// session that outlived the process that started it.
|
|
8949
9504
|
streamerInstanceId = randomUUID5();
|
|
@@ -8962,6 +9517,7 @@ var StreamerServer = class {
|
|
|
8962
9517
|
liveActivityRenewal = null;
|
|
8963
9518
|
discoveryCache = null;
|
|
8964
9519
|
cacheDir;
|
|
9520
|
+
runtimeDbPath;
|
|
8965
9521
|
tailSize;
|
|
8966
9522
|
directoryDebounceMs;
|
|
8967
9523
|
// Trailing-debounced trigger that flags the scanner stale after a quiet
|
|
@@ -9008,6 +9564,7 @@ var StreamerServer = class {
|
|
|
9008
9564
|
this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
|
|
9009
9565
|
this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
9010
9566
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join18(homedir9(), ".threadbase", "cache");
|
|
9567
|
+
this.runtimeDbPath = config.runtimeDbPath ?? process.env.THREADBASE_RUNTIME_DB ?? join18(process.env.THREADBASE_CONFIG_DIR ?? join18(homedir9(), ".threadbase"), "runtime.db");
|
|
9011
9568
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
9012
9569
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
9013
9570
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -9178,6 +9735,7 @@ var StreamerServer = class {
|
|
|
9178
9735
|
if (resp) this.wsHub.broadcast({ type: "session_ready", session: resp });
|
|
9179
9736
|
},
|
|
9180
9737
|
onStatusChange: (session) => {
|
|
9738
|
+
const previousStatus = this.sessionStore.getManaged(session.id)?.status;
|
|
9181
9739
|
this.sessionStore.updateManaged(session.id, {
|
|
9182
9740
|
status: session.status,
|
|
9183
9741
|
completedAt: session.completedAt,
|
|
@@ -9232,7 +9790,7 @@ var StreamerServer = class {
|
|
|
9232
9790
|
if (resp) {
|
|
9233
9791
|
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
9234
9792
|
}
|
|
9235
|
-
void this.liveActivityNotifier?.onStatusChange(session);
|
|
9793
|
+
void this.liveActivityNotifier?.onStatusChange(session, previousStatus);
|
|
9236
9794
|
this.sessionStatusBus.emit(`status:${session.id}`, session.status);
|
|
9237
9795
|
}
|
|
9238
9796
|
});
|
|
@@ -9283,6 +9841,7 @@ var StreamerServer = class {
|
|
|
9283
9841
|
conversationsRepo: () => this.conversationsRepo,
|
|
9284
9842
|
sessionsRepo: () => this.sessionsRepo,
|
|
9285
9843
|
cacheMetadataRepo: () => this.cacheMetadataRepo,
|
|
9844
|
+
runtimeStore: () => this.runtimeStore,
|
|
9286
9845
|
ptyAttachedIds: () => this.ptyAttachedIds(),
|
|
9287
9846
|
handleListSessions: (url, res) => this.handleListSessions(url, res),
|
|
9288
9847
|
handleSessionsCount: (res) => this.handleSessionsCount(res),
|
|
@@ -9521,14 +10080,14 @@ var StreamerServer = class {
|
|
|
9521
10080
|
}
|
|
9522
10081
|
this.apnsClient = new ApnsClient(creds);
|
|
9523
10082
|
const sender = new LiveActivitySender(this.apnsClient, pushRepo);
|
|
9524
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ??
|
|
9525
|
-
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId,
|
|
10083
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? hostname3();
|
|
10084
|
+
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, hostname3());
|
|
9526
10085
|
this.liveActivityRenewal = new LiveActivityRenewalScheduler({
|
|
9527
10086
|
repo: pushRepo,
|
|
9528
10087
|
sender,
|
|
9529
10088
|
sessionStore: this.sessionStore,
|
|
9530
10089
|
serverId,
|
|
9531
|
-
serverLabel:
|
|
10090
|
+
serverLabel: hostname3()
|
|
9532
10091
|
});
|
|
9533
10092
|
this.liveActivityRenewal.start();
|
|
9534
10093
|
this.log.info("Live Activity push enabled", {
|
|
@@ -9582,6 +10141,58 @@ var StreamerServer = class {
|
|
|
9582
10141
|
}
|
|
9583
10142
|
return verdicts;
|
|
9584
10143
|
}
|
|
10144
|
+
/**
|
|
10145
|
+
* Seed the session list with what previous runs left behind (persistence plan
|
|
10146
|
+
* Phase 1, gaps G1/G2/G8).
|
|
10147
|
+
*
|
|
10148
|
+
* Reconciliation classifies rows and stops there; a verdict is overlaid onto a
|
|
10149
|
+
* SessionResponse that already exists, and after a clean restart none does —
|
|
10150
|
+
* `SessionStore` starts empty. So the user's session did not become
|
|
10151
|
+
* `resumable`, it became *absent*. This is the half that puts it back.
|
|
10152
|
+
*
|
|
10153
|
+
* The seeded stubs hold no PTY and are never handed to `LiveSessionManager`,
|
|
10154
|
+
* so `reapIdleSessions` and `startGraceTimer` — both of which iterate
|
|
10155
|
+
* `ptyManager.listSessions()` — cannot observe them. A later resume calls
|
|
10156
|
+
* `sessionStore.addManaged` with the real session, which overwrites the stub
|
|
10157
|
+
* by id rather than duplicating it.
|
|
10158
|
+
*/
|
|
10159
|
+
rehydratePreviousSessions(verdicts) {
|
|
10160
|
+
if (!this.featureFlags.sessionRehydration || !this.managedSessionsRepo) return;
|
|
10161
|
+
try {
|
|
10162
|
+
const now = Date.now();
|
|
10163
|
+
const rows = this.managedSessionsRepo.listRecoverable({
|
|
10164
|
+
sinceMs: now - REHYDRATE_WINDOW_MS,
|
|
10165
|
+
limit: REHYDRATE_MAX + 1
|
|
10166
|
+
});
|
|
10167
|
+
const truncated = rows.length > REHYDRATE_MAX;
|
|
10168
|
+
const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;
|
|
10169
|
+
if (candidates.length === 0) return;
|
|
10170
|
+
const lifecycleByVerdict = new Map(verdicts.map((v) => [v.sessionId, v.lifecycle]));
|
|
10171
|
+
let rehydrated = 0;
|
|
10172
|
+
for (const row of candidates) {
|
|
10173
|
+
if (this.sessionStore.getManaged(row.session_id)) continue;
|
|
10174
|
+
if (!shouldRehydrate(row, { now, projectExists: existsSync11 })) continue;
|
|
10175
|
+
this.sessionStore.addManaged(rowToStubSession(row));
|
|
10176
|
+
this.sessionLifecycles.set(
|
|
10177
|
+
row.session_id,
|
|
10178
|
+
lifecycleByVerdict.get(row.session_id) ?? "resumable"
|
|
10179
|
+
);
|
|
10180
|
+
if (row.completed_at != null) this.selfPtyEndedAt.set(row.session_id, row.completed_at);
|
|
10181
|
+
rehydrated++;
|
|
10182
|
+
}
|
|
10183
|
+
this.log.info(`[rehydrate] recovered ${rehydrated} session(s) from the registry`, {
|
|
10184
|
+
event: "sessions.rehydrated",
|
|
10185
|
+
rehydrated,
|
|
10186
|
+
skipped: candidates.length - rehydrated,
|
|
10187
|
+
truncated
|
|
10188
|
+
});
|
|
10189
|
+
} catch (err) {
|
|
10190
|
+
this.log.warn("[rehydrate] failed to rehydrate previous sessions", {
|
|
10191
|
+
event: "sessions.rehydrate_failed",
|
|
10192
|
+
err
|
|
10193
|
+
});
|
|
10194
|
+
}
|
|
10195
|
+
}
|
|
9585
10196
|
/**
|
|
9586
10197
|
* Pick a token guaranteed to appear in the spawned process's argv, for the
|
|
9587
10198
|
* reconciler's pid-reuse guard.
|
|
@@ -9815,6 +10426,17 @@ var StreamerServer = class {
|
|
|
9815
10426
|
port,
|
|
9816
10427
|
event: "server.listening"
|
|
9817
10428
|
});
|
|
10429
|
+
try {
|
|
10430
|
+
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
10431
|
+
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
10432
|
+
} catch (err) {
|
|
10433
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10434
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
10435
|
+
this.log.error(
|
|
10436
|
+
`Runtime store failed to open \u2014 session persistence DISABLED; sessions will not survive a restart.` + (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : "") + ` (${message})`,
|
|
10437
|
+
{ error: message, abiMismatch, path: this.runtimeDbPath, event: "runtime.open_failed" }
|
|
10438
|
+
);
|
|
10439
|
+
}
|
|
9818
10440
|
try {
|
|
9819
10441
|
this.cache = ConversationCache.open(
|
|
9820
10442
|
join18(this.cacheDir, "cache.db"),
|
|
@@ -9842,8 +10464,20 @@ var StreamerServer = class {
|
|
|
9842
10464
|
this.projectsRepo = new ProjectsRepository(db);
|
|
9843
10465
|
this.conversationsRepo = new ConversationsRepository(this.cache);
|
|
9844
10466
|
this.sessionsRepo = new SessionsRepository(this.sessionStore);
|
|
9845
|
-
|
|
9846
|
-
|
|
10467
|
+
try {
|
|
10468
|
+
const copied = this.runtimeStore?.importLegacyManagedSessions(db) ?? 0;
|
|
10469
|
+
if (copied > 0) {
|
|
10470
|
+
this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
|
|
10471
|
+
copied,
|
|
10472
|
+
event: "runtime.legacy_import"
|
|
10473
|
+
});
|
|
10474
|
+
}
|
|
10475
|
+
} catch (err) {
|
|
10476
|
+
this.log.warn("[registry] legacy managed_sessions copy failed", {
|
|
10477
|
+
event: "runtime.legacy_import_failed",
|
|
10478
|
+
err
|
|
10479
|
+
});
|
|
10480
|
+
}
|
|
9847
10481
|
this.cacheMetadataRepo = new CacheMetadataRepository(db);
|
|
9848
10482
|
this.pushRepo = new PushRepository(db);
|
|
9849
10483
|
this.devicesRepo = new DevicesRepository(db);
|
|
@@ -9867,7 +10501,7 @@ var StreamerServer = class {
|
|
|
9867
10501
|
this.fileWatcher.watchDirectory(dir);
|
|
9868
10502
|
}
|
|
9869
10503
|
for (const dir of this.codexRoots) {
|
|
9870
|
-
if (!
|
|
10504
|
+
if (!existsSync11(dir)) continue;
|
|
9871
10505
|
this.fileWatcher.watchDirectory(dir);
|
|
9872
10506
|
}
|
|
9873
10507
|
} catch (err) {
|
|
@@ -9879,6 +10513,7 @@ var StreamerServer = class {
|
|
|
9879
10513
|
);
|
|
9880
10514
|
this.scannerPersistenceDisabled = true;
|
|
9881
10515
|
}
|
|
10516
|
+
void this.reconcilePreviousSessions().then((v) => this.rehydratePreviousSessions(v));
|
|
9882
10517
|
if (this.skipStartupWarmup) {
|
|
9883
10518
|
this.log.debug?.("startup warm-up scan skipped (skipStartupWarmup)", {
|
|
9884
10519
|
event: "cache.warmup_skipped"
|
|
@@ -10086,6 +10721,7 @@ var StreamerServer = class {
|
|
|
10086
10721
|
this.allScanners.clear();
|
|
10087
10722
|
this.scanner = null;
|
|
10088
10723
|
this.cache?.close();
|
|
10724
|
+
this.runtimeStore?.close();
|
|
10089
10725
|
this.ptyManager.dispose();
|
|
10090
10726
|
this.fileWatcher.dispose();
|
|
10091
10727
|
this.externalTails.clear();
|
|
@@ -10140,7 +10776,7 @@ var StreamerServer = class {
|
|
|
10140
10776
|
}
|
|
10141
10777
|
let body;
|
|
10142
10778
|
try {
|
|
10143
|
-
body = await
|
|
10779
|
+
body = await readBody2(req);
|
|
10144
10780
|
} catch (err) {
|
|
10145
10781
|
const message = err instanceof Error ? err.message : "Invalid body";
|
|
10146
10782
|
json(res, 400, { error: message });
|
|
@@ -10186,7 +10822,7 @@ var StreamerServer = class {
|
|
|
10186
10822
|
nonce: sealed.nonce,
|
|
10187
10823
|
ephemeralPublicKey: sealed.ephemeralPublicKey,
|
|
10188
10824
|
publicUrl: this.publicUrl,
|
|
10189
|
-
machineName:
|
|
10825
|
+
machineName: hostname3(),
|
|
10190
10826
|
...device && {
|
|
10191
10827
|
deviceId: device.deviceId,
|
|
10192
10828
|
deviceToken: device.deviceToken,
|
|
@@ -10533,7 +11169,8 @@ var StreamerServer = class {
|
|
|
10533
11169
|
}
|
|
10534
11170
|
handleSessionsCount(res) {
|
|
10535
11171
|
if (this.rejectIfWarmingUp(res)) return;
|
|
10536
|
-
|
|
11172
|
+
const total = this.sessionStore.list(this.ptyAttachedIds()).filter((s) => s.ownership !== "historical").length;
|
|
11173
|
+
json(res, 200, { total });
|
|
10537
11174
|
}
|
|
10538
11175
|
handleGetRecentSessions(url, res) {
|
|
10539
11176
|
if (this.rejectIfWarmingUp(res)) return;
|
|
@@ -10721,15 +11358,15 @@ var StreamerServer = class {
|
|
|
10721
11358
|
findJsonlPath(uuid) {
|
|
10722
11359
|
const filename = `${uuid}.jsonl`;
|
|
10723
11360
|
for (const projectsDir of this.projectsDirs()) {
|
|
10724
|
-
if (!
|
|
11361
|
+
if (!existsSync11(projectsDir)) continue;
|
|
10725
11362
|
for (const dir of readdirSync6(projectsDir)) {
|
|
10726
11363
|
const fp = join18(projectsDir, dir, filename);
|
|
10727
|
-
if (
|
|
11364
|
+
if (existsSync11(fp)) return fp;
|
|
10728
11365
|
const projectDir = join18(projectsDir, dir);
|
|
10729
11366
|
try {
|
|
10730
11367
|
for (const sub of readdirSync6(projectDir)) {
|
|
10731
11368
|
const subagentPath = join18(projectDir, sub, "subagents", filename);
|
|
10732
|
-
if (
|
|
11369
|
+
if (existsSync11(subagentPath)) return subagentPath;
|
|
10733
11370
|
}
|
|
10734
11371
|
} catch {
|
|
10735
11372
|
}
|
|
@@ -11266,7 +11903,7 @@ var StreamerServer = class {
|
|
|
11266
11903
|
}
|
|
11267
11904
|
let body;
|
|
11268
11905
|
try {
|
|
11269
|
-
body = await
|
|
11906
|
+
body = await readBody2(req);
|
|
11270
11907
|
} catch {
|
|
11271
11908
|
res.setHeader("Accept-Query", "application/json");
|
|
11272
11909
|
json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
@@ -11304,17 +11941,27 @@ var StreamerServer = class {
|
|
|
11304
11941
|
});
|
|
11305
11942
|
}
|
|
11306
11943
|
async handleSearch(url, res) {
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
|
|
11944
|
+
let parsed;
|
|
11945
|
+
try {
|
|
11946
|
+
parsed = parseSearchQuery(url.searchParams);
|
|
11947
|
+
} catch (err) {
|
|
11948
|
+
if (err instanceof SearchQueryError) {
|
|
11949
|
+
json(res, 400, { error: err.message, code: err.code });
|
|
11950
|
+
return;
|
|
11951
|
+
}
|
|
11952
|
+
throw err;
|
|
11311
11953
|
}
|
|
11312
|
-
const
|
|
11954
|
+
const { q, limit, offset, filters } = parsed;
|
|
11955
|
+
const startedAt = Date.now();
|
|
11313
11956
|
const scanner = await this.getScanner();
|
|
11314
11957
|
const results = await search(
|
|
11315
11958
|
q,
|
|
11316
11959
|
{
|
|
11317
|
-
|
|
11960
|
+
// Fetch beyond the requested page: filters below are applied AFTER the
|
|
11961
|
+
// scanner returns, so slicing at `limit` here would drop results that a
|
|
11962
|
+
// later page should contain. Bounded so a broad query cannot pull an
|
|
11963
|
+
// unbounded set into memory.
|
|
11964
|
+
limit: Math.min(offset + limit * SEARCH_OVERFETCH, SEARCH_MAX_SCAN),
|
|
11318
11965
|
include: "conversations",
|
|
11319
11966
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
11320
11967
|
...this.codexScanOpts()
|
|
@@ -11338,13 +11985,24 @@ var StreamerServer = class {
|
|
|
11338
11985
|
lastActivity: r.meta.timestamp,
|
|
11339
11986
|
firstMessage: r.meta.firstMessage ?? void 0,
|
|
11340
11987
|
lastMessage: r.meta.lastMessage ?? void 0,
|
|
11341
|
-
provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER
|
|
11988
|
+
provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER,
|
|
11989
|
+
// The scanner already computes relevance and match snippets; the previous
|
|
11990
|
+
// adapter discarded both, so results arrived in an unexplained order with
|
|
11991
|
+
// no indication of WHY anything matched.
|
|
11992
|
+
score: r.score,
|
|
11993
|
+
matches: Array.isArray(r.matches) ? r.matches.map((m) => ({
|
|
11994
|
+
field: m.field,
|
|
11995
|
+
snippet: m.snippet
|
|
11996
|
+
})) : []
|
|
11342
11997
|
}));
|
|
11998
|
+
const page = paginate(applyFilters(adapted, filters), offset, limit);
|
|
11343
11999
|
json(res, 200, {
|
|
11344
|
-
conversations:
|
|
11345
|
-
hasMore:
|
|
11346
|
-
offset:
|
|
11347
|
-
total:
|
|
12000
|
+
conversations: page.items,
|
|
12001
|
+
hasMore: page.hasMore,
|
|
12002
|
+
offset: page.offset,
|
|
12003
|
+
total: page.total,
|
|
12004
|
+
// Query timing, so a slow search is diagnosable rather than merely felt.
|
|
12005
|
+
tookMs: Date.now() - startedAt
|
|
11348
12006
|
});
|
|
11349
12007
|
}
|
|
11350
12008
|
async handleListSessions(url, res) {
|
|
@@ -11390,7 +12048,7 @@ var StreamerServer = class {
|
|
|
11390
12048
|
if (this.rejectIfWarmingUp(res)) return;
|
|
11391
12049
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
11392
12050
|
if (session) {
|
|
11393
|
-
if (!
|
|
12051
|
+
if (!existsSync11(session.projectPath)) {
|
|
11394
12052
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
11395
12053
|
}
|
|
11396
12054
|
const reconciled = this.withReconciledLifecycle([session])[0];
|
|
@@ -11417,7 +12075,7 @@ var StreamerServer = class {
|
|
|
11417
12075
|
json(res, 404, { error: "Session not found" });
|
|
11418
12076
|
}
|
|
11419
12077
|
async handleResume(req, res) {
|
|
11420
|
-
const body = await
|
|
12078
|
+
const body = await readBody2(req);
|
|
11421
12079
|
const sessionId = body.sessionId ?? body.conversationId;
|
|
11422
12080
|
if (!sessionId) {
|
|
11423
12081
|
json(res, 400, { error: "Missing sessionId" });
|
|
@@ -11549,7 +12207,7 @@ var StreamerServer = class {
|
|
|
11549
12207
|
return;
|
|
11550
12208
|
}
|
|
11551
12209
|
if (this.agentConfig.enabled) {
|
|
11552
|
-
const body2 = await
|
|
12210
|
+
const body2 = await readBody2(req);
|
|
11553
12211
|
const cache = this.cache;
|
|
11554
12212
|
if (!cache) {
|
|
11555
12213
|
json(res, 503, {
|
|
@@ -11568,7 +12226,7 @@ var StreamerServer = class {
|
|
|
11568
12226
|
json(res, result.status, result.body);
|
|
11569
12227
|
return;
|
|
11570
12228
|
}
|
|
11571
|
-
const body = await
|
|
12229
|
+
const body = await readBody2(req);
|
|
11572
12230
|
const { input, keys } = body;
|
|
11573
12231
|
let idempotencyKey;
|
|
11574
12232
|
try {
|
|
@@ -11732,7 +12390,7 @@ var StreamerServer = class {
|
|
|
11732
12390
|
});
|
|
11733
12391
|
}
|
|
11734
12392
|
async handleSendAnswer(sessionId, req, res) {
|
|
11735
|
-
const body = await
|
|
12393
|
+
const body = await readBody2(req);
|
|
11736
12394
|
const pending = this.pendingQuestions.get(sessionId);
|
|
11737
12395
|
const resolution = resolveAnswer(pending, body);
|
|
11738
12396
|
if (!resolution.ok) {
|
|
@@ -11761,7 +12419,7 @@ var StreamerServer = class {
|
|
|
11761
12419
|
json(res, 400, { error: "Session has no project path" });
|
|
11762
12420
|
return;
|
|
11763
12421
|
}
|
|
11764
|
-
const body = await
|
|
12422
|
+
const body = await readBody2(req);
|
|
11765
12423
|
const { filename, mimeType, dataBase64 } = body ?? {};
|
|
11766
12424
|
if (typeof filename !== "string" || typeof mimeType !== "string" || typeof dataBase64 !== "string") {
|
|
11767
12425
|
json(res, 400, { error: "Missing filename, mimeType, or dataBase64" });
|
|
@@ -11963,7 +12621,7 @@ var StreamerServer = class {
|
|
|
11963
12621
|
return;
|
|
11964
12622
|
}
|
|
11965
12623
|
if (this.agentConfig.enabled) {
|
|
11966
|
-
const body2 = await
|
|
12624
|
+
const body2 = await readBody2(req);
|
|
11967
12625
|
const result = await handleStartAgentSession(body2, {
|
|
11968
12626
|
sessionStore: this.sessionStore,
|
|
11969
12627
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
@@ -11977,7 +12635,7 @@ var StreamerServer = class {
|
|
|
11977
12635
|
}
|
|
11978
12636
|
return;
|
|
11979
12637
|
}
|
|
11980
|
-
const body = await
|
|
12638
|
+
const body = await readBody2(req);
|
|
11981
12639
|
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
11982
12640
|
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
11983
12641
|
json(res, 400, { error: "Invalid provider" });
|
|
@@ -12149,8 +12807,8 @@ var StreamerServer = class {
|
|
|
12149
12807
|
cleanup();
|
|
12150
12808
|
return;
|
|
12151
12809
|
}
|
|
12152
|
-
let resolvedFilePath =
|
|
12153
|
-
if (!resolvedFilePath &&
|
|
12810
|
+
let resolvedFilePath = existsSync11(filePath) ? filePath : null;
|
|
12811
|
+
if (!resolvedFilePath && existsSync11(projectsDir)) {
|
|
12154
12812
|
try {
|
|
12155
12813
|
const now = Date.now();
|
|
12156
12814
|
const match = readdirSync6(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync9(join18(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
@@ -12246,7 +12904,7 @@ var StreamerServer = class {
|
|
|
12246
12904
|
);
|
|
12247
12905
|
for (const root of this.codexRoots) {
|
|
12248
12906
|
const sessionsDir = join18(root, dateDir);
|
|
12249
|
-
if (!
|
|
12907
|
+
if (!existsSync11(sessionsDir)) continue;
|
|
12250
12908
|
let candidateFiles;
|
|
12251
12909
|
try {
|
|
12252
12910
|
candidateFiles = readdirSync6(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -12335,7 +12993,7 @@ var StreamerServer = class {
|
|
|
12335
12993
|
});
|
|
12336
12994
|
return;
|
|
12337
12995
|
}
|
|
12338
|
-
const body = await
|
|
12996
|
+
const body = await readBody2(req);
|
|
12339
12997
|
const { path: relativePath, name } = body;
|
|
12340
12998
|
if (!name || typeof name !== "string") {
|
|
12341
12999
|
json(res, 400, { error: "Missing name field" });
|
|
@@ -12365,7 +13023,7 @@ var StreamerServer = class {
|
|
|
12365
13023
|
}
|
|
12366
13024
|
let parsed;
|
|
12367
13025
|
try {
|
|
12368
|
-
parsed = await
|
|
13026
|
+
parsed = await readBody2(req);
|
|
12369
13027
|
} catch {
|
|
12370
13028
|
json(res, 400, { error: "Invalid JSON" });
|
|
12371
13029
|
return;
|
|
@@ -12422,7 +13080,7 @@ var StreamerServer = class {
|
|
|
12422
13080
|
}
|
|
12423
13081
|
let parsed;
|
|
12424
13082
|
try {
|
|
12425
|
-
parsed = await
|
|
13083
|
+
parsed = await readBody2(req);
|
|
12426
13084
|
} catch {
|
|
12427
13085
|
json(res, 400, { error: "Invalid JSON" });
|
|
12428
13086
|
return;
|
|
@@ -12481,7 +13139,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
|
|
|
12481
13139
|
}
|
|
12482
13140
|
function classifyResumability(cwd) {
|
|
12483
13141
|
if (!cwd) return { resumable: true };
|
|
12484
|
-
if (
|
|
13142
|
+
if (existsSync11(cwd)) return { resumable: true };
|
|
12485
13143
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
12486
13144
|
return {
|
|
12487
13145
|
resumable: false,
|
|
@@ -12599,7 +13257,7 @@ function parseSessionListQuery(url) {
|
|
|
12599
13257
|
const cursor = url.searchParams.get("cursor") ?? void 0;
|
|
12600
13258
|
return { query: { limit, sortBy, order, status, cursor } };
|
|
12601
13259
|
}
|
|
12602
|
-
function
|
|
13260
|
+
function readBody2(req) {
|
|
12603
13261
|
return new Promise((resolve2, reject) => {
|
|
12604
13262
|
const chunks = [];
|
|
12605
13263
|
req.on("data", (chunk) => chunks.push(chunk));
|