@agent-native/core 0.111.4 → 0.112.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.
Files changed (104) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +3 -1
  4. package/corpus/core/src/cli/code-agent-executor.ts +17 -5
  5. package/corpus/core/src/cli/code-agent-runs.ts +43 -2
  6. package/corpus/core/src/cli/multi-frontier-runs.ts +841 -0
  7. package/corpus/core/src/coding-tools/index.ts +27 -8
  8. package/corpus/core/src/embeddings/index.ts +233 -0
  9. package/corpus/core/src/index.ts +2 -0
  10. package/corpus/core/src/search/index.ts +413 -0
  11. package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +48 -5
  12. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +46 -14
  13. package/corpus/templates/brain/README.md +34 -3
  14. package/corpus/templates/brain/actions/approve-proposal.ts +2 -0
  15. package/corpus/templates/brain/actions/claim-distillation.ts +24 -27
  16. package/corpus/templates/brain/actions/create-source.ts +1 -1
  17. package/corpus/templates/brain/actions/get-knowledge.ts +6 -2
  18. package/corpus/templates/brain/actions/get-pilot-report.ts +44 -8
  19. package/corpus/templates/brain/actions/import-capture.ts +21 -9
  20. package/corpus/templates/brain/actions/import-transcript.ts +42 -14
  21. package/corpus/templates/brain/actions/list-captures.ts +10 -1
  22. package/corpus/templates/brain/actions/list-distillation-queue.ts +39 -14
  23. package/corpus/templates/brain/actions/list-projects.ts +51 -0
  24. package/corpus/templates/brain/actions/list-proposals.ts +80 -4
  25. package/corpus/templates/brain/actions/list-sources.ts +17 -3
  26. package/corpus/templates/brain/actions/manage-project.ts +118 -0
  27. package/corpus/templates/brain/actions/mark-capture-distilled.ts +68 -20
  28. package/corpus/templates/brain/actions/navigate.ts +1 -0
  29. package/corpus/templates/brain/actions/reject-proposal.ts +6 -1
  30. package/corpus/templates/brain/actions/resanitize-captures.ts +159 -33
  31. package/corpus/templates/brain/actions/review-proposal.ts +2 -0
  32. package/corpus/templates/brain/actions/search-everything.ts +10 -1
  33. package/corpus/templates/brain/actions/search-knowledge.ts +1 -1
  34. package/corpus/templates/brain/actions/set-settings.ts +36 -1
  35. package/corpus/templates/brain/actions/update-proposal.ts +2 -0
  36. package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +18 -9
  37. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +3 -0
  38. package/corpus/templates/brain/app/i18n-data.ts +731 -2
  39. package/corpus/templates/brain/app/lib/brain.ts +33 -1
  40. package/corpus/templates/brain/app/routes/ops.tsx +47 -0
  41. package/corpus/templates/brain/app/routes/review.tsx +41 -2
  42. package/corpus/templates/brain/app/routes/search.tsx +33 -2
  43. package/corpus/templates/brain/app/routes/settings.tsx +158 -0
  44. package/corpus/templates/brain/app/routes/sources.tsx +31 -0
  45. package/corpus/templates/brain/changelog/2026-07-19-added-private-source-safe-semantic-search.md +6 -0
  46. package/corpus/templates/brain/jobs/process-ingest-queue.ts +270 -36
  47. package/corpus/templates/brain/package.json +1 -0
  48. package/corpus/templates/brain/server/db/index.ts +18 -0
  49. package/corpus/templates/brain/server/db/schema.ts +197 -1
  50. package/corpus/templates/brain/server/jobs/distillation-queue.ts +12 -5
  51. package/corpus/templates/brain/server/lib/audiences.ts +708 -0
  52. package/corpus/templates/brain/server/lib/brain-health.ts +251 -38
  53. package/corpus/templates/brain/server/lib/brain.ts +877 -50
  54. package/corpus/templates/brain/server/lib/capture-sanitization.ts +217 -53
  55. package/corpus/templates/brain/server/lib/connectors.ts +556 -70
  56. package/corpus/templates/brain/server/lib/demo.ts +17 -4
  57. package/corpus/templates/brain/server/lib/hybrid-search.ts +363 -0
  58. package/corpus/templates/brain/server/lib/ingest-queue.ts +104 -0
  59. package/corpus/templates/brain/server/lib/privacy-readiness.ts +24 -0
  60. package/corpus/templates/brain/server/lib/search-index-contracts.ts +72 -0
  61. package/corpus/templates/brain/server/lib/search-index.ts +714 -0
  62. package/corpus/templates/brain/server/lib/search.ts +120 -4
  63. package/corpus/templates/brain/server/lib/sensitivity-policy.ts +174 -0
  64. package/corpus/templates/brain/server/lib/slack-events.ts +281 -0
  65. package/corpus/templates/brain/server/onboarding.ts +29 -0
  66. package/corpus/templates/brain/server/plugins/db.ts +204 -0
  67. package/corpus/templates/brain/server/register-secrets.ts +46 -0
  68. package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +36 -16
  69. package/corpus/templates/brain/server/routes/api/_agent-native/brain/slack-events.post.ts +28 -0
  70. package/corpus/templates/brain/shared/types.ts +10 -0
  71. package/dist/cli/code-agent-executor.d.ts +8 -9
  72. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  73. package/dist/cli/code-agent-executor.js +9 -3
  74. package/dist/cli/code-agent-executor.js.map +1 -1
  75. package/dist/cli/code-agent-runs.d.ts +5 -0
  76. package/dist/cli/code-agent-runs.d.ts.map +1 -1
  77. package/dist/cli/code-agent-runs.js +33 -2
  78. package/dist/cli/code-agent-runs.js.map +1 -1
  79. package/dist/cli/multi-frontier-runs.d.ts +131 -0
  80. package/dist/cli/multi-frontier-runs.d.ts.map +1 -0
  81. package/dist/cli/multi-frontier-runs.js +529 -0
  82. package/dist/cli/multi-frontier-runs.js.map +1 -0
  83. package/dist/coding-tools/index.d.ts +1 -0
  84. package/dist/coding-tools/index.d.ts.map +1 -1
  85. package/dist/coding-tools/index.js +25 -7
  86. package/dist/coding-tools/index.js.map +1 -1
  87. package/dist/collab/awareness.d.ts +2 -2
  88. package/dist/collab/awareness.d.ts.map +1 -1
  89. package/dist/embeddings/index.d.ts +24 -0
  90. package/dist/embeddings/index.d.ts.map +1 -0
  91. package/dist/embeddings/index.js +172 -0
  92. package/dist/embeddings/index.js.map +1 -0
  93. package/dist/index.d.ts +2 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +2 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/notifications/routes.d.ts +3 -3
  98. package/dist/progress/routes.d.ts +1 -1
  99. package/dist/resources/handlers.d.ts +1 -1
  100. package/dist/search/index.d.ts +88 -0
  101. package/dist/search/index.d.ts.map +1 -0
  102. package/dist/search/index.js +267 -0
  103. package/dist/search/index.js.map +1 -0
  104. package/package.json +3 -1
