@mastra/factory 0.13.0-alpha.14 → 0.13.0-alpha.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/factory.d.ts.map +1 -1
  2. package/dist/factory.js +14 -0
  3. package/dist/factory.js.map +1 -1
  4. package/dist/routes/attention-activity.d.ts +2 -3
  5. package/dist/routes/attention-activity.d.ts.map +1 -1
  6. package/dist/routes/attention-activity.js +12 -3
  7. package/dist/routes/attention-activity.js.map +1 -1
  8. package/dist/routes/attention-parked.d.ts +22 -0
  9. package/dist/routes/attention-parked.d.ts.map +1 -0
  10. package/dist/routes/attention-parked.js +136 -0
  11. package/dist/routes/attention-parked.js.map +1 -0
  12. package/dist/routes/attention-providers.js +8 -8
  13. package/dist/routes/attention-providers.js.map +1 -1
  14. package/dist/routes/attention.d.ts +2 -0
  15. package/dist/routes/attention.d.ts.map +1 -1
  16. package/dist/routes/attention.js +19 -39
  17. package/dist/routes/attention.js.map +1 -1
  18. package/dist/routes/contracts.d.ts +38 -19
  19. package/dist/routes/contracts.d.ts.map +1 -1
  20. package/dist/routes/contracts.js +14 -14
  21. package/dist/routes/contracts.js.map +1 -1
  22. package/dist/routes/surface.d.ts +3 -0
  23. package/dist/routes/surface.d.ts.map +1 -1
  24. package/dist/routes/surface.js +1 -2
  25. package/dist/routes/surface.js.map +1 -1
  26. package/dist/routes/work-items.d.ts +2 -2
  27. package/dist/routes/work-items.d.ts.map +1 -1
  28. package/dist/routes/work-items.js +8 -6
  29. package/dist/routes/work-items.js.map +1 -1
  30. package/dist/rules/dispatch-errors.d.ts +2 -2
  31. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  32. package/dist/rules/dispatch-errors.js +10 -6
  33. package/dist/rules/dispatch-errors.js.map +1 -1
  34. package/dist/rules/dispatcher.d.ts +6 -2
  35. package/dist/rules/dispatcher.d.ts.map +1 -1
  36. package/dist/rules/dispatcher.js +17 -12
  37. package/dist/rules/dispatcher.js.map +1 -1
  38. package/dist/session/live-sessions.d.ts +33 -3
  39. package/dist/session/live-sessions.d.ts.map +1 -1
  40. package/dist/session/live-sessions.js +67 -5
  41. package/dist/session/live-sessions.js.map +1 -1
  42. package/dist/storage/domains/work-items/base.d.ts +8 -4
  43. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  44. package/dist/storage/domains/work-items/base.js +20 -9
  45. package/dist/storage/domains/work-items/base.js.map +1 -1
  46. package/dist/supervisor/health.d.ts +2 -2
  47. package/dist/supervisor/health.d.ts.map +1 -1
  48. package/dist/supervisor/health.js +13 -12
  49. package/dist/supervisor/health.js.map +1 -1
  50. package/package.json +6 -6
