@nowcrew/daemon 0.6.100 → 0.6.101-beta.1

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 (59) hide show
  1. package/dist/cloud-report-upload.js +1 -10
  2. package/dist/computer-main.js +0 -0
  3. package/dist/execution-journal.js +3 -1
  4. package/dist/execution-protocol.js +1 -1
  5. package/dist/execution-runner.js +4 -1
  6. package/dist/local-execution-prompt.js +8 -3
  7. package/dist/local-executor.js +78 -55
  8. package/dist/local-memory/commit-telemetry.js +30 -0
  9. package/dist/local-memory/execution-workspace.js +62 -0
  10. package/dist/local-memory/full-index-search.js +47 -0
  11. package/dist/local-memory/index-budget.js +9 -3
  12. package/dist/local-memory/initialization-identity.js +36 -0
  13. package/dist/local-memory/inventory-cli.js +52 -8
  14. package/dist/local-memory/inventory-source.js +15 -8
  15. package/dist/local-memory/inventory-v2.js +362 -0
  16. package/dist/local-memory/legacy-adapter.js +233 -0
  17. package/dist/local-memory/migration-access.js +242 -0
  18. package/dist/local-memory/migration-cli.js +54 -0
  19. package/dist/local-memory/migration-io.js +199 -0
  20. package/dist/local-memory/migration-pending.js +9 -0
  21. package/dist/local-memory/migration-plan.js +432 -0
  22. package/dist/local-memory/migration-schema.js +55 -0
  23. package/dist/local-memory/migration-state-invariants.js +65 -0
  24. package/dist/local-memory/migration-store.js +414 -0
  25. package/dist/local-memory/migration-tool-entry.js +21 -0
  26. package/dist/local-memory/pending-query.js +5 -2
  27. package/dist/local-memory/pending-schema.js +1 -1
  28. package/dist/local-memory/pending.js +16 -2
  29. package/dist/local-memory/prompt.js +21 -5
  30. package/dist/local-memory/read-cli.js +98 -76
  31. package/dist/local-memory/release-artifact-guard.js +55 -0
  32. package/dist/local-memory/rollout.js +79 -11
  33. package/dist/local-memory/runtime.js +191 -17
  34. package/dist/local-memory/source-path.js +13 -0
  35. package/dist/local-memory/storage-format.js +249 -0
  36. package/dist/local-memory/write.js +190 -21
  37. package/dist/local-memory/writer-mode.js +13 -0
  38. package/dist/local-memory-diagnostics.js +96 -15
  39. package/dist/local-memory-telemetry.js +20 -3
  40. package/dist/machine-info.js +3 -0
  41. package/dist/main.js +0 -0
  42. package/dist/marketplace-protocol.js +1 -1
  43. package/dist/marketplace-remote.js +1 -1
  44. package/dist/marketplace-runner.js +1 -1
  45. package/dist/memory-migration-controller.js +89 -0
  46. package/dist/memory-migration-inspect.js +116 -0
  47. package/dist/memory-migration-journal.js +14 -0
  48. package/dist/memory-migration-protocol.js +14 -0
  49. package/dist/memory-migration-runtime.js +305 -0
  50. package/dist/memory-prune-diagnostics.js +74 -4
  51. package/dist/message-file-preview.js +11 -1
  52. package/dist/prompt.js +9 -17
  53. package/dist/runner.js +1 -1
  54. package/dist/runtimes/claude-thread-runner.js +25 -4
  55. package/dist/serve.js +38 -3
  56. package/dist/token.js +1 -0
  57. package/dist/workspace-control-handler.js +16 -3
  58. package/dist/workspace.js +13 -2
  59. package/package.json +9 -10
@@ -7,10 +7,25 @@ import { dslog } from "./slog.js";
7
7
  const TRACE_PATTERN = /\[memory-prune trace_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]/i;
8
8
  export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
9
9
  export const MEMORY_PRUNE_NOTES_MANIFEST_LIMIT = 128;
10
+ export const MEMORY_PRUNE_PENDING_MANIFEST_LIMIT = 128;
10
11
  export const MEMORY_PRUNE_NOTE_CHANGE_LIMIT = 128;
11
12
  function compareCodeUnits(left, right) {
12
13
  return left < right ? -1 : left > right ? 1 : 0;
13
14
  }
15
+ /**
16
+ * Release evidence is a task artifact, not a durable memory note. Keep this
17
+ * deliberately narrow: only conventional delivery-artifact filename families
18
+ * are blocked; unrelated note names are still handled by the normal candidate
19
+ * protocol instead of being deleted or rewritten.
20
+ */
21
+ const RELEASE_ARTIFACT_NOTE_PATTERN = /^(?:release|publish|build|deploy|artifact)(?:[-_.].*)?\.md$/iu;
22
+ export function isReleaseArtifactNotePath(relativePath) {
23
+ const basename = relativePath.split("/").pop() ?? relativePath;
24
+ return RELEASE_ARTIFACT_NOTE_PATTERN.test(basename);
25
+ }
26
+ export function releaseArtifactNoteChanges(diff) {
27
+ return diff.changes.filter((change) => isReleaseArtifactNotePath(change.relative_path));
28
+ }
14
29
  export function parseMemoryPruneTraceId(wakePrompt) {
15
30
  return TRACE_PATTERN.exec(wakePrompt)?.[1] ?? null;
16
31
  }
