mnemocyte 0.1.0 → 0.1.2
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/README.md +125 -142
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +181 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -24,12 +24,16 @@ const vector = customType({
|
|
|
24
24
|
return `vector(${typeof config === "number" ? config : 1536})`;
|
|
25
25
|
},
|
|
26
26
|
toDriver(value) {
|
|
27
|
-
return `[${value.join(",")}]`;
|
|
27
|
+
return `[${value.map(formatVectorComponent$1).join(",")}]`;
|
|
28
28
|
},
|
|
29
29
|
fromDriver(value) {
|
|
30
30
|
return value.slice(1, -1).split(",").filter(Boolean).map(Number);
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
|
+
function formatVectorComponent$1(value) {
|
|
34
|
+
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
35
|
+
return Object.is(value, -0) ? "0" : value.toFixed(17);
|
|
36
|
+
}
|
|
33
37
|
const memoriesTable = pgTable("mnemocyte_memories", {
|
|
34
38
|
id: text("id").primaryKey(),
|
|
35
39
|
entityId: text("entity_id").notNull(),
|
|
@@ -43,7 +47,7 @@ const memoriesTable = pgTable("mnemocyte_memories", {
|
|
|
43
47
|
embedding: vector("embedding", { dimensions: 1536 }),
|
|
44
48
|
embeddingModel: text("embedding_model").notNull(),
|
|
45
49
|
embeddingDimensions: integer("embedding_dimensions").notNull(),
|
|
46
|
-
supersededBy: text("superseded_by"),
|
|
50
|
+
supersededBy: text("superseded_by").references(() => memoriesTable.id),
|
|
47
51
|
supersededAt: timestamp("superseded_at", { withTimezone: true }),
|
|
48
52
|
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
49
53
|
lastAccessedAt: timestamp("last_accessed_at", { withTimezone: true }),
|
|
@@ -138,7 +142,11 @@ function formatMetadata(memory) {
|
|
|
138
142
|
if (memory.tags.length > 0) parts.push(`tags=${memory.tags.join(",")}`);
|
|
139
143
|
return parts.join("; ");
|
|
140
144
|
}
|
|
141
|
-
function
|
|
145
|
+
function markdownFence(value) {
|
|
146
|
+
const longestBacktickRun = Math.max(0, ...Array.from(value.matchAll(/`+/g), (match) => match[0].length));
|
|
147
|
+
return "`".repeat(Math.max(3, longestBacktickRun + 1));
|
|
148
|
+
}
|
|
149
|
+
function formatMarkdown(memories, query, omittedCount) {
|
|
142
150
|
const lines = [
|
|
143
151
|
"# Memory Context",
|
|
144
152
|
"",
|
|
@@ -146,10 +154,14 @@ function formatMarkdown(memories, query) {
|
|
|
146
154
|
"",
|
|
147
155
|
"## Relevant Memories"
|
|
148
156
|
];
|
|
149
|
-
for (const memory of memories)
|
|
157
|
+
for (const memory of memories) {
|
|
158
|
+
const fence = markdownFence(memory.content);
|
|
159
|
+
lines.push("", `- ${formatMetadata(memory)}`, ` ${fence}text`, ...memory.content.split("\n").map((line) => ` ${line}`), ` ${fence}`);
|
|
160
|
+
}
|
|
161
|
+
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
150
162
|
return lines.join("\n");
|
|
151
163
|
}
|
|
152
|
-
function formatPlain(memories, query) {
|
|
164
|
+
function formatPlain(memories, query, omittedCount) {
|
|
153
165
|
const lines = [
|
|
154
166
|
"MEMORY CONTEXT",
|
|
155
167
|
"",
|
|
@@ -157,20 +169,22 @@ function formatPlain(memories, query) {
|
|
|
157
169
|
"",
|
|
158
170
|
"RELEVANT MEMORIES"
|
|
159
171
|
];
|
|
160
|
-
for (const memory of memories) lines.push("", memory.content,
|
|
172
|
+
for (const [index, memory] of memories.entries()) lines.push("", `--- MEMORY ${index + 1} START ---`, formatMetadata(memory), memory.content, `--- MEMORY ${index + 1} END ---`);
|
|
173
|
+
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
161
174
|
return lines.join("\n");
|
|
162
175
|
}
|
|
163
|
-
function formatXml(memories, query) {
|
|
176
|
+
function formatXml(memories, query, omittedCount) {
|
|
164
177
|
const lines = [`<memory_context query="${escapeXml(query)}">`];
|
|
165
178
|
for (const memory of memories) lines.push(` <memory id="${escapeXml(memory.id)}" type="${escapeXml(memory.type)}" importance="${escapeXml(memory.importance)}" score="${memory.score.toFixed(3)}">`, ` <content>${escapeXml(memory.content)}</content>`, ` <metadata>${escapeXml(formatMetadata(memory))}</metadata>`, " </memory>");
|
|
179
|
+
if (omittedCount > 0) lines.push(` <omitted count="${omittedCount}" reason="token_budget" />`);
|
|
166
180
|
lines.push("</memory_context>");
|
|
167
181
|
return lines.join("\n");
|
|
168
182
|
}
|
|
169
|
-
function formatContext(memories, query, format) {
|
|
183
|
+
function formatContext(memories, query, format, omittedCount = 0) {
|
|
170
184
|
switch (format) {
|
|
171
|
-
case "markdown": return formatMarkdown(memories, query);
|
|
172
|
-
case "plain": return formatPlain(memories, query);
|
|
173
|
-
case "xml": return formatXml(memories, query);
|
|
185
|
+
case "markdown": return formatMarkdown(memories, query, omittedCount);
|
|
186
|
+
case "plain": return formatPlain(memories, query, omittedCount);
|
|
187
|
+
case "xml": return formatXml(memories, query, omittedCount);
|
|
174
188
|
}
|
|
175
189
|
}
|
|
176
190
|
//#endregion
|
|
@@ -199,7 +213,21 @@ async function buildContext(options) {
|
|
|
199
213
|
const format = options.input.format ?? "markdown";
|
|
200
214
|
const maxTokens = options.input.maxTokens ?? 1200;
|
|
201
215
|
const tokenCounter = options.input.tokenCounter ?? heuristicTokenCounter;
|
|
202
|
-
|
|
216
|
+
const memories = await options.recall(options.input);
|
|
217
|
+
let low = 0;
|
|
218
|
+
let high = memories.length;
|
|
219
|
+
let bestContext = "";
|
|
220
|
+
while (low <= high) {
|
|
221
|
+
const mid = Math.floor((low + high) / 2);
|
|
222
|
+
const omittedCount = memories.length - mid;
|
|
223
|
+
const context = formatContext(memories.slice(0, mid), options.input.query, format, omittedCount);
|
|
224
|
+
if (tokenCounter.count(context) <= maxTokens) {
|
|
225
|
+
bestContext = context;
|
|
226
|
+
low = mid + 1;
|
|
227
|
+
} else high = mid - 1;
|
|
228
|
+
}
|
|
229
|
+
if (bestContext || memories.length === 0) return bestContext;
|
|
230
|
+
return trimToTokenBudget(formatContext([], options.input.query, format, memories.length), maxTokens, tokenCounter);
|
|
203
231
|
}
|
|
204
232
|
//#endregion
|
|
205
233
|
//#region src/observability.ts
|
|
@@ -499,6 +527,22 @@ async function embedOne(embedder, text, options = {}) {
|
|
|
499
527
|
if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
|
|
500
528
|
return embedding;
|
|
501
529
|
}
|
|
530
|
+
async function embedMany(embedder, texts, options = {}) {
|
|
531
|
+
if (texts.length === 0) return [];
|
|
532
|
+
let embeddings;
|
|
533
|
+
try {
|
|
534
|
+
embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed(texts) : embedder.embed(texts, { signal }), {
|
|
535
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
536
|
+
...options.resilience === void 0 ? {} : { resilience: options.resilience }
|
|
537
|
+
});
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
|
|
540
|
+
throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
|
|
541
|
+
}
|
|
542
|
+
if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
|
|
543
|
+
for (const embedding of embeddings) if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
|
|
544
|
+
return embeddings;
|
|
545
|
+
}
|
|
502
546
|
function validateRememberInput(input) {
|
|
503
547
|
assertNonEmptyString(input.entityId, "entityId");
|
|
504
548
|
assertNonEmptyString(input.content, "content");
|
|
@@ -778,8 +822,45 @@ function createInMemoryClient(config) {
|
|
|
778
822
|
async rememberMany(inputs) {
|
|
779
823
|
return observe(config, "in-memory", "rememberMany", { count: inputs.length }, async () => {
|
|
780
824
|
assertOpen();
|
|
825
|
+
if (inputs.length === 0) return [];
|
|
826
|
+
for (const input of inputs) validateRememberInput(input);
|
|
827
|
+
const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
|
|
828
|
+
...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
|
|
829
|
+
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
830
|
+
});
|
|
831
|
+
const now = /* @__PURE__ */ new Date();
|
|
781
832
|
const result = [];
|
|
782
|
-
for (
|
|
833
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
834
|
+
const input = inputs[i];
|
|
835
|
+
const memory = {
|
|
836
|
+
id: createId(),
|
|
837
|
+
entityId: input.entityId,
|
|
838
|
+
content: input.content,
|
|
839
|
+
type: input.type ?? "fact",
|
|
840
|
+
importance: input.importance ?? "normal",
|
|
841
|
+
tags: normalizeTags(input.tags),
|
|
842
|
+
source: input.source ?? null,
|
|
843
|
+
metadata: input.metadata ?? {},
|
|
844
|
+
confidence: input.confidence ?? 1,
|
|
845
|
+
embeddingModel: config.embedder.model,
|
|
846
|
+
embeddingDimensions: config.embedder.dimensions,
|
|
847
|
+
supersededBy: null,
|
|
848
|
+
supersededAt: null,
|
|
849
|
+
expiresAt: input.expiresAt ?? null,
|
|
850
|
+
lastAccessedAt: null,
|
|
851
|
+
accessCount: 0,
|
|
852
|
+
createdAt: now,
|
|
853
|
+
updatedAt: now,
|
|
854
|
+
embedding: embeddings[i]
|
|
855
|
+
};
|
|
856
|
+
memories.set(memory.id, memory);
|
|
857
|
+
recordAudit(memory.entityId, "memory.created", {
|
|
858
|
+
memoryId: memory.id,
|
|
859
|
+
type: memory.type,
|
|
860
|
+
importance: memory.importance
|
|
861
|
+
});
|
|
862
|
+
result.push(cloneMemory(memory));
|
|
863
|
+
}
|
|
783
864
|
return result;
|
|
784
865
|
}, (result) => ({ count: result.length }));
|
|
785
866
|
},
|
|
@@ -1009,16 +1090,24 @@ async function listEvents(db, filter) {
|
|
|
1009
1090
|
//#endregion
|
|
1010
1091
|
//#region src/db/queries/memories.ts
|
|
1011
1092
|
function vectorLiteral(embedding) {
|
|
1012
|
-
return `[${embedding.join(",")}]`;
|
|
1093
|
+
return `[${embedding.map(formatVectorComponent).join(",")}]`;
|
|
1094
|
+
}
|
|
1095
|
+
function formatVectorComponent(value) {
|
|
1096
|
+
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
1097
|
+
return Object.is(value, -0) ? "0" : value.toFixed(17);
|
|
1013
1098
|
}
|
|
1014
1099
|
function filterConditions(filter) {
|
|
1015
|
-
return and(eq(memoriesTable.entityId, filter.entityId), filter.includeSuperseded ? void 0 : isNull(memoriesTable.supersededBy), filter.includeExpired ? void 0 : sql`(${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > now())`, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags}
|
|
1100
|
+
return and(eq(memoriesTable.entityId, filter.entityId), filter.includeSuperseded ? void 0 : isNull(memoriesTable.supersededBy), filter.includeExpired ? void 0 : sql`(${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > now())`, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags} @> ${filter.tags}` : void 0, filter.before ? lt(memoriesTable.createdAt, filter.before) : void 0, filter.after ? gt(memoriesTable.createdAt, filter.after) : void 0);
|
|
1016
1101
|
}
|
|
1017
1102
|
async function insertMemory(db, row) {
|
|
1018
1103
|
const inserted = (await db.insert(memoriesTable).values(row).returning())[0];
|
|
1019
1104
|
if (!inserted) throw new Error("Memory insert returned no rows.");
|
|
1020
1105
|
return inserted;
|
|
1021
1106
|
}
|
|
1107
|
+
async function insertMemories(db, rows) {
|
|
1108
|
+
if (rows.length === 0) return [];
|
|
1109
|
+
return db.insert(memoriesTable).values(rows).returning();
|
|
1110
|
+
}
|
|
1022
1111
|
async function getMemoryById(db, entityId, memoryId) {
|
|
1023
1112
|
return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
|
|
1024
1113
|
}
|
|
@@ -1210,7 +1299,7 @@ async function vectorSearch(db, input) {
|
|
|
1210
1299
|
${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
|
|
1211
1300
|
${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
|
|
1212
1301
|
${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
1213
|
-
${input.tags && input.tags.length > 0 ? sql`AND tags
|
|
1302
|
+
${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
|
|
1214
1303
|
${input.before ? sql`AND created_at < ${input.before}` : sql``}
|
|
1215
1304
|
${input.after ? sql`AND created_at > ${input.after}` : sql``}
|
|
1216
1305
|
AND 1 - (embedding <=> ${embedding}::vector) >= ${minScore}
|
|
@@ -1247,7 +1336,7 @@ async function lexicalSearch(db, input) {
|
|
|
1247
1336
|
${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
|
|
1248
1337
|
${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
|
|
1249
1338
|
${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
1250
|
-
${input.tags && input.tags.length > 0 ? sql`AND tags
|
|
1339
|
+
${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
|
|
1251
1340
|
${input.before ? sql`AND created_at < ${input.before}` : sql``}
|
|
1252
1341
|
${input.after ? sql`AND created_at > ${input.after}` : sql``}
|
|
1253
1342
|
AND to_tsvector('english', content) @@ websearch_to_tsquery('english', ${input.query})
|
|
@@ -1277,16 +1366,20 @@ async function hybridRecall(input) {
|
|
|
1277
1366
|
for (const row of vectorRows) merged.set(row.id, {
|
|
1278
1367
|
row,
|
|
1279
1368
|
vectorScore: row.vectorScore,
|
|
1280
|
-
lexicalScore:
|
|
1369
|
+
lexicalScore: lexicalScore(row.content, input.input.query)
|
|
1281
1370
|
});
|
|
1282
1371
|
for (const row of lexicalRows) {
|
|
1283
1372
|
const existing = merged.get(row.id);
|
|
1284
1373
|
if (existing) existing.lexicalScore = row.lexicalScore;
|
|
1285
|
-
else
|
|
1286
|
-
row
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1374
|
+
else {
|
|
1375
|
+
const embedding = row.embedding;
|
|
1376
|
+
const sim = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
|
|
1377
|
+
merged.set(row.id, {
|
|
1378
|
+
row,
|
|
1379
|
+
vectorScore: Math.max(0, sim),
|
|
1380
|
+
lexicalScore: row.lexicalScore
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1290
1383
|
}
|
|
1291
1384
|
const scored = Array.from(merged.values()).map((entry) => toScoredMemory(rowToMemory(entry.row), Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input.input, input.retrieval)).filter((memory) => memory.score >= input.minScore).sort((a, b) => b.score - a.score).slice(0, input.limit);
|
|
1292
1385
|
await markMemoryAccessed(input.db, scored.map((memory) => memory.id));
|
|
@@ -1387,9 +1480,41 @@ function createPostgresClient(config, handle) {
|
|
|
1387
1480
|
async rememberMany(inputs) {
|
|
1388
1481
|
return observe(config, "postgres", "rememberMany", { count: inputs.length }, async () => {
|
|
1389
1482
|
assertOpen();
|
|
1390
|
-
|
|
1391
|
-
for (const input of inputs)
|
|
1392
|
-
|
|
1483
|
+
if (inputs.length === 0) return [];
|
|
1484
|
+
for (const input of inputs) validateRememberInput(input);
|
|
1485
|
+
const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
|
|
1486
|
+
...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
|
|
1487
|
+
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
1488
|
+
});
|
|
1489
|
+
const now = /* @__PURE__ */ new Date();
|
|
1490
|
+
const rows = inputs.map((input, idx) => ({
|
|
1491
|
+
id: createId(),
|
|
1492
|
+
entityId: input.entityId,
|
|
1493
|
+
content: input.content,
|
|
1494
|
+
type: input.type ?? "fact",
|
|
1495
|
+
importance: input.importance ?? "normal",
|
|
1496
|
+
tags: normalizeTags(input.tags),
|
|
1497
|
+
source: input.source ?? null,
|
|
1498
|
+
metadata: input.metadata ?? {},
|
|
1499
|
+
confidence: input.confidence ?? 1,
|
|
1500
|
+
embedding: embeddings[idx],
|
|
1501
|
+
embeddingModel: config.embedder.model,
|
|
1502
|
+
embeddingDimensions: config.embedder.dimensions,
|
|
1503
|
+
supersededBy: null,
|
|
1504
|
+
supersededAt: null,
|
|
1505
|
+
expiresAt: input.expiresAt ?? null,
|
|
1506
|
+
lastAccessedAt: null,
|
|
1507
|
+
accessCount: 0,
|
|
1508
|
+
createdAt: now,
|
|
1509
|
+
updatedAt: now
|
|
1510
|
+
}));
|
|
1511
|
+
const memories = (await insertMemories(handle.db, rows)).map(rowToMemory);
|
|
1512
|
+
for (const memory of memories) recordAudit(memory.entityId, "memory.created", {
|
|
1513
|
+
memoryId: memory.id,
|
|
1514
|
+
type: memory.type,
|
|
1515
|
+
importance: memory.importance
|
|
1516
|
+
});
|
|
1517
|
+
return memories;
|
|
1393
1518
|
}, (result) => ({ count: result.length }));
|
|
1394
1519
|
},
|
|
1395
1520
|
async recall(input) {
|
|
@@ -1557,27 +1682,39 @@ function createPostgresClient(config, handle) {
|
|
|
1557
1682
|
supersededIds: []
|
|
1558
1683
|
};
|
|
1559
1684
|
const now = /* @__PURE__ */ new Date();
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
ids: losers.map((loser) => loser.id),
|
|
1564
|
-
now
|
|
1565
|
-
});
|
|
1566
|
-
const newSupersededIds = updated.map((row) => row.id);
|
|
1567
|
-
for (const id of newSupersededIds) await recordAudit(input.entityId, "memory.superseded", {
|
|
1568
|
-
memoryId: id,
|
|
1569
|
-
supersededBy: survivor.id
|
|
1570
|
-
});
|
|
1571
|
-
if (input.mergeTags !== false && updated.length > 0) {
|
|
1572
|
-
const mergedTags = new Set(survivor.tags);
|
|
1573
|
-
for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
|
|
1574
|
-
if (mergedTags.size !== survivor.tags.length) await setMemoryTags(handle.db, {
|
|
1685
|
+
const { newSupersededIds } = await handle.db.transaction(async (tx) => {
|
|
1686
|
+
const updated = await markMemoriesSuperseded(tx, {
|
|
1687
|
+
survivorId: survivor.id,
|
|
1575
1688
|
entityId: input.entityId,
|
|
1576
|
-
|
|
1577
|
-
tags: [...mergedTags],
|
|
1689
|
+
ids: losers.map((loser) => loser.id),
|
|
1578
1690
|
now
|
|
1579
1691
|
});
|
|
1580
|
-
|
|
1692
|
+
const newSupersededIds = updated.map((row) => row.id);
|
|
1693
|
+
for (const id of newSupersededIds) await insertEvent(tx, {
|
|
1694
|
+
id: createEventId(),
|
|
1695
|
+
entityId: input.entityId,
|
|
1696
|
+
description: "memory.superseded",
|
|
1697
|
+
metadata: {
|
|
1698
|
+
memoryId: id,
|
|
1699
|
+
supersededBy: survivor.id
|
|
1700
|
+
},
|
|
1701
|
+
timestamp: now
|
|
1702
|
+
});
|
|
1703
|
+
if (input.mergeTags !== false && updated.length > 0) {
|
|
1704
|
+
const mergedTags = new Set(survivor.tags);
|
|
1705
|
+
for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
|
|
1706
|
+
if (mergedTags.size !== survivor.tags.length) await setMemoryTags(tx, {
|
|
1707
|
+
entityId: input.entityId,
|
|
1708
|
+
memoryId: survivor.id,
|
|
1709
|
+
tags: [...mergedTags],
|
|
1710
|
+
now
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
return {
|
|
1714
|
+
updated,
|
|
1715
|
+
newSupersededIds
|
|
1716
|
+
};
|
|
1717
|
+
});
|
|
1581
1718
|
return {
|
|
1582
1719
|
survivorId: survivor.id,
|
|
1583
1720
|
supersededCount: newSupersededIds.length,
|