@joshuaswarren/openclaw-engram 8.3.28 → 8.3.30
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/index.js +761 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10733,6 +10733,81 @@ var GraphIndex = class {
|
|
|
10733
10733
|
}
|
|
10734
10734
|
};
|
|
10735
10735
|
|
|
10736
|
+
// src/replay/types.ts
|
|
10737
|
+
var VALID_SOURCES = /* @__PURE__ */ new Set(["openclaw", "claude", "chatgpt"]);
|
|
10738
|
+
var VALID_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
10739
|
+
var ISO_UTC_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
10740
|
+
var REPLAY_UNKNOWN_SESSION_KEY = "replay:unknown";
|
|
10741
|
+
function normalizeIsoForComparison(value) {
|
|
10742
|
+
return value.includes(".") ? value : value.replace("Z", ".000Z");
|
|
10743
|
+
}
|
|
10744
|
+
function isReplaySource(value) {
|
|
10745
|
+
return typeof value === "string" && VALID_SOURCES.has(value);
|
|
10746
|
+
}
|
|
10747
|
+
function isReplayRole(value) {
|
|
10748
|
+
return typeof value === "string" && VALID_ROLES.has(value);
|
|
10749
|
+
}
|
|
10750
|
+
function normalizeReplaySessionKey(value) {
|
|
10751
|
+
if (typeof value !== "string") return REPLAY_UNKNOWN_SESSION_KEY;
|
|
10752
|
+
const trimmed = value.trim();
|
|
10753
|
+
return trimmed.length > 0 ? trimmed : REPLAY_UNKNOWN_SESSION_KEY;
|
|
10754
|
+
}
|
|
10755
|
+
function parseIsoTimestamp(value) {
|
|
10756
|
+
if (typeof value !== "string" || !ISO_UTC_TIMESTAMP_RE.test(value)) return null;
|
|
10757
|
+
const ts = Date.parse(value);
|
|
10758
|
+
if (!Number.isFinite(ts)) return null;
|
|
10759
|
+
const roundTrip = new Date(ts).toISOString();
|
|
10760
|
+
if (roundTrip !== normalizeIsoForComparison(value)) return null;
|
|
10761
|
+
return ts;
|
|
10762
|
+
}
|
|
10763
|
+
function validateReplayTurn(turn, index) {
|
|
10764
|
+
const issues = [];
|
|
10765
|
+
if (!turn || typeof turn !== "object") {
|
|
10766
|
+
issues.push({
|
|
10767
|
+
code: "turn.invalid",
|
|
10768
|
+
message: "Replay turn must be an object.",
|
|
10769
|
+
index
|
|
10770
|
+
});
|
|
10771
|
+
return issues;
|
|
10772
|
+
}
|
|
10773
|
+
if (!isReplayRole(turn.role)) {
|
|
10774
|
+
issues.push({
|
|
10775
|
+
code: "turn.role.invalid",
|
|
10776
|
+
message: `Replay role must be 'user' or 'assistant', received '${String(turn.role)}'.`,
|
|
10777
|
+
index
|
|
10778
|
+
});
|
|
10779
|
+
}
|
|
10780
|
+
if (!isReplaySource(turn.source)) {
|
|
10781
|
+
issues.push({
|
|
10782
|
+
code: "turn.source.invalid",
|
|
10783
|
+
message: `Replay source must be 'openclaw', 'claude', or 'chatgpt', received '${String(turn.source)}'.`,
|
|
10784
|
+
index
|
|
10785
|
+
});
|
|
10786
|
+
}
|
|
10787
|
+
if (!turn.sessionKey || typeof turn.sessionKey !== "string" || turn.sessionKey.trim().length === 0) {
|
|
10788
|
+
issues.push({
|
|
10789
|
+
code: "turn.sessionKey.invalid",
|
|
10790
|
+
message: "Replay sessionKey is required.",
|
|
10791
|
+
index
|
|
10792
|
+
});
|
|
10793
|
+
}
|
|
10794
|
+
if (!turn.content || typeof turn.content !== "string" || turn.content.trim().length === 0) {
|
|
10795
|
+
issues.push({
|
|
10796
|
+
code: "turn.content.invalid",
|
|
10797
|
+
message: "Replay content must be a non-empty string.",
|
|
10798
|
+
index
|
|
10799
|
+
});
|
|
10800
|
+
}
|
|
10801
|
+
if (!turn.timestamp || typeof turn.timestamp !== "string" || parseIsoTimestamp(turn.timestamp) === null) {
|
|
10802
|
+
issues.push({
|
|
10803
|
+
code: "turn.timestamp.invalid",
|
|
10804
|
+
message: `Replay timestamp must be a valid ISO timestamp, received '${String(turn.timestamp)}'.`,
|
|
10805
|
+
index
|
|
10806
|
+
});
|
|
10807
|
+
}
|
|
10808
|
+
return issues;
|
|
10809
|
+
}
|
|
10810
|
+
|
|
10736
10811
|
// src/conversation-index/chunker.ts
|
|
10737
10812
|
function chunkTranscriptEntries(sessionKey, entries, opts) {
|
|
10738
10813
|
const maxChars = Math.max(500, opts.maxChars);
|
|
@@ -12056,10 +12131,22 @@ var Orchestrator = class _Orchestrator {
|
|
|
12056
12131
|
while (this.queueProcessing || this.extractionQueue.length > 0) {
|
|
12057
12132
|
if (Date.now() - started > timeoutMs) {
|
|
12058
12133
|
log.warn(`waitForExtractionIdle timed out after ${timeoutMs}ms`);
|
|
12059
|
-
return;
|
|
12134
|
+
return false;
|
|
12135
|
+
}
|
|
12136
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
12137
|
+
}
|
|
12138
|
+
return true;
|
|
12139
|
+
}
|
|
12140
|
+
async waitForConsolidationIdle(timeoutMs = 6e4) {
|
|
12141
|
+
const started = Date.now();
|
|
12142
|
+
while (this.consolidationInFlight) {
|
|
12143
|
+
if (Date.now() - started > timeoutMs) {
|
|
12144
|
+
log.warn(`waitForConsolidationIdle timed out after ${timeoutMs}ms`);
|
|
12145
|
+
return false;
|
|
12060
12146
|
}
|
|
12061
12147
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
12062
12148
|
}
|
|
12149
|
+
return true;
|
|
12063
12150
|
}
|
|
12064
12151
|
async getStorage(namespace) {
|
|
12065
12152
|
const ns = namespace && namespace.length > 0 ? namespace : this.config.defaultNamespace;
|
|
@@ -13120,6 +13207,46 @@ _Context: ${topQuestion.context}_`);
|
|
|
13120
13207
|
if (decision === "keep_buffering") return;
|
|
13121
13208
|
await this.queueBufferedExtraction(this.buffer.getTurns(), "trigger_mode");
|
|
13122
13209
|
}
|
|
13210
|
+
async ingestReplayBatch(turns, options = {}) {
|
|
13211
|
+
if (!Array.isArray(turns) || turns.length === 0) return;
|
|
13212
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
13213
|
+
for (const turn of turns) {
|
|
13214
|
+
if (turn.role !== "user" && turn.role !== "assistant") continue;
|
|
13215
|
+
const key = normalizeReplaySessionKey(turn.sessionKey);
|
|
13216
|
+
const list = bySession.get(key) ?? [];
|
|
13217
|
+
list.push({
|
|
13218
|
+
role: turn.role,
|
|
13219
|
+
content: turn.content,
|
|
13220
|
+
timestamp: turn.timestamp,
|
|
13221
|
+
sessionKey: key
|
|
13222
|
+
});
|
|
13223
|
+
bySession.set(key, list);
|
|
13224
|
+
}
|
|
13225
|
+
const replayTasks = [];
|
|
13226
|
+
for (const sessionTurns of bySession.values()) {
|
|
13227
|
+
if (sessionTurns.length === 0) continue;
|
|
13228
|
+
replayTasks.push(
|
|
13229
|
+
new Promise((resolve, reject) => {
|
|
13230
|
+
void this.queueBufferedExtraction(sessionTurns, "trigger_mode", {
|
|
13231
|
+
skipDedupeCheck: true,
|
|
13232
|
+
clearBufferAfterExtraction: false,
|
|
13233
|
+
skipCharThreshold: true,
|
|
13234
|
+
extractionDeadlineMs: options.deadlineMs,
|
|
13235
|
+
onTaskSettled: (err) => err ? reject(err) : resolve()
|
|
13236
|
+
}).catch(reject);
|
|
13237
|
+
})
|
|
13238
|
+
);
|
|
13239
|
+
}
|
|
13240
|
+
if (replayTasks.length > 0) {
|
|
13241
|
+
const settled = await Promise.allSettled(replayTasks);
|
|
13242
|
+
const firstRejected = settled.find(
|
|
13243
|
+
(result) => result.status === "rejected"
|
|
13244
|
+
);
|
|
13245
|
+
if (firstRejected) {
|
|
13246
|
+
throw firstRejected.reason;
|
|
13247
|
+
}
|
|
13248
|
+
}
|
|
13249
|
+
}
|
|
13123
13250
|
async observeSessionHeartbeat(sessionKey) {
|
|
13124
13251
|
if (this.config.sessionObserverEnabled !== true) return;
|
|
13125
13252
|
if (!sessionKey || sessionKey.length === 0) return;
|
|
@@ -13160,10 +13287,21 @@ _Context: ${topQuestion.context}_`);
|
|
|
13160
13287
|
async queueBufferedExtraction(turnsToExtract, reason, options = {}) {
|
|
13161
13288
|
if (!options.skipDedupeCheck && !this.shouldQueueExtraction(turnsToExtract)) {
|
|
13162
13289
|
log.debug(`extraction dedupe skip: preserving buffer (${reason})`);
|
|
13290
|
+
options.onTaskSettled?.();
|
|
13163
13291
|
return;
|
|
13164
13292
|
}
|
|
13165
13293
|
this.extractionQueue.push(async () => {
|
|
13166
|
-
|
|
13294
|
+
try {
|
|
13295
|
+
await this.runExtraction(turnsToExtract, {
|
|
13296
|
+
clearBufferAfterExtraction: options.clearBufferAfterExtraction ?? true,
|
|
13297
|
+
skipCharThreshold: options.skipCharThreshold ?? false,
|
|
13298
|
+
deadlineMs: options.extractionDeadlineMs
|
|
13299
|
+
});
|
|
13300
|
+
options.onTaskSettled?.();
|
|
13301
|
+
} catch (err) {
|
|
13302
|
+
options.onTaskSettled?.(err);
|
|
13303
|
+
throw err;
|
|
13304
|
+
}
|
|
13167
13305
|
});
|
|
13168
13306
|
if (!this.queueProcessing) {
|
|
13169
13307
|
this.queueProcessing = true;
|
|
@@ -13217,25 +13355,41 @@ _Context: ${topQuestion.context}_`);
|
|
|
13217
13355
|
}
|
|
13218
13356
|
this.queueProcessing = false;
|
|
13219
13357
|
}
|
|
13220
|
-
async runExtraction(turns) {
|
|
13358
|
+
async runExtraction(turns, options = {}) {
|
|
13221
13359
|
log.debug(`running extraction on ${turns.length} turns`);
|
|
13360
|
+
const clearBufferAfterExtraction = options.clearBufferAfterExtraction ?? true;
|
|
13361
|
+
const skipCharThreshold = options.skipCharThreshold ?? false;
|
|
13362
|
+
const deadlineMs = typeof options.deadlineMs === "number" && Number.isFinite(options.deadlineMs) ? options.deadlineMs : void 0;
|
|
13363
|
+
const throwIfDeadlineExceeded = (stage) => {
|
|
13364
|
+
if (typeof deadlineMs === "number" && Date.now() > deadlineMs) {
|
|
13365
|
+
throw new Error(`replay extraction deadline exceeded (${stage})`);
|
|
13366
|
+
}
|
|
13367
|
+
};
|
|
13368
|
+
const clearBuffer = async () => {
|
|
13369
|
+
if (clearBufferAfterExtraction) {
|
|
13370
|
+
await this.buffer.clearAfterExtraction();
|
|
13371
|
+
}
|
|
13372
|
+
};
|
|
13222
13373
|
const sessionKey = turns[0]?.sessionKey ?? "";
|
|
13223
13374
|
if (sessionKey.includes(":cron:")) {
|
|
13224
13375
|
log.debug(`skipping extraction for cron session: ${sessionKey}`);
|
|
13225
|
-
await
|
|
13376
|
+
await clearBuffer();
|
|
13226
13377
|
return;
|
|
13227
13378
|
}
|
|
13228
13379
|
const normalizedTurns = turns.filter((t) => (t.role === "user" || t.role === "assistant") && typeof t.content === "string").map((t) => ({
|
|
13229
13380
|
...t,
|
|
13230
13381
|
content: t.content.trim().slice(0, this.config.extractionMaxTurnChars)
|
|
13231
13382
|
})).filter((t) => t.content.length > 0);
|
|
13383
|
+
throwIfDeadlineExceeded("before_extract");
|
|
13232
13384
|
const userTurns = normalizedTurns.filter((t) => t.role === "user");
|
|
13233
13385
|
const totalChars = normalizedTurns.reduce((sum, t) => sum + t.content.length, 0);
|
|
13234
|
-
|
|
13386
|
+
const belowCharThreshold = totalChars < this.config.extractionMinChars;
|
|
13387
|
+
const belowUserTurnThreshold = userTurns.length < this.config.extractionMinUserTurns;
|
|
13388
|
+
if (!skipCharThreshold && belowCharThreshold || belowUserTurnThreshold) {
|
|
13235
13389
|
log.debug(
|
|
13236
13390
|
`skipping extraction: below threshold (totalChars=${totalChars}, userTurns=${userTurns.length})`
|
|
13237
13391
|
);
|
|
13238
|
-
await
|
|
13392
|
+
await clearBuffer();
|
|
13239
13393
|
return;
|
|
13240
13394
|
}
|
|
13241
13395
|
const principal = resolvePrincipal(sessionKey, this.config);
|
|
@@ -13243,19 +13397,20 @@ _Context: ${topQuestion.context}_`);
|
|
|
13243
13397
|
const storage = await this.storageRouter.storageFor(selfNamespace);
|
|
13244
13398
|
const existingEntities = await storage.listEntityNames();
|
|
13245
13399
|
const result = await this.extraction.extract(normalizedTurns, existingEntities);
|
|
13400
|
+
throwIfDeadlineExceeded("before_persist");
|
|
13246
13401
|
if (!result) {
|
|
13247
13402
|
log.warn("runExtraction: extraction returned null/undefined");
|
|
13248
|
-
await
|
|
13403
|
+
await clearBuffer();
|
|
13249
13404
|
return;
|
|
13250
13405
|
}
|
|
13251
13406
|
if (!Array.isArray(result.facts)) {
|
|
13252
13407
|
log.warn("runExtraction: extraction returned invalid facts (not an array)", { factsType: typeof result.facts, resultKeys: Object.keys(result) });
|
|
13253
|
-
await
|
|
13408
|
+
await clearBuffer();
|
|
13254
13409
|
return;
|
|
13255
13410
|
}
|
|
13256
13411
|
if (result.facts.length === 0 && result.entities.length === 0 && result.questions.length === 0 && result.profileUpdates.length === 0) {
|
|
13257
13412
|
log.debug("runExtraction: extraction produced no durable outputs; skipping persistence");
|
|
13258
|
-
await
|
|
13413
|
+
await clearBuffer();
|
|
13259
13414
|
return;
|
|
13260
13415
|
}
|
|
13261
13416
|
let threadIdForExtraction = null;
|
|
@@ -13268,7 +13423,7 @@ _Context: ${topQuestion.context}_`);
|
|
|
13268
13423
|
}
|
|
13269
13424
|
}
|
|
13270
13425
|
const persistedIds = await this.persistExtraction(result, storage, threadIdForExtraction);
|
|
13271
|
-
await
|
|
13426
|
+
await clearBuffer();
|
|
13272
13427
|
if (this.config.memoryBoxesEnabled && persistedIds.length > 0) {
|
|
13273
13428
|
const extractionTopics = deriveTopicsFromExtraction(result);
|
|
13274
13429
|
await this.boxBuilderFor(storage).onExtraction({
|
|
@@ -16666,6 +16821,471 @@ async function detectImportFormat(fromPath) {
|
|
|
16666
16821
|
return null;
|
|
16667
16822
|
}
|
|
16668
16823
|
|
|
16824
|
+
// src/replay/runner.ts
|
|
16825
|
+
function clampNonNegativeInt(value, defaultValue) {
|
|
16826
|
+
if (!Number.isFinite(value)) return defaultValue;
|
|
16827
|
+
return Math.max(0, Math.floor(value));
|
|
16828
|
+
}
|
|
16829
|
+
function clampBatchSize(value) {
|
|
16830
|
+
const parsed = clampNonNegativeInt(value, 100);
|
|
16831
|
+
if (parsed < 1) return 1;
|
|
16832
|
+
return Math.min(parsed, 1e3);
|
|
16833
|
+
}
|
|
16834
|
+
function toWarning(issue) {
|
|
16835
|
+
return {
|
|
16836
|
+
code: issue.code,
|
|
16837
|
+
message: issue.message,
|
|
16838
|
+
index: issue.index
|
|
16839
|
+
};
|
|
16840
|
+
}
|
|
16841
|
+
function inDateRange(turn, fromTs, toTs) {
|
|
16842
|
+
const turnTs = parseIsoTimestamp(turn.timestamp);
|
|
16843
|
+
if (turnTs === null) return false;
|
|
16844
|
+
if (fromTs !== null && turnTs < fromTs) return false;
|
|
16845
|
+
if (toTs !== null && turnTs > toTs) return false;
|
|
16846
|
+
return true;
|
|
16847
|
+
}
|
|
16848
|
+
function buildReplayNormalizerRegistry(normalizers) {
|
|
16849
|
+
const registry = {};
|
|
16850
|
+
for (const normalizer of normalizers) {
|
|
16851
|
+
if (!normalizer?.source) {
|
|
16852
|
+
throw new Error("replay normalizer source is required");
|
|
16853
|
+
}
|
|
16854
|
+
if (registry[normalizer.source]) {
|
|
16855
|
+
throw new Error(`duplicate replay normalizer for source '${normalizer.source}'`);
|
|
16856
|
+
}
|
|
16857
|
+
registry[normalizer.source] = normalizer;
|
|
16858
|
+
}
|
|
16859
|
+
return registry;
|
|
16860
|
+
}
|
|
16861
|
+
async function runReplay(source, input, registry, handlers = {}, options = {}) {
|
|
16862
|
+
const normalizer = registry[source];
|
|
16863
|
+
if (!normalizer) {
|
|
16864
|
+
throw new Error(`missing replay normalizer for source '${source}'`);
|
|
16865
|
+
}
|
|
16866
|
+
return runReplayWithNormalizer(normalizer, input, handlers, options);
|
|
16867
|
+
}
|
|
16868
|
+
async function runReplayWithNormalizer(normalizer, input, handlers = {}, options = {}) {
|
|
16869
|
+
const parseResult = await normalizer.parse(input, options);
|
|
16870
|
+
if (!parseResult || typeof parseResult !== "object") {
|
|
16871
|
+
throw new Error(`replay normalizer '${normalizer.source}' returned invalid parse result object`);
|
|
16872
|
+
}
|
|
16873
|
+
if (!Array.isArray(parseResult.turns)) {
|
|
16874
|
+
throw new Error(`replay normalizer '${normalizer.source}' returned invalid parse result: turns must be an array`);
|
|
16875
|
+
}
|
|
16876
|
+
if (parseResult.warnings != null && !Array.isArray(parseResult.warnings)) {
|
|
16877
|
+
throw new Error(`replay normalizer '${normalizer.source}' returned invalid parse result: warnings must be an array`);
|
|
16878
|
+
}
|
|
16879
|
+
const warnings = [...parseResult.warnings ?? []];
|
|
16880
|
+
const parsedTurns = parseResult.turns;
|
|
16881
|
+
const validTurns = [];
|
|
16882
|
+
let invalidTurns = 0;
|
|
16883
|
+
for (let i = 0; i < parsedTurns.length; i += 1) {
|
|
16884
|
+
const turn = parsedTurns[i];
|
|
16885
|
+
const issues = validateReplayTurn(turn, i);
|
|
16886
|
+
if (issues.length === 0 && turn.source !== normalizer.source) {
|
|
16887
|
+
issues.push({
|
|
16888
|
+
code: "turn.source.mismatch",
|
|
16889
|
+
message: `Replay turn source '${turn.source}' does not match normalizer source '${normalizer.source}'.`,
|
|
16890
|
+
index: i
|
|
16891
|
+
});
|
|
16892
|
+
}
|
|
16893
|
+
if (issues.length > 0) {
|
|
16894
|
+
invalidTurns += 1;
|
|
16895
|
+
for (const issue of issues) warnings.push(toWarning(issue));
|
|
16896
|
+
continue;
|
|
16897
|
+
}
|
|
16898
|
+
validTurns.push(turn);
|
|
16899
|
+
}
|
|
16900
|
+
const sorted = [...validTurns].sort((a, b) => {
|
|
16901
|
+
const left = parseIsoTimestamp(a.timestamp) ?? 0;
|
|
16902
|
+
const right = parseIsoTimestamp(b.timestamp) ?? 0;
|
|
16903
|
+
return left - right;
|
|
16904
|
+
});
|
|
16905
|
+
const fromTs = options.from ? parseIsoTimestamp(options.from) : null;
|
|
16906
|
+
const toTs = options.to ? parseIsoTimestamp(options.to) : null;
|
|
16907
|
+
if (options.from && fromTs === null) {
|
|
16908
|
+
throw new Error(`invalid replay --from timestamp '${options.from}'`);
|
|
16909
|
+
}
|
|
16910
|
+
if (options.to && toTs === null) {
|
|
16911
|
+
throw new Error(`invalid replay --to timestamp '${options.to}'`);
|
|
16912
|
+
}
|
|
16913
|
+
if (fromTs !== null && toTs !== null && fromTs > toTs) {
|
|
16914
|
+
throw new Error("invalid replay date range: --from is after --to");
|
|
16915
|
+
}
|
|
16916
|
+
const ranged = sorted.filter((turn) => inDateRange(turn, fromTs, toTs));
|
|
16917
|
+
const filteredByDate = sorted.length - ranged.length;
|
|
16918
|
+
const startOffset = clampNonNegativeInt(options.startOffset, 0);
|
|
16919
|
+
const skippedByOffset = Math.min(startOffset, ranged.length);
|
|
16920
|
+
const offsetApplied = ranged.slice(startOffset);
|
|
16921
|
+
const maxTurns = clampNonNegativeInt(options.maxTurns, offsetApplied.length);
|
|
16922
|
+
const selected = offsetApplied.slice(0, maxTurns);
|
|
16923
|
+
const dryRun = options.dryRun === true;
|
|
16924
|
+
const batchSize = clampBatchSize(options.batchSize);
|
|
16925
|
+
let batchCount = 0;
|
|
16926
|
+
if (!dryRun) {
|
|
16927
|
+
for (let i = 0; i < selected.length; i += batchSize) {
|
|
16928
|
+
const batch = selected.slice(i, i + batchSize);
|
|
16929
|
+
batchCount += 1;
|
|
16930
|
+
if (handlers.onBatch) await handlers.onBatch(batch);
|
|
16931
|
+
if (handlers.onTurn) {
|
|
16932
|
+
for (const turn of batch) {
|
|
16933
|
+
await handlers.onTurn(turn);
|
|
16934
|
+
}
|
|
16935
|
+
}
|
|
16936
|
+
}
|
|
16937
|
+
} else if (selected.length > 0) {
|
|
16938
|
+
batchCount = Math.ceil(selected.length / batchSize);
|
|
16939
|
+
}
|
|
16940
|
+
return {
|
|
16941
|
+
source: normalizer.source,
|
|
16942
|
+
parsedTurns: parsedTurns.length,
|
|
16943
|
+
validTurns: sorted.length,
|
|
16944
|
+
invalidTurns,
|
|
16945
|
+
filteredByDate,
|
|
16946
|
+
skippedByOffset,
|
|
16947
|
+
processedTurns: selected.length,
|
|
16948
|
+
batchCount,
|
|
16949
|
+
dryRun,
|
|
16950
|
+
nextOffset: startOffset + selected.length,
|
|
16951
|
+
firstTimestamp: selected[0]?.timestamp,
|
|
16952
|
+
lastTimestamp: selected[selected.length - 1]?.timestamp,
|
|
16953
|
+
warnings
|
|
16954
|
+
};
|
|
16955
|
+
}
|
|
16956
|
+
|
|
16957
|
+
// src/replay/normalizers/shared.ts
|
|
16958
|
+
function normalizeReplayRole(value, options = {}) {
|
|
16959
|
+
if (typeof value !== "string") return null;
|
|
16960
|
+
const role = value.trim().toLowerCase();
|
|
16961
|
+
if (isReplayRole(role)) return role;
|
|
16962
|
+
if (options.userAliases?.includes(role) || role === "human") return "user";
|
|
16963
|
+
if (options.assistantAliases?.includes(role) || role === "ai" || role === "model") return "assistant";
|
|
16964
|
+
return null;
|
|
16965
|
+
}
|
|
16966
|
+
function normalizeReplayContent(value) {
|
|
16967
|
+
if (typeof value === "string") {
|
|
16968
|
+
const content = value.trim();
|
|
16969
|
+
return content.length > 0 ? content : null;
|
|
16970
|
+
}
|
|
16971
|
+
if (Array.isArray(value)) {
|
|
16972
|
+
const text = value.map((part) => typeof part === "string" ? part : "").join("\n").trim();
|
|
16973
|
+
return text.length > 0 ? text : null;
|
|
16974
|
+
}
|
|
16975
|
+
if (value && typeof value === "object") {
|
|
16976
|
+
const obj = value;
|
|
16977
|
+
if (Array.isArray(obj.parts)) return normalizeReplayContent(obj.parts);
|
|
16978
|
+
if (typeof obj.text === "string") return normalizeReplayContent(obj.text);
|
|
16979
|
+
}
|
|
16980
|
+
return null;
|
|
16981
|
+
}
|
|
16982
|
+
function normalizeReplayTimestamp(value, options = {}) {
|
|
16983
|
+
const toIso = (millis) => {
|
|
16984
|
+
if (!Number.isFinite(millis)) return null;
|
|
16985
|
+
const date = new Date(millis);
|
|
16986
|
+
if (!Number.isFinite(date.getTime())) return null;
|
|
16987
|
+
try {
|
|
16988
|
+
return date.toISOString();
|
|
16989
|
+
} catch {
|
|
16990
|
+
return null;
|
|
16991
|
+
}
|
|
16992
|
+
};
|
|
16993
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
16994
|
+
const millis = value > 1e12 ? value : value * 1e3;
|
|
16995
|
+
return toIso(millis);
|
|
16996
|
+
}
|
|
16997
|
+
if (options.acceptDateObject && value instanceof Date && Number.isFinite(value.getTime())) {
|
|
16998
|
+
return toIso(value.getTime());
|
|
16999
|
+
}
|
|
17000
|
+
if (typeof value !== "string") return null;
|
|
17001
|
+
const raw = options.trimString === false ? value : value.trim();
|
|
17002
|
+
if (raw.length === 0) return null;
|
|
17003
|
+
return toIso(Date.parse(raw));
|
|
17004
|
+
}
|
|
17005
|
+
|
|
17006
|
+
// src/replay/normalizers/chatgpt.ts
|
|
17007
|
+
function gatherConversations(input) {
|
|
17008
|
+
if (Array.isArray(input)) {
|
|
17009
|
+
return input.filter((item) => !!item && typeof item === "object");
|
|
17010
|
+
}
|
|
17011
|
+
if (!input || typeof input !== "object") return [];
|
|
17012
|
+
const obj = input;
|
|
17013
|
+
if (Array.isArray(obj.conversations)) {
|
|
17014
|
+
return obj.conversations.filter((item) => !!item && typeof item === "object");
|
|
17015
|
+
}
|
|
17016
|
+
return [obj];
|
|
17017
|
+
}
|
|
17018
|
+
function extractFromMapping(conversation) {
|
|
17019
|
+
const mapping = conversation.mapping;
|
|
17020
|
+
if (!mapping || typeof mapping !== "object") return [];
|
|
17021
|
+
const nodes = mapping;
|
|
17022
|
+
const currentNodeId = typeof conversation.current_node === "string" ? conversation.current_node : null;
|
|
17023
|
+
if (!currentNodeId || !nodes[currentNodeId] || typeof nodes[currentNodeId] !== "object") {
|
|
17024
|
+
const loose = [];
|
|
17025
|
+
for (const node of Object.values(nodes)) {
|
|
17026
|
+
if (!node || typeof node !== "object") continue;
|
|
17027
|
+
const nodeObj = node;
|
|
17028
|
+
const message = nodeObj.message;
|
|
17029
|
+
if (!message || typeof message !== "object") continue;
|
|
17030
|
+
loose.push({
|
|
17031
|
+
...message,
|
|
17032
|
+
_nodeId: typeof nodeObj.id === "string" ? nodeObj.id : void 0,
|
|
17033
|
+
_nodeCreateTime: nodeObj.create_time
|
|
17034
|
+
});
|
|
17035
|
+
}
|
|
17036
|
+
return loose;
|
|
17037
|
+
}
|
|
17038
|
+
const chain = [];
|
|
17039
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17040
|
+
let cursor = currentNodeId;
|
|
17041
|
+
while (cursor && !seen.has(cursor)) {
|
|
17042
|
+
seen.add(cursor);
|
|
17043
|
+
const node = nodes[cursor];
|
|
17044
|
+
if (!node || typeof node !== "object") break;
|
|
17045
|
+
const nodeObj = node;
|
|
17046
|
+
const message = nodeObj.message;
|
|
17047
|
+
if (message && typeof message === "object") {
|
|
17048
|
+
chain.push({
|
|
17049
|
+
...message,
|
|
17050
|
+
_nodeId: typeof nodeObj.id === "string" ? nodeObj.id : cursor,
|
|
17051
|
+
_nodeCreateTime: nodeObj.create_time
|
|
17052
|
+
});
|
|
17053
|
+
}
|
|
17054
|
+
cursor = typeof nodeObj.parent === "string" ? nodeObj.parent : null;
|
|
17055
|
+
}
|
|
17056
|
+
return chain.reverse();
|
|
17057
|
+
}
|
|
17058
|
+
var chatgptReplayNormalizer = {
|
|
17059
|
+
source: "chatgpt",
|
|
17060
|
+
parse(input, options = {}) {
|
|
17061
|
+
let parsedInput = input;
|
|
17062
|
+
if (typeof input === "string") {
|
|
17063
|
+
try {
|
|
17064
|
+
parsedInput = JSON.parse(input);
|
|
17065
|
+
} catch {
|
|
17066
|
+
parsedInput = [];
|
|
17067
|
+
}
|
|
17068
|
+
}
|
|
17069
|
+
const warnings = [];
|
|
17070
|
+
const turns = [];
|
|
17071
|
+
const conversations = gatherConversations(parsedInput);
|
|
17072
|
+
for (let i = 0; i < conversations.length; i += 1) {
|
|
17073
|
+
const conversation = conversations[i];
|
|
17074
|
+
const convIdRaw = conversation.id ?? conversation.conversation_id ?? conversation.uuid;
|
|
17075
|
+
const hasSourceConversationId = typeof convIdRaw === "string" && convIdRaw.trim().length > 0;
|
|
17076
|
+
const convId = hasSourceConversationId ? convIdRaw.trim() : `conv-${i + 1}`;
|
|
17077
|
+
const sessionKey = `replay:chatgpt:${convId}`;
|
|
17078
|
+
const fallbackSessionKey = options.defaultSessionKey?.trim() || sessionKey;
|
|
17079
|
+
const messageRows = Array.isArray(conversation.messages) ? conversation.messages.filter((item) => !!item && typeof item === "object") : extractFromMapping(conversation);
|
|
17080
|
+
for (let j = 0; j < messageRows.length; j += 1) {
|
|
17081
|
+
const row = messageRows[j];
|
|
17082
|
+
const role = normalizeReplayRole(
|
|
17083
|
+
row.author?.role ?? row.role
|
|
17084
|
+
);
|
|
17085
|
+
const content = normalizeReplayContent(
|
|
17086
|
+
row.content?.parts ?? row.content ?? row.text
|
|
17087
|
+
);
|
|
17088
|
+
const timestamp = normalizeReplayTimestamp(
|
|
17089
|
+
row.create_time ?? row.timestamp ?? row._nodeCreateTime ?? row.created_at
|
|
17090
|
+
);
|
|
17091
|
+
if (!role || !content || !timestamp) {
|
|
17092
|
+
const message = `Skipping invalid ChatGPT replay message at conversation ${i + 1}, index ${j}.`;
|
|
17093
|
+
if (options.strict) throw new Error(message);
|
|
17094
|
+
warnings.push({ code: "replay.chatgpt.message.invalid", message, index: j });
|
|
17095
|
+
continue;
|
|
17096
|
+
}
|
|
17097
|
+
const externalIdRaw = row.id ?? row._nodeId;
|
|
17098
|
+
turns.push({
|
|
17099
|
+
source: "chatgpt",
|
|
17100
|
+
sessionKey: hasSourceConversationId ? sessionKey : fallbackSessionKey,
|
|
17101
|
+
role,
|
|
17102
|
+
content,
|
|
17103
|
+
timestamp,
|
|
17104
|
+
externalId: typeof externalIdRaw === "string" ? externalIdRaw : void 0,
|
|
17105
|
+
metadata: {
|
|
17106
|
+
conversationId: convId,
|
|
17107
|
+
conversationTitle: typeof conversation.title === "string" ? conversation.title : void 0
|
|
17108
|
+
}
|
|
17109
|
+
});
|
|
17110
|
+
}
|
|
17111
|
+
}
|
|
17112
|
+
return { turns, warnings };
|
|
17113
|
+
}
|
|
17114
|
+
};
|
|
17115
|
+
|
|
17116
|
+
// src/replay/normalizers/claude.ts
|
|
17117
|
+
function gatherConversations2(input) {
|
|
17118
|
+
if (Array.isArray(input)) return input.filter((item) => !!item && typeof item === "object");
|
|
17119
|
+
if (!input || typeof input !== "object") return [];
|
|
17120
|
+
const obj = input;
|
|
17121
|
+
if (Array.isArray(obj.conversations)) {
|
|
17122
|
+
return obj.conversations.filter((item) => !!item && typeof item === "object");
|
|
17123
|
+
}
|
|
17124
|
+
if (Array.isArray(obj.chat_messages) || Array.isArray(obj.messages)) {
|
|
17125
|
+
return [obj];
|
|
17126
|
+
}
|
|
17127
|
+
return [];
|
|
17128
|
+
}
|
|
17129
|
+
var claudeReplayNormalizer = {
|
|
17130
|
+
source: "claude",
|
|
17131
|
+
parse(input, options = {}) {
|
|
17132
|
+
let parsedInput = input;
|
|
17133
|
+
if (typeof input === "string") {
|
|
17134
|
+
try {
|
|
17135
|
+
parsedInput = JSON.parse(input);
|
|
17136
|
+
} catch {
|
|
17137
|
+
parsedInput = [];
|
|
17138
|
+
}
|
|
17139
|
+
}
|
|
17140
|
+
const warnings = [];
|
|
17141
|
+
const turns = [];
|
|
17142
|
+
const conversations = gatherConversations2(parsedInput);
|
|
17143
|
+
for (let i = 0; i < conversations.length; i += 1) {
|
|
17144
|
+
const conversation = conversations[i];
|
|
17145
|
+
const convoIdRaw = conversation.uuid ?? conversation.id ?? conversation.conversation_id;
|
|
17146
|
+
const hasSourceConversationId = typeof convoIdRaw === "string" && convoIdRaw.trim().length > 0;
|
|
17147
|
+
const convoId = hasSourceConversationId ? convoIdRaw.trim() : `conv-${i + 1}`;
|
|
17148
|
+
const sessionKey = `replay:claude:${convoId}`;
|
|
17149
|
+
const fallbackSessionKey = options.defaultSessionKey?.trim() || sessionKey;
|
|
17150
|
+
const messagesRaw = Array.isArray(conversation.chat_messages) ? conversation.chat_messages : Array.isArray(conversation.messages) ? conversation.messages : [];
|
|
17151
|
+
for (let j = 0; j < messagesRaw.length; j += 1) {
|
|
17152
|
+
const msg = messagesRaw[j];
|
|
17153
|
+
if (!msg || typeof msg !== "object") {
|
|
17154
|
+
warnings.push({
|
|
17155
|
+
code: "replay.claude.message.invalid",
|
|
17156
|
+
message: `Skipping malformed Claude message at conversation ${i + 1}, index ${j}.`,
|
|
17157
|
+
index: j
|
|
17158
|
+
});
|
|
17159
|
+
continue;
|
|
17160
|
+
}
|
|
17161
|
+
const row = msg;
|
|
17162
|
+
const role = normalizeReplayRole(
|
|
17163
|
+
row.sender ?? row.role ?? row.author?.role
|
|
17164
|
+
);
|
|
17165
|
+
const content = normalizeReplayContent(row.text ?? row.content ?? row.message);
|
|
17166
|
+
const timestamp = normalizeReplayTimestamp(
|
|
17167
|
+
row.created_at ?? row.createdAt ?? row.updated_at ?? row.updatedAt ?? row.timestamp
|
|
17168
|
+
);
|
|
17169
|
+
if (!role || !content || !timestamp) {
|
|
17170
|
+
const message = `Skipping invalid Claude replay message at conversation ${i + 1}, index ${j}.`;
|
|
17171
|
+
if (options.strict) throw new Error(message);
|
|
17172
|
+
warnings.push({ code: "replay.claude.message.invalid", message, index: j });
|
|
17173
|
+
continue;
|
|
17174
|
+
}
|
|
17175
|
+
const externalIdRaw = row.uuid ?? row.id;
|
|
17176
|
+
turns.push({
|
|
17177
|
+
source: "claude",
|
|
17178
|
+
sessionKey: hasSourceConversationId ? sessionKey : fallbackSessionKey,
|
|
17179
|
+
role,
|
|
17180
|
+
content,
|
|
17181
|
+
timestamp,
|
|
17182
|
+
externalId: typeof externalIdRaw === "string" ? externalIdRaw : void 0,
|
|
17183
|
+
metadata: {
|
|
17184
|
+
conversationId: convoId,
|
|
17185
|
+
conversationName: typeof conversation.name === "string" ? conversation.name : void 0
|
|
17186
|
+
}
|
|
17187
|
+
});
|
|
17188
|
+
}
|
|
17189
|
+
}
|
|
17190
|
+
return { turns, warnings };
|
|
17191
|
+
}
|
|
17192
|
+
};
|
|
17193
|
+
|
|
17194
|
+
// src/replay/normalizers/openclaw.ts
|
|
17195
|
+
function parseJsonl(raw, warnings) {
|
|
17196
|
+
const out = [];
|
|
17197
|
+
for (const [index, line] of raw.split("\n").entries()) {
|
|
17198
|
+
const trimmed = line.trim();
|
|
17199
|
+
if (!trimmed) continue;
|
|
17200
|
+
try {
|
|
17201
|
+
out.push(JSON.parse(trimmed));
|
|
17202
|
+
} catch {
|
|
17203
|
+
warnings.push({
|
|
17204
|
+
code: "replay.openclaw.jsonl.invalid_line",
|
|
17205
|
+
message: `Skipping invalid JSONL line ${index + 1}.`,
|
|
17206
|
+
index
|
|
17207
|
+
});
|
|
17208
|
+
}
|
|
17209
|
+
}
|
|
17210
|
+
return out;
|
|
17211
|
+
}
|
|
17212
|
+
function gatherCandidates(input, warnings) {
|
|
17213
|
+
if (Array.isArray(input)) return input;
|
|
17214
|
+
if (typeof input === "string") {
|
|
17215
|
+
try {
|
|
17216
|
+
return gatherCandidates(JSON.parse(input), warnings);
|
|
17217
|
+
} catch {
|
|
17218
|
+
return parseJsonl(input, warnings);
|
|
17219
|
+
}
|
|
17220
|
+
}
|
|
17221
|
+
if (!input || typeof input !== "object") return [];
|
|
17222
|
+
const obj = input;
|
|
17223
|
+
if (Array.isArray(obj.turns)) return obj.turns;
|
|
17224
|
+
if (Array.isArray(obj.entries)) return obj.entries;
|
|
17225
|
+
if (Array.isArray(obj.messages)) return obj.messages;
|
|
17226
|
+
if (Array.isArray(obj.records)) {
|
|
17227
|
+
const rows = [];
|
|
17228
|
+
for (const rec of obj.records) {
|
|
17229
|
+
if (!rec || typeof rec !== "object") continue;
|
|
17230
|
+
const record = rec;
|
|
17231
|
+
const content = typeof record.content === "string" ? record.content : null;
|
|
17232
|
+
if (!content) continue;
|
|
17233
|
+
const path36 = typeof record.path === "string" ? record.path : "";
|
|
17234
|
+
if (!path36.startsWith("transcripts/") && !path36.includes("/transcripts/")) continue;
|
|
17235
|
+
rows.push(...parseJsonl(content, warnings));
|
|
17236
|
+
}
|
|
17237
|
+
return rows;
|
|
17238
|
+
}
|
|
17239
|
+
return [];
|
|
17240
|
+
}
|
|
17241
|
+
var openclawReplayNormalizer = {
|
|
17242
|
+
source: "openclaw",
|
|
17243
|
+
parse(input, options = {}) {
|
|
17244
|
+
const warnings = [];
|
|
17245
|
+
const rawTurns = gatherCandidates(input, warnings);
|
|
17246
|
+
const turns = [];
|
|
17247
|
+
const defaultSessionKey = options.defaultSessionKey?.trim() || "replay:openclaw:import";
|
|
17248
|
+
for (let i = 0; i < rawTurns.length; i += 1) {
|
|
17249
|
+
const raw = rawTurns[i];
|
|
17250
|
+
if (!raw || typeof raw !== "object") {
|
|
17251
|
+
warnings.push({
|
|
17252
|
+
code: "replay.openclaw.turn.invalid",
|
|
17253
|
+
message: "Skipping non-object replay turn.",
|
|
17254
|
+
index: i
|
|
17255
|
+
});
|
|
17256
|
+
continue;
|
|
17257
|
+
}
|
|
17258
|
+
const row = raw;
|
|
17259
|
+
const role = normalizeReplayRole(row.role ?? row.sender ?? row.author?.role, {
|
|
17260
|
+
assistantAliases: ["bot"]
|
|
17261
|
+
});
|
|
17262
|
+
const content = normalizeReplayContent(row.content ?? row.text ?? row.message);
|
|
17263
|
+
const timestamp = normalizeReplayTimestamp(
|
|
17264
|
+
row.timestamp ?? row.createdAt ?? row.created_at ?? row.time ?? row.date,
|
|
17265
|
+
{ acceptDateObject: true }
|
|
17266
|
+
);
|
|
17267
|
+
if (!role || !content || !timestamp) {
|
|
17268
|
+
const message = `Skipping invalid openclaw turn at index ${i}.`;
|
|
17269
|
+
if (options.strict) throw new Error(message);
|
|
17270
|
+
warnings.push({ code: "replay.openclaw.turn.invalid", message, index: i });
|
|
17271
|
+
continue;
|
|
17272
|
+
}
|
|
17273
|
+
const sessionKeyRaw = row.sessionKey ?? row.session_key;
|
|
17274
|
+
const sessionKey = typeof sessionKeyRaw === "string" && sessionKeyRaw.trim().length > 0 ? sessionKeyRaw.trim() : defaultSessionKey;
|
|
17275
|
+
const externalIdRaw = row.turnId ?? row.turn_id ?? row.id;
|
|
17276
|
+
turns.push({
|
|
17277
|
+
source: "openclaw",
|
|
17278
|
+
sessionKey,
|
|
17279
|
+
role,
|
|
17280
|
+
content,
|
|
17281
|
+
timestamp,
|
|
17282
|
+
externalId: typeof externalIdRaw === "string" ? externalIdRaw : void 0
|
|
17283
|
+
});
|
|
17284
|
+
}
|
|
17285
|
+
return { turns, warnings };
|
|
17286
|
+
}
|
|
17287
|
+
};
|
|
17288
|
+
|
|
16669
17289
|
// src/cli.ts
|
|
16670
17290
|
function rankCandidateForKeep(a, b) {
|
|
16671
17291
|
const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
|
|
@@ -16715,6 +17335,83 @@ function planExactDuplicateDeletions(memories) {
|
|
|
16715
17335
|
function planAggressiveDuplicateDeletions(memories) {
|
|
16716
17336
|
return buildDedupePlan(memories, (memory) => normalizeAggressiveBody(memory.content));
|
|
16717
17337
|
}
|
|
17338
|
+
async function withTimeout(promise, timeoutMs, timeoutMessage) {
|
|
17339
|
+
let timer;
|
|
17340
|
+
try {
|
|
17341
|
+
return await Promise.race([
|
|
17342
|
+
promise,
|
|
17343
|
+
new Promise((_, reject) => {
|
|
17344
|
+
timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
17345
|
+
})
|
|
17346
|
+
]);
|
|
17347
|
+
} finally {
|
|
17348
|
+
if (timer) clearTimeout(timer);
|
|
17349
|
+
}
|
|
17350
|
+
}
|
|
17351
|
+
async function runReplayCliCommand(orchestrator, options) {
|
|
17352
|
+
const extractionIdleTimeoutMs = Number.isFinite(options.extractionIdleTimeoutMs) ? Math.max(1e3, Math.floor(options.extractionIdleTimeoutMs)) : 15 * 6e4;
|
|
17353
|
+
const inputRaw = await readFile23(options.inputPath, "utf-8");
|
|
17354
|
+
const registry = buildReplayNormalizerRegistry([
|
|
17355
|
+
openclawReplayNormalizer,
|
|
17356
|
+
claudeReplayNormalizer,
|
|
17357
|
+
chatgptReplayNormalizer
|
|
17358
|
+
]);
|
|
17359
|
+
const ingestBatchSize = clampBatchSize(options.batchSize);
|
|
17360
|
+
const turnsBySession = /* @__PURE__ */ new Map();
|
|
17361
|
+
const ingestSessionChunk = async (sessionTurns) => {
|
|
17362
|
+
const deadlineMs = Date.now() + extractionIdleTimeoutMs;
|
|
17363
|
+
await withTimeout(
|
|
17364
|
+
orchestrator.ingestReplayBatch(sessionTurns, { deadlineMs }),
|
|
17365
|
+
extractionIdleTimeoutMs,
|
|
17366
|
+
`replay extraction batch did not complete before timeout (${extractionIdleTimeoutMs}ms)`
|
|
17367
|
+
);
|
|
17368
|
+
};
|
|
17369
|
+
const summary = await runReplay(
|
|
17370
|
+
options.source,
|
|
17371
|
+
inputRaw,
|
|
17372
|
+
registry,
|
|
17373
|
+
{
|
|
17374
|
+
onBatch: async (batch) => {
|
|
17375
|
+
for (const turn of batch) {
|
|
17376
|
+
const key = normalizeReplaySessionKey(turn.sessionKey);
|
|
17377
|
+
const turns = turnsBySession.get(key) ?? [];
|
|
17378
|
+
turns.push(turn);
|
|
17379
|
+
turnsBySession.set(key, turns);
|
|
17380
|
+
while (turns.length >= ingestBatchSize) {
|
|
17381
|
+
const chunk = turns.splice(0, ingestBatchSize);
|
|
17382
|
+
await ingestSessionChunk(chunk);
|
|
17383
|
+
}
|
|
17384
|
+
}
|
|
17385
|
+
}
|
|
17386
|
+
},
|
|
17387
|
+
{
|
|
17388
|
+
from: options.from,
|
|
17389
|
+
to: options.to,
|
|
17390
|
+
dryRun: options.dryRun === true,
|
|
17391
|
+
startOffset: options.startOffset,
|
|
17392
|
+
maxTurns: options.maxTurns,
|
|
17393
|
+
batchSize: options.batchSize,
|
|
17394
|
+
defaultSessionKey: options.defaultSessionKey,
|
|
17395
|
+
strict: options.strict
|
|
17396
|
+
}
|
|
17397
|
+
);
|
|
17398
|
+
if (!summary.dryRun) {
|
|
17399
|
+
for (const turns of turnsBySession.values()) {
|
|
17400
|
+
if (turns.length === 0) continue;
|
|
17401
|
+
await ingestSessionChunk(turns);
|
|
17402
|
+
}
|
|
17403
|
+
if (options.runConsolidation === true) {
|
|
17404
|
+
const consolidationIdle = await orchestrator.waitForConsolidationIdle(extractionIdleTimeoutMs);
|
|
17405
|
+
if (!consolidationIdle) {
|
|
17406
|
+
throw new Error(
|
|
17407
|
+
`replay consolidation did not become idle before timeout (${extractionIdleTimeoutMs}ms)`
|
|
17408
|
+
);
|
|
17409
|
+
}
|
|
17410
|
+
await orchestrator.runConsolidationNow();
|
|
17411
|
+
}
|
|
17412
|
+
}
|
|
17413
|
+
return summary;
|
|
17414
|
+
}
|
|
16718
17415
|
async function getPluginVersion() {
|
|
16719
17416
|
try {
|
|
16720
17417
|
const pkgPath = new URL("../package.json", import.meta.url);
|
|
@@ -16945,6 +17642,60 @@ function registerCli(api, orchestrator) {
|
|
|
16945
17642
|
});
|
|
16946
17643
|
console.log("OK");
|
|
16947
17644
|
});
|
|
17645
|
+
cmd.command("replay").description("Import replay transcripts from external exports").option("--source <source>", "Replay source: openclaw|claude|chatgpt").option("--input <path>", "Path to replay export file").option("--from <iso>", "Inclusive lower bound timestamp (ISO UTC)").option("--to <iso>", "Inclusive upper bound timestamp (ISO UTC)").option("--dry-run", "Parse and validate only; do not enqueue extraction").option("--start-offset <n>", "Start replay at offset", "0").option("--max-turns <n>", "Maximum turns to process", "0").option("--batch-size <n>", "Replay ingestion batch size", "100").option("--default-session-key <key>", "Fallback session key when source session identifiers are missing").option("--strict", "Fail on invalid source rows").option("--run-consolidation", "Run consolidation after replay ingestion completes").option("--idle-timeout-ms <n>", "Extraction idle timeout per replay batch/final drain in milliseconds", "900000").action(async (...args) => {
|
|
17646
|
+
const options = args[0] ?? {};
|
|
17647
|
+
const sourceRaw = typeof options.source === "string" ? options.source.trim().toLowerCase() : "";
|
|
17648
|
+
const inputPath = typeof options.input === "string" ? options.input.trim() : "";
|
|
17649
|
+
if (!isReplaySource(sourceRaw)) {
|
|
17650
|
+
console.log("Missing or invalid --source. Use one of: openclaw, claude, chatgpt.");
|
|
17651
|
+
return;
|
|
17652
|
+
}
|
|
17653
|
+
if (inputPath.length === 0) {
|
|
17654
|
+
console.log("Missing --input. Example: openclaw engram replay --source openclaw --input /tmp/replay.jsonl");
|
|
17655
|
+
return;
|
|
17656
|
+
}
|
|
17657
|
+
const startOffset = parseInt(String(options.startOffset ?? "0"), 10);
|
|
17658
|
+
const maxTurnsRaw = parseInt(String(options.maxTurns ?? "0"), 10);
|
|
17659
|
+
const batchSize = parseInt(String(options.batchSize ?? "100"), 10);
|
|
17660
|
+
const idleTimeoutMs = parseInt(String(options.idleTimeoutMs ?? "900000"), 10);
|
|
17661
|
+
const summary = await runReplayCliCommand(orchestrator, {
|
|
17662
|
+
source: sourceRaw,
|
|
17663
|
+
inputPath,
|
|
17664
|
+
from: typeof options.from === "string" ? options.from : void 0,
|
|
17665
|
+
to: typeof options.to === "string" ? options.to : void 0,
|
|
17666
|
+
dryRun: options.dryRun === true,
|
|
17667
|
+
startOffset: Number.isFinite(startOffset) ? Math.max(0, startOffset) : 0,
|
|
17668
|
+
maxTurns: Number.isFinite(maxTurnsRaw) && maxTurnsRaw > 0 ? maxTurnsRaw : void 0,
|
|
17669
|
+
batchSize: Number.isFinite(batchSize) && batchSize > 0 ? batchSize : 100,
|
|
17670
|
+
defaultSessionKey: typeof options.defaultSessionKey === "string" && options.defaultSessionKey.trim().length > 0 ? options.defaultSessionKey.trim() : void 0,
|
|
17671
|
+
strict: options.strict === true,
|
|
17672
|
+
runConsolidation: options.runConsolidation === true,
|
|
17673
|
+
extractionIdleTimeoutMs: Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0 ? idleTimeoutMs : 9e5
|
|
17674
|
+
});
|
|
17675
|
+
console.log(`Replay source: ${summary.source}`);
|
|
17676
|
+
console.log(`Parsed turns: ${summary.parsedTurns}`);
|
|
17677
|
+
console.log(`Valid turns: ${summary.validTurns}`);
|
|
17678
|
+
console.log(`Invalid turns: ${summary.invalidTurns}`);
|
|
17679
|
+
console.log(`Filtered by date: ${summary.filteredByDate}`);
|
|
17680
|
+
console.log(`Skipped by offset: ${summary.skippedByOffset}`);
|
|
17681
|
+
console.log(`Processed turns: ${summary.processedTurns}`);
|
|
17682
|
+
console.log(`Batches: ${summary.batchCount}`);
|
|
17683
|
+
console.log(`Dry run: ${summary.dryRun ? "yes" : "no"}`);
|
|
17684
|
+
console.log(`Next offset: ${summary.nextOffset}`);
|
|
17685
|
+
if (summary.firstTimestamp) console.log(`First timestamp: ${summary.firstTimestamp}`);
|
|
17686
|
+
if (summary.lastTimestamp) console.log(`Last timestamp: ${summary.lastTimestamp}`);
|
|
17687
|
+
if (summary.warnings.length > 0) {
|
|
17688
|
+
console.log(`Warnings (${summary.warnings.length}):`);
|
|
17689
|
+
for (const warning of summary.warnings.slice(0, 20)) {
|
|
17690
|
+
const idx = typeof warning.index === "number" ? ` @${warning.index}` : "";
|
|
17691
|
+
console.log(` - ${warning.code}${idx}: ${warning.message}`);
|
|
17692
|
+
}
|
|
17693
|
+
if (summary.warnings.length > 20) {
|
|
17694
|
+
console.log(` ... and ${summary.warnings.length - 20} more`);
|
|
17695
|
+
}
|
|
17696
|
+
}
|
|
17697
|
+
console.log("OK");
|
|
17698
|
+
});
|
|
16948
17699
|
cmd.command("dedupe-exact").description("Delete exact duplicate memory entries (same body text), keeping highest-confidence/newest copy").option("--dry-run", "Show what would be deleted without deleting files").option("--namespace <ns>", "Namespace to dedupe (v3.0+, default: config defaultNamespace)", "").option("--qmd-sync", "Run QMD update/embed after deletions (default: off)").action(async (...args) => {
|
|
16949
17700
|
const options = args[0] ?? {};
|
|
16950
17701
|
const dryRun = options.dryRun === true;
|