@zhushanwen/pi-session-reader 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/__tests__/entry-anchor.test.ts +148 -4
- package/src/__tests__/execution-tree.test.ts +6 -12
- package/src/__tests__/find.test.ts +9 -49
- package/src/__tests__/fixtures.ts +15 -0
- package/src/__tests__/parser.test.ts +36 -11
- package/src/__tests__/render.test.ts +22 -18
- package/src/__tests__/roots.test.ts +32 -34
- package/src/__tests__/subagents.test.ts +131 -108
- package/src/__tests__/tool-handler.test.ts +496 -147
- package/src/__tests__/turns.test.ts +21 -19
- package/src/__tests__/workflow.test.ts +319 -23
- package/src/core/workflow.ts +196 -6
- package/src/discovery/entry-anchor.ts +36 -5
- package/src/discovery/roots.ts +1 -2
- package/src/discovery/workflows.ts +232 -22
- package/src/tool-handler.ts +82 -22
- package/src/zcode-anchor-classify.ts +23 -9
- package/src/__tests__/real-data.ts +0 -96
|
@@ -35,11 +35,23 @@ export interface ZcodeAnchor {
|
|
|
35
35
|
*/
|
|
36
36
|
const SUBAGENT_RECORD_CUSTOM_TYPE = 'subagent-record'
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* v2 条目 data 的认识版本词表(W1 起 = 2)。与写侧 subagent-core
|
|
40
|
+
* SUBAGENT_RECORD_ENTRY_VERSION 同值、同「本地持有 + 测试守卫漂移」模式——写侧
|
|
41
|
+
* 再升 v3 时本守卫先红,提醒评估新版本的锚读取面。
|
|
42
|
+
*/
|
|
43
|
+
const SUBAGENT_RECORD_ENTRY_V2 = 2
|
|
44
|
+
|
|
38
45
|
/**
|
|
39
46
|
* 从单条 entry 提取 zcode 锚。形状校验窄而严,任一不满足 = 该条不算命中(返回
|
|
40
47
|
* undefined,调用方继续扫——缺键条目不遮蔽更早的完整条目):
|
|
41
48
|
* - `type === 'custom'` ∧ `customType === 'subagent-record'`
|
|
42
|
-
* -
|
|
49
|
+
* - 版本/判别键二选一(schema 版本守卫,不认识的版本跳过而非猜测——写侧
|
|
50
|
+
* record-entry.ts classifySubagentRecordEntryData 同口径):
|
|
51
|
+
* - v1:`data.v === 1`(全量快照形态,兼容读面);
|
|
52
|
+
* - v2:`data.v === 2` ∧ `data.kind === 'settled'`(W1 注册/终态两条小条目——
|
|
53
|
+
* **锚只取自终态条**:registered 条目不携带 engineHandle,且设计 D1 裁决
|
|
54
|
+
* 「sessionRef 双键取自终态条」;kind 非法 = unknown-kind 跳过);
|
|
43
55
|
* - `data.id === saId`(record id 即 sa-id,record-access.ts `sa-${uuid}`)
|
|
44
56
|
* - `engineHandle.sessionRef.sessionId` / `.dbPath` 均为非空 string(F8 双键齐判据的
|
|
45
57
|
* 锚字段部分;pi record 的 sessionRef 无 dbPath → 天然不命中,无需 engine 判别)
|
|
@@ -49,10 +61,26 @@ function zcodeAnchorOfEntry(entry: Entry, saId: string): ZcodeAnchor | undefined
|
|
|
49
61
|
const data: unknown = entry.data
|
|
50
62
|
if (typeof data !== 'object' || data === null) return undefined
|
|
51
63
|
const d = data as Record<string, unknown>
|
|
52
|
-
if (d.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
if (d.id !== saId) return undefined
|
|
65
|
+
if (!isAnchorEligibleRecordEntryVersion(d)) return undefined
|
|
66
|
+
return extractZcodeSessionRefAnchor(d.engineHandle)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** [zcodeAnchorOfEntry 拆分] 条目形态判别:v1 全量快照 或 v2 终态条(registered
|
|
70
|
+
* 条目不携带 engineHandle,且设计 D1 裁决「sessionRef 双键取自终态条」;kind
|
|
71
|
+
* 非法 = unknown-kind 跳过)。 */
|
|
72
|
+
function isAnchorEligibleRecordEntryVersion(d: Record<string, unknown>): boolean {
|
|
73
|
+
if (d.v === SUBAGENT_RECORD_ENTRY_V2) {
|
|
74
|
+
return d.kind === 'settled'
|
|
75
|
+
}
|
|
76
|
+
return d.v === 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** [zcodeAnchorOfEntry 拆分] engineHandle.sessionRef 双键提取(sessionId/dbPath
|
|
80
|
+
* 均非空 string 才命中;pi record 的 sessionRef 无 dbPath → 天然不命中)。 */
|
|
81
|
+
function extractZcodeSessionRefAnchor(engineHandle: unknown): ZcodeAnchor | undefined {
|
|
82
|
+
if (typeof engineHandle !== 'object' || engineHandle === null) return undefined
|
|
83
|
+
const ref: unknown = (engineHandle as Record<string, unknown>).sessionRef
|
|
56
84
|
if (typeof ref !== 'object' || ref === null) return undefined
|
|
57
85
|
const r = ref as Record<string, unknown>
|
|
58
86
|
const { sessionId, dbPath } = r
|
|
@@ -68,6 +96,9 @@ function zcodeAnchorOfEntry(entry: Entry, saId: string): ZcodeAnchor | undefined
|
|
|
68
96
|
* 「主 session 全文 → 每 id 末条 record data」,record-store-rebuild.ts:212-227):
|
|
69
97
|
* 同一 sa-id 每次状态迁移写一条全量快照 entry 且 sessionRef 每轮覆写(F16),故
|
|
70
98
|
* 同文件内后条覆盖前条 = 文件顺序末条。**禁止另创取首条/合并语义**(附录 B-6)。
|
|
99
|
+
* W1 v2 双形态下末条锚定语义不变:registered 条目不命中(无 engineHandle),
|
|
100
|
+
* 「末条」= 最后一条可命中的 v1 快照条或 v2 终态条(v2 settled 条目也随 reopen
|
|
101
|
+
* 多轮各写一条,后写覆盖前写,与 v1 覆写同构)。
|
|
71
102
|
*
|
|
72
103
|
* 多候选文件:按候选列表顺序逐文件扫,文件内有命中锚(含末条裁决后)即止——
|
|
73
104
|
* 候选顺序 = 调用方声明的优先级(liveSessionDir 优先),不跨文件合并。
|
package/src/discovery/roots.ts
CHANGED
|
@@ -67,8 +67,7 @@ const SUBAGENTS_DIRNAME = 'subagents'
|
|
|
67
67
|
/**
|
|
68
68
|
* `[live]` 规范化(§6.1/§7B 要点 3):`getSessionDir()` 在纯 pi 返回按 cwd 编码的子目录
|
|
69
69
|
*(`<agentDir>/sessions/--Users-x--`),在 taiji 覆盖态返回根本身。判据:basename
|
|
70
|
-
* 匹配 encodeCwd 形态(`--` 开头 `--`
|
|
71
|
-
* 与 real-data.ts)则取 dirname,否则取自身。
|
|
70
|
+
* 匹配 encodeCwd 形态(`--` 开头 `--` 结尾)则取 dirname,否则取自身。
|
|
72
71
|
*/
|
|
73
72
|
export function normalizeLiveSessionDir(liveSessionDir: string): string {
|
|
74
73
|
const base = basename(liveSessionDir)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { basename } from 'node:path'
|
|
3
|
-
import { parseSessionContent } from '@zhushanwen/session-core'
|
|
3
|
+
import { parseSessionContent, type Entry } from '@zhushanwen/session-core'
|
|
4
4
|
import type { SessionRef, WorkflowRef } from '../core/family.js'
|
|
5
5
|
import { extractSessionIdFromFilename } from './subagents.js'
|
|
6
6
|
|
|
@@ -9,10 +9,22 @@ import { extractSessionIdFromFilename } from './subagents.js'
|
|
|
9
9
|
// ============================================================
|
|
10
10
|
//
|
|
11
11
|
// 本文件持有 workflow run 的发现与 sessionFile 提取逻辑(IO 适配层):
|
|
12
|
-
// - resolveWorkflows
|
|
13
|
-
//
|
|
12
|
+
// - resolveWorkflows:三档发现链(按 runId 合并,高档条目在即用高档)——
|
|
13
|
+
// ① v2 journalPath 主源([D16③] 发现链重锚:journalPath 锚点语义 = record 流路径):
|
|
14
|
+
// workflow-record v2 注册条目的 journalPath 指向 <runId>.record.jsonl([D1] record
|
|
15
|
+
// 单源事件流),发现链读 record 流行自提 calls——sessionFile 取 agent-settled 帧
|
|
16
|
+
// result 全文携带的 sessionFile([D1] 载荷表:result 全文入事件,AgentResult
|
|
17
|
+
// 自带 sessionFile 字段——家族链数据源同 record 流,无第二承载位);旧后缀锚点
|
|
18
|
+
// (.events.jsonl 形态)= 历史实体([D1] 历史数据处置:旧两件不读)→ calls=[]
|
|
19
|
+
// 仅 run 存在性兜底;
|
|
20
|
+
// ② v1 快照层:workflow-record v1 快照条目(W17~W1 期间创建的 run——快照是全量的,
|
|
21
|
+
// 直接从 snapshot.calls 提 sessionFile,家族链数据完整);
|
|
22
|
+
// ③ 旧指针 fallback:workflow-state-link 指针条目(W17 前的 run,照旧读 link 指向的
|
|
23
|
+
// wf-state 文件)。
|
|
14
24
|
// - readRunSnapshot:读 wf-state 文件尾向找首个可解析行,返回原始对象(unknown,格式收窄交 core 层)。
|
|
15
25
|
// - extractCallSessionFiles:从快照对象提 calls 的 sessionFile 绝对路径数组(NEW/OLD 双格式)。
|
|
26
|
+
// - extractRecordStreamSessionFiles:[D16③] 从 record 事件流提 calls 的 sessionFile
|
|
27
|
+
// (agent-settled 帧 result.sessionFile——活跃 run 与已收编 run 同源可用)。
|
|
16
28
|
// - sessionRefFromPath:sessionFile 路径 → SessionRef(命中 pathToRef 取完整,否则文件名提取最小 ref)。
|
|
17
29
|
//
|
|
18
30
|
// 分层约定(w5 TC-wf-core-pure-logic):IO 全在 discovery/,core/workflow.ts 的
|
|
@@ -133,13 +145,185 @@ function sessionRefFromPath(path: string, pathToRef: Map<string, SessionRef>): S
|
|
|
133
145
|
}
|
|
134
146
|
}
|
|
135
147
|
|
|
148
|
+
// ============================================================
|
|
149
|
+
// workflow-record 条目契约(三档发现链的条目判别——本地字面量 + 测试守卫漂移)
|
|
150
|
+
// ============================================================
|
|
151
|
+
|
|
136
152
|
/**
|
|
137
|
-
*
|
|
138
|
-
* 同一 runId 的多个 link(workflow 多次更新产生)按 runId 去重,取最新 link(path 相同)。
|
|
153
|
+
* `workflow-record` custom entry 的 customType 值。
|
|
139
154
|
*
|
|
140
|
-
*
|
|
141
|
-
|
|
142
|
-
*
|
|
155
|
+
* 磁盘 JSONL 协议字符串(跨侧契约):写侧单源 = subagent-core
|
|
156
|
+
* workflow-record-entry.ts 的 WORKFLOW_RECORD_CUSTOM_TYPE。reader 生产依赖面不引
|
|
157
|
+
* subagent-core(同 entry-anchor.ts 的协议字面量模式),两侧漂移由 workflow.test.ts
|
|
158
|
+
* 的跨包断言守卫。
|
|
159
|
+
*/
|
|
160
|
+
const WORKFLOW_RECORD_CUSTOM_TYPE = 'workflow-record'
|
|
161
|
+
|
|
162
|
+
/** v2 条目 data 的认识版本词表(W1 起 = 2;写侧 WORKFLOW_RECORD_ENTRY_VERSION 同值)。 */
|
|
163
|
+
const WORKFLOW_RECORD_ENTRY_V2 = 2
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* run record 流文件名尾段(`<runId>.record.jsonl`)。写侧 subagent-core
|
|
167
|
+
* RUN_EVENT_JOURNAL_SUFFIX 同值([D1] record 单源后缀)——v2 注册条目的
|
|
168
|
+
* journalPath 锚点后缀判定用(新锚点直读 record 流;旧 `.events.jsonl` 锚点 =
|
|
169
|
+
* 历史实体分流),本地持有 + 测试守卫漂移。
|
|
170
|
+
*/
|
|
171
|
+
export const RUN_RECORD_STREAM_SUFFIX = '.record.jsonl'
|
|
172
|
+
|
|
173
|
+
/** 单遍扫描 entries 的三档收集结果(档内同 runId 后写覆盖前写,跨档优先级见 resolveWorkflows)。 */
|
|
174
|
+
interface WorkflowEntryTiers {
|
|
175
|
+
/** ① v2 注册条目:runId → journalPath 锚点(W1+ 的 run)。 */
|
|
176
|
+
v2ByRunId: Map<string, string>
|
|
177
|
+
/** ② v1 快照条目:runId → 全量快照对象(W17~W1 期间创建的 run)。 */
|
|
178
|
+
v1ByRunId: Map<string, unknown>
|
|
179
|
+
/** ③ 旧指针条目:runId → link path(W17 前的 run)。 */
|
|
180
|
+
linkByRunId: Map<string, { runId: string; path: string }>
|
|
181
|
+
/** runId 首见序(输出稳定序——条目在 session 文件中的出现顺序)。 */
|
|
182
|
+
runIdOrder: string[]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 单遍扫描 entries 按三档形态分桶(纯收集,无 IO):
|
|
187
|
+
* - workflow-record v2 注册条目:`v === 2` ∧ `kind === 'registered'` ∧ runId/journalPath
|
|
188
|
+
* 均非空 string(终态条目不携带 journalPath,非发现链数据源,不收集);
|
|
189
|
+
* - workflow-record v1 快照条目:`v === 1` ∧ snapshot 为对象 ∧ `snapshot.runId` 为
|
|
190
|
+
* string(runId 在快照内——entry data 顶层无 runId 字段);
|
|
191
|
+
* - workflow-state-link 指针条目:现状判别。
|
|
192
|
+
*/
|
|
193
|
+
function collectWorkflowEntryTiers(entries: readonly Entry[]): WorkflowEntryTiers {
|
|
194
|
+
const tiers: WorkflowEntryTiers = {
|
|
195
|
+
v2ByRunId: new Map(),
|
|
196
|
+
v1ByRunId: new Map(),
|
|
197
|
+
linkByRunId: new Map(),
|
|
198
|
+
runIdOrder: [],
|
|
199
|
+
}
|
|
200
|
+
const seen = new Set<string>()
|
|
201
|
+
const noteRunId = (runId: string): void => {
|
|
202
|
+
if (!seen.has(runId)) {
|
|
203
|
+
seen.add(runId)
|
|
204
|
+
tiers.runIdOrder.push(runId)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const e of entries) {
|
|
208
|
+
const data = e.data as Record<string, unknown> | undefined
|
|
209
|
+
if (e.customType === WORKFLOW_RECORD_CUSTOM_TYPE && data !== undefined) {
|
|
210
|
+
collectWorkflowRecordEntryTier(tiers, noteRunId, data)
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
// legacy 指针档字面量:常量 SSOT 在 subagent-core(WORKFLOW_STATE_LINK_CUSTOM_TYPE,
|
|
214
|
+
// orchestration/workflow-record-entry.ts)。reader 生产依赖面不引 subagent-core
|
|
215
|
+
// (包架构裁决,同 entry-anchor.ts 的协议字面量模式),保持本地镜像,值等价由
|
|
216
|
+
// __tests__/workflow.test.ts 跨包契约守卫断言锚定。
|
|
217
|
+
if (e.customType === 'workflow-state-link') {
|
|
218
|
+
collectWorkflowStateLinkTier(tiers, noteRunId, data)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return tiers
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** [collectWorkflowEntryTiers 拆分] workflow-record 条目两代分流(v2 注册 / v1 快照)。 */
|
|
225
|
+
function collectWorkflowRecordEntryTier(
|
|
226
|
+
tiers: WorkflowEntryTiers,
|
|
227
|
+
noteRunId: (runId: string) => void,
|
|
228
|
+
data: Record<string, unknown>,
|
|
229
|
+
): void {
|
|
230
|
+
if (data.v === WORKFLOW_RECORD_ENTRY_V2 && data.kind === 'registered') {
|
|
231
|
+
const runId = data.runId
|
|
232
|
+
const journalPath = data.journalPath
|
|
233
|
+
if (typeof runId === 'string' && runId !== '' && typeof journalPath === 'string' && journalPath !== '') {
|
|
234
|
+
tiers.v2ByRunId.set(runId, journalPath)
|
|
235
|
+
noteRunId(runId)
|
|
236
|
+
}
|
|
237
|
+
} else if (data.v === 1 && typeof data.snapshot === 'object' && data.snapshot !== null) {
|
|
238
|
+
const snapshotRunId = (data.snapshot as Record<string, unknown>).runId
|
|
239
|
+
if (typeof snapshotRunId === 'string' && snapshotRunId !== '') {
|
|
240
|
+
tiers.v1ByRunId.set(snapshotRunId, data.snapshot)
|
|
241
|
+
noteRunId(snapshotRunId)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** [collectWorkflowEntryTiers 拆分] workflow-state-link 指针条目(后写覆盖前写——取最新 link)。 */
|
|
247
|
+
function collectWorkflowStateLinkTier(
|
|
248
|
+
tiers: WorkflowEntryTiers,
|
|
249
|
+
noteRunId: (runId: string) => void,
|
|
250
|
+
data: Record<string, unknown> | undefined,
|
|
251
|
+
): void {
|
|
252
|
+
const runId = data?.runId
|
|
253
|
+
const path = data?.path
|
|
254
|
+
if (typeof runId === 'string' && typeof path === 'string') {
|
|
255
|
+
tiers.linkByRunId.set(runId, { runId, path }) // 后写覆盖前写(取最新 link)
|
|
256
|
+
noteRunId(runId)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* [D16③] journalPath 后缀分流:`.record.jsonl` 锚点 = 新形态(record 流直读);
|
|
262
|
+
* 其余(含旧 `.events.jsonl` 锚点)= 历史实体([D1] 历史数据处置——旧两件不读
|
|
263
|
+
* 不写,历史 run 从发现链数据面退空即预期行为)。
|
|
264
|
+
*/
|
|
265
|
+
function isNewRecordStreamAnchor(journalPath: string): boolean {
|
|
266
|
+
return journalPath.endsWith(RUN_RECORD_STREAM_SUFFIX)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** agent-settled 帧的最小消费视图(record 流宽容解析只消费 taskIndex 与
|
|
270
|
+
* result.sessionFile 两字段,其余载荷不读取)。 */
|
|
271
|
+
interface AgentSettledSessionFileView {
|
|
272
|
+
taskIndex: number
|
|
273
|
+
result: { sessionFile: string }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** [extractRecordStreamSessionFiles 守卫] 单帧是否为携带非空 result.sessionFile
|
|
277
|
+
* 的 agent-settled 帧:逐字段 typeof 校验(type → taskIndex → result →
|
|
278
|
+
* sessionFile),不过关 = 宽容跳过(与 readRunSnapshot 同容忍度)。 */
|
|
279
|
+
function isAgentSettledSessionFileFrame(v: unknown): v is AgentSettledSessionFileView {
|
|
280
|
+
if (typeof v !== 'object' || v === null) return false
|
|
281
|
+
const rec = v as Record<string, unknown>
|
|
282
|
+
if (rec.type !== 'agent-settled' || typeof rec.taskIndex !== 'number') return false
|
|
283
|
+
const result: unknown = rec.result
|
|
284
|
+
if (typeof result !== 'object' || result === null) return false
|
|
285
|
+
const sessionFile: unknown = (result as Record<string, unknown>).sessionFile
|
|
286
|
+
return typeof sessionFile === 'string' && sessionFile !== ''
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* [D16③] record 事件流 → calls 的 sessionFile 绝对路径数组:逐行宽容解析
|
|
291
|
+
* (跳过坏行/空行——与 readRunSnapshot 同容忍度),agent-settled 帧(含
|
|
292
|
+
* result 全文)按 taskIndex 归位——首见生效(重试波多帧时终局帧 result 后到
|
|
293
|
+
* 覆盖,与 fold 后到覆盖语义一致)。
|
|
294
|
+
*/
|
|
295
|
+
export function extractRecordStreamSessionFiles(content: string): string[] {
|
|
296
|
+
const byIndex = new Map<number, string>()
|
|
297
|
+
for (const line of content.split('\n')) {
|
|
298
|
+
const trimmed = line.trim()
|
|
299
|
+
if (trimmed === '') continue
|
|
300
|
+
let parsed: unknown
|
|
301
|
+
try {
|
|
302
|
+
parsed = JSON.parse(trimmed)
|
|
303
|
+
} catch {
|
|
304
|
+
continue // 坏行(截断行)跳过
|
|
305
|
+
}
|
|
306
|
+
if (!isAgentSettledSessionFileFrame(parsed)) continue
|
|
307
|
+
byIndex.set(parsed.taskIndex, parsed.result.sessionFile)
|
|
308
|
+
}
|
|
309
|
+
return [...byIndex.entries()].sort((a, b) => a[0] - b[0]).map(([, sf]) => sf)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 读目标 session 文件全文,按三档发现链构造 WorkflowRef[](D10 断链修复:W17 前/
|
|
314
|
+
* W17~W1/W1+ 三类 run 全部有发现通道)。同一 runId 的多条目按 runId 去重取档内
|
|
315
|
+
* 最新(后写覆盖);跨档优先级 = v2 注册条目 > v1 快照条目 > 旧指针——一个 run 只
|
|
316
|
+
* 以一种形态写条目(创建时点决定),跨档同 runId 是坏数据防御,高档在即用高档。
|
|
317
|
+
*
|
|
318
|
+
* **签名与返回值结构(WorkflowRef[]{runId,stateFile,calls:SessionRef[]})完全不变**
|
|
319
|
+
* (C-resolveworkflows-signature,保 m1 已冻结交付的消费者)。三档的 stateFile 语义:
|
|
320
|
+
* - v2 档:stateFile = journalPath 本身(record 流路径——[D16③] 锚点语义重定义,
|
|
321
|
+
* D1 后 v2 条目携带的锚点即 record 事件流文件);record 流直读提 calls
|
|
322
|
+
* (agent-settled.result.sessionFile),流被清理/不可读 → calls=[](run 存在性
|
|
323
|
+
* 兜底);
|
|
324
|
+
* - v1 档:空串(v1 快照条目不携带 state 路径,快照数据直接从条目提 calls;概览
|
|
325
|
+
* 消费面 readRunSnapshot('') 自然落「快照不可读」跳过,家族链 calls 不受影响);
|
|
326
|
+
* - 旧指针档:link 的 path(现状)。
|
|
143
327
|
*/
|
|
144
328
|
export async function resolveWorkflows(
|
|
145
329
|
sessionId: string,
|
|
@@ -155,23 +339,49 @@ export async function resolveWorkflows(
|
|
|
155
339
|
return []
|
|
156
340
|
}
|
|
157
341
|
const { entries } = parseSessionContent(content)
|
|
158
|
-
const
|
|
159
|
-
for (const e of entries) {
|
|
160
|
-
if (e.customType !== 'workflow-state-link') continue
|
|
161
|
-
const data = e.data as Record<string, unknown> | undefined
|
|
162
|
-
const runId = data?.runId
|
|
163
|
-
const path = data?.path
|
|
164
|
-
if (typeof runId === 'string' && typeof path === 'string') {
|
|
165
|
-
linkByRunId.set(runId, { runId, path }) // 后写覆盖前写(取最新 link)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
342
|
+
const tiers = collectWorkflowEntryTiers(entries)
|
|
168
343
|
const workflows: WorkflowRef[] = []
|
|
169
|
-
for (const
|
|
170
|
-
const
|
|
344
|
+
for (const runId of tiers.runIdOrder) {
|
|
345
|
+
const journalPath = tiers.v2ByRunId.get(runId)
|
|
346
|
+
if (journalPath !== undefined) {
|
|
347
|
+
// ① v2 档([D16③] 重锚):journalPath = record 流路径——新后缀锚点直读流提
|
|
348
|
+
// calls(agent-settled.result.sessionFile);旧后缀锚点 = 历史实体([D1]
|
|
349
|
+
// 不读旧两件)→ calls=[] 仅 run 存在性兜底。
|
|
350
|
+
let sessionFiles: string[] = []
|
|
351
|
+
if (isNewRecordStreamAnchor(journalPath)) {
|
|
352
|
+
let content: string | undefined
|
|
353
|
+
try {
|
|
354
|
+
content = await readFile(journalPath, 'utf8')
|
|
355
|
+
} catch {
|
|
356
|
+
content = undefined // 流被保留期清理/不可读 → calls=[](run 存在性兜底)
|
|
357
|
+
}
|
|
358
|
+
sessionFiles = content === undefined ? [] : extractRecordStreamSessionFiles(content)
|
|
359
|
+
}
|
|
360
|
+
workflows.push({
|
|
361
|
+
runId,
|
|
362
|
+
stateFile: journalPath,
|
|
363
|
+
calls: sessionFiles.map((sf) => sessionRefFromPath(sf, pathToRef)),
|
|
364
|
+
})
|
|
365
|
+
continue
|
|
366
|
+
}
|
|
367
|
+
const snapshot = tiers.v1ByRunId.get(runId)
|
|
368
|
+
if (snapshot !== undefined) {
|
|
369
|
+
// ② v1 快照档:快照全量在场,直接提 calls(W17~W1 中间档恢复)
|
|
370
|
+
workflows.push({
|
|
371
|
+
runId,
|
|
372
|
+
stateFile: '',
|
|
373
|
+
calls: extractCallSessionFiles(snapshot).map((sf) => sessionRefFromPath(sf, pathToRef)),
|
|
374
|
+
})
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
// ③ 旧指针档:现状(link path → wf-state 文件)
|
|
378
|
+
const link = tiers.linkByRunId.get(runId)
|
|
379
|
+
if (link === undefined) continue
|
|
380
|
+
const snap = await readRunSnapshot(link.path)
|
|
171
381
|
const sessionFiles = snap === undefined ? [] : extractCallSessionFiles(snap)
|
|
172
382
|
workflows.push({
|
|
173
|
-
runId,
|
|
174
|
-
stateFile: path,
|
|
383
|
+
runId: link.runId,
|
|
384
|
+
stateFile: link.path,
|
|
175
385
|
calls: sessionFiles.map((sf) => sessionRefFromPath(sf, pathToRef)),
|
|
176
386
|
})
|
|
177
387
|
}
|
package/src/tool-handler.ts
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
parseSessionFile,
|
|
59
59
|
type ParseResult,
|
|
60
60
|
} from '@zhushanwen/session-core'
|
|
61
|
-
import { parseRunSnapshot, renderWorkflowOverview, type WorkflowOverview } from './core/workflow.js'
|
|
61
|
+
import { parseRunRecordStream, parseRunSnapshot, renderWorkflowOverview, type WorkflowOverview } from './core/workflow.js'
|
|
62
62
|
import { buildTreeView } from './core/tree.js'
|
|
63
63
|
import { segmentTurns } from './core/turns.js'
|
|
64
64
|
import { renderOutline, renderExpand, renderDetail, type OutlineOptions } from './core/render.js'
|
|
@@ -335,9 +335,9 @@ async function resolveSaIdRoute(
|
|
|
335
335
|
/**
|
|
336
336
|
* 候选文件里该 sa-id 的 `subagent-record` entry 若「在场但锚不完整」,返回缺失归因
|
|
337
337
|
*(§3.4 zcode_anchor_missing 的 entry 形态触发 + 日志归因;记录完全不在场返回
|
|
338
|
-
* undefined)。entry
|
|
339
|
-
* zcode-anchor-classify.ts 的 firstIncompleteAnchorReason
|
|
340
|
-
|
|
338
|
+
* undefined)。entry 级判定(v1 快照 / v2 终态条双版本门 + D5 engine 判别 + 锚完整
|
|
339
|
+
* 性链)在 zcode-anchor-classify.ts 的 firstIncompleteAnchorReason,本函数只做文件
|
|
340
|
+
* 扫描 I/O(读失败跳过该文件,首个命中归因即终止扫描)。
|
|
341
341
|
*/
|
|
342
342
|
async function classifyIncompleteEntryAnchor(
|
|
343
343
|
candidateFiles: readonly string[],
|
|
@@ -1098,7 +1098,8 @@ interface SkippedRun {
|
|
|
1098
1098
|
/**
|
|
1099
1099
|
* workflow:workflow run 概览(design §3.4 workflow,m2 IF-doWorkflow)。
|
|
1100
1100
|
*
|
|
1101
|
-
* 流程:① resolveSessionId(multi 走 disambiguate)→ ② 读目标 session 的 workflow
|
|
1101
|
+
* 流程:① resolveSessionId(multi 走 disambiguate)→ ② 读目标 session 的 workflow 条目三档
|
|
1102
|
+
* 发现链(v2 注册条目 record 流主源 > v1 快照 > 旧 workflow-state-link 指针)
|
|
1102
1103
|
* → ③ 无 run → ES-wf-no-runs(提示+👉family,不抛错)→ ④ runId 过滤,无匹配 →
|
|
1103
1104
|
* ES-wf-runid-not-found(列候选+👉,不抛错)→ ⑤ 逐 run readRunSnapshot+parseRunSnapshot,
|
|
1104
1105
|
* 不可读/不可解析 → skippedRuns(不中断其他 run,ES-wf-snapshot-read-fail/unparseable)
|
|
@@ -1116,6 +1117,81 @@ interface SkippedRun {
|
|
|
1116
1117
|
* step 的 call sessionId/sessionFile 是 LLM 跳 outline/detail 的入口(m0 resolveSessionId
|
|
1117
1118
|
* 三形态复用:sessionId/绝对路径/sa-id 均可深读,TC-wf-step-sessionfile-link)。
|
|
1118
1119
|
*/
|
|
1120
|
+
|
|
1121
|
+
/** [doWorkflow 拆分] 单 run 概览产出(⑤⑥ 的逐 run 半边):record 流直读档与快照档分流,
|
|
1122
|
+
* 产出聚合进 ctx 四容器(runs/runIds/skippedRuns/contentParts)。 */
|
|
1123
|
+
async function renderSingleWorkflowRun(
|
|
1124
|
+
wf: WorkflowRef,
|
|
1125
|
+
ctx: {
|
|
1126
|
+
runs: WorkflowOverview[]
|
|
1127
|
+
runIds: string[]
|
|
1128
|
+
skippedRuns: SkippedRun[]
|
|
1129
|
+
contentParts: string[]
|
|
1130
|
+
},
|
|
1131
|
+
): Promise<void> {
|
|
1132
|
+
// [D16③] 概览链换源:v2 档的 stateFile = record 流路径(journalPath 锚点语义
|
|
1133
|
+
// 重定义)——直读流经 parseRunRecordStream 概览([D2] 三态 status;空流/读失败
|
|
1134
|
+
// 投影 running,活跃 run 概览不退化为 skipped——红线)。record 后缀以外的
|
|
1135
|
+
// stateFile(v1 快照档空串 / 旧指针档 link path)走原快照链。
|
|
1136
|
+
if (wf.stateFile.endsWith('.record.jsonl')) {
|
|
1137
|
+
await renderRunFromRecordStream(wf, ctx)
|
|
1138
|
+
return
|
|
1139
|
+
}
|
|
1140
|
+
const snap = await readRunSnapshot(wf.stateFile)
|
|
1141
|
+
if (snap === undefined) {
|
|
1142
|
+
// ES-wf-snapshot-read-fail:文件不存在/读失败/全行不可解析 → 跳过,不中断其他 run
|
|
1143
|
+
skipRun(ctx, wf, 'snapshot-unreadable', '快照不可读')
|
|
1144
|
+
return
|
|
1145
|
+
}
|
|
1146
|
+
const overview = parseRunSnapshot(snap, wf.runId, wf.stateFile)
|
|
1147
|
+
if (overview === null) {
|
|
1148
|
+
// ES-wf-snapshot-unparseable:对象既非 NEW 也非 OLD → 跳过
|
|
1149
|
+
skipRun(ctx, wf, 'snapshot-unparseable', '快照格式不可识别')
|
|
1150
|
+
return
|
|
1151
|
+
}
|
|
1152
|
+
ctx.runs.push(overview)
|
|
1153
|
+
ctx.runIds.push(wf.runId)
|
|
1154
|
+
ctx.contentParts.push(renderWorkflowOverview(overview))
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** [renderSingleWorkflowRun 拆分] record 流直读档(读失败 → 空流解析,running 兜底不 skipped)。 */
|
|
1158
|
+
async function renderRunFromRecordStream(
|
|
1159
|
+
wf: WorkflowRef,
|
|
1160
|
+
ctx: {
|
|
1161
|
+
runs: WorkflowOverview[]
|
|
1162
|
+
runIds: string[]
|
|
1163
|
+
skippedRuns: SkippedRun[]
|
|
1164
|
+
contentParts: string[]
|
|
1165
|
+
},
|
|
1166
|
+
): Promise<void> {
|
|
1167
|
+
let content: string | undefined
|
|
1168
|
+
try {
|
|
1169
|
+
content = await readFile(wf.stateFile, 'utf8')
|
|
1170
|
+
} catch {
|
|
1171
|
+
content = undefined // 流被清理/不可读 → 空流解析(running 兜底,不 skipped)
|
|
1172
|
+
}
|
|
1173
|
+
const overview = parseRunRecordStream(content, wf.runId, wf.stateFile)
|
|
1174
|
+
ctx.runs.push(overview)
|
|
1175
|
+
ctx.runIds.push(wf.runId)
|
|
1176
|
+
ctx.contentParts.push(renderWorkflowOverview(overview))
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** [renderSingleWorkflowRun 拆分] 跳过登记(不中断其他 run)。 */
|
|
1180
|
+
function skipRun(
|
|
1181
|
+
ctx: {
|
|
1182
|
+
runs: WorkflowOverview[]
|
|
1183
|
+
runIds: string[]
|
|
1184
|
+
skippedRuns: SkippedRun[]
|
|
1185
|
+
contentParts: string[]
|
|
1186
|
+
},
|
|
1187
|
+
wf: WorkflowRef,
|
|
1188
|
+
reason: SkippedRun['reason'],
|
|
1189
|
+
label: string,
|
|
1190
|
+
): void {
|
|
1191
|
+
ctx.skippedRuns.push({ runId: wf.runId, stateFile: wf.stateFile, reason })
|
|
1192
|
+
ctx.contentParts.push(`run ${wf.runId}: ${label}(stateFile=${wf.stateFile})已跳过`)
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1119
1195
|
async function doWorkflow(
|
|
1120
1196
|
params: SessionReadParams,
|
|
1121
1197
|
agentDir: string,
|
|
@@ -1180,23 +1256,7 @@ async function doWorkflow(
|
|
|
1180
1256
|
const contentParts: string[] = []
|
|
1181
1257
|
|
|
1182
1258
|
for (const wf of selected) {
|
|
1183
|
-
|
|
1184
|
-
if (snap === undefined) {
|
|
1185
|
-
// ES-wf-snapshot-read-fail:文件不存在/读失败/全行不可解析 → 跳过,不中断其他 run
|
|
1186
|
-
skippedRuns.push({ runId: wf.runId, stateFile: wf.stateFile, reason: 'snapshot-unreadable' })
|
|
1187
|
-
contentParts.push(`run ${wf.runId}: 快照不可读(stateFile=${wf.stateFile})已跳过`)
|
|
1188
|
-
continue
|
|
1189
|
-
}
|
|
1190
|
-
const overview = parseRunSnapshot(snap, wf.runId, wf.stateFile)
|
|
1191
|
-
if (overview === null) {
|
|
1192
|
-
// ES-wf-snapshot-unparseable:对象既非 NEW 也非 OLD → 跳过
|
|
1193
|
-
skippedRuns.push({ runId: wf.runId, stateFile: wf.stateFile, reason: 'snapshot-unparseable' })
|
|
1194
|
-
contentParts.push(`run ${wf.runId}: 快照格式不可识别(stateFile=${wf.stateFile})已跳过`)
|
|
1195
|
-
continue
|
|
1196
|
-
}
|
|
1197
|
-
runs.push(overview)
|
|
1198
|
-
runIds.push(wf.runId)
|
|
1199
|
-
contentParts.push(renderWorkflowOverview(overview))
|
|
1259
|
+
await renderSingleWorkflowRun(wf, { runs, runIds, skippedRuns, contentParts })
|
|
1200
1260
|
}
|
|
1201
1261
|
|
|
1202
1262
|
const details: WorkflowDetails = { runs, runIds }
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [§3.5 entry 兜底归因] zcode subagent-record「在场但锚不完整」的缺失归因判定
|
|
3
|
-
*(classifyIncompleteEntryAnchor 的 entry
|
|
4
|
-
*
|
|
5
|
-
* 可完全单测。
|
|
3
|
+
*(classifyIncompleteEntryAnchor 的 entry 级纯逻辑提取;文件扫描与 readFile I/O
|
|
4
|
+
* 留在 tool-handler.ts)。零 I/O 纯函数,可完全单测。
|
|
6
5
|
*
|
|
7
|
-
* 判定与 entry-anchor.ts 的 zcodeAnchorOfEntry
|
|
8
|
-
|
|
9
|
-
* entry-anchor.test.ts 守卫)。
|
|
6
|
+
* 判定与 entry-anchor.ts 的 zcodeAnchorOfEntry 双键判据同构(v1 快照 / v2 终态条
|
|
7
|
+
* 双版本门),协议字面量同值(写侧 subagent-core record-entry.ts
|
|
8
|
+
* SUBAGENT_RECORD_CUSTOM_TYPE,漂移由 entry-anchor.test.ts 守卫)。
|
|
10
9
|
*/
|
|
11
10
|
import type { Entry } from '@zhushanwen/session-core'
|
|
12
11
|
import type { ZcodeAnchorMissingReason } from './discovery/zcode-manifest.js'
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* v2 条目 data 的认识版本词表(W1 起 = 2)。与写侧 subagent-core
|
|
15
|
+
* SUBAGENT_RECORD_ENTRY_VERSION 同值——本地持有 + entry-anchor.test.ts 跨包断言
|
|
16
|
+
* 守卫漂移(生产依赖面不引 subagent-core,同 entry-anchor.ts 的协议字面量模式)。
|
|
17
|
+
*/
|
|
18
|
+
const SUBAGENT_RECORD_ENTRY_V2 = 2
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 目标 sa-id 的 zcode 形态 subagent-record data:type/customType/版本关/id/engine
|
|
22
|
+
* 全过才返回。版本关(W1 v2 学习,entry-anchor.ts zcodeAnchorOfEntry 同口径):
|
|
23
|
+
* v1 全量快照(`d.v === 1`,兼容读面)或 v2 终态条(`d.v === 2` ∧
|
|
24
|
+
* `d.kind === 'settled'`——engineHandle 只在终态条,registered 不携带不归因)。
|
|
25
|
+
* engine 判别(D5)在此——`d.engine !== 'zcode'` 的记录(pi 形态:
|
|
17
26
|
* engine 缺省、sessionRef 无 dbPath)不是「zcode 锚不完整」,返回 undefined 跳过
|
|
18
27
|
* 不归因:误归因 missing-dbPath 会把「pi record 先于 manifest settle 落盘」的正常
|
|
19
28
|
* 窗口错报成 zcode_anchor_missing(旧版本产物指引不适用);正确归因 = undefined
|
|
@@ -24,7 +33,12 @@ function zcodeRecordDataOf(entry: Entry, saId: string): Record<string, unknown>
|
|
|
24
33
|
const data: unknown = entry.data
|
|
25
34
|
if (typeof data !== 'object' || data === null) return undefined
|
|
26
35
|
const d = data as Record<string, unknown>
|
|
27
|
-
if (d.
|
|
36
|
+
if (d.id !== saId) return undefined
|
|
37
|
+
if (d.v === SUBAGENT_RECORD_ENTRY_V2) {
|
|
38
|
+
if (d.kind !== 'settled') return undefined
|
|
39
|
+
} else if (d.v !== 1) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
28
42
|
if (d.engine !== 'zcode') return undefined
|
|
29
43
|
return d
|
|
30
44
|
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { execSync } from 'node:child_process'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 真实 pi agent 数据常量 + 存在性探测(MF-1:CI 无本机 ~/.pi/agent 目录,
|
|
8
|
-
* 依赖真实数据的用例必须 skipIf 守卫,否则 CI `pnpm extensions:test` 必红)。
|
|
9
|
-
*
|
|
10
|
-
* 用法:测试文件 `import { REAL_AGENT_DIR, HAS_E6 } from './real-data.js'`,
|
|
11
|
-
* 真实数据用例套 `it.skipIf(!HAS_X)` / `describe.skipIf(!HAS_X)`;
|
|
12
|
-
* fixture 用例保持无条件跑。
|
|
13
|
-
*
|
|
14
|
-
* 本文件无 .test.ts 后缀,vitest include 只收集 `src/__tests__/` 下以 .test.ts 结尾的文件,不收集本文件。
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** 真实 pi agent 目录(pi 官方家目录布局,随宿主 homedir 推导),用于集成测试。 */
|
|
18
|
-
export const REAL_AGENT_DIR = join(homedir(), '.pi', 'agent')
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 全量扫描类真实数据用例的统一超时标定(90s)。
|
|
22
|
-
*
|
|
23
|
-
* 这类用例遍历本机 ~/.pi/agent 全量会话语料(实测 4600+ 文件),空载单用例已需 6-10s;
|
|
24
|
-
* 38 包并行全量测试时 CPU/IO 争抢可降速 3-4 倍,30s 标定必假红。90s ≈ 空载 9 倍余量,
|
|
25
|
-
* 仍能兜住真正的挂死。仅用于遍历全量语料的用例(findSessions / buildFamilyFromFs /
|
|
26
|
-
* resolveSessionRoots 等);读已知单文件的用例不需要放宽。
|
|
27
|
-
*/
|
|
28
|
-
export const REAL_DATA_TIMEOUT_MS = 90_000
|
|
29
|
-
|
|
30
|
-
/** 5.4MB / 32 turn / 1204 entry 的真实 session(feat-plugin-arch-3 目录)。 */
|
|
31
|
-
export const E6 = '019e6c96-0a0c-74b8-a73f-d1854d88e2a7'
|
|
32
|
-
/** 真实 fork 家族根(fork 子代 019fe632,隔代 subagent 019fe635 挂在 019fe632 下)。 */
|
|
33
|
-
export const FAM = '019fe620-8ae1-78a7-b76a-43a1ba4cc3c7'
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* E6 的完整文件路径(parser/turns/render 直接 parseSessionFile 用)。
|
|
37
|
-
* 按 sid 运行时定位而非硬编码宿主 workspace 目录名:真实文件挂在哪个 slug 目录随采集机器而异,
|
|
38
|
-
* 硬编码会绑死本机路径形态;定位不到时回落相对布局,由下方 HAS_REAL_SESSION 守卫 skip。
|
|
39
|
-
*/
|
|
40
|
-
export const REAL_SESSION = (() => {
|
|
41
|
-
const fallback = join(REAL_AGENT_DIR, 'sessions', `2026-05-28T03-17-12-844Z_${E6}.jsonl`)
|
|
42
|
-
if (!existsSync(REAL_AGENT_DIR)) return fallback
|
|
43
|
-
try {
|
|
44
|
-
return (
|
|
45
|
-
execSync(
|
|
46
|
-
`find ${REAL_AGENT_DIR}/sessions -name '*${E6}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
47
|
-
{ encoding: 'utf8' },
|
|
48
|
-
).trim() || fallback
|
|
49
|
-
)
|
|
50
|
-
} catch {
|
|
51
|
-
return fallback
|
|
52
|
-
}
|
|
53
|
-
})()
|
|
54
|
-
|
|
55
|
-
/** 同步探测真实 session 文件是否存在(不存在则 skip,避免在无该数据的机器上硬失败)。 */
|
|
56
|
-
export function hasRealSession(sid: string): boolean {
|
|
57
|
-
if (!existsSync(REAL_AGENT_DIR)) return false
|
|
58
|
-
try {
|
|
59
|
-
return (
|
|
60
|
-
execSync(
|
|
61
|
-
`find ${REAL_AGENT_DIR}/sessions -name '*${sid}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
62
|
-
{ encoding: 'utf8' },
|
|
63
|
-
).trim().length > 0
|
|
64
|
-
)
|
|
65
|
-
} catch {
|
|
66
|
-
return false
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 同步探测真实数据中任意 session 文件存在(main sessions/ 与 subagents/ 双目录)。
|
|
72
|
-
* 与 hasRealSession 的区别:subagent session 文件位于 subagents/<cwd>/sessions/ 下,
|
|
73
|
-
* hasRealSession 只扫主 sessions/ 目录扫不到。用于对活跃数据目录中具体文件(如 fork
|
|
74
|
-
* 子代/隔代 subagent)存在性的守卫探测。
|
|
75
|
-
*/
|
|
76
|
-
export function hasAnyRealSession(fragment: string): boolean {
|
|
77
|
-
if (!existsSync(REAL_AGENT_DIR)) return false
|
|
78
|
-
try {
|
|
79
|
-
return (
|
|
80
|
-
execSync(
|
|
81
|
-
`find ${REAL_AGENT_DIR}/sessions ${REAL_AGENT_DIR}/subagents -name '*${fragment}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
82
|
-
{ encoding: 'utf8' },
|
|
83
|
-
).trim().length > 0
|
|
84
|
-
)
|
|
85
|
-
} catch {
|
|
86
|
-
return false
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const HAS_REAL_AGENT_DIR = existsSync(REAL_AGENT_DIR)
|
|
91
|
-
export const HAS_REAL_SUBAGENTS_DIR = existsSync(join(REAL_AGENT_DIR, 'subagents'))
|
|
92
|
-
export const HAS_REAL_SESSION = existsSync(REAL_SESSION)
|
|
93
|
-
export const HAS_E6 = hasRealSession(E6)
|
|
94
|
-
export const HAS_FAM = hasRealSession(FAM)
|
|
95
|
-
/** tool-handler 的 handleSessionRead 套件同时依赖 E6 + FAM。 */
|
|
96
|
-
export const HAS_REAL = HAS_E6 && HAS_FAM
|