@zhushanwen/pi-session-reader 0.5.1 → 0.6.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 +6 -3
- package/src/__tests__/cross-package-subagent-core.test.ts +42 -49
- package/src/__tests__/env.test.ts +54 -54
- package/src/__tests__/family.test.ts +0 -7
- package/src/__tests__/find.test.ts +6 -6
- package/src/__tests__/index.test.ts +1 -1
- package/src/__tests__/real-data.ts +23 -10
- package/src/__tests__/render.test.ts +36 -1
- package/src/__tests__/result.test.ts +20 -23
- package/src/__tests__/roots.test.ts +47 -81
- package/src/__tests__/session-command.test.ts +23 -0
- package/src/__tests__/subagents.test.ts +5 -7
- package/src/__tests__/tool-handler.test.ts +481 -216
- package/src/__tests__/workflow.test.ts +1 -7
- package/src/core/execution-tree.ts +1 -0
- package/src/core/family.ts +12 -9
- package/src/core/render.ts +41 -14
- package/src/core/toolcall.ts +1 -2
- package/src/core/units.ts +14 -0
- package/src/core/workflow.ts +7 -20
- package/src/discovery/env.ts +41 -41
- package/src/discovery/find.ts +43 -135
- package/src/discovery/roots.ts +18 -89
- package/src/discovery/session-header.ts +103 -0
- package/src/discovery/subagents.ts +23 -94
- package/src/doctor.ts +12 -143
- package/src/extract.ts +6 -19
- package/src/handler-utils.ts +28 -5
- package/src/index.ts +2 -2
- package/src/no-match.ts +3 -2
- package/src/result-action.ts +29 -34
- package/src/search-across.ts +6 -8
- package/src/tool-handler.ts +151 -176
- package/src/tui/hash-provider.ts +7 -3
- package/src/tui/session-command.ts +22 -10
- package/vitest.config.ts +2 -4
|
@@ -30,7 +30,6 @@ const NEW_SNAPSHOT_FIXTURE = {
|
|
|
30
30
|
opts: {
|
|
31
31
|
prompt: 'Reply with exactly: PROBE-OK',
|
|
32
32
|
model: 'deepseek-router/ds-pro',
|
|
33
|
-
thinkingLevel: 'high',
|
|
34
33
|
description: 'step-0',
|
|
35
34
|
},
|
|
36
35
|
status: 'done',
|
|
@@ -64,7 +63,7 @@ const V2_SNAPSHOT_FIXTURE = {
|
|
|
64
63
|
calls: [
|
|
65
64
|
{
|
|
66
65
|
id: 0,
|
|
67
|
-
opts: { prompt: 'task 0', model: 'default',
|
|
66
|
+
opts: { prompt: 'task 0', model: 'default', description: 'v2-step-0' },
|
|
68
67
|
status: 'done',
|
|
69
68
|
attempts: 1,
|
|
70
69
|
sessionId: '019v2a',
|
|
@@ -126,12 +125,10 @@ describe('parseRunSnapshot', () => {
|
|
|
126
125
|
expect(step.status).toBe('done')
|
|
127
126
|
expect(step.description).toBe('step-0')
|
|
128
127
|
expect(step.model).toBe('deepseek-router/ds-pro')
|
|
129
|
-
expect(step.thinkingLevel).toBe('high')
|
|
130
128
|
expect(step.attempts).toBe(1)
|
|
131
129
|
expect(step.durationMs).toBe(1234)
|
|
132
130
|
expect(step.sessionId).toBe('019xxx')
|
|
133
131
|
expect(step.sessionFile).toBe('/abs/session.jsonl')
|
|
134
|
-
expect(step.contentPreview).toBe('PROBE-OK')
|
|
135
132
|
})
|
|
136
133
|
|
|
137
134
|
it('TC-w5-parse-new-v2:v2 快照(pi-subagent-workflow 8.x 写入)解析非 null,calls sessionFile 读出', () => {
|
|
@@ -148,7 +145,6 @@ describe('parseRunSnapshot', () => {
|
|
|
148
145
|
expect(overview!.steps[0].description).toBe('v2-step-0')
|
|
149
146
|
expect(overview!.steps[1].sessionFile).toBe('/abs/v2-call1.jsonl')
|
|
150
147
|
expect(overview!.steps[1].sessionId).toBe('019v2b')
|
|
151
|
-
expect(overview!.steps[1].contentPreview).toBe('OK')
|
|
152
148
|
})
|
|
153
149
|
|
|
154
150
|
it('TC-w5-parse-new-status-unknown:NEW call.status 非 done/running/pending(如 failed)→ step.status=pending', () => {
|
|
@@ -174,7 +170,6 @@ describe('parseRunSnapshot', () => {
|
|
|
174
170
|
expect(step.index).toBe(3)
|
|
175
171
|
expect(step.status).toBe('pending')
|
|
176
172
|
expect(step.description).toBe('step-3')
|
|
177
|
-
expect(step.contentPreview).toBe('partial')
|
|
178
173
|
expect(step.durationMs).toBe(9)
|
|
179
174
|
})
|
|
180
175
|
|
|
@@ -193,7 +188,6 @@ describe('parseRunSnapshot', () => {
|
|
|
193
188
|
expect(step.index).toBe(0) // callCache 顺序索引
|
|
194
189
|
expect(step.status).toBe('pending') // content='' 空串不算完成标志 → pending
|
|
195
190
|
expect(step.sessionFile).toBeUndefined() // OLD 未持久化
|
|
196
|
-
expect(step.contentPreview).toBe('') // value.content='' 仍提取
|
|
197
191
|
})
|
|
198
192
|
|
|
199
193
|
it('TC-w5-parse-corrupt-nonobject:非对象输入(null/undefined/string/number/array)返回 null', () => {
|
|
@@ -59,6 +59,7 @@ export interface ExecutionTreeNode {
|
|
|
59
59
|
agentName?: string
|
|
60
60
|
slug?: string
|
|
61
61
|
task?: string
|
|
62
|
+
/** 仅 details 可见,不进文本渲染(formatExecutionTreeText 渲染 status/slug/agentName/task,与 SubagentRef.model 同口径) */
|
|
62
63
|
model?: string
|
|
63
64
|
/** subagent 终态 completed/failed/running(manifest.status) */
|
|
64
65
|
status?: string
|
package/src/core/family.ts
CHANGED
|
@@ -6,11 +6,14 @@ import type { Entry } from './parser.js'
|
|
|
6
6
|
* M1 范围:纯逻辑层。buildFamilyIndex 接收已读入的 entry 数组(不做文件 IO),
|
|
7
7
|
* 文件扫描/首行读取归 M2 discovery 层(roots.ts/find.ts/subagents.ts)。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* 占位约定(原「M2 回填补全」双阶段约定已随 enrichRefs 删除退役,ext-simplify-04 U4/E2
|
|
10
|
+
* ——回填的 fileName/cwd 零文本读者;SessionRef 类型与 find 路径共享,find 侧两字段有
|
|
11
|
+
* 真实消费者,字段本身保留):
|
|
12
|
+
* - SessionRef.fileName / subagent cwd:family 路径恒为占位空串(session header 推不出
|
|
13
|
+
* 路径 / identity entry 无 cwd),不再回填
|
|
11
14
|
* - SessionRef.mtime/sizeBytes:从 fileStats 取(M1 key=sessionId),取不到为 0
|
|
12
15
|
* - SubagentRef.sessionId:identity entry 不含 subagent session 的 id → 占位用 entry.id
|
|
13
|
-
*
|
|
16
|
+
* (M2 buildFamilyFromFs 以 header.id 替换为真实 id)
|
|
14
17
|
* - fileStats 的 key:sessionId(M2 实现沿用此 key,cleanedUp 按 sessionId 命中),逻辑通用
|
|
15
18
|
*
|
|
16
19
|
* 隔代关联规则(design §3.3 D-7 Q1)见 resolveFamily 注释。
|
|
@@ -35,11 +38,11 @@ export interface SubagentRef extends SessionRef {
|
|
|
35
38
|
task?: string
|
|
36
39
|
/** agent 类型名(manifest.agentName,P-fallback 时 identity.data.agent;同语义异名) */
|
|
37
40
|
agentName?: string
|
|
38
|
-
/** 模型 id(仅 manifest 有;P-fallback 时 undefined) */
|
|
41
|
+
/** 模型 id(仅 manifest 有;P-fallback 时 undefined)。仅 details 可见,不进文本渲染(D2②:展示面只取 status/agentName/task) */
|
|
39
42
|
model?: string
|
|
40
43
|
/** 终态 completed/failed/running(仅 manifest 有;P-fallback 时 undefined) */
|
|
41
44
|
status?: string
|
|
42
|
-
/** subagent session.jsonl 绝对路径(manifest.sessionFile 或 alive 文件 meta.path
|
|
45
|
+
/** subagent session.jsonl 绝对路径(manifest.sessionFile 或 alive 文件 meta.path)。仅 details 可见,不进文本渲染 */
|
|
43
46
|
sessionFile?: string
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -118,16 +121,16 @@ function isSubagentIdentityData(v: unknown): v is SubagentIdentityData {
|
|
|
118
121
|
* 文件名格式 `<timestamp>_<sessionId>.jsonl`,故路径字符串含父 sessionId。
|
|
119
122
|
* 遍历已知 sessionId 做子串匹配反查。
|
|
120
123
|
*
|
|
121
|
-
* 兼容 parentSession 直接就是 sessionId 的简化场景(测试 fixture 常用)。
|
|
122
124
|
* 假设 sessionId 互不为子串(pi 用 UUID,满足);M2 可优化为 fileName→sessionId
|
|
123
125
|
* 索引反查(O(1)),当前遍历 O(N),家族索引文件数通常几十到几百,可接受。
|
|
126
|
+
*(原「parentSession 直接是 sessionId」快路径已删除,ext-simplify-04 A4——生产数据
|
|
127
|
+
* 恒为文件路径,该分支只服务测试 fixture 简化形态。)
|
|
124
128
|
*/
|
|
125
129
|
function resolveParentSessionId(
|
|
126
130
|
parentSession: string | undefined,
|
|
127
131
|
byId: Map<string, SessionRef>,
|
|
128
132
|
): string | null {
|
|
129
133
|
if (!parentSession) return null
|
|
130
|
-
if (byId.has(parentSession)) return parentSession // 直接是 sessionId(简化场景)
|
|
131
134
|
for (const sid of byId.keys()) {
|
|
132
135
|
if (parentSession.includes(sid)) return sid
|
|
133
136
|
}
|
|
@@ -144,7 +147,7 @@ function buildById(
|
|
|
144
147
|
const stat = fileStats.get(h.id)
|
|
145
148
|
const ref: SessionRef = {
|
|
146
149
|
sessionId: h.id,
|
|
147
|
-
fileName: '', //
|
|
150
|
+
fileName: '', // 占位空串:family 路径不回填(enrichRefs 已删除,ext-simplify-04 U4)
|
|
148
151
|
mtime: stat?.mtime ?? 0,
|
|
149
152
|
sizeBytes: stat?.size ?? 0,
|
|
150
153
|
cwd: h.cwd ?? '',
|
|
@@ -198,7 +201,7 @@ function buildSubagentsByRoot(
|
|
|
198
201
|
fileName: '', // M1 占位
|
|
199
202
|
mtime: stat?.mtime ?? 0,
|
|
200
203
|
sizeBytes: stat?.size ?? 0,
|
|
201
|
-
cwd: '', // identity entry 无 cwd;
|
|
204
|
+
cwd: '', // identity entry 无 cwd;family 路径不回填(enrichRefs 已删除,ext-simplify-04 U4)
|
|
202
205
|
// fileStats key=sessionId(buildFamilyFromFs 以 header.id 写入),cleanedUp 按 sessionId 命中
|
|
203
206
|
cleanedUp: !fileStats.has(ident.id),
|
|
204
207
|
// U4 富字段:从 identity data 读(manifest 主/P-fallback 由 buildFamilyFromFs 组装时决定)
|
package/src/core/render.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Entry } from './parser.js'
|
|
2
|
+
import { BYTES_PER_KB } from './units.js'
|
|
2
3
|
import { extractToolCalls, formatToolCallSummary, type ToolCallInfo } from './toolcall.js'
|
|
3
4
|
import type { Turn } from './turns.js'
|
|
4
5
|
import type { TreeView } from './tree.js'
|
|
@@ -7,8 +8,6 @@ import type { TreeView } from './tree.js'
|
|
|
7
8
|
// 模块常量(渲染口径:截断宽度 / 换算基数 / 降级档位)
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
|
|
10
|
-
/** bytes→KB 换算基数(omitted 字节 / read 结果规模的 KB 显示)。 */
|
|
11
|
-
const BYTES_PER_KB = 1024
|
|
12
11
|
/** userBrief 截断字符数(TurnBrief.userBrief 文档口径「截 60 字符」)。 */
|
|
13
12
|
const USER_BRIEF_MAX_CHARS = 60
|
|
14
13
|
/** assistantBrief 截断字符数(TurnBrief.assistantBrief 文档口径「截 80 字符」)。 */
|
|
@@ -17,7 +16,8 @@ const ASSISTANT_BRIEF_MAX_CHARS = 80
|
|
|
17
16
|
const ENTRY_BRIEF_MAX_CHARS = 100
|
|
18
17
|
/** turn 索引显示宽度(T013 三位补零)。 */
|
|
19
18
|
const TURN_INDEX_WIDTH = 3
|
|
20
|
-
/** outline 默认 token 预算(OutlineOptions.budget
|
|
19
|
+
/** outline 默认 token 预算(OutlineOptions.budget 缺省值)。生产路径(doOutline/doExport)
|
|
20
|
+
* 唯一取值:outline 是设计定档的 ~1500 token 全貌 TOC,无按调用方调节的需求。 */
|
|
21
21
|
const OUTLINE_DEFAULT_BUDGET_TOKENS = 2000
|
|
22
22
|
/** token 估算换算基数(chars/4,与 tokenEstimate 口径一致)。 */
|
|
23
23
|
const CHARS_PER_TOKEN = 4
|
|
@@ -55,6 +55,11 @@ export interface TurnBrief {
|
|
|
55
55
|
|
|
56
56
|
export interface OutlineResult {
|
|
57
57
|
turns: TurnBrief[]
|
|
58
|
+
/**
|
|
59
|
+
* 渲染行(E7/D3 行渲染统一):formatLine 产出、降级与总预算截断后的最终展示行——
|
|
60
|
+
* 预算度量与工具层展示共用同一份(度量 = 展示 by construction),展示侧不得重建行格式。
|
|
61
|
+
*/
|
|
62
|
+
lines: string[]
|
|
58
63
|
/** skippedLines:JSON 解析失败/缺结构字段的行数(parser 检测,工具层覆盖精确值;render 层无 ParseResult 恒 0) */
|
|
59
64
|
stats: { totalTurns: number; totalEntries: number; totalBytes: number; parsedBytes: number; skippedLines: number }
|
|
60
65
|
/** chars / 4 近似(与 design P-outline 口径一致) */
|
|
@@ -64,7 +69,9 @@ export interface OutlineResult {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
export interface OutlineOptions {
|
|
67
|
-
/**
|
|
72
|
+
/** 测试注入缝(E10 定性):非模型可见参数——session_read schema 无 budget 入参,模型
|
|
73
|
+
* 无法也无需调节;存在动机 = 测试注入小预算触发降级档用例(砍 assistantBrief / 骨架档)。
|
|
74
|
+
* 生产路径不传,走 OUTLINE_DEFAULT_BUDGET_TOKENS 默认。 */
|
|
68
75
|
budget?: number
|
|
69
76
|
allBranches?: boolean
|
|
70
77
|
/** 默认 turn;entry = 每 entry 一行不聚合(D-1 兜底,坏 session 调试) */
|
|
@@ -101,6 +108,26 @@ function blockThinking(block: ContentBlock): string {
|
|
|
101
108
|
return ''
|
|
102
109
|
}
|
|
103
110
|
|
|
111
|
+
/**
|
|
112
|
+
* E11 共享核:白名单提取 message.content 中 `type==='text'` 块的 text 字段(块 → string[])。
|
|
113
|
+
* string content 直取为单元素数组;非 string 非数组返回空数组。join 语义(''/'\n'/' ')
|
|
114
|
+
* 与空态语义(空串 vs undefined)留在各调用点组合——S7 警告适用:join 差异是行为敏感点,
|
|
115
|
+
* 不可在共享核合并。消费方:result-action('' join)/ extract('\n' join)/ find(' ' join
|
|
116
|
+
* + 空返 undefined)。
|
|
117
|
+
*/
|
|
118
|
+
export function textBlockParts(content: unknown): string[] {
|
|
119
|
+
if (typeof content === 'string') return [content]
|
|
120
|
+
if (!Array.isArray(content)) return []
|
|
121
|
+
const parts: string[] = []
|
|
122
|
+
for (const b of content) {
|
|
123
|
+
if (b !== null && typeof b === 'object') {
|
|
124
|
+
const o = b as Record<string, unknown>
|
|
125
|
+
if (o.type === 'text' && typeof o.text === 'string') parts.push(o.text)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return parts
|
|
129
|
+
}
|
|
130
|
+
|
|
104
131
|
/** 拼接 text 块文本(排除 thinking / toolCall / tool_use / tool_result 块)。 */
|
|
105
132
|
function extractText(content: unknown): string {
|
|
106
133
|
if (isStringContent(content)) return content
|
|
@@ -276,7 +303,7 @@ function findBranchForkPoint(turn: Turn, tree: TreeView): string | undefined {
|
|
|
276
303
|
}
|
|
277
304
|
|
|
278
305
|
// ---------------------------------------------------------------------------
|
|
279
|
-
//
|
|
306
|
+
// 行渲染(预算度量与展示同一份渲染行,E7/D3 行渲染统一)
|
|
280
307
|
// ---------------------------------------------------------------------------
|
|
281
308
|
|
|
282
309
|
function formatHHMM(timestamp?: string): string {
|
|
@@ -292,10 +319,11 @@ export function formatBytesMarker(bytes: number): string {
|
|
|
292
319
|
}
|
|
293
320
|
|
|
294
321
|
/**
|
|
295
|
-
*
|
|
322
|
+
* 渲染单行为字符串(E7 行渲染统一:预算度量与展示同一份渲染行,OutlineResult.lines 的行工厂)。
|
|
296
323
|
* v2 O1:L1 行含 assistantBrief(补 assistant 结论行让 outline 单独可决策,不再逼反复 expand)。
|
|
297
324
|
* level: 0=全有(toolSummary + assistantBrief)/ 1=砍 assistantBrief / 2=再砍 toolSummary(骨架)。
|
|
298
325
|
* userBrief + omittedBytes 骨架永保。assistantBrief 格式 `→ <结论>`,在 toolSummary 后、omitted 前。
|
|
326
|
+
* 旁支标记 `[旁支 N entries]`(branchSize = 该 forkPoint 下旁支子树 entry 数)。
|
|
299
327
|
*/
|
|
300
328
|
function formatLine(b: TurnBrief, level: LineLevel, branchSize?: number): string {
|
|
301
329
|
const parts: string[] = []
|
|
@@ -341,7 +369,8 @@ function applyBranchLabels(briefs: TurnBrief[], turns: Turn[], tree: TreeView):
|
|
|
341
369
|
/**
|
|
342
370
|
* 降级序逐行渲染(design §3.5 算法 1 step3):level 0 全有;超预算降到 level 1
|
|
343
371
|
* 砍 assistantBrief;仍超降到 level 2 砍 toolSummary(骨架)。
|
|
344
|
-
* 直接改写 brief 的 assistantBrief/toolSummary
|
|
372
|
+
* 直接改写 brief 的 assistantBrief/toolSummary 字段,返回行缓存(E7 后随结果
|
|
373
|
+
* 返回为 OutlineResult.lines,预算度量与展示同一份)。
|
|
345
374
|
*/
|
|
346
375
|
function renderDegradingLines(
|
|
347
376
|
briefs: TurnBrief[],
|
|
@@ -408,7 +437,7 @@ export function renderOutline(
|
|
|
408
437
|
}
|
|
409
438
|
|
|
410
439
|
if (turns.length === 0) {
|
|
411
|
-
return { turns: [], stats, tokenEstimate: 0 }
|
|
440
|
+
return { turns: [], lines: [], stats, tokenEstimate: 0 }
|
|
412
441
|
}
|
|
413
442
|
|
|
414
443
|
// granularity:entry —— 每 entry 一行,不聚合 turn(D-1 兜底)
|
|
@@ -428,11 +457,11 @@ export function renderOutline(
|
|
|
428
457
|
// 3. 降级序渲染:level 0 全有 → level 1 砍 assistantBrief → level 2 骨架
|
|
429
458
|
const lineCache = renderDegradingLines(briefs, tree, perTurnCharBudget)
|
|
430
459
|
|
|
431
|
-
// 4.
|
|
460
|
+
// 4. 总预算截断(从尾部丢弃,lines 与 briefs 同步截短)
|
|
432
461
|
const { totalChars, truncated } = truncateToTotalBudget(briefs, lineCache, budget)
|
|
433
462
|
|
|
434
463
|
const tokenEstimate = Math.ceil(totalChars / CHARS_PER_TOKEN)
|
|
435
|
-
return { turns: briefs, stats, tokenEstimate, truncated }
|
|
464
|
+
return { turns: briefs, lines: lineCache, stats, tokenEstimate, truncated }
|
|
436
465
|
}
|
|
437
466
|
|
|
438
467
|
/** granularity:entry 模式:每 entry 一行 TurnBrief,不聚合,仅总预算截断。 */
|
|
@@ -486,6 +515,7 @@ function renderEntryGranularity(
|
|
|
486
515
|
}
|
|
487
516
|
return {
|
|
488
517
|
turns: briefs,
|
|
518
|
+
lines,
|
|
489
519
|
stats,
|
|
490
520
|
tokenEstimate: Math.ceil(totalChars / CHARS_PER_TOKEN),
|
|
491
521
|
truncated,
|
|
@@ -568,7 +598,7 @@ function stripThinking(content: unknown): unknown {
|
|
|
568
598
|
|
|
569
599
|
/**
|
|
570
600
|
* L3 detail 默认摘要态的 toolResult entry(v2 O3:toolResult 不再整条消失,给中间态)。
|
|
571
|
-
* includeToolResult:true 时 renderDetail
|
|
601
|
+
* includeToolResult:true 时 renderDetail 直接 push 原 Entry(全文态真实路径),否则返回此摘要。
|
|
572
602
|
*/
|
|
573
603
|
export interface ToolResultSummaryEntry {
|
|
574
604
|
type: 'toolResultSummary'
|
|
@@ -580,8 +610,6 @@ export interface ToolResultSummaryEntry {
|
|
|
580
610
|
headLines: string
|
|
581
611
|
/** 结果文本总行数 */
|
|
582
612
|
totalLines: number
|
|
583
|
-
/** 原 toolResult entry(includeToolResult:true 时 renderDetail 改用此返回全文) */
|
|
584
|
-
fullEntry: Entry
|
|
585
613
|
}
|
|
586
614
|
|
|
587
615
|
export function renderDetail(
|
|
@@ -620,7 +648,6 @@ export function renderDetail(
|
|
|
620
648
|
summary: formatToolResultSummary(msg.toolName, tc, text),
|
|
621
649
|
headLines,
|
|
622
650
|
totalLines: lines.length,
|
|
623
|
-
fullEntry: e,
|
|
624
651
|
})
|
|
625
652
|
}
|
|
626
653
|
continue
|
package/src/core/toolcall.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* parser.ts 已 additive 透出这两个字段,O2/O3 据此精确关联取参数。
|
|
10
10
|
*/
|
|
11
11
|
import type { Entry } from './parser.js'
|
|
12
|
+
import { BYTES_PER_KB } from './units.js'
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// 模块常量(参数摘要的截断宽度 / 换算基数)
|
|
@@ -20,8 +21,6 @@ const BASH_CMD_MAX_CHARS = 60
|
|
|
20
21
|
const SUBAGENT_TASK_MAX_CHARS = 40
|
|
21
22
|
/** 未知工具 arguments JSON 摘要截断字符数。 */
|
|
22
23
|
const ARGS_JSON_MAX_CHARS = 50
|
|
23
|
-
/** bytes→KB 换算基数(write content 的 KB 显示)。 */
|
|
24
|
-
const BYTES_PER_KB = 1024
|
|
25
24
|
|
|
26
25
|
/** 单次工具调用信息(从 assistant content 的 toolCall block 提取)。 */
|
|
27
26
|
export interface ToolCallInfo {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字节量纲换算基数(本包内 KB/MB 换算的唯一来源)。
|
|
3
|
+
*
|
|
4
|
+
* 本包是独立 npm 发布的 pi extension(仅依赖 pi-ext-guards),不 import taiji 内部
|
|
5
|
+
* workspace 包(@taiji/shared 的 BYTES_PER_MB 单点不含本包)——量纲基数本地定义,
|
|
6
|
+
* 数值对齐由注释锚定(1MB = 1024×1024)。三个消费域(render 的 omitted 标记 /
|
|
7
|
+
* toolcall 的 write 摘要 / search-across 的预算与人性化格式化)共用本单点,
|
|
8
|
+
* 避免同值常量在本包内多份分裂。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** bytes→KB 换算基数(omitted 字节 / read 结果规模 / write content 的 KB 显示)。 */
|
|
12
|
+
export const BYTES_PER_KB = 1024
|
|
13
|
+
/** bytes→MB 换算基数(跨会话检索预算与人性化格式化)。 */
|
|
14
|
+
export const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB
|
package/src/core/workflow.ts
CHANGED
|
@@ -11,25 +11,20 @@
|
|
|
11
11
|
// 升版连带编译期影响本扩展;且上游类型只描述 NEW,OLD 仍需自处理(TC-wf-snapshot-version-union)。
|
|
12
12
|
// session-reader 作为纯读取者,按字段存在性 + v 标记分支做「结构化快照」式解析,与上游解耦。
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
import { isRecord } from '@zhushanwen/pi-ext-guards'
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
18
|
-
return typeof v === 'object' && v !== null
|
|
19
|
-
}
|
|
16
|
+
// ---- 类型守卫 helpers ----
|
|
20
17
|
|
|
21
18
|
/** unknown → string 收窄(非 string → undefined)。快照可选字符串字段的统一入口。 */
|
|
22
19
|
function strOr(v: unknown): string | undefined {
|
|
23
20
|
return typeof v === 'string' ? v : undefined
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
/** 字符串截断(超 max
|
|
23
|
+
/** 字符串截断(超 max 加省略号)。step 行渲染用。 */
|
|
27
24
|
function truncate(s: string, max: number): string {
|
|
28
25
|
return s.length <= max ? s : s.slice(0, max) + '…'
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
/** contentPreview 截断长度(概览预览,全文走 detail)。 */
|
|
32
|
-
const CONTENT_PREVIEW_MAX = 120
|
|
33
28
|
/** step 行 call sessionId 显示截断长度(uuid 前缀段,LLM 可读)。 */
|
|
34
29
|
const SESSION_ID_PREVIEW_MAX = 12
|
|
35
30
|
|
|
@@ -61,8 +56,6 @@ export interface WorkflowStep {
|
|
|
61
56
|
description?: string
|
|
62
57
|
/** NEW call.opts.model */
|
|
63
58
|
model?: string
|
|
64
|
-
/** NEW call.opts.thinkingLevel */
|
|
65
|
-
thinkingLevel?: string
|
|
66
59
|
/** NEW call.attempts / OLD 无 → undefined */
|
|
67
60
|
attempts?: number
|
|
68
61
|
/** NEW call.result.durationMs / OLD value.result.durationMs */
|
|
@@ -71,8 +64,6 @@ export interface WorkflowStep {
|
|
|
71
64
|
sessionId?: string
|
|
72
65
|
/** call.sessionFile 或 result.sessionFile(LLM 跳 outline/detail 的绝对路径入口,OLD 多数为 undefined) */
|
|
73
66
|
sessionFile?: string
|
|
74
|
-
/** result.content 截断前 120 字(概览预览,全文走 detail) */
|
|
75
|
-
contentPreview?: string
|
|
76
67
|
}
|
|
77
68
|
|
|
78
69
|
/** workflow run 概览(parseRunSnapshot 输出 / renderWorkflowOverview 输入)。 */
|
|
@@ -83,7 +74,7 @@ export interface WorkflowOverview {
|
|
|
83
74
|
stateFile: string
|
|
84
75
|
/** NEW state.status / OLD 顶层 status */
|
|
85
76
|
status: string
|
|
86
|
-
/**
|
|
77
|
+
/** details 格式标记(随 details.runs 透传给程序化消费方,文本渲染不读)。v2 读取面形状与 v1 一致(pi-subagent-workflow 8.x 一次性生命周期) */
|
|
87
78
|
version: 'wf-run-v1' | 'wf-run-v2' | 'legacy'
|
|
88
79
|
/** NEW spec.scriptName / spec.name / OLD name */
|
|
89
80
|
script?: string
|
|
@@ -138,19 +129,16 @@ function mapCallToStep(call: unknown, fallbackIndex: number): WorkflowStep {
|
|
|
138
129
|
const index = typeof call.id === 'number' ? call.id : fallbackIndex
|
|
139
130
|
const status = normalizeCallStatus(strOr(call.status) ?? '')
|
|
140
131
|
const refs = pickSessionRefs(call, result)
|
|
141
|
-
const content = strOr(result.content)
|
|
142
132
|
|
|
143
133
|
return {
|
|
144
134
|
index,
|
|
145
135
|
status,
|
|
146
136
|
description: strOr(opts.description),
|
|
147
137
|
model: strOr(opts.model),
|
|
148
|
-
thinkingLevel: strOr(opts.thinkingLevel),
|
|
149
138
|
attempts: typeof call.attempts === 'number' ? call.attempts : undefined,
|
|
150
139
|
durationMs: typeof result.durationMs === 'number' ? result.durationMs : undefined,
|
|
151
140
|
sessionId: refs.sessionId,
|
|
152
141
|
sessionFile: refs.sessionFile,
|
|
153
|
-
contentPreview: content !== undefined ? truncate(content, CONTENT_PREVIEW_MAX) : undefined,
|
|
154
142
|
}
|
|
155
143
|
}
|
|
156
144
|
|
|
@@ -162,11 +150,11 @@ function mapCacheEntryToStep(entry: unknown, index: number): WorkflowStep {
|
|
|
162
150
|
|
|
163
151
|
// sessionFile/sessionId: value.sessionFile 或 value.result.sessionFile(OLD 多数缺失,探针 112 文件 0)
|
|
164
152
|
const refs = pickSessionRefs(value, result)
|
|
165
|
-
// content:真实 OLD 数据 value.content(wf-skip-ok)与测试 fixture value.result.content
|
|
153
|
+
// content:真实 OLD 数据 value.content(wf-skip-ok)与测试 fixture value.result.content 并存,
|
|
154
|
+
// 仅用于 status 推测(A4 死字段删除:contentPreview 已删,不再随 step 输出)
|
|
166
155
|
const content = strOr(result.content) ?? strOr(value.content)
|
|
167
156
|
// OLD 无 status 字段:有 sessionFile 或非空 content → done,否则 pending
|
|
168
|
-
//(空 content 如 wf-skip-ok 的 '' 不算完成标志,对齐 TC-w5-parse-old expected status='pending'
|
|
169
|
-
// content 仍提取为 contentPreview='')
|
|
157
|
+
//(空 content 如 wf-skip-ok 的 '' 不算完成标志,对齐 TC-w5-parse-old expected status='pending')
|
|
170
158
|
const hasContent = content !== undefined && content.length > 0
|
|
171
159
|
const status: WorkflowStep['status'] =
|
|
172
160
|
refs.sessionFile !== undefined || hasContent ? 'done' : 'pending'
|
|
@@ -177,7 +165,6 @@ function mapCacheEntryToStep(entry: unknown, index: number): WorkflowStep {
|
|
|
177
165
|
durationMs: typeof result.durationMs === 'number' ? result.durationMs : undefined,
|
|
178
166
|
sessionId: refs.sessionId,
|
|
179
167
|
sessionFile: refs.sessionFile,
|
|
180
|
-
contentPreview: content !== undefined ? truncate(content, CONTENT_PREVIEW_MAX) : undefined,
|
|
181
168
|
}
|
|
182
169
|
}
|
|
183
170
|
|
package/src/discovery/env.ts
CHANGED
|
@@ -12,35 +12,35 @@ import { dirname } from 'node:path'
|
|
|
12
12
|
* 齐备),不 import roots.ts 的信号类型,避免并行单元耦合;接线层分别组装两种信号包。
|
|
13
13
|
*
|
|
14
14
|
* 信号可靠性依据(design §4.4 活体实测表,非推断):
|
|
15
|
-
* -
|
|
16
|
-
* ENV_WHITELIST_PREFIXES 含裸 `
|
|
15
|
+
* - TAIJI_AGENT_EXT_LOG 单独使用可靠性「中」——runtime 无条件注入,但
|
|
16
|
+
* ENV_WHITELIST_PREFIXES 含裸 `TAIJI_` 前缀,用户 shell 里的同名变量会透传进任何进程;
|
|
17
17
|
* - PI_CODING_AGENT_DIR 单独使用可靠性「中」——pi 原生变量,裸 pi 用户也可能自设,
|
|
18
|
-
* 但其值形态是
|
|
19
|
-
* -
|
|
18
|
+
* 但其值形态是 taiji 专属约定:`<*>/.taiji*` 目录后接 `/agent`;
|
|
19
|
+
* - TAIJI_AGENT_PACKAGED **不可用**——在 SPAWN_ENV_OUTBOUND_DENY_LIST 被剥除
|
|
20
20
|
* (§4.4 在案结论),故不引入该信号,防后人重蹈。
|
|
21
21
|
* 因此托管判定必须是双信号合取:任一信号单独成立只判 standalone-pi,
|
|
22
22
|
* 透传/自设污染提示写入 evidence。
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
export const
|
|
27
|
-
/**
|
|
25
|
+
/** taiji 托管信号 1:runtime 无条件注入的 extension 日志开关(恒 '1') */
|
|
26
|
+
export const TAIJI_AGENT_EXT_LOG_ENV = 'TAIJI_AGENT_EXT_LOG'
|
|
27
|
+
/** taiji 托管信号 2:pi agentDir 注入变量;其**值形态**是 taiji 专属约定 */
|
|
28
28
|
export const PI_CODING_AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR'
|
|
29
29
|
/** 数据目录推导首选:runtime getDataDir() 的权威注入值 */
|
|
30
|
-
export const
|
|
30
|
+
export const TAIJI_AGENT_DATA_DIR_ENV = 'TAIJI_AGENT_DATA_DIR'
|
|
31
31
|
|
|
32
32
|
/** 托管信号 1 的唯一合法值(注释「托管语义恒为 '1'」) */
|
|
33
33
|
const EXT_LOG_MANAGED_VALUE = '1'
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
// - B 后新布局:`<*>/.
|
|
37
|
-
// - B 前旧布局:`<*>/.
|
|
38
|
-
// `.
|
|
39
|
-
// 三条正则都要求匹配到路径末尾(`$`),调用前输入先经
|
|
35
|
+
// taiji agentDir 值形态(§6.2 双形态兼容——迁移窗口期 B 前后两种布局并存):
|
|
36
|
+
// - B 后新布局:`<*>/.taiji*` 目录后接 `/agent`(一层)
|
|
37
|
+
// - B 前旧布局:`<*>/.taiji*` 目录后接 `/pi/agent`(两层)
|
|
38
|
+
// `.taiji*` = 以 `.taiji` 开头的目录段(prod `.taiji` / dev `.taiji-dev`)。
|
|
39
|
+
// 三条正则都要求匹配到路径末尾(`$`),调用前输入先经 normalizeTaijiPath 剥尾斜杠。
|
|
40
40
|
// (用行注释:形态文本含星号斜杠序列,写进块注释会提前终止注释。)
|
|
41
|
-
const AGENT_DIR_SHAPE_ANY = /(^|\/)\.
|
|
42
|
-
const AGENT_DIR_SHAPE_OLD = /(^|\/)\.
|
|
43
|
-
const AGENT_DIR_SHAPE_NEW = /(^|\/)\.
|
|
41
|
+
const AGENT_DIR_SHAPE_ANY = /(^|\/)\.taiji[^/]*\/(pi\/)?agent$/
|
|
42
|
+
const AGENT_DIR_SHAPE_OLD = /(^|\/)\.taiji[^/]*\/pi\/agent$/
|
|
43
|
+
const AGENT_DIR_SHAPE_NEW = /(^|\/)\.taiji[^/]*\/agent$/
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* 打包形态判据(§6.2:bundle 路径判发行形态;无法判定 → null 不猜)。
|
|
@@ -56,7 +56,7 @@ const MACOS_PACKAGED_PATTERN = /\.app\/Contents\/Resources\/extensions\//
|
|
|
56
56
|
const REPO_STAGED_PATTERN = /(^|\/)apps\/electron\/resources\/extensions\//
|
|
57
57
|
const WIN_LINUX_PACKAGED_PATTERN = /(^|\/)resources\/extensions\//
|
|
58
58
|
|
|
59
|
-
export type EnvironmentKind = '
|
|
59
|
+
export type EnvironmentKind = 'taiji' | 'standalone-pi'
|
|
60
60
|
export type Distribution = 'packaged' | 'dev' | null
|
|
61
61
|
|
|
62
62
|
export interface EnvironmentSignals {
|
|
@@ -71,19 +71,19 @@ export interface EnvironmentSignals {
|
|
|
71
71
|
export interface DetectedEnvironment {
|
|
72
72
|
kind: EnvironmentKind
|
|
73
73
|
distribution: Distribution
|
|
74
|
-
/** 托管时的
|
|
74
|
+
/** 托管时的 taiji 数据目录;standalone-pi 恒 undefined */
|
|
75
75
|
dataDir?: string
|
|
76
76
|
/** 每条命中/未命中信号的原文 + 判定依据,恒非空(§6.2:判定可被证据推翻,非黑盒断言) */
|
|
77
77
|
evidence: string[]
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/** 形态匹配前的归一化:Windows 分隔符 → `/`,剥尾部斜杠 */
|
|
81
|
-
function
|
|
81
|
+
function normalizeTaijiPath(p: string): string {
|
|
82
82
|
return p.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function
|
|
86
|
-
return AGENT_DIR_SHAPE_ANY.test(
|
|
85
|
+
function matchesTaijiAgentDir(value: string): boolean {
|
|
86
|
+
return AGENT_DIR_SHAPE_ANY.test(normalizeTaijiPath(value))
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** distribution 判定,判定行写入 evidence */
|
|
@@ -110,19 +110,19 @@ function detectDistribution(bundleUrl: string | undefined, evidence: string[]):
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* 托管时的数据目录推导:
|
|
113
|
+
* 托管时的数据目录推导:TAIJI_AGENT_DATA_DIR 优先(runtime 注入的权威值,与
|
|
114
114
|
* getDataDir() 同源),缺失时按布局形态从 PI_CODING_AGENT_DIR 剥层。来源写入 evidence。
|
|
115
115
|
*/
|
|
116
116
|
function deriveDataDir(env: Record<string, string | undefined>, evidence: string[]): string | undefined {
|
|
117
|
-
const dataDirEnv = env[
|
|
117
|
+
const dataDirEnv = env[TAIJI_AGENT_DATA_DIR_ENV]
|
|
118
118
|
if (dataDirEnv) {
|
|
119
|
-
evidence.push(`数据目录:'${dataDirEnv}'(来源:${
|
|
119
|
+
evidence.push(`数据目录:'${dataDirEnv}'(来源:${TAIJI_AGENT_DATA_DIR_ENV})`)
|
|
120
120
|
return dataDirEnv
|
|
121
121
|
}
|
|
122
122
|
const agentDirEnv = env[PI_CODING_AGENT_DIR_ENV]
|
|
123
123
|
if (agentDirEnv) {
|
|
124
|
-
const normalized =
|
|
125
|
-
// 旧形态更长,先判:`.
|
|
124
|
+
const normalized = normalizeTaijiPath(agentDirEnv)
|
|
125
|
+
// 旧形态更长,先判:`.taiji*/pi/agent` → 剥 pi/agent 两层
|
|
126
126
|
if (AGENT_DIR_SHAPE_OLD.test(normalized)) {
|
|
127
127
|
const dataDir = dirname(dirname(normalized))
|
|
128
128
|
evidence.push(`数据目录:'${dataDir}'(来源:${PI_CODING_AGENT_DIR_ENV} 剥 pi/agent 两层,B 前旧布局)`)
|
|
@@ -134,7 +134,7 @@ function deriveDataDir(env: Record<string, string | undefined>, evidence: string
|
|
|
134
134
|
return dataDir
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
-
evidence.push(`数据目录:无法推导(${
|
|
137
|
+
evidence.push(`数据目录:无法推导(${TAIJI_AGENT_DATA_DIR_ENV} 未设置且 ${PI_CODING_AGENT_DIR_ENV} 形态不可剥层)`)
|
|
138
138
|
return undefined
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -146,16 +146,16 @@ function pushExtLogEvidence(
|
|
|
146
146
|
): void {
|
|
147
147
|
evidence.push(
|
|
148
148
|
extLogHit
|
|
149
|
-
? `${
|
|
150
|
-
: `${
|
|
149
|
+
? `${TAIJI_AGENT_EXT_LOG_ENV}='${extLog}' → 命中`
|
|
150
|
+
: `${TAIJI_AGENT_EXT_LOG_ENV}=${extLog === undefined ? '<未设置>' : `'${extLog}'`} → 未命中(要求值为 '1')`,
|
|
151
151
|
)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/** 信号 2 的形态标注(§6.2 双形态兼容:B 前旧布局 / B 后新布局 / 非
|
|
154
|
+
/** 信号 2 的形态标注(§6.2 双形态兼容:B 前旧布局 / B 后新布局 / 非 taiji 形态)。 */
|
|
155
155
|
function describeAgentDirShape(normalized: string): string {
|
|
156
156
|
if (AGENT_DIR_SHAPE_OLD.test(normalized)) return 'B 前旧布局 pi/agent'
|
|
157
157
|
if (AGENT_DIR_SHAPE_NEW.test(normalized)) return 'B 后新布局 agent'
|
|
158
|
-
return '非
|
|
158
|
+
return '非 taiji 形态'
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/** 信号 2 evidence 行(未设置 → 未命中;有值 → 形态匹配与否 + 形态标注)。 */
|
|
@@ -168,19 +168,19 @@ function pushAgentDirEvidence(
|
|
|
168
168
|
evidence.push(`${PI_CODING_AGENT_DIR_ENV}=<未设置> → 未命中`)
|
|
169
169
|
return
|
|
170
170
|
}
|
|
171
|
-
const shape = describeAgentDirShape(
|
|
171
|
+
const shape = describeAgentDirShape(normalizeTaijiPath(agentDirEnv))
|
|
172
172
|
evidence.push(
|
|
173
|
-
`${PI_CODING_AGENT_DIR_ENV}='${agentDirEnv}' → ${agentDirHit ? `形态匹配(${shape})` : `形态不匹配(${shape},要求 <*>/.
|
|
173
|
+
`${PI_CODING_AGENT_DIR_ENV}='${agentDirEnv}' → ${agentDirHit ? `形态匹配(${shape})` : `形态不匹配(${shape},要求 <*>/.taiji*/agent)`}`,
|
|
174
174
|
)
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/** 判定行:托管(双信号合取成立)/ standalone-pi(孤置信号点名,提示透传/自设污染)。 */
|
|
178
178
|
function judgedEvidenceLine(extLogHit: boolean, agentDirHit: boolean): string {
|
|
179
179
|
if (extLogHit && agentDirHit) {
|
|
180
|
-
return `判定:
|
|
180
|
+
return `判定:taiji 托管(${TAIJI_AGENT_EXT_LOG_ENV}='1' 且 ${PI_CODING_AGENT_DIR_ENV} 形态匹配,双信号合取成立)`
|
|
181
181
|
}
|
|
182
182
|
const orphaned: string[] = []
|
|
183
|
-
if (extLogHit) orphaned.push(
|
|
183
|
+
if (extLogHit) orphaned.push(TAIJI_AGENT_EXT_LOG_ENV)
|
|
184
184
|
if (agentDirHit) orphaned.push(PI_CODING_AGENT_DIR_ENV)
|
|
185
185
|
const orphanNote =
|
|
186
186
|
orphaned.length > 0
|
|
@@ -192,8 +192,8 @@ function judgedEvidenceLine(extLogHit: boolean, agentDirHit: boolean): string {
|
|
|
192
192
|
/**
|
|
193
193
|
* 判定当前宿主环境(design §6.2)。
|
|
194
194
|
*
|
|
195
|
-
* 托管(
|
|
196
|
-
* 值匹配
|
|
195
|
+
* 托管(taiji)= 合取:`TAIJI_AGENT_EXT_LOG === '1'` **且** `PI_CODING_AGENT_DIR`
|
|
196
|
+
* 值匹配 taiji 形态(`<*>/.taiji*` 目录后接 `/agent`;B 前旧布局接
|
|
197
197
|
* `/pi/agent` 同样接受)。任一信号单独成立 → standalone-pi,evidence 点名孤置
|
|
198
198
|
* 信号(透传污染提示)。
|
|
199
199
|
* evidence 恒非空:每条命中/未命中信号的原文都在列,让 agent 能核对并推翻判定。
|
|
@@ -205,19 +205,19 @@ export function detectEnvironment(signals: EnvironmentSignals): DetectedEnvironm
|
|
|
205
205
|
const evidence: string[] = []
|
|
206
206
|
const { env } = signals
|
|
207
207
|
|
|
208
|
-
// 信号 1:
|
|
209
|
-
const extLog = env[
|
|
208
|
+
// 信号 1:TAIJI_AGENT_EXT_LOG === '1'
|
|
209
|
+
const extLog = env[TAIJI_AGENT_EXT_LOG_ENV]
|
|
210
210
|
const extLogHit = extLog === EXT_LOG_MANAGED_VALUE
|
|
211
211
|
pushExtLogEvidence(evidence, extLog, extLogHit)
|
|
212
212
|
|
|
213
213
|
// 信号 2:PI_CODING_AGENT_DIR 值形态
|
|
214
214
|
const agentDirEnv = env[PI_CODING_AGENT_DIR_ENV]
|
|
215
|
-
const agentDirHit = agentDirEnv !== undefined &&
|
|
215
|
+
const agentDirHit = agentDirEnv !== undefined && matchesTaijiAgentDir(agentDirEnv)
|
|
216
216
|
pushAgentDirEvidence(evidence, agentDirEnv, agentDirHit)
|
|
217
217
|
evidence.push(`agentDir='${signals.agentDir}' → pi getAgentDir() 透传记录`)
|
|
218
218
|
|
|
219
219
|
const managed = extLogHit && agentDirHit
|
|
220
|
-
const kind: EnvironmentKind = managed ? '
|
|
220
|
+
const kind: EnvironmentKind = managed ? 'taiji' : 'standalone-pi'
|
|
221
221
|
evidence.push(judgedEvidenceLine(extLogHit, agentDirHit))
|
|
222
222
|
|
|
223
223
|
const distribution = detectDistribution(signals.bundleUrl, evidence)
|