@zhushanwen/pi-session-reader 0.2.4 → 0.3.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 +1 -1
- package/src/__tests__/find.test.ts +15 -1
- package/src/__tests__/render.test.ts +2 -2
- package/src/__tests__/tool-handler.test.ts +148 -11
- package/src/__tests__/toolcall.test.ts +13 -0
- package/src/__tests__/workflow.test.ts +59 -1
- package/src/core/execution-tree.ts +0 -1
- package/src/core/family.ts +45 -20
- package/src/core/parser.ts +28 -17
- package/src/core/render.ts +60 -35
- package/src/core/toolcall.ts +105 -69
- package/src/core/workflow.ts +139 -135
- package/src/discovery/find.ts +120 -74
- package/src/discovery/subagents.ts +196 -119
- package/src/discovery/workflows.ts +44 -29
- package/src/index.ts +9 -9
- package/src/tool-handler.ts +115 -67
- package/src/tui/hash-provider.ts +0 -3
- package/src/tui/session-command.ts +1 -1
- package/vitest.config.ts +2 -0
package/src/discovery/find.ts
CHANGED
|
@@ -278,26 +278,38 @@ async function matchSubagentMetadata(
|
|
|
278
278
|
)
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
281
|
+
// ============================================================
|
|
282
|
+
// findSessions 编排 helpers(按处理阶段拆分:收集 → 匹配 → 排序截断 → 预览补读)
|
|
283
|
+
// ============================================================
|
|
284
|
+
|
|
285
|
+
/** meta + header → 候选构造(ref.cwd 归一化空串 + parentSession 按需挂载)。 */
|
|
286
|
+
function buildCandidate(
|
|
287
|
+
meta: SessionFileMeta,
|
|
288
|
+
header: SessionHeader,
|
|
289
|
+
src: SessionSource,
|
|
290
|
+
): Candidate {
|
|
291
|
+
const ref: SessionRef = {
|
|
292
|
+
sessionId: header.id,
|
|
293
|
+
// 完整绝对路径(与 parentSession 同构,便于 family 按 includes(sid) 反查)
|
|
294
|
+
fileName: meta.path,
|
|
295
|
+
mtime: meta.mtime,
|
|
296
|
+
sizeBytes: meta.size,
|
|
297
|
+
cwd: header.cwd ?? '',
|
|
298
|
+
}
|
|
299
|
+
if (header.parentSession) ref.parentSession = header.parentSession
|
|
300
|
+
return { meta, ref, source: src }
|
|
301
|
+
}
|
|
296
302
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
303
|
+
/** 步骤 0+1:按来源收集文件列表(source 过滤在文件列表层)+ 逐个首行扫描建候选(cwd 过滤在此应用)。 */
|
|
304
|
+
async function collectCandidates(
|
|
305
|
+
agentDir: string,
|
|
306
|
+
sourceFilter: SessionSource | undefined,
|
|
307
|
+
cwdFilter: string | undefined,
|
|
308
|
+
): Promise<Candidate[]> {
|
|
309
|
+
// source 过滤在文件列表层:source==='main' 只扫 sessions/、'subagent' 只扫 subagents/、
|
|
310
|
+
// undefined 两者合并——决策二性能意图:不扫被过滤目录。两路目录扫描相互独立 →
|
|
311
|
+
// Promise.allSettled(AGENTS.md:独立请求用 allSettled),任一目录不存在(roots.ts
|
|
312
|
+
// 静默返回空数组)不影响另一路。
|
|
301
313
|
const sources: SessionSource[] =
|
|
302
314
|
sourceFilter === undefined ? ['main', 'subagent'] : [sourceFilter]
|
|
303
315
|
const listResults = await Promise.allSettled(
|
|
@@ -307,77 +319,84 @@ export async function findSessions(
|
|
|
307
319
|
})),
|
|
308
320
|
)
|
|
309
321
|
|
|
310
|
-
//
|
|
322
|
+
// 首行扫描建候选 SessionRef(按来源打 source 标记)
|
|
311
323
|
const candidates: Candidate[] = []
|
|
312
324
|
for (const r of listResults) {
|
|
313
325
|
if (r.status !== 'fulfilled') continue
|
|
314
326
|
const { src, files } = r.value
|
|
315
327
|
for (const meta of files) {
|
|
316
|
-
const
|
|
317
|
-
const header = parseHeader(headerLine)
|
|
328
|
+
const header = parseHeader(await readFirstLine(meta.path))
|
|
318
329
|
if (!header) continue // 非 session 文件/坏 header → 跳过
|
|
319
330
|
if (cwdFilter !== undefined && (header.cwd ?? '') !== cwdFilter) continue
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
331
|
+
candidates.push(buildCandidate(meta, header, src))
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return candidates
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** 步骤 2 关键词层(U5 扩展):manifest 元数据(+ P-fallback identity 回退)与首消息预览并列匹配,命中任一即入选。 */
|
|
338
|
+
async function matchByKeywords(
|
|
339
|
+
candidates: Candidate[],
|
|
340
|
+
query: string,
|
|
341
|
+
agentDir: string,
|
|
342
|
+
): Promise<Matched[]> {
|
|
343
|
+
const manifestIndex = await buildManifestIndex(
|
|
344
|
+
agentDir,
|
|
345
|
+
candidates.some((c) => c.source === 'subagent'),
|
|
346
|
+
)
|
|
347
|
+
const keywordHits: Matched[] = []
|
|
348
|
+
for (const c of candidates) {
|
|
349
|
+
// - subagent 候选:先查 manifest 索引(命中走元数据子串,未命中 P-fallback 读尾行 identity);
|
|
350
|
+
// 元数据命中即入选(preview 留空,第 5 步补读首消息),未命中仍可走首消息 fallback
|
|
351
|
+
// - main / subagent 元数据未命中:首消息预览 query 子串匹配(m0 现状路径不变)
|
|
352
|
+
if (c.source === 'subagent' && (await matchSubagentMetadata(c, query, manifestIndex))) {
|
|
353
|
+
keywordHits.push({ ...c })
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
const text = await readFirstUserMessageText(c.meta.path)
|
|
357
|
+
if (text && text.includes(query)) {
|
|
358
|
+
keywordHits.push({ ...c, preview: text.slice(0, PREVIEW_MAX) })
|
|
330
359
|
}
|
|
331
360
|
}
|
|
361
|
+
return keywordHits
|
|
362
|
+
}
|
|
332
363
|
|
|
333
|
-
|
|
334
|
-
|
|
364
|
+
/** 步骤 2 三路匹配:recent / uuid 片段(sessionId 或文件路径含 query)/ 名称关键词+U5 元数据。 */
|
|
365
|
+
async function matchCandidates(
|
|
366
|
+
candidates: Candidate[],
|
|
367
|
+
query: string,
|
|
368
|
+
agentDir: string,
|
|
369
|
+
): Promise<Matched[]> {
|
|
335
370
|
if (query === 'recent') {
|
|
336
371
|
// recent:不经片段匹配,全部候选按 mtime 倒序后截 limit
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// 关键词层(U5 扩展):subagent manifest 元数据 task/slug/agentName(+ P-fallback identity
|
|
350
|
-
// 回退)与 main/subagent 首消息预览并列匹配,命中任一即入选(TC-find-match-priority)。
|
|
351
|
-
// - subagent 候选:先查 manifest 索引(命中走元数据子串,未命中 P-fallback 读尾行 identity);
|
|
352
|
-
// 元数据命中即入选(preview 留空,第 5 步补读首消息),未命中仍可走首消息 fallback
|
|
353
|
-
// - main / subagent 元数据未命中:首消息预览 query 子串匹配(m0 现状路径不变)
|
|
354
|
-
const manifestIndex = await buildManifestIndex(
|
|
355
|
-
agentDir,
|
|
356
|
-
candidates.some((c) => c.source === 'subagent'),
|
|
357
|
-
)
|
|
358
|
-
const keywordHits: Matched[] = []
|
|
359
|
-
for (const c of candidates) {
|
|
360
|
-
if (c.source === 'subagent' && (await matchSubagentMetadata(c, query, manifestIndex))) {
|
|
361
|
-
keywordHits.push({ ...c })
|
|
362
|
-
continue
|
|
363
|
-
}
|
|
364
|
-
const text = await readFirstUserMessageText(c.meta.path)
|
|
365
|
-
if (text && text.includes(query)) {
|
|
366
|
-
keywordHits.push({ ...c, preview: text.slice(0, PREVIEW_MAX) })
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
matched = keywordHits
|
|
370
|
-
}
|
|
372
|
+
return candidates.map((c) => ({ ...c }))
|
|
373
|
+
}
|
|
374
|
+
// 先 uuid 片段匹配(sessionId 或文件路径含 query)——cheap,已有 header
|
|
375
|
+
const uuidHits = candidates.filter(
|
|
376
|
+
(c) => c.ref.sessionId.includes(query) || c.meta.path.includes(query),
|
|
377
|
+
)
|
|
378
|
+
if (uuidHits.length > 0) {
|
|
379
|
+
return uuidHits.map((c) => ({ ...c }))
|
|
380
|
+
}
|
|
381
|
+
if (looksLikeUuidFragment(query)) {
|
|
382
|
+
// query 像 uuid 片段但无匹配 → uuid 写错的可能性高,不对全部候选深读首消息
|
|
383
|
+
return []
|
|
371
384
|
}
|
|
385
|
+
return matchByKeywords(candidates, query, agentDir)
|
|
386
|
+
}
|
|
372
387
|
|
|
373
|
-
|
|
388
|
+
/** 步骤 3+4:mtime 倒序 + limit 截断(truncated 标记是否截断)。 */
|
|
389
|
+
function sortByMtimeAndTruncate(
|
|
390
|
+
matched: Matched[],
|
|
391
|
+
limit: number,
|
|
392
|
+
): { items: Matched[]; truncated: boolean } {
|
|
374
393
|
matched.sort((a, b) => b.ref.mtime - a.ref.mtime)
|
|
375
|
-
|
|
376
|
-
// 4. 截断
|
|
377
394
|
const truncated = matched.length > limit
|
|
378
|
-
|
|
395
|
+
return { items: truncated ? matched.slice(0, limit) : matched, truncated }
|
|
396
|
+
}
|
|
379
397
|
|
|
380
|
-
|
|
398
|
+
/** 步骤 5:填 firstMessagePreview(recent/uuid 路径未读,对最终 limit 个补读——最多 limit 个 IO)。 */
|
|
399
|
+
async function fillFirstMessagePreviews(sliced: Matched[]): Promise<MatchedSession[]> {
|
|
381
400
|
const result: MatchedSession[] = []
|
|
382
401
|
for (const m of sliced) {
|
|
383
402
|
const out: MatchedSession = { ...m.ref, source: m.source }
|
|
@@ -389,6 +408,33 @@ export async function findSessions(
|
|
|
389
408
|
}
|
|
390
409
|
result.push(out)
|
|
391
410
|
}
|
|
411
|
+
return result
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 按 query 找 session(接口冻结,design §3.4 find action)。
|
|
416
|
+
*
|
|
417
|
+
* 返回按 mtime 倒序,limit 截断(默认 20),truncated 标记是否截断。
|
|
418
|
+
* cwd 过滤:opts.cwd 提供时只留 header.cwd === opts.cwd 的(在匹配前过滤,减少 fallback 深读量)。
|
|
419
|
+
* 匹配为空 → `{ matches: [], truncated: false }`(F1 恢复指引在 M3 tool-adapter 层)。
|
|
420
|
+
*/
|
|
421
|
+
export async function findSessions(
|
|
422
|
+
query: string,
|
|
423
|
+
agentDir: string,
|
|
424
|
+
opts?: { cwd?: string; limit?: number; source?: SessionSource },
|
|
425
|
+
): Promise<{ matches: MatchedSession[]; truncated: boolean }> {
|
|
426
|
+
const limit = opts?.limit ?? DEFAULT_LIMIT
|
|
427
|
+
const cwdFilter = opts?.cwd
|
|
428
|
+
const sourceFilter = opts?.source
|
|
429
|
+
|
|
430
|
+
// 0+1. 按来源收集文件列表 → 首行扫描建候选
|
|
431
|
+
const candidates = await collectCandidates(agentDir, sourceFilter, cwdFilter)
|
|
432
|
+
// 2. 三路匹配(recent / uuid 片段 / 名称关键词+U5 元数据)
|
|
433
|
+
const matched = await matchCandidates(candidates, query, agentDir)
|
|
434
|
+
// 3+4. mtime 倒序 + limit 截断
|
|
435
|
+
const { items, truncated } = sortByMtimeAndTruncate(matched, limit)
|
|
436
|
+
// 5. 填 firstMessagePreview(对最终 limit 个补读)
|
|
437
|
+
const matches = await fillFirstMessagePreviews(items)
|
|
392
438
|
|
|
393
|
-
return { matches
|
|
439
|
+
return { matches, truncated }
|
|
394
440
|
}
|