@nowcrew/daemon 0.6.93-beta.7 → 0.6.93-beta.8

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 (50) hide show
  1. package/dist/cloud-report-upload.js +1 -10
  2. package/dist/execution-journal.js +3 -1
  3. package/dist/execution-protocol.js +1 -1
  4. package/dist/execution-runner.js +4 -1
  5. package/dist/local-execution-prompt.js +8 -3
  6. package/dist/local-executor.js +70 -37
  7. package/dist/local-memory/execution-workspace.js +62 -0
  8. package/dist/local-memory/full-index-search.js +47 -0
  9. package/dist/local-memory/index-budget.js +9 -3
  10. package/dist/local-memory/initialization-identity.js +36 -0
  11. package/dist/local-memory/inventory-cli.js +45 -7
  12. package/dist/local-memory/inventory-source.js +15 -8
  13. package/dist/local-memory/inventory-v2.js +282 -0
  14. package/dist/local-memory/legacy-adapter.js +233 -0
  15. package/dist/local-memory/migration-access.js +196 -0
  16. package/dist/local-memory/migration-cli.js +54 -0
  17. package/dist/local-memory/migration-io.js +199 -0
  18. package/dist/local-memory/migration-plan.js +286 -0
  19. package/dist/local-memory/migration-schema.js +55 -0
  20. package/dist/local-memory/migration-state-invariants.js +52 -0
  21. package/dist/local-memory/migration-store.js +401 -0
  22. package/dist/local-memory/migration-tool-entry.js +21 -0
  23. package/dist/local-memory/prompt.js +20 -4
  24. package/dist/local-memory/read-cli.js +75 -77
  25. package/dist/local-memory/rollout.js +73 -11
  26. package/dist/local-memory/runtime.js +176 -13
  27. package/dist/local-memory/storage-format.js +152 -0
  28. package/dist/local-memory/write.js +135 -21
  29. package/dist/local-memory/writer-mode.js +13 -0
  30. package/dist/local-memory-diagnostics.js +94 -13
  31. package/dist/local-memory-telemetry.js +19 -2
  32. package/dist/machine-info.js +3 -0
  33. package/dist/marketplace-protocol.js +1 -1
  34. package/dist/marketplace-remote.js +1 -1
  35. package/dist/marketplace-runner.js +1 -1
  36. package/dist/memory-migration-controller.js +86 -0
  37. package/dist/memory-migration-inspect.js +108 -0
  38. package/dist/memory-migration-journal.js +14 -0
  39. package/dist/memory-migration-protocol.js +14 -0
  40. package/dist/memory-migration-runtime.js +305 -0
  41. package/dist/memory-prune-diagnostics.js +60 -4
  42. package/dist/message-file-preview.js +11 -1
  43. package/dist/prompt.js +6 -2
  44. package/dist/runner.js +1 -1
  45. package/dist/runtimes/claude-thread-runner.js +25 -4
  46. package/dist/serve.js +38 -3
  47. package/dist/token.js +1 -0
  48. package/dist/workspace-control-handler.js +16 -3
  49. package/dist/workspace.js +13 -2
  50. package/package.json +1 -1