@@ -0,0 +1,136 @@
1
+ import { factoryAgentWaitingAttentionIdentity, factoryAttentionKey } from "../storage/domains/work-items/base.js";
2
+ import { matchesView, receiptScope } from "./attention-providers.js";
3
+ //#region src/routes/attention-parked.ts
4
+ function parkedRunLabel(toolName) {
5
+ return toolName === "submit_plan" ? "Plan waiting for review" : "Agent is waiting for an answer";
6
+ }
7
+ function olderThan(entry, before) {
8
+ if (!before) return true;
9
+ const gap = entry.occurredAt.getTime() - before.occurredAt.getTime();
10
+ return gap < 0 || gap === 0 && entry.sessionId.localeCompare(before.id) > 0;
11
+ }
12
+ function toItem(scope, { entry, receipt }) {
13
+ return {
14
+ key: factoryAttentionKey(scope.factoryProjectId, entry.identity),
15
+ kind: "agent-waiting",
16
+ sessionId: entry.sessionId,
17
+ threadId: entry.threadId,
18
+ role: entry.role,
19
+ toolName: entry.toolName,
20
+ occurrence: entry.identity.occurrence,
21
+ workItemId: entry.item.id,
22
+ title: entry.item.title,
23
+ detail: parkedRunLabel(entry.toolName),
24
+ occurredAt: entry.occurredAt.toISOString(),
25
+ read: receipt !== void 0,
26
+ archived: receipt?.state === "archived",
27
+ target: {
28
+ kind: "thread",
29
+ sessionId: entry.sessionId,
30
+ threadId: entry.threadId
31
+ }
32
+ };
33
+ }
34
+ var ParkedRunAttentionProvider = class {
35
+ kind = "agent-waiting";
36
+ #workItems;
37
+ #liveSessions;
38
+ constructor({ workItems, liveSessions }) {
39
+ this.#workItems = workItems;
40
+ this.#liveSessions = liveSessions;
41
+ }
42
+ /** Newest park first. A session bound to several cards is listed once, under the first card. */
43
+ async #parked(scope) {
44
+ const runBySession = new Map(this.#liveSessions.parkedIn(scope.factoryProjectId).map(({ sessionId, run }) => [sessionId, run]));
45
+ if (runBySession.size === 0) return [];
46
+ const items = await this.#workItems.list({
47
+ orgId: scope.orgId,
48
+ factoryProjectId: scope.factoryProjectId
49
+ });
50
+ const bySession = /* @__PURE__ */ new Map();
51
+ for (const item of items) for (const [role, ref] of Object.entries(item.sessions)) {
52
+ const run = runBySession.get(ref.sessionId);
53
+ if (!run || bySession.has(ref.sessionId)) continue;
54
+ bySession.set(ref.sessionId, {
55
+ item,
56
+ role,
57
+ sessionId: ref.sessionId,
58
+ threadId: ref.threadId,
59
+ toolName: run.toolName,
60
+ occurredAt: new Date(run.suspendedAt),
61
+ identity: factoryAgentWaitingAttentionIdentity(ref.sessionId, run.suspendedAt)
62
+ });
63
+ }
64
+ return [...bySession.values()].sort((a, b) => b.occurredAt.getTime() - a.occurredAt.getTime() || a.sessionId.localeCompare(b.sessionId));
65
+ }
66
+ async #withReceipts(scope, parked) {
67
+ const receipts = await this.#workItems.listAttentionReceipts({
68
+ ...receiptScope(scope),
69
+ identities: parked.map((entry) => entry.identity)
70
+ });
71
+ const byKey = new Map(receipts.map((receipt) => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]));
72
+ return parked.map((entry) => ({
73
+ entry,
74
+ receipt: byKey.get(factoryAttentionKey(scope.factoryProjectId, entry.identity))
75
+ }));
76
+ }
77
+ async counts(scope) {
78
+ let open = 0;
79
+ let unread = 0;
80
+ for (const { receipt } of await this.#withReceipts(scope, await this.#parked(scope))) {
81
+ if (receipt?.state !== "archived") open += 1;
82
+ if (receipt === void 0) unread += 1;
83
+ }
84
+ return {
85
+ open,
86
+ unread
87
+ };
88
+ }
89
+ async latest(scope) {
90
+ const [newest] = await this.#withReceipts(scope, (await this.#parked(scope)).slice(0, 1));
91
+ if (!newest) return null;
92
+ return {
93
+ key: factoryAttentionKey(scope.factoryProjectId, newest.entry.identity),
94
+ at: newest.entry.occurredAt,
95
+ unread: newest.receipt === void 0
96
+ };
97
+ }
98
+ async page(scope, { view, search, before, limit }) {
99
+ const parked = (await this.#parked(scope)).filter((entry) => olderThan(entry, before));
100
+ const entries = [];
101
+ for (const receipted of await this.#withReceipts(scope, parked)) {
102
+ const { entry, receipt } = receipted;
103
+ if (!matchesView(view, receipt)) continue;
104
+ if (search && !`${entry.item.title} ${parkedRunLabel(entry.toolName)}`.toLowerCase().includes(search)) continue;
105
+ if (entries.length === limit) return {
106
+ entries,
107
+ hasMore: true
108
+ };
109
+ entries.push({
110
+ occurredAt: entry.occurredAt,
111
+ resumeCursor: {
112
+ occurredAt: entry.occurredAt,
113
+ id: entry.sessionId
114
+ },
115
+ item: toItem(scope, receipted)
116
+ });
117
+ }
118
+ return {
119
+ entries,
120
+ hasMore: false
121
+ };
122
+ }
123
+ async markAllRead(scope, { before, now }) {
124
+ const parked = (await this.#parked(scope)).filter((entry) => olderThan(entry, before));
125
+ if (parked.length > 0) await this.#workItems.markAttentionReceiptsRead({
126
+ ...receiptScope(scope),
127
+ identities: parked.map((entry) => entry.identity),
128
+ now
129
+ });
130
+ return { hasMore: false };
131
+ }
132
+ };
133
+ //#endregion
134
+ export { ParkedRunAttentionProvider };
135
+
136
+ //# sourceMappingURL=attention-parked.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attention-parked.js","names":["#workItems","#liveSessions","#parked","#withReceipts"],"sources":["../../src/routes/attention-parked.ts"],"sourcesContent":["/** Sessions parked on a tool, read live from the registry: the item exists exactly as long as an answer is owed. */\n\nimport type { LiveSessions } from '../session/live-sessions.js';\nimport type {\n FactoryAttentionIdentity,\n FactoryAttentionReceiptRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { factoryAgentWaitingAttentionIdentity, factoryAttentionKey } from '../storage/domains/work-items/base.js';\nimport type {\n AttentionCounts,\n AttentionEntry,\n AttentionLatest,\n AttentionPageArgs,\n AttentionPageResult,\n AttentionProvider,\n AttentionScope,\n AttentionStreamPosition,\n} from './attention-providers.js';\nimport { matchesView, receiptScope } from './attention-providers.js';\n\ninterface ParkedSession {\n item: WorkItemRow;\n role: string;\n sessionId: string;\n threadId: string;\n toolName: string;\n occurredAt: Date;\n identity: FactoryAttentionIdentity;\n}\n\ninterface ReceiptedParkedSession {\n entry: ParkedSession;\n receipt: FactoryAttentionReceiptRecord | undefined;\n}\n\nfunction parkedRunLabel(toolName: string): string {\n return toolName === 'submit_plan' ? 'Plan waiting for review' : 'Agent is waiting for an answer';\n}\n\nfunction olderThan(entry: ParkedSession, before: AttentionStreamPosition | undefined): boolean {\n if (!before) return true;\n const gap = entry.occurredAt.getTime() - before.occurredAt.getTime();\n return gap < 0 || (gap === 0 && entry.sessionId.localeCompare(before.id) > 0);\n}\n\nfunction toItem(scope: AttentionScope, { entry, receipt }: ReceiptedParkedSession): Record<string, unknown> {\n return {\n key: factoryAttentionKey(scope.factoryProjectId, entry.identity),\n kind: 'agent-waiting' as const,\n sessionId: entry.sessionId,\n threadId: entry.threadId,\n role: entry.role,\n toolName: entry.toolName,\n occurrence: entry.identity.occurrence,\n workItemId: entry.item.id,\n title: entry.item.title,\n detail: parkedRunLabel(entry.toolName),\n occurredAt: entry.occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: { kind: 'thread' as const, sessionId: entry.sessionId, threadId: entry.threadId },\n };\n}\n\nexport class ParkedRunAttentionProvider implements AttentionProvider {\n readonly kind = 'agent-waiting' as const;\n readonly #workItems: WorkItemsStorage;\n readonly #liveSessions: Pick<LiveSessions, 'parkedIn'>;\n\n constructor({\n workItems,\n liveSessions,\n }: {\n workItems: WorkItemsStorage;\n liveSessions: Pick<LiveSessions, 'parkedIn'>;\n }) {\n this.#workItems = workItems;\n this.#liveSessions = liveSessions;\n }\n\n /** Newest park first. A session bound to several cards is listed once, under the first card. */\n async #parked(scope: AttentionScope): Promise<ParkedSession[]> {\n const runBySession = new Map(\n this.#liveSessions.parkedIn(scope.factoryProjectId).map(({ sessionId, run }) => [sessionId, run]),\n );\n if (runBySession.size === 0) return [];\n const items = await this.#workItems.list({ orgId: scope.orgId, factoryProjectId: scope.factoryProjectId });\n const bySession = new Map<string, ParkedSession>();\n for (const item of items) {\n for (const [role, ref] of Object.entries(item.sessions)) {\n const run = runBySession.get(ref.sessionId);\n if (!run || bySession.has(ref.sessionId)) continue;\n bySession.set(ref.sessionId, {\n item,\n role,\n sessionId: ref.sessionId,\n threadId: ref.threadId,\n toolName: run.toolName,\n occurredAt: new Date(run.suspendedAt),\n identity: factoryAgentWaitingAttentionIdentity(ref.sessionId, run.suspendedAt),\n });\n }\n }\n return [...bySession.values()].sort(\n (a, b) => b.occurredAt.getTime() - a.occurredAt.getTime() || a.sessionId.localeCompare(b.sessionId),\n );\n }\n\n async #withReceipts(scope: AttentionScope, parked: ParkedSession[]): Promise<ReceiptedParkedSession[]> {\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: parked.map(entry => entry.identity),\n });\n const byKey = new Map(receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]));\n return parked.map(entry => ({\n entry,\n receipt: byKey.get(factoryAttentionKey(scope.factoryProjectId, entry.identity)),\n }));\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n let open = 0;\n let unread = 0;\n for (const { receipt } of await this.#withReceipts(scope, await this.#parked(scope))) {\n if (receipt?.state !== 'archived') open += 1;\n if (receipt === undefined) unread += 1;\n }\n return { open, unread };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const [newest] = await this.#withReceipts(scope, (await this.#parked(scope)).slice(0, 1));\n if (!newest) return null;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, newest.entry.identity),\n at: newest.entry.occurredAt,\n unread: newest.receipt === undefined,\n };\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n const parked = (await this.#parked(scope)).filter(entry => olderThan(entry, before));\n const entries: AttentionEntry[] = [];\n for (const receipted of await this.#withReceipts(scope, parked)) {\n const { entry, receipt } = receipted;\n if (!matchesView(view, receipt)) continue;\n if (search && !`${entry.item.title} ${parkedRunLabel(entry.toolName)}`.toLowerCase().includes(search)) continue;\n if (entries.length === limit) return { entries, hasMore: true };\n entries.push({\n occurredAt: entry.occurredAt,\n resumeCursor: { occurredAt: entry.occurredAt, id: entry.sessionId },\n item: toItem(scope, receipted),\n });\n }\n return { entries, hasMore: false };\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean }> {\n const parked = (await this.#parked(scope)).filter(entry => olderThan(entry, before));\n if (parked.length > 0) {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: parked.map(entry => entry.identity),\n now,\n });\n }\n return { hasMore: false };\n }\n}\n"],"mappings":"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"}
@@ -247,7 +247,7 @@ var SupervisorFindingAttentionProvider = class {
247
247
  ...before ? { before } : {},
248
248
  limit: 50
249
249
  }), (row) => ({
250
- occurredAt: row.updatedAt,
250
+ occurredAt: row.openedAt,
251
251
  id: row.id
252
252
  }));
253
253
  for await (const page of batches) {
@@ -282,7 +282,7 @@ var SupervisorFindingAttentionProvider = class {
282
282
  });
283
283
  return {
284
284
  key: factoryAttentionKey(scope.factoryProjectId, identity),
285
- at: newest.updatedAt,
285
+ at: newest.openedAt,
286
286
  unread: receipts.length === 0
287
287
  };
288
288
  }
@@ -292,7 +292,7 @@ var SupervisorFindingAttentionProvider = class {
292
292
  ...before ? { before } : {},
293
293
  limit: 50
294
294
  }), (row) => ({
295
- occurredAt: row.updatedAt,
295
+ occurredAt: row.openedAt,
296
296
  id: row.id
297
297
  }), args.before), args.limit, async (rows) => {
298
298
  const identities = rows.map((row) => factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence));
