@nowcrew/daemon 0.5.44 → 0.5.46
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 +27 -6
- package/dist/agent-memory/bridge.js +101 -10
- package/dist/computer-service.js +38 -5
- package/dist/daemon-installation.js +35 -18
- package/dist/daemon-update-eligibility.js +53 -12
- package/dist/daemon-updater.js +1 -1
- package/dist/execution-journal.js +1 -1
- package/dist/execution-protocol.js +3 -11
- package/dist/execution-runner.js +13 -4
- package/dist/external-output.js +4 -0
- package/dist/list-models.js +102 -8
- package/dist/local-executor.js +87 -14
- package/dist/local-memory-diagnostics.js +336 -0
- package/dist/local-memory-telemetry.js +224 -0
- package/dist/machine-info.js +22 -7
- package/dist/main.js +6 -1
- package/dist/normalize.js +14 -4
- package/dist/runtime-capabilities.js +11 -1
- package/dist/runtime-probe.js +26 -0
- package/dist/runtime-startup-gate.js +2 -0
- package/dist/runtimes/codex-app-server-runner.js +10 -0
- package/dist/runtimes/hermes-models.js +117 -0
- package/dist/runtimes/hermes.js +6 -0
- package/dist/runtimes/kimi-acp-runner.js +116 -36
- package/dist/runtimes/opencode-runner.js +122 -0
- package/dist/runtimes/opencode.js +181 -0
- package/dist/serve.js +27 -4
- package/dist/slog.js +21 -3
- package/dist/supervised-runtime.js +22 -1
- package/dist/windows-scheduled-task.js +64 -0
- package/dist/workspace.js +34 -3
- package/package.json +8 -9
package/dist/list-models.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { isWin } from "./platform.js";
|
|
4
|
+
import { listHermesModels } from "./runtimes/hermes-models.js";
|
|
4
5
|
const execFileRaw = promisify(execFile);
|
|
5
6
|
const MODEL_PROBE_TIMEOUT_MS = 8_000;
|
|
6
7
|
const MODEL_PROBE_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
@@ -11,14 +12,16 @@ const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
|
11
12
|
killSignal: "SIGKILL",
|
|
12
13
|
maxBuffer: MODEL_PROBE_MAX_BUFFER_BYTES,
|
|
13
14
|
});
|
|
14
|
-
export async function listRuntimeModels(runtime) {
|
|
15
|
+
export async function listRuntimeModels(runtime, options = {}) {
|
|
15
16
|
switch (runtime) {
|
|
16
17
|
case "codex":
|
|
17
18
|
return parseCodexModels((await execFileP("codex", ["debug", "models"])).stdout);
|
|
18
19
|
case "cursor":
|
|
19
20
|
return parseCursorModels((await execFileP("cursor-agent", ["--list-models"])).stdout);
|
|
21
|
+
case "hermes":
|
|
22
|
+
return listHermesModels(options);
|
|
20
23
|
case "opencode":
|
|
21
|
-
return
|
|
24
|
+
return listOpencodeModels();
|
|
22
25
|
case "pi":
|
|
23
26
|
return parsePiModels((await execFileP("pi", ["--list-models"])).stdout);
|
|
24
27
|
default:
|
|
@@ -76,12 +79,103 @@ function parseCursorModels(stdout) {
|
|
|
76
79
|
return { id: id.trim(), label: (label || id).trim(), ...(index === 0 ? { default: true } : {}) };
|
|
77
80
|
});
|
|
78
81
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
async function outputEvenOnFailure(bin, args) {
|
|
83
|
+
try {
|
|
84
|
+
return (await execFileP(bin, args)).stdout;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const stdout = error.stdout;
|
|
88
|
+
if (typeof stdout === "string")
|
|
89
|
+
return stdout;
|
|
90
|
+
if (Buffer.isBuffer(stdout))
|
|
91
|
+
return stdout.toString("utf8");
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function listOpencodeModels() {
|
|
96
|
+
const verbose = parseOpencodeModels(await outputEvenOnFailure("opencode", ["models", "--verbose"]));
|
|
97
|
+
if (verbose.length > 0)
|
|
98
|
+
return verbose;
|
|
99
|
+
return parseOpencodeModels(await outputEvenOnFailure("opencode", ["models"]));
|
|
100
|
+
}
|
|
101
|
+
const VARIANT_ORDER = new Map([
|
|
102
|
+
["none", 0], ["minimal", 1], ["low", 2], ["medium", 3],
|
|
103
|
+
["high", 4], ["xhigh", 5], ["max", 6],
|
|
104
|
+
]);
|
|
105
|
+
function modelIdLine(line) {
|
|
106
|
+
const id = line.trim().split(/\s+/, 1)[0] ?? "";
|
|
107
|
+
if (!id.includes("/") || /^["{[]/.test(id) || id === id.toUpperCase())
|
|
108
|
+
return null;
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
function collectJson(lines, start) {
|
|
112
|
+
let raw = "";
|
|
113
|
+
for (let index = start; index < lines.length; index += 1) {
|
|
114
|
+
if (index > start && modelIdLine(lines[index]) !== null)
|
|
115
|
+
return { raw, next: index };
|
|
116
|
+
raw += `${raw ? "\n" : ""}${lines[index]}`;
|
|
117
|
+
try {
|
|
118
|
+
JSON.parse(raw);
|
|
119
|
+
return { raw, next: index + 1 };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// A pretty-printed block is complete only when JSON.parse succeeds.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { raw, next: lines.length };
|
|
126
|
+
}
|
|
127
|
+
function parseVariants(raw) {
|
|
128
|
+
try {
|
|
129
|
+
const meta = JSON.parse(raw);
|
|
130
|
+
const variants = meta.variants ?? {};
|
|
131
|
+
const looksReasoning = meta.reasoning === true || Object.entries(variants).some(([name, value]) => VARIANT_ORDER.has(name) || typeof value.reasoningEffort === "string" || value.thinking != null);
|
|
132
|
+
if (!looksReasoning)
|
|
133
|
+
return [];
|
|
134
|
+
return Object.entries(variants)
|
|
135
|
+
.filter(([name, value]) => name.length > 0 && value.disabled !== true)
|
|
136
|
+
.map(([name]) => name)
|
|
137
|
+
.sort((left, right) => {
|
|
138
|
+
const leftOrder = VARIANT_ORDER.get(left);
|
|
139
|
+
const rightOrder = VARIANT_ORDER.get(right);
|
|
140
|
+
if (leftOrder !== undefined && rightOrder !== undefined)
|
|
141
|
+
return leftOrder - rightOrder;
|
|
142
|
+
if (leftOrder !== undefined)
|
|
143
|
+
return -1;
|
|
144
|
+
if (rightOrder !== undefined)
|
|
145
|
+
return 1;
|
|
146
|
+
return left.localeCompare(right);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function parseOpencodeModels(stdout) {
|
|
154
|
+
const lines = stdout.split(/\r?\n/);
|
|
155
|
+
const models = [];
|
|
156
|
+
const byId = new Map();
|
|
157
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
158
|
+
const id = modelIdLine(lines[index]);
|
|
159
|
+
if (id === null)
|
|
160
|
+
continue;
|
|
161
|
+
let modelIndex = byId.get(id);
|
|
162
|
+
if (modelIndex === undefined) {
|
|
163
|
+
modelIndex = models.length;
|
|
164
|
+
byId.set(id, modelIndex);
|
|
165
|
+
models.push({ id, label: id, ...(modelIndex === 0 ? { default: true } : {}) });
|
|
166
|
+
}
|
|
167
|
+
let next = index + 1;
|
|
168
|
+
while (next < lines.length && lines[next].trim() === "")
|
|
169
|
+
next += 1;
|
|
170
|
+
if (next >= lines.length || !lines[next].trim().startsWith("{"))
|
|
171
|
+
continue;
|
|
172
|
+
const block = collectJson(lines, next);
|
|
173
|
+
const reasoning = parseVariants(block.raw);
|
|
174
|
+
if (reasoning.length > 0)
|
|
175
|
+
models[modelIndex] = { ...models[modelIndex], reasoning };
|
|
176
|
+
index = block.next - 1;
|
|
177
|
+
}
|
|
178
|
+
return models;
|
|
85
179
|
}
|
|
86
180
|
function parsePiModels(stdout) {
|
|
87
181
|
return stdout
|
package/dist/local-executor.js
CHANGED
|
@@ -19,6 +19,7 @@ import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancell
|
|
|
19
19
|
import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
|
|
20
20
|
import { dslog } from "./slog.js";
|
|
21
21
|
import { evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
|
|
22
|
+
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
22
23
|
function memoryPruneSnapshotFields(snapshot) {
|
|
23
24
|
const fields = {};
|
|
24
25
|
for (const [label, fact] of Object.entries(snapshot)) {
|
|
@@ -79,6 +80,7 @@ export function withLocalExecutionFacts(serverPrompt, maxBytes) {
|
|
|
79
80
|
}
|
|
80
81
|
const STDERR_TAIL_CAP = 2_000;
|
|
81
82
|
const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
|
|
83
|
+
const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 250;
|
|
82
84
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
83
85
|
const RESERVED_ENV = new Set([
|
|
84
86
|
"PATH",
|
|
@@ -112,15 +114,17 @@ export function awaitExit(child) {
|
|
|
112
114
|
});
|
|
113
115
|
}
|
|
114
116
|
export function exitActivity(runtime, exitCode, stderrTail) {
|
|
115
|
-
if ((runtime === "codex" || runtime === "kimi" ||
|
|
117
|
+
if ((runtime === "codex" || runtime === "kimi" || runtime === "hermes"
|
|
118
|
+
|| runtime === "opencode" || exitCode === -1) && exitCode !== 0) {
|
|
116
119
|
return {
|
|
117
120
|
kind: "error",
|
|
118
121
|
label: "运行出错",
|
|
119
122
|
detail: `${runtime} exited with code ${exitCode}${stderrTail ? `: ${stderrTail}` : ""}`,
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
|
-
if (runtime === "kimi" && exitCode === 0)
|
|
125
|
+
if ((runtime === "kimi" || runtime === "hermes" || runtime === "opencode") && exitCode === 0) {
|
|
123
126
|
return { kind: "done", label: "本轮结束" };
|
|
127
|
+
}
|
|
124
128
|
return null;
|
|
125
129
|
}
|
|
126
130
|
function wrapChild(child) {
|
|
@@ -222,16 +226,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
222
226
|
: "high"
|
|
223
227
|
: runtime.reasoning;
|
|
224
228
|
const providerFp = providerFingerprint(runtime.name, providerConfig);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
let workspace;
|
|
230
|
+
try {
|
|
231
|
+
workspace = await awaitWithCancellation(prepareWorkspace({
|
|
232
|
+
agentsRoot: input.launch.agentsRoot,
|
|
233
|
+
handle: input.handle,
|
|
234
|
+
cliPath: input.launch.cliPath,
|
|
235
|
+
executionId: input.executionId,
|
|
236
|
+
...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
|
|
237
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
238
|
+
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
239
|
+
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
240
|
+
}), dependencies.cancellation);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
logLocalMemoryContextPrepareFailure({
|
|
244
|
+
executionId: input.executionId,
|
|
245
|
+
agentHandle: input.handle,
|
|
246
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
247
|
+
}, "workspace_prepare", error);
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
235
250
|
let materialized = null;
|
|
236
251
|
let knownAttachmentDirectory = null;
|
|
237
252
|
let startupReservation = null;
|
|
@@ -242,6 +257,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
242
257
|
let memoryPruneBeforeSnapshot = null;
|
|
243
258
|
let memoryPruneSharedWriteKey = null;
|
|
244
259
|
let executionWorkspace = workspace;
|
|
260
|
+
let localMemoryTelemetry = null;
|
|
245
261
|
try {
|
|
246
262
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
247
263
|
throw new Error("DeepSeek API key is not configured for this Agent");
|
|
@@ -345,6 +361,29 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
345
361
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
346
362
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
347
363
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
364
|
+
try {
|
|
365
|
+
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
366
|
+
executionId: input.executionId,
|
|
367
|
+
agentHandle: input.handle,
|
|
368
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
369
|
+
runtime: runtime.name,
|
|
370
|
+
stableProtocolRuntime: dependencies.launchRuntime !== undefined,
|
|
371
|
+
agentDir: executionWorkspace.dir,
|
|
372
|
+
runDir: executionWorkspace.runDir,
|
|
373
|
+
isMemoryPrune: memoryPruneTraceId !== null,
|
|
374
|
+
diagnosticsTimeoutMs: dependencies.localMemoryDiagnosticsTimeoutMs
|
|
375
|
+
?? LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS,
|
|
376
|
+
});
|
|
377
|
+
await localMemoryTelemetry.captureBefore();
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
localMemoryTelemetry = null;
|
|
381
|
+
logLocalMemoryDiagnosticsFailure({
|
|
382
|
+
executionId: input.executionId,
|
|
383
|
+
agentHandle: input.handle,
|
|
384
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
385
|
+
}, "initialize", error);
|
|
386
|
+
}
|
|
348
387
|
if (memoryPruneTraceId !== null) {
|
|
349
388
|
memoryPruneSharedWriteKey = JSON.stringify([input.launch.agentsRoot, input.handle]);
|
|
350
389
|
const activePruneCount = (activeMemoryPrunes.get(memoryPruneSharedWriteKey) ?? 0) + 1;
|
|
@@ -378,7 +417,25 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
378
417
|
}
|
|
379
418
|
}
|
|
380
419
|
memoryPruneFailurePhase = "prompt_write";
|
|
381
|
-
|
|
420
|
+
try {
|
|
421
|
+
await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
logLocalMemoryContextPrepareFailure({
|
|
425
|
+
executionId: input.executionId,
|
|
426
|
+
agentHandle: input.handle,
|
|
427
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
428
|
+
}, "system_prompt_write", error);
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
localMemoryTelemetry?.contextPrepared({
|
|
432
|
+
systemPrompt,
|
|
433
|
+
memory: executionWorkspace.memory,
|
|
434
|
+
resumed: resuming,
|
|
435
|
+
...(executionWorkspace.memorySeedCreated === undefined
|
|
436
|
+
? {}
|
|
437
|
+
: { memorySeedCreated: executionWorkspace.memorySeedCreated }),
|
|
438
|
+
});
|
|
382
439
|
memoryPruneFailurePhase = "runtime_prepare";
|
|
383
440
|
const inheritedEnv = { ...process.env };
|
|
384
441
|
for (const key of Object.keys(inheritedEnv)) {
|
|
@@ -454,6 +511,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
454
511
|
const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
|
|
455
512
|
? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, () => launchRuntime(launchRequest))
|
|
456
513
|
: await launchRuntime(launchRequest);
|
|
514
|
+
localMemoryTelemetry?.markRuntimeStarted();
|
|
457
515
|
memoryPruneFailurePhase = "runtime_execution";
|
|
458
516
|
if (child.cancel !== undefined) {
|
|
459
517
|
dependencies.cancellation?.register(child.cancel);
|
|
@@ -479,6 +537,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
479
537
|
if (runtimeReady)
|
|
480
538
|
return;
|
|
481
539
|
runtimeReady = true;
|
|
540
|
+
localMemoryTelemetry?.markRuntimeReady();
|
|
482
541
|
startupReservation?.release();
|
|
483
542
|
dslog("runtime.start_ready", "runtime 已完成初始化", {
|
|
484
543
|
execution_id: input.executionId,
|
|
@@ -493,6 +552,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
493
552
|
const event = parseLine(line);
|
|
494
553
|
if (!event)
|
|
495
554
|
return;
|
|
555
|
+
localMemoryTelemetry?.observe(event);
|
|
496
556
|
if (isRuntimeReadyEvent(runtime.name, event))
|
|
497
557
|
markRuntimeReady();
|
|
498
558
|
const meta = extractRunMeta(event);
|
|
@@ -511,7 +571,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
511
571
|
const extracted = extractFinalText(event);
|
|
512
572
|
if (extracted) {
|
|
513
573
|
const incremental = typeof event === "object" && event !== null
|
|
514
|
-
&& "type" in event && event.type === "kimi.acp.text_delta"
|
|
574
|
+
&& "type" in event && (event.type === "kimi.acp.text_delta"
|
|
575
|
+
|| event.type === "hermes.acp.text_delta" || event.type === "opencode.text_delta");
|
|
515
576
|
finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
|
|
516
577
|
}
|
|
517
578
|
for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
|
|
@@ -569,6 +630,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
569
630
|
throw error;
|
|
570
631
|
}
|
|
571
632
|
const { exitCode, spawnError, terminationSignal } = runtimeExit;
|
|
633
|
+
localMemoryTelemetry?.markRuntimeExited(exitCode);
|
|
572
634
|
memoryPruneRuntimeExitCode = exitCode;
|
|
573
635
|
const errorTail = [
|
|
574
636
|
stderrTail.trim(),
|
|
@@ -602,6 +664,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
602
664
|
}
|
|
603
665
|
}
|
|
604
666
|
memoryPruneExecutorCompleted = true;
|
|
667
|
+
localMemoryTelemetry?.markExecutorCompleted();
|
|
605
668
|
return {
|
|
606
669
|
workspaceRunDir: executionWorkspace.runDir,
|
|
607
670
|
exitCode,
|
|
@@ -624,6 +687,16 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
624
687
|
}
|
|
625
688
|
finally {
|
|
626
689
|
startupReservation?.release();
|
|
690
|
+
try {
|
|
691
|
+
await localMemoryTelemetry?.finish();
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
logLocalMemoryDiagnosticsFailure({
|
|
695
|
+
executionId: input.executionId,
|
|
696
|
+
agentHandle: input.handle,
|
|
697
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
698
|
+
}, "finish", error);
|
|
699
|
+
}
|
|
627
700
|
if (memoryPruneTraceId !== null) {
|
|
628
701
|
try {
|
|
629
702
|
const snapshot = await inspectMemoryPruneFilesWithinDeadline(executionWorkspace.dir, executionWorkspace.runDir, executionWorkspace.workLogPath, dependencies.memoryPruneDiagnosticsTimeoutMs
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, normalize, resolve } from "node:path";
|
|
4
|
+
import { capMemoryForInject, MEMORY_INJECT_CAP } from "./prompt.js";
|
|
5
|
+
const INJECTION_MARKERS = [
|
|
6
|
+
"## Injected MEMORY.md (bounded local context)\n",
|
|
7
|
+
"## [注入] 你的 MEMORY.md(索引,只读参考)\n",
|
|
8
|
+
];
|
|
9
|
+
const LOCAL_MEMORY_HASH_CAP_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
function errorCode(error) {
|
|
11
|
+
const code = error?.code;
|
|
12
|
+
return typeof code === "string" ? code : error instanceof Error ? error.name : "unknown";
|
|
13
|
+
}
|
|
14
|
+
async function inspectFile(path, signal) {
|
|
15
|
+
let metadata;
|
|
16
|
+
try {
|
|
17
|
+
metadata = await stat(path);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const code = errorCode(error);
|
|
21
|
+
return code === "ENOENT" ? { exists: false } : { exists: false, error_code: code };
|
|
22
|
+
}
|
|
23
|
+
const base = { exists: true, size_bytes: metadata.size, mtime_ms: metadata.mtimeMs };
|
|
24
|
+
if (!metadata.isFile())
|
|
25
|
+
return { ...base, hash_skipped_reason: "not_regular_file" };
|
|
26
|
+
if (metadata.size > LOCAL_MEMORY_HASH_CAP_BYTES) {
|
|
27
|
+
return { ...base, hash_skipped_reason: "too_large" };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const content = await readFile(path, { encoding: "utf8", signal });
|
|
31
|
+
const facts = localMemoryContentFacts(content);
|
|
32
|
+
if (facts.scan_skipped_reason !== undefined) {
|
|
33
|
+
return { ...base, hash_skipped_reason: facts.scan_skipped_reason };
|
|
34
|
+
}
|
|
35
|
+
return { ...base, ...facts };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return { ...base, error_code: errorCode(error) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function inspectLocalMemoryFilesWithinDeadline(homeDir, timeoutMs) {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
let timer;
|
|
44
|
+
const inspection = Promise.all([
|
|
45
|
+
inspectFile(join(homeDir, "MEMORY.md"), controller.signal),
|
|
46
|
+
inspectFile(join(homeDir, "notes", "lessons.md"), controller.signal),
|
|
47
|
+
]).then(([memory, lessons]) => ({ memory, lessons }));
|
|
48
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
49
|
+
timer = setTimeout(() => {
|
|
50
|
+
controller.abort();
|
|
51
|
+
reject(Object.assign(new Error("local memory diagnostics timed out"), {
|
|
52
|
+
code: "diagnostics_timeout",
|
|
53
|
+
}));
|
|
54
|
+
}, Math.max(1, timeoutMs));
|
|
55
|
+
timer.unref?.();
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
return await Promise.race([inspection, timeout]);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
if (timer !== undefined)
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function localMemoryContentFacts(content, maxScanBytes = LOCAL_MEMORY_HASH_CAP_BYTES) {
|
|
66
|
+
const sizeBytes = Buffer.byteLength(content, "utf8");
|
|
67
|
+
if (sizeBytes > Math.max(0, maxScanBytes)) {
|
|
68
|
+
return { size_bytes: sizeBytes, scan_skipped_reason: "too_large" };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
size_bytes: sizeBytes,
|
|
72
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
73
|
+
nonblank: content.trim().length > 0,
|
|
74
|
+
markdown_heading_count: content.split(/\r?\n/).filter((line) => /^#{1,6}\s+\S/.test(line)).length,
|
|
75
|
+
lessons_reference_count: content.match(/\bnotes[\\/]lessons\.md\b/gi)?.length ?? 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function analyzeLocalMemoryInjection(systemPrompt, sourceMemory) {
|
|
79
|
+
const sourceBytes = Buffer.byteLength(sourceMemory, "utf8");
|
|
80
|
+
if (sourceMemory.length === 0) {
|
|
81
|
+
return { state: "empty_source", source_bytes: 0, bounded_bytes: 0, injected_bytes: 0 };
|
|
82
|
+
}
|
|
83
|
+
const bounded = capMemoryForInject(sourceMemory);
|
|
84
|
+
const marker = INJECTION_MARKERS.find((candidate) => systemPrompt.includes(candidate));
|
|
85
|
+
if (marker === undefined) {
|
|
86
|
+
return {
|
|
87
|
+
state: "omitted",
|
|
88
|
+
source_bytes: sourceBytes,
|
|
89
|
+
bounded_bytes: Buffer.byteLength(bounded, "utf8"),
|
|
90
|
+
injected_bytes: 0,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const markerStart = systemPrompt.indexOf(marker) + marker.length;
|
|
94
|
+
const sectionTail = systemPrompt.slice(markerStart);
|
|
95
|
+
const nextSection = sectionTail.search(/\n\n## (?:Injected work-log|\[注入\] 本任务 work-log)/);
|
|
96
|
+
const injected = nextSection < 0 ? sectionTail : sectionTail.slice(0, nextSection);
|
|
97
|
+
const fullyInjected = injected.startsWith(bounded);
|
|
98
|
+
return {
|
|
99
|
+
state: fullyInjected
|
|
100
|
+
? sourceMemory.length > MEMORY_INJECT_CAP ? "cap_truncated" : "full"
|
|
101
|
+
: "prompt_budget_truncated",
|
|
102
|
+
source_bytes: sourceBytes,
|
|
103
|
+
bounded_bytes: Buffer.byteLength(bounded, "utf8"),
|
|
104
|
+
injected_bytes: Buffer.byteLength(injected, "utf8"),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function localMemoryFileChanged(before, after) {
|
|
108
|
+
if (before.exists !== after.exists)
|
|
109
|
+
return true;
|
|
110
|
+
if (!before.exists && !after.exists)
|
|
111
|
+
return false;
|
|
112
|
+
if (before.sha256 !== undefined && after.sha256 !== undefined) {
|
|
113
|
+
return before.sha256 !== after.sha256;
|
|
114
|
+
}
|
|
115
|
+
return before.size_bytes !== after.size_bytes || before.mtime_ms !== after.mtime_ms;
|
|
116
|
+
}
|
|
117
|
+
function normalized(path) {
|
|
118
|
+
return normalize(path).replaceAll("\\", "/");
|
|
119
|
+
}
|
|
120
|
+
function targetPaths(options) {
|
|
121
|
+
return {
|
|
122
|
+
homeDir: normalized(resolve(options.homeDir)),
|
|
123
|
+
runDir: normalized(resolve(options.runDir)),
|
|
124
|
+
sharedMemory: normalized(resolve(options.homeDir, "MEMORY.md")),
|
|
125
|
+
sharedLessons: normalized(resolve(options.homeDir, "notes", "lessons.md")),
|
|
126
|
+
sharedNotes: normalized(resolve(options.homeDir, "notes")),
|
|
127
|
+
cwdMemory: normalized(resolve(options.runDir, "MEMORY.md")),
|
|
128
|
+
cwdLessons: normalized(resolve(options.runDir, "notes", "lessons.md")),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function classifyExplicitPath(path, paths) {
|
|
132
|
+
const replaced = path
|
|
133
|
+
.replace(/^\$\{CREW_HOME\}/, paths.homeDir)
|
|
134
|
+
.replace(/^\$CREW_HOME/, paths.homeDir);
|
|
135
|
+
const absolute = normalized(isAbsolute(replaced) ? replaced : resolve(paths.runDir, replaced));
|
|
136
|
+
if (absolute === paths.sharedMemory)
|
|
137
|
+
return "shared_memory";
|
|
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);
|
|
163
|
+
}
|
|
164
|
+
const withoutKnownPaths = references.flatMap((reference) => reference.values)
|
|
165
|
+
.reduce((value, reference) => value.replaceAll(reference, ""), expandedCommand);
|
|
166
|
+
if (/(?:^|[\s'"`])notes[\\/]lessons\.md(?:$|[\s'"`;|&])/i.test(withoutKnownPaths)) {
|
|
167
|
+
targets.add("cwd_shadow_lessons");
|
|
168
|
+
}
|
|
169
|
+
else if (/(?:^|[\s'"`])MEMORY\.md(?:$|[\s'"`;|&])/i.test(withoutKnownPaths)) {
|
|
170
|
+
targets.add("cwd_shadow_memory");
|
|
171
|
+
}
|
|
172
|
+
return [...targets];
|
|
173
|
+
}
|
|
174
|
+
function commandOperation(command) {
|
|
175
|
+
if (/(?:^|[;&|]\s*|\s)(?:rm|unlink)\s/i.test(command))
|
|
176
|
+
return "delete";
|
|
177
|
+
if (/(?:>>?|\btee\b|\btouch\b|\btruncate\b|\bsed\s+-i\b|\bperl\b[^\n]*\s-i\b|\bapply_patch\b)/i.test(command)) {
|
|
178
|
+
return "write";
|
|
179
|
+
}
|
|
180
|
+
return "read";
|
|
181
|
+
}
|
|
182
|
+
function toolOperation(name) {
|
|
183
|
+
const lowered = name.toLowerCase();
|
|
184
|
+
if (["read", "readfile", "view"].includes(lowered))
|
|
185
|
+
return "read";
|
|
186
|
+
if (["write", "writefile", "edit", "multiedit", "apply_patch", "patch"].includes(lowered))
|
|
187
|
+
return "write";
|
|
188
|
+
if (["delete", "remove", "unlink"].includes(lowered))
|
|
189
|
+
return "delete";
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
function toolPath(input) {
|
|
193
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
194
|
+
return null;
|
|
195
|
+
const record = input;
|
|
196
|
+
for (const key of ["file_path", "path", "filePath"]) {
|
|
197
|
+
if (typeof record[key] === "string")
|
|
198
|
+
return record[key];
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
function recordObservation(observations, counts, observation) {
|
|
203
|
+
observations.push(observation);
|
|
204
|
+
const key = `${observation.target}:${observation.operation}:${observation.outcome}`;
|
|
205
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
206
|
+
}
|
|
207
|
+
export function createLocalMemoryAccessObserver(options) {
|
|
208
|
+
const paths = targetPaths(options);
|
|
209
|
+
const pending = new Map();
|
|
210
|
+
const counts = new Map();
|
|
211
|
+
return {
|
|
212
|
+
observe(event) {
|
|
213
|
+
const observations = [];
|
|
214
|
+
if (!event || typeof event !== "object")
|
|
215
|
+
return observations;
|
|
216
|
+
const record = event;
|
|
217
|
+
const message = record.message && typeof record.message === "object"
|
|
218
|
+
? record.message
|
|
219
|
+
: undefined;
|
|
220
|
+
const blocks = Array.isArray(message?.content) ? message.content : [];
|
|
221
|
+
if (record.type === "assistant") {
|
|
222
|
+
for (const block of blocks) {
|
|
223
|
+
if (block.type !== "tool_use" || typeof block.id !== "string" || typeof block.name !== "string")
|
|
224
|
+
continue;
|
|
225
|
+
const operation = toolOperation(block.name);
|
|
226
|
+
const path = toolPath(block.input);
|
|
227
|
+
const target = operation === null || path === null ? null : classifyExplicitPath(path, paths);
|
|
228
|
+
if (operation !== null && target !== null)
|
|
229
|
+
pending.set(block.id, { operation, target });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (record.type === "user") {
|
|
233
|
+
for (const block of blocks) {
|
|
234
|
+
if (block.type !== "tool_result" || typeof block.tool_use_id !== "string")
|
|
235
|
+
continue;
|
|
236
|
+
const access = pending.get(block.tool_use_id);
|
|
237
|
+
pending.delete(block.tool_use_id);
|
|
238
|
+
if (access === undefined)
|
|
239
|
+
continue;
|
|
240
|
+
recordObservation(observations, counts, {
|
|
241
|
+
...access,
|
|
242
|
+
outcome: block.is_error === true ? "failed" : "succeeded",
|
|
243
|
+
evidence: "structured_tool",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (record.type === "kimi.acp.tool_call" && typeof record.id === "string") {
|
|
248
|
+
const operation = typeof record.kind === "string"
|
|
249
|
+
? toolOperation(record.kind)
|
|
250
|
+
: null;
|
|
251
|
+
const fallbackOperation = operation ?? (typeof record.title === "string"
|
|
252
|
+
? toolOperation(record.title)
|
|
253
|
+
: null);
|
|
254
|
+
const path = toolPath(record.input);
|
|
255
|
+
const target = fallbackOperation === null || path === null
|
|
256
|
+
? null
|
|
257
|
+
: classifyExplicitPath(path, paths);
|
|
258
|
+
if (fallbackOperation !== null && target !== null) {
|
|
259
|
+
pending.set(record.id, { operation: fallbackOperation, target });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (record.type === "kimi.acp.tool_result" && typeof record.id === "string") {
|
|
263
|
+
if (record.status !== "completed" && record.status !== "failed")
|
|
264
|
+
return observations;
|
|
265
|
+
const access = pending.get(record.id);
|
|
266
|
+
pending.delete(record.id);
|
|
267
|
+
if (access !== undefined) {
|
|
268
|
+
recordObservation(observations, counts, {
|
|
269
|
+
...access,
|
|
270
|
+
outcome: record.status === "failed" ? "failed" : "succeeded",
|
|
271
|
+
evidence: "structured_tool",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const item = record.type === "item.completed" && record.item && typeof record.item === "object"
|
|
276
|
+
? record.item
|
|
277
|
+
: null;
|
|
278
|
+
if (item?.type === "command_execution" && typeof item.command === "string") {
|
|
279
|
+
const operation = commandOperation(item.command);
|
|
280
|
+
const outcome = typeof item.exit_code === "number"
|
|
281
|
+
? item.exit_code === 0 ? "succeeded" : "failed"
|
|
282
|
+
: item.status === "completed"
|
|
283
|
+
? "succeeded"
|
|
284
|
+
: item.status === "failed" || item.status === "declined"
|
|
285
|
+
? "failed"
|
|
286
|
+
: "unknown";
|
|
287
|
+
for (const target of commandTargets(item.command, paths)) {
|
|
288
|
+
recordObservation(observations, counts, { target, operation, outcome, evidence: "shell_command" });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const fileChange = record.type === "diagnostic.file_change" && Array.isArray(record.changes)
|
|
292
|
+
? record
|
|
293
|
+
: item?.type === "file_change" && Array.isArray(item.changes)
|
|
294
|
+
? item
|
|
295
|
+
: null;
|
|
296
|
+
if (fileChange !== null && Array.isArray(fileChange.changes)) {
|
|
297
|
+
const outcome = fileChange.status === "completed"
|
|
298
|
+
? "succeeded"
|
|
299
|
+
: fileChange.status === "failed" || fileChange.status === "declined"
|
|
300
|
+
? "failed"
|
|
301
|
+
: fileChange === item ? "succeeded" : "unknown";
|
|
302
|
+
for (const change of fileChange.changes) {
|
|
303
|
+
if (!change || typeof change !== "object")
|
|
304
|
+
continue;
|
|
305
|
+
const detail = change;
|
|
306
|
+
if (typeof detail.path !== "string")
|
|
307
|
+
continue;
|
|
308
|
+
const target = classifyExplicitPath(detail.path, paths);
|
|
309
|
+
if (target === null)
|
|
310
|
+
continue;
|
|
311
|
+
recordObservation(observations, counts, {
|
|
312
|
+
target,
|
|
313
|
+
operation: detail.kind === "delete" ? "delete" : "write",
|
|
314
|
+
outcome,
|
|
315
|
+
evidence: "file_change",
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return observations;
|
|
320
|
+
},
|
|
321
|
+
summary() {
|
|
322
|
+
const count = (target, operation, outcome) => counts.get(`${target}:${operation}:${outcome}`) ?? 0;
|
|
323
|
+
return {
|
|
324
|
+
memory_read_succeeded: count("shared_memory", "read", "succeeded"),
|
|
325
|
+
memory_read_failed: count("shared_memory", "read", "failed"),
|
|
326
|
+
lessons_read_succeeded: count("shared_lessons", "read", "succeeded"),
|
|
327
|
+
lessons_read_failed: count("shared_lessons", "read", "failed"),
|
|
328
|
+
shared_memory_write_succeeded: count("shared_memory", "write", "succeeded"),
|
|
329
|
+
shared_lessons_write_succeeded: count("shared_lessons", "write", "succeeded"),
|
|
330
|
+
cwd_shadow_write_succeeded: count("cwd_shadow_memory", "write", "succeeded")
|
|
331
|
+
+ count("cwd_shadow_lessons", "write", "succeeded"),
|
|
332
|
+
pending_access_count: pending.size,
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|