@@ -192,6 +207,50 @@ export async function inspectMemoryPruneNotes(homeDir, signal) {
192
207
  ...(signal?.aborted ? { error: "ABORT_ERR" } : {}),
193
208
  };
194
209
  }
210
+ export async function inspectMemoryPrunePending(homeDir, signal) {
211
+ const pendingDir = join(homeDir, ".crew", "memory", "pending");
212
+ try {
213
+ const metadata = await lstat(pendingDir);
214
+ if (metadata.isSymbolicLink())
215
+ return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: "symbolic_link" };
216
+ if (!metadata.isDirectory())
217
+ return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: "ENOTDIR" };
218
+ }
219
+ catch (error) {
220
+ if (error.code === "ENOENT")
221
+ return { files: [], total_count: 0, omitted_count: 0, truncated: false };
222
+ return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: diagnosticError(error) };
223
+ }
224
+ let names;
225
+ try {
226
+ names = (await readdir(pendingDir, { withFileTypes: true }))
227
+ .filter(entry => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".json"))
228
+ .map(entry => entry.name).sort(compareCodeUnits);
229
+ }
230
+ catch (error) {
231
+ return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: diagnosticError(error) };
232
+ }
233
+ const selected = names.slice(0, MEMORY_PRUNE_PENDING_MANIFEST_LIMIT);
234
+ const files = [];
235
+ for (const name of selected) {
236
+ if (signal?.aborted)
237
+ break;
238
+ const path = join(pendingDir, name);
239
+ try {
240
+ if ((await lstat(path)).isSymbolicLink()) {
241
+ files.push({ relative_path: `.crew/memory/pending/${name}`, exists: false, error: "symbolic_link" });
242
+ continue;
243
+ }
244
+ }
245
+ catch (error) {
246
+ files.push({ relative_path: `.crew/memory/pending/${name}`, exists: false, error: diagnosticError(error) });
247
+ continue;
248
+ }
249
+ files.push({ relative_path: `.crew/memory/pending/${name}`, ...await inspectFile(path, false, undefined, signal) });
250
+ }
251
+ return { files, total_count: names.length, omitted_count: Math.max(0, names.length - selected.length),
252
+ truncated: names.length > selected.length, ...(signal?.aborted ? { error: "ABORT_ERR" } : {}) };
253
+ }
195
254
  async function inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal) {
196
255
  const cwdIsHome = runDir === homeDir;
197
256
  const [memory, lessons, cwdMemory, cwdLessons, workLog] = await Promise.all([
@@ -210,15 +269,17 @@ async function inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal
210
269
  };
211
270
  }
212
271
  export async function inspectMemoryPruneFiles(homeDir, runDir, workLogPath, signal) {
213
- const [fixedFiles, notesManifest] = await Promise.all([
272
+ const [fixedFiles, notesManifest, pendingManifest] = await Promise.all([
214
273
  inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal),
215
274
  inspectMemoryPruneNotes(homeDir, signal),
275
+ inspectMemoryPrunePending(homeDir, signal),
216
276
  ]);
217
- return { ...fixedFiles, notes_manifest: notesManifest };
277
+ return { ...fixedFiles, notes_manifest: notesManifest, pending_manifest: pendingManifest };
218
278
  }
219
279
  export async function inspectMemoryPruneFilesWithinDeadline(homeDir, runDir, workLogPath, timeoutMs) {
220
280
  const fixedFiles = await withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal), timeoutMs);
221
281
  let notesManifest;
282
+ let pendingManifest;
222
283
  try {
223
284
  notesManifest = await withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneNotes(homeDir, signal), timeoutMs);
224
285
  }
@@ -231,7 +292,13 @@ export async function inspectMemoryPruneFilesWithinDeadline(homeDir, runDir, wor
231
292
  error: diagnosticError(error),
232
293
  };
233
294
  }
234
- return { ...fixedFiles, notes_manifest: notesManifest };
295
+ try {
296
+ pendingManifest = await withinMemoryPruneDiagnosticsDeadline(signal => inspectMemoryPrunePending(homeDir, signal), timeoutMs);
297
+ }
298
+ catch (error) {
299
+ pendingManifest = { files: [], total_count: 0, omitted_count: 0, truncated: false, error: diagnosticError(error) };
300
+ }
301
+ return { ...fixedFiles, notes_manifest: notesManifest, pending_manifest: pendingManifest };
235
302
  }
