@zhushanwen/pi-session-reader 0.6.1 → 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 +6 -3
- package/src/__tests__/entry-anchor.test.ts +226 -0
- 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 +23 -0
- package/src/__tests__/parser.test.ts +1 -2
- package/src/__tests__/render.test.ts +1 -2
- package/src/__tests__/result.test.ts +2 -2
- package/src/__tests__/tool-handler.test.ts +12 -8
- 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 +1 -1
- package/src/core/toolcall.ts +2 -2
- package/src/core/tree.ts +1 -1
- package/src/core/turns.ts +1 -1
- package/src/discovery/entry-anchor.ts +108 -0
- 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/index.ts +7 -0
- package/src/result-action.ts +14 -5
- package/src/search-across.ts +1 -1
- package/src/tool-format.ts +455 -0
- package/src/tool-handler.ts +321 -325
- package/src/zcode-anchor-classify.ts +60 -0
- package/src/core/parser.ts +0 -167
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session_read 各 action 的输出文本渲染(max-lines 拆分轮从 tool-handler.ts 机械
|
|
3
|
+
* 提取,零行为变更):错误面 message(ES1/ES2 + zcode 七码 + 开库/查询期错误映射)+
|
|
4
|
+
* content 文本(find/outline/expand/detail/family)+ entry 可读文本 + F2 消歧与
|
|
5
|
+
* find 零匹配的 ToolResult 包装。纯字符串/纯渲染函数,不触 fs(findNoMatch 的
|
|
6
|
+
* roots 由调用方传入)。
|
|
7
|
+
*
|
|
8
|
+
* 导出面:SQLITE_DRIVER_UNSUPPORTED_MARK / ZCODE_DB_MISSING_MARK /
|
|
9
|
+
* zcodeReadErrorMessage(测试契约锚消费方直接 import 本模块)。logger 用同名
|
|
10
|
+
* getLogger('session-reader')——按名缓存单例,与 tool-handler 侧同一实例,
|
|
11
|
+
* warn/error 落同一 appendEntry/文件日志通道。ToolResult 等公共类型留
|
|
12
|
+
* tool-handler 定义,本模块 type import(同 doctor/extract 先例;运行时依赖方向
|
|
13
|
+
* tool-handler → 本模块,无循环)。
|
|
14
|
+
*/
|
|
15
|
+
import { dirname } from 'node:path'
|
|
16
|
+
import { toErrorMessage } from '@zhushanwen/pi-ext-guards'
|
|
17
|
+
import { getLogger } from '@zhushanwen/pi-extension-logger'
|
|
18
|
+
import {
|
|
19
|
+
SqliteUnreadableError,
|
|
20
|
+
ZcodeSchemaDriftError,
|
|
21
|
+
} from '@zhushanwen/zcode-session-source'
|
|
22
|
+
import type { Entry } from '@zhushanwen/session-core'
|
|
23
|
+
import type { MatchedSession } from './discovery/find.js'
|
|
24
|
+
import type { SessionRoot } from './discovery/roots.js'
|
|
25
|
+
import type { RecordManifest } from './discovery/subagents.js'
|
|
26
|
+
import { formatZcodeDbUnreadable } from './discovery/whitelist.js'
|
|
27
|
+
import {
|
|
28
|
+
formatBytesMarker,
|
|
29
|
+
type EntryBrief,
|
|
30
|
+
type OutlineResult,
|
|
31
|
+
type ToolResultSummaryEntry,
|
|
32
|
+
} from './core/render.js'
|
|
33
|
+
import type { Family } from './core/family.js'
|
|
34
|
+
import { SESSION_ID_PREFIX_LEN, rangeLabel } from './handler-utils.js'
|
|
35
|
+
import { formatNoMatch } from './no-match.js'
|
|
36
|
+
import type { ToolResult } from './tool-handler.js'
|
|
37
|
+
|
|
38
|
+
// 结构化日志通道(zcodeReadErrorMessage 的降级/TOCTOU warn):getLogger 按名缓存
|
|
39
|
+
// 单例,与 tool-handler 侧调用取同一实例,appendEntry/文件日志行为不变。
|
|
40
|
+
const logger = getLogger('session-reader')
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// 小工具
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** formatDate 日期段(月/日)补零宽度。 */
|
|
47
|
+
const DATE_FIELD_WIDTH = 2
|
|
48
|
+
|
|
49
|
+
function formatDate(ms: number): string {
|
|
50
|
+
if (!ms) return ''
|
|
51
|
+
const d = new Date(ms)
|
|
52
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(DATE_FIELD_WIDTH, '0')}-${String(
|
|
53
|
+
d.getDate(),
|
|
54
|
+
).padStart(DATE_FIELD_WIDTH, '0')}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** shortCwd 保留的目录末段数。 */
|
|
58
|
+
const SHORT_CWD_SEGMENTS = 2
|
|
59
|
+
|
|
60
|
+
/** cwd 取末两段缩短显示(完整 cwd 在 details 里)。 */
|
|
61
|
+
function shortCwd(cwd: string): string {
|
|
62
|
+
const parts = cwd.split('/').filter(Boolean)
|
|
63
|
+
return parts.slice(-SHORT_CWD_SEGMENTS).join('/')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** ES1(SESSION_FILE_GC):sa-id 恰 1 命中但 sessionFile 不存在(GC/未写入)。含 manifest 元数据 + 👉。 */
|
|
67
|
+
export function formatSessionGc(record: RecordManifest): string {
|
|
68
|
+
return (
|
|
69
|
+
`subagent "${record.id}" 的 session 文件不存在(可能已被 GC 或未写入):\n` +
|
|
70
|
+
` rootSessionId: ${record.rootSessionId}\n` +
|
|
71
|
+
` agentName: ${record.agentName ?? '(未记录)'}\n` +
|
|
72
|
+
` sessionFile: ${record.sessionFile}\n` +
|
|
73
|
+
`👉 改用 session_read { action:"family" } 查该 subagent 的后代,或换一个 completed subagent 重试。`
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** ES2(SA_ID_NO_MATCH):sa-id 无精确匹配(可能仍在运行 / 片段输入)。 */
|
|
78
|
+
export function formatSaIdNotFound(saId: string): string {
|
|
79
|
+
return (
|
|
80
|
+
`subagent "${saId}" 无匹配 record(若刚启动,record 可能尚未落盘)。` +
|
|
81
|
+
`\n👉 用 session_read { action:"family" } 查活跃/已完成的 subagent;` +
|
|
82
|
+
`若是片段输入,请用完整 sa- id 或 action:"find" 重试。`
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// zcode 错误面(U9,design §3.4 七码 + 👉 逐行;错误码标识入 message 供机器识别,
|
|
88
|
+
// 形态对齐 §3.1 失败路径示例 `[zcode_db_path_forbidden] …`)
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/** `zcode_param_invalid`:sess_ 形态 id(无第一梯队直读入口,F14/D6)。 */
|
|
92
|
+
export function formatZcodeParamInvalid(id: string): string {
|
|
93
|
+
return (
|
|
94
|
+
`[zcode_param_invalid] "${id}" 是 zcode 会话 id 形态,本工具不接受(第一梯队无 sess-id 直读入口)。` +
|
|
95
|
+
`\n👉 请使用 subagent 完成通知或 /subagents 面板里的 sa- id(形如 sa-xxx),换成完整 sa- id 重试。`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `zcode_record_not_found`:manifest 缺位/残缺 ∧ entry 兜底未命中(§3.4 第 0 段末)。 */
|
|
100
|
+
export function formatZcodeRecordNotFound(): string {
|
|
101
|
+
return (
|
|
102
|
+
`[zcode_record_not_found] 该 subagent 记录不可达(未落盘 / 已被 GC / 或不属于当前会话——兜底只在当前会话内)。` +
|
|
103
|
+
`\n👉 在它被派发的那个会话中读取;或用 session_read { action:"family" } 查其后代与关联;或换一个已完成的 subagent;` +
|
|
104
|
+
`或确认输入为完整 sa- id(形如 \`sa-xxxx\`)后重试。`
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `zcode_anchor_missing`:engine==='zcode' 但锚不完整(旧版本产物 / 部分回填残余)。 */
|
|
109
|
+
export function formatZcodeAnchorMissing(saId: string): string {
|
|
110
|
+
return (
|
|
111
|
+
`[zcode_anchor_missing] subagent "${saId}" 的记录缺少 zcode 引擎定位符。` +
|
|
112
|
+
`\n👉 会话可能由旧版本产出;用会话导入对话框导入后读取。`
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `zcode_session_not_found`:锚可解析、库可开、schema 兼容,但库内无该 session 行。 */
|
|
117
|
+
export function formatZcodeSessionNotFound(): string {
|
|
118
|
+
return (
|
|
119
|
+
`[zcode_session_not_found] 该 zcode 会话不在库中(已被 zcode GC,或锚已更新到新 session——每轮锚会更新)。` +
|
|
120
|
+
`\n👉 用原 sa-id 重新发起读取;或用 session_read { action:"family" } 查其后代;或换一个已完成的 subagent。`
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `zcode_schema_drift` 的 message(观测版本区分归因,§3.4)。 */
|
|
125
|
+
function formatZcodeSchemaDrift(observed: string | undefined): string {
|
|
126
|
+
return (
|
|
127
|
+
`[zcode_schema_drift] zcode 会话库 schema 版本不兼容(observed: ${observed ?? 'unknown'})。` +
|
|
128
|
+
`\n👉 升级 taiji 到新版本,或用会话导入对话框。`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* bun 驱动探测失败(§3.4 表第 8 行理论态)的 message:宿主既无 bun:sqlite 也无
|
|
134
|
+
* node:sqlite——zcode action 整体降级为本错误面(fail-fast + 日志),pi 链路不受影响。
|
|
135
|
+
* 错误码字面量为实施期命名(设计该行未定码名,形态对齐七码 `[zcode_*]` 惯例)。
|
|
136
|
+
*/
|
|
137
|
+
function formatZcodeHostUnsupported(detail: string): string {
|
|
138
|
+
return (
|
|
139
|
+
`[zcode_host_unsupported] 宿主运行时既无 bun:sqlite 也无 node:sqlite,zcode 会话读取不可用(理论态)。` +
|
|
140
|
+
`\n👉 无需 agent 动作——升级到 node≥22.13 或使用 bun 宿主即可恢复;pi 会话读取不受影响。` +
|
|
141
|
+
`\n(detail: ${detail})`
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* sqlite 驱动探测失败的消息特征(zcode-session-source sqlite-driver.ts 探测失败的
|
|
147
|
+
* 错误消息契约;该包不导出专用错误子类,reader 侧按消息特征单列识别——跨包漂移
|
|
148
|
+
* 由 zcode-routing.test.ts 的源文本契约测试守卫:sqlite-driver 源文不再含本子串即红)。
|
|
149
|
+
* 导出面仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
|
|
150
|
+
*/
|
|
151
|
+
export const SQLITE_DRIVER_UNSUPPORTED_MARK = '不支持 node:sqlite'
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 库文件缺失的消息特征(zcode-session-source sqlite-access.ts `openZcodeSessionDb`
|
|
155
|
+
* 开头 existsSync 失败抛普通 Error「db 文件不存在:<dbPath>」的消息契约锚点)。该包
|
|
156
|
+
* 不导出专用错误子类,reader 侧按消息特征单列识别——落 §3.4 第 4 行
|
|
157
|
+
* `zcode_db_unreadable`(不误标 schema_drift)。生产路径上白名单闸
|
|
158
|
+
* (assertZcodeDbPathAllowed 第 1 段)已先行拦掉绝大多数缺失,此处命中的是两段
|
|
159
|
+
* existsSync 之间的 TOCTOU 理论窗口(闸通过后开库前文件被删)。跨包漂移由
|
|
160
|
+
* zcode-routing.test.ts 的源文本契约测试守卫(SQLITE_DRIVER_UNSUPPORTED_MARK 同范式)。
|
|
161
|
+
* 导出面仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
|
|
162
|
+
*/
|
|
163
|
+
export const ZCODE_DB_MISSING_MARK = 'db 文件不存在'
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 开库/查询期错误 → §3.4 错误面映射(SqliteUnreadableError / schema drift / 驱动探测失败 / 库缺失 / 其余)。
|
|
167
|
+
* 导出面仅测试消费(L4 包装形态的映射契约直测,zcode-routing.test.ts)。
|
|
168
|
+
*/
|
|
169
|
+
export function zcodeReadErrorMessage(e: unknown, agentDir: string): string {
|
|
170
|
+
if (e instanceof SqliteUnreadableError) {
|
|
171
|
+
// 生产路径上驱动探测错误恒经 recovery L4 包装(recovery.ts:last failure 以字符串
|
|
172
|
+
// 并入本错误 message,mark 随之存活)——instanceof 分支内先按 mark 二次判别,否则
|
|
173
|
+
// host_unsupported 面不可达(恒误映射 db_unreadable,指引动作指向错误恢复路径)。
|
|
174
|
+
if (e.message.includes(SQLITE_DRIVER_UNSUPPORTED_MARK)) {
|
|
175
|
+
logger.warn('zcode sqlite driver unsupported in host runtime', { detail: e.message })
|
|
176
|
+
return formatZcodeHostUnsupported(e.message)
|
|
177
|
+
}
|
|
178
|
+
// attempted 链进 detail 不进指引正文(§3.4 可观测性:attempted 与 message 供日志/排障)
|
|
179
|
+
return formatZcodeDbUnreadable(dirname(agentDir), `(attempted: ${e.attempted.join(' → ')})`)
|
|
180
|
+
}
|
|
181
|
+
if (e instanceof ZcodeSchemaDriftError) {
|
|
182
|
+
return formatZcodeSchemaDrift(e.observedVersion)
|
|
183
|
+
}
|
|
184
|
+
const message = toErrorMessage(e)
|
|
185
|
+
// 驱动探测失败理论态单列(§3.4 第 8 行):不与 schema 漂移混淆——指引动作不同
|
|
186
|
+
// (环境恢复 vs 升级 taiji)。fail-fast + 结构化日志(理论态触发即留痕)。
|
|
187
|
+
if (message.includes(SQLITE_DRIVER_UNSUPPORTED_MARK)) {
|
|
188
|
+
logger.warn('zcode sqlite driver unsupported in host runtime', { detail: message })
|
|
189
|
+
return formatZcodeHostUnsupported(message)
|
|
190
|
+
}
|
|
191
|
+
// 库文件缺失(openZcodeSessionDb 开头 existsSync 失败的 TOCTOU 窗口,§3.4 第 4 行)
|
|
192
|
+
// 单列:落 db_unreadable(环境恢复指引),不与 schema 漂移混淆——归因与恢复动作不同
|
|
193
|
+
if (message.includes(ZCODE_DB_MISSING_MARK)) {
|
|
194
|
+
logger.warn('zcode session db missing at open (TOCTOU window)', { detail: message })
|
|
195
|
+
return formatZcodeDbUnreadable(dirname(agentDir), `(${message})`)
|
|
196
|
+
}
|
|
197
|
+
// 其余查询期错误(data 列 JSON 非法等 schema 漂移域——sqlite-access 不静默跳过,
|
|
198
|
+
// 映射权在消费侧)→ schema_drift 语义
|
|
199
|
+
return formatZcodeSchemaDrift(undefined) + `\n(detail: ${message})`
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** ES2(SA_ID_AMBIGUOUS):sa-id 多 manifest 命中(数据异常,record.id 应唯一)。 */
|
|
203
|
+
export function formatSaIdAmbiguous(saId: string, records: RecordManifest[]): string {
|
|
204
|
+
return (
|
|
205
|
+
`subagent "${saId}" 匹配 ${records.length} 个 record(数据异常,record.id 应唯一):\n` +
|
|
206
|
+
records
|
|
207
|
+
.map((r) => ` ${r.id} (root=${r.rootSessionId} file=${r.sessionFile})`)
|
|
208
|
+
.join('\n') +
|
|
209
|
+
`\n👉 用 session_read { action:"family" } 或完整 session uuid 重试。`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 消歧提示的 uuid 片段长度(比短显略长,引导输入更长片段消歧)。 */
|
|
214
|
+
const HINT_ID_PREFIX_LEN = 12
|
|
215
|
+
|
|
216
|
+
/** F2 多匹配消歧结果(不抛错,返回候选 + 👉)。 */
|
|
217
|
+
export function disambiguate(query: string, candidates: MatchedSession[]): ToolResult {
|
|
218
|
+
const lines = candidates.map(
|
|
219
|
+
(m, i) =>
|
|
220
|
+
` ${i + 1}. ${m.sessionId} · ${formatDate(m.mtime)}${m.firstMessagePreview ? ' · ' + m.firstMessagePreview : ''}`,
|
|
221
|
+
)
|
|
222
|
+
const hint =
|
|
223
|
+
candidates[0] !== undefined
|
|
224
|
+
? `(如 ${candidates[0].sessionId.slice(0, HINT_ID_PREFIX_LEN)})`
|
|
225
|
+
: ''
|
|
226
|
+
const text =
|
|
227
|
+
`${candidates.length} 个匹配 "${query}":\n${lines.join('\n')}\n` +
|
|
228
|
+
`👉 用更长的 uuid 片段${hint},或 action:"find" 加 cwd 过滤。`
|
|
229
|
+
return { content: [{ type: 'text', text }], details: { ambiguous: true, candidates } }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// 文本渲染(content)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* find 分组渲染的单组数据(u10,design 2026-09-10 §6.7 子决策 2)。
|
|
238
|
+
* main 组恒在 groups 首位(置顶),编号跨组连续。
|
|
239
|
+
*/
|
|
240
|
+
interface FindGroup {
|
|
241
|
+
source: 'main' | 'subagent'
|
|
242
|
+
/** 配额切片后实际展示的候选 */
|
|
243
|
+
shown: MatchedSession[]
|
|
244
|
+
/** 溢出:该组命中数 > shown.length(+1 探测;溢出时精确总数未知,只知更多) */
|
|
245
|
+
overflow: boolean
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* find 输出渲染(u10 分组版,design §5.1 形态 + §6.7 子决策 2/3 精确规格):
|
|
250
|
+
*
|
|
251
|
+
* - 按 source 分组、main 段置顶(subagent 噪声不淹没目标),组头标注各组命中数,
|
|
252
|
+
* 组内编号跨组连续(§5.1 示例:subagent 段从 main 段末尾续号)。
|
|
253
|
+
* - 候选行打印完整 sessionId(废除 8 字符截断——agent 拿到截断 id 无法粘回做精确调用,
|
|
254
|
+
* §3.2 失败模式 D)。SESSION_ID_PREFIX_LEN 常量本体与 result 通路不动(§6.7 范围声明)。
|
|
255
|
+
* - 每条 main 候选附一行可直接复制执行的 outline 调用串(↳,§6.7 子决策 3);
|
|
256
|
+
* subagent 候选不附(噪声不配指针)。
|
|
257
|
+
* - 候选行末段文本:标题优先(u11,name 来自 SessionManager.listAll,§5.1 形态
|
|
258
|
+
* 「… · 福耀玻璃深度研究」),无标题回退首消息预览(现状行为)。
|
|
259
|
+
* - truncated 按**合并总量**(命中总数 vs 实际输出数)计算,由调用方传入,此处只负责标注。
|
|
260
|
+
* - subagent 段超配额折叠为一行展开提示(加 source:"subagent" 查看);main 段溢出
|
|
261
|
+
* 仅在组头标注(规格的折叠提示只针对 subagent)。
|
|
262
|
+
*/
|
|
263
|
+
export function formatFindContent(query: string, groups: FindGroup[], truncated: boolean): string {
|
|
264
|
+
const shown = groups.flatMap((g) => g.shown)
|
|
265
|
+
const head = `${shown.length} session(s) matched "${query}"${
|
|
266
|
+
truncated ? ` (truncated, showing first ${shown.length})` : ''
|
|
267
|
+
}`
|
|
268
|
+
const lines: string[] = []
|
|
269
|
+
let index = 0
|
|
270
|
+
for (const g of groups) {
|
|
271
|
+
lines.push('')
|
|
272
|
+
if (g.overflow && g.shown.length === 0) {
|
|
273
|
+
// main 占满配额、subagent 有命中但 0 条展示(§6.7:「subagent 段为 0 条仅显示计数」;
|
|
274
|
+
// 命中总数须全量深读首条 user 才能精确计数,recent 形态下 IO 不可接受,只报有命中)
|
|
275
|
+
lines.push(`${g.source}(有命中未显示——展示配额已被 main 占满):`)
|
|
276
|
+
} else if (g.overflow) {
|
|
277
|
+
lines.push(`${g.source}(>${g.shown.length} 条命中,显示前 ${g.shown.length} 条):`)
|
|
278
|
+
} else {
|
|
279
|
+
lines.push(`${g.source}(${g.shown.length} 条命中):`)
|
|
280
|
+
}
|
|
281
|
+
for (const m of g.shown) {
|
|
282
|
+
index += 1
|
|
283
|
+
const parts = [`${index}. ${m.sessionId}`, formatDate(m.mtime)]
|
|
284
|
+
if (m.cwd) parts.push(shortCwd(m.cwd))
|
|
285
|
+
if (m.name) parts.push(m.name)
|
|
286
|
+
else if (m.firstMessagePreview) parts.push(m.firstMessagePreview)
|
|
287
|
+
lines.push(` ${parts.join(' · ')}`)
|
|
288
|
+
if (g.source === 'main') {
|
|
289
|
+
lines.push(` ↳ session_read { action:"outline", session:"${m.sessionId}" }`)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (g.overflow && g.source === 'subagent') {
|
|
293
|
+
lines.push(' … 另有 subagent 命中未显示。👉 加 source:"subagent" 查看')
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return `${head}\n${lines.join('\n')}`
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* outline 尾段(stats 摘要行 + truncated 提示)。E7/D3 行渲染统一:行主体 = result.lines
|
|
301
|
+
*(renderOutline 返回的渲染行,预算度量与展示同一份),行格式知识只在 core/render.ts 的
|
|
302
|
+
* formatLine 一处,tool-handler 不再重建行格式;本函数只拼 stats 尾段(skippedLines 由
|
|
303
|
+
* doOutline 用 ParseResult 覆盖后再渲染)。doOutline/doExport 同一拼装
|
|
304
|
+
* (`lines.join('\n')` + 本尾段),两 action 输出一致 by construction。
|
|
305
|
+
*/
|
|
306
|
+
export function formatOutlineTail(r: OutlineResult): string {
|
|
307
|
+
const tail = [
|
|
308
|
+
`${r.stats.totalTurns} turns · ${r.stats.totalEntries} entries · ~${r.tokenEstimate} tokens${
|
|
309
|
+
r.stats.skippedLines > 0 ? ` · ${r.stats.skippedLines} skipped lines` : ''
|
|
310
|
+
}`,
|
|
311
|
+
r.truncated ? `[还有 ${r.truncated} 轮未显示,用 detail 的 turns 参数看指定 turn 范围]` : '',
|
|
312
|
+
]
|
|
313
|
+
.filter(Boolean)
|
|
314
|
+
.join('\n')
|
|
315
|
+
return tail
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function formatExpandText(turn: string, entries: EntryBrief[]): string {
|
|
319
|
+
const lines = entries.map(
|
|
320
|
+
(e) =>
|
|
321
|
+
` [${e.index}] ${e.type}${e.role ? '/' + e.role : ''} ${e.brief}${
|
|
322
|
+
e.omittedBytes > 0 ? ' ' + formatBytesMarker(e.omittedBytes) : ''
|
|
323
|
+
}`,
|
|
324
|
+
)
|
|
325
|
+
return `${turn}\n${lines.join('\n')}`
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** 从 message.content 提取可读文本(text/thinking 块;toolCall 留 name 占位)。 */
|
|
329
|
+
function messageReadableText(content: unknown): string {
|
|
330
|
+
if (typeof content === 'string') return content
|
|
331
|
+
if (Array.isArray(content)) {
|
|
332
|
+
return content
|
|
333
|
+
.map((b) => {
|
|
334
|
+
if (b && typeof b === 'object') {
|
|
335
|
+
const o = b as Record<string, unknown>
|
|
336
|
+
if (o.type === 'text' && typeof o.text === 'string') return o.text
|
|
337
|
+
if (o.type === 'thinking' && typeof o.thinking === 'string') return `[thinking] ${o.thinking}`
|
|
338
|
+
if (o.type === 'toolCall')
|
|
339
|
+
return `[toolCall: ${typeof o.name === 'string' ? o.name : '?'}]`
|
|
340
|
+
}
|
|
341
|
+
return ''
|
|
342
|
+
})
|
|
343
|
+
.filter(Boolean)
|
|
344
|
+
.join('\n')
|
|
345
|
+
}
|
|
346
|
+
return ''
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* ToolResultSummaryEntry 判别(Entry.type 是宽 string,TS 无法靠 === 判别联合,须显式谓词收窄)。
|
|
351
|
+
*/
|
|
352
|
+
export function isToolResultSummary(
|
|
353
|
+
e: Entry | ToolResultSummaryEntry,
|
|
354
|
+
): e is ToolResultSummaryEntry {
|
|
355
|
+
return e.type === 'toolResultSummary'
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 从 message.content 提取可读文本(text/thinking 块;toolCall 留 name 占位)。
|
|
360
|
+
* v2 O3:接受 Entry | ToolResultSummaryEntry,toolResultSummary 返摘要文本(doExport full 用)。
|
|
361
|
+
*/
|
|
362
|
+
export function entryReadableText(e: Entry | ToolResultSummaryEntry): string {
|
|
363
|
+
if (isToolResultSummary(e)) {
|
|
364
|
+
return `${e.summary} (共 ${e.totalLines} 行,前 3 行:${e.headLines})`
|
|
365
|
+
}
|
|
366
|
+
const msg = e.message
|
|
367
|
+
if (msg !== undefined) {
|
|
368
|
+
if (msg.role === 'toolResult') return `[toolResult] ${messageReadableText(msg.content)}`
|
|
369
|
+
return messageReadableText(msg.content)
|
|
370
|
+
}
|
|
371
|
+
if (e.type === 'compaction')
|
|
372
|
+
return `[compaction] ${typeof e.summary === 'string' ? e.summary : JSON.stringify(e.summary ?? '')}`
|
|
373
|
+
if (e.type === 'custom') return `[custom:${e.customType ?? '?'}]`
|
|
374
|
+
return `[${e.type}]`
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function formatDetailText(
|
|
378
|
+
range: { start: number; end: number },
|
|
379
|
+
entries: Array<Entry | ToolResultSummaryEntry>,
|
|
380
|
+
): string {
|
|
381
|
+
const head = `turns ${rangeLabel(range)} · ${entries.length} entries`
|
|
382
|
+
const body = entries
|
|
383
|
+
.map((e) => {
|
|
384
|
+
if (isToolResultSummary(e)) {
|
|
385
|
+
// v2 O3:摘要态渲染(summary + 头 3 行 + 看全文提示)
|
|
386
|
+
return `---\ntoolResultSummary (${e.id.slice(0, SESSION_ID_PREFIX_LEN)})\n${e.summary}\n │ 共 ${e.totalLines} 行,前 3 行:${e.headLines}\n │ (+ includeToolResult:true 看全文)`
|
|
387
|
+
}
|
|
388
|
+
const role = e.message ? `/${e.message.role}` : ''
|
|
389
|
+
return `---\n${e.type}${role} (${e.id.slice(0, SESSION_ID_PREFIX_LEN)})\n${entryReadableText(e)}`
|
|
390
|
+
})
|
|
391
|
+
.join('\n')
|
|
392
|
+
return `${head}\n${body}`
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* family subagents 行的 task 摘要截断宽度(D2②:LLM 判断「哪个 subagent 分支相关」所需
|
|
397
|
+
* 的信息量,信息密度对齐 find 的 firstMessagePreview;探针 P4 输出量级锚点)。
|
|
398
|
+
*/
|
|
399
|
+
const FAMILY_TASK_RENDER_LIMIT = 60
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* task → 单行摘要:压平空白(task 原文可含换行,换行会破坏 family 输出的行结构)后截断。
|
|
403
|
+
*/
|
|
404
|
+
function familyTaskSummary(task: string): string {
|
|
405
|
+
const flat = task.replace(/\s+/g, ' ').trim()
|
|
406
|
+
return flat.length <= FAMILY_TASK_RENDER_LIMIT ? flat : flat.slice(0, FAMILY_TASK_RENDER_LIMIT) + '…'
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* subagents 行富字段展示(ext-simplify-04 D2②):status 终态短标签 + agent 名 + task 摘要。
|
|
411
|
+
* 富字段来自 manifest/identity 组装(SubagentRef,不经 enrichRefs——已删除);孤儿
|
|
412
|
+
*(cleanedUp)只标 [已清理],不再展开摘要(已清理即终局,文件 GC 后无深读入口)。
|
|
413
|
+
*/
|
|
414
|
+
function formatSubagentLine(s: Family['subagents'][number]): string {
|
|
415
|
+
const base = ` ${s.sessionId.slice(0, SESSION_ID_PREFIX_LEN)} root=${s.rootSessionId.slice(0, SESSION_ID_PREFIX_LEN)} slug=${s.slug}`
|
|
416
|
+
if (s.cleanedUp) return `${base} [已清理]`
|
|
417
|
+
const parts: string[] = []
|
|
418
|
+
if (s.status) parts.push(`[${s.status}]`)
|
|
419
|
+
if (s.agentName) parts.push(s.agentName)
|
|
420
|
+
if (s.task) parts.push(`· ${familyTaskSummary(s.task)}`)
|
|
421
|
+
return parts.length > 0 ? `${base} ${parts.join(' ')}` : base
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function formatFamilyText(f: Family): string {
|
|
425
|
+
const lines: string[] = []
|
|
426
|
+
lines.push(`root: ${f.root.sessionId} (${formatDate(f.root.mtime)})`)
|
|
427
|
+
if (f.parents.length)
|
|
428
|
+
lines.push(`parents: ${f.parents.map((p) => p.sessionId.slice(0, SESSION_ID_PREFIX_LEN)).join(', ')}`)
|
|
429
|
+
if (f.forks.length)
|
|
430
|
+
lines.push(`forks: ${f.forks.map((p) => p.sessionId.slice(0, SESSION_ID_PREFIX_LEN)).join(', ')}`)
|
|
431
|
+
if (f.subagents.length)
|
|
432
|
+
lines.push(`subagents:\n${f.subagents.map(formatSubagentLine).join('\n')}`)
|
|
433
|
+
if (f.workflows.length)
|
|
434
|
+
lines.push(
|
|
435
|
+
`workflows:\n${f.workflows
|
|
436
|
+
.map((w) => ` ${w.runId} (${w.calls.length} calls)`)
|
|
437
|
+
.join('\n')}`,
|
|
438
|
+
)
|
|
439
|
+
return lines.join('\n')
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* find 零匹配:F1 自检行(u9)。计数取本次实扫(无 options 恒实扫——根扫描无缓存,
|
|
444
|
+
* doctor 缓存机已删除,ext-simplify-04 U3),完整信号包保证 [live] 根(最高优先级)
|
|
445
|
+
* 计数可见。
|
|
446
|
+
*
|
|
447
|
+
* E1(ext-simplify-04 §3 D1):roots 由 doFind 预解析传入——与匹配用同一次实扫
|
|
448
|
+
* (调用方保证无 options),不再独立第三次全量扫盘。
|
|
449
|
+
*/
|
|
450
|
+
export function findNoMatch(query: string, roots: SessionRoot[]): ToolResult {
|
|
451
|
+
return {
|
|
452
|
+
content: [{ type: 'text', text: formatNoMatch(query, roots) }],
|
|
453
|
+
details: { matches: [], truncated: false },
|
|
454
|
+
}
|
|
455
|
+
}
|