openclaw-memory-alibaba-local 1.0.10 → 1.0.12
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/db.ts +22 -16
- package/index.ts +45 -28
- package/package.json +1 -1
- package/prompts.ts +7 -5
package/db.ts
CHANGED
|
@@ -510,7 +510,7 @@ export class MemoryDB {
|
|
|
510
510
|
});
|
|
511
511
|
if (hasFts) {
|
|
512
512
|
_ftsIndexExists = true;
|
|
513
|
-
console.
|
|
513
|
+
console.debug("[openclaw-memory-alibaba-local] text FTS index already exists");
|
|
514
514
|
return;
|
|
515
515
|
}
|
|
516
516
|
} catch {
|
|
@@ -519,13 +519,13 @@ export class MemoryDB {
|
|
|
519
519
|
|
|
520
520
|
// Create FTS index (no minimum row requirement)
|
|
521
521
|
try {
|
|
522
|
-
console.
|
|
522
|
+
console.debug("[openclaw-memory-alibaba-local] creating FTS index on text column...");
|
|
523
523
|
const lancedb = await loadLanceDB();
|
|
524
524
|
await this.table.createIndex("text", {
|
|
525
525
|
config: (lancedb as any).Index.fts(),
|
|
526
526
|
});
|
|
527
527
|
_ftsIndexExists = true;
|
|
528
|
-
console.
|
|
528
|
+
console.debug("[openclaw-memory-alibaba-local] text FTS index created successfully");
|
|
529
529
|
} catch (e) {
|
|
530
530
|
console.warn("[openclaw-memory-alibaba-local] createIndex(text FTS) failed:", String(e));
|
|
531
531
|
_ftsIndexExists = false;
|
|
@@ -610,32 +610,38 @@ export class MemoryDB {
|
|
|
610
610
|
});
|
|
611
611
|
if (hasVecIdx) {
|
|
612
612
|
_vectorIndexExists = true;
|
|
613
|
-
console.
|
|
613
|
+
console.debug("[openclaw-memory-alibaba-local] vector ANN index already exists");
|
|
614
614
|
return;
|
|
615
615
|
}
|
|
616
616
|
} catch {
|
|
617
617
|
// listIndices not supported or failed; continue to try creating
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
-
// Check row count; IVF_PQ requires ≥256 training rows
|
|
621
|
-
|
|
620
|
+
// Check row count; IVF_PQ requires ≥256 training rows.
|
|
621
|
+
// Exclude full_context_* categories (zero-vector placeholders, no real embeddings).
|
|
622
|
+
let vectorRowCount: number;
|
|
622
623
|
try {
|
|
623
|
-
|
|
624
|
+
const excludeCats = [FULL_CONTEXT_MEMORY, ...FULL_CONTEXT_SOURCE_CATEGORIES]
|
|
625
|
+
.map((c) => `'${sqlEscapeLiteral(String(c))}'`)
|
|
626
|
+
.join(", ");
|
|
627
|
+
const totalRows = await this.table.countRows();
|
|
628
|
+
const fullContextRows = await this.table.countRows(`category IN (${excludeCats})`);
|
|
629
|
+
vectorRowCount = totalRows - fullContextRows;
|
|
624
630
|
} catch {
|
|
625
631
|
return; // cannot determine; skip
|
|
626
632
|
}
|
|
627
|
-
if (
|
|
633
|
+
if (vectorRowCount < VECTOR_INDEX_MIN_ROWS) {
|
|
628
634
|
_vectorIndexExists = false;
|
|
629
|
-
console.
|
|
630
|
-
`[openclaw-memory-alibaba-local] vector ANN index skipped: ${
|
|
635
|
+
console.debug(
|
|
636
|
+
`[openclaw-memory-alibaba-local] vector ANN index skipped: ${vectorRowCount} vector rows < ${VECTOR_INDEX_MIN_ROWS} minimum`,
|
|
631
637
|
);
|
|
632
638
|
return;
|
|
633
639
|
}
|
|
634
640
|
|
|
635
641
|
// Create IVF_PQ index
|
|
636
642
|
try {
|
|
637
|
-
console.
|
|
638
|
-
`[openclaw-memory-alibaba-local] creating IVF_PQ vector index (${
|
|
643
|
+
console.debug(
|
|
644
|
+
`[openclaw-memory-alibaba-local] creating IVF_PQ vector index (${vectorRowCount} vector rows, dim=${this.vectorDim})...`,
|
|
639
645
|
);
|
|
640
646
|
const lancedb = await loadLanceDB();
|
|
641
647
|
await this.table.createIndex("vector", {
|
|
@@ -1369,7 +1375,7 @@ export class MemoryDB {
|
|
|
1369
1375
|
// 2. Count remaining; if over maxRows, trim oldest
|
|
1370
1376
|
const remaining = await this.table!.countRows(base);
|
|
1371
1377
|
if (remaining <= maxRows) {
|
|
1372
|
-
console.
|
|
1378
|
+
console.debug(`[openclaw-memory-alibaba-local] gcFullContext agent=${agentId} remaining=${remaining} (within limit)`);
|
|
1373
1379
|
return;
|
|
1374
1380
|
}
|
|
1375
1381
|
const excess = remaining - maxRows;
|
|
@@ -1383,7 +1389,7 @@ export class MemoryDB {
|
|
|
1383
1389
|
const cutoff = Number(oldest[excess - 1]!.createdAt);
|
|
1384
1390
|
await this.table!.delete(`${base} AND createdAt <= ${Math.floor(cutoff)}`);
|
|
1385
1391
|
}
|
|
1386
|
-
console.
|
|
1392
|
+
console.debug(`[openclaw-memory-alibaba-local] gcFullContext agent=${agentId} deleted ${excess} excess rows (was ${remaining}, cap ${maxRows})`);
|
|
1387
1393
|
}
|
|
1388
1394
|
|
|
1389
1395
|
/**
|
|
@@ -1435,7 +1441,7 @@ export class MemoryDB {
|
|
|
1435
1441
|
const afterSoft = await this.table!.countRows(base);
|
|
1436
1442
|
const softDeleted = count - afterSoft;
|
|
1437
1443
|
if (softDeleted > 0) {
|
|
1438
|
-
console.
|
|
1444
|
+
console.debug(`[openclaw-memory-alibaba-local] gcWorldFact agent=${agentId} phase1: deleted ${softDeleted} old+low-importance rows (was ${count}, now ${afterSoft})`);
|
|
1439
1445
|
}
|
|
1440
1446
|
count = afterSoft;
|
|
1441
1447
|
}
|
|
@@ -1452,7 +1458,7 @@ export class MemoryDB {
|
|
|
1452
1458
|
const cutoff = Number(oldest[excess - 1]!.createdAt);
|
|
1453
1459
|
await this.table!.delete(`${base} AND createdAt <= ${Math.floor(cutoff)}`);
|
|
1454
1460
|
}
|
|
1455
|
-
console.
|
|
1461
|
+
console.debug(`[openclaw-memory-alibaba-local] gcWorldFact agent=${agentId} phase2: deleted ${excess} excess rows (was ${count}, cap ${hardMaxRows})`);
|
|
1456
1462
|
}
|
|
1457
1463
|
}
|
|
1458
1464
|
}
|
package/index.ts
CHANGED
|
@@ -190,7 +190,7 @@ function getThresholdForCategory(cfg: MemoryConfig, category: MemoryCategory): n
|
|
|
190
190
|
|
|
191
191
|
/** 精简日志:仅记录 tag + prompt 字符数,不贴原文。 */
|
|
192
192
|
function logLlmCall(tag: string, promptChars: number): void {
|
|
193
|
-
console.
|
|
193
|
+
console.debug(`[openclaw-memory-alibaba-local] llm ${tag} prompt (${promptChars} chars)`);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/** 记录 userImageExtraction 结果统计。 */
|
|
@@ -198,12 +198,12 @@ function logUserImageResult(total: number, actions: ReadonlyArray<{ action: stri
|
|
|
198
198
|
const counts: Record<string, number> = {};
|
|
199
199
|
for (const a of actions) counts[a.action] = (counts[a.action] ?? 0) + 1;
|
|
200
200
|
const parts = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
201
|
-
console.
|
|
201
|
+
console.debug(`[openclaw-memory-alibaba-local] userImageExtraction result: ${total} items → ${parts}`);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
/** 记忆衰减为纯公式,不向模型发提示词;仍打日志避免与「冲突检测 LLM」混淆。 */
|
|
205
205
|
function logMemoryDecayNoLlm(phase: string, detail: string): void {
|
|
206
|
-
console.
|
|
206
|
+
console.debug(`[openclaw-memory-alibaba-local] memoryDecay ${phase} (no LLM; formula-only) ${detail}`);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
// ---- Embedding-based clustering utilities ----
|
|
@@ -556,7 +556,7 @@ async function extractUserMemoriesWithLLM(
|
|
|
556
556
|
|
|
557
557
|
const raw = completion?.choices[0]?.message?.content?.trim() ?? "";
|
|
558
558
|
const items = parseExtractions(raw);
|
|
559
|
-
console.
|
|
559
|
+
console.debug(`[openclaw-memory-alibaba-local] memoryExtraction extracted ${items.length} items`);
|
|
560
560
|
return items;
|
|
561
561
|
}
|
|
562
562
|
|
|
@@ -843,47 +843,60 @@ async function extractWorldImageWithLLM(
|
|
|
843
843
|
}>;
|
|
844
844
|
};
|
|
845
845
|
const list = Array.isArray(parsed.actions) ? parsed.actions : [];
|
|
846
|
-
|
|
846
|
+
// Per-index: accumulate deletes separately from the final action (insert/update/skip)
|
|
847
|
+
const deleteMap = new Map<number, WorldImageAction[]>(); // delete actions per index
|
|
848
|
+
const finalMap = new Map<number, WorldImageAction>(); // final action per index (insert/update/skip)
|
|
847
849
|
// Default: insert for each index
|
|
848
850
|
for (let i = 0; i < newItems.length; i++) {
|
|
849
|
-
|
|
851
|
+
finalMap.set(i, {
|
|
850
852
|
action: "insert" as const,
|
|
851
853
|
text: newItems[i]!.text,
|
|
852
854
|
importance: newItems[i]!.importance,
|
|
853
|
-
}
|
|
855
|
+
});
|
|
854
856
|
}
|
|
855
857
|
|
|
856
858
|
for (const a of list) {
|
|
857
859
|
const idx = typeof a.index === "number" ? a.index : -1;
|
|
858
860
|
if (idx < 0 || idx >= newItems.length) continue;
|
|
859
861
|
|
|
860
|
-
if (a.action === "
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
862
|
+
if (a.action === "delete" && typeof a.memoryId === "string" && existingIdSet.has(a.memoryId)) {
|
|
863
|
+
// Accumulate deletes — they don't replace the final action
|
|
864
|
+
const dels = deleteMap.get(idx) ?? [];
|
|
865
|
+
dels.push({ action: "delete", memoryId: a.memoryId });
|
|
866
|
+
deleteMap.set(idx, dels);
|
|
867
|
+
} else if (a.action === "skip") {
|
|
868
|
+
finalMap.set(idx, { action: "skip" });
|
|
865
869
|
} else if (a.action === "update" && typeof a.memoryId === "string" && existingIdSet.has(a.memoryId)) {
|
|
866
870
|
const text = typeof a.text === "string" ? a.text.trim() : "";
|
|
867
871
|
if (text.length < 10) continue;
|
|
868
|
-
|
|
872
|
+
finalMap.set(idx, {
|
|
869
873
|
action: "update",
|
|
870
874
|
memoryId: a.memoryId,
|
|
871
875
|
text,
|
|
872
876
|
importance: clampImportance(a.importance ?? newItems[idx]!.importance),
|
|
873
|
-
}
|
|
877
|
+
});
|
|
874
878
|
} else if (a.action === "insert") {
|
|
875
879
|
const text = typeof a.text === "string" ? a.text.trim() : "";
|
|
876
880
|
const finalText = text.length >= 10 ? text : newItems[idx]!.text;
|
|
877
|
-
|
|
881
|
+
finalMap.set(idx, {
|
|
878
882
|
action: "insert",
|
|
879
883
|
text: finalText,
|
|
880
884
|
importance: clampImportance(a.importance ?? newItems[idx]!.importance),
|
|
881
|
-
}
|
|
885
|
+
});
|
|
882
886
|
}
|
|
883
887
|
}
|
|
888
|
+
// Assemble: deletes first, then final action per index
|
|
884
889
|
const result: WorldImageAction[] = [];
|
|
885
890
|
for (let i = 0; i < newItems.length; i++) {
|
|
886
|
-
|
|
891
|
+
const dels = deleteMap.get(i);
|
|
892
|
+
const final = finalMap.get(i);
|
|
893
|
+
// If SKIP + deletes for same index: honor SKIP (don't delete), per user rule
|
|
894
|
+
if (final?.action === "skip") {
|
|
895
|
+
result.push(final);
|
|
896
|
+
} else {
|
|
897
|
+
if (dels) result.push(...dels);
|
|
898
|
+
if (final) result.push(final);
|
|
899
|
+
}
|
|
887
900
|
}
|
|
888
901
|
return result;
|
|
889
902
|
} catch (err: unknown) {
|
|
@@ -1088,7 +1101,7 @@ async function runAgentEndCapture(
|
|
|
1088
1101
|
let { roleCounts: saved, lastMessagesLength } = resolveRoleCountsForSession(entry, messages);
|
|
1089
1102
|
|
|
1090
1103
|
if (messages.length < lastMessagesLength) {
|
|
1091
|
-
console.
|
|
1104
|
+
console.debug("[openclaw-memory-alibaba-local] transcript shrank; reset per-role capture cursors");
|
|
1092
1105
|
saved = {};
|
|
1093
1106
|
}
|
|
1094
1107
|
|
|
@@ -1105,7 +1118,7 @@ async function runAgentEndCapture(
|
|
|
1105
1118
|
}
|
|
1106
1119
|
map[key] = { version: 2, roleCounts: { ...running }, lastMessagesLength: messages.length };
|
|
1107
1120
|
saveAgentEndCursorMap(lancedbDir, map);
|
|
1108
|
-
console.
|
|
1121
|
+
console.debug(
|
|
1109
1122
|
`[openclaw-memory-alibaba-local] agent_end cursor-only advance (non-user trigger) messages=${messages.length}`,
|
|
1110
1123
|
);
|
|
1111
1124
|
return;
|
|
@@ -1167,7 +1180,7 @@ async function runAgentEndCapture(
|
|
|
1167
1180
|
const batchId = randomUUID();
|
|
1168
1181
|
const sid = sessionKey;
|
|
1169
1182
|
|
|
1170
|
-
console.
|
|
1183
|
+
console.debug(`[openclaw-memory-alibaba-local] agentEndCapture fullRows=${fullRows.length} userTexts=${userRawTexts.length} uaLines=${uaLines.length}`);
|
|
1171
1184
|
|
|
1172
1185
|
if (fullRows.length > 0) {
|
|
1173
1186
|
await db.storeMany(
|
|
@@ -1315,13 +1328,13 @@ async function captureUserMemoryFromInboundTexts(
|
|
|
1315
1328
|
}
|
|
1316
1329
|
}
|
|
1317
1330
|
const existingCandidates = [...candidateMap.values()].sort((a, b) => b.score - a.score);
|
|
1318
|
-
console.
|
|
1331
|
+
console.debug(`[openclaw-memory-alibaba-local] worldImageExtraction recall: ${embeddingResults.length} items × ${PER_ITEM_RECALL} = raw ${embeddingResults.length * PER_ITEM_RECALL}, deduped ${existingCandidates.length}, minScore=${recallMinScore}`);
|
|
1319
1332
|
if (existingCandidates.length > 0) {
|
|
1320
|
-
console.
|
|
1333
|
+
console.debug(`[openclaw-memory-alibaba-local] worldImageExtraction recall found ${existingCandidates.length} candidates: ${existingCandidates.map((c) => `[${c.score.toFixed(3)}] ${c.entry.text.slice(0, 60)}`).join(" | ")}`);
|
|
1321
1334
|
}
|
|
1322
1335
|
|
|
1323
1336
|
// 3. LLM CRUD decision
|
|
1324
|
-
console.
|
|
1337
|
+
console.debug(`[openclaw-memory-alibaba-local] worldImageExtraction input: ${eventItems.length} event items, ${existingCandidates.length} existing candidates`);
|
|
1325
1338
|
const worldActions = await extractWorldImageWithLLM(
|
|
1326
1339
|
cfg.llm,
|
|
1327
1340
|
embeddingResults.map((r) => r.item),
|
|
@@ -1337,6 +1350,7 @@ async function captureUserMemoryFromInboundTexts(
|
|
|
1337
1350
|
|
|
1338
1351
|
// 4. Execute actions
|
|
1339
1352
|
let insertCount = 0;
|
|
1353
|
+
let deleteCount = 0;
|
|
1340
1354
|
for (const action of worldActions) {
|
|
1341
1355
|
if (action.action === "skip") continue;
|
|
1342
1356
|
|
|
@@ -1344,6 +1358,7 @@ async function captureUserMemoryFromInboundTexts(
|
|
|
1344
1358
|
const hit = existingCandidates.find((c) => c.entry.id === action.memoryId);
|
|
1345
1359
|
if (hit) {
|
|
1346
1360
|
await deleteSimilarLogicalMemory(db, agentId, hit.entry.sessionId, hit);
|
|
1361
|
+
deleteCount++;
|
|
1347
1362
|
}
|
|
1348
1363
|
continue;
|
|
1349
1364
|
}
|
|
@@ -1352,6 +1367,7 @@ async function captureUserMemoryFromInboundTexts(
|
|
|
1352
1367
|
const hit = existingCandidates.find((c) => c.entry.id === action.memoryId);
|
|
1353
1368
|
if (hit) {
|
|
1354
1369
|
await deleteSimilarLogicalMemory(db, agentId, hit.entry.sessionId, hit);
|
|
1370
|
+
deleteCount++;
|
|
1355
1371
|
}
|
|
1356
1372
|
const { vectors } = await backend.encodeForStorage(action.text);
|
|
1357
1373
|
const rows = buildChunkRows(
|
|
@@ -1373,7 +1389,8 @@ async function captureUserMemoryFromInboundTexts(
|
|
|
1373
1389
|
insertCount++;
|
|
1374
1390
|
}
|
|
1375
1391
|
}
|
|
1376
|
-
|
|
1392
|
+
|
|
1393
|
+
console.log(`[openclaw-memory-alibaba-local] worldImageExtraction done: ${worldActions.length} actions, ${insertCount} stored, ${deleteCount} deleted`);
|
|
1377
1394
|
|
|
1378
1395
|
// 5. World fact LRU GC: every 10 insertions
|
|
1379
1396
|
if (insertCount > 0) {
|
|
@@ -1415,16 +1432,16 @@ async function captureUserMemoryFromInboundTexts(
|
|
|
1415
1432
|
// 2. Recall top-10 similar existing memories for ALL new extractions (agentId global, USER_MEMORY scope)
|
|
1416
1433
|
const allVectors = embeddingResults.flatMap((r) => r.vectors);
|
|
1417
1434
|
const recallMinScore = Math.max(0.5, cfg.similarityThresholdUserMemory - 0.35);
|
|
1418
|
-
console.
|
|
1435
|
+
console.debug(`[openclaw-memory-alibaba-local] userImageExtraction recall: ${allVectors.length} query vectors, minScore=${recallMinScore}`);
|
|
1419
1436
|
const existingCandidates = allVectors.length > 0
|
|
1420
1437
|
? await db.searchMerged(agentId, allVectors, 10, recallMinScore, [...USER_MEMORY_CATEGORIES])
|
|
1421
1438
|
: [];
|
|
1422
1439
|
if (existingCandidates.length > 0) {
|
|
1423
|
-
console.
|
|
1440
|
+
console.debug(`[openclaw-memory-alibaba-local] userImageExtraction recall found ${existingCandidates.length} candidates: ${existingCandidates.map((c) => `[${c.score.toFixed(3)}] ${c.entry.text.slice(0, 60)}`).join(" | ")}`);
|
|
1424
1441
|
}
|
|
1425
1442
|
|
|
1426
1443
|
// 3. Call user image extraction LLM
|
|
1427
|
-
console.
|
|
1444
|
+
console.debug(`[openclaw-memory-alibaba-local] userImageExtraction input: ${userItems.length} user items (${eventItems.length} event items bypassed), ${existingCandidates.length} existing candidates`);
|
|
1428
1445
|
const userImageActions = await extractUserImageWithLLM(
|
|
1429
1446
|
cfg.llm,
|
|
1430
1447
|
embeddingResults.map((r) => r.item),
|
|
@@ -2178,7 +2195,7 @@ const memoryPlugin = {
|
|
|
2178
2195
|
// Check ANY user message for known bootstrap patterns (not just last).
|
|
2179
2196
|
if (isUserTrigger) {
|
|
2180
2197
|
const isBootstrapSession = event.messages.some((m: any) => {
|
|
2181
|
-
if (!m || typeof m !== "object"
|
|
2198
|
+
if (!m || typeof m !== "object") return false;
|
|
2182
2199
|
const text = typeof (m as Record<string, unknown>).content === "string"
|
|
2183
2200
|
? ((m as Record<string, unknown>).content as string).toLowerCase()
|
|
2184
2201
|
: "";
|
package/package.json
CHANGED
package/prompts.ts
CHANGED
|
@@ -267,7 +267,7 @@ Only INSERT or UPDATE information that captures a concrete, verifiable fact or e
|
|
|
267
267
|
- Purely conversational filler with no informational content
|
|
268
268
|
|
|
269
269
|
# Reconciliation Principles
|
|
270
|
-
1. **Prefer the richer version**: When a batch item and a Store item describe the same topic, keep whichever has the most information. If the batch
|
|
270
|
+
1. **Prefer the richer version**: When a batch item and a Store item describe the same topic, keep whichever has the most information. If the batch is richer, DELETE the old Store item and INSERT the batch item. If they are roughly equal, UPDATE to merge details.
|
|
271
271
|
2. **Preserve temporal markers**: Keep [as of ...] or [date] prefixes — world facts are time-sensitive.
|
|
272
272
|
3. **High cohesion**: Only merge entries about the exact same event or fact. Different events stay separate even if related.
|
|
273
273
|
4. **Contradiction = replace**: If a batch item directly contradicts a Store item (e.g. different outcome), DELETE the old item and INSERT the new one.
|
|
@@ -275,8 +275,8 @@ Only INSERT or UPDATE information that captures a concrete, verifiable fact or e
|
|
|
275
275
|
# Actions (one per batch index)
|
|
276
276
|
- **INSERT**: New world fact not in Store.
|
|
277
277
|
- **UPDATE** (memoryId): Store item covers the same event/fact; merge to produce the richer version.
|
|
278
|
-
- **SKIP**: Already fully covered by Store,
|
|
279
|
-
- **DELETE** (memoryId)
|
|
278
|
+
- **SKIP**: Already fully covered by a Store item (old is sufficient). Do NOT combine SKIP with DELETE for the same index — if you skip the new item, you are keeping the old one.
|
|
279
|
+
- **DELETE** (memoryId) + **INSERT**: Batch item supersedes or contradicts a Store item. You MUST delete the old item AND insert the replacement. A single index may list one or more DELETE actions followed by one INSERT.
|
|
280
280
|
|
|
281
281
|
# Output
|
|
282
282
|
Reply with ONLY a JSON object:
|
|
@@ -284,9 +284,11 @@ Reply with ONLY a JSON object:
|
|
|
284
284
|
{"index":0,"action":"insert","text":"[2026-04-01] Project X launched v2.0","importance":0.5},
|
|
285
285
|
{"index":1,"action":"skip"},
|
|
286
286
|
{"index":2,"action":"update","memoryId":"uuid","text":"[2026-03-28] Company Y acquired Z for $2B, deal finalized","importance":0.5},
|
|
287
|
-
{"index":3,"action":"delete","memoryId":"
|
|
287
|
+
{"index":3,"action":"delete","memoryId":"uuid1"},
|
|
288
|
+
{"index":3,"action":"delete","memoryId":"uuid2"},
|
|
289
|
+
{"index":3,"action":"insert","text":"[2026-04-02] Replacement fact","importance":0.5}
|
|
288
290
|
]}
|
|
289
|
-
Every batch index must appear.
|
|
291
|
+
Every batch index must appear exactly once as the primary action. When replacing, list DELETE(s) first then INSERT for that index.
|
|
290
292
|
`;
|
|
291
293
|
|
|
292
294
|
export function buildWorldImageExtractionPrompt(
|