@nowcrew/daemon 0.6.8 → 0.6.10
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/README.md +1 -1
- package/dist/codex-startup-stage.js +54 -0
- package/dist/config.js +1 -1
- package/dist/diagnostic-json.js +14 -0
- package/dist/local-executor.js +52 -6
- package/dist/local-memory-diagnostics.js +72 -48
- package/dist/local-memory-telemetry.js +10 -3
- package/dist/memory-prune-diagnostics.js +182 -6
- package/dist/runtimes/codex-app-server-runner.js +1 -1
- package/dist/serve.js +1 -1
- package/dist/skills.js +47 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -378,7 +378,7 @@ CREW_EXECUTION_MAX_QUEUED_TOTAL=128
|
|
|
378
378
|
CREW_EXECUTION_MAX_STARTING_TOTAL=1
|
|
379
379
|
CREW_EXECUTION_MAX_STARTING_PER_RUNTIME=1
|
|
380
380
|
CREW_EXECUTION_START_GAP_MS=3000
|
|
381
|
-
CREW_EXECUTION_STARTUP_TIMEOUT_MS=
|
|
381
|
+
CREW_EXECUTION_STARTUP_TIMEOUT_MS=120000
|
|
382
382
|
```
|
|
383
383
|
|
|
384
384
|
`MAX_PARALLEL_TOTAL` is the machine-wide process cap shared by protocol-v1 and legacy work.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const STAGE_LINE_PREFIX_CAP = 512;
|
|
2
|
+
const STAGE_PATTERN = /^\[codex-app-server\] stage=(spawn|initialize|thread_start|thread_resume|model_init) status=(start|ok|error|timeout) attempt=(\d+) elapsed_ms=(\d+)(?:\s|$)/;
|
|
3
|
+
function parseBoundedStageLine(line) {
|
|
4
|
+
const match = STAGE_PATTERN.exec(line);
|
|
5
|
+
if (match === null)
|
|
6
|
+
return null;
|
|
7
|
+
const [, rawStage, status, rawAttempt, rawStageMs] = match;
|
|
8
|
+
const attempt = Number(rawAttempt);
|
|
9
|
+
const stageMs = Number(rawStageMs);
|
|
10
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1
|
|
11
|
+
|| !Number.isSafeInteger(stageMs) || stageMs < 0)
|
|
12
|
+
return null;
|
|
13
|
+
if (rawStage === "thread_resume") {
|
|
14
|
+
return { stage: "thread_start", status: status, attempt, stageMs, resumed: true };
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
stage: rawStage,
|
|
18
|
+
status: status,
|
|
19
|
+
attempt,
|
|
20
|
+
stageMs,
|
|
21
|
+
...(rawStage === "thread_start" ? { resumed: false } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export class CodexStartupStageParser {
|
|
25
|
+
onStage;
|
|
26
|
+
pendingPrefix = "";
|
|
27
|
+
constructor(onStage) {
|
|
28
|
+
this.onStage = onStage;
|
|
29
|
+
}
|
|
30
|
+
push(chunk) {
|
|
31
|
+
let remaining = chunk;
|
|
32
|
+
while (remaining.length > 0) {
|
|
33
|
+
const newline = remaining.indexOf("\n");
|
|
34
|
+
const segment = newline < 0 ? remaining : remaining.slice(0, newline);
|
|
35
|
+
const available = STAGE_LINE_PREFIX_CAP - this.pendingPrefix.length;
|
|
36
|
+
if (available > 0)
|
|
37
|
+
this.pendingPrefix += segment.slice(0, available);
|
|
38
|
+
if (newline < 0)
|
|
39
|
+
return;
|
|
40
|
+
this.emitPending();
|
|
41
|
+
remaining = remaining.slice(newline + 1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finish() {
|
|
45
|
+
if (this.pendingPrefix.length > 0)
|
|
46
|
+
this.emitPending();
|
|
47
|
+
}
|
|
48
|
+
emitPending() {
|
|
49
|
+
const stage = parseBoundedStageLine(this.pendingPrefix.replace(/\r$/, ""));
|
|
50
|
+
this.pendingPrefix = "";
|
|
51
|
+
if (stage !== null)
|
|
52
|
+
this.onStage(stage);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -17,7 +17,7 @@ export const DEFAULT_EXECUTION_LIMITS = Object.freeze({
|
|
|
17
17
|
maxStartingTotal: 1,
|
|
18
18
|
maxStartingPerRuntime: 1,
|
|
19
19
|
startupGapMs: 3_000,
|
|
20
|
-
startupTimeoutMs:
|
|
20
|
+
startupTimeoutMs: 120_000,
|
|
21
21
|
});
|
|
22
22
|
// Fits the largest mandatory v1 lifecycle envelope (UUID + timestamps + outcome facts) with margin.
|
|
23
23
|
export const MIN_EXECUTION_EVENT_BYTES = 512;
|
|
@@ -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,11 +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";
|
|
25
|
+
function memoryPruneFileFactFields(fileFacts) {
|
|
24
26
|
const fields = {};
|
|
25
|
-
for (const [label, fact] of Object.entries(
|
|
27
|
+
for (const [label, fact] of Object.entries(fileFacts)) {
|
|
26
28
|
fields[`${label}_exists`] = fact.exists;
|
|
27
29
|
fields[`${label}_size`] = fact.size;
|
|
28
30
|
fields[`${label}_mtime_ms`] = fact.mtime_ms;
|
|
@@ -35,6 +37,9 @@ function memoryPruneSnapshotFields(snapshot) {
|
|
|
35
37
|
}
|
|
36
38
|
return fields;
|
|
37
39
|
}
|
|
40
|
+
function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, ...fileFacts }) {
|
|
41
|
+
return memoryPruneFileFactFields(fileFacts);
|
|
42
|
+
}
|
|
38
43
|
function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
|
|
39
44
|
const errorCode = error?.code;
|
|
40
45
|
dslog("memory_prune.diagnostics_failed", "长期记忆收尾诊断失败", {
|
|
@@ -615,10 +620,30 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
615
620
|
callbacks.onConsole?.(chunk);
|
|
616
621
|
});
|
|
617
622
|
let stderrTail = "";
|
|
623
|
+
const codexStartupStageState = { last: null };
|
|
624
|
+
const codexStartupStageParser = runtime.name === "codex"
|
|
625
|
+
? new CodexStartupStageParser((stage) => {
|
|
626
|
+
codexStartupStageState.last = stage;
|
|
627
|
+
dslog("runtime.start_stage", "Codex runtime 启动阶段更新", {
|
|
628
|
+
level: stage.status === "error" || stage.status === "timeout" ? "ERROR" : "INFO",
|
|
629
|
+
execution_id: input.executionId,
|
|
630
|
+
runtime: runtime.name,
|
|
631
|
+
stage: stage.stage,
|
|
632
|
+
status: stage.status,
|
|
633
|
+
attempt: stage.attempt,
|
|
634
|
+
stage_ms: stage.stageMs,
|
|
635
|
+
startup_ms: Date.now() - runtimeLaunchAt,
|
|
636
|
+
...(stage.stage === "thread_start" ? { resumed: stage.resumed } : {}),
|
|
637
|
+
});
|
|
638
|
+
})
|
|
639
|
+
: null;
|
|
618
640
|
child.stderr.on("data", (data) => {
|
|
619
641
|
process.stderr.write(data);
|
|
620
|
-
|
|
642
|
+
const text = String(data);
|
|
643
|
+
stderrTail = (stderrTail + text).slice(-STDERR_TAIL_CAP);
|
|
644
|
+
codexStartupStageParser?.push(text);
|
|
621
645
|
});
|
|
646
|
+
child.stderr.on("end", () => codexStartupStageParser?.finish());
|
|
622
647
|
let runtimeExit;
|
|
623
648
|
try {
|
|
624
649
|
if (startupReservation !== null) {
|
|
@@ -629,7 +654,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
629
654
|
runtimeReadySignal.then(() => ({ kind: "ready" })),
|
|
630
655
|
child.exit.then((exit) => ({ kind: "exit", exit })),
|
|
631
656
|
new Promise((resolve) => {
|
|
632
|
-
startupTimer = setTimeout(() => resolve({ kind: "timeout" }), dependencies.startupTimeoutMs ??
|
|
657
|
+
startupTimer = setTimeout(() => resolve({ kind: "timeout" }), dependencies.startupTimeoutMs ?? 120_000);
|
|
633
658
|
}),
|
|
634
659
|
]), dependencies.cancellation);
|
|
635
660
|
}
|
|
@@ -639,11 +664,18 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
639
664
|
}
|
|
640
665
|
if (startupOutcome.kind !== "ready") {
|
|
641
666
|
if (startupOutcome.kind === "timeout") {
|
|
667
|
+
const lastCodexStartupStage = codexStartupStageState.last;
|
|
642
668
|
dslog("runtime.start_timeout", "runtime 启动超时", {
|
|
643
669
|
level: "ERROR",
|
|
644
670
|
execution_id: input.executionId,
|
|
645
671
|
runtime: runtime.name,
|
|
646
|
-
startup_timeout_ms: dependencies.startupTimeoutMs ??
|
|
672
|
+
startup_timeout_ms: dependencies.startupTimeoutMs ?? 120_000,
|
|
673
|
+
...(lastCodexStartupStage === null ? {} : {
|
|
674
|
+
last_stage: lastCodexStartupStage.stage,
|
|
675
|
+
last_stage_status: lastCodexStartupStage.status,
|
|
676
|
+
last_stage_ms: lastCodexStartupStage.stageMs,
|
|
677
|
+
attempt: lastCodexStartupStage.attempt,
|
|
678
|
+
}),
|
|
647
679
|
});
|
|
648
680
|
await child.cancel?.();
|
|
649
681
|
}
|
|
@@ -744,6 +776,20 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
744
776
|
...memoryPruneSnapshotFields(snapshot),
|
|
745
777
|
});
|
|
746
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
|
+
changes: serializedChanges.json,
|
|
792
|
+
});
|
|
747
793
|
const postcondition = evaluateMemoryPrunePostcondition(memoryPruneBeforeSnapshot, snapshot, {
|
|
748
794
|
executorCompleted: memoryPruneExecutorCompleted,
|
|
749
795
|
...(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,61 @@ function fileChanged(before, after) {
|
|
|
145
266
|
}
|
|
146
267
|
return before.size !== after.size || before.mtime_ms !== after.mtime_ms;
|
|
147
268
|
}
|
|
269
|
+
export function diffMemoryPruneNotes(before, after) {
|
|
270
|
+
const beforeByPath = new Map(before.files
|
|
271
|
+
.filter((file) => file.exists)
|
|
272
|
+
.map((file) => [file.relative_path, file]));
|
|
273
|
+
const afterByPath = new Map(after.files
|
|
274
|
+
.filter((file) => file.exists)
|
|
275
|
+
.map((file) => [file.relative_path, file]));
|
|
276
|
+
const paths = [...new Set([...beforeByPath.keys(), ...afterByPath.keys()])]
|
|
277
|
+
.sort(compareCodeUnits);
|
|
278
|
+
const allChanges = [];
|
|
279
|
+
for (const relativePath of paths) {
|
|
280
|
+
const beforeFile = beforeByPath.get(relativePath);
|
|
281
|
+
const afterFile = afterByPath.get(relativePath);
|
|
282
|
+
if (beforeFile === undefined && afterFile !== undefined) {
|
|
283
|
+
allChanges.push({
|
|
284
|
+
relative_path: relativePath,
|
|
285
|
+
change: "created",
|
|
286
|
+
...(afterFile.sha256 === undefined ? {} : { after_sha256: afterFile.sha256 }),
|
|
287
|
+
...(afterFile.size === undefined ? {} : { after_size: afterFile.size }),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else if (beforeFile !== undefined && afterFile === undefined) {
|
|
291
|
+
allChanges.push({
|
|
292
|
+
relative_path: relativePath,
|
|
293
|
+
change: "deleted",
|
|
294
|
+
...(beforeFile.sha256 === undefined ? {} : { before_sha256: beforeFile.sha256 }),
|
|
295
|
+
...(beforeFile.size === undefined ? {} : { before_size: beforeFile.size }),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
else if (beforeFile !== undefined && afterFile !== undefined
|
|
299
|
+
&& fileChanged(beforeFile, afterFile)) {
|
|
300
|
+
allChanges.push({
|
|
301
|
+
relative_path: relativePath,
|
|
302
|
+
change: "updated",
|
|
303
|
+
...(beforeFile.sha256 === undefined ? {} : { before_sha256: beforeFile.sha256 }),
|
|
304
|
+
...(afterFile.sha256 === undefined ? {} : { after_sha256: afterFile.sha256 }),
|
|
305
|
+
...(beforeFile.size === undefined ? {} : { before_size: beforeFile.size }),
|
|
306
|
+
...(afterFile.size === undefined ? {} : { after_size: afterFile.size }),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
changes: allChanges.slice(0, MEMORY_PRUNE_NOTE_CHANGE_LIMIT),
|
|
312
|
+
change_count: allChanges.length,
|
|
313
|
+
truncated: allChanges.length > MEMORY_PRUNE_NOTE_CHANGE_LIMIT,
|
|
314
|
+
evidence_complete: before.error === undefined
|
|
315
|
+
&& after.error === undefined
|
|
316
|
+
&& !before.truncated
|
|
317
|
+
&& !after.truncated
|
|
318
|
+
&& [...before.files, ...after.files].every((file) => file.exists
|
|
319
|
+
&& file.sha256 !== undefined
|
|
320
|
+
&& file.error === undefined
|
|
321
|
+
&& file.hash_skipped_reason === undefined),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
148
324
|
export function evaluateMemoryPrunePostcondition(before, after, execution) {
|
|
149
325
|
const facts = {
|
|
150
326
|
work_log_before_nonempty: before.work_log.exists && (before.work_log.size ?? 0) > 0,
|
|
@@ -468,7 +468,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
468
468
|
completionReject(new Error("Codex produced no semantic progress within the startup window"));
|
|
469
469
|
void cancel();
|
|
470
470
|
});
|
|
471
|
-
activeStage = "
|
|
471
|
+
activeStage = "model_init";
|
|
472
472
|
stageStartedAt = Date.now();
|
|
473
473
|
const started = await rpc.request("turn/start", {
|
|
474
474
|
threadId,
|
package/dist/serve.js
CHANGED
|
@@ -741,7 +741,7 @@ export function serve(config, opts = {}) {
|
|
|
741
741
|
try {
|
|
742
742
|
const data2 = req.type === "fs:list" ? await listWorkspace(root, req.path)
|
|
743
743
|
: req.type === "fs:read" ? await readWorkspaceFile(root, req.path)
|
|
744
|
-
: req.type === "skills:list" ? await listSkills(
|
|
744
|
+
: req.type === "skills:list" ? await listSkills(req.runtime)
|
|
745
745
|
: { models: await listRuntimeModels(req.type === "probe-models" ? (req.runtime ?? "") : "") };
|
|
746
746
|
reply({ ok: true, data: data2 });
|
|
747
747
|
}
|
package/dist/skills.js
CHANGED
|
@@ -1,39 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 枚举某 agent
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 枚举某 agent 可用的全局 skill,供 Profile 的 SKILLS 区展示。
|
|
3
|
+
*
|
|
4
|
+
* 目录由 runtime 决定——各 runtime 的原生发现路径互不相同:
|
|
5
|
+
* - codex: $CODEX_HOME/skills (未设时 ~/.codex/skills) + ~/.agents/skills
|
|
6
|
+
* - claude: ~/.claude/skills
|
|
7
|
+
* runtime 缺省(老 server 不下发)时按 claude 回落,与历史行为一致。
|
|
8
|
+
* CREW_GLOBAL_SKILLS_DIR 若设置则覆盖以上全部,只扫它。
|
|
9
|
+
* Windows 上目录名相同,位于用户 profile 下(codex 官方说明:"On Windows, use the
|
|
10
|
+
* equivalent path under the user profile"),差异仅在分隔符,由 platform 选 path API。
|
|
5
11
|
*
|
|
6
12
|
* 每个 skill = 一个目录,内含 SKILL.md;从其 YAML frontmatter 取 name/description,
|
|
7
13
|
* 缺失则回退到目录名。只读、容错(目录不存在/无 frontmatter 都不报错)。
|
|
14
|
+
*
|
|
15
|
+
* 不扫 agent 工作区:项目技能由 project-skills/reconciler 投影到
|
|
16
|
+
* <agentRoot>/.agents/skills 与 <agentRoot>/.crew/claude-skills/.claude/skills,
|
|
17
|
+
* 那两处是 reconciler 独占(每次整目录原子替换),且已由「项目技能」区单独展示。
|
|
8
18
|
*/
|
|
9
19
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
10
|
-
import { join } from "node:path";
|
|
20
|
+
import { join, posix, win32 } from "node:path";
|
|
11
21
|
import { homedir } from "node:os";
|
|
12
22
|
import { parseSkillFrontmatter } from "./skill-frontmatter.js";
|
|
13
|
-
|
|
14
|
-
|
|
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;
|
|
29
|
+
if (override)
|
|
30
|
+
return [override];
|
|
31
|
+
const pathApi = platform === "win32" ? win32 : posix;
|
|
32
|
+
if (runtime === "codex") {
|
|
33
|
+
return [
|
|
34
|
+
pathApi.join(env.CODEX_HOME ?? pathApi.join(userHome, ".codex"), "skills"),
|
|
35
|
+
pathApi.join(userHome, ".agents", "skills"),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
return [pathApi.join(userHome, ".claude", "skills")];
|
|
39
|
+
}
|
|
40
|
+
async function readSkillsFrom(dir) {
|
|
15
41
|
const names = await readdir(dir).catch(() => []);
|
|
16
42
|
const skills = [];
|
|
17
43
|
for (const name of names) {
|
|
18
44
|
if (name.startsWith("."))
|
|
19
45
|
continue;
|
|
46
|
+
// 读盘一律用宿主的 path:上面算出的目录已是目标平台形态,此处只在本机拼 SKILL.md
|
|
20
47
|
const skillMd = join(dir, name, "SKILL.md");
|
|
21
48
|
const s = await stat(skillMd).catch(() => null);
|
|
22
49
|
if (!s || !s.isFile())
|
|
23
50
|
continue;
|
|
24
51
|
const md = await readFile(skillMd, "utf8").catch(() => "");
|
|
25
52
|
const fm = parseSkillFrontmatter(md);
|
|
26
|
-
skills.push({ scope, name: fm.name || name, description: fm.description ?? "" });
|
|
53
|
+
skills.push({ scope: "global", name: fm.name || name, description: fm.description ?? "" });
|
|
27
54
|
}
|
|
28
55
|
return skills;
|
|
29
56
|
}
|
|
30
|
-
/**
|
|
31
|
-
export async function listSkills(
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
/** 列出该 runtime 的全局 skill,按 name 去重后排序。 */
|
|
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)));
|
|
61
|
+
// 同名 skill 可能同时存在于多个 root(如 ~/.codex/skills 与 ~/.agents/skills),
|
|
62
|
+
// 取先命中的:既符合 runtime 的目录优先级,也避免前端 key 冲突。
|
|
63
|
+
const byName = new Map();
|
|
64
|
+
for (const skills of perDir) {
|
|
65
|
+
for (const skill of skills)
|
|
66
|
+
if (!byName.has(skill.name))
|
|
67
|
+
byName.set(skill.name, skill);
|
|
68
|
+
}
|
|
69
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
39
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
21
|
-
"@nowcrew/cli": "^0.4.
|
|
21
|
+
"@nowcrew/cli": "^0.4.14",
|
|
22
22
|
"cross-spawn": "^7.0.6",
|
|
23
23
|
"ws": "^8",
|
|
24
24
|
"yaml": "^2.8.1",
|