@nowcrew/daemon 0.6.100 → 0.6.101-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloud-report-upload.js +1 -10
- package/dist/computer-main.js +0 -0
- package/dist/execution-journal.js +3 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +4 -1
- package/dist/local-execution-prompt.js +8 -3
- package/dist/local-executor.js +78 -55
- package/dist/local-memory/commit-telemetry.js +30 -0
- package/dist/local-memory/execution-workspace.js +62 -0
- package/dist/local-memory/full-index-search.js +47 -0
- package/dist/local-memory/index-budget.js +9 -3
- package/dist/local-memory/initialization-identity.js +36 -0
- package/dist/local-memory/inventory-cli.js +52 -8
- package/dist/local-memory/inventory-source.js +15 -8
- package/dist/local-memory/inventory-v2.js +362 -0
- package/dist/local-memory/legacy-adapter.js +233 -0
- package/dist/local-memory/migration-access.js +242 -0
- package/dist/local-memory/migration-cli.js +54 -0
- package/dist/local-memory/migration-io.js +199 -0
- package/dist/local-memory/migration-pending.js +9 -0
- package/dist/local-memory/migration-plan.js +432 -0
- package/dist/local-memory/migration-schema.js +55 -0
- package/dist/local-memory/migration-state-invariants.js +65 -0
- package/dist/local-memory/migration-store.js +414 -0
- package/dist/local-memory/migration-tool-entry.js +21 -0
- package/dist/local-memory/pending-query.js +5 -2
- package/dist/local-memory/pending-schema.js +1 -1
- package/dist/local-memory/pending.js +16 -2
- package/dist/local-memory/prompt.js +21 -5
- package/dist/local-memory/read-cli.js +98 -76
- package/dist/local-memory/release-artifact-guard.js +55 -0
- package/dist/local-memory/rollout.js +79 -11
- package/dist/local-memory/runtime.js +191 -17
- package/dist/local-memory/source-path.js +13 -0
- package/dist/local-memory/storage-format.js +249 -0
- package/dist/local-memory/write.js +190 -21
- package/dist/local-memory/writer-mode.js +13 -0
- package/dist/local-memory-diagnostics.js +96 -15
- package/dist/local-memory-telemetry.js +20 -3
- package/dist/machine-info.js +3 -0
- 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-migration-controller.js +89 -0
- package/dist/memory-migration-inspect.js +116 -0
- package/dist/memory-migration-journal.js +14 -0
- package/dist/memory-migration-protocol.js +14 -0
- package/dist/memory-migration-runtime.js +305 -0
- package/dist/memory-prune-diagnostics.js +74 -4
- package/dist/message-file-preview.js +11 -1
- package/dist/prompt.js +9 -17
- package/dist/runner.js +1 -1
- package/dist/runtimes/claude-thread-runner.js +25 -4
- package/dist/serve.js +38 -3
- package/dist/token.js +1 -0
- package/dist/workspace-control-handler.js +16 -3
- package/dist/workspace.js +13 -2
- package/package.json +9 -10
|
@@ -2,9 +2,9 @@ const CommandSchema = z.object({
|
|
|
2
2
|
"agent-dir": z.string().min(1).refine(isAbsolute),
|
|
3
3
|
"dry-run": z.literal(true),
|
|
4
4
|
}).strict();
|
|
5
|
-
const USAGE = "
|
|
5
|
+
const USAGE = "只读记忆盘点、schema0/1 转换预演与 schema2 完整性校验,不写文件,不支持 --apply。\n"
|
|
6
6
|
+ "pnpm --filter @nowcrew/daemon exec tsx src/local-memory/inventory-cli.ts --agent-dir <单个Agent绝对路径> --dry-run\n"
|
|
7
|
-
+ "报告包含原始记忆,需作为敏感本地资料保存;退出码0
|
|
7
|
+
+ "报告包含原始记忆,需作为敏感本地资料保存;退出码0仅代表盘点、格式预演或完整性校验无阻塞,不代表批准迁移。\n";
|
|
8
8
|
export async function runMemoryInventoryCommand(args, io) {
|
|
9
9
|
if (args.length === 1 && args[0] === "--help") {
|
|
10
10
|
io.out(USAGE);
|
|
@@ -24,19 +24,60 @@ export async function runMemoryInventoryCommand(args, io) {
|
|
|
24
24
|
try {
|
|
25
25
|
const scan = await scanMemorySources(command["agent-dir"]);
|
|
26
26
|
const scanComplete = scan.issues.length === 0;
|
|
27
|
-
const
|
|
27
|
+
const memory = scan.sources.find((source) => source.path === "MEMORY.md")?.content ?? "";
|
|
28
|
+
let storageVersion = null;
|
|
29
|
+
let detectionIssue = null;
|
|
30
|
+
try {
|
|
31
|
+
storageVersion = detectLocalMemoryStorageVersion(memory);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
detectionIssue = {
|
|
35
|
+
code: error instanceof Error && /^[a-z0-9_]+$/u.test(error.message)
|
|
36
|
+
? error.message
|
|
37
|
+
: "local_memory_storage_version_invalid",
|
|
38
|
+
path: "MEMORY.md",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const v2 = storageVersion === 2
|
|
42
|
+
? await inspectV2MemoryInventory(scan.root, scan.sources, scanComplete)
|
|
43
|
+
: null;
|
|
44
|
+
const analysis = detectionIssue
|
|
45
|
+
? { entries: [], issues: [detectionIssue], preview: null, rawMemory: memory }
|
|
46
|
+
: v2?.analysis ?? previewMemoryInventory(scan.sources, scanComplete);
|
|
28
47
|
const complete = scanComplete && !analysis.issues.some((issue) => issue.code === "missing_detail");
|
|
29
48
|
const issues = [...scan.issues, ...analysis.issues];
|
|
30
|
-
const referenced = new Set(
|
|
31
|
-
|
|
49
|
+
const referenced = new Set([
|
|
50
|
+
...analysis.entries.map((entry) => entry.sourceFields.file),
|
|
51
|
+
...(v2?.preservedSourcePaths ?? []),
|
|
52
|
+
]);
|
|
53
|
+
const reportSources = v2?.stateSource ? [...scan.sources, v2.stateSource] : scan.sources;
|
|
54
|
+
const files = reportSources.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 }));
|
|
55
|
+
const deferredFiles = files.filter((file) => isDeferredMemoryArtifactPath(file.path)).map((file) => file.path);
|
|
56
|
+
const orphanNotes = files.filter((file) => file.path.startsWith("notes/") && !isDeferredMemoryArtifactPath(file.path) && !referenced.has(file.path)).map((file) => file.path);
|
|
57
|
+
const indexedEntries = analysis.entries.filter((entry) => entry.format === "managed").length;
|
|
58
|
+
const legacyEntries = analysis.entries.length - indexedEntries;
|
|
32
59
|
const report = {
|
|
33
60
|
schemaVersion: 1, mode: "dry-run", agentDir: scan.root, applySupported: false,
|
|
34
61
|
complete, scanComplete, requiresHumanReview: true,
|
|
35
|
-
|
|
62
|
+
// 只读派生观测,不改变资格判断,也不新增持久化状态。
|
|
63
|
+
// A dry-run does not create a shadow index or server state. Existing
|
|
64
|
+
// schema1 cards therefore remain legacy; indexedEntries is only coverage
|
|
65
|
+
// information, not proof that the Agent entered shadow mode.
|
|
66
|
+
storageMode: storageVersion === 2 ? (v2?.writeReady ? "v2_write" : "repair_required")
|
|
67
|
+
: detectionIssue ? "repair_required" : "legacy",
|
|
68
|
+
contextMode: storageVersion === 2 ? (v2?.writeReady ? "bounded_preview" : "blocked")
|
|
69
|
+
: detectionIssue ? "blocked" : "legacy_context",
|
|
70
|
+
readerMode: storageVersion === 2 ? "v2" : detectionIssue ? "blocked" : "legacy",
|
|
71
|
+
detectedIndexVersion: storageVersion,
|
|
72
|
+
migrationCoverage: { detectedIndexVersion: storageVersion, storageVersion, indexedEntries, legacyEntries, unindexedFiles: orphanNotes.length,
|
|
73
|
+
totalEntries: analysis.entries.length, complete },
|
|
74
|
+
coverage: ["MEMORY.md", "notes/**", ...(storageVersion === 2 ? [".crew/memory/migration.v1.json"] : []),
|
|
75
|
+
".crew/memory/pending/**", "根目录的 .memory-result.v1-*.json"],
|
|
36
76
|
limitations: ["不扫描历史执行目录;result清单仅覆盖Agent根目录。", "两遍摘要比对不等同跨文件原子快照,正式迁移仍需停止写入并重新核对。"],
|
|
37
|
-
limits: inventoryLimits, totalBytes:
|
|
77
|
+
limits: inventoryLimits, totalBytes: reportSources.reduce((total, source) => total + source.bytes, 0), files, ...analysis, issues,
|
|
38
78
|
pending: files.filter((file) => file.path.startsWith(".crew/memory/pending/")).map((file) => file.path),
|
|
39
|
-
|
|
79
|
+
deferredFiles,
|
|
80
|
+
orphanNotes,
|
|
40
81
|
resultFiles: files.filter((file) => file.path.startsWith(".memory-result.v1-")).map((file) => file.path),
|
|
41
82
|
};
|
|
42
83
|
io.out(JSON.stringify(report, null, 2) + "\n");
|
|
@@ -59,3 +100,6 @@ import { parseArgs } from "node:util";
|
|
|
59
100
|
import { z } from "zod";
|
|
60
101
|
import { inventoryLimits, scanMemorySources } from "./inventory-source.js";
|
|
61
102
|
import { previewMemoryInventory } from "./inventory-preview.js";
|
|
103
|
+
import { inspectV2MemoryInventory } from "./inventory-v2.js";
|
|
104
|
+
import { detectLocalMemoryStorageVersion } from "./storage-format.js";
|
|
105
|
+
import { isDeferredMemoryArtifactPath } from "./source-path.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,362 @@
|
|
|
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
|
+
}
|