@mastra/factory 0.12.1-alpha.0 → 0.13.0-alpha.2
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/README.md +10 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +90 -9
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/provenance.d.ts +2 -0
- package/dist/integrations/github/provenance.d.ts.map +1 -1
- package/dist/integrations/github/provenance.js +3 -2
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +12 -39
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +33 -4
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/webhook.d.ts.map +1 -1
- package/dist/integrations/github/webhook.js +34 -2
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/routes/attention-providers.d.ts +17 -0
- package/dist/routes/attention-providers.d.ts.map +1 -1
- package/dist/routes/attention-providers.js +128 -2
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/attention.d.ts.map +1 -1
- package/dist/routes/attention.js +11 -4
- package/dist/routes/attention.js.map +1 -1
- package/dist/routes/projects.d.ts +9 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +26 -5
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/supervisor.d.ts +23 -0
- package/dist/routes/supervisor.d.ts.map +1 -0
- package/dist/routes/supervisor.js +34 -0
- package/dist/routes/supervisor.js.map +1 -0
- package/dist/routes/surface.js +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +5 -0
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/defaults.d.ts.map +1 -1
- package/dist/rules/defaults.js +20 -8
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +1 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +81 -23
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/terminal-cleanup.d.ts +3 -1
- package/dist/rules/terminal-cleanup.d.ts.map +1 -1
- package/dist/rules/terminal-cleanup.js +5 -0
- package/dist/rules/terminal-cleanup.js.map +1 -1
- package/dist/rules/transition-service.d.ts +1 -0
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +2 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +7 -0
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/sandbox/session-retirement.js +1 -1
- package/dist/storage/domains/work-items/base.d.ts +36 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +144 -2
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/supervisor/health-worker.d.ts +17 -0
- package/dist/supervisor/health-worker.d.ts.map +1 -0
- package/dist/supervisor/health-worker.js +72 -0
- package/dist/supervisor/health-worker.js.map +1 -0
- package/dist/supervisor/health.d.ts +81 -0
- package/dist/supervisor/health.d.ts.map +1 -0
- package/dist/supervisor/health.js +245 -0
- package/dist/supervisor/health.js.map +1 -0
- package/dist/supervisor/instructions.d.ts +6 -0
- package/dist/supervisor/instructions.d.ts.map +1 -0
- package/dist/supervisor/instructions.js +64 -0
- package/dist/supervisor/instructions.js.map +1 -0
- package/dist/supervisor/read-tools.d.ts +41 -0
- package/dist/supervisor/read-tools.d.ts.map +1 -0
- package/dist/supervisor/read-tools.js +338 -0
- package/dist/supervisor/read-tools.js.map +1 -0
- package/dist/supervisor/session.d.ts +38 -0
- package/dist/supervisor/session.d.ts.map +1 -0
- package/dist/supervisor/session.js +63 -0
- package/dist/supervisor/session.js.map +1 -0
- package/dist/supervisor/write-tools.d.ts +26 -0
- package/dist/supervisor/write-tools.d.ts.map +1 -0
- package/dist/supervisor/write-tools.js +227 -0
- package/dist/supervisor/write-tools.js.map +1 -0
- package/dist/workspace.d.ts +3 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +14 -4
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-triage/SKILL.md +1 -1
- package/package.json +4 -4
|
@@ -98,6 +98,23 @@ export declare class AutomationFailedAttentionProvider implements AttentionProvi
|
|
|
98
98
|
continuation?: AttentionStreamPosition;
|
|
99
99
|
}>;
|
|
100
100
|
}
|
|
101
|
+
export declare class SupervisorFindingAttentionProvider implements AttentionProvider {
|
|
102
|
+
#private;
|
|
103
|
+
readonly kind: "supervisor-finding";
|
|
104
|
+
constructor({ workItems }: {
|
|
105
|
+
workItems: WorkItemsStorage;
|
|
106
|
+
});
|
|
107
|
+
counts(scope: AttentionScope): Promise<AttentionCounts>;
|
|
108
|
+
latest(scope: AttentionScope): Promise<AttentionLatest | null>;
|
|
109
|
+
page(scope: AttentionScope, args: AttentionPageArgs): Promise<AttentionPageResult>;
|
|
110
|
+
markAllRead(scope: AttentionScope, args: {
|
|
111
|
+
before?: AttentionStreamPosition;
|
|
112
|
+
now: Date;
|
|
113
|
+
}): Promise<{
|
|
114
|
+
hasMore: boolean;
|
|
115
|
+
continuation?: AttentionStreamPosition;
|
|
116
|
+
}>;
|
|
117
|
+
}
|
|
101
118
|
export declare class MentionAttentionProvider implements AttentionProvider {
|
|
102
119
|
#private;
|
|
103
120
|
readonly kind: "mention";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attention-providers.d.ts","sourceRoot":"","sources":["../../src/routes/attention-providers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAEV,uBAAuB,EAExB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAEV,oBAAoB,EACpB,6BAA6B,EAC7B,6BAA6B,
|
|
1
|
+
{"version":3,"file":"attention-providers.d.ts","sourceRoot":"","sources":["../../src/routes/attention-providers.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { factoryAttentionKey, factoryDecisionAttentionIdentity, factoryMentionAttentionIdentity } from "../storage/domains/work-items/base.js";
|
|
1
|
+
import { factoryAttentionKey, factoryDecisionAttentionIdentity, factoryMentionAttentionIdentity, factorySupervisorFindingAttentionIdentity } from "../storage/domains/work-items/base.js";
|
|
2
2
|
import { factoryDispatchFailureMetadata } from "../rules/dispatch-errors.js";
|
|
3
3
|
//#region src/routes/attention-providers.ts
|
|
4
4
|
/**
|
|
@@ -200,6 +200,132 @@ var AutomationFailedAttentionProvider = class {
|
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
|
+
function supervisorFindingPayload(row) {
|
|
204
|
+
return row.finding;
|
|
205
|
+
}
|
|
206
|
+
var SupervisorFindingAttentionProvider = class {
|
|
207
|
+
kind = "supervisor-finding";
|
|
208
|
+
#workItems;
|
|
209
|
+
constructor({ workItems }) {
|
|
210
|
+
this.#workItems = workItems;
|
|
211
|
+
}
|
|
212
|
+
async counts(scope) {
|
|
213
|
+
let open = 0;
|
|
214
|
+
let unread = 0;
|
|
215
|
+
const batches = scanBatches((before) => this.#workItems.listSupervisorFindingPage({
|
|
216
|
+
...scope,
|
|
217
|
+
...before ? { before } : {},
|
|
218
|
+
limit: 50
|
|
219
|
+
}), (row) => ({
|
|
220
|
+
occurredAt: row.updatedAt,
|
|
221
|
+
id: row.id
|
|
222
|
+
}));
|
|
223
|
+
for await (const page of batches) {
|
|
224
|
+
const identities = page.rows.map((row) => factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence));
|
|
225
|
+
const receipts = await this.#workItems.listAttentionReceipts({
|
|
226
|
+
...scope,
|
|
227
|
+
identities
|
|
228
|
+
});
|
|
229
|
+
const byIdentity = new Map(receipts.map((receipt) => [`${receipt.sourceId}\0${receipt.occurrence}`, receipt]));
|
|
230
|
+
for (const row of page.rows) {
|
|
231
|
+
const receipt = byIdentity.get(`${row.findingKey}\0${row.occurrence}`);
|
|
232
|
+
if (receipt?.state === "archived") continue;
|
|
233
|
+
open += 1;
|
|
234
|
+
if (!receipt) unread += 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
open,
|
|
239
|
+
unread
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async latest(scope) {
|
|
243
|
+
const newest = (await this.#workItems.listSupervisorFindingPage({
|
|
244
|
+
...scope,
|
|
245
|
+
limit: 1
|
|
246
|
+
})).rows[0];
|
|
247
|
+
if (!newest) return null;
|
|
248
|
+
const identity = factorySupervisorFindingAttentionIdentity(newest.findingKey, newest.occurrence);
|
|
249
|
+
const receipts = await this.#workItems.listAttentionReceipts({
|
|
250
|
+
...scope,
|
|
251
|
+
identities: [identity]
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
key: factoryAttentionKey(scope.factoryProjectId, identity),
|
|
255
|
+
at: newest.updatedAt,
|
|
256
|
+
unread: receipts.length === 0
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
page(scope, args) {
|
|
260
|
+
return collectPage(scanBatches((before) => this.#workItems.listSupervisorFindingPage({
|
|
261
|
+
...scope,
|
|
262
|
+
...before ? { before } : {},
|
|
263
|
+
limit: 50
|
|
264
|
+
}), (row) => ({
|
|
265
|
+
occurredAt: row.updatedAt,
|
|
266
|
+
id: row.id
|
|
267
|
+
}), args.before), args.limit, async (rows) => {
|
|
268
|
+
const identities = rows.map((row) => factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence));
|
|
269
|
+
const receipts = await this.#workItems.listAttentionReceipts({
|
|
270
|
+
...scope,
|
|
271
|
+
identities
|
|
272
|
+
});
|
|
273
|
+
const byIdentity = new Map(receipts.map((receipt) => [`${receipt.sourceId}\0${receipt.occurrence}`, receipt]));
|
|
274
|
+
return rows.flatMap((row) => {
|
|
275
|
+
const finding = supervisorFindingPayload(row);
|
|
276
|
+
const receipt = byIdentity.get(`${row.findingKey}\0${row.occurrence}`);
|
|
277
|
+
if (!matchesView(args.view, receipt)) return [];
|
|
278
|
+
const text = `${finding.title} ${finding.evidence}`.toLowerCase();
|
|
279
|
+
if (args.search && !text.includes(args.search)) return [];
|
|
280
|
+
const identity = factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence);
|
|
281
|
+
return [{
|
|
282
|
+
key: factoryAttentionKey(scope.factoryProjectId, identity),
|
|
283
|
+
occurredAt: row.updatedAt,
|
|
284
|
+
resumeCursor: {
|
|
285
|
+
occurredAt: row.updatedAt,
|
|
286
|
+
id: row.id
|
|
287
|
+
},
|
|
288
|
+
receipt,
|
|
289
|
+
item: {
|
|
290
|
+
key: factoryAttentionKey(scope.factoryProjectId, identity),
|
|
291
|
+
kind: this.kind,
|
|
292
|
+
findingKey: row.findingKey,
|
|
293
|
+
occurrence: row.occurrence,
|
|
294
|
+
findingTitle: finding.title,
|
|
295
|
+
evidence: finding.evidence,
|
|
296
|
+
title: finding.title,
|
|
297
|
+
detail: finding.evidence,
|
|
298
|
+
ageMs: finding.ageMs,
|
|
299
|
+
suggestedRepair: finding.suggestedRepair,
|
|
300
|
+
workItemId: finding.workItemId,
|
|
301
|
+
occurredAt: row.updatedAt.toISOString(),
|
|
302
|
+
read: Boolean(receipt),
|
|
303
|
+
archived: receipt?.state === "archived",
|
|
304
|
+
target: finding.workItemId ? {
|
|
305
|
+
kind: "work-item",
|
|
306
|
+
workItemId: finding.workItemId,
|
|
307
|
+
board: "work"
|
|
308
|
+
} : { kind: "rules" }
|
|
309
|
+
}
|
|
310
|
+
}];
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
markAllRead(scope, args) {
|
|
315
|
+
return markScanRead(scanBatches((before) => this.#workItems.listSupervisorFindingPage({
|
|
316
|
+
...scope,
|
|
317
|
+
...before ? { before } : {},
|
|
318
|
+
limit: 50
|
|
319
|
+
}), (row) => ({
|
|
320
|
+
occurredAt: row.updatedAt,
|
|
321
|
+
id: row.id
|
|
322
|
+
}), args.before), (rows) => this.#workItems.markAttentionReceiptsRead({
|
|
323
|
+
...scope,
|
|
324
|
+
identities: rows.map((row) => factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence)),
|
|
325
|
+
now: args.now
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
};
|
|
203
329
|
var MentionAttentionProvider = class {
|
|
204
330
|
kind = "mention";
|
|
205
331
|
#workItems;
|
|
@@ -379,6 +505,6 @@ function receiptScope(scope) {
|
|
|
379
505
|
};
|
|
380
506
|
}
|
|
381
507
|
//#endregion
|
|
382
|
-
export { AutomationFailedAttentionProvider, MentionAttentionProvider, SCAN_PAGE_SIZE, collectPage, factoryDecisionType, markScanRead, matchesView, receiptScope, scanBatches, workItemBoard };
|
|
508
|
+
export { AutomationFailedAttentionProvider, MentionAttentionProvider, SCAN_PAGE_SIZE, SupervisorFindingAttentionProvider, collectPage, factoryDecisionType, markScanRead, matchesView, receiptScope, scanBatches, workItemBoard };
|
|
383
509
|
|
|
384
510
|
//# sourceMappingURL=attention-providers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attention-providers.js","names":["#workItems","#scan","#receiptsFor","#linkedItems","#comments","#resolve"],"sources":["../../src/routes/attention-providers.ts"],"sourcesContent":["/**\n * Per-kind attention providers. Each kind owns its counts, its bounded page\n * scan, and its wire item shape; the route layer k-way merges provider pages\n * on `occurredAt desc` with one resumable cursor per kind.\n */\n\nimport { factoryDispatchFailureMetadata } from '../rules/dispatch-errors.js';\nimport type {\n WorkItemCommentRow,\n WorkItemCommentsStorage,\n WorkItemMentionRow,\n} from '../storage/domains/comments/base.js';\nimport type {\n FactoryAttentionIdentity,\n FactoryAttentionKind,\n FactoryAttentionReceiptRecord,\n FactoryDeferredDecisionRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport {\n factoryAttentionKey,\n factoryDecisionAttentionIdentity,\n factoryMentionAttentionIdentity,\n} from '../storage/domains/work-items/base.js';\n\nexport type FactoryAttentionView = 'open' | 'unread' | 'archived';\n\nexport interface AttentionScope {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n}\n\nexport interface AttentionStreamPosition {\n occurredAt: Date;\n id: string;\n}\n\nexport interface AttentionEntry {\n occurredAt: Date;\n /** Position resuming the provider's stream right after this entry. */\n resumeCursor: AttentionStreamPosition;\n item: Record<string, unknown>;\n}\n\nexport interface AttentionPageArgs {\n view: FactoryAttentionView;\n search?: string;\n before?: AttentionStreamPosition;\n limit: number;\n}\n\nexport interface AttentionPageResult {\n entries: AttentionEntry[];\n /** More rows behind the scan once every returned entry is consumed. */\n hasMore: boolean;\n /** Resume point for a scan-budget stop past the last returned entry. */\n continuation?: AttentionStreamPosition;\n}\n\nexport interface AttentionCounts {\n open: number;\n unread: number;\n}\n\nexport interface AttentionLatest {\n key: string;\n at: Date;\n unread: boolean;\n}\n\nexport interface AttentionProvider {\n kind: FactoryAttentionKind;\n counts(scope: AttentionScope): Promise<AttentionCounts>;\n latest(scope: AttentionScope): Promise<AttentionLatest | null>;\n page(scope: AttentionScope, args: AttentionPageArgs): Promise<AttentionPageResult>;\n markAllRead(\n scope: AttentionScope,\n args: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }>;\n}\n\nexport const SCAN_PAGE_SIZE = 50;\n// Receipt filtering is bounded per request; the response cursor resumes after the last scan.\nconst MAX_RECEIPT_SCAN_PAGES = 4;\n\ninterface ScanBatch<T> {\n rows: T[];\n /** Set only when the page budget stopped a scan that still had rows behind it. */\n continuation?: AttentionStreamPosition;\n}\n\n/**\n * One bounded keyset scan, shared by every provider read. Walks newest-first\n * from `before` until the stream runs dry or the page budget is spent; the\n * final batch of a budget stop carries the position to resume from.\n */\nexport async function* scanBatches<T>(\n fetchBatch: (before?: AttentionStreamPosition) => Promise<{ rows: T[]; hasMore: boolean }>,\n positionOf: (row: T) => AttentionStreamPosition,\n before?: AttentionStreamPosition,\n): AsyncGenerator<ScanBatch<T>> {\n let cursor = before;\n for (let scanned = 1; scanned <= MAX_RECEIPT_SCAN_PAGES; scanned += 1) {\n const { rows, hasMore } = await fetchBatch(cursor);\n const last = rows.at(-1);\n if (!last) return;\n const position = positionOf(last);\n yield { rows, ...(hasMore && scanned === MAX_RECEIPT_SCAN_PAGES ? { continuation: position } : {}) };\n if (!hasMore) return;\n cursor = position;\n }\n}\n\n/** Fills a page from the scan, stopping at `limit` entries or at the scan budget. */\nexport async function collectPage<T>(\n batches: AsyncGenerator<ScanBatch<T>>,\n limit: number,\n toEntries: (rows: T[]) => Promise<AttentionEntry[]>,\n): Promise<AttentionPageResult> {\n const entries: AttentionEntry[] = [];\n for await (const batch of batches) {\n for (const entry of await toEntries(batch.rows)) {\n if (entries.length === limit) return { entries, hasMore: true };\n entries.push(entry);\n }\n if (batch.continuation) return { entries, hasMore: true, continuation: batch.continuation };\n }\n return { entries, hasMore: false };\n}\n\n/** Marks every scanned batch read, reporting where a budget stop left off. */\nexport async function markScanRead<T>(\n batches: AsyncGenerator<ScanBatch<T>>,\n markBatch: (rows: T[]) => Promise<void>,\n): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n for await (const batch of batches) {\n await markBatch(batch.rows);\n if (batch.continuation) return { hasMore: true, continuation: batch.continuation };\n }\n return { hasMore: false };\n}\n\nexport function factoryDecisionType(decision: FactoryDeferredDecisionRecord): string {\n return typeof decision.decision.type === 'string' ? decision.decision.type.slice(0, 64) : 'unknown';\n}\n\nfunction failureOccurredAt(decision: FactoryDeferredDecisionRecord): Date {\n return decision.completedAt ?? decision.updatedAt;\n}\n\nexport function matchesView(view: FactoryAttentionView, receipt: FactoryAttentionReceiptRecord | undefined): boolean {\n if (view === 'archived') return receipt?.state === 'archived';\n if (view === 'unread') return receipt === undefined;\n return receipt?.state !== 'archived';\n}\n\nfunction attentionTarget(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined) {\n if (!item) return { kind: 'rules' as const };\n const role = typeof decision.decision.role === 'string' ? decision.decision.role : undefined;\n const session = role ? item.sessions[role] : undefined;\n if (session) {\n return {\n kind: 'thread' as const,\n sessionId: session.sessionId,\n threadId: session.threadId,\n };\n }\n return {\n kind: 'work-item' as const,\n workItemId: item.id,\n board: workItemBoard(item),\n };\n}\n\nexport function workItemBoard(item: WorkItemRow): 'review' | 'work' {\n const review = item.externalSource?.integrationId === 'github' && item.externalSource.type === 'pull-request';\n return review ? 'review' : 'work';\n}\n\nexport class AutomationFailedAttentionProvider implements AttentionProvider {\n readonly kind = 'automation-failed' as const;\n readonly #workItems: WorkItemsStorage;\n\n constructor({ workItems }: { workItems: WorkItemsStorage }) {\n this.#workItems = workItems;\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n const [failedCount, receiptCount, archivedCount] = await Promise.all([\n this.#workItems.countDeferredDecisionsByStatuses({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n statuses: ['failed'],\n }),\n this.#workItems.countAttentionReceipts({ ...receiptScope(scope), kind: this.kind }),\n this.#workItems.countAttentionReceipts({ ...receiptScope(scope), kind: this.kind, state: 'archived' }),\n ]);\n return {\n open: Math.max(0, failedCount - archivedCount),\n unread: Math.max(0, failedCount - receiptCount),\n };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const page = await this.#workItems.listFailedDecisionPage({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n limit: 1,\n });\n const newest = page.decisions[0];\n if (!newest) return null;\n const identity = factoryDecisionAttentionIdentity(newest.id, newest.failureOccurrence);\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: [identity],\n });\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n at: failureOccurredAt(newest),\n unread: receipts.length === 0,\n };\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<FactoryDeferredDecisionRecord>(\n async cursor => {\n const page = await this.#workItems.listFailedDecisionPage({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n before: cursor,\n limit: SCAN_PAGE_SIZE,\n });\n return { rows: page.decisions, hasMore: page.hasMore };\n },\n decision => ({ occurredAt: failureOccurredAt(decision), id: decision.id }),\n before,\n );\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async decisions => {\n const [receiptByKey, itemById] = await Promise.all([\n this.#receiptsFor(scope, decisions),\n this.#linkedItems(scope, decisions),\n ]);\n const entries: AttentionEntry[] = [];\n for (const decision of decisions) {\n const identity = factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence);\n const receipt = receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity));\n if (!matchesView(view, receipt)) continue;\n const item = decision.workItemId ? itemById.get(decision.workItemId) : undefined;\n if (search && !matchesFailureSearch(decision, item, search)) continue;\n entries.push({\n occurredAt: failureOccurredAt(decision),\n resumeCursor: { occurredAt: failureOccurredAt(decision), id: decision.id },\n item: toFailureItem(scope, decision, item, receipt),\n });\n }\n return entries;\n });\n }\n\n async #receiptsFor(\n scope: AttentionScope,\n decisions: FactoryDeferredDecisionRecord[],\n ): Promise<Map<string, FactoryAttentionReceiptRecord>> {\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: decisions.map(decision => factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence)),\n });\n return new Map(receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]));\n }\n\n async #linkedItems(\n scope: AttentionScope,\n decisions: FactoryDeferredDecisionRecord[],\n ): Promise<Map<string, WorkItemRow>> {\n const items = await this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: decisions.flatMap(decision => (decision.workItemId ? [decision.workItemId] : [])),\n });\n return new Map(items.map(item => [item.id, item]));\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async decisions => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: decisions.map(decision =>\n factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence),\n ),\n now,\n });\n });\n }\n}\n\ninterface ResolvedMention {\n mention: WorkItemMentionRow;\n comment: WorkItemCommentRow;\n item: WorkItemRow;\n identity: FactoryAttentionIdentity;\n receipt: FactoryAttentionReceiptRecord | undefined;\n}\n\nexport class MentionAttentionProvider implements AttentionProvider {\n readonly kind = 'mention' as const;\n readonly #workItems: WorkItemsStorage;\n readonly #comments: WorkItemCommentsStorage;\n\n constructor({ workItems, comments }: { workItems: WorkItemsStorage; comments: WorkItemCommentsStorage }) {\n this.#workItems = workItems;\n this.#comments = comments;\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<WorkItemMentionRow>(\n async cursor => {\n const rows = await this.#comments.listMentionsForUser({\n ...receiptScope(scope),\n ...(cursor ? { before: cursor } : {}),\n limit: SCAN_PAGE_SIZE + 1,\n });\n return { rows: rows.slice(0, SCAN_PAGE_SIZE), hasMore: rows.length > SCAN_PAGE_SIZE };\n },\n mention => ({ occurredAt: mention.occurredAt, id: mention.id }),\n before,\n );\n }\n\n /**\n * Mention rows joined to what the inbox needs to show one — dropping the\n * ones it cannot: a deleted comment, or a work item gone from the project.\n */\n async #resolve(scope: AttentionScope, mentions: WorkItemMentionRow[]): Promise<ResolvedMention[]> {\n const [receipts, comments, items] = await Promise.all([\n this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: mentions.map(mention => factoryMentionAttentionIdentity(mention.commentId)),\n }),\n this.#comments.listByIds({\n orgId: scope.orgId,\n ids: [...new Set(mentions.map(mention => mention.commentId))],\n }),\n this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: [...new Set(mentions.map(mention => mention.workItemId))],\n }),\n ]);\n const receiptByKey = new Map(\n receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]),\n );\n const commentById = new Map(comments.map(comment => [comment.id, comment]));\n const itemById = new Map(items.map(item => [item.id, item]));\n\n const resolved: ResolvedMention[] = [];\n for (const mention of mentions) {\n const comment = commentById.get(mention.commentId);\n const item = itemById.get(mention.workItemId);\n if (!comment || comment.deletedAt || !item) continue;\n const identity = factoryMentionAttentionIdentity(mention.commentId);\n resolved.push({\n mention,\n comment,\n item,\n identity,\n receipt: receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity)),\n });\n }\n return resolved;\n }\n\n /**\n * Derived from the same bounded scan as `page`, never from raw aggregates:\n * the badge must count exactly what the views can show, down to stopping at\n * the same scan budget. Aggregates would count orphan mention rows the list\n * can never display or clear.\n */\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n let open = 0;\n let unread = 0;\n for await (const batch of this.#scan(scope)) {\n for (const { receipt } of await this.#resolve(scope, batch.rows)) {\n if (receipt?.state !== 'archived') open += 1;\n if (receipt === undefined) unread += 1;\n }\n }\n return { open, unread };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const mentions = await this.#comments.listMentionsForUser({ ...receiptScope(scope), limit: 10 });\n const newest = (await this.#resolve(scope, mentions))[0];\n if (!newest) return null;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, newest.identity),\n at: newest.mention.occurredAt,\n unread: newest.receipt === undefined,\n };\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async mentions => {\n const entries: AttentionEntry[] = [];\n for (const resolved of await this.#resolve(scope, mentions)) {\n if (!matchesView(view, resolved.receipt)) continue;\n if (search && !matchesMentionSearch(resolved, search)) continue;\n entries.push({\n occurredAt: resolved.mention.occurredAt,\n resumeCursor: { occurredAt: resolved.mention.occurredAt, id: resolved.mention.id },\n item: toMentionItem(scope, resolved),\n });\n }\n return entries;\n });\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async mentions => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: mentions.map(mention => factoryMentionAttentionIdentity(mention.commentId)),\n now,\n });\n });\n }\n}\n\nfunction matchesFailureSearch(\n decision: FactoryDeferredDecisionRecord,\n item: WorkItemRow | undefined,\n search: string,\n): boolean {\n return (\n item?.title.toLowerCase().includes(search) === true ||\n decision.lastError?.toLowerCase().includes(search) === true ||\n factoryDecisionType(decision).toLowerCase().includes(search)\n );\n}\n\nfunction toFailureItem(\n scope: AttentionScope,\n decision: FactoryDeferredDecisionRecord,\n item: WorkItemRow | undefined,\n receipt: FactoryAttentionReceiptRecord | undefined,\n) {\n const failure = factoryDispatchFailureMetadata(decision.failureCode);\n const identity = factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence);\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: 'automation-failed' as const,\n decisionId: decision.id,\n occurrence: decision.failureOccurrence,\n workItemId: decision.workItemId,\n title: item?.title ?? failure.label,\n detail: decision.lastError?.slice(0, 512) ?? failure.label,\n decisionType: factoryDecisionType(decision),\n failureCode: decision.failureCode,\n canRetry: failure.canRetry,\n occurredAt: failureOccurredAt(decision).toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: attentionTarget(decision, item),\n };\n}\n\nfunction matchesMentionSearch({ item, comment }: ResolvedMention, search: string): boolean {\n return (\n item.title.toLowerCase().includes(search) ||\n comment.body.toLowerCase().includes(search) ||\n comment.author.displayName?.toLowerCase().includes(search) === true\n );\n}\n\nfunction toMentionItem(scope: AttentionScope, { mention, comment, item, identity, receipt }: ResolvedMention) {\n const authorName = comment.author.displayName;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: 'mention' as const,\n commentId: mention.commentId,\n occurrence: 0,\n workItemId: mention.workItemId,\n title: item.title,\n detail: comment.body.slice(0, 512),\n authorId: comment.author.id,\n ...(authorName ? { authorName } : {}),\n occurredAt: mention.occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: {\n kind: 'work-item' as const,\n workItemId: mention.workItemId,\n board: workItemBoard(item),\n commentId: mention.commentId,\n },\n };\n}\n\nexport function receiptScope(scope: AttentionScope): { orgId: string; factoryProjectId: string; userId: string } {\n return { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId, userId: scope.userId };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"attention-providers.js","names":["#workItems","#scan","#receiptsFor","#linkedItems","#comments","#resolve"],"sources":["../../src/routes/attention-providers.ts"],"sourcesContent":["/**\n * Per-kind attention providers. Each kind owns its counts, its bounded page\n * scan, and its wire item shape; the route layer k-way merges provider pages\n * on `occurredAt desc` with one resumable cursor per kind.\n */\n\nimport { factoryDispatchFailureMetadata } from '../rules/dispatch-errors.js';\nimport type {\n WorkItemCommentRow,\n WorkItemCommentsStorage,\n WorkItemMentionRow,\n} from '../storage/domains/comments/base.js';\nimport type {\n FactoryAttentionIdentity,\n FactoryAttentionKind,\n FactoryAttentionReceiptRecord,\n FactoryDeferredDecisionRecord,\n FactorySupervisorFindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport {\n factoryAttentionKey,\n factoryDecisionAttentionIdentity,\n factoryMentionAttentionIdentity,\n factorySupervisorFindingAttentionIdentity,\n} from '../storage/domains/work-items/base.js';\nimport type { FactoryHealthFinding } from '../supervisor/health.js';\n\nexport type FactoryAttentionView = 'open' | 'unread' | 'archived';\n\nexport interface AttentionScope {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n}\n\nexport interface AttentionStreamPosition {\n occurredAt: Date;\n id: string;\n}\n\nexport interface AttentionEntry {\n occurredAt: Date;\n /** Position resuming the provider's stream right after this entry. */\n resumeCursor: AttentionStreamPosition;\n item: Record<string, unknown>;\n}\n\nexport interface AttentionPageArgs {\n view: FactoryAttentionView;\n search?: string;\n before?: AttentionStreamPosition;\n limit: number;\n}\n\nexport interface AttentionPageResult {\n entries: AttentionEntry[];\n /** More rows behind the scan once every returned entry is consumed. */\n hasMore: boolean;\n /** Resume point for a scan-budget stop past the last returned entry. */\n continuation?: AttentionStreamPosition;\n}\n\nexport interface AttentionCounts {\n open: number;\n unread: number;\n}\n\nexport interface AttentionLatest {\n key: string;\n at: Date;\n unread: boolean;\n}\n\nexport interface AttentionProvider {\n kind: FactoryAttentionKind;\n counts(scope: AttentionScope): Promise<AttentionCounts>;\n latest(scope: AttentionScope): Promise<AttentionLatest | null>;\n page(scope: AttentionScope, args: AttentionPageArgs): Promise<AttentionPageResult>;\n markAllRead(\n scope: AttentionScope,\n args: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }>;\n}\n\nexport const SCAN_PAGE_SIZE = 50;\n// Receipt filtering is bounded per request; the response cursor resumes after the last scan.\nconst MAX_RECEIPT_SCAN_PAGES = 4;\n\ninterface ScanBatch<T> {\n rows: T[];\n /** Set only when the page budget stopped a scan that still had rows behind it. */\n continuation?: AttentionStreamPosition;\n}\n\n/**\n * One bounded keyset scan, shared by every provider read. Walks newest-first\n * from `before` until the stream runs dry or the page budget is spent; the\n * final batch of a budget stop carries the position to resume from.\n */\nexport async function* scanBatches<T>(\n fetchBatch: (before?: AttentionStreamPosition) => Promise<{ rows: T[]; hasMore: boolean }>,\n positionOf: (row: T) => AttentionStreamPosition,\n before?: AttentionStreamPosition,\n): AsyncGenerator<ScanBatch<T>> {\n let cursor = before;\n for (let scanned = 1; scanned <= MAX_RECEIPT_SCAN_PAGES; scanned += 1) {\n const { rows, hasMore } = await fetchBatch(cursor);\n const last = rows.at(-1);\n if (!last) return;\n const position = positionOf(last);\n yield { rows, ...(hasMore && scanned === MAX_RECEIPT_SCAN_PAGES ? { continuation: position } : {}) };\n if (!hasMore) return;\n cursor = position;\n }\n}\n\n/** Fills a page from the scan, stopping at `limit` entries or at the scan budget. */\nexport async function collectPage<T>(\n batches: AsyncGenerator<ScanBatch<T>>,\n limit: number,\n toEntries: (rows: T[]) => Promise<AttentionEntry[]>,\n): Promise<AttentionPageResult> {\n const entries: AttentionEntry[] = [];\n for await (const batch of batches) {\n for (const entry of await toEntries(batch.rows)) {\n if (entries.length === limit) return { entries, hasMore: true };\n entries.push(entry);\n }\n if (batch.continuation) return { entries, hasMore: true, continuation: batch.continuation };\n }\n return { entries, hasMore: false };\n}\n\n/** Marks every scanned batch read, reporting where a budget stop left off. */\nexport async function markScanRead<T>(\n batches: AsyncGenerator<ScanBatch<T>>,\n markBatch: (rows: T[]) => Promise<void>,\n): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n for await (const batch of batches) {\n await markBatch(batch.rows);\n if (batch.continuation) return { hasMore: true, continuation: batch.continuation };\n }\n return { hasMore: false };\n}\n\nexport function factoryDecisionType(decision: FactoryDeferredDecisionRecord): string {\n return typeof decision.decision.type === 'string' ? decision.decision.type.slice(0, 64) : 'unknown';\n}\n\nfunction failureOccurredAt(decision: FactoryDeferredDecisionRecord): Date {\n return decision.completedAt ?? decision.updatedAt;\n}\n\nexport function matchesView(view: FactoryAttentionView, receipt: FactoryAttentionReceiptRecord | undefined): boolean {\n if (view === 'archived') return receipt?.state === 'archived';\n if (view === 'unread') return receipt === undefined;\n return receipt?.state !== 'archived';\n}\n\nfunction attentionTarget(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined) {\n if (!item) return { kind: 'rules' as const };\n const role = typeof decision.decision.role === 'string' ? decision.decision.role : undefined;\n const session = role ? item.sessions[role] : undefined;\n if (session) {\n return {\n kind: 'thread' as const,\n sessionId: session.sessionId,\n threadId: session.threadId,\n };\n }\n return {\n kind: 'work-item' as const,\n workItemId: item.id,\n board: workItemBoard(item),\n };\n}\n\nexport function workItemBoard(item: WorkItemRow): 'review' | 'work' {\n const review = item.externalSource?.integrationId === 'github' && item.externalSource.type === 'pull-request';\n return review ? 'review' : 'work';\n}\n\nexport class AutomationFailedAttentionProvider implements AttentionProvider {\n readonly kind = 'automation-failed' as const;\n readonly #workItems: WorkItemsStorage;\n\n constructor({ workItems }: { workItems: WorkItemsStorage }) {\n this.#workItems = workItems;\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n const [failedCount, receiptCount, archivedCount] = await Promise.all([\n this.#workItems.countDeferredDecisionsByStatuses({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n statuses: ['failed'],\n }),\n this.#workItems.countAttentionReceipts({ ...receiptScope(scope), kind: this.kind }),\n this.#workItems.countAttentionReceipts({ ...receiptScope(scope), kind: this.kind, state: 'archived' }),\n ]);\n return {\n open: Math.max(0, failedCount - archivedCount),\n unread: Math.max(0, failedCount - receiptCount),\n };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const page = await this.#workItems.listFailedDecisionPage({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n limit: 1,\n });\n const newest = page.decisions[0];\n if (!newest) return null;\n const identity = factoryDecisionAttentionIdentity(newest.id, newest.failureOccurrence);\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: [identity],\n });\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n at: failureOccurredAt(newest),\n unread: receipts.length === 0,\n };\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<FactoryDeferredDecisionRecord>(\n async cursor => {\n const page = await this.#workItems.listFailedDecisionPage({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n before: cursor,\n limit: SCAN_PAGE_SIZE,\n });\n return { rows: page.decisions, hasMore: page.hasMore };\n },\n decision => ({ occurredAt: failureOccurredAt(decision), id: decision.id }),\n before,\n );\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async decisions => {\n const [receiptByKey, itemById] = await Promise.all([\n this.#receiptsFor(scope, decisions),\n this.#linkedItems(scope, decisions),\n ]);\n const entries: AttentionEntry[] = [];\n for (const decision of decisions) {\n const identity = factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence);\n const receipt = receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity));\n if (!matchesView(view, receipt)) continue;\n const item = decision.workItemId ? itemById.get(decision.workItemId) : undefined;\n if (search && !matchesFailureSearch(decision, item, search)) continue;\n entries.push({\n occurredAt: failureOccurredAt(decision),\n resumeCursor: { occurredAt: failureOccurredAt(decision), id: decision.id },\n item: toFailureItem(scope, decision, item, receipt),\n });\n }\n return entries;\n });\n }\n\n async #receiptsFor(\n scope: AttentionScope,\n decisions: FactoryDeferredDecisionRecord[],\n ): Promise<Map<string, FactoryAttentionReceiptRecord>> {\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: decisions.map(decision => factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence)),\n });\n return new Map(receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]));\n }\n\n async #linkedItems(\n scope: AttentionScope,\n decisions: FactoryDeferredDecisionRecord[],\n ): Promise<Map<string, WorkItemRow>> {\n const items = await this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: decisions.flatMap(decision => (decision.workItemId ? [decision.workItemId] : [])),\n });\n return new Map(items.map(item => [item.id, item]));\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async decisions => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: decisions.map(decision =>\n factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence),\n ),\n now,\n });\n });\n }\n}\n\ninterface ResolvedMention {\n mention: WorkItemMentionRow;\n comment: WorkItemCommentRow;\n item: WorkItemRow;\n identity: FactoryAttentionIdentity;\n receipt: FactoryAttentionReceiptRecord | undefined;\n}\n\nfunction supervisorFindingPayload(row: FactorySupervisorFindingRecord): FactoryHealthFinding {\n return row.finding as unknown as FactoryHealthFinding;\n}\n\nexport class SupervisorFindingAttentionProvider implements AttentionProvider {\n readonly kind = 'supervisor-finding' as const;\n readonly #workItems: WorkItemsStorage;\n\n constructor({ workItems }: { workItems: WorkItemsStorage }) {\n this.#workItems = workItems;\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n let open = 0;\n let unread = 0;\n const batches = scanBatches<FactorySupervisorFindingRecord>(\n before =>\n this.#workItems.listSupervisorFindingPage({ ...scope, ...(before ? { before } : {}), limit: SCAN_PAGE_SIZE }),\n row => ({ occurredAt: row.updatedAt, id: row.id }),\n );\n for await (const page of batches) {\n const identities = page.rows.map(row =>\n factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence),\n );\n const receipts = await this.#workItems.listAttentionReceipts({ ...scope, identities });\n const byIdentity = new Map(\n receipts.map(receipt => [`${receipt.sourceId}\\0${receipt.occurrence}`, receipt] as const),\n );\n for (const row of page.rows) {\n const receipt = byIdentity.get(`${row.findingKey}\\0${row.occurrence}`);\n if (receipt?.state === 'archived') continue;\n open += 1;\n if (!receipt) unread += 1;\n }\n }\n return { open, unread };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const page = await this.#workItems.listSupervisorFindingPage({ ...scope, limit: 1 });\n const newest = page.rows[0];\n if (!newest) return null;\n const identity = factorySupervisorFindingAttentionIdentity(newest.findingKey, newest.occurrence);\n const receipts = await this.#workItems.listAttentionReceipts({ ...scope, identities: [identity] });\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n at: newest.updatedAt,\n unread: receipts.length === 0,\n };\n }\n\n page(scope: AttentionScope, args: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(\n scanBatches<FactorySupervisorFindingRecord>(\n before =>\n this.#workItems.listSupervisorFindingPage({ ...scope, ...(before ? { before } : {}), limit: SCAN_PAGE_SIZE }),\n row => ({ occurredAt: row.updatedAt, id: row.id }),\n args.before,\n ),\n args.limit,\n async rows => {\n const identities = rows.map(row => factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence));\n const receipts = await this.#workItems.listAttentionReceipts({ ...scope, identities });\n const byIdentity = new Map(\n receipts.map(receipt => [`${receipt.sourceId}\\0${receipt.occurrence}`, receipt] as const),\n );\n return rows.flatMap(row => {\n const finding = supervisorFindingPayload(row);\n const receipt = byIdentity.get(`${row.findingKey}\\0${row.occurrence}`);\n if (!matchesView(args.view, receipt)) return [];\n const text = `${finding.title} ${finding.evidence}`.toLowerCase();\n if (args.search && !text.includes(args.search)) return [];\n const identity = factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence);\n return [\n {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n occurredAt: row.updatedAt,\n resumeCursor: { occurredAt: row.updatedAt, id: row.id },\n receipt,\n item: {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: this.kind,\n findingKey: row.findingKey,\n occurrence: row.occurrence,\n findingTitle: finding.title,\n evidence: finding.evidence,\n title: finding.title,\n detail: finding.evidence,\n ageMs: finding.ageMs,\n suggestedRepair: finding.suggestedRepair,\n workItemId: finding.workItemId,\n occurredAt: row.updatedAt.toISOString(),\n read: Boolean(receipt),\n archived: receipt?.state === 'archived',\n target: finding.workItemId\n ? { kind: 'work-item', workItemId: finding.workItemId, board: 'work' }\n : { kind: 'rules' },\n },\n },\n ];\n });\n },\n );\n }\n\n markAllRead(\n scope: AttentionScope,\n args: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(\n scanBatches<FactorySupervisorFindingRecord>(\n before =>\n this.#workItems.listSupervisorFindingPage({ ...scope, ...(before ? { before } : {}), limit: SCAN_PAGE_SIZE }),\n row => ({ occurredAt: row.updatedAt, id: row.id }),\n args.before,\n ),\n rows =>\n this.#workItems.markAttentionReceiptsRead({\n ...scope,\n identities: rows.map(row => factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence)),\n now: args.now,\n }),\n );\n }\n}\n\nexport class MentionAttentionProvider implements AttentionProvider {\n readonly kind = 'mention' as const;\n readonly #workItems: WorkItemsStorage;\n readonly #comments: WorkItemCommentsStorage;\n\n constructor({ workItems, comments }: { workItems: WorkItemsStorage; comments: WorkItemCommentsStorage }) {\n this.#workItems = workItems;\n this.#comments = comments;\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<WorkItemMentionRow>(\n async cursor => {\n const rows = await this.#comments.listMentionsForUser({\n ...receiptScope(scope),\n ...(cursor ? { before: cursor } : {}),\n limit: SCAN_PAGE_SIZE + 1,\n });\n return { rows: rows.slice(0, SCAN_PAGE_SIZE), hasMore: rows.length > SCAN_PAGE_SIZE };\n },\n mention => ({ occurredAt: mention.occurredAt, id: mention.id }),\n before,\n );\n }\n\n /**\n * Mention rows joined to what the inbox needs to show one — dropping the\n * ones it cannot: a deleted comment, or a work item gone from the project.\n */\n async #resolve(scope: AttentionScope, mentions: WorkItemMentionRow[]): Promise<ResolvedMention[]> {\n const [receipts, comments, items] = await Promise.all([\n this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: mentions.map(mention => factoryMentionAttentionIdentity(mention.commentId)),\n }),\n this.#comments.listByIds({\n orgId: scope.orgId,\n ids: [...new Set(mentions.map(mention => mention.commentId))],\n }),\n this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: [...new Set(mentions.map(mention => mention.workItemId))],\n }),\n ]);\n const receiptByKey = new Map(\n receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]),\n );\n const commentById = new Map(comments.map(comment => [comment.id, comment]));\n const itemById = new Map(items.map(item => [item.id, item]));\n\n const resolved: ResolvedMention[] = [];\n for (const mention of mentions) {\n const comment = commentById.get(mention.commentId);\n const item = itemById.get(mention.workItemId);\n if (!comment || comment.deletedAt || !item) continue;\n const identity = factoryMentionAttentionIdentity(mention.commentId);\n resolved.push({\n mention,\n comment,\n item,\n identity,\n receipt: receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity)),\n });\n }\n return resolved;\n }\n\n /**\n * Derived from the same bounded scan as `page`, never from raw aggregates:\n * the badge must count exactly what the views can show, down to stopping at\n * the same scan budget. Aggregates would count orphan mention rows the list\n * can never display or clear.\n */\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n let open = 0;\n let unread = 0;\n for await (const batch of this.#scan(scope)) {\n for (const { receipt } of await this.#resolve(scope, batch.rows)) {\n if (receipt?.state !== 'archived') open += 1;\n if (receipt === undefined) unread += 1;\n }\n }\n return { open, unread };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const mentions = await this.#comments.listMentionsForUser({ ...receiptScope(scope), limit: 10 });\n const newest = (await this.#resolve(scope, mentions))[0];\n if (!newest) return null;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, newest.identity),\n at: newest.mention.occurredAt,\n unread: newest.receipt === undefined,\n };\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async mentions => {\n const entries: AttentionEntry[] = [];\n for (const resolved of await this.#resolve(scope, mentions)) {\n if (!matchesView(view, resolved.receipt)) continue;\n if (search && !matchesMentionSearch(resolved, search)) continue;\n entries.push({\n occurredAt: resolved.mention.occurredAt,\n resumeCursor: { occurredAt: resolved.mention.occurredAt, id: resolved.mention.id },\n item: toMentionItem(scope, resolved),\n });\n }\n return entries;\n });\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async mentions => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: mentions.map(mention => factoryMentionAttentionIdentity(mention.commentId)),\n now,\n });\n });\n }\n}\n\nfunction matchesFailureSearch(\n decision: FactoryDeferredDecisionRecord,\n item: WorkItemRow | undefined,\n search: string,\n): boolean {\n return (\n item?.title.toLowerCase().includes(search) === true ||\n decision.lastError?.toLowerCase().includes(search) === true ||\n factoryDecisionType(decision).toLowerCase().includes(search)\n );\n}\n\nfunction toFailureItem(\n scope: AttentionScope,\n decision: FactoryDeferredDecisionRecord,\n item: WorkItemRow | undefined,\n receipt: FactoryAttentionReceiptRecord | undefined,\n) {\n const failure = factoryDispatchFailureMetadata(decision.failureCode);\n const identity = factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence);\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: 'automation-failed' as const,\n decisionId: decision.id,\n occurrence: decision.failureOccurrence,\n workItemId: decision.workItemId,\n title: item?.title ?? failure.label,\n detail: decision.lastError?.slice(0, 512) ?? failure.label,\n decisionType: factoryDecisionType(decision),\n failureCode: decision.failureCode,\n canRetry: failure.canRetry,\n occurredAt: failureOccurredAt(decision).toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: attentionTarget(decision, item),\n };\n}\n\nfunction matchesMentionSearch({ item, comment }: ResolvedMention, search: string): boolean {\n return (\n item.title.toLowerCase().includes(search) ||\n comment.body.toLowerCase().includes(search) ||\n comment.author.displayName?.toLowerCase().includes(search) === true\n );\n}\n\nfunction toMentionItem(scope: AttentionScope, { mention, comment, item, identity, receipt }: ResolvedMention) {\n const authorName = comment.author.displayName;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: 'mention' as const,\n commentId: mention.commentId,\n occurrence: 0,\n workItemId: mention.workItemId,\n title: item.title,\n detail: comment.body.slice(0, 512),\n authorId: comment.author.id,\n ...(authorName ? { authorName } : {}),\n occurredAt: mention.occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: {\n kind: 'work-item' as const,\n workItemId: mention.workItemId,\n board: workItemBoard(item),\n commentId: mention.commentId,\n },\n };\n}\n\nexport function receiptScope(scope: AttentionScope): { orgId: string; factoryProjectId: string; userId: string } {\n return { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId, userId: scope.userId };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attention.d.ts","sourceRoot":"","sources":["../../src/routes/attention.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAG/C,OAAO,KAAK,EAIV,cAAc,EAGf,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"attention.d.ts","sourceRoot":"","sources":["../../src/routes/attention.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAG/C,OAAO,KAAK,EAIV,cAAc,EAGf,MAAM,0BAA0B,CAAC;AAOlC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAO/D,UAAU,0BAA0B;IAClC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,QAAQ,EAAE,uBAAuB,CAAC;IAClC,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,GAAG;QAAE,QAAQ,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CACpF;AAqMD,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,0BAA0B,GAAG,QAAQ,EAAE,CA+HzF"}
|
package/dist/routes/attention.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { factoryAttentionKey } from "../storage/domains/work-items/base.js";
|
|
2
|
-
import { AutomationFailedAttentionProvider, MentionAttentionProvider, factoryDecisionType } from "./attention-providers.js";
|
|
2
|
+
import { AutomationFailedAttentionProvider, MentionAttentionProvider, SupervisorFindingAttentionProvider, factoryDecisionType } from "./attention-providers.js";
|
|
3
3
|
import { ActivityAttentionProvider } from "./attention-activity.js";
|
|
4
4
|
import { registerApiRoute } from "@mastra/core/server";
|
|
5
5
|
//#region src/routes/attention.ts
|
|
6
6
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
|
+
const SUPERVISOR_FINDING_KEY_RE = /^[a-z0-9:_-]{1,256}$/i;
|
|
7
8
|
const DEFAULT_PAGE_SIZE = 25;
|
|
8
9
|
const MAX_PAGE_SIZE = 50;
|
|
9
10
|
function parseAttentionView(raw) {
|
|
@@ -16,10 +17,14 @@ function parseAttentionLimit(raw) {
|
|
|
16
17
|
return Math.max(1, Math.min(MAX_PAGE_SIZE, parsed));
|
|
17
18
|
}
|
|
18
19
|
function isAttentionKind(value) {
|
|
19
|
-
return value === "automation-failed" || value === "mention" || value === "activity";
|
|
20
|
+
return value === "automation-failed" || value === "mention" || value === "activity" || value === "supervisor-finding";
|
|
20
21
|
}
|
|
21
22
|
/** Kinds the sidebar badge and the notification sound answer to. */
|
|
22
|
-
const BADGE_KINDS = /* @__PURE__ */ new Set([
|
|
23
|
+
const BADGE_KINDS = /* @__PURE__ */ new Set([
|
|
24
|
+
"automation-failed",
|
|
25
|
+
"mention",
|
|
26
|
+
"supervisor-finding"
|
|
27
|
+
]);
|
|
23
28
|
function parseAttentionTier(raw) {
|
|
24
29
|
if (raw === void 0) return "all";
|
|
25
30
|
return raw === "badge" || raw === "activity" ? raw : void 0;
|
|
@@ -138,7 +143,8 @@ function receiptRoute(dependencies, verb, action) {
|
|
|
138
143
|
const kind = context.req.param("kind");
|
|
139
144
|
const sourceId = context.req.param("sourceId");
|
|
140
145
|
const occurrence = parseOccurrence(context.req.param("occurrence"));
|
|
141
|
-
|
|
146
|
+
const validSourceId = kind === "supervisor-finding" ? SUPERVISOR_FINDING_KEY_RE.test(sourceId) : UUID_RE.test(sourceId);
|
|
147
|
+
if (!kind || !isAttentionKind(kind) || !sourceId || !validSourceId || occurrence === void 0) return context.json({ error: "invalid_attention_item" }, 422);
|
|
142
148
|
await dependencies.workItems.ensureReady();
|
|
143
149
|
const receipt = await dependencies.workItems.setAttentionReceipt({
|
|
144
150
|
orgId: resolved.orgId,
|
|
@@ -166,6 +172,7 @@ function buildAttentionRoutes(dependencies) {
|
|
|
166
172
|
const { workItems, comments } = dependencies;
|
|
167
173
|
const providers = [
|
|
168
174
|
new AutomationFailedAttentionProvider({ workItems }),
|
|
175
|
+
new SupervisorFindingAttentionProvider({ workItems }),
|
|
169
176
|
new MentionAttentionProvider({
|
|
170
177
|
workItems,
|
|
171
178
|
comments
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attention.js","names":[],"sources":["../../src/routes/attention.ts"],"sourcesContent":["/**\n * Attention routes: k-way merge of the per-kind providers on `occurredAt desc`.\n * The wire cursor is a per-kind map — each kind's stream resumes independently,\n * `null` meaning \"not started yet\", an absent kind meaning \"exhausted\".\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\n\nimport type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';\nimport type {\n FactoryAttentionKind,\n FactoryAttentionReceiptAction,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { factoryAttentionKey } from '../storage/domains/work-items/base.js';\nimport { ActivityAttentionProvider } from './attention-activity.js';\nimport type {\n AttentionLatest,\n AttentionPageResult,\n AttentionProvider,\n AttentionScope,\n AttentionStreamPosition,\n FactoryAttentionView,\n} from './attention-providers.js';\nimport { AutomationFailedAttentionProvider, MentionAttentionProvider } from './attention-providers.js';\n\nexport { factoryDecisionType } from './attention-providers.js';\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst DEFAULT_PAGE_SIZE = 25;\nconst MAX_PAGE_SIZE = 50;\n\ninterface AttentionRouteDependencies {\n workItems: WorkItemsStorage;\n comments: WorkItemCommentsStorage;\n resolveProject(context: unknown): Promise<AttentionScope | { response: Response }>;\n}\n\ntype AttentionCursorMap = Map<FactoryAttentionKind, AttentionStreamPosition | undefined>;\n\nfunction parseAttentionView(raw: string | undefined): FactoryAttentionView | undefined {\n if (!raw || raw === 'open') return 'open';\n if (raw === 'unread' || raw === 'archived') return raw;\n return undefined;\n}\n\nfunction parseAttentionLimit(raw: string | undefined): number {\n const parsed = raw ? Number.parseInt(raw, 10) : DEFAULT_PAGE_SIZE;\n if (!Number.isFinite(parsed)) return DEFAULT_PAGE_SIZE;\n return Math.max(1, Math.min(MAX_PAGE_SIZE, parsed));\n}\n\nfunction isAttentionKind(value: string): value is FactoryAttentionKind {\n return value === 'automation-failed' || value === 'mention' || value === 'activity';\n}\n\n/** Kinds the sidebar badge and the notification sound answer to. */\nconst BADGE_KINDS: ReadonlySet<FactoryAttentionKind> = new Set(['automation-failed', 'mention']);\n\ntype AttentionTier = 'all' | 'badge' | 'activity';\n\nfunction parseAttentionTier(raw: string | undefined): AttentionTier | undefined {\n if (raw === undefined) return 'all';\n return raw === 'badge' || raw === 'activity' ? raw : undefined;\n}\n\nfunction kindInTier(tier: AttentionTier, kind: FactoryAttentionKind): boolean {\n if (tier === 'all') return true;\n return tier === 'badge' ? BADGE_KINDS.has(kind) : !BADGE_KINDS.has(kind);\n}\n\nfunction encodeAttentionCursor(cursors: AttentionCursorMap): string {\n const wire: Record<string, [string, string] | null> = {};\n for (const [kind, position] of cursors) {\n wire[kind] = position ? [position.occurredAt.toISOString(), position.id] : null;\n }\n return Buffer.from(JSON.stringify(wire), 'utf8').toString('base64url');\n}\n\nfunction parseStreamPosition(value: unknown): AttentionStreamPosition | undefined {\n if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== 'string' || typeof value[1] !== 'string') {\n return undefined;\n }\n const occurredAt = new Date(value[0]);\n if (Number.isNaN(occurredAt.getTime()) || !UUID_RE.test(value[1])) return undefined;\n return { occurredAt, id: value[1] };\n}\n\nfunction parseAttentionCursor(raw: string | undefined): AttentionCursorMap | undefined {\n if (!raw) return undefined;\n try {\n const decoded: unknown = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'));\n // Cursors minted before the inbox merged kinds are a bare position over the\n // only stream there was. Held by anyone mid-list when this deploys, so they\n // resume that stream rather than 400: mentions arrive on their next load.\n if (Array.isArray(decoded)) {\n const legacy = parseStreamPosition(decoded);\n return legacy ? new Map([['automation-failed', legacy]]) : undefined;\n }\n if (!decoded || typeof decoded !== 'object') return undefined;\n const cursors: AttentionCursorMap = new Map();\n for (const [kind, value] of Object.entries(decoded)) {\n if (!isAttentionKind(kind)) return undefined;\n if (value === null) {\n cursors.set(kind, undefined);\n continue;\n }\n const position = parseStreamPosition(value);\n if (!position) return undefined;\n cursors.set(kind, position);\n }\n return cursors.size > 0 ? cursors : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction parseOccurrence(raw: string | undefined): number | undefined {\n if (!raw || !/^(0|[1-9]\\d*)$/.test(raw)) return undefined;\n const occurrence = Number(raw);\n return Number.isSafeInteger(occurrence) ? occurrence : undefined;\n}\n\ninterface MergedAttentionPage {\n items: Array<Record<string, unknown>>;\n hasMore: boolean;\n nextCursor?: string;\n}\n\n/**\n * Take the newest `limit` entries across provider pages. Each kind's next\n * cursor is the resume position of its last consumed entry; a kind consumed to\n * the end inherits the provider's own continuation.\n */\nfunction mergeAttentionPages(\n pages: Array<{\n kind: FactoryAttentionKind;\n incoming: AttentionStreamPosition | undefined;\n result: AttentionPageResult;\n }>,\n limit: number,\n): MergedAttentionPage {\n const consumed = new Map(pages.map(page => [page.kind, 0]));\n const items: Array<Record<string, unknown>> = [];\n while (items.length < limit) {\n let best: { kind: FactoryAttentionKind; at: number } | undefined;\n for (const page of pages) {\n const next = page.result.entries[consumed.get(page.kind) ?? 0];\n if (!next) continue;\n const at = next.occurredAt.getTime();\n if (!best || at > best.at) best = { kind: page.kind, at };\n }\n if (!best) break;\n const index = consumed.get(best.kind) ?? 0;\n const entry = pages.find(page => page.kind === best.kind)?.result.entries[index];\n if (!entry) break;\n items.push(entry.item);\n consumed.set(best.kind, index + 1);\n }\n\n const nextCursors: AttentionCursorMap = new Map();\n let hasMore = false;\n for (const page of pages) {\n const used = consumed.get(page.kind) ?? 0;\n const entries = page.result.entries;\n if (used < entries.length) {\n hasMore = true;\n const lastConsumed = used > 0 ? entries[used - 1] : undefined;\n nextCursors.set(page.kind, lastConsumed ? lastConsumed.resumeCursor : page.incoming);\n continue;\n }\n if (page.result.hasMore) {\n hasMore = true;\n nextCursors.set(page.kind, page.result.continuation ?? entries.at(-1)?.resumeCursor ?? page.incoming);\n }\n }\n return {\n items,\n hasMore,\n ...(hasMore && nextCursors.size > 0 ? { nextCursor: encodeAttentionCursor(nextCursors) } : {}),\n };\n}\n\nfunction newestLatest(latests: Array<AttentionLatest | null>): AttentionLatest | null {\n let newest: AttentionLatest | null = null;\n for (const latest of latests) {\n if (!latest) continue;\n if (!newest || latest.at.getTime() > newest.at.getTime()) newest = latest;\n }\n return newest;\n}\n\nfunction receiptRoute(\n dependencies: AttentionRouteDependencies,\n verb: 'read' | 'archive' | 'restore',\n action: FactoryAttentionReceiptAction,\n): ApiRoute {\n return registerApiRoute(`/web/factory/projects/:id/attention/:kind/:sourceId/:occurrence/${verb}`, {\n method: 'POST',\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const kind = context.req.param('kind');\n const sourceId = context.req.param('sourceId');\n const occurrence = parseOccurrence(context.req.param('occurrence'));\n if (!kind || !isAttentionKind(kind) || !sourceId || !UUID_RE.test(sourceId) || occurrence === undefined) {\n return context.json({ error: 'invalid_attention_item' }, 422);\n }\n await dependencies.workItems.ensureReady();\n const receipt = await dependencies.workItems.setAttentionReceipt({\n orgId: resolved.orgId,\n factoryProjectId: resolved.factoryProjectId,\n userId: resolved.userId,\n identity: { kind, sourceId, occurrence },\n action,\n now: new Date(),\n });\n if (!receipt) return context.json({ error: 'attention_item_not_current' }, 409);\n return context.json({\n receipt: {\n key: factoryAttentionKey(resolved.factoryProjectId, receipt),\n state: receipt.state,\n readAt: receipt.readAt.toISOString(),\n archivedAt: receipt.archivedAt?.toISOString() ?? null,\n },\n });\n },\n });\n}\n\nexport function buildAttentionRoutes(dependencies: AttentionRouteDependencies): ApiRoute[] {\n const { workItems, comments } = dependencies;\n const providers: AttentionProvider[] = [\n new AutomationFailedAttentionProvider({ workItems }),\n new MentionAttentionProvider({ workItems, comments }),\n new ActivityAttentionProvider({ workItems, comments }),\n ];\n\n return [\n registerApiRoute('/web/factory/projects/:id/attention', {\n method: 'GET',\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const view = parseAttentionView(context.req.query('view'));\n if (view === undefined) return context.json({ error: 'invalid_attention_view' }, 400);\n // `tier` scopes the item list only; the counts always describe every\n // tier, so the badge popover can page badge kinds without losing the\n // activity numbers.\n const tier = parseAttentionTier(context.req.query('tier'));\n if (tier === undefined) return context.json({ error: 'invalid_attention_tier' }, 400);\n const cursorRaw = context.req.query('before');\n const before = parseAttentionCursor(cursorRaw);\n if (cursorRaw && !before) return context.json({ error: 'invalid_cursor' }, 400);\n await workItems.ensureReady();\n await comments.ensureReady();\n\n const search = context.req.query('search')?.trim().toLowerCase().slice(0, 200);\n const limit = parseAttentionLimit(context.req.query('limit'));\n const active = providers.filter(\n provider => kindInTier(tier, provider.kind) && (!before || before.has(provider.kind)),\n );\n\n const [approvalCount, summaries, pages] = await Promise.all([\n workItems.countDeferredDecisionsByStatuses({\n orgId: resolved.orgId,\n factoryProjectId: resolved.factoryProjectId,\n statuses: ['proposed'],\n }),\n Promise.all(\n providers.map(async provider => ({\n kind: provider.kind,\n counts: await provider.counts(resolved),\n latest: await provider.latest(resolved),\n })),\n ),\n Promise.all(\n active.map(async provider => ({\n kind: provider.kind,\n incoming: before?.get(provider.kind),\n result: await provider.page(resolved, {\n view,\n ...(search ? { search } : {}),\n before: before?.get(provider.kind),\n limit,\n }),\n })),\n ),\n ]);\n\n // The badge tier and the activity tier are counted apart: activity\n // leaking into `latests` would ring the notification sound on every\n // teammate comment.\n const badge = summaries.filter(summary => BADGE_KINDS.has(summary.kind));\n const activity = summaries.filter(summary => !BADGE_KINDS.has(summary.kind));\n const sum = (rows: typeof summaries, field: 'open' | 'unread') =>\n rows.reduce((total, row) => total + row.counts[field], 0);\n const openCount = sum(badge, 'open') + approvalCount;\n const unreadCount = sum(badge, 'unread');\n // An unread item must never be masked by a newer already-read one of\n // another kind — the streams are independent.\n const latests = badge.map(summary => summary.latest);\n const unreadLatests = latests.filter(latest => latest?.unread ?? false);\n const latest = unreadLatests.length > 0 ? newestLatest(unreadLatests) : newestLatest(latests);\n const merged = mergeAttentionPages(pages, limit);\n\n return context.json({\n items: merged.items,\n openCount,\n approvalCount,\n badgeCount: unreadCount + approvalCount,\n unreadCount,\n activityUnreadCount: sum(activity, 'unread'),\n latestOccurrenceKey: latest?.key ?? null,\n latestOccurrenceAt: latest?.at.toISOString() ?? null,\n latestOccurrenceUnread: latest?.unread ?? false,\n hasMore: merged.hasMore,\n ...(merged.nextCursor ? { nextCursor: merged.nextCursor } : {}),\n });\n },\n }),\n registerApiRoute('/web/factory/projects/:id/attention/read-all', {\n method: 'POST',\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const cursorRaw = context.req.query('before');\n const before = parseAttentionCursor(cursorRaw);\n if (cursorRaw && !before) return context.json({ error: 'invalid_cursor' }, 400);\n await workItems.ensureReady();\n await comments.ensureReady();\n\n const now = new Date();\n const active = providers.filter(provider => !before || before.has(provider.kind));\n const nextCursors: AttentionCursorMap = new Map();\n let hasMore = false;\n for (const provider of active) {\n const result = await provider.markAllRead(resolved, { before: before?.get(provider.kind), now });\n if (result.hasMore) {\n hasMore = true;\n if (result.continuation) nextCursors.set(provider.kind, result.continuation);\n }\n }\n return context.json({\n ok: true,\n hasMore,\n ...(hasMore && nextCursors.size > 0 ? { nextCursor: encodeAttentionCursor(nextCursors) } : {}),\n });\n },\n }),\n receiptRoute(dependencies, 'read', 'read'),\n receiptRoute(dependencies, 'archive', 'archive'),\n receiptRoute(dependencies, 'restore', 'restore'),\n ];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"attention.js","names":[],"sources":["../../src/routes/attention.ts"],"sourcesContent":["/**\n * Attention routes: k-way merge of the per-kind providers on `occurredAt desc`.\n * The wire cursor is a per-kind map — each kind's stream resumes independently,\n * `null` meaning \"not started yet\", an absent kind meaning \"exhausted\".\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\n\nimport type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';\nimport type {\n FactoryAttentionKind,\n FactoryAttentionReceiptAction,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { factoryAttentionKey } from '../storage/domains/work-items/base.js';\nimport { ActivityAttentionProvider } from './attention-activity.js';\nimport type {\n AttentionLatest,\n AttentionPageResult,\n AttentionProvider,\n AttentionScope,\n AttentionStreamPosition,\n FactoryAttentionView,\n} from './attention-providers.js';\nimport {\n AutomationFailedAttentionProvider,\n MentionAttentionProvider,\n SupervisorFindingAttentionProvider,\n} from './attention-providers.js';\n\nexport { factoryDecisionType } from './attention-providers.js';\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst SUPERVISOR_FINDING_KEY_RE = /^[a-z0-9:_-]{1,256}$/i;\nconst DEFAULT_PAGE_SIZE = 25;\nconst MAX_PAGE_SIZE = 50;\n\ninterface AttentionRouteDependencies {\n workItems: WorkItemsStorage;\n comments: WorkItemCommentsStorage;\n resolveProject(context: unknown): Promise<AttentionScope | { response: Response }>;\n}\n\ntype AttentionCursorMap = Map<FactoryAttentionKind, AttentionStreamPosition | undefined>;\n\nfunction parseAttentionView(raw: string | undefined): FactoryAttentionView | undefined {\n if (!raw || raw === 'open') return 'open';\n if (raw === 'unread' || raw === 'archived') return raw;\n return undefined;\n}\n\nfunction parseAttentionLimit(raw: string | undefined): number {\n const parsed = raw ? Number.parseInt(raw, 10) : DEFAULT_PAGE_SIZE;\n if (!Number.isFinite(parsed)) return DEFAULT_PAGE_SIZE;\n return Math.max(1, Math.min(MAX_PAGE_SIZE, parsed));\n}\n\nfunction isAttentionKind(value: string): value is FactoryAttentionKind {\n return value === 'automation-failed' || value === 'mention' || value === 'activity' || value === 'supervisor-finding';\n}\n\n/** Kinds the sidebar badge and the notification sound answer to. */\nconst BADGE_KINDS: ReadonlySet<FactoryAttentionKind> = new Set(['automation-failed', 'mention', 'supervisor-finding']);\n\ntype AttentionTier = 'all' | 'badge' | 'activity';\n\nfunction parseAttentionTier(raw: string | undefined): AttentionTier | undefined {\n if (raw === undefined) return 'all';\n return raw === 'badge' || raw === 'activity' ? raw : undefined;\n}\n\nfunction kindInTier(tier: AttentionTier, kind: FactoryAttentionKind): boolean {\n if (tier === 'all') return true;\n return tier === 'badge' ? BADGE_KINDS.has(kind) : !BADGE_KINDS.has(kind);\n}\n\nfunction encodeAttentionCursor(cursors: AttentionCursorMap): string {\n const wire: Record<string, [string, string] | null> = {};\n for (const [kind, position] of cursors) {\n wire[kind] = position ? [position.occurredAt.toISOString(), position.id] : null;\n }\n return Buffer.from(JSON.stringify(wire), 'utf8').toString('base64url');\n}\n\nfunction parseStreamPosition(value: unknown): AttentionStreamPosition | undefined {\n if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== 'string' || typeof value[1] !== 'string') {\n return undefined;\n }\n const occurredAt = new Date(value[0]);\n if (Number.isNaN(occurredAt.getTime()) || !UUID_RE.test(value[1])) return undefined;\n return { occurredAt, id: value[1] };\n}\n\nfunction parseAttentionCursor(raw: string | undefined): AttentionCursorMap | undefined {\n if (!raw) return undefined;\n try {\n const decoded: unknown = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'));\n // Cursors minted before the inbox merged kinds are a bare position over the\n // only stream there was. Held by anyone mid-list when this deploys, so they\n // resume that stream rather than 400: mentions arrive on their next load.\n if (Array.isArray(decoded)) {\n const legacy = parseStreamPosition(decoded);\n return legacy ? new Map([['automation-failed', legacy]]) : undefined;\n }\n if (!decoded || typeof decoded !== 'object') return undefined;\n const cursors: AttentionCursorMap = new Map();\n for (const [kind, value] of Object.entries(decoded)) {\n if (!isAttentionKind(kind)) return undefined;\n if (value === null) {\n cursors.set(kind, undefined);\n continue;\n }\n const position = parseStreamPosition(value);\n if (!position) return undefined;\n cursors.set(kind, position);\n }\n return cursors.size > 0 ? cursors : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction parseOccurrence(raw: string | undefined): number | undefined {\n if (!raw || !/^(0|[1-9]\\d*)$/.test(raw)) return undefined;\n const occurrence = Number(raw);\n return Number.isSafeInteger(occurrence) ? occurrence : undefined;\n}\n\ninterface MergedAttentionPage {\n items: Array<Record<string, unknown>>;\n hasMore: boolean;\n nextCursor?: string;\n}\n\n/**\n * Take the newest `limit` entries across provider pages. Each kind's next\n * cursor is the resume position of its last consumed entry; a kind consumed to\n * the end inherits the provider's own continuation.\n */\nfunction mergeAttentionPages(\n pages: Array<{\n kind: FactoryAttentionKind;\n incoming: AttentionStreamPosition | undefined;\n result: AttentionPageResult;\n }>,\n limit: number,\n): MergedAttentionPage {\n const consumed = new Map(pages.map(page => [page.kind, 0]));\n const items: Array<Record<string, unknown>> = [];\n while (items.length < limit) {\n let best: { kind: FactoryAttentionKind; at: number } | undefined;\n for (const page of pages) {\n const next = page.result.entries[consumed.get(page.kind) ?? 0];\n if (!next) continue;\n const at = next.occurredAt.getTime();\n if (!best || at > best.at) best = { kind: page.kind, at };\n }\n if (!best) break;\n const index = consumed.get(best.kind) ?? 0;\n const entry = pages.find(page => page.kind === best.kind)?.result.entries[index];\n if (!entry) break;\n items.push(entry.item);\n consumed.set(best.kind, index + 1);\n }\n\n const nextCursors: AttentionCursorMap = new Map();\n let hasMore = false;\n for (const page of pages) {\n const used = consumed.get(page.kind) ?? 0;\n const entries = page.result.entries;\n if (used < entries.length) {\n hasMore = true;\n const lastConsumed = used > 0 ? entries[used - 1] : undefined;\n nextCursors.set(page.kind, lastConsumed ? lastConsumed.resumeCursor : page.incoming);\n continue;\n }\n if (page.result.hasMore) {\n hasMore = true;\n nextCursors.set(page.kind, page.result.continuation ?? entries.at(-1)?.resumeCursor ?? page.incoming);\n }\n }\n return {\n items,\n hasMore,\n ...(hasMore && nextCursors.size > 0 ? { nextCursor: encodeAttentionCursor(nextCursors) } : {}),\n };\n}\n\nfunction newestLatest(latests: Array<AttentionLatest | null>): AttentionLatest | null {\n let newest: AttentionLatest | null = null;\n for (const latest of latests) {\n if (!latest) continue;\n if (!newest || latest.at.getTime() > newest.at.getTime()) newest = latest;\n }\n return newest;\n}\n\nfunction receiptRoute(\n dependencies: AttentionRouteDependencies,\n verb: 'read' | 'archive' | 'restore',\n action: FactoryAttentionReceiptAction,\n): ApiRoute {\n return registerApiRoute(`/web/factory/projects/:id/attention/:kind/:sourceId/:occurrence/${verb}`, {\n method: 'POST',\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const kind = context.req.param('kind');\n const sourceId = context.req.param('sourceId');\n const occurrence = parseOccurrence(context.req.param('occurrence'));\n const validSourceId =\n kind === 'supervisor-finding' ? SUPERVISOR_FINDING_KEY_RE.test(sourceId) : UUID_RE.test(sourceId);\n if (!kind || !isAttentionKind(kind) || !sourceId || !validSourceId || occurrence === undefined) {\n return context.json({ error: 'invalid_attention_item' }, 422);\n }\n await dependencies.workItems.ensureReady();\n const receipt = await dependencies.workItems.setAttentionReceipt({\n orgId: resolved.orgId,\n factoryProjectId: resolved.factoryProjectId,\n userId: resolved.userId,\n identity: { kind, sourceId, occurrence },\n action,\n now: new Date(),\n });\n if (!receipt) return context.json({ error: 'attention_item_not_current' }, 409);\n return context.json({\n receipt: {\n key: factoryAttentionKey(resolved.factoryProjectId, receipt),\n state: receipt.state,\n readAt: receipt.readAt.toISOString(),\n archivedAt: receipt.archivedAt?.toISOString() ?? null,\n },\n });\n },\n });\n}\n\nexport function buildAttentionRoutes(dependencies: AttentionRouteDependencies): ApiRoute[] {\n const { workItems, comments } = dependencies;\n const providers: AttentionProvider[] = [\n new AutomationFailedAttentionProvider({ workItems }),\n new SupervisorFindingAttentionProvider({ workItems }),\n new MentionAttentionProvider({ workItems, comments }),\n new ActivityAttentionProvider({ workItems, comments }),\n ];\n\n return [\n registerApiRoute('/web/factory/projects/:id/attention', {\n method: 'GET',\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const view = parseAttentionView(context.req.query('view'));\n if (view === undefined) return context.json({ error: 'invalid_attention_view' }, 400);\n // `tier` scopes the item list only; the counts always describe every\n // tier, so the badge popover can page badge kinds without losing the\n // activity numbers.\n const tier = parseAttentionTier(context.req.query('tier'));\n if (tier === undefined) return context.json({ error: 'invalid_attention_tier' }, 400);\n const cursorRaw = context.req.query('before');\n const before = parseAttentionCursor(cursorRaw);\n if (cursorRaw && !before) return context.json({ error: 'invalid_cursor' }, 400);\n await workItems.ensureReady();\n await comments.ensureReady();\n\n const search = context.req.query('search')?.trim().toLowerCase().slice(0, 200);\n const limit = parseAttentionLimit(context.req.query('limit'));\n const active = providers.filter(\n provider => kindInTier(tier, provider.kind) && (!before || before.has(provider.kind)),\n );\n\n const [approvalCount, summaries, pages] = await Promise.all([\n workItems.countDeferredDecisionsByStatuses({\n orgId: resolved.orgId,\n factoryProjectId: resolved.factoryProjectId,\n statuses: ['proposed'],\n }),\n Promise.all(\n providers.map(async provider => ({\n kind: provider.kind,\n counts: await provider.counts(resolved),\n latest: await provider.latest(resolved),\n })),\n ),\n Promise.all(\n active.map(async provider => ({\n kind: provider.kind,\n incoming: before?.get(provider.kind),\n result: await provider.page(resolved, {\n view,\n ...(search ? { search } : {}),\n before: before?.get(provider.kind),\n limit,\n }),\n })),\n ),\n ]);\n\n // The badge tier and the activity tier are counted apart: activity\n // leaking into `latests` would ring the notification sound on every\n // teammate comment.\n const badge = summaries.filter(summary => BADGE_KINDS.has(summary.kind));\n const activity = summaries.filter(summary => !BADGE_KINDS.has(summary.kind));\n const sum = (rows: typeof summaries, field: 'open' | 'unread') =>\n rows.reduce((total, row) => total + row.counts[field], 0);\n const openCount = sum(badge, 'open') + approvalCount;\n const unreadCount = sum(badge, 'unread');\n // An unread item must never be masked by a newer already-read one of\n // another kind — the streams are independent.\n const latests = badge.map(summary => summary.latest);\n const unreadLatests = latests.filter(latest => latest?.unread ?? false);\n const latest = unreadLatests.length > 0 ? newestLatest(unreadLatests) : newestLatest(latests);\n const merged = mergeAttentionPages(pages, limit);\n\n return context.json({\n items: merged.items,\n openCount,\n approvalCount,\n badgeCount: unreadCount + approvalCount,\n unreadCount,\n activityUnreadCount: sum(activity, 'unread'),\n latestOccurrenceKey: latest?.key ?? null,\n latestOccurrenceAt: latest?.at.toISOString() ?? null,\n latestOccurrenceUnread: latest?.unread ?? false,\n hasMore: merged.hasMore,\n ...(merged.nextCursor ? { nextCursor: merged.nextCursor } : {}),\n });\n },\n }),\n registerApiRoute('/web/factory/projects/:id/attention/read-all', {\n method: 'POST',\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const cursorRaw = context.req.query('before');\n const before = parseAttentionCursor(cursorRaw);\n if (cursorRaw && !before) return context.json({ error: 'invalid_cursor' }, 400);\n await workItems.ensureReady();\n await comments.ensureReady();\n\n const now = new Date();\n const active = providers.filter(provider => !before || before.has(provider.kind));\n const nextCursors: AttentionCursorMap = new Map();\n let hasMore = false;\n for (const provider of active) {\n const result = await provider.markAllRead(resolved, { before: before?.get(provider.kind), now });\n if (result.hasMore) {\n hasMore = true;\n if (result.continuation) nextCursors.set(provider.kind, result.continuation);\n }\n }\n return context.json({\n ok: true,\n hasMore,\n ...(hasMore && nextCursors.size > 0 ? { nextCursor: encodeAttentionCursor(nextCursors) } : {}),\n });\n },\n }),\n receiptRoute(dependencies, 'read', 'read'),\n receiptRoute(dependencies, 'archive', 'archive'),\n receiptRoute(dependencies, 'restore', 'restore'),\n ];\n}\n"],"mappings":";;;;;AAiCA,MAAM,UAAU;AAChB,MAAM,4BAA4B;AAClC,MAAM,oBAAoB;AAC1B,MAAM,gBAAgB;AAUtB,SAAS,mBAAmB,KAA2D;CACrF,IAAI,CAAC,OAAO,QAAQ,QAAQ,OAAO;CACnC,IAAI,QAAQ,YAAY,QAAQ,YAAY,OAAO;AAErD;AAEA,SAAS,oBAAoB,KAAiC;CAC5D,MAAM,SAAS,MAAM,OAAO,SAAS,KAAK,EAAE,IAAI;CAChD,IAAI,CAAC,OAAO,SAAS,MAAM,GAAG,OAAO;CACrC,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,eAAe,MAAM,CAAC;AACpD;AAEA,SAAS,gBAAgB,OAA8C;CACrE,OAAO,UAAU,uBAAuB,UAAU,aAAa,UAAU,cAAc,UAAU;AACnG;;AAGA,MAAM,8BAAiD,IAAI,IAAI;CAAC;CAAqB;CAAW;AAAoB,CAAC;AAIrH,SAAS,mBAAmB,KAAoD;CAC9E,IAAI,QAAQ,KAAA,GAAW,OAAO;CAC9B,OAAO,QAAQ,WAAW,QAAQ,aAAa,MAAM,KAAA;AACvD;AAEA,SAAS,WAAW,MAAqB,MAAqC;CAC5E,IAAI,SAAS,OAAO,OAAO;CAC3B,OAAO,SAAS,UAAU,YAAY,IAAI,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI;AACzE;AAEA,SAAS,sBAAsB,SAAqC;CAClE,MAAM,OAAgD,CAAC;CACvD,KAAK,MAAM,CAAC,MAAM,aAAa,SAC7B,KAAK,QAAQ,WAAW,CAAC,SAAS,WAAW,YAAY,GAAG,SAAS,EAAE,IAAI;CAE7E,OAAO,OAAO,KAAK,KAAK,UAAU,IAAI,GAAG,MAAM,CAAC,CAAC,SAAS,WAAW;AACvE;AAEA,SAAS,oBAAoB,OAAqD;CAChF,IAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,KAAK,OAAO,MAAM,OAAO,YAAY,OAAO,MAAM,OAAO,UACrG;CAEF,MAAM,aAAa,IAAI,KAAK,MAAM,EAAE;CACpC,IAAI,OAAO,MAAM,WAAW,QAAQ,CAAC,KAAK,CAAC,QAAQ,KAAK,MAAM,EAAE,GAAG,OAAO,KAAA;CAC1E,OAAO;EAAE;EAAY,IAAI,MAAM;CAAG;AACpC;AAEA,SAAS,qBAAqB,KAAyD;CACrF,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,IAAI;EACF,MAAM,UAAmB,KAAK,MAAM,OAAO,KAAK,KAAK,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC;EAIlF,IAAI,MAAM,QAAQ,OAAO,GAAG;GAC1B,MAAM,SAAS,oBAAoB,OAAO;GAC1C,OAAO,yBAAS,IAAI,IAAI,CAAC,CAAC,qBAAqB,MAAM,CAAC,CAAC,IAAI,KAAA;EAC7D;EACA,IAAI,CAAC,WAAW,OAAO,YAAY,UAAU,OAAO,KAAA;EACpD,MAAM,0BAA8B,IAAI,IAAI;EAC5C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAAG;GACnD,IAAI,CAAC,gBAAgB,IAAI,GAAG,OAAO,KAAA;GACnC,IAAI,UAAU,MAAM;IAClB,QAAQ,IAAI,MAAM,KAAA,CAAS;IAC3B;GACF;GACA,MAAM,WAAW,oBAAoB,KAAK;GAC1C,IAAI,CAAC,UAAU,OAAO,KAAA;GACtB,QAAQ,IAAI,MAAM,QAAQ;EAC5B;EACA,OAAO,QAAQ,OAAO,IAAI,UAAU,KAAA;CACtC,QAAQ;EACN;CACF;AACF;AAEA,SAAS,gBAAgB,KAA6C;CACpE,IAAI,CAAC,OAAO,CAAC,iBAAiB,KAAK,GAAG,GAAG,OAAO,KAAA;CAChD,MAAM,aAAa,OAAO,GAAG;CAC7B,OAAO,OAAO,cAAc,UAAU,IAAI,aAAa,KAAA;AACzD;;;;;;AAaA,SAAS,oBACP,OAKA,OACqB;CACrB,MAAM,WAAW,IAAI,IAAI,MAAM,KAAI,SAAQ,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC;CAC1D,MAAM,QAAwC,CAAC;CAC/C,OAAO,MAAM,SAAS,OAAO;EAC3B,IAAI;EACJ,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,OAAO,KAAK,OAAO,QAAQ,SAAS,IAAI,KAAK,IAAI,KAAK;GAC5D,IAAI,CAAC,MAAM;GACX,MAAM,KAAK,KAAK,WAAW,QAAQ;GACnC,IAAI,CAAC,QAAQ,KAAK,KAAK,IAAI,OAAO;IAAE,MAAM,KAAK;IAAM;GAAG;EAC1D;EACA,IAAI,CAAC,MAAM;EACX,MAAM,QAAQ,SAAS,IAAI,KAAK,IAAI,KAAK;EACzC,MAAM,QAAQ,MAAM,MAAK,SAAQ,KAAK,SAAS,KAAK,IAAI,CAAC,EAAE,OAAO,QAAQ;EAC1E,IAAI,CAAC,OAAO;EACZ,MAAM,KAAK,MAAM,IAAI;EACrB,SAAS,IAAI,KAAK,MAAM,QAAQ,CAAC;CACnC;CAEA,MAAM,8BAAkC,IAAI,IAAI;CAChD,IAAI,UAAU;CACd,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,SAAS,IAAI,KAAK,IAAI,KAAK;EACxC,MAAM,UAAU,KAAK,OAAO;EAC5B,IAAI,OAAO,QAAQ,QAAQ;GACzB,UAAU;GACV,MAAM,eAAe,OAAO,IAAI,QAAQ,OAAO,KAAK,KAAA;GACpD,YAAY,IAAI,KAAK,MAAM,eAAe,aAAa,eAAe,KAAK,QAAQ;GACnF;EACF;EACA,IAAI,KAAK,OAAO,SAAS;GACvB,UAAU;GACV,YAAY,IAAI,KAAK,MAAM,KAAK,OAAO,gBAAgB,QAAQ,GAAG,EAAE,CAAC,EAAE,gBAAgB,KAAK,QAAQ;EACtG;CACF;CACA,OAAO;EACL;EACA;EACA,GAAI,WAAW,YAAY,OAAO,IAAI,EAAE,YAAY,sBAAsB,WAAW,EAAE,IAAI,CAAC;CAC9F;AACF;AAEA,SAAS,aAAa,SAAgE;CACpF,IAAI,SAAiC;CACrC,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,CAAC,QAAQ;EACb,IAAI,CAAC,UAAU,OAAO,GAAG,QAAQ,IAAI,OAAO,GAAG,QAAQ,GAAG,SAAS;CACrE;CACA,OAAO;AACT;AAEA,SAAS,aACP,cACA,MACA,QACU;CACV,OAAO,iBAAiB,mEAAmE,QAAQ;EACjG,QAAQ;EACR,cAAc;EACd,SAAS,OAAM,YAAW;GACxB,MAAM,WAAW,MAAM,aAAa,eAAe,OAAO;GAC1D,IAAI,cAAc,UAAU,OAAO,SAAS;GAC5C,MAAM,OAAO,QAAQ,IAAI,MAAM,MAAM;GACrC,MAAM,WAAW,QAAQ,IAAI,MAAM,UAAU;GAC7C,MAAM,aAAa,gBAAgB,QAAQ,IAAI,MAAM,YAAY,CAAC;GAClE,MAAM,gBACJ,SAAS,uBAAuB,0BAA0B,KAAK,QAAQ,IAAI,QAAQ,KAAK,QAAQ;GAClG,IAAI,CAAC,QAAQ,CAAC,gBAAgB,IAAI,KAAK,CAAC,YAAY,CAAC,iBAAiB,eAAe,KAAA,GACnF,OAAO,QAAQ,KAAK,EAAE,OAAO,yBAAyB,GAAG,GAAG;GAE9D,MAAM,aAAa,UAAU,YAAY;GACzC,MAAM,UAAU,MAAM,aAAa,UAAU,oBAAoB;IAC/D,OAAO,SAAS;IAChB,kBAAkB,SAAS;IAC3B,QAAQ,SAAS;IACjB,UAAU;KAAE;KAAM;KAAU;IAAW;IACvC;IACA,qBAAK,IAAI,KAAK;GAChB,CAAC;GACD,IAAI,CAAC,SAAS,OAAO,QAAQ,KAAK,EAAE,OAAO,6BAA6B,GAAG,GAAG;GAC9E,OAAO,QAAQ,KAAK,EAClB,SAAS;IACP,KAAK,oBAAoB,SAAS,kBAAkB,OAAO;IAC3D,OAAO,QAAQ;IACf,QAAQ,QAAQ,OAAO,YAAY;IACnC,YAAY,QAAQ,YAAY,YAAY,KAAK;GACnD,EACF,CAAC;EACH;CACF,CAAC;AACH;AAEA,SAAgB,qBAAqB,cAAsD;CACzF,MAAM,EAAE,WAAW,aAAa;CAChC,MAAM,YAAiC;EACrC,IAAI,kCAAkC,EAAE,UAAU,CAAC;EACnD,IAAI,mCAAmC,EAAE,UAAU,CAAC;EACpD,IAAI,yBAAyB;GAAE;GAAW;EAAS,CAAC;EACpD,IAAI,0BAA0B;GAAE;GAAW;EAAS,CAAC;CACvD;CAEA,OAAO;EACL,iBAAiB,uCAAuC;GACtD,QAAQ;GACR,cAAc;GACd,SAAS,OAAM,YAAW;IACxB,MAAM,WAAW,MAAM,aAAa,eAAe,OAAO;IAC1D,IAAI,cAAc,UAAU,OAAO,SAAS;IAC5C,MAAM,OAAO,mBAAmB,QAAQ,IAAI,MAAM,MAAM,CAAC;IACzD,IAAI,SAAS,KAAA,GAAW,OAAO,QAAQ,KAAK,EAAE,OAAO,yBAAyB,GAAG,GAAG;IAIpF,MAAM,OAAO,mBAAmB,QAAQ,IAAI,MAAM,MAAM,CAAC;IACzD,IAAI,SAAS,KAAA,GAAW,OAAO,QAAQ,KAAK,EAAE,OAAO,yBAAyB,GAAG,GAAG;IACpF,MAAM,YAAY,QAAQ,IAAI,MAAM,QAAQ;IAC5C,MAAM,SAAS,qBAAqB,SAAS;IAC7C,IAAI,aAAa,CAAC,QAAQ,OAAO,QAAQ,KAAK,EAAE,OAAO,iBAAiB,GAAG,GAAG;IAC9E,MAAM,UAAU,YAAY;IAC5B,MAAM,SAAS,YAAY;IAE3B,MAAM,SAAS,QAAQ,IAAI,MAAM,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,GAAG;IAC7E,MAAM,QAAQ,oBAAoB,QAAQ,IAAI,MAAM,OAAO,CAAC;IAC5D,MAAM,SAAS,UAAU,QACvB,aAAY,WAAW,MAAM,SAAS,IAAI,MAAM,CAAC,UAAU,OAAO,IAAI,SAAS,IAAI,EACrF;IAEA,MAAM,CAAC,eAAe,WAAW,SAAS,MAAM,QAAQ,IAAI;KAC1D,UAAU,iCAAiC;MACzC,OAAO,SAAS;MAChB,kBAAkB,SAAS;MAC3B,UAAU,CAAC,UAAU;KACvB,CAAC;KACD,QAAQ,IACN,UAAU,IAAI,OAAM,cAAa;MAC/B,MAAM,SAAS;MACf,QAAQ,MAAM,SAAS,OAAO,QAAQ;MACtC,QAAQ,MAAM,SAAS,OAAO,QAAQ;KACxC,EAAE,CACJ;KACA,QAAQ,IACN,OAAO,IAAI,OAAM,cAAa;MAC5B,MAAM,SAAS;MACf,UAAU,QAAQ,IAAI,SAAS,IAAI;MACnC,QAAQ,MAAM,SAAS,KAAK,UAAU;OACpC;OACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;OAC3B,QAAQ,QAAQ,IAAI,SAAS,IAAI;OACjC;MACF,CAAC;KACH,EAAE,CACJ;IACF,CAAC;IAKD,MAAM,QAAQ,UAAU,QAAO,YAAW,YAAY,IAAI,QAAQ,IAAI,CAAC;IACvE,MAAM,WAAW,UAAU,QAAO,YAAW,CAAC,YAAY,IAAI,QAAQ,IAAI,CAAC;IAC3E,MAAM,OAAO,MAAwB,UACnC,KAAK,QAAQ,OAAO,QAAQ,QAAQ,IAAI,OAAO,QAAQ,CAAC;IAC1D,MAAM,YAAY,IAAI,OAAO,MAAM,IAAI;IACvC,MAAM,cAAc,IAAI,OAAO,QAAQ;IAGvC,MAAM,UAAU,MAAM,KAAI,YAAW,QAAQ,MAAM;IACnD,MAAM,gBAAgB,QAAQ,QAAO,WAAU,QAAQ,UAAU,KAAK;IACtE,MAAM,SAAS,cAAc,SAAS,IAAI,aAAa,aAAa,IAAI,aAAa,OAAO;IAC5F,MAAM,SAAS,oBAAoB,OAAO,KAAK;IAE/C,OAAO,QAAQ,KAAK;KAClB,OAAO,OAAO;KACd;KACA;KACA,YAAY,cAAc;KAC1B;KACA,qBAAqB,IAAI,UAAU,QAAQ;KAC3C,qBAAqB,QAAQ,OAAO;KACpC,oBAAoB,QAAQ,GAAG,YAAY,KAAK;KAChD,wBAAwB,QAAQ,UAAU;KAC1C,SAAS,OAAO;KAChB,GAAI,OAAO,aAAa,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;IAC/D,CAAC;GACH;EACF,CAAC;EACD,iBAAiB,gDAAgD;GAC/D,QAAQ;GACR,cAAc;GACd,SAAS,OAAM,YAAW;IACxB,MAAM,WAAW,MAAM,aAAa,eAAe,OAAO;IAC1D,IAAI,cAAc,UAAU,OAAO,SAAS;IAC5C,MAAM,YAAY,QAAQ,IAAI,MAAM,QAAQ;IAC5C,MAAM,SAAS,qBAAqB,SAAS;IAC7C,IAAI,aAAa,CAAC,QAAQ,OAAO,QAAQ,KAAK,EAAE,OAAO,iBAAiB,GAAG,GAAG;IAC9E,MAAM,UAAU,YAAY;IAC5B,MAAM,SAAS,YAAY;IAE3B,MAAM,sBAAM,IAAI,KAAK;IACrB,MAAM,SAAS,UAAU,QAAO,aAAY,CAAC,UAAU,OAAO,IAAI,SAAS,IAAI,CAAC;IAChF,MAAM,8BAAkC,IAAI,IAAI;IAChD,IAAI,UAAU;IACd,KAAK,MAAM,YAAY,QAAQ;KAC7B,MAAM,SAAS,MAAM,SAAS,YAAY,UAAU;MAAE,QAAQ,QAAQ,IAAI,SAAS,IAAI;MAAG;KAAI,CAAC;KAC/F,IAAI,OAAO,SAAS;MAClB,UAAU;MACV,IAAI,OAAO,cAAc,YAAY,IAAI,SAAS,MAAM,OAAO,YAAY;KAC7E;IACF;IACA,OAAO,QAAQ,KAAK;KAClB,IAAI;KACJ;KACA,GAAI,WAAW,YAAY,OAAO,IAAI,EAAE,YAAY,sBAAsB,WAAW,EAAE,IAAI,CAAC;IAC9F,CAAC;GACH;EACF,CAAC;EACD,aAAa,cAAc,QAAQ,MAAM;EACzC,aAAa,cAAc,WAAW,SAAS;EAC/C,aAAa,cAAc,WAAW,SAAS;CACjD;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ApiRoute } from '@mastra/core/server';
|
|
2
2
|
import type { SessionRetirementCoordinator } from '../sandbox/session-retirement.js';
|
|
3
3
|
import type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';
|
|
4
|
-
import type { ProjectRepository, SourceControlStorage } from '../storage/domains/source-control/base.js';
|
|
4
|
+
import type { ProjectRepository, SourceControlRepository, SourceControlStorage } from '../storage/domains/source-control/base.js';
|
|
5
5
|
import type { WorkItemsStorage } from '../storage/domains/work-items/base.js';
|
|
6
6
|
import type { RouteDependencies } from './route.js';
|
|
7
7
|
import { Route } from './route.js';
|
|
@@ -12,6 +12,14 @@ export interface ProjectRoutesDeps extends RouteDependencies {
|
|
|
12
12
|
sourceControl: SourceControlStorage;
|
|
13
13
|
/** Integration ids allowed as source-control connection targets. */
|
|
14
14
|
versionControlIntegrationIds?: string[];
|
|
15
|
+
/** Validate and persist a repository selected from a provider-backed listing. */
|
|
16
|
+
resolveRepository?: (input: {
|
|
17
|
+
integrationId: string;
|
|
18
|
+
orgId: string;
|
|
19
|
+
installationId: string;
|
|
20
|
+
externalId: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
}) => Promise<SourceControlRepository | null>;
|
|
15
23
|
/**
|
|
16
24
|
* Fire-and-forget hook invoked after a repository is linked to a project —
|
|
17
25
|
* kicks the initial base-checkpoint build. Must never throw.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/routes/projects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AACrF,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EAGrB,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/routes/projects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AACrF,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EACV,iBAAiB,EACjB,uBAAuB,EACvB,oBAAoB,EAGrB,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAmLnC,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,wDAAwD;IACxD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,2EAA2E;IAC3E,aAAa,EAAE,oBAAoB,CAAC;IACpC,oEAAoE;IACpE,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;IACxC,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE;QAC1B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;QACd,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;IAC9C;;;OAGG;IACH,yBAAyB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,iBAAiB,CAAA;KAAE,KAAK,IAAI,CAAC;IACpG,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;IACjD,kFAAkF;IAClF,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;CAC9D;AAED,qBAAa,aAAc,SAAQ,KAAK,CAAC,iBAAiB,CAAC;;gBAG7C,IAAI,EAAE,iBAAiB;IA+EnC,MAAM,IAAI,QAAQ,EAAE;CAuQrB"}
|
package/dist/routes/projects.js
CHANGED
|
@@ -93,7 +93,20 @@ function parseOptionalString(value, { maxLength, nullable = false }) {
|
|
|
93
93
|
function parseRepositoryLinkInput(value) {
|
|
94
94
|
if (!value || typeof value !== "object") return null;
|
|
95
95
|
const input = value;
|
|
96
|
-
|
|
96
|
+
const repositoryId = typeof input.repositoryId === "string" && UUID_RE.test(input.repositoryId) ? input.repositoryId : void 0;
|
|
97
|
+
const repositoryInput = input.repository;
|
|
98
|
+
let repository;
|
|
99
|
+
if (repositoryInput && typeof repositoryInput === "object") {
|
|
100
|
+
const candidate = repositoryInput;
|
|
101
|
+
const externalId = parseOptionalString(candidate.externalId, { maxLength: MAX_NAME_LENGTH });
|
|
102
|
+
const slug = parseOptionalString(candidate.slug, { maxLength: MAX_NAME_LENGTH });
|
|
103
|
+
if (typeof externalId !== "string" || typeof slug !== "string") return null;
|
|
104
|
+
repository = {
|
|
105
|
+
externalId,
|
|
106
|
+
slug
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!repositoryId && !repository) return null;
|
|
97
110
|
const branch = parseOptionalString(input.branch, {
|
|
98
111
|
maxLength: MAX_BRANCH_LENGTH,
|
|
99
112
|
nullable: true
|
|
@@ -110,7 +123,8 @@ function parseRepositoryLinkInput(value) {
|
|
|
110
123
|
});
|
|
111
124
|
if (branch === false || typeof sandboxProvider !== "string" || typeof sandboxWorkdir !== "string" || setupCommand === false || teardownCommand === false) return null;
|
|
112
125
|
return {
|
|
113
|
-
repositoryId:
|
|
126
|
+
...repositoryId ? { repositoryId } : {},
|
|
127
|
+
...repository ? { repository } : {},
|
|
114
128
|
branch: branch ?? null,
|
|
115
129
|
sandboxProvider,
|
|
116
130
|
sandboxWorkdir,
|
|
@@ -428,16 +442,23 @@ var ProjectRoutes = class extends Route {
|
|
|
428
442
|
if (!found) return context.json({ error: "Source-control connection not found" }, 404);
|
|
429
443
|
const input = parseRepositoryLinkInput(await readJson(context));
|
|
430
444
|
if (!input) return context.json({ error: "invalid_project_repository" }, 400);
|
|
431
|
-
const repository = await found.handle.repositories.get({
|
|
445
|
+
const repository = input.repositoryId ? await found.handle.repositories.get({
|
|
432
446
|
orgId: tenant.orgId,
|
|
433
447
|
id: input.repositoryId
|
|
434
|
-
})
|
|
448
|
+
}) : input.repository && this.deps.resolveRepository ? await this.deps.resolveRepository({
|
|
449
|
+
integrationId: found.connection.integrationId,
|
|
450
|
+
orgId: tenant.orgId,
|
|
451
|
+
installationId: found.connection.installationId,
|
|
452
|
+
...input.repository
|
|
453
|
+
}) : null;
|
|
435
454
|
if (!repository || repository.installationId !== found.connection.installationId) return context.json({ error: "Source-control repository not found" }, 404);
|
|
455
|
+
const { repositoryId: _repositoryId, repository: _repository, ...linkInput } = input;
|
|
436
456
|
const projectRepository = await found.handle.projectRepositories.link({
|
|
437
457
|
orgId: tenant.orgId,
|
|
438
458
|
connectionId,
|
|
439
459
|
createdByUserId: tenant.userId,
|
|
440
|
-
|
|
460
|
+
repositoryId: repository.id,
|
|
461
|
+
...linkInput
|
|
441
462
|
});
|
|
442
463
|
try {
|
|
443
464
|
this.deps.onProjectRepositoryLinked?.({
|