@gobing-ai/ts-llm-jsonl-importer 0.5.28 → 0.5.29
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/importer.d.ts.map +1 -1
- package/dist/importer.js +54 -15
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +1 -1
- package/package.json +4 -4
- package/src/importer.ts +71 -19
- package/src/schema-sql.ts +1 -1
package/dist/importer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAER,aAAa,EACb,YAAY,EAGZ,gBAAgB,EAEnB,MAAM,SAAS,CAAC;AAkKjB;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAygBrH"}
|
package/dist/importer.js
CHANGED
|
@@ -186,19 +186,37 @@ export async function runJsonlImport(source, options) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
let lineNumber = 0;
|
|
189
|
-
|
|
189
|
+
// Last line number whose producer newline was present. An unterminated final
|
|
190
|
+
// line is a write still in progress — the checkpoint must never advance past
|
|
191
|
+
// it, or the completed record is silently lost on the next incremental run
|
|
192
|
+
// (2026-10-14 17:40 history-refresh failure: a live session's torn tail was
|
|
193
|
+
// recorded as a parse error, degraded the source, and exited the scheduler job 2).
|
|
194
|
+
let terminatedLineNumber = 0;
|
|
195
|
+
// Lines that reached the write phase (parsed, validated, batched — including
|
|
196
|
+
// ledger dedup) are fully persisted atomically with a per-line checkpoint op,
|
|
197
|
+
// so the final stamp must never fall below the last of them even when the
|
|
198
|
+
// producer's trailing newline has not landed yet.
|
|
199
|
+
let lastWritePhaseLineNumber = 0;
|
|
200
|
+
for await (const { text: rawLine, terminated } of readLines(fileSystem, file)) {
|
|
190
201
|
// Cancellation boundary between bounded per-line batches: a batch already in
|
|
191
202
|
// flight settled above (its record + ledger + checkpoint writes commit
|
|
192
203
|
// atomically), so rejecting here never leaves a partial line behind.
|
|
193
204
|
throwIfImportAborted(options.signal);
|
|
194
205
|
lineNumber += 1;
|
|
206
|
+
if (terminated)
|
|
207
|
+
terminatedLineNumber = lineNumber;
|
|
195
208
|
const line = rawLine.trim();
|
|
196
209
|
if (line.length === 0 || lineNumber <= checkpoint)
|
|
197
210
|
continue;
|
|
198
211
|
processedLines += 1;
|
|
199
|
-
|
|
212
|
+
// A torn tail is not corruption: exempt it from parse/validation errors
|
|
213
|
+
// (mirrors the agy 0623 / dsh 0067 skip rulings, generalized to every live
|
|
214
|
+
// producer) and never checkpoint past it — the next run re-reads the line
|
|
215
|
+
// once the producer finishes the append.
|
|
216
|
+
const incompleteTail = !terminated;
|
|
217
|
+
const raw = parseJsonLine(line, file, lineNumber, incompleteTail ? [] : parseErrors, definition.corruptLinePolicy);
|
|
200
218
|
if (raw === undefined) {
|
|
201
|
-
if (definition.corruptLinePolicy === 'skip')
|
|
219
|
+
if (!incompleteTail && definition.corruptLinePolicy === 'skip')
|
|
202
220
|
skippedCorruptLines += 1;
|
|
203
221
|
continue;
|
|
204
222
|
}
|
|
@@ -228,11 +246,13 @@ export async function runJsonlImport(source, options) {
|
|
|
228
246
|
});
|
|
229
247
|
const parsed = definition.schema.safeParse(normalized);
|
|
230
248
|
if (!parsed.success) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
249
|
+
if (!incompleteTail) {
|
|
250
|
+
validationErrors.push({
|
|
251
|
+
sourceFile: file,
|
|
252
|
+
sourceLine: lineNumber,
|
|
253
|
+
reason: parsed.error.issues.map((issue) => issue.message).join('; '),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
236
256
|
lineRejected = true;
|
|
237
257
|
continue;
|
|
238
258
|
}
|
|
@@ -252,6 +272,7 @@ export async function runJsonlImport(source, options) {
|
|
|
252
272
|
// second pass, no checkpoint advance, no partial rows from this record.
|
|
253
273
|
if (lineRejected)
|
|
254
274
|
continue;
|
|
275
|
+
lastWritePhaseLineNumber = lineNumber;
|
|
255
276
|
// E93 task 1028: bounded capability-conflict findings. A conflict is evidence,
|
|
256
277
|
// not a rejection — the row persists with unknown capability facts and the
|
|
257
278
|
// finding is reported once per distinct reason (hard cap 10 per run).
|
|
@@ -485,7 +506,7 @@ export async function runJsonlImport(source, options) {
|
|
|
485
506
|
(entry.size !== (statAfter?.size ?? null) || entry.mtimeMs !== (statAfter?.mtimeMs ?? null));
|
|
486
507
|
if (statAfter !== null && !identityDegraded && (entry === undefined || identityChanged)) {
|
|
487
508
|
await options.db.batch([
|
|
488
|
-
checkpointUpsertOp(resolvedSource, file, Math.max(
|
|
509
|
+
checkpointUpsertOp(resolvedSource, file, Math.max(terminatedLineNumber, checkpoint, lastWritePhaseLineNumber), options.now, {
|
|
489
510
|
size: statAfter.size,
|
|
490
511
|
mtimeMs: statAfter.mtimeMs,
|
|
491
512
|
}),
|
|
@@ -675,6 +696,24 @@ async function safeStat(fileSystem, file) {
|
|
|
675
696
|
return null;
|
|
676
697
|
}
|
|
677
698
|
}
|
|
699
|
+
/** Split an in-memory payload into terminated/unterminated lines. */
|
|
700
|
+
function* splitTerminated(content) {
|
|
701
|
+
const lines = content.split(/\r?\n/);
|
|
702
|
+
// A trailing separator's final '' is a split artifact, not a line — drop it so
|
|
703
|
+
// line numbers and checkpoints match the streamed branch, which never yields it.
|
|
704
|
+
// A trailing BLANK line (content ends with "\n\n") keeps one real empty entry.
|
|
705
|
+
let allTerminated = false;
|
|
706
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
707
|
+
lines.pop();
|
|
708
|
+
allTerminated = true; // content ended with a newline: every line was terminated
|
|
709
|
+
}
|
|
710
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
711
|
+
const text = lines[i];
|
|
712
|
+
if (text === undefined)
|
|
713
|
+
continue;
|
|
714
|
+
yield { text, terminated: allTerminated || i < lines.length - 1 };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
678
717
|
/**
|
|
679
718
|
* Yield lines from a file, streaming when the FileSystem supports it.
|
|
680
719
|
*
|
|
@@ -682,6 +721,10 @@ async function safeStat(fileSystem, file) {
|
|
|
682
721
|
* once. When `fileSystem.readFileStream` is available, lines are streamed from
|
|
683
722
|
* disk in chunks. Otherwise, falls back to `readFile` + `split` — same behavior
|
|
684
723
|
* as before, preserving parity for stubs like CF Workers.
|
|
724
|
+
*
|
|
725
|
+
* Both branches report exact newline termination (ADR-021 parity): the stream
|
|
726
|
+
* contract yields `{ text, terminated }` and the in-memory split derives it
|
|
727
|
+
* from the trailing separator.
|
|
685
728
|
*/
|
|
686
729
|
async function* readLines(fileSystem, file) {
|
|
687
730
|
if (file.endsWith('.jsonl.zstd')) {
|
|
@@ -689,9 +732,7 @@ async function* readLines(fileSystem, file) {
|
|
|
689
732
|
// system zstd CLI before line parsing; the same errors/checkpointing apply
|
|
690
733
|
// to the decompressed line stream.
|
|
691
734
|
const decompressed = await zstdDecompress(file);
|
|
692
|
-
|
|
693
|
-
yield line;
|
|
694
|
-
}
|
|
735
|
+
yield* splitTerminated(decompressed);
|
|
695
736
|
return;
|
|
696
737
|
}
|
|
697
738
|
if (fileSystem.readFileStream) {
|
|
@@ -699,7 +740,5 @@ async function* readLines(fileSystem, file) {
|
|
|
699
740
|
return;
|
|
700
741
|
}
|
|
701
742
|
const content = await fileSystem.readFile(file);
|
|
702
|
-
|
|
703
|
-
yield line;
|
|
704
|
-
}
|
|
743
|
+
yield* splitTerminated(content);
|
|
705
744
|
}
|
package/dist/schema-sql.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Guaranteed to match the package version; a bump-or-fail test enforces that
|
|
5
5
|
* any modification to HISTORY_IMPORT_SCHEMA_SQL must be accompanied by a version bump.
|
|
6
6
|
*/
|
|
7
|
-
export declare const HISTORY_IMPORT_SCHEMA_VERSION = "0.5.
|
|
7
|
+
export declare const HISTORY_IMPORT_SCHEMA_VERSION = "0.5.29";
|
|
8
8
|
/**
|
|
9
9
|
* DDL string that creates the history import checkpoint, ledger, and typed contract tables.
|
|
10
10
|
*
|
package/dist/schema-sql.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Guaranteed to match the package version; a bump-or-fail test enforces that
|
|
5
5
|
* any modification to HISTORY_IMPORT_SCHEMA_SQL must be accompanied by a version bump.
|
|
6
6
|
*/
|
|
7
|
-
export const HISTORY_IMPORT_SCHEMA_VERSION = '0.5.
|
|
7
|
+
export const HISTORY_IMPORT_SCHEMA_VERSION = '0.5.29';
|
|
8
8
|
/**
|
|
9
9
|
* DDL string that creates the history import checkpoint, ledger, and typed contract tables.
|
|
10
10
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-llm-jsonl-importer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.29",
|
|
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.5.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
52
|
-
"@gobing-ai/ts-utils": "^0.5.
|
|
50
|
+
"@gobing-ai/ts-db": "^0.5.29",
|
|
51
|
+
"@gobing-ai/ts-runtime": "^0.5.29",
|
|
52
|
+
"@gobing-ai/ts-utils": "^0.5.29",
|
|
53
53
|
"zod": "^4.1.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
package/src/importer.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createNodeFileSystem,
|
|
5
5
|
type FileSystem,
|
|
6
6
|
getProcessEnv,
|
|
7
|
+
type ReadStreamLine,
|
|
7
8
|
type RuntimePaths,
|
|
8
9
|
resolvePath,
|
|
9
10
|
walkDir,
|
|
@@ -277,19 +278,43 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
let lineNumber = 0;
|
|
280
|
-
|
|
281
|
+
// Last line number whose producer newline was present. An unterminated final
|
|
282
|
+
// line is a write still in progress — the checkpoint must never advance past
|
|
283
|
+
// it, or the completed record is silently lost on the next incremental run
|
|
284
|
+
// (2026-10-14 17:40 history-refresh failure: a live session's torn tail was
|
|
285
|
+
// recorded as a parse error, degraded the source, and exited the scheduler job 2).
|
|
286
|
+
let terminatedLineNumber = 0;
|
|
287
|
+
// Lines that reached the write phase (parsed, validated, batched — including
|
|
288
|
+
// ledger dedup) are fully persisted atomically with a per-line checkpoint op,
|
|
289
|
+
// so the final stamp must never fall below the last of them even when the
|
|
290
|
+
// producer's trailing newline has not landed yet.
|
|
291
|
+
let lastWritePhaseLineNumber = 0;
|
|
292
|
+
for await (const { text: rawLine, terminated } of readLines(fileSystem, file)) {
|
|
281
293
|
// Cancellation boundary between bounded per-line batches: a batch already in
|
|
282
294
|
// flight settled above (its record + ledger + checkpoint writes commit
|
|
283
295
|
// atomically), so rejecting here never leaves a partial line behind.
|
|
284
296
|
throwIfImportAborted(options.signal);
|
|
285
297
|
lineNumber += 1;
|
|
298
|
+
if (terminated) terminatedLineNumber = lineNumber;
|
|
286
299
|
const line = rawLine.trim();
|
|
287
300
|
if (line.length === 0 || lineNumber <= checkpoint) continue;
|
|
288
301
|
processedLines += 1;
|
|
289
302
|
|
|
290
|
-
|
|
303
|
+
// A torn tail is not corruption: exempt it from parse/validation errors
|
|
304
|
+
// (mirrors the agy 0623 / dsh 0067 skip rulings, generalized to every live
|
|
305
|
+
// producer) and never checkpoint past it — the next run re-reads the line
|
|
306
|
+
// once the producer finishes the append.
|
|
307
|
+
const incompleteTail = !terminated;
|
|
308
|
+
|
|
309
|
+
const raw = parseJsonLine(
|
|
310
|
+
line,
|
|
311
|
+
file,
|
|
312
|
+
lineNumber,
|
|
313
|
+
incompleteTail ? [] : parseErrors,
|
|
314
|
+
definition.corruptLinePolicy,
|
|
315
|
+
);
|
|
291
316
|
if (raw === undefined) {
|
|
292
|
-
if (definition.corruptLinePolicy === 'skip') skippedCorruptLines += 1;
|
|
317
|
+
if (!incompleteTail && definition.corruptLinePolicy === 'skip') skippedCorruptLines += 1;
|
|
293
318
|
continue;
|
|
294
319
|
}
|
|
295
320
|
|
|
@@ -326,11 +351,13 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
326
351
|
});
|
|
327
352
|
const parsed = definition.schema.safeParse(normalized);
|
|
328
353
|
if (!parsed.success) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
354
|
+
if (!incompleteTail) {
|
|
355
|
+
validationErrors.push({
|
|
356
|
+
sourceFile: file,
|
|
357
|
+
sourceLine: lineNumber,
|
|
358
|
+
reason: parsed.error.issues.map((issue) => issue.message).join('; '),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
334
361
|
lineRejected = true;
|
|
335
362
|
continue;
|
|
336
363
|
}
|
|
@@ -351,6 +378,7 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
351
378
|
// A rejected line is reported (validationErrors above) but never persisted — no
|
|
352
379
|
// second pass, no checkpoint advance, no partial rows from this record.
|
|
353
380
|
if (lineRejected) continue;
|
|
381
|
+
lastWritePhaseLineNumber = lineNumber;
|
|
354
382
|
|
|
355
383
|
// E93 task 1028: bounded capability-conflict findings. A conflict is evidence,
|
|
356
384
|
// not a rejection — the row persists with unknown capability facts and the
|
|
@@ -660,10 +688,16 @@ export async function runJsonlImport(source: string | SourceDefinition, options:
|
|
|
660
688
|
(entry.size !== (statAfter?.size ?? null) || entry.mtimeMs !== (statAfter?.mtimeMs ?? null));
|
|
661
689
|
if (statAfter !== null && !identityDegraded && (entry === undefined || identityChanged)) {
|
|
662
690
|
await options.db.batch([
|
|
663
|
-
checkpointUpsertOp(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
691
|
+
checkpointUpsertOp(
|
|
692
|
+
resolvedSource,
|
|
693
|
+
file,
|
|
694
|
+
Math.max(terminatedLineNumber, checkpoint, lastWritePhaseLineNumber),
|
|
695
|
+
options.now,
|
|
696
|
+
{
|
|
697
|
+
size: statAfter.size,
|
|
698
|
+
mtimeMs: statAfter.mtimeMs,
|
|
699
|
+
},
|
|
700
|
+
),
|
|
667
701
|
]);
|
|
668
702
|
checkpointUpdates += 1;
|
|
669
703
|
}
|
|
@@ -874,6 +908,24 @@ async function safeStat(fileSystem: FileSystem, file: string): Promise<{ size: n
|
|
|
874
908
|
}
|
|
875
909
|
}
|
|
876
910
|
|
|
911
|
+
/** Split an in-memory payload into terminated/unterminated lines. */
|
|
912
|
+
function* splitTerminated(content: string): Generator<ReadStreamLine> {
|
|
913
|
+
const lines = content.split(/\r?\n/);
|
|
914
|
+
// A trailing separator's final '' is a split artifact, not a line — drop it so
|
|
915
|
+
// line numbers and checkpoints match the streamed branch, which never yields it.
|
|
916
|
+
// A trailing BLANK line (content ends with "\n\n") keeps one real empty entry.
|
|
917
|
+
let allTerminated = false;
|
|
918
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
919
|
+
lines.pop();
|
|
920
|
+
allTerminated = true; // content ended with a newline: every line was terminated
|
|
921
|
+
}
|
|
922
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
923
|
+
const text = lines[i];
|
|
924
|
+
if (text === undefined) continue;
|
|
925
|
+
yield { text, terminated: allTerminated || i < lines.length - 1 };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
877
929
|
/**
|
|
878
930
|
* Yield lines from a file, streaming when the FileSystem supports it.
|
|
879
931
|
*
|
|
@@ -881,16 +933,18 @@ async function safeStat(fileSystem: FileSystem, file: string): Promise<{ size: n
|
|
|
881
933
|
* once. When `fileSystem.readFileStream` is available, lines are streamed from
|
|
882
934
|
* disk in chunks. Otherwise, falls back to `readFile` + `split` — same behavior
|
|
883
935
|
* as before, preserving parity for stubs like CF Workers.
|
|
936
|
+
*
|
|
937
|
+
* Both branches report exact newline termination (ADR-021 parity): the stream
|
|
938
|
+
* contract yields `{ text, terminated }` and the in-memory split derives it
|
|
939
|
+
* from the trailing separator.
|
|
884
940
|
*/
|
|
885
|
-
async function* readLines(fileSystem: FileSystem, file: string): AsyncGenerator<
|
|
941
|
+
async function* readLines(fileSystem: FileSystem, file: string): AsyncGenerator<ReadStreamLine> {
|
|
886
942
|
if (file.endsWith('.jsonl.zstd')) {
|
|
887
943
|
// task 0067: checksummed Zstandard session logs decompress through the
|
|
888
944
|
// system zstd CLI before line parsing; the same errors/checkpointing apply
|
|
889
945
|
// to the decompressed line stream.
|
|
890
946
|
const decompressed = await zstdDecompress(file);
|
|
891
|
-
|
|
892
|
-
yield line;
|
|
893
|
-
}
|
|
947
|
+
yield* splitTerminated(decompressed);
|
|
894
948
|
return;
|
|
895
949
|
}
|
|
896
950
|
if (fileSystem.readFileStream) {
|
|
@@ -898,7 +952,5 @@ async function* readLines(fileSystem: FileSystem, file: string): AsyncGenerator<
|
|
|
898
952
|
return;
|
|
899
953
|
}
|
|
900
954
|
const content = await fileSystem.readFile(file);
|
|
901
|
-
|
|
902
|
-
yield line;
|
|
903
|
-
}
|
|
955
|
+
yield* splitTerminated(content);
|
|
904
956
|
}
|
package/src/schema-sql.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Guaranteed to match the package version; a bump-or-fail test enforces that
|
|
5
5
|
* any modification to HISTORY_IMPORT_SCHEMA_SQL must be accompanied by a version bump.
|
|
6
6
|
*/
|
|
7
|
-
export const HISTORY_IMPORT_SCHEMA_VERSION = '0.5.
|
|
7
|
+
export const HISTORY_IMPORT_SCHEMA_VERSION = '0.5.29';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* DDL string that creates the history import checkpoint, ledger, and typed contract tables.
|