wendkeep 0.58.1 → 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.
@@ -16,6 +16,11 @@ import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-han
16
16
  import { enqueueMemoryEvent, projectMemoryOutbox } from './memory-store.mjs';
17
17
  import { detectMemoryMode } from './memory-mode.mjs';
18
18
  import { sanitizeMemoryText } from './memory-schema.mjs';
19
+ import {
20
+ projectStopMemoryAttempt,
21
+ recordStopMemoryOutcome,
22
+ stageStopMemoryAttempt,
23
+ } from './session-memory-lifecycle.mjs';
19
24
  import {
20
25
  ensureDir,
21
26
  findActiveSessionByTranscript,
@@ -527,6 +532,26 @@ export function parseTranscript(transcriptPath) {
527
532
  return parseCodexTranscript(transcriptPath);
528
533
  }
529
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
+
530
555
  function compactText(text, max = 600) {
531
556
  const clean = redactSecrets(String(text || ''))
532
557
  .replace(/\r/g, '\n')
@@ -1095,7 +1120,18 @@ function pingObsidianVault(apiKey) {
1095
1120
  } catch {}
1096
1121
  }
1097
1122
 
1098
- function main() {
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 } = {}) {
1099
1135
  const input = readHookInput();
1100
1136
  if (input.stop_hook_active) {
1101
1137
  writeHookOutput({});
@@ -1132,15 +1168,34 @@ function main() {
1132
1168
  return;
1133
1169
  }
1134
1170
 
1135
- const tx = parseTranscript(input.transcript_path || input.transcriptPath);
1136
- const turnId = input.turn_id || tx.latestTurnId || String(Date.now());
1171
+ const tx = parseTranscript(identity.transcriptPath || input.transcript_path || input.transcriptPath);
1172
+ const requestedTurnId = String(input.turn_id || input.turnId || '');
1137
1173
  const sessionId = identity.canonicalConversationId;
1138
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;
1139
1196
  const now = finalizing ? new Date() : null;
1140
1197
  const endedAt = finalizing ? formatLocalIso(now) : '';
1141
- const stopTurnSequence = Number.isSafeInteger(Number(input.turn_sequence))
1142
- ? Number(input.turn_sequence)
1143
- : Number(entry.last_turn_sequence || 0);
1198
+ const stopTurnSequence = turnIdentity.order;
1144
1199
  const causalStop = finalizing
1145
1200
  ? mutateSessionRegistry(vaultBase, (registry) => {
1146
1201
  const activationId = resolveStopActivation(registry, {
@@ -1152,6 +1207,7 @@ function main() {
1152
1207
  const cas = applyStopActivation(registry, {
1153
1208
  session_id: sessionId,
1154
1209
  activation_id: activationId,
1210
+ turn_id: turnId,
1155
1211
  turn_sequence: stopTurnSequence,
1156
1212
  ended_at: endedAt,
1157
1213
  });
@@ -1176,8 +1232,44 @@ function main() {
1176
1232
  };
1177
1233
  })
1178
1234
  : null;
1179
- if (causalStop && !causalStop.canPromoteMemory) {
1180
- const message = `wendkeep: Stop ${causalStop.stopDisposition}; uma activation mais nova foi preservada e a memória não foi promovida.`;
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.`;
1181
1273
  process.stderr.write(`[wendkeep] ${message}\n`);
1182
1274
  writeHookOutput({ systemMessage: message });
1183
1275
  return;
@@ -1252,40 +1344,24 @@ function main() {
1252
1344
  last_logged_turn_id: turnId,
1253
1345
  });
1254
1346
 
1255
- let projectId = '';
1256
- try {
1257
- projectId = JSON.parse(readFileSync(join(vaultBase, '.brain', 'PROJECT.json'), 'utf8')).projectId || '';
1258
- } catch { /* store validator reports a degraded handoff below */ }
1259
- const finalSummary = sessionFinalSummary(tx);
1260
- const memoryEvidence = collectLifecycleEvidence(vaultBase, {
1261
- changeSlug: entry.change_slug,
1262
- summary: finalSummary,
1263
- noteRel: sessionRel,
1264
- });
1265
- const memoryResult = commitSessionMemory(vaultBase, {
1266
- projectId,
1267
- identity,
1268
- activation: {
1269
- id: causalStop.activationId,
1270
- epoch: Number(causalStop.activation?.epoch || entry.activation_epoch || 0),
1271
- },
1272
- turn: { id: turnId, sequence: stopTurnSequence },
1273
- noteRel: sessionRel,
1274
- observedAt: new Date().toISOString(),
1275
- summary: finalSummary,
1276
- evidence: memoryEvidence,
1277
- });
1278
- mutateSessionRegistry(vaultBase, (registry) => {
1279
- const current = registry.sessions[sessionId];
1280
- if (!current) return null;
1281
- registry.sessions[sessionId] = {
1282
- ...current,
1283
- memory_status: memoryResult.status,
1284
- memory_activation_id: causalStop.activationId,
1285
- ...(memoryResult.checkpoint ? { memory_checkpoint: memoryResult.checkpoint } : {}),
1286
- };
1287
- return null;
1288
- });
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
+ }
1289
1365
 
1290
1366
  // Reconstrói índice (camada fria) + digest (camada quente) ao finalizar. Nunca derruba o Stop.
1291
1367
  try {
@@ -121,27 +121,117 @@ function readJsonLines(path, label) {
121
121
 
122
122
  function inspectOutbox(vaultBase, projectId) {
123
123
  const dir = join(vaultBase, '.brain', 'memory-outbox');
124
- if (!existsSync(dir)) return { count: 0, errors: [] };
124
+ if (!existsSync(dir)) return { count: 0, errors: [], eventIds: new Set() };
125
125
  const files = readdirSync(dir).filter((name) => name.endsWith('.json')).sort();
126
126
  const errors = [];
127
+ const eventIds = new Set();
127
128
  for (const name of files) {
128
129
  const path = join(dir, name);
129
130
  try {
130
131
  const event = JSON.parse(readFileSync(path, 'utf8'));
131
132
  const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
132
133
  if (!validation.ok) errors.push(`${name}: ${validation.errors.join(' ')}`);
134
+ else eventIds.add(event.event_id);
133
135
  } catch (error) {
134
136
  errors.push(`${name}: JSON inválido: ${error.message}`);
135
137
  }
136
138
  }
137
- 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 };
138
228
  }
139
229
 
140
230
  /**
141
231
  * Read-only consistency check for the local memory-v2 bundle. It intentionally
142
232
  * does not acquire MEMORY.lock or invoke the projector/repair paths.
143
233
  */
144
- export function checkMemoryBundle(vaultBase) {
234
+ export function checkMemoryBundle(vaultBase, { registry } = {}) {
145
235
  const brain = join(vaultBase, '.brain');
146
236
  const mode = detectMemoryMode(vaultBase);
147
237
  if (mode.mode === 'legacy') {
@@ -207,6 +297,13 @@ export function checkMemoryBundle(vaultBase) {
207
297
  }
208
298
  }
209
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
+
210
307
  const unresolved = candidates.items.filter((item) => !['resolved', 'rejected', 'superseded'].includes(item?.status));
211
308
  const activeConflicts = unresolved.filter((item) => item?.reason === 'conflict');
212
309
  const ordinaryCandidates = unresolved.filter((item) => item?.reason !== 'conflict');
@@ -332,7 +429,7 @@ export function runVaultHealth({ vaultBase, session = '' }) {
332
429
  ];
333
430
  let memory = { status: 'legacy', metrics: {} };
334
431
  if (memoryMarkers.some((path) => existsSync(path))) {
335
- memory = checkMemoryBundle(vaultBase);
432
+ memory = checkMemoryBundle(vaultBase, { registry });
336
433
  failures.push(...memory.failures.map((item) => `Memória: ${item}`));
337
434
  warnings.push(...memory.warnings.map((item) => `Memória: ${item}`));
338
435
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.58.1",
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/taxonomy.mjs CHANGED
@@ -37,6 +37,7 @@ export const HOOK_FILES = [
37
37
  'memory-schema.mjs',
38
38
  'memory-store.mjs',
39
39
  'memory-handoff.mjs',
40
+ 'session-memory-lifecycle.mjs',
40
41
  'change-core.mjs',
41
42
  'spec-core.mjs',
42
43
  'sensors-core.mjs',