@hadooppei/hwcode 1.0.9 → 1.0.11
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 +102 -0
- package/.pi/dist/lib/knowledge/review-status.js +20 -0
- package/.pi/dist/lib/knowledge/review-worker.js +392 -0
- package/.pi/dist/lib/knowledge/sanitize.js +44 -0
- package/.pi/dist/lib/knowledge/session-scanner.js +165 -0
- package/.pi/dist/lib/knowledge/store.js +372 -0
- package/.pi/dist/lib/knowledge/types.js +1 -0
- package/.pi/dist/lib/knowledge/worker-protocol.js +1 -0
- package/.pi/dist/lib/runtime/defaults.js +50 -0
- package/.pi/dist/lib/runtime/paths.js +56 -0
- package/.pi/extensions/knowledge.ts +65 -19
- package/.pi/lib/knowledge/extractor.ts +55 -4
- package/.pi/lib/knowledge/review-status.ts +55 -0
- package/.pi/lib/knowledge/review-worker.ts +108 -6
- package/.pi/lib/knowledge/sanitize.ts +20 -0
- package/.pi/lib/knowledge/store.ts +4 -3
- package/.pi/lib/knowledge/types.ts +2 -0
- package/.pi/lib/knowledge/worker-protocol.ts +1 -0
- package/.pi/lib/runtime/defaults.ts +3 -3
- package/.pi/lib/runtime/paths.ts +2 -0
- package/package.json +5 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
3
|
+
export const KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT = `You are HWCode's background knowledge reviewer.
|
|
4
|
+
Review only the supplied conversation delta. Return strict JSON and no markdown.
|
|
5
|
+
Persist only knowledge that is likely to be useful in future sessions:
|
|
6
|
+
- explicit user corrections or stable preferences;
|
|
7
|
+
- verified engineering rules, successful procedures, or expensive failed approaches;
|
|
8
|
+
- reusable architecture, testing, debugging, deployment, or operational knowledge.
|
|
9
|
+
Do not persist task summaries, guesses, temporary IDs, credentials, secrets, private data, or facts recoverable by simply reading the repository.
|
|
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
|
+
Use storageHint "rule" only for short, precise, high-value instructions. Use "topic" for multi-step SOPs and detailed experience.
|
|
12
|
+
Use action "revise" only when the delta explicitly corrects prior knowledge; otherwise use "add" or "reinforce".
|
|
13
|
+
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
14
|
+
Each summary must be one complete sentence of at most 160 characters. Keep each body under 3000 characters. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered.
|
|
15
|
+
Return: {"candidates":[{"key":"stable semantic key","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"}]}
|
|
16
|
+
Return {"candidates":[]} when nothing meets the threshold.`;
|
|
17
|
+
export function buildKnowledgeExtractionPrompt(projectRoot, delta) {
|
|
18
|
+
return `Project root: ${projectRoot}\n\n<conversation_delta>\n${delta}\n</conversation_delta>`;
|
|
19
|
+
}
|
|
20
|
+
export function parseCandidateEnvelope(text) {
|
|
21
|
+
const trimmed = text.trim();
|
|
22
|
+
const unfenced = trimmed.replace(/^```(?:json)?\s*/iu, "").replace(/\s*```$/u, "");
|
|
23
|
+
const start = unfenced.indexOf("{");
|
|
24
|
+
const end = unfenced.lastIndexOf("}");
|
|
25
|
+
if (start < 0 || end <= start)
|
|
26
|
+
throw new Error("Knowledge reviewer did not return a JSON object");
|
|
27
|
+
let strictError;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(unfenced.slice(start, end + 1));
|
|
30
|
+
if (!Array.isArray(parsed.candidates))
|
|
31
|
+
throw new Error("Knowledge reviewer response is missing candidates[]");
|
|
32
|
+
return parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
strictError = error;
|
|
36
|
+
}
|
|
37
|
+
const arrayKey = /["']candidates["']\s*:/gu.exec(unfenced);
|
|
38
|
+
const arrayStart = arrayKey ? unfenced.indexOf("[", arrayKey.index + arrayKey[0].length) : -1;
|
|
39
|
+
if (arrayStart >= 0) {
|
|
40
|
+
const candidates = [];
|
|
41
|
+
let inString = false;
|
|
42
|
+
let escaped = false;
|
|
43
|
+
for (let index = arrayStart + 1; index < unfenced.length && candidates.length < KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates; index++) {
|
|
44
|
+
const character = unfenced[index];
|
|
45
|
+
if (inString) {
|
|
46
|
+
if (escaped)
|
|
47
|
+
escaped = false;
|
|
48
|
+
else if (character === "\\")
|
|
49
|
+
escaped = true;
|
|
50
|
+
else if (character === '"')
|
|
51
|
+
inString = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (character === '"') {
|
|
55
|
+
inString = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (character !== "{")
|
|
59
|
+
continue;
|
|
60
|
+
const objectEnd = balancedObjectEnd(unfenced, index);
|
|
61
|
+
if (objectEnd < 0)
|
|
62
|
+
break;
|
|
63
|
+
try {
|
|
64
|
+
candidates.push(JSON.parse(unfenced.slice(index, objectEnd + 1)));
|
|
65
|
+
}
|
|
66
|
+
catch { /* Salvage other complete candidates. */ }
|
|
67
|
+
index = objectEnd;
|
|
68
|
+
}
|
|
69
|
+
if (candidates.length > 0)
|
|
70
|
+
return candidates;
|
|
71
|
+
}
|
|
72
|
+
throw strictError;
|
|
73
|
+
}
|
|
74
|
+
function balancedObjectEnd(value, start) {
|
|
75
|
+
let depth = 0;
|
|
76
|
+
let inString = false;
|
|
77
|
+
let escaped = false;
|
|
78
|
+
for (let index = start; index < value.length; index++) {
|
|
79
|
+
const character = value[index];
|
|
80
|
+
if (inString) {
|
|
81
|
+
if (escaped)
|
|
82
|
+
escaped = false;
|
|
83
|
+
else if (character === "\\")
|
|
84
|
+
escaped = true;
|
|
85
|
+
else if (character === '"')
|
|
86
|
+
inString = false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (character === '"') {
|
|
90
|
+
inString = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (character === "{")
|
|
94
|
+
depth++;
|
|
95
|
+
else if (character === "}" && --depth === 0)
|
|
96
|
+
return index;
|
|
97
|
+
}
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
export function knowledgeDeltaDigest(delta) {
|
|
101
|
+
return createHash("sha256").update(delta).digest("hex");
|
|
102
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createConnection, createServer } from "node:net";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
6
|
+
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
7
|
+
import { userRuntimePaths } from "../runtime/paths.js";
|
|
8
|
+
import { parseCandidateEnvelope } from "./extractor.js";
|
|
9
|
+
import { writeKnowledgeReviewStatus } from "./review-status.js";
|
|
10
|
+
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
11
|
+
import { findNextReviewTask } from "./session-scanner.js";
|
|
12
|
+
import { commitKnowledgeReview, ensureKnowledgeDirectories, loadCurrentManifest } from "./store.js";
|
|
13
|
+
if (!parentPort)
|
|
14
|
+
throw new Error("Knowledge review worker requires a parent port");
|
|
15
|
+
const home = workerData?.home ?? homedir();
|
|
16
|
+
const paths = userRuntimePaths(home);
|
|
17
|
+
const standbySockets = new Set();
|
|
18
|
+
let eligible = false;
|
|
19
|
+
let stopped = false;
|
|
20
|
+
let sessionsRoot = "";
|
|
21
|
+
let leaderServer;
|
|
22
|
+
let leaderToken = "";
|
|
23
|
+
let standbySocket;
|
|
24
|
+
let electionPending = false;
|
|
25
|
+
let activeTask;
|
|
26
|
+
let activeAttempt = 0;
|
|
27
|
+
let activeStartedAt = 0;
|
|
28
|
+
let activeDeadlineTimer;
|
|
29
|
+
let reviewStatus;
|
|
30
|
+
const reviewAttempts = new Map();
|
|
31
|
+
function send(message) {
|
|
32
|
+
parentPort.postMessage(message);
|
|
33
|
+
}
|
|
34
|
+
function coordinatorPath() {
|
|
35
|
+
return process.platform === "win32"
|
|
36
|
+
? `\\\\.\\pipe\\hwcode-knowledge-v3-${Buffer.from(home).toString("hex").slice(0, 16)}`
|
|
37
|
+
: paths.knowledgeCoordinatorSocket;
|
|
38
|
+
}
|
|
39
|
+
function writeLeaderMetadata(electedAt) {
|
|
40
|
+
writeFileSync(paths.knowledgeLeader, `${JSON.stringify({
|
|
41
|
+
protocolVersion: 3, leaderToken, pid: process.pid, electedAt,
|
|
42
|
+
}, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
43
|
+
chmodSync(paths.knowledgeLeader, 0o600);
|
|
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
|
+
}
|
|
107
|
+
function removeLeaderMetadata(token) {
|
|
108
|
+
try {
|
|
109
|
+
const current = JSON.parse(readFileSync(paths.knowledgeLeader, "utf8"));
|
|
110
|
+
if (current.leaderToken === token)
|
|
111
|
+
rmSync(paths.knowledgeLeader, { force: true });
|
|
112
|
+
}
|
|
113
|
+
catch { /* Another contender may already have replaced it. */ }
|
|
114
|
+
}
|
|
115
|
+
function broadcast(message) {
|
|
116
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
117
|
+
for (const socket of standbySockets) {
|
|
118
|
+
if (socket.writable)
|
|
119
|
+
socket.write(line);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function scheduleElection(delay = Math.floor(Math.random() * 500)) {
|
|
123
|
+
if (!eligible || stopped || electionPending || leaderServer || standbySocket)
|
|
124
|
+
return;
|
|
125
|
+
electionPending = true;
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
electionPending = false;
|
|
128
|
+
attemptElection();
|
|
129
|
+
}, delay);
|
|
130
|
+
timer.unref();
|
|
131
|
+
}
|
|
132
|
+
function stepDown(reason) {
|
|
133
|
+
if (activeTask)
|
|
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
|
+
}
|
|
141
|
+
activeTask = undefined;
|
|
142
|
+
clearTimeout(activeDeadlineTimer);
|
|
143
|
+
activeDeadlineTimer = undefined;
|
|
144
|
+
activeAttempt = 0;
|
|
145
|
+
activeStartedAt = 0;
|
|
146
|
+
const token = leaderToken;
|
|
147
|
+
leaderToken = "";
|
|
148
|
+
for (const socket of standbySockets)
|
|
149
|
+
socket.destroy();
|
|
150
|
+
standbySockets.clear();
|
|
151
|
+
const server = leaderServer;
|
|
152
|
+
leaderServer = undefined;
|
|
153
|
+
if (server) {
|
|
154
|
+
try {
|
|
155
|
+
server.close();
|
|
156
|
+
}
|
|
157
|
+
catch { /* The failing server may already be closed. */ }
|
|
158
|
+
if (process.platform !== "win32") {
|
|
159
|
+
try {
|
|
160
|
+
unlinkSync(coordinatorPath());
|
|
161
|
+
}
|
|
162
|
+
catch { /* Socket may already be gone. */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (token)
|
|
166
|
+
removeLeaderMetadata(token);
|
|
167
|
+
reviewStatus = undefined;
|
|
168
|
+
}
|
|
169
|
+
function becomeStandby(socket) {
|
|
170
|
+
standbySocket = socket;
|
|
171
|
+
socket.unref();
|
|
172
|
+
send({ type: "leadership", state: "standby" });
|
|
173
|
+
let buffer = "";
|
|
174
|
+
socket.on("data", (chunk) => {
|
|
175
|
+
buffer += chunk.toString("utf8");
|
|
176
|
+
for (;;) {
|
|
177
|
+
const newline = buffer.indexOf("\n");
|
|
178
|
+
if (newline < 0)
|
|
179
|
+
break;
|
|
180
|
+
const line = buffer.slice(0, newline);
|
|
181
|
+
buffer = buffer.slice(newline + 1);
|
|
182
|
+
try {
|
|
183
|
+
const message = JSON.parse(line);
|
|
184
|
+
if (message.type === "generation_changed")
|
|
185
|
+
send(message);
|
|
186
|
+
}
|
|
187
|
+
catch { /* Ignore malformed coordinator notifications. */ }
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
socket.once("close", () => {
|
|
191
|
+
if (standbySocket === socket)
|
|
192
|
+
standbySocket = undefined;
|
|
193
|
+
scheduleElection();
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function cleanStaleSocketAndRetry() {
|
|
197
|
+
if (process.platform === "win32") {
|
|
198
|
+
scheduleElection(250);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
mkdirSync(paths.knowledgeElectionLock, { mode: 0o700 });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
scheduleElection(250);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
if (existsSync(coordinatorPath()))
|
|
210
|
+
unlinkSync(coordinatorPath());
|
|
211
|
+
}
|
|
212
|
+
catch { /* Retrying listen will surface persistent failures. */ }
|
|
213
|
+
finally {
|
|
214
|
+
rmSync(paths.knowledgeElectionLock, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
scheduleElection();
|
|
217
|
+
}
|
|
218
|
+
function connectToLeader(probe = 0) {
|
|
219
|
+
if (!eligible || stopped || leaderServer || standbySocket)
|
|
220
|
+
return;
|
|
221
|
+
const socket = createConnection(coordinatorPath());
|
|
222
|
+
let connected = false;
|
|
223
|
+
socket.once("connect", () => { connected = true; becomeStandby(socket); });
|
|
224
|
+
socket.once("error", (error) => {
|
|
225
|
+
socket.destroy();
|
|
226
|
+
if (connected)
|
|
227
|
+
return;
|
|
228
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOENT") {
|
|
229
|
+
if (probe < 2) {
|
|
230
|
+
const timer = setTimeout(() => connectToLeader(probe + 1), 100);
|
|
231
|
+
timer.unref();
|
|
232
|
+
}
|
|
233
|
+
else
|
|
234
|
+
cleanStaleSocketAndRetry();
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
send({ type: "review_failed", error: `Coordinator connection failed: ${error.message}` });
|
|
238
|
+
scheduleElection(1_000);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function becomeLeader(server) {
|
|
243
|
+
leaderServer = server;
|
|
244
|
+
leaderToken = randomUUID();
|
|
245
|
+
const electedAt = new Date().toISOString();
|
|
246
|
+
server.unref();
|
|
247
|
+
server.on("connection", (socket) => {
|
|
248
|
+
standbySockets.add(socket);
|
|
249
|
+
socket.unref();
|
|
250
|
+
socket.once("close", () => standbySockets.delete(socket));
|
|
251
|
+
const generationId = loadCurrentManifest(home).generationId;
|
|
252
|
+
if (generationId)
|
|
253
|
+
socket.write(`${JSON.stringify({ type: "generation_changed", generationId })}\n`);
|
|
254
|
+
});
|
|
255
|
+
server.on("error", (error) => {
|
|
256
|
+
send({ type: "review_failed", error: `Coordinator server failed: ${error.message}` });
|
|
257
|
+
stepDown("coordinator-server-failed");
|
|
258
|
+
scheduleElection();
|
|
259
|
+
});
|
|
260
|
+
writeLeaderMetadata(electedAt);
|
|
261
|
+
reviewStatus = {
|
|
262
|
+
version: 1, protocolVersion: 3, state: "leader", pid: process.pid, leaderToken, electedAt, updatedAt: electedAt,
|
|
263
|
+
};
|
|
264
|
+
publishReviewStatus();
|
|
265
|
+
send({ type: "leadership", state: "leader", leaderToken });
|
|
266
|
+
void scanForReview();
|
|
267
|
+
}
|
|
268
|
+
function attemptElection() {
|
|
269
|
+
if (!eligible || stopped || leaderServer || standbySocket)
|
|
270
|
+
return;
|
|
271
|
+
ensureKnowledgeDirectories(home);
|
|
272
|
+
const server = createServer();
|
|
273
|
+
const electionError = (error) => {
|
|
274
|
+
if (error.code === "EADDRINUSE")
|
|
275
|
+
connectToLeader();
|
|
276
|
+
else {
|
|
277
|
+
send({ type: "review_failed", error: `Coordinator election failed: ${error.message}` });
|
|
278
|
+
scheduleElection(1_000);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
server.once("error", electionError);
|
|
282
|
+
server.listen(coordinatorPath(), () => {
|
|
283
|
+
server.off("error", electionError);
|
|
284
|
+
if (process.platform !== "win32")
|
|
285
|
+
chmodSync(coordinatorPath(), 0o600);
|
|
286
|
+
becomeLeader(server);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async function scanForReview() {
|
|
290
|
+
if (!leaderServer || !leaderToken || activeTask || !sessionsRoot || !eligible)
|
|
291
|
+
return;
|
|
292
|
+
if (reviewStatus) {
|
|
293
|
+
reviewStatus.lastScanAt = new Date().toISOString();
|
|
294
|
+
reviewStatus.lastScanError = undefined;
|
|
295
|
+
publishReviewStatus();
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const manifest = loadCurrentManifest(home);
|
|
299
|
+
const task = findNextReviewTask(sessionsRoot, manifest.reviews);
|
|
300
|
+
if (!task)
|
|
301
|
+
return;
|
|
302
|
+
activeTask = task;
|
|
303
|
+
beginReview(task);
|
|
304
|
+
if (!task.delta) {
|
|
305
|
+
const result = commitKnowledgeReview([], task, leaderToken, home);
|
|
306
|
+
const generationId = loadCurrentManifest(home).generationId;
|
|
307
|
+
finishReview(task, "saved", { generationId, result });
|
|
308
|
+
send({ type: "review_saved", requestId: task.requestId, generationId, result });
|
|
309
|
+
if (generationId) {
|
|
310
|
+
send({ type: "generation_changed", generationId });
|
|
311
|
+
broadcast({ type: "generation_changed", generationId });
|
|
312
|
+
}
|
|
313
|
+
activeTask = undefined;
|
|
314
|
+
queueMicrotask(() => void scanForReview());
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
send({ type: "review_request", leaderToken, requestId: task.requestId, task });
|
|
318
|
+
}
|
|
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
|
+
}
|
|
326
|
+
activeTask = undefined;
|
|
327
|
+
send({ type: "review_failed", error: boundedError(error) });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function handleReviewResult(message) {
|
|
331
|
+
const task = activeTask;
|
|
332
|
+
if (!task || message.requestId !== task.requestId || message.leaderToken !== leaderToken || !leaderServer)
|
|
333
|
+
return;
|
|
334
|
+
let committed = false;
|
|
335
|
+
try {
|
|
336
|
+
if (message.error || message.raw === undefined)
|
|
337
|
+
throw new Error(message.error || "Knowledge review returned no content");
|
|
338
|
+
const result = commitKnowledgeReview(parseCandidateEnvelope(message.raw), task, leaderToken, home);
|
|
339
|
+
const generationId = loadCurrentManifest(home).generationId;
|
|
340
|
+
finishReview(task, "saved", { generationId, result });
|
|
341
|
+
send({ type: "review_saved", requestId: task.requestId, generationId, result });
|
|
342
|
+
if (generationId) {
|
|
343
|
+
send({ type: "generation_changed", generationId });
|
|
344
|
+
broadcast({ type: "generation_changed", generationId });
|
|
345
|
+
}
|
|
346
|
+
committed = true;
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
finishReview(task, "failed", { error });
|
|
350
|
+
send({ type: "review_failed", requestId: task.requestId, error: boundedError(error) });
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
activeTask = undefined;
|
|
354
|
+
if (committed)
|
|
355
|
+
queueMicrotask(() => void scanForReview());
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const scanTimer = setInterval(() => { void scanForReview(); }, KNOWLEDGE_RUNTIME_DEFAULTS.review.intervalMs);
|
|
359
|
+
scanTimer.unref();
|
|
360
|
+
parentPort.on("message", (message) => {
|
|
361
|
+
if (message.type === "configure") {
|
|
362
|
+
sessionsRoot = message.sessionsRoot;
|
|
363
|
+
if (eligible === message.modelAvailable)
|
|
364
|
+
return;
|
|
365
|
+
eligible = message.modelAvailable;
|
|
366
|
+
if (!eligible) {
|
|
367
|
+
standbySocket?.destroy();
|
|
368
|
+
standbySocket = undefined;
|
|
369
|
+
stepDown("model-unavailable");
|
|
370
|
+
send({ type: "leadership", state: "ineligible" });
|
|
371
|
+
}
|
|
372
|
+
else
|
|
373
|
+
scheduleElection(0);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (message.type === "review_result") {
|
|
377
|
+
handleReviewResult(message);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (message.type === "scan_now") {
|
|
381
|
+
void scanForReview();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
stopped = true;
|
|
385
|
+
eligible = false;
|
|
386
|
+
clearInterval(scanTimer);
|
|
387
|
+
standbySocket?.destroy();
|
|
388
|
+
standbySocket = undefined;
|
|
389
|
+
stepDown("worker-stopped");
|
|
390
|
+
process.exit(0);
|
|
391
|
+
});
|
|
392
|
+
send({ type: "ready" });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const PRIVATE_KEY_RE = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/giu;
|
|
2
|
+
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/giu;
|
|
3
|
+
const JWT_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/gu;
|
|
4
|
+
const SECRET_ASSIGNMENT_RE = /\b(access[_-]?key|secret(?:[_-]?(?:access|key))?|password|passwd|token|credential|client[_-]?secret)\b(\s*[:=]\s*)[^\s,;]+/giu;
|
|
5
|
+
const URL_CREDENTIAL_RE = /(https?:\/\/)[^\s/@:]+:[^\s/@]+@/giu;
|
|
6
|
+
export function sanitizeKnowledgeText(value) {
|
|
7
|
+
return value
|
|
8
|
+
.replace(PRIVATE_KEY_RE, "<redacted-private-key>")
|
|
9
|
+
.replace(BEARER_RE, "Bearer <redacted>")
|
|
10
|
+
.replace(JWT_RE, "<redacted-jwt>")
|
|
11
|
+
.replace(SECRET_ASSIGNMENT_RE, (_match, key, separator) => `${key}${separator}<redacted>`)
|
|
12
|
+
.replace(URL_CREDENTIAL_RE, "$1<redacted>@")
|
|
13
|
+
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/giu, "<runtime-uuid>")
|
|
14
|
+
.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/gu, "<runtime-ip>")
|
|
15
|
+
.replace(/\b(vpc|subnet|secgroup|sg|vol|snap|img|inst|eip)-[0-9a-zA-Z]+/gu, "<$1-runtime-id>")
|
|
16
|
+
.replace(/\/(?:Users|home)\/[a-zA-Z0-9_-]+/gu, "~");
|
|
17
|
+
}
|
|
18
|
+
export function compactKnowledgeText(value, maxChars) {
|
|
19
|
+
return sanitizeKnowledgeText(value)
|
|
20
|
+
.replace(/\r\n/gu, "\n")
|
|
21
|
+
.replace(/[ \t]+$/gmu, "")
|
|
22
|
+
.trim()
|
|
23
|
+
.slice(0, maxChars);
|
|
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
|
+
}
|