@nowcrew/daemon 0.6.88 → 0.6.90

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 (122) hide show
  1. package/README.md +66 -4
  2. package/dist/agent-ability/controller.js +11 -1
  3. package/dist/agent-ability/git-errors.js +22 -0
  4. package/dist/agent-ability/materializer.js +21 -10
  5. package/dist/agent-ability/resolver.js +12 -4
  6. package/dist/agent-environment-contract.js +55 -0
  7. package/dist/agent-environment-control.js +24 -0
  8. package/dist/agent-environment-execution.js +98 -0
  9. package/dist/agent-environments.js +275 -0
  10. package/dist/automatic-daemon-installer.js +51 -2
  11. package/dist/automatic-install-target.js +8 -0
  12. package/dist/computer-main.js +18 -0
  13. package/dist/computer-profile.js +17 -4
  14. package/dist/computer-setup-client.js +318 -0
  15. package/dist/computer-setup-orchestration.js +203 -0
  16. package/dist/computer-setup.js +244 -0
  17. package/dist/config.js +30 -12
  18. package/dist/console-formatter.js +3 -3
  19. package/dist/console.js +15 -2
  20. package/dist/daemon-update-eligibility.js +1 -1
  21. package/dist/execution-journal.js +9 -6
  22. package/dist/execution-protocol.js +1 -0
  23. package/dist/execution-runner.js +13 -8
  24. package/dist/i18n.js +19 -0
  25. package/dist/list-models.js +12 -12
  26. package/dist/local-execution-lease.js +22 -0
  27. package/dist/local-executor.js +57 -84
  28. package/dist/local-memory/candidate.js +43 -0
  29. package/dist/local-memory/index-budget.js +8 -0
  30. package/dist/local-memory/inventory-cli.js +61 -0
  31. package/dist/local-memory/inventory-preview.js +159 -0
  32. package/dist/local-memory/inventory-source.js +159 -0
  33. package/dist/local-memory/pending-match.js +29 -0
  34. package/dist/local-memory/pending-query.js +100 -0
  35. package/dist/local-memory/pending-schema.js +29 -0
  36. package/dist/local-memory/pending-source.js +126 -0
  37. package/dist/local-memory/pending.js +34 -84
  38. package/dist/local-memory/prompt.js +19 -12
  39. package/dist/local-memory/read-cli.js +97 -12
  40. package/dist/local-memory/rollout.js +82 -0
  41. package/dist/local-memory/runtime.js +23 -17
  42. package/dist/local-memory/work-log-execution.js +31 -0
  43. package/dist/local-memory/work-log.js +58 -0
  44. package/dist/local-memory/write.js +27 -58
  45. package/dist/local-runtime-environment.js +50 -0
  46. package/dist/machine-environment-cache.js +147 -0
  47. package/dist/machine-environment-contract.js +10 -0
  48. package/dist/machine-environment-control.js +27 -0
  49. package/dist/machine-environment-policy.js +75 -0
  50. package/dist/machine-environment-process.js +110 -0
  51. package/dist/machine-info.js +70 -25
  52. package/dist/machine-runtime-environment.js +189 -0
  53. package/dist/machine-shell-environment-capture.js +135 -0
  54. package/dist/machine-windows-environment-capture.js +215 -0
  55. package/dist/machine-windows-environment-script.js +158 -0
  56. package/dist/main.js +4 -69
  57. package/dist/marketplace-command.js +21 -0
  58. package/dist/marketplace-controller.js +80 -0
  59. package/dist/marketplace-protocol.js +38 -0
  60. package/dist/marketplace-remote.js +48 -0
  61. package/dist/marketplace-runner.js +235 -0
  62. package/dist/marketplace-serve.js +17 -0
  63. package/dist/marketplace-source.js +103 -0
  64. package/dist/message-file-preview.js +246 -0
  65. package/dist/pre-admission-rejection.js +64 -0
  66. package/dist/project-skills/execution-adapter.js +3 -2
  67. package/dist/project-skills/runtime-launch.js +14 -0
  68. package/dist/provider-env.js +26 -15
  69. package/dist/runner.js +17 -4
  70. package/dist/runtime-environment-merge.js +16 -0
  71. package/dist/runtime-health-handler.js +10 -0
  72. package/dist/runtime-health.js +3 -1
  73. package/dist/runtime-path.js +22 -12
  74. package/dist/runtime-probe.js +35 -6
  75. package/dist/runtimes/acp-probe-transport.js +69 -0
  76. package/dist/runtimes/claude-models.js +20 -9
  77. package/dist/runtimes/deepseek-harness-launch.js +10 -0
  78. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  79. package/dist/runtimes/deepseek-harness-runner.js +226 -0
  80. package/dist/runtimes/deepseek-harness.js +76 -15
  81. package/dist/runtimes/harness-probe-retry.js +42 -0
  82. package/dist/runtimes/hermes-models.js +2 -1
  83. package/dist/runtimes/kimi-acp-runner.js +78 -13
  84. package/dist/runtimes/opencode.js +2 -2
  85. package/dist/serve.js +88 -176
  86. package/dist/server-url.js +25 -0
  87. package/dist/slog.js +2 -1
  88. package/dist/storage-identity.js +0 -3
  89. package/dist/storage-runtime.js +2 -2
  90. package/dist/storage-serve-adapter.js +6 -72
  91. package/dist/supervised-runtime.js +17 -2
  92. package/dist/token.js +0 -1
  93. package/dist/workspace-control-handler.js +27 -0
  94. package/dist/workspace-fs.js +66 -15
  95. package/dist/workspace.js +1 -1
  96. package/package.json +6 -4
  97. package/dist/codex-home-launch.js +0 -7
  98. package/dist/codex-home-mode-controller.js +0 -68
  99. package/dist/daemon-migration-controller.js +0 -211
  100. package/dist/daemon-migration-wiring.js +0 -22
  101. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  102. package/dist/runtimes/codex-home-migration.js +0 -112
  103. package/dist/runtimes/codex-home.js +0 -236
  104. package/dist/storage-control.js +0 -212
  105. package/dist/storage-copy.js +0 -628
  106. package/dist/storage-credential-guard.js +0 -21
  107. package/dist/storage-doctor.js +0 -228
  108. package/dist/storage-manifest.js +0 -1027
  109. package/dist/storage-migration-controller.js +0 -440
  110. package/dist/storage-migration-manifest.js +0 -105
  111. package/dist/storage-migration-policy.js +0 -100
  112. package/dist/storage-runtime-controller.js +0 -381
  113. package/dist/storage-runtime-readiness.js +0 -240
  114. package/dist/storage-safe-fs.js +0 -302
  115. package/dist/storage-status-delivery.js +0 -224
  116. package/dist/storage-status-replay.js +0 -140
  117. package/dist/storage-target-confirmation.js +0 -129
  118. package/dist/storage-worker-command.js +0 -37
  119. package/dist/storage-worker-executor.js +0 -449
  120. package/dist/storage-worker-handoff.js +0 -151
  121. package/dist/storage-worker-operation.js +0 -294
  122. package/dist/storage-worker-status.js +0 -259
