@mzzsfy/dsh-session-manager 0.9.3 → 0.9.4

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/README.md CHANGED
@@ -89,4 +89,4 @@ node scripts/dev-link.mjs dsh-session-manager # 仓库根执行:归一 profile
89
89
 
90
90
  ## dsh 版本兼容
91
91
 
92
- 0.1.2-rc.1 / 0.1.5-rc.3 通过:归档入口、激活 live、diagnostics 0 findings。0.1.7-rc.1 上宿主服务拓扑变化(workspaceRegistry 激活链)导致本包七项 inject 未满足、HTTP 面 404,功能待宿主 rc 线定型后专项对齐(详见包内 兼容性测试.md 与仓库兼容性进度文档 FIND-RC1-1)。
92
+ 按仓库动态窗口验证(scripts/compat/window.mjs),不预置任何固定版本槽:主测 = 渠道 dist-tag 最新 rc,完全适配口径;基线 = npm latest,不崩溃底线口径;主测与基线同版时收敛单槽。兼容面以窗口实测为准,不逐版本承诺。逐轮实测结论见 `docs/兼容性测试/逐包测试进度.md` 与本包 `兼容性测试.md`。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mzzsfy/dsh-session-manager",
3
3
  "description": "会话自动归档与管理:超期自动归档、归档面板、删除进系统回收站、还原后一键重挂载",
4
- "version": "0.9.3",
4
+ "version": "0.9.4",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
@@ -18,6 +18,13 @@
18
18
  "external": [
19
19
  "@mzzsfy/dsh-toast/client"
20
20
  ]
21
+ },
22
+ "compatibility": {
23
+ "dshReleases": {
24
+ "0.2.0-rc.1": "unknown",
25
+ "0.2.0-rc.2": "compatible",
26
+ "0.2.1-alpha.1": "unknown"
27
+ }
21
28
  }
22
29
  },
