@nowcrew/daemon 0.6.92 → 0.6.93-beta.2
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.
- package/dist/cloud-report-upload.js +1 -10
- package/dist/execution-journal.js +3 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +3 -1
- package/dist/local-execution-prompt.js +8 -3
- package/dist/local-executor.js +64 -35
- package/dist/local-memory/execution-workspace.js +55 -0
- package/dist/local-memory/full-index-search.js +47 -0
- package/dist/local-memory/index-budget.js +9 -3
- package/dist/local-memory/initialization-identity.js +23 -0
- package/dist/local-memory/inventory-cli.js +23 -1
- package/dist/local-memory/inventory-source.js +15 -8
- package/dist/local-memory/legacy-adapter.js +233 -0
- package/dist/local-memory/migration-access.js +184 -0
- package/dist/local-memory/migration-cli.js +54 -0
- package/dist/local-memory/migration-io.js +199 -0
- package/dist/local-memory/migration-plan.js +283 -0
- package/dist/local-memory/migration-schema.js +55 -0
- package/dist/local-memory/migration-store.js +429 -0
- package/dist/local-memory/migration-tool-entry.js +21 -0
- package/dist/local-memory/prompt.js +20 -4
- package/dist/local-memory/read-cli.js +75 -77
- package/dist/local-memory/rollout.js +54 -8
- package/dist/local-memory/runtime.js +148 -13
- package/dist/local-memory/storage-format.js +152 -0
- package/dist/local-memory/write.js +125 -19
- package/dist/local-memory-diagnostics.js +41 -2
- package/dist/local-memory-telemetry.js +19 -2
- package/dist/machine-info.js +3 -0
- package/dist/marketplace-protocol.js +1 -1
- package/dist/marketplace-remote.js +1 -1
- package/dist/marketplace-runner.js +1 -1
- package/dist/memory-migration-controller.js +84 -0
- package/dist/memory-migration-inspect.js +75 -0
- package/dist/memory-migration-journal.js +14 -0
- package/dist/memory-migration-protocol.js +14 -0
- package/dist/memory-migration-runtime.js +305 -0
- package/dist/memory-prune-diagnostics.js +60 -4
- package/dist/message-file-preview.js +11 -1
- package/dist/prompt.js +5 -1
- package/dist/runner.js +1 -1
- package/dist/serve.js +36 -3
- package/dist/token.js +1 -0
- package/dist/workspace-control-handler.js +16 -3
- package/dist/workspace.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
cancel(executionId) { const run = runs.get(executionId); if (!run)
|
|
14
|
+
return false; run.cancel(); return true; },
|
|
15
|
+
async handle(raw) {
|
|
16
|
+
const type = raw?.type;
|
|
17
|
+
if (type !== "memory:migration:start" && type !== "memory:migration:inspect")
|
|
18
|
+
return false;
|
|
19
|
+
const parsed = type === "memory:migration:start" ? MemoryMigrationStartSchema.safeParse(raw) : Inspect.safeParse(raw);
|
|
20
|
+
if (!parsed.success) {
|
|
21
|
+
const reqId = raw.reqId;
|
|
22
|
+
if (typeof reqId === "string" && reqId.length <= 200)
|
|
23
|
+
input.send({ type: "fs:result", reqId, ok: false, error: "memory_migration_invalid_request" });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const request = parsed.data;
|
|
27
|
+
if (!identity || request.workspaceId !== identity.workspaceId || request.machineId !== identity.machineId) {
|
|
28
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_identity_mismatch" });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
let journal;
|
|
32
|
+
try {
|
|
33
|
+
journal = input.journal();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_journal_unavailable" });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (request.type === "memory:migration:inspect") {
|
|
40
|
+
try {
|
|
41
|
+
const data = await inspectMemoryForMigration(input.config.agentsRoot, request, journal);
|
|
42
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: true, data });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_inspection_failed" });
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const existing = runs.get(request.executionId);
|
|
50
|
+
if (existing) {
|
|
51
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: true, data: { kind: "running" } });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (input.isAdmissionClosed?.()) {
|
|
55
|
+
input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_storage_busy" });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const cancellation = createRuntimeCancellation();
|
|
59
|
+
const promise = runMemoryMigration(request, {
|
|
60
|
+
agentsRoot: input.config.agentsRoot, coordinator: input.coordinator, journal,
|
|
61
|
+
cancellation: cancellation.cancellation, send: input.send,
|
|
62
|
+
control: async (step, body) => {
|
|
63
|
+
const base = `/daemon/memory-migrations/${encodeURIComponent(request.agentId)}`;
|
|
64
|
+
const path = step === "history" ? `${base}/history` : `${base}/${request.migrationId}/${step === "needs-review" ? "review" : step}`;
|
|
65
|
+
const response = await fetch(new URL(path, input.config.serverUrl), {
|
|
66
|
+
method: step === "history" ? "GET" : "POST", signal: AbortSignal.timeout(10_000),
|
|
67
|
+
headers: { authorization: `Bearer ${input.config.machineToken}`, "content-type": "application/json" },
|
|
68
|
+
...(step === "history" ? {} : { body: JSON.stringify(body) }),
|
|
69
|
+
});
|
|
70
|
+
const result = await response.json();
|
|
71
|
+
if (!response.ok || result.success !== true)
|
|
72
|
+
throw new Error("migration_control_unconfirmed");
|
|
73
|
+
return result.data;
|
|
74
|
+
},
|
|
75
|
+
}).then(data => input.send({ type: "fs:result", reqId: request.reqId, ok: true, data }))
|
|
76
|
+
.catch(() => input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_failed" }))
|
|
77
|
+
.finally(() => runs.delete(request.executionId));
|
|
78
|
+
runs.set(request.executionId, { promise, cancel: cancellation.request });
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
async stop() { for (const run of runs.values())
|
|
82
|
+
run.cancel(); await Promise.allSettled([...runs.values()].map(run => run.promise)); },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./local-memory/storage-format.js";
|
|
7
|
+
import { resolveLocalMemoryV2Rollout } from "./local-memory/rollout.js";
|
|
8
|
+
import { readMemoryInventorySource, inventoryLimits } from "./local-memory/inventory-source.js";
|
|
9
|
+
export async function inspectMemoryForMigration(agentsRoot, input, journal) {
|
|
10
|
+
const parent = await realpath(agentsRoot);
|
|
11
|
+
const path = join(parent, input.handle);
|
|
12
|
+
const base = { workspaceId: input.workspaceId, agentId: input.agentId, machineId: input.machineId,
|
|
13
|
+
storageRootFingerprint: createHash("sha256").update(resolve(path)).digest("hex"),
|
|
14
|
+
ownerIdentity: String((await lstat(parent)).uid) };
|
|
15
|
+
const prior = input.executionId ? await journal?.get(input.executionId) : null;
|
|
16
|
+
const stoppedExecutionId = prior?.requestedRuntime === "memory_migration" && (prior.state === "completed" || prior.state === "interrupted") ? prior.executionId : null;
|
|
17
|
+
const empty = { ...base, storageVersion: null, classification: "empty", emptyWithoutResidue: true, localState: null, issueCodes: [], stoppedExecutionId };
|
|
18
|
+
try {
|
|
19
|
+
await lstat(path);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (e.code === "ENOENT")
|
|
23
|
+
return empty;
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return await withMemoryAccess(path, async () => {
|
|
28
|
+
let localState = null;
|
|
29
|
+
try {
|
|
30
|
+
const record = await readMemoryInventorySource(path, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
|
|
31
|
+
const state = MigrationStateSchema.parse(JSON.parse(record.content));
|
|
32
|
+
if (state.identity.rootFingerprint !== base.storageRootFingerprint || state.identity.agentId !== input.agentId
|
|
33
|
+
|| state.origin !== "initialized" && (state.identity.workspaceId !== input.workspaceId
|
|
34
|
+
|| state.identity.machineId !== input.machineId))
|
|
35
|
+
throw new Error("migration_identity_mismatch");
|
|
36
|
+
localState = { migrationId: state.migrationId, phase: state.phase, storageVersion: state.storageVersion,
|
|
37
|
+
sourceDigest: state.sourceDigest, planDigest: state.planDigest };
|
|
38
|
+
if (state.phase === "applying" || state.phase === "repair_required")
|
|
39
|
+
return {
|
|
40
|
+
...empty, localState, classification: "repair_required", emptyWithoutResidue: false, issueCodes: ["migration_recovery_required"],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (e.code !== "ENOENT")
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
let source;
|
|
48
|
+
try {
|
|
49
|
+
source = await readMemoryInventorySource(path, "MEMORY.md", inventoryLimits.fileBytes);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (e.code !== "ENOENT")
|
|
53
|
+
throw e;
|
|
54
|
+
const names = await readdir(path);
|
|
55
|
+
const nonMetadata = names.filter(name => name !== ".crew");
|
|
56
|
+
return { ...empty, localState, emptyWithoutResidue: nonMetadata.length === 0 && localState === null,
|
|
57
|
+
classification: nonMetadata.length === 0 && localState === null ? "empty" : "repair_required", issueCodes: ["memory_source_missing"] };
|
|
58
|
+
}
|
|
59
|
+
const storageVersion = detectLocalMemoryStorageVersion(source.content);
|
|
60
|
+
if (storageVersion === 2) {
|
|
61
|
+
parseFullMemoryIndex(source.content);
|
|
62
|
+
return { ...base, storageVersion, classification: "valid_v2", emptyWithoutResidue: false, localState, issueCodes: [], stoppedExecutionId };
|
|
63
|
+
}
|
|
64
|
+
const eligibility = await resolveLocalMemoryV2Rollout({ workspace: { dir: path, memory: source.content },
|
|
65
|
+
serverUrl: "https://nowwork-dev.nowcoder.com", handle: input.handle, requested: true });
|
|
66
|
+
return { ...base, storageVersion, classification: storageVersion === 1 && eligibility.enabled ? "valid_v1"
|
|
67
|
+
: eligibility.legacyFallback || eligibility.enabled ? "legacy" : "repair_required",
|
|
68
|
+
emptyWithoutResidue: false, localState, stoppedExecutionId, issueCodes: eligibility.enabled || eligibility.legacyFallback ? [] : ["memory_source_incompatible"] };
|
|
69
|
+
}, { maintenance: true, waitMs: 0 });
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const code = error instanceof Error && /^[a-z_]+$/u.test(error.message) ? error.message : "memory_inspection_failed";
|
|
73
|
+
return { ...empty, emptyWithoutResidue: false, classification: code === "memory_migration_access_busy" ? "busy" : "repair_required", issueCodes: [code] };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -7,6 +7,7 @@ import { dslog } from "./slog.js";
|
|
|
7
7
|
const TRACE_PATTERN = /\[memory-prune trace_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]/i;
|
|
8
8
|
export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
9
9
|
export const MEMORY_PRUNE_NOTES_MANIFEST_LIMIT = 128;
|
|
10
|
+
export const MEMORY_PRUNE_PENDING_MANIFEST_LIMIT = 128;
|
|
10
11
|
export const MEMORY_PRUNE_NOTE_CHANGE_LIMIT = 128;
|
|
11
12
|
function compareCodeUnits(left, right) {
|
|
12
13
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
@@ -192,6 +193,50 @@ export async function inspectMemoryPruneNotes(homeDir, signal) {
|
|
|
192
193
|
...(signal?.aborted ? { error: "ABORT_ERR" } : {}),
|
|
193
194
|
};
|
|
194
195
|
}
|
|
196
|
+
export async function inspectMemoryPrunePending(homeDir, signal) {
|
|
197
|
+
const pendingDir = join(homeDir, ".crew", "memory", "pending");
|
|
198
|
+
try {
|
|
199
|
+
const metadata = await lstat(pendingDir);
|
|
200
|
+
if (metadata.isSymbolicLink())
|
|
201
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: "symbolic_link" };
|
|
202
|
+
if (!metadata.isDirectory())
|
|
203
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: "ENOTDIR" };
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error.code === "ENOENT")
|
|
207
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false };
|
|
208
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: diagnosticError(error) };
|
|
209
|
+
}
|
|
210
|
+
let names;
|
|
211
|
+
try {
|
|
212
|
+
names = (await readdir(pendingDir, { withFileTypes: true }))
|
|
213
|
+
.filter(entry => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".json"))
|
|
214
|
+
.map(entry => entry.name).sort(compareCodeUnits);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
return { files: [], total_count: 0, omitted_count: 0, truncated: false, error: diagnosticError(error) };
|
|
218
|
+
}
|
|
219
|
+
const selected = names.slice(0, MEMORY_PRUNE_PENDING_MANIFEST_LIMIT);
|
|
220
|
+
const files = [];
|
|
221
|
+
for (const name of selected) {
|
|
222
|
+
if (signal?.aborted)
|
|
223
|
+
break;
|
|
224
|
+
const path = join(pendingDir, name);
|
|
225
|
+
try {
|
|
226
|
+
if ((await lstat(path)).isSymbolicLink()) {
|
|
227
|
+
files.push({ relative_path: `.crew/memory/pending/${name}`, exists: false, error: "symbolic_link" });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
files.push({ relative_path: `.crew/memory/pending/${name}`, exists: false, error: diagnosticError(error) });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
files.push({ relative_path: `.crew/memory/pending/${name}`, ...await inspectFile(path, false, undefined, signal) });
|
|
236
|
+
}
|
|
237
|
+
return { files, total_count: names.length, omitted_count: Math.max(0, names.length - selected.length),
|
|
238
|
+
truncated: names.length > selected.length, ...(signal?.aborted ? { error: "ABORT_ERR" } : {}) };
|
|
239
|
+
}
|
|
195
240
|
async function inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal) {
|
|
196
241
|
const cwdIsHome = runDir === homeDir;
|
|
197
242
|
const [memory, lessons, cwdMemory, cwdLessons, workLog] = await Promise.all([
|
|
@@ -210,15 +255,17 @@ async function inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal
|
|
|
210
255
|
};
|
|
211
256
|
}
|
|
212
257
|
export async function inspectMemoryPruneFiles(homeDir, runDir, workLogPath, signal) {
|
|
213
|
-
const [fixedFiles, notesManifest] = await Promise.all([
|
|
258
|
+
const [fixedFiles, notesManifest, pendingManifest] = await Promise.all([
|
|
214
259
|
inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal),
|
|
215
260
|
inspectMemoryPruneNotes(homeDir, signal),
|
|
261
|
+
inspectMemoryPrunePending(homeDir, signal),
|
|
216
262
|
]);
|
|
217
|
-
return { ...fixedFiles, notes_manifest: notesManifest };
|
|
263
|
+
return { ...fixedFiles, notes_manifest: notesManifest, pending_manifest: pendingManifest };
|
|
218
264
|
}
|
|
219
265
|
export async function inspectMemoryPruneFilesWithinDeadline(homeDir, runDir, workLogPath, timeoutMs) {
|
|
220
266
|
const fixedFiles = await withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneFixedFiles(homeDir, runDir, workLogPath, signal), timeoutMs);
|
|
221
267
|
let notesManifest;
|
|
268
|
+
let pendingManifest;
|
|
222
269
|
try {
|
|
223
270
|
notesManifest = await withinMemoryPruneDiagnosticsDeadline((signal) => inspectMemoryPruneNotes(homeDir, signal), timeoutMs);
|
|
224
271
|
}
|
|
@@ -231,7 +278,13 @@ export async function inspectMemoryPruneFilesWithinDeadline(homeDir, runDir, wor
|
|
|
231
278
|
error: diagnosticError(error),
|
|
232
279
|
};
|
|
233
280
|
}
|
|
234
|
-
|
|
281
|
+
try {
|
|
282
|
+
pendingManifest = await withinMemoryPruneDiagnosticsDeadline(signal => inspectMemoryPrunePending(homeDir, signal), timeoutMs);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
pendingManifest = { files: [], total_count: 0, omitted_count: 0, truncated: false, error: diagnosticError(error) };
|
|
286
|
+
}
|
|
287
|
+
return { ...fixedFiles, notes_manifest: notesManifest, pending_manifest: pendingManifest };
|
|
235
288
|
}
|
|
236
289
|
export async function withinMemoryPruneDiagnosticsDeadline(operation, timeoutMs) {
|
|
237
290
|
const controller = new AbortController();
|
|
@@ -361,6 +414,7 @@ export function evaluateMemoryPrunePostcondition(before, after, execution) {
|
|
|
361
414
|
&& (after.cwd_memory.lessons_reference_count ?? 0) > 0,
|
|
362
415
|
cwd_lessons_changed: !after.cwd_lessons.same_as_shared_path
|
|
363
416
|
&& fileChanged(before.cwd_lessons, after.cwd_lessons),
|
|
417
|
+
pending_changed: diffMemoryPruneNotes(before.pending_manifest ?? { files: [], total_count: 0, omitted_count: 0, truncated: false }, after.pending_manifest ?? { files: [], total_count: 0, omitted_count: 0, truncated: false }).change_count > 0,
|
|
364
418
|
};
|
|
365
419
|
let outcome;
|
|
366
420
|
if (!execution.executorCompleted)
|
|
@@ -388,6 +442,8 @@ export function evaluateMemoryPrunePostcondition(before, after, execution) {
|
|
|
388
442
|
outcome = "promoted_without_index_change";
|
|
389
443
|
else if (facts.shared_memory_changed)
|
|
390
444
|
outcome = "shared_memory_updated";
|
|
445
|
+
else if (facts.pending_changed)
|
|
446
|
+
outcome = "pending_updated";
|
|
391
447
|
else
|
|
392
448
|
outcome = "no_files_changed";
|
|
393
449
|
return { outcome, ...facts };
|
|
@@ -408,7 +464,7 @@ function memoryPruneFileFactFields(fileFacts) {
|
|
|
408
464
|
}
|
|
409
465
|
return fields;
|
|
410
466
|
}
|
|
411
|
-
export function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, ...fileFacts }) {
|
|
467
|
+
export function memoryPruneSnapshotFields({ notes_manifest: _notesManifest, pending_manifest: _pendingManifest, ...fileFacts }) {
|
|
412
468
|
return memoryPruneFileFactFields(fileFacts);
|
|
413
469
|
}
|
|
414
470
|
export function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
|
|
@@ -9,6 +9,7 @@ import { z } from "zod";
|
|
|
9
9
|
import { isInternalEnvironmentPath } from "./workspace-fs.js";
|
|
10
10
|
import { opaqueKey, safeKey } from "./workspace.js";
|
|
11
11
|
import { AgentHandleSchema } from "./execution-protocol.js";
|
|
12
|
+
import { withMemoryAccess } from "./local-memory/migration-access.js";
|
|
12
13
|
const MAX_BYTES = 256 * 1024;
|
|
13
14
|
const MAX_BINARY_BYTES = 8 * 1024 * 1024;
|
|
14
15
|
const MAX_CONCURRENT_READS = 4;
|
|
@@ -231,7 +232,16 @@ export async function readSharedMessageFile(agentsRoot, rawRequest, registry) {
|
|
|
231
232
|
throw new SharedFileError("file_preview_busy");
|
|
232
233
|
activeReads++;
|
|
233
234
|
try {
|
|
234
|
-
|
|
235
|
+
const candidate = await resolveCandidate(agentsRoot, parsed.data, registry);
|
|
236
|
+
const agentRoot = await realpath(resolve(agentsRoot, parsed.data.handle)).catch((error) => {
|
|
237
|
+
if (error.code === "ENOENT")
|
|
238
|
+
return undefined;
|
|
239
|
+
throw error;
|
|
240
|
+
});
|
|
241
|
+
// 记忆保护只用于该Agent根内资料,独立项目文件仍按原执行授权读取。
|
|
242
|
+
return await (agentRoot && inside(agentRoot, candidate.path)
|
|
243
|
+
? withMemoryAccess(agentRoot, () => readCandidate(candidate))
|
|
244
|
+
: readCandidate(candidate));
|
|
235
245
|
}
|
|
236
246
|
catch (error) {
|
|
237
247
|
if (error instanceof SharedFileError)
|