236
303
  export async function withinMemoryPruneDiagnosticsDeadline(operation, timeoutMs) {
237
304
  const controller = new AbortController();
@@ -361,6 +428,7 @@ export function evaluateMemoryPrunePostcondition(before, after, execution) {
361
428
  && (after.cwd_memory.lessons_reference_count ?? 0) > 0,
362
429
  cwd_lessons_changed: !after.cwd_lessons.same_as_shared_path
363
430
  && fileChanged(before.cwd_lessons, after.cwd_lessons),
431
+ pending_changed: diffMemoryPruneNotes(before.pending_manifest ?? { files: [], total_count: 0, omitted_count: 0, truncated: false }, after.pending_manifest ?? { files: [], total_count: 0, omitted_count: 0, truncated: false }).change_count > 0,
364
432
  };
365
433
  let outcome;
366
434
  if (!execution.executorCompleted)
@@ -388,6 +456,8 @@ export function evaluateMemoryPrunePostcondition(before, after, execution) {
388
456
  outcome = "promoted_without_index_change";
389
457
  else if (facts.shared_memory_changed)
390
458
  outcome = "shared_memory_updated";
459
+ else if (facts.pending_changed)
460
+ outcome = "pending_updated";
391
461
  else
392
462
  outcome = "no_files_changed";
393
463
  return { outcome, ...facts };
@@ -408,7 +478,7 @@ function memoryPruneFileFactFields(fileFacts) {
408
478
  }
409
479
  return fields;
410
480
  }
411
- export function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, ...fileFacts }) {
481
+ export function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, pending_manifest: _pendingManifest, ...fileFacts }) {
412
482
  return memoryPruneFileFactFields(fileFacts);
413
483
  }
414
484
  export function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
@@ -9,6 +9,7 @@ import { z } from "zod";
9
9
  import { isInternalEnvironmentPath } from "./workspace-fs.js";
10
10
  import { opaqueKey, safeKey } from "./workspace.js";
11
11
  import { AgentHandleSchema } from "./execution-protocol.js";
12
+ import { withMemoryAccess } from "./local-memory/migration-access.js";
12
13
  const MAX_BYTES = 256 * 1024;
13
14
  const MAX_BINARY_BYTES = 8 * 1024 * 1024;
14
15
  const MAX_CONCURRENT_READS = 4;
