@agent-native/core 0.111.3 → 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 (109) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +18 -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/migration-codemod.ts +59 -9
  7. package/corpus/core/src/cli/multi-frontier-runs.ts +841 -0
  8. package/corpus/core/src/coding-tools/index.ts +27 -8
  9. package/corpus/core/src/embeddings/index.ts +233 -0
  10. package/corpus/core/src/index.ts +2 -0
  11. package/corpus/core/src/search/index.ts +413 -0
  12. package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +48 -5
  13. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +46 -14
  14. package/corpus/templates/brain/README.md +34 -3
  15. package/corpus/templates/brain/actions/approve-proposal.ts +2 -0
  16. package/corpus/templates/brain/actions/claim-distillation.ts +24 -27
  17. package/corpus/templates/brain/actions/create-source.ts +1 -1
  18. package/corpus/templates/brain/actions/get-knowledge.ts +6 -2
  19. package/corpus/templates/brain/actions/get-pilot-report.ts +44 -8
  20. package/corpus/templates/brain/actions/import-capture.ts +21 -9
  21. package/corpus/templates/brain/actions/import-transcript.ts +42 -14
  22. package/corpus/templates/brain/actions/list-captures.ts +10 -1
  23. package/corpus/templates/brain/actions/list-distillation-queue.ts +39 -14
  24. package/corpus/templates/brain/actions/list-projects.ts +51 -0
  25. package/corpus/templates/brain/actions/list-proposals.ts +80 -4
  26. package/corpus/templates/brain/actions/list-sources.ts +17 -3
  27. package/corpus/templates/brain/actions/manage-project.ts +118 -0
  28. package/corpus/templates/brain/actions/mark-capture-distilled.ts +68 -20
  29. package/corpus/templates/brain/actions/navigate.ts +1 -0
  30. package/corpus/templates/brain/actions/reject-proposal.ts +6 -1
  31. package/corpus/templates/brain/actions/resanitize-captures.ts +159 -33
  32. package/corpus/templates/brain/actions/review-proposal.ts +2 -0
  33. package/corpus/templates/brain/actions/search-everything.ts +10 -1
  34. package/corpus/templates/brain/actions/search-knowledge.ts +1 -1
  35. package/corpus/templates/brain/actions/set-settings.ts +36 -1
  36. package/corpus/templates/brain/actions/update-proposal.ts +2 -0
  37. package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +18 -9
  38. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +3 -0
  39. package/corpus/templates/brain/app/i18n-data.ts +731 -2
  40. package/corpus/templates/brain/app/lib/brain.ts +33 -1
  41. package/corpus/templates/brain/app/routes/ops.tsx +47 -0
  42. package/corpus/templates/brain/app/routes/review.tsx +41 -2
  43. package/corpus/templates/brain/app/routes/search.tsx +33 -2
  44. package/corpus/templates/brain/app/routes/settings.tsx +158 -0
  45. package/corpus/templates/brain/app/routes/sources.tsx +31 -0
  46. package/corpus/templates/brain/changelog/2026-07-19-added-private-source-safe-semantic-search.md +6 -0
  47. package/corpus/templates/brain/jobs/process-ingest-queue.ts +270 -36
  48. package/corpus/templates/brain/package.json +1 -0
  49. package/corpus/templates/brain/server/db/index.ts +18 -0
  50. package/corpus/templates/brain/server/db/schema.ts +197 -1
  51. package/corpus/templates/brain/server/jobs/distillation-queue.ts +12 -5
  52. package/corpus/templates/brain/server/lib/audiences.ts +708 -0
  53. package/corpus/templates/brain/server/lib/brain-health.ts +251 -38
  54. package/corpus/templates/brain/server/lib/brain.ts +877 -50
  55. package/corpus/templates/brain/server/lib/capture-sanitization.ts +217 -53
  56. package/corpus/templates/brain/server/lib/connectors.ts +556 -70
  57. package/corpus/templates/brain/server/lib/demo.ts +17 -4
  58. package/corpus/templates/brain/server/lib/hybrid-search.ts +363 -0
  59. package/corpus/templates/brain/server/lib/ingest-queue.ts +104 -0
  60. package/corpus/templates/brain/server/lib/privacy-readiness.ts +24 -0
  61. package/corpus/templates/brain/server/lib/search-index-contracts.ts +72 -0
  62. package/corpus/templates/brain/server/lib/search-index.ts +714 -0
  63. package/corpus/templates/brain/server/lib/search.ts +120 -4
  64. package/corpus/templates/brain/server/lib/sensitivity-policy.ts +174 -0
  65. package/corpus/templates/brain/server/lib/slack-events.ts +281 -0
  66. package/corpus/templates/brain/server/onboarding.ts +29 -0
  67. package/corpus/templates/brain/server/plugins/db.ts +204 -0
  68. package/corpus/templates/brain/server/register-secrets.ts +46 -0
  69. package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +36 -16
  70. package/corpus/templates/brain/server/routes/api/_agent-native/brain/slack-events.post.ts +28 -0
  71. package/corpus/templates/brain/shared/types.ts +10 -0
  72. package/dist/cli/code-agent-executor.d.ts +8 -9
  73. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  74. package/dist/cli/code-agent-executor.js +9 -3
  75. package/dist/cli/code-agent-executor.js.map +1 -1
  76. package/dist/cli/code-agent-runs.d.ts +5 -0
  77. package/dist/cli/code-agent-runs.d.ts.map +1 -1
  78. package/dist/cli/code-agent-runs.js +33 -2
  79. package/dist/cli/code-agent-runs.js.map +1 -1
  80. package/dist/cli/migration-codemod.d.ts.map +1 -1
  81. package/dist/cli/migration-codemod.js +48 -10
  82. package/dist/cli/migration-codemod.js.map +1 -1
  83. package/dist/cli/multi-frontier-runs.d.ts +131 -0
  84. package/dist/cli/multi-frontier-runs.d.ts.map +1 -0
  85. package/dist/cli/multi-frontier-runs.js +529 -0
  86. package/dist/cli/multi-frontier-runs.js.map +1 -0
  87. package/dist/coding-tools/index.d.ts +1 -0
  88. package/dist/coding-tools/index.d.ts.map +1 -1
  89. package/dist/coding-tools/index.js +25 -7
  90. package/dist/coding-tools/index.js.map +1 -1
  91. package/dist/collab/awareness.d.ts +2 -2
  92. package/dist/collab/awareness.d.ts.map +1 -1
  93. package/dist/embeddings/index.d.ts +24 -0
  94. package/dist/embeddings/index.d.ts.map +1 -0
  95. package/dist/embeddings/index.js +172 -0
  96. package/dist/embeddings/index.js.map +1 -0
  97. package/dist/index.d.ts +2 -0
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +2 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/notifications/routes.d.ts +3 -3
  102. package/dist/progress/routes.d.ts +1 -1
  103. package/dist/resources/handlers.d.ts +1 -1
  104. package/dist/search/index.d.ts +88 -0
  105. package/dist/search/index.d.ts.map +1 -0
  106. package/dist/search/index.js +267 -0
  107. package/dist/search/index.js.map +1 -0
  108. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  109. package/package.json +3 -1
