@semiont/make-meaning 0.5.11 → 0.5.13

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 { createTomlConfigLoader, accessToken, baseUrl as baseUrl$1, burstBuffer, errField, resourceId, textExtractionOf, decodeRepresentation, busRequest, getResourceEntityTypes, getTargetSelector, getExactText, annotationId, getPrimaryMediaType, getPrimaryRepresentation } from '@semiont/core';
1
+ import { createTomlConfigLoader, accessToken, baseUrl as baseUrl$1, retryWithBackoff, isTransientFetchError, STARTUP_FETCH_RETRY, burstBuffer, errField, resourceId, textExtractionOf, decodeRepresentation, busRequest, getResourceEntityTypes, getTargetSelector, getExactText, annotationId, getPrimaryMediaType, getPrimaryRepresentation } from '@semiont/core';
2
2
  import { calculateChecksum } from '@semiont/content';
3
3
  import { createEmbeddingProvider, createVectorStore, chunkText } from '@semiont/vectors';
4
4
  import { registerVectorIndexSizeProvider, withActorSpan } from '@semiont/observability';
@@ -9732,8 +9732,11 @@ var SMELTER_CHANNELS = [
9732
9732
  "yield:updated",
9733
9733
  "yield:representation-added",
9734
9734
  "mark:archived",
9735
+ "mark:unarchived",
9735
9736
  "mark:added",
9736
- "mark:removed"
9737
+ "mark:removed",
9738
+ "mark:entity-tag-added",
9739
+ "mark:entity-tag-removed"
9737
9740
  ];
