oh-my-knowledge 0.52.1 → 0.52.3-oidc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/observability/codex-conversation-index.d.ts +6 -2
- package/dist/observability/codex-conversation-index.js +40 -6
- package/dist/observability/codex-protocol.d.ts +6 -0
- package/dist/observability/codex-protocol.js +37 -2
- package/dist/observability/codex-trace-adapter.js +12 -4
- package/dist/observability/conversation-catalog.js +3 -1
- package/dist/observability/experience.js +9 -1
- package/dist/observability/feedback-matchers.d.ts +5 -0
- package/dist/observability/feedback-matchers.js +15 -0
- package/dist/observability/knowledge-debugger.js +52 -19
- package/dist/observability/task-semantic-projection.d.ts +11 -0
- package/dist/observability/task-semantic-projection.js +172 -0
- package/dist/observability/task-window.d.ts +1 -0
- package/dist/observability/task-window.js +28 -7
- package/dist/observability/trace-ir.d.ts +6 -0
- package/dist/renderer/conversation-renderer.d.ts +7 -0
- package/dist/renderer/conversation-renderer.js +108 -6
- package/dist/renderer/icons.js +1 -0
- package/dist/renderer/knowledge-debugger-renderer.js +110 -22
- package/dist/renderer/trajectory-evidence.d.ts +8 -0
- package/dist/renderer/trajectory-evidence.js +12 -0
- package/dist/server/report-server.js +25 -2
- package/dist/types/observability.d.ts +4 -0
- package/package.json +4 -4
|
@@ -15,7 +15,7 @@ export interface CodexIndexedTask {
|
|
|
15
15
|
endLine: number;
|
|
16
16
|
}
|
|
17
17
|
export interface CodexRolloutIndex {
|
|
18
|
-
schemaVersion:
|
|
18
|
+
schemaVersion: 12;
|
|
19
19
|
sourcePath: string;
|
|
20
20
|
/** File size observed when the index was produced. */
|
|
21
21
|
sourceSize: number;
|
|
@@ -51,6 +51,10 @@ export interface CodexJsonlLine {
|
|
|
51
51
|
startOffset: number;
|
|
52
52
|
endOffset: number;
|
|
53
53
|
}
|
|
54
|
+
export interface CodexTaskRecordReadOptions {
|
|
55
|
+
/** Include only the next task prefix through its first human user message. */
|
|
56
|
+
includeNextHumanMessage?: boolean;
|
|
57
|
+
}
|
|
54
58
|
/**
|
|
55
59
|
* Build a compact byte-range index without retaining the rollout. Only records
|
|
56
60
|
* needed for turn boundaries and list metrics are parsed.
|
|
@@ -80,7 +84,7 @@ export declare function isCurrentCodexRolloutIndex(value: unknown, sourcePath: s
|
|
|
80
84
|
* index remains internally consistent even after newer records are appended.
|
|
81
85
|
*/
|
|
82
86
|
export declare function isReusableCodexRolloutIndex(value: unknown, sourcePath: string): value is CodexRolloutIndex;
|
|
83
|
-
export declare function readCodexTaskRecords(index: CodexRolloutIndex, task: CodexIndexedTask): {
|
|
87
|
+
export declare function readCodexTaskRecords(index: CodexRolloutIndex, task: CodexIndexedTask, options?: CodexTaskRecordReadOptions): {
|
|
84
88
|
records: Array<unknown | undefined>;
|
|
85
89
|
lines: CodexJsonlLine[];
|
|
86
90
|
malformedRecordCount: number;
|
|
@@ -3,7 +3,7 @@ import { codexUserDisplayText, codexUserMessageOrigin } from './codex-protocol.j
|
|
|
3
3
|
import { codexRuntimeToolOutcomeFromPayload, codexToolOutputOutcome, } from './codex-tool-status.js';
|
|
4
4
|
const READ_CHUNK_BYTES = 256 * 1024;
|
|
5
5
|
const MAX_RECORD_BYTES = 32 * 1024 * 1024;
|
|
6
|
-
const INDEX_SCHEMA_VERSION =
|
|
6
|
+
const INDEX_SCHEMA_VERSION = 12;
|
|
7
7
|
const MAX_CURRENT_INDEX_ATTEMPTS = 4;
|
|
8
8
|
/**
|
|
9
9
|
* Build a compact byte-range index without retaining the rollout. Only records
|
|
@@ -327,7 +327,7 @@ export function isReusableCodexRolloutIndex(value, sourcePath) {
|
|
|
327
327
|
return false;
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
|
-
export function readCodexTaskRecords(index, task) {
|
|
330
|
+
export function readCodexTaskRecords(index, task, options = {}) {
|
|
331
331
|
const records = [];
|
|
332
332
|
if (index.sessionMeta !== undefined && index.sessionMetaLine !== undefined) {
|
|
333
333
|
records[index.sessionMetaLine] = index.sessionMeta;
|
|
@@ -345,8 +345,40 @@ export function readCodexTaskRecords(index, task) {
|
|
|
345
345
|
malformedRecordCount += 1;
|
|
346
346
|
}
|
|
347
347
|
}, task.startOffset, task.endOffset, task.startLine);
|
|
348
|
+
if (options.includeNextHumanMessage) {
|
|
349
|
+
const taskIndex = index.tasks.findIndex((candidate) => candidate.turnId === task.turnId);
|
|
350
|
+
const nextTask = taskIndex >= 0 ? index.tasks[taskIndex + 1] : undefined;
|
|
351
|
+
if (nextTask) {
|
|
352
|
+
forEachJsonlLine(index.sourcePath, (record) => {
|
|
353
|
+
lines.push(record);
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(record.text);
|
|
356
|
+
records[record.line] = parsed;
|
|
357
|
+
return isHumanUserPrompt(parsed) ? false : undefined;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
malformedRecordCount += 1;
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
}, nextTask.startOffset, nextTask.endOffset, nextTask.startLine);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
348
366
|
return { records, lines, malformedRecordCount };
|
|
349
367
|
}
|
|
368
|
+
function isHumanUserPrompt(value) {
|
|
369
|
+
const raw = objectValue(value);
|
|
370
|
+
if (!raw)
|
|
371
|
+
return false;
|
|
372
|
+
const recordType = stringValue(raw.type);
|
|
373
|
+
const payload = objectValue(raw.payload) ?? {};
|
|
374
|
+
const payloadType = stringValue(payload.type);
|
|
375
|
+
if (!isUserPromptRecord(recordType, payloadType, payload))
|
|
376
|
+
return false;
|
|
377
|
+
const message = userPromptText(payload)?.trim();
|
|
378
|
+
return Boolean(message
|
|
379
|
+
&& codexUserMessageOrigin(message) === 'human'
|
|
380
|
+
&& codexUserDisplayText(message)?.trim());
|
|
381
|
+
}
|
|
350
382
|
function finalizeSupersededTask(task, endOffset, endLine, endTimestamp) {
|
|
351
383
|
return stripMutable({
|
|
352
384
|
...task,
|
|
@@ -501,7 +533,7 @@ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.P
|
|
|
501
533
|
let indexedEndsWithNewline = true;
|
|
502
534
|
let fragments = [];
|
|
503
535
|
try {
|
|
504
|
-
while (absoluteOffset < endOffset) {
|
|
536
|
+
readLoop: while (absoluteOffset < endOffset) {
|
|
505
537
|
const requestBytes = Math.min(buffer.length, endOffset - absoluteOffset);
|
|
506
538
|
const bytesRead = readSync(fd, buffer, 0, requestBytes, absoluteOffset);
|
|
507
539
|
if (bytesRead === 0)
|
|
@@ -515,7 +547,7 @@ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.P
|
|
|
515
547
|
? Buffer.concat([...fragments, tail])
|
|
516
548
|
: tail;
|
|
517
549
|
const nextOffset = absoluteOffset + index + 1;
|
|
518
|
-
emitLine(lineBuffer, lineNumber, lineStartOffset, nextOffset, visit);
|
|
550
|
+
const shouldContinue = emitLine(lineBuffer, lineNumber, lineStartOffset, nextOffset, visit);
|
|
519
551
|
indexedSize = nextOffset;
|
|
520
552
|
indexedLineCount = lineNumber + 1;
|
|
521
553
|
indexedEndsWithNewline = true;
|
|
@@ -523,6 +555,8 @@ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.P
|
|
|
523
555
|
cursor = index + 1;
|
|
524
556
|
lineStartOffset = nextOffset;
|
|
525
557
|
lineNumber += 1;
|
|
558
|
+
if (!shouldContinue)
|
|
559
|
+
break readLoop;
|
|
526
560
|
}
|
|
527
561
|
if (cursor < bytesRead)
|
|
528
562
|
fragments.push(Buffer.from(buffer.subarray(cursor, bytesRead)));
|
|
@@ -561,6 +595,6 @@ function isCompleteJsonRecord(buffer) {
|
|
|
561
595
|
function emitLine(buffer, line, startOffset, endOffset, visit) {
|
|
562
596
|
const text = buffer.toString('utf8').trim();
|
|
563
597
|
if (!text)
|
|
564
|
-
return;
|
|
565
|
-
visit({ text, line, startOffset, endOffset });
|
|
598
|
+
return true;
|
|
599
|
+
return visit({ text, line, startOffset, endOffset }) !== false;
|
|
566
600
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/** Stable inventory of Codex rollout record shapes understood by OMK. */
|
|
2
|
+
export interface CodexUserAttachment {
|
|
3
|
+
attachmentKind: 'image' | 'file';
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
2
6
|
export declare function isCodexResponseItemType(value: string | undefined): boolean;
|
|
3
7
|
export declare function isCodexEventMessageType(value: string | undefined): boolean;
|
|
4
8
|
/** Preserve source text separately while removing Codex UI transport envelopes from semantic display. */
|
|
5
9
|
export declare function codexUserDisplayText(text: string): string | undefined;
|
|
10
|
+
/** Extract privacy-safe attachment metadata without projecting local paths into Trace IR. */
|
|
11
|
+
export declare function codexUserAttachments(text: string): CodexUserAttachment[];
|
|
6
12
|
export declare function codexUserMessageOrigin(text: string): 'human' | 'runtime';
|
|
7
13
|
/**
|
|
8
14
|
* Records consumed by correlation or retained only as source provenance do not
|
|
@@ -32,6 +32,9 @@ const EVENT_MESSAGE_TYPES = new Set([
|
|
|
32
32
|
]);
|
|
33
33
|
const IN_APP_BROWSER_CONTEXT_RE = /<in-app-browser-context\b[^>]*>[\s\S]*?<\/in-app-browser-context>/gi;
|
|
34
34
|
const CODEX_RUNTIME_MESSAGE_RE = /^# AGENTS\.md instructions\b|^<(?:app-context|environment_context|permissions instructions|collaboration_mode|apps_instructions|plugins_instructions|skills_instructions|recommended_plugins)>/i;
|
|
35
|
+
const CODEX_USER_REQUEST_HEADING_RE = /^## My request(?: for Codex)?:\s*$/im;
|
|
36
|
+
const CODEX_USER_FILES_HEADING_RE = /^# Files mentioned by the user:\s*$/im;
|
|
37
|
+
const IMAGE_ATTACHMENT_RE = /\.(?:avif|bmp|gif|heic|heif|jpe?g|png|webp)$/i;
|
|
35
38
|
export function isCodexResponseItemType(value) {
|
|
36
39
|
return value !== undefined && RESPONSE_ITEM_TYPES.has(value);
|
|
37
40
|
}
|
|
@@ -40,8 +43,7 @@ export function isCodexEventMessageType(value) {
|
|
|
40
43
|
}
|
|
41
44
|
/** Preserve source text separately while removing Codex UI transport envelopes from semantic display. */
|
|
42
45
|
export function codexUserDisplayText(text) {
|
|
43
|
-
const
|
|
44
|
-
const requestMatch = requestHeading.exec(text);
|
|
46
|
+
const requestMatch = CODEX_USER_REQUEST_HEADING_RE.exec(text);
|
|
45
47
|
const request = requestMatch
|
|
46
48
|
? text.slice(requestMatch.index + requestMatch[0].length)
|
|
47
49
|
: text;
|
|
@@ -53,6 +55,39 @@ export function codexUserDisplayText(text) {
|
|
|
53
55
|
.trim();
|
|
54
56
|
return visible || undefined;
|
|
55
57
|
}
|
|
58
|
+
/** Extract privacy-safe attachment metadata without projecting local paths into Trace IR. */
|
|
59
|
+
export function codexUserAttachments(text) {
|
|
60
|
+
const attachments = new Map();
|
|
61
|
+
const requestMatch = CODEX_USER_REQUEST_HEADING_RE.exec(text);
|
|
62
|
+
const filesMatch = CODEX_USER_FILES_HEADING_RE.exec(text);
|
|
63
|
+
if (filesMatch && (!requestMatch || filesMatch.index < requestMatch.index)) {
|
|
64
|
+
const sectionEnd = requestMatch?.index ?? text.length;
|
|
65
|
+
const section = text.slice(filesMatch.index + filesMatch[0].length, sectionEnd);
|
|
66
|
+
for (const match of section.matchAll(/^##\s+(.+?):\s+(.+)\s*$/gm)) {
|
|
67
|
+
addCodexUserAttachment(attachments, match[1], match[2]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const match of text.matchAll(/<image\b[^>]*\bpath="([^"]+)"[^>]*>/gi)) {
|
|
71
|
+
addCodexUserAttachment(attachments, undefined, match[1]);
|
|
72
|
+
}
|
|
73
|
+
return [...attachments.values()];
|
|
74
|
+
}
|
|
75
|
+
function addCodexUserAttachment(attachments, declaredName, sourcePath) {
|
|
76
|
+
const normalizedPath = sourcePath?.trim();
|
|
77
|
+
const inferredName = normalizedPath?.split(/[\\/]/).at(-1);
|
|
78
|
+
const name = declaredName?.trim() || inferredName?.trim();
|
|
79
|
+
if (!name)
|
|
80
|
+
return;
|
|
81
|
+
const key = name.toLocaleLowerCase('en-US');
|
|
82
|
+
if (attachments.has(key))
|
|
83
|
+
return;
|
|
84
|
+
attachments.set(key, {
|
|
85
|
+
attachmentKind: IMAGE_ATTACHMENT_RE.test(name) || IMAGE_ATTACHMENT_RE.test(normalizedPath ?? '')
|
|
86
|
+
? 'image'
|
|
87
|
+
: 'file',
|
|
88
|
+
name,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
56
91
|
export function codexUserMessageOrigin(text) {
|
|
57
92
|
const trimmed = text.trimStart();
|
|
58
93
|
if (CODEX_RUNTIME_MESSAGE_RE.test(trimmed))
|
|
@@ -4,7 +4,7 @@ import { correlateTraceToolEvents, createTraceId, normalizeTraceTimestamp, trace
|
|
|
4
4
|
import { nonNegativeMetric, optionalTokenCount, splitInclusiveInputTokens, tokenCount, } from '../shared/token-usage.js';
|
|
5
5
|
import { normalizeToolIdentity } from '../shared/tool-identity.js';
|
|
6
6
|
import { extractCodexExecCommands } from './codex-exec-command.js';
|
|
7
|
-
import { codexUserDisplayText, codexUserMessageOrigin, isCodexEventMessageType, isCodexRecordConsumedWithoutDirectEvent, isCodexResponseItemType, } from './codex-protocol.js';
|
|
7
|
+
import { codexUserAttachments, codexUserDisplayText, codexUserMessageOrigin, isCodexEventMessageType, isCodexRecordConsumedWithoutDirectEvent, isCodexResponseItemType, } from './codex-protocol.js';
|
|
8
8
|
import { codexRuntimeToolOutcome, codexToolOutputOutcome, codexToolStatusFromValue, } from './codex-tool-status.js';
|
|
9
9
|
export function isCodexJsonl(records) {
|
|
10
10
|
return records.some((record) => {
|
|
@@ -231,6 +231,7 @@ function convertCodexRecords(rawRecords, runId) {
|
|
|
231
231
|
: normalizedRole === 'system' ? 'runtime' : 'synthetic',
|
|
232
232
|
text,
|
|
233
233
|
displayText: normalizedRole === 'user' ? codexUserDisplayText(text) : undefined,
|
|
234
|
+
attachments: normalizedRole === 'user' ? codexUserAttachments(text) : undefined,
|
|
234
235
|
model: normalizedRole === 'assistant' ? activeModel : undefined,
|
|
235
236
|
});
|
|
236
237
|
}
|
|
@@ -444,6 +445,7 @@ function convertCodexRecords(rawRecords, runId) {
|
|
|
444
445
|
origin: codexUserMessageOrigin(text),
|
|
445
446
|
text,
|
|
446
447
|
displayText: codexUserDisplayText(text),
|
|
448
|
+
attachments: codexUserAttachments(text),
|
|
447
449
|
});
|
|
448
450
|
}
|
|
449
451
|
return;
|
|
@@ -995,6 +997,7 @@ function indexDuplicateEventMessages(records) {
|
|
|
995
997
|
const text = stringValue(payload.message);
|
|
996
998
|
if (!role || !text)
|
|
997
999
|
return;
|
|
1000
|
+
const mirrorText = role === 'user' ? codexUserDisplayText(text) : text;
|
|
998
1001
|
const nearbyRecords = [-1, 1].flatMap((direction) => {
|
|
999
1002
|
let candidateIndex = sourceIndex + direction;
|
|
1000
1003
|
while (candidateIndex >= 0 && candidateIndex < records.length) {
|
|
@@ -1010,9 +1013,14 @@ function indexDuplicateEventMessages(records) {
|
|
|
1010
1013
|
if (adjacent?.type !== 'response_item')
|
|
1011
1014
|
return false;
|
|
1012
1015
|
const adjacentPayload = isObject(adjacent.payload) ? adjacent.payload : {};
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
+
if (adjacentPayload.type !== 'message' || adjacentPayload.role !== role)
|
|
1017
|
+
return false;
|
|
1018
|
+
const adjacentText = codexContentText(adjacentPayload.content);
|
|
1019
|
+
if (!adjacentText)
|
|
1020
|
+
return false;
|
|
1021
|
+
return role === 'user'
|
|
1022
|
+
? Boolean(mirrorText) && codexUserDisplayText(adjacentText) === mirrorText
|
|
1023
|
+
: adjacentText === text;
|
|
1016
1024
|
});
|
|
1017
1025
|
if (mirrored)
|
|
1018
1026
|
duplicateIndexes.add(sourceIndex);
|
|
@@ -106,7 +106,9 @@ class CodexConversationCatalog {
|
|
|
106
106
|
trajectoryFromIndex(row, index, indexedTask) {
|
|
107
107
|
const threadId = row.id;
|
|
108
108
|
const turnId = indexedTask.turnId;
|
|
109
|
-
const selected = readCodexTaskRecords(index, indexedTask
|
|
109
|
+
const selected = readCodexTaskRecords(index, indexedTask, {
|
|
110
|
+
includeNextHumanMessage: true,
|
|
111
|
+
});
|
|
110
112
|
const traceSession = parseCodexSessionFile(row.rolloutPath, selected.records);
|
|
111
113
|
const fullSessionTimeline = projectTraceSessionTimeline(traceSession);
|
|
112
114
|
const turns = reconstructExperienceTurns(fullSessionTimeline);
|
|
@@ -983,13 +983,20 @@ function isTimelineEvent(value) {
|
|
|
983
983
|
];
|
|
984
984
|
if (!optionalStrings.every((field) => field === undefined || typeof field === 'string')
|
|
985
985
|
|| !isOptionalTimestamp(value.timestamp)
|
|
986
|
-
|| (value.isError !== undefined && typeof value.isError !== 'boolean')
|
|
986
|
+
|| (value.isError !== undefined && typeof value.isError !== 'boolean')
|
|
987
|
+
|| (value.attachments !== undefined && !isTimelineAttachmentArray(value.attachments)))
|
|
987
988
|
return false;
|
|
988
989
|
return value.kind !== 'tool_result'
|
|
989
990
|
|| value.toolStatus === undefined
|
|
990
991
|
|| value.isError === undefined
|
|
991
992
|
|| value.isError === (value.toolStatus === 'failure');
|
|
992
993
|
}
|
|
994
|
+
function isTimelineAttachmentArray(value) {
|
|
995
|
+
return Array.isArray(value) && value.every((attachment) => (isObjectRecord(attachment)
|
|
996
|
+
&& (attachment.attachmentKind === 'image' || attachment.attachmentKind === 'file')
|
|
997
|
+
&& typeof attachment.name === 'string'
|
|
998
|
+
&& attachment.name.length > 0));
|
|
999
|
+
}
|
|
993
1000
|
function isTimelineEventArray(value) {
|
|
994
1001
|
return Array.isArray(value) && value.every(isTimelineEvent);
|
|
995
1002
|
}
|
|
@@ -2746,6 +2753,7 @@ function userTimelineEvents(event, base, order) {
|
|
|
2746
2753
|
order: order + (commandEnvelope ? 1 : 0),
|
|
2747
2754
|
snippet: snippet(semanticText, 700),
|
|
2748
2755
|
fullText: fullText(semanticText),
|
|
2756
|
+
attachments: event.attachments,
|
|
2749
2757
|
label: userTextEventLabel(kind),
|
|
2750
2758
|
}));
|
|
2751
2759
|
return events;
|
|
@@ -28,6 +28,11 @@ export interface TextMatchRange {
|
|
|
28
28
|
}
|
|
29
29
|
export declare function findUserCorrectionMatches(value: string): TextMatchRange[];
|
|
30
30
|
export declare function hasUserCorrectionSignal(value: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 用严格纠正信号把下一轮用户消息关联回上一任务。统计反馈匹配仍保持更宽,
|
|
33
|
+
* 因为「改成」等表达可用于指标统计,但不足以单独证明任务连续性。
|
|
34
|
+
*/
|
|
35
|
+
export declare function hasExplicitFollowUpCorrectionSignal(value: string): boolean;
|
|
31
36
|
export declare function findUserGoalShiftMatches(value: string): TextMatchRange[];
|
|
32
37
|
export declare function hasUserGoalShiftSignal(value: string): boolean;
|
|
33
38
|
export declare function findNegativeFeedbackMatches(value: string): TextMatchRange[];
|
|
@@ -40,6 +40,14 @@ const PHRASE_USER_CORRECTION_TERMS = [
|
|
|
40
40
|
'改成',
|
|
41
41
|
'重来',
|
|
42
42
|
];
|
|
43
|
+
const EXPLICIT_FOLLOW_UP_CORRECTION_PATTERNS = [
|
|
44
|
+
/(?:^|[\s,,;;.。!!??::])(?:不对|错了)(?:啊|呀|吧|呢)?(?=$|[\s,,;;.。!!??::])/iu,
|
|
45
|
+
/(?:这|这个|这样|你的|你.{0,8})(?:不对|错了)/iu,
|
|
46
|
+
/不是这个|不是我要的|不要这样|理解错|看错|你没懂|重来|重新来|重做|重新做/iu,
|
|
47
|
+
/\b(?:that(?:'s| is)|this is|you(?:'re| are)) wrong\b/iu,
|
|
48
|
+
/\b(?:this|that) is (?:incorrect|not right)\b/iu,
|
|
49
|
+
/\byou (?:misunderstood|misread)\b|\b(?:start|do)(?: it)? over\b/iu,
|
|
50
|
+
];
|
|
43
51
|
const USER_GOAL_SHIFT_TERMS = [
|
|
44
52
|
'换个方向',
|
|
45
53
|
'重新来',
|
|
@@ -165,6 +173,13 @@ export function findUserCorrectionMatches(value) {
|
|
|
165
173
|
export function hasUserCorrectionSignal(value) {
|
|
166
174
|
return findUserCorrectionMatches(value).length > 0;
|
|
167
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* 用严格纠正信号把下一轮用户消息关联回上一任务。统计反馈匹配仍保持更宽,
|
|
178
|
+
* 因为「改成」等表达可用于指标统计,但不足以单独证明任务连续性。
|
|
179
|
+
*/
|
|
180
|
+
export function hasExplicitFollowUpCorrectionSignal(value) {
|
|
181
|
+
return EXPLICIT_FOLLOW_UP_CORRECTION_PATTERNS.some((pattern) => pattern.test(value));
|
|
182
|
+
}
|
|
168
183
|
export function findUserGoalShiftMatches(value) {
|
|
169
184
|
const ranges = [];
|
|
170
185
|
for (const term of USER_GOAL_SHIFT_TERMS) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { hasUserCorrectionSignal } from './feedback-matchers.js';
|
|
2
3
|
import { resolveTaskWindow } from './task-window.js';
|
|
3
4
|
const AGENTS_CONTEXT_RE = /^# AGENTS\.md instructions for ([^\n]+)\n/gim;
|
|
4
5
|
const SKILL_PATH_RE = /((?:~|\.{0,2}|\/)?[^\s"'`]*\/skills\/(?:\.system\/)?([^/\s"'`]+)\/SKILL\.md)\b/i;
|
|
@@ -16,13 +17,16 @@ export function buildKnowledgeDebuggerViewModel(session, targetTurnId, ingestion
|
|
|
16
17
|
}) {
|
|
17
18
|
const taskWindow = resolveTaskWindow(session, targetTurnId);
|
|
18
19
|
const normalizedEvents = taskWindow.events;
|
|
20
|
+
const normalizedTimeline = normalizedEvents.filter((event) => event.runtimeKind !== 'usage');
|
|
19
21
|
const timeline = taskWindow.semanticEvents.filter((event) => event.runtimeKind !== 'usage');
|
|
22
|
+
const replayTimeline = [...timeline, ...taskWindow.relatedEvents]
|
|
23
|
+
.sort((left, right) => left.order - right.order);
|
|
20
24
|
const knowledgeEvidence = projectKnowledgeEvidence(timeline);
|
|
21
|
-
const steps = buildTaskReplaySteps(session,
|
|
22
|
-
const notices = buildIntegrityNotices(taskWindow.scope,
|
|
23
|
-
const userEvents =
|
|
24
|
-
const assistantEvents =
|
|
25
|
-
const observedModels =
|
|
25
|
+
const steps = buildTaskReplaySteps(session, replayTimeline, knowledgeEvidence);
|
|
26
|
+
const notices = buildIntegrityNotices(taskWindow.scope, normalizedEvents, ingestion);
|
|
27
|
+
const userEvents = normalizedTimeline.filter((event) => event.kind === 'user_message');
|
|
28
|
+
const assistantEvents = normalizedTimeline.filter((event) => event.kind === 'assistant_message');
|
|
29
|
+
const observedModels = normalizedTimeline.reduce((models, event) => {
|
|
26
30
|
const eventModel = event.model?.trim();
|
|
27
31
|
if (eventModel && !models.includes(eventModel))
|
|
28
32
|
models.push(eventModel);
|
|
@@ -34,11 +38,11 @@ export function buildKnowledgeDebuggerViewModel(session, targetTurnId, ingestion
|
|
|
34
38
|
summary: {
|
|
35
39
|
userGoal: eventText(userEvents[0]),
|
|
36
40
|
finalResponse: eventText(assistantEvents.at(-1)),
|
|
37
|
-
observedStartTimestamp:
|
|
38
|
-
??
|
|
39
|
-
observedEndTimestamp: [...
|
|
40
|
-
toolCallCount:
|
|
41
|
-
toolFailureCount:
|
|
41
|
+
observedStartTimestamp: normalizedTimeline.find((event) => (event.timestamp && event.runtimeKind !== 'session_context'))?.timestamp
|
|
42
|
+
?? normalizedTimeline.find((event) => event.timestamp)?.timestamp,
|
|
43
|
+
observedEndTimestamp: [...normalizedTimeline].reverse().find((event) => event.timestamp)?.timestamp,
|
|
44
|
+
toolCallCount: normalizedEvents.filter((event) => event.kind === 'tool_use').length,
|
|
45
|
+
toolFailureCount: normalizedEvents.filter((event) => (event.kind === 'tool_result' && (event.toolStatus === 'failure' || event.isError))).length,
|
|
42
46
|
hasUserCorrection: steps.some((step) => step.stepKind === 'user_correction'),
|
|
43
47
|
observedModels,
|
|
44
48
|
},
|
|
@@ -199,7 +203,7 @@ function isKnowledgeReturningCall(toolName, callText) {
|
|
|
199
203
|
const command = sourceLocator(callText);
|
|
200
204
|
return command ? READ_ONLY_SHELL_RE.test(command) : false;
|
|
201
205
|
}
|
|
202
|
-
function buildIntegrityNotices(taskScope,
|
|
206
|
+
function buildIntegrityNotices(taskScope, normalizedEvents, ingestion) {
|
|
203
207
|
const notices = [];
|
|
204
208
|
if (taskScope.basis === 'unresolved') {
|
|
205
209
|
notices.push({ code: 'task_boundary_unavailable', count: 1 });
|
|
@@ -214,17 +218,40 @@ function buildIntegrityNotices(taskScope, timeline, steps, ingestion) {
|
|
|
214
218
|
notices.push({ code: 'ignored_values', count: ingestion.ignoredValueCount });
|
|
215
219
|
if (ingestion?.unknownEventCount)
|
|
216
220
|
notices.push({ code: 'unknown_events', count: ingestion.unknownEventCount });
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
notices.push({ code: 'unmatched_tool_calls', count:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const missingTimestamps = timeline.filter((event) => !event.timestamp).length;
|
|
221
|
+
const unmatched = unmatchedToolEventCounts(normalizedEvents);
|
|
222
|
+
if (unmatched.calls > 0)
|
|
223
|
+
notices.push({ code: 'unmatched_tool_calls', count: unmatched.calls });
|
|
224
|
+
if (unmatched.results > 0)
|
|
225
|
+
notices.push({ code: 'unmatched_tool_results', count: unmatched.results });
|
|
226
|
+
const missingTimestamps = normalizedEvents.filter((event) => !event.timestamp).length;
|
|
224
227
|
if (missingTimestamps > 0)
|
|
225
228
|
notices.push({ code: 'missing_timestamps', count: missingTimestamps });
|
|
226
229
|
return notices;
|
|
227
230
|
}
|
|
231
|
+
function unmatchedToolEventCounts(events) {
|
|
232
|
+
const availableResults = new Map();
|
|
233
|
+
for (const event of events) {
|
|
234
|
+
if (event.kind !== 'tool_result')
|
|
235
|
+
continue;
|
|
236
|
+
const key = toolCorrelationKey(event);
|
|
237
|
+
availableResults.set(key, (availableResults.get(key) ?? 0) + 1);
|
|
238
|
+
}
|
|
239
|
+
let unmatchedCalls = 0;
|
|
240
|
+
for (const event of events) {
|
|
241
|
+
if (event.kind !== 'tool_use')
|
|
242
|
+
continue;
|
|
243
|
+
const key = toolCorrelationKey(event);
|
|
244
|
+
const remaining = availableResults.get(key) ?? 0;
|
|
245
|
+
if (remaining === 0) {
|
|
246
|
+
unmatchedCalls += 1;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
availableResults.set(key, remaining - 1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const unmatchedResults = [...availableResults.values()].reduce((sum, count) => sum + count, 0);
|
|
253
|
+
return { calls: unmatchedCalls, results: unmatchedResults };
|
|
254
|
+
}
|
|
228
255
|
function correctionEventIds(session, timeline) {
|
|
229
256
|
const ids = new Set();
|
|
230
257
|
for (const episode of session.sessionStory?.episodes ?? []) {
|
|
@@ -235,9 +262,15 @@ function correctionEventIds(session, timeline) {
|
|
|
235
262
|
}
|
|
236
263
|
}
|
|
237
264
|
const userCorrectionCount = session.indicators?.userCorrectionCount ?? 0;
|
|
265
|
+
const lastAssistantOrder = Math.max(-1, ...timeline.filter((event) => event.kind === 'assistant_message').map((event) => event.order));
|
|
266
|
+
const explicitCandidates = timeline
|
|
267
|
+
.filter((event) => (event.kind === 'user_message'
|
|
268
|
+
&& event.order > lastAssistantOrder
|
|
269
|
+
&& hasUserCorrectionSignal(event.fullText ?? event.snippet ?? '')));
|
|
270
|
+
for (const event of explicitCandidates)
|
|
271
|
+
ids.add(event.id);
|
|
238
272
|
if (ids.size > 0 || userCorrectionCount === 0)
|
|
239
273
|
return ids;
|
|
240
|
-
const lastAssistantOrder = Math.max(-1, ...timeline.filter((event) => event.kind === 'assistant_message').map((event) => event.order));
|
|
241
274
|
const correctionCandidates = timeline
|
|
242
275
|
.filter((event) => event.kind === 'user_message' && event.order > lastAssistantOrder)
|
|
243
276
|
.slice(-userCorrectionCount);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExperienceTimelineEvent } from '../types/index.js';
|
|
2
|
+
export interface TaskSemanticProjectionOptions {
|
|
3
|
+
preservePendingToolCalls?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Bound one task's semantic projection without separating tool exchanges or
|
|
7
|
+
* blindly discarding the middle of a long task. Failures, task boundaries and
|
|
8
|
+
* the user/assistant endpoints are retained first; remaining capacity is
|
|
9
|
+
* distributed across the task so the preview still explains its progression.
|
|
10
|
+
*/
|
|
11
|
+
export declare function projectTaskSemanticEvents(events: ExperienceTimelineEvent[], limit: number, options?: TaskSemanticProjectionOptions): ExperienceTimelineEvent[];
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const PRIORITY_BANDS = [100, 90, 80, 70, 60, 40, 20, 0];
|
|
2
|
+
/**
|
|
3
|
+
* Bound one task's semantic projection without separating tool exchanges or
|
|
4
|
+
* blindly discarding the middle of a long task. Failures, task boundaries and
|
|
5
|
+
* the user/assistant endpoints are retained first; remaining capacity is
|
|
6
|
+
* distributed across the task so the preview still explains its progression.
|
|
7
|
+
*/
|
|
8
|
+
export function projectTaskSemanticEvents(events, limit, options = {}) {
|
|
9
|
+
if (limit <= 0 || events.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
if (events.length <= limit)
|
|
12
|
+
return events;
|
|
13
|
+
const units = semanticUnits(events, options);
|
|
14
|
+
const selected = new Map();
|
|
15
|
+
let remaining = limit;
|
|
16
|
+
const required = units
|
|
17
|
+
.filter((unit) => unit.required)
|
|
18
|
+
.sort((left, right) => right.priority - left.priority || left.retentionOrder - right.retentionOrder);
|
|
19
|
+
for (const unit of required) {
|
|
20
|
+
if (unit.events.length > remaining)
|
|
21
|
+
continue;
|
|
22
|
+
selected.set(unit.id, unit);
|
|
23
|
+
remaining -= unit.events.length;
|
|
24
|
+
}
|
|
25
|
+
for (const priority of PRIORITY_BANDS) {
|
|
26
|
+
if (remaining <= 0)
|
|
27
|
+
break;
|
|
28
|
+
const candidates = units.filter((unit) => (!selected.has(unit.id) && unit.priority === priority));
|
|
29
|
+
if (candidates.length === 0)
|
|
30
|
+
continue;
|
|
31
|
+
const totalSize = candidates.reduce((sum, unit) => sum + unit.events.length, 0);
|
|
32
|
+
if (totalSize <= remaining) {
|
|
33
|
+
for (const unit of candidates)
|
|
34
|
+
selected.set(unit.id, unit);
|
|
35
|
+
remaining -= totalSize;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
remaining = selectSpreadCandidates(candidates, selected, units, remaining);
|
|
39
|
+
}
|
|
40
|
+
return [...selected.values()]
|
|
41
|
+
.flatMap((unit) => unit.events)
|
|
42
|
+
.sort((left, right) => left.order - right.order);
|
|
43
|
+
}
|
|
44
|
+
function semanticUnits(events, options) {
|
|
45
|
+
const resultByCall = new Map();
|
|
46
|
+
for (const event of events) {
|
|
47
|
+
if (event.kind !== 'tool_result')
|
|
48
|
+
continue;
|
|
49
|
+
const key = toolCorrelationKey(event);
|
|
50
|
+
resultByCall.set(key, [...(resultByCall.get(key) ?? []), event]);
|
|
51
|
+
}
|
|
52
|
+
const firstUserId = events.find((event) => event.kind === 'user_message')?.id;
|
|
53
|
+
const finalAssistantId = [...events].reverse()
|
|
54
|
+
.find((event) => event.kind === 'assistant_message')?.id;
|
|
55
|
+
const consumedResults = new Set();
|
|
56
|
+
const units = [];
|
|
57
|
+
for (const event of events) {
|
|
58
|
+
if (event.kind === 'tool_result' && consumedResults.has(event.id))
|
|
59
|
+
continue;
|
|
60
|
+
if (event.kind === 'tool_use') {
|
|
61
|
+
const result = resultByCall.get(toolCorrelationKey(event))
|
|
62
|
+
?.find((candidate) => !consumedResults.has(candidate.id));
|
|
63
|
+
if (result)
|
|
64
|
+
consumedResults.add(result.id);
|
|
65
|
+
const exchange = result ? [event, result] : [event];
|
|
66
|
+
const failed = exchange.some((candidate) => candidate.isError || candidate.toolStatus === 'failure');
|
|
67
|
+
const pending = !result && options.preservePendingToolCalls === true;
|
|
68
|
+
units.push({
|
|
69
|
+
id: `tool:${event.id}`,
|
|
70
|
+
events: exchange,
|
|
71
|
+
order: event.order,
|
|
72
|
+
priority: failed ? 135 : pending ? 140 : 70,
|
|
73
|
+
required: failed || pending,
|
|
74
|
+
retentionOrder: pending ? -event.order : event.order,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const isFirstUser = event.id === firstUserId;
|
|
79
|
+
const isFinalAssistant = event.id === finalAssistantId;
|
|
80
|
+
const isBoundary = event.kind === 'lifecycle' && isBoundaryLifecycle(event);
|
|
81
|
+
const isFailedResult = event.kind === 'tool_result'
|
|
82
|
+
&& (event.isError || event.toolStatus === 'failure');
|
|
83
|
+
const required = isFirstUser
|
|
84
|
+
|| event.id === finalAssistantId
|
|
85
|
+
|| isFailedResult
|
|
86
|
+
|| isBoundary;
|
|
87
|
+
units.push({
|
|
88
|
+
id: `event:${event.id}`,
|
|
89
|
+
events: [event],
|
|
90
|
+
order: event.order,
|
|
91
|
+
priority: requiredPriority(event, {
|
|
92
|
+
isFirstUser,
|
|
93
|
+
isFinalAssistant,
|
|
94
|
+
isBoundary,
|
|
95
|
+
isFailedResult,
|
|
96
|
+
}),
|
|
97
|
+
required,
|
|
98
|
+
retentionOrder: event.order,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return units;
|
|
102
|
+
}
|
|
103
|
+
function requiredPriority(event, flags) {
|
|
104
|
+
if (flags.isFirstUser)
|
|
105
|
+
return 160;
|
|
106
|
+
if (flags.isFinalAssistant)
|
|
107
|
+
return 150;
|
|
108
|
+
if (flags.isBoundary && event.label === 'turn_started')
|
|
109
|
+
return 145;
|
|
110
|
+
if (flags.isFailedResult)
|
|
111
|
+
return 135;
|
|
112
|
+
if (flags.isBoundary)
|
|
113
|
+
return 130;
|
|
114
|
+
return semanticPriority(event);
|
|
115
|
+
}
|
|
116
|
+
function semanticPriority(event) {
|
|
117
|
+
if (event.kind === 'user_message' || event.kind === 'assistant_message')
|
|
118
|
+
return 90;
|
|
119
|
+
if (event.kind === 'skill_context')
|
|
120
|
+
return 80;
|
|
121
|
+
if (event.kind === 'runtime_context')
|
|
122
|
+
return event.runtimeKind === 'usage' ? 0 : 80;
|
|
123
|
+
if (event.kind === 'lifecycle')
|
|
124
|
+
return 70;
|
|
125
|
+
if (event.kind === 'tool_result')
|
|
126
|
+
return event.isError || event.toolStatus === 'failure' ? 100 : 70;
|
|
127
|
+
if (event.kind === 'observation' || event.kind === 'agent_activity')
|
|
128
|
+
return 60;
|
|
129
|
+
if (event.kind === 'model_activity')
|
|
130
|
+
return event.contentVisibility === 'plaintext' ? 40 : 20;
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
function isBoundaryLifecycle(event) {
|
|
134
|
+
const label = `${event.label ?? ''} ${event.sourceType ?? ''}`.toLowerCase();
|
|
135
|
+
return /(?:turn|task)[_-]?(?:start|complete|abort|interrupt|end)/.test(label);
|
|
136
|
+
}
|
|
137
|
+
function selectSpreadCandidates(candidates, selected, allUnits, capacity) {
|
|
138
|
+
const selectedOrders = [...selected.values()].map((unit) => unit.order);
|
|
139
|
+
const firstOrder = allUnits[0]?.order ?? 0;
|
|
140
|
+
const lastOrder = allUnits.at(-1)?.order ?? firstOrder;
|
|
141
|
+
const distances = new Map(candidates.map((candidate) => [
|
|
142
|
+
candidate.id,
|
|
143
|
+
selectedOrders.length > 0
|
|
144
|
+
? Math.min(...selectedOrders.map((order) => Math.abs(candidate.order - order)))
|
|
145
|
+
: Math.min(Math.abs(candidate.order - firstOrder), Math.abs(lastOrder - candidate.order)),
|
|
146
|
+
]));
|
|
147
|
+
let remaining = capacity;
|
|
148
|
+
while (remaining > 0) {
|
|
149
|
+
const fitting = candidates.filter((unit) => (!selected.has(unit.id) && unit.events.length <= remaining));
|
|
150
|
+
if (fitting.length === 0)
|
|
151
|
+
break;
|
|
152
|
+
const next = fitting.reduce((best, candidate) => {
|
|
153
|
+
const candidateDistance = distances.get(candidate.id) ?? 0;
|
|
154
|
+
const bestDistance = distances.get(best.id) ?? 0;
|
|
155
|
+
return candidateDistance > bestDistance
|
|
156
|
+
|| (candidateDistance === bestDistance && candidate.order < best.order)
|
|
157
|
+
? candidate
|
|
158
|
+
: best;
|
|
159
|
+
});
|
|
160
|
+
selected.set(next.id, next);
|
|
161
|
+
remaining -= next.events.length;
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
if (selected.has(candidate.id))
|
|
164
|
+
continue;
|
|
165
|
+
distances.set(candidate.id, Math.min(distances.get(candidate.id) ?? Number.POSITIVE_INFINITY, Math.abs(candidate.order - next.order)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return remaining;
|
|
169
|
+
}
|
|
170
|
+
function toolCorrelationKey(event) {
|
|
171
|
+
return event.callInstanceId ?? event.toolUseId ?? event.id;
|
|
172
|
+
}
|
|
@@ -3,6 +3,7 @@ export declare const TASK_SEMANTIC_EVENT_LIMIT = 240;
|
|
|
3
3
|
export interface ResolvedTaskWindow {
|
|
4
4
|
events: ExperienceTimelineEvent[];
|
|
5
5
|
semanticEvents: ExperienceTimelineEvent[];
|
|
6
|
+
relatedEvents: ExperienceTimelineEvent[];
|
|
6
7
|
scope: TaskWindowScope;
|
|
7
8
|
}
|
|
8
9
|
export declare function resolveTaskWindow(session: TaskTrajectorySession, targetTurnId: string, semanticLimit?: number): ResolvedTaskWindow;
|