@semiont/jobs 0.5.34 → 0.5.35

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, BusRequestError, locate, createFragmentSelector, deriveViews, annotationId, estimateTokens, chunkText, reconcileSelector, getLocaleEnglishName } from '@semiont/core';
1
+ import { replyChannelsFor, createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, isObject, isString, isNumber, RETRY_RULES, busRequest, isArray, resourceId, getPrimaryMediaType, isGenerationJobParams, capabilitiesOf, assembleAnnotation, findClaimSpan, textSourceOf, yieldsGeometryOf, decodeRepresentation, GENERATABLE_MEDIA_TYPES, BusRequestError, locate, createFragmentSelector, deriveViews, annotationId, estimateTokens, reconcileSelector, getLocaleEnglishName, cutChunk, chunkText } 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';
@@ -9208,31 +9208,22 @@ var require_cjs = __commonJS({
9208
9208
 
9209
9209
  // src/job-claim-adapter.ts
9210
9210
  var import_rxjs = __toESM(require_cjs());
9211
-
9212
- // src/worker-bus-primitive.ts
9213
- function workerBusAsPrimitive(bus) {
9214
- return {
9215
- emit(channel, payload) {
9216
- return bus.emit(channel, payload);
9217
- },
9218
- stream(channel) {
9219
- return bus.on$(channel);
9220
- },
9221
- // Pass the real connection state through: a worker's first `job:claim`
9222
- // fires right after connect — exactly the attach-window emit the gate
9223
- // exists for (.plans/BUS-ATTACH-GATE.md).
9224
- state$: bus.state$,
9225
- // Pass the subscription probe through so a request against a transport
9226
- // whose narrowed channel set omits the replies fails fast
9227
- // (`bus.unsubscribed`) instead of timing out.
9228
- ...bus.isSubscribed ? { isSubscribed: (channel) => bus.isSubscribed(channel) } : {}
9229
- };
9211
+ function readUnitCursors(raw, completedUnits) {
9212
+ if (!isObject(raw)) return {};
9213
+ const done = new Set(completedUnits);
9214
+ const cursors = {};
9215
+ for (const [unit, value] of Object.entries(raw)) {
9216
+ if (done.has(unit) || !isObject(value)) continue;
9217
+ const { next, size, found, emitted } = value;
9218
+ if (!isNumber(next) || !isNumber(size) || next < 0 || size < 1) continue;
9219
+ if (!isNumber(found) || !isNumber(emitted) || found < 0 || emitted < 0) continue;
9220
+ cursors[unit] = { next, size, found, emitted };
9221
+ }
9222
+ return cursors;
9230
9223
  }
9231
-
9232
- // src/job-claim-adapter.ts
9233
9224
  function createJobClaimAdapter(options) {
9234
9225
  const { bus, jobTypes } = options;
9235
- const requestBus = workerBusAsPrimitive(bus);
9226
+ const requestBus = bus;
9236
9227
  const activeJob$ = new import_rxjs.BehaviorSubject(null);
9237
9228
  const isProcessing$ = new import_rxjs.BehaviorSubject(false);
9238
9229
  const jobsCompleted$ = new import_rxjs.BehaviorSubject(0);
@@ -9249,6 +9240,7 @@ function createJobClaimAdapter(options) {
9249
9240
  try {
9250
9241
  const job = await busRequest(requestBus, "job:claim", { jobId: assignment.jobId }, 1e4);
9251
9242
  const completedUnits = isArray(job.metadata?.completedUnits) ? job.metadata.completedUnits.filter(isString) : [];
9243
+ const unitCursors = readUnitCursors(job.metadata?.unitCursors, completedUnits);
9252
9244
  return {
9253
9245
  jobId: assignment.jobId,
9254
9246
  type: assignment.type,
@@ -9256,6 +9248,7 @@ function createJobClaimAdapter(options) {
9256
9248
  userId: job.metadata?.userId ?? "",
9257
9249
  params: job.params ?? {},
9258
9250
  completedUnits,
9251
+ unitCursors,
9259
9252
  // Absent or malformed metadata reads as "no budget left" — a worker
9260
9253
  // that cannot see the budget must not claim a retry is coming.
9261
9254
  retryCount: isNumber(job.metadata?.retryCount) ? job.metadata.retryCount : 0,
@@ -9274,7 +9267,7 @@ function createJobClaimAdapter(options) {
9274
9267
  if (started) return;
9275
9268
  started = true;
9276
9269
  bus.addChannels?.(["job:queued"]);
9277
- jobSubscription = bus.on$("job:queued").subscribe((event) => {
9270
+ jobSubscription = bus.stream("job:queued").subscribe((event) => {
9278
9271
  lastQueuedEventAt = Date.now();
9279
9272
  const jobType = event.jobType;
9280
9273
  if (jobTypes.length > 0 && !jobTypes.includes(jobType)) return;
@@ -9452,16 +9445,39 @@ function classifyFailure(error) {
9452
9445
  if (name === "APIUserAbortError" || name === "AbortError") return "transient";
9453
9446
  const status = isNumber(error.status) ? error.status : void 0;
9454
9447
  if (status !== void 0) {
9455
- if (status === 408 || status === 429 || status >= 500) return "transient";
9448
+ if (RETRY_RULES.job.retryable({ status })) return "transient";
9456
9449
  if (status >= 400) return "deterministic";
9457
9450
  }
9458
9451
  return void 0;
9459
9452
  }
9460
9453
 
9454
+ // src/workers/detection/chunk-size-controller.ts
9455
+ var DEFAULT_CHUNK_SIZING_POLICY = {
9456
+ growBelow: 0.5,
9457
+ shrinkAbove: 0.8,
9458
+ growFactor: 1.5,
9459
+ shrinkFactor: 0.7
9460
+ };
9461
+ function clamp(value, lo, hi) {
9462
+ return Math.max(lo, Math.min(hi, value));
9463
+ }
9464
+ function nextChunkSize(outcome, current, bounds, policy = DEFAULT_CHUNK_SIZING_POLICY) {
9465
+ const grow = () => clamp(Math.floor(current * policy.growFactor), bounds.floor, bounds.ceiling);
9466
+ const shrink = () => clamp(Math.floor(current * policy.shrinkFactor), bounds.floor, bounds.ceiling);
9467
+ const hold = () => clamp(current, bounds.floor, bounds.ceiling);
9468
+ if (outcome.truncated) return shrink();
9469
+ if (outcome.outputTokens === void 0) return hold();
9470
+ if (bounds.outputBudget <= 0) return hold();
9471
+ const utilization = outcome.outputTokens / bounds.outputBudget;
9472
+ if (utilization < policy.growBelow) return grow();
9473
+ if (utilization > policy.shrinkAbove) return shrink();
9474
+ return hold();
9475
+ }
9476
+
9461
9477
  // src/workers/detection/detection-chunking.ts
9462
- function assertNotTruncated(response, label, chunk, totalChunks, outputBudget) {
9478
+ function assertNotTruncated(response, label, at, totalChars, outputBudget) {
9463
9479
  if (response.stopReason === "max_tokens") {
9464
- throw new DeterministicJobError(`${label} response truncated (max_tokens) on chunk ${chunk}/${totalChunks} despite the derived output budget of ${outputBudget} tokens \u2014 failing the job rather than under-reporting annotations.`);
9480
+ throw new DeterministicJobError(`${label} response truncated (max_tokens) at character ${at} of ${totalChars} despite the derived output budget of ${outputBudget} tokens \u2014 failing the job rather than under-reporting annotations.`);
9465
9481
  }
9466
9482
  }
9467
9483
  var SELECTOR_CONTEXT_CHARS = 64;
@@ -9512,6 +9528,7 @@ function deriveDetectionBudget(limits, scaffoldTokens, typesPerCall) {
9512
9528
  outputBudget = durationSafeOutput;
9513
9529
  }
9514
9530
  }
9531
+ const capacityInput = contextTokens - scaffoldTokens - outputBudget;
9515
9532
  inputBudget = Math.min(inputBudget, Math.floor(outputBudget / (2 * typesPerCall)));
9516
9533
  if (inputBudget <= OVERLAP_TOKENS) {
9517
9534
  throw new Error(
@@ -9520,15 +9537,41 @@ function deriveDetectionBudget(limits, scaffoldTokens, typesPerCall) {
9520
9537
  }
9521
9538
  return {
9522
9539
  chunking: { chunkSize: inputBudget, overlap: OVERLAP_TOKENS },
9523
- outputBudget
9540
+ outputBudget,
9541
+ bounds: {
9542
+ // Two overlaps: at the floor a chunk is still half text it has not seen
9543
+ // before, so the cursor keeps making progress rather than re-reading its
9544
+ // own tail. Clamped under the opening size, because a cramped window can
9545
+ // derive an opening below even that — and a floor above the opening would
9546
+ // clamp every proposal upward, silently reversing the sizer.
9547
+ floor: Math.min(inputBudget, 2 * OVERLAP_TOKENS),
9548
+ // Never below the opening: a degenerate window can make the fit
9549
+ // arithmetic smaller than the size already chosen, and a ceiling under
9550
+ // the opening would clamp every proposal DOWN on the first call.
9551
+ ceiling: Math.max(inputBudget, capacityInput),
9552
+ outputBudget
9553
+ }
9524
9554
  };
9525
9555
  }
9556
+ async function runAdaptiveChunks(text, budget, onChunk, resume) {
9557
+ let at = resume?.next ?? 0;
9558
+ let size = resume ? nextChunkSize({ truncated: true }, resume.size, budget.bounds) : budget.chunking.chunkSize;
9559
+ while (at < text.length) {
9560
+ const { piece, next } = cutChunk(text, at, { chunkSize: size, overlap: budget.chunking.overlap });
9561
+ const outcome = await onChunk({ piece, size, at, next, totalChars: text.length });
9562
+ at = next;
9563
+ size = nextChunkSize(outcome, size, budget.bounds);
9564
+ }
9565
+ }
9526
9566
  var MAX_SUBDIVISION_DEPTH = 2;
9527
9567
  function unknownUnreadable(error) {
9528
9568
  return error instanceof StructuredReadError && error.stopReason === "unknown";
9529
9569
  }
9570
+ function unreadableDespiteFinishing(error) {
9571
+ return error instanceof StructuredReadError && error.stopReason === "end_turn";
9572
+ }
9530
9573
  function subdividable(error) {
9531
- return error instanceof InferenceTimeoutError || truncation(error) || unknownUnreadable(error);
9574
+ return error instanceof InferenceTimeoutError || truncation(error) || unknownUnreadable(error) || unreadableDespiteFinishing(error);
9532
9575
  }
9533
9576
  function truncation(error) {
9534
9577
  return error instanceof DeterministicJobError || error instanceof StructuredReadError && error.stopReason === "max_tokens";
@@ -9540,10 +9583,19 @@ function outcomeOf(error) {
9540
9583
  return "error";
9541
9584
  }
9542
9585
  async function callChunkSubdividing(label, chunk, chunking, call, logger2, onUnderReport, onCounted) {
9586
+ let outputTokens = 0;
9587
+ let sizeShaped = false;
9588
+ let callsMade = 0;
9589
+ let callsMeasured = 0;
9543
9590
  async function recorded(piece, depth, reroll) {
9544
9591
  const start = performance.now();
9545
9592
  try {
9546
9593
  const result = await call(piece);
9594
+ callsMade += 1;
9595
+ if (result.usage) {
9596
+ callsMeasured += 1;
9597
+ outputTokens += result.usage.outputTokens;
9598
+ }
9547
9599
  recordDetectionCall({
9548
9600
  label,
9549
9601
  pieceChars: piece.length,
@@ -9574,6 +9626,7 @@ async function callChunkSubdividing(label, chunk, chunking, call, logger2, onUnd
9574
9626
  return (await recorded(piece, depth, false)).items;
9575
9627
  } catch (error) {
9576
9628
  if (!subdividable(error)) throw error;
9629
+ sizeShaped = true;
9577
9630
  const half = Math.floor(chunkSize / 2);
9578
9631
  const pieces = chunkText(piece, { chunkSize: half, overlap: chunking.overlap });
9579
9632
  const shrinks = pieces.length > 1 || pieces[0] !== piece;
@@ -9609,7 +9662,12 @@ async function callChunkSubdividing(label, chunk, chunking, call, logger2, onUnd
9609
9662
  return collected;
9610
9663
  }
9611
9664
  }
9612
- return attempt(chunk, chunking.chunkSize, 0);
9665
+ const items = await attempt(chunk, chunking.chunkSize, 0);
9666
+ const measured = callsMade > 0 && callsMeasured === callsMade;
9667
+ return {
9668
+ items,
9669
+ outcome: { truncated: sizeShaped, ...measured ? { outputTokens } : {} }
9670
+ };
9613
9671
  }
9614
9672
  function languageName(tag) {
9615
9673
  return getLocaleEnglishName(tag) || tag;
@@ -10110,33 +10168,37 @@ function logAnchorMethod(motivation, exact, anchorMethod) {
10110
10168
  }
10111
10169
 
10112
10170
  // src/workers/annotation-detection.ts
10113
- async function detectInChunks(client, content, buildPrompt, motivation, elementSchema, parse, onActivity, onChunkResults) {
10171
+ async function detectInChunks(client, content, buildPrompt, motivation, elementSchema, parse, onActivity, resume, onChunkResults) {
10114
10172
  const limits = await client.limits();
10115
10173
  const scaffoldTokens = estimateTokens(buildPrompt(""));
10116
- const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens, 1);
10117
- const chunks = chunkText(content, chunking);
10174
+ const budget = deriveDetectionBudget(limits, scaffoldTokens, 1);
10175
+ const { outputBudget } = budget;
10118
10176
  const collected = [];
10119
- for (let i = 0; i < chunks.length; i++) {
10120
- const items = await callChunkSubdividing(motivation, chunks[i], chunking, async (piece) => {
10121
- const response = await boundedGenerateStructured(
10122
- client,
10123
- buildPrompt(piece),
10124
- outputBudget,
10125
- DETECTION_TEMPERATURE,
10126
- elementSchema,
10127
- // Still alive, same position (a long single call is otherwise silent).
10128
- () => onActivity?.(i, chunks.length)
10129
- );
10130
- assertNotTruncated(response, `${motivation} detection`, i + 1, chunks.length, outputBudget);
10131
- return { items: response.items, ...response.usage ? { usage: response.usage } : {} };
10132
- });
10177
+ await runAdaptiveChunks(content, budget, async ({ piece: chunk, size, at, next, totalChars }) => {
10178
+ const { items, outcome } = await callChunkSubdividing(
10179
+ motivation,
10180
+ chunk,
10181
+ { chunkSize: size, overlap: budget.chunking.overlap },
10182
+ async (piece) => {
10183
+ const response = await boundedGenerateStructured(
10184
+ client,
10185
+ buildPrompt(piece),
10186
+ outputBudget,
10187
+ DETECTION_TEMPERATURE,
10188
+ elementSchema,
10189
+ // Still alive, same position (a long single call is otherwise silent).
10190
+ () => onActivity?.(at, totalChars)
10191
+ );
10192
+ assertNotTruncated(response, `${motivation} detection`, at, totalChars, outputBudget);
10193
+ return { items: response.items, ...response.usage ? { usage: response.usage } : {} };
10194
+ }
10195
+ );
10133
10196
  const fromChunk = parse(items);
10134
10197
  collected.push(...fromChunk);
10135
- await onChunkResults?.(fromChunk);
10136
- if (i < chunks.length - 1) {
10137
- onActivity?.(i + 1, chunks.length);
10138
- }
10139
- }
10198
+ await onChunkResults?.(fromChunk, { next, size });
10199
+ if (next < totalChars) onActivity?.(next, totalChars);
10200
+ return outcome;
10201
+ }, resume);
10140
10202
  return collected;
10141
10203
  }
10142
10204
  var AnnotationDetection = class {
@@ -10148,7 +10210,7 @@ var AnnotationDetection = class {
10148
10210
  * (source-resource locale). See `types.ts` "Locale conventions" for the
10149
10211
  * full discussion.
10150
10212
  */
10151
- static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onActivity, onChunkResults) {
10213
+ static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onActivity, resume, onChunkResults) {
10152
10214
  return detectInChunks(
10153
10215
  client,
10154
10216
  content,
@@ -10157,6 +10219,7 @@ var AnnotationDetection = class {
10157
10219
  COMMENT_ELEMENT_SCHEMA,
10158
10220
  (items) => MotivationParsers.parseComments(items, content),
10159
10221
  onActivity,
10222
+ resume,
10160
10223
  onChunkResults
10161
10224
  );
10162
10225
  }
@@ -10167,7 +10230,7 @@ var AnnotationDetection = class {
10167
10230
  * applies, used in the prompt so the LLM analyzes non-English source
10168
10231
  * correctly.
10169
10232
  */
10170
- static async detectHighlights(content, client, instructions, density, sourceLanguage, onActivity, onChunkResults) {
10233
+ static async detectHighlights(content, client, instructions, density, sourceLanguage, onActivity, resume, onChunkResults) {
10171
10234
  return detectInChunks(
10172
10235
  client,
10173
10236
  content,
@@ -10176,6 +10239,7 @@ var AnnotationDetection = class {
10176
10239
  HIGHLIGHT_ELEMENT_SCHEMA,
10177
10240
  (items) => MotivationParsers.parseHighlights(items, content),
10178
10241
  onActivity,
10242
+ resume,
10179
10243
  onChunkResults
10180
10244
  );
10181
10245
  }
@@ -10186,7 +10250,7 @@ var AnnotationDetection = class {
10186
10250
  * (annotation body locale). `sourceLanguage` is the locale of the content
10187
10251
  * being analyzed (source-resource locale).
10188
10252
  */
10189
- static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onActivity, onChunkResults) {
10253
+ static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onActivity, resume, onChunkResults) {
10190
10254
  return detectInChunks(
10191
10255
  client,
10192
10256
  content,
@@ -10195,6 +10259,7 @@ var AnnotationDetection = class {
10195
10259
  ASSESSMENT_ELEMENT_SCHEMA,
10196
10260
  (items) => MotivationParsers.parseAssessments(items, content),
10197
10261
  onActivity,
10262
+ resume,
10198
10263
  onChunkResults
10199
10264
  );
10200
10265
  }
@@ -10210,7 +10275,7 @@ var AnnotationDetection = class {
10210
10275
  * identifiers, not LLM-generated text — so it's consumed at the body-stamp
10211
10276
  * site, not here.
10212
10277
  */
10213
- static async detectTags(content, client, schema, category, sourceLanguage, onActivity, onChunkResults) {
10278
+ static async detectTags(content, client, schema, category, sourceLanguage, onActivity, resume, onChunkResults) {
10214
10279
  const categoryInfo = schema.tags.find((t) => t.name === category);
10215
10280
  if (!categoryInfo) {
10216
10281
  throw new Error(`Invalid category "${category}" for schema ${schema.id}`);
@@ -10232,7 +10297,8 @@ var AnnotationDetection = class {
10232
10297
  TAG_ELEMENT_SCHEMA,
10233
10298
  (items) => MotivationParsers.parseTags(items),
10234
10299
  onActivity,
10235
- onChunkResults ? async (raw) => onChunkResults(MotivationParsers.validateTagOffsets(raw, content, category)) : void 0
10300
+ resume,
10301
+ onChunkResults ? async (raw, cursor) => onChunkResults(MotivationParsers.validateTagOffsets(raw, content, category), cursor) : void 0
10236
10302
  );
10237
10303
  return MotivationParsers.validateTagOffsets(parsedTags, content, category);
10238
10304
  }
@@ -10284,7 +10350,7 @@ ${piece}
10284
10350
  }
10285
10351
  return counted;
10286
10352
  }
10287
- async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger2, sourceLanguage, onActivity, onUnderReport, onCounted, onChunkResults) {
10353
+ async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger2, sourceLanguage, onActivity, onUnderReport, onCounted, resume, onChunkResults) {
10288
10354
  const entityTypesDescription = entityTypes.map((et) => {
10289
10355
  if (typeof et === "string") {
10290
10356
  return et;
@@ -10336,42 +10402,55 @@ Example output:
10336
10402
  const limits = await client.limits();
10337
10403
  const verifyYield = client.verifyDetectionYield;
10338
10404
  const scaffoldTokens = estimateTokens(buildPrompt(""));
10339
- const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens, entityTypes.length);
10340
- const chunks = chunkText(exact, chunking);
10405
+ const budget = deriveDetectionBudget(limits, scaffoldTokens, entityTypes.length);
10406
+ const { chunking, outputBudget } = budget;
10341
10407
  logger2.debug("Sending entity extraction request", {
10342
10408
  entityTypes: entityTypesDescription,
10343
- chunks: chunks.length,
10344
- chunkSizeTokens: chunking.chunkSize,
10409
+ chars: exact.length,
10410
+ // The size the run OPENS at, and how far measured yield may move it. The
10411
+ // chunk COUNT is deliberately absent: with sizing decided as the run goes,
10412
+ // there is no honest total until the cursor reaches the end.
10413
+ openingChunkSizeTokens: chunking.chunkSize,
10414
+ ceilingChunkSizeTokens: budget.bounds.ceiling,
10345
10415
  outputBudget
10346
10416
  });
10347
10417
  const collected = [];
10348
- for (let i = 0; i < chunks.length; i++) {
10349
- const items = await callChunkSubdividing("reference", chunks[i], chunking, async (piece) => {
10350
- const response = await boundedGenerateStructured(
10351
- client,
10352
- buildPrompt(piece),
10353
- outputBudget,
10354
- DETECTION_TEMPERATURE,
10355
- ENTITY_ELEMENT_SCHEMA,
10356
- // Still alive, same position: a long single call would otherwise emit
10357
- // nothing at all between start and finish.
10358
- () => onActivity?.(i, chunks.length),
10359
- logger2
10360
- );
10361
- logger2.debug("Got entity extraction response", {
10362
- chunk: i + 1,
10363
- chunks: chunks.length,
10364
- pieceChars: piece.length,
10365
- items: response.items.length
10366
- });
10367
- assertNotTruncated(response, "Entity extraction", i + 1, chunks.length, outputBudget);
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);
10418
+ await runAdaptiveChunks(exact, budget, async ({ piece: chunk, size, at, next, totalChars }) => {
10419
+ const { items, outcome } = await callChunkSubdividing(
10420
+ "reference",
10421
+ chunk,
10422
+ { chunkSize: size, overlap: chunking.overlap },
10423
+ async (piece) => {
10424
+ const response = await boundedGenerateStructured(
10425
+ client,
10426
+ buildPrompt(piece),
10427
+ outputBudget,
10428
+ DETECTION_TEMPERATURE,
10429
+ ENTITY_ELEMENT_SCHEMA,
10430
+ // Still alive, same position: a long single call would otherwise emit
10431
+ // nothing at all between start and finish.
10432
+ () => onActivity?.(at, totalChars),
10433
+ logger2
10434
+ );
10435
+ logger2.debug("Got entity extraction response", {
10436
+ at,
10437
+ totalChars,
10438
+ chunkSizeTokens: size,
10439
+ pieceChars: piece.length,
10440
+ items: response.items.length
10441
+ });
10442
+ assertNotTruncated(response, "Entity extraction", at, totalChars, outputBudget);
10443
+ const counted = verifyYield ? await assertYieldNotCollapsed(client, piece, response.items, entityTypesDescription, logger2) : void 0;
10444
+ return {
10445
+ items: response.items,
10446
+ ...response.usage ? { usage: response.usage } : {},
10447
+ ...counted !== void 0 ? { counted } : {}
10448
+ };
10449
+ },
10450
+ logger2,
10451
+ onUnderReport,
10452
+ onCounted
10453
+ );
10375
10454
  const fromChunk = [];
10376
10455
  for (const e of items) {
10377
10456
  if (isObject(e) && isString(e.exact) && isString(e.entityType)) {
@@ -10386,11 +10465,10 @@ Example output:
10386
10465
  }
10387
10466
  }
10388
10467
  collected.push(...fromChunk);
10389
- await onChunkResults?.(fromChunk);
10390
- if (i < chunks.length - 1) {
10391
- onActivity?.(i + 1, chunks.length);
10392
- }
10393
- }
10468
+ await onChunkResults?.(fromChunk, { next, size });
10469
+ if (next < totalChars) onActivity?.(next, totalChars);
10470
+ return outcome;
10471
+ }, resume);
10394
10472
  return collected;
10395
10473
  }
10396
10474
  function getLanguageName(locale) {
@@ -10855,13 +10933,14 @@ function buildPdfAnnotation(anchored, resourceId, userId, generator, motivation,
10855
10933
  ...body !== void 0 ? { body } : {}
10856
10934
  };
10857
10935
  }
10858
- async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
10936
+ async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete, resumeCursors) {
10859
10937
  const echo = detectionEcho(params);
10860
10938
  onProgress(10, { code: "loading" }, echo);
10861
10939
  onProgress(30, { code: "analyzing" }, echo);
10862
10940
  const dedupe = makeSpanDeduper();
10863
- let found = 0;
10864
- let created = 0;
10941
+ const prior = resumeCursors?.["highlighting"];
10942
+ let found = prior?.found ?? 0;
10943
+ let created = prior?.emitted ?? 0;
10865
10944
  await AnnotationDetection.detectHighlights(
10866
10945
  content,
10867
10946
  inferenceClient,
@@ -10869,13 +10948,14 @@ async function processHighlightJob(content, inferenceClient, params, buildAnnota
10869
10948
  params.density,
10870
10949
  params.sourceLanguage,
10871
10950
  // Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
10872
- (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo),
10873
- async (matches) => {
10951
+ (consumedChars, totalChars) => onProgress(30 + Math.round(consumedChars / totalChars * 30), { code: "analyzing" }, echo),
10952
+ resumeCursors?.["highlighting"],
10953
+ async (matches, cursor) => {
10874
10954
  found += matches.length;
10875
10955
  const fresh = dedupe(matches.map((h) => buildAnnotation("highlighting", h)));
10876
10956
  created += fresh.length;
10877
10957
  onProgress(60, { code: "creating-annotations", count: created }, echo);
10878
- await onChunkComplete(fresh);
10958
+ await onChunkComplete(fresh, { unit: "highlighting", cursor: { ...cursor, found, emitted: created } });
10879
10959
  }
10880
10960
  );
10881
10961
  onProgress(100, { code: "complete-created", count: created, kind: "highlight" }, echo);
@@ -10890,14 +10970,15 @@ function detectionEcho(p) {
10890
10970
  if (p.density !== void 0) requestParams.push({ label: "density", value: String(p.density) });
10891
10971
  return requestParams.length > 0 ? { requestParams } : {};
10892
10972
  }
10893
- async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
10973
+ async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete, resumeCursors) {
10894
10974
  const echo = detectionEcho(params);
10895
10975
  onProgress(10, { code: "loading" }, echo);
10896
10976
  onProgress(30, { code: "analyzing" }, echo);
10897
10977
  const bodyLanguage = params.language ?? "en";
10898
10978
  const dedupe = makeSpanDeduper();
10899
- let found = 0;
10900
- let created = 0;
10979
+ const prior = resumeCursors?.["commenting"];
10980
+ let found = prior?.found ?? 0;
10981
+ let created = prior?.emitted ?? 0;
10901
10982
  await AnnotationDetection.detectComments(
10902
10983
  content,
10903
10984
  inferenceClient,
@@ -10907,8 +10988,9 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
10907
10988
  params.language,
10908
10989
  params.sourceLanguage,
10909
10990
  // Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
10910
- (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo),
10911
- async (comments) => {
10991
+ (consumedChars, totalChars) => onProgress(30 + Math.round(consumedChars / totalChars * 30), { code: "analyzing" }, echo),
10992
+ resumeCursors?.["commenting"],
10993
+ async (comments, cursor) => {
10912
10994
  found += comments.length;
10913
10995
  const fresh = dedupe(comments.map(
10914
10996
  (c) => (
@@ -10922,7 +11004,7 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
10922
11004
  ));
10923
11005
  created += fresh.length;
10924
11006
  onProgress(60, { code: "creating-annotations", count: created }, echo);
10925
- await onChunkComplete(fresh);
11007
+ await onChunkComplete(fresh, { unit: "commenting", cursor: { ...cursor, found, emitted: created } });
10926
11008
  }
10927
11009
  );
10928
11010
  onProgress(100, { code: "complete-created", count: created, kind: "comment" }, echo);
@@ -10930,14 +11012,15 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
10930
11012
  result: { kind: "comment-annotation", commentsFound: found, commentsCreated: created }
10931
11013
  };
10932
11014
  }
10933
- async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
11015
+ async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete, resumeCursors) {
10934
11016
  const echo = detectionEcho(params);
10935
11017
  onProgress(10, { code: "loading" }, echo);
10936
11018
  onProgress(30, { code: "analyzing" }, echo);
10937
11019
  const bodyLanguage = params.language ?? "en";
10938
11020
  const dedupe = makeSpanDeduper();
10939
- let found = 0;
10940
- let created = 0;
11021
+ const prior = resumeCursors?.["assessing"];
11022
+ let found = prior?.found ?? 0;
11023
+ let created = prior?.emitted ?? 0;
10941
11024
  await AnnotationDetection.detectAssessments(
10942
11025
  content,
10943
11026
  inferenceClient,
@@ -10947,8 +11030,9 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
10947
11030
  params.language,
10948
11031
  params.sourceLanguage,
10949
11032
  // Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
10950
- (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo),
10951
- async (assessments) => {
11033
+ (consumedChars, totalChars) => onProgress(30 + Math.round(consumedChars / totalChars * 30), { code: "analyzing" }, echo),
11034
+ resumeCursors?.["assessing"],
11035
+ async (assessments, cursor) => {
10952
11036
  found += assessments.length;
10953
11037
  const fresh = dedupe(assessments.map(
10954
11038
  (a) => (
@@ -10969,7 +11053,7 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
10969
11053
  ));
10970
11054
  created += fresh.length;
10971
11055
  onProgress(60, { code: "creating-annotations", count: created }, echo);
10972
- await onChunkComplete(fresh);
11056
+ await onChunkComplete(fresh, { unit: "assessing", cursor: { ...cursor, found, emitted: created } });
10973
11057
  }
10974
11058
  );
10975
11059
  onProgress(100, { code: "complete-created", count: created, kind: "assessment" }, echo);
@@ -10977,12 +11061,12 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
10977
11061
  result: { kind: "assessment-annotation", assessmentsFound: found, assessmentsCreated: created }
10978
11062
  };
10979
11063
  }
10980
- async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger2, onUnitComplete, signal, onChunkComplete) {
11064
+ async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger2, onUnitComplete, signal, onChunkComplete, resumeCursors) {
10981
11065
  const entityTypeNames = params.entityTypes.map(String);
10982
11066
  const requestParams = [{ label: "entity-types", value: entityTypeNames.join(", ") }];
10983
11067
  const completedItems = [];
10984
- let totalFound = 0;
10985
- let totalEmitted = 0;
11068
+ let totalFound = Object.values(resumeCursors ?? {}).reduce((n, c) => n + c.found, 0);
11069
+ let totalEmitted = Object.values(resumeCursors ?? {}).reduce((n, c) => n + c.emitted, 0);
10986
11070
  let errors = 0;
10987
11071
  let totalUnderReportedPieces = 0;
10988
11072
  let totalExpected = 0;
@@ -11011,8 +11095,9 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
11011
11095
  { type: "TextualBody", value: entityTypeName, purpose: "tagging", format: "text/plain", language: bodyLanguage }
11012
11096
  ];
11013
11097
  const dedupe = makeSpanDeduper();
11014
- let unitFound = 0;
11015
- let unitPersisted = 0;
11098
+ const priorUnit = resumeCursors?.[entityTypeName];
11099
+ let unitFound = priorUnit?.found ?? 0;
11100
+ let unitPersisted = priorUnit?.emitted ?? 0;
11016
11101
  let underReported;
11017
11102
  await extractEntities(
11018
11103
  content,
@@ -11038,7 +11123,8 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
11038
11123
  totalExpected += counted;
11039
11124
  emitTypeProgress(entityTypeName);
11040
11125
  },
11041
- async (chunkEntities) => {
11126
+ resumeCursors?.[entityTypeName],
11127
+ async (chunkEntities, cursor) => {
11042
11128
  const built = [];
11043
11129
  for (const entity of chunkEntities) {
11044
11130
  const reconciled = reconcileSelector(content, {
@@ -11058,9 +11144,14 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
11058
11144
  built.push(buildAnnotation("linking", toMatch(reconciled), unresolvedBody));
11059
11145
  }
11060
11146
  const fresh = dedupe(built);
11061
- await onChunkComplete?.(fresh);
11062
- unitFound += chunkEntities.length;
11063
- unitPersisted += fresh.length;
11147
+ const nextFound = unitFound + chunkEntities.length;
11148
+ const nextEmitted = unitPersisted + fresh.length;
11149
+ await onChunkComplete?.(fresh, {
11150
+ unit: entityTypeName,
11151
+ cursor: { ...cursor, found: nextFound, emitted: nextEmitted }
11152
+ });
11153
+ unitFound = nextFound;
11154
+ unitPersisted = nextEmitted;
11064
11155
  totalFound += chunkEntities.length;
11065
11156
  totalEmitted += fresh.length;
11066
11157
  emitTypeProgress(entityTypeName);
@@ -11092,13 +11183,13 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
11092
11183
  }
11093
11184
  };
11094
11185
  }
11095
- async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete) {
11186
+ async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress, onChunkComplete, resumeCursors) {
11096
11187
  onProgress(10, { code: "loading" });
11097
11188
  onProgress(30, { code: "analyzing-tags" });
11098
11189
  const bodyLanguage = params.language ?? "en";
11099
11190
  const dedupe = makeSpanDeduper();
11100
11191
  let found = 0;
11101
- let created = 0;
11192
+ let created = Object.values(resumeCursors ?? {}).reduce((n, c) => n + c.emitted, 0);
11102
11193
  const byCategory = {};
11103
11194
  const completedItems = [];
11104
11195
  for (let c = 0; c < params.categories.length; c++) {
@@ -11114,7 +11205,9 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
11114
11205
  { code: "analyzing-tags" },
11115
11206
  position()
11116
11207
  );
11117
- let categoryFound = 0;
11208
+ const priorCategory = resumeCursors?.[category];
11209
+ let categoryFound = priorCategory?.found ?? 0;
11210
+ let categoryCreated = priorCategory?.emitted ?? 0;
11118
11211
  await AnnotationDetection.detectTags(
11119
11212
  content,
11120
11213
  inferenceClient,
@@ -11123,12 +11216,13 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
11123
11216
  params.sourceLanguage,
11124
11217
  // Liveness (chunk boundaries + in-flight heartbeat): this category's
11125
11218
  // slice of the 30–60 band.
11126
- (completed, total) => onProgress(
11127
- 30 + Math.round((c + completed / total) / params.categories.length * 30),
11219
+ (consumedChars, totalChars) => onProgress(
11220
+ 30 + Math.round((c + consumedChars / totalChars) / params.categories.length * 30),
11128
11221
  { code: "analyzing-tags" },
11129
11222
  position()
11130
11223
  ),
11131
- async (matches) => {
11224
+ resumeCursors?.[category],
11225
+ async (matches, cursor) => {
11132
11226
  categoryFound += matches.length;
11133
11227
  const fresh = dedupe(matches.map((t) => {
11134
11228
  const cat = t.category ?? "unknown";
@@ -11144,7 +11238,11 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
11144
11238
  byCategory[cat] = (byCategory[cat] ?? 0) + 1;
11145
11239
  }
11146
11240
  onProgress(60, { code: "creating-tag-annotations", count: created });
11147
- await onChunkComplete(fresh);
11241
+ categoryCreated += fresh.length;
11242
+ await onChunkComplete(fresh, {
11243
+ unit: category,
11244
+ cursor: { ...cursor, found: categoryFound, emitted: categoryCreated }
11245
+ });
11148
11246
  }
11149
11247
  );
11150
11248
  found += categoryFound;
@@ -11356,7 +11454,7 @@ async function commitAnnotations(session, resourceId, annotations) {
11356
11454
  if (annotations.length === 0) return void 0;
11357
11455
  try {
11358
11456
  await busRequest(
11359
- workerBusAsPrimitive(session.client.transport.actor),
11457
+ session.client.transport.actor,
11360
11458
  "mark:commit",
11361
11459
  { resourceId, annotations },
11362
11460
  MARK_COMMIT_TIMEOUT_MS
@@ -11399,6 +11497,7 @@ function startWorkerProcess(config) {
11399
11497
  jobTypes: config.jobTypes
11400
11498
  });
11401
11499
  const completedUnitsByJob = /* @__PURE__ */ new Map();
11500
+ const unitCursorsByJob = /* @__PURE__ */ new Map();
11402
11501
  let activeCancel = null;
11403
11502
  httpTransport.actor.addChannels?.(["job:cancel-requested"]);
11404
11503
  httpTransport.on("job:cancel-requested", (event) => {
@@ -11413,14 +11512,17 @@ function startWorkerProcess(config) {
11413
11512
  logger2.info("Processing job", { jobId: job.jobId, type: job.type, resourceId: job.resourceId });
11414
11513
  const controller = new AbortController();
11415
11514
  activeCancel = { jobId: job.jobId, controller };
11416
- handleJob(adapter, config, job, completedUnitsByJob, controller.signal).then(() => {
11515
+ handleJob(adapter, config, job, completedUnitsByJob, controller.signal, unitCursorsByJob).then(() => {
11417
11516
  completedUnitsByJob.delete(job.jobId);
11517
+ unitCursorsByJob.delete(job.jobId);
11418
11518
  }).catch((error) => {
11419
11519
  const message = error instanceof Error ? error.message : String(error);
11420
11520
  const failureClass = classifyFailure(error);
11421
11521
  logger2.error("Job failed", { jobId: job.jobId, error: message, failureClass, stack: error instanceof Error ? error.stack : void 0 });
11422
11522
  const completedUnits = completedUnitsByJob.get(job.jobId);
11523
+ const unitCursors = unitCursorsByJob.get(job.jobId);
11423
11524
  completedUnitsByJob.delete(job.jobId);
11525
+ unitCursorsByJob.delete(job.jobId);
11424
11526
  const failAnnotationId = referenceIdOf(job);
11425
11527
  if (isJobType(job.type)) {
11426
11528
  emitEvent(session, "job:fail", {
@@ -11430,6 +11532,10 @@ function startWorkerProcess(config) {
11430
11532
  ...failAnnotationId ? { annotationId: failAnnotationId } : {},
11431
11533
  error: message,
11432
11534
  ...completedUnits && completedUnits.length > 0 ? { completedUnits } : {},
11535
+ // Where each unfinished unit got to. Absent rather than `{}` when
11536
+ // nothing was reached: an empty object would claim units were
11537
+ // tracked and none progressed.
11538
+ ...unitCursors && Object.keys(unitCursors).length > 0 ? { unitCursors } : {},
11433
11539
  ...failureClass !== void 0 ? { failureClass } : {},
11434
11540
  // What the commit path OBSERVED about durability, when the failure
11435
11541
  // came from a commit at all. Present only on that path: absent means
@@ -11452,13 +11558,13 @@ function startWorkerProcess(config) {
11452
11558
  adapter.start();
11453
11559
  return adapter;
11454
11560
  }
11455
- async function handleJob(adapter, config, job, completedUnitsByJob = /* @__PURE__ */ new Map(), signal) {
11561
+ async function handleJob(adapter, config, job, completedUnitsByJob = /* @__PURE__ */ new Map(), signal, unitCursorsByJob = /* @__PURE__ */ new Map()) {
11456
11562
  const start = performance.now();
11457
11563
  let outcome = "completed";
11458
11564
  try {
11459
11565
  return await withSpan(
11460
11566
  `job:${job.type}`,
11461
- () => handleJobInner(adapter, config, job, completedUnitsByJob, signal),
11567
+ () => handleJobInner(adapter, config, job, completedUnitsByJob, signal, unitCursorsByJob),
11462
11568
  {
11463
11569
  kind: SpanKind.CONSUMER,
11464
11570
  attrs: {
@@ -11475,7 +11581,7 @@ async function handleJob(adapter, config, job, completedUnitsByJob = /* @__PURE_
11475
11581
  recordJobOutcome(job.type, outcome, performance.now() - start);
11476
11582
  }
11477
11583
  }
11478
- async function handleJobInner(adapter, config, job, completedUnitsByJob, signal) {
11584
+ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal, unitCursorsByJob = /* @__PURE__ */ new Map()) {
11479
11585
  const { session, inferenceClient, generator } = config;
11480
11586
  const { userId, jobId } = job;
11481
11587
  if (!isJobType(job.type)) {
@@ -11485,10 +11591,12 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11485
11591
  const jobType = job.type;
11486
11592
  const resourceId$1 = resourceId(job.resourceId);
11487
11593
  const annotationId = referenceIdOf(job);
11594
+ const attempt = job.retryCount + 1;
11488
11595
  const lifecycleBase = {
11489
11596
  resourceId: resourceId$1,
11490
11597
  jobId,
11491
11598
  jobType,
11599
+ attempt,
11492
11600
  ...annotationId ? { annotationId } : {}
11493
11601
  };
11494
11602
  let durability;
@@ -11548,6 +11656,17 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11548
11656
  }).catch(() => {
11549
11657
  });
11550
11658
  };
11659
+ const unitCursors = /* @__PURE__ */ new Map();
11660
+ const commitChunk = async (annotations, checkpoint) => {
11661
+ record(await commitAnnotations(session, String(resourceId$1), annotations));
11662
+ unitCursors.set(checkpoint.unit, checkpoint.cursor);
11663
+ unitCursorsByJob.set(job.jobId, Object.fromEntries(unitCursors));
11664
+ await emitEvent(session, "job:checkpoint", {
11665
+ jobId: job.jobId,
11666
+ completedUnits: [...completedUnitsByJob.get(job.jobId) ?? []],
11667
+ unitCursors: Object.fromEntries(unitCursors)
11668
+ });
11669
+ };
11551
11670
  if (jobType === "highlight-annotation") {
11552
11671
  const { result } = await processHighlightJob(
11553
11672
  ready.text,
@@ -11556,10 +11675,11 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11556
11675
  ready.buildAnnotation,
11557
11676
  onProgress,
11558
11677
  // 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
- }
11678
+ // durability evidence like every commit, and carries the unit's cursor.
11679
+ commitChunk,
11680
+ // …and the other direction: where an earlier attempt left each unit
11681
+ // (CHUNK-GRAIN-RESUME P3). Empty on a first attempt.
11682
+ job.unitCursors
11563
11683
  );
11564
11684
  await emitEvent(session, "job:complete", {
11565
11685
  ...terminalBase(),
@@ -11574,10 +11694,11 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11574
11694
  ready.buildAnnotation,
11575
11695
  onProgress,
11576
11696
  // 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
- }
11697
+ // durability evidence like every commit, and carries the unit's cursor.
11698
+ commitChunk,
11699
+ // …and the other direction: where an earlier attempt left each unit
11700
+ // (CHUNK-GRAIN-RESUME P3). Empty on a first attempt.
11701
+ job.unitCursors
11581
11702
  );
11582
11703
  await emitEvent(session, "job:complete", {
11583
11704
  ...terminalBase(),
@@ -11592,10 +11713,11 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11592
11713
  ready.buildAnnotation,
11593
11714
  onProgress,
11594
11715
  // 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
- }
11716
+ // durability evidence like every commit, and carries the unit's cursor.
11717
+ commitChunk,
11718
+ // …and the other direction: where an earlier attempt left each unit
11719
+ // (CHUNK-GRAIN-RESUME P3). Empty on a first attempt.
11720
+ job.unitCursors
11599
11721
  );
11600
11722
  await emitEvent(session, "job:complete", {
11601
11723
  ...terminalBase(),
@@ -11620,17 +11742,21 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11620
11742
  config.logger,
11621
11743
  async (unit) => {
11622
11744
  committed.push(unit);
11745
+ unitCursors.delete(unit);
11746
+ unitCursorsByJob.set(job.jobId, Object.fromEntries(unitCursors));
11623
11747
  await emitEvent(session, "job:checkpoint", {
11624
11748
  jobId: job.jobId,
11625
- completedUnits: [...committed]
11749
+ completedUnits: [...committed],
11750
+ ...unitCursors.size > 0 ? { unitCursors: Object.fromEntries(unitCursors) } : {}
11626
11751
  });
11627
11752
  },
11628
11753
  signal,
11629
11754
  // 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
- }
11755
+ // durability evidence like every commit, and carries the unit's cursor.
11756
+ commitChunk,
11757
+ // …and the other direction: where an earlier attempt left each unit
11758
+ // (CHUNK-GRAIN-RESUME P3). Empty on a first attempt.
11759
+ job.unitCursors
11634
11760
  );
11635
11761
  if (signal?.aborted) {
11636
11762
  await emitEvent(session, "job:cancel", {
@@ -11653,10 +11779,11 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal)
11653
11779
  ready.buildAnnotation,
11654
11780
  onProgress,
11655
11781
  // 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
- }
11782
+ // durability evidence like every commit, and carries the unit's cursor.
11783
+ commitChunk,
11784
+ // …and the other direction: where an earlier attempt left each unit
11785
+ // (CHUNK-GRAIN-RESUME P3). Empty on a first attempt.
11786
+ job.unitCursors
11660
11787
  );
11661
11788
  await emitEvent(session, "job:complete", {
11662
11789
  ...terminalBase(),