dsh-manage-session 0.2.3 → 0.3.3
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.en.md +14 -0
- package/README.ja.md +14 -0
- package/README.ko.md +14 -0
- package/README.md +14 -0
- package/dsh.plugin.json +4 -4
- package/lib/client.js +12 -3
- package/lib/index.d.ts +167 -10
- package/lib/index.js +331 -44
- package/package.json +10 -9
- package/screenshots.json +5 -1
- package/src/client/health-panel.tsx +5 -1
- package/src/client/host-api.ts +6 -1
- package/src/client/locales.ts +7 -2
- package/src/host/health/cache.ts +1 -1
- package/src/host/health/decode.ts +19 -1
- package/src/host/health/gates.ts +91 -3
- package/src/host/health/generation.ts +216 -0
- package/src/host/health/repair.ts +28 -7
- package/src/host/health/scan.ts +84 -27
- package/src/index.ts +27 -7
package/src/host/health/scan.ts
CHANGED
|
@@ -1,29 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 批量体检:枚举会话日志、跑门、给出汇总(供 `session-health-scan` 使用)。
|
|
3
3
|
*
|
|
4
4
|
* 只读;不激活任何 Agent,也不写任何会话数据。为控制成本,默认限制单次扫描的
|
|
5
5
|
* 会话数与并发度,并支持 `limit` / `since` 过滤。
|
|
6
|
+
*
|
|
7
|
+
* **发现按代次而不是按固定文件名。** 一个会话目录的当前日志名由目录实测的最高
|
|
8
|
+
* 代次决定(见 `generation.ts`):历史代是 `session.jsonl.zstd`,已发布的当前代
|
|
9
|
+
* 是 `session.v<N>.jsonl.zstd`。早期版本在这里写死 `session.jsonl.zstd`,于是
|
|
10
|
+
* 61 个只留当前代的会话**一条都发现不了** —— 既扫不到,`findSessionLog` 也返回
|
|
11
|
+
* undefined,连 `session-health-repair` 都会先报「未找到会话日志」。
|
|
6
12
|
*/
|
|
7
|
-
import { existsSync, readdirSync,
|
|
13
|
+
import { existsSync, readdirSync, type Dirent } from 'node:fs'
|
|
8
14
|
import { homedir } from 'node:os'
|
|
9
15
|
import { join } from 'node:path'
|
|
16
|
+
import {
|
|
17
|
+
latestArtifactMtime,
|
|
18
|
+
readSessionGenerations,
|
|
19
|
+
sessionPriority,
|
|
20
|
+
type SessionGenerations,
|
|
21
|
+
type SessionPriority,
|
|
22
|
+
} from './generation.ts'
|
|
10
23
|
import { buildSessionReport, type GateContext, type SessionHealthReport } from './gates.ts'
|
|
11
24
|
|
|
12
|
-
/**
|
|
25
|
+
/** 一个已发现的会话。 */
|
|
13
26
|
export interface DiscoveredSession {
|
|
14
27
|
sessionId: string
|
|
15
|
-
|
|
28
|
+
/** 承载日志的代次目录。 */
|
|
29
|
+
dir: string
|
|
30
|
+
/**
|
|
31
|
+
* 当前代日志路径。
|
|
32
|
+
*
|
|
33
|
+
* 目录内只有迁移暂存(尚未发布当前代)时为 undefined —— 该会话确实存在,
|
|
34
|
+
* 但没有可读的规范产物,由 `generation` / `log-integrity` 两门如实报出,
|
|
35
|
+
* 而不是当作「没有这个会话」。
|
|
36
|
+
*/
|
|
37
|
+
logPath?: string
|
|
38
|
+
/** 代次事实(当前代 / 历史代 / 暂存残留)。 */
|
|
39
|
+
generations: SessionGenerations
|
|
40
|
+
/** 处置优先级:`high` = 当前代是从暂存(TMP)发布出来的。 */
|
|
41
|
+
priority: SessionPriority
|
|
16
42
|
updatedAt: number
|
|
17
43
|
bytes: number
|
|
18
44
|
}
|
|
19
45
|
|
|
20
|
-
/** 会话根目录(`<dshHome>/sessions/<project>/<sessionId
|
|
46
|
+
/** 会话根目录(`<dshHome>/sessions/<project>/<sessionId>/`)。 */
|
|
21
47
|
export function sessionsRoot(dshHome?: string): string {
|
|
22
48
|
return join(dshHome ?? join(homedir(), '.dsh'), 'sessions')
|
|
23
49
|
}
|
|
24
50
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
51
|
+
/**
|
|
52
|
+
* 语料枚举上限。
|
|
53
|
+
*
|
|
54
|
+
* 这是防御无界遍历的**边界**,不是策略:早期版本写 200,实测语料 449 个会话时
|
|
55
|
+
* 它静默截掉了一半(体检进度分母恒为 200,末 249 个会话永远扫不到)。
|
|
56
|
+
*/
|
|
57
|
+
const DISCOVERY_LIMIT = 100_000
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 枚举全部会话(按 mtime 倒序),带可选上限。
|
|
61
|
+
*
|
|
62
|
+
* 「是会话目录」的判据是**目录里有规范代次产物或迁移暂存**,不要求任何具体文件名。
|
|
63
|
+
* @param dshHome - DSH home(缺省 ~/.dsh)。
|
|
64
|
+
* @param limit - 返回上限(缺省 `DISCOVERY_LIMIT`)。
|
|
65
|
+
*/
|
|
66
|
+
export function discoverSessions(dshHome?: string, limit = DISCOVERY_LIMIT): DiscoveredSession[] {
|
|
27
67
|
const root = sessionsRoot(dshHome)
|
|
28
68
|
const out: DiscoveredSession[] = []
|
|
29
69
|
if (!existsSync(root)) return out
|
|
@@ -38,22 +78,23 @@ export function discoverSessions(dshHome?: string, limit = 200): DiscoveredSessi
|
|
|
38
78
|
continue
|
|
39
79
|
}
|
|
40
80
|
for (const entry of entries) {
|
|
81
|
+
if (!entry.isDirectory()) continue
|
|
41
82
|
const path = join(dir, entry.name)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (existsSync(candidate)) {
|
|
45
|
-
let stats: { mtimeMs: number; size: number }
|
|
46
|
-
try {
|
|
47
|
-
const stat = statSync(candidate)
|
|
48
|
-
stats = { mtimeMs: stat.mtimeMs, size: stat.size }
|
|
49
|
-
} catch {
|
|
50
|
-
continue
|
|
51
|
-
}
|
|
52
|
-
out.push({ sessionId: entry.name, logPath: candidate, updatedAt: stats.mtimeMs, bytes: stats.size })
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
83
|
+
const generations = readSessionGenerations(path)
|
|
84
|
+
if (generations.canonical.length === 0 && generations.staging.length === 0) {
|
|
55
85
|
stack.push(path)
|
|
86
|
+
continue
|
|
56
87
|
}
|
|
88
|
+
const current = generations.current
|
|
89
|
+
out.push({
|
|
90
|
+
sessionId: entry.name,
|
|
91
|
+
dir: path,
|
|
92
|
+
...(current === undefined ? {} : { logPath: current.path }),
|
|
93
|
+
generations,
|
|
94
|
+
priority: sessionPriority(generations),
|
|
95
|
+
updatedAt: latestArtifactMtime(generations) ?? 0,
|
|
96
|
+
bytes: current?.bytes ?? 0,
|
|
97
|
+
})
|
|
57
98
|
}
|
|
58
99
|
}
|
|
59
100
|
out.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
@@ -63,7 +104,7 @@ export function discoverSessions(dshHome?: string, limit = 200): DiscoveredSessi
|
|
|
63
104
|
/**
|
|
64
105
|
* 语料总数:只做目录枚举,**不跑体检**。
|
|
65
106
|
*
|
|
66
|
-
* 用于判断缓存是否已过期——枚举很便宜,而重扫要解 zstd
|
|
107
|
+
* 用于判断缓存是否已过期——枚举很便宜,而重扫要解 zstd、跑门。
|
|
67
108
|
* 正因为两者代价差着量级,「对账」才不构成缓存失效策略本身。
|
|
68
109
|
* @param dshHome - DSH home(缺省 ~/.dsh)。
|
|
69
110
|
*/
|
|
@@ -71,9 +112,21 @@ export function countCorpus(dshHome?: string): number {
|
|
|
71
112
|
return discoverSessions(dshHome, DISCOVERY_LIMIT).length
|
|
72
113
|
}
|
|
73
114
|
|
|
74
|
-
/**
|
|
115
|
+
/** 按 id 定位一个会话(跨工程目录查找)。 */
|
|
116
|
+
export function findSession(sessionId: string, dshHome?: string): DiscoveredSession | undefined {
|
|
117
|
+
return discoverSessions(dshHome, DISCOVERY_LIMIT).find(session => session.sessionId === sessionId)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 定位一个会话的**当前代**日志路径(跨工程目录查找)。
|
|
122
|
+
*
|
|
123
|
+
* 返回 undefined 有两种含义,调用方必须区分:会话不存在,或会话存在但尚未发布
|
|
124
|
+
* 当前代(只有迁移暂存)。需要区分时用 `findSession`。
|
|
125
|
+
* @param sessionId - 会话 id。
|
|
126
|
+
* @param dshHome - DSH home(缺省 ~/.dsh)。
|
|
127
|
+
*/
|
|
75
128
|
export function findSessionLog(sessionId: string, dshHome?: string): string | undefined {
|
|
76
|
-
return
|
|
129
|
+
return findSession(sessionId, dshHome)?.logPath
|
|
77
130
|
}
|
|
78
131
|
|
|
79
132
|
/** 扫描结果。 */
|
|
@@ -94,9 +147,6 @@ export interface HealthScanResult {
|
|
|
94
147
|
error?: string
|
|
95
148
|
}
|
|
96
149
|
|
|
97
|
-
/** 语料枚举上限(进度分母的来源;与 route 侧 MAX_SCAN_LIMIT 同量级)。 */
|
|
98
|
-
const DISCOVERY_LIMIT = 200
|
|
99
|
-
|
|
100
150
|
/** 未显式传 limit 时的单批会话数。 */
|
|
101
151
|
const DEFAULT_BATCH_LIMIT = 50
|
|
102
152
|
|
|
@@ -106,6 +156,9 @@ const DEFAULT_BATCH_LIMIT = 50
|
|
|
106
156
|
* 语料先整体列出(按 mtime 倒序,≤ DISCOVERY_LIMIT),再按 `[offset, offset+limit)`
|
|
107
157
|
* 切片扫描。这样客户端可以用小批次连续调用、自己累计真实进度,而宿主保持无状态
|
|
108
158
|
* ——不必把同步循环改成异步,也不必新增进度轮询端点。
|
|
159
|
+
*
|
|
160
|
+
* 批内**命中顺序**按处置优先级排(`high` 在前,稳定排序)。分批切片仍按 mtime,
|
|
161
|
+
* 所以 offset/total 的算术不受影响:只有同一批里的呈现顺序变了。
|
|
109
162
|
* @param options - dshHome / 批大小 / 批起点 / 是否只返回非 ok / 归属查询 / 热态状态提供者。
|
|
110
163
|
*/
|
|
111
164
|
export function scanSessions(options: {
|
|
@@ -137,7 +190,8 @@ export function scanSessions(options: {
|
|
|
137
190
|
const report = buildSessionReport({
|
|
138
191
|
sessionId: session.sessionId,
|
|
139
192
|
...(options.dshHome === undefined ? {} : { dshHome: options.dshHome }),
|
|
140
|
-
logPath: session.logPath,
|
|
193
|
+
...(session.logPath === undefined ? {} : { logPath: session.logPath }),
|
|
194
|
+
generations: session.generations,
|
|
141
195
|
...(projectionState === undefined ? {} : { projectionState }),
|
|
142
196
|
...(options.attribute === undefined ? {} : { attribute: options.attribute }),
|
|
143
197
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
@@ -145,6 +199,9 @@ export function scanSessions(options: {
|
|
|
145
199
|
if (options.onlyProblems === true && report.level === 'ok') continue
|
|
146
200
|
findings.push(report)
|
|
147
201
|
}
|
|
202
|
+
// 「从 TMP 移出来」的会话排在最前:它们是本插件此前读错产物(历史代)或
|
|
203
|
+
// 完全发现不了的会话,先看到才能先处置。
|
|
204
|
+
findings.sort((left, right) => (left.priority === right.priority ? 0 : left.priority === 'high' ? -1 : 1))
|
|
148
205
|
return {
|
|
149
206
|
ok: true,
|
|
150
207
|
scanned: batch.length,
|
package/src/index.ts
CHANGED
|
@@ -27,19 +27,36 @@ import { buildProjectionOwnerIndex, createAttributor, defaultProfileNodeModules
|
|
|
27
27
|
import { HealthCache } from './host/health/cache.ts'
|
|
28
28
|
import { buildSessionReport, type SessionHealthReport } from './host/health/gates.ts'
|
|
29
29
|
import { assessRepair, prescribe, quarantineProjectionCache } from './host/health/repair.ts'
|
|
30
|
-
import { countCorpus,
|
|
30
|
+
import { countCorpus, findSession, scanSessions } from './host/health/scan.ts'
|
|
31
31
|
|
|
32
32
|
export { DEFAULT_CONFIG, STEWARD_API_PREFIX, STEWARD_SETTINGS_NAMESPACE } from './config.ts'
|
|
33
33
|
export type { StewardConfig } from './config.ts'
|
|
34
34
|
export { readArchiveSet, pruneArchiveFile, editWorkspaceDocument } from './host/history/archive-source.ts'
|
|
35
35
|
export { listHistory, pruneHistory } from './host/history/archive.ts'
|
|
36
36
|
export { purgeHistory, locateSessionUsage, indexSessionDirs, isSafeChild, dirSize, sessionsRootFor, projCacheRootFor } from './host/history/purge.ts'
|
|
37
|
-
export { buildSessionReport, gateColdRead, gateLogIntegrity, gateLosslessJson, gateProjectionCache, readProjectionCache, readTailFacts } from './host/health/gates.ts'
|
|
37
|
+
export { buildSessionReport, gateColdRead, gateGeneration, gateLogIntegrity, gateLosslessJson, gateProjectionCache, readProjectionCache, readTailFacts } from './host/health/gates.ts'
|
|
38
38
|
export { firstLosslessViolation, isLossless } from './host/health/lossless.ts'
|
|
39
39
|
export { decodeSessionLogBytes, decodeSessionLogFile, scanZstdFrames } from './host/health/decode.ts'
|
|
40
|
+
export {
|
|
41
|
+
classifyGenerationFilename,
|
|
42
|
+
generationLogFilename,
|
|
43
|
+
isMigrationStagingFilename,
|
|
44
|
+
latestArtifactMtime,
|
|
45
|
+
parseGenerationLogFilename,
|
|
46
|
+
readSessionGenerations,
|
|
47
|
+
sessionPriority,
|
|
48
|
+
} from './host/health/generation.ts'
|
|
49
|
+
export type {
|
|
50
|
+
GenerationArtifact,
|
|
51
|
+
LogArtifact,
|
|
52
|
+
LogCompression,
|
|
53
|
+
SessionGenerations,
|
|
54
|
+
SessionPriority,
|
|
55
|
+
} from './host/health/generation.ts'
|
|
40
56
|
export { buildProjectionOwnerIndex, createAttributor } from './host/health/attribution.ts'
|
|
41
57
|
export { prescribe, quarantineProjectionCache } from './host/health/repair.ts'
|
|
42
|
-
export { countCorpus, discoverSessions, findSessionLog, scanSessions } from './host/health/scan.ts'
|
|
58
|
+
export { countCorpus, discoverSessions, findSession, findSessionLog, scanSessions } from './host/health/scan.ts'
|
|
59
|
+
export type { DiscoveredSession } from './host/health/scan.ts'
|
|
43
60
|
export { HealthCache } from './host/health/cache.ts'
|
|
44
61
|
export type { HealthCacheEntry } from './host/health/cache.ts'
|
|
45
62
|
export { assessRepair } from './host/health/repair.ts'
|
|
@@ -335,14 +352,17 @@ export async function handleMethod(
|
|
|
335
352
|
const request = (payload ?? {}) as { sessionId?: unknown }
|
|
336
353
|
const sessionId = asSessionId(request.sessionId)
|
|
337
354
|
if (sessionId === undefined) return { ok: false, error: '缺少合法的 sessionId' }
|
|
338
|
-
|
|
339
|
-
|
|
355
|
+
// 按代次定位:会话存在但尚未发布当前代时 logPath 为 undefined,此时仍要出报告
|
|
356
|
+
// (generation / log-integrity 两门如实报出),不能笼统回「未找到会话日志」。
|
|
357
|
+
const session = findSession(sessionId, runtime.dshHome)
|
|
358
|
+
if (session === undefined) return { ok: false, error: `未找到会话日志:${sessionId}` }
|
|
340
359
|
|
|
341
360
|
const projectionState = runtime.projectionStateFor(sessionId)
|
|
342
361
|
const reportFor = (): SessionHealthReport => buildSessionReport({
|
|
343
362
|
sessionId,
|
|
344
363
|
dshHome: runtime.dshHome,
|
|
345
|
-
logPath,
|
|
364
|
+
...(session.logPath === undefined ? {} : { logPath: session.logPath }),
|
|
365
|
+
generations: session.generations,
|
|
346
366
|
...(projectionState === undefined ? {} : { projectionState }),
|
|
347
367
|
attribute: runtime.attribute,
|
|
348
368
|
})
|
|
@@ -371,7 +391,7 @@ export async function handleMethod(
|
|
|
371
391
|
verdict: clean.verdict,
|
|
372
392
|
explanation: clean.explanation,
|
|
373
393
|
residual: clean.residual,
|
|
374
|
-
prescriptions: ['#
|
|
394
|
+
prescriptions: ['# 全部检查通过:无需处置'],
|
|
375
395
|
}
|
|
376
396
|
}
|
|
377
397
|
const repair = quarantineProjectionCache(sessionId, runtime.dshHome)
|