@nowcrew/daemon 0.6.93-beta.1 → 0.6.93-beta.10
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/agent-environment-execution.js +7 -2
- package/dist/cloud-report-upload.js +1 -10
- package/dist/execution-runner.js +1 -0
- package/dist/local-executor.js +25 -30
- package/dist/local-memory/commit-telemetry.js +30 -0
- package/dist/local-memory/execution-workspace.js +9 -2
- package/dist/local-memory/initialization-identity.js +18 -5
- package/dist/local-memory/inventory-cli.js +33 -17
- package/dist/local-memory/inventory-v2.js +282 -0
- package/dist/local-memory/migration-access.js +31 -8
- package/dist/local-memory/migration-pending.js +9 -0
- package/dist/local-memory/migration-plan.js +37 -3
- package/dist/local-memory/migration-state-invariants.js +65 -0
- package/dist/local-memory/migration-store.js +7 -35
- 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 +2 -2
- package/dist/local-memory/read-cli.js +4 -3
- package/dist/local-memory/rollout.js +32 -10
- package/dist/local-memory/runtime.js +46 -13
- package/dist/local-memory/write.js +63 -2
- package/dist/local-memory/writer-mode.js +13 -0
- package/dist/local-memory-diagnostics.js +55 -13
- 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 +2 -0
- package/dist/memory-migration-inspect.js +40 -7
- package/dist/prompt.js +1 -1
- package/dist/runtimes/claude-thread-runner.js +25 -4
- package/dist/serve.js +2 -0
- package/dist/workspace.js +3 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./storage
|
|
|
9
9
|
import { readMemoryInventorySource, inventoryLimits } from "./inventory-source.js";
|
|
10
10
|
import { MEMORY_STATE_GUARD_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY } from "../memory-migration-protocol.js";
|
|
11
11
|
import { isLocalMemoryInitializationIdentity, localMemoryInitializationIdentity } from "./initialization-identity.js";
|
|
12
|
+
import { assertMigrationStateIntegrity } from "./migration-state-invariants.js";
|
|
12
13
|
export const LOCAL_MEMORY_V2_WRITER_CAPABILITIES = [MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY];
|
|
13
14
|
/**
|
|
14
15
|
* Keep an execution that started before migration from writing a committed V2
|
|
@@ -77,14 +78,33 @@ export async function assertMemoryAccessAllowed(agentDir, options = {}) {
|
|
|
77
78
|
try {
|
|
78
79
|
const source = await readMemoryInventorySource(agentDir, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
|
|
79
80
|
state = StateGate.parse(JSON.parse(source.content));
|
|
80
|
-
if (state.phase === "applying" || state.phase === "repair_required") {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
if (state.phase === "prepared" || state.phase === "applying" || state.phase === "repair_required") {
|
|
82
|
+
if (options.pendingOnly && (state.phase === "prepared" || state.phase === "applying")) {
|
|
83
|
+
const fullState = MigrationStateSchema.parse(state);
|
|
84
|
+
assertMigrationStateIntegrity(fullState, fullState.identity);
|
|
85
|
+
// The migration inventory owns the pending directory while its journal
|
|
86
|
+
// is active. Allowing an execution to append a candidate here would
|
|
87
|
+
// make the next classify() see an unplanned file and force
|
|
88
|
+
// repair_required. Keep the journal fail-closed and let pending-only
|
|
89
|
+
// apply only to the marker-only legacy state (no active journal).
|
|
90
|
+
throw new Error("memory_migration_in_progress");
|
|
91
|
+
}
|
|
92
|
+
const recovery = options.initializedRecoveryIdentity;
|
|
93
|
+
if (recovery === undefined)
|
|
83
94
|
throw new Error("memory_migration_in_progress");
|
|
84
95
|
const fullState = MigrationStateSchema.parse(state);
|
|
85
|
-
|
|
96
|
+
assertMigrationStateIntegrity(fullState, fullState.identity);
|
|
97
|
+
const binding = recovery.workspaceId !== undefined && recovery.machineId !== undefined
|
|
98
|
+
? { workspaceId: recovery.workspaceId, machineId: recovery.machineId }
|
|
99
|
+
: undefined;
|
|
100
|
+
const identity = await localMemoryInitializationIdentity(agentDir, recovery.agentId, binding);
|
|
101
|
+
const legacyIdentity = binding === undefined
|
|
102
|
+
? identity
|
|
103
|
+
: await localMemoryInitializationIdentity(agentDir, recovery.agentId);
|
|
86
104
|
if (fullState.origin !== "initialized"
|
|
87
|
-
||
|
|
105
|
+
|| (fullState.phase === "prepared" && fullState.intentReceipt !== undefined)
|
|
106
|
+
|| (!isLocalMemoryInitializationIdentity(fullState.identity, identity)
|
|
107
|
+
&& !isLocalMemoryInitializationIdentity(fullState.identity, legacyIdentity))) {
|
|
88
108
|
throw new Error("memory_migration_in_progress");
|
|
89
109
|
}
|
|
90
110
|
return;
|
|
@@ -112,8 +132,10 @@ export async function assertMemoryAccessAllowed(agentDir, options = {}) {
|
|
|
112
132
|
if (!state)
|
|
113
133
|
throw new Error("memory_migration_state_missing");
|
|
114
134
|
const verified = MigrationStateSchema.parse(state);
|
|
135
|
+
assertMigrationStateIntegrity(verified, verified.identity);
|
|
115
136
|
if (verified.phase !== "committed" || verified.storageVersion !== 2 || version !== 2
|
|
116
|
-
|| verified.identity.rootFingerprint !== createHash("sha256").update(await realpath(agentDir)).digest("hex")
|
|
137
|
+
|| verified.identity.rootFingerprint !== createHash("sha256").update(await realpath(agentDir)).digest("hex")
|
|
138
|
+
|| verified.identity.ownerIdentity !== String((await lstat(agentDir)).uid)) {
|
|
117
139
|
throw new Error("memory_migration_state_invalid");
|
|
118
140
|
}
|
|
119
141
|
parseFullMemoryIndex(source.content);
|
|
@@ -171,9 +193,10 @@ export async function withMemoryAccess(agentDir, operation, options = {}) {
|
|
|
171
193
|
try {
|
|
172
194
|
if (!options.maintenance)
|
|
173
195
|
await assertMemoryAccessAllowed(root, {
|
|
174
|
-
...(options.
|
|
175
|
-
|
|
196
|
+
...(options.initializedRecoveryIdentity === undefined ? {} : {
|
|
197
|
+
initializedRecoveryIdentity: options.initializedRecoveryIdentity,
|
|
176
198
|
}),
|
|
199
|
+
...(options.pendingOnly ? { pendingOnly: true } : {}),
|
|
177
200
|
});
|
|
178
201
|
lease.assertUsable();
|
|
179
202
|
return await operation({ created });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { detectLocalMemoryStorageVersion } from "./storage-format.js";
|
|
2
|
+
export function shouldUseMigrationDeferredPending(input) {
|
|
3
|
+
return input.pruneTraceId !== null
|
|
4
|
+
&& input.writerMode === "blocked"
|
|
5
|
+
&& input.rolloutEnabled
|
|
6
|
+
&& input.legacyFallback !== true
|
|
7
|
+
&& input.failure !== "inspection_failed"
|
|
8
|
+
&& detectLocalMemoryStorageVersion(input.memory) === 1;
|
|
9
|
+
}
|
|
@@ -17,7 +17,10 @@ export function initializeExactWorkspaceSeed(bytes) {
|
|
|
17
17
|
return null;
|
|
18
18
|
const heading = '\n## Knowledge Index\n';
|
|
19
19
|
const insertion = memory.length - tail.length + tail.indexOf(heading) + heading.length;
|
|
20
|
-
|
|
20
|
+
const initialized = `${memory.slice(0, insertion)}${renderFullMemoryIndex([])}\n${memory.slice(insertion)}`;
|
|
21
|
+
return Buffer.from(initialized
|
|
22
|
+
.replace(/<!-- 使用以下格式:[\s\S]*?-->\n<!-- 示例:[\s\S]*?-->\n/u, "<!-- V2 结构化索引由 daemon 管理。规范字段为 id、file、type、status、summary、triggers、priority、revision、anchor、updated,可选 review_after;不包含 scope。 -->\n")
|
|
23
|
+
.replace("- 每条索引必须保留 file、type、scope、summary、triggers、priority、updated 这些英文协议字段名。", "- V2 结构化索引由 daemon 管理;不要直接编辑索引字段或正文锚点。"));
|
|
21
24
|
}
|
|
22
25
|
const categories = /^(?:preferences|project-rules|testing|debugging|git|incidents|lessons)$/u;
|
|
23
26
|
const blockPattern = /<!--\s*NOWWORK:MEMORY_ENTRY:BEGIN\s+id=([A-Za-z0-9_-]+)\s+revision=([1-9][0-9]*)\s*-->([\s\S]*?)<!--\s*NOWWORK:MEMORY_ENTRY:END\s+id=\1\s*-->/gu;
|
|
@@ -28,6 +31,29 @@ function migrationSummary(body, title) {
|
|
|
28
31
|
.replace(/[\u0000-\u001f\u007f]+/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 180);
|
|
29
32
|
return clean(body) || clean(title) || 'Migrated memory';
|
|
30
33
|
}
|
|
34
|
+
const MIGRATION_EPOCH_DATE = '1970-01-01';
|
|
35
|
+
function parseLegacyCardMetadata(card) {
|
|
36
|
+
const fieldMatches = [...card.matchAll(/^- ([a-z_]+):[^\S\r\n]*(.*)$/gmu)];
|
|
37
|
+
const fields = Object.fromEntries(fieldMatches.map(match => [match[1], match[2].trim()]));
|
|
38
|
+
const allowedFields = new Set(['file', 'scope', 'type', 'status', 'summary', 'priority', 'updated', 'triggers', 'review_after', 'id', 'anchor', 'revision']);
|
|
39
|
+
const category = /^notes\/(.+)\.md$/u.exec(fields.file ?? '')?.[1] ?? '';
|
|
40
|
+
const valid = !card.replace(/^- ([a-z_]+):[^\S\r\n]*(.*)$/gmu, '').replace(/^### .+\r?\n/u, '').trim()
|
|
41
|
+
&& fieldMatches.length === Object.keys(fields).length
|
|
42
|
+
&& Object.keys(fields).every(key => allowedFields.has(key))
|
|
43
|
+
&& Boolean(categories.test(category) && fields.summary
|
|
44
|
+
&& ['procedural', 'semantic', 'episodic', 'preference'].includes(fields.type ?? '')
|
|
45
|
+
&& ['candidate', 'active', 'pinned', 'stale', 'superseded', 'archived'].includes(fields.status ?? '')
|
|
46
|
+
&& ['high', 'medium', 'low'].includes(fields.priority ?? '') && fields.updated);
|
|
47
|
+
if (!valid)
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
type: fields.type, summary: fields.summary,
|
|
51
|
+
priority: fields.priority, updated: fields.updated,
|
|
52
|
+
triggers: (fields.triggers ?? '').split(/[,,、]/u).map(item => item.trim()).filter(Boolean),
|
|
53
|
+
reviewAfter: fields.review_after ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const migrationDate = () => new Date().toISOString().slice(0, 10);
|
|
31
57
|
export function validateMigrationTarget(files) {
|
|
32
58
|
let totalBytes = 0;
|
|
33
59
|
for (const [path, bytes] of files) {
|
|
@@ -61,6 +87,7 @@ export function planMemoryMigration(identity, files, supplied = []) {
|
|
|
61
87
|
if (detectLocalMemoryStorageVersion(memory) !== identity.sourceStorageVersion)
|
|
62
88
|
throw new MigrationError('migration_source_version_mismatch');
|
|
63
89
|
const confirmedMappings = supplied.map(value => MigrationRecordMappingSchema.parse(value));
|
|
90
|
+
const fallbackUpdated = migrationDate();
|
|
64
91
|
const mappingSourceDigests = new Map();
|
|
65
92
|
for (const mapping of confirmedMappings) {
|
|
66
93
|
if (mapping.sourceSha256 === undefined)
|
|
@@ -136,7 +163,7 @@ export function planMemoryMigration(identity, files, supplied = []) {
|
|
|
136
163
|
const newEntries = additions.map(mapping => ({ id: mapping.targetId, title: mapping.title,
|
|
137
164
|
file: `notes/${mapping.category}.md`, type: 'semantic', status: mapping.status,
|
|
138
165
|
summary: migrationSummary(retainedHistory.get(mapping.targetId)?.body ?? files.get(mapping.sourceFile).toString('utf8').slice(mapping.start, mapping.end), mapping.title),
|
|
139
|
-
triggers: [], priority: 'medium', updated:
|
|
166
|
+
triggers: [], priority: 'medium', updated: fallbackUpdated, anchor: mapping.targetId, revision: retainedHistory.get(mapping.targetId)?.revision ?? 1, reviewAfter: null }));
|
|
140
167
|
for (const mapping of additions) {
|
|
141
168
|
if (retainedHistory.has(mapping.targetId))
|
|
142
169
|
continue;
|
|
@@ -229,8 +256,15 @@ export function planMemoryMigration(identity, files, supplied = []) {
|
|
|
229
256
|
continue;
|
|
230
257
|
}
|
|
231
258
|
const id = mapping.targetId ?? `mem_${digest(`${identity.migrationId}:${mapping.sourceFile}:${mapping.start}:${mapping.end}`).slice(0, 24)}`;
|
|
259
|
+
const confirmedMetadata = mapping.sourceFile === 'MEMORY.md' ? parseLegacyCardMetadata(body.slice(mapping.start, mapping.end)) : null;
|
|
232
260
|
entries.push({ id, title: mapping.title, file: `notes/${mapping.category}.md`, type: 'semantic', status: mapping.status, summary: migrationSummary(body.slice(mapping.start, mapping.end), mapping.title),
|
|
233
|
-
triggers: [], priority: 'medium', updated:
|
|
261
|
+
triggers: [], priority: 'medium', updated: fallbackUpdated, anchor: id, revision: 1, reviewAfter: null,
|
|
262
|
+
...knownMetadata.get(mapping.sourceFile),
|
|
263
|
+
...(confirmedMetadata ? (() => {
|
|
264
|
+
const source = confirmedMetadata;
|
|
265
|
+
return { type: source.type, summary: source.summary, triggers: source.triggers, priority: source.priority,
|
|
266
|
+
updated: source.updated === MIGRATION_EPOCH_DATE ? fallbackUpdated : source.updated, reviewAfter: source.reviewAfter };
|
|
267
|
+
})() : {}) });
|
|
234
268
|
}
|
|
235
269
|
for (const [path, bytes] of files) {
|
|
236
270
|
if (!path.startsWith('notes/') && path !== 'MEMORY.md')
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { MigrationError, MigrationIntentReceiptSchema, } from './migration-schema.js';
|
|
3
|
+
export const migrationOperationRoot = (identity) => `.crew/memory/migrations/${identity.migrationId}`;
|
|
4
|
+
function canonical(data) {
|
|
5
|
+
if (Array.isArray(data))
|
|
6
|
+
return data.map(canonical);
|
|
7
|
+
if (data !== null && typeof data === 'object') {
|
|
8
|
+
return Object.fromEntries(Object.entries(data)
|
|
9
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
10
|
+
.map(([key, value]) => [key, canonical(value)]));
|
|
11
|
+
}
|
|
12
|
+
return data;
|
|
13
|
+
}
|
|
14
|
+
export const encodeMigrationData = (data) => Buffer.from(JSON.stringify(canonical(data)));
|
|
15
|
+
export const sameMigrationData = (left, right) => encodeMigrationData(left).equals(encodeMigrationData(right));
|
|
16
|
+
export function migrationPlanDigest(identity, sourceDigest, manifest) {
|
|
17
|
+
return createHash('sha256')
|
|
18
|
+
.update(encodeMigrationData({ identity, sourceDigest, manifest }))
|
|
19
|
+
.digest('hex');
|
|
20
|
+
}
|
|
21
|
+
export function verifyMigrationIntent(receipt, state) {
|
|
22
|
+
const parsed = MigrationIntentReceiptSchema.parse(receipt);
|
|
23
|
+
for (const key of Object.keys(state.identity)) {
|
|
24
|
+
if (parsed[key] !== state.identity[key])
|
|
25
|
+
throw new MigrationError('migration_intent_identity_mismatch');
|
|
26
|
+
}
|
|
27
|
+
if (parsed.sourceDigest !== state.sourceDigest || parsed.planDigest !== state.planDigest) {
|
|
28
|
+
throw new MigrationError('migration_intent_digest_mismatch');
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
/** Pure validation shared by migration recovery and read-only inventory. */
|
|
33
|
+
export function assertMigrationStateIntegrity(state, identity) {
|
|
34
|
+
if (!sameMigrationData(state.identity, identity) || state.migrationId !== identity.migrationId
|
|
35
|
+
|| state.planDigest !== migrationPlanDigest(identity, state.sourceDigest, state.manifest)) {
|
|
36
|
+
throw new MigrationError('migration_identity_or_manifest_invalid');
|
|
37
|
+
}
|
|
38
|
+
// A prepared/applying/rolled-back journal still describes the source store;
|
|
39
|
+
// only a committed journal describes the V2 target. repair_required may
|
|
40
|
+
// represent either side because it can be recorded after a committed target
|
|
41
|
+
// is found damaged. Reject contradictory states before any recovery or
|
|
42
|
+
// pending-only escape hatch can use them.
|
|
43
|
+
const sourceVersion = state.identity.sourceStorageVersion;
|
|
44
|
+
const sourcePhase = state.phase === 'prepared' || state.phase === 'applying' || state.phase === 'rolled_back';
|
|
45
|
+
if (state.origin === 'initialized' && sourceVersion !== 0
|
|
46
|
+
|| sourcePhase && state.storageVersion !== sourceVersion
|
|
47
|
+
|| state.phase === 'committed' && state.storageVersion !== state.targetStorageVersion
|
|
48
|
+
|| state.targetStorageVersion !== identity.targetStorageVersion) {
|
|
49
|
+
throw new MigrationError('migration_identity_or_manifest_invalid');
|
|
50
|
+
}
|
|
51
|
+
const paths = state.manifest.files.map(file => file.path);
|
|
52
|
+
if (new Set(paths).size !== paths.length || !paths.includes('MEMORY.md')) {
|
|
53
|
+
throw new MigrationError('migration_manifest_invalid');
|
|
54
|
+
}
|
|
55
|
+
const operationRoot = `${migrationOperationRoot(identity)}/`;
|
|
56
|
+
for (const file of state.manifest.files) {
|
|
57
|
+
for (const image of [file.before, file.after]) {
|
|
58
|
+
if (image.exists && !image.ref.startsWith(operationRoot)) {
|
|
59
|
+
throw new MigrationError('migration_manifest_invalid');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (state.intentReceipt !== undefined)
|
|
64
|
+
verifyMigrationIntent(state.intentReceipt, state);
|
|
65
|
+
}
|
|
@@ -3,33 +3,10 @@ import { dirname } from 'node:path';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { atomicBytes, digest, inventory, readBytes, safePath, STATE_PATH, syncDirectory, validateMigrationRoot } from './migration-io.js';
|
|
5
5
|
import { initializeExactWorkspaceSeed, planMemoryMigration, validateMigrationTarget } from './migration-plan.js';
|
|
6
|
-
import { MigrationError, MigrationHistorySchema,
|
|
6
|
+
import { MigrationError, MigrationHistorySchema, MigrationProcessStoppedReceiptSchema, MigrationStateSchema } from './migration-schema.js';
|
|
7
|
+
import { assertMigrationStateIntegrity as assertIdentity, encodeMigrationData as encode, migrationOperationRoot as opRoot, migrationPlanDigest as planDigest, sameMigrationData as same, verifyMigrationIntent as verifyIntent, } from './migration-state-invariants.js';
|
|
7
8
|
import { detectLocalMemoryStorageVersion } from './storage-format.js';
|
|
8
9
|
import { isLocalMemoryInitializationIdentity, localMemoryInitializationIdentity } from './initialization-identity.js';
|
|
9
|
-
const opRoot = (identity) => `.crew/memory/migrations/${identity.migrationId}`;
|
|
10
|
-
function canonical(data) {
|
|
11
|
-
if (Array.isArray(data))
|
|
12
|
-
return data.map(canonical);
|
|
13
|
-
if (data !== null && typeof data === 'object')
|
|
14
|
-
return Object.fromEntries(Object.entries(data).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => [key, canonical(value)]));
|
|
15
|
-
return data;
|
|
16
|
-
}
|
|
17
|
-
const encode = (data) => Buffer.from(JSON.stringify(canonical(data)));
|
|
18
|
-
const same = (a, b) => encode(a).equals(encode(b));
|
|
19
|
-
const planDigest = (identity, sourceDigest, manifest) => digest(encode({ identity, sourceDigest, manifest }));
|
|
20
|
-
function assertIdentity(state, identity) {
|
|
21
|
-
if (!same(state.identity, identity) || state.migrationId !== identity.migrationId
|
|
22
|
-
|| state.planDigest !== planDigest(identity, state.sourceDigest, state.manifest))
|
|
23
|
-
throw new MigrationError('migration_identity_or_manifest_invalid');
|
|
24
|
-
const paths = state.manifest.files.map(file => file.path);
|
|
25
|
-
if (new Set(paths).size !== paths.length || !paths.includes('MEMORY.md'))
|
|
26
|
-
throw new MigrationError('migration_manifest_invalid');
|
|
27
|
-
for (const file of state.manifest.files)
|
|
28
|
-
for (const image of [file.before, file.after]) {
|
|
29
|
-
if (image.exists && !image.ref.startsWith(`${opRoot(identity)}/`))
|
|
30
|
-
throw new MigrationError('migration_manifest_invalid');
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
10
|
async function immutable(root, path, data) {
|
|
34
11
|
const previous = await readBytes(root, path, 64 * 1024 * 1024);
|
|
35
12
|
if (previous) {
|
|
@@ -41,6 +18,7 @@ async function immutable(root, path, data) {
|
|
|
41
18
|
}
|
|
42
19
|
async function save(root, state) {
|
|
43
20
|
const parsed = MigrationStateSchema.parse(state);
|
|
21
|
+
assertIdentity(parsed, parsed.identity);
|
|
44
22
|
if (parsed.phase === 'committed' || parsed.phase === 'rolled_back') {
|
|
45
23
|
const previous = await readBytes(root, `${opRoot(parsed.identity)}/terminal.json`, 32 * 1024 * 1024);
|
|
46
24
|
if (previous && !same(JSON.parse(previous.toString('utf8')), parsed)) {
|
|
@@ -76,15 +54,6 @@ export async function statusMemoryMigration(input) {
|
|
|
76
54
|
const identity = await validateMigrationRoot(input.agentDir, input.identity);
|
|
77
55
|
return load({ ...input, identity });
|
|
78
56
|
}
|
|
79
|
-
function verifyIntent(receipt, state) {
|
|
80
|
-
const parsed = MigrationIntentReceiptSchema.parse(receipt);
|
|
81
|
-
for (const key of Object.keys(state.identity))
|
|
82
|
-
if (parsed[key] !== state.identity[key])
|
|
83
|
-
throw new MigrationError('migration_intent_identity_mismatch');
|
|
84
|
-
if (parsed.sourceDigest !== state.sourceDigest || parsed.planDigest !== state.planDigest)
|
|
85
|
-
throw new MigrationError('migration_intent_digest_mismatch');
|
|
86
|
-
return parsed;
|
|
87
|
-
}
|
|
88
57
|
export async function prepareMemoryMigration(input, hooks = {}) {
|
|
89
58
|
const identity = await validateMigrationRoot(input.agentDir, input.identity);
|
|
90
59
|
const history = MigrationHistorySchema.parse(input.serverHistory);
|
|
@@ -120,7 +89,10 @@ export async function prepareMemoryMigration(input, hooks = {}) {
|
|
|
120
89
|
const previousSeed = existing?.origin === 'initialized' && existing.phase === 'rolled_back'
|
|
121
90
|
? existing.manifest.files.find(file => file.path === 'MEMORY.md')?.before : undefined;
|
|
122
91
|
const retryingOriginalSeed = previousSeed?.exists === true && sourceMemory !== undefined && digest(sourceMemory) === previousSeed.sha256;
|
|
123
|
-
const expectedInitializationIdentity = await localMemoryInitializationIdentity(input.agentDir, identity.agentId
|
|
92
|
+
const expectedInitializationIdentity = await localMemoryInitializationIdentity(input.agentDir, identity.agentId, {
|
|
93
|
+
workspaceId: identity.workspaceId,
|
|
94
|
+
machineId: identity.machineId,
|
|
95
|
+
});
|
|
124
96
|
const onlyCurrentOperationResidue = isLocalMemoryInitializationIdentity(identity, expectedInitializationIdentity)
|
|
125
97
|
&& existing === null && operations.length === 1
|
|
126
98
|
&& operations[0] === identity.migrationId;
|
|
@@ -15,6 +15,7 @@ const SearchSchema = z.object({
|
|
|
15
15
|
cursor: z.string().max(2048).optional(), limit: z.number().int().min(1).max(40).default(10),
|
|
16
16
|
}).strict();
|
|
17
17
|
const excerpt = (text, cap) => Array.from(text).slice(0, cap).join("");
|
|
18
|
+
const pendingState = (reason) => reason === "legacy_requires_migration" ? "migration_deferred" : "capacity_deferred";
|
|
18
19
|
export async function searchPendingMemoryPage(input) {
|
|
19
20
|
const parsed = SearchSchema.safeParse(input);
|
|
20
21
|
if (!parsed.success)
|
|
@@ -61,7 +62,8 @@ export async function searchPendingMemoryPage(input) {
|
|
|
61
62
|
const match = source && isRecallablePending(source.candidate) && pendingRecallScore(source.candidate, args.query) > 0
|
|
62
63
|
? { pendingId: source.candidate.pendingId, sha256: source.sha256, title: source.candidate.candidate.title,
|
|
63
64
|
summary: excerpt(source.candidate.candidate.summary, 180), triggers: source.candidate.candidate.triggers.slice(0, 4),
|
|
64
|
-
operation: "create",
|
|
65
|
+
operation: "create", reason: source.candidate.reason,
|
|
66
|
+
state: pendingState(source.candidate.reason) }
|
|
65
67
|
: null;
|
|
66
68
|
const addition = match ?? warning;
|
|
67
69
|
const size = addition ? Buffer.byteLength(JSON.stringify(addition), "utf8") + 2 : 0;
|
|
@@ -96,5 +98,6 @@ export async function readPendingMemoryBody(input) {
|
|
|
96
98
|
throw new Error("memory_pending_not_recallable");
|
|
97
99
|
const candidate = source.candidate.candidate;
|
|
98
100
|
return { pendingId: source.candidate.pendingId, sha256: source.sha256, title: candidate.title,
|
|
99
|
-
operation: "create",
|
|
101
|
+
operation: "create", reason: source.candidate.reason,
|
|
102
|
+
state: pendingState(source.candidate.reason), content: candidate.detailBody };
|
|
100
103
|
}
|
|
@@ -8,7 +8,7 @@ const ArchivedMemoryChangeSchema = LocalMemoryChangeSchema.extend({
|
|
|
8
8
|
export const PendingIdSchema = z.string().max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
|
|
9
9
|
export const PendingMemorySchema = z.object({
|
|
10
10
|
schemaVersion: z.literal(1), pendingId: PendingIdSchema,
|
|
11
|
-
createdAt: z.string().min(1).max(100), reason: z.
|
|
11
|
+
createdAt: z.string().min(1).max(100), reason: z.enum(["index_over_budget", "legacy_requires_migration"]),
|
|
12
12
|
source: z.object({ executionId: z.string().min(1).max(500), pruneTraceId: z.string().min(1).max(500).nullable() }).strict(),
|
|
13
13
|
candidate: ArchivedMemoryChangeSchema,
|
|
14
14
|
}).strict();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, realpath, rename, rm, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { isRecallablePending, parsePendingMemoryCandidate } from "./pending-schema.js";
|
|
5
5
|
import { pendingFileNames, pendingReadDirectory, PENDING_FILE_BYTES, readPendingSource } from "./pending-source.js";
|
|
@@ -103,6 +103,17 @@ export async function persistPendingMemoryCandidate(input) {
|
|
|
103
103
|
const directory = await pendingDirectory(input.agentDir);
|
|
104
104
|
const target = join(directory, `${parsed.pendingId}.json`);
|
|
105
105
|
await assertRegularFile(target);
|
|
106
|
+
try {
|
|
107
|
+
const existing = parsePendingMemoryCandidate(JSON.parse(await readFile(target, "utf8")));
|
|
108
|
+
const dedupKey = (value) => value.reason === "legacy_requires_migration"
|
|
109
|
+
? JSON.stringify({ schemaVersion: value.schemaVersion, reason: value.reason, candidate: value.candidate })
|
|
110
|
+
: JSON.stringify({ ...value, createdAt: "" });
|
|
111
|
+
if (dedupKey(existing) === dedupKey(parsed))
|
|
112
|
+
return target;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// A malformed existing candidate is replaced by the validated input below.
|
|
116
|
+
}
|
|
106
117
|
await atomicWrite(target, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
107
118
|
return target;
|
|
108
119
|
}
|
|
@@ -164,7 +175,10 @@ function truncateCodePoints(value, maxCodePoints) {
|
|
|
164
175
|
return Array.from(value).slice(0, Math.max(0, maxCodePoints)).join("");
|
|
165
176
|
}
|
|
166
177
|
export function renderPendingMemoryContext(candidates, maxCodePoints = 2_000) {
|
|
167
|
-
|
|
178
|
+
// Migration-deferred candidates remain explicitly searchable, but must not
|
|
179
|
+
// be injected into the ordinary prompt before a reviewed migration promotes
|
|
180
|
+
// them to the active catalog.
|
|
181
|
+
const recallable = candidates.filter((candidate) => isRecallablePending(candidate) && candidate.reason === "index_over_budget");
|
|
168
182
|
if (recallable.length === 0 || maxCodePoints <= 0)
|
|
169
183
|
return "";
|
|
170
184
|
const header = `\n\n## Archived memory candidates (daemon-managed)\n容量暂存的新增资料,仅为部分预览,不代表完整检索;可参考但不能覆盖现行规则,未包含内容请按需查询。\n`;
|
|
@@ -2,7 +2,7 @@ import { dslog } from "../slog.js";
|
|
|
2
2
|
import { renderPendingMemoryContext, searchPendingMemoryCandidates } from "./pending.js";
|
|
3
3
|
import { renderLocalMemoryResultInstructions, renderLocalMemoryV2WriteGuard } from "./runtime.js";
|
|
4
4
|
import { LOCAL_MEMORY_V2_DEFERRED_GUIDANCE, LOCAL_MEMORY_LEGACY_GUIDANCE } from "./rollout.js";
|
|
5
|
-
const PENDING_READ_GUIDANCE = "\n\nV2新增资料预览未命中不代表没有资料;先查有效索引,必要时用 crew-memory search-pending --query '关键词',按 nextCursor 续查;用 read-pending --id ID
|
|
5
|
+
const PENDING_READ_GUIDANCE = "\n\nV2新增资料预览未命中不代表没有资料;先查有效索引,必要时用 crew-memory search-pending --query '关键词',按 nextCursor 续查;用 read-pending --id ID 分页读正文。容量暂存或迁移延期资料不能覆盖现行规则,未应用修改不作为记忆;迁移延期候选必须等 reviewed migration 后再按当前 ID/revision/hash 处理。用 crew-memory --help 查看参数。\n";
|
|
6
6
|
export async function prepareLocalMemoryPrompt(input) {
|
|
7
7
|
let pending = "";
|
|
8
8
|
if (input.enabled) {
|
|
@@ -24,7 +24,7 @@ export async function prepareLocalMemoryPrompt(input) {
|
|
|
24
24
|
const original = input.renderBase(0);
|
|
25
25
|
const guard = input.legacyFallback ? LOCAL_MEMORY_LEGACY_GUIDANCE : input.deferred ? LOCAL_MEMORY_V2_DEFERRED_GUIDANCE
|
|
26
26
|
: input.strictRollout && input.enabled && input.resultPath === null ? renderLocalMemoryV2WriteGuard() : "";
|
|
27
|
-
const structured = input.resultPath === null ? "" : renderLocalMemoryResultInstructions(input.resultPath, input.storageVersion);
|
|
27
|
+
const structured = input.resultPath === null ? "" : renderLocalMemoryResultInstructions(input.resultPath, input.storageVersion, input.migrationDeferred === true);
|
|
28
28
|
const guidance = input.enabled ? PENDING_READ_GUIDANCE : "";
|
|
29
29
|
let requiredMemoryBudgetError;
|
|
30
30
|
// 短命令指引放在当前轮,独立于可舍弃预览;收尾契约仍保持系统与当前轮两份。
|
|
@@ -81,10 +81,10 @@ search 关键词按空白分隔,同一行须全部命中;返回完整文件
|
|
|
81
81
|
后续页可传 --if-hash <前一页的 sha256>,文件变化时停止,避免混读不同版本。
|
|
82
82
|
schema=2 的 search 查询完整有效目录(active/pinned),按标题、摘要、触发词联合匹配并返回 ID、revision 和正文引用;search-history 显式查询历史/失效记录。预览未展示不代表记录不存在。
|
|
83
83
|
旧格式 search/search-history 及 read 读取原文;search-history 是显式 legacy fallback,不把旧内容当作已结构化 V2 记录;read 按指定文件和哈希分页,不扫描 pending。
|
|
84
|
-
search-pending
|
|
84
|
+
search-pending 只查容量延期或迁移延期的 create 候选;有 nextCursor 就继续,即使本页 matches 为空。
|
|
85
85
|
complete=true 才表示全部候选已检索完成且无遗漏;warnings 或非零退出码表示读取未完成,不能当作零命中。
|
|
86
86
|
read-pending 只返回候选正文;offset 非零时必须传 --if-hash,update/supersede 候选不可读取。
|
|
87
|
-
|
|
87
|
+
候选只供相关问题参考,未生效,不能覆盖有效规则;先读取当前有效索引与明细。迁移延期候选会标记为 migration_deferred,必须等 reviewed migration 后再按当前 ID/revision/hash 处理。`;
|
|
88
88
|
function inside(root, path) {
|
|
89
89
|
const value = relative(root, path);
|
|
90
90
|
return value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
|
|
@@ -180,8 +180,9 @@ function writeResult(result, io) {
|
|
|
180
180
|
export async function runMemoryReadCommand(argv, io) {
|
|
181
181
|
if (!io.env.CREW_HOME || argv.includes("--help") || argv.includes("-h"))
|
|
182
182
|
return runMemoryReadUnlocked(argv, io);
|
|
183
|
+
const pendingOnly = argv[0] === "search-pending" || argv[0] === "read-pending";
|
|
183
184
|
try {
|
|
184
|
-
return await withMemoryAccess(io.env.CREW_HOME, () => runMemoryReadUnlocked(argv, io));
|
|
185
|
+
return await withMemoryAccess(io.env.CREW_HOME, () => runMemoryReadUnlocked(argv, io), pendingOnly ? { pendingOnly: true } : {});
|
|
185
186
|
}
|
|
186
187
|
catch (error) {
|
|
187
188
|
const reason = error instanceof Error && /^(?:local_)?memory_[a-z_]+$/u.test(error.message) ? error.message : "memory_migration_access_failed";
|
|
@@ -10,6 +10,19 @@ import { dslog } from "../slog.js";
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./storage-format.js";
|
|
12
12
|
import { MigrationStateSchema } from "./migration-schema.js";
|
|
13
|
+
import { assertMigrationStateIntegrity } from "./migration-state-invariants.js";
|
|
14
|
+
const TRANSIENT_INSPECTION_ERRORS = new Set([
|
|
15
|
+
"EACCES", "EPERM", "EIO", "EBUSY", "EAGAIN", "EINTR", "ESTALE", "EMFILE", "ENFILE", "ENOMEM", "ETIMEDOUT",
|
|
16
|
+
"source_changed", "memory_migration_access_busy",
|
|
17
|
+
]);
|
|
18
|
+
export function localMemoryRolloutFailure(error) {
|
|
19
|
+
const code = error?.code;
|
|
20
|
+
const reason = error instanceof Error ? error.message : "";
|
|
21
|
+
return (typeof code === "string" && TRANSIENT_INSPECTION_ERRORS.has(code))
|
|
22
|
+
|| TRANSIENT_INSPECTION_ERRORS.has(reason)
|
|
23
|
+
? "inspection_failed"
|
|
24
|
+
: "requires_migration";
|
|
25
|
+
}
|
|
13
26
|
export const LOCAL_MEMORY_V2_DEFERRED_GUIDANCE = `
|
|
14
27
|
|
|
15
28
|
## 本轮本地记忆兼容状态
|
|
@@ -22,12 +35,19 @@ export const LOCAL_MEMORY_LEGACY_GUIDANCE = `
|
|
|
22
35
|
本 Agent 尚未完成 V2 格式转换,本轮继续原记忆流程。用户明确要求新增或修改长期规则时,可按原规则最小范围更新共享记忆;记忆收尾继续依据任务日志更新原 MEMORY.md 和 notes,确认成功后再清理日志。
|
|
23
36
|
本节覆盖旧会话中因等待 V2 转换而冻结共享记忆写入的要求,但不是新的用户写入授权;本任务如明确要求不更新记忆,继续遵守。
|
|
24
37
|
保留原身份、目标及无关资料,不自行迁移或重置文件,不宣称已启用 V2。`;
|
|
25
|
-
export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune, legacyFallback = false) {
|
|
26
|
-
dslog("local_memory.v2_deferred",
|
|
27
|
-
|
|
38
|
+
export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune, legacyFallback = false, failure = "requires_migration", allowPendingOnly = false) {
|
|
39
|
+
dslog("local_memory.v2_deferred", allowPendingOnly
|
|
40
|
+
? "迁移尚未完成,新增候选隔离保存到 pending"
|
|
41
|
+
: legacyFallback ? "旧格式继续原记忆流程,转换成功后再启用V2" : "记忆格式异常,保留原资料和待处理日志", {
|
|
42
|
+
execution_id: executionId,
|
|
43
|
+
agent_handle: handle,
|
|
44
|
+
reason: legacyFallback ? "legacy_compatible" : failure,
|
|
45
|
+
writer_mode: allowPendingOnly ? "pending_only" : legacyFallback ? "legacy" : "blocked",
|
|
46
|
+
...(allowPendingOnly ? { pending_reason: "legacy_requires_migration" } : {}),
|
|
28
47
|
});
|
|
29
|
-
if (memoryPrune && !legacyFallback)
|
|
30
|
-
throw new Error("local_memory_v2_requires_migration");
|
|
48
|
+
if (memoryPrune && !legacyFallback && !allowPendingOnly) {
|
|
49
|
+
throw new Error(failure === "inspection_failed" ? "local_memory_inspection_failed" : "local_memory_v2_requires_migration");
|
|
50
|
+
}
|
|
31
51
|
}
|
|
32
52
|
/** 只核对小型索引和其引用;不扫描全部历史任务、不迁移或生成新存储。 */
|
|
33
53
|
export async function resolveLocalMemoryV2Rollout(input) {
|
|
@@ -43,14 +63,16 @@ export async function resolveLocalMemoryV2Rollout(input) {
|
|
|
43
63
|
const root = await realpath(input.workspace.dir);
|
|
44
64
|
const record = await readMemoryInventorySource(root, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
|
|
45
65
|
const state = MigrationStateSchema.parse(JSON.parse(record.content));
|
|
66
|
+
assertMigrationStateIntegrity(state, state.identity);
|
|
46
67
|
if (state.phase !== "committed" || state.storageVersion !== 2
|
|
47
68
|
|| state.identity.agentId !== (input.agentId ?? input.handle)
|
|
48
|
-
|| state.identity.rootFingerprint !== createHash("sha256").update(root).digest("hex")
|
|
69
|
+
|| state.identity.rootFingerprint !== createHash("sha256").update(root).digest("hex")
|
|
70
|
+
|| state.identity.ownerIdentity !== String((await lstat(root)).uid))
|
|
49
71
|
throw new Error("invalid_state");
|
|
50
72
|
return { enabled: true, deferred: false };
|
|
51
73
|
}
|
|
52
|
-
catch {
|
|
53
|
-
return { enabled: false, deferred: true };
|
|
74
|
+
catch (error) {
|
|
75
|
+
return { enabled: false, deferred: true, failure: localMemoryRolloutFailure(error) };
|
|
54
76
|
}
|
|
55
77
|
}
|
|
56
78
|
if (environment === "production")
|
|
@@ -122,7 +144,7 @@ export async function resolveLocalMemoryV2Rollout(input) {
|
|
|
122
144
|
}
|
|
123
145
|
return { enabled: true, deferred: false };
|
|
124
146
|
}
|
|
125
|
-
catch {
|
|
126
|
-
return deferred;
|
|
147
|
+
catch (error) {
|
|
148
|
+
return { ...deferred, failure: localMemoryRolloutFailure(error) };
|
|
127
149
|
}
|
|
128
150
|
}
|
|
@@ -55,8 +55,8 @@ function initializedIntent(identity, state) {
|
|
|
55
55
|
* publication path used by migration. The marker is written only after the
|
|
56
56
|
* committed catalog and state have both been verified.
|
|
57
57
|
*/
|
|
58
|
-
|
|
59
|
-
const identity = await localMemoryInitializationIdentity(agentDir, agentId);
|
|
58
|
+
async function initializeLocalMemoryV2WithBinding(agentDir, agentId, binding) {
|
|
59
|
+
const identity = await localMemoryInitializationIdentity(agentDir, agentId, binding);
|
|
60
60
|
let state = await prepareMemoryMigration({
|
|
61
61
|
agentDir,
|
|
62
62
|
identity,
|
|
@@ -96,7 +96,11 @@ export async function initializeLocalMemoryV2(agentDir, agentId) {
|
|
|
96
96
|
}
|
|
97
97
|
await writeLocalMemoryV2Marker(agentDir);
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
/** Fresh initialization requires the identity from the authenticated ready frame. */
|
|
100
|
+
export function initializeLocalMemoryV2(agentDir, agentId, binding) {
|
|
101
|
+
return initializeLocalMemoryV2WithBinding(agentDir, agentId, binding);
|
|
102
|
+
}
|
|
103
|
+
export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForHandle, agentId, binding) {
|
|
100
104
|
const initialized = workspace.localMemoryV2Initialized === true;
|
|
101
105
|
if (initialized)
|
|
102
106
|
return true;
|
|
@@ -105,12 +109,19 @@ export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForH
|
|
|
105
109
|
if (workspace.localMemoryV2Initialized !== false)
|
|
106
110
|
return false;
|
|
107
111
|
let resumable = false;
|
|
112
|
+
let effectiveBinding = binding;
|
|
108
113
|
try {
|
|
109
114
|
const state = MigrationStateSchema.parse(JSON.parse(await readFile(join(workspace.dir, ".crew", "memory", "migration.v1.json"), "utf8")));
|
|
110
|
-
const identity = await localMemoryInitializationIdentity(workspace.dir, agentId);
|
|
115
|
+
const identity = await localMemoryInitializationIdentity(workspace.dir, agentId, binding);
|
|
116
|
+
const legacyIdentity = binding === undefined
|
|
117
|
+
? identity
|
|
118
|
+
: await localMemoryInitializationIdentity(workspace.dir, agentId);
|
|
111
119
|
resumable = state.origin === "initialized"
|
|
112
120
|
&& state.phase !== "rolled_back"
|
|
113
|
-
&& isLocalMemoryInitializationIdentity(state.identity, identity)
|
|
121
|
+
&& (isLocalMemoryInitializationIdentity(state.identity, identity)
|
|
122
|
+
|| isLocalMemoryInitializationIdentity(state.identity, legacyIdentity));
|
|
123
|
+
if (resumable && isLocalMemoryInitializationIdentity(state.identity, legacyIdentity))
|
|
124
|
+
effectiveBinding = undefined;
|
|
114
125
|
}
|
|
115
126
|
catch (error) {
|
|
116
127
|
if (error.code !== "ENOENT")
|
|
@@ -120,8 +131,18 @@ export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForH
|
|
|
120
131
|
if (!migrations.isDirectory() || migrations.isSymbolicLink()) {
|
|
121
132
|
throw new Error("local_memory_v2_initialization_residue_invalid");
|
|
122
133
|
}
|
|
123
|
-
const identity = await localMemoryInitializationIdentity(workspace.dir, agentId);
|
|
124
|
-
const
|
|
134
|
+
const identity = await localMemoryInitializationIdentity(workspace.dir, agentId, binding);
|
|
135
|
+
const legacyIdentity = binding === undefined
|
|
136
|
+
? identity
|
|
137
|
+
: await localMemoryInitializationIdentity(workspace.dir, agentId);
|
|
138
|
+
let operationIdentity = identity;
|
|
139
|
+
const operation = await lstat(join(workspace.dir, ".crew", "memory", "migrations", identity.migrationId))
|
|
140
|
+
.catch(async (error) => {
|
|
141
|
+
if (error.code !== "ENOENT" || identity.migrationId === legacyIdentity.migrationId)
|
|
142
|
+
throw error;
|
|
143
|
+
operationIdentity = legacyIdentity;
|
|
144
|
+
return lstat(join(workspace.dir, ".crew", "memory", "migrations", legacyIdentity.migrationId));
|
|
145
|
+
});
|
|
125
146
|
if (!operation.isDirectory() || operation.isSymbolicLink()) {
|
|
126
147
|
throw new Error("local_memory_v2_initialization_residue_invalid");
|
|
127
148
|
}
|
|
@@ -130,6 +151,8 @@ export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForH
|
|
|
130
151
|
resumable = current.files.size === 1 && memory !== undefined
|
|
131
152
|
&& current.directories.every(path => path === "notes" || path === ".crew/memory/pending")
|
|
132
153
|
&& initializeExactWorkspaceSeed(memory) !== null;
|
|
154
|
+
if (resumable && operationIdentity.migrationId === legacyIdentity.migrationId)
|
|
155
|
+
effectiveBinding = undefined;
|
|
133
156
|
}
|
|
134
157
|
catch (residueError) {
|
|
135
158
|
if (residueError.code !== "ENOENT")
|
|
@@ -142,17 +165,23 @@ export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForH
|
|
|
142
165
|
&& workspace.memorySeedCreated === true;
|
|
143
166
|
if (!resumable && !pristineFirstInitialization)
|
|
144
167
|
return false;
|
|
145
|
-
|
|
168
|
+
// Never create another unbound sentinel. Undefined is retained solely to
|
|
169
|
+
// finish a pre-upgrade journal already proven above.
|
|
170
|
+
if (!resumable && effectiveBinding === undefined)
|
|
171
|
+
return false;
|
|
172
|
+
await initializeLocalMemoryV2WithBinding(workspace.dir, agentId, effectiveBinding);
|
|
146
173
|
return true;
|
|
147
174
|
}
|
|
148
175
|
export function localMemoryResultPath(runDir, executionId) {
|
|
149
176
|
const suffix = createHash("sha256").update(executionId, "utf8").digest("hex").slice(0, 32);
|
|
150
177
|
return join(runDir, `.memory-result.v1-${suffix}.json`);
|
|
151
178
|
}
|
|
152
|
-
export function renderLocalMemoryResultInstructions(resultPath, storageVersion = 1) {
|
|
153
|
-
const storagePolicy =
|
|
154
|
-
? "
|
|
155
|
-
:
|
|
179
|
+
export function renderLocalMemoryResultInstructions(resultPath, storageVersion = 1, migrationDeferred = false) {
|
|
180
|
+
const storagePolicy = migrationDeferred
|
|
181
|
+
? "该 Agent 的旧记忆仍在迁移审阅中。本轮只允许提交新的 create 候选;daemon 会把候选原子保存到 .crew/memory/pending/,reason=legacy_requires_migration,并提供 search-pending/read-pending 查询。候选不会进入 active,也不会自动注入普通上下文;更新、替代和 statusChanges 必须留待迁移完成后按当前 V2 ID/revision/hash 重新核对。旧 MEMORY.md、notes 和已有 active/pending 保持不变。"
|
|
182
|
+
: storageVersion === 2
|
|
183
|
+
? "完整目录保存在 MEMORY.md,模型只看到有界预览;未展示记录仍然有效。涉及旧规则时先用 crew-memory search 查询完整目录,按当前 ID/revision 更新,不因预览未命中就重复新建。20条预览不是有效记录上限;完整存储超出资源上限会拒绝提交,不得自行删除或降级旧记录。已有 pending 仍是未应用候选,须重新核对授权、重复与版本后通过 changes 申请保存,不因迁移自动生效。"
|
|
184
|
+
: "daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。";
|
|
156
185
|
return `
|
|
157
186
|
|
|
158
187
|
## Structured memory prune (daemon-managed)
|
|
@@ -243,6 +272,8 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
243
272
|
executionId: input.executionId,
|
|
244
273
|
pruneTraceId: input.pruneTraceId,
|
|
245
274
|
...(input.writerCapabilities === undefined ? {} : { writerCapabilities: input.writerCapabilities }),
|
|
275
|
+
...(input.requiredStorageVersion === undefined ? {} : { requiredStorageVersion: input.requiredStorageVersion }),
|
|
276
|
+
...(input.migrationDeferredPending ? { migrationDeferredPending: true } : {}),
|
|
246
277
|
});
|
|
247
278
|
storageAfter = await localMemoryStorageFacts(input.agentDir);
|
|
248
279
|
if (input.requiredStorageVersion === 2
|
|
@@ -282,5 +313,7 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
282
313
|
return { status: "applied", storageVersionBefore: storageBefore.storageVersion,
|
|
283
314
|
storageVersionAfter: storageAfter.storageVersion, committedState: storageAfter.committedState,
|
|
284
315
|
createdIds: applied.createdIds, updatedIds: applied.updatedIds,
|
|
285
|
-
statusChangedIds: applied.statusChangedIds, pendingIds: applied.pendingIds
|
|
316
|
+
statusChangedIds: applied.statusChangedIds, pendingIds: applied.pendingIds,
|
|
317
|
+
...(input.migrationDeferredPending
|
|
318
|
+
? { pendingReason: "legacy_requires_migration" } : {}) };
|
|
286
319
|
}
|