@nowcrew/daemon 0.6.88-beta.24 → 0.6.88-beta.26
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/agent-ability/controller.js +11 -1
- package/dist/agent-ability/git-errors.js +22 -0
- package/dist/agent-ability/resolver.js +12 -4
- package/dist/config.js +24 -4
- package/dist/execution-runner.js +9 -6
- package/dist/local-executor.js +13 -2
- package/dist/local-memory/inventory-preview.js +9 -4
- package/dist/local-memory/inventory-source.js +3 -3
- package/dist/local-memory/prompt.js +9 -4
- package/dist/local-memory/rollout.js +82 -0
- package/dist/runner.js +3 -3
- package/dist/workspace.js +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,17 @@ import { resolveAbilityRelease } from "./resolver.js";
|
|
|
3
3
|
export const normalizeAbilityCommandError = (error, fallback) => {
|
|
4
4
|
const message = error instanceof Error ? error.message : fallback;
|
|
5
5
|
const structured = message.match(/^(ability_[a-z0-9_]+)(?::|$)/u)?.[1];
|
|
6
|
-
|
|
6
|
+
if (structured)
|
|
7
|
+
return structured;
|
|
8
|
+
if (error instanceof Error && "code" in error) {
|
|
9
|
+
if (error.code === "ENOSPC")
|
|
10
|
+
return "ability_disk_full";
|
|
11
|
+
if (error.code === "EACCES" || error.code === "EPERM")
|
|
12
|
+
return "ability_filesystem_permission_denied";
|
|
13
|
+
if (error.code === "ENOENT")
|
|
14
|
+
return "ability_local_file_missing";
|
|
15
|
+
}
|
|
16
|
+
return fallback;
|
|
7
17
|
};
|
|
8
18
|
export function createAgentAbilityController(deps) {
|
|
9
19
|
const resolver = deps.resolve ?? resolveAbilityRelease;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Classify Git failures without returning command arguments or raw stderr. */
|
|
2
|
+
export function abilityGitErrorCode(error) {
|
|
3
|
+
if (!(error instanceof Error))
|
|
4
|
+
return "ability_git_failed";
|
|
5
|
+
if ("code" in error && error.code === "ENOENT")
|
|
6
|
+
return "ability_git_unavailable";
|
|
7
|
+
if ("killed" in error && error.killed === true)
|
|
8
|
+
return "ability_git_timeout";
|
|
9
|
+
const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr : "";
|
|
10
|
+
if (/no space left on device/i.test(stderr))
|
|
11
|
+
return "ability_disk_full";
|
|
12
|
+
if (/host key verification failed|remote host identification has changed/i.test(stderr)) {
|
|
13
|
+
return "ability_git_host_verification_failed";
|
|
14
|
+
}
|
|
15
|
+
if (/permission denied \(publickey|authentication failed|could not read username|repository not found|access denied/i.test(stderr)) {
|
|
16
|
+
return "ability_git_access_denied";
|
|
17
|
+
}
|
|
18
|
+
if (/could not resolve host|could not resolve hostname|connection timed out|connection refused|network is unreachable|could not connect to server/i.test(stderr)) {
|
|
19
|
+
return "ability_git_network_failed";
|
|
20
|
+
}
|
|
21
|
+
return "ability_git_failed";
|
|
22
|
+
}
|
|
@@ -7,6 +7,7 @@ import { parse } from "yaml";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
|
|
9
9
|
import { PROJECT_MANIFEST_SOURCE_PATH, ProjectDependencyManifestSchema, } from "../project-dependency-manifest.js";
|
|
10
|
+
import { abilityGitErrorCode } from "./git-errors.js";
|
|
10
11
|
import { normalizeRepositoryIdentity } from "../repository-identity.js";
|
|
11
12
|
const runFile = promisify(execFile);
|
|
12
13
|
const MAX_FILES = 5_000;
|
|
@@ -42,6 +43,7 @@ const gitEnv = () => ({
|
|
|
42
43
|
GIT_CONFIG_NOSYSTEM: "1",
|
|
43
44
|
GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
|
|
44
45
|
GIT_TERMINAL_PROMPT: "0",
|
|
46
|
+
LC_ALL: "C",
|
|
45
47
|
});
|
|
46
48
|
export const abilitySha256 = (content) => `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
47
49
|
export const safeAbilityName = (value) => value.replace(/[^A-Za-z0-9._-]/gu, "_").slice(0, 128);
|
|
@@ -73,10 +75,16 @@ const comparePosixPaths = (left, right) => {
|
|
|
73
75
|
return leftParts.length - rightParts.length;
|
|
74
76
|
};
|
|
75
77
|
const git = async (args, cwd) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
try {
|
|
79
|
+
const result = await runFile("git", [...GIT_ARGS, ...args], {
|
|
80
|
+
...(cwd ? { cwd } : {}), env: gitEnv(), timeout: 60_000, maxBuffer: 16 * 1024 * 1024,
|
|
81
|
+
});
|
|
82
|
+
return result.stdout.trim();
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
// 仅回传固定错误分类,不把 Git stderr 中的凭据、仓库地址或本地路径发到界面。
|
|
86
|
+
throw new Error(abilityGitErrorCode(error));
|
|
87
|
+
}
|
|
80
88
|
};
|
|
81
89
|
export const parseAbilityRefLine = (line) => {
|
|
82
90
|
const [branch, commit, rawTime, ...extra] = line.split("\t");
|
package/dist/config.js
CHANGED
|
@@ -76,12 +76,32 @@ function defaultCliPath() {
|
|
|
76
76
|
return resolve(here, "../../cli/dist/main.js");
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
//
|
|
80
|
-
// 测试完成后删除此常量,恢复未配置时默认关闭的行为。
|
|
79
|
+
// 保留本地开发的原默认名单;远端 dev 全量不再依赖该集合。
|
|
81
80
|
const TEMP_LOCAL_MEMORY_V2_TEST_HANDLES = [
|
|
82
|
-
"94533bf3-b5b7-4a1e-9c41-6a1ab0a0e1ea", // memory-v2-test-
|
|
83
|
-
"967ab9ce-724c-4f22-a664-2f855cd798af", // memory-v2-test-
|
|
81
|
+
"94533bf3-b5b7-4a1e-9c41-6a1ab0a0e1ea", // memory-v2-test-b
|
|
82
|
+
"967ab9ce-724c-4f22-a664-2f855cd798af", // memory-v2-test-a
|
|
84
83
|
];
|
|
84
|
+
export function localMemoryV2Environment(serverUrl) {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(serverUrl);
|
|
87
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
88
|
+
return "custom";
|
|
89
|
+
const hostname = url.hostname.toLowerCase().replace(/\.$/u, "");
|
|
90
|
+
if (hostname === "nowwork-dev.nowcoder.com")
|
|
91
|
+
return "dev";
|
|
92
|
+
if (hostname === "nowwork-in.nowcoder.com" || hostname === "nowwork.nowcoder.com")
|
|
93
|
+
return "production";
|
|
94
|
+
}
|
|
95
|
+
catch { /* 其它地址保留原配置行为。 */ }
|
|
96
|
+
return "custom";
|
|
97
|
+
}
|
|
98
|
+
/** 环境资格不代表旧格式已兼容;实际格式由本轮工作区准备阶段核对。 */
|
|
99
|
+
export function isLocalMemoryV2Enabled(config, handle) {
|
|
100
|
+
const environment = localMemoryV2Environment(config.serverUrl);
|
|
101
|
+
if (environment !== "custom")
|
|
102
|
+
return environment === "dev";
|
|
103
|
+
return config.localMemoryV2Handles?.has(handle) === true;
|
|
104
|
+
}
|
|
85
105
|
function isTemporaryDevServer(serverUrl) {
|
|
86
106
|
try {
|
|
87
107
|
const hostname = new URL(serverUrl).hostname.toLowerCase();
|
package/dist/execution-runner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { resolveReposRoot } from "./config.js";
|
|
3
|
+
import { isLocalMemoryV2Enabled, localMemoryV2Environment, resolveReposRoot } from "./config.js";
|
|
4
4
|
import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
|
|
5
5
|
import { JournalConflictError } from "./execution-journal.js";
|
|
6
6
|
import { boundExecutionFrame } from "./execution-event-limit.js";
|
|
@@ -830,12 +830,15 @@ export async function runExecution(config, input, dependencies) {
|
|
|
830
830
|
const baseSystemPromptBudget = spec.agent.abilityRelease === undefined
|
|
831
831
|
? systemPromptBudget
|
|
832
832
|
: Math.max(0, Math.min(systemPromptBudget, Math.max(32 * 1024, systemPromptBudget - 168 * 1024)));
|
|
833
|
-
const localMemoryV2Enabled = config
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
|
|
837
|
-
});
|
|
833
|
+
const localMemoryV2Enabled = !selfTraining && isLocalMemoryV2Enabled(config, spec.agent.handle);
|
|
834
|
+
// 开发环境的系统/当前轮写保护由 prepareLocalMemoryPrompt 统一注入并预留预算。
|
|
835
|
+
const localMemoryGuardInBase = localMemoryV2Environment(config.serverUrl) === "custom";
|
|
838
836
|
const boundedSystemPrompt = (context) => {
|
|
837
|
+
const enabled = context.workspace.localMemoryV2Enabled ?? localMemoryV2Enabled;
|
|
838
|
+
const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
|
|
839
|
+
...(selfTraining ? { isolateLocalMemory: true } : {}),
|
|
840
|
+
collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
|
|
841
|
+
});
|
|
839
842
|
const base = typeof systemPromptWithLocalFacts === "string"
|
|
840
843
|
? systemPromptWithLocalFacts
|
|
841
844
|
: systemPromptWithLocalFacts(context);
|
package/dist/local-executor.js
CHANGED
|
@@ -44,6 +44,8 @@ import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.j
|
|
|
44
44
|
import { sanitizeEnvVars } from "./runtime-env.js";
|
|
45
45
|
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
|
|
46
46
|
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
47
|
+
import { reportLocalMemoryV2Deferred, resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
|
|
48
|
+
import { localMemoryV2Environment } from "./config.js";
|
|
47
49
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
48
50
|
constructor(runtime, timeoutMs) {
|
|
49
51
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -234,7 +236,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
234
236
|
}, "workspace_prepare", error);
|
|
235
237
|
throw error;
|
|
236
238
|
}
|
|
237
|
-
const
|
|
239
|
+
const memoryRollout = await resolveLocalMemoryV2Rollout({ workspace, handle: input.handle,
|
|
240
|
+
serverUrl: input.launch.serverUrl, requested: input.localMemoryV2Enabled === true,
|
|
241
|
+
selfTraining: isolatedMaintenance, ...(input.launch.description ? { description: input.launch.description } : {}) });
|
|
242
|
+
workspace = { ...workspace, localMemoryV2Enabled: memoryRollout.enabled };
|
|
243
|
+
input = { ...input, localMemoryV2Enabled: memoryRollout.enabled };
|
|
244
|
+
const localMemoryV2Initialized = await initializeLocalMemoryV2ForWorkspace(workspace, memoryRollout.enabled);
|
|
238
245
|
let materialized = null;
|
|
239
246
|
let knownAttachmentDirectory = null;
|
|
240
247
|
const startupReservationState = { current: null };
|
|
@@ -380,7 +387,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
380
387
|
}
|
|
381
388
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
382
389
|
let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
383
|
-
memoryPruneTraceId = parseMemoryPruneTraceId(baseWakePrompt);
|
|
390
|
+
memoryPruneTraceId = isolatedMaintenance ? null : parseMemoryPruneTraceId(baseWakePrompt);
|
|
391
|
+
if (memoryRollout.deferred)
|
|
392
|
+
reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune");
|
|
384
393
|
if (memoryPruneTraceId !== null) {
|
|
385
394
|
executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
|
|
386
395
|
promptContext = { ...promptContext, workspace: executionWorkspace };
|
|
@@ -400,6 +409,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
400
409
|
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
401
410
|
enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
|
|
402
411
|
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
412
|
+
strictRollout: !isolatedMaintenance && localMemoryV2Environment(input.launch.serverUrl) === "dev",
|
|
413
|
+
deferred: memoryRollout.deferred,
|
|
403
414
|
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
404
415
|
});
|
|
405
416
|
localMemoryResultPath = memoryPrompt.resultPath;
|
|
@@ -8,7 +8,7 @@ const WRITABLE_FILES = new Set(["preferences", "project-rules", "testing", "debu
|
|
|
8
8
|
.map((category) => `notes/${category}.md`));
|
|
9
9
|
const length = (text) => Array.from(text).length;
|
|
10
10
|
const digest = (text) => createHash("sha256").update(text).digest("hex");
|
|
11
|
-
export function previewMemoryInventory(sources, readComplete) {
|
|
11
|
+
export function previewMemoryInventory(sources, readComplete, mode = "migration") {
|
|
12
12
|
const issues = [];
|
|
13
13
|
const entries = [];
|
|
14
14
|
const sourceMap = new Map(sources.map((source) => [source.path, source]));
|
|
@@ -16,8 +16,9 @@ export function previewMemoryInventory(sources, readComplete) {
|
|
|
16
16
|
const add = (code, path = "MEMORY.md") => issues.push({ code, path });
|
|
17
17
|
// 掩盖注释但保留UTF-16位置,避免把模板示例当成条目,也保留原文供人工复核。
|
|
18
18
|
const comments = [...memory.matchAll(/<!--[\s\S]*?-->/gu)];
|
|
19
|
+
// 现有写入器允许正文包含箭头 A --> B;未闭合的注释仍须拒绝。
|
|
19
20
|
if ((memory.match(/<!--/gu) ?? []).length !== comments.length
|
|
20
|
-
|| (memory.match(/-->/gu) ?? []).length !== comments.length)
|
|
21
|
+
|| (mode === "migration" && (memory.match(/-->/gu) ?? []).length !== comments.length))
|
|
21
22
|
add("comment_boundary_invalid");
|
|
22
23
|
const masked = memory.replace(/<!--[\s\S]*?-->/gu, (comment) => comment.replace(/[^\r\n]/gu, (character) => " ".repeat(character.length)));
|
|
23
24
|
const sections = [...masked.matchAll(/^##[\t ]+Knowledge Index[\t ]*\r?$/gmu)];
|
|
@@ -63,7 +64,9 @@ export function previewMemoryInventory(sources, readComplete) {
|
|
|
63
64
|
const format = generated.some((block) => cardStart >= block.index && cardStart < block.index + block[0].length)
|
|
64
65
|
? "managed" : "legacy";
|
|
65
66
|
const parsed = parseLocalMemoryIndex(`## Knowledge Index\n${rawCard}`)[0];
|
|
66
|
-
|
|
67
|
+
// 状态更新会把已归一化的空触发词数组写回;字段缺失仍是格式错误。
|
|
68
|
+
const missingTriggers = mode === "existing_v2" && format === "managed" ? fields.triggers === undefined : !fields.triggers;
|
|
69
|
+
if (!parsed || !fields.type || !fields.summary || missingTriggers || !fields.priority || !fields.updated
|
|
67
70
|
|| (format === "legacy" && !fields.scope)
|
|
68
71
|
|| !["procedural", "semantic", "episodic", "preference"].includes(fields.type)
|
|
69
72
|
|| !["high", "medium", "low"].includes(fields.priority))
|
|
@@ -108,7 +111,9 @@ export function previewMemoryInventory(sources, readComplete) {
|
|
|
108
111
|
add("shared_legacy_detail", entry.sourceFields.file);
|
|
109
112
|
if (entries.filter((other) => other.proposed?.id === entry.proposed?.id).length > 1)
|
|
110
113
|
add("duplicate_id");
|
|
111
|
-
|
|
114
|
+
// 已管理条目由ID区分,现有update允许同名;迁移预演仍要求人工审核同名来源。
|
|
115
|
+
if ((mode === "migration" || entry.format === "legacy")
|
|
116
|
+
&& entries.filter((other) => other.title.trim().toLowerCase() === entry.title.trim().toLowerCase()).length > 1)
|
|
112
117
|
add("duplicate_title");
|
|
113
118
|
}
|
|
114
119
|
const compatibilityIssues = entries.filter((entry) => !WRITABLE_FILES.has(entry.sourceFields.file ?? ""))
|
|
@@ -35,7 +35,7 @@ async function safePath(root, file) {
|
|
|
35
35
|
}
|
|
36
36
|
return path;
|
|
37
37
|
}
|
|
38
|
-
async function
|
|
38
|
+
export async function readMemoryInventorySource(root, file, budget) {
|
|
39
39
|
const path = await safePath(root, file);
|
|
40
40
|
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
41
41
|
try {
|
|
@@ -109,7 +109,7 @@ export async function scanMemorySources(agentDir) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
else {
|
|
112
|
-
const source = await
|
|
112
|
+
const source = await readMemoryInventorySource(root, file, MAX_TOTAL_BYTES - totalBytes);
|
|
113
113
|
totalBytes += source.bytes;
|
|
114
114
|
sources.push(source);
|
|
115
115
|
}
|
|
@@ -131,7 +131,7 @@ export async function scanMemorySources(agentDir) {
|
|
|
131
131
|
// 第二遍比对摘要和目录清单,发现扫描期间的变化就拒绝完整性结论;这不是跨文件事务锁。
|
|
132
132
|
for (const source of sources) {
|
|
133
133
|
try {
|
|
134
|
-
if ((await
|
|
134
|
+
if ((await readMemoryInventorySource(root, source.path, MAX_FILE_BYTES)).sha256 !== source.sha256) {
|
|
135
135
|
addIssue("source_changed", source.path);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dslog } from "../slog.js";
|
|
2
2
|
import { renderPendingMemoryContext, searchPendingMemoryCandidates } from "./pending.js";
|
|
3
|
-
import { renderLocalMemoryResultInstructions } from "./runtime.js";
|
|
3
|
+
import { renderLocalMemoryResultInstructions, renderLocalMemoryV2WriteGuard } from "./runtime.js";
|
|
4
|
+
import { LOCAL_MEMORY_V2_DEFERRED_GUIDANCE } from "./rollout.js";
|
|
4
5
|
const PENDING_READ_GUIDANCE = "\n\nV2新增资料预览未命中不代表没有资料;先查有效索引,必要时用 crew-memory search-pending --query '关键词',按 nextCursor 续查;用 read-pending --id ID 分页读正文。容量暂存资料不能覆盖现行规则,未应用修改不作为记忆。用 crew-memory --help 查看参数。\n";
|
|
5
6
|
export async function prepareLocalMemoryPrompt(input) {
|
|
6
7
|
let pending = "";
|
|
@@ -21,13 +22,15 @@ export async function prepareLocalMemoryPrompt(input) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
const original = input.renderBase(0);
|
|
25
|
+
const guard = input.deferred ? LOCAL_MEMORY_V2_DEFERRED_GUIDANCE
|
|
26
|
+
: input.strictRollout && input.enabled && input.resultPath === null ? renderLocalMemoryV2WriteGuard() : "";
|
|
24
27
|
const structured = input.resultPath === null ? "" : renderLocalMemoryResultInstructions(input.resultPath);
|
|
25
28
|
const guidance = input.enabled ? PENDING_READ_GUIDANCE : "";
|
|
26
29
|
// 短命令指引放在当前轮,独立于可舍弃预览;收尾契约仍保持系统与当前轮两份。
|
|
27
30
|
for (const choice of [
|
|
28
|
-
{ suffix: pending + structured, wake: structured + guidance },
|
|
29
|
-
{ suffix: structured, wake: structured + guidance },
|
|
30
|
-
{ suffix: structured, wake: structured },
|
|
31
|
+
{ suffix: pending + guard + structured, wake: guard + structured + guidance },
|
|
32
|
+
{ suffix: guard + structured, wake: guard + structured + guidance },
|
|
33
|
+
{ suffix: guard + structured, wake: guard + structured },
|
|
31
34
|
]) {
|
|
32
35
|
const reserved = Buffer.byteLength(choice.suffix + choice.wake, "utf8");
|
|
33
36
|
const systemPrompt = (reserved === 0 ? original : input.renderBase(reserved)) + choice.suffix;
|
|
@@ -35,6 +38,8 @@ export async function prepareLocalMemoryPrompt(input) {
|
|
|
35
38
|
return { systemPrompt, resultPath: input.resultPath, wakePromptSuffix: choice.wake };
|
|
36
39
|
}
|
|
37
40
|
}
|
|
41
|
+
if (input.strictRollout)
|
|
42
|
+
throw new Error("local_memory_v2_prompt_over_budget");
|
|
38
43
|
// 强制基础指令与契约本身仍无法共存时,保留既有非结构化兼容回退,不裁基础指令。
|
|
39
44
|
return { systemPrompt: original, resultPath: null, wakePromptSuffix: "" };
|
|
40
45
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { lstat, readdir, realpath } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { localMemoryV2Environment } from "../config.js";
|
|
4
|
+
import { memorySeed } from "../workspace.js";
|
|
5
|
+
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP } from "./index-budget.js";
|
|
6
|
+
import { inventoryLimits, readMemoryInventorySource } from "./inventory-source.js";
|
|
7
|
+
import { previewMemoryInventory } from "./inventory-preview.js";
|
|
8
|
+
import { parseLocalMemoryIndex } from "./write.js";
|
|
9
|
+
import { dslog } from "../slog.js";
|
|
10
|
+
export const LOCAL_MEMORY_V2_DEFERRED_GUIDANCE = `
|
|
11
|
+
|
|
12
|
+
## 本轮本地记忆兼容状态
|
|
13
|
+
开发环境已开放 V2,但该 Agent 的旧记忆尚未通过格式兼容检查,本轮未启用结构化 V2。
|
|
14
|
+
本节覆盖前文和旧会话中直接改写共享记忆的要求:本轮 MEMORY.md 与 notes 只读,不得改写、迁移或清空。
|
|
15
|
+
用户要求新增或更新长期规则时,内容和授权均只记录到本任务的 $CREW_TASK_LOG;保留日志,不要清空,不要宣称长期记忆已生效。旧资料兼容处理完成前,记忆收尾会明确报告需要处理。`;
|
|
16
|
+
export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune) {
|
|
17
|
+
dslog("local_memory.v2_deferred", "旧记忆尚未通过兼容检查,保留原资料和待处理日志", {
|
|
18
|
+
execution_id: executionId, agent_handle: handle, reason: "requires_migration"
|
|
19
|
+
});
|
|
20
|
+
if (memoryPrune)
|
|
21
|
+
throw new Error("local_memory_v2_requires_migration");
|
|
22
|
+
}
|
|
23
|
+
/** 只核对小型索引和其引用;不扫描全部历史任务、不迁移或生成新存储。 */
|
|
24
|
+
export async function resolveLocalMemoryV2Rollout(input) {
|
|
25
|
+
const environment = localMemoryV2Environment(input.serverUrl);
|
|
26
|
+
if (input.selfTraining || environment === "production")
|
|
27
|
+
return { enabled: false, deferred: false };
|
|
28
|
+
if (environment === "custom")
|
|
29
|
+
return { enabled: input.requested, deferred: false };
|
|
30
|
+
const deferred = { enabled: false, deferred: true };
|
|
31
|
+
const { workspace } = input;
|
|
32
|
+
if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
33
|
+
return deferred;
|
|
34
|
+
try {
|
|
35
|
+
const root = await realpath(workspace.dir);
|
|
36
|
+
const notesStat = await lstat(join(root, "notes"));
|
|
37
|
+
if (!notesStat.isDirectory() || notesStat.isSymbolicLink())
|
|
38
|
+
return deferred;
|
|
39
|
+
const memory = await readMemoryInventorySource(root, "MEMORY.md", inventoryLimits.fileBytes);
|
|
40
|
+
if (memory.content !== workspace.memory)
|
|
41
|
+
return deferred;
|
|
42
|
+
// 完整种子且无旧明细才可按空记忆接入;marker 不能替代这项核对。
|
|
43
|
+
const seededRole = /^## Role\n([\s\S]*?)\n\n## Core Goals\n/mu.exec(memory.content)?.[1];
|
|
44
|
+
if (seededRole !== undefined && memory.content === memorySeed(input.handle, seededRole)) {
|
|
45
|
+
return (await readdir(join(root, "notes"))).length === 0 ? { enabled: true, deferred: false } : deferred;
|
|
46
|
+
}
|
|
47
|
+
const generated = /<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->[\s\S]*?<!-- NOWWORK:MEMORY_INDEX:END -->/u.exec(memory.content)?.[0];
|
|
48
|
+
if (!generated || Array.from(generated).length > LOCAL_MEMORY_INDEX_CODE_POINT_CAP)
|
|
49
|
+
return deferred;
|
|
50
|
+
const entries = parseLocalMemoryIndex(memory.content);
|
|
51
|
+
if (entries.length > LOCAL_MEMORY_INDEX_ENTRY_CAP)
|
|
52
|
+
return deferred;
|
|
53
|
+
const sources = [memory];
|
|
54
|
+
let remaining = inventoryLimits.totalBytes - memory.bytes;
|
|
55
|
+
for (const file of new Set(entries.map(entry => entry.file))) {
|
|
56
|
+
if (!/^notes\/(preferences|project-rules|testing|debugging|git|incidents|lessons)\.md$/u.test(file))
|
|
57
|
+
return deferred;
|
|
58
|
+
const source = await readMemoryInventorySource(root, file, Math.min(inventoryLimits.fileBytes, remaining));
|
|
59
|
+
sources.push(source);
|
|
60
|
+
remaining -= source.bytes;
|
|
61
|
+
}
|
|
62
|
+
const preview = previewMemoryInventory(sources, true, "existing_v2");
|
|
63
|
+
// 此处只放行现有V2;旧格式和未标记混合卡片都留待显式兼容处理。
|
|
64
|
+
if (preview.entries.some(entry => entry.format !== "managed")
|
|
65
|
+
|| preview.issues.some(issue => !["managed_anchor_invalid", "projection_over_budget"].includes(issue.code)))
|
|
66
|
+
return deferred;
|
|
67
|
+
// 现有writer只改状态时不会重写正文块的revision;兼容门槛不能因此拒绝合法置顶。
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (!/^[A-Za-z0-9_-]+$/u.test(entry.id))
|
|
70
|
+
return deferred;
|
|
71
|
+
const detail = sources.find(source => source.path === entry.file)?.content ?? "";
|
|
72
|
+
const begins = [...detail.matchAll(new RegExp(`<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${entry.id} revision=[1-9][0-9]* -->`, "gu"))];
|
|
73
|
+
const end = `<!-- NOWWORK:MEMORY_ENTRY:END id=${entry.id} -->`;
|
|
74
|
+
if (begins.length !== 1 || detail.split(end).length !== 2 || detail.indexOf(end) < begins[0].index)
|
|
75
|
+
return deferred;
|
|
76
|
+
}
|
|
77
|
+
return { enabled: true, deferred: false };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return deferred;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/runner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Legacy agent orchestration around the business-neutral local executor. */
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { resolveReposRoot } from "./config.js";
|
|
4
|
+
import { isLocalMemoryV2Enabled, resolveReposRoot } from "./config.js";
|
|
5
5
|
import { mintAgentToken } from "./token.js";
|
|
6
6
|
import { buildSystemPrompt, buildWakePrompt, capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
7
7
|
import { deliverScheduledReport, } from "./scheduled-report.js";
|
|
@@ -36,7 +36,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
36
36
|
}), dependencies.cancellation);
|
|
37
37
|
const providerConfig = credential.config ?? {};
|
|
38
38
|
const runtime = runtimeName(providerConfig.runtime ?? config.runtimeBin);
|
|
39
|
-
const localMemoryV2Enabled = config
|
|
39
|
+
const localMemoryV2Enabled = isLocalMemoryV2Enabled(config, input.handle);
|
|
40
40
|
const reasoning = ReasoningSchema.safeParse(providerConfig.reasoning);
|
|
41
41
|
const baseWake = input.wake ?? buildWakePrompt(input.channelId);
|
|
42
42
|
const executionId = input.runId ?? `legacy-${randomUUID()}`;
|
|
@@ -70,7 +70,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
70
70
|
memory: capMemoryForInject(workspace.memory),
|
|
71
71
|
...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
|
|
72
72
|
collaborationToolsEnabled: config.codexCollaborationTools && runtime === "codex",
|
|
73
|
-
localMemoryV2Enabled,
|
|
73
|
+
localMemoryV2Enabled: workspace.localMemoryV2Enabled ?? localMemoryV2Enabled,
|
|
74
74
|
}),
|
|
75
75
|
// Native adapters, not a launch-intent prompt, choose history versus delta.
|
|
76
76
|
wakePrompt: () => baseWake,
|
package/dist/workspace.js
CHANGED
|
@@ -205,7 +205,7 @@ export async function rotateAgentSession(sessionDir) {
|
|
|
205
205
|
* 第一次准备工作区时写入;之后 agent 自己维护(系统提示词里有协议)。
|
|
206
206
|
* 建 agent 时填了 description,则用它种子化 ## Role(否则留占位符,由 agent 自填)。
|
|
207
207
|
*/
|
|
208
|
-
function memorySeed(handle, description) {
|
|
208
|
+
export function memorySeed(handle, description) {
|
|
209
209
|
const role = description?.trim()
|
|
210
210
|
? description.trim()
|
|
211
211
|
: "(填写你的身份与职责,保持在几行以内。)";
|