@nowcrew/daemon 0.6.87-beta.2 → 0.6.87-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloud-report-uploader.js +27 -25
- package/dist/execution-runner.js +4 -2
- package/dist/external-output.js +154 -68
- package/dist/local-executor.js +37 -5
- package/dist/local-memory/pending.js +136 -2
- package/dist/local-memory/runtime.js +8 -1
- package/dist/local-memory/write.js +126 -4
- package/dist/main.js +6 -2
- package/dist/project-skills/capability.js +109 -0
- package/dist/project-skills/controller-convergence.js +57 -0
- package/dist/project-skills/projection-set-switch.js +419 -0
- package/dist/project-skills/projection-state-domain.js +170 -0
- package/dist/project-skills/projection-state-store.js +842 -0
- package/dist/project-skills/projection-state-transaction.js +322 -0
- package/dist/project-skills/projection-state.js +3 -0
- package/dist/project-skills/runtime-root-bootstrap.js +47 -0
- package/dist/project-skills/runtime-root-domain.js +268 -0
- package/dist/project-skills/runtime-root-gc.js +293 -0
- package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
- package/dist/project-skills/runtime-root-leases.js +487 -0
- package/dist/project-skills/runtime-root-source-identity.js +60 -0
- package/dist/project-skills/runtime-root-startup.js +49 -0
- package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
- package/dist/project-skills/runtime-root-state-index.js +356 -0
- package/dist/project-skills/runtime-root-store.js +722 -0
- package/dist/project-skills/serve-capability.js +28 -0
- package/dist/project-skills/serve-startup.js +37 -0
- package/dist/prompt.js +4 -1
- package/dist/runner.js +3 -1
- package/dist/serve.js +17 -11
- package/dist/storage-identity.js +4 -1
- package/dist/storage-ownership.js +93 -5
- package/dist/storage-runtime-controller.js +40 -20
- package/dist/storage-runtime.js +38 -2
- package/dist/storage-serve-adapter.js +22 -5
- package/package.json +1 -1
|
@@ -13,22 +13,17 @@ const fileFingerprint = async (path) => {
|
|
|
13
13
|
const stat = await lstat(path);
|
|
14
14
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
15
15
|
};
|
|
16
|
-
function isReportFile(root, path) {
|
|
16
|
+
function isReportFile(root, path, kind) {
|
|
17
17
|
const parts = relative(root, path).split(sep).filter(Boolean);
|
|
18
18
|
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) {
|
|
19
|
+
if (kind === "static")
|
|
20
|
+
return parts.length === 1 && extname(filename).toLowerCase() === ".html";
|
|
21
|
+
if (parts.length >= 2) {
|
|
27
22
|
return REPORT_FILENAMES.has(filename) || extname(filename).toLowerCase() === ".html";
|
|
28
23
|
}
|
|
29
24
|
return false;
|
|
30
25
|
}
|
|
31
|
-
async function walkReports(root, directory, depth, output) {
|
|
26
|
+
async function walkReports(root, directory, depth, output, kind) {
|
|
32
27
|
if (depth > MAX_DEPTH || output.length >= MAX_FILES)
|
|
33
28
|
return;
|
|
34
29
|
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
@@ -39,28 +34,26 @@ async function walkReports(root, directory, depth, output) {
|
|
|
39
34
|
continue;
|
|
40
35
|
const path = join(directory, entry.name);
|
|
41
36
|
if (entry.isDirectory()) {
|
|
42
|
-
await walkReports(root, path, depth + 1, output);
|
|
37
|
+
await walkReports(root, path, depth + 1, output, kind);
|
|
43
38
|
}
|
|
44
|
-
else if (entry.isFile() && isReportFile(root, path)) {
|
|
39
|
+
else if (entry.isFile() && isReportFile(root, path, kind)) {
|
|
45
40
|
output.push(resolve(path));
|
|
46
41
|
}
|
|
47
42
|
}
|
|
48
43
|
}
|
|
49
|
-
export async function snapshotReportFiles(root) {
|
|
44
|
+
export async function snapshotReportFiles(root, qaReportRoot) {
|
|
50
45
|
const files = [];
|
|
51
46
|
const workspaceRoot = resolve(root);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
join(
|
|
55
|
-
|
|
56
|
-
]) {
|
|
57
|
-
await walkReports(workspaceRoot, directory, 0, files);
|
|
47
|
+
const hub = resolve(qaReportRoot ?? join(workspaceRoot, "qa-report-server-v2"));
|
|
48
|
+
for (const kind of ["tasks", "qa-results", "static"]) {
|
|
49
|
+
const directory = join(kind === "tasks" ? workspaceRoot : hub, kind);
|
|
50
|
+
await walkReports(directory, directory, 0, files, kind);
|
|
58
51
|
}
|
|
59
52
|
const entries = await Promise.all(files.map(async (path) => [path, await fileFingerprint(path)]));
|
|
60
53
|
return new Map(entries);
|
|
61
54
|
}
|
|
62
|
-
export async function discoverReportFiles(root, before) {
|
|
63
|
-
const after = await snapshotReportFiles(root);
|
|
55
|
+
export async function discoverReportFiles(root, before, qaReportRoot) {
|
|
56
|
+
const after = await snapshotReportFiles(root, qaReportRoot);
|
|
64
57
|
return [...after.entries()]
|
|
65
58
|
.filter(([path, fingerprint]) => before.get(path) !== fingerprint)
|
|
66
59
|
.map(([path]) => path)
|
|
@@ -97,7 +90,7 @@ async function verifyOssUrl(url) {
|
|
|
97
90
|
}
|
|
98
91
|
}
|
|
99
92
|
export async function uploadCloudReports(input) {
|
|
100
|
-
const files = await discoverReportFiles(input.root, input.before);
|
|
93
|
+
const files = await discoverReportFiles(input.root, input.before, input.qaReportRoot);
|
|
101
94
|
if (files.length === 0)
|
|
102
95
|
return { urls: [], failures: [] };
|
|
103
96
|
const run = input.execFile ?? (async (file, args) => {
|
|
@@ -131,10 +124,10 @@ export async function uploadCloudReports(input) {
|
|
|
131
124
|
}
|
|
132
125
|
return { urls, failures };
|
|
133
126
|
}
|
|
134
|
-
export async function prepareCloudReportSnapshot(facts, root) {
|
|
127
|
+
export async function prepareCloudReportSnapshot(facts, root, qaReportRoot) {
|
|
135
128
|
if (!facts.enabled)
|
|
136
129
|
return null;
|
|
137
|
-
return snapshotReportFiles(root);
|
|
130
|
+
return snapshotReportFiles(root, qaReportRoot);
|
|
138
131
|
}
|
|
139
132
|
export async function prepareCloudReportExecution(input) {
|
|
140
133
|
const facts = cloudReportUploadFacts(input.env);
|
|
@@ -147,7 +140,7 @@ export async function prepareCloudReportExecution(input) {
|
|
|
147
140
|
return { facts, snapshot: null };
|
|
148
141
|
}
|
|
149
142
|
try {
|
|
150
|
-
return { facts, snapshot: await prepareCloudReportSnapshot(facts, input.root) };
|
|
143
|
+
return { facts, snapshot: await prepareCloudReportSnapshot(facts, input.root, input.qaReportRoot) };
|
|
151
144
|
}
|
|
152
145
|
catch (error) {
|
|
153
146
|
dslog("cloud_report_upload.snapshot_failed", "报告文件快照失败", {
|
|
@@ -177,6 +170,7 @@ export async function deliverCloudReports(input) {
|
|
|
177
170
|
const result = await (input.upload ?? uploadCloudReports)({
|
|
178
171
|
script: input.facts.ossUploadScript,
|
|
179
172
|
root: input.root,
|
|
173
|
+
...(input.qaReportRoot === undefined ? {} : { qaReportRoot: input.qaReportRoot }),
|
|
180
174
|
before: input.before,
|
|
181
175
|
});
|
|
182
176
|
if (result.failures.length > 0) {
|
|
@@ -185,6 +179,14 @@ export async function deliverCloudReports(input) {
|
|
|
185
179
|
uploaded_count: result.urls.length, failed_count: result.failures.length,
|
|
186
180
|
});
|
|
187
181
|
}
|
|
182
|
+
else {
|
|
183
|
+
dslog(result.urls.length === 0 ? "cloud_report_upload.no_files" : "cloud_report_upload.completed", result.urls.length === 0 ? "未发现新增或变化的报告文件" : "报告上传完成", {
|
|
184
|
+
execution_id: input.executionId, agent_handle: input.agentHandle,
|
|
185
|
+
uploaded_count: result.urls.length,
|
|
186
|
+
report_root: input.root,
|
|
187
|
+
qa_report_root: input.qaReportRoot ?? join(input.root, "qa-report-server-v2"),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
188
190
|
return {
|
|
189
191
|
finalText: input.captureFinal && result.urls.length > 0
|
|
190
192
|
? appendReportLinks(input.finalText ?? "", result.urls)
|
package/dist/execution-runner.js
CHANGED
|
@@ -25,6 +25,7 @@ import { PROJECT_WORKSPACES_CAPABILITY } from "./machine-info.js";
|
|
|
25
25
|
import { PROJECT_SKILL_PROJECTION_V2_CAPABILITY } from "./project-skills/types.js";
|
|
26
26
|
import { dslog } from "./slog.js";
|
|
27
27
|
import { identityRosterPromptFacts } from "./identity-roster-telemetry.js";
|
|
28
|
+
import { renderLocalMemoryV2WriteGuard } from "./local-memory/runtime.js";
|
|
28
29
|
export { supervisorLaunch } from "./supervised-runtime.js";
|
|
29
30
|
const MAX_LOCAL_EXECUTION_ATTEMPTS = 3;
|
|
30
31
|
const ACTIVITY_KIND = {
|
|
@@ -828,7 +829,8 @@ export async function runExecution(config, input, dependencies) {
|
|
|
828
829
|
const baseSystemPromptBudget = spec.agent.abilityRelease === undefined
|
|
829
830
|
? systemPromptBudget
|
|
830
831
|
: Math.max(0, Math.min(systemPromptBudget, Math.max(32 * 1024, systemPromptBudget - 168 * 1024)));
|
|
831
|
-
const
|
|
832
|
+
const localMemoryV2Enabled = config.localMemoryV2Handles?.has(spec.agent.handle) === true;
|
|
833
|
+
const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${localMemoryV2Enabled ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
|
|
832
834
|
...(selfTraining ? { isolateLocalMemory: true } : {}),
|
|
833
835
|
collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
|
|
834
836
|
});
|
|
@@ -887,7 +889,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
887
889
|
maxTurns: config.sessionMaxTurns,
|
|
888
890
|
},
|
|
889
891
|
...(selfTraining ? { maintenancePurpose: "self_training" } : {}),
|
|
890
|
-
localMemoryV2Enabled
|
|
892
|
+
localMemoryV2Enabled,
|
|
891
893
|
};
|
|
892
894
|
const result = await Promise.race([
|
|
893
895
|
execute(localInput, callbacks, localDependencies),
|
package/dist/external-output.js
CHANGED
|
@@ -1,15 +1,104 @@
|
|
|
1
1
|
export const EXTERNAL_ANSWER_OPEN = "<nowwork_external_answer>";
|
|
2
2
|
export const EXTERNAL_ANSWER_CLOSE = "</nowwork_external_answer>";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const MAX_MARKER_TAG_LENGTH = 96;
|
|
4
|
+
const KNOWN_EXTERNAL_ANSWER_TAG = /^now+orkexternalanswer$/u;
|
|
5
|
+
const CANONICAL_EXTERNAL_ANSWER_TAG = "noworkexternalanswer";
|
|
6
|
+
function editDistanceWithin(value, target, maximum) {
|
|
7
|
+
if (Math.abs(value.length - target.length) > maximum)
|
|
8
|
+
return false;
|
|
9
|
+
let previous = Array.from({ length: target.length + 1 }, (_, index) => index);
|
|
10
|
+
for (let row = 1; row <= value.length; row += 1) {
|
|
11
|
+
const current = [row];
|
|
12
|
+
let rowMinimum = row;
|
|
13
|
+
for (let column = 1; column <= target.length; column += 1) {
|
|
14
|
+
const cost = value[row - 1] === target[column - 1] ? 0 : 1;
|
|
15
|
+
const distance = Math.min(previous[column] + 1, current[column - 1] + 1, previous[column - 1] + cost);
|
|
16
|
+
current.push(distance);
|
|
17
|
+
rowMinimum = Math.min(rowMinimum, distance);
|
|
18
|
+
}
|
|
19
|
+
if (rowMinimum > maximum)
|
|
20
|
+
return false;
|
|
21
|
+
previous = current;
|
|
22
|
+
}
|
|
23
|
+
return previous[target.length] <= maximum;
|
|
24
|
+
}
|
|
25
|
+
function resemblesExternalAnswerTag(name) {
|
|
26
|
+
if (name.includes("externalanswer"))
|
|
27
|
+
return true;
|
|
28
|
+
const canonicalized = name.replace(/^now+ork/u, "nowork");
|
|
29
|
+
return editDistanceWithin(canonicalized, CANONICAL_EXTERNAL_ANSWER_TAG, 2);
|
|
30
|
+
}
|
|
31
|
+
function classifyMarkerTag(body) {
|
|
32
|
+
const trimmed = body.trim();
|
|
33
|
+
const closing = trimmed.startsWith("/");
|
|
34
|
+
const name = (closing ? trimmed.slice(1) : trimmed)
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[_\-\s]/gu, "");
|
|
38
|
+
if (KNOWN_EXTERNAL_ANSWER_TAG.test(name))
|
|
39
|
+
return { closing, kind: "known" };
|
|
40
|
+
return {
|
|
41
|
+
closing,
|
|
42
|
+
kind: resemblesExternalAnswerTag(name) ? "suspicious" : "other",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function nextMarkerTag(value, from) {
|
|
46
|
+
let cursor = from;
|
|
47
|
+
while (cursor < value.length) {
|
|
48
|
+
const start = value.indexOf("<", cursor);
|
|
49
|
+
if (start < 0)
|
|
50
|
+
return null;
|
|
51
|
+
const end = value.indexOf(">", start + 1);
|
|
52
|
+
if (end < 0)
|
|
53
|
+
return null;
|
|
54
|
+
const body = value.slice(start + 1, end);
|
|
55
|
+
const classified = classifyMarkerTag(body);
|
|
56
|
+
if (classified.kind === "suspicious"
|
|
57
|
+
|| (classified.kind === "known" && body.length > MAX_MARKER_TAG_LENGTH)) {
|
|
58
|
+
return { start, end: end + 1, closing: classified.closing, kind: "suspicious" };
|
|
59
|
+
}
|
|
60
|
+
if (classified.kind === "known") {
|
|
61
|
+
return { start, end: end + 1, closing: classified.closing, kind: "known" };
|
|
62
|
+
}
|
|
63
|
+
cursor = end + 1;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function parseExternalAnswerText(value) {
|
|
68
|
+
const sections = [];
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
let contentStart = 0;
|
|
71
|
+
let inside = false;
|
|
72
|
+
let found = false;
|
|
73
|
+
for (;;) {
|
|
74
|
+
const tag = nextMarkerTag(value, cursor);
|
|
75
|
+
if (!tag)
|
|
76
|
+
break;
|
|
77
|
+
if (tag.kind === "suspicious")
|
|
78
|
+
return { kind: "rejected", text: "" };
|
|
79
|
+
if (tag.closing) {
|
|
80
|
+
if (!inside)
|
|
81
|
+
return { kind: "rejected", text: "" };
|
|
82
|
+
sections.push(value.slice(contentStart, tag.start));
|
|
83
|
+
inside = false;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (inside)
|
|
87
|
+
return { kind: "rejected", text: "" };
|
|
88
|
+
found = true;
|
|
89
|
+
inside = true;
|
|
90
|
+
contentStart = tag.end;
|
|
91
|
+
}
|
|
92
|
+
cursor = tag.end;
|
|
8
93
|
}
|
|
9
|
-
|
|
94
|
+
if (inside)
|
|
95
|
+
sections.push(value.slice(contentStart));
|
|
96
|
+
if (!found)
|
|
97
|
+
return { kind: "plain", text: value.trim() };
|
|
98
|
+
return { kind: "marked", text: sections.join("").trim() };
|
|
10
99
|
}
|
|
11
100
|
/**
|
|
12
|
-
* 跨 chunk
|
|
101
|
+
* 跨 chunk 的契约标签解码器:push() 在合法闭合后返回完整标签块(供 answerStream 转发);
|
|
13
102
|
* 同时记住整轮里**最后一个完整闭合**的标签块(`lastClosedBlock`),供完结时作为契约答案——
|
|
14
103
|
* 只有闭合了的块才算数:半截块(被工具调用拆开的前半段)与叙述里提到的开标签
|
|
15
104
|
* ("我会把结论包在 <marker> 里,先去跑测试")都不会被当成答案(review of a027bf15, C1)。
|
|
@@ -40,34 +129,63 @@ export class ExternalAnswerDecoder {
|
|
|
40
129
|
}
|
|
41
130
|
consume(value, output) {
|
|
42
131
|
this.pending = "";
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
132
|
+
const appendInside = (text) => {
|
|
133
|
+
if (!text)
|
|
134
|
+
return;
|
|
135
|
+
this.block += text;
|
|
136
|
+
};
|
|
137
|
+
for (const character of value) {
|
|
138
|
+
if (this.pending) {
|
|
139
|
+
this.pending += character;
|
|
140
|
+
if (character === ">") {
|
|
141
|
+
const body = this.pending.slice(1, -1);
|
|
142
|
+
const tag = classifyMarkerTag(body);
|
|
143
|
+
if (tag.kind === "known" && body.length <= MAX_MARKER_TAG_LENGTH) {
|
|
144
|
+
if (tag.closing && this.state === "inside") {
|
|
145
|
+
const completed = this.block.trim();
|
|
146
|
+
if (completed) {
|
|
147
|
+
this.closed = completed;
|
|
148
|
+
output.push(completed);
|
|
149
|
+
}
|
|
150
|
+
this.block = "";
|
|
151
|
+
this.state = "outside";
|
|
152
|
+
}
|
|
153
|
+
else if (!tag.closing && this.state === "outside") {
|
|
154
|
+
this.block = "";
|
|
155
|
+
this.state = "inside";
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.block = "";
|
|
159
|
+
this.state = tag.closing ? "outside" : "inside";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (tag.kind === "suspicious" || tag.kind === "known") {
|
|
163
|
+
this.block = "";
|
|
164
|
+
this.state = "outside";
|
|
165
|
+
}
|
|
166
|
+
else if (this.state === "inside") {
|
|
167
|
+
appendInside(this.pending);
|
|
168
|
+
}
|
|
169
|
+
this.pending = "";
|
|
170
|
+
}
|
|
171
|
+
else if (this.pending.length > MAX_MARKER_TAG_LENGTH + 2) {
|
|
172
|
+
const tag = classifyMarkerTag(this.pending.slice(1));
|
|
173
|
+
if (tag.kind === "suspicious" || tag.kind === "known") {
|
|
174
|
+
this.block = "";
|
|
175
|
+
this.state = "outside";
|
|
176
|
+
}
|
|
177
|
+
else if (this.state === "inside") {
|
|
178
|
+
appendInside(this.pending);
|
|
179
|
+
}
|
|
180
|
+
this.pending = "";
|
|
181
|
+
}
|
|
55
182
|
}
|
|
56
|
-
else {
|
|
57
|
-
this.
|
|
58
|
-
|
|
183
|
+
else if (character === "<") {
|
|
184
|
+
this.pending = character;
|
|
185
|
+
}
|
|
186
|
+
else if (this.state === "inside") {
|
|
187
|
+
appendInside(character);
|
|
59
188
|
}
|
|
60
|
-
const remaining = value.slice(markerAt + marker.length);
|
|
61
|
-
if (remaining)
|
|
62
|
-
this.consume(remaining, output);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const retained = retainedMarkerPrefix(value, marker);
|
|
66
|
-
const safe = retained > 0 ? value.slice(0, -retained) : value;
|
|
67
|
-
this.pending = retained > 0 ? value.slice(-retained) : "";
|
|
68
|
-
if (this.state === "inside" && safe) {
|
|
69
|
-
output.push(safe);
|
|
70
|
-
this.block += safe;
|
|
71
189
|
}
|
|
72
190
|
}
|
|
73
191
|
}
|
|
@@ -104,20 +222,7 @@ export function decodeExternalOutputEvent(runtime, event, decoder) {
|
|
|
104
222
|
: [];
|
|
105
223
|
}
|
|
106
224
|
export function stripExternalAnswerMarkers(value) {
|
|
107
|
-
|
|
108
|
-
let cursor = 0;
|
|
109
|
-
while (cursor < value.length) {
|
|
110
|
-
const openAt = value.indexOf(EXTERNAL_ANSWER_OPEN, cursor);
|
|
111
|
-
if (openAt < 0)
|
|
112
|
-
break;
|
|
113
|
-
const contentAt = openAt + EXTERNAL_ANSWER_OPEN.length;
|
|
114
|
-
const closeAt = value.indexOf(EXTERNAL_ANSWER_CLOSE, contentAt);
|
|
115
|
-
if (closeAt < 0)
|
|
116
|
-
return value.slice(contentAt).trim();
|
|
117
|
-
sections.push(value.slice(contentAt, closeAt));
|
|
118
|
-
cursor = closeAt + EXTERNAL_ANSWER_CLOSE.length;
|
|
119
|
-
}
|
|
120
|
-
return (sections.length > 0 ? sections.join("") : value).trim();
|
|
225
|
+
return parseExternalAnswerText(value).text;
|
|
121
226
|
}
|
|
122
227
|
/**
|
|
123
228
|
* 提取 marker 内容:有 marker 返回拼接内容(trim,空→null),无 marker 返回 null。
|
|
@@ -125,25 +230,6 @@ export function stripExternalAnswerMarkers(value) {
|
|
|
125
230
|
* 本函数用于判定"agent 是否给出了频道直接回复"——必须能区分有无 marker。
|
|
126
231
|
*/
|
|
127
232
|
export function extractExternalAnswer(value) {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
let rest = value;
|
|
131
|
-
for (;;) {
|
|
132
|
-
const open = rest.indexOf(EXTERNAL_ANSWER_OPEN);
|
|
133
|
-
if (open < 0)
|
|
134
|
-
break;
|
|
135
|
-
found = true;
|
|
136
|
-
const afterOpen = rest.slice(open + EXTERNAL_ANSWER_OPEN.length);
|
|
137
|
-
const close = afterOpen.indexOf(EXTERNAL_ANSWER_CLOSE);
|
|
138
|
-
if (close < 0) {
|
|
139
|
-
parts.push(afterOpen);
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
parts.push(afterOpen.slice(0, close));
|
|
143
|
-
rest = afterOpen.slice(close + EXTERNAL_ANSWER_CLOSE.length);
|
|
144
|
-
}
|
|
145
|
-
if (!found)
|
|
146
|
-
return null;
|
|
147
|
-
const answer = parts.join("").trim();
|
|
148
|
-
return answer.length > 0 ? answer : null;
|
|
233
|
+
const parsed = parseExternalAnswerText(value);
|
|
234
|
+
return parsed.kind === "marked" && parsed.text ? parsed.text : null;
|
|
149
235
|
}
|
package/dist/local-executor.js
CHANGED
|
@@ -37,6 +37,7 @@ 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
39
|
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, renderLocalMemoryResultInstructions, } from "./local-memory/runtime.js";
|
|
40
|
+
import { renderPendingMemoryContext, searchPendingMemoryCandidates, } from "./local-memory/pending.js";
|
|
40
41
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
41
42
|
constructor(runtime, timeoutMs) {
|
|
42
43
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -253,7 +254,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
253
254
|
let executionWorkspace = workspace;
|
|
254
255
|
let localMemoryTelemetry = null;
|
|
255
256
|
const inheritedEnv = { ...process.env };
|
|
256
|
-
const
|
|
257
|
+
const reportRoots = {
|
|
258
|
+
root: workspace.dir,
|
|
259
|
+
...(input.launch.qaReportRoot === undefined ? {} : { qaReportRoot: input.launch.qaReportRoot }),
|
|
260
|
+
};
|
|
261
|
+
const cloudReportPreparation = await prepareCloudReportExecution({ env: inheritedEnv, ...reportRoots, executionId: input.executionId, agentHandle: input.handle });
|
|
257
262
|
const { facts: cloudReportUpload, snapshot: reportSnapshot } = cloudReportPreparation;
|
|
258
263
|
for (const key of Object.keys(inheritedEnv)) {
|
|
259
264
|
if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
|
|
@@ -381,6 +386,26 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
381
386
|
}
|
|
382
387
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
383
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
|
+
}
|
|
384
409
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
385
410
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
386
411
|
&& isLocalMemoryV2Eligible({
|
|
@@ -392,14 +417,21 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
392
417
|
})) {
|
|
393
418
|
localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
|
|
394
419
|
}
|
|
395
|
-
const
|
|
420
|
+
const promptWithoutPending = resolvePrompt(input.systemPrompt, promptContext);
|
|
421
|
+
let baseSystemPrompt = `${promptWithoutPending}${pendingMemoryContext}`;
|
|
396
422
|
const structuredMemoryInstructions = localMemoryResultPath === null
|
|
397
423
|
? null
|
|
398
424
|
: renderLocalMemoryResultInstructions(localMemoryResultPath);
|
|
399
|
-
const
|
|
425
|
+
const fitsPromptBudget = (prompt) => structuredMemoryInstructions === null
|
|
400
426
|
|| input.maxSystemPromptBytes === undefined
|
|
401
|
-
|| Buffer.byteLength(
|
|
427
|
+
|| Buffer.byteLength(prompt, "utf8") + Buffer.byteLength(structuredMemoryInstructions, "utf8")
|
|
402
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);
|
|
403
435
|
if (!structuredPromptFits)
|
|
404
436
|
localMemoryResultPath = null;
|
|
405
437
|
const systemPrompt = localMemoryResultPath === null
|
|
@@ -990,7 +1022,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
990
1022
|
}
|
|
991
1023
|
const cloudReportResult = exitCode === 0
|
|
992
1024
|
? await deliverCloudReports({
|
|
993
|
-
facts: cloudReportUpload,
|
|
1025
|
+
facts: cloudReportUpload, ...reportRoots, before: reportSnapshot, finalText,
|
|
994
1026
|
externalAnswer: externalOutput.lastClosedBlock
|
|
995
1027
|
?? (finalText === null ? null : extractExternalAnswer(finalText)),
|
|
996
1028
|
captureFinal: input.captureFinal, executionId: input.executionId, agentHandle: input.handle,
|
|
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { lstat, mkdir, readdir, realpath, rename, rm, writeFile, readFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
export const LOCAL_MEMORY_PENDING_SCAN_FILE_CAP = 128;
|
|
6
|
+
export const LOCAL_MEMORY_PENDING_SCAN_BYTE_CAP = 1_000_000;
|
|
5
7
|
const CandidateSchema = z.object({
|
|
6
8
|
operation: z.enum(["create", "update", "supersede"]),
|
|
7
9
|
memoryId: z.string().nullable(),
|
|
@@ -59,6 +61,44 @@ async function pendingDirectory(agentDir) {
|
|
|
59
61
|
}
|
|
60
62
|
return directory;
|
|
61
63
|
}
|
|
64
|
+
async function existingPendingDirectory(agentDir) {
|
|
65
|
+
const agentRoot = await realpath(agentDir);
|
|
66
|
+
const memoryRoot = resolve(agentRoot, ".crew", "memory");
|
|
67
|
+
let memoryStat;
|
|
68
|
+
try {
|
|
69
|
+
memoryStat = await lstat(memoryRoot);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code === "ENOENT")
|
|
73
|
+
return null;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (!memoryStat.isDirectory() || memoryStat.isSymbolicLink()) {
|
|
77
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
78
|
+
}
|
|
79
|
+
const resolvedMemoryRoot = await realpath(memoryRoot);
|
|
80
|
+
if (resolvedMemoryRoot !== memoryRoot || !within(agentRoot, resolvedMemoryRoot)) {
|
|
81
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
82
|
+
}
|
|
83
|
+
const directory = join(memoryRoot, "pending");
|
|
84
|
+
let stat;
|
|
85
|
+
try {
|
|
86
|
+
stat = await lstat(directory);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error.code === "ENOENT")
|
|
90
|
+
return null;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
94
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
95
|
+
}
|
|
96
|
+
const resolvedDirectory = await realpath(directory);
|
|
97
|
+
if (resolvedDirectory !== directory || !within(agentRoot, resolvedDirectory)) {
|
|
98
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
99
|
+
}
|
|
100
|
+
return directory;
|
|
101
|
+
}
|
|
62
102
|
async function assertRegularFile(path) {
|
|
63
103
|
try {
|
|
64
104
|
const stat = await lstat(path);
|
|
@@ -91,15 +131,40 @@ export async function persistPendingMemoryCandidate(input) {
|
|
|
91
131
|
await atomicWrite(target, `${JSON.stringify(parsed.data, null, 2)}\n`);
|
|
92
132
|
return target;
|
|
93
133
|
}
|
|
134
|
+
export async function removePendingMemoryCandidate(input) {
|
|
135
|
+
const directory = await existingPendingDirectory(input.agentDir);
|
|
136
|
+
if (directory === null)
|
|
137
|
+
return;
|
|
138
|
+
const target = join(directory, `${input.pendingId}.json`);
|
|
139
|
+
await assertRegularFile(target);
|
|
140
|
+
await rm(target, { force: true });
|
|
141
|
+
}
|
|
94
142
|
export async function readPendingMemoryCandidates(input) {
|
|
95
|
-
const directory = await
|
|
143
|
+
const directory = await existingPendingDirectory(input.agentDir);
|
|
144
|
+
if (directory === null)
|
|
145
|
+
return [];
|
|
96
146
|
const names = (await readdir(directory)).filter((name) => name.endsWith(".json")).sort();
|
|
147
|
+
const maxFiles = Math.max(0, input.maxFiles ?? LOCAL_MEMORY_PENDING_SCAN_FILE_CAP);
|
|
148
|
+
const maxBytes = Math.max(0, input.maxBytes ?? LOCAL_MEMORY_PENDING_SCAN_BYTE_CAP);
|
|
97
149
|
const result = [];
|
|
150
|
+
let scannedBytes = 0;
|
|
98
151
|
for (const name of names) {
|
|
152
|
+
if (result.length >= maxFiles || scannedBytes >= maxBytes)
|
|
153
|
+
break;
|
|
99
154
|
const path = join(directory, name);
|
|
100
155
|
try {
|
|
101
156
|
await assertRegularFile(path);
|
|
102
|
-
const
|
|
157
|
+
const stat = await lstat(path);
|
|
158
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
159
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
160
|
+
if (scannedBytes + stat.size > maxBytes)
|
|
161
|
+
break;
|
|
162
|
+
const raw = await readFile(path, "utf8");
|
|
163
|
+
const size = Buffer.byteLength(raw, "utf8");
|
|
164
|
+
if (scannedBytes + size > maxBytes)
|
|
165
|
+
break;
|
|
166
|
+
scannedBytes += size;
|
|
167
|
+
const parsed = PendingSchema.safeParse(JSON.parse(raw));
|
|
103
168
|
if (parsed.success && `${parsed.data.pendingId}.json` === name)
|
|
104
169
|
result.push(parsed.data);
|
|
105
170
|
}
|
|
@@ -109,3 +174,72 @@ export async function readPendingMemoryCandidates(input) {
|
|
|
109
174
|
}
|
|
110
175
|
return result;
|
|
111
176
|
}
|
|
177
|
+
function normalize(value) {
|
|
178
|
+
return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
|
|
179
|
+
}
|
|
180
|
+
function priorityRank(priority) {
|
|
181
|
+
return priority === "high" ? 0 : priority === "medium" ? 1 : 2;
|
|
182
|
+
}
|
|
183
|
+
function candidateText(candidate) {
|
|
184
|
+
return [
|
|
185
|
+
candidate.candidate.title,
|
|
186
|
+
candidate.candidate.summary,
|
|
187
|
+
...candidate.candidate.triggers,
|
|
188
|
+
candidate.candidate.detailCategory,
|
|
189
|
+
candidate.candidate.detailBody,
|
|
190
|
+
].join("\n");
|
|
191
|
+
}
|
|
192
|
+
function recallScore(candidate, query) {
|
|
193
|
+
const normalizedQuery = normalize(query);
|
|
194
|
+
if (normalizedQuery === "")
|
|
195
|
+
return 1;
|
|
196
|
+
const text = normalize(candidateText(candidate));
|
|
197
|
+
if (text.includes(normalizedQuery))
|
|
198
|
+
return 100;
|
|
199
|
+
const terms = normalizedQuery.split(/[\s,,、。!?;::()[\]{}"'`]+/u).filter((term) => term.length > 0);
|
|
200
|
+
return terms.reduce((score, term) => score + (text.includes(term) ? 1 : 0), 0);
|
|
201
|
+
}
|
|
202
|
+
export async function searchPendingMemoryCandidates(input) {
|
|
203
|
+
const candidates = await readPendingMemoryCandidates({ agentDir: input.agentDir });
|
|
204
|
+
const limit = Math.max(0, input.limit ?? 5);
|
|
205
|
+
if (limit === 0)
|
|
206
|
+
return [];
|
|
207
|
+
return candidates
|
|
208
|
+
.map((candidate) => ({ candidate, score: recallScore(candidate, input.query) }))
|
|
209
|
+
.filter((item) => normalize(input.query) === "" || item.score > 0)
|
|
210
|
+
.sort((left, right) => right.score - left.score
|
|
211
|
+
|| priorityRank(left.candidate.candidate.priority) - priorityRank(right.candidate.candidate.priority)
|
|
212
|
+
|| right.candidate.createdAt.localeCompare(left.candidate.createdAt)
|
|
213
|
+
|| left.candidate.pendingId.localeCompare(right.candidate.pendingId))
|
|
214
|
+
.slice(0, limit)
|
|
215
|
+
.map((item) => item.candidate);
|
|
216
|
+
}
|
|
217
|
+
function truncateCodePoints(value, maxCodePoints) {
|
|
218
|
+
return Array.from(value).slice(0, Math.max(0, maxCodePoints)).join("");
|
|
219
|
+
}
|
|
220
|
+
export function renderPendingMemoryContext(candidates, maxCodePoints = 2_000) {
|
|
221
|
+
if (candidates.length === 0 || maxCodePoints <= 0)
|
|
222
|
+
return "";
|
|
223
|
+
const header = `\n\n## Archived memory candidates (daemon-managed)\n这些记忆已持久化,但尚未进入常驻热索引;只在当前请求相关时参考,必要时再由 memory-prune 提升。\n`;
|
|
224
|
+
let remaining = maxCodePoints - Array.from(header).length;
|
|
225
|
+
if (remaining <= 0)
|
|
226
|
+
return truncateCodePoints(header, maxCodePoints);
|
|
227
|
+
const sections = [];
|
|
228
|
+
for (const item of candidates) {
|
|
229
|
+
const change = item.candidate;
|
|
230
|
+
const prefix = `\n### ${change.title}\n`
|
|
231
|
+
+ `- pending_file: .crew/memory/pending/${item.pendingId}.json\n`
|
|
232
|
+
+ `- summary: ${change.summary}\n`
|
|
233
|
+
+ `- triggers: ${change.triggers.join(", ")}\n`
|
|
234
|
+
+ "- detail: ";
|
|
235
|
+
const prefixLength = Array.from(prefix).length;
|
|
236
|
+
if (prefixLength + 1 > remaining)
|
|
237
|
+
break;
|
|
238
|
+
const detail = truncateCodePoints(change.detailBody, remaining - prefixLength - 1);
|
|
239
|
+
const section = `${prefix}${detail}\n`;
|
|
240
|
+
const length = Array.from(section).length;
|
|
241
|
+
sections.push(section);
|
|
242
|
+
remaining -= length;
|
|
243
|
+
}
|
|
244
|
+
return `${header}${sections.join("")}`;
|
|
245
|
+
}
|
|
@@ -57,9 +57,16 @@ ${resultPath}
|
|
|
57
57
|
JSON 顶层必须包含:
|
|
58
58
|
{"schemaVersion":1,"purpose":"memory_prune","baseCatalogRevision":0,"outcome":"no_change","changes":[],"statusChanges":[]}
|
|
59
59
|
每个 changes 项使用 operation=create|update|supersede、title、kind、summary、triggers、priority、detailCategory、detailBody、reviewAfter;create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null,update/supersede 按现有索引填写版本和 id。
|
|
60
|
-
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending
|
|
60
|
+
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中;后续相关任务会按需召回,索引腾出空间后可自动提升。
|
|
61
61
|
`;
|
|
62
62
|
}
|
|
63
|
+
/** Prevent ordinary V2 turns from racing the daemon-managed memory writer. */
|
|
64
|
+
export function renderLocalMemoryV2WriteGuard() {
|
|
65
|
+
return `
|
|
66
|
+
|
|
67
|
+
## V2 shared memory write safety
|
|
68
|
+
本节覆盖前文关于普通任务直接更新共享记忆的例外规则。本 Agent 已启用结构化本地记忆 V2。普通任务不得直接编辑共享的 $CREW_HOME/MEMORY.md 或 $CREW_HOME/notes/ 文件;即使用户明确要求记住内容,也只把候选、用户授权和分类理由记录到本任务的 $CREW_TASK_LOG,留给任务结束后的 memory-prune 结构化收尾应用。只有 memory-prune 收尾提示明确要求写入 daemon 提供的 result 文件时,才按该协议提交候选。`;
|
|
69
|
+
}
|
|
63
70
|
export async function commitLocalMemoryResultAfterExit(input) {
|
|
64
71
|
if (input.exitCode !== 0)
|
|
65
72
|
return { status: "skipped" };
|