@nowcrew/daemon 0.6.99 → 0.6.100-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 (66) hide show
  1. package/dist/cloud-report-upload.js +1 -10
  2. package/dist/computer-main.js +0 -0
  3. package/dist/console.js +17 -2
  4. package/dist/execution-journal.js +3 -1
  5. package/dist/execution-protocol.js +1 -1
  6. package/dist/execution-runner.js +4 -1
  7. package/dist/local-execution-prompt.js +8 -3
  8. package/dist/local-executor.js +78 -55
  9. package/dist/local-memory/commit-telemetry.js +30 -0
  10. package/dist/local-memory/execution-workspace.js +62 -0
  11. package/dist/local-memory/full-index-search.js +47 -0
  12. package/dist/local-memory/index-budget.js +9 -3
  13. package/dist/local-memory/initialization-identity.js +36 -0
  14. package/dist/local-memory/inventory-cli.js +52 -8
  15. package/dist/local-memory/inventory-source.js +15 -8
  16. package/dist/local-memory/inventory-v2.js +362 -0
  17. package/dist/local-memory/legacy-adapter.js +233 -0
  18. package/dist/local-memory/migration-access.js +242 -0
  19. package/dist/local-memory/migration-cli.js +54 -0
  20. package/dist/local-memory/migration-io.js +199 -0
  21. package/dist/local-memory/migration-pending.js +9 -0
  22. package/dist/local-memory/migration-plan.js +432 -0
  23. package/dist/local-memory/migration-schema.js +55 -0
  24. package/dist/local-memory/migration-state-invariants.js +65 -0
  25. package/dist/local-memory/migration-store.js +414 -0
  26. package/dist/local-memory/migration-tool-entry.js +21 -0
  27. package/dist/local-memory/pending-query.js +5 -2
  28. package/dist/local-memory/pending-schema.js +1 -1
  29. package/dist/local-memory/pending.js +16 -2
  30. package/dist/local-memory/prompt.js +21 -5
  31. package/dist/local-memory/read-cli.js +98 -76
  32. package/dist/local-memory/release-artifact-guard.js +55 -0
  33. package/dist/local-memory/rollout.js +79 -11
  34. package/dist/local-memory/runtime.js +191 -17
  35. package/dist/local-memory/source-path.js +13 -0
  36. package/dist/local-memory/storage-format.js +249 -0
  37. package/dist/local-memory/write.js +190 -21
  38. package/dist/local-memory/writer-mode.js +13 -0
  39. package/dist/local-memory-diagnostics.js +96 -15
  40. package/dist/local-memory-telemetry.js +20 -3
  41. package/dist/machine-info.js +6 -1
  42. package/dist/main.js +0 -0
  43. package/dist/marketplace-controller.js +21 -8
  44. package/dist/marketplace-maintenance-bridge.js +79 -0
  45. package/dist/marketplace-maintenance-protocol.js +40 -0
  46. package/dist/marketplace-protocol.js +1 -1
  47. package/dist/marketplace-remote.js +1 -1
  48. package/dist/marketplace-runner.js +1 -1
  49. package/dist/marketplace-serve.js +4 -2
  50. package/dist/memory-migration-controller.js +89 -0
  51. package/dist/memory-migration-inspect.js +108 -0
  52. package/dist/memory-migration-journal.js +14 -0
  53. package/dist/memory-migration-protocol.js +14 -0
  54. package/dist/memory-migration-runtime.js +305 -0
  55. package/dist/memory-prune-diagnostics.js +74 -4
  56. package/dist/message-file-preview.js +11 -1
  57. package/dist/prompt.js +9 -17
  58. package/dist/runner.js +1 -1
  59. package/dist/runtimes/claude-thread-runner.js +25 -4
  60. package/dist/runtimes/pi-runner.js +1 -0
  61. package/dist/runtimes/pi.js +54 -20
  62. package/dist/serve.js +39 -4
  63. package/dist/token.js +1 -0
  64. package/dist/workspace-control-handler.js +16 -3
  65. package/dist/workspace.js +13 -2
  66. package/package.json +9 -10
