@nowcrew/daemon 0.6.9 → 0.6.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/dist/diagnostic-json.js +14 -0
- package/dist/local-executor.js +31 -3
- package/dist/local-memory-diagnostics.js +72 -48
- package/dist/local-memory-telemetry.js +10 -3
- package/dist/memory-prune-diagnostics.js +208 -6
- package/dist/skills.js +19 -9
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const DIAGNOSTIC_JSON_STRING_LIMIT = 3_800;
|
|
2
|
+
export function boundedDiagnosticJsonArray(items, limit = DIAGNOSTIC_JSON_STRING_LIMIT) {
|
|
3
|
+
const selected = [];
|
|
4
|
+
for (const item of items) {
|
|
5
|
+
const candidate = JSON.stringify([...selected, item]);
|
|
6
|
+
if (candidate.length > limit)
|
|
7
|
+
break;
|
|
8
|
+
selected.push(item);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
json: JSON.stringify(selected),
|
|
12
|
+
truncated: selected.length < items.length,
|
|
13
|
+
};
|
|
14
|
+
}
|
package/dist/local-executor.js
CHANGED
|
@@ -18,12 +18,13 @@ import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilit
|
|
|
18
18
|
import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
|
|
19
19
|
import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
|
|
20
20
|
import { dslog } from "./slog.js";
|
|
21
|
-
import {
|
|
21
|
+
import { boundedDiagnosticJsonArray } from "./diagnostic-json.js";
|
|
22
|
+
import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
|
|
22
23
|
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
23
24
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
24
|
-
function
|
|
25
|
+
function memoryPruneFileFactFields(fileFacts) {
|
|
25
26
|
const fields = {};
|
|
26
|
-
for (const [label, fact] of Object.entries(
|
|
27
|
+
for (const [label, fact] of Object.entries(fileFacts)) {
|
|
27
28
|
fields[`${label}_exists`] = fact.exists;
|
|
28
29
|
fields[`${label}_size`] = fact.size;
|
|
29
30
|
fields[`${label}_mtime_ms`] = fact.mtime_ms;
|
|
@@ -36,6 +37,9 @@ function memoryPruneSnapshotFields(snapshot) {
|
|
|
36
37
|
}
|
|
37
38
|
return fields;
|
|
38
39
|
}
|
|
40
|
+
function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, ...fileFacts }) {
|
|
41
|
+
return memoryPruneFileFactFields(fileFacts);
|
|
42
|
+
}
|
|
39
43
|
function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
|
|
40
44
|
const errorCode = error?.code;
|
|
41
45
|
dslog("memory_prune.diagnostics_failed", "长期记忆收尾诊断失败", {
|
|
@@ -772,6 +776,30 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
772
776
|
...memoryPruneSnapshotFields(snapshot),
|
|
773
777
|
});
|
|
774
778
|
if (memoryPruneBeforeSnapshot !== null) {
|
|
779
|
+
const noteDiff = diffMemoryPruneNotes(memoryPruneBeforeSnapshot.notes_manifest, snapshot.notes_manifest);
|
|
780
|
+
const serializedChanges = boundedDiagnosticJsonArray(noteDiff.changes);
|
|
781
|
+
const changesTruncated = noteDiff.truncated || serializedChanges.truncated;
|
|
782
|
+
dslog("memory_prune.notes_changed", "长期记忆功能文件变化已评估", {
|
|
783
|
+
execution_id: input.executionId,
|
|
784
|
+
agent_handle: input.handle,
|
|
785
|
+
task_key: input.taskKey,
|
|
786
|
+
prune_trace_id: memoryPruneTraceId,
|
|
787
|
+
change_count: noteDiff.change_count,
|
|
788
|
+
changes_truncated: changesTruncated,
|
|
789
|
+
evidence_complete: noteDiff.evidence_complete
|
|
790
|
+
&& !changesTruncated,
|
|
791
|
+
before_manifest_error: noteDiff.before_manifest.error,
|
|
792
|
+
after_manifest_error: noteDiff.after_manifest.error,
|
|
793
|
+
before_manifest_truncated: noteDiff.before_manifest.truncated,
|
|
794
|
+
after_manifest_truncated: noteDiff.after_manifest.truncated,
|
|
795
|
+
before_total_count: noteDiff.before_manifest.total_count,
|
|
796
|
+
after_total_count: noteDiff.after_manifest.total_count,
|
|
797
|
+
before_omitted_count: noteDiff.before_manifest.omitted_count,
|
|
798
|
+
after_omitted_count: noteDiff.after_manifest.omitted_count,
|
|
799
|
+
before_incomplete_file_count: noteDiff.before_manifest.incomplete_file_count,
|
|
800
|
+
after_incomplete_file_count: noteDiff.after_manifest.incomplete_file_count,
|
|
801
|
+
changes: serializedChanges.json,
|
|
802
|
+
});
|
|
775
803
|
const postcondition = evaluateMemoryPrunePostcondition(memoryPruneBeforeSnapshot, snapshot, {
|
|
776
804
|
executorCompleted: memoryPruneExecutorCompleted,
|
|
777
805
|
...(memoryPruneRuntimeExitCode === undefined
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
|
-
import { isAbsolute, join, normalize, resolve } from "node:path";
|
|
3
|
+
import { isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
4
4
|
import { capMemoryForInject, MEMORY_INJECT_CAP } from "./prompt.js";
|
|
5
5
|
const INJECTION_MARKERS = [
|
|
6
6
|
"## Injected MEMORY.md (bounded local context)\n",
|
|
@@ -134,50 +134,71 @@ function classifyExplicitPath(path, paths) {
|
|
|
134
134
|
.replace(/^\$CREW_HOME/, paths.homeDir);
|
|
135
135
|
const absolute = normalized(isAbsolute(replaced) ? replaced : resolve(paths.runDir, replaced));
|
|
136
136
|
if (absolute === paths.sharedMemory)
|
|
137
|
-
return "shared_memory";
|
|
137
|
+
return { target: "shared_memory" };
|
|
138
138
|
if (absolute === paths.sharedLessons)
|
|
139
|
-
return "shared_lessons";
|
|
140
|
-
if (absolute === paths.cwdMemory && paths.cwdMemory !== paths.sharedMemory)
|
|
141
|
-
return "cwd_shadow_memory";
|
|
142
|
-
if (absolute === paths.cwdLessons && paths.cwdLessons !== paths.sharedLessons)
|
|
143
|
-
return "cwd_shadow_lessons";
|
|
144
|
-
if (absolute.startsWith(`${paths.sharedNotes}/`))
|
|
145
|
-
return "other_shared_note";
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
function commandTargets(command, paths) {
|
|
149
|
-
const targets = new Set();
|
|
150
|
-
const expandedCommand = command
|
|
151
|
-
.replaceAll("${CREW_HOME}", paths.homeDir)
|
|
152
|
-
.replaceAll("$CREW_HOME", paths.homeDir)
|
|
153
|
-
.replace(/["']/g, "");
|
|
154
|
-
const references = [
|
|
155
|
-
{ values: [paths.sharedMemory], target: "shared_memory" },
|
|
156
|
-
{ values: [paths.sharedLessons], target: "shared_lessons" },
|
|
157
|
-
{ values: [paths.cwdMemory], target: "cwd_shadow_memory" },
|
|
158
|
-
{ values: [paths.cwdLessons], target: "cwd_shadow_lessons" },
|
|
159
|
-
];
|
|
160
|
-
for (const reference of references) {
|
|
161
|
-
if (reference.values.some((value) => expandedCommand.includes(value)))
|
|
162
|
-
targets.add(reference.target);
|
|
139
|
+
return { target: "shared_lessons" };
|
|
140
|
+
if (absolute === paths.cwdMemory && paths.cwdMemory !== paths.sharedMemory) {
|
|
141
|
+
return { target: "cwd_shadow_memory" };
|
|
163
142
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (/(?:^|[\s'"`])notes[\\/]lessons\.md(?:$|[\s'"`;|&])/i.test(withoutKnownPaths)) {
|
|
167
|
-
targets.add("cwd_shadow_lessons");
|
|
143
|
+
if (absolute === paths.cwdLessons && paths.cwdLessons !== paths.sharedLessons) {
|
|
144
|
+
return { target: "cwd_shadow_lessons" };
|
|
168
145
|
}
|
|
169
|
-
|
|
170
|
-
|
|
146
|
+
const relativePath = normalized(relative(paths.homeDir, absolute));
|
|
147
|
+
const relativeSegments = relativePath.split("/");
|
|
148
|
+
if (relativeSegments.length === 2
|
|
149
|
+
&& relativeSegments[0] === "notes"
|
|
150
|
+
&& relativeSegments[1]?.toLowerCase().endsWith(".md")) {
|
|
151
|
+
return { target: "other_shared_note", relative_path: relativePath };
|
|
171
152
|
}
|
|
172
|
-
return
|
|
153
|
+
return null;
|
|
173
154
|
}
|
|
174
155
|
function commandOperation(command) {
|
|
175
|
-
if (/
|
|
156
|
+
if (/[;|&\n]/.test(command))
|
|
157
|
+
return null;
|
|
158
|
+
const executable = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*([^\s]+)/
|
|
159
|
+
.exec(command)?.[1]?.split("/").at(-1)?.toLowerCase();
|
|
160
|
+
if (executable === undefined)
|
|
161
|
+
return null;
|
|
162
|
+
if (["rm", "unlink"].includes(executable))
|
|
176
163
|
return "delete";
|
|
177
|
-
if (
|
|
164
|
+
if (["tee", "touch", "truncate"].includes(executable))
|
|
178
165
|
return "write";
|
|
166
|
+
if (executable === "sed" || executable === "perl") {
|
|
167
|
+
return /(?:^|\s)-i\S*(?:\s|$)/.test(command)
|
|
168
|
+
? "write"
|
|
169
|
+
: "read";
|
|
170
|
+
}
|
|
171
|
+
return ["cat", "rg", "grep", "head", "tail", "less", "more", "wc", "awk"].includes(executable)
|
|
172
|
+
? "read"
|
|
173
|
+
: null;
|
|
174
|
+
}
|
|
175
|
+
function commandAccesses(command, paths) {
|
|
176
|
+
if (/[;|&\n]/.test(command))
|
|
177
|
+
return [];
|
|
178
|
+
const accesses = new Map();
|
|
179
|
+
const expanded = command
|
|
180
|
+
.replaceAll("${CREW_HOME}", paths.homeDir)
|
|
181
|
+
.replaceAll("$CREW_HOME", paths.homeDir)
|
|
182
|
+
.replace(/["']/g, "");
|
|
183
|
+
const redirect = /^(.*?)\s+>>?\s*([^\s]+)\s*$/.exec(expanded);
|
|
184
|
+
const sourceCommand = redirect?.[1] ?? expanded;
|
|
185
|
+
const operation = commandOperation(sourceCommand);
|
|
186
|
+
if (operation !== null) {
|
|
187
|
+
for (const path of sourceCommand.split(/[\s<>]+/)) {
|
|
188
|
+
const classified = classifyExplicitPath(path, paths);
|
|
189
|
+
if (classified === null)
|
|
190
|
+
continue;
|
|
191
|
+
accesses.set(`${classified.target}:${classified.relative_path ?? ""}:${operation}`, { ...classified, operation });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const redirectTarget = redirect?.[2];
|
|
195
|
+
if (redirectTarget !== undefined) {
|
|
196
|
+
const classified = classifyExplicitPath(redirectTarget, paths);
|
|
197
|
+
if (classified !== null) {
|
|
198
|
+
accesses.set(`${classified.target}:${classified.relative_path ?? ""}:write`, { ...classified, operation: "write" });
|
|
199
|
+
}
|
|
179
200
|
}
|
|
180
|
-
return
|
|
201
|
+
return [...accesses.values()];
|
|
181
202
|
}
|
|
182
203
|
function toolOperation(name) {
|
|
183
204
|
const lowered = name.toLowerCase();
|
|
@@ -224,9 +245,9 @@ export function createLocalMemoryAccessObserver(options) {
|
|
|
224
245
|
continue;
|
|
225
246
|
const operation = toolOperation(block.name);
|
|
226
247
|
const path = toolPath(block.input);
|
|
227
|
-
const
|
|
228
|
-
if (operation !== null &&
|
|
229
|
-
pending.set(block.id, { operation,
|
|
248
|
+
const classified = operation === null || path === null ? null : classifyExplicitPath(path, paths);
|
|
249
|
+
if (operation !== null && classified !== null)
|
|
250
|
+
pending.set(block.id, { operation, ...classified });
|
|
230
251
|
}
|
|
231
252
|
}
|
|
232
253
|
if (record.type === "user") {
|
|
@@ -252,11 +273,11 @@ export function createLocalMemoryAccessObserver(options) {
|
|
|
252
273
|
? toolOperation(record.title)
|
|
253
274
|
: null);
|
|
254
275
|
const path = toolPath(record.input);
|
|
255
|
-
const
|
|
276
|
+
const classified = fallbackOperation === null || path === null
|
|
256
277
|
? null
|
|
257
278
|
: classifyExplicitPath(path, paths);
|
|
258
|
-
if (fallbackOperation !== null &&
|
|
259
|
-
pending.set(record.id, { operation: fallbackOperation,
|
|
279
|
+
if (fallbackOperation !== null && classified !== null) {
|
|
280
|
+
pending.set(record.id, { operation: fallbackOperation, ...classified });
|
|
260
281
|
}
|
|
261
282
|
}
|
|
262
283
|
if (record.type === "kimi.acp.tool_result" && typeof record.id === "string") {
|
|
@@ -276,7 +297,6 @@ export function createLocalMemoryAccessObserver(options) {
|
|
|
276
297
|
? record.item
|
|
277
298
|
: null;
|
|
278
299
|
if (item?.type === "command_execution" && typeof item.command === "string") {
|
|
279
|
-
const operation = commandOperation(item.command);
|
|
280
300
|
const outcome = typeof item.exit_code === "number"
|
|
281
301
|
? item.exit_code === 0 ? "succeeded" : "failed"
|
|
282
302
|
: item.status === "completed"
|
|
@@ -284,8 +304,12 @@ export function createLocalMemoryAccessObserver(options) {
|
|
|
284
304
|
: item.status === "failed" || item.status === "declined"
|
|
285
305
|
? "failed"
|
|
286
306
|
: "unknown";
|
|
287
|
-
for (const
|
|
288
|
-
recordObservation(observations, counts, {
|
|
307
|
+
for (const access of commandAccesses(item.command, paths)) {
|
|
308
|
+
recordObservation(observations, counts, {
|
|
309
|
+
...access,
|
|
310
|
+
outcome,
|
|
311
|
+
evidence: "shell_command",
|
|
312
|
+
});
|
|
289
313
|
}
|
|
290
314
|
}
|
|
291
315
|
const fileChange = record.type === "diagnostic.file_change" && Array.isArray(record.changes)
|
|
@@ -305,11 +329,11 @@ export function createLocalMemoryAccessObserver(options) {
|
|
|
305
329
|
const detail = change;
|
|
306
330
|
if (typeof detail.path !== "string")
|
|
307
331
|
continue;
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
332
|
+
const classified = classifyExplicitPath(detail.path, paths);
|
|
333
|
+
if (classified === null)
|
|
310
334
|
continue;
|
|
311
335
|
recordObservation(observations, counts, {
|
|
312
|
-
|
|
336
|
+
...classified,
|
|
313
337
|
operation: detail.kind === "delete" ? "delete" : "write",
|
|
314
338
|
outcome,
|
|
315
339
|
evidence: "file_change",
|
|
@@ -86,6 +86,7 @@ export function createLocalMemoryTelemetry(options) {
|
|
|
86
86
|
let runtimeReady = false;
|
|
87
87
|
let runtimeExitCode;
|
|
88
88
|
let executorCompleted = false;
|
|
89
|
+
let sharedMutationObserved = false;
|
|
89
90
|
return {
|
|
90
91
|
async captureBefore() {
|
|
91
92
|
const startedAt = Date.now();
|
|
@@ -140,6 +141,14 @@ export function createLocalMemoryTelemetry(options) {
|
|
|
140
141
|
observe(event) {
|
|
141
142
|
try {
|
|
142
143
|
for (const observation of accessObserver.observe(event)) {
|
|
144
|
+
const isSharedMutation = observation.outcome === "succeeded"
|
|
145
|
+
&& (observation.operation === "write" || observation.operation === "delete")
|
|
146
|
+
&& (observation.target === "shared_memory"
|
|
147
|
+
|| observation.target === "shared_lessons"
|
|
148
|
+
|| observation.target === "other_shared_note");
|
|
149
|
+
if (isSharedMutation) {
|
|
150
|
+
sharedMutationObserved = true;
|
|
151
|
+
}
|
|
143
152
|
accessSequence += 1;
|
|
144
153
|
logAccess(options, observation, accessSequence);
|
|
145
154
|
}
|
|
@@ -178,8 +187,6 @@ export function createLocalMemoryTelemetry(options) {
|
|
|
178
187
|
const sharedFilesChanged = memoryChanged === undefined || lessonsChanged === undefined
|
|
179
188
|
? undefined
|
|
180
189
|
: memoryChanged || lessonsChanged;
|
|
181
|
-
const currentExecutionSharedWriteObserved = accessSummary.shared_memory_write_succeeded > 0
|
|
182
|
-
|| accessSummary.shared_lessons_write_succeeded > 0;
|
|
183
190
|
safeLog("local_memory.execution_observed", "本地记忆执行观察已完成", {
|
|
184
191
|
execution_id: options.executionId,
|
|
185
192
|
agent_handle: options.agentHandle,
|
|
@@ -196,7 +203,7 @@ export function createLocalMemoryTelemetry(options) {
|
|
|
196
203
|
shared_files_changed_during_execution: sharedFilesChanged,
|
|
197
204
|
unexpected_shared_write: options.isMemoryPrune
|
|
198
205
|
? undefined
|
|
199
|
-
:
|
|
206
|
+
: sharedMutationObserved
|
|
200
207
|
? true
|
|
201
208
|
: sharedFilesChanged === false ? false : undefined,
|
|
202
209
|
access_observation_count: accessSequence,
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
6
|
const TRACE_PATTERN = /\[memory-prune trace_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]/i;
|
|
7
7
|
export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
export const MEMORY_PRUNE_NOTES_MANIFEST_LIMIT = 128;
|
|
9
|
+
export const MEMORY_PRUNE_NOTE_CHANGE_LIMIT = 128;
|
|
10
|
+
function compareCodeUnits(left, right) {
|
|
11
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
12
|
+
}
|
|
8
13
|
export function parseMemoryPruneTraceId(wakePrompt) {
|
|
9
14
|
return TRACE_PATTERN.exec(wakePrompt)?.[1] ?? null;
|
|
10
15
|
}
|
|
11
16
|
async function inspectFile(path, markdownFacts = false, sameAsSharedPath, signal) {
|
|
12
17
|
let metadata;
|
|
13
18
|
try {
|
|
14
|
-
metadata = await
|
|
19
|
+
metadata = await lstat(path);
|
|
15
20
|
}
|
|
16
21
|
catch (error) {
|
|
17
22
|
const code = error.code;
|
|
@@ -32,6 +37,8 @@ async function inspectFile(path, markdownFacts = false, sameAsSharedPath, signal
|
|
|
32
37
|
mtime_ms: metadata.mtimeMs,
|
|
33
38
|
...(sameAsSharedPath === undefined ? {} : { same_as_shared_path: sameAsSharedPath }),
|
|
34
39
|
};
|
|
40
|
+
if (metadata.isSymbolicLink())
|
|
41
|
+
return { ...fact, error: "symbolic_link" };
|
|
35
42
|
if (!metadata.isFile())
|
|
36
43
|
return { ...fact, hash_skipped_reason: "not_regular_file" };
|
|
37
44
|
if (metadata.size > MEMORY_PRUNE_HASH_LIMIT_BYTES) {
|
|
@@ -91,13 +98,106 @@ async function inspectFile(path, markdownFacts = false, sameAsSharedPath, signal
|
|
|
91
98
|
return { ...fact, error: code ?? (error instanceof Error ? error.name : "unknown") };
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
|
-
|
|
101
|
+
function diagnosticError(error) {
|
|
102
|
+
const code = error.code;
|
|
103
|
+
return code ?? (error instanceof Error ? error.name : "unknown");
|
|
104
|
+
}
|
|
105
|
+
async function inspectLessonsFile(rootDir, sameAsSharedPath, signal) {
|
|
106
|
+
const pathFact = sameAsSharedPath === undefined ? {} : { same_as_shared_path: sameAsSharedPath };
|
|
107
|
+
try {
|
|
108
|
+
const notesMetadata = await lstat(join(rootDir, "notes"));
|
|
109
|
+
if (notesMetadata.isSymbolicLink()) {
|
|
110
|
+
return { exists: false, ...pathFact, error: "symbolic_link" };
|
|
111
|
+
}
|
|
112
|
+
if (!notesMetadata.isDirectory())
|
|
113
|
+
return { exists: false, ...pathFact, error: "ENOTDIR" };
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const code = diagnosticError(error);
|
|
117
|
+
return code === "ENOENT"
|
|
118
|
+
? { exists: false, ...pathFact }
|
|
119
|
+
: { exists: false, ...pathFact, error: code };
|
|
120
|
+
}
|
|
121
|
+
return inspectFile(join(rootDir, "notes", "lessons.md"), true, sameAsSharedPath, signal);
|
|
122
|
+
}
|
|
123
|
+
export async function inspectMemoryPruneNotes(homeDir, signal) {
|
|
124
|
+
const notesDir = join(homeDir, "notes");
|
|
125
|
+
try {
|
|
126
|
+
const metadata = await lstat(notesDir);
|
|
127
|
+
if (metadata.isSymbolicLink()) {
|
|
128
|
+
return {
|
|
129
|
+
files: [], total_count: 0, omitted_count: 0, truncated: false, error: "symbolic_link",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (!metadata.isDirectory()) {
|
|
133
|
+
return {
|
|
134
|
+
files: [], total_count: 0, omitted_count: 0, truncated: false, error: "ENOTDIR",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error.code === "ENOENT") {
|
|
140
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
files: [],
|
|
144
|
+
total_count: 0,
|
|
145
|
+
omitted_count: 0,
|
|
146
|
+
truncated: false,
|
|
147
|
+
error: diagnosticError(error),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
let noteNames;
|
|
151
|
+
try {
|
|
152
|
+
noteNames = (await readdir(notesDir, { withFileTypes: true }))
|
|
153
|
+
.filter((entry) => (entry.isFile() || entry.isSymbolicLink())
|
|
154
|
+
&& entry.name.toLowerCase().endsWith(".md"))
|
|
155
|
+
.map((entry) => entry.name)
|
|
156
|
+
.sort(compareCodeUnits);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
files: [], total_count: 0, omitted_count: 0, truncated: false,
|
|
161
|
+
error: diagnosticError(error),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const totalCount = noteNames.length;
|
|
165
|
+
const selected = noteNames.slice(0, MEMORY_PRUNE_NOTES_MANIFEST_LIMIT);
|
|
166
|
+
const files = [];
|
|
167
|
+
for (const name of selected) {
|
|
168
|
+
if (signal?.aborted)
|
|
169
|
+
break;
|
|
170
|
+
const path = join(notesDir, name);
|
|
171
|
+
try {
|
|
172
|
+
if ((await lstat(path)).isSymbolicLink()) {
|
|
173
|
+
files.push({ relative_path: `notes/${name}`, exists: false, error: "symbolic_link" });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
files.push({ relative_path: `notes/${name}`, exists: false, error: diagnosticError(error) });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
files.push({
|
|
182
|
+
relative_path: `notes/${name}`,
|
|
183
|
+
...await inspectFile(path, false, undefined, signal),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
files,
|
|
188
|
+
total_count: totalCount,
|
|
189
|
+
omitted_count: Math.max(0, totalCount - selected.length),
|
|
190
|
+
truncated: totalCount > selected.length,
|
|
191
|
+
...(signal?.aborted ? { error: "ABORT_ERR" } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal) {
|
|
95
195
|
const cwdIsHome = runDir === homeDir;
|
|
96
196
|
const [memory, lessons, cwdMemory, cwdLessons, workLog] = await Promise.all([
|
|
97
197
|
inspectFile(join(homeDir, "MEMORY.md"), true, undefined, signal),
|
|
98
|
-
|
|
198
|
+
inspectLessonsFile(homeDir, undefined, signal),
|
|
99
199
|
inspectFile(join(runDir, "MEMORY.md"), true, cwdIsHome, signal),
|
|
100
|
-
|
|
200
|
+
inspectLessonsFile(runDir, cwdIsHome, signal),
|
|
101
201
|
inspectFile(workLogPath, false, undefined, signal),
|
|
102
202
|
]);
|
|
103
203
|
return {
|
|
@@ -108,8 +208,29 @@ export async function inspectMemoryPruneFiles(homeDir, runDir, workLogPath, sign
|
|
|
108
208
|
work_log: workLog,
|
|
109
209
|
};
|
|
110
210
|
}
|
|
211
|
+
export async function inspectMemoryPruneFiles(homeDir, runDir, workLogPath, signal) {
|
|
212
|
+
const [fixedFiles, notesManifest] = await Promise.all([
|
|
213
|
+
inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal),
|
|
214
|
+
inspectMemoryPruneNotes(homeDir, signal),
|
|
215
|
+
]);
|
|
216
|
+
return { ...fixedFiles, notes_manifest: notesManifest };
|
|
217
|
+
}
|
|
111
218
|
export async function inspectMemoryPruneFilesWithinDeadline(homeDir, runDir, workLogPath, timeoutMs) {
|
|
112
|
-
|
|
219
|
+
const fixedFiles = await withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal), timeoutMs);
|
|
220
|
+
let notesManifest;
|
|
221
|
+
try {
|
|
222
|
+
notesManifest = await withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneNotes(homeDir, signal), timeoutMs);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
notesManifest = {
|
|
226
|
+
files: [],
|
|
227
|
+
total_count: 0,
|
|
228
|
+
omitted_count: 0,
|
|
229
|
+
truncated: false,
|
|
230
|
+
error: diagnosticError(error),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { ...fixedFiles, notes_manifest: notesManifest };
|
|
113
234
|
}
|
|
114
235
|
export async function withinMemoryPruneDiagnosticsDeadline(operation, timeoutMs) {
|
|
115
236
|
const controller = new AbortController();
|
|
@@ -145,6 +266,87 @@ function fileChanged(before, after) {
|
|
|
145
266
|
}
|
|
146
267
|
return before.size !== after.size || before.mtime_ms !== after.mtime_ms;
|
|
147
268
|
}
|
|
269
|
+
function notePresence(file) {
|
|
270
|
+
if (file === undefined)
|
|
271
|
+
return "absent";
|
|
272
|
+
if (file.exists)
|
|
273
|
+
return "present";
|
|
274
|
+
if (!file.exists && (file.error === undefined || file.error === "ENOENT"))
|
|
275
|
+
return "absent";
|
|
276
|
+
return "unknown";
|
|
277
|
+
}
|
|
278
|
+
export function diffMemoryPruneNotes(before, after) {
|
|
279
|
+
const manifestDiagnostics = (manifest) => ({
|
|
280
|
+
total_count: manifest.total_count,
|
|
281
|
+
omitted_count: manifest.omitted_count,
|
|
282
|
+
truncated: manifest.truncated,
|
|
283
|
+
incomplete_file_count: manifest.files.filter((file) => !file.exists
|
|
284
|
+
|| file.sha256 === undefined
|
|
285
|
+
|| file.error !== undefined
|
|
286
|
+
|| file.hash_skipped_reason !== undefined).length,
|
|
287
|
+
...(manifest.error === undefined ? {} : { error: manifest.error }),
|
|
288
|
+
});
|
|
289
|
+
const beforeDiagnostics = manifestDiagnostics(before);
|
|
290
|
+
const afterDiagnostics = manifestDiagnostics(after);
|
|
291
|
+
const beforeByPath = new Map(before.files.map((file) => [file.relative_path, file]));
|
|
292
|
+
const afterByPath = new Map(after.files.map((file) => [file.relative_path, file]));
|
|
293
|
+
const paths = [...new Set([...beforeByPath.keys(), ...afterByPath.keys()])]
|
|
294
|
+
.sort(compareCodeUnits);
|
|
295
|
+
const allChanges = [];
|
|
296
|
+
if (before.error === undefined && after.error === undefined) {
|
|
297
|
+
const presenceDiffIsReliable = !before.truncated && !after.truncated;
|
|
298
|
+
for (const relativePath of paths) {
|
|
299
|
+
const beforeFile = beforeByPath.get(relativePath);
|
|
300
|
+
const afterFile = afterByPath.get(relativePath);
|
|
301
|
+
const beforePresence = notePresence(beforeFile);
|
|
302
|
+
const afterPresence = notePresence(afterFile);
|
|
303
|
+
if (presenceDiffIsReliable && beforePresence === "absent" && afterPresence === "present"
|
|
304
|
+
&& afterFile !== undefined) {
|
|
305
|
+
allChanges.push({
|
|
306
|
+
relative_path: relativePath,
|
|
307
|
+
change: "created",
|
|
308
|
+
...(afterFile.sha256 === undefined ? {} : { after_sha256: afterFile.sha256 }),
|
|
309
|
+
...(afterFile.size === undefined ? {} : { after_size: afterFile.size }),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
else if (presenceDiffIsReliable && beforePresence === "present" && afterPresence === "absent"
|
|
313
|
+
&& beforeFile !== undefined) {
|
|
314
|
+
allChanges.push({
|
|
315
|
+
relative_path: relativePath,
|
|
316
|
+
change: "deleted",
|
|
317
|
+
...(beforeFile.sha256 === undefined ? {} : { before_sha256: beforeFile.sha256 }),
|
|
318
|
+
...(beforeFile.size === undefined ? {} : { before_size: beforeFile.size }),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
else if (beforePresence === "present" && afterPresence === "present"
|
|
322
|
+
&& beforeFile !== undefined && afterFile !== undefined && fileChanged(beforeFile, afterFile)) {
|
|
323
|
+
allChanges.push({
|
|
324
|
+
relative_path: relativePath,
|
|
325
|
+
change: "updated",
|
|
326
|
+
...(beforeFile.sha256 === undefined ? {} : { before_sha256: beforeFile.sha256 }),
|
|
327
|
+
...(afterFile.sha256 === undefined ? {} : { after_sha256: afterFile.sha256 }),
|
|
328
|
+
...(beforeFile.size === undefined ? {} : { before_size: beforeFile.size }),
|
|
329
|
+
...(afterFile.size === undefined ? {} : { after_size: afterFile.size }),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
changes: allChanges.slice(0, MEMORY_PRUNE_NOTE_CHANGE_LIMIT),
|
|
336
|
+
change_count: allChanges.length,
|
|
337
|
+
truncated: allChanges.length > MEMORY_PRUNE_NOTE_CHANGE_LIMIT,
|
|
338
|
+
evidence_complete: before.error === undefined
|
|
339
|
+
&& after.error === undefined
|
|
340
|
+
&& !before.truncated
|
|
341
|
+
&& !after.truncated
|
|
342
|
+
&& before.omitted_count === 0
|
|
343
|
+
&& after.omitted_count === 0
|
|
344
|
+
&& beforeDiagnostics.incomplete_file_count === 0
|
|
345
|
+
&& afterDiagnostics.incomplete_file_count === 0,
|
|
346
|
+
before_manifest: beforeDiagnostics,
|
|
347
|
+
after_manifest: afterDiagnostics,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
148
350
|
export function evaluateMemoryPrunePostcondition(before, after, execution) {
|
|
149
351
|
const facts = {
|
|
150
352
|
work_log_before_nonempty: before.work_log.exists && (before.work_log.size ?? 0) > 0,
|
package/dist/skills.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* - claude: ~/.claude/skills
|
|
7
7
|
* runtime 缺省(老 server 不下发)时按 claude 回落,与历史行为一致。
|
|
8
8
|
* CREW_GLOBAL_SKILLS_DIR 若设置则覆盖以上全部,只扫它。
|
|
9
|
+
* Windows 上目录名相同,位于用户 profile 下(codex 官方说明:"On Windows, use the
|
|
10
|
+
* equivalent path under the user profile"),差异仅在分隔符,由 platform 选 path API。
|
|
9
11
|
*
|
|
10
12
|
* 每个 skill = 一个目录,内含 SKILL.md;从其 YAML frontmatter 取 name/description,
|
|
11
13
|
* 缺失则回退到目录名。只读、容错(目录不存在/无 frontmatter 都不报错)。
|
|
@@ -15,19 +17,25 @@
|
|
|
15
17
|
* 那两处是 reconciler 独占(每次整目录原子替换),且已由「项目技能」区单独展示。
|
|
16
18
|
*/
|
|
17
19
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
18
|
-
import { join } from "node:path";
|
|
20
|
+
import { join, posix, win32 } from "node:path";
|
|
19
21
|
import { homedir } from "node:os";
|
|
20
22
|
import { parseSkillFrontmatter } from "./skill-frontmatter.js";
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 该 runtime 的原生全局 skill 目录;顺序即优先级(重名时靠前的胜出)。
|
|
25
|
+
* 纯路径计算、不碰文件系统——所以能在任意宿主上跨平台单测。
|
|
26
|
+
*/
|
|
27
|
+
export function globalSkillsDirs(runtime, userHome = homedir(), platform = process.platform, env = process.env) {
|
|
28
|
+
const override = env.CREW_GLOBAL_SKILLS_DIR;
|
|
24
29
|
if (override)
|
|
25
30
|
return [override];
|
|
26
|
-
const
|
|
31
|
+
const pathApi = platform === "win32" ? win32 : posix;
|
|
27
32
|
if (runtime === "codex") {
|
|
28
|
-
return [
|
|
33
|
+
return [
|
|
34
|
+
pathApi.join(env.CODEX_HOME ?? pathApi.join(userHome, ".codex"), "skills"),
|
|
35
|
+
pathApi.join(userHome, ".agents", "skills"),
|
|
36
|
+
];
|
|
29
37
|
}
|
|
30
|
-
return [join(
|
|
38
|
+
return [pathApi.join(userHome, ".claude", "skills")];
|
|
31
39
|
}
|
|
32
40
|
async function readSkillsFrom(dir) {
|
|
33
41
|
const names = await readdir(dir).catch(() => []);
|
|
@@ -35,6 +43,7 @@ async function readSkillsFrom(dir) {
|
|
|
35
43
|
for (const name of names) {
|
|
36
44
|
if (name.startsWith("."))
|
|
37
45
|
continue;
|
|
46
|
+
// 读盘一律用宿主的 path:上面算出的目录已是目标平台形态,此处只在本机拼 SKILL.md
|
|
38
47
|
const skillMd = join(dir, name, "SKILL.md");
|
|
39
48
|
const s = await stat(skillMd).catch(() => null);
|
|
40
49
|
if (!s || !s.isFile())
|
|
@@ -46,8 +55,9 @@ async function readSkillsFrom(dir) {
|
|
|
46
55
|
return skills;
|
|
47
56
|
}
|
|
48
57
|
/** 列出该 runtime 的全局 skill,按 name 去重后排序。 */
|
|
49
|
-
export async function listSkills(runtime) {
|
|
50
|
-
const
|
|
58
|
+
export async function listSkills(runtime, userHome = homedir(), platform = process.platform, env = process.env) {
|
|
59
|
+
const dirs = globalSkillsDirs(runtime, userHome, platform, env);
|
|
60
|
+
const perDir = await Promise.all(dirs.map((dir) => readSkillsFrom(dir)));
|
|
51
61
|
// 同名 skill 可能同时存在于多个 root(如 ~/.codex/skills 与 ~/.agents/skills),
|
|
52
62
|
// 取先命中的:既符合 runtime 的目录优先级,也避免前端 key 冲突。
|
|
53
63
|
const byName = new Map();
|