@semiont/jobs 0.5.34 → 0.5.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { JobId, UserId, ResourceId, EntityType, JobReferenceAnnotationResult, GenerationJobParams, JobHighlightAnnotationResult, JobAssessmentAnnotationResult, JobCommentAnnotationResult, TagSchema, JobTagAnnotationResult, Logger, EventBus, components, Annotation, SupportedMediaType, GatheredContext } from '@semiont/core';
1
+ import { JobId, UserId, UnitCursor, ResourceId, EntityType, JobReferenceAnnotationResult, GenerationJobParams, JobHighlightAnnotationResult, JobAssessmentAnnotationResult, JobCommentAnnotationResult, TagSchema, JobTagAnnotationResult, Logger, EventBus, components, Annotation, SupportedMediaType, GatheredContext } from '@semiont/core';
2
2
  import { SemiontState } from '@semiont/core/node';
3
3
  import { InferenceClient } from '@semiont/inference';
4
4
 
@@ -47,6 +47,18 @@ interface JobMetadata {
47
47
  * duplicated.
48
48
  */
49
49
  completedUnits?: string[];
50
+ /**
51
+ * The finer grain `completedUnits` cannot express (CHUNK-GRAIN-RESUME P2):
52
+ * how far each UNFINISHED unit got, keyed by unit. Written per committed
53
+ * chunk, so a job that dies mid-unit resumes there rather than at the top —
54
+ * which for a one-unit job (every motivation job, and the 1958 document's
55
+ * single `Person` type) is the difference between resuming and restarting.
56
+ *
57
+ * A unit here is in progress, never complete; the two sets are disjoint by
58
+ * construction in `checkpointUnits`. Merged monotonically per unit, never
59
+ * unioned — see `mergeUnitCursors`.
60
+ */
61
+ unitCursors?: Record<string, UnitCursor>;
50
62
  }
51
63
  /**
52
64
  * Locale conventions for detection/generation params.
@@ -290,7 +302,7 @@ interface JobQueue {
290
302
  * `failureClass` of 'deterministic' goes straight to `failed` with any
291
303
  * budget remaining — a second identical attempt cannot succeed (P3).
292
304
  */
293
- failJob(jobId: JobId, error: string, completedUnits?: string[], failureClass?: 'transient' | 'deterministic'): Promise<'retried' | 'failed' | null>;
305
+ failJob(jobId: JobId, error: string, completedUnits?: string[], failureClass?: 'transient' | 'deterministic', unitCursors?: Record<string, UnitCursor>): Promise<'retried' | 'failed' | null>;
294
306
  /**
295
307
  * Persist a running job's completed-unit checkpoint AT unit completion —
296
308
  * not only when a job fails (JOB-RESTART-SAFETY P2). `failJob` carries the
@@ -300,8 +312,25 @@ interface JobQueue {
300
312
  * file's metadata as each unit lands, unioned with any existing
301
313
  * checkpoint, so recovery resumes rather than restarts. Unthrottled (a
302
314
  * unit completion must never be dropped); a no-op for non-running jobs.
315
+ *
316
+ * `unitCursors` is the finer grain `completedUnits` cannot express
317
+ * (CHUNK-GRAIN-RESUME P2): how far an UNFINISHED unit got, so a job that
318
+ * dies mid-unit resumes there instead of at the top. It is written per
319
+ * committed chunk, not per unit.
320
+ *
321
+ * **The two merge differently, and the difference is the contract.**
322
+ * `completedUnits` is a set, so a union converges under concurrent snapshots
323
+ * — a set only grows. A cursor converges only if the merge is **monotone per
324
+ * unit**: a stale snapshot must never move one backward. `next` and `size`
325
+ * move TOGETHER as one observation; taking `size` from one snapshot and
326
+ * `next` from another would describe a chunk that never existed. A unit that
327
+ * reaches `completedUnits` drops its cursor, so "in progress with a cursor"
328
+ * and "complete" stay structurally exclusive rather than by convention.
329
+ *
330
+ * The rule belongs here rather than to any one driver because it is what
331
+ * makes a cursor safe to merge at all.
303
332
  */
304
- checkpointUnits(jobId: JobId, completedUnits: string[]): Promise<void>;
333
+ checkpointUnits(jobId: JobId, completedUnits: string[], unitCursors?: Record<string, UnitCursor>): Promise<void>;
305
334
  /** Write progress into a running job's file (throttled, best-effort). */
306
335
  recordProgress(jobId: JobId, progress: Record<string, unknown>): Promise<void>;
307
336
  /**
@@ -385,7 +414,7 @@ declare class FsJobQueue implements JobQueue {
385
414
  * re-announced); after that it lands in `failed` with the error.
386
415
  * Returns null (and changes nothing) if the job isn't running.
387
416
  */