@@ -1,22 +1,26 @@
1
+ import { deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
2
+ import { buildLocalRuntimeEnvironment } from "./local-runtime-environment.js";
3
+ import { environmentForExecution } from "./agent-environments.js";
4
+ import { prepareEnvironmentExecutionSnapshot } from "./agent-environment-execution.js";
1
5
  import { createInterface } from "node:readline";
2
6
  import { readFile, rm, writeFile } from "node:fs/promises";
3
- import { delimiter, join } from "node:path";
7
+ import { join } from "node:path";
4
8
  import { cleanupSelfTrainingRunDir } from "./self-training-workspace.js";
5
9
  import { prepareWorkspace, rotateAgentSession, safeKey, } from "./workspace.js";
6
10
  import { CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV, isClaudeAdditionalDirectoryInstructionsSupported, probeClaudeVersion, spawnClaude, } from "./runtimes/claude.js";
7
11
  import { spawnCodex } from "./runtimes/codex.js";
8
12
  import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
9
- import { materializeDefaultCodexHome } from "./runtimes/codex-home.js";
10
- import { materializePrivateCodexHome } from "./codex-home-launch.js";
11
- import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
13
+ import { spawnKimi } from "./runtimes/kimi.js";
12
14
  import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
13
- import { augmentedPath } from "./runtime-path.js";
14
15
  import { deliverCloudReports, prepareCloudReportExecution } from "./cloud-report-uploader.js";
15
16
  import { reportDeliveryContext } from "./cloud-report-receipt.js";
16
17
  import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
17
18
  import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
18
19
  import { NativeThreadContext, isolateMaintenanceSession } from "./native-thread-context.js";
19
20
  import { prepareThreadDelivery } from "./thread-input-delivery.js";
21
+ import { resolveMemoryPruneWorkLog, workLogSha256 } from "./local-memory/work-log.js";
22
+ import { withKeyedLease } from "./local-execution-lease.js";
23
+ import { withWorkLogExecution, withWorkLogCommit, workLogExecutionKey } from "./local-memory/work-log-execution.js";
20
24
  import { providerErrorFromExit } from "./provider-error.js";
21
25
  import { createConsoleFormatter } from "./console-formatter.js";
22
26
  import { withLocalExecutionFacts, } from "./local-execution-prompt.js";
@@ -37,11 +41,11 @@ import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPr
37
41
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
38
42
  import { CodexStartupStageParser } from "./codex-startup-stage.js";
39
43
  import { formatRuntimeStartupError } from "./runtime-startup-error.js";
40
- import { machinePackageManagerEnv } from "./package-manager-env.js";
41
44
  import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
42
- import { sanitizeEnvVars } from "./runtime-env.js";
43
45
  import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
44
46
  import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
47
+ import { reportLocalMemoryV2Deferred, resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
48
+ import { localMemoryV2Environment } from "./config.js";
45
49
  export class RuntimeRunningCallbackTimeoutError extends Error {
46
50
  constructor(runtime, timeoutMs) {
47
51
  super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
@@ -159,28 +163,12 @@ async function launchLegacyRuntime(request) {
159
163
  function resolvePrompt(prompt, context) {
160
164
  return typeof prompt === "string" ? prompt : prompt(context);
161
165
  }
162
- const nativeSessionLeaseTails = new Map();
163
166
  const activeMemoryPrunes = new Map();
164
- async function withKeyedLease(key, operation, cancellation) {
165
- const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
166
- let release;
167
- const current = new Promise((resolve) => { release = resolve; });
168
- const tail = predecessor.then(() => current);
169
- nativeSessionLeaseTails.set(key, tail);
170
- try {
171
- await awaitWithCancellation(predecessor, cancellation);
172
- return await operation();
173
- }
174
- finally {
175
- release();
176
- void tail.then(() => {
177
- if (nativeSessionLeaseTails.get(key) === tail)
178
- nativeSessionLeaseTails.delete(key);
179
- });
180
- }
181
- }
182
167
  export async function executeLocal(input, callbacks = {}, dependencies = {}) {
183
168
  input = isolateMaintenanceSession(input);
169
+ return withWorkLogExecution(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), () => executeLocalWithSession(input, callbacks, dependencies), dependencies.cancellation);
170
+ }
171
+ async function executeLocalWithSession(input, callbacks, dependencies) {
184
172
  const versionedProjectSkills = input.projectSkills !== undefined
185
173
  && !Array.isArray(input.projectSkills);
186
174
  if (versionedProjectSkills && (input.runtime.name !== "codex" && input.runtime.name !== "claude")) {
@@ -248,7 +236,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
248
236
  }, "workspace_prepare", error);
249
237
  throw error;
250
238
  }
251
- 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);
252
245
  let materialized = null;
253
246
  let knownAttachmentDirectory = null;
254
247
  const startupReservationState = { current: null };
@@ -259,10 +252,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
259
252
  let memoryPruneBeforeSnapshot = null;
260
253
  let memoryPruneSharedWriteKey = null;
261
254
  let localMemoryResultPath = null;
255
+ let environmentSnapshot;
262
256
  let executionWorkspace = workspace;
263
257
  let localMemoryTelemetry = null;
264
258
  let nativeThreadContext = null;
265
- const inheritedEnv = { ...process.env };
259
+ const machineSnapshot = dependencies.machineEnvironment ? await awaitWithCancellation(dependencies.machineEnvironment.acquire(), dependencies.cancellation) : undefined;
260
+ const inheritedEnv = { ...(machineSnapshot?.env ?? process.env) };
266
261
  const reportRoots = {
267
262
  root: workspace.dir,
268
263
  ...(input.launch.qaReportRoot === undefined ? {} : { qaReportRoot: input.launch.qaReportRoot }),
@@ -274,8 +269,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
274
269
  if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
275
270
  delete inheritedEnv[key];
276
271
  }
277
- const sourceCodexHome = inheritedEnv.CODEX_HOME
278
- ?? (inheritedEnv.HOME === undefined ? undefined : join(inheritedEnv.HOME, ".codex"));
279
272
  let materializedDeepSeekHome;
280
273
  try {
281
274
  if (isDeepSeekCodex && !providerConfig.providerApiKey) {
@@ -356,8 +349,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
356
349
  const launchSessionId = resumeSessionId ?? (rotated
357
350
  ? await rotateAgentSession(workspace.sessionDir)
358
351
  : workspace.agentSessionId);
359
- await materializePrivateCodexHome({ runtime: runtime.name, homeDir: workspace.homeDir, ...(input.launch.codexHomeMode === undefined ? {} : { mode: input.launch.codexHomeMode }), ...(sourceCodexHome === undefined ? {} : { sourceHome: sourceCodexHome }), ...(launchSessionId === undefined ? {} : { sessionId: launchSessionId }), ...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }), ...(dependencies.materializeDefaultCodexHome === undefined ? {} : { materialize: dependencies.materializeDefaultCodexHome }) });
360
- const promptContext = {
352
+ let promptContext = {
361
353
  workspace: executionWorkspace,
362
354
  resuming,
363
355
  rotatedForBudget,
@@ -396,8 +388,15 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
396
388
  }
397
389
  }
398
390
  const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
399
- const baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
400
- memoryPruneTraceId = parseMemoryPruneTraceId(baseWakePrompt);
391
+ let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
392
+ memoryPruneTraceId = isolatedMaintenance ? null : parseMemoryPruneTraceId(baseWakePrompt);
393
+ if (memoryRollout.deferred)
394
+ reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune");
395
+ if (memoryPruneTraceId !== null) {
396
+ executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
397
+ promptContext = { ...promptContext, workspace: executionWorkspace };
398
+ baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
399
+ }
401
400
  if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
402
401
  && isLocalMemoryV2Eligible({
403
402
  enabledForHandle: true,
@@ -412,6 +411,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
412
411
  agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
413
412
  enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
414
413
  resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
414
+ strictRollout: !isolatedMaintenance && localMemoryV2Environment(input.launch.serverUrl) === "dev",
415
+ deferred: memoryRollout.deferred,
415
416
  renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
416
417
  });
417
418
  localMemoryResultPath = memoryPrompt.resultPath;
@@ -480,48 +481,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
480
481
  nativeThreadContext = new NativeThreadContext(join(executionWorkspace.runDir, `.crew-context-${input.executionId}.json`), contextThreadId, runtime.name, resumeSessionId, prior);
481
482
  await nativeThreadContext.initialize();
482
483
  }
483
- const baseEnv = {
484
- ...inheritedEnv,
485
- ...sanitizeEnvVars(providerConfig.envVars),
486
- ...(input.trainingProjectContext === undefined
487
- ? {}
488
- : machinePackageManagerEnv(inheritedEnv)),
489
- ...input.launch.systemEnv,
490
- PATH: `${workspace.crewDir}${delimiter}${augmentedPath()}`,
491
- CREW_SERVER_URL: input.launch.serverUrl,
492
- CREW_TOKEN: input.launch.token,
493
- CREW_CHANNEL: input.channelId,
494
- CREW_HOME: crewHome,
495
- // Empty values override any stale inherited hints from an enclosing runtime.
496
- CREW_THREAD_ROOT: contextThreadId ?? "",
497
- CREW_THREAD_CONTEXT_FILE: nativeThreadContext?.path ?? "",
498
- // Keep the daemon-selected storage root authoritative for runtimes.
499
- // In profile-backed NowWork sessions the daemon config may differ from
500
- // the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
501
- // sufficient for QA/project diagnostics.
502
- CREW_AGENTS_ROOT: input.launch.agentsRoot,
503
- ...(input.launch.reposRoot === undefined ? {} : { NOWWORK_REPOS_ROOT: input.launch.reposRoot }),
504
- ...(input.launch.qaReportRoot === undefined ? {} : { QA_REPORT_HUB: input.launch.qaReportRoot }),
505
- // Expose only the training root; ability-specific workspace layout stays
506
- // owned by the training package rather than the NowWork daemon.
507
- QA_RUNTIME_ROOT: join(crewHome, "training"),
508
- QA_REPORT_DELIVERY_CONTEXT: JSON.stringify(reportContext),
509
- CREW_TASK_DIR: executionWorkspace.runDir,
510
- CREW_TASK_LOG: executionWorkspace.workLogPath,
511
- ...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
512
- XDG_CONFIG_HOME: join(workspace.homeDir, ".config"),
513
- XDG_DATA_HOME: join(workspace.homeDir, ".local", "share"),
514
- XDG_CACHE_HOME: join(workspace.homeDir, ".cache"),
515
- GH_CONFIG_DIR: join(workspace.homeDir, ".config", "gh"),
516
- CLOUDSDK_CONFIG: join(workspace.homeDir, ".config", "gcloud"),
517
- ...(runtime.name === "kimi" && runtime.reasoning
518
- && KIMI_EFFORT_LEVELS.includes(runtime.reasoning)
519
- ? { KIMI_MODEL_THINKING_EFFORT: runtime.reasoning }
520
- : {}),
521
- };
522
- const providerEnv = applyProviderEnv(baseEnv, runtime.name, input.launch.codexHomeMode === undefined
523
- ? providerConfig
524
- : { ...providerConfig, codexHomeMode: input.launch.codexHomeMode }, workspace.homeDir, materializedDeepSeekHome);
484
+ const baseEnv = buildLocalRuntimeEnvironment({ input, workspace, executionWorkspace, inheritedEnv, providerConfig,
485
+ crewHome, contextThreadId, nativeThreadContextPath: nativeThreadContext?.path, reportContext, runtime });
486
+ const providerEnv = applyProviderEnv(baseEnv, runtime.name, providerConfig, workspace.homeDir, materializedDeepSeekHome);
525
487
  const childEnv = runtime.name === "deepseek-harness"
526
488
  ? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
527
489
  : providerEnv;
@@ -538,10 +500,15 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
538
500
  const abilityContext = input.abilityRelease !== undefined && dependencies.abilityRelease !== undefined
539
501
  ? await dependencies.abilityRelease.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.abilityRelease, async (context) => context)
