@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
@@ -4,15 +4,19 @@ import { and, desc, eq, inArray, like, or } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import { ensureCaptureAudience } from "../server/lib/audiences.js";
7
8
  import {
8
9
  contentHash,
9
10
  getAccessibleCapture,
11
+ invalidateDerivedForCapture,
10
12
  nowIso,
11
13
  parseJson,
12
14
  readBrainSettings,
15
+ recordBlockedCapture,
13
16
  stableJson,
14
17
  } from "../server/lib/brain.js";
15
18
  import { sanitizeCaptureForStorage } from "../server/lib/capture-sanitization.js";
19
+ import { enqueueCaptureInvalidation } from "../server/lib/ingest-queue.js";
16
20
  import { redactSensitiveText } from "../server/lib/search.js";
17
21
  import type { BrainCaptureKind, BrainSourceProvider } from "../shared/types.js";
18
22
  import { idSchema, stringArrayCliSchema } from "./_schemas.js";
@@ -21,6 +25,10 @@ function reviewPreview(value: string) {
21
25
  return redactSensitiveText(value).replace(/\s+/g, " ").trim().slice(0, 320);
22
26
  }
23
27
 
28
+ function canEditSource(role: "viewer" | "editor" | "admin" | "owner") {
29
+ return role === "editor" || role === "admin" || role === "owner";
30
+ }
31
+
24
32
  async function findDerivedCitationRefs(captureId: string) {
25
33
  const db = getDb();
26
34
  const needle = `%${captureId}%`;
@@ -95,35 +103,51 @@ export default defineAction({
95
103
  const source =
96
104
  sourceAccess.resource as typeof schema.brainSources.$inferSelect;
97
105
  const captures = await db
98
- .select()
106
+ .select({ id: schema.brainRawCaptures.id })
99
107
  .from(schema.brainRawCaptures)
100
108
  .where(
101
109
  args.includeNonTranscript
102
- ? eq(schema.brainRawCaptures.sourceId, source.id)
110
+ ? and(
111
+ eq(schema.brainRawCaptures.sourceId, source.id),
112
+ eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
113
+ )
103
114
  : and(
104
115
  eq(schema.brainRawCaptures.sourceId, source.id),
105
116
  eq(schema.brainRawCaptures.kind, "transcript"),
117
+ eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
106
118
  ),
107
119
  )
108
120
  .orderBy(desc(schema.brainRawCaptures.capturedAt))
109
121
  .limit(args.limit);
110
- for (const capture of captures) rows.push({ capture, source });
122
+ for (const candidate of captures) {
123
+ const access = await getAccessibleCapture(candidate.id);
124
+ if (!access) continue;
125
+ rows.push({ capture: access.capture, source: access.source });
126
+ }
111
127
  }
112
128
 
113
129
  if (args.captureIds?.length) {
114
130
  const captures = await db
115
- .select()
131
+ .select({ id: schema.brainRawCaptures.id })
116
132
  .from(schema.brainRawCaptures)
117
- .where(inArray(schema.brainRawCaptures.id, args.captureIds))
133
+ .where(
134
+ and(
135
+ inArray(schema.brainRawCaptures.id, args.captureIds),
136
+ eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
137
+ ),
138
+ )
118
139
  .limit(args.captureIds.length);
119
- for (const capture of captures) {
120
- if (!args.includeNonTranscript && capture.kind !== "transcript") {
140
+ for (const candidate of captures) {
141
+ const access = await getAccessibleCapture(candidate.id);
142
+ if (!access || !canEditSource(access.role)) continue;
143
+ if (
144
+ !args.includeNonTranscript &&
145
+ access.capture.kind !== "transcript"
146
+ ) {
121
147
  continue;
122
148
  }
123
- const access = await getAccessibleCapture(capture.id);
124
- if (!access) continue;
125
149
  rows.push({
126
- capture,
150
+ capture: access.capture,
127
151
  source: access.source,
128
152
  });
129
153
  }
@@ -139,26 +163,6 @@ export default defineAction({
139
163
  const hasDerivedRefs =
140
164
  derivedRefs.knowledgeIds.length > 0 ||
141
165
  derivedRefs.proposalIds.length > 0;
142
- if (!args.dryRun && hasDerivedRefs && !args.allowCitationDrift) {
143
- results.push({
144
- id: row.capture.id,
145
- sourceId: row.capture.sourceId,
146
- externalId: row.capture.externalId,
147
- title: row.capture.title,
148
- capturedAt: row.capture.capturedAt,
149
- beforeLength,
150
- afterLength: beforeLength,
151
- method: "skipped-derived-citations",
152
- rawContentRetained: true,
153
- skipped: true,
154
- skipReason:
155
- "Capture already has derived knowledge/proposals citing its current text. Run with dryRun first, then re-distill or pass allowCitationDrift=true intentionally.",
156
- dependentKnowledgeIds: derivedRefs.knowledgeIds,
157
- dependentProposalIds: derivedRefs.proposalIds,
158
- preview: reviewPreview(row.capture.content),
159
- });
160
- continue;
161
- }
162
166
  const sanitized = await sanitizeCaptureForStorage({
163
167
  kind: row.capture.kind as BrainCaptureKind,
164
168
  title: row.capture.title,
@@ -183,17 +187,139 @@ export default defineAction({
183
187
  const sanitizer = sanitized.metadata.captureSanitization as
184
188
  | Record<string, unknown>
185
189
  | undefined;
190
+ if (hasDerivedRefs && !args.allowCitationDrift) {
191
+ results.push({
192
+ id: row.capture.id,
193
+ sourceId: row.capture.sourceId,
194
+ externalId: row.capture.externalId,
195
+ title: sanitized.title,
196
+ capturedAt: row.capture.capturedAt,
197
+ beforeLength,
198
+ afterLength: sanitized.content.length,
199
+ method: sanitizer?.method ?? "not-sanitized",
200
+ rawContentRetained: sanitizer?.rawContentRetained ?? false,
201
+ skipped: true,
202
+ skipReason: "cited-derived-data",
203
+ dependentKnowledgeIds: derivedRefs.knowledgeIds,
204
+ dependentProposalIds: derivedRefs.proposalIds,
205
+ preview: reviewPreview(sanitized.content),
206
+ });
207
+ continue;
208
+ }
209
+ if (
210
+ !args.dryRun &&
211
+ sanitized.decision &&
212
+ sanitized.decision.disposition !== "allowed"
213
+ ) {
214
+ await recordBlockedCapture({
215
+ id: row.capture.id,
216
+ existing: row.capture,
217
+ source: row.source,
218
+ values: {
219
+ id: row.capture.id,
220
+ sourceId: row.capture.sourceId,
221
+ externalId: row.capture.externalId,
222
+ title: row.capture.title,
223
+ kind: row.capture.kind as BrainCaptureKind,
224
+ content: row.capture.content,
225
+ metadata: parseJson<Record<string, unknown>>(
226
+ row.capture.metadataJson,
227
+ {},
228
+ ),
229
+ capturedAt: row.capture.capturedAt,
230
+ status: row.capture.status,
231
+ },
232
+ decision: sanitized.decision,
233
+ retentionHours: settings.quarantineRetentionHours ?? 72,
234
+ });
235
+ results.push({
236
+ id: row.capture.id,
237
+ sourceId: row.capture.sourceId,
238
+ externalId: row.capture.externalId,
239
+ title: "Privacy-blocked capture",
240
+ capturedAt: row.capture.capturedAt,
241
+ beforeLength,
242
+ afterLength: 0,
243
+ method: sanitizer?.method ?? "deterministic",
244
+ rawContentRetained: false,
245
+ skipped: false,
246
+ dependentKnowledgeIds: derivedRefs.knowledgeIds,
247
+ dependentProposalIds: derivedRefs.proposalIds,
248
+ preview: "",
249
+ });
250
+ continue;
251
+ }
186
252
  if (!args.dryRun) {
253
+ const nextContentHash = await contentHash(sanitized.content);
254
+ let aclHash = row.capture.audienceAclHash;
255
+ if (!aclHash && sanitized.decision?.disposition === "allowed") {
256
+ aclHash = (
257
+ await ensureCaptureAudience({
258
+ captureId: row.capture.id,
259
+ source: row.source,
260
+ memberEmails:
261
+ row.source.visibility === "org"
262
+ ? undefined
263
+ : [row.source.ownerEmail],
264
+ })
265
+ ).aclHash;
266
+ }
187
267
  await db
188
268
  .update(schema.brainRawCaptures)
189
269
  .set({
190
270
  title: sanitized.title,
191
- content: sanitized.content,
192
- contentHash: await contentHash(sanitized.content),
271
+ content:
272
+ sanitized.decision?.disposition === "allowed"
273
+ ? sanitized.content
274
+ : "",
275
+ contentHash:
276
+ sanitized.decision?.disposition === "allowed"
277
+ ? nextContentHash
278
+ : await contentHash(""),
193
279
  metadataJson: stableJson(sanitized.metadata),
280
+ sensitivityDisposition:
281
+ sanitized.decision?.disposition === "allowed"
282
+ ? "allowed"
283
+ : "pending",
284
+ sensitivityPolicyVersion: sanitized.decision?.policyVersion ?? null,
285
+ audienceAclHash:
286
+ sanitized.decision?.disposition === "allowed" ? aclHash : null,
287
+ status:
288
+ sanitized.decision?.disposition === "allowed"
289
+ ? row.capture.status
290
+ : "ignored",
194
291
  updatedAt: nowIso(),
195
292
  })
196
293
  .where(eq(schema.brainRawCaptures.id, row.capture.id));
294
+ if (
295
+ hasDerivedRefs &&
296
+ (sanitized.decision?.disposition !== "allowed" ||
297
+ row.capture.contentHash !== nextContentHash)
298
+ ) {
299
+ await invalidateDerivedForCapture(row.capture.id);
300
+ }
301
+ await enqueueCaptureInvalidation({
302
+ captureId: row.capture.id,
303
+ sourceId: row.capture.sourceId,
304
+ reason:
305
+ sanitized.decision?.disposition === "allowed"
306
+ ? "content-changed"
307
+ : "sensitivity-changed",
308
+ previous: {
309
+ contentHash: row.capture.contentHash ?? undefined,
310
+ sensitivityPolicyVersion:
311
+ row.capture.sensitivityPolicyVersion ?? undefined,
312
+ aclHash: row.capture.audienceAclHash ?? undefined,
313
+ },
314
+ next:
315
+ sanitized.decision?.disposition === "allowed" && aclHash
316
+ ? {
317
+ contentHash: nextContentHash,
318
+ sensitivityPolicyVersion: sanitized.decision.policyVersion,
319
+ aclHash,
320
+ }
321
+ : undefined,
322
+ });
197
323
  }
198
324
  results.push({
199
325
  id: row.capture.id,
@@ -204,7 +330,7 @@ export default defineAction({
204
330
  beforeLength,
205
331
  afterLength: sanitized.content.length,
206
332
  method: sanitizer?.method ?? "not-sanitized",
207
- rawContentRetained: sanitizer?.rawContentRetained ?? true,
333
+ rawContentRetained: sanitizer?.rawContentRetained ?? false,
208
334
  skipped: false,
209
335
  dependentKnowledgeIds: derivedRefs.knowledgeIds,
210
336
  dependentProposalIds: derivedRefs.proposalIds,
@@ -6,6 +6,7 @@ import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
8
8
  import {
9
+ assertDerivedAudienceAccess,
9
10
  nowIso,
10
11
  parseJson,
11
12
  serializeProposal,
@@ -31,6 +32,7 @@ export default defineAction({
31
32
  run: async ({ id, decision, reviewerNotes, publishCanonical }) => {
32
33
  const access = await assertAccess("brain-proposal", id, "editor");
33
34
  const proposal = access.resource;
35
+ await assertDerivedAudienceAccess(proposal);
34
36
  if (proposal.status !== "pending") {
35
37
  throw new Error(`Proposal ${id} is already ${proposal.status}`);
36
38
  }
@@ -53,6 +53,15 @@ export default defineAction({
53
53
  .enum(["manual", "generic", "clips", "slack", "granola", "github"])
54
54
  .optional()
55
55
  .describe("Restrict results to one Brain source provider."),
56
+ kind: z
57
+ .enum(["transcript", "note", "message", "document", "generic"])
58
+ .optional()
59
+ .describe("Restrict capture results to one capture kind."),
60
+ projectId: z
61
+ .string()
62
+ .min(1)
63
+ .optional()
64
+ .describe("Restrict captures to a Brain project."),
56
65
  status: z.string().optional().describe("Restrict results to one status."),
57
66
  limit: z.coerce.number().int().min(1).max(100).default(25),
58
67
  }),
@@ -61,7 +70,7 @@ export default defineAction({
61
70
  publicAgent: {
62
71
  expose: true,
63
72
  readOnly: true,
64
- requiresAuth: false,
73
+ requiresAuth: true,
65
74
  isConsequential: false,
66
75
  },
67
76
  run: async (args) => {
@@ -25,7 +25,7 @@ export default defineAction({
25
25
  publicAgent: {
26
26
  expose: true,
27
27
  readOnly: true,
28
- requiresAuth: false,
28
+ requiresAuth: true,
29
29
  isConsequential: false,
30
30
  },
31
31
  run: async (args) => {
@@ -2,7 +2,30 @@ import { defineAction } from "@agent-native/core";
2
2
  import { z } from "zod";
3
3
 
4
4
  import { writeBrainSettings } from "../server/lib/brain.js";
5
- import { publishTierSchema } from "./_schemas.js";
5
+ import { publishTierSchema, stringArrayCliSchema } from "./_schemas.js";
6
+
7
+ function hasPrivacyWeakeningInstruction(value: string) {
8
+ const matches = value.matchAll(
9
+ /\b(?:allow|permit|release|retain|store|index|ignore|override|relax|weaken|lower|disable)\b/gi,
10
+ );
11
+ for (const match of matches) {
12
+ const index = match.index ?? 0;
13
+ const prefix = value.slice(Math.max(0, index - 32), index);
14
+ if (/\b(?:do not|don't|never|must not|cannot|can't)\s*$/i.test(prefix)) {
15
+ continue;
16
+ }
17
+ return true;
18
+ }
19
+ return false;
20
+ }
21
+
22
+ const privacyInstructionsSchema = z
23
+ .string()
24
+ .max(4_000)
25
+ .refine((value) => !hasPrivacyWeakeningInstruction(value), {
26
+ message:
27
+ "Privacy instructions may only add stricter exclusions; they cannot permit, retain, store, index, or release content.",
28
+ });
6
29
 
7
30
  export default defineAction({
8
31
  description: "Update Brain template settings.",
@@ -20,6 +43,18 @@ export default defineAction({
20
43
  captureSanitizationEnabled: z.coerce.boolean().optional(),
21
44
  captureSanitizationModel: z.string().max(160).optional(),
22
45
  captureSanitizationInstructions: z.string().max(4000).optional(),
46
+ privacyClassifierModel: z.string().max(160).optional(),
47
+ privacyClassifierEngine: z.string().max(160).optional(),
48
+ sensitivityCustomInstructions: privacyInstructionsSchema.optional(),
49
+ publicChannelExclusionPatterns: stringArrayCliSchema({ max: 100 })
50
+ .transform((patterns) => patterns.map((pattern) => pattern.trim()))
51
+ .optional(),
52
+ quarantineRetentionHours: z.coerce
53
+ .number()
54
+ .int()
55
+ .min(1)
56
+ .max(720)
57
+ .optional(),
23
58
  connectorPollMinutes: z.coerce.number().int().min(5).max(1440).optional(),
24
59
  requireCitations: z.coerce.boolean().optional(),
25
60
  autoArchiveResolved: z.coerce.boolean().optional(),
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
7
  import {
8
+ assertDerivedAudienceAccess,
8
9
  nowIso,
9
10
  parseJson,
10
11
  serializeProposal,
@@ -23,6 +24,7 @@ export default defineAction({
23
24
  run: async ({ proposalId, title, body, rationale }) => {
24
25
  const access = await assertAccess("brain-proposal", proposalId, "editor");
25
26
  const proposal = access.resource;
27
+ await assertDerivedAudienceAccess(proposal);
26
28
  if (proposal.status !== "pending") {
27
29
  throw new Error(`Proposal ${proposalId} is already ${proposal.status}`);
28
30
  }
@@ -12,6 +12,7 @@ interface DistillationRequest {
12
12
  captureId?: string;
13
13
  queueId?: string;
14
14
  sourceId?: string;
15
+ claimToken?: string;
15
16
  requestedAt?: string;
16
17
  instructions?: string | null;
17
18
  guidance?: Record<string, unknown>;
@@ -52,15 +53,17 @@ async function clearRequest(captureId: string): Promise<void> {
52
53
  async function claimDistillation(
53
54
  captureId: string,
54
55
  queueId?: string,
55
- ): Promise<boolean> {
56
+ ): Promise<string | null> {
56
57
  try {
57
58
  const payload = await callAction(
58
59
  "claim-distillation" as any,
59
60
  { captureId, queueId } as any,
60
61
  );
61
- return Boolean(payload?.claimed);
62
+ return payload?.claimed && typeof payload.claimToken === "string"
63
+ ? payload.claimToken
64
+ : null;
62
65
  } catch {
63
- return false;
66
+ return null;
64
67
  }
65
68
  }
66
69
 
@@ -121,18 +124,19 @@ export function useDistillationBridge(): void {
121
124
  request.requestedAt ?? "0"
122
125
  }`;
123
126
  if (dispatched.current.has(dispatchKey)) continue;
124
- const claimed = await claimDistillation(capture.id, queueId);
125
- if (!claimed) continue;
127
+ const claimToken = await claimDistillation(capture.id, queueId);
128
+ if (!claimToken || !queueId) continue;
126
129
  dispatched.current.add(dispatchKey);
127
130
 
128
131
  sendToAgentChat({
129
- message: request.message ?? buildMessage(capture),
132
+ message: buildMessage(capture, queueId, claimToken),
130
133
  context: JSON.stringify(
131
134
  {
132
- request,
135
+ request: { ...request, claimToken },
133
136
  capture: summarizeCapture(capture),
134
137
  instructions: request.instructions ?? undefined,
135
138
  guidance: request.guidance,
139
+ claim: { queueId, claimToken },
136
140
  },
137
141
  null,
138
142
  2,
@@ -164,12 +168,17 @@ export function useDistillationBridge(): void {
164
168
  }, [capturesKey, dispatchableCaptures, capturesQuery.dataUpdatedAt]);
165
169
  }
166
170
 
167
- function buildMessage(capture: BrainCaptureReviewItem) {
171
+ function buildMessage(
172
+ capture: BrainCaptureReviewItem,
173
+ queueId: string,
174
+ claimToken: string,
175
+ ) {
168
176
  return (
169
177
  `Distill Brain capture ${capture.id} (${capture.title}). ` +
170
178
  `Use get-capture with includeRawContent=true before exact quote ` +
171
179
  `validation, write durable company knowledge with write-knowledge, then ` +
172
- `mark the capture distilled or ignored.`
180
+ `mark the capture distilled or ignored with queueId ${queueId} and ` +
181
+ `claimToken ${claimToken}.`
173
182
  );
174
183
  }
175
184
 
@@ -13,6 +13,7 @@ export interface NavigationState {
13
13
  view: BrainView;
14
14
  path?: string;
15
15
  query?: string;
16
+ projectId?: string;
16
17
  source?: string;
17
18
  sourceType?: string;
18
19
  type?: string;
@@ -45,6 +46,7 @@ export function useNavigationState() {
45
46
  view: viewFromPath(localPathname),
46
47
  path: appPath(`${localPathname}${search}`),
47
48
  query: params.get("q") || undefined,
49
+ projectId: params.get("projectId") || undefined,
48
50
  source: params.get("source") || undefined,
49
51
  sourceType: params.get("type") || undefined,
50
52
  type: params.get("type") || undefined,
@@ -64,6 +66,7 @@ export function useNavigationState() {
64
66
  getCommandPath: (navCommand) => {
65
67
  const params = new URLSearchParams();
66
68
  if (navCommand.query) params.set("q", navCommand.query);
69
+ if (navCommand.projectId) params.set("projectId", navCommand.projectId);
67
70
  if (navCommand.source) params.set("source", navCommand.source);
68
71
  if (navCommand.type || navCommand.sourceType) {
69
72
  params.set("type", navCommand.type ?? navCommand.sourceType ?? "");