@semiont/jobs 0.5.32 → 0.5.34

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,4 +1,4 @@
1
- import { replyChannelsFor, createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, isObject, isString, isNumber, busRequest, isArray, resourceId, getPrimaryMediaType, isGenerationJobParams, capabilitiesOf, assembleAnnotation, findClaimSpan, textSourceOf, yieldsGeometryOf, decodeRepresentation, GENERATABLE_MEDIA_TYPES, locate, createFragmentSelector, deriveViews, estimateTokens, chunkText, reconcileSelector, getLocaleEnglishName } from '@semiont/core';
1
+ import { replyChannelsFor, createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, isObject, isString, isNumber, busRequest, isArray, resourceId, getPrimaryMediaType, isGenerationJobParams, capabilitiesOf, assembleAnnotation, findClaimSpan, textSourceOf, yieldsGeometryOf, decodeRepresentation, GENERATABLE_MEDIA_TYPES, BusRequestError, locate, createFragmentSelector, deriveViews, annotationId, estimateTokens, chunkText, reconcileSelector, getLocaleEnglishName } from '@semiont/core';
2
2
  import { archivistContentReads, extractPdfTextLayer, withinByteBudget, MAX_PDF_BYTES } from '@semiont/content';
3
3
  import { withSpan, SpanKind, recordJobOutcome, recordAnchorOutcome, recordDetectionCall } from '@semiont/observability';
4
4
  import { createInferenceClient, StructuredReadError } from '@semiont/inference';