23
30
  "scripts": {
package/src/client.js CHANGED
@@ -665,7 +665,7 @@ function SessionManagerApp(props) {
665
665
 
666
666
  // 面板设置项悬停说明:原生 title(设置侧栏为滚动容器,CSS 气泡会被 overflow
667
667
  // 裁剪,JS 定位复杂度不成比例);文案与功能行为同源维护,由源码契约测试锁定
668
- const ARCHIVE_OVERVIEW_TITLE = '超过阈值天数未活跃的会话自动移入归档;触发时机为新会话创建、插件启动与周期检查;归档会话在本面板管理,可恢复或删除。'
668
+ const ARCHIVE_OVERVIEW_TITLE = '超过阈值天数未活跃的会话自动移入归档;触发时机为新会话创建、插件启动与周期检查;归档会话在本面板管理,可恢复或删除;恢复后 10 分钟内暂停自动归档,期间发言即恢复正常活跃计时。'
669
669
  const ARCHIVE_DAYS_TITLE = '会话超过该天数未活跃(产物无更新)即自动归档,空白(无消息)会话不参与归档;0 = 关闭自动归档。仅对产物可读的会话评估,产物不可读的跳过以免误归档,已归档的会话不会重复处理。'
670
670
  const ARCHIVE_INTERVAL_TITLE = '周期检查的间隔小时数,到期会话在下一轮检查时归档;0 = 关闭周期检查(新会话创建与插件启动时的检查不受影响);调小最迟等新间隔即可提前触发,调大自下一轮排期起生效。'
671
671
 
package/src/core.mjs CHANGED
@@ -24,10 +24,19 @@ export function updatedAtOf(header, activityAtMs) {
24
24
  return Math.max(header.createdAt, activityAtMs ?? 0)
25
25
  }
26
26
 
27
+ // 取消归档全局宽限时长:恢复会话到用户发出首条消息之间的评估窗口保护,
28
+ // 首条消息更新产物 mtime 后由阈值自然接管;只查阅不发言的会话宽限后正常归档
29
+ export const UNARCHIVE_GRACE_MS = 10 * 60 * 1000
30
+
31
+ /** 宽限期判定:取消归档后 graceMs 内暂停自动归档(用户恢复即意图信号)。 */
32
+ export function unarchiveGraceActive(unarchivedAtMs, nowMs, graceMs = UNARCHIVE_GRACE_MS) {
33
+ if (!Number.isFinite(unarchivedAtMs) || unarchivedAtMs <= 0) return false
34
+ return nowMs - unarchivedAtMs < graceMs
35
+ }
36
+
27
37
  /**
28
38
  * 归档评估状态机:按阈值筛出待归档会话 id。
29
39
  * @param records - 候选行 {id, archived, running, blank, updatedAt}
30
- * @returns 超期且未归档、非运行中、非空白的会话 id
31
40
  */
32
41
  export function selectArchiveCandidates({ records, nowMs, thresholdDays }) {
33
42
  if (!Number.isFinite(thresholdDays) || thresholdDays <= 0) return []
package/src/index.js CHANGED
@@ -26,6 +26,8 @@ import {
26
26
  mergeDeletedEntry,
27
27
  removeDeletedEntry,
28
28
  selectArchiveCandidates,
29
+ UNARCHIVE_GRACE_MS,
30
+ unarchiveGraceActive,
29
31
  updatedAtOf,
30
32
  } from './core.mjs'
31
33
  import { moveToQuarantine, restoreFromQuarantine, trashPath } from './trash.mjs'
@@ -217,6 +219,9 @@ function rejectMethod(req, res, method) {
217
219
  /** @param {import('@deepseek-ai/cordis').Context} ctx */
218
220
  export function apply(ctx, config) {
219
221
  let evaluating = false
222
+ // 取消归档全局宽限起点(进程内,重启失效可接受):恢复会话到用户发出首条
223
+ // 消息之间的评估窗口保护,首条消息更新产物 mtime 后由阈值自然接管
224
+ let unarchivedAtMs = 0
220
225
  // 删除路由同 id 并发去重(进程内)
221
226
  const inFlightDeletes = new Set()
222
227
 
@@ -258,6 +263,8 @@ export function apply(ctx, config) {
258
263
  const persistence = ctx.get('sessionPersistence')
259
264
  const records = await ctx.sessionQuery.listSessions()
260
265
  const nowMs = Date.now()
266
+ // 全局宽限短路:恢复动作后 graceMs 内暂停归档,保护「恢复 → 首条消息」窗口
267
+ if (unarchiveGraceActive(unarchivedAtMs, nowMs)) return
261
268
  const cutoff = nowMs - days * DAY_MS
262
269
  const candidates = []
263
270
  for (const recordItem of records) {
@@ -385,6 +392,11 @@ export function apply(ctx, config) {
385
392
  })
386
393
  }
387
394
 
395
+ // 宽限记账:取消归档时更新宽限起点(进程内变量,无持久化副作用)
396
+ function recordUnarchiveGrace() {
397
+ unarchivedAtMs = Date.now()
398
+ }
399
+
388
400
  // 台账命中查询:同 id 曾删除过(重删场景的幽灵资格依据);域不可用时按未命中
389
401
  async function ledgerHasEntry(sessionId) {
390
402
  try {
@@ -451,7 +463,9 @@ export function apply(ctx, config) {
451
463
  handler: async (req, res) => {
452
464
  if (!rejectMethod(req, res, 'POST')) return
453
465
  try {
454
- await removeArchivedId(await requireSessionId(req))
466
+ const sessionId = await requireSessionId(req)
467
+ await removeArchivedId(sessionId)
468
+ await recordUnarchiveGrace()
455
469
  sendJson(res, 200, { ok: true })
456
470
  } catch (error) {
457
471
  respondError(ctx, res, error)
@@ -26,6 +26,8 @@ import {
26
26
  projectDeletedRows,
27
27
  removeDeletedEntry,
28
28
  selectArchiveCandidates,
29
+ UNARCHIVE_GRACE_MS,
30
+ unarchiveGraceActive,
29
31
  updatedAtOf,
30
32
  workspaceTitleForSession,
31
33
  workspaceTitleOf,
@@ -80,6 +82,18 @@ test('阈值负值与非有限值防御性关闭', () => {
80
82
  assert.deepEqual(selectArchiveCandidates({ records: [record({})], nowMs: NOW, thresholdDays: Number.NaN }), [])
81
83
  })
82
84
 
85
+ test('全局宽限:取消归档后 graceMs 内宽限生效,到期与非法输入失效', () => {
86
+ assert.equal(unarchiveGraceActive(NOW - 2 * 60 * 1000, NOW), true)
87
+ assert.equal(unarchiveGraceActive(NOW - UNARCHIVE_GRACE_MS, NOW), false)
88
+ assert.equal(unarchiveGraceActive(0, NOW), false)
89
+ assert.equal(unarchiveGraceActive(undefined, NOW), false)
90
+ assert.equal(unarchiveGraceActive(Number.NaN, NOW), false)
91
+ })
92
+
93
+ test('全局宽限:常量与天级时长的量级自洽', () => {
94
+ assert.equal(UNARCHIVE_GRACE_MS, 10 * 60 * 1000)
95
+ })
96
+
83
97
  test('updatedAt 取创建时间与最近活跃的较大者', () => {
84
98
  assert.equal(updatedAtOf({ createdAt: 100 }, 50), 100)
85
99
  assert.equal(updatedAtOf({ createdAt: 100 }, 500), 500)
@@ -76,7 +76,7 @@ function makeDomain(initialIds) {
76
76
  // 台账域句柄;openRejected 模拟台账域打开失败
77
77
  function makeLedgerDomain(entries) {
78
78
  const domain = {
79
- state: { deleted: entries },
79
+ state: { deleted: entries, unarchivedAt: 0 },
80
80
  writes: 0,
81
81
  global: {
82
82
  get: () => domain.state,
@@ -481,6 +481,40 @@ test('取消归档路由:快照领先域时补全清理(重试路径)', skipMiss
481
481
  assert.deepEqual(registry.state.archivedSessionIds, [])
482
482
  })
483
483
 
484
+ test('取消归档宽限:恢复后 graceMs 内同进程评估暂停,不秒归档', skipMissingDeps, async () => {
485
+ const dir = await mkdtemp(path.join(tmpdir(), 'sm-grace-'))
486
+ try {
487
+ const artifact = path.join(dir, 's1.jsonl')
488
+ await writeFile(artifact, '{"header":1}\n{"event":0}\n')
489
+ const DAY_MS = 24 * 60 * 60 * 1000
490
+ const stale = Date.now() - 30 * DAY_MS
491
+ await utimes(artifact, stale / 1000, stale / 1000)
492
+ const headers = [{ id: 's1', cwd: 'C:\\x', createdAt: stale }]
493
+ const ctxBundle = makeCtx({
494
+ archivedIds: [],
495
+ headers,
496
+ agents: new Map(),
497
+ sessionPersistence: { locate: () => ({ path: artifact }) },
498
+ domain: makeDomain([]),
499
+ })
500
+ const { activateSettings, registry, handlers, eventHandlers } = ctxBundle
501
+ activateSettings()
502
+ await waitFor(() => registry.archiveCalls.length > 0)
503
+ assert.deepEqual(registry.archiveCalls, ['s1'])
504
+ // 手动恢复:归档集合移除 + 进程内宽限生效
505
+ const res = response()
506
+ await handlers.get('/api/session-manager/unarchive')(request('s1'), res)
507
+ assert.equal(res.status, 200)
508
+ assert.deepEqual(registry.state.archivedSessionIds, [])
509
+ // 宽限内经新会话创建触发评估:同产物仍超期,但不再归档
510
+ eventHandlers['session/created']({ header: { cwd: 'C:\\x' } })
511
+ await new Promise((resolve) => setTimeout(resolve, 80))
512
+ assert.deepEqual(registry.archiveCalls, ['s1'], '宽限期内不得重新归档')
513
+ } finally {
514
+ await rm(dir, { recursive: true, force: true })
515
+ }
516
+ })
517
+
484
518
  test('info 路由:会话不存在拒绝', skipMissingDeps, async () => {
485
519
  const { handlers } = makeCtx({ archivedIds: [], headers: [], agents: new Map() })
486
520
  const res = response()