@semiont/make-meaning 0.5.25 → 0.5.27

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.
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { STALL_THRESHOLD_MS, FsJobQueue } from '@semiont/jobs';
2
2
  import { FilesystemViewStorage, resolveStorageUri, EventQuery, createEventStore } from '@semiont/event-sourcing';
3
- import { getResourceEntityTypes, getResourceId, getTargetSource, resourceId, getPrimaryRepresentation, decodeRepresentation, getBodySource, getTargetSelector, deriveViews, getTextPositionSelector, annotationId, errField, userId, generateUuid, getExactText, busRequest, cloneToken, baseMediaType, isSupportedMediaType, capabilitiesOf, didToAgent, assembleAnnotation, jobId, entityType, baseUrl, busLog, BRIDGED_CHANNELS, burstBuffer, textExtractionOf, chunkText, getPrimaryMediaType, extensionForMediaType, applyBodyOperations, softwareToAgent } from '@semiont/core';
3
+ import { getResourceEntityTypes, getResourceId, getTargetSource, resourceId, getPrimaryRepresentation, decodeRepresentation, getBodySource, getTargetSelector, deriveViews, getTextPositionSelector, annotationId, errField, userId, generateUuid, getExactText, busRequest, cloneToken, baseMediaType, isSupportedMediaType, capabilitiesOf, didToAgent, assembleAnnotation, isGenerationJobParams, jobId, entityType, baseUrl, busLog, BRIDGED_CHANNELS, burstBuffer, textExtractionOf, chunkText, getPrimaryMediaType, extensionForMediaType, applyBodyOperations, softwareToAgent } from '@semiont/core';
4
4
  import { recordGatherDegrade, withActorSpan, recordBusEmit, withSpan, SpanKind, registerJobQueueProvider, registerVectorIndexSizeProvider } from '@semiont/observability';
5
5
  import { createInferenceClient } from '@semiont/inference';
6
- import { getGraphDatabase } from '@semiont/graph';
6
+ import { compareByRecencyThenId, getGraphDatabase } from '@semiont/graph';
7
7
  import { WorkingTreeStore, createAnchoredTextStore, deriveStorageUri, anchoredTextStoreOverTransport, EXTRACTORS, calculateChecksum } from '@semiont/content';
8
8
  import { getEntityTypes, DEFAULT_ENTITY_TYPES } from '@semiont/ontology';
9
+ import { mergeByResource } from '@semiont/vectors';
9
10
  import { promises } from 'fs';
10
11
  import * as path3 from 'path';
11
12
  import { createGzip, createGunzip } from 'zlib';
@@ -9905,11 +9906,9 @@ async function createKnowledgeBase(eventStore, project, graphDb, eventBus, logge
9905
9906
  graph: graphDb,
9906
9907
  weaveProgress,
9907
9908
  smeltProgress,
9909
+ vectors: options.vectorStore,
9908
9910
  projectionsDir: project.projectionsDir
9909
9911
  };
9910
- if (options?.vectorStore) {
9911
- kb.vectors = options.vectorStore;
9912
- }
9913
9912
  return kb;
9914
9913
  }
9915
9914
  var PROJECTION_LAG_BACKOFF_MS = [25, 50, 100, 200];
@@ -9937,13 +9936,6 @@ var GraphContext = class {
9937
9936
  static async getResourceConnections(resourceId10, kb) {
9938
9937
  return kb.graph.getResourceConnections(resourceId10);
9939
9938
  }
9940
- /**
9941
- * Search resources by name (cross-resource query)
9942
- * Requires full-text search - must use graph database
9943
- */
9944
- static async searchResources(query, kb, limit) {
9945
- return kb.graph.searchResources(query, limit);
9946
- }
9947
9939
  /**
9948
9940
  * Build the unified knowledge graph for a resource's neighborhood:
9949
9941
  * resources AND annotations as typed nodes, typed/directional edges.
@@ -10064,6 +10056,18 @@ Format as a simple list, one suggestion per line.`;
10064
10056
  }
10065
10057
  return response.split("\n").map((line) => line.replace(/^[-*•]\s*/, "").trim()).filter((line) => line.length > 0).slice(0, 3);
10066
10058
  }