@@ -9476,12 +9476,16 @@ var YieldCollapseError = class extends DeterministicJobError {
9476
9476
  /** What the flagged extraction DID find — every span write-time-verified,
9477
9477
  * so discarding it at the floor would add loss on top of the under-report.
9478
9478
  * Carried on the error because the flag site cannot know whether descent
9479
- * remains possible; the floor is the subdivider's knowledge. */
9480
- constructor(message, salvage = []) {
9479
+ * remains possible; the floor is the subdivider's knowledge. The verdict
9480
+ * rides beside it so an acceptance can report evidence, not a message
9481
+ * string. */
9482
+ constructor(message, salvage, verdict) {
9481
9483
  super(message);
9482
9484
  this.salvage = salvage;
9485
+ this.verdict = verdict;
9483
9486
  }
9484
9487
  salvage;
9488
+ verdict;
9485
9489
  name = "YieldCollapseError";
9486
9490
  };
9487
9491
  function deriveDetectionBudget(limits, scaffoldTokens, typesPerCall) {
@@ -9535,7 +9539,7 @@ function outcomeOf(error) {
9535
9539
  if (error instanceof InferenceTimeoutError) return "timeout";
9536
9540
  return "error";
9537
9541
  }
9538
- async function callChunkSubdividing(label, chunk, chunking, call, logger2) {
9542
+ async function callChunkSubdividing(label, chunk, chunking, call, logger2, onUnderReport, onCounted) {
9539
9543
  async function recorded(piece, depth, reroll) {
9540
9544
  const start = performance.now();
9541
9545
  try {
@@ -9550,6 +9554,7 @@ async function callChunkSubdividing(label, chunk, chunking, call, logger2) {
9550
9554
  outcome: "success",
9551
9555
  ...result.usage ? { inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens } : {}
9552
9556
  });
9557
+ if (result.counted !== void 0) onCounted?.(result.counted);
9553
9558
  return result;
9554
9559
  } catch (error) {
9555
9560
  recordDetectionCall({
@@ -9580,6 +9585,8 @@ async function callChunkSubdividing(label, chunk, chunking, call, logger2) {
9580
9585
  salvaged: error.salvage.length,
9581
9586
  error: error.message
9582
9587
  });
9588
+ onUnderReport?.(error.verdict);
9589
+ onCounted?.(error.verdict.counted);
9583
9590
  return error.salvage;
9584
9591
  }
9585
9592
  if (!truncation(error)) throw error;
@@ -10103,7 +10110,7 @@ function logAnchorMethod(motivation, exact, anchorMethod) {
10103
10110
  }
10104
10111
 
10105
10112
  // src/workers/annotation-detection.ts
10106
- async function detectInChunks(client, content, buildPrompt, motivation, elementSchema, parse, onActivity) {
10113
+ async function detectInChunks(client, content, buildPrompt, motivation, elementSchema, parse, onActivity, onChunkResults) {
10107
10114
  const limits = await client.limits();
10108
10115
  const scaffoldTokens = estimateTokens(buildPrompt(""));
10109
10116
  const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens, 1);
@@ -10123,7 +10130,9 @@ async function detectInChunks(client, content, buildPrompt, motivation, elementS
10123
10130
  assertNotTruncated(response, `${motivation} detection`, i + 1, chunks.length, outputBudget);
10124
10131
  return { items: response.items, ...response.usage ? { usage: response.usage } : {} };
10125
10132
  });
10126
- collected.push(...parse(items));
10133
+ const fromChunk = parse(items);
10134
+ collected.push(...fromChunk);
10135
+ await onChunkResults?.(fromChunk);
10127
10136
  if (i < chunks.length - 1) {
10128
10137
  onActivity?.(i + 1, chunks.length);
10129
10138
  }
@@ -10139,7 +10148,7 @@ var AnnotationDetection = class {
10139
10148
  * (source-resource locale). See `types.ts` "Locale conventions" for the
10140
10149
  * full discussion.
10141
10150
  */
10142
- static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
10151
+ static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onActivity, onChunkResults) {
10143
10152
  return detectInChunks(
10144
10153
  client,
10145
10154
  content,
@@ -10147,7 +10156,8 @@ var AnnotationDetection = class {
10147
10156
  "comment",
10148
10157
  COMMENT_ELEMENT_SCHEMA,
10149
10158
  (items) => MotivationParsers.parseComments(items, content),
10150
- onActivity
10159
+ onActivity,
10160
+ onChunkResults
10151
10161
  );
10152
10162
  }
10153
10163
  /**
@@ -10157,7 +10167,7 @@ var AnnotationDetection = class {
10157
10167
  * applies, used in the prompt so the LLM analyzes non-English source
10158
10168
  * correctly.
10159
10169
  */
10160
- static async detectHighlights(content, client, instructions, density, sourceLanguage, onActivity) {
10170
+ static async detectHighlights(content, client, instructions, density, sourceLanguage, onActivity, onChunkResults) {
10161
10171
  return detectInChunks(
10162
10172
  client,
10163
10173
  content,
@@ -10165,7 +10175,8 @@ var AnnotationDetection = class {
10165
10175
  "highlight",
10166
10176
  HIGHLIGHT_ELEMENT_SCHEMA,
10167
10177
  (items) => MotivationParsers.parseHighlights(items, content),
10168
- onActivity
10178
+ onActivity,
10179
+ onChunkResults
10169
10180
  );
10170
10181
  }
10171
10182
  /**
@@ -10175,7 +10186,7 @@ var AnnotationDetection = class {
10175
10186
  * (annotation body locale). `sourceLanguage` is the locale of the content
10176
10187
  * being analyzed (source-resource locale).
10177
10188
  */
10178
- static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
10189
+ static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onActivity, onChunkResults) {
10179
10190
  return detectInChunks(
10180
10191
  client,
10181
10192
  content,
@@ -10183,7 +10194,8 @@ var AnnotationDetection = class {
10183
10194
  "assessment",
10184
10195
  ASSESSMENT_ELEMENT_SCHEMA,
10185
10196
  (items) => MotivationParsers.parseAssessments(items, content),
10186
- onActivity
10197
+ onActivity,
10198
+ onChunkResults
10187
10199
  );
10188
10200
  }
10189
10201
  /**
@@ -10198,7 +10210,7 @@ var AnnotationDetection = class {
10198
10210
  * identifiers, not LLM-generated text — so it's consumed at the body-stamp
10199
10211
  * site, not here.
10200
10212
  */
10201
- static async detectTags(content, client, schema, category, sourceLanguage, onActivity) {
10213
+ static async detectTags(content, client, schema, category, sourceLanguage, onActivity, onChunkResults) {
10202
10214
  const categoryInfo = schema.tags.find((t) => t.name === category);
10203
10215
  if (!categoryInfo) {
10204
10216
  throw new Error(`Invalid category "${category}" for schema ${schema.id}`);
@@ -10219,7 +10231,8 @@ var AnnotationDetection = class {
10219
10231
  "tag",
10220
10232
  TAG_ELEMENT_SCHEMA,
10221
10233
  (items) => MotivationParsers.parseTags(items),
10222
- onActivity
10234
+ onActivity,
10235
+ onChunkResults ? async (raw) => onChunkResults(MotivationParsers.validateTagOffsets(raw, content, category)) : void 0
10223
10236
  );
10224
10237
  return MotivationParsers.validateTagOffsets(parsedTags, content, category);
10225
10238
  }
@@ -10256,20 +10269,22 @@ ${piece}
10256
10269
  pieceChars: piece.length,
10257
10270
  error: err instanceof Error ? err.message : String(err)
10258
10271
  });
10259
- return;
10272
+ return void 0;
10260
10273
  }
10261
10274
  if (counted === void 0) {
10262
10275
  logger2.warn("Count-verifier answer carried no number \u2014 yield check skipped for this chunk", { pieceChars: piece.length });
10263
- return;
10276
+ return void 0;
10264
10277
  }
10265
10278
  if (items.length * YIELD_COLLAPSE_BAND < counted) {
10266
10279
  throw new YieldCollapseError(
10267
10280
  `Extraction found ${items.length} entities where a count call reports ~${counted} mentions (band \xD7${YIELD_COLLAPSE_BAND}) on a ${piece.length}-char chunk \u2014 silent yield collapse (F7): deterministic \u2014 a same-size retry returns the identical under-report.`,
10268
- [...items]
10281
+ [...items],
10282
+ { found: items.length, counted, pieceChars: piece.length }
10269
10283
  );
10270
10284
  }
10285
+ return counted;
10271
10286
  }
10272
- async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger2, sourceLanguage, onActivity) {
10287
+ async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger2, sourceLanguage, onActivity, onUnderReport, onCounted, onChunkResults) {
10273
10288
  const entityTypesDescription = entityTypes.map((et) => {
10274
10289
  if (typeof et === "string") {
10275
10290
  return et;
@@ -10350,14 +10365,17 @@ Example output:
10350
10365
  items: response.items.length
10351
10366
  });
10352
10367
  assertNotTruncated(response, "Entity extraction", i + 1, chunks.length, outputBudget);
10353
- if (verifyYield) {
10354
- await assertYieldNotCollapsed(client, piece, response.items, entityTypesDescription, logger2);
10355
- }
10356
- return { items: response.items, ...response.usage ? { usage: response.usage } : {} };
10357
- }, logger2);
10368
+ const counted = verifyYield ? await assertYieldNotCollapsed(client, piece, response.items, entityTypesDescription, logger2) : void 0;
10369
+ return {
10370
+ items: response.items,
10371
+ ...response.usage ? { usage: response.usage } : {},
10372
+ ...counted !== void 0 ? { counted } : {}
10373
+ };
10374
+ }, logger2, onUnderReport, onCounted);
10375
+ const fromChunk = [];
10358
10376
  for (const e of items) {
10359
10377
  if (isObject(e) && isString(e.exact) && isString(e.entityType)) {
10360
- collected.push({
10378
+ fromChunk.push({
10361
10379
  exact: e.exact,
10362
10380
  entityType: e.entityType,
10363
10381
  ...isString(e.prefix) ? { prefix: e.prefix } : {},
@@ -10367,6 +10385,8 @@ Example output:
10367
10385
  logger2.debug("Dropped malformed LLM entity", { entity: e });
10368
10386
  }
10369
10387
  }
10388
+ collected.push(...fromChunk);
10389
+ await onChunkResults?.(fromChunk);
10370
10390
  if (i < chunks.length - 1) {
10371
10391
  onActivity?.(i + 1, chunks.length);
10372
10392
  }
@@ -10725,16 +10745,18 @@ function annotationDedupeKey(ann) {
10725
10745
  }
10726
10746
  return [ann.motivation, anchor, JSON.stringify(ann.body ?? null)].join("|");
10727
10747
  }
10728
- function dedupeAnnotations(annotations) {
10748
+ function makeSpanDeduper() {
10729
10749
  const seen = /* @__PURE__ */ new Set();
10730
- const out = [];
10731
- for (const ann of annotations) {
10732
- const key = annotationDedupeKey(ann);
10733
- if (seen.has(key)) continue;
10734
- seen.add(key);
10735
- out.push(ann);
10736
- }
10737
- return out;
10750
+ return (annotations) => {
10751
+ const out = [];
10752
+ for (const ann of annotations) {
10753
+ const key = annotationDedupeKey(ann);
10754
+ if (seen.has(key)) continue;
10755
+ seen.add(key);
10756
+ out.push(ann);
10757
+ }
10758
+ return out;
10759
+ };
10738
10760
  }
10739
10761
  function buildTextAnnotation(content, resourceId, userId, generator, motivation, match, body) {
10740
10762
  if (content.substring(match.start, match.end) !== match.exact) {
@@ -10792,7 +10814,12 @@ function buildPdfAnnotation(anchored, resourceId, userId, generator, motivation,
10792
10814
  Math.max(...overlap.map((i) => i.end))
10793
10815
  ) : "";
10794
10816
  const normalize = (s) => s.replace(/\s+/g, " ").trim();
10795
- if (rects.length === 0 || !normalize(coveredText).includes(normalize(match.exact))) {
10817
+ if (rects.length === 0) {
10818
+ throw new Error(
10819
+ `buildPdfAnnotation invariant: no rects located for offsets ${match.start}-${match.end} for resource ${resourceId}, motivation ${motivation}`
10820
+ );
10821
+ }
10822
+ if (!normalize(coveredText).includes(normalize(match.exact))) {
10796
10823
  throw new Error(
10797
10824
  `buildPdfAnnotation invariant: covered text does not contain exact for resource ${resourceId}, motivation ${motivation}`
10798
10825
  );
@@ -10828,27 +10855,32 @@ function buildPdfAnnotation(anchored, resourceId, userId, generator, motivation,
10828
10855
  ...body !== void 0 ? { body } : {}
10829
10856
  };
10830
10857
  }
10831
- async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress) {
10858
+ async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
10832
10859
  const echo = detectionEcho(params);
10833
10860
  onProgress(10, { code: "loading" }, echo);
10834
10861
  onProgress(30, { code: "analyzing" }, echo);
10835
- const highlights = await AnnotationDetection.detectHighlights(
10862
+ const dedupe = makeSpanDeduper();
10863
+ let found = 0;
10864
+ let created = 0;
10865
+ await AnnotationDetection.detectHighlights(
10836
10866
  content,
10837
10867
  inferenceClient,
10838
10868
  params.instructions,
10839
10869
  params.density,
10840
10870
  params.sourceLanguage,
10841
10871
  // Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
10842
- (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
10872
+ (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo),
10873
+ async (matches) => {
10874
+ found += matches.length;
10875
+ const fresh = dedupe(matches.map((h) => buildAnnotation("highlighting", h)));
10876
+ created += fresh.length;
10877
+ onProgress(60, { code: "creating-annotations", count: created }, echo);
10878
+ await onChunkComplete(fresh);
10879
+ }
10843
10880
  );
10844
- onProgress(60, { code: "creating-annotations", count: highlights.length }, echo);
10845
- const annotations = dedupeAnnotations(highlights.map(
10846
- (h) => buildAnnotation("highlighting", h)
10847
- ));
10848
- onProgress(100, { code: "complete-created", count: annotations.length, kind: "highlight" }, echo);
10881
+ onProgress(100, { code: "complete-created", count: created, kind: "highlight" }, echo);
10849
10882
  return {
10850
- annotations,
10851
- result: { kind: "highlight-annotation", highlightsFound: highlights.length, highlightsCreated: annotations.length }
10883
+ result: { kind: "highlight-annotation", highlightsFound: found, highlightsCreated: created }
10852
10884
  };
10853
10885
  }
10854
10886
  function detectionEcho(p) {
@@ -10858,11 +10890,15 @@ function detectionEcho(p) {
10858
10890
  if (p.density !== void 0) requestParams.push({ label: "density", value: String(p.density) });
10859
10891
  return requestParams.length > 0 ? { requestParams } : {};
10860
10892
  }
10861
- async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
10893
+ async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
10862
10894
  const echo = detectionEcho(params);
10863
10895
  onProgress(10, { code: "loading" }, echo);
10864
10896
  onProgress(30, { code: "analyzing" }, echo);
10865
- const comments = await AnnotationDetection.detectComments(
10897
+ const bodyLanguage = params.language ?? "en";
10898
+ const dedupe = makeSpanDeduper();
10899
+ let found = 0;
10900
+ let created = 0;
10901
+ await AnnotationDetection.detectComments(
10866
10902
  content,
10867
10903
  inferenceClient,
10868
10904
  params.instructions,
@@ -10871,31 +10907,38 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
10871
10907
  params.language,
10872
10908
  params.sourceLanguage,
10873
10909
  // Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
10874
- (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
10910
+ (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo),
10911
+ async (comments) => {
10912
+ found += comments.length;
10913
+ const fresh = dedupe(comments.map(
10914
+ (c) => (
10915
+ // Match the pre-#651 CommentAnnotationWorker: include format and
10916
+ // language on the body TextualBody. Optional in the schema, but
10917
+ // consumers that do language-aware rendering rely on them.
10918
+ buildAnnotation("commenting", c, [
10919
+ { type: "TextualBody", value: c.comment, purpose: "commenting", format: "text/plain", language: bodyLanguage }
10920
+ ])
10921
+ )
10922
+ ));
10923
+ created += fresh.length;
10924
+ onProgress(60, { code: "creating-annotations", count: created }, echo);
10925
+ await onChunkComplete(fresh);
10926
+ }
10875
10927
  );
10876
- onProgress(60, { code: "creating-annotations", count: comments.length }, echo);
10877
- const bodyLanguage = params.language ?? "en";
10878
- const annotations = dedupeAnnotations(comments.map(
10879
- (c) => (
10880
- // Match the pre-#651 CommentAnnotationWorker: include format and
10881
- // language on the body TextualBody. Optional in the schema, but
10882
- // consumers that do language-aware rendering rely on them.
10883
- buildAnnotation("commenting", c, [
10884
- { type: "TextualBody", value: c.comment, purpose: "commenting", format: "text/plain", language: bodyLanguage }
10885
- ])
10886
- )
10887
- ));
10888
- onProgress(100, { code: "complete-created", count: annotations.length, kind: "comment" }, echo);
10928
+ onProgress(100, { code: "complete-created", count: created, kind: "comment" }, echo);
10889
10929
  return {
10890
- annotations,
10891
- result: { kind: "comment-annotation", commentsFound: comments.length, commentsCreated: annotations.length }
10930
+ result: { kind: "comment-annotation", commentsFound: found, commentsCreated: created }
10892
10931
  };
10893
10932
  }
10894
- async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
10933
+ async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
10895
10934
  const echo = detectionEcho(params);
10896
10935
  onProgress(10, { code: "loading" }, echo);
10897
10936
  onProgress(30, { code: "analyzing" }, echo);
10898
- const assessments = await AnnotationDetection.detectAssessments(
10937
+ const bodyLanguage = params.language ?? "en";
10938
+ const dedupe = makeSpanDeduper();
10939
+ let found = 0;
10940
+ let created = 0;
10941
+ await AnnotationDetection.detectAssessments(
10899
10942
  content,
10900
10943
  inferenceClient,
10901
10944
  params.instructions,
@@ -10904,40 +10947,45 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
10904
10947
  params.language,
10905
10948
  params.sourceLanguage,
10906
10949
  // Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
10907
- (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
10950
+ (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo),
10951
+ async (assessments) => {
10952
+ found += assessments.length;
10953
+ const fresh = dedupe(assessments.map(
10954
+ (a) => (
10955
+ // Single-object body with purpose aligned to motivation, matching the
10956
+ // pre-#651 AssessmentAnnotationWorker's shape and the majority of
10957
+ // persisted assessments. Do not switch to an array or to
10958
+ // purpose='describing' — that loses the "this is an assessment, not
10959
+ // a description" signal and breaks existing readers that access
10960
+ // `body.value` directly on the object.
10961
+ buildAnnotation("assessing", a, {
10962
+ type: "TextualBody",
10963
+ value: a.assessment,
10964
+ purpose: "assessing",
10965
+ format: "text/plain",
10966
+ language: bodyLanguage
10967
+ })
10968
+ )
10969
+ ));
10970
+ created += fresh.length;
10971
+ onProgress(60, { code: "creating-annotations", count: created }, echo);
10972
+ await onChunkComplete(fresh);
10973
+ }
10908
10974
  );
10909
- onProgress(60, { code: "creating-annotations", count: assessments.length }, echo);
10910
- const bodyLanguage = params.language ?? "en";
10911
- const annotations = dedupeAnnotations(assessments.map(
10912
- (a) => (
10913
- // Single-object body with purpose aligned to motivation, matching the
10914
- // pre-#651 AssessmentAnnotationWorker's shape and the majority of
10915
- // persisted assessments. Do not switch to an array or to
10916
- // purpose='describing' — that loses the "this is an assessment, not
10917
- // a description" signal and breaks existing readers that access
10918
- // `body.value` directly on the object.
10919
- buildAnnotation("assessing", a, {
10920
- type: "TextualBody",
10921
- value: a.assessment,
10922
- purpose: "assessing",
10923
- format: "text/plain",
10924
- language: bodyLanguage
10925
- })
10926
- )
10927
- ));
10928
- onProgress(100, { code: "complete-created", count: annotations.length, kind: "assessment" }, echo);
10975
+ onProgress(100, { code: "complete-created", count: created, kind: "assessment" }, echo);
10929
10976
  return {
10930
- annotations,
10931
- result: { kind: "assessment-annotation", assessmentsFound: assessments.length, assessmentsCreated: annotations.length }
10977
+ result: { kind: "assessment-annotation", assessmentsFound: found, assessmentsCreated: created }
10932
10978
  };
10933
10979
  }
10934
- async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger2, onUnitComplete, signal) {
10980
+ async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger2, onUnitComplete, signal, onChunkComplete) {
10935
10981
  const entityTypeNames = params.entityTypes.map(String);
10936
10982
  const requestParams = [{ label: "entity-types", value: entityTypeNames.join(", ") }];
10937
10983
  const completedItems = [];
10938
10984
  let totalFound = 0;
10939
10985
  let totalEmitted = 0;
10940
10986
  let errors = 0;
10987
+ let totalUnderReportedPieces = 0;
10988
+ let totalExpected = 0;
10941
10989
  onProgress(10, { code: "loading" }, { requestParams });
10942
10990
  const bodyLanguage = params.language ?? "en";
10943
10991
  let completed = 0;
@@ -10949,6 +10997,7 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
10949
10997
  processed: completed,
10950
10998
  total,
10951
10999
  entitiesFound: totalFound,
11000
+ ...totalExpected > 0 ? { entitiesExpected: totalExpected } : {},
10952
11001
  entitiesEmitted: totalEmitted,
10953
11002
  completedItems: [...completedItems],
10954
11003
  requestParams
@@ -10958,7 +11007,14 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
10958
11007
  if (!entityTypeName) return;
10959
11008
  if (signal?.aborted) return;
10960
11009
  emitTypeProgress(entityTypeName);
10961
- const extractedEntities = await extractEntities(
11010
+ const unresolvedBody = [
11011
+ { type: "TextualBody", value: entityTypeName, purpose: "tagging", format: "text/plain", language: bodyLanguage }
11012
+ ];
11013
+ const dedupe = makeSpanDeduper();
11014
+ let unitFound = 0;
11015
+ let unitPersisted = 0;
11016
+ let underReported;
11017
+ await extractEntities(
10962
11018
  content,
10963
11019
  [entityTypeName],
10964
11020
  inferenceClient,
@@ -10970,54 +11026,80 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
10970
11026
  // not silent. It repeats the current position rather than inventing an
10971
11027
  // advance — the stall watchdog, janitor and client timeout need a signal,
10972
11028
  // not a monotone.
10973
- () => emitTypeProgress(entityTypeName)
10974
- );
10975
- const unresolvedBody = [
10976
- { type: "TextualBody", value: entityTypeName, purpose: "tagging", format: "text/plain", language: bodyLanguage }
10977
- ];
10978
- const built = [];
10979
- for (const entity of extractedEntities) {
10980
- const reconciled = reconcileSelector(content, {
10981
- exact: entity.exact,
10982
- ...entity.prefix !== void 0 ? { prefix: entity.prefix } : {},
10983
- ...entity.suffix !== void 0 ? { suffix: entity.suffix } : {}
10984
- });
10985
- if (!reconciled) {
10986
- logger2.error("Entity dropped \u2014 text not found in source", {
10987
- text: entity.exact,
10988
- entityType: entity.entityType
10989
- });
10990
- errors++;
10991
- continue;
11029
+ () => emitTypeProgress(entityTypeName),
11030
+ (verdict) => {
11031
+ underReported = {
11032
+ pieces: (underReported?.pieces ?? 0) + 1,
11033
+ found: (underReported?.found ?? 0) + verdict.found,
11034
+ counted: (underReported?.counted ?? 0) + verdict.counted
11035
+ };
11036
+ },
11037
+ (counted) => {
11038
+ totalExpected += counted;
11039
+ emitTypeProgress(entityTypeName);
11040
+ },
11041
+ async (chunkEntities) => {
11042
+ const built = [];
11043
+ for (const entity of chunkEntities) {
11044
+ const reconciled = reconcileSelector(content, {
11045
+ exact: entity.exact,
11046
+ ...entity.prefix !== void 0 ? { prefix: entity.prefix } : {},
11047
+ ...entity.suffix !== void 0 ? { suffix: entity.suffix } : {}
11048
+ });
11049
+ if (!reconciled) {
11050
+ logger2.error("Entity dropped \u2014 text not found in source", {
11051
+ text: entity.exact,
11052
+ entityType: entity.entityType
11053
+ });
11054
+ errors++;
11055
+ continue;
11056
+ }
11057
+ noteAnchor("reference", entity.exact, reconciled.anchorMethod, logger2);
11058
+ built.push(buildAnnotation("linking", toMatch(reconciled), unresolvedBody));
11059
+ }
11060
+ const fresh = dedupe(built);
11061
+ await onChunkComplete?.(fresh);
11062
+ unitFound += chunkEntities.length;
11063
+ unitPersisted += fresh.length;
11064
+ totalFound += chunkEntities.length;
11065
+ totalEmitted += fresh.length;
11066
+ emitTypeProgress(entityTypeName);
10992
11067
  }
10993
- noteAnchor("reference", entity.exact, reconciled.anchorMethod, logger2);
10994
- const ann = buildAnnotation("linking", toMatch(reconciled), unresolvedBody);
10995
- built.push(ann);
10996
- }
10997
- const unitAnnotations = dedupeAnnotations(built);
10998
- await onUnitComplete(entityTypeName, unitAnnotations);
10999
- totalEmitted += unitAnnotations.length;
11000
- totalFound += extractedEntities.length;
11068
+ );
11069
+ await onUnitComplete(entityTypeName);
11001
11070
  completedItems.push({
11002
11071
  value: entityTypeName,
11003
- foundCount: extractedEntities.length,
11004
- persistedCount: unitAnnotations.length
11072
+ foundCount: unitFound,
11073
+ persistedCount: unitPersisted,
11074
+ ...underReported ? { underReported } : {}
11005
11075
  });
11076
+ if (underReported) totalUnderReportedPieces += underReported.pieces;
11006
11077
  completed++;
11007
11078
  emitTypeProgress(entityTypeName);
11008
11079
  });
11009
11080
  onProgress(100, { code: "complete-created", count: totalEmitted, kind: "reference" }, {
11081
+ ...totalExpected > 0 ? { entitiesExpected: totalExpected } : {},
11010
11082
  completedItems: [...completedItems],
11011
11083
  requestParams
11012
11084
  });
11013
11085
  return {
11014
- result: { kind: "reference-annotation", totalFound, totalEmitted, errors }
11086
+ result: {
11087
+ kind: "reference-annotation",
11088
+ totalFound,
11089
+ totalEmitted,
11090
+ errors,
11091
+ ...totalUnderReportedPieces > 0 ? { underReportedPieces: totalUnderReportedPieces } : {}
11092
+ }
11015
11093
  };
11016
11094
  }
11017
- async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress) {
11095
+ async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
11018
11096
  onProgress(10, { code: "loading" });
11019
11097
  onProgress(30, { code: "analyzing-tags" });
11020
- const allTags = [];
11098
+ const bodyLanguage = params.language ?? "en";
11099
+ const dedupe = makeSpanDeduper();
11100
+ let found = 0;
11101
+ let created = 0;
11102
+ const byCategory = {};
11021
11103
  const completedItems = [];
11022
11104
  for (let c = 0; c < params.categories.length; c++) {
11023
11105
  const category = params.categories[c];
@@ -11032,7 +11114,8 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
11032
11114
  { code: "analyzing-tags" },
11033
11115
  position()
11034
11116
  );
11035
- const categoryTags = await AnnotationDetection.detectTags(
11117
+ let categoryFound = 0;
11118
+ await AnnotationDetection.detectTags(
11036
11119
  content,
11037
11120
  inferenceClient,
11038
11121
  params.schema,
@@ -11044,31 +11127,32 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
11044
11127
  30 + Math.round((c + completed / total) / params.categories.length * 30),
11045
11128
  { code: "analyzing-tags" },
11046
11129
  position()
11047
- )
11130
+ ),
11131
+ async (matches) => {
11132
+ categoryFound += matches.length;
11133
+ const fresh = dedupe(matches.map((t) => {
11134
+ const cat = t.category ?? "unknown";
11135
+ return buildAnnotation("tagging", t, [
11136
+ { type: "TextualBody", value: cat, purpose: "tagging", format: "text/plain", language: bodyLanguage },
11137
+ { type: "TextualBody", value: params.schema.id, purpose: "classifying", format: "text/plain" }
11138
+ ]);
11139
+ }));
11140
+ created += fresh.length;
11141
+ for (const ann of fresh) {
11142
+ const body = ann.body;
11143
+ const cat = Array.isArray(body) && typeof body[0]?.value === "string" ? body[0].value : "unknown";
11144
+ byCategory[cat] = (byCategory[cat] ?? 0) + 1;
11145
+ }
11146
+ onProgress(60, { code: "creating-tag-annotations", count: created });
11147
+ await onChunkComplete(fresh);
11148
+ }
11048
11149
  );
11049
- completedItems.push({ value: category, foundCount: categoryTags.length });
11050
- allTags.push(...categoryTags);
11051
- }
11052
- const tags = allTags;
11053
- onProgress(60, { code: "creating-tag-annotations", count: tags.length });
11054
- const bodyLanguage = params.language ?? "en";
11055
- const annotations = dedupeAnnotations(tags.map((t) => {
11056
- const category = t.category ?? "unknown";
11057
- return buildAnnotation("tagging", t, [
11058
- { type: "TextualBody", value: category, purpose: "tagging", format: "text/plain", language: bodyLanguage },
11059
- { type: "TextualBody", value: params.schema.id, purpose: "classifying", format: "text/plain" }
11060
- ]);
11061
- }));
11062
- const byCategory = {};
11063
- for (const ann of annotations) {
11064
- const body = ann.body;
11065
- const category = Array.isArray(body) && typeof body[0]?.value === "string" ? body[0].value : "unknown";
11066
- byCategory[category] = (byCategory[category] ?? 0) + 1;
11150
+ found += categoryFound;
11151
+ completedItems.push({ value: category, foundCount: categoryFound });
11067
11152
  }
11068
- onProgress(100, { code: "complete-created", count: annotations.length, kind: "tag" });
11153
+ onProgress(100, { code: "complete-created", count: created, kind: "tag" });
11069
11154
  return {
11070
- annotations,
11071
- result: { kind: "tag-annotation", tagsFound: tags.length, tagsCreated: annotations.length, byCategory }
11155
+ result: { kind: "tag-annotation", tagsFound: found, tagsCreated: created, byCategory }
11072
11156
  };
11073
11157
  }
11074
11158
  function assertWithinOutputBudget(byteLength) {
@@ -11269,13 +11353,40 @@ function referenceIdOf(job) {
11269
11353
  }
11270
11354
  var MARK_COMMIT_TIMEOUT_MS = 6e4;
11271
11355
  async function commitAnnotations(session, resourceId, annotations) {
11272
- if (annotations.length === 0) return;
11273
- await busRequest(
11274
- workerBusAsPrimitive(session.client.transport.actor),
11275
- "mark:commit",
11276
- { resourceId, annotations },
11277
- MARK_COMMIT_TIMEOUT_MS
11278
- );
11356
+ if (annotations.length === 0) return void 0;
11357
+ try {
11358
+ await busRequest(
11359
+ workerBusAsPrimitive(session.client.transport.actor),
11360
+ "mark:commit",
11361
+ { resourceId, annotations },
11362
+ MARK_COMMIT_TIMEOUT_MS
11363
+ );
11364
+ return "acknowledged";
11365
+ } catch (error) {
11366
+ if (!(error instanceof BusRequestError) || error.code !== "bus.timeout") throw error;
11367
+ const evidence = await probeDurability(session, resourceId, annotations);
11368
+ if (evidence === "probe-confirmed") return evidence;
11369
+ throw new CommitDurabilityError(error.message, evidence, error);
11370
+ }
11371
+ }
11372
+ var CommitDurabilityError = class extends Error {
11373
+ constructor(message, durability, cause) {
11374
+ super(message, { cause });
11375
+ this.durability = durability;
11376
+ }
11377
+ durability;
11378
+ name = "CommitDurabilityError";
11379
+ };
11380
+ async function probeDurability(session, resourceId$1, annotations) {
11381
+ const last = annotations[annotations.length - 1];
11382
+ if (!last) return "probe-unreachable";
11383
+ try {
11384
+ await session.client.browse.annotation(resourceId(resourceId$1), annotationId(String(last.id))).fresh();
11385
+ return "probe-confirmed";
11386
+ } catch (error) {
11387
+ const code = isObject(error) && isString(error.code) ? error.code : void 0;
11388
+ return code === "bus.rejected" ? "probe-refused" : "probe-unreachable";
11389
+ }
11279
11390
  }
11280
11391
  async function emitEvent(session, channel, payload) {
11281
11392
  await session.client.transport.emit(channel, payload);
@@ -11320,6 +11431,10 @@ function startWorkerProcess(config) {
11320
11431
  error: message,
11321
11432
  ...completedUnits && completedUnits.length > 0 ? { completedUnits } : {},
11322
11433
  ...failureClass !== void 0 ? { failureClass } : {},
11434
+ // What the commit path OBSERVED about durability, when the failure
11435
+ // came from a commit at all. Present only on that path: absent means
11436
+ // the question never arose, never that durability was ruled out.
11437
+ ...error instanceof CommitDurabilityError ? { durability: error.durability } : {},
11323
11438
  // Whether this failure is the END, answered by the same predicate
11324
11439
  // the queue applies at failJob (JOB-RESTART-SAFETY P5). Without
11325
11440
  // it a client cannot tell a recovering run from a dead one: it
@@ -11376,6 +11491,12 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11376
11491
  jobType,
11377
11492
  ...annotationId ? { annotationId } : {}
11378
11493
  };
11494
+ let durability;
11495
+ const record = (evidence) => {
11496
+ if (evidence === void 0) return;
11497
+ if (durability === void 0 || durability === "acknowledged") durability = evidence;
11498
+ };
11499
+ const terminalBase = () => ({ ...lifecycleBase, ...durability ? { durability } : {} });
11379
11500
  await emitEvent(session, "job:start", lifecycleBase);
11380
11501
  if (!config.jobTypes.includes(jobType)) {
11381
11502
  adapter.failJob(jobId, `Worker not configured for job type: ${jobType}`);
@@ -11401,7 +11522,7 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11401
11522
  throw new DeterministicJobError(`Cannot run ${jobType} on resource ${resourceId$1}: anchored-text consult returned '${source.declined}'`);
11402
11523
  }
11403
11524
  await emitEvent(session, "job:complete", {
11404
- ...lifecycleBase,
11525
+ ...terminalBase(),
11405
11526
  result: {
11406
11527
  kind: "declined",
11407
11528
  declined: true,
@@ -11416,7 +11537,7 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11416
11537
  const onProgress = (percentage, message, extra) => {
11417
11538
  adapter.touchActivity();
11418
11539
  emitEvent(session, "job:report-progress", {
11419
- ...lifecycleBase,
11540
+ ...terminalBase(),
11420
11541
  percentage,
11421
11542
  progress: {
11422
11543
  percentage,
@@ -11428,44 +11549,56 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11428
11549
  });
11429
11550
  };
11430
11551
  if (jobType === "highlight-annotation") {
11431
- const { annotations, result } = await processHighlightJob(
11552
+ const { result } = await processHighlightJob(
11432
11553
  ready.text,
11433
11554
  inferenceClient,
11434
11555
  asJobParams(job.params),
11435
11556
  ready.buildAnnotation,
11436
- onProgress
11557
+ onProgress,
11558
+ // The durability write, per chunk, awaited; folds into the terminal
11559
+ // durability evidence like every commit.
11560
+ async (annotations) => {
11561
+ record(await commitAnnotations(session, String(resourceId$1), annotations));
11562
+ }
11437
11563
  );
11438
- await commitAnnotations(session, String(resourceId$1), annotations);
11439
11564
  await emitEvent(session, "job:complete", {
11440
- ...lifecycleBase,
11565
+ ...terminalBase(),
11441
11566
  result
11442
11567
  });
11443
11568
  adapter.completeJob();
11444
11569
  } else if (jobType === "comment-annotation") {
11445
- const { annotations, result } = await processCommentJob(
11570
+ const { result } = await processCommentJob(
11446
11571
  ready.text,
11447
11572
  inferenceClient,
11448
11573
  asJobParams(job.params),
11449
11574
  ready.buildAnnotation,
11450
- onProgress
11575
+ onProgress,
11576
+ // The durability write, per chunk, awaited; folds into the terminal
11577
+ // durability evidence like every commit.
11578
+ async (annotations) => {
11579
+ record(await commitAnnotations(session, String(resourceId$1), annotations));
11580
+ }
11451
11581
  );
11452
- await commitAnnotations(session, String(resourceId$1), annotations);
11453
11582
  await emitEvent(session, "job:complete", {
11454
- ...lifecycleBase,
11583
+ ...terminalBase(),
11455
11584
  result
11456
11585
  });
11457
11586
  adapter.completeJob();
11458
11587
  } else if (jobType === "assessment-annotation") {
11459
- const { annotations, result } = await processAssessmentJob(
11588
+ const { result } = await processAssessmentJob(
11460
11589
  ready.text,
11461
11590
  inferenceClient,
11462
11591
  asJobParams(job.params),
11463
11592
  ready.buildAnnotation,
11464
- onProgress
11593
+ onProgress,
11594
+ // The durability write, per chunk, awaited; folds into the terminal
11595
+ // durability evidence like every commit.
11596
+ async (annotations) => {
11597
+ record(await commitAnnotations(session, String(resourceId$1), annotations));
11598
+ }
11465
11599
  );
11466
- await commitAnnotations(session, String(resourceId$1), annotations);
11467
11600
  await emitEvent(session, "job:complete", {
11468
- ...lifecycleBase,
11601
+ ...terminalBase(),
11469
11602
  result
11470
11603
  });
11471
11604
  adapter.completeJob();
@@ -11485,40 +11618,48 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11485
11618
  ready.buildAnnotation,
11486
11619
  onProgress,
11487
11620
  config.logger,
11488
- async (unit, annotations) => {
11489
- await commitAnnotations(session, String(resourceId$1), annotations);
11621
+ async (unit) => {
11490
11622
  committed.push(unit);
11491
11623
  await emitEvent(session, "job:checkpoint", {
11492
11624
  jobId: job.jobId,
11493
11625
  completedUnits: [...committed]
11494
11626
  });
11495
11627
  },
11496
- signal
11628
+ signal,
11629
+ // The durability write, per chunk, awaited; folds into the terminal
11630
+ // durability evidence like every commit.
11631
+ async (annotations) => {
11632
+ record(await commitAnnotations(session, String(resourceId$1), annotations));
11633
+ }
11497
11634
  );
11498
11635
  if (signal?.aborted) {
11499
11636
  await emitEvent(session, "job:cancel", {
11500
- ...lifecycleBase,
11637
+ ...terminalBase(),
11501
11638
  ...committed.length > 0 ? { completedUnits: [...committed] } : {}
11502
11639
  });
11503
11640
  adapter.completeJob();
11504
11641
  return;
11505
11642
  }
11506
11643
  await emitEvent(session, "job:complete", {
11507
- ...lifecycleBase,
11644
+ ...terminalBase(),
11508
11645
  result
11509
11646
  });
11510
11647
  adapter.completeJob();
11511
11648
  } else if (jobType === "tag-annotation") {
11512
- const { annotations, result } = await processTagJob(
11649
+ const { result } = await processTagJob(
11513
11650
  ready.text,
11514
11651
  inferenceClient,
11515
11652
  asJobParams(job.params),
11516
11653
  ready.buildAnnotation,
11517
- onProgress
11654
+ onProgress,
11655
+ // The durability write, per chunk, awaited; folds into the terminal
11656
+ // durability evidence like every commit.
11657
+ async (annotations) => {
11658
+ record(await commitAnnotations(session, String(resourceId$1), annotations));
11659
+ }
11518
11660
  );
11519
- await commitAnnotations(session, String(resourceId$1), annotations);
11520
11661
  await emitEvent(session, "job:complete", {
11521
- ...lifecycleBase,
11662
+ ...terminalBase(),
11522
11663
  result
11523
11664
  });
11524
11665
  adapter.completeJob();
@@ -11567,7 +11708,7 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11567
11708
  },
11568
11709
  generator
11569
11710
  );
11570
- await commitAnnotations(session, String(resourceId$1), [provenanceRef]);
11711
+ record(await commitAnnotations(session, String(resourceId$1), [provenanceRef]));
11571
11712
  }
11572
11713
  const citationRefs = [];
11573
11714
  if (genResult.format === "application/pdf" && genResult.citations.length > 0) {
@@ -11621,9 +11762,9 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11621
11762
  citationRefs.push(citationRef);
11622
11763
  }
11623
11764
  }
11624
- await commitAnnotations(session, String(newResourceId), citationRefs);
11765
+ record(await commitAnnotations(session, String(newResourceId), citationRefs));
11625
11766
  await emitEvent(session, "job:complete", {
11626
- ...lifecycleBase,
11767
+ ...terminalBase(),
11627
11768
  result: { kind: "generation", resourceId: newResourceId, resourceName: genResult.title, truncated: genResult.result.truncated }
11628
11769
  });
11629
11770
  adapter.completeJob();
@@ -11684,7 +11825,12 @@ var WORKER_AWAITED_OPERATIONS = [
11684
11825
  // P6). The worker AWAITS this one — a unit may not advance until its
11685
11826
  // annotations are in the event log — so its replies must be in the narrow
11686
11827
  // channel set or every commit fails fast with `bus.unsubscribed`.
11687
- "mark:commit"
11828
+ "mark:commit",
11829
+ // The durability probe for a commit whose acknowledgement never routed
11830
+ // (COMMIT-ACK-FALSE-FAILURE F1). SINGULAR by design: the annotation LIST
11831
+ // channel is the multi-MB fan-out this narrowing exists to keep out, and a
11832
+ // rare error path is no reason to let it back in.
11833
+ "browse:annotation-requested"
11688
11834
  ];
11689
11835
  var WORKER_CHANNELS = replyChannelsFor(WORKER_AWAITED_OPERATIONS);
11690
11836
  function parseGatewayUrl(url) {
@@ -11888,8 +12034,9 @@ for (const jobType of ALL_JOB_TYPES) {
11888
12034
  group.jobTypes.push(jobType);
11889
12035
  }
11890
12036
  async function main() {
11891
- const { initObservabilityNode } = await import('@semiont/observability/node');
12037
+ const { initObservabilityNode, registerSupervisorRestartCount } = await import('@semiont/observability/node');
11892
12038
  initObservabilityNode({ serviceName: "semiont-worker" });
12039
+ registerSupervisorRestartCount();
11893
12040
  logger.info("Starting agents", {
11894
12041
  baseUrl: gatewayBaseUrl,
11895
12042
  agents: Array.from(groups.values()).map((g) => ({