@@ -231,7 +232,16 @@ export async function readSharedMessageFile(agentsRoot, rawRequest, registry) {
231
232
  throw new SharedFileError("file_preview_busy");
232
233
  activeReads++;
233
234
  try {
234
- return await readCandidate(await resolveCandidate(agentsRoot, parsed.data, registry));
235
+ const candidate = await resolveCandidate(agentsRoot, parsed.data, registry);
236
+ const agentRoot = await realpath(resolve(agentsRoot, parsed.data.handle)).catch((error) => {
237
+ if (error.code === "ENOENT")
238
+ return undefined;
239
+ throw error;
240
+ });
241
+ // 记忆保护只用于该Agent根内资料,独立项目文件仍按原执行授权读取。
242
+ return await (agentRoot && inside(agentRoot, candidate.path)
243
+ ? withMemoryAccess(agentRoot, () => readCandidate(candidate))
244
+ : readCandidate(candidate));
235
245
  }
236
246
  catch (error) {
237
247
  if (error instanceof SharedFileError)
package/dist/prompt.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * claim-before-work、freshness/draft、"做完所有事再停"、inbox notice 语义、
6
6
  * 分层记忆与压缩安全、协作礼仪。措辞为本项目原创。
7
7
  */
8
+ import { detectLocalMemoryStorageVersion, selectLegacyMemoryPreview, selectLocalMemoryPreview, } from "./local-memory/storage-format.js";
8
9
  /** work-log 注入上限(字符):防冷启动注入随文件膨胀。超限保头(目标/结论)+尾(最近进展),
9
10
  * 截中段并提示读全文——全文仍在盘上,只限"默认推送量"不限"可获取量"。纯函数。 */
10
11
  export const WORKLOG_INJECT_CAP = 8000;
@@ -67,23 +68,14 @@ export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
67
68
  const tail = workLog.slice(-WORKLOG_TAIL);
68
69
  return `${head}\n…(work-log 过长已截去中段,全文用 Read 读 $CREW_TASK_LOG)\n${tail}`;
69
70
  }
70
- export function capMemoryForInject(memory, cap = MEMORY_INJECT_CAP, maxBytes = Number.POSITIVE_INFINITY) {
71
- if (memory.length <= cap && Buffer.byteLength(memory, "utf8") <= maxBytes)
72
- return memory;
73
- const notice = "[MEMORY.md preview truncated; use crew-memory search/read for more.]\n";
74
- const noticeBytes = Buffer.byteLength(notice, "utf8");
75
- if (notice.length > cap || noticeBytes > maxBytes)
76
- return "";
77
- let end = 0;
78
- let bytes = noticeBytes;
79
- for (const character of memory) {
80
- const size = Buffer.byteLength(character, "utf8");
81
- if (notice.length + end + character.length > cap || bytes + size > maxBytes)
82
- break;
83
- end += character.length;
84
- bytes += size;
71
+ export function capMemoryForInject(memory, cap = MEMORY_INJECT_CAP, maxBytes = Number.POSITIVE_INFINITY, query = "") {
72
+ if (detectLocalMemoryStorageVersion(memory) === 2) {
73
+ return selectLocalMemoryPreview(memory, query, { maxCodePoints: cap, maxBytes }).memory;
74
+ }
75
+ if (memory.length > cap || Buffer.byteLength(memory, "utf8") > maxBytes) {
76
+ return selectLegacyMemoryPreview(memory, query, { maxCodePoints: cap, maxBytes });
85
77
  }
86
- return notice + memory.slice(0, end);
78
+ return memory;
87
79
  }
88
80
  export function buildSystemPrompt(ctx) {
89
81
  const product = ctx.productName ?? "nowwork";
@@ -342,7 +334,7 @@ ${interactiveTaskRules}${freshnessRule}${externalReplyRule}${collaborationEtique
342
334
 
343
335
  ## Workspace 与分层记忆(CRITICAL — 索引+按需,配合上面的并行规则)
344
336
  你的持久记忆在 \`$CREW_HOME\`(跨你所有任务共享),按“常驻索引 + 功能明细 + 当前工作”分层:
345
- 1. **MEMORY.md = 常驻索引 + 角色**:提示词末尾可能只有它的有界预览,预算不足时也可能省略。预览未命中不等于没有记忆。索引条目包含 \`file/type/scope/summary/triggers/priority/updated\`,其中 file 是相对于 \`$CREW_HOME\` 的路径(例如 \`notes/testing.md\`);需要历史约束、经验或偏好时先检索完整索引,再按需分段读取。
337
+ 1. **MEMORY.md = 常驻索引 + 角色**:提示词末尾可能只有它的有界预览,预算不足时也可能省略。预览未命中不等于没有记忆。旧版索引可能包含 \`scope\`;V2 规范字段为 \`id/file/type/status/summary/triggers/priority/revision/anchor/updated\`(可选 \`review_after\`),不含 \`scope\`。其中 file 是相对于 \`$CREW_HOME\` 的路径(例如 \`notes/testing.md\`);需要历史约束、经验或偏好时先检索完整索引,再按需分段读取。
346
338
  2. **notes/<topic>.md = 功能明细**:按用户偏好、项目规则、测试、调试、Git、事故复盘等主题拆分。优先读取与当前任务匹配的文件;\`notes/lessons.md\` 只保留未分类经验和历史兼容内容,不是所有新经验的默认目的地。普通任务默认只读不写,但当前用户明确要求时可以按最小范围更新;任务结束后的专门收尾执行仍按收尾指令复核。
347
339
  - 对旧版 MEMORY.md,如果条目没有完整结构化字段但明确指向 \`notes/<file>.md\`,把它视为 legacy 索引;仍需结合条目标题/链接和当前请求判断相关性,不要因为缺少 summary/triggers 就假定正文已注入。
348
340
  3. **本任务 work-log = \`$CREW_TASK_LOG\`**:每任务一份(别的运行不会碰),记当前阶段/下一步/卡点/关键结论。系统已把它的当前内容注入末尾让你续上;有进展就更新它——这是你**唯一**该在任务中持续写的记忆文件。**work-log 是现状快照,不是流水账**:保持 ≤100 行,更新时重写全文替换过时内容,不要无限追加(过长会被注入截断)。
package/dist/runner.js CHANGED
@@ -80,7 +80,7 @@ async function runAgentWithMachineEnvironment(config, input, onActivity = defaul
80
80
  ? { scheduledExternalNotificationPolicy: input.scheduled.externalNotificationPolicy }
81
81
  : {}),
82
82
  ...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
83
- memory: capMemoryForInject(workspace.memory),
83
+ memory: capMemoryForInject(workspace.memory, undefined, undefined, baseWake),
84
84
  ...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
85
85
  collaborationToolsEnabled: config.codexCollaborationTools && runtime === "codex",
86
86
  localMemoryV2Enabled: workspace.localMemoryV2Enabled ?? localMemoryV2Enabled,
@@ -12,6 +12,14 @@ const InputSchema = z.object({ bin: z.string().min(1), args: z.array(z.string())
12
12
  wakePrompt: z.string().min(1), sessionId: z.string().min(1).optional(), resume: z.boolean() }).strict();
13
13
  class PreflightError extends Error {
14
14
  }
15
+ // Claude waits for every merged SessionStart hook before acknowledging SDK
16
+ // initialization. Those hooks may legitimately outlive the old 30s adapter
17
+ // deadline. Cap this phase below the daemon's default 120s fail-closed startup
18
+ // window so wrapper failure propagation and supervisor-owned tree cleanup retain
19
+ // margin. The 100s cap leaves a nominal 20s outside this phase; this runner's
20
+ // local TERM/KILL cleanup is itself bounded to 2s.
21
+ // Resume and cold recovery share this one budget; hook activity never extends it.
22
+ export const CLAUDE_INITIALIZE_TIMEOUT_MS = 100_000;
15
23
  function hookCommand(path, nonce) {
16
24
  const source = import.meta.url.endsWith(".ts");
17
25
  const hook = fileURLToPath(new URL(`./claude-session-start-hook.${source ? "ts" : "js"}`, import.meta.url));
@@ -52,6 +60,9 @@ async function bounded(promise, milliseconds, label) {
52
60
  clearTimeout(timer);
53
61
  }
54
62
  }
63
+ export function waitForClaudeInitialize(initialized, timeoutMs = CLAUDE_INITIALIZE_TIMEOUT_MS) {
64
+ return bounded(initialized, timeoutMs, "claude_initialize_timeout");
65
+ }
55
66
  async function attempt(input, sessionId, resume, options) {
56
67
  const proofDir = await mkdtemp(join(tmpdir(), "nowwork-claude-session-"));
57
68
  const proofPath = join(proofDir, "proof.json");
@@ -188,7 +199,7 @@ async function attempt(input, sessionId, resume, options) {
188
199
  try {
189
200
  try {
190
201
  await send({ type: "control_request", request_id: initId, request: { subtype: "initialize" } });
191
- await bounded(initialized.promise, options.initializeTimeoutMs ?? 30_000, "claude_initialize_timeout");
202
+ await waitForClaudeInitialize(initialized.promise, options.initializeTimeoutMs);
192
203
  const proof = JSON.parse(await readFile(proofPath, "utf8"));
193
204
  if (proof.nonce !== nonce || proof.sessionId !== sessionId || proof.source !== (resume ? "resume" : "startup")) {
194
205
  throw new Error("claude_session_proof_mismatch");
@@ -238,15 +249,25 @@ async function attempt(input, sessionId, resume, options) {
238
249
  }
239
250
  export async function runClaudeThread(input, options = {}) {
240
251
  const sessionId = input.sessionId ?? randomUUID();
252
+ const initializeBudgetMs = options.initializeTimeoutMs ?? CLAUDE_INITIALIZE_TIMEOUT_MS;
253
+ const now = options.now ?? (() => performance.now());
254
+ const initializeDeadline = now() + Math.max(0, initializeBudgetMs);
255
+ const runAttempt = (attemptSessionId, resume) => {
256
+ const remainingMs = Math.max(0, Math.ceil(initializeDeadline - now()));
257
+ if (remainingMs === 0)
258
+ return Promise.reject(new PreflightError("claude_native_session_unconfirmed"));
259
+ return attempt(input, attemptSessionId, resume, { ...options, initializeTimeoutMs: remainingMs });
260
+ };
241
261
  try {
242
- return await attempt(input, sessionId, input.resume, options);
262
+ return await runAttempt(sessionId, input.resume);
243
263
  }
244
264
  catch (error) {
245
265
  // The failed attempt never received a business user turn or entered its
246
- // model/tool loop. Native startup hooks may run again on cold recovery.
266
+ // model/tool loop. Native startup hooks may run again on cold recovery,
267
+ // within the original aggregate initialize deadline.
247
268
  if (!input.resume || !(error instanceof PreflightError || error instanceof ThreadDeliveryColdStartRequired))
248
269
  throw error;
249
- return attempt(input, randomUUID(), false, options);
270
+ return runAttempt(randomUUID(), false);
250
271
  }
251
272
  }
252
273
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
package/dist/serve.js CHANGED
@@ -14,6 +14,7 @@ import { reconcileExecutionJournal } from "./execution-recovery.js";
14
14
  import { initReleaseSlog, reportDaemonConnectionClose, reportReleaseError } from "./release-channel-guard.js";
15
15
  import { handleWorkspaceControlRequest } from "./workspace-control-handler.js";
16
16
  import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
17
+ import { withMemoryAccess } from "./local-memory/migration-access.js";
17
18
  import { normalizeScheduledContext } from "./scheduled-report.js";
18
19
  import { formatDaemonLogLine } from "./log-format.js";
19
20
  import { reportAgentRunComplete } from "./scheduled-run-report.js";
@@ -50,6 +51,8 @@ import { probeRuntimeHealth } from "./runtime-health.js";
50
51
  import { createServeMarketplace } from "./marketplace-serve.js";
51
52
  import { buildControlPlaneUrl } from "./control-plane-url.js";
52
53
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
54
+ import { createMemoryMigrationController } from "./memory-migration-controller.js";
55
+ import { createMemoryMigrationJournal, replayExecutionJournals } from "./memory-migration-journal.js";
53
56
  import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
54
57
  export { buildControlPlaneUrl } from "./control-plane-url.js";
55
58
  // normalize.ts 的活动种类 → activity 枚举
@@ -75,6 +78,8 @@ export function serve(config, opts = {}) {
75
78
  let reconnectTimer = null;
76
79
  let stopPromise = null;
77
80
  let executionJournal;
81
+ let migrationExecutionJournal;
82
+ let migrationJournalReady = false;
78
83
  const injectedExecutionJournal = opts.execution?.journal;
79
84
  const agentMemory = opts.execution?.dependencies?.agentMemory
80
85
  ?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
@@ -89,6 +94,15 @@ export function serve(config, opts = {}) {
89
94
  startupGapMs: config.executionLimits.startupGapMs,
90
95
  });
91
96
  const localSlots = createSharedSlotManager(config.executionLimits);
97
+ const memoryMigrations = createMemoryMigrationController({ config, coordinator: hostCoordinator,
98
+ isAdmissionClosed: () => stopped || storageIdentityAdmission.isAdmissionClosed(),
99
+ journal: () => { if (!migrationJournalReady)
100
+ throw new Error("memory_migration_journal_unavailable"); return migrationExecutionJournal; }, send: frame => {
101
+ if (frame.type === "execution:completed")
102
+ completionRetransmitter.track(frame);
103
+ else if (ws?.readyState === WebSocket.OPEN)
104
+ ws.send(JSON.stringify(frame));
105
+ } });
92
106
  const sharedSlots = hostCoordinatedSlotManager(localSlots, hostCoordinator);
93
107
  const concurrencyTelemetry = createExecutionConcurrencyTelemetry(sharedSlots, {
94
108
  ...(opts.execution?.concurrencySnapshotIntervalMs === undefined
@@ -270,7 +284,7 @@ export function serve(config, opts = {}) {
270
284
  return null;
271
285
  };
272
286
  const sendSnapshot = async (reqId) => {
273
- const entries = (await executionJournal.replay()).map(snapshotEntry).filter((entry) => entry !== null);
287
+ const entries = (await replayExecutionJournals(executionJournal, migrationExecutionJournal)).map(snapshotEntry).filter((entry) => entry !== null);
274
288
  safeExecutionSend(ExecutionSnapshotSchema.parse({
275
289
  type: "execution:snapshot", protocolVersion: 1, reqId, entries,
276
290
  }));
@@ -352,6 +366,12 @@ export function serve(config, opts = {}) {
352
366
  machineRuntime.open(openedSocket, { capabilities: capabilities.machineHello, beforePublish: Promise.resolve(), connectionId });
353
367
  opts.onOpen?.(ws);
354
368
  void sendSnapshot(`reconnect-${randomUUID()}`);
369
+ // 普通回执不等待维护日志IO;两类记录分别重放到同一发送器。
370
+ void migrationExecutionJournal.replay().then(entries => {
371
+ for (const entry of entries)
372
+ if (entry.completion && !entry.completionAcknowledged)
373
+ completionRetransmitter.track(entry.completion);
374
+ }).catch(() => dslog("memory_migration.journal_unavailable", "维护回执重放失败,普通回执继续", { level: "ERROR" }));
355
375
  void executionJournal.replay()
356
376
  .then((entries) => {
357
377
  for (const entry of entries) {
@@ -431,7 +451,8 @@ export function serve(config, opts = {}) {
431
451
  return;
432
452
  if (frame.type === "execution:completion-ack") {
433
453
  try {
434
- await executionJournal.acknowledgeCompletion(frame.executionId);
454
+ const journal = await executionJournal.get(frame.executionId) ? executionJournal : migrationExecutionJournal;
455
+ await journal.acknowledgeCompletion(frame.executionId);
435
456
  const attempts = completionRetransmitter.acknowledge(frame.executionId);
436
457
  dslog("execution.completion_acknowledged", "execution completion ACK 已持久化", {
437
458
  execution_id: frame.executionId,
@@ -457,6 +478,8 @@ export function serve(config, opts = {}) {
457
478
  return;
458
479
  }
459
480
  if (frame.type === "execution:cancel") {
481
+ if (memoryMigrations.cancel(frame.executionId))
482
+ return;
460
483
  if (!knownExecutionHashes.has(frame.executionId)
461
484
  && !executionRuns.has(frame.executionId)
462
485
  && await executionJournal.get(frame.executionId) === null)
@@ -689,6 +712,7 @@ export function serve(config, opts = {}) {
689
712
  const execution = executeProtocol(config, spec, {
690
713
  ...opts.execution?.dependencies,
691
714
  machineEnvironment: machineRuntime.provider,
715
+ ...(memoryMigrations.getIdentity() === null ? {} : { controlIdentity: memoryMigrations.getIdentity() }),
692
716
  capabilities: capabilities.executionRunner,
693
717
  projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkills,
694
718
  abilityRelease: opts.execution?.dependencies?.abilityRelease ?? agentAbilityRuntime.materializer,
@@ -779,6 +803,8 @@ export function serve(config, opts = {}) {
779
803
  // ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
780
804
  const r = msg;
781
805
  setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
806
+ if (r.workspaceId && r.machineId)
807
+ memoryMigrations.setIdentity({ workspaceId: r.workspaceId, machineId: r.machineId });
782
808
  // 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。ackSupported() 依赖它判断是否重试,
783
809
  // 必须在任何 republish() 之前就位;否则本轮 republish 看到空集合→不排重试→整套 ack/重连机制哑火。
784
810
  serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
@@ -847,6 +873,8 @@ export function serve(config, opts = {}) {
847
873
  }
848
874
  catch { /* server 会按请求超时处理 */ } }))
849
875
  return;
876
+ if (await memoryMigrations.handle(msg))
877
+ return;
850
878
  if (await agentAbilityRuntime.tryHandleControlMessage(msg, serverCapabilities, (frame) => ws?.send(JSON.stringify(frame))))
851
879
  return;
852
880
  // 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
@@ -861,7 +889,7 @@ export function serve(config, opts = {}) {
861
889
  try {
862
890
  const data2 = req.type === "raft:inspect"
863
891
  ? await inspectRaftWorkspace(req.path)
864
- : await importRaftWorkspace(req.path, join(config.agentsRoot, req.handle));
892
+ : await withMemoryAccess(join(config.agentsRoot, req.handle), () => importRaftWorkspace(req.path, join(config.agentsRoot, req.handle)), { create: true });
865
893
  if (req.type === "raft:import") {
866
894
  log(`📦 已导入 raft 工作区 → ${req.handle}: 复制 ${data2.copied.length} 项`);
867
895
  }
@@ -1254,6 +1282,7 @@ export function serve(config, opts = {}) {
1254
1282
  return;
1255
1283
  // server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
1256
1284
  serverCapabilities = new Set();
1285
+ memoryMigrations.clearIdentity();
1257
1286
  completionRetransmitter.pause();
1258
1287
  // 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
1259
1288
  // error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
@@ -1306,6 +1335,9 @@ export function serve(config, opts = {}) {
1306
1335
  writeStderr: (line) => process.stderr.write(line),
1307
1336
  });
1308
1337
  await runProjectSkillStartupCleanup(config.agentsRoot, executionJournal, dslog);
1338
+ migrationExecutionJournal = createMemoryMigrationJournal(config.agentsRoot);
1339
+ await migrationExecutionJournal.reconcileAfterRestart().then(() => { migrationJournalReady = true; })
1340
+ .catch(() => dslog("memory_migration.journal_unavailable", "迁移日志恢复失败,普通执行继续,迁移请求单独报错", { level: "ERROR" }));
1309
1341
  await connect();
1310
1342
  })();
1311
1343
  const stop = () => {
@@ -1323,6 +1355,7 @@ export function serve(config, opts = {}) {
1323
1355
  const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
1324
1356
  const pending = [
1325
1357
  machineRuntime.stop(),
1358
+ memoryMigrations.stop(),
1326
1359
  webSocketClosed,
1327
1360
  updateController.drain(),
1328
1361
  marketplace.stop(),
@@ -1341,6 +1374,8 @@ export function serve(config, opts = {}) {
1341
1374
  if (executionJournal !== undefined) {
1342
1375
  await executionJournal.close({ signal: deadline.signal });
1343
1376
  }
1377
+ if (migrationExecutionJournal !== undefined)
1378
+ await migrationExecutionJournal.close({ signal: deadline.signal });
1344
1379
  }
1345
1380
  finally {
1346
1381
  deadline.dispose();
package/dist/token.js CHANGED
@@ -7,6 +7,7 @@ export async function mintAgentToken(serverUrl, machineToken, handle, displayNam
7
7
  },
8
8
  // 把本轮唤醒的线程根钉到 per-launch 令牌:服务端据此让 task/消息默认绑/回当前线程。
9
9
  body: JSON.stringify({
10
+ memoryStateGuardVersion: 1,
10
11
  handle,
11
12
  ...(displayName ? { displayName } : {}),
12
13
  ...(options.wakeThreadRoot ? { wakeThreadRoot: options.wakeThreadRoot } : {}),
@@ -1,9 +1,22 @@
1
1
  /** 控制面文件/技能查看的按需处理,独立于执行生命周期。 */
2
- import { join } from "node:path";
2
+ import { lstat } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
3
4
  import { readSharedMessageFile } from "./message-file-preview.js";
4
5
  import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
5
6
  import { listSkills } from "./skills.js";
6
7
  import { listRuntimeModels } from "./list-models.js";
8
+ import { withMemoryAccess } from "./local-memory/migration-access.js";
9
+ async function listProtectedWorkspace(root, path) {
10
+ try {
11
+ await lstat(root);
12
+ }
13
+ catch (error) {
14
+ if (error.code !== "ENOENT")
15
+ throw error;
16
+ return { root: resolve(root), path: path.replace(/^\/+/, ""), entries: [] };
17
+ }
18
+ return withMemoryAccess(root, () => listWorkspace(root, path));
19
+ }
7
20
  export async function handleWorkspaceControlRequest(raw, agentsRoot, registry, models = listRuntimeModels) {
8
21
  if (!raw || typeof raw !== "object")
9
22
  return null;
@@ -15,8 +28,8 @@ export async function handleWorkspaceControlRequest(raw, agentsRoot, registry, m
15
28
  try {
16
29
  const root = join(agentsRoot, request.handle);
17
30
  const data = request.type === "fs:read-shared" ? await readSharedMessageFile(agentsRoot, raw, registry)
18
- : request.type === "fs:list" ? await listWorkspace(root, request.path)
19
- : request.type === "fs:read" ? await readWorkspaceFile(root, request.path)
31
+ : request.type === "fs:list" ? await listProtectedWorkspace(root, request.path)
32
+ : request.type === "fs:read" ? await withMemoryAccess(root, () => readWorkspaceFile(root, request.path))
20
33
  : request.type === "skills:list" ? await listSkills(request.runtime)
21
34
  : { models: await models(request.runtime ?? "") };
22
35
  return { type: "fs:result", reqId: request.reqId, ok: true, data };
package/dist/workspace.js CHANGED
@@ -8,12 +8,15 @@
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, lstat, realpath } from "node:fs/promises";
12
12
  import { createHash, randomUUID } from "node:crypto";
13
13
  import { dirname, join } from "node:path";
14
14
  import { fileURLToPath, pathToFileURL } from "node:url";
15
15
  import { createRequire } from "node:module";
16
16
  import { dslog } from "./slog.js";
17
+ import { MigrationStateSchema } from "./local-memory/migration-schema.js";
18
+ import { assertMigrationStateIntegrity } from "./local-memory/migration-state-invariants.js";
19
+ import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
17
20
  /** 文件系统安全的 taskKey:仅留 [\w.-],其余转 _,截断,避免路径穿越/超长。 */
18
21
  export function safeKey(key) {
19
22
  return key.replace(/[^\w.-]+/g, "_").replace(/^[-.]+/, "").slice(0, 80) || "default";
@@ -49,7 +52,15 @@ async function hasLocalMemoryV2Marker(agentDir) {
49
52
  const stat = await lstat(markerPath);
50
53
  if (!stat.isFile() || stat.isSymbolicLink())
51
54
  return false;
52
- return (await readFile(markerPath, "utf8")) === "1\n";
55
+ if ((await readFile(markerPath, "utf8")) !== "1\n")
56
+ return false;
57
+ const root = await realpath(agentDir);
58
+ const state = MigrationStateSchema.parse(JSON.parse(await readFile(join(memoryDir, "migration.v1.json"), "utf8")));
59
+ assertMigrationStateIntegrity(state, state.identity);
60
+ return state.phase === "committed" && state.storageVersion === 2
61
+ && state.identity.rootFingerprint === createHash("sha256").update(root).digest("hex")
62
+ && state.identity.ownerIdentity === String((await lstat(root)).uid)
63
+ && detectLocalMemoryStorageVersion(await readFile(join(root, "MEMORY.md"), "utf8")) === 2;
53
64
  }
54
65
  catch {
55
66
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.100",
3
+ "version": "0.6.101-beta.1",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -18,14 +18,6 @@
18
18
  "publishConfig": {
19
19
  "access": "public"
20
20
  },
21
- "scripts": {
22
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
23
- "build": "tsc -p tsconfig.json",
24
- "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
25
- "test": "vitest run",
26
- "test:release-critical": "vitest run test/automatic-daemon-installer.test.ts test/automatic-install-target.test.ts test/automatic-service-migration.test.ts test/computer-cli.test.ts test/computer-service.test.ts test/computer-setup-client.test.ts test/computer-setup-orchestration.test.ts test/computer-setup.test.ts test/daemon-release-version.test.ts test/daemon-update-controller.test.ts test/daemon-update-eligibility.test.ts test/daemon-updater.test.ts test/execution-protocol.test.ts test/machine-info.test.ts --maxWorkers=2 --minWorkers=1",
27
- "typecheck": "tsc --noEmit"
28
- },
29
21
  "dependencies": {
30
22
  "@agentclientprotocol/sdk": "1.2.1",
31
23
  "@nowcrew/cli": "0.4.17",
@@ -47,5 +39,12 @@
47
39
  "tsx": "^4.19.0",
48
40
  "typescript": "^5.6.0",
49
41
  "vitest": "^2.1.0"
42
+ },
43
+ "scripts": {
44
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
45
+ "build": "tsc -p tsconfig.json",
46
+ "test": "vitest run",
47
+ "test:release-critical": "vitest run test/automatic-daemon-installer.test.ts test/automatic-install-target.test.ts test/automatic-service-migration.test.ts test/computer-cli.test.ts test/computer-service.test.ts test/computer-setup-client.test.ts test/computer-setup-orchestration.test.ts test/computer-setup.test.ts test/daemon-release-version.test.ts test/daemon-update-controller.test.ts test/daemon-update-eligibility.test.ts test/daemon-updater.test.ts test/execution-protocol.test.ts test/machine-info.test.ts --maxWorkers=2 --minWorkers=1",
48
+ "typecheck": "tsc --noEmit"
50
49
  }
51
- }
50
+ }