dsh-aris-panel 0.1.8 → 0.1.9

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.
Files changed (2) hide show
  1. package/dsh/workbench.mjs +130 -3
  2. package/package.json +1 -1
package/dsh/workbench.mjs CHANGED
@@ -11,6 +11,7 @@ import { readdir, readFile, stat } from 'node:fs/promises'
11
11
  import { isAbsolute, join } from 'node:path'
12
12
  import { homedir } from 'node:os'
13
13
  import { DatabaseSync } from 'node:sqlite'
14
+ import { zstdDecompressSync } from 'node:zlib'
14
15
 
15
16
  export const WORKBENCH_RPC_CHANNEL = '/aris-wb'
16
17
 
@@ -236,6 +237,122 @@ async function readProjCache() {
236
237
  }
237
238
  }
238
239
 
240
+ // ---------------------------------------------------------------------------
241
+ // Persisted-session metadata fallback (titles / message counts / last activity
242
+ // for sessions that are not live and missing from the projection cache).
243
+ // The durable log is a concatenation of independent Zstandard frames, each
244
+ // storing one event batch (see dsh-session-persistence-jsonl).
245
+ // ---------------------------------------------------------------------------
246
+
247
+ const DSH_HOME = process.env.DSH_HOME || join(homedir(), '.dsh')
248
+ const SESSIONS_DIR = join(DSH_HOME, 'sessions')
249
+ const ZSTD_MAGIC = 4247762216
250
+ const persistedMetaCache = new Map() // sid -> { at, title, msgCount, updatedAt }
251
+
252
+ /** Split a buffer into complete Zstandard frame ranges (torn tail ignored). */
253
+ function scanZstdFrames(buffer) {
254
+ const frames = []
255
+ let offset = 0
256
+ while (offset < buffer.length) {
257
+ const start = offset
258
+ if (buffer.length - offset < 4 || buffer.readUInt32LE(offset) !== ZSTD_MAGIC) break
259
+ offset += 4
260
+ if (offset === buffer.length) break
261
+ const descriptor = buffer.readUInt8(offset)
262
+ offset += 1
263
+ if ((descriptor & 24) !== 0) break
264
+ const contentSizeFlag = descriptor >>> 6
265
+ const singleSegment = (descriptor & 32) !== 0
266
+ const checksum = (descriptor & 4) !== 0
267
+ const dictionaryFlag = descriptor & 3
268
+ const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag
269
+ const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : (1 << contentSizeFlag)
270
+ const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes
271
+ if (buffer.length - offset < remainingHeaderBytes) break
272
+ offset += remainingHeaderBytes
273
+ for (;;) {
274
+ if (buffer.length - offset < 3) return frames
275
+ const blockHeader = buffer.readUIntLE(offset, 3)
276
+ offset += 3
277
+ const lastBlock = (blockHeader & 1) !== 0
278
+ const blockType = (blockHeader >>> 1) & 3
279
+ const blockSize = blockHeader >>> 3
280
+ if (blockType === 3) return frames
281
+ const payloadBytes = blockType === 1 ? 1 : blockSize
282
+ if (buffer.length - offset < payloadBytes) return frames
283
+ offset += payloadBytes
284
+ if (lastBlock) break
285
+ }
286
+ if (checksum) {
287
+ if (buffer.length - offset < 4) return frames
288
+ offset += 4
289
+ }
290
+ frames.push({ start, end: offset })
291
+ }
292
+ return frames
293
+ }
294
+
295
+ /** Decompress the full multi-frame zstd event log to text. */
296
+ function decompressSessionLog(buffer) {
297
+ const frames = scanZstdFrames(buffer)
298
+ if (frames.length === 0) return undefined
299
+ try {
300
+ const parts = []
301
+ for (const f of frames) parts.push(zstdDecompressSync(buffer.subarray(f.start, f.end)))
302
+ return Buffer.concat(parts).toString('utf8')
303
+ } catch {
304
+ return undefined
305
+ }
306
+ }
307
+
308
+ /** Best-effort title / user-message count / last-activity from the durable log. */
309
+ async function readPersistedSessionMeta(sid, cwd) {
310
+ const cached = persistedMetaCache.get(sid)
311
+ if (cached && Date.now() - cached.at < 60000) return cached
312
+ const out = { at: Date.now(), title: undefined, msgCount: 0, updatedAt: undefined }
313
+ try {
314
+ let dir
315
+ if (cwd) {
316
+ const bucket = '--' + String(cwd).replace(/[\\/:]+/g, '-') + '--'
317
+ const cand = join(SESSIONS_DIR, bucket, sid)
318
+ try { if ((await stat(cand)).isDirectory()) dir = cand } catch { /* miss */ }
319
+ }
320
+ if (!dir) {
321
+ try {
322
+ const buckets = await readdir(SESSIONS_DIR)
323
+ for (const b of buckets) {
324
+ const cand = join(SESSIONS_DIR, b, sid)
325
+ try {
326
+ if ((await stat(cand)).isDirectory()) { dir = cand; break }
327
+ } catch { /* miss */ }
328
+ }
329
+ } catch { /* ignore */ }
330
+ }
331
+ if (!dir) return out
332
+ const buffer = await readFile(join(dir, 'session.jsonl.zstd'))
333
+ const text = decompressSessionLog(buffer)
334
+ if (typeof text === 'string') {
335
+ let msgCount = 0
336
+ let title
337
+ let updatedAt
338
+ for (const line of text.split('\n')) {
339
+ if (!line) continue
340
+ let ev
341
+ try { ev = JSON.parse(line) } catch { continue }
342
+ if (!ev || typeof ev !== 'object') continue
343
+ if (typeof ev.time === 'number' && (updatedAt === undefined || ev.time > updatedAt)) updatedAt = ev.time
344
+ if (ev.type === 'user/message') msgCount++
345
+ else if (ev.type === 'session/title' && ev.data && typeof ev.data.title === 'string' && ev.data.title.trim() !== '') title = ev.data.title
346
+ }
347
+ out.title = title
348
+ out.msgCount = msgCount
349
+ out.updatedAt = updatedAt
350
+ }
351
+ } catch { /* ignore */ }
352
+ persistedMetaCache.set(sid, out)
353
+ return out
354
+ }
355
+
239
356
 
