@gobing-ai/ts-llm-jsonl-importer 0.4.28 → 0.4.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/README.md CHANGED
@@ -145,7 +145,8 @@ Downstream consumers should treat `source_file`, `source_line`, and `split_index
145
145
 
146
146
  Beyond the built-in source keys, `runJsonlImport()` accepts a fully-specified `SourceDefinition`
147
147
  for any custom agent history format. Unknown strings throw `HistoryImportError`; custom definitions
148
- are validated before any I/O (target-table names must match `VALID_TABLE_NAME` = `/^history_etl_[a-z_]+$/`,
148
+ are validated before any I/O (target-table names must match `VALID_TABLE_NAME` = `/^history_[a-z_]+$/`,
149
+ covering `history_etl_*` blob tables and the typed `history_message` / `history_tool_call` tables,
149
150
  and required fields must be present).
150
151
 
151
152
  ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAER,aAAa,EACb,YAAY,EAGZ,gBAAgB,EAEnB,MAAM,SAAS,CAAC;AAOjB;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA4KrH"}
1
+ {"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAER,aAAa,EACb,YAAY,EAGZ,gBAAgB,EAEnB,MAAM,SAAS,CAAC;AAOjB;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAgMrH"}
package/dist/importer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ambientRuntimePaths, createNodeFileSystem, resolvePath, walkDir, } from '@gobing-ai/ts-runtime';
2
2
  import { sha256 } from './hash.js';
3
- import { applyHistoryImportSchema, ensureTargetTables, insertLedger, insertRecord, ledgerExists, readCheckpoint, reconcileFullImport, resetCheckpoints, targetTableFor, writeCheckpoint, } from './jsonl-importer-dao.js';
3
+ import { applyHistoryImportSchema, checkpointUpsertOp, ensureTargetTables, ledgerExistingHashes, ledgerInsertOp, readCheckpoint, reconcileFullImport, recordInsertOp, resetCheckpoints, targetTableFor, } from './jsonl-importer-dao.js';
4
4
  import { redactRecord } from './redaction.js';
5
5
  import { resolveSourceDefinition } from './sources.js';
6
6
  /** Run the JSONL import pipeline for one source.
@@ -86,19 +86,26 @@ export async function runJsonlImport(source, options) {
86
86
  record: redacted,
87
87
  });
88
88
  desiredHashes.add(recordHash);
89
- if (await ledgerExists(options.db, recordHash)) {
90
- skippedDuplicates += 1;
91
- continue;
92
- }
93
89
  prepared.push({ split, splitIndex, normalized: redacted, recordHash });
94
90
  }
95
91
  // A rejected line is reported (validationErrors above) but never persisted — no
96
92
  // second pass, no checkpoint advance, no partial rows from this record.
97
93
  if (lineRejected)
98
94
  continue;
99
- // Second pass: resolve _messageSplitIndex → message_hash, then insert.
95
+ // Chunked ledger lookup (task 0060 F9): one `IN (...)` query per ≤200 hashes
96
+ // instead of a SELECT per record; duplicates are dropped before the write pass.
97
+ const existing = await ledgerExistingHashes(options.db, prepared.map((e) => e.recordHash));
98
+ const accepted = prepared.filter((entry) => {
99
+ if (existing.has(entry.recordHash)) {
100
+ skippedDuplicates += 1;
101
+ return false;
102
+ }
103
+ return true;
104
+ });
105
+ // Second pass: resolve _messageSplitIndex → message_hash, then batch.
100
106
  const recordHashBySplitIndex = new Map(prepared.map((e) => [e.splitIndex, e.recordHash]));
101
- for (const entry of prepared) {
107
+ const ops = [];
108
+ for (const entry of accepted) {
102
109
  const { split, splitIndex, normalized, recordHash } = entry;
103
110
  if (normalized.disposition === 'unknown') {
104
111
  unknownRecords += 1;
@@ -113,14 +120,21 @@ export async function runJsonlImport(source, options) {
113
120
  }
114
121
  }
115
122
  if (!options.dryRun) {
116
- await insertRecord(options.db, split.targetTable, recordHash, file, lineNumber, splitIndex, normalized, options.now);
117
- await insertLedger(options.db, recordHash, resolvedSource, file, lineNumber, splitIndex, split.targetTable, options.now);
123
+ // Record row + ledger row join one batch op pair — a crash between the two
124
+ // writes is impossible (task 0060 F9).
125
+ ops.push(recordInsertOp(split.targetTable, recordHash, file, lineNumber, splitIndex, normalized, options.now));
126
+ ops.push(ledgerInsertOp(recordHash, resolvedSource, file, lineNumber, splitIndex, split.targetTable, options.now));
118
127
  }
119
128
  importedRecords += 1;
120
129
  }
121
- if (lineSucceeded && !options.dryRun) {
122
- await writeCheckpoint(options.db, resolvedSource, file, lineNumber, options.now);
123
- checkpointUpdates += 1;
130
+ if (!options.dryRun && lineSucceeded) {
131
+ // Checkpoint upsert joins the same batch so an accepted line can never be
132
+ // re-imported after a mid-batch crash.
133
+ ops.push(checkpointUpsertOp(resolvedSource, file, lineNumber, options.now));
134
+ if (ops.length > 0) {
135
+ await options.db.batch(ops);
136
+ checkpointUpdates += 1;
137
+ }
124
138
  }
125
139
  }
126
140
  }
@@ -6,24 +6,49 @@ interface CheckpointRow {
6
6
  declare function targetTableFor(table: string): string;
7
7
  declare function timestamp(now: ImportOptions['now']): string;
8
8
  declare function ETL_TABLE_DDL(table: string): string;
9
- /** Apply importer-owned schema to the target database. */
9
+ /**
10
+ * Apply importer-owned schema to the target database.
11
+ *
12
+ * Creates the checkpoint, ledger, and typed contract tables from the static SQL, then materializes
13
+ * every built-in `history_etl_*` table by looping {@link SOURCE_DEFINITIONS} through
14
+ * {@link ensureTargetTables}. This keeps the migration contract "schema applied ⇒ all built-in ETL
15
+ * tables exist" for callers who apply the schema and then raw-insert without a prior import.
16
+ * Adding a new built-in source to {@link SOURCE_DEFINITIONS} is now the only edit needed.
17
+ */
10
18
  export declare function applyHistoryImportSchema(db: ImportOptions['db']): Promise<void>;
11
19
  /**
12
20
  * Ensure the ETL table(s) for a source definition exist.
13
21
  *
14
- * WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} only creates the built-in
15
- * `history_etl_*` tables and the typed contract tables. Custom source definitions
16
- * (and built-in definitions with a `splitConfig.targetTable` override) need their
17
- * target table(s) created on demand. The table name is already gated by
22
+ * WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} creates only the checkpoint, ledger, and
23
+ * typed contract tables. Every `history_etl_*` blob table — built-in and custom — is created here
24
+ * (and by {@link applyHistoryImportSchema} looping {@link SOURCE_DEFINITIONS}). Built-in definitions
25
+ * with a `splitConfig.targetTable` override also need their target table(s) created on demand. The
26
+ * table name is already gated by
18
27
  * {@link VALID_TABLE_NAME} in {@link validateSourceDefinition} / {@link targetTableFor},
19
- * so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` makes this
20
- * idempotent for built-in tables that the static schema already created.
28
+ * so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` is
29
+ * idempotent, so apply-then-import and a second apply are both safe.
21
30
  */
22
31
  declare function ensureTargetTables(db: ImportOptions['db'], definition: SourceDefinition): Promise<void>;
23
32
  declare function readCheckpoint(db: ImportOptions['db'], source: string, sourceFile: string): Promise<number>;
24
33
  declare function resetCheckpoints(db: ImportOptions['db'], source: string, files: readonly string[]): Promise<void>;
25
34
  declare function writeCheckpoint(db: ImportOptions['db'], source: string, sourceFile: string, line: number, now: ImportOptions['now']): Promise<void>;
26
35
  declare function ledgerExists(db: ImportOptions['db'], recordHash: string): Promise<boolean>;
36
+ /**
37
+ * INSERT op for one accepted record row (task 0060 F9). Pure builder so the
38
+ * per-record batch path and the single-write `insertRecord` can never drift.
39
+ * Typed tables validate unknown keys here; the caller must have ensured the
40
+ * target table exists (`ensureTargetTables` runs at import start).
41
+ */
42
+ export declare function recordInsertOp(targetTable: string, recordHash: string, sourceFile: string, sourceLine: number, splitIndex: number, payload: JsonObject, now: ImportOptions['now']): DbBatchOp;
43
+ /** INSERT op for one ledger row (task 0060 F9). */
44
+ export declare function ledgerInsertOp(recordHash: string, source: string, sourceFile: string, sourceLine: number, splitIndex: number, targetTable: string, now: ImportOptions['now']): DbBatchOp;
45
+ /** UPSERT op for one checkpoint row (task 0060 F9: checkpoint joins the record batch). */
46
+ export declare function checkpointUpsertOp(source: string, sourceFile: string, line: number, now: ImportOptions['now']): DbBatchOp;
47
+ /**
48
+ * Query which of the given hashes already exist in the ledger, in chunks of at
49
+ * most 200 per `IN (...)` (task 0060 F9 — replaces the per-record SELECT loop).
50
+ */
51
+ export declare function ledgerExistingHashes(db: ImportOptions['db'], hashes: readonly string[]): Promise<Set<string>>;
27
52
  declare function insertRecord(db: ImportOptions['db'], targetTable: string, recordHash: string, sourceFile: string, sourceLine: number, splitIndex: number, payload: JsonObject, now: ImportOptions['now']): Promise<void>;
28
53
  declare function insertLedger(db: ImportOptions['db'], recordHash: string, source: string, sourceFile: string, sourceLine: number, splitIndex: number, targetTable: string, now: ImportOptions['now']): Promise<void>;
29
54
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"jsonl-importer-dao.d.ts","sourceRoot":"","sources":["../src/jsonl-importer-dao.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"jsonl-importer-dao.d.ts","sourceRoot":"","sources":["../src/jsonl-importer-dao.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { HistoryImportError } from './errors.js';
2
2
  import { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql.js';
3
- import { VALID_TABLE_NAME } from './sources.js';
3
+ import { SOURCE_DEFINITIONS, VALID_TABLE_NAME } from './sources.js';
4
4
  /** Column allowlist per typed contract table; order is the INSERT column order. */
5
5
  const TYPED_TABLE_COLUMNS = {
6
6
  history_message: [
@@ -69,7 +69,15 @@ function ETL_TABLE_DDL(table) {
69
69
  imported_at TEXT NOT NULL
70
70
  );`;