388
- failJob(jobId: JobId, error: string, completedUnits?: string[], failureClass?: 'transient' | 'deterministic'): Promise<'retried' | 'failed' | null>;
417
+ failJob(jobId: JobId, error: string, completedUnits?: string[], failureClass?: 'transient' | 'deterministic', unitCursors?: Record<string, UnitCursor>): Promise<'retried' | 'failed' | null>;
389
418
  /**
390
419
  * Persist a running job's completed-unit checkpoint at unit completion
391
420
  * (JOB-RESTART-SAFETY P2). `failJob` writes this checkpoint on a clean
@@ -398,7 +427,7 @@ declare class FsJobQueue implements JobQueue {
398
427
  * non-running jobs. Written directly, like `recordProgress`, so it also
399
428
  * refreshes the mtime heartbeat.
400
429
  */
401
- checkpointUnits(jobId: JobId, completedUnits: string[]): Promise<void>;
430
+ checkpointUnits(jobId: JobId, completedUnits: string[], unitCursors?: Record<string, UnitCursor>): Promise<void>;
402
431
  /**
403
432
  * Write progress into a running job's file. Throttled per job, and
404
433
  * a no-op for jobs that aren't running. Beyond surfacing live
@@ -521,15 +550,40 @@ type Motivation = Annotation['motivation'];
521
550
  interface ProcessorResult<R> {
522
551
  result: R;
523
552
  }
553
+ /**
554
+ * Where one unit stands once the chunk just handed over is durable
555
+ * (CHUNK-GRAIN-RESUME P2).
556
+ *
557
+ * The unit is named HERE rather than in the detection layer, which knows about
558
+ * chunks and nothing about jobs: for `reference-annotation` a unit is an entity
559
+ * type, for `tag-annotation` a category — both loop over several — and for the
560
+ * other three the job runs exactly one unit, its own motivation.
561
+ */
562
+ interface UnitCheckpoint {
563
+ unit: string;
564
+ cursor: UnitCursor;
565
+ }
524
566
  declare function processHighlightJob(content: string, inferenceClient: InferenceClient, params: HighlightDetectionParams, buildAnnotation: BuildAnnotation, onProgress: OnProgress,
525
567
  /** This chunk's novel annotations, awaited: the durability write. */
526
- onChunkComplete: (annotations: Annotation[]) => Promise<void>): Promise<ProcessorResult<JobHighlightAnnotationResult>>;
568
+ onChunkComplete: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
569
+ /** Where earlier attempts left each unit (CHUNK-GRAIN-RESUME P3), keyed the
570
+ * same way the checkpoint is. A unit absent here starts at the top, which is
571
+ * every unit of a first attempt. */
572
+ resumeCursors?: Record<string, UnitCursor>): Promise<ProcessorResult<JobHighlightAnnotationResult>>;
527
573
  declare function processCommentJob(content: string, inferenceClient: InferenceClient, params: CommentDetectionParams, buildAnnotation: BuildAnnotation, onProgress: OnProgress,
528
574
  /** This chunk's novel annotations, awaited: the durability write. */
529
- onChunkComplete: (annotations: Annotation[]) => Promise<void>): Promise<ProcessorResult<JobCommentAnnotationResult>>;
575
+ onChunkComplete: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
576
+ /** Where earlier attempts left each unit (CHUNK-GRAIN-RESUME P3), keyed the
577
+ * same way the checkpoint is. A unit absent here starts at the top, which is
578
+ * every unit of a first attempt. */
579
+ resumeCursors?: Record<string, UnitCursor>): Promise<ProcessorResult<JobCommentAnnotationResult>>;
530
580
  declare function processAssessmentJob(content: string, inferenceClient: InferenceClient, params: AssessmentDetectionParams, buildAnnotation: BuildAnnotation, onProgress: OnProgress,
531
581
  /** This chunk's novel annotations, awaited: the durability write. */
532
- onChunkComplete: (annotations: Annotation[]) => Promise<void>): Promise<ProcessorResult<JobAssessmentAnnotationResult>>;
582
+ onChunkComplete: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
583
+ /** Where earlier attempts left each unit (CHUNK-GRAIN-RESUME P3), keyed the
584
+ * same way the checkpoint is. A unit absent here starts at the top, which is
585
+ * every unit of a first attempt. */
586
+ resumeCursors?: Record<string, UnitCursor>): Promise<ProcessorResult<JobAssessmentAnnotationResult>>;
533
587
  /**
534
588
  * Reference detection commits per UNIT — one entity type — through
535
589
  * `onUnitComplete` (ABANDONED-INFERENCE P2, checkpointed resume): the
@@ -548,12 +602,20 @@ declare function processReferenceJob(content: string, inferenceClient: Inference
548
602
  */
