@anuma/sdk 3.12.1 → 3.13.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/dist/expo/{chunk-ORVYPS3R.mjs → chunk-BOAKGY4R.mjs} +277 -46
- package/dist/expo/index.cjs +331 -48
- package/dist/expo/index.d.mts +138 -14
- package/dist/expo/index.d.ts +138 -14
- package/dist/expo/index.mjs +57 -4
- package/dist/expo/{recall-A4WZELH5.mjs → recall-O65PGPIQ.mjs} +1 -1
- package/dist/react/{chunk-QURYPMCE.mjs → chunk-XXCZVG4Z.mjs} +290 -49
- package/dist/react/index.cjs +344 -51
- package/dist/react/index.d.mts +151 -15
- package/dist/react/index.d.ts +151 -15
- package/dist/react/index.mjs +57 -4
- package/dist/react/{recall-NQNZDPNU.mjs → recall-KTJCUAXN.mjs} +1 -1
- package/dist/server/{chunk-FXGG7JA5.mjs → chunk-ME6ZQN52.mjs} +277 -46
- package/dist/server/index.cjs +331 -48
- package/dist/server/index.d.mts +138 -14
- package/dist/server/index.d.ts +138 -14
- package/dist/server/index.mjs +57 -4
- package/dist/server/{recall-SLMMIFHM.mjs → recall-RMCSM6L6.mjs} +1 -1
- package/package.json +2 -1
package/dist/expo/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
VAULT_SIZE_HOP_CAP,
|
|
21
21
|
archiveVaultMemoryOp,
|
|
22
22
|
assertVaultScopeForSweep,
|
|
23
|
+
backfillMemoryTopicsOp,
|
|
23
24
|
capHopsForDensity,
|
|
24
25
|
chunkAndEmbedAllMessages,
|
|
25
26
|
chunkAndEmbedMessage,
|
|
@@ -91,6 +92,7 @@ import {
|
|
|
91
92
|
prepareVaultCandidates,
|
|
92
93
|
rankPreparedVaultCandidates,
|
|
93
94
|
recall,
|
|
95
|
+
relinkMemoryTopicsOp,
|
|
94
96
|
replaceMemoryEntitiesGuardedOp,
|
|
95
97
|
requestEncryptionKey,
|
|
96
98
|
resolveStoredUserContent,
|
|
@@ -114,7 +116,7 @@ import {
|
|
|
114
116
|
upsertMessageOp,
|
|
115
117
|
useEncryption,
|
|
116
118
|
vaultMemoryToStored
|
|
117
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-BOAKGY4R.mjs";
|
|
118
120
|
import {
|
|
119
121
|
PII_PATTERNS,
|
|
120
122
|
PiiRedactor,
|
|
@@ -5596,6 +5598,12 @@ __decorateClass([
|
|
|
5596
5598
|
__decorateClass([
|
|
5597
5599
|
field3("topics_user_managed")
|
|
5598
5600
|
], VaultMemory.prototype, "topicsUserManaged", 2);
|
|
5601
|
+
__decorateClass([
|
|
5602
|
+
field3("topics")
|
|
5603
|
+
], VaultMemory.prototype, "topics", 2);
|
|
5604
|
+
__decorateClass([
|
|
5605
|
+
field3("topics_updated_at")
|
|
5606
|
+
], VaultMemory.prototype, "topicsUpdatedAt", 2);
|
|
5599
5607
|
__decorateClass([
|
|
5600
5608
|
field3("topics_extracted_at")
|
|
5601
5609
|
], VaultMemory.prototype, "topicsExtractedAt", 2);
|
|
@@ -7368,7 +7376,6 @@ function createAutoExtractor(options) {
|
|
|
7368
7376
|
return true;
|
|
7369
7377
|
}
|
|
7370
7378
|
function drainPending() {
|
|
7371
|
-
if (disposed) return;
|
|
7372
7379
|
for (const [conversationId, state] of conversations) {
|
|
7373
7380
|
if (state.pending) {
|
|
7374
7381
|
const messages = state.pending;
|
|
@@ -7383,7 +7390,7 @@ function createAutoExtractor(options) {
|
|
|
7383
7390
|
isProcessing: () => inflight > 0,
|
|
7384
7391
|
dispose: () => {
|
|
7385
7392
|
disposed = true;
|
|
7386
|
-
|
|
7393
|
+
if (inflight === 0) drainPending();
|
|
7387
7394
|
}
|
|
7388
7395
|
};
|
|
7389
7396
|
}
|
|
@@ -11774,7 +11781,7 @@ __decorateClass([
|
|
|
11774
11781
|
], VaultFolder.prototype, "context", 2);
|
|
11775
11782
|
|
|
11776
11783
|
// src/lib/db/schema.ts
|
|
11777
|
-
var SDK_SCHEMA_VERSION =
|
|
11784
|
+
var SDK_SCHEMA_VERSION = 42;
|
|
11778
11785
|
var sdkSchema = appSchema2({
|
|
11779
11786
|
version: SDK_SCHEMA_VERSION,
|
|
11780
11787
|
tables: [
|
|
@@ -11896,9 +11903,33 @@ var sdkSchema = appSchema2({
|
|
|
11896
11903
|
// links). Auto-extraction then leaves its links alone — the user owns
|
|
11897
11904
|
// them. Null/false = topics are auto-derived (default).
|
|
11898
11905
|
{ name: "topics_user_managed", type: "boolean", isOptional: true },
|
|
11906
|
+
// The memory's topics as a DURABLE, SYNCED record: a JSON array of
|
|
11907
|
+
// `{name, kind?, source}`, `name` in the caller's display casing (unlike
|
|
11908
|
+
// `entity.canonical_name`, which is lowercased and has no display
|
|
11909
|
+
// column). `entity` / `memory_entity` are a device-local INDEX over this
|
|
11910
|
+
// — their ids are locally generated, so they can never sync — and a
|
|
11911
|
+
// restored device rebuilds them from these names with no LLM call.
|
|
11912
|
+
// Null = predates v42 (backfilled from the row's current links by the
|
|
11913
|
+
// sweep's topicsBackfill bucket). See getMemoriesNeedingTopicExtractionOp.
|
|
11914
|
+
{ name: "topics", type: "string", isOptional: true },
|
|
11915
|
+
// Unix ms of the last write to `topics`. A SECOND timestamp is required:
|
|
11916
|
+
// every topic writer deliberately pins `updated_at` so a topic change
|
|
11917
|
+
// doesn't inflate recall's recency multiplier, and both client sync
|
|
11918
|
+
// paths key on `updated_at` — so without this a topic-only change would
|
|
11919
|
+
// neither upload nor merge. Null = `topics` never written.
|
|
11920
|
+
{ name: "topics_updated_at", type: "number", isOptional: true },
|
|
11899
11921
|
// Unix ms of the last LLM topic-extraction pass over this memory's
|
|
11900
11922
|
// content. Null = never extracted standalone (legacy rows with entity
|
|
11901
11923
|
// links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
|
|
11924
|
+
//
|
|
11925
|
+
// DEPRECATED (v42) — `topics_updated_at` subsumes this and
|
|
11926
|
+
// `topics_extracted_version` both: null there means never processed, and
|
|
11927
|
+
// non-null with an empty `topics` means processed and found nothing
|
|
11928
|
+
// (today's "answered empty" case), while a release-time
|
|
11929
|
+
// EXTRACTOR_CHANGED_AT constant compared against `topics_updated_at`
|
|
11930
|
+
// replaces the version check. Both columns are kept only to avoid a
|
|
11931
|
+
// column-drop migration in an otherwise additive list; removing them is
|
|
11932
|
+
// a follow-up once `topics` is proven in production.
|
|
11902
11933
|
{ name: "topics_extracted_at", type: "number", isOptional: true },
|
|
11903
11934
|
// Write-time supersession (A2). When set, this fact was replaced by a
|
|
11904
11935
|
// newer, incompatible-value fact (e.g. "Lives in Portland" superseded by
|
|
@@ -11912,6 +11943,9 @@ var sdkSchema = appSchema2({
|
|
|
11912
11943
|
// The extraction-logic version this memory was last stamped under. Null
|
|
11913
11944
|
// (pre-v38) is treated as version 0, so a bump of TOPICS_EXTRACTION_VERSION
|
|
11914
11945
|
// re-extracts stale rows. See getMemoriesNeedingTopicExtractionOp.
|
|
11946
|
+
//
|
|
11947
|
+
// DEPRECATED (v42) alongside `topics_extracted_at` — same rationale and
|
|
11948
|
+
// same removal follow-up; see that column's note above.
|
|
11915
11949
|
{ name: "topics_extracted_version", type: "number", isOptional: true },
|
|
11916
11950
|
// Re-observation watermark (C3). Unix ms of the last time retain() merged
|
|
11917
11951
|
// a duplicate observation into this fact (proof_count++). Distinct from
|
|
@@ -12645,6 +12679,23 @@ var sdkMigrations = schemaMigrations2({
|
|
|
12645
12679
|
]
|
|
12646
12680
|
})
|
|
12647
12681
|
]
|
|
12682
|
+
},
|
|
12683
|
+
// v41 -> v42: topics + topics_updated_at on memory_vault — the durable,
|
|
12684
|
+
// synced record of a memory's topics. Existing rows keep both NULL: the
|
|
12685
|
+
// sweep's topicsBackfill bucket fills them from each row's current entity
|
|
12686
|
+
// links (no LLM), capped under the worker's `limit` so the one-time
|
|
12687
|
+
// re-upload it triggers drains across sweeps instead of spiking.
|
|
12688
|
+
{
|
|
12689
|
+
toVersion: 42,
|
|
12690
|
+
steps: [
|
|
12691
|
+
addColumns2({
|
|
12692
|
+
table: "memory_vault",
|
|
12693
|
+
columns: [
|
|
12694
|
+
{ name: "topics", type: "string", isOptional: true },
|
|
12695
|
+
{ name: "topics_updated_at", type: "number", isOptional: true }
|
|
12696
|
+
]
|
|
12697
|
+
})
|
|
12698
|
+
]
|
|
12648
12699
|
}
|
|
12649
12700
|
]
|
|
12650
12701
|
});
|
|
@@ -14025,6 +14076,7 @@ export {
|
|
|
14025
14076
|
WalletPoller,
|
|
14026
14077
|
addConversationMemoriesOp,
|
|
14027
14078
|
archiveVaultMemoryOp,
|
|
14079
|
+
backfillMemoryTopicsOp,
|
|
14028
14080
|
buildNotionAuthUrl,
|
|
14029
14081
|
capHopsForDensity,
|
|
14030
14082
|
chatStorageMigrations,
|
|
@@ -14122,6 +14174,7 @@ export {
|
|
|
14122
14174
|
reflect,
|
|
14123
14175
|
refreshNotionAccessToken,
|
|
14124
14176
|
registerNotionClient,
|
|
14177
|
+
relinkMemoryTopicsOp,
|
|
14125
14178
|
requestEncryptionKey,
|
|
14126
14179
|
resolvePiiRedactor,
|
|
14127
14180
|
restoreVaultMemoryOp,
|
|
@@ -1918,6 +1918,31 @@ var ENTITY_KINDS = [
|
|
|
1918
1918
|
function normalizeEntityName(name) {
|
|
1919
1919
|
return name.trim().toLowerCase();
|
|
1920
1920
|
}
|
|
1921
|
+
function serializeTopics(topics) {
|
|
1922
|
+
return JSON.stringify(topics);
|
|
1923
|
+
}
|
|
1924
|
+
function parseTopics(value) {
|
|
1925
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
1926
|
+
let parsed;
|
|
1927
|
+
try {
|
|
1928
|
+
parsed = JSON.parse(value);
|
|
1929
|
+
} catch {
|
|
1930
|
+
return null;
|
|
1931
|
+
}
|
|
1932
|
+
if (!Array.isArray(parsed)) return null;
|
|
1933
|
+
const out = [];
|
|
1934
|
+
for (const raw of parsed) {
|
|
1935
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
1936
|
+
const { name, kind, source } = raw;
|
|
1937
|
+
if (typeof name !== "string" || name.trim().length === 0) continue;
|
|
1938
|
+
out.push({
|
|
1939
|
+
name,
|
|
1940
|
+
...typeof kind === "string" && kind.length > 0 ? { kind } : {},
|
|
1941
|
+
source: source === "user" ? "user" : "auto"
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
return out;
|
|
1945
|
+
}
|
|
1921
1946
|
|
|
1922
1947
|
// src/lib/db/entities/operations.ts
|
|
1923
1948
|
function entityToStored(e) {
|
|
@@ -1967,9 +1992,62 @@ async function upsertEntitiesInWrite(ctx, entities) {
|
|
|
1967
1992
|
for (const record of created) out.set(record.canonicalName, entityToStored(record));
|
|
1968
1993
|
return { entities: out, operations: [...created, ...updated] };
|
|
1969
1994
|
}
|
|
1995
|
+
async function findVaultRowInWrite(ctx, memoryId) {
|
|
1996
|
+
try {
|
|
1997
|
+
const rows = await ctx.database.get("memory_vault").query(Q2.where("id", memoryId)).fetch();
|
|
1998
|
+
return rows[0] ?? null;
|
|
1999
|
+
} catch {
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
function prepareTopicsUpdate(row, topics, now) {
|
|
2004
|
+
const originalUpdatedAt = row.updatedAt.getTime();
|
|
2005
|
+
return row.prepareUpdate((r) => {
|
|
2006
|
+
r._setRaw("topics", serializeTopics(topics));
|
|
2007
|
+
r._setRaw("topics_updated_at", now);
|
|
2008
|
+
r._setRaw("updated_at", originalUpdatedAt);
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
function displayNamesOf(entities) {
|
|
2012
|
+
const out = /* @__PURE__ */ new Map();
|
|
2013
|
+
for (const e of entities) {
|
|
2014
|
+
const name = e.name.trim();
|
|
2015
|
+
const normalized = normalizeEntityName(name);
|
|
2016
|
+
if (normalized.length === 0 || out.has(normalized)) continue;
|
|
2017
|
+
out.set(normalized, name);
|
|
2018
|
+
}
|
|
2019
|
+
return out;
|
|
2020
|
+
}
|
|
2021
|
+
function topicsEqual(stored, computed) {
|
|
2022
|
+
if (stored === null || stored.length !== computed.length) return false;
|
|
2023
|
+
const key = (t) => JSON.stringify([t.name, t.kind ?? null, t.source]);
|
|
2024
|
+
const storedKeys = stored.map(key).sort();
|
|
2025
|
+
const computedKeys = computed.map(key).sort();
|
|
2026
|
+
return storedKeys.every((k, i) => k === computedKeys[i]);
|
|
2027
|
+
}
|
|
2028
|
+
function resolveTopicsWrite(row, linked, displayNames, source) {
|
|
2029
|
+
if (row === null || source === null) return null;
|
|
2030
|
+
const topics = topicsForEntities(linked, displayNames, source);
|
|
2031
|
+
if (topicsEqual(parseTopics(row.topics), topics)) return null;
|
|
2032
|
+
return { row, topics };
|
|
2033
|
+
}
|
|
2034
|
+
function topicsForEntities(entities, displayNames, source) {
|
|
2035
|
+
return entities.map((e) => {
|
|
2036
|
+
const name = displayNames.get(e.canonicalName) ?? e.canonicalName;
|
|
2037
|
+
return { name, ...e.kind ? { kind: e.kind } : {}, source };
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
function toEntityObjects(entityInputs) {
|
|
2041
|
+
return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
|
|
2042
|
+
}
|
|
2043
|
+
async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
|
|
2044
|
+
const row = await findVaultRowInWrite(ctx, memoryId);
|
|
2045
|
+
const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
|
|
2046
|
+
return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
|
|
2047
|
+
}
|
|
1970
2048
|
async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
|
|
1971
2049
|
if (entityInputs.length === 0) return [];
|
|
1972
|
-
const normalized = entityInputs
|
|
2050
|
+
const normalized = toEntityObjects(entityInputs);
|
|
1973
2051
|
const userId = ctx.userId;
|
|
1974
2052
|
let skipped = false;
|
|
1975
2053
|
let entities = [];
|
|
@@ -1979,7 +2057,8 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
|
|
|
1979
2057
|
normalized
|
|
1980
2058
|
);
|
|
1981
2059
|
entities = Array.from(byName.values());
|
|
1982
|
-
|
|
2060
|
+
const vaultRow = await findVaultRowInWrite(ctx, memoryId);
|
|
2061
|
+
if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
|
|
1983
2062
|
if (entityOps.length > 0) {
|
|
1984
2063
|
await ctx.database.batch(...entityOps);
|
|
1985
2064
|
}
|
|
@@ -1990,7 +2069,18 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
|
|
|
1990
2069
|
const existingLinks = await ctx.memoryEntityCollection.query(Q2.where("memory_id", memoryId)).fetch();
|
|
1991
2070
|
const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
|
|
1992
2071
|
const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
|
|
1993
|
-
|
|
2072
|
+
const keptIds = entities.map((e) => e.uniqueId);
|
|
2073
|
+
const carriedOver = await resolveEntitiesByIds(
|
|
2074
|
+
ctx,
|
|
2075
|
+
[...existingEntityIds].filter((id) => !keptIds.includes(id))
|
|
2076
|
+
);
|
|
2077
|
+
const topicsWrite = resolveTopicsWrite(
|
|
2078
|
+
vaultRow,
|
|
2079
|
+
[...carriedOver, ...entities],
|
|
2080
|
+
displayNamesOf(normalized),
|
|
2081
|
+
options?.topicsSource ?? "auto"
|
|
2082
|
+
);
|
|
2083
|
+
if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
|
|
1994
2084
|
const linkOps = toCreate.map(
|
|
1995
2085
|
(e) => ctx.memoryEntityCollection.prepareCreate((record) => {
|
|
1996
2086
|
record._setRaw("memory_id", memoryId);
|
|
@@ -1998,26 +2088,42 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
|
|
|
1998
2088
|
if (userId !== void 0) record._setRaw("user_id", userId);
|
|
1999
2089
|
})
|
|
2000
2090
|
);
|
|
2001
|
-
await ctx.database.batch(
|
|
2091
|
+
await ctx.database.batch(
|
|
2092
|
+
...entityOps,
|
|
2093
|
+
...linkOps,
|
|
2094
|
+
...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
|
|
2095
|
+
);
|
|
2002
2096
|
});
|
|
2003
2097
|
return skipped ? [] : entities;
|
|
2004
2098
|
}
|
|
2005
|
-
async function
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
}
|
|
2099
|
+
async function resolveEntitiesByIds(ctx, entityIds) {
|
|
2100
|
+
if (entityIds.length === 0) return [];
|
|
2101
|
+
const rows = await ctx.entityCollection.query(Q2.where("id", Q2.oneOf(Array.from(entityIds)))).fetch();
|
|
2102
|
+
return rows.map(entityToStored);
|
|
2103
|
+
}
|
|
2104
|
+
function autoLinkBlocked(row) {
|
|
2105
|
+
return !row || !!row.isDeleted || !!row.topicsUserManaged;
|
|
2013
2106
|
}
|
|
2014
2107
|
async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
|
|
2015
|
-
|
|
2108
|
+
return replaceMemoryEntities(ctx, memoryId, entityInputs, {
|
|
2109
|
+
guarded: true,
|
|
2110
|
+
topicsSource: "auto"
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
async function relinkMemoryEntitiesFromTopicsOp(ctx, memoryId, topics) {
|
|
2114
|
+
return replaceMemoryEntities(ctx, memoryId, topics, {
|
|
2115
|
+
guarded: false,
|
|
2116
|
+
topicsSource: null
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
|
|
2120
|
+
const normalized = toEntityObjects(entityInputs);
|
|
2016
2121
|
const userId = ctx.userId;
|
|
2017
2122
|
let skipped = false;
|
|
2018
2123
|
let entities = [];
|
|
2019
2124
|
await ctx.database.write(async () => {
|
|
2020
|
-
|
|
2125
|
+
const vaultRow = await findVaultRowInWrite(ctx, memoryId);
|
|
2126
|
+
if (options.guarded && autoLinkBlocked(vaultRow)) {
|
|
2021
2127
|
skipped = true;
|
|
2022
2128
|
return;
|
|
2023
2129
|
}
|
|
@@ -2031,7 +2137,15 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
|
|
|
2031
2137
|
const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
|
|
2032
2138
|
const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
|
|
2033
2139
|
const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
|
|
2034
|
-
|
|
2140
|
+
const topicsWrite = resolveTopicsWrite(
|
|
2141
|
+
vaultRow,
|
|
2142
|
+
entities,
|
|
2143
|
+
displayNamesOf(normalized),
|
|
2144
|
+
options.topicsSource
|
|
2145
|
+
);
|
|
2146
|
+
if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2035
2149
|
const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
|
|
2036
2150
|
await ctx.database.batch(
|
|
2037
2151
|
...entityOps,
|
|
@@ -2043,7 +2157,8 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
|
|
|
2043
2157
|
})
|
|
2044
2158
|
),
|
|
2045
2159
|
...toDestroy.map((l) => l.prepareDestroyPermanently()),
|
|
2046
|
-
...orphans.map((e) => e.prepareDestroyPermanently())
|
|
2160
|
+
...orphans.map((e) => e.prepareDestroyPermanently()),
|
|
2161
|
+
...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
|
|
2047
2162
|
);
|
|
2048
2163
|
});
|
|
2049
2164
|
return skipped ? null : entities;
|
|
@@ -2253,6 +2368,8 @@ function vaultMemoryToStoredRaw(memory) {
|
|
|
2253
2368
|
eventTimeEnd: memory.eventTimeEnd ?? null,
|
|
2254
2369
|
eventTimeKind: memory.eventTimeKind ?? null,
|
|
2255
2370
|
topicsUserManaged: memory.topicsUserManaged ?? false,
|
|
2371
|
+
topics: parseTopics(memory.topics),
|
|
2372
|
+
topicsUpdatedAt: memory.topicsUpdatedAt ?? null,
|
|
2256
2373
|
topicsExtractedAt: memory.topicsExtractedAt ?? null,
|
|
2257
2374
|
topicsExtractedVersion: memory.topicsExtractedVersion ?? null,
|
|
2258
2375
|
supersededBy: memory.supersededBy ?? null,
|
|
@@ -2529,6 +2646,8 @@ function vaultMemoryRawToStoredRaw(raw) {
|
|
|
2529
2646
|
eventTimeKind: raw.event_time_kind ?? null,
|
|
2530
2647
|
// SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
|
|
2531
2648
|
topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
|
|
2649
|
+
topics: parseTopics(raw.topics),
|
|
2650
|
+
topicsUpdatedAt: raw.topics_updated_at ?? null,
|
|
2532
2651
|
topicsExtractedAt: raw.topics_extracted_at ?? null,
|
|
2533
2652
|
topicsExtractedVersion: raw.topics_extracted_version ?? null,
|
|
2534
2653
|
supersededBy: raw.superseded_by ?? null,
|
|
@@ -2819,13 +2938,23 @@ async function setMemoryEntitiesOp(ctx, memoryId, entities) {
|
|
|
2819
2938
|
});
|
|
2820
2939
|
});
|
|
2821
2940
|
if (stale) return null;
|
|
2822
|
-
const linked = entities.length > 0 ? await linkMemoryEntitiesOp(entityCtx, memoryId, entities) : [];
|
|
2941
|
+
const linked = entities.length > 0 ? await linkMemoryEntitiesOp(entityCtx, memoryId, entities, { topicsSource: "user" }) : [];
|
|
2823
2942
|
const keep = new Set(linked.map((e) => e.uniqueId));
|
|
2824
2943
|
const existing = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", memoryId)).fetch();
|
|
2825
2944
|
const staleLinks = existing.filter((l) => !keep.has(String(l.entityId)));
|
|
2826
|
-
if (staleLinks.length > 0) {
|
|
2945
|
+
if (staleLinks.length > 0 || entities.length === 0) {
|
|
2827
2946
|
await ctx.database.write(async () => {
|
|
2828
|
-
|
|
2947
|
+
const topicsOp = await prepareMemoryTopicsUpdate(
|
|
2948
|
+
entityCtx,
|
|
2949
|
+
memoryId,
|
|
2950
|
+
linked,
|
|
2951
|
+
entities,
|
|
2952
|
+
"user"
|
|
2953
|
+
);
|
|
2954
|
+
await ctx.database.batch(
|
|
2955
|
+
...staleLinks.map((l) => l.prepareDestroyPermanently()),
|
|
2956
|
+
...topicsOp ? [topicsOp] : []
|
|
2957
|
+
);
|
|
2829
2958
|
});
|
|
2830
2959
|
}
|
|
2831
2960
|
return vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
|
|
@@ -2956,56 +3085,111 @@ async function getUnfiledVaultMemoriesOp(ctx) {
|
|
|
2956
3085
|
);
|
|
2957
3086
|
}
|
|
2958
3087
|
var TOPICS_EXTRACTION_VERSION = 3;
|
|
3088
|
+
async function linkedEntityNamesByMemory(entityCtx, memoryIds) {
|
|
3089
|
+
const CHUNK = 500;
|
|
3090
|
+
const entityIdByMemory = /* @__PURE__ */ new Map();
|
|
3091
|
+
const allEntityIds = /* @__PURE__ */ new Set();
|
|
3092
|
+
for (let i = 0; i < memoryIds.length; i += CHUNK) {
|
|
3093
|
+
const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", Q3.oneOf(memoryIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
|
|
3094
|
+
for (const link of links) {
|
|
3095
|
+
const memoryId = String(link.memory_id);
|
|
3096
|
+
const entityId = String(link.entity_id);
|
|
3097
|
+
allEntityIds.add(entityId);
|
|
3098
|
+
let bucket = entityIdByMemory.get(memoryId);
|
|
3099
|
+
if (!bucket) {
|
|
3100
|
+
bucket = /* @__PURE__ */ new Set();
|
|
3101
|
+
entityIdByMemory.set(memoryId, bucket);
|
|
3102
|
+
}
|
|
3103
|
+
bucket.add(entityId);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
const entityIds = Array.from(allEntityIds);
|
|
3107
|
+
const nameById = /* @__PURE__ */ new Map();
|
|
3108
|
+
for (let i = 0; i < entityIds.length; i += CHUNK) {
|
|
3109
|
+
const entities = await entityCtx.entityCollection.query(Q3.where("id", Q3.oneOf(entityIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
|
|
3110
|
+
for (const e of entities) {
|
|
3111
|
+
if (typeof e.canonical_name === "string") nameById.set(String(e.id), e.canonical_name);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
const out = /* @__PURE__ */ new Map();
|
|
3115
|
+
for (const [memoryId, ids] of entityIdByMemory) {
|
|
3116
|
+
const names = /* @__PURE__ */ new Set();
|
|
3117
|
+
for (const id of ids) {
|
|
3118
|
+
const name = nameById.get(id);
|
|
3119
|
+
if (name !== void 0) names.add(name);
|
|
3120
|
+
}
|
|
3121
|
+
if (names.size > 0) out.set(memoryId, names);
|
|
3122
|
+
}
|
|
3123
|
+
return out;
|
|
3124
|
+
}
|
|
3125
|
+
function linksDivergeFromTopics(topics, linked) {
|
|
3126
|
+
const wanted = new Set(
|
|
3127
|
+
topics.map((t) => normalizeEntityName(t.name)).filter((n) => n.length > 0)
|
|
3128
|
+
);
|
|
3129
|
+
if (wanted.size !== linked.size) return true;
|
|
3130
|
+
for (const name of wanted) if (!linked.has(name)) return true;
|
|
3131
|
+
return false;
|
|
3132
|
+
}
|
|
2959
3133
|
async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
|
|
2960
3134
|
const entityCtx = ctx.entityCtx;
|
|
2961
3135
|
if (!entityCtx) {
|
|
2962
3136
|
throw new Error("getMemoriesNeedingTopicExtractionOp requires ctx.entityCtx");
|
|
2963
3137
|
}
|
|
2964
|
-
const conditions = [
|
|
2965
|
-
// Boolean optional column: null (legacy) and false both mean auto-managed.
|
|
2966
|
-
// Q.notEq(true) is a NULL trap on SQLite (NULL <> TRUE is NULL) — enumerate.
|
|
2967
|
-
Q3.or(Q3.where("topics_user_managed", null), Q3.where("topics_user_managed", false)),
|
|
2968
|
-
...baseVaultConditions(ctx),
|
|
2969
|
-
Q3.sortBy("created_at", Q3.desc)
|
|
2970
|
-
];
|
|
3138
|
+
const conditions = [...baseVaultConditions(ctx), Q3.sortBy("created_at", Q3.desc)];
|
|
2971
3139
|
const rows = await ctx.vaultMemoryCollection.query(...conditions).unsafeFetchRaw();
|
|
2972
|
-
const
|
|
2973
|
-
|
|
3140
|
+
const linkedNames = await linkedEntityNamesByMemory(
|
|
3141
|
+
entityCtx,
|
|
3142
|
+
rows.map((r) => r.id)
|
|
3143
|
+
);
|
|
3144
|
+
const pendingRaw = [];
|
|
3145
|
+
const stampedPendingRaw = [];
|
|
3146
|
+
const linkedUnstampedAll = [];
|
|
3147
|
+
const topicsToRelinkAll = [];
|
|
3148
|
+
const topicsBackfillAll = [];
|
|
2974
3149
|
for (const raw of rows) {
|
|
3150
|
+
const id = raw.id;
|
|
3151
|
+
const topics = parseTopics(raw.topics);
|
|
3152
|
+
const linked = linkedNames.get(id);
|
|
3153
|
+
if (topics !== null && topics.length > 0 && linksDivergeFromTopics(topics, linked ?? /* @__PURE__ */ new Set())) {
|
|
3154
|
+
topicsToRelinkAll.push(id);
|
|
3155
|
+
continue;
|
|
3156
|
+
}
|
|
3157
|
+
if (raw.topics_user_managed) {
|
|
3158
|
+
if (topics === null && linked !== void 0) topicsBackfillAll.push(id);
|
|
3159
|
+
continue;
|
|
3160
|
+
}
|
|
2975
3161
|
const stamp = raw.topics_extracted_at ?? null;
|
|
3162
|
+
let isPending = false;
|
|
2976
3163
|
if (stamp !== null) {
|
|
2977
3164
|
const version = raw.topics_extracted_version ?? 0;
|
|
2978
3165
|
if (raw.updated_at > stamp || version < TOPICS_EXTRACTION_VERSION) {
|
|
2979
|
-
|
|
3166
|
+
stampedPendingRaw.push(raw);
|
|
3167
|
+
isPending = true;
|
|
2980
3168
|
}
|
|
2981
|
-
} else {
|
|
2982
|
-
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
|
-
const linkedIds = /* @__PURE__ */ new Set();
|
|
2986
|
-
const CHUNK = 500;
|
|
2987
|
-
for (let i = 0; i < unstamped.length; i += CHUNK) {
|
|
2988
|
-
const ids = unstamped.slice(i, i + CHUNK).map((r) => r.id);
|
|
2989
|
-
const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", Q3.oneOf(ids))).unsafeFetchRaw();
|
|
2990
|
-
for (const link of links) linkedIds.add(String(link.memory_id));
|
|
2991
|
-
}
|
|
2992
|
-
const linkedUnstampedAll = [];
|
|
2993
|
-
const pendingRaw = [...stampedPending];
|
|
2994
|
-
for (const raw of unstamped) {
|
|
2995
|
-
if (linkedIds.has(raw.id)) {
|
|
2996
|
-
linkedUnstampedAll.push(raw.id);
|
|
3169
|
+
} else if (linked !== void 0) {
|
|
3170
|
+
linkedUnstampedAll.push(id);
|
|
2997
3171
|
} else {
|
|
2998
3172
|
pendingRaw.push(raw);
|
|
3173
|
+
isPending = true;
|
|
3174
|
+
}
|
|
3175
|
+
if (!isPending && topics === null && linked !== void 0) {
|
|
3176
|
+
topicsBackfillAll.push(id);
|
|
2999
3177
|
}
|
|
3000
3178
|
}
|
|
3001
3179
|
const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
|
|
3002
|
-
const
|
|
3003
|
-
const
|
|
3180
|
+
const orderedPendingRaw = [...stampedPendingRaw, ...pendingRaw];
|
|
3181
|
+
const limitedPendingRaw = cap !== void 0 ? orderedPendingRaw.slice(0, cap) : orderedPendingRaw;
|
|
3004
3182
|
const pending = await mapInBatches(
|
|
3005
3183
|
limitedPendingRaw,
|
|
3006
3184
|
(raw) => vaultMemoryRawToStored(raw, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
|
|
3007
3185
|
);
|
|
3008
|
-
|
|
3186
|
+
const applyCap = (ids) => cap !== void 0 ? ids.slice(0, cap) : ids;
|
|
3187
|
+
return {
|
|
3188
|
+
pending,
|
|
3189
|
+
linkedUnstamped: applyCap(linkedUnstampedAll),
|
|
3190
|
+
topicsToRelink: applyCap(topicsToRelinkAll),
|
|
3191
|
+
topicsBackfill: applyCap(topicsBackfillAll)
|
|
3192
|
+
};
|
|
3009
3193
|
}
|
|
3010
3194
|
async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = TOPICS_EXTRACTION_VERSION) {
|
|
3011
3195
|
if (memoryIds.length === 0) return [];
|
|
@@ -3039,6 +3223,61 @@ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = T
|
|
|
3039
3223
|
}
|
|
3040
3224
|
return stamped;
|
|
3041
3225
|
}
|
|
3226
|
+
async function relinkMemoryTopicsOp(ctx, memoryIds) {
|
|
3227
|
+
const entityCtx = ctx.entityCtx;
|
|
3228
|
+
if (!entityCtx) {
|
|
3229
|
+
throw new Error("relinkMemoryTopicsOp requires ctx.entityCtx");
|
|
3230
|
+
}
|
|
3231
|
+
const relinked = [];
|
|
3232
|
+
for (const id of Array.from(new Set(memoryIds))) {
|
|
3233
|
+
let record;
|
|
3234
|
+
try {
|
|
3235
|
+
record = await ctx.vaultMemoryCollection.find(id);
|
|
3236
|
+
} catch {
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
|
|
3240
|
+
const topics = parseTopics(record.topics);
|
|
3241
|
+
if (topics === null || topics.length === 0) continue;
|
|
3242
|
+
try {
|
|
3243
|
+
await relinkMemoryEntitiesFromTopicsOp(entityCtx, id, topics);
|
|
3244
|
+
relinked.push(id);
|
|
3245
|
+
} catch (err) {
|
|
3246
|
+
getLogger().warn("[memory/topics] relink failed", err);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
return relinked;
|
|
3250
|
+
}
|
|
3251
|
+
async function backfillMemoryTopicsOp(ctx, memoryIds) {
|
|
3252
|
+
const entityCtx = ctx.entityCtx;
|
|
3253
|
+
if (!entityCtx) {
|
|
3254
|
+
throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
|
|
3255
|
+
}
|
|
3256
|
+
const filled = [];
|
|
3257
|
+
for (const id of Array.from(new Set(memoryIds))) {
|
|
3258
|
+
let record;
|
|
3259
|
+
try {
|
|
3260
|
+
record = await ctx.vaultMemoryCollection.find(id);
|
|
3261
|
+
} catch {
|
|
3262
|
+
continue;
|
|
3263
|
+
}
|
|
3264
|
+
if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
|
|
3265
|
+
if (parseTopics(record.topics) !== null) continue;
|
|
3266
|
+
const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", id)).unsafeFetchRaw();
|
|
3267
|
+
const entityIds = links.map((l) => String(l.entity_id));
|
|
3268
|
+
if (entityIds.length === 0) continue;
|
|
3269
|
+
const entities = await entityCtx.entityCollection.query(Q3.where("id", Q3.oneOf(entityIds))).fetch();
|
|
3270
|
+
if (entities.length === 0) continue;
|
|
3271
|
+
const source = record.topicsUserManaged ? "user" : "auto";
|
|
3272
|
+
await ctx.database.write(async () => {
|
|
3273
|
+
const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
|
|
3274
|
+
if (!topicsOp) return;
|
|
3275
|
+
await ctx.database.batch(topicsOp);
|
|
3276
|
+
filled.push(id);
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
return filled;
|
|
3280
|
+
}
|
|
3042
3281
|
async function deleteAllVaultMemoriesForUserOp(ctx, userId) {
|
|
3043
3282
|
if (ctx.userId !== void 0 && ctx.userId !== userId) return 0;
|
|
3044
3283
|
const records = await ctx.vaultMemoryCollection.query(Q3.where("user_id", userId), Q3.where("is_deleted", false)).fetch();
|
|
@@ -6191,7 +6430,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
|
|
|
6191
6430
|
legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
|
|
6192
6431
|
);
|
|
6193
6432
|
}
|
|
6194
|
-
const { recall: recall2 } = await import("./recall-
|
|
6433
|
+
const { recall: recall2 } = await import("./recall-KTJCUAXN.mjs");
|
|
6195
6434
|
let recallDegraded = [];
|
|
6196
6435
|
const result = await recall2(
|
|
6197
6436
|
query,
|
|
@@ -7257,6 +7496,8 @@ export {
|
|
|
7257
7496
|
TOPICS_EXTRACTION_VERSION,
|
|
7258
7497
|
getMemoriesNeedingTopicExtractionOp,
|
|
7259
7498
|
stampTopicsExtractedAtOp,
|
|
7499
|
+
relinkMemoryTopicsOp,
|
|
7500
|
+
backfillMemoryTopicsOp,
|
|
7260
7501
|
deleteAllVaultMemoriesForUserOp,
|
|
7261
7502
|
assertVaultScopeForSweep,
|
|
7262
7503
|
getDecayCandidatesRawOp,
|