@nowcrew/daemon 0.6.91-beta.3 → 0.6.91-beta.4
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/daemon-update-controller.js +16 -8
- package/dist/daemon-updater.js +8 -10
- package/dist/execution-journal.js +3 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +3 -1
- package/dist/host-execution-coordinator.js +12 -6
- package/dist/local-execution-prompt.js +8 -3
- package/dist/local-executor.js +14 -35
- package/dist/local-memory/execution-workspace.js +36 -0
- package/dist/local-memory/full-index-search.js +47 -0
- package/dist/local-memory/index-budget.js +9 -3
- package/dist/local-memory/migration-access.js +118 -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 +278 -0
- package/dist/local-memory/migration-schema.js +55 -0
- package/dist/local-memory/migration-store.js +424 -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 +36 -1
- package/dist/local-memory/rollout.js +53 -8
- package/dist/local-memory/runtime.js +5 -2
- package/dist/local-memory/storage-format.js +152 -0
- package/dist/local-memory/write.js +108 -17
- package/dist/machine-info.js +3 -0
- package/dist/memory-migration-controller.js +84 -0
- package/dist/memory-migration-inspect.js +74 -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/message-file-preview.js +11 -1
- package/dist/pre-admission-rejection.js +5 -4
- package/dist/prompt.js +5 -1
- package/dist/runner.js +1 -1
- package/dist/serve.js +37 -4
- package/dist/shared-execution-slots.js +2 -0
- package/dist/token.js +1 -0
- package/dist/workspace-control-handler.js +16 -3
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
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.identity.workspaceId !== input.workspaceId || state.identity.machineId !== input.machineId)
|
|
34
|
+
throw new Error("migration_identity_mismatch");
|
|
35
|
+
localState = { migrationId: state.migrationId, phase: state.phase, storageVersion: state.storageVersion,
|
|
36
|
+
sourceDigest: state.sourceDigest, planDigest: state.planDigest };
|
|
37
|
+
if (state.phase === "applying" || state.phase === "repair_required")
|
|
38
|
+
return {
|
|
39
|
+
...empty, localState, classification: "repair_required", emptyWithoutResidue: false, issueCodes: ["migration_recovery_required"],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
if (e.code !== "ENOENT")
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
46
|
+
let source;
|
|
47
|
+
try {
|
|
48
|
+
source = await readMemoryInventorySource(path, "MEMORY.md", inventoryLimits.fileBytes);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
if (e.code !== "ENOENT")
|
|
52
|
+
throw e;
|
|
53
|
+
const names = await readdir(path);
|
|
54
|
+
const nonMetadata = names.filter(name => name !== ".crew");
|
|
55
|
+
return { ...empty, localState, emptyWithoutResidue: nonMetadata.length === 0 && localState === null,
|
|
56
|
+
classification: nonMetadata.length === 0 && localState === null ? "empty" : "repair_required", issueCodes: ["memory_source_missing"] };
|
|
57
|
+
}
|
|
58
|
+
const storageVersion = detectLocalMemoryStorageVersion(source.content);
|
|
59
|
+
if (storageVersion === 2) {
|
|
60
|
+
parseFullMemoryIndex(source.content);
|
|
61
|
+
return { ...base, storageVersion, classification: "valid_v2", emptyWithoutResidue: false, localState, issueCodes: [], stoppedExecutionId };
|
|
62
|
+
}
|
|
63
|
+
const eligibility = await resolveLocalMemoryV2Rollout({ workspace: { dir: path, memory: source.content },
|
|
64
|
+
serverUrl: "https://nowwork-dev.nowcoder.com", handle: input.handle, requested: true });
|
|
65
|
+
return { ...base, storageVersion, classification: storageVersion === 1 && eligibility.enabled ? "valid_v1"
|
|
66
|
+
: eligibility.legacyFallback || eligibility.enabled ? "legacy" : "repair_required",
|
|
67
|
+
emptyWithoutResidue: false, localState, stoppedExecutionId, issueCodes: eligibility.enabled || eligibility.legacyFallback ? [] : ["memory_source_incompatible"] };
|
|
68
|
+
}, { maintenance: true, waitMs: 0 });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const code = error instanceof Error && /^[a-z_]+$/u.test(error.message) ? error.message : "memory_inspection_failed";
|
|
72
|
+
return { ...empty, emptyWithoutResidue: false, classification: code === "memory_migration_access_busy" ? "busy" : "repair_required", issueCodes: [code] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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)
|
|
@@ -2,7 +2,7 @@ import { ExecutionCompletedSchema, ExecutionRejectedSchema } from "./execution-p
|
|
|
2
2
|
import { hashExecutionSpec } from "./execution-runner.js";
|
|
3
3
|
import { dslog } from "./slog.js";
|
|
4
4
|
/** Queue rejection is admission work too: own the ID before any asynchronous journal write. */
|
|
5
|
-
export async function rejectQueuedAdmission(context, spec, executionClass, facts) {
|
|
5
|
+
export async function rejectQueuedAdmission(context, spec, executionClass, facts, rejectionReason = "machine_queue_full") {
|
|
6
6
|
const id = spec.executionId;
|
|
7
7
|
const hash = hashExecutionSpec(spec);
|
|
8
8
|
if (context.knownExecutionHashes.has(id) || context.executionRuns.has(id)) {
|
|
@@ -16,10 +16,11 @@ export async function rejectQueuedAdmission(context, spec, executionClass, facts
|
|
|
16
16
|
context.knownExecutionHashes.set(id, hash);
|
|
17
17
|
const rejected = ExecutionRejectedSchema.parse({
|
|
18
18
|
type: "execution:rejected", protocolVersion: 1, executionId: id,
|
|
19
|
-
reason: "resource_limit", message: "Local machine
|
|
19
|
+
reason: "resource_limit", message: rejectionReason === "maintenance" ? "Local machine is temporarily unavailable for maintenance"
|
|
20
|
+
: rejectionReason === "agent_queue_full" ? "Local agent execution queue is full" : "Local machine execution queue is full", at: new Date().toISOString(),
|
|
20
21
|
});
|
|
21
|
-
dslog("execution.machine_queue_rejected", "
|
|
22
|
-
level: "WARN", execution_id: id, agent_handle: spec.agent.handle, execution_class: executionClass, ...facts,
|
|
22
|
+
dslog("execution.machine_queue_rejected", rejectionReason === "maintenance" ? "机器维护期间暂停接收执行" : "执行等待队列已满", {
|
|
23
|
+
level: "WARN", execution_id: id, agent_handle: spec.agent.handle, execution_class: executionClass, admission_reason: rejectionReason, ...facts,
|
|
23
24
|
});
|
|
24
25
|
const rejecting = persistPreAdmissionRejection(context.journal, spec, rejected)
|
|
25
26
|
.then(async (entry) => {
|
package/dist/prompt.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* claim-before-work、freshness/draft、"做完所有事再停"、inbox notice 语义、
|
|
6
6
|
* 分层记忆与压缩安全、协作礼仪。措辞为本项目原创。
|
|
7
7
|
*/
|
|
8
|
+
import { detectLocalMemoryStorageVersion, selectLocalMemoryPreview } from "./local-memory/storage-format.js";
|
|
8
9
|
/** work-log 注入上限(字符):防冷启动注入随文件膨胀。超限保头(目标/结论)+尾(最近进展),
|
|
9
10
|
* 截中段并提示读全文——全文仍在盘上,只限"默认推送量"不限"可获取量"。纯函数。 */
|
|
10
11
|
export const WORKLOG_INJECT_CAP = 8000;
|
|
@@ -67,7 +68,10 @@ export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
|
|
|
67
68
|
const tail = workLog.slice(-WORKLOG_TAIL);
|
|
68
69
|
return `${head}\n…(work-log 过长已截去中段,全文用 Read 读 $CREW_TASK_LOG)\n${tail}`;
|
|
69
70
|
}
|
|
70
|
-
export function capMemoryForInject(memory, cap = MEMORY_INJECT_CAP, maxBytes = Number.POSITIVE_INFINITY) {
|
|
71
|
+
export function capMemoryForInject(memory, cap = MEMORY_INJECT_CAP, maxBytes = Number.POSITIVE_INFINITY, query = "") {
|
|
72
|
+
if (detectLocalMemoryStorageVersion(memory) === 2) {
|
|
73
|
+
return selectLocalMemoryPreview(memory, query, { maxCodePoints: cap, maxBytes }).memory;
|
|
74
|
+
}
|
|
71
75
|
if (memory.length <= cap && Buffer.byteLength(memory, "utf8") <= maxBytes)
|
|
72
76
|
return memory;
|
|
73
77
|
const notice = "[MEMORY.md preview truncated; use crew-memory search/read for more.]\n";
|
package/dist/runner.js
CHANGED
|
@@ -80,7 +80,7 @@ async function runAgentWithMachineEnvironment(config, input, onActivity = defaul
|
|
|
80
80
|
? { scheduledExternalNotificationPolicy: input.scheduled.externalNotificationPolicy }
|
|
81
81
|
: {}),
|
|
82
82
|
...(input.wakeOrigin ? { wakeOrigin: input.wakeOrigin } : {}),
|
|
83
|
-
memory: capMemoryForInject(workspace.memory),
|
|
83
|
+
memory: capMemoryForInject(workspace.memory, undefined, undefined, baseWake),
|
|
84
84
|
...(resuming ? {} : { workLog: capWorkLogForInject(workspace.workLog) }),
|
|
85
85
|
collaborationToolsEnabled: config.codexCollaborationTools && runtime === "codex",
|
|
86
86
|
localMemoryV2Enabled: workspace.localMemoryV2Enabled ?? localMemoryV2Enabled,
|
package/dist/serve.js
CHANGED
|
@@ -14,6 +14,7 @@ import { reconcileExecutionJournal } from "./execution-recovery.js";
|
|
|
14
14
|
import { initReleaseSlog, reportDaemonConnectionClose, reportReleaseError } from "./release-channel-guard.js";
|
|
15
15
|
import { handleWorkspaceControlRequest } from "./workspace-control-handler.js";
|
|
16
16
|
import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
|
|
17
|
+
import { withMemoryAccess } from "./local-memory/migration-access.js";
|
|
17
18
|
import { normalizeScheduledContext } from "./scheduled-report.js";
|
|
18
19
|
import { formatDaemonLogLine } from "./log-format.js";
|
|
19
20
|
import { reportAgentRunComplete } from "./scheduled-run-report.js";
|
|
@@ -50,6 +51,8 @@ import { probeRuntimeHealth } from "./runtime-health.js";
|
|
|
50
51
|
import { createServeMarketplace } from "./marketplace-serve.js";
|
|
51
52
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
52
53
|
import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
|
|
54
|
+
import { createMemoryMigrationController } from "./memory-migration-controller.js";
|
|
55
|
+
import { createMemoryMigrationJournal, replayExecutionJournals } from "./memory-migration-journal.js";
|
|
53
56
|
import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
|
|
54
57
|
export { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
55
58
|
// normalize.ts 的活动种类 → activity 枚举
|
|
@@ -71,6 +74,8 @@ export function serve(config, opts = {}) {
|
|
|
71
74
|
let reconnectTimer = null;
|
|
72
75
|
let stopPromise = null;
|
|
73
76
|
let executionJournal;
|
|
77
|
+
let migrationExecutionJournal;
|
|
78
|
+
let migrationJournalReady = false;
|
|
74
79
|
const injectedExecutionJournal = opts.execution?.journal;
|
|
75
80
|
const agentMemory = opts.execution?.dependencies?.agentMemory
|
|
76
81
|
?? (config.agentMemory === null ? undefined : createAgentMemoryBridge(config.agentMemory));
|
|
@@ -85,6 +90,15 @@ export function serve(config, opts = {}) {
|
|
|
85
90
|
startupGapMs: config.executionLimits.startupGapMs,
|
|
86
91
|
});
|
|
87
92
|
const localSlots = createSharedSlotManager(config.executionLimits);
|
|
93
|
+
const memoryMigrations = createMemoryMigrationController({ config, coordinator: hostCoordinator,
|
|
94
|
+
isAdmissionClosed: () => stopped || storageIdentityAdmission.isAdmissionClosed(),
|
|
95
|
+
journal: () => { if (!migrationJournalReady)
|
|
96
|
+
throw new Error("memory_migration_journal_unavailable"); return migrationExecutionJournal; }, send: frame => {
|
|
97
|
+
if (frame.type === "execution:completed")
|
|
98
|
+
completionRetransmitter.track(frame);
|
|
99
|
+
else if (ws?.readyState === WebSocket.OPEN)
|
|
100
|
+
ws.send(JSON.stringify(frame));
|
|
101
|
+
} });
|
|
88
102
|
const sharedSlots = hostCoordinatedSlotManager(localSlots, hostCoordinator);
|
|
89
103
|
const concurrencyTelemetry = createExecutionConcurrencyTelemetry(sharedSlots, {
|
|
90
104
|
...(opts.execution?.concurrencySnapshotIntervalMs === undefined
|
|
@@ -232,7 +246,7 @@ export function serve(config, opts = {}) {
|
|
|
232
246
|
return null;
|
|
233
247
|
};
|
|
234
248
|
const sendSnapshot = async (reqId) => {
|
|
235
|
-
const entries = (await executionJournal
|
|
249
|
+
const entries = (await replayExecutionJournals(executionJournal, migrationExecutionJournal)).map(snapshotEntry).filter((entry) => entry !== null);
|
|
236
250
|
safeExecutionSend(ExecutionSnapshotSchema.parse({
|
|
237
251
|
type: "execution:snapshot", protocolVersion: 1, reqId, entries,
|
|
238
252
|
}));
|
|
@@ -313,6 +327,12 @@ export function serve(config, opts = {}) {
|
|
|
313
327
|
machineRuntime.open(openedSocket, { capabilities: capabilities.machineHello, beforePublish: Promise.resolve(), connectionId });
|
|
314
328
|
opts.onOpen?.(ws);
|
|
315
329
|
void sendSnapshot(`reconnect-${randomUUID()}`);
|
|
330
|
+
// 普通回执不等待维护日志IO;两类记录分别重放到同一发送器。
|
|
331
|
+
void migrationExecutionJournal.replay().then(entries => {
|
|
332
|
+
for (const entry of entries)
|
|
333
|
+
if (entry.completion && !entry.completionAcknowledged)
|
|
334
|
+
completionRetransmitter.track(entry.completion);
|
|
335
|
+
}).catch(() => dslog("memory_migration.journal_unavailable", "维护回执重放失败,普通回执继续", { level: "ERROR" }));
|
|
316
336
|
void executionJournal.replay()
|
|
317
337
|
.then((entries) => {
|
|
318
338
|
for (const entry of entries) {
|
|
@@ -387,7 +407,8 @@ export function serve(config, opts = {}) {
|
|
|
387
407
|
return;
|
|
388
408
|
if (frame.type === "execution:completion-ack") {
|
|
389
409
|
try {
|
|
390
|
-
await executionJournal.
|
|
410
|
+
const journal = await executionJournal.get(frame.executionId) ? executionJournal : migrationExecutionJournal;
|
|
411
|
+
await journal.acknowledgeCompletion(frame.executionId);
|
|
391
412
|
const attempts = completionRetransmitter.acknowledge(frame.executionId);
|
|
392
413
|
dslog("execution.completion_acknowledged", "execution completion ACK 已持久化", {
|
|
393
414
|
execution_id: frame.executionId,
|
|
@@ -413,6 +434,8 @@ export function serve(config, opts = {}) {
|
|
|
413
434
|
return;
|
|
414
435
|
}
|
|
415
436
|
if (frame.type === "execution:cancel") {
|
|
437
|
+
if (memoryMigrations.cancel(frame.executionId))
|
|
438
|
+
return;
|
|
416
439
|
if (!knownExecutionHashes.has(frame.executionId)
|
|
417
440
|
&& !executionRuns.has(frame.executionId)
|
|
418
441
|
&& await executionJournal.get(frame.executionId) === null)
|
|
@@ -488,7 +511,7 @@ export function serve(config, opts = {}) {
|
|
|
488
511
|
let reservation = sharedSlots.reserve(spec.agent.handle, "execution", executionClass);
|
|
489
512
|
if (!reservation.accepted) {
|
|
490
513
|
await rejectQueuedAdmission({ journal: executionJournal, knownExecutionHashes, executionRuns,
|
|
491
|
-
report: reportExecutionFrame, replay: sendJournalStatus }, spec, executionClass, reservation.facts);
|
|
514
|
+
report: reportExecutionFrame, replay: sendJournalStatus }, spec, executionClass, reservation.facts, reservation.rejectionReason);
|
|
492
515
|
return;
|
|
493
516
|
}
|
|
494
517
|
knownExecutionHashes.set(spec.executionId, hash);
|
|
@@ -735,6 +758,8 @@ export function serve(config, opts = {}) {
|
|
|
735
758
|
// ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
|
|
736
759
|
const r = msg;
|
|
737
760
|
setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
|
|
761
|
+
if (r.workspaceId && r.machineId)
|
|
762
|
+
memoryMigrations.setIdentity({ workspaceId: r.workspaceId, machineId: r.machineId });
|
|
738
763
|
if (opts.storageRuntime !== undefined && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
|
|
739
764
|
const bindingSocket = ws;
|
|
740
765
|
bindStorageServerIdentity(opts.storageRuntime, { workspaceId: r.workspaceId, machineId: r.machineId }, {
|
|
@@ -801,6 +826,8 @@ export function serve(config, opts = {}) {
|
|
|
801
826
|
}
|
|
802
827
|
catch { /* server 会按请求超时处理 */ } }))
|
|
803
828
|
return;
|
|
829
|
+
if (await memoryMigrations.handle(msg))
|
|
830
|
+
return;
|
|
804
831
|
if (await agentAbilityRuntime.tryHandleControlMessage(msg, serverCapabilities, (frame) => ws?.send(JSON.stringify(frame))))
|
|
805
832
|
return;
|
|
806
833
|
// 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
|
|
@@ -815,7 +842,7 @@ export function serve(config, opts = {}) {
|
|
|
815
842
|
try {
|
|
816
843
|
const data2 = req.type === "raft:inspect"
|
|
817
844
|
? await inspectRaftWorkspace(req.path)
|
|
818
|
-
: await importRaftWorkspace(req.path, join(config.agentsRoot, req.handle));
|
|
845
|
+
: await withMemoryAccess(join(config.agentsRoot, req.handle), () => importRaftWorkspace(req.path, join(config.agentsRoot, req.handle)), { create: true });
|
|
819
846
|
if (req.type === "raft:import") {
|
|
820
847
|
log(`📦 已导入 raft 工作区 → ${req.handle}: 复制 ${data2.copied.length} 项`);
|
|
821
848
|
}
|
|
@@ -1259,6 +1286,9 @@ export function serve(config, opts = {}) {
|
|
|
1259
1286
|
writeStderr: (line) => process.stderr.write(line),
|
|
1260
1287
|
});
|
|
1261
1288
|
await runProjectSkillStartupCleanup(config.agentsRoot, executionJournal, dslog);
|
|
1289
|
+
migrationExecutionJournal = createMemoryMigrationJournal(config.agentsRoot);
|
|
1290
|
+
await migrationExecutionJournal.reconcileAfterRestart().then(() => { migrationJournalReady = true; })
|
|
1291
|
+
.catch(() => dslog("memory_migration.journal_unavailable", "迁移日志恢复失败,普通执行继续,迁移请求单独报错", { level: "ERROR" }));
|
|
1262
1292
|
await connect();
|
|
1263
1293
|
})();
|
|
1264
1294
|
const stop = () => {
|
|
@@ -1276,6 +1306,7 @@ export function serve(config, opts = {}) {
|
|
|
1276
1306
|
const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
|
|
1277
1307
|
const pending = [
|
|
1278
1308
|
machineRuntime.stop(),
|
|
1309
|
+
memoryMigrations.stop(),
|
|
1279
1310
|
webSocketClosed,
|
|
1280
1311
|
updateController.drain(),
|
|
1281
1312
|
marketplace.stop(),
|
|
@@ -1294,6 +1325,8 @@ export function serve(config, opts = {}) {
|
|
|
1294
1325
|
if (executionJournal !== undefined) {
|
|
1295
1326
|
await executionJournal.close({ signal: deadline.signal });
|
|
1296
1327
|
}
|
|
1328
|
+
if (migrationExecutionJournal !== undefined)
|
|
1329
|
+
await migrationExecutionJournal.close({ signal: deadline.signal });
|
|
1297
1330
|
}
|
|
1298
1331
|
finally {
|
|
1299
1332
|
deadline.dispose();
|