wendkeep 0.66.3 → 0.66.5

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.
@@ -510,16 +510,19 @@ function detectTranscriptFormat(lines) {
510
510
  return 'codex';
511
511
  }
512
512
 
513
- export function parseTokenUsageFromTranscript(transcriptPath) {
513
+ export function parseTokenUsageFromContent(content, { transcriptPath = '' } = {}) {
514
514
  const result = emptyParseResult(transcriptPath);
515
- if (!transcriptPath || !existsSync(transcriptPath)) return result;
516
-
517
- const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
515
+ const lines = String(content || '').split('\n').filter(Boolean);
518
516
  return detectTranscriptFormat(lines) === 'claude'
519
517
  ? parseClaudeLines(lines, result)
520
518
  : parseCodexLines(lines, result);
521
519
  }
522
520
 
521
+ export function parseTokenUsageFromTranscript(transcriptPath) {
522
+ if (!transcriptPath || !existsSync(transcriptPath)) return emptyParseResult(transcriptPath);
523
+ return parseTokenUsageFromContent(readFileSync(transcriptPath, 'utf-8'), { transcriptPath });
524
+ }
525
+
523
526
  function modelCost(usage, model) {
524
527
  const normalized = normalizeModelName(model);
525
528
  const price = PRICE_REFERENCE[normalized];
@@ -935,6 +938,80 @@ export function collectSessionUsage({ sessionContent, transcriptPath }) {
935
938
  };
936
939
  }
937
940
 
941
+ function normalizedTranscriptKey(value) {
942
+ return String(value || '').replace(/\.jsonl?$/i, '').trim().toLowerCase();
943
+ }
944
+
945
+ // Rebuild the main bucket from every validated top-level rollout. Existing history is used
946
+ // only to preserve stable timestamps; an entry that is neither a known root nor a proven
947
+ // descendant makes the reconstruction fail closed instead of silently deleting a legitimate
948
+ // reopening.
949
+ export function collectSessionUsageForRoots({
950
+ sessionContent,
951
+ rootPaths = [],
952
+ descendantIds = [],
953
+ } = {}) {
954
+ const fmMatch = String(sessionContent || '').match(/^---\n([\s\S]*?)\n---/);
955
+ if (!fmMatch) return null;
956
+
957
+ const roots = [...new Set(rootPaths.filter(Boolean))].sort((a, b) => String(a).localeCompare(String(b)));
958
+ const rootKeys = new Set(roots.map((path) => normalizedTranscriptKey(transcriptIdFromPath(path))));
959
+ const descendantKeys = new Set(descendantIds.map(normalizedTranscriptKey).filter(Boolean));
960
+ const existingEntries = parseUsageHistory(fmMatch[1]);
961
+ const unresolved = existingEntries.filter((entry) => {
962
+ const key = normalizedTranscriptKey(entry.transcript_id);
963
+ return key && !rootKeys.has(key) && !descendantKeys.has(key);
964
+ });
965
+ if (unresolved.length) {
966
+ return {
967
+ state: 'degraded',
968
+ diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: unresolved.length }],
969
+ entries: existingEntries,
970
+ content: sessionContent,
971
+ };
972
+ }
973
+
974
+ const entries = [];
975
+ const summaries = [];
976
+ for (const transcriptPath of roots) {
977
+ if (!existsSync(transcriptPath)) {
978
+ return {
979
+ state: 'degraded',
980
+ diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
981
+ entries: existingEntries,
982
+ content: sessionContent,
983
+ };
984
+ }
985
+ const summary = summarizeTokenUsage(parseTokenUsageFromTranscript(transcriptPath));
986
+ if (!summary.calls) {
987
+ return {
988
+ state: 'degraded',
989
+ diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
990
+ entries: existingEntries,
991
+ content: sessionContent,
992
+ };
993
+ }
994
+ const transcriptId = transcriptIdFromPath(transcriptPath);
995
+ const current = entryFromSummary(summary, transcriptId);
996
+ const previous = existingEntries.find((entry) => normalizedTranscriptKey(entry.transcript_id) === normalizedTranscriptKey(transcriptId));
997
+ if (previous && sameUsageData(previous, current)) current.atualizado_em = previous.atualizado_em;
998
+ entries.push(current);
999
+ summaries.push(summary);
1000
+ }
1001
+
1002
+ const agg = aggregateEntries(entries);
1003
+ const content = upsertSessionFrontmatter(sessionContent, agg, entries);
1004
+ if (content === null) return null;
1005
+ return {
1006
+ state: 'complete',
1007
+ diagnostics: [],
1008
+ summaries,
1009
+ aggregate: agg,
1010
+ entries,
1011
+ content,
1012
+ };
1013
+ }
1014
+
938
1015
  export function updateSessionUsage({ vaultBase, sessionRel, sessionPath, transcriptPath, lockTimeoutMs }) {
939
1016
  if (!sessionPath || !existsSync(sessionPath)) return null;
940
1017
  let result = null;
@@ -338,6 +338,12 @@ function checkMemoryAttempts(registry, {
338
338
  const missing = eventIds.filter((eventId) => !ledgerEventIds.has(eventId) && !outboxEventIds.has(eventId));
339
339
  if (missing.length) {
340
340
  failures.push(`Attempt v2 perdeu ${missing.length} evento(s): ausentes do ledger e da outbox. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
341
+ } else if (
342
+ state === 'enqueued'
343
+ && eventIds.every((eventId) => ledgerEventIds.has(eventId))
344
+ && eventIds.every((eventId) => !outboxEventIds.has(eventId))
345
+ ) {
346
+ warnings.push(`Attempt de memória v2 possui acknowledgement projetado pendente. Recupere com: wendkeep memory recover-attempt ${sessionId}.`);
341
347
  } else {
342
348
  warnings.push(`Attempt de memória v2 ${state} permanece recuperável: ${eventIds.length} evento(s) durável(is) no ledger e/ou outbox.`);
343
349
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.66.3",
3
+ "version": "0.66.5",
4
4
  "description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -100,6 +100,7 @@ Usage:
100
100
  behind the closing block. Dry-run by default · --apply · --json.
101
101
  wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
102
102
  wendkeep memory <sub> Shared memory v2: status | migrate [--apply] | repair |
103
+ recover-attempt <session> [--apply] |
103
104
  reconcile <session> --by-session <session> --reason <text> [--apply] |
104
105
  promote <candidate> [--event <event-id>] | reject <candidate>. --vault P.
105
106
  Reconcile is dry-run by default; the original attempt remains audited.
@@ -313,6 +313,8 @@ export function readMemoryLedger(vaultBase) {
313
313
  if (eventIds.has(parsed.event_id)) {
314
314
  if (eventPayloads.get(parsed.event_id) !== payload) {
315
315
  errors.push(ledgerError(lineNumber, `event_id collision: ${parsed.event_id}`, partial));
316
+ } else {
317
+ errors.push(ledgerError(lineNumber, `duplicate event_id: ${parsed.event_id}`, partial));
316
318
  }
317
319
  return;
318
320
  }
@@ -911,15 +913,18 @@ function projectLocked(vaultBase, { faultAt } = {}) {
911
913
  const projection = publishMemoryProjection(vaultBase, prepared);
912
914
  injectFault(faultAt, 'after-projection');
913
915
 
916
+ const consumedEventIds = [];
914
917
  for (const entry of outbox) {
915
918
  unlinkVaultFile(vaultBase, entry.path, {
916
919
  missingOk: false, label: 'evento consumido do outbox de memória',
917
920
  });
921
+ consumedEventIds.push(entry.event.event_id);
918
922
  }
919
923
  return {
920
924
  status: 'projected',
921
925
  appended: newEvents.length,
922
926
  consumed: outbox.length,
927
+ consumedEventIds,
923
928
  pending: 0,
924
929
  ...projection,
925
930
  };
package/src/cost.mjs CHANGED
@@ -196,19 +196,53 @@ function opt(argv, name) {
196
196
  return eq ? eq.slice(name.length + 1) : undefined;
197
197
  }
198
198
 
199
+ const REBUILD_LIMIT_FLAGS = [
200
+ ['--max-graph-nodes', 'maxGraphNodes'],
201
+ ['--max-fallback-days', 'maxFallbackDays'],
202
+ ['--max-fallback-candidates', 'maxFallbackCandidates'],
203
+ ];
204
+
205
+ export function parseRebuildOptions(argv = []) {
206
+ const session = opt(argv, '--session') || '';
207
+ const overrides = {};
208
+ for (const [flag, key] of REBUILD_LIMIT_FLAGS) {
209
+ const present = argv.includes(flag) || argv.some((arg) => arg.startsWith(`${flag}=`));
210
+ if (!present) continue;
211
+ const value = Number(opt(argv, flag));
212
+ if (!Number.isSafeInteger(value) || value <= 0) {
213
+ return { ok: false, exitCode: 2, code: 'INVALID_LIMIT_OVERRIDE' };
214
+ }
215
+ overrides[key] = value;
216
+ }
217
+ if (Object.keys(overrides).length > 0 && !session) {
218
+ return { ok: false, exitCode: 2, code: 'TARGET_REQUIRED_FOR_LIMIT_OVERRIDE' };
219
+ }
220
+ return {
221
+ ok: true,
222
+ options: {
223
+ apply: argv.includes('--apply'),
224
+ session,
225
+ limit: Number(opt(argv, '--limit')) || 0,
226
+ limits: { ...overrides },
227
+ overrides: { ...overrides },
228
+ },
229
+ };
230
+ }
231
+
199
232
  export function runCost(argv) {
200
233
  const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
201
234
  if (!vaultRaw) { process.stderr.write('wendkeep cost: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
202
235
  const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
203
236
  if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep cost: vault not found: ${vaultBase}\n`); process.exit(2); }
204
237
  if (argv[0] === 'rebuild') {
205
- const report = rebuildSessionCosts(vaultBase, {
206
- apply: argv.includes('--apply'),
207
- session: opt(argv, '--session') || '',
208
- limit: Number(opt(argv, '--limit')) || 0,
209
- });
238
+ const parsed = parseRebuildOptions(argv);
239
+ if (!parsed.ok) {
240
+ process.stderr.write(`wendkeep cost rebuild: ${parsed.code}\n`);
241
+ process.exit(parsed.exitCode);
242
+ }
243
+ const report = rebuildSessionCosts(vaultBase, parsed.options);
210
244
  if (argv.includes('--json')) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
211
- else process.stdout.write(`cost rebuild (${report.mode}): ${report.scanned} lidas · ${report.changed} alteradas · ${report.unchanged} iguais · ${report.missing.length} sem fonte · ${report.errors.length} erros\n${report.mode === 'apply' ? 'Relatório: .brain/COST_REBUILD.json\n' : 'Nenhum arquivo foi alterado; use --apply para gravar.\n'}`);
245
+ else process.stdout.write(`cost rebuild (${report.mode}): ${report.scanned} lidas · ${report.changed} alteradas · ${report.unchanged} iguais · ${report.degraded} degradadas · ${report.stale} stale · ${report.missing} sem fonte · ${report.errors} erros\n${report.mode === 'apply' ? 'Relatório: .brain/COST_REBUILD.json\n' : 'Nenhum arquivo foi alterado; use --apply para gravar.\n'}`);
212
246
  process.exit(report.ok ? 0 : 1);
213
247
  }
214
248
  const agg = collectVaultCost(vaultBase, { since: opt(argv, '--since') });
package/src/doctor.mjs CHANGED
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process';
4
4
  import { existsSync } from 'node:fs';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines, checkStaleDerivedSections, renderStaleDerivedSectionLines } from '../hooks/harness-doctor.mjs';
7
+ import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines, checkStaleDerivedSections, renderStaleDerivedSectionLines, checkSessionObservability, renderSessionObservabilityLines } from '../hooks/harness-doctor.mjs';
8
8
  import { checkSyncDefs } from './sync-defs.mjs';
9
9
  import { resolveProjectVault } from './project-vault.mjs';
10
10
 
@@ -79,6 +79,9 @@ export function runDoctor(argv) {
79
79
  // 3d. Seções derivadas do corpo que ficaram para trás do Encerramento (notas pré-0.53.0).
80
80
  process.stdout.write(`\n${renderStaleDerivedSectionLines(checkStaleDerivedSections(vaultBase)).join('\n')}\n`);
81
81
 
82
+ // 3e. Observabilidade materializada: schema vigente não basta sem frontier + manifest frescos.
83
+ process.stdout.write(`\n${renderSessionObservabilityLines(checkSessionObservability(vaultBase)).join('\n')}\n`);
84
+
82
85
  // 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
83
86
  const act = checkSessionActivity(vaultBase);
84
87
  if (act.lastSession) {