@@ -310,9 +310,9 @@ var SupervisorFindingAttentionProvider = class {
310
310
  const identity = factorySupervisorFindingAttentionIdentity(row.findingKey, row.occurrence);
311
311
  return [{
312
312
  key: factoryAttentionKey(scope.factoryProjectId, identity),
313
- occurredAt: row.updatedAt,
313
+ occurredAt: row.openedAt,
314
314
  resumeCursor: {
315
- occurredAt: row.updatedAt,
315
+ occurredAt: row.openedAt,
316
316
  id: row.id
317
317
  },
318
318
  receipt,
@@ -325,10 +325,10 @@ var SupervisorFindingAttentionProvider = class {
325
325
  evidence: finding.evidence,
326
326
  title: finding.title,
327
327
  detail: finding.evidence,
328
- ageMs: finding.ageMs,
328
+ beganAt: finding.beganAt ?? null,
329
329
  suggestedRepair: finding.suggestedRepair,
330
330
  workItemId: finding.workItemId,
331
- occurredAt: row.updatedAt.toISOString(),
331
+ occurredAt: row.openedAt.toISOString(),
332
332
  read: Boolean(receipt),
333
333
  archived: receipt?.state === "archived",
334
334
  target: finding.workItemId ? {
@@ -347,7 +347,7 @@ var SupervisorFindingAttentionProvider = class {
347
347
  ...before ? { before } : {},
348
348
  limit: 50
349
349
  }), (row) => ({
350
- occurredAt: row.updatedAt,
350
+ occurredAt: row.openedAt,
351
351
  id: row.id
352
352
  }), args.before), (rows) => this.#workItems.markAttentionReceiptsRead({
353
353
  ...scope,
@@ -1 +1 @@
1
- {"version":3,"file":"attention-providers.js","names":["#workItems","#spec","#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 FactoryDispatchStatus,\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\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\n/** One deferred-decision status seen as attention: failures and parked runs differ only by this. */\nexport interface DecisionAttentionSpec {\n kind: FactoryAttentionKind;\n status: FactoryDispatchStatus;\n identity(decision: FactoryDeferredDecisionRecord): FactoryAttentionIdentity;\n occurredAt(decision: FactoryDeferredDecisionRecord): Date;\n title(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string;\n detail(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string;\n extra?(decision: FactoryDeferredDecisionRecord): Record<string, unknown>;\n matches(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined, search: string): boolean;\n}\n\nexport class DecisionAttentionProvider implements AttentionProvider {\n readonly kind: FactoryAttentionKind;\n readonly #workItems: WorkItemsStorage;\n readonly #spec: DecisionAttentionSpec;\n\n constructor({ workItems }: { workItems: WorkItemsStorage }, spec: DecisionAttentionSpec) {\n this.#workItems = workItems;\n this.#spec = spec;\n this.kind = spec.kind;\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n const [total, receiptCount, archivedCount] = await Promise.all([\n this.#workItems.countDeferredDecisionsByStatuses({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n statuses: [this.#spec.status],\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, total - archivedCount),\n unread: Math.max(0, total - receiptCount),\n };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const page = await this.#workItems.listDecisionPageByStatus({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n status: this.#spec.status,\n limit: 1,\n });\n const newest = page.decisions[0];\n if (!newest) return null;\n const identity = this.#spec.identity(newest);\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: [identity],\n });\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n at: this.#spec.occurredAt(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.listDecisionPageByStatus({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n status: this.#spec.status,\n before: cursor,\n limit: SCAN_PAGE_SIZE,\n });\n return { rows: page.decisions, hasMore: page.hasMore };\n },\n decision => ({ occurredAt: this.#spec.occurredAt(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 = this.#spec.identity(decision);\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 && !this.#spec.matches(decision, item, search)) continue;\n const occurredAt = this.#spec.occurredAt(decision);\n entries.push({\n occurredAt,\n resumeCursor: { occurredAt, id: decision.id },\n item: {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: this.kind,\n decisionId: decision.id,\n occurrence: identity.occurrence,\n workItemId: decision.workItemId,\n title: this.#spec.title(decision, item),\n detail: this.#spec.detail(decision, item),\n decisionType: factoryDecisionType(decision),\n ...this.#spec.extra?.(decision),\n occurredAt: occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: attentionTarget(decision, item),\n },\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 => this.#spec.identity(decision)),\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 => this.#spec.identity(decision)),\n now,\n });\n });\n }\n}\n\nexport const failedDecisionAttentionSpec: DecisionAttentionSpec = {\n kind: 'automation-failed',\n status: 'failed',\n identity: decision => factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence),\n occurredAt: decision => decision.completedAt ?? decision.updatedAt,\n title: (decision, item) => item?.title ?? factoryDispatchFailureMetadata(decision.failureCode).label,\n detail: decision => decision.lastError?.slice(0, 512) ?? factoryDispatchFailureMetadata(decision.failureCode).label,\n extra: decision => ({\n failureCode: decision.failureCode,\n canRetry: factoryDispatchFailureMetadata(decision.failureCode).canRetry,\n }),\n matches: (decision, item, search) =>\n item?.title.toLowerCase().includes(search) === true ||\n decision.lastError?.toLowerCase().includes(search) === true ||\n factoryDecisionType(decision).toLowerCase().includes(search),\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 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":";;;;;;;;AAuFA,MAAa,iBAAiB;AAE9B,MAAM,yBAAyB;;;;;;AAa/B,gBAAuB,YACrB,YACA,YACA,QAC8B;CAC9B,IAAI,SAAS;CACb,KAAK,IAAI,UAAU,GAAG,WAAW,wBAAwB,WAAW,GAAG;EACrE,MAAM,EAAE,MAAM,YAAY,MAAM,WAAW,MAAM;EACjD,MAAM,OAAO,KAAK,GAAG,EAAE;EACvB,IAAI,CAAC,MAAM;EACX,MAAM,WAAW,WAAW,IAAI;EAChC,MAAM;GAAE;GAAM,GAAI,WAAW,YAAY,yBAAyB,EAAE,cAAc,SAAS,IAAI,CAAC;EAAG;EACnG,IAAI,CAAC,SAAS;EACd,SAAS;CACX;AACF;;AAGA,eAAsB,YACpB,SACA,OACA,WAC8B;CAC9B,MAAM,UAA4B,CAAC;CACnC,WAAW,MAAM,SAAS,SAAS;EACjC,KAAK,MAAM,SAAS,MAAM,UAAU,MAAM,IAAI,GAAG;GAC/C,IAAI,QAAQ,WAAW,OAAO,OAAO;IAAE;IAAS,SAAS;GAAK;GAC9D,QAAQ,KAAK,KAAK;EACpB;EACA,IAAI,MAAM,cAAc,OAAO;GAAE;GAAS,SAAS;GAAM,cAAc,MAAM;EAAa;CAC5F;CACA,OAAO;EAAE;EAAS,SAAS;CAAM;AACnC;;AAGA,eAAsB,aACpB,SACA,WACuE;CACvE,WAAW,MAAM,SAAS,SAAS;EACjC,MAAM,UAAU,MAAM,IAAI;EAC1B,IAAI,MAAM,cAAc,OAAO;GAAE,SAAS;GAAM,cAAc,MAAM;EAAa;CACnF;CACA,OAAO,EAAE,SAAS,MAAM;AAC1B;AAEA,SAAgB,oBAAoB,UAAiD;CACnF,OAAO,OAAO,SAAS,SAAS,SAAS,WAAW,SAAS,SAAS,KAAK,MAAM,GAAG,EAAE,IAAI;AAC5F;AAEA,SAAgB,YAAY,MAA4B,SAA6D;CACnH,IAAI,SAAS,YAAY,OAAO,SAAS,UAAU;CACnD,IAAI,SAAS,UAAU,OAAO,YAAY,KAAA;CAC1C,OAAO,SAAS,UAAU;AAC5B;AAEA,SAAS,gBAAgB,UAAyC,MAA+B;CAC/F,IAAI,CAAC,MAAM,OAAO,EAAE,MAAM,QAAiB;CAC3C,MAAM,OAAO,OAAO,SAAS,SAAS,SAAS,WAAW,SAAS,SAAS,OAAO,KAAA;CACnF,MAAM,UAAU,OAAO,KAAK,SAAS,QAAQ,KAAA;CAC7C,IAAI,SACF,OAAO;EACL,MAAM;EACN,WAAW,QAAQ;EACnB,UAAU,QAAQ;CACpB;CAEF,OAAO;EACL,MAAM;EACN,YAAY,KAAK;EACjB,OAAO,cAAc,IAAI;CAC3B;AACF;AAEA,SAAgB,cAAc,MAAsC;CAElE,OADe,KAAK,gBAAgB,kBAAkB,YAAY,KAAK,eAAe,SAAS,iBAC/E,WAAW;AAC7B;AAcA,IAAa,4BAAb,MAAoE;CAClE;CACA;CACA;CAEA,YAAY,EAAE,aAA8C,MAA6B;EACvF,KAAKA,aAAa;EAClB,KAAKC,QAAQ;EACb,KAAK,OAAO,KAAK;CACnB;CAEA,MAAM,OAAO,OAAiD;EAC5D,MAAM,CAAC,OAAO,cAAc,iBAAiB,MAAM,QAAQ,IAAI;GAC7D,KAAKD,WAAW,iCAAiC;IAC/C,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,UAAU,CAAC,KAAKC,MAAM,MAAM;GAC9B,CAAC;GACD,KAAKD,WAAW,uBAAuB;IAAE,GAAG,aAAa,KAAK;IAAG,MAAM,KAAK;GAAK,CAAC;GAClF,KAAKA,WAAW,uBAAuB;IAAE,GAAG,aAAa,KAAK;IAAG,MAAM,KAAK;IAAM,OAAO;GAAW,CAAC;EACvG,CAAC;EACD,OAAO;GACL,MAAM,KAAK,IAAI,GAAG,QAAQ,aAAa;GACvC,QAAQ,KAAK,IAAI,GAAG,QAAQ,YAAY;EAC1C;CACF;CAEA,MAAM,OAAO,OAAwD;EAOnE,MAAM,UAAS,MANI,KAAKA,WAAW,yBAAyB;GAC1D,OAAO,MAAM;GACb,kBAAkB,MAAM;GACxB,QAAQ,KAAKC,MAAM;GACnB,OAAO;EACT,CAAC,EAAA,CACmB,UAAU;EAC9B,IAAI,CAAC,QAAQ,OAAO;EACpB,MAAM,WAAW,KAAKA,MAAM,SAAS,MAAM;EAC3C,MAAM,WAAW,MAAM,KAAKD,WAAW,sBAAsB;GAC3D,GAAG,aAAa,KAAK;GACrB,YAAY,CAAC,QAAQ;EACvB,CAAC;EACD,OAAO;GACL,KAAK,oBAAoB,MAAM,kBAAkB,QAAQ;GACzD,IAAI,KAAKC,MAAM,WAAW,MAAM;GAChC,QAAQ,SAAS,WAAW;EAC9B;CACF;CAEA,MAAM,OAAuB,QAAkC;EAC7D,OAAO,YACL,OAAM,WAAU;GACd,MAAM,OAAO,MAAM,KAAKD,WAAW,yBAAyB;IAC1D,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,QAAQ,KAAKC,MAAM;IACnB,QAAQ;IACR,OAAA;GACF,CAAC;GACD,OAAO;IAAE,MAAM,KAAK;IAAW,SAAS,KAAK;GAAQ;EACvD,IACA,cAAa;GAAE,YAAY,KAAKA,MAAM,WAAW,QAAQ;GAAG,IAAI,SAAS;EAAG,IAC5E,MACF;CACF;CAEA,MAAM,KAAK,OAAuB,EAAE,MAAM,QAAQ,QAAQ,SAA0D;EAClH,OAAO,YAAY,KAAKC,MAAM,OAAO,MAAM,GAAG,OAAO,OAAM,cAAa;GACtE,MAAM,CAAC,cAAc,YAAY,MAAM,QAAQ,IAAI,CACjD,KAAKC,aAAa,OAAO,SAAS,GAClC,KAAKC,aAAa,OAAO,SAAS,CACpC,CAAC;GACD,MAAM,UAA4B,CAAC;GACnC,KAAK,MAAM,YAAY,WAAW;IAChC,MAAM,WAAW,KAAKH,MAAM,SAAS,QAAQ;IAC7C,MAAM,UAAU,aAAa,IAAI,oBAAoB,MAAM,kBAAkB,QAAQ,CAAC;IACtF,IAAI,CAAC,YAAY,MAAM,OAAO,GAAG;IACjC,MAAM,OAAO,SAAS,aAAa,SAAS,IAAI,SAAS,UAAU,IAAI,KAAA;IACvE,IAAI,UAAU,CAAC,KAAKA,MAAM,QAAQ,UAAU,MAAM,MAAM,GAAG;IAC3D,MAAM,aAAa,KAAKA,MAAM,WAAW,QAAQ;IACjD,QAAQ,KAAK;KACX;KACA,cAAc;MAAE;MAAY,IAAI,SAAS;KAAG;KAC5C,MAAM;MACJ,KAAK,oBAAoB,MAAM,kBAAkB,QAAQ;MACzD,MAAM,KAAK;MACX,YAAY,SAAS;MACrB,YAAY,SAAS;MACrB,YAAY,SAAS;MACrB,OAAO,KAAKA,MAAM,MAAM,UAAU,IAAI;MACtC,QAAQ,KAAKA,MAAM,OAAO,UAAU,IAAI;MACxC,cAAc,oBAAoB,QAAQ;MAC1C,GAAG,KAAKA,MAAM,QAAQ,QAAQ;MAC9B,YAAY,WAAW,YAAY;MACnC,MAAM,YAAY,KAAA;MAClB,UAAU,SAAS,UAAU;MAC7B,QAAQ,gBAAgB,UAAU,IAAI;KACxC;IACF,CAAC;GACH;GACA,OAAO;EACT,CAAC;CACH;CAEA,MAAME,aACJ,OACA,WACqD;EACrD,MAAM,WAAW,MAAM,KAAKH,WAAW,sBAAsB;GAC3D,GAAG,aAAa,KAAK;GACrB,YAAY,UAAU,KAAI,aAAY,KAAKC,MAAM,SAAS,QAAQ,CAAC;EACrE,CAAC;EACD,OAAO,IAAI,IAAI,SAAS,KAAI,YAAW,CAAC,oBAAoB,MAAM,kBAAkB,OAAO,GAAG,OAAO,CAAC,CAAC;CACzG;CAEA,MAAMG,aACJ,OACA,WACmC;EACnC,MAAM,QAAQ,MAAM,KAAKJ,WAAW,UAAU;GAC5C,OAAO,MAAM;GACb,kBAAkB,MAAM;GACxB,KAAK,UAAU,SAAQ,aAAa,SAAS,aAAa,CAAC,SAAS,UAAU,IAAI,CAAC,CAAE;EACvF,CAAC;EACD,OAAO,IAAI,IAAI,MAAM,KAAI,SAAQ,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;CACnD;CAEA,MAAM,YACJ,OACA,EAAE,QAAQ,OAC6D;EACvE,OAAO,aAAa,KAAKE,MAAM,OAAO,MAAM,GAAG,OAAM,cAAa;GAChE,MAAM,KAAKF,WAAW,0BAA0B;IAC9C,GAAG,aAAa,KAAK;IACrB,YAAY,UAAU,KAAI,aAAY,KAAKC,MAAM,SAAS,QAAQ,CAAC;IACnE;GACF,CAAC;EACH,CAAC;CACH;AACF;AAEA,MAAa,8BAAqD;CAChE,MAAM;CACN,QAAQ;CACR,WAAU,aAAY,iCAAiC,SAAS,IAAI,SAAS,iBAAiB;CAC9F,aAAY,aAAY,SAAS,eAAe,SAAS;CACzD,QAAQ,UAAU,SAAS,MAAM,SAAS,+BAA+B,SAAS,WAAW,CAAC,CAAC;CAC/F,SAAQ,aAAY,SAAS,WAAW,MAAM,GAAG,GAAG,KAAK,+BAA+B,SAAS,WAAW,CAAC,CAAC;CAC9G,QAAO,cAAa;EAClB,aAAa,SAAS;EACtB,UAAU,+BAA+B,SAAS,WAAW,CAAC,CAAC;CACjE;CACA,UAAU,UAAU,MAAM,WACxB,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,MAAM,QAC/C,SAAS,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,MAAM,QACvD,oBAAoB,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,SAAS,MAAM;AAC/D;AAUA,SAAS,yBAAyB,KAA2D;CAC3F,OAAO,IAAI;AACb;AAEA,IAAa,qCAAb,MAA6E;CAC3E,OAAgB;CAChB;CAEA,YAAY,EAAE,aAA8C;EAC1D,KAAKD,aAAa;CACpB;CAEA,MAAM,OAAO,OAAiD;EAC5D,IAAI,OAAO;EACX,IAAI,SAAS;EACb,MAAM,UAAU,aACd,WACE,KAAKA,WAAW,0BAA0B;GAAE,GAAG;GAAO,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAAI,OAAA;EAAsB,CAAC,IAC9G,SAAQ;GAAE,YAAY,IAAI;GAAW,IAAI,IAAI;EAAG,EAClD;EACA,WAAW,MAAM,QAAQ,SAAS;GAChC,MAAM,aAAa,KAAK,KAAK,KAAI,QAC/B,0CAA0C,IAAI,YAAY,IAAI,UAAU,CAC1E;GACA,MAAM,WAAW,MAAM,KAAKA,WAAW,sBAAsB;IAAE,GAAG;IAAO;GAAW,CAAC;GACrF,MAAM,aAAa,IAAI,IACrB,SAAS,KAAI,YAAW,CAAC,GAAG,QAAQ,SAAS,IAAI,QAAQ,cAAc,OAAO,CAAU,CAC1F;GACA,KAAK,MAAM,OAAO,KAAK,MAAM;IAC3B,MAAM,UAAU,WAAW,IAAI,GAAG,IAAI,WAAW,IAAI,IAAI,YAAY;IACrE,IAAI,SAAS,UAAU,YAAY;IACnC,QAAQ;IACR,IAAI,CAAC,SAAS,UAAU;GAC1B;EACF;EACA,OAAO;GAAE;GAAM;EAAO;CACxB;CAEA,MAAM,OAAO,OAAwD;EAEnE,MAAM,UAAS,MADI,KAAKA,WAAW,0BAA0B;GAAE,GAAG;GAAO,OAAO;EAAE,CAAC,EAAA,CAC/D,KAAK;EACzB,IAAI,CAAC,QAAQ,OAAO;EACpB,MAAM,WAAW,0CAA0C,OAAO,YAAY,OAAO,UAAU;EAC/F,MAAM,WAAW,MAAM,KAAKA,WAAW,sBAAsB;GAAE,GAAG;GAAO,YAAY,CAAC,QAAQ;EAAE,CAAC;EACjG,OAAO;GACL,KAAK,oBAAoB,MAAM,kBAAkB,QAAQ;GACzD,IAAI,OAAO;GACX,QAAQ,SAAS,WAAW;EAC9B;CACF;CAEA,KAAK,OAAuB,MAAuD;EACjF,OAAO,YACL,aACE,WACE,KAAKA,WAAW,0BAA0B;GAAE,GAAG;GAAO,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAAI,OAAA;EAAsB,CAAC,IAC9G,SAAQ;GAAE,YAAY,IAAI;GAAW,IAAI,IAAI;EAAG,IAChD,KAAK,MACP,GACA,KAAK,OACL,OAAM,SAAQ;GACZ,MAAM,aAAa,KAAK,KAAI,QAAO,0CAA0C,IAAI,YAAY,IAAI,UAAU,CAAC;GAC5G,MAAM,WAAW,MAAM,KAAKA,WAAW,sBAAsB;IAAE,GAAG;IAAO;GAAW,CAAC;GACrF,MAAM,aAAa,IAAI,IACrB,SAAS,KAAI,YAAW,CAAC,GAAG,QAAQ,SAAS,IAAI,QAAQ,cAAc,OAAO,CAAU,CAC1F;GACA,OAAO,KAAK,SAAQ,QAAO;IACzB,MAAM,UAAU,yBAAyB,GAAG;IAC5C,MAAM,UAAU,WAAW,IAAI,GAAG,IAAI,WAAW,IAAI,IAAI,YAAY;IACrE,IAAI,CAAC,YAAY,KAAK,MAAM,OAAO,GAAG,OAAO,CAAC;IAC9C,MAAM,OAAO,GAAG,QAAQ,MAAM,GAAG,QAAQ,WAAW,YAAY;IAChE,IAAI,KAAK,UAAU,CAAC,KAAK,SAAS,KAAK,MAAM,GAAG,OAAO,CAAC;IACxD,MAAM,WAAW,0CAA0C,IAAI,YAAY,IAAI,UAAU;IACzF,OAAO,CACL;KACE,KAAK,oBAAoB,MAAM,kBAAkB,QAAQ;KACzD,YAAY,IAAI;KAChB,cAAc;MAAE,YAAY,IAAI;MAAW,IAAI,IAAI;KAAG;KACtD;KACA,MAAM;MACJ,KAAK,oBAAoB,MAAM,kBAAkB,QAAQ;MACzD,MAAM,KAAK;MACX,YAAY,IAAI;MAChB,YAAY,IAAI;MAChB,cAAc,QAAQ;MACtB,UAAU,QAAQ;MAClB,OAAO,QAAQ;MACf,QAAQ,QAAQ;MAChB,OAAO,QAAQ;MACf,iBAAiB,QAAQ;MACzB,YAAY,QAAQ;MACpB,YAAY,IAAI,UAAU,YAAY;MACtC,MAAM,QAAQ,OAAO;MACrB,UAAU,SAAS,UAAU;MAC7B,QAAQ,QAAQ,aACZ;OAAE,MAAM;OAAa,YAAY,QAAQ;OAAY,OAAO;MAAO,IACnE,EAAE,MAAM,QAAQ;KACtB;IACF,CACF;GACF,CAAC;EACH,CACF;CACF;CAEA,YACE,OACA,MACuE;EACvE,OAAO,aACL,aACE,WACE,KAAKA,WAAW,0BAA0B;GAAE,GAAG;GAAO,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAAI,OAAA;EAAsB,CAAC,IAC9G,SAAQ;GAAE,YAAY,IAAI;GAAW,IAAI,IAAI;EAAG,IAChD,KAAK,MACP,IACA,SACE,KAAKA,WAAW,0BAA0B;GACxC,GAAG;GACH,YAAY,KAAK,KAAI,QAAO,0CAA0C,IAAI,YAAY,IAAI,UAAU,CAAC;GACrG,KAAK,KAAK;EACZ,CAAC,CACL;CACF;AACF;AAEA,IAAa,2BAAb,MAAmE;CACjE,OAAgB;CAChB;CACA;CAEA,YAAY,EAAE,WAAW,YAAgF;EACvG,KAAKA,aAAa;EAClB,KAAKK,YAAY;CACnB;CAEA,MAAM,OAAuB,QAAkC;EAC7D,OAAO,YACL,OAAM,WAAU;GACd,MAAM,OAAO,MAAM,KAAKA,UAAU,oBAAoB;IACpD,GAAG,aAAa,KAAK;IACrB,GAAI,SAAS,EAAE,QAAQ,OAAO,IAAI,CAAC;IACnC,OAAO;GACT,CAAC;GACD,OAAO;IAAE,MAAM,KAAK,MAAM,GAAA,EAAiB;IAAG,SAAS,KAAK,SAAA;GAAwB;EACtF,IACA,aAAY;GAAE,YAAY,QAAQ;GAAY,IAAI,QAAQ;EAAG,IAC7D,MACF;CACF;;;;;CAMA,MAAMC,SAAS,OAAuB,UAA4D;EAChG,MAAM,CAAC,UAAU,UAAU,SAAS,MAAM,QAAQ,IAAI;GACpD,KAAKN,WAAW,sBAAsB;IACpC,GAAG,aAAa,KAAK;IACrB,YAAY,SAAS,KAAI,YAAW,gCAAgC,QAAQ,SAAS,CAAC;GACxF,CAAC;GACD,KAAKK,UAAU,UAAU;IACvB,OAAO,MAAM;IACb,KAAK,CAAC,GAAG,IAAI,IAAI,SAAS,KAAI,YAAW,QAAQ,SAAS,CAAC,CAAC;GAC9D,CAAC;GACD,KAAKL,WAAW,UAAU;IACxB,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,KAAK,CAAC,GAAG,IAAI,IAAI,SAAS,KAAI,YAAW,QAAQ,UAAU,CAAC,CAAC;GAC/D,CAAC;EACH,CAAC;EACD,MAAM,eAAe,IAAI,IACvB,SAAS,KAAI,YAAW,CAAC,oBAAoB,MAAM,kBAAkB,OAAO,GAAG,OAAO,CAAC,CACzF;EACA,MAAM,cAAc,IAAI,IAAI,SAAS,KAAI,YAAW,CAAC,QAAQ,IAAI,OAAO,CAAC,CAAC;EAC1E,MAAM,WAAW,IAAI,IAAI,MAAM,KAAI,SAAQ,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;EAE3D,MAAM,WAA8B,CAAC;EACrC,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,UAAU,YAAY,IAAI,QAAQ,SAAS;GACjD,MAAM,OAAO,SAAS,IAAI,QAAQ,UAAU;GAC5C,IAAI,CAAC,WAAW,QAAQ,aAAa,CAAC,MAAM;GAC5C,MAAM,WAAW,gCAAgC,QAAQ,SAAS;GAClE,SAAS,KAAK;IACZ;IACA;IACA;IACA;IACA,SAAS,aAAa,IAAI,oBAAoB,MAAM,kBAAkB,QAAQ,CAAC;GACjF,CAAC;EACH;EACA,OAAO;CACT;;;;;;;CAQA,MAAM,OAAO,OAAiD;EAC5D,IAAI,OAAO;EACX,IAAI,SAAS;EACb,WAAW,MAAM,SAAS,KAAKE,MAAM,KAAK,GACxC,KAAK,MAAM,EAAE,aAAa,MAAM,KAAKI,SAAS,OAAO,MAAM,IAAI,GAAG;GAChE,IAAI,SAAS,UAAU,YAAY,QAAQ;GAC3C,IAAI,YAAY,KAAA,GAAW,UAAU;EACvC;EAEF,OAAO;GAAE;GAAM;EAAO;CACxB;CAEA,MAAM,OAAO,OAAwD;EACnE,MAAM,WAAW,MAAM,KAAKD,UAAU,oBAAoB;GAAE,GAAG,aAAa,KAAK;GAAG,OAAO;EAAG,CAAC;EAC/F,MAAM,UAAU,MAAM,KAAKC,SAAS,OAAO,QAAQ,EAAA,CAAG;EACtD,IAAI,CAAC,QAAQ,OAAO;EACpB,OAAO;GACL,KAAK,oBAAoB,MAAM,kBAAkB,OAAO,QAAQ;GAChE,IAAI,OAAO,QAAQ;GACnB,QAAQ,OAAO,YAAY,KAAA;EAC7B;CACF;CAEA,MAAM,KAAK,OAAuB,EAAE,MAAM,QAAQ,QAAQ,SAA0D;EAClH,OAAO,YAAY,KAAKJ,MAAM,OAAO,MAAM,GAAG,OAAO,OAAM,aAAY;GACrE,MAAM,UAA4B,CAAC;GACnC,KAAK,MAAM,YAAY,MAAM,KAAKI,SAAS,OAAO,QAAQ,GAAG;IAC3D,IAAI,CAAC,YAAY,MAAM,SAAS,OAAO,GAAG;IAC1C,IAAI,UAAU,CAAC,qBAAqB,UAAU,MAAM,GAAG;IACvD,QAAQ,KAAK;KACX,YAAY,SAAS,QAAQ;KAC7B,cAAc;MAAE,YAAY,SAAS,QAAQ;MAAY,IAAI,SAAS,QAAQ;KAAG;KACjF,MAAM,cAAc,OAAO,QAAQ;IACrC,CAAC;GACH;GACA,OAAO;EACT,CAAC;CACH;CAEA,MAAM,YACJ,OACA,EAAE,QAAQ,OAC6D;EACvE,OAAO,aAAa,KAAKJ,MAAM,OAAO,MAAM,GAAG,OAAM,aAAY;GAC/D,MAAM,KAAKF,WAAW,0BAA0B;IAC9C,GAAG,aAAa,KAAK;IACrB,YAAY,SAAS,KAAI,YAAW,gCAAgC,QAAQ,SAAS,CAAC;IACtF;GACF,CAAC;EACH,CAAC;CACH;AACF;AAEA,SAAS,qBAAqB,EAAE,MAAM,WAA4B,QAAyB;CACzF,OACE,KAAK,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,KACxC,QAAQ,KAAK,YAAY,CAAC,CAAC,SAAS,MAAM,KAC1C,QAAQ,OAAO,aAAa,YAAY,CAAC,CAAC,SAAS,MAAM,MAAM;AAEnE;AAEA,SAAS,cAAc,OAAuB,EAAE,SAAS,SAAS,MAAM,UAAU,WAA4B;CAC5G,MAAM,aAAa,QAAQ,OAAO;CAClC,OAAO;EACL,KAAK,oBAAoB,MAAM,kBAAkB,QAAQ;EACzD,MAAM;EACN,WAAW,QAAQ;EACnB,YAAY;EACZ,YAAY,QAAQ;EACpB,OAAO,KAAK;EACZ,QAAQ,QAAQ,KAAK,MAAM,GAAG,GAAG;EACjC,UAAU,QAAQ,OAAO;EACzB,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;EACnC,YAAY,QAAQ,WAAW,YAAY;EAC3C,MAAM,YAAY,KAAA;EAClB,UAAU,SAAS,UAAU;EAC7B,QAAQ;GACN,MAAM;GACN,YAAY,QAAQ;GACpB,OAAO,cAAc,IAAI;GACzB,WAAW,QAAQ;EACrB;CACF;AACF;AAEA,SAAgB,aAAa,OAAoF;CAC/G,OAAO;EAAE,OAAO,MAAM;EAAO,kBAAkB,MAAM;EAAkB,QAAQ,MAAM;CAAO;AAC9F"}
1
+ {"version":3,"file":"attention-providers.js","names":["#workItems","#spec","#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 FactoryDispatchStatus,\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\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\n/** One deferred-decision status seen as attention: failures and parked runs differ only by this. */\nexport interface DecisionAttentionSpec {\n kind: FactoryAttentionKind;\n status: FactoryDispatchStatus;\n identity(decision: FactoryDeferredDecisionRecord): FactoryAttentionIdentity;\n occurredAt(decision: FactoryDeferredDecisionRecord): Date;\n title(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string;\n detail(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string;\n extra?(decision: FactoryDeferredDecisionRecord): Record<string, unknown>;\n matches(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined, search: string): boolean;\n}\n\nexport class DecisionAttentionProvider implements AttentionProvider {\n readonly kind: FactoryAttentionKind;\n readonly #workItems: WorkItemsStorage;\n readonly #spec: DecisionAttentionSpec;\n\n constructor({ workItems }: { workItems: WorkItemsStorage }, spec: DecisionAttentionSpec) {\n this.#workItems = workItems;\n this.#spec = spec;\n this.kind = spec.kind;\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n const [total, receiptCount, archivedCount] = await Promise.all([\n this.#workItems.countDeferredDecisionsByStatuses({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n statuses: [this.#spec.status],\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, total - archivedCount),\n unread: Math.max(0, total - receiptCount),\n };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const page = await this.#workItems.listDecisionPageByStatus({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n status: this.#spec.status,\n limit: 1,\n });\n const newest = page.decisions[0];\n if (!newest) return null;\n const identity = this.#spec.identity(newest);\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: [identity],\n });\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n at: this.#spec.occurredAt(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.listDecisionPageByStatus({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n status: this.#spec.status,\n before: cursor,\n limit: SCAN_PAGE_SIZE,\n });\n return { rows: page.decisions, hasMore: page.hasMore };\n },\n decision => ({ occurredAt: this.#spec.occurredAt(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 = this.#spec.identity(decision);\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 && !this.#spec.matches(decision, item, search)) continue;\n const occurredAt = this.#spec.occurredAt(decision);\n entries.push({\n occurredAt,\n resumeCursor: { occurredAt, id: decision.id },\n item: {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: this.kind,\n decisionId: decision.id,\n occurrence: identity.occurrence,\n workItemId: decision.workItemId,\n title: this.#spec.title(decision, item),\n detail: this.#spec.detail(decision, item),\n decisionType: factoryDecisionType(decision),\n ...this.#spec.extra?.(decision),\n occurredAt: occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: attentionTarget(decision, item),\n },\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 => this.#spec.identity(decision)),\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 => this.#spec.identity(decision)),\n now,\n });\n });\n }\n}\n\nexport const failedDecisionAttentionSpec: DecisionAttentionSpec = {\n kind: 'automation-failed',\n status: 'failed',\n identity: decision => factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence),\n occurredAt: decision => decision.completedAt ?? decision.updatedAt,\n title: (decision, item) => item?.title ?? factoryDispatchFailureMetadata(decision.failureCode).label,\n detail: decision => decision.lastError?.slice(0, 512) ?? factoryDispatchFailureMetadata(decision.failureCode).label,\n extra: decision => ({\n failureCode: decision.failureCode,\n canRetry: factoryDispatchFailureMetadata(decision.failureCode).canRetry,\n }),\n matches: (decision, item, search) =>\n item?.title.toLowerCase().includes(search) === true ||\n decision.lastError?.toLowerCase().includes(search) === true ||\n factoryDecisionType(decision).toLowerCase().includes(search),\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.openedAt, 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.openedAt,\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.openedAt, 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.openedAt,\n resumeCursor: { occurredAt: row.openedAt, 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 beganAt: finding.beganAt ?? null,\n suggestedRepair: finding.suggestedRepair,\n workItemId: finding.workItemId,\n occurredAt: row.openedAt.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.openedAt, 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 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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"}
@@ -4,6 +4,7 @@
4
4
  * `null` meaning "not started yet", an absent kind meaning "exhausted".
5
5
  */
6
6
  import type { ApiRoute } from '@mastra/core/server';
7
+ import type { LiveSessions } from '../session/live-sessions.js';
7
8
  import type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';
8
9
  import type { WorkItemsStorage } from '../storage/domains/work-items/base.js';
9
10
  import type { AttentionScope } from './attention-providers.js';
@@ -11,6 +12,7 @@ export { factoryDecisionType } from './attention-providers.js';
11
12
  interface AttentionRouteDependencies {
12
13
  workItems: WorkItemsStorage;
13
14
  comments: WorkItemCommentsStorage;
15
+ liveSessions: Pick<LiveSessions, 'parked' | 'parkedIn'>;
14
16
  resolveProject(context: unknown): Promise<AttentionScope | {
15
17
  response: Response;
16
18
  }>;
@@ -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;AAI/C,OAAO,KAAK,EAIV,cAAc,EAEf,MAAM,0BAA0B,CAAC;AASlC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/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;AA+ID,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,0BAA0B,GAAG,QAAQ,EAAE,CAyIzF"}
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,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAK/C,OAAO,KAAK,EAGV,cAAc,EAEf,MAAM,0BAA0B,CAAC;AASlC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,UAAU,0BAA0B;IAClC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,QAAQ,EAAE,uBAAuB,CAAC;IAClC,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IACxD,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,GAAG;QAAE,QAAQ,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CACpF;AA0HD,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,0BAA0B,GAAG,QAAQ,EAAE,CA4HzF"}
@@ -2,20 +2,10 @@ import { factoryAttentionKey } from "../storage/domains/work-items/base.js";
2
2
  import { FACTORY_ROUTE_CONTRACTS } from "./contracts.js";
3
3
  import { DecisionAttentionProvider, MentionAttentionProvider, SupervisorFindingAttentionProvider, factoryDecisionType, failedDecisionAttentionSpec } from "./attention-providers.js";
4
4
  import { ActivityAttentionProvider } from "./attention-activity.js";
5
+ import { ParkedRunAttentionProvider } from "./attention-parked.js";
5
6
  import { proposedDecisionAttentionSpec } from "./attention-proposed.js";
6
7
  import { registerApiRoute } from "@mastra/core/server";
7
8
  //#region src/routes/attention.ts
8
- /** Kinds the sidebar badge and the notification sound answer to. */
9
- const BADGE_KINDS = /* @__PURE__ */ new Set([
10
- "automation-failed",
11
- "automation-proposed",
12
- "mention",
13
- "supervisor-finding"
14
- ]);
15
- function kindInTier(tier, kind) {
16
- if (tier === "all") return true;
17
- return tier === "badge" ? BADGE_KINDS.has(kind) : !BADGE_KINDS.has(kind);
18
- }
19
9
  function encodeAttentionCursor(cursors) {
20
10
  const wire = {};
21
11
  for (const [kind, position] of cursors) wire[kind] = position ? [position.occurredAt.toISOString(), position.id] : null;
@@ -69,14 +59,6 @@ function mergeAttentionPages(pages, limit) {
69
59
  ...hasMore && nextCursors.size > 0 ? { nextCursor: encodeAttentionCursor(nextCursors) } : {}
70
60
  };
71
61
  }
72
- function newestLatest(latests) {
73
- let newest = null;
74
- for (const latest of latests) {
75
- if (!latest) continue;
76
- if (!newest || latest.at.getTime() > newest.at.getTime()) newest = latest;
77
- }
78
- return newest;
79
- }
80
62
  function receiptRoute(dependencies, verb, action) {
81
63
  const contract = verb === "read" ? FACTORY_ROUTE_CONTRACTS.attentionRead : verb === "archive" ? FACTORY_ROUTE_CONTRACTS.attentionArchive : FACTORY_ROUTE_CONTRACTS.attentionRestore;
82
64
  return registerApiRoute(contract.path, {
@@ -93,6 +75,7 @@ function receiptRoute(dependencies, verb, action) {
93
75
  });
94
76
  if (!parsedPath.success) return context.json({ error: "invalid_attention_item" }, 422);
95
77
  const { kind, sourceId, occurrence } = parsedPath.data;
78
+ if (!(kind !== "agent-waiting" || dependencies.liveSessions.parked(sourceId)?.suspendedAt === occurrence)) return context.json({ error: "attention_item_not_current" }, 409);
96
79
  await dependencies.workItems.ensureReady();
97
80
  const receipt = await dependencies.workItems.setAttentionReceipt({
98
81
  orgId: resolved.orgId,
@@ -117,7 +100,7 @@ function receiptRoute(dependencies, verb, action) {
117
100
  });
118
101
  }
119
102
  function buildAttentionRoutes(dependencies) {
120
- const { workItems, comments } = dependencies;
103
+ const { workItems, comments, liveSessions } = dependencies;
121
104
  const providers = [
122
105
  new DecisionAttentionProvider({ workItems }, failedDecisionAttentionSpec),
123
106
  new DecisionAttentionProvider({ workItems }, proposedDecisionAttentionSpec),
@@ -129,6 +112,10 @@ function buildAttentionRoutes(dependencies) {
129
112
  new ActivityAttentionProvider({
130
113
  workItems,
131
114
  comments
115
+ }),
116
+ new ParkedRunAttentionProvider({
117
+ workItems,
118
+ liveSessions
132
119
  })
133
120
  ];
134
121
  return [
@@ -140,19 +127,20 @@ function buildAttentionRoutes(dependencies) {
140
127
  if ("response" in resolved) return resolved.response;
141
128
  const query = FACTORY_ROUTE_CONTRACTS.attentionList.querySchema.safeParse({
142
129
  view: context.req.query("view"),
143
- tier: context.req.query("tier"),
130
+ kind: context.req.queries("kind"),
144
131
  before: context.req.query("before"),
145
132
  limit: context.req.query("limit"),
146
133
  search: context.req.query("search")
147
134
  });
148
135
  if (!query.success) {
149
136
  const field = query.error.issues[0]?.path[0];
150
- return context.json({ error: field === "tier" ? "invalid_attention_tier" : field === "before" ? "invalid_cursor" : "invalid_attention_view" }, 400);
137
+ return context.json({ error: field === "kind" ? "invalid_attention_kind" : field === "before" ? "invalid_cursor" : "invalid_attention_view" }, 400);
151
138
  }
152
- const { view, tier, before, search, limit } = query.data;
139
+ const { view, before, search, limit } = query.data;
140
+ const kinds = query.data.kind ?? providers.map((provider) => provider.kind);
153
141
  await workItems.ensureReady();
154
142
  await comments.ensureReady();
155
- const active = providers.filter((provider) => kindInTier(tier, provider.kind) && (!before || before.has(provider.kind)));
143
+ const active = providers.filter((provider) => kinds.includes(provider.kind) && (!before || before.has(provider.kind)));
156
144
  const [summaries, pages] = await Promise.all([Promise.all(providers.map(async (provider) => ({
157
145
  kind: provider.kind,
158
146
  counts: await provider.counts(resolved),
@@ -167,24 +155,16 @@ function buildAttentionRoutes(dependencies) {
167
155
  limit
168
156
  })
169
157
  })))]);
170
- const badge = summaries.filter((summary) => BADGE_KINDS.has(summary.kind));
171
- const activity = summaries.filter((summary) => !BADGE_KINDS.has(summary.kind));
172
- const sum = (rows, field) => rows.reduce((total, row) => total + row.counts[field], 0);
173
- const openCount = sum(badge, "open");
174
- const unreadCount = sum(badge, "unread");
175
- const latests = badge.map((summary) => summary.latest);
176
- const unreadLatests = latests.filter((latest) => latest?.unread ?? false);
177
- const latest = unreadLatests.length > 0 ? newestLatest(unreadLatests) : newestLatest(latests);
178
158
  const merged = mergeAttentionPages(pages, limit);
179
159
  return context.json({
180
160
  items: merged.items,
181
- openCount,
182
- badgeCount: unreadCount,
183
- unreadCount,
184
- activityUnreadCount: sum(activity, "unread"),
185
- latestOccurrenceKey: latest?.key ?? null,
186
- latestOccurrenceAt: latest?.at.toISOString() ?? null,
187
- latestOccurrenceUnread: latest?.unread ?? false,
161
+ kinds: Object.fromEntries(summaries.map((summary) => [summary.kind, {
162
+ ...summary.counts,
163
+ latest: summary.latest ? {
164
+ ...summary.latest,
165
+ at: summary.latest.at.toISOString()
166
+ } : null
167
+ }])),
188
168
  hasMore: merged.hasMore,
189
169
  ...merged.nextCursor ? { nextCursor: merged.nextCursor } : {}
190
170
  });
@@ -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 { proposedDecisionAttentionSpec } from './attention-proposed.js';\nimport type {\n AttentionLatest,\n AttentionPageResult,\n AttentionProvider,\n AttentionScope,\n AttentionStreamPosition,\n} from './attention-providers.js';\nimport {\n DecisionAttentionProvider,\n failedDecisionAttentionSpec,\n MentionAttentionProvider,\n SupervisorFindingAttentionProvider,\n} from './attention-providers.js';\nimport { FACTORY_ROUTE_CONTRACTS } from './contracts.js';\n\nexport { factoryDecisionType } from './attention-providers.js';\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\n/** Kinds the sidebar badge and the notification sound answer to. */\nconst BADGE_KINDS: ReadonlySet<FactoryAttentionKind> = new Set([\n 'automation-failed',\n 'automation-proposed',\n 'mention',\n 'supervisor-finding',\n]);\n\ntype AttentionTier = 'all' | 'badge' | 'activity';\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\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 const contract =\n verb === 'read'\n ? FACTORY_ROUTE_CONTRACTS.attentionRead\n : verb === 'archive'\n ? FACTORY_ROUTE_CONTRACTS.attentionArchive\n : FACTORY_ROUTE_CONTRACTS.attentionRestore;\n return registerApiRoute(contract.path, {\n method: contract.method,\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const parsedPath = contract.pathSchema.safeParse({\n id: resolved.factoryProjectId,\n kind: context.req.param('kind'),\n sourceId: context.req.param('sourceId'),\n occurrence: context.req.param('occurrence'),\n });\n if (!parsedPath.success) return context.json({ error: 'invalid_attention_item' }, 422);\n const { kind, sourceId, occurrence } = parsedPath.data;\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 DecisionAttentionProvider({ workItems }, failedDecisionAttentionSpec),\n new DecisionAttentionProvider({ workItems }, proposedDecisionAttentionSpec),\n new SupervisorFindingAttentionProvider({ workItems }),\n new MentionAttentionProvider({ workItems, comments }),\n new ActivityAttentionProvider({ workItems, comments }),\n ];\n\n return [\n registerApiRoute(FACTORY_ROUTE_CONTRACTS.attentionList.path, {\n method: FACTORY_ROUTE_CONTRACTS.attentionList.method,\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const query = FACTORY_ROUTE_CONTRACTS.attentionList.querySchema.safeParse({\n view: context.req.query('view'),\n tier: context.req.query('tier'),\n before: context.req.query('before'),\n limit: context.req.query('limit'),\n search: context.req.query('search'),\n });\n if (!query.success) {\n const field = query.error.issues[0]?.path[0];\n return context.json(\n {\n error:\n field === 'tier'\n ? 'invalid_attention_tier'\n : field === 'before'\n ? 'invalid_cursor'\n : 'invalid_attention_view',\n },\n 400,\n );\n }\n const { view, tier, before, search, limit } = query.data;\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 await workItems.ensureReady();\n await comments.ensureReady();\n\n const active = providers.filter(\n provider => kindInTier(tier, provider.kind) && (!before || before.has(provider.kind)),\n );\n\n const [summaries, pages] = await Promise.all([\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');\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 badgeCount: unreadCount,\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(FACTORY_ROUTE_CONTRACTS.attentionReadAll.path, {\n method: FACTORY_ROUTE_CONTRACTS.attentionReadAll.method,\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const query = FACTORY_ROUTE_CONTRACTS.attentionReadAll.querySchema.safeParse({\n before: context.req.query('before'),\n });\n if (!query.success) return context.json({ error: 'invalid_cursor' }, 400);\n const { before } = query.data;\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 { LiveSessions } from '../session/live-sessions.js';\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 { ParkedRunAttentionProvider } from './attention-parked.js';\nimport { proposedDecisionAttentionSpec } from './attention-proposed.js';\nimport type {\n AttentionPageResult,\n AttentionProvider,\n AttentionScope,\n AttentionStreamPosition,\n} from './attention-providers.js';\nimport {\n DecisionAttentionProvider,\n failedDecisionAttentionSpec,\n MentionAttentionProvider,\n SupervisorFindingAttentionProvider,\n} from './attention-providers.js';\nimport { FACTORY_ROUTE_CONTRACTS } from './contracts.js';\n\nexport { factoryDecisionType } from './attention-providers.js';\n\ninterface AttentionRouteDependencies {\n workItems: WorkItemsStorage;\n comments: WorkItemCommentsStorage;\n liveSessions: Pick<LiveSessions, 'parked' | 'parkedIn'>;\n resolveProject(context: unknown): Promise<AttentionScope | { response: Response }>;\n}\n\ntype AttentionCursorMap = Map<FactoryAttentionKind, AttentionStreamPosition | undefined>;\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\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 receiptRoute(\n dependencies: AttentionRouteDependencies,\n verb: 'read' | 'archive' | 'restore',\n action: FactoryAttentionReceiptAction,\n): ApiRoute {\n const contract =\n verb === 'read'\n ? FACTORY_ROUTE_CONTRACTS.attentionRead\n : verb === 'archive'\n ? FACTORY_ROUTE_CONTRACTS.attentionArchive\n : FACTORY_ROUTE_CONTRACTS.attentionRestore;\n return registerApiRoute(contract.path, {\n method: contract.method,\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const parsedPath = contract.pathSchema.safeParse({\n id: resolved.factoryProjectId,\n kind: context.req.param('kind'),\n sourceId: context.req.param('sourceId'),\n occurrence: context.req.param('occurrence'),\n });\n if (!parsedPath.success) return context.json({ error: 'invalid_attention_item' }, 422);\n const { kind, sourceId, occurrence } = parsedPath.data;\n const stillParked =\n kind !== 'agent-waiting' || dependencies.liveSessions.parked(sourceId)?.suspendedAt === occurrence;\n if (!stillParked) return context.json({ error: 'attention_item_not_current' }, 409);\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, liveSessions } = dependencies;\n const providers: AttentionProvider[] = [\n new DecisionAttentionProvider({ workItems }, failedDecisionAttentionSpec),\n new DecisionAttentionProvider({ workItems }, proposedDecisionAttentionSpec),\n new SupervisorFindingAttentionProvider({ workItems }),\n new MentionAttentionProvider({ workItems, comments }),\n new ActivityAttentionProvider({ workItems, comments }),\n new ParkedRunAttentionProvider({ workItems, liveSessions }),\n ];\n\n return [\n registerApiRoute(FACTORY_ROUTE_CONTRACTS.attentionList.path, {\n method: FACTORY_ROUTE_CONTRACTS.attentionList.method,\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const query = FACTORY_ROUTE_CONTRACTS.attentionList.querySchema.safeParse({\n view: context.req.query('view'),\n kind: context.req.queries('kind'),\n before: context.req.query('before'),\n limit: context.req.query('limit'),\n search: context.req.query('search'),\n });\n if (!query.success) {\n const field = query.error.issues[0]?.path[0];\n return context.json(\n {\n error:\n field === 'kind'\n ? 'invalid_attention_kind'\n : field === 'before'\n ? 'invalid_cursor'\n : 'invalid_attention_view',\n },\n 400,\n );\n }\n const { view, before, search, limit } = query.data;\n const kinds = query.data.kind ?? providers.map(provider => provider.kind);\n await workItems.ensureReady();\n await comments.ensureReady();\n\n const active = providers.filter(\n provider => kinds.includes(provider.kind) && (!before || before.has(provider.kind)),\n );\n\n const [summaries, pages] = await Promise.all([\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 const merged = mergeAttentionPages(pages, limit);\n\n return context.json({\n items: merged.items,\n kinds: Object.fromEntries(\n summaries.map(summary => [\n summary.kind,\n {\n ...summary.counts,\n latest: summary.latest ? { ...summary.latest, at: summary.latest.at.toISOString() } : null,\n },\n ]),\n ),\n hasMore: merged.hasMore,\n ...(merged.nextCursor ? { nextCursor: merged.nextCursor } : {}),\n });\n },\n }),\n registerApiRoute(FACTORY_ROUTE_CONTRACTS.attentionReadAll.path, {\n method: FACTORY_ROUTE_CONTRACTS.attentionReadAll.method,\n requiresAuth: false,\n handler: async context => {\n const resolved = await dependencies.resolveProject(context);\n if ('response' in resolved) return resolved.response;\n const query = FACTORY_ROUTE_CONTRACTS.attentionReadAll.querySchema.safeParse({\n before: context.req.query('before'),\n });\n if (!query.success) return context.json({ error: 'invalid_cursor' }, 400);\n const { before } = query.data;\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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"}