dsh-expert-group 0.1.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/LICENSE +38 -0
- package/README.md +355 -0
- package/THIRD_PARTY_NOTICES.md +35 -0
- package/assets/notification-whoosh.wav +0 -0
- package/bin/prompt-inspector.mjs +63 -0
- package/client-bundle.js +14891 -0
- package/client-expert-group-model.js +426 -0
- package/client-expert-group-sidebar.js +5401 -0
- package/client-freshness.js +364 -0
- package/client-model-config.js +2155 -0
- package/client-notifications.js +38 -0
- package/client.js +6487 -0
- package/cordis.patch.yml +10 -0
- package/fanout-host.js +1507 -0
- package/fanout.js +2981 -0
- package/host/composition.js +155 -0
- package/host/expert-group-runtime.js +822 -0
- package/host/expert-group-snapshot.js +450 -0
- package/host/fanout-runtime.js +822 -0
- package/host/handoff.js +80 -0
- package/host/lineage.js +274 -0
- package/host/notifications.js +189 -0
- package/host/persistence-health.js +389 -0
- package/host/recovery.js +242 -0
- package/host/rpc-transport.js +573 -0
- package/host/settings-runtime.js +168 -0
- package/host/settings-source.js +265 -0
- package/host/storage-binding.js +94 -0
- package/host/system-toast.js +916 -0
- package/image-routing.js +263 -0
- package/index.js +9327 -0
- package/install.ps1 +219 -0
- package/orchestrator.js +239 -0
- package/package.json +142 -0
- package/prompt-contracts.js +148 -0
- package/prompt-inspector.js +584 -0
- package/prompt-sidecar.js +610 -0
- package/resource-budget.js +540 -0
- package/role-registry.js +286 -0
- package/rpc-host.js +310 -0
- package/scripts/assemble-client.mjs +605 -0
- package/scripts/build.mjs +124 -0
- package/shared.js +4003 -0
- package/storage.js +190 -0
- package/uninstall.ps1 +47 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地 session.jsonl 的 Prompt Inspector 纯函数。
|
|
3
|
+
*
|
|
4
|
+
* 该模块不依赖 DSH runtime,也不会修改输入事件;只保留提示词诊断所需的
|
|
5
|
+
* 稳定元数据。完整 prompt 仅在返回记录的 text/prompt 字段中保留,sidecar
|
|
6
|
+
* manifest 只保存长度、hash 与分块引用。
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
|
|
10
|
+
const UTF8 = new TextEncoder();
|
|
11
|
+
const FATAL_UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
12
|
+
// 记录层保留 descriptor 的可诊断元数据;写入 manifest 前由 sidecar 再做严格白名单过滤。
|
|
13
|
+
const SAFE_METADATA_KEYS = new Set([
|
|
14
|
+
"provider",
|
|
15
|
+
"model",
|
|
16
|
+
"reasoningEffort",
|
|
17
|
+
"maxTokens",
|
|
18
|
+
"temperature",
|
|
19
|
+
"topP",
|
|
20
|
+
"role",
|
|
21
|
+
"label",
|
|
22
|
+
"name",
|
|
23
|
+
"taskId",
|
|
24
|
+
"agentId",
|
|
25
|
+
"sessionId",
|
|
26
|
+
"parentSession",
|
|
27
|
+
"origin",
|
|
28
|
+
"delegationDepth",
|
|
29
|
+
"route",
|
|
30
|
+
"routeKey",
|
|
31
|
+
"status",
|
|
32
|
+
]);
|
|
33
|
+
const SENSITIVE_KEY = /(?:authorization|cookie|password|secret|credential|encrypted|cipher|private|api.?key|access.?token|refresh.?token)/i;
|
|
34
|
+
const DISPLAY_TRUNCATION = /(?:\(\s*(?:line|output|content|read)\s+truncated(?:\s+to)?[^)]*\)|\[\s*(?:line|output|content|read)\s+truncated[^\]]*\])/i;
|
|
35
|
+
const EXPLICIT_TRUNCATION = /(?:\bTRUNCAT(?:ED|ION)\b|\[\s*截断\s*\]|\bPROMPT\s+TRUNCATED\b|内容已截断|提示词已截断|已截断)/iu;
|
|
36
|
+
const ELLIPSIS = /(?:\.{3}|…{1,3})/u;
|
|
37
|
+
const GRAPHEME_SEGMENTER = typeof Intl?.Segmenter === "function"
|
|
38
|
+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
function asBytes(input) {
|
|
42
|
+
if (Buffer.isBuffer(input)) return input;
|
|
43
|
+
if (typeof input === "string") return Buffer.from(input, "utf8");
|
|
44
|
+
throw new TypeError("parseJsonlEvents(input) 只接受 Buffer 或 string");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function decodeLine(bytes) {
|
|
48
|
+
try {
|
|
49
|
+
return FATAL_UTF8.decode(bytes);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// 保留可诊断 raw;同时把非法 UTF-8 作为该行 parseError,不吞掉后续行。
|
|
52
|
+
return { raw: new TextDecoder().decode(bytes), error };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 按 UTF-8 字节逐行解析 JSONL。byteLength 不包含换行符,byteOffset 指向该行起点。
|
|
58
|
+
* 每一行都返回,即使 JSON.parse 失败也不会影响后续行。
|
|
59
|
+
*/
|
|
60
|
+
export function parseJsonlEvents(input) {
|
|
61
|
+
const bytes = asBytes(input);
|
|
62
|
+
if (bytes.length === 0) return [];
|
|
63
|
+
const events = [];
|
|
64
|
+
let lineStart = 0;
|
|
65
|
+
let lineNumber = 1;
|
|
66
|
+
|
|
67
|
+
const consume = (lineEnd) => {
|
|
68
|
+
let contentEnd = lineEnd;
|
|
69
|
+
if (contentEnd > lineStart && bytes[contentEnd - 1] === 0x0d) contentEnd -= 1;
|
|
70
|
+
const lineBytes = bytes.subarray(lineStart, contentEnd);
|
|
71
|
+
const decoded = decodeLine(lineBytes);
|
|
72
|
+
let raw;
|
|
73
|
+
let parseError;
|
|
74
|
+
let value;
|
|
75
|
+
if (typeof decoded === "string") {
|
|
76
|
+
raw = decoded;
|
|
77
|
+
if (raw.length > 0) {
|
|
78
|
+
try {
|
|
79
|
+
value = JSON.parse(raw);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
parseError = error;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
parseError = new SyntaxError("空 JSONL 行");
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
raw = decoded.raw;
|
|
88
|
+
parseError = new SyntaxError("该 JSONL 行不是合法 UTF-8", { cause: decoded.error });
|
|
89
|
+
}
|
|
90
|
+
events.push({
|
|
91
|
+
lineNumber,
|
|
92
|
+
byteOffset: lineStart,
|
|
93
|
+
byteLength: lineBytes.length,
|
|
94
|
+
raw,
|
|
95
|
+
value,
|
|
96
|
+
parseError,
|
|
97
|
+
});
|
|
98
|
+
lineStart = lineEnd + 1;
|
|
99
|
+
lineNumber += 1;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
103
|
+
if (bytes[index] === 0x0a) consume(index);
|
|
104
|
+
}
|
|
105
|
+
if (lineStart < bytes.length) consume(bytes.length);
|
|
106
|
+
return events;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function eventValue(event) {
|
|
110
|
+
if (event !== null && typeof event === "object" && Object.prototype.hasOwnProperty.call(event, "value")) {
|
|
111
|
+
return event.value;
|
|
112
|
+
}
|
|
113
|
+
return event;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function objectOf(value) {
|
|
117
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function firstString(...values) {
|
|
121
|
+
for (const value of values) {
|
|
122
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function firstValue(...values) {
|
|
128
|
+
for (const value of values) {
|
|
129
|
+
if (value !== undefined && value !== null) return value;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function eventType(value) {
|
|
135
|
+
return String(firstValue(value?.type, value?.eventType, value?.event, value?.kind, ""))
|
|
136
|
+
.replaceAll("\\", "/")
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sessionObject(value) {
|
|
141
|
+
return objectOf(firstValue(value?.session, value?.agent?.session, value?.data?.session));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function sessionIdOf(value) {
|
|
145
|
+
const session = sessionObject(value);
|
|
146
|
+
const data = objectOf(value?.data);
|
|
147
|
+
const header = objectOf(data?.header) ?? objectOf(value?.header) ?? objectOf(session?.header);
|
|
148
|
+
const descriptor = objectOf(firstValue(data?.descriptor, value?.descriptor));
|
|
149
|
+
const candidate = firstValue(
|
|
150
|
+
value?.sessionId,
|
|
151
|
+
session?.id,
|
|
152
|
+
data?.sessionId,
|
|
153
|
+
data?.id,
|
|
154
|
+
value?.id,
|
|
155
|
+
header?.sessionId,
|
|
156
|
+
header?.id,
|
|
157
|
+
descriptor?.sessionId,
|
|
158
|
+
descriptor?.id,
|
|
159
|
+
);
|
|
160
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function headerOf(value, data) {
|
|
164
|
+
const session = sessionObject(value);
|
|
165
|
+
return objectOf(firstValue(data?.header, value?.header, session?.header));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function descriptorOf(value, data) {
|
|
169
|
+
// 兼容顶层 descriptor、data.descriptor 和 descriptor 事件直接 data 三种导出形状。
|
|
170
|
+
return objectOf(firstValue(data?.descriptor, value?.descriptor, data));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function textValue(value) {
|
|
174
|
+
return typeof value === "string" ? value : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scalarMetadataValue(value) {
|
|
178
|
+
if (value === null || value === undefined) return undefined;
|
|
179
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function safeMetadata(sources) {
|
|
184
|
+
const metadata = {};
|
|
185
|
+
for (const source of sources) {
|
|
186
|
+
const object = objectOf(source);
|
|
187
|
+
if (object === undefined) continue;
|
|
188
|
+
for (const [key, rawValue] of Object.entries(object)) {
|
|
189
|
+
if (!SAFE_METADATA_KEYS.has(key) || SENSITIVE_KEY.test(key)) continue;
|
|
190
|
+
const value = scalarMetadataValue(rawValue);
|
|
191
|
+
if (value !== undefined && metadata[key] === undefined) metadata[key] = value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return metadata;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function promptRecord({ event, value, data, kind, text, sourceFile }) {
|
|
198
|
+
const session = sessionObject(value);
|
|
199
|
+
const header = headerOf(value, data);
|
|
200
|
+
const descriptor = descriptorOf(value, data);
|
|
201
|
+
const sessionId = sessionIdOf(value);
|
|
202
|
+
const parentSession = firstString(
|
|
203
|
+
value?.parentSession,
|
|
204
|
+
session?.parentSession,
|
|
205
|
+
header?.parentSession,
|
|
206
|
+
data?.parentSession,
|
|
207
|
+
descriptor?.parentSession,
|
|
208
|
+
);
|
|
209
|
+
const origin = firstString(value?.origin, session?.origin, header?.origin, data?.origin, descriptor?.origin);
|
|
210
|
+
const delegationDepth = firstValue(
|
|
211
|
+
value?.delegationDepth,
|
|
212
|
+
session?.delegationDepth,
|
|
213
|
+
header?.delegationDepth,
|
|
214
|
+
data?.delegationDepth,
|
|
215
|
+
descriptor?.delegationDepth,
|
|
216
|
+
);
|
|
217
|
+
const seq = firstValue(value?.seq, value?.sequence, data?.seq, header?.seq, descriptor?.seq, event?.seq);
|
|
218
|
+
const time = firstValue(value?.time, value?.timestamp, data?.time, header?.time, descriptor?.time, event?.time);
|
|
219
|
+
const metadata = safeMetadata([value, session, header, data, descriptor]);
|
|
220
|
+
if (sessionId !== undefined) metadata.sessionId = sessionId;
|
|
221
|
+
if (parentSession !== undefined) metadata.parentSession = parentSession;
|
|
222
|
+
if (origin !== undefined) metadata.origin = origin;
|
|
223
|
+
if (delegationDepth !== undefined) metadata.delegationDepth = delegationDepth;
|
|
224
|
+
return {
|
|
225
|
+
kind,
|
|
226
|
+
text,
|
|
227
|
+
prompt: text,
|
|
228
|
+
sourceFile: sourceFile ?? event?.sourceFile,
|
|
229
|
+
lineNumber: event?.lineNumber,
|
|
230
|
+
byteOffset: event?.byteOffset,
|
|
231
|
+
byteLength: event?.byteLength,
|
|
232
|
+
sessionId,
|
|
233
|
+
parentSession,
|
|
234
|
+
origin,
|
|
235
|
+
delegationDepth,
|
|
236
|
+
seq,
|
|
237
|
+
time,
|
|
238
|
+
metadata,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 从 request/header 的 data.header.system 与 subagent/descriptor 的 data.persona 提取完整文本。
|
|
244
|
+
* sourceFile 可通过第二参数提供;不会把原始 event/raw 带入返回记录。
|
|
245
|
+
*/
|
|
246
|
+
export function extractPromptRecords(events, { sourceFile } = {}) {
|
|
247
|
+
if (!Array.isArray(events)) throw new TypeError("extractPromptRecords(events) 需要事件数组");
|
|
248
|
+
const records = [];
|
|
249
|
+
const contexts = new Map();
|
|
250
|
+
let defaultSessionId;
|
|
251
|
+
|
|
252
|
+
const explicitContext = (value, data, header, descriptor, session) => ({
|
|
253
|
+
parentSession: firstString(value?.parentSession, session?.parentSession, data?.parentSession, header?.parentSession, descriptor?.parentSession),
|
|
254
|
+
origin: firstString(value?.origin, session?.origin, data?.origin, header?.origin, descriptor?.origin),
|
|
255
|
+
delegationDepth: firstValue(value?.delegationDepth, session?.delegationDepth, data?.delegationDepth, header?.delegationDepth, descriptor?.delegationDepth),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
for (const event of events) {
|
|
259
|
+
if (event?.parseError) continue;
|
|
260
|
+
const originalValue = eventValue(event);
|
|
261
|
+
const data = objectOf(originalValue?.data) ?? {};
|
|
262
|
+
const type = eventType(originalValue);
|
|
263
|
+
const header = headerOf(originalValue, data);
|
|
264
|
+
const descriptor = descriptorOf(originalValue, data);
|
|
265
|
+
const system = textValue(header?.system);
|
|
266
|
+
const persona = textValue(data.persona) ?? textValue(descriptor?.persona);
|
|
267
|
+
const ownSessionId = sessionIdOf(originalValue);
|
|
268
|
+
const session = sessionObject(originalValue);
|
|
269
|
+
const isSessionEvent = type === "session" || type.startsWith("session/");
|
|
270
|
+
const explicit = explicitContext(originalValue, data, header, descriptor, session);
|
|
271
|
+
|
|
272
|
+
// 文件首个 session 事件定义默认上下文;后续每个有 id 的事件独立维护自己的上下文。
|
|
273
|
+
if (isSessionEvent && ownSessionId !== undefined) {
|
|
274
|
+
if (defaultSessionId === undefined) defaultSessionId = ownSessionId;
|
|
275
|
+
const previous = contexts.get(ownSessionId) ?? { sessionId: ownSessionId };
|
|
276
|
+
contexts.set(ownSessionId, {
|
|
277
|
+
sessionId: ownSessionId,
|
|
278
|
+
parentSession: explicit.parentSession ?? previous.parentSession,
|
|
279
|
+
origin: explicit.origin ?? previous.origin,
|
|
280
|
+
delegationDepth: explicit.delegationDepth ?? previous.delegationDepth,
|
|
281
|
+
});
|
|
282
|
+
} else if (ownSessionId !== undefined) {
|
|
283
|
+
const previous = contexts.get(ownSessionId) ?? { sessionId: ownSessionId };
|
|
284
|
+
contexts.set(ownSessionId, {
|
|
285
|
+
sessionId: ownSessionId,
|
|
286
|
+
parentSession: explicit.parentSession ?? previous.parentSession,
|
|
287
|
+
origin: explicit.origin ?? previous.origin,
|
|
288
|
+
delegationDepth: explicit.delegationDepth ?? previous.delegationDepth,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const selectedSessionId = ownSessionId ?? defaultSessionId;
|
|
293
|
+
const selectedContext = selectedSessionId === undefined
|
|
294
|
+
? undefined
|
|
295
|
+
: (contexts.get(selectedSessionId) ?? { sessionId: selectedSessionId });
|
|
296
|
+
const inherited = {
|
|
297
|
+
sessionId: selectedContext?.sessionId,
|
|
298
|
+
parentSession: explicit.parentSession ?? selectedContext?.parentSession,
|
|
299
|
+
origin: explicit.origin ?? selectedContext?.origin,
|
|
300
|
+
delegationDepth: explicit.delegationDepth ?? selectedContext?.delegationDepth,
|
|
301
|
+
};
|
|
302
|
+
// 合并只用于当前 record;无 id 的 child 事件不会写回默认 root context。
|
|
303
|
+
const value = { ...originalValue, ...inherited };
|
|
304
|
+
const requestEvent = type === "request/header" || type.endsWith("/request/header") || (type === "request" && system !== undefined);
|
|
305
|
+
const descriptorEvent = type === "subagent/descriptor" || type.endsWith("/subagent/descriptor") || (type === "subagent" && persona !== undefined) || (persona !== undefined && type.includes("descriptor"));
|
|
306
|
+
if (requestEvent && system !== undefined) {
|
|
307
|
+
records.push(promptRecord({ event, value, data, kind: "system", text: system, sourceFile }));
|
|
308
|
+
}
|
|
309
|
+
if (descriptorEvent && persona !== undefined) {
|
|
310
|
+
records.push(promptRecord({ event, value, data, kind: "persona", text: persona, sourceFile }));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return records;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function normalizedSessionId(value) {
|
|
317
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 按 sessionId 建树。父节点缺失、self-parent 和 parent cycle 都保留为异常信息,
|
|
322
|
+
* 不把 origin=subagent 猜成某个父节点,也不让异常关系触发无限递归。
|
|
323
|
+
*/
|
|
324
|
+
export function buildAgentTree(records) {
|
|
325
|
+
if (!Array.isArray(records)) throw new TypeError("buildAgentTree(records) 需要记录数组");
|
|
326
|
+
const bySession = new Map();
|
|
327
|
+
const unattachedRecords = [];
|
|
328
|
+
for (const record of records) {
|
|
329
|
+
const sessionId = normalizedSessionId(record?.sessionId);
|
|
330
|
+
if (sessionId === undefined) {
|
|
331
|
+
unattachedRecords.push(record);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
let node = bySession.get(sessionId);
|
|
335
|
+
if (node === undefined) {
|
|
336
|
+
node = {
|
|
337
|
+
sessionId,
|
|
338
|
+
parentSession: undefined,
|
|
339
|
+
origin: undefined,
|
|
340
|
+
delegationDepth: undefined,
|
|
341
|
+
records: [],
|
|
342
|
+
children: [],
|
|
343
|
+
dangling: false,
|
|
344
|
+
external: false,
|
|
345
|
+
parentConflict: false,
|
|
346
|
+
cycle: false,
|
|
347
|
+
corruptLineage: false,
|
|
348
|
+
};
|
|
349
|
+
bySession.set(sessionId, node);
|
|
350
|
+
}
|
|
351
|
+
node.records.push(record);
|
|
352
|
+
const parent = normalizedSessionId(record.parentSession);
|
|
353
|
+
if (node.parentSession === undefined && parent !== undefined) node.parentSession = parent;
|
|
354
|
+
else if (parent !== undefined && node.parentSession !== parent) node.parentConflict = true;
|
|
355
|
+
if (node.origin === undefined && typeof record.origin === "string") node.origin = record.origin;
|
|
356
|
+
if (node.delegationDepth === undefined && record.delegationDepth !== undefined) node.delegationDepth = record.delegationDepth;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const nodes = [...bySession.values()];
|
|
360
|
+
const cycles = [];
|
|
361
|
+
const completed = new Set();
|
|
362
|
+
for (const start of nodes) {
|
|
363
|
+
if (completed.has(start.sessionId)) continue;
|
|
364
|
+
const path = [];
|
|
365
|
+
const positions = new Map();
|
|
366
|
+
let cursor = start;
|
|
367
|
+
while (cursor !== undefined && !completed.has(cursor.sessionId)) {
|
|
368
|
+
const previous = positions.get(cursor.sessionId);
|
|
369
|
+
if (previous !== undefined) {
|
|
370
|
+
const cycleNodes = path.slice(previous);
|
|
371
|
+
for (const cycleNode of cycleNodes) {
|
|
372
|
+
cycleNode.cycle = true;
|
|
373
|
+
cycleNode.corruptLineage = true;
|
|
374
|
+
}
|
|
375
|
+
cycles.push(cycleNodes);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
positions.set(cursor.sessionId, path.length);
|
|
379
|
+
path.push(cursor);
|
|
380
|
+
cursor = cursor.parentSession === undefined ? undefined : bySession.get(cursor.parentSession);
|
|
381
|
+
}
|
|
382
|
+
for (const node of path) completed.add(node.sessionId);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const roots = [];
|
|
386
|
+
const dangling = [];
|
|
387
|
+
const external = [];
|
|
388
|
+
for (const node of nodes) {
|
|
389
|
+
if (node.parentSession === undefined) {
|
|
390
|
+
// 没有 parentSession 且 origin=subagent 的会话身份不完整,不能冒充主代理。
|
|
391
|
+
if (node.origin === "subagent") {
|
|
392
|
+
node.external = true;
|
|
393
|
+
external.push(node);
|
|
394
|
+
} else {
|
|
395
|
+
roots.push(node);
|
|
396
|
+
}
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const parent = bySession.get(node.parentSession);
|
|
400
|
+
if (parent === undefined) {
|
|
401
|
+
node.dangling = true;
|
|
402
|
+
node.external = true;
|
|
403
|
+
node.corruptLineage = true;
|
|
404
|
+
dangling.push(node);
|
|
405
|
+
} else if (node.cycle || parent.cycle) {
|
|
406
|
+
// Cycle 边及其下游不挂载,避免 publicTree 递归;平铺异常集合仍完整保留。
|
|
407
|
+
if (!node.cycle) node.corruptLineage = true;
|
|
408
|
+
} else {
|
|
409
|
+
parent.children.push(node);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const node of nodes) {
|
|
413
|
+
if (node.parentConflict) node.corruptLineage = true;
|
|
414
|
+
node.records.sort((left, right) => (left.lineNumber ?? 0) - (right.lineNumber ?? 0));
|
|
415
|
+
node.children.sort((left, right) => left.sessionId.localeCompare(right.sessionId));
|
|
416
|
+
}
|
|
417
|
+
roots.sort((left, right) => left.sessionId.localeCompare(right.sessionId));
|
|
418
|
+
dangling.sort((left, right) => left.sessionId.localeCompare(right.sessionId));
|
|
419
|
+
external.sort((left, right) => left.sessionId.localeCompare(right.sessionId));
|
|
420
|
+
const corruptLineage = nodes.filter((node) => node.corruptLineage);
|
|
421
|
+
return { roots, nodes, bySession, dangling, external, cycles, corruptLineage, unattachedRecords };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function ensureUtf8Text(text, label = "text") {
|
|
425
|
+
if (typeof text !== "string") throw new TypeError(label + " 必须是 string");
|
|
426
|
+
// TextEncoder 会把孤立代理编码成替换字符,无法保证重组后得到原始 JS 字符串。
|
|
427
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
428
|
+
const code = text.charCodeAt(index);
|
|
429
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
430
|
+
const next = text.charCodeAt(index + 1);
|
|
431
|
+
if (next < 0xdc00 || next > 0xdfff) throw new TypeError(label + " 包含未配对 surrogate");
|
|
432
|
+
index += 1;
|
|
433
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
434
|
+
throw new TypeError(label + " 包含未配对 surrogate");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function utf8Bytes(text) {
|
|
440
|
+
ensureUtf8Text(text);
|
|
441
|
+
return UTF8.encode(text);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function validateMaxBytes(maxBytes) {
|
|
445
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
446
|
+
throw new RangeError("maxBytes 必须是大于 0 的安全整数");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** 按完整 UTF-8 code point 切分,byteOffset/byteLength 均以 UTF-8 字节计。 */
|
|
451
|
+
export function chunkUtf8(text, maxBytes) {
|
|
452
|
+
ensureUtf8Text(text);
|
|
453
|
+
validateMaxBytes(maxBytes);
|
|
454
|
+
if (text.length === 0) return [];
|
|
455
|
+
const chunks = [];
|
|
456
|
+
let currentText = "";
|
|
457
|
+
let currentBytes = 0;
|
|
458
|
+
let byteOffset = 0;
|
|
459
|
+
const graphemes = GRAPHEME_SEGMENTER === undefined
|
|
460
|
+
? [...text]
|
|
461
|
+
: [...GRAPHEME_SEGMENTER.segment(text)].map((segment) => segment.segment);
|
|
462
|
+
// grapheme cluster 可能由多个 code point 组成;超预算时退回 code point 粒度,
|
|
463
|
+
// 只有单个 code point 本身超预算才报错。
|
|
464
|
+
const segments = [];
|
|
465
|
+
for (const grapheme of graphemes) {
|
|
466
|
+
if (UTF8.encode(grapheme).length <= maxBytes) segments.push(grapheme);
|
|
467
|
+
else segments.push(...grapheme);
|
|
468
|
+
}
|
|
469
|
+
for (const segment of segments) {
|
|
470
|
+
const pointBytes = UTF8.encode(segment);
|
|
471
|
+
if (pointBytes.length > maxBytes) {
|
|
472
|
+
throw new RangeError("maxBytes 小于单个 UTF-8 code point(需要 " + pointBytes.length + " 字节)");
|
|
473
|
+
}
|
|
474
|
+
if (currentBytes > 0 && currentBytes + pointBytes.length > maxBytes) {
|
|
475
|
+
chunks.push({ index: chunks.length, byteOffset, byteLength: currentBytes, text: currentText });
|
|
476
|
+
byteOffset += currentBytes;
|
|
477
|
+
currentText = "";
|
|
478
|
+
currentBytes = 0;
|
|
479
|
+
}
|
|
480
|
+
currentText += segment;
|
|
481
|
+
currentBytes += pointBytes.length;
|
|
482
|
+
}
|
|
483
|
+
if (currentBytes > 0) chunks.push({ index: chunks.length, byteOffset, byteLength: currentBytes, text: currentText });
|
|
484
|
+
return chunks;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function chunkBytes(chunk, index) {
|
|
488
|
+
if (chunk?.bytes !== undefined) {
|
|
489
|
+
if (!(chunk.bytes instanceof Uint8Array) && !Buffer.isBuffer(chunk.bytes)) {
|
|
490
|
+
throw new TypeError("第 " + index + " 块 bytes 必须是 Uint8Array");
|
|
491
|
+
}
|
|
492
|
+
return new Uint8Array(chunk.bytes);
|
|
493
|
+
}
|
|
494
|
+
return utf8Bytes(chunk?.text);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** 严格校验 index、连续 byte offset/length,并以 fatal UTF-8 解码重组。 */
|
|
498
|
+
export function reassembleUtf8Chunks(chunks) {
|
|
499
|
+
if (!Array.isArray(chunks)) throw new TypeError("chunks 必须是数组");
|
|
500
|
+
let expectedOffset = 0;
|
|
501
|
+
const parts = [];
|
|
502
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
503
|
+
const chunk = chunks[index];
|
|
504
|
+
if (chunk === null || typeof chunk !== "object") throw new TypeError("第 " + index + " 块不是对象");
|
|
505
|
+
if (chunk.index !== index) throw new Error("chunk index 必须从 0 连续递增,发现第 " + index + " 块为 " + chunk.index);
|
|
506
|
+
if (!Number.isSafeInteger(chunk.byteOffset) || chunk.byteOffset !== expectedOffset) {
|
|
507
|
+
throw new Error("chunk byteOffset 不连续:第 " + index + " 块应为 " + expectedOffset);
|
|
508
|
+
}
|
|
509
|
+
const bytes = chunkBytes(chunk, index);
|
|
510
|
+
if (!Number.isSafeInteger(chunk.byteLength) || chunk.byteLength !== bytes.length) {
|
|
511
|
+
throw new Error("chunk byteLength 与 UTF-8 字节数不一致:第 " + index + " 块");
|
|
512
|
+
}
|
|
513
|
+
let decoded;
|
|
514
|
+
try {
|
|
515
|
+
decoded = FATAL_UTF8.decode(bytes);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
throw new Error("chunk 包含非法 UTF-8:第 " + index + " 块", { cause: error });
|
|
518
|
+
}
|
|
519
|
+
if (typeof chunk.text === "string" && decoded !== chunk.text) {
|
|
520
|
+
throw new Error("chunk text 与 UTF-8 bytes 不一致:第 " + index + " 块");
|
|
521
|
+
}
|
|
522
|
+
parts.push(decoded);
|
|
523
|
+
expectedOffset += bytes.length;
|
|
524
|
+
}
|
|
525
|
+
return parts.join("");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** 返回 SHA-256 hex;长度诊断由调用方用 text.length/UTF-8 字节长度记录。 */
|
|
529
|
+
export function hashUtf8(text) {
|
|
530
|
+
const bytes = utf8Bytes(text);
|
|
531
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* 分辨原始 JSONL 错误、提示词显式截断、普通省略号与展示层标记。
|
|
536
|
+
* display marker(例如 read 工具的 line truncated 提示)不会被当作原始截断证据。
|
|
537
|
+
*/
|
|
538
|
+
export function classifyTruncation(record) {
|
|
539
|
+
if (record?.parseError) {
|
|
540
|
+
return {
|
|
541
|
+
kind: "jsonl-parse-error",
|
|
542
|
+
type: "jsonl-parse-error",
|
|
543
|
+
source: "jsonl",
|
|
544
|
+
truncated: false,
|
|
545
|
+
evidence: [String(record.parseError.message ?? record.parseError)],
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const text = typeof record === "string" ? record : (typeof record?.text === "string" ? record.text : typeof record?.prompt === "string" ? record.prompt : "");
|
|
549
|
+
const display = typeof record?.displayMarker === "string" ? record.displayMarker : "";
|
|
550
|
+
if (display.length > 0 || DISPLAY_TRUNCATION.test(text)) {
|
|
551
|
+
return {
|
|
552
|
+
kind: "display-layer-marker",
|
|
553
|
+
type: "display-layer-marker",
|
|
554
|
+
source: "display",
|
|
555
|
+
truncated: false,
|
|
556
|
+
evidence: [display || text.match(DISPLAY_TRUNCATION)?.[0]],
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const explicit = text.match(EXPLICIT_TRUNCATION);
|
|
560
|
+
if (explicit !== null) {
|
|
561
|
+
return {
|
|
562
|
+
kind: "prompt-explicit-truncation",
|
|
563
|
+
type: "prompt-explicit-truncation",
|
|
564
|
+
source: "prompt",
|
|
565
|
+
truncated: true,
|
|
566
|
+
evidence: [explicit[0]],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const ellipsis = text.match(ELLIPSIS);
|
|
570
|
+
if (ellipsis !== null) {
|
|
571
|
+
return {
|
|
572
|
+
kind: "ordinary-ellipsis",
|
|
573
|
+
type: "ordinary-ellipsis",
|
|
574
|
+
source: "prompt",
|
|
575
|
+
truncated: false,
|
|
576
|
+
evidence: [ellipsis[0]],
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
return { kind: "none", type: "none", source: "none", truncated: false, evidence: [] };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function utf8Length(text) {
|
|
583
|
+
return utf8Bytes(text).length;
|
|
584
|
+
}
|