@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
|
@@ -5,8 +5,8 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { parentPort, workerData } from "node:worker_threads";
|
|
6
6
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
7
7
|
import { userRuntimePaths } from "../runtime/paths.js";
|
|
8
|
-
import {
|
|
9
|
-
import { writeKnowledgeReviewStatus } from "./review-status.js";
|
|
8
|
+
import { parseKnowledgeReviewEnvelope } from "./extractor.js";
|
|
9
|
+
import { readKnowledgeReviewStatus, writeKnowledgeReviewStatus } from "./review-status.js";
|
|
10
10
|
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
11
11
|
import { findNextReviewTask } from "./session-scanner.js";
|
|
12
12
|
import { commitKnowledgeReview, ensureKnowledgeDirectories, loadCurrentManifest } from "./store.js";
|
|
@@ -54,9 +54,28 @@ function publishReviewStatus() {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
function boundedError(error) {
|
|
57
|
-
return sanitizeKnowledgeText(error instanceof Error ? error.message : String(error))
|
|
57
|
+
return sanitizeKnowledgeText(error instanceof Error ? error.message : String(error))
|
|
58
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxFailureDetailChars);
|
|
59
|
+
}
|
|
60
|
+
function clearSupersededReviewState(task) {
|
|
61
|
+
if (!reviewStatus)
|
|
62
|
+
return;
|
|
63
|
+
for (const field of ["blockedReviews", "reviewFailures"]) {
|
|
64
|
+
const records = reviewStatus[field];
|
|
65
|
+
if (!records)
|
|
66
|
+
continue;
|
|
67
|
+
for (const [reviewKey, record] of Object.entries(records)) {
|
|
68
|
+
if (record.sessionKey === task.sessionKey && reviewKey !== task.reviewKey) {
|
|
69
|
+
delete records[reviewKey];
|
|
70
|
+
reviewAttempts.delete(reviewKey);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (Object.keys(records).length === 0)
|
|
74
|
+
reviewStatus[field] = undefined;
|
|
75
|
+
}
|
|
58
76
|
}
|
|
59
77
|
function beginReview(task) {
|
|
78
|
+
clearSupersededReviewState(task);
|
|
60
79
|
activeAttempt = (reviewAttempts.get(task.reviewKey) ?? 0) + 1;
|
|
61
80
|
reviewAttempts.set(task.reviewKey, activeAttempt);
|
|
62
81
|
activeStartedAt = Date.now();
|
|
@@ -64,11 +83,17 @@ function beginReview(task) {
|
|
|
64
83
|
activeDeadlineTimer = setTimeout(() => {
|
|
65
84
|
if (activeTask?.requestId !== task.requestId)
|
|
66
85
|
return;
|
|
67
|
-
const error =
|
|
68
|
-
|
|
86
|
+
const error = [
|
|
87
|
+
`knowledge-review-result-timeout after ${KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs + 5_000}ms`,
|
|
88
|
+
`reviewKey=${task.reviewKey}`,
|
|
89
|
+
`deltaChars=${task.delta.length}`,
|
|
90
|
+
`evidenceItems=${task.evidence?.length ?? 0}`,
|
|
91
|
+
`episodeItems=${task.episodes?.length ?? 0}`,
|
|
92
|
+
].join("; ");
|
|
93
|
+
const terminal = finishReview(task, "failed", { error });
|
|
69
94
|
activeTask = undefined;
|
|
70
95
|
send({ type: "review_cancel", requestId: task.requestId, reason: error });
|
|
71
|
-
send({ type: "review_failed", requestId: task.requestId, error });
|
|
96
|
+
send({ type: "review_failed", requestId: task.requestId, error, terminal });
|
|
72
97
|
}, KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs + 5_000);
|
|
73
98
|
activeDeadlineTimer.unref();
|
|
74
99
|
if (reviewStatus) {
|
|
@@ -87,22 +112,57 @@ function finishReview(task, outcome, detail) {
|
|
|
87
112
|
clearTimeout(activeDeadlineTimer);
|
|
88
113
|
activeDeadlineTimer = undefined;
|
|
89
114
|
if (!reviewStatus)
|
|
90
|
-
return;
|
|
115
|
+
return false;
|
|
116
|
+
const error = detail.error === undefined ? undefined : boundedError(detail.error);
|
|
117
|
+
const exhausted = outcome === "failed" && activeAttempt >= KNOWLEDGE_RUNTIME_DEFAULTS.review.maxAttempts;
|
|
118
|
+
const completedAt = new Date().toISOString();
|
|
119
|
+
const durationMs = Math.max(0, Date.now() - activeStartedAt);
|
|
91
120
|
reviewStatus.activeReview = undefined;
|
|
92
121
|
reviewStatus.lastReview = {
|
|
93
122
|
reviewKey: task.reviewKey,
|
|
94
123
|
sessionKey: task.sessionKey,
|
|
95
124
|
attempt: activeAttempt,
|
|
96
125
|
outcome,
|
|
97
|
-
completedAt
|
|
98
|
-
durationMs
|
|
126
|
+
completedAt,
|
|
127
|
+
durationMs,
|
|
99
128
|
generationId: detail.generationId,
|
|
100
129
|
result: detail.result,
|
|
101
|
-
error
|
|
130
|
+
error,
|
|
102
131
|
};
|
|
103
|
-
if (outcome === "saved")
|
|
132
|
+
if (outcome === "saved") {
|
|
104
133
|
reviewAttempts.delete(task.reviewKey);
|
|
134
|
+
delete reviewStatus.reviewFailures?.[task.reviewKey];
|
|
135
|
+
clearSupersededReviewState(task);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const reviewFailures = reviewStatus.reviewFailures ?? {};
|
|
139
|
+
const prior = reviewFailures[task.reviewKey];
|
|
140
|
+
reviewFailures[task.reviewKey] = {
|
|
141
|
+
sessionKey: task.sessionKey,
|
|
142
|
+
sessionFileHash: task.sessionFileHash,
|
|
143
|
+
attempts: [...(prior?.attempts ?? []), {
|
|
144
|
+
attempt: activeAttempt,
|
|
145
|
+
failedAt: completedAt,
|
|
146
|
+
durationMs,
|
|
147
|
+
error: error ?? "unknown-review-failure",
|
|
148
|
+
}].slice(-KNOWLEDGE_RUNTIME_DEFAULTS.review.maxAttempts),
|
|
149
|
+
};
|
|
150
|
+
reviewStatus.reviewFailures = Object.fromEntries(Object.entries(reviewFailures).slice(-100));
|
|
151
|
+
}
|
|
152
|
+
if (exhausted) {
|
|
153
|
+
const blockedReviews = reviewStatus.blockedReviews ?? {};
|
|
154
|
+
blockedReviews[task.reviewKey] = {
|
|
155
|
+
reviewKey: task.reviewKey,
|
|
156
|
+
sessionKey: task.sessionKey,
|
|
157
|
+
sessionFileHash: task.sessionFileHash,
|
|
158
|
+
blockedAt: new Date().toISOString(),
|
|
159
|
+
attempts: activeAttempt,
|
|
160
|
+
error: error ?? "unknown-review-failure",
|
|
161
|
+
};
|
|
162
|
+
reviewStatus.blockedReviews = Object.fromEntries(Object.entries(blockedReviews).slice(-100));
|
|
163
|
+
}
|
|
105
164
|
publishReviewStatus();
|
|
165
|
+
return exhausted;
|
|
106
166
|
}
|
|
107
167
|
function deferReview(task, reason) {
|
|
108
168
|
clearTimeout(activeDeadlineTimer);
|
|
@@ -149,8 +209,14 @@ function scheduleElection(delay = Math.floor(Math.random() * 500)) {
|
|
|
149
209
|
timer.unref();
|
|
150
210
|
}
|
|
151
211
|
function stepDown(reason) {
|
|
152
|
-
if (activeTask)
|
|
212
|
+
if (activeTask) {
|
|
213
|
+
const attempts = reviewAttempts.get(activeTask.reviewKey) ?? 1;
|
|
214
|
+
if (attempts <= 1)
|
|
215
|
+
reviewAttempts.delete(activeTask.reviewKey);
|
|
216
|
+
else
|
|
217
|
+
reviewAttempts.set(activeTask.reviewKey, attempts - 1);
|
|
153
218
|
send({ type: "review_cancel", requestId: activeTask.requestId, reason });
|
|
219
|
+
}
|
|
154
220
|
if (reviewStatus) {
|
|
155
221
|
reviewStatus.state = "stepped-down";
|
|
156
222
|
reviewStatus.stepDownReason = reason;
|
|
@@ -277,8 +343,17 @@ function becomeLeader(server) {
|
|
|
277
343
|
scheduleElection();
|
|
278
344
|
});
|
|
279
345
|
writeLeaderMetadata(electedAt);
|
|
346
|
+
const priorStatus = readKnowledgeReviewStatus(home);
|
|
347
|
+
reviewAttempts.clear();
|
|
348
|
+
for (const [reviewKey, failure] of Object.entries(priorStatus?.reviewFailures ?? {})) {
|
|
349
|
+
const attempt = Math.max(0, ...failure.attempts.map((item) => item.attempt));
|
|
350
|
+
if (attempt > 0)
|
|
351
|
+
reviewAttempts.set(reviewKey, attempt);
|
|
352
|
+
}
|
|
280
353
|
reviewStatus = {
|
|
281
354
|
version: 1, protocolVersion: 3, state: "leader", pid: process.pid, leaderToken, electedAt, updatedAt: electedAt,
|
|
355
|
+
blockedReviews: priorStatus?.blockedReviews,
|
|
356
|
+
reviewFailures: priorStatus?.reviewFailures,
|
|
282
357
|
};
|
|
283
358
|
publishReviewStatus();
|
|
284
359
|
send({ type: "leadership", state: "leader", leaderToken });
|
|
@@ -315,7 +390,8 @@ async function scanForReview() {
|
|
|
315
390
|
}
|
|
316
391
|
try {
|
|
317
392
|
const manifest = loadCurrentManifest(home);
|
|
318
|
-
const
|
|
393
|
+
const blockedReviewKeys = new Set(Object.keys(reviewStatus?.blockedReviews ?? {}));
|
|
394
|
+
const task = findNextReviewTask(sessionsRoot, manifest.reviews, Date.now(), blockedReviewKeys);
|
|
319
395
|
if (!task)
|
|
320
396
|
return;
|
|
321
397
|
activeTask = task;
|
|
@@ -336,14 +412,14 @@ async function scanForReview() {
|
|
|
336
412
|
send({ type: "review_request", leaderToken, requestId: task.requestId, task });
|
|
337
413
|
}
|
|
338
414
|
catch (error) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
415
|
+
const failedTask = activeTask;
|
|
416
|
+
const terminal = failedTask ? finishReview(failedTask, "failed", { error }) : true;
|
|
417
|
+
if (!failedTask && reviewStatus) {
|
|
342
418
|
reviewStatus.lastScanError = { at: new Date().toISOString(), error: boundedError(error) };
|
|
343
419
|
publishReviewStatus();
|
|
344
420
|
}
|
|
345
421
|
activeTask = undefined;
|
|
346
|
-
send({ type: "review_failed", error: boundedError(error) });
|
|
422
|
+
send({ type: "review_failed", requestId: failedTask?.requestId, error: boundedError(error), terminal });
|
|
347
423
|
}
|
|
348
424
|
}
|
|
349
425
|
function handleReviewResult(message) {
|
|
@@ -354,7 +430,8 @@ function handleReviewResult(message) {
|
|
|
354
430
|
try {
|
|
355
431
|
if (message.error || message.raw === undefined)
|
|
356
432
|
throw new Error(message.error || "Knowledge review returned no content");
|
|
357
|
-
const
|
|
433
|
+
const envelope = parseKnowledgeReviewEnvelope(message.raw);
|
|
434
|
+
const result = commitKnowledgeReview(envelope.candidates, task, leaderToken, home, envelope.episodeDecisions);
|
|
358
435
|
const generationId = loadCurrentManifest(home).generationId;
|
|
359
436
|
finishReview(task, "saved", { generationId, result });
|
|
360
437
|
send({ type: "review_saved", requestId: task.requestId, generationId, result });
|
|
@@ -365,8 +442,8 @@ function handleReviewResult(message) {
|
|
|
365
442
|
committed = true;
|
|
366
443
|
}
|
|
367
444
|
catch (error) {
|
|
368
|
-
finishReview(task, "failed", { error });
|
|
369
|
-
send({ type: "review_failed", requestId: task.requestId, error: boundedError(error) });
|
|
445
|
+
const terminal = finishReview(task, "failed", { error });
|
|
446
|
+
send({ type: "review_failed", requestId: task.requestId, error: boundedError(error), terminal });
|
|
370
447
|
}
|
|
371
448
|
finally {
|
|
372
449
|
activeTask = undefined;
|
|
@@ -3,6 +3,14 @@ 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
|
+
export function containsInjectedSkill(value) {
|
|
9
|
+
return INJECTED_SKILL_MARKER_RE.test(value);
|
|
10
|
+
}
|
|
11
|
+
export function stripInjectedSkillBlocks(value) {
|
|
12
|
+
return value.replace(INJECTED_SKILL_BLOCK_RE, "");
|
|
13
|
+
}
|
|
6
14
|
export function sanitizeKnowledgeText(value) {
|
|
7
15
|
return value
|
|
8
16
|
.replace(PRIVATE_KEY_RE, "<redacted-private-key>")
|
|
@@ -4,11 +4,11 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { buildContextEntries, parseSessionEntries, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
6
6
|
import { canonicalizeDirectory } from "../working-directory.js";
|
|
7
|
+
import { buildKnowledgeEpisodes, buildKnowledgeEvidence } from "./evidence.js";
|
|
7
8
|
import { knowledgeDeltaDigest } from "./extractor.js";
|
|
8
|
-
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
9
|
+
import { containsInjectedSkill, sanitizeKnowledgeText, stripInjectedSkillBlocks } from "./sanitize.js";
|
|
9
10
|
import { projectKnowledgeKey } from "./store.js";
|
|
10
11
|
const FAILURE_EVIDENCE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|mistyp(?:e|ed)|typo)\b|not supported|失败|错误|不支持|无效)/iu;
|
|
11
|
-
const INJECTED_SKILL_RE = /<skill\b[^>]*>[\s\S]*?<\/skill>/giu;
|
|
12
12
|
const SAFE_KNOWLEDGE_ID_RE = /^[a-z0-9][a-z0-9-]{0,79}$/u;
|
|
13
13
|
const MAX_RECALL_QUERY_CHARS = 4_000;
|
|
14
14
|
const MAX_LOADED_KNOWLEDGE_IDS = 8;
|
|
@@ -41,17 +41,53 @@ function reviewPiece(entry, index) {
|
|
|
41
41
|
return undefined;
|
|
42
42
|
const message = entry.message;
|
|
43
43
|
const role = typeof message.role === "string" ? message.role : "message";
|
|
44
|
-
const
|
|
44
|
+
const rawContent = textContent(message.content);
|
|
45
|
+
const injectedContext = role === "user" && containsInjectedSkill(rawContent);
|
|
46
|
+
const content = sanitizeKnowledgeText(injectedContext ? stripInjectedSkillBlocks(rawContent) : rawContent);
|
|
45
47
|
if (!content)
|
|
46
48
|
return undefined;
|
|
49
|
+
if (injectedContext) {
|
|
50
|
+
return { index, priority: 3, category: "assistant", text: `[injected_context]\n${content.slice(0, 4_000)}` };
|
|
51
|
+
}
|
|
47
52
|
const priority = role === "user" ? 0 : role === "toolResult" && FAILURE_EVIDENCE_RE.test(content) ? 2
|
|
48
53
|
: role === "toolResult" ? 4 : 3;
|
|
49
54
|
const cap = role === "user" ? 8_000 : role === "toolResult" ? 1_500 : 4_000;
|
|
50
55
|
const category = role === "user" ? "user" : role === "toolResult" ? "tool" : "assistant";
|
|
51
56
|
return { index, priority, category, text: `[${role}]\n${content.slice(0, cap)}` };
|
|
52
57
|
}
|
|
53
|
-
function
|
|
54
|
-
const
|
|
58
|
+
function entryId(entry) {
|
|
59
|
+
const value = entry.id;
|
|
60
|
+
return typeof value === "string" ? value : undefined;
|
|
61
|
+
}
|
|
62
|
+
function knowledgeLookupResultEntryIds(entries) {
|
|
63
|
+
const lookupCalls = new Set();
|
|
64
|
+
const resultEntries = new Set();
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const message = messageRecord(entry);
|
|
67
|
+
const content = message?.content;
|
|
68
|
+
if (Array.isArray(content)) {
|
|
69
|
+
for (const raw of content) {
|
|
70
|
+
if (!raw || typeof raw !== "object")
|
|
71
|
+
continue;
|
|
72
|
+
const item = raw;
|
|
73
|
+
if (item.type === "toolCall" && item.name === "hwcode_knowledge_lookup" && typeof item.id === "string") {
|
|
74
|
+
lookupCalls.add(item.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (message?.role === "toolResult" && typeof message.toolCallId === "string" && lookupCalls.has(message.toolCallId)) {
|
|
79
|
+
const id = entryId(entry);
|
|
80
|
+
if (id)
|
|
81
|
+
resultEntries.add(id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return resultEntries;
|
|
85
|
+
}
|
|
86
|
+
function buildDelta(entries, excludedEntryIds = new Set()) {
|
|
87
|
+
const pieces = entries.map((entry, index) => {
|
|
88
|
+
const id = entryId(entry);
|
|
89
|
+
return id && excludedEntryIds.has(id) ? undefined : reviewPiece(entry, index);
|
|
90
|
+
}).filter((piece) => Boolean(piece));
|
|
55
91
|
const selected = [];
|
|
56
92
|
const categoryRemaining = {
|
|
57
93
|
user: 14_000, workflow: 6_000, summary: 4_000, assistant: 8_000, tool: 4_000,
|
|
@@ -78,8 +114,11 @@ function buildDelta(entries) {
|
|
|
78
114
|
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
79
115
|
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars);
|
|
80
116
|
}
|
|
81
|
-
function buildValidationContext(entries) {
|
|
82
|
-
const pieces = entries.map(
|
|
117
|
+
function buildValidationContext(entries, excludedEntryIds = new Set()) {
|
|
118
|
+
const pieces = entries.map((entry, index) => {
|
|
119
|
+
const id = entryId(entry);
|
|
120
|
+
return id && excludedEntryIds.has(id) ? undefined : reviewPiece(entry, index);
|
|
121
|
+
}).filter((piece) => Boolean(piece));
|
|
83
122
|
const latestWorkflow = pieces.filter((piece) => piece.category === "workflow").at(-1);
|
|
84
123
|
const evidence = pieces.filter((piece) => ((piece.category === "tool" || piece.category === "assistant" || piece.category === "summary")
|
|
85
124
|
&& FAILURE_EVIDENCE_RE.test(piece.text)));
|
|
@@ -183,7 +222,10 @@ function buildRecallQuery(entries, ids) {
|
|
|
183
222
|
const message = messageRecord(entry);
|
|
184
223
|
if (!message || message.role !== "user")
|
|
185
224
|
continue;
|
|
186
|
-
const
|
|
225
|
+
const raw = textContent(message.content);
|
|
226
|
+
if (containsInjectedSkill(raw))
|
|
227
|
+
continue;
|
|
228
|
+
const text = compactRecallValue(raw, 800);
|
|
187
229
|
if (text)
|
|
188
230
|
lines.push(`User request: ${text}`);
|
|
189
231
|
if (lines.filter((line) => line.startsWith("User request:")).length >= 2)
|
|
@@ -268,13 +310,23 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
268
310
|
const pending = branch.slice(cursorIndex + 1);
|
|
269
311
|
if (pending.length === 0)
|
|
270
312
|
return undefined;
|
|
271
|
-
const
|
|
313
|
+
const pendingEntryIds = new Set(pending.map((entry) => entry.id));
|
|
314
|
+
const evidenceWindow = cursorIndex >= 0 ? branch.slice(Math.max(0, cursorIndex - 40)) : pending;
|
|
315
|
+
const evidence = buildKnowledgeEvidence(evidenceWindow).filter((item) => pendingEntryIds.has(item.entryId));
|
|
316
|
+
const episodes = buildKnowledgeEpisodes(evidence);
|
|
317
|
+
const lookupResultEntryIds = knowledgeLookupResultEntryIds(branch);
|
|
318
|
+
const delta = buildDelta(pending, lookupResultEntryIds);
|
|
272
319
|
const loadedIds = loadedKnowledgeIds(pending);
|
|
273
320
|
const recallQuery = buildRecallQuery(pending, loadedIds);
|
|
274
|
-
const context = cursorIndex >= 0
|
|
321
|
+
const context = cursorIndex >= 0
|
|
322
|
+
? buildValidationContext(branch.slice(0, cursorIndex + 1), lookupResultEntryIds)
|
|
323
|
+
: "";
|
|
275
324
|
const firstEntryId = pending[0].id;
|
|
276
325
|
const lastEntryId = pending[pending.length - 1].id;
|
|
277
|
-
const deltaDigest = knowledgeDeltaDigest(
|
|
326
|
+
const deltaDigest = knowledgeDeltaDigest([
|
|
327
|
+
delta || `${firstEntryId}\0${lastEntryId}`, recallQuery, loadedIds.join("\0"),
|
|
328
|
+
JSON.stringify(evidence), JSON.stringify(episodes),
|
|
329
|
+
].join("\0"));
|
|
278
330
|
const sessionFileHash = hash(resolve(sessionFile), 16);
|
|
279
331
|
const sessionKey = hash(header.id, 24);
|
|
280
332
|
const reviewKey = hash(`${header.id}\0${firstEntryId}\0${lastEntryId}\0${deltaDigest}`);
|
|
@@ -283,10 +335,11 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
283
335
|
requestId: randomUUID(), reviewKey, sessionId: header.id, sessionKey, sessionFileHash,
|
|
284
336
|
projectRoot, projectKey: projectKnowledgeKey(projectRoot),
|
|
285
337
|
firstEntryId, lastEntryId, context, delta, recallQuery, loadedKnowledgeIds: loadedIds,
|
|
338
|
+
evidence, episodes,
|
|
286
339
|
deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
287
340
|
};
|
|
288
341
|
}
|
|
289
|
-
export function findNextReviewTask(sessionsRoot, reviews, now = Date.now()) {
|
|
342
|
+
export function findNextReviewTask(sessionsRoot, reviews, now = Date.now(), excludedReviewKeys = new Set()) {
|
|
290
343
|
const files = discoverSessionFiles(sessionsRoot).map((path) => {
|
|
291
344
|
try {
|
|
292
345
|
return { path, mtimeMs: statSync(path).mtimeMs };
|
|
@@ -303,7 +356,7 @@ export function findNextReviewTask(sessionsRoot, reviews, now = Date.now()) {
|
|
|
303
356
|
const stableCursor = task ? reviews[task.sessionKey] : undefined;
|
|
304
357
|
if (task && stableCursor && stableCursor !== cursor)
|
|
305
358
|
task = readReviewTask(file.path, stableCursor, now);
|
|
306
|
-
if (task)
|
|
359
|
+
if (task && !excludedReviewKeys.has(task.reviewKey))
|
|
307
360
|
return task;
|
|
308
361
|
}
|
|
309
362
|
return undefined;
|