9738
9741
  function createSmelterActorStateUnit(options) {
9739
9742
  const { bus } = options;
@@ -9782,8 +9785,20 @@ function partitionByType(events) {
9782
9785
  }
9783
9786
 
9784
9787
  // src/smelter.ts
9788
+ function sameStringSet(a, b) {
9789
+ if (a.length !== b.length) return false;
9790
+ const set = new Set(a);
9791
+ return b.every((t) => set.has(t));
9792
+ }
9793
+ var WORK_ITEM_TYPES = /* @__PURE__ */ new Set([
9794
+ "smelt:embed",
9795
+ "smelt:restamp",
9796
+ "smelt:purge",
9797
+ "smelt:embed-annotation",
9798
+ "smelt:purge-annotation"
9799
+ ]);
9785
9800
  function isWorkItem(input) {
9786
- return input.type.startsWith("smelt:");
9801
+ return WORK_ITEM_TYPES.has(input.type);
9787
9802
  }
9788
9803
  var Smelter = class _Smelter {
9789
9804
  constructor(events$, vectorStore, embeddingProvider, content, bus, chunkingConfig2, timing, logger2) {
@@ -9947,16 +9962,26 @@ var Smelter = class _Smelter {
9947
9962
  case "mark:archived":
9948
9963
  await this.handleResourceArchived(event);
9949
9964
  break;
9965
+ case "mark:unarchived":
9966
+ await this.handleResourceUnarchived(event);
9967
+ break;
9950
9968
  case "mark:added":
9951
9969
  await this.handleAnnotationAdded(event);
9952
9970
  break;
9953
9971
  case "mark:removed":
9954
9972
  await this.handleAnnotationRemoved(event);
9955
9973
  break;
9974
+ case "mark:entity-tag-added":
9975
+ case "mark:entity-tag-removed":
9976
+ await this.restampResource(event);
9977
+ break;
9956
9978
  // Reconcile work items — same handlers, distinct provenance.
9957
9979
  case "smelt:embed":
9958
9980
  await this.embedResource(event, "Reconcile-indexed resource");
9959
9981
  break;
9982
+ case "smelt:restamp":
9983
+ await this.restampResource(event);
9984
+ break;
9960
9985
  case "smelt:purge":
9961
9986
  await this.handleResourcePurge(event);
9962
9987
  break;
@@ -9968,6 +9993,21 @@ var Smelter = class _Smelter {
9968
9993
  break;
9969
9994
  }
9970
9995
  }
9996
+ /**
9997
+ * Payload-only stamp refresh: re-read the resource's CURRENT entity types
9998
+ * (one code path — `resolveEntityTypes` — so any prior drift self-corrects
9999
+ * on first touch) and rewrite the stamp on its existing points. Never calls
10000
+ * the embedding provider (S13): content is unchanged by definition on every
10001
+ * path that lands here. A resource with no points is a no-op — the stamp
10002
+ * rides the next embed.
10003
+ */
10004
+ async restampResource(event) {
10005
+ const rid = event.resourceId;
10006
+ if (!rid) return;
10007
+ const entityTypes = await this.resolveEntityTypes(rid);
10008
+ await this.vectorStore.updateResourceEntityTypes(resourceId(rid), entityTypes);
10009
+ this.logger.info("Restamped resource entity types", { resourceId: rid, entityTypes });
10010
+ }
9971
10011
  async handleResourcePurge(event) {
9972
10012
  const rid = event.resourceId;
9973
10013
  if (!rid) return;
@@ -9985,16 +10025,30 @@ var Smelter = class _Smelter {
9985
10025
  async fetchEmbeddableText(resourceId$1) {
9986
10026
  try {
9987
10027
  const { data, contentType } = await this.content.getBinary(resourceId(resourceId$1));
10028
+ const bytes = Buffer.from(data);
10029
+ const checksum = calculateChecksum(bytes);
9988
10030
  if (textExtractionOf(contentType) !== "decode") {
9989
10031
  this.logger.debug("Skipping resource that does not decode as text", { resourceId: resourceId$1, contentType });
9990
- return null;
10032
+ return { kind: "skipped", checksum };
9991
10033
  }
9992
- const bytes = Buffer.from(data);
9993
10034
  const text = decodeRepresentation(bytes, contentType);
9994
- return text.trim() ? { text, checksum: calculateChecksum(bytes) } : null;
10035
+ return text.trim() ? { kind: "text", text, checksum } : { kind: "skipped", checksum };
9995
10036
  } catch (error) {
9996
10037
  this.logger.warn("Content unavailable for embedding", { resourceId: resourceId$1, error: errField(error) });
9997
- return null;
10038
+ return { kind: "unavailable" };
10039
+ }
10040
+ }
10041
+ /**
10042
+ * The Smelter's single outbound signal (SMELTER-AXIOMS D3 as amended by
10043
+ * SMELTER-INDEX-SYNC): a per-resource decision report for the barrier
10044
+ * fold. Best-effort — waiters degrade to their bounded timeout; a signal
10045
+ * failure must never fail the embed.
10046
+ */
10047
+ async emitSettled(resourceId, contentChecksum, outcome) {
10048
+ try {
10049
+ await this.bus.emit("smelt:settled", { resourceId, contentChecksum, outcome });
10050
+ } catch (error) {
10051
+ this.logger.warn("Failed to emit smelt:settled", { resourceId, outcome, error: errField(error) });
9998
10052
  }
9999
10053
  }
10000
10054
  /**
@@ -10018,9 +10072,16 @@ var Smelter = class _Smelter {
10018
10072
  const rid = event.resourceId;
10019
10073
  if (!rid) return;
10020
10074
  const fetched = await this.fetchEmbeddableText(rid);
10021
- if (!fetched) return;
10075
+ if (fetched.kind === "unavailable") return;
10076
+ if (fetched.kind === "skipped") {
10077
+ await this.emitSettled(rid, fetched.checksum, "skipped");
10078
+ return;
10079
+ }
10022
10080
  const chunks = chunkText(fetched.text, this.chunkingConfig);
10023
- if (chunks.length === 0) return;
10081
+ if (chunks.length === 0) {
10082
+ await this.emitSettled(rid, fetched.checksum, "skipped");
10083
+ return;
10084
+ }
10024
10085
  const entityTypes = await this.resolveEntityTypes(rid);
10025
10086
  const embeddings = await this.embeddingProvider.embedBatch(chunks);
10026
10087
  const embeddingChunks = chunks.map((t, i) => ({
@@ -10029,6 +10090,7 @@ var Smelter = class _Smelter {
10029
10090
  embedding: embeddings[i]
10030
10091
  }));
10031
10092
  await this.vectorStore.upsertResourceVectors(resourceId(rid), embeddingChunks, fetched.checksum, entityTypes);
10093
+ await this.emitSettled(rid, fetched.checksum, "indexed");
10032
10094
  this.logger.info(logMessage, { resourceId: rid, chunks: chunks.length });
10033
10095
  }
10034
10096
  async handleResourceArchived(event) {
@@ -10038,11 +10100,34 @@ var Smelter = class _Smelter {
10038
10100
  await this.vectorStore.deleteAnnotationVectorsForResource(resourceId(rid));
10039
10101
  this.logger.info("Deleted vectors for archived resource", { resourceId: rid });
10040
10102
  }
10103
+ /**
10104
+ * Restore what `handleResourceArchived` deleted, from CURRENT state: the
10105
+ * resource's vectors (media-gated, full-replace) and its current exact-text
10106
+ * annotations — the same catalog read `reconcile()` uses, so the live path
10107
+ * and a restart agree (bugs/smelter-misses-unarchive.md).
10108
+ */
10109
+ async handleResourceUnarchived(event) {
10110
+ const rid = event.resourceId;
10111
+ if (!rid) return;
10112
+ await this.embedResource(event, "Re-embedded unarchived resource");
10113
+ const { annotations } = await busRequest(
10114
+ this.bus,
10115
+ "browse:annotations-requested",
10116
+ { resourceId: rid }
10117
+ );
10118
+ for (const annotation of annotations) {
10119
+ await this.indexAnnotation(rid, annotation);
10120
+ }
10121
+ }
10041
10122
  async handleAnnotationAdded(event) {
10042
10123
  const annotation = event.payload.annotation;
10043
10124
  if (!annotation?.id) return;
10044
10125
  const rid = event.resourceId;
10045
10126
  if (!rid) return;
10127
+ await this.indexAnnotation(rid, annotation);
10128
+ }
10129
+ async indexAnnotation(rid, annotation) {
10130
+ if (!annotation.id) return;
10046
10131
  const selector = getTargetSelector(annotation.target);
10047
10132
  const exactText = getExactText(selector);
10048
10133
  if (!exactText?.trim()) return;
@@ -10076,9 +10161,16 @@ var Smelter = class _Smelter {
10076
10161
  const rid = event.resourceId;
10077
10162
  if (!rid) continue;
10078
10163
  const fetched = await this.fetchEmbeddableText(rid);
10079
- if (!fetched) continue;
10164
+ if (fetched.kind === "unavailable") continue;
10165
+ if (fetched.kind === "skipped") {
10166
+ await this.emitSettled(rid, fetched.checksum, "skipped");
10167
+ continue;
10168
+ }
10080
10169
  const chunks = chunkText(fetched.text, this.chunkingConfig);
10081
- if (chunks.length === 0) continue;
10170
+ if (chunks.length === 0) {
10171
+ await this.emitSettled(rid, fetched.checksum, "skipped");
10172
+ continue;
10173
+ }
10082
10174
  const entityTypes = await this.resolveEntityTypes(rid);
10083
10175
  resourceData.push({ rid: resourceId(rid), chunks, checksum: fetched.checksum, entityTypes });
10084
10176
  allChunks.push(...chunks);
@@ -10093,6 +10185,7 @@ var Smelter = class _Smelter {
10093
10185
  embedding: allEmbeddings[offset + i]
10094
10186
  }));
10095
10187
  await this.vectorStore.upsertResourceVectors(rid, embeddingChunks, checksum, entityTypes);
10188
+ await this.emitSettled(String(rid), checksum, "indexed");
10096
10189
  this.logger.info("Batch-indexed resource", { resourceId: String(rid), chunks: chunks.length });
10097
10190
  offset += chunks.length;
10098
10191
  }
@@ -10165,7 +10258,7 @@ var Smelter = class _Smelter {
10165
10258
  this._reconcileState = { phase: "running" };
10166
10259
  try {
10167
10260
  const [indexedResources, indexedAnnotations] = await Promise.all([
10168
- this.vectorStore.listResourceChecksums(),
10261
+ this.vectorStore.listResourceStamps(),
10169
10262
  this.vectorStore.listAnnotationIds()
10170
10263
  ]);
10171
10264
  const resources = await this.listAllResources();
@@ -10178,18 +10271,24 @@ var Smelter = class _Smelter {
10178
10271
  for (const resource of resources) {
10179
10272
  const mediaType = getPrimaryMediaType(resource);
10180
10273
  if (resource["@id"] && mediaType && textExtractionOf(mediaType) === "decode") {
10181
- embeddable.set(resource["@id"], getPrimaryRepresentation(resource)?.checksum);
10274
+ embeddable.set(resource["@id"], {
10275
+ checksum: getPrimaryRepresentation(resource)?.checksum,
10276
+ entityTypes: getResourceEntityTypes(resource)
10277
+ });
10182
10278
  }
10183
10279
  }
10184
10280
  const work = [];
10185
10281
  for (const rid of indexedResources.keys()) {
10186
10282
  if (!embeddable.has(rid)) work.push({ type: "smelt:purge", resourceId: rid, payload: {} });
10187
10283
  }
10188
- for (const [rid, catalogChecksum] of embeddable) {
10189
- if (!indexedResources.has(rid)) {
10284
+ for (const [rid, catalog] of embeddable) {
10285
+ const indexed = indexedResources.get(rid);
10286
+ if (!indexed) {
10190
10287
  work.push({ type: "smelt:embed", resourceId: rid, payload: {} });
10191
- } else if (catalogChecksum !== void 0 && indexedResources.get(rid) !== catalogChecksum) {
10288
+ } else if (catalog.checksum !== void 0 && indexed.contentChecksum !== catalog.checksum) {
10192
10289
  work.push({ type: "smelt:embed", resourceId: rid, payload: {} });
10290
+ } else if (!sameStringSet(indexed.entityTypes, catalog.entityTypes)) {
10291
+ work.push({ type: "smelt:restamp", resourceId: rid, payload: {} });
10193
10292
  }
10194
10293
  }
10195
10294
  const liveAnnotationIds = /* @__PURE__ */ new Set();
@@ -10218,6 +10317,7 @@ var Smelter = class _Smelter {
10218
10317
  await this.drain(work);
10219
10318
  const summary = {
10220
10319
  resourcesEmbedded: work.filter((w) => w.type === "smelt:embed").length,
10320
+ resourcesRestamped: work.filter((w) => w.type === "smelt:restamp").length,
10221
10321
  resourceVectorsDeleted: work.filter((w) => w.type === "smelt:purge").length,
10222
10322
  annotationsEmbedded: work.filter((w) => w.type === "smelt:embed-annotation").length,
10223
10323
  annotationVectorsDeleted: work.filter((w) => w.type === "smelt:purge-annotation").length
@@ -10306,20 +10406,34 @@ async function authenticate() {
10306
10406
  logger.warn("No SEMIONT_WORKER_SECRET set \u2014 using empty token");
10307
10407
  return "";
10308
10408
  }
10309
- const response = await fetch(`${baseUrl}/api/tokens/agent`, {
10310
- method: "POST",
10311
- headers: { "Content-Type": "application/json" },
10312
- body: JSON.stringify({
10313
- secret: workerSecret,
10314
- provider: embeddingType,
10315
- model: embeddingModel
10316
- })
10317
- });
10318
- if (!response.ok) {
10319
- throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
10320
- }
10321
- const { token } = await response.json();
10322
- return token;
10409
+ return retryWithBackoff(
10410
+ async () => {
10411
+ const response = await fetch(`${baseUrl}/api/tokens/agent`, {
10412
+ method: "POST",
10413
+ headers: { "Content-Type": "application/json" },
10414
+ body: JSON.stringify({
10415
+ secret: workerSecret,
10416
+ provider: embeddingType,
10417
+ model: embeddingModel
10418
+ })
10419
+ });
10420
+ if (!response.ok) {
10421
+ throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
10422
+ }
10423
+ const { token } = await response.json();
10424
+ return token;
10425
+ },
10426
+ isTransientFetchError,
10427
+ STARTUP_FETCH_RETRY,
10428
+ ({ attempt, attempts, delayMs, error }) => {
10429
+ logger.warn("Backend unreachable, retrying authentication", {
10430
+ attempt,
10431
+ attempts,
10432
+ retryInMs: delayMs,
10433
+ error: error instanceof Error ? error.message : String(error)
10434
+ });
10435
+ }
10436
+ );
10323
10437
  }
10324
10438
  async function main() {
10325
10439
  const { initObservabilityNode } = await import('@semiont/observability/node');