71
71
  }
72
- /** Apply importer-owned schema to the target database. */
72
+ /**
73
+ * Apply importer-owned schema to the target database.
74
+ *
75
+ * Creates the checkpoint, ledger, and typed contract tables from the static SQL, then materializes
76
+ * every built-in `history_etl_*` table by looping {@link SOURCE_DEFINITIONS} through
77
+ * {@link ensureTargetTables}. This keeps the migration contract "schema applied ⇒ all built-in ETL
78
+ * tables exist" for callers who apply the schema and then raw-insert without a prior import.
79
+ * Adding a new built-in source to {@link SOURCE_DEFINITIONS} is now the only edit needed.
80
+ */
73
81
  export async function applyHistoryImportSchema(db) {
74
82
  for (const statement of HISTORY_IMPORT_SCHEMA_SQL.split(';')) {
75
83
  const sql = statement.trim();
@@ -77,17 +85,21 @@ export async function applyHistoryImportSchema(db) {
77
85
  await db.exec(sql);
78
86
  }
79
87
  }
88
+ for (const definition of Object.values(SOURCE_DEFINITIONS)) {
89
+ await ensureTargetTables(db, definition);
90
+ }
80
91
  }
81
92
  /**
82
93
  * Ensure the ETL table(s) for a source definition exist.
83
94
  *
84
- * WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} only creates the built-in
85
- * `history_etl_*` tables and the typed contract tables. Custom source definitions
86
- * (and built-in definitions with a `splitConfig.targetTable` override) need their
87
- * target table(s) created on demand. The table name is already gated by
95
+ * WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} creates only the checkpoint, ledger, and
96
+ * typed contract tables. Every `history_etl_*` blob table — built-in and custom — is created here
97
+ * (and by {@link applyHistoryImportSchema} looping {@link SOURCE_DEFINITIONS}). Built-in definitions
98
+ * with a `splitConfig.targetTable` override also need their target table(s) created on demand. The
99
+ * table name is already gated by
88
100
  * {@link VALID_TABLE_NAME} in {@link validateSourceDefinition} / {@link targetTableFor},
89
- * so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` makes this
90
- * idempotent for built-in tables that the static schema already created.
101
+ * so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` is
102
+ * idempotent, so apply-then-import and a second apply are both safe.
91
103
  */
