@nowcrew/daemon 0.6.88-beta.25 → 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.
@@ -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
- return structured ?? fallback;
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
- const result = await runFile("git", [...GIT_ARGS, ...args], {
77
- ...(cwd ? { cwd } : {}), env: gitEnv(), timeout: 60_000, maxBuffer: 16 * 1024 * 1024,
78
- });
79
- return result.stdout.trim();
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
- // 临时 dev 验收名单。这里必须填写 Server 生成的 Agent handle,而不是显示名称或频道 ID。
80
- // 测试完成后删除此常量,恢复未配置时默认关闭的行为。
79
+ // 保留本地开发的原默认名单;远端 dev 全量不再依赖该集合。
81
80
  const TEMP_LOCAL_MEMORY_V2_TEST_HANDLES = [
82
- "94533bf3-b5b7-4a1e-9c41-6a1ab0a0e1ea", // memory-v2-test-a
83
- "967ab9ce-724c-4f22-a664-2f855cd798af", // memory-v2-test-b
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();
@@ -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.localMemoryV2Handles?.has(spec.agent.handle) === true;
834
- const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${localMemoryV2Enabled ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
835
- ...(selfTraining ? { isolateLocalMemory: true } : {}),
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);
@@ -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 localMemoryV2Initialized = await initializeLocalMemoryV2ForWorkspace(workspace, input.localMemoryV2Enabled === true);
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
- if (!parsed || !fields.type || !fields.summary || !fields.triggers || !fields.priority || !fields.updated
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
- if (entries.filter((other) => other.title.trim().toLowerCase() === entry.title.trim().toLowerCase()).length > 1)
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 readSource(root, file, budget) {
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 readSource(root, file, MAX_TOTAL_BYTES - totalBytes);
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 readSource(root, source.path, MAX_FILE_BYTES)).sha256 !== source.sha256) {
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.localMemoryV2Handles?.has(input.handle) === true;
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
  : "(填写你的身份与职责,保持在几行以内。)";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.88-beta.25",
3
+ "version": "0.6.88-beta.26",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",