@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 +1 -1
- package/package.json +8 -1
- package/src/client.js +1 -1
- package/src/core.mjs +10 -1
- package/src/index.js +15 -1
- package/test/core.test.mjs +14 -0
- package/test/index.test.mjs +35 -1
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
|
-
|
|
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.
|
|
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
|
-
|
|
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)
|
package/test/core.test.mjs
CHANGED
|
@@ -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)
|
package/test/index.test.mjs
CHANGED
|
@@ -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()
|