@mastra/factory 0.11.1 → 0.12.0-alpha.1

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 (157) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/factory.d.ts +2 -22
  3. package/dist/factory.d.ts.map +1 -1
  4. package/dist/factory.js +21 -59
  5. package/dist/factory.js.map +1 -1
  6. package/dist/feed-events.d.ts +13 -0
  7. package/dist/feed-events.d.ts.map +1 -0
  8. package/dist/feed-events.js +25 -0
  9. package/dist/feed-events.js.map +1 -0
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/integrations/base.d.ts +5 -13
  13. package/dist/integrations/base.d.ts.map +1 -1
  14. package/dist/integrations/github/config.d.ts +4 -4
  15. package/dist/integrations/github/config.d.ts.map +1 -1
  16. package/dist/integrations/github/config.js +3 -3
  17. package/dist/integrations/github/config.js.map +1 -1
  18. package/dist/integrations/github/integration.d.ts.map +1 -1
  19. package/dist/integrations/github/integration.js +2 -4
  20. package/dist/integrations/github/integration.js.map +1 -1
  21. package/dist/integrations/github/reconcile-worker.d.ts +0 -5
  22. package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
  23. package/dist/integrations/github/reconcile-worker.js +1 -8
  24. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  25. package/dist/integrations/github/routes.d.ts +6 -5
  26. package/dist/integrations/github/routes.d.ts.map +1 -1
  27. package/dist/integrations/github/routes.js +36 -263
  28. package/dist/integrations/github/routes.js.map +1 -1
  29. package/dist/integrations/github/sandbox-release.d.ts +17 -47
  30. package/dist/integrations/github/sandbox-release.d.ts.map +1 -1
  31. package/dist/integrations/github/sandbox-release.js +24 -103
  32. package/dist/integrations/github/sandbox-release.js.map +1 -1
  33. package/dist/integrations/github/sandbox.d.ts +25 -139
  34. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  35. package/dist/integrations/github/sandbox.js +24 -212
  36. package/dist/integrations/github/sandbox.js.map +1 -1
  37. package/dist/integrations/github/webhook.d.ts.map +1 -1
  38. package/dist/integrations/github/webhook.js +1 -2
  39. package/dist/integrations/github/webhook.js.map +1 -1
  40. package/dist/integrations/issue-reconcile-worker.js +1 -1
  41. package/dist/integrations/platform/github/event-worker.d.ts +0 -2
  42. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
  43. package/dist/integrations/platform/github/event-worker.js +1 -8
  44. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  45. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  46. package/dist/integrations/platform/github/integration.js +5 -7
  47. package/dist/integrations/platform/github/integration.js.map +1 -1
  48. package/dist/integrations/platform/linear/event-worker.js +1 -1
  49. package/dist/routes/attention-activity.d.ts +24 -0
  50. package/dist/routes/attention-activity.d.ts.map +1 -0
  51. package/dist/routes/attention-activity.js +143 -0
  52. package/dist/routes/attention-activity.js.map +1 -0
  53. package/dist/routes/attention-providers.d.ts +31 -1
  54. package/dist/routes/attention-providers.d.ts.map +1 -1
  55. package/dist/routes/attention-providers.js +4 -4
  56. package/dist/routes/attention-providers.js.map +1 -1
  57. package/dist/routes/attention.d.ts.map +1 -1
  58. package/dist/routes/attention.js +39 -11
  59. package/dist/routes/attention.js.map +1 -1
  60. package/dist/routes/config.d.ts.map +1 -1
  61. package/dist/routes/config.js +3 -1
  62. package/dist/routes/config.js.map +1 -1
  63. package/dist/routes/fs.d.ts +4 -6
  64. package/dist/routes/fs.d.ts.map +1 -1
  65. package/dist/routes/fs.js +28 -34
  66. package/dist/routes/fs.js.map +1 -1
  67. package/dist/routes/skills.d.ts.map +1 -1
  68. package/dist/routes/skills.js +6 -3
  69. package/dist/routes/skills.js.map +1 -1
  70. package/dist/routes/surface.d.ts +4 -9
  71. package/dist/routes/surface.d.ts.map +1 -1
  72. package/dist/routes/surface.js +3 -5
  73. package/dist/routes/surface.js.map +1 -1
  74. package/dist/sandbox/materialization.d.ts +36 -0
  75. package/dist/sandbox/materialization.d.ts.map +1 -0
  76. package/dist/sandbox/materialization.js +18 -0
  77. package/dist/sandbox/materialization.js.map +1 -0
  78. package/dist/sandbox/session-retirement.d.ts +8 -6
  79. package/dist/sandbox/session-retirement.d.ts.map +1 -1
  80. package/dist/sandbox/session-retirement.js +28 -123
  81. package/dist/sandbox/session-retirement.js.map +1 -1
  82. package/dist/sandbox/session-sandbox.d.ts +117 -0
  83. package/dist/sandbox/session-sandbox.d.ts.map +1 -0
  84. package/dist/sandbox/session-sandbox.js +132 -0
  85. package/dist/sandbox/session-sandbox.js.map +1 -0
  86. package/dist/sandbox/workdir.d.ts +30 -0
  87. package/dist/sandbox/workdir.d.ts.map +1 -0
  88. package/dist/sandbox/workdir.js +48 -0
  89. package/dist/sandbox/workdir.js.map +1 -0
  90. package/dist/session/filesystem-capture.d.ts +2 -1
  91. package/dist/session/filesystem-capture.d.ts.map +1 -1
  92. package/dist/session/filesystem-capture.js +45 -4
  93. package/dist/session/filesystem-capture.js.map +1 -1
  94. package/dist/session/first-exec-capture.d.ts +1 -0
  95. package/dist/session/first-exec-capture.d.ts.map +1 -1
  96. package/dist/session/first-exec-capture.js +1 -1
  97. package/dist/session/first-exec-capture.js.map +1 -1
  98. package/dist/storage/domains/comments/activity.d.ts +40 -0
  99. package/dist/storage/domains/comments/activity.d.ts.map +1 -0
  100. package/dist/storage/domains/comments/activity.js +94 -0
  101. package/dist/storage/domains/comments/activity.js.map +1 -0
  102. package/dist/storage/domains/comments/base.d.ts +13 -1
  103. package/dist/storage/domains/comments/base.d.ts.map +1 -1
  104. package/dist/storage/domains/comments/base.js +41 -3
  105. package/dist/storage/domains/comments/base.js.map +1 -1
  106. package/dist/storage/domains/comments/domain.d.ts +4 -1
  107. package/dist/storage/domains/comments/domain.d.ts.map +1 -1
  108. package/dist/storage/domains/comments/domain.js +17 -5
  109. package/dist/storage/domains/comments/domain.js.map +1 -1
  110. package/dist/storage/domains/comments/parse.d.ts +1 -0
  111. package/dist/storage/domains/comments/parse.d.ts.map +1 -1
  112. package/dist/storage/domains/comments/parse.js +2 -2
  113. package/dist/storage/domains/comments/parse.js.map +1 -1
  114. package/dist/storage/domains/comments/routes.d.ts +2 -0
  115. package/dist/storage/domains/comments/routes.d.ts.map +1 -1
  116. package/dist/storage/domains/comments/routes.js +49 -3
  117. package/dist/storage/domains/comments/routes.js.map +1 -1
  118. package/dist/storage/domains/comments/schema.d.ts +1 -0
  119. package/dist/storage/domains/comments/schema.d.ts.map +1 -1
  120. package/dist/storage/domains/comments/schema.js +45 -1
  121. package/dist/storage/domains/comments/schema.js.map +1 -1
  122. package/dist/storage/domains/integrations/base.d.ts +1 -1
  123. package/dist/storage/domains/integrations/base.js +1 -1
  124. package/dist/storage/domains/integrations/base.js.map +1 -1
  125. package/dist/storage/domains/source-control/base.d.ts +0 -144
  126. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  127. package/dist/storage/domains/source-control/base.js +3 -290
  128. package/dist/storage/domains/source-control/base.js.map +1 -1
  129. package/dist/storage/domains/source-control/inmemory.d.ts +1 -63
  130. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  131. package/dist/storage/domains/source-control/inmemory.js +0 -89
  132. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  133. package/dist/storage/domains/work-items/base.d.ts +14 -1
  134. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  135. package/dist/storage/domains/work-items/base.js +60 -11
  136. package/dist/storage/domains/work-items/base.js.map +1 -1
  137. package/dist/workspace.d.ts +5 -37
  138. package/dist/workspace.d.ts.map +1 -1
  139. package/dist/workspace.js +107 -245
  140. package/dist/workspace.js.map +1 -1
  141. package/package.json +6 -6
  142. package/dist/sandbox/base-checkpoint-triggers.d.ts +0 -77
  143. package/dist/sandbox/base-checkpoint-triggers.d.ts.map +0 -1
  144. package/dist/sandbox/base-checkpoint-triggers.js +0 -137
  145. package/dist/sandbox/base-checkpoint-triggers.js.map +0 -1
  146. package/dist/sandbox/base-checkpoint.d.ts +0 -59
  147. package/dist/sandbox/base-checkpoint.d.ts.map +0 -1
  148. package/dist/sandbox/base-checkpoint.js +0 -145
  149. package/dist/sandbox/base-checkpoint.js.map +0 -1
  150. package/dist/sandbox/fleet.d.ts +0 -258
  151. package/dist/sandbox/fleet.d.ts.map +0 -1
  152. package/dist/sandbox/fleet.js +0 -356
  153. package/dist/sandbox/fleet.js.map +0 -1
  154. package/dist/sandbox/reattach.d.ts +0 -3
  155. package/dist/sandbox/reattach.d.ts.map +0 -1
  156. package/dist/sandbox/reattach.js +0 -16
  157. package/dist/sandbox/reattach.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity.js","names":[],"sources":["../../../../src/storage/domains/comments/activity.ts"],"sourcesContent":["/**\n * Lower attention tier: one collapsed row per (work item, participant), bumped\n * per comment to stale-date read receipts. Participants derive per write —\n * recent authors + creator, minus the comment's mentions; nothing subscribes.\n */\n\nimport type { FactoryStorageOps } from '@mastra/core/storage';\nimport { UniqueViolationError } from '@mastra/core/storage';\n\nimport { isMentionableActorId } from './actor.js';\nimport type { FactoryMentionRef, WorkItemCommentRow } from './base.js';\n\n/** The actor rule-materialized work items are created by; a phantom, never a participant. */\nconst RULE_DISPATCHER_ACTOR_ID = 'factory-rule-dispatcher';\n\n/** Ceiling: an author silent for this many comments stops receiving; a real participants table is the upgrade. */\nexport const PARTICIPANT_SCAN_LIMIT = 200;\n\nexport interface WorkItemActivityRow {\n id: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n participantId: string;\n occurrence: number;\n latestCommentId: string;\n latestAuthorId: string;\n latestAuthorName: string | null;\n occurredAt: Date;\n}\n\ninterface WorkItemActivityDbRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n participant_id: string;\n occurrence: number;\n latest_comment_id: string;\n latest_author_id: string;\n latest_author_name: string | null;\n occurred_at: Date;\n created_at: Date;\n updated_at: Date;\n}\n\nfunction toWorkItemActivity(row: WorkItemActivityDbRow): WorkItemActivityRow {\n return {\n id: row.id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n participantId: row.participant_id,\n occurrence: Number(row.occurrence),\n latestCommentId: row.latest_comment_id,\n latestAuthorId: row.latest_author_id,\n latestAuthorName: row.latest_author_name,\n occurredAt: row.occurred_at,\n };\n}\n\n/**\n * Everyone who should hear about `comment`: the item's recent human comment\n * authors plus its creator, minus the author themselves and anyone the comment\n * already mentions.\n */\nfunction activityParticipants({\n recentAuthors,\n createdBy,\n comment,\n mentions,\n}: {\n recentAuthors: string[];\n createdBy: string;\n comment: WorkItemCommentRow;\n mentions: FactoryMentionRef[];\n}): string[] {\n const excluded = new Set([comment.author.id, ...mentions.map(mention => mention.id)]);\n const candidates = [...recentAuthors, createdBy].filter(\n id => id !== RULE_DISPATCHER_ACTOR_ID && isMentionableActorId(id) && !excluded.has(id),\n );\n return [...new Set(candidates)];\n}\n\n/**\n * Bumps one participant's row, or opens it at occurrence 1. The pointer only\n * ever moves forward, so a backdated ingest can't bury a row someone read.\n *\n * `UniqueViolationError` stays contained here: `create()`'s recover path tests\n * only `instanceof` and would misread an escaped one as a source-key replay.\n */\nasync function bumpParticipant(\n ops: FactoryStorageOps,\n comment: WorkItemCommentRow,\n participantId: string,\n now: Date,\n): Promise<void> {\n const where = {\n work_item_id: comment.workItemId,\n participant_id: participantId,\n org_id: comment.orgId,\n factory_project_id: comment.factoryProjectId,\n };\n const pointer = {\n latest_comment_id: comment.id,\n latest_author_id: comment.author.id,\n latest_author_name: comment.author.displayName ?? null,\n occurred_at: comment.occurredAt,\n };\n // Mutator reads only `current`: a query inside `updateAtomic` checks out a\n // second pool connection while the first is held open.\n const bump = () =>\n ops.updateAtomic<WorkItemActivityDbRow>('work_item_activity', where, current => ({\n occurrence: Number(current.occurrence) + 1,\n ...(current.occurred_at.getTime() <= comment.occurredAt.getTime() ? pointer : {}),\n updated_at: now,\n }));\n\n if (await bump()) return;\n try {\n await ops.insertOne<WorkItemActivityDbRow>('work_item_activity', {\n ...where,\n ...pointer,\n occurrence: 1,\n created_at: now,\n updated_at: now,\n });\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n // Someone opened the row between the miss and the insert. A second miss\n // means a concurrent item delete purged it — nothing left to bump.\n await bump();\n }\n}\n\nexport async function fanOutWorkItemActivity(\n ops: FactoryStorageOps,\n {\n comment,\n recentAuthors,\n createdBy,\n mentions,\n now = new Date(),\n }: {\n comment: WorkItemCommentRow;\n recentAuthors: string[];\n createdBy: string;\n mentions: FactoryMentionRef[];\n now?: Date;\n },\n): Promise<void> {\n for (const participantId of activityParticipants({ recentAuthors, createdBy, comment, mentions })) {\n await bumpParticipant(ops, comment, participantId, now);\n }\n}\n\n/** One bounded keyset page of a participant's activity rows, newest-first. */\nexport async function listActivityForUser(\n ops: FactoryStorageOps,\n {\n orgId,\n factoryProjectId,\n userId,\n before,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n userId: string;\n before?: { occurredAt: Date; id: string };\n limit: number;\n },\n): Promise<WorkItemActivityRow[]> {\n const rows = await ops.findMany<WorkItemActivityDbRow>(\n 'work_item_activity',\n { org_id: orgId, factory_project_id: factoryProjectId, participant_id: userId },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n ...(before ? { cursor: { values: [before.occurredAt, before.id] } } : {}),\n },\n );\n return rows.map(toWorkItemActivity);\n}\n"],"mappings":";;;;AAaA,MAAM,2BAA2B;;AAGjC,MAAa,yBAAyB;AA8BtC,SAAS,mBAAmB,KAAiD;CAC3E,OAAO;EACL,IAAI,IAAI;EACR,OAAO,IAAI;EACX,kBAAkB,IAAI;EACtB,YAAY,IAAI;EAChB,eAAe,IAAI;EACnB,YAAY,OAAO,IAAI,UAAU;EACjC,iBAAiB,IAAI;EACrB,gBAAgB,IAAI;EACpB,kBAAkB,IAAI;EACtB,YAAY,IAAI;CAClB;AACF;;;;;;AAOA,SAAS,qBAAqB,EAC5B,eACA,WACA,SACA,YAMW;CACX,MAAM,2BAAW,IAAI,IAAI,CAAC,QAAQ,OAAO,IAAI,GAAG,SAAS,KAAI,YAAW,QAAQ,EAAE,CAAC,CAAC;CACpF,MAAM,aAAa,CAAC,GAAG,eAAe,SAAS,CAAC,CAAC,QAC/C,OAAM,OAAO,4BAA4B,qBAAqB,EAAE,KAAK,CAAC,SAAS,IAAI,EAAE,CACvF;CACA,OAAO,CAAC,GAAG,IAAI,IAAI,UAAU,CAAC;AAChC;;;;;;;;AASA,eAAe,gBACb,KACA,SACA,eACA,KACe;CACf,MAAM,QAAQ;EACZ,cAAc,QAAQ;EACtB,gBAAgB;EAChB,QAAQ,QAAQ;EAChB,oBAAoB,QAAQ;CAC9B;CACA,MAAM,UAAU;EACd,mBAAmB,QAAQ;EAC3B,kBAAkB,QAAQ,OAAO;EACjC,oBAAoB,QAAQ,OAAO,eAAe;EAClD,aAAa,QAAQ;CACvB;CAGA,MAAM,aACJ,IAAI,aAAoC,sBAAsB,QAAO,aAAY;EAC/E,YAAY,OAAO,QAAQ,UAAU,IAAI;EACzC,GAAI,QAAQ,YAAY,QAAQ,KAAK,QAAQ,WAAW,QAAQ,IAAI,UAAU,CAAC;EAC/E,YAAY;CACd,EAAE;CAEJ,IAAI,MAAM,KAAK,GAAG;CAClB,IAAI;EACF,MAAM,IAAI,UAAiC,sBAAsB;GAC/D,GAAG;GACH,GAAG;GACH,YAAY;GACZ,YAAY;GACZ,YAAY;EACd,CAAC;CACH,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,uBAAuB,MAAM;EAGpD,MAAM,KAAK;CACb;AACF;AAEA,eAAsB,uBACpB,KACA,EACE,SACA,eACA,WACA,UACA,sBAAM,IAAI,KAAK,KAQF;CACf,KAAK,MAAM,iBAAiB,qBAAqB;EAAE;EAAe;EAAW;EAAS;CAAS,CAAC,GAC9F,MAAM,gBAAgB,KAAK,SAAS,eAAe,GAAG;AAE1D;;AAGA,eAAsB,oBACpB,KACA,EACE,OACA,kBACA,QACA,QACA,SAQ8B;CAahC,QAAO,MAZY,IAAI,SACrB,sBACA;EAAE,QAAQ;EAAO,oBAAoB;EAAkB,gBAAgB;CAAO,GAC9E;EACE,SAAS,CACP,CAAC,eAAe,MAAM,GACtB,CAAC,MAAM,MAAM,CACf;EACA;EACA,GAAI,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC,OAAO,YAAY,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC;CACzE,CACF,EAAA,CACY,IAAI,kBAAkB;AACpC"}
@@ -12,8 +12,10 @@
12
12
  */
13
13
  import { FactoryStorageDomain } from '@mastra/core/storage';
14
14
  import type { ExternalWorkItemSource } from '../work-items/base.js';
15
+ import type { WorkItemActivityRow } from './activity.js';
15
16
  import type { FactoryActorRef } from './actor.js';
16
- export { WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';
17
+ export type { WorkItemActivityRow } from './activity.js';
18
+ export { WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';
17
19
  export type WorkItemCommentKind = 'comment';
18
20
  export interface FactoryMentionRef {
19
21
  kind: 'user';
@@ -146,6 +148,16 @@ export declare class WorkItemCommentsStorage extends FactoryStorageDomain {
146
148
  init(): Promise<void>;
147
149
  dangerouslyClearAll(): Promise<void>;
148
150
  create(input: CreateWorkItemCommentInput): Promise<WorkItemCommentRow>;
151
+ listActivityForUser(args: {
152
+ orgId: string;
153
+ factoryProjectId: string;
154
+ userId: string;
155
+ before?: {
156
+ occurredAt: Date;
157
+ id: string;
158
+ };
159
+ limit: number;
160
+ }): Promise<WorkItemActivityRow[]>;
149
161
  /**
150
162
  * Idempotent counter refresh on the parent work item: both columns are read
151
163
  * back off the comments (never incremented or stamped with the wall clock —
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/base.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import { WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA } from "./schema.js";
1
+ import { WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA } from "./schema.js";
2
+ import { fanOutWorkItemActivity, listActivityForUser } from "./activity.js";
2
3
  import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
3
4
  //#region src/storage/domains/comments/base.ts
4
5
  /**
@@ -135,9 +136,14 @@ var WorkItemCommentsStorage = class extends FactoryStorageDomain {
135
136
  super("work-item-comments");
136
137
  }
137
138
  async init() {
138
- await this.ensureCollections([WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA]);
139
+ await this.ensureCollections([
140
+ WORK_ITEM_COMMENTS_SCHEMA,
141
+ WORK_ITEM_COMMENT_MENTIONS_SCHEMA,
142
+ WORK_ITEM_ACTIVITY_SCHEMA
143
+ ]);
139
144
  }
140
145
  async dangerouslyClearAll() {
146
+ await this.ops.deleteMany("work_item_activity", {});
141
147
  await this.ops.deleteMany("work_item_comment_mentions", {});
142
148
  await this.ops.deleteMany("work_item_comments", {});
143
149
  }
@@ -196,12 +202,44 @@ var WorkItemCommentsStorage = class extends FactoryStorageDomain {
196
202
  }
197
203
  return this.#insertWithMentions(row);
198
204
  }
205
+ /**
206
+ * The one insert path a genuinely new comment takes — `create()`'s recover
207
+ * branches never reach it, so a replay writes no mention or activity rows.
208
+ */
199
209
  async #insertWithMentions(row) {
200
210
  const comment = toComment(await this.ops.insertOne("work_item_comments", row));
201
211
  await this.#writeMentionRows(comment, comment.mentions);
212
+ await this.#fanOutActivity(comment);
202
213
  return comment;
203
214
  }
204
215
  /**
216
+ * Participants come off the item's recent authors, so the scan happens here
217
+ * rather than in the fan-out, which must stay free of its own queries.
218
+ */
219
+ async #fanOutActivity(comment) {
220
+ const item = await this.ops.findOne("work_items", {
221
+ id: comment.workItemId,
222
+ org_id: comment.orgId,
223
+ factory_project_id: comment.factoryProjectId
224
+ });
225
+ if (!item) return;
226
+ const recent = await this.listRecent({
227
+ orgId: comment.orgId,
228
+ factoryProjectId: comment.factoryProjectId,
229
+ workItemId: comment.workItemId,
230
+ limit: 200
231
+ });
232
+ await fanOutWorkItemActivity(this.ops, {
233
+ comment,
234
+ recentAuthors: recent.filter((row) => row.author.kind === "user").map((row) => row.author.id),
235
+ createdBy: item.created_by,
236
+ mentions: comment.mentions
237
+ });
238
+ }
239
+ async listActivityForUser(args) {
240
+ return listActivityForUser(this.ops, args);
241
+ }
242
+ /**
205
243
  * Idempotent counter refresh on the parent work item: both columns are read
206
244
  * back off the comments (never incremented or stamped with the wall clock —
207
245
  * replays and races double an increment, and a replayed create would move a
@@ -485,6 +523,6 @@ var WorkItemCommentsStorage = class extends FactoryStorageDomain {
485
523
  }
486
524
  };
487
525
  //#endregion
488
- export { CommentTokenConflictError, MAX_COMMENT_BODY_LENGTH, MAX_COMMENT_MENTIONS, MAX_COMMENT_QUOTE_LENGTH, WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WorkItemCommentsStorage, clampCommentLimit, commentBodyError, commentSourceKey, decodeCommentCursor, encodeCommentCursor, supersedesFeedActivity };
526
+ export { CommentTokenConflictError, MAX_COMMENT_BODY_LENGTH, MAX_COMMENT_MENTIONS, MAX_COMMENT_QUOTE_LENGTH, WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WorkItemCommentsStorage, clampCommentLimit, commentBodyError, commentSourceKey, decodeCommentCursor, encodeCommentCursor, supersedesFeedActivity };
489
527
 
490
528
  //# sourceMappingURL=base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"base.js","names":["#insertWithMentions","#writeMentionRows","#listAround","#listMentionRows"],"sources":["../../../../src/storage/domains/comments/base.ts"],"sourcesContent":["/**\n * Work-item comments domain — one message list per work item, materialized\n * from every source. Local creates and platform mirrors (Slack, Linear,\n * GitHub) share one idempotency story: `external_source` json + derived\n * `source_key` under a partial unique index, a local retry token being just\n * another source (`local:comment:<uuid>`).\n *\n * Ordering is caller-settable `occurred_at`, never insert time — ingest\n * backdates to the platform timestamp and retries arrive out of order.\n * Deletes are soft: the tombstone holds the ordering and its kept\n * `source_key` stops a redelivery from resurrecting the row.\n */\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\n\nimport type { ExternalWorkItemSource } from '../work-items/base.js';\nimport type { FactoryActorExternalIdentity, FactoryActorRef } from './actor.js';\nimport { WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport { WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport type WorkItemCommentKind = 'comment';\n\nexport interface FactoryMentionRef {\n kind: 'user';\n id: string;\n}\n\nexport interface WorkItemCommentReplyRef {\n commentId: string;\n quote?: string;\n authorId?: string;\n authorName?: string;\n}\n\nexport interface WorkItemCommentRow {\n id: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n kind: WorkItemCommentKind;\n body: string;\n bodyFormat: string;\n author: FactoryActorRef;\n replyTo: WorkItemCommentReplyRef | null;\n mentions: FactoryMentionRef[];\n externalSource: ExternalWorkItemSource | null;\n sourceKey: string | null;\n occurredAt: Date;\n editedAt: Date | null;\n deletedAt: Date | null;\n deletedBy: string | null;\n revision: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateWorkItemCommentInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n author: FactoryActorRef;\n body: string;\n bodyFormat?: string;\n replyTo?: WorkItemCommentReplyRef;\n mentions?: FactoryMentionRef[];\n externalSource?: ExternalWorkItemSource;\n /** Local idempotent-retry token; mutually exclusive with `externalSource`. */\n clientToken?: string;\n occurredAt?: Date;\n}\n\nexport interface EditWorkItemCommentInput {\n orgId: string;\n commentId: string;\n body: string;\n mentions?: FactoryMentionRef[];\n /** The acting user; their own handle never becomes a mention row. */\n editorId?: string;\n /** When set, the edit only lands if the row is still at this revision. */\n expectedRevision?: number;\n now?: Date;\n}\n\nexport interface EditWorkItemCommentResult {\n comment: WorkItemCommentRow;\n addedMentions: FactoryMentionRef[];\n removedMentions: FactoryMentionRef[];\n}\n\nexport interface ListWorkItemCommentsInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n before?: string;\n limit?: number;\n /**\n * Anchor the first page on this comment: it and every comment newer than it,\n * so a deep link opens on the target instead of paging back to find it.\n * Ignored when the comment is gone, or sits further back than one page holds.\n */\n around?: string;\n}\n\n/** The only two work-item columns the feed refresh reads or writes. */\ninterface WorkItemFeedColumns extends Record<string, unknown> {\n comment_count: number;\n feed_activity_at: Date | null;\n}\n\n/** A feed snapshot: how many comments the item had, and when it last moved. */\nexport interface FeedActivitySnapshot {\n commentCount: number;\n feedActivityAt: Date;\n}\n\n/**\n * The refresh reads its snapshot outside the write transaction, so two of them\n * can interleave and the loser can write last. This is the one rule that keeps\n * that from undoing a newer refresh: feed activity never moves backwards, and\n * on the same stamp the fuller count wins.\n *\n * Known ceiling: a soft delete sharing a millisecond with a create leaves the\n * count one high until the next feed mutation recounts. Closing that needs the\n * aggregate read inside the write transaction, which the ops layer cannot do\n * without checking out a second pool connection per open transaction.\n */\nexport function supersedesFeedActivity(next: FeedActivitySnapshot, stored: FeedActivitySnapshot): boolean {\n const drift = next.feedActivityAt.getTime() - stored.feedActivityAt.getTime();\n return drift > 0 || (drift === 0 && next.commentCount >= stored.commentCount);\n}\n\nexport interface WorkItemCommentPage {\n comments: WorkItemCommentRow[];\n nextCursor?: string;\n}\n\nexport interface WorkItemMentionRow {\n id: string;\n commentId: string;\n mentionedKind: 'user';\n mentionedId: string;\n authorId: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n occurredAt: Date;\n}\n\nexport const MAX_COMMENT_BODY_LENGTH = 16_000;\nexport const MAX_COMMENT_QUOTE_LENGTH = 500;\nexport const MAX_COMMENT_MENTIONS = 20;\n\n/** The one body policy: HTTP parsing and the service both reject on it. */\nexport function commentBodyError(body: string): string | undefined {\n if (!body.trim()) return 'Comment body must not be empty.';\n if (body.length > MAX_COMMENT_BODY_LENGTH)\n return `Comment body must be at most ${MAX_COMMENT_BODY_LENGTH} characters.`;\n return undefined;\n}\n\nconst DEFAULT_PAGE_SIZE = 30;\nconst MAX_PAGE_SIZE = 100;\n\nexport function clampCommentLimit(limit: number | undefined): number {\n const normalized = typeof limit === 'number' && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE;\n return Math.min(Math.max(normalized, 1), MAX_PAGE_SIZE);\n}\n\nexport function encodeCommentCursor(row: WorkItemCommentRow): string {\n return `${row.occurredAt.toISOString()}_${row.id}`;\n}\n\nexport function decodeCommentCursor(cursor: string): { occurredAt: Date; id: string } | undefined {\n const sep = cursor.lastIndexOf('_');\n if (sep <= 0) return undefined;\n const occurredAt = new Date(cursor.slice(0, sep));\n const id = cursor.slice(sep + 1);\n if (Number.isNaN(occurredAt.getTime()) || !id) return undefined;\n return { occurredAt, id };\n}\n\nexport function commentSourceKey(input: {\n externalSource?: ExternalWorkItemSource;\n clientToken?: string;\n}): string | null {\n if (input.externalSource) {\n const source = input.externalSource;\n return `${source.integrationId}:${source.type}:${source.externalId}`;\n }\n if (input.clientToken) return `local:comment:${input.clientToken}`;\n return null;\n}\n\n/** A `clientToken` replay that resolved to a different work item or author. */\nexport class CommentTokenConflictError extends Error {\n constructor() {\n super('Client token already used by a different comment.');\n this.name = 'CommentTokenConflictError';\n }\n}\n\ninterface WorkItemCommentDbRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n kind: WorkItemCommentKind;\n body: string;\n body_format: string;\n author_kind: FactoryActorRef['kind'];\n author_id: string;\n author_display_name: string | null;\n author_avatar_url: string | null;\n author_external: FactoryActorExternalIdentity | null;\n reply_to_comment_id: string | null;\n reply_quote: string | null;\n reply_to_author_id: string | null;\n reply_to_author_name: string | null;\n mentions: FactoryMentionRef[];\n external_source: ExternalWorkItemSource | null;\n source_key: string | null;\n occurred_at: Date;\n edited_at: Date | null;\n deleted_at: Date | null;\n deleted_by: string | null;\n revision: number;\n created_at: Date;\n updated_at: Date;\n}\n\ninterface WorkItemMentionDbRow extends Record<string, unknown> {\n id: string;\n comment_id: string;\n mentioned_kind: 'user';\n mentioned_id: string;\n author_id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n occurred_at: Date;\n}\n\nfunction toComment(row: WorkItemCommentDbRow): WorkItemCommentRow {\n return {\n id: row.id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n kind: row.kind,\n body: row.body,\n bodyFormat: row.body_format,\n author: {\n kind: row.author_kind,\n id: row.author_id,\n ...(row.author_display_name ? { displayName: row.author_display_name } : {}),\n ...(row.author_avatar_url ? { avatarUrl: row.author_avatar_url } : {}),\n ...(row.author_external ? { external: row.author_external } : {}),\n },\n replyTo: row.reply_to_comment_id\n ? {\n commentId: row.reply_to_comment_id,\n ...(row.reply_quote ? { quote: row.reply_quote } : {}),\n ...(row.reply_to_author_id ? { authorId: row.reply_to_author_id } : {}),\n ...(row.reply_to_author_name ? { authorName: row.reply_to_author_name } : {}),\n }\n : null,\n mentions: row.mentions,\n externalSource: row.external_source,\n sourceKey: row.source_key,\n occurredAt: row.occurred_at,\n editedAt: row.edited_at,\n deletedAt: row.deleted_at,\n deletedBy: row.deleted_by,\n revision: row.revision,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nfunction toMention(row: WorkItemMentionDbRow): WorkItemMentionRow {\n return {\n id: row.id,\n commentId: row.comment_id,\n mentionedKind: row.mentioned_kind,\n mentionedId: row.mentioned_id,\n authorId: row.author_id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n occurredAt: row.occurred_at,\n };\n}\n\nfunction toMentionRef(row: WorkItemMentionRow): FactoryMentionRef {\n return { kind: row.mentionedKind, id: row.mentionedId };\n}\n\nfunction mentionKey(mention: { kind: string; id: string }): string {\n return `${mention.kind}\\0${mention.id}`;\n}\n\nfunction dedupeMentions(mentions: FactoryMentionRef[] | undefined): FactoryMentionRef[] {\n if (!mentions?.length) return [];\n return [...new Map(mentions.map(mention => [mentionKey(mention), mention])).values()];\n}\n\nexport class WorkItemCommentsStorage extends FactoryStorageDomain {\n constructor() {\n super('work-item-comments');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('work_item_comment_mentions', {});\n await this.ops.deleteMany('work_item_comments', {});\n }\n\n async create(input: CreateWorkItemCommentInput): Promise<WorkItemCommentRow> {\n const now = new Date();\n const occurredAt = input.occurredAt ?? now;\n const sourceKey = commentSourceKey(input);\n const author = input.author;\n const row: Partial<WorkItemCommentDbRow> = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n kind: 'comment',\n body: input.body,\n body_format: input.bodyFormat ?? 'markdown',\n author_kind: author.kind,\n author_id: author.id,\n author_display_name: author.displayName ?? null,\n author_avatar_url: author.avatarUrl ?? null,\n author_external: author.external ?? null,\n reply_to_comment_id: input.replyTo?.commentId ?? null,\n reply_quote: input.replyTo?.quote ?? null,\n reply_to_author_id: input.replyTo?.authorId ?? null,\n reply_to_author_name: input.replyTo?.authorName ?? null,\n mentions: dedupeMentions(input.mentions),\n external_source: input.externalSource ?? null,\n source_key: sourceKey,\n occurred_at: occurredAt,\n edited_at: null,\n deleted_at: null,\n deleted_by: null,\n revision: 1,\n created_at: now,\n updated_at: now,\n };\n\n // Insert-or-recover, never upsert: a replayed create must return the\n // existing row untouched (an upsert would clobber a later edit's body).\n // A local token resolving to another item or author is a conflict, not a\n // recovery. External keys stay lenient: a redelivery after a thread\n // re-link maps to a new item id and must still no-op.\n if (sourceKey) {\n const sourceWhere = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n source_key: sourceKey,\n };\n const recover = (found: WorkItemCommentDbRow): WorkItemCommentRow => {\n if (!input.externalSource && (found.work_item_id !== input.workItemId || found.author_id !== author.id)) {\n throw new CommentTokenConflictError();\n }\n return toComment(found);\n };\n const existing = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (existing) return recover(existing);\n try {\n return await this.#insertWithMentions(row);\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n const raced = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (!raced) throw error;\n return recover(raced);\n }\n }\n\n return this.#insertWithMentions(row);\n }\n\n async #insertWithMentions(row: Partial<WorkItemCommentDbRow>): Promise<WorkItemCommentRow> {\n const comment = toComment(await this.ops.insertOne<WorkItemCommentDbRow>('work_item_comments', row));\n await this.#writeMentionRows(comment, comment.mentions);\n return comment;\n }\n\n /**\n * Idempotent counter refresh on the parent work item: both columns are read\n * back off the comments (never incremented or stamped with the wall clock —\n * replays and races double an increment, and a replayed create would move a\n * work item in the feed without adding a comment; a read-back can't drift).\n * Read BEFORE `updateAtomic`: its mutator runs inside an open transaction\n * holding a pool connection, and a query in there checks out a second one —\n * concurrent posts would exhaust the pool. Reading outside means two\n * refreshes can interleave, so the write goes through\n * {@link supersedesFeedActivity} rather than landing whatever it read.\n * Touches ONLY the counter columns: `revision`/`updated_at` are the\n * stage-transition concurrency token.\n */\n async refreshWorkItemFeedActivity({\n orgId,\n factoryProjectId,\n workItemId,\n now = new Date(),\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n now?: Date;\n }): Promise<void> {\n const commentCount = await this.countForWorkItem({ orgId, factoryProjectId, workItemId });\n // Every feed mutation stamps `updated_at`, edits and tombstones included,\n // so the newest one is when this feed last moved.\n const [latest] = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n { orderBy: [['updated_at', 'desc']], limit: 1 },\n );\n const feedActivityAt = latest?.updated_at ?? now;\n await this.ops.updateAtomic<WorkItemFeedColumns>(\n 'work_items',\n { id: workItemId, org_id: orgId, factory_project_id: factoryProjectId },\n row => {\n const next = { commentCount, feedActivityAt };\n const stored = row.feed_activity_at;\n if (stored && !supersedesFeedActivity(next, { commentCount: row.comment_count, feedActivityAt: stored })) {\n return null;\n }\n return { comment_count: commentCount, feed_activity_at: feedActivityAt };\n },\n );\n }\n\n async get({ orgId, commentId }: { orgId: string; commentId: string }): Promise<WorkItemCommentRow | null> {\n const row = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', {\n id: commentId,\n org_id: orgId,\n });\n return row ? toComment(row) : null;\n }\n\n async listByIds({ orgId, ids }: { orgId: string; ids: string[] }): Promise<WorkItemCommentRow[]> {\n if (ids.length === 0) return [];\n const rows = await this.ops.findMany<WorkItemCommentDbRow>('work_item_comments', {\n org_id: orgId,\n id: { in: ids },\n });\n return rows.map(toComment);\n }\n\n async list(input: ListWorkItemCommentsInput): Promise<WorkItemCommentPage> {\n const anchored = input.around ? await this.#listAround(input, input.around) : undefined;\n if (anchored) return anchored;\n const limit = clampCommentLimit(input.limit);\n const cursor = input.before ? decodeCommentCursor(input.before) : undefined;\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit: limit + 1,\n ...(cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}),\n },\n );\n const comments = rows.slice(0, limit).map(toComment);\n const hasMore = rows.length > limit;\n const last = comments[comments.length - 1];\n return {\n comments,\n ...(hasMore && last ? { nextCursor: encodeCommentCursor(last) } : {}),\n };\n }\n\n async #listAround(\n { orgId, factoryProjectId, workItemId }: ListWorkItemCommentsInput,\n commentId: string,\n ): Promise<WorkItemCommentPage | undefined> {\n const target = await this.get({ orgId, commentId });\n if (!target || target.factoryProjectId !== factoryProjectId || target.workItemId !== workItemId) return undefined;\n\n const newer = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n {\n orderBy: [\n ['occurred_at', 'asc'],\n ['id', 'asc'],\n ],\n limit: MAX_PAGE_SIZE + 1,\n cursor: { values: [target.occurredAt, target.id] },\n },\n );\n if (newer.length > MAX_PAGE_SIZE) return undefined;\n\n const before = encodeCommentCursor(target);\n const older = await this.list({ orgId, factoryProjectId, workItemId, before, limit: 1 });\n return {\n comments: [...newer.map(toComment).reverse(), target],\n ...(older.comments.length > 0 ? { nextCursor: before } : {}),\n };\n }\n\n /**\n * Newest non-deleted comments for run-context injection, newest-first (the\n * caller reverses for display order).\n */\n async listRecent({\n orgId,\n factoryProjectId,\n workItemId,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n limit: number;\n }): Promise<WorkItemCommentRow[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n return rows.map(toComment);\n }\n\n async edit(input: EditWorkItemCommentInput): Promise<EditWorkItemCommentResult | 'conflict' | null> {\n const now = input.now ?? new Date();\n let rejection: 'deleted' | 'conflict' | undefined;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: input.commentId, org_id: input.orgId },\n current => {\n if (current.deleted_at) {\n rejection = 'deleted';\n return null;\n }\n if (input.expectedRevision !== undefined && Number(current.revision) !== input.expectedRevision) {\n rejection = 'conflict';\n return null;\n }\n return {\n body: input.body,\n mentions: dedupeMentions(input.mentions ?? current.mentions),\n edited_at: now,\n revision: Number(current.revision) + 1,\n updated_at: now,\n };\n },\n );\n if (rejection === 'conflict') return 'conflict';\n if (!updated || rejection) return null;\n const comment = toComment(updated);\n\n const existing = await this.#listMentionRows(comment.id);\n const existingKeys = new Set(existing.map(mention => mentionKey(toMentionRef(mention))));\n const nextKeys = new Set(comment.mentions.map(mentionKey));\n // Self-mentions (author, or the acting editor) never become rows, so they\n // must not report as \"added\" either — they would re-report on every edit.\n const skippedIds = new Set([comment.author.id, ...(input.editorId ? [input.editorId] : [])]);\n const addedMentions = comment.mentions.filter(\n mention => !existingKeys.has(mentionKey(mention)) && !skippedIds.has(mention.id),\n );\n const removedMentions = existing.map(toMentionRef).filter(mention => !nextKeys.has(mentionKey(mention)));\n\n // Stamped with the edit time, not the comment's creation time: the inbox\n // is a keyset on `occurred_at`, and a backdated row lands buried under\n // everything the user already saw.\n await this.#writeMentionRows(comment, addedMentions, now);\n for (const mention of removedMentions) {\n await this.ops.deleteMany('work_item_comment_mentions', {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n });\n }\n return { comment, addedMentions, removedMentions };\n }\n\n async softDelete({\n orgId,\n commentId,\n deletedBy,\n now = new Date(),\n }: {\n orgId: string;\n commentId: string;\n deletedBy: string;\n now?: Date;\n }): Promise<WorkItemCommentRow | null> {\n let alreadyDeleted = false;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.deleted_at) {\n alreadyDeleted = true;\n return null;\n }\n return {\n body: '',\n mentions: [],\n deleted_at: now,\n deleted_by: deletedBy,\n updated_at: now,\n };\n },\n );\n if (!updated || alreadyDeleted) return null;\n await this.ops.deleteMany('work_item_comment_mentions', { comment_id: commentId });\n return toComment(updated);\n }\n\n /**\n * Provenance write-back after an outbound publish. First platform wins, and\n * an existing `source_key` is kept: replacing a `local:comment:<token>` one\n * would let a client retry duplicate the row. So a web-born comment is never\n * key-deduped against its own platform echo — the host's bot-sender check is\n * the only echo layer for those rows (COR-1174).\n */\n async attachExternalSource({\n orgId,\n commentId,\n source,\n }: {\n orgId: string;\n commentId: string;\n source: ExternalWorkItemSource;\n }): Promise<WorkItemCommentRow | null> {\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.external_source) return null;\n return {\n external_source: source,\n source_key: current.source_key ?? commentSourceKey({ externalSource: source }),\n updated_at: new Date(),\n };\n },\n );\n return updated ? toComment(updated) : null;\n }\n\n async listMentionsForComment(commentId: string): Promise<WorkItemMentionRow[]> {\n return this.#listMentionRows(commentId);\n }\n\n /** Keyset inbox read for the mention attention provider, newest-first. */\n async listMentionsForUser({\n orgId,\n factoryProjectId,\n userId,\n before,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n userId: string;\n before?: { occurredAt: Date; id: string };\n limit: number;\n }): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n mentioned_id: userId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n ...(before ? { cursor: { values: [before.occurredAt, before.id] } } : {}),\n },\n );\n return rows.map(toMention);\n }\n\n async countForWorkItem({\n orgId,\n factoryProjectId,\n workItemId,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n }): Promise<number> {\n if (!this.ops.count) {\n throw new Error('[WorkItemCommentsStorage] storage backend does not support collection counts.');\n }\n return this.ops.count('work_item_comments', {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n });\n }\n\n /** Recent distinct comment authors of a project, for the roster fallback. */\n async listRecentAuthors({\n orgId,\n factoryProjectId,\n limit = 200,\n }: {\n orgId: string;\n factoryProjectId: string;\n limit?: number;\n }): Promise<FactoryActorRef[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n const authors = new Map<string, FactoryActorRef>();\n for (const row of rows) {\n if (authors.has(row.author_id)) continue;\n authors.set(row.author_id, toComment(row).author);\n }\n return [...authors.values()];\n }\n\n async #listMentionRows(commentId: string): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>('work_item_comment_mentions', {\n comment_id: commentId,\n });\n return rows.map(toMention);\n }\n\n /**\n * Self-mentions never get rows: the join exists solely for the attention\n * inbox, and `CollectionWhere` cannot express `author_id != userId` at read\n * time, so the filter happens at write time.\n */\n async #writeMentionRows(\n comment: WorkItemCommentRow,\n mentions: FactoryMentionRef[],\n occurredAt = comment.occurredAt,\n ): Promise<void> {\n for (const mention of mentions) {\n if (mention.id === comment.author.id) continue;\n await this.ops.upsertOne<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n ['comment_id', 'mentioned_kind', 'mentioned_id'],\n {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n author_id: comment.author.id,\n org_id: comment.orgId,\n factory_project_id: comment.factoryProjectId,\n work_item_id: comment.workItemId,\n occurred_at: occurredAt,\n },\n );\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"base.js","names":["#insertWithMentions","#writeMentionRows","#fanOutActivity","#listAround","#listMentionRows"],"sources":["../../../../src/storage/domains/comments/base.ts"],"sourcesContent":["/**\n * Work-item comments domain — one message list per work item, materialized\n * from every source. Local creates and platform mirrors (Slack, Linear,\n * GitHub) share one idempotency story: `external_source` json + derived\n * `source_key` under a partial unique index, a local retry token being just\n * another source (`local:comment:<uuid>`).\n *\n * Ordering is caller-settable `occurred_at`, never insert time — ingest\n * backdates to the platform timestamp and retries arrive out of order.\n * Deletes are soft: the tombstone holds the ordering and its kept\n * `source_key` stops a redelivery from resurrecting the row.\n */\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\n\nimport type { ExternalWorkItemSource } from '../work-items/base.js';\nimport { fanOutWorkItemActivity, listActivityForUser, PARTICIPANT_SCAN_LIMIT } from './activity.js';\nimport type { WorkItemActivityRow } from './activity.js';\nimport type { FactoryActorExternalIdentity, FactoryActorRef } from './actor.js';\nimport { WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport type { WorkItemActivityRow } from './activity.js';\nexport { WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport type WorkItemCommentKind = 'comment';\n\nexport interface FactoryMentionRef {\n kind: 'user';\n id: string;\n}\n\nexport interface WorkItemCommentReplyRef {\n commentId: string;\n quote?: string;\n authorId?: string;\n authorName?: string;\n}\n\nexport interface WorkItemCommentRow {\n id: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n kind: WorkItemCommentKind;\n body: string;\n bodyFormat: string;\n author: FactoryActorRef;\n replyTo: WorkItemCommentReplyRef | null;\n mentions: FactoryMentionRef[];\n externalSource: ExternalWorkItemSource | null;\n sourceKey: string | null;\n occurredAt: Date;\n editedAt: Date | null;\n deletedAt: Date | null;\n deletedBy: string | null;\n revision: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateWorkItemCommentInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n author: FactoryActorRef;\n body: string;\n bodyFormat?: string;\n replyTo?: WorkItemCommentReplyRef;\n mentions?: FactoryMentionRef[];\n externalSource?: ExternalWorkItemSource;\n /** Local idempotent-retry token; mutually exclusive with `externalSource`. */\n clientToken?: string;\n occurredAt?: Date;\n}\n\nexport interface EditWorkItemCommentInput {\n orgId: string;\n commentId: string;\n body: string;\n mentions?: FactoryMentionRef[];\n /** The acting user; their own handle never becomes a mention row. */\n editorId?: string;\n /** When set, the edit only lands if the row is still at this revision. */\n expectedRevision?: number;\n now?: Date;\n}\n\nexport interface EditWorkItemCommentResult {\n comment: WorkItemCommentRow;\n addedMentions: FactoryMentionRef[];\n removedMentions: FactoryMentionRef[];\n}\n\nexport interface ListWorkItemCommentsInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n before?: string;\n limit?: number;\n /**\n * Anchor the first page on this comment: it and every comment newer than it,\n * so a deep link opens on the target instead of paging back to find it.\n * Ignored when the comment is gone, or sits further back than one page holds.\n */\n around?: string;\n}\n\n/** The only two work-item columns the feed refresh reads or writes. */\ninterface WorkItemFeedColumns extends Record<string, unknown> {\n comment_count: number;\n feed_activity_at: Date | null;\n}\n\n/** A feed snapshot: how many comments the item had, and when it last moved. */\nexport interface FeedActivitySnapshot {\n commentCount: number;\n feedActivityAt: Date;\n}\n\n/**\n * The refresh reads its snapshot outside the write transaction, so two of them\n * can interleave and the loser can write last. This is the one rule that keeps\n * that from undoing a newer refresh: feed activity never moves backwards, and\n * on the same stamp the fuller count wins.\n *\n * Known ceiling: a soft delete sharing a millisecond with a create leaves the\n * count one high until the next feed mutation recounts. Closing that needs the\n * aggregate read inside the write transaction, which the ops layer cannot do\n * without checking out a second pool connection per open transaction.\n */\nexport function supersedesFeedActivity(next: FeedActivitySnapshot, stored: FeedActivitySnapshot): boolean {\n const drift = next.feedActivityAt.getTime() - stored.feedActivityAt.getTime();\n return drift > 0 || (drift === 0 && next.commentCount >= stored.commentCount);\n}\n\nexport interface WorkItemCommentPage {\n comments: WorkItemCommentRow[];\n nextCursor?: string;\n}\n\nexport interface WorkItemMentionRow {\n id: string;\n commentId: string;\n mentionedKind: 'user';\n mentionedId: string;\n authorId: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n occurredAt: Date;\n}\n\nexport const MAX_COMMENT_BODY_LENGTH = 16_000;\nexport const MAX_COMMENT_QUOTE_LENGTH = 500;\nexport const MAX_COMMENT_MENTIONS = 20;\n\n/** The one body policy: HTTP parsing and the service both reject on it. */\nexport function commentBodyError(body: string): string | undefined {\n if (!body.trim()) return 'Comment body must not be empty.';\n if (body.length > MAX_COMMENT_BODY_LENGTH)\n return `Comment body must be at most ${MAX_COMMENT_BODY_LENGTH} characters.`;\n return undefined;\n}\n\nconst DEFAULT_PAGE_SIZE = 30;\nconst MAX_PAGE_SIZE = 100;\n\nexport function clampCommentLimit(limit: number | undefined): number {\n const normalized = typeof limit === 'number' && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE;\n return Math.min(Math.max(normalized, 1), MAX_PAGE_SIZE);\n}\n\nexport function encodeCommentCursor(row: WorkItemCommentRow): string {\n return `${row.occurredAt.toISOString()}_${row.id}`;\n}\n\nexport function decodeCommentCursor(cursor: string): { occurredAt: Date; id: string } | undefined {\n const sep = cursor.lastIndexOf('_');\n if (sep <= 0) return undefined;\n const occurredAt = new Date(cursor.slice(0, sep));\n const id = cursor.slice(sep + 1);\n if (Number.isNaN(occurredAt.getTime()) || !id) return undefined;\n return { occurredAt, id };\n}\n\nexport function commentSourceKey(input: {\n externalSource?: ExternalWorkItemSource;\n clientToken?: string;\n}): string | null {\n if (input.externalSource) {\n const source = input.externalSource;\n return `${source.integrationId}:${source.type}:${source.externalId}`;\n }\n if (input.clientToken) return `local:comment:${input.clientToken}`;\n return null;\n}\n\n/** A `clientToken` replay that resolved to a different work item or author. */\nexport class CommentTokenConflictError extends Error {\n constructor() {\n super('Client token already used by a different comment.');\n this.name = 'CommentTokenConflictError';\n }\n}\n\ninterface WorkItemCommentDbRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n kind: WorkItemCommentKind;\n body: string;\n body_format: string;\n author_kind: FactoryActorRef['kind'];\n author_id: string;\n author_display_name: string | null;\n author_avatar_url: string | null;\n author_external: FactoryActorExternalIdentity | null;\n reply_to_comment_id: string | null;\n reply_quote: string | null;\n reply_to_author_id: string | null;\n reply_to_author_name: string | null;\n mentions: FactoryMentionRef[];\n external_source: ExternalWorkItemSource | null;\n source_key: string | null;\n occurred_at: Date;\n edited_at: Date | null;\n deleted_at: Date | null;\n deleted_by: string | null;\n revision: number;\n created_at: Date;\n updated_at: Date;\n}\n\ninterface WorkItemMentionDbRow extends Record<string, unknown> {\n id: string;\n comment_id: string;\n mentioned_kind: 'user';\n mentioned_id: string;\n author_id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n occurred_at: Date;\n}\n\nfunction toComment(row: WorkItemCommentDbRow): WorkItemCommentRow {\n return {\n id: row.id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n kind: row.kind,\n body: row.body,\n bodyFormat: row.body_format,\n author: {\n kind: row.author_kind,\n id: row.author_id,\n ...(row.author_display_name ? { displayName: row.author_display_name } : {}),\n ...(row.author_avatar_url ? { avatarUrl: row.author_avatar_url } : {}),\n ...(row.author_external ? { external: row.author_external } : {}),\n },\n replyTo: row.reply_to_comment_id\n ? {\n commentId: row.reply_to_comment_id,\n ...(row.reply_quote ? { quote: row.reply_quote } : {}),\n ...(row.reply_to_author_id ? { authorId: row.reply_to_author_id } : {}),\n ...(row.reply_to_author_name ? { authorName: row.reply_to_author_name } : {}),\n }\n : null,\n mentions: row.mentions,\n externalSource: row.external_source,\n sourceKey: row.source_key,\n occurredAt: row.occurred_at,\n editedAt: row.edited_at,\n deletedAt: row.deleted_at,\n deletedBy: row.deleted_by,\n revision: row.revision,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nfunction toMention(row: WorkItemMentionDbRow): WorkItemMentionRow {\n return {\n id: row.id,\n commentId: row.comment_id,\n mentionedKind: row.mentioned_kind,\n mentionedId: row.mentioned_id,\n authorId: row.author_id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n occurredAt: row.occurred_at,\n };\n}\n\nfunction toMentionRef(row: WorkItemMentionRow): FactoryMentionRef {\n return { kind: row.mentionedKind, id: row.mentionedId };\n}\n\nfunction mentionKey(mention: { kind: string; id: string }): string {\n return `${mention.kind}\\0${mention.id}`;\n}\n\nfunction dedupeMentions(mentions: FactoryMentionRef[] | undefined): FactoryMentionRef[] {\n if (!mentions?.length) return [];\n return [...new Map(mentions.map(mention => [mentionKey(mention), mention])).values()];\n}\n\nexport class WorkItemCommentsStorage extends FactoryStorageDomain {\n constructor() {\n super('work-item-comments');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([\n WORK_ITEM_COMMENTS_SCHEMA,\n WORK_ITEM_COMMENT_MENTIONS_SCHEMA,\n WORK_ITEM_ACTIVITY_SCHEMA,\n ]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('work_item_activity', {});\n await this.ops.deleteMany('work_item_comment_mentions', {});\n await this.ops.deleteMany('work_item_comments', {});\n }\n\n async create(input: CreateWorkItemCommentInput): Promise<WorkItemCommentRow> {\n const now = new Date();\n const occurredAt = input.occurredAt ?? now;\n const sourceKey = commentSourceKey(input);\n const author = input.author;\n const row: Partial<WorkItemCommentDbRow> = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n kind: 'comment',\n body: input.body,\n body_format: input.bodyFormat ?? 'markdown',\n author_kind: author.kind,\n author_id: author.id,\n author_display_name: author.displayName ?? null,\n author_avatar_url: author.avatarUrl ?? null,\n author_external: author.external ?? null,\n reply_to_comment_id: input.replyTo?.commentId ?? null,\n reply_quote: input.replyTo?.quote ?? null,\n reply_to_author_id: input.replyTo?.authorId ?? null,\n reply_to_author_name: input.replyTo?.authorName ?? null,\n mentions: dedupeMentions(input.mentions),\n external_source: input.externalSource ?? null,\n source_key: sourceKey,\n occurred_at: occurredAt,\n edited_at: null,\n deleted_at: null,\n deleted_by: null,\n revision: 1,\n created_at: now,\n updated_at: now,\n };\n\n // Insert-or-recover, never upsert: a replayed create must return the\n // existing row untouched (an upsert would clobber a later edit's body).\n // A local token resolving to another item or author is a conflict, not a\n // recovery. External keys stay lenient: a redelivery after a thread\n // re-link maps to a new item id and must still no-op.\n if (sourceKey) {\n const sourceWhere = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n source_key: sourceKey,\n };\n const recover = (found: WorkItemCommentDbRow): WorkItemCommentRow => {\n if (!input.externalSource && (found.work_item_id !== input.workItemId || found.author_id !== author.id)) {\n throw new CommentTokenConflictError();\n }\n return toComment(found);\n };\n const existing = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (existing) return recover(existing);\n try {\n return await this.#insertWithMentions(row);\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n const raced = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (!raced) throw error;\n return recover(raced);\n }\n }\n\n return this.#insertWithMentions(row);\n }\n\n /**\n * The one insert path a genuinely new comment takes — `create()`'s recover\n * branches never reach it, so a replay writes no mention or activity rows.\n */\n async #insertWithMentions(row: Partial<WorkItemCommentDbRow>): Promise<WorkItemCommentRow> {\n const comment = toComment(await this.ops.insertOne<WorkItemCommentDbRow>('work_item_comments', row));\n await this.#writeMentionRows(comment, comment.mentions);\n await this.#fanOutActivity(comment);\n return comment;\n }\n\n /**\n * Participants come off the item's recent authors, so the scan happens here\n * rather than in the fan-out, which must stay free of its own queries.\n */\n async #fanOutActivity(comment: WorkItemCommentRow): Promise<void> {\n const item = await this.ops.findOne<{ created_by: string } & Record<string, unknown>>('work_items', {\n id: comment.workItemId,\n org_id: comment.orgId,\n factory_project_id: comment.factoryProjectId,\n });\n if (!item) return;\n const recent = await this.listRecent({\n orgId: comment.orgId,\n factoryProjectId: comment.factoryProjectId,\n workItemId: comment.workItemId,\n limit: PARTICIPANT_SCAN_LIMIT,\n });\n await fanOutWorkItemActivity(this.ops, {\n comment,\n recentAuthors: recent.filter(row => row.author.kind === 'user').map(row => row.author.id),\n createdBy: item.created_by,\n mentions: comment.mentions,\n });\n }\n\n async listActivityForUser(args: {\n orgId: string;\n factoryProjectId: string;\n userId: string;\n before?: { occurredAt: Date; id: string };\n limit: number;\n }): Promise<WorkItemActivityRow[]> {\n return listActivityForUser(this.ops, args);\n }\n\n /**\n * Idempotent counter refresh on the parent work item: both columns are read\n * back off the comments (never incremented or stamped with the wall clock —\n * replays and races double an increment, and a replayed create would move a\n * work item in the feed without adding a comment; a read-back can't drift).\n * Read BEFORE `updateAtomic`: its mutator runs inside an open transaction\n * holding a pool connection, and a query in there checks out a second one —\n * concurrent posts would exhaust the pool. Reading outside means two\n * refreshes can interleave, so the write goes through\n * {@link supersedesFeedActivity} rather than landing whatever it read.\n * Touches ONLY the counter columns: `revision`/`updated_at` are the\n * stage-transition concurrency token.\n */\n async refreshWorkItemFeedActivity({\n orgId,\n factoryProjectId,\n workItemId,\n now = new Date(),\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n now?: Date;\n }): Promise<void> {\n const commentCount = await this.countForWorkItem({ orgId, factoryProjectId, workItemId });\n // Every feed mutation stamps `updated_at`, edits and tombstones included,\n // so the newest one is when this feed last moved.\n const [latest] = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n { orderBy: [['updated_at', 'desc']], limit: 1 },\n );\n const feedActivityAt = latest?.updated_at ?? now;\n await this.ops.updateAtomic<WorkItemFeedColumns>(\n 'work_items',\n { id: workItemId, org_id: orgId, factory_project_id: factoryProjectId },\n row => {\n const next = { commentCount, feedActivityAt };\n const stored = row.feed_activity_at;\n if (stored && !supersedesFeedActivity(next, { commentCount: row.comment_count, feedActivityAt: stored })) {\n return null;\n }\n return { comment_count: commentCount, feed_activity_at: feedActivityAt };\n },\n );\n }\n\n async get({ orgId, commentId }: { orgId: string; commentId: string }): Promise<WorkItemCommentRow | null> {\n const row = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', {\n id: commentId,\n org_id: orgId,\n });\n return row ? toComment(row) : null;\n }\n\n async listByIds({ orgId, ids }: { orgId: string; ids: string[] }): Promise<WorkItemCommentRow[]> {\n if (ids.length === 0) return [];\n const rows = await this.ops.findMany<WorkItemCommentDbRow>('work_item_comments', {\n org_id: orgId,\n id: { in: ids },\n });\n return rows.map(toComment);\n }\n\n async list(input: ListWorkItemCommentsInput): Promise<WorkItemCommentPage> {\n const anchored = input.around ? await this.#listAround(input, input.around) : undefined;\n if (anchored) return anchored;\n const limit = clampCommentLimit(input.limit);\n const cursor = input.before ? decodeCommentCursor(input.before) : undefined;\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit: limit + 1,\n ...(cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}),\n },\n );\n const comments = rows.slice(0, limit).map(toComment);\n const hasMore = rows.length > limit;\n const last = comments[comments.length - 1];\n return {\n comments,\n ...(hasMore && last ? { nextCursor: encodeCommentCursor(last) } : {}),\n };\n }\n\n async #listAround(\n { orgId, factoryProjectId, workItemId }: ListWorkItemCommentsInput,\n commentId: string,\n ): Promise<WorkItemCommentPage | undefined> {\n const target = await this.get({ orgId, commentId });\n if (!target || target.factoryProjectId !== factoryProjectId || target.workItemId !== workItemId) return undefined;\n\n const newer = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n {\n orderBy: [\n ['occurred_at', 'asc'],\n ['id', 'asc'],\n ],\n limit: MAX_PAGE_SIZE + 1,\n cursor: { values: [target.occurredAt, target.id] },\n },\n );\n if (newer.length > MAX_PAGE_SIZE) return undefined;\n\n const before = encodeCommentCursor(target);\n const older = await this.list({ orgId, factoryProjectId, workItemId, before, limit: 1 });\n return {\n comments: [...newer.map(toComment).reverse(), target],\n ...(older.comments.length > 0 ? { nextCursor: before } : {}),\n };\n }\n\n /**\n * Newest non-deleted comments for run-context injection, newest-first (the\n * caller reverses for display order).\n */\n async listRecent({\n orgId,\n factoryProjectId,\n workItemId,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n limit: number;\n }): Promise<WorkItemCommentRow[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n return rows.map(toComment);\n }\n\n async edit(input: EditWorkItemCommentInput): Promise<EditWorkItemCommentResult | 'conflict' | null> {\n const now = input.now ?? new Date();\n let rejection: 'deleted' | 'conflict' | undefined;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: input.commentId, org_id: input.orgId },\n current => {\n if (current.deleted_at) {\n rejection = 'deleted';\n return null;\n }\n if (input.expectedRevision !== undefined && Number(current.revision) !== input.expectedRevision) {\n rejection = 'conflict';\n return null;\n }\n return {\n body: input.body,\n mentions: dedupeMentions(input.mentions ?? current.mentions),\n edited_at: now,\n revision: Number(current.revision) + 1,\n updated_at: now,\n };\n },\n );\n if (rejection === 'conflict') return 'conflict';\n if (!updated || rejection) return null;\n const comment = toComment(updated);\n\n const existing = await this.#listMentionRows(comment.id);\n const existingKeys = new Set(existing.map(mention => mentionKey(toMentionRef(mention))));\n const nextKeys = new Set(comment.mentions.map(mentionKey));\n // Self-mentions (author, or the acting editor) never become rows, so they\n // must not report as \"added\" either — they would re-report on every edit.\n const skippedIds = new Set([comment.author.id, ...(input.editorId ? [input.editorId] : [])]);\n const addedMentions = comment.mentions.filter(\n mention => !existingKeys.has(mentionKey(mention)) && !skippedIds.has(mention.id),\n );\n const removedMentions = existing.map(toMentionRef).filter(mention => !nextKeys.has(mentionKey(mention)));\n\n // Stamped with the edit time, not the comment's creation time: the inbox\n // is a keyset on `occurred_at`, and a backdated row lands buried under\n // everything the user already saw.\n await this.#writeMentionRows(comment, addedMentions, now);\n for (const mention of removedMentions) {\n await this.ops.deleteMany('work_item_comment_mentions', {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n });\n }\n return { comment, addedMentions, removedMentions };\n }\n\n async softDelete({\n orgId,\n commentId,\n deletedBy,\n now = new Date(),\n }: {\n orgId: string;\n commentId: string;\n deletedBy: string;\n now?: Date;\n }): Promise<WorkItemCommentRow | null> {\n let alreadyDeleted = false;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.deleted_at) {\n alreadyDeleted = true;\n return null;\n }\n return {\n body: '',\n mentions: [],\n deleted_at: now,\n deleted_by: deletedBy,\n updated_at: now,\n };\n },\n );\n if (!updated || alreadyDeleted) return null;\n await this.ops.deleteMany('work_item_comment_mentions', { comment_id: commentId });\n return toComment(updated);\n }\n\n /**\n * Provenance write-back after an outbound publish. First platform wins, and\n * an existing `source_key` is kept: replacing a `local:comment:<token>` one\n * would let a client retry duplicate the row. So a web-born comment is never\n * key-deduped against its own platform echo — the host's bot-sender check is\n * the only echo layer for those rows (COR-1174).\n */\n async attachExternalSource({\n orgId,\n commentId,\n source,\n }: {\n orgId: string;\n commentId: string;\n source: ExternalWorkItemSource;\n }): Promise<WorkItemCommentRow | null> {\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.external_source) return null;\n return {\n external_source: source,\n source_key: current.source_key ?? commentSourceKey({ externalSource: source }),\n updated_at: new Date(),\n };\n },\n );\n return updated ? toComment(updated) : null;\n }\n\n async listMentionsForComment(commentId: string): Promise<WorkItemMentionRow[]> {\n return this.#listMentionRows(commentId);\n }\n\n /** Keyset inbox read for the mention attention provider, newest-first. */\n async listMentionsForUser({\n orgId,\n factoryProjectId,\n userId,\n before,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n userId: string;\n before?: { occurredAt: Date; id: string };\n limit: number;\n }): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n mentioned_id: userId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n ...(before ? { cursor: { values: [before.occurredAt, before.id] } } : {}),\n },\n );\n return rows.map(toMention);\n }\n\n async countForWorkItem({\n orgId,\n factoryProjectId,\n workItemId,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n }): Promise<number> {\n if (!this.ops.count) {\n throw new Error('[WorkItemCommentsStorage] storage backend does not support collection counts.');\n }\n return this.ops.count('work_item_comments', {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n });\n }\n\n /** Recent distinct comment authors of a project, for the roster fallback. */\n async listRecentAuthors({\n orgId,\n factoryProjectId,\n limit = 200,\n }: {\n orgId: string;\n factoryProjectId: string;\n limit?: number;\n }): Promise<FactoryActorRef[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n const authors = new Map<string, FactoryActorRef>();\n for (const row of rows) {\n if (authors.has(row.author_id)) continue;\n authors.set(row.author_id, toComment(row).author);\n }\n return [...authors.values()];\n }\n\n async #listMentionRows(commentId: string): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>('work_item_comment_mentions', {\n comment_id: commentId,\n });\n return rows.map(toMention);\n }\n\n /**\n * Self-mentions never get rows: the join exists solely for the attention\n * inbox, and `CollectionWhere` cannot express `author_id != userId` at read\n * time, so the filter happens at write time.\n */\n async #writeMentionRows(\n comment: WorkItemCommentRow,\n mentions: FactoryMentionRef[],\n occurredAt = comment.occurredAt,\n ): Promise<void> {\n for (const mention of mentions) {\n if (mention.id === comment.author.id) continue;\n await this.ops.upsertOne<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n ['comment_id', 'mentioned_kind', 'mentioned_id'],\n {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n author_id: comment.author.id,\n org_id: comment.orgId,\n factory_project_id: comment.factoryProjectId,\n work_item_id: comment.workItemId,\n occurred_at: occurredAt,\n },\n );\n }\n }\n}\n"],"mappings":"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"}
@@ -5,6 +5,7 @@
5
5
  * inside), so agent-authored comments later become a thin tool over
6
6
  * `createComment` instead of a re-implementation.
7
7
  */
8
+ import type { PubSub } from '@mastra/core/events';
8
9
  import type { ApiRoute } from '@mastra/core/server';
9
10
  import type { RouteAuth } from '../../../routes/route.js';
10
11
  import type { AuditEmitter } from '../audit/domain.js';
@@ -33,6 +34,8 @@ export interface CommentsDomainOptions {
33
34
  audit?: AuditEmitter;
34
35
  /** Outbound platform mirrors (COR-1174); empty until a platform wires one. */
35
36
  publishers?: WorkItemFeedPublisher[];
37
+ /** Carries feed touches to every replica's open SSE streams. */
38
+ pubsub: PubSub;
36
39
  }
37
40
  export interface CreateCommentServiceInput {
38
41
  orgId: string;
@@ -105,7 +108,7 @@ export type DeleteCommentServiceResult = {
105
108
  };
106
109
  export declare class CommentsDomain {
107
110
  #private;
108
- constructor({ auth, comments, workItems, projects, channelIdentity, members, audit, publishers, }: CommentsDomainOptions);
111
+ constructor({ auth, comments, workItems, projects, channelIdentity, members, audit, publishers, pubsub, }: CommentsDomainOptions);
109
112
  createComment(input: CreateCommentServiceInput): Promise<CreateCommentServiceResult>;
110
113
  editComment(input: EditCommentServiceInput): Promise<EditCommentServiceResult>;
111
114
  deleteComment(input: DeleteCommentServiceInput): Promise<DeleteCommentServiceResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/domain.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/domain.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
+ import { touchFeed } from "../../../feed-events.js";
1
2
  import { factoryMentionAttentionIdentity } from "../work-items/base.js";
2
- import { CommentTokenConflictError, commentBodyError } from "./base.js";
3
3
  import { isMentionableActorId } from "./actor.js";
4
+ import { CommentTokenConflictError, commentBodyError } from "./base.js";
4
5
  import { toWireComment } from "./wire.js";
5
6
  import { buildCommentRoutes } from "./routes.js";
6
7
  //#region src/storage/domains/comments/domain.ts
@@ -15,8 +16,9 @@ var CommentsDomain = class {
15
16
  #members;
16
17
  #audit;
17
18
  #publishers;
19
+ #pubsub;
18
20
  #rosterCache = /* @__PURE__ */ new Map();
19
- constructor({ auth, comments, workItems, projects, channelIdentity, members, audit, publishers }) {
21
+ constructor({ auth, comments, workItems, projects, channelIdentity, members, audit, publishers, pubsub }) {
20
22
  this.#auth = auth;
21
23
  this.#comments = comments;
22
24
  this.#workItems = workItems;
@@ -25,6 +27,15 @@ var CommentsDomain = class {
25
27
  this.#members = members;
26
28
  this.#audit = audit;
27
29
  this.#publishers = publishers ?? [];
30
+ this.#pubsub = pubsub;
31
+ }
32
+ /**
33
+ * The one seam every feed mutation routes through — future
34
+ * `WorkItemFeedIngest` impls included.
35
+ */
36
+ async #touchFeed(scope) {
37
+ await this.#comments.refreshWorkItemFeedActivity(scope);
38
+ touchFeed(this.#pubsub, scope, scope.workItemId);
28
39
  }
29
40
  async createComment(input) {
30
41
  await this.#comments.ensureReady();
@@ -76,7 +87,7 @@ var CommentsDomain = class {
76
87
  if (error instanceof CommentTokenConflictError) return { status: "token_conflict" };
77
88
  throw error;
78
89
  }
79
- await this.#comments.refreshWorkItemFeedActivity({
90
+ await this.#touchFeed({
80
91
  orgId: input.orgId,
81
92
  factoryProjectId: workItem.factoryProjectId,
82
93
  workItemId: input.workItemId
@@ -146,7 +157,7 @@ var CommentsDomain = class {
146
157
  if (edited === "conflict") return { status: "conflict" };
147
158
  if (!edited) return { status: "not_editable" };
148
159
  await this.#cleanupMentionReceipts(existing, edited.removedMentions);
149
- await this.#comments.refreshWorkItemFeedActivity({
160
+ await this.#touchFeed({
150
161
  orgId: existing.orgId,
151
162
  factoryProjectId: existing.factoryProjectId,
152
163
  workItemId: existing.workItemId
@@ -178,7 +189,7 @@ var CommentsDomain = class {
178
189
  factoryProjectId: existing.factoryProjectId,
179
190
  identities: [factoryMentionAttentionIdentity(existing.id)]
180
191
  });
181
- await this.#comments.refreshWorkItemFeedActivity({
192
+ await this.#touchFeed({
182
193
  orgId: existing.orgId,
183
194
  factoryProjectId: existing.factoryProjectId,
184
195
  workItemId: existing.workItemId
@@ -260,6 +271,7 @@ var CommentsDomain = class {
260
271
  comments: this.#comments,
261
272
  workItems: this.#workItems,
262
273
  projects: this.#projects,
274
+ pubsub: this.#pubsub,
263
275
  ...this.#audit ? { audit: this.#audit } : {}
264
276
  });
265
277
  }
@@ -1 +1 @@
1
- {"version":3,"file":"domain.js","names":["#auth","#comments","#workItems","#projects","#channelIdentity","#members","#audit","#publishers","#rosterCache","#mirrorComment","#cleanupMentionReceipts","#organizationRoster","#seenRoster"],"sources":["../../../../src/storage/domains/comments/domain.ts"],"sourcesContent":["/**\n * Comments domain — feed orchestration behind service methods; the HTTP layer\n * lives in `routes.ts` and only maps their result statuses. Service methods\n * take the author as a `FactoryActorRef` (never derived from the request\n * inside), so agent-authored comments later become a thin tool over\n * `createComment` instead of a re-implementation.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\n\nimport type { RouteAuth } from '../../../routes/route.js';\nimport type { AuditEmitter } from '../audit/domain.js';\nimport type { ChannelIdentityStorage } from '../channel-identity/base.js';\nimport type { FactoryProjectsStorage } from '../projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../work-items/base.js';\nimport { factoryMentionAttentionIdentity } from '../work-items/base.js';\nimport type { FactoryActorRef } from './actor.js';\nimport { isMentionableActorId } from './actor.js';\nimport type {\n EditWorkItemCommentResult,\n FactoryMentionRef,\n WorkItemCommentReplyRef,\n WorkItemCommentRow,\n WorkItemCommentsStorage,\n} from './base.js';\nimport { CommentTokenConflictError, commentBodyError, MAX_COMMENT_MENTIONS, MAX_COMMENT_QUOTE_LENGTH } from './base.js';\nimport type { WorkItemFeedPublisher } from './feed-sync.js';\nimport { buildCommentRoutes } from './routes.js';\n\nexport interface FactoryRosterMember {\n id: string;\n name?: string;\n avatarUrl?: string;\n}\n\nexport interface OrganizationMembersProvider {\n listOrganizationMembers(orgId: string): Promise<FactoryRosterMember[]>;\n}\n\nexport interface CommentsDomainOptions {\n auth: RouteAuth;\n comments: WorkItemCommentsStorage;\n workItems: WorkItemsStorage;\n projects: FactoryProjectsStorage;\n channelIdentity?: ChannelIdentityStorage;\n /** Host-wired org membership listing for the mention roster (e.g. WorkOS). */\n members?: OrganizationMembersProvider;\n audit?: AuditEmitter;\n /** Outbound platform mirrors (COR-1174); empty until a platform wires one. */\n publishers?: WorkItemFeedPublisher[];\n}\n\nexport interface CreateCommentServiceInput {\n orgId: string;\n workItemId: string;\n author: FactoryActorRef;\n body: string;\n replyTo?: { commentId: string; quote?: string };\n mentions?: FactoryMentionRef[];\n clientToken?: string;\n}\n\nexport type CreateCommentServiceResult =\n | { status: 'created'; comment: WorkItemCommentRow; workItem: WorkItemRow }\n | { status: 'work_item_not_found' }\n | { status: 'token_conflict' }\n | { status: 'invalid'; message: string };\n\nexport interface CommentEditor {\n userId: string;\n /** Lazily checked, only when the caller is not the author. */\n canModerate: () => Promise<boolean>;\n}\n\nexport interface EditCommentServiceInput {\n orgId: string;\n workItemId: string;\n commentId: string;\n editor: CommentEditor;\n body: string;\n mentions?: FactoryMentionRef[];\n expectedRevision?: number;\n}\n\nexport type EditCommentServiceResult =\n | ({ status: 'edited'; previousBody: string } & EditWorkItemCommentResult)\n | { status: 'not_found' }\n | { status: 'forbidden' }\n | { status: 'not_editable' }\n | { status: 'conflict' }\n | { status: 'invalid'; message: string };\n\nexport interface DeleteCommentServiceInput {\n orgId: string;\n workItemId: string;\n commentId: string;\n editor: CommentEditor;\n}\n\nexport type DeleteCommentServiceResult =\n | { status: 'deleted'; comment: WorkItemCommentRow }\n | { status: 'not_found' }\n | { status: 'forbidden' }\n | { status: 'not_editable' };\n\nconst MAX_ROSTER_SIZE = 100;\nconst ROSTER_CACHE_TTL_MS = 60_000;\n\nexport class CommentsDomain {\n readonly #auth: RouteAuth;\n readonly #comments: WorkItemCommentsStorage;\n readonly #workItems: WorkItemsStorage;\n readonly #projects: FactoryProjectsStorage;\n readonly #channelIdentity: ChannelIdentityStorage | undefined;\n readonly #members: OrganizationMembersProvider | undefined;\n readonly #audit: AuditEmitter | undefined;\n readonly #publishers: WorkItemFeedPublisher[];\n readonly #rosterCache = new Map<string, { at: number; members: FactoryRosterMember[] }>();\n\n constructor({\n auth,\n comments,\n workItems,\n projects,\n channelIdentity,\n members,\n audit,\n publishers,\n }: CommentsDomainOptions) {\n this.#auth = auth;\n this.#comments = comments;\n this.#workItems = workItems;\n this.#projects = projects;\n this.#channelIdentity = channelIdentity;\n this.#members = members;\n this.#audit = audit;\n this.#publishers = publishers ?? [];\n }\n\n async createComment(input: CreateCommentServiceInput): Promise<CreateCommentServiceResult> {\n await this.#comments.ensureReady();\n await this.#workItems.ensureReady();\n\n const bodyError = commentBodyError(input.body);\n if (bodyError) return { status: 'invalid', message: bodyError };\n if ((input.mentions?.length ?? 0) > MAX_COMMENT_MENTIONS) {\n return { status: 'invalid', message: `At most ${MAX_COMMENT_MENTIONS} mentions per comment.` };\n }\n\n const workItem = await this.#workItems.get({ orgId: input.orgId, id: input.workItemId });\n if (!workItem) return { status: 'work_item_not_found' };\n\n let replyTo: WorkItemCommentReplyRef | undefined;\n if (input.replyTo) {\n const parent = await this.#comments.get({ orgId: input.orgId, commentId: input.replyTo.commentId });\n if (!parent || parent.workItemId !== input.workItemId) {\n return { status: 'invalid', message: 'Reply target is not a comment on this work item.' };\n }\n replyTo = {\n commentId: parent.id,\n ...(input.replyTo.quote ? { quote: input.replyTo.quote.slice(0, MAX_COMMENT_QUOTE_LENGTH) } : {}),\n authorId: parent.author.id,\n ...(parent.author.displayName ? { authorName: parent.author.displayName } : {}),\n };\n }\n\n let comment: WorkItemCommentRow;\n try {\n comment = await this.#comments.create({\n orgId: input.orgId,\n factoryProjectId: workItem.factoryProjectId,\n workItemId: input.workItemId,\n author: input.author,\n body: input.body,\n ...(replyTo ? { replyTo } : {}),\n ...(input.mentions ? { mentions: input.mentions } : {}),\n ...(input.clientToken ? { clientToken: input.clientToken } : {}),\n });\n } catch (error) {\n if (error instanceof CommentTokenConflictError) return { status: 'token_conflict' };\n throw error;\n }\n await this.#comments.refreshWorkItemFeedActivity({\n orgId: input.orgId,\n factoryProjectId: workItem.factoryProjectId,\n workItemId: input.workItemId,\n });\n await this.#mirrorComment(comment, workItem);\n return { status: 'created', comment, workItem };\n }\n\n /**\n * Best-effort: a failed publish never fails the create. The write-back is\n * the replay guard — a replayed create sees its own platform on the row and\n * skips it. Known ceiling, single publisher only: `external_source` is\n * single-valued, so with two publishers a replayed create re-publishes to\n * the one that is not recorded, and nothing persists a failed attempt for a\n * later pass to retry. Both need an outbox if a second publisher ever lands.\n */\n async #mirrorComment(comment: WorkItemCommentRow, workItem: WorkItemRow): Promise<void> {\n let current = comment;\n for (const publisher of this.#publishers) {\n if (current.externalSource?.integrationId === publisher.id) continue;\n try {\n const { source } = await publisher.publish(current, workItem);\n current =\n (await this.#comments.attachExternalSource({ orgId: current.orgId, commentId: current.id, source })) ??\n current;\n } catch (err) {\n console.warn('[Comments] Failed to mirror a comment to a platform', {\n publisherId: publisher.id,\n commentId: current.id,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n }\n\n async editComment(input: EditCommentServiceInput): Promise<EditCommentServiceResult> {\n await this.#comments.ensureReady();\n await this.#workItems.ensureReady();\n\n const bodyError = commentBodyError(input.body);\n if (bodyError) return { status: 'invalid', message: bodyError };\n if ((input.mentions?.length ?? 0) > MAX_COMMENT_MENTIONS) {\n return { status: 'invalid', message: `At most ${MAX_COMMENT_MENTIONS} mentions per comment.` };\n }\n\n const existing = await this.#comments.get({ orgId: input.orgId, commentId: input.commentId });\n if (!existing || existing.workItemId !== input.workItemId) return { status: 'not_found' };\n if (existing.deletedAt) return { status: 'not_editable' };\n if (existing.author.id !== input.editor.userId && !(await input.editor.canModerate())) {\n return { status: 'forbidden' };\n }\n\n const edited = await this.#comments.edit({\n orgId: input.orgId,\n commentId: input.commentId,\n body: input.body,\n editorId: input.editor.userId,\n ...(input.mentions ? { mentions: input.mentions } : {}),\n ...(input.expectedRevision !== undefined ? { expectedRevision: input.expectedRevision } : {}),\n });\n if (edited === 'conflict') return { status: 'conflict' };\n if (!edited) return { status: 'not_editable' };\n\n await this.#cleanupMentionReceipts(existing, edited.removedMentions);\n await this.#comments.refreshWorkItemFeedActivity({\n orgId: existing.orgId,\n factoryProjectId: existing.factoryProjectId,\n workItemId: existing.workItemId,\n });\n return { status: 'edited', previousBody: existing.body, ...edited };\n }\n\n async deleteComment(input: DeleteCommentServiceInput): Promise<DeleteCommentServiceResult> {\n await this.#comments.ensureReady();\n await this.#workItems.ensureReady();\n\n const existing = await this.#comments.get({ orgId: input.orgId, commentId: input.commentId });\n if (!existing || existing.workItemId !== input.workItemId) return { status: 'not_found' };\n if (existing.deletedAt) return { status: 'not_editable' };\n if (existing.author.id !== input.editor.userId && !(await input.editor.canModerate())) {\n return { status: 'forbidden' };\n }\n\n const deleted = await this.#comments.softDelete({\n orgId: input.orgId,\n commentId: input.commentId,\n deletedBy: input.editor.userId,\n });\n if (!deleted) return { status: 'not_editable' };\n\n await this.#workItems.deleteAttentionReceipts({\n orgId: existing.orgId,\n factoryProjectId: existing.factoryProjectId,\n identities: [factoryMentionAttentionIdentity(existing.id)],\n });\n await this.#comments.refreshWorkItemFeedActivity({\n orgId: existing.orgId,\n factoryProjectId: existing.factoryProjectId,\n workItemId: existing.workItemId,\n });\n return { status: 'deleted', comment: deleted };\n }\n\n /**\n * A removed mention deletes only that user's receipt: the receipt identity\n * is per-comment, and other still-mentioned users must keep their read state.\n */\n async #cleanupMentionReceipts(comment: WorkItemCommentRow, removed: FactoryMentionRef[]): Promise<void> {\n for (const mention of removed) {\n await this.#workItems.deleteAttentionReceipts({\n orgId: comment.orgId,\n factoryProjectId: comment.factoryProjectId,\n userId: mention.id,\n identities: [factoryMentionAttentionIdentity(comment.id)],\n });\n }\n }\n\n /**\n * Mentionable people: the host's org roster when it answers, otherwise the\n * people the project has actually seen (comment authors, linked channel\n * accounts). Cached per project so a dropdown session costs one read.\n */\n async mentionRoster({\n orgId,\n factoryProjectId,\n }: {\n orgId: string;\n factoryProjectId: string;\n }): Promise<FactoryRosterMember[]> {\n const cacheKey = `${orgId}\\0${factoryProjectId}`;\n const cached = this.#rosterCache.get(cacheKey);\n if (cached && Date.now() - cached.at < ROSTER_CACHE_TTL_MS) return cached.members;\n\n const members = await this.#organizationRoster(orgId);\n if (members.size === 0) await this.#seenRoster(orgId, factoryProjectId, members);\n\n const roster = [...members.values()].slice(0, MAX_ROSTER_SIZE);\n // Sweep on write: a long-lived server would otherwise hold one roster per\n // project it ever served.\n const cutoff = Date.now() - ROSTER_CACHE_TTL_MS;\n for (const [key, entry] of this.#rosterCache) {\n if (entry.at < cutoff) this.#rosterCache.delete(key);\n }\n this.#rosterCache.set(cacheKey, { at: Date.now(), members: roster });\n return roster;\n }\n\n /** Empty when no provider is wired, or when the host listing fails. */\n async #organizationRoster(orgId: string): Promise<Map<string, FactoryRosterMember>> {\n const members = new Map<string, FactoryRosterMember>();\n if (!this.#members) return members;\n try {\n for (const member of await this.#members.listOrganizationMembers(orgId)) {\n if (isMentionableActorId(member.id)) members.set(member.id, member);\n }\n } catch (err) {\n console.warn('[Comments] Failed to list organization members for the mention roster', {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return members;\n }\n\n async #seenRoster(orgId: string, factoryProjectId: string, members: Map<string, FactoryRosterMember>): Promise<void> {\n for (const author of await this.#comments.listRecentAuthors({ orgId, factoryProjectId })) {\n if (author.kind !== 'user' || !isMentionableActorId(author.id)) continue;\n members.set(author.id, {\n id: author.id,\n ...(author.displayName ? { name: author.displayName } : {}),\n ...(author.avatarUrl ? { avatarUrl: author.avatarUrl } : {}),\n });\n }\n if (!this.#channelIdentity) return;\n await this.#channelIdentity.ensureReady();\n for (const link of await this.#channelIdentity.listAccountLinksForOrg(orgId)) {\n if (members.has(link.userId)) continue;\n members.set(link.userId, {\n id: link.userId,\n ...(link.externalUserName ? { name: link.externalUserName } : {}),\n });\n }\n }\n\n routes(): ApiRoute[] {\n return buildCommentRoutes({\n domain: this,\n auth: this.#auth,\n comments: this.#comments,\n workItems: this.#workItems,\n projects: this.#projects,\n ...(this.#audit ? { audit: this.#audit } : {}),\n });\n }\n}\n\nexport { toWireComment } from './wire.js';\nexport type { WireComment, WireCommentPage } from './wire.js';\n"],"mappings":";;;;;;AAyGA,MAAM,kBAAkB;AACxB,MAAM,sBAAsB;AAE5B,IAAa,iBAAb,MAA4B;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,+BAAwB,IAAI,IAA4D;CAExF,YAAY,EACV,MACA,UACA,WACA,UACA,iBACA,SACA,OACA,cACwB;EACxB,KAAKA,QAAQ;EACb,KAAKC,YAAY;EACjB,KAAKC,aAAa;EAClB,KAAKC,YAAY;EACjB,KAAKC,mBAAmB;EACxB,KAAKC,WAAW;EAChB,KAAKC,SAAS;EACd,KAAKC,cAAc,cAAc,CAAC;CACpC;CAEA,MAAM,cAAc,OAAuE;EACzF,MAAM,KAAKN,UAAU,YAAY;EACjC,MAAM,KAAKC,WAAW,YAAY;EAElC,MAAM,YAAY,iBAAiB,MAAM,IAAI;EAC7C,IAAI,WAAW,OAAO;GAAE,QAAQ;GAAW,SAAS;EAAU;EAC9D,KAAK,MAAM,UAAU,UAAU,KAAA,IAC7B,OAAO;GAAE,QAAQ;GAAW,SAAS;EAAwD;EAG/F,MAAM,WAAW,MAAM,KAAKA,WAAW,IAAI;GAAE,OAAO,MAAM;GAAO,IAAI,MAAM;EAAW,CAAC;EACvF,IAAI,CAAC,UAAU,OAAO,EAAE,QAAQ,sBAAsB;EAEtD,IAAI;EACJ,IAAI,MAAM,SAAS;GACjB,MAAM,SAAS,MAAM,KAAKD,UAAU,IAAI;IAAE,OAAO,MAAM;IAAO,WAAW,MAAM,QAAQ;GAAU,CAAC;GAClG,IAAI,CAAC,UAAU,OAAO,eAAe,MAAM,YACzC,OAAO;IAAE,QAAQ;IAAW,SAAS;GAAmD;GAE1F,UAAU;IACR,WAAW,OAAO;IAClB,GAAI,MAAM,QAAQ,QAAQ,EAAE,OAAO,MAAM,QAAQ,MAAM,MAAM,GAAA,GAA2B,EAAE,IAAI,CAAC;IAC/F,UAAU,OAAO,OAAO;IACxB,GAAI,OAAO,OAAO,cAAc,EAAE,YAAY,OAAO,OAAO,YAAY,IAAI,CAAC;GAC/E;EACF;EAEA,IAAI;EACJ,IAAI;GACF,UAAU,MAAM,KAAKA,UAAU,OAAO;IACpC,OAAO,MAAM;IACb,kBAAkB,SAAS;IAC3B,YAAY,MAAM;IAClB,QAAQ,MAAM;IACd,MAAM,MAAM;IACZ,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;IAC7B,GAAI,MAAM,WAAW,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;IACrD,GAAI,MAAM,cAAc,EAAE,aAAa,MAAM,YAAY,IAAI,CAAC;GAChE,CAAC;EACH,SAAS,OAAO;GACd,IAAI,iBAAiB,2BAA2B,OAAO,EAAE,QAAQ,iBAAiB;GAClF,MAAM;EACR;EACA,MAAM,KAAKA,UAAU,4BAA4B;GAC/C,OAAO,MAAM;GACb,kBAAkB,SAAS;GAC3B,YAAY,MAAM;EACpB,CAAC;EACD,MAAM,KAAKQ,eAAe,SAAS,QAAQ;EAC3C,OAAO;GAAE,QAAQ;GAAW;GAAS;EAAS;CAChD;;;;;;;;;CAUA,MAAMA,eAAe,SAA6B,UAAsC;EACtF,IAAI,UAAU;EACd,KAAK,MAAM,aAAa,KAAKF,aAAa;GACxC,IAAI,QAAQ,gBAAgB,kBAAkB,UAAU,IAAI;GAC5D,IAAI;IACF,MAAM,EAAE,WAAW,MAAM,UAAU,QAAQ,SAAS,QAAQ;IAC5D,UACG,MAAM,KAAKN,UAAU,qBAAqB;KAAE,OAAO,QAAQ;KAAO,WAAW,QAAQ;KAAI;IAAO,CAAC,KAClG;GACJ,SAAS,KAAK;IACZ,QAAQ,KAAK,uDAAuD;KAClE,aAAa,UAAU;KACvB,WAAW,QAAQ;KACnB,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;IACxD,CAAC;GACH;EACF;CACF;CAEA,MAAM,YAAY,OAAmE;EACnF,MAAM,KAAKA,UAAU,YAAY;EACjC,MAAM,KAAKC,WAAW,YAAY;EAElC,MAAM,YAAY,iBAAiB,MAAM,IAAI;EAC7C,IAAI,WAAW,OAAO;GAAE,QAAQ;GAAW,SAAS;EAAU;EAC9D,KAAK,MAAM,UAAU,UAAU,KAAA,IAC7B,OAAO;GAAE,QAAQ;GAAW,SAAS;EAAwD;EAG/F,MAAM,WAAW,MAAM,KAAKD,UAAU,IAAI;GAAE,OAAO,MAAM;GAAO,WAAW,MAAM;EAAU,CAAC;EAC5F,IAAI,CAAC,YAAY,SAAS,eAAe,MAAM,YAAY,OAAO,EAAE,QAAQ,YAAY;EACxF,IAAI,SAAS,WAAW,OAAO,EAAE,QAAQ,eAAe;EACxD,IAAI,SAAS,OAAO,OAAO,MAAM,OAAO,UAAU,CAAE,MAAM,MAAM,OAAO,YAAY,GACjF,OAAO,EAAE,QAAQ,YAAY;EAG/B,MAAM,SAAS,MAAM,KAAKA,UAAU,KAAK;GACvC,OAAO,MAAM;GACb,WAAW,MAAM;GACjB,MAAM,MAAM;GACZ,UAAU,MAAM,OAAO;GACvB,GAAI,MAAM,WAAW,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;GACrD,GAAI,MAAM,qBAAqB,KAAA,IAAY,EAAE,kBAAkB,MAAM,iBAAiB,IAAI,CAAC;EAC7F,CAAC;EACD,IAAI,WAAW,YAAY,OAAO,EAAE,QAAQ,WAAW;EACvD,IAAI,CAAC,QAAQ,OAAO,EAAE,QAAQ,eAAe;EAE7C,MAAM,KAAKS,wBAAwB,UAAU,OAAO,eAAe;EACnE,MAAM,KAAKT,UAAU,4BAA4B;GAC/C,OAAO,SAAS;GAChB,kBAAkB,SAAS;GAC3B,YAAY,SAAS;EACvB,CAAC;EACD,OAAO;GAAE,QAAQ;GAAU,cAAc,SAAS;GAAM,GAAG;EAAO;CACpE;CAEA,MAAM,cAAc,OAAuE;EACzF,MAAM,KAAKA,UAAU,YAAY;EACjC,MAAM,KAAKC,WAAW,YAAY;EAElC,MAAM,WAAW,MAAM,KAAKD,UAAU,IAAI;GAAE,OAAO,MAAM;GAAO,WAAW,MAAM;EAAU,CAAC;EAC5F,IAAI,CAAC,YAAY,SAAS,eAAe,MAAM,YAAY,OAAO,EAAE,QAAQ,YAAY;EACxF,IAAI,SAAS,WAAW,OAAO,EAAE,QAAQ,eAAe;EACxD,IAAI,SAAS,OAAO,OAAO,MAAM,OAAO,UAAU,CAAE,MAAM,MAAM,OAAO,YAAY,GACjF,OAAO,EAAE,QAAQ,YAAY;EAG/B,MAAM,UAAU,MAAM,KAAKA,UAAU,WAAW;GAC9C,OAAO,MAAM;GACb,WAAW,MAAM;GACjB,WAAW,MAAM,OAAO;EAC1B,CAAC;EACD,IAAI,CAAC,SAAS,OAAO,EAAE,QAAQ,eAAe;EAE9C,MAAM,KAAKC,WAAW,wBAAwB;GAC5C,OAAO,SAAS;GAChB,kBAAkB,SAAS;GAC3B,YAAY,CAAC,gCAAgC,SAAS,EAAE,CAAC;EAC3D,CAAC;EACD,MAAM,KAAKD,UAAU,4BAA4B;GAC/C,OAAO,SAAS;GAChB,kBAAkB,SAAS;GAC3B,YAAY,SAAS;EACvB,CAAC;EACD,OAAO;GAAE,QAAQ;GAAW,SAAS;EAAQ;CAC/C;;;;;CAMA,MAAMS,wBAAwB,SAA6B,SAA6C;EACtG,KAAK,MAAM,WAAW,SACpB,MAAM,KAAKR,WAAW,wBAAwB;GAC5C,OAAO,QAAQ;GACf,kBAAkB,QAAQ;GAC1B,QAAQ,QAAQ;GAChB,YAAY,CAAC,gCAAgC,QAAQ,EAAE,CAAC;EAC1D,CAAC;CAEL;;;;;;CAOA,MAAM,cAAc,EAClB,OACA,oBAIiC;EACjC,MAAM,WAAW,GAAG,MAAM,IAAI;EAC9B,MAAM,SAAS,KAAKM,aAAa,IAAI,QAAQ;EAC7C,IAAI,UAAU,KAAK,IAAI,IAAI,OAAO,KAAK,qBAAqB,OAAO,OAAO;EAE1E,MAAM,UAAU,MAAM,KAAKG,oBAAoB,KAAK;EACpD,IAAI,QAAQ,SAAS,GAAG,MAAM,KAAKC,YAAY,OAAO,kBAAkB,OAAO;EAE/E,MAAM,SAAS,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,eAAe;EAG7D,MAAM,SAAS,KAAK,IAAI,IAAI;EAC5B,KAAK,MAAM,CAAC,KAAK,UAAU,KAAKJ,cAC9B,IAAI,MAAM,KAAK,QAAQ,KAAKA,aAAa,OAAO,GAAG;EAErD,KAAKA,aAAa,IAAI,UAAU;GAAE,IAAI,KAAK,IAAI;GAAG,SAAS;EAAO,CAAC;EACnE,OAAO;CACT;;CAGA,MAAMG,oBAAoB,OAA0D;EAClF,MAAM,0BAAU,IAAI,IAAiC;EACrD,IAAI,CAAC,KAAKN,UAAU,OAAO;EAC3B,IAAI;GACF,KAAK,MAAM,UAAU,MAAM,KAAKA,SAAS,wBAAwB,KAAK,GACpE,IAAI,qBAAqB,OAAO,EAAE,GAAG,QAAQ,IAAI,OAAO,IAAI,MAAM;EAEtE,SAAS,KAAK;GACZ,QAAQ,KAAK,yEAAyE,EACpF,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EACxD,CAAC;EACH;EACA,OAAO;CACT;CAEA,MAAMO,YAAY,OAAe,kBAA0B,SAA0D;EACnH,KAAK,MAAM,UAAU,MAAM,KAAKX,UAAU,kBAAkB;GAAE;GAAO;EAAiB,CAAC,GAAG;GACxF,IAAI,OAAO,SAAS,UAAU,CAAC,qBAAqB,OAAO,EAAE,GAAG;GAChE,QAAQ,IAAI,OAAO,IAAI;IACrB,IAAI,OAAO;IACX,GAAI,OAAO,cAAc,EAAE,MAAM,OAAO,YAAY,IAAI,CAAC;IACzD,GAAI,OAAO,YAAY,EAAE,WAAW,OAAO,UAAU,IAAI,CAAC;GAC5D,CAAC;EACH;EACA,IAAI,CAAC,KAAKG,kBAAkB;EAC5B,MAAM,KAAKA,iBAAiB,YAAY;EACxC,KAAK,MAAM,QAAQ,MAAM,KAAKA,iBAAiB,uBAAuB,KAAK,GAAG;GAC5E,IAAI,QAAQ,IAAI,KAAK,MAAM,GAAG;GAC9B,QAAQ,IAAI,KAAK,QAAQ;IACvB,IAAI,KAAK;IACT,GAAI,KAAK,mBAAmB,EAAE,MAAM,KAAK,iBAAiB,IAAI,CAAC;GACjE,CAAC;EACH;CACF;CAEA,SAAqB;EACnB,OAAO,mBAAmB;GACxB,QAAQ;GACR,MAAM,KAAKJ;GACX,UAAU,KAAKC;GACf,WAAW,KAAKC;GAChB,UAAU,KAAKC;GACf,GAAI,KAAKG,SAAS,EAAE,OAAO,KAAKA,OAAO,IAAI,CAAC;EAC9C,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"domain.js","names":["#auth","#comments","#workItems","#projects","#channelIdentity","#members","#audit","#publishers","#pubsub","#rosterCache","#touchFeed","#mirrorComment","#cleanupMentionReceipts","#organizationRoster","#seenRoster"],"sources":["../../../../src/storage/domains/comments/domain.ts"],"sourcesContent":["/**\n * Comments domain — feed orchestration behind service methods; the HTTP layer\n * lives in `routes.ts` and only maps their result statuses. Service methods\n * take the author as a `FactoryActorRef` (never derived from the request\n * inside), so agent-authored comments later become a thin tool over\n * `createComment` instead of a re-implementation.\n */\n\nimport type { PubSub } from '@mastra/core/events';\nimport type { ApiRoute } from '@mastra/core/server';\n\nimport { touchFeed } from '../../../feed-events.js';\nimport type { RouteAuth } from '../../../routes/route.js';\nimport type { AuditEmitter } from '../audit/domain.js';\nimport type { ChannelIdentityStorage } from '../channel-identity/base.js';\nimport type { FactoryProjectsStorage } from '../projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../work-items/base.js';\nimport { factoryMentionAttentionIdentity } from '../work-items/base.js';\nimport type { FactoryActorRef } from './actor.js';\nimport { isMentionableActorId } from './actor.js';\nimport type {\n EditWorkItemCommentResult,\n FactoryMentionRef,\n WorkItemCommentReplyRef,\n WorkItemCommentRow,\n WorkItemCommentsStorage,\n} from './base.js';\nimport { CommentTokenConflictError, commentBodyError, MAX_COMMENT_MENTIONS, MAX_COMMENT_QUOTE_LENGTH } from './base.js';\nimport type { WorkItemFeedPublisher } from './feed-sync.js';\nimport { buildCommentRoutes } from './routes.js';\n\nexport interface FactoryRosterMember {\n id: string;\n name?: string;\n avatarUrl?: string;\n}\n\nexport interface OrganizationMembersProvider {\n listOrganizationMembers(orgId: string): Promise<FactoryRosterMember[]>;\n}\n\nexport interface CommentsDomainOptions {\n auth: RouteAuth;\n comments: WorkItemCommentsStorage;\n workItems: WorkItemsStorage;\n projects: FactoryProjectsStorage;\n channelIdentity?: ChannelIdentityStorage;\n /** Host-wired org membership listing for the mention roster (e.g. WorkOS). */\n members?: OrganizationMembersProvider;\n audit?: AuditEmitter;\n /** Outbound platform mirrors (COR-1174); empty until a platform wires one. */\n publishers?: WorkItemFeedPublisher[];\n /** Carries feed touches to every replica's open SSE streams. */\n pubsub: PubSub;\n}\n\nexport interface CreateCommentServiceInput {\n orgId: string;\n workItemId: string;\n author: FactoryActorRef;\n body: string;\n replyTo?: { commentId: string; quote?: string };\n mentions?: FactoryMentionRef[];\n clientToken?: string;\n}\n\nexport type CreateCommentServiceResult =\n | { status: 'created'; comment: WorkItemCommentRow; workItem: WorkItemRow }\n | { status: 'work_item_not_found' }\n | { status: 'token_conflict' }\n | { status: 'invalid'; message: string };\n\nexport interface CommentEditor {\n userId: string;\n /** Lazily checked, only when the caller is not the author. */\n canModerate: () => Promise<boolean>;\n}\n\nexport interface EditCommentServiceInput {\n orgId: string;\n workItemId: string;\n commentId: string;\n editor: CommentEditor;\n body: string;\n mentions?: FactoryMentionRef[];\n expectedRevision?: number;\n}\n\nexport type EditCommentServiceResult =\n | ({ status: 'edited'; previousBody: string } & EditWorkItemCommentResult)\n | { status: 'not_found' }\n | { status: 'forbidden' }\n | { status: 'not_editable' }\n | { status: 'conflict' }\n | { status: 'invalid'; message: string };\n\nexport interface DeleteCommentServiceInput {\n orgId: string;\n workItemId: string;\n commentId: string;\n editor: CommentEditor;\n}\n\nexport type DeleteCommentServiceResult =\n | { status: 'deleted'; comment: WorkItemCommentRow }\n | { status: 'not_found' }\n | { status: 'forbidden' }\n | { status: 'not_editable' };\n\nconst MAX_ROSTER_SIZE = 100;\nconst ROSTER_CACHE_TTL_MS = 60_000;\n\nexport class CommentsDomain {\n readonly #auth: RouteAuth;\n readonly #comments: WorkItemCommentsStorage;\n readonly #workItems: WorkItemsStorage;\n readonly #projects: FactoryProjectsStorage;\n readonly #channelIdentity: ChannelIdentityStorage | undefined;\n readonly #members: OrganizationMembersProvider | undefined;\n readonly #audit: AuditEmitter | undefined;\n readonly #publishers: WorkItemFeedPublisher[];\n readonly #pubsub: PubSub;\n readonly #rosterCache = new Map<string, { at: number; members: FactoryRosterMember[] }>();\n\n constructor({\n auth,\n comments,\n workItems,\n projects,\n channelIdentity,\n members,\n audit,\n publishers,\n pubsub,\n }: CommentsDomainOptions) {\n this.#auth = auth;\n this.#comments = comments;\n this.#workItems = workItems;\n this.#projects = projects;\n this.#channelIdentity = channelIdentity;\n this.#members = members;\n this.#audit = audit;\n this.#publishers = publishers ?? [];\n this.#pubsub = pubsub;\n }\n\n /**\n * The one seam every feed mutation routes through — future\n * `WorkItemFeedIngest` impls included.\n */\n async #touchFeed(scope: { orgId: string; factoryProjectId: string; workItemId: string }): Promise<void> {\n await this.#comments.refreshWorkItemFeedActivity(scope);\n touchFeed(this.#pubsub, scope, scope.workItemId);\n }\n\n async createComment(input: CreateCommentServiceInput): Promise<CreateCommentServiceResult> {\n await this.#comments.ensureReady();\n await this.#workItems.ensureReady();\n\n const bodyError = commentBodyError(input.body);\n if (bodyError) return { status: 'invalid', message: bodyError };\n if ((input.mentions?.length ?? 0) > MAX_COMMENT_MENTIONS) {\n return { status: 'invalid', message: `At most ${MAX_COMMENT_MENTIONS} mentions per comment.` };\n }\n\n const workItem = await this.#workItems.get({ orgId: input.orgId, id: input.workItemId });\n if (!workItem) return { status: 'work_item_not_found' };\n\n let replyTo: WorkItemCommentReplyRef | undefined;\n if (input.replyTo) {\n const parent = await this.#comments.get({ orgId: input.orgId, commentId: input.replyTo.commentId });\n if (!parent || parent.workItemId !== input.workItemId) {\n return { status: 'invalid', message: 'Reply target is not a comment on this work item.' };\n }\n replyTo = {\n commentId: parent.id,\n ...(input.replyTo.quote ? { quote: input.replyTo.quote.slice(0, MAX_COMMENT_QUOTE_LENGTH) } : {}),\n authorId: parent.author.id,\n ...(parent.author.displayName ? { authorName: parent.author.displayName } : {}),\n };\n }\n\n let comment: WorkItemCommentRow;\n try {\n comment = await this.#comments.create({\n orgId: input.orgId,\n factoryProjectId: workItem.factoryProjectId,\n workItemId: input.workItemId,\n author: input.author,\n body: input.body,\n ...(replyTo ? { replyTo } : {}),\n ...(input.mentions ? { mentions: input.mentions } : {}),\n ...(input.clientToken ? { clientToken: input.clientToken } : {}),\n });\n } catch (error) {\n if (error instanceof CommentTokenConflictError) return { status: 'token_conflict' };\n throw error;\n }\n await this.#touchFeed({\n orgId: input.orgId,\n factoryProjectId: workItem.factoryProjectId,\n workItemId: input.workItemId,\n });\n await this.#mirrorComment(comment, workItem);\n return { status: 'created', comment, workItem };\n }\n\n /**\n * Best-effort: a failed publish never fails the create. The write-back is\n * the replay guard — a replayed create sees its own platform on the row and\n * skips it. Known ceiling, single publisher only: `external_source` is\n * single-valued, so with two publishers a replayed create re-publishes to\n * the one that is not recorded, and nothing persists a failed attempt for a\n * later pass to retry. Both need an outbox if a second publisher ever lands.\n */\n async #mirrorComment(comment: WorkItemCommentRow, workItem: WorkItemRow): Promise<void> {\n let current = comment;\n for (const publisher of this.#publishers) {\n if (current.externalSource?.integrationId === publisher.id) continue;\n try {\n const { source } = await publisher.publish(current, workItem);\n current =\n (await this.#comments.attachExternalSource({ orgId: current.orgId, commentId: current.id, source })) ??\n current;\n } catch (err) {\n console.warn('[Comments] Failed to mirror a comment to a platform', {\n publisherId: publisher.id,\n commentId: current.id,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n }\n\n async editComment(input: EditCommentServiceInput): Promise<EditCommentServiceResult> {\n await this.#comments.ensureReady();\n await this.#workItems.ensureReady();\n\n const bodyError = commentBodyError(input.body);\n if (bodyError) return { status: 'invalid', message: bodyError };\n if ((input.mentions?.length ?? 0) > MAX_COMMENT_MENTIONS) {\n return { status: 'invalid', message: `At most ${MAX_COMMENT_MENTIONS} mentions per comment.` };\n }\n\n const existing = await this.#comments.get({ orgId: input.orgId, commentId: input.commentId });\n if (!existing || existing.workItemId !== input.workItemId) return { status: 'not_found' };\n if (existing.deletedAt) return { status: 'not_editable' };\n if (existing.author.id !== input.editor.userId && !(await input.editor.canModerate())) {\n return { status: 'forbidden' };\n }\n\n const edited = await this.#comments.edit({\n orgId: input.orgId,\n commentId: input.commentId,\n body: input.body,\n editorId: input.editor.userId,\n ...(input.mentions ? { mentions: input.mentions } : {}),\n ...(input.expectedRevision !== undefined ? { expectedRevision: input.expectedRevision } : {}),\n });\n if (edited === 'conflict') return { status: 'conflict' };\n if (!edited) return { status: 'not_editable' };\n\n await this.#cleanupMentionReceipts(existing, edited.removedMentions);\n await this.#touchFeed({\n orgId: existing.orgId,\n factoryProjectId: existing.factoryProjectId,\n workItemId: existing.workItemId,\n });\n return { status: 'edited', previousBody: existing.body, ...edited };\n }\n\n async deleteComment(input: DeleteCommentServiceInput): Promise<DeleteCommentServiceResult> {\n await this.#comments.ensureReady();\n await this.#workItems.ensureReady();\n\n const existing = await this.#comments.get({ orgId: input.orgId, commentId: input.commentId });\n if (!existing || existing.workItemId !== input.workItemId) return { status: 'not_found' };\n if (existing.deletedAt) return { status: 'not_editable' };\n if (existing.author.id !== input.editor.userId && !(await input.editor.canModerate())) {\n return { status: 'forbidden' };\n }\n\n const deleted = await this.#comments.softDelete({\n orgId: input.orgId,\n commentId: input.commentId,\n deletedBy: input.editor.userId,\n });\n if (!deleted) return { status: 'not_editable' };\n\n await this.#workItems.deleteAttentionReceipts({\n orgId: existing.orgId,\n factoryProjectId: existing.factoryProjectId,\n identities: [factoryMentionAttentionIdentity(existing.id)],\n });\n await this.#touchFeed({\n orgId: existing.orgId,\n factoryProjectId: existing.factoryProjectId,\n workItemId: existing.workItemId,\n });\n return { status: 'deleted', comment: deleted };\n }\n\n /**\n * A removed mention deletes only that user's receipt: the receipt identity\n * is per-comment, and other still-mentioned users must keep their read state.\n */\n async #cleanupMentionReceipts(comment: WorkItemCommentRow, removed: FactoryMentionRef[]): Promise<void> {\n for (const mention of removed) {\n await this.#workItems.deleteAttentionReceipts({\n orgId: comment.orgId,\n factoryProjectId: comment.factoryProjectId,\n userId: mention.id,\n identities: [factoryMentionAttentionIdentity(comment.id)],\n });\n }\n }\n\n /**\n * Mentionable people: the host's org roster when it answers, otherwise the\n * people the project has actually seen (comment authors, linked channel\n * accounts). Cached per project so a dropdown session costs one read.\n */\n async mentionRoster({\n orgId,\n factoryProjectId,\n }: {\n orgId: string;\n factoryProjectId: string;\n }): Promise<FactoryRosterMember[]> {\n const cacheKey = `${orgId}\\0${factoryProjectId}`;\n const cached = this.#rosterCache.get(cacheKey);\n if (cached && Date.now() - cached.at < ROSTER_CACHE_TTL_MS) return cached.members;\n\n const members = await this.#organizationRoster(orgId);\n if (members.size === 0) await this.#seenRoster(orgId, factoryProjectId, members);\n\n const roster = [...members.values()].slice(0, MAX_ROSTER_SIZE);\n // Sweep on write: a long-lived server would otherwise hold one roster per\n // project it ever served.\n const cutoff = Date.now() - ROSTER_CACHE_TTL_MS;\n for (const [key, entry] of this.#rosterCache) {\n if (entry.at < cutoff) this.#rosterCache.delete(key);\n }\n this.#rosterCache.set(cacheKey, { at: Date.now(), members: roster });\n return roster;\n }\n\n /** Empty when no provider is wired, or when the host listing fails. */\n async #organizationRoster(orgId: string): Promise<Map<string, FactoryRosterMember>> {\n const members = new Map<string, FactoryRosterMember>();\n if (!this.#members) return members;\n try {\n for (const member of await this.#members.listOrganizationMembers(orgId)) {\n if (isMentionableActorId(member.id)) members.set(member.id, member);\n }\n } catch (err) {\n console.warn('[Comments] Failed to list organization members for the mention roster', {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return members;\n }\n\n async #seenRoster(orgId: string, factoryProjectId: string, members: Map<string, FactoryRosterMember>): Promise<void> {\n for (const author of await this.#comments.listRecentAuthors({ orgId, factoryProjectId })) {\n if (author.kind !== 'user' || !isMentionableActorId(author.id)) continue;\n members.set(author.id, {\n id: author.id,\n ...(author.displayName ? { name: author.displayName } : {}),\n ...(author.avatarUrl ? { avatarUrl: author.avatarUrl } : {}),\n });\n }\n if (!this.#channelIdentity) return;\n await this.#channelIdentity.ensureReady();\n for (const link of await this.#channelIdentity.listAccountLinksForOrg(orgId)) {\n if (members.has(link.userId)) continue;\n members.set(link.userId, {\n id: link.userId,\n ...(link.externalUserName ? { name: link.externalUserName } : {}),\n });\n }\n }\n\n routes(): ApiRoute[] {\n return buildCommentRoutes({\n domain: this,\n auth: this.#auth,\n comments: this.#comments,\n workItems: this.#workItems,\n projects: this.#projects,\n pubsub: this.#pubsub,\n ...(this.#audit ? { audit: this.#audit } : {}),\n });\n }\n}\n\nexport { toWireComment } from './wire.js';\nexport type { WireComment, WireCommentPage } from './wire.js';\n"],"mappings":"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"}
@@ -2,6 +2,7 @@
2
2
  import type { Context } from 'hono';
3
3
  import type { FactoryMentionRef } from './base.js';
4
4
  export declare const UUID_RE: RegExp;
5
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
5
6
  export declare function readJson(c: Context): Promise<unknown>;
6
7
  export interface ParsedCreateComment {
7
8
  body: string;
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/parse.ts"],"names":[],"mappings":"AAAA,iGAAiG;AAEjG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGnD,eAAO,MAAM,OAAO,QAAoE,CAAC;AAOzF,wBAAsB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAM3D;AAyCD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAChC;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAkB/E;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,GAAG,IAAI,CAe3E"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/parse.ts"],"names":[],"mappings":"AAAA,iGAAiG;AAEjG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGnD,eAAO,MAAM,OAAO,QAAoE,CAAC;AAGzF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAM3D;AAyCD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAChC;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAkB/E;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,GAAG,IAAI,CAe3E"}
@@ -1,5 +1,5 @@
1
- import { commentBodyError } from "./base.js";
2
1
  import { isMentionableActorId } from "./actor.js";
2
+ import { commentBodyError } from "./base.js";
3
3
  //#region src/storage/domains/comments/parse.ts
4
4
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
5
  const CLIENT_TOKEN_RE = /^[A-Za-z0-9-]{8,64}$/;
@@ -85,6 +85,6 @@ function parseEditCommentBody(raw) {
85
85
  };
86
86
  }
87
87
  //#endregion
88
- export { UUID_RE, parseCreateCommentBody, parseEditCommentBody, readJson };
88
+ export { UUID_RE, isRecord, parseCreateCommentBody, parseEditCommentBody, readJson };
89
89
 
90
90
  //# sourceMappingURL=parse.js.map