@@ -38,6 +38,7 @@ export function logLocalMemoryDiagnosticsFailure(identity, phase, error) {
38
38
  function snapshotFields(snapshot) {
39
39
  if (snapshot === null)
40
40
  return {};
41
+ const pending = snapshot.pending ?? { exists: false, entry_count: 0 };
41
42
  return {
42
43
  memory_exists: snapshot.memory.exists,
43
44
  memory_size_bytes: snapshot.memory.size_bytes,
@@ -54,6 +55,12 @@ function snapshotFields(snapshot) {
54
55
  lessons_hash_skipped_reason: snapshot.lessons.hash_skipped_reason,
55
56
  lessons_error_code: snapshot.lessons.error_code,
56
57
  lessons_markdown_heading_count: snapshot.lessons.markdown_heading_count,
58
+ pending_exists: pending.exists,
59
+ pending_size_bytes: pending.size_bytes,
60
+ pending_mtime_ms: pending.mtime_ms,
61
+ pending_sha256: pending.sha256,
62
+ pending_error_code: pending.error_code,
63
+ pending_entry_count: pending.entry_count,
57
64
  };
58
65
  }
59
66
  function promptDelivery(runtime, stableProtocolRuntime) {
@@ -183,10 +190,13 @@ export function createLocalMemoryTelemetry(options) {
183
190
  const lessonsChanged = beforeSnapshot !== null && afterSnapshot !== null
184
191
  ? localMemoryFileChanged(beforeSnapshot.lessons, afterSnapshot.lessons)
185
192
  : undefined;
193
+ const pendingChanged = beforeSnapshot !== null && afterSnapshot !== null
194
+ ? localMemoryFileChanged(beforeSnapshot.pending ?? { exists: false }, afterSnapshot.pending ?? { exists: false })
195
+ : undefined;
186
196
  const accessSummary = accessObserver.summary();
187
- const sharedFilesChanged = memoryChanged === undefined || lessonsChanged === undefined
197
+ const sharedFilesChanged = memoryChanged === undefined || lessonsChanged === undefined || pendingChanged === undefined
188
198
  ? undefined
189
- : memoryChanged || lessonsChanged;
199
+ : memoryChanged || lessonsChanged || pendingChanged;
190
200
  safeLog("local_memory.execution_observed", "本地记忆执行观察已完成", {
191
201
  execution_id: options.executionId,
192
202
  agent_handle: options.agentHandle,
@@ -200,6 +210,7 @@ export function createLocalMemoryTelemetry(options) {
200
210
  executor_outcome: executorCompleted ? "succeeded" : "failed",
201
211
  memory_changed: memoryChanged,
202
212
  lessons_changed: lessonsChanged,
213
+ pending_changed: pendingChanged,
203
214
  shared_files_changed_during_execution: sharedFilesChanged,
204
215
  unexpected_shared_write: options.isMemoryPrune
205
216
  ? undefined
@@ -224,6 +235,12 @@ export function createLocalMemoryTelemetry(options) {
224
235
  lessons_after_hash_skipped_reason: afterSnapshot.lessons.hash_skipped_reason,
225
236
  lessons_after_error_code: afterSnapshot.lessons.error_code,
226
237
  lessons_after_markdown_heading_count: afterSnapshot.lessons.markdown_heading_count,
238
+ pending_after_exists: afterSnapshot.pending?.exists ?? false,
239
+ pending_after_size_bytes: afterSnapshot.pending?.size_bytes,
240
+ pending_after_mtime_ms: afterSnapshot.pending?.mtime_ms,
241
+ pending_after_sha256: afterSnapshot.pending?.sha256,
242
+ pending_after_error_code: afterSnapshot.pending?.error_code,
243
+ pending_after_entry_count: afterSnapshot.pending?.entry_count ?? 0,
227
244
  }),
228
245
  });
229
246
  },
@@ -25,6 +25,7 @@ import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
25
25
  import { probeDeepSeekHarnessAcp, deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
26
26
  import { daemonHome } from "./computer-profile.js";
27
27
  import { daemonReleaseProvenance } from "./release-provenance.js";
28
+ import { MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY } from "./memory-migration-protocol.js";
28
29
  export { executableRuntimes } from "./runtime-capabilities.js";
29
30
  const execFileP = promisify(execFile);
30
31
  export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
@@ -39,6 +40,7 @@ export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
39
40
  });
40
41
  export const DAEMON_CAPABILITIES = Object.freeze([
41
42
  MARKETPLACE_CAPABILITY,
43
+ MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY,
42
44
  "scheduled_job_v1",
43
45
  "reply_origin_v1",
44
46
  "origin_decision_v1",
@@ -73,6 +75,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
73
75
  return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== MARKETPLACE_CAPABILITY || nativeWorkspacePlatform)
74
76
  && (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
75
77
  && (capability !== "message_file_preview_v1" || nativeWorkspacePlatform)
78
+ && (nativeWorkspacePlatform || ![MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY].includes(capability))
76
79
  && (nativeWorkspacePlatform
77
80
  || (capability !== "project_skills_v1"
78
81
  && capability !== "project_skill_projection_v2"
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  // 与另一个进程中的 marketplace-protocol.ts 保持一致;跨进程契约测试校验摘要。
3
3
  export const MARKETPLACE_CAPABILITY = "marketplace_update_v1";
4
4
  export const MARKETPLACE = "nowwork-pm";
5
- export const PLUGIN = "AIinterview-plugin";
5
+ export const PLUGIN = "aiinterview-plugin";
6
6
  export const MarketplaceModeSchema = z.enum(["inspect", "refresh", "update"]);
7
7
  export const MarketplaceStatusSchema = z.enum(["queued", "running", "completed", "partial", "failed", "unknown"]);
8
8
  export const MarketplaceTargetSchema = z.object({
@@ -10,7 +10,7 @@ const Target = z.object({
10
10
  version: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/),
11
11
  source: z.object({ source: z.literal("git-subdir"),
12
12
  url: z.literal("git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git"),
13
- path: z.literal("plugins/AIinterview-plugin"), sha: z.string().regex(/^[a-f0-9]{40}$/) }),
13
+ path: z.literal("plugins/aiinterview-plugin"), sha: z.string().regex(/^[a-f0-9]{40}$/) }),
14
14
  });
15
15
  export async function readRemoteMarketplace(runtime, registered, env, command) {
16
16
  const source = validateMarketplaceSource(registered.url, registered.ref);
@@ -12,7 +12,7 @@ import { readMarketplaceSource } from "./marketplace-source.js";
12
12
  export { MarketplaceFailure, runMarketplaceCommand } from "./marketplace-command.js";
13
13
  const SELECTOR = `${PLUGIN}@${MARKETPLACE}`;
14
14
  const SOURCE = "git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git";
15
- const SOURCE_PATH = "plugins/AIinterview-plugin";
15
+ const SOURCE_PATH = "plugins/aiinterview-plugin";
16
16
  const obj = (x) => x !== null && typeof x === "object" && !Array.isArray(x) ? x : {};
17
17
  const parse = (text) => { try {
18
18
  return JSON.parse(text);
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+ import { createRuntimeCancellation } from "./runtime-cancellation.js";
3
+ import { MemoryMigrationStartSchema } from "./memory-migration-protocol.js";
4
+ import { runMemoryMigration } from "./memory-migration-runtime.js";
5
+ import { inspectMemoryForMigration } from "./memory-migration-inspect.js";
6
+ const Inspect = MemoryMigrationStartSchema.pick({ reqId: true, protocolVersion: true, workspaceId: true,
7
+ agentId: true, handle: true, machineId: true }).extend({ type: z.literal("memory:migration:inspect"), executionId: z.string().uuid().optional() }).strict();
8
+ export function createMemoryMigrationController(input) {
9
+ let identity = null;
10
+ const runs = new Map();
11
+ return {
12
+ setIdentity(value) { identity = value; },
13
+ clearIdentity() { identity = null; },
14
+ getIdentity() { return identity === null ? null : { ...identity }; },
15
+ cancel(executionId) { const run = runs.get(executionId); if (!run)
16
+ return false; run.cancel(); return true; },
17
+ async handle(raw) {
18
+ const type = raw?.type;
19
+ if (type !== "memory:migration:start" && type !== "memory:migration:inspect")
20
+ return false;
21
+ const parsed = type === "memory:migration:start" ? MemoryMigrationStartSchema.safeParse(raw) : Inspect.safeParse(raw);
22
+ if (!parsed.success) {
23
+ const reqId = raw.reqId;
24
+ if (typeof reqId === "string" && reqId.length <= 200)
25
+ input.send({ type: "fs:result", reqId, ok: false, error: "memory_migration_invalid_request" });
26
+ return true;
27
+ }
28
+ const request = parsed.data;
29
+ if (!identity || request.workspaceId !== identity.workspaceId || request.machineId !== identity.machineId) {
30
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_identity_mismatch" });
31
+ return true;
32
+ }
33
+ let journal;
34
+ try {
35
+ journal = input.journal();
36
+ }
37
+ catch {
38
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_journal_unavailable" });
39
+ return true;
40
+ }
41
+ if (request.type === "memory:migration:inspect") {
42
+ try {
43
+ const data = await inspectMemoryForMigration(input.config.agentsRoot, request, journal);
44
+ input.send({ type: "fs:result", reqId: request.reqId, ok: true, data });
45
+ }
46
+ catch {
47
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_inspection_failed" });
48
+ }
49
+ return true;
50
+ }
51
+ const existing = runs.get(request.executionId);
52
+ if (existing) {
53
+ input.send({ type: "fs:result", reqId: request.reqId, ok: true, data: { kind: "running" } });
54
+ return true;
55
+ }
56
+ if (input.isAdmissionClosed?.()) {
57
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_storage_busy" });
58
+ return true;
59
+ }
60
+ const cancellation = createRuntimeCancellation();
61
+ const promise = runMemoryMigration(request, {
62
+ agentsRoot: input.config.agentsRoot, coordinator: input.coordinator, journal,
63
+ cancellation: cancellation.cancellation, send: input.send,
64
+ control: async (step, body) => {
65
+ const base = `/daemon/memory-migrations/${encodeURIComponent(request.agentId)}`;
66
+ const path = step === "history" ? `${base}/history` : `${base}/${request.migrationId}/${step === "needs-review" ? "review" : step}`;
67
+ const response = await fetch(new URL(path, input.config.serverUrl), {
68
+ method: step === "history" ? "GET" : "POST", signal: AbortSignal.timeout(10_000),
69
+ headers: { authorization: `Bearer ${input.config.machineToken}`, "content-type": "application/json" },
70
+ ...(step === "history" ? {} : { body: JSON.stringify(body) }),
71
+ });
72
+ const result = await response.json();
73
+ if (!response.ok || result.success !== true)
74
+ throw new Error("migration_control_unconfirmed");
75
+ return result.data;
76
+ },
77
+ }).then(data => input.send({ type: "fs:result", reqId: request.reqId, ok: true, data }))
78
+ .catch(() => input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_failed" }))
79
+ .finally(() => runs.delete(request.executionId));
80
+ runs.set(request.executionId, { promise, cancel: cancellation.request });
81
+ return true;
82
+ },
83
+ async stop() { for (const run of runs.values())
84
+ run.cancel(); await Promise.allSettled([...runs.values()].map(run => run.promise)); },
85
+ };
86
+ }
@@ -0,0 +1,108 @@
1
+ import { lstat, readFile, realpath, readdir } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { withMemoryAccess } from "./local-memory/migration-access.js";
5
+ import { MigrationStateSchema } from "./local-memory/migration-schema.js";
6
+ import { assertMigrationStateIntegrity } from "./local-memory/migration-state-invariants.js";
7
+ import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./local-memory/storage-format.js";
8
+ import { resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
9
+ import { readMemoryInventorySource, inventoryLimits } from "./local-memory/inventory-source.js";
10
+ import { isLocalMemoryInitializationIdentity, localMemoryInitializationIdentity } from "./local-memory/initialization-identity.js";
11
+ export async function inspectMemoryForMigration(agentsRoot, input, journal) {
12
+ const parent = await realpath(agentsRoot);
13
+ const path = join(parent, input.handle);
14
+ const missingBase = { workspaceId: input.workspaceId, agentId: input.agentId, machineId: input.machineId,
15
+ storageRootFingerprint: createHash("sha256").update(resolve(path)).digest("hex"),
16
+ ownerIdentity: String((await lstat(parent)).uid) };
17
+ const prior = input.executionId ? await journal?.get(input.executionId) : null;
18
+ const stoppedExecutionId = prior?.requestedRuntime === "memory_migration" && (prior.state === "completed" || prior.state === "interrupted") ? prior.executionId : null;
19
+ const missing = { ...missingBase, storageVersion: null, classification: "empty", emptyWithoutResidue: true, localState: null, issueCodes: [], stoppedExecutionId };
20
+ try {
21
+ await lstat(path);
22
+ }
23
+ catch (e) {
24
+ if (e.code === "ENOENT")
25
+ return missing;
26
+ throw e;
27
+ }
28
+ const root = await realpath(path);
29
+ const base = { ...missingBase,
30
+ storageRootFingerprint: createHash("sha256").update(root).digest("hex"),
31
+ ownerIdentity: String((await lstat(root)).uid) };
32
+ const empty = { ...missing, ...base };
33
+ try {
34
+ return await withMemoryAccess(path, async () => {
35
+ let localState = null;
36
+ try {
37
+ const record = await readMemoryInventorySource(path, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
38
+ const state = MigrationStateSchema.parse(JSON.parse(record.content));
39
+ assertMigrationStateIntegrity(state, state.identity);
40
+ const expectedInitialization = state.origin === "initialized"
41
+ ? await localMemoryInitializationIdentity(path, input.agentId, {
42
+ workspaceId: input.workspaceId,
43
+ machineId: input.machineId,
44
+ })
45
+ : null;
46
+ const expectedLegacyInitialization = state.origin === "initialized"
47
+ ? await localMemoryInitializationIdentity(path, input.agentId)
48
+ : null;
49
+ const localInitialization = expectedInitialization !== null
50
+ && (isLocalMemoryInitializationIdentity(state.identity, expectedInitialization)
51
+ || expectedLegacyInitialization !== null
52
+ && isLocalMemoryInitializationIdentity(state.identity, expectedLegacyInitialization));
53
+ const serverBoundIdentity = state.identity.workspaceId === input.workspaceId
54
+ && state.identity.machineId === input.machineId;
55
+ const operationIdentityValid = state.origin === "initialized" ? localInitialization : serverBoundIdentity;
56
+ if (state.identity.rootFingerprint !== base.storageRootFingerprint
57
+ || state.identity.ownerIdentity !== base.ownerIdentity
58
+ || state.identity.agentId !== input.agentId || !operationIdentityValid) {
59
+ throw new Error("migration_identity_mismatch");
60
+ }
61
+ localState = { migrationId: state.migrationId, phase: state.phase, storageVersion: state.storageVersion,
62
+ sourceDigest: state.sourceDigest, planDigest: state.planDigest, origin: state.origin,
63
+ issues: state.issues.map(issue => issue.reason),
64
+ initializationIdentity: localInitialization ? {
65
+ agentId: state.identity.agentId, migrationId: state.identity.migrationId,
66
+ workspaceId: state.identity.workspaceId, machineId: state.identity.machineId,
67
+ executionId: state.identity.executionId, sourceStorageVersion: state.identity.sourceStorageVersion,
68
+ targetStorageVersion: state.identity.targetStorageVersion,
69
+ rootFingerprint: state.identity.rootFingerprint, ownerIdentity: state.identity.ownerIdentity,
70
+ } : null };
71
+ if (state.phase === "applying" || state.phase === "repair_required")
72
+ return {
73
+ ...empty, localState, classification: "repair_required", emptyWithoutResidue: false, issueCodes: ["migration_recovery_required"],
74
+ };
75
+ }
76
+ catch (e) {
77
+ if (e.code !== "ENOENT")
78
+ throw e;
79
+ }
80
+ let source;
81
+ try {
82
+ source = await readMemoryInventorySource(path, "MEMORY.md", inventoryLimits.fileBytes);
83
+ }
84
+ catch (e) {
85
+ if (e.code !== "ENOENT")
86
+ throw e;
87
+ const names = await readdir(path);
88
+ const nonMetadata = names.filter(name => name !== ".crew");
89
+ return { ...empty, localState, emptyWithoutResidue: nonMetadata.length === 0 && localState === null,
90
+ classification: nonMetadata.length === 0 && localState === null ? "empty" : "repair_required", issueCodes: ["memory_source_missing"] };
91
+ }
92
+ const storageVersion = detectLocalMemoryStorageVersion(source.content);
93
+ if (storageVersion === 2) {
94
+ parseFullMemoryIndex(source.content);
95
+ return { ...base, storageVersion, classification: "valid_v2", emptyWithoutResidue: false, localState, issueCodes: [], stoppedExecutionId };
96
+ }
97
+ const eligibility = await resolveLocalMemoryV2Rollout({ workspace: { dir: path, memory: source.content },
98
+ serverUrl: "https://nowwork-dev.nowcoder.com", handle: input.handle, requested: true });
99
+ return { ...base, storageVersion, classification: storageVersion === 1 && eligibility.enabled ? "valid_v1"
100
+ : eligibility.legacyFallback || eligibility.enabled ? "legacy" : "repair_required",
101
+ emptyWithoutResidue: false, localState, stoppedExecutionId, issueCodes: eligibility.enabled || eligibility.legacyFallback ? [] : ["memory_source_incompatible"] };
102
+ }, { maintenance: true, waitMs: 0 });
103
+ }
104
+ catch (error) {
105
+ const code = error instanceof Error && /^[a-z_]+$/u.test(error.message) ? error.message : "memory_inspection_failed";
106
+ return { ...empty, emptyWithoutResidue: false, classification: code === "memory_migration_access_busy" ? "busy" : "repair_required", issueCodes: [code] };
107
+ }
108
+ }
@@ -0,0 +1,14 @@
1
+ import { join } from "node:path";
2
+ import { createExecutionJournal } from "./execution-journal.js";
3
+ import { dslog } from "./slog.js";
4
+ /** 复用journal的监管/恢复实现;旧daemon只读普通目录,不会解析维护类型。 */
5
+ export function createMemoryMigrationJournal(agentsRoot) {
6
+ return createExecutionJournal(join(agentsRoot, ".crew", "memory-migration-runtime"), { retainCompleted: true });
7
+ }
8
+ export async function replayExecutionJournals(normal, maintenance) {
9
+ const [ordinary, migration] = await Promise.all([normal.replay(), maintenance.replay().catch(() => {
10
+ dslog("memory_migration.journal_unavailable", "迁移日志暂不可读,保留普通执行能力", { level: "ERROR" });
11
+ return [];
12
+ })]);
13
+ return [...ordinary, ...migration];
14
+ }
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { MigrationRecordMappingSchema } from "./local-memory/migration-schema.js";
3
+ export const MEMORY_MIGRATION_CAPABILITY = "memory_migration_v1";
4
+ export const MEMORY_STORAGE_V2_CAPABILITY = "memory_storage_v2";
5
+ export const MEMORY_STATE_GUARD_CAPABILITY = "memory_state_guard_v1";
6
+ export const MemoryMigrationStartSchema = z.object({
7
+ type: z.literal("memory:migration:start"), reqId: z.string().min(1).max(200), protocolVersion: z.literal(1),
8
+ workspaceId: z.string().uuid(), agentId: z.string().uuid(), handle: z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u),
9
+ machineId: z.string().uuid(), executionId: z.string().uuid(), migrationId: z.string().uuid(),
10
+ expectedRevision: z.number().int().nonnegative(), attemptKey: z.string().min(1).max(200),
11
+ sourceStorageVersion: z.union([z.literal(0), z.literal(1)]), targetStorageVersion: z.literal(2),
12
+ action: z.enum(["migrate", "recover"]),
13
+ recordMappings: z.array(MigrationRecordMappingSchema).max(10_000).optional(),
14
+ }).strict();
@@ -0,0 +1,305 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, lstat, realpath, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { createRequire } from "node:module";
6
+ import { z } from "zod";
7
+ import { ExecutionCompletedSchema } from "./execution-protocol.js";
8
+ import { startDormantSupervisor } from "./execution-supervisor.js";
9
+ import { MemoryMigrationStartSchema } from "./memory-migration-protocol.js";
10
+ import { MigrationHistorySchema, MigrationIdentitySchema, MigrationStateSchema } from "./local-memory/migration-schema.js";
11
+ import { readMemoryInventorySource, inventoryLimits } from "./local-memory/inventory-source.js";
12
+ const digest = (value) => createHash("sha256").update(value).digest("hex");
13
+ const protectedWindows = new Map();
14
+ const Operation = z.object({ id: z.string(), revision: z.number().int(), executionId: z.string().nullable(),
15
+ status: z.string(), sourceDigest: z.string().nullable(), planDigest: z.string().nullable(),
16
+ writeIntentConfirmedAt: z.string().nullable(), newAgentConfirmed: z.boolean().optional(),
17
+ cancelRequestedAt: z.string().nullable(),
18
+ }).passthrough();
19
+ const Summary = z.object({ result: z.enum(["success", "failed", "needs_review", "repair_required", "missing"]),
20
+ phase: z.string().nullable().optional(), issues: z.array(z.object({ path: z.string(), reason: z.string() }).passthrough()) }).passthrough();
21
+ /** 系统维护在普通执行槽之前分流;只有 server 持久确认意图后才运行活动替换。 */
22
+ export async function runMemoryMigration(raw, deps) {
23
+ const frame = MemoryMigrationStartSchema.parse(raw);
24
+ const common = { agentId: frame.agentId, migrationId: frame.migrationId, machineId: frame.machineId,
25
+ executionId: frame.executionId, attemptKey: frame.attemptKey };
26
+ const protectionKey = `${deps.agentsRoot}:${frame.migrationId}`;
27
+ const exclusive = protectedWindows.get(protectionKey) ?? await deps.coordinator.tryAcquireExclusiveExecution();
28
+ if (!exclusive) {
29
+ await deps.control("busy", { ...common, expectedRevision: frame.expectedRevision });
30
+ return { kind: "busy" };
31
+ }
32
+ let revision = frame.expectedRevision, intentConfirmed = false, acquired = false, acquisitionRequested = false, keepProtection = false;
33
+ let bound;
34
+ let identity, state;
35
+ let serverHistory;
36
+ let intentReceipt;
37
+ let lastPid = null, stopped = true, phaseCount = 0;
38
+ const startedAt = new Date().toISOString();
39
+ const dir = join(deps.agentsRoot, frame.handle);
40
+ const timeoutMs = deps.timeoutMs ?? 60_000;
41
+ const guard = () => ({ ...common, expectedRevision: revision, ...bound });
42
+ const control = async (step, extra = {}) => {
43
+ const response = await deps.control(step, { ...guard(), ...extra });
44
+ const parsed = z.object({ operation: Operation }).parse(response);
45
+ revision = parsed.operation.revision;
46
+ return parsed.operation;
47
+ };
48
+ const readKnownState = async (path) => {
49
+ const source = await readMemoryInventorySource(await realpath(dir), path, inventoryLimits.fileBytes);
50
+ return MigrationStateSchema.parse(JSON.parse(source.content));
51
+ };
52
+ const readState = () => readKnownState(".crew/memory/migration.v1.json");
53
+ const runTool = async (command, extra = {}) => {
54
+ if (!identity)
55
+ throw new Error("migration_identity_missing");
56
+ if (command === "recover" && deps.cancellation?.isRequested())
57
+ extra = { ...extra, cancelled: true };
58
+ const rollback = command === "recover" && extra.cancelled === true;
59
+ if (!rollback && deps.cancellation?.isRequested())
60
+ throw new Error("migration_cancelled");
61
+ if (phaseCount++ > 0) {
62
+ if (!stopped || !deps.journal.retry)
63
+ throw new Error("migration_stop_unproven");
64
+ await deps.journal.retry(frame.executionId);
65
+ }
66
+ const sourceMode = import.meta.url.endsWith(".ts");
67
+ const entry = fileURLToPath(new URL(`./local-memory/migration-tool-entry.${sourceMode ? "ts" : "js"}`, import.meta.url));
68
+ const args = sourceMode ? ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href, entry] : [entry];
69
+ let stdout = "";
70
+ const launched = await deps.journal.startGuarded(frame.executionId, new Date().toISOString(), () => (deps.startSupervisor ?? startDormantSupervisor)({ command: process.execPath, args, cwd: dir,
71
+ env: { PATH: process.env.PATH ?? "", CREW_SLS_LOG: "off" },
72
+ stdinText: JSON.stringify({ command, agentDir: dir, identity, ...extra }),
73
+ }, sourceMode ? {
74
+ childEntry: fileURLToPath(new URL("./execution-supervisor-child.ts", import.meta.url)),
75
+ execArgv: ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href],
76
+ } : {}), { beforeRelease: async ({ handle }) => {
77
+ lastPid = handle.pid;
78
+ stopped = false;
79
+ handle.stdout.on("data", chunk => { stdout += String(chunk); if (stdout.length > 2 * 1024 * 1024)
80
+ void handle.cancel(); });
81
+ handle.stderr.resume();
82
+ } });
83
+ if (launched.kind !== "started")
84
+ throw new Error("migration_execution_already_running");
85
+ const processHandle = launched.handle;
86
+ deps.send({ type: "execution:started", protocolVersion: 1, executionId: frame.executionId, at: new Date().toISOString() });
87
+ let timedOut = false;
88
+ const timer = setTimeout(() => { timedOut = true; void processHandle.cancel(); }, timeoutMs);
89
+ if (!rollback)
90
+ deps.cancellation?.register(async () => { if (!stopped)
91
+ await processHandle.cancel(); });
92
+ try {
93
+ await processHandle.exit;
94
+ await processHandle.cancel();
95
+ stopped = true;
96
+ if (timedOut)
97
+ throw new Error("migration_window_timeout");
98
+ return Summary.parse(JSON.parse(stdout.trim()));
99
+ }
100
+ finally {
101
+ clearTimeout(timer);
102
+ }
103
+ };
104
+ const finish = async (outcome, reason) => {
105
+ const completion = ExecutionCompletedSchema.parse({ type: "execution:completed", protocolVersion: 1,
106
+ executionId: frame.executionId, runtime: "memory_migration", resumed: false, outcome,
107
+ exitCode: outcome === "succeeded" ? 0 : 1, ...(reason ? { errorCode: reason, errorMessage: reason } : {}),
108
+ startedAt, finishedAt: new Date().toISOString() });
109
+ await deps.journal.complete(frame.executionId, completion);
110
+ deps.send(completion);
111
+ };
112
+ try {
113
+ const history = z.object({ operations: z.array(Operation), serverHistory: MigrationHistorySchema }).passthrough().parse(await deps.control("history", {}));
114
+ serverHistory = history.serverHistory;
115
+ const operation = history.operations.find(op => op.id === frame.migrationId);
116
+ if (!operation || operation.revision !== revision || operation.executionId !== frame.executionId)
117
+ throw new Error("migration_server_identity_mismatch");
118
+ intentConfirmed = operation.writeIntentConfirmedAt !== null;
119
+ await mkdir(dir, { recursive: true });
120
+ const root = await realpath(dir);
121
+ if ((await lstat(dir)).isSymbolicLink())
122
+ throw new Error("migration_root_invalid");
123
+ bound = { storageRootFingerprint: digest(root), ownerIdentity: String((await lstat(root)).uid) };
124
+ identity = MigrationIdentitySchema.parse({ agentId: frame.agentId, migrationId: frame.migrationId,
125
+ machineId: frame.machineId, executionId: frame.executionId, workspaceId: frame.workspaceId,
126
+ rootFingerprint: bound.storageRootFingerprint, ownerIdentity: bound.ownerIdentity,
127
+ sourceStorageVersion: frame.sourceStorageVersion, targetStorageVersion: 2 });
128
+ const { reqId: _transportRequest, ...stableFrame } = frame;
129
+ const accepted = await deps.journal.accept(frame.executionId, digest(JSON.stringify(stableFrame)), { runtime: "memory_migration" });
130
+ if (accepted.kind === "existing" && accepted.completion) {
131
+ deps.send(accepted.completion);
132
+ return { kind: "completed" };
133
+ }
134
+ deps.send({ type: "execution:accepted", protocolVersion: 1, executionId: frame.executionId,
135
+ state: "ready", effectivePermission: "workspace_write", at: new Date().toISOString() });
136
+ acquisitionRequested = true;
137
+ await control("acquired");
138
+ acquired = true;
139
+ if (frame.action === "recover") {
140
+ state = await readState().catch(() => undefined);
141
+ if (state && state.migrationId !== frame.migrationId && state.phase === "rolled_back"
142
+ && history.serverHistory.rolledBackOperations?.some(previous => previous.migrationId === state.migrationId
143
+ && previous.sourceDigest === state.sourceDigest && previous.planDigest === state.planDigest))
144
+ state = undefined;
145
+ if (state) {
146
+ if (state.migrationId !== frame.migrationId || state.identity.rootFingerprint !== bound.storageRootFingerprint
147
+ || state.identity.machineId !== frame.machineId || state.identity.agentId !== frame.agentId)
148
+ throw new Error("migration_state_identity_mismatch");
149
+ identity = state.identity;
150
+ }
151
+ if (!state) {
152
+ state = await readKnownState(`.crew/memory/migrations/${frame.migrationId}/terminal.json`).catch((e) => {
153
+ if (e.code !== "ENOENT")
154
+ throw e;
155
+ return readKnownState(`.crew/memory/migrations/${frame.migrationId}/manifest.json`);
156
+ });
157
+ identity = state.identity;
158
+ }
159
+ if (state.planDigest !== operation.planDigest || state.sourceDigest !== operation.sourceDigest)
160
+ throw new Error("migration_server_plan_mismatch");
161
+ const prior = await deps.journal.get(identity.executionId);
162
+ if (state.phase !== "committed" && (!prior || (prior.state !== "completed" && prior.state !== "interrupted"))) {
163
+ throw new Error("migration_stop_unproven");
164
+ }
165
+ const result = await runTool("recover", { serverHistory: history.serverHistory, cancelled: !intentConfirmed || operation.cancelRequestedAt !== null,
166
+ stoppedReceipt: { kind: "migration_process_stopped", migrationId: frame.migrationId, machineId: frame.machineId,
167
+ executionId: identity.executionId, ownerIdentity: identity.ownerIdentity, pid: null,
168
+ stoppedAt: new Date().toISOString(), proof: "journal_reconciled" } });
169
+ if (result.result !== "success")
170
+ throw new Error("migration_recovery_failed");
171
+ }
172
+ else {
173
+ const prepared = await runTool("prepare", { serverHistory: history.serverHistory,
174
+ ...(frame.recordMappings ? { recordMappings: frame.recordMappings } : {}) });
175
+ if (prepared.result === "needs_review") {
176
+ await control("needs-review", { issues: prepared.issues });
177
+ await finish("failed", "migration_needs_review");
178
+ return { kind: "needs_review" };
179
+ }
180
+ if (prepared.result !== "success")
181
+ throw new Error("migration_prepare_failed");
182
+ state = await readState();
183
+ const confirmed = await control("intent", { sourceDigest: state.sourceDigest, planDigest: state.planDigest,
184
+ minReaderCapabilities: ["memory_storage_v2", "memory_state_guard_v1"], minWriterCapabilities: ["memory_storage_v2", "memory_state_guard_v1"] });
185
+ intentConfirmed = true;
186
+ intentReceipt = { ...identity, kind: "server_write_intent", sourceDigest: state.sourceDigest,
187
+ planDigest: state.planDigest, intentVersion: confirmed.revision, confirmedAt: confirmed.writeIntentConfirmedAt };
188
+ const result = await runTool("apply", { intentReceipt });
189
+ if (result.result !== "success")
190
+ throw new Error("migration_apply_failed");
191
+ }
192
+ state = await readState();
193
+ if (state.phase !== "committed" && state.phase !== "rolled_back")
194
+ throw new Error("migration_terminal_unverified");
195
+ if (!intentConfirmed) {
196
+ if (state.phase !== "rolled_back")
197
+ throw new Error("migration_commit_without_intent");
198
+ await control("no-write", { sourceDigest: state.sourceDigest, planDigest: state.planDigest, verified: true });
199
+ }
200
+ else
201
+ await control("result", { outcome: state.phase, storageVersion: state.storageVersion,
202
+ sourceDigest: state.sourceDigest, planDigest: state.planDigest, origin: state.origin, verified: true });
203
+ await finish("succeeded");
204
+ return { kind: "completed", phase: state.phase };
205
+ }
206
+ catch (error) {
207
+ const reason = error instanceof Error && /^[a-z_]+$/u.test(error.message) ? error.message : "migration_execution_failed";
208
+ if (acquisitionRequested) {
209
+ try {
210
+ const latest = z.object({ operations: z.array(Operation) }).passthrough().parse(await deps.control("history", {}))
211
+ .operations.find(op => op.id === frame.migrationId);
212
+ if (!latest || latest.executionId !== frame.executionId)
213
+ throw new Error("migration_control_owner_changed");
214
+ revision = latest.revision;
215
+ intentConfirmed = latest.writeIntentConfirmedAt !== null;
216
+ acquired = !["needs_review", "rolled_back"].includes(latest.status);
217
+ if (intentConfirmed && identity && state && latest.sourceDigest === state.sourceDigest && latest.planDigest === state.planDigest) {
218
+ intentReceipt = { ...identity, kind: "server_write_intent", sourceDigest: state.sourceDigest, planDigest: state.planDigest,
219
+ intentVersion: latest.revision, confirmedAt: latest.writeIntentConfirmedAt };
220
+ }
221
+ }
222
+ catch {
223
+ keepProtection = true;
224
+ }
225
+ }
226
+ if (acquired) {
227
+ if (intentConfirmed) {
228
+ keepProtection = true;
229
+ if (stopped && identity && lastPid !== null) {
230
+ try {
231
+ const recovered = await runTool("recover", { cancelled: true, ...(intentReceipt ? { intentReceipt } : {}),
232
+ stoppedReceipt: { kind: "migration_process_stopped", migrationId: frame.migrationId, machineId: frame.machineId,
233
+ executionId: identity.executionId, ownerIdentity: identity.ownerIdentity, pid: lastPid,
234
+ stoppedAt: new Date().toISOString(), proof: "supervisor_exit" } });
235
+ state = await readState();
236
+ if (recovered.result === "success" && (state.phase === "committed" || state.phase === "rolled_back")) {
237
+ await control("result", { outcome: state.phase, storageVersion: state.storageVersion,
238
+ sourceDigest: state.sourceDigest, planDigest: state.planDigest, origin: state.origin, verified: true });
239
+ keepProtection = false;
240
+ }
241
+ }
242
+ catch { /* 未证明提交或完整回退时保留保护。 */ }
243
+ }
244
+ if (keepProtection)
245
+ await control("repair", { reason }).catch(() => undefined);
246
+ }
247
+ else if (stopped) {
248
+ let digests;
249
+ try {
250
+ let local = await readState().catch((error) => { if (error.code === "ENOENT")
251
+ return undefined; throw error; });
252
+ if (local?.migrationId !== frame.migrationId) {
253
+ local = await readKnownState(`.crew/memory/migrations/${frame.migrationId}/manifest.json`).catch(async (error) => {
254
+ if (error.code !== "ENOENT")
255
+ throw error;
256
+ const residue = await lstat(join(dir, ".crew/memory/migrations", frame.migrationId)).catch((statError) => {
257
+ if (statError.code === "ENOENT")
258
+ return undefined;
259
+ throw statError;
260
+ });
261
+ if (residue)
262
+ throw new Error("migration_prepare_residue_unverified");
263
+ return undefined;
264
+ });
265
+ }
266
+ if (local?.migrationId === frame.migrationId) {
267
+ if (!identity || lastPid === null || Object.keys(local.identity).some(key => local.identity[key] !== identity[key])
268
+ || local.intentReceipt || !["prepared", "rolled_back"].includes(local.phase))
269
+ throw new Error("migration_no_write_state_unverified");
270
+ digests = { sourceDigest: local.sourceDigest, planDigest: local.planDigest };
271
+ const recovered = await runTool("recover", { cancelled: true,
272
+ serverHistory: { kind: "existing", migrationId: frame.migrationId, writeIntentConfirmed: false,
273
+ ...(serverHistory?.rolledBackOperations ? { rolledBackOperations: serverHistory.rolledBackOperations } : {}) },
274
+ stoppedReceipt: { kind: "migration_process_stopped", migrationId: frame.migrationId, machineId: frame.machineId,
275
+ executionId: identity.executionId, ownerIdentity: identity.ownerIdentity, pid: lastPid,
276
+ stoppedAt: new Date().toISOString(), proof: "supervisor_exit" } });
277
+ state = await readState();
278
+ if (recovered.result !== "success" || state.phase !== "rolled_back")
279
+ throw new Error("migration_no_write_rollback_unverified");
280
+ digests = { sourceDigest: state.sourceDigest, planDigest: state.planDigest };
281
+ }
282
+ await control("no-write", { verified: true, ...digests });
283
+ keepProtection = false;
284
+ }
285
+ catch {
286
+ keepProtection = true;
287
+ await control("repair", { reason: "migration_no_write_rollback_unverified", ...digests }).catch(() => undefined);
288
+ }
289
+ }
290
+ else
291
+ keepProtection = true;
292
+ }
293
+ if (stopped)
294
+ await finish("failed", reason).catch(() => undefined);
295
+ return { kind: "failed", reason, processStopped: stopped, lastPid };
296
+ }
297
+ finally {
298
+ if (!keepProtection && stopped) {
299
+ protectedWindows.delete(protectionKey);
300
+ await exclusive.release();
301
+ }
302
+ else
303
+ protectedWindows.set(protectionKey, exclusive);
304
+ }
305
+ }