@mastra/factory 0.25.0-alpha.0 → 0.25.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,6 +50,22 @@ async function hasFinalReport(notifications, { senderSessionId, recipientThreadI
50
50
  }
51
51
  return false;
52
52
  }
53
+ /** The newest report `senderSessionId` put on the recipient thread. */
54
+ async function lastReport(notifications, { senderSessionId, recipientThreadId }) {
55
+ return (await notifications.listNotifications({
56
+ threadId: recipientThreadId,
57
+ source: FACTORY_SESSION_NOTIFICATION_SOURCE,
58
+ limit: FINAL_REPORT_SCAN
59
+ })).filter((record) => record.attributes?.["from-session"] === senderSessionId).reduce((newest, record) => !newest || reportedAt(record) > reportedAt(newest) ? record : newest, void 0);
60
+ }
61
+ /**
62
+ * When a report last said something. `updatedAt` also moves when a report is
63
+ * read, so it only counts once a later report has coalesced into this one;
64
+ * otherwise the report says what it said when it was created.
65
+ */
66
+ function reportedAt(record) {
67
+ return (record.coalescedCount ?? 1) > 1 ? record.updatedAt : record.createdAt;
68
+ }
53
69
  /**
54
70
  * Whether the newest report `senderSessionId` put on the recipient thread is
55
71
  * already this exact `failed` backstop. A session stuck failing the same way
@@ -57,13 +73,22 @@ async function hasFinalReport(notifications, { senderSessionId, recipientThreadI
57
73
  * once, not once per attempt; any other report in between resets it.
58
74
  */
59
75
  async function repeatsLastFailure(notifications, { senderSessionId, recipientThreadId, summary }) {
60
- const last = (await notifications.listNotifications({
61
- threadId: recipientThreadId,
62
- source: FACTORY_SESSION_NOTIFICATION_SOURCE,
63
- limit: FINAL_REPORT_SCAN
64
- })).filter((record) => record.attributes?.["from-session"] === senderSessionId).reduce((newest, record) => !newest || record.createdAt > newest.createdAt ? record : newest, void 0);
76
+ const last = await lastReport(notifications, {
77
+ senderSessionId,
78
+ recipientThreadId
79
+ });
65
80
  return last?.kind === "failed" && last.attributes?.backstop === "true" && last.summary === summary;
66
81
  }
