@zhushanwen/pi-session-reader 0.6.0 → 0.7.0
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 +100 -0
- package/package.json +7 -4
- package/src/__tests__/cross-package-subagent-core.test.ts +36 -44
- package/src/__tests__/entry-anchor.test.ts +226 -0
- package/src/__tests__/env.test.ts +54 -54
- package/src/__tests__/execution-tree.test.ts +6 -0
- package/src/__tests__/family-zcode.test.ts +255 -0
- package/src/__tests__/family.test.ts +1 -1
- package/src/__tests__/find.test.ts +24 -1
- package/src/__tests__/index.test.ts +1 -1
- package/src/__tests__/parser.test.ts +1 -2
- package/src/__tests__/real-data.ts +22 -9
- package/src/__tests__/render.test.ts +1 -2
- package/src/__tests__/result.test.ts +2 -2
- package/src/__tests__/roots.test.ts +1 -1
- package/src/__tests__/subagents.test.ts +1 -4
- package/src/__tests__/tool-handler.test.ts +76 -85
- package/src/__tests__/toolcall.test.ts +1 -1
- package/src/__tests__/tree.test.ts +1 -1
- package/src/__tests__/turns.test.ts +1 -2
- package/src/__tests__/zcode-manifest.test.ts +361 -0
- package/src/__tests__/zcode-routing.test.ts +751 -0
- package/src/__tests__/zcode-whitelist.test.ts +119 -0
- package/src/core/execution-tree.ts +3 -1
- package/src/core/family.ts +80 -1
- package/src/core/render.ts +2 -3
- package/src/core/toolcall.ts +3 -4
- package/src/core/tree.ts +1 -1
- package/src/core/turns.ts +1 -1
- package/src/core/units.ts +14 -0
- package/src/discovery/entry-anchor.ts +108 -0
- package/src/discovery/env.ts +41 -41
- package/src/discovery/find.ts +1 -1
- package/src/discovery/roots.ts +2 -2
- package/src/discovery/session-header.ts +30 -69
- package/src/discovery/subagents.ts +9 -2
- package/src/discovery/whitelist.ts +87 -0
- package/src/discovery/workflows.ts +1 -1
- package/src/discovery/zcode-manifest.ts +277 -0
- package/src/doctor.ts +5 -70
- package/src/index.ts +9 -2
- package/src/result-action.ts +15 -6
- package/src/search-across.ts +2 -5
- package/src/tool-format.ts +455 -0
- package/src/tool-handler.ts +321 -325
- package/src/zcode-anchor-classify.ts +60 -0
- package/vitest.config.ts +2 -4
- package/src/core/parser.ts +0 -167
package/src/tool-handler.ts
CHANGED
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
* 分层约定(同 scheduler/cw-tool):本文件零 pi 依赖——agentDir 作参数注入,
|
|
5
5
|
* 不调用 getAgentDir(),可完全单测;pi 注册与 getAgentDir() 调用在 index.ts。
|
|
6
6
|
*
|
|
7
|
-
* 按 action 分发到 11
|
|
7
|
+
* 按 action 分发到 11 条路径,串联基座解析(session-core parse)+ M1 core(tree/turns/render)+ M2 discovery
|
|
8
8
|
*(find/subagents)+ doctor 的环境判定与根表渲染(u8,discovery/env)。content 给 LLM
|
|
9
9
|
* 读(人类可读摘要),details 供程序化消费/测试断言。
|
|
10
10
|
*
|
|
11
11
|
* 域模块拆分(max-lines 拆分轮机械提取,零行为变更,result-action.ts 先例同型):
|
|
12
12
|
* result-action.ts(result)/ doctor.ts(doctor + SessionReadSignals)/
|
|
13
13
|
* search-across.ts(search 管线 + u12 跨会话)/ extract.ts(extract 预设)/
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* tool-format.ts(各 action 输出文本渲染 + 错误面 message + F2 消歧与
|
|
15
|
+
* find 零匹配包装)/ no-match.ts(F1 自检行)/ zcode-anchor-classify.ts(entry
|
|
16
|
+
* 兜底归因的 entry 级纯判定)/ handler-utils.ts(pad/err/
|
|
17
|
+
* stripHash/requireStr/SESSION_ID_PREFIX_LEN/turn 索引解析低层小工具)。
|
|
18
|
+
* 本模块保留公共类型、定位解析(resolveSessionId)、zcode 读链与各 action 编排;
|
|
17
19
|
* 域模块符号不经此 re-export——从所属域模块直接 import(唯一例外 SessionReadSignals:
|
|
18
20
|
* 本模块 re-export 供 index.ts 生产消费)。
|
|
19
21
|
*
|
|
@@ -23,38 +25,43 @@
|
|
|
23
25
|
* 例外:F2 多匹配与 F1 find 零匹配「不视为错误」,返回消歧/提示结果而非抛错。
|
|
24
26
|
*/
|
|
25
27
|
import { existsSync } from 'node:fs'
|
|
26
|
-
import { mkdir, stat, writeFile } from 'node:fs/promises'
|
|
28
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
27
29
|
import { join, isAbsolute } from 'node:path'
|
|
28
30
|
import { homedir } from 'node:os'
|
|
29
31
|
import { toErrorMessage } from '@zhushanwen/pi-ext-guards'
|
|
32
|
+
import { getLogger } from '@zhushanwen/pi-extension-logger'
|
|
33
|
+
import { convertZcodeTranscript, openZcodeSessionDb } from '@zhushanwen/zcode-session-source'
|
|
30
34
|
import {
|
|
35
|
+
buildSessionFileIndex,
|
|
31
36
|
findSessions,
|
|
32
37
|
type MatchedSession,
|
|
33
38
|
type SessionMetadataEntry,
|
|
34
39
|
type SessionMetadataProvider,
|
|
35
40
|
} from './discovery/find.js'
|
|
36
|
-
import { resolveSessionRoots
|
|
41
|
+
import { resolveSessionRoots } from './discovery/roots.js'
|
|
37
42
|
import { readSessionHeaderIdSync } from './discovery/session-header.js'
|
|
43
|
+
import { findZcodeEntryAnchor, type ZcodeAnchor } from './discovery/entry-anchor.js'
|
|
44
|
+
import {
|
|
45
|
+
listZcodeManifests,
|
|
46
|
+
readZcodeManifest,
|
|
47
|
+
type ZcodeAnchorMissingReason,
|
|
48
|
+
} from './discovery/zcode-manifest.js'
|
|
49
|
+
import { assertZcodeDbPathAllowed } from './discovery/whitelist.js'
|
|
38
50
|
import {
|
|
39
51
|
buildFamilyFromFs,
|
|
40
52
|
listRecordManifests,
|
|
41
53
|
type RecordManifest,
|
|
42
54
|
} from './discovery/subagents.js'
|
|
43
55
|
import { readRunSnapshot, resolveWorkflows } from './discovery/workflows.js'
|
|
44
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
parseSessionContent,
|
|
58
|
+
parseSessionFile,
|
|
59
|
+
type ParseResult,
|
|
60
|
+
} from '@zhushanwen/session-core'
|
|
45
61
|
import { parseRunSnapshot, renderWorkflowOverview, type WorkflowOverview } from './core/workflow.js'
|
|
46
62
|
import { buildTreeView } from './core/tree.js'
|
|
47
63
|
import { segmentTurns } from './core/turns.js'
|
|
48
|
-
import {
|
|
49
|
-
renderOutline,
|
|
50
|
-
renderExpand,
|
|
51
|
-
renderDetail,
|
|
52
|
-
formatBytesMarker,
|
|
53
|
-
type OutlineOptions,
|
|
54
|
-
type OutlineResult,
|
|
55
|
-
type EntryBrief,
|
|
56
|
-
type ToolResultSummaryEntry,
|
|
57
|
-
} from './core/render.js'
|
|
64
|
+
import { renderOutline, renderExpand, renderDetail, type OutlineOptions } from './core/render.js'
|
|
58
65
|
import type { Family, SessionRef, WorkflowRef } from './core/family.js'
|
|
59
66
|
import { doResult } from './result-action.js'
|
|
60
67
|
|
|
@@ -95,11 +102,41 @@ import {
|
|
|
95
102
|
extractUserMessages,
|
|
96
103
|
type ExtractWhat,
|
|
97
104
|
} from './extract.js'
|
|
105
|
+
// 同轮拆分的渲染域模块(format* 纯函数簇,含 zcode 错误面 message 与
|
|
106
|
+
// zcodeReadErrorMessage 错误映射):运行时依赖方向本模块 → tool-format,
|
|
107
|
+
// tool-format 对本模块仅 type import(ToolResult),无循环。
|
|
108
|
+
import {
|
|
109
|
+
disambiguate,
|
|
110
|
+
entryReadableText,
|
|
111
|
+
findNoMatch,
|
|
112
|
+
formatDetailText,
|
|
113
|
+
formatExpandText,
|
|
114
|
+
formatFamilyText,
|
|
115
|
+
formatFindContent,
|
|
116
|
+
formatOutlineTail,
|
|
117
|
+
formatSaIdAmbiguous,
|
|
118
|
+
formatSaIdNotFound,
|
|
119
|
+
formatSessionGc,
|
|
120
|
+
formatZcodeAnchorMissing,
|
|
121
|
+
formatZcodeParamInvalid,
|
|
122
|
+
formatZcodeRecordNotFound,
|
|
123
|
+
formatZcodeSessionNotFound,
|
|
124
|
+
isToolResultSummary,
|
|
125
|
+
zcodeReadErrorMessage,
|
|
126
|
+
} from './tool-format.js'
|
|
98
127
|
import { doDoctor, type SessionReadSignals } from './doctor.js'
|
|
128
|
+
// entry 兜底归因的 entry 级判定(同目录域模块,零 I/O 纯函数,复杂度偿还提取)。
|
|
129
|
+
import { firstIncompleteAnchorReason } from './zcode-anchor-classify.js'
|
|
99
130
|
|
|
100
131
|
// SessionReadSignals re-export 是生产链(index.ts 工具注册消费),非测试兼容转发。
|
|
101
132
|
export type { SessionReadSignals }
|
|
102
133
|
|
|
134
|
+
// zcode 读链的结构化日志(「事后排查」通道:degradations 留痕 / L2-L3 恢复成功 /
|
|
135
|
+
// anchor-missing 归因——§3.4 可观测性 + P2 降级隔离契约 F13,均不进 LLM 可见面)。
|
|
136
|
+
// pi handle 由 index.ts 初始化时 setPiHandle 注入(appendEntry 通道);未注入时
|
|
137
|
+
// warn/error 降级文件日志,双开关缺省 no-op(纯 pi 独立用户零行为影响)。
|
|
138
|
+
const logger = getLogger('session-reader')
|
|
139
|
+
|
|
103
140
|
// ---------------------------------------------------------------------------
|
|
104
141
|
// 公共类型(与 index.ts 的 TypeBox schema 对齐)
|
|
105
142
|
// ---------------------------------------------------------------------------
|
|
@@ -155,40 +192,32 @@ export interface ToolResult {
|
|
|
155
192
|
details: unknown
|
|
156
193
|
}
|
|
157
194
|
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
// 小工具
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
|
|
162
|
-
/** formatDate 日期段(月/日)补零宽度。 */
|
|
163
|
-
const DATE_FIELD_WIDTH = 2
|
|
164
|
-
|
|
165
|
-
function formatDate(ms: number): string {
|
|
166
|
-
if (!ms) return ''
|
|
167
|
-
const d = new Date(ms)
|
|
168
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(DATE_FIELD_WIDTH, '0')}-${String(
|
|
169
|
-
d.getDate(),
|
|
170
|
-
).padStart(DATE_FIELD_WIDTH, '0')}`
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** shortCwd 保留的目录末段数。 */
|
|
174
|
-
const SHORT_CWD_SEGMENTS = 2
|
|
175
|
-
|
|
176
|
-
/** cwd 取末两段缩短显示(完整 cwd 在 details 里)。 */
|
|
177
|
-
function shortCwd(cwd: string): string {
|
|
178
|
-
const parts = cwd.split('/').filter(Boolean)
|
|
179
|
-
return parts.slice(-SHORT_CWD_SEGMENTS).join('/')
|
|
180
|
-
}
|
|
181
|
-
|
|
182
195
|
// ---------------------------------------------------------------------------
|
|
183
196
|
// resolveSessionId:片段 → 完整 id(design §3.4 resolveSessionId 辅助)
|
|
184
197
|
// ---------------------------------------------------------------------------
|
|
185
198
|
|
|
186
199
|
export type ResolveResult =
|
|
187
|
-
| {
|
|
200
|
+
| {
|
|
201
|
+
kind: 'ok'
|
|
202
|
+
sessionId: string
|
|
203
|
+
/**
|
|
204
|
+
* 内容源路径。pi = session .jsonl 绝对路径;zcode 路由命中(非 family)= 会话库
|
|
205
|
+
* .sqlite 绝对路径(loadParsed 按 zcodeAnchor 分流,safeParse 不会以此路径开
|
|
206
|
+
* JSONL);zcode family 路由 = rootSessionId 反查的 .jsonl 路径(反查未命中为
|
|
207
|
+
* 空串——仅作树构建 sessionFile 用途,buildExecutionTree 侧归一为 undefined)。
|
|
208
|
+
*/
|
|
209
|
+
fileName: string
|
|
210
|
+
/**
|
|
211
|
+
* zcode 锚(U9 路由命中时在场):内容加载走 zcode 读链(白名单闸 →
|
|
212
|
+
* openZcodeSessionDb → convertZcodeTranscript),pi 路径恒 undefined——
|
|
213
|
+
* 可选字段不改变既有消费方的 pi 行为(toEqual 语义下 undefined 键不可见)。
|
|
214
|
+
*/
|
|
215
|
+
zcodeAnchor?: ZcodeAnchor
|
|
216
|
+
}
|
|
188
217
|
| { kind: 'multi'; query: string; candidates: MatchedSession[] }
|
|
189
218
|
|
|
190
219
|
// readSessionHeaderIdSync(同步读首行 header 取 id,resolveSessionId 形态①/②消费)
|
|
191
|
-
// 在 discovery/session-header.ts
|
|
220
|
+
// 在 discovery/session-header.ts(基座 readFirstJsonlLineSync 的谓词/降级薄包装,G2 单源)。
|
|
192
221
|
|
|
193
222
|
/** ~ 前缀(home 目录简写),与 expandHome 配套避免 magic number。 */
|
|
194
223
|
const HOME_TILDE_PREFIX = '~/'
|
|
@@ -231,15 +260,161 @@ async function resolveSessionId(
|
|
|
231
260
|
return resolveBySessionPath(session)
|
|
232
261
|
}
|
|
233
262
|
|
|
234
|
-
// ② sa-id 前缀 →
|
|
263
|
+
// ② sa-id 前缀 → zcode 路由(§3.5 定位链,U9)前置,pi 现路径零改动地在其中承接
|
|
235
264
|
if (session.startsWith('sa-')) {
|
|
236
|
-
return
|
|
265
|
+
return resolveSaIdRoute(session, agentDir, prefetchedManifests, action, liveSessionDir)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// §3.4 第 0 段:sess_ 形态 id 无第一梯队直读入口(agent 从不持有 sess id,F14;
|
|
269
|
+
// 且无 zcode 候选发现,D6)→ zcode_param_invalid。pi 的 session id 是 uuid(无
|
|
270
|
+
// sess_ 前缀),此检测只改变错误输入的错误面文案。
|
|
271
|
+
if (session.startsWith('sess_')) {
|
|
272
|
+
throw err(formatZcodeParamInvalid(session))
|
|
237
273
|
}
|
|
238
274
|
|
|
239
275
|
// ③ 其余:findSessions 透传 source/liveSessionDir 沿用 F1/F2
|
|
240
276
|
return resolveByFragment(session, agentDir, source, liveSessionDir)
|
|
241
277
|
}
|
|
242
278
|
|
|
279
|
+
/**
|
|
280
|
+
* sa- 形态的路由前置(U9,design §3.5 三层定位链;**不加任何工具参数**,D4——
|
|
281
|
+
* 判别源 = manifest 自带 engine 字段):
|
|
282
|
+
*
|
|
283
|
+
* ① zcode manifest 直读(`readZcodeManifest`,文件名由 sa-id 确定性推出,窄 walk
|
|
284
|
+
* 命中即止):`{kind:'zcode'}` → zcode 读链;`{kind:'anchor-missing'}` →
|
|
285
|
+
* `zcode_anchor_missing`。
|
|
286
|
+
* ② `{kind:'not-zcode'}` → **pi 现路径零改动承接**:pi manifest 有命中即走既有
|
|
287
|
+
* `resolveByRecordId`(engine 缺省/'pi' 形态,行为与今天逐字节一致)。
|
|
288
|
+
* ③ pi 无命中(今天此处直接 ES2)→ entry 兜底(§3.5 第②层,仅 liveSessionDir 内,
|
|
289
|
+
* 越界显式失败):命中 → zcode 读链;未命中 → `zcode_record_not_found` + 👉
|
|
290
|
+
* (§3.4:sa-id 语境的统一错误面——F14 agent 唯一持有的 id 就是 sa-id,指引动作
|
|
291
|
+
* 覆盖除 find 指针外的全部恢复路径(find 按 §3.4 刻意不保留)。
|
|
292
|
+
*/
|
|
293
|
+
async function resolveSaIdRoute(
|
|
294
|
+
session: string,
|
|
295
|
+
agentDir: string,
|
|
296
|
+
prefetchedManifests: RecordManifest[] | undefined,
|
|
297
|
+
action: SessionReadAction,
|
|
298
|
+
liveSessionDir: string | undefined,
|
|
299
|
+
): Promise<ResolveResult> {
|
|
300
|
+
const zm = await readZcodeManifest(agentDir, session)
|
|
301
|
+
if (zm.kind === 'anchor-missing') {
|
|
302
|
+
// 归因进结构化日志不进 LLM 可见面(§3.4):reason 逐键归因(missing-engineHandle /
|
|
303
|
+
// missing-sessionRef / missing-sessionId / missing-dbPath,见 zcode-manifest.ts)
|
|
304
|
+
logger.warn('zcode manifest anchor-missing', { saId: session, reason: zm.reason })
|
|
305
|
+
throw err(formatZcodeAnchorMissing(session))
|
|
306
|
+
}
|
|
307
|
+
if (zm.kind === 'zcode') {
|
|
308
|
+
return resolveZcodeRoute(session, zm.anchor, agentDir, action, liveSessionDir)
|
|
309
|
+
}
|
|
310
|
+
// not-zcode:pi manifest 命中 → 现路径(同一 manifests 传入,不二次扫描)
|
|
311
|
+
const manifests = prefetchedManifests ?? (await listRecordManifests(agentDir))
|
|
312
|
+
if (manifests.some((m) => m.id === session)) {
|
|
313
|
+
return resolveByRecordId(session, agentDir, manifests)
|
|
314
|
+
}
|
|
315
|
+
// entry 兜底(§3.5 第②层):候选 = liveSessionDir 内主 session 文件(第一梯队
|
|
316
|
+
// 只在 liveSessionDir 内做,根内全扫不做——越界显式失败)
|
|
317
|
+
const candidates = await liveSessionCandidateFiles(agentDir, liveSessionDir)
|
|
318
|
+
const anchor = await findZcodeEntryAnchor(candidates, session)
|
|
319
|
+
if (anchor === undefined) {
|
|
320
|
+
// 兜底失败的两类归因(§3.4 第 0 段,场景 5 ②):候选里有该 sa-id 的 record entry
|
|
321
|
+
// 但锚不完整 → zcode_anchor_missing(缺失键名进结构化日志——engineHandle 整体
|
|
322
|
+
// 缺席/缺 sessionId/缺 dbPath 归因不同);完全无该 sa-id → zcode_record_not_found。
|
|
323
|
+
// findZcodeEntryAnchor 契约是「锚或 not-found」不分归因,分类只在罕见失败路径补
|
|
324
|
+
// 一次形态判定。
|
|
325
|
+
const incomplete = await classifyIncompleteEntryAnchor(candidates, session)
|
|
326
|
+
if (incomplete !== undefined) {
|
|
327
|
+
logger.warn('zcode entry anchor incomplete', { saId: session, reason: incomplete })
|
|
328
|
+
throw err(formatZcodeAnchorMissing(session))
|
|
329
|
+
}
|
|
330
|
+
throw err(formatZcodeRecordNotFound())
|
|
331
|
+
}
|
|
332
|
+
return resolveZcodeRoute(session, anchor, agentDir, action, liveSessionDir)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 候选文件里该 sa-id 的 `subagent-record` entry 若「在场但锚不完整」,返回缺失归因
|
|
337
|
+
*(§3.4 zcode_anchor_missing 的 entry 形态触发 + 日志归因;记录完全不在场返回
|
|
338
|
+
* undefined)。entry 级判定(五关过滤 + D5 engine 判别 + 锚完整性链)在
|
|
339
|
+
* zcode-anchor-classify.ts 的 firstIncompleteAnchorReason,本函数只做文件扫描 I/O
|
|
340
|
+
*(读失败跳过该文件,首个命中归因即终止扫描)。
|
|
341
|
+
*/
|
|
342
|
+
async function classifyIncompleteEntryAnchor(
|
|
343
|
+
candidateFiles: readonly string[],
|
|
344
|
+
saId: string,
|
|
345
|
+
): Promise<ZcodeAnchorMissingReason | undefined> {
|
|
346
|
+
for (const file of candidateFiles) {
|
|
347
|
+
let content: string
|
|
348
|
+
try {
|
|
349
|
+
content = await readFile(file, 'utf8')
|
|
350
|
+
} catch {
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
const reason = firstIncompleteAnchorReason(parseSessionContent(content).entries, saId)
|
|
354
|
+
if (reason !== undefined) return reason
|
|
355
|
+
}
|
|
356
|
+
return undefined
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** liveSessionDir 内的主 session 文件(entry 兜底候选;无 live 信号 → 空集=越界失败)。 */
|
|
360
|
+
async function liveSessionCandidateFiles(
|
|
361
|
+
agentDir: string,
|
|
362
|
+
liveSessionDir: string | undefined,
|
|
363
|
+
): Promise<string[]> {
|
|
364
|
+
if (liveSessionDir === undefined || liveSessionDir === '') return []
|
|
365
|
+
const roots = await resolveSessionRoots({ agentDir, liveSessionDir })
|
|
366
|
+
return roots.filter((r) => r.kind === 'live').flatMap((r) => r.files.map((f) => f.path))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* zcode 锚 → ResolveResult(路由命中后的 action 分叉):
|
|
371
|
+
* - family:以**发起 session(rootSessionId)**为家族视图入口——zcode 会话无 JSONL
|
|
372
|
+
* 文件,buildFamilyFromFs 的 byId 索引查不到锚 sessionId;zcode 节点按 rootSessionId
|
|
373
|
+
* 挂载(D5-1),rootSessionId 视图即「该 subagent 的后代与关联」(§3.4 指引语义)。
|
|
374
|
+
* rootSessionId 取自 `listZcodeManifests` 枚举(readZcodeManifest 直读信号只携带锚,
|
|
375
|
+
* 不携带 rootSessionId——family 是低频 action,枚举一次可接受)。fileName 按完整
|
|
376
|
+
* rootSessionId 反查其 .jsonl 路径回填(MF-1 链路:recursive 树 main root 填
|
|
377
|
+
* sessionFile 后才能读到该 session 自身发起的 workflow run);反查未命中(文件已
|
|
378
|
+
* GC)回退空串(buildExecutionTree 侧归一为 undefined,树退化为无 sessionFile
|
|
379
|
+
* root,不伪造路径)。
|
|
380
|
+
* - 其余 action:sessionId = 锚会话 id、fileName = 锚库路径(内容源),zcodeAnchor
|
|
381
|
+
* 在场使 loadParsed 分流到 zcode 读链。
|
|
382
|
+
*/
|
|
383
|
+
async function resolveZcodeRoute(
|
|
384
|
+
saId: string,
|
|
385
|
+
anchor: ZcodeAnchor,
|
|
386
|
+
agentDir: string,
|
|
387
|
+
action: SessionReadAction,
|
|
388
|
+
liveSessionDir: string | undefined,
|
|
389
|
+
): Promise<ResolveResult> {
|
|
390
|
+
if (action === 'family') {
|
|
391
|
+
const zcodeNodes = await listZcodeManifests(agentDir)
|
|
392
|
+
const rootSessionId = zcodeNodes.find((n) => n.id === saId)?.rootSessionId
|
|
393
|
+
// 枚举未含该 id(直读命中与枚举之间 manifest 被迁移的极端窗口)→ 退回锚 sessionId,
|
|
394
|
+
// 后续 byId 查不到走既有 not-found 错误面(不静默伪造家族)
|
|
395
|
+
if (rootSessionId === undefined) {
|
|
396
|
+
return { kind: 'ok', sessionId: anchor.sessionId, fileName: '' }
|
|
397
|
+
}
|
|
398
|
+
const rootFile = await findSessionFileById(rootSessionId, agentDir, liveSessionDir)
|
|
399
|
+
return { kind: 'ok', sessionId: rootSessionId, fileName: rootFile ?? '' }
|
|
400
|
+
}
|
|
401
|
+
return { kind: 'ok', sessionId: anchor.sessionId, fileName: anchor.dbPath, zcodeAnchor: anchor }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 按完整 session id 反查其 .jsonl 路径(zcode family 路由回填 fileName 用)。
|
|
406
|
+
* 单次根扫描建 id→path 索引(u12 既有管线,同 id 多根取 mtime 新者);未命中
|
|
407
|
+
*(文件已 GC / 根外)→ undefined,调用方自行降级。
|
|
408
|
+
*/
|
|
409
|
+
async function findSessionFileById(
|
|
410
|
+
sessionId: string,
|
|
411
|
+
agentDir: string,
|
|
412
|
+
liveSessionDir: string | undefined,
|
|
413
|
+
): Promise<string | undefined> {
|
|
414
|
+
const index = await buildSessionFileIndex({ agentDir, liveSessionDir })
|
|
415
|
+
return index.get(sessionId)?.path
|
|
416
|
+
}
|
|
417
|
+
|
|
243
418
|
/** 形态①:绝对路径 / ~ 前缀 → 展开后读首行 header,sessionId=header 真实 id(文件名仅定位)。 */
|
|
244
419
|
function resolveBySessionPath(session: string): ResolveResult {
|
|
245
420
|
const expanded = expandHome(session)
|
|
@@ -319,278 +494,91 @@ async function resolveByFragment(
|
|
|
319
494
|
return { kind: 'multi', query: session, candidates: matches }
|
|
320
495
|
}
|
|
321
496
|
|
|
322
|
-
/** ES1(SESSION_FILE_GC):sa-id 恰 1 命中但 sessionFile 不存在(GC/未写入)。含 manifest 元数据 + 👉。 */
|
|
323
|
-
function formatSessionGc(record: RecordManifest): string {
|
|
324
|
-
return (
|
|
325
|
-
`subagent "${record.id}" 的 session 文件不存在(可能已被 GC 或未写入):\n` +
|
|
326
|
-
` rootSessionId: ${record.rootSessionId}\n` +
|
|
327
|
-
` agentName: ${record.agentName ?? '(未记录)'}\n` +
|
|
328
|
-
` sessionFile: ${record.sessionFile}\n` +
|
|
329
|
-
`👉 改用 session_read { action:"family" } 查该 subagent 的后代,或换一个 completed subagent 重试。`
|
|
330
|
-
)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/** ES2(SA_ID_NO_MATCH):sa-id 无精确匹配(可能仍在运行 / 片段输入)。 */
|
|
334
|
-
function formatSaIdNotFound(saId: string): string {
|
|
335
|
-
return (
|
|
336
|
-
`subagent "${saId}" 无匹配 record(若刚启动,record 可能尚未落盘)。` +
|
|
337
|
-
`\n👉 用 session_read { action:"family" } 查活跃/已完成的 subagent;` +
|
|
338
|
-
`若是片段输入,请用完整 sa- id 或 action:"find" 重试。`
|
|
339
|
-
)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** ES2(SA_ID_AMBIGUOUS):sa-id 多 manifest 命中(数据异常,record.id 应唯一)。 */
|
|
343
|
-
function formatSaIdAmbiguous(saId: string, records: RecordManifest[]): string {
|
|
344
|
-
return (
|
|
345
|
-
`subagent "${saId}" 匹配 ${records.length} 个 record(数据异常,record.id 应唯一):\n` +
|
|
346
|
-
records
|
|
347
|
-
.map((r) => ` ${r.id} (root=${r.rootSessionId} file=${r.sessionFile})`)
|
|
348
|
-
.join('\n') +
|
|
349
|
-
`\n👉 用 session_read { action:"family" } 或完整 session uuid 重试。`
|
|
350
|
-
)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** 消歧提示的 uuid 片段长度(比短显略长,引导输入更长片段消歧)。 */
|
|
354
|
-
const HINT_ID_PREFIX_LEN = 12
|
|
355
|
-
|
|
356
|
-
/** F2 多匹配消歧结果(不抛错,返回候选 + 👉)。 */
|
|
357
|
-
function disambiguate(query: string, candidates: MatchedSession[]): ToolResult {
|
|
358
|
-
const lines = candidates.map(
|
|
359
|
-
(m, i) =>
|
|
360
|
-
` ${i + 1}. ${m.sessionId} · ${formatDate(m.mtime)}${m.firstMessagePreview ? ' · ' + m.firstMessagePreview : ''}`,
|
|
361
|
-
)
|
|
362
|
-
const hint =
|
|
363
|
-
candidates[0] !== undefined
|
|
364
|
-
? `(如 ${candidates[0].sessionId.slice(0, HINT_ID_PREFIX_LEN)})`
|
|
365
|
-
: ''
|
|
366
|
-
const text =
|
|
367
|
-
`${candidates.length} 个匹配 "${query}":\n${lines.join('\n')}\n` +
|
|
368
|
-
`👉 用更长的 uuid 片段${hint},或 action:"find" 加 cwd 过滤。`
|
|
369
|
-
return { content: [{ type: 'text', text }], details: { ambiguous: true, candidates } }
|
|
370
|
-
}
|
|
371
|
-
|
|
372
497
|
// ---------------------------------------------------------------------------
|
|
373
|
-
//
|
|
498
|
+
// zcode 读链(U9,design §3.5:白名单闸在开库前——dbPath 来自 session 数据不可信)
|
|
374
499
|
// ---------------------------------------------------------------------------
|
|
375
500
|
|
|
376
|
-
async function safeParse(fileName: string): Promise<ParseResult> {
|
|
377
|
-
try {
|
|
378
|
-
return await parseSessionFile(fileName)
|
|
379
|
-
} catch (e) {
|
|
380
|
-
throw err(
|
|
381
|
-
`读取失败:${fileName}(${toErrorMessage(e)})。👉 检查文件或换 session。`,
|
|
382
|
-
)
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// ---------------------------------------------------------------------------
|
|
387
|
-
// 文本渲染(content)
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* find 分组渲染的单组数据(u10,design 2026-09-10 §6.7 子决策 2)。
|
|
392
|
-
* main 组恒在 groups 首位(置顶),编号跨组连续。
|
|
393
|
-
*/
|
|
394
|
-
interface FindGroup {
|
|
395
|
-
source: 'main' | 'subagent'
|
|
396
|
-
/** 配额切片后实际展示的候选 */
|
|
397
|
-
shown: MatchedSession[]
|
|
398
|
-
/** 溢出:该组命中数 > shown.length(+1 探测;溢出时精确总数未知,只知更多) */
|
|
399
|
-
overflow: boolean
|
|
400
|
-
}
|
|
401
|
-
|
|
402
501
|
/**
|
|
403
|
-
*
|
|
502
|
+
* zcode 锚 → ParseResult(既有 turns/render 管线的统一内容入口)。
|
|
404
503
|
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* §3.2 失败模式 D)。SESSION_ID_PREFIX_LEN 常量本体与 result 通路不动(§6.7 范围声明)。
|
|
409
|
-
* - 每条 main 候选附一行可直接复制执行的 outline 调用串(↳,§6.7 子决策 3);
|
|
410
|
-
* subagent 候选不附(噪声不配指针)。
|
|
411
|
-
* - 候选行末段文本:标题优先(u11,name 来自 SessionManager.listAll,§5.1 形态
|
|
412
|
-
* 「… · 福耀玻璃深度研究」),无标题回退首消息预览(现状行为)。
|
|
413
|
-
* - truncated 按**合并总量**(命中总数 vs 实际输出数)计算,由调用方传入,此处只负责标注。
|
|
414
|
-
* - subagent 段超配额折叠为一行展开提示(加 source:"subagent" 查看);main 段溢出
|
|
415
|
-
* 仅在组头标注(规格的折叠提示只针对 subagent)。
|
|
504
|
+
* 检查顺序三段递进(§3.4,实现与测试双锚定):第 1 段存在性 → 第 2 段路径闸
|
|
505
|
+
* (whitelist.ts)→ 第 3 段开库与查询(四级恢复阶梯 + schema 已知集闸门在
|
|
506
|
+
* openZcodeSessionDb 单点)。读库发生在 ext 进程内(§3.5 关键论断),不经 taiji。
|
|
416
507
|
*/
|
|
417
|
-
function
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
for (const m of g.shown) {
|
|
436
|
-
index += 1
|
|
437
|
-
const parts = [`${index}. ${m.sessionId}`, formatDate(m.mtime)]
|
|
438
|
-
if (m.cwd) parts.push(shortCwd(m.cwd))
|
|
439
|
-
if (m.name) parts.push(m.name)
|
|
440
|
-
else if (m.firstMessagePreview) parts.push(m.firstMessagePreview)
|
|
441
|
-
lines.push(` ${parts.join(' · ')}`)
|
|
442
|
-
if (g.source === 'main') {
|
|
443
|
-
lines.push(` ↳ session_read { action:"outline", session:"${m.sessionId}" }`)
|
|
444
|
-
}
|
|
508
|
+
async function loadZcodeParsed(anchor: ZcodeAnchor, agentDir: string): Promise<ParseResult> {
|
|
509
|
+
assertZcodeDbPathAllowed(anchor.dbPath, agentDir)
|
|
510
|
+
let handle: Awaited<ReturnType<typeof openZcodeSessionDb>>
|
|
511
|
+
try {
|
|
512
|
+
handle = await openZcodeSessionDb(anchor.dbPath)
|
|
513
|
+
} catch (e) {
|
|
514
|
+
throw err(zcodeReadErrorMessage(e, agentDir))
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
// 恢复成功不再是静默事件(§3.4 硬要求):结构化日志(恢复方式 + db 路径),不进
|
|
518
|
+
// LLM 可见面。用 warn 而非 debug——L2/L3 是恢复降级路径,语义表 warn=内部降级与
|
|
519
|
+
// 失败(appendEntry 持久化);debug 缺省 no-op 会让恢复成功重新变回静默事件。
|
|
520
|
+
if (handle.via !== 'L1-direct') {
|
|
521
|
+
logger.warn('zcode session db recovered via recovery ladder', {
|
|
522
|
+
dbPath: anchor.dbPath,
|
|
523
|
+
via: handle.via,
|
|
524
|
+
})
|
|
445
525
|
}
|
|
446
|
-
|
|
447
|
-
|
|
526
|
+
const row = handle.db.getSessionRow(anchor.sessionId)
|
|
527
|
+
if (row === undefined) {
|
|
528
|
+
throw err(formatZcodeSessionNotFound())
|
|
448
529
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const tail = [
|
|
462
|
-
`${r.stats.totalTurns} turns · ${r.stats.totalEntries} entries · ~${r.tokenEstimate} tokens${
|
|
463
|
-
r.stats.skippedLines > 0 ? ` · ${r.stats.skippedLines} skipped lines` : ''
|
|
464
|
-
}`,
|
|
465
|
-
r.truncated ? `[还有 ${r.truncated} 轮未显示,用 detail 的 turns 参数看指定 turn 范围]` : '',
|
|
466
|
-
]
|
|
467
|
-
.filter(Boolean)
|
|
468
|
-
.join('\n')
|
|
469
|
-
return tail
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function formatExpandText(turn: string, entries: EntryBrief[]): string {
|
|
473
|
-
const lines = entries.map(
|
|
474
|
-
(e) =>
|
|
475
|
-
` [${e.index}] ${e.type}${e.role ? '/' + e.role : ''} ${e.brief}${
|
|
476
|
-
e.omittedBytes > 0 ? ' ' + formatBytesMarker(e.omittedBytes) : ''
|
|
477
|
-
}`,
|
|
478
|
-
)
|
|
479
|
-
return `${turn}\n${lines.join('\n')}`
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/** 从 message.content 提取可读文本(text/thinking 块;toolCall 留 name 占位)。 */
|
|
483
|
-
function messageReadableText(content: unknown): string {
|
|
484
|
-
if (typeof content === 'string') return content
|
|
485
|
-
if (Array.isArray(content)) {
|
|
486
|
-
return content
|
|
487
|
-
.map((b) => {
|
|
488
|
-
if (b && typeof b === 'object') {
|
|
489
|
-
const o = b as Record<string, unknown>
|
|
490
|
-
if (o.type === 'text' && typeof o.text === 'string') return o.text
|
|
491
|
-
if (o.type === 'thinking' && typeof o.thinking === 'string') return `[thinking] ${o.thinking}`
|
|
492
|
-
if (o.type === 'toolCall')
|
|
493
|
-
return `[toolCall: ${typeof o.name === 'string' ? o.name : '?'}]`
|
|
494
|
-
}
|
|
495
|
-
return ''
|
|
530
|
+
const transcript = handle.db.getSessionTranscript(anchor.sessionId)
|
|
531
|
+
const normalized = convertZcodeTranscript(transcript, {
|
|
532
|
+
id: anchor.sessionId,
|
|
533
|
+
title: row.title,
|
|
534
|
+
timeCreated: row.timeCreated,
|
|
535
|
+
})
|
|
536
|
+
if (normalized.degradations.length > 0) {
|
|
537
|
+
// P2 契约(F13):明细只进日志;压缩点以 custom entry 形态在 detail 可见
|
|
538
|
+
logger.warn('zcode session conversion degraded', {
|
|
539
|
+
sessionId: anchor.sessionId,
|
|
540
|
+
dbPath: anchor.dbPath,
|
|
541
|
+
degradations: normalized.degradations,
|
|
496
542
|
})
|
|
497
|
-
|
|
498
|
-
|
|
543
|
+
}
|
|
544
|
+
// ParseResult 形状对齐:zcode 库读无「文件字节/坏行」概念(strict Entry 树已由
|
|
545
|
+
// converter 保证),totalBytes/skippedLines 恒 0
|
|
546
|
+
return { entries: normalized.entries, totalBytes: 0, skippedLines: 0, lastLinePartial: false }
|
|
547
|
+
} catch (e) {
|
|
548
|
+
if (e instanceof Error && e.message.startsWith('[zcode_')) throw e // 本函数产出的错误面原样传播
|
|
549
|
+
throw err(zcodeReadErrorMessage(e, agentDir))
|
|
550
|
+
} finally {
|
|
551
|
+
handle.dispose()
|
|
499
552
|
}
|
|
500
|
-
return ''
|
|
501
553
|
}
|
|
502
554
|
|
|
503
555
|
/**
|
|
504
|
-
*
|
|
556
|
+
* 内容加载统一入口:zcode 路由命中 → zcode 读链(agentDir 供白名单派生,F11);pi →
|
|
557
|
+
* 既有 safeParse(行为零改动)。各 do* 的 safeParse(resolved.fileName) 消费点统一换
|
|
558
|
+
* 此函数(pi 输出逐字节不变——zcodeAnchor 缺省即原路径)。
|
|
505
559
|
*/
|
|
506
|
-
function
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* 从 message.content 提取可读文本(text/thinking 块;toolCall 留 name 占位)。
|
|
514
|
-
* v2 O3:接受 Entry | ToolResultSummaryEntry,toolResultSummary 返摘要文本(doExport full 用)。
|
|
515
|
-
*/
|
|
516
|
-
function entryReadableText(e: Entry | ToolResultSummaryEntry): string {
|
|
517
|
-
if (isToolResultSummary(e)) {
|
|
518
|
-
return `${e.summary} (共 ${e.totalLines} 行,前 3 行:${e.headLines})`
|
|
519
|
-
}
|
|
520
|
-
const msg = e.message
|
|
521
|
-
if (msg !== undefined) {
|
|
522
|
-
if (msg.role === 'toolResult') return `[toolResult] ${messageReadableText(msg.content)}`
|
|
523
|
-
return messageReadableText(msg.content)
|
|
560
|
+
async function loadParsed(
|
|
561
|
+
resolved: Extract<ResolveResult, { kind: 'ok' }>,
|
|
562
|
+
agentDir: string,
|
|
563
|
+
): Promise<ParseResult> {
|
|
564
|
+
if (resolved.zcodeAnchor !== undefined) {
|
|
565
|
+
return loadZcodeParsed(resolved.zcodeAnchor, agentDir)
|
|
524
566
|
}
|
|
525
|
-
|
|
526
|
-
return `[compaction] ${typeof e.summary === 'string' ? e.summary : JSON.stringify(e.summary ?? '')}`
|
|
527
|
-
if (e.type === 'custom') return `[custom:${e.customType ?? '?'}]`
|
|
528
|
-
return `[${e.type}]`
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function formatDetailText(
|
|
532
|
-
range: { start: number; end: number },
|
|
533
|
-
entries: Array<Entry | ToolResultSummaryEntry>,
|
|
534
|
-
): string {
|
|
535
|
-
const head = `turns ${rangeLabel(range)} · ${entries.length} entries`
|
|
536
|
-
const body = entries
|
|
537
|
-
.map((e) => {
|
|
538
|
-
if (isToolResultSummary(e)) {
|
|
539
|
-
// v2 O3:摘要态渲染(summary + 头 3 行 + 看全文提示)
|
|
540
|
-
return `---\ntoolResultSummary (${e.id.slice(0, SESSION_ID_PREFIX_LEN)})\n${e.summary}\n │ 共 ${e.totalLines} 行,前 3 行:${e.headLines}\n │ (+ includeToolResult:true 看全文)`
|
|
541
|
-
}
|
|
542
|
-
const role = e.message ? `/${e.message.role}` : ''
|
|
543
|
-
return `---\n${e.type}${role} (${e.id.slice(0, SESSION_ID_PREFIX_LEN)})\n${entryReadableText(e)}`
|
|
544
|
-
})
|
|
545
|
-
.join('\n')
|
|
546
|
-
return `${head}\n${body}`
|
|
567
|
+
return safeParse(resolved.fileName)
|
|
547
568
|
}
|
|
548
569
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
*/
|
|
553
|
-
const FAMILY_TASK_RENDER_LIMIT = 60
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* task → 单行摘要:压平空白(task 原文可含换行,换行会破坏 family 输出的行结构)后截断。
|
|
557
|
-
*/
|
|
558
|
-
function familyTaskSummary(task: string): string {
|
|
559
|
-
const flat = task.replace(/\s+/g, ' ').trim()
|
|
560
|
-
return flat.length <= FAMILY_TASK_RENDER_LIMIT ? flat : flat.slice(0, FAMILY_TASK_RENDER_LIMIT) + '…'
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* subagents 行富字段展示(ext-simplify-04 D2②):status 终态短标签 + agent 名 + task 摘要。
|
|
565
|
-
* 富字段来自 manifest/identity 组装(SubagentRef,不经 enrichRefs——已删除);孤儿
|
|
566
|
-
*(cleanedUp)只标 [已清理],不再展开摘要(已清理即终局,文件 GC 后无深读入口)。
|
|
567
|
-
*/
|
|
568
|
-
function formatSubagentLine(s: Family['subagents'][number]): string {
|
|
569
|
-
const base = ` ${s.sessionId.slice(0, SESSION_ID_PREFIX_LEN)} root=${s.rootSessionId.slice(0, SESSION_ID_PREFIX_LEN)} slug=${s.slug}`
|
|
570
|
-
if (s.cleanedUp) return `${base} [已清理]`
|
|
571
|
-
const parts: string[] = []
|
|
572
|
-
if (s.status) parts.push(`[${s.status}]`)
|
|
573
|
-
if (s.agentName) parts.push(s.agentName)
|
|
574
|
-
if (s.task) parts.push(`· ${familyTaskSummary(s.task)}`)
|
|
575
|
-
return parts.length > 0 ? `${base} ${parts.join(' ')}` : base
|
|
576
|
-
}
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// 文件读取(F6 包装)
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
577
573
|
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
lines.push(`forks: ${f.forks.map((p) => p.sessionId.slice(0, SESSION_ID_PREFIX_LEN)).join(', ')}`)
|
|
585
|
-
if (f.subagents.length)
|
|
586
|
-
lines.push(`subagents:\n${f.subagents.map(formatSubagentLine).join('\n')}`)
|
|
587
|
-
if (f.workflows.length)
|
|
588
|
-
lines.push(
|
|
589
|
-
`workflows:\n${f.workflows
|
|
590
|
-
.map((w) => ` ${w.runId} (${w.calls.length} calls)`)
|
|
591
|
-
.join('\n')}`,
|
|
574
|
+
async function safeParse(fileName: string): Promise<ParseResult> {
|
|
575
|
+
try {
|
|
576
|
+
return await parseSessionFile(fileName)
|
|
577
|
+
} catch (e) {
|
|
578
|
+
throw err(
|
|
579
|
+
`读取失败:${fileName}(${toErrorMessage(e)})。👉 检查文件或换 session。`,
|
|
592
580
|
)
|
|
593
|
-
|
|
581
|
+
}
|
|
594
582
|
}
|
|
595
583
|
|
|
596
584
|
// ===========================================================================
|
|
@@ -600,21 +588,6 @@ function formatFamilyText(f: Family): string {
|
|
|
600
588
|
/** find action 的默认匹配数上限。 */
|
|
601
589
|
const FIND_DEFAULT_LIMIT = 20
|
|
602
590
|
|
|
603
|
-
/**
|
|
604
|
-
* find 零匹配:F1 自检行(u9)。计数取本次实扫(无 options 恒实扫——根扫描无缓存,
|
|
605
|
-
* doctor 缓存机已删除,ext-simplify-04 U3),完整信号包保证 [live] 根(最高优先级)
|
|
606
|
-
* 计数可见。
|
|
607
|
-
*
|
|
608
|
-
* E1(ext-simplify-04 §3 D1):roots 由 doFind 预解析传入——与匹配用同一次实扫
|
|
609
|
-
* (调用方保证无 options),不再独立第三次全量扫盘。
|
|
610
|
-
*/
|
|
611
|
-
function findNoMatch(query: string, roots: SessionRoot[]): ToolResult {
|
|
612
|
-
return {
|
|
613
|
-
content: [{ type: 'text', text: formatNoMatch(query, roots) }],
|
|
614
|
-
details: { matches: [], truncated: false },
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
591
|
/**
|
|
619
592
|
* find:按片段/名称/recent 定位 session(design §3.4 find)。零匹配不抛,返回提示。
|
|
620
593
|
*
|
|
@@ -794,7 +767,7 @@ async function doOutline(
|
|
|
794
767
|
liveSessionDir,
|
|
795
768
|
)
|
|
796
769
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
797
|
-
const { entries, totalBytes, skippedLines } = await
|
|
770
|
+
const { entries, totalBytes, skippedLines } = await loadParsed(resolved, agentDir)
|
|
798
771
|
const tree = buildTreeView(entries)
|
|
799
772
|
const turns = segmentTurns(entries, new Set(tree.leafPath))
|
|
800
773
|
const opts: OutlineOptions = {
|
|
@@ -805,7 +778,7 @@ async function doOutline(
|
|
|
805
778
|
// 覆盖 stats.totalBytes:render 用 parsedBytes(leaf entry JSON 字节和)近似,
|
|
806
779
|
// 此处用 ParseResult.totalBytes(原始文件字节数,design §3.4 stats.totalBytes 语义)
|
|
807
780
|
result.stats.totalBytes = totalBytes
|
|
808
|
-
// [D8d] skippedLines
|
|
781
|
+
// [D8d] skippedLines 同模式覆盖:解析层(session-core parseSessionFile)已检测坏行计数(render 签名不含 ParseResult 恒 0),
|
|
809
782
|
// 有检测必有报告——静默跳过行对调用方不可见 = 数据完整性缺口
|
|
810
783
|
result.stats.skippedLines = skippedLines
|
|
811
784
|
// E7 行渲染统一:行主体 = result.lines(renderOutline 渲染行),handler 只拼 stats 尾段
|
|
@@ -831,7 +804,7 @@ async function doExpand(
|
|
|
831
804
|
)
|
|
832
805
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
833
806
|
const turnIdx = parseTurnIndex(requireStr(params.turn, 'turn', 'expand'))
|
|
834
|
-
const { entries } = await
|
|
807
|
+
const { entries } = await loadParsed(resolved, agentDir)
|
|
835
808
|
const tree = buildTreeView(entries)
|
|
836
809
|
const turns = segmentTurns(entries, new Set(tree.leafPath))
|
|
837
810
|
const turn = turns.find((t) => t.index === turnIdx)
|
|
@@ -864,7 +837,7 @@ async function doDetail(
|
|
|
864
837
|
)
|
|
865
838
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
866
839
|
const range = parseTurnsRange(requireStr(params.turns, 'turns', 'detail'))
|
|
867
|
-
const { entries } = await
|
|
840
|
+
const { entries } = await loadParsed(resolved, agentDir)
|
|
868
841
|
const tree = buildTreeView(entries)
|
|
869
842
|
const turns = segmentTurns(entries, new Set(tree.leafPath))
|
|
870
843
|
const max = turns.length - 1
|
|
@@ -923,7 +896,7 @@ async function doSearch(
|
|
|
923
896
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
924
897
|
const scope = params.scope ?? 'all'
|
|
925
898
|
const limit = params.limit ?? SEARCH_DEFAULT_LIMIT
|
|
926
|
-
const { entries } = await
|
|
899
|
+
const { entries } = await loadParsed(resolved, agentDir)
|
|
927
900
|
const tree = buildTreeView(entries)
|
|
928
901
|
const turns = segmentTurns(entries, new Set(tree.leafPath))
|
|
929
902
|
const regex = compilePattern(pattern)
|
|
@@ -970,7 +943,7 @@ async function doExport(
|
|
|
970
943
|
text = formatFamilyText(family)
|
|
971
944
|
label = 'family'
|
|
972
945
|
} else if (format === 'full') {
|
|
973
|
-
const { entries } = await
|
|
946
|
+
const { entries } = await loadParsed(resolved, agentDir)
|
|
974
947
|
const tree = buildTreeView(entries)
|
|
975
948
|
const turns = segmentTurns(entries, new Set(tree.leafPath))
|
|
976
949
|
const det = renderDetail(turns, {
|
|
@@ -987,7 +960,7 @@ async function doExport(
|
|
|
987
960
|
.join('\n')
|
|
988
961
|
label = 'full'
|
|
989
962
|
} else {
|
|
990
|
-
const { entries } = await
|
|
963
|
+
const { entries } = await loadParsed(resolved, agentDir)
|
|
991
964
|
const tree = buildTreeView(entries)
|
|
992
965
|
const turns = segmentTurns(entries, new Set(tree.leafPath))
|
|
993
966
|
const result = renderOutline(turns, tree, {
|
|
@@ -1054,7 +1027,7 @@ async function doExtract(
|
|
|
1054
1027
|
liveSessionDir,
|
|
1055
1028
|
)
|
|
1056
1029
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
1057
|
-
const { entries } = await
|
|
1030
|
+
const { entries } = await loadParsed(resolved, agentDir)
|
|
1058
1031
|
// extract 遍历全量 entry(含旁支/压缩历史),与 outline/expand/detail 的 leaf 视图不同:
|
|
1059
1032
|
// 素材提取要全量(design §2.3 实测全量 519 toolCall / 26 user / 515 toolResult),
|
|
1060
1033
|
// 用 leafPath 过滤会漏掉旁支素材。turn 标注是全量分段 index(含 compaction 周期 + 旁支
|
|
@@ -1357,18 +1330,41 @@ export async function handleSessionRead(
|
|
|
1357
1330
|
return doExtract(params, agentDir, signals.liveSessionDir)
|
|
1358
1331
|
case 'workflow':
|
|
1359
1332
|
return doWorkflow(params, agentDir, signals.liveSessionDir)
|
|
1360
|
-
case 'result':
|
|
1333
|
+
case 'result': {
|
|
1361
1334
|
// per-call 构造注入面(仅剩 tool-handler 文件私有 helper,纯函数经 handler-utils
|
|
1362
1335
|
// 直接 import——ext-simplify-04 E5):resolveSessionId 包装把信号包中的
|
|
1363
1336
|
// liveSessionDir 闭包进解析调用(ResultActionDeps 接口签名固定 5 参,包装保持
|
|
1364
1337
|
// 同形、末位补传),result 的片段形态与 find/outline 消费同一 roots
|
|
1365
1338
|
//(sa-/绝对路径分支在 resolveSessionId 内不受影响)。
|
|
1339
|
+
// U9 zcode 路由:deps.safeParse 只认 fileName 字符串,而 zcode 内容源是库——
|
|
1340
|
+
// per-call Map 按 fileName(=锚库路径)携带锚,safeParse 包装分流到 zcode 读链
|
|
1341
|
+
//(pi 路径 Map 未命中 → 原样 safeParse,行为零变化)。
|
|
1342
|
+
const zcodeByFile = new Map<string, ZcodeAnchor>()
|
|
1366
1343
|
return doResult(params, agentDir, {
|
|
1367
|
-
resolveSessionId: (rawSession, action, ad, source, prefetchedManifests) =>
|
|
1368
|
-
|
|
1344
|
+
resolveSessionId: async (rawSession, action, ad, source, prefetchedManifests) => {
|
|
1345
|
+
const resolved = await resolveSessionId(
|
|
1346
|
+
rawSession,
|
|
1347
|
+
action,
|
|
1348
|
+
ad,
|
|
1349
|
+
source,
|
|
1350
|
+
prefetchedManifests,
|
|
1351
|
+
signals.liveSessionDir,
|
|
1352
|
+
)
|
|
1353
|
+
if (resolved.kind === 'ok' && resolved.zcodeAnchor !== undefined) {
|
|
1354
|
+
zcodeByFile.set(resolved.fileName, resolved.zcodeAnchor)
|
|
1355
|
+
}
|
|
1356
|
+
return resolved
|
|
1357
|
+
},
|
|
1369
1358
|
disambiguate,
|
|
1370
|
-
safeParse
|
|
1359
|
+
safeParse: (fileName) => {
|
|
1360
|
+
const anchor = zcodeByFile.get(fileName)
|
|
1361
|
+
if (anchor !== undefined) {
|
|
1362
|
+
return loadZcodeParsed(anchor, agentDir)
|
|
1363
|
+
}
|
|
1364
|
+
return safeParse(fileName)
|
|
1365
|
+
},
|
|
1371
1366
|
})
|
|
1367
|
+
}
|
|
1372
1368
|
case 'doctor':
|
|
1373
1369
|
return doDoctor(params, signals)
|
|
1374
1370
|
default: {
|