@oscharko-dev/keiko 0.2.0-beta.6 → 0.2.0-beta.8
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/dist/ui/csp-hashes.json +14 -14
- package/dist/ui/static/404.html +1 -1
- package/dist/ui/static/__next.__PAGE__.txt +2 -2
- package/dist/ui/static/__next._full.txt +3 -3
- package/dist/ui/static/__next._head.txt +1 -1
- package/dist/ui/static/__next._index.txt +2 -2
- package/dist/ui/static/__next._tree.txt +2 -2
- package/dist/ui/static/_next/static/chunks/0i3jzgrj42so8.css +1 -0
- package/dist/ui/static/_next/static/chunks/1ru_021szp0u7.js +1 -0
- package/dist/ui/static/_next/static/chunks/1t7vb5d9ed2e7.js +1 -0
- package/dist/ui/static/_next/static/chunks/23o2c6pyjq92z.js +109 -0
- package/dist/ui/static/_not-found/__next._full.txt +2 -2
- package/dist/ui/static/_not-found/__next._head.txt +1 -1
- package/dist/ui/static/_not-found/__next._index.txt +2 -2
- package/dist/ui/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/dist/ui/static/_not-found/__next._not-found.txt +1 -1
- package/dist/ui/static/_not-found/__next._tree.txt +2 -2
- package/dist/ui/static/_not-found.html +1 -1
- package/dist/ui/static/_not-found.txt +2 -2
- package/dist/ui/static/fonts/OFL.txt +93 -0
- package/dist/ui/static/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/dist/ui/static/index.html +1 -1
- package/dist/ui/static/index.txt +3 -3
- package/dist/ui/static/launch/__next._full.txt +3 -3
- package/dist/ui/static/launch/__next._head.txt +1 -1
- package/dist/ui/static/launch/__next._index.txt +2 -2
- package/dist/ui/static/launch/__next._tree.txt +2 -2
- package/dist/ui/static/launch/__next.launch.__PAGE__.txt +2 -2
- package/dist/ui/static/launch/__next.launch.txt +1 -1
- package/dist/ui/static/launch.html +1 -1
- package/dist/ui/static/launch.txt +3 -3
- package/dist/ui/static/local-knowledge/__next._full.txt +3 -3
- package/dist/ui/static/local-knowledge/__next._head.txt +1 -1
- package/dist/ui/static/local-knowledge/__next._index.txt +2 -2
- package/dist/ui/static/local-knowledge/__next._tree.txt +2 -2
- package/dist/ui/static/local-knowledge/__next.local-knowledge.__PAGE__.txt +2 -2
- package/dist/ui/static/local-knowledge/__next.local-knowledge.txt +1 -1
- package/dist/ui/static/local-knowledge/capsule/__next._full.txt +3 -3
- package/dist/ui/static/local-knowledge/capsule/__next._head.txt +1 -1
- package/dist/ui/static/local-knowledge/capsule/__next._index.txt +2 -2
- package/dist/ui/static/local-knowledge/capsule/__next._tree.txt +2 -2
- package/dist/ui/static/local-knowledge/capsule/__next.local-knowledge.capsule.__PAGE__.txt +2 -2
- package/dist/ui/static/local-knowledge/capsule/__next.local-knowledge.capsule.txt +1 -1
- package/dist/ui/static/local-knowledge/capsule/__next.local-knowledge.txt +1 -1
- package/dist/ui/static/local-knowledge/capsule.html +1 -1
- package/dist/ui/static/local-knowledge/capsule.txt +3 -3
- package/dist/ui/static/local-knowledge.html +1 -1
- package/dist/ui/static/local-knowledge.txt +3 -3
- package/dist/ui/static/memoriaviva/__next._full.txt +2 -2
- package/dist/ui/static/memoriaviva/__next._head.txt +1 -1
- package/dist/ui/static/memoriaviva/__next._index.txt +2 -2
- package/dist/ui/static/memoriaviva/__next._tree.txt +2 -2
- package/dist/ui/static/memoriaviva/__next.memoriaviva.__PAGE__.txt +1 -1
- package/dist/ui/static/memoriaviva/__next.memoriaviva.txt +1 -1
- package/dist/ui/static/memoriaviva/consolidation/__next._full.txt +2 -2
- package/dist/ui/static/memoriaviva/consolidation/__next._head.txt +1 -1
- package/dist/ui/static/memoriaviva/consolidation/__next._index.txt +2 -2
- package/dist/ui/static/memoriaviva/consolidation/__next._tree.txt +2 -2
- package/dist/ui/static/memoriaviva/consolidation/__next.memoriaviva.consolidation.__PAGE__.txt +1 -1
- package/dist/ui/static/memoriaviva/consolidation/__next.memoriaviva.consolidation.txt +1 -1
- package/dist/ui/static/memoriaviva/consolidation/__next.memoriaviva.txt +1 -1
- package/dist/ui/static/memoriaviva/consolidation.html +1 -1
- package/dist/ui/static/memoriaviva/consolidation.txt +2 -2
- package/dist/ui/static/memoriaviva/detail/__next._full.txt +2 -2
- package/dist/ui/static/memoriaviva/detail/__next._head.txt +1 -1
- package/dist/ui/static/memoriaviva/detail/__next._index.txt +2 -2
- package/dist/ui/static/memoriaviva/detail/__next._tree.txt +2 -2
- package/dist/ui/static/memoriaviva/detail/__next.memoriaviva.detail.__PAGE__.txt +1 -1
- package/dist/ui/static/memoriaviva/detail/__next.memoriaviva.detail.txt +1 -1
- package/dist/ui/static/memoriaviva/detail/__next.memoriaviva.txt +1 -1
- package/dist/ui/static/memoriaviva/detail.html +1 -1
- package/dist/ui/static/memoriaviva/detail.txt +2 -2
- package/dist/ui/static/memoriaviva/review-queue/__next._full.txt +2 -2
- package/dist/ui/static/memoriaviva/review-queue/__next._head.txt +1 -1
- package/dist/ui/static/memoriaviva/review-queue/__next._index.txt +2 -2
- package/dist/ui/static/memoriaviva/review-queue/__next._tree.txt +2 -2
- package/dist/ui/static/memoriaviva/review-queue/__next.memoriaviva.review-queue.__PAGE__.txt +1 -1
- package/dist/ui/static/memoriaviva/review-queue/__next.memoriaviva.review-queue.txt +1 -1
- package/dist/ui/static/memoriaviva/review-queue/__next.memoriaviva.txt +1 -1
- package/dist/ui/static/memoriaviva/review-queue.html +1 -1
- package/dist/ui/static/memoriaviva/review-queue.txt +2 -2
- package/dist/ui/static/memoriaviva.html +1 -1
- package/dist/ui/static/memoriaviva.txt +2 -2
- package/dist/ui/static/sw.js +7 -3
- package/node_modules/@oscharko-dev/keiko-cli/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-cli/dist/memory.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-cli/dist/memory.js +4 -5
- package/node_modules/@oscharko-dev/keiko-cli/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-contracts/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-contracts/dist/index.d.ts +1 -1
- package/node_modules/@oscharko-dev/keiko-contracts/dist/index.js +1 -1
- package/node_modules/@oscharko-dev/keiko-contracts/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-evaluations/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-evaluations/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-evidence/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-evidence/dist/qualityIntelligence/figmaSnapshot/store.d.ts +5 -0
- package/node_modules/@oscharko-dev/keiko-evidence/dist/qualityIntelligence/figmaSnapshot/store.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-evidence/dist/qualityIntelligence/figmaSnapshot/store.js +16 -0
- package/node_modules/@oscharko-dev/keiko-evidence/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-harness/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-harness/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-local-knowledge/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-local-knowledge/dist/parsers/pdf-parser.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-local-knowledge/dist/parsers/pdf-parser.js +0 -10
- package/node_modules/@oscharko-dev/keiko-local-knowledge/dist/parsers/types.d.ts +2 -2
- package/node_modules/@oscharko-dev/keiko-local-knowledge/dist/parsers/types.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-local-knowledge/dist/parsers/types.js +3 -3
- package/node_modules/@oscharko-dev/keiko-local-knowledge/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-capture/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-consolidation/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/index.d.ts +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/index.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/index.js +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/maintenance.d.ts +2 -16
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/maintenance.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/maintenance.js +49 -48
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/retention.d.ts +2 -1
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/retention.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-governance/dist/retention.js +15 -0
- package/node_modules/@oscharko-dev/keiko-memory-governance/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/decay.d.ts +2 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/decay.d.ts.map +1 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/decay.js +22 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/diversity.d.ts +8 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/diversity.d.ts.map +1 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/diversity.js +87 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/index.d.ts +4 -2
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/index.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/index.js +4 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/ranking.d.ts +5 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/ranking.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/ranking.js +140 -21
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/recency.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/recency.js +4 -10
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/retrieve.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/retrieve.js +11 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/strength.d.ts +9 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/strength.d.ts.map +1 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/strength.js +51 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/types.d.ts +11 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/types.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/dist/types.js +7 -0
- package/node_modules/@oscharko-dev/keiko-memory-retrieval/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/access.d.ts +3 -0
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/access.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/access.js +31 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/schema.d.ts +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/schema.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/schema.js +16 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/types.d.ts +1 -0
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/types.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/vault.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/dist/vault.js +4 -1
- package/node_modules/@oscharko-dev/keiko-memory-vault/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-model-gateway/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-model-gateway/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-quality-intelligence/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-quality-intelligence/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-sdk/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-sdk/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-security/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-security/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/chat-handlers.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/chat-handlers.js +44 -65
- package/node_modules/@oscharko-dev/keiko-server/dist/deps.d.ts +2 -0
- package/node_modules/@oscharko-dev/keiko-server/dist/deps.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/deps.js +1 -0
- package/node_modules/@oscharko-dev/keiko-server/dist/gateway-setup.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/gateway-setup.js +348 -64
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-conv-handlers.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-conv-handlers.js +34 -5
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-embedding.d.ts +12 -2
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-embedding.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-embedding.js +127 -0
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-handlers.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-handlers.js +40 -11
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-maintenance-handlers.d.ts +16 -3
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-maintenance-handlers.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-maintenance-handlers.js +72 -50
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-retrieval-signals.d.ts +12 -0
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-retrieval-signals.d.ts.map +1 -0
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-retrieval-signals.js +84 -0
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-salience.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/memory-salience.js +11 -6
- package/node_modules/@oscharko-dev/keiko-server/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +15 -0
- package/node_modules/@oscharko-dev/keiko-server/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/qualityIntelligence/figmaSnapshotRoutes.js +105 -0
- package/node_modules/@oscharko-dev/keiko-server/dist/routes.d.ts.map +1 -1
- package/node_modules/@oscharko-dev/keiko-server/dist/routes.js +2 -1
- package/node_modules/@oscharko-dev/keiko-server/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-tools/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-tools/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-verification/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-verification/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-workflows/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-workflows/package.json +1 -1
- package/node_modules/@oscharko-dev/keiko-workspace/dist/.tsbuildinfo +1 -1
- package/node_modules/@oscharko-dev/keiko-workspace/package.json +1 -1
- package/package.json +2 -1
- package/dist/ui/static/_next/static/chunks/0-qhhdvxg2j_y.js +0 -1
- package/dist/ui/static/_next/static/chunks/0ke4ratkgvcxo.css +0 -1
- package/dist/ui/static/_next/static/chunks/3vf3oh2-sl2nc.js +0 -1
- package/dist/ui/static/_next/static/chunks/3wmd4-2vznp2g.js +0 -106
- /package/dist/ui/static/_next/static/{fQMXe8UmV01bh25WOoIt3 → Ppze_8n_i3yc1FS_Qdj0I}/_buildManifest.js +0 -0
- /package/dist/ui/static/_next/static/{fQMXe8UmV01bh25WOoIt3 → Ppze_8n_i3yc1FS_Qdj0I}/_clientMiddlewareManifest.js +0 -0
- /package/dist/ui/static/_next/static/{fQMXe8UmV01bh25WOoIt3 → Ppze_8n_i3yc1FS_Qdj0I}/_ssgManifest.js +0 -0
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// function is inert (embedMemoryText -> null, embedAndStoreMemory -> no-op) and the caller keeps
|
|
15
15
|
// its pre-semantic behaviour byte-for-byte.
|
|
16
16
|
import { requestOpenAIEmbedding, } from "@oscharko-dev/keiko-model-gateway";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
17
18
|
import { currentGatewayConfig } from "./deps.js";
|
|
18
19
|
import { selectEmbeddingModelId } from "./local-knowledge-handlers.js";
|
|
19
20
|
const MEMORY_VECTOR_METRIC = "cosine";
|
|
@@ -135,3 +136,129 @@ export function cosineSimilarity(a, b) {
|
|
|
135
136
|
return 0;
|
|
136
137
|
return cosine > 1 ? 1 : cosine;
|
|
137
138
|
}
|
|
139
|
+
// ─── Semantic novelty gate at capture (#204, O-F1) ──────────────────────────────
|
|
140
|
+
// Lexical Jaccard dedup at capture misses semantic restatements ("I use Postgres" vs "my database is
|
|
141
|
+
// PostgreSQL"). This catches them BEFORE a second copy is stored: embed the candidate once, compare
|
|
142
|
+
// to the in-scope stored vectors, and if it is NEAR-IDENTICAL to an existing memory, reinforce that
|
|
143
|
+
// canonical memory instead of duplicating it.
|
|
144
|
+
//
|
|
145
|
+
// SAFETY: the threshold is deliberately HIGH (0.95) and the gate is applied ONLY to the low-stakes
|
|
146
|
+
// salience firehose — NOT to explicit user instructions. A pure cosine signal cannot tell a
|
|
147
|
+
// paraphrase ("uses PostgreSQL") from a value-change ("region is eu-central-1" vs "us-east-1"), so a
|
|
148
|
+
// lower threshold could merge a contradicting update. At 0.95 only near-verbatim restatements merge;
|
|
149
|
+
// value-changes (differing entities/numbers) stay below it and are inserted normally, where
|
|
150
|
+
// consolidation's conflict detection backstops them. Merging reinforces the canonical rather than
|
|
151
|
+
// deleting, so even a false merge loses no stored fact. Graceful: with no embedder the candidate
|
|
152
|
+
// embedding is null and the gate is inert (prior lexical-only behaviour, byte-for-byte).
|
|
153
|
+
export const SEMANTIC_DEDUP_COSINE_THRESHOLD = 0.95;
|
|
154
|
+
// Pure: the id of the nearest in-scope memory whose cosine to the candidate is at/above the
|
|
155
|
+
// threshold, or null (no candidate embedding, no neighbours, or none similar enough). First-max wins
|
|
156
|
+
// on ties so the result is deterministic for a fixed neighbour iteration order.
|
|
157
|
+
export function findSemanticDuplicate(candidate, neighbors, threshold = SEMANTIC_DEDUP_COSINE_THRESHOLD) {
|
|
158
|
+
if (candidate === null)
|
|
159
|
+
return null;
|
|
160
|
+
let bestId = null;
|
|
161
|
+
let bestSim = -1;
|
|
162
|
+
for (const [id, row] of neighbors) {
|
|
163
|
+
const sim = cosineSimilarity(candidate.vector, row.vector);
|
|
164
|
+
if (sim > bestSim) {
|
|
165
|
+
bestSim = sim;
|
|
166
|
+
bestId = id;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return bestSim >= threshold ? bestId : null;
|
|
170
|
+
}
|
|
171
|
+
// ─── Semantic auto-linking at capture (#204, O-P4) ──────────────────────────────
|
|
172
|
+
// A-MEM (2502.12110) self-organises memory by LINKING a new note to its semantic neighbours at
|
|
173
|
+
// write time, so associative recall has structure to traverse immediately. Memoria Viva already
|
|
174
|
+
// runs graph-proximity recall live (the ranker's `graph` subscore), but until now the only edges
|
|
175
|
+
// were supersedes/correction links and the batch consolidation pass — a fresh capture had no
|
|
176
|
+
// associations. This forms them deterministically from the SAME embedding used for the novelty
|
|
177
|
+
// gate: a novel capture is linked to its nearest in-scope neighbours that fall in a "related" band
|
|
178
|
+
// (similar enough to associate, below the dedup threshold so they are not the same fact). No model
|
|
179
|
+
// call, no non-determinism — a pure cosine band over vectors already in hand.
|
|
180
|
+
// Lower bound of the related band. Above this two memory bodies are topically associated; below it
|
|
181
|
+
// the link would be noise. Conservative starting point for text-embedding-3-large; tunable per the
|
|
182
|
+
// opt-in flag before it is switched on by default.
|
|
183
|
+
export const RELATED_LINK_COSINE_THRESHOLD = 0.82;
|
|
184
|
+
// At most this many associations per capture, so the graph stays sparse and traversal stays cheap.
|
|
185
|
+
export const MAX_AUTO_LINKS = 3;
|
|
186
|
+
// Pure: the in-scope neighbours whose cosine to the candidate falls in the band [lower, upper) —
|
|
187
|
+
// associated but not a duplicate — ranked by similarity desc (id asc tiebreak) and capped at
|
|
188
|
+
// maxLinks. Empty for a null candidate or when nothing lands in the band. Deterministic for a fixed
|
|
189
|
+
// neighbour set.
|
|
190
|
+
export function findRelatedNeighbors(candidate, neighbors, lower = RELATED_LINK_COSINE_THRESHOLD, upper = SEMANTIC_DEDUP_COSINE_THRESHOLD, maxLinks = MAX_AUTO_LINKS) {
|
|
191
|
+
if (candidate === null)
|
|
192
|
+
return [];
|
|
193
|
+
const scored = [];
|
|
194
|
+
for (const [id, row] of neighbors) {
|
|
195
|
+
const similarity = cosineSimilarity(candidate.vector, row.vector);
|
|
196
|
+
if (similarity >= lower && similarity < upper)
|
|
197
|
+
scored.push({ id, similarity });
|
|
198
|
+
}
|
|
199
|
+
scored.sort((a, b) => a.similarity !== b.similarity ? b.similarity - a.similarity : a.id.localeCompare(b.id));
|
|
200
|
+
return scored.slice(0, maxLinks).map((n) => n.id);
|
|
201
|
+
}
|
|
202
|
+
// Opt-in (KEIKO_MEMORY_AUTO_LINK=1, default off => byte-identical: no edges, no behaviour change).
|
|
203
|
+
// Best-effort: a rejected edge insert must never break the capture that already succeeded.
|
|
204
|
+
function autoLinkRelatedMemories(deps, vault, fromId, embedding, neighbors) {
|
|
205
|
+
if (deps.env.KEIKO_MEMORY_AUTO_LINK !== "1")
|
|
206
|
+
return;
|
|
207
|
+
const relatedIds = findRelatedNeighbors(embedding, neighbors);
|
|
208
|
+
if (relatedIds.length === 0)
|
|
209
|
+
return;
|
|
210
|
+
const nowMs = Date.now();
|
|
211
|
+
for (const toId of relatedIds) {
|
|
212
|
+
try {
|
|
213
|
+
vault.insertEdge({
|
|
214
|
+
id: randomUUID(),
|
|
215
|
+
schemaVersion: "1",
|
|
216
|
+
fromMemoryId: fromId,
|
|
217
|
+
toMemoryId: toId,
|
|
218
|
+
kind: "related",
|
|
219
|
+
createdAt: nowMs,
|
|
220
|
+
provenanceSummary: "semantic auto-link",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Validator / storage rejection — association enrichment is best-effort, never fatal.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Bounds the neighbour set so the cosine sweep stays cheap on a large vault (mirrors the lexical
|
|
229
|
+
// dedup corpus bound). Scope-local list preserves cross-scope isolation.
|
|
230
|
+
const MAX_DEDUP_NEIGHBORS = 200;
|
|
231
|
+
function gatherScopeEmbeddings(vault, scope) {
|
|
232
|
+
const ids = vault
|
|
233
|
+
.listMemoriesByScope(scope)
|
|
234
|
+
.slice(0, MAX_DEDUP_NEIGHBORS)
|
|
235
|
+
.map((record) => record.id);
|
|
236
|
+
return ids.length === 0 ? new Map() : vault.getEmbeddings(ids);
|
|
237
|
+
}
|
|
238
|
+
// Insert a freshly-built salience capture record UNLESS it is a semantic near-duplicate of an
|
|
239
|
+
// existing in-scope memory, in which case reinforce the canonical (recordAccess) and skip the
|
|
240
|
+
// duplicate. Embeds the body exactly ONCE (reused for both the novelty check and storage), so this
|
|
241
|
+
// replaces — not adds to — the prior best-effort embed-on-capture call. Never throws past the
|
|
242
|
+
// vault's own guards; a null embedding degrades to a plain insert.
|
|
243
|
+
export async function insertSalienceMemoryWithNoveltyGate(deps, vault, record) {
|
|
244
|
+
const embedding = await embedMemoryText(deps, record.body);
|
|
245
|
+
const neighbors = gatherScopeEmbeddings(vault, record.scope);
|
|
246
|
+
const duplicateOf = findSemanticDuplicate(embedding, neighbors);
|
|
247
|
+
if (duplicateOf !== null) {
|
|
248
|
+
vault.recordAccess([duplicateOf], Date.now());
|
|
249
|
+
return { inserted: null, mergedInto: duplicateOf };
|
|
250
|
+
}
|
|
251
|
+
const inserted = vault.insertMemory(record);
|
|
252
|
+
if (embedding !== null) {
|
|
253
|
+
try {
|
|
254
|
+
vault.upsertEmbedding(inserted.id, embedding);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// gateEmbeddingInput / storage rejection — capture already succeeded; drop the embedding.
|
|
258
|
+
}
|
|
259
|
+
// A-MEM-style associative linking (#204, O-P4). Reuses the neighbour set already fetched for the
|
|
260
|
+
// novelty gate — no extra IO. Opt-in (default off => no edges, byte-identical).
|
|
261
|
+
autoLinkRelatedMemories(deps, vault, inserted.id, embedding, neighbors);
|
|
262
|
+
}
|
|
263
|
+
return { inserted, mergedInto: null };
|
|
264
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-handlers.d.ts","sourceRoot":"","sources":["../src/memory-handlers.ts"],"names":[],"mappings":"AAkBA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"memory-handlers.d.ts","sourceRoot":"","sources":["../src/memory-handlers.ts"],"names":[],"mappings":"AAkBA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,kCAAkC,CAAC;AAqC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAAY,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA6SvE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,GAAG,WAAW,CA8BtF;AAID,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,GAAG,WAAW,CAmB5F;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,GAAG,WAAW,CAqBnF;AAwDD,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,WAAW,CAAC,CA+BtB;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,GAAG,WAAW,CA6BnF;AAID,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,GAAG,WAAW,CA6BrF;AAID,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,WAAW,CAAC,CAsCtB;AAkND,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,WAAW,CAAC,CA0BtB;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,WAAW,CAAC,CAiBtB;AAKD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,WAAW,CAAC,CAkCtB;AAkND,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,WAAW,CAAC,CAiBtB;AA8GD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,WAAW,CAAC,CA8CtB;AAyJD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,GAAG,WAAW,CAc9F;AAyBD,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,WAAW,CAAC,CAoCtB;AAOD,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EACnC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,kCAAkC,EAAE,WAAW,KAAK,IAAI,EACvF,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GACjD,gBAAgB,CASlB"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Every response is redacted through `deps.redactor` before serialisation to honour D9.
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
17
17
|
import { createMemoryVault, MemoryStorageError, } from "@oscharko-dev/keiko-memory-vault";
|
|
18
|
-
import { GovernanceError, buildArchiveOperation, buildConflictTransitions, buildCorrection, buildForgetOperations, buildPinOperation, buildUnpinOperation, detectConflictPair, selectMemoriesForForget, } from "@oscharko-dev/keiko-memory-governance";
|
|
18
|
+
import { GovernanceError, buildArchiveOperation, buildConflictTransitions, buildCorrection, buildForgetOperations, buildPinOperation, buildUnpinOperation, detectConflictPair, selectMemoriesForForget, supersededValidity, } from "@oscharko-dev/keiko-memory-governance";
|
|
19
19
|
import { checkStatusTransition, MEMORY_SCOPE_KINDS, MEMORY_STATUSES, MEMORY_TYPES, MEMORY_SENSITIVITIES, validateMemoryScope, } from "@oscharko-dev/keiko-contracts";
|
|
20
20
|
import { errorBody } from "./routes.js";
|
|
21
21
|
import { auditRunIdFor, recordMemoryAudit } from "./memory-audit-handler.js";
|
|
@@ -826,7 +826,12 @@ function findMemoryById(memories, id) {
|
|
|
826
826
|
}
|
|
827
827
|
function persistConflictTransitions(vault, resolution, reason) {
|
|
828
828
|
for (const transition of resolution.statusTransitions) {
|
|
829
|
-
|
|
829
|
+
// Bi-temporal-lite (#204, C1): a record losing a conflict and being SUPERSEDED gets its belief
|
|
830
|
+
// window closed at the transition time, same as the correction path. Other transitions (e.g.
|
|
831
|
+
// the winner re-accepted) leave validity untouched.
|
|
832
|
+
const existing = transition.to === "superseded" ? vault.getMemory(transition.memoryId) : undefined;
|
|
833
|
+
const validity = existing !== undefined ? supersededValidity(existing, transition.transitionedAt) : null;
|
|
834
|
+
vault.updateMemory(transition.memoryId, { status: transition.to, staleReason: reason, ...(validity !== null ? { validity } : {}) }, transition.transitionedAt);
|
|
830
835
|
}
|
|
831
836
|
}
|
|
832
837
|
function persistConflictSupersessions(vault, deps, memories, supersessions, nowMs) {
|
|
@@ -851,6 +856,11 @@ function executeConflictResolution(vault, deps, input) {
|
|
|
851
856
|
const resolution = buildConflictTransitions(memories, { winner: input.winner, losers: input.losers }, { reviewerId: DEFAULT_REVIEWER_ID, nowMs });
|
|
852
857
|
persistConflictTransitions(vault, resolution, input.reason);
|
|
853
858
|
const edgeIds = persistConflictSupersessions(vault, deps, memories, resolution.supersessions, nowMs);
|
|
859
|
+
// Outcome-driven forgetting (#204, O-V1): the winner proved more correct (utility 1), the losers
|
|
860
|
+
// proved wrong (utility 0). These bias the maintenance utility factor toward keeping the winner
|
|
861
|
+
// and forgetting the superseded losers.
|
|
862
|
+
vault.recordOutcome([input.winner], 1, nowMs);
|
|
863
|
+
vault.recordOutcome(input.losers, 0, nowMs);
|
|
854
864
|
return {
|
|
855
865
|
resolved: true,
|
|
856
866
|
winner: input.winner,
|
|
@@ -1054,14 +1064,21 @@ function buildAcceptProposalPatch(origins) {
|
|
|
1054
1064
|
function buildCorrectionAcceptanceUpdates(proposalId, acceptPatch, origins, nowMs) {
|
|
1055
1065
|
return [
|
|
1056
1066
|
{ id: proposalId, patch: acceptPatch, nowMs },
|
|
1057
|
-
...origins.map(({ edge, original }) =>
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1067
|
+
...origins.map(({ edge, original }) => {
|
|
1068
|
+
// Bi-temporal-lite (#204, C1): close the superseded fact's belief window at acceptance time so
|
|
1069
|
+
// it drops out of default retrieval and "as of date T" stays answerable. Additive — only when
|
|
1070
|
+
// it forms a valid, non-extending interval.
|
|
1071
|
+
const validity = supersededValidity(original, nowMs);
|
|
1072
|
+
return {
|
|
1073
|
+
id: original.id,
|
|
1074
|
+
patch: {
|
|
1075
|
+
status: "superseded",
|
|
1076
|
+
staleReason: edge.provenanceSummary ?? "accepted correction",
|
|
1077
|
+
...(validity !== null ? { validity } : {}),
|
|
1078
|
+
},
|
|
1079
|
+
nowMs,
|
|
1080
|
+
};
|
|
1081
|
+
}),
|
|
1065
1082
|
];
|
|
1066
1083
|
}
|
|
1067
1084
|
function recordCorrectionSupersessionAudits(deps, acceptedCorrection, origins, nowMs) {
|
|
@@ -1093,6 +1110,14 @@ function acceptMemoryProposal(vault, deps, id) {
|
|
|
1093
1110
|
if (updated === undefined) {
|
|
1094
1111
|
throw new GovernanceError("invalid-resolution", "acceptance update produced no records");
|
|
1095
1112
|
}
|
|
1113
|
+
// Outcome-driven forgetting (#204, O-V1): acceptance is a positive retention outcome for the
|
|
1114
|
+
// proposal; any origin it supersedes proved wrong (utility 0). Both feed the maintenance utility
|
|
1115
|
+
// factor so the kept memory resists disuse decay and the corrected-away origin fades sooner.
|
|
1116
|
+
vault.recordOutcome([id], 1, nowMs);
|
|
1117
|
+
const supersededOriginIds = origins.map((origin) => origin.original.id);
|
|
1118
|
+
if (supersededOriginIds.length > 0) {
|
|
1119
|
+
vault.recordOutcome(supersededOriginIds, 0, nowMs);
|
|
1120
|
+
}
|
|
1096
1121
|
recordCorrectionSupersessionAudits(deps, updated, origins, nowMs);
|
|
1097
1122
|
return { status: 200, body: { memory: redactMemory(deps, updated) } };
|
|
1098
1123
|
}
|
|
@@ -1146,7 +1171,11 @@ export async function handleRejectMemoryProposal(ctx, deps) {
|
|
|
1146
1171
|
const existing = ensureRejectableMemory(vault.getMemory(id));
|
|
1147
1172
|
if (isRouteResult(existing))
|
|
1148
1173
|
return existing;
|
|
1149
|
-
const
|
|
1174
|
+
const nowMs = Date.now();
|
|
1175
|
+
const updated = vault.updateMemory(id, { status: "rejected", staleReason: reason }, nowMs);
|
|
1176
|
+
// Outcome-driven forgetting (#204, O-V1): a user rejection is a negative retention outcome
|
|
1177
|
+
// (utility 0), so the rejected memory fades faster under maintenance instead of lingering.
|
|
1178
|
+
vault.recordOutcome([id], 0, nowMs);
|
|
1150
1179
|
return { status: 200, body: { memory: redactMemory(deps, updated) } };
|
|
1151
1180
|
}
|
|
1152
1181
|
catch (err) {
|
|
@@ -5,8 +5,6 @@ import type { UiHandlerDeps } from "./deps.js";
|
|
|
5
5
|
import type { RouteContext, RouteResult } from "./routes.js";
|
|
6
6
|
export interface MaintenanceCounts {
|
|
7
7
|
promoted: number;
|
|
8
|
-
reinforced: number;
|
|
9
|
-
decayed: number;
|
|
10
8
|
archived: number;
|
|
11
9
|
forgotten: number;
|
|
12
10
|
superseded: number;
|
|
@@ -17,6 +15,21 @@ export interface MaintenanceCounts {
|
|
|
17
15
|
export interface MaintenanceResult extends MaintenanceCounts {
|
|
18
16
|
readonly reviewItems: readonly ReviewItem[];
|
|
19
17
|
}
|
|
20
|
-
export
|
|
18
|
+
export interface RunMaintenanceOptions {
|
|
19
|
+
/** Injected clock. Defaults to Date.now(). Pass a fixed value to make the pass replay-stable. */
|
|
20
|
+
readonly nowMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function runMemoryMaintenance(vault: MemoryVaultStore, evidenceStore?: EvidenceStore, options?: RunMaintenanceOptions): MaintenanceResult;
|
|
23
|
+
export declare const MEMORY_AUTO_MAINTENANCE_MIN_INTERVAL_MS: number;
|
|
24
|
+
export declare function isMaintenanceDue(lastRunAtMs: number | undefined, nowMs: number, minIntervalMs?: number): boolean;
|
|
25
|
+
export interface AutoMaintenanceState {
|
|
26
|
+
lastRunAtMs?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface MaybeRunAutoMaintenanceOptions {
|
|
29
|
+
readonly nowMs: number;
|
|
30
|
+
readonly enabled: boolean;
|
|
31
|
+
readonly minIntervalMs?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function maybeRunAutoMaintenance(vault: MemoryVaultStore, evidenceStore: EvidenceStore | undefined, state: AutoMaintenanceState, options: MaybeRunAutoMaintenanceOptions): MaintenanceResult | null;
|
|
21
34
|
export declare function handleRunMaintenance(ctx: RouteContext, deps: UiHandlerDeps): RouteResult;
|
|
22
35
|
//# sourceMappingURL=memory-maintenance-handlers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-maintenance-handlers.d.ts","sourceRoot":"","sources":["../src/memory-maintenance-handlers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memory-maintenance-handlers.d.ts","sourceRoot":"","sources":["../src/memory-maintenance-handlers.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAc7F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI7D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,QAAQ,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,CAAC;CAC7C;AAuMD,MAAM,WAAW,qBAAqB;IACpC,iGAAiG;IACjG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,gBAAgB,EACvB,aAAa,CAAC,EAAE,aAAa,EAC7B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,iBAAiB,CAkCnB;AAQD,eAAO,MAAM,uCAAuC,QAAqB,CAAC;AAI1E,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,KAAK,EAAE,MAAM,EACb,aAAa,GAAE,MAAgD,GAC9D,OAAO,CAIT;AAGD,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAMD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,gBAAgB,EACvB,aAAa,EAAE,aAAa,GAAG,SAAS,EACxC,KAAK,EAAE,oBAAoB,EAC3B,OAAO,EAAE,8BAA8B,GACtC,iBAAiB,GAAG,IAAI,CAS1B;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,GAAG,WAAW,CAWxF"}
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// 2. Run consolidation on the accepted subset; persist auto-applicable relationship edges and
|
|
7
7
|
// return unresolved review items for MemoriaViva or CLI operators. Conflict and merge review
|
|
8
8
|
// items are NEVER auto-applied here.
|
|
9
|
-
// 3. Compute the maintenance plan and apply it: promote (-> accepted),
|
|
10
|
-
//
|
|
9
|
+
// 3. Compute the maintenance plan and apply it: promote (-> accepted), archive (-> archived),
|
|
10
|
+
// forget (vault delete + tombstone + reason). Confidence is immutable provenance and is never
|
|
11
|
+
// patched here (O-V2): reuse strengthens memories live in retrieval ranking, and disuse-decay
|
|
12
|
+
// is computed on the fly via the strength curve that gates archive/forget.
|
|
11
13
|
// 4. Emit one audit event per applied effect and return the counts.
|
|
12
14
|
//
|
|
13
15
|
// CSRF: the server dispatch layer enforces x-keiko-csrf for POST, so this route is guarded without
|
|
@@ -21,8 +23,6 @@ import { recordMemoryAudit } from "./memory-audit-handler.js";
|
|
|
21
23
|
function emptyCounts() {
|
|
22
24
|
return {
|
|
23
25
|
promoted: 0,
|
|
24
|
-
reinforced: 0,
|
|
25
|
-
decayed: 0,
|
|
26
26
|
archived: 0,
|
|
27
27
|
forgotten: 0,
|
|
28
28
|
superseded: 0,
|
|
@@ -44,24 +44,20 @@ function resolveVault(deps) {
|
|
|
44
44
|
}
|
|
45
45
|
return deps.memoryVault;
|
|
46
46
|
}
|
|
47
|
-
function emitAudit(evidenceStore, kind, surface, summary, extra) {
|
|
47
|
+
function emitAudit(evidenceStore, nowMs, kind, surface, summary, extra) {
|
|
48
48
|
if (evidenceStore === undefined)
|
|
49
49
|
return;
|
|
50
50
|
const event = {
|
|
51
51
|
schemaVersion: "1",
|
|
52
52
|
kind,
|
|
53
53
|
eventId: randomUUID(),
|
|
54
|
-
occurredAt:
|
|
54
|
+
occurredAt: nowMs,
|
|
55
55
|
initiatorSurface: surface,
|
|
56
56
|
summary,
|
|
57
57
|
...extra,
|
|
58
58
|
};
|
|
59
59
|
recordMemoryAudit({ evidenceStore }, event);
|
|
60
60
|
}
|
|
61
|
-
// Patch a record's confidence by rebuilding the provenance envelope (confidence lives there).
|
|
62
|
-
function patchConfidence(vault, record, confidence) {
|
|
63
|
-
vault.updateMemory(record.id, { provenance: { ...record.provenance, confidence } }, Date.now());
|
|
64
|
-
}
|
|
65
61
|
function recordsById(records) {
|
|
66
62
|
const map = new Map();
|
|
67
63
|
for (const record of records)
|
|
@@ -82,9 +78,9 @@ function applyEdges(vault, edges) {
|
|
|
82
78
|
}
|
|
83
79
|
return created;
|
|
84
80
|
}
|
|
85
|
-
function runConsolidationPass(vault, records, counts) {
|
|
81
|
+
function runConsolidationPass(vault, nowMs, records, counts) {
|
|
86
82
|
const result = runConsolidation(records, {
|
|
87
|
-
nowMs
|
|
83
|
+
nowMs,
|
|
88
84
|
newEdgeId: () => randomUUID(),
|
|
89
85
|
newReviewItemId: () => randomUUID(),
|
|
90
86
|
});
|
|
@@ -94,48 +90,41 @@ function runConsolidationPass(vault, records, counts) {
|
|
|
94
90
|
counts.reviewItems.push(...result.reviewItems);
|
|
95
91
|
}
|
|
96
92
|
// ─── Plan application ──────────────────────────────────────────────────────────
|
|
97
|
-
// Applies the
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
93
|
+
// Applies the archive / forget effects on the post-consolidation snapshot. Promotions are applied
|
|
94
|
+
// SEPARATELY and BEFORE consolidation (see runMemoryMaintenance) so that freshly-accepted memories
|
|
95
|
+
// are visible to conflict detection within the same maintenance pass.
|
|
96
|
+
//
|
|
97
|
+
// Confidence is NEVER mutated here (#204, O-V2): reinforcement-on-reuse is realised live in
|
|
98
|
+
// retrieval ranking, and disuse-decay is computed on the fly via the strength curve that already
|
|
99
|
+
// gates archive/forget — so provenance stays intact and every run is idempotent.
|
|
100
|
+
function applyFadeEffects(vault, evidenceStore, nowMs, plan, byId, counts) {
|
|
101
|
+
applyArchives(vault, evidenceStore, nowMs, plan.archive, byId, counts);
|
|
102
|
+
applyForgets(vault, evidenceStore, nowMs, plan.forget, byId, counts);
|
|
103
|
+
}
|
|
104
|
+
function applyPromotions(vault, evidenceStore, nowMs, ids, byId, counts) {
|
|
107
105
|
for (const id of ids) {
|
|
108
106
|
const record = byId.get(id);
|
|
109
107
|
if (record === undefined)
|
|
110
108
|
continue;
|
|
111
|
-
vault.updateMemory(id, { status: "accepted" },
|
|
109
|
+
vault.updateMemory(id, { status: "accepted" }, nowMs);
|
|
112
110
|
counts.promoted += 1;
|
|
113
|
-
emitAudit(evidenceStore, "memory:accepted", "memory-center", "Promoted a strong proposed memory.", { memoryId: id, scope: record.scope });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function applyConfidencePatches(vault, patches, byId, bump) {
|
|
117
|
-
for (const patch of patches) {
|
|
118
|
-
const record = byId.get(patch.id);
|
|
119
|
-
if (record === undefined)
|
|
120
|
-
continue;
|
|
121
|
-
patchConfidence(vault, record, patch.confidence);
|
|
122
|
-
bump(1);
|
|
111
|
+
emitAudit(evidenceStore, nowMs, "memory:accepted", "memory-center", "Promoted a strong proposed memory.", { memoryId: id, scope: record.scope });
|
|
123
112
|
}
|
|
124
113
|
}
|
|
125
|
-
function applyArchives(vault, evidenceStore, ids, byId, counts) {
|
|
114
|
+
function applyArchives(vault, evidenceStore, nowMs, ids, byId, counts) {
|
|
126
115
|
for (const id of ids) {
|
|
127
116
|
const record = byId.get(id);
|
|
128
117
|
if (record === undefined)
|
|
129
118
|
continue;
|
|
130
|
-
vault.updateMemory(id, { status: "archived" },
|
|
119
|
+
vault.updateMemory(id, { status: "archived" }, nowMs);
|
|
131
120
|
counts.archived += 1;
|
|
132
|
-
emitAudit(evidenceStore, "memory:archived", "retention", "Archived a faded memory.", {
|
|
121
|
+
emitAudit(evidenceStore, nowMs, "memory:archived", "retention", "Archived a faded memory.", {
|
|
133
122
|
memoryId: id,
|
|
134
123
|
scope: record.scope,
|
|
135
124
|
});
|
|
136
125
|
}
|
|
137
126
|
}
|
|
138
|
-
function applyForgets(vault, evidenceStore, forgets, byId, counts) {
|
|
127
|
+
function applyForgets(vault, evidenceStore, nowMs, forgets, byId, counts) {
|
|
139
128
|
for (const forget of forgets) {
|
|
140
129
|
const record = byId.get(forget.id);
|
|
141
130
|
if (record === undefined)
|
|
@@ -144,20 +133,21 @@ function applyForgets(vault, evidenceStore, forgets, byId, counts) {
|
|
|
144
133
|
tombstone: true,
|
|
145
134
|
forgetterSurface: "memory-maintenance",
|
|
146
135
|
reason: forget.reason,
|
|
147
|
-
nowMs
|
|
136
|
+
nowMs,
|
|
148
137
|
});
|
|
149
138
|
counts.forgotten += 1;
|
|
150
|
-
emitAudit(evidenceStore, "memory:forgotten", "retention", `Forgot a memory (${forget.reason}).`, {
|
|
139
|
+
emitAudit(evidenceStore, nowMs, "memory:forgotten", "retention", `Forgot a memory (${forget.reason}).`, {
|
|
151
140
|
memoryId: forget.id,
|
|
152
141
|
scope: record.scope,
|
|
153
142
|
tombstoned: true,
|
|
154
143
|
});
|
|
155
144
|
}
|
|
156
145
|
}
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
146
|
+
export function runMemoryMaintenance(vault, evidenceStore, options) {
|
|
147
|
+
// ONE clock for the whole pass: both plan phases and every vault write / audit timestamp use the
|
|
148
|
+
// same nowMs, so selection is consistent within a run and fully replay-stable when nowMs is
|
|
149
|
+
// injected (the deterministic-verification invariant the rest of the stack honours).
|
|
150
|
+
const nowMs = options?.nowMs ?? Date.now();
|
|
161
151
|
const counts = emptyCounts();
|
|
162
152
|
// Phase 1 — promote strong `proposed` memories FIRST. Consolidation and conflict detection only
|
|
163
153
|
// inspect `accepted` records, so without this a vault full of freshly-captured `proposed`
|
|
@@ -165,22 +155,54 @@ export function runMemoryMaintenance(vault, evidenceStore) {
|
|
|
165
155
|
// resolved. Promoting up front makes a single "Run maintenance" fully effective.
|
|
166
156
|
const beforePromote = vault.listMemories({ includeExpired: true });
|
|
167
157
|
const promoteStats = vault.getAccessStats();
|
|
168
|
-
const promotePlan = planMemoryMaintenance(beforePromote, promoteStats, { nowMs
|
|
169
|
-
applyPromotions(vault, evidenceStore, promotePlan.promote, recordsById(beforePromote), counts);
|
|
158
|
+
const promotePlan = planMemoryMaintenance(beforePromote, promoteStats, { nowMs });
|
|
159
|
+
applyPromotions(vault, evidenceStore, nowMs, promotePlan.promote, recordsById(beforePromote), counts);
|
|
170
160
|
// Phase 2 — consolidate the now-accepted set: link safe near-duplicate metadata and surface
|
|
171
161
|
// conflicts / merges as explicit review items. Status mutations require a later governed review.
|
|
172
162
|
const accepted = vault
|
|
173
163
|
.listMemories({ includeExpired: true })
|
|
174
164
|
.filter((record) => record.status === "accepted");
|
|
175
|
-
runConsolidationPass(vault, accepted, counts);
|
|
176
|
-
// Phase 3 —
|
|
177
|
-
//
|
|
165
|
+
runConsolidationPass(vault, nowMs, accepted, counts);
|
|
166
|
+
// Phase 3 — archive / forget on the post-consolidation snapshot. The access stats feed the
|
|
167
|
+
// strength model; confidence itself is never mutated (O-V2).
|
|
178
168
|
const all = vault.listMemories({ includeExpired: true });
|
|
179
169
|
const accessStats = vault.getAccessStats();
|
|
180
|
-
const plan = planMemoryMaintenance(all, accessStats, { nowMs
|
|
181
|
-
|
|
170
|
+
const plan = planMemoryMaintenance(all, accessStats, { nowMs });
|
|
171
|
+
applyFadeEffects(vault, evidenceStore, nowMs, plan, recordsById(all), counts);
|
|
182
172
|
return counts;
|
|
183
173
|
}
|
|
174
|
+
// ─── Bounded autonomous maintenance (#204, O-V4) ───────────────────────────────
|
|
175
|
+
// The strength/decay/forget pass only "lives" if it actually runs. Rather than a free-running
|
|
176
|
+
// background loop (forbidden by the no-unbounded-hidden-activity invariant), maintenance is fired
|
|
177
|
+
// opportunistically — once memory is used — and rate-limited to at most once per interval. The pass
|
|
178
|
+
// itself is already bounded (maxForgetPerRun) and audited, so an autonomous fire is governed.
|
|
179
|
+
export const MEMORY_AUTO_MAINTENANCE_MIN_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
180
|
+
// Pure: due iff never run, or the interval has elapsed. A non-finite/negative interval is treated as
|
|
181
|
+
// "never auto-run" so a misconfiguration can only DISABLE, never spin.
|
|
182
|
+
export function isMaintenanceDue(lastRunAtMs, nowMs, minIntervalMs = MEMORY_AUTO_MAINTENANCE_MIN_INTERVAL_MS) {
|
|
183
|
+
if (!Number.isFinite(minIntervalMs) || minIntervalMs <= 0)
|
|
184
|
+
return false;
|
|
185
|
+
if (lastRunAtMs === undefined)
|
|
186
|
+
return true;
|
|
187
|
+
return nowMs - lastRunAtMs >= minIntervalMs;
|
|
188
|
+
}
|
|
189
|
+
// Runs ONE bounded maintenance pass iff enabled AND due, advancing the cursor BEFORE running so a
|
|
190
|
+
// re-entrant call within the same tick cannot double-fire. Returns the result, or null when skipped.
|
|
191
|
+
// Never throws: a maintenance fault must not break the caller (e.g. a chat turn); it is swallowed
|
|
192
|
+
// after advancing the cursor so a persistently-failing pass cannot hot-loop.
|
|
193
|
+
export function maybeRunAutoMaintenance(vault, evidenceStore, state, options) {
|
|
194
|
+
if (!options.enabled)
|
|
195
|
+
return null;
|
|
196
|
+
if (!isMaintenanceDue(state.lastRunAtMs, options.nowMs, options.minIntervalMs))
|
|
197
|
+
return null;
|
|
198
|
+
state.lastRunAtMs = options.nowMs;
|
|
199
|
+
try {
|
|
200
|
+
return runMemoryMaintenance(vault, evidenceStore, { nowMs: options.nowMs });
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
184
206
|
export function handleRunMaintenance(ctx, deps) {
|
|
185
207
|
void ctx;
|
|
186
208
|
const vault = resolveVault(deps);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MemoryId, MemoryScope } from "@oscharko-dev/keiko-contracts/memory";
|
|
2
|
+
import { type RankingFusionMode } from "@oscharko-dev/keiko-memory-retrieval";
|
|
3
|
+
import type { MemoryVaultStore } from "@oscharko-dev/keiko-memory-vault";
|
|
4
|
+
import type { UiHandlerDeps } from "./deps.js";
|
|
5
|
+
export declare function conversationFusionMode(deps: UiHandlerDeps): RankingFusionMode;
|
|
6
|
+
export interface ConversationRetrievalSignals {
|
|
7
|
+
readonly semanticById?: ReadonlyMap<MemoryId, number> | undefined;
|
|
8
|
+
readonly strengthById: ReadonlyMap<MemoryId, number>;
|
|
9
|
+
readonly embeddingById: ReadonlyMap<MemoryId, Float32Array>;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildConversationRetrievalSignals(deps: UiHandlerDeps, vault: MemoryVaultStore, queryText: string | undefined, scopes: readonly MemoryScope[], nowMs: number, safeForSecondaryModel: boolean): Promise<ConversationRetrievalSignals>;
|
|
12
|
+
//# sourceMappingURL=memory-retrieval-signals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-retrieval-signals.d.ts","sourceRoot":"","sources":["../src/memory-retrieval-signals.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,QAAQ,EAAgB,WAAW,EAAE,MAAM,sCAAsC,CAAC;AAChG,OAAO,EAKL,KAAK,iBAAiB,EACvB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAE7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAwD/C,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,aAAa,GAAG,iBAAiB,CAE7E;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAClE,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAIrD,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;CAC7D;AAED,wBAAsB,iCAAiC,CACrD,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,KAAK,EAAE,MAAM,EACb,qBAAqB,EAAE,OAAO,GAC7B,OAAO,CAAC,4BAA4B,CAAC,CAkBvC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Shared conversation-retrieval signal builder (#204, O-F4).
|
|
2
|
+
//
|
|
3
|
+
// Both conversation retrieval surfaces — the desktop chat path (chat-handlers) and the BFF
|
|
4
|
+
// /api/memory/context route (memory-conv-handlers) — need the same two model/usage-derived ranking
|
|
5
|
+
// signals on top of the pure lexical ranker:
|
|
6
|
+
// - semanticById: per-memory cosine of the query embedding to each candidate's stored vector
|
|
7
|
+
// (embedding-based recall), gated by the secondary-model egress check.
|
|
8
|
+
// - strengthById: per-memory reinforcement strength from the vault's access counters (O-P1).
|
|
9
|
+
// Previously only the chat path built them; the BFF route silently ran lexical-only. Centralising
|
|
10
|
+
// them here keeps the two surfaces from drifting (the same class of duplication C3 guards for
|
|
11
|
+
// suppression) and gives any future consumer of the route the stronger embedding signal by default.
|
|
12
|
+
//
|
|
13
|
+
// Pure of policy: the caller decides whether the query is egress-safe and passes that in. Graceful:
|
|
14
|
+
// no embedding model => semanticById undefined (byte-identical lexical fallback); empty access
|
|
15
|
+
// history => strengthById empty (the ranker zeroes its weight).
|
|
16
|
+
import { buildStrengthById, DEFAULT_LIST_BY_SCOPE_MAX_RESULTS, DEFAULT_STALE_CONFIDENCE_THRESHOLD, isMemorySuppressed, } from "@oscharko-dev/keiko-memory-retrieval";
|
|
17
|
+
import { cosineSimilarity, embedMemoryText } from "./memory-embedding.js";
|
|
18
|
+
// A candidate is worth scoring iff the ranker could surface it. A superset of the ranked set is
|
|
19
|
+
// harmless: ids the ranker filters out simply never read their semantic score.
|
|
20
|
+
function isSemanticRetrievalCandidate(record, nowMs) {
|
|
21
|
+
if (record.status === "superseded")
|
|
22
|
+
return false;
|
|
23
|
+
return !isMemorySuppressed(record, nowMs, DEFAULT_STALE_CONFIDENCE_THRESHOLD).suppressed;
|
|
24
|
+
}
|
|
25
|
+
function gatherCandidateIds(vault, scopes, nowMs) {
|
|
26
|
+
const ids = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
for (const scope of scopes) {
|
|
29
|
+
for (const record of vault.listMemoriesByScope(scope, {
|
|
30
|
+
includeExpired: true,
|
|
31
|
+
limit: DEFAULT_LIST_BY_SCOPE_MAX_RESULTS,
|
|
32
|
+
})) {
|
|
33
|
+
if (!isSemanticRetrievalCandidate(record, nowMs))
|
|
34
|
+
continue;
|
|
35
|
+
if (seen.has(record.id))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(record.id);
|
|
38
|
+
ids.push(record.id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return ids;
|
|
42
|
+
}
|
|
43
|
+
// Per-memory semantic score map for the candidate set, or undefined when no embedding model is
|
|
44
|
+
// configured (query embedding null) — that undefined drives the byte-identical lexical fallback in
|
|
45
|
+
// the ranker. A candidate whose stored vector is missing is omitted (semantic subscore 0 for it).
|
|
46
|
+
// Query-cosine scores for the candidate set, computed from the already-fetched embeddings. Gated by
|
|
47
|
+
// the egress check (it embeds the query). Returns undefined when no model / no query embedding.
|
|
48
|
+
async function semanticScoresFrom(deps, queryText, candidateIds, embeddings) {
|
|
49
|
+
const queryEmbedding = await embedMemoryText(deps, queryText);
|
|
50
|
+
if (queryEmbedding === null)
|
|
51
|
+
return undefined;
|
|
52
|
+
const scores = new Map();
|
|
53
|
+
for (const id of candidateIds) {
|
|
54
|
+
const stored = embeddings.get(id);
|
|
55
|
+
if (stored === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
scores.set(id, cosineSimilarity(queryEmbedding.vector, stored.vector));
|
|
58
|
+
}
|
|
59
|
+
return scores;
|
|
60
|
+
}
|
|
61
|
+
// Signal-fusion mode for the conversation retrieval surfaces (#204, O-F2). Opt-in via env to keep
|
|
62
|
+
// the release on the byte-identical weighted-sum default; set KEIKO_MEMORY_FUSION=rrf to enable
|
|
63
|
+
// rank-based Reciprocal Rank Fusion across both the chat and BFF paths.
|
|
64
|
+
export function conversationFusionMode(deps) {
|
|
65
|
+
return deps.env.KEIKO_MEMORY_FUSION === "rrf" ? "rrf" : "weighted-sum";
|
|
66
|
+
}
|
|
67
|
+
export async function buildConversationRetrievalSignals(deps, vault, queryText, scopes, nowMs, safeForSecondaryModel) {
|
|
68
|
+
const strengthById = buildStrengthById(vault.getAccessStats(), nowMs);
|
|
69
|
+
const candidateIds = gatherCandidateIds(vault, scopes, nowMs);
|
|
70
|
+
const embeddings = candidateIds.length > 0
|
|
71
|
+
? vault.getEmbeddings(candidateIds)
|
|
72
|
+
: new Map();
|
|
73
|
+
const embeddingById = new Map();
|
|
74
|
+
for (const [id, row] of embeddings)
|
|
75
|
+
embeddingById.set(id, row.vector);
|
|
76
|
+
const semanticById = safeForSecondaryModel && queryText !== undefined && queryText.length > 0 && embeddings.size > 0
|
|
77
|
+
? await semanticScoresFrom(deps, queryText, candidateIds, embeddings)
|
|
78
|
+
: undefined;
|
|
79
|
+
return {
|
|
80
|
+
strengthById,
|
|
81
|
+
embeddingById,
|
|
82
|
+
...(semanticById !== undefined ? { semanticById } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-salience.d.ts","sourceRoot":"","sources":["../src/memory-salience.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AAU3F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"memory-salience.d.ts","sourceRoot":"","sources":["../src/memory-salience.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AAU3F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AA0H1C,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;CAC5D;AAID,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,gCAAgC,EACzC,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,SAAS,4BAA4B,EAAE,CAAC,CA2ClD"}
|