540
502
  : undefined;
503
+ if (providerConfig.environmentPlan !== undefined && !isolatedMaintenance) {
504
+ environmentSnapshot = await prepareEnvironmentExecutionSnapshot({ plan: providerConfig.environmentPlan, agentRoot: workspace.dir, runDir: executionWorkspace.runDir, executionId: input.executionId, ambient: inheritedEnv, runtime: runtime.name, providerConfig, runtimeEnvironment: childEnv, authoritativeKeys: Object.keys(input.launch.systemEnv ?? {}),
505
+ executionBaseEnvironment: childEnv,
506
+ ...(machineSnapshot === undefined ? {} : { machineEnvironmentVersion: { bootId: machineSnapshot.status.bootId, generation: machineSnapshot.status.generation } }),
507
+ selectedEnvironment: environmentForExecution(providerConfig.environmentPlan, workspace.dir, input.projectContext?.primary?.root ?? executionWorkspace.runDir, input.projectContext?.primary?.projectId),
508
+ projectRoots: [...(input.projectContext?.primary ? [input.projectContext.primary] : []), ...(input.projectContext?.secondary ?? [])] });
509
+ }
541
510
  const prepareRuntimeLaunch = async (runtimeRoot) => {
542
- const effectiveSystemPrompt = abilityContext === undefined
543
- ? systemPrompt
544
- : `${systemPrompt}\n\n${abilityContext.prompt}`;
511
+ const effectiveSystemPrompt = [systemPrompt, abilityContext?.prompt, environmentSnapshot?.prompt].filter(Boolean).join("\n\n");
545
512
  if (Buffer.byteLength(effectiveSystemPrompt, "utf8") + Buffer.byteLength(memoryPrompt.wakePromptSuffix, "utf8")
546
513
  > (input.maxSystemPromptBytes ?? Number.MAX_SAFE_INTEGER)) {
547
514
  throw new Error("ability_runtime_context_size_exceeded");
@@ -577,7 +544,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
577
544
  });
