@graphorin/memory 0.6.1 → 0.7.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 +71 -0
- package/README.md +1 -1
- package/dist/conflict/index.js +0 -1
- package/dist/conflict/pipeline.js +0 -1
- package/dist/conflict/pipeline.js.map +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
- package/dist/conflict/types.d.ts +1 -1
- package/dist/conflict/types.js.map +1 -1
- package/dist/consolidator/index.js +2 -0
- package/dist/consolidator/index.js.map +1 -1
- package/dist/consolidator/lock.js +7 -1
- package/dist/consolidator/lock.js.map +1 -1
- package/dist/consolidator/phases/deep.js +19 -12
- package/dist/consolidator/phases/deep.js.map +1 -1
- package/dist/consolidator/phases/induce.d.ts.map +1 -1
- package/dist/consolidator/phases/induce.js +2 -3
- package/dist/consolidator/phases/induce.js.map +1 -1
- package/dist/consolidator/phases/reflect.js +35 -8
- package/dist/consolidator/phases/reflect.js.map +1 -1
- package/dist/consolidator/phases/standard.js +199 -28
- package/dist/consolidator/phases/standard.js.map +1 -1
- package/dist/consolidator/reconcile.js +16 -15
- package/dist/consolidator/reconcile.js.map +1 -1
- package/dist/consolidator/runtime.d.ts.map +1 -1
- package/dist/consolidator/runtime.js +129 -47
- package/dist/consolidator/runtime.js.map +1 -1
- package/dist/consolidator/scheduler.d.ts +17 -3
- package/dist/consolidator/scheduler.d.ts.map +1 -1
- package/dist/consolidator/scheduler.js.map +1 -1
- package/dist/consolidator/types.d.ts +30 -1
- package/dist/consolidator/types.d.ts.map +1 -1
- package/dist/consolidator/types.js +10 -0
- package/dist/consolidator/types.js.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
- package/dist/context-engine/compaction/compactor.d.ts +1 -0
- package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
- package/dist/context-engine/compaction/compactor.js +32 -8
- package/dist/context-engine/compaction/compactor.js.map +1 -1
- package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
- package/dist/context-engine/compaction/index.d.ts +1 -0
- package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
- package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
- package/dist/context-engine/compaction/templates/marker.js +34 -0
- package/dist/context-engine/compaction/templates/marker.js.map +1 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
- package/dist/context-engine/engine.d.ts +7 -176
- package/dist/context-engine/engine.d.ts.map +1 -1
- package/dist/context-engine/engine.js +0 -2
- package/dist/context-engine/engine.js.map +1 -1
- package/dist/context-engine/index.d.ts +8 -49
- package/dist/context-engine/io-types.d.ts +144 -0
- package/dist/context-engine/io-types.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/types.d.ts +1 -1
- package/dist/context-engine/locale-packs/types.js.map +1 -1
- package/dist/context-engine/privacy-filter.d.ts +1 -2
- package/dist/context-engine/privacy-filter.d.ts.map +1 -1
- package/dist/context-engine/privacy-filter.js +1 -2
- package/dist/context-engine/privacy-filter.js.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +2 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/facade.d.ts +43 -52
- package/dist/facade.d.ts.map +1 -1
- package/dist/facade.js +5 -3
- package/dist/facade.js.map +1 -1
- package/dist/graph/entity-resolver.js +1 -1
- package/dist/graph/entity-resolver.js.map +1 -1
- package/dist/index.d.ts +16 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/envelope.js +57 -0
- package/dist/internal/envelope.js.map +1 -0
- package/dist/internal/injection-heuristics.js +0 -0
- package/dist/internal/injection-heuristics.js.map +1 -1
- package/dist/internal/llm-json.js +40 -0
- package/dist/internal/llm-json.js.map +1 -0
- package/dist/internal/storage-adapter.d.ts +23 -10
- package/dist/internal/storage-adapter.d.ts.map +1 -1
- package/dist/memory-interface.d.ts +131 -0
- package/dist/memory-interface.d.ts.map +1 -0
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/search/iterative.d.ts.map +1 -1
- package/dist/search/iterative.js +2 -3
- package/dist/search/iterative.js.map +1 -1
- package/dist/search/query-transform.d.ts.map +1 -1
- package/dist/search/query-transform.js +2 -3
- package/dist/search/query-transform.js.map +1 -1
- package/dist/tiers/episodic-memory.js +3 -3
- package/dist/tiers/episodic-memory.js.map +1 -1
- package/dist/tiers/index.d.ts +2 -2
- package/dist/tiers/insight-memory.js +1 -1
- package/dist/tiers/insight-memory.js.map +1 -1
- package/dist/tiers/procedural-memory.d.ts +1 -1
- package/dist/tiers/procedural-memory.js +2 -2
- package/dist/tiers/procedural-memory.js.map +1 -1
- package/dist/tiers/semantic-memory.d.ts +58 -4
- package/dist/tiers/semantic-memory.d.ts.map +1 -1
- package/dist/tiers/semantic-memory.js +103 -20
- package/dist/tiers/semantic-memory.js.map +1 -1
- package/dist/tools/block-tools.d.ts +19 -57
- package/dist/tools/block-tools.d.ts.map +1 -1
- package/dist/tools/block-tools.js.map +1 -1
- package/dist/tools/fact-tools.d.ts +53 -214
- package/dist/tools/fact-tools.d.ts.map +1 -1
- package/dist/tools/fact-tools.js +3 -2
- package/dist/tools/fact-tools.js.map +1 -1
- package/dist/tools/recall-tools.d.ts +34 -175
- package/dist/tools/recall-tools.d.ts.map +1 -1
- package/dist/tools/recall-tools.js +5 -4
- package/dist/tools/recall-tools.js.map +1 -1
- package/dist/tools/runbook-tools.d.ts +15 -58
- package/dist/tools/runbook-tools.d.ts.map +1 -1
- package/dist/tools/runbook-tools.js.map +1 -1
- package/dist/tools/types.d.ts +1 -1
- package/package.json +18 -17
- package/src/conflict/index.ts +29 -0
- package/src/conflict/locale-packs/en.ts +142 -0
- package/src/conflict/locale-packs/index.ts +17 -0
- package/src/conflict/locale-packs/types.ts +138 -0
- package/src/conflict/pipeline.ts +371 -0
- package/src/conflict/stages/helpers.ts +65 -0
- package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
- package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
- package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
- package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
- package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
- package/src/conflict/types.ts +263 -0
- package/src/consolidator/budget.ts +312 -0
- package/src/consolidator/decay.ts +246 -0
- package/src/consolidator/dlq.ts +61 -0
- package/src/consolidator/errors.ts +87 -0
- package/src/consolidator/idempotency.ts +27 -0
- package/src/consolidator/index.ts +269 -0
- package/src/consolidator/lock.ts +141 -0
- package/src/consolidator/noise-filter.ts +235 -0
- package/src/consolidator/phases/deep.ts +308 -0
- package/src/consolidator/phases/induce.ts +423 -0
- package/src/consolidator/phases/learned-context.ts +233 -0
- package/src/consolidator/phases/light.ts +166 -0
- package/src/consolidator/phases/reflect.ts +504 -0
- package/src/consolidator/phases/standard.ts +1002 -0
- package/src/consolidator/reconcile.ts +273 -0
- package/src/consolidator/runtime.ts +1049 -0
- package/src/consolidator/scheduler.ts +182 -0
- package/src/consolidator/triggers.ts +134 -0
- package/src/consolidator/types.ts +622 -0
- package/src/context-engine/annotations.ts +161 -0
- package/src/context-engine/auto-recall.ts +92 -0
- package/src/context-engine/compaction/clear-tool-results.ts +232 -0
- package/src/context-engine/compaction/compactor.ts +403 -0
- package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
- package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
- package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
- package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
- package/src/context-engine/compaction/hooks/types.ts +68 -0
- package/src/context-engine/compaction/index.ts +60 -0
- package/src/context-engine/compaction/templates/marker.ts +33 -0
- package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
- package/src/context-engine/compaction/thresholds.ts +107 -0
- package/src/context-engine/compaction/types.ts +255 -0
- package/src/context-engine/engine.ts +970 -0
- package/src/context-engine/index.ts +118 -0
- package/src/context-engine/io-types.ts +162 -0
- package/src/context-engine/locale-packs/en.ts +107 -0
- package/src/context-engine/locale-packs/index.ts +19 -0
- package/src/context-engine/locale-packs/resolver.ts +197 -0
- package/src/context-engine/locale-packs/types.ts +177 -0
- package/src/context-engine/metadata.ts +114 -0
- package/src/context-engine/preambles/inbound-en.ts +19 -0
- package/src/context-engine/privacy-filter.ts +190 -0
- package/src/context-engine/templates/base-en.ts +19 -0
- package/src/context-engine/templates/composer.ts +91 -0
- package/src/context-engine/token-budget.ts +319 -0
- package/src/context-engine/token-counter.ts +157 -0
- package/src/errors/index.ts +273 -0
- package/src/facade.ts +750 -0
- package/src/graph/entity-resolver.ts +364 -0
- package/src/graph/index.ts +24 -0
- package/src/index.ts +259 -0
- package/src/internal/contextualize.ts +173 -0
- package/src/internal/embedder-binding.ts +53 -0
- package/src/internal/envelope.ts +70 -0
- package/src/internal/id.ts +23 -0
- package/src/internal/injection-heuristics.ts +0 -0
- package/src/internal/llm-json.ts +38 -0
- package/src/internal/spans.ts +35 -0
- package/src/internal/storage-adapter.ts +963 -0
- package/src/memory-interface.ts +158 -0
- package/src/migration/embedder-migration.ts +237 -0
- package/src/migration/index.ts +13 -0
- package/src/search/explain.ts +123 -0
- package/src/search/fit-weights.ts +109 -0
- package/src/search/graph-ppr.ts +38 -0
- package/src/search/index.ts +55 -0
- package/src/search/iterative.ts +658 -0
- package/src/search/query-transform.ts +285 -0
- package/src/search/rrf.ts +225 -0
- package/src/search/trust.ts +55 -0
- package/src/search/types.ts +50 -0
- package/src/tiers/episodic-memory.ts +463 -0
- package/src/tiers/index.ts +50 -0
- package/src/tiers/insight-memory.ts +208 -0
- package/src/tiers/procedural-memory.ts +482 -0
- package/src/tiers/semantic-memory.ts +1729 -0
- package/src/tiers/session-memory.ts +271 -0
- package/src/tiers/shared-memory.ts +62 -0
- package/src/tiers/working-memory.ts +368 -0
- package/src/tools/block-tools.ts +172 -0
- package/src/tools/fact-tools.ts +473 -0
- package/src/tools/index.ts +109 -0
- package/src/tools/recall-tools.ts +303 -0
- package/src/tools/runbook-tools.ts +107 -0
- package/src/tools/taint.ts +39 -0
- package/src/tools/types.ts +37 -0
- package/dist/conflict/locale-packs/index.js +0 -4
- package/dist/context-engine/compaction/index.js +0 -10
- package/dist/context-engine/index.d.ts.map +0 -1
- package/dist/context-engine/index.js +0 -25
- package/dist/context-engine/locale-packs/index.js +0 -4
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers shared across the five pipeline stages.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { md5 } from '@graphorin/core';
|
|
9
|
+
import type { LocalePack } from '../locale-packs/index.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Lowercase + collapse whitespace + trim. Used by Stage 1 (exact
|
|
13
|
+
* dedup) to compute the canonical fact body that feeds the MD5
|
|
14
|
+
* comparison.
|
|
15
|
+
*/
|
|
16
|
+
export function canonicaliseFactBody(text: string): string {
|
|
17
|
+
return text.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stable hash for a canonical fact body. We re-use the framework's
|
|
22
|
+
* MD5 helper to stay consistent with the storage layer's existing
|
|
23
|
+
* dedup hash column (see `facts.hash`).
|
|
24
|
+
*/
|
|
25
|
+
export function fastFactHash(text: string): string {
|
|
26
|
+
return md5(text);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Strip leading articles + trim a candidate subject token. */
|
|
30
|
+
export function normaliseSubject(input: string, pack: LocalePack): string {
|
|
31
|
+
const tokens = canonicaliseFactBody(input).split(' ');
|
|
32
|
+
const stop = new Set(pack.subjectStopWords.map((s) => s.toLowerCase()));
|
|
33
|
+
return tokens.filter((token) => token.length > 0 && !stop.has(token)).join(' ');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Naive subject / predicate / object split. The locale pack supplies
|
|
38
|
+
* the verb tokens used as predicate normalisers - the first matching
|
|
39
|
+
* verb in the sentence becomes the predicate and splits the rest
|
|
40
|
+
* into subject (left side) and object (right side). The split is
|
|
41
|
+
* intentionally simple: anything more clever lives behind the entity-
|
|
42
|
+
* linking layer (post-MVP).
|
|
43
|
+
*/
|
|
44
|
+
export function splitSubjectPredicateObject(
|
|
45
|
+
text: string,
|
|
46
|
+
pack: LocalePack,
|
|
47
|
+
): { readonly subject: string; readonly predicate: string; readonly object: string } | null {
|
|
48
|
+
const canonical = canonicaliseFactBody(text);
|
|
49
|
+
if (canonical.length === 0) return null;
|
|
50
|
+
const tokens = canonical.split(' ');
|
|
51
|
+
const verbs = new Set(pack.predicateNormalisers.map((v) => v.toLowerCase()));
|
|
52
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
53
|
+
const token = tokens[i];
|
|
54
|
+
if (token === undefined) continue;
|
|
55
|
+
const stripped = token.replace(/[.,!?;:]/g, '');
|
|
56
|
+
if (verbs.has(stripped)) {
|
|
57
|
+
const subject = normaliseSubject(tokens.slice(0, i).join(' '), pack);
|
|
58
|
+
const predicate = stripped;
|
|
59
|
+
const object = canonicaliseFactBody(tokens.slice(i + 1).join(' '));
|
|
60
|
+
if (subject.length === 0 || object.length === 0) continue;
|
|
61
|
+
return { subject, predicate, object };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 1 - Exact dedup. Computes an MD5 hash on the canonical fact
|
|
3
|
+
* body (lowercase + trimmed + collapsed whitespace) and short-circuits
|
|
4
|
+
* to `dedup` when an existing fact in the supplied vector hit list
|
|
5
|
+
* shares the same canonical form.
|
|
6
|
+
*
|
|
7
|
+
* Stage 1 only inspects `existing` when Stage 2 already populated it
|
|
8
|
+
* for the orchestrator (re-using the embedding round-trip). When the
|
|
9
|
+
* orchestrator calls Stage 1 with an empty list the stage falls
|
|
10
|
+
* through to `'continue'` - Stage 2 will gather candidates and Stage 1
|
|
11
|
+
* is folded into Stage 2's HOT zone via the same canonical-form
|
|
12
|
+
* comparison.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { PipelineStage } from '../types.js';
|
|
18
|
+
import { canonicaliseFactBody, fastFactHash } from './helpers.js';
|
|
19
|
+
|
|
20
|
+
export const stage1ExactDedup: PipelineStage = {
|
|
21
|
+
id: 'exact-dedup',
|
|
22
|
+
async evaluate(ctx) {
|
|
23
|
+
const candidateHash = fastFactHash(canonicaliseFactBody(ctx.candidate.text));
|
|
24
|
+
if (ctx.existing.length === 0) {
|
|
25
|
+
return { kind: 'continue' };
|
|
26
|
+
}
|
|
27
|
+
for (const hit of ctx.existing) {
|
|
28
|
+
const existingHash = fastFactHash(canonicaliseFactBody(hit.record.text));
|
|
29
|
+
if (existingHash === candidateHash) {
|
|
30
|
+
return {
|
|
31
|
+
kind: 'dedup',
|
|
32
|
+
existingId: hit.record.id,
|
|
33
|
+
similarity: 1,
|
|
34
|
+
reason: 'exact-hash-match',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { kind: 'continue' };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 2 - Embedding three-zone classifier (RB-02 §8.1).
|
|
3
|
+
*
|
|
4
|
+
* Inspects the candidate similarity against the top-K nearest
|
|
5
|
+
* neighbours surfaced by the semantic vector store and routes the
|
|
6
|
+
* decision into one of four zones:
|
|
7
|
+
*
|
|
8
|
+
* - HOT (`sim ≥ thresholds.hot`): silent dedup - the existing fact is
|
|
9
|
+
* semantically identical, no audit-worthy conflict.
|
|
10
|
+
* - NEAR-DUP (`thresholds.nearDup ≤ sim < thresholds.hot`): same
|
|
11
|
+
* fact phrased differently - dedup with `near-dup` zone label so
|
|
12
|
+
* operators can spot the case in `fact_conflicts`.
|
|
13
|
+
* - CONFLICT-CHECK (`thresholds.cold < sim < thresholds.nearDup`):
|
|
14
|
+
* candidate sounds related; defer decision to Stages 3 → 4 → 5.
|
|
15
|
+
* - COLD (`sim ≤ thresholds.cold`): no conflict - admit the candidate.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { PipelineStage, StageOutcome } from '../types.js';
|
|
21
|
+
|
|
22
|
+
export const stage2EmbeddingThreeZone: PipelineStage = {
|
|
23
|
+
id: 'embedding-three-zone',
|
|
24
|
+
async evaluate(ctx): Promise<StageOutcome> {
|
|
25
|
+
if (ctx.existing.length === 0) {
|
|
26
|
+
return { kind: 'admit', reason: 'no-existing-candidates' };
|
|
27
|
+
}
|
|
28
|
+
const top = ctx.existing[0];
|
|
29
|
+
if (top === undefined) {
|
|
30
|
+
return { kind: 'admit', reason: 'no-existing-candidates' };
|
|
31
|
+
}
|
|
32
|
+
const similarity = top.score;
|
|
33
|
+
if (similarity >= ctx.thresholds.hot) {
|
|
34
|
+
return {
|
|
35
|
+
kind: 'dedup',
|
|
36
|
+
existingId: top.record.id,
|
|
37
|
+
similarity,
|
|
38
|
+
reason: 'embedding-hot-zone',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (similarity >= ctx.thresholds.nearDup) {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'dedup',
|
|
44
|
+
existingId: top.record.id,
|
|
45
|
+
similarity,
|
|
46
|
+
reason: 'embedding-near-dup-zone',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (similarity > ctx.thresholds.cold) {
|
|
50
|
+
return { kind: 'continue' };
|
|
51
|
+
}
|
|
52
|
+
return { kind: 'admit', reason: 'embedding-cold-zone' };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 3 - Heuristic regex (per-locale, pluggable).
|
|
3
|
+
*
|
|
4
|
+
* Inspects the candidate fact body for explicit supersede or negation
|
|
5
|
+
* markers from the active locale pack (English by default). When a
|
|
6
|
+
* marker fires the stage emits a `supersede` outcome against the
|
|
7
|
+
* highest-scoring CONFLICT-CHECK candidate from Stage 2.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { evaluateMarkers } from '../locale-packs/index.js';
|
|
13
|
+
import type { PipelineStage, StageOutcome } from '../types.js';
|
|
14
|
+
|
|
15
|
+
export const stage3HeuristicRegex: PipelineStage = {
|
|
16
|
+
id: 'heuristic-regex',
|
|
17
|
+
async evaluate(ctx): Promise<StageOutcome> {
|
|
18
|
+
const top = ctx.existing[0];
|
|
19
|
+
if (top === undefined) {
|
|
20
|
+
return { kind: 'continue' };
|
|
21
|
+
}
|
|
22
|
+
const candidateText = ctx.candidate.text;
|
|
23
|
+
const supersede = evaluateMarkers(candidateText, ctx.localePack.supersedeMarkers);
|
|
24
|
+
if (supersede.matched) {
|
|
25
|
+
return {
|
|
26
|
+
kind: 'supersede',
|
|
27
|
+
existingId: top.record.id,
|
|
28
|
+
reason: formatReason('regex-supersede-marker', supersede),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const negation = evaluateMarkers(candidateText, ctx.localePack.negationMarkers);
|
|
32
|
+
if (negation.matched) {
|
|
33
|
+
return {
|
|
34
|
+
kind: 'supersede',
|
|
35
|
+
existingId: top.record.id,
|
|
36
|
+
reason: formatReason('regex-negation', negation),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { kind: 'continue' };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function formatReason(
|
|
44
|
+
base: string,
|
|
45
|
+
match: { kind?: string | undefined; excerpt?: string | undefined },
|
|
46
|
+
): string {
|
|
47
|
+
const parts = [base];
|
|
48
|
+
if (match.kind !== undefined) parts.push(`kind=${match.kind}`);
|
|
49
|
+
if (match.excerpt !== undefined) parts.push(`marker=${match.excerpt}`);
|
|
50
|
+
return parts.join('; ');
|
|
51
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 4 - Subject / predicate match.
|
|
3
|
+
*
|
|
4
|
+
* Naively splits each fact body into `(subject, predicate, object)`
|
|
5
|
+
* triples using the locale pack's predicate normalisers. When the
|
|
6
|
+
* candidate and an existing CONFLICT-CHECK fact share the same
|
|
7
|
+
* subject + predicate but differ on the object, Stage 4 marks the
|
|
8
|
+
* existing fact as superseded.
|
|
9
|
+
*
|
|
10
|
+
* The split is intentionally simple: anything more sophisticated
|
|
11
|
+
* (full entity linking) lives in the post-MVP knowledge-graph layer.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { PipelineStage, StageOutcome } from '../types.js';
|
|
17
|
+
import { splitSubjectPredicateObject } from './helpers.js';
|
|
18
|
+
|
|
19
|
+
export const stage4SubjectPredicate: PipelineStage = {
|
|
20
|
+
id: 'subject-predicate',
|
|
21
|
+
async evaluate(ctx): Promise<StageOutcome> {
|
|
22
|
+
const candidateTriple = splitSubjectPredicateObject(ctx.candidate.text, ctx.localePack);
|
|
23
|
+
if (candidateTriple === null) {
|
|
24
|
+
return { kind: 'continue' };
|
|
25
|
+
}
|
|
26
|
+
for (const hit of ctx.existing) {
|
|
27
|
+
const existingTriple = splitSubjectPredicateObject(hit.record.text, ctx.localePack);
|
|
28
|
+
if (existingTriple === null) continue;
|
|
29
|
+
if (candidateTriple.subject !== existingTriple.subject) continue;
|
|
30
|
+
if (candidateTriple.predicate !== existingTriple.predicate) continue;
|
|
31
|
+
if (candidateTriple.object === existingTriple.object) {
|
|
32
|
+
return {
|
|
33
|
+
kind: 'dedup',
|
|
34
|
+
existingId: hit.record.id,
|
|
35
|
+
reason: 'subject-predicate-match-same-object',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
kind: 'supersede',
|
|
40
|
+
existingId: hit.record.id,
|
|
41
|
+
reason: `subject-predicate-match; subject=${candidateTriple.subject}; predicate=${candidateTriple.predicate}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { kind: 'continue' };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 5 - Defer to deep LLM judge.
|
|
3
|
+
*
|
|
4
|
+
* Reached only when Stages 1-4 left the decision unresolved. The
|
|
5
|
+
* stage records every CONFLICT-CHECK candidate id surfaced by Stage 2
|
|
6
|
+
* so the orchestrator can hand the queue write to the storage layer
|
|
7
|
+
* (the actual `conflict_check_pending` insert is the orchestrator's
|
|
8
|
+
* responsibility - Stage 5 only computes the payload).
|
|
9
|
+
*
|
|
10
|
+
* The candidate fact itself is admitted with a `pending` flag so it
|
|
11
|
+
* remains searchable; the deep phase (Phase 10c) drains the queue and
|
|
12
|
+
* applies the resolution later.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { PipelineStage, StageOutcome } from '../types.js';
|
|
18
|
+
|
|
19
|
+
export const stage5DeferToDeep: PipelineStage = {
|
|
20
|
+
id: 'defer-to-deep',
|
|
21
|
+
async evaluate(ctx): Promise<StageOutcome> {
|
|
22
|
+
if (ctx.existing.length === 0) {
|
|
23
|
+
return { kind: 'admit', reason: 'stage5-no-candidates' };
|
|
24
|
+
}
|
|
25
|
+
const conflictingIds = ctx.existing.map((hit) => hit.record.id);
|
|
26
|
+
const top = ctx.existing[0];
|
|
27
|
+
return {
|
|
28
|
+
kind: 'pending',
|
|
29
|
+
conflictingIds,
|
|
30
|
+
...(top !== undefined ? { similarity: top.score } : {}),
|
|
31
|
+
reason: 'awaiting-deep-llm-judge',
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the multi-stage conflict resolution pipeline shipped
|
|
3
|
+
* in `@graphorin/memory` Phase 10b (DEC-117 / ADR-018 ext / RB-02).
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EmbedderProvider, Fact, MemoryHit, Tracer } from '@graphorin/core';
|
|
9
|
+
import type { ConflictMemoryStoreExt, MemoryStoreAdapter } from '../internal/storage-adapter.js';
|
|
10
|
+
import type { LocalePack } from './locale-packs/index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Stable lowercase identifier of the pipeline stage. Mirrored
|
|
14
|
+
* byte-for-byte by `@graphorin/store-sqlite`'s `ConflictPipelineStage`
|
|
15
|
+
* so `fact_conflicts` / `conflict_check_pending` rows can be grouped
|
|
16
|
+
* deterministically.
|
|
17
|
+
*
|
|
18
|
+
* @stable
|
|
19
|
+
*/
|
|
20
|
+
export type ConflictStage =
|
|
21
|
+
| 'exact-dedup'
|
|
22
|
+
| 'embedding-three-zone'
|
|
23
|
+
| 'heuristic-regex'
|
|
24
|
+
| 'subject-predicate'
|
|
25
|
+
| 'defer-to-deep';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Configurable similarity thresholds for Stage 2 (embedding three-
|
|
29
|
+
* zone). Defaults pinned at `0.95 / 0.85 / 0.4` per RB-02 §8 - the
|
|
30
|
+
* production values for the default `Xenova/multilingual-e5-base`
|
|
31
|
+
* embedder (DEC-130).
|
|
32
|
+
*
|
|
33
|
+
* @stable
|
|
34
|
+
*/
|
|
35
|
+
export interface ConflictThresholds {
|
|
36
|
+
readonly hot: number;
|
|
37
|
+
readonly nearDup: number;
|
|
38
|
+
readonly cold: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hard-coded defaults for the three-zone thresholds. Imported by the
|
|
43
|
+
* pipeline + the test suite so call sites stay aligned.
|
|
44
|
+
*
|
|
45
|
+
* @stable
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds = Object.freeze({
|
|
48
|
+
hot: 0.95,
|
|
49
|
+
nearDup: 0.85,
|
|
50
|
+
cold: 0.4,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Final pipeline outcome - discriminated union returned by
|
|
55
|
+
* {@link runConflictPipeline}. Mirrors RB-02 §8.1 / DEC-117 - every
|
|
56
|
+
* variant carries the originating `stage` so audit + replay tooling
|
|
57
|
+
* can pattern-match without inspecting the message.
|
|
58
|
+
*
|
|
59
|
+
* @stable
|
|
60
|
+
*/
|
|
61
|
+
export type ConflictDecision =
|
|
62
|
+
| {
|
|
63
|
+
readonly kind: 'admit';
|
|
64
|
+
readonly stage: ConflictStage;
|
|
65
|
+
readonly reason?: string;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: 'dedup';
|
|
69
|
+
readonly stage: ConflictStage;
|
|
70
|
+
readonly existingId: string;
|
|
71
|
+
readonly similarity?: number;
|
|
72
|
+
readonly reason?: string;
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
readonly kind: 'supersede';
|
|
76
|
+
readonly stage: ConflictStage;
|
|
77
|
+
readonly existingId: string;
|
|
78
|
+
readonly reason: string;
|
|
79
|
+
}
|
|
80
|
+
| {
|
|
81
|
+
readonly kind: 'pending';
|
|
82
|
+
readonly stage: ConflictStage;
|
|
83
|
+
/** The candidate id that was admitted as `pending`. */
|
|
84
|
+
readonly candidateId: string;
|
|
85
|
+
/** Top-K conflicting existing fact ids from Stage 2's vector search. */
|
|
86
|
+
readonly conflictingIds: ReadonlyArray<string>;
|
|
87
|
+
readonly similarity?: number;
|
|
88
|
+
readonly reason?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Pipeline configuration accepted by `createMemory({ conflictPipeline:
|
|
93
|
+
* ... })` and surfaced through {@link createConflictPipeline}.
|
|
94
|
+
*
|
|
95
|
+
* The `mode` field is the master switch:
|
|
96
|
+
*
|
|
97
|
+
* - `'on'` (default) - the multi-stage pipeline runs on every
|
|
98
|
+
* `SemanticMemory.remember(...)` call.
|
|
99
|
+
* - `'off'` - bypass the pipeline and fall back to 10a's straight-
|
|
100
|
+
* through write. Emits a one-shot WARN (per process) so operators
|
|
101
|
+
* notice the regression risk.
|
|
102
|
+
*
|
|
103
|
+
* @stable
|
|
104
|
+
*/
|
|
105
|
+
export interface ConflictPipelineOptions {
|
|
106
|
+
readonly mode?: 'on' | 'off';
|
|
107
|
+
readonly thresholds?: Partial<ConflictThresholds>;
|
|
108
|
+
readonly localePack?: LocalePack;
|
|
109
|
+
/** Per-list candidate count fed into Stage 2. Default `5` (RB-02 §8.1). */
|
|
110
|
+
readonly stage2TopK?: number;
|
|
111
|
+
/** Override the audit / pending sink. Defaults to `store.conflicts` when present. */
|
|
112
|
+
readonly conflictStore?: ConflictMemoryStoreExt;
|
|
113
|
+
/** Inject a deterministic clock. Defaults to `() => new Date().toISOString()`. */
|
|
114
|
+
readonly now?: () => string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Inputs the orchestrator hands every stage. The `existing` array is
|
|
119
|
+
* populated during Stage 2 (vector search top-K); Stage 1 receives an
|
|
120
|
+
* empty array because the dedup hash is computed off the candidate
|
|
121
|
+
* alone.
|
|
122
|
+
*
|
|
123
|
+
* @stable
|
|
124
|
+
*/
|
|
125
|
+
export interface StageContext {
|
|
126
|
+
readonly candidate: Fact;
|
|
127
|
+
readonly existing: ReadonlyArray<MemoryHit<Fact>>;
|
|
128
|
+
readonly localePack: LocalePack;
|
|
129
|
+
readonly thresholds: ConflictThresholds;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Per-stage outcome surfaced to the orchestrator. `'admit'` means the
|
|
134
|
+
* stage decided not to short-circuit - the pipeline continues to the
|
|
135
|
+
* next stage. Every other variant terminates the pipeline.
|
|
136
|
+
*
|
|
137
|
+
* @stable
|
|
138
|
+
*/
|
|
139
|
+
export type StageOutcome =
|
|
140
|
+
| { readonly kind: 'continue' }
|
|
141
|
+
| { readonly kind: 'admit'; readonly reason?: string }
|
|
142
|
+
| {
|
|
143
|
+
readonly kind: 'dedup';
|
|
144
|
+
readonly existingId: string;
|
|
145
|
+
readonly similarity?: number;
|
|
146
|
+
readonly reason?: string;
|
|
147
|
+
}
|
|
148
|
+
| {
|
|
149
|
+
readonly kind: 'supersede';
|
|
150
|
+
readonly existingId: string;
|
|
151
|
+
readonly reason: string;
|
|
152
|
+
}
|
|
153
|
+
| {
|
|
154
|
+
readonly kind: 'pending';
|
|
155
|
+
readonly conflictingIds: ReadonlyArray<string>;
|
|
156
|
+
readonly similarity?: number;
|
|
157
|
+
readonly reason?: string;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Stage interface every step implements. The orchestrator visits
|
|
162
|
+
* stages in declaration order and stops at the first non-`'continue'`
|
|
163
|
+
* outcome.
|
|
164
|
+
*
|
|
165
|
+
* @stable
|
|
166
|
+
*/
|
|
167
|
+
export interface PipelineStage {
|
|
168
|
+
readonly id: ConflictStage;
|
|
169
|
+
evaluate(ctx: StageContext): Promise<StageOutcome>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Pre-built pipeline returned by {@link createConflictPipeline}. The
|
|
174
|
+
* `run` method is what `SemanticMemory.remember(...)` calls to make
|
|
175
|
+
* the conflict decision.
|
|
176
|
+
*
|
|
177
|
+
* @stable
|
|
178
|
+
*/
|
|
179
|
+
export interface ConflictPipeline {
|
|
180
|
+
readonly mode: 'on' | 'off';
|
|
181
|
+
readonly localePack: LocalePack;
|
|
182
|
+
readonly thresholds: ConflictThresholds;
|
|
183
|
+
run(deps: ConflictPipelineDeps, candidate: Fact): Promise<ConflictDecision>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Per-call dependency bag handed to the pipeline by `SemanticMemory`.
|
|
188
|
+
*
|
|
189
|
+
* @stable
|
|
190
|
+
*/
|
|
191
|
+
export interface ConflictPipelineDeps {
|
|
192
|
+
readonly store: MemoryStoreAdapter;
|
|
193
|
+
readonly tracer: Tracer;
|
|
194
|
+
readonly embedder: EmbedderProvider | null;
|
|
195
|
+
readonly embedderId: string | null;
|
|
196
|
+
/** Optional cancellation signal forwarded to embedder + searchVector. */
|
|
197
|
+
readonly signal?: AbortSignal;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The four actions the neighbour-aware reconcile loop (P0-3) may choose
|
|
202
|
+
* for a candidate fact once the LLM has the most-similar existing
|
|
203
|
+
* memories in view. The de-facto-standard memory write loop
|
|
204
|
+
* (Mem0 / LangMem / Letta), with Graphorin's twist: `update` and
|
|
205
|
+
* `conflict` route through a **bi-temporal supersede** (close the old
|
|
206
|
+
* interval, insert the new) rather than a destructive delete.
|
|
207
|
+
*
|
|
208
|
+
* @stable
|
|
209
|
+
*/
|
|
210
|
+
export type ReconcileAction = 'add' | 'update' | 'noop' | 'conflict';
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Outcome of `reconcileCandidate`. `add` is independent / unsure
|
|
214
|
+
* (a fresh fact); `noop` is a duplicate that adds nothing; `update` is
|
|
215
|
+
* a newer version of `targetId`; `conflict` contradicts `targetId` and
|
|
216
|
+
* closes it. The three neighbour-referencing variants carry the
|
|
217
|
+
* `targetId` of the existing fact they resolve against.
|
|
218
|
+
*
|
|
219
|
+
* @stable
|
|
220
|
+
*/
|
|
221
|
+
export type ReconcileDecision =
|
|
222
|
+
| { readonly action: 'add'; readonly reason?: string }
|
|
223
|
+
| { readonly action: 'update'; readonly targetId: string; readonly reason: string }
|
|
224
|
+
| { readonly action: 'noop'; readonly targetId: string; readonly reason?: string }
|
|
225
|
+
| { readonly action: 'conflict'; readonly targetId: string; readonly reason: string };
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Map a {@link ReconcileDecision} onto the existing {@link ConflictDecision}
|
|
229
|
+
* so reconcile outcomes land in `fact_conflicts` through the same audit
|
|
230
|
+
* path as the inline pipeline (no new stage / schema): `add` → `admit`,
|
|
231
|
+
* `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile
|
|
232
|
+
* decisions are stamped with the `defer-to-deep` stage - the reconcile
|
|
233
|
+
* loop is the consolidator's replacement for the deferred deep-judge
|
|
234
|
+
* step it supersedes.
|
|
235
|
+
*
|
|
236
|
+
* @stable
|
|
237
|
+
*/
|
|
238
|
+
export function reconcileToConflictDecision(decision: ReconcileDecision): ConflictDecision {
|
|
239
|
+
const stage: ConflictStage = 'defer-to-deep';
|
|
240
|
+
switch (decision.action) {
|
|
241
|
+
case 'add':
|
|
242
|
+
return {
|
|
243
|
+
kind: 'admit',
|
|
244
|
+
stage,
|
|
245
|
+
...(decision.reason !== undefined ? { reason: decision.reason } : {}),
|
|
246
|
+
};
|
|
247
|
+
case 'noop':
|
|
248
|
+
return {
|
|
249
|
+
kind: 'dedup',
|
|
250
|
+
stage,
|
|
251
|
+
existingId: decision.targetId,
|
|
252
|
+
...(decision.reason !== undefined ? { reason: decision.reason } : {}),
|
|
253
|
+
};
|
|
254
|
+
case 'update':
|
|
255
|
+
case 'conflict':
|
|
256
|
+
return {
|
|
257
|
+
kind: 'supersede',
|
|
258
|
+
stage,
|
|
259
|
+
existingId: decision.targetId,
|
|
260
|
+
reason: decision.reason,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|