10059
+
10060
+ // src/graph-read-grace.ts
10061
+ async function resourceWithViewGrace(kb, rid) {
10062
+ const fromGraph = await kb.graph.getResource(rid).catch(() => null);
10063
+ if (fromGraph) return { resource: fromGraph, laggedBehindView: false };
10064
+ const view = await kb.views.get(rid);
10065
+ if (view) return { resource: view.resource, laggedBehindView: true };
10066
+ return { resource: null, laggedBehindView: false };
10067
+ }
10068
+
10069
+ // src/resource-context.ts
10070
+ var SEMANTIC_OVER_FETCH = 4;
10067
10071
  var ResourceContext = class _ResourceContext {
10068
10072
  /**
10069
10073
  * Get resource metadata from view storage
@@ -10076,38 +10080,80 @@ var ResourceContext = class _ResourceContext {
10076
10080
  return view.resource;
10077
10081
  }
10078
10082
  /**
10079
- * List resources, optionally filtered.
10083
+ * List resources, optionally filtered, as one page plus the size of the whole
10084
+ * match set. Every filter is applied before pagination on both paths — a
10085
+ * filter applied afterwards narrows the page rather than the match set, which
10086
+ * is how a search scoped to an entity type can come back empty while hundreds
10087
+ * of resources match.
10080
10088
  *
10081
- * When `search` is set, delegates to `kb.graph.searchResources`, which runs
10082
- * the name match in the graph engine instead of scanning every view in JS.
10083
- * The graph result is then narrowed by `archived` if requested.
10089
+ * When `search` is set, the entire query filtering, ordering and
10090
+ * pagination runs inside the graph engine.
10084
10091
  *
10085
- * When `search` is unset, falls back to scanning all materialized views.
10086
- * (TODO: also push the listing path through the graph for large KBs.)
10092
+ * When `search` is unset, the materialized views answer instead. They are the
10093
+ * barrier-stamped projection, so an unsearched listing is read-your-writes
10094
+ * where the graph is only eventually consistent.
10087
10095
  */
10088
- static async listResources(filters, kb) {
10089
- if (filters?.search) {
10090
- const matches = await kb.graph.searchResources(filters.search);
10091
- const filtered = filters.archived !== void 0 ? matches.filter((doc) => doc.archived === filters.archived) : matches;
10092
- return _ResourceContext.sortByDateDesc(filtered);
10096
+ static async listResources(filters, kb, semantic) {
10097
+ const { search: rawSearch, archived, entityType: entityType2, offset = 0, limit = 50 } = filters ?? {};
10098
+ const search = rawSearch?.trim() || void 0;
10099
+ if (search) {
10100
+ const lexical = await kb.graph.listResources({
10101
+ search,
10102
+ archived,
10103
+ entityTypes: entityType2 ? [entityType2] : void 0,
10104
+ offset,
10105
+ limit
10106
+ });
10107
+ if (lexical.total > 0 || offset > 0) return { ...lexical, matchKind: "lexical" };
10108
+ return _ResourceContext.semanticFallback(search, limit, kb, semantic);
10093
10109
  }
10094
10110
  const allViews = await kb.views.getAll();
10095
- const resources = [];
10096
- for (const view of allViews) {
10097
- const doc = view.resource;
10098
- if (filters?.archived !== void 0 && doc.archived !== filters.archived) {
10099
- continue;
10100
- }
10101
- resources.push(doc);
10102
- }
10103
- return _ResourceContext.sortByDateDesc(resources);
10111
+ const matches = allViews.map((view) => view.resource).filter((doc) => archived === void 0 || doc.archived === archived).filter((doc) => !entityType2 || getResourceEntityTypes(doc).includes(entityType2)).sort(compareByRecencyThenId);
10112
+ return { resources: matches.slice(offset, offset + limit), total: matches.length, matchKind: "lexical" };
10104
10113
  }
10105
- static sortByDateDesc(resources) {
10106
- return [...resources].sort((a, b) => {
10107
- const aTime = a.dateCreated ? new Date(a.dateCreated).getTime() : 0;
10108
- const bTime = b.dateCreated ? new Date(b.dateCreated).getTime() : 0;
10109
- return bTime - aTime;
10110
- });
10114
+ /**
10115
+ * Answer an empty lexical search from the vector index (SEMANTIC-FALLBACK):
10116
+ * embed the query once, fold chunk hits per resource, floor them, and label
10117
+ * the answer 'semantic' so the UI can say "no title matches, but these
10118
+ * documents discuss it".
10119
+ *
10120
+ * Degradation is the contract (axioms S3–S5): unconfigured vectors, an
10121
+ * absent provider, or ANY failure inside the fallback yields the same
10122
+ * empty page the caller already had, labelled 'lexical' — a broken
10123
+ * fallback must never turn a working empty search into an error.
10124
+ *
10125
+ * The floor is applied HERE rather than passed as `scoreThreshold`, so the
10126
+ * below-floor hits exist to be counted — the debug line is the evidence
10127
+ * decision #1's guessed 0.6 gets tuned from.
10128
+ */
10129
+ static async semanticFallback(search, limit, kb, semantic) {
10130
+ const empty = { resources: [], total: 0, matchKind: "lexical" };
10131
+ try {
10132
+ const embedding = await semantic.embeddingProvider.embed(search);
10133
+ const hits = await kb.vectors.searchResources(embedding, { limit: limit * SEMANTIC_OVER_FETCH });
10134
+ const merged = mergeByResource(hits);
10135
+ const aboveFloor = merged.filter((h) => h.score >= semantic.semanticFloor);
10136
+ semantic.logger.debug("[search FALLBACK] semantic score distribution", {
10137
+ chunkHits: hits.length,
10138
+ resources: merged.length,
10139
+ aboveFloor: aboveFloor.length,
10140
+ belowFloor: merged.length - aboveFloor.length,
10141
+ topScore: merged[0]?.score,
10142
+ bottomScore: merged[merged.length - 1]?.score,
10143
+ floor: semantic.semanticFloor
10144
+ });
10145
+ const resources = [];
10146
+ for (const hit of aboveFloor.slice(0, limit)) {
10147
+ const { resource } = await resourceWithViewGrace(kb, hit.resourceId);
10148
+ if (resource) resources.push({ ...resource, content: hit.text });
10149
+ }
10150
+ return { resources, total: aboveFloor.length, matchKind: "semantic" };
10151
+ } catch (error) {
10152
+ semantic.logger.warn("[search FALLBACK] degraded to the empty lexical page", {
10153
+ reason: error instanceof Error ? error.message : String(error)
10154
+ });
10155
+ return empty;
10156
+ }
10111
10157
  }
10112
10158
  /**
10113
10159
  * Add content previews to resources (for search results)
@@ -10157,7 +10203,7 @@ var AnnotationContext = class {
10157
10203
  * @returns Rich context for LLM processing
10158
10204
  * @throws Error if annotation or resource not found
10159
10205
  */
10160
- static async buildLLMContext(annotationId5, resourceId10, kb, options = {}, inferenceClient, logger, embeddingProvider) {
10206
+ static async buildLLMContext(annotationId5, resourceId10, kb, embeddingProvider, options = {}, inferenceClient, logger) {
10161
10207
  const {
10162
10208
  includeSourceContext = true,
10163
10209
  includeTargetContext = true,
@@ -10289,7 +10335,7 @@ Summary:`;
10289
10335
  }
10290
10336
  }
10291
10337
  let semanticContext;
10292
- if (kb.vectors && embeddingProvider && sourceContext?.selected) {
10338
+ if (sourceContext?.selected) {
10293
10339
  try {
10294
10340
  const focalEmbedding = await embeddingProvider.embed(sourceContext.selected);
10295
10341
  const results = await kb.vectors.searchAnnotations(focalEmbedding, {
@@ -10613,46 +10659,44 @@ var LLMContext = class {
10613
10659
  if (options.includeContent) content.related = relatedContent;
10614
10660
  let semanticContext;
10615
10661
  const vectors = kb.vectors;
10616
- if (vectors) {
10617
- const excludeEntityTypes = options.excludeEntityTypes ?? [];
10618
- const search = () => vectors.searchByResource(resourceId10, {
10619
- limit: options.maxResources,
10620
- scoreThreshold: 0.5,
10621
- ...excludeEntityTypes.length ? { filter: { excludeEntityTypes } } : {}
10622
- });
10623
- let matches = await search();
10624
- if (matches.length === 0) {
10625
- const contentChecksum = getPrimaryRepresentation(mainDoc)?.checksum;
10626
- if (contentChecksum) {
10627
- try {
10628
- const outcome = await kb.smeltProgress.whenSettled(resourceIdStr, contentChecksum, settleTimeoutMs);
10629
- if (outcome === "indexed") {
10630
- matches = await search();
10631
- }
10632
- } catch (error) {
10633
- if (!(error instanceof SmeltProgressTimeout)) throw error;
10634
- recordGatherDegrade("vectors");
10635
- logger.warn("[gather DEGRADED] semanticContext absent \u2014 the vector projection did not settle within the barrier", {
10636
- resourceId: resourceIdStr,
10637
- contentChecksum,
10638
- timeoutMs: settleTimeoutMs
10639
- });
10640
- }
10662
+ const excludeEntityTypes = options.excludeEntityTypes ?? [];
10663
+ const search = () => vectors.searchByResource(resourceId10, {
10664
+ limit: options.maxResources,
10665
+ scoreThreshold: 0.5,
10666
+ ...excludeEntityTypes.length ? { filter: { excludeEntityTypes } } : {}
10667
+ });
10668
+ let matches = await search();
10669
+ if (matches.length === 0) {
10670
+ const contentChecksum = getPrimaryRepresentation(mainDoc)?.checksum;
10671
+ if (contentChecksum) {
10672
+ try {
10673
+ const outcome = await kb.smeltProgress.whenSettled(resourceIdStr, contentChecksum, settleTimeoutMs);
10674
+ if (outcome === "indexed") {
10675
+ matches = await search();
10676
+ }
10677
+ } catch (error) {
10678
+ if (!(error instanceof SmeltProgressTimeout)) throw error;
10679
+ recordGatherDegrade("vectors");
10680
+ logger.warn("[gather DEGRADED] semanticContext absent \u2014 the vector projection did not settle within the barrier", {
10681
+ resourceId: resourceIdStr,
10682
+ contentChecksum,
10683
+ timeoutMs: settleTimeoutMs
10684
+ });
10641
10685
  }
10642
10686
  }
10643
- if (matches.length > 0) {
10644
- semanticContext = {
10645
- similar: matches.map((m) => ({
10646
- text: m.text,
10647
- resourceId: m.resourceId,
10648
- ...m.annotationId ? { annotationId: m.annotationId } : {},
10649
- score: m.score,
10650
- ...m.entityTypes ? { entityTypes: m.entityTypes } : {},
10651
- ...m.machineRead ? { machineRead: true } : {}
10652
- })),
10653
- ...excludeEntityTypes.length ? { excludedEntityTypes: excludeEntityTypes } : {}
10654
- };
10655
- }
10687
+ }
10688
+ if (matches.length > 0) {
10689
+ semanticContext = {
10690
+ similar: matches.map((m) => ({
10691
+ text: m.text,
10692
+ resourceId: m.resourceId,
10693
+ ...m.annotationId ? { annotationId: m.annotationId } : {},
10694
+ score: m.score,
10695
+ ...m.entityTypes ? { entityTypes: m.entityTypes } : {},
10696
+ ...m.machineRead ? { machineRead: true } : {}
10697
+ })),
10698
+ ...excludeEntityTypes.length ? { excludedEntityTypes: excludeEntityTypes } : {}
10699
+ };
10656
10700
  }
10657
10701
  return {
10658
10702
  focus: {
@@ -10730,10 +10774,10 @@ var Gatherer = class {
10730
10774
  annotationId(event.annotationId),
10731
10775
  resourceId(event.resourceId),
10732
10776
  this.kb,
10777
+ this.embeddingProvider,
10733
10778
  event.options ?? {},
10734
10779
  this.inferenceClient,
10735
- this.logger,
10736
- this.embeddingProvider
10780
+ this.logger
10737
10781
  );
10738
10782
  this.eventBus.get("gather:complete").next({
10739
10783
  correlationId: event.correlationId,
@@ -10802,17 +10846,6 @@ var Gatherer = class {
10802
10846
  // src/matcher.ts
10803
10847
  var import_rxjs2 = __toESM(require_cjs());
10804
10848
  var import_operators2 = __toESM(require_operators());
10805
-
10806
- // src/graph-read-grace.ts
10807
- async function resourceWithViewGrace(kb, rid) {
10808
- const fromGraph = await kb.graph.getResource(rid).catch(() => null);
10809
- if (fromGraph) return { resource: fromGraph, laggedBehindView: false };
10810
- const view = await kb.views.get(rid);
10811
- if (view) return { resource: view.resource, laggedBehindView: true };
10812
- return { resource: null, laggedBehindView: false };
10813
- }
10814
-
10815
- // src/matcher.ts
10816
10849
  var Matcher = class {
10817
10850
  constructor(kb, eventBus, logger, inferenceClient, embeddingProvider) {
10818
10851
  this.kb = kb;
@@ -10885,7 +10918,7 @@ var Matcher = class {
10885
10918
  * Context-driven search: multi-source retrieval + composite scoring
10886
10919
  *
10887
10920
  * Retrieval sources:
10888
- * 1. Name match — graph.searchResources(searchTerm)
10921
+ * 1. Name match — graph.listResources({ search: searchTerm })
10889
10922
  * 2. Entity type match — graph.listResources({ entityTypes })
10890
10923
  * 3. Graph neighborhood — connections from GatheredContext
10891
10924
  *
@@ -10901,7 +10934,7 @@ var Matcher = class {
10901
10934
  const views = deriveViews(context.graph, mainResourceId);
10902
10935
  const connections = views.connections;
10903
10936
  const [nameMatches, entityTypeMatches, semanticMatches] = await Promise.all([
10904
- this.kb.graph.searchResources(searchTerm),
10937
+ this.kb.graph.listResources({ search: searchTerm, limit: 20 }).then((r) => r.resources),
10905
10938
  annotationEntityTypes.length > 0 ? this.kb.graph.listResources({ entityTypes: annotationEntityTypes, limit: 50 }).then((r) => r.resources) : Promise.resolve([]),
10906
10939
  // 4. Semantic match — vector similarity search (if vectors configured)
10907
10940
  this.searchVectors(searchTerm)
@@ -11127,7 +11160,7 @@ For each candidate, output a line with the number and score, like:
11127
11160
  * from the scorer.
11128
11161
  */
11129
11162
  async searchVectors(searchTerm) {
11130
- if (!this.kb.vectors || !this.embeddingProvider || !searchTerm.trim()) return [];
11163
+ if (!searchTerm.trim()) return [];
11131
11164
  try {
11132
11165
  const embedding = await this.embeddingProvider.embed(searchTerm);
11133
11166
  const results = await this.kb.vectors.searchResources(embedding, {
@@ -11691,6 +11724,29 @@ var JOB_TYPES = [
11691
11724
  "tag-annotation",
11692
11725
  "generation"
11693
11726
  ];
11727
+ var inferencePairKey = (provider, model) => `${provider} ${model}`;
11728
+ function eachAdmittedInference(config, visit) {
11729
+ for (const jobType of JOB_TYPES) {
11730
+ try {
11731
+ visit(resolveWorkerInference(config, jobType), jobType);
11732
+ } catch {
11733
+ }
11734
+ }
11735
+ for (const actor of ["gatherer", "matcher"]) {
11736
+ try {
11737
+ visit(resolveActorInference(config, actor));
11738
+ } catch {
11739
+ }
11740
+ }
11741
+ }
11742
+ function deriveInferencePairs(config) {
11743
+ const pairs = /* @__PURE__ */ new Map();
11744
+ eachAdmittedInference(config, (inference) => {
11745
+ const key = inferencePairKey(inference.type, inference.model);
11746
+ if (!pairs.has(key)) pairs.set(key, inference);
11747
+ });
11748
+ return pairs;
11749
+ }
11694
11750
  function deriveAgentRoster(config) {
11695
11751
  const domain = config.site?.domain;
11696
11752
  if (!domain) {
@@ -11699,8 +11755,8 @@ function deriveAgentRoster(config) {
11699
11755
  );
11700
11756
  }
11701
11757
  const roster = /* @__PURE__ */ new Map();
11702
- const admit = (inference, jobType) => {
11703
- const key = `${inference.type}\0${inference.model}`;
11758
+ eachAdmittedInference(config, (inference, jobType) => {
11759
+ const key = inferencePairKey(inference.type, inference.model);
11704
11760
  let entry = roster.get(key);
11705
11761
  if (!entry) {
11706
11762
  entry = {
@@ -11710,19 +11766,7 @@ function deriveAgentRoster(config) {
11710
11766
  roster.set(key, entry);
11711
11767
  }
11712
11768
  if (jobType) entry.servesJobTypes.push(jobType);
11713
- };
11714
- for (const jobType of JOB_TYPES) {
11715
- try {
11716
- admit(resolveWorkerInference(config, jobType), jobType);
11717
- } catch {
11718
- }
11719
- }
11720
- for (const actor of ["gatherer", "matcher"]) {
11721
- try {
11722
- admit(resolveActorInference(config, actor));
11723
- } catch {
11724
- }
11725
- }
11769
+ });
11726
11770
  return [...roster.values()].map(
11727
11771
  ({ agent, servesJobTypes }) => servesJobTypes.length ? { agent, servesJobTypes } : { agent }
11728
11772
  );
@@ -11730,12 +11774,14 @@ function deriveAgentRoster(config) {
11730
11774
 
11731
11775
  // src/browser.ts
11732
11776
  var Browser = class {
11733
- constructor(views, kb, eventBus, project, config, logger) {
11777
+ constructor(views, kb, eventBus, project, config, limitsDiscovery, embeddingProvider, logger) {
11734
11778
  this.views = views;
11735
11779
  this.kb = kb;
11736
11780
  this.eventBus = eventBus;
11737
11781
  this.project = project;
11738
11782
  this.config = config;
11783
+ this.limitsDiscovery = limitsDiscovery;
11784
+ this.embeddingProvider = embeddingProvider;
11739
11785
  this.logger = logger;
11740
11786
  }
11741
11787
  views;
@@ -11743,6 +11789,8 @@ var Browser = class {
11743
11789
  eventBus;
11744
11790
  project;
11745
11791
  config;
11792
+ limitsDiscovery;
11793
+ embeddingProvider;
11746
11794
  subscriptions = [];
11747
11795
  logger;
11748
11796
  async initialize() {
@@ -11835,24 +11883,30 @@ var Browser = class {
11835
11883
  }
11836
11884
  async handleBrowseResources(event) {
11837
11885
  try {
11838
- let filteredDocs = await ResourceContext.listResources({
11839
- search: event.search,
11840
- archived: event.archived
11841
- }, this.kb);
11842
- if (event.entityType) {
11843
- filteredDocs = filteredDocs.filter((doc) => getResourceEntityTypes(doc).includes(event.entityType));
11844
- }
11845
11886
  const offset = event.offset ?? 0;
11846
11887
  const limit = event.limit ?? 50;
11847
- const paginatedDocs = filteredDocs.slice(offset, offset + limit);
11848
- const formattedDocs = event.search ? await ResourceContext.addContentPreviews(paginatedDocs, this.kb) : paginatedDocs;
11888
+ const result = await ResourceContext.listResources({
11889
+ search: event.search,
11890
+ archived: event.archived,
11891
+ entityType: event.entityType,
11892
+ offset,
11893
+ limit
11894
+ }, this.kb, {
11895
+ embeddingProvider: this.embeddingProvider,
11896
+ semanticFloor: this.config.search.semanticFloor,
11897
+ logger: this.logger
11898
+ });
11899
+ const formattedDocs = event.search && result.matchKind === "lexical" ? await ResourceContext.addContentPreviews(result.resources, this.kb) : result.resources;
11849
11900
  this.eventBus.get("browse:resources-result").next({
11850
11901
  correlationId: event.correlationId,
11851
11902
  response: {
11852
11903
  resources: formattedDocs,
11853
- total: filteredDocs.length,
11904
+ total: result.total,
11854
11905
  offset,
11855
- limit
11906
+ limit,
11907
+ // The producer of the answer labels it (P1b moved the label here
11908
+ // from a hardcoded 'lexical' when the fallback landed).
11909
+ matchKind: result.matchKind
11856
11910
  }
11857
11911
  });
11858
11912
  } catch (error) {
@@ -12061,7 +12115,7 @@ var Browser = class {
12061
12115
  }
12062
12116
  async handleBrowseAgents(event) {
12063
12117
  try {
12064
- const agents = deriveAgentRoster(this.config);
12118
+ const agents = await this.limitsDiscovery.enrich(deriveAgentRoster(this.config));
12065
12119
  this.eventBus.get("browse:agents-result").next({
12066
12120
  correlationId: event.correlationId,
12067
12121
  response: { agents }
@@ -12178,6 +12232,63 @@ var Browser = class {
12178
12232
  this.logger.info("Browser actor stopped");
12179
12233
  }
12180
12234
  };
12235
+ var LIMITS_ENRICH_BUDGET_MS = 1500;
12236
+ function createLimitsDiscovery(config, logger, options) {
12237
+ const clientFactory = createInferenceClient;
12238
+ const budgetMs = LIMITS_ENRICH_BUDGET_MS;
12239
+ const clients = /* @__PURE__ */ new Map();
12240
+ for (const [pair, inference] of deriveInferencePairs(config)) {
12241
+ try {
12242
+ clients.set(pair, clientFactory(inference, logger));
12243
+ } catch (error) {
12244
+ logger.debug("Limits discovery: client construction failed \u2014 pair enriches as absent", {
12245
+ pair,
12246
+ reason: error instanceof Error ? error.message : String(error)
12247
+ });
12248
+ }
12249
+ }
12250
+ const consult = async (pair, client) => {
12251
+ let timer2;
12252
+ try {
12253
+ const discovery = client.limits();
12254
+ const raced = await Promise.race([
12255
+ discovery,
12256
+ new Promise((resolve2) => {
12257
+ timer2 = setTimeout(() => resolve2(void 0), budgetMs);
12258
+ })
12259
+ ]);
12260
+ if (raced === void 0) {
12261
+ void discovery.catch(() => {
12262
+ });
12263
+ logger.debug("Limits discovery: consult exceeded budget \u2014 entry enriches as absent", { pair, budgetMs });
12264
+ return void 0;
12265
+ }
12266
+ return { contextTokens: raced.contextTokens, maxOutputTokens: raced.maxOutputTokens };
12267
+ } catch (error) {
12268
+ logger.debug("Limits discovery: consult failed \u2014 entry enriches as absent", {
12269
+ pair,
12270
+ reason: error instanceof Error ? error.message : String(error)
12271
+ });
12272
+ return void 0;
12273
+ } finally {
12274
+ clearTimeout(timer2);
12275
+ }
12276
+ };
12277
+ return {
12278
+ async enrich(entries) {
12279
+ const settled = await Promise.allSettled(entries.map(async (entry) => {
12280
+ const agent = entry.agent;
12281
+ if (agent["@type"] !== "Software" || !agent.provider || !agent.model) return entry;
12282
+ const pair = inferencePairKey(agent.provider, agent.model);
12283
+ const client = clients.get(pair);
12284
+ if (!client) return entry;
12285
+ const limits = await consult(pair, client);
12286
+ return limits ? { ...entry, limits } : entry;
12287
+ }));
12288
+ return settled.map((s, i) => s.status === "fulfilled" ? s.value : entries[i]);
12289
+ }
12290
+ };
12291
+ }
12181
12292
 
12182
12293
  // src/event-enrichment.ts
12183
12294
  function eventAnnotationId(event) {
@@ -12207,7 +12318,7 @@ function asBusRequestPrimitive(eventBus) {
12207
12318
  return {
12208
12319
  emit(channel, payload) {
12209
12320
  eventBus.get(channel).next(payload);
12210
- return Promise.resolve();
12321
+ return Promise.resolve(-1);
12211
12322
  },
12212
12323
  stream(channel) {
12213
12324
  return eventBus.get(channel).asObservable();
@@ -12656,6 +12767,38 @@ function registerJobCommandHandlers(eventBus, jobQueue, project, parentLogger) {
12656
12767
  throw new Error("_userId is required (injected by bus gateway)");
12657
12768
  }
12658
12769
  const user = parseDidUser(_userId);
12770
+ let effectiveResourceId;
12771
+ if (jobType === "generation") {
12772
+ if (resId !== void 0) {
12773
+ throw new Error(
12774
+ "generation job:create must omit resourceId \u2014 the context's focus is authoritative"
12775
+ );
12776
+ }
12777
+ const bag = params;
12778
+ if (bag && bag.referenceId !== void 0) {
12779
+ throw new Error(
12780
+ "generation job:create must omit params.referenceId \u2014 the context's focus is authoritative"
12781
+ );
12782
+ }
12783
+ if (!isGenerationJobParams(params)) {
12784
+ throw new Error(
12785
+ "generation params do not satisfy GenerationJobParams (title, storageUri, and context are required)"
12786
+ );
12787
+ }
12788
+ const focus = params.context.focus;
12789
+ const rid = focus?.kind === "resource" ? focus.resource?.["@id"] : focus?.kind === "annotation" ? focus.sourceResource?.["@id"] : void 0;
12790
+ if (typeof rid !== "string" || rid.length === 0) {
12791
+ throw new Error(
12792
+ "generation context has no usable focus \u2014 pass a GatheredContext produced by gather.resource(...) or gather.annotation(...)"
12793
+ );
12794
+ }
12795
+ effectiveResourceId = rid;
12796
+ } else {
12797
+ if (typeof resId !== "string" || resId.length === 0) {
12798
+ throw new Error(`${jobType} job:create requires resourceId`);
12799
+ }
12800
+ effectiveResourceId = resId;
12801
+ }
12659
12802
  const job = {
12660
12803
  status: "pending",
12661
12804
  metadata: {
@@ -12674,7 +12817,7 @@ function registerJobCommandHandlers(eventBus, jobQueue, project, parentLogger) {
12674
12817
  maxRetries: jobType === "generation" ? 0 : 1
12675
12818
  },
12676
12819
  params: {
12677
- resourceId: resourceId(resId),
12820
+ resourceId: resourceId(effectiveResourceId),
12678
12821
  ...params
12679
12822
  }
12680
12823
  };
@@ -12880,35 +13023,40 @@ async function createKnowledgeSystemFromConfig(project, config, eventBus, logger
12880
13023
  logger.info("Connecting to graph database", { type: graphConfig.type });
12881
13024
  const graphDb = await withStartupTimeout("Graph database", getGraphDatabase(graphConfig));
12882
13025
  const eventStore = createEventStore(project, eventBus, logger.child({ component: "event-store" }));
12883
- let vectorStore;
12884
- let embeddingProvider;
12885
13026
  const vectorsConfig = config.services.vectors;
12886
13027
  const embeddingConfig = config.services.embedding;
12887
- if (vectorsConfig && embeddingConfig) {
12888
- const { createVectorStore, createEmbeddingProvider } = await import('@semiont/vectors');
12889
- logger.info("Connecting to embedding provider", { type: embeddingConfig.type, model: embeddingConfig.model });
12890
- embeddingProvider = await withStartupTimeout(
12891
- "Embedding provider",
12892
- createEmbeddingProvider(embeddingConfig)
12893
- );
12894
- logger.info("Connecting to vector store", { type: vectorsConfig.type ?? "qdrant" });
12895
- vectorStore = await withStartupTimeout(
12896
- "Vector store",
12897
- createVectorStore({
12898
- type: vectorsConfig.type ?? "qdrant",
12899
- host: vectorsConfig.host,
12900
- port: vectorsConfig.port,
12901
- dimensions: embeddingProvider.dimensions()
12902
- })
12903
- );
12904
- logger.info("Vector search initialized", {
12905
- store: vectorsConfig.type,
12906
- embedding: embeddingConfig.type,
12907
- model: embeddingConfig.model
12908
- });
12909
- const store = vectorStore;
12910
- registerVectorIndexSizeProvider(() => store.count());
13028
+ const { createVectorStore, createEmbeddingProvider } = await import('@semiont/vectors');
13029
+ logger.info("Connecting to embedding provider", { type: embeddingConfig.type, model: embeddingConfig.model });
13030
+ const embeddingProvider = await withStartupTimeout(
13031
+ "Embedding provider",
13032
+ createEmbeddingProvider(embeddingConfig)
13033
+ );
13034
+ logger.info("Connecting to vector store", { type: vectorsConfig.type });
13035
+ const vectorStore = await withStartupTimeout(
13036
+ "Vector store",
13037
+ createVectorStore({
13038
+ type: vectorsConfig.type,
13039
+ host: vectorsConfig.host,
13040
+ port: vectorsConfig.port,
13041
+ // Dimensionality is discovered from the provider, so it is passed as a
13042
+ // thunk rather than probed here: the store calls it only if it needs it
13043
+ // (Qdrant, and only to CREATE a collection). This matches how inference
13044
+ // treats provider-derived facts — the client is built with no I/O and
13045
+ // `limits()` are discovered at the point of use — instead of making a
13046
+ // network round-trip a precondition of booting. A `memory` store, or a
13047
+ // Qdrant whose collections already exist, never consults the provider.
13048
+ dimensions: () => embeddingProvider.dimensions()
13049
+ })
13050
+ );
13051
+ if (vectorsConfig.type === "memory") {
13052
+ logger.info("memory vector store: the index rebuilds from the event log on every restart (reconcile re-embeds)");
12911
13053
  }
13054
+ logger.info("Vector search initialized", {
13055
+ store: vectorsConfig.type,
13056
+ embedding: embeddingConfig.type,
13057
+ model: embeddingConfig.model
13058
+ });
13059
+ registerVectorIndexSizeProvider(() => vectorStore.count());
12912
13060
  const kb = await createKnowledgeBase(eventStore, project, graphDb, eventBus, logger, {
12913
13061
  vectorStore,
12914
13062
  skipRebuild
@@ -12940,7 +13088,8 @@ async function createKnowledgeSystemFromConfig(project, config, eventBus, logger
12940
13088
  embeddingProvider
12941
13089
  );
12942
13090
  await matcher.initialize();
12943
- const browser = new Browser(kb.views, kb, eventBus, project, config, logger.child({ component: "browser" }));
13091
+ const limitsDiscovery = createLimitsDiscovery(config, logger.child({ component: "limits-discovery" }));
13092
+ const browser = new Browser(kb.views, kb, eventBus, project, config, limitsDiscovery, embeddingProvider, logger.child({ component: "browser" }));
12944
13093
  await browser.initialize();
12945
13094
  const cloneTokenManager = new CloneTokenManager(kb, eventBus, logger.child({ component: "clone-token-manager" }));
12946
13095
  await cloneTokenManager.initialize();
@@ -13023,6 +13172,7 @@ var LocalTransport = class {
13023
13172
  }
13024
13173
  }
13025
13174
  );
13175
+ return -1;
13026
13176
  }
13027
13177
  on(channel, handler) {
13028
13178
  const sub = this.bus.get(channel).subscribe(handler);
@@ -14250,7 +14400,6 @@ async function replayEvent(event, eventBus, eventStore, resolveBlob, contentStor
14250
14400
  break;
14251
14401
  // Job events are transient — skip during replay
14252
14402
  case "job:started":
14253
- case "job:progress":
14254
14403
  case "job:completed":
14255
14404
  case "job:failed":
14256
14405
  logger?.debug("Skipping job event during replay", { type: event.type });