@@ -1,9 +1,10 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { accessFilter } from "@agent-native/core/sharing";
3
- import { and, desc, eq, lte, or } from "drizzle-orm";
3
+ import { and, desc, eq, inArray, isNull, lte, or } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import { listAccessibleAudienceIds } from "../server/lib/audiences.js";
7
8
  import { redactSensitiveText } from "../server/lib/search.js";
8
9
 
9
10
  const queueStatuses = ["queued", "processing", "done", "failed"] as const;
@@ -118,27 +119,45 @@ export async function readDistillationQueue(
118
119
  const staleCutoff = staleProcessingCutoff(now);
119
120
  const issue = args.staleOnly ? "stale" : (args.issue ?? "all");
120
121
  const limit = args.limit ?? 100;
122
+ const accessibleAudienceIds = await listAccessibleAudienceIds();
123
+ const captureVisibility = or(
124
+ isNull(schema.brainIngestQueue.captureId),
125
+ accessibleAudienceIds.length
126
+ ? and(
127
+ eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
128
+ inArray(
129
+ schema.brainCaptureAudiences.audienceId,
130
+ accessibleAudienceIds,
131
+ ),
132
+ )
133
+ : undefined,
134
+ );
121
135
  const baseClauses = [
122
136
  eq(schema.brainIngestQueue.operation, "distill"),
123
137
  accessFilter(schema.brainSources, schema.brainSourceShares),
138
+ captureVisibility,
124
139
  ];
125
140
  if (args.sourceId) {
126
141
  baseClauses.push(eq(schema.brainSources.id, args.sourceId));
127
142
  }
128
143
 
129
144
  const summaryRows = await db
130
- .select({
145
+ .selectDistinct({
131
146
  status: schema.brainIngestQueue.status,
132
147
  updatedAt: schema.brainIngestQueue.updatedAt,
133
148
  })
134
149
  .from(schema.brainIngestQueue)
135
150
  .innerJoin(
151
+ schema.brainSources,
152
+ eq(schema.brainIngestQueue.sourceId, schema.brainSources.id),
153
+ )
154
+ .leftJoin(
136
155
  schema.brainRawCaptures,
137
156
  eq(schema.brainIngestQueue.captureId, schema.brainRawCaptures.id),
138
157
  )
139
- .innerJoin(
140
- schema.brainSources,
141
- eq(schema.brainRawCaptures.sourceId, schema.brainSources.id),
158
+ .leftJoin(
159
+ schema.brainCaptureAudiences,
160
+ eq(schema.brainCaptureAudiences.captureId, schema.brainRawCaptures.id),
142
161
  )
143
162
  .where(and(...baseClauses));
144
163
 
@@ -164,7 +183,7 @@ export async function readDistillationQueue(
164
183
  }
165
184
 
166
185
  const rows = await db
167
- .select({
186
+ .selectDistinct({
168
187
  id: schema.brainIngestQueue.id,
169
188
  sourceId: schema.brainIngestQueue.sourceId,
170
189
  captureId: schema.brainIngestQueue.captureId,
@@ -184,12 +203,16 @@ export async function readDistillationQueue(
184
203
  })
185
204
  .from(schema.brainIngestQueue)
186
205
  .innerJoin(
206
+ schema.brainSources,
207
+ eq(schema.brainIngestQueue.sourceId, schema.brainSources.id),
208
+ )
209
+ .leftJoin(
187
210
  schema.brainRawCaptures,
188
211
  eq(schema.brainIngestQueue.captureId, schema.brainRawCaptures.id),
189
212
  )
190
- .innerJoin(
191
- schema.brainSources,
192
- eq(schema.brainRawCaptures.sourceId, schema.brainSources.id),
213
+ .leftJoin(
214
+ schema.brainCaptureAudiences,
215
+ eq(schema.brainCaptureAudiences.captureId, schema.brainRawCaptures.id),
193
216
  )
194
217
  .where(and(...clauses))
195
218
  .orderBy(desc(schema.brainIngestQueue.updatedAt))
@@ -223,11 +246,13 @@ export async function readDistillationQueue(
223
246
  provider: row.sourceProvider,
224
247
  status: row.sourceStatus,
225
248
  },
226
- capture: {
227
- id: row.captureId,
228
- title: redactSensitiveText(row.captureTitle),
229
- status: row.captureStatus,
230
- },
249
+ capture: row.captureId
250
+ ? {
251
+ id: row.captureId,
252
+ title: redactOptionalText(row.captureTitle),
253
+ status: row.captureStatus,
254
+ }
255
+ : null,
231
256
  };
232
257
  });
233
258
 
@@ -0,0 +1,51 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
+ import { getUserSetting } from "@agent-native/core/settings";
4
+ import { accessFilter } from "@agent-native/core/sharing";
5
+ import { inArray } from "drizzle-orm";
6
+ import { z } from "zod";
7
+
8
+ import { getDb, schema } from "../server/db/index.js";
9
+
10
+ const DEFAULT_PROJECT_SETTING = "brain-default-project";
11
+
12
+ export default defineAction({
13
+ description: "List accessible Brain projects and their source membership.",
14
+ schema: z.object({}),
15
+ http: { method: "GET" },
16
+ readOnly: true,
17
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
18
+ run: async () => {
19
+ const db = getDb();
20
+ const projects = await db
21
+ .select()
22
+ .from(schema.brainProjects)
23
+ .where(accessFilter(schema.brainProjects, schema.brainProjectShares));
24
+ const projectIds = projects.map((project) => project.id);
25
+ const links = projectIds.length
26
+ ? await db
27
+ .select()
28
+ .from(schema.brainProjectSources)
29
+ .where(inArray(schema.brainProjectSources.projectId, projectIds))
30
+ : [];
31
+ const userEmail = getRequestUserEmail();
32
+ const setting = userEmail
33
+ ? ((await getUserSetting(userEmail, DEFAULT_PROJECT_SETTING)) as {
34
+ projectId?: string;
35
+ } | null)
36
+ : null;
37
+ return {
38
+ projects: projects.map((project) => ({
39
+ id: project.id,
40
+ title: project.title,
41
+ description: project.description,
42
+ visibility: project.visibility,
43
+ sourceIds: links
44
+ .filter((link) => link.projectId === project.id)
45
+ .map((link) => link.sourceId),
46
+ isDefault: setting?.projectId === project.id,
47
+ updatedAt: project.updatedAt,
48
+ })),
49
+ };
50
+ },
51
+ });
@@ -1,26 +1,102 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { accessFilter } from "@agent-native/core/sharing";
3
- import { and, desc, eq } from "drizzle-orm";
2
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
3
+ import { and, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
- import { serializeProposal } from "../server/lib/brain.js";
7
+ import { listAccessibleAudienceIds } from "../server/lib/audiences.js";
8
+ import { parseJson, serializeProposal } from "../server/lib/brain.js";
8
9
 
9
10
  export default defineAction({
10
11
  description: "List Brain knowledge proposals requiring review.",
11
12
  schema: z.object({
12
- status: z.enum(["pending", "approved", "rejected"]).default("pending"),
13
+ status: z
14
+ .enum(["pending", "approved", "rejected", "quarantine"])
15
+ .default("pending"),
13
16
  limit: z.coerce.number().int().min(1).max(100).default(50),
14
17
  }),
15
18
  http: { method: "GET" },
16
19
  readOnly: true,
17
20
  run: async ({ status, limit }) => {
21
+ if (status === "quarantine") {
22
+ const accessibleSources = await getDb()
23
+ .select({ id: schema.brainSources.id })
24
+ .from(schema.brainSources)
25
+ .where(accessFilter(schema.brainSources, schema.brainSourceShares));
26
+ const adminSourceIds = (
27
+ await Promise.all(
28
+ accessibleSources.map(async ({ id }) => {
29
+ try {
30
+ await assertAccess("brain-source", id, "admin");
31
+ return id;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }),
36
+ )
37
+ ).filter((sourceId): sourceId is string => Boolean(sourceId));
38
+ if (!adminSourceIds.length) return { count: 0, proposals: [] };
39
+ const rows = await getDb()
40
+ .select({
41
+ id: schema.brainSensitivityEvents.id,
42
+ sourceId: schema.brainSensitivityEvents.sourceId,
43
+ sourceName: schema.brainSources.title,
44
+ disposition: schema.brainSensitivityEvents.disposition,
45
+ categoriesJson: schema.brainSensitivityEvents.categoriesJson,
46
+ confidenceBand: schema.brainSensitivityEvents.confidenceBand,
47
+ expiresAt: schema.brainSensitivityEvents.expiresAt,
48
+ createdAt: schema.brainSensitivityEvents.createdAt,
49
+ updatedAt: schema.brainSensitivityEvents.updatedAt,
50
+ })
51
+ .from(schema.brainSensitivityEvents)
52
+ .innerJoin(
53
+ schema.brainSources,
54
+ eq(schema.brainSources.id, schema.brainSensitivityEvents.sourceId),
55
+ )
56
+ .where(
57
+ and(
58
+ eq(schema.brainSensitivityEvents.disposition, "quarantined"),
59
+ inArray(schema.brainSensitivityEvents.sourceId, adminSourceIds),
60
+ ),
61
+ )
62
+ .orderBy(desc(schema.brainSensitivityEvents.createdAt))
63
+ .limit(limit);
64
+ const proposals = rows.map((row) => ({
65
+ id: row.id,
66
+ title: "Privacy quarantine",
67
+ sourceId: row.sourceId,
68
+ sourceName: row.sourceName,
69
+ reason: [
70
+ row.confidenceBand,
71
+ ...parseJson<string[]>(row.categoriesJson, []),
72
+ row.expiresAt ? `expires ${row.expiresAt}` : "metadata-only",
73
+ ].join(" · "),
74
+ status: "quarantine" as const,
75
+ createdAt: row.createdAt,
76
+ updatedAt: row.updatedAt,
77
+ }));
78
+ return { count: proposals.length, proposals };
79
+ }
80
+ const audienceIds = await listAccessibleAudienceIds();
18
81
  const rows = await getDb()
19
82
  .select()
20
83
  .from(schema.brainProposals)
21
84
  .where(
22
85
  and(
23
86
  accessFilter(schema.brainProposals, schema.brainProposalShares),
87
+ or(
88
+ isNull(schema.brainProposals.captureId),
89
+ audienceIds.length
90
+ ? and(
91
+ sql`exists (
92
+ select 1 from ${schema.brainSources}
93
+ where ${schema.brainSources.id} = ${schema.brainProposals.sourceId}
94
+ and ${accessFilter(schema.brainSources, schema.brainSourceShares)}
95
+ )`,
96
+ inArray(schema.brainProposals.audienceId, audienceIds),
97
+ )
98
+ : undefined,
99
+ ),
24
100
  eq(schema.brainProposals.status, status),
25
101
  ),
26
102
  )
@@ -1,20 +1,34 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { accessFilter } from "@agent-native/core/sharing";
3
- import { and, count, desc, eq, ne } from "drizzle-orm";
3
+ import { and, countDistinct, desc, eq, inArray, ne } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
7
  import { nextBrainSourceSyncAt } from "../server/jobs/sync-sources.js";
8
+ import { listAccessibleAudienceIds } from "../server/lib/audiences.js";
8
9
  import { parseJson, serializeSource } from "../server/lib/brain.js";
9
10
  import { sourceProviderSchema } from "./_schemas.js";
10
11
 
11
12
  async function sourceRecordCount(sourceId: string): Promise<number> {
13
+ const audienceIds = await listAccessibleAudienceIds([sourceId]);
14
+ if (!audienceIds.length) return 0;
15
+
12
16
  // Count only — never load the heavy `content` blob of every capture row
13
17
  // just to take `.length`.
14
18
  const [row] = await getDb()
15
- .select({ value: count() })
19
+ .select({ value: countDistinct(schema.brainRawCaptures.id) })
16
20
  .from(schema.brainRawCaptures)
17
- .where(eq(schema.brainRawCaptures.sourceId, sourceId));
21
+ .innerJoin(
22
+ schema.brainCaptureAudiences,
23
+ eq(schema.brainCaptureAudiences.captureId, schema.brainRawCaptures.id),
24
+ )
25
+ .where(
26
+ and(
27
+ eq(schema.brainRawCaptures.sourceId, sourceId),
28
+ eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
29
+ inArray(schema.brainCaptureAudiences.audienceId, audienceIds),
30
+ ),
31
+ );
18
32
  return row?.value ?? 0;
19
33
  }
20
34
 
@@ -0,0 +1,118 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server/request-context";
6
+ import { putUserSetting } from "@agent-native/core/settings";
7
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
8
+ import { and, eq, inArray } from "drizzle-orm";
9
+ import { z } from "zod";
10
+
11
+ import { getDb, schema } from "../server/db/index.js";
12
+ import { nanoid, nowIso } from "../server/lib/brain.js";
13
+
14
+ const DEFAULT_PROJECT_SETTING = "brain-default-project";
15
+
16
+ export default defineAction({
17
+ description:
18
+ "Create, update, remove, or select a Brain project that scopes related knowledge sources.",
19
+ schema: z.object({
20
+ operation: z.enum(["create", "update", "delete", "set-default"]),
21
+ projectId: z.string().optional(),
22
+ title: z.string().min(1).optional(),
23
+ description: z.string().optional(),
24
+ sourceIds: z.array(z.string().min(1)).optional(),
25
+ }),
26
+ run: async (args) => {
27
+ const userEmail = getRequestUserEmail();
28
+ if (!userEmail) throw new Error("Not authenticated");
29
+ const db = getDb();
30
+ const now = nowIso();
31
+
32
+ if (args.operation === "create") {
33
+ if (!args.title) throw new Error("title is required");
34
+ const id = args.projectId ?? nanoid();
35
+ await db.insert(schema.brainProjects).values({
36
+ id,
37
+ title: args.title,
38
+ description: args.description ?? "",
39
+ ownerEmail: userEmail,
40
+ orgId: getRequestOrgId() ?? null,
41
+ visibility: "private",
42
+ createdAt: now,
43
+ updatedAt: now,
44
+ });
45
+ await replaceProjectSources(id, args.sourceIds ?? []);
46
+ return { projectId: id, operation: args.operation };
47
+ }
48
+
49
+ if (!args.projectId) throw new Error("projectId is required");
50
+ if (args.operation === "set-default") {
51
+ await assertAccess("brain-project", args.projectId, "viewer");
52
+ await putUserSetting(userEmail, DEFAULT_PROJECT_SETTING, {
53
+ projectId: args.projectId,
54
+ });
55
+ return { projectId: args.projectId, default: true };
56
+ }
57
+
58
+ await assertAccess("brain-project", args.projectId, "editor");
59
+ if (args.operation === "delete") {
60
+ await db
61
+ .delete(schema.brainProjectSources)
62
+ .where(eq(schema.brainProjectSources.projectId, args.projectId));
63
+ await db
64
+ .delete(schema.brainProjectShares)
65
+ .where(eq(schema.brainProjectShares.resourceId, args.projectId));
66
+ await db
67
+ .delete(schema.brainProjects)
68
+ .where(eq(schema.brainProjects.id, args.projectId));
69
+ return { projectId: args.projectId, deleted: true };
70
+ }
71
+
72
+ await db
73
+ .update(schema.brainProjects)
74
+ .set({
75
+ ...(args.title ? { title: args.title } : {}),
76
+ ...(args.description !== undefined
77
+ ? { description: args.description }
78
+ : {}),
79
+ updatedAt: now,
80
+ })
81
+ .where(eq(schema.brainProjects.id, args.projectId));
82
+ if (args.sourceIds) {
83
+ await replaceProjectSources(args.projectId, args.sourceIds);
84
+ }
85
+ return { projectId: args.projectId, operation: args.operation };
86
+ },
87
+ });
88
+
89
+ async function replaceProjectSources(projectId: string, sourceIds: string[]) {
90
+ const db = getDb();
91
+ const unique = Array.from(new Set(sourceIds));
92
+ if (unique.length) {
93
+ const accessible = await db
94
+ .select({ id: schema.brainSources.id })
95
+ .from(schema.brainSources)
96
+ .where(
97
+ and(
98
+ accessFilter(schema.brainSources, schema.brainSourceShares),
99
+ inArray(schema.brainSources.id, unique),
100
+ ),
101
+ );
102
+ if (accessible.length !== unique.length) {
103
+ throw new Error("One or more Brain sources were not found");
104
+ }
105
+ }
106
+ await db
107
+ .delete(schema.brainProjectSources)
108
+ .where(eq(schema.brainProjectSources.projectId, projectId));
109
+ if (!unique.length) return;
110
+ await db.insert(schema.brainProjectSources).values(
111
+ unique.map((sourceId) => ({
112
+ id: nanoid(),
113
+ projectId,
114
+ sourceId,
115
+ createdAt: nowIso(),
116
+ })),
117
+ );
118
+ }
@@ -1,5 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { and, eq, inArray } from "drizzle-orm";
2
+ import { getDbExec } from "@agent-native/core/db";
3
+ import { and, desc, eq } from "drizzle-orm";
3
4
  import { z } from "zod";
4
5
 
5
6
  import { getDb, schema } from "../server/db/index.js";
@@ -15,15 +16,76 @@ import {
15
16
 
16
17
  export default defineAction({
17
18
  description: "Mark a raw Brain capture as distilled or ignored.",
18
- schema: z.object({
19
- captureId: z.string().min(1),
20
- status: z.enum(["distilled", "ignored"]).default("distilled"),
21
- }),
22
- run: async ({ captureId, status }) => {
19
+ schema: z
20
+ .object({
21
+ captureId: z.string().min(1),
22
+ status: z.enum(["distilled", "ignored"]).default("distilled"),
23
+ queueId: z.string().min(1).optional(),
24
+ claimToken: z.string().min(1).optional(),
25
+ })
26
+ .superRefine((value, ctx) => {
27
+ if (Boolean(value.queueId) !== Boolean(value.claimToken)) {
28
+ ctx.addIssue({
29
+ code: z.ZodIssueCode.custom,
30
+ message: "queueId and claimToken must be supplied together.",
31
+ });
32
+ }
33
+ }),
34
+ run: async ({ captureId, status, queueId, claimToken }) => {
23
35
  const access = await getAccessibleCapture(captureId);
24
36
  if (!access) throw new Error(`No access to capture ${captureId}`);
25
37
  const db = getDb();
26
38
  const now = nowIso();
39
+
40
+ if (queueId && claimToken) {
41
+ const result = await getDbExec().execute({
42
+ sql: `UPDATE brain_ingest_queue
43
+ SET status = ?, error = NULL, lease_token = NULL,
44
+ lease_expires_at = NULL, updated_at = ?
45
+ WHERE id = ? AND capture_id = ? AND operation = 'distill'
46
+ AND status = 'processing' AND lease_token = ?`,
47
+ args: ["done", now, queueId, captureId, claimToken],
48
+ });
49
+ if (result.rowsAffected === 0) {
50
+ throw new Error(
51
+ "The distillation claim is no longer active; do not complete a newer worker's queue item.",
52
+ );
53
+ }
54
+ } else {
55
+ const [processing] = await db
56
+ .select()
57
+ .from(schema.brainIngestQueue)
58
+ .where(
59
+ and(
60
+ eq(schema.brainIngestQueue.captureId, captureId),
61
+ eq(schema.brainIngestQueue.operation, "distill"),
62
+ eq(schema.brainIngestQueue.status, "processing"),
63
+ ),
64
+ )
65
+ .orderBy(desc(schema.brainIngestQueue.updatedAt))
66
+ .limit(1);
67
+ if (processing) {
68
+ throw new Error(
69
+ "This capture has an active distillation claim. Supply its queueId and claimToken to complete it.",
70
+ );
71
+ }
72
+ await db
73
+ .update(schema.brainIngestQueue)
74
+ .set({
75
+ status: "done",
76
+ error: null,
77
+ leaseToken: null,
78
+ leaseExpiresAt: null,
79
+ updatedAt: now,
80
+ })
81
+ .where(
82
+ and(
83
+ eq(schema.brainIngestQueue.captureId, captureId),
84
+ eq(schema.brainIngestQueue.operation, "distill"),
85
+ eq(schema.brainIngestQueue.status, "queued"),
86
+ ),
87
+ );
88
+ }
27
89
  await db
28
90
  .update(schema.brainRawCaptures)
29
91
  .set({
@@ -32,20 +94,6 @@ export default defineAction({
32
94
  updatedAt: now,
33
95
  })
34
96
  .where(eq(schema.brainRawCaptures.id, captureId));
35
- await db
36
- .update(schema.brainIngestQueue)
37
- .set({
38
- status: "done",
39
- error: null,
40
- updatedAt: now,
41
- })
42
- .where(
43
- and(
44
- eq(schema.brainIngestQueue.captureId, captureId),
45
- eq(schema.brainIngestQueue.operation, "distill"),
46
- inArray(schema.brainIngestQueue.status, ["queued", "processing"]),
47
- ),
48
- );
49
97
  const updated = await getAccessibleCapture(captureId);
50
98
  if (!updated) return { capture: null };
51
99
  const capture = serializeCapture(updated.capture);
@@ -28,6 +28,7 @@ export default defineAction({
28
28
  proposalId: z.string().optional(),
29
29
  extensionId: z.string().optional(),
30
30
  query: z.string().optional(),
31
+ projectId: z.string().optional(),
31
32
  type: z.enum(["knowledge", "capture", "source", "all"]).optional(),
32
33
  provider: z.string().optional(),
33
34
  status: z.string().optional(),
@@ -5,7 +5,11 @@ import { eq } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
8
- import { nowIso, serializeProposal } from "../server/lib/brain.js";
8
+ import {
9
+ assertDerivedAudienceAccess,
10
+ nowIso,
11
+ serializeProposal,
12
+ } from "../server/lib/brain.js";
9
13
 
10
14
  export default defineAction({
11
15
  description: "Reject a pending Brain proposal with optional reviewer notes.",
@@ -15,6 +19,7 @@ export default defineAction({
15
19
  }),
16
20
  run: async ({ proposalId, reviewerNotes }) => {
17
21
  const access = await assertAccess("brain-proposal", proposalId, "editor");
22
+ await assertDerivedAudienceAccess(access.resource);
18
23
  if (access.resource.status !== "pending") {
19
24
  throw new Error(
20
25
  `Proposal ${proposalId} is already ${access.resource.status}`,