@hadooppei/hwcode 1.0.19 → 1.0.22
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 +19 -10
- package/.pi/dist/lib/knowledge/extractor.js +41 -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 +5 -1
- package/.pi/extensions/knowledge.ts +60 -19
- package/.pi/extensions/workflows/cloud/provider-tools.ts +37 -7
- package/.pi/lib/knowledge/evidence.ts +19 -9
- package/.pi/lib/knowledge/extractor.ts +39 -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 +5 -1
- 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;
|
|
@@ -81,8 +81,11 @@ function boundedExcerpt(value, limit = 900) {
|
|
|
81
81
|
if (clean.length <= limit)
|
|
82
82
|
return clean;
|
|
83
83
|
const lines = clean.split("\n");
|
|
84
|
-
const
|
|
85
|
-
|
|
84
|
+
const priority = (line) => REVERSAL_RE.test(line) ? 0
|
|
85
|
+
: FAILURE_RE.test(line) && VERIFICATION_RE.test(line) ? 1
|
|
86
|
+
: FAILURE_RE.test(line) ? 2 : 3;
|
|
87
|
+
const significant = lines.filter((line) => (REVERSAL_RE.test(line) || FAILURE_RE.test(line) || VERIFICATION_RE.test(line))).sort((left, right) => priority(left) - priority(right));
|
|
88
|
+
const selected = [...lines.slice(0, 2), ...significant.slice(0, 7), ...lines.slice(-2)];
|
|
86
89
|
return [...new Set(selected)].join("\n").slice(0, limit);
|
|
87
90
|
}
|
|
88
91
|
function compactArguments(args) {
|
|
@@ -152,10 +155,8 @@ function entryIdOf(entry, index) {
|
|
|
152
155
|
function evidenceId(entryId, suffix) {
|
|
153
156
|
return `ev-${digest(`${entryId}\0${suffix}`)}`;
|
|
154
157
|
}
|
|
155
|
-
function outcomeFor(message
|
|
156
|
-
|
|
157
|
-
return "failure";
|
|
158
|
-
return "success";
|
|
158
|
+
function outcomeFor(message) {
|
|
159
|
+
return message.isError === true ? "failure" : "success";
|
|
159
160
|
}
|
|
160
161
|
function userKind(text) {
|
|
161
162
|
return CORRECTION_RE.test(text) ? "user-correction" : "user-directive";
|
|
@@ -189,6 +190,10 @@ export function buildKnowledgeEvidence(entries) {
|
|
|
189
190
|
const role = typeof message.role === "string" ? message.role : "";
|
|
190
191
|
const text = textContent(content).trim();
|
|
191
192
|
if (role === "user" && text) {
|
|
193
|
+
// Skill bodies and workflow activation envelopes are injected as user-role messages by
|
|
194
|
+
// the host. They are execution context, not an explicit human directive to persist.
|
|
195
|
+
if (containsInjectedSkill(text))
|
|
196
|
+
return;
|
|
192
197
|
evidence.push({
|
|
193
198
|
id: evidenceId(entryId, "user"), entryId, timestamp, kind: userKind(text),
|
|
194
199
|
subject: "User requirement or correction", outcome: "unknown", excerpt: boundedExcerpt(text, 700),
|
|
@@ -198,11 +203,15 @@ export function buildKnowledgeEvidence(entries) {
|
|
|
198
203
|
if (role === "toolResult" && text) {
|
|
199
204
|
const toolCallId = typeof message.toolCallId === "string" ? message.toolCallId : "";
|
|
200
205
|
const call = calls.get(toolCallId);
|
|
201
|
-
|
|
206
|
+
// Recalled knowledge is comparison context, not fresh execution evidence. Treating a
|
|
207
|
+
// lookup result as verification would let knowledge reinforce itself merely by loading it.
|
|
208
|
+
if (call?.name === "hwcode_knowledge_lookup")
|
|
209
|
+
return;
|
|
210
|
+
const outcome = outcomeFor(message);
|
|
202
211
|
const callText = call ? `[call] ${call.name}: ${compactArguments(call.args)}\n` : "";
|
|
203
212
|
evidence.push({
|
|
204
213
|
id: evidenceId(entryId, toolCallId || "tool"), entryId, timestamp,
|
|
205
|
-
kind: outcome === "failure" ? "tool-failure" :
|
|
214
|
+
kind: outcome === "failure" ? "tool-failure" : "tool-success",
|
|
206
215
|
subject: evidenceSubject(call, text), operation: operationName(call), outcome,
|
|
207
216
|
excerpt: boundedExcerpt(`${callText}[result] ${text}`),
|
|
208
217
|
});
|
|
@@ -211,7 +220,7 @@ export function buildKnowledgeEvidence(entries) {
|
|
|
211
220
|
if (role === "assistant" && text && (FAILURE_RE.test(text) || VERIFICATION_RE.test(text) || REVERSAL_RE.test(text))) {
|
|
212
221
|
evidence.push({
|
|
213
222
|
id: evidenceId(entryId, "assistant"), entryId, timestamp,
|
|
214
|
-
kind:
|
|
223
|
+
kind: "assistant-claim",
|
|
215
224
|
subject: "Assistant diagnosis or conclusion", outcome: "unknown", excerpt: boundedExcerpt(text, 800),
|
|
216
225
|
});
|
|
217
226
|
}
|
|
@@ -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:
|
|
@@ -13,9 +14,11 @@ Treat failed, unsupported, invalid, or corrected operations as negative evidence
|
|
|
13
14
|
Use storageHint "rule" only for one short imperative instruction of at most 240 characters. A rule must not contain incident narration, example resource names, IDs, timestamps, or evidence details. Use "topic" for multi-step SOPs and detailed experience.
|
|
14
15
|
Rank knowledge by future time saved, repeated failures, hypothesis reversals, user corrections, verified resolution, reuse, and novelty. Do not favor the final task result merely because it completed the objective. Every supplied episode scoring 45 or higher must have an episodeDecisions entry, even when skipped.
|
|
15
16
|
Relevant existing knowledge may be supplied. Cheap recall only proposes related items; it does not prove duplication. Compare execution environment, symptom, and root cause. Use action "reinforce" only when the exact target already covers every claim and there is no new content. Use "extend" for new compatible claims, "correct" when direct evidence disproves old content, "retire" when the whole target is invalid, and "add" for a distinct environment or root cause. Never turn lexical similarity into reinforcement.
|
|
17
|
+
Prefer the most specific existing topic that already covers a claim over a broad conventions or SOP topic.
|
|
16
18
|
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
17
19
|
The prior validation context is only for checking facts and contradictions; do not persist it again unless the new delta independently reinforces it. Prefer accumulated workflow successfulSteps and failedApproaches over assistant narration.
|
|
18
20
|
Every technical claim must have a stable claim key and cite evidenceIds from the supplied evidence ledger. Tool success, tool failure, and verification evidence outrank assistant narration. Do not support a service-wide or universal claim with one operation-specific failure. Evidence for an unrelated parameter error cannot support an API-version claim.
|
|
21
|
+
Keep each candidate atomic: put separable claim sets in separate candidates, omit any claim without direct cited evidence, and prioritize the highest-value supported candidates. One unsupported claim rejects its entire candidate.
|
|
19
22
|
Each summary must be one complete sentence of at most 160 characters. Keep each topic body under 2200 characters. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered. Return at most 3 candidates.
|
|
20
23
|
The body must not repeat the title as a Markdown H1. The storage renderer supplies the H1. Replace any necessary example resource value with an obvious placeholder instead of a real project, namespace, image, network, or resource name.
|
|
21
24
|
Return: {"episodeDecisions":[{"episodeId":"ep-id","decision":"add|reinforce|extend|correct|retire|skip","targetId":"existing-id or omitted","reason":"..."}],"candidates":[{"key":"stable semantic key","identityKey":"platform/runtime/problem/root-cause","targetId":"existing-id or null","episodeIds":["ep-id"],"title":"...","summary":"one complete sentence","keywords":["..."],"scope":"global|project","body":"markdown body or only the new section for extend","claims":[{"key":"stable claim key","text":"one precise claim","evidenceIds":["ev-id"]}],"evidenceIds":["ev-id"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|extend|correct|retire","explicitUserDirective":false,"durability":"stable"}]}
|
|
@@ -37,117 +40,54 @@ export function buildKnowledgeExtractionPrompt(projectRoot, delta, context = "",
|
|
|
37
40
|
].join("\n");
|
|
38
41
|
}
|
|
39
42
|
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
|
-
}
|
|
43
|
+
const parsed = parseEnvelopeObject(text);
|
|
44
|
+
if (!Array.isArray(parsed.candidates))
|
|
45
|
+
throw new Error("Knowledge reviewer response is missing candidates[]");
|
|
46
|
+
const validDecisions = new Set(["add", "reinforce", "extend", "correct", "retire", "skip"]);
|
|
47
|
+
const decisions = Array.isArray(parsed.episodeDecisions)
|
|
48
|
+
? parsed.episodeDecisions.filter((item) => {
|
|
49
|
+
if (!item || typeof item !== "object")
|
|
50
|
+
return false;
|
|
51
|
+
const value = item;
|
|
52
|
+
return typeof value.episodeId === "string" && validDecisions.has(String(value.decision))
|
|
53
|
+
&& typeof value.reason === "string";
|
|
54
|
+
})
|
|
55
|
+
: [];
|
|
56
|
+
return {
|
|
57
|
+
episodeDecisions: decisions,
|
|
58
|
+
candidates: parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates),
|
|
59
|
+
};
|
|
62
60
|
}
|
|
63
61
|
function parseEnvelopeObject(text) {
|
|
64
62
|
const trimmed = text.trim();
|
|
65
63
|
const unfenced = trimmed.replace(/^```(?:json)?\s*/iu, "").replace(/\s*```$/u, "");
|
|
66
64
|
const start = unfenced.indexOf("{");
|
|
67
65
|
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;
|
|
66
|
+
if (start < 0 || end <= start) {
|
|
67
|
+
throw new Error([
|
|
68
|
+
"Knowledge reviewer did not return a complete JSON object",
|
|
69
|
+
`responseChars=${unfenced.length}`,
|
|
70
|
+
`responseExcerpt=${JSON.stringify(compactKnowledgeText(unfenced, 1_200))}`,
|
|
71
|
+
].join("; "));
|
|
72
|
+
}
|
|
73
|
+
const payload = unfenced.slice(start, end + 1);
|
|
80
74
|
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);
|
|
75
|
+
return JSON.parse(payload);
|
|
85
76
|
}
|
|
86
77
|
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;
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
const offsetMatch = /position\s+(\d+)/iu.exec(message);
|
|
80
|
+
const offset = offsetMatch ? Number.parseInt(offsetMatch[1], 10) : -1;
|
|
81
|
+
const excerptStart = offset >= 0 ? Math.max(0, offset - 600) : 0;
|
|
82
|
+
const excerptEnd = offset >= 0 ? Math.min(payload.length, offset + 600) : Math.min(payload.length, 1_200);
|
|
83
|
+
const excerpt = compactKnowledgeText(payload.slice(excerptStart, excerptEnd), 1_200);
|
|
84
|
+
throw new Error([
|
|
85
|
+
`Knowledge reviewer returned invalid JSON: ${message}`,
|
|
86
|
+
`responseChars=${payload.length}`,
|
|
87
|
+
offset >= 0 ? `errorOffset=${offset}` : undefined,
|
|
88
|
+
`responseExcerpt=${JSON.stringify(excerpt)}`,
|
|
89
|
+
].filter(Boolean).join("; "));
|
|
149
90
|
}
|
|
150
|
-
return -1;
|
|
151
91
|
}
|
|
152
92
|
export function knowledgeDeltaDigest(delta) {
|
|
153
93
|
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;
|
|
@@ -30,14 +30,18 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
|
|
|
30
30
|
intervalMs: 60_000,
|
|
31
31
|
idleMs: 60_000,
|
|
32
32
|
capabilityPollMs: 5_000,
|
|
33
|
+
requestTimeoutMs: 45_000,
|
|
33
34
|
modelTimeoutMs: 180_000,
|
|
35
|
+
maxOutputTokens: 4_096,
|
|
36
|
+
maxAttempts: 2,
|
|
37
|
+
maxFailureDetailChars: 4_000,
|
|
34
38
|
maxDeltaChars: 12_000,
|
|
35
39
|
maxPriorContextChars: 4_000,
|
|
36
40
|
maxNarrativeChars: 6_000,
|
|
37
41
|
maxEvidenceItems: 24,
|
|
38
42
|
maxEvidenceChars: 12_000,
|
|
39
43
|
mandatoryEpisodeScore: 45,
|
|
40
|
-
maxExistingContextItems:
|
|
44
|
+
maxExistingContextItems: 8,
|
|
41
45
|
maxExistingBodyItems: 2,
|
|
42
46
|
maxExistingBodyChars: 600,
|
|
43
47
|
maxCandidates: 3,
|