@semiont/jobs 0.5.28 → 0.5.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
- import { createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, busRequest, resourceId, getPrimaryMediaType, isGenerationJobParams, assembleAnnotation, findClaimSpan, textExtractionOf, reconcileSelector, GENERATABLE_MEDIA_TYPES, locate, createFragmentSelector, getLocaleEnglishName, estimateTokens, chunkText, isObject, isString, deriveViews } from '@semiont/core';
2
- import { anchoredTextStoreOverTransport, deriveStorageUri, extractPdfTextLayer, EXTRACTORS, calculateChecksum, withinByteBudget, MAX_PDF_BYTES } from '@semiont/content';
1
+ import { createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, busRequest, resourceId, getPrimaryMediaType, isGenerationJobParams, capabilitiesOf, assembleAnnotation, findClaimSpan, textExtractionOf, reconcileSelector, GENERATABLE_MEDIA_TYPES, locate, createFragmentSelector, getLocaleEnglishName, estimateTokens, chunkText, isObject, isString, deriveViews } from '@semiont/core';
2
+ import { archivistContentReads, extractPdfTextLayer, EXTRACTORS, calculateChecksum, withinByteBudget, MAX_PDF_BYTES } from '@semiont/content';
3
3
  import { withSpan, SpanKind, recordJobOutcome } from '@semiont/observability';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { existsSync, readFileSync, mkdtempSync, writeFileSync, rmSync } from 'fs';
6
6
  import { homedir, hostname, tmpdir } from 'os';
7
7
  import { join } from 'path';
8
8
  import { generateAnnotationId } from '@semiont/event-sourcing';
9
- import { InMemorySessionStorage, setStoredSession, kbBackendUrl, SemiontClient, SemiontSession } from '@semiont/sdk';
9
+ import { InMemorySessionStorage, setStoredSession, kbGatewayUrl, SemiontClient, SemiontSession } from '@semiont/sdk';
10
10
  import { HttpTransport, HttpContentTransport } from '@semiont/http-transport';
11
11
  import { createInferenceClient } from '@semiont/inference';
12
12
  import { createServer } from 'http';
@@ -10995,10 +10995,10 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
10995
10995
  }
10996
10996
 
10997
10997
  // src/workers/detection/prepare-detection.ts
