@hadooppei/hwcode 1.0.24 → 1.0.25
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 +1 -1
- package/.pi/dist/lib/knowledge/extractor.js +6 -4
- package/.pi/dist/lib/knowledge/session-scanner.js +3 -1
- package/.pi/dist/lib/knowledge/store.js +15 -3
- package/.pi/dist/lib/runtime/defaults.js +1 -1
- package/.pi/extensions/knowledge.ts +1 -1
- package/.pi/extensions/model-providers.ts +9 -2
- package/.pi/lib/knowledge/evidence.ts +1 -1
- package/.pi/lib/knowledge/extractor.ts +8 -5
- package/.pi/lib/knowledge/session-scanner.ts +3 -1
- package/.pi/lib/knowledge/store.ts +16 -4
- package/.pi/lib/knowledge/types.ts +3 -0
- package/.pi/lib/models/sse-response.ts +50 -0
- package/.pi/lib/runtime/defaults.ts +1 -1
- package/package.json +1 -1
|
@@ -299,5 +299,5 @@ export function buildKnowledgeEpisodes(evidence) {
|
|
|
299
299
|
? episodes.filter((episode) => !episode.id.includes("-general-reusable-work-") || episode.userSignal)
|
|
300
300
|
: episodes;
|
|
301
301
|
return retained.sort((left, right) => right.score - left.score || right.evidenceIds.length - left.evidenceIds.length)
|
|
302
|
-
.slice(0,
|
|
302
|
+
.slice(0, 2);
|
|
303
303
|
}
|
|
@@ -14,14 +14,16 @@ Treat failed, unsupported, invalid, or corrected operations as negative evidence
|
|
|
14
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.
|
|
15
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.
|
|
16
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
|
-
|
|
17
|
+
For every candidate, first compare the most specific recalled existing topic. When it covers the same concrete failure environment, symptom, root cause, or verified workaround, you must target that topic with "reinforce", "extend", or "correct" as warranted instead of adding knowledge. Prefer the concrete fault topic over broad conventions, generic SOP topics, or incidental access facts such as a temporary SSH user, port, or resource name.
|
|
18
18
|
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
19
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.
|
|
20
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
|
|
22
|
-
|
|
21
|
+
Keep each candidate atomic: put separable claim sets in separate candidates, omit any claim without direct cited evidence, and select only the two highest-value supported information items. One unsupported claim rejects its entire candidate.
|
|
22
|
+
Classify every candidate as exactly one category: "principle" for a durable invariant or rule; "configuration" for a verified setting or parameter contract; "method" for a repeatable procedure; "strategy" for reusable diagnosis or design reasoning; or "caution" for a trap, failure signature, or boundary.
|
|
23
|
+
Return compact JSON intended for a background task: at most 2 candidates; each summary at most 120 characters; each topic body at most 800 characters; each episode decision reason at most 120 characters; and all candidate bodies combined at most 1400 characters. Prefer fewer complete high-value candidates over filling the limit. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered.
|
|
23
24
|
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.
|
|
24
|
-
|
|
25
|
+
Evidence IDs are short handles such as "e1" and "e2". Copy them exactly from the evidence ledger.
|
|
26
|
+
Return: {"episodeDecisions":[{"episodeId":"ep-id","decision":"add|reinforce|extend|correct|retire|skip","targetId":"existing-id or omitted","reason":"..."}],"candidates":[{"key":"stable semantic key","category":"principle|configuration|method|strategy|caution","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":["e1"]}],"evidenceIds":["e1"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|extend|correct|retire","explicitUserDirective":false,"durability":"stable"}]}
|
|
25
27
|
Return {"episodeDecisions":[],"candidates":[]} only when there are no mandatory episodes and nothing meets the threshold.`;
|
|
26
28
|
export function buildKnowledgeExtractionPrompt(projectRoot, delta, context = "", existing = [], evidence = [], episodes = []) {
|
|
27
29
|
const related = existing.length > 0 ? JSON.stringify(existing) : "[]";
|
|
@@ -312,7 +312,9 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
312
312
|
return undefined;
|
|
313
313
|
const pendingEntryIds = new Set(pending.map((entry) => entry.id));
|
|
314
314
|
const evidenceWindow = cursorIndex >= 0 ? branch.slice(Math.max(0, cursorIndex - 40)) : pending;
|
|
315
|
-
const evidence = buildKnowledgeEvidence(evidenceWindow)
|
|
315
|
+
const evidence = buildKnowledgeEvidence(evidenceWindow)
|
|
316
|
+
.filter((item) => pendingEntryIds.has(item.entryId))
|
|
317
|
+
.map((item, index) => ({ ...item, id: `e${index + 1}` }));
|
|
316
318
|
const episodes = buildKnowledgeEpisodes(evidence);
|
|
317
319
|
const lookupResultEntryIds = knowledgeLookupResultEntryIds(branch);
|
|
318
320
|
const delta = buildDelta(pending, lookupResultEntryIds);
|
|
@@ -11,6 +11,9 @@ const SAFE_GENERATION_RE = /^[0-9TZ-]+-[a-f0-9]{12}$/u;
|
|
|
11
11
|
const PROJECT_SCOPE_RE = /^project:[a-f0-9]{16}$/u;
|
|
12
12
|
const UNVERIFIED_CLAIM_RE = /(?:待验证|尚未(?:验证|确认)|未经(?:验证|确认)|未(?:经)?证实|推测|猜测|疑似|可能是|可能由于|可能导致|或许|unverified|unconfirmed|speculat(?:e|ive|ion)|hypothes(?:is|ize)|suspect(?:ed|ion)?|possibly|probably)/iu;
|
|
13
13
|
const VOLATILE_ARGUMENT_RE = /--(?:namespace|name|server_name|security_group_id|subnet_id|vpc_id|image_id|project_id)(?:\.\d+)?=(?!<[^>]+>|\$?\{)[^\s`"']+/iu;
|
|
14
|
+
const KNOWLEDGE_CATEGORIES = new Set([
|
|
15
|
+
"principle", "configuration", "method", "strategy", "caution",
|
|
16
|
+
]);
|
|
14
17
|
export class KnowledgeCommitBusyError extends Error {
|
|
15
18
|
}
|
|
16
19
|
function emptyCatalog() {
|
|
@@ -219,6 +222,9 @@ function normalizeCandidate(value, task, validationText) {
|
|
|
219
222
|
if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
|
|
220
223
|
|| typeof raw.body !== "string" || typeof raw.confidence !== "number")
|
|
221
224
|
return { reason: "invalid-schema" };
|
|
225
|
+
if (typeof raw.category !== "string" || !KNOWLEDGE_CATEGORIES.has(raw.category)) {
|
|
226
|
+
return { reason: "invalid-schema" };
|
|
227
|
+
}
|
|
222
228
|
if (raw.durability !== "stable")
|
|
223
229
|
return { reason: "unstable" };
|
|
224
230
|
if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence)
|
|
@@ -283,6 +289,7 @@ function normalizeCandidate(value, task, validationText) {
|
|
|
283
289
|
|| raw.action === "retire" ? raw.action : "add";
|
|
284
290
|
return { candidate: {
|
|
285
291
|
key: compactKnowledgeText(raw.key, 160),
|
|
292
|
+
category: raw.category,
|
|
286
293
|
identityKey: typeof raw.identityKey === "string" ? compactKnowledgeText(raw.identityKey, 240).toLowerCase() : undefined,
|
|
287
294
|
targetId,
|
|
288
295
|
title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
@@ -315,7 +322,8 @@ function renderKnowledgeFile(candidate) {
|
|
|
315
322
|
if (candidate.storageHint === "rule")
|
|
316
323
|
return `# ${candidate.title}\n\n${candidate.body}\n`;
|
|
317
324
|
return [
|
|
318
|
-
`# ${candidate.title}`, "", candidate.summary, "", `
|
|
325
|
+
`# ${candidate.title}`, "", candidate.summary, "", `Category: ${candidate.category}`,
|
|
326
|
+
`Keywords: ${candidate.keywords.join(", ")}`, "",
|
|
319
327
|
candidate.body, "", "## Evidence", "", ...candidate.evidence.map((item) => `- ${item}`), "",
|
|
320
328
|
].join("\n");
|
|
321
329
|
}
|
|
@@ -342,7 +350,8 @@ function generateMemory(catalog, projectKey) {
|
|
|
342
350
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
343
351
|
for (const item of topics) {
|
|
344
352
|
const scope = item.scope === "global" ? "global" : "project";
|
|
345
|
-
const
|
|
353
|
+
const category = item.category ? `${item.category}; ` : "";
|
|
354
|
+
const line = `- [${item.id}] (${category}${scope}; ${item.keywords.join(", ")}) ${item.title}: ${item.summary}`;
|
|
346
355
|
if (lines.length + 1 >= STORAGE.maxMemoryLines || [...lines, line].join("\n").length > STORAGE.maxMemoryChars) {
|
|
347
356
|
lines.push("- Additional topics remain searchable through hwcode_knowledge_lookup(query: \"keywords\").");
|
|
348
357
|
break;
|
|
@@ -493,6 +502,7 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
493
502
|
continue;
|
|
494
503
|
}
|
|
495
504
|
existing.evidenceCount += candidate.evidence.length;
|
|
505
|
+
existing.category ??= candidate.category;
|
|
496
506
|
existing.updatedAt = new Date().toISOString();
|
|
497
507
|
result.updated++;
|
|
498
508
|
continue;
|
|
@@ -535,6 +545,7 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
535
545
|
existing.contentHash = createHash("sha256").update(content).digest("hex");
|
|
536
546
|
existing.keywords = [...new Set([...existing.keywords, ...candidate.keywords])].slice(0, STORAGE.maxKeywordCount);
|
|
537
547
|
existing.claimKeys = [...new Set([...(existing.claimKeys ?? []), ...newClaimKeys])];
|
|
548
|
+
existing.category ??= candidate.category;
|
|
538
549
|
existing.evidenceCount += candidate.evidence.length;
|
|
539
550
|
existing.updatedAt = new Date().toISOString();
|
|
540
551
|
result.updated++;
|
|
@@ -566,7 +577,8 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
566
577
|
const entry = {
|
|
567
578
|
id, fingerprint: existing?.fingerprint ?? fingerprint, contentHash: hash,
|
|
568
579
|
title: candidate.title, summary: candidate.summary,
|
|
569
|
-
keywords: candidate.keywords,
|
|
580
|
+
keywords: candidate.keywords, category: candidate.category,
|
|
581
|
+
scope: existing?.scope ?? candidate.scope, track, file: relativeFile,
|
|
570
582
|
evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
|
|
571
583
|
identityKey: candidate.identityKey ?? existing?.identityKey,
|
|
572
584
|
claimKeys: candidate.claims?.map((claim) => claim.key) ?? existing?.claimKeys,
|
|
@@ -208,6 +208,7 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
208
208
|
title: entry.title,
|
|
209
209
|
summary: entry.summary,
|
|
210
210
|
keywords: entry.keywords,
|
|
211
|
+
category: entry.category,
|
|
211
212
|
track: entry.track,
|
|
212
213
|
identityKey: entry.identityKey,
|
|
213
214
|
claimKeys: entry.claimKeys,
|
|
@@ -254,7 +255,6 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
254
255
|
},
|
|
255
256
|
{
|
|
256
257
|
signal: controller.signal,
|
|
257
|
-
reasoningEffort: "low",
|
|
258
258
|
cacheRetention: "none",
|
|
259
259
|
temperature: 0,
|
|
260
260
|
timeoutMs: KNOWLEDGE_RUNTIME_DEFAULTS.review.requestTimeoutMs,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type ModelInput,
|
|
23
23
|
type ModelProviderConfig,
|
|
24
24
|
} from "../lib/models/provider-config.ts";
|
|
25
|
+
import { closeOpenAiSseAtDone } from "../lib/models/sse-response.ts";
|
|
25
26
|
|
|
26
27
|
type OpenAIModel = Model<"openai-completions">;
|
|
27
28
|
|
|
@@ -373,8 +374,14 @@ function createLoginProvider(
|
|
|
373
374
|
update: () => { models = refreshed; },
|
|
374
375
|
});
|
|
375
376
|
},
|
|
376
|
-
stream: (model, context, options) => stream(model, context,
|
|
377
|
-
|
|
377
|
+
stream: (model, context, options) => stream(model, context, {
|
|
378
|
+
...options,
|
|
379
|
+
fetch: closeOpenAiSseAtDone(options?.fetch),
|
|
380
|
+
} as Parameters<typeof stream>[2]),
|
|
381
|
+
streamSimple: (model, context, options) => streamSimple(model, context, {
|
|
382
|
+
...options,
|
|
383
|
+
fetch: closeOpenAiSseAtDone(options?.fetch),
|
|
384
|
+
}),
|
|
378
385
|
};
|
|
379
386
|
}
|
|
380
387
|
|
|
@@ -314,5 +314,5 @@ export function buildKnowledgeEpisodes(evidence: KnowledgeEvidence[]): Knowledge
|
|
|
314
314
|
? episodes.filter((episode) => !episode.id.includes("-general-reusable-work-") || episode.userSignal)
|
|
315
315
|
: episodes;
|
|
316
316
|
return retained.sort((left, right) => right.score - left.score || right.evidenceIds.length - left.evidenceIds.length)
|
|
317
|
-
.slice(0,
|
|
317
|
+
.slice(0, 2);
|
|
318
318
|
}
|
|
@@ -2,13 +2,14 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
|
|
4
4
|
import { compactKnowledgeText } from "./sanitize.ts";
|
|
5
|
-
import type { KnowledgeEpisode, KnowledgeEpisodeDecision, KnowledgeEvidence } from "./types.ts";
|
|
5
|
+
import type { KnowledgeCategory, KnowledgeEpisode, KnowledgeEpisodeDecision, KnowledgeEvidence } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
export interface ExistingKnowledgeContext {
|
|
8
8
|
id: string;
|
|
9
9
|
title: string;
|
|
10
10
|
summary: string;
|
|
11
11
|
keywords: string[];
|
|
12
|
+
category?: KnowledgeCategory;
|
|
12
13
|
track: "rule" | "topic";
|
|
13
14
|
identityKey?: string;
|
|
14
15
|
claimKeys?: string[];
|
|
@@ -33,14 +34,16 @@ Treat failed, unsupported, invalid, or corrected operations as negative evidence
|
|
|
33
34
|
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.
|
|
34
35
|
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.
|
|
35
36
|
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.
|
|
36
|
-
|
|
37
|
+
For every candidate, first compare the most specific recalled existing topic. When it covers the same concrete failure environment, symptom, root cause, or verified workaround, you must target that topic with "reinforce", "extend", or "correct" as warranted instead of adding knowledge. Prefer the concrete fault topic over broad conventions, generic SOP topics, or incidental access facts such as a temporary SSH user, port, or resource name.
|
|
37
38
|
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
38
39
|
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.
|
|
39
40
|
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.
|
|
40
|
-
Keep each candidate atomic: put separable claim sets in separate candidates, omit any claim without direct cited evidence, and
|
|
41
|
-
|
|
41
|
+
Keep each candidate atomic: put separable claim sets in separate candidates, omit any claim without direct cited evidence, and select only the two highest-value supported information items. One unsupported claim rejects its entire candidate.
|
|
42
|
+
Classify every candidate as exactly one category: "principle" for a durable invariant or rule; "configuration" for a verified setting or parameter contract; "method" for a repeatable procedure; "strategy" for reusable diagnosis or design reasoning; or "caution" for a trap, failure signature, or boundary.
|
|
43
|
+
Return compact JSON intended for a background task: at most 2 candidates; each summary at most 120 characters; each topic body at most 800 characters; each episode decision reason at most 120 characters; and all candidate bodies combined at most 1400 characters. Prefer fewer complete high-value candidates over filling the limit. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered.
|
|
42
44
|
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.
|
|
43
|
-
|
|
45
|
+
Evidence IDs are short handles such as "e1" and "e2". Copy them exactly from the evidence ledger.
|
|
46
|
+
Return: {"episodeDecisions":[{"episodeId":"ep-id","decision":"add|reinforce|extend|correct|retire|skip","targetId":"existing-id or omitted","reason":"..."}],"candidates":[{"key":"stable semantic key","category":"principle|configuration|method|strategy|caution","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":["e1"]}],"evidenceIds":["e1"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|extend|correct|retire","explicitUserDirective":false,"durability":"stable"}]}
|
|
44
47
|
Return {"episodeDecisions":[],"candidates":[]} only when there are no mandatory episodes and nothing meets the threshold.`;
|
|
45
48
|
|
|
46
49
|
export function buildKnowledgeExtractionPrompt(
|
|
@@ -283,7 +283,9 @@ export function readReviewTask(
|
|
|
283
283
|
if (pending.length === 0) return undefined;
|
|
284
284
|
const pendingEntryIds = new Set(pending.map((entry) => entry.id));
|
|
285
285
|
const evidenceWindow = cursorIndex >= 0 ? branch.slice(Math.max(0, cursorIndex - 40)) : pending;
|
|
286
|
-
const evidence = buildKnowledgeEvidence(evidenceWindow)
|
|
286
|
+
const evidence = buildKnowledgeEvidence(evidenceWindow)
|
|
287
|
+
.filter((item) => pendingEntryIds.has(item.entryId))
|
|
288
|
+
.map((item, index) => ({ ...item, id: `e${index + 1}` }));
|
|
287
289
|
const episodes = buildKnowledgeEpisodes(evidence);
|
|
288
290
|
const lookupResultEntryIds = knowledgeLookupResultEntryIds(branch);
|
|
289
291
|
const delta = buildDelta(pending, lookupResultEntryIds);
|
|
@@ -10,7 +10,7 @@ import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
|
|
|
10
10
|
import { userRuntimePaths } from "../runtime/paths.ts";
|
|
11
11
|
import { compactKnowledgeSummary, compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.ts";
|
|
12
12
|
import type {
|
|
13
|
-
KnowledgeCandidate, KnowledgeCatalog, KnowledgeCatalogEntry, KnowledgeClaim, KnowledgeEpisodeDecision,
|
|
13
|
+
KnowledgeCandidate, KnowledgeCatalog, KnowledgeCatalogEntry, KnowledgeCategory, KnowledgeClaim, KnowledgeEpisodeDecision,
|
|
14
14
|
KnowledgeEvidence, KnowledgeManifest, KnowledgeReviewCursor, KnowledgeReviewTask, KnowledgeScope,
|
|
15
15
|
KnowledgeSkipReason, KnowledgeSnapshot, KnowledgeTrack,
|
|
16
16
|
PersistKnowledgeResult,
|
|
@@ -22,6 +22,9 @@ const SAFE_GENERATION_RE = /^[0-9TZ-]+-[a-f0-9]{12}$/u;
|
|
|
22
22
|
const PROJECT_SCOPE_RE = /^project:[a-f0-9]{16}$/u;
|
|
23
23
|
const UNVERIFIED_CLAIM_RE = /(?:待验证|尚未(?:验证|确认)|未经(?:验证|确认)|未(?:经)?证实|推测|猜测|疑似|可能是|可能由于|可能导致|或许|unverified|unconfirmed|speculat(?:e|ive|ion)|hypothes(?:is|ize)|suspect(?:ed|ion)?|possibly|probably)/iu;
|
|
24
24
|
const VOLATILE_ARGUMENT_RE = /--(?:namespace|name|server_name|security_group_id|subnet_id|vpc_id|image_id|project_id)(?:\.\d+)?=(?!<[^>]+>|\$?\{)[^\s`"']+/iu;
|
|
25
|
+
const KNOWLEDGE_CATEGORIES = new Set<KnowledgeCategory>([
|
|
26
|
+
"principle", "configuration", "method", "strategy", "caution",
|
|
27
|
+
]);
|
|
25
28
|
|
|
26
29
|
export class KnowledgeCommitBusyError extends Error {}
|
|
27
30
|
|
|
@@ -248,6 +251,9 @@ function normalizeCandidate(
|
|
|
248
251
|
const raw = value as Record<string, unknown>;
|
|
249
252
|
if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
|
|
250
253
|
|| typeof raw.body !== "string" || typeof raw.confidence !== "number") return { reason: "invalid-schema" };
|
|
254
|
+
if (typeof raw.category !== "string" || !KNOWLEDGE_CATEGORIES.has(raw.category as KnowledgeCategory)) {
|
|
255
|
+
return { reason: "invalid-schema" };
|
|
256
|
+
}
|
|
251
257
|
if (raw.durability !== "stable") return { reason: "unstable" };
|
|
252
258
|
if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence) return { reason: "low-confidence" };
|
|
253
259
|
if (raw.targetId !== undefined && raw.targetId !== null
|
|
@@ -301,6 +307,7 @@ function normalizeCandidate(
|
|
|
301
307
|
|| raw.action === "retire" ? raw.action : "add";
|
|
302
308
|
return { candidate: {
|
|
303
309
|
key: compactKnowledgeText(raw.key, 160),
|
|
310
|
+
category: raw.category as KnowledgeCategory,
|
|
304
311
|
identityKey: typeof raw.identityKey === "string" ? compactKnowledgeText(raw.identityKey, 240).toLowerCase() : undefined,
|
|
305
312
|
targetId,
|
|
306
313
|
title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
@@ -330,7 +337,8 @@ function writeRejectedPending(directory: string, reason: KnowledgeSkipReason, va
|
|
|
330
337
|
function renderKnowledgeFile(candidate: KnowledgeCandidate): string {
|
|
331
338
|
if (candidate.storageHint === "rule") return `# ${candidate.title}\n\n${candidate.body}\n`;
|
|
332
339
|
return [
|
|
333
|
-
`# ${candidate.title}`, "", candidate.summary, "", `
|
|
340
|
+
`# ${candidate.title}`, "", candidate.summary, "", `Category: ${candidate.category}`,
|
|
341
|
+
`Keywords: ${candidate.keywords.join(", ")}`, "",
|
|
334
342
|
candidate.body, "", "## Evidence", "", ...candidate.evidence.map((item) => `- ${item}`), "",
|
|
335
343
|
].join("\n");
|
|
336
344
|
}
|
|
@@ -359,7 +367,8 @@ function generateMemory(catalog: KnowledgeCatalog, projectKey?: string): string
|
|
|
359
367
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
360
368
|
for (const item of topics) {
|
|
361
369
|
const scope = item.scope === "global" ? "global" : "project";
|
|
362
|
-
const
|
|
370
|
+
const category = item.category ? `${item.category}; ` : "";
|
|
371
|
+
const line = `- [${item.id}] (${category}${scope}; ${item.keywords.join(", ")}) ${item.title}: ${item.summary}`;
|
|
363
372
|
if (lines.length + 1 >= STORAGE.maxMemoryLines || [...lines, line].join("\n").length > STORAGE.maxMemoryChars) {
|
|
364
373
|
lines.push("- Additional topics remain searchable through hwcode_knowledge_lookup(query: \"keywords\").");
|
|
365
374
|
break;
|
|
@@ -509,6 +518,7 @@ export function commitKnowledgeReview(
|
|
|
509
518
|
continue;
|
|
510
519
|
}
|
|
511
520
|
existing.evidenceCount += candidate.evidence.length;
|
|
521
|
+
existing.category ??= candidate.category;
|
|
512
522
|
existing.updatedAt = new Date().toISOString();
|
|
513
523
|
result.updated++;
|
|
514
524
|
continue;
|
|
@@ -552,6 +562,7 @@ export function commitKnowledgeReview(
|
|
|
552
562
|
existing.contentHash = createHash("sha256").update(content).digest("hex");
|
|
553
563
|
existing.keywords = [...new Set([...existing.keywords, ...candidate.keywords])].slice(0, STORAGE.maxKeywordCount);
|
|
554
564
|
existing.claimKeys = [...new Set([...(existing.claimKeys ?? []), ...newClaimKeys])];
|
|
565
|
+
existing.category ??= candidate.category;
|
|
555
566
|
existing.evidenceCount += candidate.evidence.length;
|
|
556
567
|
existing.updatedAt = new Date().toISOString();
|
|
557
568
|
result.updated++;
|
|
@@ -584,7 +595,8 @@ export function commitKnowledgeReview(
|
|
|
584
595
|
const entry: KnowledgeCatalogEntry = {
|
|
585
596
|
id, fingerprint: existing?.fingerprint ?? fingerprint, contentHash: hash,
|
|
586
597
|
title: candidate.title, summary: candidate.summary,
|
|
587
|
-
keywords: candidate.keywords,
|
|
598
|
+
keywords: candidate.keywords, category: candidate.category,
|
|
599
|
+
scope: existing?.scope ?? candidate.scope, track, file: relativeFile,
|
|
588
600
|
evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
|
|
589
601
|
identityKey: candidate.identityKey ?? existing?.identityKey,
|
|
590
602
|
claimKeys: candidate.claims?.map((claim) => claim.key) ?? existing?.claimKeys,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type KnowledgeTrack = "rule" | "topic";
|
|
2
|
+
export type KnowledgeCategory = "principle" | "configuration" | "method" | "strategy" | "caution";
|
|
2
3
|
export type KnowledgeScope = "global" | `project:${string}`;
|
|
3
4
|
export type KnowledgeAction = "add" | "reinforce" | "extend" | "correct" | "retire";
|
|
4
5
|
export type KnowledgeDurability = "stable";
|
|
@@ -50,6 +51,7 @@ export interface KnowledgeEpisodeDecision {
|
|
|
50
51
|
|
|
51
52
|
export interface KnowledgeCandidate {
|
|
52
53
|
key: string;
|
|
54
|
+
category: KnowledgeCategory;
|
|
53
55
|
identityKey?: string;
|
|
54
56
|
targetId?: string;
|
|
55
57
|
title: string;
|
|
@@ -75,6 +77,7 @@ export interface KnowledgeCatalogEntry {
|
|
|
75
77
|
title: string;
|
|
76
78
|
summary: string;
|
|
77
79
|
keywords: string[];
|
|
80
|
+
category?: KnowledgeCategory;
|
|
78
81
|
scope: KnowledgeScope;
|
|
79
82
|
track: KnowledgeTrack;
|
|
80
83
|
file: string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const SSE_CONTENT_TYPE = "text/event-stream";
|
|
2
|
+
const DONE_EVENT_RE = /(?:^|\r?\n)data:\s*\[DONE\]\s*(?:\r?\n|$)/u;
|
|
3
|
+
const MARKER_TAIL_CHARS = 64;
|
|
4
|
+
|
|
5
|
+
function closeAtDone(response: Response): Response {
|
|
6
|
+
if (!response.body || !response.headers.get("content-type")?.toLowerCase().includes(SSE_CONTENT_TYPE)) {
|
|
7
|
+
return response;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const reader = response.body.getReader();
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
let tail = "";
|
|
13
|
+
let closed = false;
|
|
14
|
+
const body = new ReadableStream<Uint8Array>({
|
|
15
|
+
async pull(controller) {
|
|
16
|
+
try {
|
|
17
|
+
const chunk = await reader.read();
|
|
18
|
+
if (chunk.done) {
|
|
19
|
+
closed = true;
|
|
20
|
+
controller.close();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
controller.enqueue(chunk.value);
|
|
24
|
+
const text = `${tail}${decoder.decode(chunk.value, { stream: true })}`;
|
|
25
|
+
tail = text.slice(-MARKER_TAIL_CHARS);
|
|
26
|
+
if (!DONE_EVENT_RE.test(text)) return;
|
|
27
|
+
closed = true;
|
|
28
|
+
controller.close();
|
|
29
|
+
void reader.cancel("OpenAI SSE completed with [DONE]").catch(() => {});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
closed = true;
|
|
32
|
+
controller.error(error);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
cancel(reason) {
|
|
36
|
+
if (closed) return;
|
|
37
|
+
closed = true;
|
|
38
|
+
return reader.cancel(reason);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return new Response(body, {
|
|
42
|
+
status: response.status,
|
|
43
|
+
statusText: response.statusText,
|
|
44
|
+
headers: response.headers,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function closeOpenAiSseAtDone(fetchImplementation: typeof fetch = globalThis.fetch): typeof fetch {
|
|
49
|
+
return (async (input, init) => closeAtDone(await fetchImplementation(input, init))) as typeof fetch;
|
|
50
|
+
}
|