@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.
@@ -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, SubagentRef } from '../core/family.js'
5
+ import type { Family, SessionRef } from '../core/family.js'
6
6
  import { buildFamilyIndex, resolveFamily } from '../core/family.js'
7
- import { listMainSessions, listSubagentSessions, resolveSessionRoots, type SessionFileMeta } from './roots.js'
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. 补 M1 占位字段(fileName / subagent cwd)+ workflows ----
60
- enrichRefs(family, scan.pathToRef)
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 实扫集合一致(listMainSessions = agentDir 信号包下未去重的 main 根)。
75
+ * collectMainSessions 实扫集合一致(agentDir 信号包下未去重的 main 根)。
75
76
  */
76
77
  async function formatSessionNotFound(sessionId: string, agentDir: string): Promise<string> {
77
- const mainRoots = (await resolveSessionRoots({ agentDir })).filter(
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 找目标文件 + enrich 补 fileName */
103
+ /** sessionId → 真实文件路径,供 workflows 找目标文件 */
101
104
  sessionIdToPath: Map<string, string>
102
- /** path → 完整 SessionRef(含真实 cwd/fileName/mtime/size),供 enrich + workflow calls 反查 */
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
- const mainMetas = await listMainSessions(agentDir)
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 = parseHeaderLine(await readFirstLine(meta.path))
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
- const subMetas = await listSubagentSessions(agentDir)
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 = parseHeaderLine(await readFirstLine(meta.path))
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
- * doctor 扫描缓存 + 旧布局残留 glob 探测。SessionReadSignals(u3 信号包超集)随域迁移,
6
- * tool-handler re-export 保持导出面不变(index.ts / 单测白盒 import 路径不变)。
7
- * statDirMtimeOrNull / DOCTOR_CACHE_TTL_MS 供留守的 u11 metadata 缓存复用(一并导出)。
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
- * 旧布局残留独立 glob 探测(§6.11 v9.1)。基点 = dirname(agentDir)(xyz-agent 下 =
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
- // 才扫(走同一缓存句柄)。find/F1 路径恒不传 options——不读缓存(§7B 要点 8)。
47
+ // 才扫。无缓存——每次实扫(原缓存机已删,ext-simplify-04 U3,§7B 要点 8 新态)。
159
48
  subagents: params.includeSubagents === true ? 'scan' : 'stat',
160
- cache: doctorRootCache,
161
49
  })
162
- const leftovers = await detectPiLayoutLeftovers(signals.agentDir)
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 === 'xyz-agent' ? 'xyz-agent(托管)' : 'standalone-pi'}` +
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
- // 首位根恒有计数——实扫或缓存命中)。0 文件也是事实——首条 assistant 前 jsonl 不落盘
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 预设函数分发。renderExtractItems 导出面不变:
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(string 直取;数组拼接 text 块)。
33
- *
34
- * 与 render.ts 内部 extractText 同语义,但那未导出;extract 仅需纯 text
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
- if (typeof content === 'string') return content
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 正文用)。 */
@@ -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
- * 模块保持单向依赖(handler-utils ← 各域模块 ← tool-handler),不引入循环 import
7
- *(result-action.ts 的 Deps 注入先例解决不了纯函数跨模块复用,低层模块更直接)。
8
- * stripHash/formatDate/shortCwd/requireStr 仅 tool-handler 留守部分使用,不在此列。
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 xyz-agent) or where session roots live? Run action:"doctor" first—it prints the detected environment and every candidate session root.',
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
- // (xyz-agent 子进程)不用 pi TUI editor / slash 命令,加载即跳过。
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 实扫(find/doctor 缓存句柄不注入,§7B 要点 8:
140
- * 缓存计数会把 PS-14「首条 assistant 前 0 文件」误报成「主根为空」),即本次实扫结果。
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 实扫文件名提取的