dsh-session-mng 0.1.1 → 0.1.3
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/CHANGELOG.md +37 -0
- package/README.md +25 -6
- package/ROADMAP.md +30 -4
- package/client.js +272 -66
- package/docs/01-design.md +23 -13
- package/docs/02-open-source-and-marketplace.md +21 -0
- package/docs/HANDOVER-2026-10-27.md +133 -0
- package/docs/PLAN-2026-10-27.md +2 -0
- package/docs/PLAN-2026-10-28-branch-visibility.md +116 -0
- package/docs/PLAN-2026-10-28-legacy-fork-safety.md +62 -0
- package/docs/RELEASE-CHECKLIST-0.1.1.md +18 -5
- package/docs/RELEASE-CHECKLIST-0.1.2.md +31 -0
- package/docs/RELEASE-CHECKLIST-0.1.3.md +38 -0
- package/lib/service.js +64 -16
- package/lib/state.js +54 -9
- package/package.json +4 -3
package/lib/service.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { SessionMngError, messageOf } from './errors.js'
|
|
14
14
|
import { normalizeTitle, proposeTitle, textOfEvent } from './title.js'
|
|
15
|
-
import { buildState } from './state.js'
|
|
15
|
+
import { buildState, classifySession } from './state.js'
|
|
16
16
|
import {
|
|
17
17
|
defaultSessionsRoot,
|
|
18
18
|
dirSize,
|
|
@@ -134,12 +134,30 @@ function normalizeIds(value, max, label = 'sessionIds') {
|
|
|
134
134
|
return ids
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Walk a lineage tree, splitting descendants into the two kinds the plugin
|
|
139
|
+
* treats differently.
|
|
140
|
+
*
|
|
141
|
+
* - `subagents` — delegation sessions; they belong to the root and are
|
|
142
|
+
* cascaded with it.
|
|
143
|
+
* - `forks` — ordinary branches (including historical logs that still record
|
|
144
|
+
* a parent). They are independent user sessions, so the walk records one and
|
|
145
|
+
* stops: nothing below it may be cascaded with the root.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} node - one `SessionLineageNode` returned by `traceSession`.
|
|
148
|
+
* @param {{ subagents: object[], forks: object[] }} out - accumulator.
|
|
149
|
+
*/
|
|
150
|
+
function partitionDescendants(node, out) {
|
|
139
151
|
for (const child of node?.descendants ?? []) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
const record = child?.session
|
|
153
|
+
const id = record?.header?.id
|
|
154
|
+
if (typeof id !== 'string') continue
|
|
155
|
+
if (classifySession(record.header) === 'subagent') {
|
|
156
|
+
out.subagents.push(record)
|
|
157
|
+
partitionDescendants(child, out)
|
|
158
|
+
} else {
|
|
159
|
+
out.forks.push(record)
|
|
160
|
+
}
|
|
143
161
|
}
|
|
144
162
|
return out
|
|
145
163
|
}
|
|
@@ -200,7 +218,7 @@ export function createService(ctx, config) {
|
|
|
200
218
|
}
|
|
201
219
|
try {
|
|
202
220
|
const lineage = await trace.call(query(), id)
|
|
203
|
-
return
|
|
221
|
+
return partitionDescendants(lineage, { subagents: [], forks: [] })
|
|
204
222
|
} catch (error) {
|
|
205
223
|
throw new SessionMngError('trace-failed', `could not trace descendants of "${id}": ${messageOf(error)}`, 500)
|
|
206
224
|
}
|
|
@@ -441,9 +459,16 @@ export function createService(ctx, config) {
|
|
|
441
459
|
throw new SessionMngError('unsupported-backend', 'permanent deletion requires the local JSONL session backend', 501)
|
|
442
460
|
}
|
|
443
461
|
const archived = new Set(archivedIds())
|
|
462
|
+
const selected = new Set(ids)
|
|
444
463
|
const roots = []
|
|
445
464
|
const errors = []
|
|
446
465
|
const sessions = []
|
|
466
|
+
// Ordinary forks are independent sessions: they keep their own log when a
|
|
467
|
+
// parent is deleted and only lose the "from <parent>" label. Collect them
|
|
468
|
+
// separately so the confirmation can say so instead of silently cascading
|
|
469
|
+
// a user branch. A fork selected in this same batch is deleted on its own.
|
|
470
|
+
const forkChildren = []
|
|
471
|
+
const forkSeen = new Set()
|
|
447
472
|
for (const rootId of ids) {
|
|
448
473
|
try {
|
|
449
474
|
if (!archived.has(rootId)) {
|
|
@@ -454,8 +479,18 @@ export function createService(ctx, config) {
|
|
|
454
479
|
const snapshot = await store.stat(rootId)
|
|
455
480
|
const header = snapshot?.header
|
|
456
481
|
if (header === undefined) throw new SessionMngError('not-found', `session "${rootId}" is not stored`, 404)
|
|
457
|
-
const
|
|
458
|
-
const all = [rootId, ...
|
|
482
|
+
const lineage = await descendantsOf(rootId)
|
|
483
|
+
const all = [rootId, ...lineage.subagents.map((record) => record.header.id).filter((id) => id !== rootId)]
|
|
484
|
+
for (const record of lineage.forks) {
|
|
485
|
+
const forkId = record.header.id
|
|
486
|
+
if (selected.has(forkId) || forkSeen.has(forkId)) continue
|
|
487
|
+
forkSeen.add(forkId)
|
|
488
|
+
forkChildren.push({
|
|
489
|
+
id: forkId,
|
|
490
|
+
parentId: record.header.parentSession ?? rootId,
|
|
491
|
+
title: await readTitle(forkId),
|
|
492
|
+
})
|
|
493
|
+
}
|
|
459
494
|
const rootRow = {
|
|
460
495
|
id: rootId,
|
|
461
496
|
title: await readTitle(rootId),
|
|
@@ -508,6 +543,7 @@ export function createService(ctx, config) {
|
|
|
508
543
|
errors,
|
|
509
544
|
sessions,
|
|
510
545
|
descendantCount: roots.reduce((sum, root) => sum + root.descendantCount, 0),
|
|
546
|
+
forkChildren,
|
|
511
547
|
totalBytes: sessions.reduce((sum, row) => sum + row.bytes, 0),
|
|
512
548
|
}
|
|
513
549
|
}
|
|
@@ -535,14 +571,17 @@ export function createService(ctx, config) {
|
|
|
535
571
|
})
|
|
536
572
|
continue
|
|
537
573
|
}
|
|
538
|
-
let
|
|
574
|
+
let lineage
|
|
539
575
|
try {
|
|
540
|
-
|
|
576
|
+
lineage = await descendantsOf(rootId)
|
|
541
577
|
} catch (error) {
|
|
542
578
|
failures.set(rootId, { sessionId: rootId, ok: false, error: resultError(error) })
|
|
543
579
|
continue
|
|
544
580
|
}
|
|
545
|
-
|
|
581
|
+
// Only delegation sessions are cascaded. An ordinary fork (including a
|
|
582
|
+
// historical log that still records a parent) is an independent user
|
|
583
|
+
// session and stays where it is.
|
|
584
|
+
for (const id of [rootId, ...lineage.subagents.map((record) => record.header.id)]) {
|
|
546
585
|
if (seen.has(id)) continue
|
|
547
586
|
seen.add(id)
|
|
548
587
|
const snapshot = await store.stat(id).catch(() => undefined)
|
|
@@ -732,13 +771,22 @@ export function createService(ctx, config) {
|
|
|
732
771
|
if (serviceOf(ctx, 'agents')?.get?.(id)?.status === 'running') {
|
|
733
772
|
throw new SessionMngError('session-running', 'this session is running; stop it before moving', 409)
|
|
734
773
|
}
|
|
735
|
-
if (header.origin === 'subagent' || header.
|
|
774
|
+
if (header.origin === 'subagent' || (typeof header.delegationDepth === 'number' && header.delegationDepth >= 1)) {
|
|
736
775
|
throw new SessionMngError('subagent-session', 'subagent sessions are not moved on their own', 409)
|
|
737
776
|
}
|
|
738
|
-
|
|
739
|
-
|
|
777
|
+
if (header.parentSession !== undefined && header.parentSession !== null) {
|
|
778
|
+
throw new SessionMngError(
|
|
779
|
+
'branch-session',
|
|
780
|
+
"a branch session must stay in its parent's workspace, so it is not moved on its own",
|
|
781
|
+
409,
|
|
782
|
+
)
|
|
783
|
+
}
|
|
784
|
+
// Only subagent descendants block a move. An ordinary fork (historical or
|
|
785
|
+
// not) is an independent session, so it no longer blocks its source.
|
|
786
|
+
const lineage = await descendantsOf(id)
|
|
787
|
+
if (lineage.subagents.length > 0) {
|
|
740
788
|
throw new SessionMngError('has-descendants', 'this session has subagent descendants; they cannot follow it yet', 409, {
|
|
741
|
-
descendants:
|
|
789
|
+
descendants: lineage.subagents.length,
|
|
742
790
|
})
|
|
743
791
|
}
|
|
744
792
|
const source = workspaceOfId(id)
|
package/lib/state.js
CHANGED
|
@@ -140,10 +140,31 @@ function headerOf(row, snapshotsById, ctx) {
|
|
|
140
140
|
return snapshotsById.get(row.sessionId)?.header
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Classify one session from whatever identifying fields are available.
|
|
145
|
+
*
|
|
146
|
+
* Callers pass a stored header, a list summary, or a merged view, so both
|
|
147
|
+
* field spellings are accepted (`parentSession` on a header, `parentSessionId`
|
|
148
|
+
* on a summary). Keeping this in one place is what makes the listing, the
|
|
149
|
+
* delete scope and the move guard agree on what an ordinary fork is: only
|
|
150
|
+
* delegation (`origin: 'subagent'` or a delegation depth) marks an internal
|
|
151
|
+
* session. A parent alone is not enough — an ordinary fork carries one too,
|
|
152
|
+
* and historical forks still record it.
|
|
153
|
+
*/
|
|
154
|
+
export function classifySession(fields) {
|
|
155
|
+
if (fields?.origin === 'subagent') return 'subagent'
|
|
156
|
+
if (typeof fields?.delegationDepth === 'number' && fields.delegationDepth >= 1) return 'subagent'
|
|
157
|
+
const parent = fields?.parentSession ?? fields?.parentSessionId
|
|
158
|
+
return parent === undefined || parent === null || parent === '' ? 'root' : 'fork'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Classify one row of the listing from its header and summary. */
|
|
162
|
+
function classifyKind(header, summary) {
|
|
163
|
+
return classifySession({
|
|
164
|
+
origin: header?.origin ?? summary?.origin,
|
|
165
|
+
delegationDepth: header?.delegationDepth ?? summary?.delegationDepth,
|
|
166
|
+
parentSession: header?.parentSession ?? summary?.parentSessionId,
|
|
167
|
+
})
|
|
147
168
|
}
|
|
148
169
|
|
|
149
170
|
/**
|
|
@@ -201,6 +222,7 @@ export async function buildState(ctx, config, options = {}) {
|
|
|
201
222
|
for (const id of snapshotById.keys()) knownIds.add(id)
|
|
202
223
|
const rows = []
|
|
203
224
|
const missingTitles = []
|
|
225
|
+
let hiddenSubagents = 0
|
|
204
226
|
|
|
205
227
|
const pushRow = (base) => {
|
|
206
228
|
const id = base.sessionId
|
|
@@ -208,8 +230,11 @@ export async function buildState(ctx, config, options = {}) {
|
|
|
208
230
|
seen.add(id)
|
|
209
231
|
const header = headerOf(base, snapshotById, ctx)
|
|
210
232
|
const summary = base
|
|
211
|
-
const
|
|
212
|
-
if (subagent && !includeSubagents)
|
|
233
|
+
const kind = classifyKind(header, summary)
|
|
234
|
+
if (kind === 'subagent' && !includeSubagents) {
|
|
235
|
+
hiddenSubagents += 1
|
|
236
|
+
return
|
|
237
|
+
}
|
|
213
238
|
|
|
214
239
|
const cwd = summary.cwd ?? header?.cwd ?? ''
|
|
215
240
|
const workspace =
|
|
@@ -234,13 +259,15 @@ export async function buildState(ctx, config, options = {}) {
|
|
|
234
259
|
running,
|
|
235
260
|
blank: summary.blank === true,
|
|
236
261
|
origin: header?.origin ?? summary.origin,
|
|
237
|
-
|
|
262
|
+
kind,
|
|
263
|
+
isSubagent: kind === 'subagent',
|
|
264
|
+
isFork: kind === 'fork',
|
|
265
|
+
parentSessionId: header?.parentSession ?? summary.parentSessionId ?? null,
|
|
238
266
|
agentPreset: header?.agentPreset ?? '',
|
|
239
267
|
createdAt: header?.createdAt ?? summary.updatedAt ?? 0,
|
|
240
268
|
updatedAt: summary.updatedAt ?? header?.createdAt ?? 0,
|
|
241
269
|
sizeBytes: typeof snapshot?.sizeBytes === 'number' ? snapshot.sizeBytes : 0,
|
|
242
270
|
formatStatus: summary.formatStatus ?? snapshot?.formatStatus,
|
|
243
|
-
isSubagent: subagent,
|
|
244
271
|
})
|
|
245
272
|
}
|
|
246
273
|
|
|
@@ -268,6 +295,22 @@ export async function buildState(ctx, config, options = {}) {
|
|
|
268
295
|
if (row.title === '') row.title = titles.get(row.id) ?? ''
|
|
269
296
|
}
|
|
270
297
|
|
|
298
|
+
// A fork points at its parent by id only; resolving the title here lets the
|
|
299
|
+
// list show the relationship without a second round trip. `parentPresent`
|
|
300
|
+
// lets the client tell "untitled parent" apart from "the parent session is
|
|
301
|
+
// gone" (possible after the parent is permanently deleted).
|
|
302
|
+
const titlesById = new Map(rows.map((row) => [row.id, row.title]))
|
|
303
|
+
const forkCountByParent = new Map()
|
|
304
|
+
for (const row of rows) {
|
|
305
|
+
row.parentTitle = row.kind === 'fork' ? (titlesById.get(row.parentSessionId) ?? '') : ''
|
|
306
|
+
row.parentPresent = row.kind === 'fork' && titlesById.has(row.parentSessionId)
|
|
307
|
+
if (row.kind !== 'fork' || row.parentSessionId === null) continue
|
|
308
|
+
forkCountByParent.set(row.parentSessionId, (forkCountByParent.get(row.parentSessionId) ?? 0) + 1)
|
|
309
|
+
}
|
|
310
|
+
for (const row of rows) {
|
|
311
|
+
row.forkCount = forkCountByParent.get(row.id) ?? 0
|
|
312
|
+
}
|
|
313
|
+
|
|
271
314
|
const jsonl =
|
|
272
315
|
typeof ctx.sessionPersistence?.locate === 'function' && typeof ctx.sessionPersistence?.stat === 'function'
|
|
273
316
|
const capabilities = {
|
|
@@ -282,7 +325,7 @@ export async function buildState(ctx, config, options = {}) {
|
|
|
282
325
|
title: capabilities.title,
|
|
283
326
|
archive: !row.archived,
|
|
284
327
|
restore: row.archived,
|
|
285
|
-
move: capabilities.move && !row.live && !row.running &&
|
|
328
|
+
move: capabilities.move && !row.live && !row.running && row.kind === 'root',
|
|
286
329
|
delete: capabilities.delete && row.archived && !row.live,
|
|
287
330
|
open: true,
|
|
288
331
|
}
|
|
@@ -323,6 +366,8 @@ export async function buildState(ctx, config, options = {}) {
|
|
|
323
366
|
active: rows.filter((row) => !row.archived).length,
|
|
324
367
|
archived: rows.filter((row) => row.archived).length,
|
|
325
368
|
subagents: rows.filter((row) => row.isSubagent).length,
|
|
369
|
+
hiddenSubagents,
|
|
370
|
+
forks: rows.filter((row) => row.isFork).length,
|
|
326
371
|
bytes: totalBytes,
|
|
327
372
|
},
|
|
328
373
|
limits: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-mng",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Session management for DeepSeek Harness: one Settings page for every session — whole-session title generation, archive/restore, cross-workspace move, and permanent deletion of archived sessions.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/ZilongYang/dsh-session-mng.git"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://
|
|
13
|
+
"homepage": "https://dsh.zlmix.com/session-mng/",
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/ZilongYang/dsh-session-mng/issues"
|
|
16
16
|
},
|
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
"compatibility": {
|
|
72
72
|
"dsh": ">=0.2.0-rc.1 <0.3.0-0",
|
|
73
73
|
"dshReleases": {
|
|
74
|
-
"0.2.1-rc.2": "compatible"
|
|
74
|
+
"0.2.1-rc.2": "compatible",
|
|
75
|
+
"0.2.1-rc.3": "compatible"
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
}
|