549
603
  onUnitComplete: (entityType: string) => Promise<void>, signal?: AbortSignal,
550
604
  /** This chunk's novel annotations, awaited: the durability write. */
551
- onChunkComplete?: (annotations: Annotation[]) => Promise<void>): Promise<{
605
+ onChunkComplete?: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
606
+ /** Where earlier attempts left each entity-type unit (CHUNK-GRAIN-RESUME P3).
607
+ * A unit absent here starts at the top; units already COMPLETE never reach
608
+ * this function at all, the caller having filtered them out. */
609
+ resumeCursors?: Record<string, UnitCursor>): Promise<{
552
610
  result: JobReferenceAnnotationResult;
553
611
  }>;
554
612
  declare function processTagJob(content: string, inferenceClient: InferenceClient, params: TagDetectionParams, buildAnnotation: BuildAnnotation, onProgress: OnProgress,
555
613
  /** This chunk's novel annotations, awaited: the durability write. */
556
- onChunkComplete: (annotations: Annotation[]) => Promise<void>): Promise<ProcessorResult<JobTagAnnotationResult>>;
614
+ onChunkComplete: (annotations: Annotation[], checkpoint: UnitCheckpoint) => Promise<void>,
615
+ /** Where earlier attempts left each CATEGORY (CHUNK-GRAIN-RESUME P3) — a tag
616
+ * job's units are its categories, not its motivation: each walks the whole
617
+ * document, so one shared cursor would skip text for all but one of them. */
618
+ resumeCursors?: Record<string, UnitCursor>): Promise<ProcessorResult<JobTagAnnotationResult>>;
557
619
  declare function processGenerationJob(inferenceClient: InferenceClient, params: GenerationJobParams, onProgress: OnProgress, logger: Logger): Promise<{
558
620
  content: Uint8Array;
559
621
  title: string;
@@ -562,6 +624,44 @@ declare function processGenerationJob(inferenceClient: InferenceClient, params:
562
624
  result: GenerationResult;
563
625
  }>;
564
626
 
627
+ /**
628
+ * Detection budget derivation — pure window arithmetic over the inference
629
+ * provider's published limits. No hand-tuned chunk constants, no density or
630
+ * yield modeling: document content never enters this arithmetic. The guard
631
+ * for the pathological tail (a chunk whose annotation JSON still overflows
632
+ * the output budget) is `assertNotTruncated` below — invoked per chunk by
633
+ * the callers on every response, not a prediction here.
634
+ *
635
+ * Provider shapes (see `@semiont/inference` interface.ts):
636
+ * - Shared window (Ollama): the provider publishes
637
+ * `maxOutputTokens === contextTokens` — prompt and response share one
638
+ * `num_ctx`. What remains after the prompt scaffold is split
639
+ * input:output = 1:2 (annotation JSON echoes each span plus a fixed
640
+ * key/context envelope, so output needs the larger share). The 1:2 ratio
641
+ * is the plan's one allocation policy — doc-independent, tuned only on
642
+ * live evidence.
643
+ * - Separate ceilings (Anthropic): output takes its full ceiling (duration-
644
+ * capped below), and input follows the same 1:2 allocation — never more
645
+ * than half the output budget. Input does NOT get "the rest of the
646
+ * window": measured 2026-09-02, a window-sized chunk of entity-dense text
647
+ * demands more output than any budget holds, so the model grinds toward
648
+ * max_tokens for minutes (killed at the call bound as a "stall") or
649
+ * collapses to the degenerate []. Large documents chunk; that is the fix,
650
+ * not a cost.
651
+ */
652
+
653
+ /** One chunk handed out by `runAdaptiveChunks`, with the cursor either side of
654
+ * it. `at`/`next` over `totalChars` is exact progress — and, once
655
+ * CHUNK-GRAIN-RESUME lands, the checkpoint identity a variable boundary forces
656
+ * (an ordinal cannot name a chunk whose size is decided while the job runs). */
657
+ /**
658
+ * The half of a `UnitCursor` the DETECTION layer can honestly report: where the
659
+ * walk stands and how it is cutting. The tallies belong to the processor, which
660
+ * owns unit identity and unit counts — this loop counts chunks, not annotations,
661
+ * and a zero it invented would be indistinguishable from a real one.
662
+ */
663
+ type ChunkCursor = Pick<UnitCursor, 'next' | 'size'>;
664
+
565
665
  /**
566
666
  * Response parsers for annotation detection motivations
567
667
  *
@@ -638,9 +738,12 @@ declare class AnnotationDetection {
638
738
  * (source-resource locale). See `types.ts` "Locale conventions" for the
639
739
  * full discussion.
640
740
  */
641
- static detectComments(content: string, client: InferenceClient, instructions?: string, tone?: string, density?: number, language?: string, sourceLanguage?: string, onActivity?: (completedChunks: number, totalChunks: number) => void,
741
+ static detectComments(content: string, client: InferenceClient, instructions?: string, tone?: string, density?: number, language?: string, sourceLanguage?: string, onActivity?: (consumedChars: number, totalChars: number) => void,
642
742
  /** This chunk's matches, as the chunk completes. */
643
- onChunkResults?: (matches: CommentMatch[]) => Promise<void>): Promise<CommentMatch[]>;
743
+ /** Where an earlier attempt left this unit (CHUNK-GRAIN-RESUME P3). */
744
+ resume?: UnitCursor,
745
+ /** This chunk's matches, as the chunk completes. Kept LAST. */
746
+ onChunkResults?: (matches: CommentMatch[], cursor: ChunkCursor) => Promise<void>): Promise<CommentMatch[]>;
644
747
  /**
645
748
  * Detect highlights in content.
646
749
  *
@@ -648,9 +751,12 @@ declare class AnnotationDetection {
648
751
  * applies, used in the prompt so the LLM analyzes non-English source
649
752
  * correctly.
650
753
  */
651
- static detectHighlights(content: string, client: InferenceClient, instructions?: string, density?: number, sourceLanguage?: string, onActivity?: (completedChunks: number, totalChunks: number) => void,
754
+ static detectHighlights(content: string, client: InferenceClient, instructions?: string, density?: number, sourceLanguage?: string, onActivity?: (consumedChars: number, totalChars: number) => void,
652
755
  /** This chunk's matches, as the chunk completes. */
653
- onChunkResults?: (matches: HighlightMatch[]) => Promise<void>): Promise<HighlightMatch[]>;
756
+ /** Where an earlier attempt left this unit (CHUNK-GRAIN-RESUME P3). */
757
+ resume?: UnitCursor,
758
+ /** This chunk's matches, as the chunk completes. Kept LAST. */
759
+ onChunkResults?: (matches: HighlightMatch[], cursor: ChunkCursor) => Promise<void>): Promise<HighlightMatch[]>;
654
760
  /**
655
761
  * Detect assessments in content.
656
762
  *
@@ -658,9 +764,12 @@ declare class AnnotationDetection {
658
764
  * (annotation body locale). `sourceLanguage` is the locale of the content
659
765
  * being analyzed (source-resource locale).
660
766
  */
661
- static detectAssessments(content: string, client: InferenceClient, instructions?: string, tone?: string, density?: number, language?: string, sourceLanguage?: string, onActivity?: (completedChunks: number, totalChunks: number) => void,
767
+ static detectAssessments(content: string, client: InferenceClient, instructions?: string, tone?: string, density?: number, language?: string, sourceLanguage?: string, onActivity?: (consumedChars: number, totalChars: number) => void,
662
768
  /** This chunk's matches, as the chunk completes. */
663
- onChunkResults?: (matches: AssessmentMatch[]) => Promise<void>): Promise<AssessmentMatch[]>;
769
+ /** Where an earlier attempt left this unit (CHUNK-GRAIN-RESUME P3). */
770
+ resume?: UnitCursor,
771
+ /** This chunk's matches, as the chunk completes. Kept LAST. */
772
+ onChunkResults?: (matches: AssessmentMatch[], cursor: ChunkCursor) => Promise<void>): Promise<AssessmentMatch[]>;
664
773
  /**
665
774
  * Detect tags in content for a specific category.
666
775
  *
@@ -673,13 +782,16 @@ declare class AnnotationDetection {
673
782
  * identifiers, not LLM-generated text — so it's consumed at the body-stamp
674
783
  * site, not here.
675
784
  */
676
- static detectTags(content: string, client: InferenceClient, schema: TagSchema, category: string, sourceLanguage?: string, onActivity?: (completedChunks: number, totalChunks: number) => void,
785
+ static detectTags(content: string, client: InferenceClient, schema: TagSchema, category: string, sourceLanguage?: string, onActivity?: (consumedChars: number, totalChars: number) => void,
677
786
  /**
678
787
  * This chunk's matches, ANCHORED before they leave: `parse` here yields
679
788
  * raw tags, so this path runs `validateTagOffsets` per chunk — a per-item
680
789
  * anchor against the full document, so partitioning changes nothing.
681
790
  */
682
- onChunkResults?: (matches: TagMatch[]) => Promise<void>): Promise<TagMatch[]>;
791
+ /** Where an earlier attempt left this unit (CHUNK-GRAIN-RESUME P3). */
792
+ resume?: UnitCursor,
793
+ /** This chunk's matches, as the chunk completes. Kept LAST. */
794
+ onChunkResults?: (matches: TagMatch[], cursor: ChunkCursor) => Promise<void>): Promise<TagMatch[]>;
683
795
  }
684
796
 
685
797
  /**