@semiont/jobs 0.5.38 → 0.6.0

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, 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';
1
+ import { replyChannelsFor, createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, serviceAccountToken, isTransientFetchError, isObject, isString, isNumber, RETRY_RULES, resourceId, getPrimaryMediaType, isGenerationJobParams, capabilitiesOf, assembleAnnotation, findClaimSpan, busRequest, BusRequestError, isArray, textSourceOf, yieldsGeometryOf, decodeRepresentation, GENERATABLE_MEDIA_TYPES, 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';
@@ -9223,13 +9223,15 @@ function readUnitCursors(raw, completedUnits) {
9223
9223
  }
9224
9224
  function createJobClaimAdapter(options) {
9225
9225
  const { bus, jobTypes } = options;
9226
- const requestBus = bus;
9227
9226
  const activeJob$ = new import_rxjs.BehaviorSubject(null);
9228
9227
  const isProcessing$ = new import_rxjs.BehaviorSubject(false);
9229
9228
  const jobsCompleted$ = new import_rxjs.BehaviorSubject(0);
9230
9229
  const errors$ = new import_rxjs.Subject();
9231
- let jobSubscription = null;
9230
+ const refused$ = new import_rxjs.Subject();
9231
+ let subscriptions = [];
9232
9232
  let started = false;
9233
+ let claimInFlight = false;
9234
+ let wakePending = false;
9233
9235
  let lastQueuedEventAt = null;
9234
9236
  let lastClaimAt = null;
9235
9237
  let lastFinishedAt = null;
@@ -9237,62 +9239,98 @@ function createJobClaimAdapter(options) {
9237
9239
  let activeSince = null;
9238
9240
  const iso = (t) => t === null ? null : new Date(t).toISOString();
9239
9241
  const claimNext = async () => {
9242
+ let record;
9240
9243
  try {
9241
- const job = await busRequest(requestBus, "job:claim", { types: jobTypes }, 1e4);
9242
- if (!isString(job.metadata?.id) || !isString(job.metadata?.type)) return null;
9243
- const completedUnits = isArray(job.metadata?.completedUnits) ? job.metadata.completedUnits.filter(isString) : [];
9244
- const unitCursors = readUnitCursors(job.metadata?.unitCursors, completedUnits);
9245
- const params = job.params ?? {};
9246
- return {
9247
- jobId: job.metadata.id,
9248
- type: job.metadata.type,
9244
+ record = await busRequest(bus, "job:claim", { types: jobTypes }, 1e4);
9245
+ } catch (error) {
9246
+ if (error instanceof BusRequestError) {
9247
+ if (error.code === "bus.none-pending") return { declined: true };
9248
+ return { refused: { code: error.code, message: error.message } };
9249
+ }
9250
+ return { refused: { code: null, message: error instanceof Error ? error.message : String(error) } };
9251
+ }
9252
+ if (!isString(record.metadata?.id) || !isString(record.metadata?.type)) {
9253
+ return { refused: { code: null, message: "claimed record carries no job id or type" } };
9254
+ }
9255
+ const completedUnits = isArray(record.metadata?.completedUnits) ? record.metadata.completedUnits.filter(isString) : [];
9256
+ const unitCursors = readUnitCursors(record.metadata?.unitCursors, completedUnits);
9257
+ const params = record.params ?? {};
9258
+ return {
9259
+ job: {
9260
+ jobId: record.metadata.id,
9261
+ type: record.metadata.type,
9249
9262
  resourceId: isString(params.resourceId) ? params.resourceId : "",
9250
- userId: job.metadata?.userId ?? "",
9263
+ userId: record.metadata?.userId ?? "",
9251
9264
  params,
9252
9265
  completedUnits,
9253
9266
  unitCursors,
9254
9267
  // Absent or malformed metadata reads as "no budget left" — a worker
9255
9268
  // that cannot see the budget must not claim a retry is coming.
9256
- retryCount: isNumber(job.metadata?.retryCount) ? job.metadata.retryCount : 0,
9257
- maxRetries: isNumber(job.metadata?.maxRetries) ? job.metadata.maxRetries : 0
9258
- };
9259
- } catch {
9260
- return null;
9269
+ retryCount: isNumber(record.metadata?.retryCount) ? record.metadata.retryCount : 0,
9270
+ maxRetries: isNumber(record.metadata?.maxRetries) ? record.metadata.maxRetries : 0
9271
+ }
9272
+ };
9273
+ };
9274
+ const pull = () => {
9275
+ if (!started) return;
9276
+ if (activeJob$.getValue() !== null) return;
9277
+ if (claimInFlight) {
9278
+ wakePending = true;
9279
+ return;
9261
9280
  }
9281
+ claimInFlight = true;
9282
+ wakePending = false;
9283
+ isProcessing$.next(true);
9284
+ void claimNext().then((outcome) => {
9285
+ claimInFlight = false;
9286
+ if ("job" in outcome) {
9287
+ const now = Date.now();
9288
+ lastClaimAt = now;
9289
+ lastActivityAt = now;
9290
+ activeSince = now;
9291
+ wakePending = false;
9292
+ activeJob$.next(outcome.job);
9293
+ return;
9294
+ }
9295
+ if ("refused" in outcome) refused$.next(outcome.refused);
9296
+ isProcessing$.next(false);
9297
+ if (wakePending) {
9298
+ wakePending = false;
9299
+ pull();
9300
+ }
9301
+ });
9262
9302
  };
9263
9303
  return {
9264
9304
  activeJob$: activeJob$.asObservable(),
9265
9305
  isProcessing$: isProcessing$.asObservable(),
9266
9306
  jobsCompleted$: jobsCompleted$.asObservable(),
9267
9307
  errors$: errors$.asObservable(),
9308
+ refused$: refused$.asObservable(),
9268
9309
  start: () => {
9269
9310
  if (started) return;
9270
9311
  started = true;
9271
- bus.addChannels?.(["job:queued"]);
9272
- jobSubscription = bus.stream("job:queued").subscribe((event) => {
9273
- lastQueuedEventAt = Date.now();
9274
- const jobType = event.jobType;
9275
- if (jobTypes.length > 0 && !jobTypes.includes(jobType)) return;
9276
- if (isProcessing$.getValue()) return;
9277
- isProcessing$.next(true);
9278
- claimNext().then((job) => {
9279
- if (job) {
9280
- const now = Date.now();
9281
- lastClaimAt = now;
9282
- lastActivityAt = now;
9283
- activeSince = now;
9284
- activeJob$.next(job);
9285
- } else {
9286
- isProcessing$.next(false);
9287
- }
9288
- }).catch(() => {
9289
- isProcessing$.next(false);
9290
- });
9291
- });
9312
+ subscriptions.push(
9313
+ bus.stream("job:queued").subscribe((event) => {
9314
+ lastQueuedEventAt = Date.now();
9315
+ if (jobTypes.length > 0 && !jobTypes.includes(event.jobType)) return;
9316
+ pull();
9317
+ })
9318
+ );
9319
+ let observed = false;
9320
+ let wasOpen = false;
9321
+ subscriptions.push(
9322
+ bus.state$.subscribe((state) => {
9323
+ const open = state === "open";
9324
+ if (observed && open && !wasOpen) pull();
9325
+ observed = true;
9326
+ wasOpen = open;
9327
+ })
9328
+ );
9329
+ if (wasOpen) pull();
9292
9330
  },
9293
9331
  stop: () => {
9294
- jobSubscription?.unsubscribe();
9295
- jobSubscription = null;
9332
+ for (const s of subscriptions) s.unsubscribe();
9333
+ subscriptions = [];
9296
9334
  started = false;
9297
9335
  },
9298
9336
  completeJob: () => {
@@ -9303,6 +9341,7 @@ function createJobClaimAdapter(options) {
9303
9341
  activeJob$.next(null);
9304
9342
  isProcessing$.next(false);
9305
9343
  jobsCompleted$.next(jobsCompleted$.getValue() + 1);
9344
+ pull();
9306
9345
  },
9307
9346
  failJob: (jid, error) => {
9308
9347
  const now = Date.now();
@@ -9312,6 +9351,7 @@ function createJobClaimAdapter(options) {
9312
9351
  activeJob$.next(null);
9313
9352
  isProcessing$.next(false);
9314
9353
  errors$.next({ jobId: jid, error });
9354
+ pull();
9315
9355
  },
9316
9356
  vitals: () => {
9317
9357
  const active = activeJob$.getValue();
@@ -9328,13 +9368,14 @@ function createJobClaimAdapter(options) {
9328
9368
  lastActivityAt = Date.now();
9329
9369
  },
9330
9370
  dispose: () => {
9331
- jobSubscription?.unsubscribe();
9332
- jobSubscription = null;
9371
+ for (const s of subscriptions) s.unsubscribe();
9372
+ subscriptions = [];
9333
9373
  started = false;
9334
9374
  activeJob$.complete();
9335
9375
  isProcessing$.complete();
9336
9376
  jobsCompleted$.complete();
9337
9377
  errors$.complete();
9378
+ refused$.complete();
9338
9379
  }
9339
9380
  };
9340
9381
  }
@@ -10838,7 +10879,7 @@ function makeSpanDeduper() {
10838
10879
  return out;
10839
10880
  };
10840
10881
  }
10841
- function buildTextAnnotation(content, resourceId, userId, generator, motivation, match, body) {
10882
+ function buildTextAnnotation(content, resourceId, generator, motivation, match, body) {
10842
10883
  if (content.substring(match.start, match.end) !== match.exact) {
10843
10884
  throw new Error(
10844
10885
  `buildTextAnnotation invariant: content.substring(${match.start}, ${match.end}) !== exact for resource ${resourceId}, motivation ${motivation}`
@@ -10860,16 +10901,12 @@ function buildTextAnnotation(content, resourceId, userId, generator, motivation,
10860
10901
  );
10861
10902
  }
10862
10903
  }
10863
- const creator = didToAgent(userId);
10864
- const wasAttributedTo = creator["@id"] === generator["@id"] ? [generator] : [creator, generator];
10865
10904
  return {
10866
10905
  "@context": "http://www.w3.org/ns/anno.jsonld",
10867
10906
  "type": "Annotation",
10868
10907
  "id": annotationIdFor({ resourceId, motivation, anchor: spanAnchor(match), body }),
10869
10908
  motivation,
10870
- creator,
10871
10909
  generator,
10872
- wasAttributedTo,
10873
10910
  created: (/* @__PURE__ */ new Date()).toISOString(),
10874
10911
  target: {
10875
10912
  type: "SpecificResource",
@@ -10887,7 +10924,7 @@ function buildTextAnnotation(content, resourceId, userId, generator, motivation,
10887
10924
  ...body !== void 0 ? { body } : {}
10888
10925
  };
10889
10926
  }
10890
- function buildPdfAnnotation(anchored, resourceId, userId, generator, motivation, match, body) {
10927
+ function buildPdfAnnotation(anchored, resourceId, generator, motivation, match, body) {
10891
10928
  const { rects, overlap } = locate(anchored, match.start, match.end);
10892
10929
  const coveredText = overlap.length ? anchored.text.substring(
10893
10930
  Math.min(...overlap.map((i) => i.start)),
@@ -10904,16 +10941,12 @@ function buildPdfAnnotation(anchored, resourceId, userId, generator, motivation,
10904
10941
  `buildPdfAnnotation invariant: covered text does not contain exact for resource ${resourceId}, motivation ${motivation}`
10905
10942
  );
10906
10943
  }
10907
- const creator = didToAgent(userId);
10908
- const wasAttributedTo = creator["@id"] === generator["@id"] ? [generator] : [creator, generator];
10909
10944
  return {
10910
10945
  "@context": "http://www.w3.org/ns/anno.jsonld",
10911
10946
  "type": "Annotation",
10912
10947
  "id": annotationIdFor({ resourceId, motivation, anchor: spanAnchor(match), body }),
10913
10948
  motivation,
10914
- creator,
10915
10949
  generator,
10916
- wasAttributedTo,
10917
10950
  created: (/* @__PURE__ */ new Date()).toISOString(),
10918
10951
  target: {
10919
10952
  type: "SpecificResource",
@@ -11400,7 +11433,7 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
11400
11433
  }
11401
11434
 
11402
11435
  // src/workers/detection/prepare-detection.ts
11403
- async function prepareDetection(mediaType, content, resourceId, userId, generator, consult) {
11436
+ async function prepareDetection(mediaType, content, resourceId, generator, consult) {
11404
11437
  if (textSourceOf(mediaType) === "none") return { declined: "no-extractor" };
11405
11438
  if (yieldsGeometryOf(mediaType)) {
11406
11439
  const answer = await consult(resourceId);
@@ -11410,7 +11443,7 @@ async function prepareDetection(mediaType, content, resourceId, userId, generato
11410
11443
  const anchored = { text: answer.text, items: answer.items ?? [] };
11411
11444
  return {
11412
11445
  text: answer.text,
11413
- buildAnnotation: (motivation, match, body) => buildPdfAnnotation(anchored, resourceId, userId, generator, motivation, match, body)
11446
+ buildAnnotation: (motivation, match, body) => buildPdfAnnotation(anchored, resourceId, generator, motivation, match, body)
11414
11447
  };
11415
11448
  }
11416
11449
  // The Smelter's own decline (encrypted, corrupt) — passed through by name.
@@ -11436,7 +11469,7 @@ async function prepareDetection(mediaType, content, resourceId, userId, generato
11436
11469
  if (!text.trim()) return { declined: "empty" };
11437
11470
  return {
11438
11471
  text,
11439
- buildAnnotation: (motivation, match, body) => buildTextAnnotation(text, resourceId, userId, generator, motivation, match, body)
11472
+ buildAnnotation: (motivation, match, body) => buildTextAnnotation(text, resourceId, generator, motivation, match, body)
11440
11473
  };
11441
11474
  }
11442
11475
  function referenceIdOf(job) {
@@ -11452,13 +11485,15 @@ function referenceIdOf(job) {
11452
11485
  return typeof ref === "string" ? ref : void 0;
11453
11486
  }
11454
11487
  var MARK_COMMIT_TIMEOUT_MS = 6e4;
11455
- async function commitAnnotations(session, resourceId, annotations) {
11488
+ async function commitAnnotations(session, resourceId, annotations, jobId) {
11456
11489
  if (annotations.length === 0) return void 0;
11457
11490
  try {
11458
11491
  await busRequest(
11459
11492
  session.client.transport.actor,
11460
11493
  "mark:commit",
11461
- { resourceId, annotations },
11494
+ // The batch cites the job it fulfils. Who asked for these annotations is
11495
+ // derived downstream from that job's own events; the worker never says.
11496
+ { resourceId, annotations, jobId },
11462
11497
  MARK_COMMIT_TIMEOUT_MS
11463
11498
  );
11464
11499
  return "acknowledged";
@@ -11498,10 +11533,22 @@ function startWorkerProcess(config) {
11498
11533
  bus: httpTransport.actor,
11499
11534
  jobTypes: config.jobTypes
11500
11535
  });
11536
+ const exit = config.exit ?? ((code) => process.exit(code));
11537
+ adapter.refused$.subscribe(({ code, message }) => {
11538
+ if (code === "bus.unauthorized") {
11539
+ logger2.error("Claim refused: this worker is not authorized to claim jobs \u2014 exiting for restart", {
11540
+ code,
11541
+ message,
11542
+ jobTypes: config.jobTypes
11543
+ });
11544
+ exit(1);
11545
+ return;
11546
+ }
11547
+ logger2.warn("Claim declined; parked until the next wake-up", { code, message });
11548
+ });
11501
11549
  const completedUnitsByJob = /* @__PURE__ */ new Map();
11502
11550
  const unitCursorsByJob = /* @__PURE__ */ new Map();
11503
11551
  let activeCancel = null;
11504
- httpTransport.actor.addChannels?.(["job:cancel-requested"]);
11505
11552
  httpTransport.on("job:cancel-requested", (event) => {
11506
11553
  const targetId = event.jobId;
11507
11554
  if (targetId && activeCancel?.jobId === targetId) {
@@ -11585,7 +11632,7 @@ async function handleJob(adapter, config, job, completedUnitsByJob = /* @__PURE_
11585
11632
  }
11586
11633
  async function handleJobInner(adapter, config, job, completedUnitsByJob, signal, unitCursorsByJob = /* @__PURE__ */ new Map()) {
11587
11634
  const { session, inferenceClient, generator } = config;
11588
- const { userId, jobId } = job;
11635
+ const { jobId } = job;
11589
11636
  if (!isJobType(job.type)) {
11590
11637
  adapter.failJob(jobId, `Unrecognized job type: ${job.type}`);
11591
11638
  return;
@@ -11618,7 +11665,7 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal,
11618
11665
  const mediaType = getPrimaryMediaType(descriptor);
11619
11666
  const source = await withSpan(
11620
11667
  "detection:prepare",
11621
- () => prepareDetection(mediaType ?? "", config.contentReads, resourceId$1, userId, generator, (rid) => session.client.browse.resourceAnchoredText(rid)),
11668
+ () => prepareDetection(mediaType ?? "", config.contentReads, resourceId$1, generator, (rid) => session.client.browse.resourceAnchoredText(rid)),
11622
11669
  { attrs: { "resource.id": resourceId$1, "media.type": mediaType ?? "unknown" } }
11623
11670
  );
11624
11671
  if ("declined" in source) {
@@ -11660,7 +11707,7 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal,
11660
11707
  };
11661
11708
  const unitCursors = /* @__PURE__ */ new Map();
11662
11709
  const commitChunk = async (annotations, checkpoint) => {
11663
- record(await commitAnnotations(session, String(resourceId$1), annotations));
11710
+ record(await commitAnnotations(session, String(resourceId$1), annotations, jobId));
11664
11711
  unitCursors.set(checkpoint.unit, checkpoint.cursor);
11665
11712
  unitCursorsByJob.set(job.jobId, Object.fromEntries(unitCursors));
11666
11713
  await emitEvent(session, "job:checkpoint", {
@@ -11826,7 +11873,10 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal,
11826
11873
  ...genParams.prompt ? { generationPrompt: genParams.prompt } : {},
11827
11874
  ...genParams.language ? { language: genParams.language } : {},
11828
11875
  ...genParams.entityTypes && genParams.entityTypes.length > 0 ? { entityTypes: genParams.entityTypes } : {},
11829
- generator
11876
+ generator,
11877
+ // The resource cites the job it fulfils; who asked for it is derived
11878
+ // downstream from that job's events, never stated here.
11879
+ jobId
11830
11880
  });
11831
11881
  if (!genReferenceId) {
11832
11882
  const { annotation: provenanceRef } = assembleAnnotation(
@@ -11837,7 +11887,7 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal,
11837
11887
  },
11838
11888
  generator
11839
11889
  );
11840
- record(await commitAnnotations(session, String(resourceId$1), [provenanceRef]));
11890
+ record(await commitAnnotations(session, String(resourceId$1), [provenanceRef], jobId));
11841
11891
  }
11842
11892
  const citationRefs = [];
11843
11893
  if (genResult.format === "application/pdf" && genResult.citations.length > 0) {
@@ -11863,7 +11913,6 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal,
11863
11913
  const citationRef = buildPdfAnnotation(
11864
11914
  layer,
11865
11915
  resourceId(String(newResourceId)),
11866
- userId,
11867
11916
  generator,
11868
11917
  "linking",
11869
11918
  { exact: layer.text.slice(span.start, span.end), start: span.start, end: span.end },
@@ -11891,7 +11940,7 @@ async function handleJobInner(adapter, config, job, completedUnitsByJob, signal,
11891
11940
  citationRefs.push(citationRef);
11892
11941
  }
11893
11942
  }
11894
- record(await commitAnnotations(session, String(newResourceId), citationRefs));
11943
+ record(await commitAnnotations(session, String(newResourceId), citationRefs, jobId));
11895
11944
  await emitEvent(session, "job:complete", {
11896
11945
  ...terminalBase(),
11897
11946
  result: { kind: "generation", resourceId: newResourceId, resourceName: genResult.title, truncated: genResult.result.truncated }
@@ -11961,7 +12010,16 @@ var WORKER_AWAITED_OPERATIONS = [
11961
12010
  // rare error path is no reason to let it back in.
11962
12011
  "browse:annotation-requested"
11963
12012
  ];
11964
- var WORKER_CHANNELS = replyChannelsFor(WORKER_AWAITED_OPERATIONS);
12013
+ var WORKER_CONSUMED_BROADCASTS = [
12014
+ // The queue announcement the claim adapter races for.
12015
+ "job:queued",
12016
+ // Cooperative cancellation of the ACTIVE job (JOB-RESTART-SAFETY P4).
12017
+ "job:cancel-requested"
12018
+ ];
12019
+ var WORKER_CHANNELS = [
12020
+ ...replyChannelsFor(WORKER_AWAITED_OPERATIONS),
12021
+ ...WORKER_CONSUMED_BROADCASTS
12022
+ ];
11965
12023
  function parseGatewayUrl(url) {
11966
12024
  const parsed = new URL(url);
11967
12025
  const protocol = parsed.protocol.replace(":", "") === "https" ? "https" : "http";
@@ -11970,16 +12028,17 @@ function parseGatewayUrl(url) {
11970
12028
  return { protocol, host, port };
11971
12029
  }
11972
12030
  async function authenticateAgent(opts) {
11973
- const { gatewayBaseUrl: gatewayBaseUrl2, workerSecret: workerSecret2, provider, model, logger: logger2, retry = STARTUP_FETCH_RETRY } = opts;
11974
- if (!workerSecret2) {
11975
- throw new Error("SEMIONT_WORKER_SECRET is required to authenticate worker agents");
11976
- }
12031
+ const { gatewayBaseUrl: gatewayBaseUrl2, credential: credential2, provider, model, logger: logger2, retry = STARTUP_FETCH_RETRY } = opts;
11977
12032
  return retryWithBackoff(
11978
12033
  async () => {
12034
+ const caller = await serviceAccountToken(credential2);
11979
12035
  const response = await fetch(`${gatewayBaseUrl2}/api/tokens/agent`, {
11980
12036
  method: "POST",
11981
- headers: { "Content-Type": "application/json" },
11982
- body: JSON.stringify({ secret: workerSecret2, provider, model })
12037
+ headers: {
12038
+ "Content-Type": "application/json",
12039
+ Authorization: `Bearer ${caller}`
12040
+ },
12041
+ body: JSON.stringify({ provider, model })
11983
12042
  });
11984
12043
  if (!response.ok) {
11985
12044
  throw new Error(`Agent authentication failed for ${provider}:${model}: ${response.status} ${response.statusText}`);
@@ -12000,12 +12059,12 @@ async function authenticateAgent(opts) {
12000
12059
  );
12001
12060
  }
12002
12061
  async function startAgentWorker(opts) {
12003
- const { group, gatewayBaseUrl: gatewayBaseUrl2, workerSecret: workerSecret2, contentReads: contentReads2, logger: logger2 } = opts;
12062
+ const { group, gatewayBaseUrl: gatewayBaseUrl2, credential: credential2, contentReads: contentReads2, logger: logger2 } = opts;
12004
12063
  const { inference } = group;
12005
12064
  const { protocol, host, port } = parseGatewayUrl(gatewayBaseUrl2);
12006
12065
  const { token: initialToken, did } = await authenticateAgent({
12007
12066
  gatewayBaseUrl: gatewayBaseUrl2,
12008
- workerSecret: workerSecret2,
12067
+ credential: credential2,
12009
12068
  provider: inference.type,
12010
12069
  model: inference.model,
12011
12070
  logger: logger2
@@ -12016,11 +12075,15 @@ async function startAgentWorker(opts) {
12016
12075
  const kb = {
12017
12076
  id: kbId,
12018
12077
  label: `${inference.type} / ${inference.model} @ ${host}`,
12019
- email: `agent@${host}`,
12020
12078
  endpoint
12021
12079
  };
12022
12080
  const storage = new InMemorySessionStorage();
12023
- setStoredSession(storage, kbId, { access: initialToken, refresh: "" });
12081
+ setStoredSession(storage, kbId, {
12082
+ access: initialToken,
12083
+ refresh: "",
12084
+ clientId: credential2.clientId,
12085
+ tokenEndpoint: credential2.issuer
12086
+ });
12024
12087
  const token$ = new import_rxjs2.BehaviorSubject(null);
12025
12088
  let session;
12026
12089
  const transport = new HttpTransport({
@@ -12042,7 +12105,7 @@ async function startAgentWorker(opts) {
12042
12105
  try {
12043
12106
  const { token } = await authenticateAgent({
12044
12107
  gatewayBaseUrl: gatewayBaseUrl2,
12045
- workerSecret: workerSecret2,
12108
+ credential: credential2,
12046
12109
  provider: inference.type,
12047
12110
  model: inference.model,
12048
12111
  logger: logger2
@@ -12131,8 +12194,17 @@ if (!gatewayPublicURL) {
12131
12194
  throw new Error("services.gateway.publicURL is required in ~/.semiontconfig");
12132
12195
  }
12133
12196
  var gatewayBaseUrl = gatewayPublicURL;
12134
- var workerSecret = process.env.SEMIONT_WORKER_SECRET ?? "";
12135
- var contentReads = archivistContentReads(envConfig);
12197
+ var issuerUrl = envConfig.services?.identity?.issuer;
12198
+ if (!issuerUrl) {
12199
+ throw new Error("services.identity.issuer is required: a worker authenticates at the knowledge base's issuer");
12200
+ }
12201
+ var clientId = process.env.SEMIONT_OIDC_CLIENT_ID;
12202
+ var clientSecret = process.env.SEMIONT_OIDC_CLIENT_SECRET;
12203
+ if (!clientId || !clientSecret) {
12204
+ throw new Error("SEMIONT_OIDC_CLIENT_ID and SEMIONT_OIDC_CLIENT_SECRET are required to authenticate as a service account");
12205
+ }
12206
+ var credential = { issuer: issuerUrl, clientId, clientSecret };
12207
+ var contentReads = archivistContentReads(envConfig, credential);
12136
12208
  var healthPort = 24100;
12137
12209
  var logger = createProcessLogger("worker");
12138
12210
  function clientKey(w) {
@@ -12176,7 +12248,7 @@ async function main() {
12176
12248
  });
12177
12249
  const workers = await Promise.all(
12178
12250
  Array.from(groups.values()).map(
12179
- (group) => startAgentWorker({ group, gatewayBaseUrl, workerSecret, contentReads, logger })
12251
+ (group) => startAgentWorker({ group, gatewayBaseUrl, credential, contentReads, logger })
12180
12252
  )
12181
12253
  );
12182
12254
  const health = createServer((req, res) => {