@nowcrew/daemon 0.6.60 → 0.6.63

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.
Files changed (46) hide show
  1. package/dist/agent-ability/materializer.js +1 -20
  2. package/dist/automatic-install-target.js +10 -15
  3. package/dist/computer-cli.js +1 -10
  4. package/dist/computer-service.js +1 -41
  5. package/dist/config.js +0 -35
  6. package/dist/daemon-migration-controller.js +18 -16
  7. package/dist/execution-runner.js +0 -1
  8. package/dist/local-executor.js +1 -44
  9. package/dist/machine-info.js +2 -3
  10. package/dist/main.js +0 -83
  11. package/dist/managed-service-lifecycle.js +2 -47
  12. package/dist/managed-service-startup.js +0 -2
  13. package/dist/profile-layout.js +4 -10
  14. package/dist/runner.js +0 -1
  15. package/dist/serve.js +3 -64
  16. package/dist/workspace.js +1 -24
  17. package/package.json +10 -9
  18. package/dist/local-memory/index-budget.js +0 -74
  19. package/dist/local-memory/pending.js +0 -111
  20. package/dist/local-memory/runtime.js +0 -87
  21. package/dist/local-memory/types.js +0 -1
  22. package/dist/local-memory/write.js +0 -448
  23. package/dist/storage-config.js +0 -175
  24. package/dist/storage-control.js +0 -131
  25. package/dist/storage-copy.js +0 -444
  26. package/dist/storage-credential-guard.js +0 -21
  27. package/dist/storage-doctor.js +0 -175
  28. package/dist/storage-identity.js +0 -29
  29. package/dist/storage-layout.js +0 -143
  30. package/dist/storage-manifest.js +0 -440
  31. package/dist/storage-migration-controller.js +0 -417
  32. package/dist/storage-migration-manifest.js +0 -105
  33. package/dist/storage-migration-policy.js +0 -100
  34. package/dist/storage-runtime-controller.js +0 -313
  35. package/dist/storage-runtime-readiness.js +0 -184
  36. package/dist/storage-runtime.js +0 -39
  37. package/dist/storage-safe-fs.js +0 -293
  38. package/dist/storage-serve-adapter.js +0 -65
  39. package/dist/storage-status-delivery.js +0 -201
  40. package/dist/storage-status-replay.js +0 -125
  41. package/dist/storage-target-confirmation.js +0 -129
  42. package/dist/storage-worker-command.js +0 -27
  43. package/dist/storage-worker-executor.js +0 -383
  44. package/dist/storage-worker-handoff.js +0 -151
  45. package/dist/storage-worker-operation.js +0 -283
  46. package/dist/storage-worker-status.js +0 -105
@@ -14,7 +14,6 @@ export async function assertManagedServiceStartup(input) {
14
14
  nodePath: input.nodePath,
15
15
  entryPath: input.entryPath,
16
16
  profileHome: input.daemonHome,
17
- ...(input.environment === undefined ? {} : { environment: input.environment }),
18
17
  });
19
18
  const readRegistry = input.readManagedServices
20
19
  ?? (() => readManagedServiceRegistry({ userHome: input.userHome }));
@@ -43,7 +42,6 @@ export async function assertManagedServiceStartup(input) {
43
42
  nodePath: input.nodePath,
44
43
  entryPath: input.entryPath,
45
44
  profileHome: input.daemonHome,
46
- ...(input.environment === undefined ? {} : { environment: input.environment }),
47
45
  legacyServiceId: true,
48
46
  });
49
47
  const legacyDescriptor = await readDescriptor(legacySpec);
@@ -2,10 +2,6 @@ import { posix, win32 } from "node:path";
2
2
  export const PROFILE_LAYOUT_VERSION = 1;
3
3
  const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
4
4
  const LOCAL_INSTANCE_NAME = /^[a-z0-9][a-z0-9_-]{0,39}$/u;
