@hadooppei/hwcode 1.0.16 → 1.0.20
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/.pi/dist/lib/knowledge/evidence.js +302 -0
- package/.pi/dist/lib/knowledge/extractor.js +53 -76
- package/.pi/dist/lib/knowledge/review-worker.js +97 -20
- package/.pi/dist/lib/knowledge/sanitize.js +8 -0
- package/.pi/dist/lib/knowledge/session-scanner.js +66 -13
- package/.pi/dist/lib/knowledge/store.js +215 -69
- package/.pi/dist/lib/runtime/defaults.js +6 -0
- package/.pi/extensions/knowledge.ts +90 -15
- package/.pi/extensions/workflows/cloud/provider-tools.ts +37 -7
- package/.pi/lib/knowledge/evidence.ts +314 -0
- package/.pi/lib/knowledge/extractor.ts +61 -55
- package/.pi/lib/knowledge/review-status.ts +18 -0
- package/.pi/lib/knowledge/review-worker.ts +94 -20
- package/.pi/lib/knowledge/sanitize.ts +10 -0
- package/.pi/lib/knowledge/session-scanner.ts +65 -12
- package/.pi/lib/knowledge/store.ts +220 -69
- package/.pi/lib/knowledge/types.ts +59 -2
- package/.pi/lib/knowledge/worker-protocol.ts +1 -1
- package/.pi/lib/runtime/defaults.ts +6 -0
- package/.pi/lib/workflows/cloud/process.ts +19 -0
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ import { parentPort, workerData } from "node:worker_threads";
|
|
|
6
6
|
|
|
7
7
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
|
|
8
8
|
import { userRuntimePaths } from "../runtime/paths.ts";
|
|
9
|
-
import {
|
|
10
|
-
import { type KnowledgeReviewStatus, writeKnowledgeReviewStatus } from "./review-status.ts";
|
|
9
|
+
import { parseKnowledgeReviewEnvelope } from "./extractor.ts";
|
|
10
|
+
import { readKnowledgeReviewStatus, type KnowledgeReviewStatus, writeKnowledgeReviewStatus } from "./review-status.ts";
|
|
11
11
|
import { sanitizeKnowledgeText } from "./sanitize.ts";
|
|
12
12
|
import { findNextReviewTask } from "./session-scanner.ts";
|
|
13
13
|
import { commitKnowledgeReview, ensureKnowledgeDirectories, loadCurrentManifest } from "./store.ts";
|
|
@@ -59,21 +59,44 @@ function publishReviewStatus(): void {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function boundedError(error: unknown): string {
|
|
62
|
-
return sanitizeKnowledgeText(error instanceof Error ? error.message : String(error))
|
|
62
|
+
return sanitizeKnowledgeText(error instanceof Error ? error.message : String(error))
|
|
63
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxFailureDetailChars);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clearSupersededReviewState(task: KnowledgeReviewTask): void {
|
|
67
|
+
if (!reviewStatus) return;
|
|
68
|
+
for (const field of ["blockedReviews", "reviewFailures"] as const) {
|
|
69
|
+
const records = reviewStatus[field];
|
|
70
|
+
if (!records) continue;
|
|
71
|
+
for (const [reviewKey, record] of Object.entries(records)) {
|
|
72
|
+
if (record.sessionKey === task.sessionKey && reviewKey !== task.reviewKey) {
|
|
73
|
+
delete records[reviewKey];
|
|
74
|
+
reviewAttempts.delete(reviewKey);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (Object.keys(records).length === 0) reviewStatus[field] = undefined;
|
|
78
|
+
}
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
function beginReview(task: KnowledgeReviewTask): void {
|
|
82
|
+
clearSupersededReviewState(task);
|
|
66
83
|
activeAttempt = (reviewAttempts.get(task.reviewKey) ?? 0) + 1;
|
|
67
84
|
reviewAttempts.set(task.reviewKey, activeAttempt);
|
|
68
85
|
activeStartedAt = Date.now();
|
|
69
86
|
clearTimeout(activeDeadlineTimer);
|
|
70
87
|
activeDeadlineTimer = setTimeout(() => {
|
|
71
88
|
if (activeTask?.requestId !== task.requestId) return;
|
|
72
|
-
const error =
|
|
73
|
-
|
|
89
|
+
const error = [
|
|
90
|
+
`knowledge-review-result-timeout after ${KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs + 5_000}ms`,
|
|
91
|
+
`reviewKey=${task.reviewKey}`,
|
|
92
|
+
`deltaChars=${task.delta.length}`,
|
|
93
|
+
`evidenceItems=${task.evidence?.length ?? 0}`,
|
|
94
|
+
`episodeItems=${task.episodes?.length ?? 0}`,
|
|
95
|
+
].join("; ");
|
|
96
|
+
const terminal = finishReview(task, "failed", { error });
|
|
74
97
|
activeTask = undefined;
|
|
75
98
|
send({ type: "review_cancel", requestId: task.requestId, reason: error });
|
|
76
|
-
send({ type: "review_failed", requestId: task.requestId, error });
|
|
99
|
+
send({ type: "review_failed", requestId: task.requestId, error, terminal });
|
|
77
100
|
}, KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs + 5_000);
|
|
78
101
|
activeDeadlineTimer.unref();
|
|
79
102
|
if (reviewStatus) {
|
|
@@ -93,24 +116,59 @@ function finishReview(
|
|
|
93
116
|
task: KnowledgeReviewTask,
|
|
94
117
|
outcome: "saved" | "failed",
|
|
95
118
|
detail: { generationId?: string; result?: PersistKnowledgeResult; error?: unknown },
|
|
96
|
-
):
|
|
119
|
+
): boolean {
|
|
97
120
|
clearTimeout(activeDeadlineTimer);
|
|
98
121
|
activeDeadlineTimer = undefined;
|
|
99
|
-
if (!reviewStatus) return;
|
|
122
|
+
if (!reviewStatus) return false;
|
|
123
|
+
const error = detail.error === undefined ? undefined : boundedError(detail.error);
|
|
124
|
+
const exhausted = outcome === "failed" && activeAttempt >= KNOWLEDGE_RUNTIME_DEFAULTS.review.maxAttempts;
|
|
125
|
+
const completedAt = new Date().toISOString();
|
|
126
|
+
const durationMs = Math.max(0, Date.now() - activeStartedAt);
|
|
100
127
|
reviewStatus.activeReview = undefined;
|
|
101
128
|
reviewStatus.lastReview = {
|
|
102
129
|
reviewKey: task.reviewKey,
|
|
103
130
|
sessionKey: task.sessionKey,
|
|
104
131
|
attempt: activeAttempt,
|
|
105
132
|
outcome,
|
|
106
|
-
completedAt
|
|
107
|
-
durationMs
|
|
133
|
+
completedAt,
|
|
134
|
+
durationMs,
|
|
108
135
|
generationId: detail.generationId,
|
|
109
136
|
result: detail.result,
|
|
110
|
-
error
|
|
137
|
+
error,
|
|
111
138
|
};
|
|
112
|
-
if (outcome === "saved")
|
|
139
|
+
if (outcome === "saved") {
|
|
140
|
+
reviewAttempts.delete(task.reviewKey);
|
|
141
|
+
delete reviewStatus.reviewFailures?.[task.reviewKey];
|
|
142
|
+
clearSupersededReviewState(task);
|
|
143
|
+
} else {
|
|
144
|
+
const reviewFailures = reviewStatus.reviewFailures ?? {};
|
|
145
|
+
const prior = reviewFailures[task.reviewKey];
|
|
146
|
+
reviewFailures[task.reviewKey] = {
|
|
147
|
+
sessionKey: task.sessionKey,
|
|
148
|
+
sessionFileHash: task.sessionFileHash,
|
|
149
|
+
attempts: [...(prior?.attempts ?? []), {
|
|
150
|
+
attempt: activeAttempt,
|
|
151
|
+
failedAt: completedAt,
|
|
152
|
+
durationMs,
|
|
153
|
+
error: error ?? "unknown-review-failure",
|
|
154
|
+
}].slice(-KNOWLEDGE_RUNTIME_DEFAULTS.review.maxAttempts),
|
|
155
|
+
};
|
|
156
|
+
reviewStatus.reviewFailures = Object.fromEntries(Object.entries(reviewFailures).slice(-100));
|
|
157
|
+
}
|
|
158
|
+
if (exhausted) {
|
|
159
|
+
const blockedReviews = reviewStatus.blockedReviews ?? {};
|
|
160
|
+
blockedReviews[task.reviewKey] = {
|
|
161
|
+
reviewKey: task.reviewKey,
|
|
162
|
+
sessionKey: task.sessionKey,
|
|
163
|
+
sessionFileHash: task.sessionFileHash,
|
|
164
|
+
blockedAt: new Date().toISOString(),
|
|
165
|
+
attempts: activeAttempt,
|
|
166
|
+
error: error ?? "unknown-review-failure",
|
|
167
|
+
};
|
|
168
|
+
reviewStatus.blockedReviews = Object.fromEntries(Object.entries(blockedReviews).slice(-100));
|
|
169
|
+
}
|
|
113
170
|
publishReviewStatus();
|
|
171
|
+
return exhausted;
|
|
114
172
|
}
|
|
115
173
|
|
|
116
174
|
function deferReview(task: KnowledgeReviewTask, reason: string): void {
|
|
@@ -155,7 +213,12 @@ function scheduleElection(delay = Math.floor(Math.random() * 500)): void {
|
|
|
155
213
|
}
|
|
156
214
|
|
|
157
215
|
function stepDown(reason: string): void {
|
|
158
|
-
if (activeTask)
|
|
216
|
+
if (activeTask) {
|
|
217
|
+
const attempts = reviewAttempts.get(activeTask.reviewKey) ?? 1;
|
|
218
|
+
if (attempts <= 1) reviewAttempts.delete(activeTask.reviewKey);
|
|
219
|
+
else reviewAttempts.set(activeTask.reviewKey, attempts - 1);
|
|
220
|
+
send({ type: "review_cancel", requestId: activeTask.requestId, reason });
|
|
221
|
+
}
|
|
159
222
|
if (reviewStatus) {
|
|
160
223
|
reviewStatus.state = "stepped-down";
|
|
161
224
|
reviewStatus.stepDownReason = reason;
|
|
@@ -260,8 +323,16 @@ function becomeLeader(server: Server): void {
|
|
|
260
323
|
scheduleElection();
|
|
261
324
|
});
|
|
262
325
|
writeLeaderMetadata(electedAt);
|
|
326
|
+
const priorStatus = readKnowledgeReviewStatus(home);
|
|
327
|
+
reviewAttempts.clear();
|
|
328
|
+
for (const [reviewKey, failure] of Object.entries(priorStatus?.reviewFailures ?? {})) {
|
|
329
|
+
const attempt = Math.max(0, ...failure.attempts.map((item) => item.attempt));
|
|
330
|
+
if (attempt > 0) reviewAttempts.set(reviewKey, attempt);
|
|
331
|
+
}
|
|
263
332
|
reviewStatus = {
|
|
264
333
|
version: 1, protocolVersion: 3, state: "leader", pid: process.pid, leaderToken, electedAt, updatedAt: electedAt,
|
|
334
|
+
blockedReviews: priorStatus?.blockedReviews,
|
|
335
|
+
reviewFailures: priorStatus?.reviewFailures,
|
|
265
336
|
};
|
|
266
337
|
publishReviewStatus();
|
|
267
338
|
send({ type: "leadership", state: "leader", leaderToken });
|
|
@@ -296,7 +367,8 @@ async function scanForReview(): Promise<void> {
|
|
|
296
367
|
}
|
|
297
368
|
try {
|
|
298
369
|
const manifest = loadCurrentManifest(home);
|
|
299
|
-
const
|
|
370
|
+
const blockedReviewKeys = new Set(Object.keys(reviewStatus?.blockedReviews ?? {}));
|
|
371
|
+
const task = findNextReviewTask(sessionsRoot, manifest.reviews, Date.now(), blockedReviewKeys);
|
|
300
372
|
if (!task) return;
|
|
301
373
|
activeTask = task;
|
|
302
374
|
beginReview(task);
|
|
@@ -315,13 +387,14 @@ async function scanForReview(): Promise<void> {
|
|
|
315
387
|
}
|
|
316
388
|
send({ type: "review_request", leaderToken, requestId: task.requestId, task });
|
|
317
389
|
} catch (error) {
|
|
318
|
-
|
|
319
|
-
|
|
390
|
+
const failedTask = activeTask;
|
|
391
|
+
const terminal = failedTask ? finishReview(failedTask, "failed", { error }) : true;
|
|
392
|
+
if (!failedTask && reviewStatus) {
|
|
320
393
|
reviewStatus.lastScanError = { at: new Date().toISOString(), error: boundedError(error) };
|
|
321
394
|
publishReviewStatus();
|
|
322
395
|
}
|
|
323
396
|
activeTask = undefined;
|
|
324
|
-
send({ type: "review_failed", error: boundedError(error) });
|
|
397
|
+
send({ type: "review_failed", requestId: failedTask?.requestId, error: boundedError(error), terminal });
|
|
325
398
|
}
|
|
326
399
|
}
|
|
327
400
|
|
|
@@ -331,7 +404,8 @@ function handleReviewResult(message: Extract<KnowledgeWorkerInput, { type: "revi
|
|
|
331
404
|
let committed = false;
|
|
332
405
|
try {
|
|
333
406
|
if (message.error || message.raw === undefined) throw new Error(message.error || "Knowledge review returned no content");
|
|
334
|
-
const
|
|
407
|
+
const envelope = parseKnowledgeReviewEnvelope(message.raw);
|
|
408
|
+
const result = commitKnowledgeReview(envelope.candidates, task, leaderToken, home, envelope.episodeDecisions);
|
|
335
409
|
const generationId = loadCurrentManifest(home).generationId;
|
|
336
410
|
finishReview(task, "saved", { generationId, result });
|
|
337
411
|
send({ type: "review_saved", requestId: task.requestId, generationId, result });
|
|
@@ -341,8 +415,8 @@ function handleReviewResult(message: Extract<KnowledgeWorkerInput, { type: "revi
|
|
|
341
415
|
}
|
|
342
416
|
committed = true;
|
|
343
417
|
} catch (error) {
|
|
344
|
-
finishReview(task, "failed", { error });
|
|
345
|
-
send({ type: "review_failed", requestId: task.requestId, error: boundedError(error) });
|
|
418
|
+
const terminal = finishReview(task, "failed", { error });
|
|
419
|
+
send({ type: "review_failed", requestId: task.requestId, error: boundedError(error), terminal });
|
|
346
420
|
} finally {
|
|
347
421
|
activeTask = undefined;
|
|
348
422
|
if (committed) queueMicrotask(() => void scanForReview());
|
|
@@ -3,6 +3,16 @@ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/giu;
|
|
|
3
3
|
const JWT_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/gu;
|
|
4
4
|
const SECRET_ASSIGNMENT_RE = /\b(access[_-]?key|secret(?:[_-]?(?:access|key))?|password|passwd|token|credential|client[_-]?secret)\b(\s*[:=]\s*)[^\s,;]+/giu;
|
|
5
5
|
const URL_CREDENTIAL_RE = /(https?:\/\/)[^\s/@:]+:[^\s/@]+@/giu;
|
|
6
|
+
const INJECTED_SKILL_BLOCK_RE = /<skill\b[^>]*>[\s\S]*?<\/skill>/giu;
|
|
7
|
+
const INJECTED_SKILL_MARKER_RE = /<skill\b[^>]*>/iu;
|
|
8
|
+
|
|
9
|
+
export function containsInjectedSkill(value: string): boolean {
|
|
10
|
+
return INJECTED_SKILL_MARKER_RE.test(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stripInjectedSkillBlocks(value: string): string {
|
|
14
|
+
return value.replace(INJECTED_SKILL_BLOCK_RE, "");
|
|
15
|
+
}
|
|
6
16
|
|
|
7
17
|
export function sanitizeKnowledgeText(value: string): string {
|
|
8
18
|
return value
|
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
|
|
10
10
|
import { canonicalizeDirectory } from "../working-directory.ts";
|
|
11
|
+
import { buildKnowledgeEpisodes, buildKnowledgeEvidence } from "./evidence.ts";
|
|
11
12
|
import { knowledgeDeltaDigest } from "./extractor.ts";
|
|
12
|
-
import { sanitizeKnowledgeText } from "./sanitize.ts";
|
|
13
|
+
import { containsInjectedSkill, sanitizeKnowledgeText, stripInjectedSkillBlocks } from "./sanitize.ts";
|
|
13
14
|
import { projectKnowledgeKey } from "./store.ts";
|
|
14
15
|
import type { KnowledgeReviewCursor, KnowledgeReviewTask } from "./types.ts";
|
|
15
16
|
|
|
@@ -21,7 +22,6 @@ interface ReviewPiece {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const FAILURE_EVIDENCE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|mistyp(?:e|ed)|typo)\b|not supported|失败|错误|不支持|无效)/iu;
|
|
24
|
-
const INJECTED_SKILL_RE = /<skill\b[^>]*>[\s\S]*?<\/skill>/giu;
|
|
25
25
|
const SAFE_KNOWLEDGE_ID_RE = /^[a-z0-9][a-z0-9-]{0,79}$/u;
|
|
26
26
|
const MAX_RECALL_QUERY_CHARS = 4_000;
|
|
27
27
|
const MAX_LOADED_KNOWLEDGE_IDS = 8;
|
|
@@ -53,8 +53,13 @@ function reviewPiece(entry: SessionEntry, index: number): ReviewPiece | undefine
|
|
|
53
53
|
if (entry.type !== "message" || !entry.message || typeof entry.message !== "object") return undefined;
|
|
54
54
|
const message = entry.message as unknown as Record<string, unknown>;
|
|
55
55
|
const role = typeof message.role === "string" ? message.role : "message";
|
|
56
|
-
const
|
|
56
|
+
const rawContent = textContent(message.content);
|
|
57
|
+
const injectedContext = role === "user" && containsInjectedSkill(rawContent);
|
|
58
|
+
const content = sanitizeKnowledgeText(injectedContext ? stripInjectedSkillBlocks(rawContent) : rawContent);
|
|
57
59
|
if (!content) return undefined;
|
|
60
|
+
if (injectedContext) {
|
|
61
|
+
return { index, priority: 3, category: "assistant", text: `[injected_context]\n${content.slice(0, 4_000)}` };
|
|
62
|
+
}
|
|
58
63
|
const priority = role === "user" ? 0 : role === "toolResult" && FAILURE_EVIDENCE_RE.test(content) ? 2
|
|
59
64
|
: role === "toolResult" ? 4 : 3;
|
|
60
65
|
const cap = role === "user" ? 8_000 : role === "toolResult" ? 1_500 : 4_000;
|
|
@@ -62,8 +67,39 @@ function reviewPiece(entry: SessionEntry, index: number): ReviewPiece | undefine
|
|
|
62
67
|
return { index, priority, category, text: `[${role}]\n${content.slice(0, cap)}` };
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
function
|
|
66
|
-
const
|
|
70
|
+
function entryId(entry: SessionEntry): string | undefined {
|
|
71
|
+
const value = (entry as unknown as Record<string, unknown>).id;
|
|
72
|
+
return typeof value === "string" ? value : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function knowledgeLookupResultEntryIds(entries: SessionEntry[]): Set<string> {
|
|
76
|
+
const lookupCalls = new Set<string>();
|
|
77
|
+
const resultEntries = new Set<string>();
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const message = messageRecord(entry);
|
|
80
|
+
const content = message?.content;
|
|
81
|
+
if (Array.isArray(content)) {
|
|
82
|
+
for (const raw of content) {
|
|
83
|
+
if (!raw || typeof raw !== "object") continue;
|
|
84
|
+
const item = raw as Record<string, unknown>;
|
|
85
|
+
if (item.type === "toolCall" && item.name === "hwcode_knowledge_lookup" && typeof item.id === "string") {
|
|
86
|
+
lookupCalls.add(item.id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (message?.role === "toolResult" && typeof message.toolCallId === "string" && lookupCalls.has(message.toolCallId)) {
|
|
91
|
+
const id = entryId(entry);
|
|
92
|
+
if (id) resultEntries.add(id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return resultEntries;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildDelta(entries: SessionEntry[], excludedEntryIds: ReadonlySet<string> = new Set()): string {
|
|
99
|
+
const pieces = entries.map((entry, index) => {
|
|
100
|
+
const id = entryId(entry);
|
|
101
|
+
return id && excludedEntryIds.has(id) ? undefined : reviewPiece(entry, index);
|
|
102
|
+
}).filter((piece): piece is ReviewPiece => Boolean(piece));
|
|
67
103
|
const selected: ReviewPiece[] = [];
|
|
68
104
|
const categoryRemaining: Record<ReviewPiece["category"], number> = {
|
|
69
105
|
user: 14_000, workflow: 6_000, summary: 4_000, assistant: 8_000, tool: 4_000,
|
|
@@ -86,8 +122,11 @@ function buildDelta(entries: SessionEntry[]): string {
|
|
|
86
122
|
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars);
|
|
87
123
|
}
|
|
88
124
|
|
|
89
|
-
function buildValidationContext(entries: SessionEntry[]): string {
|
|
90
|
-
const pieces = entries.map(
|
|
125
|
+
function buildValidationContext(entries: SessionEntry[], excludedEntryIds: ReadonlySet<string> = new Set()): string {
|
|
126
|
+
const pieces = entries.map((entry, index) => {
|
|
127
|
+
const id = entryId(entry);
|
|
128
|
+
return id && excludedEntryIds.has(id) ? undefined : reviewPiece(entry, index);
|
|
129
|
+
}).filter((piece): piece is ReviewPiece => Boolean(piece));
|
|
91
130
|
const latestWorkflow = pieces.filter((piece) => piece.category === "workflow").at(-1);
|
|
92
131
|
const evidence = pieces.filter((piece) => (
|
|
93
132
|
(piece.category === "tool" || piece.category === "assistant" || piece.category === "summary")
|
|
@@ -182,7 +221,9 @@ function buildRecallQuery(entries: SessionEntry[], ids: string[]): string {
|
|
|
182
221
|
for (const entry of entries.slice().reverse()) {
|
|
183
222
|
const message = messageRecord(entry);
|
|
184
223
|
if (!message || message.role !== "user") continue;
|
|
185
|
-
const
|
|
224
|
+
const raw = textContent(message.content);
|
|
225
|
+
if (containsInjectedSkill(raw)) continue;
|
|
226
|
+
const text = compactRecallValue(raw, 800);
|
|
186
227
|
if (text) lines.push(`User request: ${text}`);
|
|
187
228
|
if (lines.filter((line) => line.startsWith("User request:")).length >= 2) break;
|
|
188
229
|
}
|
|
@@ -240,13 +281,23 @@ export function readReviewTask(
|
|
|
240
281
|
: -1;
|
|
241
282
|
const pending = branch.slice(cursorIndex + 1);
|
|
242
283
|
if (pending.length === 0) return undefined;
|
|
243
|
-
const
|
|
284
|
+
const pendingEntryIds = new Set(pending.map((entry) => entry.id));
|
|
285
|
+
const evidenceWindow = cursorIndex >= 0 ? branch.slice(Math.max(0, cursorIndex - 40)) : pending;
|
|
286
|
+
const evidence = buildKnowledgeEvidence(evidenceWindow).filter((item) => pendingEntryIds.has(item.entryId));
|
|
287
|
+
const episodes = buildKnowledgeEpisodes(evidence);
|
|
288
|
+
const lookupResultEntryIds = knowledgeLookupResultEntryIds(branch);
|
|
289
|
+
const delta = buildDelta(pending, lookupResultEntryIds);
|
|
244
290
|
const loadedIds = loadedKnowledgeIds(pending);
|
|
245
291
|
const recallQuery = buildRecallQuery(pending, loadedIds);
|
|
246
|
-
const context = cursorIndex >= 0
|
|
292
|
+
const context = cursorIndex >= 0
|
|
293
|
+
? buildValidationContext(branch.slice(0, cursorIndex + 1), lookupResultEntryIds)
|
|
294
|
+
: "";
|
|
247
295
|
const firstEntryId = pending[0].id;
|
|
248
296
|
const lastEntryId = pending[pending.length - 1].id;
|
|
249
|
-
const deltaDigest = knowledgeDeltaDigest(
|
|
297
|
+
const deltaDigest = knowledgeDeltaDigest([
|
|
298
|
+
delta || `${firstEntryId}\0${lastEntryId}`, recallQuery, loadedIds.join("\0"),
|
|
299
|
+
JSON.stringify(evidence), JSON.stringify(episodes),
|
|
300
|
+
].join("\0"));
|
|
250
301
|
const sessionFileHash = hash(resolve(sessionFile), 16);
|
|
251
302
|
const sessionKey = hash(header.id, 24);
|
|
252
303
|
const reviewKey = hash(`${header.id}\0${firstEntryId}\0${lastEntryId}\0${deltaDigest}`);
|
|
@@ -255,6 +306,7 @@ export function readReviewTask(
|
|
|
255
306
|
requestId: randomUUID(), reviewKey, sessionId: header.id, sessionKey, sessionFileHash,
|
|
256
307
|
projectRoot, projectKey: projectKnowledgeKey(projectRoot),
|
|
257
308
|
firstEntryId, lastEntryId, context, delta, recallQuery, loadedKnowledgeIds: loadedIds,
|
|
309
|
+
evidence, episodes,
|
|
258
310
|
deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
259
311
|
};
|
|
260
312
|
}
|
|
@@ -263,6 +315,7 @@ export function findNextReviewTask(
|
|
|
263
315
|
sessionsRoot: string,
|
|
264
316
|
reviews: Record<string, KnowledgeReviewCursor>,
|
|
265
317
|
now = Date.now(),
|
|
318
|
+
excludedReviewKeys: ReadonlySet<string> = new Set(),
|
|
266
319
|
): KnowledgeReviewTask | undefined {
|
|
267
320
|
const files = discoverSessionFiles(sessionsRoot).map((path) => {
|
|
268
321
|
try { return { path, mtimeMs: statSync(path).mtimeMs }; } catch { return undefined; }
|
|
@@ -274,7 +327,7 @@ export function findNextReviewTask(
|
|
|
274
327
|
let task = readReviewTask(file.path, cursor, now);
|
|
275
328
|
const stableCursor = task ? reviews[task.sessionKey] : undefined;
|
|
276
329
|
if (task && stableCursor && stableCursor !== cursor) task = readReviewTask(file.path, stableCursor, now);
|
|
277
|
-
if (task) return task;
|
|
330
|
+
if (task && !excludedReviewKeys.has(task.reviewKey)) return task;
|
|
278
331
|
}
|
|
279
332
|
return undefined;
|
|
280
333
|
}
|