@nowcrew/daemon 0.6.87 → 0.6.88-beta.2
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/cloud-report-receipt.js +83 -0
- package/dist/cloud-report-uploader.js +36 -25
- package/dist/execution-runner.js +6 -3
- package/dist/local-execution-prompt.js +19 -25
- package/dist/local-executor.js +32 -73
- package/dist/local-memory/prompt.js +37 -0
- package/dist/local-memory/read-cli.js +232 -0
- package/dist/local-memory/runtime.js +54 -6
- package/dist/local-memory-diagnostics.js +79 -12
- package/dist/prompt.js +24 -9
- package/dist/runtime-environment-telemetry.js +27 -0
- package/dist/workspace.js +15 -0
- package/package.json +1 -1
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.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
const MAX_REPORT_BYTES = 32 * 1024 * 1024;
|
|
5
|
+
export function reportDeliveryContext(executionId, runDir, facts) {
|
|
6
|
+
return {
|
|
7
|
+
version: 1,
|
|
8
|
+
provider: facts.provider,
|
|
9
|
+
executionId,
|
|
10
|
+
receiptFile: join(runDir, `.qa-report-delivery-${createHash("sha256").update(executionId).digest("hex")}.json`),
|
|
11
|
+
...(facts.ossUploadScript === undefined ? {} : { script: facts.ossUploadScript }),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function inside(root, file) {
|
|
15
|
+
const part = relative(root, file);
|
|
16
|
+
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !isAbsolute(part);
|
|
17
|
+
}
|
|
18
|
+
export async function verifiedReportReceipt(input) {
|
|
19
|
+
if (!input.receiptFile || !input.answer)
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
const stat = await lstat(input.receiptFile);
|
|
23
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024)
|
|
24
|
+
return false;
|
|
25
|
+
const receipt = JSON.parse(await readFile(input.receiptFile, "utf8"));
|
|
26
|
+
if (!receipt || typeof receipt !== "object")
|
|
27
|
+
return false;
|
|
28
|
+
const r = receipt;
|
|
29
|
+
if (r.version !== 1 || r.executionId !== input.executionId || r.evidenceComplete !== true
|
|
30
|
+
|| !Array.isArray(r.runIds) || r.runIds.length === 0 || r.runIds.length > 100
|
|
31
|
+
|| r.runIds.some(id => typeof id !== "string" || !id || id.length > 256)
|
|
32
|
+
|| typeof r.file !== "string" || !isAbsolute(r.file) || !r.file.endsWith(".html")
|
|
33
|
+
|| typeof r.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(r.sha256)
|
|
34
|
+
|| typeof r.url !== "string")
|
|
35
|
+
return false;
|
|
36
|
+
const url = new URL(r.url);
|
|
37
|
+
if (url.protocol !== "https:" || url.hostname !== "ai-claw-in.nowcoder.com"
|
|
38
|
+
|| !url.pathname.endsWith(".html") || url.username || url.password || url.search || url.hash)
|
|
39
|
+
return false;
|
|
40
|
+
const answerUrls = input.answer.match(/https:\/\/ai-claw-in\.nowcoder\.com\/[^\s<>"')\]]+/g) ?? [];
|
|
41
|
+
if (!answerUrls.includes(url.href))
|
|
42
|
+
return false;
|
|
43
|
+
const file = await realpath(r.file);
|
|
44
|
+
const roots = await Promise.all([input.root, input.qaReportRoot ?? join(input.root, "qa-report-server-v2")]
|
|
45
|
+
.map(root => realpath(resolve(root)).catch(() => null)));
|
|
46
|
+
if (!roots.some(root => root !== null && inside(root, file)))
|
|
47
|
+
return false;
|
|
48
|
+
const fileStat = await lstat(file);
|
|
49
|
+
if (!fileStat.isFile() || fileStat.size > MAX_REPORT_BYTES)
|
|
50
|
+
return false;
|
|
51
|
+
const expectedHash = createHash("sha256").update(await readFile(file)).digest("hex");
|
|
52
|
+
if (expectedHash !== r.sha256)
|
|
53
|
+
return false;
|
|
54
|
+
const response = await fetch(url.href, { redirect: "error", signal: AbortSignal.timeout(15_000) });
|
|
55
|
+
if (!response.ok || !/^text\/html(?:;|$)/i.test(response.headers.get("content-type") ?? "")
|
|
56
|
+
|| /attachment/i.test(response.headers.get("content-disposition") ?? "") || !response.body) {
|
|
57
|
+
await response.body?.cancel();
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const reader = response.body.getReader();
|
|
61
|
+
const hash = createHash("sha256");
|
|
62
|
+
let bytes = 0;
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const chunk = await reader.read();
|
|
66
|
+
if (chunk.done)
|
|
67
|
+
break;
|
|
68
|
+
bytes += chunk.value.byteLength;
|
|
69
|
+
if (bytes > MAX_REPORT_BYTES)
|
|
70
|
+
return false;
|
|
71
|
+
hash.update(chunk.value);
|
|
72
|
+
}
|
|
73
|
+
return bytes === fileStat.size && hash.digest("hex") === expectedHash;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await reader.cancel().catch(() => undefined);
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -4,6 +4,7 @@ import { extname, join, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { cloudReportUploadFacts } from "./cloud-report-upload.js";
|
|
6
6
|
import { dslog } from "./slog.js";
|
|
7
|
+
import { verifiedReportReceipt } from "./cloud-report-receipt.js";
|
|
7
8
|
const execFileP = promisify(execFile);
|
|
8
9
|
const REPORT_FILENAMES = new Set(["report.md", "result.json"]);
|
|
9
10
|
const MAX_DEPTH = 8;
|
|
@@ -13,22 +14,17 @@ const fileFingerprint = async (path) => {
|
|
|
13
14
|
const stat = await lstat(path);
|
|
14
15
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
15
16
|
};
|
|
16
|
-
function isReportFile(root, path) {
|
|
17
|
+
function isReportFile(root, path, kind) {
|
|
17
18
|
const parts = relative(root, path).split(sep).filter(Boolean);
|
|
18
19
|
const filename = parts.at(-1) ?? "";
|
|
19
|
-
if (
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
if (parts[0] !== "qa-report-server-v2")
|
|
23
|
-
return false;
|
|
24
|
-
if (parts[1] === "static" && parts.length === 3)
|
|
25
|
-
return extname(filename).toLowerCase() === ".html";
|
|
26
|
-
if (parts[1] === "qa-results" && parts.length >= 4) {
|
|
20
|
+
if (kind === "static")
|
|
21
|
+
return parts.length === 1 && extname(filename).toLowerCase() === ".html";
|
|
22
|
+
if (parts.length >= 2) {
|
|
27
23
|
return REPORT_FILENAMES.has(filename) || extname(filename).toLowerCase() === ".html";
|
|
28
24
|
}
|
|
29
25
|
return false;
|
|
30
26
|
}
|
|
31
|
-
async function walkReports(root, directory, depth, output) {
|
|
27
|
+
async function walkReports(root, directory, depth, output, kind) {
|
|
32
28
|
if (depth > MAX_DEPTH || output.length >= MAX_FILES)
|
|
33
29
|
return;
|
|
34
30
|
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
@@ -39,28 +35,26 @@ async function walkReports(root, directory, depth, output) {
|
|
|
39
35
|
continue;
|
|
40
36
|
const path = join(directory, entry.name);
|
|
41
37
|
if (entry.isDirectory()) {
|
|
42
|
-
await walkReports(root, path, depth + 1, output);
|
|
38
|
+
await walkReports(root, path, depth + 1, output, kind);
|
|
43
39
|
}
|
|
44
|
-
else if (entry.isFile() && isReportFile(root, path)) {
|
|
40
|
+
else if (entry.isFile() && isReportFile(root, path, kind)) {
|
|
45
41
|
output.push(resolve(path));
|
|
46
42
|
}
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
|
-
export async function snapshotReportFiles(root) {
|
|
45
|
+
export async function snapshotReportFiles(root, qaReportRoot) {
|
|
50
46
|
const files = [];
|
|
51
47
|
const workspaceRoot = resolve(root);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
join(
|
|
55
|
-
|
|
56
|
-
]) {
|
|
57
|
-
await walkReports(workspaceRoot, directory, 0, files);
|
|
48
|
+
const hub = resolve(qaReportRoot ?? join(workspaceRoot, "qa-report-server-v2"));
|
|
49
|
+
for (const kind of ["tasks", "qa-results", "static"]) {
|
|
50
|
+
const directory = join(kind === "tasks" ? workspaceRoot : hub, kind);
|
|
51
|
+
await walkReports(directory, directory, 0, files, kind);
|
|
58
52
|
}
|
|
59
53
|
const entries = await Promise.all(files.map(async (path) => [path, await fileFingerprint(path)]));
|
|
60
54
|
return new Map(entries);
|
|
61
55
|
}
|
|
62
|
-
export async function discoverReportFiles(root, before) {
|
|
63
|
-
const after = await snapshotReportFiles(root);
|
|
56
|
+
export async function discoverReportFiles(root, before, qaReportRoot) {
|
|
57
|
+
const after = await snapshotReportFiles(root, qaReportRoot);
|
|
64
58
|
return [...after.entries()]
|
|
65
59
|
.filter(([path, fingerprint]) => before.get(path) !== fingerprint)
|
|
66
60
|
.map(([path]) => path)
|
|
@@ -97,7 +91,7 @@ async function verifyOssUrl(url) {
|
|
|
97
91
|
}
|
|
98
92
|
}
|
|
99
93
|
export async function uploadCloudReports(input) {
|
|
100
|
-
const files = await discoverReportFiles(input.root, input.before);
|
|
94
|
+
const files = await discoverReportFiles(input.root, input.before, input.qaReportRoot);
|
|
101
95
|
if (files.length === 0)
|
|
102
96
|
return { urls: [], failures: [] };
|
|
103
97
|
const run = input.execFile ?? (async (file, args) => {
|
|
@@ -131,10 +125,10 @@ export async function uploadCloudReports(input) {
|
|
|
131
125
|
}
|
|
132
126
|
return { urls, failures };
|
|
133
127
|
}
|
|
134
|
-
export async function prepareCloudReportSnapshot(facts, root) {
|
|
128
|
+
export async function prepareCloudReportSnapshot(facts, root, qaReportRoot) {
|
|
135
129
|
if (!facts.enabled)
|
|
136
130
|
return null;
|
|
137
|
-
return snapshotReportFiles(root);
|
|
131
|
+
return snapshotReportFiles(root, qaReportRoot);
|
|
138
132
|
}
|
|
139
133
|
export async function prepareCloudReportExecution(input) {
|
|
140
134
|
const facts = cloudReportUploadFacts(input.env);
|
|
@@ -147,7 +141,7 @@ export async function prepareCloudReportExecution(input) {
|
|
|
147
141
|
return { facts, snapshot: null };
|
|
148
142
|
}
|
|
149
143
|
try {
|
|
150
|
-
return { facts, snapshot: await prepareCloudReportSnapshot(facts, input.root) };
|
|
144
|
+
return { facts, snapshot: await prepareCloudReportSnapshot(facts, input.root, input.qaReportRoot) };
|
|
151
145
|
}
|
|
152
146
|
catch (error) {
|
|
153
147
|
dslog("cloud_report_upload.snapshot_failed", "报告文件快照失败", {
|
|
@@ -173,10 +167,19 @@ export async function deliverCloudReports(input) {
|
|
|
173
167
|
if (!input.facts.enabled || input.facts.ossUploadScript === undefined || input.before === null) {
|
|
174
168
|
return { finalText: input.finalText, externalAnswer: input.externalAnswer };
|
|
175
169
|
}
|
|
170
|
+
if (input.captureFinal && await verifiedReportReceipt({
|
|
171
|
+
...input, answer: input.externalAnswer ?? input.finalText,
|
|
172
|
+
})) {
|
|
173
|
+
dslog("cloud_report_upload.already_delivered", "本次执行的 HTML 交付收据及内容已验证,跳过重复上传", {
|
|
174
|
+
execution_id: input.executionId, agent_handle: input.agentHandle,
|
|
175
|
+
});
|
|
176
|
+
return { finalText: input.finalText, externalAnswer: input.externalAnswer };
|
|
177
|
+
}
|
|
176
178
|
try {
|
|
177
179
|
const result = await (input.upload ?? uploadCloudReports)({
|
|
178
180
|
script: input.facts.ossUploadScript,
|
|
179
181
|
root: input.root,
|
|
182
|
+
...(input.qaReportRoot === undefined ? {} : { qaReportRoot: input.qaReportRoot }),
|
|
180
183
|
before: input.before,
|
|
181
184
|
});
|
|
182
185
|
if (result.failures.length > 0) {
|
|
@@ -185,6 +188,14 @@ export async function deliverCloudReports(input) {
|
|
|
185
188
|
uploaded_count: result.urls.length, failed_count: result.failures.length,
|
|
186
189
|
});
|
|
187
190
|
}
|
|
191
|
+
else {
|
|
192
|
+
dslog(result.urls.length === 0 ? "cloud_report_upload.no_files" : "cloud_report_upload.completed", result.urls.length === 0 ? "未发现新增或变化的报告文件" : "报告上传完成", {
|
|
193
|
+
execution_id: input.executionId, agent_handle: input.agentHandle,
|
|
194
|
+
uploaded_count: result.urls.length,
|
|
195
|
+
report_root: input.root,
|
|
196
|
+
qa_report_root: input.qaReportRoot ?? join(input.root, "qa-report-server-v2"),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
188
199
|
return {
|
|
189
200
|
finalText: input.captureFinal && result.urls.length > 0
|
|
190
201
|
? appendReportLinks(input.finalText ?? "", result.urls)
|
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
|
@@ -12,6 +12,7 @@ import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
|
12
12
|
import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
|
|
13
13
|
import { augmentedPath } from "./runtime-path.js";
|
|
14
14
|
import { deliverCloudReports, prepareCloudReportExecution } from "./cloud-report-uploader.js";
|
|
15
|
+
import { reportDeliveryContext } from "./cloud-report-receipt.js";
|
|
15
16
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
16
17
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
17
18
|
import { providerErrorFromExit } from "./provider-error.js";
|
|
@@ -34,10 +35,11 @@ import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPr
|
|
|
34
35
|
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
35
36
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
36
37
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
37
|
-
import { machinePackageManagerEnv
|
|
38
|
+
import { machinePackageManagerEnv } from "./package-manager-env.js";
|
|
39
|
+
import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
|
|
38
40
|
import { sanitizeEnvVars } from "./runtime-env.js";
|
|
39
|
-
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath,
|
|
40
|
-
import {
|
|
41
|
+
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
|
|
42
|
+
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
41
43
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
42
44
|
constructor(runtime, timeoutMs) {
|
|
43
45
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -254,8 +256,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
254
256
|
let executionWorkspace = workspace;
|
|
255
257
|
let localMemoryTelemetry = null;
|
|
256
258
|
const inheritedEnv = { ...process.env };
|
|
257
|
-
const
|
|
259
|
+
const reportRoots = {
|
|
260
|
+
root: workspace.dir,
|
|
261
|
+
...(input.launch.qaReportRoot === undefined ? {} : { qaReportRoot: input.launch.qaReportRoot }),
|
|
262
|
+
};
|
|
263
|
+
const cloudReportPreparation = await prepareCloudReportExecution({ env: inheritedEnv, ...reportRoots, executionId: input.executionId, agentHandle: input.handle });
|
|
258
264
|
const { facts: cloudReportUpload, snapshot: reportSnapshot } = cloudReportPreparation;
|
|
265
|
+
const reportContext = reportDeliveryContext(input.executionId, workspace.runDir, cloudReportUpload);
|
|
259
266
|
for (const key of Object.keys(inheritedEnv)) {
|
|
260
267
|
if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
|
|
261
268
|
delete inheritedEnv[key];
|
|
@@ -382,26 +389,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
382
389
|
}
|
|
383
390
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
384
391
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
385
|
-
let pendingMemoryContext = "";
|
|
386
|
-
if (input.localMemoryV2Enabled === true) {
|
|
387
|
-
try {
|
|
388
|
-
const pendingCandidates = await searchPendingMemoryCandidates({
|
|
389
|
-
agentDir: executionWorkspace.dir,
|
|
390
|
-
query: wakePrompt,
|
|
391
|
-
limit: 5,
|
|
392
|
-
});
|
|
393
|
-
pendingMemoryContext = renderPendingMemoryContext(pendingCandidates);
|
|
394
|
-
}
|
|
395
|
-
catch (error) {
|
|
396
|
-
dslog("local_memory.pending_recall_failed", "本地归档记忆召回失败", {
|
|
397
|
-
level: "WARN",
|
|
398
|
-
execution_id: input.executionId,
|
|
399
|
-
agent_handle: input.handle,
|
|
400
|
-
error_name: error instanceof Error ? error.name : "unknown",
|
|
401
|
-
error_code: error?.code,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
392
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
406
393
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
407
394
|
&& isLocalMemoryV2Eligible({
|
|
@@ -413,26 +400,14 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
413
400
|
})) {
|
|
414
401
|
localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
|
|
415
402
|
}
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
:
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
<= input.maxSystemPromptBytes;
|
|
425
|
-
// The structured prune contract is mandatory for V2. Drop optional archived
|
|
426
|
-
// recall before dropping the daemon result file when the prompt budget is tight.
|
|
427
|
-
if (!fitsPromptBudget(baseSystemPrompt) && pendingMemoryContext !== "") {
|
|
428
|
-
baseSystemPrompt = promptWithoutPending;
|
|
429
|
-
}
|
|
430
|
-
const structuredPromptFits = fitsPromptBudget(baseSystemPrompt);
|
|
431
|
-
if (!structuredPromptFits)
|
|
432
|
-
localMemoryResultPath = null;
|
|
433
|
-
const systemPrompt = localMemoryResultPath === null
|
|
434
|
-
? baseSystemPrompt
|
|
435
|
-
: `${baseSystemPrompt}${structuredMemoryInstructions ?? ""}`;
|
|
403
|
+
const memoryPrompt = await prepareLocalMemoryPrompt({
|
|
404
|
+
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
405
|
+
enabled: input.localMemoryV2Enabled === true, query: wakePrompt,
|
|
406
|
+
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
407
|
+
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
408
|
+
});
|
|
409
|
+
localMemoryResultPath = memoryPrompt.resultPath;
|
|
410
|
+
const systemPrompt = memoryPrompt.systemPrompt;
|
|
436
411
|
if (!isolatedMaintenance)
|
|
437
412
|
try {
|
|
438
413
|
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
@@ -513,6 +488,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
513
488
|
// Expose only the training root; ability-specific workspace layout stays
|
|
514
489
|
// owned by the training package rather than the NowWork daemon.
|
|
515
490
|
QA_RUNTIME_ROOT: join(crewHome, "training"),
|
|
491
|
+
QA_REPORT_DELIVERY_CONTEXT: JSON.stringify(reportContext),
|
|
516
492
|
CREW_TASK_DIR: executionWorkspace.runDir,
|
|
517
493
|
CREW_TASK_LOG: executionWorkspace.workLogPath,
|
|
518
494
|
...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
|
|
@@ -532,30 +508,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
532
508
|
const childEnv = runtime.name === "deepseek-harness"
|
|
533
509
|
? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
|
|
534
510
|
: providerEnv;
|
|
535
|
-
|
|
536
|
-
execution_id: input.executionId,
|
|
537
|
-
agent_handle: input.handle,
|
|
538
|
-
runtime: runtime.name,
|
|
539
|
-
qa_runtime_root: childEnv.QA_RUNTIME_ROOT ?? null,
|
|
540
|
-
qa_runtime_root_present: childEnv.QA_RUNTIME_ROOT !== undefined,
|
|
541
|
-
inherited_qa_runtime_root_present: inheritedEnv.QA_RUNTIME_ROOT !== undefined,
|
|
542
|
-
...packageManagerEnvFacts(childEnv),
|
|
543
|
-
training_root: join(crewHome, "training"),
|
|
544
|
-
cloud_provider: cloudReportUpload.provider,
|
|
545
|
-
oss_report_upload_enabled: cloudReportUpload.enabled,
|
|
546
|
-
project_context_present: input.projectContext !== undefined,
|
|
547
|
-
project_ids: JSON.stringify(input.projectContext === undefined
|
|
548
|
-
? []
|
|
549
|
-
: [
|
|
550
|
-
...(input.projectContext.primary === undefined
|
|
551
|
-
? []
|
|
552
|
-
: [input.projectContext.primary.projectId]),
|
|
553
|
-
...input.projectContext.secondary.map((project) => project.projectId),
|
|
554
|
-
]),
|
|
555
|
-
ability_release_present: input.abilityRelease !== undefined,
|
|
556
|
-
ability_release_id: input.abilityRelease?.releaseId,
|
|
557
|
-
ability_root_commit: input.abilityRelease?.rootCommit,
|
|
558
|
-
});
|
|
511
|
+
logRuntimeEnvironmentContract(input, childEnv, inheritedEnv, crewHome, cloudReportUpload);
|
|
559
512
|
const warn = dependencies.warnRuntime ?? warnRuntimeLocally;
|
|
560
513
|
const launchRuntime = dependencies.launchRuntime ?? (async (launchReq) => {
|
|
561
514
|
if (launchReq.runtime === "codex" && launchReq.codexCollaborationTools === true) {
|
|
@@ -964,22 +917,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
964
917
|
localMemoryTelemetry?.markRuntimeExited(exitCode);
|
|
965
918
|
memoryPruneRuntimeExitCode = exitCode;
|
|
966
919
|
if (localMemoryResultPath !== null) {
|
|
920
|
+
memoryPruneFailurePhase = "memory_commit";
|
|
967
921
|
const committed = await commitLocalMemoryResultAfterExit({
|
|
968
922
|
resultPath: localMemoryResultPath,
|
|
969
923
|
agentDir: executionWorkspace.dir,
|
|
970
924
|
executionId: input.executionId,
|
|
971
925
|
pruneTraceId: memoryPruneTraceId,
|
|
972
926
|
exitCode,
|
|
927
|
+
workLogPath: executionWorkspace.workLogPath,
|
|
973
928
|
});
|
|
974
|
-
if (committed.status === "failed") {
|
|
975
|
-
|
|
976
|
-
|
|
929
|
+
if (committed.status === "failed" || committed.status === "missing") {
|
|
930
|
+
const reason = committed.error ?? "local_memory_result_missing";
|
|
931
|
+
dslog("local_memory.commit_failed", "结构化本地记忆提交或清理未完成,保留可用恢复材料", {
|
|
932
|
+
level: "ERROR",
|
|
977
933
|
execution_id: input.executionId,
|
|
978
934
|
agent_handle: input.handle,
|
|
979
|
-
error_code:
|
|
935
|
+
error_code: reason,
|
|
980
936
|
result_path: localMemoryResultPath,
|
|
981
937
|
});
|
|
938
|
+
throw new Error(`local_memory_commit_failed: ${reason}`);
|
|
982
939
|
}
|
|
940
|
+
memoryPruneFailurePhase = "runtime_finalize";
|
|
983
941
|
}
|
|
984
942
|
const errorTail = [
|
|
985
943
|
stderrTail.trim(),
|
|
@@ -1018,7 +976,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
1018
976
|
}
|
|
1019
977
|
const cloudReportResult = exitCode === 0
|
|
1020
978
|
? await deliverCloudReports({
|
|
1021
|
-
facts: cloudReportUpload,
|
|
979
|
+
facts: cloudReportUpload, ...reportRoots, before: reportSnapshot, finalText,
|
|
980
|
+
receiptFile: reportContext.receiptFile,
|
|
1022
981
|
externalAnswer: externalOutput.lastClosedBlock
|
|
1023
982
|
?? (finalText === null ? null : extractExternalAnswer(finalText)),
|
|
1024
983
|
captureFinal: input.captureFinal, executionId: input.executionId, agentHandle: input.handle,
|
|
@@ -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
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
5
|
import { applyLocalMemoryResult, parseLocalMemoryResult } from "./write.js";
|
|
5
6
|
export function isLocalMemoryV2Eligible(input) {
|
|
6
7
|
// The allowlist is the rollout boundary. The marker and seed/pre-existing
|
|
@@ -54,10 +55,21 @@ export function renderLocalMemoryResultInstructions(resultPath) {
|
|
|
54
55
|
将一个合法 JSON 对象写入以下 daemon 提供的文件,写入完成后再结束本轮:
|
|
55
56
|
${resultPath}
|
|
56
57
|
|
|
57
|
-
JSON
|
|
58
|
+
outcome 只允许 no_change、proposed、conflict、failed。新增、更新、替代记忆或任何 statusChanges 都必须使用 proposed;no_change 的 changes/statusChanges 必须都为空。conflict/failed 表示未完成,不能当作提交成功。自然语言“已修正”不是 JSON 枚举值,不能写成 corrected。
|
|
59
|
+
|
|
60
|
+
新增记忆的完整合法示例(内容须替换为本任务实际候选):
|
|
61
|
+
\`\`\`json
|
|
62
|
+
{"schemaVersion":1,"purpose":"memory_prune","baseCatalogRevision":0,"outcome":"proposed","changes":[{"operation":"create","memoryId":null,"baseRevision":null,"supersedesMemoryId":null,"title":"发布验收约定","kind":"procedural","summary":"发布前必须验证回滚步骤。","triggers":["发布","回滚"],"priority":"medium","detailCategory":"project-rules","detailBody":"发布前必须验证回滚步骤,并将验证结果写入发布报告。","reviewAfter":null}],"statusChanges":[]}
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
没有需要变更的记忆时:
|
|
66
|
+
\`\`\`json
|
|
58
67
|
{"schemaVersion":1,"purpose":"memory_prune","baseCatalogRevision":0,"outcome":"no_change","changes":[],"statusChanges":[]}
|
|
59
|
-
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
每个 changes 项须保留示例中的全部字段。operation=create|update|supersede;kind=procedural|semantic|episodic|preference;priority=high|medium|low;detailCategory=preferences|project-rules|testing|debugging|git|incidents|lessons。create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null;update 按现有索引填写 memoryId/baseRevision,supersedesMemoryId 为 null;supersede 填 supersedesMemoryId/baseRevision,memoryId 为 null。
|
|
60
71
|
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中;后续相关任务会按需召回,索引腾出空间后可自动提升。
|
|
72
|
+
写入候选文件不等于已经记住,只有 daemon 校验并提交成功后记忆才生效。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在提交成功后清理,失败时保留日志和候选用于排查或重试。
|
|
61
73
|
`;
|
|
62
74
|
}
|
|
63
75
|
/** Prevent ordinary V2 turns from racing the daemon-managed memory writer. */
|
|
@@ -81,16 +93,52 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
81
93
|
}
|
|
82
94
|
try {
|
|
83
95
|
const result = parseLocalMemoryResult(raw);
|
|
96
|
+
if (result.outcome === "conflict" || result.outcome === "failed") {
|
|
97
|
+
return { status: "failed", error: `local_memory_result_${result.outcome}` };
|
|
98
|
+
}
|
|
84
99
|
await applyLocalMemoryResult({
|
|
85
100
|
agentDir: input.agentDir,
|
|
86
101
|
result,
|
|
87
102
|
executionId: input.executionId,
|
|
88
103
|
pruneTraceId: input.pruneTraceId,
|
|
89
104
|
});
|
|
90
|
-
await rm(input.resultPath, { force: true });
|
|
91
|
-
return { status: "applied" };
|
|
92
105
|
}
|
|
93
106
|
catch (error) {
|
|
94
107
|
return { status: "failed", error: error instanceof Error ? error.message : "apply_failed" };
|
|
95
108
|
}
|
|
109
|
+
if (input.workLogPath !== undefined) {
|
|
110
|
+
try {
|
|
111
|
+
const relativeLogPath = relative(resolve(input.agentDir), resolve(input.workLogPath));
|
|
112
|
+
if (relativeLogPath === "" || relativeLogPath === ".." || relativeLogPath.startsWith(`..${sep}`)
|
|
113
|
+
|| isAbsolute(relativeLogPath))
|
|
114
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
115
|
+
const expectedLogPath = join(await realpath(input.agentDir), relativeLogPath);
|
|
116
|
+
const before = await lstat(input.workLogPath);
|
|
117
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
118
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
119
|
+
if (await realpath(input.workLogPath) !== expectedLogPath)
|
|
120
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
121
|
+
const handle = await open(input.workLogPath, constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
122
|
+
try {
|
|
123
|
+
const opened = await handle.stat();
|
|
124
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
125
|
+
throw new Error("local_memory_work_log_path_changed");
|
|
126
|
+
}
|
|
127
|
+
await handle.truncate(0);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await handle.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return { status: "failed", error: "local_memory_work_log_cleanup_failed" };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await rm(input.resultPath, { force: true });
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { status: "failed", error: "local_memory_result_cleanup_failed" };
|
|
142
|
+
}
|
|
143
|
+
return { status: "applied" };
|
|
96
144
|
}
|
|
@@ -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/prompt.js
CHANGED
|
@@ -67,10 +67,23 @@ export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
|
|
|
67
67
|
const tail = workLog.slice(-WORKLOG_TAIL);
|
|
68
68
|
return `${head}\n…(work-log 过长已截去中段,全文用 Read 读 $CREW_TASK_LOG)\n${tail}`;
|
|
69
69
|
}
|
|
70
|
-
export function capMemoryForInject(memory, cap = MEMORY_INJECT_CAP) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
export function capMemoryForInject(memory, cap = MEMORY_INJECT_CAP, maxBytes = Number.POSITIVE_INFINITY) {
|
|
71
|
+
if (memory.length <= cap && Buffer.byteLength(memory, "utf8") <= maxBytes)
|
|
72
|
+
return memory;
|
|
73
|
+
const notice = "[MEMORY.md preview truncated; use crew-memory search/read for more.]\n";
|
|
74
|
+
const noticeBytes = Buffer.byteLength(notice, "utf8");
|
|
75
|
+
if (notice.length > cap || noticeBytes > maxBytes)
|
|
76
|
+
return "";
|
|
77
|
+
let end = 0;
|
|
78
|
+
let bytes = noticeBytes;
|
|
79
|
+
for (const character of memory) {
|
|
80
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
81
|
+
if (notice.length + end + character.length > cap || bytes + size > maxBytes)
|
|
82
|
+
break;
|
|
83
|
+
end += character.length;
|
|
84
|
+
bytes += size;
|
|
85
|
+
}
|
|
86
|
+
return notice + memory.slice(0, end);
|
|
74
87
|
}
|
|
75
88
|
export function buildSystemPrompt(ctx) {
|
|
76
89
|
const product = ctx.productName ?? "nowwork";
|
|
@@ -99,7 +112,7 @@ export function buildSystemPrompt(ctx) {
|
|
|
99
112
|
// scheduled 换成静默版,不出现任何"先确认/先回复"的措辞。
|
|
100
113
|
const startupSequence = alwaysReport
|
|
101
114
|
? `## 启动序列(每轮汇报定时任务)
|
|
102
|
-
1.
|
|
115
|
+
1. 参考本提示词中的 MEMORY.md 预览;需要历史约束或经验时检索完整索引,再按索引中的 file 从 $CREW_HOME 读取明细。
|
|
103
116
|
2. 执行唤醒提示词里给出的定时指令。
|
|
104
117
|
3. Return exactly one self-contained final report as your runtime final response.
|
|
105
118
|
4. Do not call crew message send; the daemon delivers the final response.
|
|
@@ -107,14 +120,14 @@ export function buildSystemPrompt(ctx) {
|
|
|
107
120
|
6. 只有存在需要后续跟进的具体事项时,才用 \`crew task create\` 创建任务。`
|
|
108
121
|
: scheduled
|
|
109
122
|
? `## 启动序列(静默定时任务)
|
|
110
|
-
1.
|
|
123
|
+
1. 参考本提示词中的 MEMORY.md 预览;需要历史约束或经验时检索完整索引,再按索引中的 file 从 $CREW_HOME 读取明细。
|
|
111
124
|
2. 执行唤醒提示词里给出的定时指令。**默认零输出**:不判断"是否需要先确认/声明接手",本轮没有人在等你回复。
|
|
112
125
|
3. 只有发现异常、需要人处理、或指令明确要求汇报时,才用 \`crew message send --send-draft\` 输出到频道(用法见上面"通信"一节)。
|
|
113
126
|
4. 只有存在需要后续跟进的具体事项时,才用 \`crew task create\` 创建任务。
|
|
114
127
|
5. 无异常时直接结束,不需要任何输出,不必读频道历史(除非指令要求)。`
|
|
115
128
|
: `## 启动序列
|
|
116
129
|
1. 若本轮来信可以直接回答,不要先发确认消息;处理完后只发一条完整回复。只有需要澄清、调用工具、协调他人或较长时间处理时,才先发有信息量的确认/进度消息。
|
|
117
|
-
2.
|
|
130
|
+
2. 参考本提示词中的 MEMORY.md 预览;需要历史约束或经验时检索完整索引,再按索引中的 file 从 $CREW_HOME 读取明细。
|
|
118
131
|
3. 若本轮只有"有未读"的 inbox notice、没有正文:notice 表示存在你尚未看到的消息(正文被暂时省略以免刷屏,不是没有内容)。是否读、何时读由你判断,可用 \`crew message check\` / \`crew message read\` 拉取。**绝不能仅凭一条 content-free notice 就断定"没有工作"**;若选择暂不读,要诚实当作 defer。
|
|
119
132
|
4. 收到消息就处理,并用 \`crew message send\` 回复。
|
|
120
133
|
5. **做完你负责的所有事再停。** 多步任务(调研/改码/测试)要全部完成、汇报结果后再停。新消息会在你存活期间自动送达,无需轮询等待。`;
|
|
@@ -329,13 +342,15 @@ ${interactiveTaskRules}${freshnessRule}${externalReplyRule}${collaborationEtique
|
|
|
329
342
|
|
|
330
343
|
## Workspace 与分层记忆(CRITICAL — 索引+按需,配合上面的并行规则)
|
|
331
344
|
你的持久记忆在 \`$CREW_HOME\`(跨你所有任务共享),按“常驻索引 + 功能明细 + 当前工作”分层:
|
|
332
|
-
1. **MEMORY.md = 常驻索引 +
|
|
345
|
+
1. **MEMORY.md = 常驻索引 + 角色**:提示词末尾可能只有它的有界预览,预算不足时也可能省略。预览未命中不等于没有记忆。索引条目包含 \`file/type/scope/summary/triggers/priority/updated\`,其中 file 是相对于 \`$CREW_HOME\` 的路径(例如 \`notes/testing.md\`);需要历史约束、经验或偏好时先检索完整索引,再按需分段读取。
|
|
333
346
|
2. **notes/<topic>.md = 功能明细**:按用户偏好、项目规则、测试、调试、Git、事故复盘等主题拆分。优先读取与当前任务匹配的文件;\`notes/lessons.md\` 只保留未分类经验和历史兼容内容,不是所有新经验的默认目的地。普通任务默认只读不写,但当前用户明确要求时可以按最小范围更新;任务结束后的专门收尾执行仍按收尾指令复核。
|
|
334
347
|
- 对旧版 MEMORY.md,如果条目没有完整结构化字段但明确指向 \`notes/<file>.md\`,把它视为 legacy 索引;仍需结合条目标题/链接和当前请求判断相关性,不要因为缺少 summary/triggers 就假定正文已注入。
|
|
335
348
|
3. **本任务 work-log = \`$CREW_TASK_LOG\`**:每任务一份(别的运行不会碰),记当前阶段/下一步/卡点/关键结论。系统已把它的当前内容注入末尾让你续上;有进展就更新它——这是你**唯一**该在任务中持续写的记忆文件。**work-log 是现状快照,不是流水账**:保持 ≤100 行,更新时重写全文替换过时内容,不要无限追加(过长会被注入截断)。
|
|
336
349
|
|
|
337
350
|
**按需读取规则(提示词引导,不是 Daemon 自动召回保证)**:
|
|
338
|
-
-
|
|
351
|
+
- 需要历史约束、经验或偏好时,在最终回答或执行动作前先用只读命令 \`crew-memory search --query '关键词' [--after-line N]\` 搜索完整 \`$CREW_HOME/MEMORY.md\`;默认返回 JSON,有后续结果时按返回游标继续,不要把预览中未命中当成全文无结果。
|
|
352
|
+
- 用 \`crew-memory read [--file MEMORY.md|notes/xxx.md] [--offset N] [--limit N]\` 读取命中范围,再按索引中的 file 从 $CREW_HOME 读取明细;默认返回 JSON。以当前有效索引及其指向的正文为依据,不自动读取 pending,也不把未决或已替代片段当成有效记忆。
|
|
353
|
+
- 旧 daemon 没有 \`crew-memory\` 时,回退到 runtime 原生文件搜索和分段 Read:先搜索完整 \`$CREW_HOME/MEMORY.md\`,再用 \`Read $CREW_HOME/<file>\` 读取命中范围;不要再次拼接 \`notes/\`。工具提示截断或返回下一页时继续分页,不要一次读取整份大文件。
|
|
339
354
|
- 未命中时不要为了“保险”读取全部 notes;只读取能直接影响当前任务的文件。MEMORY.md 注入的是索引,不会自动让 note 正文可用。
|
|
340
355
|
|
|
341
356
|
**沉淀与瘦身由系统触发,不要平时自行扩展范围**:任务被改为 done/closed 时,系统会单独唤醒你收尾——那时按功能分类把可复用经验提炼进合适的 \`notes/<topic>.md\`,更新 MEMORY.md 索引。普通执行已经按用户明确要求写入时,收尾先检查、后修改,只在明确缺漏时做最小必要修正,不得重复追加或只为优化措辞而改写。最终结论必须且只能归类为:无变化、已修正、冲突、失败;无变化或已修正时必须满足对应后置条件并清空 work-log,冲突或失败时保留 work-log,任一记忆写入失败则保留 work-log。不要默认全部写入 \`notes/lessons.md\`。默认不要改 MEMORY.md/notes,但不能覆盖当前用户明确授权的最小范围更新。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { packageManagerEnvFacts } from "./package-manager-env.js";
|
|
3
|
+
import { dslog } from "./slog.js";
|
|
4
|
+
export function logRuntimeEnvironmentContract(input, childEnv, inheritedEnv, crewHome, cloudReportUpload) {
|
|
5
|
+
dslog("runtime.environment_contract", "Runtime 子进程环境契约", {
|
|
6
|
+
execution_id: input.executionId,
|
|
7
|
+
agent_handle: input.handle,
|
|
8
|
+
runtime: input.runtime.name,
|
|
9
|
+
qa_runtime_root: childEnv.QA_RUNTIME_ROOT ?? null,
|
|
10
|
+
qa_runtime_root_present: childEnv.QA_RUNTIME_ROOT !== undefined,
|
|
11
|
+
inherited_qa_runtime_root_present: inheritedEnv.QA_RUNTIME_ROOT !== undefined,
|
|
12
|
+
...packageManagerEnvFacts(childEnv),
|
|
13
|
+
training_root: join(crewHome, "training"),
|
|
14
|
+
cloud_provider: cloudReportUpload.provider,
|
|
15
|
+
oss_report_upload_enabled: cloudReportUpload.enabled,
|
|
16
|
+
project_context_present: input.projectContext !== undefined,
|
|
17
|
+
project_ids: JSON.stringify(input.projectContext === undefined
|
|
18
|
+
? []
|
|
19
|
+
: [
|
|
20
|
+
...(input.projectContext.primary === undefined ? [] : [input.projectContext.primary.projectId]),
|
|
21
|
+
...input.projectContext.secondary.map((project) => project.projectId),
|
|
22
|
+
]),
|
|
23
|
+
ability_release_present: input.abilityRelease !== undefined,
|
|
24
|
+
ability_release_id: input.abilityRelease?.releaseId,
|
|
25
|
+
ability_root_commit: input.abilityRelease?.rootCommit,
|
|
26
|
+
});
|
|
27
|
+
}
|
package/dist/workspace.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { mkdir, writeFile, readFile, chmod, access, lstat } from "node:fs/promises";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
14
16
|
import { dslog } from "./slog.js";
|
|
15
17
|
/** 文件系统安全的 taskKey:仅留 [\w.-],其余转 _,截断,避免路径穿越/超长。 */
|
|
16
18
|
export function safeKey(key) {
|
|
@@ -96,6 +98,19 @@ export async function prepareWorkspace(input) {
|
|
|
96
98
|
await chmod(wrapper, 0o755);
|
|
97
99
|
// win32 侧 wrapper:cmd/PowerShell 不认 shebang 脚本,并存 crew.cmd(unix 下无害不会被命中)
|
|
98
100
|
await writeFile(join(crewDir, "crew.cmd"), `@echo off\r\nnode ${JSON.stringify(input.cliPath)} %*\r\n`, "utf8");
|
|
101
|
+
if (!input.skipLocalMemory) {
|
|
102
|
+
const sourceMode = import.meta.url.endsWith(".ts");
|
|
103
|
+
const reader = fileURLToPath(new URL(`./local-memory/read-cli.${sourceMode ? "ts" : "js"}`, import.meta.url));
|
|
104
|
+
const readerArgs = sourceMode
|
|
105
|
+
? ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href, reader]
|
|
106
|
+
: [reader];
|
|
107
|
+
const readerWrapper = join(crewDir, "crew-memory");
|
|
108
|
+
const posixArgs = readerArgs.map((arg) => `'${arg.replace(/'/gu, "'\\''")}'`).join(" ");
|
|
109
|
+
const windowsArgs = readerArgs.map((arg) => `"${arg.replace(/%/gu, "%%")}"`).join(" ");
|
|
110
|
+
await writeFile(readerWrapper, `#!/bin/sh\nexec node ${posixArgs} "$@"\n`, "utf8");
|
|
111
|
+
await chmod(readerWrapper, 0o755);
|
|
112
|
+
await writeFile(join(crewDir, "crew-memory.cmd"), `@echo off\r\nnode ${windowsArgs} %*\r\n`, "utf8");
|
|
113
|
+
}
|
|
99
114
|
// 每个 agent 独立的 XDG 配置根:隔离第三方 CLI 凭证,避免 agent 之间互相串号
|
|
100
115
|
const homeDir = join(dir, ".home");
|
|
101
116
|
await mkdir(join(homeDir, ".config"), { recursive: true });
|