@nowcrew/daemon 0.6.87-beta.4 → 0.6.87-beta.5
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 +22 -0
- package/dist/execution-runner.js +6 -3
- package/dist/local-execution-prompt.js +19 -25
- package/dist/local-executor.js +10 -42
- package/dist/local-memory/prompt.js +37 -0
- package/dist/local-memory/read-cli.js +232 -0
- package/dist/local-memory-diagnostics.js +79 -12
- package/dist/main.js +0 -0
- package/dist/prompt.js +24 -9
- package/dist/workspace.js +15 -0
- package/package.json +10 -9
- package/dist/project-skills/capability.js +0 -109
- package/dist/project-skills/controller-convergence.js +0 -57
- package/dist/project-skills/projection-set-switch.js +0 -419
- package/dist/project-skills/projection-state-domain.js +0 -170
- package/dist/project-skills/projection-state-store.js +0 -842
- package/dist/project-skills/projection-state-transaction.js +0 -322
- package/dist/project-skills/projection-state.js +0 -3
- package/dist/project-skills/runtime-root-bootstrap.js +0 -47
- package/dist/project-skills/runtime-root-domain.js +0 -268
- package/dist/project-skills/runtime-root-gc.js +0 -293
- package/dist/project-skills/runtime-root-lease-artifact.js +0 -46
- package/dist/project-skills/runtime-root-leases.js +0 -487
- package/dist/project-skills/runtime-root-source-identity.js +0 -60
- package/dist/project-skills/runtime-root-startup.js +0 -49
- package/dist/project-skills/runtime-root-state-artifact-domain.js +0 -143
- package/dist/project-skills/runtime-root-state-index.js +0 -356
- package/dist/project-skills/runtime-root-store.js +0 -722
- package/dist/project-skills/serve-capability.js +0 -28
- package/dist/project-skills/serve-startup.js +0 -37
package/README.md
CHANGED
|
@@ -418,6 +418,28 @@ Local policy can reduce server-requested access and limits; it cannot grant more
|
|
|
418
418
|
Provider credentials and configured environment are prepared locally and never carried in execution
|
|
419
419
|
control frames.
|
|
420
420
|
|
|
421
|
+
## 完整本地记忆读取
|
|
422
|
+
|
|
423
|
+
运行时注入的 `MEMORY.md` 是有界预览,未命中不代表完整文件没有相关记忆。
|
|
424
|
+
普通 Agent 工作区会在现有 PATH 目录中提供只读 `crew-memory` 工具,无需 Server 凭证:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
crew-memory search --query '提交前检查'
|
|
428
|
+
crew-memory search --query '提交前检查' --after-line 120
|
|
429
|
+
crew-memory read --offset 2400 --limit 2000
|
|
430
|
+
crew-memory read --file notes/testing.md --offset 0 --limit 2000
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
命令默认返回 JSON。搜索遍历完整指定文件,返回 `line`、`offset`、命中摘要和
|
|
434
|
+
`nextAfterLine`;读取按 Unicode 字符偏移分页,使用 `nextOffset` 继续直到 `eof=true`。
|
|
435
|
+
后续页可传 `--if-hash <上一页的sha256>`,文件变化时明确停止,不混读不同版本。
|
|
436
|
+
每页 JSON 有 12 KiB 上限,输入文件超过 32 MiB 时明确报错,不将未完成搜索伪装为零命中。
|
|
437
|
+
|
|
438
|
+
工具仅支持 `MEMORY.md` 和 `notes/` 下明确指定的 Markdown 文件,不遍历 pending,
|
|
439
|
+
不写入或迁移记忆。搜索结果是原始资料定位,读取明细前仍需确认当前有效索引和正文片段。
|
|
440
|
+
这些修改不改变现有 V2 写入上限、候选状态或归档策略,也不提供自动语义召回保证。
|
|
441
|
+
工具随 daemon 包发布;旧 daemon 没有该入口时,Agent 使用原生文件搜索与分段读取回退。
|
|
442
|
+
|
|
421
443
|
## Optional TencentDB Agent Memory Bridge
|
|
422
444
|
|
|
423
445
|
Execution protocol v1 can opt NowWork Agents into the private TencentDB Agent Memory Panel.
|
package/dist/execution-runner.js
CHANGED
|
@@ -834,9 +834,12 @@ export async function runExecution(config, input, dependencies) {
|
|
|
834
834
|
...(selfTraining ? { isolateLocalMemory: true } : {}),
|
|
835
835
|
collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
|
|
836
836
|
});
|
|
837
|
-
const boundedSystemPrompt = (context) =>
|
|
838
|
-
|
|
839
|
-
|
|
837
|
+
const boundedSystemPrompt = (context) => {
|
|
838
|
+
const base = typeof systemPromptWithLocalFacts === "string"
|
|
839
|
+
? systemPromptWithLocalFacts
|
|
840
|
+
: systemPromptWithLocalFacts(context);
|
|
841
|
+
return appendAgentMemoryContext(base, recalledMemory, Math.max(Buffer.byteLength(base, "utf8"), baseSystemPromptBudget - (context.reservedPromptBytes ?? 0)));
|
|
842
|
+
};
|
|
840
843
|
const localInput = {
|
|
841
844
|
executionId: spec.executionId,
|
|
842
845
|
handle: spec.agent.handle,
|
|
@@ -34,45 +34,39 @@ export function truncateUtf8(value, maxBytes) {
|
|
|
34
34
|
}
|
|
35
35
|
return value.slice(0, end);
|
|
36
36
|
}
|
|
37
|
-
function takeCompletePromptSections(sections, maxBytes) {
|
|
38
|
-
return sections.reduce((state, section) => {
|
|
39
|
-
if (!state.full)
|
|
40
|
-
return state;
|
|
41
|
-
const sectionBytes = Buffer.byteLength(section, "utf8");
|
|
42
|
-
if (state.bytes + sectionBytes > maxBytes)
|
|
43
|
-
return { ...state, full: false };
|
|
44
|
-
return { value: `${state.value}${section}`, bytes: state.bytes + sectionBytes, full: true };
|
|
45
|
-
}, { value: "", bytes: 0, full: true }).value;
|
|
46
|
-
}
|
|
47
37
|
/** v1 server instructions opt in to daemon-owned paths and bounded local workspace context. */
|
|
48
38
|
export function withLocalExecutionFacts(serverPrompt, maxBytes, options = {}) {
|
|
49
|
-
return ({ workspace, resuming, projectContext, trainingProjectContext }) => {
|
|
50
|
-
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory);
|
|
39
|
+
return ({ workspace, resuming, projectContext, trainingProjectContext, reservedPromptBytes = 0 }) => {
|
|
51
40
|
const workLog = resuming ? "" : capWorkLogForInject(workspace.workLog);
|
|
52
41
|
const collaborationPrompt = options.collaborationTools === true ? buildCollaborationToolsPrompt() : "";
|
|
53
42
|
const mandatoryWorkspacePrompt = `${buildRuntimeWorkspacePrompt(projectContext)}${buildTrainingGitProjectPrompt(trainingProjectContext)}`;
|
|
54
43
|
const crewHome = options.isolateLocalMemory ? workspace.runDir : workspace.dir;
|
|
55
|
-
const
|
|
44
|
+
const localFacts = `\n\n## Local execution facts
|
|
56
45
|
- $CREW_HOME: ${crewHome}
|
|
57
|
-
- $CREW_TASK_LOG: ${workspace.workLogPath}
|
|
58
|
-
...(memory ? [`\n\n## Injected MEMORY.md (bounded local context)\n${memory}`] : []),
|
|
59
|
-
...(workLog ? [`\n\n## Injected work-log (bounded local context)\n${workLog}`] : []),
|
|
60
|
-
];
|
|
46
|
+
- $CREW_TASK_LOG: ${workspace.workLogPath}`;
|
|
61
47
|
const serverPromptBytes = Buffer.byteLength(serverPrompt, "utf8");
|
|
62
48
|
const collaborationBytes = Buffer.byteLength(collaborationPrompt, "utf8");
|
|
63
49
|
const mandatoryBytes = Buffer.byteLength(mandatoryWorkspacePrompt, "utf8");
|
|
64
50
|
const requiredBytes = serverPromptBytes + collaborationBytes + mandatoryBytes;
|
|
65
|
-
//
|
|
51
|
+
// 先保留全部必需内容,再按实际剩余字节一次性构造预览。
|
|
66
52
|
if (collaborationPrompt !== "" && requiredBytes > maxBytes) {
|
|
67
53
|
throw new Error("collaboration_tools_prompt_budget_exceeded");
|
|
68
54
|
}
|
|
69
|
-
if (mandatoryWorkspacePrompt !== "") {
|
|
70
|
-
|
|
71
|
-
throw new Error("project_workspace_prompt_budget_exceeded");
|
|
72
|
-
const optionalFacts = takeCompletePromptSections(localFactSections, maxBytes - requiredBytes);
|
|
73
|
-
return `${serverPrompt}${collaborationPrompt}${optionalFacts}${mandatoryWorkspacePrompt}`;
|
|
55
|
+
if (mandatoryWorkspacePrompt !== "" && requiredBytes > maxBytes) {
|
|
56
|
+
throw new Error("project_workspace_prompt_budget_exceeded");
|
|
74
57
|
}
|
|
75
|
-
const remaining = maxBytes - requiredBytes;
|
|
76
|
-
|
|
58
|
+
const remaining = maxBytes - requiredBytes - Math.max(0, reservedPromptBytes);
|
|
59
|
+
const factsBytes = Buffer.byteLength(localFacts, "utf8");
|
|
60
|
+
if (factsBytes > remaining)
|
|
61
|
+
return `${serverPrompt}${collaborationPrompt}${mandatoryWorkspacePrompt}`;
|
|
62
|
+
const memoryHeading = "\n\n## Injected MEMORY.md (bounded local context)\n";
|
|
63
|
+
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory, undefined, remaining - factsBytes - Buffer.byteLength(memoryHeading, "utf8"));
|
|
64
|
+
const memorySection = memory === "" ? "" : memoryHeading + memory;
|
|
65
|
+
const workLogSection = workLog === "" ? "" : `\n\n## Injected work-log (bounded local context)\n${workLog}`;
|
|
66
|
+
const workLogBudget = remaining - factsBytes - Buffer.byteLength(memorySection, "utf8");
|
|
67
|
+
const boundedWorkLog = mandatoryWorkspacePrompt === ""
|
|
68
|
+
? truncateUtf8(workLogSection, workLogBudget)
|
|
69
|
+
: Buffer.byteLength(workLogSection, "utf8") <= workLogBudget ? workLogSection : "";
|
|
70
|
+
return `${serverPrompt}${collaborationPrompt}${localFacts}${memorySection}${boundedWorkLog}${mandatoryWorkspacePrompt}`;
|
|
77
71
|
};
|
|
78
72
|
}
|
package/dist/local-executor.js
CHANGED
|
@@ -36,8 +36,8 @@ import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
|
36
36
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
37
37
|
import { machinePackageManagerEnv, packageManagerEnvFacts } from "./package-manager-env.js";
|
|
38
38
|
import { sanitizeEnvVars } from "./runtime-env.js";
|
|
39
|
-
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath,
|
|
40
|
-
import {
|
|
39
|
+
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
|
|
40
|
+
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
41
41
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
42
42
|
constructor(runtime, timeoutMs) {
|
|
43
43
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -386,26 +386,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
386
386
|
}
|
|
387
387
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
388
388
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
389
|
-
let pendingMemoryContext = "";
|
|
390
|
-
if (input.localMemoryV2Enabled === true) {
|
|
391
|
-
try {
|
|
392
|
-
const pendingCandidates = await searchPendingMemoryCandidates({
|
|
393
|
-
agentDir: executionWorkspace.dir,
|
|
394
|
-
query: wakePrompt,
|
|
395
|
-
limit: 5,
|
|
396
|
-
});
|
|
397
|
-
pendingMemoryContext = renderPendingMemoryContext(pendingCandidates);
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
dslog("local_memory.pending_recall_failed", "本地归档记忆召回失败", {
|
|
401
|
-
level: "WARN",
|
|
402
|
-
execution_id: input.executionId,
|
|
403
|
-
agent_handle: input.handle,
|
|
404
|
-
error_name: error instanceof Error ? error.name : "unknown",
|
|
405
|
-
error_code: error?.code,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
389
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
410
390
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
411
391
|
&& isLocalMemoryV2Eligible({
|
|
@@ -417,26 +397,14 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
417
397
|
})) {
|
|
418
398
|
localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
|
|
419
399
|
}
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
<= input.maxSystemPromptBytes;
|
|
429
|
-
// The structured prune contract is mandatory for V2. Drop optional archived
|
|
430
|
-
// recall before dropping the daemon result file when the prompt budget is tight.
|
|
431
|
-
if (!fitsPromptBudget(baseSystemPrompt) && pendingMemoryContext !== "") {
|
|
432
|
-
baseSystemPrompt = promptWithoutPending;
|
|
433
|
-
}
|
|
434
|
-
const structuredPromptFits = fitsPromptBudget(baseSystemPrompt);
|
|
435
|
-
if (!structuredPromptFits)
|
|
436
|
-
localMemoryResultPath = null;
|
|
437
|
-
const systemPrompt = localMemoryResultPath === null
|
|
438
|
-
? baseSystemPrompt
|
|
439
|
-
: `${baseSystemPrompt}${structuredMemoryInstructions ?? ""}`;
|
|
400
|
+
const memoryPrompt = await prepareLocalMemoryPrompt({
|
|
401
|
+
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
402
|
+
enabled: input.localMemoryV2Enabled === true, query: wakePrompt,
|
|
403
|
+
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
404
|
+
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
405
|
+
});
|
|
406
|
+
localMemoryResultPath = memoryPrompt.resultPath;
|
|
407
|
+
const systemPrompt = memoryPrompt.systemPrompt;
|
|
440
408
|
if (!isolatedMaintenance)
|
|
441
409
|
try {
|
|
442
410
|
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { dslog } from "../slog.js";
|
|
2
|
+
import { renderPendingMemoryContext, searchPendingMemoryCandidates } from "./pending.js";
|
|
3
|
+
import { renderLocalMemoryResultInstructions } from "./runtime.js";
|
|
4
|
+
export async function prepareLocalMemoryPrompt(input) {
|
|
5
|
+
let pending = "";
|
|
6
|
+
if (input.enabled) {
|
|
7
|
+
try {
|
|
8
|
+
pending = renderPendingMemoryContext(await searchPendingMemoryCandidates({
|
|
9
|
+
agentDir: input.agentDir, query: input.query, limit: 5,
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
dslog("local_memory.pending_recall_failed", "本地归档记忆召回失败", {
|
|
14
|
+
level: "WARN",
|
|
15
|
+
execution_id: input.executionId,
|
|
16
|
+
agent_handle: input.agentHandle,
|
|
17
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
18
|
+
error_code: error?.code,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const original = input.renderBase(0);
|
|
23
|
+
const structured = input.resultPath === null ? "" : renderLocalMemoryResultInstructions(input.resultPath);
|
|
24
|
+
const fits = (prompt) => input.maxBytes === undefined || Buffer.byteLength(prompt, "utf8") <= input.maxBytes;
|
|
25
|
+
const render = (suffix) => (suffix === "" ? original : input.renderBase(Buffer.byteLength(suffix, "utf8"))) + suffix;
|
|
26
|
+
// 后缀先占预算,可选预览重建;结构化契约优先于 pending,不能被预览挤掉。
|
|
27
|
+
const complete = render(pending + structured);
|
|
28
|
+
if (fits(complete))
|
|
29
|
+
return { systemPrompt: complete, resultPath: input.resultPath };
|
|
30
|
+
if (pending !== "") {
|
|
31
|
+
const withoutPending = render(structured);
|
|
32
|
+
if (fits(withoutPending))
|
|
33
|
+
return { systemPrompt: withoutPending, resultPath: input.resultPath };
|
|
34
|
+
}
|
|
35
|
+
// 强制基础指令与契约本身仍无法共存时,保留既有非结构化兼容回退,不裁基础指令。
|
|
36
|
+
return { systemPrompt: original, resultPath: null };
|
|
37
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open, realpath } from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { parseArgs, TextDecoder } from "node:util";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
const OUTPUT_BYTES = 12 * 1024;
|
|
9
|
+
const FILE_BYTES = 32 * 1024 * 1024;
|
|
10
|
+
const FileSchema = z.string().min(1).refine((file) => {
|
|
11
|
+
if (isAbsolute(file) || /[\\\u0000-\u001f\u007f]/u.test(file))
|
|
12
|
+
return false;
|
|
13
|
+
if (Buffer.byteLength(file, "utf8") > 1024)
|
|
14
|
+
return false;
|
|
15
|
+
const parts = file.split("/");
|
|
16
|
+
if (parts.some((part) => part === "" || part === "." || part === ".."))
|
|
17
|
+
return false;
|
|
18
|
+
return file === "MEMORY.md" || (parts[0] === "notes" && parts.length > 1 && file.endsWith(".md"));
|
|
19
|
+
}, "memory_path_invalid");
|
|
20
|
+
const Common = {
|
|
21
|
+
file: FileSchema.default("MEMORY.md"),
|
|
22
|
+
ifHash: z.string().regex(/^[a-f0-9]{64}$/u).optional(),
|
|
23
|
+
};
|
|
24
|
+
const CommandSchema = z.discriminatedUnion("command", [
|
|
25
|
+
z.object({
|
|
26
|
+
command: z.literal("search"), ...Common,
|
|
27
|
+
query: z.string().trim().min(1).max(256),
|
|
28
|
+
limit: z.coerce.number().int().min(1).max(40).default(10),
|
|
29
|
+
afterLine: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
30
|
+
}).strict(),
|
|
31
|
+
z.object({
|
|
32
|
+
command: z.literal("read"), ...Common,
|
|
33
|
+
offset: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
34
|
+
limit: z.coerce.number().int().min(1).max(4000).default(2000),
|
|
35
|
+
}).strict(),
|
|
36
|
+
]);
|
|
37
|
+
const USAGE = `只读记忆工具,默认输出 JSON,不修改任何记忆文件。
|
|
38
|
+
crew-memory search --query <关键词> [--file MEMORY.md|notes/文件.md] [--limit 10] [--after-line 0]
|
|
39
|
+
crew-memory read [--file MEMORY.md|notes/文件.md] [--offset 0] [--limit 2000]
|
|
40
|
+
搜索关键词按空白分隔,同一行须全部命中;返回完整文件中的行号和字符偏移。
|
|
41
|
+
读取的 offset/limit 按 Unicode 字符计数;用 nextOffset 继续,eof=true 表示结束。
|
|
42
|
+
后续页可传 --if-hash <前一页的 sha256>,文件变化时停止,避免混读不同版本。
|
|
43
|
+
搜索结果是原始资料定位,不代表候选已生效;按当前有效索引读取明细,不扫描 pending。`;
|
|
44
|
+
function inside(root, path) {
|
|
45
|
+
const value = relative(root, path);
|
|
46
|
+
return value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
|
|
47
|
+
}
|
|
48
|
+
async function readSource(home, file) {
|
|
49
|
+
if (!home)
|
|
50
|
+
throw new Error("memory_home_missing");
|
|
51
|
+
const root = await realpath(home);
|
|
52
|
+
const path = await realpath(resolve(root, file));
|
|
53
|
+
if (!inside(root, path) || !FileSchema.safeParse(relative(root, path).split(sep).join("/")).success) {
|
|
54
|
+
throw new Error("memory_path_invalid");
|
|
55
|
+
}
|
|
56
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
57
|
+
try {
|
|
58
|
+
const stat = await handle.stat();
|
|
59
|
+
if (!stat.isFile())
|
|
60
|
+
throw new Error("memory_file_not_regular");
|
|
61
|
+
if (stat.size > FILE_BYTES)
|
|
62
|
+
throw new Error("memory_file_too_large");
|
|
63
|
+
// 允许读完整来源,但绝不能在文件并发增长时无界分配内存。
|
|
64
|
+
const chunks = [];
|
|
65
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
66
|
+
let bytes = 0;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, FILE_BYTES + 1 - bytes), null);
|
|
69
|
+
if (bytesRead === 0)
|
|
70
|
+
break;
|
|
71
|
+
bytes += bytesRead;
|
|
72
|
+
if (bytes > FILE_BYTES)
|
|
73
|
+
throw new Error("memory_file_too_large");
|
|
74
|
+
chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));
|
|
75
|
+
}
|
|
76
|
+
const buffer = Buffer.concat(chunks);
|
|
77
|
+
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer);
|
|
78
|
+
return { text, sha256: createHash("sha256").update(buffer).digest("hex") };
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await handle.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function characterCount(text) {
|
|
85
|
+
let count = 0;
|
|
86
|
+
for (const _character of text)
|
|
87
|
+
count += 1;
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
function excerpt(text, limit) {
|
|
91
|
+
let value = "";
|
|
92
|
+
let count = 0;
|
|
93
|
+
for (const character of text) {
|
|
94
|
+
if (count++ >= limit)
|
|
95
|
+
return `${value}...`;
|
|
96
|
+
value += character;
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function originalLowercaseIndex(text, index) {
|
|
101
|
+
let originalIndex = 0;
|
|
102
|
+
let lowerIndex = 0;
|
|
103
|
+
for (const character of text) {
|
|
104
|
+
// 逐字符只累计小写长度;匹配仍用整行小写,保留长度不变的词尾 sigma 上下文规则。
|
|
105
|
+
lowerIndex += character.toLowerCase().length;
|
|
106
|
+
if (index < lowerIndex)
|
|
107
|
+
return originalIndex;
|
|
108
|
+
originalIndex += character.length;
|
|
109
|
+
}
|
|
110
|
+
return text.length;
|
|
111
|
+
}
|
|
112
|
+
function searchSource(text, sha256, command) {
|
|
113
|
+
const terms = command.query.toLowerCase().split(/\s+/u);
|
|
114
|
+
const matches = [];
|
|
115
|
+
let line = 0;
|
|
116
|
+
let offset = 0;
|
|
117
|
+
let heading = "";
|
|
118
|
+
let matchCount = 0;
|
|
119
|
+
let hasMore = false;
|
|
120
|
+
let remaining = OUTPUT_BYTES - Buffer.byteLength(JSON.stringify({ file: command.file, sha256 })) - 512;
|
|
121
|
+
for (const match of text.matchAll(/[^\n]*(?:\n|$)/gu)) {
|
|
122
|
+
if (match[0] === "")
|
|
123
|
+
break;
|
|
124
|
+
line += 1;
|
|
125
|
+
const raw = match[0];
|
|
126
|
+
const content = raw.replace(/\r?\n$/u, "");
|
|
127
|
+
const title = /^#{1,6}\s+(.+?)\s*$/u.exec(content)?.[1];
|
|
128
|
+
if (title !== undefined)
|
|
129
|
+
heading = excerpt(title, 96);
|
|
130
|
+
const lower = content.toLowerCase();
|
|
131
|
+
if (terms.every((term) => lower.includes(term))) {
|
|
132
|
+
matchCount += 1;
|
|
133
|
+
if (line > command.afterLine) {
|
|
134
|
+
let start = Math.max(0, originalLowercaseIndex(content, lower.indexOf(terms[0])) - 60);
|
|
135
|
+
if (start > 0 && /[\uDC00-\uDFFF]/u.test(content[start]))
|
|
136
|
+
start -= 1;
|
|
137
|
+
const item = { line, offset, heading, text: `${start > 0 ? "..." : ""}${excerpt(content.slice(start), 240)}` };
|
|
138
|
+
const bytes = Buffer.byteLength(JSON.stringify(item), "utf8") + 1;
|
|
139
|
+
if (hasMore || matches.length >= command.limit || bytes > remaining) {
|
|
140
|
+
hasMore = true;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
matches.push(item);
|
|
144
|
+
remaining -= bytes;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
offset += characterCount(raw);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
file: command.file, sha256, matches, matchCount, hasMore,
|
|
152
|
+
nextAfterLine: hasMore ? matches.at(-1)?.line ?? null : null,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function readPage(text, sha256, command) {
|
|
156
|
+
const metadata = { file: command.file, sha256, offset: command.offset, nextOffset: Number.MAX_SAFE_INTEGER, eof: false, content: "" };
|
|
157
|
+
let remaining = OUTPUT_BYTES - Buffer.byteLength(JSON.stringify(metadata), "utf8");
|
|
158
|
+
let position = 0;
|
|
159
|
+
let offset = 0;
|
|
160
|
+
while (position < text.length && offset < command.offset) {
|
|
161
|
+
position += text.codePointAt(position) > 0xffff ? 2 : 1;
|
|
162
|
+
offset += 1;
|
|
163
|
+
}
|
|
164
|
+
let content = "";
|
|
165
|
+
let count = 0;
|
|
166
|
+
while (position < text.length && count < command.limit) {
|
|
167
|
+
const character = String.fromCodePoint(text.codePointAt(position));
|
|
168
|
+
const bytes = Buffer.byteLength(JSON.stringify(character), "utf8") - 2;
|
|
169
|
+
if (bytes > remaining)
|
|
170
|
+
break;
|
|
171
|
+
remaining -= bytes;
|
|
172
|
+
content += character;
|
|
173
|
+
position += character.length;
|
|
174
|
+
offset += 1;
|
|
175
|
+
count += 1;
|
|
176
|
+
}
|
|
177
|
+
const eof = position === text.length;
|
|
178
|
+
return { ...metadata, content, eof, nextOffset: eof ? null : offset };
|
|
179
|
+
}
|
|
180
|
+
export async function runMemoryReadCommand(argv, io) {
|
|
181
|
+
let command;
|
|
182
|
+
try {
|
|
183
|
+
const { values, positionals } = parseArgs({
|
|
184
|
+
args: [...argv], allowPositionals: true,
|
|
185
|
+
options: {
|
|
186
|
+
query: { type: "string", short: "q" }, file: { type: "string" },
|
|
187
|
+
limit: { type: "string" }, offset: { type: "string" },
|
|
188
|
+
"after-line": { type: "string" }, "if-hash": { type: "string" },
|
|
189
|
+
json: { type: "boolean" }, help: { type: "boolean", short: "h" },
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
if (values.help) {
|
|
193
|
+
io.out(USAGE);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
if (positionals.length !== 1)
|
|
197
|
+
throw new Error("memory_command_invalid");
|
|
198
|
+
const { help: _help, json: _json, "after-line": afterLine, "if-hash": ifHash, ...options } = values;
|
|
199
|
+
command = CommandSchema.parse({
|
|
200
|
+
command: positionals[0], ...options,
|
|
201
|
+
...(afterLine === undefined ? {} : { afterLine }),
|
|
202
|
+
...(ifHash === undefined ? {} : { ifHash }),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
io.err(`memory_arguments_invalid: 参数不合法。\n${USAGE}`);
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const source = await readSource(io.env.CREW_HOME, command.file);
|
|
211
|
+
if (command.ifHash !== undefined && command.ifHash !== source.sha256)
|
|
212
|
+
throw new Error("memory_file_changed");
|
|
213
|
+
const result = command.command === "search"
|
|
214
|
+
? searchSource(source.text, source.sha256, command)
|
|
215
|
+
: readPage(source.text, source.sha256, command);
|
|
216
|
+
io.out(JSON.stringify(result));
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
const code = error.code;
|
|
221
|
+
const known = error instanceof Error && /^memory_[a-z_]+$/u.test(error.message) ? error.message : code ?? "memory_read_failed";
|
|
222
|
+
io.err(JSON.stringify({ error: known, message: "记忆读取未完成;检查路径、文件版本或大小,不要将此错误当作没有记忆。" }));
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
227
|
+
void runMemoryReadCommand(process.argv.slice(2), {
|
|
228
|
+
env: process.env,
|
|
229
|
+
out: (text) => process.stdout.write(`${text}\n`),
|
|
230
|
+
err: (text) => process.stderr.write(`${text}\n`),
|
|
231
|
+
}).then((code) => { process.exitCode = code; });
|
|
232
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
4
5
|
import { capMemoryForInject, MEMORY_INJECT_CAP } from "./prompt.js";
|
|
5
6
|
const INJECTION_MARKERS = [
|
|
6
7
|
"## Injected MEMORY.md (bounded local context)\n",
|
|
@@ -172,10 +173,58 @@ function commandOperation(command) {
|
|
|
172
173
|
? "read"
|
|
173
174
|
: null;
|
|
174
175
|
}
|
|
176
|
+
function crewMemoryReadAccess(command, paths) {
|
|
177
|
+
if (!/^\s*crew-memory(?:\s|$)/u.test(command))
|
|
178
|
+
return null;
|
|
179
|
+
const args = [];
|
|
180
|
+
let remaining = command.trim();
|
|
181
|
+
// Only complete literal arguments are supported; shell expansion and concatenated quoting are not inferred.
|
|
182
|
+
while (remaining !== "") {
|
|
183
|
+
const match = /^(?:"([^"\\$`]*)"|'([^']*)'|([^\s"'\\$`<>;|&()\[\]{}*?!#]+))(?=\s|$)\s*/u.exec(remaining);
|
|
184
|
+
if (match === null)
|
|
185
|
+
return null;
|
|
186
|
+
args.push(match[1] ?? match[2] ?? match[3]);
|
|
187
|
+
remaining = remaining.slice(match[0].length);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const { values, positionals } = parseArgs({
|
|
191
|
+
args: args.slice(1),
|
|
192
|
+
allowPositionals: true,
|
|
193
|
+
options: {
|
|
194
|
+
file: { type: "string" }, query: { type: "string", short: "q" },
|
|
195
|
+
limit: { type: "string" }, offset: { type: "string" },
|
|
196
|
+
"after-line": { type: "string" }, "if-hash": { type: "string" },
|
|
197
|
+
json: { type: "boolean" }, help: { type: "boolean", short: "h" },
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
if (values.help || positionals.length !== 1)
|
|
201
|
+
return null;
|
|
202
|
+
const operation = positionals[0];
|
|
203
|
+
if (operation === "search") {
|
|
204
|
+
if (!values.query?.trim() || values.offset !== undefined)
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
else if (operation !== "read" || values.query !== undefined || values["after-line"] !== undefined) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const file = values.file ?? "MEMORY.md";
|
|
211
|
+
if (file !== "MEMORY.md" && !/^notes\/[^/\\\u0000-\u001f\u007f]+\.md$/u.test(file))
|
|
212
|
+
return null;
|
|
213
|
+
const classified = classifyExplicitPath(resolve(paths.homeDir, file), paths);
|
|
214
|
+
return classified === null ? null : { ...classified, operation: "read" };
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
175
220
|
function commandAccesses(command, paths) {
|
|
176
221
|
if (/[;|&\n]/.test(command))
|
|
177
222
|
return [];
|
|
178
223
|
const accesses = new Map();
|
|
224
|
+
const memoryRead = crewMemoryReadAccess(command, paths);
|
|
225
|
+
if (memoryRead !== null) {
|
|
226
|
+
accesses.set(`${memoryRead.target}:${memoryRead.relative_path ?? ""}:read`, memoryRead);
|
|
227
|
+
}
|
|
179
228
|
const expanded = command
|
|
180
229
|
.replaceAll("${CREW_HOME}", paths.homeDir)
|
|
181
230
|
.replaceAll("$CREW_HOME", paths.homeDir)
|
|
@@ -243,26 +292,41 @@ export function createLocalMemoryAccessObserver(options) {
|
|
|
243
292
|
for (const block of blocks) {
|
|
244
293
|
if (block.type !== "tool_use" || typeof block.id !== "string" || typeof block.name !== "string")
|
|
245
294
|
continue;
|
|
295
|
+
if (block.name === "Bash") {
|
|
296
|
+
const input = block.input && typeof block.input === "object" && !Array.isArray(block.input)
|
|
297
|
+
? block.input
|
|
298
|
+
: null;
|
|
299
|
+
const accesses = typeof input?.command === "string" ? commandAccesses(input.command, paths) : [];
|
|
300
|
+
if (accesses.length > 0)
|
|
301
|
+
pending.set(block.id, { accesses, evidence: "shell_command" });
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
246
304
|
const operation = toolOperation(block.name);
|
|
247
305
|
const path = toolPath(block.input);
|
|
248
306
|
const classified = operation === null || path === null ? null : classifyExplicitPath(path, paths);
|
|
249
|
-
if (operation !== null && classified !== null)
|
|
250
|
-
pending.set(block.id, {
|
|
307
|
+
if (operation !== null && classified !== null) {
|
|
308
|
+
pending.set(block.id, {
|
|
309
|
+
accesses: [{ operation, ...classified }],
|
|
310
|
+
evidence: "structured_tool",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
251
313
|
}
|
|
252
314
|
}
|
|
253
315
|
if (record.type === "user") {
|
|
254
316
|
for (const block of blocks) {
|
|
255
317
|
if (block.type !== "tool_result" || typeof block.tool_use_id !== "string")
|
|
256
318
|
continue;
|
|
257
|
-
const
|
|
319
|
+
const toolAccess = pending.get(block.tool_use_id);
|
|
258
320
|
pending.delete(block.tool_use_id);
|
|
259
|
-
if (
|
|
321
|
+
if (toolAccess === undefined)
|
|
260
322
|
continue;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
323
|
+
for (const access of toolAccess.accesses) {
|
|
324
|
+
recordObservation(observations, counts, {
|
|
325
|
+
...access,
|
|
326
|
+
outcome: block.is_error === true ? "failed" : "succeeded",
|
|
327
|
+
evidence: toolAccess.evidence,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
266
330
|
}
|
|
267
331
|
}
|
|
268
332
|
if (record.type === "kimi.acp.tool_call" && typeof record.id === "string") {
|
|
@@ -277,15 +341,18 @@ export function createLocalMemoryAccessObserver(options) {
|
|
|
277
341
|
? null
|
|
278
342
|
: classifyExplicitPath(path, paths);
|
|
279
343
|
if (fallbackOperation !== null && classified !== null) {
|
|
280
|
-
pending.set(record.id, {
|
|
344
|
+
pending.set(record.id, {
|
|
345
|
+
accesses: [{ operation: fallbackOperation, ...classified }],
|
|
346
|
+
evidence: "structured_tool",
|
|
347
|
+
});
|
|
281
348
|
}
|
|
282
349
|
}
|
|
283
350
|
if (record.type === "kimi.acp.tool_result" && typeof record.id === "string") {
|
|
284
351
|
if (record.status !== "completed" && record.status !== "failed")
|
|
285
352
|
return observations;
|
|
286
|
-
const
|
|
353
|
+
const toolAccess = pending.get(record.id);
|
|
287
354
|
pending.delete(record.id);
|
|
288
|
-
|
|
355
|
+
for (const access of toolAccess?.accesses ?? []) {
|
|
289
356
|
recordObservation(observations, counts, {
|
|
290
357
|
...access,
|
|
291
358
|
outcome: record.status === "failed" ? "failed" : "succeeded",
|
package/dist/main.js
CHANGED
|
File without changes
|