@cjhyy/code-shell-server 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sessions-service.d.ts +11 -5
- package/dist/sessions-service.js +101 -33
- package/package.json +4 -4
|
@@ -7,6 +7,8 @@ export interface DesktopSessionSummary {
|
|
|
7
7
|
id: string;
|
|
8
8
|
/** Path to the session file on disk. */
|
|
9
9
|
file: string;
|
|
10
|
+
/** Durable title/summary; UI-side renamed titles can override it. */
|
|
11
|
+
title?: string;
|
|
10
12
|
size: number;
|
|
11
13
|
createdAt: number;
|
|
12
14
|
updatedAt: number;
|
|
@@ -35,8 +37,10 @@ export interface DiskSessionMeta {
|
|
|
35
37
|
cwd: string;
|
|
36
38
|
title: string;
|
|
37
39
|
updatedAt: number;
|
|
38
|
-
/**
|
|
39
|
-
origin: "desktop" | "automation";
|
|
40
|
+
/** Sub-agents are returned only by an explicit parentSessionId query. */
|
|
41
|
+
origin: "desktop" | "automation" | "subagent";
|
|
42
|
+
/** Present only when listing the direct children of a session. */
|
|
43
|
+
parentSessionId?: string;
|
|
40
44
|
/** Normalized durable status for Pet/display consumers. */
|
|
41
45
|
status?: "active" | "paused" | "completed" | "failed" | "cancelled";
|
|
42
46
|
/** Exceptional completed-run boundary; absent means a normal final answer. */
|
|
@@ -51,16 +55,18 @@ export interface ListDiskSessionsResult {
|
|
|
51
55
|
nextCursor: string | null;
|
|
52
56
|
}
|
|
53
57
|
/**
|
|
54
|
-
* List
|
|
58
|
+
* List sessions from disk, newest first, paginated. The default catalog is
|
|
59
|
+
* top-level only; parentSessionId selects direct children for transcript views.
|
|
55
60
|
* Filter on state.json `parentSessionId`:
|
|
56
61
|
* - key absent → legacy → skip (存量 not auto-rebuilt)
|
|
57
|
-
* - null / "" → top-level → show
|
|
58
|
-
* - non-empty → sub-agent →
|
|
62
|
+
* - null / "" → top-level → show only in the default catalog
|
|
63
|
+
* - non-empty → sub-agent → show only for the matching parent query
|
|
59
64
|
*/
|
|
60
65
|
export declare function listDiskSessions(opts: {
|
|
61
66
|
limit: number;
|
|
62
67
|
cursor?: string;
|
|
63
68
|
includeArchived?: boolean;
|
|
69
|
+
parentSessionId?: string;
|
|
64
70
|
}, baseDir?: string): Promise<ListDiskSessionsResult>;
|
|
65
71
|
/**
|
|
66
72
|
* Set (number) or clear (undefined) a session's durable archival marker on
|
package/dist/sessions-service.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as path from "node:path";
|
|
|
8
8
|
const SAFE_ID = /^[A-Za-z0-9_.-]+$/;
|
|
9
9
|
const QUICK_CHAT_SESSION_PREFIX = "qchat-";
|
|
10
10
|
const PANEL_TASK_SESSION_PREFIX = "panel-task-";
|
|
11
|
+
const MAX_HISTORY_STATE_BYTES = 2 * 1024 * 1024;
|
|
11
12
|
function isQuickChatSessionId(id) {
|
|
12
13
|
return id.startsWith(QUICK_CHAT_SESSION_PREFIX);
|
|
13
14
|
}
|
|
@@ -17,32 +18,17 @@ function isInternalEphemeralSessionId(id) {
|
|
|
17
18
|
export async function listSessions(baseDir = sessionsRoot()) {
|
|
18
19
|
try {
|
|
19
20
|
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
continue;
|
|
29
|
-
const full = path.join(baseDir, e.name);
|
|
30
|
-
try {
|
|
31
|
-
const st = await fs.stat(full);
|
|
32
|
-
summaries.push({
|
|
33
|
-
id,
|
|
34
|
-
file: full,
|
|
35
|
-
size: st.size,
|
|
36
|
-
createdAt: st.birthtimeMs || st.mtimeMs,
|
|
37
|
-
updatedAt: st.mtimeMs,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
// Skip files we cannot stat (race with delete, permissions).
|
|
21
|
+
// Prefer the current directory layout when a migration left a flat copy.
|
|
22
|
+
const candidates = entries.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()));
|
|
23
|
+
const summaries = new Map();
|
|
24
|
+
for (let offset = 0; offset < candidates.length; offset += 32) {
|
|
25
|
+
const batch = await Promise.all(candidates.slice(offset, offset + 32).map((entry) => readHistorySummary(entry, baseDir)));
|
|
26
|
+
for (const summary of batch) {
|
|
27
|
+
if (summary && !summaries.has(summary.id))
|
|
28
|
+
summaries.set(summary.id, summary);
|
|
42
29
|
}
|
|
43
30
|
}
|
|
44
|
-
summaries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
45
|
-
return summaries;
|
|
31
|
+
return [...summaries.values()].sort((a, b) => b.updatedAt - a.updatedAt || a.id.localeCompare(b.id));
|
|
46
32
|
}
|
|
47
33
|
catch (e) {
|
|
48
34
|
if (e.code === "ENOENT")
|
|
@@ -50,6 +36,66 @@ export async function listSessions(baseDir = sessionsRoot()) {
|
|
|
50
36
|
throw e;
|
|
51
37
|
}
|
|
52
38
|
}
|
|
39
|
+
async function readHistorySummary(entry, baseDir) {
|
|
40
|
+
const directory = entry.isDirectory();
|
|
41
|
+
if (!directory && (!entry.isFile() || !/\.jsonl?$/.test(entry.name)))
|
|
42
|
+
return null;
|
|
43
|
+
const id = directory ? entry.name : entry.name.replace(/\.jsonl?$/, "");
|
|
44
|
+
if (!SAFE_ID.test(id) ||
|
|
45
|
+
id.startsWith(".") ||
|
|
46
|
+
id.includes("..") ||
|
|
47
|
+
id.length > 128 ||
|
|
48
|
+
isInternalEphemeralSessionId(id))
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const file = path.join(baseDir, entry.name, ...(directory ? ["state.json"] : []));
|
|
52
|
+
const info = await fs.lstat(file);
|
|
53
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
54
|
+
return null;
|
|
55
|
+
const summary = {
|
|
56
|
+
id,
|
|
57
|
+
file,
|
|
58
|
+
size: info.size,
|
|
59
|
+
createdAt: info.birthtimeMs || info.mtimeMs,
|
|
60
|
+
updatedAt: info.mtimeMs,
|
|
61
|
+
};
|
|
62
|
+
if (!directory)
|
|
63
|
+
return summary;
|
|
64
|
+
if (info.size > MAX_HISTORY_STATE_BYTES)
|
|
65
|
+
return null;
|
|
66
|
+
const state = JSON.parse(await fs.readFile(file, "utf8"));
|
|
67
|
+
if (!state || typeof state !== "object" || Array.isArray(state))
|
|
68
|
+
return null;
|
|
69
|
+
// History includes archived/legacy sessions, but internal conversations
|
|
70
|
+
// and temporary forks must stay out of the user-facing catalog.
|
|
71
|
+
if (state.ephemeral === true || state.kind === "pet" || state.parentSessionId)
|
|
72
|
+
return null;
|
|
73
|
+
const title = typeof state.title === "string" && state.title ? state.title : state.summary;
|
|
74
|
+
if (typeof title === "string" && title)
|
|
75
|
+
summary.title = title.slice(0, 1024);
|
|
76
|
+
if (typeof state.startedAt === "number" && Number.isFinite(new Date(state.startedAt).getTime()))
|
|
77
|
+
summary.createdAt = state.startedAt;
|
|
78
|
+
const transcript = path.join(baseDir, id, "transcript.jsonl");
|
|
79
|
+
try {
|
|
80
|
+
const transcriptInfo = await fs.lstat(transcript);
|
|
81
|
+
if (!transcriptInfo.isFile() || transcriptInfo.isSymbolicLink())
|
|
82
|
+
return null;
|
|
83
|
+
summary.file = transcript;
|
|
84
|
+
summary.size += transcriptInfo.size;
|
|
85
|
+
summary.updatedAt = Math.max(info.mtimeMs, transcriptInfo.mtimeMs);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code !== "ENOENT")
|
|
89
|
+
throw error;
|
|
90
|
+
// A newly persisted session may not have a transcript yet.
|
|
91
|
+
}
|
|
92
|
+
return summary;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// One corrupt/unreadable session or a concurrent delete cannot hide the rest.
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
53
99
|
/**
|
|
54
100
|
* Remove a session's on-disk data. Modern sessions are a directory
|
|
55
101
|
* `<sessionsDir>/<id>/` (transcript.jsonl + state.json — see core
|
|
@@ -147,13 +193,23 @@ function decodeDiskSessionCursor(value) {
|
|
|
147
193
|
return Number.isFinite(mtime) ? { mtime, id: match[2] } : null;
|
|
148
194
|
}
|
|
149
195
|
/**
|
|
150
|
-
* List
|
|
196
|
+
* List sessions from disk, newest first, paginated. The default catalog is
|
|
197
|
+
* top-level only; parentSessionId selects direct children for transcript views.
|
|
151
198
|
* Filter on state.json `parentSessionId`:
|
|
152
199
|
* - key absent → legacy → skip (存量 not auto-rebuilt)
|
|
153
|
-
* - null / "" → top-level → show
|
|
154
|
-
* - non-empty → sub-agent →
|
|
200
|
+
* - null / "" → top-level → show only in the default catalog
|
|
201
|
+
* - non-empty → sub-agent → show only for the matching parent query
|
|
155
202
|
*/
|
|
156
203
|
export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
|
|
204
|
+
const parentSessionId = opts.parentSessionId;
|
|
205
|
+
if (parentSessionId !== undefined &&
|
|
206
|
+
(typeof parentSessionId !== "string" ||
|
|
207
|
+
!SAFE_ID.test(parentSessionId) ||
|
|
208
|
+
parentSessionId.length > 128 ||
|
|
209
|
+
parentSessionId === "." ||
|
|
210
|
+
parentSessionId.includes(".."))) {
|
|
211
|
+
throw new Error("invalid parent session id");
|
|
212
|
+
}
|
|
157
213
|
// Async fs throughout: this is an IPC handler on the Electron main thread
|
|
158
214
|
// (sessions:listDisk). The previous synchronous statSync/readFileSync loops
|
|
159
215
|
// never yielded the event loop and froze the UI while enumerating sessions.
|
|
@@ -219,13 +275,22 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
|
|
|
219
275
|
continue;
|
|
220
276
|
if (!("parentSessionId" in state))
|
|
221
277
|
continue; // legacy → skip
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
278
|
+
if (parentSessionId !== undefined) {
|
|
279
|
+
if (state.parentSessionId !== parentSessionId)
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
else if (state.parentSessionId) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// Only desktop + automation belong in the desktop sidebar. Targeted
|
|
286
|
+
// queries additionally expose subagent origin. tui / missing origins stay
|
|
287
|
+
// filtered out (tui shares ~/.code-shell; legacy has no origin).
|
|
226
288
|
const origin = state.origin;
|
|
227
|
-
if (origin !== "desktop" &&
|
|
289
|
+
if (origin !== "desktop" &&
|
|
290
|
+
origin !== "automation" &&
|
|
291
|
+
!(parentSessionId !== undefined && origin === "subagent")) {
|
|
228
292
|
continue;
|
|
293
|
+
}
|
|
229
294
|
// Skip sessions whose project root has been deleted. Listing them led the
|
|
230
295
|
// renderer's disk-rebuild to call createRepoForCwd → the deleted project
|
|
231
296
|
// reappeared in the sidebar, and any subsequent write under that cwd
|
|
@@ -233,7 +298,9 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
|
|
|
233
298
|
// is intentionally NOT filtered here — the renderer handles that via
|
|
234
299
|
// isNoRepoCwd. (deleted-project resurrection)
|
|
235
300
|
const cwdStr = typeof state.cwd === "string" ? state.cwd : "";
|
|
236
|
-
|
|
301
|
+
// A child transcript remains useful after its temporary worktree is gone;
|
|
302
|
+
// targeted reads do not rebuild projects in the sidebar.
|
|
303
|
+
if (parentSessionId === undefined && cwdStr && !(await pathExists(cwdStr)))
|
|
237
304
|
continue;
|
|
238
305
|
// Archived sessions are hidden from the default catalog. Filtering happens
|
|
239
306
|
// before the push (like every other skip above), so the mtime cursor —
|
|
@@ -254,6 +321,7 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
|
|
|
254
321
|
(typeof state.summary === "string" && state.summary ? state.summary : id),
|
|
255
322
|
updatedAt: mtime,
|
|
256
323
|
origin,
|
|
324
|
+
...(parentSessionId !== undefined ? { parentSessionId } : {}),
|
|
257
325
|
status: state.status === "active" || state.status === "paused" || state.status === "completed"
|
|
258
326
|
? state.status
|
|
259
327
|
: state.status === "aborted_streaming" || state.status === "aborted_tools"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-server",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@cjhyy/code-shell-capability-coding": "0.9.
|
|
43
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
44
|
-
"@cjhyy/code-shell-web": "0.9.
|
|
42
|
+
"@cjhyy/code-shell-capability-coding": "0.9.8",
|
|
43
|
+
"@cjhyy/code-shell-core": "0.9.8",
|
|
44
|
+
"@cjhyy/code-shell-web": "0.9.8",
|
|
45
45
|
"@types/ws": "^8.18.1",
|
|
46
46
|
"ws": "^8.21.0"
|
|
47
47
|
},
|