wendkeep 0.58.0 → 0.58.3
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/CHANGELOG.md +61 -0
- package/README.en.md +28 -36
- package/README.md +28 -36
- package/docs/en/commands/changes-and-verification.md +79 -0
- package/docs/en/commands/costs-and-observability.md +65 -0
- package/docs/en/commands/getting-started.md +82 -0
- package/docs/en/commands/maintenance-and-diagnostics.md +77 -0
- package/docs/en/commands/memory-migration.md +73 -0
- package/docs/en/commands/memory.md +84 -0
- package/docs/en/commands/notes-and-knowledge.md +70 -0
- package/docs/en/commands/retroactive-import.md +67 -0
- package/docs/en/commands/sessions-and-import.md +85 -0
- package/docs/en/commands/verify.md +86 -0
- package/docs/pt-BR/commands/changes-and-verification.md +80 -0
- package/docs/pt-BR/commands/costs-and-observability.md +65 -0
- package/docs/pt-BR/commands/getting-started.md +83 -0
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +77 -0
- package/docs/pt-BR/commands/memory-migration.md +73 -0
- package/docs/pt-BR/commands/memory.md +83 -0
- package/docs/pt-BR/commands/notes-and-knowledge.md +69 -0
- package/docs/pt-BR/commands/retroactive-import.md +67 -0
- package/docs/pt-BR/commands/sessions-and-import.md +85 -0
- package/docs/pt-BR/commands/verify.md +87 -0
- package/hooks/brain-inject.mjs +2 -1
- package/hooks/memory-mode.mjs +39 -0
- package/hooks/memory-schema.mjs +15 -0
- package/hooks/obsidian-common.mjs +80 -29
- package/hooks/session-ensure.mjs +15 -8
- package/hooks/session-memory-lifecycle.mjs +330 -0
- package/hooks/session-stop.mjs +122 -42
- package/hooks/vault-health.mjs +124 -5
- package/package.json +3 -1
- package/src/memory.mjs +32 -7
- package/src/taxonomy.mjs +1 -0
package/hooks/session-stop.mjs
CHANGED
|
@@ -14,7 +14,13 @@ import { mutateSessionNote } from './session-note-io.mjs';
|
|
|
14
14
|
import { applyDerivedSections, provenanceSessions } from './derived-sections.mjs';
|
|
15
15
|
import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-handoff.mjs';
|
|
16
16
|
import { enqueueMemoryEvent, projectMemoryOutbox } from './memory-store.mjs';
|
|
17
|
+
import { detectMemoryMode } from './memory-mode.mjs';
|
|
17
18
|
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
19
|
+
import {
|
|
20
|
+
projectStopMemoryAttempt,
|
|
21
|
+
recordStopMemoryOutcome,
|
|
22
|
+
stageStopMemoryAttempt,
|
|
23
|
+
} from './session-memory-lifecycle.mjs';
|
|
18
24
|
import {
|
|
19
25
|
ensureDir,
|
|
20
26
|
findActiveSessionByTranscript,
|
|
@@ -526,6 +532,26 @@ export function parseTranscript(transcriptPath) {
|
|
|
526
532
|
return parseCodexTranscript(transcriptPath);
|
|
527
533
|
}
|
|
528
534
|
|
|
535
|
+
export function resolveTurnIdentity(transcript, requestedTurnId = '') {
|
|
536
|
+
const turns = Array.isArray(transcript?.turns) ? transcript.turns : [];
|
|
537
|
+
const requested = String(requestedTurnId || '');
|
|
538
|
+
let index = requested
|
|
539
|
+
? turns.findIndex((turn) => String(turn?.turnId || '') === requested)
|
|
540
|
+
: -1;
|
|
541
|
+
if (requested && index < 0) return null;
|
|
542
|
+
if (index < 0 && transcript?.latestTurnId) {
|
|
543
|
+
index = turns.findIndex((turn) => String(turn?.turnId || '') === String(transcript.latestTurnId));
|
|
544
|
+
}
|
|
545
|
+
if (index < 0) index = turns.length - 1;
|
|
546
|
+
const turn = turns[index];
|
|
547
|
+
if (!turn?.turnId) return null;
|
|
548
|
+
return {
|
|
549
|
+
id: String(turn.turnId),
|
|
550
|
+
order: index + 1,
|
|
551
|
+
observedAt: String(turn.timestamp || ''),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
529
555
|
function compactText(text, max = 600) {
|
|
530
556
|
const clean = redactSecrets(String(text || ''))
|
|
531
557
|
.replace(/\r/g, '\n')
|
|
@@ -799,6 +825,9 @@ function shouldFinalizeSession() {
|
|
|
799
825
|
}
|
|
800
826
|
|
|
801
827
|
export function commitSessionMemory(vaultBase, handoff, { projectOptions = {} } = {}) {
|
|
828
|
+
if (detectMemoryMode(vaultBase).mode === 'legacy') {
|
|
829
|
+
return { status: 'legacy', eventCount: 0, eventIds: [], checkpoint: null };
|
|
830
|
+
}
|
|
802
831
|
const events = buildSessionMemoryEvents(handoff);
|
|
803
832
|
const eventIds = events.map((event) => event.event_id);
|
|
804
833
|
try {
|
|
@@ -1091,7 +1120,18 @@ function pingObsidianVault(apiKey) {
|
|
|
1091
1120
|
} catch {}
|
|
1092
1121
|
}
|
|
1093
1122
|
|
|
1094
|
-
function
|
|
1123
|
+
export function shouldAbortStopAfterStaging(causalStop, memoryAttempt) {
|
|
1124
|
+
const rejectedByV2Revalidation = memoryAttempt?.memory_mode === 'v2'
|
|
1125
|
+
&& memoryAttempt?.state === 'skipped';
|
|
1126
|
+
if (rejectedByV2Revalidation) return true;
|
|
1127
|
+
return Boolean(
|
|
1128
|
+
causalStop
|
|
1129
|
+
&& !causalStop.canPromoteMemory
|
|
1130
|
+
&& memoryAttempt?.state !== 'enqueued'
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
export function main({ stageMemory = stageStopMemoryAttempt } = {}) {
|
|
1095
1135
|
const input = readHookInput();
|
|
1096
1136
|
if (input.stop_hook_active) {
|
|
1097
1137
|
writeHookOutput({});
|
|
@@ -1128,15 +1168,34 @@ function main() {
|
|
|
1128
1168
|
return;
|
|
1129
1169
|
}
|
|
1130
1170
|
|
|
1131
|
-
const tx = parseTranscript(input.transcript_path || input.transcriptPath);
|
|
1132
|
-
const
|
|
1171
|
+
const tx = parseTranscript(identity.transcriptPath || input.transcript_path || input.transcriptPath);
|
|
1172
|
+
const requestedTurnId = String(input.turn_id || input.turnId || '');
|
|
1133
1173
|
const sessionId = identity.canonicalConversationId;
|
|
1134
1174
|
const finalizing = shouldFinalizeSession();
|
|
1175
|
+
const turnIdentity = resolveTurnIdentity(tx, requestedTurnId);
|
|
1176
|
+
if (!turnIdentity) {
|
|
1177
|
+
if (finalizing) {
|
|
1178
|
+
const activeId = String(entry.active_activation_id || '');
|
|
1179
|
+
const active = entry.activations?.[activeId] || {};
|
|
1180
|
+
stageMemory(vaultBase, {
|
|
1181
|
+
sessionId,
|
|
1182
|
+
activationId: activeId,
|
|
1183
|
+
activationEpoch: Number(active.epoch || entry.activation_epoch || 0),
|
|
1184
|
+
turnId: requestedTurnId || 'unresolved-turn',
|
|
1185
|
+
turnSequence: Number(entry.last_turn_sequence || 0),
|
|
1186
|
+
disposition: 'ambiguous',
|
|
1187
|
+
observedAt: new Date(0).toISOString(),
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
const message = 'wendkeep: Stop ambiguous; o turno solicitado não foi provado pelo transcript.';
|
|
1191
|
+
process.stderr.write(`[wendkeep] ${message}\n`);
|
|
1192
|
+
writeHookOutput({ systemMessage: message });
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const turnId = turnIdentity.id;
|
|
1135
1196
|
const now = finalizing ? new Date() : null;
|
|
1136
1197
|
const endedAt = finalizing ? formatLocalIso(now) : '';
|
|
1137
|
-
const stopTurnSequence =
|
|
1138
|
-
? Number(input.turn_sequence)
|
|
1139
|
-
: Number(entry.last_turn_sequence || 0);
|
|
1198
|
+
const stopTurnSequence = turnIdentity.order;
|
|
1140
1199
|
const causalStop = finalizing
|
|
1141
1200
|
? mutateSessionRegistry(vaultBase, (registry) => {
|
|
1142
1201
|
const activationId = resolveStopActivation(registry, {
|
|
@@ -1148,6 +1207,7 @@ function main() {
|
|
|
1148
1207
|
const cas = applyStopActivation(registry, {
|
|
1149
1208
|
session_id: sessionId,
|
|
1150
1209
|
activation_id: activationId,
|
|
1210
|
+
turn_id: turnId,
|
|
1151
1211
|
turn_sequence: stopTurnSequence,
|
|
1152
1212
|
ended_at: endedAt,
|
|
1153
1213
|
});
|
|
@@ -1172,8 +1232,44 @@ function main() {
|
|
|
1172
1232
|
};
|
|
1173
1233
|
})
|
|
1174
1234
|
: null;
|
|
1175
|
-
|
|
1176
|
-
|
|
1235
|
+
let memoryHandoff = null;
|
|
1236
|
+
let memoryAttempt = null;
|
|
1237
|
+
if (finalizing) {
|
|
1238
|
+
let projectId = '';
|
|
1239
|
+
try {
|
|
1240
|
+
projectId = JSON.parse(readFileSync(join(vaultBase, '.brain', 'PROJECT.json'), 'utf8')).projectId || '';
|
|
1241
|
+
} catch { /* the staging validator exposes an observable failure below */ }
|
|
1242
|
+
const finalSummary = sessionFinalSummary(tx);
|
|
1243
|
+
const memoryEvidence = collectLifecycleEvidence(vaultBase, {
|
|
1244
|
+
changeSlug: entry.change_slug,
|
|
1245
|
+
summary: finalSummary,
|
|
1246
|
+
noteRel: sessionRel,
|
|
1247
|
+
});
|
|
1248
|
+
memoryHandoff = {
|
|
1249
|
+
projectId,
|
|
1250
|
+
identity,
|
|
1251
|
+
activation: {
|
|
1252
|
+
id: causalStop?.activationId || '',
|
|
1253
|
+
epoch: Number(causalStop?.activation?.epoch || entry.activation_epoch || 0),
|
|
1254
|
+
},
|
|
1255
|
+
turn: { id: turnId, sequence: stopTurnSequence },
|
|
1256
|
+
noteRel: sessionRel,
|
|
1257
|
+
observedAt: turnIdentity.observedAt || new Date(0).toISOString(),
|
|
1258
|
+
summary: finalSummary,
|
|
1259
|
+
evidence: memoryEvidence,
|
|
1260
|
+
};
|
|
1261
|
+
memoryAttempt = stageMemory(vaultBase, {
|
|
1262
|
+
handoff: memoryHandoff,
|
|
1263
|
+
disposition: causalStop?.stopDisposition || 'ambiguous',
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
if (shouldAbortStopAfterStaging(causalStop, memoryAttempt)) {
|
|
1267
|
+
const disposition = memoryAttempt?.disposition || causalStop?.stopDisposition || 'ambiguous';
|
|
1268
|
+
if (disposition === 'duplicate' && memoryAttempt?.state === 'duplicate') {
|
|
1269
|
+
writeHookOutput({});
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const message = `wendkeep: Stop ${disposition}; uma activation mais nova foi preservada e a memória não foi promovida.`;
|
|
1177
1273
|
process.stderr.write(`[wendkeep] ${message}\n`);
|
|
1178
1274
|
writeHookOutput({ systemMessage: message });
|
|
1179
1275
|
return;
|
|
@@ -1248,40 +1344,24 @@ function main() {
|
|
|
1248
1344
|
last_logged_turn_id: turnId,
|
|
1249
1345
|
});
|
|
1250
1346
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
noteRel: sessionRel,
|
|
1270
|
-
observedAt: new Date().toISOString(),
|
|
1271
|
-
summary: finalSummary,
|
|
1272
|
-
evidence: memoryEvidence,
|
|
1273
|
-
});
|
|
1274
|
-
mutateSessionRegistry(vaultBase, (registry) => {
|
|
1275
|
-
const current = registry.sessions[sessionId];
|
|
1276
|
-
if (!current) return null;
|
|
1277
|
-
registry.sessions[sessionId] = {
|
|
1278
|
-
...current,
|
|
1279
|
-
memory_status: memoryResult.status,
|
|
1280
|
-
memory_activation_id: causalStop.activationId,
|
|
1281
|
-
...(memoryResult.checkpoint ? { memory_checkpoint: memoryResult.checkpoint } : {}),
|
|
1282
|
-
};
|
|
1283
|
-
return null;
|
|
1284
|
-
});
|
|
1347
|
+
const memoryResult = projectStopMemoryAttempt(vaultBase, memoryAttempt);
|
|
1348
|
+
if (memoryResult.status === 'legacy') {
|
|
1349
|
+
mutateSessionRegistry(vaultBase, (registry) => {
|
|
1350
|
+
const current = registry.sessions[sessionId];
|
|
1351
|
+
const active = current?.activations?.[current.active_activation_id || ''];
|
|
1352
|
+
if (!current
|
|
1353
|
+
|| current.active_activation_id !== memoryAttempt.activation_id
|
|
1354
|
+
|| Number(active?.epoch || 0) !== Number(memoryAttempt.activation_epoch || 0)) return null;
|
|
1355
|
+
registry.sessions[sessionId] = {
|
|
1356
|
+
...current,
|
|
1357
|
+
memory_status: 'legacy',
|
|
1358
|
+
memory_activation_id: memoryAttempt.activation_id,
|
|
1359
|
+
};
|
|
1360
|
+
return null;
|
|
1361
|
+
});
|
|
1362
|
+
} else {
|
|
1363
|
+
recordStopMemoryOutcome(vaultBase, memoryAttempt, memoryResult);
|
|
1364
|
+
}
|
|
1285
1365
|
|
|
1286
1366
|
// Reconstrói índice (camada fria) + digest (camada quente) ao finalizar. Nunca derruba o Stop.
|
|
1287
1367
|
try {
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from './obsidian-common.mjs';
|
|
13
13
|
import { getLocale } from './locale.mjs';
|
|
14
14
|
import { parseSharedMemory, validateMemoryEvent } from './memory-schema.mjs';
|
|
15
|
+
import { detectMemoryMode, LEGACY_MEMORY_WARNING } from './memory-mode.mjs';
|
|
15
16
|
import { reduceMemoryEvents } from './memory-store.mjs';
|
|
16
17
|
import { validateMemoryBundle } from '../src/validate-memory.mjs';
|
|
17
18
|
|
|
@@ -101,7 +102,9 @@ const MEMORY_REPAIR_COMMAND = 'wendkeep memory repair --vault <vault>';
|
|
|
101
102
|
|
|
102
103
|
function readJsonLines(path, label) {
|
|
103
104
|
if (!existsSync(path)) return { items: [], errors: [] };
|
|
104
|
-
|
|
105
|
+
let raw;
|
|
106
|
+
try { raw = readFileSync(path, 'utf8').replace(/\r\n/g, '\n'); }
|
|
107
|
+
catch (error) { return { items: [], errors: [`${label} ilegível: ${error?.message || error}`] }; }
|
|
105
108
|
const lines = raw.endsWith('\n') ? raw.split('\n').slice(0, -1) : raw.split('\n');
|
|
106
109
|
const items = [];
|
|
107
110
|
const errors = [];
|
|
@@ -118,28 +121,137 @@ function readJsonLines(path, label) {
|
|
|
118
121
|
|
|
119
122
|
function inspectOutbox(vaultBase, projectId) {
|
|
120
123
|
const dir = join(vaultBase, '.brain', 'memory-outbox');
|
|
121
|
-
if (!existsSync(dir)) return { count: 0, errors: [] };
|
|
124
|
+
if (!existsSync(dir)) return { count: 0, errors: [], eventIds: new Set() };
|
|
122
125
|
const files = readdirSync(dir).filter((name) => name.endsWith('.json')).sort();
|
|
123
126
|
const errors = [];
|
|
127
|
+
const eventIds = new Set();
|
|
124
128
|
for (const name of files) {
|
|
125
129
|
const path = join(dir, name);
|
|
126
130
|
try {
|
|
127
131
|
const event = JSON.parse(readFileSync(path, 'utf8'));
|
|
128
132
|
const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
|
|
129
133
|
if (!validation.ok) errors.push(`${name}: ${validation.errors.join(' ')}`);
|
|
134
|
+
else eventIds.add(event.event_id);
|
|
130
135
|
} catch (error) {
|
|
131
136
|
errors.push(`${name}: JSON inválido: ${error.message}`);
|
|
132
137
|
}
|
|
133
138
|
}
|
|
134
|
-
return { count: files.length, errors };
|
|
139
|
+
return { count: files.length, errors, eventIds };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function checkpointMatchesLedgerPrefix(checkpoint, eventIds, ledgerEvents) {
|
|
143
|
+
if (!checkpoint || typeof checkpoint !== 'object') return false;
|
|
144
|
+
if (!Number.isInteger(checkpoint.revision) || checkpoint.revision < 0) return false;
|
|
145
|
+
if (typeof checkpoint.event_cursor !== 'string' || !checkpoint.event_cursor) return false;
|
|
146
|
+
if (typeof checkpoint.state_hash !== 'string' || !checkpoint.state_hash) return false;
|
|
147
|
+
|
|
148
|
+
const cursorIndex = ledgerEvents.findIndex((event) => event?.event_id === checkpoint.event_cursor);
|
|
149
|
+
if (cursorIndex < 0) return false;
|
|
150
|
+
const prefix = ledgerEvents.slice(0, cursorIndex + 1);
|
|
151
|
+
const prefixIds = new Set(prefix.map((event) => event.event_id));
|
|
152
|
+
if (eventIds.some((eventId) => !prefixIds.has(eventId))) return false;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const replay = reduceMemoryEvents(prefix);
|
|
156
|
+
return checkpoint.revision === replay.revision
|
|
157
|
+
&& checkpoint.event_cursor === replay.eventCursor
|
|
158
|
+
&& checkpoint.state_hash === replay.stateHash;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function checkMemoryAttempts(registry, { ledgerEvents = [], outboxEventIds = new Set() } = {}) {
|
|
165
|
+
const failures = [];
|
|
166
|
+
const warnings = [];
|
|
167
|
+
const ledgerEventIds = new Set(ledgerEvents.map((event) => event?.event_id).filter(Boolean));
|
|
168
|
+
const attempts = Object.values(registry?.sessions || {})
|
|
169
|
+
.map((entry) => entry?.last_memory_attempt)
|
|
170
|
+
.filter((attempt) => attempt && typeof attempt === 'object' && attempt.memory_mode === 'v2');
|
|
171
|
+
|
|
172
|
+
for (const attempt of attempts) {
|
|
173
|
+
const state = String(attempt.state || '');
|
|
174
|
+
const disposition = String(attempt.disposition || '');
|
|
175
|
+
const eventIds = Array.isArray(attempt.event_ids)
|
|
176
|
+
? [...new Set(attempt.event_ids.filter((eventId) => typeof eventId === 'string' && eventId))]
|
|
177
|
+
: [];
|
|
178
|
+
|
|
179
|
+
if (state === 'skipped' && disposition === 'ambiguous') {
|
|
180
|
+
failures.push(`Lifecycle de memória v2 ambíguo: Stop pulou a publicação sem identidade causal suficiente. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (state === 'skipped' && ['stale_turn', 'superseded'].includes(disposition)) {
|
|
185
|
+
if (eventIds.length) {
|
|
186
|
+
failures.push(`Stop stale/superseded emitiu event_ids apesar da rejeição causal. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
187
|
+
} else {
|
|
188
|
+
warnings.push('Stop stale/superseded foi descartado sem publicar memória.');
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (disposition !== 'applied') {
|
|
194
|
+
if (state === 'skipped') warnings.push('Attempt de memória v2 foi descartado sem publicação.');
|
|
195
|
+
else failures.push(`Attempt de memória v2 possui disposition não reconhecida para o estado informado. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!eventIds.length) {
|
|
200
|
+
failures.push(`Attempt v2 aplicado não declarou event_ids; publicação perdida. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (state === 'enqueued' || state === 'degraded') {
|
|
205
|
+
const missing = eventIds.filter((eventId) => !ledgerEventIds.has(eventId) && !outboxEventIds.has(eventId));
|
|
206
|
+
if (missing.length) {
|
|
207
|
+
failures.push(`Attempt v2 perdeu ${missing.length} evento(s): ausentes do ledger e da outbox. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
208
|
+
} else {
|
|
209
|
+
warnings.push(`Attempt de memória v2 ${state} permanece recuperável: ${eventIds.length} evento(s) durável(is) no ledger e/ou outbox.`);
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (state === 'projected') {
|
|
215
|
+
const outsideLedger = eventIds.filter((eventId) => !ledgerEventIds.has(eventId));
|
|
216
|
+
if (outsideLedger.length) {
|
|
217
|
+
failures.push(`Attempt projetado perdeu ${outsideLedger.length} evento(s) no ledger. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
218
|
+
} else if (!checkpointMatchesLedgerPrefix(attempt.checkpoint, eventIds, ledgerEvents)) {
|
|
219
|
+
failures.push(`Checkpoint do attempt projetado diverge do prefixo rederivado do ledger. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
failures.push(`Attempt de memória v2 possui state inválido. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { failures, warnings };
|
|
135
228
|
}
|
|
136
229
|
|
|
137
230
|
/**
|
|
138
231
|
* Read-only consistency check for the local memory-v2 bundle. It intentionally
|
|
139
232
|
* does not acquire MEMORY.lock or invoke the projector/repair paths.
|
|
140
233
|
*/
|
|
141
|
-
export function checkMemoryBundle(vaultBase) {
|
|
234
|
+
export function checkMemoryBundle(vaultBase, { registry } = {}) {
|
|
142
235
|
const brain = join(vaultBase, '.brain');
|
|
236
|
+
const mode = detectMemoryMode(vaultBase);
|
|
237
|
+
if (mode.mode === 'legacy') {
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
status: 'legacy',
|
|
241
|
+
failures: [],
|
|
242
|
+
warnings: [LEGACY_MEMORY_WARNING],
|
|
243
|
+
metrics: {
|
|
244
|
+
schemaVersion: null,
|
|
245
|
+
revision: null,
|
|
246
|
+
eventCursor: null,
|
|
247
|
+
stateHash: null,
|
|
248
|
+
ledgerEvents: 0,
|
|
249
|
+
pendingOutbox: 0,
|
|
250
|
+
candidates: 0,
|
|
251
|
+
activeConflicts: 0,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
143
255
|
const bundle = validateMemoryBundle(vaultBase);
|
|
144
256
|
const failures = [];
|
|
145
257
|
const warnings = [];
|
|
@@ -185,6 +297,13 @@ export function checkMemoryBundle(vaultBase) {
|
|
|
185
297
|
}
|
|
186
298
|
}
|
|
187
299
|
|
|
300
|
+
const lifecycle = checkMemoryAttempts(registry || readSessionRegistry(vaultBase), {
|
|
301
|
+
ledgerEvents: bundle.ledger?.events || [],
|
|
302
|
+
outboxEventIds: outbox.eventIds,
|
|
303
|
+
});
|
|
304
|
+
failures.push(...lifecycle.failures);
|
|
305
|
+
warnings.push(...lifecycle.warnings);
|
|
306
|
+
|
|
188
307
|
const unresolved = candidates.items.filter((item) => !['resolved', 'rejected', 'superseded'].includes(item?.status));
|
|
189
308
|
const activeConflicts = unresolved.filter((item) => item?.reason === 'conflict');
|
|
190
309
|
const ordinaryCandidates = unresolved.filter((item) => item?.reason !== 'conflict');
|
|
@@ -310,7 +429,7 @@ export function runVaultHealth({ vaultBase, session = '' }) {
|
|
|
310
429
|
];
|
|
311
430
|
let memory = { status: 'legacy', metrics: {} };
|
|
312
431
|
if (memoryMarkers.some((path) => existsSync(path))) {
|
|
313
|
-
memory = checkMemoryBundle(vaultBase);
|
|
432
|
+
memory = checkMemoryBundle(vaultBase, { registry });
|
|
314
433
|
failures.push(...memory.failures.map((item) => `Memória: ${item}`));
|
|
315
434
|
warnings.push(...memory.warnings.map((item) => `Memória: ${item}`));
|
|
316
435
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.3",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"src",
|
|
13
13
|
"hooks",
|
|
14
14
|
"schema",
|
|
15
|
+
"docs/pt-BR/commands/*.md",
|
|
16
|
+
"docs/en/commands/*.md",
|
|
15
17
|
"README.md",
|
|
16
18
|
"README.en.md",
|
|
17
19
|
"CHANGELOG.md"
|
package/src/memory.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import {
|
|
3
|
-
copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync,
|
|
3
|
+
copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync,
|
|
4
4
|
} from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
6
|
import { sanitizeMemoryText, renderSharedMemory, validateSharedMemory } from '../hooks/memory-schema.mjs';
|
|
7
7
|
import {
|
|
8
8
|
enqueueMemoryEvent, projectMemoryOutbox, repairMemoryLedger,
|
|
@@ -75,7 +75,11 @@ export function memoryStatus(vault) {
|
|
|
75
75
|
return checkMemoryBundle(vault);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export function migrateMemory(vault, {
|
|
78
|
+
export function migrateMemory(vault, {
|
|
79
|
+
apply = false,
|
|
80
|
+
validateBundle = validateMemoryBundle,
|
|
81
|
+
publishArtifact = writeFileAtomic,
|
|
82
|
+
} = {}) {
|
|
79
83
|
projectId(vault);
|
|
80
84
|
const sharedPath = brainPath(vault, SHARED);
|
|
81
85
|
const hadShared = existsSync(sharedPath);
|
|
@@ -95,16 +99,37 @@ export function migrateMemory(vault, { apply = false, validateBundle = validateM
|
|
|
95
99
|
const sharedValidation = validateSharedMemory(emptyShared, { eventIds: new Set() });
|
|
96
100
|
if (!sharedValidation.ok) throw new Error(`Migração inválida: ${sharedValidation.errors.join(' ')}`);
|
|
97
101
|
if (backupPath && !existsSync(backupPath)) copyFileSync(sharedPath, backupPath);
|
|
102
|
+
|
|
103
|
+
// Build and validate a complete candidate vault away from the live paths. This makes
|
|
104
|
+
// the validation callback incapable of observing a half-published live bundle.
|
|
105
|
+
const stagingVault = mkdtempSync(join(dirname(vault), '.wendkeep-memory-stage-'));
|
|
106
|
+
const stagingBrain = join(stagingVault, BRAIN);
|
|
107
|
+
let stagedValidation;
|
|
108
|
+
try {
|
|
109
|
+
mkdirSync(stagingBrain, { recursive: true });
|
|
110
|
+
copyFileSync(brainPath(vault, 'CORE.md'), join(stagingBrain, 'CORE.md'));
|
|
111
|
+
copyFileSync(brainPath(vault, 'PROJECT.json'), join(stagingBrain, 'PROJECT.json'));
|
|
112
|
+
writeFileSync(join(stagingBrain, LEDGER), '', 'utf8');
|
|
113
|
+
writeFileSync(join(stagingBrain, SHARED), emptyShared, 'utf8');
|
|
114
|
+
writeFileSync(join(stagingBrain, CANDIDATES), candidateText(candidates), 'utf8');
|
|
115
|
+
stagedValidation = validateBundle(stagingVault);
|
|
116
|
+
if (!stagedValidation.ok) {
|
|
117
|
+
throw new Error(`Bundle migrado inválido: ${(stagedValidation.errors || []).join(' ')}`);
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
rmSync(stagingVault, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
const targets = [LEDGER, SHARED, CANDIDATES].map((name) => brainPath(vault, name));
|
|
99
124
|
const before = new Map(targets.map((path) => [path, {
|
|
100
125
|
existed: existsSync(path),
|
|
101
126
|
content: existsSync(path) ? readFileSync(path, 'utf8') : '',
|
|
102
127
|
}]));
|
|
103
128
|
try {
|
|
104
|
-
if (!existsSync(brainPath(vault, LEDGER)))
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const validation =
|
|
129
|
+
if (!existsSync(brainPath(vault, LEDGER))) publishArtifact(brainPath(vault, LEDGER), '');
|
|
130
|
+
publishArtifact(sharedPath, emptyShared);
|
|
131
|
+
publishArtifact(brainPath(vault, CANDIDATES), candidateText(candidates));
|
|
132
|
+
const validation = validateMemoryBundle(vault);
|
|
108
133
|
if (!validation.ok) throw new Error(`Bundle migrado inválido: ${validation.errors.join(' ')}`);
|
|
109
134
|
return { status: 'migrated', alreadyV2: false, candidates: candidates.length, backupPath, validation };
|
|
110
135
|
} catch (error) {
|