@kentwynn/kgraph 0.2.29 → 0.2.31
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/dist/cli/commands/knowledge.js +15 -7
- package/dist/cli/commands/pack.js +12 -4
- package/dist/context/context-pack.js +40 -10
- package/dist/integrations/adapters/claude-code.js +2 -9
- package/dist/integrations/adapters/copilot.js +14 -1
- package/dist/integrations/workflow-steps.js +21 -16
- package/dist/knowledge/atom-store.js +59 -14
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { atomToCognitionNote, readKnowledgeAtoms, updateKnowledgeAtom, updateKnowledgeAtoms, } from '../../knowledge/atom-store.js';
|
|
2
1
|
import { rebuildDomainRecords } from '../../cognition/domain-records.js';
|
|
2
|
+
import { atomToCognitionNote, readKnowledgeAtoms, updateKnowledgeAtom, updateKnowledgeAtoms, } from '../../knowledge/atom-store.js';
|
|
3
3
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
4
4
|
import { readMaps } from '../../storage/map-store.js';
|
|
5
5
|
import { KGraphError, runCommand } from '../errors.js';
|
|
@@ -109,7 +109,10 @@ export function registerKnowledgeCommand(program) {
|
|
|
109
109
|
lifecycle: {
|
|
110
110
|
...nextAtoms[newIndex].lifecycle,
|
|
111
111
|
supersedes: [
|
|
112
|
-
...new Set([
|
|
112
|
+
...new Set([
|
|
113
|
+
...nextAtoms[newIndex].lifecycle.supersedes,
|
|
114
|
+
oldId,
|
|
115
|
+
]),
|
|
113
116
|
],
|
|
114
117
|
},
|
|
115
118
|
};
|
|
@@ -144,13 +147,18 @@ function filterAtoms(atoms, options) {
|
|
|
144
147
|
return atoms.filter((atom) => {
|
|
145
148
|
if (options.type && atom.type !== options.type)
|
|
146
149
|
return false;
|
|
147
|
-
if (options.status &&
|
|
148
|
-
atom.status !== normalizeStatus(options.status)) {
|
|
150
|
+
if (options.status && atom.status !== normalizeStatus(options.status)) {
|
|
149
151
|
return false;
|
|
150
152
|
}
|
|
151
|
-
if (options.topic
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
if (options.topic) {
|
|
154
|
+
const q = options.topic.toLowerCase();
|
|
155
|
+
const matches = atom.topic.toLowerCase().includes(q) ||
|
|
156
|
+
atom.claim.toLowerCase().includes(q) ||
|
|
157
|
+
(atom.summary?.toLowerCase().includes(q) ?? false) ||
|
|
158
|
+
atom.scopeRefs.files.some((f) => f.toLowerCase().includes(q)) ||
|
|
159
|
+
atom.scopeRefs.symbols.some((s) => s.toLowerCase().includes(q));
|
|
160
|
+
if (!matches)
|
|
161
|
+
return false;
|
|
154
162
|
}
|
|
155
163
|
return true;
|
|
156
164
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { loadConfig } from '../../config/config.js';
|
|
2
3
|
import { buildContextPack } from '../../context/context-pack.js';
|
|
3
4
|
import { queryContext } from '../../context/context-query.js';
|
|
4
|
-
import { loadConfig } from '../../config/config.js';
|
|
5
5
|
import { assertSessionAgent, recordSessionEvent, } from '../../session/session-store.js';
|
|
6
|
-
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
7
6
|
import { listInboxNotes } from '../../storage/cognition-store.js';
|
|
7
|
+
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
8
8
|
import { mapsExist, readMaps } from '../../storage/map-store.js';
|
|
9
9
|
import { KGraphError, runCommand } from '../errors.js';
|
|
10
10
|
export function registerPackCommand(program) {
|
|
@@ -41,7 +41,7 @@ export function registerPackCommand(program) {
|
|
|
41
41
|
]);
|
|
42
42
|
const response = await queryContext(workspace, config, maps, task);
|
|
43
43
|
const pack = buildContextPack(response, budget, workspace.rootPath);
|
|
44
|
-
const pendingInboxFiles = (await listInboxNotes(workspace)).map((file) => path.relative(workspace.rootPath, file));
|
|
44
|
+
const pendingInboxFiles = (await listInboxNotes(workspace)).map((file) => path.relative(workspace.rootPath, file).split(path.sep).join('/'));
|
|
45
45
|
if (pendingInboxFiles.length > 0) {
|
|
46
46
|
pack.pendingInbox = {
|
|
47
47
|
count: pendingInboxFiles.length,
|
|
@@ -122,6 +122,12 @@ function appendGroup(lines, title, items) {
|
|
|
122
122
|
lines.push(` range ${data.path}:${data.startLine}-${data.endLine}`);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
if (item.kind === 'symbol') {
|
|
126
|
+
const data = item.data;
|
|
127
|
+
if (data.filePath && data.startLine != null && data.endLine != null) {
|
|
128
|
+
lines.push(` ${data.filePath}:${data.startLine}-${data.endLine}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
125
131
|
}
|
|
126
132
|
if (items.length > 6)
|
|
127
133
|
lines.push(` ◌ ${items.length - 6} more ${title.toLowerCase()} omitted from display`);
|
|
@@ -132,5 +138,7 @@ function formatReasons(reasons) {
|
|
|
132
138
|
return 'included by pack ranking';
|
|
133
139
|
const shown = reasons.slice(0, 3);
|
|
134
140
|
const remaining = reasons.length - shown.length;
|
|
135
|
-
return remaining > 0
|
|
141
|
+
return remaining > 0
|
|
142
|
+
? `${shown.join('; ')}; and ${remaining} more`
|
|
143
|
+
: shown.join('; ');
|
|
136
144
|
}
|
|
@@ -13,14 +13,7 @@ export function buildContextPack(response, budget, rootPath) {
|
|
|
13
13
|
data: ranked.item,
|
|
14
14
|
})),
|
|
15
15
|
...buildFileRangeCandidates(response, budget, rootPath),
|
|
16
|
-
...response.relevantSymbols.map((ranked) => (
|
|
17
|
-
kind: 'symbol',
|
|
18
|
-
id: ranked.item.id,
|
|
19
|
-
title: ranked.item.name,
|
|
20
|
-
tokenEstimate: 20,
|
|
21
|
-
reasons: ranked.reasons,
|
|
22
|
-
data: ranked.item,
|
|
23
|
-
})),
|
|
16
|
+
...response.relevantSymbols.map((ranked) => buildSymbolCandidate(ranked, rootPath)),
|
|
24
17
|
...response.relevantCognition.map((ranked) => ({
|
|
25
18
|
kind: 'atom',
|
|
26
19
|
id: ranked.item.id,
|
|
@@ -131,14 +124,18 @@ function strongPackPaths(candidates) {
|
|
|
131
124
|
function isLowSignalCandidate(candidate, strongPaths) {
|
|
132
125
|
if (strongPaths.size === 0)
|
|
133
126
|
return false;
|
|
134
|
-
if (candidate.kind === 'atom' ||
|
|
127
|
+
if (candidate.kind === 'atom' ||
|
|
128
|
+
candidate.kind === 'git-change' ||
|
|
129
|
+
candidate.kind === 'file-range') {
|
|
135
130
|
return false;
|
|
136
131
|
}
|
|
137
132
|
if (hasStrongReason(candidate))
|
|
138
133
|
return false;
|
|
139
134
|
if (candidateTouchesStrongPath(candidate, strongPaths))
|
|
140
135
|
return false;
|
|
141
|
-
return candidate.kind === 'file' ||
|
|
136
|
+
return (candidate.kind === 'file' ||
|
|
137
|
+
candidate.kind === 'symbol' ||
|
|
138
|
+
candidate.kind === 'relationship');
|
|
142
139
|
}
|
|
143
140
|
function hasStrongReason(candidate) {
|
|
144
141
|
return candidate.reasons.some((reason) => reason.includes('matched atom') ||
|
|
@@ -170,6 +167,39 @@ const GENERIC_RANGE_TOKENS = new Set([
|
|
|
170
167
|
'repo',
|
|
171
168
|
'work',
|
|
172
169
|
]);
|
|
170
|
+
const MAX_SYMBOL_EXCERPT_LINES = 40;
|
|
171
|
+
function buildSymbolCandidate(ranked, rootPath) {
|
|
172
|
+
const symbol = ranked.item;
|
|
173
|
+
let excerpt;
|
|
174
|
+
let tokenEstimate = 20;
|
|
175
|
+
if (rootPath &&
|
|
176
|
+
symbol.startLine != null &&
|
|
177
|
+
symbol.endLine != null &&
|
|
178
|
+
symbol.endLine - symbol.startLine + 1 <= MAX_SYMBOL_EXCERPT_LINES) {
|
|
179
|
+
const fullPath = path.join(rootPath, symbol.filePath);
|
|
180
|
+
if (existsSync(fullPath)) {
|
|
181
|
+
try {
|
|
182
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
183
|
+
const allLines = content.split(/\r?\n/);
|
|
184
|
+
const from = symbol.startLine - 1; // 0-based
|
|
185
|
+
const to = symbol.endLine; // exclusive
|
|
186
|
+
excerpt = allLines.slice(from, to).join('\n');
|
|
187
|
+
tokenEstimate = estimateTokens(excerpt, symbol.filePath);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// best-effort; fall back to default estimate
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
kind: 'symbol',
|
|
196
|
+
id: symbol.id,
|
|
197
|
+
title: symbol.name,
|
|
198
|
+
tokenEstimate,
|
|
199
|
+
reasons: ranked.reasons,
|
|
200
|
+
data: excerpt != null ? { ...symbol, excerpt } : symbol,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
173
203
|
function buildFileRangeCandidates(response, budget, rootPath) {
|
|
174
204
|
if (!rootPath)
|
|
175
205
|
return [];
|
|
@@ -12,16 +12,9 @@ ${numberedWorkflow('claude-code', {
|
|
|
12
12
|
commandFiles: [
|
|
13
13
|
{
|
|
14
14
|
path: '.claude/commands/kgraph.md',
|
|
15
|
-
content:
|
|
15
|
+
content: `## KGraph Workflow
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
2. Run exactly one command from the repository root: \`kgraph "<topic>" --agent claude-code\`.
|
|
19
|
-
3. Treat the returned files, symbols, relationships, atoms, and warnings as the first-pass source of truth.
|
|
20
|
-
4. If the user asked for an edit, inspect only the returned candidate file or the smallest necessary range, then make the edit.
|
|
21
|
-
5. Verify the change actually landed before claiming completion. Prefer a narrow read of the changed range or \`git diff -- <path>\`; if there is no diff or the expected text is missing, say the edit did not apply and fix it before summarizing.
|
|
22
|
-
6. Do not run \`kgraph\` again, \`kgraph context\`, \`kgraph pack\`, \`kgraph knowledge\`, \`kgraph stale\`, \`kgraph blame\`, \`kgraph scan\`, \`kgraph update\`, \`kgraph compact\`, or \`kgraph repair\` unless the user explicitly asks for that lower-level command.
|
|
23
|
-
7. Do not continue broad repository search after the target file is identified. If a path must be located, prefer \`rg --files\` and quote paths containing spaces or parentheses.
|
|
24
|
-
8. At the end of repository-file changes, run \`kgraph "<topic>" --final --agent claude-code\`. If KGraph reports capture-required, run \`kgraph "<topic>" --capture "<durable conclusion>" --capture-file <path> --capture-symbol <name> --agent claude-code\`, or explicitly say "No durable knowledge created" only when there is genuinely no reusable knowledge.
|
|
17
|
+
${numberedWorkflow('claude-code')}
|
|
25
18
|
`,
|
|
26
19
|
},
|
|
27
20
|
{
|
|
@@ -8,6 +8,19 @@ export const copilotAdapter = {
|
|
|
8
8
|
|
|
9
9
|
${numberedWorkflow('copilot')}
|
|
10
10
|
`,
|
|
11
|
-
commandFiles:
|
|
11
|
+
commandFiles: [
|
|
12
|
+
...agentSkillFiles('copilot'),
|
|
13
|
+
{
|
|
14
|
+
path: '.github/agents/kgraph.agent.md',
|
|
15
|
+
content: `---
|
|
16
|
+
description: Use KGraph persistent repo intelligence — runs kgraph before answering to provide file maps, symbols, relationships, and durable knowledge atoms.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## KGraph Workflow
|
|
20
|
+
|
|
21
|
+
${numberedWorkflow('copilot')}
|
|
22
|
+
`,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
12
25
|
obsoleteCommandFiles: [],
|
|
13
26
|
};
|
|
@@ -8,8 +8,11 @@ const REPAIR_STEP = `Run \`kgraph repair --dry-run\` before cleanup when stale/n
|
|
|
8
8
|
const COMPACT_STEP = `Run \`kgraph compact --dry-run\` when cognition looks duplicated, noisy, or stale. Run \`kgraph compact\` only when the user asks to merge/archive cognition.`;
|
|
9
9
|
const HISTORY_STEP = `Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.`;
|
|
10
10
|
const KNOWLEDGE_STEP = `Run \`kgraph knowledge list --topic "<topic>"\` or \`kgraph knowledge get <atom-id>\` when the user asks what KGraph remembers or atom provenance/lifecycle matters.`;
|
|
11
|
+
function inboxStep(agentName) {
|
|
12
|
+
return `If the previous command output contains "pending inbox" or a \`pendingInbox\` field with count > 0, you MUST run \`kgraph "<topic>" --agent ${agentName}\` or \`kgraph update\` immediately before proceeding. Do not skip this step.`;
|
|
13
|
+
}
|
|
11
14
|
function packStep(agentName) {
|
|
12
|
-
return `For normal coding context, treat \`kgraph pack "<task>" --budget 8000 --json --agent ${agentName}\` as the machine-readable context contract: use atoms, source ranges, git changes, omitted items, and inclusion reasons from the ContextPack before reading files
|
|
15
|
+
return `For normal coding context, treat \`kgraph pack "<task>" --budget 8000 --json --agent ${agentName}\` as the machine-readable context contract: use atoms, source ranges, git changes, omitted items, and inclusion reasons from the ContextPack before reading files.`;
|
|
13
16
|
}
|
|
14
17
|
function smartRootStep(agentName) {
|
|
15
18
|
return `Use the root workflow when refresh or memory processing matters: run \`kgraph "<topic>" --agent ${agentName}\` to refresh maps, process inbox notes, and return a briefing; run \`kgraph "<topic>" --final --agent ${agentName}\` before the final answer when repository files changed; run \`kgraph "<topic>" --capture "<durable conclusion>" --capture-file <path> --capture-symbol <name> --agent ${agentName}\` when the final check requires durable knowledge.`;
|
|
@@ -29,24 +32,25 @@ function sessionStep(agentName, qualifier) {
|
|
|
29
32
|
export function numberedWorkflow(agentName, options = {}) {
|
|
30
33
|
return `1. Infer the topic from the user's request.
|
|
31
34
|
2. {{KGRAPH_CONTEXT_POLICY}}
|
|
32
|
-
3. ${
|
|
33
|
-
4.
|
|
34
|
-
5.
|
|
35
|
-
6. ${
|
|
36
|
-
7. ${
|
|
37
|
-
8. ${
|
|
38
|
-
9. ${
|
|
39
|
-
10. ${
|
|
40
|
-
11. ${
|
|
41
|
-
12. ${
|
|
42
|
-
13. ${
|
|
35
|
+
3. ${inboxStep(agentName)}
|
|
36
|
+
4. ${ROUTING_STEP}
|
|
37
|
+
5. Use the returned files, symbols, relationships, and cognition before broad exploration.
|
|
38
|
+
6. ${EXPLORATION_BOUNDARY_STEP}
|
|
39
|
+
7. ${VERIFY_EDIT_STEP}
|
|
40
|
+
8. ${smartRootStep(agentName)}
|
|
41
|
+
9. ${packStep(agentName)}
|
|
42
|
+
10. ${KNOWLEDGE_STEP}
|
|
43
|
+
11. ${DOCTOR_STEP}
|
|
44
|
+
12. ${STALE_STEP}
|
|
45
|
+
13. ${sessionStep(agentName, options.sessionQualifier)}
|
|
46
|
+
14. ${IMPACT_STEP}
|
|
43
47
|
|
|
44
48
|
{{KGRAPH_CAPTURE_POLICY}}
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
15. ${REPAIR_STEP}
|
|
51
|
+
16. ${COMPACT_STEP}
|
|
52
|
+
17. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
|
|
53
|
+
18. ${HISTORY_STEP}`;
|
|
50
54
|
}
|
|
51
55
|
/**
|
|
52
56
|
* Returns the bullet-list workflow for rules files.
|
|
@@ -54,6 +58,7 @@ export function numberedWorkflow(agentName, options = {}) {
|
|
|
54
58
|
*/
|
|
55
59
|
export function bulletWorkflow(agentName, options = {}) {
|
|
56
60
|
return `- {{KGRAPH_CONTEXT_POLICY}}
|
|
61
|
+
- ${inboxStep(agentName)}
|
|
57
62
|
- ${ROUTING_STEP}
|
|
58
63
|
- ${EXPLORATION_BOUNDARY_STEP}
|
|
59
64
|
- ${VERIFY_EDIT_STEP}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, rename, rm, unlink, writeFile, } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
5
|
import { KGraphError } from '../cli/errors.js';
|
|
@@ -120,8 +120,7 @@ export async function migrateLegacyCognitionToAtoms(workspace) {
|
|
|
120
120
|
const current = await readAtomsFile(workspace);
|
|
121
121
|
const currentIds = new Set(current.map((atom) => atom.id));
|
|
122
122
|
const nextMigrated = migrated.filter((atom) => !currentIds.has(atom.id) &&
|
|
123
|
-
!current.some((existing) => existing.topic === atom.topic &&
|
|
124
|
-
existing.claim === atom.claim));
|
|
123
|
+
!current.some((existing) => existing.topic === atom.topic && existing.claim === atom.claim));
|
|
125
124
|
if (nextMigrated.length > 0) {
|
|
126
125
|
await writeKnowledgeAtomsUnlocked(workspace, [
|
|
127
126
|
...current,
|
|
@@ -206,7 +205,10 @@ export async function refreshKnowledgeAtomStatuses(workspace, maps, dryRun = fal
|
|
|
206
205
|
export async function validateKnowledgeStore(workspace, maps) {
|
|
207
206
|
const issues = [];
|
|
208
207
|
if (!(await pathExists(schemaPath(workspace)))) {
|
|
209
|
-
issues.push({
|
|
208
|
+
issues.push({
|
|
209
|
+
code: 'missing-schema',
|
|
210
|
+
message: 'missing knowledge/schema.json',
|
|
211
|
+
});
|
|
210
212
|
}
|
|
211
213
|
else {
|
|
212
214
|
try {
|
|
@@ -220,7 +222,10 @@ export async function validateKnowledgeStore(workspace, maps) {
|
|
|
220
222
|
}
|
|
221
223
|
catch (error) {
|
|
222
224
|
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
-
issues.push({
|
|
225
|
+
issues.push({
|
|
226
|
+
code: 'missing-schema',
|
|
227
|
+
message: `invalid knowledge schema: ${message}`,
|
|
228
|
+
});
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
let atoms = [];
|
|
@@ -252,11 +257,15 @@ export async function validateKnowledgeStore(workspace, maps) {
|
|
|
252
257
|
});
|
|
253
258
|
}
|
|
254
259
|
else if (ref.contentHash && ref.contentHash !== file.contentHash) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
// Only report if the atom hasn't already been moved to needs-review;
|
|
261
|
+
// needs-review atoms are already caught by the quality gate.
|
|
262
|
+
if (atom.status !== 'needs-review') {
|
|
263
|
+
issues.push({
|
|
264
|
+
code: 'stale-file-hash',
|
|
265
|
+
atomId: atom.id,
|
|
266
|
+
message: `${atom.id} references changed file ${ref.path}; run \`kgraph stale\` to update atom status`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
260
269
|
}
|
|
261
270
|
}
|
|
262
271
|
if (ref.type === 'symbol') {
|
|
@@ -344,7 +353,11 @@ function buildEvidenceRefs(input, maps) {
|
|
|
344
353
|
refs.push({ type: 'git', commit: input.commit });
|
|
345
354
|
}
|
|
346
355
|
if (input.sessionId || input.agent) {
|
|
347
|
-
refs.push({
|
|
356
|
+
refs.push({
|
|
357
|
+
type: 'session',
|
|
358
|
+
sessionId: input.sessionId,
|
|
359
|
+
agent: input.agent,
|
|
360
|
+
});
|
|
348
361
|
}
|
|
349
362
|
return refs;
|
|
350
363
|
}
|
|
@@ -373,7 +386,8 @@ function evaluateAtomHealth(refs, maps) {
|
|
|
373
386
|
}
|
|
374
387
|
}
|
|
375
388
|
if (ref.type === 'symbol') {
|
|
376
|
-
const exists = (ref.symbolId && symbolIds.has(ref.symbolId)) ||
|
|
389
|
+
const exists = (ref.symbolId && symbolIds.has(ref.symbolId)) ||
|
|
390
|
+
symbolNames.has(ref.name);
|
|
377
391
|
if (!exists) {
|
|
378
392
|
stale = true;
|
|
379
393
|
reasons.push(`missing symbol:${ref.name}`);
|
|
@@ -393,7 +407,8 @@ function computeConfidence(initial, status, atom) {
|
|
|
393
407
|
return 'low';
|
|
394
408
|
if (status === 'needs-review' && initial === 'high')
|
|
395
409
|
return 'medium';
|
|
396
|
-
if (atom?.provenance.sourceCommand === 'legacy-migration' &&
|
|
410
|
+
if (atom?.provenance.sourceCommand === 'legacy-migration' &&
|
|
411
|
+
initial === 'high') {
|
|
397
412
|
return 'medium';
|
|
398
413
|
}
|
|
399
414
|
return initial;
|
|
@@ -518,7 +533,37 @@ async function atomicWriteFile(targetPath, content) {
|
|
|
518
533
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
519
534
|
const tempPath = path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
520
535
|
await writeFile(tempPath, content, 'utf8');
|
|
521
|
-
|
|
536
|
+
// On Windows, rename over an existing file can transiently fail with EPERM
|
|
537
|
+
// when another handle was recently released. Retry with short delays.
|
|
538
|
+
let lastError;
|
|
539
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
540
|
+
try {
|
|
541
|
+
await rename(tempPath, targetPath);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
const code = error.code;
|
|
546
|
+
if ((code === 'EPERM' || code === 'EACCES') && attempt < 2) {
|
|
547
|
+
lastError = error;
|
|
548
|
+
await delay(20 * (attempt + 1));
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
await unlink(tempPath);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
/* best-effort cleanup */
|
|
556
|
+
}
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
await unlink(tempPath);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
/* best-effort cleanup */
|
|
565
|
+
}
|
|
566
|
+
throw lastError;
|
|
522
567
|
}
|
|
523
568
|
async function withKnowledgeWriteLock(workspace, operation) {
|
|
524
569
|
await mkdir(workspace.knowledgePath, { recursive: true });
|