82
+ /**
83
+ * Whether the newest report `senderSessionId` put on the recipient thread
84
+ * already says it is done. A session woken again after it finished (a PR it
85
+ * subscribed to gets comments, reviews and CI results) ends each of those runs
86
+ * with nothing new to say, so the follower hears "finished" once, not per wake.
87
+ */
88
+ async function alreadyReportedDone(notifications, target) {
89
+ const last = await lastReport(notifications, target);
90
+ return last?.kind === "done" || last?.kind === "reply" && last.attributes?.status === "done";
91
+ }
67
92
  function reportNotification({ sender, status, message, attributes = {} }) {
68
93
  const form = FACTORY_REPORT_FORMS[status ?? "report"];
69
94
  return {
@@ -158,6 +183,10 @@ async function sendRunEndReport(reporting, { sender, recipients, since, outcome,
158
183
  waiting = true;
159
184
  continue;
160
185
  }
186
+ if (outcome.status === "done" && await alreadyReportedDone(reporting.notifications, {
187
+ senderSessionId: sender.sessionId,
188
+ recipientThreadId: recipient.threadId
189
+ })) continue;
161
190
  if (outcome.status === "failed" && await repeatsLastFailure(reporting.notifications, {
162
191
  senderSessionId: sender.sessionId,
163
192
  recipientThreadId: recipient.threadId,
@@ -1 +1 @@
1
- {"version":3,"file":"reports.js","names":[],"sources":["../../src/tools/reports.ts"],"sourcesContent":["/**\n * Reports from sessions doing the work back to whoever follows it: a side\n * session's origin, or every subscriber of a work item. The agent reports with\n * `factory_message_origin`; when a run ends without one, the run-end backstop\n * sends it, so each run produces exactly one final report per recipient.\n */\n\nimport type {\n NotificationPriority,\n NotificationsStorage,\n SendNotificationSignalInput,\n} from '@mastra/core/notifications';\n\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { FACTORY_SESSION_NOTIFICATION_SOURCE } from './deliver.js';\nimport type { FactoryDeliveryResult, FactoryDeliveryTarget } from './deliver.js';\nimport { workItemNumber } from './read-tools.js';\n\nexport type FactoryReportStatus = 'progress' | 'blocked' | 'done' | 'failed';\n\n/** How each report status travels. Final reports never coalesce, so a later report can't overwrite them. */\nexport const FACTORY_REPORT_FORMS: Record<\n FactoryReportStatus | 'report',\n { kind: string; priority: NotificationPriority; coalesce: boolean }\n> = {\n failed: { kind: 'failed', priority: 'urgent', coalesce: false },\n blocked: { kind: 'blocked', priority: 'urgent', coalesce: false },\n done: { kind: 'done', priority: 'high', coalesce: false },\n progress: { kind: 'progress', priority: 'low', coalesce: true },\n // Not `message`: only messages can be answered, so a report can't start a reply loop.\n report: { kind: 'report', priority: 'high', coalesce: false },\n};\n\n/** How many recent `factory-session` notifications on a recipient thread the final-report check reads. */\nconst FINAL_REPORT_SCAN = 100;\n\nexport interface FactoryReportSender {\n sessionId: string;\n threadId: string;\n userId: string;\n}\n\nexport type FactoryReportDeliver = (\n target: FactoryDeliveryTarget,\n notification: Omit<SendNotificationSignalInput, 'source'>,\n options?: { onlyIfLive?: boolean },\n) => Promise<FactoryDeliveryResult>;\n\nexport interface FactoryReporting {\n deliver: FactoryReportDeliver;\n notifications: Pick<NotificationsStorage, 'listNotifications'>;\n}\n\n/**\n * Whether `senderSessionId` already put a report of one of `kinds` on the\n * recipient thread since `since`. Reads the durable notifications store, so it\n * holds across restarts. `listNotifications` has no time filter: records come\n * newest-updated first, and the scan stops at the first one last updated\n * before `since` (nothing after it can have been created later).\n */\nexport async function hasFinalReport(\n notifications: FactoryReporting['notifications'],\n {\n senderSessionId,\n recipientThreadId,\n since,\n kinds,\n agentOnly = false,\n }: {\n senderSessionId: string;\n recipientThreadId: string;\n since: Date;\n kinds: readonly string[];\n /** Ignore reports Factory sent on the session's behalf (the backstop). */\n agentOnly?: boolean;\n },\n): Promise<boolean> {\n const records = await notifications.listNotifications({\n threadId: recipientThreadId,\n source: FACTORY_SESSION_NOTIFICATION_SOURCE,\n limit: FINAL_REPORT_SCAN,\n });\n for (const record of records) {\n if (record.updatedAt < since) break;\n if (\n record.attributes?.['from-session'] === senderSessionId &&\n !(agentOnly && record.attributes?.backstop === 'true') &&\n // A reply carrying a final status counts like the report itself.\n (kinds.includes(record.kind) ||\n (record.kind === 'reply' &&\n typeof record.attributes?.status === 'string' &&\n kinds.includes(record.attributes.status))) &&\n record.createdAt >= since\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Whether the newest report `senderSessionId` put on the recipient thread is\n * already this exact `failed` backstop. A session stuck failing the same way\n * (a run that dies before the model, restarted over and over) then reports\n * once, not once per attempt; any other report in between resets it.\n */\nexport async function repeatsLastFailure(\n notifications: FactoryReporting['notifications'],\n {\n senderSessionId,\n recipientThreadId,\n summary,\n }: { senderSessionId: string; recipientThreadId: string; summary: string },\n): Promise<boolean> {\n const records = await notifications.listNotifications({\n threadId: recipientThreadId,\n source: FACTORY_SESSION_NOTIFICATION_SOURCE,\n limit: FINAL_REPORT_SCAN,\n });\n // Storage orders by `updatedAt`, which moves when a report is read; the\n // newest report is the one created last.\n const last = records\n .filter(record => record.attributes?.['from-session'] === senderSessionId)\n .reduce<(typeof records)[number] | undefined>(\n (newest, record) => (!newest || record.createdAt > newest.createdAt ? record : newest),\n undefined,\n );\n return last?.kind === 'failed' && last.attributes?.backstop === 'true' && last.summary === summary;\n}\n\nexport function reportNotification({\n sender,\n status,\n message,\n attributes = {},\n}: {\n sender: FactoryReportSender;\n status?: FactoryReportStatus;\n message: string;\n attributes?: Record<string, string>;\n}): Omit<SendNotificationSignalInput, 'source'> {\n const form = FACTORY_REPORT_FORMS[status ?? 'report'];\n return {\n kind: form.kind,\n priority: form.priority,\n // The whole text: a notification's summary is all the recipient's agent ever sees, even\n // through notification_inbox (which hides payloads), so a cut-off report loses its verdict.\n summary: message,\n payload: { message },\n sourceId: sender.sessionId,\n ...(form.coalesce ? { coalesceKey: `factory-session:${sender.sessionId}:${form.kind}` } : {}),\n attributes: {\n ...attributes,\n ...(status ? { status } : {}),\n // Last, so nothing above can claim another sender.\n 'from-session': sender.sessionId,\n 'from-thread': sender.threadId,\n 'from-user': sender.userId,\n },\n };\n}\n\n/** Everyone following a work item, except the session doing the reporting. */\nexport async function workItemReportRecipients(\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers'>,\n { orgId, workItemId, senderSessionId }: { orgId: string; workItemId: string; senderSessionId: string },\n): Promise<FactoryDeliveryTarget[]> {\n const subscribers = await workItems.listWorkItemSubscribers({ orgId, workItemId });\n return subscribers\n .filter(subscriber => subscriber.sessionId !== senderSessionId)\n .map(subscriber => ({ sessionId: subscriber.sessionId, threadId: subscriber.threadId }));\n}\n\n/** How reports name an item: `#n`, else its quoted title, else its id. */\nasync function workItemReference(\n workItems: Pick<WorkItemsStorage, 'getForProject'>,\n { orgId, factoryProjectId, workItemId }: { orgId: string; factoryProjectId: string; workItemId: string },\n): Promise<{ label: string; ref: string }> {\n const item = await workItems.getForProject(orgId, factoryProjectId, workItemId);\n const number = item ? workItemNumber(item) : null;\n if (number !== null) return { label: `#${number}`, ref: `#${number}` };\n return { label: item ? `\"${item.title}\"` : workItemId, ref: workItemId };\n}\n\n/** Attributes that say which item and role a worker's report is about. */\nexport async function workItemReportAttributes(\n workItems: Pick<WorkItemsStorage, 'getForProject'>,\n {\n orgId,\n factoryProjectId,\n workItemId,\n role,\n }: { orgId: string; factoryProjectId: string; workItemId: string; role: string },\n): Promise<{ label: string; attributes: Record<string, string> }> {\n const { label, ref } = await workItemReference(workItems, { orgId, factoryProjectId, workItemId });\n return { label, attributes: { 'work-item': ref, role } };\n}\n\nexport type FactoryRunOutcome =\n | { status: 'done' }\n | { status: 'failed'; reason: string; failureCode?: string }\n | { status: 'blocked'; toolName?: string; approval?: boolean };\n\n/** `waiting`: the session is waiting for an answer, so a later run owes the final report. */\nexport interface FactoryRunEndResult {\n waiting: boolean;\n deliveries?: FactoryDeliveryResult[];\n}\n\n/**\n * The run-end backstop: send `outcome` to each recipient that hasn't had a\n * final report from this sender since the run started. An agent's own `done`\n * or `failed` report wins. A run that completes after the agent's own\n * `blocked` sends nothing: the agent ended its turn to wait for an answer, and\n * its `done` comes from a later run. A `failed` backstop still goes out after an\n * earlier `blocked`.\n */\nexport async function sendRunEndReport(\n reporting: FactoryReporting,\n {\n sender,\n recipients,\n since,\n outcome,\n subject,\n attributes = {},\n }: {\n sender: FactoryReportSender;\n recipients: FactoryDeliveryTarget[];\n since: Date;\n outcome: FactoryRunOutcome;\n /** Who finished, e.g. \"The work session for #12\". */\n subject: string;\n attributes?: Record<string, string>;\n },\n): Promise<FactoryRunEndResult> {\n const kinds = outcome.status === 'blocked' ? ['blocked'] : ['done', 'failed'];\n const message =\n outcome.status === 'done'\n ? `${subject} finished.`\n : outcome.status === 'failed'\n ? `${subject} failed${outcome.failureCode ? ` (${outcome.failureCode})` : ''}: ${outcome.reason}`\n : outcome.approval\n ? `${subject} is waiting for someone to approve ${outcome.toolName ?? 'a tool call'} in that session.`\n : `${subject} is waiting${outcome.toolName ? ` on ${outcome.toolName}` : ''} and needs an answer.`;\n const notification = reportNotification({\n sender,\n status: outcome.status,\n message,\n attributes: {\n ...attributes,\n backstop: 'true',\n ...(outcome.status === 'failed' && outcome.failureCode ? { 'failure-code': outcome.failureCode } : {}),\n ...(outcome.status === 'blocked' && outcome.toolName ? { 'parked-tool': outcome.toolName } : {}),\n ...(outcome.status === 'blocked' && outcome.approval ? { 'waiting-for': 'approval' } : {}),\n },\n });\n const report = (recipient: FactoryDeliveryTarget, reportKinds: string[], agentOnly = false) =>\n hasFinalReport(reporting.notifications, {\n senderSessionId: sender.sessionId,\n recipientThreadId: recipient.threadId,\n since,\n kinds: reportKinds,\n agentOnly,\n });\n let waiting = outcome.status === 'blocked';\n const deliveries: FactoryDeliveryResult[] = [];\n for (const recipient of recipients) {\n // Only the agent's own reports count: an earlier run's backstop lands after this\n // run's window opened. A reply already told that follower how a finished run went.\n if (await report(recipient, outcome.status === 'done' ? [...kinds, 'reply'] : kinds, true)) continue;\n // Only the agent's own `blocked` means it ended its turn to wait; a backstop `blocked` from a parked run doesn't.\n if (outcome.status === 'done' && (await report(recipient, ['blocked'], true))) {\n waiting = true;\n continue;\n }\n if (\n outcome.status === 'failed' &&\n (await repeatsLastFailure(reporting.notifications, {\n senderSessionId: sender.sessionId,\n recipientThreadId: recipient.threadId,\n summary: message,\n }))\n ) {\n continue;\n }\n deliveries.push(await reporting.deliver(recipient, notification));\n }\n return { waiting, deliveries };\n}\n\nexport interface FactoryRunEndInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n role: string;\n sender: FactoryReportSender;\n since: Date;\n outcome: FactoryRunOutcome;\n}\n\nexport type FactoryRunEndReporter = (input: FactoryRunEndInput) => Promise<FactoryRunEndResult>;\n\n/** The backstop for a work-item run: reports to the item's subscribers. Failures are logged, never thrown. */\nexport function createWorkItemRunEndReporter({\n workItems,\n reporting,\n}: {\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers' | 'getForProject'>;\n /** Resolved per report: the controller exists only once the mount is up. */\n reporting: () => Promise<FactoryReporting | null>;\n}): FactoryRunEndReporter {\n return async input => {\n try {\n const resolved = await reporting();\n if (!resolved) return { waiting: false };\n const recipients = await workItemReportRecipients(workItems, {\n orgId: input.orgId,\n workItemId: input.workItemId,\n senderSessionId: input.sender.sessionId,\n });\n if (recipients.length === 0) return { waiting: false };\n const { label, attributes } = await workItemReportAttributes(workItems, input);\n return await sendRunEndReport(resolved, {\n sender: input.sender,\n recipients,\n since: input.since,\n outcome: input.outcome,\n subject: `The ${input.role} session for ${label}`,\n attributes,\n });\n } catch (error) {\n console.warn('[Factory reports] Unable to send the run-end report.', error);\n return { waiting: false };\n }\n };\n}\n\nexport interface FactoryStageChange {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n fromStage: string | undefined;\n toStage: string;\n}\n\n/**\n * Tells a work item's subscribers it moved, at `low` priority. Only sessions\n * already live hear it: a stage change never brings a session online; an\n * offline subscriber sees the stage in its state signal on its next turn.\n */\nexport function createStageChangeNotifier({\n workItems,\n reporting,\n}: {\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers' | 'getForProject'>;\n reporting: () => Promise<FactoryReporting | null>;\n}): (change: FactoryStageChange) => Promise<void> {\n return async change => {\n const subscribers = await workItems.listWorkItemSubscribers({\n orgId: change.orgId,\n workItemId: change.workItemId,\n });\n if (subscribers.length === 0) return;\n const resolved = await reporting();\n if (!resolved) return;\n const { label, ref } = await workItemReference(workItems, change);\n const summary = change.fromStage\n ? `${label} moved from ${change.fromStage} to ${change.toStage}.`\n : `${label} moved to ${change.toStage}.`;\n const notification: Omit<SendNotificationSignalInput, 'source'> = {\n kind: 'stage',\n priority: 'low',\n summary,\n payload: { message: summary },\n sourceId: change.workItemId,\n coalesceKey: `factory-stage:${change.workItemId}`,\n attributes: {\n 'work-item': ref,\n ...(change.fromStage ? { 'from-stage': change.fromStage } : {}),\n 'to-stage': change.toStage,\n },\n };\n for (const subscriber of subscribers) {\n try {\n await resolved.deliver({ sessionId: subscriber.sessionId, threadId: subscriber.threadId }, notification, {\n onlyIfLive: true,\n });\n } catch (error) {\n console.warn(`[Factory reports] Unable to tell ${subscriber.sessionId} that ${label} moved.`, error);\n }\n }\n };\n}\n\n/** Reports one run end of a session; the thread is the one the run used. */\nexport type FactorySessionRunEndReporter = (input: {\n sessionId: string;\n threadId: string | null;\n outcome: FactoryRunOutcome;\n since: Date;\n}) => Promise<void>;\n\n/** A tool call a session's run is holding until someone approves or declines it. */\nexport interface FactoryApprovalRequest {\n sessionId: string;\n threadId: string | null;\n toolName: string;\n toolCallId: string;\n args: unknown;\n}\n\nexport type FactoryApprovalNotifier = (request: FactoryApprovalRequest) => Promise<void>;\n\ninterface SessionReportDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<\n WorkItemsStorage,\n 'findActiveRunBindingForSession' | 'findRunBindingBySession' | 'listWorkItemSubscribers' | 'getForProject'\n >;\n reporting: () => Promise<FactoryReporting | null>;\n}\n\n/** How much of a gated call's arguments an approval notice quotes. */\nconst APPROVAL_ARGS_LENGTH = 3000;\n\n/**\n * Who hears from a session: a work session (one with an active run binding)\n * reports to its item's followers, a side session to the session that started\n * it. Any other session (a person's own) has nobody to tell.\n */\nasync function sessionAudience(\n { sessions, workItems }: Omit<SessionReportDependencies, 'reporting'>,\n sessionId: string,\n threadId: string | null,\n /** When the run started: a binding revoked since then (the run moved its card to a finished stage) still counts. */\n since?: Date,\n): Promise<\n | {\n kind: 'work-item';\n binding: NonNullable<Awaited<ReturnType<WorkItemsStorage['findActiveRunBindingForSession']>>>;\n sender: FactoryReportSender;\n }\n | {\n kind: 'origin';\n sender: FactoryReportSender;\n recipient: FactoryDeliveryTarget;\n subject: string;\n /** Set while the origin has muted this side session's reports (approval notices still go through). */\n mutedAt: Date | null;\n }\n | null\n> {\n const row = await sessions.getBySessionId(sessionId);\n if (!row?.factoryProjectId) return null;\n let binding = await workItems.findActiveRunBindingForSession({\n orgId: row.orgId,\n factoryProjectId: row.factoryProjectId,\n sessionId: row.sessionId,\n });\n if (!binding && since && threadId) {\n // The binding records the memory resource the run used: the Factory's, its\n // own id for a run kept on its own resource, or its owner's for a person's\n // session the run started in.\n for (const resourceId of new Set([row.factoryProjectId, row.sessionId, row.userId])) {\n const latest = await workItems.findRunBindingBySession({\n factoryProjectId: row.factoryProjectId,\n threadId,\n resourceId,\n sessionId: row.sessionId,\n });\n if (latest?.orgId === row.orgId && latest.revokedAt && latest.revokedAt >= since) {\n binding = latest;\n break;\n }\n }\n }\n if (binding) {\n return {\n kind: 'work-item',\n binding,\n sender: { sessionId: row.sessionId, threadId: binding.threadId, userId: row.userId },\n };\n }\n if (!row.originSessionId || !row.originThreadId || !threadId) return null;\n return {\n kind: 'origin',\n sender: { sessionId: row.sessionId, threadId, userId: row.userId },\n recipient: { sessionId: row.originSessionId, threadId: row.originThreadId },\n subject: `The side session \"${row.title ?? row.sessionId}\"`,\n mutedAt: row.originMutedAt ?? null,\n };\n}\n\n/** Reports each run end of a work or side session to whoever follows it. */\nexport function createSessionRunEndReporter(deps: SessionReportDependencies): FactorySessionRunEndReporter {\n const workItemRunEnd = createWorkItemRunEndReporter(deps);\n return async ({ sessionId, threadId, outcome, since }) => {\n const audience = await sessionAudience(deps, sessionId, threadId, since);\n if (!audience) return;\n if (audience.kind === 'work-item') {\n const { binding, sender } = audience;\n await workItemRunEnd({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n role: binding.role,\n sender,\n since,\n outcome,\n });\n return;\n }\n // A muted origin still hears when the session is blocked: nothing else would tell it the session is waiting.\n if (audience.mutedAt && outcome.status !== 'blocked') return;\n const resolved = await deps.reporting();\n if (!resolved) return;\n await sendRunEndReport(resolved, {\n sender: audience.sender,\n recipients: [audience.recipient],\n since,\n outcome,\n subject: audience.subject,\n });\n };\n}\n\n/**\n * Tells whoever follows a session, urgently, the moment its run holds a tool\n * call for approval. The run stays open on the gate, so no run end would say\n * so; the notice names the session and call so a follower can answer it with\n * `factory_answer_approval`.\n */\nexport function createSessionApprovalNotifier(deps: SessionReportDependencies): FactoryApprovalNotifier {\n return async ({ sessionId, threadId, toolName, toolCallId, args }) => {\n const audience = await sessionAudience(deps, sessionId, threadId);\n if (!audience) return;\n const resolved = await deps.reporting();\n if (!resolved) return;\n let subject: string;\n let recipients: FactoryDeliveryTarget[];\n let attributes: Record<string, string> = {};\n if (audience.kind === 'work-item') {\n const { binding } = audience;\n recipients = await workItemReportRecipients(deps.workItems, {\n orgId: binding.orgId,\n workItemId: binding.workItemId,\n senderSessionId: audience.sender.sessionId,\n });\n const named = await workItemReportAttributes(deps.workItems, binding);\n subject = `The ${binding.role} session for ${named.label}`;\n attributes = named.attributes;\n } else {\n recipients = [audience.recipient];\n subject = audience.subject;\n }\n if (recipients.length === 0) return;\n const argsText = JSON.stringify(args ?? {}) ?? '{}';\n const quoted = argsText.length > APPROVAL_ARGS_LENGTH ? `${argsText.slice(0, APPROVAL_ARGS_LENGTH)}…` : argsText;\n const message = [\n `${subject} is waiting for approval to run ${toolName} with ${quoted}.`,\n `Answer it with factory_answer_approval (sessionId \"${audience.sender.sessionId}\", toolCallId \"${toolCallId}\"), or open that session to approve or decline it there.`,\n ].join(' ');\n const notification = {\n ...reportNotification({\n sender: audience.sender,\n message,\n attributes: { ...attributes, 'waiting-for': 'approval', 'tool-name': toolName, 'tool-call-id': toolCallId },\n }),\n kind: 'approval',\n priority: 'urgent' as const,\n // One notice per held call, even if the gate is announced again.\n dedupeKey: `factory-approval:${toolCallId}`,\n };\n for (const recipient of recipients) await resolved.deliver(recipient, notification);\n };\n}\n"],"mappings":";;;;AAsBA,MAAa,uBAGT;CACF,QAAQ;EAAE,MAAM;EAAU,UAAU;EAAU,UAAU;CAAM;CAC9D,SAAS;EAAE,MAAM;EAAW,UAAU;EAAU,UAAU;CAAM;CAChE,MAAM;EAAE,MAAM;EAAQ,UAAU;EAAQ,UAAU;CAAM;CACxD,UAAU;EAAE,MAAM;EAAY,UAAU;EAAO,UAAU;CAAK;CAE9D,QAAQ;EAAE,MAAM;EAAU,UAAU;EAAQ,UAAU;CAAM;AAC9D;;AAGA,MAAM,oBAAoB;;;;;;;;AA0B1B,eAAsB,eACpB,eACA,EACE,iBACA,mBACA,OACA,OACA,YAAY,SASI;CAClB,MAAM,UAAU,MAAM,cAAc,kBAAkB;EACpD,UAAU;EACV,QAAQ;EACR,OAAO;CACT,CAAC;CACD,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,OAAO,YAAY,OAAO;EAC9B,IACE,OAAO,aAAa,oBAAoB,mBACxC,EAAE,aAAa,OAAO,YAAY,aAAa,YAE9C,MAAM,SAAS,OAAO,IAAI,KACxB,OAAO,SAAS,WACf,OAAO,OAAO,YAAY,WAAW,YACrC,MAAM,SAAS,OAAO,WAAW,MAAM,MAC3C,OAAO,aAAa,OAEpB,OAAO;CAEX;CACA,OAAO;AACT;;;;;;;AAQA,eAAsB,mBACpB,eACA,EACE,iBACA,mBACA,WAEgB;CAQlB,MAAM,QAAO,MAPS,cAAc,kBAAkB;EACpD,UAAU;EACV,QAAQ;EACR,OAAO;CACT,CAAC,EAAA,CAIE,QAAO,WAAU,OAAO,aAAa,oBAAoB,eAAe,CAAC,CACzE,QACE,QAAQ,WAAY,CAAC,UAAU,OAAO,YAAY,OAAO,YAAY,SAAS,QAC/E,KAAA,CACF;CACF,OAAO,MAAM,SAAS,YAAY,KAAK,YAAY,aAAa,UAAU,KAAK,YAAY;AAC7F;AAEA,SAAgB,mBAAmB,EACjC,QACA,QACA,SACA,aAAa,CAAC,KAMgC;CAC9C,MAAM,OAAO,qBAAqB,UAAU;CAC5C,OAAO;EACL,MAAM,KAAK;EACX,UAAU,KAAK;EAGf,SAAS;EACT,SAAS,EAAE,QAAQ;EACnB,UAAU,OAAO;EACjB,GAAI,KAAK,WAAW,EAAE,aAAa,mBAAmB,OAAO,UAAU,GAAG,KAAK,OAAO,IAAI,CAAC;EAC3F,YAAY;GACV,GAAG;GACH,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAE3B,gBAAgB,OAAO;GACvB,eAAe,OAAO;GACtB,aAAa,OAAO;EACtB;CACF;AACF;;AAGA,eAAsB,yBACpB,WACA,EAAE,OAAO,YAAY,mBACa;CAElC,QAAO,MADmB,UAAU,wBAAwB;EAAE;EAAO;CAAW,CAAC,EAAA,CAE9E,QAAO,eAAc,WAAW,cAAc,eAAe,CAAC,CAC9D,KAAI,gBAAe;EAAE,WAAW,WAAW;EAAW,UAAU,WAAW;CAAS,EAAE;AAC3F;;AAGA,eAAe,kBACb,WACA,EAAE,OAAO,kBAAkB,cACc;CACzC,MAAM,OAAO,MAAM,UAAU,cAAc,OAAO,kBAAkB,UAAU;CAC9E,MAAM,SAAS,OAAO,eAAe,IAAI,IAAI;CAC7C,IAAI,WAAW,MAAM,OAAO;EAAE,OAAO,IAAI;EAAU,KAAK,IAAI;CAAS;CACrE,OAAO;EAAE,OAAO,OAAO,IAAI,KAAK,MAAM,KAAK;EAAY,KAAK;CAAW;AACzE;;AAGA,eAAsB,yBACpB,WACA,EACE,OACA,kBACA,YACA,QAE8D;CAChE,MAAM,EAAE,OAAO,QAAQ,MAAM,kBAAkB,WAAW;EAAE;EAAO;EAAkB;CAAW,CAAC;CACjG,OAAO;EAAE;EAAO,YAAY;GAAE,aAAa;GAAK;EAAK;CAAE;AACzD;;;;;;;;;AAqBA,eAAsB,iBACpB,WACA,EACE,QACA,YACA,OACA,SACA,SACA,aAAa,CAAC,KAUc;CAC9B,MAAM,QAAQ,QAAQ,WAAW,YAAY,CAAC,SAAS,IAAI,CAAC,QAAQ,QAAQ;CAC5E,MAAM,UACJ,QAAQ,WAAW,SACf,GAAG,QAAQ,cACX,QAAQ,WAAW,WACjB,GAAG,QAAQ,SAAS,QAAQ,cAAc,KAAK,QAAQ,YAAY,KAAK,GAAG,IAAI,QAAQ,WACvF,QAAQ,WACN,GAAG,QAAQ,qCAAqC,QAAQ,YAAY,cAAc,qBAClF,GAAG,QAAQ,aAAa,QAAQ,WAAW,OAAO,QAAQ,aAAa,GAAG;CACpF,MAAM,eAAe,mBAAmB;EACtC;EACA,QAAQ,QAAQ;EAChB;EACA,YAAY;GACV,GAAG;GACH,UAAU;GACV,GAAI,QAAQ,WAAW,YAAY,QAAQ,cAAc,EAAE,gBAAgB,QAAQ,YAAY,IAAI,CAAC;GACpG,GAAI,QAAQ,WAAW,aAAa,QAAQ,WAAW,EAAE,eAAe,QAAQ,SAAS,IAAI,CAAC;GAC9F,GAAI,QAAQ,WAAW,aAAa,QAAQ,WAAW,EAAE,eAAe,WAAW,IAAI,CAAC;EAC1F;CACF,CAAC;CACD,MAAM,UAAU,WAAkC,aAAuB,YAAY,UACnF,eAAe,UAAU,eAAe;EACtC,iBAAiB,OAAO;EACxB,mBAAmB,UAAU;EAC7B;EACA,OAAO;EACP;CACF,CAAC;CACH,IAAI,UAAU,QAAQ,WAAW;CACjC,MAAM,aAAsC,CAAC;CAC7C,KAAK,MAAM,aAAa,YAAY;EAGlC,IAAI,MAAM,OAAO,WAAW,QAAQ,WAAW,SAAS,CAAC,GAAG,OAAO,OAAO,IAAI,OAAO,IAAI,GAAG;EAE5F,IAAI,QAAQ,WAAW,UAAW,MAAM,OAAO,WAAW,CAAC,SAAS,GAAG,IAAI,GAAI;GAC7E,UAAU;GACV;EACF;EACA,IACE,QAAQ,WAAW,YAClB,MAAM,mBAAmB,UAAU,eAAe;GACjD,iBAAiB,OAAO;GACxB,mBAAmB,UAAU;GAC7B,SAAS;EACX,CAAC,GAED;EAEF,WAAW,KAAK,MAAM,UAAU,QAAQ,WAAW,YAAY,CAAC;CAClE;CACA,OAAO;EAAE;EAAS;CAAW;AAC/B;;AAeA,SAAgB,6BAA6B,EAC3C,WACA,aAKwB;CACxB,OAAO,OAAM,UAAS;EACpB,IAAI;GACF,MAAM,WAAW,MAAM,UAAU;GACjC,IAAI,CAAC,UAAU,OAAO,EAAE,SAAS,MAAM;GACvC,MAAM,aAAa,MAAM,yBAAyB,WAAW;IAC3D,OAAO,MAAM;IACb,YAAY,MAAM;IAClB,iBAAiB,MAAM,OAAO;GAChC,CAAC;GACD,IAAI,WAAW,WAAW,GAAG,OAAO,EAAE,SAAS,MAAM;GACrD,MAAM,EAAE,OAAO,eAAe,MAAM,yBAAyB,WAAW,KAAK;GAC7E,OAAO,MAAM,iBAAiB,UAAU;IACtC,QAAQ,MAAM;IACd;IACA,OAAO,MAAM;IACb,SAAS,MAAM;IACf,SAAS,OAAO,MAAM,KAAK,eAAe;IAC1C;GACF,CAAC;EACH,SAAS,OAAO;GACd,QAAQ,KAAK,wDAAwD,KAAK;GAC1E,OAAO,EAAE,SAAS,MAAM;EAC1B;CACF;AACF;;;;;;AAeA,SAAgB,0BAA0B,EACxC,WACA,aAIgD;CAChD,OAAO,OAAM,WAAU;EACrB,MAAM,cAAc,MAAM,UAAU,wBAAwB;GAC1D,OAAO,OAAO;GACd,YAAY,OAAO;EACrB,CAAC;EACD,IAAI,YAAY,WAAW,GAAG;EAC9B,MAAM,WAAW,MAAM,UAAU;EACjC,IAAI,CAAC,UAAU;EACf,MAAM,EAAE,OAAO,QAAQ,MAAM,kBAAkB,WAAW,MAAM;EAChE,MAAM,UAAU,OAAO,YACnB,GAAG,MAAM,cAAc,OAAO,UAAU,MAAM,OAAO,QAAQ,KAC7D,GAAG,MAAM,YAAY,OAAO,QAAQ;EACxC,MAAM,eAA4D;GAChE,MAAM;GACN,UAAU;GACV;GACA,SAAS,EAAE,SAAS,QAAQ;GAC5B,UAAU,OAAO;GACjB,aAAa,iBAAiB,OAAO;GACrC,YAAY;IACV,aAAa;IACb,GAAI,OAAO,YAAY,EAAE,cAAc,OAAO,UAAU,IAAI,CAAC;IAC7D,YAAY,OAAO;GACrB;EACF;EACA,KAAK,MAAM,cAAc,aACvB,IAAI;GACF,MAAM,SAAS,QAAQ;IAAE,WAAW,WAAW;IAAW,UAAU,WAAW;GAAS,GAAG,cAAc,EACvG,YAAY,KACd,CAAC;EACH,SAAS,OAAO;GACd,QAAQ,KAAK,oCAAoC,WAAW,UAAU,QAAQ,MAAM,UAAU,KAAK;EACrG;CAEJ;AACF;;AA+BA,MAAM,uBAAuB;;;;;;AAO7B,eAAe,gBACb,EAAE,UAAU,aACZ,WACA,UAEA,OAgBA;CACA,MAAM,MAAM,MAAM,SAAS,eAAe,SAAS;CACnD,IAAI,CAAC,KAAK,kBAAkB,OAAO;CACnC,IAAI,UAAU,MAAM,UAAU,+BAA+B;EAC3D,OAAO,IAAI;EACX,kBAAkB,IAAI;EACtB,WAAW,IAAI;CACjB,CAAC;CACD,IAAI,CAAC,WAAW,SAAS,UAIvB,KAAK,MAAM,8BAAc,IAAI,IAAI;EAAC,IAAI;EAAkB,IAAI;EAAW,IAAI;CAAM,CAAC,GAAG;EACnF,MAAM,SAAS,MAAM,UAAU,wBAAwB;GACrD,kBAAkB,IAAI;GACtB;GACA;GACA,WAAW,IAAI;EACjB,CAAC;EACD,IAAI,QAAQ,UAAU,IAAI,SAAS,OAAO,aAAa,OAAO,aAAa,OAAO;GAChF,UAAU;GACV;EACF;CACF;CAEF,IAAI,SACF,OAAO;EACL,MAAM;EACN;EACA,QAAQ;GAAE,WAAW,IAAI;GAAW,UAAU,QAAQ;GAAU,QAAQ,IAAI;EAAO;CACrF;CAEF,IAAI,CAAC,IAAI,mBAAmB,CAAC,IAAI,kBAAkB,CAAC,UAAU,OAAO;CACrE,OAAO;EACL,MAAM;EACN,QAAQ;GAAE,WAAW,IAAI;GAAW;GAAU,QAAQ,IAAI;EAAO;EACjE,WAAW;GAAE,WAAW,IAAI;GAAiB,UAAU,IAAI;EAAe;EAC1E,SAAS,qBAAqB,IAAI,SAAS,IAAI,UAAU;EACzD,SAAS,IAAI,iBAAiB;CAChC;AACF;;AAGA,SAAgB,4BAA4B,MAA+D;CACzG,MAAM,iBAAiB,6BAA6B,IAAI;CACxD,OAAO,OAAO,EAAE,WAAW,UAAU,SAAS,YAAY;EACxD,MAAM,WAAW,MAAM,gBAAgB,MAAM,WAAW,UAAU,KAAK;EACvE,IAAI,CAAC,UAAU;EACf,IAAI,SAAS,SAAS,aAAa;GACjC,MAAM,EAAE,SAAS,WAAW;GAC5B,MAAM,eAAe;IACnB,OAAO,QAAQ;IACf,kBAAkB,QAAQ;IAC1B,YAAY,QAAQ;IACpB,MAAM,QAAQ;IACd;IACA;IACA;GACF,CAAC;GACD;EACF;EAEA,IAAI,SAAS,WAAW,QAAQ,WAAW,WAAW;EACtD,MAAM,WAAW,MAAM,KAAK,UAAU;EACtC,IAAI,CAAC,UAAU;EACf,MAAM,iBAAiB,UAAU;GAC/B,QAAQ,SAAS;GACjB,YAAY,CAAC,SAAS,SAAS;GAC/B;GACA;GACA,SAAS,SAAS;EACpB,CAAC;CACH;AACF;;;;;;;AAQA,SAAgB,8BAA8B,MAA0D;CACtG,OAAO,OAAO,EAAE,WAAW,UAAU,UAAU,YAAY,WAAW;EACpE,MAAM,WAAW,MAAM,gBAAgB,MAAM,WAAW,QAAQ;EAChE,IAAI,CAAC,UAAU;EACf,MAAM,WAAW,MAAM,KAAK,UAAU;EACtC,IAAI,CAAC,UAAU;EACf,IAAI;EACJ,IAAI;EACJ,IAAI,aAAqC,CAAC;EAC1C,IAAI,SAAS,SAAS,aAAa;GACjC,MAAM,EAAE,YAAY;GACpB,aAAa,MAAM,yBAAyB,KAAK,WAAW;IAC1D,OAAO,QAAQ;IACf,YAAY,QAAQ;IACpB,iBAAiB,SAAS,OAAO;GACnC,CAAC;GACD,MAAM,QAAQ,MAAM,yBAAyB,KAAK,WAAW,OAAO;GACpE,UAAU,OAAO,QAAQ,KAAK,eAAe,MAAM;GACnD,aAAa,MAAM;EACrB,OAAO;GACL,aAAa,CAAC,SAAS,SAAS;GAChC,UAAU,SAAS;EACrB;EACA,IAAI,WAAW,WAAW,GAAG;EAC7B,MAAM,WAAW,KAAK,UAAU,QAAQ,CAAC,CAAC,KAAK;EAC/C,MAAM,SAAS,SAAS,SAAS,uBAAuB,GAAG,SAAS,MAAM,GAAG,oBAAoB,EAAE,KAAK;EACxG,MAAM,UAAU,CACd,GAAG,QAAQ,kCAAkC,SAAS,QAAQ,OAAO,IACrE,sDAAsD,SAAS,OAAO,UAAU,iBAAiB,WAAW,yDAC9G,CAAC,CAAC,KAAK,GAAG;EACV,MAAM,eAAe;GACnB,GAAG,mBAAmB;IACpB,QAAQ,SAAS;IACjB;IACA,YAAY;KAAE,GAAG;KAAY,eAAe;KAAY,aAAa;KAAU,gBAAgB;IAAW;GAC5G,CAAC;GACD,MAAM;GACN,UAAU;GAEV,WAAW,oBAAoB;EACjC;EACA,KAAK,MAAM,aAAa,YAAY,MAAM,SAAS,QAAQ,WAAW,YAAY;CACpF;AACF"}
1
+ {"version":3,"file":"reports.js","names":[],"sources":["../../src/tools/reports.ts"],"sourcesContent":["/**\n * Reports from sessions doing the work back to whoever follows it: a side\n * session's origin, or every subscriber of a work item. The agent reports with\n * `factory_message_origin`; when a run ends without one, the run-end backstop\n * sends it, so each run produces exactly one final report per recipient.\n */\n\nimport type {\n NotificationPriority,\n NotificationsStorage,\n SendNotificationSignalInput,\n} from '@mastra/core/notifications';\n\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { FACTORY_SESSION_NOTIFICATION_SOURCE } from './deliver.js';\nimport type { FactoryDeliveryResult, FactoryDeliveryTarget } from './deliver.js';\nimport { workItemNumber } from './read-tools.js';\n\nexport type FactoryReportStatus = 'progress' | 'blocked' | 'done' | 'failed';\n\n/** How each report status travels. Final reports never coalesce, so a later report can't overwrite them. */\nexport const FACTORY_REPORT_FORMS: Record<\n FactoryReportStatus | 'report',\n { kind: string; priority: NotificationPriority; coalesce: boolean }\n> = {\n failed: { kind: 'failed', priority: 'urgent', coalesce: false },\n blocked: { kind: 'blocked', priority: 'urgent', coalesce: false },\n done: { kind: 'done', priority: 'high', coalesce: false },\n progress: { kind: 'progress', priority: 'low', coalesce: true },\n // Not `message`: only messages can be answered, so a report can't start a reply loop.\n report: { kind: 'report', priority: 'high', coalesce: false },\n};\n\n/** How many recent `factory-session` notifications on a recipient thread the final-report check reads. */\nconst FINAL_REPORT_SCAN = 100;\n\nexport interface FactoryReportSender {\n sessionId: string;\n threadId: string;\n userId: string;\n}\n\nexport type FactoryReportDeliver = (\n target: FactoryDeliveryTarget,\n notification: Omit<SendNotificationSignalInput, 'source'>,\n options?: { onlyIfLive?: boolean },\n) => Promise<FactoryDeliveryResult>;\n\nexport interface FactoryReporting {\n deliver: FactoryReportDeliver;\n notifications: Pick<NotificationsStorage, 'listNotifications'>;\n}\n\n/**\n * Whether `senderSessionId` already put a report of one of `kinds` on the\n * recipient thread since `since`. Reads the durable notifications store, so it\n * holds across restarts. `listNotifications` has no time filter: records come\n * newest-updated first, and the scan stops at the first one last updated\n * before `since` (nothing after it can have been created later).\n */\nexport async function hasFinalReport(\n notifications: FactoryReporting['notifications'],\n {\n senderSessionId,\n recipientThreadId,\n since,\n kinds,\n agentOnly = false,\n }: {\n senderSessionId: string;\n recipientThreadId: string;\n since: Date;\n kinds: readonly string[];\n /** Ignore reports Factory sent on the session's behalf (the backstop). */\n agentOnly?: boolean;\n },\n): Promise<boolean> {\n const records = await notifications.listNotifications({\n threadId: recipientThreadId,\n source: FACTORY_SESSION_NOTIFICATION_SOURCE,\n limit: FINAL_REPORT_SCAN,\n });\n for (const record of records) {\n if (record.updatedAt < since) break;\n if (\n record.attributes?.['from-session'] === senderSessionId &&\n !(agentOnly && record.attributes?.backstop === 'true') &&\n // A reply carrying a final status counts like the report itself.\n (kinds.includes(record.kind) ||\n (record.kind === 'reply' &&\n typeof record.attributes?.status === 'string' &&\n kinds.includes(record.attributes.status))) &&\n record.createdAt >= since\n ) {\n return true;\n }\n }\n return false;\n}\n\n/** The newest report `senderSessionId` put on the recipient thread. */\nasync function lastReport(\n notifications: FactoryReporting['notifications'],\n { senderSessionId, recipientThreadId }: { senderSessionId: string; recipientThreadId: string },\n) {\n const records = await notifications.listNotifications({\n threadId: recipientThreadId,\n source: FACTORY_SESSION_NOTIFICATION_SOURCE,\n limit: FINAL_REPORT_SCAN,\n });\n return records\n .filter(record => record.attributes?.['from-session'] === senderSessionId)\n .reduce<(typeof records)[number] | undefined>(\n (newest, record) => (!newest || reportedAt(record) > reportedAt(newest) ? record : newest),\n undefined,\n );\n}\n\n/**\n * When a report last said something. `updatedAt` also moves when a report is\n * read, so it only counts once a later report has coalesced into this one;\n * otherwise the report says what it said when it was created.\n */\nfunction reportedAt(record: { createdAt: Date; updatedAt: Date; coalescedCount?: number }): Date {\n return (record.coalescedCount ?? 1) > 1 ? record.updatedAt : record.createdAt;\n}\n\n/**\n * Whether the newest report `senderSessionId` put on the recipient thread is\n * already this exact `failed` backstop. A session stuck failing the same way\n * (a run that dies before the model, restarted over and over) then reports\n * once, not once per attempt; any other report in between resets it.\n */\nexport async function repeatsLastFailure(\n notifications: FactoryReporting['notifications'],\n {\n senderSessionId,\n recipientThreadId,\n summary,\n }: { senderSessionId: string; recipientThreadId: string; summary: string },\n): Promise<boolean> {\n const last = await lastReport(notifications, { senderSessionId, recipientThreadId });\n return last?.kind === 'failed' && last.attributes?.backstop === 'true' && last.summary === summary;\n}\n\n/**\n * Whether the newest report `senderSessionId` put on the recipient thread\n * already says it is done. A session woken again after it finished (a PR it\n * subscribed to gets comments, reviews and CI results) ends each of those runs\n * with nothing new to say, so the follower hears \"finished\" once, not per wake.\n */\nasync function alreadyReportedDone(\n notifications: FactoryReporting['notifications'],\n target: { senderSessionId: string; recipientThreadId: string },\n): Promise<boolean> {\n const last = await lastReport(notifications, target);\n return last?.kind === 'done' || (last?.kind === 'reply' && last.attributes?.status === 'done');\n}\n\nexport function reportNotification({\n sender,\n status,\n message,\n attributes = {},\n}: {\n sender: FactoryReportSender;\n status?: FactoryReportStatus;\n message: string;\n attributes?: Record<string, string>;\n}): Omit<SendNotificationSignalInput, 'source'> {\n const form = FACTORY_REPORT_FORMS[status ?? 'report'];\n return {\n kind: form.kind,\n priority: form.priority,\n // The whole text: a notification's summary is all the recipient's agent ever sees, even\n // through notification_inbox (which hides payloads), so a cut-off report loses its verdict.\n summary: message,\n payload: { message },\n sourceId: sender.sessionId,\n ...(form.coalesce ? { coalesceKey: `factory-session:${sender.sessionId}:${form.kind}` } : {}),\n attributes: {\n ...attributes,\n ...(status ? { status } : {}),\n // Last, so nothing above can claim another sender.\n 'from-session': sender.sessionId,\n 'from-thread': sender.threadId,\n 'from-user': sender.userId,\n },\n };\n}\n\n/** Everyone following a work item, except the session doing the reporting. */\nexport async function workItemReportRecipients(\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers'>,\n { orgId, workItemId, senderSessionId }: { orgId: string; workItemId: string; senderSessionId: string },\n): Promise<FactoryDeliveryTarget[]> {\n const subscribers = await workItems.listWorkItemSubscribers({ orgId, workItemId });\n return subscribers\n .filter(subscriber => subscriber.sessionId !== senderSessionId)\n .map(subscriber => ({ sessionId: subscriber.sessionId, threadId: subscriber.threadId }));\n}\n\n/** How reports name an item: `#n`, else its quoted title, else its id. */\nasync function workItemReference(\n workItems: Pick<WorkItemsStorage, 'getForProject'>,\n { orgId, factoryProjectId, workItemId }: { orgId: string; factoryProjectId: string; workItemId: string },\n): Promise<{ label: string; ref: string }> {\n const item = await workItems.getForProject(orgId, factoryProjectId, workItemId);\n const number = item ? workItemNumber(item) : null;\n if (number !== null) return { label: `#${number}`, ref: `#${number}` };\n return { label: item ? `\"${item.title}\"` : workItemId, ref: workItemId };\n}\n\n/** Attributes that say which item and role a worker's report is about. */\nexport async function workItemReportAttributes(\n workItems: Pick<WorkItemsStorage, 'getForProject'>,\n {\n orgId,\n factoryProjectId,\n workItemId,\n role,\n }: { orgId: string; factoryProjectId: string; workItemId: string; role: string },\n): Promise<{ label: string; attributes: Record<string, string> }> {\n const { label, ref } = await workItemReference(workItems, { orgId, factoryProjectId, workItemId });\n return { label, attributes: { 'work-item': ref, role } };\n}\n\nexport type FactoryRunOutcome =\n | { status: 'done' }\n | { status: 'failed'; reason: string; failureCode?: string }\n | { status: 'blocked'; toolName?: string; approval?: boolean };\n\n/** `waiting`: the session is waiting for an answer, so a later run owes the final report. */\nexport interface FactoryRunEndResult {\n waiting: boolean;\n deliveries?: FactoryDeliveryResult[];\n}\n\n/**\n * The run-end backstop: send `outcome` to each recipient that hasn't had a\n * final report from this sender since the run started. An agent's own `done`\n * or `failed` report wins. A run that completes after the agent's own\n * `blocked` sends nothing: the agent ended its turn to wait for an answer, and\n * its `done` comes from a later run. A `failed` backstop still goes out after an\n * earlier `blocked`.\n */\nexport async function sendRunEndReport(\n reporting: FactoryReporting,\n {\n sender,\n recipients,\n since,\n outcome,\n subject,\n attributes = {},\n }: {\n sender: FactoryReportSender;\n recipients: FactoryDeliveryTarget[];\n since: Date;\n outcome: FactoryRunOutcome;\n /** Who finished, e.g. \"The work session for #12\". */\n subject: string;\n attributes?: Record<string, string>;\n },\n): Promise<FactoryRunEndResult> {\n const kinds = outcome.status === 'blocked' ? ['blocked'] : ['done', 'failed'];\n const message =\n outcome.status === 'done'\n ? `${subject} finished.`\n : outcome.status === 'failed'\n ? `${subject} failed${outcome.failureCode ? ` (${outcome.failureCode})` : ''}: ${outcome.reason}`\n : outcome.approval\n ? `${subject} is waiting for someone to approve ${outcome.toolName ?? 'a tool call'} in that session.`\n : `${subject} is waiting${outcome.toolName ? ` on ${outcome.toolName}` : ''} and needs an answer.`;\n const notification = reportNotification({\n sender,\n status: outcome.status,\n message,\n attributes: {\n ...attributes,\n backstop: 'true',\n ...(outcome.status === 'failed' && outcome.failureCode ? { 'failure-code': outcome.failureCode } : {}),\n ...(outcome.status === 'blocked' && outcome.toolName ? { 'parked-tool': outcome.toolName } : {}),\n ...(outcome.status === 'blocked' && outcome.approval ? { 'waiting-for': 'approval' } : {}),\n },\n });\n const report = (recipient: FactoryDeliveryTarget, reportKinds: string[], agentOnly = false) =>\n hasFinalReport(reporting.notifications, {\n senderSessionId: sender.sessionId,\n recipientThreadId: recipient.threadId,\n since,\n kinds: reportKinds,\n agentOnly,\n });\n let waiting = outcome.status === 'blocked';\n const deliveries: FactoryDeliveryResult[] = [];\n for (const recipient of recipients) {\n // Only the agent's own reports count: an earlier run's backstop lands after this\n // run's window opened. A reply already told that follower how a finished run went.\n if (await report(recipient, outcome.status === 'done' ? [...kinds, 'reply'] : kinds, true)) continue;\n // Only the agent's own `blocked` means it ended its turn to wait; a backstop `blocked` from a parked run doesn't.\n if (outcome.status === 'done' && (await report(recipient, ['blocked'], true))) {\n waiting = true;\n continue;\n }\n if (\n outcome.status === 'done' &&\n (await alreadyReportedDone(reporting.notifications, {\n senderSessionId: sender.sessionId,\n recipientThreadId: recipient.threadId,\n }))\n ) {\n continue;\n }\n if (\n outcome.status === 'failed' &&\n (await repeatsLastFailure(reporting.notifications, {\n senderSessionId: sender.sessionId,\n recipientThreadId: recipient.threadId,\n summary: message,\n }))\n ) {\n continue;\n }\n deliveries.push(await reporting.deliver(recipient, notification));\n }\n return { waiting, deliveries };\n}\n\nexport interface FactoryRunEndInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n role: string;\n sender: FactoryReportSender;\n since: Date;\n outcome: FactoryRunOutcome;\n}\n\nexport type FactoryRunEndReporter = (input: FactoryRunEndInput) => Promise<FactoryRunEndResult>;\n\n/** The backstop for a work-item run: reports to the item's subscribers. Failures are logged, never thrown. */\nexport function createWorkItemRunEndReporter({\n workItems,\n reporting,\n}: {\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers' | 'getForProject'>;\n /** Resolved per report: the controller exists only once the mount is up. */\n reporting: () => Promise<FactoryReporting | null>;\n}): FactoryRunEndReporter {\n return async input => {\n try {\n const resolved = await reporting();\n if (!resolved) return { waiting: false };\n const recipients = await workItemReportRecipients(workItems, {\n orgId: input.orgId,\n workItemId: input.workItemId,\n senderSessionId: input.sender.sessionId,\n });\n if (recipients.length === 0) return { waiting: false };\n const { label, attributes } = await workItemReportAttributes(workItems, input);\n return await sendRunEndReport(resolved, {\n sender: input.sender,\n recipients,\n since: input.since,\n outcome: input.outcome,\n subject: `The ${input.role} session for ${label}`,\n attributes,\n });\n } catch (error) {\n console.warn('[Factory reports] Unable to send the run-end report.', error);\n return { waiting: false };\n }\n };\n}\n\nexport interface FactoryStageChange {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n fromStage: string | undefined;\n toStage: string;\n}\n\n/**\n * Tells a work item's subscribers it moved, at `low` priority. Only sessions\n * already live hear it: a stage change never brings a session online; an\n * offline subscriber sees the stage in its state signal on its next turn.\n */\nexport function createStageChangeNotifier({\n workItems,\n reporting,\n}: {\n workItems: Pick<WorkItemsStorage, 'listWorkItemSubscribers' | 'getForProject'>;\n reporting: () => Promise<FactoryReporting | null>;\n}): (change: FactoryStageChange) => Promise<void> {\n return async change => {\n const subscribers = await workItems.listWorkItemSubscribers({\n orgId: change.orgId,\n workItemId: change.workItemId,\n });\n if (subscribers.length === 0) return;\n const resolved = await reporting();\n if (!resolved) return;\n const { label, ref } = await workItemReference(workItems, change);\n const summary = change.fromStage\n ? `${label} moved from ${change.fromStage} to ${change.toStage}.`\n : `${label} moved to ${change.toStage}.`;\n const notification: Omit<SendNotificationSignalInput, 'source'> = {\n kind: 'stage',\n priority: 'low',\n summary,\n payload: { message: summary },\n sourceId: change.workItemId,\n coalesceKey: `factory-stage:${change.workItemId}`,\n attributes: {\n 'work-item': ref,\n ...(change.fromStage ? { 'from-stage': change.fromStage } : {}),\n 'to-stage': change.toStage,\n },\n };\n for (const subscriber of subscribers) {\n try {\n await resolved.deliver({ sessionId: subscriber.sessionId, threadId: subscriber.threadId }, notification, {\n onlyIfLive: true,\n });\n } catch (error) {\n console.warn(`[Factory reports] Unable to tell ${subscriber.sessionId} that ${label} moved.`, error);\n }\n }\n };\n}\n\n/** Reports one run end of a session; the thread is the one the run used. */\nexport type FactorySessionRunEndReporter = (input: {\n sessionId: string;\n threadId: string | null;\n outcome: FactoryRunOutcome;\n since: Date;\n}) => Promise<void>;\n\n/** A tool call a session's run is holding until someone approves or declines it. */\nexport interface FactoryApprovalRequest {\n sessionId: string;\n threadId: string | null;\n toolName: string;\n toolCallId: string;\n args: unknown;\n}\n\nexport type FactoryApprovalNotifier = (request: FactoryApprovalRequest) => Promise<void>;\n\ninterface SessionReportDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<\n WorkItemsStorage,\n 'findActiveRunBindingForSession' | 'findRunBindingBySession' | 'listWorkItemSubscribers' | 'getForProject'\n >;\n reporting: () => Promise<FactoryReporting | null>;\n}\n\n/** How much of a gated call's arguments an approval notice quotes. */\nconst APPROVAL_ARGS_LENGTH = 3000;\n\n/**\n * Who hears from a session: a work session (one with an active run binding)\n * reports to its item's followers, a side session to the session that started\n * it. Any other session (a person's own) has nobody to tell.\n */\nasync function sessionAudience(\n { sessions, workItems }: Omit<SessionReportDependencies, 'reporting'>,\n sessionId: string,\n threadId: string | null,\n /** When the run started: a binding revoked since then (the run moved its card to a finished stage) still counts. */\n since?: Date,\n): Promise<\n | {\n kind: 'work-item';\n binding: NonNullable<Awaited<ReturnType<WorkItemsStorage['findActiveRunBindingForSession']>>>;\n sender: FactoryReportSender;\n }\n | {\n kind: 'origin';\n sender: FactoryReportSender;\n recipient: FactoryDeliveryTarget;\n subject: string;\n /** Set while the origin has muted this side session's reports (approval notices still go through). */\n mutedAt: Date | null;\n }\n | null\n> {\n const row = await sessions.getBySessionId(sessionId);\n if (!row?.factoryProjectId) return null;\n let binding = await workItems.findActiveRunBindingForSession({\n orgId: row.orgId,\n factoryProjectId: row.factoryProjectId,\n sessionId: row.sessionId,\n });\n if (!binding && since && threadId) {\n // The binding records the memory resource the run used: the Factory's, its\n // own id for a run kept on its own resource, or its owner's for a person's\n // session the run started in.\n for (const resourceId of new Set([row.factoryProjectId, row.sessionId, row.userId])) {\n const latest = await workItems.findRunBindingBySession({\n factoryProjectId: row.factoryProjectId,\n threadId,\n resourceId,\n sessionId: row.sessionId,\n });\n if (latest?.orgId === row.orgId && latest.revokedAt && latest.revokedAt >= since) {\n binding = latest;\n break;\n }\n }\n }\n if (binding) {\n return {\n kind: 'work-item',\n binding,\n sender: { sessionId: row.sessionId, threadId: binding.threadId, userId: row.userId },\n };\n }\n if (!row.originSessionId || !row.originThreadId || !threadId) return null;\n return {\n kind: 'origin',\n sender: { sessionId: row.sessionId, threadId, userId: row.userId },\n recipient: { sessionId: row.originSessionId, threadId: row.originThreadId },\n subject: `The side session \"${row.title ?? row.sessionId}\"`,\n mutedAt: row.originMutedAt ?? null,\n };\n}\n\n/** Reports each run end of a work or side session to whoever follows it. */\nexport function createSessionRunEndReporter(deps: SessionReportDependencies): FactorySessionRunEndReporter {\n const workItemRunEnd = createWorkItemRunEndReporter(deps);\n return async ({ sessionId, threadId, outcome, since }) => {\n const audience = await sessionAudience(deps, sessionId, threadId, since);\n if (!audience) return;\n if (audience.kind === 'work-item') {\n const { binding, sender } = audience;\n await workItemRunEnd({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n role: binding.role,\n sender,\n since,\n outcome,\n });\n return;\n }\n // A muted origin still hears when the session is blocked: nothing else would tell it the session is waiting.\n if (audience.mutedAt && outcome.status !== 'blocked') return;\n const resolved = await deps.reporting();\n if (!resolved) return;\n await sendRunEndReport(resolved, {\n sender: audience.sender,\n recipients: [audience.recipient],\n since,\n outcome,\n subject: audience.subject,\n });\n };\n}\n\n/**\n * Tells whoever follows a session, urgently, the moment its run holds a tool\n * call for approval. The run stays open on the gate, so no run end would say\n * so; the notice names the session and call so a follower can answer it with\n * `factory_answer_approval`.\n */\nexport function createSessionApprovalNotifier(deps: SessionReportDependencies): FactoryApprovalNotifier {\n return async ({ sessionId, threadId, toolName, toolCallId, args }) => {\n const audience = await sessionAudience(deps, sessionId, threadId);\n if (!audience) return;\n const resolved = await deps.reporting();\n if (!resolved) return;\n let subject: string;\n let recipients: FactoryDeliveryTarget[];\n let attributes: Record<string, string> = {};\n if (audience.kind === 'work-item') {\n const { binding } = audience;\n recipients = await workItemReportRecipients(deps.workItems, {\n orgId: binding.orgId,\n workItemId: binding.workItemId,\n senderSessionId: audience.sender.sessionId,\n });\n const named = await workItemReportAttributes(deps.workItems, binding);\n subject = `The ${binding.role} session for ${named.label}`;\n attributes = named.attributes;\n } else {\n recipients = [audience.recipient];\n subject = audience.subject;\n }\n if (recipients.length === 0) return;\n const argsText = JSON.stringify(args ?? {}) ?? '{}';\n const quoted = argsText.length > APPROVAL_ARGS_LENGTH ? `${argsText.slice(0, APPROVAL_ARGS_LENGTH)}…` : argsText;\n const message = [\n `${subject} is waiting for approval to run ${toolName} with ${quoted}.`,\n `Answer it with factory_answer_approval (sessionId \"${audience.sender.sessionId}\", toolCallId \"${toolCallId}\"), or open that session to approve or decline it there.`,\n ].join(' ');\n const notification = {\n ...reportNotification({\n sender: audience.sender,\n message,\n attributes: { ...attributes, 'waiting-for': 'approval', 'tool-name': toolName, 'tool-call-id': toolCallId },\n }),\n kind: 'approval',\n priority: 'urgent' as const,\n // One notice per held call, even if the gate is announced again.\n dedupeKey: `factory-approval:${toolCallId}`,\n };\n for (const recipient of recipients) await resolved.deliver(recipient, notification);\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
package/dist/workspace.js CHANGED
@@ -379,6 +379,10 @@ function createWorkspaceFactory(options = {}) {
379
379
  if (!sandboxConfig) throw new Error("A sandbox is required to create a Factory session workspace");
380
380
  const factorySandbox = sandboxConfig;
381
381
  const githubProvider = sourceControl.github;
382
+ const gitIdentity = githubProvider?.gitIdentity ?? {
383
+ name: user.name,
384
+ email: user.email
385
+ };
382
386
  const storage = sourceControl.storage;
383
387
  const projectRepository = session.projectRepositoryId ? await storage.projectRepositories.get({
384
388
  orgId: session.orgId,
@@ -754,6 +758,7 @@ function createWorkspaceFactory(options = {}) {
754
758
  storage: storage.sessions
755
759
  });
756
760
  await checkoutSessionBranch(target, workdir, {
761
+ identity: gitIdentity,
757
762
  branch: session.branch,
758
763
  baseBranch: session.baseBranch || projectRepository.branch || repository.defaultBranch,
759
764
  token,
@@ -872,6 +877,7 @@ function createWorkspaceFactory(options = {}) {
872
877
  storage: { markMaterialized: async () => {} }
873
878
  });
874
879
  state.branch = (await timedPhase(`workspace.sync(${repo.slug})`, () => syncEnvironmentRepository(target, entry.dir, {
880
+ identity: gitIdentity,
875
881
  branch: session.branch,
876
882
  defaultBranch: repo.defaultBranch,
877
883
  token,