@zhushanwen/pi-session-reader 0.5.1 → 0.6.1
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/package.json +6 -3
- package/src/__tests__/cross-package-subagent-core.test.ts +42 -49
- package/src/__tests__/env.test.ts +54 -54
- package/src/__tests__/family.test.ts +0 -7
- package/src/__tests__/find.test.ts +6 -6
- package/src/__tests__/index.test.ts +1 -1
- package/src/__tests__/real-data.ts +23 -10
- package/src/__tests__/render.test.ts +36 -1
- package/src/__tests__/result.test.ts +20 -23
- package/src/__tests__/roots.test.ts +47 -81
- package/src/__tests__/session-command.test.ts +23 -0
- package/src/__tests__/subagents.test.ts +5 -7
- package/src/__tests__/tool-handler.test.ts +481 -216
- package/src/__tests__/workflow.test.ts +1 -7
- package/src/core/execution-tree.ts +1 -0
- package/src/core/family.ts +12 -9
- package/src/core/render.ts +41 -14
- package/src/core/toolcall.ts +1 -2
- package/src/core/units.ts +14 -0
- package/src/core/workflow.ts +7 -20
- package/src/discovery/env.ts +41 -41
- package/src/discovery/find.ts +43 -135
- package/src/discovery/roots.ts +18 -89
- package/src/discovery/session-header.ts +103 -0
- package/src/discovery/subagents.ts +23 -94
- package/src/doctor.ts +12 -143
- package/src/extract.ts +6 -19
- package/src/handler-utils.ts +28 -5
- package/src/index.ts +2 -2
- package/src/no-match.ts +3 -2
- package/src/result-action.ts +29 -34
- package/src/search-across.ts +6 -8
- package/src/tool-handler.ts +151 -176
- package/src/tui/hash-provider.ts +7 -3
- package/src/tui/session-command.ts +22 -10
- package/vitest.config.ts +2 -4
|
@@ -2,9 +2,10 @@ import { readFile, readdir, open, stat } from 'node:fs/promises'
|
|
|
2
2
|
import type { FileHandle } from 'node:fs/promises'
|
|
3
3
|
import { join, basename } from 'node:path'
|
|
4
4
|
import type { Entry } from '../core/parser.js'
|
|
5
|
-
import type { Family, SessionRef
|
|
5
|
+
import type { Family, SessionRef } from '../core/family.js'
|
|
6
6
|
import { buildFamilyIndex, resolveFamily } from '../core/family.js'
|
|
7
|
-
import {
|
|
7
|
+
import { resolveSessionRoots, type SessionFileMeta } from './roots.js'
|
|
8
|
+
import { parseSessionHeader, readSessionHeaderFirstLine } from './session-header.js'
|
|
8
9
|
import { resolveWorkflows } from './workflows.js'
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -56,8 +57,8 @@ export async function buildFamilyFromFs(sessionId: string, agentDir: string): Pr
|
|
|
56
57
|
const index = buildFamilyIndex(scan.headers, scan.identities, scan.fileStats)
|
|
57
58
|
const family = resolveFamily(sessionId, index)
|
|
58
59
|
|
|
59
|
-
// ---- 6.
|
|
60
|
-
|
|
60
|
+
// ---- 6. workflows(enrichRefs 回填已删除,ext-simplify-04 U4/E2:fileName/cwd 占位
|
|
61
|
+
// 零文本读者,family 路径维持 core 层占位空串;富字段的消费者 = formatFamilyText 展示)----
|
|
61
62
|
family.workflows = await resolveWorkflows(sessionId, scan.sessionIdToPath, scan.pathToRef)
|
|
62
63
|
|
|
63
64
|
return family
|
|
@@ -71,10 +72,12 @@ export async function buildFamilyFromFs(sessionId: string, agentDir: string): Pr
|
|
|
71
72
|
* not-found 错误文案:列出实际扫描过的 main 候选根(U1 并入部分,design §6.1 自证能力
|
|
72
73
|
* ——「没有这条 session」与「根配错了」必须可区分,错误信息携带发现层内部状态)。
|
|
73
74
|
* 仅失败路径调用,额外一次 resolveSessionRoots 的扫描成本可接受;根过滤条件与
|
|
74
|
-
* collectMainSessions 实扫集合一致(
|
|
75
|
+
* collectMainSessions 实扫集合一致(agentDir 信号包下未去重的 main 根)。
|
|
75
76
|
*/
|
|
76
77
|
async function formatSessionNotFound(sessionId: string, agentDir: string): Promise<string> {
|
|
77
|
-
|
|
78
|
+
// subagents:'stat'——本路径只渲染 main 根行,subagent 根不做深扫(ext-simplify-04 A2:
|
|
79
|
+
// 原默认 'scan' 白扫整个 subagent 根后 filter 丢弃)
|
|
80
|
+
const mainRoots = (await resolveSessionRoots({ agentDir }, { subagents: 'stat' })).filter(
|
|
78
81
|
(r) => r.source === 'main' && r.dedupedInto === undefined,
|
|
79
82
|
)
|
|
80
83
|
const lines = mainRoots.map((r) => {
|
|
@@ -97,9 +100,9 @@ interface FamilyFsScan {
|
|
|
97
100
|
identities: Entry[]
|
|
98
101
|
/** sessionId → { mtime, size }(buildFamilyIndex 存活判定素材) */
|
|
99
102
|
fileStats: Map<string, { mtime: number; size: number }>
|
|
100
|
-
/** sessionId → 真实文件路径,供 workflows 找目标文件
|
|
103
|
+
/** sessionId → 真实文件路径,供 workflows 找目标文件 */
|
|
101
104
|
sessionIdToPath: Map<string, string>
|
|
102
|
-
/** path → 完整 SessionRef(含真实 cwd/fileName/mtime/size),供
|
|
105
|
+
/** path → 完整 SessionRef(含真实 cwd/fileName/mtime/size),供 workflow calls 反查 */
|
|
103
106
|
pathToRef: Map<string, SessionRef>
|
|
104
107
|
/** 已扫描到的 subagent 文件路径集合,供 manifest 孤儿判定(alive 则跳过 manifest) */
|
|
105
108
|
aliveSubPaths: Set<string>
|
|
@@ -114,9 +117,13 @@ function indexManifestsBySessionFile(manifests: RecordManifest[]): Map<string, R
|
|
|
114
117
|
|
|
115
118
|
/** 步骤 1:main sessions —— 首行 header → headers + fileStats + 路径反查 */
|
|
116
119
|
async function collectMainSessions(agentDir: string, scan: FamilyFsScan): Promise<void> {
|
|
117
|
-
|
|
120
|
+
// A3(ext-simplify-04):直调 resolveSessionRoots + filter(原 listMainSessions 薄包装
|
|
121
|
+
// 已删除)——agentDir 信号包下 main 源未去重根([default]+[legacy],被 [live] 去重的除外)的文件并集
|
|
122
|
+
const mainMetas = (await resolveSessionRoots({ agentDir }))
|
|
123
|
+
.filter((r) => r.source === 'main' && r.dedupedInto === undefined)
|
|
124
|
+
.flatMap((r) => r.files)
|
|
118
125
|
for (const meta of mainMetas) {
|
|
119
|
-
const h =
|
|
126
|
+
const h = parseSessionHeader(await readSessionHeaderFirstLine(meta.path))
|
|
120
127
|
if (!h) continue // 非 session/坏 header → 跳过(不入 byId)
|
|
121
128
|
const entry: Entry = { type: 'session', id: h.id, parentId: null, cwd: h.cwd ?? '' }
|
|
122
129
|
if (h.parentSession) entry.parentSession = h.parentSession
|
|
@@ -171,9 +178,12 @@ async function collectSubagentIdentities(
|
|
|
171
178
|
): Promise<void> {
|
|
172
179
|
// U4 数据流:manifest 命中透全字段;未命中 P-fallback 读尾行 identity 取 task/slug/agent
|
|
173
180
|
//(model/status 不可回退,留 undefined);无 manifest 无 identity(运行中/异常)跳过。
|
|
174
|
-
|
|
181
|
+
// A3(ext-simplify-04):直调 resolveSessionRoots(原 listSubagentSessions 薄包装已删除)
|
|
182
|
+
//——agentDir 信号包下 [subagent] 根 = <agentDir>/subagents(常量推导,恒唯一)
|
|
183
|
+
const subMetas =
|
|
184
|
+
(await resolveSessionRoots({ agentDir })).find((r) => r.kind === 'subagent')?.files ?? []
|
|
175
185
|
for (const meta of subMetas) {
|
|
176
|
-
const h =
|
|
186
|
+
const h = parseSessionHeader(await readSessionHeaderFirstLine(meta.path))
|
|
177
187
|
if (!h) continue // 非 session/坏 header → 无真实 session id,无法 id 修正,跳过
|
|
178
188
|
const realId = h.id
|
|
179
189
|
// header 可解析即视为 alive(MF-3):identity 在文件尾行、完成时才写入,运行中的 subagent
|
|
@@ -244,55 +254,12 @@ function appendOrphanIdentities(manifests: RecordManifest[], scan: FamilyFsScan)
|
|
|
244
254
|
|
|
245
255
|
// ============================================================
|
|
246
256
|
// 文件读取 helpers(定长 buffer,避免全文读:subagent 文件均 269KB、总量 ~923MB)
|
|
257
|
+
// 首行 header 读取/解析在 session-header.ts(D5 单源);本文件仅保留尾行 identity 读取。
|
|
247
258
|
// ============================================================
|
|
248
259
|
|
|
249
|
-
/** 首行读取 buffer 上限。session header(id/cwd/parentSession)实测 < 300 字节,8KB 足够。 */
|
|
250
|
-
const HEADER_READ_BYTES = 8192
|
|
251
260
|
/** 尾行 identity 读取 buffer 上限。identity 含完整 task 文本可达数 KB;实测 64KB 覆盖 3203/3430。 */
|
|
252
261
|
const TAIL_READ_BYTES = 65536
|
|
253
262
|
|
|
254
|
-
/** 读文件首行(header)。定长 8KB 一次 read;空文件/读失败返回 undefined。 */
|
|
255
|
-
async function readFirstLine(path: string): Promise<string | undefined> {
|
|
256
|
-
let fh: FileHandle | undefined
|
|
257
|
-
try {
|
|
258
|
-
fh = await open(path, 'r')
|
|
259
|
-
const buf = Buffer.alloc(HEADER_READ_BYTES)
|
|
260
|
-
const { bytesRead } = await fh.read(buf, 0, HEADER_READ_BYTES, 0)
|
|
261
|
-
if (bytesRead === 0) return undefined
|
|
262
|
-
const text = buf.subarray(0, bytesRead).toString('utf8')
|
|
263
|
-
const nl = text.indexOf('\n')
|
|
264
|
-
return nl === -1 ? text : text.slice(0, nl)
|
|
265
|
-
} catch {
|
|
266
|
-
return undefined
|
|
267
|
-
} finally {
|
|
268
|
-
await fh?.close().catch(() => {})
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
interface SessionHeader {
|
|
273
|
-
id: string
|
|
274
|
-
cwd?: string
|
|
275
|
-
parentSession?: string
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** 解析 header 首行为 SessionHeader。非 session 行/缺 id → null。 */
|
|
279
|
-
function parseHeaderLine(line: string | undefined): SessionHeader | null {
|
|
280
|
-
if (!line) return null
|
|
281
|
-
let raw: unknown
|
|
282
|
-
try {
|
|
283
|
-
raw = JSON.parse(line)
|
|
284
|
-
} catch {
|
|
285
|
-
return null
|
|
286
|
-
}
|
|
287
|
-
if (typeof raw !== 'object' || raw === null) return null
|
|
288
|
-
const o = raw as Record<string, unknown>
|
|
289
|
-
if (o.type !== 'session' || typeof o.id !== 'string') return null
|
|
290
|
-
const h: SessionHeader = { id: o.id }
|
|
291
|
-
if (typeof o.cwd === 'string') h.cwd = o.cwd
|
|
292
|
-
if (typeof o.parentSession === 'string') h.parentSession = o.parentSession
|
|
293
|
-
return h
|
|
294
|
-
}
|
|
295
|
-
|
|
296
263
|
/** readTailIdentity 的成功返回形状(rootSessionId 必有,其余存在才带) */
|
|
297
264
|
type TailIdentity = {
|
|
298
265
|
rootSessionId: string
|
|
@@ -488,41 +455,3 @@ export function extractSessionIdFromFilename(name: string): string {
|
|
|
488
455
|
const candidate = idx >= 0 ? noExt.slice(idx + 1) : noExt
|
|
489
456
|
return /^[0-9a-f-]{8,}$/i.test(candidate) ? candidate : ''
|
|
490
457
|
}
|
|
491
|
-
|
|
492
|
-
// ============================================================
|
|
493
|
-
// enrich:补 M1 占位字段(fileName / subagent cwd)
|
|
494
|
-
// ============================================================
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* M1 buildFamilyIndex 设 SessionRef.fileName=''(header 推不出路径)、subagent cwd=''
|
|
498
|
-
*(identity 无 cwd)。此处用已扫描的真实文件信息补全:alive 的 ref 补 fileName + cwd;
|
|
499
|
-
* cleanedUp 孤儿(无文件)保持占位。
|
|
500
|
-
*/
|
|
501
|
-
function enrichRefs(family: Family, pathToRef: Map<string, SessionRef>): void {
|
|
502
|
-
// sessionId → 完整 ref(含真实 fileName/cwd),由 pathToRef 反建
|
|
503
|
-
const bySid = new Map<string, SessionRef>()
|
|
504
|
-
for (const ref of pathToRef.values()) bySid.set(ref.sessionId, ref)
|
|
505
|
-
|
|
506
|
-
const enrichSessionRef = (ref: SessionRef): SessionRef => {
|
|
507
|
-
const full = bySid.get(ref.sessionId)
|
|
508
|
-
if (!full) return ref
|
|
509
|
-
return {
|
|
510
|
-
...ref,
|
|
511
|
-
fileName: full.fileName || ref.fileName,
|
|
512
|
-
cwd: full.cwd || ref.cwd,
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
family.root = enrichSessionRef(family.root)
|
|
517
|
-
family.parents = family.parents.map(enrichSessionRef)
|
|
518
|
-
family.forks = family.forks.map(enrichSessionRef)
|
|
519
|
-
family.subagents = family.subagents.map((s) => {
|
|
520
|
-
const full = bySid.get(s.sessionId)
|
|
521
|
-
if (!full) return s
|
|
522
|
-
return {
|
|
523
|
-
...s,
|
|
524
|
-
fileName: full.fileName || s.fileName,
|
|
525
|
-
cwd: full.cwd || s.cwd,
|
|
526
|
-
} as SubagentRef
|
|
527
|
-
})
|
|
528
|
-
}
|
package/src/doctor.ts
CHANGED
|
@@ -2,21 +2,12 @@
|
|
|
2
2
|
* doctor action(u8:design 2026-09-10 §6.2/§6.3/§6.4/§7B 要点 2/4/5/8 + §6.11 U14b 段)。
|
|
3
3
|
*
|
|
4
4
|
* 从 tool-handler.ts 机械提取(max-lines 拆分轮,零行为变更):环境判定 + 根表渲染 +
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* SessionReadSignals(u3 信号包超集)在域内定义,tool-handler
|
|
6
|
+
* re-export 供 index.ts 生产消费。原 u8 的扫描缓存机已删除(ext-simplify-04 U3):
|
|
7
|
+
* 每次调用实扫,无缓存命中语义;TTL/mtime 判定随 metadata 缓存留在 tool-handler 侧。
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync } from 'node:fs'
|
|
10
|
-
import { readdir, stat } from 'node:fs/promises'
|
|
11
|
-
import { join, dirname } from 'node:path'
|
|
12
9
|
import { detectEnvironment, type DetectedEnvironment } from './discovery/env.js'
|
|
13
|
-
import {
|
|
14
|
-
resolveSessionRoots,
|
|
15
|
-
type SessionRoot,
|
|
16
|
-
type SessionRootCache,
|
|
17
|
-
type SessionRootCacheEntry,
|
|
18
|
-
type SessionRootSignals,
|
|
19
|
-
} from './discovery/roots.js'
|
|
10
|
+
import { resolveSessionRoots, type SessionRoot, type SessionRootSignals } from './discovery/roots.js'
|
|
20
11
|
import type { SessionReadParams, ToolResult } from './tool-handler.js'
|
|
21
12
|
|
|
22
13
|
/**
|
|
@@ -32,113 +23,11 @@ export interface SessionReadSignals extends SessionRootSignals {
|
|
|
32
23
|
bundleUrl?: string
|
|
33
24
|
}
|
|
34
25
|
|
|
35
|
-
/**
|
|
36
|
-
* doctor 扫描缓存(进程内,keyed by 根字面路径;Map 生命周期 = pi 进程生命周期,
|
|
37
|
-
* 即「进程生命周期兜底上限」)。失效 = 秒级 TTL 到期 **或** 根目录 mtime 变化(任一)。
|
|
38
|
-
*
|
|
39
|
-
* 以 SessionRootCache 句柄注入 resolveSessionRoots(§6.3「同一数据源两处渲染」——根
|
|
40
|
-
* 骨架/去重/扫描全在 roots.ts,doctor 只注入缓存与 subagent 扫描模式);TTL/mtime
|
|
41
|
-
* 失效判定全在本侧实现,roots.ts 不内置。**仅 doctor 消费——find 一律不读缓存**
|
|
42
|
-
*(§7B 要点 8):find 恒不传 options;若 find 读缓存,最坏形态是 PS-14(首条
|
|
43
|
-
* assistant 前 main 根 0 文件被缓存,之后每次都把「根解析正常」误报成「主根为空」)
|
|
44
|
-
* ——正是本设计要消灭的错误归因。
|
|
45
|
-
*/
|
|
46
|
-
interface DoctorCacheEntry extends SessionRootCacheEntry {
|
|
47
|
-
/** 写入时刻(Date.now()),TTL 判定用 */
|
|
48
|
-
cachedAt: number
|
|
49
|
-
/** 根目录 mtime(ms);目录不存在为 null(存在性翻转即失效) */
|
|
50
|
-
dirMtimeMs: number | null
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const doctorScanCache = new Map<string, DoctorCacheEntry>()
|
|
54
|
-
// §7.5 豁免(development-guide.md「纯性能缓存豁免」):TTL 纯性能缓存,jiti 双路径加载
|
|
55
|
-
// 分裂成两份仅多一次 miss 重扫,无正确性影响,不升级 globalThis 单例。
|
|
56
|
-
|
|
57
26
|
/** doctor 根表来源标签列宽(最长 `[subagent]` 10 字符 + 1 对齐间距,§5.1 表格形态)。 */
|
|
58
27
|
const DOCTOR_ROOT_LABEL_PAD = 11
|
|
59
28
|
|
|
60
|
-
/** 缓存 TTL(秒级,§6.3)。mtime 是主失效通道;TTL 兜「子目录内增删不改变根 mtime」的陈旧面。 */
|
|
61
|
-
export const DOCTOR_CACHE_TTL_MS = 5000
|
|
62
|
-
|
|
63
|
-
/** stat 根目录 mtime;不存在返回 null(与缓存条目的 null 比对 = 存在性未翻转)。 */
|
|
64
|
-
export async function statDirMtimeOrNull(path: string): Promise<number | null> {
|
|
65
|
-
try {
|
|
66
|
-
return (await stat(path)).mtimeMs
|
|
67
|
-
} catch (err) {
|
|
68
|
-
// 目录不存在是 doctor 的常态输入(三根降级形态),非异常——void 同 roots.ts 容错
|
|
69
|
-
void err
|
|
70
|
-
return null
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** doctor 侧缓存句柄:get 做失效判定(未命中即删除条目),set 快照 mtime 与写入时刻。 */
|
|
75
|
-
const doctorRootCache: SessionRootCache = {
|
|
76
|
-
async get(key) {
|
|
77
|
-
const hit = doctorScanCache.get(key)
|
|
78
|
-
if (hit === undefined) return undefined
|
|
79
|
-
if (Date.now() - hit.cachedAt >= DOCTOR_CACHE_TTL_MS) {
|
|
80
|
-
doctorScanCache.delete(key)
|
|
81
|
-
return undefined
|
|
82
|
-
}
|
|
83
|
-
if ((await statDirMtimeOrNull(key)) !== hit.dirMtimeMs) {
|
|
84
|
-
doctorScanCache.delete(key)
|
|
85
|
-
return undefined
|
|
86
|
-
}
|
|
87
|
-
return { exists: hit.exists, fileCount: hit.fileCount, scanMs: hit.scanMs }
|
|
88
|
-
},
|
|
89
|
-
async set(key, value) {
|
|
90
|
-
doctorScanCache.set(key, {
|
|
91
|
-
...value,
|
|
92
|
-
cachedAt: Date.now(),
|
|
93
|
-
dirMtimeMs: await statDirMtimeOrNull(key),
|
|
94
|
-
})
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** 旧布局残留条目(§6.11 U14b doctor 侧独立 glob 探测)。 */
|
|
99
|
-
/** 旧布局残留探测单条命中(doctor 与 u14b 启动探测同源判据,§6.11)。 */
|
|
100
|
-
export interface PiLayoutLeftover {
|
|
101
|
-
path: string
|
|
102
|
-
kind: 'backup' | 'unmigrated'
|
|
103
|
-
}
|
|
104
|
-
|
|
105
29
|
/**
|
|
106
|
-
*
|
|
107
|
-
* `<dataDir>`,纯 pi 下 = `~/.pi`);`pi/`(未迁移)与 `pi.backup-v2-` 前缀目录(迁移备份)
|
|
108
|
-
* 都不在任何候选根推导式内([legacy] 够不到带时间戳的备份名与 pi/ 层),故须独立探测。
|
|
109
|
-
*
|
|
110
|
-
* 形态判据(与 u14b 启动探测 / u14a 脚本 0a 同源):目录下含 `agent/` 或 `sessions/`
|
|
111
|
-
* 子目录才算残留——防纯 pi 宿主下任意来源的 `~/.pi/pi/` 目录误报。读探测,每次现查
|
|
112
|
-
* 不入缓存(两次 readdir 成本可忽略,缓存只服务扫盘贵的根)。
|
|
113
|
-
*/
|
|
114
|
-
async function detectPiLayoutLeftovers(
|
|
115
|
-
agentDir: string,
|
|
116
|
-
): Promise<{ base: string; hits: PiLayoutLeftover[] }> {
|
|
117
|
-
const base = dirname(agentDir)
|
|
118
|
-
if (agentDir.length === 0) return { base, hits: [] }
|
|
119
|
-
let names: string[]
|
|
120
|
-
try {
|
|
121
|
-
names = (await readdir(base, { withFileTypes: true }))
|
|
122
|
-
.filter((e) => e.isDirectory())
|
|
123
|
-
.map((e) => e.name)
|
|
124
|
-
} catch {
|
|
125
|
-
return { base, hits: [] }
|
|
126
|
-
}
|
|
127
|
-
const hits: PiLayoutLeftover[] = []
|
|
128
|
-
for (const name of names) {
|
|
129
|
-
const isUnmigrated = name === 'pi'
|
|
130
|
-
const isBackup = name.startsWith('pi.backup-v2-')
|
|
131
|
-
if (!isUnmigrated && !isBackup) continue
|
|
132
|
-
const dir = join(base, name)
|
|
133
|
-
// 形态判据:含 agent/ 或 sessions/ 子目录(迁移备份必含其一——备份即原 pi/ 改名)
|
|
134
|
-
if (!existsSync(join(dir, 'agent')) && !existsSync(join(dir, 'sessions'))) continue
|
|
135
|
-
hits.push({ path: dir, kind: isBackup ? 'backup' : 'unmigrated' })
|
|
136
|
-
}
|
|
137
|
-
return { base, hits }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* doctor:环境判定 + 根表 + 事实型诊断 + legacy 非空告警 + 旧布局残留探测。
|
|
30
|
+
* doctor:环境判定 + 根表 + 事实型诊断 + legacy 非空告警。
|
|
142
31
|
*
|
|
143
32
|
* 诊断结论只陈述事实(「最高优先级 main 根 N 文件」),禁止「真的没有 session」类
|
|
144
33
|
* 归因断言(§7B 要点 5 / §3.3 教训);env/bundleUrl 信号缺失时环境判定按降级输出,
|
|
@@ -155,12 +44,10 @@ export async function doDoctor(
|
|
|
155
44
|
})
|
|
156
45
|
const roots = await resolveSessionRoots(signals, {
|
|
157
46
|
// subagent 根默认只 stat(§6.3 成本控制:只列路径与可扫性);includeSubagents:true
|
|
158
|
-
//
|
|
47
|
+
// 才扫。无缓存——每次实扫(原缓存机已删,ext-simplify-04 U3,§7B 要点 8 新态)。
|
|
159
48
|
subagents: params.includeSubagents === true ? 'scan' : 'stat',
|
|
160
|
-
cache: doctorRootCache,
|
|
161
49
|
})
|
|
162
|
-
|
|
163
|
-
return renderDoctor(roots, environment, leftovers)
|
|
50
|
+
return renderDoctor(roots, environment)
|
|
164
51
|
}
|
|
165
52
|
|
|
166
53
|
/**
|
|
@@ -170,24 +57,21 @@ export async function doDoctor(
|
|
|
170
57
|
export interface DoctorDetails {
|
|
171
58
|
environment: DetectedEnvironment
|
|
172
59
|
roots: SessionRoot[]
|
|
173
|
-
leftovers: PiLayoutLeftover[]
|
|
174
|
-
globBase: string
|
|
175
60
|
}
|
|
176
61
|
|
|
177
62
|
/**
|
|
178
63
|
* 渲染 doctor 输出(§5.1 形态):环境判定 + evidence + 会话根表(来源标签/路径/存在/
|
|
179
|
-
* 文件数/扫描耗时/去重注记)+ 事实型诊断 + legacy
|
|
64
|
+
* 文件数/扫描耗时/去重注记)+ 事实型诊断 + legacy 非空告警。
|
|
180
65
|
*/
|
|
181
66
|
function renderDoctor(
|
|
182
67
|
roots: SessionRoot[],
|
|
183
68
|
environment: DetectedEnvironment,
|
|
184
|
-
leftovers: { base: string; hits: PiLayoutLeftover[] },
|
|
185
69
|
): ToolResult {
|
|
186
70
|
const lines: string[] = []
|
|
187
71
|
|
|
188
72
|
// 环境判定行 + evidence(§6.2:判定可被证据推翻,非黑盒断言;evidence 恒非空)
|
|
189
73
|
lines.push(
|
|
190
|
-
`环境判定:${environment.kind === '
|
|
74
|
+
`环境判定:${environment.kind === 'taiji' ? 'taiji(托管)' : 'standalone-pi'}` +
|
|
191
75
|
` · 发行形态:${environment.distribution ?? '未知(不猜)'}`,
|
|
192
76
|
)
|
|
193
77
|
if (environment.dataDir !== undefined) lines.push(`数据目录:${environment.dataDir}`)
|
|
@@ -206,19 +90,18 @@ function renderDoctor(
|
|
|
206
90
|
}
|
|
207
91
|
const facts = [r.exists ? '存在' : '不存在']
|
|
208
92
|
if (r.fileCount === undefined) {
|
|
209
|
-
// 仅 subagents:'stat' 的 subagent 根(doctor
|
|
210
|
-
|
|
93
|
+
// 仅 subagents:'stat' 的 subagent 根(doctor 默认形态)——其余根恒有计数(实扫);
|
|
94
|
+
// 被去重根已在上方提前返回
|
|
211
95
|
facts.push('未扫描(subagent 根默认不扫,includeSubagents:true 开启)')
|
|
212
96
|
} else {
|
|
213
97
|
facts.push(`${r.fileCount} 文件`, `扫 ${Math.round(r.scanMs ?? 0)}ms`)
|
|
214
98
|
}
|
|
215
|
-
if (r.cached === true) facts.push('缓存命中')
|
|
216
99
|
facts.push(r.source === 'main' ? 'main' : 'subagent')
|
|
217
100
|
lines.push(` ${facts.join(' · ')}`)
|
|
218
101
|
})
|
|
219
102
|
|
|
220
103
|
// 事实型诊断(§7B 要点 5):列表内首个 main 根即最高优先级 main 根(去重只移除后位根,
|
|
221
|
-
//
|
|
104
|
+
// 首位根恒有计数——实扫)。0 文件也是事实——首条 assistant 前 jsonl 不落盘
|
|
222
105
|
//(PS-14),不做归因。
|
|
223
106
|
lines.push('')
|
|
224
107
|
const firstMain = roots.find((r) => r.source === 'main')
|
|
@@ -238,25 +121,11 @@ function renderDoctor(
|
|
|
238
121
|
)
|
|
239
122
|
}
|
|
240
123
|
|
|
241
|
-
// 旧布局残留(§6.11:命中才列,附同一迁移指引)
|
|
242
|
-
if (leftovers.hits.length > 0) {
|
|
243
|
-
lines.push('')
|
|
244
|
-
lines.push(`旧布局残留探测(基点 ${leftovers.base}):`)
|
|
245
|
-
for (const h of leftovers.hits) {
|
|
246
|
-
lines.push(
|
|
247
|
-
` - ${h.path}(${h.kind === 'backup' ? '迁移备份 pi.backup-v2-*' : '未迁移旧布局 pi/'})`,
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
lines.push(' 👉 关闭应用后运行 scripts/migrate-pi-layout-v2.mjs 完成迁移。')
|
|
251
|
-
}
|
|
252
|
-
|
|
253
124
|
return {
|
|
254
125
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
255
126
|
details: {
|
|
256
127
|
environment,
|
|
257
128
|
roots,
|
|
258
|
-
leftovers: leftovers.hits,
|
|
259
|
-
globBase: leftovers.base,
|
|
260
129
|
} satisfies DoctorDetails,
|
|
261
130
|
}
|
|
262
131
|
}
|
package/src/extract.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* 从 tool-handler.ts 机械提取(max-lines 拆分轮,零行为变更)。
|
|
5
5
|
* design §3.3 D3 的 5 个预设 + F8/F9 规格;F7(what 校验)与 doExtract 编排
|
|
6
6
|
*(resolveSessionId/safeParse/turns 范围限定依赖 tool-handler 私有定位层)留守
|
|
7
|
-
* tool-handler,经本模块导出的 5
|
|
8
|
-
* tool-handler re-export(单测 F9 白盒 import 路径不变)。纯提取,不调 LLM。
|
|
7
|
+
* tool-handler,经本模块导出的 5 预设函数分发。纯提取,不调 LLM。
|
|
9
8
|
*/
|
|
9
|
+
import { textBlockParts } from './core/render.js'
|
|
10
10
|
import { extractToolCalls, formatToolCallSummary, basename } from './core/toolcall.js'
|
|
11
11
|
import type { Turn } from './core/turns.js'
|
|
12
12
|
import { pad } from './handler-utils.js'
|
|
@@ -29,25 +29,12 @@ const SHORT_HASH_RE = /\b[0-9a-f]{7,8}\b/g
|
|
|
29
29
|
const COMMIT_CTX_RE = /feat:|fix:|refactor:|chore:|docs:|\b(commit|commits|merged|pushed|merge)\b/i
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* 从 message.content 提取纯 text(
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*(user-messages / tool-results 的正文),不要 thinking/toolCall 占位,本地实现。
|
|
36
|
-
* content 是 unknown 做类型守卫。
|
|
32
|
+
* 从 message.content 提取纯 text(E11 归一:共享核 textBlockParts 组合调用,'\n' join
|
|
33
|
+
* 语义留在本调用点)。extract 仅需纯 text(user-messages / tool-results 的正文),
|
|
34
|
+
* 不要 thinking/toolCall 占位。
|
|
37
35
|
*/
|
|
38
36
|
function extractContentText(content: unknown): string {
|
|
39
|
-
|
|
40
|
-
if (Array.isArray(content)) {
|
|
41
|
-
const parts: string[] = []
|
|
42
|
-
for (const b of content) {
|
|
43
|
-
if (b !== null && typeof b === 'object') {
|
|
44
|
-
const o = b as Record<string, unknown>
|
|
45
|
-
if (o.type === 'text' && typeof o.text === 'string') parts.push(o.text)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return parts.join('\n')
|
|
49
|
-
}
|
|
50
|
-
return ''
|
|
37
|
+
return textBlockParts(content).join('\n')
|
|
51
38
|
}
|
|
52
39
|
|
|
53
40
|
/** 截断到 max 字符,超出加省略号(防爆;tool-results 正文用)。 */
|
package/src/handler-utils.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tool-handler 共享小工具(max-lines
|
|
2
|
+
* tool-handler 共享小工具(max-lines 拆分轮机械提取;ext-simplify-04 E5 扩充,零行为变更)。
|
|
3
3
|
*
|
|
4
4
|
* 从 tool-handler.ts「小工具」与「turn/turns 索引解析」段搬出:pad/err/turn 索引解析
|
|
5
|
-
* 被留守的 tool-handler 与拆出的 search-across.ts / extract.ts
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
*
|
|
5
|
+
* 被留守的 tool-handler 与拆出的 search-across.ts / extract.ts 共同消费;
|
|
6
|
+
* err 原属此列,E5 起同包 helper 获取范式单一化——result-action.ts 不再经
|
|
7
|
+
* ResultActionDeps 注入 err/stripHash/requireStr/SESSION_ID_PREFIX_LEN,与本模块
|
|
8
|
+
* 其他消费者一样直接 import。formatDate/shortCwd 仅 tool-handler 留守部分使用,不在此列。
|
|
9
|
+
* requireStr 的 action 参数类型经 type import 取自 tool-handler.ts(编译期擦除,
|
|
10
|
+
* 无运行时循环——与域模块对本模块「仅 type import」同一先例)。
|
|
9
11
|
*/
|
|
12
|
+
import type { SessionReadAction } from './tool-handler.js'
|
|
10
13
|
|
|
11
14
|
/** turn 索引显示宽度(T013 三位补零)。 */
|
|
12
15
|
const TURN_INDEX_WIDTH = 3
|
|
13
16
|
|
|
17
|
+
/** sessionId 列表行内的短显前缀长度。 */
|
|
18
|
+
export const SESSION_ID_PREFIX_LEN = 8
|
|
19
|
+
|
|
14
20
|
export const pad = (n: number): string => String(n).padStart(TURN_INDEX_WIDTH, '0')
|
|
15
21
|
|
|
16
22
|
/** 构造带 👉 恢复指引的 Error。契约:handler 直接 throw,execute 不 catch、原样传播给 pi;
|
|
@@ -20,6 +26,23 @@ export function err(message: string): Error {
|
|
|
20
26
|
return new Error(message)
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
/** 剥 # 前缀(TUI `#e6c96` 引用 → 纯片段,design §3.3 D-3/D-4)。 */
|
|
30
|
+
export function stripHash(s: string): string {
|
|
31
|
+
return s.replace(/^#+/, '')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** F5 必填参数校验。 */
|
|
35
|
+
export function requireStr(
|
|
36
|
+
val: string | undefined,
|
|
37
|
+
name: string,
|
|
38
|
+
action: SessionReadAction,
|
|
39
|
+
): string {
|
|
40
|
+
if (val === undefined || val === null || val.trim() === '') {
|
|
41
|
+
throw err(`action:"${action}" 需要参数 "${name}"。👉 补上 "${name}" 重试。`)
|
|
42
|
+
}
|
|
43
|
+
return val.trim()
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
// ---------------------------------------------------------------------------
|
|
24
47
|
// turn / turns 索引解析
|
|
25
48
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -157,7 +157,7 @@ const guidelines = [
|
|
|
157
157
|
"workflow action to see workflow run overviews (status/budget/steps). Each step's call sessionId can jump to outline/detail for deep reading.",
|
|
158
158
|
"result action to fetch a subagent's final result text (same content as its completion notice): session takes a single sa-id/uuid/path or a comma-separated batch of at most 10; optional limit caps chars per item (default 8000, truncated items carry a pointer to the full file).",
|
|
159
159
|
'Errors carry a 👉 recovery hint—follow it to retry in one step.',
|
|
160
|
-
'Unsure about the host environment (pure pi vs
|
|
160
|
+
'Unsure about the host environment (pure pi vs taiji) or where session roots live? Run action:"doctor" first—it prints the detected environment and every candidate session root.',
|
|
161
161
|
]
|
|
162
162
|
|
|
163
163
|
// ---- 工具 description(design §3.4,照搬措辞)----
|
|
@@ -231,7 +231,7 @@ export default function sessionReaderExtension(pi: ExtensionAPI): void {
|
|
|
231
231
|
|
|
232
232
|
// ── M4 TUI 层(design §1 + §3.3 D-3/D-4 + 附录 P-hash-trigger)──────────
|
|
233
233
|
// # 引用补全 provider + /session-pick 命令(命令名避开 pi 内置 /session 冲突)。仅 ctx.mode === 'tui' 注册:RPC 模式
|
|
234
|
-
// (
|
|
234
|
+
// (taiji 子进程)不用 pi TUI editor / slash 命令,加载即跳过。
|
|
235
235
|
//
|
|
236
236
|
// addAutocompleteProvider 挂在 ctx.ui(非 ExtensionAPI),setup 入口无 ctx,
|
|
237
237
|
// 只能在 event handler 里拿——session_start 是最早且每 session 触发的 event。
|
package/src/no-match.ts
CHANGED
|
@@ -136,8 +136,9 @@ function guidanceLines(): string[] {
|
|
|
136
136
|
*
|
|
137
137
|
* ① 事实型自检行:main 根 N 文件 / subagent 根 M 文件 /「候选集非空|为空」/「查询已做
|
|
138
138
|
* uuid 归一化匹配(小写+去连字符)后仍无命中」。计数取 roots——调用方在 F1 路径
|
|
139
|
-
* 恒以无 options 的 resolveSessionRoots
|
|
140
|
-
*
|
|
139
|
+
* 恒以无 options 的 resolveSessionRoots 实扫(根扫描无缓存——doctor 缓存机已删除,
|
|
140
|
+
* ext-simplify-04 U3,§7B 要点 8 新态;原 PS-14「缓存计数误报主根为空」的防污染
|
|
141
|
+
* 规则随之失效),即本次实扫结果。
|
|
141
142
|
* 只陈述事实,**不得**出现「真的没有这个 session」类归因断言(§3.3 教训:大写/去连
|
|
142
143
|
* 字符输入在归一化前会被错误归因为「不存在」)。
|
|
143
144
|
* ② 编辑距离最近候选 top-3(标注 source 与差异位),候选 = roots 实扫文件名提取的
|