@ixo/oracle-runtime-workers 0.37.1 → 0.38.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/package.json +3 -3
- package/src/attachments/channel-links.ts +74 -7
- package/src/attachments/channel.ts +15 -4
- package/src/attachments/content-blocks.ts +19 -16
- package/src/attachments/download.ts +173 -50
- package/src/attachments/index.ts +6 -5
- package/src/attachments/pipeline.ts +117 -49
- package/src/attachments/resize.ts +142 -0
- package/src/attachments/retention.ts +26 -70
- package/src/attachments/room-media.ts +643 -0
- package/src/attachments/route.ts +47 -4
- package/src/attachments/sandbox-archive.ts +15 -4
- package/src/attachments/sanitize.ts +24 -13
- package/src/attachments/view.ts +82 -38
- package/src/core/main-agent-types.ts +8 -0
- package/src/core/main-agent.ts +18 -3
- package/src/core/middlewares/attachment-recall.ts +502 -0
- package/src/core/middlewares/index.ts +5 -0
- package/src/do/contracts.ts +53 -23
- package/src/do/flush-schedule.ts +8 -2
- package/src/do/room-mirror.ts +144 -20
- package/src/do/transcript.ts +1 -1
- package/src/do/turn-attachments.ts +45 -6
- package/src/do/user-oracle-do.ts +588 -105
- package/src/llm/provider-error.ts +5 -2
- package/src/llm/stream-liveness.ts +3 -2
- package/src/matrix/gateway-do.ts +164 -102
- package/src/matrix/ingest.ts +12 -3
- package/src/matrix/media-deadline.ts +5 -47
- package/src/plugins/attachments/attachments.plugin.ts +3 -3
- package/src/plugins/attachments/view-attachment-tool.ts +6 -4
- package/src/sqlite/inline-payloads.ts +121 -0
- package/src/sqlite/sqlite-saver.ts +241 -42
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* messages(thread_id, checkpoint_ns, checkpoint_id, message_id PK,
|
|
15
15
|
* message_type, message_content, message BLOB, created_at)
|
|
16
16
|
* schema_migrations(version PK, name, applied_at)
|
|
17
|
+
* attachment_payload_sweeps(thread_id PK, swept_at)
|
|
17
18
|
*
|
|
18
19
|
* Behaviour kept from the original:
|
|
19
20
|
* - `channel_values.messages` is stripped from the stored checkpoint and each
|
|
@@ -25,6 +26,18 @@
|
|
|
25
26
|
* - `pending_sends` migration for pre-v4 checkpoints.
|
|
26
27
|
* - Serialization is `JsonPlusSerializer` (the `BaseCheckpointSaver` default).
|
|
27
28
|
*
|
|
29
|
+
* Attachment bytes are never stored: every write path (message rows, the
|
|
30
|
+
* checkpoint blob's channel values, pending writes) stores a human message
|
|
31
|
+
* that carries inline base64 blocks as its placeholder copy
|
|
32
|
+
* (`inline-payloads.ts`), while the run's in-memory state keeps the blocks.
|
|
33
|
+
* Whatever reads the database back — the next turn, a durable run resumed
|
|
34
|
+
* after a reset, the transcript, the owner copy — sees the placeholder.
|
|
35
|
+
* Human message rows written before that (with their bytes) are rewritten
|
|
36
|
+
* once per thread, on the thread's first load (`sweepInlinePayloads`; the
|
|
37
|
+
* thread is then recorded in `attachment_payload_sweeps`). A write whose
|
|
38
|
+
* serialized form still holds a payload block is logged
|
|
39
|
+
* (`containsInlinePayload`): tool and AI messages are never stripped.
|
|
40
|
+
*
|
|
28
41
|
* Differences: everything is async (wa-sqlite API), `console` replaces the
|
|
29
42
|
* `@ixo/logger` Logger, the oracle name stamped on messages comes from
|
|
30
43
|
* `options.oracleName` instead of `process.env`, and each `put`/`putWrites`/
|
|
@@ -54,6 +67,15 @@ import {
|
|
|
54
67
|
stringify,
|
|
55
68
|
type CleanAdditionalKwargs,
|
|
56
69
|
} from './serialization';
|
|
70
|
+
import {
|
|
71
|
+
containsInlinePayload,
|
|
72
|
+
INLINE_PAYLOAD_NEEDLE,
|
|
73
|
+
withoutInlinePayloads,
|
|
74
|
+
} from './inline-payloads';
|
|
75
|
+
import {
|
|
76
|
+
hasInlinePayload,
|
|
77
|
+
offloadInlinePayloads,
|
|
78
|
+
} from '../attachments/retention';
|
|
57
79
|
|
|
58
80
|
export interface SqliteSaverOptions {
|
|
59
81
|
/**
|
|
@@ -145,6 +167,17 @@ function sameFingerprint(
|
|
|
145
167
|
);
|
|
146
168
|
}
|
|
147
169
|
|
|
170
|
+
/**
|
|
171
|
+
* The stored form of one message object: the fingerprint it was encoded
|
|
172
|
+
* from, its blob and, when the stored form is a placeholder copy (inline
|
|
173
|
+
* attachment bytes left out), that copy's `message_content` text.
|
|
174
|
+
*/
|
|
175
|
+
interface EncodedMessage {
|
|
176
|
+
fingerprint: MessageFingerprint;
|
|
177
|
+
blob: Uint8Array;
|
|
178
|
+
storedContent?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
/** A message's row parameters, and whether its stored row already holds them. */
|
|
149
182
|
interface EncodedMessageRow {
|
|
150
183
|
row: SqlParam[];
|
|
@@ -270,7 +303,31 @@ const migration002: Migration = {
|
|
|
270
303
|
},
|
|
271
304
|
};
|
|
272
305
|
|
|
273
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Migration 003: `attachment_payload_sweeps`, the threads whose human
|
|
308
|
+
* message rows no longer hold inline attachment bytes (see
|
|
309
|
+
* `sweepInlinePayloads`).
|
|
310
|
+
*/
|
|
311
|
+
const migration003: Migration = {
|
|
312
|
+
version: 3,
|
|
313
|
+
name: 'add_attachment_payload_sweeps',
|
|
314
|
+
up: async (db) => {
|
|
315
|
+
await db.run(`
|
|
316
|
+
CREATE TABLE IF NOT EXISTS attachment_payload_sweeps (
|
|
317
|
+
thread_id TEXT NOT NULL PRIMARY KEY,
|
|
318
|
+
swept_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
319
|
+
)`);
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const MIGRATIONS: readonly Migration[] = [
|
|
324
|
+
migration001,
|
|
325
|
+
migration002,
|
|
326
|
+
migration003,
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
/** Rows the one-time attachment sweep inflates and rewrites per transaction. */
|
|
330
|
+
const SWEEP_BATCH_ROWS = 8;
|
|
274
331
|
|
|
275
332
|
// `writes.value` blobs may be gzipped, so they travel as hex() — a
|
|
276
333
|
// CAST(... AS TEXT) of compressed bytes would mangle them irreversibly.
|
|
@@ -334,10 +391,11 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
334
391
|
* third one per step — on a long thread, the peak that reset a 128 MB
|
|
335
392
|
* isolate at the end of a tool turn.
|
|
336
393
|
*/
|
|
337
|
-
private readonly encoded = new WeakMap<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
394
|
+
private readonly encoded = new WeakMap<BaseMessage, EncodedMessage>();
|
|
395
|
+
/** Threads known to hold no inline attachment bytes in their rows. */
|
|
396
|
+
private readonly sweptThreads = new Set<string>();
|
|
397
|
+
/** The sweep in progress per thread: concurrent loads share it. */
|
|
398
|
+
private readonly sweeps = new Map<string, Promise<void>>();
|
|
341
399
|
|
|
342
400
|
constructor(
|
|
343
401
|
db: DoSqliteDatabase,
|
|
@@ -470,6 +528,7 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
470
528
|
*/
|
|
471
529
|
async listThreadMessages(threadId: string): Promise<BaseMessage[]> {
|
|
472
530
|
await this.setup();
|
|
531
|
+
await this.ensureSwept(threadId);
|
|
473
532
|
const rows = await this.db.exec<MessageRow>(
|
|
474
533
|
'SELECT message FROM messages WHERE thread_id = ? ORDER BY created_at ASC, rowid ASC',
|
|
475
534
|
[threadId],
|
|
@@ -514,6 +573,7 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
514
573
|
},
|
|
515
574
|
): Promise<ThreadMessageRow[]> {
|
|
516
575
|
await this.setup();
|
|
576
|
+
await this.ensureSwept(threadId);
|
|
517
577
|
const older = opts.direction === 'older';
|
|
518
578
|
const cmp = older ? '<' : '>';
|
|
519
579
|
const rowCmp = opts.inclusive ? `${cmp}=` : cmp;
|
|
@@ -548,8 +608,8 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
548
608
|
/**
|
|
549
609
|
* The thread's messages whose stored plain content (`message_content`)
|
|
550
610
|
* contains `contentContains`, oldest first — a SQL `LIKE` pre-filter, so
|
|
551
|
-
* only the matching rows' blobs are inflated.
|
|
552
|
-
*
|
|
611
|
+
* only the matching rows' blobs are inflated. `view_attachment` uses it to
|
|
612
|
+
* find the messages that quote an attachment reference.
|
|
553
613
|
*/
|
|
554
614
|
async listThreadMessagesMatching(
|
|
555
615
|
threadId: string,
|
|
@@ -589,25 +649,103 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
589
649
|
}
|
|
590
650
|
|
|
591
651
|
/**
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
* uses it to swap inline bytes for a placeholder after a turn.
|
|
652
|
+
* Run the thread's one-time attachment sweep unless it ran already (in
|
|
653
|
+
* this object, or recorded in `attachment_payload_sweeps`). A failed
|
|
654
|
+
* sweep is logged and left for the next load; the read goes on.
|
|
596
655
|
*/
|
|
597
|
-
|
|
598
|
-
threadId
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
656
|
+
private ensureSwept(threadId: string): Promise<void> {
|
|
657
|
+
if (this.sweptThreads.has(threadId)) return Promise.resolve();
|
|
658
|
+
const running = this.sweeps.get(threadId);
|
|
659
|
+
if (running) return running;
|
|
660
|
+
const sweep = this.sweepInlinePayloads(threadId)
|
|
661
|
+
.then(
|
|
662
|
+
() => {
|
|
663
|
+
this.sweptThreads.add(threadId);
|
|
664
|
+
},
|
|
665
|
+
(error: unknown) => {
|
|
666
|
+
console.warn(
|
|
667
|
+
`[sqlite-saver] thread ${threadId}: attachment sweep failed, retried on the next load`,
|
|
668
|
+
error,
|
|
669
|
+
);
|
|
670
|
+
},
|
|
671
|
+
)
|
|
672
|
+
.finally(() => {
|
|
673
|
+
this.sweeps.delete(threadId);
|
|
674
|
+
});
|
|
675
|
+
this.sweeps.set(threadId, sweep);
|
|
676
|
+
return sweep;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Rewrite the thread's human message rows that still hold inline
|
|
681
|
+
* attachment bytes (written before the saver left them out) as their
|
|
682
|
+
* placeholder copy, then record the thread as swept. Rows summarised out
|
|
683
|
+
* of graph state are never written again, so without this their bytes
|
|
684
|
+
* would stay in the transcript and the owner copy for good. The matching
|
|
685
|
+
* rows are found by their `message_content` (no blob inflated for the
|
|
686
|
+
* rest) and rewritten `SWEEP_BATCH_ROWS` at a time, each row in place
|
|
687
|
+
* (same rowid, checkpoint linkage, `created_at` and kwargs). Tool and AI
|
|
688
|
+
* messages are left as they are (see `inline-payloads.ts`).
|
|
689
|
+
*/
|
|
690
|
+
private async sweepInlinePayloads(threadId: string): Promise<void> {
|
|
691
|
+
const swept = await this.db.get<{ found: number }>(
|
|
692
|
+
'SELECT 1 AS found FROM attachment_payload_sweeps WHERE thread_id = ?',
|
|
693
|
+
[threadId],
|
|
609
694
|
);
|
|
610
|
-
|
|
695
|
+
if (swept !== undefined) return;
|
|
696
|
+
const needle = likeContains(INLINE_PAYLOAD_NEEDLE);
|
|
697
|
+
const matching = await this.db.exec<{ rowid: number }>(
|
|
698
|
+
`SELECT rowid FROM messages WHERE thread_id = ? AND message_type = 'human' AND message_content LIKE ? ESCAPE '\\' ORDER BY rowid`,
|
|
699
|
+
[threadId, needle],
|
|
700
|
+
);
|
|
701
|
+
let rewritten = 0;
|
|
702
|
+
for (let i = 0; i < matching.length; i += SWEEP_BATCH_ROWS) {
|
|
703
|
+
const batch = matching.slice(i, i + SWEEP_BATCH_ROWS).map((r) => r.rowid);
|
|
704
|
+
const rows = await this.db.exec<{
|
|
705
|
+
rowid: number;
|
|
706
|
+
message_id: string;
|
|
707
|
+
message: Uint8Array | string;
|
|
708
|
+
}>(
|
|
709
|
+
'SELECT rowid, message_id, message FROM messages WHERE rowid IN (SELECT value FROM json_each(?))',
|
|
710
|
+
[JSON.stringify(batch)],
|
|
711
|
+
);
|
|
712
|
+
const updates: SqlParam[][] = [];
|
|
713
|
+
for (const row of rows) {
|
|
714
|
+
const placeholder = offloadInlinePayloads(
|
|
715
|
+
await this.loadMessage(row.message),
|
|
716
|
+
);
|
|
717
|
+
if (!placeholder) continue;
|
|
718
|
+
updates.push([
|
|
719
|
+
contentColumn(placeholder.content),
|
|
720
|
+
await compressForStorage(
|
|
721
|
+
stringify(placeholder, (_key, value) => _default(value)),
|
|
722
|
+
),
|
|
723
|
+
row.rowid,
|
|
724
|
+
row.message_id,
|
|
725
|
+
needle,
|
|
726
|
+
]);
|
|
727
|
+
}
|
|
728
|
+
if (updates.length === 0) continue;
|
|
729
|
+
await this.db.transaction(async () => {
|
|
730
|
+
for (const update of updates) {
|
|
731
|
+
// Only a row that is still the one read: same message, bytes
|
|
732
|
+
// still in it (a `put` in between stored its placeholder).
|
|
733
|
+
const result = await this.db.run(
|
|
734
|
+
`UPDATE messages SET message_content = ?, message = ? WHERE rowid = ? AND message_id = ? AND message_content LIKE ? ESCAPE '\\'`,
|
|
735
|
+
update,
|
|
736
|
+
);
|
|
737
|
+
rewritten += result.changes;
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
await this.db.run(
|
|
742
|
+
'INSERT OR IGNORE INTO attachment_payload_sweeps (thread_id) VALUES (?)',
|
|
743
|
+
[threadId],
|
|
744
|
+
);
|
|
745
|
+
if (matching.length > 0)
|
|
746
|
+
console.info(
|
|
747
|
+
`[sqlite-saver] thread ${threadId}: ${rewritten} of ${matching.length} stored human message(s) with inline attachment bytes rewritten as their placeholder`,
|
|
748
|
+
);
|
|
611
749
|
}
|
|
612
750
|
|
|
613
751
|
private async loadMessage(raw: Uint8Array | string): Promise<BaseMessage> {
|
|
@@ -643,6 +781,7 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
643
781
|
const checkpointId: unknown = configurable.checkpoint_id;
|
|
644
782
|
if (typeof threadId !== 'string' || typeof checkpointNs !== 'string')
|
|
645
783
|
return undefined;
|
|
784
|
+
await this.ensureSwept(threadId);
|
|
646
785
|
|
|
647
786
|
const row =
|
|
648
787
|
typeof checkpointId === 'string' && checkpointId.length > 0
|
|
@@ -859,7 +998,7 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
859
998
|
if (typeof checkpointNs !== 'string')
|
|
860
999
|
throw new Error('"checkpoint_ns" must be a string.');
|
|
861
1000
|
|
|
862
|
-
const { checkpoint, messages } =
|
|
1001
|
+
const { checkpoint, messages } = toStoredCheckpoint(checkpointIn);
|
|
863
1002
|
const [[type1, serializedCheckpoint], [type2, serializedMetadata]] =
|
|
864
1003
|
await Promise.all([
|
|
865
1004
|
this.serde.dumpsTyped(checkpoint),
|
|
@@ -869,6 +1008,8 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
869
1008
|
throw new Error(
|
|
870
1009
|
'Failed to serialized checkpoint and metadata to the same type.',
|
|
871
1010
|
);
|
|
1011
|
+
if (containsInlinePayload(serializedCheckpoint))
|
|
1012
|
+
warnStoredPayload(`the checkpoint of thread ${threadId}`);
|
|
872
1013
|
|
|
873
1014
|
const messageRows = await Promise.all(
|
|
874
1015
|
messages?.map((message) =>
|
|
@@ -1034,6 +1175,9 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
1034
1175
|
*/
|
|
1035
1176
|
private rememberLoaded(message: BaseMessage, raw: Uint8Array | string): void {
|
|
1036
1177
|
if (!(raw instanceof Uint8Array)) return;
|
|
1178
|
+
// A row written before attachment bytes were left out of storage still
|
|
1179
|
+
// holds them: not remembered, so the next `put` stores its placeholder.
|
|
1180
|
+
if (hasInlinePayload(message)) return;
|
|
1037
1181
|
this.encoded.set(message, {
|
|
1038
1182
|
fingerprint: fingerprintOf(message, this.normalizeKwargs(message)),
|
|
1039
1183
|
blob: raw,
|
|
@@ -1044,6 +1188,9 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
1044
1188
|
* A message's row parameters. A message whose fingerprint matches the
|
|
1045
1189
|
* blob last encoded or loaded for it reuses that blob without
|
|
1046
1190
|
* serializing it (`unchanged`); any other is serialized and compressed.
|
|
1191
|
+
* A human message with inline attachment bytes is stored as its
|
|
1192
|
+
* placeholder copy (`offloadInlinePayloads`): the copy is serialized once
|
|
1193
|
+
* and dropped, and only its blob and content text are remembered.
|
|
1047
1194
|
*/
|
|
1048
1195
|
private async encodeMessageRow(
|
|
1049
1196
|
message: BaseMessage,
|
|
@@ -1053,20 +1200,26 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
1053
1200
|
): Promise<EncodedMessageRow> {
|
|
1054
1201
|
const merged = this.normalizeKwargs(message);
|
|
1055
1202
|
const fingerprint = fingerprintOf(message, merged);
|
|
1056
|
-
|
|
1057
|
-
let serialized: Uint8Array;
|
|
1203
|
+
let encoded = this.encoded.get(message);
|
|
1058
1204
|
let unchanged = false;
|
|
1059
1205
|
if (
|
|
1060
|
-
|
|
1061
|
-
sameFingerprint(
|
|
1206
|
+
encoded !== undefined &&
|
|
1207
|
+
sameFingerprint(encoded.fingerprint, fingerprint)
|
|
1062
1208
|
) {
|
|
1063
|
-
serialized = cached.blob;
|
|
1064
1209
|
unchanged = true;
|
|
1065
1210
|
} else {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1211
|
+
const placeholder = offloadInlinePayloads(message);
|
|
1212
|
+
const stored = placeholder ?? message;
|
|
1213
|
+
encoded = {
|
|
1214
|
+
fingerprint,
|
|
1215
|
+
blob: await compressForStorage(
|
|
1216
|
+
stringify(stored, (_key, value) => _default(value)),
|
|
1217
|
+
),
|
|
1218
|
+
...(placeholder
|
|
1219
|
+
? { storedContent: contentColumn(placeholder.content) }
|
|
1220
|
+
: {}),
|
|
1221
|
+
};
|
|
1222
|
+
this.encoded.set(message, encoded);
|
|
1070
1223
|
}
|
|
1071
1224
|
const messageId =
|
|
1072
1225
|
message.id ??
|
|
@@ -1081,10 +1234,8 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
1081
1234
|
checkpointId,
|
|
1082
1235
|
messageId,
|
|
1083
1236
|
message.getType(),
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
: JSON.stringify(message.content),
|
|
1087
|
-
serialized,
|
|
1237
|
+
encoded.storedContent ?? contentColumn(message.content),
|
|
1238
|
+
encoded.blob,
|
|
1088
1239
|
merged.timestamp,
|
|
1089
1240
|
],
|
|
1090
1241
|
unchanged,
|
|
@@ -1170,9 +1321,13 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
1170
1321
|
const checkpointNs: unknown = configurable.checkpoint_ns;
|
|
1171
1322
|
const ns = typeof checkpointNs === 'string' ? checkpointNs : '';
|
|
1172
1323
|
|
|
1324
|
+
const holding: string[] = [];
|
|
1173
1325
|
const rows = await Promise.all(
|
|
1174
1326
|
writes.map(async (write, idx): Promise<SqlParam[]> => {
|
|
1175
|
-
const [type, serialized] = await this.serde.dumpsTyped(
|
|
1327
|
+
const [type, serialized] = await this.serde.dumpsTyped(
|
|
1328
|
+
withoutInlinePayloads(write[1]),
|
|
1329
|
+
);
|
|
1330
|
+
if (containsInlinePayload(serialized)) holding.push(write[0]);
|
|
1176
1331
|
return [
|
|
1177
1332
|
resolvedThreadId,
|
|
1178
1333
|
ns,
|
|
@@ -1185,6 +1340,10 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
1185
1340
|
];
|
|
1186
1341
|
}),
|
|
1187
1342
|
);
|
|
1343
|
+
if (holding.length > 0)
|
|
1344
|
+
warnStoredPayload(
|
|
1345
|
+
`a pending write of thread ${resolvedThreadId} (task ${taskId}, channel ${holding.join(', ')})`,
|
|
1346
|
+
);
|
|
1188
1347
|
await this.db.transaction(async () => {
|
|
1189
1348
|
for (const row of rows) {
|
|
1190
1349
|
await this.db.run(
|
|
@@ -1198,7 +1357,12 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
1198
1357
|
|
|
1199
1358
|
async deleteThread(threadId: string): Promise<void> {
|
|
1200
1359
|
await this.setup();
|
|
1360
|
+
this.sweptThreads.delete(threadId);
|
|
1201
1361
|
await this.db.transaction(async () => {
|
|
1362
|
+
await this.db.run(
|
|
1363
|
+
'DELETE FROM attachment_payload_sweeps WHERE thread_id = ?',
|
|
1364
|
+
[threadId],
|
|
1365
|
+
);
|
|
1202
1366
|
await this.db.run('DELETE FROM checkpoints WHERE thread_id = ?', [
|
|
1203
1367
|
threadId,
|
|
1204
1368
|
]);
|
|
@@ -1253,18 +1417,53 @@ function asReasoningDetails(
|
|
|
1253
1417
|
);
|
|
1254
1418
|
}
|
|
1255
1419
|
|
|
1420
|
+
/**
|
|
1421
|
+
* One line for a write that stores inline base64 after stripping
|
|
1422
|
+
* (`containsInlinePayload`): a tool or AI message carrying base64, or a
|
|
1423
|
+
* human message in a shape `withoutInlinePayloads` does not reach.
|
|
1424
|
+
*/
|
|
1425
|
+
function warnStoredPayload(what: string): void {
|
|
1426
|
+
console.warn(
|
|
1427
|
+
`[sqlite-saver] ${what} still holds inline base64 after attachment stripping: a tool/AI message with base64 content, or a message shape inline-payloads.ts does not reach; those bytes are stored`,
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/** The `messages.message_content` text of a message's content. */
|
|
1432
|
+
function contentColumn(content: BaseMessage['content']): string {
|
|
1433
|
+
return typeof content === 'string' ? content : JSON.stringify(content);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1256
1436
|
function isCheckpointWithMessages(
|
|
1257
1437
|
checkpoint: Checkpoint,
|
|
1258
1438
|
): checkpoint is CheckpointWithMessages {
|
|
1259
1439
|
return 'messages' in checkpoint.channel_values;
|
|
1260
1440
|
}
|
|
1261
1441
|
|
|
1262
|
-
|
|
1442
|
+
/**
|
|
1443
|
+
* The checkpoint as its blob stores it: `channel_values.messages` taken out
|
|
1444
|
+
* (they become message rows, returned) and inline attachment bytes left out
|
|
1445
|
+
* of every other channel value. The input is never modified; it is returned
|
|
1446
|
+
* as is when there is nothing to take out.
|
|
1447
|
+
*/
|
|
1448
|
+
function toStoredCheckpoint(checkpoint: Checkpoint): {
|
|
1263
1449
|
checkpoint: Checkpoint;
|
|
1264
1450
|
messages?: BaseMessage[];
|
|
1265
1451
|
} {
|
|
1266
|
-
|
|
1452
|
+
const messages = isCheckpointWithMessages(checkpoint)
|
|
1453
|
+
? checkpoint.channel_values.messages
|
|
1454
|
+
: undefined;
|
|
1455
|
+
let channelValues: ChannelValues | undefined;
|
|
1456
|
+
for (const [channel, value] of Object.entries(checkpoint.channel_values)) {
|
|
1457
|
+
if (channel === 'messages') continue;
|
|
1458
|
+
const stored = withoutInlinePayloads(value);
|
|
1459
|
+
if (stored === value) continue;
|
|
1460
|
+
channelValues ??= { ...checkpoint.channel_values };
|
|
1461
|
+
channelValues[channel] = stored;
|
|
1462
|
+
}
|
|
1463
|
+
if (messages === undefined && channelValues === undefined)
|
|
1464
|
+
return { checkpoint };
|
|
1267
1465
|
const copy = copyCheckpoint(checkpoint);
|
|
1466
|
+
if (channelValues !== undefined) copy.channel_values = channelValues;
|
|
1268
1467
|
delete copy.channel_values.messages;
|
|
1269
|
-
return { checkpoint: copy, messages
|
|
1468
|
+
return { checkpoint: copy, ...(messages ? { messages } : {}) };
|
|
1270
1469
|
}
|