dsh-session-mng 0.1.3 → 0.2.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.
@@ -21,7 +21,7 @@
21
21
 
22
22
  import zlib from 'node:zlib'
23
23
  import { homedir } from 'node:os'
24
- import { dirname, join } from 'node:path'
24
+ import { basename, dirname, join } from 'node:path'
25
25
  import { access, cp, mkdir, open, readdir, readFile, rename, rm, stat } from 'node:fs/promises'
26
26
 
27
27
  import { SessionMngError, messageOf } from './errors.js'
@@ -160,7 +160,81 @@ export async function currentLogFile(dir) {
160
160
  }
161
161
  }
162
162
  if (best === undefined) return undefined
163
- return { path: join(dir, best.name), name: best.name, compressed: best.compressed }
163
+ return { path: join(dir, best.name), name: best.name, compressed: best.compressed, generation: bestGeneration }
164
+ }
165
+
166
+ /**
167
+ * Size and format facts of one stored session's current log, at the cost of
168
+ * exactly one readdir plus one stat.
169
+ *
170
+ * The directory comes from the backend's own zero-I/O locator, then
171
+ * {@link currentLogFile} picks the highest released generation inside it. That
172
+ * is deliberately NOT `findSessionDirs`/`stat(id)`: both fall back to scanning
173
+ * every project directory, which must never happen on a page request. The
174
+ * format status is inferred by comparing the selected generation with the one
175
+ * `locate()` names as current, so an older-generation log is reported honestly
176
+ * instead of being labelled current.
177
+ *
178
+ * @param {object} persistence - the host's JSONL persistence service.
179
+ * @param {string} id - stored session id.
180
+ * @param {string} [cwd] - the session's stored working directory.
181
+ * @returns {Promise<{sizeBytes: number, formatStatus: string, logName: string}|undefined>}
182
+ */
183
+ export async function readLogFacts(persistence, id, cwd) {
184
+ let located
185
+ try {
186
+ located = persistence?.locate?.({ id, cwd })
187
+ } catch {
188
+ return undefined
189
+ }
190
+ const path = typeof located?.path === 'string' ? located.path : ''
191
+ if (path === '') return undefined
192
+ const currentGeneration = generationOf(basename(path))
193
+ const log = await currentLogFile(dirname(path))
194
+ if (log === undefined) return undefined
195
+ let size
196
+ try {
197
+ size = (await stat(log.path)).size
198
+ } catch {
199
+ return undefined
200
+ }
201
+ const migrationRequired = currentGeneration !== undefined && log.generation < currentGeneration
202
+ return {
203
+ sizeBytes: size,
204
+ formatStatus: migrationRequired ? 'migration-required' : 'current',
205
+ logName: log.name,
206
+ }
207
+ }
208
+
209
+ /** Maximum concurrent per-log stat batches; small enough to stay polite. */
210
+ const FACT_CONCURRENCY = 8
211
+
212
+ /**
213
+ * Read {@link readLogFacts} for a bounded batch (one page at most in practice).
214
+ *
215
+ * @param {object} persistence
216
+ * @param {{id: string, cwd?: string}[]} items
217
+ * @param {number} [concurrency]
218
+ * @returns {Promise<Map<string, {sizeBytes: number, formatStatus: string, logName: string}>>}
219
+ */
220
+ export async function readLogFactsBatch(persistence, items, concurrency = FACT_CONCURRENCY) {
221
+ const found = new Map()
222
+ if (!Array.isArray(items) || items.length === 0) return found
223
+ let next = 0
224
+ const worker = async () => {
225
+ for (;;) {
226
+ const index = next
227
+ next += 1
228
+ if (index >= items.length) return
229
+ const item = items[index]
230
+ if (typeof item?.id !== 'string') continue
231
+ const facts = await readLogFacts(persistence, item.id, item.cwd).catch(() => undefined)
232
+ if (facts !== undefined) found.set(item.id, facts)
233
+ }
234
+ }
235
+ const width = Math.max(1, Math.min(Math.trunc(concurrency) || 1, items.length))
236
+ await Promise.all(Array.from({ length: width }, worker))
237
+ return found
164
238
  }
165
239
 
166
240
  /** Every offset where a Zstandard frame magic begins. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-mng",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
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",