wendkeep 0.73.0 → 0.74.0
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 +31 -0
- package/README.en.md +12 -8
- package/README.md +12 -8
- package/docs/en/commands/memory.md +16 -1
- package/docs/en/commands/observer.md +8 -1
- package/docs/en/commands/operating-profiles.md +1 -1
- package/docs/pt-BR/commands/memory.md +16 -1
- package/docs/pt-BR/commands/observer.md +7 -1
- package/docs/pt-BR/commands/operating-profiles.md +1 -1
- package/hooks/brain-core.mjs +2 -0
- package/hooks/brain-recall.mjs +5 -1
- package/hooks/evidence-context.mjs +41 -0
- package/hooks/evidence-recall.mjs +1 -0
- package/hooks/memory-scope.mjs +1 -0
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +1 -1
- package/packages/integrations/src/host-hooks.mjs +1 -0
- package/packages/vault/src/evidence-recall.mjs +343 -0
- package/packages/vault/src/index.mjs +2 -0
- package/packages/vault/src/memory-handoff.mjs +58 -3
- package/packages/vault/src/memory-schema.mjs +12 -2
- package/packages/vault/src/memory-scope.mjs +119 -0
- package/packages/vault/src/memory-store.mjs +86 -24
- package/schema/observer/004-evidence-recall.sql +25 -0
- package/src/memory.mjs +95 -2
- package/src/observer-sql-store.mjs +141 -5
- package/src/taxonomy.mjs +4 -0
|
@@ -14,6 +14,12 @@ import {
|
|
|
14
14
|
sanitizeMemoryText,
|
|
15
15
|
validateMemoryEvent,
|
|
16
16
|
} from './memory-schema.mjs';
|
|
17
|
+
import {
|
|
18
|
+
effectiveMemoryScope,
|
|
19
|
+
isRegisterMemoryKey,
|
|
20
|
+
memoryRecordKey,
|
|
21
|
+
sameMemoryScope,
|
|
22
|
+
} from './memory-scope.mjs';
|
|
17
23
|
import {
|
|
18
24
|
assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, unlinkVaultFile,
|
|
19
25
|
VAULT_LOCK_BUSY, withVaultPathLock, writeVaultFileAtomic,
|
|
@@ -376,6 +382,7 @@ function sameCompleteCausalLineage(left, right) {
|
|
|
376
382
|
}
|
|
377
383
|
|
|
378
384
|
function comparable(left, right) {
|
|
385
|
+
if (left?.project_id !== right?.project_id || !sameMemoryScope(left, right)) return false;
|
|
379
386
|
if (sameCausalActivation(left, right)) return true;
|
|
380
387
|
const leftSupersedes = left.supersedes_event_id || left.supersedes;
|
|
381
388
|
const rightSupersedes = right.supersedes_event_id || right.supersedes;
|
|
@@ -387,7 +394,7 @@ function comparable(left, right) {
|
|
|
387
394
|
function conflictGroupKey(event) {
|
|
388
395
|
if (event.operation !== 'replace') return null;
|
|
389
396
|
if (!Number.isInteger(event.base_revision) || typeof event.base_value_hash !== 'string') return null;
|
|
390
|
-
return `${event
|
|
397
|
+
return `${memoryRecordKey(event)}\u0000${event.base_revision}\u0000${event.base_value_hash}`;
|
|
391
398
|
}
|
|
392
399
|
|
|
393
400
|
function candidateId(reason, memoryKey, eventIds) {
|
|
@@ -421,6 +428,7 @@ function blockedByCoreCandidate(event, coreValue) {
|
|
|
421
428
|
reason: 'blocked_by_core',
|
|
422
429
|
status: 'blocked_by_core',
|
|
423
430
|
memory_key: event.memory_key,
|
|
431
|
+
...(event.scope ? { scope: effectiveMemoryScope(event), record_key: memoryRecordKey(event) } : {}),
|
|
424
432
|
event_ids: [event.event_id],
|
|
425
433
|
proposed_value: event.value,
|
|
426
434
|
core_value: coreValue,
|
|
@@ -442,6 +450,10 @@ function conflictCandidate(memoryKey, events, currentEvent = null) {
|
|
|
442
450
|
candidate_id: candidateId('conflict', memoryKey, eventIds),
|
|
443
451
|
reason: 'conflict',
|
|
444
452
|
memory_key: memoryKey,
|
|
453
|
+
...((ordered[0] || currentEvent)?.scope ? {
|
|
454
|
+
scope: effectiveMemoryScope(ordered[0] || currentEvent || {}),
|
|
455
|
+
record_key: memoryRecordKey(ordered[0] || currentEvent || { memory_key: memoryKey }),
|
|
456
|
+
} : {}),
|
|
445
457
|
event_ids: eventIds,
|
|
446
458
|
values: eventIds.map((id) => byId.get(id).value),
|
|
447
459
|
base_revision: ordered[0]?.base_revision ?? currentEvent?.revision ?? 0,
|
|
@@ -459,6 +471,7 @@ function conflictReviewEvent(candidate, candidateCount = 1) {
|
|
|
459
471
|
event_id: `mem-review-${candidate.candidate_id}`,
|
|
460
472
|
project_id: source.project_id || '',
|
|
461
473
|
memory_key: candidate.memory_key,
|
|
474
|
+
scope: candidate.scope,
|
|
462
475
|
operation: 'assert',
|
|
463
476
|
value: `[revisão pendente: ${memoryKey}; candidates: ${candidateCount}; events: ${eventCount}]`,
|
|
464
477
|
authority: 'candidate',
|
|
@@ -508,7 +521,9 @@ function isCausallyOlder(event, current) {
|
|
|
508
521
|
if (sameCausalActivation(event, current)) {
|
|
509
522
|
return Number(event.turn_sequence) < Number(current.turn_sequence);
|
|
510
523
|
}
|
|
511
|
-
if (
|
|
524
|
+
if (event.canonical_session_id && event.canonical_session_id === current.canonical_session_id
|
|
525
|
+
&& sameMemoryScope(event, current)
|
|
526
|
+
&& Number.isInteger(event.activation_epoch) && Number.isInteger(current.activation_epoch)
|
|
512
527
|
&& event.activation_epoch !== current.activation_epoch) {
|
|
513
528
|
return event.activation_epoch < current.activation_epoch;
|
|
514
529
|
}
|
|
@@ -518,6 +533,30 @@ function isCausallyOlder(event, current) {
|
|
|
518
533
|
&& eventEffective < currentEffective;
|
|
519
534
|
}
|
|
520
535
|
|
|
536
|
+
const AUTHORITY_RANK = Object.freeze({ candidate: 0, reported: 1, verified: 2 });
|
|
537
|
+
|
|
538
|
+
function sameRegisterLineage(left, right) {
|
|
539
|
+
return Boolean(left?.canonical_session_id)
|
|
540
|
+
&& left.canonical_session_id === right?.canonical_session_id
|
|
541
|
+
&& left.project_id === right?.project_id
|
|
542
|
+
&& sameMemoryScope(left, right);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Positive means `incoming` is a safe successor; null means human comparison is required. */
|
|
546
|
+
function registerPrecedence(incoming, current) {
|
|
547
|
+
if (!incoming?.scope || !current?.scope
|
|
548
|
+
|| !isRegisterMemoryKey(incoming?.memory_key) || !sameRegisterLineage(incoming, current)) return null;
|
|
549
|
+
const epoch = Number(incoming.activation_epoch ?? -1) - Number(current.activation_epoch ?? -1);
|
|
550
|
+
if (epoch) return epoch;
|
|
551
|
+
const turn = Number(incoming.turn_sequence ?? -1) - Number(current.turn_sequence ?? -1);
|
|
552
|
+
if (turn) return turn;
|
|
553
|
+
const authority = (AUTHORITY_RANK[incoming.authority] ?? -1) - (AUTHORITY_RANK[current.authority] ?? -1);
|
|
554
|
+
if (authority) return authority;
|
|
555
|
+
const observed = String(incoming.observed_at || '').localeCompare(String(current.observed_at || ''));
|
|
556
|
+
if (observed) return observed;
|
|
557
|
+
return String(incoming.event_id || '').localeCompare(String(current.event_id || ''));
|
|
558
|
+
}
|
|
559
|
+
|
|
521
560
|
/**
|
|
522
561
|
* Pure deterministic reducer. It pre-detects incomparable scalar siblings so replay order
|
|
523
562
|
* never turns one concurrent writer into an accidental winner.
|
|
@@ -537,7 +576,21 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
537
576
|
}
|
|
538
577
|
if (!existing) unique.set(event.event_id, event);
|
|
539
578
|
}
|
|
540
|
-
const
|
|
579
|
+
const rescopeTargets = new Set(
|
|
580
|
+
[...unique.values()].flatMap((item) => [
|
|
581
|
+
item.rescopes_event_id,
|
|
582
|
+
...(Array.isArray(item.rescopes_event_ids) ? item.rescopes_event_ids : []),
|
|
583
|
+
]).filter(Boolean),
|
|
584
|
+
);
|
|
585
|
+
const projectIds = new Set([...unique.values()].map((item) => item.project_id).filter(Boolean));
|
|
586
|
+
if (projectIds.size > 1) {
|
|
587
|
+
const error = new TypeError('Memory reducer cannot compare events from different projects.');
|
|
588
|
+
error.code = 'MEMORY_PROJECT_MIXED';
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
const events = [...unique.values()]
|
|
592
|
+
.filter((item) => !rescopeTargets.has(item.event_id))
|
|
593
|
+
.sort(eventOrder);
|
|
541
594
|
const candidateDecisions = new Map();
|
|
542
595
|
for (const item of events) {
|
|
543
596
|
const decision = item.candidate_decision;
|
|
@@ -604,7 +657,8 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
604
657
|
continue;
|
|
605
658
|
}
|
|
606
659
|
|
|
607
|
-
const
|
|
660
|
+
const recordKey = memoryRecordKey(item);
|
|
661
|
+
const current = records.get(recordKey);
|
|
608
662
|
const currentSource = current?.source;
|
|
609
663
|
if (isCausallyOlder(item, currentSource)) {
|
|
610
664
|
superseded.push({ event_id: item.event_id, by_event_id: currentSource.event_id });
|
|
@@ -613,10 +667,12 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
613
667
|
|
|
614
668
|
if (item.operation === 'assert') {
|
|
615
669
|
if (current && hashMemoryValue(current.value) !== hashMemoryValue(item.value)) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
670
|
+
const precedence = registerPrecedence(item, currentSource);
|
|
671
|
+
if ((sameCausalActivation(item, currentSource)
|
|
672
|
+
&& Number(item.turn_sequence) > Number(currentSource.turn_sequence))
|
|
673
|
+
|| precedence > 0) {
|
|
674
|
+
records.set(recordKey, { value: item.value, revision: current.revision + 1, source: item });
|
|
675
|
+
tombstones.delete(recordKey);
|
|
620
676
|
superseded.push({ event_id: currentSource.event_id, by_event_id: item.event_id });
|
|
621
677
|
revision += 1;
|
|
622
678
|
appliedEventIds.push(item.event_id);
|
|
@@ -628,8 +684,8 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
628
684
|
continue;
|
|
629
685
|
}
|
|
630
686
|
if (!current) {
|
|
631
|
-
records.set(
|
|
632
|
-
tombstones.delete(
|
|
687
|
+
records.set(recordKey, { value: item.value, revision: 1, source: item });
|
|
688
|
+
tombstones.delete(recordKey);
|
|
633
689
|
revision += 1;
|
|
634
690
|
}
|
|
635
691
|
appliedEventIds.push(item.event_id);
|
|
@@ -642,8 +698,8 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
642
698
|
const byHash = new Map(oldValues.map((value) => [hashMemoryValue(value), value]));
|
|
643
699
|
additions.forEach((value) => byHash.set(hashMemoryValue(value), value));
|
|
644
700
|
const value = [...byHash.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([, entry]) => entry);
|
|
645
|
-
records.set(
|
|
646
|
-
tombstones.delete(
|
|
701
|
+
records.set(recordKey, { value, revision: (current?.revision || 0) + 1, source: item });
|
|
702
|
+
tombstones.delete(recordKey);
|
|
647
703
|
revision += 1;
|
|
648
704
|
appliedEventIds.push(item.event_id);
|
|
649
705
|
continue;
|
|
@@ -664,13 +720,13 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
664
720
|
if (item.value !== null && item.value !== undefined && Array.isArray(current.value)) {
|
|
665
721
|
const removalHash = hashMemoryValue(item.value);
|
|
666
722
|
const value = current.value.filter((entry) => hashMemoryValue(entry) !== removalHash);
|
|
667
|
-
records.set(
|
|
668
|
-
tombstones.set(`${
|
|
723
|
+
records.set(recordKey, { value, revision: current.revision + 1, source: item });
|
|
724
|
+
tombstones.set(`${recordKey}:${removalHash}`, {
|
|
669
725
|
event_id: item.event_id, removed_event_id: current.source.event_id, value_hash: removalHash,
|
|
670
726
|
});
|
|
671
727
|
} else {
|
|
672
|
-
records.delete(
|
|
673
|
-
tombstones.set(
|
|
728
|
+
records.delete(recordKey);
|
|
729
|
+
tombstones.set(recordKey, {
|
|
674
730
|
event_id: item.event_id,
|
|
675
731
|
removed_event_id: current.source.event_id,
|
|
676
732
|
value_hash: hashMemoryValue(current.value),
|
|
@@ -691,8 +747,8 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
691
747
|
candidates.push(conflictCandidate(item.memory_key, current ? [currentEventFromRecord(current), item] : [item]));
|
|
692
748
|
continue;
|
|
693
749
|
}
|
|
694
|
-
records.set(
|
|
695
|
-
tombstones.delete(
|
|
750
|
+
records.set(recordKey, { value: item.value, revision: current.revision + 1, source: item });
|
|
751
|
+
tombstones.delete(recordKey);
|
|
696
752
|
const explicitlySupersededIds = new Set(
|
|
697
753
|
Array.isArray(item.supersedes)
|
|
698
754
|
? item.supersedes
|
|
@@ -732,16 +788,16 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
732
788
|
advanced = false;
|
|
733
789
|
for (const pending of deferredAsserts) {
|
|
734
790
|
if (resolvedCandidateIds.has(pending.candidate.candidate_id)) continue;
|
|
735
|
-
const current = records.get(pending.event
|
|
791
|
+
const current = records.get(memoryRecordKey(pending.event));
|
|
736
792
|
const currentSource = current?.source;
|
|
737
793
|
if (!sameCompleteCausalLineage(pending.event, currentSource)) continue;
|
|
738
794
|
if (pending.event.turn_sequence > currentSource.turn_sequence) {
|
|
739
|
-
records.set(pending.event
|
|
795
|
+
records.set(memoryRecordKey(pending.event), {
|
|
740
796
|
value: pending.event.value,
|
|
741
797
|
revision: current.revision + 1,
|
|
742
798
|
source: pending.event,
|
|
743
799
|
});
|
|
744
|
-
tombstones.delete(pending.event
|
|
800
|
+
tombstones.delete(memoryRecordKey(pending.event));
|
|
745
801
|
superseded.push({ event_id: currentSource.event_id, by_event_id: pending.event.event_id });
|
|
746
802
|
revision += 1;
|
|
747
803
|
appliedEventIds.push(pending.event.event_id);
|
|
@@ -762,7 +818,7 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
762
818
|
.map((candidate) => {
|
|
763
819
|
const pending = pendingByCandidateId.get(candidate.candidate_id);
|
|
764
820
|
if (!pending) return candidate;
|
|
765
|
-
const finalSource = currentEventFromRecord(records.get(candidate.memory_key));
|
|
821
|
+
const finalSource = currentEventFromRecord(records.get(candidate.record_key || candidate.memory_key));
|
|
766
822
|
const previousSource = candidate.events?.find(
|
|
767
823
|
(event) => event.event_id !== pending.event.event_id,
|
|
768
824
|
);
|
|
@@ -791,10 +847,16 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
791
847
|
superseded.sort((left, right) => left.event_id.localeCompare(right.event_id));
|
|
792
848
|
const eventCursor = events.at(-1)?.event_id || 'none';
|
|
793
849
|
const stateHash = hashMemoryValue({ state, tombstones: tombstoneObject });
|
|
850
|
+
const ambiguousRecordKeys = new Set(
|
|
851
|
+
unresolvedCandidates.map((candidate) => candidate.record_key || candidate.memory_key),
|
|
852
|
+
);
|
|
794
853
|
const activeEvents = [
|
|
795
|
-
...Object.entries(recordObject)
|
|
854
|
+
...Object.entries(recordObject)
|
|
855
|
+
.filter(([recordKey]) => !ambiguousRecordKeys.has(recordKey))
|
|
856
|
+
.map(([recordKey, record]) => ({
|
|
796
857
|
...record.source,
|
|
797
|
-
memory_key:
|
|
858
|
+
memory_key: record.source.memory_key,
|
|
859
|
+
projection_key: recordKey,
|
|
798
860
|
operation: 'assert',
|
|
799
861
|
value: record.value,
|
|
800
862
|
})),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS document_chunks (
|
|
2
|
+
chunk_id TEXT PRIMARY KEY,
|
|
3
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
|
|
4
|
+
logical_path TEXT NOT NULL,
|
|
5
|
+
title TEXT NOT NULL DEFAULT '',
|
|
6
|
+
heading TEXT NOT NULL DEFAULT '',
|
|
7
|
+
entity_type TEXT NOT NULL DEFAULT 'document',
|
|
8
|
+
change_slug TEXT NOT NULL DEFAULT '',
|
|
9
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
10
|
+
work_session_id TEXT NOT NULL DEFAULT '',
|
|
11
|
+
authority TEXT NOT NULL DEFAULT 'candidate',
|
|
12
|
+
observed_at TEXT NOT NULL,
|
|
13
|
+
validity TEXT NOT NULL DEFAULT 'active',
|
|
14
|
+
content_hash TEXT NOT NULL,
|
|
15
|
+
ordinal INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
content TEXT NOT NULL,
|
|
17
|
+
FOREIGN KEY(project_id, logical_path) REFERENCES documents(project_id, logical_path) ON DELETE CASCADE,
|
|
18
|
+
UNIQUE(project_id, logical_path, ordinal)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_document_chunks_project_path
|
|
22
|
+
ON document_chunks(project_id, logical_path, ordinal);
|
|
23
|
+
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_document_chunks_project_type
|
|
25
|
+
ON document_chunks(project_id, entity_type, validity, observed_at);
|
package/src/memory.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from './validate-memory.mjs';
|
|
21
21
|
import { validateCore } from './validate-core.mjs';
|
|
22
22
|
import { checkMemoryBundle } from '../hooks/vault-health.mjs';
|
|
23
|
+
import { scopeForMemoryKey } from '../hooks/memory-scope.mjs';
|
|
23
24
|
|
|
24
25
|
const BRAIN = '.brain';
|
|
25
26
|
const LEDGER = 'MEMORY_EVENTS.jsonl';
|
|
@@ -229,6 +230,92 @@ export function migrateMemory(vault, {
|
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
function rescopeEventId(sourceEventId, scope) {
|
|
234
|
+
return `mem-rescope-${hash(`${sourceEventId}\0${scope.type}\0${scope.id}`).slice(0, 24)}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Plan an append-only migration without choosing a winner for ambiguous keys. */
|
|
238
|
+
export function planScopedMemoryMigration(vault) {
|
|
239
|
+
const ledger = readMemoryLedger(vault);
|
|
240
|
+
if (ledger.status !== 'ok') throw new Error('Ledger corrompido; execute memory repair antes do rescope.');
|
|
241
|
+
const projection = deriveMemoryProjection(vault, ledger.events);
|
|
242
|
+
const ambiguous = new Set(projection.candidates.map((candidate) => candidate.record_key || candidate.memory_key));
|
|
243
|
+
const existingIds = new Set(ledger.events.map((event) => event.event_id));
|
|
244
|
+
const planned = [];
|
|
245
|
+
|
|
246
|
+
for (const [recordKey, record] of Object.entries(projection.records)) {
|
|
247
|
+
if (ambiguous.has(recordKey) || record.source?.scope) continue;
|
|
248
|
+
const source = record.source;
|
|
249
|
+
const sameLegacyKey = ledger.events.filter((event) => (
|
|
250
|
+
!event.scope && event.memory_key === source.memory_key && !event.rescopes_event_id
|
|
251
|
+
));
|
|
252
|
+
const scope = scopeForMemoryKey(source.memory_key, {
|
|
253
|
+
...source,
|
|
254
|
+
projectId: source.project_id,
|
|
255
|
+
workSessionId: source.work_session_id,
|
|
256
|
+
branch: source.value?.branch || source.value?.branch_name,
|
|
257
|
+
worktreeId: source.value?.worktree_id,
|
|
258
|
+
repositoryId: source.value?.repository_id,
|
|
259
|
+
});
|
|
260
|
+
const eventId = rescopeEventId(source.event_id, scope);
|
|
261
|
+
if (existingIds.has(eventId)) continue;
|
|
262
|
+
planned.push({
|
|
263
|
+
v: 1,
|
|
264
|
+
event_id: eventId,
|
|
265
|
+
project_id: source.project_id,
|
|
266
|
+
memory_key: source.memory_key,
|
|
267
|
+
scope,
|
|
268
|
+
operation: 'assert',
|
|
269
|
+
value: record.value,
|
|
270
|
+
authority: source.authority,
|
|
271
|
+
canonical_session_id: source.canonical_session_id || 'memory-rescope',
|
|
272
|
+
activation_id: source.activation_id || 'memory-rescope',
|
|
273
|
+
activation_epoch: Number.isInteger(source.activation_epoch) ? source.activation_epoch : 0,
|
|
274
|
+
turn_sequence: Number.isInteger(source.turn_sequence) ? source.turn_sequence : 0,
|
|
275
|
+
source_turn_id: source.source_turn_id || 'memory-rescope',
|
|
276
|
+
observed_at: source.observed_at || new Date(0).toISOString(),
|
|
277
|
+
evidence: [...new Set([...(source.evidence || []), `memory-rescope:${source.event_id}`])],
|
|
278
|
+
rescopes_event_id: source.event_id,
|
|
279
|
+
rescopes_event_ids: sameLegacyKey.map((event) => event.event_id).sort(),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
status: 'dry-run',
|
|
285
|
+
ledger_events: ledger.events.length,
|
|
286
|
+
planned: planned.length,
|
|
287
|
+
ambiguous: projection.candidates.length,
|
|
288
|
+
events: planned,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function rescopeMemoryEvents(vault, { apply = false } = {}) {
|
|
293
|
+
projectId(vault);
|
|
294
|
+
const plan = planScopedMemoryMigration(vault);
|
|
295
|
+
if (!apply) {
|
|
296
|
+
return {
|
|
297
|
+
status: 'dry-run',
|
|
298
|
+
ledger_events: plan.ledger_events,
|
|
299
|
+
planned: plan.planned,
|
|
300
|
+
ambiguous: plan.ambiguous,
|
|
301
|
+
scopes: plan.events.map((event) => ({
|
|
302
|
+
event_id: event.event_id, memory_key: event.memory_key, scope: event.scope,
|
|
303
|
+
})),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (!plan.events.length) {
|
|
307
|
+
return { status: 'unchanged', migrated: 0, ambiguous: plan.ambiguous };
|
|
308
|
+
}
|
|
309
|
+
for (const event of plan.events) enqueueMemoryEvent(vault, event);
|
|
310
|
+
const projection = projectMemoryOutbox(vault);
|
|
311
|
+
return {
|
|
312
|
+
status: projection.status === 'projected' ? 'migrated' : projection.status,
|
|
313
|
+
migrated: plan.events.length,
|
|
314
|
+
ambiguous: plan.ambiguous,
|
|
315
|
+
checkpoint: projection.checkpoint || null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
232
319
|
function readCandidates(vault) {
|
|
233
320
|
const path = brainPath(vault, CANDIDATES);
|
|
234
321
|
const checked = checkedVaultFile(vault, path, 'MEMORY_CANDIDATES.jsonl');
|
|
@@ -266,6 +353,7 @@ function sanitizedCandidate(candidate, index) {
|
|
|
266
353
|
reason: candidate.reason,
|
|
267
354
|
status: candidate.status || 'active',
|
|
268
355
|
memory_key: candidate.memory_key,
|
|
356
|
+
...(candidate.scope ? { scope: candidate.scope } : {}),
|
|
269
357
|
event_ids: [...eventIds].sort(lexicalCompare),
|
|
270
358
|
};
|
|
271
359
|
}
|
|
@@ -328,6 +416,7 @@ export function listMemoryCandidatesForCuration(vault) {
|
|
|
328
416
|
reason: safe.reason,
|
|
329
417
|
status: safe.status,
|
|
330
418
|
memory_key: safe.memory_key,
|
|
419
|
+
...(safe.scope ? { scope: safe.scope } : {}),
|
|
331
420
|
events: safe.event_ids.map((eventId) => sanitizedCurationEvent(candidate, eventId, index)),
|
|
332
421
|
};
|
|
333
422
|
})
|
|
@@ -388,7 +477,7 @@ function promotedSupersedes(vault, candidate, selected) {
|
|
|
388
477
|
const ledger = readMemoryLedger(vault);
|
|
389
478
|
if (ledger.status !== 'ok') throw new Error('Ledger de memória inválido durante a promoção.');
|
|
390
479
|
const projection = deriveMemoryProjection(vault, ledger.events);
|
|
391
|
-
const current = projection.records?.[candidate.memory_key]?.source;
|
|
480
|
+
const current = projection.records?.[candidate.record_key || candidate.memory_key]?.source;
|
|
392
481
|
if (!current?.event_id) return [...memberIds].sort();
|
|
393
482
|
if (memberIds.has(current.event_id)) {
|
|
394
483
|
projection.superseded
|
|
@@ -572,6 +661,9 @@ export function decideMemoryCandidate(vault, {
|
|
|
572
661
|
event_id: `cli-${action}-${hash(`${candidateId}\0${selected?.event_id || ''}`).slice(0, 20)}`,
|
|
573
662
|
project_id: projectId(vault),
|
|
574
663
|
memory_key: action === 'promote' ? candidate.memory_key : `candidate.decision.${candidateId}`,
|
|
664
|
+
scope: action === 'promote'
|
|
665
|
+
? (candidate.scope || selected?.scope || { type: 'project', id: projectId(vault) })
|
|
666
|
+
: { type: 'project', id: projectId(vault) },
|
|
575
667
|
operation: action === 'promote' && selected ? 'replace' : 'assert',
|
|
576
668
|
value: action === 'promote' ? promotedMemoryValue(selectedValue) : 'rejected',
|
|
577
669
|
authority: 'verified',
|
|
@@ -2087,6 +2179,7 @@ export function runMemory(argv) {
|
|
|
2087
2179
|
result = listMemoryCandidates(vault, { activeOnly: candidatesArgs.activeOnly });
|
|
2088
2180
|
}
|
|
2089
2181
|
else if (sub === 'migrate') result = migrateMemory(vault, { apply: argv.includes('--apply') });
|
|
2182
|
+
else if (sub === 'rescope') result = rescopeMemoryEvents(vault, { apply: argv.includes('--apply') });
|
|
2090
2183
|
else if (sub === 'repair') result = repairMemory(vault);
|
|
2091
2184
|
else if (sub === 'reconcile') {
|
|
2092
2185
|
result = reconcileMemory(vault, {
|
|
@@ -2109,7 +2202,7 @@ export function runMemory(argv) {
|
|
|
2109
2202
|
action: sub, candidateId: positional, ...(eventId ? { eventId } : {}),
|
|
2110
2203
|
});
|
|
2111
2204
|
}
|
|
2112
|
-
else { process.stderr.write('wendkeep memory: use status | candidates [--active] | migrate [--apply] | repair | recover-attempt <session> [--apply] | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
|
|
2205
|
+
else { process.stderr.write('wendkeep memory: use status | candidates [--active] | curate | migrate [--apply] | rescope [--apply] | repair | recover-attempt <session> [--apply] | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
|
|
2113
2206
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
2114
2207
|
if (sub === 'status' && argv.includes('--gate')) process.exitCode = result.status === 'blocked' ? 1 : 0;
|
|
2115
2208
|
else if (sub === 'reconcile' && reconcileArgs.apply) process.exitCode = result.health?.status === 'blocked' ? 1 : 0;
|
|
@@ -4,9 +4,12 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import { join, basename, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { decodeTranscript, encodeTranscript } from './observer-transcript-store.mjs';
|
|
7
|
+
import {
|
|
8
|
+
chunkMarkdownDocument, recallEvidence, recallTerms,
|
|
9
|
+
} from '../packages/vault/src/evidence-recall.mjs';
|
|
7
10
|
|
|
8
11
|
export const OBSERVER_SQL_FILE = 'observer.sqlite';
|
|
9
|
-
export const OBSERVER_SQL_SCHEMA_VERSION =
|
|
12
|
+
export const OBSERVER_SQL_SCHEMA_VERSION = 4;
|
|
10
13
|
export const OBSERVER_EVENT_SCHEMA_VERSION = 1;
|
|
11
14
|
|
|
12
15
|
const SCHEMA_DIR = fileURLToPath(new URL('../schema/observer/', import.meta.url));
|
|
@@ -117,12 +120,106 @@ export function migrateObserverDatabase(db) {
|
|
|
117
120
|
throw error;
|
|
118
121
|
}
|
|
119
122
|
}
|
|
123
|
+
rebuildSqlEvidenceIndex(db, { missingOnly: true });
|
|
120
124
|
return {
|
|
121
125
|
version: Number(db.prepare('SELECT COALESCE(MAX(version), 0) AS version FROM schema_migrations').get().version || 0),
|
|
122
126
|
applied: db.prepare('SELECT version, name, applied_at FROM schema_migrations ORDER BY version').all(),
|
|
123
127
|
};
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
export function observerFts5Support(db) {
|
|
131
|
+
try {
|
|
132
|
+
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS evidence_chunks_fts USING fts5(
|
|
133
|
+
chunk_id UNINDEXED,
|
|
134
|
+
project_id UNINDEXED,
|
|
135
|
+
logical_path,
|
|
136
|
+
title,
|
|
137
|
+
heading,
|
|
138
|
+
content,
|
|
139
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
140
|
+
)`);
|
|
141
|
+
return { supported: true, engine: 'fts5' };
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return { supported: false, engine: 'lexical-fallback', reason: text(error?.message || error) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function synchronizeSqlFts(db) {
|
|
148
|
+
const support = observerFts5Support(db);
|
|
149
|
+
if (!support.supported) return support;
|
|
150
|
+
const chunks = Number(db.prepare('SELECT COUNT(*) AS count FROM document_chunks').get().count || 0);
|
|
151
|
+
const indexed = Number(db.prepare('SELECT COUNT(*) AS count FROM evidence_chunks_fts').get().count || 0);
|
|
152
|
+
if (chunks === indexed) return support;
|
|
153
|
+
db.exec(`DELETE FROM evidence_chunks_fts;
|
|
154
|
+
INSERT INTO evidence_chunks_fts(chunk_id, project_id, logical_path, title, heading, content)
|
|
155
|
+
SELECT chunk_id, project_id, logical_path, title, heading, content
|
|
156
|
+
FROM document_chunks;`);
|
|
157
|
+
return { ...support, rebuilt: true, chunks };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeSqlDocumentChunks(db, {
|
|
161
|
+
projectId, logicalPath, content, metadata = {}, entityType = 'document', capturedAt = '',
|
|
162
|
+
} = {}) {
|
|
163
|
+
const chunks = chunkMarkdownDocument({
|
|
164
|
+
projectId,
|
|
165
|
+
logicalPath,
|
|
166
|
+
content,
|
|
167
|
+
metadata: { ...metadata, observed_at: metadata.observed_at || capturedAt },
|
|
168
|
+
entityType,
|
|
169
|
+
});
|
|
170
|
+
db.prepare('DELETE FROM document_chunks WHERE project_id = ? AND logical_path = ?').run(projectId, logicalPath);
|
|
171
|
+
const fts = observerFts5Support(db);
|
|
172
|
+
if (fts.supported) {
|
|
173
|
+
db.prepare('DELETE FROM evidence_chunks_fts WHERE project_id = ? AND logical_path = ?').run(projectId, logicalPath);
|
|
174
|
+
}
|
|
175
|
+
const insert = db.prepare(`INSERT INTO document_chunks(
|
|
176
|
+
chunk_id, project_id, logical_path, title, heading, entity_type, change_slug,
|
|
177
|
+
session_id, work_session_id, authority, observed_at, validity, content_hash, ordinal, content
|
|
178
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
179
|
+
const insertFts = fts.supported
|
|
180
|
+
? db.prepare('INSERT INTO evidence_chunks_fts(chunk_id, project_id, logical_path, title, heading, content) VALUES (?, ?, ?, ?, ?, ?)')
|
|
181
|
+
: null;
|
|
182
|
+
for (const chunk of chunks) {
|
|
183
|
+
insert.run(
|
|
184
|
+
chunk.chunk_id, chunk.project_id, chunk.logical_path, chunk.title, chunk.heading,
|
|
185
|
+
chunk.entity_type, chunk.change_slug, chunk.session_id, chunk.work_session_id,
|
|
186
|
+
chunk.authority, chunk.observed_at, chunk.validity, chunk.content_hash,
|
|
187
|
+
chunk.ordinal, chunk.content,
|
|
188
|
+
);
|
|
189
|
+
insertFts?.run(chunk.chunk_id, chunk.project_id, chunk.logical_path, chunk.title, chunk.heading, chunk.content);
|
|
190
|
+
}
|
|
191
|
+
return { chunks: chunks.length, fts };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function rebuildSqlEvidenceIndex(db, { missingOnly = false } = {}) {
|
|
195
|
+
const table = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'document_chunks'").get();
|
|
196
|
+
if (!table) return { documents: 0, chunks: 0, fts: { supported: false, engine: 'unavailable' } };
|
|
197
|
+
const documents = db.prepare(`SELECT d.project_id, d.logical_path, d.content, d.metadata_json, d.entity_type, d.captured_at
|
|
198
|
+
FROM documents d
|
|
199
|
+
WHERE d.deleted_at IS NULL
|
|
200
|
+
AND (? = 0 OR NOT EXISTS (
|
|
201
|
+
SELECT 1 FROM document_chunks c
|
|
202
|
+
WHERE c.project_id = d.project_id AND c.logical_path = d.logical_path
|
|
203
|
+
))
|
|
204
|
+
ORDER BY d.project_id, d.logical_path`).all(missingOnly ? 1 : 0);
|
|
205
|
+
let chunks = 0;
|
|
206
|
+
let fts = observerFts5Support(db);
|
|
207
|
+
for (const document of documents) {
|
|
208
|
+
const result = writeSqlDocumentChunks(db, {
|
|
209
|
+
projectId: document.project_id,
|
|
210
|
+
logicalPath: document.logical_path,
|
|
211
|
+
content: document.content,
|
|
212
|
+
metadata: parseJson(document.metadata_json),
|
|
213
|
+
entityType: document.entity_type,
|
|
214
|
+
capturedAt: document.captured_at,
|
|
215
|
+
});
|
|
216
|
+
chunks += result.chunks;
|
|
217
|
+
fts = result.fts;
|
|
218
|
+
}
|
|
219
|
+
fts = synchronizeSqlFts(db);
|
|
220
|
+
return { documents: documents.length, chunks, fts };
|
|
221
|
+
}
|
|
222
|
+
|
|
126
223
|
export function ensureObserverDatabase(dataDir) {
|
|
127
224
|
const db = openObserverDatabase(dataDir);
|
|
128
225
|
try {
|
|
@@ -266,6 +363,14 @@ function applyDocument(db, event) {
|
|
|
266
363
|
VALUES (?, ?, ?, ?, 'upsert', ?, ?, ?, ?, ?, ?)
|
|
267
364
|
`).run(event.event_id, event.project_id, text(p.entity_type || p.entityType, 'memory'), logicalPath, revision, contentHash,
|
|
268
365
|
text(p.source_session_id || p.sourceSessionId), text(p.source_turn_id || p.sourceTurnId), event.occurred_at, json(p));
|
|
366
|
+
writeSqlDocumentChunks(db, {
|
|
367
|
+
projectId: event.project_id,
|
|
368
|
+
logicalPath,
|
|
369
|
+
content,
|
|
370
|
+
metadata: p.metadata,
|
|
371
|
+
entityType: text(p.entity_type || p.entityType, 'memory'),
|
|
372
|
+
capturedAt: text(p.captured_at || event.occurred_at),
|
|
373
|
+
});
|
|
269
374
|
return { stale: false };
|
|
270
375
|
}
|
|
271
376
|
|
|
@@ -279,6 +384,10 @@ function applyDocumentDelete(db, event) {
|
|
|
279
384
|
VALUES (?, ?, ?, ?, 'delete', ?, '', ?, ?, ?, ?)
|
|
280
385
|
`).run(event.event_id, event.project_id, text(event.payload.entity_type || event.payload.entityType, 'memory'), logicalPath,
|
|
281
386
|
integer(event.payload.revision, 1), text(event.payload.source_session_id || event.payload.sourceSessionId), text(event.payload.source_turn_id || event.payload.sourceTurnId), event.occurred_at, json(event.payload));
|
|
387
|
+
db.prepare('DELETE FROM document_chunks WHERE project_id = ? AND logical_path = ?').run(event.project_id, logicalPath);
|
|
388
|
+
if (observerFts5Support(db).supported) {
|
|
389
|
+
db.prepare('DELETE FROM evidence_chunks_fts WHERE project_id = ? AND logical_path = ?').run(event.project_id, logicalPath);
|
|
390
|
+
}
|
|
282
391
|
return { stale: false };
|
|
283
392
|
}
|
|
284
393
|
|
|
@@ -542,11 +651,38 @@ export function readSqlTree(db, projectId, prefix = '') {
|
|
|
542
651
|
return { schema_version: OBSERVER_SQL_SCHEMA_VERSION, project_id: projectId, prefix, documents: rows };
|
|
543
652
|
}
|
|
544
653
|
|
|
545
|
-
export function searchSqlDocuments(db, projectId, query = '') {
|
|
654
|
+
export function searchSqlDocuments(db, projectId, query = '', { forceLexical = false } = {}) {
|
|
546
655
|
requireProject(db, projectId);
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
656
|
+
const terms = recallTerms(query);
|
|
657
|
+
if (!terms.length) return [];
|
|
658
|
+
const fts = forceLexical
|
|
659
|
+
? { supported: false, engine: 'lexical-fallback' }
|
|
660
|
+
: observerFts5Support(db);
|
|
661
|
+
const columns = `c.chunk_id, c.project_id, c.logical_path, c.title, c.heading,
|
|
662
|
+
c.entity_type, c.change_slug, c.session_id, c.work_session_id, c.authority,
|
|
663
|
+
c.observed_at, c.validity, c.content_hash AS chunk_content_hash, c.ordinal, c.content,
|
|
664
|
+
d.content_hash, d.revision, d.captured_at, d.source_session_id`;
|
|
665
|
+
let rows;
|
|
666
|
+
if (fts.supported) {
|
|
667
|
+
const expression = [...new Set(terms)]
|
|
668
|
+
.map((term) => `"${term.replaceAll('"', '""')}"`)
|
|
669
|
+
.join(' OR ');
|
|
670
|
+
rows = db.prepare(`SELECT ${columns} FROM evidence_chunks_fts f
|
|
671
|
+
JOIN document_chunks c ON c.chunk_id = f.chunk_id
|
|
672
|
+
JOIN documents d ON d.project_id = c.project_id AND d.logical_path = c.logical_path
|
|
673
|
+
WHERE evidence_chunks_fts MATCH ? AND c.project_id = ? AND d.deleted_at IS NULL
|
|
674
|
+
ORDER BY bm25(evidence_chunks_fts, 0, 0, 1.5, 3.0, 2.5, 1.0), c.observed_at DESC
|
|
675
|
+
LIMIT 200`).all(expression, projectId);
|
|
676
|
+
} else {
|
|
677
|
+
// FTS5 may be unavailable in a valid Observer runtime. Rank the complete project corpus
|
|
678
|
+
// instead of truncating by recency before matching, which would make old exact evidence
|
|
679
|
+
// permanently unreachable.
|
|
680
|
+
rows = db.prepare(`SELECT ${columns} FROM document_chunks c
|
|
681
|
+
JOIN documents d ON d.project_id = c.project_id AND d.logical_path = c.logical_path
|
|
682
|
+
WHERE c.project_id = ? AND d.deleted_at IS NULL
|
|
683
|
+
ORDER BY c.observed_at DESC, c.logical_path, c.ordinal`).all(projectId);
|
|
684
|
+
}
|
|
685
|
+
return recallEvidence(rows, query, { topK: 5 });
|
|
550
686
|
}
|
|
551
687
|
|
|
552
688
|
export function readSqlSync(db, projectId) {
|
package/src/taxonomy.mjs
CHANGED
|
@@ -65,6 +65,8 @@ export const HOOK_FILES = [
|
|
|
65
65
|
'subagent-usage.mjs',
|
|
66
66
|
'pricing.json',
|
|
67
67
|
'brain-core.mjs',
|
|
68
|
+
'evidence-recall.mjs',
|
|
69
|
+
'memory-scope.mjs',
|
|
68
70
|
'memory-schema.mjs',
|
|
69
71
|
'memory-store.mjs',
|
|
70
72
|
'memory-handoff.mjs',
|
|
@@ -83,6 +85,7 @@ export const HOOK_FILES = [
|
|
|
83
85
|
'harness-doctor.mjs',
|
|
84
86
|
'lessons-core.mjs',
|
|
85
87
|
'brain-inject.mjs',
|
|
88
|
+
'evidence-context.mjs',
|
|
86
89
|
'brain-recall.mjs',
|
|
87
90
|
'brain-reindex.mjs',
|
|
88
91
|
'session-backfill.mjs',
|
|
@@ -114,6 +117,7 @@ export const RUNNABLE_HOOKS = [
|
|
|
114
117
|
'subagent-stop',
|
|
115
118
|
'task-log',
|
|
116
119
|
'brain-inject',
|
|
120
|
+
'evidence-context',
|
|
117
121
|
'brain-recall',
|
|
118
122
|
'brain-reindex',
|
|
119
123
|
'vault-health',
|