578
545
  }
579
546
  const trainingRoots = input.trainingProjectContext?.projects.map((project) => project.root) ?? [];
580
- const { codexSkillRoots, claudeAdditionalDirectories } = await projectSkillRuntimeDirectories({
547
+ const { codexSkillRoots, claudeAdditionalDirectories, harnessSkillRoots } = await projectSkillRuntimeDirectories({
581
548
  runtime: runtime.name,
582
549
  ...(runtimeRoot === undefined ? {} : { runtimeRoot }),
583
550
  agentRoot: workspace.dir,
@@ -615,7 +582,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
615
582
  const workspaceRoots = [...legacyRoots, ...trainingRoots];
616
583
  const launchRequest = {
617
584
  runtime: runtime.name,
618
- bin: runtime.name === "deepseek-harness" ? "dsh-acp-demo" : runtime.name,
585
+ bin: runtime.name === "deepseek-harness" ? deepSeekHarnessLauncher(runtimeEnv).bin : runtime.name,
586
+ ...(runtime.name === "deepseek-harness" ? { harnessStateRoot: executionWorkspace.runDir, harnessSkillRoots: harnessSkillRoots ?? [] } : {}),
619
587
  cwd: input.projectContext?.primary?.root ?? executionWorkspace.runDir,
620
588
  ...(runtimeRoot !== undefined
621
589
  || (input.projectSkills === undefined && input.abilityRelease === undefined)
@@ -731,7 +699,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
731
699
  // (incident: 2026-09-02, boss-alarm 排查结论未投递)。所有 runtime 的文本都喂给它。
732
700
  const externalOutput = new ExternalAnswerDecoder();
733
701
  // 每轮独立:tool_use/result 关联状态不能跨 execution 泄漏。
734
- const consoleFormatter = createConsoleFormatter();
702
+ const consoleFormatter = createConsoleFormatter(runtime.name);
735
703
  let runtimeReady = false;
736
704
  let runtimeRunning = false;
737
705
  let resolveRuntimeReady;
@@ -937,14 +905,17 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
937
905
  memoryPruneRuntimeExitCode = exitCode;
938
906
  if (localMemoryResultPath !== null) {
939
907
  memoryPruneFailurePhase = "memory_commit";
940
- const committed = await commitLocalMemoryResultAfterExit({
941
- resultPath: localMemoryResultPath,
908
+ const resultPath = localMemoryResultPath;
909
+ const commit = () => commitLocalMemoryResultAfterExit({
910
+ resultPath,
942
911
  agentDir: executionWorkspace.dir,
943
912
  executionId: input.executionId,
944
913
  pruneTraceId: memoryPruneTraceId,
945
914
  exitCode,
946
915
  workLogPath: executionWorkspace.workLogPath,
916
+ expectedWorkLogSha256: workLogSha256(executionWorkspace.workLog),
947
917
  });
918
+ const committed = exitCode !== 0 ? await commit() : await withWorkLogCommit(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), commit, dependencies.cancellation);
948
919
  if (committed.status === "failed" || committed.status === "missing") {
949
920
  const reason = committed.error ?? "local_memory_result_missing";
950
921
  dslog("local_memory.commit_failed", "结构化本地记忆提交或清理未完成,保留可用恢复材料", {
@@ -1122,6 +1093,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
1122
1093
  active_prune_count: activePruneCount,
1123
1094
  });
1124
1095
  }
1096
+ if (environmentSnapshot)
1097
+ await rm(environmentSnapshot.directory, { recursive: true, force: true }).catch(() => { });
1125
1098
  const attachmentDirectories = new Set([
1126
1099
  ...(knownAttachmentDirectory === null ? [] : [knownAttachmentDirectory]),
1127
1100
  ...(materialized === null ? [] : [materialized.directory]),
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { containsLikelySecret } from "../agent-memory/policy.js";
3
+ const CategorySchema = z.enum([
4
+ "preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
5
+ ]);
6
+ export const LocalMemorySingleLineText = z.string().trim().min(1).max(500).regex(/^[^\r\n]+$/u);
7
+ export const LocalMemoryChangeSchema = z.object({
8
+ operation: z.enum(["create", "update", "supersede"]),
9
+ memoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
10
+ baseRevision: z.number().int().nonnegative().nullable(),
11
+ supersedesMemoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
12
+ title: LocalMemorySingleLineText.max(120),
13
+ kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
14
+ summary: LocalMemorySingleLineText,
15
+ triggers: z.array(LocalMemorySingleLineText.max(80)).min(1).max(16),
16
+ priority: z.enum(["high", "medium", "low"]),
17
+ detailCategory: CategorySchema,
18
+ detailBody: z.string().trim().min(1).max(12_000),
19
+ reviewAfter: LocalMemorySingleLineText.nullable(),
20
+ }).strict();
21
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
22
+ const RESERVED_MEMORY_MARKER = /NOWWORK:(?:MEMORY_ENTRY|MEMORY_INDEX):/iu;
23
+ export function validateLocalMemoryChange(change) {
24
+ const text = [change.title, change.summary, ...change.triggers, change.detailBody].join("\n");
25
+ if (containsLikelySecret(text))
26
+ throw new Error("local_memory_secret_candidate");
27
+ if (CONTROL_CHARACTER_PATTERN.test(text))
28
+ throw new Error("local_memory_control_character");
29
+ if (RESERVED_MEMORY_MARKER.test(text))
30
+ throw new Error("local_memory_reserved_marker");
31
+ if (change.operation === "create"
32
+ && (change.memoryId !== null || change.baseRevision !== null || change.supersedesMemoryId !== null)) {
33
+ throw new Error("local_memory_create_revision_invalid");
34
+ }
35
+ if (change.operation === "update"
36
+ && (change.memoryId === null || change.baseRevision === null || change.supersedesMemoryId !== null)) {
37
+ throw new Error("local_memory_update_revision_missing");
38
+ }
39
+ if (change.operation === "supersede"
40
+ && (change.memoryId !== null || change.baseRevision === null || change.supersedesMemoryId === null)) {
41
+ throw new Error("local_memory_supersede_revision_missing");
42
+ }
43
+ }
@@ -19,6 +19,14 @@ export function renderLocalMemoryIndexCard(entry) {
19
19
  + `- updated: ${entry.updated}\n`
20
20
  + (entry.reviewAfter === null ? "" : `- review_after: ${entry.reviewAfter}\n`);
21
21
  }
22
+ /** 写入和预演共用完整索引,预算包含起止标记与分隔换行。 */
23
+ export function renderGeneratedIndex(entries) {
24
+ const cards = entries
25
+ .filter(entry => PROJECTABLE_STATUSES.has(entry.status))
26
+ .map(renderLocalMemoryIndexCard)
27
+ .join("\n");
28
+ return `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->\n${cards}<!-- NOWWORK:MEMORY_INDEX:END -->`;
29
+ }
22
30
  function compareEntries(left, right) {
23
31
  const statusRank = (status) => status === "pinned" ? 0 : 1;
24
32
  const priorityRank = (priority) => (priority === "high" ? 0 : priority === "medium" ? 1 : 2);
@@ -0,0 +1,61 @@
1
+ const CommandSchema = z.object({
2
+ "agent-dir": z.string().min(1).refine(isAbsolute),
3
+ "dry-run": z.literal(true),
4
+ }).strict();
5
+ const USAGE = "只读记忆盘点与转换预演,不写文件,不支持 --apply。\n"
6
+ + "pnpm --filter @nowcrew/daemon exec tsx src/local-memory/inventory-cli.ts --agent-dir <单个Agent绝对路径> --dry-run\n"
7
+ + "报告包含原始记忆,需作为敏感本地资料保存;退出码0仅代表盘点及格式预演无阻塞,不代表批准迁移。\n";
8
+ export async function runMemoryInventoryCommand(args, io) {
9
+ if (args.length === 1 && args[0] === "--help") {
10
+ io.out(USAGE);
11
+ return 0;
12
+ }
13
+ let command;
14
+ try {
15
+ const parsed = parseArgs({ args, allowPositionals: false, options: {
16
+ "agent-dir": { type: "string" }, "dry-run": { type: "boolean" },
17
+ } });
18
+ command = CommandSchema.parse(parsed.values);
19
+ }
20
+ catch {
21
+ io.err(USAGE);
22
+ return 2;
23
+ }
24
+ try {
25
+ const scan = await scanMemorySources(command["agent-dir"]);
26
+ const scanComplete = scan.issues.length === 0;
27
+ const analysis = previewMemoryInventory(scan.sources, scanComplete);
28
+ const complete = scanComplete && !analysis.issues.some((issue) => issue.code === "missing_detail");
29
+ const issues = [...scan.issues, ...analysis.issues];
30
+ const referenced = new Set(analysis.entries.map((entry) => entry.sourceFields.file));
31
+ const files = scan.sources.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 }));
32
+ const report = {
33
+ schemaVersion: 1, mode: "dry-run", agentDir: scan.root, applySupported: false,
34
+ complete, scanComplete, requiresHumanReview: true,
35
+ coverage: ["MEMORY.md", "notes/**", ".crew/memory/pending/**", "根目录的 .memory-result.v1-*.json"],
36
+ limitations: ["不扫描历史执行目录;result清单仅覆盖Agent根目录。", "两遍摘要比对不等同跨文件原子快照,正式迁移仍需停止写入并重新核对。"],
37
+ limits: inventoryLimits, totalBytes: scan.totalBytes, files, ...analysis, issues,
38
+ pending: files.filter((file) => file.path.startsWith(".crew/memory/pending/")).map((file) => file.path),
39
+ orphanNotes: files.filter((file) => file.path.startsWith("notes/") && !referenced.has(file.path)).map((file) => file.path),
40
+ resultFiles: files.filter((file) => file.path.startsWith(".memory-result.v1-")).map((file) => file.path),
41
+ };
42
+ io.out(JSON.stringify(report, null, 2) + "\n");
43
+ return issues.length ? 1 : 0;
44
+ }
45
+ catch (error) {
46
+ const code = error instanceof Error ? error.code ?? error.message : "inventory_failed";
47
+ io.err(`记忆盘点失败:${code}\n`);
48
+ return 1;
49
+ }
50
+ }
51
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
52
+ process.exitCode = await runMemoryInventoryCommand(process.argv.slice(2), {
53
+ out: (text) => process.stdout.write(text), err: (text) => process.stderr.write(text),
54
+ });
55
+ }
56
+ import { isAbsolute, resolve } from "node:path";
57
+ import { pathToFileURL } from "node:url";
58
+ import { parseArgs } from "node:util";
59
+ import { z } from "zod";
60
+ import { inventoryLimits, scanMemorySources } from "./inventory-source.js";
61
+ import { previewMemoryInventory } from "./inventory-preview.js";
@@ -0,0 +1,159 @@
1
+ import { createHash } from "node:crypto";
2
+ import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, } from "./index-budget.js";
3
+ import { parseLocalMemoryIndex } from "./write.js";
4
+ const GENERATED = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=1\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/gu;
5
+ const REFERENCE = /^notes\/(?!.*(?:^|\/)\.\.?\/)(?:[^/\\\u0000-\u001f]+\/)*[^/\\\u0000-\u001f]+\.md$/u;
6
+ const FIELD = /^-\s+([a-z][a-z_]*)\s*:\s*(.*?)\s*$/iu;
7
+ const WRITABLE_FILES = new Set(["preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons"]
8
+ .map((category) => `notes/${category}.md`));
9
+ const length = (text) => Array.from(text).length;
10
+ const digest = (text) => createHash("sha256").update(text).digest("hex");
11
+ export function previewMemoryInventory(sources, readComplete, mode = "migration") {
12
+ const issues = [];
13
+ const entries = [];
14
+ const sourceMap = new Map(sources.map((source) => [source.path, source]));
15
+ const memory = sourceMap.get("MEMORY.md")?.content ?? "";
16
+ const add = (code, path = "MEMORY.md") => issues.push({ code, path });
17
+ // 掩盖注释但保留UTF-16位置,避免把模板示例当成条目,也保留原文供人工复核。
18
+ const comments = [...memory.matchAll(/<!--[\s\S]*?-->/gu)];
19
+ // 现有写入器允许正文包含箭头 A --> B;未闭合的注释仍须拒绝。
20
+ if ((memory.match(/<!--/gu) ?? []).length !== comments.length
21
+ || (mode === "migration" && (memory.match(/-->/gu) ?? []).length !== comments.length))
22
+ add("comment_boundary_invalid");
23
+ const masked = memory.replace(/<!--[\s\S]*?-->/gu, (comment) => comment.replace(/[^\r\n]/gu, (character) => " ".repeat(character.length)));
24
+ const sections = [...masked.matchAll(/^##[\t ]+Knowledge Index[\t ]*\r?$/gmu)];
25
+ const heading = sections[0];
26
+ if (sections.length !== 1 || heading?.index === undefined)
27
+ add("index_heading_invalid");
28
+ const start = heading === undefined ? 0 : heading.index + heading[0].length;
29
+ const next = /^##[\t ]+(?!#)/mu.exec(masked.slice(start));
30
+ const end = next?.index === undefined ? memory.length : start + next.index;
31
+ const generated = [...memory.matchAll(GENERATED)];
32
+ const markerCount = (memory.match(/NOWWORK:MEMORY_INDEX:/gu) ?? []).length;
33
+ if (markerCount !== generated.length * 2 || generated.length > 1
34
+ || generated.some((block) => block.index < start || block.index + block[0].length > end))
35
+ add("index_marker_invalid");
36
+ const heads = [...masked.slice(start, end).matchAll(/^###[\t ]+(.+?)[\t ]*\r?$/gmu)];
37
+ if (heads.length > 1000)
38
+ add("card_limit_exceeded");
39
+ const ranges = [];
40
+ for (const [i, head] of heads.slice(0, 1000).entries()) {
41
+ const cardStart = start + head.index;
42
+ const cardEnd = heads[i + 1]?.index === undefined ? end : start + heads[i + 1].index;
43
+ const tailStart = cardStart + head[0].length;
44
+ const tail = masked.slice(tailStart, cardEnd);
45
+ const fields = {};
46
+ const issueStart = issues.length;
47
+ let lastFieldEnd = tailStart;
48
+ let proseSeen = false;
49
+ for (const line of tail.matchAll(/[^\n]*(?:\n|$)/gu)) {
50
+ const field = FIELD.exec(line[0].replace(/\r?\n$/u, ""));
51
+ if (field) {
52
+ const key = field[1].toLowerCase();
53
+ if (fields[key] !== undefined)
54
+ add("duplicate_field");
55
+ if (proseSeen)
56
+ add("ambiguous_card_body");
57
+ fields[key] = field[2];
58
+ lastFieldEnd = tailStart + line.index + line[0].length;
59
+ }
60
+ else if (line[0].trim())
61
+ proseSeen = true;
62
+ }
63
+ const rawCard = memory.slice(cardStart, lastFieldEnd);
64
+ const format = generated.some((block) => cardStart >= block.index && cardStart < block.index + block[0].length)
65
+ ? "managed" : "legacy";
66
+ const parsed = parseLocalMemoryIndex(`## Knowledge Index\n${rawCard}`)[0];
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
70
+ || (format === "legacy" && !fields.scope)
71
+ || !["procedural", "semantic", "episodic", "preference"].includes(fields.type)
72
+ || !["high", "medium", "low"].includes(fields.priority))
73
+ add("invalid_card");
74
+ const file = fields.file ?? "";
75
+ if (!REFERENCE.test(file) || file.split("/").some((part) => part === "." || part === ".."))
76
+ add("invalid_reference", file);
77
+ const detail = sourceMap.get(file)?.content ?? null;
78
+ if (detail === null)
79
+ add("missing_detail", file);
80
+ let proposed = null;
81
+ if (parsed) {
82
+ const id = format === "legacy" ? `mem_${digest(rawCard).slice(0, 32)}` : parsed.id;
83
+ proposed = { ...parsed, id, anchor: id, revision: format === "legacy" ? 1 : parsed.revision };
84
+ if (format === "managed") {
85
+ if (!fields.id || !/^[a-zA-Z0-9_-]+$/u.test(fields.id) || fields.anchor !== fields.id
86
+ || !/^[1-9][0-9]*$/u.test(fields.revision ?? "") || !Number.isSafeInteger(parsed.revision)
87
+ || !["active", "pinned"].includes(fields.status ?? ""))
88
+ add("managed_metadata_invalid");
89
+ const begin = `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${parsed.id} revision=${parsed.revision} -->`;
90
+ const finish = `<!-- NOWWORK:MEMORY_ENTRY:END id=${parsed.id} -->`;
91
+ if (!detail?.includes(begin) || !detail.includes(finish)
92
+ || detail.indexOf(finish) < detail.indexOf(begin)
93
+ || detail.split(begin).length !== 2 || detail.split(finish).length !== 2)
94
+ add("managed_anchor_invalid", file);
95
+ }
96
+ else if (fields.status && fields.status !== "active") {
97
+ add("legacy_status_requires_review", file);
98
+ }
99
+ if (format === "legacy" && /NOWWORK:MEMORY_(?:ENTRY|INDEX):/u.test(detail ?? ""))
100
+ add("legacy_detail_contains_markers", file);
101
+ }
102
+ entries.push({
103
+ title: head[1], format, disposition: issues.length === issueStart ? "review_required" : "blocked",
104
+ sourceFields: fields, rawCard, detail, proposed,
105
+ });
106
+ ranges.push({ start: cardStart, end: lastFieldEnd });
107
+ }
108
+ for (const entry of entries) {
109
+ if (entries.filter((other) => other.sourceFields.file === entry.sourceFields.file).length > 1
110
+ && entry.format === "legacy")
111
+ add("shared_legacy_detail", entry.sourceFields.file);
112
+ if (entries.filter((other) => other.proposed?.id === entry.proposed?.id).length > 1)
113
+ add("duplicate_id");
114
+ // 已管理条目由ID区分,现有update允许同名;迁移预演仍要求人工审核同名来源。
115
+ if ((mode === "migration" || entry.format === "legacy")
116
+ && entries.filter((other) => other.title.trim().toLowerCase() === entry.title.trim().toLowerCase()).length > 1)
117
+ add("duplicate_title");
118
+ }
119
+ const compatibilityIssues = entries.filter((entry) => !WRITABLE_FILES.has(entry.sourceFields.file ?? ""))
120
+ .map((entry) => ({ code: "detail_category_mapping_required", path: entry.sourceFields.file ?? "" }));
121
+ let preview = null;
122
+ if (readComplete && issues.length === 0) {
123
+ const index = renderGeneratedIndex(entries.map(entry => entry.proposed));
124
+ let retained = memory;
125
+ for (const range of [...ranges].reverse())
126
+ retained = retained.slice(0, range.start) + retained.slice(range.end);
127
+ retained = retained.replace(GENERATED, "");
128
+ // 新生成块仅替换索引卡片,角色、目标、维护说明和其它段落保留。
129
+ const insertAt = start;
130
+ const projected = retained.slice(0, insertAt) + `\n${index}\n` + retained.slice(insertAt);
131
+ const budget = {
132
+ entries: entries.length, indexCodePoints: length(index), totalCodePoints: length(projected),
133
+ entryCap: LOCAL_MEMORY_INDEX_ENTRY_CAP, indexCodePointCap: LOCAL_MEMORY_INDEX_CODE_POINT_CAP,
134
+ totalCodePointCap: LOCAL_MEMORY_TOTAL_CODE_POINT_CAP,
135
+ fits: entries.length <= LOCAL_MEMORY_INDEX_ENTRY_CAP && length(index) <= LOCAL_MEMORY_INDEX_CODE_POINT_CAP
136
+ && length(projected) <= LOCAL_MEMORY_TOTAL_CODE_POINT_CAP,
137
+ };
138
+ preview = {
139
+ memory: projected, budget, writerCompatible: compatibilityIssues.length === 0,
140
+ notes: entries.filter((entry) => entry.format === "legacy").map((entry) => ({
141
+ path: entry.sourceFields.file,
142
+ content: `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${entry.proposed.id} revision=1 -->\n`
143
+ + `## ${entry.title}\n\n旧索引范围(仍需结合正文人工确认):${entry.sourceFields.scope}\n\n`
144
+ + `${entry.detail}${entry.detail.endsWith("\n") ? "" : "\n"}`
145
+ + `<!-- NOWWORK:MEMORY_ENTRY:END id=${entry.proposed.id} -->\n`,
146
+ })),
147
+ };
148
+ if (!budget.fits)
149
+ add("projection_over_budget");
150
+ }
151
+ const shownDetails = new Set();
152
+ const compactEntries = entries.map((entry) => {
153
+ const file = entry.sourceFields.file ?? "";
154
+ const detail = shownDetails.has(file) ? null : entry.detail;
155
+ shownDetails.add(file);
156
+ return { ...entry, detail };
157
+ });
158
+ return { entries: compactEntries, issues: [...issues, ...compatibilityIssues], preview, rawMemory: memory };
159
+ }