@@ -478,7 +478,7 @@ const retrievalEvalCases: RetrievalEvalCase[] = [
478
478
  "For a stale Fusion branch, should the agent checkout, reset, stash, or repair the branch automatically?",
479
479
  requiredTerms: [
480
480
  "fusion",
481
- ["do not", "don't", "never"],
481
+ ["do not", "don't", "never", "avoid"],
482
482
  "git checkout",
483
483
  "reset",
484
484
  ],
@@ -568,7 +568,6 @@ const retrievalEvalCases: RetrievalEvalCase[] = [
568
568
  label: "PII is redacted from retrieval output",
569
569
  question:
570
570
  "What does the #dev-fusion privacy redaction output say about durable escalation rotation?",
571
- expectedTitle: "#dev-fusion privacy redaction output",
572
571
  requiredTerms: [
573
572
  "durable escalation rotation",
574
573
  "[redacted]",
@@ -694,13 +693,27 @@ function evidence(
694
693
  const metadata = parseJson<Record<string, unknown>>(capture.metadataJson, {});
695
694
  return {
696
695
  captureId: capture.id,
697
- quote,
696
+ quote: exactPersistedEvidenceQuote(capture.content, quote),
698
697
  note,
699
698
  sourceUrl:
700
699
  typeof metadata.sourceUrl === "string" ? metadata.sourceUrl : undefined,
701
700
  };
702
701
  }
703
702
 
703
+ function exactPersistedEvidenceQuote(content: string, preferred: string) {
704
+ const exactPreferred = preferred.trim();
705
+ if (exactPreferred && content.includes(exactPreferred)) return exactPreferred;
706
+
707
+ const longestLine = content
708
+ .split(/\r?\n/)
709
+ .map((line) => line.trim())
710
+ .filter(Boolean)
711
+ .sort((a, b) => b.length - a.length)[0];
712
+ if (longestLine) return longestLine;
713
+
714
+ return content.slice(0, 280).trim();
715
+ }
716
+
704
717
  function serializeSeedCapture(
705
718
  row: typeof schema.brainRawCaptures.$inferSelect,
706
719
  ) {
@@ -1615,7 +1628,7 @@ export async function runBrainDemoEval(
1615
1628
  "pii-redaction",
1616
1629
  "PII is redacted before queryable storage",
1617
1630
  !!redacted &&
1618
- redacted.status === "redacted" &&
1631
+ ["published", "redacted"].includes(redacted.status) &&
1619
1632
  !redactedText.includes("ava.cho@example.com") &&
1620
1633
  !redactedText.includes("+1 415 555 1212"),
1621
1634
  redacted
@@ -0,0 +1,363 @@
1
+ import { getDbExec, isPostgres } from "@agent-native/core/db";
2
+ import {
3
+ availableEmbeddingFamilies,
4
+ defaultEmbeddingFamily,
5
+ } from "@agent-native/core/embeddings";
6
+ import {
7
+ queryPgVectorIndex,
8
+ queryPostgresFts,
9
+ } from "@agent-native/core/search";
10
+ import { normalizeSearchTerms } from "@agent-native/core/search-utils";
11
+ import { accessFilter } from "@agent-native/core/sharing";
12
+ import { and, desc, eq, inArray } from "drizzle-orm";
13
+
14
+ import type {
15
+ BrainCaptureKind,
16
+ BrainSourceProvider,
17
+ } from "../../shared/types.js";
18
+ import { getDb, schema } from "../db/index.js";
19
+ import { listAccessibleAudienceIds } from "./audiences.js";
20
+ import { BRAIN_SEARCH_INDEX_VERSION } from "./search-index-contracts.js";
21
+
22
+ export interface HybridCandidate {
23
+ id: string;
24
+ artifactId: string;
25
+ captureId: string;
26
+ sourceId: string;
27
+ audienceId: string;
28
+ title: string;
29
+ text: string;
30
+ capturedAt: string;
31
+ lexicalRank?: number;
32
+ semanticRank?: number;
33
+ authority?: number;
34
+ freshness?: number;
35
+ lane: "lexical" | "semantic" | "hybrid";
36
+ }
37
+
38
+ export interface HybridSearchResult extends HybridCandidate {
39
+ score: number;
40
+ reasons: string[];
41
+ }
42
+
43
+ export const RRF_K = 60;
44
+ const SEARCH_NAMESPACE = "brain";
45
+
46
+ export function reciprocalRankFusion(
47
+ candidates: HybridCandidate[],
48
+ k = RRF_K,
49
+ ): HybridSearchResult[] {
50
+ return candidates
51
+ .map((candidate) => {
52
+ const lexical = candidate.lexicalRank
53
+ ? 1 / (k + candidate.lexicalRank)
54
+ : 0;
55
+ const semantic = candidate.semanticRank
56
+ ? 1 / (k + candidate.semanticRank)
57
+ : 0;
58
+ const authority = candidate.authority ?? 0;
59
+ const freshness = candidate.freshness ?? 0;
60
+ const reasons = [
61
+ ...(candidate.lexicalRank ? ["lexical-match"] : []),
62
+ ...(candidate.semanticRank ? ["semantic-match"] : []),
63
+ ...(authority > 0 ? ["authority"] : []),
64
+ ...(freshness > 0 ? ["fresh"] : []),
65
+ ];
66
+ return {
67
+ ...candidate,
68
+ score: lexical + semantic + authority * 0.01 + freshness * 0.005,
69
+ reasons,
70
+ };
71
+ })
72
+ .sort(
73
+ (a, b) => b.score - a.score || b.capturedAt.localeCompare(a.capturedAt),
74
+ );
75
+ }
76
+
77
+ export function incrementalIdf(
78
+ term: string,
79
+ documents: readonly string[],
80
+ ): number {
81
+ const normalized = term.toLowerCase();
82
+ const count = documents.filter((document) =>
83
+ document.toLowerCase().includes(normalized),
84
+ ).length;
85
+ return Math.log((documents.length + 1) / (count + 1)) + 1;
86
+ }
87
+
88
+ export function lexicalScore(
89
+ text: string,
90
+ terms: string[],
91
+ corpus: readonly string[],
92
+ ): number {
93
+ const lower = text.toLowerCase();
94
+ return terms.reduce(
95
+ (score, term) =>
96
+ score + (lower.includes(term) ? incrementalIdf(term, corpus) : 0),
97
+ 0,
98
+ );
99
+ }
100
+
101
+ /** Audience membership is the first database predicate for every index lane. */
102
+ export async function hybridSearchArtifacts(input: {
103
+ query: string;
104
+ provider?: string;
105
+ kind?: string;
106
+ projectId?: string;
107
+ limit?: number;
108
+ rerank?: boolean;
109
+ }): Promise<HybridSearchResult[]> {
110
+ const terms = normalizeSearchTerms(input.query);
111
+ if (!terms.length) return [];
112
+ const db = getDb();
113
+ const projectSourceIds = input.projectId
114
+ ? (
115
+ await db
116
+ .select({ sourceId: schema.brainProjectSources.sourceId })
117
+ .from(schema.brainProjectSources)
118
+ .innerJoin(
119
+ schema.brainProjects,
120
+ eq(schema.brainProjects.id, schema.brainProjectSources.projectId),
121
+ )
122
+ .where(
123
+ and(
124
+ eq(schema.brainProjectSources.projectId, input.projectId),
125
+ accessFilter(schema.brainProjects, schema.brainProjectShares),
126
+ ),
127
+ )
128
+ ).map((row) => row.sourceId)
129
+ : undefined;
130
+ if (projectSourceIds && !projectSourceIds.length) return [];
131
+ const audienceIds = await listAccessibleAudienceIds(projectSourceIds);
132
+ if (!audienceIds.length) return [];
133
+ const baseFilter = and(
134
+ eq(schema.brainSearchArtifacts.status, "active"),
135
+ eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
136
+ eq(
137
+ schema.brainSearchArtifacts.contentHash,
138
+ schema.brainRawCaptures.contentHash,
139
+ ),
140
+ eq(
141
+ schema.brainSearchArtifacts.aclHash,
142
+ schema.brainRawCaptures.audienceAclHash,
143
+ ),
144
+ eq(
145
+ schema.brainSearchArtifacts.sensitivityPolicyVersion,
146
+ schema.brainRawCaptures.sensitivityPolicyVersion,
147
+ ),
148
+ eq(schema.brainSearchArtifacts.indexVersion, BRAIN_SEARCH_INDEX_VERSION),
149
+ inArray(schema.brainSearchArtifacts.audienceId, audienceIds),
150
+ accessFilter(schema.brainSources, schema.brainSourceShares),
151
+ projectSourceIds
152
+ ? inArray(schema.brainSearchArtifacts.sourceId, projectSourceIds)
153
+ : undefined,
154
+ input.provider
155
+ ? eq(schema.brainSources.provider, input.provider as BrainSourceProvider)
156
+ : undefined,
157
+ input.kind
158
+ ? eq(schema.brainRawCaptures.kind, input.kind as BrainCaptureKind)
159
+ : undefined,
160
+ );
161
+ const selectArtifacts = (artifactIds?: string[]) =>
162
+ db
163
+ .select({
164
+ id: schema.brainSearchArtifacts.id,
165
+ captureId: schema.brainSearchArtifacts.captureId,
166
+ sourceId: schema.brainSearchArtifacts.sourceId,
167
+ audienceId: schema.brainSearchArtifacts.audienceId,
168
+ title: schema.brainSearchArtifacts.title,
169
+ question: schema.brainSearchArtifacts.question,
170
+ summary: schema.brainSearchArtifacts.summary,
171
+ resolution: schema.brainSearchArtifacts.resolution,
172
+ capturedAt: schema.brainSearchArtifacts.capturedAt,
173
+ })
174
+ .from(schema.brainSearchArtifacts)
175
+ .innerJoin(
176
+ schema.brainSources,
177
+ eq(schema.brainSources.id, schema.brainSearchArtifacts.sourceId),
178
+ )
179
+ .innerJoin(
180
+ schema.brainRawCaptures,
181
+ eq(schema.brainRawCaptures.id, schema.brainSearchArtifacts.captureId),
182
+ )
183
+ .where(
184
+ and(
185
+ baseFilter,
186
+ artifactIds?.length
187
+ ? inArray(schema.brainSearchArtifacts.id, artifactIds)
188
+ : undefined,
189
+ ),
190
+ );
191
+ const recentRows = await selectArtifacts()
192
+ .orderBy(desc(schema.brainSearchArtifacts.capturedAt))
193
+ .limit(Math.max((input.limit ?? 25) * 5, 50));
194
+ let ftsRanks = new Map<string, number>();
195
+ let semanticRanks = new Map<string, number>();
196
+ if (isPostgres()) {
197
+ try {
198
+ const fts = await queryPostgresFts(getDbExec(), {
199
+ query: input.query,
200
+ allowedAudienceIds: audienceIds,
201
+ limit: Math.max((input.limit ?? 25) * 3, 30),
202
+ namespace: SEARCH_NAMESPACE,
203
+ });
204
+ ftsRanks = new Map(fts.map((hit, index) => [hit.chunkId, index + 1]));
205
+ const family = defaultEmbeddingFamily(await availableEmbeddingFamilies());
206
+ if (family) {
207
+ const [queryVector] = await family.embed(
208
+ [{ text: input.query }],
209
+ "query",
210
+ );
211
+ if (queryVector) {
212
+ const vectorHits = await queryPgVectorIndex(
213
+ getDbExec(),
214
+ {
215
+ embeddingSetId: family.id,
216
+ dimensions: family.dimensions,
217
+ vector: queryVector,
218
+ allowedAudienceIds: audienceIds,
219
+ limit: Math.max((input.limit ?? 25) * 3, 30),
220
+ namespace: SEARCH_NAMESPACE,
221
+ },
222
+ true,
223
+ );
224
+ const vectorKeys = vectorHits.map((hit) => hit.vectorKey);
225
+ const embeddingRows = vectorKeys.length
226
+ ? await db
227
+ .select({
228
+ vectorKey: schema.brainSearchEmbeddings.vectorKey,
229
+ targetType: schema.brainSearchEmbeddings.targetType,
230
+ targetId: schema.brainSearchEmbeddings.targetId,
231
+ })
232
+ .from(schema.brainSearchEmbeddings)
233
+ .where(
234
+ and(
235
+ eq(schema.brainSearchEmbeddings.status, "active"),
236
+ inArray(
237
+ schema.brainSearchEmbeddings.audienceId,
238
+ audienceIds,
239
+ ),
240
+ inArray(schema.brainSearchEmbeddings.vectorKey, vectorKeys),
241
+ ),
242
+ )
243
+ : [];
244
+ const burstIds = embeddingRows
245
+ .filter((row) => row.targetType === "burst")
246
+ .map((row) => row.targetId);
247
+ const burstArtifacts = burstIds.length
248
+ ? await db
249
+ .select({
250
+ id: schema.brainSearchBursts.id,
251
+ artifactId: schema.brainSearchBursts.artifactId,
252
+ })
253
+ .from(schema.brainSearchBursts)
254
+ .where(inArray(schema.brainSearchBursts.id, burstIds))
255
+ : [];
256
+ const burstToArtifact = new Map(
257
+ burstArtifacts.map((row) => [row.id, row.artifactId]),
258
+ );
259
+ const keyToArtifact = new Map(
260
+ embeddingRows.map((row) => [
261
+ row.vectorKey,
262
+ row.targetType === "artifact"
263
+ ? row.targetId
264
+ : burstToArtifact.get(row.targetId),
265
+ ]),
266
+ );
267
+ semanticRanks = new Map();
268
+ vectorHits.forEach((hit, index) => {
269
+ const artifactId = keyToArtifact.get(hit.vectorKey);
270
+ if (artifactId && !semanticRanks.has(artifactId))
271
+ semanticRanks.set(artifactId, index + 1);
272
+ });
273
+ }
274
+ }
275
+ } catch {
276
+ ftsRanks = new Map();
277
+ semanticRanks = new Map();
278
+ }
279
+ }
280
+ const externalIds = Array.from(
281
+ new Set([...ftsRanks.keys(), ...semanticRanks.keys()]),
282
+ );
283
+ const externalRows = externalIds.length
284
+ ? await selectArtifacts(externalIds)
285
+ : [];
286
+ const rowMap = new Map(
287
+ [...recentRows, ...externalRows].map((row) => [row.id, row]),
288
+ );
289
+ const rows = [...rowMap.values()];
290
+ const corpus = rows.map(
291
+ (row) => `${row.title} ${row.question} ${row.summary} ${row.resolution}`,
292
+ );
293
+ const lexicalCandidates = rows
294
+ .map((row) => ({
295
+ id: row.id,
296
+ artifactId: row.id,
297
+ captureId: row.captureId,
298
+ sourceId: row.sourceId,
299
+ audienceId: row.audienceId,
300
+ title: row.title,
301
+ text: `${row.question} ${row.summary} ${row.resolution}`,
302
+ capturedAt: row.capturedAt,
303
+ lexicalRank: undefined,
304
+ authority: 1,
305
+ freshness: Math.max(
306
+ 0,
307
+ 1 -
308
+ (Date.now() - Date.parse(row.capturedAt)) /
309
+ (1000 * 60 * 60 * 24 * 365),
310
+ ),
311
+ lane: "lexical" as const,
312
+ }))
313
+ .map((candidate) => ({
314
+ candidate,
315
+ lexical: lexicalScore(
316
+ `${candidate.title} ${candidate.text}`,
317
+ terms,
318
+ corpus,
319
+ ),
320
+ }))
321
+ .filter(({ lexical }) => lexical > 0)
322
+ .sort((a, b) => b.lexical - a.lexical)
323
+ .map(({ candidate }, index) => ({ ...candidate, lexicalRank: index + 1 }));
324
+ const lexicalRanks = new Map(
325
+ lexicalCandidates.map((candidate) => [candidate.id, candidate.lexicalRank]),
326
+ );
327
+ const candidates = rows
328
+ .filter(
329
+ (row) =>
330
+ lexicalRanks.has(row.id) ||
331
+ ftsRanks.has(row.id) ||
332
+ semanticRanks.has(row.id),
333
+ )
334
+ .map((row) => {
335
+ const lexicalRank = ftsRanks.get(row.id) ?? lexicalRanks.get(row.id);
336
+ const semanticRank = semanticRanks.get(row.id);
337
+ return {
338
+ id: row.id,
339
+ artifactId: row.id,
340
+ captureId: row.captureId,
341
+ sourceId: row.sourceId,
342
+ audienceId: row.audienceId,
343
+ title: row.title,
344
+ text: `${row.question} ${row.summary} ${row.resolution}`,
345
+ capturedAt: row.capturedAt,
346
+ lexicalRank,
347
+ semanticRank,
348
+ authority: 1,
349
+ freshness: Math.max(
350
+ 0,
351
+ 1 -
352
+ (Date.now() - Date.parse(row.capturedAt)) /
353
+ (1000 * 60 * 60 * 24 * 365),
354
+ ),
355
+ lane: semanticRank
356
+ ? lexicalRank
357
+ ? ("hybrid" as const)
358
+ : ("semantic" as const)
359
+ : ("lexical" as const),
360
+ };
361
+ });
362
+ return reciprocalRankFusion(candidates).slice(0, input.limit ?? 25);
363
+ }
@@ -0,0 +1,104 @@
1
+ import { and, eq, isNull, lt, ne, or } from "drizzle-orm";
2
+
3
+ import { getDb, schema } from "../db/index.js";
4
+ import { nanoid, nowIso, stableJson } from "./brain.js";
5
+ import type {
6
+ BrainCaptureInvalidation,
7
+ BrainIngestOperation,
8
+ } from "./search-index-contracts.js";
9
+
10
+ export async function enqueueBrainOperation(input: {
11
+ operation: BrainIngestOperation;
12
+ dedupeKey: string;
13
+ sourceId?: string | null;
14
+ captureId?: string | null;
15
+ priority?: number;
16
+ runAfter?: string | null;
17
+ payload?: Record<string, unknown>;
18
+ }) {
19
+ const db = getDb();
20
+ const now = nowIso();
21
+ const id = nanoid();
22
+ await db
23
+ .insert(schema.brainIngestQueue)
24
+ .values({
25
+ id,
26
+ sourceId: input.sourceId ?? null,
27
+ captureId: input.captureId ?? null,
28
+ operation: input.operation,
29
+ status: "queued",
30
+ priority: input.priority ?? 50,
31
+ attempts: 0,
32
+ payloadJson: stableJson(input.payload ?? {}),
33
+ dedupeKey: input.dedupeKey,
34
+ leaseToken: null,
35
+ leaseExpiresAt: null,
36
+ error: null,
37
+ runAfter: input.runAfter ?? null,
38
+ createdAt: now,
39
+ updatedAt: now,
40
+ })
41
+ .onConflictDoUpdate({
42
+ target: schema.brainIngestQueue.dedupeKey,
43
+ set: {
44
+ sourceId: input.sourceId ?? null,
45
+ captureId: input.captureId ?? null,
46
+ operation: input.operation,
47
+ status: "queued",
48
+ priority: input.priority ?? 50,
49
+ payloadJson: stableJson(input.payload ?? {}),
50
+ leaseToken: null,
51
+ leaseExpiresAt: null,
52
+ error: null,
53
+ runAfter: input.runAfter ?? null,
54
+ updatedAt: now,
55
+ },
56
+ setWhere: or(
57
+ ne(schema.brainIngestQueue.status, "processing"),
58
+ isNull(schema.brainIngestQueue.leaseExpiresAt),
59
+ lt(schema.brainIngestQueue.leaseExpiresAt, now),
60
+ ),
61
+ });
62
+ const [row] = await db
63
+ .select()
64
+ .from(schema.brainIngestQueue)
65
+ .where(eq(schema.brainIngestQueue.dedupeKey, input.dedupeKey))
66
+ .limit(1);
67
+ return row;
68
+ }
69
+
70
+ export async function enqueueCaptureInvalidation(
71
+ invalidation: BrainCaptureInvalidation,
72
+ ) {
73
+ const version =
74
+ invalidation.next?.contentHash ??
75
+ invalidation.previous?.contentHash ??
76
+ "deleted";
77
+ const operation = invalidation.next ? "search-index" : "search-unindex";
78
+ return enqueueBrainOperation({
79
+ operation,
80
+ dedupeKey: `${operation}:${invalidation.captureId}:${version}`,
81
+ sourceId: invalidation.sourceId,
82
+ captureId: invalidation.captureId,
83
+ priority: operation === "search-unindex" ? 10 : 40,
84
+ payload: { invalidation },
85
+ });
86
+ }
87
+
88
+ export async function markBrainOperationDone(id: string, leaseToken: string) {
89
+ return getDb()
90
+ .update(schema.brainIngestQueue)
91
+ .set({
92
+ status: "done",
93
+ leaseToken: null,
94
+ leaseExpiresAt: null,
95
+ updatedAt: nowIso(),
96
+ })
97
+ .where(
98
+ and(
99
+ eq(schema.brainIngestQueue.id, id),
100
+ eq(schema.brainIngestQueue.status, "processing"),
101
+ eq(schema.brainIngestQueue.leaseToken, leaseToken),
102
+ ),
103
+ );
104
+ }
@@ -0,0 +1,24 @@
1
+ import type { BrainSettings } from "../../shared/types.js";
2
+
3
+ export interface BrainPrivacyReadiness {
4
+ configured: boolean;
5
+ model: string | null;
6
+ engine: string | null;
7
+ warning: string | null;
8
+ }
9
+
10
+ export function brainPrivacyReadiness(
11
+ settings: BrainSettings,
12
+ ): BrainPrivacyReadiness {
13
+ const model = settings.privacyClassifierModel?.trim() || null;
14
+ const engine = settings.privacyClassifierEngine?.trim() || null;
15
+ const configured = Boolean(model && engine);
16
+ return {
17
+ configured,
18
+ model: configured ? model : null,
19
+ engine: configured ? engine : null,
20
+ warning: configured
21
+ ? null
22
+ : "No approved privacy classifier is configured. Deterministic-clean captures may be stored; uncertain captures are quarantined and deterministic hard-category captures are suppressed. Neither can be searched, cited, distilled, or exposed to agents.",
23
+ };
24
+ }
@@ -0,0 +1,72 @@
1
+ export const BRAIN_SEARCH_INDEX_VERSION = "1";
2
+ export const BRAIN_SENSITIVITY_POLICY_VERSION = "1";
3
+
4
+ export type BrainSensitivityCategory =
5
+ | "performance"
6
+ | "discipline"
7
+ | "termination"
8
+ | "layoff-reorg"
9
+ | "compensation"
10
+ | "recruiting"
11
+ | "health-accommodation"
12
+ | "investigation"
13
+ | "privileged-legal"
14
+ | "secret-credential"
15
+ | "personal";
16
+
17
+ export type BrainSensitivityDisposition =
18
+ | "allowed"
19
+ | "suppressed"
20
+ | "quarantined";
21
+
22
+ export interface BrainSafeSegment {
23
+ id: string;
24
+ authorKey?: string;
25
+ capturedAt: string;
26
+ sourceUrl?: string;
27
+ text: string;
28
+ reactionCount: number;
29
+ }
30
+
31
+ export interface BrainSensitivityDecision {
32
+ disposition: BrainSensitivityDisposition;
33
+ categories: BrainSensitivityCategory[];
34
+ confidenceBand: "deterministic" | "high" | "medium" | "uncertain";
35
+ policyVersion: string;
36
+ safeSegments: BrainSafeSegment[];
37
+ safeContent: string;
38
+ classifier: "deterministic" | "approved-model";
39
+ }
40
+
41
+ export interface BrainAudienceAssignment {
42
+ audienceId: string;
43
+ aclHash: string;
44
+ kind: "org" | "slack-private-channel" | "meeting" | "restricted";
45
+ }
46
+
47
+ export interface BrainSearchStalenessKey {
48
+ contentHash: string;
49
+ indexVersion: string;
50
+ sensitivityPolicyVersion: string;
51
+ aclHash: string;
52
+ }
53
+
54
+ export type BrainIngestOperation =
55
+ | "distill"
56
+ | "sync"
57
+ | "search-index"
58
+ | "search-unindex"
59
+ | "slack-thread-refresh";
60
+
61
+ export interface BrainCaptureInvalidation {
62
+ captureId: string;
63
+ sourceId: string;
64
+ reason:
65
+ | "content-changed"
66
+ | "sensitivity-changed"
67
+ | "access-changed"
68
+ | "source-deleted"
69
+ | "upstream-deleted";
70
+ previous?: Partial<BrainSearchStalenessKey>;
71
+ next?: Partial<BrainSearchStalenessKey>;
72
+ }