@hadooppei/hwcode 1.0.19 → 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 +9 -1
- package/.pi/dist/lib/knowledge/extractor.js +39 -101
- package/.pi/dist/lib/knowledge/review-worker.js +94 -18
- package/.pi/dist/lib/knowledge/sanitize.js +8 -0
- package/.pi/dist/lib/knowledge/session-scanner.js +56 -12
- package/.pi/dist/lib/runtime/defaults.js +2 -0
- package/.pi/extensions/knowledge.ts +28 -13
- package/.pi/extensions/workflows/cloud/provider-tools.ts +37 -7
- package/.pi/lib/knowledge/evidence.ts +7 -1
- package/.pi/lib/knowledge/extractor.ts +37 -78
- package/.pi/lib/knowledge/review-status.ts +18 -0
- package/.pi/lib/knowledge/review-worker.ts +91 -18
- package/.pi/lib/knowledge/sanitize.ts +10 -0
- package/.pi/lib/knowledge/session-scanner.ts +55 -11
- package/.pi/lib/knowledge/worker-protocol.ts +1 -1
- package/.pi/lib/runtime/defaults.ts +2 -0
- package/.pi/lib/workflows/cloud/process.ts +19 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
2
|
+
import { containsInjectedSkill, sanitizeKnowledgeText } from "./sanitize.js";
|
|
3
3
|
const FAILURE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|timeout|timed out|unreachable|servfail|refused)\b|not supported|失败|错误|不支持|超时|无法|不可达)/iu;
|
|
4
4
|
const VERIFICATION_RE = /(?:\b(?:verified|verification|active|running|succeeded|success|http\s*=?\s*200|downloaded newer image)\b|验证通过|运行成功|成功创建|已恢复)/iu;
|
|
5
5
|
const CORRECTION_RE = /(?:\b(?:must|should|always|never|correction|most valuable)\b|必须|应该|不应该|纠正|最有价值|不要|务必)/iu;
|
|
@@ -189,6 +189,10 @@ export function buildKnowledgeEvidence(entries) {
|
|
|
189
189
|
const role = typeof message.role === "string" ? message.role : "";
|
|
190
190
|
const text = textContent(content).trim();
|
|
191
191
|
if (role === "user" && text) {
|
|
192
|
+
// Skill bodies and workflow activation envelopes are injected as user-role messages by
|
|
193
|
+
// the host. They are execution context, not an explicit human directive to persist.
|
|
194
|
+
if (containsInjectedSkill(text))
|
|
195
|
+
return;
|
|
192
196
|
evidence.push({
|
|
193
197
|
id: evidenceId(entryId, "user"), entryId, timestamp, kind: userKind(text),
|
|
194
198
|
subject: "User requirement or correction", outcome: "unknown", excerpt: boundedExcerpt(text, 700),
|
|
@@ -198,6 +202,10 @@ export function buildKnowledgeEvidence(entries) {
|
|
|
198
202
|
if (role === "toolResult" && text) {
|
|
199
203
|
const toolCallId = typeof message.toolCallId === "string" ? message.toolCallId : "";
|
|
200
204
|
const call = calls.get(toolCallId);
|
|
205
|
+
// Recalled knowledge is comparison context, not fresh execution evidence. Treating a
|
|
206
|
+
// lookup result as verification would let knowledge reinforce itself merely by loading it.
|
|
207
|
+
if (call?.name === "hwcode_knowledge_lookup")
|
|
208
|
+
return;
|
|
201
209
|
const outcome = outcomeFor(message, text);
|
|
202
210
|
const callText = call ? `[call] ${call.name}: ${compactArguments(call.args)}\n` : "";
|
|
203
211
|
evidence.push({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
3
|
+
import { compactKnowledgeText } from "./sanitize.js";
|
|
3
4
|
export const KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT = `You are HWCode's background knowledge reviewer.
|
|
4
5
|
Review the supplied structured evidence and conversation delta. Return strict JSON and no markdown.
|
|
5
6
|
Persist only knowledge that is likely to be useful in future sessions:
|
|
@@ -37,117 +38,54 @@ export function buildKnowledgeExtractionPrompt(projectRoot, delta, context = "",
|
|
|
37
38
|
].join("\n");
|
|
38
39
|
}
|
|
39
40
|
export function parseKnowledgeReviewEnvelope(text) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return { episodeDecisions: [], candidates: parseCandidateEnvelope(text) };
|
|
61
|
-
}
|
|
41
|
+
const parsed = parseEnvelopeObject(text);
|
|
42
|
+
if (!Array.isArray(parsed.candidates))
|
|
43
|
+
throw new Error("Knowledge reviewer response is missing candidates[]");
|
|
44
|
+
const validDecisions = new Set(["add", "reinforce", "extend", "correct", "retire", "skip"]);
|
|
45
|
+
const decisions = Array.isArray(parsed.episodeDecisions)
|
|
46
|
+
? parsed.episodeDecisions.filter((item) => {
|
|
47
|
+
if (!item || typeof item !== "object")
|
|
48
|
+
return false;
|
|
49
|
+
const value = item;
|
|
50
|
+
return typeof value.episodeId === "string" && validDecisions.has(String(value.decision))
|
|
51
|
+
&& typeof value.reason === "string";
|
|
52
|
+
})
|
|
53
|
+
: [];
|
|
54
|
+
return {
|
|
55
|
+
episodeDecisions: decisions,
|
|
56
|
+
candidates: parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates),
|
|
57
|
+
};
|
|
62
58
|
}
|
|
63
59
|
function parseEnvelopeObject(text) {
|
|
64
60
|
const trimmed = text.trim();
|
|
65
61
|
const unfenced = trimmed.replace(/^```(?:json)?\s*/iu, "").replace(/\s*```$/u, "");
|
|
66
62
|
const start = unfenced.indexOf("{");
|
|
67
63
|
const end = unfenced.lastIndexOf("}");
|
|
68
|
-
if (start < 0 || end <= start)
|
|
69
|
-
throw new Error(
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const end = unfenced.lastIndexOf("}");
|
|
77
|
-
if (start < 0 || end <= start)
|
|
78
|
-
throw new Error("Knowledge reviewer did not return a JSON object");
|
|
79
|
-
let strictError;
|
|
64
|
+
if (start < 0 || end <= start) {
|
|
65
|
+
throw new Error([
|
|
66
|
+
"Knowledge reviewer did not return a complete JSON object",
|
|
67
|
+
`responseChars=${unfenced.length}`,
|
|
68
|
+
`responseExcerpt=${JSON.stringify(compactKnowledgeText(unfenced, 1_200))}`,
|
|
69
|
+
].join("; "));
|
|
70
|
+
}
|
|
71
|
+
const payload = unfenced.slice(start, end + 1);
|
|
80
72
|
try {
|
|
81
|
-
|
|
82
|
-
if (!Array.isArray(parsed.candidates))
|
|
83
|
-
throw new Error("Knowledge reviewer response is missing candidates[]");
|
|
84
|
-
return parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates);
|
|
73
|
+
return JSON.parse(payload);
|
|
85
74
|
}
|
|
86
75
|
catch (error) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
escaped = false;
|
|
100
|
-
else if (character === "\\")
|
|
101
|
-
escaped = true;
|
|
102
|
-
else if (character === '"')
|
|
103
|
-
inString = false;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (character === '"') {
|
|
107
|
-
inString = true;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (character !== "{")
|
|
111
|
-
continue;
|
|
112
|
-
const objectEnd = balancedObjectEnd(unfenced, index);
|
|
113
|
-
if (objectEnd < 0)
|
|
114
|
-
break;
|
|
115
|
-
try {
|
|
116
|
-
candidates.push(JSON.parse(unfenced.slice(index, objectEnd + 1)));
|
|
117
|
-
}
|
|
118
|
-
catch { /* Salvage other complete candidates. */ }
|
|
119
|
-
index = objectEnd;
|
|
120
|
-
}
|
|
121
|
-
if (candidates.length > 0)
|
|
122
|
-
return candidates;
|
|
123
|
-
}
|
|
124
|
-
throw strictError;
|
|
125
|
-
}
|
|
126
|
-
function balancedObjectEnd(value, start) {
|
|
127
|
-
let depth = 0;
|
|
128
|
-
let inString = false;
|
|
129
|
-
let escaped = false;
|
|
130
|
-
for (let index = start; index < value.length; index++) {
|
|
131
|
-
const character = value[index];
|
|
132
|
-
if (inString) {
|
|
133
|
-
if (escaped)
|
|
134
|
-
escaped = false;
|
|
135
|
-
else if (character === "\\")
|
|
136
|
-
escaped = true;
|
|
137
|
-
else if (character === '"')
|
|
138
|
-
inString = false;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (character === '"') {
|
|
142
|
-
inString = true;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (character === "{")
|
|
146
|
-
depth++;
|
|
147
|
-
else if (character === "}" && --depth === 0)
|
|
148
|
-
return index;
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
const offsetMatch = /position\s+(\d+)/iu.exec(message);
|
|
78
|
+
const offset = offsetMatch ? Number.parseInt(offsetMatch[1], 10) : -1;
|
|
79
|
+
const excerptStart = offset >= 0 ? Math.max(0, offset - 600) : 0;
|
|
80
|
+
const excerptEnd = offset >= 0 ? Math.min(payload.length, offset + 600) : Math.min(payload.length, 1_200);
|
|
81
|
+
const excerpt = compactKnowledgeText(payload.slice(excerptStart, excerptEnd), 1_200);
|
|
82
|
+
throw new Error([
|
|
83
|
+
`Knowledge reviewer returned invalid JSON: ${message}`,
|
|
84
|
+
`responseChars=${payload.length}`,
|
|
85
|
+
offset >= 0 ? `errorOffset=${offset}` : undefined,
|
|
86
|
+
`responseExcerpt=${JSON.stringify(excerpt)}`,
|
|
87
|
+
].filter(Boolean).join("; "));
|
|
149
88
|
}
|
|
150
|
-
return -1;
|
|
151
89
|
}
|
|
152
90
|
export function knowledgeDeltaDigest(delta) {
|
|
153
91
|
return createHash("sha256").update(delta).digest("hex");
|
|
@@ -6,7 +6,7 @@ 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
8
|
import { parseKnowledgeReviewEnvelope } from "./extractor.js";
|
|
9
|
-
import { writeKnowledgeReviewStatus } from "./review-status.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) {
|
|
@@ -366,8 +442,8 @@ function handleReviewResult(message) {
|
|
|
366
442
|
committed = true;
|
|
367
443
|
}
|
|
368
444
|
catch (error) {
|
|
369
|
-
finishReview(task, "failed", { error });
|
|
370
|
-
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 });
|
|
371
447
|
}
|
|
372
448
|
finally {
|
|
373
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>")
|
|
@@ -6,10 +6,9 @@ import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
|
6
6
|
import { canonicalizeDirectory } from "../working-directory.js";
|
|
7
7
|
import { buildKnowledgeEpisodes, buildKnowledgeEvidence } from "./evidence.js";
|
|
8
8
|
import { knowledgeDeltaDigest } from "./extractor.js";
|
|
9
|
-
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
9
|
+
import { containsInjectedSkill, sanitizeKnowledgeText, stripInjectedSkillBlocks } from "./sanitize.js";
|
|
10
10
|
import { projectKnowledgeKey } from "./store.js";
|
|
11
11
|
const FAILURE_EVIDENCE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|mistyp(?:e|ed)|typo)\b|not supported|失败|错误|不支持|无效)/iu;
|
|
12
|
-
const INJECTED_SKILL_RE = /<skill\b[^>]*>[\s\S]*?<\/skill>/giu;
|
|
13
12
|
const SAFE_KNOWLEDGE_ID_RE = /^[a-z0-9][a-z0-9-]{0,79}$/u;
|
|
14
13
|
const MAX_RECALL_QUERY_CHARS = 4_000;
|
|
15
14
|
const MAX_LOADED_KNOWLEDGE_IDS = 8;
|
|
@@ -42,17 +41,53 @@ function reviewPiece(entry, index) {
|
|
|
42
41
|
return undefined;
|
|
43
42
|
const message = entry.message;
|
|
44
43
|
const role = typeof message.role === "string" ? message.role : "message";
|
|
45
|
-
const
|
|
44
|
+
const rawContent = textContent(message.content);
|
|
45
|
+
const injectedContext = role === "user" && containsInjectedSkill(rawContent);
|
|
46
|
+
const content = sanitizeKnowledgeText(injectedContext ? stripInjectedSkillBlocks(rawContent) : rawContent);
|
|
46
47
|
if (!content)
|
|
47
48
|
return undefined;
|
|
49
|
+
if (injectedContext) {
|
|
50
|
+
return { index, priority: 3, category: "assistant", text: `[injected_context]\n${content.slice(0, 4_000)}` };
|
|
51
|
+
}
|
|
48
52
|
const priority = role === "user" ? 0 : role === "toolResult" && FAILURE_EVIDENCE_RE.test(content) ? 2
|
|
49
53
|
: role === "toolResult" ? 4 : 3;
|
|
50
54
|
const cap = role === "user" ? 8_000 : role === "toolResult" ? 1_500 : 4_000;
|
|
51
55
|
const category = role === "user" ? "user" : role === "toolResult" ? "tool" : "assistant";
|
|
52
56
|
return { index, priority, category, text: `[${role}]\n${content.slice(0, cap)}` };
|
|
53
57
|
}
|
|
54
|
-
function
|
|
55
|
-
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));
|
|
56
91
|
const selected = [];
|
|
57
92
|
const categoryRemaining = {
|
|
58
93
|
user: 14_000, workflow: 6_000, summary: 4_000, assistant: 8_000, tool: 4_000,
|
|
@@ -79,8 +114,11 @@ function buildDelta(entries) {
|
|
|
79
114
|
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
80
115
|
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars);
|
|
81
116
|
}
|
|
82
|
-
function buildValidationContext(entries) {
|
|
83
|
-
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));
|
|
84
122
|
const latestWorkflow = pieces.filter((piece) => piece.category === "workflow").at(-1);
|
|
85
123
|
const evidence = pieces.filter((piece) => ((piece.category === "tool" || piece.category === "assistant" || piece.category === "summary")
|
|
86
124
|
&& FAILURE_EVIDENCE_RE.test(piece.text)));
|
|
@@ -184,7 +222,10 @@ function buildRecallQuery(entries, ids) {
|
|
|
184
222
|
const message = messageRecord(entry);
|
|
185
223
|
if (!message || message.role !== "user")
|
|
186
224
|
continue;
|
|
187
|
-
const
|
|
225
|
+
const raw = textContent(message.content);
|
|
226
|
+
if (containsInjectedSkill(raw))
|
|
227
|
+
continue;
|
|
228
|
+
const text = compactRecallValue(raw, 800);
|
|
188
229
|
if (text)
|
|
189
230
|
lines.push(`User request: ${text}`);
|
|
190
231
|
if (lines.filter((line) => line.startsWith("User request:")).length >= 2)
|
|
@@ -273,10 +314,13 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
273
314
|
const evidenceWindow = cursorIndex >= 0 ? branch.slice(Math.max(0, cursorIndex - 40)) : pending;
|
|
274
315
|
const evidence = buildKnowledgeEvidence(evidenceWindow).filter((item) => pendingEntryIds.has(item.entryId));
|
|
275
316
|
const episodes = buildKnowledgeEpisodes(evidence);
|
|
276
|
-
const
|
|
317
|
+
const lookupResultEntryIds = knowledgeLookupResultEntryIds(branch);
|
|
318
|
+
const delta = buildDelta(pending, lookupResultEntryIds);
|
|
277
319
|
const loadedIds = loadedKnowledgeIds(pending);
|
|
278
320
|
const recallQuery = buildRecallQuery(pending, loadedIds);
|
|
279
|
-
const context = cursorIndex >= 0
|
|
321
|
+
const context = cursorIndex >= 0
|
|
322
|
+
? buildValidationContext(branch.slice(0, cursorIndex + 1), lookupResultEntryIds)
|
|
323
|
+
: "";
|
|
280
324
|
const firstEntryId = pending[0].id;
|
|
281
325
|
const lastEntryId = pending[pending.length - 1].id;
|
|
282
326
|
const deltaDigest = knowledgeDeltaDigest([
|
|
@@ -295,7 +339,7 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
295
339
|
deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
296
340
|
};
|
|
297
341
|
}
|
|
298
|
-
export function findNextReviewTask(sessionsRoot, reviews, now = Date.now()) {
|
|
342
|
+
export function findNextReviewTask(sessionsRoot, reviews, now = Date.now(), excludedReviewKeys = new Set()) {
|
|
299
343
|
const files = discoverSessionFiles(sessionsRoot).map((path) => {
|
|
300
344
|
try {
|
|
301
345
|
return { path, mtimeMs: statSync(path).mtimeMs };
|
|
@@ -312,7 +356,7 @@ export function findNextReviewTask(sessionsRoot, reviews, now = Date.now()) {
|
|
|
312
356
|
const stableCursor = task ? reviews[task.sessionKey] : undefined;
|
|
313
357
|
if (task && stableCursor && stableCursor !== cursor)
|
|
314
358
|
task = readReviewTask(file.path, stableCursor, now);
|
|
315
|
-
if (task)
|
|
359
|
+
if (task && !excludedReviewKeys.has(task.reviewKey))
|
|
316
360
|
return task;
|
|
317
361
|
}
|
|
318
362
|
return undefined;
|
|
@@ -31,6 +31,8 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
|
|
|
31
31
|
idleMs: 60_000,
|
|
32
32
|
capabilityPollMs: 5_000,
|
|
33
33
|
modelTimeoutMs: 180_000,
|
|
34
|
+
maxAttempts: 2,
|
|
35
|
+
maxFailureDetailChars: 4_000,
|
|
34
36
|
maxDeltaChars: 12_000,
|
|
35
37
|
maxPriorContextChars: 4_000,
|
|
36
38
|
maxNarrativeChars: 6_000,
|
|
@@ -22,6 +22,7 @@ interface ActiveReview {
|
|
|
22
22
|
leaderToken: string;
|
|
23
23
|
controller: AbortController;
|
|
24
24
|
abortReason?: string;
|
|
25
|
+
diagnostic?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
interface ProcessKnowledgeRuntime {
|
|
@@ -117,7 +118,7 @@ function reportWorkerIssue(error: unknown): void {
|
|
|
117
118
|
const prior = runtime.lastWorkerIssue;
|
|
118
119
|
if (prior?.message === message && now - prior.reportedAt < 60_000) return;
|
|
119
120
|
runtime.lastWorkerIssue = { message, reportedAt: now };
|
|
120
|
-
const rendered = `HWCode knowledge worker failed: ${message}`;
|
|
121
|
+
const rendered = `HWCode knowledge worker failed: ${message.slice(0, 600)}`;
|
|
121
122
|
if (runtime.context?.hasUI) runtime.context.ui.notify(rendered, "error");
|
|
122
123
|
else console.error(rendered);
|
|
123
124
|
}
|
|
@@ -162,7 +163,7 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
162
163
|
runtime.activeReview = review;
|
|
163
164
|
timeout = setTimeout(() => {
|
|
164
165
|
if (controller.signal.aborted) return;
|
|
165
|
-
review!.abortReason =
|
|
166
|
+
review!.abortReason = `knowledge-review-model-timeout after ${KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs}ms${review!.diagnostic ? `; ${review!.diagnostic}` : ""}`;
|
|
166
167
|
controller.abort();
|
|
167
168
|
}, KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs);
|
|
168
169
|
timeout.unref();
|
|
@@ -210,6 +211,23 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
210
211
|
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxExistingBodyChars);
|
|
211
212
|
return content ? { ...reference, content } : reference;
|
|
212
213
|
});
|
|
214
|
+
const prompt = buildKnowledgeExtractionPrompt(
|
|
215
|
+
message.task.projectRoot,
|
|
216
|
+
message.task.delta.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxNarrativeChars),
|
|
217
|
+
message.task.context,
|
|
218
|
+
related,
|
|
219
|
+
promptContext.evidence,
|
|
220
|
+
promptContext.episodes,
|
|
221
|
+
);
|
|
222
|
+
review.diagnostic = [
|
|
223
|
+
`reviewKey=${message.task.reviewKey}`,
|
|
224
|
+
`model=${ctx.model.provider}/${ctx.model.id}`,
|
|
225
|
+
`promptChars=${prompt.length}`,
|
|
226
|
+
`deltaChars=${message.task.delta.length}`,
|
|
227
|
+
`evidenceItems=${promptContext.evidence.length}`,
|
|
228
|
+
`episodeItems=${promptContext.episodes.length}`,
|
|
229
|
+
`existingItems=${related.length}`,
|
|
230
|
+
].join("; ");
|
|
213
231
|
const response = await ctx.modelRegistry.complete(
|
|
214
232
|
ctx.model,
|
|
215
233
|
{
|
|
@@ -218,14 +236,7 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
218
236
|
role: "user",
|
|
219
237
|
content: [{
|
|
220
238
|
type: "text",
|
|
221
|
-
text:
|
|
222
|
-
message.task.projectRoot,
|
|
223
|
-
message.task.delta.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxNarrativeChars),
|
|
224
|
-
message.task.context,
|
|
225
|
-
related,
|
|
226
|
-
promptContext.evidence,
|
|
227
|
-
promptContext.episodes,
|
|
228
|
-
),
|
|
239
|
+
text: prompt,
|
|
229
240
|
}],
|
|
230
241
|
timestamp: Date.now(),
|
|
231
242
|
}],
|
|
@@ -237,10 +248,14 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
237
248
|
.map((item) => item.text).join("\n");
|
|
238
249
|
post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, raw });
|
|
239
250
|
} catch (error) {
|
|
240
|
-
const
|
|
251
|
+
const baseReason = review?.controller.signal.aborted
|
|
241
252
|
? review.abortReason ?? "knowledge-review-interrupted"
|
|
242
253
|
: error instanceof Error ? error.message : String(error);
|
|
243
|
-
|
|
254
|
+
const expectedInterruption = baseReason === "model-executor-is-busy" || isExpectedReviewInterruption(baseReason);
|
|
255
|
+
const reason = review?.diagnostic && !expectedInterruption && !baseReason.includes("reviewKey=")
|
|
256
|
+
? `${baseReason}; ${review.diagnostic}`
|
|
257
|
+
: baseReason;
|
|
258
|
+
if (expectedInterruption) {
|
|
244
259
|
post({ type: "review_deferred", leaderToken: message.leaderToken, requestId: message.requestId, reason });
|
|
245
260
|
} else {
|
|
246
261
|
post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, error: reason });
|
|
@@ -263,7 +278,7 @@ function handleWorkerMessage(message: KnowledgeWorkerOutput): void {
|
|
|
263
278
|
cancelActiveReview(message.reason);
|
|
264
279
|
return;
|
|
265
280
|
}
|
|
266
|
-
if (message.type === "review_failed") reportWorkerIssue(message.error);
|
|
281
|
+
if (message.type === "review_failed" && message.terminal !== false) reportWorkerIssue(message.error);
|
|
267
282
|
}
|
|
268
283
|
|
|
269
284
|
function ensureWorker(): Worker | undefined {
|