@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/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-a
|
|
|
2
2
|
import { getAgentDir, SessionManager } from '@earendil-works/pi-coding-agent'
|
|
3
3
|
import { StringEnum } from '@earendil-works/pi-ai'
|
|
4
4
|
import { Type } from 'typebox'
|
|
5
|
+
import { setPiHandle } from '@zhushanwen/pi-extension-logger'
|
|
5
6
|
import type { SessionMetadataProvider } from './discovery/find.js'
|
|
6
7
|
import { handleSessionRead, type SessionReadParams, type SessionReadSignals } from './tool-handler.js'
|
|
7
8
|
import { createHashAutocompleteProvider } from './tui/hash-provider.js'
|
|
@@ -153,11 +154,14 @@ const guidelines = [
|
|
|
153
154
|
'find first to locate a session by uuid fragment or name. TUI #references are full uuids.',
|
|
154
155
|
'outline before detail. Never read raw .jsonl files—use this tool.',
|
|
155
156
|
'family traces fork parents/children, subagent sessions, and workflow runs.',
|
|
157
|
+
// zcode 路由(U9)对 LLM 的唯一可见面:sa-id 输入自动路由,无需任何参数;首遇
|
|
158
|
+
// zcode_* 错误码前先有背景预告(schema 面按设计 D4 零变化,不在此重复)
|
|
159
|
+
'sa- ids route transparently to pi or zcode engine sessions; sess_ prefixed ids are not accepted—use the sa- id from the completion notice.',
|
|
156
160
|
'extract what=<type> to pull user messages / commands / files / commits / tool results across turns (optional tool= filter for commands/tool-results).',
|
|
157
161
|
"workflow action to see workflow run overviews (status/budget/steps). Each step's call sessionId can jump to outline/detail for deep reading.",
|
|
158
162
|
"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
163
|
'Errors carry a 👉 recovery hint—follow it to retry in one step.',
|
|
160
|
-
'Unsure about the host environment (pure pi vs
|
|
164
|
+
'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
165
|
]
|
|
162
166
|
|
|
163
167
|
// ---- 工具 description(design §3.4,照搬措辞)----
|
|
@@ -181,6 +185,9 @@ const registeredPis = new WeakSet<ExtensionAPI>()
|
|
|
181
185
|
let currentCwdSessionDir: string | null = null
|
|
182
186
|
|
|
183
187
|
export default function sessionReaderExtension(pi: ExtensionAPI): void {
|
|
188
|
+
// zcode 读链的结构化日志走「事后排查」通道(appendEntry,不进 LLM 上下文不显 TUI):
|
|
189
|
+
// 最早期注入 pi handle——tool-handler 的 getLogger('session-reader') 由此生效(U9)。
|
|
190
|
+
setPiHandle(pi)
|
|
184
191
|
// u11(design 2026-09-10 §6.6):标题元数据走 pi 的 SessionManager.listAll(dir)——
|
|
185
192
|
// session_info name 提取、首消息采集与并发解析由 pi 维护。注入范式同 §6.2 信号包:
|
|
186
193
|
// pi 类型只在本层出现(SessionInfo 对发现层 SessionMetadataEntry 结构兼容,直接透传),
|
|
@@ -231,7 +238,7 @@ export default function sessionReaderExtension(pi: ExtensionAPI): void {
|
|
|
231
238
|
|
|
232
239
|
// ── M4 TUI 层(design §1 + §3.3 D-3/D-4 + 附录 P-hash-trigger)──────────
|
|
233
240
|
// # 引用补全 provider + /session-pick 命令(命令名避开 pi 内置 /session 冲突)。仅 ctx.mode === 'tui' 注册:RPC 模式
|
|
234
|
-
// (
|
|
241
|
+
// (taiji 子进程)不用 pi TUI editor / slash 命令,加载即跳过。
|
|
235
242
|
//
|
|
236
243
|
// addAutocompleteProvider 挂在 ctx.ui(非 ExtensionAPI),setup 入口无 ctx,
|
|
237
244
|
// 只能在 event handler 里拿——session_start 是最早且每 session 触发的 event。
|
package/src/result-action.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* result action
|
|
2
|
+
* result action。
|
|
3
3
|
*
|
|
4
4
|
* 从 tool-handler.ts 机械提取(max-lines 拆分轮):纯移动零行为变更。纯函数 helper
|
|
5
5
|
*(err/stripHash/requireStr/SESSION_ID_PREFIX_LEN)直接 import 自 handler-utils
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 可选 limit 参数(默认 8000 字符/条,超出截断 + 提示读原文件)。
|
|
16
16
|
* 定位复用既有发现机制(sa-xxx manifest 反查 / uuid 片段 / 路径),不新造目录或文件。
|
|
17
17
|
*/
|
|
18
|
-
import type { Entry, ParseResult } from '
|
|
18
|
+
import type { Entry, ParseResult } from '@zhushanwen/session-core'
|
|
19
19
|
import { textBlockParts } from './core/render.js'
|
|
20
20
|
import type { MatchedSession } from './discovery/find.js'
|
|
21
21
|
import { listRecordManifests, type RecordManifest } from './discovery/subagents.js'
|
|
@@ -107,7 +107,8 @@ interface ResultItem {
|
|
|
107
107
|
session: string
|
|
108
108
|
/** 反查到的真实 session id(header id,非 sa- 占位) */
|
|
109
109
|
sessionId: string
|
|
110
|
-
/** session .jsonl
|
|
110
|
+
/** 内容源路径:pi = session .jsonl 绝对路径(截断提示 read 用);zcode 路由 = 会话库
|
|
111
|
+
* .sqlite 绝对路径(二进制不可 read,截断提示只留 session_read 指针,见 formatResultTruncation) */
|
|
111
112
|
sessionFile: string
|
|
112
113
|
/** 全量正文字符数(截断前口径) */
|
|
113
114
|
totalChars: number
|
|
@@ -122,16 +123,23 @@ interface ResultItem {
|
|
|
122
123
|
* notifier.ts buildTruncationPointer 同模板的 X = **丢弃**字符数(total - kept)。
|
|
123
124
|
* 两处措辞同构但口径相反(主 agent 会先后消费两种通知),统一字符串 = 行为变更,
|
|
124
125
|
* 勿顺手改口径——读本行时先确认在消费哪一侧。
|
|
125
|
-
* [C6] 英文去 emoji(对齐 notifier 指针行的纯英文形态,adversarial-review-fixes §3.4)。
|
|
126
|
+
* [C6] 英文去 emoji(对齐 notifier 指针行的纯英文形态,adversarial-review-fixes §3.4)。
|
|
127
|
+
*
|
|
128
|
+
* zcode 路由(zcodeRoute=true)下 sessionFile 是 sqlite 库路径,`read` 半边指针不可执行
|
|
129
|
+
* ——抑制之,只保留 `session_read {action:"detail"}` 可执行半边(错误 → 权威源 → 重试
|
|
130
|
+
* 闭环不破:权威源动作必须是 agent 真能执行的动作)。sessionFile 参数此形态下不进文案,
|
|
131
|
+
* 保留是为 pi/zcode 两路由签名同形。 */
|
|
126
132
|
function formatResultTruncation(
|
|
127
133
|
raw: string,
|
|
128
134
|
sessionFile: string,
|
|
129
135
|
limit: number,
|
|
130
136
|
totalChars: number,
|
|
137
|
+
zcodeRoute: boolean,
|
|
131
138
|
): string {
|
|
139
|
+
const readHalf = zcodeRoute ? '' : `read ${sessionFile}, or `
|
|
132
140
|
return (
|
|
133
141
|
`\n\n[truncated ${limit} of ${totalChars} chars — ` +
|
|
134
|
-
`full text:
|
|
142
|
+
`full text: ${readHalf}session_read { action:"detail", session:"${raw}" }]`
|
|
135
143
|
)
|
|
136
144
|
}
|
|
137
145
|
|
|
@@ -217,7 +225,8 @@ export async function doResult(
|
|
|
217
225
|
totalChars,
|
|
218
226
|
truncated,
|
|
219
227
|
text: truncated
|
|
220
|
-
? text.slice(0, limit) +
|
|
228
|
+
? text.slice(0, limit) +
|
|
229
|
+
formatResultTruncation(id, resolved.fileName, limit, totalChars, resolved.zcodeAnchor !== undefined)
|
|
221
230
|
: text,
|
|
222
231
|
})
|
|
223
232
|
}
|
package/src/search-across.ts
CHANGED
|
@@ -13,9 +13,10 @@ import {
|
|
|
13
13
|
type SessionMetadataEntry,
|
|
14
14
|
type SessionMetadataProvider,
|
|
15
15
|
} from './discovery/find.js'
|
|
16
|
-
import { parseSessionFile } from '
|
|
16
|
+
import { parseSessionFile } from '@zhushanwen/session-core'
|
|
17
17
|
import { segmentTurns, type Turn } from './core/turns.js'
|
|
18
18
|
import { buildTreeView } from './core/tree.js'
|
|
19
|
+
import { BYTES_PER_KB, BYTES_PER_MB } from './core/units.js'
|
|
19
20
|
import { err, pad } from './handler-utils.js'
|
|
20
21
|
import type { SessionReadSignals } from './doctor.js'
|
|
21
22
|
import type { SessionReadParams, ToolResult } from './tool-handler.js'
|
|
@@ -163,10 +164,6 @@ export function formatSearchText(
|
|
|
163
164
|
*/
|
|
164
165
|
export const MULTI_SEARCH_MAX_SESSIONS = 10
|
|
165
166
|
|
|
166
|
-
/** 字节展示/预算换算基数(人话格式化与预算常量共用同一量纲)。 */
|
|
167
|
-
const BYTES_PER_KB = 1024
|
|
168
|
-
const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB
|
|
169
|
-
|
|
170
167
|
/** 跨会话单次检索的预算 MB 数(§8.2 V8 字节上限的量纲;换算见 SEARCH_SCAN_BYTE_BUDGET)。 */
|
|
171
168
|
const SEARCH_SCAN_BUDGET_MB = 64
|
|
172
169
|
|
|
@@ -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
|
+
}
|