dsh-sessions-manager 3.2.1
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/LICENSE +21 -0
- package/README.en.md +152 -0
- package/README.md +152 -0
- package/assets/screenshot-session-details.png +0 -0
- package/assets/screenshot-session-settingsmenu.png +0 -0
- package/assets/screenshot-session-submenu.png +0 -0
- package/assets/screenshot-session-trash.png +0 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +1582 -0
- package/lib/client.js.map +7 -0
- package/lib/index.js +1159 -0
- package/lib/index.js.map +7 -0
- package/package.json +73 -0
- package/src/client/index.jsx +1466 -0
- package/src/index.js +1158 -0
- package/src/zstd-frame.js +137 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// dsh-sessions-manager — zstd frame helpers.
|
|
2
|
+
//
|
|
3
|
+
// DSH persists session logs as a sequence of concatenated zstd frames. The
|
|
4
|
+
// FIRST frame must be exactly one line: the session header JSON (type
|
|
5
|
+
// 'session'). The persistence layer enforces this on startup
|
|
6
|
+
// (assertZstdHeaderFrame), so any corruption of frame0 takes down the whole
|
|
7
|
+
// web profile.
|
|
8
|
+
//
|
|
9
|
+
// Moving a session between workspaces requires rewriting frame0's `cwd`
|
|
10
|
+
// without re-encoding the rest of the log. That rewrite is where a bad frame
|
|
11
|
+
// boundary can silently destroy a session — hence the defensive checks here.
|
|
12
|
+
|
|
13
|
+
import zlib from 'node:zlib'
|
|
14
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
15
|
+
|
|
16
|
+
// zstd magic bytes are 28 B5 2F FD; read as a little-endian uint32 that is
|
|
17
|
+
// 0xFD2FB528 (4247762216).
|
|
18
|
+
export const ZSTD_MAGIC = 0xFD2FB528
|
|
19
|
+
|
|
20
|
+
const CHECKSUM_OPTS = { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Locate real zstd frame boundaries in a concatenated-frame buffer.
|
|
24
|
+
*
|
|
25
|
+
* Scanning for the 4-byte magic alone produces FALSE POSITIVES: the same byte
|
|
26
|
+
* sequence can occur inside compressed data. Every candidate is therefore
|
|
27
|
+
* validated by attempting decompression; only offsets that decode are kept.
|
|
28
|
+
*
|
|
29
|
+
* @param {Buffer} buf
|
|
30
|
+
* @returns {number[]} ascending offsets of real frame starts
|
|
31
|
+
*/
|
|
32
|
+
export function findZstdFrameStarts(buf) {
|
|
33
|
+
const starts = []
|
|
34
|
+
for (let i = 0; i + 4 <= buf.length; i++) {
|
|
35
|
+
if (buf.readUInt32LE(i) !== ZSTD_MAGIC) continue
|
|
36
|
+
try {
|
|
37
|
+
// Two checks are needed, not just one:
|
|
38
|
+
// - a magic inside compressed data fails to decode and throws
|
|
39
|
+
// - a BARE 4-byte magic at the very end of the buffer decodes to an
|
|
40
|
+
// EMPTY result without throwing, so non-empty output is required too
|
|
41
|
+
// Every real frame carries at least one JSON line, so neither case can
|
|
42
|
+
// be a genuine frame start.
|
|
43
|
+
const out = zlib.zstdDecompressSync(buf.subarray(i, i + Math.min(buf.length - i, 1000000)))
|
|
44
|
+
if (out.length > 0) starts.push(i)
|
|
45
|
+
} catch (_) {
|
|
46
|
+
// Not a real frame boundary — the magic bytes occurred inside compressed data.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return starts
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Rewrite the `cwd` field of a session log's first frame, leaving all
|
|
54
|
+
* subsequent frames byte-identical.
|
|
55
|
+
*
|
|
56
|
+
* Refuses to write anything unless frame0 is a session header. A corrupted
|
|
57
|
+
* frame0 (e.g. an `agent/inbox/spliced` event) is reported as an error rather
|
|
58
|
+
* than being re-serialized back to disk — rewriting it would bake the
|
|
59
|
+
* corruption in permanently and make the file unrecoverable.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} filePath path to session.jsonl.zstd
|
|
62
|
+
* @param {string} newCwd workspace path to write into frame0
|
|
63
|
+
* @throws {Error} when the log has no zstd frame or frame0 is not a session header
|
|
64
|
+
*/
|
|
65
|
+
export function rewriteFrame0Cwd(filePath, newCwd) {
|
|
66
|
+
const buf = readFileSync(filePath)
|
|
67
|
+
const starts = findZstdFrameStarts(buf)
|
|
68
|
+
if (starts.length === 0) throw new Error('会话日志格式异常(无 zstd 帧)')
|
|
69
|
+
const end0 = starts.length > 1 ? starts[1] : buf.length
|
|
70
|
+
const frame0 = buf.subarray(starts[0], end0)
|
|
71
|
+
const text = zlib.zstdDecompressSync(frame0).toString('utf8')
|
|
72
|
+
const nl = text.indexOf('\n')
|
|
73
|
+
const line = nl >= 0 ? text.slice(0, nl) : text
|
|
74
|
+
const obj = JSON.parse(line)
|
|
75
|
+
if (obj.type !== 'session') {
|
|
76
|
+
throw new Error(`会话日志格式异常(帧0 不是 session header,实际 type=${obj.type})`)
|
|
77
|
+
}
|
|
78
|
+
if (obj.cwd === newCwd) return // already correct, no rewrite needed
|
|
79
|
+
obj.cwd = newCwd
|
|
80
|
+
const newFrame0 = zlib.zstdCompressSync(JSON.stringify(obj) + '\n', CHECKSUM_OPTS)
|
|
81
|
+
const rest = buf.subarray(end0)
|
|
82
|
+
writeFileSync(filePath, Buffer.concat([newFrame0, rest]))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Non-destructive variant of rewriteFrame0Cwd: returns the rewritten buffer
|
|
87
|
+
* instead of touching the file on disk. Used by tests.
|
|
88
|
+
*
|
|
89
|
+
* @param {Buffer} buf
|
|
90
|
+
* @param {string} newCwd
|
|
91
|
+
* @returns {Buffer} rewritten log
|
|
92
|
+
*/
|
|
93
|
+
export function rewriteFrame0CwdInMemory(buf, newCwd) {
|
|
94
|
+
const starts = findZstdFrameStarts(buf)
|
|
95
|
+
if (starts.length === 0) throw new Error('会话日志格式异常(无 zstd 帧)')
|
|
96
|
+
const end0 = starts.length > 1 ? starts[1] : buf.length
|
|
97
|
+
const frame0 = buf.subarray(starts[0], end0)
|
|
98
|
+
const text = zlib.zstdDecompressSync(frame0).toString('utf8')
|
|
99
|
+
const nl = text.indexOf('\n')
|
|
100
|
+
const line = nl >= 0 ? text.slice(0, nl) : text
|
|
101
|
+
const obj = JSON.parse(line)
|
|
102
|
+
if (obj.type !== 'session') {
|
|
103
|
+
throw new Error(`会话日志格式异常(帧0 不是 session header,实际 type=${obj.type})`)
|
|
104
|
+
}
|
|
105
|
+
obj.cwd = newCwd
|
|
106
|
+
const newFrame0 = zlib.zstdCompressSync(JSON.stringify(obj) + '\n', CHECKSUM_OPTS)
|
|
107
|
+
const rest = buf.subarray(end0)
|
|
108
|
+
return Buffer.concat([newFrame0, rest])
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build a multi-frame session log buffer (header frame + event frames),
|
|
113
|
+
* matching the layout DSH's persistence layer writes. Used by tests.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} header session header (must have type: 'session')
|
|
116
|
+
* @param {object[]} events subsequent records, one zstd frame each
|
|
117
|
+
* @returns {Buffer}
|
|
118
|
+
*/
|
|
119
|
+
export function buildSessionLog(header, events = []) {
|
|
120
|
+
const frames = [JSON.stringify(header) + '\n', ...events.map((e) => JSON.stringify(e) + '\n')]
|
|
121
|
+
return Buffer.concat(frames.map((f) => zlib.zstdCompressSync(Buffer.from(f, 'utf8'), CHECKSUM_OPTS)))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Read frame0 of a session log and return the parsed header line.
|
|
126
|
+
*
|
|
127
|
+
* @param {Buffer} buf
|
|
128
|
+
* @returns {{obj: object, lineCount: number}}
|
|
129
|
+
*/
|
|
130
|
+
export function readFrame0(buf) {
|
|
131
|
+
const starts = findZstdFrameStarts(buf)
|
|
132
|
+
if (starts.length === 0) throw new Error('会话日志格式异常(无 zstd 帧)')
|
|
133
|
+
const end0 = starts.length > 1 ? starts[1] : buf.length
|
|
134
|
+
const text = zlib.zstdDecompressSync(buf.subarray(starts[0], end0)).toString('utf8')
|
|
135
|
+
const lines = text.split('\n').filter((l) => l.length > 0)
|
|
136
|
+
return { obj: JSON.parse(lines[0]), lineCount: lines.length }
|
|
137
|
+
}
|