@nowcrew/daemon 0.6.91-beta.11 → 0.6.91-beta.13
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/computer-main.js +0 -0
- 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 +18 -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/inventory-cli.js +21 -1
- package/dist/local-memory/inventory-source.js +15 -8
- package/dist/local-memory/legacy-adapter.js +221 -0
- package/dist/local-memory/migration-access.js +169 -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 +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 +75 -77
- package/dist/local-memory/rollout.js +53 -8
- package/dist/local-memory/runtime.js +6 -2
- package/dist/local-memory/storage-format.js +152 -0
- package/dist/local-memory/write.js +125 -19
- package/dist/local-runtime-environment.js +6 -1
- package/dist/machine-info.js +3 -0
- package/dist/main.js +0 -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/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/package.json +9 -8
package/dist/computer-main.js
CHANGED
|
File without changes
|
|
@@ -8,7 +8,7 @@ import { provePosixProcessGroupStopped } from "./execution-posix-stop-proof.js";
|
|
|
8
8
|
export { JournalLockedError, JournalLockCorruptionError } from "./execution-journal-lock.js";
|
|
9
9
|
const ExecutionIdSchema = z.string().uuid();
|
|
10
10
|
const TimestampSchema = z.string().datetime({ offset: true });
|
|
11
|
-
const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "pi"]);
|
|
11
|
+
const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "memory_migration", "pi"]);
|
|
12
12
|
const RUNTIME_READY_SUFFIX = ".runtime-ready";
|
|
13
13
|
const RawJournalEntrySchema = z.object({
|
|
14
14
|
executionId: ExecutionIdSchema,
|
|
@@ -523,6 +523,8 @@ export function createExecutionJournal(agentsRoot, options = {}) {
|
|
|
523
523
|
}
|
|
524
524
|
};
|
|
525
525
|
const pruneInternal = async () => {
|
|
526
|
+
if (options.retainCompleted)
|
|
527
|
+
return;
|
|
526
528
|
const protectedExecutionIds = new Set(options.protectedExecutionIds?.() ?? []);
|
|
527
529
|
const entries = await readAll();
|
|
528
530
|
const acknowledged = entries
|
|
@@ -319,7 +319,7 @@ const RawExecutionCompletedSchema = z.object({
|
|
|
319
319
|
providerErrorCategory: ProviderErrorCategorySchema.optional(),
|
|
320
320
|
providerHttpStatus: ProviderHttpStatusSchema.optional(),
|
|
321
321
|
providerErrorCode: ProviderErrorCodeSchema.optional(),
|
|
322
|
-
runtime: RuntimeSchema,
|
|
322
|
+
runtime: z.union([RuntimeSchema, z.literal("memory_migration")]),
|
|
323
323
|
model: z.string().optional(),
|
|
324
324
|
resumed: z.boolean(),
|
|
325
325
|
finalText: z.string().optional(),
|
package/dist/execution-runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { isLocalMemoryV2Enabled, localMemoryV2Environment, resolveReposRoot } from "./config.js";
|
|
4
|
+
import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
|
|
4
5
|
import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
|
|
5
6
|
import { JournalConflictError } from "./execution-journal.js";
|
|
6
7
|
import { boundExecutionFrame } from "./execution-event-limit.js";
|
|
@@ -838,8 +839,9 @@ export async function runExecution(config, input, dependencies) {
|
|
|
838
839
|
const localMemoryGuardInBase = localMemoryV2Environment(config.serverUrl) === "custom";
|
|
839
840
|
const boundedSystemPrompt = (context) => {
|
|
840
841
|
const enabled = context.workspace.localMemoryV2Enabled ?? localMemoryV2Enabled;
|
|
841
|
-
const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
|
|
842
|
+
const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase && detectLocalMemoryStorageVersion(context.workspace.memory) !== 2 ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
|
|
842
843
|
...(selfTraining ? { isolateLocalMemory: true } : {}),
|
|
844
|
+
memoryQuery: spec.instructions.wakePrompt,
|
|
843
845
|
collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
|
|
844
846
|
});
|
|
845
847
|
const base = typeof systemPromptWithLocalFacts === "string"
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* 本机执行提示词的合成与预算裁剪。
|
|
3
3
|
*
|
|
4
4
|
* server 提示词是权威部分,绝不裁剪;项目工作区提示词是绑定执行的强制部分,放不下就直接失败;
|
|
5
|
-
*
|
|
5
|
+
* 本机事实与旧格式预览可按预算省略;schema2身份和置顶规则必须保留,放不下则拒绝执行。
|
|
6
6
|
*/
|
|
7
|
+
import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
|
|
7
8
|
import { buildCollaborationToolsPrompt, buildRuntimeWorkspacePrompt, capMemoryForInject, capWorkLogForInject, } from "./prompt.js";
|
|
8
9
|
const trainingProjectJson = (project) => JSON.stringify({
|
|
9
10
|
repositoryKey: project.repositoryKey,
|
|
@@ -57,10 +58,14 @@ export function withLocalExecutionFacts(serverPrompt, maxBytes, options = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
const remaining = maxBytes - requiredBytes - Math.max(0, reservedPromptBytes);
|
|
59
60
|
const factsBytes = Buffer.byteLength(localFacts, "utf8");
|
|
60
|
-
if (factsBytes > remaining)
|
|
61
|
+
if (factsBytes > remaining) {
|
|
62
|
+
if (!options.isolateLocalMemory && detectLocalMemoryStorageVersion(workspace.memory) === 2) {
|
|
63
|
+
throw new Error("local_memory_identity_budget_exceeded");
|
|
64
|
+
}
|
|
61
65
|
return `${serverPrompt}${collaborationPrompt}${mandatoryWorkspacePrompt}`;
|
|
66
|
+
}
|
|
62
67
|
const memoryHeading = "\n\n## Injected MEMORY.md (bounded local context)\n";
|
|
63
|
-
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory, undefined, remaining - factsBytes - Buffer.byteLength(memoryHeading, "utf8"));
|
|
68
|
+
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory, undefined, remaining - factsBytes - Buffer.byteLength(memoryHeading, "utf8"), options.memoryQuery);
|
|
64
69
|
const memorySection = memory === "" ? "" : memoryHeading + memory;
|
|
65
70
|
const workLogSection = workLog === "" ? "" : `\n\n## Injected work-log (bounded local context)\n${workLog}`;
|
|
66
71
|
const workLogBudget = remaining - factsBytes - Buffer.byteLength(memorySection, "utf8");
|
package/dist/local-executor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createInterface } from "node:readline";
|
|
|
6
6
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { cleanupSelfTrainingRunDir } from "./self-training-workspace.js";
|
|
9
|
-
import {
|
|
9
|
+
import { rotateAgentSession, safeKey, } from "./workspace.js";
|
|
10
10
|
import { CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV, isClaudeAdditionalDirectoryInstructionsSupported, probeClaudeVersion, spawnClaude, } from "./runtimes/claude.js";
|
|
11
11
|
import { spawnCodex } from "./runtimes/codex.js";
|
|
12
12
|
import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
|
|
@@ -42,10 +42,13 @@ import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLoc
|
|
|
42
42
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
43
43
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
44
44
|
import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
|
|
45
|
-
import { commitLocalMemoryResultAfterExit,
|
|
45
|
+
import { commitLocalMemoryResultAfterExit, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
|
|
46
46
|
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
47
|
-
import { reportLocalMemoryV2Deferred
|
|
47
|
+
import { reportLocalMemoryV2Deferred } from "./local-memory/rollout.js";
|
|
48
|
+
import { prepareExecutionMemoryWorkspace } from "./local-memory/execution-workspace.js";
|
|
48
49
|
import { localMemoryV2Environment } from "./config.js";
|
|
50
|
+
import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
|
|
51
|
+
import { MEMORY_STATE_GUARD_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY } from "./memory-migration-protocol.js";
|
|
49
52
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
50
53
|
constructor(runtime, timeoutMs) {
|
|
51
54
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -211,37 +214,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
211
214
|
: "high"
|
|
212
215
|
: runtime.reasoning;
|
|
213
216
|
const providerFp = providerFingerprint(runtime.name, providerConfig);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
agentsRoot: input.launch.agentsRoot,
|
|
218
|
-
handle: input.handle,
|
|
219
|
-
cliPath: input.launch.cliPath,
|
|
220
|
-
executionId: input.executionId,
|
|
221
|
-
...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
|
|
222
|
-
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
223
|
-
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
224
|
-
...(input.sessionContextFingerprint === undefined
|
|
225
|
-
? {}
|
|
226
|
-
: { sessionContextFingerprint: input.sessionContextFingerprint }),
|
|
227
|
-
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
228
|
-
...(isolatedMaintenance ? { skipLocalMemory: true } : {}),
|
|
229
|
-
}), dependencies.cancellation);
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
logLocalMemoryContextPrepareFailure({
|
|
233
|
-
executionId: input.executionId,
|
|
234
|
-
agentHandle: input.handle,
|
|
235
|
-
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
236
|
-
}, "workspace_prepare", error);
|
|
237
|
-
throw error;
|
|
238
|
-
}
|
|
239
|
-
const memoryRollout = await resolveLocalMemoryV2Rollout({ workspace, handle: input.handle,
|
|
240
|
-
serverUrl: input.launch.serverUrl, requested: input.localMemoryV2Enabled === true,
|
|
241
|
-
selfTraining: isolatedMaintenance, ...(input.launch.description ? { description: input.launch.description } : {}) });
|
|
242
|
-
workspace = { ...workspace, localMemoryV2Enabled: memoryRollout.enabled };
|
|
217
|
+
const preparedMemory = await prepareExecutionMemoryWorkspace(input, isolatedMaintenance, dependencies.cancellation);
|
|
218
|
+
let workspace = preparedMemory.workspace;
|
|
219
|
+
const memoryRollout = preparedMemory.mode;
|
|
243
220
|
input = { ...input, localMemoryV2Enabled: memoryRollout.enabled };
|
|
244
|
-
const localMemoryV2Initialized =
|
|
221
|
+
const localMemoryV2Initialized = preparedMemory.initialized;
|
|
245
222
|
let materialized = null;
|
|
246
223
|
let knownAttachmentDirectory = null;
|
|
247
224
|
const startupReservationState = { current: null };
|
|
@@ -391,7 +368,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
391
368
|
let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
392
369
|
memoryPruneTraceId = isolatedMaintenance ? null : parseMemoryPruneTraceId(baseWakePrompt);
|
|
393
370
|
if (memoryRollout.deferred)
|
|
394
|
-
reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune");
|
|
371
|
+
reportLocalMemoryV2Deferred(input.executionId, input.handle, memoryPruneTraceId !== null || input.maintenancePurpose === "memory_prune", memoryRollout.legacyFallback);
|
|
395
372
|
if (memoryPruneTraceId !== null) {
|
|
396
373
|
executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
|
|
397
374
|
promptContext = { ...promptContext, workspace: executionWorkspace };
|
|
@@ -411,8 +388,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
411
388
|
agentDir: executionWorkspace.dir, executionId: input.executionId, agentHandle: input.handle,
|
|
412
389
|
enabled: input.localMemoryV2Enabled === true, query: baseWakePrompt,
|
|
413
390
|
resultPath: localMemoryResultPath, maxBytes: input.maxSystemPromptBytes,
|
|
414
|
-
|
|
391
|
+
storageVersion: detectLocalMemoryStorageVersion(executionWorkspace.memory),
|
|
392
|
+
strictRollout: !isolatedMaintenance && !memoryRollout.legacyFallback && (localMemoryV2Environment(input.launch.serverUrl) === "dev"
|
|
393
|
+
|| detectLocalMemoryStorageVersion(executionWorkspace.memory) === 2),
|
|
415
394
|
deferred: memoryRollout.deferred,
|
|
395
|
+
...(memoryRollout.legacyFallback ? { legacyFallback: true } : {}),
|
|
416
396
|
renderBase: (reservedPromptBytes) => resolvePrompt(input.systemPrompt, { ...promptContext, reservedPromptBytes }),
|
|
417
397
|
});
|
|
418
398
|
localMemoryResultPath = memoryPrompt.resultPath;
|
|
@@ -914,6 +894,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
914
894
|
exitCode,
|
|
915
895
|
workLogPath: executionWorkspace.workLogPath,
|
|
916
896
|
expectedWorkLogSha256: workLogSha256(executionWorkspace.workLog),
|
|
897
|
+
...(input.localMemoryV2Enabled === true ? {
|
|
898
|
+
writerCapabilities: [MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY],
|
|
899
|
+
} : {}),
|
|
917
900
|
});
|
|
918
901
|
const committed = exitCode !== 0 ? await commit() : await withWorkLogCommit(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), commit, dependencies.cancellation);
|
|
919
902
|
if (committed.status === "failed" || committed.status === "missing") {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { prepareWorkspace } from "../workspace.js";
|
|
2
|
+
import { awaitWithCancellation } from "../runtime-cancellation.js";
|
|
3
|
+
import { logLocalMemoryContextPrepareFailure } from "../local-memory-telemetry.js";
|
|
4
|
+
import { resolveLocalMemoryV2Rollout } from "./rollout.js";
|
|
5
|
+
import { initializeLocalMemoryV2ForWorkspace } from "./runtime.js";
|
|
6
|
+
import { withMemoryAccess } from "./migration-access.js";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
export async function prepareExecutionMemoryWorkspace(input, isolated, cancellation) {
|
|
9
|
+
try {
|
|
10
|
+
const prepare = async (context) => {
|
|
11
|
+
const prepared = await prepareWorkspace({
|
|
12
|
+
agentsRoot: input.launch.agentsRoot, handle: input.handle, cliPath: input.launch.cliPath,
|
|
13
|
+
executionId: input.executionId,
|
|
14
|
+
...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
|
|
15
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
16
|
+
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
17
|
+
...(input.sessionContextFingerprint === undefined ? {} : { sessionContextFingerprint: input.sessionContextFingerprint }),
|
|
18
|
+
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
19
|
+
...(isolated ? { skipLocalMemory: true } : {}),
|
|
20
|
+
});
|
|
21
|
+
const workspace = context?.created ? { ...prepared, agentHomePreexisted: false } : prepared;
|
|
22
|
+
const mode = await resolveLocalMemoryV2Rollout({ workspace, handle: input.handle, serverUrl: input.launch.serverUrl,
|
|
23
|
+
requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
|
|
24
|
+
...(input.launch.description ? { description: input.launch.description } : {}) });
|
|
25
|
+
const initialized = await initializeLocalMemoryV2ForWorkspace(workspace, mode.enabled);
|
|
26
|
+
return { workspace: { ...workspace, localMemoryV2Enabled: mode.enabled }, mode, initialized };
|
|
27
|
+
};
|
|
28
|
+
return await awaitWithCancellation(isolated ? prepare()
|
|
29
|
+
: withMemoryAccess(join(input.launch.agentsRoot, input.handle), prepare, { create: true }), cancellation);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logLocalMemoryContextPrepareFailure({ executionId: input.executionId, agentHandle: input.handle,
|
|
33
|
+
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }) }, "workspace_prepare", error);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parseFullMemoryIndex, LOCAL_MEMORY_GENERATED_INDEX_PATTERN } from "./storage-format.js";
|
|
2
|
+
const excerpt = (text, size) => Array.from(text).slice(0, size).join("");
|
|
3
|
+
/** 查询完整元数据;只在明确的历史入口返回非有效记录。正文仍按引用和哈希分页读取。 */
|
|
4
|
+
export function searchFullMemoryIndex(memory, sha256, input, outputBytes) {
|
|
5
|
+
const records = parseFullMemoryIndex(memory);
|
|
6
|
+
const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory);
|
|
7
|
+
const prefix = memory.slice(0, block.index);
|
|
8
|
+
let line = (prefix.match(/\n/gu) ?? []).length + 1;
|
|
9
|
+
let offset = Array.from(prefix).length;
|
|
10
|
+
const positions = new Map();
|
|
11
|
+
for (const part of block[0].matchAll(/[^\n]*(?:\n|$)/gu)) {
|
|
12
|
+
const id = /^- id: ([A-Za-z0-9_-]+)\r?\n?$/u.exec(part[0])?.[1];
|
|
13
|
+
if (id)
|
|
14
|
+
positions.set(id, { line, offset });
|
|
15
|
+
line += 1;
|
|
16
|
+
offset += Array.from(part[0]).length;
|
|
17
|
+
}
|
|
18
|
+
const terms = input.query.normalize("NFKC").toLowerCase().split(/\s+/u).filter(Boolean);
|
|
19
|
+
const candidates = records.filter(entry => {
|
|
20
|
+
const active = entry.status === "active" || entry.status === "pinned";
|
|
21
|
+
const content = `${entry.id} ${entry.title} ${entry.summary} ${entry.triggers.join(" ")}`.normalize("NFKC").toLowerCase();
|
|
22
|
+
return active !== input.history && terms.every(term => content.includes(term));
|
|
23
|
+
});
|
|
24
|
+
const matches = [];
|
|
25
|
+
let bytes = 512, hasMore = false;
|
|
26
|
+
for (const entry of candidates) {
|
|
27
|
+
const position = positions.get(entry.id);
|
|
28
|
+
if (!position)
|
|
29
|
+
throw new Error("memory_storage_position_invalid");
|
|
30
|
+
if (position.line <= input.afterLine)
|
|
31
|
+
continue;
|
|
32
|
+
const item = { ...position, id: entry.id, status: entry.status, revision: entry.revision,
|
|
33
|
+
file: entry.file, anchor: entry.anchor, heading: excerpt(entry.title, 96),
|
|
34
|
+
title: excerpt(entry.title, 120), summary: excerpt(entry.summary, 240), text: excerpt(entry.summary, 240) };
|
|
35
|
+
const size = Buffer.byteLength(JSON.stringify(item), "utf8") + 1;
|
|
36
|
+
if (matches.length >= input.limit || bytes + size > outputBytes) {
|
|
37
|
+
hasMore = true;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
matches.push(item);
|
|
41
|
+
bytes += size;
|
|
42
|
+
}
|
|
43
|
+
if (hasMore && matches.length === 0)
|
|
44
|
+
throw new Error("memory_output_too_large");
|
|
45
|
+
return { file: "MEMORY.md", storageVersion: 2, sha256, matches, matchCount: candidates.length,
|
|
46
|
+
hasMore, complete: !hasMore, nextAfterLine: hasMore ? matches.at(-1).line : null };
|
|
47
|
+
}
|
|
@@ -50,14 +50,17 @@ export function selectLocalMemoryIndexEntries(entries, budget, options = {}) {
|
|
|
50
50
|
.map((entry) => entry.id);
|
|
51
51
|
const protectedIds = options.protectedIds ?? new Set();
|
|
52
52
|
const protectedEntries = projectable.filter((entry) => protectedIds.has(entry.id)).sort(compareEntries);
|
|
53
|
-
const candidates = projectable.filter((entry) => !protectedIds.has(entry.id)).sort(compareEntries);
|
|
53
|
+
const candidates = projectable.filter((entry) => !protectedIds.has(entry.id)).sort((left, right) => (options.relevance?.(right) ?? 0) - (options.relevance?.(left) ?? 0) || compareEntries(left, right));
|
|
54
54
|
const selected = [];
|
|
55
55
|
let renderedLength = 0;
|
|
56
|
+
let renderedBytes = 0;
|
|
56
57
|
for (const entry of protectedEntries) {
|
|
57
58
|
selected.push(entry);
|
|
58
59
|
renderedLength += cardLength(entry, selected.length > 1);
|
|
60
|
+
renderedBytes += Buffer.byteLength(renderLocalMemoryIndexCard(entry), "utf8") + (selected.length > 1 ? 1 : 0);
|
|
59
61
|
}
|
|
60
|
-
if (selected.length > budget.entryCap || renderedLength > budget.indexCodePointCap
|
|
62
|
+
if (selected.length > budget.entryCap || renderedLength > budget.indexCodePointCap
|
|
63
|
+
|| renderedBytes > (budget.indexByteCap ?? Infinity)) {
|
|
61
64
|
return {
|
|
62
65
|
entries: selected,
|
|
63
66
|
omittedIds: [...omittedIds, ...candidates.map((entry) => entry.id)],
|
|
@@ -65,12 +68,15 @@ export function selectLocalMemoryIndexEntries(entries, budget, options = {}) {
|
|
|
65
68
|
};
|
|
66
69
|
}
|
|
67
70
|
for (const entry of candidates) {
|
|
68
|
-
|
|
71
|
+
const bytes = Buffer.byteLength(renderLocalMemoryIndexCard(entry), "utf8") + (selected.length > 0 ? 1 : 0);
|
|
72
|
+
if (selected.length >= budget.entryCap || renderedLength + cardLength(entry, selected.length > 0) > budget.indexCodePointCap
|
|
73
|
+
|| renderedBytes + bytes > (budget.indexByteCap ?? Infinity)) {
|
|
69
74
|
omittedIds.push(entry.id);
|
|
70
75
|
continue;
|
|
71
76
|
}
|
|
72
77
|
selected.push(entry);
|
|
73
78
|
renderedLength += cardLength(entry, selected.length > 1);
|
|
79
|
+
renderedBytes += bytes;
|
|
74
80
|
}
|
|
75
81
|
return {
|
|
76
82
|
entries: selected,
|
|
@@ -29,14 +29,33 @@ export async function runMemoryInventoryCommand(args, io) {
|
|
|
29
29
|
const issues = [...scan.issues, ...analysis.issues];
|
|
30
30
|
const referenced = new Set(analysis.entries.map((entry) => entry.sourceFields.file));
|
|
31
31
|
const files = scan.sources.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 }));
|
|
32
|
+
const orphanNotes = files.filter((file) => file.path.startsWith("notes/") && !referenced.has(file.path)).map((file) => file.path);
|
|
33
|
+
const storageVersion = (() => {
|
|
34
|
+
try {
|
|
35
|
+
return detectLocalMemoryStorageVersion(scan.sources.find((source) => source.path === "MEMORY.md")?.content ?? "");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
const indexedEntries = analysis.entries.filter((entry) => entry.format === "managed").length;
|
|
42
|
+
const legacyEntries = analysis.entries.length - indexedEntries;
|
|
32
43
|
const report = {
|
|
33
44
|
schemaVersion: 1, mode: "dry-run", agentDir: scan.root, applySupported: false,
|
|
34
45
|
complete, scanComplete, requiresHumanReview: true,
|
|
46
|
+
// 只读派生观测,不改变资格判断,也不新增持久化状态。
|
|
47
|
+
// A dry-run does not create a shadow index or server state. Existing
|
|
48
|
+
// schema1 cards therefore remain legacy; indexedEntries is only coverage
|
|
49
|
+
// information, not proof that the Agent entered shadow mode.
|
|
50
|
+
storageMode: storageVersion === 2 ? "v2_write" : "legacy",
|
|
51
|
+
contextMode: storageVersion === 2 ? "bounded_preview" : "legacy_context",
|
|
52
|
+
migrationCoverage: { storageVersion, indexedEntries, legacyEntries, unindexedFiles: orphanNotes.length,
|
|
53
|
+
totalEntries: analysis.entries.length, complete },
|
|
35
54
|
coverage: ["MEMORY.md", "notes/**", ".crew/memory/pending/**", "根目录的 .memory-result.v1-*.json"],
|
|
36
55
|
limitations: ["不扫描历史执行目录;result清单仅覆盖Agent根目录。", "两遍摘要比对不等同跨文件原子快照,正式迁移仍需停止写入并重新核对。"],
|
|
37
56
|
limits: inventoryLimits, totalBytes: scan.totalBytes, files, ...analysis, issues,
|
|
38
57
|
pending: files.filter((file) => file.path.startsWith(".crew/memory/pending/")).map((file) => file.path),
|
|
39
|
-
orphanNotes
|
|
58
|
+
orphanNotes,
|
|
40
59
|
resultFiles: files.filter((file) => file.path.startsWith(".memory-result.v1-")).map((file) => file.path),
|
|
41
60
|
};
|
|
42
61
|
io.out(JSON.stringify(report, null, 2) + "\n");
|
|
@@ -59,3 +78,4 @@ import { parseArgs } from "node:util";
|
|
|
59
78
|
import { z } from "zod";
|
|
60
79
|
import { inventoryLimits, scanMemorySources } from "./inventory-source.js";
|
|
61
80
|
import { previewMemoryInventory } from "./inventory-preview.js";
|
|
81
|
+
import { detectLocalMemoryStorageVersion } from "./storage-format.js";
|
|
@@ -73,7 +73,8 @@ export async function readMemoryInventorySource(root, file, budget) {
|
|
|
73
73
|
await handle.close();
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
export async function scanMemorySources(agentDir) {
|
|
76
|
+
export async function scanMemorySources(agentDir, options = {}) {
|
|
77
|
+
const includeOperationalFiles = options.includeOperationalFiles ?? true;
|
|
77
78
|
const root = await realpath(agentDir);
|
|
78
79
|
if ((await lstat(agentDir)).isSymbolicLink())
|
|
79
80
|
throw new Error("agent_root_symlink");
|
|
@@ -124,10 +125,14 @@ export async function scanMemorySources(agentDir) {
|
|
|
124
125
|
}
|
|
125
126
|
await visit("MEMORY.md");
|
|
126
127
|
await visit("notes", true);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
const resultNames = includeOperationalFiles
|
|
129
|
+
? (await readdir(root)).filter((name) => /^\.memory-result\.v1-.*\.json$/u.test(name)).sort()
|
|
130
|
+
: [];
|
|
131
|
+
if (includeOperationalFiles) {
|
|
132
|
+
await visit(".crew/memory/pending", true);
|
|
133
|
+
for (const name of resultNames)
|
|
134
|
+
await visit(name);
|
|
135
|
+
}
|
|
131
136
|
// 第二遍比对摘要和目录清单,发现扫描期间的变化就拒绝完整性结论;这不是跨文件事务锁。
|
|
132
137
|
for (const source of sources) {
|
|
133
138
|
try {
|
|
@@ -150,9 +155,11 @@ export async function scanMemorySources(agentDir) {
|
|
|
150
155
|
addIssue("source_changed", file);
|
|
151
156
|
}
|
|
152
157
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
if (includeOperationalFiles) {
|
|
159
|
+
const currentResults = (await readdir(root)).filter((name) => /^\.memory-result\.v1-.*\.json$/u.test(name)).sort();
|
|
160
|
+
if (JSON.stringify(currentResults) !== JSON.stringify(resultNames))
|
|
161
|
+
addIssue("source_changed", ".memory-result.v1-*.json");
|
|
162
|
+
}
|
|
156
163
|
if (await realpath(agentDir) !== root)
|
|
157
164
|
addIssue("source_changed", "agent_root");
|
|
158
165
|
return { root, sources: sources.sort((a, b) => a.path.localeCompare(b.path)), issues, totalBytes };
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { inventoryLimits, readMemoryInventorySource, scanMemorySources } from "./inventory-source.js";
|
|
4
|
+
import { LOCAL_MEMORY_GENERATED_INDEX_PATTERN } from "./storage-format.js";
|
|
5
|
+
const CURSOR_VERSION = 1;
|
|
6
|
+
const MAX_METADATA_PATH_BYTES = 4 * 1024;
|
|
7
|
+
const MAX_METADATA_WARNING_BYTES = 2 * 1024;
|
|
8
|
+
const excerpt = (text, size) => {
|
|
9
|
+
const characters = Array.from(text);
|
|
10
|
+
return characters.length > size ? `${characters.slice(0, size).join("")}...` : text;
|
|
11
|
+
};
|
|
12
|
+
function originalLowercaseIndex(text, index) {
|
|
13
|
+
let originalIndex = 0;
|
|
14
|
+
let lowerIndex = 0;
|
|
15
|
+
for (const character of text) {
|
|
16
|
+
lowerIndex += character.toLowerCase().length;
|
|
17
|
+
if (index < lowerIndex)
|
|
18
|
+
return originalIndex;
|
|
19
|
+
originalIndex += character.length;
|
|
20
|
+
}
|
|
21
|
+
return text.length;
|
|
22
|
+
}
|
|
23
|
+
function encodeCursor(cursor) {
|
|
24
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
25
|
+
}
|
|
26
|
+
function decodeCursor(value) {
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error("memory_cursor_invalid");
|
|
33
|
+
}
|
|
34
|
+
if (!parsed || typeof parsed !== "object")
|
|
35
|
+
throw new Error("memory_cursor_invalid");
|
|
36
|
+
const record = parsed;
|
|
37
|
+
if (record.version !== CURSOR_VERSION || typeof record.file !== "string"
|
|
38
|
+
|| !/^[a-f0-9]{64}$/u.test(String(record.sha256))
|
|
39
|
+
|| typeof record.afterLine !== "number" || !Number.isSafeInteger(record.afterLine) || record.afterLine < 0) {
|
|
40
|
+
throw new Error("memory_cursor_invalid");
|
|
41
|
+
}
|
|
42
|
+
return { version: 1, file: record.file, sha256: String(record.sha256), afterLine: record.afterLine };
|
|
43
|
+
}
|
|
44
|
+
function boundStringList(values, maxBytes) {
|
|
45
|
+
const bounded = [];
|
|
46
|
+
for (const value of values) {
|
|
47
|
+
const candidate = [...bounded, value];
|
|
48
|
+
if (Buffer.byteLength(JSON.stringify(candidate), "utf8") > maxBytes)
|
|
49
|
+
break;
|
|
50
|
+
bounded.push(value);
|
|
51
|
+
}
|
|
52
|
+
return { values: bounded, truncated: bounded.length < values.length };
|
|
53
|
+
}
|
|
54
|
+
function sourceMatches(source, query, afterLine, limit, outputBytes) {
|
|
55
|
+
const terms = query.normalize("NFKC").toLowerCase().split(/\s+/u).filter(Boolean);
|
|
56
|
+
const matches = [];
|
|
57
|
+
let line = 0;
|
|
58
|
+
let offset = 0;
|
|
59
|
+
let heading = "";
|
|
60
|
+
let matchCount = 0;
|
|
61
|
+
let hasMore = false;
|
|
62
|
+
let outputTooLarge = false;
|
|
63
|
+
let firstMatchLine = null;
|
|
64
|
+
let remaining = outputBytes;
|
|
65
|
+
const generatedIndex = source.path === "MEMORY.md"
|
|
66
|
+
? LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(source.content)
|
|
67
|
+
: null;
|
|
68
|
+
const generatedStart = generatedIndex === null ? -1 : Array.from(source.content.slice(0, generatedIndex.index)).length;
|
|
69
|
+
const generatedEnd = generatedIndex === null ? -1 : generatedStart + Array.from(generatedIndex[0]).length;
|
|
70
|
+
for (const match of source.content.matchAll(/[^\n]*(?:\n|$)/gu)) {
|
|
71
|
+
if (match[0] === "")
|
|
72
|
+
break;
|
|
73
|
+
line += 1;
|
|
74
|
+
const raw = match[0];
|
|
75
|
+
const lineStart = offset;
|
|
76
|
+
const lineEnd = offset + Array.from(raw).length;
|
|
77
|
+
const content = raw.replace(/\r?\n$/u, "");
|
|
78
|
+
const title = /^#{1,6}\s+(.+?)\s*$/u.exec(content)?.[1];
|
|
79
|
+
if (title !== undefined)
|
|
80
|
+
heading = excerpt(title, 96);
|
|
81
|
+
const lower = content.normalize("NFKC").toLowerCase();
|
|
82
|
+
const insideGeneratedIndex = generatedStart >= 0 && lineStart < generatedEnd && lineEnd > generatedStart;
|
|
83
|
+
if (!insideGeneratedIndex && terms.every(term => lower.includes(term))) {
|
|
84
|
+
matchCount += 1;
|
|
85
|
+
if (line > afterLine) {
|
|
86
|
+
firstMatchLine ??= line;
|
|
87
|
+
const matchIndex = lower.indexOf(terms[0] ?? "");
|
|
88
|
+
let start = Math.max(0, originalLowercaseIndex(content, matchIndex) - 60);
|
|
89
|
+
if (start > 0 && /[\uDC00-\uDFFF]/u.test(content[start]))
|
|
90
|
+
start -= 1;
|
|
91
|
+
const item = { file: source.path, sourceSha256: source.sha256, legacy: true, opaque: true,
|
|
92
|
+
line, offset, heading, text: `${start > 0 ? "..." : ""}${excerpt(content.slice(start), 240)}` };
|
|
93
|
+
const bytes = Buffer.byteLength(JSON.stringify(item), "utf8") + 1;
|
|
94
|
+
if (matches.length >= limit)
|
|
95
|
+
hasMore = true;
|
|
96
|
+
else if (bytes > remaining) {
|
|
97
|
+
hasMore = true;
|
|
98
|
+
outputTooLarge = matches.length === 0;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
matches.push(item);
|
|
102
|
+
remaining -= bytes;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
offset += Array.from(raw).length;
|
|
107
|
+
}
|
|
108
|
+
return { matches, matchCount, hasMore, outputTooLarge, firstMatchLine,
|
|
109
|
+
bytesUsed: outputBytes - remaining,
|
|
110
|
+
nextAfterLine: hasMore ? matches.at(-1)?.line ?? null : null };
|
|
111
|
+
}
|
|
112
|
+
function issueCode(error) {
|
|
113
|
+
if (error instanceof TypeError)
|
|
114
|
+
return "invalid_utf8";
|
|
115
|
+
if (error instanceof Error)
|
|
116
|
+
return error.code ?? error.message;
|
|
117
|
+
return "read_failed";
|
|
118
|
+
}
|
|
119
|
+
async function readRequestedSources(agentDir, files, excludeFiles) {
|
|
120
|
+
const root = await realpath(agentDir);
|
|
121
|
+
const sources = [];
|
|
122
|
+
const issues = [];
|
|
123
|
+
let remaining = inventoryLimits.totalBytes;
|
|
124
|
+
for (const file of [...new Set(files)].sort((left, right) => left.localeCompare(right))) {
|
|
125
|
+
if (excludeFiles?.has(file))
|
|
126
|
+
continue;
|
|
127
|
+
try {
|
|
128
|
+
const source = await readMemoryInventorySource(root, file, remaining);
|
|
129
|
+
remaining -= source.bytes;
|
|
130
|
+
sources.push(source);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
issues.push({ code: issueCode(error), path: file });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { sources, issues };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Read-only adapter for old MEMORY.md/notes content. It is only called from
|
|
140
|
+
* explicit search commands; prompt preparation never invokes it.
|
|
141
|
+
*/
|
|
142
|
+
export async function searchLegacyMemorySources(input) {
|
|
143
|
+
const scan = input.files === undefined
|
|
144
|
+
? await scanMemorySources(input.agentDir, { includeOperationalFiles: false })
|
|
145
|
+
: null;
|
|
146
|
+
const requestedResult = input.files === undefined
|
|
147
|
+
? null
|
|
148
|
+
: await readRequestedSources(input.agentDir, input.files, input.excludeFiles);
|
|
149
|
+
const requested = input.files === undefined
|
|
150
|
+
? scan.sources.filter(source => source.path === "MEMORY.md" || source.path.startsWith("notes/"))
|
|
151
|
+
: requestedResult.sources;
|
|
152
|
+
const sources = input.files === undefined
|
|
153
|
+
? requested.filter(source => !(input.excludeFiles?.has(source.path) ?? false))
|
|
154
|
+
: requested;
|
|
155
|
+
const relevantIssues = input.files === undefined
|
|
156
|
+
? scan.issues.filter(issue => issue.path === "MEMORY.md" || issue.path.startsWith("notes/"))
|
|
157
|
+
: requestedResult.issues;
|
|
158
|
+
const cursor = input.cursor === undefined ? null : decodeCursor(input.cursor);
|
|
159
|
+
if (cursor !== null && !sources.some(source => source.path === cursor.file))
|
|
160
|
+
throw new Error("memory_cursor_invalid");
|
|
161
|
+
const ordered = [...sources].sort((left, right) => left.path.localeCompare(right.path));
|
|
162
|
+
const matches = [];
|
|
163
|
+
let matchCount = 0;
|
|
164
|
+
let hasMore = false;
|
|
165
|
+
let nextCursor = null;
|
|
166
|
+
let started = cursor === null;
|
|
167
|
+
let pageFull = false;
|
|
168
|
+
const allUnindexedFiles = ordered.map(source => source.path);
|
|
169
|
+
const boundedFiles = boundStringList(allUnindexedFiles, MAX_METADATA_PATH_BYTES);
|
|
170
|
+
const allWarnings = relevantIssues.map(issue => ({ code: issue.code, path: issue.path }));
|
|
171
|
+
const boundedWarnings = boundStringList(allWarnings.map(warning => `${warning.code}\u0000${warning.path}`), MAX_METADATA_WARNING_BYTES);
|
|
172
|
+
const warnings = boundedWarnings.values.map(value => {
|
|
173
|
+
const separator = value.indexOf("\u0000");
|
|
174
|
+
return { code: separator < 0 ? value : value.slice(0, separator), path: separator < 0 ? "" : value.slice(separator + 1) };
|
|
175
|
+
});
|
|
176
|
+
const outputReservation = Buffer.byteLength(JSON.stringify({
|
|
177
|
+
source: "legacy", legacy: true, opaque: true, storageVersion: 0,
|
|
178
|
+
...(ordered.length === 1 ? { file: ordered[0].path, sha256: ordered[0].sha256 } : {}),
|
|
179
|
+
matches: [], matchCount: 0, hasMore: false, complete: false, nextAfterLine: null,
|
|
180
|
+
unindexedFiles: boundedFiles.values, unindexedFileCount: allUnindexedFiles.length,
|
|
181
|
+
unindexedFilesTruncated: boundedFiles.truncated, nextCursor: null,
|
|
182
|
+
warnings, warningCount: allWarnings.length, warningsTruncated: boundedWarnings.truncated,
|
|
183
|
+
}), "utf8") + 256;
|
|
184
|
+
let remainingOutputBytes = Math.max(0, input.outputBytes - outputReservation);
|
|
185
|
+
for (const source of ordered) {
|
|
186
|
+
if (!started) {
|
|
187
|
+
if (source.path !== cursor.file)
|
|
188
|
+
continue;
|
|
189
|
+
if (source.sha256 !== cursor.sha256)
|
|
190
|
+
throw new Error("memory_file_changed");
|
|
191
|
+
started = true;
|
|
192
|
+
}
|
|
193
|
+
const afterLine = source.path === cursor?.file ? cursor.afterLine
|
|
194
|
+
: cursor === null && source.path === ordered[0]?.path ? input.afterLine ?? 0 : 0;
|
|
195
|
+
const result = sourceMatches(source, input.query, afterLine, pageFull ? 0 : Math.max(0, input.limit - matches.length), remainingOutputBytes);
|
|
196
|
+
matches.push(...result.matches);
|
|
197
|
+
matchCount += result.matchCount;
|
|
198
|
+
remainingOutputBytes -= result.bytesUsed;
|
|
199
|
+
if (result.hasMore) {
|
|
200
|
+
hasMore = true;
|
|
201
|
+
pageFull = true;
|
|
202
|
+
if (result.outputTooLarge && matches.length === 0)
|
|
203
|
+
throw new Error("memory_output_too_large");
|
|
204
|
+
if (nextCursor === null) {
|
|
205
|
+
const last = result.matches.at(-1);
|
|
206
|
+
nextCursor = encodeCursor({ version: 1, file: source.path, sha256: source.sha256,
|
|
207
|
+
afterLine: Number(last?.line ?? Math.max(0, result.firstMatchLine ?? 1) - 1) });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const complete = !hasMore && relevantIssues.length === 0;
|
|
212
|
+
return {
|
|
213
|
+
source: "legacy", legacy: true, opaque: true, storageVersion: 0,
|
|
214
|
+
...(ordered.length === 1 ? { file: ordered[0].path, sha256: ordered[0].sha256 } : {}),
|
|
215
|
+
matches, matchCount, hasMore, complete, nextAfterLine: hasMore && ordered.length === 1
|
|
216
|
+
? Number(matches.at(-1)?.line ?? 0) : null,
|
|
217
|
+
unindexedFiles: boundedFiles.values, unindexedFileCount: allUnindexedFiles.length,
|
|
218
|
+
unindexedFilesTruncated: boundedFiles.truncated, nextCursor, warnings,
|
|
219
|
+
warningCount: allWarnings.length, warningsTruncated: boundedWarnings.truncated,
|
|
220
|
+
};
|
|
221
|
+
}
|