@@ -1,3 +1,4 @@
1
+ import { MaintenanceRequestSchema, MaintenanceReportSchema } from "./marketplace-maintenance-protocol.js";
1
2
  import { MarketplaceRequestSchema, marketplaceActive } from "./marketplace-protocol.js";
2
3
  import { resolveMarketplaceScopes, runMarketplaceScope } from "./marketplace-runner.js";
3
4
  export function createMarketplaceController(deps) {
@@ -11,22 +12,31 @@ export function createMarketplaceController(deps) {
11
12
  const deadline = now() + 10 * 60_000;
12
13
  let lease = null;
13
14
  try {
14
- const scopes = await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot);
15
+ const scopes = request.type === "marketplace:operation" ? await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot) : [];
15
16
  // 查询不使用任务执行槽;只有写入运行时共用市场/插件目录时才等待独占保护。
16
- if (request.mode === "update") {
17
+ if (request.type === "marketplace:maintenance" || request.mode === "update") {
17
18
  while (!stopped && now() < deadline && lease === null) {
18
19
  lease = await deps.acquire();
19
20
  if (!lease)
20
21
  await wait();
21
22
  }
22
23
  if (!lease || stopped) {
23
- operations.set(id, { operationId: id, status: "failed", results: [], errorCode: "runtime_busy" });
24
+ operations.set(id, { ...operations.get(id), status: "failed", errorCode: "runtime_busy" });
24
25
  return;
25
26
  }
26
27
  }
27
28
  if (stopped || now() >= deadline)
28
29
  throw new Error("operation_deadline");
29
- operations.set(id, { operationId: id, status: "running", results: [], errorCode: null });
30
+ operations.set(id, { ...operations.get(id), status: "running" });
31
+ if (request.type === "marketplace:maintenance") {
32
+ if (!deps.maintain)
33
+ throw new Error("maintenance_unavailable");
34
+ const result = MaintenanceReportSchema.parse(await deps.maintain(request));
35
+ if (result.operationId !== id)
36
+ throw new Error("operation_mismatch");
37
+ operations.set(id, result);
38
+ return;
39
+ }
30
40
  const results = [];
31
41
  for (const scope of scopes) {
32
42
  if (stopped || now() >= deadline)
@@ -41,7 +51,9 @@ export function createMarketplaceController(deps) {
41
51
  }
42
52
  catch {
43
53
  const previous = operations.get(id);
44
- operations.set(id, { ...previous, status: previous.results.some(x => !x.errorCode) ? "partial" : "failed", errorCode: "execution_failed" });
54
+ operations.set(id, request.type === "marketplace:maintenance"
55
+ ? { ...previous, status: "unknown", errorCode: "operation_unknown" }
56
+ : { ...previous, status: previous.results.some(x => !x.errorCode) ? "partial" : "failed", errorCode: "execution_failed" });
45
57
  }
46
58
  finally {
47
59
  try {
@@ -55,22 +67,23 @@ export function createMarketplaceController(deps) {
55
67
  };
56
68
  return {
57
69
  handle(raw) {
58
- const parsed = MarketplaceRequestSchema.safeParse(raw);
70
+ const parsed = MarketplaceRequestSchema.or(MaintenanceRequestSchema).safeParse(raw);
59
71
  if (!parsed.success || stopped)
60
72
  return { ok: false, error: "invalid_request" };
61
73
  const request = parsed.data;
74
+ const version = request.type === "marketplace:maintenance" ? { version: 2 } : {};
62
75
  const existing = operations.get(request.operationId);
63
76
  if (existing)
64
77
  return { ok: true, data: existing };
65
78
  if (request.action === "status")
66
- return { ok: true, data: { operationId: request.operationId, status: "unknown", results: [], errorCode: "operation_unknown" } };
79
+ return { ok: true, data: { ...version, operationId: request.operationId, status: "unknown", results: [], errorCode: "operation_unknown" } };
67
80
  if (process.platform !== "darwin" && process.platform !== "linux")
68
81
  return { ok: false, error: "unsupported_platform" };
69
82
  if (current !== null || [...operations.values()].some(x => marketplaceActive(x.status)))
70
83
  return { ok: false, error: "runtime_busy" };
71
84
  if (operations.size >= 100)
72
85
  operations.delete(operations.keys().next().value);
73
- const accepted = { operationId: request.operationId, status: "queued", results: [], errorCode: null };
86
+ const accepted = { ...version, operationId: request.operationId, status: "queued", results: [], errorCode: null };
74
87
  operations.set(request.operationId, accepted);
75
88
  current = execute(request).finally(() => { current = null; });
76
89
  return { ok: true, data: accepted };
@@ -0,0 +1,79 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { augmentedPath } from "./runtime-path.js";
7
+ import { MaintenanceReportSchema } from "./marketplace-maintenance-protocol.js";
8
+ export function maintenanceEnvironment(env) {
9
+ // Keep the machine user's login/config, never forward control-plane credentials.
10
+ return Object.fromEntries(Object.entries(env).filter(([key]) => !/^(CREW_|NOWCREW_|NOWWORK_)/i.test(key)));
11
+ }
12
+ async function supervise(file, id, cwd, env) {
13
+ await new Promise((resolve, reject) => {
14
+ const child = spawn(process.execPath, [file, "--run", id], { cwd, env, detached: true, stdio: "ignore" });
15
+ let expired = false;
16
+ const kill = () => { if (child.pid) {
17
+ try {
18
+ process.kill(-child.pid, "SIGKILL");
19
+ }
20
+ catch { /* already gone */ }
21
+ } };
22
+ const timer = setTimeout(() => { expired = true; kill(); }, 18 * 60_000);
23
+ child.once("error", () => { clearTimeout(timer); kill(); reject(new Error("worker_failed")); });
24
+ child.once("close", code => {
25
+ clearTimeout(timer);
26
+ // Never release the machine lock while descendants can still write shared configuration.
27
+ kill();
28
+ if (expired || code !== 0)
29
+ reject(new Error("worker_failed"));
30
+ else
31
+ resolve();
32
+ });
33
+ });
34
+ }
35
+ export async function runMaintenanceBridge(config, request, acquireEnv) {
36
+ const fail = (errorCode) => ({ version: 2, operationId: request.operationId, status: "failed", results: [], errorCode });
37
+ let dir;
38
+ let started = false;
39
+ try {
40
+ const response = await fetch(`${config.serverUrl.replace(/\/+$/, "")}/machine/marketplace-maintenance/${request.operationId}/runner/${request.plan.runner.releaseId}`, {
41
+ headers: { authorization: `Bearer ${config.machineToken}` }, redirect: "error", signal: AbortSignal.timeout(15_000),
42
+ });
43
+ if (!response.ok)
44
+ return fail("runner_unavailable");
45
+ // Bound even a malformed server response before buffering or executing it.
46
+ if (!response.body)
47
+ return fail("runner_unavailable");
48
+ const chunks = [];
49
+ let bytes = 0;
50
+ for await (const chunk of response.body) {
51
+ bytes += chunk.length;
52
+ if (bytes > 256_000)
53
+ return fail("runner_unavailable");
54
+ chunks.push(chunk);
55
+ }
56
+ const content = Buffer.concat(chunks);
57
+ if (createHash("sha256").update(content).digest("hex") !== request.plan.runner.sha256)
58
+ return fail("runner_unavailable");
59
+ const env = maintenanceEnvironment(await acquireEnv());
60
+ env.PATH = augmentedPath(env, {}, process.platform, "append");
61
+ dir = await mkdtemp(join(tmpdir(), "nowwork-maintenance-"));
62
+ const file = join(dir, "runner.mjs");
63
+ await writeFile(file, content, { mode: 0o600 });
64
+ started = true;
65
+ await supervise(file, request.operationId, dir, env);
66
+ const report = MaintenanceReportSchema.parse(JSON.parse(await readFile(join(dir, "report.json"), "utf8")));
67
+ if (report.operationId !== request.operationId)
68
+ throw new Error("operation_mismatch");
69
+ return report;
70
+ }
71
+ catch {
72
+ // A killed worker may have changed files: require readback/manual confirmation before another write.
73
+ return started ? { ...fail("operation_unknown"), status: "unknown" } : fail("runner_unavailable");
74
+ }
75
+ finally {
76
+ if (dir)
77
+ await rm(dir, { recursive: true, force: true });
78
+ }
79
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ // 与 daemon 同名文件逐字一致;这是独立于旧单插件协议的版本化契约。
3
+ export const MAINTENANCE_CAPABILITY = "marketplace_codex_maintenance_v1";
4
+ export const MaintenancePlanSchema = z.object({
5
+ marketplace: z.literal("nowwork-pm"),
6
+ actions: z.tuple([z.literal("install-missing"), z.literal("update-installed")]),
7
+ hosts: z.tuple([z.literal("claude"), z.literal("codex"), z.literal("pi")]),
8
+ runner: z.object({ releaseId: z.string().regex(/^[a-f0-9]{64}$/), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict(),
9
+ }).strict();
10
+ export const MaintenanceRequestSchema = z.discriminatedUnion("action", [
11
+ z.object({ type: z.literal("marketplace:maintenance"), action: z.literal("start"), operationId: z.string().uuid(), plan: MaintenancePlanSchema }).strict(),
12
+ z.object({ type: z.literal("marketplace:maintenance"), action: z.literal("status"), operationId: z.string().uuid() }).strict(),
13
+ ]);
14
+ export const MaintenanceErrorSchema = z.enum(["runtime_busy", "execution_failed", "operation_unknown", "runner_unavailable",
15
+ "manager_missing", "cli_missing", "command_failed", "verification_failed", "source_conflict", "unsupported_platform"]);
16
+ const Version = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/).nullable();
17
+ export const MaintenanceResultSchema = z.object({
18
+ runtime: z.enum(["claude", "codex", "pi"]), plugin: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,127}$/).nullable(),
19
+ status: z.enum(["installed", "updated", "unchanged", "empty", "missing_cli", "failed"]),
20
+ beforeVersion: Version, afterVersion: Version, targetVersion: Version,
21
+ activation: z.enum(["reload_required", "not_changed", "unverified"]), errorCode: MaintenanceErrorSchema.nullable(),
22
+ }).strict();
23
+ export const MaintenanceReportSchema = z.object({
24
+ version: z.literal(2), operationId: z.string().uuid(),
25
+ status: z.enum(["queued", "running", "completed", "partial", "failed", "unknown"]),
26
+ results: z.array(MaintenanceResultSchema).max(1500), errorCode: MaintenanceErrorSchema.nullable(),
27
+ }).strict().superRefine((r, ctx) => {
28
+ const keys = r.results.map(x => `${x.runtime}:${x.plugin}`);
29
+ if (new Set(keys).size !== keys.length)
30
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Duplicate plugin result" });
31
+ for (const x of r.results) {
32
+ if (["installed", "updated", "unchanged"].includes(x.status) && (!x.plugin || !x.targetVersion || x.targetVersion !== x.afterVersion || x.errorCode)) {
33
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Successful plugin result requires a verified target version" });
34
+ }
35
+ }
36
+ if (r.status === "completed" && (r.errorCode || ["claude", "codex", "pi"].some(host => !r.results.some(x => x.runtime === host))
37
+ || r.results.some(x => x.errorCode || x.status === "failed" || x.status === "missing_cli"))) {
38
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Completion requires verified coverage of every host" });
39
+ }
40
+ });
@@ -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);
@@ -1,7 +1,9 @@
1
+ import { runMaintenanceBridge } from "./marketplace-maintenance-bridge.js";
1
2
  import { createMarketplaceController } from "./marketplace-controller.js";
2
3
  import { prepareDaemonRestart } from "./daemon-updater.js";
3
- export function createServeMarketplace(config, localSlots, hostCoordinator) {
4
+ export function createServeMarketplace(config, localSlots, hostCoordinator, acquireEnv = async () => ({ ...process.env })) {
4
5
  const controller = createMarketplaceController({ agentsRoot: config.agentsRoot,
6
+ maintain: request => runMaintenanceBridge(config, request, acquireEnv),
5
7
  acquire: async () => {
6
8
  // Marketplace updates can mutate shared Codex/Claude homes across profiles.
7
9
  const local = await prepareDaemonRestart({ localSlots });
@@ -29,7 +31,7 @@ export function createServeMarketplace(config, localSlots, hostCoordinator) {
29
31
  } });
30
32
  return {
31
33
  handle(raw, send) {
32
- if (!raw || typeof raw !== "object" || !("type" in raw) || raw.type !== "marketplace:operation")
34
+ if (!raw || typeof raw !== "object" || !("type" in raw) || (raw.type !== "marketplace:operation" && raw.type !== "marketplace:maintenance"))
33
35
  return false;
34
36
  const { reqId, ...request } = raw;
35
37
  if (typeof reqId === "string")
@@ -0,0 +1,89 @@
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, {
44
+ ...request,
45
+ serverUrl: input.config.serverUrl,
46
+ }, journal);
47
+ input.send({ type: "fs:result", reqId: request.reqId, ok: true, data });
48
+ }
49
+ catch {
50
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_inspection_failed" });
51
+ }
52
+ return true;
53
+ }
54
+ const existing = runs.get(request.executionId);
55
+ if (existing) {
56
+ input.send({ type: "fs:result", reqId: request.reqId, ok: true, data: { kind: "running" } });
57
+ return true;
58
+ }
59
+ if (input.isAdmissionClosed?.()) {
60
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_storage_busy" });
61
+ return true;
62
+ }
63
+ const cancellation = createRuntimeCancellation();
64
+ const promise = runMemoryMigration(request, {
65
+ agentsRoot: input.config.agentsRoot, coordinator: input.coordinator, journal,
66
+ cancellation: cancellation.cancellation, send: input.send,
67
+ control: async (step, body) => {
68
+ const base = `/daemon/memory-migrations/${encodeURIComponent(request.agentId)}`;
69
+ const path = step === "history" ? `${base}/history` : `${base}/${request.migrationId}/${step === "needs-review" ? "review" : step}`;
70
+ const response = await fetch(new URL(path, input.config.serverUrl), {
71
+ method: step === "history" ? "GET" : "POST", signal: AbortSignal.timeout(10_000),
72
+ headers: { authorization: `Bearer ${input.config.machineToken}`, "content-type": "application/json" },
73
+ ...(step === "history" ? {} : { body: JSON.stringify(body) }),
74
+ });
75
+ const result = await response.json();
76
+ if (!response.ok || result.success !== true)
77
+ throw new Error("migration_control_unconfirmed");
78
+ return result.data;
79
+ },
80
+ }).then(data => input.send({ type: "fs:result", reqId: request.reqId, ok: true, data }))
81
+ .catch(() => input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_failed" }))
82
+ .finally(() => runs.delete(request.executionId));
83
+ runs.set(request.executionId, { promise, cancel: cancellation.request });
84
+ return true;
85
+ },
86
+ async stop() { for (const run of runs.values())
87
+ run.cancel(); await Promise.allSettled([...runs.values()].map(run => run.promise)); },
88
+ };
89
+ }
@@ -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: input.serverUrl, 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();