10998
- async function prepareDetection(mediaType, session, resourceId, userId, generator, store) {
10998
+ async function prepareDetection(mediaType, content, resourceId, userId, generator, store) {
10999
10999
  const extractor = EXTRACTORS[textExtractionOf(mediaType)];
11000
11000
  if (!extractor) return { declined: "no-extractor" };
11001
- const { data } = await session.client.browse.resourceRepresentation(resourceId);
11001
+ const { data } = await content.getBinary(resourceId);
11002
11002
  const bytes = Buffer.from(data);
11003
11003
  const extracted = await extractor.extract(bytes, mediaType, {
11004
11004
  key: calculateChecksum(bytes),
@@ -11114,7 +11114,7 @@ async function handleJobInner(adapter, config, job) {
11114
11114
  const mediaType = getPrimaryMediaType(descriptor);
11115
11115
  const source = await withSpan(
11116
11116
  "detection:prepare",
11117
- () => prepareDetection(mediaType ?? "", session, resourceId$1, userId, generator, config.anchoredTextStore),
11117
+ () => prepareDetection(mediaType ?? "", config.contentReads, resourceId$1, userId, generator, config.anchoredTextStore),
11118
11118
  { attrs: { "resource.id": resourceId$1, "media.type": mediaType ?? "unknown" } }
11119
11119
  );
11120
11120
  if ("declined" in source) {
@@ -11243,7 +11243,16 @@ async function handleJobInner(adapter, config, job) {
11243
11243
  );
11244
11244
  const genParams = job.params;
11245
11245
  const genReferenceId = referenceIdOf(job);
11246
- const storageUri = deriveStorageUri(genResult.title, genResult.format);
11246
+ const storageUri = job.params.storageUri;
11247
+ const expectedExtension = capabilitiesOf(genResult.format)?.extension;
11248
+ if (expectedExtension && !storageUri.toLowerCase().endsWith(expectedExtension)) {
11249
+ config.logger.warn("Storage URI extension does not match the generated format \u2014 writing it as requested", {
11250
+ jobId,
11251
+ storageUri,
11252
+ format: genResult.format,
11253
+ expectedExtension
11254
+ });
11255
+ }
11247
11256
  const { resourceId: newResourceId } = await session.client.yield.resource({
11248
11257
  name: genResult.title,
11249
11258
  file: Buffer.from(genResult.content),
@@ -11328,6 +11337,29 @@ async function handleJobInner(adapter, config, job) {
11328
11337
  }
11329
11338
  }
11330
11339
 
11340
+ // src/anchored-text-over-bus.ts
11341
+ function anchoredTextOverBus(client, logger2) {
11342
+ return {
11343
+ async read(key) {
11344
+ try {
11345
+ return await client.browse.anchoredTextByChecksum(key);
11346
+ } catch (error) {
11347
+ logger2?.debug("Anchored-text consult failed \u2014 treating as miss", {
11348
+ key,
11349
+ reason: error instanceof Error ? error.message : String(error)
11350
+ });
11351
+ return null;
11352
+ }
11353
+ },
11354
+ async write(key) {
11355
+ logger2?.debug("Anchored-text write skipped \u2014 the Smelter is the sole writer", { key });
11356
+ },
11357
+ async list() {
11358
+ return [];
11359
+ }
11360
+ };
11361
+ }
11362
+
11331
11363
  // src/worker-runtime.ts
11332
11364
  var import_rxjs2 = __toESM(require_cjs());
11333
11365
  function buildHealthPayload(workers) {
@@ -11367,7 +11399,7 @@ function startStallWatchdog(opts) {
11367
11399
  timer.unref?.();
11368
11400
  return { dispose: () => clearInterval(timer) };
11369
11401
  }
11370
- function parseBackendUrl(url) {
11402
+ function parseGatewayUrl(url) {
11371
11403
  const parsed = new URL(url);
11372
11404
  const protocol = parsed.protocol.replace(":", "") === "https" ? "https" : "http";
11373
11405
  const host = parsed.hostname;
@@ -11375,13 +11407,13 @@ function parseBackendUrl(url) {
11375
11407
  return { protocol, host, port };
11376
11408
  }
11377
11409
  async function authenticateAgent(opts) {
11378
- const { backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, provider, model, logger: logger2, retry = STARTUP_FETCH_RETRY } = opts;
11410
+ const { gatewayBaseUrl: gatewayBaseUrl2, workerSecret: workerSecret2, provider, model, logger: logger2, retry = STARTUP_FETCH_RETRY } = opts;
11379
11411
  if (!workerSecret2) {
11380
11412
  throw new Error("SEMIONT_WORKER_SECRET is required to authenticate worker agents");
11381
11413
  }
11382
11414
  return retryWithBackoff(
11383
11415
  async () => {
11384
- const response = await fetch(`${backendBaseUrl2}/api/tokens/agent`, {
11416
+ const response = await fetch(`${gatewayBaseUrl2}/api/tokens/agent`, {
11385
11417
  method: "POST",
11386
11418
  headers: { "Content-Type": "application/json" },
11387
11419
  body: JSON.stringify({ secret: workerSecret2, provider, model })
@@ -11394,7 +11426,7 @@ async function authenticateAgent(opts) {
11394
11426
  isTransientFetchError,
11395
11427
  retry,
11396
11428
  ({ attempt, attempts, delayMs, error }) => {
11397
- logger2?.warn("Backend unreachable, retrying agent authentication", {
11429
+ logger2?.warn("Gateway unreachable, retrying agent authentication", {
11398
11430
  agent: `${provider}:${model}`,
11399
11431
  attempt,
11400
11432
  attempts,
@@ -11405,11 +11437,11 @@ async function authenticateAgent(opts) {
11405
11437
  );
11406
11438
  }
11407
11439
  async function startAgentWorker(opts) {
11408
- const { group, backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, logger: logger2 } = opts;
11440
+ const { group, gatewayBaseUrl: gatewayBaseUrl2, workerSecret: workerSecret2, contentReads: contentReads2, logger: logger2 } = opts;
11409
11441
  const { inference } = group;
11410
- const { protocol, host, port } = parseBackendUrl(backendBaseUrl2);
11442
+ const { protocol, host, port } = parseGatewayUrl(gatewayBaseUrl2);
11411
11443
  const { token: initialToken, did } = await authenticateAgent({
11412
- backendBaseUrl: backendBaseUrl2,
11444
+ gatewayBaseUrl: gatewayBaseUrl2,
11413
11445
  workerSecret: workerSecret2,
11414
11446
  provider: inference.type,
11415
11447
  model: inference.model,
@@ -11429,7 +11461,7 @@ async function startAgentWorker(opts) {
11429
11461
  const token$ = new import_rxjs2.BehaviorSubject(null);
11430
11462
  let session;
11431
11463
  const transport = new HttpTransport({
11432
- baseUrl: baseUrl(kbBackendUrl(endpoint)),
11464
+ baseUrl: baseUrl(kbGatewayUrl(endpoint)),
11433
11465
  token$,
11434
11466
  tokenRefresher: () => session.refresh().then((t) => t ?? null)
11435
11467
  });
@@ -11443,7 +11475,7 @@ async function startAgentWorker(opts) {
11443
11475
  refresh: async () => {
11444
11476
  try {
11445
11477
  const { token } = await authenticateAgent({
11446
- backendBaseUrl: backendBaseUrl2,
11478
+ gatewayBaseUrl: gatewayBaseUrl2,
11447
11479
  workerSecret: workerSecret2,
11448
11480
  provider: inference.type,
11449
11481
  model: inference.model,
@@ -11468,10 +11500,12 @@ async function startAgentWorker(opts) {
11468
11500
  jobTypes: group.jobTypes,
11469
11501
  inferenceClient: group.client,
11470
11502
  generator,
11471
- // The extraction seam's cache, over this worker's content transport
11472
- // (PERSIST-ANCHORS P2d). Built here because the client keeps its
11473
- // content transport private — this is where it is in hand.
11474
- anchoredTextStore: anchoredTextStoreOverTransport(content, logger2),
11503
+ // The extraction seam's cache, consulted over the bus and never written
11504
+ // (ANCHORED-TEXT-TO-SMELTER D2): the Smelter owns this store, the
11505
+ // Archivist answers the checksum-addressed read, and a worker that
11506
+ // misses extracts locally and discards.
11507
+ anchoredTextStore: anchoredTextOverBus(client, logger2),
11508
+ contentReads: contentReads2,
11475
11509
  logger: logger2
11476
11510
  });
11477
11511
  logger2.info("Agent ready", {
@@ -11527,12 +11561,13 @@ function resolveWorker(jobType) {
11527
11561
  `No inference config for worker '${jobType}' and no workers.default in ~/.semiontconfig.`
11528
11562
  );
11529
11563
  }
11530
- var backendPublicURL = envConfig.services?.backend?.publicURL;
11531
- if (!backendPublicURL) {
11532
- throw new Error("services.backend.publicURL is required in ~/.semiontconfig");
11564
+ var gatewayPublicURL = envConfig.services?.gateway?.publicURL;
11565
+ if (!gatewayPublicURL) {
11566
+ throw new Error("services.gateway.publicURL is required in ~/.semiontconfig");
11533
11567
  }
11534
- var backendBaseUrl = backendPublicURL;
11568
+ var gatewayBaseUrl = gatewayPublicURL;
11535
11569
  var workerSecret = process.env.SEMIONT_WORKER_SECRET ?? "";
11570
+ var contentReads = archivistContentReads(envConfig);
11536
11571
  var healthPort = 9090;
11537
11572
  var logger = createProcessLogger("worker");
11538
11573
  function clientKey(w) {
@@ -11566,7 +11601,7 @@ async function main() {
11566
11601
  const { initObservabilityNode } = await import('@semiont/observability/node');
11567
11602
  initObservabilityNode({ serviceName: "semiont-worker" });
11568
11603
  logger.info("Starting agents", {
11569
- baseUrl: backendBaseUrl,
11604
+ baseUrl: gatewayBaseUrl,
11570
11605
  agents: Array.from(groups.values()).map((g) => ({
11571
11606
  provider: g.inference.type,
11572
11607
  model: g.inference.model,
@@ -11575,7 +11610,7 @@ async function main() {
11575
11610
  });
11576
11611
  const workers = await Promise.all(
11577
11612
  Array.from(groups.values()).map(
11578
- (group) => startAgentWorker({ group, backendBaseUrl, workerSecret, logger })
11613
+ (group) => startAgentWorker({ group, gatewayBaseUrl, workerSecret, contentReads, logger })
11579
11614
  )
11580
11615
  );
11581
11616
  const health = createServer((req, res) => {