@hadooppei/hwcode 1.0.10 → 1.0.12
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/extractor.js +90 -9
- package/.pi/dist/lib/knowledge/review-status.js +20 -0
- package/.pi/dist/lib/knowledge/review-worker.js +105 -5
- package/.pi/dist/lib/knowledge/sanitize.js +20 -0
- package/.pi/dist/lib/knowledge/session-scanner.js +46 -9
- package/.pi/dist/lib/knowledge/store.js +83 -10
- package/.pi/dist/lib/runtime/defaults.js +9 -5
- package/.pi/dist/lib/runtime/paths.js +1 -0
- package/.pi/extensions/knowledge.ts +37 -16
- package/.pi/lib/knowledge/extractor.ts +84 -8
- package/.pi/lib/knowledge/review-status.ts +55 -0
- package/.pi/lib/knowledge/review-worker.ts +106 -6
- package/.pi/lib/knowledge/sanitize.ts +20 -0
- package/.pi/lib/knowledge/session-scanner.ts +43 -9
- package/.pi/lib/knowledge/store.ts +77 -10
- package/.pi/lib/knowledge/types.ts +4 -0
- package/.pi/lib/runtime/defaults.ts +9 -5
- package/.pi/lib/runtime/paths.ts +2 -0
- package/package.json +1 -1
|
@@ -7,13 +7,26 @@ Persist only knowledge that is likely to be useful in future sessions:
|
|
|
7
7
|
- verified engineering rules, successful procedures, or expensive failed approaches;
|
|
8
8
|
- reusable architecture, testing, debugging, deployment, or operational knowledge.
|
|
9
9
|
Do not persist task summaries, guesses, temporary IDs, credentials, secrets, private data, or facts recoverable by simply reading the repository.
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Do not persist mutable inventory such as current cloud resources, names, availability, status, timestamps, or account snapshots. Preserve the discovery method or selection rule instead. If useful procedure is mixed with volatile facts, remove the volatile facts.
|
|
11
|
+
Treat failed, unsupported, invalid, or corrected operations as negative evidence. Never present an operation as valid when the supplied evidence says it failed; use only a verified alternative or record an explicit warning. Never invent commands or parameters absent from successful evidence.
|
|
12
|
+
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.
|
|
13
|
+
Relevant existing knowledge may be supplied. If a candidate has the same intent as an existing item, do not add a translated, renamed, or reformatted duplicate. Set targetId to that exact existing ID and action to "reinforce" when the existing content remains correct, or "revise" only when the delta explicitly proves a correction. Use targetId null only for genuinely new knowledge.
|
|
12
14
|
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
13
|
-
|
|
15
|
+
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.
|
|
16
|
+
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.
|
|
17
|
+
Return: {"candidates":[{"key":"stable semantic key","targetId":"existing-id or null","title":"...","summary":"one complete sentence","keywords":["..."],"scope":"global|project","body":"markdown body","evidence":["verified evidence"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|revise","explicitUserDirective":false,"durability":"stable"}]}
|
|
14
18
|
Return {"candidates":[]} when nothing meets the threshold.`;
|
|
15
|
-
export function buildKnowledgeExtractionPrompt(projectRoot, delta) {
|
|
16
|
-
|
|
19
|
+
export function buildKnowledgeExtractionPrompt(projectRoot, delta, context = "", existing = []) {
|
|
20
|
+
const related = existing.length > 0 ? JSON.stringify(existing) : "[]";
|
|
21
|
+
return [
|
|
22
|
+
`Project root: ${projectRoot}`,
|
|
23
|
+
"",
|
|
24
|
+
"<relevant_existing_knowledge>", related, "</relevant_existing_knowledge>",
|
|
25
|
+
"",
|
|
26
|
+
"<prior_validation_context>", context, "</prior_validation_context>",
|
|
27
|
+
"",
|
|
28
|
+
"<conversation_delta>", delta, "</conversation_delta>",
|
|
29
|
+
].join("\n");
|
|
17
30
|
}
|
|
18
31
|
export function parseCandidateEnvelope(text) {
|
|
19
32
|
const trimmed = text.trim();
|
|
@@ -22,10 +35,78 @@ export function parseCandidateEnvelope(text) {
|
|
|
22
35
|
const end = unfenced.lastIndexOf("}");
|
|
23
36
|
if (start < 0 || end <= start)
|
|
24
37
|
throw new Error("Knowledge reviewer did not return a JSON object");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
let strictError;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(unfenced.slice(start, end + 1));
|
|
41
|
+
if (!Array.isArray(parsed.candidates))
|
|
42
|
+
throw new Error("Knowledge reviewer response is missing candidates[]");
|
|
43
|
+
return parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
strictError = error;
|
|
47
|
+
}
|
|
48
|
+
const arrayKey = /["']candidates["']\s*:/gu.exec(unfenced);
|
|
49
|
+
const arrayStart = arrayKey ? unfenced.indexOf("[", arrayKey.index + arrayKey[0].length) : -1;
|
|
50
|
+
if (arrayStart >= 0) {
|
|
51
|
+
const candidates = [];
|
|
52
|
+
let inString = false;
|
|
53
|
+
let escaped = false;
|
|
54
|
+
for (let index = arrayStart + 1; index < unfenced.length && candidates.length < KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates; index++) {
|
|
55
|
+
const character = unfenced[index];
|
|
56
|
+
if (inString) {
|
|
57
|
+
if (escaped)
|
|
58
|
+
escaped = false;
|
|
59
|
+
else if (character === "\\")
|
|
60
|
+
escaped = true;
|
|
61
|
+
else if (character === '"')
|
|
62
|
+
inString = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (character === '"') {
|
|
66
|
+
inString = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (character !== "{")
|
|
70
|
+
continue;
|
|
71
|
+
const objectEnd = balancedObjectEnd(unfenced, index);
|
|
72
|
+
if (objectEnd < 0)
|
|
73
|
+
break;
|
|
74
|
+
try {
|
|
75
|
+
candidates.push(JSON.parse(unfenced.slice(index, objectEnd + 1)));
|
|
76
|
+
}
|
|
77
|
+
catch { /* Salvage other complete candidates. */ }
|
|
78
|
+
index = objectEnd;
|
|
79
|
+
}
|
|
80
|
+
if (candidates.length > 0)
|
|
81
|
+
return candidates;
|
|
82
|
+
}
|
|
83
|
+
throw strictError;
|
|
84
|
+
}
|
|
85
|
+
function balancedObjectEnd(value, start) {
|
|
86
|
+
let depth = 0;
|
|
87
|
+
let inString = false;
|
|
88
|
+
let escaped = false;
|
|
89
|
+
for (let index = start; index < value.length; index++) {
|
|
90
|
+
const character = value[index];
|
|
91
|
+
if (inString) {
|
|
92
|
+
if (escaped)
|
|
93
|
+
escaped = false;
|
|
94
|
+
else if (character === "\\")
|
|
95
|
+
escaped = true;
|
|
96
|
+
else if (character === '"')
|
|
97
|
+
inString = false;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (character === '"') {
|
|
101
|
+
inString = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (character === "{")
|
|
105
|
+
depth++;
|
|
106
|
+
else if (character === "}" && --depth === 0)
|
|
107
|
+
return index;
|
|
108
|
+
}
|
|
109
|
+
return -1;
|
|
29
110
|
}
|
|
30
111
|
export function knowledgeDeltaDigest(delta) {
|
|
31
112
|
return createHash("sha256").update(delta).digest("hex");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { userRuntimePaths } from "../runtime/paths.js";
|
|
4
|
+
export function writeKnowledgeReviewStatus(status, home) {
|
|
5
|
+
const path = userRuntimePaths(home).knowledgeStatus;
|
|
6
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
7
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
8
|
+
writeFileSync(temporary, `${JSON.stringify(status, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
9
|
+
renameSync(temporary, path);
|
|
10
|
+
chmodSync(path, 0o600);
|
|
11
|
+
}
|
|
12
|
+
export function readKnowledgeReviewStatus(home) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(readFileSync(userRuntimePaths(home).knowledgeStatus, "utf8"));
|
|
15
|
+
return parsed.version === 1 && parsed.protocolVersion === 3 ? parsed : undefined;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -6,6 +6,8 @@ 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 { parseCandidateEnvelope } from "./extractor.js";
|
|
9
|
+
import { writeKnowledgeReviewStatus } from "./review-status.js";
|
|
10
|
+
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
9
11
|
import { findNextReviewTask } from "./session-scanner.js";
|
|
10
12
|
import { commitKnowledgeReview, ensureKnowledgeDirectories, loadCurrentManifest } from "./store.js";
|
|
11
13
|
if (!parentPort)
|
|
@@ -21,6 +23,11 @@ let leaderToken = "";
|
|
|
21
23
|
let standbySocket;
|
|
22
24
|
let electionPending = false;
|
|
23
25
|
let activeTask;
|
|
26
|
+
let activeAttempt = 0;
|
|
27
|
+
let activeStartedAt = 0;
|
|
28
|
+
let activeDeadlineTimer;
|
|
29
|
+
let reviewStatus;
|
|
30
|
+
const reviewAttempts = new Map();
|
|
24
31
|
function send(message) {
|
|
25
32
|
parentPort.postMessage(message);
|
|
26
33
|
}
|
|
@@ -29,12 +36,74 @@ function coordinatorPath() {
|
|
|
29
36
|
? `\\\\.\\pipe\\hwcode-knowledge-v3-${Buffer.from(home).toString("hex").slice(0, 16)}`
|
|
30
37
|
: paths.knowledgeCoordinatorSocket;
|
|
31
38
|
}
|
|
32
|
-
function writeLeaderMetadata() {
|
|
39
|
+
function writeLeaderMetadata(electedAt) {
|
|
33
40
|
writeFileSync(paths.knowledgeLeader, `${JSON.stringify({
|
|
34
|
-
protocolVersion: 3, leaderToken, pid: process.pid, electedAt
|
|
41
|
+
protocolVersion: 3, leaderToken, pid: process.pid, electedAt,
|
|
35
42
|
}, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
36
43
|
chmodSync(paths.knowledgeLeader, 0o600);
|
|
37
44
|
}
|
|
45
|
+
function publishReviewStatus() {
|
|
46
|
+
if (!reviewStatus)
|
|
47
|
+
return;
|
|
48
|
+
reviewStatus.updatedAt = new Date().toISOString();
|
|
49
|
+
try {
|
|
50
|
+
writeKnowledgeReviewStatus(reviewStatus, home);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
send({ type: "review_failed", error: `Unable to write knowledge status: ${error instanceof Error ? error.message : String(error)}` });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function boundedError(error) {
|
|
57
|
+
return sanitizeKnowledgeText(error instanceof Error ? error.message : String(error)).slice(0, 1_000);
|
|
58
|
+
}
|
|
59
|
+
function beginReview(task) {
|
|
60
|
+
activeAttempt = (reviewAttempts.get(task.reviewKey) ?? 0) + 1;
|
|
61
|
+
reviewAttempts.set(task.reviewKey, activeAttempt);
|
|
62
|
+
activeStartedAt = Date.now();
|
|
63
|
+
clearTimeout(activeDeadlineTimer);
|
|
64
|
+
activeDeadlineTimer = setTimeout(() => {
|
|
65
|
+
if (activeTask?.requestId !== task.requestId)
|
|
66
|
+
return;
|
|
67
|
+
const error = "knowledge-review-result-timeout";
|
|
68
|
+
finishReview(task, "failed", { error });
|
|
69
|
+
activeTask = undefined;
|
|
70
|
+
send({ type: "review_cancel", requestId: task.requestId, reason: error });
|
|
71
|
+
send({ type: "review_failed", requestId: task.requestId, error });
|
|
72
|
+
}, KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs + 5_000);
|
|
73
|
+
activeDeadlineTimer.unref();
|
|
74
|
+
if (reviewStatus) {
|
|
75
|
+
reviewStatus.activeReview = {
|
|
76
|
+
reviewKey: task.reviewKey,
|
|
77
|
+
sessionKey: task.sessionKey,
|
|
78
|
+
sessionFileHash: task.sessionFileHash,
|
|
79
|
+
projectKey: task.projectKey,
|
|
80
|
+
attempt: activeAttempt,
|
|
81
|
+
startedAt: new Date(activeStartedAt).toISOString(),
|
|
82
|
+
};
|
|
83
|
+
publishReviewStatus();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function finishReview(task, outcome, detail) {
|
|
87
|
+
clearTimeout(activeDeadlineTimer);
|
|
88
|
+
activeDeadlineTimer = undefined;
|
|
89
|
+
if (!reviewStatus)
|
|
90
|
+
return;
|
|
91
|
+
reviewStatus.activeReview = undefined;
|
|
92
|
+
reviewStatus.lastReview = {
|
|
93
|
+
reviewKey: task.reviewKey,
|
|
94
|
+
sessionKey: task.sessionKey,
|
|
95
|
+
attempt: activeAttempt,
|
|
96
|
+
outcome,
|
|
97
|
+
completedAt: new Date().toISOString(),
|
|
98
|
+
durationMs: Math.max(0, Date.now() - activeStartedAt),
|
|
99
|
+
generationId: detail.generationId,
|
|
100
|
+
result: detail.result,
|
|
101
|
+
error: detail.error === undefined ? undefined : boundedError(detail.error),
|
|
102
|
+
};
|
|
103
|
+
if (outcome === "saved")
|
|
104
|
+
reviewAttempts.delete(task.reviewKey);
|
|
105
|
+
publishReviewStatus();
|
|
106
|
+
}
|
|
38
107
|
function removeLeaderMetadata(token) {
|
|
39
108
|
try {
|
|
40
109
|
const current = JSON.parse(readFileSync(paths.knowledgeLeader, "utf8"));
|
|
@@ -63,7 +132,17 @@ function scheduleElection(delay = Math.floor(Math.random() * 500)) {
|
|
|
63
132
|
function stepDown(reason) {
|
|
64
133
|
if (activeTask)
|
|
65
134
|
send({ type: "review_cancel", requestId: activeTask.requestId, reason });
|
|
135
|
+
if (reviewStatus) {
|
|
136
|
+
reviewStatus.state = "stepped-down";
|
|
137
|
+
reviewStatus.stepDownReason = reason;
|
|
138
|
+
reviewStatus.activeReview = undefined;
|
|
139
|
+
publishReviewStatus();
|
|
140
|
+
}
|
|
66
141
|
activeTask = undefined;
|
|
142
|
+
clearTimeout(activeDeadlineTimer);
|
|
143
|
+
activeDeadlineTimer = undefined;
|
|
144
|
+
activeAttempt = 0;
|
|
145
|
+
activeStartedAt = 0;
|
|
67
146
|
const token = leaderToken;
|
|
68
147
|
leaderToken = "";
|
|
69
148
|
for (const socket of standbySockets)
|
|
@@ -85,6 +164,7 @@ function stepDown(reason) {
|
|
|
85
164
|
}
|
|
86
165
|
if (token)
|
|
87
166
|
removeLeaderMetadata(token);
|
|
167
|
+
reviewStatus = undefined;
|
|
88
168
|
}
|
|
89
169
|
function becomeStandby(socket) {
|
|
90
170
|
standbySocket = socket;
|
|
@@ -162,6 +242,7 @@ function connectToLeader(probe = 0) {
|
|
|
162
242
|
function becomeLeader(server) {
|
|
163
243
|
leaderServer = server;
|
|
164
244
|
leaderToken = randomUUID();
|
|
245
|
+
const electedAt = new Date().toISOString();
|
|
165
246
|
server.unref();
|
|
166
247
|
server.on("connection", (socket) => {
|
|
167
248
|
standbySockets.add(socket);
|
|
@@ -176,7 +257,11 @@ function becomeLeader(server) {
|
|
|
176
257
|
stepDown("coordinator-server-failed");
|
|
177
258
|
scheduleElection();
|
|
178
259
|
});
|
|
179
|
-
writeLeaderMetadata();
|
|
260
|
+
writeLeaderMetadata(electedAt);
|
|
261
|
+
reviewStatus = {
|
|
262
|
+
version: 1, protocolVersion: 3, state: "leader", pid: process.pid, leaderToken, electedAt, updatedAt: electedAt,
|
|
263
|
+
};
|
|
264
|
+
publishReviewStatus();
|
|
180
265
|
send({ type: "leadership", state: "leader", leaderToken });
|
|
181
266
|
void scanForReview();
|
|
182
267
|
}
|
|
@@ -204,15 +289,22 @@ function attemptElection() {
|
|
|
204
289
|
async function scanForReview() {
|
|
205
290
|
if (!leaderServer || !leaderToken || activeTask || !sessionsRoot || !eligible)
|
|
206
291
|
return;
|
|
292
|
+
if (reviewStatus) {
|
|
293
|
+
reviewStatus.lastScanAt = new Date().toISOString();
|
|
294
|
+
reviewStatus.lastScanError = undefined;
|
|
295
|
+
publishReviewStatus();
|
|
296
|
+
}
|
|
207
297
|
try {
|
|
208
298
|
const manifest = loadCurrentManifest(home);
|
|
209
299
|
const task = findNextReviewTask(sessionsRoot, manifest.reviews);
|
|
210
300
|
if (!task)
|
|
211
301
|
return;
|
|
212
302
|
activeTask = task;
|
|
303
|
+
beginReview(task);
|
|
213
304
|
if (!task.delta) {
|
|
214
305
|
const result = commitKnowledgeReview([], task, leaderToken, home);
|
|
215
306
|
const generationId = loadCurrentManifest(home).generationId;
|
|
307
|
+
finishReview(task, "saved", { generationId, result });
|
|
216
308
|
send({ type: "review_saved", requestId: task.requestId, generationId, result });
|
|
217
309
|
if (generationId) {
|
|
218
310
|
send({ type: "generation_changed", generationId });
|
|
@@ -225,8 +317,14 @@ async function scanForReview() {
|
|
|
225
317
|
send({ type: "review_request", leaderToken, requestId: task.requestId, task });
|
|
226
318
|
}
|
|
227
319
|
catch (error) {
|
|
320
|
+
if (activeTask)
|
|
321
|
+
finishReview(activeTask, "failed", { error });
|
|
322
|
+
else if (reviewStatus) {
|
|
323
|
+
reviewStatus.lastScanError = { at: new Date().toISOString(), error: boundedError(error) };
|
|
324
|
+
publishReviewStatus();
|
|
325
|
+
}
|
|
228
326
|
activeTask = undefined;
|
|
229
|
-
send({ type: "review_failed", error:
|
|
327
|
+
send({ type: "review_failed", error: boundedError(error) });
|
|
230
328
|
}
|
|
231
329
|
}
|
|
232
330
|
function handleReviewResult(message) {
|
|
@@ -239,6 +337,7 @@ function handleReviewResult(message) {
|
|
|
239
337
|
throw new Error(message.error || "Knowledge review returned no content");
|
|
240
338
|
const result = commitKnowledgeReview(parseCandidateEnvelope(message.raw), task, leaderToken, home);
|
|
241
339
|
const generationId = loadCurrentManifest(home).generationId;
|
|
340
|
+
finishReview(task, "saved", { generationId, result });
|
|
242
341
|
send({ type: "review_saved", requestId: task.requestId, generationId, result });
|
|
243
342
|
if (generationId) {
|
|
244
343
|
send({ type: "generation_changed", generationId });
|
|
@@ -247,7 +346,8 @@ function handleReviewResult(message) {
|
|
|
247
346
|
committed = true;
|
|
248
347
|
}
|
|
249
348
|
catch (error) {
|
|
250
|
-
|
|
349
|
+
finishReview(task, "failed", { error });
|
|
350
|
+
send({ type: "review_failed", requestId: task.requestId, error: boundedError(error) });
|
|
251
351
|
}
|
|
252
352
|
finally {
|
|
253
353
|
activeTask = undefined;
|
|
@@ -22,3 +22,23 @@ export function compactKnowledgeText(value, maxChars) {
|
|
|
22
22
|
.trim()
|
|
23
23
|
.slice(0, maxChars);
|
|
24
24
|
}
|
|
25
|
+
export function compactKnowledgeSummary(value, maxChars) {
|
|
26
|
+
const normalized = sanitizeKnowledgeText(value).replace(/\s+/gu, " ").trim();
|
|
27
|
+
if (normalized.length <= maxChars || maxChars <= 0)
|
|
28
|
+
return normalized.slice(0, Math.max(0, maxChars));
|
|
29
|
+
if (maxChars === 1)
|
|
30
|
+
return "…";
|
|
31
|
+
const limit = maxChars - 1;
|
|
32
|
+
const prefix = normalized.slice(0, limit);
|
|
33
|
+
const minimumBoundary = Math.floor(limit * 0.6);
|
|
34
|
+
const sentenceEnd = Math.max(prefix.lastIndexOf("。"), prefix.lastIndexOf("!"), prefix.lastIndexOf("?"), prefix.lastIndexOf("."), prefix.lastIndexOf("!"), prefix.lastIndexOf("?"));
|
|
35
|
+
if (sentenceEnd >= minimumBoundary)
|
|
36
|
+
return prefix.slice(0, sentenceEnd + 1).trimEnd();
|
|
37
|
+
const next = normalized.charAt(limit);
|
|
38
|
+
if (/[A-Za-z0-9]/u.test(prefix.at(-1) ?? "") && /[A-Za-z0-9]/u.test(next)) {
|
|
39
|
+
const wordBoundary = Math.max(prefix.lastIndexOf(" "), prefix.lastIndexOf("-"), prefix.lastIndexOf("/"));
|
|
40
|
+
if (wordBoundary >= minimumBoundary)
|
|
41
|
+
return `${prefix.slice(0, wordBoundary).trimEnd()}…`;
|
|
42
|
+
}
|
|
43
|
+
return `${prefix.trimEnd()}…`;
|
|
44
|
+
}
|
|
@@ -6,6 +6,8 @@ import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
|
6
6
|
import { knowledgeDeltaDigest } from "./extractor.js";
|
|
7
7
|
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
8
8
|
import { projectKnowledgeKey } from "./store.js";
|
|
9
|
+
const FAILURE_EVIDENCE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|mistyp(?:e|ed)|typo)\b|not supported|失败|错误|不支持|无效)/iu;
|
|
10
|
+
const INJECTED_SKILL_RE = /<skill\b[^>]*>[\s\S]*?<\/skill>/giu;
|
|
9
11
|
function hash(value, length = 64) {
|
|
10
12
|
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
11
13
|
}
|
|
@@ -23,11 +25,11 @@ function textContent(content) {
|
|
|
23
25
|
}
|
|
24
26
|
function reviewPiece(entry, index) {
|
|
25
27
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
26
|
-
return { index, priority:
|
|
28
|
+
return { index, priority: 2, category: "summary", text: `[summary]\n${sanitizeKnowledgeText(entry.summary).slice(0, 6_000)}` };
|
|
27
29
|
}
|
|
28
30
|
if (entry.type === "custom" && entry.customType.startsWith("hwcode-workflow")) {
|
|
29
31
|
return {
|
|
30
|
-
index, priority:
|
|
32
|
+
index, priority: 1, category: "workflow",
|
|
31
33
|
text: `[workflow_state:${entry.customType}]\n${sanitizeKnowledgeText(JSON.stringify(entry.data ?? {})).slice(0, 6_000)}`,
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -35,10 +37,11 @@ function reviewPiece(entry, index) {
|
|
|
35
37
|
return undefined;
|
|
36
38
|
const message = entry.message;
|
|
37
39
|
const role = typeof message.role === "string" ? message.role : "message";
|
|
38
|
-
const content = sanitizeKnowledgeText(textContent(message.content));
|
|
40
|
+
const content = sanitizeKnowledgeText(textContent(message.content).replace(INJECTED_SKILL_RE, "[loaded skill omitted]"));
|
|
39
41
|
if (!content)
|
|
40
42
|
return undefined;
|
|
41
|
-
const priority = role === "user" ? 0 : role === "toolResult"
|
|
43
|
+
const priority = role === "user" ? 0 : role === "toolResult" && FAILURE_EVIDENCE_RE.test(content) ? 2
|
|
44
|
+
: role === "toolResult" ? 4 : 3;
|
|
42
45
|
const cap = role === "user" ? 8_000 : role === "toolResult" ? 1_500 : 4_000;
|
|
43
46
|
const category = role === "user" ? "user" : role === "toolResult" ? "tool" : "assistant";
|
|
44
47
|
return { index, priority, category, text: `[${role}]\n${content.slice(0, cap)}` };
|
|
@@ -50,16 +53,49 @@ function buildDelta(entries) {
|
|
|
50
53
|
user: 14_000, workflow: 6_000, summary: 4_000, assistant: 8_000, tool: 4_000,
|
|
51
54
|
};
|
|
52
55
|
let remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars;
|
|
53
|
-
for (const piece of pieces.slice().sort((left, right) =>
|
|
56
|
+
for (const piece of pieces.slice().sort((left, right) => {
|
|
57
|
+
if (left.priority !== right.priority)
|
|
58
|
+
return left.priority - right.priority;
|
|
59
|
+
if (left.category === right.category && left.category !== "user")
|
|
60
|
+
return right.index - left.index;
|
|
61
|
+
return left.index - right.index;
|
|
62
|
+
})) {
|
|
54
63
|
if (remaining <= 0)
|
|
55
64
|
break;
|
|
56
|
-
const
|
|
65
|
+
const separatorCharacters = selected.length > 0 ? 2 : 0;
|
|
66
|
+
if (remaining <= separatorCharacters)
|
|
67
|
+
break;
|
|
68
|
+
const text = piece.text.slice(0, Math.min(remaining - separatorCharacters, categoryRemaining[piece.category]));
|
|
57
69
|
if (text)
|
|
58
70
|
selected.push({ ...piece, text });
|
|
59
|
-
remaining -= text.length;
|
|
71
|
+
remaining -= text.length + separatorCharacters;
|
|
60
72
|
categoryRemaining[piece.category] -= text.length;
|
|
61
73
|
}
|
|
62
|
-
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
74
|
+
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
75
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars);
|
|
76
|
+
}
|
|
77
|
+
function buildValidationContext(entries) {
|
|
78
|
+
const pieces = entries.map(reviewPiece).filter((piece) => Boolean(piece));
|
|
79
|
+
const latestWorkflow = pieces.filter((piece) => piece.category === "workflow").at(-1);
|
|
80
|
+
const evidence = pieces.filter((piece) => ((piece.category === "tool" || piece.category === "assistant" || piece.category === "summary")
|
|
81
|
+
&& FAILURE_EVIDENCE_RE.test(piece.text)));
|
|
82
|
+
if (latestWorkflow && !evidence.includes(latestWorkflow))
|
|
83
|
+
evidence.push(latestWorkflow);
|
|
84
|
+
let remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxPriorContextChars;
|
|
85
|
+
const selected = [];
|
|
86
|
+
for (const piece of evidence.slice().sort((left, right) => right.index - left.index)) {
|
|
87
|
+
if (remaining <= 0)
|
|
88
|
+
break;
|
|
89
|
+
const separatorCharacters = selected.length > 0 ? 2 : 0;
|
|
90
|
+
if (remaining <= separatorCharacters)
|
|
91
|
+
break;
|
|
92
|
+
const text = piece.text.slice(0, remaining - separatorCharacters);
|
|
93
|
+
if (text)
|
|
94
|
+
selected.push({ ...piece, text });
|
|
95
|
+
remaining -= text.length + separatorCharacters;
|
|
96
|
+
}
|
|
97
|
+
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
98
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxPriorContextChars);
|
|
63
99
|
}
|
|
64
100
|
function projectRootForSession(header, branch) {
|
|
65
101
|
for (const entry of branch.slice().reverse()) {
|
|
@@ -128,6 +164,7 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
128
164
|
if (pending.length === 0)
|
|
129
165
|
return undefined;
|
|
130
166
|
const delta = buildDelta(pending);
|
|
167
|
+
const context = cursorIndex >= 0 ? buildValidationContext(branch.slice(0, cursorIndex + 1)) : "";
|
|
131
168
|
const firstEntryId = pending[0].id;
|
|
132
169
|
const lastEntryId = pending[pending.length - 1].id;
|
|
133
170
|
const deltaDigest = knowledgeDeltaDigest(delta || `${firstEntryId}\0${lastEntryId}`);
|
|
@@ -138,7 +175,7 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
138
175
|
return {
|
|
139
176
|
requestId: randomUUID(), reviewKey, sessionId: header.id, sessionKey, sessionFileHash,
|
|
140
177
|
projectRoot, projectKey: projectKnowledgeKey(projectRoot),
|
|
141
|
-
firstEntryId, lastEntryId, delta, deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
178
|
+
firstEntryId, lastEntryId, context, delta, deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
142
179
|
};
|
|
143
180
|
}
|
|
144
181
|
export function findNextReviewTask(sessionsRoot, reviews, now = Date.now()) {
|
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
6
6
|
import { userRuntimePaths } from "../runtime/paths.js";
|
|
7
|
-
import { compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.js";
|
|
7
|
+
import { compactKnowledgeSummary, compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.js";
|
|
8
8
|
const STORAGE = KNOWLEDGE_RUNTIME_DEFAULTS.storage;
|
|
9
9
|
const SAFE_ID_RE = /^[a-z0-9][a-z0-9-]{0,79}$/u;
|
|
10
10
|
const SAFE_GENERATION_RE = /^[0-9TZ-]+-[a-f0-9]{12}$/u;
|
|
@@ -101,6 +101,57 @@ function normalizeSlug(value) {
|
|
|
101
101
|
function normalizedFingerprint(candidate) {
|
|
102
102
|
return createHash("sha256").update(`${candidate.scope}\0${candidate.key.trim().toLowerCase()}`).digest("hex");
|
|
103
103
|
}
|
|
104
|
+
function comparisonTerms(value) {
|
|
105
|
+
return new Set((value.normalize("NFKC").toLowerCase().match(/\p{Script=Han}+|[\p{L}\p{N}]+/gu) ?? [])
|
|
106
|
+
.filter((term) => term.length >= 2));
|
|
107
|
+
}
|
|
108
|
+
function containment(left, right) {
|
|
109
|
+
const minimum = Math.min(left.size, right.size);
|
|
110
|
+
if (minimum === 0)
|
|
111
|
+
return 0;
|
|
112
|
+
let shared = 0;
|
|
113
|
+
for (const term of left)
|
|
114
|
+
if (right.has(term))
|
|
115
|
+
shared++;
|
|
116
|
+
return shared / minimum;
|
|
117
|
+
}
|
|
118
|
+
function semanticDuplicateScore(candidate, entry) {
|
|
119
|
+
if (candidate.scope !== entry.scope || candidate.storageHint !== entry.track)
|
|
120
|
+
return 0;
|
|
121
|
+
const titleScore = containment(comparisonTerms(candidate.title), comparisonTerms(entry.title));
|
|
122
|
+
const keywordScore = containment(comparisonTerms(candidate.keywords.join(" ")), comparisonTerms(entry.keywords.join(" ")));
|
|
123
|
+
if (titleScore < 0.8 || keywordScore < 0.5)
|
|
124
|
+
return 0;
|
|
125
|
+
return titleScore * 0.7 + keywordScore * 0.3;
|
|
126
|
+
}
|
|
127
|
+
function semanticDuplicateIndex(candidate, catalog) {
|
|
128
|
+
let bestIndex = -1;
|
|
129
|
+
let bestScore = 0;
|
|
130
|
+
for (const [index, entry] of catalog.items.entries()) {
|
|
131
|
+
const score = semanticDuplicateScore(candidate, entry);
|
|
132
|
+
if (score > bestScore) {
|
|
133
|
+
bestIndex = index;
|
|
134
|
+
bestScore = score;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return bestIndex;
|
|
138
|
+
}
|
|
139
|
+
function unsupportedOperations(value) {
|
|
140
|
+
return [...value.matchAll(/\bOperation\s+([A-Za-z][A-Za-z0-9]+)\s+is not supported\b/gu)]
|
|
141
|
+
.map((match) => match[1].toLowerCase());
|
|
142
|
+
}
|
|
143
|
+
function contradictsVerifiedFailures(body, validationText) {
|
|
144
|
+
const normalizedBody = body.toLowerCase();
|
|
145
|
+
for (const operation of unsupportedOperations(validationText)) {
|
|
146
|
+
const index = normalizedBody.search(new RegExp(`\\b${operation}\\b`, "u"));
|
|
147
|
+
if (index < 0)
|
|
148
|
+
continue;
|
|
149
|
+
const context = normalizedBody.slice(Math.max(0, index - 80), index + operation.length + 80);
|
|
150
|
+
if (!/(?:not supported|unsupported|do not|don't|avoid|不支持|不要|避免)/u.test(context))
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
104
155
|
function contentHash(candidate) {
|
|
105
156
|
return createHash("sha256").update(`${candidate.title}\0${candidate.summary}\0${candidate.body}`).digest("hex");
|
|
106
157
|
}
|
|
@@ -110,13 +161,15 @@ function asStringArray(value, maxItems) {
|
|
|
110
161
|
return [...new Set(value.filter((item) => typeof item === "string")
|
|
111
162
|
.map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
|
|
112
163
|
}
|
|
113
|
-
function normalizeCandidate(value, projectKey) {
|
|
164
|
+
function normalizeCandidate(value, projectKey, validationText) {
|
|
114
165
|
if (!value || typeof value !== "object")
|
|
115
166
|
return undefined;
|
|
116
167
|
const raw = value;
|
|
117
168
|
if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
|
|
118
169
|
|| typeof raw.body !== "string" || typeof raw.confidence !== "number")
|
|
119
170
|
return undefined;
|
|
171
|
+
if (raw.durability !== "stable")
|
|
172
|
+
return undefined;
|
|
120
173
|
if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence)
|
|
121
174
|
return undefined;
|
|
122
175
|
const explicitUserDirective = raw.explicitUserDirective === true;
|
|
@@ -125,17 +178,22 @@ function normalizeCandidate(value, projectKey) {
|
|
|
125
178
|
const evidence = asStringArray(raw.evidence, 8);
|
|
126
179
|
if (!body || evidence.length === 0)
|
|
127
180
|
return undefined;
|
|
181
|
+
if (contradictsVerifiedFailures(body, validationText))
|
|
182
|
+
return undefined;
|
|
128
183
|
const requestedTrack = raw.storageHint === "rule" ? "rule" : "topic";
|
|
129
184
|
const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
|
|
185
|
+
&& body.split(/\n\s*\n/gu).length === 1
|
|
130
186
|
&& (explicitUserDirective || raw.confidence >= 0.9);
|
|
187
|
+
const targetId = typeof raw.targetId === "string" && SAFE_ID_RE.test(raw.targetId) ? raw.targetId : undefined;
|
|
131
188
|
return {
|
|
132
|
-
key: compactKnowledgeText(raw.key, 160),
|
|
133
|
-
|
|
189
|
+
key: compactKnowledgeText(raw.key, 160), targetId,
|
|
190
|
+
title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
191
|
+
summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
|
|
134
192
|
keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
|
|
135
193
|
scope, body, evidence, confidence: Math.min(1, raw.confidence),
|
|
136
194
|
storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
|
|
137
195
|
action: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
|
|
138
|
-
explicitUserDirective,
|
|
196
|
+
explicitUserDirective, durability: "stable",
|
|
139
197
|
};
|
|
140
198
|
}
|
|
141
199
|
function renderKnowledgeFile(candidate) {
|
|
@@ -267,16 +325,30 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
267
325
|
const catalog = structuredClone(current.catalog);
|
|
268
326
|
const result = { saved: 0, updated: 0, pending: 0, skipped: 0 };
|
|
269
327
|
for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
|
|
270
|
-
const candidate = normalizeCandidate(value, task.projectKey);
|
|
328
|
+
const candidate = normalizeCandidate(value, task.projectKey, `${task.context ?? ""}\n${task.delta}`);
|
|
271
329
|
if (!candidate) {
|
|
272
330
|
result.skipped++;
|
|
273
331
|
continue;
|
|
274
332
|
}
|
|
275
333
|
const fingerprint = normalizedFingerprint(candidate);
|
|
276
334
|
const hash = contentHash(candidate);
|
|
277
|
-
const
|
|
335
|
+
const targetIndex = candidate.targetId
|
|
336
|
+
? catalog.items.findIndex((item) => item.id === candidate.targetId && applicable(item, task.projectKey))
|
|
337
|
+
: -1;
|
|
338
|
+
if (candidate.targetId && targetIndex < 0) {
|
|
339
|
+
result.skipped++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
let existingIndex = targetIndex >= 0
|
|
343
|
+
? targetIndex
|
|
344
|
+
: catalog.items.findIndex((item) => item.fingerprint === fingerprint);
|
|
345
|
+
let semanticReinforcement = false;
|
|
346
|
+
if (existingIndex < 0) {
|
|
347
|
+
existingIndex = semanticDuplicateIndex(candidate, catalog);
|
|
348
|
+
semanticReinforcement = existingIndex >= 0;
|
|
349
|
+
}
|
|
278
350
|
const existing = existingIndex >= 0 ? catalog.items[existingIndex] : undefined;
|
|
279
|
-
if (existing
|
|
351
|
+
if (existing && (existing.contentHash === hash || candidate.action === "reinforce" || semanticReinforcement)) {
|
|
280
352
|
existing.evidenceCount += candidate.evidence.length;
|
|
281
353
|
existing.updatedAt = new Date().toISOString();
|
|
282
354
|
result.updated++;
|
|
@@ -299,8 +371,9 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
299
371
|
const relativeFile = `${track === "rule" ? "rules" : "topics"}/${id}.md`;
|
|
300
372
|
atomicWrite(join(temporary, relativeFile), renderKnowledgeFile(candidate));
|
|
301
373
|
const entry = {
|
|
302
|
-
id, fingerprint
|
|
303
|
-
|
|
374
|
+
id, fingerprint: existing?.fingerprint ?? fingerprint, contentHash: hash,
|
|
375
|
+
title: candidate.title, summary: candidate.summary,
|
|
376
|
+
keywords: candidate.keywords, scope: existing?.scope ?? candidate.scope, track, file: relativeFile,
|
|
304
377
|
evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
|
|
305
378
|
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
306
379
|
};
|
|
@@ -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
|
-
modelTimeoutMs:
|
|
34
|
-
maxDeltaChars:
|
|
35
|
-
|
|
33
|
+
modelTimeoutMs: 180_000,
|
|
34
|
+
maxDeltaChars: 12_000,
|
|
35
|
+
maxPriorContextChars: 4_000,
|
|
36
|
+
maxExistingContextItems: 5,
|
|
37
|
+
maxExistingBodyItems: 2,
|
|
38
|
+
maxExistingBodyChars: 600,
|
|
39
|
+
maxCandidates: 3,
|
|
36
40
|
minimumConfidence: 0.72,
|
|
37
41
|
}),
|
|
38
42
|
storage: Object.freeze({
|
|
39
|
-
maxRuleChars:
|
|
40
|
-
maxRuleFileLines:
|
|
43
|
+
maxRuleChars: 320,
|
|
44
|
+
maxRuleFileLines: 6,
|
|
41
45
|
maxRulesPromptChars: 12_000,
|
|
42
46
|
maxMemoryChars: 25_000,
|
|
43
47
|
maxMemoryLines: 200,
|
|
@@ -40,6 +40,7 @@ export function userRuntimePaths(home = homedir()) {
|
|
|
40
40
|
// Unix-domain socket paths are short on purpose (macOS caps them at roughly 104 bytes).
|
|
41
41
|
knowledgeCoordinatorSocket: join(root, "knowledge-v3.sock"),
|
|
42
42
|
knowledgeLeader: join(knowledge, "runtime", "leader.json"),
|
|
43
|
+
knowledgeStatus: join(knowledge, "runtime", "status.json"),
|
|
43
44
|
knowledgeCommitLock: join(knowledge, "runtime", "commit.lock"),
|
|
44
45
|
knowledgeElectionLock: join(knowledge, "runtime", "election.lock"),
|
|
45
46
|
};
|