92
104
  async function ensureTargetTables(db, definition) {
93
105
  const tables = new Set([targetTableFor(definition.targetTable)]);
@@ -121,8 +133,13 @@ async function ledgerExists(db, recordHash) {
121
133
  const row = await db.queryFirst('SELECT record_hash FROM history_import_ledger WHERE record_hash = ?', recordHash);
122
134
  return row !== undefined && row !== null;
123
135
  }
124
- const _ensuredTables = new Set();
125
- async function insertRecord(db, targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now) {
136
+ /**
137
+ * INSERT op for one accepted record row (task 0060 F9). Pure builder so the
138
+ * per-record batch path and the single-write `insertRecord` can never drift.
139
+ * Typed tables validate unknown keys here; the caller must have ensured the
140
+ * target table exists (`ensureTargetTables` runs at import start).
141
+ */
142
+ export function recordInsertOp(targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now) {
126
143
  const table = targetTableFor(targetTable);
127
144
  const typedColumns = TYPED_TABLE_COLUMNS[table];
128
145
  if (typedColumns !== undefined) {
@@ -142,24 +159,63 @@ async function insertRecord(db, targetTable, recordHash, sourceFile, sourceLine,
142
159
  const v = payload[col];
143
160
  return v === undefined ? null : v;
144
161
  });
145
- if (!_ensuredTables.has(table)) {
146
- // Typed tables are created by the static schema in applyHistoryImportSchema.
147
- _ensuredTables.add(table);
148
- }
149
- await db.run(`INSERT INTO ${table} (${typedColumns.join(', ')})
150
- VALUES (${typedColumns.map(() => '?').join(', ')})
151
- ON CONFLICT(record_hash) DO NOTHING`, ...values);
162
+ return {
163
+ sql: `INSERT INTO ${table} (${typedColumns.join(', ')})
164
+ VALUES (${typedColumns.map(() => '?').join(', ')})
165
+ ON CONFLICT(record_hash) DO NOTHING`,
166
+ params: values,
167
+ };
152
168
  }
153
- else {
154
- // Generic ETL insert path: store as payload_json blob.
155
- if (!_ensuredTables.has(table)) {
156
- await db.exec(ETL_TABLE_DDL(table));
157
- _ensuredTables.add(table);
158
- }
159
- await db.run(`INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
169
+ // Generic ETL insert path: store as payload_json blob.
170
+ return {
171
+ sql: `INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
160
172
  VALUES (?, ?, ?, ?, ?, ?)
161
- ON CONFLICT(record_hash) DO NOTHING`, recordHash, sourceFile, sourceLine, splitIndex, JSON.stringify(payload), timestamp(now));
173
+ ON CONFLICT(record_hash) DO NOTHING`,
174
+ params: [recordHash, sourceFile, sourceLine, splitIndex, JSON.stringify(payload), timestamp(now)],
175
+ };
176
+ }
177
+ /** INSERT op for one ledger row (task 0060 F9). */
178
+ export function ledgerInsertOp(recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, now) {
179
+ return {
180
+ sql: `INSERT INTO history_import_ledger (record_hash, source, source_file, source_line, split_index, target_table, imported_at)
181
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
182
+ params: [recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, timestamp(now)],
183
+ };
184
+ }
185
+ /** UPSERT op for one checkpoint row (task 0060 F9: checkpoint joins the record batch). */
186
+ export function checkpointUpsertOp(source, sourceFile, line, now) {
187
+ return {
188
+ sql: `INSERT INTO history_import_checkpoint (source, source_file, last_imported_line, updated_at)
189
+ VALUES (?, ?, ?, ?)
190
+ ON CONFLICT(source, source_file) DO UPDATE SET
191
+ last_imported_line = excluded.last_imported_line,
192
+ updated_at = excluded.updated_at`,
193
+ params: [source, sourceFile, line, timestamp(now)],
194
+ };
195
+ }
196
+ /**
197
+ * Query which of the given hashes already exist in the ledger, in chunks of at
198
+ * most 200 per `IN (...)` (task 0060 F9 — replaces the per-record SELECT loop).
199
+ */
200
+ export async function ledgerExistingHashes(db, hashes) {
201
+ const existing = new Set();
202
+ for (let i = 0; i < hashes.length; i += 200) {
203
+ const chunk = hashes.slice(i, i + 200);
204
+ const placeholders = chunk.map(() => '?').join(', ');
205
+ const rows = await db.queryAll(`SELECT record_hash FROM history_import_ledger WHERE record_hash IN (${placeholders})`, ...chunk);
206
+ for (const row of rows)
207
+ existing.add(row.record_hash);
208
+ }
209
+ return existing;
210
+ }
211
+ async function insertRecord(db, targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now) {
212
+ const op = recordInsertOp(targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now);
213
+ if (TYPED_TABLE_COLUMNS[targetTableFor(targetTable)] === undefined) {
214
+ // Custom ETL tables are created on demand; `CREATE TABLE IF NOT EXISTS` is idempotent,
215
+ // so there is no process-global cache to go stale across adapter instances (task 0060 F8).
216
+ await db.exec(ETL_TABLE_DDL(targetTableFor(targetTable)));
162
217
  }
218
+ await db.run(op.sql, ...op.params);
163
219
  }
164
220
  async function insertLedger(db, recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, now) {
165
221
  await db.run(`INSERT INTO history_import_ledger (record_hash, source, source_file, source_line, split_index, target_table, imported_at)
@@ -1 +1 @@
1
- {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEzD,qFAAqF;AACrF,eAAO,MAAM,uBAAuB,EAAE,SAAS,aAAa,EAgB3D,CAAC;AAEF,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,GAAE,SAAS,aAAa,EAA4B,GAAG,OAAO,CAa9G;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,OAAO,SAAS,UAAU,EACnD,MAAM,EAAE,OAAO,EACf,KAAK,GAAE,SAAS,aAAa,EAA4B,GAC1D,OAAO,CAET"}
1
+ {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEzD,qFAAqF;AACrF,eAAO,MAAM,uBAAuB,EAAE,SAAS,aAAa,EA+B3D,CAAC;AAEF,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,GAAE,SAAS,aAAa,EAA4B,GAAG,OAAO,CAa9G;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,OAAO,SAAS,UAAU,EACnD,MAAM,EAAE,OAAO,EACf,KAAK,GAAE,SAAS,aAAa,EAA4B,GAC1D,OAAO,CAET"}
package/dist/redaction.js CHANGED
@@ -15,6 +15,21 @@ export const DEFAULT_REDACTION_RULES = [
15
15
  pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
16
16
  replacement: '[REDACTED:email]',
17
17
  },
18
+ {
19
+ name: 'xai-key',
20
+ pattern: /\bxai-[A-Za-z0-9_-]{12,}\b/g,
21
+ replacement: '[REDACTED:token]',
22
+ },
23
+ {
24
+ name: 'aws-access-key-id',
25
+ pattern: /\bAKIA[0-9A-Z]{16}\b/g,
26
+ replacement: '[REDACTED:aws-key]',
27
+ },
28
+ {
29
+ name: 'bearer-token',
30
+ pattern: /\bBearer\s+[A-Za-z0-9._\-=]+\b/g,
31
+ replacement: '[REDACTED:bearer]',
32
+ },
18
33
  ];
19
34
  /** Redact supported scalar and composite JSON values recursively. */
20
35
  export function redactValue(value, rules = DEFAULT_REDACTION_RULES) {
@@ -1,3 +1,11 @@
1
- /** DDL string that creates the history import checkpoint, ledger, and per-source ETL tables. */
1
+ /**
2
+ * DDL string that creates the history import checkpoint, ledger, and typed contract tables.
3
+ *
4
+ * WHY: per-source `history_etl_*` blob tables are no longer hard-coded here. They are created
5
+ * dynamically by {@link ensureTargetTables} / {@link ETL_TABLE_DDL} looping every entry of
6
+ * {@link SOURCE_DEFINITIONS}, so adding a new built-in source needs no edit to this file. This
7
+ * single-owner rule closes the R16 drift where `omp` / `grok` / `agy` existed only via import-time
8
+ * DDL while the seven other sources were duplicated in the static string.
9
+ */
2
10
  export declare const HISTORY_IMPORT_SCHEMA_SQL: string;
3
11
  //# sourceMappingURL=schema-sql.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-sql.d.ts","sourceRoot":"","sources":["../src/schema-sql.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,QA0I9B,CAAC"}
1
+ {"version":3,"file":"schema-sql.d.ts","sourceRoot":"","sources":["../src/schema-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,QA2E9B,CAAC"}
@@ -1,4 +1,12 @@
1
- /** DDL string that creates the history import checkpoint, ledger, and per-source ETL tables. */
1
+ /**
2
+ * DDL string that creates the history import checkpoint, ledger, and typed contract tables.
3
+ *
4
+ * WHY: per-source `history_etl_*` blob tables are no longer hard-coded here. They are created
5
+ * dynamically by {@link ensureTargetTables} / {@link ETL_TABLE_DDL} looping every entry of
6
+ * {@link SOURCE_DEFINITIONS}, so adding a new built-in source needs no edit to this file. This
7
+ * single-owner rule closes the R16 drift where `omp` / `grok` / `agy` existed only via import-time
8
+ * DDL while the seven other sources were duplicated in the static string.
9
+ */
2
10
  export const HISTORY_IMPORT_SCHEMA_SQL = `
3
11
  CREATE TABLE IF NOT EXISTS history_import_checkpoint (
4
12
  source TEXT NOT NULL,
@@ -18,69 +26,6 @@ CREATE TABLE IF NOT EXISTS history_import_ledger (
18
26
  imported_at TEXT NOT NULL
19
27
  );
20
28
 
21
- CREATE TABLE IF NOT EXISTS history_etl_pi (
22
- record_hash TEXT PRIMARY KEY,
23
- source_file TEXT NOT NULL,
24
- source_line INTEGER NOT NULL,
25
- split_index INTEGER NOT NULL,
26
- payload_json TEXT NOT NULL,
27
- imported_at TEXT NOT NULL
28
- );
29
-
30
- CREATE TABLE IF NOT EXISTS history_etl_claude (
31
- record_hash TEXT PRIMARY KEY,
32
- source_file TEXT NOT NULL,
33
- source_line INTEGER NOT NULL,
34
- split_index INTEGER NOT NULL,
35
- payload_json TEXT NOT NULL,
36
- imported_at TEXT NOT NULL
37
- );
38
-
39
- CREATE TABLE IF NOT EXISTS history_etl_codex (
40
- record_hash TEXT PRIMARY KEY,
41
- source_file TEXT NOT NULL,
42
- source_line INTEGER NOT NULL,
43
- split_index INTEGER NOT NULL,
44
- payload_json TEXT NOT NULL,
45
- imported_at TEXT NOT NULL
46
- );
47
-
48
- CREATE TABLE IF NOT EXISTS history_etl_gemini (
49
- record_hash TEXT PRIMARY KEY,
50
- source_file TEXT NOT NULL,
51
- source_line INTEGER NOT NULL,
52
- split_index INTEGER NOT NULL,
53
- payload_json TEXT NOT NULL,
54
- imported_at TEXT NOT NULL
55
- );
56
-
57
- CREATE TABLE IF NOT EXISTS history_etl_opencode (
58
- record_hash TEXT PRIMARY KEY,
59
- source_file TEXT NOT NULL,
60
- source_line INTEGER NOT NULL,
61
- split_index INTEGER NOT NULL,
62
- payload_json TEXT NOT NULL,
63
- imported_at TEXT NOT NULL
64
- );
65
-
66
- CREATE TABLE IF NOT EXISTS history_etl_antigravity (
67
- record_hash TEXT PRIMARY KEY,
68
- source_file TEXT NOT NULL,
69
- source_line INTEGER NOT NULL,
70
- split_index INTEGER NOT NULL,
71
- payload_json TEXT NOT NULL,
72
- imported_at TEXT NOT NULL
73
- );
74
-
75
- CREATE TABLE IF NOT EXISTS history_etl_openclaw (
76
- record_hash TEXT PRIMARY KEY,
77
- source_file TEXT NOT NULL,
78
- source_line INTEGER NOT NULL,
79
- split_index INTEGER NOT NULL,
80
- payload_json TEXT NOT NULL,
81
- imported_at TEXT NOT NULL
82
- );
83
-
84
29
  CREATE TABLE IF NOT EXISTS history_message (
85
30
  record_hash TEXT PRIMARY KEY,
86
31
  source TEXT NOT NULL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-llm-jsonl-importer",
3
- "version": "0.4.28",
3
+ "version": "0.4.30",
4
4
  "description": "@gobing-ai/ts-llm-jsonl-importer — Generic JSONL importer for LLM agent history files.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,9 +47,9 @@
47
47
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-llm-jsonl-importer-v<version> && git push --tags' && exit 1"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-db": "^0.4.28",
51
- "@gobing-ai/ts-runtime": "^0.4.28",
52
- "@gobing-ai/ts-utils": "^0.4.28",
50
+ "@gobing-ai/ts-db": "^0.4.30",
51
+ "@gobing-ai/ts-runtime": "^0.4.30",
52
+ "@gobing-ai/ts-utils": "^0.4.30",
53
53
  "zod": "^4.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/importer.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { DbBatchOp } from '@gobing-ai/ts-db';
1
2
  import {
2
3
  ambientRuntimePaths,
3
4
  createNodeFileSystem,
@@ -9,15 +10,15 @@ import {
9
10
  import { sha256 } from './hash';
10
11
  import {
11
12
  applyHistoryImportSchema,
13
+ checkpointUpsertOp,
12
14
  ensureTargetTables,
13
- insertLedger,
14
- insertRecord,
15
- ledgerExists,
15
+ ledgerExistingHashes,
16
+ ledgerInsertOp,
16
17
  readCheckpoint,
17
18
  reconcileFullImport,
19
+ recordInsertOp,
18
20
  resetCheckpoints,
19
21
  targetTableFor,
20
- writeCheckpoint,
21
22
  } from './jsonl-importer-dao';
22
23
  import { redactRecord } from './redaction';
23
24
  import { resolveSourceDefinition } from './sources';
@@ -130,10 +131,6 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
130
131
  record: redacted,
131
132
  });
132
133
  desiredHashes.add(recordHash);
133
- if (await ledgerExists(options.db, recordHash)) {
134
- skippedDuplicates += 1;
135
- continue;
136
- }
137
134
  prepared.push({ split, splitIndex, normalized: redacted, recordHash });
138
135
  }
139
136
 
@@ -141,9 +138,24 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
141
138
  // second pass, no checkpoint advance, no partial rows from this record.
142
139
  if (lineRejected) continue;
143
140
 
144
- // Second pass: resolve _messageSplitIndex → message_hash, then insert.
141
+ // Chunked ledger lookup (task 0060 F9): one `IN (...)` query per ≤200 hashes
142
+ // instead of a SELECT per record; duplicates are dropped before the write pass.
143
+ const existing = await ledgerExistingHashes(
144
+ options.db,
145
+ prepared.map((e) => e.recordHash),
146
+ );
147
+ const accepted = prepared.filter((entry) => {
148
+ if (existing.has(entry.recordHash)) {
149
+ skippedDuplicates += 1;
150
+ return false;
151
+ }
152
+ return true;
153
+ });
154
+
155
+ // Second pass: resolve _messageSplitIndex → message_hash, then batch.
145
156
  const recordHashBySplitIndex = new Map(prepared.map((e) => [e.splitIndex, e.recordHash] as const));
146
- for (const entry of prepared) {
157
+ const ops: DbBatchOp[] = [];
158
+ for (const entry of accepted) {
147
159
  const { split, splitIndex, normalized, recordHash } = entry;
148
160
  if (normalized.disposition === 'unknown') {
149
161
  unknownRecords += 1;
@@ -158,33 +170,42 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
158
170
  }
159
171
  }
160
172
  if (!options.dryRun) {
161
- await insertRecord(
162
- options.db,
163
- split.targetTable,
164
- recordHash,
165
- file,
166
- lineNumber,
167
- splitIndex,
168
- normalized,
169
- options.now,
173
+ // Record row + ledger row join one batch op pair — a crash between the two
174
+ // writes is impossible (task 0060 F9).
175
+ ops.push(
176
+ recordInsertOp(
177
+ split.targetTable,
178
+ recordHash,
179
+ file,
180
+ lineNumber,
181
+ splitIndex,
182
+ normalized,
183
+ options.now,
184
+ ),
170
185
  );
171
- await insertLedger(
172
- options.db,
173
- recordHash,
174
- resolvedSource,
175
- file,
176
- lineNumber,
177
- splitIndex,
178
- split.targetTable,
179
- options.now,
186
+ ops.push(
187
+ ledgerInsertOp(
188
+ recordHash,
189
+ resolvedSource,
190
+ file,
191
+ lineNumber,
192
+ splitIndex,
193
+ split.targetTable,
194
+ options.now,
195
+ ),
180
196
  );
181
197
  }
182
198
  importedRecords += 1;
183
199
  }
184
200
 
185
- if (lineSucceeded && !options.dryRun) {
186
- await writeCheckpoint(options.db, resolvedSource, file, lineNumber, options.now);
187
- checkpointUpdates += 1;
201
+ if (!options.dryRun && lineSucceeded) {
202
+ // Checkpoint upsert joins the same batch so an accepted line can never be
203
+ // re-imported after a mid-batch crash.
204
+ ops.push(checkpointUpsertOp(resolvedSource, file, lineNumber, options.now));
205
+ if (ops.length > 0) {
206
+ await options.db.batch(ops);
207
+ checkpointUpdates += 1;
208
+ }
188
209
  }
189
210
  }
190
211
  }
@@ -1,7 +1,7 @@
1
1
  import type { DbAdapter, DbBatchOp } from '@gobing-ai/ts-db';
2
2
  import { HistoryImportError } from './errors';
3
3
  import { HISTORY_IMPORT_SCHEMA_SQL } from './schema-sql';
4
- import { VALID_TABLE_NAME } from './sources';
4
+ import { SOURCE_DEFINITIONS, VALID_TABLE_NAME } from './sources';
5
5
  import type { ImportOptions, JsonObject, ReconcileSummary, SourceDefinition } from './types';
6
6
 
7
7
  interface CheckpointRow {
@@ -81,7 +81,15 @@ function ETL_TABLE_DDL(table: string): string {
81
81
  );`;
82
82
  }
83
83
 
84
- /** Apply importer-owned schema to the target database. */
84
+ /**
85
+ * Apply importer-owned schema to the target database.
86
+ *
87
+ * Creates the checkpoint, ledger, and typed contract tables from the static SQL, then materializes
88
+ * every built-in `history_etl_*` table by looping {@link SOURCE_DEFINITIONS} through
89
+ * {@link ensureTargetTables}. This keeps the migration contract "schema applied ⇒ all built-in ETL
90
+ * tables exist" for callers who apply the schema and then raw-insert without a prior import.
91
+ * Adding a new built-in source to {@link SOURCE_DEFINITIONS} is now the only edit needed.
92
+ */
85
93
  export async function applyHistoryImportSchema(db: ImportOptions['db']): Promise<void> {
86
94
  for (const statement of HISTORY_IMPORT_SCHEMA_SQL.split(';')) {
87
95
  const sql = statement.trim();
@@ -89,18 +97,22 @@ export async function applyHistoryImportSchema(db: ImportOptions['db']): Promise
89
97
  await db.exec(sql);
90
98
  }
91
99
  }
100
+ for (const definition of Object.values(SOURCE_DEFINITIONS)) {
101
+ await ensureTargetTables(db, definition);
102
+ }
92
103
  }
93
104
 
94
105
  /**
95
106
  * Ensure the ETL table(s) for a source definition exist.
96
107
  *
97
- * WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} only creates the built-in
98
- * `history_etl_*` tables and the typed contract tables. Custom source definitions
99
- * (and built-in definitions with a `splitConfig.targetTable` override) need their
100
- * target table(s) created on demand. The table name is already gated by
108
+ * WHY: the static {@link HISTORY_IMPORT_SCHEMA_SQL} creates only the checkpoint, ledger, and
109
+ * typed contract tables. Every `history_etl_*` blob table — built-in and custom — is created here
110
+ * (and by {@link applyHistoryImportSchema} looping {@link SOURCE_DEFINITIONS}). Built-in definitions
111
+ * with a `splitConfig.targetTable` override also need their target table(s) created on demand. The
112
+ * table name is already gated by
101
113
  * {@link VALID_TABLE_NAME} in {@link validateSourceDefinition} / {@link targetTableFor},
102
- * so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` makes this
103
- * idempotent for built-in tables that the static schema already created.
114
+ * so it is safe to interpolate into DDL. `CREATE TABLE IF NOT EXISTS` is
115
+ * idempotent, so apply-then-import and a second apply are both safe.
104
116
  */
105
117
  async function ensureTargetTables(db: ImportOptions['db'], definition: SourceDefinition): Promise<void> {
106
118
  const tables = new Set<string>([targetTableFor(definition.targetTable)]);
@@ -157,10 +169,13 @@ async function ledgerExists(db: ImportOptions['db'], recordHash: string): Promis
157
169
  return row !== undefined && row !== null;
158
170
  }
159
171
 
160
- const _ensuredTables = new Set<string>();
161
-
162
- async function insertRecord(
163
- db: ImportOptions['db'],
172
+ /**
173
+ * INSERT op for one accepted record row (task 0060 F9). Pure builder so the
174
+ * per-record batch path and the single-write `insertRecord` can never drift.
175
+ * Typed tables validate unknown keys here; the caller must have ensured the
176
+ * target table exists (`ensureTargetTables` runs at import start).
177
+ */
178
+ export function recordInsertOp(
164
179
  targetTable: string,
165
180
  recordHash: string,
166
181
  sourceFile: string,
@@ -168,7 +183,7 @@ async function insertRecord(
168
183
  splitIndex: number,
169
184
  payload: JsonObject,
170
185
  now: ImportOptions['now'],
171
- ): Promise<void> {
186
+ ): DbBatchOp {
172
187
  const table = targetTableFor(targetTable);
173
188
  const typedColumns = TYPED_TABLE_COLUMNS[table];
174
189
  if (typedColumns !== undefined) {
@@ -189,34 +204,91 @@ async function insertRecord(
189
204
  const v = payload[col];
190
205
  return v === undefined ? null : v;
191
206
  });
192
- if (!_ensuredTables.has(table)) {
193
- // Typed tables are created by the static schema in applyHistoryImportSchema.
194
- _ensuredTables.add(table);
195
- }
196
- await db.run(
197
- `INSERT INTO ${table} (${typedColumns.join(', ')})
198
- VALUES (${typedColumns.map(() => '?').join(', ')})
199
- ON CONFLICT(record_hash) DO NOTHING`,
200
- ...values,
201
- );
202
- } else {
203
- // Generic ETL insert path: store as payload_json blob.
204
- if (!_ensuredTables.has(table)) {
205
- await db.exec(ETL_TABLE_DDL(table));
206
- _ensuredTables.add(table);
207
- }
208
- await db.run(
209
- `INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
207
+ return {
208
+ sql: `INSERT INTO ${table} (${typedColumns.join(', ')})
209
+ VALUES (${typedColumns.map(() => '?').join(', ')})
210
+ ON CONFLICT(record_hash) DO NOTHING`,
211
+ params: values,
212
+ };
213
+ }
214
+ // Generic ETL insert path: store as payload_json blob.
215
+ return {
216
+ sql: `INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
210
217
  VALUES (?, ?, ?, ?, ?, ?)
211
218
  ON CONFLICT(record_hash) DO NOTHING`,
212
- recordHash,
213
- sourceFile,
214
- sourceLine,
215
- splitIndex,
216
- JSON.stringify(payload),
217
- timestamp(now),
219
+ params: [recordHash, sourceFile, sourceLine, splitIndex, JSON.stringify(payload), timestamp(now)],
220
+ };
221
+ }
222
+
223
+ /** INSERT op for one ledger row (task 0060 F9). */
224
+ export function ledgerInsertOp(
225
+ recordHash: string,
226
+ source: string,
227
+ sourceFile: string,
228
+ sourceLine: number,
229
+ splitIndex: number,
230
+ targetTable: string,
231
+ now: ImportOptions['now'],
232
+ ): DbBatchOp {
233
+ return {
234
+ sql: `INSERT INTO history_import_ledger (record_hash, source, source_file, source_line, split_index, target_table, imported_at)
235
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
236
+ params: [recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, timestamp(now)],
237
+ };
238
+ }
239
+
240
+ /** UPSERT op for one checkpoint row (task 0060 F9: checkpoint joins the record batch). */
241
+ export function checkpointUpsertOp(
242
+ source: string,
243
+ sourceFile: string,
244
+ line: number,
245
+ now: ImportOptions['now'],
246
+ ): DbBatchOp {
247
+ return {
248
+ sql: `INSERT INTO history_import_checkpoint (source, source_file, last_imported_line, updated_at)
249
+ VALUES (?, ?, ?, ?)
250
+ ON CONFLICT(source, source_file) DO UPDATE SET
251
+ last_imported_line = excluded.last_imported_line,
252
+ updated_at = excluded.updated_at`,
253
+ params: [source, sourceFile, line, timestamp(now)],
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Query which of the given hashes already exist in the ledger, in chunks of at
259
+ * most 200 per `IN (...)` (task 0060 F9 — replaces the per-record SELECT loop).
260
+ */
261
+ export async function ledgerExistingHashes(db: ImportOptions['db'], hashes: readonly string[]): Promise<Set<string>> {
262
+ const existing = new Set<string>();
263
+ for (let i = 0; i < hashes.length; i += 200) {
264
+ const chunk = hashes.slice(i, i + 200);
265
+ const placeholders = chunk.map(() => '?').join(', ');
266
+ const rows = await db.queryAll<{ record_hash: string }>(
267
+ `SELECT record_hash FROM history_import_ledger WHERE record_hash IN (${placeholders})`,
268
+ ...chunk,
218
269
  );
270
+ for (const row of rows) existing.add(row.record_hash);
271
+ }
272
+ return existing;
273
+ }
274
+
275
+ async function insertRecord(
276
+ db: ImportOptions['db'],
277
+ targetTable: string,
278
+ recordHash: string,
279
+ sourceFile: string,
280
+ sourceLine: number,
281
+ splitIndex: number,
282
+ payload: JsonObject,
283
+ now: ImportOptions['now'],
284
+ ): Promise<void> {
285
+ const op = recordInsertOp(targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now);
286
+ if (TYPED_TABLE_COLUMNS[targetTableFor(targetTable)] === undefined) {
287
+ // Custom ETL tables are created on demand; `CREATE TABLE IF NOT EXISTS` is idempotent,
288
+ // so there is no process-global cache to go stale across adapter instances (task 0060 F8).
289
+ await db.exec(ETL_TABLE_DDL(targetTableFor(targetTable)));
219
290
  }
291
+ await db.run(op.sql, ...op.params);
220
292
  }
221
293
 
222
294
  async function insertLedger(
package/src/redaction.ts CHANGED
@@ -17,6 +17,21 @@ export const DEFAULT_REDACTION_RULES: readonly RedactionRule[] = [
17
17
  pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
18
18
  replacement: '[REDACTED:email]',
19
19
  },
20
+ {
21
+ name: 'xai-key',
22
+ pattern: /\bxai-[A-Za-z0-9_-]{12,}\b/g,
23
+ replacement: '[REDACTED:token]',
24
+ },
25
+ {
26
+ name: 'aws-access-key-id',
27
+ pattern: /\bAKIA[0-9A-Z]{16}\b/g,
28
+ replacement: '[REDACTED:aws-key]',
29
+ },
30
+ {
31
+ name: 'bearer-token',
32
+ pattern: /\bBearer\s+[A-Za-z0-9._\-=]+\b/g,
33
+ replacement: '[REDACTED:bearer]',
34
+ },
20
35
  ];
21
36
 
22
37
  /** Redact supported scalar and composite JSON values recursively. */
package/src/schema-sql.ts CHANGED
@@ -1,4 +1,12 @@
1
- /** DDL string that creates the history import checkpoint, ledger, and per-source ETL tables. */
1
+ /**
2
+ * DDL string that creates the history import checkpoint, ledger, and typed contract tables.
3
+ *
4
+ * WHY: per-source `history_etl_*` blob tables are no longer hard-coded here. They are created
5
+ * dynamically by {@link ensureTargetTables} / {@link ETL_TABLE_DDL} looping every entry of
6
+ * {@link SOURCE_DEFINITIONS}, so adding a new built-in source needs no edit to this file. This
7
+ * single-owner rule closes the R16 drift where `omp` / `grok` / `agy` existed only via import-time
8
+ * DDL while the seven other sources were duplicated in the static string.
9
+ */
2
10
  export const HISTORY_IMPORT_SCHEMA_SQL = `
3
11
  CREATE TABLE IF NOT EXISTS history_import_checkpoint (
4
12
  source TEXT NOT NULL,
@@ -18,69 +26,6 @@ CREATE TABLE IF NOT EXISTS history_import_ledger (
18
26
  imported_at TEXT NOT NULL
19
27
  );
20
28
 
21
- CREATE TABLE IF NOT EXISTS history_etl_pi (
22
- record_hash TEXT PRIMARY KEY,
23
- source_file TEXT NOT NULL,
24
- source_line INTEGER NOT NULL,
25
- split_index INTEGER NOT NULL,
26
- payload_json TEXT NOT NULL,
27
- imported_at TEXT NOT NULL
28
- );
29
-
30
- CREATE TABLE IF NOT EXISTS history_etl_claude (
31
- record_hash TEXT PRIMARY KEY,
32
- source_file TEXT NOT NULL,
33
- source_line INTEGER NOT NULL,
34
- split_index INTEGER NOT NULL,
35
- payload_json TEXT NOT NULL,
36
- imported_at TEXT NOT NULL
37
- );
38
-
39
- CREATE TABLE IF NOT EXISTS history_etl_codex (
40
- record_hash TEXT PRIMARY KEY,
41
- source_file TEXT NOT NULL,
42
- source_line INTEGER NOT NULL,
43
- split_index INTEGER NOT NULL,
44
- payload_json TEXT NOT NULL,
45
- imported_at TEXT NOT NULL
46
- );
47
-
48
- CREATE TABLE IF NOT EXISTS history_etl_gemini (
49
- record_hash TEXT PRIMARY KEY,
50
- source_file TEXT NOT NULL,
51
- source_line INTEGER NOT NULL,
52
- split_index INTEGER NOT NULL,
53
- payload_json TEXT NOT NULL,
54
- imported_at TEXT NOT NULL
55
- );
56
-
57
- CREATE TABLE IF NOT EXISTS history_etl_opencode (
58
- record_hash TEXT PRIMARY KEY,
59
- source_file TEXT NOT NULL,
60
- source_line INTEGER NOT NULL,
61
- split_index INTEGER NOT NULL,
62
- payload_json TEXT NOT NULL,
63
- imported_at TEXT NOT NULL
64
- );
65
-
66
- CREATE TABLE IF NOT EXISTS history_etl_antigravity (
67
- record_hash TEXT PRIMARY KEY,
68
- source_file TEXT NOT NULL,
69
- source_line INTEGER NOT NULL,
70
- split_index INTEGER NOT NULL,
71
- payload_json TEXT NOT NULL,
72
- imported_at TEXT NOT NULL
73
- );
74
-
75
- CREATE TABLE IF NOT EXISTS history_etl_openclaw (
76
- record_hash TEXT PRIMARY KEY,
77
- source_file TEXT NOT NULL,
78
- source_line INTEGER NOT NULL,
79
- split_index INTEGER NOT NULL,
80
- payload_json TEXT NOT NULL,
81
- imported_at TEXT NOT NULL
82
- );
83
-
84
29
  CREATE TABLE IF NOT EXISTS history_message (
85
30
  record_hash TEXT PRIMARY KEY,
86
31
  source TEXT NOT NULL,