5
- /** Accept the legacy `in` tag at the boundary, but never persist it in a new layout. */
6
- export function normalizeProfileEnvironment(value) {
7
- return value === "in" ? "production" : value;
8
- }
9
5
  export function validateLocalInstanceName(value) {
10
6
  const normalized = value.trim().toLowerCase();
11
7
  if (!LOCAL_INSTANCE_NAME.test(normalized) || normalized.includes("..")) {
@@ -14,21 +10,19 @@ export function validateLocalInstanceName(value) {
14
10
  return normalized;
15
11
  }
16
12
  export function profileForLocalInstance(environment, localInstanceName) {
17
- const canonicalEnvironment = normalizeProfileEnvironment(environment);
18
- const profile = `nw-${canonicalEnvironment}-${validateLocalInstanceName(localInstanceName)}`;
13
+ const profile = `nw-${environment}-${validateLocalInstanceName(localInstanceName)}`;
19
14
  if (!PROFILE_NAME.test(profile))
20
15
  throw new Error("derived daemon profile exceeds 48 characters");
21
16
  return profile;
22
17
  }
23
18
  export function profileLayout(input) {
24
- const environment = normalizeProfileEnvironment(input.environment);
25
19
  const localInstanceName = validateLocalInstanceName(input.localInstanceName);
26
- const profile = profileForLocalInstance(environment, localInstanceName);
20
+ const profile = profileForLocalInstance(input.environment, localInstanceName);
27
21
  const paths = input.platform === "win32" ? win32 : posix;
28
- const base = `${environment}-${localInstanceName}`;
22
+ const base = `${input.environment}-${localInstanceName}`;
29
23
  return Object.freeze({
30
24
  layoutVersion: PROFILE_LAYOUT_VERSION,
31
- environment,
25
+ environment: input.environment,
32
26
  localInstanceName,
33
27
  profile,
34
28
  daemonHome: paths.join(input.userHome, ".crew", `daemon-${base}`),
package/dist/runner.js CHANGED
@@ -105,7 +105,6 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
105
105
  softTokens: config.sessionSoftTokens,
106
106
  maxTurns: config.sessionMaxTurns,
107
107
  },
108
- localMemoryV2Enabled: config.localMemoryV2Handles?.has(input.handle) === true,
109
108
  }, {
110
109
  onActivity,
111
110
  onConsole,
package/dist/serve.js CHANGED
@@ -51,7 +51,6 @@ import { handleRuntimeProbeFrame, probeRuntimeHealth } from "./runtime-health.js
51
51
  import { buildControlPlaneUrl } from "./control-plane-url.js";
52
52
  import { createServeMigrationController, handleMigrationControlMessage } from "./daemon-migration-wiring.js";
53
53
  import { createCodexHomeModeController } from "./codex-home-mode-controller.js";
54
- import { attachStorageMigrationRuntime, createServeStorageStatusDelivery, handleStorageMigrationControl, replayStorageMigrationStatus, storageMigrationCapabilities, } from "./storage-serve-adapter.js";
55
54
  export { buildControlPlaneUrl } from "./control-plane-url.js";
56
55
  // normalize.ts 的活动种类 → activity 枚举
57
56
  const ACTIVITY_MAP = {
@@ -176,36 +175,13 @@ export function serve(config, opts = {}) {
176
175
  });
177
176
  const machineHelloPublisher = createProjectSkillMachineHelloPublisher({
178
177
  status: projectSkillControllerReadiness.status,
179
- effective: (hello, status) => {
180
- const effective = withProjectSkillCapabilityStatus(hello, status);
181
- const configuredIdentity = opts.storageMigration?.identity;
182
- const currentIdentity = typeof configuredIdentity === "function"
183
- ? configuredIdentity()
184
- : configuredIdentity;
185
- return currentIdentity === undefined ? effective : { ...effective, ...currentIdentity };
186
- },
178
+ effective: withProjectSkillCapabilityStatus,
187
179
  onSent: (hello) => log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`),
188
180
  });
189
181
  const initializedProjectSkills = initializedProjectSkillsReconciler(projectSkillControllerReadiness.ensure, projectSkillsReconciler);
190
- const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
191
- const storageCapabilities = storageMigrationCapabilities(configuredCapabilities, opts.storageMigration);
192
- const storageStatusDelivery = createServeStorageStatusDelivery(opts.storageMigration, {
193
- onPersistenceError: (error) => dslog("daemon.storage_status_outbox_failed", "存储迁移状态 outbox 持久化失败", { level: "ERROR", error_message: error.message }),
194
- transmit: (frame) => { try {
195
- if (ws?.readyState === WebSocket.OPEN)
196
- ws.send(JSON.stringify(frame));
197
- }
198
- catch { /* retry */ } },
199
- });
200
- const storageMigrationRuntime = attachStorageMigrationRuntime(opts.storageMigration, {
201
- isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
202
- waitForExecutionFrames: () => executionFrameQueue,
203
- flushLocalState: flushSlog,
204
- sendStatus: storageStatusDelivery.send,
205
- });
206
182
  const prepareProjectSkillCapabilities = createProjectSkillConnectionCapabilityProvider({
207
183
  platform: process.platform,
208
- baseCapabilities: storageCapabilities,
184
+ baseCapabilities: opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform),
209
185
  additionalCapabilities: () => managedDaemonCapabilities(updateEligibility),
210
186
  probeRuntimeRootStorePrimitive: projectSkillRuntimeRootStartup.probeNativePublicationPrimitiveReadiness,
211
187
  initializeController: projectSkillControllerReadiness.ensure,
@@ -334,12 +310,6 @@ export function serve(config, opts = {}) {
334
310
  // 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
335
311
  void drainSpool();
336
312
  // 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
337
- storageStatusDelivery.replayPending();
338
- const storageReplay = replayStorageMigrationStatus(opts.storageMigration, openedSocket, () => ws === openedSocket, (error) => {
339
- dslog("daemon.storage_status_replay_failed", "存储迁移状态重放失败", {
340
- level: "ERROR", error_message: error.message,
341
- });
342
- }, storageStatusDelivery.send);
343
313
  const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
344
314
  const helloPromise = (opts.machineInfo?.collect ?? collectMachineHello)(config.agentsRoot, config.executionLimits, process.platform, {
345
315
  ...(detectInstalled ? { detectInstalled } : {}),
@@ -355,7 +325,6 @@ export function serve(config, opts = {}) {
355
325
  if (hello.executionProtocol === undefined)
356
326
  return [];
357
327
  const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable);
358
- await storageReplay;
359
328
  if (ws === openedSocket && openedSocket.readyState === WebSocket.OPEN
360
329
  && detected.some((runtime) => !hello.executionRuntimes.includes(runtime))) {
361
330
  detectedExecutionRuntimes = detected;
@@ -370,8 +339,7 @@ export function serve(config, opts = {}) {
370
339
  return [];
371
340
  });
372
341
  void helloPromise
373
- .then(async (hello) => {
374
- await storageReplay;
342
+ .then((hello) => {
375
343
  if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
376
344
  return;
377
345
  detectedExecutionRuntimes = hello.executionRuntimes;
@@ -419,8 +387,6 @@ export function serve(config, opts = {}) {
419
387
  if (typeof decoded !== "object" || decoded === null)
420
388
  return;
421
389
  const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
422
- if (storageStatusDelivery.acknowledge(decoded))
423
- return;
424
390
  if (rawType === "daemon:update" || rawType === "daemon:restart") {
425
391
  void updateController.handle(decoded).catch((error) => {
426
392
  dslog("daemon.control_failed", "daemon 控制操作处理失败", {
@@ -437,11 +403,6 @@ export function serve(config, opts = {}) {
437
403
  void codexHomeModeController.handle(decoded).catch((error) => dslog("daemon.codex_home_mode_failed", "Codex Home 模式切换失败", { level: "ERROR", error_message: error.message }));
438
404
  return;
439
405
  }
440
- if (handleStorageMigrationControl(opts.storageMigration, rawType, decoded, storageStatusDelivery.send, (kind, error) => {
441
- dslog(kind === "migration" ? "daemon.storage_migration_failed" : "daemon.storage_migration_confirmation_failed", kind === "migration" ? "存储迁移控制操作失败" : "存储迁移 hello 确认处理失败", { level: "ERROR", error_message: error.message });
442
- })) {
443
- return;
444
- }
445
406
  if (rawType.startsWith("execution:")) {
446
407
  const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
447
408
  if (!parsedExecution.success) {
@@ -503,14 +464,6 @@ export function serve(config, opts = {}) {
503
464
  return;
504
465
  }
505
466
  const spec = frame;
506
- if (storageMigrationRuntime.isAdmissionClosed()) {
507
- safeExecutionSend(ExecutionRejectedSchema.parse({
508
- type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
509
- reason: "resource_limit", message: "Storage migration is quiescing this machine",
510
- at: new Date().toISOString(),
511
- }));
512
- return;
513
- }
514
467
  const workspaceRejection = projectWorkspaceCapabilityRejection(spec, capabilities.executionRunner, new Date().toISOString());
515
468
  if (workspaceRejection !== null)
516
469
  return void safeExecutionSend(workspaceRejection);
@@ -817,17 +770,6 @@ export function serve(config, opts = {}) {
817
770
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
818
771
  const r = msg;
819
772
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
820
- if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string") {
821
- try {
822
- opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId });
823
- machineHelloPublisher.republish();
824
- }
825
- catch (error) {
826
- dslog("daemon.storage_identity_bind_failed", "存储迁移 Workspace 身份绑定失败", {
827
- level: "ERROR", error_message: error.message,
828
- });
829
- }
830
- }
831
773
  // 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。
832
774
  serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
833
775
  ? r.serverCapabilities.filter((c) => typeof c === "string")
@@ -926,8 +868,6 @@ export function serve(config, opts = {}) {
926
868
  }
927
869
  if (msg.type !== "agent:start")
928
870
  return; // ready/error 等忽略
929
- if (storageMigrationRuntime.isAdmissionClosed())
930
- return;
931
871
  // 任务键:scheduled run 用 runId(每次运行独立 cwd/work-log;overlap 由 server 端 skip_if_running 管,
932
872
  // daemon 的 running 去重只兜底"同一 run 重复投递");普通唤醒仍是 线程锚点 ?? 频道。
933
873
  const scheduled = msg.reason === "scheduled_job" && msg.scheduledRun
@@ -1343,7 +1283,6 @@ export function serve(config, opts = {}) {
1343
1283
  concurrencyTelemetry.stop();
1344
1284
  runtimeProbe.stop();
1345
1285
  completionRetransmitter.stop();
1346
- storageStatusDelivery.stop();
1347
1286
  const deadline = createShutdownDeadline(shutdownTimeoutMs);
1348
1287
  if (reconnectTimer !== null) {
1349
1288
  clearTimeout(reconnectTimer);
package/dist/workspace.js CHANGED
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * spawn runtime 时把 <ws>/.crew 置于 PATH 最前,于是 agent 的 `crew ...` 命中 wrapper。
10
10
  */
11
- import { mkdir, writeFile, readFile, chmod, access, lstat } from "node:fs/promises";
11
+ import { mkdir, writeFile, readFile, chmod, access } from "node:fs/promises";
12
12
  import { createHash, randomUUID } from "node:crypto";
13
13
  import { dirname, join } from "node:path";
14
14
  import { dslog } from "./slog.js";
@@ -34,28 +34,8 @@ async function exists(p) {
34
34
  return false;
35
35
  }
36
36
  }
37
- async function hasLocalMemoryV2Marker(agentDir) {
38
- const crewDir = join(agentDir, ".crew");
39
- const memoryDir = join(crewDir, "memory");
40
- const markerPath = join(agentDir, ".crew", "memory", "v2-enabled");
41
- try {
42
- const crewStat = await lstat(crewDir);
43
- const memoryStat = await lstat(memoryDir);
44
- if (!crewStat.isDirectory() || crewStat.isSymbolicLink()
45
- || !memoryStat.isDirectory() || memoryStat.isSymbolicLink())
46
- return false;
47
- const stat = await lstat(markerPath);
48
- if (!stat.isFile() || stat.isSymbolicLink())
49
- return false;
50
- return (await readFile(markerPath, "utf8")) === "1\n";
51
- }
52
- catch {
53
- return false;
54
- }
55
- }
56
37
  export async function prepareWorkspace(input) {
57
38
  const dir = join(input.agentsRoot, input.handle);
58
- const agentHomePreexisted = await exists(dir);
59
39
  const crewDir = join(dir, ".crew");
60
40
  await mkdir(crewDir, { recursive: true });
61
41
  // 预先创建明细目录,让收尾流程可以按功能创建 notes/<topic>.md。
@@ -63,7 +43,6 @@ export async function prepareWorkspace(input) {
63
43
  // MEMORY.md:首次创建"持久身份 + 知识索引"骨架,之后由 agent 自己维护
64
44
  const memoryPath = join(dir, "MEMORY.md");
65
45
  const memorySeedCreated = !(await exists(memoryPath));
66
- const localMemoryV2Initialized = await hasLocalMemoryV2Marker(dir);
67
46
  if (memorySeedCreated) {
68
47
  try {
69
48
  await writeFile(memoryPath, memorySeed(input.handle, input.description), "utf8");
@@ -145,9 +124,7 @@ export async function prepareWorkspace(input) {
145
124
  crewDir,
146
125
  systemPromptPath,
147
126
  memory,
148
- agentHomePreexisted,
149
127
  memorySeedCreated,
150
- localMemoryV2Initialized,
151
128
  homeDir,
152
129
  runDir,
153
130
  workLogPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.60",
3
+ "version": "0.6.63",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -16,6 +16,14 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
+ "scripts": {
20
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
21
+ "build": "tsc -p tsconfig.json",
22
+ "codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
23
+ "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
24
+ "test": "vitest run",
25
+ "typecheck": "tsc --noEmit"
26
+ },
19
27
  "dependencies": {
20
28
  "@agentclientprotocol/sdk": "1.2.1",
21
29
  "@nowcrew/cli": "^0.4.13",
@@ -34,12 +42,5 @@
34
42
  "tsx": "^4.19.0",
35
43
  "typescript": "^5.6.0",
36
44
  "vitest": "^2.1.0"
37
- },
38
- "scripts": {
39
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
40
- "build": "tsc -p tsconfig.json",
41
- "codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
42
- "test": "vitest run",
43
- "typecheck": "tsc --noEmit"
44
45
  }
45
- }
46
+ }
@@ -1,74 +0,0 @@
1
- export const LOCAL_MEMORY_TOTAL_CODE_POINT_CAP = 6_000;
2
- export const LOCAL_MEMORY_INDEX_CODE_POINT_CAP = 3_000;
3
- export const LOCAL_MEMORY_INDEX_ENTRY_CAP = 20;
4
- const PROJECTABLE_STATUSES = new Set(["pinned", "active"]);
5
- function codePointLength(value) {
6
- return Array.from(value).length;
7
- }
8
- export function renderLocalMemoryIndexCard(entry) {
9
- return `### ${entry.title}\n`
10
- + `- id: ${entry.id}\n`
11
- + `- file: ${entry.file}\n`
12
- + `- type: ${entry.type}\n`
13
- + `- status: ${entry.status}\n`
14
- + `- summary: ${entry.summary}\n`
15
- + `- triggers: ${entry.triggers.join(", ")}\n`
16
- + `- priority: ${entry.priority}\n`
17
- + `- revision: ${entry.revision}\n`
18
- + `- anchor: ${entry.anchor ?? ""}\n`
19
- + `- updated: ${entry.updated}\n`
20
- + (entry.reviewAfter === null ? "" : `- review_after: ${entry.reviewAfter}\n`);
21
- }
22
- function compareEntries(left, right) {
23
- const statusRank = (status) => status === "pinned" ? 0 : 1;
24
- const priorityRank = (priority) => (priority === "high" ? 0 : priority === "medium" ? 1 : 2);
25
- return statusRank(left.status) - statusRank(right.status)
26
- || priorityRank(left.priority) - priorityRank(right.priority)
27
- || right.updated.localeCompare(left.updated)
28
- || left.id.localeCompare(right.id);
29
- }
30
- function cardLength(entry, hasPrevious) {
31
- return codePointLength(renderLocalMemoryIndexCard(entry)) + (hasPrevious ? 1 : 0);
32
- }
33
- function overBudgetReason(entries) {
34
- return entries.some((entry) => entry.status === "pinned")
35
- ? "index_over_budget_pinned"
36
- : "index_over_budget_active";
37
- }
38
- export function selectLocalMemoryIndexEntries(entries, budget, options = {}) {
39
- const projectable = entries.filter((entry) => PROJECTABLE_STATUSES.has(entry.status));
40
- const omittedIds = entries
41
- .filter((entry) => !PROJECTABLE_STATUSES.has(entry.status))
42
- .map((entry) => entry.id);
43
- const protectedIds = options.protectedIds ?? new Set();
44
- const protectedEntries = projectable.filter((entry) => protectedIds.has(entry.id)).sort(compareEntries);
45
- const candidates = projectable.filter((entry) => !protectedIds.has(entry.id)).sort(compareEntries);
46
- const selected = [];
47
- let renderedLength = 0;
48
- for (const entry of protectedEntries) {
49
- selected.push(entry);
50
- renderedLength += cardLength(entry, selected.length > 1);
51
- }
52
- if (selected.length > budget.entryCap || renderedLength > budget.indexCodePointCap) {
53
- return {
54
- entries: selected,
55
- omittedIds: [...omittedIds, ...candidates.map((entry) => entry.id)],
56
- reason: overBudgetReason(selected),
57
- };
58
- }
59
- for (const entry of candidates) {
60
- if (selected.length >= budget.entryCap || renderedLength + cardLength(entry, selected.length > 0) > budget.indexCodePointCap) {
61
- omittedIds.push(entry.id);
62
- continue;
63
- }
64
- selected.push(entry);
65
- renderedLength += cardLength(entry, selected.length > 1);
66
- }
67
- return {
68
- entries: selected,
69
- omittedIds,
70
- reason: omittedIds.length === entries.filter((item) => !PROJECTABLE_STATUSES.has(item.status)).length
71
- ? null
72
- : "index_over_budget",
73
- };
74
- }
@@ -1,111 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { lstat, mkdir, readdir, realpath, rename, rm, writeFile, readFile } from "node:fs/promises";
3
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
- import { z } from "zod";
5
- const CandidateSchema = z.object({
6
- operation: z.enum(["create", "update", "supersede"]),
7
- memoryId: z.string().nullable(),
8
- baseRevision: z.number().int().nonnegative().nullable(),
9
- supersedesMemoryId: z.string().nullable(),
10
- title: z.string().min(1),
11
- kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
12
- summary: z.string().min(1),
13
- triggers: z.array(z.string()).min(1),
14
- priority: z.enum(["high", "medium", "low"]),
15
- detailCategory: z.enum(["preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons"]),
16
- detailBody: z.string().min(1),
17
- reviewAfter: z.string().nullable(),
18
- }).strict();
19
- const PendingSchema = z.object({
20
- schemaVersion: z.literal(1),
21
- pendingId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
22
- createdAt: z.string().min(1),
23
- reason: z.literal("index_over_budget"),
24
- source: z.object({
25
- executionId: z.string().min(1),
26
- pruneTraceId: z.string().min(1).nullable(),
27
- }).strict(),
28
- candidate: CandidateSchema,
29
- }).strict();
30
- function within(root, candidate) {
31
- const path = relative(root, candidate);
32
- return path === "" || (path !== ".." && !path.startsWith(`..${sep}`));
33
- }
34
- async function pendingDirectory(agentDir) {
35
- const agentRoot = await realpath(agentDir);
36
- const memoryRoot = resolve(agentRoot, ".crew", "memory");
37
- await mkdir(memoryRoot, { recursive: true });
38
- const memoryStat = await lstat(memoryRoot);
39
- if (!memoryStat.isDirectory() || memoryStat.isSymbolicLink())
40
- throw new Error("local_memory_pending_path_invalid");
41
- const resolvedMemoryRoot = await realpath(memoryRoot);
42
- if (resolvedMemoryRoot !== memoryRoot || !within(agentRoot, resolvedMemoryRoot)) {
43
- throw new Error("local_memory_pending_path_invalid");
44
- }
45
- const directory = join(memoryRoot, "pending");
46
- try {
47
- const stat = await lstat(directory);
48
- if (!stat.isDirectory() || stat.isSymbolicLink())
49
- throw new Error("local_memory_pending_path_invalid");
50
- }
51
- catch (error) {
52
- if (error.code !== "ENOENT")
53
- throw error;
54
- await mkdir(directory);
55
- }
56
- const resolvedDirectory = await realpath(directory);
57
- if (resolvedDirectory !== directory || !within(agentRoot, resolvedDirectory)) {
58
- throw new Error("local_memory_pending_path_invalid");
59
- }
60
- return directory;
61
- }
62
- async function assertRegularFile(path) {
63
- try {
64
- const stat = await lstat(path);
65
- if (!stat.isFile() || stat.isSymbolicLink())
66
- throw new Error("local_memory_pending_path_invalid");
67
- }
68
- catch (error) {
69
- if (error.code !== "ENOENT")
70
- throw error;
71
- }
72
- }
73
- async function atomicWrite(path, content) {
74
- const temporary = join(dirname(path), `.${basename(path)}.tmp-${randomUUID()}`);
75
- try {
76
- await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
77
- await rename(temporary, path);
78
- }
79
- catch (error) {
80
- await rm(temporary, { force: true }).catch(() => undefined);
81
- throw error;
82
- }
83
- }
84
- export async function persistPendingMemoryCandidate(input) {
85
- const parsed = PendingSchema.safeParse(input.candidate);
86
- if (!parsed.success)
87
- throw new Error("local_memory_pending_invalid");
88
- const directory = await pendingDirectory(input.agentDir);
89
- const target = join(directory, `${parsed.data.pendingId}.json`);
90
- await assertRegularFile(target);
91
- await atomicWrite(target, `${JSON.stringify(parsed.data, null, 2)}\n`);
92
- return target;
93
- }
94
- export async function readPendingMemoryCandidates(input) {
95
- const directory = await pendingDirectory(input.agentDir);
96
- const names = (await readdir(directory)).filter((name) => name.endsWith(".json")).sort();
97
- const result = [];
98
- for (const name of names) {
99
- const path = join(directory, name);
100
- try {
101
- await assertRegularFile(path);
102
- const parsed = PendingSchema.safeParse(JSON.parse(await readFile(path, "utf8")));
103
- if (parsed.success && `${parsed.data.pendingId}.json` === name)
104
- result.push(parsed.data);
105
- }
106
- catch {
107
- // A damaged pending item is diagnosable and must not hide other candidates.
108
- }
109
- }
110
- return result;
111
- }
@@ -1,87 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { applyLocalMemoryResult, parseLocalMemoryResult } from "./write.js";
5
- export function isLocalMemoryV2Eligible(input) {
6
- return input.enabledForHandle
7
- && (input.localMemoryV2Initialized || (!input.agentHomePreexisted && input.memorySeedCreated))
8
- && input.memoryPrune;
9
- }
10
- const LOCAL_MEMORY_V2_MARKER = "v2-enabled";
11
- export async function initializeLocalMemoryV2(agentDir) {
12
- const crewDir = join(agentDir, ".crew");
13
- const memoryDir = join(crewDir, "memory");
14
- await mkdir(crewDir, { recursive: true });
15
- const crewStat = await lstat(crewDir);
16
- if (!crewStat.isDirectory() || crewStat.isSymbolicLink())
17
- throw new Error("local_memory_marker_path_invalid");
18
- await mkdir(memoryDir, { recursive: true });
19
- const memoryStat = await lstat(memoryDir);
20
- if (!memoryStat.isDirectory() || memoryStat.isSymbolicLink())
21
- throw new Error("local_memory_marker_path_invalid");
22
- try {
23
- await writeFile(join(memoryDir, LOCAL_MEMORY_V2_MARKER), "1\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
24
- }
25
- catch (error) {
26
- if (error.code !== "EEXIST")
27
- throw error;
28
- }
29
- }
30
- export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForHandle) {
31
- const initialized = workspace.localMemoryV2Initialized === true;
32
- if (!enabledForHandle || initialized || workspace.localMemoryV2Initialized !== false
33
- || workspace.agentHomePreexisted === true || workspace.memorySeedCreated !== true)
34
- return initialized;
35
- try {
36
- await initializeLocalMemoryV2(workspace.dir);
37
- return true;
38
- }
39
- catch {
40
- return false;
41
- }
42
- }
43
- export function localMemoryResultPath(runDir, executionId) {
44
- const suffix = createHash("sha256").update(executionId, "utf8").digest("hex").slice(0, 32);
45
- return join(runDir, `.memory-result.v1-${suffix}.json`);
46
- }
47
- export function renderLocalMemoryResultInstructions(resultPath) {
48
- return `
49
-
50
- ## Structured memory prune (daemon-managed)
51
- 本轮长期记忆收尾只提交候选,不要直接编辑 $CREW_HOME/MEMORY.md 或 $CREW_HOME/notes/。
52
- 将一个合法 JSON 对象写入以下 daemon 提供的文件,写入完成后再结束本轮:
53
- ${resultPath}
54
-
55
- JSON 顶层必须包含:
56
- {"schemaVersion":1,"purpose":"memory_prune","baseCatalogRevision":0,"outcome":"no_change","changes":[],"statusChanges":[]}
57
- 每个 changes 项使用 operation=create|update|supersede、title、kind、summary、triggers、priority、detailCategory、detailBody、reviewAfter;create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null,update/supersede 按现有索引填写版本和 id。
58
- 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 中等待后续治理。
59
- `;
60
- }
61
- export async function commitLocalMemoryResultAfterExit(input) {
62
- if (input.exitCode !== 0)
63
- return { status: "skipped" };
64
- let raw;
65
- try {
66
- raw = await readFile(input.resultPath, "utf8");
67
- }
68
- catch (error) {
69
- if (error.code === "ENOENT")
70
- return { status: "missing" };
71
- return { status: "failed", error: error instanceof Error ? error.message : "read_failed" };
72
- }
73
- try {
74
- const result = parseLocalMemoryResult(raw);
75
- await applyLocalMemoryResult({
76
- agentDir: input.agentDir,
77
- result,
78
- executionId: input.executionId,
79
- pruneTraceId: input.pruneTraceId,
80
- });
81
- await rm(input.resultPath, { force: true });
82
- return { status: "applied" };
83
- }
84
- catch (error) {
85
- return { status: "failed", error: error instanceof Error ? error.message : "apply_failed" };
86
- }
87
- }
@@ -1 +0,0 @@
1
- export {};