@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.cjs
CHANGED
|
@@ -500,6 +500,12 @@ var FEATURE_FLAGS = [
|
|
|
500
500
|
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.",
|
|
501
501
|
default: false,
|
|
502
502
|
env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: "sessionRehydration",
|
|
506
|
+
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.",
|
|
507
|
+
default: true,
|
|
508
|
+
env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
|
|
503
509
|
}
|
|
504
510
|
];
|
|
505
511
|
function findFeatureFlag(id) {
|
|
@@ -1936,6 +1942,12 @@ function detectShellPrompt(lines) {
|
|
|
1936
1942
|
return null;
|
|
1937
1943
|
}
|
|
1938
1944
|
|
|
1945
|
+
// src/utils/deriveSessionName.ts
|
|
1946
|
+
function deriveSessionName(firstMessageText) {
|
|
1947
|
+
const firstLine = firstMessageText.split("\n", 1)[0]?.trim() ?? "";
|
|
1948
|
+
return firstLine.slice(0, 80);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1939
1951
|
// src/pty-manager.ts
|
|
1940
1952
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1941
1953
|
var INPUT_HISTORY_MAX2 = 50;
|
|
@@ -2409,6 +2421,10 @@ var PTYManager = class {
|
|
|
2409
2421
|
if (session.inputHistory.length > INPUT_HISTORY_MAX2) {
|
|
2410
2422
|
session.inputHistory.shift();
|
|
2411
2423
|
}
|
|
2424
|
+
if (session.firstMessageText === void 0) {
|
|
2425
|
+
session.firstMessageText = text;
|
|
2426
|
+
session.sessionName = deriveSessionName(text);
|
|
2427
|
+
}
|
|
2412
2428
|
this.onUserMessage?.(session.id, text, ts);
|
|
2413
2429
|
}
|
|
2414
2430
|
getSession(sessionId) {
|
|
@@ -2674,7 +2690,9 @@ function toPublicSession2(s) {
|
|
|
2674
2690
|
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
2675
2691
|
...s.statusSource != null && { statusSource: s.statusSource },
|
|
2676
2692
|
...s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt },
|
|
2677
|
-
...s.filePath != null && { filePath: s.filePath }
|
|
2693
|
+
...s.filePath != null && { filePath: s.filePath },
|
|
2694
|
+
...s.sessionName != null && { sessionName: s.sessionName },
|
|
2695
|
+
...s.firstMessageText != null && { firstMessageText: s.firstMessageText }
|
|
2678
2696
|
};
|
|
2679
2697
|
}
|
|
2680
2698
|
function stripAnsi2(str) {
|
|
@@ -3061,10 +3079,10 @@ var import_client = require("@temporalio/client");
|
|
|
3061
3079
|
var import_scanner3 = require("@threadbase-sh/scanner");
|
|
3062
3080
|
var import_crypto11 = require("crypto");
|
|
3063
3081
|
var import_events = require("events");
|
|
3064
|
-
var
|
|
3082
|
+
var import_fs19 = require("fs");
|
|
3065
3083
|
var import_promises7 = require("fs/promises");
|
|
3066
3084
|
var import_http = require("http");
|
|
3067
|
-
var
|
|
3085
|
+
var import_os10 = require("os");
|
|
3068
3086
|
var import_path18 = require("path");
|
|
3069
3087
|
var import_readline = require("readline");
|
|
3070
3088
|
|
|
@@ -3324,7 +3342,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
3324
3342
|
}
|
|
3325
3343
|
|
|
3326
3344
|
// src/api/app.ts
|
|
3327
|
-
var
|
|
3345
|
+
var import_hono18 = require("hono");
|
|
3328
3346
|
|
|
3329
3347
|
// src/db/repositories/devices.repository.ts
|
|
3330
3348
|
var import_crypto5 = require("crypto");
|
|
@@ -3644,12 +3662,222 @@ var errorMiddleware = (err, c) => {
|
|
|
3644
3662
|
return c.json({ error: message }, 500);
|
|
3645
3663
|
};
|
|
3646
3664
|
|
|
3647
|
-
// src/api/routes/
|
|
3665
|
+
// src/api/routes/backup.routes.ts
|
|
3648
3666
|
var import_hono2 = require("hono");
|
|
3667
|
+
var import_os5 = require("os");
|
|
3668
|
+
|
|
3669
|
+
// src/services/backup/backup.ts
|
|
3670
|
+
var BACKUP_FORMAT_VERSION = 1;
|
|
3671
|
+
var BackupError = class extends Error {
|
|
3672
|
+
constructor(message, code) {
|
|
3673
|
+
super(message);
|
|
3674
|
+
this.code = code;
|
|
3675
|
+
}
|
|
3676
|
+
code;
|
|
3677
|
+
};
|
|
3678
|
+
function validateArchive(input) {
|
|
3679
|
+
if (!input || typeof input !== "object") {
|
|
3680
|
+
throw new BackupError("Backup is not an object", "INVALID_ARCHIVE");
|
|
3681
|
+
}
|
|
3682
|
+
const archive = input;
|
|
3683
|
+
const manifest = archive.manifest;
|
|
3684
|
+
if (!manifest || typeof manifest !== "object") {
|
|
3685
|
+
throw new BackupError("Backup is missing its manifest", "INVALID_ARCHIVE");
|
|
3686
|
+
}
|
|
3687
|
+
if (manifest.formatVersion !== BACKUP_FORMAT_VERSION) {
|
|
3688
|
+
throw new BackupError(
|
|
3689
|
+
`Unsupported backup format version ${String(manifest.formatVersion)}; this build reads version ${BACKUP_FORMAT_VERSION}`,
|
|
3690
|
+
"UNSUPPORTED_VERSION"
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
if (!Array.isArray(archive.projects)) {
|
|
3694
|
+
throw new BackupError("Backup is missing its projects array", "INVALID_ARCHIVE");
|
|
3695
|
+
}
|
|
3696
|
+
for (const [i, p] of archive.projects.entries()) {
|
|
3697
|
+
if (!p || typeof p !== "object") {
|
|
3698
|
+
throw new BackupError(`Project at index ${i} is not an object`, "INVALID_ARCHIVE");
|
|
3699
|
+
}
|
|
3700
|
+
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
3701
|
+
throw new BackupError(`Project at index ${i} has no id`, "INVALID_ARCHIVE");
|
|
3702
|
+
}
|
|
3703
|
+
if (typeof p.path !== "string" || p.path.length === 0) {
|
|
3704
|
+
throw new BackupError(`Project at index ${i} has no path`, "INVALID_ARCHIVE");
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
const ids = new Set(archive.projects.map((p) => p.id));
|
|
3708
|
+
if (ids.size !== archive.projects.length) {
|
|
3709
|
+
throw new BackupError("Backup contains duplicate project ids", "INVALID_ARCHIVE");
|
|
3710
|
+
}
|
|
3711
|
+
return archive;
|
|
3712
|
+
}
|
|
3713
|
+
function remapPaths(projects, rules) {
|
|
3714
|
+
const ordered = [...rules].sort((a, b) => b.from.length - a.from.length);
|
|
3715
|
+
return projects.map((p) => {
|
|
3716
|
+
const rule = ordered.find((r) => p.path === r.from || p.path.startsWith(`${r.from}/`));
|
|
3717
|
+
if (!rule) return p;
|
|
3718
|
+
return { ...p, path: `${rule.to}${p.path.slice(rule.from.length)}` };
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
function planRestore(incoming, existing) {
|
|
3722
|
+
const byId = new Map(existing.map((e) => [e.id, e]));
|
|
3723
|
+
const byPath = new Map(existing.map((e) => [e.path, e]));
|
|
3724
|
+
const plan = { create: [], update: [], conflict: [] };
|
|
3725
|
+
for (const p of incoming) {
|
|
3726
|
+
const sameId = byId.get(p.id);
|
|
3727
|
+
if (sameId) {
|
|
3728
|
+
if (sameId.path !== p.path) plan.update.push(p);
|
|
3729
|
+
continue;
|
|
3730
|
+
}
|
|
3731
|
+
const samePath = byPath.get(p.path);
|
|
3732
|
+
if (samePath) {
|
|
3733
|
+
plan.conflict.push({ incoming: p, existingId: samePath.id });
|
|
3734
|
+
continue;
|
|
3735
|
+
}
|
|
3736
|
+
plan.create.push(p);
|
|
3737
|
+
}
|
|
3738
|
+
return plan;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
// src/version.ts
|
|
3742
|
+
var import_node_fs2 = require("fs");
|
|
3743
|
+
var import_node_path3 = require("path");
|
|
3744
|
+
var cached;
|
|
3745
|
+
function getVersion() {
|
|
3746
|
+
if (cached !== void 0) return cached;
|
|
3747
|
+
cached = resolveVersion();
|
|
3748
|
+
return cached;
|
|
3749
|
+
}
|
|
3750
|
+
function resolveVersion() {
|
|
3751
|
+
const scriptPath = process.argv[1] ?? "";
|
|
3752
|
+
const here = scriptPath ? (0, import_node_path3.dirname)(scriptPath) : process.cwd();
|
|
3753
|
+
let realHere = here;
|
|
3754
|
+
try {
|
|
3755
|
+
realHere = (0, import_node_path3.dirname)((0, import_node_fs2.realpathSync)(scriptPath));
|
|
3756
|
+
} catch {
|
|
3757
|
+
}
|
|
3758
|
+
const searchDirs = realHere === here ? [here, (0, import_node_path3.join)(here, "..")] : [here, (0, import_node_path3.join)(here, ".."), realHere, (0, import_node_path3.join)(realHere, "..")];
|
|
3759
|
+
for (const dir of searchDirs) {
|
|
3760
|
+
try {
|
|
3761
|
+
const v = (0, import_node_fs2.readFileSync)((0, import_node_path3.join)(dir, "version.txt"), "utf8").trim();
|
|
3762
|
+
if (v) return v;
|
|
3763
|
+
} catch {
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
try {
|
|
3767
|
+
const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path3.join)(here, "..", "package.json"), "utf8"));
|
|
3768
|
+
if (pkg.version) return `${pkg.version}+source`;
|
|
3769
|
+
} catch {
|
|
3770
|
+
}
|
|
3771
|
+
return "0.0.0+unknown";
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
// src/api/routes/backup.routes.ts
|
|
3775
|
+
function readBody(c) {
|
|
3776
|
+
return new Promise((resolve2, reject) => {
|
|
3777
|
+
const chunks = [];
|
|
3778
|
+
c.env.incoming.on("data", (chunk) => chunks.push(chunk));
|
|
3779
|
+
c.env.incoming.on("end", () => {
|
|
3780
|
+
try {
|
|
3781
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
3782
|
+
resolve2(raw ? JSON.parse(raw) : {});
|
|
3783
|
+
} catch {
|
|
3784
|
+
reject(new Error("Invalid JSON body"));
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3787
|
+
c.env.incoming.on("error", reject);
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
var createBackupRoutes = (deps) => {
|
|
3791
|
+
const app = new import_hono2.Hono();
|
|
3792
|
+
app.get("/export", (c) => {
|
|
3793
|
+
const repo = deps.projectsRepo();
|
|
3794
|
+
if (!repo) {
|
|
3795
|
+
return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
3796
|
+
}
|
|
3797
|
+
const projects = repo.listProjects().map((p) => ({
|
|
3798
|
+
id: p.id,
|
|
3799
|
+
path: p.path,
|
|
3800
|
+
name: p.name ?? null,
|
|
3801
|
+
createdAt: p.createdAt,
|
|
3802
|
+
updatedAt: p.updatedAt
|
|
3803
|
+
}));
|
|
3804
|
+
return c.json({
|
|
3805
|
+
manifest: {
|
|
3806
|
+
formatVersion: BACKUP_FORMAT_VERSION,
|
|
3807
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3808
|
+
streamerVersion: getVersion(),
|
|
3809
|
+
sourceHost: (0, import_os5.hostname)(),
|
|
3810
|
+
// No endpoint here exports the API key. The flag is recorded so an
|
|
3811
|
+
// archive is self-describing about its own sensitivity rather than
|
|
3812
|
+
// requiring a reader to infer it.
|
|
3813
|
+
includesSecrets: false,
|
|
3814
|
+
counts: { projects: projects.length }
|
|
3815
|
+
},
|
|
3816
|
+
projects
|
|
3817
|
+
});
|
|
3818
|
+
});
|
|
3819
|
+
app.post("/restore", async (c) => {
|
|
3820
|
+
const repo = deps.projectsRepo();
|
|
3821
|
+
if (!repo) {
|
|
3822
|
+
return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
3823
|
+
}
|
|
3824
|
+
let body;
|
|
3825
|
+
try {
|
|
3826
|
+
body = await readBody(c);
|
|
3827
|
+
} catch {
|
|
3828
|
+
return c.json({ error: "Invalid JSON body", code: "INVALID_BODY" }, 400);
|
|
3829
|
+
}
|
|
3830
|
+
let archive;
|
|
3831
|
+
try {
|
|
3832
|
+
archive = validateArchive(body.archive);
|
|
3833
|
+
} catch (err) {
|
|
3834
|
+
if (err instanceof BackupError) {
|
|
3835
|
+
return c.json({ error: err.message, code: err.code }, 400);
|
|
3836
|
+
}
|
|
3837
|
+
throw err;
|
|
3838
|
+
}
|
|
3839
|
+
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 })) : [];
|
|
3840
|
+
const incoming = rules.length > 0 ? remapPaths(archive.projects, rules) : archive.projects;
|
|
3841
|
+
const existing = repo.listProjects().map((p) => ({ id: p.id, path: p.path }));
|
|
3842
|
+
const plan = planRestore(incoming, existing);
|
|
3843
|
+
const summary = {
|
|
3844
|
+
create: plan.create.length,
|
|
3845
|
+
update: plan.update.length,
|
|
3846
|
+
conflict: plan.conflict.length
|
|
3847
|
+
};
|
|
3848
|
+
if (body.apply !== true) {
|
|
3849
|
+
return c.json({ applied: false, summary, plan });
|
|
3850
|
+
}
|
|
3851
|
+
if (plan.conflict.length > 0) {
|
|
3852
|
+
return c.json(
|
|
3853
|
+
{
|
|
3854
|
+
error: "Restore has unresolved conflicts",
|
|
3855
|
+
code: "RESTORE_CONFLICT",
|
|
3856
|
+
summary,
|
|
3857
|
+
plan
|
|
3858
|
+
},
|
|
3859
|
+
409
|
|
3860
|
+
);
|
|
3861
|
+
}
|
|
3862
|
+
let applied = 0;
|
|
3863
|
+
for (const p of [...plan.create, ...plan.update]) {
|
|
3864
|
+
try {
|
|
3865
|
+
repo.upsertProjectByPath(p.path, { name: p.name });
|
|
3866
|
+
applied++;
|
|
3867
|
+
} catch {
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
return c.json({ applied: true, summary, appliedCount: applied });
|
|
3871
|
+
});
|
|
3872
|
+
return app;
|
|
3873
|
+
};
|
|
3874
|
+
|
|
3875
|
+
// src/api/routes/browse.routes.ts
|
|
3876
|
+
var import_hono3 = require("hono");
|
|
3649
3877
|
var ALREADY_HANDLED = 597;
|
|
3650
3878
|
var alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });
|
|
3651
3879
|
var createBrowseRoutes = (deps) => {
|
|
3652
|
-
const app = new
|
|
3880
|
+
const app = new import_hono3.Hono();
|
|
3653
3881
|
app.get("/browse", async (c) => {
|
|
3654
3882
|
const url = new URL(c.req.url);
|
|
3655
3883
|
await deps.handleBrowse(url, c.env.outgoing);
|
|
@@ -3663,7 +3891,7 @@ var createBrowseRoutes = (deps) => {
|
|
|
3663
3891
|
};
|
|
3664
3892
|
|
|
3665
3893
|
// src/api/routes/cacheAlert.routes.ts
|
|
3666
|
-
var
|
|
3894
|
+
var import_hono4 = require("hono");
|
|
3667
3895
|
|
|
3668
3896
|
// src/schemas/cacheAlert.schema.ts
|
|
3669
3897
|
var import_zod = require("zod");
|
|
@@ -3686,7 +3914,7 @@ function readRawBody2(req) {
|
|
|
3686
3914
|
});
|
|
3687
3915
|
}
|
|
3688
3916
|
var createCacheAlertRoutes = (deps) => {
|
|
3689
|
-
const app = new
|
|
3917
|
+
const app = new import_hono4.Hono();
|
|
3690
3918
|
app.get("/", (c) => {
|
|
3691
3919
|
const monitor = deps.cacheMonitor();
|
|
3692
3920
|
return c.json({ pending: monitor?.pending ?? null });
|
|
@@ -3723,7 +3951,7 @@ var createCacheAlertRoutes = (deps) => {
|
|
|
3723
3951
|
};
|
|
3724
3952
|
|
|
3725
3953
|
// src/api/routes/config.routes.ts
|
|
3726
|
-
var
|
|
3954
|
+
var import_hono5 = require("hono");
|
|
3727
3955
|
|
|
3728
3956
|
// src/schemas/claudeFlags.schema.ts
|
|
3729
3957
|
var import_zod2 = require("zod");
|
|
@@ -3744,7 +3972,7 @@ function readRawBody3(req) {
|
|
|
3744
3972
|
});
|
|
3745
3973
|
}
|
|
3746
3974
|
var createConfigRoutes = (deps) => {
|
|
3747
|
-
const app = new
|
|
3975
|
+
const app = new import_hono5.Hono();
|
|
3748
3976
|
app.get("/claude-flags", (c) => c.json(deps.claudeFlagsConfig()));
|
|
3749
3977
|
app.get("/feature-flags", (c) => c.json(deps.featureFlagsConfig()));
|
|
3750
3978
|
app.put("/claude-flags", async (c) => {
|
|
@@ -3779,11 +4007,11 @@ var createConfigRoutes = (deps) => {
|
|
|
3779
4007
|
};
|
|
3780
4008
|
|
|
3781
4009
|
// src/api/routes/conversations.routes.ts
|
|
3782
|
-
var
|
|
4010
|
+
var import_hono6 = require("hono");
|
|
3783
4011
|
var ALREADY_HANDLED2 = 597;
|
|
3784
4012
|
var alreadyHandled2 = () => new Response(null, { status: ALREADY_HANDLED2 });
|
|
3785
4013
|
var createConversationRoutes = (deps) => {
|
|
3786
|
-
const app = new
|
|
4014
|
+
const app = new import_hono6.Hono();
|
|
3787
4015
|
app.get("/count", async (c) => {
|
|
3788
4016
|
const url = new URL(c.req.url);
|
|
3789
4017
|
await deps.handleConversationsCount(url, c.env.outgoing);
|
|
@@ -3810,9 +4038,9 @@ var createConversationRoutes = (deps) => {
|
|
|
3810
4038
|
};
|
|
3811
4039
|
|
|
3812
4040
|
// src/api/routes/devices.routes.ts
|
|
3813
|
-
var
|
|
4041
|
+
var import_hono7 = require("hono");
|
|
3814
4042
|
var createDeviceRoutes = (deps) => {
|
|
3815
|
-
const app = new
|
|
4043
|
+
const app = new import_hono7.Hono();
|
|
3816
4044
|
app.get("/", (c) => {
|
|
3817
4045
|
const repo = deps.devicesRepo();
|
|
3818
4046
|
if (!repo) return c.json({ devices: [], available: false });
|
|
@@ -3835,45 +4063,134 @@ var createDeviceRoutes = (deps) => {
|
|
|
3835
4063
|
return app;
|
|
3836
4064
|
};
|
|
3837
4065
|
|
|
3838
|
-
// src/api/routes/
|
|
3839
|
-
var
|
|
4066
|
+
// src/api/routes/diagnostics.routes.ts
|
|
4067
|
+
var import_fs7 = require("fs");
|
|
4068
|
+
var import_hono8 = require("hono");
|
|
3840
4069
|
|
|
3841
|
-
// src/
|
|
3842
|
-
var
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
if (
|
|
3847
|
-
|
|
3848
|
-
|
|
4070
|
+
// src/services/diagnostics/diagnostics.ts
|
|
4071
|
+
var DIAGNOSTICS_CONTRACT_VERSION = 1;
|
|
4072
|
+
function redactPath(path) {
|
|
4073
|
+
if (!path) return null;
|
|
4074
|
+
const parts = path.split(/[/\\]/).filter(Boolean);
|
|
4075
|
+
if (parts.length <= 2) return parts.join("/");
|
|
4076
|
+
return `\u2026/${parts.slice(-2).join("/")}`;
|
|
4077
|
+
}
|
|
4078
|
+
function worstStatus(checks) {
|
|
4079
|
+
const rank = { ok: 0, unknown: 1, degraded: 2, failed: 3 };
|
|
4080
|
+
return checks.reduce(
|
|
4081
|
+
(worst, c) => rank[c.status] > rank[worst] ? c.status : worst,
|
|
4082
|
+
"ok"
|
|
4083
|
+
);
|
|
3849
4084
|
}
|
|
3850
|
-
function
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
}
|
|
4085
|
+
function buildReport(checks, now = /* @__PURE__ */ new Date()) {
|
|
4086
|
+
return {
|
|
4087
|
+
contractVersion: DIAGNOSTICS_CONTRACT_VERSION,
|
|
4088
|
+
generatedAt: now.toISOString(),
|
|
4089
|
+
overall: worstStatus(checks),
|
|
4090
|
+
checks
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
var SECRET_KEY_RE = /(key|token|secret|password|passwd|credential|authorization|cookie)/i;
|
|
4094
|
+
function redactValue(value) {
|
|
4095
|
+
if (Array.isArray(value)) {
|
|
4096
|
+
return value.map((v) => redactValue(v));
|
|
3857
4097
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
if (v) return v;
|
|
3863
|
-
} catch {
|
|
4098
|
+
if (value && typeof value === "object") {
|
|
4099
|
+
const out = {};
|
|
4100
|
+
for (const [k, v] of Object.entries(value)) {
|
|
4101
|
+
out[k] = SECRET_KEY_RE.test(k) ? "[redacted]" : redactValue(v);
|
|
3864
4102
|
}
|
|
4103
|
+
return out;
|
|
3865
4104
|
}
|
|
4105
|
+
return value;
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
// src/api/routes/diagnostics.routes.ts
|
|
4109
|
+
function providerCheck(name, resolve2) {
|
|
3866
4110
|
try {
|
|
3867
|
-
const
|
|
3868
|
-
|
|
4111
|
+
const exe = resolve2();
|
|
4112
|
+
return {
|
|
4113
|
+
id: `provider:${name}`,
|
|
4114
|
+
status: "ok",
|
|
4115
|
+
summary: `${name} CLI is installed.`,
|
|
4116
|
+
remediation: "NONE",
|
|
4117
|
+
detail: { location: redactPath(exe) }
|
|
4118
|
+
};
|
|
3869
4119
|
} catch {
|
|
4120
|
+
return {
|
|
4121
|
+
id: `provider:${name}`,
|
|
4122
|
+
status: "failed",
|
|
4123
|
+
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
4124
|
+
remediation: "PROVIDER_NOT_INSTALLED"
|
|
4125
|
+
};
|
|
3870
4126
|
}
|
|
3871
|
-
return "0.0.0+unknown";
|
|
3872
4127
|
}
|
|
4128
|
+
var createDiagnosticsRoutes = (deps) => {
|
|
4129
|
+
const app = new import_hono8.Hono();
|
|
4130
|
+
app.get("/", (c) => {
|
|
4131
|
+
const checks = [];
|
|
4132
|
+
checks.push({
|
|
4133
|
+
id: "streamer",
|
|
4134
|
+
status: "ok",
|
|
4135
|
+
summary: "Streamer is running.",
|
|
4136
|
+
remediation: "NONE",
|
|
4137
|
+
detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
|
|
4138
|
+
});
|
|
4139
|
+
checks.push(providerCheck("claude-code", resolveClaudeExe));
|
|
4140
|
+
checks.push(providerCheck("codex-cli", resolveCodexExe));
|
|
4141
|
+
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
4142
|
+
checks.push(
|
|
4143
|
+
cacheAlert ? {
|
|
4144
|
+
id: "cache",
|
|
4145
|
+
status: "degraded",
|
|
4146
|
+
summary: "Conversation cache reported an integrity alert.",
|
|
4147
|
+
remediation: "CACHE_DEGRADED"
|
|
4148
|
+
} : {
|
|
4149
|
+
id: "cache",
|
|
4150
|
+
status: "ok",
|
|
4151
|
+
summary: "Conversation cache is healthy.",
|
|
4152
|
+
remediation: "NONE"
|
|
4153
|
+
}
|
|
4154
|
+
);
|
|
4155
|
+
let ptyOk = true;
|
|
4156
|
+
try {
|
|
4157
|
+
require.resolve("node-pty");
|
|
4158
|
+
} catch {
|
|
4159
|
+
ptyOk = false;
|
|
4160
|
+
}
|
|
4161
|
+
checks.push(
|
|
4162
|
+
ptyOk ? { id: "pty", status: "ok", summary: "PTY subsystem is available.", remediation: "NONE" } : {
|
|
4163
|
+
id: "pty",
|
|
4164
|
+
status: "failed",
|
|
4165
|
+
summary: "node-pty failed to load, so no managed session can start.",
|
|
4166
|
+
remediation: "PTY_UNAVAILABLE"
|
|
4167
|
+
}
|
|
4168
|
+
);
|
|
4169
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
4170
|
+
const claudeProjects = home ? `${home}/.claude/projects` : "";
|
|
4171
|
+
checks.push(
|
|
4172
|
+
claudeProjects && (0, import_fs7.existsSync)(claudeProjects) ? {
|
|
4173
|
+
id: "filesystem",
|
|
4174
|
+
status: "ok",
|
|
4175
|
+
summary: "Provider history directory is present.",
|
|
4176
|
+
remediation: "NONE",
|
|
4177
|
+
detail: { location: redactPath(claudeProjects) }
|
|
4178
|
+
} : {
|
|
4179
|
+
id: "filesystem",
|
|
4180
|
+
status: "degraded",
|
|
4181
|
+
summary: "Provider history directory was not found; history may be unavailable.",
|
|
4182
|
+
remediation: "FS_SCOPE_MISSING"
|
|
4183
|
+
}
|
|
4184
|
+
);
|
|
4185
|
+
return c.json(redactValue(buildReport(checks)));
|
|
4186
|
+
});
|
|
4187
|
+
return app;
|
|
4188
|
+
};
|
|
3873
4189
|
|
|
3874
4190
|
// src/api/routes/health.routes.ts
|
|
4191
|
+
var import_hono9 = require("hono");
|
|
3875
4192
|
var createHealthRoutes = (deps) => {
|
|
3876
|
-
const app = new
|
|
4193
|
+
const app = new import_hono9.Hono();
|
|
3877
4194
|
app.get("/", (c) => {
|
|
3878
4195
|
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
3879
4196
|
return c.json({ ok: true, version: getVersion(), ...cacheAlert ? { cacheAlert } : {} });
|
|
@@ -3884,7 +4201,7 @@ var createHealthRoutes = (deps) => {
|
|
|
3884
4201
|
// src/api/routes/logs.routes.ts
|
|
3885
4202
|
var import_node_fs3 = require("fs");
|
|
3886
4203
|
var import_node_path5 = require("path");
|
|
3887
|
-
var
|
|
4204
|
+
var import_hono10 = require("hono");
|
|
3888
4205
|
|
|
3889
4206
|
// src/lifecycle/constants.ts
|
|
3890
4207
|
var import_node_os = require("os");
|
|
@@ -3942,7 +4259,7 @@ function readLogLines(filePath, sinceOffset, limit) {
|
|
|
3942
4259
|
}
|
|
3943
4260
|
}
|
|
3944
4261
|
function createLogsRoutes() {
|
|
3945
|
-
const app = new
|
|
4262
|
+
const app = new import_hono10.Hono();
|
|
3946
4263
|
app.get("/", (c) => {
|
|
3947
4264
|
try {
|
|
3948
4265
|
const sourceParam = (c.req.query("source") || "").toLowerCase();
|
|
@@ -4013,8 +4330,8 @@ function createLogsRoutes() {
|
|
|
4013
4330
|
// src/api/routes/misc.routes.ts
|
|
4014
4331
|
var import_node_child_process = require("child_process");
|
|
4015
4332
|
var import_node_crypto2 = require("crypto");
|
|
4016
|
-
var
|
|
4017
|
-
var
|
|
4333
|
+
var import_hono11 = require("hono");
|
|
4334
|
+
var import_os6 = require("os");
|
|
4018
4335
|
|
|
4019
4336
|
// src/config/update-config.ts
|
|
4020
4337
|
var import_node_fs4 = require("fs");
|
|
@@ -4344,12 +4661,12 @@ function verifyWebhookSignature(body, header, secret) {
|
|
|
4344
4661
|
}
|
|
4345
4662
|
var clientLog = getLogger("client");
|
|
4346
4663
|
var createMiscRoutes = (deps) => {
|
|
4347
|
-
const app = new
|
|
4664
|
+
const app = new import_hono11.Hono();
|
|
4348
4665
|
app.get("/api/info", (c) => {
|
|
4349
4666
|
const ptyIds = deps.ptyAttachedIds();
|
|
4350
4667
|
return c.json({
|
|
4351
4668
|
version: getVersion(),
|
|
4352
|
-
machineName: (0,
|
|
4669
|
+
machineName: (0, import_os6.hostname)(),
|
|
4353
4670
|
platform: process.platform,
|
|
4354
4671
|
activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === "running").length,
|
|
4355
4672
|
publicUrl: deps.publicUrl,
|
|
@@ -4475,11 +4792,11 @@ var createMiscRoutes = (deps) => {
|
|
|
4475
4792
|
};
|
|
4476
4793
|
|
|
4477
4794
|
// src/api/routes/pair.routes.ts
|
|
4478
|
-
var
|
|
4795
|
+
var import_hono12 = require("hono");
|
|
4479
4796
|
var ALREADY_HANDLED3 = 597;
|
|
4480
4797
|
var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
|
|
4481
4798
|
var createPairRoutes = (deps) => {
|
|
4482
|
-
const app = new
|
|
4799
|
+
const app = new import_hono12.Hono();
|
|
4483
4800
|
app.post("/start", (c) => {
|
|
4484
4801
|
deps.handlePairStart(c.env.outgoing);
|
|
4485
4802
|
return alreadyHandled3();
|
|
@@ -4492,11 +4809,11 @@ var createPairRoutes = (deps) => {
|
|
|
4492
4809
|
};
|
|
4493
4810
|
|
|
4494
4811
|
// src/api/routes/projects.routes.ts
|
|
4495
|
-
var
|
|
4812
|
+
var import_hono13 = require("hono");
|
|
4496
4813
|
var ALREADY_HANDLED4 = 597;
|
|
4497
4814
|
var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
|
|
4498
4815
|
var createProjectRoutes = (deps) => {
|
|
4499
|
-
const app = new
|
|
4816
|
+
const app = new import_hono13.Hono();
|
|
4500
4817
|
app.get("/", (c) => {
|
|
4501
4818
|
const url = new URL(c.req.url);
|
|
4502
4819
|
deps.handleListProjects(url, c.env.outgoing);
|
|
@@ -4511,7 +4828,7 @@ var createProjectRoutes = (deps) => {
|
|
|
4511
4828
|
};
|
|
4512
4829
|
|
|
4513
4830
|
// src/api/routes/providers.routes.ts
|
|
4514
|
-
var
|
|
4831
|
+
var import_hono14 = require("hono");
|
|
4515
4832
|
|
|
4516
4833
|
// src/services/providers/providerHealth.ts
|
|
4517
4834
|
var import_child_process3 = require("child_process");
|
|
@@ -4634,7 +4951,7 @@ async function providerHealth(name, resolveExe, detect = runVersion) {
|
|
|
4634
4951
|
|
|
4635
4952
|
// src/api/routes/providers.routes.ts
|
|
4636
4953
|
var createProviderRoutes = () => {
|
|
4637
|
-
const app = new
|
|
4954
|
+
const app = new import_hono14.Hono();
|
|
4638
4955
|
app.get("/", async (c) => {
|
|
4639
4956
|
const providers = await Promise.all([
|
|
4640
4957
|
providerHealth(CLAUDE_CODE_PROVIDER, resolveClaudeExe),
|
|
@@ -4646,11 +4963,11 @@ var createProviderRoutes = () => {
|
|
|
4646
4963
|
};
|
|
4647
4964
|
|
|
4648
4965
|
// src/api/routes/scanner.routes.ts
|
|
4649
|
-
var
|
|
4966
|
+
var import_hono15 = require("hono");
|
|
4650
4967
|
var ALREADY_HANDLED5 = 597;
|
|
4651
4968
|
var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
|
|
4652
4969
|
var createScannerRoutes = (deps) => {
|
|
4653
|
-
const app = new
|
|
4970
|
+
const app = new import_hono15.Hono();
|
|
4654
4971
|
app.get("/api/search", async (c) => {
|
|
4655
4972
|
const url = new URL(c.req.url);
|
|
4656
4973
|
await deps.handleSearch(url, c.env.outgoing);
|
|
@@ -4660,11 +4977,11 @@ var createScannerRoutes = (deps) => {
|
|
|
4660
4977
|
};
|
|
4661
4978
|
|
|
4662
4979
|
// src/api/routes/sessions.routes.ts
|
|
4663
|
-
var
|
|
4980
|
+
var import_hono16 = require("hono");
|
|
4664
4981
|
var ALREADY_HANDLED6 = 597;
|
|
4665
4982
|
var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
|
|
4666
4983
|
var createSessionRoutes = (deps) => {
|
|
4667
|
-
const app = new
|
|
4984
|
+
const app = new import_hono16.Hono();
|
|
4668
4985
|
app.get("/count", (c) => {
|
|
4669
4986
|
deps.handleSessionsCount(c.env.outgoing);
|
|
4670
4987
|
return alreadyHandled6();
|
|
@@ -4739,9 +5056,9 @@ var createSessionRoutes = (deps) => {
|
|
|
4739
5056
|
};
|
|
4740
5057
|
|
|
4741
5058
|
// src/api/routes/ws.routes.ts
|
|
4742
|
-
var
|
|
5059
|
+
var import_hono17 = require("hono");
|
|
4743
5060
|
var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
4744
|
-
const app = new
|
|
5061
|
+
const app = new import_hono17.Hono();
|
|
4745
5062
|
app.get(
|
|
4746
5063
|
"/ws",
|
|
4747
5064
|
upgradeWebSocket(() => {
|
|
@@ -4767,7 +5084,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
4767
5084
|
|
|
4768
5085
|
// src/api/app.ts
|
|
4769
5086
|
var createHonoApp = (deps, upgradeWebSocket) => {
|
|
4770
|
-
const app = new
|
|
5087
|
+
const app = new import_hono18.Hono();
|
|
4771
5088
|
const httpLog = getLogger("http");
|
|
4772
5089
|
app.use("*", async (c, next) => {
|
|
4773
5090
|
const start = Date.now();
|
|
@@ -4788,6 +5105,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
4788
5105
|
app.use("*", authMiddleware(deps));
|
|
4789
5106
|
app.onError(errorMiddleware);
|
|
4790
5107
|
app.route("/healthz", createHealthRoutes(deps));
|
|
5108
|
+
app.route("/api/diagnostics", createDiagnosticsRoutes(deps));
|
|
4791
5109
|
app.route("/", createMiscRoutes(deps));
|
|
4792
5110
|
app.route("/api/sessions", createSessionRoutes(deps));
|
|
4793
5111
|
app.route("/api/conversations", createConversationRoutes(deps));
|
|
@@ -4796,6 +5114,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
4796
5114
|
app.route("/api/projects", createProjectRoutes(deps));
|
|
4797
5115
|
app.route("/api/providers", createProviderRoutes());
|
|
4798
5116
|
app.route("/api/devices", createDeviceRoutes(deps));
|
|
5117
|
+
app.route("/api/backup", createBackupRoutes(deps));
|
|
4799
5118
|
app.route("/api/pair", createPairRoutes(deps));
|
|
4800
5119
|
app.route("/api", createBrowseRoutes(deps));
|
|
4801
5120
|
app.route("/", createScannerRoutes(deps));
|
|
@@ -4861,13 +5180,13 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
4861
5180
|
// src/conversation-cache.ts
|
|
4862
5181
|
var import_scanner2 = require("@threadbase-sh/scanner");
|
|
4863
5182
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
4864
|
-
var
|
|
5183
|
+
var import_fs10 = require("fs");
|
|
4865
5184
|
var import_promises3 = require("fs/promises");
|
|
4866
5185
|
var import_path12 = require("path");
|
|
4867
5186
|
var import_promises4 = require("timers/promises");
|
|
4868
5187
|
|
|
4869
5188
|
// src/db/sqlite-migrate.ts
|
|
4870
|
-
var
|
|
5189
|
+
var import_fs8 = require("fs");
|
|
4871
5190
|
var import_path10 = require("path");
|
|
4872
5191
|
var import_url2 = require("url");
|
|
4873
5192
|
var import_meta2 = {};
|
|
@@ -4877,6 +5196,9 @@ function getMigrationsDir2() {
|
|
|
4877
5196
|
}
|
|
4878
5197
|
return __dirname;
|
|
4879
5198
|
}
|
|
5199
|
+
function resolveMigrationsDir(name = "migrations") {
|
|
5200
|
+
return (0, import_path10.join)(getMigrationsDir2(), name);
|
|
5201
|
+
}
|
|
4880
5202
|
var SCHEMA_MIGRATIONS_SQL = `
|
|
4881
5203
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
4882
5204
|
id TEXT PRIMARY KEY,
|
|
@@ -4885,8 +5207,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
4885
5207
|
`;
|
|
4886
5208
|
function runSqliteMigrations(db, migrationsDir) {
|
|
4887
5209
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
4888
|
-
const dir = migrationsDir ?? (
|
|
4889
|
-
const files = (0,
|
|
5210
|
+
const dir = migrationsDir ?? resolveMigrationsDir();
|
|
5211
|
+
const files = (0, import_fs8.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
4890
5212
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
4891
5213
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
4892
5214
|
const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
@@ -4897,7 +5219,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
4897
5219
|
skipped.push(file);
|
|
4898
5220
|
continue;
|
|
4899
5221
|
}
|
|
4900
|
-
const sql = (0,
|
|
5222
|
+
const sql = (0, import_fs8.readFileSync)((0, import_path10.join)(dir, file), "utf-8");
|
|
4901
5223
|
const tx = db.transaction(() => {
|
|
4902
5224
|
db.exec(sql);
|
|
4903
5225
|
recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -4909,7 +5231,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
4909
5231
|
}
|
|
4910
5232
|
|
|
4911
5233
|
// src/services/conversations/isAgentConversation.ts
|
|
4912
|
-
var
|
|
5234
|
+
var import_fs9 = require("fs");
|
|
4913
5235
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
4914
5236
|
var CHUNK_BYTES = 64 * 1024;
|
|
4915
5237
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -4931,12 +5253,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
4931
5253
|
if (cached2 !== void 0) return cached2;
|
|
4932
5254
|
let fd;
|
|
4933
5255
|
try {
|
|
4934
|
-
fd = (0,
|
|
5256
|
+
fd = (0, import_fs9.openSync)(filePath, "r");
|
|
4935
5257
|
} catch {
|
|
4936
5258
|
return false;
|
|
4937
5259
|
}
|
|
4938
5260
|
try {
|
|
4939
|
-
const fileSize = (0,
|
|
5261
|
+
const fileSize = (0, import_fs9.statSync)(filePath).size;
|
|
4940
5262
|
if (fileSize === 0) {
|
|
4941
5263
|
fileDecisionCache.set(key, false);
|
|
4942
5264
|
return false;
|
|
@@ -4947,7 +5269,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
4947
5269
|
let carry = "";
|
|
4948
5270
|
while (offset < fileSize) {
|
|
4949
5271
|
const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
|
|
4950
|
-
const got = (0,
|
|
5272
|
+
const got = (0, import_fs9.readSync)(fd, buf, 0, toRead, offset);
|
|
4951
5273
|
if (got <= 0) break;
|
|
4952
5274
|
const chunk = carry + buf.toString("utf8", 0, got);
|
|
4953
5275
|
for (const marker of markers) {
|
|
@@ -4968,7 +5290,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
4968
5290
|
} catch {
|
|
4969
5291
|
return false;
|
|
4970
5292
|
} finally {
|
|
4971
|
-
(0,
|
|
5293
|
+
(0, import_fs9.closeSync)(fd);
|
|
4972
5294
|
}
|
|
4973
5295
|
}
|
|
4974
5296
|
function parseAgentEntrypointsEnv(raw) {
|
|
@@ -5543,7 +5865,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5543
5865
|
if (!fileState) return null;
|
|
5544
5866
|
let stat3;
|
|
5545
5867
|
try {
|
|
5546
|
-
stat3 = (0,
|
|
5868
|
+
stat3 = (0, import_fs10.statSync)(filePath);
|
|
5547
5869
|
} catch {
|
|
5548
5870
|
return null;
|
|
5549
5871
|
}
|
|
@@ -5564,17 +5886,17 @@ var ConversationCache = class _ConversationCache {
|
|
|
5564
5886
|
);
|
|
5565
5887
|
if (rows.length === 0) return { messages: [], total, fromIndex: from };
|
|
5566
5888
|
const messages = [];
|
|
5567
|
-
const fd = (0,
|
|
5889
|
+
const fd = (0, import_fs10.openSync)(filePath, "r");
|
|
5568
5890
|
try {
|
|
5569
5891
|
const state = (0, import_scanner2.createJsonlParseState)();
|
|
5570
5892
|
for (const row of rows) {
|
|
5571
5893
|
const buf = Buffer.alloc(row.byte_length);
|
|
5572
|
-
(0,
|
|
5894
|
+
(0, import_fs10.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
|
|
5573
5895
|
const msg = (0, import_scanner2.parseJsonlLine)(buf.toString("utf-8"), state);
|
|
5574
5896
|
if (msg) messages.push(msg);
|
|
5575
5897
|
}
|
|
5576
5898
|
} finally {
|
|
5577
|
-
(0,
|
|
5899
|
+
(0, import_fs10.closeSync)(fd);
|
|
5578
5900
|
}
|
|
5579
5901
|
return { messages, total, fromIndex: from };
|
|
5580
5902
|
}
|
|
@@ -5602,14 +5924,14 @@ var ConversationCache = class _ConversationCache {
|
|
|
5602
5924
|
isAgentFileCached(filePath) {
|
|
5603
5925
|
let s;
|
|
5604
5926
|
try {
|
|
5605
|
-
s = (0,
|
|
5927
|
+
s = (0, import_fs10.statSync)(filePath);
|
|
5606
5928
|
} catch {
|
|
5607
5929
|
return false;
|
|
5608
5930
|
}
|
|
5609
5931
|
return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
|
|
5610
5932
|
}
|
|
5611
5933
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
5612
|
-
(0,
|
|
5934
|
+
(0, import_fs10.mkdirSync)((0, import_path12.dirname)(dbPath), { recursive: true });
|
|
5613
5935
|
const db = new import_better_sqlite3.default(dbPath);
|
|
5614
5936
|
db.pragma("journal_mode = WAL");
|
|
5615
5937
|
db.pragma("foreign_keys = ON");
|
|
@@ -5796,7 +6118,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5796
6118
|
let mtimeMs = null;
|
|
5797
6119
|
let fileSize = null;
|
|
5798
6120
|
try {
|
|
5799
|
-
const s = (0,
|
|
6121
|
+
const s = (0, import_fs10.statSync)(m.filePath);
|
|
5800
6122
|
mtimeMs = s.mtimeMs;
|
|
5801
6123
|
fileSize = s.size;
|
|
5802
6124
|
} catch {
|
|
@@ -5856,8 +6178,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
5856
6178
|
let fileSize;
|
|
5857
6179
|
let fd;
|
|
5858
6180
|
try {
|
|
5859
|
-
fileSize = (0,
|
|
5860
|
-
fd = (0,
|
|
6181
|
+
fileSize = (0, import_fs10.statSync)(filePath).size;
|
|
6182
|
+
fd = (0, import_fs10.openSync)(filePath, "r");
|
|
5861
6183
|
} catch {
|
|
5862
6184
|
return false;
|
|
5863
6185
|
}
|
|
@@ -5870,7 +6192,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5870
6192
|
while (pos > 0 && lines.length < this.tailSize * 4) {
|
|
5871
6193
|
const toRead = Math.min(CHUNK, pos);
|
|
5872
6194
|
pos -= toRead;
|
|
5873
|
-
(0,
|
|
6195
|
+
(0, import_fs10.readSync)(fd, buf, 0, toRead, pos);
|
|
5874
6196
|
const chunk = buf.subarray(0, toRead).toString("utf8");
|
|
5875
6197
|
const combined = chunk + partial;
|
|
5876
6198
|
const parts = combined.split("\n");
|
|
@@ -5881,7 +6203,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5881
6203
|
}
|
|
5882
6204
|
if (partial) lines.push(partial);
|
|
5883
6205
|
} finally {
|
|
5884
|
-
(0,
|
|
6206
|
+
(0, import_fs10.closeSync)(fd);
|
|
5885
6207
|
}
|
|
5886
6208
|
const msgs = [];
|
|
5887
6209
|
for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
|
|
@@ -6094,7 +6416,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6094
6416
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
6095
6417
|
* JSONL has been deleted.
|
|
6096
6418
|
*/
|
|
6097
|
-
pruneGhostFiles(exists =
|
|
6419
|
+
pruneGhostFiles(exists = import_fs10.existsSync) {
|
|
6098
6420
|
const rows = this.stmts.allFilePaths.all();
|
|
6099
6421
|
const ghosts = [];
|
|
6100
6422
|
const prune = this.db.transaction((ids) => {
|
|
@@ -6149,7 +6471,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6149
6471
|
* Returns the removed IDs.
|
|
6150
6472
|
*/
|
|
6151
6473
|
reconcileDeletions(livePaths, opts) {
|
|
6152
|
-
const exists = opts?.exists ??
|
|
6474
|
+
const exists = opts?.exists ?? import_fs10.existsSync;
|
|
6153
6475
|
const rows = this.stmts.allFilePaths.all();
|
|
6154
6476
|
const removed = [];
|
|
6155
6477
|
const drop = this.db.transaction((ids) => {
|
|
@@ -6184,7 +6506,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6184
6506
|
* reports drift for the CacheIntegrityMonitor to classify. `tailed` flags
|
|
6185
6507
|
* rows that still have cached history (which pruneGhostFiles would keep).
|
|
6186
6508
|
*/
|
|
6187
|
-
listMissingFiles(exists =
|
|
6509
|
+
listMissingFiles(exists = import_fs10.existsSync) {
|
|
6188
6510
|
const rows = this.stmts.allFilePathsWithTitle.all();
|
|
6189
6511
|
const missing = [];
|
|
6190
6512
|
for (const row of rows) {
|
|
@@ -6296,6 +6618,7 @@ var ManagedSessionsRepository = class {
|
|
|
6296
6618
|
updateStatusStmt;
|
|
6297
6619
|
getStmt;
|
|
6298
6620
|
listNonTerminalStmt;
|
|
6621
|
+
listRecoverableStmt;
|
|
6299
6622
|
deleteStmt;
|
|
6300
6623
|
constructor(db) {
|
|
6301
6624
|
this.upsertStmt = db.prepare(`
|
|
@@ -6348,6 +6671,13 @@ var ManagedSessionsRepository = class {
|
|
|
6348
6671
|
WHERE completed_at IS NULL
|
|
6349
6672
|
ORDER BY started_at ASC
|
|
6350
6673
|
`);
|
|
6674
|
+
this.listRecoverableStmt = db.prepare(`
|
|
6675
|
+
SELECT * FROM managed_sessions
|
|
6676
|
+
WHERE (completed_at IS NULL OR status_source = 'shutdown')
|
|
6677
|
+
AND status_updated_at >= @since
|
|
6678
|
+
ORDER BY status_updated_at DESC
|
|
6679
|
+
LIMIT @limit
|
|
6680
|
+
`);
|
|
6351
6681
|
this.deleteStmt = db.prepare("DELETE FROM managed_sessions WHERE session_id = ?");
|
|
6352
6682
|
}
|
|
6353
6683
|
/** Record a session at spawn, or refresh every field of an existing row. */
|
|
@@ -6399,6 +6729,14 @@ var ManagedSessionsRepository = class {
|
|
|
6399
6729
|
listNonTerminal() {
|
|
6400
6730
|
return this.listNonTerminalStmt.all();
|
|
6401
6731
|
}
|
|
6732
|
+
/**
|
|
6733
|
+
* Rows a restart could bring back: still open, or closed by our own shutdown,
|
|
6734
|
+
* and touched no longer ago than `sinceMs`. Newest first, capped — the caller
|
|
6735
|
+
* decides which of these actually deserve rehydrating (`shouldRehydrate`).
|
|
6736
|
+
*/
|
|
6737
|
+
listRecoverable({ sinceMs, limit }) {
|
|
6738
|
+
return this.listRecoverableStmt.all({ since: sinceMs, limit });
|
|
6739
|
+
}
|
|
6402
6740
|
delete(sessionId) {
|
|
6403
6741
|
this.deleteStmt.run(sessionId);
|
|
6404
6742
|
}
|
|
@@ -6534,6 +6872,54 @@ var SessionsRepository = class {
|
|
|
6534
6872
|
}
|
|
6535
6873
|
};
|
|
6536
6874
|
|
|
6875
|
+
// src/db/runtime-store.ts
|
|
6876
|
+
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
6877
|
+
var RuntimeStore = class _RuntimeStore {
|
|
6878
|
+
constructor(db) {
|
|
6879
|
+
this.db = db;
|
|
6880
|
+
}
|
|
6881
|
+
db;
|
|
6882
|
+
static open(dbPath, migrationsDir) {
|
|
6883
|
+
const db = new import_better_sqlite32.default(dbPath);
|
|
6884
|
+
db.pragma("journal_mode = WAL");
|
|
6885
|
+
runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
|
|
6886
|
+
return new _RuntimeStore(db);
|
|
6887
|
+
}
|
|
6888
|
+
getDatabase() {
|
|
6889
|
+
return this.db;
|
|
6890
|
+
}
|
|
6891
|
+
/**
|
|
6892
|
+
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
6893
|
+
*
|
|
6894
|
+
* Non-destructive by design: the source table is left in place so an older
|
|
6895
|
+
* streamer rolled back onto the same machine still finds its registry. Runs
|
|
6896
|
+
* only when this file's table is empty, so a second boot is a no-op rather
|
|
6897
|
+
* than a re-copy that would resurrect rows deleted since.
|
|
6898
|
+
*
|
|
6899
|
+
* Returns the number of rows copied.
|
|
6900
|
+
*/
|
|
6901
|
+
importLegacyManagedSessions(source) {
|
|
6902
|
+
const existing = this.db.prepare("SELECT COUNT(*) AS n FROM managed_sessions").get();
|
|
6903
|
+
if (existing.n > 0) return 0;
|
|
6904
|
+
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'managed_sessions'").get();
|
|
6905
|
+
if (!hasTable) return 0;
|
|
6906
|
+
const rows = source.prepare("SELECT * FROM managed_sessions").all();
|
|
6907
|
+
if (rows.length === 0) return 0;
|
|
6908
|
+
const columns = Object.keys(rows[0]);
|
|
6909
|
+
const insert = this.db.prepare(
|
|
6910
|
+
`INSERT OR IGNORE INTO managed_sessions (${columns.join(", ")})
|
|
6911
|
+
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
6912
|
+
);
|
|
6913
|
+
this.db.transaction((batch) => {
|
|
6914
|
+
for (const row of batch) insert.run(row);
|
|
6915
|
+
})(rows);
|
|
6916
|
+
return rows.length;
|
|
6917
|
+
}
|
|
6918
|
+
close() {
|
|
6919
|
+
this.db.close();
|
|
6920
|
+
}
|
|
6921
|
+
};
|
|
6922
|
+
|
|
6537
6923
|
// src/db/upload-records.ts
|
|
6538
6924
|
async function recordUpload(pool2, instanceId, row) {
|
|
6539
6925
|
if (!pool2) return;
|
|
@@ -6554,8 +6940,8 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
6554
6940
|
}
|
|
6555
6941
|
|
|
6556
6942
|
// src/handlers/handleListProjects.ts
|
|
6557
|
-
var
|
|
6558
|
-
var
|
|
6943
|
+
var import_fs11 = require("fs");
|
|
6944
|
+
var import_os7 = require("os");
|
|
6559
6945
|
var import_path13 = require("path");
|
|
6560
6946
|
function decodeProjectPath(dirName) {
|
|
6561
6947
|
return dirName.replace(/-/g, "/");
|
|
@@ -6563,14 +6949,14 @@ function decodeProjectPath(dirName) {
|
|
|
6563
6949
|
function handleListProjects(url, res) {
|
|
6564
6950
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
6565
6951
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
6566
|
-
const projectsDir = (0, import_path13.join)((0,
|
|
6952
|
+
const projectsDir = (0, import_path13.join)((0, import_os7.homedir)(), ".claude", "projects");
|
|
6567
6953
|
let entries;
|
|
6568
6954
|
try {
|
|
6569
|
-
entries = (0,
|
|
6955
|
+
entries = (0, import_fs11.readdirSync)(projectsDir).map((dirName) => {
|
|
6570
6956
|
const fullPath = (0, import_path13.join)(projectsDir, dirName);
|
|
6571
6957
|
let mtime = 0;
|
|
6572
6958
|
try {
|
|
6573
|
-
mtime = (0,
|
|
6959
|
+
mtime = (0, import_fs11.statSync)(fullPath).mtimeMs;
|
|
6574
6960
|
} catch {
|
|
6575
6961
|
}
|
|
6576
6962
|
const path = decodeProjectPath(dirName);
|
|
@@ -6685,19 +7071,19 @@ function setCacheMetadata(repo, key, value) {
|
|
|
6685
7071
|
|
|
6686
7072
|
// src/services/cache-integrity/cacheIntegrityMonitor.ts
|
|
6687
7073
|
var import_crypto9 = require("crypto");
|
|
6688
|
-
var
|
|
7074
|
+
var import_fs14 = require("fs");
|
|
6689
7075
|
|
|
6690
7076
|
// src/services/cache-integrity/alertStore.ts
|
|
6691
|
-
var
|
|
6692
|
-
var
|
|
7077
|
+
var import_fs12 = require("fs");
|
|
7078
|
+
var import_os8 = require("os");
|
|
6693
7079
|
var import_path14 = require("path");
|
|
6694
7080
|
function alertStatePath() {
|
|
6695
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path14.join)((0,
|
|
7081
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path14.join)((0, import_os8.homedir)(), ".threadbase");
|
|
6696
7082
|
return (0, import_path14.join)(dir, "cache-alert.json");
|
|
6697
7083
|
}
|
|
6698
7084
|
function loadAlertState() {
|
|
6699
7085
|
try {
|
|
6700
|
-
const parsed = JSON.parse((0,
|
|
7086
|
+
const parsed = JSON.parse((0, import_fs12.readFileSync)(alertStatePath(), "utf-8"));
|
|
6701
7087
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6702
7088
|
} catch {
|
|
6703
7089
|
return {};
|
|
@@ -6705,13 +7091,13 @@ function loadAlertState() {
|
|
|
6705
7091
|
}
|
|
6706
7092
|
function saveAlertState(state) {
|
|
6707
7093
|
const path = alertStatePath();
|
|
6708
|
-
(0,
|
|
6709
|
-
(0,
|
|
7094
|
+
(0, import_fs12.mkdirSync)((0, import_path14.dirname)(path), { recursive: true });
|
|
7095
|
+
(0, import_fs12.writeFileSync)(path, `${JSON.stringify(state, null, 2)}
|
|
6710
7096
|
`);
|
|
6711
7097
|
}
|
|
6712
7098
|
|
|
6713
7099
|
// src/services/cache-integrity/backup.ts
|
|
6714
|
-
var
|
|
7100
|
+
var import_fs13 = require("fs");
|
|
6715
7101
|
var import_path15 = require("path");
|
|
6716
7102
|
var DEFAULT_RETAIN = 3;
|
|
6717
7103
|
function retainCount() {
|
|
@@ -6724,16 +7110,16 @@ function timestamp(d) {
|
|
|
6724
7110
|
}
|
|
6725
7111
|
async function backupCacheDb(db, cacheDir) {
|
|
6726
7112
|
const backupsDir = (0, import_path15.join)(cacheDir, "backups");
|
|
6727
|
-
(0,
|
|
7113
|
+
(0, import_fs13.mkdirSync)(backupsDir, { recursive: true });
|
|
6728
7114
|
const destPath = (0, import_path15.join)(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
|
|
6729
7115
|
await db.backup(destPath);
|
|
6730
7116
|
const retain = retainCount();
|
|
6731
|
-
const backups = (0,
|
|
7117
|
+
const backups = (0, import_fs13.readdirSync)(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
|
|
6732
7118
|
const full = (0, import_path15.join)(backupsDir, f);
|
|
6733
|
-
return { full, mtime: (0,
|
|
7119
|
+
return { full, mtime: (0, import_fs13.statSync)(full).mtimeMs };
|
|
6734
7120
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
6735
7121
|
for (const stale of backups.slice(retain)) {
|
|
6736
|
-
if ((0,
|
|
7122
|
+
if ((0, import_fs13.existsSync)(stale.full)) (0, import_fs13.unlinkSync)(stale.full);
|
|
6737
7123
|
}
|
|
6738
7124
|
return destPath;
|
|
6739
7125
|
}
|
|
@@ -6830,7 +7216,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6830
7216
|
* the pending record, back up on high severity, and broadcast the alert.
|
|
6831
7217
|
*/
|
|
6832
7218
|
async runDetection(detectedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
6833
|
-
const all = this.cache.listMissingFiles(
|
|
7219
|
+
const all = this.cache.listMissingFiles(import_fs14.existsSync);
|
|
6834
7220
|
const missing = all.filter((m) => !this.ignoredIds.has(m.id));
|
|
6835
7221
|
if (missing.length === 0) {
|
|
6836
7222
|
if (this._pending) {
|
|
@@ -6934,7 +7320,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6934
7320
|
case "prune_all": {
|
|
6935
7321
|
await this.ensureBackup(pending);
|
|
6936
7322
|
const backupPath = pending.backupPath;
|
|
6937
|
-
const stillMissing = pending.missing.filter((m) => !(0,
|
|
7323
|
+
const stillMissing = pending.missing.filter((m) => !(0, import_fs14.existsSync)(m.filePath)).map((m) => m.id);
|
|
6938
7324
|
const pruned = this.cache.dropRowsById(stillMissing);
|
|
6939
7325
|
this.applyDeferredUnlinks();
|
|
6940
7326
|
this.clearPending();
|
|
@@ -6996,7 +7382,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6996
7382
|
|
|
6997
7383
|
// src/services/conversations/conversationWatcher.ts
|
|
6998
7384
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
6999
|
-
var
|
|
7385
|
+
var import_fs15 = require("fs");
|
|
7000
7386
|
var import_promises5 = require("fs/promises");
|
|
7001
7387
|
var ConversationWatcher = class {
|
|
7002
7388
|
files = /* @__PURE__ */ new Map();
|
|
@@ -7022,7 +7408,7 @@ var ConversationWatcher = class {
|
|
|
7022
7408
|
if (this.files.has(key)) return;
|
|
7023
7409
|
let offset;
|
|
7024
7410
|
try {
|
|
7025
|
-
offset = (0,
|
|
7411
|
+
offset = (0, import_fs15.statSync)(filePath).size;
|
|
7026
7412
|
} catch {
|
|
7027
7413
|
offset = 0;
|
|
7028
7414
|
}
|
|
@@ -7204,14 +7590,14 @@ function findSearchTarget(messages, query) {
|
|
|
7204
7590
|
}
|
|
7205
7591
|
|
|
7206
7592
|
// src/services/conversations/pruneAgentConversations.ts
|
|
7207
|
-
var
|
|
7593
|
+
var import_fs16 = require("fs");
|
|
7208
7594
|
function pruneAgentConversations(cache) {
|
|
7209
7595
|
const db = cache.getDatabase();
|
|
7210
7596
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
7211
7597
|
let pruned = 0;
|
|
7212
7598
|
let missing = 0;
|
|
7213
7599
|
for (const row of rows) {
|
|
7214
|
-
if (!(0,
|
|
7600
|
+
if (!(0, import_fs16.existsSync)(row.file_path)) {
|
|
7215
7601
|
missing += 1;
|
|
7216
7602
|
continue;
|
|
7217
7603
|
}
|
|
@@ -7312,22 +7698,22 @@ function refreshConversationCache(deps) {
|
|
|
7312
7698
|
}
|
|
7313
7699
|
|
|
7314
7700
|
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
7315
|
-
var
|
|
7316
|
-
var
|
|
7701
|
+
var import_fs17 = require("fs");
|
|
7702
|
+
var import_os9 = require("os");
|
|
7317
7703
|
var import_path16 = require("path");
|
|
7318
|
-
var DEFAULT_PROJECTS_DIR = (0, import_path16.join)((0,
|
|
7704
|
+
var DEFAULT_PROJECTS_DIR = (0, import_path16.join)((0, import_os9.homedir)(), ".claude", "projects");
|
|
7319
7705
|
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
7320
7706
|
let maxMs;
|
|
7321
7707
|
try {
|
|
7322
|
-
maxMs = (0,
|
|
7708
|
+
maxMs = (0, import_fs17.statSync)(projectsDir).mtimeMs;
|
|
7323
7709
|
} catch {
|
|
7324
7710
|
return null;
|
|
7325
7711
|
}
|
|
7326
7712
|
try {
|
|
7327
|
-
for (const ent of (0,
|
|
7713
|
+
for (const ent of (0, import_fs17.readdirSync)(projectsDir, { withFileTypes: true })) {
|
|
7328
7714
|
if (!ent.isDirectory()) continue;
|
|
7329
7715
|
try {
|
|
7330
|
-
const childMs = (0,
|
|
7716
|
+
const childMs = (0, import_fs17.statSync)((0, import_path16.join)(projectsDir, ent.name)).mtimeMs;
|
|
7331
7717
|
if (childMs > maxMs) maxMs = childMs;
|
|
7332
7718
|
} catch {
|
|
7333
7719
|
}
|
|
@@ -7551,7 +7937,8 @@ function contentStateForSession(args) {
|
|
|
7551
7937
|
status,
|
|
7552
7938
|
startedAt: args.startedAtOverride ?? args.session.startedAt.getTime(),
|
|
7553
7939
|
lastOutput: truncateLastOutput(args.session.lastOutput ?? ""),
|
|
7554
|
-
...args.serverLabel != null && { serverLabel: args.serverLabel }
|
|
7940
|
+
...args.serverLabel != null && { serverLabel: args.serverLabel },
|
|
7941
|
+
...args.session.sessionName != null && { sessionName: args.session.sessionName }
|
|
7555
7942
|
};
|
|
7556
7943
|
}
|
|
7557
7944
|
var LiveActivityNotifier = class {
|
|
@@ -7564,14 +7951,17 @@ var LiveActivityNotifier = class {
|
|
|
7564
7951
|
serverId;
|
|
7565
7952
|
serverLabel;
|
|
7566
7953
|
/**
|
|
7567
|
-
*
|
|
7954
|
+
* Sessions with a currently open (pushed) activity.
|
|
7568
7955
|
*
|
|
7569
|
-
*
|
|
7570
|
-
*
|
|
7571
|
-
*
|
|
7572
|
-
*
|
|
7956
|
+
* An activity opens on a `waiting_input → running` edge (the user sent a
|
|
7957
|
+
* prompt) and closes on the matching `running → waiting_input` edge (the
|
|
7958
|
+
* response, including any sub-agents, finished) — so this set is what makes
|
|
7959
|
+
* the notifier per-turn rather than per-session. A session's very first
|
|
7960
|
+
* `running` (right after spawn, before any user prompt) has no prior
|
|
7961
|
+
* `waiting_input` and therefore no edge, so it never opens an activity —
|
|
7962
|
+
* this is what keeps a fresh/idle session from pushing anything.
|
|
7573
7963
|
*/
|
|
7574
|
-
|
|
7964
|
+
openActivity = /* @__PURE__ */ new Map();
|
|
7575
7965
|
/**
|
|
7576
7966
|
* React to a session status change.
|
|
7577
7967
|
*
|
|
@@ -7579,34 +7969,22 @@ var LiveActivityNotifier = class {
|
|
|
7579
7969
|
* transition, so this returns a promise the caller may ignore and every error
|
|
7580
7970
|
* is logged rather than propagated.
|
|
7581
7971
|
*/
|
|
7582
|
-
async onStatusChange(session) {
|
|
7972
|
+
async onStatusChange(session, previousStatus) {
|
|
7583
7973
|
const status = toLiveActivityStatus(session.status);
|
|
7584
7974
|
try {
|
|
7585
7975
|
if (!status) {
|
|
7586
|
-
await this.endFor(session);
|
|
7976
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
7587
7977
|
return;
|
|
7588
7978
|
}
|
|
7589
|
-
if (
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
const outcome = await this.sender.send({
|
|
7597
|
-
sessionId: session.id,
|
|
7598
|
-
event: "update",
|
|
7599
|
-
contentState
|
|
7600
|
-
});
|
|
7601
|
-
this.lastPushed.set(session.id, status);
|
|
7602
|
-
if (outcome.attempted > 0) {
|
|
7603
|
-
log4.info("live_activity.updated", {
|
|
7604
|
-
event: "live_activity.updated",
|
|
7605
|
-
sessionId: session.id,
|
|
7606
|
-
status,
|
|
7607
|
-
...outcome
|
|
7608
|
-
});
|
|
7979
|
+
if (status === "running" && previousStatus === "waiting_input") {
|
|
7980
|
+
await this.startTurn(session);
|
|
7981
|
+
return;
|
|
7982
|
+
}
|
|
7983
|
+
if (status === "waiting_input" && previousStatus === "running") {
|
|
7984
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
7985
|
+
return;
|
|
7609
7986
|
}
|
|
7987
|
+
await this.maybeSendName(session);
|
|
7610
7988
|
} catch (err) {
|
|
7611
7989
|
log4.error("live_activity.notify_failed", {
|
|
7612
7990
|
event: "live_activity.notify_failed",
|
|
@@ -7616,14 +7994,57 @@ var LiveActivityNotifier = class {
|
|
|
7616
7994
|
});
|
|
7617
7995
|
}
|
|
7618
7996
|
}
|
|
7997
|
+
async startTurn(session) {
|
|
7998
|
+
const contentState = contentStateForSession({
|
|
7999
|
+
session,
|
|
8000
|
+
serverId: this.serverId,
|
|
8001
|
+
serverLabel: this.serverLabel
|
|
8002
|
+
});
|
|
8003
|
+
if (!contentState) return;
|
|
8004
|
+
const outcome = await this.sender.send({
|
|
8005
|
+
sessionId: session.id,
|
|
8006
|
+
event: "update",
|
|
8007
|
+
contentState
|
|
8008
|
+
});
|
|
8009
|
+
this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
|
|
8010
|
+
if (outcome.attempted > 0) {
|
|
8011
|
+
log4.info("live_activity.updated", {
|
|
8012
|
+
event: "live_activity.updated",
|
|
8013
|
+
sessionId: session.id,
|
|
8014
|
+
status: contentState.status,
|
|
8015
|
+
...outcome
|
|
8016
|
+
});
|
|
8017
|
+
}
|
|
8018
|
+
}
|
|
8019
|
+
async maybeSendName(session) {
|
|
8020
|
+
const open2 = this.openActivity.get(session.id);
|
|
8021
|
+
if (!open2 || open2.sessionNameSent || session.sessionName == null) return;
|
|
8022
|
+
const contentState = contentStateForSession({
|
|
8023
|
+
session,
|
|
8024
|
+
serverId: this.serverId,
|
|
8025
|
+
serverLabel: this.serverLabel
|
|
8026
|
+
});
|
|
8027
|
+
if (!contentState) return;
|
|
8028
|
+
const outcome = await this.sender.send({
|
|
8029
|
+
sessionId: session.id,
|
|
8030
|
+
event: "update",
|
|
8031
|
+
contentState
|
|
8032
|
+
});
|
|
8033
|
+
open2.sessionNameSent = true;
|
|
8034
|
+
if (outcome.attempted > 0) {
|
|
8035
|
+
log4.info("live_activity.updated", {
|
|
8036
|
+
event: "live_activity.updated",
|
|
8037
|
+
sessionId: session.id,
|
|
8038
|
+
status: contentState.status,
|
|
8039
|
+
...outcome
|
|
8040
|
+
});
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
7619
8043
|
async endFor(session) {
|
|
7620
|
-
|
|
7621
|
-
|
|
8044
|
+
this.openActivity.delete(session.id);
|
|
8045
|
+
const status = toLiveActivityStatus(session.status);
|
|
7622
8046
|
const contentState = contentStateForSession({
|
|
7623
|
-
session: {
|
|
7624
|
-
...session,
|
|
7625
|
-
status: lastStatus === "waiting_input" ? "waiting_input" : "running"
|
|
7626
|
-
},
|
|
8047
|
+
session: { ...session, status: status ?? "waiting_input" },
|
|
7627
8048
|
serverId: this.serverId,
|
|
7628
8049
|
serverLabel: this.serverLabel
|
|
7629
8050
|
});
|
|
@@ -7637,9 +8058,9 @@ var LiveActivityNotifier = class {
|
|
|
7637
8058
|
});
|
|
7638
8059
|
}
|
|
7639
8060
|
}
|
|
7640
|
-
/** Drop cached state for a session, so a resume re-
|
|
8061
|
+
/** Drop cached state for a session, so a resume re-opens on its next turn. */
|
|
7641
8062
|
forget(sessionId) {
|
|
7642
|
-
this.
|
|
8063
|
+
this.openActivity.delete(sessionId);
|
|
7643
8064
|
}
|
|
7644
8065
|
};
|
|
7645
8066
|
|
|
@@ -7870,7 +8291,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
7870
8291
|
status,
|
|
7871
8292
|
startedAt,
|
|
7872
8293
|
lastOutput: session.lastOutput ?? "",
|
|
7873
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
8294
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
8295
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
7874
8296
|
};
|
|
7875
8297
|
try {
|
|
7876
8298
|
await this.deps.sender.send({
|
|
@@ -7930,7 +8352,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
7930
8352
|
// Carried through unchanged — the whole point of the renewal.
|
|
7931
8353
|
startedAt: args.startedAt,
|
|
7932
8354
|
lastOutput: truncateLastOutput(session.lastOutput ?? ""),
|
|
7933
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
8355
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
8356
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
7934
8357
|
},
|
|
7935
8358
|
now: args.now,
|
|
7936
8359
|
staleDate: args.startedAt + ACTIVITY_MAX_LIFETIME_MS
|
|
@@ -8088,8 +8511,90 @@ function resolveAnswer(pending, body) {
|
|
|
8088
8511
|
}
|
|
8089
8512
|
}
|
|
8090
8513
|
|
|
8514
|
+
// src/services/search/searchQuery.ts
|
|
8515
|
+
var DEFAULT_SEARCH_LIMIT = 50;
|
|
8516
|
+
var MAX_SEARCH_LIMIT = 200;
|
|
8517
|
+
var MAX_QUERY_LENGTH = 256;
|
|
8518
|
+
var SearchQueryError = class extends Error {
|
|
8519
|
+
constructor(message, code) {
|
|
8520
|
+
super(message);
|
|
8521
|
+
this.code = code;
|
|
8522
|
+
}
|
|
8523
|
+
code;
|
|
8524
|
+
};
|
|
8525
|
+
function intOr(raw, fallback) {
|
|
8526
|
+
if (raw === null) return fallback;
|
|
8527
|
+
const n = Number.parseInt(raw, 10);
|
|
8528
|
+
return Number.isFinite(n) ? n : fallback;
|
|
8529
|
+
}
|
|
8530
|
+
function parseSearchQuery(params) {
|
|
8531
|
+
const q = (params.get("q") ?? "").trim();
|
|
8532
|
+
if (!q) {
|
|
8533
|
+
throw new SearchQueryError("Missing query parameter: q", "invalid_query");
|
|
8534
|
+
}
|
|
8535
|
+
if (q.length > MAX_QUERY_LENGTH) {
|
|
8536
|
+
throw new SearchQueryError(`Query exceeds ${MAX_QUERY_LENGTH} characters`, "query_too_long");
|
|
8537
|
+
}
|
|
8538
|
+
const limit = Math.min(
|
|
8539
|
+
Math.max(intOr(params.get("limit"), DEFAULT_SEARCH_LIMIT), 1),
|
|
8540
|
+
MAX_SEARCH_LIMIT
|
|
8541
|
+
);
|
|
8542
|
+
const offset = Math.max(intOr(params.get("offset"), 0), 0);
|
|
8543
|
+
const filters = {};
|
|
8544
|
+
const provider = params.get("provider");
|
|
8545
|
+
if (provider !== null) {
|
|
8546
|
+
if (!isProviderName(provider)) {
|
|
8547
|
+
throw new SearchQueryError(`Unknown provider: ${provider}`, "invalid_filter");
|
|
8548
|
+
}
|
|
8549
|
+
filters.provider = provider;
|
|
8550
|
+
}
|
|
8551
|
+
const projectPath = params.get("projectPath");
|
|
8552
|
+
if (projectPath) filters.projectPath = projectPath;
|
|
8553
|
+
const branch = params.get("branch");
|
|
8554
|
+
if (branch) filters.branch = branch;
|
|
8555
|
+
for (const [key, field] of [
|
|
8556
|
+
["since", "since"],
|
|
8557
|
+
["until", "until"]
|
|
8558
|
+
]) {
|
|
8559
|
+
const raw = params.get(key);
|
|
8560
|
+
if (raw === null) continue;
|
|
8561
|
+
const ms = Date.parse(raw);
|
|
8562
|
+
if (Number.isNaN(ms)) {
|
|
8563
|
+
throw new SearchQueryError(`Invalid ${key}: expected an ISO 8601 date`, "invalid_filter");
|
|
8564
|
+
}
|
|
8565
|
+
filters[field] = ms;
|
|
8566
|
+
}
|
|
8567
|
+
if (filters.since != null && filters.until != null && filters.since > filters.until) {
|
|
8568
|
+
throw new SearchQueryError("`since` must not be after `until`", "invalid_filter");
|
|
8569
|
+
}
|
|
8570
|
+
return { q, limit, offset, filters };
|
|
8571
|
+
}
|
|
8572
|
+
function applyFilters(results, filters) {
|
|
8573
|
+
return results.filter((r) => {
|
|
8574
|
+
if (filters.provider && r.provider !== filters.provider) return false;
|
|
8575
|
+
if (filters.projectPath && r.projectPath !== filters.projectPath) return false;
|
|
8576
|
+
if (filters.branch && r.branch !== filters.branch) return false;
|
|
8577
|
+
if (filters.since != null || filters.until != null) {
|
|
8578
|
+
const ts = r.lastActivity == null ? Number.NaN : new Date(r.lastActivity).getTime();
|
|
8579
|
+
if (Number.isNaN(ts)) return false;
|
|
8580
|
+
if (filters.since != null && ts < filters.since) return false;
|
|
8581
|
+
if (filters.until != null && ts > filters.until) return false;
|
|
8582
|
+
}
|
|
8583
|
+
return true;
|
|
8584
|
+
});
|
|
8585
|
+
}
|
|
8586
|
+
function paginate(results, offset, limit) {
|
|
8587
|
+
const items = results.slice(offset, offset + limit);
|
|
8588
|
+
return {
|
|
8589
|
+
items,
|
|
8590
|
+
total: results.length,
|
|
8591
|
+
offset,
|
|
8592
|
+
hasMore: offset + items.length < results.length
|
|
8593
|
+
};
|
|
8594
|
+
}
|
|
8595
|
+
|
|
8091
8596
|
// src/services/sessions/conversationBusy.ts
|
|
8092
|
-
var
|
|
8597
|
+
var import_fs18 = require("fs");
|
|
8093
8598
|
var RESUME_BUSY_WINDOW_MS = 12e4;
|
|
8094
8599
|
function resolveResumeBusyWindowMs(env = process.env) {
|
|
8095
8600
|
const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
|
|
@@ -8106,7 +8611,7 @@ function conversationBusy(input) {
|
|
|
8106
8611
|
let lastActivityMs = null;
|
|
8107
8612
|
if (input.jsonlPath) {
|
|
8108
8613
|
try {
|
|
8109
|
-
const mtimeMs = (0,
|
|
8614
|
+
const mtimeMs = (0, import_fs18.statSync)(input.jsonlPath).mtimeMs;
|
|
8110
8615
|
const age = now - mtimeMs;
|
|
8111
8616
|
lastActivityMs = Math.max(0, age);
|
|
8112
8617
|
const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
|
|
@@ -8235,6 +8740,50 @@ async function reconcileSessions(rows, probe, currentInstanceId) {
|
|
|
8235
8740
|
return Promise.all(rows.map((row) => classifySession(row, probe, currentInstanceId)));
|
|
8236
8741
|
}
|
|
8237
8742
|
|
|
8743
|
+
// src/services/sessions/rehydrateSessions.ts
|
|
8744
|
+
var REHYDRATE_MAX = 25;
|
|
8745
|
+
var REHYDRATE_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8746
|
+
var AGENT_EXIT_SOURCES = /* @__PURE__ */ new Set(["exit", "process-exit"]);
|
|
8747
|
+
function shouldRehydrate(row, opts) {
|
|
8748
|
+
if (!opts.projectExists(row.project_path)) return false;
|
|
8749
|
+
if (opts.now - row.status_updated_at > REHYDRATE_WINDOW_MS) return false;
|
|
8750
|
+
if (AGENT_EXIT_SOURCES.has(row.status_source) && row.failure_reason == null) return false;
|
|
8751
|
+
return true;
|
|
8752
|
+
}
|
|
8753
|
+
function rowToStubSession(row) {
|
|
8754
|
+
return {
|
|
8755
|
+
id: row.session_id,
|
|
8756
|
+
provider: row.provider,
|
|
8757
|
+
projectPath: row.project_path,
|
|
8758
|
+
projectName: row.project_name,
|
|
8759
|
+
branch: row.branch,
|
|
8760
|
+
// No PTY exists for a stub, so this is the only truthful status.
|
|
8761
|
+
status: "idle",
|
|
8762
|
+
startedAt: new Date(row.started_at),
|
|
8763
|
+
completedAt: row.completed_at != null ? new Date(row.completed_at) : null,
|
|
8764
|
+
promptCount: row.prompt_count,
|
|
8765
|
+
lastOutput: "",
|
|
8766
|
+
rehydrated: true,
|
|
8767
|
+
...row.session_name != null && { sessionName: row.session_name },
|
|
8768
|
+
...row.project_id != null && { projectId: row.project_id },
|
|
8769
|
+
...row.bound_conversation_id != null && { boundConversationId: row.bound_conversation_id },
|
|
8770
|
+
...row.resumed_from_conversation_id != null && {
|
|
8771
|
+
resumedFromConversationId: row.resumed_from_conversation_id
|
|
8772
|
+
},
|
|
8773
|
+
...row.failure_reason != null && { failureReason: row.failure_reason },
|
|
8774
|
+
...row.last_activity_at != null && { lastActivityAt: new Date(row.last_activity_at) },
|
|
8775
|
+
// Only `shutdown` crosses over. It is the one registry source that is also a
|
|
8776
|
+
// wire StatusSource *and* that genuinely describes the `idle` above — the
|
|
8777
|
+
// streamer stopped this session. A crashed row still says `transition` over
|
|
8778
|
+
// a `running` status, and copying that here would attach observed-confidence
|
|
8779
|
+
// provenance to a status we derived at boot, so leave it unset instead.
|
|
8780
|
+
...row.status_source === "shutdown" && {
|
|
8781
|
+
statusSource: "shutdown",
|
|
8782
|
+
statusUpdatedAt: new Date(row.status_updated_at)
|
|
8783
|
+
}
|
|
8784
|
+
};
|
|
8785
|
+
}
|
|
8786
|
+
|
|
8238
8787
|
// src/types.ts
|
|
8239
8788
|
function confidenceForSource(source) {
|
|
8240
8789
|
return source === "timeout-fallback" || source === "quiet-fallback" ? "inferred" : "observed";
|
|
@@ -8382,14 +8931,15 @@ function managedToResponse(s, ptyAttached) {
|
|
|
8382
8931
|
// Lifecycle for a session this run knows about. `attached` while we hold
|
|
8383
8932
|
// its PTY; once the PTY is gone the session is terminal from this run's
|
|
8384
8933
|
// perspective — `failed` when it recorded a reason, else `completed`.
|
|
8385
|
-
//
|
|
8386
|
-
//
|
|
8387
|
-
//
|
|
8388
|
-
|
|
8389
|
-
|
|
8934
|
+
// A `rehydrated` stub is the exception: the boot rehydrator seeded it from
|
|
8935
|
+
// the durable registry, so it is a previous run's session with no process
|
|
8936
|
+
// behind it — `resumable`, and `historical` rather than `managed`
|
|
8937
|
+
// (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).
|
|
8938
|
+
lifecycle: ptyAttached ? "attached" : s.rehydrated ? "resumable" : s.failureReason != null ? "failed" : "completed",
|
|
8939
|
+
lifecycleSource: ptyAttached ? "spawn" : s.rehydrated ? "reconcile" : "exit",
|
|
8390
8940
|
// We spawned it, so `status` is the authoritative signal — no inferred
|
|
8391
8941
|
// `activity` is attached for managed sessions.
|
|
8392
|
-
ownership: "managed",
|
|
8942
|
+
ownership: s.rehydrated ? "historical" : "managed",
|
|
8393
8943
|
projectPath: s.projectPath,
|
|
8394
8944
|
projectName: s.projectName,
|
|
8395
8945
|
branch: s.branch,
|
|
@@ -8790,6 +9340,8 @@ var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
|
8790
9340
|
var GRACE_MAX_DEFERS = 4;
|
|
8791
9341
|
var IDLE_REAP_AFTER_MS = 6 * 60 * 60 * 1e3;
|
|
8792
9342
|
var IDLE_REAP_SWEEP_MS = 5 * 60 * 1e3;
|
|
9343
|
+
var SEARCH_OVERFETCH = 4;
|
|
9344
|
+
var SEARCH_MAX_SCAN = 1e3;
|
|
8793
9345
|
var RESUME_DISCOVERY_TIMEOUT_MS = 750;
|
|
8794
9346
|
var DISCOVERY_TTL_MS = 15e3;
|
|
8795
9347
|
var ADOPT_KILL_TIMEOUT_MS = 5e3;
|
|
@@ -8977,10 +9529,13 @@ var StreamerServer = class {
|
|
|
8977
9529
|
projectsRepo = null;
|
|
8978
9530
|
conversationsRepo = null;
|
|
8979
9531
|
sessionsRepo = null;
|
|
8980
|
-
// Durable session registry (C1 Phase 2). Null when
|
|
8981
|
-
//
|
|
8982
|
-
// taking the server down with it, so every write goes through `?.`.
|
|
9532
|
+
// Durable session registry (C1 Phase 2). Null when runtime.db failed to open
|
|
9533
|
+
// — persistence degrades to today's in-memory-only behaviour rather than
|
|
9534
|
+
// taking the server down with it, so every write goes through `?.`. Note the
|
|
9535
|
+
// handle is runtime.db, NOT the conversation cache: a cache failure used to
|
|
9536
|
+
// null this repo and silently disable all session persistence.
|
|
8983
9537
|
managedSessionsRepo = null;
|
|
9538
|
+
runtimeStore = null;
|
|
8984
9539
|
// Identifies this streamer run. A registry row carrying a different id is a
|
|
8985
9540
|
// session that outlived the process that started it.
|
|
8986
9541
|
streamerInstanceId = (0, import_crypto11.randomUUID)();
|
|
@@ -8999,6 +9554,7 @@ var StreamerServer = class {
|
|
|
8999
9554
|
liveActivityRenewal = null;
|
|
9000
9555
|
discoveryCache = null;
|
|
9001
9556
|
cacheDir;
|
|
9557
|
+
runtimeDbPath;
|
|
9002
9558
|
tailSize;
|
|
9003
9559
|
directoryDebounceMs;
|
|
9004
9560
|
// Trailing-debounced trigger that flags the scanner stale after a quiet
|
|
@@ -9030,7 +9586,7 @@ var StreamerServer = class {
|
|
|
9030
9586
|
this.skipStartupWarmup = config.skipStartupWarmup ?? false;
|
|
9031
9587
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
9032
9588
|
this.scanProfiles = config.scanProfiles;
|
|
9033
|
-
this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0,
|
|
9589
|
+
this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0, import_os10.homedir)(), ".codex", "sessions")];
|
|
9034
9590
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
9035
9591
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
9036
9592
|
this.featureFlags = resolveFeatureFlags({ cli: config.featureFlags, yaml: loadFeatureFlags() });
|
|
@@ -9044,7 +9600,8 @@ var StreamerServer = class {
|
|
|
9044
9600
|
this.claudeFlagsPersistable = config.claudeFlags === void 0;
|
|
9045
9601
|
this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
|
|
9046
9602
|
this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
9047
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0,
|
|
9603
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0, import_os10.homedir)(), ".threadbase", "cache");
|
|
9604
|
+
this.runtimeDbPath = config.runtimeDbPath ?? process.env.THREADBASE_RUNTIME_DB ?? (0, import_path18.join)(process.env.THREADBASE_CONFIG_DIR ?? (0, import_path18.join)((0, import_os10.homedir)(), ".threadbase"), "runtime.db");
|
|
9048
9605
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
9049
9606
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
9050
9607
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -9095,7 +9652,7 @@ var StreamerServer = class {
|
|
|
9095
9652
|
const seqs = cache.extendMessageIndex(
|
|
9096
9653
|
filePath,
|
|
9097
9654
|
spans,
|
|
9098
|
-
(0,
|
|
9655
|
+
(0, import_fs19.statSync)(filePath),
|
|
9099
9656
|
readFrom,
|
|
9100
9657
|
endOffset
|
|
9101
9658
|
);
|
|
@@ -9215,6 +9772,7 @@ var StreamerServer = class {
|
|
|
9215
9772
|
if (resp) this.wsHub.broadcast({ type: "session_ready", session: resp });
|
|
9216
9773
|
},
|
|
9217
9774
|
onStatusChange: (session) => {
|
|
9775
|
+
const previousStatus = this.sessionStore.getManaged(session.id)?.status;
|
|
9218
9776
|
this.sessionStore.updateManaged(session.id, {
|
|
9219
9777
|
status: session.status,
|
|
9220
9778
|
completedAt: session.completedAt,
|
|
@@ -9269,7 +9827,7 @@ var StreamerServer = class {
|
|
|
9269
9827
|
if (resp) {
|
|
9270
9828
|
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
9271
9829
|
}
|
|
9272
|
-
void this.liveActivityNotifier?.onStatusChange(session);
|
|
9830
|
+
void this.liveActivityNotifier?.onStatusChange(session, previousStatus);
|
|
9273
9831
|
this.sessionStatusBus.emit(`status:${session.id}`, session.status);
|
|
9274
9832
|
}
|
|
9275
9833
|
});
|
|
@@ -9320,6 +9878,7 @@ var StreamerServer = class {
|
|
|
9320
9878
|
conversationsRepo: () => this.conversationsRepo,
|
|
9321
9879
|
sessionsRepo: () => this.sessionsRepo,
|
|
9322
9880
|
cacheMetadataRepo: () => this.cacheMetadataRepo,
|
|
9881
|
+
runtimeStore: () => this.runtimeStore,
|
|
9323
9882
|
ptyAttachedIds: () => this.ptyAttachedIds(),
|
|
9324
9883
|
handleListSessions: (url, res) => this.handleListSessions(url, res),
|
|
9325
9884
|
handleSessionsCount: (res) => this.handleSessionsCount(res),
|
|
@@ -9558,14 +10117,14 @@ var StreamerServer = class {
|
|
|
9558
10117
|
}
|
|
9559
10118
|
this.apnsClient = new ApnsClient(creds);
|
|
9560
10119
|
const sender = new LiveActivitySender(this.apnsClient, pushRepo);
|
|
9561
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
9562
|
-
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0,
|
|
10120
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os10.hostname)();
|
|
10121
|
+
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os10.hostname)());
|
|
9563
10122
|
this.liveActivityRenewal = new LiveActivityRenewalScheduler({
|
|
9564
10123
|
repo: pushRepo,
|
|
9565
10124
|
sender,
|
|
9566
10125
|
sessionStore: this.sessionStore,
|
|
9567
10126
|
serverId,
|
|
9568
|
-
serverLabel: (0,
|
|
10127
|
+
serverLabel: (0, import_os10.hostname)()
|
|
9569
10128
|
});
|
|
9570
10129
|
this.liveActivityRenewal.start();
|
|
9571
10130
|
this.log.info("Live Activity push enabled", {
|
|
@@ -9619,6 +10178,58 @@ var StreamerServer = class {
|
|
|
9619
10178
|
}
|
|
9620
10179
|
return verdicts;
|
|
9621
10180
|
}
|
|
10181
|
+
/**
|
|
10182
|
+
* Seed the session list with what previous runs left behind (persistence plan
|
|
10183
|
+
* Phase 1, gaps G1/G2/G8).
|
|
10184
|
+
*
|
|
10185
|
+
* Reconciliation classifies rows and stops there; a verdict is overlaid onto a
|
|
10186
|
+
* SessionResponse that already exists, and after a clean restart none does —
|
|
10187
|
+
* `SessionStore` starts empty. So the user's session did not become
|
|
10188
|
+
* `resumable`, it became *absent*. This is the half that puts it back.
|
|
10189
|
+
*
|
|
10190
|
+
* The seeded stubs hold no PTY and are never handed to `LiveSessionManager`,
|
|
10191
|
+
* so `reapIdleSessions` and `startGraceTimer` — both of which iterate
|
|
10192
|
+
* `ptyManager.listSessions()` — cannot observe them. A later resume calls
|
|
10193
|
+
* `sessionStore.addManaged` with the real session, which overwrites the stub
|
|
10194
|
+
* by id rather than duplicating it.
|
|
10195
|
+
*/
|
|
10196
|
+
rehydratePreviousSessions(verdicts) {
|
|
10197
|
+
if (!this.featureFlags.sessionRehydration || !this.managedSessionsRepo) return;
|
|
10198
|
+
try {
|
|
10199
|
+
const now = Date.now();
|
|
10200
|
+
const rows = this.managedSessionsRepo.listRecoverable({
|
|
10201
|
+
sinceMs: now - REHYDRATE_WINDOW_MS,
|
|
10202
|
+
limit: REHYDRATE_MAX + 1
|
|
10203
|
+
});
|
|
10204
|
+
const truncated = rows.length > REHYDRATE_MAX;
|
|
10205
|
+
const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;
|
|
10206
|
+
if (candidates.length === 0) return;
|
|
10207
|
+
const lifecycleByVerdict = new Map(verdicts.map((v) => [v.sessionId, v.lifecycle]));
|
|
10208
|
+
let rehydrated = 0;
|
|
10209
|
+
for (const row of candidates) {
|
|
10210
|
+
if (this.sessionStore.getManaged(row.session_id)) continue;
|
|
10211
|
+
if (!shouldRehydrate(row, { now, projectExists: import_fs19.existsSync })) continue;
|
|
10212
|
+
this.sessionStore.addManaged(rowToStubSession(row));
|
|
10213
|
+
this.sessionLifecycles.set(
|
|
10214
|
+
row.session_id,
|
|
10215
|
+
lifecycleByVerdict.get(row.session_id) ?? "resumable"
|
|
10216
|
+
);
|
|
10217
|
+
if (row.completed_at != null) this.selfPtyEndedAt.set(row.session_id, row.completed_at);
|
|
10218
|
+
rehydrated++;
|
|
10219
|
+
}
|
|
10220
|
+
this.log.info(`[rehydrate] recovered ${rehydrated} session(s) from the registry`, {
|
|
10221
|
+
event: "sessions.rehydrated",
|
|
10222
|
+
rehydrated,
|
|
10223
|
+
skipped: candidates.length - rehydrated,
|
|
10224
|
+
truncated
|
|
10225
|
+
});
|
|
10226
|
+
} catch (err) {
|
|
10227
|
+
this.log.warn("[rehydrate] failed to rehydrate previous sessions", {
|
|
10228
|
+
event: "sessions.rehydrate_failed",
|
|
10229
|
+
err
|
|
10230
|
+
});
|
|
10231
|
+
}
|
|
10232
|
+
}
|
|
9622
10233
|
/**
|
|
9623
10234
|
* Pick a token guaranteed to appear in the spawned process's argv, for the
|
|
9624
10235
|
* reconciler's pid-reuse guard.
|
|
@@ -9852,6 +10463,17 @@ var StreamerServer = class {
|
|
|
9852
10463
|
port,
|
|
9853
10464
|
event: "server.listening"
|
|
9854
10465
|
});
|
|
10466
|
+
try {
|
|
10467
|
+
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
10468
|
+
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
10469
|
+
} catch (err) {
|
|
10470
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10471
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
10472
|
+
this.log.error(
|
|
10473
|
+
`Runtime store failed to open \u2014 session persistence DISABLED; sessions will not survive a restart.` + (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : "") + ` (${message})`,
|
|
10474
|
+
{ error: message, abiMismatch, path: this.runtimeDbPath, event: "runtime.open_failed" }
|
|
10475
|
+
);
|
|
10476
|
+
}
|
|
9855
10477
|
try {
|
|
9856
10478
|
this.cache = ConversationCache.open(
|
|
9857
10479
|
(0, import_path18.join)(this.cacheDir, "cache.db"),
|
|
@@ -9879,8 +10501,20 @@ var StreamerServer = class {
|
|
|
9879
10501
|
this.projectsRepo = new ProjectsRepository(db);
|
|
9880
10502
|
this.conversationsRepo = new ConversationsRepository(this.cache);
|
|
9881
10503
|
this.sessionsRepo = new SessionsRepository(this.sessionStore);
|
|
9882
|
-
|
|
9883
|
-
|
|
10504
|
+
try {
|
|
10505
|
+
const copied = this.runtimeStore?.importLegacyManagedSessions(db) ?? 0;
|
|
10506
|
+
if (copied > 0) {
|
|
10507
|
+
this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
|
|
10508
|
+
copied,
|
|
10509
|
+
event: "runtime.legacy_import"
|
|
10510
|
+
});
|
|
10511
|
+
}
|
|
10512
|
+
} catch (err) {
|
|
10513
|
+
this.log.warn("[registry] legacy managed_sessions copy failed", {
|
|
10514
|
+
event: "runtime.legacy_import_failed",
|
|
10515
|
+
err
|
|
10516
|
+
});
|
|
10517
|
+
}
|
|
9884
10518
|
this.cacheMetadataRepo = new CacheMetadataRepository(db);
|
|
9885
10519
|
this.pushRepo = new PushRepository(db);
|
|
9886
10520
|
this.devicesRepo = new DevicesRepository(db);
|
|
@@ -9904,7 +10538,7 @@ var StreamerServer = class {
|
|
|
9904
10538
|
this.fileWatcher.watchDirectory(dir);
|
|
9905
10539
|
}
|
|
9906
10540
|
for (const dir of this.codexRoots) {
|
|
9907
|
-
if (!(0,
|
|
10541
|
+
if (!(0, import_fs19.existsSync)(dir)) continue;
|
|
9908
10542
|
this.fileWatcher.watchDirectory(dir);
|
|
9909
10543
|
}
|
|
9910
10544
|
} catch (err) {
|
|
@@ -9916,6 +10550,7 @@ var StreamerServer = class {
|
|
|
9916
10550
|
);
|
|
9917
10551
|
this.scannerPersistenceDisabled = true;
|
|
9918
10552
|
}
|
|
10553
|
+
void this.reconcilePreviousSessions().then((v) => this.rehydratePreviousSessions(v));
|
|
9919
10554
|
if (this.skipStartupWarmup) {
|
|
9920
10555
|
this.log.debug?.("startup warm-up scan skipped (skipStartupWarmup)", {
|
|
9921
10556
|
event: "cache.warmup_skipped"
|
|
@@ -10123,6 +10758,7 @@ var StreamerServer = class {
|
|
|
10123
10758
|
this.allScanners.clear();
|
|
10124
10759
|
this.scanner = null;
|
|
10125
10760
|
this.cache?.close();
|
|
10761
|
+
this.runtimeStore?.close();
|
|
10126
10762
|
this.ptyManager.dispose();
|
|
10127
10763
|
this.fileWatcher.dispose();
|
|
10128
10764
|
this.externalTails.clear();
|
|
@@ -10177,7 +10813,7 @@ var StreamerServer = class {
|
|
|
10177
10813
|
}
|
|
10178
10814
|
let body;
|
|
10179
10815
|
try {
|
|
10180
|
-
body = await
|
|
10816
|
+
body = await readBody2(req);
|
|
10181
10817
|
} catch (err) {
|
|
10182
10818
|
const message = err instanceof Error ? err.message : "Invalid body";
|
|
10183
10819
|
json(res, 400, { error: message });
|
|
@@ -10223,7 +10859,7 @@ var StreamerServer = class {
|
|
|
10223
10859
|
nonce: sealed.nonce,
|
|
10224
10860
|
ephemeralPublicKey: sealed.ephemeralPublicKey,
|
|
10225
10861
|
publicUrl: this.publicUrl,
|
|
10226
|
-
machineName: (0,
|
|
10862
|
+
machineName: (0, import_os10.hostname)(),
|
|
10227
10863
|
...device && {
|
|
10228
10864
|
deviceId: device.deviceId,
|
|
10229
10865
|
deviceToken: device.deviceToken,
|
|
@@ -10351,7 +10987,7 @@ var StreamerServer = class {
|
|
|
10351
10987
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
10352
10988
|
return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
|
|
10353
10989
|
}
|
|
10354
|
-
return [(0, import_path18.join)((0,
|
|
10990
|
+
return [(0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects")];
|
|
10355
10991
|
}
|
|
10356
10992
|
/**
|
|
10357
10993
|
* Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
|
|
@@ -10570,7 +11206,8 @@ var StreamerServer = class {
|
|
|
10570
11206
|
}
|
|
10571
11207
|
handleSessionsCount(res) {
|
|
10572
11208
|
if (this.rejectIfWarmingUp(res)) return;
|
|
10573
|
-
|
|
11209
|
+
const total = this.sessionStore.list(this.ptyAttachedIds()).filter((s) => s.ownership !== "historical").length;
|
|
11210
|
+
json(res, 200, { total });
|
|
10574
11211
|
}
|
|
10575
11212
|
handleGetRecentSessions(url, res) {
|
|
10576
11213
|
if (this.rejectIfWarmingUp(res)) return;
|
|
@@ -10753,20 +11390,20 @@ var StreamerServer = class {
|
|
|
10753
11390
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
10754
11391
|
return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
|
|
10755
11392
|
}
|
|
10756
|
-
return [(0, import_path18.join)((0,
|
|
11393
|
+
return [(0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects")];
|
|
10757
11394
|
}
|
|
10758
11395
|
findJsonlPath(uuid) {
|
|
10759
11396
|
const filename = `${uuid}.jsonl`;
|
|
10760
11397
|
for (const projectsDir of this.projectsDirs()) {
|
|
10761
|
-
if (!(0,
|
|
10762
|
-
for (const dir of (0,
|
|
11398
|
+
if (!(0, import_fs19.existsSync)(projectsDir)) continue;
|
|
11399
|
+
for (const dir of (0, import_fs19.readdirSync)(projectsDir)) {
|
|
10763
11400
|
const fp = (0, import_path18.join)(projectsDir, dir, filename);
|
|
10764
|
-
if ((0,
|
|
11401
|
+
if ((0, import_fs19.existsSync)(fp)) return fp;
|
|
10765
11402
|
const projectDir = (0, import_path18.join)(projectsDir, dir);
|
|
10766
11403
|
try {
|
|
10767
|
-
for (const sub of (0,
|
|
11404
|
+
for (const sub of (0, import_fs19.readdirSync)(projectDir)) {
|
|
10768
11405
|
const subagentPath = (0, import_path18.join)(projectDir, sub, "subagents", filename);
|
|
10769
|
-
if ((0,
|
|
11406
|
+
if ((0, import_fs19.existsSync)(subagentPath)) return subagentPath;
|
|
10770
11407
|
}
|
|
10771
11408
|
} catch {
|
|
10772
11409
|
}
|
|
@@ -10776,7 +11413,7 @@ var StreamerServer = class {
|
|
|
10776
11413
|
}
|
|
10777
11414
|
async readCwdFromJsonl(filePath) {
|
|
10778
11415
|
return new Promise((resolve2) => {
|
|
10779
|
-
const rl = (0, import_readline.createInterface)({ input: (0,
|
|
11416
|
+
const rl = (0, import_readline.createInterface)({ input: (0, import_fs19.createReadStream)(filePath), crlfDelay: Infinity });
|
|
10780
11417
|
let found = false;
|
|
10781
11418
|
rl.on("line", (line) => {
|
|
10782
11419
|
if (found) return;
|
|
@@ -10866,7 +11503,7 @@ var StreamerServer = class {
|
|
|
10866
11503
|
if (this.isManagedTailPath(key)) return;
|
|
10867
11504
|
let mtimeMs;
|
|
10868
11505
|
try {
|
|
10869
|
-
mtimeMs = (0,
|
|
11506
|
+
mtimeMs = (0, import_fs19.statSync)(filePath).mtimeMs;
|
|
10870
11507
|
} catch {
|
|
10871
11508
|
return;
|
|
10872
11509
|
}
|
|
@@ -11048,7 +11685,7 @@ var StreamerServer = class {
|
|
|
11048
11685
|
if (!conv.filePath) return false;
|
|
11049
11686
|
let mtimeMs = null;
|
|
11050
11687
|
try {
|
|
11051
|
-
mtimeMs = (0,
|
|
11688
|
+
mtimeMs = (0, import_fs19.statSync)(conv.filePath).mtimeMs;
|
|
11052
11689
|
} catch {
|
|
11053
11690
|
return false;
|
|
11054
11691
|
}
|
|
@@ -11303,7 +11940,7 @@ var StreamerServer = class {
|
|
|
11303
11940
|
}
|
|
11304
11941
|
let body;
|
|
11305
11942
|
try {
|
|
11306
|
-
body = await
|
|
11943
|
+
body = await readBody2(req);
|
|
11307
11944
|
} catch {
|
|
11308
11945
|
res.setHeader("Accept-Query", "application/json");
|
|
11309
11946
|
json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
@@ -11341,17 +11978,27 @@ var StreamerServer = class {
|
|
|
11341
11978
|
});
|
|
11342
11979
|
}
|
|
11343
11980
|
async handleSearch(url, res) {
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11981
|
+
let parsed;
|
|
11982
|
+
try {
|
|
11983
|
+
parsed = parseSearchQuery(url.searchParams);
|
|
11984
|
+
} catch (err) {
|
|
11985
|
+
if (err instanceof SearchQueryError) {
|
|
11986
|
+
json(res, 400, { error: err.message, code: err.code });
|
|
11987
|
+
return;
|
|
11988
|
+
}
|
|
11989
|
+
throw err;
|
|
11348
11990
|
}
|
|
11349
|
-
const
|
|
11991
|
+
const { q, limit, offset, filters } = parsed;
|
|
11992
|
+
const startedAt = Date.now();
|
|
11350
11993
|
const scanner = await this.getScanner();
|
|
11351
11994
|
const results = await (0, import_scanner3.search)(
|
|
11352
11995
|
q,
|
|
11353
11996
|
{
|
|
11354
|
-
|
|
11997
|
+
// Fetch beyond the requested page: filters below are applied AFTER the
|
|
11998
|
+
// scanner returns, so slicing at `limit` here would drop results that a
|
|
11999
|
+
// later page should contain. Bounded so a broad query cannot pull an
|
|
12000
|
+
// unbounded set into memory.
|
|
12001
|
+
limit: Math.min(offset + limit * SEARCH_OVERFETCH, SEARCH_MAX_SCAN),
|
|
11355
12002
|
include: "conversations",
|
|
11356
12003
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
11357
12004
|
...this.codexScanOpts()
|
|
@@ -11375,13 +12022,24 @@ var StreamerServer = class {
|
|
|
11375
12022
|
lastActivity: r.meta.timestamp,
|
|
11376
12023
|
firstMessage: r.meta.firstMessage ?? void 0,
|
|
11377
12024
|
lastMessage: r.meta.lastMessage ?? void 0,
|
|
11378
|
-
provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER
|
|
12025
|
+
provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER,
|
|
12026
|
+
// The scanner already computes relevance and match snippets; the previous
|
|
12027
|
+
// adapter discarded both, so results arrived in an unexplained order with
|
|
12028
|
+
// no indication of WHY anything matched.
|
|
12029
|
+
score: r.score,
|
|
12030
|
+
matches: Array.isArray(r.matches) ? r.matches.map((m) => ({
|
|
12031
|
+
field: m.field,
|
|
12032
|
+
snippet: m.snippet
|
|
12033
|
+
})) : []
|
|
11379
12034
|
}));
|
|
12035
|
+
const page = paginate(applyFilters(adapted, filters), offset, limit);
|
|
11380
12036
|
json(res, 200, {
|
|
11381
|
-
conversations:
|
|
11382
|
-
hasMore:
|
|
11383
|
-
offset:
|
|
11384
|
-
total:
|
|
12037
|
+
conversations: page.items,
|
|
12038
|
+
hasMore: page.hasMore,
|
|
12039
|
+
offset: page.offset,
|
|
12040
|
+
total: page.total,
|
|
12041
|
+
// Query timing, so a slow search is diagnosable rather than merely felt.
|
|
12042
|
+
tookMs: Date.now() - startedAt
|
|
11385
12043
|
});
|
|
11386
12044
|
}
|
|
11387
12045
|
async handleListSessions(url, res) {
|
|
@@ -11427,7 +12085,7 @@ var StreamerServer = class {
|
|
|
11427
12085
|
if (this.rejectIfWarmingUp(res)) return;
|
|
11428
12086
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
11429
12087
|
if (session) {
|
|
11430
|
-
if (!(0,
|
|
12088
|
+
if (!(0, import_fs19.existsSync)(session.projectPath)) {
|
|
11431
12089
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
11432
12090
|
}
|
|
11433
12091
|
const reconciled = this.withReconciledLifecycle([session])[0];
|
|
@@ -11454,7 +12112,7 @@ var StreamerServer = class {
|
|
|
11454
12112
|
json(res, 404, { error: "Session not found" });
|
|
11455
12113
|
}
|
|
11456
12114
|
async handleResume(req, res) {
|
|
11457
|
-
const body = await
|
|
12115
|
+
const body = await readBody2(req);
|
|
11458
12116
|
const sessionId = body.sessionId ?? body.conversationId;
|
|
11459
12117
|
if (!sessionId) {
|
|
11460
12118
|
json(res, 400, { error: "Missing sessionId" });
|
|
@@ -11586,7 +12244,7 @@ var StreamerServer = class {
|
|
|
11586
12244
|
return;
|
|
11587
12245
|
}
|
|
11588
12246
|
if (this.agentConfig.enabled) {
|
|
11589
|
-
const body2 = await
|
|
12247
|
+
const body2 = await readBody2(req);
|
|
11590
12248
|
const cache = this.cache;
|
|
11591
12249
|
if (!cache) {
|
|
11592
12250
|
json(res, 503, {
|
|
@@ -11605,7 +12263,7 @@ var StreamerServer = class {
|
|
|
11605
12263
|
json(res, result.status, result.body);
|
|
11606
12264
|
return;
|
|
11607
12265
|
}
|
|
11608
|
-
const body = await
|
|
12266
|
+
const body = await readBody2(req);
|
|
11609
12267
|
const { input, keys } = body;
|
|
11610
12268
|
let idempotencyKey;
|
|
11611
12269
|
try {
|
|
@@ -11769,7 +12427,7 @@ var StreamerServer = class {
|
|
|
11769
12427
|
});
|
|
11770
12428
|
}
|
|
11771
12429
|
async handleSendAnswer(sessionId, req, res) {
|
|
11772
|
-
const body = await
|
|
12430
|
+
const body = await readBody2(req);
|
|
11773
12431
|
const pending = this.pendingQuestions.get(sessionId);
|
|
11774
12432
|
const resolution = resolveAnswer(pending, body);
|
|
11775
12433
|
if (!resolution.ok) {
|
|
@@ -11798,7 +12456,7 @@ var StreamerServer = class {
|
|
|
11798
12456
|
json(res, 400, { error: "Session has no project path" });
|
|
11799
12457
|
return;
|
|
11800
12458
|
}
|
|
11801
|
-
const body = await
|
|
12459
|
+
const body = await readBody2(req);
|
|
11802
12460
|
const { filename, mimeType, dataBase64 } = body ?? {};
|
|
11803
12461
|
if (typeof filename !== "string" || typeof mimeType !== "string" || typeof dataBase64 !== "string") {
|
|
11804
12462
|
json(res, 400, { error: "Missing filename, mimeType, or dataBase64" });
|
|
@@ -12000,7 +12658,7 @@ var StreamerServer = class {
|
|
|
12000
12658
|
return;
|
|
12001
12659
|
}
|
|
12002
12660
|
if (this.agentConfig.enabled) {
|
|
12003
|
-
const body2 = await
|
|
12661
|
+
const body2 = await readBody2(req);
|
|
12004
12662
|
const result = await handleStartAgentSession(body2, {
|
|
12005
12663
|
sessionStore: this.sessionStore,
|
|
12006
12664
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
@@ -12014,7 +12672,7 @@ var StreamerServer = class {
|
|
|
12014
12672
|
}
|
|
12015
12673
|
return;
|
|
12016
12674
|
}
|
|
12017
|
-
const body = await
|
|
12675
|
+
const body = await readBody2(req);
|
|
12018
12676
|
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
12019
12677
|
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
12020
12678
|
json(res, 400, { error: "Invalid provider" });
|
|
@@ -12151,7 +12809,7 @@ var StreamerServer = class {
|
|
|
12151
12809
|
// file isn't slurped in full.
|
|
12152
12810
|
readFirstLineSessionId(filePath) {
|
|
12153
12811
|
try {
|
|
12154
|
-
const content = (0,
|
|
12812
|
+
const content = (0, import_fs19.readFileSync)(filePath, "utf8");
|
|
12155
12813
|
const nl = content.indexOf("\n");
|
|
12156
12814
|
const firstLine = nl === -1 ? content : content.slice(0, nl);
|
|
12157
12815
|
if (!firstLine.trim()) return null;
|
|
@@ -12166,7 +12824,7 @@ var StreamerServer = class {
|
|
|
12166
12824
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
12167
12825
|
watchForJsonl(sessionId, projectPath) {
|
|
12168
12826
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
12169
|
-
const projectsDir = (0, import_path18.join)((0,
|
|
12827
|
+
const projectsDir = (0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects", encoded);
|
|
12170
12828
|
const expectedFile = `${sessionId}.jsonl`;
|
|
12171
12829
|
const filePath = (0, import_path18.join)(projectsDir, expectedFile);
|
|
12172
12830
|
const deadline = Date.now() + 12e4;
|
|
@@ -12186,11 +12844,11 @@ var StreamerServer = class {
|
|
|
12186
12844
|
cleanup();
|
|
12187
12845
|
return;
|
|
12188
12846
|
}
|
|
12189
|
-
let resolvedFilePath = (0,
|
|
12190
|
-
if (!resolvedFilePath && (0,
|
|
12847
|
+
let resolvedFilePath = (0, import_fs19.existsSync)(filePath) ? filePath : null;
|
|
12848
|
+
if (!resolvedFilePath && (0, import_fs19.existsSync)(projectsDir)) {
|
|
12191
12849
|
try {
|
|
12192
12850
|
const now = Date.now();
|
|
12193
|
-
const match = (0,
|
|
12851
|
+
const match = (0, import_fs19.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs19.statSync)((0, import_path18.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
12194
12852
|
({ f }) => (0, import_path18.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path18.join)(projectsDir, f)) === sessionId
|
|
12195
12853
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
12196
12854
|
if (match) resolvedFilePath = (0, import_path18.join)(projectsDir, match.f);
|
|
@@ -12201,7 +12859,7 @@ var StreamerServer = class {
|
|
|
12201
12859
|
cleanup();
|
|
12202
12860
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
12203
12861
|
try {
|
|
12204
|
-
const existing = (0,
|
|
12862
|
+
const existing = (0, import_fs19.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
12205
12863
|
if (existing.length > 0) {
|
|
12206
12864
|
this.broadcastConversationLines(sessionId, existing);
|
|
12207
12865
|
}
|
|
@@ -12225,7 +12883,7 @@ var StreamerServer = class {
|
|
|
12225
12883
|
if (this.sessionFileMap.has(sessionId)) return;
|
|
12226
12884
|
try {
|
|
12227
12885
|
require("fs").mkdirSync(projectsDir, { recursive: true });
|
|
12228
|
-
watcher = (0,
|
|
12886
|
+
watcher = (0, import_fs19.watch)(projectsDir, tryWire);
|
|
12229
12887
|
watcher.on("error", cleanup);
|
|
12230
12888
|
} catch {
|
|
12231
12889
|
}
|
|
@@ -12254,7 +12912,7 @@ var StreamerServer = class {
|
|
|
12254
12912
|
};
|
|
12255
12913
|
const matchesProjectPath = (candidatePath) => {
|
|
12256
12914
|
try {
|
|
12257
|
-
const firstLine = (0,
|
|
12915
|
+
const firstLine = (0, import_fs19.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
|
|
12258
12916
|
if (!firstLine) return null;
|
|
12259
12917
|
const parsed = JSON.parse(firstLine);
|
|
12260
12918
|
if (parsed?.type !== "session_meta") return null;
|
|
@@ -12283,15 +12941,15 @@ var StreamerServer = class {
|
|
|
12283
12941
|
);
|
|
12284
12942
|
for (const root of this.codexRoots) {
|
|
12285
12943
|
const sessionsDir = (0, import_path18.join)(root, dateDir);
|
|
12286
|
-
if (!(0,
|
|
12944
|
+
if (!(0, import_fs19.existsSync)(sessionsDir)) continue;
|
|
12287
12945
|
let candidateFiles;
|
|
12288
12946
|
try {
|
|
12289
|
-
candidateFiles = (0,
|
|
12947
|
+
candidateFiles = (0, import_fs19.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
12290
12948
|
} catch {
|
|
12291
12949
|
continue;
|
|
12292
12950
|
}
|
|
12293
12951
|
const nowMs = Date.now();
|
|
12294
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0,
|
|
12952
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs19.statSync)((0, import_path18.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
12295
12953
|
for (const { f } of recentCandidates) {
|
|
12296
12954
|
const candidatePath = (0, import_path18.join)(sessionsDir, f);
|
|
12297
12955
|
const match = matchesProjectPath(candidatePath);
|
|
@@ -12303,7 +12961,7 @@ var StreamerServer = class {
|
|
|
12303
12961
|
this.sessionFileMap.set(sessionId, candidatePath);
|
|
12304
12962
|
this.fileWatcher.watch(candidatePath);
|
|
12305
12963
|
try {
|
|
12306
|
-
const existing = (0,
|
|
12964
|
+
const existing = (0, import_fs19.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
12307
12965
|
if (existing.length > 0) {
|
|
12308
12966
|
this.broadcastConversationLines(sessionId, existing);
|
|
12309
12967
|
}
|
|
@@ -12372,7 +13030,7 @@ var StreamerServer = class {
|
|
|
12372
13030
|
});
|
|
12373
13031
|
return;
|
|
12374
13032
|
}
|
|
12375
|
-
const body = await
|
|
13033
|
+
const body = await readBody2(req);
|
|
12376
13034
|
const { path: relativePath, name } = body;
|
|
12377
13035
|
if (!name || typeof name !== "string") {
|
|
12378
13036
|
json(res, 400, { error: "Missing name field" });
|
|
@@ -12402,7 +13060,7 @@ var StreamerServer = class {
|
|
|
12402
13060
|
}
|
|
12403
13061
|
let parsed;
|
|
12404
13062
|
try {
|
|
12405
|
-
parsed = await
|
|
13063
|
+
parsed = await readBody2(req);
|
|
12406
13064
|
} catch {
|
|
12407
13065
|
json(res, 400, { error: "Invalid JSON" });
|
|
12408
13066
|
return;
|
|
@@ -12459,7 +13117,7 @@ var StreamerServer = class {
|
|
|
12459
13117
|
}
|
|
12460
13118
|
let parsed;
|
|
12461
13119
|
try {
|
|
12462
|
-
parsed = await
|
|
13120
|
+
parsed = await readBody2(req);
|
|
12463
13121
|
} catch {
|
|
12464
13122
|
json(res, 400, { error: "Invalid JSON" });
|
|
12465
13123
|
return;
|
|
@@ -12518,7 +13176,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
|
|
|
12518
13176
|
}
|
|
12519
13177
|
function classifyResumability(cwd) {
|
|
12520
13178
|
if (!cwd) return { resumable: true };
|
|
12521
|
-
if ((0,
|
|
13179
|
+
if ((0, import_fs19.existsSync)(cwd)) return { resumable: true };
|
|
12522
13180
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
12523
13181
|
return {
|
|
12524
13182
|
resumable: false,
|
|
@@ -12636,7 +13294,7 @@ function parseSessionListQuery(url) {
|
|
|
12636
13294
|
const cursor = url.searchParams.get("cursor") ?? void 0;
|
|
12637
13295
|
return { query: { limit, sortBy, order, status, cursor } };
|
|
12638
13296
|
}
|
|
12639
|
-
function
|
|
13297
|
+
function readBody2(req) {
|
|
12640
13298
|
return new Promise((resolve2, reject) => {
|
|
12641
13299
|
const chunks = [];
|
|
12642
13300
|
req.on("data", (chunk) => chunks.push(chunk));
|