240
357
  export function registerWorkbench(ctx) {
241
358
  ctx.inject(['connection', 'sessions', 'agents', 'workspaceRegistry', 'sessionPersistence'], (scoped) => {
@@ -287,11 +404,21 @@ export function registerWorkbench(ctx) {
287
404
  const a = reg.archivedSessionIds
288
405
  archived = Array.isArray(a) ? a : (typeof a === 'function' ? a() : [])
289
406
  } catch (e) { /* ignore */ }
290
- const list = typeof reg.list === 'function' ? reg.list().map(function (w) {
407
+ const list = typeof reg.list === 'function' ? await Promise.all(reg.list().map(async function (w) {
291
408
  const sids = (Array.isArray(w.sessionIds) ? w.sessionIds : []).filter(function (sid) { return archived.indexOf(sid) === -1 })
292
- const sessions = sids.map(sessionMetaOf)
409
+ const sessions = await Promise.all(sids.map(async function (sid) {
410
+ const out = sessionMetaOf(sid)
411
+ if (out.title === undefined || out.msgCount === 0) {
412
+ // 历史会话不在内存、也可能不在投影缓存:从持久化日志补标题/消息数/最近活动
413
+ const pm = await readPersistedSessionMeta(sid, w.path)
414
+ if (out.title === undefined && pm.title !== undefined) out.title = pm.title
415
+ if (out.msgCount === 0 && pm.msgCount > 0) out.msgCount = pm.msgCount
416
+ if (out.updatedAt === undefined && pm.updatedAt !== undefined) out.updatedAt = pm.updatedAt
417
+ }
418
+ return out
419
+ }))
293
420
  return { id: w.id, path: w.path, title: w.title, sessions: sessions }
294
- }) : []
421
+ })) : []
295
422
  return { ok: true, value: { workspaces: list } }
296
423
  } catch (error) {
297
424
  return { ok: false, error: { code: 'internal', message: String(error && error.message || error), details: { issues: [] } } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-aris-panel",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "ARIS research-workflow skills + interactive workbench panel for DeepSeek Harness (user fork of dsh-aris: 82 skills, cross-model Codex review, idea/workflow launcher, research status board).",
5
5
  "license": "MIT",
6
6
  "type": "module",