@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.
@@ -1 +1 @@
1
- {"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAiCA,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,CAwerH"}
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
- for await (const rawLine of readLines(fileSystem, file)) {
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
- const raw = parseJsonLine(line, file, lineNumber, parseErrors, definition.corruptLinePolicy);
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
- validationErrors.push({
232
- sourceFile: file,
233
- sourceLine: lineNumber,
234
- reason: parsed.error.issues.map((issue) => issue.message).join('; '),
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(lineNumber, checkpoint), options.now, {
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
- for (const line of decompressed.split(/\r?\n/)) {
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
- for (const line of content.split(/\r?\n/)) {
703
- yield line;
704
- }
743
+ yield* splitTerminated(content);
705
744
  }
@@ -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.28";
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
  *
@@ -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.28';
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.28",
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.28",
51
- "@gobing-ai/ts-runtime": "^0.5.28",
52
- "@gobing-ai/ts-utils": "^0.5.28",
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
- for await (const rawLine of readLines(fileSystem, file)) {
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
- const raw = parseJsonLine(line, file, lineNumber, parseErrors, definition.corruptLinePolicy);
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
- validationErrors.push({
330
- sourceFile: file,
331
- sourceLine: lineNumber,
332
- reason: parsed.error.issues.map((issue) => issue.message).join('; '),
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(resolvedSource, file, Math.max(lineNumber, checkpoint), options.now, {
664
- size: statAfter.size,
665
- mtimeMs: statAfter.mtimeMs,
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<string> {
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
- for (const line of decompressed.split(/\r?\n/)) {
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
- for (const line of content.split(/\r?\n/)) {
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.28';
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.