@remit/imap-worker 0.0.59 → 0.0.61

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/imap-worker",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -15,6 +15,7 @@ import {
15
15
  type PlacementMoveService,
16
16
  } from "@remit/mailbox-service";
17
17
  import { buildFilterConfig, type FilterConfigDeps } from "./filter-config.js";
18
+ import { _resetMessageEmbedderForTest } from "./message-embedder.js";
18
19
 
19
20
  const anchorOnlyFilter = (destinationMailboxId: string): FilterItem =>
20
21
  ({
@@ -117,3 +118,54 @@ describe("buildFilterConfig", () => {
117
118
  assert.equal(decision.move, undefined);
118
119
  });
119
120
  });
121
+
122
+ // An instance with semantic search off (issue #1068). `off` selects an embedder
123
+ // that throws by design, and the throw lands in the pipeline's per-filter catch:
124
+ // one error-level `filter_anchor_match_failed` for every semantic filter on
125
+ // every message it syncs. The pipeline already has a designed skip for a
126
+ // missing embedder, so the config passes none.
127
+ describe("buildFilterConfig with semantic search off", () => {
128
+ const withProviderOff = async (body: () => Promise<void>): Promise<void> => {
129
+ const previous = process.env.SEARCH_EMBEDDING_PROVIDER;
130
+ process.env.SEARCH_EMBEDDING_PROVIDER = "off";
131
+ _resetMessageEmbedderForTest();
132
+ try {
133
+ await body();
134
+ } finally {
135
+ if (previous === undefined) {
136
+ delete process.env.SEARCH_EMBEDDING_PROVIDER;
137
+ } else {
138
+ process.env.SEARCH_EMBEDDING_PROVIDER = previous;
139
+ }
140
+ _resetMessageEmbedderForTest();
141
+ }
142
+ };
143
+
144
+ it("wires no embedder", async () => {
145
+ await withProviderOff(async () => {
146
+ const config = buildFilterConfig(repos(anchorOnlyFilter("mbx-archive")));
147
+
148
+ assert.ok(config);
149
+ assert.equal(config.embedder, undefined);
150
+ });
151
+ });
152
+
153
+ it("skips the semantic filter without logging an error", async () => {
154
+ await withProviderOff(async () => {
155
+ const config = buildFilterConfig(repos(anchorOnlyFilter("mbx-archive")));
156
+ assert.ok(config);
157
+
158
+ const errors: unknown[] = [];
159
+ const decision = await new FilterPipeline(config, {
160
+ info: () => {},
161
+ debug: () => {},
162
+ warn: () => {},
163
+ error: (fields) => errors.push(fields.alert),
164
+ }).evaluate("cfg-1", "m-off", message);
165
+
166
+ assert.deepEqual(decision.labels, []);
167
+ assert.equal(decision.move, undefined);
168
+ assert.deepEqual(errors, []);
169
+ });
170
+ });
171
+ });
@@ -8,6 +8,10 @@ import type {
8
8
  MessageEmbedder,
9
9
  PlacementMoveService,
10
10
  } from "@remit/mailbox-service";
11
+ import {
12
+ EMBEDDING_PROVIDER_OFF,
13
+ readEmbeddingProviderFromEnv,
14
+ } from "@remit/search-service/from-env";
11
15
  import { getMessageEmbedder } from "./message-embedder.js";
12
16
 
13
17
  export interface FilterConfigDeps {
@@ -17,6 +21,21 @@ export interface FilterConfigDeps {
17
21
  placementMoveService?: PlacementMoveService;
18
22
  }
19
23
 
24
+ /**
25
+ * The env-selected embedder, or none on an instance with semantic search off.
26
+ *
27
+ * `off` embeds nothing by design, and its embedder says so by throwing
28
+ * (EmbeddingDisabledError). The pipeline has a designed skip for a *missing*
29
+ * embedder — semantic filters are passed over and logged at debug — but a
30
+ * present embedder that throws lands in the per-filter catch instead, which is
31
+ * an error-level `filter_anchor_match_failed` for every semantic filter on
32
+ * every synced message. Same outcome, a log nobody can read.
33
+ */
34
+ const embedderFromEnv = (): MessageEmbedder | undefined =>
35
+ readEmbeddingProviderFromEnv() === EMBEDDING_PROVIDER_OFF
36
+ ? undefined
37
+ : getMessageEmbedder();
38
+
20
39
  /**
21
40
  * Assemble the index-time filter config the body-sync pass runs (RFC 034). The
22
41
  * embedder is provisioned from env exactly as the backend read-path and
@@ -33,11 +52,12 @@ export const buildFilterConfig = (
33
52
  ): FilterConfig | undefined => {
34
53
  const { placementMoveService } = deps;
35
54
  if (!placementMoveService) return undefined;
55
+ const resolved = embedder ?? embedderFromEnv();
36
56
  return {
37
57
  filterService: deps.filterService,
38
58
  filterAnchorService: deps.filterAnchorService,
39
59
  messageLabelService: deps.messageLabelService,
40
60
  placementMoveService,
41
- embedder: embedder ?? getMessageEmbedder(),
61
+ ...(resolved ? { embedder: resolved } : {}),
42
62
  };
43
63
  };
@@ -158,6 +158,8 @@ export const syncMessageBody = async (
158
158
  mailboxSpecialUse: mailboxSpecialUseRepository,
159
159
  quarantine: quarantineRepository,
160
160
  flagQueue: flagQueueService,
161
+ calendarSuggestion: calendarSuggestionService,
162
+ calendarUnitOfWork,
161
163
  } = await getClient();
162
164
 
163
165
  const account = await accountService.get(accountId);
@@ -274,6 +276,9 @@ export const syncMessageBody = async (
274
276
  attempts: receiveCount,
275
277
  },
276
278
  { flagQueueService },
279
+ // An invitation becomes a card the first time the message is seen,
280
+ // alongside the filters that run on the same pass (issue #1033).
281
+ { calendarSuggestionService, calendarUnitOfWork, filterService },
277
282
  );
278
283
 
279
284
  // Guard at the openBox choke point (epic #1281 invariants 3 & 5). The