@nowcrew/daemon 0.6.101-beta.5 → 0.6.101-beta.7
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 +10 -1
- package/dist/computer-main.js +0 -0
- package/dist/execution-journal.js +1 -3
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +1 -4
- package/dist/local-execution-prompt.js +3 -8
- package/dist/local-executor.js +55 -78
- package/dist/local-memory/index-budget.js +3 -9
- package/dist/local-memory/inventory-cli.js +8 -52
- package/dist/local-memory/inventory-source.js +8 -15
- package/dist/local-memory/pending-query.js +2 -5
- package/dist/local-memory/pending-schema.js +1 -1
- package/dist/local-memory/pending.js +2 -16
- package/dist/local-memory/prompt.js +5 -21
- package/dist/local-memory/read-cli.js +76 -98
- package/dist/local-memory/rollout.js +11 -79
- package/dist/local-memory/runtime.js +17 -191
- package/dist/local-memory/write.js +21 -190
- package/dist/local-memory-diagnostics.js +15 -96
- package/dist/local-memory-telemetry.js +3 -31
- package/dist/machine-info.js +0 -3
- package/dist/main.js +0 -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-prune-diagnostics.js +4 -74
- package/dist/message-file-preview.js +1 -11
- package/dist/prompt.js +17 -9
- package/dist/runner.js +1 -1
- package/dist/runtimes/claude-thread-runner.js +4 -25
- package/dist/serve.js +3 -38
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +3 -16
- package/dist/workspace.js +4 -23
- package/package.json +10 -9
- package/dist/local-memory/commit-telemetry.js +0 -30
- package/dist/local-memory/execution-workspace.js +0 -117
- package/dist/local-memory/full-index-search.js +0 -47
- package/dist/local-memory/initialization-identity.js +0 -36
- package/dist/local-memory/inventory-v2.js +0 -362
- package/dist/local-memory/legacy-adapter.js +0 -233
- package/dist/local-memory/migration-access.js +0 -242
- package/dist/local-memory/migration-cli.js +0 -54
- package/dist/local-memory/migration-io.js +0 -199
- package/dist/local-memory/migration-pending.js +0 -9
- package/dist/local-memory/migration-plan.js +0 -432
- package/dist/local-memory/migration-schema.js +0 -58
- package/dist/local-memory/migration-state-invariants.js +0 -65
- package/dist/local-memory/migration-store.js +0 -414
- package/dist/local-memory/migration-tool-entry.js +0 -21
- package/dist/local-memory/release-artifact-guard.js +0 -55
- package/dist/local-memory/source-path.js +0 -13
- package/dist/local-memory/storage-format.js +0 -249
- package/dist/local-memory/writer-mode.js +0 -13
- package/dist/memory-migration-controller.js +0 -89
- package/dist/memory-migration-inspect.js +0 -116
- package/dist/memory-migration-journal.js +0 -14
- package/dist/memory-migration-protocol.js +0 -14
- package/dist/memory-migration-runtime.js +0 -367
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { LocalMemoryWorkspacePrepareError, prepareWorkspace } from "../workspace.js";
|
|
2
|
-
import { awaitWithCancellation } from "../runtime-cancellation.js";
|
|
3
|
-
import { logLocalMemoryContextDegraded, 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
|
-
import { readFile } from "node:fs/promises";
|
|
9
|
-
// A fresh Agent may receive the daemon's full ten-wide execution burst. The
|
|
10
|
-
// first turn publishes schema2 durably; the remaining turns must wait to reuse
|
|
11
|
-
// that committed fact instead of timing out on the generic short IO window.
|
|
12
|
-
const EXECUTION_MEMORY_ACCESS_WAIT_MS = 30_000;
|
|
13
|
-
function errorCode(error) {
|
|
14
|
-
const code = error?.code;
|
|
15
|
-
return typeof code === "string" ? code : error instanceof Error ? error.message : undefined;
|
|
16
|
-
}
|
|
17
|
-
function canSafelyDisableLocalMemory(error) {
|
|
18
|
-
if (error instanceof Error && error.name === "RuntimeCancelledError")
|
|
19
|
-
return false;
|
|
20
|
-
const code = errorCode(error);
|
|
21
|
-
if (code === undefined)
|
|
22
|
-
return false;
|
|
23
|
-
if (code === "memory_migration_access_busy"
|
|
24
|
-
|| code === "memory_migration_in_progress"
|
|
25
|
-
|| code === "memory_migration_path_invalid"
|
|
26
|
-
|| code === "memory_migration_process_unknown"
|
|
27
|
-
|| code === "local_memory_v2_initialization_residue_invalid")
|
|
28
|
-
return false;
|
|
29
|
-
return error instanceof LocalMemoryWorkspacePrepareError
|
|
30
|
-
|| code.startsWith("local_memory_v2_initialization_")
|
|
31
|
-
|| code.startsWith("memory_migration_")
|
|
32
|
-
|| code.startsWith("migration_");
|
|
33
|
-
}
|
|
34
|
-
async function prepareWithoutLocalMemory(input, cancellation) {
|
|
35
|
-
const workspace = await awaitWithCancellation(prepareWorkspace({
|
|
36
|
-
agentsRoot: input.launch.agentsRoot, handle: input.handle, cliPath: input.launch.cliPath,
|
|
37
|
-
executionId: input.executionId,
|
|
38
|
-
...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
|
|
39
|
-
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
40
|
-
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
41
|
-
...(input.sessionContextFingerprint === undefined ? {} : { sessionContextFingerprint: input.sessionContextFingerprint }),
|
|
42
|
-
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
43
|
-
skipLocalMemory: true,
|
|
44
|
-
}), cancellation);
|
|
45
|
-
return {
|
|
46
|
-
workspace: {
|
|
47
|
-
...workspace,
|
|
48
|
-
memory: "",
|
|
49
|
-
localMemoryV2Initialized: false,
|
|
50
|
-
localMemoryV2Enabled: false,
|
|
51
|
-
},
|
|
52
|
-
mode: { enabled: false, deferred: true, failure: "inspection_failed" },
|
|
53
|
-
initialized: false,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
export async function prepareExecutionMemoryWorkspace(input, isolated, cancellation) {
|
|
57
|
-
try {
|
|
58
|
-
const prepare = async (context) => {
|
|
59
|
-
const prepared = await prepareWorkspace({
|
|
60
|
-
agentsRoot: input.launch.agentsRoot, handle: input.handle, cliPath: input.launch.cliPath,
|
|
61
|
-
executionId: input.executionId,
|
|
62
|
-
...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
|
|
63
|
-
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
|
|
64
|
-
...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
|
|
65
|
-
...(input.sessionContextFingerprint === undefined ? {} : { sessionContextFingerprint: input.sessionContextFingerprint }),
|
|
66
|
-
...(input.launch.description ? { description: input.launch.description } : {}),
|
|
67
|
-
...(isolated ? { skipLocalMemory: true } : {}),
|
|
68
|
-
});
|
|
69
|
-
const workspace = context?.created ? { ...prepared, agentHomePreexisted: false } : prepared;
|
|
70
|
-
const initialMode = await resolveLocalMemoryV2Rollout({ workspace, handle: input.handle, serverUrl: input.launch.serverUrl,
|
|
71
|
-
agentId: input.launch.agentId, requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
|
|
72
|
-
...(input.launch.description ? { description: input.launch.description } : {}) });
|
|
73
|
-
const initializationBinding = input.launch.workspaceId !== undefined && input.launch.machineId !== undefined
|
|
74
|
-
? { workspaceId: input.launch.workspaceId, machineId: input.launch.machineId }
|
|
75
|
-
: undefined;
|
|
76
|
-
const initialized = await initializeLocalMemoryV2ForWorkspace(workspace, initialMode.enabled, input.launch.agentId, initializationBinding);
|
|
77
|
-
const memory = initialized && !workspace.localMemoryV2Initialized
|
|
78
|
-
? await readFile(join(workspace.dir, "MEMORY.md"), "utf8")
|
|
79
|
-
: workspace.memory;
|
|
80
|
-
const currentWorkspace = { ...workspace, memory };
|
|
81
|
-
const mode = initialized && !workspace.localMemoryV2Initialized
|
|
82
|
-
? await resolveLocalMemoryV2Rollout({ workspace: currentWorkspace, handle: input.handle,
|
|
83
|
-
serverUrl: input.launch.serverUrl, agentId: input.launch.agentId,
|
|
84
|
-
requested: input.localMemoryV2Enabled === true, selfTraining: isolated,
|
|
85
|
-
...(input.launch.description ? { description: input.launch.description } : {}) })
|
|
86
|
-
: initialMode;
|
|
87
|
-
return { workspace: { ...currentWorkspace, localMemoryV2Enabled: mode.enabled }, mode, initialized };
|
|
88
|
-
};
|
|
89
|
-
return await awaitWithCancellation(isolated ? prepare()
|
|
90
|
-
: withMemoryAccess(join(input.launch.agentsRoot, input.handle), prepare, {
|
|
91
|
-
create: true,
|
|
92
|
-
waitMs: EXECUTION_MEMORY_ACCESS_WAIT_MS,
|
|
93
|
-
initializedRecoveryIdentity: {
|
|
94
|
-
agentId: input.launch.agentId,
|
|
95
|
-
...(input.launch.workspaceId === undefined ? {} : { workspaceId: input.launch.workspaceId }),
|
|
96
|
-
...(input.launch.machineId === undefined ? {} : { machineId: input.launch.machineId }),
|
|
97
|
-
},
|
|
98
|
-
}), cancellation);
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
logLocalMemoryContextPrepareFailure({ executionId: input.executionId, agentHandle: input.handle,
|
|
102
|
-
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }) }, "workspace_prepare", error);
|
|
103
|
-
if (!isolated && canSafelyDisableLocalMemory(error)) {
|
|
104
|
-
try {
|
|
105
|
-
const degraded = await prepareWithoutLocalMemory(input, cancellation);
|
|
106
|
-
logLocalMemoryContextDegraded({ executionId: input.executionId, agentHandle: input.handle,
|
|
107
|
-
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }) }, error);
|
|
108
|
-
return degraded;
|
|
109
|
-
}
|
|
110
|
-
catch (fallbackError) {
|
|
111
|
-
logLocalMemoryContextPrepareFailure({ executionId: input.executionId, agentHandle: input.handle,
|
|
112
|
-
...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }) }, "workspace_prepare", fallbackError);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
-
export const LEGACY_LOCAL_INITIALIZATION_IDENTITY = "local-initialized";
|
|
4
|
-
export async function localMemoryInitializationIdentity(agentDir, agentId, binding) {
|
|
5
|
-
const root = await realpath(agentDir);
|
|
6
|
-
const rootFingerprint = createHash("sha256").update(root).digest("hex");
|
|
7
|
-
const workspaceId = binding?.workspaceId ?? LEGACY_LOCAL_INITIALIZATION_IDENTITY;
|
|
8
|
-
const machineId = binding?.machineId ?? LEGACY_LOCAL_INITIALIZATION_IDENTITY;
|
|
9
|
-
// Keep the legacy digest byte-for-byte stable so an interrupted pre-upgrade
|
|
10
|
-
// initialization can still be inspected. Every newly authenticated daemon
|
|
11
|
-
// initialization includes both control-plane identities in its operation id.
|
|
12
|
-
const operationSeed = workspaceId === LEGACY_LOCAL_INITIALIZATION_IDENTITY
|
|
13
|
-
&& machineId === LEGACY_LOCAL_INITIALIZATION_IDENTITY
|
|
14
|
-
? `${agentId}\0${root}`
|
|
15
|
-
: `${workspaceId}\0${machineId}\0${agentId}\0${root}`;
|
|
16
|
-
const operation = createHash("sha256").update(operationSeed, "utf8").digest("hex").slice(0, 24);
|
|
17
|
-
return {
|
|
18
|
-
workspaceId,
|
|
19
|
-
agentId,
|
|
20
|
-
migrationId: `initialize-${operation}`,
|
|
21
|
-
sourceStorageVersion: 0,
|
|
22
|
-
targetStorageVersion: 2,
|
|
23
|
-
rootFingerprint,
|
|
24
|
-
ownerIdentity: String((await lstat(root)).uid),
|
|
25
|
-
machineId,
|
|
26
|
-
executionId: `initialize-${operation}`,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
export function isLegacyLocalMemoryInitializationIdentity(identity) {
|
|
30
|
-
return identity.workspaceId === LEGACY_LOCAL_INITIALIZATION_IDENTITY
|
|
31
|
-
&& identity.machineId === LEGACY_LOCAL_INITIALIZATION_IDENTITY;
|
|
32
|
-
}
|
|
33
|
-
export function isLocalMemoryInitializationIdentity(actual, expected) {
|
|
34
|
-
return Object.keys(expected)
|
|
35
|
-
.every(key => actual[key] === expected[key]);
|
|
36
|
-
}
|
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
-
import { renderLocalMemoryIndexCard } from "./index-budget.js";
|
|
4
|
-
import { inventoryLimits, readMemoryInventorySource } from "./inventory-source.js";
|
|
5
|
-
import { MigrationStateSchema } from "./migration-schema.js";
|
|
6
|
-
import { assertMigrationStateIntegrity } from "./migration-state-invariants.js";
|
|
7
|
-
import { isHarmlessMigrationRemainder } from "./migration-plan.js";
|
|
8
|
-
import { parseFullMemoryIndex, selectLocalMemoryPreview } from "./storage-format.js";
|
|
9
|
-
import { isDeferredMemoryArtifactPath } from "./source-path.js";
|
|
10
|
-
const STATE_PATH = ".crew/memory/migration.v1.json";
|
|
11
|
-
const DETAIL_MARKER_PATTERN = /<!--\s*NOWWORK:MEMORY_ENTRY:(?:BEGIN\s+id=([A-Za-z0-9_-]+)\s+revision=([1-9][0-9]*)|END\s+id=([A-Za-z0-9_-]+))\s*-->/gu;
|
|
12
|
-
const CONTROL_MARKER_PATTERN = /NOWWORK:MEMORY_(?:ENTRY|INDEX):/u;
|
|
13
|
-
function errorCode(error, fallback) {
|
|
14
|
-
if (error instanceof Error && /^[a-z0-9_]+$/u.test(error.message))
|
|
15
|
-
return error.message;
|
|
16
|
-
return fallback;
|
|
17
|
-
}
|
|
18
|
-
function sourceFields(entry) {
|
|
19
|
-
return {
|
|
20
|
-
id: entry.id,
|
|
21
|
-
file: entry.file,
|
|
22
|
-
type: entry.type,
|
|
23
|
-
status: entry.status,
|
|
24
|
-
summary: entry.summary,
|
|
25
|
-
triggers: entry.triggers.join(", "),
|
|
26
|
-
priority: entry.priority,
|
|
27
|
-
revision: String(entry.revision),
|
|
28
|
-
anchor: entry.anchor ?? "",
|
|
29
|
-
updated: entry.updated,
|
|
30
|
-
...(entry.reviewAfter === null ? {} : { review_after: entry.reviewAfter }),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
function exactManagedDetailBodies(detail, targetId) {
|
|
34
|
-
const escapedId = targetId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
35
|
-
const pattern = new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escapedId}\\s+revision=[1-9][0-9]*\\s*-->[\\s\\S]*?<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escapedId}\\s*-->`, "gu");
|
|
36
|
-
return [...detail.matchAll(pattern)].map(match => {
|
|
37
|
-
const beginEnd = match[0].indexOf("-->") + 3;
|
|
38
|
-
const endStart = match[0].lastIndexOf("<!--");
|
|
39
|
-
return match[0].slice(beginEnd, endStart);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
function harmlessDetailRemainder(text) {
|
|
43
|
-
let cursor = 0;
|
|
44
|
-
let lineStart = true;
|
|
45
|
-
const whitespace = /\s+/uy;
|
|
46
|
-
while (cursor < text.length) {
|
|
47
|
-
whitespace.lastIndex = cursor;
|
|
48
|
-
const spacing = whitespace.exec(text);
|
|
49
|
-
if (spacing !== null) {
|
|
50
|
-
const lastNewline = spacing[0].lastIndexOf("\n");
|
|
51
|
-
lineStart = lastNewline === spacing[0].length - 1;
|
|
52
|
-
cursor = whitespace.lastIndex;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (text.startsWith("<!--", cursor)) {
|
|
56
|
-
const end = text.indexOf("-->", cursor + 4);
|
|
57
|
-
if (end === -1)
|
|
58
|
-
return false;
|
|
59
|
-
cursor = end + 3;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (lineStart && text[cursor] === "#") {
|
|
63
|
-
const end = text.indexOf("\n", cursor + 1);
|
|
64
|
-
if (end === -1)
|
|
65
|
-
return true;
|
|
66
|
-
cursor = end + 1;
|
|
67
|
-
lineStart = true;
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* A migrated legacy note can remain on disk for external references. It is
|
|
76
|
-
* safe to exclude it from the V2 orphan scan only when the immutable
|
|
77
|
-
* migration mapping proves that the complete, unchanged source was copied to
|
|
78
|
-
* the indexed target entry.
|
|
79
|
-
*/
|
|
80
|
-
export function findControlledPreservedSourcePaths(entries, sources, mappings, sourceHashes = new Map()) {
|
|
81
|
-
const entriesById = new Map(entries.map(entry => [entry.id, entry]));
|
|
82
|
-
const sourcesByPath = new Map(sources.map(source => [source.path, source]));
|
|
83
|
-
const grouped = new Map();
|
|
84
|
-
for (const mapping of mappings) {
|
|
85
|
-
if (!mapping.sourceFile.startsWith("notes/"))
|
|
86
|
-
continue;
|
|
87
|
-
const current = grouped.get(mapping.sourceFile) ?? [];
|
|
88
|
-
grouped.set(mapping.sourceFile, [...current, mapping]);
|
|
89
|
-
}
|
|
90
|
-
const preserved = [];
|
|
91
|
-
for (const [path, values] of grouped) {
|
|
92
|
-
const source = sourcesByPath.get(path);
|
|
93
|
-
if (source === undefined || values.length === 0)
|
|
94
|
-
continue;
|
|
95
|
-
const ranges = [...values].sort((left, right) => left.start - right.start);
|
|
96
|
-
let cursor = 0;
|
|
97
|
-
let valid = true;
|
|
98
|
-
for (const mapping of ranges) {
|
|
99
|
-
const targetFile = `notes/${mapping.category}.md`;
|
|
100
|
-
const target = mapping.targetId === undefined ? undefined : entriesById.get(mapping.targetId);
|
|
101
|
-
const targetSource = target === undefined ? undefined : sourcesByPath.get(target.file);
|
|
102
|
-
const expectedSourceSha256 = mapping.sourceSha256 ?? sourceHashes.get(path);
|
|
103
|
-
const sourceBody = source.content.slice(mapping.start, mapping.end);
|
|
104
|
-
const renderedBody = mapping.sourceFile === targetFile
|
|
105
|
-
? sourceBody
|
|
106
|
-
: `\n## ${mapping.title}\n\nScope: ${mapping.scope}\n\n${sourceBody}\n`;
|
|
107
|
-
const sourceBlockBodies = mapping.targetId === undefined
|
|
108
|
-
? []
|
|
109
|
-
: exactManagedDetailBodies(sourceBody, mapping.targetId);
|
|
110
|
-
const expectedBody = mapping.sourceFile === targetFile && sourceBlockBodies.length === 1
|
|
111
|
-
? sourceBlockBodies[0]
|
|
112
|
-
: renderedBody;
|
|
113
|
-
const targetBodies = mapping.targetId === undefined || targetSource === undefined
|
|
114
|
-
? []
|
|
115
|
-
: exactManagedDetailBodies(targetSource.content, mapping.targetId);
|
|
116
|
-
if (expectedSourceSha256 === undefined || expectedSourceSha256 !== source.sha256
|
|
117
|
-
|| mapping.start < cursor || !isHarmlessMigrationRemainder(source.content.slice(cursor, mapping.start))
|
|
118
|
-
|| mapping.end > source.content.length
|
|
119
|
-
|| target === undefined || target.file !== targetFile || targetSource === undefined
|
|
120
|
-
|| targetBodies.length !== 1 || targetBodies[0] !== expectedBody) {
|
|
121
|
-
valid = false;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
cursor = mapping.end;
|
|
125
|
-
}
|
|
126
|
-
if (valid && isHarmlessMigrationRemainder(source.content.slice(cursor)))
|
|
127
|
-
preserved.push(path);
|
|
128
|
-
}
|
|
129
|
-
return preserved.sort((left, right) => left.localeCompare(right));
|
|
130
|
-
}
|
|
131
|
-
export function* scanManagedDetailMarkers(detail) {
|
|
132
|
-
for (const marker of detail.matchAll(DETAIL_MARKER_PATTERN)) {
|
|
133
|
-
const beginId = marker[1];
|
|
134
|
-
yield {
|
|
135
|
-
start: marker.index,
|
|
136
|
-
end: marker.index + marker[0].length,
|
|
137
|
-
kind: beginId === undefined ? "end" : "begin",
|
|
138
|
-
id: beginId ?? marker[3],
|
|
139
|
-
revision: beginId === undefined ? null : Number(marker[2]),
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
/** Parse one note once, then let every index entry use constant-time lookups. */
|
|
144
|
-
export function analyzeManagedDetailFile(detail, expectedIds, scan = scanManagedDetailMarkers) {
|
|
145
|
-
const revisionsById = new Map();
|
|
146
|
-
const remainder = [];
|
|
147
|
-
let cursor = 0;
|
|
148
|
-
let markerInvalid = false;
|
|
149
|
-
let unindexed = false;
|
|
150
|
-
let open = null;
|
|
151
|
-
for (const marker of scan(detail)) {
|
|
152
|
-
const between = detail.slice(cursor, marker.start);
|
|
153
|
-
if (CONTROL_MARKER_PATTERN.test(between))
|
|
154
|
-
markerInvalid = true;
|
|
155
|
-
if (open === null)
|
|
156
|
-
remainder.push(between);
|
|
157
|
-
cursor = marker.end;
|
|
158
|
-
if (marker.kind === "begin") {
|
|
159
|
-
if (open !== null || marker.revision === null)
|
|
160
|
-
markerInvalid = true;
|
|
161
|
-
else
|
|
162
|
-
open = { id: marker.id, revision: marker.revision };
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (open === null || marker.id !== open.id) {
|
|
166
|
-
markerInvalid = true;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const revisions = revisionsById.get(open.id) ?? [];
|
|
170
|
-
revisions.push(open.revision);
|
|
171
|
-
revisionsById.set(open.id, revisions);
|
|
172
|
-
if (!expectedIds.has(open.id))
|
|
173
|
-
unindexed = true;
|
|
174
|
-
open = null;
|
|
175
|
-
}
|
|
176
|
-
const tail = detail.slice(cursor);
|
|
177
|
-
if (CONTROL_MARKER_PATTERN.test(tail) || open !== null)
|
|
178
|
-
markerInvalid = true;
|
|
179
|
-
if (open === null)
|
|
180
|
-
remainder.push(tail);
|
|
181
|
-
const uncovered = remainder.join("");
|
|
182
|
-
const coverageIssue = markerInvalid
|
|
183
|
-
? "managed_anchor_invalid"
|
|
184
|
-
: unindexed
|
|
185
|
-
? "unindexed_detail"
|
|
186
|
-
: harmlessDetailRemainder(uncovered)
|
|
187
|
-
? null
|
|
188
|
-
: "unindexed_text_requires_mapping";
|
|
189
|
-
return { revisionsById, coverageIssue };
|
|
190
|
-
}
|
|
191
|
-
function inspectDetailEntry(entry, analysis) {
|
|
192
|
-
if (analysis === undefined)
|
|
193
|
-
return "missing_detail";
|
|
194
|
-
if (analysis.coverageIssue === "managed_anchor_invalid")
|
|
195
|
-
return "managed_anchor_invalid";
|
|
196
|
-
const revisions = analysis.revisionsById.get(entry.id) ?? [];
|
|
197
|
-
if (revisions.length !== 1)
|
|
198
|
-
return "managed_anchor_invalid";
|
|
199
|
-
return revisions[0] > entry.revision ? "managed_revision_invalid" : null;
|
|
200
|
-
}
|
|
201
|
-
export function inspectManagedDetailFiles(entries, sources, analyze = analyzeManagedDetailFile) {
|
|
202
|
-
return inspectManagedDetailFilesWithMappings(entries, sources, [], new Map(), analyze);
|
|
203
|
-
}
|
|
204
|
-
export function inspectManagedDetailFilesWithMappings(entries, sources, mappings, sourceHashes = new Map(), analyze = analyzeManagedDetailFile) {
|
|
205
|
-
const sourceMap = new Map(sources.map(source => [source.path, source]));
|
|
206
|
-
const preservedSourcePaths = findControlledPreservedSourcePaths(entries, sources, mappings, sourceHashes);
|
|
207
|
-
const preserved = new Set(preservedSourcePaths);
|
|
208
|
-
const expectedByFile = new Map();
|
|
209
|
-
for (const entry of entries) {
|
|
210
|
-
const expected = expectedByFile.get(entry.file) ?? new Set();
|
|
211
|
-
expected.add(entry.id);
|
|
212
|
-
expectedByFile.set(entry.file, expected);
|
|
213
|
-
}
|
|
214
|
-
const paths = new Set([
|
|
215
|
-
...expectedByFile.keys(),
|
|
216
|
-
...sources.filter(source => source.path.startsWith("notes/") && !isDeferredMemoryArtifactPath(source.path)).map(source => source.path),
|
|
217
|
-
]);
|
|
218
|
-
const analyses = new Map();
|
|
219
|
-
const fileIssues = [];
|
|
220
|
-
for (const path of paths) {
|
|
221
|
-
const source = sourceMap.get(path);
|
|
222
|
-
if (source === undefined)
|
|
223
|
-
continue;
|
|
224
|
-
const analysis = analyze(source.content, expectedByFile.get(path) ?? new Set());
|
|
225
|
-
analyses.set(path, analysis);
|
|
226
|
-
if (analysis.coverageIssue !== null
|
|
227
|
-
&& !(preserved.has(path) && analysis.coverageIssue === "unindexed_text_requires_mapping")) {
|
|
228
|
-
fileIssues.push({ code: analysis.coverageIssue, path });
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
const entryIssues = new Map();
|
|
232
|
-
for (const entry of entries) {
|
|
233
|
-
const issue = inspectDetailEntry(entry, analyses.get(entry.file));
|
|
234
|
-
if (issue !== null)
|
|
235
|
-
entryIssues.set(entry.id, issue);
|
|
236
|
-
}
|
|
237
|
-
return { entryIssues, fileIssues, preservedSourcePaths };
|
|
238
|
-
}
|
|
239
|
-
async function inspectCommittedState(root, budget) {
|
|
240
|
-
let source;
|
|
241
|
-
try {
|
|
242
|
-
source = await readMemoryInventorySource(root, STATE_PATH, Math.min(inventoryLimits.fileBytes, budget));
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
return {
|
|
246
|
-
valid: false,
|
|
247
|
-
source: null,
|
|
248
|
-
state: null,
|
|
249
|
-
issue: {
|
|
250
|
-
code: error.code === "ENOENT"
|
|
251
|
-
? "memory_migration_state_missing"
|
|
252
|
-
: errorCode(error, "memory_migration_state_invalid"),
|
|
253
|
-
path: STATE_PATH,
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
try {
|
|
258
|
-
const state = MigrationStateSchema.parse(JSON.parse(source.content));
|
|
259
|
-
const fingerprint = createHash("sha256").update(await realpath(root)).digest("hex");
|
|
260
|
-
const ownerIdentity = String((await lstat(root)).uid);
|
|
261
|
-
if (state.phase !== "committed" || state.storageVersion !== 2
|
|
262
|
-
|| state.identity.rootFingerprint !== fingerprint
|
|
263
|
-
|| state.identity.ownerIdentity !== ownerIdentity) {
|
|
264
|
-
throw new Error("memory_migration_state_invalid");
|
|
265
|
-
}
|
|
266
|
-
assertMigrationStateIntegrity(state, state.identity);
|
|
267
|
-
const confirmed = await readMemoryInventorySource(root, STATE_PATH, Math.min(inventoryLimits.fileBytes, budget));
|
|
268
|
-
if (confirmed.sha256 !== source.sha256) {
|
|
269
|
-
return { valid: false, source, state: null, issue: { code: "source_changed", path: STATE_PATH } };
|
|
270
|
-
}
|
|
271
|
-
return { valid: true, source, state, issue: null };
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
return {
|
|
275
|
-
valid: false,
|
|
276
|
-
source,
|
|
277
|
-
state: null,
|
|
278
|
-
issue: {
|
|
279
|
-
code: "memory_migration_state_invalid",
|
|
280
|
-
path: STATE_PATH,
|
|
281
|
-
},
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
/** Validate an already-committed schema2 store without sending it through the schema1 migration preview. */
|
|
286
|
-
export async function inspectV2MemoryInventory(root, sources, readComplete) {
|
|
287
|
-
const sourceMap = new Map(sources.map(source => [source.path, source]));
|
|
288
|
-
const memory = sourceMap.get("MEMORY.md")?.content ?? "";
|
|
289
|
-
const issues = [];
|
|
290
|
-
const addIssue = (code, path) => {
|
|
291
|
-
if (!issues.some(issue => issue.code === code && issue.path === path))
|
|
292
|
-
issues.push({ code, path });
|
|
293
|
-
};
|
|
294
|
-
let entries = [];
|
|
295
|
-
try {
|
|
296
|
-
entries = parseFullMemoryIndex(memory);
|
|
297
|
-
}
|
|
298
|
-
catch (error) {
|
|
299
|
-
addIssue(errorCode(error, "local_memory_storage_entry_invalid"), "MEMORY.md");
|
|
300
|
-
}
|
|
301
|
-
const scannedBytes = sources.reduce((total, source) => total + source.bytes, 0);
|
|
302
|
-
const state = await inspectCommittedState(root, Math.max(0, inventoryLimits.totalBytes - scannedBytes));
|
|
303
|
-
if (state.issue)
|
|
304
|
-
addIssue(state.issue.code, state.issue.path);
|
|
305
|
-
const sourceHashes = new Map((state.state?.manifest.files ?? [])
|
|
306
|
-
.flatMap(file => file.before.exists ? [[file.path, file.before.sha256]] : []));
|
|
307
|
-
const detailInspection = inspectManagedDetailFilesWithMappings(entries, sources, state.state?.manifest.recordMappings ?? [], sourceHashes);
|
|
308
|
-
for (const issue of detailInspection.fileIssues)
|
|
309
|
-
addIssue(issue.code, issue.path);
|
|
310
|
-
for (const entry of entries) {
|
|
311
|
-
const issue = detailInspection.entryIssues.get(entry.id);
|
|
312
|
-
if (issue !== undefined)
|
|
313
|
-
addIssue(issue, entry.file);
|
|
314
|
-
}
|
|
315
|
-
let preview = null;
|
|
316
|
-
if (readComplete && !issues.some(issue => issue.path === "MEMORY.md")) {
|
|
317
|
-
try {
|
|
318
|
-
const selected = selectLocalMemoryPreview(memory);
|
|
319
|
-
preview = {
|
|
320
|
-
memory: selected.memory,
|
|
321
|
-
budget: {
|
|
322
|
-
entries: selected.entries.length,
|
|
323
|
-
omittedEntries: selected.omittedIds.length,
|
|
324
|
-
// The bounded projection itself fits; reason records normal long-tail omission.
|
|
325
|
-
fits: true,
|
|
326
|
-
reason: selected.reason,
|
|
327
|
-
},
|
|
328
|
-
writerCompatible: state.valid && issues.length === 0,
|
|
329
|
-
notes: [],
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
addIssue(errorCode(error, "local_memory_preview_invalid"), "MEMORY.md");
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
const shownDetails = new Set();
|
|
337
|
-
const writeReady = readComplete && state.valid && issues.length === 0;
|
|
338
|
-
return {
|
|
339
|
-
stateValid: state.valid,
|
|
340
|
-
writeReady,
|
|
341
|
-
stateSource: state.source,
|
|
342
|
-
preservedSourcePaths: detailInspection.preservedSourcePaths,
|
|
343
|
-
analysis: {
|
|
344
|
-
entries: entries.map(entry => {
|
|
345
|
-
const detail = shownDetails.has(entry.file) ? null : sourceMap.get(entry.file)?.content ?? null;
|
|
346
|
-
shownDetails.add(entry.file);
|
|
347
|
-
return {
|
|
348
|
-
title: entry.title,
|
|
349
|
-
format: "managed",
|
|
350
|
-
disposition: detailInspection.entryIssues.has(entry.id) || !writeReady ? "blocked" : "review_required",
|
|
351
|
-
sourceFields: sourceFields(entry),
|
|
352
|
-
rawCard: renderLocalMemoryIndexCard(entry),
|
|
353
|
-
detail,
|
|
354
|
-
proposed: entry,
|
|
355
|
-
};
|
|
356
|
-
}),
|
|
357
|
-
issues,
|
|
358
|
-
preview,
|
|
359
|
-
rawMemory: memory,
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
|
-
}
|